diff --git a/Cargo.lock b/Cargo.lock index d58125d68e..d1b34f9938 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12400,6 +12400,7 @@ dependencies = [ "katana-rpc-api", "saya-core", "serde_json", + "starknet", "starknet-crypto 0.6.2", "tokio", "tracing", @@ -12431,6 +12432,7 @@ dependencies = [ "lazy_static", "num-bigint", "num-traits 0.2.19", + "once_cell", "parking_lot 0.12.3", "prover-sdk", "rand", diff --git a/bin/saya/Cargo.toml b/bin/saya/Cargo.toml index 0cb85c01e7..2e2816c1c0 100644 --- a/bin/saya/Cargo.toml +++ b/bin/saya/Cargo.toml @@ -14,6 +14,7 @@ katana-primitives.workspace = true katana-rpc-api.workspace = true saya-core.workspace = true serde_json.workspace = true +starknet.workspace = true tokio.workspace = true tracing-subscriber.workspace = true tracing.workspace = true diff --git a/bin/saya/README.md b/bin/saya/README.md index 913b6ad039..781b438109 100644 --- a/bin/saya/README.md +++ b/bin/saya/README.md @@ -58,6 +58,7 @@ cargo run -r --bin sozo -- \ --fee-estimate-multiplier 20 \ --name ``` + Once the migration is done, please take note of the address of the world as it will be re-used in the commands below. 1. Set world configs @@ -133,6 +134,7 @@ cargo run -r --bin sozo -- model get Position \ --rpc-url \ --world ``` + ```json // Expected on Sepolia as we've executed the transaction on the Katana shard. { @@ -163,8 +165,7 @@ If not (this includes Apple Silicon), some emulation will take place to run the It's important that the `--start-block` of Saya is the first block produced by Katana as for now Katana is not fetching events from the forked network. -**IMPORTANT NOTE:** -For now, please add your account address and account private key in `saya/core/src/dojo_os/mod.rs` as those parameters are still not exposed currently. As you are using `cargo run`, it will rebuild with your account configuration before running `saya`. +Starknet sepolia network chain id is `0x00000000000000000000000000000000000000000000534e5f5345504f4c4941`. ```bash cargo run -r --bin saya -- \ @@ -173,7 +174,11 @@ cargo run -r --bin saya -- \ --world \ --url \ --private-key \ - --start-block + --start-block \ + --starknet-url \ + --chain-id \ + --signer-address \ + --signer-key \ ``` After this command, Saya will pick up the blocks with transactions, generate the proof for the state transition, and send it to the base layer world contract. diff --git a/bin/saya/src/args/mod.rs b/bin/saya/src/args/mod.rs index f0a257d6e4..b16fce3cdc 100644 --- a/bin/saya/src/args/mod.rs +++ b/bin/saya/src/args/mod.rs @@ -6,7 +6,9 @@ use std::path::PathBuf; use clap::Parser; use saya_core::data_availability::celestia::CelestiaConfig; use saya_core::data_availability::DataAvailabilityConfig; -use saya_core::{ProverAccessKey, SayaConfig}; +use saya_core::{ProverAccessKey, SayaConfig, StarknetAccountData}; +use starknet::core::utils::cairo_short_string_to_felt; +use starknet_account::StarknetAccountOptions; use tracing::Subscriber; use tracing_subscriber::{fmt, EnvFilter}; use url::Url; @@ -16,6 +18,7 @@ use crate::args::proof::ProofOptions; mod data_availability; mod proof; +mod starknet_account; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -28,18 +31,6 @@ pub struct SayaArgs { #[arg(default_value = "http://localhost:5050")] pub rpc_url: Url, - /// Specify the Prover URL. - #[arg(long)] - #[arg(value_name = "PROVER URL")] - #[arg(help = "The Prover URL for remote proving.")] - pub url: Url, - - /// Specify the Prover Key. - #[arg(long)] - #[arg(value_name = "PROVER KEY")] - #[arg(help = "An authorized prover key for remote proving.")] - pub private_key: String, - #[arg(long)] #[arg(value_name = "STORE PROOFS")] #[arg(help = "When enabled all proofs are saved as a file.")] @@ -73,6 +64,10 @@ pub struct SayaArgs { #[command(flatten)] #[command(next_help_heading = "Choose the proof pipeline configuration")] pub proof: ProofOptions, + + #[command(flatten)] + #[command(next_help_heading = "Starknet account configuration for settlement")] + pub starknet_account: StarknetAccountOptions, } impl SayaArgs { @@ -135,14 +130,22 @@ impl TryFrom for SayaConfig { None => None, }; - let prover_key = ProverAccessKey::from_hex_string(&args.private_key).map_err(|e| { - Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string())) - })?; + let starknet_account = StarknetAccountData { + starknet_url: args.starknet_account.starknet_url, + chain_id: cairo_short_string_to_felt(&args.starknet_account.chain_id)?, + signer_address: args.starknet_account.signer_address, + signer_key: args.starknet_account.signer_key, + }; + + let prover_key = + ProverAccessKey::from_hex_string(&args.proof.private_key).map_err(|e| { + Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string())) + })?; Ok(SayaConfig { katana_rpc: args.rpc_url, - url: args.url, - private_key: prover_key, + prover_url: args.proof.prover_url, + prover_key, store_proofs: args.store_proofs, start_block: args.start_block, batch_size: args.batch_size, @@ -150,6 +153,7 @@ impl TryFrom for SayaConfig { world_address: args.proof.world_address, fact_registry_address: args.proof.fact_registry_address, skip_publishing_proof, + starknet_account, }) } } @@ -157,6 +161,8 @@ impl TryFrom for SayaConfig { #[cfg(test)] mod tests { + use katana_primitives::FieldElement; + use super::*; use crate::args::data_availability::CelestiaOptions; @@ -171,9 +177,6 @@ mod tests { let args = SayaArgs { config_file: Some(config_file_path.clone()), rpc_url: Url::parse("http://localhost:5050").unwrap(), - url: Url::parse("http://localhost:5050").unwrap(), - private_key: "0xd0fa91f4949e9a777ebec071ca3ca6acc1f5cd6c6827f123b798f94e73425027" - .into(), store_proofs: true, json_log: false, start_block: 0, @@ -190,16 +193,24 @@ mod tests { proof: ProofOptions { world_address: Default::default(), fact_registry_address: Default::default(), + prover_url: Url::parse("http://localhost:5050").unwrap(), + private_key: Default::default(), + }, + starknet_account: StarknetAccountOptions { + starknet_url: Url::parse("http://localhost:5030").unwrap(), + chain_id: "SN_SEPOLIA".to_string(), + signer_address: Default::default(), + signer_key: Default::default(), }, }; let config: SayaConfig = args.try_into().unwrap(); assert_eq!(config.katana_rpc.as_str(), "http://localhost:5050/"); - assert_eq!(config.url.as_str(), "http://localhost:1234/"); + assert_eq!(config.prover_url.as_str(), "http://localhost:1234/"); assert_eq!(config.batch_size, 4); assert_eq!( - config.private_key.signing_key_as_hex_string(), + config.prover_key.signing_key_as_hex_string(), "0xd0fa91f4949e9a777ebec071ca3ca6acc1f5cd6c6827f123b798f94e73425027" ); assert!(!config.store_proofs); @@ -212,5 +223,20 @@ mod tests { } else { panic!("Expected Celestia config"); } + + let expected = StarknetAccountData { + starknet_url: Url::parse("http://localhost:5030").unwrap(), + chain_id: FieldElement::from_hex_be("0x534e5f5345504f4c4941").unwrap(), + signer_address: FieldElement::from_hex_be( + "0x3aa0a12c62a46a200b1a1211e8cd09b520164104e76d79648ca459cf05db94", + ) + .unwrap(), + signer_key: FieldElement::from_hex_be( + "0x6b41bfa82e791a8b4e6b3ee058cb25b89714e4a23bd9a1ad6e6ba0bbc0b145b", + ) + .unwrap(), + }; + + assert_eq!(config.starknet_account, expected); } } diff --git a/bin/saya/src/args/proof.rs b/bin/saya/src/args/proof.rs index 12c3cfed3a..23375d1dc2 100644 --- a/bin/saya/src/args/proof.rs +++ b/bin/saya/src/args/proof.rs @@ -1,5 +1,6 @@ use clap::Args; use katana_primitives::FieldElement; +use url::Url; #[derive(Debug, Args, Clone)] pub struct ProofOptions { @@ -10,4 +11,14 @@ pub struct ProofOptions { #[arg(help = "The address of the Fact Registry contract.")] #[arg(long = "registry")] pub fact_registry_address: FieldElement, + + #[arg(long)] + #[arg(value_name = "PROVER URL")] + #[arg(help = "The Prover URL for remote proving.")] + pub prover_url: Url, + + #[arg(long)] + #[arg(value_name = "PROVER KEY")] + #[arg(help = "An authorized prover key for remote proving.")] + pub private_key: String, } diff --git a/bin/saya/src/args/starknet_account.rs b/bin/saya/src/args/starknet_account.rs new file mode 100644 index 0000000000..45d772ce08 --- /dev/null +++ b/bin/saya/src/args/starknet_account.rs @@ -0,0 +1,28 @@ +//! Data availability options. + +use clap::Args; +use katana_primitives::FieldElement; +use url::Url; + +#[derive(Debug, Args, Clone)] +pub struct StarknetAccountOptions { + #[arg(long)] + #[arg(env)] + #[arg(help = "The url of the starknet node.")] + pub starknet_url: Url, + + #[arg(long)] + #[arg(env)] + #[arg(help = "The chain id of the starknet node.")] + pub chain_id: String, + + #[arg(long)] + #[arg(env)] + #[arg(help = "The address of the starknet account.")] + pub signer_address: FieldElement, + + #[arg(long)] + #[arg(env)] + #[arg(help = "The private key of the starknet account.")] + pub signer_key: FieldElement, +} diff --git a/bin/saya/src/args/test_saya_config_file.json b/bin/saya/src/args/test_saya_config_file.json index ba73f97434..dc52810d8e 100644 --- a/bin/saya/src/args/test_saya_config_file.json +++ b/bin/saya/src/args/test_saya_config_file.json @@ -1,7 +1,7 @@ { "katana_rpc": "http://localhost:5050", - "url": "http://localhost:1234", - "private_key": "0xd0fa91f4949e9a777ebec071ca3ca6acc1f5cd6c6827f123b798f94e73425027", + "prover_url": "http://localhost:1234", + "prover_key": "0xd0fa91f4949e9a777ebec071ca3ca6acc1f5cd6c6827f123b798f94e73425027", "store_proofs": false, "batch_size": 4, "world_address": "0x332b8ff41b1b026991fa9b7f0ec352909f8bc33416b65a80527edc988a9b082", @@ -16,5 +16,11 @@ } }, "prover": "Stone", - "verifier": "StoneLocal" + "verifier": "StoneLocal", + "starknet_account": { + "starknet_url": "http://localhost:5030", + "chain_id": "SN_SEPOLIA", + "signer_address": "0x3aa0a12c62a46a200b1a1211e8cd09b520164104e76d79648ca459cf05db94", + "signer_key": "0x6b41bfa82e791a8b4e6b3ee058cb25b89714e4a23bd9a1ad6e6ba0bbc0b145b" + } } diff --git a/crates/saya/core/Cargo.toml b/crates/saya/core/Cargo.toml index b44ecbeabc..bb319cbc3a 100644 --- a/crates/saya/core/Cargo.toml +++ b/crates/saya/core/Cargo.toml @@ -38,7 +38,7 @@ thiserror.workspace = true tokio.workspace = true tracing.workspace = true url.workspace = true - +once_cell.workspace = true # TODO: use features for each possible DA. celestia-rpc = "0.2.0" celestia-types = "0.2.0" diff --git a/crates/saya/core/src/dojo_os/mod.rs b/crates/saya/core/src/dojo_os/mod.rs index bf479bc8d3..74d387539c 100644 --- a/crates/saya/core/src/dojo_os/mod.rs +++ b/crates/saya/core/src/dojo_os/mod.rs @@ -7,12 +7,14 @@ // pub mod input; // pub mod transaction; +use std::sync::Arc; use std::time::Duration; use anyhow::{bail, Context}; use dojo_world::migration::TxnConfig; use dojo_world::utils::TransactionExt; use itertools::chain; +use once_cell::sync::OnceCell; use starknet::accounts::{Account, Call, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; use starknet::core::types::{ BlockId, BlockTag, FieldElement, TransactionExecutionStatus, TransactionStatus, @@ -21,32 +23,36 @@ use starknet::core::utils::get_selector_from_name; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; use starknet::signers::{LocalWallet, SigningKey}; +use tokio::sync::Mutex; use tokio::time::sleep; -use url::Url; -// will need to be read from the environment for chains other than sepoia -pub const STARKNET_URL: &str = "https://free-rpc.nethermind.io/sepolia-juno/v0_7"; -pub const CHAIN_ID: &str = "0x00000000000000000000000000000000000000000000534e5f5345504f4c4941"; -pub const SIGNER_ADDRESS: &str = - "0x00ceE714eAF27390e630c62aa4b51319f9EdA813d6DDd12dA0ae8Ce00453cb4b"; -pub const SIGNER_KEY: &str = "0x01c49f9a0f5d2ca87fe7bb0530c611f91faf4adda6b7fcff479ce92ea13b1b4c"; -lazy_static::lazy_static!( - pub static ref STARKNET_ACCOUNT: SingleOwnerAccount, LocalWallet> = { - let provider = JsonRpcClient::new(HttpTransport::new( - Url::parse(STARKNET_URL).unwrap(), - )); +use crate::StarknetAccountData; - let signer = FieldElement::from_hex_be(SIGNER_KEY).expect("invalid signer hex"); - let signer = LocalWallet::from(SigningKey::from_secret_scalar(signer)); +type AccountType = SingleOwnerAccount, LocalWallet>; - let address = FieldElement::from_hex_be(SIGNER_ADDRESS).expect("invalid signer address"); - let chain_id = FieldElement::from_hex_be(CHAIN_ID).expect("invalid chain id"); +pub static STARKNET_ACCOUNT: OnceCell>> = OnceCell::new(); - let mut account = SingleOwnerAccount::new(provider, signer, address, chain_id, ExecutionEncoding::New); - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - account - }; -); +pub fn get_starknet_account( + config: StarknetAccountData, +) -> anyhow::Result>> { + Ok(STARKNET_ACCOUNT + .get_or_init(|| { + let provider = JsonRpcClient::new(HttpTransport::new(config.starknet_url)); + let signer = LocalWallet::from(SigningKey::from_secret_scalar(config.signer_key)); + + let mut account = SingleOwnerAccount::new( + provider, + signer, + config.signer_address, + config.chain_id, + ExecutionEncoding::New, + ); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + Arc::new(Mutex::new(account)) + }) + .clone()) +} pub async fn starknet_apply_diffs( world: FieldElement, @@ -54,6 +60,7 @@ pub async fn starknet_apply_diffs( program_output: Vec, program_hash: FieldElement, nonce: FieldElement, + starknet_account: StarknetAccountData, ) -> anyhow::Result { let calldata = chain![ vec![FieldElement::from(new_state.len() as u64 / 2)].into_iter(), @@ -63,8 +70,10 @@ pub async fn starknet_apply_diffs( ] .collect(); + let account = get_starknet_account(starknet_account)?; + let account = account.lock().await; let txn_config = TxnConfig { wait: true, receipt: true, ..Default::default() }; - let tx = STARKNET_ACCOUNT + let tx = account .execute(vec![Call { to: world, selector: get_selector_from_name("upgrade_state").expect("invalid selector"), @@ -82,14 +91,13 @@ pub async fn starknet_apply_diffs( bail!("Transaction not mined in {} seconds.", wait_for.as_secs()); } - let status = - match STARKNET_ACCOUNT.provider().get_transaction_status(tx.transaction_hash).await { - Ok(status) => status, - Err(_e) => { - sleep(Duration::from_secs(1)).await; - continue; - } - }; + let status = match account.provider().get_transaction_status(tx.transaction_hash).await { + Ok(status) => status, + Err(_e) => { + sleep(Duration::from_secs(1)).await; + continue; + } + }; break match status { TransactionStatus::Received => { diff --git a/crates/saya/core/src/error.rs b/crates/saya/core/src/error.rs index aba773abd5..54b7785298 100644 --- a/crates/saya/core/src/error.rs +++ b/crates/saya/core/src/error.rs @@ -14,6 +14,8 @@ pub enum Error { BlockNotFound(katana_primitives::block::BlockIdOrTag), // #[error(transparent)] // Snos(#[from] snos::error::SnOsError), + #[error("Invalid chain_id ")] + InvalidChainId, } pub type SayaResult = Result; diff --git a/crates/saya/core/src/lib.rs b/crates/saya/core/src/lib.rs index 60e4f891bd..fa982c5eb4 100644 --- a/crates/saya/core/src/lib.rs +++ b/crates/saya/core/src/lib.rs @@ -18,6 +18,7 @@ pub use prover_sdk::ProverAccessKey; use saya_provider::rpc::JsonRpcProvider; use saya_provider::Provider as SayaProvider; use serde::{Deserialize, Serialize}; +use starknet::core::utils::cairo_short_string_to_felt; use starknet_crypto::poseidon_hash_many; use tokio::fs::File; use tokio::io::AsyncWriteExt; @@ -45,8 +46,8 @@ pub struct SayaConfig { #[serde(deserialize_with = "url_deserializer")] pub katana_rpc: Url, #[serde(deserialize_with = "url_deserializer")] - pub url: Url, - pub private_key: ProverAccessKey, + pub prover_url: Url, + pub prover_key: ProverAccessKey, pub store_proofs: bool, pub start_block: u64, pub batch_size: usize, @@ -54,9 +55,20 @@ pub struct SayaConfig { pub world_address: FieldElement, pub fact_registry_address: FieldElement, pub skip_publishing_proof: bool, + pub starknet_account: StarknetAccountData, } -fn url_deserializer<'de, D>(deserializer: D) -> Result +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct StarknetAccountData { + #[serde(deserialize_with = "url_deserializer")] + pub starknet_url: Url, + #[serde(deserialize_with = "felt_string_deserializer")] + pub chain_id: FieldElement, + pub signer_address: FieldElement, + pub signer_key: FieldElement, +} + +pub fn url_deserializer<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { @@ -64,6 +76,14 @@ where Url::parse(&s).map_err(serde::de::Error::custom) } +pub fn felt_string_deserializer<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + cairo_short_string_to_felt(&s).map_err(serde::de::Error::custom) +} + /// Saya. pub struct Saya { /// The main Saya configuration. @@ -120,8 +140,8 @@ impl Saya { let mut previous_block_state_root = block_before_the_first?.header.header.state_root; let prover_identifier = ProverIdentifier::Http(Arc::new(HttpProverParams { - prover_url: self.config.url.clone(), - prover_key: self.config.private_key.clone(), + prover_url: self.config.prover_url.clone(), + prover_key: self.config.prover_key.clone(), })); // The structure responsible for proving. @@ -348,6 +368,7 @@ impl Saya { let (transaction_hash, nonce_after) = verifier::verify( VerifierIdentifier::HerodotusStarknetSepolia(self.config.fact_registry_address), serialized_proof, + self.config.starknet_account.clone(), ) .await?; info!(target: LOG_TARGET, last_block, transaction_hash, "Block verified."); @@ -368,6 +389,7 @@ impl Saya { program_output, program_hash, nonce_after + 1u64.into(), + self.config.starknet_account.clone(), ) .await?; info!(target: LOG_TARGET, last_block, transaction_hash, "Diffs applied."); diff --git a/crates/saya/core/src/verifier/mod.rs b/crates/saya/core/src/verifier/mod.rs index 28323f3e30..5cad2bb643 100644 --- a/crates/saya/core/src/verifier/mod.rs +++ b/crates/saya/core/src/verifier/mod.rs @@ -10,6 +10,8 @@ use ::starknet::core::types::FieldElement; use serde::{Deserialize, Serialize}; +use crate::StarknetAccountData; + mod starknet; /// Supported verifiers. @@ -23,10 +25,11 @@ pub enum VerifierIdentifier { pub async fn verify( verifier: VerifierIdentifier, serialized_proof: Vec, + account: StarknetAccountData, ) -> anyhow::Result<(String, FieldElement)> { match verifier { VerifierIdentifier::HerodotusStarknetSepolia(fact_registry_address) => { - starknet::starknet_verify(fact_registry_address, serialized_proof).await + starknet::starknet_verify(fact_registry_address, serialized_proof, account).await } VerifierIdentifier::StoneLocal => unimplemented!("Stone Verifier not yet supported"), VerifierIdentifier::StarkwareEthereum => { diff --git a/crates/saya/core/src/verifier/starknet.rs b/crates/saya/core/src/verifier/starknet.rs index f948336575..3c8f78f38e 100644 --- a/crates/saya/core/src/verifier/starknet.rs +++ b/crates/saya/core/src/verifier/starknet.rs @@ -9,15 +9,20 @@ use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; use tokio::time::sleep; -use crate::dojo_os::STARKNET_ACCOUNT; +use crate::dojo_os::get_starknet_account; +use crate::StarknetAccountData; pub async fn starknet_verify( fact_registry_address: FieldElement, serialized_proof: Vec, + starknet_config: StarknetAccountData, ) -> anyhow::Result<(String, FieldElement)> { let txn_config = TxnConfig { wait: true, receipt: true, ..Default::default() }; - let nonce = STARKNET_ACCOUNT.get_nonce().await?; - let tx = STARKNET_ACCOUNT + let account = get_starknet_account(starknet_config)?; + let account = account.lock().await; + + let nonce = account.get_nonce().await?; + let tx = account .execute(vec![Call { to: fact_registry_address, selector: get_selector_from_name("verify_and_register_fact").expect("invalid selector"), @@ -35,14 +40,13 @@ pub async fn starknet_verify( anyhow::bail!("Transaction not mined in {} seconds.", wait_for.as_secs()); } - let status = - match STARKNET_ACCOUNT.provider().get_transaction_status(tx.transaction_hash).await { - Ok(status) => status, - Err(_e) => { - sleep(Duration::from_secs(1)).await; - continue; - } - }; + let status = match account.provider().get_transaction_status(tx.transaction_hash).await { + Ok(status) => status, + Err(_e) => { + sleep(Duration::from_secs(1)).await; + continue; + } + }; break match status { TransactionStatus::Received => {