mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-02-19 03:04:27 -05:00
chore(stateless): Remove reth-stateless crate (#22115)
This commit is contained in:
5
.changelog/easy-clouds-meow.md
Normal file
5
.changelog/easy-clouds-meow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
ef-tests: patch
|
||||
---
|
||||
|
||||
Removed reth-stateless crate and stateless validation from ef-tests.
|
||||
1
.github/scripts/check_rv32imac.sh
vendored
1
.github/scripts/check_rv32imac.sh
vendored
@@ -27,7 +27,6 @@ crates_to_check=(
|
||||
reth-ethereum-forks
|
||||
reth-ethereum-primitives
|
||||
reth-ethereum-consensus
|
||||
reth-stateless
|
||||
)
|
||||
|
||||
any_failed=0
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -3328,7 +3328,6 @@ dependencies = [
|
||||
"reth-primitives-traits",
|
||||
"reth-provider",
|
||||
"reth-revm",
|
||||
"reth-stateless",
|
||||
"reth-tracing",
|
||||
"reth-trie",
|
||||
"reth-trie-db",
|
||||
@@ -10268,32 +10267,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reth-stateless"
|
||||
version = "1.10.2"
|
||||
dependencies = [
|
||||
"alloy-consensus",
|
||||
"alloy-genesis",
|
||||
"alloy-primitives",
|
||||
"alloy-rlp",
|
||||
"alloy-rpc-types-debug",
|
||||
"alloy-trie",
|
||||
"itertools 0.14.0",
|
||||
"reth-chainspec",
|
||||
"reth-consensus",
|
||||
"reth-errors",
|
||||
"reth-ethereum-consensus",
|
||||
"reth-ethereum-primitives",
|
||||
"reth-evm",
|
||||
"reth-primitives-traits",
|
||||
"reth-revm",
|
||||
"reth-trie-common",
|
||||
"reth-trie-sparse",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reth-static-file"
|
||||
version = "1.10.2"
|
||||
|
||||
@@ -99,7 +99,6 @@ members = [
|
||||
"crates/stages/api/",
|
||||
"crates/stages/stages/",
|
||||
"crates/stages/types/",
|
||||
"crates/stateless",
|
||||
"crates/static-file/static-file",
|
||||
"crates/static-file/types/",
|
||||
"crates/storage/codecs/",
|
||||
@@ -420,7 +419,6 @@ reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
|
||||
reth-stages = { path = "crates/stages/stages" }
|
||||
reth-stages-api = { path = "crates/stages/api" }
|
||||
reth-stages-types = { path = "crates/stages/types", default-features = false }
|
||||
reth-stateless = { path = "crates/stateless", default-features = false }
|
||||
reth-static-file = { path = "crates/static-file/static-file" }
|
||||
reth-static-file-types = { path = "crates/static-file/types", default-features = false }
|
||||
reth-storage-api = { path = "crates/storage/storage-api", default-features = false }
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "reth-stateless"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
exclude.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# alloy
|
||||
alloy-primitives.workspace = true
|
||||
alloy-rlp.workspace = true
|
||||
alloy-trie.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-rpc-types-debug.workspace = true
|
||||
alloy-genesis = { workspace = true, features = ["serde-bincode-compat"] }
|
||||
|
||||
# reth
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-ethereum-primitives = { workspace = true, features = ["serde", "serde-bincode-compat"] }
|
||||
reth-errors.workspace = true
|
||||
reth-evm.workspace = true
|
||||
reth-revm.workspace = true
|
||||
reth-trie-common.workspace = true
|
||||
reth-trie-sparse.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
reth-consensus.workspace = true
|
||||
|
||||
# misc
|
||||
thiserror.workspace = true
|
||||
itertools.workspace = true
|
||||
serde.workspace = true
|
||||
serde_with.workspace = true
|
||||
@@ -1,78 +0,0 @@
|
||||
//! Provides types and functions for stateless execution and validation of Ethereum blocks.
|
||||
//!
|
||||
//! This crate enables the verification of block execution without requiring access to a
|
||||
//! full node's persistent database. Instead, it relies on pre-generated "witness" data
|
||||
//! that proves the specific state accessed during the block's execution.
|
||||
//!
|
||||
//! # Key Components
|
||||
//!
|
||||
//! * `WitnessDatabase`: An implementation of [`reth_revm::Database`] that uses a
|
||||
//! [`reth_trie_sparse::SparseStateTrie`] populated from witness data, along with provided
|
||||
//! bytecode and ancestor block hashes, to serve state reads during execution.
|
||||
//! * `stateless_validation`: The core function that orchestrates the stateless validation process.
|
||||
//! It takes a block, its execution witness, ancestor headers, and chain specification, then
|
||||
//! performs:
|
||||
//! 1. Witness verification against the parent block's state root.
|
||||
//! 2. Block execution using the `WitnessDatabase`.
|
||||
//! 3. Post-execution consensus checks.
|
||||
//! 4. Post-state root calculation and comparison against the block header.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! The primary entry point is typically the `validation::stateless_validation` function. Callers
|
||||
//! need to provide the block to be validated along with accurately generated `ExecutionWitness`
|
||||
//! data corresponding to that block's execution trace and the necessary Headers of ancestor
|
||||
//! blocks.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
|
||||
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod recover_block;
|
||||
/// Sparse trie implementation for stateless validation
|
||||
pub mod trie;
|
||||
|
||||
use alloy_genesis::ChainConfig;
|
||||
#[doc(inline)]
|
||||
pub use recover_block::UncompressedPublicKey;
|
||||
#[doc(inline)]
|
||||
pub use trie::StatelessTrie;
|
||||
#[doc(inline)]
|
||||
pub use validation::stateless_validation;
|
||||
#[doc(inline)]
|
||||
pub use validation::stateless_validation_with_trie;
|
||||
|
||||
/// Implementation of stateless validation
|
||||
pub mod validation;
|
||||
pub(crate) mod witness_db;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use alloy_rpc_types_debug::ExecutionWitness;
|
||||
|
||||
pub use alloy_genesis::Genesis;
|
||||
|
||||
use reth_ethereum_primitives::Block;
|
||||
|
||||
/// `StatelessInput` is a convenience structure for serializing the input needed
|
||||
/// for the stateless validation function.
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StatelessInput {
|
||||
/// The block being executed in the stateless validation function
|
||||
#[serde_as(
|
||||
as = "reth_primitives_traits::serde_bincode_compat::Block<reth_ethereum_primitives::TransactionSigned, alloy_consensus::Header>"
|
||||
)]
|
||||
pub block: Block,
|
||||
/// `ExecutionWitness` for the stateless validation function
|
||||
pub witness: ExecutionWitness,
|
||||
/// Chain configuration for the stateless validation function
|
||||
#[serde_as(as = "alloy_genesis::serde_bincode_compat::ChainConfig<'_>")]
|
||||
pub chain_config: ChainConfig,
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::validation::StatelessValidationError;
|
||||
use alloc::vec::Vec;
|
||||
use alloy_consensus::BlockHeader;
|
||||
use alloy_primitives::Address;
|
||||
use core::ops::Deref;
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_ethereum_primitives::{Block, TransactionSigned};
|
||||
use reth_primitives_traits::{Block as _, RecoveredBlock};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, Bytes};
|
||||
|
||||
/// Serialized uncompressed public key
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UncompressedPublicKey(#[serde_as(as = "Bytes")] pub [u8; 65]);
|
||||
|
||||
impl Deref for UncompressedPublicKey {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies all transactions in a block against a list of public keys and signatures.
|
||||
///
|
||||
/// Returns a `RecoveredBlock`
|
||||
pub(crate) fn recover_block_with_public_keys<ChainSpec>(
|
||||
block: Block,
|
||||
public_keys: Vec<UncompressedPublicKey>,
|
||||
chain_spec: &ChainSpec,
|
||||
) -> Result<RecoveredBlock<Block>, StatelessValidationError>
|
||||
where
|
||||
ChainSpec: EthereumHardforks,
|
||||
{
|
||||
if block.body().transactions.len() != public_keys.len() {
|
||||
return Err(StatelessValidationError::Custom(
|
||||
"Number of public keys must match number of transactions",
|
||||
));
|
||||
}
|
||||
|
||||
// Determine if we're in the Homestead fork for signature validation
|
||||
let is_homestead = chain_spec.is_homestead_active_at_block(block.header().number());
|
||||
|
||||
// Verify each transaction signature against its corresponding public key
|
||||
let senders = public_keys
|
||||
.iter()
|
||||
.zip(block.body().transactions())
|
||||
.map(|(vk, tx)| verify_and_compute_sender(vk, tx, is_homestead))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Create RecoveredBlock with verified senders
|
||||
let block_hash = block.hash_slow();
|
||||
Ok(RecoveredBlock::new(block, senders, block_hash))
|
||||
}
|
||||
|
||||
/// Verifies a transaction using its signature and the given public key.
|
||||
///
|
||||
/// Note: If the signature or the public key is incorrect, then this method
|
||||
/// will return an error.
|
||||
///
|
||||
/// Returns the address derived from the public key.
|
||||
fn verify_and_compute_sender(
|
||||
vk: &UncompressedPublicKey,
|
||||
tx: &TransactionSigned,
|
||||
is_homestead: bool,
|
||||
) -> Result<Address, StatelessValidationError> {
|
||||
let sig = tx.signature();
|
||||
|
||||
// non-normalized signatures are only valid pre-homestead
|
||||
let sig_is_normalized = sig.normalize_s().is_none();
|
||||
if is_homestead && !sig_is_normalized {
|
||||
return Err(StatelessValidationError::HomesteadSignatureNotNormalized);
|
||||
}
|
||||
let sig_hash = tx.signature_hash();
|
||||
alloy_consensus::crypto::secp256k1::verify_and_compute_signer_unchecked(&vk.0, sig, sig_hash)
|
||||
.map_err(|_| StatelessValidationError::SignerRecovery)
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
use crate::validation::StatelessValidationError;
|
||||
use alloc::{format, vec::Vec};
|
||||
use alloy_primitives::{keccak256, map::B256Map, Address, B256, U256};
|
||||
use alloy_rlp::{Decodable, Encodable};
|
||||
use alloy_rpc_types_debug::ExecutionWitness;
|
||||
use alloy_trie::{TrieAccount, EMPTY_ROOT_HASH};
|
||||
use itertools::Itertools;
|
||||
use reth_errors::ProviderError;
|
||||
use reth_revm::state::Bytecode;
|
||||
use reth_trie_common::{HashedPostState, Nibbles, TRIE_ACCOUNT_RLP_MAX_SIZE};
|
||||
use reth_trie_sparse::{
|
||||
errors::SparseStateTrieResult,
|
||||
provider::{DefaultTrieNodeProvider, DefaultTrieNodeProviderFactory},
|
||||
RevealableSparseTrie, SparseStateTrie, SparseTrie,
|
||||
};
|
||||
|
||||
/// Trait for stateless trie implementations that can be used for stateless validation.
|
||||
pub trait StatelessTrie: core::fmt::Debug {
|
||||
/// Initialize the stateless trie using the `ExecutionWitness`
|
||||
fn new(
|
||||
witness: &ExecutionWitness,
|
||||
pre_state_root: B256,
|
||||
) -> Result<(Self, B256Map<Bytecode>), StatelessValidationError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Returns the `TrieAccount` that corresponds to the `Address`
|
||||
///
|
||||
/// This method will error if the `ExecutionWitness` is not able to guarantee
|
||||
/// that the account is missing from the Trie _and_ the witness was complete.
|
||||
fn account(&self, address: Address) -> Result<Option<TrieAccount>, ProviderError>;
|
||||
|
||||
/// Returns the storage slot value that corresponds to the given (address, slot) tuple.
|
||||
///
|
||||
/// This method will error if the `ExecutionWitness` is not able to guarantee
|
||||
/// that the storage was missing from the Trie _and_ the witness was complete.
|
||||
fn storage(&self, address: Address, slot: U256) -> Result<U256, ProviderError>;
|
||||
|
||||
/// Computes the new state root from the `HashedPostState`.
|
||||
fn calculate_state_root(
|
||||
&mut self,
|
||||
state: HashedPostState,
|
||||
) -> Result<B256, StatelessValidationError>;
|
||||
}
|
||||
|
||||
/// `StatelessSparseTrie` structure for usage during stateless validation
|
||||
#[derive(Debug)]
|
||||
pub struct StatelessSparseTrie {
|
||||
inner: SparseStateTrie,
|
||||
}
|
||||
|
||||
impl StatelessSparseTrie {
|
||||
/// Initialize the stateless trie using the `ExecutionWitness`
|
||||
///
|
||||
/// Note: Currently this method does not check that the `ExecutionWitness`
|
||||
/// is complete for all of the preimage keys.
|
||||
pub fn new(
|
||||
witness: &ExecutionWitness,
|
||||
pre_state_root: B256,
|
||||
) -> Result<(Self, B256Map<Bytecode>), StatelessValidationError> {
|
||||
verify_execution_witness(witness, pre_state_root)
|
||||
.map(|(inner, bytecode)| (Self { inner }, bytecode))
|
||||
}
|
||||
|
||||
/// Returns the `TrieAccount` that corresponds to the `Address`
|
||||
///
|
||||
/// This method will error if the `ExecutionWitness` is not able to guarantee
|
||||
/// that the account is missing from the Trie _and_ the witness was complete.
|
||||
pub fn account(&self, address: Address) -> Result<Option<TrieAccount>, ProviderError> {
|
||||
let hashed_address = keccak256(address);
|
||||
|
||||
if let Some(bytes) = self.inner.get_account_value(&hashed_address) {
|
||||
let account = TrieAccount::decode(&mut bytes.as_slice())?;
|
||||
return Ok(Some(account))
|
||||
}
|
||||
|
||||
if !self.inner.check_valid_account_witness(hashed_address) {
|
||||
return Err(ProviderError::TrieWitnessError(format!(
|
||||
"incomplete account witness for {hashed_address:?}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the storage slot value that corresponds to the given (address, slot) tuple.
|
||||
///
|
||||
/// This method will error if the `ExecutionWitness` is not able to guarantee
|
||||
/// that the storage was missing from the Trie _and_ the witness was complete.
|
||||
pub fn storage(&self, address: Address, slot: U256) -> Result<U256, ProviderError> {
|
||||
let hashed_address = keccak256(address);
|
||||
let hashed_slot = keccak256(B256::from(slot));
|
||||
|
||||
if let Some(raw) = self.inner.get_storage_slot_value(&hashed_address, &hashed_slot) {
|
||||
return Ok(U256::decode(&mut raw.as_slice())?)
|
||||
}
|
||||
|
||||
// Storage slot value is not present in the trie, validate that the witness is complete.
|
||||
// If the account exists in the trie...
|
||||
if let Some(bytes) = self.inner.get_account_value(&hashed_address) {
|
||||
// ...check that its storage is either empty or the storage trie was sufficiently
|
||||
// revealed...
|
||||
let account = TrieAccount::decode(&mut bytes.as_slice())?;
|
||||
if account.storage_root != EMPTY_ROOT_HASH &&
|
||||
!self.inner.check_valid_storage_witness(hashed_address, hashed_slot)
|
||||
{
|
||||
return Err(ProviderError::TrieWitnessError(format!(
|
||||
"incomplete storage witness: prover must supply exclusion proof for slot {hashed_slot:?} in account {hashed_address:?}"
|
||||
)));
|
||||
}
|
||||
} else if !self.inner.check_valid_account_witness(hashed_address) {
|
||||
// ...else if account is missing, validate that the account trie was sufficiently
|
||||
// revealed.
|
||||
return Err(ProviderError::TrieWitnessError(format!(
|
||||
"incomplete account witness for {hashed_address:?}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(U256::ZERO)
|
||||
}
|
||||
|
||||
/// Computes the new state root from the `HashedPostState`.
|
||||
pub fn calculate_state_root(
|
||||
&mut self,
|
||||
state: HashedPostState,
|
||||
) -> Result<B256, StatelessValidationError> {
|
||||
calculate_state_root(&mut self.inner, state)
|
||||
.map_err(|_e| StatelessValidationError::StatelessStateRootCalculationFailed)
|
||||
}
|
||||
}
|
||||
|
||||
impl StatelessTrie for StatelessSparseTrie {
|
||||
fn new(
|
||||
witness: &ExecutionWitness,
|
||||
pre_state_root: B256,
|
||||
) -> Result<(Self, B256Map<Bytecode>), StatelessValidationError> {
|
||||
Self::new(witness, pre_state_root)
|
||||
}
|
||||
|
||||
fn account(&self, address: Address) -> Result<Option<TrieAccount>, ProviderError> {
|
||||
self.account(address)
|
||||
}
|
||||
|
||||
fn storage(&self, address: Address, slot: U256) -> Result<U256, ProviderError> {
|
||||
self.storage(address, slot)
|
||||
}
|
||||
|
||||
fn calculate_state_root(
|
||||
&mut self,
|
||||
state: HashedPostState,
|
||||
) -> Result<B256, StatelessValidationError> {
|
||||
self.calculate_state_root(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies execution witness [`ExecutionWitness`] against an expected pre-state root.
|
||||
///
|
||||
/// This function takes the RLP-encoded values provided in [`ExecutionWitness`]
|
||||
/// (which includes state trie nodes, storage trie nodes, and contract bytecode)
|
||||
/// and uses it to populate a new [`SparseStateTrie`].
|
||||
///
|
||||
/// If the computed root hash matches the `pre_state_root`, it signifies that the
|
||||
/// provided execution witness is consistent with that pre-state root. In this case, the function
|
||||
/// returns the populated [`SparseStateTrie`] and a [`B256Map`] containing the
|
||||
/// contract bytecode (mapping code hash to [`Bytecode`]).
|
||||
///
|
||||
/// The bytecode has a separate mapping because the [`SparseStateTrie`] does not store the
|
||||
/// contract bytecode, only the hash of it (code hash).
|
||||
///
|
||||
/// If the roots do not match, it returns an error indicating the witness is invalid
|
||||
/// for the given `pre_state_root` (see `StatelessValidationError::PreStateRootMismatch`).
|
||||
// Note: This approach might be inefficient for ZKVMs requiring minimal memory operations, which
|
||||
// would explain why they have for the most part re-implemented this function.
|
||||
fn verify_execution_witness(
|
||||
witness: &ExecutionWitness,
|
||||
pre_state_root: B256,
|
||||
) -> Result<(SparseStateTrie, B256Map<Bytecode>), StatelessValidationError> {
|
||||
let provider_factory = DefaultTrieNodeProviderFactory;
|
||||
let mut trie = SparseStateTrie::new();
|
||||
let mut state_witness = B256Map::default();
|
||||
let mut bytecode = B256Map::default();
|
||||
|
||||
for rlp_encoded in &witness.state {
|
||||
let hash = keccak256(rlp_encoded);
|
||||
state_witness.insert(hash, rlp_encoded.clone());
|
||||
}
|
||||
for rlp_encoded in &witness.codes {
|
||||
let hash = keccak256(rlp_encoded);
|
||||
bytecode.insert(hash, Bytecode::new_raw(rlp_encoded.clone()));
|
||||
}
|
||||
|
||||
// Reveal the witness with our state root
|
||||
// This method builds a trie using the sparse trie using the state_witness with
|
||||
// the root being the pre_state_root.
|
||||
// Here are some things to note:
|
||||
// - You can pass in more witnesses than is needed for the block execution.
|
||||
// - If you try to get an account and it has not been seen. This means that the account
|
||||
// was not inserted into the Trie. It does not mean that the account does not exist.
|
||||
// In order to determine an account not existing, we must do an exclusion proof.
|
||||
trie.reveal_witness(pre_state_root, &state_witness)
|
||||
.map_err(|_e| StatelessValidationError::WitnessRevealFailed { pre_state_root })?;
|
||||
|
||||
// Calculate the root
|
||||
let computed_root = trie
|
||||
.root(&provider_factory)
|
||||
.map_err(|_e| StatelessValidationError::StatelessPreStateRootCalculationFailed)?;
|
||||
|
||||
if computed_root == pre_state_root {
|
||||
Ok((trie, bytecode))
|
||||
} else {
|
||||
Err(StatelessValidationError::PreStateRootMismatch {
|
||||
got: computed_root,
|
||||
expected: pre_state_root,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Copied and modified from ress: https://github.com/paradigmxyz/ress/blob/06bf2c4788e45b8fcbd640e38b6243e6f87c4d0e/crates/engine/src/tree/root.rs
|
||||
/// Calculates the post-execution state root by applying state changes to a sparse trie.
|
||||
///
|
||||
/// This function takes a [`SparseStateTrie`] with the pre-state and a [`HashedPostState`]
|
||||
/// containing account and storage changes resulting from block execution (state diff).
|
||||
///
|
||||
/// It modifies the input `trie` in place to reflect these changes and then calculates the
|
||||
/// final post-execution state root.
|
||||
fn calculate_state_root(
|
||||
trie: &mut SparseStateTrie,
|
||||
state: HashedPostState,
|
||||
) -> SparseStateTrieResult<B256> {
|
||||
// 1. Apply storage‑slot updates and compute each contract’s storage root
|
||||
//
|
||||
//
|
||||
// We walk over every (address, storage) pair in deterministic order
|
||||
// and update the corresponding per‑account storage trie in‑place.
|
||||
// When we’re done we collect (address, updated_storage_trie) in a `Vec`
|
||||
// so that we can insert them back into the outer state trie afterwards ― this avoids
|
||||
// borrowing issues.
|
||||
let mut storage_results = Vec::with_capacity(state.storages.len());
|
||||
|
||||
// In `verify_execution_witness` a `DefaultTrieNodeProviderFactory` is used, so we use the same
|
||||
// again in here.
|
||||
let provider_factory = DefaultTrieNodeProviderFactory;
|
||||
let storage_provider = DefaultTrieNodeProvider;
|
||||
|
||||
for (address, storage) in state.storages.into_iter().sorted_unstable_by_key(|(addr, _)| *addr) {
|
||||
// Take the existing storage trie (or create an empty, “revealed” one)
|
||||
let mut storage_trie =
|
||||
trie.take_storage_trie(&address).unwrap_or_else(RevealableSparseTrie::revealed_empty);
|
||||
|
||||
if storage.wiped {
|
||||
storage_trie.wipe()?;
|
||||
}
|
||||
|
||||
// Apply slot‑level changes
|
||||
for (hashed_slot, value) in
|
||||
storage.storage.into_iter().sorted_unstable_by_key(|(slot, _)| *slot)
|
||||
{
|
||||
let nibbles = Nibbles::unpack(hashed_slot);
|
||||
if value.is_zero() {
|
||||
storage_trie.remove_leaf(&nibbles, &storage_provider)?;
|
||||
} else {
|
||||
storage_trie.update_leaf(
|
||||
nibbles,
|
||||
alloy_rlp::encode_fixed_size(&value).to_vec(),
|
||||
&storage_provider,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalise the storage‑trie root before pushing the result
|
||||
storage_trie.root();
|
||||
storage_results.push((address, storage_trie));
|
||||
}
|
||||
|
||||
// Insert every updated storage trie back into the outer state trie
|
||||
for (address, storage_trie) in storage_results {
|
||||
trie.insert_storage_trie(address, storage_trie);
|
||||
}
|
||||
|
||||
// 2. Apply account‑level updates and (re)encode the account nodes
|
||||
// Update accounts with new values
|
||||
// TODO: upstream changes into reth so that `SparseStateTrie::update_account` handles this
|
||||
let mut account_rlp_buf = Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE);
|
||||
|
||||
for (hashed_address, account) in
|
||||
state.accounts.into_iter().sorted_unstable_by_key(|(addr, _)| *addr)
|
||||
{
|
||||
let nibbles = Nibbles::unpack(hashed_address);
|
||||
|
||||
// Determine which storage root should be used for this account
|
||||
let storage_root = if let Some(storage_trie) = trie.storage_trie_mut(&hashed_address) {
|
||||
storage_trie.root()
|
||||
} else if let Some(value) = trie.get_account_value(&hashed_address) {
|
||||
TrieAccount::decode(&mut &value[..])?.storage_root
|
||||
} else {
|
||||
EMPTY_ROOT_HASH
|
||||
};
|
||||
|
||||
// Decide whether to remove or update the account leaf
|
||||
if let Some(account) = account {
|
||||
account_rlp_buf.clear();
|
||||
account.into_trie_account(storage_root).encode(&mut account_rlp_buf);
|
||||
trie.update_account_leaf(nibbles, account_rlp_buf.clone(), &provider_factory)?;
|
||||
} else {
|
||||
trie.remove_account_leaf(&nibbles, &provider_factory)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Return new state root
|
||||
trie.root(&provider_factory)
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
use crate::{
|
||||
recover_block::{recover_block_with_public_keys, UncompressedPublicKey},
|
||||
trie::{StatelessSparseTrie, StatelessTrie},
|
||||
witness_db::WitnessDatabase,
|
||||
ExecutionWitness,
|
||||
};
|
||||
use alloc::{
|
||||
collections::BTreeMap,
|
||||
fmt::Debug,
|
||||
string::{String, ToString},
|
||||
sync::Arc,
|
||||
vec::Vec,
|
||||
};
|
||||
use alloy_consensus::{BlockHeader, Header};
|
||||
use alloy_primitives::{keccak256, B256};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_consensus::{Consensus, HeaderValidator};
|
||||
use reth_errors::ConsensusError;
|
||||
use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
|
||||
use reth_ethereum_primitives::{Block, EthPrimitives, EthereumReceipt};
|
||||
use reth_evm::{
|
||||
execute::{BlockExecutionOutput, Executor},
|
||||
ConfigureEvm,
|
||||
};
|
||||
use reth_primitives_traits::{RecoveredBlock, SealedHeader};
|
||||
use reth_trie_common::{HashedPostState, KeccakKeyHasher};
|
||||
|
||||
/// BLOCKHASH ancestor lookup window limit per EVM (number of most recent blocks accessible).
|
||||
const BLOCKHASH_ANCESTOR_LIMIT: usize = 256;
|
||||
|
||||
/// Errors that can occur during stateless validation.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StatelessValidationError {
|
||||
/// Error when the number of ancestor headers exceeds the limit.
|
||||
#[error("ancestor header count ({count}) exceeds limit ({limit})")]
|
||||
AncestorHeaderLimitExceeded {
|
||||
/// The number of headers provided.
|
||||
count: usize,
|
||||
/// The limit.
|
||||
limit: usize,
|
||||
},
|
||||
|
||||
/// Error when the ancestor headers do not form a contiguous chain.
|
||||
#[error("invalid ancestor chain")]
|
||||
InvalidAncestorChain,
|
||||
|
||||
/// Error when revealing the witness data failed.
|
||||
#[error("failed to reveal witness data for pre-state root {pre_state_root}")]
|
||||
WitnessRevealFailed {
|
||||
/// The pre-state root used for verification.
|
||||
pre_state_root: B256,
|
||||
},
|
||||
|
||||
/// Error during stateless block execution.
|
||||
#[error("stateless block execution failed: {0}")]
|
||||
StatelessExecutionFailed(String),
|
||||
|
||||
/// Error during consensus validation of the block.
|
||||
#[error("consensus validation failed: {0}")]
|
||||
ConsensusValidationFailed(#[from] ConsensusError),
|
||||
|
||||
/// Error during stateless state root calculation.
|
||||
#[error("stateless state root calculation failed")]
|
||||
StatelessStateRootCalculationFailed,
|
||||
|
||||
/// Error calculating the pre-state root from the witness data.
|
||||
#[error("stateless pre-state root calculation failed")]
|
||||
StatelessPreStateRootCalculationFailed,
|
||||
|
||||
/// Error when required ancestor headers are missing (e.g., parent header for pre-state root).
|
||||
#[error("missing required ancestor headers")]
|
||||
MissingAncestorHeader,
|
||||
|
||||
/// Error when deserializing ancestor headers
|
||||
#[error("could not deserialize ancestor headers")]
|
||||
HeaderDeserializationFailed,
|
||||
|
||||
/// Error when the computed state root does not match the one in the block header.
|
||||
#[error("mismatched post-state root: {got}\n {expected}")]
|
||||
PostStateRootMismatch {
|
||||
/// The computed post-state root
|
||||
got: B256,
|
||||
/// The expected post-state root; in the block header
|
||||
expected: B256,
|
||||
},
|
||||
|
||||
/// Error when the computed pre-state root does not match the expected one.
|
||||
#[error("mismatched pre-state root: {got} \n {expected}")]
|
||||
PreStateRootMismatch {
|
||||
/// The computed pre-state root
|
||||
got: B256,
|
||||
/// The expected pre-state root from the previous block
|
||||
expected: B256,
|
||||
},
|
||||
|
||||
/// Error during signer recovery.
|
||||
#[error("signer recovery failed")]
|
||||
SignerRecovery,
|
||||
|
||||
/// Error when signature has non-normalized s value in homestead block.
|
||||
#[error("signature s value not normalized for homestead block")]
|
||||
HomesteadSignatureNotNormalized,
|
||||
|
||||
/// Custom error.
|
||||
#[error("{0}")]
|
||||
Custom(&'static str),
|
||||
}
|
||||
|
||||
/// Performs stateless validation of a block using the provided witness data.
|
||||
///
|
||||
/// This function attempts to fully validate a given `current_block` statelessly, ie without access
|
||||
/// to a persistent database.
|
||||
/// It relies entirely on the `witness` data and `ancestor_headers`
|
||||
/// provided alongside the block.
|
||||
///
|
||||
/// The witness data is validated in the following way:
|
||||
///
|
||||
/// 1. **Ancestor Header Verification:** Checks if the `ancestor_headers` are present, form a
|
||||
/// contiguous chain back from `current_block`'s parent, and do not exceed the `BLOCKHASH` opcode
|
||||
/// limit using `compute_ancestor_hashes`. We must have at least one ancestor header, even if the
|
||||
/// `BLOCKHASH` opcode is not used because we need the state root of the previous block to verify
|
||||
/// the pre state reads.
|
||||
///
|
||||
/// 2. **Pre-State Verification:** Retrieves the expected `pre_state_root` from the parent header
|
||||
/// from `ancestor_headers`. Verifies the provided [`ExecutionWitness`] against the
|
||||
/// `pre_state_root`.
|
||||
///
|
||||
/// 3. **Chain Verification:** The code currently does not verify the [`EthChainSpec`] and expects a
|
||||
/// higher level function to assert that this is correct by, for example, asserting that it is
|
||||
/// equal to the Ethereum Mainnet `ChainSpec` or asserting against the genesis hash that this
|
||||
/// `ChainSpec` defines.
|
||||
///
|
||||
/// High Level Overview of functionality:
|
||||
///
|
||||
/// - Verify all state accesses against a trusted pre-state root
|
||||
/// - Put all state accesses into an in-memory database
|
||||
/// - Use the in-memory database to execute the block
|
||||
/// - Validate the output of block execution (e.g. receipts, logs, requests)
|
||||
/// - Compute the post-state root using the state-diff from block execution
|
||||
/// - Check that the post-state root is the state root in the block.
|
||||
///
|
||||
/// If all steps succeed the function returns `Some` containing the hash of the validated
|
||||
/// `current_block`.
|
||||
pub fn stateless_validation<ChainSpec, E>(
|
||||
current_block: Block,
|
||||
public_keys: Vec<UncompressedPublicKey>,
|
||||
witness: ExecutionWitness,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
evm_config: E,
|
||||
) -> Result<(B256, BlockExecutionOutput<EthereumReceipt>), StatelessValidationError>
|
||||
where
|
||||
ChainSpec: Send + Sync + EthChainSpec<Header = Header> + EthereumHardforks + Debug,
|
||||
E: ConfigureEvm<Primitives = EthPrimitives> + Clone + 'static,
|
||||
{
|
||||
stateless_validation_with_trie::<StatelessSparseTrie, ChainSpec, E>(
|
||||
current_block,
|
||||
public_keys,
|
||||
witness,
|
||||
chain_spec,
|
||||
evm_config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Performs stateless validation of a block using a custom `StatelessTrie` implementation.
|
||||
///
|
||||
/// This is a generic version of `stateless_validation` that allows users to provide their own
|
||||
/// implementation of the `StatelessTrie` for custom trie backends or optimizations.
|
||||
///
|
||||
/// See `stateless_validation` for detailed documentation of the validation process.
|
||||
pub fn stateless_validation_with_trie<T, ChainSpec, E>(
|
||||
current_block: Block,
|
||||
public_keys: Vec<UncompressedPublicKey>,
|
||||
witness: ExecutionWitness,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
evm_config: E,
|
||||
) -> Result<(B256, BlockExecutionOutput<EthereumReceipt>), StatelessValidationError>
|
||||
where
|
||||
T: StatelessTrie,
|
||||
ChainSpec: Send + Sync + EthChainSpec<Header = Header> + EthereumHardforks + Debug,
|
||||
E: ConfigureEvm<Primitives = EthPrimitives> + Clone + 'static,
|
||||
{
|
||||
let current_block = recover_block_with_public_keys(current_block, public_keys, &*chain_spec)?;
|
||||
|
||||
let mut ancestor_headers: Vec<_> = witness
|
||||
.headers
|
||||
.iter()
|
||||
.map(|bytes| {
|
||||
let hash = keccak256(bytes);
|
||||
alloy_rlp::decode_exact::<Header>(bytes)
|
||||
.map(|h| SealedHeader::new(h, hash))
|
||||
.map_err(|_| StatelessValidationError::HeaderDeserializationFailed)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
// Sort the headers by their block number to ensure that they are in
|
||||
// ascending order.
|
||||
ancestor_headers.sort_by_key(|header| header.number());
|
||||
|
||||
// Enforce BLOCKHASH ancestor headers limit (256 most recent blocks)
|
||||
let count = ancestor_headers.len();
|
||||
if count > BLOCKHASH_ANCESTOR_LIMIT {
|
||||
return Err(StatelessValidationError::AncestorHeaderLimitExceeded {
|
||||
count,
|
||||
limit: BLOCKHASH_ANCESTOR_LIMIT,
|
||||
});
|
||||
}
|
||||
|
||||
// Check that the ancestor headers form a contiguous chain and are not just random headers.
|
||||
let ancestor_hashes = compute_ancestor_hashes(¤t_block, &ancestor_headers)?;
|
||||
|
||||
// There should be at least one ancestor header.
|
||||
// The edge case here would be the genesis block, but we do not create proofs for the genesis
|
||||
// block.
|
||||
let parent = match ancestor_headers.last() {
|
||||
Some(prev_header) => prev_header,
|
||||
None => return Err(StatelessValidationError::MissingAncestorHeader),
|
||||
};
|
||||
|
||||
// Validate block against pre-execution consensus rules
|
||||
validate_block_consensus(chain_spec.clone(), ¤t_block, parent)?;
|
||||
|
||||
// First verify that the pre-state reads are correct
|
||||
let (mut trie, bytecode) = T::new(&witness, parent.state_root)?;
|
||||
|
||||
// Create an in-memory database that will use the reads to validate the block
|
||||
let db = WitnessDatabase::new(&trie, bytecode, ancestor_hashes);
|
||||
|
||||
// Execute the block
|
||||
let executor = evm_config.executor(db);
|
||||
let output = executor
|
||||
.execute(¤t_block)
|
||||
.map_err(|e| StatelessValidationError::StatelessExecutionFailed(e.to_string()))?;
|
||||
|
||||
// Post validation checks
|
||||
validate_block_post_execution(
|
||||
¤t_block,
|
||||
&chain_spec,
|
||||
&output.receipts,
|
||||
&output.requests,
|
||||
None,
|
||||
)
|
||||
.map_err(StatelessValidationError::ConsensusValidationFailed)?;
|
||||
|
||||
// Compute and check the post state root
|
||||
let hashed_state = HashedPostState::from_bundle_state::<KeccakKeyHasher>(&output.state.state);
|
||||
let state_root = trie.calculate_state_root(hashed_state)?;
|
||||
if state_root != current_block.state_root {
|
||||
return Err(StatelessValidationError::PostStateRootMismatch {
|
||||
got: state_root,
|
||||
expected: current_block.state_root,
|
||||
});
|
||||
}
|
||||
|
||||
// Return block hash
|
||||
Ok((current_block.hash_slow(), output))
|
||||
}
|
||||
|
||||
/// Performs consensus validation checks on a block without execution or state validation.
|
||||
///
|
||||
/// This function validates a block against Ethereum consensus rules by:
|
||||
///
|
||||
/// 1. **Header Validation:** Validates the sealed header against protocol specifications,
|
||||
/// including:
|
||||
/// - Gas limit checks
|
||||
/// - Base fee validation for EIP-1559
|
||||
/// - Withdrawals root validation for Shanghai fork
|
||||
/// - Blob-related fields validation for Cancun fork
|
||||
///
|
||||
/// 2. **Pre-Execution Validation:** Validates block structure, transaction format, signature
|
||||
/// validity, and other pre-execution requirements.
|
||||
///
|
||||
/// This function acts as a preliminary validation before executing and validating the state
|
||||
/// transition function.
|
||||
fn validate_block_consensus<ChainSpec>(
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
block: &RecoveredBlock<Block>,
|
||||
parent: &SealedHeader<Header>,
|
||||
) -> Result<(), StatelessValidationError>
|
||||
where
|
||||
ChainSpec: Send + Sync + EthChainSpec<Header = Header> + EthereumHardforks + Debug,
|
||||
{
|
||||
let consensus = EthBeaconConsensus::new(chain_spec);
|
||||
|
||||
consensus.validate_header(block.sealed_header())?;
|
||||
consensus.validate_header_against_parent(block.sealed_header(), parent)?;
|
||||
|
||||
consensus.validate_block_pre_execution(block)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies the contiguity, number of ancestor headers and extracts their hashes.
|
||||
///
|
||||
/// This function is used to prepare the data required for the `BLOCKHASH`
|
||||
/// opcode in a stateless execution context.
|
||||
///
|
||||
/// It verifies that the provided `ancestor_headers` form a valid, unbroken chain leading back from
|
||||
/// the parent of the `current_block`.
|
||||
///
|
||||
/// Note: This function becomes obsolete if EIP-2935 is implemented.
|
||||
/// Note: The headers are assumed to be in ascending order.
|
||||
///
|
||||
/// If both checks pass, it returns a [`BTreeMap`] mapping the block number of each
|
||||
/// ancestor header to its corresponding block hash.
|
||||
fn compute_ancestor_hashes(
|
||||
current_block: &RecoveredBlock<Block>,
|
||||
ancestor_headers: &[SealedHeader],
|
||||
) -> Result<BTreeMap<u64, B256>, StatelessValidationError> {
|
||||
let mut ancestor_hashes = BTreeMap::new();
|
||||
|
||||
let mut child_header = current_block.sealed_header();
|
||||
|
||||
// Next verify that headers supplied are contiguous
|
||||
for parent_header in ancestor_headers.iter().rev() {
|
||||
let parent_hash = child_header.parent_hash();
|
||||
ancestor_hashes.insert(parent_header.number, parent_hash);
|
||||
|
||||
if parent_hash != parent_header.hash() {
|
||||
return Err(StatelessValidationError::InvalidAncestorChain); // Blocks must be contiguous
|
||||
}
|
||||
|
||||
if parent_header.number + 1 != child_header.number {
|
||||
return Err(StatelessValidationError::InvalidAncestorChain); // Header number should be
|
||||
// contiguous
|
||||
}
|
||||
|
||||
child_header = parent_header
|
||||
}
|
||||
|
||||
Ok(ancestor_hashes)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
//! Provides the [`WitnessDatabase`] type, an implementation of [`reth_revm::Database`]
|
||||
//! specifically designed for stateless execution environments.
|
||||
|
||||
use crate::trie::StatelessTrie;
|
||||
use alloc::{collections::btree_map::BTreeMap, format};
|
||||
use alloy_primitives::{map::B256Map, Address, B256, U256};
|
||||
use reth_errors::ProviderError;
|
||||
use reth_revm::{bytecode::Bytecode, state::AccountInfo, Database};
|
||||
|
||||
/// An EVM database implementation backed by witness data.
|
||||
///
|
||||
/// This struct implements the [`reth_revm::Database`] trait, allowing the EVM to execute
|
||||
/// transactions using:
|
||||
/// - Account and storage slot data provided by a [`StatelessTrie`] implementation.
|
||||
/// - Bytecode and ancestor block hashes provided by in-memory maps.
|
||||
///
|
||||
/// This is designed for stateless execution scenarios where direct access to a full node's
|
||||
/// database is not available or desired.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WitnessDatabase<'a, T>
|
||||
where
|
||||
T: StatelessTrie,
|
||||
{
|
||||
/// Map of block numbers to block hashes.
|
||||
/// This is used to service the `BLOCKHASH` opcode.
|
||||
block_hashes_by_block_number: BTreeMap<u64, B256>,
|
||||
/// Map of code hashes to bytecode.
|
||||
/// Used to fetch contract code needed during execution.
|
||||
bytecode: B256Map<Bytecode>,
|
||||
/// The sparse Merkle Patricia Trie containing account and storage state.
|
||||
/// This is used to provide account/storage values during EVM execution.
|
||||
trie: &'a T,
|
||||
}
|
||||
|
||||
impl<'a, T> WitnessDatabase<'a, T>
|
||||
where
|
||||
T: StatelessTrie,
|
||||
{
|
||||
/// Creates a new [`WitnessDatabase`] instance.
|
||||
///
|
||||
/// # Assumptions
|
||||
///
|
||||
/// This function assumes:
|
||||
/// 1. The provided `trie` has been populated with state data consistent with a known state root
|
||||
/// (e.g., using witness data and verifying against a parent block's state root).
|
||||
/// 2. The `bytecode` map contains all bytecode corresponding to code hashes present in the
|
||||
/// account data within the `trie`.
|
||||
/// 3. The `ancestor_hashes` map contains the block hashes for the relevant ancestor blocks (up
|
||||
/// to 256 including the current block number). It assumes these hashes correspond to a
|
||||
/// contiguous chain of blocks. The caller is responsible for verifying the contiguity and
|
||||
/// the block limit.
|
||||
pub(crate) const fn new(
|
||||
trie: &'a T,
|
||||
bytecode: B256Map<Bytecode>,
|
||||
ancestor_hashes: BTreeMap<u64, B256>,
|
||||
) -> Self {
|
||||
Self { trie, block_hashes_by_block_number: ancestor_hashes, bytecode }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Database for WitnessDatabase<'_, T>
|
||||
where
|
||||
T: StatelessTrie,
|
||||
{
|
||||
/// The database error type.
|
||||
type Error = ProviderError;
|
||||
|
||||
/// Get basic account information by hashing the address and looking up the account RLP
|
||||
/// in the underlying [`StatelessTrie`] implementation.
|
||||
///
|
||||
/// Returns `Ok(None)` if the account is not found in the trie.
|
||||
fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
|
||||
self.trie.account(address).map(|opt| {
|
||||
opt.map(|account| AccountInfo {
|
||||
balance: account.balance,
|
||||
nonce: account.nonce,
|
||||
code_hash: account.code_hash,
|
||||
code: None,
|
||||
account_id: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get storage value of an account at a specific slot.
|
||||
///
|
||||
/// Returns `U256::ZERO` if the slot is not found in the trie.
|
||||
fn storage(&mut self, address: Address, slot: U256) -> Result<U256, Self::Error> {
|
||||
self.trie.storage(address, slot)
|
||||
}
|
||||
|
||||
/// Get account code by its hash from the provided bytecode map.
|
||||
///
|
||||
/// Returns an error if the bytecode for the given hash is not found in the map.
|
||||
fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
|
||||
self.bytecode.get(&code_hash).cloned().ok_or_else(|| {
|
||||
ProviderError::TrieWitnessError(format!("bytecode for {code_hash} not found"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get block hash by block number from the provided ancestor hashes map.
|
||||
///
|
||||
/// Returns an error if the hash for the given block number is not found in the map.
|
||||
fn block_hash(&mut self, block_number: u64) -> Result<B256, Self::Error> {
|
||||
self.block_hashes_by_block_number
|
||||
.get(&block_number)
|
||||
.copied()
|
||||
.ok_or(ProviderError::StateForNumberNotFound(block_number))
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,7 @@ reth-provider = { workspace = true, features = ["test-utils"] }
|
||||
reth-evm.workspace = true
|
||||
reth-evm-ethereum.workspace = true
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-revm = { workspace = true, features = ["std", "witness"] }
|
||||
reth-stateless = { workspace = true }
|
||||
reth-revm.workspace = true
|
||||
reth-tracing.workspace = true
|
||||
reth-trie.workspace = true
|
||||
reth-trie-db.workspace = true
|
||||
|
||||
@@ -4,28 +4,22 @@ use crate::{
|
||||
models::{BlockchainTest, ForkSpec},
|
||||
Case, Error, Suite,
|
||||
};
|
||||
use alloy_rlp::{Decodable, Encodable};
|
||||
use alloy_rlp::Decodable;
|
||||
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
|
||||
use reth_chainspec::ChainSpec;
|
||||
use reth_consensus::{Consensus, HeaderValidator};
|
||||
use reth_db_common::init::{insert_genesis_hashes, insert_genesis_history, insert_genesis_state};
|
||||
use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
|
||||
use reth_ethereum_primitives::{Block, TransactionSigned};
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_evm::{execute::Executor, ConfigureEvm};
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_primitives_traits::{
|
||||
Block as BlockTrait, ParallelBridgeBuffered, RecoveredBlock, SealedBlock,
|
||||
};
|
||||
use reth_primitives_traits::{ParallelBridgeBuffered, RecoveredBlock, SealedBlock};
|
||||
use reth_provider::{
|
||||
test_utils::create_test_provider_factory_with_chain_spec, BlockWriter, DatabaseProviderFactory,
|
||||
ExecutionOutcome, HeaderProvider, HistoryWriter, OriginalValuesKnown, StateProofProvider,
|
||||
StateWriteConfig, StateWriter, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
|
||||
};
|
||||
use reth_revm::{database::StateProviderDatabase, witness::ExecutionWitnessRecord, State};
|
||||
use reth_stateless::{
|
||||
trie::StatelessSparseTrie, validation::stateless_validation_with_trie, ExecutionWitness,
|
||||
UncompressedPublicKey,
|
||||
ExecutionOutcome, HistoryWriter, OriginalValuesKnown, StateWriteConfig, StateWriter,
|
||||
StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
|
||||
};
|
||||
use reth_revm::database::StateProviderDatabase;
|
||||
use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot};
|
||||
use reth_trie_db::DatabaseStateRoot;
|
||||
use std::{
|
||||
@@ -103,35 +97,31 @@ impl BlockchainTestCase {
|
||||
}
|
||||
|
||||
/// Execute a single `BlockchainTest`, validating the outcome against the
|
||||
/// expectations encoded in the JSON file. Returns the list of executed blocks
|
||||
/// with their execution witnesses.
|
||||
pub fn run_single_case(
|
||||
name: &str,
|
||||
case: &BlockchainTest,
|
||||
) -> Result<Vec<(RecoveredBlock<Block>, ExecutionWitness)>, Error> {
|
||||
/// expectations encoded in the JSON file.
|
||||
pub fn run_single_case(name: &str, case: &BlockchainTest) -> Result<(), Error> {
|
||||
let expectation = Self::expected_failure(case);
|
||||
match run_case(case) {
|
||||
// All blocks executed successfully.
|
||||
Ok(program_inputs) => {
|
||||
Ok(()) => {
|
||||
// Check if the test case specifies that it should have failed
|
||||
if let Some((block, msg)) = expectation {
|
||||
Err(Error::Assertion(format!(
|
||||
"Test case: {name}\nExpected failure at block {block} - {msg}, but all blocks succeeded",
|
||||
)))
|
||||
} else {
|
||||
Ok(program_inputs)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// A block processing failure occurred.
|
||||
Err(Error::BlockProcessingFailed { block_number, partial_program_inputs, err }) => {
|
||||
Err(Error::BlockProcessingFailed { block_number, err }) => {
|
||||
match expectation {
|
||||
// It happened on exactly the block we were told to fail on
|
||||
Some((expected, _)) if block_number == expected => Ok(partial_program_inputs),
|
||||
Some((expected, _)) if block_number == expected => Ok(()),
|
||||
|
||||
// Uncle side‑chain edge case, we accept as long as it failed.
|
||||
// But we don't check the exact block number.
|
||||
_ if Self::is_uncle_sidechain_case(name) => Ok(partial_program_inputs),
|
||||
_ if Self::is_uncle_sidechain_case(name) => Ok(()),
|
||||
|
||||
// Expected failure, but block number does not match
|
||||
Some((expected, _)) => Err(Error::Assertion(format!(
|
||||
@@ -139,7 +129,7 @@ impl BlockchainTestCase {
|
||||
))),
|
||||
|
||||
// No failure expected at all - bubble up original error.
|
||||
None => Err(Error::BlockProcessingFailed { block_number, partial_program_inputs, err }),
|
||||
None => Err(Error::BlockProcessingFailed { block_number, err }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,13 +188,9 @@ impl Case for BlockchainTestCase {
|
||||
/// outcome.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(_)` if all blocks execute successfully, returning recovered blocks and full block
|
||||
/// execution witness.
|
||||
/// - `Err(Error)` if any block fails to execute correctly, returning a partial block execution
|
||||
/// witness if the error is of variant `BlockProcessingFailed`.
|
||||
fn run_case(
|
||||
case: &BlockchainTest,
|
||||
) -> Result<Vec<(RecoveredBlock<Block>, ExecutionWitness)>, Error> {
|
||||
/// - `Ok(())` if all blocks execute successfully.
|
||||
/// - `Err(Error)` if any block fails to execute correctly.
|
||||
fn run_case(case: &BlockchainTest) -> Result<(), Error> {
|
||||
// Create a new test database and initialize a provider for the test case.
|
||||
let chain_spec = case.network.to_chain_spec();
|
||||
let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
|
||||
@@ -218,53 +204,43 @@ fn run_case(
|
||||
.try_recover()
|
||||
.unwrap();
|
||||
|
||||
provider
|
||||
.insert_block(&genesis_block)
|
||||
.map_err(|err| Error::block_failed(0, Default::default(), err))?;
|
||||
provider.insert_block(&genesis_block).map_err(|err| Error::block_failed(0, err))?;
|
||||
|
||||
// Increment block number for receipts static file
|
||||
provider
|
||||
.static_file_provider()
|
||||
.latest_writer(StaticFileSegment::Receipts)
|
||||
.and_then(|mut writer| writer.increment_block(0))
|
||||
.map_err(|err| Error::block_failed(0, Default::default(), err))?;
|
||||
.map_err(|err| Error::block_failed(0, err))?;
|
||||
|
||||
let genesis_state = case.pre.clone().into_genesis_state();
|
||||
insert_genesis_state(&provider, genesis_state.iter())
|
||||
.map_err(|err| Error::block_failed(0, Default::default(), err))?;
|
||||
.map_err(|err| Error::block_failed(0, err))?;
|
||||
insert_genesis_hashes(&provider, genesis_state.iter())
|
||||
.map_err(|err| Error::block_failed(0, Default::default(), err))?;
|
||||
.map_err(|err| Error::block_failed(0, err))?;
|
||||
insert_genesis_history(&provider, genesis_state.iter())
|
||||
.map_err(|err| Error::block_failed(0, Default::default(), err))?;
|
||||
.map_err(|err| Error::block_failed(0, err))?;
|
||||
|
||||
// Decode blocks
|
||||
let blocks = decode_blocks(&case.blocks)?;
|
||||
|
||||
let executor_provider = EthEvmConfig::ethereum(chain_spec.clone());
|
||||
let mut parent = genesis_block;
|
||||
let mut program_inputs = Vec::new();
|
||||
|
||||
for (block_index, block) in blocks.iter().enumerate() {
|
||||
// Note: same as the comment on `decode_blocks` as to why we cannot use block.number
|
||||
let block_number = (block_index + 1) as u64;
|
||||
|
||||
// Insert the block into the database
|
||||
provider
|
||||
.insert_block(block)
|
||||
.map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
|
||||
// Commit static files, so we can query the headers for stateless execution below
|
||||
provider.insert_block(block).map_err(|err| Error::block_failed(block_number, err))?;
|
||||
provider
|
||||
.static_file_provider()
|
||||
.commit()
|
||||
.map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Consensus checks before block execution
|
||||
pre_execution_checks(chain_spec.clone(), &parent, block).map_err(|err| {
|
||||
program_inputs.push((block.clone(), execution_witness_with_parent(&parent)));
|
||||
Error::block_failed(block_number, program_inputs.clone(), err)
|
||||
})?;
|
||||
|
||||
let mut witness_record = ExecutionWitnessRecord::default();
|
||||
pre_execution_checks(chain_spec.clone(), &parent, block)
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Execute the block
|
||||
let state_provider = provider.latest();
|
||||
@@ -272,41 +248,12 @@ fn run_case(
|
||||
let executor = executor_provider.batch_executor(state_db);
|
||||
|
||||
let output = executor
|
||||
.execute_with_state_closure_always(&(*block).clone(), |statedb: &State<_>| {
|
||||
witness_record.record_executed_state(statedb);
|
||||
})
|
||||
.map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
|
||||
.execute(&(*block).clone())
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Consensus checks after block execution
|
||||
validate_block_post_execution(block, &chain_spec, &output.receipts, &output.requests, None)
|
||||
.map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
|
||||
|
||||
// Generate the stateless witness
|
||||
// TODO: Most of this code is copy-pasted from debug_executionWitness
|
||||
let ExecutionWitnessRecord { hashed_state, codes, keys, lowest_block_number } =
|
||||
witness_record;
|
||||
let state = state_provider.witness(Default::default(), hashed_state)?;
|
||||
let mut exec_witness = ExecutionWitness { state, codes, keys, headers: Default::default() };
|
||||
|
||||
let smallest = lowest_block_number.unwrap_or_else(|| {
|
||||
// Return only the parent header, if there were no calls to the
|
||||
// BLOCKHASH opcode.
|
||||
block_number.saturating_sub(1)
|
||||
});
|
||||
|
||||
let range = smallest..block_number;
|
||||
|
||||
exec_witness.headers = provider
|
||||
.headers_range(range)?
|
||||
.into_iter()
|
||||
.map(|header| {
|
||||
let mut serialized_header = Vec::new();
|
||||
header.encode(&mut serialized_header);
|
||||
serialized_header.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
program_inputs.push((block.clone(), exec_witness));
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Compute and check the post state root
|
||||
let hashed_state =
|
||||
@@ -315,11 +262,10 @@ fn run_case(
|
||||
provider.tx_ref(),
|
||||
&hashed_state.clone_into_sorted(),
|
||||
)
|
||||
.map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
if computed_state_root != block.state_root {
|
||||
return Err(Error::block_failed(
|
||||
block_number,
|
||||
program_inputs.clone(),
|
||||
Error::Assertion("state root mismatch".to_string()),
|
||||
));
|
||||
}
|
||||
@@ -331,14 +277,14 @@ fn run_case(
|
||||
OriginalValuesKnown::Yes,
|
||||
StateWriteConfig::default(),
|
||||
)
|
||||
.map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
provider
|
||||
.write_hashed_state(&hashed_state.into_sorted())
|
||||
.map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
provider
|
||||
.update_history_indices(block.number..=block.number)
|
||||
.map_err(|err| Error::block_failed(block_number, program_inputs.clone(), err))?;
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Since there were no errors, update the parent block
|
||||
parent = block.clone()
|
||||
@@ -365,25 +311,7 @@ fn run_case(
|
||||
}
|
||||
}
|
||||
|
||||
// Now validate using the stateless client if everything else passes
|
||||
for (recovered_block, execution_witness) in &program_inputs {
|
||||
let block = recovered_block.clone().into_block();
|
||||
|
||||
// Recover the actual public keys from the transaction signatures
|
||||
let public_keys = recover_signers(block.body().transactions())
|
||||
.expect("Failed to recover public keys from transaction signatures");
|
||||
|
||||
stateless_validation_with_trie::<StatelessSparseTrie, _, _>(
|
||||
block,
|
||||
public_keys,
|
||||
execution_witness.clone(),
|
||||
chain_spec.clone(),
|
||||
EthEvmConfig::new(chain_spec.clone()),
|
||||
)
|
||||
.expect("stateless validation failed");
|
||||
}
|
||||
|
||||
Ok(program_inputs)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_blocks(
|
||||
@@ -396,12 +324,10 @@ fn decode_blocks(
|
||||
let block_number = (block_index + 1) as u64;
|
||||
|
||||
let decoded = SealedBlock::<Block>::decode(&mut block.rlp.as_ref())
|
||||
.map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
let recovered_block = decoded
|
||||
.clone()
|
||||
.try_recover()
|
||||
.map_err(|err| Error::block_failed(block_number, Default::default(), err))?;
|
||||
let recovered_block =
|
||||
decoded.clone().try_recover().map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
blocks.push(recovered_block);
|
||||
}
|
||||
@@ -430,26 +356,6 @@ fn pre_execution_checks(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recover public keys from transaction signatures.
|
||||
fn recover_signers<'a, I>(txs: I) -> Result<Vec<UncompressedPublicKey>, Box<dyn std::error::Error>>
|
||||
where
|
||||
I: IntoIterator<Item = &'a TransactionSigned>,
|
||||
{
|
||||
txs.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, tx)| {
|
||||
tx.signature()
|
||||
.recover_from_prehash(&tx.signature_hash())
|
||||
.map(|keys| {
|
||||
UncompressedPublicKey(
|
||||
keys.to_encoded_point(false).as_bytes().try_into().unwrap(),
|
||||
)
|
||||
})
|
||||
.map_err(|e| format!("failed to recover signature for tx #{i}: {e}").into())
|
||||
})
|
||||
.collect::<Result<Vec<UncompressedPublicKey>, _>>()
|
||||
}
|
||||
|
||||
/// Returns whether the test at the given path should be skipped.
|
||||
///
|
||||
/// Some tests are edge cases that cannot happen on mainnet, while others are skipped for
|
||||
@@ -516,9 +422,3 @@ fn path_contains(path_str: &str, rhs: &[&str]) -> bool {
|
||||
let rhs = rhs.join(std::path::MAIN_SEPARATOR_STR);
|
||||
path_str.contains(&rhs)
|
||||
}
|
||||
|
||||
fn execution_witness_with_parent(parent: &RecoveredBlock<Block>) -> ExecutionWitness {
|
||||
let mut serialized_header = Vec::new();
|
||||
parent.header().encode(&mut serialized_header);
|
||||
ExecutionWitness { headers: vec![serialized_header.into()], ..Default::default() }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
//! Test results and errors
|
||||
|
||||
use reth_db::DatabaseError;
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_primitives_traits::RecoveredBlock;
|
||||
use reth_provider::ProviderError;
|
||||
use reth_stateless::ExecutionWitness;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -26,9 +23,6 @@ pub enum Error {
|
||||
BlockProcessingFailed {
|
||||
/// The block number for the block that failed
|
||||
block_number: u64,
|
||||
/// Contains the inputs necessary for the block stateless validation guest program used in
|
||||
/// zkVMs to prove the block is invalid.
|
||||
partial_program_inputs: Vec<(RecoveredBlock<Block>, ExecutionWitness)>,
|
||||
/// The specific error
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync>,
|
||||
@@ -72,10 +66,9 @@ impl Error {
|
||||
/// Create a new [`Error::BlockProcessingFailed`] error.
|
||||
pub fn block_failed(
|
||||
block_number: u64,
|
||||
partial_program_inputs: Vec<(RecoveredBlock<Block>, ExecutionWitness)>,
|
||||
err: impl std::error::Error + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self::BlockProcessingFailed { block_number, partial_program_inputs, err: Box::new(err) }
|
||||
Self::BlockProcessingFailed { block_number, err: Box::new(err) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user