mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-08 03:01:12 -04:00
1110 lines
39 KiB
Rust
1110 lines
39 KiB
Rust
//! Merkle trie proofs.
|
|
|
|
use crate::{BranchNodeMasksMap, Nibbles, ProofTrieNode, TrieAccount};
|
|
use alloc::{borrow::Cow, vec::Vec};
|
|
use alloy_consensus::constants::KECCAK_EMPTY;
|
|
use alloy_primitives::{
|
|
keccak256,
|
|
map::{hash_map, B256Map, B256Set},
|
|
Address, Bytes, B256, U256,
|
|
};
|
|
use alloy_rlp::{encode_fixed_size, Decodable, EMPTY_STRING_CODE};
|
|
use alloy_trie::{
|
|
nodes::TrieNode,
|
|
proof::{verify_proof, DecodedProofNodes, ProofNodes, ProofVerificationError},
|
|
EMPTY_ROOT_HASH,
|
|
};
|
|
use derive_more::{Deref, DerefMut, IntoIterator};
|
|
use itertools::Itertools;
|
|
use reth_primitives_traits::Account;
|
|
|
|
/// Proof targets map.
|
|
#[derive(Deref, DerefMut, IntoIterator, Clone, PartialEq, Eq, Default, Debug)]
|
|
pub struct MultiProofTargets(B256Map<B256Set>);
|
|
|
|
impl FromIterator<(B256, B256Set)> for MultiProofTargets {
|
|
fn from_iter<T: IntoIterator<Item = (B256, B256Set)>>(iter: T) -> Self {
|
|
Self(B256Map::from_iter(iter))
|
|
}
|
|
}
|
|
|
|
impl MultiProofTargets {
|
|
/// Creates an empty `MultiProofTargets` with at least the specified capacity.
|
|
pub fn with_capacity(capacity: usize) -> Self {
|
|
Self(B256Map::with_capacity_and_hasher(capacity, Default::default()))
|
|
}
|
|
|
|
/// Create `MultiProofTargets` with a single account as a target.
|
|
pub fn account(hashed_address: B256) -> Self {
|
|
Self::accounts([hashed_address])
|
|
}
|
|
|
|
/// Create `MultiProofTargets` with a single account and slots as targets.
|
|
pub fn account_with_slots<I: IntoIterator<Item = B256>>(
|
|
hashed_address: B256,
|
|
slots_iter: I,
|
|
) -> Self {
|
|
Self(B256Map::from_iter([(hashed_address, slots_iter.into_iter().collect())]))
|
|
}
|
|
|
|
/// Create `MultiProofTargets` only from accounts.
|
|
pub fn accounts<I: IntoIterator<Item = B256>>(iter: I) -> Self {
|
|
Self(iter.into_iter().map(|hashed_address| (hashed_address, Default::default())).collect())
|
|
}
|
|
|
|
/// Retains the targets representing the difference,
|
|
/// i.e., the values that are in `self` but not in `other`.
|
|
pub fn retain_difference(&mut self, other: &Self) {
|
|
self.0.retain(|hashed_address, hashed_slots| {
|
|
if let Some(other_hashed_slots) = other.get(hashed_address) {
|
|
hashed_slots.retain(|hashed_slot| !other_hashed_slots.contains(hashed_slot));
|
|
!hashed_slots.is_empty()
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Extend multi proof targets with contents of other.
|
|
pub fn extend(&mut self, other: Self) {
|
|
self.extend_inner(Cow::Owned(other));
|
|
}
|
|
|
|
/// Extend multi proof targets with contents of other.
|
|
///
|
|
/// Slightly less efficient than [`Self::extend`], but preferred to `extend(other.clone())`.
|
|
pub fn extend_ref(&mut self, other: &Self) {
|
|
self.extend_inner(Cow::Borrowed(other));
|
|
}
|
|
|
|
fn extend_inner(&mut self, other: Cow<'_, Self>) {
|
|
for (hashed_address, hashed_slots) in other.iter() {
|
|
self.entry(*hashed_address).or_default().extend(hashed_slots);
|
|
}
|
|
}
|
|
|
|
/// Returns an iterator that yields chunks of the specified size.
|
|
///
|
|
/// See [`ChunkedMultiProofTargets`] for more information.
|
|
pub fn chunks(self, size: usize) -> ChunkedMultiProofTargets {
|
|
ChunkedMultiProofTargets::new(self, size)
|
|
}
|
|
|
|
/// Returns the number of items that will be considered during chunking in `[Self::chunks]`.
|
|
pub fn chunking_length(&self) -> usize {
|
|
self.values().map(|slots| 1 + slots.len().saturating_sub(1)).sum::<usize>()
|
|
}
|
|
}
|
|
|
|
/// An iterator that yields chunks of the proof targets of at most `size` account and storage
|
|
/// targets.
|
|
///
|
|
/// For example, for the following proof targets:
|
|
/// ```text
|
|
/// - 0x1: [0x10, 0x20, 0x30]
|
|
/// - 0x2: [0x40]
|
|
/// - 0x3: []
|
|
/// ```
|
|
///
|
|
/// and `size = 2`, the iterator will yield the following chunks:
|
|
/// ```text
|
|
/// - { 0x1: [0x10, 0x20] }
|
|
/// - { 0x1: [0x30], 0x2: [0x40] }
|
|
/// - { 0x3: [] }
|
|
/// ```
|
|
///
|
|
/// It follows two rules:
|
|
/// - If account has associated storage slots, each storage slot is counted towards the chunk size.
|
|
/// - If account has no associated storage slots, the account is counted towards the chunk size.
|
|
#[derive(Debug)]
|
|
pub struct ChunkedMultiProofTargets {
|
|
flattened_targets: alloc::vec::IntoIter<(B256, Option<B256>)>,
|
|
size: usize,
|
|
}
|
|
|
|
impl ChunkedMultiProofTargets {
|
|
fn new(targets: MultiProofTargets, size: usize) -> Self {
|
|
let flattened_targets = targets
|
|
.into_iter()
|
|
.flat_map(|(address, slots)| {
|
|
if slots.is_empty() {
|
|
// If the account has no storage slots, we still need to yield the account
|
|
// address with empty storage slots. `None` here means that
|
|
// there's no storage slot to fetch.
|
|
itertools::Either::Left(core::iter::once((address, None)))
|
|
} else {
|
|
itertools::Either::Right(
|
|
slots.into_iter().map(move |slot| (address, Some(slot))),
|
|
)
|
|
}
|
|
})
|
|
.sorted_unstable();
|
|
Self { flattened_targets, size }
|
|
}
|
|
}
|
|
|
|
impl Iterator for ChunkedMultiProofTargets {
|
|
type Item = MultiProofTargets;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let chunk = self.flattened_targets.by_ref().take(self.size).fold(
|
|
MultiProofTargets::default(),
|
|
|mut acc, (address, slot)| {
|
|
let entry = acc.entry(address).or_default();
|
|
if let Some(slot) = slot {
|
|
entry.insert(slot);
|
|
}
|
|
acc
|
|
},
|
|
);
|
|
|
|
if chunk.is_empty() {
|
|
None
|
|
} else {
|
|
Some(chunk)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The state multiproof of target accounts and multiproofs of their storage tries.
|
|
/// Multiproof is effectively a state subtrie that only contains the nodes
|
|
/// in the paths of target accounts.
|
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
|
pub struct MultiProof {
|
|
/// State trie multiproof for requested accounts.
|
|
pub account_subtree: ProofNodes,
|
|
/// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the account
|
|
/// proof.
|
|
pub branch_node_masks: BranchNodeMasksMap,
|
|
/// Storage trie multiproofs.
|
|
pub storages: B256Map<StorageMultiProof>,
|
|
}
|
|
|
|
impl MultiProof {
|
|
/// Returns true if the multiproof is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.account_subtree.is_empty() &&
|
|
self.branch_node_masks.is_empty() &&
|
|
self.storages.is_empty()
|
|
}
|
|
|
|
/// Return the account proof nodes for the given account path.
|
|
pub fn account_proof_nodes(&self, path: &Nibbles) -> Vec<(Nibbles, Bytes)> {
|
|
self.account_subtree.matching_nodes_sorted(path)
|
|
}
|
|
|
|
/// Return the storage proof nodes for the given storage slots of the account path.
|
|
pub fn storage_proof_nodes(
|
|
&self,
|
|
hashed_address: B256,
|
|
slots: impl IntoIterator<Item = B256>,
|
|
) -> Vec<(B256, Vec<(Nibbles, Bytes)>)> {
|
|
self.storages
|
|
.get(&hashed_address)
|
|
.map(|storage_mp| {
|
|
slots
|
|
.into_iter()
|
|
.map(|slot| {
|
|
let nibbles = Nibbles::unpack(slot);
|
|
(slot, storage_mp.subtree.matching_nodes_sorted(&nibbles))
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Construct the account proof from the multiproof.
|
|
pub fn account_proof(
|
|
&self,
|
|
address: Address,
|
|
slots: &[B256],
|
|
) -> Result<AccountProof, alloy_rlp::Error> {
|
|
let hashed_address = keccak256(address);
|
|
let nibbles = Nibbles::unpack(hashed_address);
|
|
|
|
// Retrieve the account proof.
|
|
let proof = self
|
|
.account_proof_nodes(&nibbles)
|
|
.into_iter()
|
|
.map(|(_, node)| node)
|
|
.collect::<Vec<_>>();
|
|
|
|
// Inspect the last node in the proof. If it's a leaf node with matching suffix,
|
|
// then the node contains the encoded trie account.
|
|
let info = 'info: {
|
|
if let Some(last) = proof.last() &&
|
|
let TrieNode::Leaf(leaf) = TrieNode::decode(&mut &last[..])? &&
|
|
nibbles.ends_with(&leaf.key)
|
|
{
|
|
let account = TrieAccount::decode(&mut &leaf.value[..])?;
|
|
break 'info Some(Account {
|
|
balance: account.balance,
|
|
nonce: account.nonce,
|
|
bytecode_hash: (account.code_hash != KECCAK_EMPTY).then_some(account.code_hash),
|
|
})
|
|
}
|
|
None
|
|
};
|
|
|
|
// Retrieve proofs for requested storage slots.
|
|
let storage_multiproof = self.storages.get(&hashed_address);
|
|
let storage_root = storage_multiproof.map(|m| m.root).unwrap_or(EMPTY_ROOT_HASH);
|
|
let mut storage_proofs = Vec::with_capacity(slots.len());
|
|
for slot in slots {
|
|
let proof = if let Some(multiproof) = &storage_multiproof {
|
|
multiproof.storage_proof(*slot)?
|
|
} else {
|
|
StorageProof::new(*slot)
|
|
};
|
|
storage_proofs.push(proof);
|
|
}
|
|
Ok(AccountProof { address, info, proof, storage_root, storage_proofs })
|
|
}
|
|
|
|
/// Extends this multiproof with another one, merging both account and storage
|
|
/// proofs.
|
|
pub fn extend(&mut self, other: Self) {
|
|
self.account_subtree.extend_from(other.account_subtree);
|
|
self.branch_node_masks.extend(other.branch_node_masks);
|
|
|
|
for (hashed_address, storage) in other.storages {
|
|
match self.storages.entry(hashed_address) {
|
|
hash_map::Entry::Occupied(mut entry) => {
|
|
debug_assert_eq!(entry.get().root, storage.root);
|
|
let entry = entry.get_mut();
|
|
entry.subtree.extend_from(storage.subtree);
|
|
entry.branch_node_masks.extend(storage.branch_node_masks);
|
|
}
|
|
hash_map::Entry::Vacant(entry) => {
|
|
entry.insert(storage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a [`MultiProof`] from a [`StorageMultiProof`].
|
|
pub fn from_storage_proof(hashed_address: B256, storage_proof: StorageMultiProof) -> Self {
|
|
Self {
|
|
storages: B256Map::from_iter([(hashed_address, storage_proof)]),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is a type of [`MultiProof`] that uses decoded proofs, meaning these proofs are stored as a
|
|
/// collection of [`TrieNode`]s instead of RLP-encoded bytes.
|
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
|
pub struct DecodedMultiProof {
|
|
/// State trie multiproof for requested accounts.
|
|
pub account_subtree: DecodedProofNodes,
|
|
/// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the account
|
|
/// proof.
|
|
pub branch_node_masks: BranchNodeMasksMap,
|
|
/// Storage trie multiproofs.
|
|
pub storages: B256Map<DecodedStorageMultiProof>,
|
|
}
|
|
|
|
impl DecodedMultiProof {
|
|
/// Returns true if the multiproof is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.account_subtree.is_empty() &&
|
|
self.branch_node_masks.is_empty() &&
|
|
self.storages.is_empty()
|
|
}
|
|
|
|
/// Return the account proof nodes for the given account path.
|
|
pub fn account_proof_nodes(&self, path: &Nibbles) -> Vec<(Nibbles, TrieNode)> {
|
|
self.account_subtree.matching_nodes_sorted(path)
|
|
}
|
|
|
|
/// Return the storage proof nodes for the given storage slots of the account path.
|
|
pub fn storage_proof_nodes(
|
|
&self,
|
|
hashed_address: B256,
|
|
slots: impl IntoIterator<Item = B256>,
|
|
) -> Vec<(B256, Vec<(Nibbles, TrieNode)>)> {
|
|
self.storages
|
|
.get(&hashed_address)
|
|
.map(|storage_mp| {
|
|
slots
|
|
.into_iter()
|
|
.map(|slot| {
|
|
let nibbles = Nibbles::unpack(slot);
|
|
(slot, storage_mp.subtree.matching_nodes_sorted(&nibbles))
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Construct the account proof from the multiproof.
|
|
pub fn account_proof(
|
|
&self,
|
|
address: Address,
|
|
slots: &[B256],
|
|
) -> Result<DecodedAccountProof, alloy_rlp::Error> {
|
|
let hashed_address = keccak256(address);
|
|
let nibbles = Nibbles::unpack(hashed_address);
|
|
|
|
// Retrieve the account proof.
|
|
let proof = self
|
|
.account_proof_nodes(&nibbles)
|
|
.into_iter()
|
|
.map(|(_, node)| node)
|
|
.collect::<Vec<_>>();
|
|
|
|
// Inspect the last node in the proof. If it's a leaf node with matching suffix,
|
|
// then the node contains the encoded trie account.
|
|
let info = 'info: {
|
|
if let Some(TrieNode::Leaf(leaf)) = proof.last() &&
|
|
nibbles.ends_with(&leaf.key)
|
|
{
|
|
let account = TrieAccount::decode(&mut &leaf.value[..])?;
|
|
break 'info Some(Account {
|
|
balance: account.balance,
|
|
nonce: account.nonce,
|
|
bytecode_hash: (account.code_hash != KECCAK_EMPTY).then_some(account.code_hash),
|
|
})
|
|
}
|
|
None
|
|
};
|
|
|
|
// Retrieve proofs for requested storage slots.
|
|
let storage_multiproof = self.storages.get(&hashed_address);
|
|
let storage_root = storage_multiproof.map(|m| m.root).unwrap_or(EMPTY_ROOT_HASH);
|
|
let mut storage_proofs = Vec::with_capacity(slots.len());
|
|
for slot in slots {
|
|
let proof = if let Some(multiproof) = &storage_multiproof {
|
|
multiproof.storage_proof(*slot)?
|
|
} else {
|
|
DecodedStorageProof::new(*slot)
|
|
};
|
|
storage_proofs.push(proof);
|
|
}
|
|
Ok(DecodedAccountProof { address, info, proof, storage_root, storage_proofs })
|
|
}
|
|
|
|
/// Extends this multiproof with another one, merging both account and storage
|
|
/// proofs.
|
|
pub fn extend(&mut self, other: Self) {
|
|
self.account_subtree.extend_from(other.account_subtree);
|
|
self.branch_node_masks.extend(other.branch_node_masks);
|
|
|
|
for (hashed_address, storage) in other.storages {
|
|
match self.storages.entry(hashed_address) {
|
|
hash_map::Entry::Occupied(mut entry) => {
|
|
debug_assert_eq!(entry.get().root, storage.root);
|
|
let entry = entry.get_mut();
|
|
entry.subtree.extend_from(storage.subtree);
|
|
entry.branch_node_masks.extend(storage.branch_node_masks);
|
|
}
|
|
hash_map::Entry::Vacant(entry) => {
|
|
entry.insert(storage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a [`DecodedMultiProof`] from a [`DecodedStorageMultiProof`].
|
|
pub fn from_storage_proof(
|
|
hashed_address: B256,
|
|
storage_proof: DecodedStorageMultiProof,
|
|
) -> Self {
|
|
Self {
|
|
storages: B256Map::from_iter([(hashed_address, storage_proof)]),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<MultiProof> for DecodedMultiProof {
|
|
type Error = alloy_rlp::Error;
|
|
|
|
fn try_from(multi_proof: MultiProof) -> Result<Self, Self::Error> {
|
|
let account_subtree = DecodedProofNodes::try_from(multi_proof.account_subtree)?;
|
|
let storages = multi_proof
|
|
.storages
|
|
.into_iter()
|
|
.map(|(address, storage)| Ok((address, storage.try_into()?)))
|
|
.collect::<Result<B256Map<_>, alloy_rlp::Error>>()?;
|
|
Ok(Self { account_subtree, branch_node_masks: multi_proof.branch_node_masks, storages })
|
|
}
|
|
}
|
|
|
|
/// V2 decoded multiproof which contains the results of both account and storage V2 proof
|
|
/// calculations.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
|
pub struct DecodedMultiProofV2 {
|
|
/// Account trie proof nodes
|
|
pub account_proofs: Vec<ProofTrieNode>,
|
|
/// Storage trie proof nodes indexed by account
|
|
pub storage_proofs: B256Map<Vec<ProofTrieNode>>,
|
|
}
|
|
|
|
impl DecodedMultiProofV2 {
|
|
/// Returns true if there are no proofs
|
|
pub fn is_empty(&self) -> bool {
|
|
self.account_proofs.is_empty() && self.storage_proofs.is_empty()
|
|
}
|
|
|
|
/// Appends the given multiproof's data to this one.
|
|
///
|
|
/// This implementation does not deduplicate redundant proofs.
|
|
pub fn extend(&mut self, other: Self) {
|
|
self.account_proofs.extend(other.account_proofs);
|
|
for (hashed_address, other_storage_proofs) in other.storage_proofs {
|
|
self.storage_proofs.entry(hashed_address).or_default().extend(other_storage_proofs);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The merkle multiproof of storage trie.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct StorageMultiProof {
|
|
/// Storage trie root.
|
|
pub root: B256,
|
|
/// Storage multiproof for requested slots.
|
|
pub subtree: ProofNodes,
|
|
/// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the storage
|
|
/// proof.
|
|
pub branch_node_masks: BranchNodeMasksMap,
|
|
}
|
|
|
|
impl StorageMultiProof {
|
|
/// Create new storage multiproof for empty trie.
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
root: EMPTY_ROOT_HASH,
|
|
subtree: ProofNodes::from_iter([(
|
|
Nibbles::default(),
|
|
Bytes::from([EMPTY_STRING_CODE]),
|
|
)]),
|
|
branch_node_masks: BranchNodeMasksMap::default(),
|
|
}
|
|
}
|
|
|
|
/// Return storage proofs for the target storage slot (unhashed).
|
|
pub fn storage_proof(&self, slot: B256) -> Result<StorageProof, alloy_rlp::Error> {
|
|
let nibbles = Nibbles::unpack(keccak256(slot));
|
|
|
|
// Retrieve the storage proof.
|
|
let proof = self
|
|
.subtree
|
|
.matching_nodes_iter(&nibbles)
|
|
.sorted_by(|a, b| a.0.cmp(b.0))
|
|
.map(|(_, node)| node.clone())
|
|
.collect::<Vec<_>>();
|
|
|
|
// Inspect the last node in the proof. If it's a leaf node with matching suffix,
|
|
// then the node contains the encoded slot value.
|
|
let value = 'value: {
|
|
if let Some(last) = proof.last() &&
|
|
let TrieNode::Leaf(leaf) = TrieNode::decode(&mut &last[..])? &&
|
|
nibbles.ends_with(&leaf.key)
|
|
{
|
|
break 'value U256::decode(&mut &leaf.value[..])?
|
|
}
|
|
U256::ZERO
|
|
};
|
|
|
|
Ok(StorageProof { key: slot, nibbles, value, proof })
|
|
}
|
|
}
|
|
|
|
/// The decoded merkle multiproof for a storage trie.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct DecodedStorageMultiProof {
|
|
/// Storage trie root.
|
|
pub root: B256,
|
|
/// Storage multiproof for requested slots.
|
|
pub subtree: DecodedProofNodes,
|
|
/// Consolidated branch node masks (`hash_mask`, `tree_mask`) for each path in the storage
|
|
/// proof.
|
|
pub branch_node_masks: BranchNodeMasksMap,
|
|
}
|
|
|
|
impl DecodedStorageMultiProof {
|
|
/// Create new storage multiproof for empty trie.
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
root: EMPTY_ROOT_HASH,
|
|
subtree: DecodedProofNodes::from_iter([(Nibbles::default(), TrieNode::EmptyRoot)]),
|
|
branch_node_masks: BranchNodeMasksMap::default(),
|
|
}
|
|
}
|
|
|
|
/// Return storage proofs for the target storage slot (unhashed).
|
|
pub fn storage_proof(&self, slot: B256) -> Result<DecodedStorageProof, alloy_rlp::Error> {
|
|
let nibbles = Nibbles::unpack(keccak256(slot));
|
|
|
|
// Retrieve the storage proof.
|
|
let proof = self
|
|
.subtree
|
|
.matching_nodes_iter(&nibbles)
|
|
.sorted_by(|a, b| a.0.cmp(b.0))
|
|
.map(|(_, node)| node.clone())
|
|
.collect::<Vec<_>>();
|
|
|
|
// Inspect the last node in the proof. If it's a leaf node with matching suffix,
|
|
// then the node contains the encoded slot value.
|
|
let value = 'value: {
|
|
if let Some(TrieNode::Leaf(leaf)) = proof.last() &&
|
|
nibbles.ends_with(&leaf.key)
|
|
{
|
|
break 'value U256::decode(&mut &leaf.value[..])?
|
|
}
|
|
U256::ZERO
|
|
};
|
|
|
|
Ok(DecodedStorageProof { key: slot, nibbles, value, proof })
|
|
}
|
|
}
|
|
|
|
impl TryFrom<StorageMultiProof> for DecodedStorageMultiProof {
|
|
type Error = alloy_rlp::Error;
|
|
|
|
fn try_from(multi_proof: StorageMultiProof) -> Result<Self, Self::Error> {
|
|
let subtree = DecodedProofNodes::try_from(multi_proof.subtree)?;
|
|
Ok(Self {
|
|
root: multi_proof.root,
|
|
subtree,
|
|
branch_node_masks: multi_proof.branch_node_masks,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The merkle proof with the relevant account info.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
|
|
#[cfg_attr(any(test, feature = "serde"), serde(rename_all = "camelCase"))]
|
|
pub struct AccountProof {
|
|
/// The address associated with the account.
|
|
pub address: Address,
|
|
/// Account info, if any.
|
|
pub info: Option<Account>,
|
|
/// Array of rlp-serialized merkle trie nodes which starting from the root node and
|
|
/// following the path of the hashed address as key.
|
|
pub proof: Vec<Bytes>,
|
|
/// The storage trie root.
|
|
pub storage_root: B256,
|
|
/// Array of storage proofs as requested.
|
|
pub storage_proofs: Vec<StorageProof>,
|
|
}
|
|
|
|
#[cfg(feature = "eip1186")]
|
|
impl AccountProof {
|
|
/// Convert into an EIP-1186 account proof response
|
|
pub fn into_eip1186_response(
|
|
self,
|
|
slots: Vec<alloy_serde::JsonStorageKey>,
|
|
) -> alloy_rpc_types_eth::EIP1186AccountProofResponse {
|
|
let info = self.info.unwrap_or_default();
|
|
alloy_rpc_types_eth::EIP1186AccountProofResponse {
|
|
address: self.address,
|
|
balance: info.balance,
|
|
code_hash: info.get_bytecode_hash(),
|
|
nonce: info.nonce,
|
|
storage_hash: self.storage_root,
|
|
account_proof: self.proof,
|
|
storage_proof: self
|
|
.storage_proofs
|
|
.into_iter()
|
|
.filter_map(|proof| {
|
|
let input_slot = slots.iter().find(|s| s.as_b256() == proof.key)?;
|
|
Some(proof.into_eip1186_proof(*input_slot))
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
/// Converts an
|
|
/// [`EIP1186AccountProofResponse`](alloy_rpc_types_eth::EIP1186AccountProofResponse) to an
|
|
/// [`AccountProof`].
|
|
///
|
|
/// This is the inverse of [`Self::into_eip1186_response`]
|
|
pub fn from_eip1186_proof(proof: alloy_rpc_types_eth::EIP1186AccountProofResponse) -> Self {
|
|
let alloy_rpc_types_eth::EIP1186AccountProofResponse {
|
|
nonce,
|
|
address,
|
|
balance,
|
|
code_hash,
|
|
storage_hash,
|
|
account_proof,
|
|
storage_proof,
|
|
..
|
|
} = proof;
|
|
let storage_proofs = storage_proof.into_iter().map(Into::into).collect();
|
|
|
|
let (storage_root, info) = if nonce == 0 &&
|
|
balance.is_zero() &&
|
|
storage_hash.is_zero() &&
|
|
code_hash == KECCAK_EMPTY
|
|
{
|
|
// Account does not exist in state. Return `None` here to prevent proof
|
|
// verification.
|
|
(EMPTY_ROOT_HASH, None)
|
|
} else {
|
|
(storage_hash, Some(Account { nonce, balance, bytecode_hash: code_hash.into() }))
|
|
};
|
|
|
|
Self { address, info, proof: account_proof, storage_root, storage_proofs }
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "eip1186")]
|
|
impl From<alloy_rpc_types_eth::EIP1186AccountProofResponse> for AccountProof {
|
|
fn from(proof: alloy_rpc_types_eth::EIP1186AccountProofResponse) -> Self {
|
|
Self::from_eip1186_proof(proof)
|
|
}
|
|
}
|
|
|
|
impl Default for AccountProof {
|
|
fn default() -> Self {
|
|
Self::new(Address::default())
|
|
}
|
|
}
|
|
|
|
impl AccountProof {
|
|
/// Create new account proof entity.
|
|
pub const fn new(address: Address) -> Self {
|
|
Self {
|
|
address,
|
|
info: None,
|
|
proof: Vec::new(),
|
|
storage_root: EMPTY_ROOT_HASH,
|
|
storage_proofs: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Verify the storage proofs and account proof against the provided state root.
|
|
pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> {
|
|
// Verify storage proofs.
|
|
for storage_proof in &self.storage_proofs {
|
|
storage_proof.verify(self.storage_root)?;
|
|
}
|
|
|
|
// Verify the account proof.
|
|
let expected = if self.info.is_none() && self.storage_root == EMPTY_ROOT_HASH {
|
|
None
|
|
} else {
|
|
Some(alloy_rlp::encode(
|
|
self.info.unwrap_or_default().into_trie_account(self.storage_root),
|
|
))
|
|
};
|
|
let nibbles = Nibbles::unpack(keccak256(self.address));
|
|
verify_proof(root, nibbles, expected, &self.proof)
|
|
}
|
|
}
|
|
|
|
/// The merkle proof with the relevant account info.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub struct DecodedAccountProof {
|
|
/// The address associated with the account.
|
|
pub address: Address,
|
|
/// Account info.
|
|
pub info: Option<Account>,
|
|
/// Array of merkle trie nodes which starting from the root node and following the path of the
|
|
/// hashed address as key.
|
|
pub proof: Vec<TrieNode>,
|
|
/// The storage trie root.
|
|
pub storage_root: B256,
|
|
/// Array of storage proofs as requested.
|
|
pub storage_proofs: Vec<DecodedStorageProof>,
|
|
}
|
|
|
|
impl Default for DecodedAccountProof {
|
|
fn default() -> Self {
|
|
Self::new(Address::default())
|
|
}
|
|
}
|
|
|
|
impl DecodedAccountProof {
|
|
/// Create new account proof entity.
|
|
pub const fn new(address: Address) -> Self {
|
|
Self {
|
|
address,
|
|
info: None,
|
|
proof: Vec::new(),
|
|
storage_root: EMPTY_ROOT_HASH,
|
|
storage_proofs: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The merkle proof of the storage entry.
|
|
#[derive(Clone, PartialEq, Eq, Default, Debug)]
|
|
#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))]
|
|
pub struct StorageProof {
|
|
/// The raw storage key.
|
|
pub key: B256,
|
|
/// The hashed storage key nibbles.
|
|
pub nibbles: Nibbles,
|
|
/// The storage value.
|
|
pub value: U256,
|
|
/// Array of rlp-serialized merkle trie nodes which starting from the storage root node and
|
|
/// following the path of the hashed storage slot as key.
|
|
pub proof: Vec<Bytes>,
|
|
}
|
|
|
|
impl StorageProof {
|
|
/// Create new storage proof from the storage slot.
|
|
pub fn new(key: B256) -> Self {
|
|
let nibbles = Nibbles::unpack(keccak256(key));
|
|
Self { key, nibbles, ..Default::default() }
|
|
}
|
|
|
|
/// Create new storage proof from the storage slot and its pre-hashed image.
|
|
pub fn new_with_hashed(key: B256, hashed_key: B256) -> Self {
|
|
Self { key, nibbles: Nibbles::unpack(hashed_key), ..Default::default() }
|
|
}
|
|
|
|
/// Create new storage proof from the storage slot and its pre-hashed image.
|
|
pub fn new_with_nibbles(key: B256, nibbles: Nibbles) -> Self {
|
|
Self { key, nibbles, ..Default::default() }
|
|
}
|
|
|
|
/// Set proof nodes on storage proof.
|
|
pub fn with_proof(mut self, proof: Vec<Bytes>) -> Self {
|
|
self.proof = proof;
|
|
self
|
|
}
|
|
|
|
/// Verify the proof against the provided storage root.
|
|
pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> {
|
|
let expected =
|
|
if self.value.is_zero() { None } else { Some(encode_fixed_size(&self.value).to_vec()) };
|
|
verify_proof(root, self.nibbles, expected, &self.proof)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "eip1186")]
|
|
impl StorageProof {
|
|
/// Convert into an EIP-1186 storage proof
|
|
pub fn into_eip1186_proof(
|
|
self,
|
|
slot: alloy_serde::JsonStorageKey,
|
|
) -> alloy_rpc_types_eth::EIP1186StorageProof {
|
|
alloy_rpc_types_eth::EIP1186StorageProof { key: slot, value: self.value, proof: self.proof }
|
|
}
|
|
|
|
/// Convert from an
|
|
/// [`EIP1186StorageProof`](alloy_rpc_types_eth::EIP1186StorageProof)
|
|
///
|
|
/// This is the inverse of [`Self::into_eip1186_proof`].
|
|
pub fn from_eip1186_proof(storage_proof: alloy_rpc_types_eth::EIP1186StorageProof) -> Self {
|
|
Self {
|
|
value: storage_proof.value,
|
|
proof: storage_proof.proof,
|
|
..Self::new(storage_proof.key.as_b256())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "eip1186")]
|
|
impl From<alloy_rpc_types_eth::EIP1186StorageProof> for StorageProof {
|
|
fn from(proof: alloy_rpc_types_eth::EIP1186StorageProof) -> Self {
|
|
Self::from_eip1186_proof(proof)
|
|
}
|
|
}
|
|
|
|
/// The merkle proof of the storage entry, using decoded proofs.
|
|
#[derive(Clone, PartialEq, Eq, Default, Debug)]
|
|
pub struct DecodedStorageProof {
|
|
/// The raw storage key.
|
|
pub key: B256,
|
|
/// The hashed storage key nibbles.
|
|
pub nibbles: Nibbles,
|
|
/// The storage value.
|
|
pub value: U256,
|
|
/// Array of merkle trie nodes which starting from the storage root node and following the path
|
|
/// of the hashed storage slot as key.
|
|
pub proof: Vec<TrieNode>,
|
|
}
|
|
|
|
impl DecodedStorageProof {
|
|
/// Create new storage proof from the storage slot.
|
|
pub fn new(key: B256) -> Self {
|
|
let nibbles = Nibbles::unpack(keccak256(key));
|
|
Self { key, nibbles, ..Default::default() }
|
|
}
|
|
|
|
/// Create new storage proof from the storage slot and its pre-hashed image.
|
|
pub fn new_with_hashed(key: B256, hashed_key: B256) -> Self {
|
|
Self { key, nibbles: Nibbles::unpack(hashed_key), ..Default::default() }
|
|
}
|
|
|
|
/// Create new storage proof from the storage slot and its pre-hashed image.
|
|
pub fn new_with_nibbles(key: B256, nibbles: Nibbles) -> Self {
|
|
Self { key, nibbles, ..Default::default() }
|
|
}
|
|
|
|
/// Set proof nodes on storage proof.
|
|
pub fn with_proof(mut self, proof: Vec<TrieNode>) -> Self {
|
|
self.proof = proof;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Implementation of hasher using our keccak256 hashing function
|
|
/// for compatibility with `triehash` crate.
|
|
#[cfg(any(test, feature = "test-utils"))]
|
|
pub mod triehash {
|
|
use alloy_primitives::{keccak256, B256};
|
|
use alloy_rlp::RlpEncodable;
|
|
use hash_db::Hasher;
|
|
use plain_hasher::PlainHasher;
|
|
|
|
/// A [Hasher] that calculates a keccak256 hash of the given data.
|
|
#[derive(Default, Debug, Clone, PartialEq, Eq, RlpEncodable)]
|
|
#[non_exhaustive]
|
|
pub struct KeccakHasher;
|
|
|
|
#[cfg(any(test, feature = "test-utils"))]
|
|
impl Hasher for KeccakHasher {
|
|
type Out = B256;
|
|
type StdHasher = PlainHasher;
|
|
|
|
const LENGTH: usize = 32;
|
|
|
|
fn hash(x: &[u8]) -> Self::Out {
|
|
keccak256(x)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_multiproof_extend_account_proofs() {
|
|
let mut proof1 = MultiProof::default();
|
|
let mut proof2 = MultiProof::default();
|
|
|
|
let addr1 = B256::random();
|
|
let addr2 = B256::random();
|
|
|
|
proof1.account_subtree.insert(
|
|
Nibbles::unpack(addr1),
|
|
alloy_rlp::encode_fixed_size(&U256::from(42)).to_vec().into(),
|
|
);
|
|
proof2.account_subtree.insert(
|
|
Nibbles::unpack(addr2),
|
|
alloy_rlp::encode_fixed_size(&U256::from(43)).to_vec().into(),
|
|
);
|
|
|
|
proof1.extend(proof2);
|
|
|
|
assert!(proof1.account_subtree.contains_key(&Nibbles::unpack(addr1)));
|
|
assert!(proof1.account_subtree.contains_key(&Nibbles::unpack(addr2)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiproof_extend_storage_proofs() {
|
|
let mut proof1 = MultiProof::default();
|
|
let mut proof2 = MultiProof::default();
|
|
|
|
let addr = B256::random();
|
|
let root = B256::random();
|
|
|
|
let mut subtree1 = ProofNodes::default();
|
|
subtree1.insert(
|
|
Nibbles::from_nibbles(vec![0]),
|
|
alloy_rlp::encode_fixed_size(&U256::from(42)).to_vec().into(),
|
|
);
|
|
proof1.storages.insert(
|
|
addr,
|
|
StorageMultiProof {
|
|
root,
|
|
subtree: subtree1,
|
|
branch_node_masks: BranchNodeMasksMap::default(),
|
|
},
|
|
);
|
|
|
|
let mut subtree2 = ProofNodes::default();
|
|
subtree2.insert(
|
|
Nibbles::from_nibbles(vec![1]),
|
|
alloy_rlp::encode_fixed_size(&U256::from(43)).to_vec().into(),
|
|
);
|
|
proof2.storages.insert(
|
|
addr,
|
|
StorageMultiProof {
|
|
root,
|
|
subtree: subtree2,
|
|
branch_node_masks: BranchNodeMasksMap::default(),
|
|
},
|
|
);
|
|
|
|
proof1.extend(proof2);
|
|
|
|
let storage = proof1.storages.get(&addr).unwrap();
|
|
assert_eq!(storage.root, root);
|
|
assert!(storage.subtree.contains_key(&Nibbles::from_nibbles(vec![0])));
|
|
assert!(storage.subtree.contains_key(&Nibbles::from_nibbles(vec![1])));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_proof_retain_difference() {
|
|
let mut empty = MultiProofTargets::default();
|
|
empty.retain_difference(&Default::default());
|
|
assert!(empty.is_empty());
|
|
|
|
let targets = MultiProofTargets::accounts((0..10).map(B256::with_last_byte));
|
|
|
|
let mut diffed = targets.clone();
|
|
diffed.retain_difference(&MultiProofTargets::account(B256::with_last_byte(11)));
|
|
assert_eq!(diffed, targets);
|
|
|
|
diffed.retain_difference(&MultiProofTargets::accounts((0..5).map(B256::with_last_byte)));
|
|
assert_eq!(diffed, MultiProofTargets::accounts((5..10).map(B256::with_last_byte)));
|
|
|
|
diffed.retain_difference(&targets);
|
|
assert!(diffed.is_empty());
|
|
|
|
let mut targets = MultiProofTargets::default();
|
|
let (account1, account2, account3) =
|
|
(1..=3).map(B256::with_last_byte).collect_tuple().unwrap();
|
|
let account2_slots = (1..5).map(B256::with_last_byte).collect::<B256Set>();
|
|
targets.insert(account1, B256Set::from_iter([B256::with_last_byte(1)]));
|
|
targets.insert(account2, account2_slots.clone());
|
|
targets.insert(account3, B256Set::from_iter([B256::with_last_byte(1)]));
|
|
|
|
let mut diffed = targets.clone();
|
|
diffed.retain_difference(&MultiProofTargets::accounts((1..=3).map(B256::with_last_byte)));
|
|
assert_eq!(diffed, targets);
|
|
|
|
// remove last 3 slots for account 2
|
|
let mut account2_slots_expected_len = account2_slots.len();
|
|
for slot in account2_slots.iter().skip(1) {
|
|
diffed.retain_difference(&MultiProofTargets::account_with_slots(account2, [*slot]));
|
|
account2_slots_expected_len -= 1;
|
|
assert_eq!(
|
|
diffed.get(&account2).map(|slots| slots.len()),
|
|
Some(account2_slots_expected_len)
|
|
);
|
|
}
|
|
|
|
diffed.retain_difference(&targets);
|
|
assert!(diffed.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_proof_retain_difference_no_overlap() {
|
|
let mut targets = MultiProofTargets::default();
|
|
|
|
// populate some targets
|
|
let (addr1, addr2) = (B256::random(), B256::random());
|
|
let (slot1, slot2) = (B256::random(), B256::random());
|
|
targets.insert(addr1, std::iter::once(slot1).collect());
|
|
targets.insert(addr2, std::iter::once(slot2).collect());
|
|
|
|
let mut retained = targets.clone();
|
|
retained.retain_difference(&Default::default());
|
|
assert_eq!(retained, targets);
|
|
|
|
// add a different addr and slot to fetched proof targets
|
|
let mut other_targets = MultiProofTargets::default();
|
|
let addr3 = B256::random();
|
|
let slot3 = B256::random();
|
|
other_targets.insert(addr3, B256Set::from_iter([slot3]));
|
|
|
|
// check that the prefetch proof targets are the same because the fetched proof targets
|
|
// don't overlap with the prefetch targets
|
|
let mut retained = targets.clone();
|
|
retained.retain_difference(&other_targets);
|
|
assert_eq!(retained, targets);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_prefetch_proof_targets_remove_subset() {
|
|
// populate some targets
|
|
let mut targets = MultiProofTargets::default();
|
|
let (addr1, addr2) = (B256::random(), B256::random());
|
|
let (slot1, slot2) = (B256::random(), B256::random());
|
|
targets.insert(addr1, B256Set::from_iter([slot1]));
|
|
targets.insert(addr2, B256Set::from_iter([slot2]));
|
|
|
|
// add a subset of the first target to other proof targets
|
|
let other_targets = MultiProofTargets::account_with_slots(addr1, [slot1]);
|
|
|
|
let mut retained = targets.clone();
|
|
retained.retain_difference(&other_targets);
|
|
|
|
// check that the prefetch proof targets do not include the subset
|
|
assert_eq!(retained.len(), 1);
|
|
assert!(!retained.contains_key(&addr1));
|
|
assert!(retained.contains_key(&addr2));
|
|
|
|
// now add one more slot to the prefetch targets
|
|
let slot3 = B256::random();
|
|
targets.get_mut(&addr1).unwrap().insert(slot3);
|
|
|
|
let mut retained = targets.clone();
|
|
retained.retain_difference(&other_targets);
|
|
|
|
// check that the prefetch proof targets do not include the subset
|
|
// but include the new slot
|
|
assert_eq!(retained.len(), 2);
|
|
assert!(retained.contains_key(&addr1));
|
|
assert_eq!(retained.get(&addr1), Some(&B256Set::from_iter([slot3])));
|
|
assert!(retained.contains_key(&addr2));
|
|
assert_eq!(retained.get(&addr2), Some(&B256Set::from_iter([slot2])));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "eip1186")]
|
|
fn eip_1186_roundtrip() {
|
|
let mut acc = AccountProof {
|
|
address: Address::random(),
|
|
info: Some(
|
|
// non-empty account
|
|
Account { nonce: 100, balance: U256::ZERO, bytecode_hash: Some(KECCAK_EMPTY) },
|
|
),
|
|
proof: vec![],
|
|
storage_root: B256::ZERO,
|
|
storage_proofs: vec![],
|
|
};
|
|
|
|
let rpc_proof = acc.clone().into_eip1186_response(Vec::new());
|
|
let inverse: AccountProof = rpc_proof.into();
|
|
assert_eq!(acc, inverse);
|
|
|
|
// make account empty
|
|
acc.info.as_mut().unwrap().nonce = 0;
|
|
let rpc_proof = acc.clone().into_eip1186_response(Vec::new());
|
|
let inverse: AccountProof = rpc_proof.into();
|
|
acc.info.take();
|
|
acc.storage_root = EMPTY_ROOT_HASH;
|
|
assert_eq!(acc, inverse);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiproof_targets_chunking_length() {
|
|
let mut targets = MultiProofTargets::default();
|
|
targets.insert(B256::with_last_byte(1), B256Set::default());
|
|
targets.insert(
|
|
B256::with_last_byte(2),
|
|
B256Set::from_iter([B256::with_last_byte(10), B256::with_last_byte(20)]),
|
|
);
|
|
targets.insert(
|
|
B256::with_last_byte(3),
|
|
B256Set::from_iter([
|
|
B256::with_last_byte(30),
|
|
B256::with_last_byte(31),
|
|
B256::with_last_byte(32),
|
|
]),
|
|
);
|
|
|
|
let chunking_length = targets.chunking_length();
|
|
for size in 1..=targets.clone().chunks(1).count() {
|
|
let chunk_count = targets.clone().chunks(size).count();
|
|
let expected_count = chunking_length.div_ceil(size);
|
|
assert_eq!(
|
|
chunk_count, expected_count,
|
|
"chunking_length: {}, size: {}",
|
|
chunking_length, size
|
|
);
|
|
}
|
|
}
|
|
}
|