diff --git a/Cargo.lock b/Cargo.lock index c75a4d73..553c3498 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,7 @@ dependencies = [ "starknet-crypto 0.7.2", "starknet-types-core", "starknet_api 0.13.0-rc.1 (git+https://github.com/sergey-melnychuk/sequencer.git?tag=beerus-wasm-2024-09-22)", + "tempfile", "thiserror", "tokio", "toml", @@ -879,6 +880,8 @@ dependencies = [ "once_cell", "paste", "phf", + "rand 0.8.5", + "rstest", "serde", "serde_json", "sha2 0.10.8", diff --git a/Cargo.toml b/Cargo.toml index 7ea0e868..13e252fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ gloo-timers = { version = "0.3.0", features = ["futures"] } [dev-dependencies] anyhow = "1.0.89" alloy-primitives = { version = "0.8.5", default-features = false } +blockifier = { git = "https://github.com/sergey-melnychuk/sequencer.git", tag = "beerus-wasm-2024-09-22", version = "=0.8.0-rc.2", features = ["testing"] } chrono = "0.4.38" katana-core = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9" } katana-executor = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.9" } @@ -79,6 +80,7 @@ katana-rpc-api = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alp scarb = { git = "https://github.com/software-mansion/scarb/", tag = "v2.8.3" } semver = { version = "1", features = ["serde"] } wiremock = "0.6.2" +tempfile = "3.13.0" [patch.crates-io] starknet-core = { git = "https://github.com/kariy/starknet-rs", branch = "dojo-patch" } diff --git a/src/config.rs b/src/config.rs index 94b14af0..5f072694 100644 --- a/src/config.rs +++ b/src/config.rs @@ -171,6 +171,7 @@ async fn call_method(url: &str, method: &str) -> Result { #[cfg(test)] mod tests { use super::*; + use std::io::Write; use wiremock::{ matchers::body_partial_json, Mock, MockServer, ResponseTemplate, }; @@ -335,6 +336,41 @@ mod tests { result.unwrap_err().to_string(), "rpc error: computer says no" ); + + drop(server); + + let server = mock(&[ + ( + serde_json::json!({ + "method": "eth_chainId" + }), + serde_json::json!({ + "id": 0, + "jsonrpc": "2.0", + "result": "0xcafebabe" + }), + ), + ( + serde_json::json!({ + "method": "starknet_chainId" + }), + serde_json::json!({ + "id": 0, + "jsonrpc": "2.0", + "error": serde_json::json!({ + "object": "error" + }) + }), + ), + ]) + .await; + + let rpc = format!("http://{}/", server.address()); + let result = check_chain_id(&rpc, &rpc).await; + assert_eq!( + result.unwrap_err().to_string(), + "rpc error: {\"object\":\"error\"}" + ); } #[tokio::test] @@ -353,4 +389,121 @@ mod tests { assert!(response.is_err()); assert!(response.unwrap_err().to_string().contains("poll_secs")); } + + #[tokio::test] + async fn test_default_poll_seconds_returns_default_value() { + assert_eq!(default_poll_secs(), DEFAULT_POLL_SECS); + } + + #[tokio::test] + async fn test_default_rpc_addr() { + assert_eq!( + default_rpc_addr().to_string(), + String::from("0.0.0.0:3030") + ); + } + + #[tokio::test] + async fn test_default_data_dir() { + assert_eq!(default_data_dir(), String::from("tmp")); + } + + #[tokio::test] + async fn test_server_config_from_env() { + // lets make a clean state for our test + std::env::remove_var("POLL_SECS"); + std::env::remove_var("RPC_ADDR"); + std::env::remove_var("ETHEREUM_RPC"); + std::env::remove_var("STARKNET_RPC"); + std::env::remove_var("DATA_DIR"); + + let config = ServerConfig::from_env(); + assert!(config.is_err()); + assert!(config.unwrap_err().to_string().contains("ETHEREUM_RPC env var missing")); + + std::env::set_var("ETHEREUM_RPC", "ethereum_rpc"); + + let config = ServerConfig::from_env(); + assert!(config.is_err()); + assert!(config.unwrap_err().to_string().contains("STARKNET_RPC env var missing")); + + std::env::set_var("STARKNET_RPC", "starknet_rpc"); + + let config = ServerConfig::from_env().unwrap(); + assert_eq!(config.client.ethereum_rpc, "ethereum_rpc"); + assert_eq!(config.client.starknet_rpc, "starknet_rpc"); + + // test default values + assert_eq!(config.client.data_dir, default_data_dir()); + assert_eq!(config.poll_secs, DEFAULT_POLL_SECS); + assert_eq!(config.rpc_addr, default_rpc_addr()); + + + std::env::set_var("DATA_DIR", "data_dir"); + let config = ServerConfig::from_env().unwrap(); + assert_eq!(config.client.data_dir, "data_dir"); + + + std::env::set_var("POLL_SECS", "invalid_data"); + assert!(ServerConfig::from_env().is_err()); + + std::env::set_var("POLL_SECS", "10"); + let config = ServerConfig::from_env().unwrap(); + assert_eq!(config.poll_secs, 10); + + + std::env::set_var("RPC_ADDR", "invalid_data"); + assert!(ServerConfig::from_env().is_err()); + + std::env::set_var("RPC_ADDR", "0.0.0.0:3000"); + let config = ServerConfig::from_env().unwrap(); + assert_eq!(config.rpc_addr, "0.0.0.0:3000".parse().unwrap()); + } + + + #[tokio::test] + async fn test_server_config_from_file_returns_error_for_non_exisiting_path() { + assert!(!std::path::Path::new("/beerus/does_not_exists").exists()); + assert!(ServerConfig::from_file("/beerus/does_not_exists").is_err()); + } + + #[tokio::test] + async fn test_server_config_from_file() { + let mut ntmpfile = tempfile::NamedTempFile::new().unwrap(); + write!(ntmpfile,r#" + ethereum_rpc = "ethereum_rpc" + starknet_rpc = "starknet_rpc" + data_dir = "tmp" + poll_secs = 5 + rpc_addr = "127.0.0.1:3030" + "#).unwrap(); + + let config = ServerConfig::from_file(ntmpfile.path().to_str().unwrap()).unwrap(); + assert_eq!(config.client.ethereum_rpc, "ethereum_rpc"); + assert_eq!(config.client.starknet_rpc, "starknet_rpc"); + assert_eq!(config.client.data_dir, "tmp"); + assert_eq!(config.poll_secs, 5); + assert_eq!(config.rpc_addr, SocketAddr::from(([127, 0, 0, 1], 3030))); + } + + #[tokio::test] + async fn test_check_data_dir() { + assert!(check_data_dir(&"does_not_exists").is_err()); + + let tmp_dir = tempfile::tempdir().unwrap(); + let tmp_path = tmp_dir.path().to_owned(); + let mut perms = tmp_path.metadata().unwrap().permissions(); + perms.set_readonly(true); + std::fs::set_permissions(&tmp_path, perms).unwrap(); + + let check = check_data_dir(&tmp_path); + assert!(check.is_err()); + assert!(check.unwrap_err().to_string().contains("path is readonly")); + + let mut perms = tmp_path.metadata().unwrap().permissions(); + perms.set_readonly(false); + std::fs::set_permissions(&tmp_path, perms).unwrap(); + let check = check_data_dir(&tmp_path); + assert!(check.is_ok()); + } } diff --git a/src/exe/err.rs b/src/exe/err.rs index 71c92112..c4ba1f34 100644 --- a/src/exe/err.rs +++ b/src/exe/err.rs @@ -47,3 +47,34 @@ impl From for iamgroot::jsonrpc::Error { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_conversion_to_iamgroot_jsonrpc_error() { + let error = Error::Custom("test"); + let iamgroot_jsonrpc_error: iamgroot::jsonrpc::Error = error.into(); + assert_eq!(iamgroot_jsonrpc_error, iamgroot::jsonrpc::Error {code: 500, message: "test".to_string()}); + + + let error = Error::IamGroot(iamgroot::jsonrpc::Error {code: 500, message: "test".to_string()}); + let iamgroot_jsonrpc_error: iamgroot::jsonrpc::Error = error.into(); + + assert_eq!( + iamgroot::jsonrpc::Error {code: 500, message: "test".to_string()}, + iamgroot_jsonrpc_error, + ); + } + #[test] + fn test_conversion_to_blockifier_state_errors_state_error() { + let error = Error::Custom("test"); + let blockifier_error: blockifier::state::errors::StateError = error.into(); + + assert_eq!( + blockifier_error.to_string(), + blockifier::state::errors::StateError::StateReadError("Custom(\"test\")".into()).to_string() + ); + } +} diff --git a/src/exe/mod.rs b/src/exe/mod.rs index 8c682191..8e5b28b0 100644 --- a/src/exe/mod.rs +++ b/src/exe/mod.rs @@ -318,3 +318,105 @@ impl BlockifierState for StateProxy { tracing::info!(?class_hash, pcs.len = pcs.len(), "add_visited_pcs"); } } + +#[cfg(test)] +mod tests { + + use blockifier::execution::contract_class::ContractClassV1; + use starknet_api::core::PatriciaKey; + + use super::*; + struct MockHttpClient { + response: std::result::Result, + } + + impl MockHttpClient { + fn new(response: std::result::Result) -> Self { + Self { + response, + } + } + } + + impl Default for MockHttpClient { + fn default() -> Self { + Self::new(Ok(iamgroot::jsonrpc::Response::result(serde_json::Value::default()))) + } + } + + impl gen::client::blocking::HttpClient for MockHttpClient { + fn post( + &self, + _url: &str, + _request: &iamgroot::jsonrpc::Request, + ) -> std::result::Result { + self.response.clone() + } + } + + fn state_proxy_with_response ( + response: std::result::Result, + proxy: Option> + ) -> StateProxy { + let state = if let Some(test) = proxy { + test.state.clone() + } else { + State { + block_number: 0, + block_hash: gen::Felt::try_new("0x0").unwrap(), + root: gen::Felt::try_new("0x0").unwrap(), + } + }; + StateProxy { + state, + client: gen::client::blocking::Client::new("test", MockHttpClient::new(response)), + } + } + + #[test] + fn test_exe() { + let p = StateProxy { + client: gen::client::blocking::Client::new("test", MockHttpClient::default()), + state: State { + block_number: 0, + block_hash: gen::Felt::try_new("0x0").unwrap(), + root: gen::Felt::try_new("0x0").unwrap(), + } + }; + + let mut proxy = state_proxy_with_response( + Ok(iamgroot::jsonrpc::Response::result(serde_json::Value::String("0x0".into()))), + Some(p) + ); + + let response = proxy.get_storage_at( + ContractAddress(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + StarknetStorageKey(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + ); + + let nonce = proxy.get_nonce_at( + ContractAddress(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + ).unwrap(); + + assert_eq!(nonce.0, + starknet_crypto::Felt::from_hex("0x0").unwrap(), + ); + + proxy.set_storage_at( + ContractAddress(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + StarknetStorageKey(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + starknet_crypto::Felt::ZERO, + ).unwrap(); + proxy.increment_nonce( + ContractAddress(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + ).unwrap(); + proxy.set_class_hash_at( + ContractAddress(PatriciaKey::try_from(starknet_crypto::Felt::ZERO).unwrap()), + ClassHash(starknet_crypto::Felt::ZERO), + ).unwrap(); + proxy.set_contract_class(ClassHash(starknet_crypto::Felt::ZERO), ContractClass::V1(ContractClassV1::empty_for_testing())).unwrap(); + proxy.set_compiled_class_hash(ClassHash(starknet_crypto::Felt::ZERO), CompiledClassHash(starknet_crypto::Felt::ZERO)).unwrap(); + proxy.add_visited_pcs(ClassHash(starknet_crypto::Felt::ZERO), &HashSet::new()); + + } +} \ No newline at end of file diff --git a/src/proof.rs b/src/proof.rs index cf8b1ecd..02d4e7ce 100644 --- a/src/proof.rs +++ b/src/proof.rs @@ -26,6 +26,29 @@ impl From for Direction { } } +use thiserror::Error as ThisError; + +#[derive(Debug, ThisError)] +pub enum ProofVerifyError { + #[error("{0}")] + Other(String), + #[error("{0}")] + Parse(String), + #[error("{0}")] + RPC(#[from] iamgroot::jsonrpc::Error), + // iamgroot::jsonrpc::Error(#[from] reqwest::Error), +} + +impl From for iamgroot::jsonrpc::Error { + fn from(error: ProofVerifyError) -> Self { + match error { + ProofVerifyError::Other(e) => jsonrpc::Error::new(-32700, format!("{e}").to_string()), + ProofVerifyError::Parse(e) => jsonrpc::Error::new(-32701, format!("{e}").to_string()), + ProofVerifyError::RPC(e) => e, + } + } +} + impl GetProofResult { pub fn verify( &self, @@ -37,8 +60,13 @@ impl GetProofResult { let contract_data = self.contract_data.as_ref().ok_or( jsonrpc::Error::new(-32700, "No contract data found".to_string()), )?; - self.verify_storage_proofs(contract_data, key, value)?; - self.verify_contract_proof(contract_data, global_root, contract_address) + + let storage_proofs = &contract_data.storage_proofs.as_ref().ok_or( + jsonrpc::Error::new(-32700, "No storage proof found".to_string()), + )?; + self.verify_storage_proofs(contract_data, key, value, storage_proofs)?; + self.verify_contract_proof(contract_data, global_root, contract_address)?; + Ok(()) } fn verify_storage_proofs( @@ -46,29 +74,20 @@ impl GetProofResult { contract_data: &ContractData, key: StorageKey, value: Felt, - ) -> Result<(), jsonrpc::Error> { + storage_proofs: &Vec>, + ) -> Result<(), ProofVerifyError> { let root = &contract_data.root; - let storage_proofs = &contract_data.storage_proofs.as_ref().ok_or( - jsonrpc::Error::new(-32700, "No storage proof found".to_string()), - )?[0]; - - match Self::parse_proof(key.as_ref(), value, storage_proofs)? { + let sp = &storage_proofs[0]; + match Self::parse_proof(key.as_ref(), value, &sp)? { Some(computed_root) if computed_root.as_ref() == root.as_ref() => { Ok(()) } Some(computed_root) => { - Err(jsonrpc::Error::new( - -32700, - format!( - "Proof invalid:\nprovided-root -> {}\ncomputed-root -> {}\n", - root.as_ref(), computed_root.as_ref() - ), - )) + Err(ProofVerifyError::Other( + format!("Proof invalid:\nprovided-root -> {root:?}\ncomputed-root -> {computed_root:?}\n")) + ) }, - None => Err(jsonrpc::Error::new( - -32700, - format!("Proof invalid for root -> {}\n", root.as_ref()), - )), + None => Err(ProofVerifyError::Other(format!("Proof invalid for root -> {root:?}\n"))), } } @@ -77,7 +96,7 @@ impl GetProofResult { contract_data: &ContractData, global_root: Felt, contract_address: Address, - ) -> Result<(), jsonrpc::Error> { + ) -> Result<(), ProofVerifyError> { let state_hash = Self::calculate_contract_state_hash(contract_data)?; match Self::parse_proof( @@ -87,10 +106,7 @@ impl GetProofResult { )? { Some(storage_commitment) => { let class_commitment = self.class_commitment.as_ref().ok_or( - jsonrpc::Error::new( - -32700, - "No class commitment".to_string(), - ), + ProofVerifyError::Other("No class commitment".to_string()) )?; let parsed_global_root = Self::calculate_global_root( class_commitment, @@ -103,30 +119,21 @@ impl GetProofResult { ) })?; let state_commitment = self.state_commitment.as_ref().ok_or( - jsonrpc::Error::new( - -32700, - "No state commitment".to_string(), - ), + ProofVerifyError::Other("No state state".to_string()) )?; if state_commitment.as_ref() == parsed_global_root.as_ref() && global_root.as_ref() == parsed_global_root.as_ref() { Ok(()) } else { - Err(jsonrpc::Error::new( - -32700, - format!("Proof invalid:\nstate commitment -> {}\nparsed global root -> {}\n global root -> {}", - state_commitment.as_ref(), parsed_global_root.as_ref(), global_root.as_ref()) + Err(ProofVerifyError::Other( + format!( + "Proof invalid:\nstate commitment -> {state_commitment:?}\nparsed global root -> {parsed_global_root:?}\n global root -> {global_root:?}" + ) )) } } - None => Err(jsonrpc::Error::new( - -32700, - format!( - "Could not parse global root for root: {}", - global_root.as_ref() - ), - )), + None => Err(ProofVerifyError::Parse(format!("Could not parse global root for root: {}", global_root.as_ref()))) } } @@ -136,32 +143,12 @@ impl GetProofResult { // The contract state hash is defined as H(H(H(hash, root), nonce), CONTRACT_STATE_HASH_VERSION) const CONTRACT_STATE_HASH_VERSION: FieldElement = FieldElement::ZERO; let hash = pedersen_hash( - &FieldElement::from_hex(contract_data.class_hash.as_ref()) - .map_err(|_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - })?, - &FieldElement::from_hex(contract_data.root.as_ref()).map_err( - |_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - }, - )?, + &Self::create_field_element_from_hex(&contract_data.class_hash.as_ref())?, + &Self::create_field_element_from_hex(&contract_data.root.as_ref())?, ); let hash = pedersen_hash( &hash, - &FieldElement::from_hex(contract_data.nonce.as_ref()).map_err( - |_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - }, - )?, + &Self::create_field_element_from_hex(&contract_data.nonce.as_ref())?, ); let hash = pedersen_hash(&hash, &CONTRACT_STATE_HASH_VERSION); Felt::try_new(&format!("0x{:x}", hash)).map_err(|_| { @@ -180,22 +167,8 @@ impl GetProofResult { FieldElement::from_bytes_be_slice(b"STARKNET_STATE_V0"); let hash = poseidon_hash_many(&[ global_state_ver, - FieldElement::from_hex(storage_commitment.as_ref()).map_err( - |_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - }, - )?, - FieldElement::from_hex(class_commitment.as_ref()).map_err( - |_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - }, - )?, + Self::create_field_element_from_hex(&storage_commitment.as_ref())?, + Self::create_field_element_from_hex(&class_commitment.as_ref())?, ]); Felt::try_new(&format!("0x{:x}", hash)).map_err(|_| { jsonrpc::Error::new( @@ -209,23 +182,13 @@ impl GetProofResult { key: impl Into, value: Felt, proof: &[Node], - ) -> Result, jsonrpc::Error> { - let key = FieldElement::from_hex(&key.into()).map_err(|_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - })?; + ) -> Result, ProofVerifyError> { + let key = Self::create_field_element_from_hex(&key.into())?; let key = felt_to_bits(&key.to_bytes_be()); if key.len() != 251 { return Ok(None); } - let value = FieldElement::from_hex(value.as_ref()).map_err(|_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - })?; + let value = Self::create_field_element_from_hex(&value.as_ref())?; // initialized to the value so if the last node // in the proof is a binary node we can still verify let (mut hold, mut path_len) = (value, 0); @@ -236,22 +199,8 @@ impl GetProofResult { edge: EdgeNodeEdge { child, path }, }) => { // calculate edge hash given by provider - let child_felt = FieldElement::from_hex(child.as_ref()) - .map_err(|_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - })?; - let path_value = FieldElement::from_hex( - path.value.as_ref(), - ) - .map_err(|_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - })?; + let child_felt = Self::create_field_element_from_hex(&child.as_ref())?; + let path_value = Self::create_field_element_from_hex(&path.value.as_ref())?; let provided_hash = pedersen_hash(&child_felt, &path_value) + FieldElement::from(path.len as u64); if i == 0 { @@ -280,21 +229,8 @@ impl GetProofResult { binary: BinaryNodeBinary { left, right }, }) => { path_len += 1; - let left = FieldElement::from_hex(left.as_ref()).map_err( - |_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - }, - )?; - let right = FieldElement::from_hex(right.as_ref()) - .map_err(|_| { - jsonrpc::Error::new( - -32701, - "Failed to create Field Element".to_string(), - ) - })?; + let left = Self::create_field_element_from_hex(&left.as_ref())?; + let right = Self::create_field_element_from_hex(&right.as_ref())?; // identify path direction for this node let expected_hash = match Direction::from(key[251 - path_len as usize]) { @@ -313,6 +249,10 @@ impl GetProofResult { Ok(Some(Felt::try_new(&format!("0x{:x}", hold))?)) } + + fn create_field_element_from_hex(hex: &str) -> Result { + FieldElement::from_hex(hex).map_err(|_| ProofVerifyError::Parse("Failed to create Field Element".to_string())) + } } #[cfg(test)] mod tests { @@ -520,9 +460,10 @@ mod tests { state_commitment: Some(Felt::try_new("0x0").unwrap()) }; let contract_data = storage_proof.contract_data.as_ref().unwrap(); + let proofs = contract_data.storage_proofs.as_ref().unwrap(); assert!(storage_proof - .verify_storage_proofs(contract_data, key, value) + .verify_storage_proofs(contract_data, key, value, proofs) .is_ok()); } @@ -556,9 +497,10 @@ mod tests { state_commitment: Some(Felt::try_new("0x0").unwrap()), }; let contract_data = storage_proof.contract_data.as_ref().unwrap(); + let proofs = contract_data.storage_proofs.as_ref().unwrap(); assert!(storage_proof - .verify_storage_proofs(contract_data, key, value) + .verify_storage_proofs(contract_data, key, value, proofs) .is_err()); }