diff --git a/Cargo.lock b/Cargo.lock index 507d029327..33cc87dca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7553,6 +7553,7 @@ dependencies = [ "bitvec", "bytes", "dashmap", + "fake", "flate2", "futures", "gateway-test-utils", @@ -7572,6 +7573,7 @@ dependencies = [ "pathfinder-storage", "pretty_assertions_sorted", "primitive-types", + "rayon", "reqwest", "rstest", "serde", diff --git a/crates/pathfinder/src/sync.rs b/crates/pathfinder/src/sync.rs index da688c8cd2..2262274383 100644 --- a/crates/pathfinder/src/sync.rs +++ b/crates/pathfinder/src/sync.rs @@ -369,6 +369,7 @@ mod tests { calculate_receipt_commitment: Box::new(calculate_receipt_commitment), calculate_event_commitment: Box::new(calculate_event_commitment), update_tries: Box::new(update_starknet_state), + ..Default::default() }, ); (public_key, blocks) diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index f7c88f0902..228efaf67b 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -61,11 +61,13 @@ zstd = { workspace = true } assert_matches = { workspace = true } bitvec = { workspace = true } bytes = { workspace = true } +fake = { workspace = true } flate2 = { workspace = true } gateway-test-utils = { path = "../gateway-test-utils" } hex = { workspace = true } pathfinder-crypto = { path = "../crypto" } pretty_assertions_sorted = { workspace = true } +rayon = { workspace = true } rstest = { workspace = true } tempfile = { workspace = true } test-log = { workspace = true, features = ["trace"] } diff --git a/crates/rpc/src/method/get_storage_proof.rs b/crates/rpc/src/method/get_storage_proof.rs index ba18d53c5c..1b96f4f9ad 100644 --- a/crates/rpc/src/method/get_storage_proof.rs +++ b/crates/rpc/src/method/get_storage_proof.rs @@ -5,6 +5,7 @@ use pathfinder_common::trie::TrieNode; use pathfinder_common::{ BlockHash, BlockId, + BlockNumber, ClassHash, ContractAddress, ContractNonce, @@ -13,6 +14,7 @@ use pathfinder_common::{ use pathfinder_crypto::Felt; use pathfinder_merkle_tree::tree::GetProofError; use pathfinder_merkle_tree::{ClassCommitmentTree, ContractsStorageTree, StorageCommitmentTree}; +use pathfinder_storage::Transaction; use crate::context::RpcContext; use crate::dto::serialize::SerializeForVersion; @@ -155,7 +157,7 @@ impl SerializeForVersion for &NodeHashToNodeMapping { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] struct NodeHashToNodeMappings(Vec); impl SerializeForVersion for &NodeHashToNodeMappings { @@ -167,7 +169,7 @@ impl SerializeForVersion for &NodeHashToNodeMappings { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] struct ContractLeafData { nonce: ContractNonce, class_hash: ClassHash, @@ -185,7 +187,7 @@ impl SerializeForVersion for &ContractLeafData { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] struct ContractsProof { nodes: NodeHashToNodeMappings, contract_leaves_data: Vec, @@ -207,7 +209,7 @@ impl SerializeForVersion for ContractsProof { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] struct GlobalRoots { contracts_tree_root: Felt, classes_tree_root: Felt, @@ -227,7 +229,7 @@ impl SerializeForVersion for GlobalRoots { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Output { classes_proof: NodeHashToNodeMappings, contracts_proof: ContractsProof, @@ -309,130 +311,12 @@ pub async fn get_storage_proof(context: RpcContext, input: Input) -> Result return Err(Error::StorageProofNotSupported), - None => Felt::default(), - Some(hash) => hash, - }; - - let classes_proof = if let Some(class_hashes) = input.class_hashes { - let nodes: Vec = - ClassCommitmentTree::get_proofs(&tx, header.number, &class_hashes, class_root_idx)? - .into_iter() - .flatten() - .map(|(node, node_hash)| NodeHashToNodeMapping { - node_hash, - node: ProofNode(node), - }) - .collect::>() - .into_iter() - .collect(); - - NodeHashToNodeMappings(nodes) - } else { - NodeHashToNodeMappings(vec![]) - }; - - let storage_root_idx = tx - .storage_root_index(header.number) - .context("Querying storage root index")? - .ok_or(Error::StorageProofNotSupported)?; - - let storage_root_hash = match tx - .storage_trie_node_hash(storage_root_idx) - .context("Querying class root hash")? - { - None if input.contract_addresses.is_some() => { - return Err(Error::StorageProofNotSupported) - } - None => Felt::default(), - Some(hash) => hash, - }; - - let (contract_proof_nodes, contract_leaves_data) = - if let Some(contract_addresses) = input.contract_addresses { - let nodes = StorageCommitmentTree::get_proofs( - &tx, - header.number, - &contract_addresses, - storage_root_idx, - )? - .into_iter() - .flatten() - .map(|(node, node_hash)| NodeHashToNodeMapping { - node_hash, - node: ProofNode(node), - }) - .collect::>() - .into_iter() - .collect(); - - let contract_leaves_data = contract_addresses - .iter() - .map(|&address| { - let class_hash = tx - .contract_class_hash(header.number.into(), address) - .context("Querying contract's class hash")? - .unwrap_or_default(); - - let nonce = tx - .contract_nonce(address, header.number.into()) - .context("Querying contract's nonce")? - .unwrap_or_default(); - - Ok(ContractLeafData { nonce, class_hash }) - }) - .collect::, Error>>()?; - - (NodeHashToNodeMappings(nodes), contract_leaves_data) - } else { - (NodeHashToNodeMappings(vec![]), vec![]) - }; - - let contracts_storage_proofs = match input.contracts_storage_keys { - None => vec![], - Some(contracts_storage_keys) => { - let mut proofs = vec![]; - for csk in contracts_storage_keys { - let root = tx - .contract_root_index(header.number, csk.contract_address) - .context("Querying contract root index")?; - - if let Some(root) = root { - let nodes: Vec = ContractsStorageTree::get_proofs( - &tx, - csk.contract_address, - header.number, - &csk.storage_keys, - root, - )? - .into_iter() - .flatten() - .map(|(node, node_hash)| NodeHashToNodeMapping { - node_hash, - node: ProofNode(node), - }) - .collect::>() - .into_iter() - .collect(); - - proofs.push(NodeHashToNodeMappings(nodes)); - } else { - proofs.push(NodeHashToNodeMappings(vec![])); - } - } - - proofs - } - }; + let (class_root_hash, classes_proof) = + get_class_proofs(&tx, header.number, &input.class_hashes)?; + let (storage_root_hash, contract_proof_nodes, contract_leaves_data) = + get_contract_proofs(&tx, header.number, &input.contract_addresses)?; + let contracts_storage_proofs = + get_contract_storage_proofs(tx, &input.contracts_storage_keys, header.number)?; let contracts_proof = ContractsProof { nodes: contract_proof_nodes, @@ -456,10 +340,168 @@ pub async fn get_storage_proof(context: RpcContext, input: Input) -> Result, + block_number: BlockNumber, + class_hashes: &Option>, +) -> Result<(Felt, NodeHashToNodeMappings), Error> { + let Some(class_root_idx) = tx + .class_root_index(block_number) + .context("Querying class root index")? + else { + if tx.trie_pruning_enabled() { + return Err(Error::StorageProofNotSupported); + } else { + // Either: + // - the chain is empty (no declared classes) up to and including this block + // - or all leaves were removed resulting in an empty trie + // An empty proof is then a proof of non-membership in an empty block. + return Ok((Felt::default(), NodeHashToNodeMappings(vec![]))); + } + }; + + let class_root_hash = tx + .class_trie_node_hash(class_root_idx) + .context("Querying class root hash")? + .context("Class root hash missing")?; + + let Some(class_hashes) = class_hashes.as_ref() else { + return Ok((class_root_hash, NodeHashToNodeMappings(vec![]))); + }; + + let nodes: Vec = + ClassCommitmentTree::get_proofs(tx, block_number, class_hashes, class_root_idx)? + .into_iter() + .flatten() + .map(|(node, node_hash)| NodeHashToNodeMapping { + node_hash, + node: ProofNode(node), + }) + .collect::>() + .into_iter() + .collect(); + let classes_proof = NodeHashToNodeMappings(nodes); + + Ok((class_root_hash, classes_proof)) +} + +fn get_contract_proofs( + tx: &Transaction<'_>, + block_number: BlockNumber, + contract_addresses: &Option>, +) -> Result<(Felt, NodeHashToNodeMappings, Vec), Error> { + let Some(storage_root_idx) = tx + .storage_root_index(block_number) + .context("Querying storage root index")? + else { + if tx.trie_pruning_enabled() { + return Err(Error::StorageProofNotSupported); + } else { + // Either: + // - the chain is empty (no contract updates) up to and including this block + // - or all leaves were removed resulting in an empty trie + // An empty proof is then a proof of non-membership in an empty block. + return Ok((Felt::default(), NodeHashToNodeMappings(vec![]), vec![])); + } + }; + + let storage_root_hash = tx + .storage_trie_node_hash(storage_root_idx) + .context("Querying storage root hash")? + .context("Storage root hash missing")?; + + let Some(contract_addresses) = contract_addresses.as_ref() else { + return Ok((storage_root_hash, NodeHashToNodeMappings(vec![]), vec![])); + }; + + let nodes = + StorageCommitmentTree::get_proofs(tx, block_number, contract_addresses, storage_root_idx)? + .into_iter() + .flatten() + .map(|(node, node_hash)| NodeHashToNodeMapping { + node_hash, + node: ProofNode(node), + }) + .collect::>() + .into_iter() + .collect(); + + let contract_proof_nodes = NodeHashToNodeMappings(nodes); + + let contract_leaves_data = contract_addresses + .iter() + .map(|&address| { + let class_hash = tx + .contract_class_hash(block_number.into(), address) + .context("Querying contract's class hash")? + .unwrap_or_default(); + + let nonce = tx + .contract_nonce(address, block_number.into()) + .context("Querying contract's nonce")? + .unwrap_or_default(); + + Ok(ContractLeafData { nonce, class_hash }) + }) + .collect::, Error>>()?; + Ok(( + storage_root_hash, + contract_proof_nodes, + contract_leaves_data, + )) +} + +fn get_contract_storage_proofs( + tx: Transaction<'_>, + contracts_storage_keys: &Option>, + block_number: BlockNumber, +) -> Result, Error> { + Ok(match contracts_storage_keys { + None => vec![], + Some(contracts_storage_keys) => { + let mut proofs = vec![]; + for csk in contracts_storage_keys { + let root = tx + .contract_root_index(block_number, csk.contract_address) + .context("Querying contract root index")?; + + if let Some(root) = root { + let nodes: Vec = ContractsStorageTree::get_proofs( + &tx, + csk.contract_address, + block_number, + &csk.storage_keys, + root, + )? + .into_iter() + .flatten() + .map(|(node, node_hash)| NodeHashToNodeMapping { + node_hash, + node: ProofNode(node), + }) + .collect::>() + .into_iter() + .collect(); + + proofs.push(NodeHashToNodeMappings(nodes)); + } else { + proofs.push(NodeHashToNodeMappings(vec![])); + } + } + + proofs + } + }) +} + #[cfg(test)] mod tests { + use std::num::NonZeroU32; + use pathfinder_common::macro_prelude::*; use pathfinder_common::*; + use pathfinder_merkle_tree::starknet_state::update_starknet_state; + use pathfinder_storage::fake::{Block, Config, OccurrencePerBlock}; use super::*; use crate::dto::serialize::SerializeForVersion; @@ -731,9 +773,7 @@ mod tests { }]), }; - let output = get_storage_proof(context, input).await; - - assert!(output.is_ok()); + get_storage_proof(context, input).await.unwrap(); } #[tokio::test] @@ -765,4 +805,190 @@ mod tests { assert!(matches!(output, Err(Error::BlockNotFound))); } + + #[tokio::test] + async fn chain_without_declarations_and_contract_updates() { + let storage = pathfinder_storage::StorageBuilder::in_memory().unwrap(); + let blocks = pathfinder_storage::fake::generate::with_config( + 1, + Config { + update_tries: Box::new(update_starknet_state), + occurrence: OccurrencePerBlock { + cairo: 0..=0, + sierra: 0..=0, + storage: 0..=0, + nonce: 0..=0, + system_storage: 0..=0, + }, + ..Default::default() + }, + ); + pathfinder_storage::fake::fill(&storage, &blocks, Some(Box::new(update_starknet_state))); + + let context = RpcContext::for_tests().with_storage(storage); + + let input = Input { + block_id: BlockId::Number(BlockNumber::GENESIS), + class_hashes: Some(vec![class_hash!("0x1")]), + contract_addresses: Some(vec![contract_address!("0x2")]), + contracts_storage_keys: Some(vec![ContractStorageKeys { + contract_address: contract_address!("0x3"), + storage_keys: vec![storage_address!("0xabcd")], + }]), + }; + + let output = get_storage_proof(context, input).await.unwrap(); + + // We expect 3 empty proofs + let expected = Output { + classes_proof: NodeHashToNodeMappings(vec![]), + contracts_proof: ContractsProof { + nodes: NodeHashToNodeMappings(vec![]), + contract_leaves_data: vec![], + }, + contracts_storage_proofs: vec![NodeHashToNodeMappings(vec![])], + global_roots: GlobalRoots { + contracts_tree_root: Felt::ZERO, + classes_tree_root: Felt::ZERO, + block_hash: blocks.first().unwrap().header.header.hash, + }, + }; + + assert_eq!(output, expected); + } + + #[derive(Copy, Clone)] + enum PartialRequest { + Class, + ContractNonce, + ContractStorage, + } + + impl PartialRequest { + fn into_input(self, fake_blocks: &[Block]) -> Input { + let block = fake_blocks.get(1).unwrap(); + match self { + Self::Class => { + let class_hash = ClassHash( + block + .state_update + .as_ref() + .unwrap() + .declared_sierra_classes + .iter() + .next() + .unwrap() + .0 + .0, + ); + Input { + block_id: BlockId::Number(BlockNumber::new_or_panic(1)), + class_hashes: Some(vec![class_hash]), + contract_addresses: None, + contracts_storage_keys: None, + } + } + Self::ContractNonce => { + let contract_address = block + .state_update + .as_ref() + .unwrap() + .contract_updates + .keys() + .next() + .unwrap(); + Input { + block_id: BlockId::Number(BlockNumber::new_or_panic(1)), + class_hashes: None, + contract_addresses: Some(vec![*contract_address]), + contracts_storage_keys: None, + } + } + Self::ContractStorage => { + let (contract_address, update) = block + .state_update + .as_ref() + .unwrap() + .contract_updates + .iter() + .next() + .unwrap(); + Input { + block_id: BlockId::Number(BlockNumber::new_or_panic(1)), + class_hashes: None, + contract_addresses: None, + contracts_storage_keys: Some(vec![ContractStorageKeys { + contract_address: *contract_address, + storage_keys: update.storage.keys().cloned().collect(), + }]), + } + } + } + } + } + + #[rstest::rstest] + #[case::class_request(PartialRequest::Class)] + #[case::contract_request(PartialRequest::ContractNonce)] + #[case::contract_storage_request(PartialRequest::ContractStorage)] + #[tokio::test] + async fn partial_query(#[case] req: PartialRequest) { + use pathfinder_storage::fake::{fill, generate}; + use pathfinder_storage::{StorageBuilder, TriePruneMode}; + + let storage = StorageBuilder::in_tempdir_with_trie_pruning_and_pool_size( + TriePruneMode::Archive, + NonZeroU32::new(5).unwrap(), + ) + .unwrap(); + let blocks = generate::with_config( + 2, + Config { + update_tries: Box::new(update_starknet_state), + occurrence: OccurrencePerBlock { + cairo: 1..=10, + sierra: 1..=10, + storage: 1..=10, + nonce: 1..=10, + system_storage: 0..=0, + }, + ..Default::default() + }, + ); + + fill(&storage, &blocks, Some(Box::new(update_starknet_state))); + + let context = RpcContext::for_tests().with_storage(storage); + let input = req.into_input(&blocks); + + let output = get_storage_proof(context, input).await.unwrap(); + + match req { + PartialRequest::Class => { + // Some class proof should be present + assert!(!output.classes_proof.0.is_empty()); + // The rest should be empty + assert!(output.contracts_proof.nodes.0.is_empty()); + assert!(output.contracts_proof.contract_leaves_data.is_empty()); + assert!(output.contracts_storage_proofs.is_empty()); + } + PartialRequest::ContractNonce => { + // Some contract proof should be present + assert!(!output.contracts_proof.nodes.0.is_empty()); + // At least one nonce update occurred + assert!(!output.contracts_proof.contract_leaves_data.is_empty()); + // The rest should be empty + assert!(output.classes_proof.0.is_empty()); + assert!(output.contracts_storage_proofs.is_empty()); + } + PartialRequest::ContractStorage => { + // Some contract storage proof should be present + assert!(!output.contracts_storage_proofs.is_empty()); + // The rest should be empty + assert!(output.classes_proof.0.is_empty()); + assert!(output.contracts_proof.nodes.0.is_empty()); + assert!(output.contracts_proof.contract_leaves_data.is_empty()); + } + } + } } diff --git a/crates/rpc/src/pathfinder/methods/get_proof.rs b/crates/rpc/src/pathfinder/methods/get_proof.rs index 7d4103be00..c3b4fa41cd 100644 --- a/crates/rpc/src/pathfinder/methods/get_proof.rs +++ b/crates/rpc/src/pathfinder/methods/get_proof.rs @@ -101,7 +101,7 @@ struct PathWrapper { /// Wrapper around [`Vec`] as we don't control [TrieNode] in this /// crate. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct ProofNodes(Vec); impl Serialize for ProofNodes { @@ -198,7 +198,7 @@ pub struct GetProofOutput { contract_data: Option, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, PartialEq)] #[skip_serializing_none] pub struct GetClassProofOutput { /// Required to verify that the hash of the class commitment and the root of @@ -263,8 +263,24 @@ pub async fn get_proof( let storage_root_idx = tx .storage_root_index(header.number) - .context("Querying storage root index")? - .ok_or(GetProofError::ProofMissing)?; + .context("Querying storage root index")?; + + let Some(storage_root_idx) = storage_root_idx else { + if tx.trie_pruning_enabled() { + return Err(GetProofError::ProofMissing); + } else { + // Either: + // - the chain is empty (no contract updates) up to and including this block + // - or all leaves were removed resulting in an empty trie + // An empty proof is then a proof of non-membership in an empty block. + return Ok(GetProofOutput { + state_commitment, + class_commitment, + contract_proof: ProofNodes(vec![]), + contract_data: None, + }); + } + }; // Generate a proof for this contract. If the contract does not exist, this will // be a "non membership" proof. @@ -354,7 +370,7 @@ pub async fn get_proof( /// Returns all the necessary data to trustlessly verify class changes for a /// particular contract. -pub async fn get_proof_class( +pub async fn get_class_proof( context: RpcContext, input: GetClassProofInput, ) -> Result { @@ -393,8 +409,22 @@ pub async fn get_proof_class( let class_root_idx = tx .class_root_index(header.number) - .context("Querying class root index")? - .ok_or(GetProofError::ProofMissing)?; + .context("Querying class root index")?; + + let Some(class_root_idx) = class_root_idx else { + if tx.trie_pruning_enabled() { + return Err(GetProofError::ProofMissing); + } else { + // Either: + // - the chain is empty (no declared classes) up to and including this block + // - or all leaves were removed resulting in an empty trie + // An empty proof is then a proof of non-membership in an empty block. + return Ok(GetClassProofOutput { + class_commitment, + class_proof: ProofNodes(vec![]), + }); + } + }; // Generate a proof for this class. If the class does not exist, this will // be a "non membership" proof. @@ -417,65 +447,223 @@ pub async fn get_proof_class( #[cfg(test)] mod tests { + use std::num::NonZeroU32; + use pathfinder_common::macro_prelude::*; + use pathfinder_merkle_tree::starknet_state::update_starknet_state; use super::*; - #[tokio::test] - async fn limit_exceeded() { - let context = RpcContext::for_tests(); - let input = GetProofInput { - block_id: BlockId::Latest, - contract_address: contract_address!("0xdeadbeef"), - keys: (0..10_000) - .map(|idx| StorageAddress::new_or_panic(Felt::from_u64(idx))) - .collect(), - }; + mod get_proof { + use super::*; + + #[tokio::test] + async fn success() { + let context = RpcContext::for_tests(); + + let input = GetProofInput { + block_id: BlockId::Number(pathfinder_common::BlockNumber::GENESIS + 2), + contract_address: contract_address_bytes!(b"contract 2 (sierra)"), + keys: vec![storage_address_bytes!(b"storage addr 0")], + }; + + get_proof(context, input).await.unwrap(); + } + + #[tokio::test] + async fn limit_exceeded() { + let context = RpcContext::for_tests(); + let input = GetProofInput { + block_id: BlockId::Latest, + contract_address: contract_address!("0xdeadbeef"), + keys: (0..10_000) + .map(|idx| StorageAddress::new_or_panic(Felt::from_u64(idx))) + .collect(), + }; + + let err = get_proof(context, input).await.unwrap_err(); + assert_matches::assert_matches!(err, GetProofError::ProofLimitExceeded { .. }); + } + + #[tokio::test] + async fn proof_pruned() { + let context = + RpcContext::for_tests_with_trie_pruning(pathfinder_storage::TriePruneMode::Prune { + num_blocks_kept: 0, + }); + let mut conn = context.storage.connection().unwrap(); + let tx = conn.transaction().unwrap(); + + // Ensure that all storage tries are pruned, hence the node does not store + // historic proofs. + tx.insert_storage_trie( + &pathfinder_storage::TrieUpdate { + nodes_added: vec![(Felt::from_u64(0), pathfinder_storage::Node::LeafBinary)], + nodes_removed: (0..100).collect(), + root_commitment: Felt::ZERO, + }, + BlockNumber::GENESIS + 3, + ) + .unwrap(); + tx.commit().unwrap(); + let tx = conn.transaction().unwrap(); + tx.insert_storage_trie( + &pathfinder_storage::TrieUpdate { + nodes_added: vec![(Felt::from_u64(1), pathfinder_storage::Node::LeafBinary)], + nodes_removed: vec![], + root_commitment: Felt::ZERO, + }, + BlockNumber::GENESIS + 4, + ) + .unwrap(); + tx.commit().unwrap(); + drop(conn); + + let input = GetProofInput { + block_id: BlockId::Latest, + contract_address: contract_address_bytes!(b"contract 1"), + keys: vec![storage_address_bytes!(b"storage addr 0")], + }; + let err = get_proof(context, input).await.unwrap_err(); + assert_matches::assert_matches!(err, GetProofError::ProofMissing); + } - let err = get_proof(context, input).await.unwrap_err(); - assert_matches::assert_matches!(err, GetProofError::ProofLimitExceeded { .. }); + #[tokio::test] + async fn chain_without_contract_updates() { + let storage = + pathfinder_storage::StorageBuilder::in_memory_with_trie_pruning_and_pool_size( + pathfinder_storage::TriePruneMode::Archive, + NonZeroU32::new(5).unwrap(), + ) + .unwrap(); + let blocks = pathfinder_storage::fake::generate::with_config( + 1, + pathfinder_storage::fake::Config { + occurrence: pathfinder_storage::fake::OccurrencePerBlock { + nonce: 0..=0, + storage: 0..=0, + system_storage: 0..=0, + ..Default::default() + }, + update_tries: Box::new(update_starknet_state), + ..Default::default() + }, + ); + + pathfinder_storage::fake::fill( + &storage, + &blocks, + Some(Box::new(update_starknet_state)), + ); + + let context = RpcContext::for_tests().with_storage(storage); + + let input = GetProofInput { + block_id: BlockId::Latest, + contract_address: contract_address!("0xabcd"), + keys: vec![storage_address!("0x1234")], + }; + + let output = get_proof(context, input).await.unwrap(); + assert!(output.contract_proof.0.is_empty()); + assert!(output.contract_data.is_none()); + } } - #[tokio::test] - async fn proof_pruned() { - let context = - RpcContext::for_tests_with_trie_pruning(pathfinder_storage::TriePruneMode::Prune { + mod get_class_proof { + use pathfinder_storage::fake::{Config, OccurrencePerBlock}; + use pathfinder_storage::{StorageBuilder, TriePruneMode}; + + use super::*; + + #[tokio::test] + async fn success() { + let context = RpcContext::for_tests(); + + let input = GetClassProofInput { + block_id: BlockId::Number(pathfinder_common::BlockNumber::GENESIS + 2), + class_hash: class_hash_bytes!(b"class 2 hash (sierra)"), + }; + + get_class_proof(context, input).await.unwrap(); + } + + #[tokio::test] + async fn proof_pruned() { + let storage = StorageBuilder::in_memory_with_trie_pruning(TriePruneMode::Prune { num_blocks_kept: 0, - }); - let mut conn = context.storage.connection().unwrap(); - let tx = conn.transaction().unwrap(); - - // Ensure that all storage tries are pruned, hence the node does not store - // historic proofs. - tx.insert_storage_trie( - &pathfinder_storage::TrieUpdate { - nodes_added: vec![(Felt::from_u64(0), pathfinder_storage::Node::LeafBinary)], - nodes_removed: (0..100).collect(), - root_commitment: Felt::ZERO, - }, - BlockNumber::GENESIS + 3, - ) - .unwrap(); - tx.commit().unwrap(); - let tx = conn.transaction().unwrap(); - tx.insert_storage_trie( - &pathfinder_storage::TrieUpdate { - nodes_added: vec![(Felt::from_u64(1), pathfinder_storage::Node::LeafBinary)], - nodes_removed: vec![], - root_commitment: Felt::ZERO, - }, - BlockNumber::GENESIS + 4, - ) - .unwrap(); - tx.commit().unwrap(); - drop(conn); - - let input = GetProofInput { - block_id: BlockId::Latest, - contract_address: contract_address!("0xdeadbeef"), - keys: vec![storage_address_bytes!(b"storage addr 0")], - }; - let err = get_proof(context, input).await.unwrap_err(); - assert_matches::assert_matches!(err, GetProofError::ProofMissing); + }) + .unwrap(); + + let blocks = pathfinder_storage::fake::generate::with_config( + 1, + Config { + occurrence: OccurrencePerBlock { + sierra: 1..=10, + ..Default::default() + }, + ..Default::default() + }, + ); + pathfinder_storage::fake::fill( + &storage, &blocks, /* Simulates pruned tries */ None, + ); + + let context = RpcContext::for_tests().with_storage(storage); + let class_hash = ClassHash(blocks.first().unwrap().sierra_defs.first().unwrap().0 .0); + + let input = GetClassProofInput { + block_id: BlockId::Latest, + // Declared in the block but the tries are missing + class_hash, + }; + + let err = get_class_proof(context, input).await.unwrap_err(); + assert_matches::assert_matches!(err, GetProofError::ProofMissing); + } + + #[tokio::test] + async fn chain_without_class_declarations() { + let storage = + pathfinder_storage::StorageBuilder::in_memory_with_trie_pruning_and_pool_size( + pathfinder_storage::TriePruneMode::Archive, + NonZeroU32::new(5).unwrap(), + ) + .unwrap(); + let blocks = pathfinder_storage::fake::generate::with_config( + 1, + pathfinder_storage::fake::Config { + occurrence: pathfinder_storage::fake::OccurrencePerBlock { + cairo: 0..=0, + sierra: 0..=0, + ..Default::default() + }, + update_tries: Box::new(update_starknet_state), + ..Default::default() + }, + ); + + pathfinder_storage::fake::fill( + &storage, + &blocks, + Some(Box::new(update_starknet_state)), + ); + + let context = RpcContext::for_tests().with_storage(storage); + + let input = GetClassProofInput { + block_id: BlockId::Latest, + class_hash: class_hash!("0xabcd"), + }; + + let output = get_class_proof(context, input).await.unwrap(); + assert_eq!( + output, + GetClassProofOutput { + class_commitment: None, + class_proof: ProofNodes(vec![]) + } + ); + } } } diff --git a/crates/storage/src/connection.rs b/crates/storage/src/connection.rs index 3476be3be8..56eeb467e9 100644 --- a/crates/storage/src/connection.rs +++ b/crates/storage/src/connection.rs @@ -118,4 +118,8 @@ impl Transaction<'_> { pub fn commit(self) -> anyhow::Result<()> { Ok(self.transaction.commit()?) } + + pub fn trie_pruning_enabled(&self) -> bool { + matches!(self.trie_prune_mode, TriePruneMode::Prune { .. }) + } } diff --git a/crates/storage/src/fake.rs b/crates/storage/src/fake.rs index 57403c82e2..36736dec64 100644 --- a/crates/storage/src/fake.rs +++ b/crates/storage/src/fake.rs @@ -1,5 +1,6 @@ //! Create fake blockchain storage for test purposes use std::collections::{HashMap, HashSet}; +use std::ops::RangeInclusive; use fake::{Fake, Faker}; use pathfinder_common::event::Event; @@ -79,6 +80,16 @@ pub struct Config { pub calculate_receipt_commitment: ReceiptCommitmentFn, pub calculate_event_commitment: EventCommitmentFn, pub update_tries: UpdateTriesFn, + pub occurrence: OccurrencePerBlock, +} + +pub struct OccurrencePerBlock { + pub cairo: RangeInclusive, + pub sierra: RangeInclusive, + pub storage: RangeInclusive, + pub nonce: RangeInclusive, + /// Ranges longer than `0..=1` will be truncated to `0..=1` + pub system_storage: RangeInclusive, } impl Default for Config { @@ -90,6 +101,19 @@ impl Default for Config { calculate_receipt_commitment: Box::new(|_| Ok(Faker.fake())), calculate_event_commitment: Box::new(|_, _| Ok(Faker.fake())), update_tries: Box::new(|_, _, _, _, _| Ok((Faker.fake(), Faker.fake()))), + occurrence: Default::default(), + } + } +} + +impl Default for OccurrencePerBlock { + fn default() -> Self { + Self { + cairo: 0..=10, + sierra: 0..=10, + storage: 0..=10, + nonce: 0..=10, + system_storage: 0..=1, } } } @@ -225,6 +249,7 @@ pub mod generate { calculate_receipt_commitment, calculate_event_commitment, update_tries, + occurrence: min_per_block, } = config; let mut blocks = generate_inner( @@ -233,6 +258,7 @@ pub mod generate { calculate_transaction_commitment, calculate_receipt_commitment, calculate_event_commitment, + min_per_block, ); update_commitments(&mut blocks, update_tries); @@ -246,6 +272,7 @@ pub mod generate { calculate_transaction_commitment: TransactionCommitmentFn, calculate_receipt_commitment: ReceiptCommitmentFn, calculate_event_commitment: EventCommitmentFn, + occurrence: OccurrencePerBlock, ) -> Vec { let mut init = Vec::with_capacity(n); let mut declared_classes_accum = HashSet::new(); @@ -315,8 +342,8 @@ pub mod generate { .map(|(_, _, events)| events.len()) .sum(); - let num_cairo_classes = rng.gen_range(0..=0); - let num_sierra_classes = rng.gen_range(0..=10); + let num_cairo_classes = rng.gen_range(occurrence.cairo.clone()); + let num_sierra_classes = rng.gen_range(occurrence.sierra.clone()); let cairo_defs = (0..num_cairo_classes) .map(|_| { @@ -352,6 +379,22 @@ pub mod generate { .chain(declared_sierra_classes.keys().map(|x| ClassHash(x.0))) .collect::>(); + let storage_updates = rng.gen_range(occurrence.storage.clone()); + let nonce_updates = rng.gen_range(occurrence.nonce.clone()); + + let num_contract_updates = storage_updates.max(nonce_updates); + + let mut do_storage_update = vec![false; num_contract_updates]; + (0..num_contract_updates) + .choose_multiple(rng, storage_updates) + .into_iter() + .for_each(|i| do_storage_update[i] = true); + let mut do_nonce_update = vec![false; num_contract_updates]; + (0..num_contract_updates) + .choose_multiple(rng, nonce_updates) + .into_iter() + .for_each(|i| do_nonce_update[i] = true); + init.push(Block { header: SignedBlockHeader { header, @@ -367,29 +410,41 @@ pub mod generate { parent_state_commitment: StateCommitment::ZERO, declared_cairo_classes, declared_sierra_classes, - system_contract_updates: HashMap::from([( - ContractAddress::ONE, - SystemContractUpdate { - storage: fake_non_empty_with_rng(rng), - }, - )]), + system_contract_updates: if occurrence.system_storage.contains(&1) { + Some(( + ContractAddress::ONE, + SystemContractUpdate { + storage: fake_non_empty_with_rng(rng), + }, + )) + .into_iter() + .collect() + } else { + Default::default() + }, contract_updates: { // We can only deploy what was declared so far in the chain if declared_classes_accum.is_empty() { Default::default() } else { - Faker - .fake_with_rng::, _>(rng) - .into_iter() - .map(|contract_address| { + (0..num_contract_updates) + .map(|i| { ( - contract_address, + Faker.fake_with_rng::(rng), ContractUpdate { class: Some(ContractClassUpdate::Deploy( *declared_classes_accum.iter().choose(rng).unwrap(), )), - storage: fake_non_empty_with_rng(rng), - nonce: Faker.fake(), + storage: if do_storage_update[i] { + fake_non_empty_with_rng(rng) + } else { + Default::default() + }, + nonce: if do_nonce_update[i] { + Faker.fake() + } else { + Default::default() + }, }, ) }) diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 5078081c7e..c91178a6be 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -258,6 +258,22 @@ impl StorageBuilder { .create_pool(NonZeroU32::new(32).unwrap()) } + /// Convenience function for tests to create an in-tempdir database with a + /// specific trie prune mode. + pub fn in_tempdir_with_trie_pruning_and_pool_size( + trie_prune_mode: TriePruneMode, + pool_size: NonZeroU32, + ) -> anyhow::Result { + let db_dir = tempfile::TempDir::new()?; + let mut db_path = PathBuf::from(db_dir.path()); + db_path.push("db.sqlite"); + crate::StorageBuilder::file(db_path) + .trie_prune_mode(Some(trie_prune_mode)) + .migrate() + .unwrap() + .create_pool(pool_size) + } + /// Performs the database schema migration and returns a [storage /// manager](StorageManager). ///