Skip to main content

reth_trie_common/
range_proof.rs

1//! Merkle Patricia trie range-proof verification.
2//!
3//! Reconstructs a trie root from consecutive hashed leaves and boundary proof nodes, rejecting
4//! altered or incomplete ranges and reporting where the trie continues past the range.
5//! Boundary paths are expanded, outside commitments are retained, and response leaves replace the
6//! covered interior before the reconstructed root is compared with the requested root.
7
8use crate::{HashBuilder, Nibbles, RlpNode, TrieNode};
9use alloc::vec::Vec;
10use alloy_primitives::{keccak256, map::B256Map, Bytes, B256};
11use alloy_rlp::Decodable;
12
13const KEY_NIBBLES: usize = B256::len_bytes() * 2;
14
15// Coordinates proof traversal and root reconstruction through one shared frontier.
16struct RangeProofVerifier<'a> {
17    // Determines which trie paths belong to the response and which remain proof-owned.
18    range: ProofRange,
19    // Resolves hashed boundary references without depending on proof wire order.
20    nodes: ProofNodes<'a>,
21    // Accumulates the disjoint entries needed to reconstruct the requested root.
22    frontier: ProofFrontier,
23    // Tracks the lowest known path after the response to report whether the trie continues.
24    next: Option<Nibbles>,
25}
26
27impl<'a> RangeProofVerifier<'a> {
28    // Creates a verifier with fixed bounds so traversal cannot drift from the supplied leaf range.
29    fn new(left: B256, right: B256, proof: &'a [Bytes], frontier: ProofFrontier) -> Self {
30        Self {
31            range: ProofRange::new(left, right),
32            nodes: ProofNodes::new(proof),
33            frontier,
34            next: None,
35        }
36    }
37
38    // Verifies the range by rebuilding its root, making omitted or altered leaves change the
39    // result.
40    fn verify(mut self, root: B256) -> Result<Option<B256>, RangeProofError> {
41        self.visit_reference(Nibbles::new(), &RlpNode::word_rlp(&root))?;
42
43        let got = self.frontier.root()?;
44        if got != root {
45            return Err(RangeProofError::RootMismatch { expected: root, got })
46        }
47        Ok(self.next.as_ref().map(TriePath::lowest_key))
48    }
49
50    // Visits a trie reference, expanding only boundaries because response leaves replace the
51    // interior.
52    fn visit_reference(
53        &mut self,
54        prefix: Nibbles,
55        reference: &RlpNode,
56    ) -> Result<(), RangeProofError> {
57        match self.range.subtree_relation(&prefix)? {
58            SubtreeRelation::OutsideLeft => self.add_outside_reference(prefix, reference),
59            SubtreeRelation::OutsideRight => {
60                self.note_next(prefix);
61                self.add_outside_reference(prefix, reference)
62            }
63            SubtreeRelation::Inside => Ok(()),
64            SubtreeRelation::Boundary => {
65                let node = self.nodes.resolve(prefix, reference)?;
66                self.visit_node(node, prefix)
67            }
68        }
69    }
70
71    // Visits a boundary node to expose the disjoint commitments needed for root reconstruction.
72    fn visit_node(&mut self, node: TrieNode, prefix: Nibbles) -> Result<(), RangeProofError> {
73        match node {
74            TrieNode::EmptyRoot => Ok(()),
75            TrieNode::Leaf(leaf) => {
76                let path = prefix.descend_leaf(&leaf.key)?;
77                match self.range.key_relation(&path) {
78                    KeyRelation::Before => self.frontier.push_leaf(path, leaf.value),
79                    KeyRelation::Inside => {}
80                    KeyRelation::After => {
81                        self.note_next(path);
82                        self.frontier.push_leaf(path, leaf.value);
83                    }
84                }
85                Ok(())
86            }
87            TrieNode::Extension(extension) => {
88                self.visit_reference(prefix.descend_extension(&extension.key)?, &extension.child)
89            }
90            TrieNode::Branch(branch) => {
91                for (nibble, child) in branch
92                    .as_ref()
93                    .children()
94                    .filter_map(|(nibble, child)| child.map(|child| (nibble, child)))
95                {
96                    self.visit_reference(prefix.descend_child(nibble)?, child)?;
97                }
98                Ok(())
99            }
100        }
101    }
102
103    // Adds an outside reference without expanding hashes because returned leaves cannot overlap it.
104    fn add_outside_reference(
105        &mut self,
106        prefix: Nibbles,
107        reference: &RlpNode,
108    ) -> Result<(), RangeProofError> {
109        if let Some(hash) = reference.as_hash() {
110            self.frontier.push_subtree(prefix, hash);
111            return Ok(())
112        }
113        self.add_outside_node(TrieNode::decode(&mut reference.as_slice())?, prefix)
114    }
115
116    // Adds an inline outside node by descending until a retainable leaf or hashed child is reached.
117    fn add_outside_node(&mut self, node: TrieNode, prefix: Nibbles) -> Result<(), RangeProofError> {
118        match node {
119            TrieNode::EmptyRoot => Ok(()),
120            TrieNode::Leaf(leaf) => {
121                let path = prefix.descend_leaf(&leaf.key)?;
122                self.frontier.push_leaf(path, leaf.value);
123                Ok(())
124            }
125            TrieNode::Extension(extension) => self
126                .add_outside_reference(prefix.descend_extension(&extension.key)?, &extension.child),
127            TrieNode::Branch(branch) => {
128                for (nibble, child) in branch
129                    .as_ref()
130                    .children()
131                    .filter_map(|(nibble, child)| child.map(|child| (nibble, child)))
132                {
133                    self.add_outside_reference(prefix.descend_child(nibble)?, child)?;
134                }
135                Ok(())
136            }
137        }
138    }
139
140    // Records the lowest right-side path needed to determine whether the interval is covered.
141    fn note_next(&mut self, path: Nibbles) {
142        if self.next.is_none_or(|next| path < next) {
143            self.next = Some(path);
144        }
145    }
146}
147
148// Stores unpacked range bounds so recursive comparisons avoid repeatedly expanding hashed keys.
149struct ProofRange {
150    // Inclusive origin paired with the proof's left boundary path.
151    left: Nibbles,
152    // Inclusive last leaf, or the requested limit when the response is empty.
153    right: Nibbles,
154}
155
156impl ProofRange {
157    // Creates range bounds in the nibble representation used throughout trie traversal.
158    fn new(left: B256, right: B256) -> Self {
159        Self { left: Nibbles::unpack(left), right: Nibbles::unpack(right) }
160    }
161
162    // Classifies a subtree prefix to avoid resolving subtries that cannot cross a boundary.
163    fn subtree_relation(&self, prefix: &Nibbles) -> Result<SubtreeRelation, RangeProofError> {
164        if prefix.len() > KEY_NIBBLES {
165            return Err(RangeProofError::PathTooLong { path: *prefix })
166        }
167        let left = self.left.slice(..prefix.len());
168        let right = self.right.slice(..prefix.len());
169
170        Ok(if *prefix < left {
171            SubtreeRelation::OutsideLeft
172        } else if *prefix > right {
173            SubtreeRelation::OutsideRight
174        } else if *prefix > left && *prefix < right {
175            SubtreeRelation::Inside
176        } else {
177            SubtreeRelation::Boundary
178        })
179    }
180
181    // Classifies a complete key because boundary proof leaves may sit outside the supplied range.
182    fn key_relation(&self, path: &Nibbles) -> KeyRelation {
183        if path < &self.left {
184            KeyRelation::Before
185        } else if path > &self.right {
186            KeyRelation::After
187        } else {
188            KeyRelation::Inside
189        }
190    }
191}
192
193// Prevents proof-owned subtries from being discarded or expanded unnecessarily by making each
194// prefix's relationship to the requested range explicit.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196enum SubtreeRelation {
197    // Retains a subtree that lies entirely before the requested range.
198    OutsideLeft,
199    // Retains a subtree after the range and uses its prefix to bound the next key.
200    OutsideRight,
201    // Expands a subtree because it may contain both proof-owned and response-owned paths.
202    Boundary,
203    // Replaces a wholly covered subtree with the response leaves being authenticated.
204    Inside,
205}
206
207// Prevents boundary proof leaves from being confused with response-owned interior leaves.
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209enum KeyRelation {
210    // Retains a proof leaf needed to reconstruct the trie before the response range.
211    Before,
212    // Defers to the response value so the reconstructed root authenticates the supplied leaf.
213    Inside,
214    // Retains a proof leaf and records that the trie continues beyond the response range.
215    After,
216}
217
218// Indexes proof blobs by commitment because proof wire order has no semantic meaning.
219struct ProofNodes<'a>(B256Map<&'a [u8]>);
220
221impl<'a> ProofNodes<'a> {
222    // Builds the proof index once to avoid rescanning it for every boundary reference.
223    fn new(proof: &'a [Bytes]) -> Self {
224        Self(proof.iter().map(|node| (keccak256(node), node.as_ref())).collect())
225    }
226
227    // Resolves inline references directly and requires proof backing for hashed references.
228    fn resolve(&self, path: Nibbles, reference: &RlpNode) -> Result<TrieNode, RangeProofError> {
229        let Some(hash) = reference.as_hash() else {
230            return Ok(TrieNode::decode(&mut reference.as_slice())?)
231        };
232        let node = self.0.get(&hash).ok_or(RangeProofError::MissingProofNode { path })?;
233        Ok(TrieNode::decode(&mut &node[..])?)
234    }
235}
236
237// Collects disjoint leaves and subtree commitments for canonical root reconstruction.
238#[derive(Default)]
239struct ProofFrontier(Vec<FrontierEntry>);
240
241impl ProofFrontier {
242    // Builds a frontier from validated leaves before HashBuilder enforces ordering with assertions.
243    fn from_leaves<I, V>(origin: B256, leaves: I) -> Result<(Self, Option<B256>), RangeProofError>
244    where
245        I: IntoIterator<Item = (B256, V)>,
246        V: Into<Vec<u8>>,
247    {
248        let mut frontier = Self::default();
249        let mut previous = None;
250
251        for (key, value) in leaves {
252            let value = value.into();
253            if key < origin {
254                return Err(RangeProofError::LeafBeforeOrigin { key, origin })
255            }
256            if previous.is_some_and(|previous| key <= previous) {
257                return Err(RangeProofError::NonMonotonicLeaves)
258            }
259            if value.is_empty() {
260                return Err(RangeProofError::EmptyLeafValue { key })
261            }
262            previous = Some(key);
263            frontier.push_leaf(Nibbles::unpack(key), value);
264        }
265
266        Ok((frontier, previous))
267    }
268
269    // Adds a leaf only at the fixed depth required by secure-trie hashed keys.
270    fn push_leaf(&mut self, path: Nibbles, value: Vec<u8>) {
271        debug_assert_eq!(path.len(), KEY_NIBBLES);
272        self.0.push(FrontierEntry::Leaf { path, value });
273    }
274
275    // Adds an opaque subtree at any prefix within the fixed hashed-key depth.
276    fn push_subtree(&mut self, path: Nibbles, hash: B256) {
277        debug_assert!(path.len() <= KEY_NIBBLES);
278        self.0.push(FrontierEntry::Subtree { path, hash });
279    }
280
281    // Reconstructs the root after sorting leaves and subtries into HashBuilder's strict path order.
282    fn root(mut self) -> Result<B256, RangeProofError> {
283        // Outside subtries are disjoint from returned leaves, so sorting produces the strict path
284        // order required by HashBuilder. Reject duplicates before they reach its assertion.
285        self.0.sort_unstable_by_key(FrontierEntry::path);
286        let mut builder = HashBuilder::default();
287        let mut previous = None;
288
289        for entry in self.0 {
290            let path = entry.path();
291            if previous.is_some_and(|previous| path <= previous) {
292                return Err(RangeProofError::DuplicateFrontierPath { path })
293            }
294            previous = Some(path);
295            match entry {
296                FrontierEntry::Leaf { path, value } => builder.add_leaf(path, &value),
297                FrontierEntry::Subtree { path, hash } => builder.add_branch(path, hash, false),
298            }
299        }
300        Ok(builder.root())
301    }
302}
303
304// Unifies supplied leaves and proof-owned subtrees so HashBuilder receives one globally ordered
305// stream without losing which payload each path carries.
306#[derive(Clone, Debug)]
307enum FrontierEntry {
308    // Carries a response value so root reconstruction authenticates the supplied leaf.
309    Leaf { path: Nibbles, value: Vec<u8> },
310    // Carries an opaque commitment so proof-owned state outside the range remains unchanged.
311    Subtree { path: Nibbles, hash: B256 },
312}
313
314impl FrontierEntry {
315    // Exposes the common ordering key because HashBuilder requires strictly increasing paths.
316    const fn path(&self) -> Nibbles {
317        match self {
318            Self::Leaf { path, .. } | Self::Subtree { path, .. } => *path,
319        }
320    }
321}
322
323/// Error returned when a trie range proof is invalid.
324#[derive(Debug, thiserror::Error, PartialEq, Eq)]
325pub enum RangeProofError {
326    /// The response leaves are not strictly increasing.
327    #[error("range leaves are not strictly increasing")]
328    NonMonotonicLeaves,
329    /// A returned leaf precedes the requested origin.
330    #[error("range leaf {key} precedes origin {origin}")]
331    LeafBeforeOrigin {
332        /// Hashed key of the offending leaf.
333        key: B256,
334        /// Inclusive origin the range was requested from.
335        origin: B256,
336    },
337    /// A returned leaf has no value and would represent a deletion.
338    #[error("range leaf {key} has an empty value")]
339    EmptyLeafValue {
340        /// Hashed key of the valueless leaf.
341        key: B256,
342    },
343    /// A proof node required on a range boundary is missing.
344    #[error("missing proof node at path {path:?}")]
345    MissingProofNode {
346        /// Trie path the missing node was referenced from.
347        path: Nibbles,
348    },
349    /// An extension node consumes no nibble, so a crafted chain could recurse without descending.
350    #[error("extension node at path {path:?} has an empty key")]
351    EmptyExtensionKey {
352        /// Trie path the extension was reached at.
353        path: Nibbles,
354    },
355    /// A proof path exceeds a hashed key's fixed length.
356    #[error("proof path {path:?} exceeds hashed key length")]
357    PathTooLong {
358        /// Trie path that could not be descended any further.
359        path: Nibbles,
360    },
361    /// A leaf does not resolve to a complete hashed key.
362    #[error("leaf path {path:?} does not resolve to a hashed key")]
363    InvalidLeafPath {
364        /// Incomplete trie path the leaf terminated at.
365        path: Nibbles,
366    },
367    /// Two reconstructed trie entries occupy the same path.
368    #[error("duplicate range-proof frontier path {path:?}")]
369    DuplicateFrontierPath {
370        /// Trie path claimed by more than one entry.
371        path: Nibbles,
372    },
373    /// The reconstructed trie does not match the requested root.
374    #[error("range proof root mismatch: expected {expected}, got {got}")]
375    RootMismatch {
376        /// Root the range was requested against.
377        expected: B256,
378        /// Root reconstructed from the response.
379        got: B256,
380    },
381    /// A trie node failed to decode.
382    #[error(transparent)]
383    Rlp(#[from] alloy_rlp::Error),
384}
385
386/// Verifies a consecutive leaf range against `root`, from `origin` through `limit`.
387///
388/// When `leaves` is empty, `limit` supplies the response's right boundary so an empty interval can
389/// be authenticated without requiring a leaf past the limit.
390///
391/// Returns a lower bound for the next key, or `None` if the range exhausts the trie.
392pub fn verify_range_proof<I, V>(
393    root: B256,
394    origin: B256,
395    limit: B256,
396    leaves: I,
397    proof: &[Bytes],
398) -> Result<Option<B256>, RangeProofError>
399where
400    I: IntoIterator<Item = (B256, V)>,
401    V: Into<Vec<u8>>,
402{
403    let (frontier, last_key) = ProofFrontier::from_leaves(origin, leaves)?;
404
405    // Without boundary nodes, only the complete leaf set can reproduce the requested root.
406    if proof.is_empty() {
407        let got = frontier.root()?;
408        if got != root {
409            return Err(RangeProofError::RootMismatch { expected: root, got })
410        }
411        return Ok(None)
412    }
413
414    RangeProofVerifier::new(origin, last_key.unwrap_or(limit), proof, frontier).verify(root)
415}
416
417// Keeps path mutation behind one checked API because external `Nibbles` cannot have inherent
418// methods and malformed proofs must not bypass secure-trie depth invariants.
419trait TriePath: Sized {
420    // Produces the conservative bound needed when an opaque subtree hides its first exact key.
421    fn lowest_key(&self) -> B256;
422
423    // Requires extensions to consume bounded path space so hostile proofs cannot recurse in place.
424    fn descend_extension(self, key: &Nibbles) -> Result<Self, RangeProofError>;
425
426    // Rejects branches beyond the key depth before mutating the path.
427    fn descend_child(self, nibble: u8) -> Result<Self, RangeProofError>;
428
429    // Requires leaves to resolve to one full hashed key before entering the frontier.
430    fn descend_leaf(self, key: &Nibbles) -> Result<Self, RangeProofError>;
431
432    // Shares the overflow guard used by extension and leaf descent.
433    fn join_checked(self, key: &Nibbles) -> Result<Self, RangeProofError>;
434}
435
436impl TriePath for Nibbles {
437    // Returns the subtree's lowest possible key because packing zero-fills unconsumed nibbles.
438    fn lowest_key(&self) -> B256 {
439        B256::right_padding_from(&self.pack())
440    }
441
442    // Descends an extension while rejecting empty keys that could recurse without consuming space.
443    fn descend_extension(self, key: &Nibbles) -> Result<Self, RangeProofError> {
444        if key.is_empty() {
445            return Err(RangeProofError::EmptyExtensionKey { path: self })
446        }
447        self.join_checked(key)
448    }
449
450    // Descends one branch nibble while rejecting nodes below the fixed hashed-key depth.
451    fn descend_child(self, nibble: u8) -> Result<Self, RangeProofError> {
452        if self.len() >= KEY_NIBBLES {
453            return Err(RangeProofError::PathTooLong { path: self })
454        }
455        let mut path = self;
456        path.push(nibble);
457        Ok(path)
458    }
459
460    // Completes a leaf path while rejecting leaves that do not resolve to one full hashed key.
461    fn descend_leaf(self, key: &Nibbles) -> Result<Self, RangeProofError> {
462        let path = self.join_checked(key)?;
463        if path.len() != KEY_NIBBLES {
464            return Err(RangeProofError::InvalidLeafPath { path })
465        }
466        Ok(path)
467    }
468
469    // Joins path segments while rejecting proof nodes that exceed the fixed hashed-key depth.
470    fn join_checked(self, key: &Nibbles) -> Result<Self, RangeProofError> {
471        if self.len() + key.len() > KEY_NIBBLES {
472            return Err(RangeProofError::PathTooLong { path: self })
473        }
474        Ok(self.join(key))
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::{proof::ProofRetainer, BranchNode, ExtensionNode, TrieMask, EMPTY_ROOT_HASH};
482    use alloc::{vec, vec::Vec};
483
484    const MAX_HASH: B256 = B256::new([0xff; B256::len_bytes()]);
485
486    fn verify_range_proof<I, V>(
487        root: B256,
488        origin: B256,
489        leaves: I,
490        proof: &[Bytes],
491    ) -> Result<Option<B256>, RangeProofError>
492    where
493        I: IntoIterator<Item = (B256, V)>,
494        V: Into<Vec<u8>>,
495    {
496        super::verify_range_proof(root, origin, MAX_HASH, leaves, proof)
497    }
498
499    fn key(value: u64) -> B256 {
500        B256::left_padding_from(&value.to_be_bytes())
501    }
502
503    fn encode_node(node: &TrieNode) -> Bytes {
504        alloy_rlp::encode(node).into()
505    }
506
507    fn no_leaves() -> Vec<(B256, Vec<u8>)> {
508        Vec::new()
509    }
510
511    fn value(byte: u8) -> Vec<u8> {
512        vec![byte; 64]
513    }
514
515    fn build_proof(leaves: &[(B256, Vec<u8>)], targets: &[B256]) -> (B256, Vec<Bytes>) {
516        let targets = targets.iter().copied().map(Nibbles::unpack).collect();
517        let mut builder = HashBuilder::default().with_proof_retainer(ProofRetainer::new(targets));
518        for (key, value) in leaves {
519            builder.add_leaf(Nibbles::unpack(*key), value);
520        }
521        let root = builder.root();
522        let proof = builder
523            .take_proof_nodes()
524            .into_nodes_sorted()
525            .into_iter()
526            .map(|(_, node)| node)
527            .collect();
528        (root, proof)
529    }
530
531    #[test]
532    fn partial_range_authenticates_and_reports_the_next_key() {
533        let leaves =
534            vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3)), (key(4), value(4))];
535        let (root, proof) = build_proof(&leaves, &[key(2), key(3)]);
536
537        assert_eq!(
538            verify_range_proof(root, key(2), leaves[1..3].to_vec(), &proof).unwrap(),
539            Some(key(4))
540        );
541    }
542
543    // An unexpanded subtree reports its lowest possible key.
544    #[test]
545    fn unexpanded_right_subtree_reports_a_prefix_bound() {
546        let right = |tail: u8| {
547            let mut key = B256::ZERO;
548            key.0[0] = 0x40;
549            key.0[31] = tail;
550            key
551        };
552        let leaves = vec![
553            (key(1), value(1)),
554            (key(2), value(2)),
555            (right(1), value(3)),
556            (right(2), value(4)),
557        ];
558        let (root, proof) = build_proof(&leaves, &[key(1), key(2)]);
559
560        assert_eq!(
561            verify_range_proof(root, key(1), leaves[..2].to_vec(), &proof).unwrap(),
562            Some(B256::right_padding_from(&[0x40]))
563        );
564    }
565
566    #[test]
567    fn inline_outside_nodes_are_reconstructed() {
568        let leaves =
569            vec![(key(1), vec![1]), (key(2), vec![2]), (key(3), vec![3]), (key(4), vec![4])];
570        let (root, proof) = build_proof(&leaves, &[key(2), key(3)]);
571
572        assert_eq!(
573            verify_range_proof(root, key(2), leaves[1..3].to_vec(), &proof).unwrap(),
574            Some(key(4))
575        );
576    }
577
578    #[test]
579    fn unused_proof_nodes_are_accepted() {
580        let leaves =
581            vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3)), (key(4), value(4))];
582        let (root, mut proof) = build_proof(&leaves, &[key(2), key(3)]);
583        proof.push(Bytes::from_static(&[alloy_rlp::EMPTY_STRING_CODE]));
584
585        assert_eq!(
586            verify_range_proof(root, key(2), leaves[1..3].to_vec(), &proof).unwrap(),
587            Some(key(4))
588        );
589    }
590
591    // Reject paths before they exceed the fixed hashed-key length.
592    #[test]
593    fn node_paths_are_bounded_before_they_overflow_a_key() {
594        let dangling = RlpNode::word_rlp(&B256::repeat_byte(0xaa));
595
596        // An extension one nibble deep whose key spans a whole hashed key: 1 + 64 nibbles.
597        let overlong = encode_node(&TrieNode::Extension(ExtensionNode::new(
598            Nibbles::unpack(key(1)),
599            dangling.clone(),
600        )));
601        let branch = encode_node(&TrieNode::Branch(BranchNode::new(
602            vec![RlpNode::word_rlp(&keccak256(&overlong))],
603            TrieMask::new(1),
604        )));
605        let proof = vec![branch.clone(), overlong];
606
607        assert!(matches!(
608            verify_range_proof(keccak256(&branch), key(1), no_leaves(), &proof),
609            Err(RangeProofError::PathTooLong { .. })
610        ));
611
612        // A branch sitting at the full hashed-key depth, which has no room for a child.
613        let deep_branch =
614            encode_node(&TrieNode::Branch(BranchNode::new(vec![dangling], TrieMask::new(1))));
615        let reach = encode_node(&TrieNode::Extension(ExtensionNode::new(
616            Nibbles::unpack(key(1)),
617            RlpNode::word_rlp(&keccak256(&deep_branch)),
618        )));
619        let proof = vec![reach.clone(), deep_branch];
620
621        assert!(matches!(
622            verify_range_proof(keccak256(&reach), key(1), no_leaves(), &proof),
623            Err(RangeProofError::PathTooLong { .. })
624        ));
625    }
626
627    // Empty extensions could recurse indefinitely without increasing the path depth.
628    #[test]
629    fn empty_extension_keys_are_rejected() {
630        let empty_key = Nibbles::new();
631        let mut proof = Vec::new();
632        let mut child = RlpNode::word_rlp(&B256::repeat_byte(0xaa));
633
634        for _ in 0..64 {
635            let node = encode_node(&TrieNode::Extension(ExtensionNode::new(empty_key, child)));
636            child = RlpNode::word_rlp(&keccak256(&node));
637            proof.push(node);
638        }
639        let root = keccak256(proof.last().unwrap());
640
641        assert!(matches!(
642            verify_range_proof(root, key(1), no_leaves(), &proof),
643            Err(RangeProofError::EmptyExtensionKey { .. })
644        ));
645    }
646
647    #[test]
648    fn missing_boundary_node_is_rejected() {
649        let leaves =
650            vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3)), (key(4), value(4))];
651        let (root, _) = build_proof(&leaves, &[key(2), key(3)]);
652        let unrelated = [Bytes::from_static(&[alloy_rlp::EMPTY_STRING_CODE])];
653
654        assert!(matches!(
655            verify_range_proof(root, key(2), leaves[1..3].to_vec(), &unrelated),
656            Err(RangeProofError::MissingProofNode { .. })
657        ));
658    }
659
660    #[test]
661    fn boundary_proof_can_authenticate_an_exhausted_range() {
662        let leaves =
663            vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3)), (key(4), value(4))];
664        let (root, proof) = build_proof(&leaves, &[key(2), key(4)]);
665
666        assert_eq!(verify_range_proof(root, key(2), leaves[1..].to_vec(), &proof).unwrap(), None);
667    }
668
669    #[test]
670    fn proof_free_full_trie_is_exhausted() {
671        let leaves = vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3))];
672        let (root, _) = build_proof(&leaves, &[]);
673
674        assert_eq!(verify_range_proof(root, B256::ZERO, leaves, &[]).unwrap(), None);
675    }
676
677    #[test]
678    fn omitted_interior_leaf_changes_root() {
679        let leaves =
680            vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3)), (key(4), value(4))];
681        let (root, proof) = build_proof(&leaves, &[key(2), key(4)]);
682        let returned = vec![(key(2), value(2)), (key(4), value(4))];
683
684        assert!(matches!(
685            verify_range_proof(root, key(2), returned, &proof),
686            Err(RangeProofError::RootMismatch { .. })
687        ));
688    }
689
690    #[test]
691    fn mutated_leaf_changes_root() {
692        let leaves = vec![(key(1), value(1)), (key(2), value(2)), (key(3), value(3))];
693        let (root, proof) = build_proof(&leaves, &[key(2), key(3)]);
694        let returned = vec![(key(2), value(9)), (key(3), value(3))];
695
696        assert!(matches!(
697            verify_range_proof(root, key(2), returned, &proof),
698            Err(RangeProofError::RootMismatch { .. })
699        ));
700    }
701
702    #[test]
703    fn empty_tail_is_authenticated() {
704        let leaves = vec![(key(1), value(1)), (key(2), value(2))];
705        let (root, proof) = build_proof(&leaves, &[key(3)]);
706
707        assert_eq!(
708            verify_range_proof(root, key(3), core::iter::empty::<(B256, Vec<u8>)>(), &proof)
709                .unwrap(),
710            None
711        );
712    }
713
714    #[test]
715    fn empty_range_cannot_hide_a_right_leaf() {
716        let leaves = vec![(key(1), value(1)), (key(3), value(3))];
717        let (root, proof) = build_proof(&leaves, &[key(2)]);
718
719        assert!(matches!(
720            verify_range_proof(root, key(2), core::iter::empty::<(B256, Vec<u8>)>(), &proof,),
721            Err(RangeProofError::RootMismatch { .. })
722        ));
723    }
724
725    #[test]
726    fn empty_interval_is_authenticated_through_its_limit() {
727        let leaves = vec![(key(1), value(1)), (key(3), value(3))];
728        let (root, proof) = build_proof(&leaves, &[key(2)]);
729
730        assert_eq!(
731            super::verify_range_proof(root, key(2), key(2), no_leaves(), &proof).unwrap(),
732            Some(key(3))
733        );
734    }
735
736    #[test]
737    fn rejects_non_monotonic_leaves_and_leaves_before_origin() {
738        let leaves = vec![(key(2), value(2)), (key(1), value(1))];
739
740        assert_eq!(
741            verify_range_proof(B256::ZERO, B256::ZERO, leaves, &[]),
742            Err(RangeProofError::NonMonotonicLeaves)
743        );
744        assert!(matches!(
745            verify_range_proof(B256::ZERO, key(2), [(key(1), value(1))], &[],),
746            Err(RangeProofError::LeafBeforeOrigin { .. })
747        ));
748        assert_eq!(
749            verify_range_proof(B256::ZERO, B256::ZERO, [(key(1), Vec::new())], &[]),
750            Err(RangeProofError::EmptyLeafValue { key: key(1) })
751        );
752    }
753
754    #[test]
755    fn empty_root_accepts_only_an_empty_range() {
756        assert_eq!(
757            verify_range_proof(
758                EMPTY_ROOT_HASH,
759                B256::ZERO,
760                core::iter::empty::<(B256, Vec<u8>)>(),
761                &[]
762            )
763            .unwrap(),
764            None
765        );
766        assert!(matches!(
767            verify_range_proof(EMPTY_ROOT_HASH, B256::ZERO, [(key(1), value(1))], &[],),
768            Err(RangeProofError::RootMismatch { .. })
769        ));
770        assert!(
771            verify_range_proof(EMPTY_ROOT_HASH, B256::ZERO, no_leaves(), &[Bytes::new()]).is_err()
772        );
773    }
774}