feat: op-reth (#4377)

Co-authored-by: Roberto Bayardo <bayardo@alum.mit.edu>
Co-authored-by: refcell.eth <abigger87@gmail.com>
Co-authored-by: Roman Krasiuk <rokrassyuk@gmail.com>
Co-authored-by: refcell <refcell@oplabs.co>
Co-authored-by: nicolas <48695862+merklefruit@users.noreply.github.com>
This commit is contained in:
clabby
2023-11-05 18:33:42 +01:00
committed by GitHub
parent 390abf3a44
commit 52670a8b24
105 changed files with 33415 additions and 408 deletions

View File

@ -22,3 +22,10 @@ revm.workspace = true
# common
tracing.workspace = true
[features]
optimism = [
"revm/optimism",
"reth-primitives/optimism",
"reth-consensus-common/optimism",
"reth-interfaces/optimism",
]

View File

@ -32,3 +32,7 @@ pub use revm::{self, *};
/// Ethereum DAO hardfork state change data.
pub mod eth_dao_fork;
/// Optimism-specific implementation and utilities for the executor
#[cfg(feature = "optimism")]
pub mod optimism;

View File

@ -0,0 +1,175 @@
use reth_interfaces::executor::{self as reth_executor, BlockExecutionError};
use reth_primitives::{Block, Bytes, ChainSpec, Hardfork, TransactionKind, U256};
use revm::{
primitives::{BedrockSpec, RegolithSpec},
L1BlockInfo,
};
/// Optimism-specific processor implementation for the `EVMProcessor`
pub mod processor;
/// Extracts the [L1BlockInfo] from the L2 block. The L1 info transaction is always the first
/// transaction in the L2 block.
pub fn extract_l1_info(block: &Block) -> Result<L1BlockInfo, BlockExecutionError> {
let l1_info_tx_data = block
.body
.iter()
.find(|tx| matches!(tx.kind(), TransactionKind::Call(to) if to == &revm::optimism::L1_BLOCK_CONTRACT))
.ok_or(reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "could not find l1 block info tx in the L2 block".to_string(),
}))
.and_then(|tx| {
tx.input().get(4..).ok_or(reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "could not get l1 block info tx calldata bytes".to_string(),
}))
})?;
parse_l1_info_tx(l1_info_tx_data)
}
/// Parses the calldata of the [L1BlockInfo] transaction.
pub fn parse_l1_info_tx(data: &[u8]) -> Result<L1BlockInfo, BlockExecutionError> {
// The setL1BlockValues tx calldata must be exactly 260 bytes long, considering that
// we already removed the first 4 bytes (the function selector). Detailed breakdown:
// 32 bytes for the block number
// + 32 bytes for the block timestamp
// + 32 bytes for the base fee
// + 32 bytes for the block hash
// + 32 bytes for the block sequence number
// + 32 bytes for the batcher hash
// + 32 bytes for the fee overhead
// + 32 bytes for the fee scalar
if data.len() != 256 {
return Err(reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "unexpected l1 block info tx calldata length found".to_string(),
},
))
}
let l1_base_fee = U256::try_from_be_slice(&data[64..96]).ok_or(
reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "could not convert l1 base fee".to_string(),
},
),
)?;
let l1_fee_overhead = U256::try_from_be_slice(&data[192..224]).ok_or(
reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "could not convert l1 fee overhead".to_string(),
},
),
)?;
let l1_fee_scalar = U256::try_from_be_slice(&data[224..256]).ok_or(
reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "could not convert l1 fee scalar".to_string(),
},
),
)?;
Ok(L1BlockInfo { l1_base_fee, l1_fee_overhead, l1_fee_scalar })
}
/// An extension trait for [L1BlockInfo] that allows us to calculate the L1 cost of a transaction
/// based off of the [ChainSpec]'s activated hardfork.
pub trait RethL1BlockInfo {
/// Forwards an L1 transaction calculation to revm and returns the gas cost.
///
/// ### Takes
/// - `chain_spec`: The [ChainSpec] for the node.
/// - `timestamp`: The timestamp of the current block.
/// - `input`: The calldata of the transaction.
/// - `is_deposit`: Whether or not the transaction is a deposit.
fn l1_tx_data_fee(
&self,
chain_spec: &ChainSpec,
timestamp: u64,
input: &Bytes,
is_deposit: bool,
) -> Result<U256, BlockExecutionError>;
/// Computes the data gas cost for an L2 transaction.
///
/// ### Takes
/// - `chain_spec`: The [ChainSpec] for the node.
/// - `timestamp`: The timestamp of the current block.
/// - `input`: The calldata of the transaction.
fn l1_data_gas(
&self,
chain_spec: &ChainSpec,
timestamp: u64,
input: &Bytes,
) -> Result<U256, BlockExecutionError>;
}
impl RethL1BlockInfo for L1BlockInfo {
fn l1_tx_data_fee(
&self,
chain_spec: &ChainSpec,
timestamp: u64,
input: &Bytes,
is_deposit: bool,
) -> Result<U256, BlockExecutionError> {
if is_deposit {
return Ok(U256::ZERO)
}
if chain_spec.is_fork_active_at_timestamp(Hardfork::Regolith, timestamp) {
Ok(self.calculate_tx_l1_cost::<RegolithSpec>(input))
} else if chain_spec.is_fork_active_at_timestamp(Hardfork::Bedrock, timestamp) {
Ok(self.calculate_tx_l1_cost::<BedrockSpec>(input))
} else {
Err(reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "Optimism hardforks are not active".to_string(),
},
))
}
}
fn l1_data_gas(
&self,
chain_spec: &ChainSpec,
timestamp: u64,
input: &Bytes,
) -> Result<U256, BlockExecutionError> {
if chain_spec.is_fork_active_at_timestamp(Hardfork::Regolith, timestamp) {
Ok(self.data_gas::<RegolithSpec>(input))
} else if chain_spec.is_fork_active_at_timestamp(Hardfork::Bedrock, timestamp) {
Ok(self.data_gas::<BedrockSpec>(input))
} else {
Err(reth_executor::BlockExecutionError::OptimismBlockExecution(
reth_executor::OptimismBlockExecutionError::L1BlockInfoError {
message: "Optimism hardforks are not active".to_string(),
},
))
}
}
}
#[cfg(test)]
mod test_l1_fee {
#[test]
fn sanity_l1_block() {
use super::*;
use reth_primitives::{hex_literal::hex, Bytes, Header, TransactionSigned};
let bytes = Bytes::from_static(&hex!("7ef9015aa044bae9d41b8380d781187b426c6fe43df5fb2fb57bd4466ef6a701e1f01e015694deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000001580808408f0d18001b90104015d8eb900000000000000000000000000000000000000000000000000000000008057650000000000000000000000000000000000000000000000000000000063d96d10000000000000000000000000000000000000000000000000000000000009f35273d89754a1e0387b89520d989d3be9c37c1f32495a88faf1ea05c61121ab0d1900000000000000000000000000000000000000000000000000000000000000010000000000000000000000002d679b567db6187c0c8323fa982cfb88b74dbcc7000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240"));
let l1_info_tx = TransactionSigned::decode_enveloped(bytes).unwrap();
let mock_block = Block {
header: Header::default(),
body: vec![l1_info_tx],
ommers: Vec::default(),
withdrawals: None,
};
let l1_info: L1BlockInfo = super::extract_l1_info(&mock_block).unwrap();
assert_eq!(l1_info.l1_base_fee, U256::from(652_114));
assert_eq!(l1_info.l1_fee_overhead, U256::from(2100));
assert_eq!(l1_info.l1_fee_scalar, U256::from(1_000_000));
}
}

