mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
525 lines
20 KiB
Rust
525 lines
20 KiB
Rust
//! Optimism block execution strategy.
|
||
|
||
use crate::{l1::ensure_create2_deployer, OpBlockExecutionError, OpEvmConfig};
|
||
use alloc::{boxed::Box, sync::Arc, vec::Vec};
|
||
use alloy_consensus::{Header, Transaction as _};
|
||
use alloy_eips::eip7685::Requests;
|
||
use core::fmt::Display;
|
||
use op_alloy_consensus::DepositTransaction;
|
||
use reth_chainspec::EthereumHardforks;
|
||
use reth_consensus::ConsensusError;
|
||
use reth_evm::{
|
||
execute::{
|
||
BasicBlockExecutorProvider, BlockExecutionError, BlockExecutionStrategy,
|
||
BlockExecutionStrategyFactory, BlockValidationError, ExecuteOutput, ProviderError,
|
||
},
|
||
state_change::post_block_balance_increments,
|
||
system_calls::{OnStateHook, SystemCaller},
|
||
ConfigureEvm, TxEnvOverrides,
|
||
};
|
||
use reth_optimism_chainspec::OpChainSpec;
|
||
use reth_optimism_consensus::validate_block_post_execution;
|
||
use reth_optimism_forks::OpHardfork;
|
||
use reth_optimism_primitives::OpPrimitives;
|
||
use reth_primitives::{BlockWithSenders, Receipt, TxType};
|
||
use reth_revm::{Database, State};
|
||
use revm_primitives::{db::DatabaseCommit, EnvWithHandlerCfg, ResultAndState, U256};
|
||
use tracing::trace;
|
||
|
||
/// Factory for [`OpExecutionStrategy`].
|
||
#[derive(Debug, Clone)]
|
||
pub struct OpExecutionStrategyFactory<EvmConfig = OpEvmConfig> {
|
||
/// The chainspec
|
||
chain_spec: Arc<OpChainSpec>,
|
||
/// How to create an EVM.
|
||
evm_config: EvmConfig,
|
||
}
|
||
|
||
impl OpExecutionStrategyFactory {
|
||
/// Creates a new default optimism executor strategy factory.
|
||
pub fn optimism(chain_spec: Arc<OpChainSpec>) -> Self {
|
||
Self::new(chain_spec.clone(), OpEvmConfig::new(chain_spec))
|
||
}
|
||
}
|
||
|
||
impl<EvmConfig> OpExecutionStrategyFactory<EvmConfig> {
|
||
/// Creates a new executor strategy factory.
|
||
pub const fn new(chain_spec: Arc<OpChainSpec>, evm_config: EvmConfig) -> Self {
|
||
Self { chain_spec, evm_config }
|
||
}
|
||
}
|
||
|
||
impl<EvmConfig> BlockExecutionStrategyFactory for OpExecutionStrategyFactory<EvmConfig>
|
||
where
|
||
EvmConfig:
|
||
Clone + Unpin + Sync + Send + 'static + ConfigureEvm<Header = alloy_consensus::Header>,
|
||
{
|
||
type Primitives = OpPrimitives;
|
||
type Strategy<DB: Database<Error: Into<ProviderError> + Display>> =
|
||
OpExecutionStrategy<DB, EvmConfig>;
|
||
|
||
fn create_strategy<DB>(&self, db: DB) -> Self::Strategy<DB>
|
||
where
|
||
DB: Database<Error: Into<ProviderError> + Display>,
|
||
{
|
||
let state =
|
||
State::builder().with_database(db).with_bundle_update().without_state_clear().build();
|
||
OpExecutionStrategy::new(state, self.chain_spec.clone(), self.evm_config.clone())
|
||
}
|
||
}
|
||
|
||
/// Block execution strategy for Optimism.
|
||
#[allow(missing_debug_implementations)]
|
||
pub struct OpExecutionStrategy<DB, EvmConfig>
|
||
where
|
||
EvmConfig: Clone,
|
||
{
|
||
/// The chainspec
|
||
chain_spec: Arc<OpChainSpec>,
|
||
/// How to create an EVM.
|
||
evm_config: EvmConfig,
|
||
/// Optional overrides for the transactions environment.
|
||
tx_env_overrides: Option<Box<dyn TxEnvOverrides>>,
|
||
/// Current state for block execution.
|
||
state: State<DB>,
|
||
/// Utility to call system smart contracts.
|
||
system_caller: SystemCaller<EvmConfig, OpChainSpec>,
|
||
}
|
||
|
||
impl<DB, EvmConfig> OpExecutionStrategy<DB, EvmConfig>
|
||
where
|
||
EvmConfig: Clone,
|
||
{
|
||
/// Creates a new [`OpExecutionStrategy`]
|
||
pub fn new(state: State<DB>, chain_spec: Arc<OpChainSpec>, evm_config: EvmConfig) -> Self {
|
||
let system_caller = SystemCaller::new(evm_config.clone(), chain_spec.clone());
|
||
Self { state, chain_spec, evm_config, system_caller, tx_env_overrides: None }
|
||
}
|
||
}
|
||
|
||
impl<DB, EvmConfig> OpExecutionStrategy<DB, EvmConfig>
|
||
where
|
||
DB: Database<Error: Into<ProviderError> + Display>,
|
||
EvmConfig: ConfigureEvm<Header = alloy_consensus::Header>,
|
||
{
|
||
/// Configures a new evm configuration and block environment for the given block.
|
||
///
|
||
/// Caution: this does not initialize the tx environment.
|
||
fn evm_env_for_block(&self, header: &Header, total_difficulty: U256) -> EnvWithHandlerCfg {
|
||
let (cfg, block_env) = self.evm_config.cfg_and_block_env(header, total_difficulty);
|
||
EnvWithHandlerCfg::new_with_cfg_env(cfg, block_env, Default::default())
|
||
}
|
||
}
|
||
|
||
impl<DB, EvmConfig> BlockExecutionStrategy for OpExecutionStrategy<DB, EvmConfig>
|
||
where
|
||
DB: Database<Error: Into<ProviderError> + Display>,
|
||
EvmConfig: ConfigureEvm<Header = alloy_consensus::Header>,
|
||
{
|
||
type DB = DB;
|
||
type Primitives = OpPrimitives;
|
||
type Error = BlockExecutionError;
|
||
|
||
fn init(&mut self, tx_env_overrides: Box<dyn TxEnvOverrides>) {
|
||
self.tx_env_overrides = Some(tx_env_overrides);
|
||
}
|
||
|
||
fn apply_pre_execution_changes(
|
||
&mut self,
|
||
block: &BlockWithSenders,
|
||
total_difficulty: U256,
|
||
) -> Result<(), Self::Error> {
|
||
// Set state clear flag if the block is after the Spurious Dragon hardfork.
|
||
let state_clear_flag =
|
||
(*self.chain_spec).is_spurious_dragon_active_at_block(block.header.number);
|
||
self.state.set_state_clear_flag(state_clear_flag);
|
||
|
||
let env = self.evm_env_for_block(&block.header, total_difficulty);
|
||
let mut evm = self.evm_config.evm_with_env(&mut self.state, env);
|
||
|
||
self.system_caller.apply_beacon_root_contract_call(
|
||
block.timestamp,
|
||
block.number,
|
||
block.parent_beacon_block_root,
|
||
&mut evm,
|
||
)?;
|
||
|
||
// Ensure that the create2deployer is force-deployed at the canyon transition. Optimism
|
||
// blocks will always have at least a single transaction in them (the L1 info transaction),
|
||
// so we can safely assume that this will always be triggered upon the transition and that
|
||
// the above check for empty blocks will never be hit on OP chains.
|
||
ensure_create2_deployer(self.chain_spec.clone(), block.timestamp, evm.db_mut())
|
||
.map_err(|_| OpBlockExecutionError::ForceCreate2DeployerFail)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn execute_transactions(
|
||
&mut self,
|
||
block: &BlockWithSenders,
|
||
total_difficulty: U256,
|
||
) -> Result<ExecuteOutput, Self::Error> {
|
||
let env = self.evm_env_for_block(&block.header, total_difficulty);
|
||
let mut evm = self.evm_config.evm_with_env(&mut self.state, env);
|
||
|
||
let is_regolith =
|
||
self.chain_spec.fork(OpHardfork::Regolith).active_at_timestamp(block.timestamp);
|
||
|
||
let mut cumulative_gas_used = 0;
|
||
let mut receipts = Vec::with_capacity(block.body.transactions.len());
|
||
for (sender, transaction) in block.transactions_with_sender() {
|
||
// The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior,
|
||
// must be no greater than the block’s gasLimit.
|
||
let block_available_gas = block.header.gas_limit - cumulative_gas_used;
|
||
if transaction.gas_limit() > block_available_gas &&
|
||
(is_regolith || !transaction.is_system_transaction())
|
||
{
|
||
return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
|
||
transaction_gas_limit: transaction.gas_limit(),
|
||
block_available_gas,
|
||
}
|
||
.into())
|
||
}
|
||
|
||
// An optimism block should never contain blob transactions.
|
||
if matches!(transaction.tx_type(), TxType::Eip4844) {
|
||
return Err(OpBlockExecutionError::BlobTransactionRejected.into())
|
||
}
|
||
|
||
// Cache the depositor account prior to the state transition for the deposit nonce.
|
||
//
|
||
// Note that this *only* needs to be done post-regolith hardfork, as deposit nonces
|
||
// were not introduced in Bedrock. In addition, regular transactions don't have deposit
|
||
// nonces, so we don't need to touch the DB for those.
|
||
let depositor = (is_regolith && transaction.is_deposit())
|
||
.then(|| {
|
||
evm.db_mut()
|
||
.load_cache_account(*sender)
|
||
.map(|acc| acc.account_info().unwrap_or_default())
|
||
})
|
||
.transpose()
|
||
.map_err(|_| OpBlockExecutionError::AccountLoadFailed(*sender))?;
|
||
|
||
self.evm_config.fill_tx_env(evm.tx_mut(), transaction, *sender);
|
||
|
||
if let Some(tx_env_overrides) = &mut self.tx_env_overrides {
|
||
tx_env_overrides.apply(evm.tx_mut());
|
||
}
|
||
|
||
// Execute transaction.
|
||
let result_and_state = evm.transact().map_err(move |err| {
|
||
let new_err = err.map_db_err(|e| e.into());
|
||
// Ensure hash is calculated for error log, if not already done
|
||
BlockValidationError::EVM {
|
||
hash: transaction.recalculate_hash(),
|
||
error: Box::new(new_err),
|
||
}
|
||
})?;
|
||
|
||
trace!(
|
||
target: "evm",
|
||
?transaction,
|
||
"Executed transaction"
|
||
);
|
||
self.system_caller.on_state(&result_and_state);
|
||
let ResultAndState { result, state } = result_and_state;
|
||
evm.db_mut().commit(state);
|
||
|
||
// append gas used
|
||
cumulative_gas_used += result.gas_used();
|
||
|
||
// Push transaction changeset and calculate header bloom filter for receipt.
|
||
receipts.push(Receipt {
|
||
tx_type: transaction.tx_type(),
|
||
// Success flag was added in `EIP-658: Embedding transaction status code in
|
||
// receipts`.
|
||
success: result.is_success(),
|
||
cumulative_gas_used,
|
||
logs: result.into_logs(),
|
||
deposit_nonce: depositor.map(|account| account.nonce),
|
||
// The deposit receipt version was introduced in Canyon to indicate an update to how
|
||
// receipt hashes should be computed when set. The state transition process ensures
|
||
// this is only set for post-Canyon deposit transactions.
|
||
deposit_receipt_version: (transaction.is_deposit() &&
|
||
self.chain_spec
|
||
.is_fork_active_at_timestamp(OpHardfork::Canyon, block.timestamp))
|
||
.then_some(1),
|
||
});
|
||
}
|
||
|
||
Ok(ExecuteOutput { receipts, gas_used: cumulative_gas_used })
|
||
}
|
||
|
||
fn apply_post_execution_changes(
|
||
&mut self,
|
||
block: &BlockWithSenders,
|
||
total_difficulty: U256,
|
||
_receipts: &[Receipt],
|
||
) -> Result<Requests, Self::Error> {
|
||
let balance_increments =
|
||
post_block_balance_increments(&self.chain_spec.clone(), block, total_difficulty);
|
||
// increment balances
|
||
self.state
|
||
.increment_balances(balance_increments)
|
||
.map_err(|_| BlockValidationError::IncrementBalanceFailed)?;
|
||
|
||
Ok(Requests::default())
|
||
}
|
||
|
||
fn state_ref(&self) -> &State<DB> {
|
||
&self.state
|
||
}
|
||
|
||
fn state_mut(&mut self) -> &mut State<DB> {
|
||
&mut self.state
|
||
}
|
||
|
||
fn with_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
|
||
self.system_caller.with_state_hook(hook);
|
||
}
|
||
|
||
fn validate_block_post_execution(
|
||
&self,
|
||
block: &BlockWithSenders,
|
||
receipts: &[Receipt],
|
||
_requests: &Requests,
|
||
) -> Result<(), ConsensusError> {
|
||
validate_block_post_execution(block, &self.chain_spec.clone(), receipts)
|
||
}
|
||
}
|
||
|
||
/// Helper type with backwards compatible methods to obtain executor providers.
|
||
#[derive(Debug)]
|
||
pub struct OpExecutorProvider;
|
||
|
||
impl OpExecutorProvider {
|
||
/// Creates a new default optimism executor strategy factory.
|
||
pub fn optimism(
|
||
chain_spec: Arc<OpChainSpec>,
|
||
) -> BasicBlockExecutorProvider<OpExecutionStrategyFactory> {
|
||
BasicBlockExecutorProvider::new(OpExecutionStrategyFactory::optimism(chain_spec))
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::OpChainSpec;
|
||
use alloy_consensus::TxEip1559;
|
||
use alloy_primitives::{
|
||
b256, Address, PrimitiveSignature as Signature, StorageKey, StorageValue,
|
||
};
|
||
use op_alloy_consensus::TxDeposit;
|
||
use reth_chainspec::MIN_TRANSACTION_GAS;
|
||
use reth_evm::execute::{BasicBlockExecutorProvider, BatchExecutor, BlockExecutorProvider};
|
||
use reth_optimism_chainspec::OpChainSpecBuilder;
|
||
use reth_primitives::{Account, Block, BlockBody, Transaction, TransactionSigned};
|
||
use reth_revm::{
|
||
database::StateProviderDatabase, test_utils::StateProviderTest, L1_BLOCK_CONTRACT,
|
||
};
|
||
use std::{collections::HashMap, str::FromStr};
|
||
|
||
fn create_op_state_provider() -> StateProviderTest {
|
||
let mut db = StateProviderTest::default();
|
||
|
||
let l1_block_contract_account =
|
||
Account { balance: U256::ZERO, bytecode_hash: None, nonce: 1 };
|
||
|
||
let mut l1_block_storage = HashMap::default();
|
||
// base fee
|
||
l1_block_storage.insert(StorageKey::with_last_byte(1), StorageValue::from(1000000000));
|
||
// l1 fee overhead
|
||
l1_block_storage.insert(StorageKey::with_last_byte(5), StorageValue::from(188));
|
||
// l1 fee scalar
|
||
l1_block_storage.insert(StorageKey::with_last_byte(6), StorageValue::from(684000));
|
||
// l1 free scalars post ecotone
|
||
l1_block_storage.insert(
|
||
StorageKey::with_last_byte(3),
|
||
StorageValue::from_str(
|
||
"0x0000000000000000000000000000000000001db0000d27300000000000000005",
|
||
)
|
||
.unwrap(),
|
||
);
|
||
|
||
db.insert_account(L1_BLOCK_CONTRACT, l1_block_contract_account, None, l1_block_storage);
|
||
|
||
db
|
||
}
|
||
|
||
fn executor_provider(
|
||
chain_spec: Arc<OpChainSpec>,
|
||
) -> BasicBlockExecutorProvider<OpExecutionStrategyFactory> {
|
||
let strategy_factory =
|
||
OpExecutionStrategyFactory::new(chain_spec.clone(), OpEvmConfig::new(chain_spec));
|
||
|
||
BasicBlockExecutorProvider::new(strategy_factory)
|
||
}
|
||
|
||
#[test]
|
||
fn op_deposit_fields_pre_canyon() {
|
||
let header = Header {
|
||
timestamp: 1,
|
||
number: 1,
|
||
gas_limit: 1_000_000,
|
||
gas_used: 42_000,
|
||
receipts_root: b256!(
|
||
"83465d1e7d01578c0d609be33570f91242f013e9e295b0879905346abbd63731"
|
||
),
|
||
..Default::default()
|
||
};
|
||
|
||
let mut db = create_op_state_provider();
|
||
|
||
let addr = Address::ZERO;
|
||
let account = Account { balance: U256::MAX, ..Account::default() };
|
||
db.insert_account(addr, account, None, HashMap::default());
|
||
|
||
let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().regolith_activated().build());
|
||
|
||
let tx = TransactionSigned::new_unhashed(
|
||
Transaction::Eip1559(TxEip1559 {
|
||
chain_id: chain_spec.chain.id(),
|
||
nonce: 0,
|
||
gas_limit: MIN_TRANSACTION_GAS,
|
||
to: addr.into(),
|
||
..Default::default()
|
||
}),
|
||
Signature::test_signature(),
|
||
);
|
||
|
||
let tx_deposit = TransactionSigned::new_unhashed(
|
||
Transaction::Deposit(op_alloy_consensus::TxDeposit {
|
||
from: addr,
|
||
to: addr.into(),
|
||
gas_limit: MIN_TRANSACTION_GAS,
|
||
..Default::default()
|
||
}),
|
||
Signature::test_signature(),
|
||
);
|
||
|
||
let provider = executor_provider(chain_spec);
|
||
let mut executor = provider.batch_executor(StateProviderDatabase::new(&db));
|
||
|
||
// make sure the L1 block contract state is preloaded.
|
||
executor.with_state_mut(|state| {
|
||
state.load_cache_account(L1_BLOCK_CONTRACT).unwrap();
|
||
});
|
||
|
||
// Attempt to execute a block with one deposit and one non-deposit transaction
|
||
executor
|
||
.execute_and_verify_one(
|
||
(
|
||
&BlockWithSenders {
|
||
block: Block {
|
||
header,
|
||
body: BlockBody {
|
||
transactions: vec![tx, tx_deposit],
|
||
..Default::default()
|
||
},
|
||
},
|
||
senders: vec![addr, addr],
|
||
},
|
||
U256::ZERO,
|
||
)
|
||
.into(),
|
||
)
|
||
.unwrap();
|
||
|
||
let receipts = executor.receipts();
|
||
let tx_receipt = receipts[0][0].as_ref().unwrap();
|
||
let deposit_receipt = receipts[0][1].as_ref().unwrap();
|
||
|
||
// deposit_receipt_version is not present in pre canyon transactions
|
||
assert!(deposit_receipt.deposit_receipt_version.is_none());
|
||
assert!(tx_receipt.deposit_receipt_version.is_none());
|
||
|
||
// deposit_nonce is present only in deposit transactions
|
||
assert!(deposit_receipt.deposit_nonce.is_some());
|
||
assert!(tx_receipt.deposit_nonce.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn op_deposit_fields_post_canyon() {
|
||
// ensure_create2_deployer will fail if timestamp is set to less then 2
|
||
let header = Header {
|
||
timestamp: 2,
|
||
number: 1,
|
||
gas_limit: 1_000_000,
|
||
gas_used: 42_000,
|
||
receipts_root: b256!(
|
||
"fffc85c4004fd03c7bfbe5491fae98a7473126c099ac11e8286fd0013f15f908"
|
||
),
|
||
..Default::default()
|
||
};
|
||
|
||
let mut db = create_op_state_provider();
|
||
let addr = Address::ZERO;
|
||
let account = Account { balance: U256::MAX, ..Account::default() };
|
||
|
||
db.insert_account(addr, account, None, HashMap::default());
|
||
|
||
let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().canyon_activated().build());
|
||
|
||
let tx = TransactionSigned::new_unhashed(
|
||
Transaction::Eip1559(TxEip1559 {
|
||
chain_id: chain_spec.chain.id(),
|
||
nonce: 0,
|
||
gas_limit: MIN_TRANSACTION_GAS,
|
||
to: addr.into(),
|
||
..Default::default()
|
||
}),
|
||
Signature::test_signature(),
|
||
);
|
||
|
||
let tx_deposit = TransactionSigned::new_unhashed(
|
||
Transaction::Deposit(op_alloy_consensus::TxDeposit {
|
||
from: addr,
|
||
to: addr.into(),
|
||
gas_limit: MIN_TRANSACTION_GAS,
|
||
..Default::default()
|
||
}),
|
||
TxDeposit::signature(),
|
||
);
|
||
|
||
let provider = executor_provider(chain_spec);
|
||
let mut executor = provider.batch_executor(StateProviderDatabase::new(&db));
|
||
|
||
// make sure the L1 block contract state is preloaded.
|
||
executor.with_state_mut(|state| {
|
||
state.load_cache_account(L1_BLOCK_CONTRACT).unwrap();
|
||
});
|
||
|
||
// attempt to execute an empty block with parent beacon block root, this should not fail
|
||
executor
|
||
.execute_and_verify_one(
|
||
(
|
||
&BlockWithSenders {
|
||
block: Block {
|
||
header,
|
||
body: BlockBody {
|
||
transactions: vec![tx, tx_deposit],
|
||
..Default::default()
|
||
},
|
||
},
|
||
senders: vec![addr, addr],
|
||
},
|
||
U256::ZERO,
|
||
)
|
||
.into(),
|
||
)
|
||
.expect("Executing a block while canyon is active should not fail");
|
||
|
||
let receipts = executor.receipts();
|
||
let tx_receipt = receipts[0][0].as_ref().unwrap();
|
||
let deposit_receipt = receipts[0][1].as_ref().unwrap();
|
||
|
||
// deposit_receipt_version is set to 1 for post canyon deposit transactions
|
||
assert_eq!(deposit_receipt.deposit_receipt_version, Some(1));
|
||
assert!(tx_receipt.deposit_receipt_version.is_none());
|
||
|
||
// deposit_nonce is present only in deposit transactions
|
||
assert!(deposit_receipt.deposit_nonce.is_some());
|
||
assert!(tx_receipt.deposit_nonce.is_none());
|
||
}
|
||
}
|