diff --git a/Cargo.lock b/Cargo.lock index 0032ca47..1b6d79da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3345,7 +3345,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5329,7 +5329,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/bin/output_segment/src/main.rs b/crates/bin/output_segment/src/main.rs index 7a9b5535..b6976729 100644 --- a/crates/bin/output_segment/src/main.rs +++ b/crates/bin/output_segment/src/main.rs @@ -19,6 +19,10 @@ struct Args { /// RPC endpoint to use for fact fetching #[arg(long = "rpc-provider", default_value = "http://localhost:9545")] rpc_provider: String, + + /// RPC version to use for fact fetching + #[arg(long = "rpc-version", default_value = "v0_7")] + rpc_version: String, } fn init_logging() { @@ -48,7 +52,7 @@ async fn main() { log::info!("Runnin SNOS for block number: {}", block_number); let (snos_pie, _snos_output) = - prove_block(DEFAULT_COMPILED_OS, block_number, &endpoint, LayoutName::all_cairo, true) + prove_block(DEFAULT_COMPILED_OS, block_number, &endpoint, &args.rpc_version, LayoutName::all_cairo, true) .await .map_err(debug_prove_error) .expect("OS generate Cairo PIE"); diff --git a/crates/bin/prove_block/src/lib.rs b/crates/bin/prove_block/src/lib.rs index 417ef8a3..8b322937 100644 --- a/crates/bin/prove_block/src/lib.rs +++ b/crates/bin/prove_block/src/lib.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::rc::Rc; +use std::str::FromStr; use blockifier::state::cached_state::CachedState; use cairo_vm::types::layout_name::LayoutName; @@ -8,6 +9,7 @@ use cairo_vm::vm::errors::cairo_run_errors::CairoRunError; use cairo_vm::vm::runners::cairo_pie::CairoPie; use cairo_vm::Felt252; use reexecute::{reexecute_transactions_with_blockifier, ProverPerContractStorage}; +use rpc_client::client::{ClientError, ClientVersion}; use rpc_client::pathfinder::proofs::{PathfinderClassProof, ProofVerificationError}; use rpc_client::RpcClient; use rpc_replay::block_context::build_block_context; @@ -68,6 +70,8 @@ pub enum ProveBlockError { ToBlockifierError(#[from] ToBlockifierError), #[error("Felt Conversion Error: {0}")] FeltConversionError(#[from] FeltConversionError), + #[error("Client Error: {0}")] + ClientError(#[from] ClientError), } fn compute_class_commitment( @@ -116,13 +120,15 @@ pub async fn prove_block( compiled_os: &[u8], block_number: u64, rpc_provider: &str, + rpc_version: &str, layout: LayoutName, full_output: bool, ) -> Result<(CairoPie, StarknetOsOutput), ProveBlockError> { let block_id = BlockId::Number(block_number); let previous_block_id = BlockId::Number(block_number - 1); - let rpc_client = RpcClient::new(rpc_provider); + let client_version = ClientVersion::from_str(rpc_version)?; + let rpc_client = RpcClient::new(rpc_provider, client_version); // Step 1: build the block context let chain_id = chain_id_from_felt(rpc_client.starknet_rpc().chain_id().await?); @@ -166,7 +172,8 @@ pub async fn prove_block( let transactions: Vec<_> = block_with_txs.transactions.clone().into_iter().map(starknet_rs_tx_to_internal_tx).collect(); - let (processed_state_update, traces) = get_formatted_state_update(&rpc_client, previous_block_id, block_id).await?; + let (processed_state_update, traces) = + get_formatted_state_update(&rpc_client, previous_block_id, block_id, block_number).await?; let class_hash_to_compiled_class_hash = processed_state_update.class_hash_to_compiled_class_hash; @@ -209,7 +216,6 @@ pub async fn prove_block( let mut contract_storages = ContractStorageMap::new(); let mut contract_address_to_class_hash = HashMap::new(); - // TODO: remove this clone() for (contract_address, storage_proof) in storage_proofs.clone() { let previous_storage_proof = previous_storage_proofs.get(&contract_address).expect("failed to find previous storage proof"); diff --git a/crates/bin/prove_block/src/main.rs b/crates/bin/prove_block/src/main.rs index 0871c1a4..871677ec 100644 --- a/crates/bin/prove_block/src/main.rs +++ b/crates/bin/prove_block/src/main.rs @@ -13,6 +13,10 @@ struct Args { /// RPC endpoint to use for fact fetching #[arg(long = "rpc-provider", default_value = "http://localhost:9545")] rpc_provider: String, + + /// RPC version to use for fact fetching + #[arg(long = "rpc-version", default_value = "v0_7")] + rpc_version: String, } fn init_logging() { @@ -32,7 +36,15 @@ async fn main() { let block_number = args.block_number; let layout = LayoutName::all_cairo; - let result = prove_block::prove_block(DEFAULT_COMPILED_OS, block_number, &args.rpc_provider, layout, true).await; + let result = prove_block::prove_block( + DEFAULT_COMPILED_OS, + block_number, + &args.rpc_provider, + &args.rpc_version, + layout, + true, + ) + .await; let (pie, _snos_output) = result.map_err(debug_prove_error).expect("Block proven"); pie.run_validity_checks().expect("Valid PIE"); } diff --git a/crates/bin/prove_block/src/reexecute.rs b/crates/bin/prove_block/src/reexecute.rs index 15c49195..a73909ab 100644 --- a/crates/bin/prove_block/src/reexecute.rs +++ b/crates/bin/prove_block/src/reexecute.rs @@ -167,7 +167,6 @@ impl PerContractStorage for ProverPerContractStorage { None => &ContractData::default(), Some(data) => data, }; - let updated_root = contract_data.root; let commitment_facts = format_commitment_facts::(&contract_data.storage_proofs); diff --git a/crates/bin/prove_block/src/rpc_utils.rs b/crates/bin/prove_block/src/rpc_utils.rs index 5c9dc9ae..6635150e 100644 --- a/crates/bin/prove_block/src/rpc_utils.rs +++ b/crates/bin/prove_block/src/rpc_utils.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use blockifier::transaction::objects::TransactionExecutionInfo; use cairo_vm::Felt252; use num_bigint::BigInt; -use rpc_client::pathfinder::client::ClientError; +use rpc_client::client::ClientError; use rpc_client::pathfinder::proofs::{ ContractData, EdgePath, PathfinderClassProof, PathfinderProof, ProofVerificationError, TrieNode, }; @@ -18,6 +18,59 @@ use starknet_types_core::felt::Felt; use crate::utils::get_all_accessed_keys; +/// Fetches the storage proof for the specified contract and storage keys. +/// This function can fetch additional keys if required to fill gaps in the storage trie +/// that must be filled to get the OS to function. See `get_key_following_edge` for more details. +pub(crate) async fn get_storage_proofs( + client: &RpcClient, + block_number: u64, + tx_execution_infos: &[TransactionExecutionInfo], + old_block_number: Felt, +) -> Result, ClientError> { + let accessed_keys_by_address = { + let mut keys = get_all_accessed_keys(tx_execution_infos); + // We need to fetch the storage proof for the block hash contract + keys.entry(contract_address!("0x1")).or_default().insert(old_block_number.try_into().unwrap()); + // Include extra keys for contracts that trigger get_block_hash_syscall + insert_extra_storage_reads_keys(old_block_number, &mut keys); + keys + }; + + let mut storage_proofs = HashMap::new(); + + log::info!("Contracts we're fetching proofs for:"); + for (contract_address, storage_keys) in accessed_keys_by_address { + log::info!(" Fetching proof for {}", contract_address.to_string()); + let contract_address_felt = *contract_address.key(); + let keys: Vec<_> = storage_keys.into_iter().map(|storage_key| *storage_key.key()).collect(); + + let mut storage_proof = + fetch_storage_proof_for_contract(client, contract_address_felt, &keys, block_number).await?; + + match &storage_proof.contract_data { + None => { + storage_proofs.insert(contract_address_felt, storage_proof); + } + Some(contract_data) => { + let additional_keys = verify_storage_proof(contract_data, &keys); + + // Fetch additional proofs required to fill gaps in the storage trie that could make + // the OS crash otherwise. + if !additional_keys.is_empty() { + let additional_proof = + fetch_storage_proof_for_contract(client, contract_address_felt, &additional_keys, block_number) + .await?; + + storage_proof = merge_storage_proofs(vec![storage_proof, additional_proof]); + } + storage_proofs.insert(contract_address_felt, storage_proof); + } + }; + } + + Ok(storage_proofs) +} + /// Fetches the state + storage proof for a single contract for all the specified keys. /// This function handles the chunking of requests imposed by the RPC API and merges /// the proofs returned from multiple calls into one. @@ -28,14 +81,15 @@ async fn fetch_storage_proof_for_contract( block_number: u64, ) -> Result { let storage_proof = if keys.is_empty() { - rpc_client.pathfinder_rpc().get_proof(block_number, contract_address, &[]).await? + rpc_client.pathfinder_rpc().get_contract_proof(block_number, contract_address, &[]).await? } else { - // The endpoint is limited to 100 keys at most per call - const MAX_KEYS: usize = 100; + // The endpoint is limited to 100 keys at most per call, including the contract address + const MAX_KEYS: usize = 99; let mut chunked_storage_proofs = Vec::new(); for keys_chunk in keys.chunks(MAX_KEYS) { - chunked_storage_proofs - .push(rpc_client.pathfinder_rpc().get_proof(block_number, contract_address, keys_chunk).await?); + chunked_storage_proofs.push( + rpc_client.pathfinder_rpc().get_contract_proof(block_number, contract_address, keys_chunk).await?, + ); } merge_storage_proofs(chunked_storage_proofs) }; @@ -43,41 +97,6 @@ async fn fetch_storage_proof_for_contract( Ok(storage_proof) } -/// Fetches the storage proof for the specified contract and storage keys. -/// This function can fetch additional keys if required to fill gaps in the storage trie -/// that must be filled to get the OS to function. See `get_key_following_edge` for more details. -async fn get_storage_proof_for_contract>( - rpc_client: &RpcClient, - contract_address: ContractAddress, - storage_keys: KeyIter, - block_number: u64, -) -> Result { - let contract_address_felt = *contract_address.key(); - let keys: Vec<_> = storage_keys.map(|storage_key| *storage_key.key()).collect(); - - let mut storage_proof = - fetch_storage_proof_for_contract(rpc_client, contract_address_felt, &keys, block_number).await?; - - let contract_data = match &storage_proof.contract_data { - None => { - return Ok(storage_proof); - } - Some(contract_data) => contract_data, - }; - let additional_keys = verify_storage_proof(contract_data, &keys); - - // Fetch additional proofs required to fill gaps in the storage trie that could make - // the OS crash otherwise. - if !additional_keys.is_empty() { - let additional_proof = - fetch_storage_proof_for_contract(rpc_client, contract_address_felt, &additional_keys, block_number).await?; - - storage_proof = merge_storage_proofs(vec![storage_proof, additional_proof]); - } - - Ok(storage_proof) -} - /// Verify the storage proofs and handle errors. /// Returns a list of additional keys to fetch to fill gaps in the tree that will make the OS /// crash otherwise. @@ -141,36 +160,6 @@ fn insert_extra_storage_reads_keys( } } -pub(crate) async fn get_storage_proofs( - client: &RpcClient, - block_number: u64, - tx_execution_infos: &[TransactionExecutionInfo], - old_block_number: Felt, -) -> Result, ClientError> { - let accessed_keys_by_address = { - let mut keys = get_all_accessed_keys(tx_execution_infos); - // We need to fetch the storage proof for the block hash contract - keys.entry(contract_address!("0x1")).or_default().insert(old_block_number.try_into().unwrap()); - // Include extra keys for contracts that trigger get_block_hash_syscall - insert_extra_storage_reads_keys(old_block_number, &mut keys); - keys - }; - - let mut storage_proofs = HashMap::new(); - - log::info!("Contracts we're fetching proofs for:"); - for (contract_address, storage_keys) in accessed_keys_by_address { - log::info!(" Fetching proof for {}", contract_address.to_string()); - let contract_address_felt = *contract_address.key(); - let storage_proof = - get_storage_proof_for_contract(client, contract_address, storage_keys.into_iter(), block_number).await?; - - storage_proofs.insert(contract_address_felt, storage_proof); - } - - Ok(storage_proofs) -} - /// Returns a modified key that follows the specified edge path. /// This function is used to work around an issue where the OS fails if it encounters a /// write to 0 and the last node in the storage proof is an edge node of length 1. diff --git a/crates/bin/prove_block/src/state_utils.rs b/crates/bin/prove_block/src/state_utils.rs index dfda9223..1415ab34 100644 --- a/crates/bin/prove_block/src/state_utils.rs +++ b/crates/bin/prove_block/src/state_utils.rs @@ -32,6 +32,7 @@ pub(crate) async fn get_formatted_state_update( rpc_client: &RpcClient, previous_block_id: BlockId, block_id: BlockId, + block_number: u64, ) -> Result<(FormattedStateUpdate, Vec), ProveBlockError> { let state_update = match rpc_client.starknet_rpc().get_state_update(block_id).await.expect("Failed to get state update") { @@ -45,7 +46,7 @@ pub(crate) async fn get_formatted_state_update( // Extract other contracts used in our block from the block trace // We need this to get all the class hashes used and correctly feed address_to_class_hash let traces = - rpc_client.starknet_rpc().trace_block_transactions(block_id).await.expect("Failed to get block tx traces"); + rpc_client.pathfinder_rpc().get_block_traces(block_number).await.expect("Failed to get block tx traces"); let (accessed_addresses, accessed_classes) = get_subcalled_contracts_from_tx_traces(&traces); let declared_classes: HashSet<_> = diff --git a/crates/bin/prove_block/tests/hash_tests.rs b/crates/bin/prove_block/tests/hash_tests.rs index 345d1d7b..2ad90233 100644 --- a/crates/bin/prove_block/tests/hash_tests.rs +++ b/crates/bin/prove_block/tests/hash_tests.rs @@ -1,3 +1,6 @@ +use std::str::FromStr; + +use rpc_client::client::ClientVersion; use rpc_client::pathfinder::proofs::ProofVerificationError; use rpc_client::RpcClient; use rstest::rstest; @@ -19,7 +22,8 @@ async fn test_recompute_class_hash(#[case] class_hash_str: String, #[case] block let class_hash = Felt::from_hex(&class_hash_str).unwrap(); let block_id = BlockId::Number(block_number); - let rpc_client = RpcClient::new(&endpoint); + let rpc_version = ClientVersion::from_str("v0_7").unwrap(); + let rpc_client = RpcClient::new(&endpoint, rpc_version); let contract_class = rpc_client.starknet_rpc().get_class(block_id, class_hash).await.unwrap(); let compiled_class = if let starknet::core::types::ContractClass::Legacy(legacy_cc) = contract_class { @@ -48,7 +52,8 @@ async fn test_recompute_class_hash(#[case] class_hash_str: String, #[case] block async fn test_class_proof_verification_non_inclusion(#[case] class_hash_str: String, #[case] block_number: u64) { let endpoint = std::env::var("PATHFINDER_RPC_URL").expect("Missing PATHFINDER_RPC_URL in env"); let class_hash = Felt::from_hex(&class_hash_str).unwrap(); - let rpc_client = RpcClient::new(&endpoint); + let rpc_version = ClientVersion::from_str("v0_7").unwrap(); + let rpc_client = RpcClient::new(&endpoint, rpc_version); let class_proof = rpc_client.pathfinder_rpc().get_class_proof(block_number, &class_hash).await.unwrap(); let result = class_proof.verify(class_hash); @@ -72,7 +77,8 @@ async fn test_class_proof_verification_non_inclusion(#[case] class_hash_str: Str async fn test_class_proof_verification_ok(#[case] class_hash_str: String, #[case] block_number: u64) { let endpoint = std::env::var("PATHFINDER_RPC_URL").expect("Missing PATHFINDER_RPC_URL in env"); let class_hash = Felt::from_hex(&class_hash_str).unwrap(); - let rpc_client = RpcClient::new(&endpoint); + let rpc_version = ClientVersion::from_str("v0_7").unwrap(); + let rpc_client = RpcClient::new(&endpoint, rpc_version); let class_proof = rpc_client.pathfinder_rpc().get_class_proof(block_number, &class_hash).await.unwrap(); assert!(class_proof.verify(class_hash).is_ok()); diff --git a/crates/bin/prove_block/tests/prove_block.rs b/crates/bin/prove_block/tests/prove_block.rs index d3bdf672..fa86648b 100644 --- a/crates/bin/prove_block/tests/prove_block.rs +++ b/crates/bin/prove_block/tests/prove_block.rs @@ -20,75 +20,81 @@ const DEFAULT_COMPILED_OS: &[u8] = include_bytes!("../../../../build/os_latest.j // # * 164333 / 169203: Declare and Deploy on the same block // # * 155140 / 155830: dest_ptr not a relocatable #[rstest] -#[case::small_block_with_only_invoke_txs(76793)] -#[case::additional_basic_blocks_1(76766)] -#[case::additional_basic_blocks_2(76775)] -#[case::block_with_reverted_tx(76832)] -#[case::failing_assert_on_versioned_constants_1(86507)] -#[case::core_hint_test_less_than_or_equal_address(87023)] -#[case::failing_assert_on_versioned_constants_2(124533)] -#[case::fix_diff_assert_values_in_contract_subcall(87019)] -#[case::invoke_with_replace_class(90000)] -#[case::write_to_zero_with_edge_node(125622)] -#[case::l1_handler(98000)] -#[case::invoke_with_call_to_deploy_syscall(124534)] -#[case::block_with_nonce_bump_inconsistency(87041)] -#[case::block_with_blob_da_1(66645)] -#[case::block_with_blob_da_2(66776)] -#[case::declare_tx(76840)] -#[case::deploy_account_v1(97581)] -#[case::deploy_account_v3(101556)] -#[case::deploy_account_many_txs(102076)] -#[case::edge_bottom_not_found(155016)] -#[case::eval_circuit(160035)] -#[case::declare_and_deploy_in_same_block(164333)] -#[case::declare_and_deploy_in_same_block(169206)] -#[case::dest_ptr_not_a_relocatable(155140)] -#[case::dest_ptr_not_a_relocatable_2(155830)] -#[case::inconsistent_cairo0_class_hash_0(30000)] -#[case::inconsistent_cairo0_class_hash_1(204936)] -#[case::no_possible_convertion_1(155007)] -#[case::no_possible_convertion_2(155029)] -#[case::reference_pie_with_full_output_enabled(173404)] -#[case::inconsistent_cairo0_class_hash_2(159674)] -#[case::inconsistent_cairo0_class_hash_3(164180)] -#[case::key_not_in_proof_0(155087)] -#[case::key_not_in_proof_1(162388)] -#[case::key_not_in_proof_2(155172)] -#[case::l1_gas_and_l1_gas_price_are_0(161476)] -#[case::key_not_in_proof_3(156855)] -#[case::key_not_in_proof_4(174968)] -#[case::timestamp_rounding_1(162389)] -#[case::timestamp_rounding_2(167815)] -#[case::missing_constant_max_high(164684)] -#[case::retdata_not_a_relocatable(160033)] -#[case::get_tx_info_using_ptr_over_relocatable(243766)] +#[case::small_block_with_only_invoke_txs(76793, "v0_7")] +#[case::additional_basic_blocks_1(76766, "v0_7")] +#[case::additional_basic_blocks_2(76775, "v0_7")] +#[case::block_with_reverted_tx(76832, "v0_7")] +#[case::failing_assert_on_versioned_constants_1(86507, "v0_7")] +#[case::core_hint_test_less_than_or_equal_address(87023, "v0_7")] +#[case::failing_assert_on_versioned_constants_2(124533, "v0_7")] +#[case::fix_diff_assert_values_in_contract_subcall(87019, "v0_7")] +#[case::invoke_with_replace_class(90000, "v0_7")] +#[case::write_to_zero_with_edge_node(125622, "v0_7")] +#[case::l1_handler(98000, "v0_7")] +#[case::invoke_with_call_to_deploy_syscall(124534, "v0_7")] +#[case::block_with_nonce_bump_inconsistency(87041, "v0_7")] +#[case::block_with_blob_da_1(66645, "v0_7")] +#[case::block_with_blob_da_2(66776, "v0_7")] +#[case::declare_tx(76840, "v0_7")] +#[case::deploy_account_v1(97581, "v0_7")] +#[case::deploy_account_v3(101556, "v0_7")] +#[case::deploy_account_many_txs(102076, "v0_7")] +#[case::edge_bottom_not_found(155016, "v0_7")] +#[case::eval_circuit(160035, "v0_7")] +#[case::declare_and_deploy_in_same_block(164333, "v0_7")] +#[case::declare_and_deploy_in_same_block(169206, "v0_7")] +#[case::dest_ptr_not_a_relocatable(155140, "v0_7")] +#[case::dest_ptr_not_a_relocatable_2(155830, "v0_7")] +#[case::inconsistent_cairo0_class_hash_0(30000, "v0_7")] +#[case::inconsistent_cairo0_class_hash_1(204936, "v0_7")] +#[case::no_possible_convertion_1(155007, "v0_7")] +#[case::no_possible_convertion_2(155029, "v0_7")] +#[case::reference_pie_with_full_output_enabled(173404, "v0_7")] +#[case::inconsistent_cairo0_class_hash_2(159674, "v0_7")] +#[case::inconsistent_cairo0_class_hash_3(164180, "v0_7")] +#[case::key_not_in_proof_0(155087, "v0_7")] +#[case::key_not_in_proof_1(162388, "v0_7")] +#[case::key_not_in_proof_2(155172, "v0_7")] +#[case::l1_gas_and_l1_gas_price_are_0(161476, "v0_7")] +#[case::key_not_in_proof_3(156855, "v0_7")] +#[case::key_not_in_proof_4(174968, "v0_7")] +#[case::timestamp_rounding_1(162389, "v0_7")] +#[case::timestamp_rounding_2(167815, "v0_7")] +#[case::missing_constant_max_high(164684, "v0_7")] +#[case::retdata_not_a_relocatable(160033, "v0_7")] +#[case::get_tx_info_using_ptr_over_relocatable(243766, "v0_7")] // The following four tests were added due to errors encountered during reexecution with blockifier -#[case::dict_error_no_value_found_for_key(161599)] -#[case::peekable_peek_is_none(174156)] -#[case::no_more_storage_reads_available(161884)] -#[case::no_more_storage_reads_available(174027)] -#[case::memory_addresses_must_be_relocatable(202083)] -#[case::memory_invalid_signature(216914)] -#[case::diff_assert_values(218624)] -#[case::could_nt_compute_operand_op1(204337)] +#[case::dict_error_no_value_found_for_key(161599, "v0_7")] +#[case::peekable_peek_is_none(174156, "v0_7")] +#[case::no_more_storage_reads_available(161884, "v0_7")] +#[case::no_more_storage_reads_available(174027, "v0_7")] +#[case::memory_addresses_must_be_relocatable(202083, "v0_7")] +#[case::memory_invalid_signature(216914, "v0_7")] +#[case::diff_assert_values(218624, "v0_7")] +#[case::could_nt_compute_operand_op1(204337, "v0_7")] // The following ten tests were added due key not found in preimage (verify_game contract function related) -#[case::key_not_found_in_preimage_0(237025)] -#[case::key_not_found_in_preimage_1(237030)] -#[case::key_not_found_in_preimage_2(237037)] -#[case::key_not_found_in_preimage_3(237042)] -#[case::key_not_found_in_preimage_4(237044)] -#[case::key_not_found_in_preimage_5(237053)] -#[case::key_not_found_in_preimage_6(237083)] -#[case::key_not_found_in_preimage_7(237086)] -#[case::key_not_found_in_preimage_8(235385)] -#[case::key_not_found_in_preimage_9(235620)] +#[case::key_not_found_in_preimage_0(237025, "v0_7")] +#[case::key_not_found_in_preimage_1(237030, "v0_7")] +#[case::key_not_found_in_preimage_2(237037, "v0_7")] +#[case::key_not_found_in_preimage_3(237042, "v0_7")] +#[case::key_not_found_in_preimage_4(237044, "v0_7")] +#[case::key_not_found_in_preimage_5(237053, "v0_7")] +#[case::key_not_found_in_preimage_6(237083, "v0_7")] +#[case::key_not_found_in_preimage_7(237086, "v0_7")] +#[case::key_not_found_in_preimage_8(235385, "v0_7")] +#[case::key_not_found_in_preimage_9(235620, "v0_7")] +// The following five tests were added due to test the json rpc api v0_8 +#[case::small_block_with_only_invoke_txs_v08(76793, "v0_8")] +#[case::block_with_reverted_tx_v08(76832, "v0_8")] +#[case::invoke_with_replace_class_v08(90000, "v0_8")] +#[case::l1_handler_v08(98000, "v0_8")] +#[case::key_not_in_proof_v08(155087, "v0_8")] #[ignore = "Requires a running Pathfinder node"] #[tokio::test(flavor = "multi_thread")] -async fn test_prove_selected_blocks(#[case] block_number: u64) { +async fn test_prove_selected_blocks(#[case] block_number: u64, #[case] rpc_version: &str) { let endpoint = std::env::var("PATHFINDER_RPC_URL").expect("Missing PATHFINDER_RPC_URL in env"); let (snos_pie, _snos_output) = - prove_block(DEFAULT_COMPILED_OS, block_number, &endpoint, LayoutName::all_cairo, true) + prove_block(DEFAULT_COMPILED_OS, block_number, &endpoint, rpc_version, LayoutName::all_cairo, true) .await .map_err(debug_prove_error) .expect("OS generate Cairo PIE"); diff --git a/crates/rpc-client/src/client.rs b/crates/rpc-client/src/client.rs index ac5a25c4..7830a696 100644 --- a/crates/rpc-client/src/client.rs +++ b/crates/rpc-client/src/client.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::sync::Arc; use reqwest::Url; @@ -6,6 +7,42 @@ use starknet::providers::JsonRpcClient; use crate::pathfinder::client::PathfinderRpcClient; +#[derive(Debug, thiserror::Error)] +pub enum ClientError { + #[error("Encountered a request error: {0}")] + ReqwestError(#[from] reqwest::Error), + #[error("Encountered a version error: {0}")] + VersionError(String), + #[error("Encountered a custom error: {0}")] + CustomError(String), +} + +pub enum ClientVersion { + Rpcv07, + Rpcv08, +} + +impl FromStr for ClientVersion { + type Err = ClientError; + + fn from_str(s: &str) -> Result { + match s { + "v0_7" => Ok(ClientVersion::Rpcv07), + "v0_8" => Ok(ClientVersion::Rpcv08), + _ => Err(ClientError::VersionError(format!("Received invalid version: {s:?}"))), + } + } +} + +impl std::fmt::Display for ClientVersion { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self { + ClientVersion::Rpcv07 => write!(f, "v0_7"), + ClientVersion::Rpcv08 => write!(f, "v0_8"), + } + } +} + struct RpcClientInner { /// starknet-rs client, used to access data from endpoints defined in the Starknet RPC spec. starknet_client: JsonRpcClient, @@ -14,14 +51,14 @@ struct RpcClientInner { } impl RpcClientInner { - fn new(base_url: &str) -> Self { - let starknet_rpc_url = format!("{}/rpc/v0_7", base_url); + fn new(base_url: &str, version: ClientVersion) -> Self { + let starknet_rpc_url = format!("{}/rpc/{}", base_url, version); log::info!("Starknet RPC URL: {}", starknet_rpc_url); let provider = JsonRpcClient::new(HttpTransport::new( Url::parse(starknet_rpc_url.as_str()) .unwrap_or_else(|e| panic!("Could not parse provider URL ({}): {}", starknet_rpc_url, e)), )); - let pathfinder_client = PathfinderRpcClient::new(base_url); + let pathfinder_client = PathfinderRpcClient::new(base_url, version); Self { starknet_client: provider, pathfinder_client } } @@ -33,8 +70,8 @@ pub struct RpcClient { } impl RpcClient { - pub fn new(base_url: &str) -> Self { - Self { inner: Arc::new(RpcClientInner::new(base_url)) } + pub fn new(base_url: &str, version: ClientVersion) -> Self { + Self { inner: Arc::new(RpcClientInner::new(base_url, version)) } } pub fn starknet_rpc(&self) -> &JsonRpcClient { diff --git a/crates/rpc-client/src/pathfinder/client.rs b/crates/rpc-client/src/pathfinder/client.rs index c167c107..ada99d31 100644 --- a/crates/rpc-client/src/pathfinder/client.rs +++ b/crates/rpc-client/src/pathfinder/client.rs @@ -2,17 +2,14 @@ use reqwest::{Response, StatusCode}; use serde::de::DeserializeOwned; use serde::Deserialize; use serde_json::json; +use starknet::core::types::TransactionTraceWithHash; use starknet_types_core::felt::Felt; -use crate::pathfinder::proofs::{PathfinderClassProof, PathfinderProof}; - -#[derive(Debug, thiserror::Error)] -pub enum ClientError { - #[error("Encountered a request error: {0}")] - ReqwestError(#[from] reqwest::Error), - #[error("Encountered a custom error: {0}")] - CustomError(String), -} +use crate::client::{ClientError, ClientVersion}; +use crate::pathfinder::proofs::{ + convert_storage_to_pathfinder_class_proof, convert_storage_to_pathfinder_proof, ContractStorageKeysItem, + PathfinderClassProof, PathfinderProof, StorageProof, +}; fn jsonrpc_request(method: &str, params: serde_json::Value) -> serde_json::Value { json!({ @@ -30,7 +27,7 @@ async fn post_jsonrpc_request( params: serde_json::Value, ) -> Result { let request = jsonrpc_request(method, params); - let response = client.post(format!("{}/rpc/pathfinder/v0.1", rpc_provider)).json(&request).send().await?; + let response = client.post(rpc_provider).json(&request).send().await?; #[derive(Deserialize)] struct TransactionReceiptResponse { @@ -57,31 +54,48 @@ pub struct PathfinderRpcClient { http_client: reqwest::Client, /// The base URL of the RPC client rpc_base_url: String, + /// The URL version of the RPC client + rpc_version: ClientVersion, } impl PathfinderRpcClient { - pub fn new(base_url: &str) -> Self { - let starknet_rpc_url = format!("{}/rpc/v0_7", base_url); - log::info!("Starknet RPC URL: {}", starknet_rpc_url); + pub fn new(base_url: &str, rpc_version: ClientVersion) -> Self { let http_client = reqwest::ClientBuilder::new().build().unwrap_or_else(|e| panic!("Could not build reqwest client: {e}")); - Self { http_client, rpc_base_url: base_url.to_string() } + Self { http_client, rpc_base_url: base_url.to_string(), rpc_version } } - pub async fn get_proof( + pub async fn get_contract_proof( &self, block_number: u64, contract_address: Felt, keys: &[Felt], ) -> Result { - post_jsonrpc_request( - &self.http_client, - &self.rpc_base_url, - "pathfinder_getProof", - json!({ "block_id": { "block_number": block_number }, "contract_address": contract_address, "keys": keys }), - ) - .await + match &self.rpc_version { + ClientVersion::Rpcv07 => { + post_jsonrpc_request( + &self.http_client, + &format!("{}/rpc/pathfinder/v0_1", &self.rpc_base_url), + "pathfinder_getProof", + json!({ "block_id": { "block_number": block_number }, "contract_address": contract_address, "keys": keys }), + ) + .await + } + ClientVersion::Rpcv08 => { + let contracts_storage_keys = ContractStorageKeysItem { contract_address, storage_keys: keys.to_vec() }; + + let response: StorageProof = post_jsonrpc_request( + &self.http_client, + &format!("{}/rpc/{}", &self.rpc_base_url, &self.rpc_version), + "starknet_getStorageProof", + json!({ "block_id": { "block_number": block_number }, "contract_addresses": &[contract_address], "contracts_storage_keys": &[contracts_storage_keys] }), + ) + .await?; + + Ok(convert_storage_to_pathfinder_proof(response)) + } + } } pub async fn get_class_proof( @@ -89,12 +103,36 @@ impl PathfinderRpcClient { block_number: u64, class_hash: &Felt, ) -> Result { - log::debug!("querying pathfinder_getClassProof for {:x}", class_hash); + match &self.rpc_version { + ClientVersion::Rpcv07 => { + post_jsonrpc_request( + &self.http_client, + &format!("{}/rpc/pathfinder/v0_1", &self.rpc_base_url), + "pathfinder_getClassProof", + json!({ "block_id": { "block_number": block_number }, "class_hash": class_hash }), + ) + .await + } + ClientVersion::Rpcv08 => { + let response: StorageProof = post_jsonrpc_request( + &self.http_client, + &format!("{}/rpc/{}", &self.rpc_base_url, &self.rpc_version), + "starknet_getStorageProof", + json!({ "block_id": { "block_number": block_number }, "class_hashes": [&class_hash] }), + ) + .await?; + + Ok(convert_storage_to_pathfinder_class_proof(response)) + } + } + } + + pub async fn get_block_traces(&self, block_number: u64) -> Result, ClientError> { post_jsonrpc_request( &self.http_client, - &self.rpc_base_url, - "pathfinder_getClassProof", - json!({ "block_id": { "block_number": block_number }, "class_hash": class_hash }), + &format!("{}/rpc/v0_7", &self.rpc_base_url), + "starknet_traceBlockTransactions", + json!({ "block_id": { "block_number": block_number }}), ) .await } diff --git a/crates/rpc-client/src/pathfinder/proofs.rs b/crates/rpc-client/src/pathfinder/proofs.rs index f0315a40..4538373a 100644 --- a/crates/rpc-client/src/pathfinder/proofs.rs +++ b/crates/rpc-client/src/pathfinder/proofs.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use starknet_os::config::DEFAULT_STORAGE_TREE_HEIGHT; use starknet_os::crypto::pedersen::PedersenHash; use starknet_os::crypto::poseidon::PoseidonHash; @@ -8,6 +8,18 @@ use starknet_os::storage::dict_storage::DictStorage; use starknet_os::storage::storage::{Fact, HashFunctionType}; use starknet_types_core::felt::Felt; +#[derive(thiserror::Error, Debug)] +pub enum ProofVerificationError<'a> { + #[error("Non-inclusion proof for key {}. Height {}.", key.to_hex_string(), height.0)] + NonExistenceProof { key: Felt, height: Height, proof: &'a [TrieNode] }, + + #[error("Proof verification failed, node_hash {node_hash:x} != parent_hash {parent_hash:x}")] + InvalidChildNodeHash { node_hash: Felt, parent_hash: Felt }, + + #[error("Conversion error")] + ConversionError, +} + #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub enum TrieNode { #[serde(rename = "binary")] @@ -38,6 +50,13 @@ impl TrieNode { } } +// Types defined for Deserialize functionality +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct EdgePath { + pub len: u64, + pub value: Felt, +} + #[derive(Debug, Clone, Deserialize, Default)] pub struct ContractData { /// Root of the Contract state tree @@ -46,18 +65,6 @@ pub struct ContractData { pub storage_proofs: Vec>, } -#[derive(thiserror::Error, Debug)] -pub enum ProofVerificationError<'a> { - #[error("Non-inclusion proof for key {}. Height {}.", key.to_hex_string(), height.0)] - NonExistenceProof { key: Felt, height: Height, proof: &'a [TrieNode] }, - - #[error("Proof verification failed, node_hash {node_hash:x} != parent_hash {parent_hash:x}")] - InvalidChildNodeHash { node_hash: Felt, parent_hash: Felt }, - - #[error("Conversion error")] - ConversionError, -} - impl ContractData { /// Verifies that each contract state proof is valid. pub fn verify(&self, storage_keys: &[Felt]) -> Result<(), Vec> { @@ -82,7 +89,7 @@ pub struct PathfinderProof { } #[allow(dead_code)] -#[derive(Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct PathfinderClassProof { pub class_commitment: Felt, pub class_proof: Vec, @@ -95,13 +102,6 @@ impl PathfinderClassProof { } } -// Types defined for Deserialize functionality -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -pub struct EdgePath { - pub len: u64, - pub value: Felt, -} - /// This function goes through the tree from top to bottom and verifies that /// the hash of each node is equal to the corresponding hash in the parent node. pub fn verify_proof( @@ -156,3 +156,106 @@ pub fn verify_proof( Ok(()) } + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct ContractStorageKeysItem { + pub contract_address: Felt, + pub storage_keys: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(untagged)] +pub enum MerkleNode { + Binary { left: Felt, right: Felt }, + Edge { child: Felt, path: Felt, length: usize }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct NodeHashToNodeMappingItem { + pub node_hash: Felt, + pub node: MerkleNode, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct ContractLeavesDataItem { + pub nonce: Felt, + pub class_hash: Felt, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct ContractsProof { + pub nodes: Vec, + pub contract_leaves_data: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct GlobalRoots { + pub contracts_tree_root: Felt, + pub classes_tree_root: Felt, + pub block_hash: Felt, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct StorageProof { + pub classes_proof: Vec, + pub contracts_proof: ContractsProof, + pub contracts_storage_proofs: Vec>, + pub global_roots: GlobalRoots, +} + +pub fn convert_storage_to_pathfinder_proof(storage_proof: StorageProof) -> PathfinderProof { + let contract_proof: Vec = storage_proof.contracts_proof.nodes.iter().map(convert_to_trie_node).collect(); + + let storage_proofs: Vec> = storage_proof + .contracts_storage_proofs + .iter() + .map(|nodes| nodes.iter().map(convert_to_trie_node).collect()) + .collect(); + + // This is a temporary solution due to the root not being explicitely returned + let contract_data = storage_proof + .contracts_storage_proofs + // Get the first "proof" only if it exists + .first() + // Filter out if the first proof is empty + .filter(|proof_nodes| !proof_nodes.is_empty()) + .map(|proof_nodes| { + let first_node = &proof_nodes[0]; + let root = first_node.node_hash; + + match &first_node.node { + MerkleNode::Binary { left, .. } if left.to_hex_string() == "0x0" => { + // If left is "0x0", then there is a root but storage_proofs is empty + ContractData { + root, + storage_proofs: vec![], + } + } + // For all other cases (including `MerkleNode::Edge`), + // store the computed `storage_proofs`. + _ => ContractData { root, storage_proofs }, + } + }); + + PathfinderProof { + state_commitment: None, + class_commitment: Some(storage_proof.global_roots.classes_tree_root), + contract_proof, + contract_data, + } +} + +pub fn convert_storage_to_pathfinder_class_proof(storage_proof: StorageProof) -> PathfinderClassProof { + let class_proof: Vec = storage_proof.classes_proof.iter().map(convert_to_trie_node).collect(); + PathfinderClassProof { class_commitment: storage_proof.classes_proof[0].node_hash, class_proof } +} + +pub fn convert_to_trie_node(node: &NodeHashToNodeMappingItem) -> TrieNode { + match node.node { + MerkleNode::Binary { left, right } => TrieNode::Binary { left, right }, + MerkleNode::Edge { child, path, length } => TrieNode::Edge { + child, + path: EdgePath { len: length.try_into().expect("length must be a valid integer"), value: path }, + }, + } +} diff --git a/crates/rpc-replay/tests/test_replay_block.rs b/crates/rpc-replay/tests/test_replay_block.rs index 93b6f497..3eccd91e 100644 --- a/crates/rpc-replay/tests/test_replay_block.rs +++ b/crates/rpc-replay/tests/test_replay_block.rs @@ -1,7 +1,10 @@ +use std::str::FromStr; + use blockifier::blockifier::block::GasPrices; use blockifier::state::cached_state::CachedState; use blockifier::transaction::transactions::ExecutableTransaction as _; use blockifier::versioned_constants::StarknetVersion; +use rpc_client::client::ClientVersion; use rpc_client::RpcClient; use rpc_replay::block_context::build_block_context; use rpc_replay::rpc_state_reader::AsyncRpcStateReader; @@ -22,7 +25,8 @@ async fn test_replay_block() { println!("block: {block_with_txs:?}"); let rpc_provider = "http://localhost:9545"; - let rpc_client = RpcClient::new(rpc_provider); + let rpc_version = ClientVersion::from_str("v0_7").unwrap(); + let rpc_client = RpcClient::new(rpc_provider, rpc_version); let previous_block_number = block_with_txs.block_number - 1; let previous_block_id = BlockId::Number(previous_block_number); let state_reader = AsyncRpcStateReader::new(rpc_client.clone(), previous_block_id);