diff --git a/Cargo.toml b/Cargo.toml index aeb2bb8f..788dc307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ alloy-op-evm = { version = "0.25.0", path = "crates/op-evm", default-features = # alloy alloy-eip2124 = { version = "0.2", default-features = false } +alloy-eip7928 = { git = "https://github.com/alloy-rs/eips/", default-features = false } alloy-chains = { version = "0.2.0", default-features = false } alloy-eips = { version = "1.0.34", default-features = false } alloy-consensus = { version = "1.0.27", default-features = false } diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 27230925..907dc86f 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -18,6 +18,7 @@ alloy-consensus = { workspace = true, features = ["k256"] } alloy-primitives.workspace = true alloy-sol-types.workspace = true alloy-eips.workspace = true +alloy-eip7928.workspace = true alloy-hardforks.workspace = true alloy-op-hardforks = { workspace = true, optional = true } alloy-rpc-types-eth = { workspace = true, optional = true } diff --git a/crates/evm/src/block/bal.rs b/crates/evm/src/block/bal.rs new file mode 100644 index 00000000..76ee70a5 --- /dev/null +++ b/crates/evm/src/block/bal.rs @@ -0,0 +1,137 @@ +//! Helper types for [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928#asynchronous-validation) aware execution. + +use alloy_eip7928::AccountChanges; +use alloy_primitives::{Address, B256}; +use alloy_primitives::map::{HashMap, HashSet}; +use revm::bytecode::Bytecode; +use revm::Database; +use revm::primitives::{StorageKey, StorageValue}; +use revm::state::AccountInfo; +use crate::block::BlockExecutionError; + +/// A Block level access list aware database. +pub trait BalDb { + + /// Sets the index of the transaction that's being executed. + fn set_transaction_index(&mut self, index: usize); + +} + +/// A block level accesslist aware database/state implementation that is diff aware. +/// +/// This type is intended to be used for block level execution. +#[derive(Debug)] +pub struct BalState { + /// The underlying database used to fetch reads from. + inner: DB, + /// The block level + bal : IndexedBal, + /// Tracks the index of the transaction that is currently being executed. + current_tx_index: usize, +} + +impl BalState { + + /// Marks the execution as completed + fn finish(&mut self) { + // TODO: do we need this? + } + +} + +impl BalDb for BalState { + fn set_transaction_index(&mut self, index: usize) { + self.current_tx_index = index; + } +} + +impl Database for BalState +where DB: Database +{ + type Error = ::Error; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + todo!() + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + todo!() + } + + fn storage(&mut self, address: Address, index: StorageKey) -> Result { + todo!() + } + + fn block_hash(&mut self, number: u64) -> Result { + todo!() + } +} + + +/// A transaction indexed representation of EIP-7928 accesslist. +/// TODO: Arc this +#[derive(Debug, Clone)] +pub struct IndexedBal { + /// All accounts grouped by their address. + accounts: HashMap +} + +impl IndexedBal { + + /// Creates a new instance from the given list of account changes + pub fn new(bal: Vec) -> Self { + Self { + accounts: bal.into_iter().map(|change|(change.address, change)).collect(), + } + } + + /// Returns the account + fn account_at(&self, address: &Address, idx: usize) -> Option { + None + } + + +} + + +// /// Required context for executing a BAL transaction. +// #[derive(Debug)] +// pub struct BalTxContext { +// /// The mandatory input for this transaction. +// /// +// /// This is derived from the state changes in previous transactions in the block. +// pub input: TxInput, +// /// The expected output set for this specific transaction +// pub output: BalTxExpectedOutput, +// } +// +// /// The exection output of a BAL aware transaction +// #[derive(Debug)] +// pub struct BalTxOutput {} +// +// /// The transaction specific input set. +// /// +// /// This is similar to state overrides that must be applied before executing the transaction. +// /// For example any account/storage changes from previous transactions in the block. +// #[derive(Debug)] +// pub struct TxInput {} + +// /// The expected changes performed by the transaction. +// /// +// /// This is effectively the write portion of the [`EvmState`] in the execution result and must be +// /// complete match. +// #[derive(Debug)] +// pub struct BalTxExpectedOutput {} + +/// A BAL aware transaction failed to execute +#[derive(Debug)] +pub enum BalExecutionError { + /// Output of the transaction invalidated the BAL input. + BalDiff(BalDiffError), + /// Executing the transaction failed. + Execution(BlockExecutionError), +} + +/// A BAL aware transaction execution resulted in an invalid BAL. +#[derive(Debug)] +pub enum BalDiffError {} diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index a6f4394c..0ef6f683 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -10,6 +10,8 @@ use revm::{ Inspector, }; +mod bal; +pub use bal::*; mod error; pub use error::*; @@ -92,6 +94,56 @@ impl CommitChanges { /// /// The output of [`BlockExecutor::finish`] is a [`BlockExecutionResult`] which contains all /// relevant information about the block execution. +/// +/// ## Conflict free parallel execution +/// +/// With [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928) Execution can be fully parallelized if the BAL is known in advance. +/// The BAL must still be validated by checking if the BAL recorded during validation matches the +/// input: +/// > The BAL MUST be complete and accurate. Missing or spurious entries invalidate the block. +/// +/// ### Transaction independence +/// +/// The BAL tracks all touched accounts with its accessed storage (read+write) and changes to the +/// account itself (balance,nonce,code). All changes (storage changes, balance, nonce, code) are +/// assigned a tx index (the tx in the block that caused the change). From this input we can make +/// transaction execution operate on: +/// - dependent inputs: +/// * Any accessed account data changed in previous transactions in the block +/// - expected outputs: +/// * The changes recorded for this transaction in the BAL +/// +/// Validation must then ensure that: +/// - The state output matches the storage input: +/// * All accessed, but unchanged slots in the _output_ set appear in the provided input set. +/// * All storage writes provided in the input match the output +/// * All account modifications in the input match the output +/// +/// If any of those mismatch then the input BAL is invalid and the block is invalid. +/// +/// Storage read caveat: +/// Account level storage reads aren't indexed by transactions, the view of storage views is however +/// transaction specific. In total validation must ensure that _all_ reads are present in the block, +/// this condition can only be validation after executing all transactions in the block. +/// +/// ### Deriving transaction level input +/// +/// The EIP-7928 BAL tracks account level access, all writes are assigned an index (the index of the +/// transaction in the block). Starting from the parent state, the input and output set can be +/// derived from the BAL by sorting and grouping changes for transactions. Each transaction has a +/// unique input and output set, which is derived from the input and output set of previous changes +/// in the block. +/// Because EIP-7928 BAL is just a list of `Vec` where each `AccountChange` includes +/// indexed changes, we get the transaction specific [`TxInput`] by folding the account changeset +/// with the transaction indexes. +/// +/// ### BAL execution/validation abstraction +/// +/// If the BAL is valid (transaction specific input and output sets are correct) then transaction +/// changes don't need to be committed across transactions. Parallel execution can then be achieved +/// by using multiple instances of the [`BlockExecutor`], like a pool of executors. Hence this trait +/// does only provide BAL abstractions for executing single transactions with the transaction +/// specific input sets. pub trait BlockExecutor { /// Input transaction type. /// @@ -233,6 +285,36 @@ pub trait BlockExecutor { tx: impl ExecutableTx, ) -> Result::HaltReason>, BlockExecutionError>; + /// Executes a single transaction using the transaction specific context without committing + /// state changes. + /// + /// This method uses the BAL context's input for applying state changes that happened in + /// previous transactions in the block. + /// + /// This returns an error if the transaction's output deviates from the expected output. + /// + /// Returns a [`revm::context_interface::result::ResultAndState`] containing the execution + /// result and state changes. + /// TODO: we need the same functions for pre and post sections. + fn execute_transaction_with_index( + &self, + tx: impl ExecutableTx, + index: usize, + ) -> Result::HaltReason>, BalExecutionError> + where ::DB: BalDb + { + // TODO: we need a away to effectively apply state overrides for previous state changes + // because we don't know the tx specific reads, all state changes carry over, so ideally we + // don't need to apply them for _every_ transaction and instead only apply the new ones, for + // example if this executor was used first to execute tx 5, we need to apply changes from tx + // 0..=4 before, if its then reused to execute tx 9 (because 6,7,8 are executed by different + // block executors in the pool), then we only want to apply missing changes from 5..=8 and + // should reuse changes applied before executing tx5 execute the transaction and + // compare against the expected output for this tx, the BAL context is invalid if + // the expected output is not an exact match. + todo!() + } + /// Commits a previously executed transaction's state changes. /// /// Takes the output from