reth_seismic_payload_builder/
builder.rs

1//! A basic Seismic payload builder implementation.
2
3use alloy_consensus::{Transaction, Typed2718};
4use alloy_primitives::U256;
5use reth_basic_payload_builder::{
6    is_better_payload, BuildArguments, BuildOutcome, MissingPayloadBehaviour, PayloadBuilder,
7    PayloadConfig,
8};
9use reth_chainspec::{ChainSpec, ChainSpecProvider, EthereumHardforks};
10use reth_errors::{BlockExecutionError, BlockValidationError};
11use reth_evm::{
12    execute::{BlockBuilder, BlockBuilderOutcome},
13    ConfigureEvm, Evm, NextBlockEnvAttributes,
14};
15use reth_payload_builder::{BlobSidecars, EthBuiltPayload, EthPayloadBuilderAttributes};
16use reth_payload_builder_primitives::PayloadBuilderError;
17use reth_payload_primitives::PayloadBuilderAttributes;
18use reth_primitives_traits::SignedTransaction;
19use reth_revm::{database::StateProviderDatabase, db::State};
20use reth_seismic_evm::SeismicEvmConfig;
21use reth_seismic_primitives::{SeismicPrimitives, SeismicTransactionSigned};
22use reth_storage_api::StateProviderFactory;
23use reth_transaction_pool::{
24    error::InvalidPoolTransactionError, BestTransactions, BestTransactionsAttributes,
25    PoolTransaction, TransactionPool, ValidPoolTransaction,
26};
27use revm::context_interface::Block as _;
28use seismic_enclave::EnclaveClientBuilder;
29use std::sync::Arc;
30use tracing::{debug, trace, warn};
31
32use reth_primitives_traits::transaction::error::InvalidTransactionError;
33
34type BestTransactionsIter<Pool> = Box<
35    dyn BestTransactions<Item = Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>,
36>;
37
38use super::SeismicBuilderConfig;
39
40/// Seismic payload builder
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct SeismicPayloadBuilder<Pool, Client, EvmConfig = SeismicEvmConfig<EnclaveClientBuilder>> {
43    /// Client providing access to node state.
44    client: Client,
45    /// Transaction pool.
46    pool: Pool,
47    /// The type responsible for creating the evm.
48    evm_config: EvmConfig,
49    /// Payload builder configuration.
50    builder_config: SeismicBuilderConfig,
51}
52
53impl<Pool, Client, EvmConfig> SeismicPayloadBuilder<Pool, Client, EvmConfig> {
54    /// [`SeismicPayloadBuilder`] constructor.
55    pub const fn new(
56        client: Client,
57        pool: Pool,
58        evm_config: EvmConfig,
59        builder_config: SeismicBuilderConfig,
60    ) -> Self {
61        Self { client, pool, evm_config, builder_config }
62    }
63}
64
65// Default implementation of [`PayloadBuilder`] for unit type
66impl<Pool, Client, EvmConfig> PayloadBuilder for SeismicPayloadBuilder<Pool, Client, EvmConfig>
67where
68    EvmConfig:
69        ConfigureEvm<Primitives = SeismicPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
70    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec> + Clone,
71    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = SeismicTransactionSigned>>,
72{
73    type Attributes = EthPayloadBuilderAttributes;
74    type BuiltPayload = EthBuiltPayload<SeismicPrimitives>;
75
76    fn try_build(
77        &self,
78        args: BuildArguments<EthPayloadBuilderAttributes, Self::BuiltPayload>,
79    ) -> Result<BuildOutcome<EthBuiltPayload<SeismicPrimitives>>, PayloadBuilderError> {
80        default_seismic_payload(
81            self.evm_config.clone(),
82            self.client.clone(),
83            self.pool.clone(),
84            self.builder_config.clone(),
85            args,
86            |attributes| self.pool.best_transactions_with_attributes(attributes),
87        )
88    }
89
90    fn on_missing_payload(
91        &self,
92        _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
93    ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
94        if self.builder_config.await_payload_on_missing {
95            MissingPayloadBehaviour::AwaitInProgress
96        } else {
97            MissingPayloadBehaviour::RaceEmptyPayload
98        }
99    }
100
101    fn build_empty_payload(
102        &self,
103        config: PayloadConfig<Self::Attributes>,
104    ) -> Result<Self::BuiltPayload, PayloadBuilderError> {
105        let args = BuildArguments::new(Default::default(), config, Default::default(), None);
106
107        default_seismic_payload(
108            self.evm_config.clone(),
109            self.client.clone(),
110            self.pool.clone(),
111            self.builder_config.clone(),
112            args,
113            |attributes| self.pool.best_transactions_with_attributes(attributes),
114        )?
115        .into_payload()
116        .ok_or_else(|| PayloadBuilderError::MissingPayload)
117    }
118}
119
120/// Constructs an Seismic transaction payload using the best transactions from the pool.
121///
122/// Given build arguments including an Seismic client, transaction pool,
123/// and configuration, this function creates a transaction payload. Returns
124/// a result indicating success with the payload or an error in case of failure.
125#[inline]
126pub fn default_seismic_payload<EvmConfig, Client, Pool, F>(
127    evm_config: EvmConfig,
128    client: Client,
129    pool: Pool,
130    builder_config: SeismicBuilderConfig,
131    args: BuildArguments<EthPayloadBuilderAttributes, EthBuiltPayload<SeismicPrimitives>>,
132    best_txs: F,
133) -> Result<BuildOutcome<EthBuiltPayload<SeismicPrimitives>>, PayloadBuilderError>
134where
135    EvmConfig:
136        ConfigureEvm<Primitives = SeismicPrimitives, NextBlockEnvCtx = NextBlockEnvAttributes>,
137    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = ChainSpec>,
138    Pool: TransactionPool<Transaction: PoolTransaction<Consensus = SeismicTransactionSigned>>,
139    F: FnOnce(BestTransactionsAttributes) -> BestTransactionsIter<Pool>,
140{
141    let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
142    let PayloadConfig { parent_header, attributes } = config;
143
144    let state_provider = client.state_by_block_hash(parent_header.hash())?;
145    let state = StateProviderDatabase::new(&state_provider);
146    let mut db =
147        State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
148
149    let mut builder = evm_config
150        .builder_for_next_block(
151            &mut db,
152            &parent_header,
153            NextBlockEnvAttributes {
154                timestamp: attributes.timestamp(),
155                suggested_fee_recipient: attributes.suggested_fee_recipient(),
156                prev_randao: attributes.prev_randao(),
157                gas_limit: builder_config.gas_limit(parent_header.gas_limit),
158                parent_beacon_block_root: attributes.parent_beacon_block_root(),
159                withdrawals: Some(attributes.withdrawals().clone()),
160            },
161        )
162        .map_err(PayloadBuilderError::other)?;
163
164    let chain_spec = client.chain_spec();
165
166    debug!(target: "payload_builder", id=%attributes.id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
167    let mut cumulative_gas_used = 0;
168    let block_gas_limit: u64 = builder.evm_mut().block().gas_limit;
169    let base_fee = builder.evm_mut().block().basefee;
170
171    let mut best_txs = best_txs(BestTransactionsAttributes::new(
172        base_fee,
173        builder.evm_mut().block().blob_gasprice().map(|gasprice| gasprice as u64),
174    ));
175    let mut total_fees = U256::ZERO;
176
177    builder.apply_pre_execution_changes().map_err(|err| {
178        warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
179        PayloadBuilderError::Internal(err.into())
180    })?;
181
182    while let Some(pool_tx) = best_txs.next() {
183        // ensure we still have capacity for this transaction
184        if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
185            // we can't fit this transaction into the block, so we need to mark it as invalid
186            // which also removes all dependent transaction from the iterator before we can
187            // continue
188            best_txs.mark_invalid(
189                &pool_tx,
190                InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
191            );
192            continue
193        }
194
195        // check if the job was cancelled, if so we can exit early
196        if cancel.is_cancelled() {
197            return Ok(BuildOutcome::Cancelled)
198        }
199
200        // convert tx to a signed transaction
201        let tx = pool_tx.to_consensus();
202        debug!("default_seismic_payload: tx: {:?}", tx);
203
204        let gas_used = match builder.execute_transaction(tx.clone()) {
205            Ok(gas_used) => gas_used,
206            Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
207                error, ..
208            })) => {
209                if error.is_nonce_too_low() {
210                    // if the nonce is too low, we can skip this transaction
211                    trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
212                } else {
213                    // if the transaction is invalid, we can skip it and all of its
214                    // descendants
215                    trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
216                    best_txs.mark_invalid(
217                        &pool_tx,
218                        InvalidPoolTransactionError::Consensus(
219                            InvalidTransactionError::TxTypeNotSupported,
220                        ),
221                    );
222                }
223                continue
224            }
225            // this is an error that we should treat as fatal for this attempt
226            Err(err) => return Err(PayloadBuilderError::evm(err)),
227        };
228
229        // update add to total fees
230        let miner_fee =
231            tx.effective_tip_per_gas(base_fee).expect("fee is always valid; execution succeeded");
232        total_fees += U256::from(miner_fee) * U256::from(gas_used);
233        cumulative_gas_used += gas_used;
234    }
235
236    // check if we have a better block
237    if !is_better_payload(best_payload.as_ref(), total_fees) {
238        // Release db
239        drop(builder);
240        // can skip building the block
241        return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
242    }
243
244    let BlockBuilderOutcome { execution_result, block, .. } = builder.finish(&state_provider)?;
245
246    let requests = chain_spec
247        .is_prague_active_at_timestamp(attributes.timestamp)
248        .then_some(execution_result.requests);
249
250    // initialize empty blob sidecars at first. If cancun is active then this will
251    let mut blob_sidecars = Vec::new();
252
253    // only determine cancun fields when active
254    if chain_spec.is_cancun_active_at_timestamp(attributes.timestamp) {
255        // grab the blob sidecars from the executed txs
256        blob_sidecars = pool
257            .get_all_blobs_exact(
258                block
259                    .body()
260                    .transactions()
261                    .filter(|tx| tx.is_eip4844())
262                    .map(|tx| *tx.tx_hash())
263                    .collect(),
264            )
265            .map_err(PayloadBuilderError::other)?;
266    }
267
268    let mut sidecars = BlobSidecars::Empty;
269    blob_sidecars
270        .into_iter()
271        .map(Arc::unwrap_or_clone)
272        .for_each(|s| sidecars.push_sidecar_variant(s));
273
274    let sealed_block = Arc::new(block.sealed_block().clone());
275    debug!(target: "payload_builder", id=%attributes.id, sealed_block_header = ?sealed_block.sealed_header(), "sealed built block");
276
277    let payload = EthBuiltPayload::<SeismicPrimitives>::new_seismic_payload(
278        attributes.id,
279        sealed_block,
280        total_fees,
281        sidecars,
282        requests,
283    );
284
285    Ok(BuildOutcome::Better { payload, cached_reads })
286}