From e5ecd4af065027bd999f0c8ebd876394a3bbef99 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Thu, 28 Dec 2023 20:57:07 +0100 Subject: [PATCH] refactor: move ethereum and op builder into separate crates (#5876) --- Cargo.lock | 30 ++ Cargo.toml | 3 + bin/reth/Cargo.toml | 6 +- bin/reth/src/cli/ext.rs | 9 +- bin/reth/src/debug_cmd/build_block.rs | 14 +- crates/payload/basic/Cargo.toml | 11 +- crates/payload/basic/src/lib.rs | 510 +++++--------------------- crates/payload/basic/src/optimism.rs | 326 ---------------- crates/payload/ethereum/Cargo.toml | 29 ++ crates/payload/ethereum/src/lib.rs | 306 ++++++++++++++++ crates/payload/optimism/Cargo.toml | 35 ++ crates/payload/optimism/src/lib.rs | 407 ++++++++++++++++++++ 12 files changed, 924 insertions(+), 762 deletions(-) delete mode 100644 crates/payload/basic/src/optimism.rs create mode 100644 crates/payload/ethereum/Cargo.toml create mode 100644 crates/payload/ethereum/src/lib.rs create mode 100644 crates/payload/optimism/Cargo.toml create mode 100644 crates/payload/optimism/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c5482ac9f..db68da191 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5624,12 +5624,14 @@ dependencies = [ "reth-db", "reth-discv4", "reth-downloaders", + "reth-ethereum-payload-builder", "reth-interfaces", "reth-metrics", "reth-net-nat", "reth-network", "reth-network-api", "reth-nippy-jar", + "reth-optimism-payload-builder", "reth-payload-builder", "reth-payload-validator", "reth-primitives", @@ -6005,6 +6007,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "reth-ethereum-payload-builder" +version = "0.1.0-alpha.13" +dependencies = [ + "reth-basic-payload-builder", + "reth-payload-builder", + "reth-primitives", + "reth-provider", + "reth-revm", + "reth-transaction-pool", + "revm", + "tracing", +] + [[package]] name = "reth-interfaces" version = "0.1.0-alpha.13" @@ -6206,6 +6222,20 @@ dependencies = [ "zstd 0.12.4", ] +[[package]] +name = "reth-optimism-payload-builder" +version = "0.1.0-alpha.13" +dependencies = [ + "reth-basic-payload-builder", + "reth-payload-builder", + "reth-primitives", + "reth-provider", + "reth-revm", + "reth-transaction-pool", + "revm", + "tracing", +] + [[package]] name = "reth-payload-builder" version = "0.1.0-alpha.13" diff --git a/Cargo.toml b/Cargo.toml index 1a7fb4a5c..4c32de962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ members = [ "crates/net/network-api/", "crates/payload/basic/", "crates/payload/builder/", + "crates/payload/ethereum/", + "crates/payload/optimism/", "crates/payload/validator/", "crates/primitives/", "crates/prune/", @@ -105,6 +107,7 @@ reth-downloaders = { path = "crates/net/downloaders" } reth-ecies = { path = "crates/net/ecies" } reth-eth-wire = { path = "crates/net/eth-wire" } reth-ethereum-forks = { path = "crates/ethereum-forks" } +reth-ethereum-payload-builder = { path = "crates/payload/ethereum" } reth-interfaces = { path = "crates/interfaces" } reth-ipc = { path = "crates/rpc/ipc" } reth-libmdbx = { path = "crates/storage/libmdbx-rs" } diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index d1747b069..281fe9e3b 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -43,6 +43,8 @@ reth-downloaders = { workspace = true, features = ["test-utils"] } reth-tracing.workspace = true reth-tasks.workspace = true reth-net-nat.workspace = true +reth-optimism-payload-builder = { path = "../../crates/payload/optimism", optional = true } +reth-ethereum-payload-builder.workspace = true reth-payload-builder.workspace = true reth-payload-validator.workspace = true reth-basic-payload-builder.workspace = true @@ -134,10 +136,12 @@ optimism = [ "reth-provider/optimism", "reth-beacon-consensus/optimism", "reth-auto-seal-consensus/optimism", - "reth-basic-payload-builder/optimism", "reth-network/optimism", "reth-network-api/optimism", "reth-blockchain-tree/optimism", + "reth-payload-builder/optimism", + "reth-optimism-payload-builder/optimism", + "reth-ethereum-payload-builder/optimism", ] # no-op feature flag for switching between the `optimism` and default functionality in CI matrices ethereum = [] diff --git a/bin/reth/src/cli/ext.rs b/bin/reth/src/cli/ext.rs index 939c11ae5..d72db048b 100644 --- a/bin/reth/src/cli/ext.rs +++ b/bin/reth/src/cli/ext.rs @@ -140,17 +140,18 @@ pub trait RethNodeCommandConfig: fmt::Debug { .extradata(conf.extradata_rlp_bytes()) .max_gas_limit(conf.max_gas_limit()); + // no extradata for optimism #[cfg(feature = "optimism")] - let payload_job_config = - payload_job_config.compute_pending_block(conf.compute_pending_block()); + let payload_job_config = payload_job_config.extradata(Default::default()); // The default payload builder is implemented on the unit type. #[cfg(not(feature = "optimism"))] - let payload_builder = reth_basic_payload_builder::EthereumPayloadBuilder::default(); + let payload_builder = reth_ethereum_payload_builder::EthereumPayloadBuilder::default(); // Optimism's payload builder is implemented on the OptimismPayloadBuilder type. #[cfg(feature = "optimism")] - let payload_builder = reth_basic_payload_builder::OptimismPayloadBuilder::default(); + let payload_builder = reth_optimism_payload_builder::OptimismPayloadBuilder::default() + .set_compute_pending_block(conf.compute_pending_block()); let payload_generator = BasicPayloadJobGenerator::with_builder( components.provider(), diff --git a/bin/reth/src/debug_cmd/build_block.rs b/bin/reth/src/debug_cmd/build_block.rs index 29728692b..c4fe2b52f 100644 --- a/bin/reth/src/debug_cmd/build_block.rs +++ b/bin/reth/src/debug_cmd/build_block.rs @@ -12,7 +12,7 @@ use alloy_rlp::Decodable; use clap::Parser; use eyre::Context; use reth_basic_payload_builder::{ - default_payload_builder, BuildArguments, BuildOutcome, Cancelled, PayloadConfig, + BuildArguments, BuildOutcome, Cancelled, PayloadBuilder, PayloadConfig, }; use reth_beacon_consensus::BeaconConsensus; use reth_blockchain_tree::{ @@ -244,8 +244,6 @@ impl Command { Bytes::default(), PayloadBuilderAttributes::try_new(best_block.hash, payload_attrs)?, self.chain.clone(), - #[cfg(feature = "optimism")] - true, ); let args = BuildArguments::new( blockchain_db.clone(), @@ -255,7 +253,15 @@ impl Command { Cancelled::default(), None, ); - match default_payload_builder(args)? { + + #[cfg(feature = "optimism")] + let payload_builder = reth_optimism_payload_builder::OptimismPayloadBuilder::default() + .compute_pending_block(); + + #[cfg(not(feature = "optimism"))] + let payload_builder = reth_ethereum_payload_builder::EthereumPayloadBuilder::default(); + + match payload_builder.try_build(args)? { BuildOutcome::Better { payload, .. } => { let block = payload.block(); debug!(target: "reth::cli", ?block, "Built new payload"); diff --git a/crates/payload/basic/Cargo.toml b/crates/payload/basic/Cargo.toml index 6d06aa4b0..4ecbaa698 100644 --- a/crates/payload/basic/Cargo.toml +++ b/crates/payload/basic/Cargo.toml @@ -32,13 +32,4 @@ reth-metrics.workspace = true metrics.workspace = true # misc -tracing.workspace = true - -[features] -optimism = [ - "reth-primitives/optimism", - "reth-revm/optimism", - "reth-transaction-pool/optimism", - "reth-provider/optimism", - "reth-payload-builder/optimism" -] +tracing.workspace = true \ No newline at end of file diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index 2b5c397f2..3f07ca28f 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -5,43 +5,22 @@ html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" )] -#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![warn( + missing_debug_implementations, + missing_docs, + unused_crate_dependencies, + unreachable_pub, + rustdoc::all +)] #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use crate::metrics::PayloadBuilderMetrics; use alloy_rlp::Encodable; use futures_core::ready; use futures_util::FutureExt; -use reth_interfaces::RethResult; -use reth_payload_builder::{ - database::CachedReads, error::PayloadBuilderError, BuiltPayload, KeepPayloadJobAlive, - PayloadBuilderAttributes, PayloadId, PayloadJob, PayloadJobGenerator, -}; -use reth_primitives::{ - bytes::BytesMut, - constants::{ - eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE, EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, - EMPTY_WITHDRAWALS, ETHEREUM_BLOCK_GAS_LIMIT, RETH_CLIENT_VERSION, SLOT_DURATION, - }, - eip4844::calculate_excess_blob_gas, - proofs, - revm::{compat::into_reth_log, env::tx_env_with_recovered}, - Block, BlockNumberOrTag, Bytes, ChainSpec, Header, IntoRecoveredTransaction, Receipt, Receipts, - SealedBlock, Withdrawal, B256, EMPTY_OMMER_ROOT_HASH, U256, -}; -use reth_provider::{ - BlockReaderIdExt, BlockSource, BundleStateWithReceipts, ProviderError, StateProviderFactory, -}; -use reth_revm::{ - database::StateProviderDatabase, - state_change::{apply_beacon_root_contract_call, post_block_withdrawals_balance_increments}, -}; -use reth_tasks::TaskSpawner; -use reth_transaction_pool::TransactionPool; use revm::{ db::states::bundle_state::BundleRetention, - primitives::{BlockEnv, CfgEnv, EVMError, Env, InvalidTransaction, ResultAndState}, + primitives::{BlockEnv, CfgEnv, Env}, Database, DatabaseCommit, State, }; use std::{ @@ -57,21 +36,37 @@ use tokio::{ }; use tracing::{debug, trace, warn}; +use reth_interfaces::RethResult; +use reth_payload_builder::{ + database::CachedReads, error::PayloadBuilderError, BuiltPayload, KeepPayloadJobAlive, + PayloadBuilderAttributes, PayloadId, PayloadJob, PayloadJobGenerator, +}; +use reth_primitives::{ + bytes::BytesMut, + constants::{ + BEACON_NONCE, EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, EMPTY_WITHDRAWALS, + ETHEREUM_BLOCK_GAS_LIMIT, RETH_CLIENT_VERSION, SLOT_DURATION, + }, + proofs, Block, BlockNumberOrTag, Bytes, ChainSpec, Header, Receipts, SealedBlock, Withdrawal, + B256, EMPTY_OMMER_ROOT_HASH, U256, +}; +use reth_provider::{ + BlockReaderIdExt, BlockSource, BundleStateWithReceipts, ProviderError, StateProviderFactory, +}; +use reth_revm::{ + database::StateProviderDatabase, + state_change::{apply_beacon_root_contract_call, post_block_withdrawals_balance_increments}, +}; +use reth_tasks::TaskSpawner; +use reth_transaction_pool::TransactionPool; + +use crate::metrics::PayloadBuilderMetrics; + mod metrics; -#[cfg(feature = "optimism")] -mod optimism; -#[cfg(feature = "optimism")] -pub use optimism::OptimismPayloadBuilder; - -/// Ethereum payload builder -#[derive(Debug, Clone, Copy, Default)] -#[non_exhaustive] -pub struct EthereumPayloadBuilder; - /// The [`PayloadJobGenerator`] that creates [`BasicPayloadJob`]s. #[derive(Debug)] -pub struct BasicPayloadJobGenerator { +pub struct BasicPayloadJobGenerator { /// The client that can interact with the chain. client: Client, /// txpool @@ -92,26 +87,6 @@ pub struct BasicPayloadJobGenerator BasicPayloadJobGenerator { - /// Creates a new [BasicPayloadJobGenerator] with the given config. - pub fn new( - client: Client, - pool: Pool, - executor: Tasks, - config: BasicPayloadJobGeneratorConfig, - chain_spec: Arc, - ) -> Self { - BasicPayloadJobGenerator::with_builder( - client, - pool, - executor, - config, - chain_spec, - EthereumPayloadBuilder, - ) - } -} - impl BasicPayloadJobGenerator { /// Creates a new [BasicPayloadJobGenerator] with the given config and custom [PayloadBuilder] pub fn with_builder( @@ -195,8 +170,6 @@ where self.config.extradata.clone(), attributes, Arc::clone(&self.chain_spec), - #[cfg(feature = "optimism")] - self.config.compute_pending_block, ); let until = self.job_deadline(config.attributes.timestamp); @@ -246,9 +219,6 @@ pub struct BasicPayloadJobGeneratorConfig { deadline: Duration, /// Maximum number of tasks to spawn for building a payload. max_payload_tasks: usize, - /// The rollup's compute pending block configuration option. - #[cfg(feature = "optimism")] - compute_pending_block: bool, } // === impl BasicPayloadJobGeneratorConfig === @@ -292,15 +262,6 @@ impl BasicPayloadJobGeneratorConfig { self.max_gas_limit = max_gas_limit; self } - - /// Sets the compute pending block configuration option. - /// - /// Defaults to `false`. - #[cfg(feature = "optimism")] - pub fn compute_pending_block(mut self, compute_pending_block: bool) -> Self { - self.compute_pending_block = compute_pending_block; - self - } } impl Default for BasicPayloadJobGeneratorConfig { @@ -314,8 +275,6 @@ impl Default for BasicPayloadJobGeneratorConfig { // 12s slot time deadline: SLOT_DURATION, max_payload_tasks: 3, - #[cfg(feature = "optimism")] - compute_pending_block: false, } } } @@ -344,7 +303,7 @@ pub struct BasicPayloadJob { /// Caches all disk reads for the state the new payloads builds on /// /// This is used to avoid reading the same state over and over again when new attempts are - /// triggerd, because during the building process we'll repeatedly execute the transactions. + /// triggered, because during the building process we'll repeatedly execute the transactions. cached_reads: Option, /// metrics for this type metrics: PayloadBuilderMetrics, @@ -478,6 +437,22 @@ where if best_payload.is_none() { debug!(target: "payload_builder", id=%self.config.payload_id(), "no best payload yet to resolve, building empty payload"); + let args = BuildArguments { + client: self.client.clone(), + pool: self.pool.clone(), + cached_reads: self.cached_reads.take().unwrap_or_default(), + config: self.config.clone(), + cancel: Cancelled::default(), + best_payload: None, + }; + + if let Some(payload) = self.builder.on_missing_payload(args) { + return ( + ResolveBestPayload { best_payload: Some(payload), maybe_better, empty_payload }, + KeepPayloadJobAlive::Yes, + ) + } + // if no payload has been built yet self.metrics.inc_requested_empty_payload(); // no payload built yet, so we need to return an empty payload @@ -489,41 +464,6 @@ where let _ = tx.send(res); })); - // In Optimism, the PayloadAttributes can specify a `no_tx_pool` option that implies we - // should not pull transactions from the tx pool. In this case, we build the payload - // upfront with the list of transactions sent in the attributes without caring about - // the results of the polling job, if a best payload has not already been built. - #[cfg(feature = "optimism")] - { - if self.config.chain_spec.is_optimism() && - self.config.attributes.optimism_payload_attributes.no_tx_pool - { - let args = BuildArguments { - client: self.client.clone(), - pool: self.pool.clone(), - cached_reads: self.cached_reads.take().unwrap_or_default(), - config: self.config.clone(), - cancel: Cancelled::default(), - best_payload: None, - }; - if let Ok(BuildOutcome::Better { payload, cached_reads }) = - self.builder.try_build(args) - { - self.cached_reads = Some(cached_reads); - trace!(target: "payload_builder", "[OPTIMISM] Forced best payload"); - let payload = Arc::new(payload); - return ( - ResolveBestPayload { - best_payload: Some(payload), - maybe_better, - empty_payload, - }, - KeepPayloadJobAlive::Yes, - ) - } - } - } - empty_payload = Some(rx); } @@ -636,35 +576,27 @@ impl Drop for Cancelled { #[derive(Clone, Debug)] pub struct PayloadConfig { /// Pre-configured block environment. - initialized_block_env: BlockEnv, + pub initialized_block_env: BlockEnv, /// Configuration for the environment. - initialized_cfg: CfgEnv, + pub initialized_cfg: CfgEnv, /// The parent block. - parent_block: Arc, + pub parent_block: Arc, /// Block extra data. - extra_data: Bytes, + pub extra_data: Bytes, /// Requested attributes for the payload. - attributes: PayloadBuilderAttributes, + pub attributes: PayloadBuilderAttributes, /// The chain spec. - chain_spec: Arc, - /// The rollup's compute pending block configuration option. - // TODO(clabby): Implement this feature. - #[cfg(feature = "optimism")] - #[allow(dead_code)] - compute_pending_block: bool, + pub chain_spec: Arc, } impl PayloadConfig { /// Returns an owned instance of the [PayloadConfig]'s extra_data bytes. - pub(crate) fn extra_data(&self) -> reth_primitives::Bytes { - #[cfg(feature = "optimism")] - if self.chain_spec.is_optimism() { - return Default::default() - } + pub fn extra_data(&self) -> Bytes { self.extra_data.clone() } - pub(crate) fn payload_id(&self) -> PayloadId { + /// Returns the payload id. + pub fn payload_id(&self) -> PayloadId { self.attributes.id } } @@ -676,7 +608,6 @@ impl PayloadConfig { extra_data: Bytes, attributes: PayloadBuilderAttributes, chain_spec: Arc, - #[cfg(feature = "optimism")] compute_pending_block: bool, ) -> Self { // configure evm env based on parent block let (initialized_cfg, initialized_block_env) = @@ -689,8 +620,6 @@ impl PayloadConfig { extra_data, attributes, chain_spec, - #[cfg(feature = "optimism")] - compute_pending_block, } } } @@ -723,12 +652,18 @@ pub enum BuildOutcome { /// payload configuration, cancellation status, and the best payload achieved so far. #[derive(Debug)] pub struct BuildArguments { - client: Client, - pool: Pool, - cached_reads: CachedReads, - config: PayloadConfig, - cancel: Cancelled, - best_payload: Option>, + /// How to interact with the chain. + pub client: Client, + /// The transaction pool. + pub pool: Pool, + /// Previously cached disk reads + pub cached_reads: CachedReads, + /// How to configure the payload. + pub config: PayloadConfig, + /// A marker that can be used to cancel the job. + pub cancel: Cancelled, + /// The best payload achieved so far. + pub best_payload: Option>, } impl BuildArguments { @@ -770,280 +705,18 @@ pub trait PayloadBuilder: Send + Sync + Clone { &self, args: BuildArguments, ) -> Result; -} -// Default implementation of [PayloadBuilder] for unit type -impl PayloadBuilder for EthereumPayloadBuilder -where - Client: StateProviderFactory, - Pool: TransactionPool, -{ - fn try_build( - &self, - args: BuildArguments, - ) -> Result { - default_payload_builder(args) + /// Invoked when the payload job is being resolved and there is no payload yet. + /// + /// If this returns a payload, it will be used as the final payload for the job. + /// + /// TODO(mattsse): This needs to be refined a bit because this only exists for OP atm + fn on_missing_payload(&self, args: BuildArguments) -> Option> { + let _args = args; + None } } -/// Constructs an Ethereum transaction payload using the best transactions from the pool. -/// -/// 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 fn default_payload_builder( - args: BuildArguments, -) -> Result -where - 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 extra_data = config.extra_data(); - let PayloadConfig { - initialized_block_env, - initialized_cfg, - parent_block, - attributes, - chain_spec, - .. - } = config; - - debug!(target: "payload_builder", id=%attributes.id, parent_hash = ?parent_block.hash, parent_number = parent_block.number, "building new payload"); - let mut cumulative_gas_used = 0; - let mut sum_blob_gas_used = 0; - let block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); - let base_fee = initialized_block_env.basefee.to::(); - - let mut executed_txs = Vec::new(); - let mut best_txs = pool.best_transactions_with_base_fee(base_fee); - - let mut total_fees = U256::ZERO; - - let block_number = initialized_block_env.number.to::(); - - // apply eip-4788 pre block contract call - pre_block_beacon_root_contract_call( - &mut db, - &chain_spec, - block_number, - &initialized_cfg, - &initialized_block_env, - &attributes, - )?; - - let mut receipts = Vec::new(); - 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 { - // 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 - } - - // check if the job was cancelled, if so we can exit early - if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) - } - - // convert tx to a signed transaction - let tx = pool_tx.to_recovered_transaction(); - - // There's only limited amount of blob space available per block, so we need to check if the - // EIP-4844 can still fit in the block - if let Some(blob_tx) = tx.transaction.as_eip4844() { - let tx_blob_gas = blob_tx.blob_gas(); - if sum_blob_gas_used + tx_blob_gas > MAX_DATA_GAS_PER_BLOCK { - // we can't fit this _blob_ transaction into the block, so we mark it as invalid, - // which removes its dependent transactions from the iterator. This is similar to - // the gas limit condition for regular transactions above. - trace!(target: "payload_builder", tx=?tx.hash, ?sum_blob_gas_used, ?tx_blob_gas, "skipping blob transaction because it would exceed the max data gas per block"); - best_txs.mark_invalid(&pool_tx); - continue - } - } - - // Configure the environment for the block. - let env = Env { - cfg: initialized_cfg.clone(), - block: initialized_block_env.clone(), - tx: tx_env_with_recovered(&tx), - }; - - let mut evm = revm::EVM::with_env(env); - evm.database(&mut db); - - let ResultAndState { result, state } = match evm.transact() { - Ok(res) => res, - Err(err) => { - match err { - EVMError::Transaction(err) => { - if matches!(err, InvalidTransaction::NonceTooLow { .. }) { - // if the nonce is too low, we can skip this transaction - trace!(target: "payload_builder", ?err, ?tx, "skipping nonce too low transaction"); - } else { - // if the transaction is invalid, we can skip it and all of its - // descendants - trace!(target: "payload_builder", ?err, ?tx, "skipping invalid transaction and its descendants"); - best_txs.mark_invalid(&pool_tx); - } - - continue - } - err => { - // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(err)) - } - } - } - }; - - // commit changes - db.commit(state); - - // add to the total blob gas used if the transaction successfully executed - if let Some(blob_tx) = tx.transaction.as_eip4844() { - let tx_blob_gas = blob_tx.blob_gas(); - sum_blob_gas_used += tx_blob_gas; - - // if we've reached the max data gas per block, we can skip blob txs entirely - if sum_blob_gas_used == MAX_DATA_GAS_PER_BLOCK { - best_txs.skip_blobs(); - } - } - - 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: tx.tx_type(), - success: result.is_success(), - cumulative_gas_used, - logs: result.logs().into_iter().map(into_reth_log).collect(), - #[cfg(feature = "optimism")] - deposit_nonce: None, - #[cfg(feature = "optimism")] - deposit_receipt_version: None, - })); - - // update add to total fees - let miner_fee = tx - .effective_tip_per_gas(Some(base_fee)) - .expect("fee is always valid; execution succeeded"); - total_fees += U256::from(miner_fee) * U256::from(gas_used); - - // append transaction to the list of executed transactions - executed_txs.push(tx.into_signed()); - } - - // check if we have a better block - if !is_better_payload(best_payload.as_deref(), 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.timestamp, attributes.withdrawals)?; - - // merge all transitions into bundle state, this would apply the withdrawal balance changes and - // 4788 contract call - db.merge_transitions(BundleRetention::PlainState); - - let bundle = BundleStateWithReceipts::new( - db.take_bundle(), - Receipts::from_vec(vec![receipts]), - block_number, - ); - let receipts_root = bundle - .receipts_root_slow( - block_number, - #[cfg(feature = "optimism")] - chain_spec.as_ref(), - #[cfg(feature = "optimism")] - attributes.timestamp, - ) - .expect("Number is in range"); - let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); - - // calculate the state root - let state_root = state_provider.state_root(&bundle)?; - - // create the block header - let transactions_root = proofs::calculate_transaction_root(&executed_txs); - - // initialize empty blob sidecars at first. If cancun is active then this will - let mut 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.timestamp) { - // grab the blob sidecars from the executed txs - blob_sidecars = pool.get_all_blobs_exact( - executed_txs.iter().filter(|tx| tx.is_eip4844()).map(|tx| tx.hash).collect(), - )?; - - 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(sum_blob_gas_used); - } - - 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.timestamp, - mix_hash: 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.parent_beacon_block_root, - blob_gas_used, - excess_blob_gas, - }; - - // seal the block - let block = Block { header, body: executed_txs, ommers: vec![], withdrawals }; - - let sealed_block = block.seal_slow(); - debug!(target: "payload_builder", ?sealed_block, "sealed built block"); - - let mut payload = BuiltPayload::new(attributes.id, sealed_block, total_fees); - - // extend the payload with the blob sidecars from the executed txs - payload.extend_sidecars(blob_sidecars); - - Ok(BuildOutcome::Better { payload, cached_reads }) -} - /// Builds an empty payload without any transactions. fn build_empty_payload( client: &Client, @@ -1139,19 +812,22 @@ where /// Represents the outcome of committing withdrawals to the runtime database and post state. /// Pre-shanghai these are `None` values. -#[derive(Default)] -struct WithdrawalsOutcome { - withdrawals: Option>, - withdrawals_root: Option, +#[derive(Default, Debug)] +pub struct WithdrawalsOutcome { + /// committed withdrawals, if any. + pub withdrawals: Option>, + /// withdrawals root if any. + pub withdrawals_root: Option, } impl WithdrawalsOutcome { /// No withdrawals pre shanghai - fn pre_shanghai() -> Self { + pub fn pre_shanghai() -> Self { Self { withdrawals: None, withdrawals_root: None } } - fn empty() -> Self { + /// No withdrawals + pub fn empty() -> Self { Self { withdrawals: Some(vec![]), withdrawals_root: Some(EMPTY_WITHDRAWALS) } } } @@ -1161,7 +837,7 @@ impl WithdrawalsOutcome { /// Returns the withdrawals root. /// /// Returns `None` values pre shanghai -fn commit_withdrawals>( +pub fn commit_withdrawals>( db: &mut State, chain_spec: &ChainSpec, timestamp: u64, @@ -1199,7 +875,7 @@ fn commit_withdrawals>( /// /// This uses [apply_beacon_root_contract_call] to ultimately apply the beacon root contract state /// change. -fn pre_block_beacon_root_contract_call( +pub fn pre_block_beacon_root_contract_call( db: &mut DB, chain_spec: &ChainSpec, block_number: u64, @@ -1236,7 +912,7 @@ where /// /// This compares the total fees of the blocks, higher is better. #[inline(always)] -fn is_better_payload(best_payload: Option<&BuiltPayload>, new_fees: U256) -> bool { +pub fn is_better_payload(best_payload: Option<&BuiltPayload>, new_fees: U256) -> bool { if let Some(best_payload) = best_payload { new_fees > best_payload.fees() } else { diff --git a/crates/payload/basic/src/optimism.rs b/crates/payload/basic/src/optimism.rs deleted file mode 100644 index 2f2785458..000000000 --- a/crates/payload/basic/src/optimism.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! Optimism's [PayloadBuilder] implementation. - -use super::*; -use reth_payload_builder::error::OptimismPayloadBuilderError; -use reth_primitives::Hardfork; - -/// 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] -pub(crate) fn optimism_payload_builder( - args: BuildArguments, -) -> Result -where - 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 extra_data = config.extra_data(); - let PayloadConfig { - initialized_block_env, - initialized_cfg, - parent_block, - attributes, - chain_spec, - .. - } = config; - - debug!(target: "payload_builder", id=%attributes.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 - .optimism_payload_attributes - .gas_limit - .unwrap_or(initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX)); - let base_fee = initialized_block_env.basefee.to::(); - - let mut executed_txs = Vec::new(); - let mut best_txs = pool.best_transactions_with_base_fee(base_fee); - - let mut total_fees = U256::ZERO; - - let block_number = initialized_block_env.number.to::(); - - let is_regolith = - chain_spec.is_fork_active_at_timestamp(Hardfork::Regolith, attributes.timestamp); - - // 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_revm::optimism::ensure_create2_deployer(chain_spec.clone(), attributes.timestamp, &mut db) - .map_err(|_| { - PayloadBuilderError::Optimism(OptimismPayloadBuilderError::ForceCreate2DeployerFail) - })?; - - let mut receipts = Vec::new(); - for sequencer_tx in attributes.optimism_payload_attributes.transactions { - // Check if the job was cancelled, if so we can exit early. - if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) - } - - // Convert the transaction to a [TransactionSignedEcRecovered]. This is - // purely for the purposes of utilizing the [tx_env_with_recovered] 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.clone().try_into_ecrecovered().map_err(|_| { - PayloadBuilderError::Optimism(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::Optimism(OptimismPayloadBuilderError::AccountLoadFailed( - sequencer_tx.signer(), - )) - })?; - - // Configure the environment for the block. - let env = Env { - cfg: initialized_cfg.clone(), - block: initialized_block_env.clone(), - tx: tx_env_with_recovered(&sequencer_tx), - }; - - let mut evm = revm::EVM::with_env(env); - evm.database(&mut db); - - let ResultAndState { result, state } = match evm.transact() { - Ok(res) => res, - Err(err) => { - match err { - EVMError::Transaction(err) => { - trace!(target: "optimism_payload_builder", ?err, ?sequencer_tx, "Error in sequencer transaction, skipping."); - continue - } - err => { - // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(err)) - } - } - } - }; - - // 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.logs().into_iter().map(into_reth_log).collect(), - #[cfg(feature = "optimism")] - 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. - #[cfg(feature = "optimism")] - deposit_receipt_version: chain_spec - .is_fork_active_at_timestamp(Hardfork::Canyon, attributes.timestamp) - .then_some(1), - })); - - // append transaction to the list of executed transactions - executed_txs.push(sequencer_tx.into_signed()); - } - - if !attributes.optimism_payload_attributes.no_tx_pool { - 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 { - // 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 - } - - // check if the job was cancelled, if so we can exit early - if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) - } - - // convert tx to a signed transaction - let tx = pool_tx.to_recovered_transaction(); - - // Configure the environment for the block. - let env = Env { - cfg: initialized_cfg.clone(), - block: initialized_block_env.clone(), - tx: tx_env_with_recovered(&tx), - }; - - let mut evm = revm::EVM::with_env(env); - evm.database(&mut db); - - let ResultAndState { result, state } = match evm.transact() { - Ok(res) => res, - Err(err) => { - match err { - EVMError::Transaction(err) => { - if matches!(err, InvalidTransaction::NonceTooLow { .. }) { - // if the nonce is too low, we can skip this transaction - trace!(target: "payload_builder", ?err, ?tx, "skipping nonce too low transaction"); - } else { - // if the transaction is invalid, we can skip it and all of its - // descendants - trace!(target: "payload_builder", ?err, ?tx, "skipping invalid transaction and its descendants"); - best_txs.mark_invalid(&pool_tx); - } - - continue - } - err => { - // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(err)) - } - } - } - }; - - // 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: tx.tx_type(), - success: result.is_success(), - cumulative_gas_used, - logs: result.logs().into_iter().map(into_reth_log).collect(), - #[cfg(feature = "optimism")] - deposit_nonce: None, - #[cfg(feature = "optimism")] - deposit_receipt_version: None, - })); - - // update add to total fees - let miner_fee = tx - .effective_tip_per_gas(Some(base_fee)) - .expect("fee is always valid; execution succeeded"); - total_fees += U256::from(miner_fee) * U256::from(gas_used); - - // append transaction to the list of executed transactions - executed_txs.push(tx.into_signed()); - } - } - - // check if we have a better block - if !is_better_payload(best_payload.as_deref(), 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.timestamp, attributes.withdrawals)?; - - // merge all transitions into bundle state, this would apply the withdrawal balance changes - // and 4788 contract call - db.merge_transitions(BundleRetention::PlainState); - - let bundle = BundleStateWithReceipts::new( - db.take_bundle(), - Receipts::from_vec(vec![receipts]), - block_number, - ); - let receipts_root = bundle - .receipts_root_slow(block_number, chain_spec.as_ref(), attributes.timestamp) - .expect("Number is in range"); - let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); - - // calculate the state root - let state_root = state_provider.state_root(&bundle)?; - - // create the block header - let transactions_root = proofs::calculate_transaction_root(&executed_txs); - - // Cancun is not yet active on Optimism chains. - let blob_sidecars = Vec::new(); - let excess_blob_gas = None; - let blob_gas_used = None; - - 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.timestamp, - mix_hash: 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.parent_beacon_block_root, - blob_gas_used, - excess_blob_gas, - }; - - // seal the block - let block = Block { header, body: executed_txs, ommers: vec![], withdrawals }; - - let sealed_block = block.seal_slow(); - debug!(target: "payload_builder", ?sealed_block, "sealed built block"); - - let mut payload = BuiltPayload::new(attributes.id, sealed_block, total_fees); - - // extend the payload with the blob sidecars from the executed txs - payload.extend_sidecars(blob_sidecars); - - Ok(BuildOutcome::Better { payload, cached_reads }) -} - -/// Optimism's payload builder -#[derive(Debug, Clone, Copy, Default)] -#[non_exhaustive] -pub struct OptimismPayloadBuilder; - -/// Implementation of the [PayloadBuilder] trait for [OptimismPayloadBuilder]. -impl PayloadBuilder for OptimismPayloadBuilder -where - Client: StateProviderFactory, - Pool: TransactionPool, -{ - fn try_build( - &self, - args: BuildArguments, - ) -> Result { - optimism_payload_builder(args) - } -} diff --git a/crates/payload/ethereum/Cargo.toml b/crates/payload/ethereum/Cargo.toml new file mode 100644 index 000000000..91e2410f2 --- /dev/null +++ b/crates/payload/ethereum/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "reth-ethereum-payload-builder" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "A basic ethereum payload builder for reth that uses the txpool API to build payloads." + +[dependencies] +# reth +reth-primitives.workspace = true +reth-revm.workspace = true +reth-transaction-pool.workspace = true +reth-provider.workspace = true +reth-payload-builder.workspace = true +reth-basic-payload-builder.workspace = true + +# ethereum +revm.workspace = true + +# misc +tracing.workspace = true + +[features] +# This is a workaround for reth-cli crate to allow this as mandatory dependency without breaking the build even if unused. +# This makes managing features and testing workspace easier because clippy always builds all members if --workspace is provided +optimism = [] \ No newline at end of file diff --git a/crates/payload/ethereum/src/lib.rs b/crates/payload/ethereum/src/lib.rs new file mode 100644 index 000000000..5738b51c3 --- /dev/null +++ b/crates/payload/ethereum/src/lib.rs @@ -0,0 +1,306 @@ +//! A basic Ethereum payload builder implementation. + +#![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/" +)] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[cfg(not(feature = "optimism"))] +pub use builder::*; + +#[cfg(not(feature = "optimism"))] +mod builder { + use reth_basic_payload_builder::{ + commit_withdrawals, is_better_payload, pre_block_beacon_root_contract_call, BuildArguments, + BuildOutcome, PayloadBuilder, PayloadConfig, WithdrawalsOutcome, + }; + use reth_payload_builder::{error::PayloadBuilderError, BuiltPayload}; + use reth_primitives::{ + constants::{eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE}, + eip4844::calculate_excess_blob_gas, + proofs, + revm::{compat::into_reth_log, env::tx_env_with_recovered}, + Block, Header, IntoRecoveredTransaction, Receipt, Receipts, EMPTY_OMMER_ROOT_HASH, U256, + }; + use reth_provider::{BundleStateWithReceipts, StateProviderFactory}; + use reth_revm::database::StateProviderDatabase; + use reth_transaction_pool::TransactionPool; + use revm::{ + db::states::bundle_state::BundleRetention, + primitives::{EVMError, Env, InvalidTransaction, ResultAndState}, + DatabaseCommit, State, + }; + use tracing::{debug, trace}; + + /// Ethereum payload builder + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + #[non_exhaustive] + pub struct EthereumPayloadBuilder; + + // Default implementation of [PayloadBuilder] for unit type + impl PayloadBuilder for EthereumPayloadBuilder + where + Client: StateProviderFactory, + Pool: TransactionPool, + { + fn try_build( + &self, + args: BuildArguments, + ) -> Result { + default_ethereum_payload_builder(args) + } + } + + /// Constructs an Ethereum transaction payload using the best transactions from the pool. + /// + /// 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 fn default_ethereum_payload_builder( + args: BuildArguments, + ) -> Result + where + 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 extra_data = config.extra_data(); + let PayloadConfig { + initialized_block_env, + initialized_cfg, + parent_block, + attributes, + chain_spec, + .. + } = config; + + debug!(target: "payload_builder", id=%attributes.id, parent_hash = ?parent_block.hash, parent_number = parent_block.number, "building new payload"); + let mut cumulative_gas_used = 0; + let mut sum_blob_gas_used = 0; + let block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); + let base_fee = initialized_block_env.basefee.to::(); + + let mut executed_txs = Vec::new(); + let mut best_txs = pool.best_transactions_with_base_fee(base_fee); + + let mut total_fees = U256::ZERO; + + let block_number = initialized_block_env.number.to::(); + + // apply eip-4788 pre block contract call + pre_block_beacon_root_contract_call( + &mut db, + &chain_spec, + block_number, + &initialized_cfg, + &initialized_block_env, + &attributes, + )?; + + let mut receipts = Vec::new(); + 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 { + // 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 + } + + // check if the job was cancelled, if so we can exit early + if cancel.is_cancelled() { + return Ok(BuildOutcome::Cancelled) + } + + // convert tx to a signed transaction + let tx = pool_tx.to_recovered_transaction(); + + // There's only limited amount of blob space available per block, so we need to check if + // the EIP-4844 can still fit in the block + if let Some(blob_tx) = tx.transaction.as_eip4844() { + let tx_blob_gas = blob_tx.blob_gas(); + if sum_blob_gas_used + tx_blob_gas > MAX_DATA_GAS_PER_BLOCK { + // we can't fit this _blob_ transaction into the block, so we mark it as + // invalid, which removes its dependent transactions from + // the iterator. This is similar to the gas limit condition + // for regular transactions above. + trace!(target: "payload_builder", tx=?tx.hash, ?sum_blob_gas_used, ?tx_blob_gas, "skipping blob transaction because it would exceed the max data gas per block"); + best_txs.mark_invalid(&pool_tx); + continue + } + } + + // Configure the environment for the block. + let env = Env { + cfg: initialized_cfg.clone(), + block: initialized_block_env.clone(), + tx: tx_env_with_recovered(&tx), + }; + + let mut evm = revm::EVM::with_env(env); + evm.database(&mut db); + + let ResultAndState { result, state } = match evm.transact() { + Ok(res) => res, + Err(err) => { + match err { + EVMError::Transaction(err) => { + if matches!(err, InvalidTransaction::NonceTooLow { .. }) { + // if the nonce is too low, we can skip this transaction + trace!(target: "payload_builder", ?err, ?tx, "skipping nonce too low transaction"); + } else { + // if the transaction is invalid, we can skip it and all of its + // descendants + trace!(target: "payload_builder", ?err, ?tx, "skipping invalid transaction and its descendants"); + best_txs.mark_invalid(&pool_tx); + } + + continue + } + err => { + // this is an error that we should treat as fatal for this attempt + return Err(PayloadBuilderError::EvmExecutionError(err)) + } + } + } + }; + + // commit changes + db.commit(state); + + // add to the total blob gas used if the transaction successfully executed + if let Some(blob_tx) = tx.transaction.as_eip4844() { + let tx_blob_gas = blob_tx.blob_gas(); + sum_blob_gas_used += tx_blob_gas; + + // if we've reached the max data gas per block, we can skip blob txs entirely + if sum_blob_gas_used == MAX_DATA_GAS_PER_BLOCK { + best_txs.skip_blobs(); + } + } + + 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: tx.tx_type(), + success: result.is_success(), + cumulative_gas_used, + logs: result.logs().into_iter().map(into_reth_log).collect(), + })); + + // update add to total fees + let miner_fee = tx + .effective_tip_per_gas(Some(base_fee)) + .expect("fee is always valid; execution succeeded"); + total_fees += U256::from(miner_fee) * U256::from(gas_used); + + // append transaction to the list of executed transactions + executed_txs.push(tx.into_signed()); + } + + // check if we have a better block + if !is_better_payload(best_payload.as_deref(), 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.timestamp, attributes.withdrawals)?; + + // merge all transitions into bundle state, this would apply the withdrawal balance changes + // and 4788 contract call + db.merge_transitions(BundleRetention::PlainState); + + let bundle = BundleStateWithReceipts::new( + db.take_bundle(), + Receipts::from_vec(vec![receipts]), + block_number, + ); + let receipts_root = bundle.receipts_root_slow(block_number).expect("Number is in range"); + let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); + + // calculate the state root + let state_root = state_provider.state_root(&bundle)?; + + // create the block header + let transactions_root = proofs::calculate_transaction_root(&executed_txs); + + // initialize empty blob sidecars at first. If cancun is active then this will + let mut 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.timestamp) { + // grab the blob sidecars from the executed txs + blob_sidecars = pool.get_all_blobs_exact( + executed_txs.iter().filter(|tx| tx.is_eip4844()).map(|tx| tx.hash).collect(), + )?; + + 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(sum_blob_gas_used); + } + + 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.timestamp, + mix_hash: 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.parent_beacon_block_root, + blob_gas_used, + excess_blob_gas, + }; + + // seal the block + let block = Block { header, body: executed_txs, ommers: vec![], withdrawals }; + + let sealed_block = block.seal_slow(); + debug!(target: "payload_builder", ?sealed_block, "sealed built block"); + + let mut payload = BuiltPayload::new(attributes.id, sealed_block, total_fees); + + // extend the payload with the blob sidecars from the executed txs + payload.extend_sidecars(blob_sidecars); + + Ok(BuildOutcome::Better { payload, cached_reads }) + } +} diff --git a/crates/payload/optimism/Cargo.toml b/crates/payload/optimism/Cargo.toml new file mode 100644 index 000000000..388cb4c40 --- /dev/null +++ b/crates/payload/optimism/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "reth-optimism-payload-builder" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "A payload builder for op-reth that builds optimistic payloads." + +[dependencies] +# reth +reth-primitives.workspace = true +reth-revm.workspace = true +reth-transaction-pool.workspace = true +reth-provider.workspace = true +reth-payload-builder.workspace = true +reth-basic-payload-builder.workspace = true + +# ethereum +revm.workspace = true + +# misc +tracing.workspace = true + +[features] +# This is a workaround for reth-cli crate to allow this as mandatory dependency without breaking the build even if unused. +# This makes managing features and testing workspace easier because clippy always builds all members if --workspace is provided +optimism = [ + "reth-primitives/optimism", + "reth-revm/optimism", + "reth-transaction-pool/optimism", + "reth-provider/optimism", + "reth-payload-builder/optimism", +] \ No newline at end of file diff --git a/crates/payload/optimism/src/lib.rs b/crates/payload/optimism/src/lib.rs new file mode 100644 index 000000000..11b36e987 --- /dev/null +++ b/crates/payload/optimism/src/lib.rs @@ -0,0 +1,407 @@ +//! Optimism's payload builder implementation. + +#![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/" +)] +#![warn(missing_debug_implementations, missing_docs, unreachable_pub, rustdoc::all)] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[cfg(feature = "optimism")] +pub use builder::*; + +#[cfg(feature = "optimism")] +mod builder { + use reth_basic_payload_builder::*; + use reth_payload_builder::{ + error::{OptimismPayloadBuilderError, PayloadBuilderError}, + BuiltPayload, + }; + use reth_primitives::{ + constants::BEACON_NONCE, + proofs, + revm::{compat::into_reth_log, env::tx_env_with_recovered}, + Block, Hardfork, Header, IntoRecoveredTransaction, Receipt, Receipts, + EMPTY_OMMER_ROOT_HASH, U256, + }; + use reth_provider::{BundleStateWithReceipts, StateProviderFactory}; + use reth_revm::database::StateProviderDatabase; + use reth_transaction_pool::TransactionPool; + use revm::{ + db::states::bundle_state::BundleRetention, + primitives::{EVMError, Env, InvalidTransaction, ResultAndState}, + DatabaseCommit, State, + }; + use std::sync::Arc; + use tracing::{debug, trace}; + + /// Optimism's payload builder + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + #[non_exhaustive] + pub struct OptimismPayloadBuilder { + /// The rollup's compute pending block configuration option. + // TODO(clabby): Implement this feature. + compute_pending_block: bool, + } + + impl OptimismPayloadBuilder { + /// Sets the rollup's compute pending block configuration option. + pub fn set_compute_pending_block(mut self, compute_pending_block: bool) -> Self { + self.compute_pending_block = compute_pending_block; + self + } + + /// Enables the rollup's compute pending block configuration option. + pub fn compute_pending_block(self) -> Self { + self.set_compute_pending_block(true) + } + + /// Returns the rollup's compute pending block configuration option. + pub fn is_compute_pending_block(&self) -> bool { + self.compute_pending_block + } + } + + /// Implementation of the [PayloadBuilder] trait for [OptimismPayloadBuilder]. + impl PayloadBuilder for OptimismPayloadBuilder + where + Client: StateProviderFactory, + Pool: TransactionPool, + { + fn try_build( + &self, + args: BuildArguments, + ) -> Result { + optimism_payload_builder(args, self.compute_pending_block) + } + + fn on_missing_payload( + &self, + args: BuildArguments, + ) -> Option> { + // In Optimism, the PayloadAttributes can specify a `no_tx_pool` option that implies we + // should not pull transactions from the tx pool. In this case, we build the payload + // upfront with the list of transactions sent in the attributes without caring about + // the results of the polling job, if a best payload has not already been built. + if args.config.attributes.optimism_payload_attributes.no_tx_pool { + if let Ok(BuildOutcome::Better { payload, .. }) = self.try_build(args) { + trace!(target: "payload_builder", "[OPTIMISM] Forced best payload"); + let payload = Arc::new(payload); + return Some(payload) + } + } + + None + } + } + + /// 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] + pub(crate) fn optimism_payload_builder( + args: BuildArguments, + _compute_pending_block: bool, + ) -> Result + where + 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 extra_data = config.extra_data(); + let PayloadConfig { + initialized_block_env, + initialized_cfg, + parent_block, + attributes, + chain_spec, + .. + } = config; + + debug!(target: "payload_builder", id=%attributes.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 + .optimism_payload_attributes + .gas_limit + .unwrap_or(initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX)); + let base_fee = initialized_block_env.basefee.to::(); + + let mut executed_txs = Vec::new(); + let mut best_txs = pool.best_transactions_with_base_fee(base_fee); + + let mut total_fees = U256::ZERO; + + let block_number = initialized_block_env.number.to::(); + + let is_regolith = + chain_spec.is_fork_active_at_timestamp(Hardfork::Regolith, attributes.timestamp); + + // 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_revm::optimism::ensure_create2_deployer( + chain_spec.clone(), + attributes.timestamp, + &mut db, + ) + .map_err(|_| { + PayloadBuilderError::Optimism(OptimismPayloadBuilderError::ForceCreate2DeployerFail) + })?; + + let mut receipts = Vec::new(); + for sequencer_tx in attributes.optimism_payload_attributes.transactions { + // Check if the job was cancelled, if so we can exit early. + if cancel.is_cancelled() { + return Ok(BuildOutcome::Cancelled) + } + + // Convert the transaction to a [TransactionSignedEcRecovered]. This is + // purely for the purposes of utilizing the [tx_env_with_recovered] 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.clone().try_into_ecrecovered().map_err(|_| { + PayloadBuilderError::Optimism( + 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::Optimism(OptimismPayloadBuilderError::AccountLoadFailed( + sequencer_tx.signer(), + )) + })?; + + // Configure the environment for the block. + let env = Env { + cfg: initialized_cfg.clone(), + block: initialized_block_env.clone(), + tx: tx_env_with_recovered(&sequencer_tx), + }; + + let mut evm = revm::EVM::with_env(env); + evm.database(&mut db); + + let ResultAndState { result, state } = match evm.transact() { + Ok(res) => res, + Err(err) => { + match err { + EVMError::Transaction(err) => { + trace!(target: "optimism_payload_builder", ?err, ?sequencer_tx, "Error in sequencer transaction, skipping."); + continue + } + err => { + // this is an error that we should treat as fatal for this attempt + return Err(PayloadBuilderError::EvmExecutionError(err)) + } + } + } + }; + + // 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.logs().into_iter().map(into_reth_log).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(Hardfork::Canyon, attributes.timestamp) + .then_some(1), + })); + + // append transaction to the list of executed transactions + executed_txs.push(sequencer_tx.into_signed()); + } + + if !attributes.optimism_payload_attributes.no_tx_pool { + 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 { + // 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 + } + + // check if the job was cancelled, if so we can exit early + if cancel.is_cancelled() { + return Ok(BuildOutcome::Cancelled) + } + + // convert tx to a signed transaction + let tx = pool_tx.to_recovered_transaction(); + + // Configure the environment for the block. + let env = Env { + cfg: initialized_cfg.clone(), + block: initialized_block_env.clone(), + tx: tx_env_with_recovered(&tx), + }; + + let mut evm = revm::EVM::with_env(env); + evm.database(&mut db); + + let ResultAndState { result, state } = match evm.transact() { + Ok(res) => res, + Err(err) => { + match err { + EVMError::Transaction(err) => { + if matches!(err, InvalidTransaction::NonceTooLow { .. }) { + // if the nonce is too low, we can skip this transaction + trace!(target: "payload_builder", ?err, ?tx, "skipping nonce too low transaction"); + } else { + // if the transaction is invalid, we can skip it and all of its + // descendants + trace!(target: "payload_builder", ?err, ?tx, "skipping invalid transaction and its descendants"); + best_txs.mark_invalid(&pool_tx); + } + + continue + } + err => { + // this is an error that we should treat as fatal for this attempt + return Err(PayloadBuilderError::EvmExecutionError(err)) + } + } + } + }; + + // 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: tx.tx_type(), + success: result.is_success(), + cumulative_gas_used, + logs: result.logs().into_iter().map(into_reth_log).collect(), + deposit_nonce: None, + deposit_receipt_version: None, + })); + + // update add to total fees + let miner_fee = tx + .effective_tip_per_gas(Some(base_fee)) + .expect("fee is always valid; execution succeeded"); + total_fees += U256::from(miner_fee) * U256::from(gas_used); + + // append transaction to the list of executed transactions + executed_txs.push(tx.into_signed()); + } + } + + // check if we have a better block + if !is_better_payload(best_payload.as_deref(), 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.timestamp, attributes.withdrawals)?; + + // merge all transitions into bundle state, this would apply the withdrawal balance changes + // and 4788 contract call + db.merge_transitions(BundleRetention::PlainState); + + let bundle = BundleStateWithReceipts::new( + db.take_bundle(), + Receipts::from_vec(vec![receipts]), + block_number, + ); + let receipts_root = bundle + .receipts_root_slow(block_number, chain_spec.as_ref(), attributes.timestamp) + .expect("Number is in range"); + let logs_bloom = bundle.block_logs_bloom(block_number).expect("Number is in range"); + + // calculate the state root + let state_root = state_provider.state_root(&bundle)?; + + // create the block header + let transactions_root = proofs::calculate_transaction_root(&executed_txs); + + // Cancun is not yet active on Optimism chains. + let blob_sidecars = Vec::new(); + let excess_blob_gas = None; + let blob_gas_used = None; + + 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.timestamp, + mix_hash: 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.parent_beacon_block_root, + blob_gas_used, + excess_blob_gas, + }; + + // seal the block + let block = Block { header, body: executed_txs, ommers: vec![], withdrawals }; + + let sealed_block = block.seal_slow(); + debug!(target: "payload_builder", ?sealed_block, "sealed built block"); + + let mut payload = BuiltPayload::new(attributes.id, sealed_block, total_fees); + + // extend the payload with the blob sidecars from the executed txs + payload.extend_sidecars(blob_sidecars); + + Ok(BuildOutcome::Better { payload, cached_reads }) + } +}