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