Skip to main content

reth_trie_common/
proofs.rs

1//! Merkle trie proofs.
2
3use crate::{
4    BranchNodeMasks, BranchNodeMasksMap, Nibbles, ProofTrieNodeV2, TrieAccount, TrieNodeV2,
5};
6use alloc::{borrow::Cow, collections::VecDeque, vec::Vec};
7use alloy_primitives::{
8    keccak256,
9    map::{hash_map, B256Map, B256Set},
10    Address, Bytes, B256, U256,
11};
12use alloy_rlp::{encode_fixed_size, Decodable, Encodable, EMPTY_STRING_CODE};
13use alloy_trie::{
14    nodes::{BranchNodeRef, TrieNode},
15    proof::{verify_proof, DecodedProofNodes, ProofNodes, ProofVerificationError},
16    EMPTY_ROOT_HASH,
17};
18use derive_more::{Deref, DerefMut, IntoIterator};
19use itertools::Itertools;
20use reth_primitives_traits::Account;
21
22/// Proof targets map.
23#[derive(Deref, DerefMut, IntoIterator, Clone, PartialEq, Eq, Default, Debug)]
24pub struct MultiProofTargets(B256Map<B256Set>);
25
26impl FromIterator<(B256, B256Set)> for MultiProofTargets {
27    fn from_iter<T: IntoIterator<Item = (B256, B256Set)>>(iter: T) -> Self {
28        Self(B256Map::from_iter(iter))
29    }
30}
31
32impl MultiProofTargets {
33    /// Creates an empty `MultiProofTargets` with at least the specified capacity.
34    pub fn with_capacity(capacity: usize) -> Self {
35        Self(B256Map::with_capacity_and_hasher(capacity, Default::default()))
36    }
37
38    /// Create `MultiProofTargets` with a single account as a target.
39    pub fn account(hashed_address: B256) -> Self {
40        Self::accounts([hashed_address])
41    }
42
43    /// Create `MultiProofTargets` with a single account and slots as targets.
44    pub fn account_with_slots<I: IntoIterator<Item = B256>>(
45        hashed_address: B256,
46        slots_iter: I,
47    ) -> Self {
48        Self(B256Map::from_iter([(hashed_address, slots_iter.into_iter().collect())]))
49    }
50
51    /// Create `MultiProofTargets` only from accounts.
52    pub fn accounts<I: IntoIterator<Item = B256>>(iter: I) -> Self {
53        Self(iter.into_iter().map(|hashed_address| (hashed_address, Default::default())).collect())
54    }
55
56    /// Retains the targets representing the difference,
57    /// i.e., the values that are in `self` but not in `other`.
58    pub fn retain_difference(&mut self, other: &Self) {
59        self.0.retain(|hashed_address, hashed_slots| {
60            other.get(hashed_address).is_none_or(|other_hashed_slots| {
61                hashed_slots.retain(|hashed_slot| !other_hashed_slots.contains(hashed_slot));
62                !hashed_slots.is_empty()
63            })
64        });
65    }
66
67    /// Extend multi proof targets with contents of other.
68    pub fn extend(&mut self, other: Self) {
69        self.extend_inner(Cow::Owned(other));
70    }
71
72    /// Extend multi proof targets with contents of other.
73    ///
74    /// Slightly less efficient than [`Self::extend`], but preferred to `extend(other.clone())`.
75    pub fn extend_ref(&mut self, other: &Self) {
76        self.extend_inner(Cow::Borrowed(other));
77    }
78
79    fn extend_inner(&mut self, other: Cow<'_, Self>) {
80        for (hashed_address, hashed_slots) in other.iter() {
81            match self.entry(*hashed_address) {
82                hash_map::Entry::Vacant(entry) => {
83                    entry.insert(hashed_slots.clone());
84                }
85                hash_map::Entry::Occupied(mut entry) => {
86                    entry.get_mut().extend(hashed_slots);
87                }
88            }
89        }
90    }
91
92    /// Returns an iterator that yields chunks of the specified size.
93    ///
94    /// See [`ChunkedMultiProofTargets`] for more information.
95    ///
96    /// # Panics
97    ///
98    /// Panics if the chunk size is zero.
99    pub fn chunks(self, size: usize) -> ChunkedMultiProofTargets {
100        ChunkedMultiProofTargets::new(self, size)
101    }
102
103    /// Returns the number of items that will be considered during chunking in `[Self::chunks]`.
104    pub fn chunking_length(&self) -> usize {
105        self.values().map(|slots| 1 + slots.len().saturating_sub(1)).sum::<usize>()
106    }
107}
108
109/// An iterator that yields chunks of the proof targets of at most `size` account and storage
110/// targets.
111///
112/// For example, for the following proof targets:
113/// ```text
114/// - 0x1: [0x10, 0x20, 0x30]
115/// - 0x2: [0x40]
116/// - 0x3: []
117/// ```
118///
119/// and `size = 2`, the iterator will yield the following chunks:
120/// ```text
121/// - { 0x1: [0x10, 0x20] }
122/// - { 0x1: [0x30], 0x2: [0x40] }
123/// - { 0x3: [] }
124/// ```
125///
126/// It follows two rules:
127/// - If account has associated storage slots, each storage slot is counted towards the chunk size.
128/// - If account has no associated storage slots, the account is counted towards the chunk size.
129#[derive(Debug)]
130pub struct ChunkedMultiProofTargets {
131    flattened_targets: alloc::vec::IntoIter<(B256, Option<B256>)>,
132    size: usize,
133}
134
135impl ChunkedMultiProofTargets {
136    fn new(targets: MultiProofTargets, size: usize) -> Self {
137        assert!(size > 0, "chunk size must be non-zero");
138        let flattened_targets = targets
139            .into_iter()
140            .flat_map(|(address, slots)| {
141                if slots.is_empty() {
142                    // If the account has no storage slots, we still need to yield the account
143                    // address with empty storage slots. `None` here means that
144                    // there's no storage slot to fetch.
145                    itertools::Either::Left(core::iter::once((address, None)))
146                } else {
147                    itertools::Either::Right(
148                        slots.into_iter().map(move |slot| (address, Some(slot))),
149                    )
150                }
151            })
152            .sorted_unstable();
153        Self { flattened_targets, size }
154    }
155}
156
157impl Iterator for ChunkedMultiProofTargets {
158    type Item = MultiProofTargets;
159
160    fn next(&mut self) -> Option<Self::Item> {
161        let chunk = self.flattened_targets.by_ref().take(self.size).fold(
162            MultiProofTargets::default(),
163            |mut acc, (address, slot)| {
164                let entry = acc.entry(address).or_default();
165                if let Some(slot) = slot {
166                    entry.insert(slot);
167                }
168                acc
169            },
170        );
171
172        if chunk.is_empty() {
173            None
174        } else {
175            Some(chunk)
176        }
177    }
178}
179
180/// The state multiproof of target accounts and multiproofs of their storage tries.
181/// Multiproof is effectively a state subtrie that only contains the nodes
182/// in the paths of target accounts.
183#[derive(Clone, Default, Debug, PartialEq, Eq)]
184pub struct MultiProof {
185    /// State trie multiproof for requested accounts.
186    pub account_subtree: ProofNodes,
187    /// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the account
188    /// proof.
189    pub branch_node_masks: BranchNodeMasksMap,
190    /// Storage trie multiproofs.
191    pub storages: B256Map<StorageMultiProof>,
192}
193
194impl MultiProof {
195    /// Returns true if the multiproof is empty.
196    pub fn is_empty(&self) -> bool {
197        self.account_subtree.is_empty() &&
198            self.branch_node_masks.is_empty() &&
199            self.storages.is_empty()
200    }
201
202    /// Return the account proof nodes for the given account path.
203    pub fn account_proof_nodes(&self, path: &Nibbles) -> Vec<(Nibbles, Bytes)> {
204        self.account_subtree.matching_nodes_sorted(path)
205    }
206
207    /// Return the storage proof nodes for the given storage slots of the account path.
208    pub fn storage_proof_nodes(
209        &self,
210        hashed_address: B256,
211        slots: impl IntoIterator<Item = B256>,
212    ) -> Vec<(B256, Vec<(Nibbles, Bytes)>)> {
213        self.storages
214            .get(&hashed_address)
215            .map(|storage_mp| {
216                slots
217                    .into_iter()
218                    .map(|slot| {
219                        let nibbles = Nibbles::unpack(slot);
220                        (slot, storage_mp.subtree.matching_nodes_sorted(&nibbles))
221                    })
222                    .collect()
223            })
224            .unwrap_or_default()
225    }
226
227    /// Construct the account proof from the multiproof.
228    pub fn account_proof(
229        &self,
230        address: Address,
231        slots: &[B256],
232    ) -> Result<AccountProof, alloy_rlp::Error> {
233        let hashed_address = keccak256(address);
234        let nibbles = Nibbles::unpack(hashed_address);
235
236        // Retrieve the account proof.
237        let proof = self
238            .account_proof_nodes(&nibbles)
239            .into_iter()
240            .map(|(_, node)| node)
241            .collect::<Vec<_>>();
242
243        // Inspect the last node in the proof. If it's a leaf node with matching suffix,
244        // then the node contains the encoded trie account.
245        let info = 'info: {
246            if let Some(last) = proof.last() &&
247                let TrieNode::Leaf(leaf) = TrieNode::decode(&mut &last[..])? &&
248                nibbles.ends_with(&leaf.key)
249            {
250                let account = TrieAccount::decode(&mut &leaf.value[..])?;
251                break 'info Some(Account::from(account))
252            }
253            None
254        };
255
256        // Retrieve proofs for requested storage slots.
257        let storage_multiproof = self.storages.get(&hashed_address);
258        let storage_root = storage_multiproof.map(|m| m.root).unwrap_or(EMPTY_ROOT_HASH);
259        let mut storage_proofs = Vec::with_capacity(slots.len());
260        for slot in slots {
261            let proof = if let Some(multiproof) = &storage_multiproof {
262                multiproof.storage_proof(*slot)?
263            } else {
264                StorageProof::new(*slot)
265            };
266            storage_proofs.push(proof);
267        }
268        Ok(AccountProof { address, info, proof, storage_root, storage_proofs })
269    }
270
271    /// Extends this multiproof with another one, merging both account and storage
272    /// proofs.
273    pub fn extend(&mut self, other: Self) {
274        self.account_subtree.extend_from(other.account_subtree);
275        self.branch_node_masks.extend(other.branch_node_masks);
276
277        let reserve = if self.storages.is_empty() {
278            other.storages.len()
279        } else {
280            other.storages.len().div_ceil(2)
281        };
282        self.storages.reserve(reserve);
283        for (hashed_address, storage) in other.storages {
284            match self.storages.entry(hashed_address) {
285                hash_map::Entry::Occupied(mut entry) => {
286                    debug_assert_eq!(entry.get().root, storage.root);
287                    let entry = entry.get_mut();
288                    entry.subtree.extend_from(storage.subtree);
289                    entry.branch_node_masks.extend(storage.branch_node_masks);
290                }
291                hash_map::Entry::Vacant(entry) => {
292                    entry.insert(storage);
293                }
294            }
295        }
296    }
297
298    /// Create a [`MultiProof`] from a [`StorageMultiProof`].
299    pub fn from_storage_proof(hashed_address: B256, storage_proof: StorageMultiProof) -> Self {
300        Self {
301            storages: B256Map::from_iter([(hashed_address, storage_proof)]),
302            ..Default::default()
303        }
304    }
305}
306
307/// This is a type of [`MultiProof`] that uses decoded proofs, meaning these proofs are stored as a
308/// collection of [`TrieNode`]s instead of RLP-encoded bytes.
309#[derive(Clone, Default, Debug, PartialEq, Eq)]
310pub struct DecodedMultiProof {
311    /// State trie multiproof for requested accounts.
312    pub account_subtree: DecodedProofNodes,
313    /// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the account
314    /// proof.
315    pub branch_node_masks: BranchNodeMasksMap,
316    /// Storage trie multiproofs.
317    pub storages: B256Map<DecodedStorageMultiProof>,
318}
319
320impl DecodedMultiProof {
321    /// Returns true if the multiproof is empty.
322    pub fn is_empty(&self) -> bool {
323        self.account_subtree.is_empty() &&
324            self.branch_node_masks.is_empty() &&
325            self.storages.is_empty()
326    }
327
328    /// Return the account proof nodes for the given account path.
329    pub fn account_proof_nodes(&self, path: &Nibbles) -> Vec<(Nibbles, TrieNode)> {
330        self.account_subtree.matching_nodes_sorted(path)
331    }
332
333    /// Return the storage proof nodes for the given storage slots of the account path.
334    pub fn storage_proof_nodes(
335        &self,
336        hashed_address: B256,
337        slots: impl IntoIterator<Item = B256>,
338    ) -> Vec<(B256, Vec<(Nibbles, TrieNode)>)> {
339        self.storages
340            .get(&hashed_address)
341            .map(|storage_mp| {
342                slots
343                    .into_iter()
344                    .map(|slot| {
345                        let nibbles = Nibbles::unpack(slot);
346                        (slot, storage_mp.subtree.matching_nodes_sorted(&nibbles))
347                    })
348                    .collect()
349            })
350            .unwrap_or_default()
351    }
352
353    /// Construct the account proof from the multiproof.
354    pub fn account_proof(
355        &self,
356        address: Address,
357        slots: &[B256],
358    ) -> Result<DecodedAccountProof, alloy_rlp::Error> {
359        let hashed_address = keccak256(address);
360        let nibbles = Nibbles::unpack(hashed_address);
361
362        // Retrieve the account proof.
363        let proof = self
364            .account_proof_nodes(&nibbles)
365            .into_iter()
366            .map(|(_, node)| node)
367            .collect::<Vec<_>>();
368
369        // Inspect the last node in the proof. If it's a leaf node with matching suffix,
370        // then the node contains the encoded trie account.
371        let info = 'info: {
372            if let Some(TrieNode::Leaf(leaf)) = proof.last() &&
373                nibbles.ends_with(&leaf.key)
374            {
375                let account = TrieAccount::decode(&mut &leaf.value[..])?;
376                break 'info Some(Account::from(account))
377            }
378            None
379        };
380
381        // Retrieve proofs for requested storage slots.
382        let storage_multiproof = self.storages.get(&hashed_address);
383        let storage_root = storage_multiproof.map(|m| m.root).unwrap_or(EMPTY_ROOT_HASH);
384        let mut storage_proofs = Vec::with_capacity(slots.len());
385        for slot in slots {
386            let proof = if let Some(multiproof) = &storage_multiproof {
387                multiproof.storage_proof(*slot)?
388            } else {
389                DecodedStorageProof::new(*slot)
390            };
391            storage_proofs.push(proof);
392        }
393        Ok(DecodedAccountProof { address, info, proof, storage_root, storage_proofs })
394    }
395
396    /// Extends this multiproof with another one, merging both account and storage
397    /// proofs.
398    pub fn extend(&mut self, other: Self) {
399        self.account_subtree.extend_from(other.account_subtree);
400        self.branch_node_masks.extend(other.branch_node_masks);
401
402        let reserve = if self.storages.is_empty() {
403            other.storages.len()
404        } else {
405            other.storages.len().div_ceil(2)
406        };
407        self.storages.reserve(reserve);
408        for (hashed_address, storage) in other.storages {
409            match self.storages.entry(hashed_address) {
410                hash_map::Entry::Occupied(mut entry) => {
411                    debug_assert_eq!(entry.get().root, storage.root);
412                    let entry = entry.get_mut();
413                    entry.subtree.extend_from(storage.subtree);
414                    entry.branch_node_masks.extend(storage.branch_node_masks);
415                }
416                hash_map::Entry::Vacant(entry) => {
417                    entry.insert(storage);
418                }
419            }
420        }
421    }
422
423    /// Create a [`DecodedMultiProof`] from a [`DecodedStorageMultiProof`].
424    pub fn from_storage_proof(
425        hashed_address: B256,
426        storage_proof: DecodedStorageMultiProof,
427    ) -> Self {
428        Self {
429            storages: B256Map::from_iter([(hashed_address, storage_proof)]),
430            ..Default::default()
431        }
432    }
433}
434
435impl TryFrom<MultiProof> for DecodedMultiProof {
436    type Error = alloy_rlp::Error;
437
438    fn try_from(multi_proof: MultiProof) -> Result<Self, Self::Error> {
439        let account_subtree = DecodedProofNodes::try_from(multi_proof.account_subtree)?;
440        let storages = multi_proof
441            .storages
442            .into_iter()
443            .map(|(address, storage)| Ok((address, storage.try_into()?)))
444            .collect::<Result<B256Map<_>, alloy_rlp::Error>>()?;
445        Ok(Self { account_subtree, branch_node_masks: multi_proof.branch_node_masks, storages })
446    }
447}
448
449/// V2 decoded multiproof which contains the results of both account and storage V2 proof
450/// calculations.
451#[derive(Clone, Debug, PartialEq, Eq, Default)]
452pub struct DecodedMultiProofV2 {
453    /// Account trie proof nodes
454    pub account_proofs: Vec<ProofTrieNodeV2>,
455    /// Storage trie proof nodes indexed by account
456    pub storage_proofs: B256Map<Vec<ProofTrieNodeV2>>,
457}
458
459impl DecodedMultiProofV2 {
460    /// Returns true if there are no proofs
461    pub fn is_empty(&self) -> bool {
462        self.account_proofs.is_empty() && self.storage_proofs.is_empty()
463    }
464
465    /// Construct an account proof from the V2 multiproof.
466    pub fn account_proof(
467        &self,
468        address: Address,
469        slots: &[B256],
470    ) -> Result<AccountProof, alloy_rlp::Error> {
471        let hashed_address = keccak256(address);
472        let nibbles = Nibbles::unpack(hashed_address);
473        let account_nodes = matching_v2_proof_nodes(&self.account_proofs, &nibbles);
474
475        let (info, storage_root) = 'account: {
476            if let Some(ProofTrieNodeV2 { node: TrieNodeV2::Leaf(leaf), .. }) =
477                account_nodes.clone().last() &&
478                nibbles.ends_with(&leaf.key)
479            {
480                let account = TrieAccount::decode(&mut &leaf.value[..])?;
481                let storage_root = account.storage_root;
482                break 'account (Some(Account::from(account)), storage_root)
483            }
484            (None, EMPTY_ROOT_HASH)
485        };
486
487        let proof = encode_v2_proof_nodes(account_nodes);
488        let storage_nodes = self.storage_proofs.get(&hashed_address);
489        let mut storage_proofs = Vec::with_capacity(slots.len());
490        for slot in slots {
491            let nibbles = Nibbles::unpack(keccak256(slot));
492            let nodes =
493                storage_nodes.iter().flat_map(|nodes| matching_v2_proof_nodes(nodes, &nibbles));
494            let value = 'value: {
495                if let Some(ProofTrieNodeV2 { node: TrieNodeV2::Leaf(leaf), .. }) =
496                    nodes.clone().last() &&
497                    nibbles.ends_with(&leaf.key)
498                {
499                    break 'value U256::decode(&mut &leaf.value[..])?
500                }
501                U256::ZERO
502            };
503            storage_proofs.push(StorageProof {
504                key: *slot,
505                nibbles,
506                value,
507                proof: encode_v2_proof_nodes(nodes),
508            });
509        }
510
511        Ok(AccountProof { address, info, proof, storage_root, storage_proofs })
512    }
513
514    /// Builds a `DecodedMultiProofV2` from a flat witness map (hash → RLP-encoded trie node).
515    ///
516    /// This performs a BFS traversal starting from `state_root`, following hashed and inline
517    /// children and organizing decoded nodes into account and storage proof vectors. This is the
518    /// inverse of witness generation — it reconstructs the structured multiproof from the flat
519    /// format used in `ExecutionWitness`.
520    pub fn from_witness(
521        state_root: B256,
522        witness: &B256Map<impl AsRef<[u8]>>,
523    ) -> Result<Self, alloy_rlp::Error> {
524        Self::from_witness_with(state_root, |hash| {
525            witness.get(hash).map(|bytes| TrieNode::decode(&mut bytes.as_ref())).transpose()
526        })
527    }
528
529    /// Builds a `DecodedMultiProofV2` from a witness map of already decoded trie nodes, keyed by
530    /// node hash.
531    ///
532    /// Same traversal as [`Self::from_witness`], for callers that build multiproofs at several
533    /// roots over one witness and decode each node once. Inline children are still decoded from
534    /// their parent.
535    pub fn from_witness_decoded(
536        state_root: B256,
537        witness: &B256Map<TrieNode>,
538    ) -> Result<Self, alloy_rlp::Error> {
539        Self::from_witness_with(state_root, |hash| Ok(witness.get(hash).cloned()))
540    }
541
542    /// Builds a `DecodedMultiProofV2` by BFS from `state_root`, resolving hashed nodes with
543    /// `node_by_hash`.
544    ///
545    /// `node_by_hash` returns the decoded node for a hash, `None` to leave that node blinded, or
546    /// an error that aborts the traversal. Inline children are decoded from their parent.
547    pub fn from_witness_with(
548        state_root: B256,
549        mut node_by_hash: impl FnMut(&B256) -> Result<Option<TrieNode>, alloy_rlp::Error>,
550    ) -> Result<Self, alloy_rlp::Error> {
551        let mut account_nodes: Vec<(Nibbles, TrieNode, Option<BranchNodeMasks>)> = Vec::new();
552        let mut storage_nodes: B256Map<Vec<(Nibbles, TrieNode, Option<BranchNodeMasks>)>> =
553            B256Map::default();
554
555        let Some(root) = node_by_hash(&state_root)? else { return Ok(Self::default()) };
556        let mut queue = VecDeque::from([(root, Nibbles::default(), None)]);
557
558        while let Some((trie_node, path, maybe_account)) = queue.pop_front() {
559            match &trie_node {
560                TrieNode::Branch(branch) => {
561                    if path.len() >= 64 {
562                        return Err(alloy_rlp::Error::Custom("branch path exceeds 64 nibbles"));
563                    }
564                    for (idx, maybe_child) in branch.as_ref().children() {
565                        if let Some(child) = maybe_child {
566                            let mut child_path = path;
567                            child_path.push_unchecked(idx);
568                            let child = if let Some(hash) = child.as_hash() {
569                                let Some(node) = node_by_hash(&hash)? else { continue };
570                                node
571                            } else {
572                                TrieNode::decode(&mut child.as_slice())?
573                            };
574                            queue.push_back((child, child_path, maybe_account));
575                        }
576                    }
577                }
578                TrieNode::Extension(ext) => {
579                    if path.len() + ext.key.len() > 64 {
580                        return Err(alloy_rlp::Error::Custom("extension path exceeds 64 nibbles"));
581                    }
582                    let mut child_path = path;
583                    child_path.extend(&ext.key);
584                    let child = if let Some(hash) = ext.child.as_hash() {
585                        let Some(node) = node_by_hash(&hash)? else { continue };
586                        node
587                    } else {
588                        TrieNode::decode(&mut ext.child.as_slice())?
589                    };
590                    queue.push_back((child, child_path, maybe_account));
591                }
592                TrieNode::Leaf(leaf) => {
593                    if path.len() + leaf.key.len() != 64 {
594                        return Err(alloy_rlp::Error::Custom("leaf path must contain 64 nibbles"));
595                    }
596                    if maybe_account.is_none() {
597                        let mut full_path = path;
598                        full_path.extend(&leaf.key);
599                        let hashed_address = B256::from_slice(&full_path.pack());
600                        let account = TrieAccount::decode(&mut &leaf.value[..])?;
601                        if account.storage_root != EMPTY_ROOT_HASH &&
602                            let Some(node) = node_by_hash(&account.storage_root)?
603                        {
604                            queue.push_back((node, Nibbles::default(), Some(hashed_address)));
605                        }
606                    }
607                }
608                TrieNode::EmptyRoot => {}
609            }
610
611            if let Some(account) = maybe_account {
612                storage_nodes.entry(account).or_default().push((path, trie_node, None));
613            } else {
614                account_nodes.push((path, trie_node, None));
615            }
616        }
617
618        account_nodes.sort_by(|(a, _, _), (b, _, _)| crate::depth_first_cmp(a, b));
619        let account_proofs = ProofTrieNodeV2::from_sorted_trie_nodes(account_nodes);
620
621        let mut storage_proofs = B256Map::default();
622        for (account, mut nodes) in storage_nodes {
623            nodes.sort_by(|(a, _, _), (b, _, _)| crate::depth_first_cmp(a, b));
624            storage_proofs.insert(account, ProofTrieNodeV2::from_sorted_trie_nodes(nodes));
625        }
626
627        Ok(Self { account_proofs, storage_proofs })
628    }
629
630    /// Appends the given multiproof's data to this one.
631    ///
632    /// This implementation does not deduplicate redundant proofs.
633    pub fn extend(&mut self, other: Self) {
634        self.account_proofs.extend(other.account_proofs);
635        for (hashed_address, other_storage_proofs) in other.storage_proofs {
636            match self.storage_proofs.entry(hashed_address) {
637                hash_map::Entry::Vacant(entry) => {
638                    entry.insert(other_storage_proofs);
639                }
640                hash_map::Entry::Occupied(mut entry) => {
641                    entry.get_mut().extend(other_storage_proofs);
642                }
643            }
644        }
645    }
646}
647
648/// Returns proof nodes matching `path` in root-to-leaf order.
649fn matching_v2_proof_nodes<'a>(
650    nodes: &'a [ProofTrieNodeV2],
651    path: &'a Nibbles,
652) -> impl Iterator<Item = &'a ProofTrieNodeV2> + Clone {
653    nodes.iter().rev().filter(move |node| path.starts_with(&node.path))
654}
655
656/// Encodes V2 proof nodes as standard MPT proof nodes.
657///
658/// Inline children are already encoded in their parent and must not be emitted separately.
659/// V2 combines extensions with their child branches, so hashed branches are emitted separately.
660fn encode_v2_proof_nodes<'a>(nodes: impl Iterator<Item = &'a ProofTrieNodeV2>) -> Vec<Bytes> {
661    let mut proof = Vec::new();
662    for proof_node in nodes {
663        let mut encoded = Vec::new();
664        proof_node.node.encode(&mut encoded);
665        if proof_node.path.is_empty() || encoded.len() >= B256::len_bytes() {
666            proof.push(Bytes::from(encoded));
667        }
668
669        if let TrieNodeV2::Branch(branch) = &proof_node.node &&
670            !branch.key.is_empty() &&
671            branch.branch_rlp_node.as_ref().is_some_and(|node| node.is_hash())
672        {
673            let mut encoded = Vec::new();
674            BranchNodeRef::new(&branch.stack, branch.state_mask).encode(&mut encoded);
675            proof.push(Bytes::from(encoded));
676        }
677    }
678    proof
679}
680
681impl From<DecodedMultiProof> for DecodedMultiProofV2 {
682    fn from(proof: DecodedMultiProof) -> Self {
683        let account_proofs =
684            decoded_proof_nodes_to_v2(proof.account_subtree, &proof.branch_node_masks);
685        let storage_proofs = proof
686            .storages
687            .into_iter()
688            .map(|(address, storage)| {
689                (address, decoded_proof_nodes_to_v2(storage.subtree, &storage.branch_node_masks))
690            })
691            .collect();
692        Self { account_proofs, storage_proofs }
693    }
694}
695
696/// Converts a [`DecodedProofNodes`] (path → [`TrieNode`] map) into a `Vec<ProofTrieNodeV2>`,
697/// merging extension nodes into their child branch nodes.
698fn decoded_proof_nodes_to_v2(
699    nodes: DecodedProofNodes,
700    masks: &BranchNodeMasksMap,
701) -> Vec<ProofTrieNodeV2> {
702    let mut sorted: Vec<_> = nodes.into_inner().into_iter().collect();
703    sorted.sort_unstable_by(|a, b| crate::depth_first_cmp(&a.0, &b.0));
704    ProofTrieNodeV2::from_sorted_trie_nodes(
705        sorted.into_iter().map(|(path, node)| (path, node, masks.get(&path).copied())),
706    )
707}
708
709/// The merkle multiproof of storage trie.
710#[derive(Clone, Debug, PartialEq, Eq)]
711pub struct StorageMultiProof {
712    /// Storage trie root.
713    pub root: B256,
714    /// Storage multiproof for requested slots.
715    pub subtree: ProofNodes,
716    /// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the storage
717    /// proof.
718    pub branch_node_masks: BranchNodeMasksMap,
719}
720
721impl StorageMultiProof {
722    /// Create new storage multiproof for empty trie.
723    pub fn empty() -> Self {
724        Self {
725            root: EMPTY_ROOT_HASH,
726            subtree: ProofNodes::from_iter([(
727                Nibbles::default(),
728                Bytes::from([EMPTY_STRING_CODE]),
729            )]),
730            branch_node_masks: BranchNodeMasksMap::default(),
731        }
732    }
733
734    /// Return storage proofs for the target storage slot (unhashed).
735    pub fn storage_proof(&self, slot: B256) -> Result<StorageProof, alloy_rlp::Error> {
736        let nibbles = Nibbles::unpack(keccak256(slot));
737
738        // Retrieve the storage proof.
739        let proof = self
740            .subtree
741            .matching_nodes_iter(&nibbles)
742            .sorted_by(|a, b| a.0.cmp(b.0))
743            .map(|(_, node)| node.clone())
744            .collect::<Vec<_>>();
745
746        // Inspect the last node in the proof. If it's a leaf node with matching suffix,
747        // then the node contains the encoded slot value.
748        let value = 'value: {
749            if let Some(last) = proof.last() &&
750                let TrieNode::Leaf(leaf) = TrieNode::decode(&mut &last[..])? &&
751                nibbles.ends_with(&leaf.key)
752            {
753                break 'value U256::decode(&mut &leaf.value[..])?
754            }
755            U256::ZERO
756        };
757
758        Ok(StorageProof { key: slot, nibbles, value, proof })
759    }
760}
761
762/// The decoded merkle multiproof for a storage trie.
763#[derive(Clone, Debug, PartialEq, Eq)]
764pub struct DecodedStorageMultiProof {
765    /// Storage trie root.
766    pub root: B256,
767    /// Storage multiproof for requested slots.
768    pub subtree: DecodedProofNodes,
769    /// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the storage
770    /// proof.
771    pub branch_node_masks: BranchNodeMasksMap,
772}
773
774impl DecodedStorageMultiProof {
775    /// Create new storage multiproof for empty trie.
776    pub fn empty() -> Self {
777        Self {
778            root: EMPTY_ROOT_HASH,
779            subtree: DecodedProofNodes::from_iter([(Nibbles::default(), TrieNode::EmptyRoot)]),
780            branch_node_masks: BranchNodeMasksMap::default(),
781        }
782    }
783
784    /// Return storage proofs for the target storage slot (unhashed).
785    pub fn storage_proof(&self, slot: B256) -> Result<DecodedStorageProof, alloy_rlp::Error> {
786        let nibbles = Nibbles::unpack(keccak256(slot));
787
788        // Retrieve the storage proof.
789        let proof = self
790            .subtree
791            .matching_nodes_iter(&nibbles)
792            .sorted_by(|a, b| a.0.cmp(b.0))
793            .map(|(_, node)| node.clone())
794            .collect::<Vec<_>>();
795
796        // Inspect the last node in the proof. If it's a leaf node with matching suffix,
797        // then the node contains the encoded slot value.
798        let value = 'value: {
799            if let Some(TrieNode::Leaf(leaf)) = proof.last() &&
800                nibbles.ends_with(&leaf.key)
801            {
802                break 'value U256::decode(&mut &leaf.value[..])?
803            }
804            U256::ZERO
805        };
806
807        Ok(DecodedStorageProof { key: slot, nibbles, value, proof })
808    }
809}
810
811impl TryFrom<StorageMultiProof> for DecodedStorageMultiProof {
812    type Error = alloy_rlp::Error;
813
814    fn try_from(multi_proof: StorageMultiProof) -> Result<Self, Self::Error> {
815        let subtree = DecodedProofNodes::try_from(multi_proof.subtree)?;
816        Ok(Self {
817            root: multi_proof.root,
818            subtree,
819            branch_node_masks: multi_proof.branch_node_masks,
820        })
821    }
822}
823
824/// The merkle proof with the relevant account info.
825#[derive(Clone, PartialEq, Eq, Debug)]
826#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
827#[cfg_attr(any(test, feature = "serde"), serde(rename_all = "camelCase"))]
828pub struct AccountProof {
829    /// The address associated with the account.
830    pub address: Address,
831    /// Account info, if any.
832    pub info: Option<Account>,
833    /// Array of rlp-serialized merkle trie nodes which starting from the root node and
834    /// following the path of the hashed address as key.
835    pub proof: Vec<Bytes>,
836    /// The storage trie root.
837    pub storage_root: B256,
838    /// Array of storage proofs as requested.
839    pub storage_proofs: Vec<StorageProof>,
840}
841
842/// Normalize an empty-trie proof for the EIP-1186 (`eth_getProof`) response.
843///
844/// An empty trie is internally represented by a single empty-root sentinel node (`0x80`, the
845/// RLP empty string whose hash is `EMPTY_ROOT_HASH`). EIP-1186 defines the proof field as the
846/// array of trie nodes along the key path; an empty trie has none, and geth returns `[]`. This
847/// strips that lone sentinel so the response matches geth and the spec. It is applied only at
848/// the response boundary, leaving the underlying proof construction unchanged.
849#[cfg(feature = "eip1186")]
850fn normalize_eip1186_empty_trie_proof(proof: Vec<Bytes>) -> Vec<Bytes> {
851    if proof.len() == 1 && proof[0].as_ref() == [EMPTY_STRING_CODE] {
852        Vec::new()
853    } else {
854        proof
855    }
856}
857
858#[cfg(feature = "eip1186")]
859impl AccountProof {
860    /// Convert into an EIP-1186 account proof response.
861    ///
862    /// For non-existent accounts, this returns `KECCAK_EMPTY` for `codeHash` and
863    /// `EMPTY_ROOT_HASH` for `storageHash`, matching reth's default behavior.
864    ///
865    /// Use [`Self::into_eip1186_response_with`] to customize the behavior for
866    /// non-existent accounts (e.g. returning `B256::ZERO` for geth compatibility).
867    pub fn into_eip1186_response(
868        self,
869        slots: Vec<alloy_serde::JsonStorageKey>,
870    ) -> alloy_rpc_types_eth::EIP1186AccountProofResponse {
871        self.into_eip1186_response_with(slots, false)
872    }
873
874    /// Convert into an EIP-1186 account proof response, with optional geth-compatible
875    /// zero hashes for non-existent accounts.
876    ///
877    /// When `zero_empty_account` is `true`, non-existent accounts return `B256::ZERO`
878    /// for both `codeHash` and `storageHash`, matching geth's behavior since v1.13.4
879    /// ([go-ethereum#28357](https://github.com/ethereum/go-ethereum/pull/28357)).
880    ///
881    /// When `false`, returns `KECCAK_EMPTY` / `EMPTY_ROOT_HASH` (reth default).
882    ///
883    /// See: <https://github.com/ethereum/go-ethereum/issues/28441>
884    pub fn into_eip1186_response_with(
885        self,
886        slots: Vec<alloy_serde::JsonStorageKey>,
887        zero_empty_account: bool,
888    ) -> alloy_rpc_types_eth::EIP1186AccountProofResponse {
889        // The proof nodes carry the complete account RLP, including any account extension.
890        let is_non_existent = self.info.is_none();
891        let info = self.info.unwrap_or_default();
892        let (code_hash, storage_hash) = if is_non_existent && zero_empty_account {
893            (B256::ZERO, B256::ZERO)
894        } else {
895            (info.get_bytecode_hash(), self.storage_root)
896        };
897        alloy_rpc_types_eth::EIP1186AccountProofResponse {
898            address: self.address,
899            balance: info.balance,
900            code_hash,
901            nonce: info.nonce,
902            storage_hash,
903            account_proof: normalize_eip1186_empty_trie_proof(self.proof),
904            storage_proof: self
905                .storage_proofs
906                .into_iter()
907                .zip(slots)
908                .map(|(proof, slot)| proof.into_eip1186_proof(slot))
909                .collect(),
910        }
911    }
912
913    /// Converts an
914    /// [`EIP1186AccountProofResponse`](alloy_rpc_types_eth::EIP1186AccountProofResponse) to an
915    /// [`AccountProof`].
916    ///
917    /// This is the inverse of [`Self::into_eip1186_response`]
918    #[allow(clippy::needless_update)]
919    pub fn from_eip1186_proof(proof: alloy_rpc_types_eth::EIP1186AccountProofResponse) -> Self {
920        let alloy_rpc_types_eth::EIP1186AccountProofResponse {
921            nonce,
922            address,
923            balance,
924            code_hash,
925            storage_hash,
926            account_proof,
927            storage_proof,
928            ..
929        } = proof;
930        let storage_proofs = storage_proof.into_iter().map(Into::into).collect();
931
932        // EIP-1186's summary fields omit the account extension, so take it from the account leaf
933        // of an inclusion proof.
934        let leaf_account = Account::EXTENSIONS_ENABLED
935            .then(|| inclusion_proof_account(address, &account_proof))
936            .flatten();
937
938        let (storage_root, info) = if nonce == 0 &&
939            balance.is_zero() &&
940            (storage_hash.is_zero() || storage_hash == EMPTY_ROOT_HASH) &&
941            (code_hash == alloy_consensus::constants::KECCAK_EMPTY || code_hash.is_zero()) &&
942            !leaf_account.as_ref().is_some_and(Account::has_extension)
943        {
944            // Account does not exist in state. Return `None` here to prevent proof
945            // verification.
946            //
947            // Note: geth (since v1.13.4, go-ethereum#28357) returns `B256::ZERO` for
948            // both `codeHash` and `storageHash` in exclusion proofs, while reth
949            // returns `KECCAK_EMPTY` / `EMPTY_ROOT_HASH`. We accept both formats here
950            // so that proofs obtained from any client can be deserialized correctly.
951            // See: https://github.com/ethereum/go-ethereum/issues/28441
952            (EMPTY_ROOT_HASH, None)
953        } else {
954            (
955                storage_hash,
956                Some(Account {
957                    nonce,
958                    balance,
959                    bytecode_hash: code_hash.into(),
960                    ..leaf_account.unwrap_or_default()
961                }),
962            )
963        };
964
965        Self { address, info, proof: account_proof, storage_root, storage_proofs }
966    }
967}
968
969#[cfg(feature = "eip1186")]
970impl From<alloy_rpc_types_eth::EIP1186AccountProofResponse> for AccountProof {
971    fn from(proof: alloy_rpc_types_eth::EIP1186AccountProofResponse) -> Self {
972        Self::from_eip1186_proof(proof)
973    }
974}
975
976/// Returns the account committed by the leaf of an inclusion proof for `address`.
977///
978/// The leaf has to lie on the full path of `address`, so an exclusion proof cannot supply another
979/// account. This does not authenticate the proof root; callers still have to verify the proof
980/// against a trusted state root.
981#[cfg(feature = "eip1186")]
982fn inclusion_proof_account(address: Address, proof: &[Bytes]) -> Option<Account> {
983    let root = keccak256(proof.first()?);
984    let TrieNode::Leaf(leaf) = alloy_rlp::decode_exact::<TrieNode>(proof.last()?).ok()? else {
985        return None
986    };
987    verify_proof(root, Nibbles::unpack(keccak256(address)), Some(leaf.value.clone()), proof)
988        .ok()?;
989    alloy_rlp::decode_exact::<TrieAccount>(&leaf.value).ok().map(Account::from)
990}
991
992impl Default for AccountProof {
993    fn default() -> Self {
994        Self::new(Address::default())
995    }
996}
997
998impl AccountProof {
999    /// Create new account proof entity.
1000    pub const fn new(address: Address) -> Self {
1001        Self {
1002            address,
1003            info: None,
1004            proof: Vec::new(),
1005            storage_root: EMPTY_ROOT_HASH,
1006            storage_proofs: Vec::new(),
1007        }
1008    }
1009
1010    /// Verify the storage proofs and account proof against the provided state root.
1011    #[allow(clippy::clone_on_copy)]
1012    pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> {
1013        // Verify storage proofs.
1014        for storage_proof in &self.storage_proofs {
1015            storage_proof.verify(self.storage_root)?;
1016        }
1017
1018        // Verify the account proof.
1019        let expected = if self.info.is_none() && self.storage_root == EMPTY_ROOT_HASH {
1020            None
1021        } else {
1022            Some(alloy_rlp::encode(
1023                self.info.clone().unwrap_or_default().into_trie_account(self.storage_root),
1024            ))
1025        };
1026        let nibbles = Nibbles::unpack(keccak256(self.address));
1027        verify_proof(root, nibbles, expected, &self.proof)
1028    }
1029}
1030
1031/// The merkle proof with the relevant account info.
1032#[derive(Clone, PartialEq, Eq, Debug)]
1033pub struct DecodedAccountProof {
1034    /// The address associated with the account.
1035    pub address: Address,
1036    /// Account info.
1037    pub info: Option<Account>,
1038    /// Array of merkle trie nodes which starting from the root node and following the path of the
1039    /// hashed address as key.
1040    pub proof: Vec<TrieNode>,
1041    /// The storage trie root.
1042    pub storage_root: B256,
1043    /// Array of storage proofs as requested.
1044    pub storage_proofs: Vec<DecodedStorageProof>,
1045}
1046
1047impl Default for DecodedAccountProof {
1048    fn default() -> Self {
1049        Self::new(Address::default())
1050    }
1051}
1052
1053impl DecodedAccountProof {
1054    /// Create new account proof entity.
1055    pub const fn new(address: Address) -> Self {
1056        Self {
1057            address,
1058            info: None,
1059            proof: Vec::new(),
1060            storage_root: EMPTY_ROOT_HASH,
1061            storage_proofs: Vec::new(),
1062        }
1063    }
1064}
1065
1066/// The merkle proof of the storage entry.
1067#[derive(Clone, PartialEq, Eq, Default, Debug)]
1068#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
1069pub struct StorageProof {
1070    /// The raw storage key.
1071    pub key: B256,
1072    /// The hashed storage key nibbles.
1073    pub nibbles: Nibbles,
1074    /// The storage value.
1075    pub value: U256,
1076    /// Array of rlp-serialized merkle trie nodes which starting from the storage root node and
1077    /// following the path of the hashed storage slot as key.
1078    pub proof: Vec<Bytes>,
1079}
1080
1081impl StorageProof {
1082    /// Create new storage proof from the storage slot.
1083    pub fn new(key: B256) -> Self {
1084        let nibbles = Nibbles::unpack(keccak256(key));
1085        Self { key, nibbles, ..Default::default() }
1086    }
1087
1088    /// Create new storage proof from the storage slot and its pre-hashed image.
1089    pub fn new_with_hashed(key: B256, hashed_key: B256) -> Self {
1090        Self { key, nibbles: Nibbles::unpack(hashed_key), ..Default::default() }
1091    }
1092
1093    /// Create new storage proof from the storage slot and its pre-hashed image.
1094    pub fn new_with_nibbles(key: B256, nibbles: Nibbles) -> Self {
1095        Self { key, nibbles, ..Default::default() }
1096    }
1097
1098    /// Set proof nodes on storage proof.
1099    pub fn with_proof(mut self, proof: Vec<Bytes>) -> Self {
1100        self.proof = proof;
1101        self
1102    }
1103
1104    /// Verify the proof against the provided storage root.
1105    pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> {
1106        let expected =
1107            if self.value.is_zero() { None } else { Some(encode_fixed_size(&self.value).to_vec()) };
1108        verify_proof(root, self.nibbles, expected, &self.proof)
1109    }
1110}
1111
1112#[cfg(feature = "eip1186")]
1113impl StorageProof {
1114    /// Convert into an EIP-1186 storage proof
1115    pub fn into_eip1186_proof(
1116        self,
1117        slot: alloy_serde::JsonStorageKey,
1118    ) -> alloy_rpc_types_eth::EIP1186StorageProof {
1119        alloy_rpc_types_eth::EIP1186StorageProof {
1120            key: slot,
1121            value: self.value,
1122            proof: normalize_eip1186_empty_trie_proof(self.proof),
1123        }
1124    }
1125
1126    /// Convert from an
1127    /// [`EIP1186StorageProof`](alloy_rpc_types_eth::EIP1186StorageProof)
1128    ///
1129    /// This is the inverse of [`Self::into_eip1186_proof`].
1130    pub fn from_eip1186_proof(storage_proof: alloy_rpc_types_eth::EIP1186StorageProof) -> Self {
1131        Self {
1132            value: storage_proof.value,
1133            proof: storage_proof.proof,
1134            ..Self::new(storage_proof.key.as_b256())
1135        }
1136    }
1137}
1138
1139#[cfg(feature = "eip1186")]
1140impl From<alloy_rpc_types_eth::EIP1186StorageProof> for StorageProof {
1141    fn from(proof: alloy_rpc_types_eth::EIP1186StorageProof) -> Self {
1142        Self::from_eip1186_proof(proof)
1143    }
1144}
1145
1146/// The merkle proof of the storage entry, using decoded proofs.
1147#[derive(Clone, PartialEq, Eq, Default, Debug)]
1148pub struct DecodedStorageProof {
1149    /// The raw storage key.
1150    pub key: B256,
1151    /// The hashed storage key nibbles.
1152    pub nibbles: Nibbles,
1153    /// The storage value.
1154    pub value: U256,
1155    /// Array of merkle trie nodes which starting from the storage root node and following the path
1156    /// of the hashed storage slot as key.
1157    pub proof: Vec<TrieNode>,
1158}
1159
1160impl DecodedStorageProof {
1161    /// Create new storage proof from the storage slot.
1162    pub fn new(key: B256) -> Self {
1163        let nibbles = Nibbles::unpack(keccak256(key));
1164        Self { key, nibbles, ..Default::default() }
1165    }
1166
1167    /// Create new storage proof from the storage slot and its pre-hashed image.
1168    pub fn new_with_hashed(key: B256, hashed_key: B256) -> Self {
1169        Self { key, nibbles: Nibbles::unpack(hashed_key), ..Default::default() }
1170    }
1171
1172    /// Create new storage proof from the storage slot and its pre-hashed image.
1173    pub fn new_with_nibbles(key: B256, nibbles: Nibbles) -> Self {
1174        Self { key, nibbles, ..Default::default() }
1175    }
1176
1177    /// Set proof nodes on storage proof.
1178    pub fn with_proof(mut self, proof: Vec<TrieNode>) -> Self {
1179        self.proof = proof;
1180        self
1181    }
1182}
1183
1184/// Implementation of hasher using our keccak256 hashing function
1185/// for compatibility with `triehash` crate.
1186#[cfg(any(test, feature = "test-utils"))]
1187pub mod triehash {
1188    use alloy_primitives::{keccak256, B256};
1189    use alloy_rlp::RlpEncodable;
1190    use hash_db::Hasher;
1191    use plain_hasher::PlainHasher;
1192
1193    /// A [Hasher] that calculates a keccak256 hash of the given data.
1194    #[derive(Default, Debug, Clone, PartialEq, Eq, RlpEncodable)]
1195    #[non_exhaustive]
1196    pub struct KeccakHasher;
1197
1198    #[cfg(any(test, feature = "test-utils"))]
1199    impl Hasher for KeccakHasher {
1200        type Out = B256;
1201        type StdHasher = PlainHasher;
1202
1203        const LENGTH: usize = 32;
1204
1205        fn hash(x: &[u8]) -> Self::Out {
1206            keccak256(x)
1207        }
1208    }
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213    use super::*;
1214    use alloy_consensus::constants::KECCAK_EMPTY;
1215    use alloy_trie::{
1216        nodes::{BranchNode, ExtensionNode, LeafNode, RlpNode},
1217        TrieMask,
1218    };
1219
1220    #[test]
1221    fn v2_account_proof_expands_extension_branch() {
1222        let branch =
1223            BranchNode::new(vec![RlpNode::word_rlp(&B256::repeat_byte(0x11))], TrieMask::from(1));
1224        let branch_rlp = alloy_rlp::encode(&branch);
1225        let combined = TrieNodeV2::Branch(crate::BranchNodeV2::new(
1226            Nibbles::from_nibbles([0xa, 0xb]),
1227            branch.stack,
1228            branch.state_mask,
1229            Some(RlpNode::from_rlp(&branch_rlp)),
1230        ));
1231        let extension_rlp = alloy_rlp::encode(&combined);
1232        let multiproof = DecodedMultiProofV2 {
1233            account_proofs: vec![ProofTrieNodeV2 {
1234                path: Nibbles::default(),
1235                node: combined,
1236                masks: None,
1237            }],
1238            ..Default::default()
1239        };
1240
1241        let proof = multiproof.account_proof(Address::ZERO, &[]).unwrap();
1242
1243        assert_eq!(proof.proof, vec![Bytes::from(extension_rlp), Bytes::from(branch_rlp)]);
1244        assert!(proof.info.is_none());
1245    }
1246
1247    fn decode_witness(witness: &B256Map<impl AsRef<[u8]>>) -> B256Map<TrieNode> {
1248        witness
1249            .iter()
1250            .map(|(hash, bytes)| (*hash, TrieNode::decode(&mut bytes.as_ref()).unwrap()))
1251            .collect()
1252    }
1253
1254    #[test]
1255    fn witness_nodes_are_depth_first_ordered() {
1256        fn insert_node(witness: &mut B256Map<Bytes>, node: impl alloy_rlp::Encodable) -> RlpNode {
1257            let encoded = alloy_rlp::encode(node);
1258            witness.insert(keccak256(&encoded), encoded.clone().into());
1259            RlpNode::from_rlp(&encoded)
1260        }
1261
1262        let mut witness = B256Map::default();
1263        let leaf_key = Nibbles::from_nibbles([0; 63]);
1264
1265        let storage_leaf_0 = insert_node(
1266            &mut witness,
1267            LeafNode::new(leaf_key, encode_fixed_size(&U256::ONE).to_vec()),
1268        );
1269        let storage_leaf_1 = insert_node(
1270            &mut witness,
1271            LeafNode::new(leaf_key, encode_fixed_size(&U256::from(2)).to_vec()),
1272        );
1273        let storage_root = insert_node(
1274            &mut witness,
1275            BranchNode::new(vec![storage_leaf_0, storage_leaf_1], TrieMask::new(0b11)),
1276        );
1277
1278        let account_leaf_0 = insert_node(
1279            &mut witness,
1280            LeafNode::new(
1281                leaf_key,
1282                alloy_rlp::encode(TrieAccount {
1283                    storage_root: storage_root.as_hash().expect("storage root is hashed"),
1284                    ..Default::default()
1285                }),
1286            ),
1287        );
1288        let account_leaf_1 = insert_node(
1289            &mut witness,
1290            LeafNode::new(leaf_key, alloy_rlp::encode(TrieAccount::default())),
1291        );
1292        let state_root = insert_node(
1293            &mut witness,
1294            BranchNode::new(vec![account_leaf_0, account_leaf_1], TrieMask::new(0b11)),
1295        )
1296        .as_hash()
1297        .expect("state root is hashed");
1298
1299        let proof = DecodedMultiProofV2::from_witness(state_root, &witness).unwrap();
1300        assert_eq!(
1301            DecodedMultiProofV2::from_witness_decoded(state_root, &decode_witness(&witness)),
1302            Ok(proof.clone())
1303        );
1304        let expected_paths =
1305            [Nibbles::from_nibbles([0]), Nibbles::from_nibbles([1]), Nibbles::default()];
1306
1307        assert_eq!(
1308            proof.account_proofs.iter().map(|node| node.path).collect::<Vec<_>>(),
1309            expected_paths
1310        );
1311        assert_eq!(
1312            proof.storage_proofs[&B256::ZERO].iter().map(|node| node.path).collect::<Vec<_>>(),
1313            expected_paths
1314        );
1315    }
1316
1317    #[test]
1318    fn witness_nodes_follow_inline_children() {
1319        let leaf_key = Nibbles::from_nibbles([0]);
1320        let leaf_0 =
1321            alloy_rlp::encode(LeafNode::new(leaf_key, encode_fixed_size(&U256::ONE).to_vec()));
1322        let leaf_1 =
1323            alloy_rlp::encode(LeafNode::new(leaf_key, encode_fixed_size(&U256::from(2)).to_vec()));
1324        assert!(leaf_0.len() < B256::len_bytes());
1325        assert!(leaf_1.len() < B256::len_bytes());
1326
1327        let branch = alloy_rlp::encode(BranchNode::new(
1328            vec![RlpNode::from_rlp(&leaf_0), RlpNode::from_rlp(&leaf_1)],
1329            TrieMask::new(0b11),
1330        ));
1331        assert!(branch.len() < B256::len_bytes());
1332
1333        let extension_key = Nibbles::from_nibbles([0; 62]);
1334        let storage_root_node =
1335            alloy_rlp::encode(ExtensionNode::new(extension_key, RlpNode::from_rlp(&branch)));
1336        let storage_root = keccak256(&storage_root_node);
1337
1338        let account_root_node = alloy_rlp::encode(LeafNode::new(
1339            Nibbles::from_nibbles([0; 64]),
1340            alloy_rlp::encode(TrieAccount { storage_root, ..Default::default() }),
1341        ));
1342        let state_root = keccak256(&account_root_node);
1343        let witness = B256Map::from_iter([
1344            (state_root, Bytes::from(account_root_node)),
1345            (storage_root, Bytes::from(storage_root_node)),
1346        ]);
1347
1348        let proof = DecodedMultiProofV2::from_witness(state_root, &witness).unwrap();
1349        assert_eq!(
1350            DecodedMultiProofV2::from_witness_decoded(state_root, &decode_witness(&witness)),
1351            Ok(proof.clone())
1352        );
1353        let mut leaf_0_path = extension_key;
1354        leaf_0_path.push_unchecked(0);
1355        let mut leaf_1_path = extension_key;
1356        leaf_1_path.push_unchecked(1);
1357
1358        assert_eq!(
1359            proof.storage_proofs[&B256::ZERO].iter().map(|node| node.path).collect::<Vec<_>>(),
1360            [leaf_0_path, leaf_1_path, Nibbles::default()]
1361        );
1362
1363        let nodes = &proof.storage_proofs[&B256::ZERO];
1364        for (path, value) in [(leaf_0_path, 1), (leaf_1_path, 2)] {
1365            let key = path.join(&leaf_key);
1366            let encoded = encode_v2_proof_nodes(matching_v2_proof_nodes(nodes, &key));
1367            assert_eq!(encoded, vec![witness[&storage_root].clone()]);
1368            verify_proof(storage_root, key, Some(alloy_rlp::encode(U256::from(value))), &encoded)
1369                .unwrap();
1370        }
1371    }
1372
1373    #[test]
1374    fn v2_proof_encoding_keeps_short_root() {
1375        let leaf = LeafNode::new(Nibbles::default(), vec![1]);
1376        let encoded = alloy_rlp::encode(&leaf);
1377        assert!(encoded.len() < 32);
1378        let nodes = [ProofTrieNodeV2 {
1379            path: Nibbles::default(),
1380            node: TrieNodeV2::Leaf(leaf),
1381            masks: None,
1382        }];
1383        assert_eq!(encode_v2_proof_nodes(nodes.iter()), vec![Bytes::from(encoded)]);
1384    }
1385
1386    #[test]
1387    fn witness_rejects_invalid_path_lengths() {
1388        let leaf = alloy_rlp::encode(LeafNode::new(Nibbles::default(), vec![1]));
1389        let branch = alloy_rlp::encode(BranchNode::new(
1390            vec![RlpNode::from_rlp(&leaf), RlpNode::from_rlp(&leaf)],
1391            TrieMask::new(0b11),
1392        ));
1393        let extension = alloy_rlp::encode(ExtensionNode::new(
1394            Nibbles::from_nibbles([0]),
1395            RlpNode::from_rlp(&branch),
1396        ));
1397
1398        let long_leaf = alloy_rlp::encode(LeafNode::new(Nibbles::from_nibbles([0]), vec![1]));
1399        for (child, depth, expected) in [
1400            (&branch, 64, "branch path exceeds 64 nibbles"),
1401            (&extension, 64, "extension path exceeds 64 nibbles"),
1402            (&leaf, 63, "leaf path must contain 64 nibbles"),
1403            (&long_leaf, 64, "leaf path must contain 64 nibbles"),
1404        ] {
1405            let storage = alloy_rlp::encode(ExtensionNode::new(
1406                Nibbles::from_nibbles(vec![0; depth]),
1407                RlpNode::from_rlp(child),
1408            ));
1409            let storage_root = keccak256(&storage);
1410            let account = alloy_rlp::encode(LeafNode::new(
1411                Nibbles::from_nibbles([0; 64]),
1412                alloy_rlp::encode(TrieAccount { storage_root, ..Default::default() }),
1413            ));
1414            let state_root = keccak256(&account);
1415            let witness = B256Map::from_iter([(state_root, account), (storage_root, storage)]);
1416            assert_eq!(
1417                DecodedMultiProofV2::from_witness(state_root, &witness),
1418                Err(alloy_rlp::Error::Custom(expected)),
1419            );
1420            assert_eq!(
1421                DecodedMultiProofV2::from_witness_decoded(state_root, &decode_witness(&witness)),
1422                Err(alloy_rlp::Error::Custom(expected)),
1423            );
1424        }
1425
1426        for length in [63, 65] {
1427            // A branch adds one nibble to the account leaf's path.
1428            let account = alloy_rlp::encode(LeafNode::new(
1429                Nibbles::from_nibbles(vec![0; length - 1]),
1430                alloy_rlp::encode(TrieAccount::default()),
1431            ));
1432            let hash = keccak256(&account);
1433            let root = alloy_rlp::encode(BranchNode::new(
1434                vec![RlpNode::word_rlp(&hash), RlpNode::word_rlp(&hash)],
1435                TrieMask::new(0b11),
1436            ));
1437            let state_root = keccak256(&root);
1438            let witness = B256Map::from_iter([(state_root, root), (hash, account)]);
1439            assert_eq!(
1440                DecodedMultiProofV2::from_witness(state_root, &witness),
1441                Err(alloy_rlp::Error::Custom("leaf path must contain 64 nibbles")),
1442            );
1443            assert_eq!(
1444                DecodedMultiProofV2::from_witness_decoded(state_root, &decode_witness(&witness)),
1445                Err(alloy_rlp::Error::Custom("leaf path must contain 64 nibbles")),
1446            );
1447        }
1448    }
1449
1450    #[test]
1451    fn test_multiproof_extend_account_proofs() {
1452        let mut proof1 = MultiProof::default();
1453        let mut proof2 = MultiProof::default();
1454
1455        let addr1 = B256::random();
1456        let addr2 = B256::random();
1457
1458        proof1.account_subtree.insert(
1459            Nibbles::unpack(addr1),
1460            alloy_rlp::encode_fixed_size(&U256::from(42)).to_vec().into(),
1461        );
1462        proof2.account_subtree.insert(
1463            Nibbles::unpack(addr2),
1464            alloy_rlp::encode_fixed_size(&U256::from(43)).to_vec().into(),
1465        );
1466
1467        proof1.extend(proof2);
1468
1469        assert!(proof1.account_subtree.contains_key(&Nibbles::unpack(addr1)));
1470        assert!(proof1.account_subtree.contains_key(&Nibbles::unpack(addr2)));
1471    }
1472
1473    #[test]
1474    fn test_multiproof_extend_storage_proofs() {
1475        let mut proof1 = MultiProof::default();
1476        let mut proof2 = MultiProof::default();
1477
1478        let addr = B256::random();
1479        let root = B256::random();
1480
1481        let mut subtree1 = ProofNodes::default();
1482        subtree1.insert(
1483            Nibbles::from_nibbles(vec![0]),
1484            alloy_rlp::encode_fixed_size(&U256::from(42)).to_vec().into(),
1485        );
1486        proof1.storages.insert(
1487            addr,
1488            StorageMultiProof {
1489                root,
1490                subtree: subtree1,
1491                branch_node_masks: BranchNodeMasksMap::default(),
1492            },
1493        );
1494
1495        let mut subtree2 = ProofNodes::default();
1496        subtree2.insert(
1497            Nibbles::from_nibbles(vec![1]),
1498            alloy_rlp::encode_fixed_size(&U256::from(43)).to_vec().into(),
1499        );
1500        proof2.storages.insert(
1501            addr,
1502            StorageMultiProof {
1503                root,
1504                subtree: subtree2,
1505                branch_node_masks: BranchNodeMasksMap::default(),
1506            },
1507        );
1508
1509        proof1.extend(proof2);
1510
1511        let storage = proof1.storages.get(&addr).unwrap();
1512        assert_eq!(storage.root, root);
1513        assert!(storage.subtree.contains_key(&Nibbles::from_nibbles(vec![0])));
1514        assert!(storage.subtree.contains_key(&Nibbles::from_nibbles(vec![1])));
1515    }
1516
1517    #[test]
1518    fn test_multi_proof_retain_difference() {
1519        let mut empty = MultiProofTargets::default();
1520        empty.retain_difference(&Default::default());
1521        assert!(empty.is_empty());
1522
1523        let targets = MultiProofTargets::accounts((0..10).map(B256::with_last_byte));
1524
1525        let mut diffed = targets.clone();
1526        diffed.retain_difference(&MultiProofTargets::account(B256::with_last_byte(11)));
1527        assert_eq!(diffed, targets);
1528
1529        diffed.retain_difference(&MultiProofTargets::accounts((0..5).map(B256::with_last_byte)));
1530        assert_eq!(diffed, MultiProofTargets::accounts((5..10).map(B256::with_last_byte)));
1531
1532        diffed.retain_difference(&targets);
1533        assert!(diffed.is_empty());
1534
1535        let mut targets = MultiProofTargets::default();
1536        let (account1, account2, account3) =
1537            (1..=3).map(B256::with_last_byte).collect_tuple().unwrap();
1538        let account2_slots = (1..5).map(B256::with_last_byte).collect::<B256Set>();
1539        targets.insert(account1, B256Set::from_iter([B256::with_last_byte(1)]));
1540        targets.insert(account2, account2_slots.clone());
1541        targets.insert(account3, B256Set::from_iter([B256::with_last_byte(1)]));
1542
1543        let mut diffed = targets.clone();
1544        diffed.retain_difference(&MultiProofTargets::accounts((1..=3).map(B256::with_last_byte)));
1545        assert_eq!(diffed, targets);
1546
1547        // remove last 3 slots for account 2
1548        let mut account2_slots_expected_len = account2_slots.len();
1549        for slot in account2_slots.iter().skip(1) {
1550            diffed.retain_difference(&MultiProofTargets::account_with_slots(account2, [*slot]));
1551            account2_slots_expected_len -= 1;
1552            assert_eq!(
1553                diffed.get(&account2).map(|slots| slots.len()),
1554                Some(account2_slots_expected_len)
1555            );
1556        }
1557
1558        diffed.retain_difference(&targets);
1559        assert!(diffed.is_empty());
1560    }
1561
1562    #[test]
1563    fn test_multi_proof_retain_difference_no_overlap() {
1564        let mut targets = MultiProofTargets::default();
1565
1566        // populate some targets
1567        let (addr1, addr2) = (B256::random(), B256::random());
1568        let (slot1, slot2) = (B256::random(), B256::random());
1569        targets.insert(addr1, std::iter::once(slot1).collect());
1570        targets.insert(addr2, std::iter::once(slot2).collect());
1571
1572        let mut retained = targets.clone();
1573        retained.retain_difference(&Default::default());
1574        assert_eq!(retained, targets);
1575
1576        // add a different addr and slot to fetched proof targets
1577        let mut other_targets = MultiProofTargets::default();
1578        let addr3 = B256::random();
1579        let slot3 = B256::random();
1580        other_targets.insert(addr3, B256Set::from_iter([slot3]));
1581
1582        // check that the prefetch proof targets are the same because the fetched proof targets
1583        // don't overlap with the prefetch targets
1584        let mut retained = targets.clone();
1585        retained.retain_difference(&other_targets);
1586        assert_eq!(retained, targets);
1587    }
1588
1589    #[test]
1590    fn test_get_prefetch_proof_targets_remove_subset() {
1591        // populate some targets
1592        let mut targets = MultiProofTargets::default();
1593        let (addr1, addr2) = (B256::random(), B256::random());
1594        let (slot1, slot2) = (B256::random(), B256::random());
1595        targets.insert(addr1, B256Set::from_iter([slot1]));
1596        targets.insert(addr2, B256Set::from_iter([slot2]));
1597
1598        // add a subset of the first target to other proof targets
1599        let other_targets = MultiProofTargets::account_with_slots(addr1, [slot1]);
1600
1601        let mut retained = targets.clone();
1602        retained.retain_difference(&other_targets);
1603
1604        // check that the prefetch proof targets do not include the subset
1605        assert_eq!(retained.len(), 1);
1606        assert!(!retained.contains_key(&addr1));
1607        assert!(retained.contains_key(&addr2));
1608
1609        // now add one more slot to the prefetch targets
1610        let slot3 = B256::random();
1611        targets.get_mut(&addr1).unwrap().insert(slot3);
1612
1613        let mut retained = targets.clone();
1614        retained.retain_difference(&other_targets);
1615
1616        // check that the prefetch proof targets do not include the subset
1617        // but include the new slot
1618        assert_eq!(retained.len(), 2);
1619        assert!(retained.contains_key(&addr1));
1620        assert_eq!(retained.get(&addr1), Some(&B256Set::from_iter([slot3])));
1621        assert!(retained.contains_key(&addr2));
1622        assert_eq!(retained.get(&addr2), Some(&B256Set::from_iter([slot2])));
1623    }
1624
1625    #[test]
1626    #[cfg(feature = "eip1186")]
1627    fn eip_1186_roundtrip() {
1628        let mut acc = AccountProof {
1629            address: Address::random(),
1630            info: Some(
1631                // non-empty account
1632                Account { nonce: 100, bytecode_hash: Some(KECCAK_EMPTY), ..Default::default() },
1633            ),
1634            proof: vec![],
1635            storage_root: B256::ZERO,
1636            storage_proofs: vec![],
1637        };
1638
1639        let rpc_proof = acc.clone().into_eip1186_response(Vec::new());
1640        let inverse: AccountProof = rpc_proof.into();
1641        assert_eq!(acc, inverse);
1642
1643        // make account empty
1644        acc.info.as_mut().unwrap().nonce = 0;
1645        let rpc_proof = acc.clone().into_eip1186_response(Vec::new());
1646        let inverse: AccountProof = rpc_proof.into();
1647        acc.info.take();
1648        acc.storage_root = EMPTY_ROOT_HASH;
1649        assert_eq!(acc, inverse);
1650    }
1651
1652    #[test]
1653    #[cfg(feature = "eip1186")]
1654    fn from_eip1186_proof_accepts_geth_zero_hashes() {
1655        // geth (since v1.13.4) returns B256::ZERO for codeHash and storageHash
1656        // in exclusion proofs for non-existent accounts, instead of
1657        // KECCAK_EMPTY / EMPTY_ROOT_HASH. Verify that from_eip1186_proof
1658        // correctly recognizes this format as a non-existent account.
1659        let geth_proof = alloy_rpc_types_eth::EIP1186AccountProofResponse {
1660            address: Address::random(),
1661            balance: U256::ZERO,
1662            code_hash: B256::ZERO,
1663            nonce: 0,
1664            storage_hash: B256::ZERO,
1665            account_proof: vec![],
1666            storage_proof: vec![],
1667        };
1668
1669        let acc: AccountProof = geth_proof.into();
1670        // Should be interpreted as a non-existent account (info = None)
1671        assert!(acc.info.is_none());
1672        assert_eq!(acc.storage_root, EMPTY_ROOT_HASH);
1673    }
1674
1675    #[test]
1676    #[cfg(feature = "eip1186")]
1677    fn from_eip1186_proof_recovers_account_extension() {
1678        if !Account::EXTENSIONS_ENABLED {
1679            return;
1680        }
1681
1682        // An account that only has an extension, encoded as the fifth field of the trie account.
1683        let extension = [0x82, 0xaa];
1684        let payload_length = 0u64.length() +
1685            U256::ZERO.length() +
1686            EMPTY_ROOT_HASH.length() +
1687            KECCAK_EMPTY.length() +
1688            extension[..].length();
1689        let mut value = Vec::new();
1690        alloy_rlp::Header { list: true, payload_length }.encode(&mut value);
1691        0u64.encode(&mut value);
1692        U256::ZERO.encode(&mut value);
1693        EMPTY_ROOT_HASH.encode(&mut value);
1694        KECCAK_EMPTY.encode(&mut value);
1695        extension[..].encode(&mut value);
1696
1697        // A trie holding only this account, so its leaf is the root.
1698        let address = Address::with_last_byte(1);
1699        let leaf = alloy_rlp::encode(LeafNode::new(Nibbles::unpack(keccak256(address)), value));
1700        let root = keccak256(&leaf);
1701        let response = alloy_rpc_types_eth::EIP1186AccountProofResponse {
1702            address,
1703            balance: U256::ZERO,
1704            code_hash: KECCAK_EMPTY,
1705            nonce: 0,
1706            storage_hash: EMPTY_ROOT_HASH,
1707            account_proof: vec![leaf.into()],
1708            storage_proof: vec![],
1709        };
1710
1711        let proof = AccountProof::from_eip1186_proof(response.clone());
1712        assert!(proof.info.as_ref().is_some_and(Account::has_extension));
1713        assert_eq!(proof.verify(root), Ok(()));
1714
1715        // An exclusion proof for another address ends in the same leaf, which is not its account.
1716        let other =
1717            AccountProof::from_eip1186_proof(alloy_rpc_types_eth::EIP1186AccountProofResponse {
1718                address: Address::with_last_byte(2),
1719                ..response
1720            });
1721        assert_eq!(other.info, None);
1722        assert_eq!(other.verify(root), Ok(()));
1723    }
1724
1725    #[test]
1726    #[cfg(feature = "eip1186")]
1727    fn from_eip1186_proof_accepts_empty_hashes() {
1728        let proof = alloy_rpc_types_eth::EIP1186AccountProofResponse {
1729            address: Address::random(),
1730            balance: U256::ZERO,
1731            code_hash: KECCAK_EMPTY,
1732            nonce: 0,
1733            storage_hash: EMPTY_ROOT_HASH,
1734            account_proof: vec![],
1735            storage_proof: vec![],
1736        };
1737
1738        let acc: AccountProof = proof.into();
1739        assert!(acc.info.is_none());
1740        assert_eq!(acc.storage_root, EMPTY_ROOT_HASH);
1741    }
1742
1743    #[test]
1744    #[cfg(feature = "eip1186")]
1745    #[allow(clippy::needless_update)]
1746    fn into_eip1186_response_zero_empty_account() {
1747        // Non-existent account (info = None)
1748        let acc = AccountProof {
1749            address: Address::random(),
1750            info: None,
1751            proof: vec![],
1752            storage_root: EMPTY_ROOT_HASH,
1753            storage_proofs: vec![],
1754        };
1755
1756        // Default behavior: KECCAK_EMPTY / EMPTY_ROOT_HASH
1757        let rpc_default = acc.clone().into_eip1186_response(Vec::new());
1758        assert_eq!(rpc_default.code_hash, KECCAK_EMPTY);
1759        assert_eq!(rpc_default.storage_hash, EMPTY_ROOT_HASH);
1760
1761        // zero_empty_account = false: same as default
1762        let rpc_compat_off = acc.clone().into_eip1186_response_with(Vec::new(), false);
1763        assert_eq!(rpc_compat_off.code_hash, KECCAK_EMPTY);
1764        assert_eq!(rpc_compat_off.storage_hash, EMPTY_ROOT_HASH);
1765
1766        // zero_empty_account = true: B256::ZERO (geth-compat)
1767        let rpc_compat_on = acc.into_eip1186_response_with(Vec::new(), true);
1768        assert_eq!(rpc_compat_on.code_hash, B256::ZERO);
1769        assert_eq!(rpc_compat_on.storage_hash, B256::ZERO);
1770
1771        // Existing account should NOT be affected by zero_empty_account
1772        let existing_acc = AccountProof {
1773            address: Address::random(),
1774            info: Some(Account {
1775                nonce: 42,
1776                balance: U256::from(100),
1777                bytecode_hash: Some(KECCAK_EMPTY),
1778                ..Default::default()
1779            }),
1780            proof: vec![],
1781            storage_root: B256::random(),
1782            storage_proofs: vec![],
1783        };
1784        let rpc_existing = existing_acc.clone().into_eip1186_response_with(Vec::new(), true);
1785        assert_eq!(rpc_existing.code_hash, KECCAK_EMPTY);
1786        assert_eq!(rpc_existing.storage_hash, existing_acc.storage_root);
1787    }
1788
1789    #[test]
1790    fn test_multiproof_targets_chunking_length() {
1791        let mut targets = MultiProofTargets::default();
1792        targets.insert(B256::with_last_byte(1), B256Set::default());
1793        targets.insert(
1794            B256::with_last_byte(2),
1795            B256Set::from_iter([B256::with_last_byte(10), B256::with_last_byte(20)]),
1796        );
1797        targets.insert(
1798            B256::with_last_byte(3),
1799            B256Set::from_iter([
1800                B256::with_last_byte(30),
1801                B256::with_last_byte(31),
1802                B256::with_last_byte(32),
1803            ]),
1804        );
1805
1806        let chunking_length = targets.chunking_length();
1807        for size in 1..=targets.clone().chunks(1).count() {
1808            let chunk_count = targets.clone().chunks(size).count();
1809            let expected_count = chunking_length.div_ceil(size);
1810            assert_eq!(
1811                chunk_count, expected_count,
1812                "chunking_length: {}, size: {}",
1813                chunking_length, size
1814            );
1815        }
1816    }
1817
1818    #[test]
1819    fn test_nonempty_storage_trie_returns_nonempty_proof() {
1820        let slot = B256::with_last_byte(1);
1821        let nibbles = Nibbles::unpack(keccak256(slot));
1822        let value = U256::from(999);
1823        let leaf = alloy_trie::nodes::LeafNode::new(nibbles, encode_fixed_size(&value).to_vec());
1824        let mut encoded = vec![];
1825        alloy_rlp::Encodable::encode(&leaf, &mut encoded);
1826
1827        let mut subtree = ProofNodes::default();
1828        subtree.insert(nibbles, encoded.into());
1829
1830        let multiproof = StorageMultiProof {
1831            root: B256::with_last_byte(0xFF),
1832            subtree,
1833            branch_node_masks: BranchNodeMasksMap::default(),
1834        };
1835
1836        let proof = multiproof.storage_proof(slot).unwrap();
1837        assert!(!proof.proof.is_empty(), "non-empty trie must return non-empty proof");
1838        assert_eq!(proof.value, value);
1839    }
1840
1841    #[cfg(feature = "eip1186")]
1842    #[test]
1843    fn eip1186_response_normalizes_empty_trie_proof() {
1844        let slot = B256::with_last_byte(1);
1845        let sentinel = || vec![Bytes::from([EMPTY_STRING_CODE])];
1846
1847        // Empty account trie + empty storage trie: both proofs are the lone `0x80` sentinel.
1848        let account = AccountProof {
1849            address: Address::ZERO,
1850            info: None,
1851            proof: sentinel(),
1852            storage_root: EMPTY_ROOT_HASH,
1853            storage_proofs: vec![StorageProof::new(slot).with_proof(sentinel())],
1854        };
1855
1856        let resp = account.into_eip1186_response(vec![alloy_serde::JsonStorageKey::from(slot)]);
1857
1858        assert!(
1859            resp.account_proof.is_empty(),
1860            "empty account trie must yield empty account_proof, got {:?}",
1861            resp.account_proof
1862        );
1863        assert_eq!(resp.storage_proof.len(), 1);
1864        assert!(
1865            resp.storage_proof[0].proof.is_empty(),
1866            "empty storage trie must yield empty storage proof, got {:?}",
1867            resp.storage_proof[0].proof
1868        );
1869    }
1870
1871    #[cfg(feature = "eip1186")]
1872    #[test]
1873    fn eip1186_response_keeps_nonempty_proof() {
1874        // A real (non-empty) proof must pass through unchanged: multiple nodes, and a single
1875        // node that is not the `0x80` sentinel.
1876        let multi = vec![Bytes::from([0x01, 0x02]), Bytes::from([0x03])];
1877        let single_non_sentinel = vec![Bytes::from([0xf8, 0x44])];
1878
1879        let account = AccountProof {
1880            address: Address::ZERO,
1881            info: None,
1882            proof: multi.clone(),
1883            storage_root: EMPTY_ROOT_HASH,
1884            storage_proofs: vec![
1885                StorageProof::new(B256::with_last_byte(1)).with_proof(single_non_sentinel.clone())
1886            ],
1887        };
1888
1889        let resp = account.into_eip1186_response(vec![alloy_serde::JsonStorageKey::from(
1890            B256::with_last_byte(1),
1891        )]);
1892
1893        assert_eq!(resp.account_proof, multi);
1894        assert_eq!(resp.storage_proof[0].proof, single_non_sentinel);
1895    }
1896
1897    #[cfg(feature = "eip1186")]
1898    #[test]
1899    fn eip1186_response_keeps_key_of_each_requested_slot() {
1900        let slot = B256::with_last_byte(1);
1901        let keys = vec![
1902            alloy_serde::JsonStorageKey::Number(U256::from(1)),
1903            alloy_serde::JsonStorageKey::Hash(slot),
1904        ];
1905        let mut account = AccountProof::new(Address::ZERO);
1906        account.storage_proofs = vec![StorageProof::new(slot); 2];
1907
1908        let resp = account.into_eip1186_response(keys.clone());
1909
1910        assert_eq!(resp.storage_proof.iter().map(|p| p.key).collect::<Vec<_>>(), keys);
1911    }
1912}