From 883975dc903da5c9a553e3b8f15b31703b457474 Mon Sep 17 00:00:00 2001 From: 0xKitsune <77890308+0xKitsune@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:18:29 -0400 Subject: [PATCH] chore(op-payload-builder): Refactor `optimism_payload` into smaller, reusable functions (#10904) --- crates/optimism/payload/src/builder.rs | 964 ++++++++++++++++--------- crates/optimism/payload/src/lib.rs | 1 + crates/payload/builder/src/error.rs | 3 + 3 files changed, 621 insertions(+), 347 deletions(-) diff --git a/crates/optimism/payload/src/builder.rs b/crates/optimism/payload/src/builder.rs index a7bfd0aecca6..97871a315ce1 100644 --- a/crates/optimism/payload/src/builder.rs +++ b/crates/optimism/payload/src/builder.rs @@ -7,10 +7,9 @@ use crate::{ use alloy_primitives::U256; use reth_basic_payload_builder::*; use reth_chain_state::ExecutedBlock; -use reth_chainspec::{EthereumHardforks, OptimismHardfork}; +use reth_chainspec::{ChainSpec, EthereumHardforks, OptimismHardfork}; use reth_evm::{ - system_calls::pre_block_beacon_root_contract_call, ConfigureEvm, ConfigureEvmEnv, - NextBlockEnvAttributes, + system_calls::pre_block_beacon_root_contract_call, ConfigureEvm, NextBlockEnvAttributes, }; use reth_execution_types::ExecutionOutcome; use reth_payload_builder::error::PayloadBuilderError; @@ -20,22 +19,23 @@ use reth_primitives::{ eip4844::calculate_excess_blob_gas, proofs, revm_primitives::{BlockEnv, CfgEnvWithHandlerCfg}, - Block, Header, IntoRecoveredTransaction, Receipt, TxType, EMPTY_OMMER_ROOT_HASH, + transaction::WithEncoded, + Address, Block, Bytes, Header, IntoRecoveredTransaction, Receipt, Receipts, SealedBlock, + TransactionSigned, TransactionSignedEcRecovered, TxType, B256, EMPTY_OMMER_ROOT_HASH, }; -use reth_provider::StateProviderFactory; +use reth_provider::{ProviderError, StateProviderFactory}; use reth_revm::database::StateProviderDatabase; use reth_transaction_pool::{ noop::NoopTransactionPool, BestTransactionsAttributes, TransactionPool, }; -use reth_trie::HashedPostState; +use reth_trie::{updates::TrieUpdates, HashedPostState}; use revm::{ db::states::bundle_state::BundleRetention, - primitives::{EVMError, EnvWithHandlerCfg, InvalidTransaction, ResultAndState}, - DatabaseCommit, State, + primitives::{EVMError, EnvWithHandlerCfg, ExecutionResult, InvalidTransaction}, + State, }; use std::sync::Arc; use tracing::{debug, trace, warn}; - /// Optimism's payload builder #[derive(Debug, Clone, PartialEq, Eq)] pub struct OptimismPayloadBuilder { @@ -46,7 +46,10 @@ pub struct OptimismPayloadBuilder { evm_config: EvmConfig, } -impl OptimismPayloadBuilder { +impl OptimismPayloadBuilder +where + EvmConfig: ConfigureEvm
, +{ /// `OptimismPayloadBuilder` constructor. pub const fn new(evm_config: EvmConfig) -> Self { Self { compute_pending_block: true, evm_config } @@ -67,24 +70,359 @@ impl OptimismPayloadBuilder { pub const fn is_compute_pending_block(&self) -> bool { self.compute_pending_block } -} -impl OptimismPayloadBuilder -where - EvmConfig: ConfigureEvmEnv
, -{ + + /// Constructs an Ethereum transaction payload from the transactions sent through the + /// Payload attributes by the sequencer. If the `no_tx_pool` argument is passed in + /// the payload attributes, the transaction pool will be ignored and the only transactions + /// included in the payload will be those sent through the attributes. + /// + /// Given build arguments including an Ethereum client, transaction pool, + /// and configuration, this function creates a transaction payload. Returns + /// a result indicating success with the payload or an error in case of failure. + #[inline] + fn build_payload( + &self, + evm_config: EvmConfig, + args: BuildArguments, + _compute_pending_block: bool, + ) -> Result, PayloadBuilderError> + where + EvmConfig: ConfigureEvm, + Client: StateProviderFactory, + Pool: TransactionPool, + { + let BuildArguments { client, pool, mut cached_reads, config, cancel, best_payload } = args; + + let state_provider = client.state_by_block_hash(config.parent_block.hash())?; + let state = StateProviderDatabase::new(state_provider); + let mut db = State::builder() + .with_database_ref(cached_reads.as_db(state)) + .with_bundle_update() + .build(); + + let (initialized_cfg, initialized_block_env) = self.cfg_and_block_env(&config); + + self.init_pre_block_state( + &config, + evm_config, + &initialized_cfg, + &initialized_block_env, + &mut db, + )?; + + let op_block_attributes = match self.construct_block_attributes( + pool, + &config, + initialized_cfg, + initialized_block_env, + &mut db, + &cancel, + ) { + Ok(outcome) => Ok(outcome), + Err(PayloadBuilderError::BuildOutcomeCancelled) => return Ok(BuildOutcome::Cancelled), + Err(err) => Err(err), + }?; + + // check if we have a better block + if !is_better_payload(best_payload.as_ref(), op_block_attributes.total_fees) { + // can skip building the block + return Ok(BuildOutcome::Aborted { fees: op_block_attributes.total_fees, cached_reads }); + } + + let (withdrawals_outcome, execution_outcome) = + self.construct_outcome(&op_block_attributes, &mut db)?; + + // calculate the state root + let parent_block = config.parent_block; + let hashed_state = HashedPostState::from_bundle_state(&execution_outcome.state().state); + let (state_root, trie_output) = { + let state_provider = db.database.0.inner.borrow_mut(); + state_provider.db.state_root_with_updates(hashed_state.clone()).inspect_err(|err| { + warn!(target: "payload_builder", + parent_hash=%parent_block.hash(), + %err, + "failed to calculate state root for empty payload" + ); + })? + }; + + let payload = self.construct_built_payload( + op_block_attributes, + execution_outcome, + state_root, + withdrawals_outcome, + hashed_state, + trie_output, + ); + + Ok(BuildOutcome::Better { payload, cached_reads }) + } + + /// Initializes the pre-block state for payload building. + /// + /// # Returns + /// + /// * `Ok(())` if the pre-block state is successfully initialized. + /// * `Err(PayloadBuilderError::Internal(_))` if applying the beacon root contract call fails. + /// * `Err(PayloadBuilderError::other(OptimismPayloadBuilderError::ForceCreate2DeployerFail))` + /// if ensuring the create2 deployer fails. + #[inline] + pub fn init_pre_block_state( + &self, + payload_config: &PayloadConfig, + evm_config: EvmConfig, + initialized_cfg: &CfgEnvWithHandlerCfg, + initialized_block_env: &BlockEnv, + db: &mut revm::State, + ) -> Result<(), PayloadBuilderError> + where + DB: revm::Database, + EvmConfig: ConfigureEvm, + { + let PayloadConfig { parent_block, attributes, chain_spec, .. } = payload_config; + + debug!(target: "payload_builder", id=%attributes.payload_attributes.payload_id(), parent_hash = ?parent_block.hash(), parent_number = parent_block.number, "building new payload"); + + pre_block_beacon_root_contract_call( + db, + &evm_config, + chain_spec, + initialized_cfg, + initialized_block_env, + attributes.payload_attributes.parent_beacon_block_root, + ) + .map_err(|err| { + warn!(target: "payload_builder", + parent_hash=%parent_block.hash(), + %err, + "failed to apply beacon root contract call for empty payload" + ); + PayloadBuilderError::Internal(err.into()) + })?; + + // 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. + reth_evm_optimism::ensure_create2_deployer( + chain_spec.clone(), + attributes.payload_attributes.timestamp, + db, + ) + .map_err(|err| { + warn!(target: "payload_builder", %err, "missing create2 deployer, skipping block."); + PayloadBuilderError::other(OptimismPayloadBuilderError::ForceCreate2DeployerFail) + }) + } + + /// Constructs new block attributes with a populated list of transactions + /// This method uses the provided payload attributes and transaction pool to populate the + /// block attributes. + /// + /// # Returns + /// + /// * `Ok(OptimismBlockAttributes)` with populated block attributes on success + /// * `Err(PayloadBuilderError::BuildOutcomeCancelled)` if the operation was cancelled + /// * `Err(PayloadBuilderError::BlobTransactionRejected)` if a blob transaction is encountered + /// * `Err(PayloadBuilderError::TransactionEcRecoverFailed)` if EC recovery fails for a + /// transaction + /// * `Err(PayloadBuilderError::EvmExecutionError(_))` if an EVM execution error occurs + #[inline] + pub fn construct_block_attributes( + &self, + pool: Pool, + payload_config: &PayloadConfig, + initialized_cfg: CfgEnvWithHandlerCfg, + initialized_block_env: BlockEnv, + db: &mut revm::State, + cancel: &Cancelled, + ) -> Result, PayloadBuilderError> + where + Pool: TransactionPool, + DB: revm::Database, + EvmConfig: ConfigureEvm, + { + let mut op_block_attributes = OptimismBlockAttributes::new( + payload_config, + initialized_cfg, + initialized_block_env, + self.evm_config.clone(), + ); + + // add sequencer transactions to block + op_block_attributes.add_sequencer_transactions( + &payload_config.attributes.transactions, + db, + cancel, + )?; + + // add pooled transactions to block + op_block_attributes.add_pooled_transactions(&pool, db, cancel)?; + + Ok(op_block_attributes) + } + + /// Constructs a built payload from the block attributes and execution outcomes. + /// This method assembles the final `OptimismBuiltPayload` by combining the block attributes, + /// execution outcomes, state roots, and other necessary components. It ensures that all + /// parts of the payload are correctly integrated and sealed for submission. + /// + /// # Returns + /// + /// An `OptimismBuiltPayload` representing the fully constructed block payload. + #[inline] + pub fn construct_built_payload( + &self, + op_block_attributes: OptimismBlockAttributes, + execution_outcome: ExecutionOutcome, + state_root: B256, + withdrawals_outcome: WithdrawalsOutcome, + hashed_state: HashedPostState, + trie_output: TrieUpdates, + ) -> OptimismBuiltPayload + where + EvmConfig: ConfigureEvm, + { + let chain_spec = op_block_attributes.chain_spec; + let block_number = op_block_attributes.initialized_block_env.number.to::(); + let timestamp = op_block_attributes.payload_attributes.timestamp(); + + let receipts_root = execution_outcome + .optimism_receipts_root_slow(block_number, chain_spec.as_ref(), timestamp) + .expect("Number is in range"); + + let logs_bloom = + execution_outcome.block_logs_bloom(block_number).expect("Number is in range"); + + // create the block header + let transactions_root = + proofs::calculate_transaction_root(&op_block_attributes.executed_txs); + + let mut excess_blob_gas = None; + let mut blob_gas_used = None; + + // only determine cancun fields when active + let parent_block = op_block_attributes.parent_block; + if chain_spec.is_cancun_active_at_timestamp(timestamp) { + excess_blob_gas = if chain_spec.is_cancun_active_at_timestamp(parent_block.timestamp) { + let parent_excess_blob_gas = parent_block.excess_blob_gas.unwrap_or_default(); + let parent_blob_gas_used = parent_block.blob_gas_used.unwrap_or_default(); + Some(calculate_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used)) + } else { + // for the first post-fork block, both parent.blob_gas_used and + // parent.excess_blob_gas are evaluated as 0 + Some(calculate_excess_blob_gas(0, 0)) + }; + + blob_gas_used = Some(0); + } + + let header = Header { + parent_hash: parent_block.hash(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: op_block_attributes.initialized_block_env.coinbase, + state_root, + transactions_root, + receipts_root, + withdrawals_root: withdrawals_outcome.withdrawals_root, + logs_bloom, + timestamp, + mix_hash: op_block_attributes.payload_attributes.prev_randao(), + nonce: BEACON_NONCE, + base_fee_per_gas: Some(op_block_attributes.base_fee), + number: parent_block.number + 1, + gas_limit: op_block_attributes.block_gas_limit, + difficulty: U256::ZERO, + gas_used: op_block_attributes.cumulative_gas_used, + extra_data: op_block_attributes.extra_data, + parent_beacon_block_root: op_block_attributes + .payload_attributes + .parent_beacon_block_root(), + blob_gas_used, + excess_blob_gas, + requests_root: None, + }; + + let block = Block { + header, + body: op_block_attributes.executed_txs, + ommers: vec![], + withdrawals: withdrawals_outcome.withdrawals, + requests: None, + }; + + let sealed_block = block.seal_slow(); + debug!(target: "payload_builder", ?sealed_block, "sealed built block"); + + // create the executed block data + let executed = ExecutedBlock { + block: Arc::new(sealed_block.clone()), + senders: Arc::new(op_block_attributes.executed_senders), + execution_output: Arc::new(execution_outcome), + hashed_state: Arc::new(hashed_state), + trie: Arc::new(trie_output), + }; + + OptimismBuiltPayload::new( + op_block_attributes.payload_attributes.payload_id(), + sealed_block, + op_block_attributes.total_fees, + chain_spec, + op_block_attributes.payload_attributes, + Some(executed), + ) + } + + /// Constructs the execution and withdrawal outcomes resulting from transactions in the block. + /// + /// # Returns + /// + /// * `Ok((WithdrawalsOutcome, ExecutionOutcome))` containing the outcomes of withdrawals and + /// transaction executions. + /// * `Err(ProviderError)` if processing withdrawals fails or state merging encounters an error. + #[inline] + pub fn construct_outcome( + &self, + op_block_attributes: &OptimismBlockAttributes, + db: &mut revm::State, + ) -> Result<(WithdrawalsOutcome, ExecutionOutcome), ProviderError> + where + EvmConfig: ConfigureEvm, + DB: revm::Database, + { + let timestamp = op_block_attributes.payload_attributes.timestamp(); + + let withdrawals_outcome = commit_withdrawals( + db, + &op_block_attributes.chain_spec, + timestamp, + op_block_attributes.payload_attributes.withdrawals().to_owned(), + )?; + + // merge all transitions into bundle state, this would apply the withdrawal balance changes + // and 4788 contract call + db.merge_transitions(BundleRetention::Reverts); + + let block_number = op_block_attributes.initialized_block_env.number.to::(); + let receipts = Receipts { receipt_vec: vec![op_block_attributes.receipts.clone()] }; + let execution_outcome = + ExecutionOutcome::new(db.take_bundle(), receipts, block_number, Vec::new()); + + Ok((withdrawals_outcome, execution_outcome)) + } + /// Returns the configured [`CfgEnvWithHandlerCfg`] and [`BlockEnv`] for the targeted payload /// (that has the `parent` as its parent). fn cfg_and_block_env( &self, config: &PayloadConfig, - parent: &Header, ) -> (CfgEnvWithHandlerCfg, BlockEnv) { let next_attributes = NextBlockEnvAttributes { timestamp: config.attributes.timestamp(), suggested_fee_recipient: config.attributes.suggested_fee_recipient(), prev_randao: config.attributes.prev_randao(), }; - self.evm_config.next_cfg_and_block_env(parent, next_attributes) + self.evm_config.next_cfg_and_block_env(&config.parent_block, next_attributes) } } @@ -102,14 +440,7 @@ where &self, args: BuildArguments, ) -> Result, PayloadBuilderError> { - let (cfg_env, block_env) = self.cfg_and_block_env(&args.config, &args.config.parent_block); - optimism_payload( - self.evm_config.clone(), - args, - cfg_env, - block_env, - self.compute_pending_block, - ) + self.build_payload(self.evm_config.clone(), args, self.compute_pending_block) } fn on_missing_payload( @@ -137,229 +468,267 @@ where cancel: Default::default(), best_payload: None, }; - let (cfg_env, block_env) = self.cfg_and_block_env(&args.config, &args.config.parent_block); - optimism_payload(self.evm_config.clone(), args, cfg_env, block_env, false)? + + self.build_payload(self.evm_config.clone(), args, false)? .into_payload() .ok_or_else(|| PayloadBuilderError::MissingPayload) } } -/// Constructs an Ethereum transaction payload from the transactions sent through the -/// Payload attributes by the sequencer. If the `no_tx_pool` argument is passed in -/// the payload attributes, the transaction pool will be ignored and the only transactions -/// included in the payload will be those sent through the attributes. +/// Represents the attributes and state required to build an Optimism block /// -/// Given build arguments including an Ethereum client, transaction pool, -/// and configuration, this function creates a transaction payload. Returns -/// a result indicating success with the payload or an error in case of failure. -#[inline] -pub(crate) fn optimism_payload( - evm_config: EvmConfig, - args: BuildArguments, - initialized_cfg: CfgEnvWithHandlerCfg, - initialized_block_env: BlockEnv, - _compute_pending_block: bool, -) -> Result, PayloadBuilderError> +/// This struct holds all necessary data for constructing a block on Optimism +/// including executed transactions, receipts, gas usage, and EVM-specific +/// configuration parameters +#[derive(Debug)] +pub struct OptimismBlockAttributes { + /// Receipts to each transaction in the block + pub receipts: Vec>, + /// Signed transactions executed in the block + pub executed_txs: Vec, + /// Addresses of the senders of the executed transactions + pub executed_senders: Vec
, + /// Indicates if the Regolith hardfork is active at the block's timestamp + pub is_regolith: bool, + /// Gas limit for the block + pub block_gas_limit: u64, + /// Base fee per execution gas + pub base_fee: u64, + /// Total fees collected from all transactions + pub total_fees: U256, + /// Cumulative gas used by all transactions + pub cumulative_gas_used: u64, + /// Configuration parameters for the EVM environment + pub evm_config: EvmConfig, + /// Initialized block environment containing block-specific information + pub initialized_block_env: BlockEnv, + /// Initialized configuration environment with handler settings + pub initialized_cfg: CfgEnvWithHandlerCfg, + /// Shared reference to the chain specification + pub chain_spec: Arc, + /// Payload attributes sent from op-node for block construction + pub payload_attributes: OptimismPayloadBuilderAttributes, + /// Reference to the parent block + pub parent_block: Arc, + /// Extra data to include in the block header + pub extra_data: Bytes, +} + +impl OptimismBlockAttributes where EvmConfig: ConfigureEvm
, - Client: StateProviderFactory, - Pool: TransactionPool, { - let BuildArguments { client, pool, mut cached_reads, config, cancel, best_payload } = args; - - let state_provider = client.state_by_block_hash(config.parent_block.hash())?; - let state = StateProviderDatabase::new(state_provider); - let mut db = - State::builder().with_database_ref(cached_reads.as_db(state)).with_bundle_update().build(); - let PayloadConfig { parent_block, attributes, chain_spec, extra_data } = config; - - debug!(target: "payload_builder", id=%attributes.payload_attributes.payload_id(), parent_hash = ?parent_block.hash(), parent_number = parent_block.number, "building new payload"); - - let mut cumulative_gas_used = 0; - let block_gas_limit: u64 = attributes.gas_limit.unwrap_or_else(|| { - initialized_block_env.gas_limit.try_into().unwrap_or(chain_spec.max_gas_limit) - }); - let base_fee = initialized_block_env.basefee.to::(); - - let mut executed_txs = Vec::with_capacity(attributes.transactions.len()); - let mut executed_senders = Vec::with_capacity(attributes.transactions.len()); - - let mut best_txs = pool.best_transactions_with_attributes(BestTransactionsAttributes::new( - base_fee, - initialized_block_env.get_blob_gasprice().map(|gasprice| gasprice as u64), - )); - - let mut total_fees = U256::ZERO; - - let block_number = initialized_block_env.number.to::(); - - let is_regolith = chain_spec.is_fork_active_at_timestamp( - OptimismHardfork::Regolith, - attributes.payload_attributes.timestamp, - ); - - // apply eip-4788 pre block contract call - pre_block_beacon_root_contract_call( - &mut db, - &evm_config, - &chain_spec, - &initialized_cfg, - &initialized_block_env, - attributes.payload_attributes.parent_beacon_block_root, - ) - .map_err(|err| { - warn!(target: "payload_builder", - parent_hash=%parent_block.hash(), - %err, - "failed to apply beacon root contract call for empty payload" + /// Creates a new `OptimismBlockAttributes` instance. + /// Initializes the block attributes based on the provided payload configuration + /// and EVM configuration. + pub fn new( + payload_config: &PayloadConfig, + initialized_cfg: CfgEnvWithHandlerCfg, + initialized_block_env: BlockEnv, + evm_config: EvmConfig, + ) -> Self { + let attributes = &payload_config.attributes; + + let is_regolith = payload_config.chain_spec.is_fork_active_at_timestamp( + OptimismHardfork::Regolith, + attributes.payload_attributes.timestamp, ); - PayloadBuilderError::Internal(err.into()) - })?; - - // 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. - reth_evm_optimism::ensure_create2_deployer( - chain_spec.clone(), - attributes.payload_attributes.timestamp, - &mut db, - ) - .map_err(|err| { - warn!(target: "payload_builder", %err, "missing create2 deployer, skipping block."); - PayloadBuilderError::other(OptimismPayloadBuilderError::ForceCreate2DeployerFail) - })?; - - let mut receipts = Vec::with_capacity(attributes.transactions.len()); - for sequencer_tx in &attributes.transactions { - // Check if the job was cancelled, if so we can exit early. - if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) - } - // A sequencer's block should never contain blob transactions. - if sequencer_tx.value().is_eip4844() { - return Err(PayloadBuilderError::other( - OptimismPayloadBuilderError::BlobTransactionRejected, - )) + let block_gas_limit: u64 = attributes.gas_limit.unwrap_or_else(|| { + initialized_block_env + .gas_limit + .try_into() + .unwrap_or(payload_config.chain_spec.max_gas_limit) + }); + + let base_fee = initialized_block_env.basefee.to::(); + + Self { + executed_senders: Vec::with_capacity(attributes.transactions.len()), + executed_txs: Vec::with_capacity(attributes.transactions.len()), + receipts: Vec::with_capacity(attributes.transactions.len()), + is_regolith, + block_gas_limit, + base_fee, + total_fees: U256::ZERO, + cumulative_gas_used: 0, + evm_config, + initialized_block_env, + initialized_cfg, + chain_spec: payload_config.chain_spec.clone(), + payload_attributes: attributes.clone(), + parent_block: payload_config.parent_block.clone(), + extra_data: payload_config.extra_data.clone(), } + } - // Convert the transaction to a [TransactionSignedEcRecovered]. This is - // purely for the purposes of utilizing the `evm_config.tx_env`` function. - // Deposit transactions do not have signatures, so if the tx is a deposit, this - // will just pull in its `from` address. - let sequencer_tx = sequencer_tx.value().clone().try_into_ecrecovered().map_err(|_| { - PayloadBuilderError::other(OptimismPayloadBuilderError::TransactionEcRecoverFailed) - })?; - - // 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 && sequencer_tx.is_deposit()) - .then(|| { - db.load_cache_account(sequencer_tx.signer()) - .map(|acc| acc.account_info().unwrap_or_default()) - }) - .transpose() - .map_err(|_| { - PayloadBuilderError::other(OptimismPayloadBuilderError::AccountLoadFailed( - sequencer_tx.signer(), - )) - })?; - - let env = EnvWithHandlerCfg::new_with_cfg_env( - initialized_cfg.clone(), - initialized_block_env.clone(), - evm_config.tx_env(&sequencer_tx), - ); + /// Adds sequencer transactions to the block + /// + /// # Returns + /// + /// * `Ok(())` on successful addition of all valid sequencer transactions + /// * `Err(PayloadBuilderError::BuildOutcomeCancelled)` if the operation was cancelled + /// * `Err(PayloadBuilderError::BlobTransactionRejected)` if a blob transaction is encountered + /// * `Err(PayloadBuilderError::TransactionEcRecoverFailed)` if EC recovery fails for a + /// transaction + /// * `Err(PayloadBuilderError::EvmExecutionError(EVMError::Transaction(_)))` if an EVM + /// execution error occurs + #[inline] + pub fn add_sequencer_transactions( + &mut self, + transactions: &[WithEncoded], + db: &mut revm::State, + cancel: &Cancelled, + ) -> Result<(), PayloadBuilderError> + where + DB: revm::Database, + { + // Add sequencer transactions to the block + for sequencer_tx in transactions { + // Check if the job was cancelled, if so we can exit early. + if cancel.is_cancelled() { + return Err(PayloadBuilderError::BuildOutcomeCancelled); + } - let mut evm = evm_config.evm_with_env(&mut db, env); + // Payload attributes transactions should never contain blob transactions. + if sequencer_tx.value().is_eip4844() { + return Err(PayloadBuilderError::other( + OptimismPayloadBuilderError::BlobTransactionRejected, + )); + } - let ResultAndState { result, state } = match evm.transact() { - Ok(res) => res, - Err(err) => { - match err { + // Convert the transaction to a [TransactionSignedEcRecovered]. This is + // purely for the purposes of utilizing the `evm_config.tx_env`` function. + // Deposit transactions do not have signatures, so if the tx is a deposit, this + // will just pull in its `from` address. + let sequencer_tx = + sequencer_tx.value().clone().try_into_ecrecovered().map_err(|_| { + PayloadBuilderError::other( + OptimismPayloadBuilderError::TransactionEcRecoverFailed, + ) + })?; + + // + // 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 = (self.is_regolith && sequencer_tx.is_deposit()) + .then(|| { + db.load_cache_account(sequencer_tx.signer()) + .map(|acc| acc.account_info().unwrap_or_default()) + }) + .transpose() + .map_err(|_| { + PayloadBuilderError::other(OptimismPayloadBuilderError::AccountLoadFailed( + sequencer_tx.signer(), + )) + })?; + + let execution_result = match evm_transact_commit( + &sequencer_tx, + self.initialized_cfg.clone(), + self.initialized_block_env.clone(), + self.evm_config.clone(), + db, + ) { + Ok(result) => result, + Err(err) => match err { EVMError::Transaction(err) => { - trace!(target: "payload_builder", %err, ?sequencer_tx, "Error in sequencer transaction, skipping."); - continue + trace!(target: "payload_builder", %err, ?sequencer_tx, "Error insequencer transaction, skipping."); + continue; } - err => { - // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(err)) + other => { + return Err(PayloadBuilderError::EvmExecutionError(other)); } - } - } - }; + }, + }; + + self.cumulative_gas_used += execution_result.gas_used(); + + let receipt = Receipt { + tx_type: sequencer_tx.tx_type(), + success: execution_result.is_success(), + cumulative_gas_used: self.cumulative_gas_used, + logs: execution_result.into_logs().into_iter().map(Into::into).collect(), + 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: self + .chain_spec + .is_fork_active_at_timestamp( + OptimismHardfork::Canyon, + self.payload_attributes.payload_attributes.timestamp, + ) + .then_some(1), + }; - // to release the db reference drop evm. - drop(evm); - // commit changes - db.commit(state); - - let gas_used = result.gas_used(); - - // add gas used by the transaction to cumulative gas used, before creating the receipt - cumulative_gas_used += gas_used; - - // Push transaction changeset and calculate header bloom filter for receipt. - receipts.push(Some(Receipt { - tx_type: sequencer_tx.tx_type(), - success: result.is_success(), - cumulative_gas_used, - logs: result.into_logs().into_iter().map(Into::into).collect(), - 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: chain_spec - .is_fork_active_at_timestamp( - OptimismHardfork::Canyon, - attributes.payload_attributes.timestamp, - ) - .then_some(1), - })); - - // append sender and transaction to the respective lists - executed_senders.push(sequencer_tx.signer()); - executed_txs.push(sequencer_tx.into_signed()); + self.receipts.push(Some(receipt)); + self.executed_senders.push(sequencer_tx.signer()); + self.executed_txs.push(sequencer_tx.clone().into_signed()); + } + + Ok(()) } - if !attributes.no_tx_pool { + /// Adds transactions to the block from the transaction pool + /// + /// # Returns + /// + /// * `Ok(())` on successful addition of all valid pooled transactions + /// * `Err(PayloadBuilderError::BuildOutcomeCancelled)` if the operation was cancelled + /// * `Err(PayloadBuilderError::EvmExecutionError(_))` if an EVM execution error occurs + #[inline] + pub fn add_pooled_transactions( + &mut self, + pool: &Pool, + db: &mut revm::State, + cancel: &Cancelled, + ) -> Result<(), PayloadBuilderError> + where + DB: revm::Database, + Pool: TransactionPool, + { + if self.payload_attributes.no_tx_pool { + return Ok(()); + } + + let mut best_txs = pool.best_transactions_with_attributes(BestTransactionsAttributes::new( + self.base_fee, + self.initialized_block_env.get_blob_gasprice().map(|gasprice| gasprice as u64), + )); + while let Some(pool_tx) = best_txs.next() { // ensure we still have capacity for this transaction - if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit { + if self.cumulative_gas_used + pool_tx.gas_limit() > self.block_gas_limit { // we can't fit this transaction into the block, so we need to mark it as // invalid which also removes all dependent transaction from // the iterator before we can continue best_txs.mark_invalid(&pool_tx); - continue + continue; } // A sequencer's block should never contain blob or deposit transactions from the pool. if pool_tx.is_eip4844() || pool_tx.tx_type() == TxType::Deposit as u8 { best_txs.mark_invalid(&pool_tx); - continue + continue; } - // check if the job was cancelled, if so we can exit early + // Check if the job was cancelled, if so we can exit early. if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) + return Err(PayloadBuilderError::BuildOutcomeCancelled); } // convert tx to a signed transaction let tx = pool_tx.to_recovered_transaction(); - let env = EnvWithHandlerCfg::new_with_cfg_env( - initialized_cfg.clone(), - initialized_block_env.clone(), - evm_config.tx_env(&tx), - ); - // Configure the environment for the block. - let mut evm = evm_config.evm_with_env(&mut db, env); - - let ResultAndState { result, state } = match evm.transact() { + let execution_result = match evm_transact_commit( + &tx, + self.initialized_cfg.clone(), + self.initialized_block_env.clone(), + self.evm_config.clone(), + db, + ) { Ok(res) => res, Err(err) => { match err { @@ -374,166 +743,67 @@ where best_txs.mark_invalid(&pool_tx); } - continue + continue; } err => { // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(err)) + return Err(PayloadBuilderError::EvmExecutionError(err)); } } } }; - // drop evm so db is released. - drop(evm); - // commit changes - db.commit(state); - - let gas_used = result.gas_used(); - // add gas used by the transaction to cumulative gas used, before creating the - // receipt - cumulative_gas_used += gas_used; + let gas_used = execution_result.gas_used(); + self.cumulative_gas_used += gas_used; - // Push transaction changeset and calculate header bloom filter for receipt. - receipts.push(Some(Receipt { + let receipt = Receipt { tx_type: tx.tx_type(), - success: result.is_success(), - cumulative_gas_used, - logs: result.into_logs().into_iter().map(Into::into).collect(), + success: execution_result.is_success(), + cumulative_gas_used: self.cumulative_gas_used, + logs: execution_result.into_logs().into_iter().map(Into::into).collect(), deposit_nonce: None, deposit_receipt_version: None, - })); + }; // update add to total fees let miner_fee = tx - .effective_tip_per_gas(Some(base_fee)) + .effective_tip_per_gas(Some(self.base_fee)) .expect("fee is always valid; execution succeeded"); - total_fees += U256::from(miner_fee) * U256::from(gas_used); + self.total_fees += U256::from(miner_fee) * U256::from(gas_used); - // append sender and transaction to the respective lists - executed_senders.push(tx.signer()); - executed_txs.push(tx.into_signed()); + self.receipts.push(Some(receipt)); + self.executed_senders.push(tx.signer()); + self.executed_txs.push(tx.clone().into_signed()); } - } - // check if we have a better block - if !is_better_payload(best_payload.as_ref(), total_fees) { - // can skip building the block - return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }) - } - - let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals( - &mut db, - &chain_spec, - attributes.payload_attributes.timestamp, - attributes.clone().payload_attributes.withdrawals, - )?; - - // merge all transitions into bundle state, this would apply the withdrawal balance changes - // and 4788 contract call - db.merge_transitions(BundleRetention::Reverts); - - let execution_outcome = ExecutionOutcome::new( - db.take_bundle(), - vec![receipts.clone()].into(), - block_number, - Vec::new(), - ); - let receipts_root = execution_outcome - .optimism_receipts_root_slow( - block_number, - chain_spec.as_ref(), - attributes.payload_attributes.timestamp, - ) - .expect("Number is in range"); - let logs_bloom = execution_outcome.block_logs_bloom(block_number).expect("Number is in range"); - - // calculate the state root - let hashed_state = HashedPostState::from_bundle_state(&execution_outcome.state().state); - let (state_root, trie_output) = { - let state_provider = db.database.0.inner.borrow_mut(); - state_provider.db.state_root_with_updates(hashed_state.clone()).inspect_err(|err| { - warn!(target: "payload_builder", - parent_hash=%parent_block.hash(), - %err, - "failed to calculate state root for empty payload" - ); - })? - }; - - // create the block header - let transactions_root = proofs::calculate_transaction_root(&executed_txs); - - // initialize empty blob sidecars. There are no blob transactions on L2. - let blob_sidecars = Vec::new(); - let mut excess_blob_gas = None; - let mut blob_gas_used = None; - - // only determine cancun fields when active - if chain_spec.is_cancun_active_at_timestamp(attributes.payload_attributes.timestamp) { - excess_blob_gas = if chain_spec.is_cancun_active_at_timestamp(parent_block.timestamp) { - let parent_excess_blob_gas = parent_block.excess_blob_gas.unwrap_or_default(); - let parent_blob_gas_used = parent_block.blob_gas_used.unwrap_or_default(); - Some(calculate_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used)) - } else { - // for the first post-fork block, both parent.blob_gas_used and - // parent.excess_blob_gas are evaluated as 0 - Some(calculate_excess_blob_gas(0, 0)) - }; - - blob_gas_used = Some(0); + Ok(()) } +} - let header = Header { - parent_hash: parent_block.hash(), - ommers_hash: EMPTY_OMMER_ROOT_HASH, - beneficiary: initialized_block_env.coinbase, - state_root, - transactions_root, - receipts_root, - withdrawals_root, - logs_bloom, - timestamp: attributes.payload_attributes.timestamp, - mix_hash: attributes.payload_attributes.prev_randao, - nonce: BEACON_NONCE, - base_fee_per_gas: Some(base_fee), - number: parent_block.number + 1, - gas_limit: block_gas_limit, - difficulty: U256::ZERO, - gas_used: cumulative_gas_used, - extra_data, - parent_beacon_block_root: attributes.payload_attributes.parent_beacon_block_root, - blob_gas_used, - excess_blob_gas, - requests_root: None, - }; - - // seal the block - let block = Block { header, body: executed_txs, ommers: vec![], withdrawals, requests: None }; - - let sealed_block = block.seal_slow(); - debug!(target: "payload_builder", ?sealed_block, "sealed built block"); - - // create the executed block data - let executed = ExecutedBlock { - block: Arc::new(sealed_block.clone()), - senders: Arc::new(executed_senders), - execution_output: Arc::new(execution_outcome), - hashed_state: Arc::new(hashed_state), - trie: Arc::new(trie_output), - }; - - let mut payload = OptimismBuiltPayload::new( - attributes.payload_attributes.id, - sealed_block, - total_fees, - chain_spec, - attributes, - Some(executed), +/// Executes a transaction within the EVM and commits the state changes to the db +/// +/// # Returns +/// +/// * `Ok(ExecutionResult)` with the result of the transaction execution on success +/// * `Err(EVMError)` if the transaction execution fails due to EVM-related errors +#[inline] +pub fn evm_transact_commit( + transaction: &TransactionSignedEcRecovered, + initialized_cfg: CfgEnvWithHandlerCfg, + initialized_block_env: BlockEnv, + evm_config: EvmConfig, + db: &mut revm::State, +) -> Result> +where + DB: revm::Database, + EvmConfig: ConfigureEvm, +{ + let env = EnvWithHandlerCfg::new_with_cfg_env( + initialized_cfg, + initialized_block_env, + evm_config.tx_env(transaction), ); + let mut evm = evm_config.evm_with_env(db, env); - // extend the payload with the blob sidecars from the executed txs - payload.extend_sidecars(blob_sidecars); - - Ok(BuildOutcome::Better { payload, cached_reads }) + evm.transact_commit() } diff --git a/crates/optimism/payload/src/lib.rs b/crates/optimism/payload/src/lib.rs index 2bb60594287a..53bb3bebd238 100644 --- a/crates/optimism/payload/src/lib.rs +++ b/crates/optimism/payload/src/lib.rs @@ -15,6 +15,7 @@ pub mod builder; pub use builder::OptimismPayloadBuilder; pub mod error; pub mod payload; + pub use payload::{ OptimismBuiltPayload, OptimismPayloadAttributes, OptimismPayloadBuilderAttributes, }; diff --git a/crates/payload/builder/src/error.rs b/crates/payload/builder/src/error.rs index 8c430000f7c3..ef3874e74feb 100644 --- a/crates/payload/builder/src/error.rs +++ b/crates/payload/builder/src/error.rs @@ -14,6 +14,9 @@ pub enum PayloadBuilderError { /// An oneshot channels has been closed. #[error("sender has been dropped")] ChannelClosed, + /// Build cancelled + #[error("build outcome cancelled")] + BuildOutcomeCancelled, /// If there's no payload to resolve. #[error("missing payload")] MissingPayload,