Skip to content

Commit

Permalink
Saya New Inputs (#1757)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update crates/saya/core/src/prover/program_input.rs

Co-authored-by: glihm <[email protected]>

* Update crates/saya/core/src/prover/program_input.rs

Co-authored-by: glihm <[email protected]>

* Update crates/saya/core/src/prover/program_input.rs

Co-authored-by: glihm <[email protected]>

* unused import

* extracted recursive messages to function

---------

Co-authored-by: Mateusz Zając <[email protected]>
Co-authored-by: Mateusz Zając <[email protected]>
Co-authored-by: glihm <[email protected]>
  • Loading branch information
4 people authored Apr 9, 2024
1 parent 1fe14ea commit 5164e58
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 10 deletions.
8 changes: 8 additions & 0 deletions crates/saya/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 41 additions & 6 deletions crates/saya/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ 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;

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;
Expand Down Expand Up @@ -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?;
Expand All @@ -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::<Vec<_>>();

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.");
Expand Down
2 changes: 2 additions & 0 deletions crates/saya/core/src/prover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
225 changes: 225 additions & 0 deletions crates/saya/core/src/prover/program_input.rs
Original file line number Diff line number Diff line change
@@ -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<MessageToStarknet>,
pub message_to_appchain_segment: Vec<MessageToAppchain>,
pub state_updates: StateUpdates,
}

fn get_messages_recursively(info: &CallInfo) -> Vec<MessageToStarknet> {
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<TxExecInfo>,
mut transactions: Vec<&L1HandlerTx>,
) -> (Vec<MessageToStarknet>, Vec<MessageToAppchain>) {
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<String> {
let message_to_starknet = self
.message_to_starknet_segment
.iter()
.map(MessageToStarknet::serialize)
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
.flatten()
.map(|e| format!("{}", e))
.collect::<Vec<_>>()
.join(",");

let message_to_appchain = self
.message_to_appchain_segment
.iter()
.map(|m| m.serialize())
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
.flatten()
.map(|e| format!("{}", e))
.collect::<Vec<_>>()
.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<FieldElement>,
}

impl MessageToStarknet {
pub fn serialize(&self) -> anyhow::Result<Vec<FieldElement>> {
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<FieldElement>,
}

impl MessageToAppchain {
pub fn serialize(&self) -> anyhow::Result<Vec<FieldElement>> {
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::<String>();

println!("{}", expected);

assert_eq!(serialized, expected);

Ok(())
}
50 changes: 49 additions & 1 deletion crates/saya/core/src/prover/state_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.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::<Vec<_>>().join(",");

format!(r#""{}":{{{}}}"#, k.0, storage)
})
.collect::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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('{');
Expand Down
Loading

0 comments on commit 5164e58

Please sign in to comment.