From 53780cb7031ca42e50ada75a59429bbbaeaff979 Mon Sep 17 00:00:00 2001 From: Ariel Elperin Date: Tue, 2 Jun 2026 14:50:38 +0300 Subject: [PATCH] starknet_patricia: add storage proof verification --- Cargo.lock | 1 + crates/apollo_committer/Cargo.toml | 1 + .../src/patricia_merkle_tree.rs | 2 + .../storage_proof_verification.rs | 129 ++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 crates/starknet_patricia/src/patricia_merkle_tree/storage_proof_verification.rs diff --git a/Cargo.lock b/Cargo.lock index 436fe4d59b7..498a3763254 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1096,6 +1096,7 @@ dependencies = [ "indexmap 2.14.0", "starknet_api", "starknet_committer", + "starknet_patricia", "starknet_patricia_storage", "tokio", "tracing", diff --git a/crates/apollo_committer/Cargo.toml b/crates/apollo_committer/Cargo.toml index 22e76256be4..db064c11086 100644 --- a/crates/apollo_committer/Cargo.toml +++ b/crates/apollo_committer/Cargo.toml @@ -24,6 +24,7 @@ tracing.workspace = true [dev-dependencies] assert_matches.workspace = true indexmap.workspace = true +starknet_patricia = { workspace = true, features = ["testing"] } tokio.workspace = true [lints] diff --git a/crates/starknet_patricia/src/patricia_merkle_tree.rs b/crates/starknet_patricia/src/patricia_merkle_tree.rs index 8e6bd96bb50..69fe240e90e 100644 --- a/crates/starknet_patricia/src/patricia_merkle_tree.rs +++ b/crates/starknet_patricia/src/patricia_merkle_tree.rs @@ -2,6 +2,8 @@ pub mod errors; pub mod filled_tree; pub mod node_data; pub mod original_skeleton_tree; +#[cfg(feature = "testing")] +pub mod storage_proof_verification; pub mod traversal; pub mod types; pub mod updated_skeleton_tree; diff --git a/crates/starknet_patricia/src/patricia_merkle_tree/storage_proof_verification.rs b/crates/starknet_patricia/src/patricia_merkle_tree/storage_proof_verification.rs new file mode 100644 index 00000000000..b816e62483d --- /dev/null +++ b/crates/starknet_patricia/src/patricia_merkle_tree/storage_proof_verification.rs @@ -0,0 +1,129 @@ +use std::collections::{HashMap, VecDeque}; + +use starknet_api::hash::HashOutput; +use thiserror::Error; + +use crate::patricia_merkle_tree::node_data::inner_node::{ + BinaryData, + EdgeData, + NodeData, + Preimage, + PreimageMap, +}; +use crate::patricia_merkle_tree::node_data::leaf::Leaf; +use crate::patricia_merkle_tree::types::NodeIndex; +use crate::patricia_merkle_tree::updated_skeleton_tree::hash_function::TreeHashFunction; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ProofVerificationError { + #[error("missing leaf at index {index:?}")] + MissingLeaf { index: NodeIndex }, + #[error("hash mismatch at index {index:?}: proof {proof_value:?}, actual {actual:?}")] + HashMismatch { index: NodeIndex, proof_value: HashOutput, actual: HashOutput }, + #[error("duplicate parent for index {index:?}")] + DuplicateParent { index: NodeIndex }, +} + +/// Verifies that `preimages` form valid Patricia paths from each accessed leaf to `root_hash`. +pub fn verify_patricia_proof>( + root_hash: HashOutput, + preimages: &PreimageMap, + requested_leaves: &HashMap, +) -> Result<(), ProofVerificationError> { + if requested_leaves.is_empty() { + return Ok(()); + } + + let hash_by_index = build_proof_index_maps::(root_hash, preimages)?; + + // Once an index-->hash map is built, it suffices to verify that the expected leaf hashes are + // present in the map, since we already verified the validity of the path from the root to the + // leaf during the map construction. + for (leaf_index, actual_leaf_hash) in requested_leaves { + match hash_by_index.get(leaf_index) { + None => return Err(ProofVerificationError::MissingLeaf { index: *leaf_index }), + Some(proof_value) if *proof_value != *actual_leaf_hash => { + return Err(ProofVerificationError::HashMismatch { + index: *leaf_index, + proof_value: *proof_value, + actual: *actual_leaf_hash, + }); + } + Some(_) => {} + } + } + + Ok(()) +} + +/// Builds an `index -> hash` map by expanding a Patricia proof from the root. +/// +/// Verifies that the supplied hashes are consistent with the supplied preimages. +pub fn build_proof_index_maps>( + root_hash: HashOutput, + preimages: &PreimageMap, +) -> Result, ProofVerificationError> { + let mut hash_by_index = HashMap::from([(NodeIndex::ROOT, root_hash)]); + let mut queue = VecDeque::from([NodeIndex::ROOT]); + + while let Some(index) = queue.pop_front() { + let hash = hash_by_index[&index]; + + let Some(preimage) = preimages.get(&hash) else { + // We reach a leaf or a child-node of a node in `preimages` that is supposedly not + // required in the proof (e.g. sibling of a node with no requested leaves in + // its subtree). + continue; + }; + + let computed_hash = TH::compute_node_hash(&preimage_to_node_data::(preimage)); + if hash != computed_hash { + return Err(ProofVerificationError::HashMismatch { + index, + proof_value: hash, + actual: computed_hash, + }); + } + + match preimage { + Preimage::Binary(binary) => { + let [left_index, right_index] = index.get_children_indices(); + register_child(&mut hash_by_index, &mut queue, left_index, binary.left_data)?; + register_child(&mut hash_by_index, &mut queue, right_index, binary.right_data)?; + } + Preimage::Edge(edge) => { + let bottom_index = edge.path_to_bottom.bottom_index(index); + register_child(&mut hash_by_index, &mut queue, bottom_index, edge.bottom_data)?; + } + } + } + + Ok(hash_by_index) +} + +fn register_child( + hash_by_index: &mut HashMap, + queue: &mut VecDeque, + child_index: NodeIndex, + child_hash: HashOutput, +) -> Result<(), ProofVerificationError> { + if hash_by_index.contains_key(&child_index) { + return Err(ProofVerificationError::DuplicateParent { index: child_index }); + } + hash_by_index.insert(child_index, child_hash); + queue.push_back(child_index); + Ok(()) +} + +fn preimage_to_node_data(preimage: &Preimage) -> NodeData { + match preimage { + Preimage::Binary(binary) => NodeData::Binary(BinaryData { + left_data: binary.left_data, + right_data: binary.right_data, + }), + Preimage::Edge(edge) => NodeData::Edge(EdgeData { + bottom_data: edge.bottom_data, + path_to_bottom: edge.path_to_bottom, + }), + } +}