Skip to content

Commit 6340321

Browse files
committed
starknet_patricia: add storage proof verification
1 parent c26c606 commit 6340321

4 files changed

Lines changed: 213 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/apollo_committer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tracing.workspace = true
2424
[dev-dependencies]
2525
assert_matches.workspace = true
2626
indexmap.workspace = true
27+
starknet_patricia = { workspace = true, features = ["testing"] }
2728
tokio.workspace = true
2829

2930
[lints]

crates/starknet_patricia/src/patricia_merkle_tree.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub mod errors;
22
pub mod filled_tree;
33
pub mod node_data;
44
pub mod original_skeleton_tree;
5+
#[cfg(feature = "testing")]
6+
pub mod storage_proof_verification;
57
pub mod traversal;
68
pub mod types;
79
pub mod updated_skeleton_tree;
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
use std::collections::{HashMap, VecDeque};
2+
3+
use starknet_api::hash::HashOutput;
4+
use thiserror::Error;
5+
6+
use crate::patricia_merkle_tree::node_data::inner_node::{
7+
BinaryData,
8+
EdgeData,
9+
NodeData,
10+
Preimage,
11+
PreimageMap,
12+
};
13+
use crate::patricia_merkle_tree::node_data::leaf::Leaf;
14+
use crate::patricia_merkle_tree::types::NodeIndex;
15+
use crate::patricia_merkle_tree::updated_skeleton_tree::hash_function::TreeHashFunction;
16+
17+
#[derive(Debug, PartialEq, Eq)]
18+
pub struct ProofIndexMaps {
19+
pub hash_by_index: HashMap<NodeIndex, HashOutput>,
20+
pub parent_by_index: HashMap<NodeIndex, NodeIndex>,
21+
}
22+
23+
#[derive(Debug, Error, PartialEq, Eq)]
24+
pub enum ProofVerificationError {
25+
#[error("missing leaf at index {index:?}")]
26+
MissingLeaf { index: NodeIndex },
27+
#[error("missing inner node on path at index {index:?}")]
28+
MissingNode { index: NodeIndex },
29+
#[error("hash mismatch at index {index:?}: proof {proof_value:?}, actual {actual:?}")]
30+
HashMismatch { index: NodeIndex, proof_value: HashOutput, actual: HashOutput },
31+
}
32+
33+
/// Verifies that `preimages` form valid Patricia paths from each accessed leaf to `root_hash`.
34+
pub fn verify_patricia_proof<L: Leaf, TH: TreeHashFunction<L>>(
35+
root_hash: HashOutput,
36+
preimages: &PreimageMap,
37+
leaf_indices: &[NodeIndex],
38+
leaf_hashes: &HashMap<NodeIndex, HashOutput>,
39+
) -> Result<(), ProofVerificationError> {
40+
if leaf_indices.is_empty() || root_hash == HashOutput::ROOT_OF_EMPTY_TREE {
41+
return Ok(());
42+
}
43+
44+
let maps = build_proof_index_maps::<L, TH>(root_hash, preimages)?;
45+
verify_leaf_paths(&maps, preimages, root_hash, leaf_indices, leaf_hashes)
46+
}
47+
48+
/// Builds `index -> hash` and `index -> parent` maps by expanding a Patricia proof from the root.
49+
///
50+
/// Verifies that the supplied hashes are consistent with the supplied preimages. This is, if the
51+
/// proof contains a path from the root to some leaf, it is a valid proof for that leaf.
52+
pub fn build_proof_index_maps<L: Leaf, TH: TreeHashFunction<L>>(
53+
root_hash: HashOutput,
54+
preimages: &PreimageMap,
55+
) -> Result<ProofIndexMaps, ProofVerificationError> {
56+
let mut hash_by_index = HashMap::from([(NodeIndex::ROOT, root_hash)]);
57+
let mut parent_by_index = HashMap::new();
58+
let mut queue = VecDeque::from([NodeIndex::ROOT]);
59+
60+
while let Some(index) = queue.pop_front() {
61+
let hash = hash_by_index[&index];
62+
63+
// Either a leaf (sibling or accessed) or a child of a node in `preimages` which is not
64+
// necessarily required to verify the proof.
65+
let Some(preimage) = preimages.get(&hash) else {
66+
continue;
67+
};
68+
69+
let computed_hash = TH::compute_node_hash(&preimage_to_node_data::<L>(preimage));
70+
if hash != computed_hash {
71+
return Err(ProofVerificationError::HashMismatch {
72+
index,
73+
proof_value: hash,
74+
actual: computed_hash,
75+
});
76+
}
77+
78+
match preimage {
79+
Preimage::Binary(binary) => {
80+
let [left_index, right_index] = index.get_children_indices();
81+
register_child(
82+
&mut hash_by_index,
83+
&mut parent_by_index,
84+
&mut queue,
85+
index,
86+
left_index,
87+
binary.left_data,
88+
);
89+
register_child(
90+
&mut hash_by_index,
91+
&mut parent_by_index,
92+
&mut queue,
93+
index,
94+
right_index,
95+
binary.right_data,
96+
);
97+
}
98+
Preimage::Edge(edge) => {
99+
let bottom_index = edge.path_to_bottom.bottom_index(index);
100+
register_child(
101+
&mut hash_by_index,
102+
&mut parent_by_index,
103+
&mut queue,
104+
index,
105+
bottom_index,
106+
edge.bottom_data,
107+
);
108+
}
109+
}
110+
}
111+
112+
Ok(ProofIndexMaps { hash_by_index, parent_by_index })
113+
}
114+
115+
/// Verifies that `preimages` contains a path from the root to each of the leaves in `leaf_indices`.
116+
fn verify_leaf_paths(
117+
maps: &ProofIndexMaps,
118+
preimages: &PreimageMap,
119+
root_hash: HashOutput,
120+
leaf_indices: &[NodeIndex],
121+
leaf_hashes: &HashMap<NodeIndex, HashOutput>,
122+
) -> Result<(), ProofVerificationError> {
123+
for leaf_index in leaf_indices {
124+
let actual_leaf_hash = leaf_hashes.get(leaf_index).ok_or_else(|| {
125+
panic!("leaf_hashes must contain every requested leaf index; missing {leaf_index:?}")
126+
})?;
127+
128+
match maps.hash_by_index.get(leaf_index) {
129+
None => return Err(ProofVerificationError::MissingLeaf { index: *leaf_index }),
130+
Some(proof_value) if *proof_value != *actual_leaf_hash => {
131+
return Err(ProofVerificationError::HashMismatch {
132+
index: *leaf_index,
133+
proof_value: *proof_value,
134+
actual: *actual_leaf_hash,
135+
});
136+
}
137+
Some(_) => {}
138+
}
139+
140+
let mut current_index = *leaf_index;
141+
while current_index != NodeIndex::ROOT {
142+
let parent_index = maps
143+
.parent_by_index
144+
.get(&current_index)
145+
.copied()
146+
.ok_or(ProofVerificationError::MissingNode { index: current_index })?;
147+
let parent_hash = maps
148+
.hash_by_index
149+
.get(&parent_index)
150+
.ok_or(ProofVerificationError::MissingNode { index: current_index })?;
151+
let preimage = preimages
152+
.get(parent_hash)
153+
.ok_or(ProofVerificationError::MissingNode { index: current_index })?;
154+
if !is_valid_child(current_index, parent_index, preimage) {
155+
return Err(ProofVerificationError::MissingNode { index: current_index });
156+
}
157+
current_index = parent_index;
158+
}
159+
}
160+
161+
match maps.hash_by_index.get(&NodeIndex::ROOT) {
162+
Some(proof_value) if *proof_value == root_hash => Ok(()),
163+
Some(proof_value) => Err(ProofVerificationError::HashMismatch {
164+
index: NodeIndex::ROOT,
165+
proof_value: *proof_value,
166+
actual: root_hash,
167+
}),
168+
None => Err(ProofVerificationError::MissingNode { index: NodeIndex::ROOT }),
169+
}
170+
}
171+
172+
fn register_child(
173+
hash_by_index: &mut HashMap<NodeIndex, HashOutput>,
174+
parent_by_index: &mut HashMap<NodeIndex, NodeIndex>,
175+
queue: &mut VecDeque<NodeIndex>,
176+
parent_index: NodeIndex,
177+
child_index: NodeIndex,
178+
child_hash: HashOutput,
179+
) {
180+
if hash_by_index.contains_key(&child_index) || parent_by_index.contains_key(&child_index) {
181+
unreachable!("child index {child_index:?} already registered");
182+
}
183+
hash_by_index.insert(child_index, child_hash);
184+
parent_by_index.insert(child_index, parent_index);
185+
queue.push_back(child_index);
186+
}
187+
188+
fn is_valid_child(child_index: NodeIndex, parent_index: NodeIndex, preimage: &Preimage) -> bool {
189+
match preimage {
190+
Preimage::Binary(_) => {
191+
let [left_index, right_index] = parent_index.get_children_indices();
192+
child_index == left_index || child_index == right_index
193+
}
194+
Preimage::Edge(edge) => child_index == edge.path_to_bottom.bottom_index(parent_index),
195+
}
196+
}
197+
198+
fn preimage_to_node_data<L: Leaf>(preimage: &Preimage) -> NodeData<L, HashOutput> {
199+
match preimage {
200+
Preimage::Binary(binary) => NodeData::Binary(BinaryData {
201+
left_data: binary.left_data,
202+
right_data: binary.right_data,
203+
}),
204+
Preimage::Edge(edge) => NodeData::Edge(EdgeData {
205+
bottom_data: edge.bottom_data,
206+
path_to_bottom: edge.path_to_bottom,
207+
}),
208+
}
209+
}

0 commit comments

Comments
 (0)