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