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::signed::OpTransaction;
28use reth_optimism_txpool::OpPooledTx;
29use reth_payload_builder_primitives::PayloadBuilderError;
30use reth_payload_primitives::PayloadBuilderAttributes;
31use reth_payload_util::{BestPayloadTransactions, NoopPayloadTransactions, PayloadTransactions};
32use reth_primitives::{transaction::SignedTransaction, NodePrimitives, SealedHeader, TxTy};
33use reth_provider::{ProviderError, StateProvider, StateProviderFactory};
34use reth_revm::{
35 cancelled::CancelOnDrop, database::StateProviderDatabase, db::State,
36 witness::ExecutionWitnessRecord,
37};
38use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool};
39use revm::context::{Block, BlockEnv};
40use std::sync::Arc;
41use tracing::{debug, trace, warn};
42
43#[derive(Debug, Clone)]
45pub struct OpPayloadBuilder<Pool, Client, Evm, Txs = ()> {
46 pub compute_pending_block: bool,
49 pub evm_config: Evm,
51 pub pool: Pool,
53 pub client: Client,
55 pub config: OpBuilderConfig,
57 pub best_transactions: Txs,
60}
61
62impl<Pool, Client, Evm> OpPayloadBuilder<Pool, Client, Evm> {
63 pub fn new(pool: Pool, client: Client, evm_config: Evm) -> Self {
67 Self::with_builder_config(pool, client, evm_config, Default::default())
68 }
69
70 pub fn with_builder_config(
72 pool: Pool,
73 client: Client,
74 evm_config: Evm,
75 config: OpBuilderConfig,
76 ) -> Self {
77 Self {
78 pool,
79 client,
80 compute_pending_block: true,
81 evm_config,
82 config,
83 best_transactions: (),
84 }
85 }
86}
87
88impl<Pool, Client, Evm, Txs> OpPayloadBuilder<Pool, Client, Evm, Txs> {
89 pub const fn set_compute_pending_block(mut self, compute_pending_block: bool) -> Self {
91 self.compute_pending_block = compute_pending_block;
92 self
93 }
94
95 pub fn with_transactions<T>(
98 self,
99 best_transactions: T,
100 ) -> OpPayloadBuilder<Pool, Client, Evm, T> {
101 let Self { pool, client, compute_pending_block, evm_config, config, .. } = self;
102 OpPayloadBuilder {
103 pool,
104 client,
105 compute_pending_block,
106 evm_config,
107 best_transactions,
108 config,
109 }
110 }
111
112 pub const fn compute_pending_block(self) -> Self {
114 self.set_compute_pending_block(true)
115 }
116
117 pub const fn is_compute_pending_block(&self) -> bool {
119 self.compute_pending_block
120 }
121}
122
123impl<Pool, Client, Evm, N, T> OpPayloadBuilder<Pool, Client, Evm, T>
124where
125 Pool: TransactionPool<Transaction: OpPooledTx<Consensus = N::SignedTx>>,
126 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthChainSpec + OpHardforks>,
127 N: OpPayloadPrimitives,
128 Evm: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
129{
130 fn build_payload<'a, Txs>(
139 &self,
140 args: BuildArguments<OpPayloadBuilderAttributes<N::SignedTx>, OpBuiltPayload<N>>,
141 best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a,
142 ) -> Result<BuildOutcome<OpBuiltPayload<N>>, PayloadBuilderError>
143 where
144 Txs: PayloadTransactions<Transaction: PoolTransaction<Consensus = N::SignedTx>>,
145 {
146 let BuildArguments { mut cached_reads, config, cancel, best_payload } = args;
147
148 let ctx = OpPayloadBuilderCtx {
149 evm_config: self.evm_config.clone(),
150 da_config: self.config.da_config.clone(),
151 chain_spec: self.client.chain_spec(),
152 config,
153 cancel,
154 best_payload,
155 };
156
157 let builder = OpBuilder::new(best);
158
159 let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?;
160 let state = StateProviderDatabase::new(&state_provider);
161
162 if ctx.attributes().no_tx_pool {
163 builder.build(state, &state_provider, ctx)
164 } else {
165 builder.build(cached_reads.as_db_mut(state), &state_provider, ctx)
167 }
168 .map(|out| out.with_cached_reads(cached_reads))
169 }
170
171 pub fn payload_witness(
173 &self,
174 parent: SealedHeader,
175 attributes: OpPayloadAttributes,
176 ) -> Result<ExecutionWitness, PayloadBuilderError> {
177 let attributes = OpPayloadBuilderAttributes::try_new(parent.hash(), attributes, 3)
178 .map_err(PayloadBuilderError::other)?;
179
180 let config = PayloadConfig { parent_header: Arc::new(parent), attributes };
181 let ctx = OpPayloadBuilderCtx {
182 evm_config: self.evm_config.clone(),
183 da_config: self.config.da_config.clone(),
184 chain_spec: self.client.chain_spec(),
185 config,
186 cancel: Default::default(),
187 best_payload: Default::default(),
188 };
189
190 let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?;
191
192 let builder = OpBuilder::new(|_| NoopPayloadTransactions::<Pool::Transaction>::default());
193 builder.witness(state_provider, &ctx)
194 }
195}
196
197impl<Pool, Client, Evm, N, Txs> PayloadBuilder for OpPayloadBuilder<Pool, Client, Evm, Txs>
199where
200 Client: StateProviderFactory + ChainSpecProvider<ChainSpec: EthChainSpec + OpHardforks> + Clone,
201 N: OpPayloadPrimitives,
202 Pool: TransactionPool<Transaction: OpPooledTx<Consensus = N::SignedTx>>,
203 Evm: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
204 Txs: OpPayloadTransactions<Pool::Transaction>,
205{
206 type Attributes = OpPayloadBuilderAttributes<N::SignedTx>;
207 type BuiltPayload = OpBuiltPayload<N>;
208
209 fn try_build(
210 &self,
211 args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
212 ) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError> {
213 let pool = self.pool.clone();
214 self.build_payload(args, |attrs| self.best_transactions.best_transactions(pool, attrs))
215 }
216
217 fn on_missing_payload(
218 &self,
219 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
220 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
221 MissingPayloadBehaviour::AwaitInProgress
224 }
225
226 fn build_empty_payload(
229 &self,
230 config: PayloadConfig<Self::Attributes>,
231 ) -> Result<Self::BuiltPayload, PayloadBuilderError> {
232 let args = BuildArguments {
233 config,
234 cached_reads: Default::default(),
235 cancel: Default::default(),
236 best_payload: None,
237 };
238 self.build_payload(args, |_| NoopPayloadTransactions::<Pool::Transaction>::default())?
239 .into_payload()
240 .ok_or_else(|| PayloadBuilderError::MissingPayload)
241 }
242}
243
244#[derive(derive_more::Debug)]
260pub struct OpBuilder<'a, Txs> {
261 #[debug(skip)]
263 best: Box<dyn FnOnce(BestTransactionsAttributes) -> Txs + 'a>,
264}
265
266impl<'a, Txs> OpBuilder<'a, Txs> {
267 fn new(best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a) -> Self {
268 Self { best: Box::new(best) }
269 }
270}
271
272impl<Txs> OpBuilder<'_, Txs> {
273 pub fn build<EvmConfig, ChainSpec, N>(
275 self,
276 db: impl Database<Error = ProviderError>,
277 state_provider: impl StateProvider,
278 ctx: OpPayloadBuilderCtx<EvmConfig, ChainSpec>,
279 ) -> Result<BuildOutcomeKind<OpBuiltPayload<N>>, PayloadBuilderError>
280 where
281 EvmConfig: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
282 ChainSpec: EthChainSpec + OpHardforks,
283 N: OpPayloadPrimitives,
284 Txs: PayloadTransactions<Transaction: PoolTransaction<Consensus = N::SignedTx>>,
285 {
286 let Self { best } = self;
287 debug!(target: "payload_builder", id=%ctx.payload_id(), parent_header = ?ctx.parent().hash(), parent_number = ctx.parent().number, "building new payload");
288
289 let mut db = State::builder().with_database(db).with_bundle_update().build();
290
291 let mut builder = ctx.block_builder(&mut db)?;
292
293 builder.apply_pre_execution_changes().map_err(|err| {
295 warn!(target: "payload_builder", %err, "failed to apply pre-execution changes");
296 PayloadBuilderError::Internal(err.into())
297 })?;
298
299 let mut info = ctx.execute_sequencer_transactions(&mut builder)?;
301
302 if !ctx.attributes().no_tx_pool {
304 let best_txs = best(ctx.best_transaction_attributes(builder.evm_mut().block()));
305 if ctx.execute_best_transactions(&mut info, &mut builder, best_txs)?.is_some() {
306 return Ok(BuildOutcomeKind::Cancelled)
307 }
308
309 if !ctx.is_better_payload(info.total_fees) {
311 return Ok(BuildOutcomeKind::Aborted { fees: info.total_fees })
313 }
314 }
315
316 let BlockBuilderOutcome { execution_result, hashed_state, trie_updates, block } =
317 builder.finish(state_provider)?;
318
319 let sealed_block = Arc::new(block.sealed_block().clone());
320 debug!(target: "payload_builder", id=%ctx.attributes().payload_id(), sealed_block_header = ?sealed_block.header(), "sealed built block");
321
322 let execution_outcome = ExecutionOutcome::new(
323 db.take_bundle(),
324 vec![execution_result.receipts],
325 block.number,
326 Vec::new(),
327 );
328
329 let executed: ExecutedBlockWithTrieUpdates<N> = ExecutedBlockWithTrieUpdates {
331 block: ExecutedBlock {
332 recovered_block: Arc::new(block),
333 execution_output: Arc::new(execution_outcome),
334 hashed_state: Arc::new(hashed_state),
335 },
336 trie: Arc::new(trie_updates),
337 };
338
339 let no_tx_pool = ctx.attributes().no_tx_pool;
340
341 let payload =
342 OpBuiltPayload::new(ctx.payload_id(), sealed_block, info.total_fees, Some(executed));
343
344 if no_tx_pool {
345 Ok(BuildOutcomeKind::Freeze(payload))
349 } else {
350 Ok(BuildOutcomeKind::Better { payload })
351 }
352 }
353
354 pub fn witness<Evm, ChainSpec, N>(
356 self,
357 state_provider: impl StateProvider,
358 ctx: &OpPayloadBuilderCtx<Evm, ChainSpec>,
359 ) -> Result<ExecutionWitness, PayloadBuilderError>
360 where
361 Evm: ConfigureEvm<Primitives = N, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
362 ChainSpec: EthChainSpec + OpHardforks,
363 N: OpPayloadPrimitives,
364 Txs: PayloadTransactions<Transaction: PoolTransaction<Consensus = N::SignedTx>>,
365 {
366 let mut db = State::builder()
367 .with_database(StateProviderDatabase::new(&state_provider))
368 .with_bundle_update()
369 .build();
370 let mut builder = ctx.block_builder(&mut db)?;
371
372 builder.apply_pre_execution_changes()?;
373 ctx.execute_sequencer_transactions(&mut builder)?;
374 builder.into_executor().apply_post_execution_changes()?;
375
376 let ExecutionWitnessRecord { hashed_state, codes, keys } =
377 ExecutionWitnessRecord::from_executed_state(&db);
378 let state = state_provider.witness(Default::default(), hashed_state)?;
379 Ok(ExecutionWitness { state: state.into_iter().collect(), codes, keys })
380 }
381}
382
383pub trait OpPayloadTransactions<Transaction>: Clone + Send + Sync + Unpin + 'static {
385 fn best_transactions<Pool: TransactionPool<Transaction = Transaction>>(
388 &self,
389 pool: Pool,
390 attr: BestTransactionsAttributes,
391 ) -> impl PayloadTransactions<Transaction = Transaction>;
392}
393
394impl<T: PoolTransaction> OpPayloadTransactions<T> for () {
395 fn best_transactions<Pool: TransactionPool<Transaction = T>>(
396 &self,
397 pool: Pool,
398 attr: BestTransactionsAttributes,
399 ) -> impl PayloadTransactions<Transaction = T> {
400 BestPayloadTransactions::new(pool.best_transactions_with_attributes(attr))
401 }
402}
403
404#[derive(Debug)]
406pub struct ExecutedPayload<N: NodePrimitives> {
407 pub info: ExecutionInfo,
409 pub withdrawals_root: Option<B256>,
411 pub receipts: Vec<N::Receipt>,
413 pub block_env: BlockEnv,
415}
416
417#[derive(Default, Debug)]
419pub struct ExecutionInfo {
420 pub cumulative_gas_used: u64,
422 pub cumulative_da_bytes_used: u64,
424 pub total_fees: U256,
426}
427
428impl ExecutionInfo {
429 pub fn new() -> Self {
431 Self { cumulative_gas_used: 0, cumulative_da_bytes_used: 0, total_fees: U256::ZERO }
432 }
433
434 pub fn is_tx_over_limits(
441 &self,
442 tx: &(impl Encodable + Transaction),
443 block_gas_limit: u64,
444 tx_data_limit: Option<u64>,
445 block_data_limit: Option<u64>,
446 ) -> bool {
447 if tx_data_limit.is_some_and(|da_limit| tx.length() as u64 > da_limit) {
448 return true;
449 }
450
451 if block_data_limit
452 .is_some_and(|da_limit| self.cumulative_da_bytes_used + (tx.length() as u64) > da_limit)
453 {
454 return true;
455 }
456
457 self.cumulative_gas_used + tx.gas_limit() > block_gas_limit
458 }
459}
460
461#[derive(derive_more::Debug)]
463pub struct OpPayloadBuilderCtx<Evm: ConfigureEvm, ChainSpec> {
464 pub evm_config: Evm,
466 pub da_config: OpDAConfig,
468 pub chain_spec: Arc<ChainSpec>,
470 pub config: PayloadConfig<OpPayloadBuilderAttributes<TxTy<Evm::Primitives>>>,
472 pub cancel: CancelOnDrop,
474 pub best_payload: Option<OpBuiltPayload<Evm::Primitives>>,
476}
477
478impl<Evm, ChainSpec> OpPayloadBuilderCtx<Evm, ChainSpec>
479where
480 Evm: ConfigureEvm<Primitives: OpPayloadPrimitives, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
481 ChainSpec: EthChainSpec + OpHardforks,
482{
483 pub fn parent(&self) -> &SealedHeader {
485 &self.config.parent_header
486 }
487
488 pub const fn attributes(&self) -> &OpPayloadBuilderAttributes<TxTy<Evm::Primitives>> {
490 &self.config.attributes
491 }
492
493 pub fn extra_data(&self) -> Result<Bytes, PayloadBuilderError> {
497 if self.is_holocene_active() {
498 self.attributes()
499 .get_holocene_extra_data(
500 self.chain_spec.base_fee_params_at_timestamp(
501 self.attributes().payload_attributes.timestamp,
502 ),
503 )
504 .map_err(PayloadBuilderError::other)
505 } else {
506 Ok(Default::default())
507 }
508 }
509
510 pub fn best_transaction_attributes(&self, block_env: &BlockEnv) -> BestTransactionsAttributes {
512 BestTransactionsAttributes::new(
513 block_env.basefee,
514 block_env.blob_gasprice().map(|p| p as u64),
515 )
516 }
517
518 pub fn payload_id(&self) -> PayloadId {
520 self.attributes().payload_id()
521 }
522
523 pub fn is_holocene_active(&self) -> bool {
525 self.chain_spec.is_holocene_active_at_timestamp(self.attributes().timestamp())
526 }
527
528 pub fn is_better_payload(&self, total_fees: U256) -> bool {
530 is_better_payload(self.best_payload.as_ref(), total_fees)
531 }
532
533 pub fn block_builder<'a, DB: Database>(
535 &'a self,
536 db: &'a mut State<DB>,
537 ) -> Result<impl BlockBuilder<Primitives = Evm::Primitives> + 'a, PayloadBuilderError> {
538 self.evm_config
539 .builder_for_next_block(
540 db,
541 self.parent(),
542 OpNextBlockEnvAttributes {
543 timestamp: self.attributes().timestamp(),
544 suggested_fee_recipient: self.attributes().suggested_fee_recipient(),
545 prev_randao: self.attributes().prev_randao(),
546 gas_limit: self.attributes().gas_limit.unwrap_or(self.parent().gas_limit),
547 parent_beacon_block_root: self.attributes().parent_beacon_block_root(),
548 extra_data: self.extra_data()?,
549 },
550 )
551 .map_err(PayloadBuilderError::other)
552 }
553}
554
555impl<Evm, ChainSpec> OpPayloadBuilderCtx<Evm, ChainSpec>
556where
557 Evm: ConfigureEvm<Primitives: OpPayloadPrimitives, NextBlockEnvCtx = OpNextBlockEnvAttributes>,
558 ChainSpec: EthChainSpec + OpHardforks,
559{
560 pub fn execute_sequencer_transactions(
562 &self,
563 builder: &mut impl BlockBuilder<Primitives = Evm::Primitives>,
564 ) -> Result<ExecutionInfo, PayloadBuilderError> {
565 let mut info = ExecutionInfo::new();
566
567 for sequencer_tx in &self.attributes().transactions {
568 if sequencer_tx.value().is_eip4844() {
570 return Err(PayloadBuilderError::other(
571 OpPayloadBuilderError::BlobTransactionRejected,
572 ))
573 }
574
575 let sequencer_tx = sequencer_tx.value().try_clone_into_recovered().map_err(|_| {
580 PayloadBuilderError::other(OpPayloadBuilderError::TransactionEcRecoverFailed)
581 })?;
582
583 let gas_used = match builder.execute_transaction(sequencer_tx.clone()) {
584 Ok(gas_used) => gas_used,
585 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
586 error,
587 ..
588 })) => {
589 trace!(target: "payload_builder", %error, ?sequencer_tx, "Error in sequencer transaction, skipping.");
590 continue
591 }
592 Err(err) => {
593 return Err(PayloadBuilderError::EvmExecutionError(Box::new(err)))
595 }
596 };
597
598 info.cumulative_gas_used += gas_used;
600 }
601
602 Ok(info)
603 }
604
605 pub fn execute_best_transactions(
609 &self,
610 info: &mut ExecutionInfo,
611 builder: &mut impl BlockBuilder<Primitives = Evm::Primitives>,
612 mut best_txs: impl PayloadTransactions<
613 Transaction: PoolTransaction<Consensus = TxTy<Evm::Primitives>>,
614 >,
615 ) -> Result<Option<()>, PayloadBuilderError> {
616 let block_gas_limit = builder.evm_mut().block().gas_limit;
617 let block_da_limit = self.da_config.max_da_block_size();
618 let tx_da_limit = self.da_config.max_da_tx_size();
619 let base_fee = builder.evm_mut().block().basefee;
620
621 while let Some(tx) = best_txs.next(()) {
622 let tx = tx.into_consensus();
623 if info.is_tx_over_limits(tx.inner(), block_gas_limit, tx_da_limit, block_da_limit) {
624 best_txs.mark_invalid(tx.signer(), tx.nonce());
628 continue
629 }
630
631 if tx.is_eip4844() || tx.is_deposit() {
633 best_txs.mark_invalid(tx.signer(), tx.nonce());
634 continue
635 }
636
637 if self.cancel.is_cancelled() {
639 return Ok(Some(()))
640 }
641
642 let gas_used = match builder.execute_transaction(tx.clone()) {
643 Ok(gas_used) => gas_used,
644 Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
645 error,
646 ..
647 })) => {
648 if error.is_nonce_too_low() {
649 trace!(target: "payload_builder", %error, ?tx, "skipping nonce too low transaction");
651 } else {
652 trace!(target: "payload_builder", %error, ?tx, "skipping invalid transaction and its descendants");
655 best_txs.mark_invalid(tx.signer(), tx.nonce());
656 }
657 continue
658 }
659 Err(err) => {
660 return Err(PayloadBuilderError::EvmExecutionError(Box::new(err)))
662 }
663 };
664
665 info.cumulative_gas_used += gas_used;
668 info.cumulative_da_bytes_used += tx.length() as u64;
669
670 let miner_fee = tx
672 .effective_tip_per_gas(base_fee)
673 .expect("fee is always valid; execution succeeded");
674 info.total_fees += U256::from(miner_fee) * U256::from(gas_used);
675 }
676
677 Ok(None)
678 }
679}