View File

@ -0,0 +1,150 @@
use reth_interfaces::executor::{BlockExecutionError, BlockValidationError};
use reth_primitives::{
revm::compat::into_reth_log, revm_primitives::ResultAndState, Address, Block, Hardfork,
Receipt, U256,
};
use reth_provider::{BlockExecutor, BlockExecutorStats, BundleStateWithReceipts};
use revm::DatabaseCommit;
use std::time::Instant;
use tracing::{debug, trace};
use crate::processor::{verify_receipt, EVMProcessor};
impl<'a> BlockExecutor for EVMProcessor<'a> {
fn execute(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<(), BlockExecutionError> {
let receipts = self.execute_inner(block, total_difficulty, senders)?;
self.save_receipts(receipts)
}
fn execute_and_verify_receipt(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<(), BlockExecutionError> {
// execute block
let receipts = self.execute_inner(block, total_difficulty, senders)?;
// TODO Before Byzantium, receipts contained state root that would mean that expensive
// operation as hashing that is needed for state root got calculated in every
// transaction This was replaced with is_success flag.
// See more about EIP here: https://eips.ethereum.org/EIPS/eip-658
if self.chain_spec.fork(Hardfork::Byzantium).active_at_block(block.header.number) {
let time = Instant::now();
if let Err(error) =
verify_receipt(block.header.receipts_root, block.header.logs_bloom, receipts.iter())
{
debug!(target: "evm", ?error, ?receipts, "receipts verification failed");
return Err(error)
};
self.stats.receipt_root_duration += time.elapsed();
}
self.save_receipts(receipts)
}
fn execute_transactions(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<(Vec<Receipt>, u64), BlockExecutionError> {
self.init_env(&block.header, total_difficulty);
// perf: do not execute empty blocks
if block.body.is_empty() {
return Ok((Vec::new(), 0))
}
let senders = self.recover_senders(&block.body, senders)?;
let is_regolith =
self.chain_spec.fork(Hardfork::Regolith).active_at_timestamp(block.timestamp);
let mut cumulative_gas_used = 0;
let mut receipts = Vec::with_capacity(block.body.len());
for (transaction, sender) in block.body.iter().zip(senders) {
let time = Instant::now();
// The sum of the transactions gas limit, Tg, and the gas utilized in this block prior,
// must be no greater than the blocks 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())
}
// 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(|| {
self.db_mut()
.load_cache_account(sender)
.map(|acc| acc.account_info().unwrap_or_default())
})
.transpose()
.map_err(|_| BlockExecutionError::ProviderError)?;
// Execute transaction.
let ResultAndState { result, state } = self.transact(transaction, sender)?;
trace!(
target: "evm",
?transaction, ?result, ?state,
"Executed transaction"
);
self.stats.execution_duration += time.elapsed();
let time = Instant::now();
self.db_mut().commit(state);
self.stats.apply_state_duration += time.elapsed();
// 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,
// convert to reth log
logs: result.into_logs().into_iter().map(into_reth_log).collect(),
#[cfg(feature = "optimism")]
deposit_nonce: depositor.map(|account| account.nonce),
});
}
Ok((receipts, cumulative_gas_used))
}
fn take_output_state(&mut self) -> BundleStateWithReceipts {
let receipts = std::mem::take(&mut self.receipts);
BundleStateWithReceipts::new(
self.evm.db().unwrap().take_bundle(),
receipts,
self.first_block.unwrap_or_default(),
)
}
fn stats(&self) -> BlockExecutorStats {
self.stats.clone()
}
fn size_hint(&self) -> Option<usize> {
self.evm.db.as_ref().map(|db| db.bundle_size_hint())
}
}

