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