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