From 5164e588c74fc3b9132d100752f6cb30be00e0a3 Mon Sep 17 00:00:00 2001 From: Neo <128649481+neotheprogramist@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:30:11 +0200 Subject: [PATCH] Saya New Inputs (#1757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * new inputs and serialization * L2 -> L1 messages * L1 -> L2 messages * leftover * format * saya to new input * extracting nonce from transaction * typo for rust fmt * Update crates/saya/core/src/prover/program_input.rs Co-authored-by: glihm * Update crates/saya/core/src/prover/program_input.rs Co-authored-by: glihm * Update crates/saya/core/src/prover/program_input.rs Co-authored-by: glihm * Update crates/saya/core/src/prover/program_input.rs Co-authored-by: glihm * unused import * extracted recursive messages to function --------- Co-authored-by: Mateusz Zając Co-authored-by: Mateusz Zając <60236390+matzayonc@users.noreply.github.com> Co-authored-by: glihm --- crates/saya/README.md | 8 + crates/saya/core/src/lib.rs | 47 +++- crates/saya/core/src/prover/mod.rs | 2 + crates/saya/core/src/prover/program_input.rs | 225 +++++++++++++++++++ crates/saya/core/src/prover/state_diff.rs | 50 ++++- crates/saya/core/src/prover/stone_image.rs | 7 +- 6 files changed, 329 insertions(+), 10 deletions(-) create mode 100644 crates/saya/core/src/prover/program_input.rs diff --git a/crates/saya/README.md b/crates/saya/README.md index 112cc89d8a..5c0542d503 100644 --- a/crates/saya/README.md +++ b/crates/saya/README.md @@ -67,6 +67,14 @@ However, papyrus and blockifier which we depend on are still in `-dev` version, * cairo-lang (we should support `2.5` now) * scarb (breaking changes between 2.4 and 2.5 to be addresses, not required to only build saya and SNOS) +## Local Testing + +```bash +cargo run -r -p katana # Start an appchain +cargo run -r -p sozo -- build --manifest-path examples/spawn-and-move/Scarb.toml +cargo run -r -p sozo -- migrate --manifest-path examples/spawn-and-move/Scarb.toml # Make some transactions +cargo run -r --bin saya -- --rpc-url http://localhost:5050 # Run Saya +``` ## Additional documentation [Hackmd note](https://hackmd.io/@glihm/saya) diff --git a/crates/saya/core/src/lib.rs b/crates/saya/core/src/lib.rs index b36261df73..bf7d14bf2a 100644 --- a/crates/saya/core/src/lib.rs +++ b/crates/saya/core/src/lib.rs @@ -4,11 +4,13 @@ use std::sync::Arc; use futures::future::join; use katana_primitives::block::{BlockNumber, FinalityStatus, SealedBlock, SealedBlockWithStatus}; +use katana_primitives::transaction::Tx; use katana_primitives::FieldElement; use prover::ProverIdentifier; use saya_provider::rpc::JsonRpcProvider; use saya_provider::Provider as SayaProvider; use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; use tracing::{error, info, trace}; use url::Url; use verifier::VerifierIdentifier; @@ -16,7 +18,7 @@ use verifier::VerifierIdentifier; use crate::blockchain::Blockchain; use crate::data_availability::{DataAvailabilityClient, DataAvailabilityConfig}; use crate::error::SayaResult; -use crate::prover::state_diff::ProvedStateDiff; +use crate::prover::{extract_messages, ProgramInput}; pub mod blockchain; pub mod data_availability; @@ -145,7 +147,7 @@ impl Saya { ) -> SayaResult<()> { trace!(target: LOG_TARGET, block_number = %block_number, "Processing block."); - let (block, prev_block, genesis_state_hash) = blocks; + let (block, prev_block, _genesis_state_hash) = blocks; let (state_updates, da_state_update) = self.provider.fetch_state_updates(block_number).await?; @@ -171,16 +173,49 @@ impl Saya { return Ok(()); } - let to_prove = ProvedStateDiff { - genesis_state_hash, - prev_state_hash: prev_block.header.header.state_root, + let transactions = block + .block + .body + .iter() + .filter_map(|t| match &t.transaction { + Tx::L1Handler(tx) => Some(tx), + _ => None, + }) + .collect::>(); + + let (message_to_starknet_segment, message_to_appchain_segment) = + extract_messages(&exec_infos, transactions); + + let new_program_input = ProgramInput { + prev_state_root: prev_block.header.header.state_root, + block_number: FieldElement::from(block_number), + block_hash: block.block.header.hash, + config_hash: FieldElement::from(0u64), + message_to_starknet_segment, + message_to_appchain_segment, state_updates: state_updates_to_prove, }; + println!("Program input: {}", new_program_input.serialize()?); + + // let to_prove = ProvedStateDiff { + // genesis_state_hash, + // prev_state_hash: prev_block.header.header.state_root, + // state_updates: state_updates_to_prove, + // }; + trace!(target: "saya_core", "Proving block {block_number}."); - let proof = prover::prove(to_prove.serialize(), self.config.prover).await?; + let proof = prover::prove(new_program_input.serialize()?, self.config.prover).await?; info!(target: "saya_core", block_number, "Block proven."); + // save proof to file + tokio::fs::File::create(format!("proof_{}.json", block_number)) + .await + .unwrap() + .write_all(proof.as_bytes()) + .await + .unwrap(); + trace!(target: "saya_core", "Verifying block {block_number}."); let transaction_hash = verifier::verify(proof, self.config.verifier).await?; info!(target: "saya_core", block_number, transaction_hash, "Block verified."); diff --git a/crates/saya/core/src/prover/mod.rs b/crates/saya/core/src/prover/mod.rs index 1dee9a1353..3214a7f4ae 100644 --- a/crates/saya/core/src/prover/mod.rs +++ b/crates/saya/core/src/prover/mod.rs @@ -6,11 +6,13 @@ use std::str::FromStr; use anyhow::bail; use async_trait::async_trait; +mod program_input; mod serializer; pub mod state_diff; mod stone_image; mod vec252; +pub use program_input::*; use serde::{Deserialize, Serialize}; pub use serializer::parse_proof; pub use stone_image::*; diff --git a/crates/saya/core/src/prover/program_input.rs b/crates/saya/core/src/prover/program_input.rs new file mode 100644 index 0000000000..d39a7e7b64 --- /dev/null +++ b/crates/saya/core/src/prover/program_input.rs @@ -0,0 +1,225 @@ +use katana_primitives::contract::ContractAddress; +use katana_primitives::state::StateUpdates; +use katana_primitives::trace::{CallInfo, EntryPointType, TxExecInfo}; +use katana_primitives::transaction::L1HandlerTx; +use katana_primitives::utils::transaction::compute_l1_message_hash; +use starknet::core::types::FieldElement; + +use super::state_diff::state_updates_to_json_like; + +/// Based on https://github.com/cartridge-gg/piltover/blob/2be9d46f00c9c71e2217ab74341f77b09f034c81/src/snos_output.cairo#L19-L20 +/// With the new state root computed by the prover. +pub struct ProgramInput { + pub prev_state_root: FieldElement, + pub block_number: FieldElement, + pub block_hash: FieldElement, + pub config_hash: FieldElement, + pub message_to_starknet_segment: Vec, + pub message_to_appchain_segment: Vec, + pub state_updates: StateUpdates, +} + +fn get_messages_recursively(info: &CallInfo) -> Vec { + let mut messages = vec![]; + + // By default, `from_address` must correspond to the contract address that + // is sending the message. In the case of library calls, `code_address` is `None`, + // we then use the `caller_address` instead (which can also be an account). + let from_address = + if let Some(code_address) = info.code_address { code_address } else { info.caller_address }; + + messages.extend(info.l2_to_l1_messages.iter().map(|m| MessageToStarknet { + from_address, + to_address: ContractAddress::from(m.to_address), + payload: m.payload.clone(), + })); + + info.inner_calls.iter().for_each(|call| { + messages.extend(get_messages_recursively(call)); + }); + + messages +} + +pub fn extract_messages( + exec_infos: &Vec, + mut transactions: Vec<&L1HandlerTx>, +) -> (Vec, Vec) { + let message_to_starknet_segment = exec_infos + .iter() + .map(|t| t.execute_call_info.iter().chain(t.validate_call_info.iter()).chain(t.fee_transfer_call_info.iter())) // Take into account both validate and execute calls. + .flatten() + .map(get_messages_recursively) + .flatten() + .collect(); + + let message_to_appchain_segment = exec_infos + .iter() + .map(|t| t.execute_call_info.iter()) + .flatten() + .filter(|c| c.entry_point_type == EntryPointType::L1Handler) + .map(|c| { + let message_hash = + compute_l1_message_hash(*c.caller_address, *c.contract_address, &c.calldata[..]); + + // Matching execution to a transaction to extract nonce. + let matching = transactions + .iter() + .enumerate() + .find(|(_, &t)| { + t.message_hash == message_hash + && c.contract_address == t.contract_address + && c.calldata == t.calldata + }) + .expect(&format!( + "No matching transaction found for message hash: {}", + message_hash + )) + .0; + + // Removing, to have different nonces, even for the same message content. + let removed = transactions.remove(matching); + + (c, removed) + }) + .map(|(c, t)| MessageToAppchain { + from_address: c.caller_address, + to_address: c.contract_address, + nonce: t.nonce, + selector: c.entry_point_selector, + payload: c.calldata.clone(), + }) + .collect(); + + (message_to_starknet_segment, message_to_appchain_segment) +} + +impl ProgramInput { + pub fn serialize(&self) -> anyhow::Result { + let message_to_starknet = self + .message_to_starknet_segment + .iter() + .map(MessageToStarknet::serialize) + .collect::>>()? + .into_iter() + .flatten() + .map(|e| format!("{}", e)) + .collect::>() + .join(","); + + let message_to_appchain = self + .message_to_appchain_segment + .iter() + .map(|m| m.serialize()) + .collect::>>()? + .into_iter() + .flatten() + .map(|e| format!("{}", e)) + .collect::>() + .join(","); + + let mut result = String::from('{'); + result.push_str(&format!(r#""prev_state_root":{},"#, self.prev_state_root)); + result.push_str(&format!(r#""block_number":{},"#, self.block_number)); + result.push_str(&format!(r#""block_hash":{},"#, self.block_hash)); + result.push_str(&format!(r#""config_hash":{},"#, self.config_hash)); + + result.push_str(&format!(r#""message_to_starknet_segment":[{}],"#, message_to_starknet)); + result.push_str(&format!(r#""message_to_appchain_segment":[{}],"#, message_to_appchain)); + + result.push_str(&state_updates_to_json_like(&self.state_updates)); + + result.push_str(&format!("{}", "}")); + + Ok(result) + } +} + +/// Based on https://github.com/cartridge-gg/piltover/blob/2be9d46f00c9c71e2217ab74341f77b09f034c81/src/messaging/output_process.cairo#L16 +pub struct MessageToStarknet { + pub from_address: ContractAddress, + pub to_address: ContractAddress, + pub payload: Vec, +} + +impl MessageToStarknet { + pub fn serialize(&self) -> anyhow::Result> { + let mut result = vec![*self.from_address, *self.to_address]; + result.push(FieldElement::try_from(self.payload.len())?); + result.extend(self.payload.iter().cloned()); + Ok(result) + } +} + +/// Based on https://github.com/cartridge-gg/piltover/blob/2be9d46f00c9c71e2217ab74341f77b09f034c81/src/messaging/output_process.cairo#L28 +pub struct MessageToAppchain { + pub from_address: ContractAddress, + pub to_address: ContractAddress, + pub nonce: FieldElement, + pub selector: FieldElement, + pub payload: Vec, +} + +impl MessageToAppchain { + pub fn serialize(&self) -> anyhow::Result> { + let mut result = vec![*self.from_address, *self.to_address, self.nonce, self.selector]; + result.push(FieldElement::try_from(self.payload.len())?); + result.extend(self.payload.iter().cloned()); + Ok(result) + } +} + +#[test] +fn test_program_input() -> anyhow::Result<()> { + use std::str::FromStr; + + let input = ProgramInput { + prev_state_root: FieldElement::from_str("101")?, + block_number: FieldElement::from_str("102")?, + block_hash: FieldElement::from_str("103")?, + config_hash: FieldElement::from_str("104")?, + message_to_starknet_segment: vec![MessageToStarknet { + from_address: ContractAddress::from(FieldElement::from_str("105")?), + to_address: ContractAddress::from(FieldElement::from_str("106")?), + payload: vec![FieldElement::from_str("107")?], + }], + message_to_appchain_segment: vec![MessageToAppchain { + from_address: ContractAddress::from(FieldElement::from_str("108")?), + to_address: ContractAddress::from(FieldElement::from_str("109")?), + nonce: FieldElement::from_str("110")?, + selector: FieldElement::from_str("111")?, + payload: vec![FieldElement::from_str("112")?], + }], + state_updates: StateUpdates { + nonce_updates: std::collections::HashMap::new(), + storage_updates: std::collections::HashMap::new(), + contract_updates: std::collections::HashMap::new(), + declared_classes: std::collections::HashMap::new(), + }, + }; + + let serialized = input.serialize().unwrap(); + + println!("Serialized: {}", serialized); + + pub const EXPECTED: &str = r#"{ + "prev_state_root": 101, + "block_number": 102, + "block_hash": 103, + "config_hash": 104, + "message_to_starknet_segment": [105,106,1,107], + "message_to_appchain_segment": [108,109,110,111,1,112], + "nonce_updates": {}, + "storage_updates": {}, + "contract_updates": {}, + "declared_classes": {} + }"#; + + let expected = EXPECTED.chars().filter(|c| !c.is_whitespace()).collect::(); + + println!("{}", expected); + + assert_eq!(serialized, expected); + + Ok(()) +} diff --git a/crates/saya/core/src/prover/state_diff.rs b/crates/saya/core/src/prover/state_diff.rs index d87c9090da..ad0572a322 100644 --- a/crates/saya/core/src/prover/state_diff.rs +++ b/crates/saya/core/src/prover/state_diff.rs @@ -54,7 +54,55 @@ pub const EXAMPLE_KATANA_DIFF: &str = r#"{ } }"#; -/// We need custom implentation because of dynamic keys in json +pub fn state_updates_to_json_like(state_updates: &StateUpdates) -> String { + let mut result = String::new(); + + result.push_str(&format!(r#""nonce_updates":{}"#, "{")); + let nonce_updates = state_updates + .nonce_updates + .iter() + .map(|(k, v)| format!(r#""{}":{}"#, k.0, v)) + .collect::>() + .join(","); + result.push_str(&format!("{}{}", nonce_updates, "}")); + + result.push_str(&format!(r#","storage_updates":{}"#, "{")); + let storage_updates = state_updates + .storage_updates + .iter() + .map(|(k, v)| { + let storage = + v.iter().map(|(k, v)| format!(r#""{}":{}"#, k, v)).collect::>().join(","); + + format!(r#""{}":{{{}}}"#, k.0, storage) + }) + .collect::>() + .join(","); + result.push_str(&format!("{}{}", storage_updates, "}")); + + result.push_str(&format!(r#","contract_updates":{}"#, "{")); + let contract_updates = state_updates + .contract_updates + .iter() + .map(|(k, v)| format!(r#""{}":{}"#, k.0, v)) + .collect::>() + .join(","); + result.push_str(&format!("{}{}", contract_updates, "}")); + + result.push_str(&format!(r#","declared_classes":{}"#, "{")); + let declared_classes = state_updates + .declared_classes + .iter() + .map(|(k, v)| format!(r#""{}":{}"#, k, v)) + .collect::>() + .join(","); + + result.push_str(&format!("{}{}", declared_classes, "}")); + + result +} + +/// We need custom implementation because of dynamic keys in json impl ProvedStateDiff { pub fn serialize(&self) -> String { let mut result = String::from('{'); diff --git a/crates/saya/core/src/prover/stone_image.rs b/crates/saya/core/src/prover/stone_image.rs index 714a12f733..c5ad5de5d9 100644 --- a/crates/saya/core/src/prover/stone_image.rs +++ b/crates/saya/core/src/prover/stone_image.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; use tokio::sync::OnceCell; +use tracing::warn; use super::{ProverClient, ProverIdentifier}; @@ -50,8 +51,8 @@ impl StoneProver { static STONE_PROVER: OnceCell<(anyhow::Result, anyhow::Result)> = OnceCell::const_new(); - let source = "neotheprogramist/state-diff-commitment"; - let verifier = "neotheprogramist/verifier:latest"; + let source = "piniom/state-diff-commitment"; + let verifier = "piniom/verifier:latest"; let result = STONE_PROVER .get_or_init(|| async { @@ -71,7 +72,7 @@ impl StoneProver { if result.0.is_err() { bail!("Failed to pull prover"); } else if result.1.is_err() { - bail!("Failed to pull verifier"); + warn!("Failed to pull verifier"); } Ok(StoneProver(source.to_string()))