View File

@ -9,24 +9,26 @@ use reth_interfaces::{
RethError,
};
use reth_primitives::{
revm::{
compat::into_reth_log,
env::{fill_cfg_and_block_env, fill_tx_env},
},
revm::env::{fill_cfg_and_block_env, fill_tx_env},
Address, Block, BlockNumber, Bloom, ChainSpec, Hardfork, Header, PruneMode, PruneModes,
PruneSegmentError, Receipt, ReceiptWithBloom, Receipts, TransactionSigned, B256,
MINIMUM_PRUNING_DISTANCE, U256,
};
use reth_provider::{
BlockExecutor, BlockExecutorStats, BundleStateWithReceipts, PrunableBlockExecutor,
StateProvider,
};
use reth_provider::{BlockExecutor, BlockExecutorStats, PrunableBlockExecutor, StateProvider};
use revm::{
db::{states::bundle_state::BundleRetention, StateDBBox},
primitives::ResultAndState,
DatabaseCommit, State, EVM,
State, EVM,
};
use std::{sync::Arc, time::Instant};
#[cfg(not(feature = "optimism"))]
use reth_primitives::revm::compat::into_reth_log;
#[cfg(not(feature = "optimism"))]
use reth_provider::BundleStateWithReceipts;
#[cfg(not(feature = "optimism"))]
use revm::DatabaseCommit;
#[cfg(not(feature = "optimism"))]
use tracing::{debug, trace};
/// EVMProcessor is a block executor that uses revm to execute blocks or multiple blocks.
@ -49,9 +51,9 @@ use tracing::{debug, trace};
#[allow(missing_debug_implementations)]
pub struct EVMProcessor<'a> {
/// The configured chain-spec
chain_spec: Arc<ChainSpec>,
pub(crate) chain_spec: Arc<ChainSpec>,
/// revm instance that contains database and env environment.
evm: EVM<StateDBBox<'a, RethError>>,
pub(crate) evm: EVM<StateDBBox<'a, RethError>>,
/// Hook and inspector stack that we want to invoke on that hook.
stack: InspectorStack,
/// The collection of receipts.
@ -59,10 +61,10 @@ pub struct EVMProcessor<'a> {
/// The inner vector stores receipts ordered by transaction number.
///
/// If receipt is None it means it is pruned.
receipts: Receipts,
pub(crate) receipts: Receipts,
/// First block will be initialized to `None`
/// and be set to the block number of first block executed.
first_block: Option<BlockNumber>,
pub(crate) first_block: Option<BlockNumber>,
/// The maximum known block.
tip: Option<BlockNumber>,
/// Pruning configuration.
@ -72,7 +74,7 @@ pub struct EVMProcessor<'a> {
/// block. None means there isn't any kind of configuration.
pruning_address_filter: Option<(u64, Vec<Address>)>,
/// Execution stats
stats: BlockExecutorStats,
pub(crate) stats: BlockExecutorStats,
}
impl<'a> EVMProcessor<'a> {
@ -148,7 +150,7 @@ impl<'a> EVMProcessor<'a> {
self.evm.db().expect("Database inside EVM is always set")
}
fn recover_senders(
pub(crate) fn recover_senders(
&mut self,
body: &[TransactionSigned],
senders: Option<Vec<Address>>,
@ -169,7 +171,7 @@ impl<'a> EVMProcessor<'a> {
}
/// Initializes the config and block env.
fn init_env(&mut self, header: &Header, total_difficulty: U256) {
pub(crate) fn init_env(&mut self, header: &Header, total_difficulty: U256) {
// Set state clear flag.
let state_clear_flag =
self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(header.number);
@ -252,8 +254,16 @@ impl<'a> EVMProcessor<'a> {
sender: Address,
) -> Result<ResultAndState, BlockExecutionError> {
// Fill revm structure.
#[cfg(not(feature = "optimism"))]
fill_tx_env(&mut self.evm.env.tx, transaction, sender);
#[cfg(feature = "optimism")]
{
let mut envelope_buf = Vec::with_capacity(transaction.length_without_header());
transaction.encode_enveloped(&mut envelope_buf);
fill_tx_env(&mut self.evm.env.tx, transaction, sender, envelope_buf.into());
}
let hash = transaction.hash();
let out = if self.stack.should_inspect(&self.evm.env, hash) {
// execution with inspector.
@ -271,79 +281,8 @@ impl<'a> EVMProcessor<'a> {
out.map_err(|e| BlockValidationError::EVM { hash, error: e.into() }.into())
}
/// Runs the provided transactions and commits their state to the run-time database.
///
/// The returned [BundleStateWithReceipts] can be used to persist the changes to disk, and
/// contains the changes made by each transaction.
///
/// The changes in [BundleStateWithReceipts] have a transition ID associated with them: there is
/// one transition ID for each transaction (with the first executed tx having transition ID
/// 0, and so on).
///
/// The second returned value represents the total gas used by this block of transactions.
pub fn execute_transactions(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<(Vec<Receipt>, u64), BlockExecutionError> {
self.init_env(&block.header, total_difficulty);
// perf: do not execute empty blocks
if block.body.is_empty() {
return Ok((Vec::new(), 0))
}
let senders = self.recover_senders(&block.body, senders)?;
let mut cumulative_gas_used = 0;
let mut receipts = Vec::with_capacity(block.body.len());
for (transaction, sender) in block.body.iter().zip(senders) {
let time = Instant::now();
// The sum of the transactions gas limit, Tg, and the gas utilized in this block prior,
// must be no greater than the blocks gasLimit.
let block_available_gas = block.header.gas_limit - cumulative_gas_used;
if transaction.gas_limit() > block_available_gas {
return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
transaction_gas_limit: transaction.gas_limit(),
block_available_gas,
}
.into())
}
// Execute transaction.
let ResultAndState { result, state } = self.transact(transaction, sender)?;
trace!(
target: "evm",
?transaction, ?result, ?state,
"Executed transaction"
);
self.stats.execution_duration += time.elapsed();
let time = Instant::now();
self.db_mut().commit(state);
self.stats.apply_state_duration += time.elapsed();
// 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,
// convert to reth log
logs: result.into_logs().into_iter().map(into_reth_log).collect(),
});
}
Ok((receipts, cumulative_gas_used))
}
/// Execute the block, verify gas usage and apply post-block state changes.
fn execute_inner(
pub(crate) fn execute_inner(
&mut self,
block: &Block,
total_difficulty: U256,
@ -457,6 +396,8 @@ impl<'a> EVMProcessor<'a> {
}
}
/// Default Ethereum implementation of the [BlockExecutor] trait for the [EVMProcessor].
#[cfg(not(feature = "optimism"))]
impl<'a> BlockExecutor for EVMProcessor<'a> {
fn execute(
&mut self,
@ -495,6 +436,69 @@ impl<'a> BlockExecutor for EVMProcessor<'a> {
self.save_receipts(receipts)
}
fn execute_transactions(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<(Vec<Receipt>, u64), BlockExecutionError> {
self.init_env(&block.header, total_difficulty);
// perf: do not execute empty blocks
if block.body.is_empty() {
return Ok((Vec::new(), 0))
}
let senders = self.recover_senders(&block.body, senders)?;
let mut cumulative_gas_used = 0;
let mut receipts = Vec::with_capacity(block.body.len());
for (transaction, sender) in block.body.iter().zip(senders) {
let time = Instant::now();
// The sum of the transactions gas limit, Tg, and the gas utilized in this block prior,
// must be no greater than the blocks gasLimit.
let block_available_gas = block.header.gas_limit - cumulative_gas_used;
if transaction.gas_limit() > block_available_gas {
return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
transaction_gas_limit: transaction.gas_limit(),
block_available_gas,
}
.into())
}
// Execute transaction.
let ResultAndState { result, state } = self.transact(transaction, sender)?;
trace!(
target: "evm",
?transaction, ?result, ?state,
"Executed transaction"
);
self.stats.execution_duration += time.elapsed();
let time = Instant::now();
self.db_mut().commit(state);
self.stats.apply_state_duration += time.elapsed();
// 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,
// convert to reth log
logs: result.into_logs().into_iter().map(into_reth_log).collect(),
#[cfg(feature = "optimism")]
deposit_nonce: None,
});
}
Ok((receipts, cumulative_gas_used))
}
fn take_output_state(&mut self) -> BundleStateWithReceipts {
let receipts = std::mem::take(&mut self.receipts);
BundleStateWithReceipts::new(
@ -564,7 +568,9 @@ mod tests {
trie::AccountProof,
Account, Bytecode, Bytes, ChainSpecBuilder, ForkCondition, StorageKey, MAINNET,
};
use reth_provider::{AccountReader, BlockHashReader, StateRootProvider};
use reth_provider::{
AccountReader, BlockHashReader, BundleStateWithReceipts, StateRootProvider,
};
use revm::{Database, TransitionState};
use std::collections::HashMap;