Skip to main content

ethrex_trie/
verify_range.rs

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