1use crate::{
4 config::{OpBuilderConfig, OpDAConfig},
5 error::OpPayloadBuilderError,
6 payload::{OpBuiltPayload, OpPayloadBuilderAttributes},
7 OpPayloadPrimitives,
8};
9use alloy_consensus::{Transaction, Typed2718};
10use alloy_primitives::{Bytes, B256, U256};
11use alloy_rlp::Encodable;
12use alloy_rpc_types_debug::ExecutionWitness;
13use alloy_rpc_types_engine::PayloadId;
14use op_alloy_rpc_types_engine::OpPayloadAttributes;
15use reth_basic_payload_builder::*;
16use reth_chain_state::{ExecutedBlock, ExecutedBlockWithTrieUpdates};
17use reth_chainspec::{ChainSpecProvider, EthChainSpec};
18use reth_evm::{
19 execute::{
20 BlockBuilder, BlockBuilderOutcome, BlockExecutionError, BlockExecutor, BlockValidationError,
21 },
22 ConfigureEvm, Database, Evm,
23};
24use reth_execution_types::ExecutionOutcome;
25use reth_optimism_evm::OpNextBlockEnvAttributes;
26use reth_optimism_forks::OpHardforks;
27use reth_optimism_primitives::transaction::OpTransaction;
28use reth_optimism_txpool::{
29 interop::{is_valid_interop, MaybeInteropTransaction},
30 OpPooledTx,
31};
32use reth_payload_builder_primitives::PayloadBuilderError;
33use reth_payload_primitives::PayloadBuilderAttributes;
34use reth_payload_util::{BestPayloadTransactions, NoopPayloadTransactions, PayloadTransactions};
35use reth_primitives_traits::{NodePrimitives, SealedHeader, SignedTransaction, TxTy};
36use reth_revm::{
37 cancelled::CancelOnDrop, database::StateProviderDatabase, db::State,
38 witness::ExecutionWitnessRecord,
39};
40use reth_storage_api::{errors::ProviderError, StateProvider, StateProviderFactory};
41use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool};
42use revm::context::{Block, BlockEnv};
43use std::sync::Arc;
44use tracing::{debug, trace, warn};
45
46#[derive(Debug, Clone)]
48pub struct OpPayloadBuilder<Pool, Client, Evm, Txs = ()> {
49 pub compute_pending_block: bool,
52 pub evm_config: Evm,
54 pub pool: Pool,
56 pub client: Client,
58 pub config: OpBuilderConfig,
60 pub best_transactions: Txs,
63}
64
65impl<Pool, Client, Evm> OpPayloadBuilder<Pool, Client, Evm> {
66 pub fn new(pool: Pool, client: Client, evm_config: Evm) -> Self {
70 Self::with_builder_config(pool, client, evm_config, Default::default())
71 }
72
73 pub const fn with_builder_config(
75 pool: Pool,
76 client: Client,
77 evm_config: Evm,
78 config: OpBuilderConfig,
79 ) -> Self {
80 Self {
81 pool,
82 client,
83 compute_pending_block: true,
84 evm_config,
85 config,
86 best_transactions: (),
87 }
88 }
89}
90
91impl<Pool, Client, Evm, Txs> OpPayloadBuilder<Pool, Client, Evm, Txs> {
92 pub const fn set_compute_pending_block(mut self, compute_pending_block: bool) -> Self {
94 self.compute_pending_block = compute_pending_block;
95 self
96 }
97
98 pub fn with_transactions<T>(
101 self,
102 best_transactions: T,
103 ) -> OpPayloadBuilder<Pool, Client, Evm, T> {
104 let Self { pool, client, compute_pending_block, evm_config, config, .. } = self;
105 OpPayloadBuilder {
106 pool,
107 client,
108 compute_pending_block,
109 evm_config,
110 best_transactions,
111 config,
112 }
113 }
114
115 pub const fn compute_pending_block(self) -> Self {
117 self.set_compute_pending_block(true)
118 }
119
120 pub const fn is_compute_pending_block(&self) -> bool {
122 self.compute_pending_block
123 }
124}
125
126impl<Pool, Client, Evm, N, T> OpPayloadBuilder<Pool, Client, Evm, T>
127where
128 Pool: TransactionPool<Transaction: OpPooledTx<Consensus = N::SignedTx>>,
129 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthChainSpec + OpHardforks>,
130 N: OpPayloadPrimitives,
131 Evm: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
132{
133 fn build_payload<'a, Txs>(
142 &self,
143 args: BuildArguments<OpPayloadBuilderAttributes<N::SignedTx>, OpBuiltPayload<N>>,
144 best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a,
145 ) -> Result<BuildOutcome<OpBuiltPayload<N>>, PayloadBuilderError>
146 where
147 Txs: PayloadTransactions<
148 Transaction: PoolTransaction<Consensus = N::SignedTx> + MaybeInteropTransaction,
149 >,
150 {
151 let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
152
153 let ctx = OpPayloadBuilderCtx {
154 evm_config: self.evm_config.clone(),
155 da_config: self.config.da_config.clone(),
156 chain_spec: self.client.chain_spec(),
157 config,
158 cancel,
159 best_payload,
160 };
161
162 let builder = OpBuilder::new(best);
163
164 let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?;
165 let state = StateProviderDatabase::new(&state_provider);
166
167 if ctx.attributes().no_tx_pool {
168 builder.build(state, &state_provider, ctx)
169 } else {
170 builder.build(cached_reads.as_db_mut(state), &state_provider, ctx)
172 }
173 .map(|out| out.with_cached_reads(cached_reads))
174 }
175
176 pub fn payload_witness(
178 &self,
179 parent: SealedHeader,
180 attributes: OpPayloadAttributes,
181 ) -> Result<ExecutionWitness, PayloadBuilderError> {
182 let attributes = OpPayloadBuilderAttributes::try_new(parent.hash(), attributes, 3)
183 .map_err(PayloadBuilderError::other)?;
184
185 let config = PayloadConfig { parent_header: Arc::new(parent), attributes };
186 let ctx = OpPayloadBuilderCtx {
187 evm_config: self.evm_config.clone(),
188 da_config: self.config.da_config.clone(),
189 chain_spec: self.client.chain_spec(),
190 config,
191 cancel: Default::default(),
192 best_payload: Default::default(),
193 };
194
195 let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?;
196
197 let builder = OpBuilder::new(|_| NoopPayloadTransactions::<Pool::Transaction>::default());
198 builder.witness(state_provider, &ctx)
199 }
200}
201
202impl<Pool, Client, Evm, N, Txs> PayloadBuilder for OpPayloadBuilder<Pool, Client, Evm, Txs>
204where
205 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthChainSpec + OpHardforks> + Clone,
206 N: OpPayloadPrimitives,
207 Pool: TransactionPool<Transaction: OpPooledTx<Consensus = N::SignedTx>>,
208 Evm: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
209 Txs: OpPayloadTransactions<Pool::Transaction>,
210{
211 type Attributes = OpPayloadBuilderAttributes<N::SignedTx>;
212 type BuiltPayload = OpBuiltPayload<N>;
213
214 fn try_build(
215 &self,
216 args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
217 ) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError> {
218 let pool = self.pool.clone();
219 self.build_payload(args, |attrs| self.best_transactions.best_transactions(pool, attrs))
220 }
221
222 fn on_missing_payload(
223 &self,
224 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
225 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
226 MissingPayloadBehaviour::AwaitInProgress
229 }
230
231 fn build_empty_payload(
234 &self,
235 config: PayloadConfig<Self::Attributes>,
236 ) -> Result<Self::BuiltPayload, PayloadBuilderError> {
237 let args = BuildArguments {
238 config,
239 cached_reads: Default::default(),
240 cancel: Default::default(),
241 best_payload: None,
242 };
243 self.build_payload(args, |_| NoopPayloadTransactions::<Pool::Transaction>::default())?
244 .into_payload()
245 .ok_or_else(|| PayloadBuilderError::MissingPayload)
246 }
247}
248
249#[derive(derive_more::Debug)]
265pub struct OpBuilder<'a, Txs> {
266 #[debug(skip)]
268 best: Box<dyn FnOnce(BestTransactionsAttributes) -> Txs + 'a>,
269}
270
271impl<'a, Txs> OpBuilder<'a, Txs> {
272 pub fn new(best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a) -> Self {
274 Self { best: Box::new(best) }
275 }
276}
277
278impl<Txs> OpBuilder<'_, Txs> {
279 pub fn build<EvmConfig, ChainSpec, N>(
281 self,
282 db: impl Database<Error = ProviderError>,
283 state_provider: impl StateProvider,
284 ctx: OpPayloadBuilderCtx<EvmConfig, ChainSpec>,
285 ) -> Result<BuildOutcomeKind<OpBuiltPayload<N>>, PayloadBuilderError>
286 where
287 EvmConfig: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
288 ChainSpec: EthChainSpec + OpHardforks,
289 N: OpPayloadPrimitives,
290 Txs: PayloadTransactions<
291 Transaction: PoolTransaction<Consensus = N::SignedTx> + MaybeInteropTransaction,
292 >,
293 {
294 let Self { best } = self;
295 debug!(target: "payload_builder", id=%ctx.payload_id(), parent_header = ?ctx.parent().hash(), parent_number = ctx.parent().number, "building new payload");
296
297 let mut db = State::builder().with_database(db).with_bundle_update().build();
298
299 let mut builder = ctx.block_builder(&mut db)?;
300
301 builder.apply_pre_execution_changes().map_err(|err| {
303 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
304 PayloadBuilderError::Internal(err.into())
305 })?;
306
307 let mut info = ctx.execute_sequencer_transactions(&mut builder)?;
309
310 if !ctx.attributes().no_tx_pool {
312 let best_txs = best(ctx.best_transaction_attributes(builder.evm_mut().block()));
313 if ctx.execute_best_transactions(&mut info, &mut builder, best_txs)?.is_some() {
314 return Ok(BuildOutcomeKind::Cancelled)
315 }
316
317 if !ctx.is_better_payload(info.total_fees) {
319 return Ok(BuildOutcomeKind::Aborted { fees: info.total_fees })
321 }
322 }
323
324 let BlockBuilderOutcome { execution_result, hashed_state, trie_updates, block } =
325 builder.finish(state_provider)?;
326
327 let sealed_block = Arc::new(block.sealed_block().clone());
328 debug!(target: "payload_builder", id=%ctx.attributes().payload_id(), sealed_block_header = ?sealed_block.header(), "sealed built block");
329
330 let execution_outcome = ExecutionOutcome::new(
331 db.take_bundle(),
332 vec![execution_result.receipts],
333 block.number,
334 Vec::new(),
335 );
336
337 let executed: ExecutedBlockWithTrieUpdates<N> = ExecutedBlockWithTrieUpdates {
339 block: ExecutedBlock {
340 recovered_block: Arc::new(block),
341 execution_output: Arc::new(execution_outcome),
342 hashed_state: Arc::new(hashed_state),
343 },
344 trie: Arc::new(trie_updates),
345 };
346
347 let no_tx_pool = ctx.attributes().no_tx_pool;
348
349 let payload =
350 OpBuiltPayload::new(ctx.payload_id(), sealed_block, info.total_fees, Some(executed));
351
352 if no_tx_pool {
353 Ok(BuildOutcomeKind::Freeze(payload))
357 } else {
358 Ok(BuildOutcomeKind::Better { payload })
359 }
360 }
361
362 pub fn witness<Evm, ChainSpec, N>(
364 self,
365 state_provider: impl StateProvider,
366 ctx: &OpPayloadBuilderCtx<Evm, ChainSpec>,
367 ) -> Result<ExecutionWitness, PayloadBuilderError>
368 where
369 Evm: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
370 ChainSpec: EthChainSpec + OpHardforks,
371 N: OpPayloadPrimitives,
372 Txs: PayloadTransactions<Transaction: PoolTransaction<Consensus = N::SignedTx>>,
373 {
374 let mut db = State::builder()
375 .with_database(StateProviderDatabase::new(&state_provider))
376 .with_bundle_update()
377 .build();
378 let mut builder = ctx.block_builder(&mut db)?;
379
380 builder.apply_pre_execution_changes()?;
381 ctx.execute_sequencer_transactions(&mut builder)?;
382 builder.into_executor().apply_post_execution_changes()?;
383
384 let ExecutionWitnessRecord { hashed_state, codes, keys, lowest_block_number: _ } =
385 ExecutionWitnessRecord::from_executed_state(&db);
386 let state = state_provider.witness(Default::default(), hashed_state)?;
387 Ok(ExecutionWitness {
388 state: state.into_iter().collect(),
389 codes,
390 keys,
391 ..Default::default()
392 })
393 }
394}
395
396pub trait OpPayloadTransactions<Transaction>: Clone + Send + Sync + Unpin + 'static {
398 fn best_transactions<Pool: TransactionPool<Transaction = Transaction>>(
401 &self,
402 pool: Pool,
403 attr: BestTransactionsAttributes,
404 ) -> impl PayloadTransactions<Transaction = Transaction>;
405}
406
407impl<T: PoolTransaction + MaybeInteropTransaction> OpPayloadTransactions<T> for () {
408 fn best_transactions<Pool: TransactionPool<Transaction = T>>(
409 &self,
410 pool: Pool,
411 attr: BestTransactionsAttributes,
412 ) -> impl PayloadTransactions<Transaction = T> {
413 BestPayloadTransactions::new(pool.best_transactions_with_attributes(attr))
414 }
415}
416
417#[derive(Debug)]
419pub struct ExecutedPayload<N: NodePrimitives> {
420 pub info: ExecutionInfo,
422 pub withdrawals_root: Option<B256>,
424 pub receipts: Vec<N::Receipt>,
426 pub block_env: BlockEnv,
428}
429
430#[derive(Default, Debug)]
432pub struct ExecutionInfo {
433 pub cumulative_gas_used: u64,
435 pub cumulative_da_bytes_used: u64,
437 pub total_fees: U256,
439}
440
441impl ExecutionInfo {
442 pub const fn new() -> Self {
444 Self { cumulative_gas_used: 0, cumulative_da_bytes_used: 0, total_fees: U256::ZERO }
445 }
446
447 pub fn is_tx_over_limits(
454 &self,
455 tx: &(impl Encodable + Transaction),
456 block_gas_limit: u64,
457 tx_data_limit: Option<u64>,
458 block_data_limit: Option<u64>,
459 ) -> bool {
460 if tx_data_limit.is_some_and(|da_limit| tx.length() as u64 > da_limit) {
461 return true;
462 }
463
464 if block_data_limit
465 .is_some_and(|da_limit| self.cumulative_da_bytes_used + (tx.length() as u64) > da_limit)
466 {
467 return true;
468 }
469
470 self.cumulative_gas_used + tx.gas_limit() > block_gas_limit
471 }
472}
473
474#[derive(derive_more::Debug)]
476pub struct OpPayloadBuilderCtx<Evm: ConfigureEvm, ChainSpec> {
477 pub evm_config: Evm,
479 pub da_config: OpDAConfig,
481 pub chain_spec: Arc<ChainSpec>,
483 pub config: PayloadConfig<OpPayloadBuilderAttributes<TxTy<Evm::Primitives>>>,
485 pub cancel: CancelOnDrop,
487 pub best_payload: Option<OpBuiltPayload<Evm::Primitives>>,
489}
490
491impl<Evm, ChainSpec> OpPayloadBuilderCtx<Evm, ChainSpec>
492where
493 Evm: ConfigureEvm<Primitives: OpPayloadPrimitives, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
494 ChainSpec: EthChainSpec + OpHardforks,
495{
496 pub fn parent(&self) -> &SealedHeader {
498 &self.config.parent_header
499 }
500
501 pub const fn attributes(&self) -> &OpPayloadBuilderAttributes<TxTy<Evm::Primitives>> {
503 &self.config.attributes
504 }
505
506 pub fn extra_data(&self) -> Result<Bytes, PayloadBuilderError> {
510 if self.is_holocene_active() {
511 self.attributes()
512 .get_holocene_extra_data(
513 self.chain_spec.base_fee_params_at_timestamp(
514 self.attributes().payload_attributes.timestamp,
515 ),
516 )
517 .map_err(PayloadBuilderError::other)
518 } else {
519 Ok(Default::default())
520 }
521 }
522
523 pub fn best_transaction_attributes(&self, block_env: &BlockEnv) -> BestTransactionsAttributes {
525 BestTransactionsAttributes::new(
526 block_env.basefee,
527 block_env.blob_gasprice().map(|p| p as u64),
528 )
529 }
530
531 pub fn payload_id(&self) -> PayloadId {
533 self.attributes().payload_id()
534 }
535
536 pub fn is_holocene_active(&self) -> bool {
538 self.chain_spec.is_holocene_active_at_timestamp(self.attributes().timestamp())
539 }
540
541 pub fn is_better_payload(&self, total_fees: U256) -> bool {
543 is_better_payload(self.best_payload.as_ref(), total_fees)
544 }
545
546 pub fn block_builder<'a, DB: Database>(
548 &'a self,
549 db: &'a mut State<DB>,
550 ) -> Result<impl BlockBuilder<Primitives = Evm::Primitives> + 'a, PayloadBuilderError> {
551 self.evm_config
552 .builder_for_next_block(
553 db,
554 self.parent(),
555 OpNextBlockEnvAttributes {
556 timestamp: self.attributes().timestamp(),
557 suggested_fee_recipient: self.attributes().suggested_fee_recipient(),
558 prev_randao: self.attributes().prev_randao(),
559 gas_limit: self.attributes().gas_limit.unwrap_or(self.parent().gas_limit),
560 parent_beacon_block_root: self.attributes().parent_beacon_block_root(),
561 extra_data: self.extra_data()?,
562 },
563 )
564 .map_err(PayloadBuilderError::other)
565 }
566}
567
568impl<Evm, ChainSpec> OpPayloadBuilderCtx<Evm, ChainSpec>
569where
570 Evm: ConfigureEvm<Primitives: OpPayloadPrimitives, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
571 ChainSpec: EthChainSpec + OpHardforks,
572{
573 pub fn execute_sequencer_transactions(
575 &self,
576 builder: &mut impl BlockBuilder<Primitives = Evm::Primitives>,
577 ) -> Result<ExecutionInfo, PayloadBuilderError> {
578 let mut info = ExecutionInfo::new();
579
580 for sequencer_tx in &self.attributes().transactions {
581 if sequencer_tx.value().is_eip4844() {
583 return Err(PayloadBuilderError::other(
584 OpPayloadBuilderError::BlobTransactionRejected,
585 ))
586 }
587
588 let sequencer_tx = sequencer_tx.value().try_clone_into_recovered().map_err(|_| {
593 PayloadBuilderError::other(OpPayloadBuilderError::TransactionEcRecoverFailed)
594 })?;
595
596 let gas_used = match builder.execute_transaction(sequencer_tx.clone()) {
597 Ok(gas_used) => gas_used,
598 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
599 error,
600 ..
601 })) => {
602 trace!(target: "payload_builder", %error, ?sequencer_tx, "Error in sequencer transaction, skipping.");
603 continue
604 }
605 Err(err) => {
606 return Err(PayloadBuilderError::EvmExecutionError(Box::new(err)))
608 }
609 };
610
611 info.cumulative_gas_used += gas_used;
613 }
614
615 Ok(info)
616 }
617
618 pub fn execute_best_transactions(
622 &self,
623 info: &mut ExecutionInfo,
624 builder: &mut impl BlockBuilder<Primitives = Evm::Primitives>,
625 mut best_txs: impl PayloadTransactions<
626 Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>
627 + MaybeInteropTransaction,
628 >,
629 ) -> Result<Option<()>, PayloadBuilderError> {
630 let block_gas_limit = builder.evm_mut().block().gas_limit;
631 let block_da_limit = self.da_config.max_da_block_size();
632 let tx_da_limit = self.da_config.max_da_tx_size();
633 let base_fee = builder.evm_mut().block().basefee;
634
635 while let Some(tx) = best_txs.next(()) {
636 let interop = tx.interop_deadline();
637 let tx = tx.into_consensus();
638 if info.is_tx_over_limits(tx.inner(), block_gas_limit, tx_da_limit, block_da_limit) {
639 best_txs.mark_invalid(tx.signer(), tx.nonce());
643 continue
644 }
645
646 if tx.is_eip4844() || tx.is_deposit() {
648 best_txs.mark_invalid(tx.signer(), tx.nonce());
649 continue
650 }
651
652 if let Some(interop) = interop {
655 if !is_valid_interop(interop, self.config.attributes.timestamp()) {
656 best_txs.mark_invalid(tx.signer(), tx.nonce());
657 continue
658 }
659 }
660 if self.cancel.is_cancelled() {
662 return Ok(Some(()))
663 }
664
665 let gas_used = match builder.execute_transaction(tx.clone()) {
666 Ok(gas_used) => gas_used,
667 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
668 error,
669 ..
670 })) => {
671 if error.is_nonce_too_low() {
672 trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
674 } else {
675 trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
678 best_txs.mark_invalid(tx.signer(), tx.nonce());
679 }
680 continue
681 }
682 Err(err) => {
683 return Err(PayloadBuilderError::EvmExecutionError(Box::new(err)))
685 }
686 };
687
688 info.cumulative_gas_used += gas_used;
691 info.cumulative_da_bytes_used += tx.length() as u64;
692
693 let miner_fee = tx
695 .effective_tip_per_gas(base_fee)
696 .expect("fee is always valid; execution succeeded");
697 info.total_fees += U256::from(miner_fee) * U256::from(gas_used);
698 }
699
700 Ok(None)
701 }
702}