From dec197ead5c8533032322c7e0e4fbb5934c178ba Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Krieger Date: Thu, 19 Dec 2024 11:54:30 -0300 Subject: [PATCH] Fetch contract storage proof from v0_8 --- crates/bin/prove_block/src/lib.rs | 48 ++++++---- crates/bin/prove_block/src/reexecute.rs | 85 +++++++++-------- crates/bin/prove_block/src/rpc_utils.rs | 72 +++++++++++++- crates/rpc-client/src/pathfinder/client.rs | 45 ++++++++- crates/rpc-client/src/pathfinder/proofs.rs | 105 ++++++++++++++++++++- 5 files changed, 292 insertions(+), 63 deletions(-) diff --git a/crates/bin/prove_block/src/lib.rs b/crates/bin/prove_block/src/lib.rs index ea5de21a..2ca86ff1 100644 --- a/crates/bin/prove_block/src/lib.rs +++ b/crates/bin/prove_block/src/lib.rs @@ -14,7 +14,7 @@ use rpc_replay::block_context::build_block_context; use rpc_replay::rpc_state_reader::AsyncRpcStateReader; use rpc_replay::transactions::{starknet_rs_to_blockifier, ToBlockifierError}; use rpc_replay::utils::FeltConversionError; -use rpc_utils::{get_08_class_proofs, get_class_proofs, get_storage_proofs}; +use rpc_utils::{get_08_class_proofs, get_08_contracts_proofs, get_class_proofs, get_storage_proofs}; use starknet::core::types::{BlockId, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, StarknetError}; use starknet::providers::{Provider, ProviderError}; use starknet_api::StarknetApiError; @@ -119,11 +119,11 @@ fn compute_08_class_commitment( updated_root: Felt, ) -> CommitmentInfo { let previous_class_proofs: Vec<_> = - previous_class_proofs.classes_proof.clone().into_iter().map(|proof| proof.node).collect(); - let class_proofs: Vec<_> = class_proofs.classes_proof.clone().into_iter().map(|proof| proof.node).collect(); + previous_class_proofs.classes_proof.clone().into_iter().map(|proof| proof).collect(); + let class_proofs: Vec<_> = class_proofs.classes_proof.clone().into_iter().map(|proof| proof).collect(); - let previous_class_commitment_facts = format_08_commitment_facts::(&previous_class_proofs); - let current_class_commitment_facts = format_08_commitment_facts::(&class_proofs); + let previous_class_commitment_facts = format_08_commitment_facts::(&[previous_class_proofs]); + let current_class_commitment_facts = format_08_commitment_facts::(&[class_proofs]); let class_commitment_facts: HashMap<_, _> = previous_class_commitment_facts.into_iter().chain(current_class_commitment_facts).collect(); @@ -217,6 +217,15 @@ pub async fn prove_block( .await .expect("Failed to fetch storage proofs"); + let storage_proofs_08 = get_08_contracts_proofs(&rpc_client, block_number, &tx_execution_infos, old_block_number) + .await + .expect("Failed to fetch storage proofs"); + + let previous_storage_proofs_08 = + get_08_contracts_proofs(&rpc_client, block_number - 1, &tx_execution_infos, old_block_number) + .await + .expect("Failed to fetch storage proofs"); + let default_general_config = StarknetGeneralConfig::default(); let general_config = StarknetGeneralConfig { @@ -233,15 +242,10 @@ pub async fn prove_block( let mut contract_address_to_class_hash = HashMap::new(); // TODO: remove this clone() - for (contract_address, storage_proof) in storage_proofs.clone() { + for (contract_address, storage_proof) in storage_proofs_08.clone() { let previous_storage_proof = - previous_storage_proofs.get(&contract_address).expect("failed to find previous storage proof"); - let contract_storage_root = previous_storage_proof - .contract_data - .as_ref() - .map(|contract_data| contract_data.root) - .unwrap_or(Felt::ZERO) - .into(); + previous_storage_proofs_08.get(&contract_address).expect("failed to find previous storage proof"); + let contract_storage_root = previous_storage_proof.contracts_storage_proofs[0][0].node_hash.into(); log::debug!( "Storage root 0x{:x} for contract 0x{:x}", @@ -324,6 +328,13 @@ pub async fn prove_block( }) .collect(); + // We can extract data from any storage proof, use the one of the block hash contract + let block_hash_storage_proof_08 = + storage_proofs_08.get(&Felt::ONE).expect("there should be a storage proof for the block hash contract"); + let previous_block_hash_storage_proof_08 = previous_storage_proofs_08 + .get(&Felt::ONE) + .expect("there should be a storage proof for the block hash contract"); + // We can extract data from any storage proof, use the one of the block hash contract let block_hash_storage_proof = storage_proofs.get(&Felt::ONE).expect("there should be a storage proof for the block hash contract"); @@ -348,6 +359,11 @@ pub async fn prove_block( None => Felt252::ZERO, }; + let current_contract_trie_root_08 = + block_hash_storage_proof_08.contracts_proof.nodes[0].node.hash::(); + let previous_contract_trie_root_08 = + previous_block_hash_storage_proof_08.contracts_proof.nodes[0].node.hash::(); + let previous_contract_proofs: Vec<_> = previous_storage_proofs.values().map(|proof| proof.contract_proof.clone()).collect(); let previous_state_commitment_facts = format_commitment_facts::(&previous_contract_proofs); @@ -358,8 +374,8 @@ pub async fn prove_block( previous_state_commitment_facts.into_iter().chain(current_state_commitment_facts).collect(); let contract_state_commitment_info = CommitmentInfo { - previous_root: previous_contract_trie_root, - updated_root: current_contract_trie_root, + previous_root: previous_contract_trie_root_08, + updated_root: current_contract_trie_root_08, tree_height: 251, commitment_facts: global_state_commitment_facts, }; @@ -368,7 +384,7 @@ pub async fn prove_block( compute_class_commitment(&previous_class_proofs, &class_proofs, previous_root, updated_root); let contract_class_commitment_info_08 = - compute_08_class_commitment(&previous_class_proofs_08, &class_proofs_08, previous_root, updated_root); + compute_08_class_commitment(&previous_class_proofs_08, &class_proofs_08, previous_root_08, updated_root_08); let os_input = Rc::new(StarknetOsInput { contract_state_commitment_info, diff --git a/crates/bin/prove_block/src/reexecute.rs b/crates/bin/prove_block/src/reexecute.rs index f3d3eb2f..e7f1b66b 100644 --- a/crates/bin/prove_block/src/reexecute.rs +++ b/crates/bin/prove_block/src/reexecute.rs @@ -10,7 +10,7 @@ use blockifier::transaction::objects::TransactionExecutionInfo; use blockifier::transaction::transaction_execution::Transaction; use blockifier::transaction::transactions::ExecutableTransaction; use cairo_vm::Felt252; -use rpc_client::pathfinder::proofs::{ContractData, MerkleNode, PathfinderProof, PathfinderProof, TrieNode, TrieNode}; +use rpc_client::pathfinder::proofs::{MerkleNode, NodeHashToNodeMappingItem, StorageProof, TrieNode}; use rpc_client::RpcClient; use starknet::core::types::{BlockId, StarknetError}; use starknet::providers::{Provider as _, ProviderError}; @@ -95,8 +95,8 @@ pub(crate) struct ProverPerContractStorage { block_id: BlockId, contract_address: Felt252, previous_tree_root: Felt252, - storage_proof: PathfinderProof, - previous_storage_proof: PathfinderProof, + storage_proof: StorageProof, + previous_storage_proof: StorageProof, ongoing_storage_changes: HashMap, } @@ -106,8 +106,8 @@ impl ProverPerContractStorage { block_id: BlockId, contract_address: Felt252, previous_tree_root: Felt252, - storage_proof: PathfinderProof, - previous_storage_proof: PathfinderProof, + storage_proof: StorageProof, + previous_storage_proof: StorageProof, ) -> Result { Ok(Self { rpc_client, @@ -161,39 +161,43 @@ pub(crate) fn format_commitment_facts( } pub(crate) fn format_08_commitment_facts( - trie_nodes: &Vec, + trie_nodes: &[Vec], ) -> HashMap> { let mut facts = HashMap::new(); - for node in trie_nodes { - let (key, fact_as_tuple) = match node { - MerkleNode::Binary { left, right } => { - let fact = BinaryNodeFact::new((*left).into(), (*right).into()) - .expect("storage proof endpoint gave us an invalid binary node"); + for nodes in trie_nodes { + for node in nodes { + let (key, fact_as_tuple) = match node.node { + MerkleNode::Binary { left, right } => { + let fact = BinaryNodeFact::new((left).into(), (right).into()) + .expect("storage proof endpoint gave us an invalid binary node"); - // TODO: the hash function should probably be split from the Fact trait. - // we use a placeholder for the Storage trait in the meantime. - let node_hash = Felt252::from(>::hash(&fact)); - let fact_as_tuple = >::to_tuple(&fact); + // TODO: the hash function should probably be split from the Fact trait. + // we use a placeholder for the Storage trait in the meantime. + let node_hash = Felt252::from(>::hash(&fact)); + let fact_as_tuple = >::to_tuple(&fact); - (node_hash, fact_as_tuple) - } - MerkleNode::Edge { child, path, length } => { - let len = *length; - let fact = - EdgeNodeFact::new((*child).into(), NodePath(path.to_biguint()), Length(len.try_into().unwrap())) - .expect("storage proof endpoint gave us an invalid edge node"); - // TODO: the hash function should probably be split from the Fact trait. - // we use a placeholder for the Storage trait in the meantime. - let node_hash = Felt252::from(>::hash(&fact)); - let fact_as_tuple = >::to_tuple(&fact); + (node_hash, fact_as_tuple) + } + MerkleNode::Edge { child, path, length } => { + let fact = EdgeNodeFact::new( + (child).into(), + NodePath(path.to_biguint()), + Length(length.try_into().unwrap()), + ) + .expect("storage proof endpoint gave us an invalid edge node"); + // TODO: the hash function should probably be split from the Fact trait. + // we use a placeholder for the Storage trait in the meantime. + let node_hash = Felt252::from(>::hash(&fact)); + let fact_as_tuple = >::to_tuple(&fact); - (node_hash, fact_as_tuple) - } - }; + (node_hash, fact_as_tuple) + } + }; - let fact_as_tuple_of_felts: Vec<_> = fact_as_tuple.into_iter().map(Felt252::from).collect(); - facts.insert(key, fact_as_tuple_of_felts); + let fact_as_tuple_of_felts: Vec<_> = fact_as_tuple.into_iter().map(Felt252::from).collect(); + facts.insert(key, fact_as_tuple_of_felts); + } } facts @@ -202,20 +206,15 @@ pub(crate) fn format_08_commitment_facts( impl PerContractStorage for ProverPerContractStorage { async fn compute_commitment(&mut self) -> Result { // TODO: error code - let contract_data = match self.storage_proof.contract_data.as_ref() { - None => &ContractData::default(), - Some(data) => data, - }; + let contract_data: &[Vec] = self.storage_proof.contracts_storage_proofs.as_ref(); + let updated_root = contract_data[0][0].node_hash; - let updated_root = contract_data.root; + let commitment_facts = format_08_commitment_facts::(&contract_data); - let commitment_facts = format_commitment_facts::(&contract_data.storage_proofs); - - let previous_commitment_facts = match &self.previous_storage_proof.contract_data { - None => HashMap::default(), - Some(previous_contract_data) => { - format_commitment_facts::(&previous_contract_data.storage_proofs) - } + let previous_commitment_facts = if self.previous_storage_proof.contracts_storage_proofs.is_empty() { + HashMap::default() + } else { + format_08_commitment_facts::(&self.previous_storage_proof.contracts_storage_proofs) }; let commitment_facts = commitment_facts.into_iter().chain(previous_commitment_facts.into_iter()).collect(); diff --git a/crates/bin/prove_block/src/rpc_utils.rs b/crates/bin/prove_block/src/rpc_utils.rs index e4a18226..3e4ef9a3 100644 --- a/crates/bin/prove_block/src/rpc_utils.rs +++ b/crates/bin/prove_block/src/rpc_utils.rs @@ -5,7 +5,8 @@ use cairo_vm::Felt252; use num_bigint::BigInt; use rpc_client::pathfinder::client::ClientError; use rpc_client::pathfinder::proofs::{ - ContractData, EdgePath, PathfinderClassProof, PathfinderProof, ProofVerificationError, StorageProof, TrieNode, + ContractData, ContractStorageKeysItem, EdgePath, MerkleNode, PathfinderClassProof, PathfinderProof, + ProofVerificationError, StorageProof, TrieNode, }; use rpc_client::RpcClient; use starknet::core::types::BlockWithTxs; @@ -43,6 +44,22 @@ async fn fetch_storage_proof_for_contract( Ok(storage_proof) } +/// 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. +async fn fetch_storage_proof_for_contract_08( + rpc_client: &RpcClient, + block_number: u64, + contract_address: Felt, + keys: &[Felt], +) -> Result { + let request_storage_keys = ContractStorageKeysItem { contract_address, storage_keys: keys.to_vec() }; + rpc_client + .pathfinder_rpc() + .get_contract_storage_proof(block_number, &[contract_address], &[request_storage_keys]) + .await +} + /// 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. @@ -64,6 +81,7 @@ async fn get_storage_proof_for_contract>( } 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 @@ -201,6 +219,17 @@ fn get_key_following_edge(key: Felt, height: Height, edge_path: &EdgePath) -> Fe Felt::from(new_key) } +fn get_key_following_edge_08(key: Felt, height: Height, length: usize, path: Felt) -> Felt { + assert!(height.0 < DEFAULT_STORAGE_TREE_HEIGHT); + + let shift = height.0; + let clear_mask = ((BigInt::from(1) << length) - BigInt::from(1)) << shift; + let mask = path.to_bigint() << shift; + let new_key = (key.to_bigint() & !clear_mask) | mask; + + Felt::from(new_key) +} + fn merge_storage_proofs(proofs: Vec) -> PathfinderProof { let class_commitment = proofs[0].class_commitment; let state_commitment = proofs[0].state_commitment; @@ -248,6 +277,47 @@ pub(crate) async fn get_08_class_proofs( rpc_client.pathfinder_rpc().get_storage_class_proof(block_number, class_hashes).await } +pub(crate) async fn get_08_contracts_proofs( + rpc_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()); + keys + }; + + let mut storage_proofs = HashMap::new(); + + 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_08(rpc_client, block_number, contract_address_felt, &keys).await?; + + let additional_keys = verify_storage_proof_08(&storage_proof, &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_08(rpc_client, block_number, contract_address_felt, &additional_keys) + .await?; + + storage_proof.contracts_storage_proofs.extend(additional_proof.contracts_storage_proofs); + } + + storage_proofs.insert(contract_address_felt, storage_proof); + } + + Ok(storage_proofs) +} + pub(crate) fn get_starknet_version(block_with_txs: &BlockWithTxs) -> blockifier::versioned_constants::StarknetVersion { let starknet_version_str = &block_with_txs.starknet_version; match starknet_version_str.as_ref() { diff --git a/crates/rpc-client/src/pathfinder/client.rs b/crates/rpc-client/src/pathfinder/client.rs index 924a389f..e3d33ca9 100644 --- a/crates/rpc-client/src/pathfinder/client.rs +++ b/crates/rpc-client/src/pathfinder/client.rs @@ -5,7 +5,7 @@ use serde_json::json; use starknet::core::types::TransactionTraceWithHash; use starknet_types_core::felt::Felt; -use crate::pathfinder::proofs::{PathfinderClassProof, PathfinderProof, StorageProof}; +use crate::pathfinder::proofs::{ContractStorageKeysItem, PathfinderClassProof, PathfinderProof, StorageProof}; #[derive(Debug, thiserror::Error)] pub enum ClientError { @@ -150,6 +150,49 @@ impl PathfinderRpcClient { .await } + pub async fn get_storage_contracts_addresses( + &self, + block_number: u64, + contract_addresses: &[Felt], + ) -> Result { + post_jsonrpc_08_request( + &self.http_client, + &self.rpc_base_url, + "starknet_getStorageProof", + json!({ "block_id": { "block_number": block_number }, "contract_addresses": contract_addresses }), + ) + .await + } + + pub async fn get_storage_contracts_keys( + &self, + block_number: u64, + contracts_storage_keys: &[ContractStorageKeysItem], + ) -> Result { + post_jsonrpc_08_request( + &self.http_client, + &self.rpc_base_url, + "starknet_getStorageProof", + json!({ "block_id": { "block_number": block_number }, "contracts_storage_keys": contracts_storage_keys }), + ) + .await + } + + pub async fn get_contract_storage_proof( + &self, + block_number: u64, + contract_addresses: &[Felt], + contracts_storage_keys: &[ContractStorageKeysItem], + ) -> Result { + post_jsonrpc_08_request( + &self.http_client, + &self.rpc_base_url, + "starknet_getStorageProof", + json!({ "block_id": { "block_number": block_number }, "contract_addresses": contract_addresses, "contracts_storage_keys": contracts_storage_keys }), + ) + .await + } + pub async fn get_block_traces(&self, block_number: u64) -> Result, ClientError> { post_jsonrpc_07_request( &self.http_client, diff --git a/crates/rpc-client/src/pathfinder/proofs.rs b/crates/rpc-client/src/pathfinder/proofs.rs index b7feddd8..b0c787da 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; @@ -51,6 +51,9 @@ 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("Non-inclusion proof for key {}. Height {}.", key.to_hex_string(), height.0)] + NonExistenceProof08 { key: Felt, height: Height, proof: &'a [NodeHashToNodeMappingItem] }, + #[error("Proof verification failed, node_hash {node_hash:x} != parent_hash {parent_hash:x}")] InvalidChildNodeHash { node_hash: Felt, parent_hash: Felt }, @@ -157,7 +160,7 @@ pub fn verify_proof( Ok(()) } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct ContractStorageKeysItem { pub contract_address: Felt, pub storage_keys: Vec, @@ -170,6 +173,30 @@ pub enum MerkleNode { Edge { child: Felt, path: Felt, length: usize }, } +impl MerkleNode { + pub fn hash(&self) -> Felt { + match self { + MerkleNode::Binary { left, right } => { + let fact = BinaryNodeFact::new((*left).into(), (*right).into()) + .expect("storage proof endpoint gave us an invalid binary node"); + + // TODO: the hash function should probably be split from the Fact trait. + // we use a placeholder for the Storage trait in the meantime. + Felt::from(>::hash(&fact)) + } + MerkleNode::Edge { child, path, length } => { + let len = *length; + let fact = + EdgeNodeFact::new((*child).into(), NodePath(path.to_biguint()), Length(len.try_into().unwrap())) + .expect("storage proof endpoint gave us an invalid edge node"); + // TODO: the hash function should probably be split from the Fact trait. + // we use a placeholder for the Storage trait in the meantime. + Felt::from(>::hash(&fact)) + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct NodeHashToNodeMappingItem { pub node_hash: Felt, @@ -202,3 +229,77 @@ pub struct StorageProof { pub contracts_storage_proofs: Vec>, pub global_roots: GlobalRoots, } + +impl StorageProof { + /// Verifies that each contract state proof is valid. + pub fn verify(&self, storage_keys: &[Felt]) -> Result<(), Vec> { + let mut errors = vec![]; + + for (index, storage_key) in storage_keys.iter().enumerate() { + if let Err(e) = verify_proof_08::( + *storage_key, + self.contracts_storage_proofs[index][0].node_hash, + &self.contracts_storage_proofs[index], + ) { + errors.push(e); + } + } + + if errors.is_empty() { Ok(()) } else { Err(errors) } + } +} + +/// 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_08( + key: Felt, + commitment: Felt, + proof: &[NodeHashToNodeMappingItem], +) -> Result<(), ProofVerificationError> { + let bits = key.to_bits_be(); + + let mut parent_hash = commitment; + + // The tree height is 251, so the first 5 bits are ignored. + let start = 5; + let mut index = start; + + for node in proof.iter() { + let node_hash = node.node.hash::(); + if node_hash != parent_hash { + return Err(ProofVerificationError::InvalidChildNodeHash { node_hash, parent_hash }); + } + + match node.node { + MerkleNode::Binary { left, right } => { + parent_hash = if bits[index as usize] { right } else { left }; + index += 1; + } + MerkleNode::Edge { child, path, length } => { + let index_usize: usize = index.try_into().map_err(|_| ProofVerificationError::ConversionError)?; + + let path_bits = path.to_bits_be(); + let relevant_path_bits = &path_bits[path_bits.len() - length..]; + let key_bits_slice = &bits[index_usize..(index_usize + length)]; + + parent_hash = child; + index += length; + let height: u64 = (index - start).try_into().unwrap(); + + if relevant_path_bits != key_bits_slice { + // If paths don't match, we've found a proof of non-membership because: + // 1. We correctly moved towards the target as far as possible, and + // 2. Hashing all the nodes along the path results in the root hash, which means + // 3. The target definitely does not exist in this tree + return Err(ProofVerificationError::NonExistenceProof08 { + key, + height: Height(DEFAULT_STORAGE_TREE_HEIGHT - height), + proof, + }); + } + } + } + } + + Ok(()) +}