reth_ethereum_payload_builder/
lib.rs

1//! A basic Ethereum payload builder implementation.
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/SeismicSystems/seismic-reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
10#![allow(clippy::useless_let_if_seq)]
11
12use alloy_consensus::{Header, EMPTY_OMMER_ROOT_HASH};
13use alloy_eips::{
14    eip4844::MAX_DATA_GAS_PER_BLOCK, eip7002::WITHDRAWAL_REQUEST_TYPE,
15    eip7251::CONSOLIDATION_REQUEST_TYPE, eip7685::Requests, merge::BEACON_NONCE,
16};
17use alloy_primitives::U256;
18use reth_basic_payload_builder::{
19    commit_withdrawals, is_better_payload, BuildArguments, BuildOutcome, PayloadBuilder,
20    PayloadConfig,
21};
22use reth_chain_state::ExecutedBlock;
23use reth_chainspec::{ChainSpec, ChainSpecProvider};
24use reth_errors::RethError;
25use reth_evm::{system_calls::SystemCaller, ConfigureEvm, NextBlockEnvAttributes};
26use reth_evm_ethereum::{eip6110::parse_deposits_from_receipts, EthEvmConfig};
27use reth_execution_types::ExecutionOutcome;
28use reth_payload_builder::{EthBuiltPayload, EthPayloadBuilderAttributes};
29use reth_payload_builder_primitives::PayloadBuilderError;
30use reth_payload_primitives::PayloadBuilderAttributes;
31use reth_primitives::{
32    proofs::{self},
33    Block, BlockBody, BlockExt, EthereumHardforks, InvalidTransactionError, Receipt,
34    TransactionSigned,
35};
36use reth_revm::database::StateProviderDatabase;
37use reth_transaction_pool::{
38    error::InvalidPoolTransactionError, noop::NoopTransactionPool, BestTransactions,
39    BestTransactionsAttributes, PoolTransaction, TransactionPool, ValidPoolTransaction,
40};
41use revm::{
42    db::{states::bundle_state::BundleRetention, State},
43    primitives::{
44        calc_excess_blob_gas, BlockEnv, CfgEnvWithHandlerCfg, EVMError, EnvWithHandlerCfg,
45        InvalidTransaction, ResultAndState, TxEnv,
46    },
47    DatabaseCommit,
48};
49use std::sync::Arc;
50use tracing::{debug, trace, warn};
51
52mod config;
53pub use config::*;
54use reth_storage_api::StateProviderFactory;
55
56type BestTransactionsIter<Pool> = Box<
57    dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
58>;
59
60/// Ethereum payload builder
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct EthereumPayloadBuilder<EvmConfig = EthEvmConfig> {
63    /// The type responsible for creating the evm.
64    evm_config: EvmConfig,
65    /// Payload builder configuration.
66    builder_config: EthereumBuilderConfig,
67}
68
69impl<EvmConfig> EthereumPayloadBuilder<EvmConfig> {
70    /// `EthereumPayloadBuilder` constructor.
71    pub const fn new(evm_config: EvmConfig, builder_config: EthereumBuilderConfig) -> Self {
72        Self { evm_config, builder_config }
73    }
74}
75
76impl<EvmConfig> EthereumPayloadBuilder<EvmConfig>
77where
78    EvmConfig: ConfigureEvm<Header = Header>,
79{
80    /// Returns the configured [`CfgEnvWithHandlerCfg`] and [`BlockEnv`] for the targeted payload
81    /// (that has the `parent` as its parent).
82    fn cfg_and_block_env(
83        &self,
84        config: &PayloadConfig<EthPayloadBuilderAttributes>,
85        parent: &Header,
86    ) -> Result<(CfgEnvWithHandlerCfg, BlockEnv), EvmConfig::Error> {
87        let next_attributes = NextBlockEnvAttributes {
88            timestamp: config.attributes.timestamp(),
89            suggested_fee_recipient: config.attributes.suggested_fee_recipient(),
90            prev_randao: config.attributes.prev_randao(),
91        };
92        self.evm_config.next_cfg_and_block_env(parent, next_attributes)
93    }
94}
95
96// Default implementation of [PayloadBuilder] for unit type
97impl<EvmConfig, Pool, Client> PayloadBuilder<Pool, Client> for EthereumPayloadBuilder<EvmConfig>
98where
99    EvmConfig: ConfigureEvm<Header = Header, Transaction = TransactionSigned>,
100    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
101    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
102{
103    type Attributes = EthPayloadBuilderAttributes;
104    type BuiltPayload = EthBuiltPayload;
105
106    fn try_build(
107        &self,
108        args: BuildArguments<Pool, Client, EthPayloadBuilderAttributes, EthBuiltPayload>,
109    ) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError> {
110        let (cfg_env, block_env) = self
111            .cfg_and_block_env(&args.config, &args.config.parent_header)
112            .map_err(PayloadBuilderError::other)?;
113
114        let pool = args.pool.clone();
115        default_ethereum_payload(
116            self.evm_config.clone(),
117            self.builder_config.clone(),
118            args,
119            cfg_env,
120            block_env,
121            |attributes| pool.best_transactions_with_attributes(attributes),
122        )
123    }
124
125    fn build_empty_payload(
126        &self,
127        client: &Client,
128        config: PayloadConfig<Self::Attributes>,
129    ) -> Result<EthBuiltPayload, PayloadBuilderError> {
130        let args = BuildArguments::new(
131            client,
132            // we use defaults here because for the empty payload we don't need to execute anything
133            NoopTransactionPool::default(),
134            Default::default(),
135            config,
136            Default::default(),
137            None,
138        );
139
140        let (cfg_env, block_env) = self
141            .cfg_and_block_env(&args.config, &args.config.parent_header)
142            .map_err(PayloadBuilderError::other)?;
143
144        let pool = args.pool.clone();
145
146        default_ethereum_payload(
147            self.evm_config.clone(),
148            self.builder_config.clone(),
149            args,
150            cfg_env,
151            block_env,
152            |attributes| pool.best_transactions_with_attributes(attributes),
153        )?
154        .into_payload()
155        .ok_or_else(|| PayloadBuilderError::MissingPayload)
156    }
157}
158
159/// Constructs an Ethereum transaction payload using the best transactions from the pool.
160///
161/// Given build arguments including an Ethereum client, transaction pool,
162/// and configuration, this function creates a transaction payload. Returns
163/// a result indicating success with the payload or an error in case of failure.
164#[inline]
165pub fn default_ethereum_payload<EvmConfig, Pool, Client, F>(
166    evm_config: EvmConfig,
167    builder_config: EthereumBuilderConfig,
168    args: BuildArguments<Pool, Client, EthPayloadBuilderAttributes, EthBuiltPayload>,
169    initialized_cfg: CfgEnvWithHandlerCfg,
170    initialized_block_env: BlockEnv,
171    best_txs: F,
172) -> Result<BuildOutcome<EthBuiltPayload>, PayloadBuilderError>
173where
174    EvmConfig: ConfigureEvm<Header = Header, Transaction = TransactionSigned>,
175    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
176    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
177    F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
178{
179    let BuildArguments { client, pool, mut cached_reads, config, cancel, best_payload } = args;
180
181    let chain_spec = client.chain_spec();
182    let state_provider = client.state_by_block_hash(config.parent_header.hash())?;
183    let state = StateProviderDatabase::new(state_provider);
184    let mut db =
185        State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
186    let PayloadConfig { parent_header, attributes } = config;
187
188    debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
189    let mut cumulative_gas_used = 0;
190    let mut sum_blob_gas_used = 0;
191    let block_gas_limit: u64 = initialized_block_env.gas_limit.to::<u64>();
192    let base_fee = initialized_block_env.basefee.to::<u64>();
193
194    let mut executed_txs = Vec::new();
195    let mut executed_senders = Vec::new();
196
197    let mut best_txs = best_txs(BestTransactionsAttributes::new(
198        base_fee,
199        initialized_block_env.get_blob_gasprice().map(|gasprice| gasprice as u64),
200    ));
201    let mut total_fees = U256::ZERO;
202
203    let block_number = initialized_block_env.number.to::<u64>();
204
205    let mut system_caller = SystemCaller::new(evm_config.clone(), chain_spec.clone());
206
207    // apply eip-4788 pre block contract call
208    system_caller
209        .pre_block_beacon_root_contract_call(
210            &mut db,
211            &initialized_cfg,
212            &initialized_block_env,
213            attributes.parent_beacon_block_root,
214        )
215        .map_err(|err| {
216            warn!(target: "payload_builder",
217                parent_hash=%parent_header.hash(),
218                %err,
219                "failed to apply beacon root contract call for payload"
220            );
221            PayloadBuilderError::Internal(err.into())
222        })?;
223
224    // apply eip-2935 blockhashes update
225    system_caller.pre_block_blockhashes_contract_call(
226        &mut db,
227        &initialized_cfg,
228        &initialized_block_env,
229        parent_header.hash(),
230    )
231    .map_err(|err| {
232        warn!(target: "payload_builder", parent_hash=%parent_header.hash(), %err, "failed to update parent header blockhashes for payload");
233        PayloadBuilderError::Internal(err.into())
234    })?;
235
236    let env = EnvWithHandlerCfg::new_with_cfg_env(
237        initialized_cfg.clone(),
238        initialized_block_env.clone(),
239        TxEnv::default(),
240    );
241    let mut evm = evm_config.evm_with_env(&mut db, env);
242
243    let mut receipts = Vec::new();
244    while let Some(pool_tx) = best_txs.next() {
245        // ensure we still have capacity for this transaction
246        if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
247            // we can't fit this transaction into the block, so we need to mark it as invalid
248            // which also removes all dependent transaction from the iterator before we can
249            // continue
250            best_txs.mark_invalid(
251                &pool_tx,
252                InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
253            );
254            continue
255        }
256
257        // check if the job was cancelled, if so we can exit early
258        if cancel.is_cancelled() {
259            return Ok(BuildOutcome::Cancelled)
260        }
261
262        // convert tx to a signed transaction
263        let tx = pool_tx.to_consensus();
264
265        // There's only limited amount of blob space available per block, so we need to check if
266        // the EIP-4844 can still fit in the block
267        if let Some(blob_tx) = tx.transaction.as_eip4844() {
268            let tx_blob_gas = blob_tx.blob_gas();
269            if sum_blob_gas_used + tx_blob_gas > MAX_DATA_GAS_PER_BLOCK {
270                // we can't fit this _blob_ transaction into the block, so we mark it as
271                // invalid, which removes its dependent transactions from
272                // the iterator. This is similar to the gas limit condition
273                // for regular transactions above.
274                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");
275                best_txs.mark_invalid(
276                    &pool_tx,
277                    InvalidPoolTransactionError::ExceedsGasLimit(
278                        tx_blob_gas,
279                        MAX_DATA_GAS_PER_BLOCK,
280                    ),
281                );
282                continue
283            }
284        }
285
286        // Configure the environment for the tx.
287        *evm.tx_mut() = evm_config.tx_env(tx.as_signed(), tx.signer()).map_err(|err| {
288            warn!(target: "payload_builder", %err, ?tx, "failed to configure tx environment for payload");
289            PayloadBuilderError::EvmExecutionError(err.map_db_err(|err|err.into()))
290        })?;
291
292        let ResultAndState { result, state } = match evm.transact() {
293            Ok(res) => res,
294            Err(err) => {
295                match err {
296                    EVMError::Transaction(err) => {
297                        if matches!(err, InvalidTransaction::NonceTooLow { .. }) {
298                            // if the nonce is too low, we can skip this transaction
299                            trace!(target: "payload_builder", %err, ?tx, "skipping nonce too low transaction");
300                        } else {
301                            // if the transaction is invalid, we can skip it and all of its
302                            // descendants
303                            trace!(target: "payload_builder", %err, ?tx, "skipping invalid transaction and its descendants");
304                            best_txs.mark_invalid(
305                                &pool_tx,
306                                InvalidPoolTransactionError::Consensus(
307                                    InvalidTransactionError::TxTypeNotSupported,
308                                ),
309                            );
310                        }
311
312                        continue
313                    }
314                    err => {
315                        // this is an error that we should treat as fatal for this attempt
316                        return Err(PayloadBuilderError::EvmExecutionError(err))
317                    }
318                }
319            }
320        };
321
322        // commit changes
323        evm.db_mut().commit(state);
324
325        // add to the total blob gas used if the transaction successfully executed
326        if let Some(blob_tx) = tx.transaction.as_eip4844() {
327            let tx_blob_gas = blob_tx.blob_gas();
328            sum_blob_gas_used += tx_blob_gas;
329
330            // if we've reached the max data gas per block, we can skip blob txs entirely
331            if sum_blob_gas_used == MAX_DATA_GAS_PER_BLOCK {
332                best_txs.skip_blobs();
333            }
334        }
335
336        let gas_used = result.gas_used();
337
338        // add gas used by the transaction to cumulative gas used, before creating the receipt
339        cumulative_gas_used += gas_used;
340
341        // Push transaction changeset and calculate header bloom filter for receipt.
342        #[allow(clippy::needless_update)] // side-effect of optimism fields
343        receipts.push(Some(Receipt {
344            tx_type: tx.tx_type(),
345            success: result.is_success(),
346            cumulative_gas_used,
347            logs: result.into_logs().into_iter().map(Into::into).collect(),
348            ..Default::default()
349        }));
350
351        // update add to total fees
352        let miner_fee = tx
353            .effective_tip_per_gas(Some(base_fee))
354            .expect("fee is always valid; execution succeeded");
355        total_fees += U256::from(miner_fee) * U256::from(gas_used);
356
357        // append sender and transaction to the respective lists
358        executed_senders.push(tx.signer());
359        executed_txs.push(tx.into_signed());
360    }
361
362    // Release db
363    drop(evm);
364
365    // check if we have a better block
366    if !is_better_payload(best_payload.as_ref(), total_fees) {
367        // can skip building the block
368        return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
369    }
370
371    // calculate the requests and the requests root
372    let requests = if chain_spec.is_prague_active_at_timestamp(attributes.timestamp) {
373        let deposit_requests = parse_deposits_from_receipts(&chain_spec, receipts.iter().flatten())
374            .map_err(|err| PayloadBuilderError::Internal(RethError::Execution(err.into())))?;
375        let withdrawal_requests = system_caller
376            .post_block_withdrawal_requests_contract_call(
377                &mut db,
378                &initialized_cfg,
379                &initialized_block_env,
380            )
381            .map_err(|err| PayloadBuilderError::Internal(err.into()))?;
382        let consolidation_requests = system_caller
383            .post_block_consolidation_requests_contract_call(
384                &mut db,
385                &initialized_cfg,
386                &initialized_block_env,
387            )
388            .map_err(|err| PayloadBuilderError::Internal(err.into()))?;
389
390        let mut requests = Requests::default();
391
392        if !deposit_requests.is_empty() {
393            requests.push_request(core::iter::once(0).chain(deposit_requests).collect());
394        }
395
396        if !withdrawal_requests.is_empty() {
397            requests.push_request(
398                core::iter::once(WITHDRAWAL_REQUEST_TYPE).chain(withdrawal_requests).collect(),
399            );
400        }
401
402        if !consolidation_requests.is_empty() {
403            requests.push_request(
404                core::iter::once(CONSOLIDATION_REQUEST_TYPE)
405                    .chain(consolidation_requests)
406                    .collect(),
407            );
408        }
409
410        Some(requests)
411    } else {
412        None
413    };
414
415    let withdrawals_root =
416        commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, &attributes.withdrawals)?;
417
418    // merge all transitions into bundle state, this would apply the withdrawal balance changes
419    // and 4788 contract call
420    db.merge_transitions(BundleRetention::Reverts);
421
422    let requests_hash = requests.as_ref().map(|requests| requests.requests_hash());
423    let execution_outcome = ExecutionOutcome::new(
424        db.take_bundle(),
425        vec![receipts].into(),
426        block_number,
427        vec![requests.clone().unwrap_or_default()],
428    );
429    let receipts_root =
430        execution_outcome.ethereum_receipts_root(block_number).expect("Number is in range");
431    let logs_bloom = execution_outcome.block_logs_bloom(block_number).expect("Number is in range");
432
433    // calculate the state root
434    let hashed_state = db.database.db.hashed_post_state(execution_outcome.state());
435    let (state_root, trie_output) = {
436        db.database.inner().state_root_with_updates(hashed_state.clone()).inspect_err(|err| {
437            warn!(target: "payload_builder",
438                parent_hash=%parent_header.hash(),
439                %err,
440                "failed to calculate state root for payload"
441            );
442        })?
443    };
444
445    // create the block header
446    let transactions_root = proofs::calculate_transaction_root(&executed_txs);
447
448    // initialize empty blob sidecars at first. If cancun is active then this will
449    let mut blob_sidecars = Vec::new();
450    let mut excess_blob_gas = None;
451    let mut blob_gas_used = None;
452
453    // only determine cancun fields when active
454    if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp) {
455        // grab the blob sidecars from the executed txs
456        blob_sidecars = pool
457            .get_all_blobs_exact(
458                executed_txs.iter().filter(|tx| tx.is_eip4844()).map(|tx| tx.hash()).collect(),
459            )
460            .map_err(PayloadBuilderError::other)?;
461
462        excess_blob_gas = if chain_spec.is_cancun_active_at_timestamp(parent_header.timestamp) {
463            let parent_excess_blob_gas = parent_header.excess_blob_gas.unwrap_or_default();
464            let parent_blob_gas_used = parent_header.blob_gas_used.unwrap_or_default();
465            Some(calc_excess_blob_gas(parent_excess_blob_gas, parent_blob_gas_used))
466        } else {
467            // for the first post-fork block, both parent.blob_gas_used and
468            // parent.excess_blob_gas are evaluated as 0
469            Some(calc_excess_blob_gas(0, 0))
470        };
471
472        blob_gas_used = Some(sum_blob_gas_used);
473    }
474
475    let header = Header {
476        parent_hash: parent_header.hash(),
477        ommers_hash: EMPTY_OMMER_ROOT_HASH,
478        beneficiary: initialized_block_env.coinbase,
479        state_root,
480        transactions_root,
481        receipts_root,
482        withdrawals_root,
483        logs_bloom,
484        timestamp: attributes.timestamp,
485        mix_hash: attributes.prev_randao,
486        nonce: BEACON_NONCE.into(),
487        base_fee_per_gas: Some(base_fee),
488        number: parent_header.number + 1,
489        gas_limit: block_gas_limit,
490        difficulty: U256::ZERO,
491        gas_used: cumulative_gas_used,
492        extra_data: builder_config.extra_data,
493        parent_beacon_block_root: attributes.parent_beacon_block_root,
494        blob_gas_used: blob_gas_used.map(Into::into),
495        excess_blob_gas: excess_blob_gas.map(Into::into),
496        requests_hash,
497        target_blobs_per_block: None,
498    };
499
500    let withdrawals = chain_spec
501        .is_shanghai_active_at_timestamp(attributes.timestamp)
502        .then(|| attributes.withdrawals.clone());
503
504    // seal the block
505    let block = Block {
506        header,
507        body: BlockBody { transactions: executed_txs, ommers: vec![], withdrawals },
508    };
509
510    let sealed_block = Arc::new(block.seal_slow());
511    debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.header, "sealed built block");
512
513    // create the executed block data
514    let executed = ExecutedBlock {
515        block: sealed_block.clone(),
516        senders: Arc::new(executed_senders),
517        execution_output: Arc::new(execution_outcome),
518        hashed_state: Arc::new(hashed_state),
519        trie: Arc::new(trie_output),
520    };
521
522    let mut payload =
523        EthBuiltPayload::new(attributes.id, sealed_block, total_fees, Some(executed), requests);
524
525    // extend the payload with the blob sidecars from the executed txs
526    payload.extend_sidecars(blob_sidecars.into_iter().map(Arc::unwrap_or_clone));
527
528    Ok(BuildOutcome::Better { payload, cached_reads })
529}