Skip to main content

ethrex_trie/
verify_range.rs

1use std::collections::{BTreeMap, VecDeque};
2
3use ethereum_types::H256;
4use ethrex_crypto::{NativeCrypto, keccak::keccak_hash};
5use ethrex_rlp::decode::RLPDecode;
6
7use crate::{
8    ProofTrie, Trie, TrieError, ValueRLP,
9    nibbles::Nibbles,
10    node::{Node, NodeRef},
11    node_hash::NodeHash,
12};
13
14/// Verifies that the key value range belongs to the trie with the given root given the edge proofs for the range
15/// Also returns true if there is more state to be fetched (aka if there are more keys to the right of the given range)
16pub fn verify_range(
17    root: H256,
18    left_bound: &H256,
19    keys: &[H256],
20    values: &[ValueRLP],
21    proof: &[Vec<u8>],
22) -> Result<bool, TrieError> {
23    // Validate range
24    if keys.len() != values.len() {
25        return Err(TrieError::Verify(format!(
26            "inconsistent proof data, got {} keys and {} values",
27            keys.len(),
28            values.len()
29        )));
30    }
31    // Check that the key range is monotonically increasing
32    for keys in keys.windows(2) {
33        if keys[0] >= keys[1] {
34            return Err(TrieError::Verify(String::from(
35                "key range is not monotonically increasing",
36            )));
37        }
38    }
39    // Check for empty values
40    if values.iter().any(|value| value.is_empty()) {
41        return Err(TrieError::Verify(String::from(
42            "value range contains empty value",
43        )));
44    }
45
46    let mut trie = Trie::stateless();
47
48    // Special Case: No proofs given, the range is expected to be the full set of leaves
49    if proof.is_empty() {
50        // Check that the trie constructed from the given keys and values has the expected root
51        for (key, value) in keys.iter().zip(values.iter()) {
52            trie.insert(key.0.to_vec(), value.clone())?;
53        }
54        let hash = trie.hash(&NativeCrypto)?;
55        if hash != root {
56            return Err(TrieError::Verify(format!(
57                "invalid proof, expected root hash {root}, got  {hash}",
58            )));
59        }
60        return Ok(false);
61    }
62
63    // Special Case: One edge proof, no range given, there are no more values in the trie
64    if keys.is_empty() {
65        // We need to check that the proof confirms the non-existance of the first key
66        // and that there are no more elements to the right of the first key
67        let result = process_proof_nodes(proof, root.into(), (*left_bound, None), None)?;
68        if result.num_right_references > 0 || !result.left_value.is_empty() {
69            return Err(TrieError::Verify(
70                "no keys returned but more are available on the trie".to_string(),
71            ));
72        } else {
73            return Ok(false);
74        };
75    }
76
77    let last_key = keys.last().unwrap();
78
79    // Special Case: There is only one element and the two edge keys are the same
80    if keys.len() == 1 && left_bound == last_key {
81        // We need to check that the proof confirms the existence of the first key
82        if left_bound != &keys[0] {
83            return Err(TrieError::Verify(
84                "correct proof but invalid key".to_string(),
85            ));
86        }
87        let result = process_proof_nodes(
88            proof,
89            root.into(),
90            (*left_bound, Some(*last_key)),
91            Some(*keys.first().unwrap()),
92        )?;
93        if result.left_value != values[0] {
94            return Err(TrieError::Verify(
95                "correct proof but invalid data".to_string(),
96            ));
97        }
98        return Ok(result.num_right_references > 0);
99    }
100
101    // Regular Case: Two edge proofs
102    if left_bound >= last_key {
103        return Err(TrieError::Verify("invalid edge keys".to_string()));
104    }
105
106    // Process proofs to check if they are valid.
107    let result = process_proof_nodes(
108        proof,
109        root.into(),
110        (*left_bound, Some(*last_key)),
111        Some(*keys.first().unwrap()),
112    )?;
113
114    // Reconstruct the internal nodes by inserting the elements on the range
115    for (key, value) in keys.iter().zip(values.iter()) {
116        trie.insert(key.0.to_vec(), value.clone())?;
117    }
118
119    // Fill up the state with the nodes from the proof
120    let mut trie = ProofTrie::from(trie);
121    for (partial_path, external_ref) in result.external_references {
122        trie.insert(partial_path, external_ref)?;
123    }
124
125    // Check that the hash is the one we expected (aka the trie was properly reconstructed from the edge proofs and the range)
126    let hash = trie.hash(&NativeCrypto);
127    if hash != root {
128        return Err(TrieError::Verify(format!(
129            "invalid proof, expected root hash {root}, got  {hash}",
130        )));
131    }
132    Ok(result.num_right_references > 0)
133}
134
135/// Parsed range proof
136/// Has a mapping of node hashes to the encoded node data, useful for verifying the proof.
137struct RangeProof<'a> {
138    node_refs: BTreeMap<H256, &'a [u8]>,
139}
140
141impl<'a> From<&'a [Vec<u8>]> for RangeProof<'a> {
142    fn from(proof: &'a [Vec<u8>]) -> Self {
143        let node_refs = proof
144            .iter()
145            .map(|node| {
146                let hash = H256(keccak_hash(node));
147                let encoded_data = node.as_slice();
148                (hash, encoded_data)
149            })
150            .collect();
151        RangeProof { node_refs }
152    }
153}
154
155impl RangeProof<'_> {
156    /// Get a node by its hash, returning `None` if the node is not present in the proof.
157    /// If the node is inline in the hash, it will be decoded directly from it.
158    fn get_node(&self, hash: NodeHash) -> Result<Option<Node>, TrieError> {
159        let encoded_node = match hash {
160            NodeHash::Hashed(hash) => self.node_refs.get(&hash).copied(),
161            NodeHash::Inline(_) => Some(hash.as_ref()),
162        };
163        Ok(encoded_node.map(Node::decode).transpose()?)
164    }
165}
166
167/// Iterate over all provided proofs starting from the root and generate a set of hashes that fall
168/// outside the verification bounds.
169///
170/// For example, calling this function with the proofs for the range `(hash_a, hash_b)` will return
171/// all node references contained within those proofs except the ones that are contained between
172/// `hash_a` and `hash_b` lexicographically.
173///
174/// Also returns the number of references strictly to the right of the bounds. If the right bound
175/// is unbounded (aka. not provided), all nodes to the right (inclusive) of the left bound will
176/// be counted. Leaf nodes are not counted (the leaf nodes within the proof do not count).
177struct ProofProcessingResult {
178    external_references: Vec<(Nibbles, NodeHash)>,
179    left_value: Vec<u8>,
180    num_right_references: usize,
181}
182
183fn process_proof_nodes(
184    raw_proof: &[Vec<u8>],
185    root: NodeHash,
186    bounds: (H256, Option<H256>),
187    first_key: Option<H256>,
188) -> Result<ProofProcessingResult, TrieError> {
189    // Convert `H256` bounds into `Nibble` bounds for convenience.
190    let bounds = (
191        Nibbles::from_bytes(&bounds.0.0),
192        // In case there's no right bound, we use the left bound as the right bound.
193        Nibbles::from_bytes(&bounds.1.unwrap_or(bounds.0).0),
194    );
195    let first_key = first_key.map(|first_key| Nibbles::from_bytes(&first_key.0));
196
197    // Generate a map of node hashes to node data for obtaining proof nodes given their hashes.
198    let proof = RangeProof::from(raw_proof);
199
200    // Initialize the external references container.
201    let mut external_references = Vec::new();
202    let mut left_value = Vec::new();
203    let mut num_right_references = 0;
204
205    // Iterate over the proofs tree.
206    //
207    // The children are processed as follows:
208    //   1. Nodes that fall within bounds will be filtered out.
209    //   2. Nodes for which we have the proof will push themselves into the queue.
210    //   3. Nodes for which we do not have the proof are treated as external references.
211    let mut stack = VecDeque::from_iter([(
212        Nibbles::default(),
213        proof.get_node(root)?.ok_or(TrieError::Verify(format!(
214            "root node missing from proof: {root:?}"
215        )))?,
216    )]);
217    while let Some((mut current_path, current_node)) = stack.pop_front() {
218        let value = match current_node {
219            Node::Branch(node) => {
220                for (index, choice) in node.choices.into_iter().enumerate() {
221                    if !choice.is_valid() {
222                        continue;
223                    }
224                    num_right_references += visit_child_node(
225                        &mut stack,
226                        &mut external_references,
227                        &proof,
228                        &bounds,
229                        first_key.as_ref(),
230                        current_path.append_new(index as u8),
231                        choice,
232                    )?;
233                }
234                node.value
235            }
236            Node::Extension(node) => {
237                current_path.extend(&node.prefix);
238                num_right_references += visit_child_node(
239                    &mut stack,
240                    &mut external_references,
241                    &proof,
242                    &bounds,
243                    first_key.as_ref(),
244                    current_path.clone(),
245                    node.child,
246                )?;
247                Vec::new()
248            }
249            Node::Leaf(node) => node.value,
250        };
251
252        if !value.is_empty() && current_path == bounds.0 {
253            left_value = value.clone();
254        }
255    }
256
257    let result = ProofProcessingResult {
258        external_references,
259        left_value,
260        num_right_references,
261    };
262    Ok(result)
263}
264
265fn visit_child_node(
266    stack: &mut VecDeque<(Nibbles, Node)>,
267    external_refs: &mut Vec<(Nibbles, NodeHash)>,
268    proof: &RangeProof,
269    (left_bound, right_bound): &(Nibbles, Nibbles),
270    first_key: Option<&Nibbles>,
271    mut partial_path: Nibbles,
272    child: NodeRef,
273) -> Result<usize, TrieError> {
274    let cmp_l = left_bound.compare_prefix(&partial_path);
275    let cmp_r = right_bound.compare_prefix(&partial_path);
276
277    // We don't process nodes that lie inside bounds
278    // left_bound < partial_path < right_bound
279    if cmp_l.is_lt() && cmp_r.is_gt() {
280        return Ok(0);
281    }
282    let NodeRef::Hash(hash) = child else {
283        // This is unreachable because the nodes have just been decoded, therefore only
284        // having hash references.
285        unreachable!()
286    };
287
288    match proof.get_node(hash)? {
289        Some(node) => {
290            // Handle proofs of absences in the left bound.
291            //
292            // When the proof proves an absence, the left bound won't end up in a leaf
293            // and there will not be a path that the external references can follow to
294            // avoid inconsistent trie errors. In those cases, there will be subtrees
295            // completely outside of the verification range. Since we have the hash of
296            // the entire subtree within the proof, we can just treat it as an external
297            // reference and ignore everything inside.
298            //
299            // This optimization should not be a problem because we're the ones that
300            // have computed the hash of the subtree (it's not part of the proof)
301            // therefore we can always be sure it's representing the data the proof has
302            // provided.
303            //
304            // Note: The right bound cannot be a proof of absence because it cannot be
305            //   specified externally, and is always keys.last(). In other words, if
306            //   there is a right bound, it'll always exist.
307            if first_key.is_some_and(|fk| fk.compare_prefix(&partial_path).is_gt()) {
308                // The subtree is not part of the path to the first available key. Treat
309                // the entire subtree as an external reference.
310                external_refs.push((partial_path, hash));
311            } else {
312                // Append implicit leaf extension when pushing leaves.
313                if let Node::Leaf(node) = &node {
314                    partial_path.extend(&node.partial);
315                }
316                if right_bound.compare_prefix(&partial_path).is_lt() {
317                    external_refs.push((partial_path.clone(), hash));
318                }
319
320                stack.push_back((partial_path, node));
321            }
322        }
323        None => {
324            if cmp_l.is_eq() || cmp_r.is_eq() {
325                return Err(TrieError::Verify(format!("proof node missing: {hash:?}")));
326            }
327
328            external_refs.push((partial_path, hash));
329        }
330    }
331
332    // left_bound < partial_path && right_bound < partial_path
333    let n_right_references = if cmp_l.is_lt() && cmp_r.is_lt() { 1 } else { 0 };
334
335    Ok(n_right_references)
336}