|
| 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(¤t_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