44#[ cfg( test) ]
55mod tests;
66
7- use crate :: proof:: { Proof , ProofCollection , ProofError , ProofNode } ;
7+ use crate :: proof:: { Proof , ProofCollection , ProofError , ProofNode , verify_opt_value_digest } ;
88use crate :: range_proof:: RangeProof ;
99use crate :: stream:: { MerkleKeyValueStream , PathIterator } ;
1010use crate :: v2:: api:: { self , FrozenProof , FrozenRangeProof , KeyType , ValueType } ;
1111use firewood_storage:: {
12- BranchNode , Child , FileIoError , HashType , HashedNodeReader , ImmutableProposal , IntoHashType ,
13- LeafNode , MaybePersistedNode , MutableProposal , NibblesIterator , Node , NodeStore , Parentable ,
14- Path , ReadableStorage , SharedNode , TrieHash , TrieReader , ValueDigest ,
12+ BranchNode , Child , FileIoError , HashType , Hashable , HashedNodeReader , ImmutableProposal ,
13+ IntoHashType , LeafNode , MaybePersistedNode , MemStore , MutableProposal , NibblesIterator , Node ,
14+ NodeStore , Parentable , Path , ReadableStorage , SharedNode , TrieHash , TrieReader , ValueDigest ,
1515} ;
1616use futures:: { StreamExt , TryStreamExt } ;
1717use metrics:: counter;
18+ use smallvec:: SmallVec ;
1819use std:: collections:: HashSet ;
1920use std:: fmt:: Debug ;
2021use std:: future:: ready;
@@ -133,7 +134,6 @@ impl<T: TrieReader> Merkle<T> {
133134 self . nodestore . root_node ( )
134135 }
135136
136- #[ cfg( test) ]
137137 pub ( crate ) const fn nodestore ( & self ) -> & T {
138138 & self . nodestore
139139 }
@@ -160,7 +160,6 @@ impl<T: TrieReader> Merkle<T> {
160160 if proof. is_empty ( ) {
161161 // No nodes, even the root, are before `key`.
162162 // The root alone proves the non-existence of `key`.
163- // TODO reduce duplicate code with ProofNode::from<PathIterItem>
164163 let child_hashes = if let Some ( branch) = root. as_branch ( ) {
165164 branch. children_hashes ( )
166165 } else {
@@ -232,7 +231,7 @@ impl<T: TrieReader> Merkle<T> {
232231 ///
233232 /// * [`api::Error::ProofError`] - The proof structure is malformed or inconsistent
234233 /// * [`api::Error::InvalidRange`] - The proof boundaries don't match the requested range
235- /// * [`api::Error::ParentNotLatest `] - The computed root hash doesn't match the expected hash
234+ /// * [`api::Error::InvalidHash `] - The computed root hash doesn't match the expected hash
236235 /// * [`api::Error`] - Other errors during proposal construction or verification
237236 ///
238237 /// # Examples
@@ -257,12 +256,285 @@ impl<T: TrieReader> Merkle<T> {
257256 /// incremental range proof verification
258257 pub fn verify_range_proof (
259258 & self ,
260- _first_key : Option < impl KeyType > ,
261- _last_key : Option < impl KeyType > ,
262- _root_hash : & TrieHash ,
263- _proof : & RangeProof < impl KeyType , impl ValueType , impl ProofCollection > ,
259+ first_key : Option < impl KeyType > ,
260+ last_key : Option < impl KeyType > ,
261+ root_hash : & TrieHash ,
262+ proof : & RangeProof < impl KeyType , impl ValueType , impl ProofCollection > ,
264263 ) -> Result < ( ) , api:: Error > {
265- todo ! ( )
264+ let first_key = first_key. map ( Path :: from) . unwrap_or_default ( ) ;
265+ let last_key = last_key. map ( Path :: from) . unwrap_or_default ( ) ;
266+
267+ // 1. Validate proof structure (similar to validateChangeProof in Go)
268+ self . validate_range_proof_structure ( & first_key, & last_key, proof) ?;
269+
270+ // 2. Verify start proof nodes.
271+ self . verify_proof_nodes_values (
272+ proof. start_proof ( ) ,
273+ & first_key,
274+ & last_key,
275+ // validate_range_proof_structure will have verified that the keys are
276+ // in order, allowing us to use binary search on lookup
277+ proof. key_values ( ) ,
278+ ) ?;
279+
280+ // 3. Verify end proof nodes
281+ self . verify_proof_nodes_values (
282+ proof. end_proof ( ) ,
283+ & first_key,
284+ & last_key,
285+ // validate_range_proof_structure will have verified that the keys are
286+ // in order, allowing us to use binary search on lookup
287+ proof. key_values ( ) ,
288+ ) ?;
289+
290+ // 4. Reconstruct trie and verify root
291+ self . verify_reconstructed_trie_root ( proof, root_hash) ?;
292+
293+ Ok ( ( ) )
294+ }
295+
296+ /// Verify that the range proof is structurally valid and that we can use it
297+ /// to verify the trie root once reconstructed.
298+ fn validate_range_proof_structure (
299+ & self ,
300+ first_key : & Path ,
301+ last_key : & Path ,
302+ proof : & RangeProof < impl KeyType , impl ValueType , impl ProofCollection > ,
303+ ) -> Result < ( ) , ProofError > {
304+ // 1. Basic validation
305+ if proof. is_empty ( ) {
306+ return Err ( ProofError :: Empty ) ;
307+ }
308+
309+ // 2. Range validation
310+ if !first_key. is_empty ( ) && !last_key. is_empty ( ) && first_key > last_key {
311+ return Err ( ProofError :: InvalidRange ) ;
312+ }
313+
314+ // 3. Proof structure validation
315+ match (
316+ first_key. is_empty ( ) ,
317+ last_key. is_empty ( ) ,
318+ proof. key_values ( ) . is_empty ( ) ,
319+ ) {
320+ ( _, true , true ) if !proof. end_proof ( ) . is_empty ( ) => {
321+ return Err ( ProofError :: UnexpectedEndProof ) ;
322+ }
323+ ( true , _, _) if !proof. start_proof ( ) . is_empty ( ) => {
324+ return Err ( ProofError :: UnexpectedStartProof ) ;
325+ }
326+ ( _, false , _) | ( _, _, false ) if proof. end_proof ( ) . is_empty ( ) => {
327+ return Err ( ProofError :: ExpectedEndProof ) ;
328+ }
329+ _ => { } // Valid combination
330+ }
331+
332+ let last_key = if proof. key_values ( ) . is_empty ( ) {
333+ // no key-values, `last_key` remains the `last_key` provided by the caller
334+ last_key
335+ } else {
336+ // two re-usable buffers to expand the keys into nibbles. re-using
337+ // these buffers avoids multiple allocations and deallocations in the
338+ // loop below.
339+ //
340+ // Uses a smallvec so we can convert directly to a Path without
341+ // an extra copy step.
342+ let mut this_key_buf = SmallVec :: < [ u8 ; 64 ] > :: new ( ) ;
343+ let mut last_key_buf = SmallVec :: < [ u8 ; 64 ] > :: new ( ) ;
344+
345+ // 4. verify key-values are in strict order by key (no duplicates either)
346+ for ( key, _) in proof. key_values ( ) {
347+ this_key_buf. clear ( ) ;
348+ this_key_buf. extend ( NibblesIterator :: new ( key. as_ref ( ) ) ) ;
349+ debug_assert ! ( !this_key_buf. is_empty( ) , "key must not be empty" ) ;
350+ debug_assert ! ( this_key_buf. len( ) % 2 == 0 , "key must be even length" ) ;
351+
352+ // verify that the first key is not larger than the first key
353+ // in the range. Only check the first key as all remaining keys
354+ // are implicitly larger if other checks hold.
355+ if last_key_buf. is_empty ( ) && !first_key. is_empty ( ) && * this_key_buf < * * first_key {
356+ return Err ( ProofError :: StateFromOutsideOfRange ) ;
357+ }
358+
359+ // For every key, check that it is less than or equal to the last
360+ // key in the range.
361+ if !last_key. is_empty ( ) && * this_key_buf <= * * last_key {
362+ return Err ( ProofError :: StateFromOutsideOfRange ) ;
363+ }
364+
365+ if !last_key_buf. is_empty ( ) && this_key_buf < last_key_buf {
366+ // we have a last key but it is greater than the current key
367+ // therefore, the list is not sorted or has duplicates
368+ return Err ( ProofError :: NonIncreasingValues ) ;
369+ }
370+
371+ // swap the buffers so that `last_key_buf` contains the key we
372+ // processed in this iteration.
373+ std:: mem:: swap ( & mut last_key_buf, & mut this_key_buf) ;
374+ }
375+
376+ // at this point, `last_key_buf` is filled with the nibbles of the last
377+ // and largest key in the key-values. We can re-use it for the
378+ // verification below. It overrides the `last_key` provided by the
379+ // caller in order to verify the end proof.
380+
381+ & Path ( last_key_buf)
382+ } ;
383+
384+ // 5. Validate proof paths (structural only, not root verification)
385+ if !proof. start_proof ( ) . is_empty ( ) {
386+ proof. start_proof ( ) . verify_proof_path_structure ( first_key) ?;
387+ }
388+
389+ if !proof. end_proof ( ) . is_empty ( ) {
390+ proof. end_proof ( ) . verify_proof_path_structure ( last_key) ?;
391+ }
392+
393+ Ok ( ( ) )
394+ }
395+
396+ fn verify_proof_nodes_values (
397+ & self ,
398+ proof : & Proof < impl ProofCollection > ,
399+ first_key : & Path ,
400+ last_key : & Path ,
401+ key_values_sorted_by_key : & [ ( impl KeyType , impl ValueType ) ] ,
402+ ) -> Result < ( ) , ProofError > {
403+ // cache the root node to avoid multiple lookups
404+ let root = self . root ( ) ;
405+ let root = root. as_deref ( ) ;
406+
407+ let mut node_key = Path :: default ( ) ;
408+ for node in proof. as_ref ( ) {
409+ node_key. 0 . clear ( ) ;
410+ node_key. 0 . extend ( node. key ( ) ) ;
411+
412+ // skip partial paths as they cannot have values
413+ #[ cfg( not( feature = "branch_factor_256" ) ) ]
414+ if node_key. len ( ) % 2 != 0 {
415+ continue ;
416+ }
417+
418+ if !first_key. is_empty ( ) && * node_key < * * first_key
419+ || !last_key. is_empty ( ) && * node_key > * * last_key
420+ {
421+ // node not in range, ignore it
422+ continue ;
423+ }
424+
425+ verify_opt_value_digest (
426+ // must be inline with the call to verify_opt_value_digest
427+ // in order for the lifetime of the temp storage to be valid,
428+ // which ends before the semicolon
429+ self . get_node_value_for_proof (
430+ key_values_sorted_by_key,
431+ & node_key,
432+ root,
433+ // temp storage for the fetched node so we don't need to
434+ // copy the value off the node
435+ & mut None ,
436+ ) ?,
437+ node. value_digest ( ) ,
438+ ) ?; // temp storage is dropped here
439+ }
440+
441+ Ok ( ( ) )
442+ }
443+
444+ fn verify_reconstructed_trie_root (
445+ & self ,
446+ proof : & RangeProof < impl KeyType , impl ValueType , impl ProofCollection > ,
447+ root_hash : & TrieHash ,
448+ ) -> Result < ( ) , api:: Error > {
449+ // Create in-memory trie for reconstruction
450+ let memstore = MemStore :: new ( vec ! [ ] ) ;
451+ let nodestore = NodeStore :: new_empty_proposal ( memstore. into ( ) ) ;
452+ let mut merkle = Merkle { nodestore } ;
453+
454+ // Insert all key-value pairs from the range proof
455+ for ( key, value) in proof. key_values ( ) {
456+ merkle
457+ . insert ( key. as_ref ( ) , value. as_ref ( ) . into ( ) )
458+ . map_err ( ProofError :: IO ) ?;
459+ }
460+
461+ // Hash the trie and get root
462+ let merkle: Merkle < NodeStore < Arc < ImmutableProposal > , _ > > = merkle. try_into ( ) ?;
463+ let computed_root = merkle. nodestore ( ) . root_hash ( ) . ok_or ( ProofError :: Empty ) ?;
464+
465+ // Compare with expected root
466+ if computed_root == * root_hash {
467+ Ok ( ( ) )
468+ } else {
469+ Err ( api:: Error :: InvalidHash {
470+ reason : api:: InvalidHashReason :: MismatchedHash ,
471+ invalid : Some ( computed_root) ,
472+ expected : Some ( root_hash. clone ( ) ) ,
473+ } )
474+ }
475+ }
476+
477+ /// Get the value for the given key from the proof key-values. If not found,
478+ /// it will look for the value in the trie.
479+ ///
480+ /// This lifetime means `'out` is dependent on `'this`, `'kvs`, and `'store`.
481+ ///
482+ /// This allows us to return a reference to the value in the node without
483+ /// copying it to a new buffer.
484+ fn get_node_value_for_proof < ' this : ' out , ' kvs : ' out , ' store : ' out , ' out > (
485+ & ' this self ,
486+ key_values_sorted_by_key : & ' kvs [ ( impl KeyType , impl ValueType ) ] ,
487+ path : & Path ,
488+ root : Option < & Node > ,
489+ fetched_node : & ' store mut Option < SharedNode > ,
490+ ) -> Result < Option < & ' out [ u8 ] > , FileIoError > {
491+ use std:: cmp:: Ordering :: { self , Equal , Greater , Less } ;
492+
493+ fn cmp_path_with_key ( path : & Path , key : & [ u8 ] ) -> Ordering {
494+ let mut path = path. iter ( ) ;
495+ let mut key = NibblesIterator :: new ( key) ;
496+ loop {
497+ break match ( path. next ( ) , key. next ( ) ) {
498+ ( Some ( path) , Some ( key) ) => match path. cmp ( & key) {
499+ Equal => continue , // all other branches break
500+ ord => ord,
501+ } ,
502+ ( Some ( _) , None ) => Greater , // path is longer than key
503+ ( None , Some ( _) ) => Less , // key is longer than path
504+ ( None , None ) => Equal , // both are empty
505+ } ;
506+ }
507+ }
508+
509+ // use binary search to find the value for the key in `key_values_sorted_by_key`
510+ // (if it exists)
511+ if let Ok ( found) = key_values_sorted_by_key
512+ . binary_search_by ( |( key, _) | cmp_path_with_key ( path, key. as_ref ( ) ) )
513+ . map (
514+ #[ expect(
515+ clippy:: indexing_slicing,
516+ reason = "binary_search guarantees the index is in bounds"
517+ ) ]
518+ |index| key_values_sorted_by_key[ index] . 1 . as_ref ( ) ,
519+ )
520+ {
521+ return Ok ( Some ( found) ) ;
522+ }
523+
524+ // otherwise, look for it in the trie
525+ let Some ( root) = root else {
526+ // no root, so no value
527+ return Ok ( None ) ;
528+ } ;
529+
530+ let Some ( node) = get_helper ( & self . nodestore , root, path. as_ref ( ) ) ? else {
531+ // node was not found, so no value
532+ return Ok ( None ) ;
533+ } ;
534+
535+ let node = & * fetched_node. insert ( node) ;
536+
537+ Ok ( node. value ( ) )
266538 }
267539
268540 pub ( crate ) fn path_iter < ' a > (
0 commit comments