Skip to main content

tempo_payload_builder/
lib.rs

1//! Tempo Payload Builder.
2
3#![cfg_attr(not(test), warn(unused_crate_dependencies))]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6mod metrics;
7
8use crate::metrics::{InstrumentedFinishProvider, TempoPayloadBuilderMetrics};
9use alloy_consensus::{BlockHeader as _, Signed, Transaction, TxLegacy};
10use alloy_primitives::{Address, U256};
11use alloy_rlp::{Decodable, Encodable};
12use either::Either;
13use reth_basic_payload_builder::{
14    BuildArguments, BuildOutcome, MissingPayloadBehaviour, PayloadBuilder, PayloadConfig,
15    is_better_payload,
16};
17use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
18use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
19use reth_engine_tree::tree::instrumented_state::InstrumentedStateProvider;
20use reth_errors::{ConsensusError, ProviderError};
21use reth_evm::{
22    ConfigureEvm, Database, Evm, NextBlockEnvAttributes,
23    block::{BlockExecutionError, BlockValidationError},
24    execute::{BlockBuilder, BlockBuilderOutcome},
25};
26use reth_execution_types::BlockExecutionOutput;
27use reth_payload_builder::{EthBuiltPayload, PayloadBuilderError};
28use reth_payload_primitives::{BuiltPayload, BuiltPayloadExecutedBlock, PayloadBuilderAttributes};
29use reth_primitives_traits::{Recovered, transaction::error::InvalidTransactionError};
30use reth_revm::{
31    State,
32    context::{Block, BlockEnv},
33    database::StateProviderDatabase,
34};
35use reth_storage_api::{StateProvider, StateProviderFactory};
36use reth_transaction_pool::{
37    BestTransactions, BestTransactionsAttributes, TransactionPool, ValidPoolTransaction,
38    error::InvalidPoolTransactionError,
39};
40use std::{
41    sync::{
42        Arc,
43        atomic::{AtomicU64, Ordering},
44    },
45    time::Instant,
46};
47use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
48use tempo_consensus::TEMPO_SHARED_GAS_DIVISOR;
49use tempo_evm::{TempoEvmConfig, TempoNextBlockEnvAttributes};
50use tempo_payload_types::{TempoBuiltPayload, TempoPayloadBuilderAttributes};
51use tempo_primitives::{
52    RecoveredSubBlock, SubBlockMetadata, TempoHeader, TempoTxEnvelope,
53    subblock::PartialValidatorKey,
54    transaction::{
55        calc_gas_balance_spending,
56        envelope::{TEMPO_SYSTEM_TX_SENDER, TEMPO_SYSTEM_TX_SIGNATURE},
57    },
58};
59use tempo_transaction_pool::{
60    TempoTransactionPool,
61    transaction::{TempoPoolTransactionError, TempoPooledTransaction},
62};
63use tracing::{Level, debug, debug_span, error, info, instrument, trace, warn};
64
65/// Returns true if a subblock has any expired transactions for the given timestamp.
66fn has_expired_transactions(subblock: &RecoveredSubBlock, timestamp: u64) -> bool {
67    subblock.transactions.iter().any(|tx| {
68        tx.as_aa()
69            .is_some_and(|tx| tx.tx().valid_before.is_some_and(|valid| valid <= timestamp))
70    })
71}
72
73#[derive(Debug, Clone)]
74pub struct TempoPayloadBuilder<Provider> {
75    pool: TempoTransactionPool<Provider>,
76    provider: Provider,
77    evm_config: TempoEvmConfig,
78    metrics: TempoPayloadBuilderMetrics,
79    /// Height at which we've seen an invalid subblock.
80    ///
81    /// We pre-validate all of the subblock transactions when collecting subblocks, so this
82    /// should never be set because subblocks with invalid transactions should never make it to the payload builder.
83    ///
84    /// However, due to disruptive nature of subblock-related bugs (invalid subblock
85    /// we're continuously failing to apply halts block building), we protect against this by tracking
86    /// last height at which we've seen an invalid subblock, and not including any subblocks
87    /// at this height for any payloads.
88    highest_invalid_subblock: Arc<AtomicU64>,
89    /// Whether to enable state provider metrics.
90    state_provider_metrics: bool,
91    /// Whether to disable state cache.
92    disable_state_cache: bool,
93}
94
95impl<Provider> TempoPayloadBuilder<Provider> {
96    pub fn new(
97        pool: TempoTransactionPool<Provider>,
98        provider: Provider,
99        evm_config: TempoEvmConfig,
100        state_provider_metrics: bool,
101        disable_state_cache: bool,
102    ) -> Self {
103        Self {
104            pool,
105            provider,
106            evm_config,
107            metrics: TempoPayloadBuilderMetrics::default(),
108            highest_invalid_subblock: Default::default(),
109            state_provider_metrics,
110            disable_state_cache,
111        }
112    }
113}
114
115impl<Provider: ChainSpecProvider<ChainSpec = TempoChainSpec>> TempoPayloadBuilder<Provider> {
116    /// Builds system transactions to seal the block.
117    ///
118    /// Returns a vector of system transactions that must be executed at the end of each block:
119    /// - Subblocks signatures - validates subblock signatures
120    fn build_seal_block_txs(
121        &self,
122        block_env: &BlockEnv,
123        subblocks: &[RecoveredSubBlock],
124    ) -> Vec<Recovered<TempoTxEnvelope>> {
125        let chain_spec = self.provider.chain_spec();
126        let chain_id = Some(chain_spec.chain().id());
127
128        // Build subblocks signatures system transaction
129        let subblocks_metadata = subblocks
130            .iter()
131            .map(|s| s.metadata())
132            .collect::<Vec<SubBlockMetadata>>();
133        let subblocks_input = alloy_rlp::encode(&subblocks_metadata)
134            .into_iter()
135            .chain(block_env.number.to_be_bytes_vec())
136            .collect();
137
138        let subblocks_signatures_tx = Recovered::new_unchecked(
139            TempoTxEnvelope::Legacy(Signed::new_unhashed(
140                TxLegacy {
141                    chain_id,
142                    nonce: 0,
143                    gas_price: 0,
144                    gas_limit: 0,
145                    to: Address::ZERO.into(),
146                    value: U256::ZERO,
147                    input: subblocks_input,
148                },
149                TEMPO_SYSTEM_TX_SIGNATURE,
150            )),
151            TEMPO_SYSTEM_TX_SENDER,
152        );
153
154        vec![subblocks_signatures_tx]
155    }
156}
157
158impl<Provider> PayloadBuilder for TempoPayloadBuilder<Provider>
159where
160    Provider:
161        StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + Clone + 'static,
162{
163    type Attributes = TempoPayloadBuilderAttributes;
164    type BuiltPayload = TempoBuiltPayload;
165
166    fn try_build(
167        &self,
168        args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
169    ) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError> {
170        self.build_payload(
171            args,
172            |attributes| self.pool.best_transactions_with_attributes(attributes),
173            false,
174        )
175    }
176
177    fn on_missing_payload(
178        &self,
179        _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
180    ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
181        MissingPayloadBehaviour::AwaitInProgress
182    }
183
184    fn build_empty_payload(
185        &self,
186        config: PayloadConfig<Self::Attributes, TempoHeader>,
187    ) -> Result<Self::BuiltPayload, PayloadBuilderError> {
188        self.build_payload(
189            BuildArguments::new(
190                Default::default(),
191                config,
192                Default::default(),
193                Default::default(),
194            ),
195            |_| core::iter::empty(),
196            true,
197        )?
198        .into_payload()
199        .ok_or_else(|| PayloadBuilderError::MissingPayload)
200    }
201}
202
203impl<Provider> TempoPayloadBuilder<Provider>
204where
205    Provider: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec>,
206{
207    #[instrument(
208        target = "payload_builder",
209        skip_all,
210        fields(
211            id = %args.config.attributes.payload_id(),
212            parent_number = %args.config.parent_header.number(),
213            parent_hash = %args.config.parent_header.hash()
214        )
215    )]
216    fn build_payload<Txs>(
217        &self,
218        args: BuildArguments<TempoPayloadBuilderAttributes, TempoBuiltPayload>,
219        best_txs: impl FnOnce(BestTransactionsAttributes) -> Txs,
220        empty: bool,
221    ) -> Result<BuildOutcome<TempoBuiltPayload>, PayloadBuilderError>
222    where
223        Txs: BestTransactions<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
224    {
225        let BuildArguments {
226            mut cached_reads,
227            config,
228            cancel,
229            best_payload,
230        } = args;
231        let PayloadConfig {
232            parent_header,
233            attributes,
234        } = config;
235
236        let start = Instant::now();
237
238        let block_time_millis =
239            (attributes.timestamp_millis() - parent_header.timestamp_millis()) as f64;
240        self.metrics.block_time_millis.record(block_time_millis);
241        self.metrics.block_time_millis_last.set(block_time_millis);
242
243        let state_setup_start = Instant::now();
244        let _state_setup_span = debug_span!(target: "payload_builder", "state_setup").entered();
245        let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
246        let state_provider: Box<dyn StateProvider> = if self.state_provider_metrics {
247            Box::new(InstrumentedStateProvider::new(state_provider, "builder"))
248        } else {
249            state_provider
250        };
251        let state = StateProviderDatabase::new(&state_provider);
252        let mut db = State::builder()
253            .with_database(if self.disable_state_cache {
254                Box::new(state) as Box<dyn Database<Error = ProviderError>>
255            } else {
256                Box::new(cached_reads.as_db_mut(state))
257            })
258            .with_bundle_update()
259            .build();
260        drop(_state_setup_span);
261        self.metrics
262            .state_setup_duration_seconds
263            .record(state_setup_start.elapsed());
264
265        let chain_spec = self.provider.chain_spec();
266        let is_osaka = self
267            .provider
268            .chain_spec()
269            .is_osaka_active_at_timestamp(attributes.timestamp());
270
271        let block_gas_limit: u64 = parent_header.gas_limit();
272        let shared_gas_limit = block_gas_limit / TEMPO_SHARED_GAS_DIVISOR;
273        // Non-shared gas limit is the maximum gas available for proposer's pool transactions.
274        // The remaining `shared_gas_limit` is reserved for validator subblocks.
275        let non_shared_gas_limit = block_gas_limit - shared_gas_limit;
276        let general_gas_limit = chain_spec.general_gas_limit_at(
277            attributes.timestamp(),
278            block_gas_limit,
279            shared_gas_limit,
280        );
281
282        let mut cumulative_gas_used = 0;
283        let mut non_payment_gas_used = 0;
284        // initial block size usage - size of withdrawals plus 1Kb of overhead for the block header
285        let mut block_size_used = attributes.withdrawals().length() + 1024;
286        let mut payment_transactions = 0u64;
287        let mut total_fees = U256::ZERO;
288
289        // If building an empty payload, don't include any subblocks
290        //
291        // Also don't include any subblocks if we've seen an invalid subblock
292        // at this height or above.
293        let mut subblocks = if empty
294            || self.highest_invalid_subblock.load(Ordering::Relaxed) > parent_header.number()
295        {
296            vec![]
297        } else {
298            attributes.subblocks()
299        };
300
301        subblocks.retain(|subblock| {
302            // Edge case: remove subblocks with expired transactions
303            //
304            // We pre-validate all of the subblocks on top of parent state in subblocks service
305            // which leaves the only reason for transactions to get invalidated by expiry of
306            // `valid_before` field.
307            if has_expired_transactions(subblock, attributes.timestamp()) {
308                self.metrics.inc_subblocks_expired();
309                return false;
310            }
311
312            // Account for the subblock's size
313            block_size_used += subblock.total_tx_size();
314
315            true
316        });
317
318        let subblock_fee_recipients = subblocks
319            .iter()
320            .map(|subblock| {
321                (
322                    PartialValidatorKey::from_slice(&subblock.validator()[..15]),
323                    subblock.fee_recipient,
324                )
325            })
326            .collect();
327
328        let mut builder = self
329            .evm_config
330            .builder_for_next_block(
331                &mut db,
332                &parent_header,
333                TempoNextBlockEnvAttributes {
334                    inner: NextBlockEnvAttributes {
335                        timestamp: attributes.timestamp(),
336                        suggested_fee_recipient: attributes.suggested_fee_recipient(),
337                        prev_randao: attributes.prev_randao(),
338                        gas_limit: block_gas_limit,
339                        parent_beacon_block_root: attributes.parent_beacon_block_root(),
340                        withdrawals: Some(attributes.withdrawals().clone()),
341                        extra_data: attributes.extra_data().clone(),
342                    },
343                    general_gas_limit,
344                    shared_gas_limit,
345                    timestamp_millis_part: attributes.timestamp_millis_part(),
346                    subblock_fee_recipients,
347                },
348            )
349            .map_err(PayloadBuilderError::other)?;
350
351        builder.apply_pre_execution_changes().map_err(|err| {
352            warn!(%err, "failed to apply pre-execution changes");
353            PayloadBuilderError::Internal(err.into())
354        })?;
355
356        debug!("building new payload");
357
358        // Prepare system transactions before actual block building and account for their size.
359        let prepare_system_txs_start = Instant::now();
360        let system_txs = self.build_seal_block_txs(builder.evm().block(), &subblocks);
361        for tx in &system_txs {
362            block_size_used += tx.inner().length();
363        }
364        let prepare_system_txs_elapsed = prepare_system_txs_start.elapsed();
365        self.metrics
366            .prepare_system_transactions_duration_seconds
367            .record(prepare_system_txs_elapsed);
368
369        let base_fee = builder.evm_mut().block().basefee;
370        let pool_fetch_start = Instant::now();
371        let mut best_txs = best_txs(BestTransactionsAttributes::new(
372            base_fee,
373            builder
374                .evm_mut()
375                .block()
376                .blob_gasprice()
377                .map(|gasprice| gasprice as u64),
378        ));
379        self.metrics
380            .pool_fetch_duration_seconds
381            .record(pool_fetch_start.elapsed());
382
383        let execution_start = Instant::now();
384        let _block_fill_span = debug_span!(target: "payload_builder", "block_fill").entered();
385        while let Some(pool_tx) = best_txs.next() {
386            // Ensure we still have capacity for this transaction within the non-shared gas limit.
387            // The remaining `shared_gas_limit` is reserved for validator subblocks and must not
388            // be consumed by proposer's pool transactions.
389            if cumulative_gas_used + pool_tx.gas_limit() > non_shared_gas_limit {
390                // Mark this transaction as invalid since it doesn't fit
391                // The iterator will handle lane switching internally when appropriate
392                best_txs.mark_invalid(
393                    &pool_tx,
394                    &InvalidPoolTransactionError::ExceedsGasLimit(
395                        pool_tx.gas_limit(),
396                        non_shared_gas_limit - cumulative_gas_used,
397                    ),
398                );
399                self.metrics
400                    .inc_pool_tx_skipped("exceeds_non_shared_gas_limit");
401                continue;
402            }
403
404            // If the tx is not a payment and will exceed the general gas limit
405            // mark the tx as invalid and continue
406            if !pool_tx.transaction.is_payment()
407                && non_payment_gas_used + pool_tx.gas_limit() > general_gas_limit
408            {
409                best_txs.mark_invalid(
410                    &pool_tx,
411                    &InvalidPoolTransactionError::Other(Box::new(
412                        TempoPoolTransactionError::ExceedsNonPaymentLimit,
413                    )),
414                );
415                self.metrics
416                    .inc_pool_tx_skipped("exceeds_general_gas_limit");
417                continue;
418            }
419
420            // check if the job was interrupted, if so we can skip remaining transactions
421            if attributes.is_interrupted() {
422                break;
423            }
424
425            // check if the job was cancelled, if so we can exit early
426            if cancel.is_cancelled() {
427                return Ok(BuildOutcome::Cancelled);
428            }
429
430            let is_payment = pool_tx.transaction.is_payment();
431            if is_payment {
432                payment_transactions += 1;
433            }
434
435            let tx_rlp_length = pool_tx.transaction.inner().length();
436            let estimated_block_size_with_tx = block_size_used + tx_rlp_length;
437
438            if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
439                best_txs.mark_invalid(
440                    &pool_tx,
441                    &InvalidPoolTransactionError::OversizedData {
442                        size: estimated_block_size_with_tx,
443                        limit: MAX_RLP_BLOCK_SIZE,
444                    },
445                );
446                self.metrics.inc_pool_tx_skipped("oversized_block");
447                continue;
448            }
449
450            let effective_gas_price = pool_tx.transaction.effective_gas_price(Some(base_fee));
451
452            let tx_debug_repr = tracing::enabled!(Level::TRACE)
453                .then(|| format!("{:?}", pool_tx.transaction))
454                .unwrap_or_default();
455
456            let tx_with_env = pool_tx.transaction.clone().into_with_tx_env();
457            let tx_execution_start = Instant::now();
458            let gas_used = match builder.execute_transaction(tx_with_env) {
459                Ok(gas_used) => gas_used,
460                Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
461                    error,
462                    ..
463                })) => {
464                    if error.is_nonce_too_low() {
465                        // if the nonce is too low, we can skip this transaction
466                        trace!(%error, tx = %tx_debug_repr, "skipping nonce too low transaction");
467                        self.metrics.inc_pool_tx_skipped("nonce_too_low");
468                    } else {
469                        // if the transaction is invalid, we can skip it and all of its
470                        // descendants
471                        trace!(%error, tx = %tx_debug_repr, "skipping invalid transaction and its descendants");
472                        best_txs.mark_invalid(
473                            &pool_tx,
474                            &InvalidPoolTransactionError::Consensus(
475                                InvalidTransactionError::TxTypeNotSupported,
476                            ),
477                        );
478                        self.metrics.inc_pool_tx_skipped("invalid_tx");
479                    }
480                    continue;
481                }
482                // this is an error that we should treat as fatal for this attempt
483                Err(err) => return Err(PayloadBuilderError::evm(err)),
484            };
485            let elapsed = tx_execution_start.elapsed();
486            self.metrics
487                .transaction_execution_duration_seconds
488                .record(elapsed);
489            trace!(?elapsed, "Transaction executed");
490
491            // update and add to total fees
492            total_fees += calc_gas_balance_spending(gas_used, effective_gas_price);
493            cumulative_gas_used += gas_used;
494            if !is_payment {
495                non_payment_gas_used += gas_used;
496            }
497            block_size_used += tx_rlp_length;
498        }
499        drop(_block_fill_span);
500        let total_normal_transaction_execution_elapsed = execution_start.elapsed();
501        self.metrics
502            .total_normal_transaction_execution_duration_seconds
503            .record(total_normal_transaction_execution_elapsed);
504        self.metrics
505            .payment_transactions
506            .record(payment_transactions as f64);
507        self.metrics
508            .payment_transactions_last
509            .set(payment_transactions as f64);
510
511        // check if we have a better block or received more subblocks
512        if !is_better_payload(best_payload.as_ref(), total_fees)
513            && !is_more_subblocks(best_payload.as_ref(), &subblocks)
514        {
515            // Release db
516            drop(builder);
517            drop(db);
518            // can skip building the block
519            return Ok(BuildOutcome::Aborted {
520                fees: total_fees,
521                cached_reads,
522            });
523        }
524
525        let subblocks_start = Instant::now();
526        let _subblock_txs_span =
527            debug_span!(target: "payload_builder", "execute_subblock_txs").entered();
528        let subblocks_count = subblocks.len() as f64;
529        let mut subblock_transactions = 0f64;
530        // Apply subblock transactions
531        for subblock in &subblocks {
532            let subblock_start = Instant::now();
533            let mut subblock_tx_count = 0f64;
534
535            for tx in subblock.transactions_recovered() {
536                if let Err(err) = builder.execute_transaction(tx.cloned()) {
537                    if let BlockExecutionError::Validation(BlockValidationError::InvalidTx {
538                        ..
539                    }) = &err
540                    {
541                        error!(
542                            ?err,
543                            "subblock transaction failed execution, aborting payload building"
544                        );
545                        self.highest_invalid_subblock
546                            .store(builder.evm().block().number.to(), Ordering::Relaxed);
547                        self.metrics.inc_build_failure("subblock_invalid_tx");
548                        return Err(PayloadBuilderError::evm(err));
549                    } else {
550                        return Err(PayloadBuilderError::evm(err));
551                    }
552                }
553
554                subblock_tx_count += 1.0;
555            }
556
557            self.metrics
558                .subblock_execution_duration_seconds
559                .record(subblock_start.elapsed());
560            self.metrics
561                .subblock_transaction_count
562                .record(subblock_tx_count);
563            subblock_transactions += subblock_tx_count;
564        }
565        drop(_subblock_txs_span);
566        let total_subblock_transaction_execution_elapsed = subblocks_start.elapsed();
567        self.metrics
568            .total_subblock_transaction_execution_duration_seconds
569            .record(total_subblock_transaction_execution_elapsed);
570        self.metrics.subblocks.record(subblocks_count);
571        self.metrics.subblocks_last.set(subblocks_count);
572        self.metrics
573            .subblock_transactions
574            .record(subblock_transactions);
575        self.metrics
576            .subblock_transactions_last
577            .set(subblock_transactions);
578
579        // Apply system transactions
580        let system_txs_execution_start = Instant::now();
581        let _system_txs_span =
582            debug_span!(target: "payload_builder", "execute_system_txs").entered();
583        for system_tx in system_txs {
584            builder
585                .execute_transaction(system_tx)
586                .map_err(PayloadBuilderError::evm)?;
587        }
588        drop(_system_txs_span);
589        let system_txs_execution_elapsed = system_txs_execution_start.elapsed();
590        self.metrics
591            .system_transactions_execution_duration_seconds
592            .record(system_txs_execution_elapsed);
593
594        let total_transaction_execution_elapsed = execution_start.elapsed();
595        self.metrics
596            .total_transaction_execution_duration_seconds
597            .record(total_transaction_execution_elapsed);
598
599        let builder_finish_start = Instant::now();
600        let _finish_span = debug_span!(target: "payload_builder", "finish_block").entered();
601        let instrumented_provider = InstrumentedFinishProvider {
602            inner: &*state_provider,
603            metrics: self.metrics.clone(),
604        };
605        let BlockBuilderOutcome {
606            execution_result,
607            block,
608            hashed_state,
609            trie_updates,
610        } = builder.finish(instrumented_provider)?;
611        drop(_finish_span);
612        let builder_finish_elapsed = builder_finish_start.elapsed();
613        self.metrics
614            .payload_finalization_duration_seconds
615            .record(builder_finish_elapsed);
616
617        let total_transactions = block.transaction_count();
618        self.metrics
619            .total_transactions
620            .record(total_transactions as f64);
621        self.metrics
622            .total_transactions_last
623            .set(total_transactions as f64);
624
625        let gas_used = block.gas_used();
626        self.metrics.gas_used.record(gas_used as f64);
627        self.metrics.gas_used_last.set(gas_used as f64);
628        self.metrics
629            .general_gas_used_last
630            .set(non_payment_gas_used as f64);
631        self.metrics
632            .payment_gas_used_last
633            .set(cumulative_gas_used as f64 - non_payment_gas_used as f64);
634        self.metrics
635            .general_gas_limit_last
636            .set(general_gas_limit as f64);
637        self.metrics
638            .payment_gas_limit_last
639            .set(non_shared_gas_limit as f64 - general_gas_limit as f64);
640        self.metrics
641            .shared_gas_limit_last
642            .set(shared_gas_limit as f64);
643
644        let requests = chain_spec
645            .is_prague_active_at_timestamp(attributes.timestamp())
646            .then(|| execution_result.requests.clone());
647
648        let sealed_block = Arc::new(block.sealed_block().clone());
649        let rlp_length = sealed_block.rlp_length();
650
651        if is_osaka && rlp_length > MAX_RLP_BLOCK_SIZE {
652            return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
653                rlp_length,
654                max_rlp_length: MAX_RLP_BLOCK_SIZE,
655            }));
656        }
657
658        let elapsed = start.elapsed();
659        self.metrics.payload_build_duration_seconds.record(elapsed);
660        let gas_per_second = sealed_block.gas_used() as f64 / elapsed.as_secs_f64();
661        self.metrics.gas_per_second.record(gas_per_second);
662        self.metrics.gas_per_second_last.set(gas_per_second);
663        self.metrics.rlp_block_size_bytes.record(rlp_length as f64);
664        self.metrics
665            .rlp_block_size_bytes_last
666            .set(rlp_length as f64);
667
668        info!(
669            parent_hash = ?sealed_block.parent_hash(),
670            number = sealed_block.number(),
671            hash = ?sealed_block.hash(),
672            timestamp = sealed_block.timestamp_millis(),
673            gas_limit = sealed_block.gas_limit(),
674            gas_used,
675            extra_data = %sealed_block.extra_data(),
676            subblocks_count,
677            payment_transactions,
678            subblock_transactions,
679            total_transactions,
680            ?elapsed,
681            ?total_normal_transaction_execution_elapsed,
682            ?total_subblock_transaction_execution_elapsed,
683            ?total_transaction_execution_elapsed,
684            ?builder_finish_elapsed,
685            "Built payload"
686        );
687
688        let eth_payload =
689            EthBuiltPayload::new(attributes.payload_id(), sealed_block, total_fees, requests);
690
691        let execution_output = BlockExecutionOutput {
692            result: execution_result,
693            state: db.take_bundle(),
694        };
695
696        let executed_block = BuiltPayloadExecutedBlock {
697            recovered_block: Arc::new(block),
698            execution_output: Arc::new(execution_output),
699            hashed_state: Either::Left(Arc::new(hashed_state)),
700            trie_updates: Either::Left(Arc::new(trie_updates)),
701        };
702
703        let payload = TempoBuiltPayload::new(eth_payload, Some(executed_block));
704
705        drop(db);
706        Ok(BuildOutcome::Better {
707            payload,
708            cached_reads,
709        })
710    }
711}
712
713pub fn is_more_subblocks(
714    best_payload: Option<&TempoBuiltPayload>,
715    subblocks: &[RecoveredSubBlock],
716) -> bool {
717    let Some(best_payload) = best_payload else {
718        return false;
719    };
720    let Some(best_metadata) = best_payload
721        .block()
722        .body()
723        .transactions
724        .iter()
725        .rev()
726        .find_map(|tx| Vec::<SubBlockMetadata>::decode(&mut tx.input().as_ref()).ok())
727    else {
728        return false;
729    };
730
731    subblocks.len() > best_metadata.len()
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use alloy_consensus::BlockBody;
738    use alloy_primitives::{Address, B256, Bytes, Signature};
739    use reth_payload_builder::PayloadId;
740    use reth_primitives_traits::SealedBlock;
741    use tempo_primitives::{
742        AASigned, Block, SignedSubBlock, SubBlock, SubBlockVersion, TempoSignature,
743        TempoTransaction,
744    };
745
746    trait TestExt {
747        fn random() -> Self;
748        fn with_valid_before(_: Option<u64>) -> Self
749        where
750            Self: Sized,
751        {
752            Self::random()
753        }
754    }
755
756    impl TestExt for SubBlockMetadata {
757        fn random() -> Self {
758            Self {
759                version: SubBlockVersion::V1,
760                validator: B256::random(),
761                fee_recipient: Address::random(),
762                signature: Bytes::new(),
763            }
764        }
765    }
766
767    impl TestExt for RecoveredSubBlock {
768        fn random() -> Self {
769            Self::with_valid_before(None)
770        }
771
772        fn with_valid_before(valid_before: Option<u64>) -> Self {
773            let tx = TempoTxEnvelope::AA(AASigned::new_unhashed(
774                TempoTransaction {
775                    valid_before,
776                    ..Default::default()
777                },
778                TempoSignature::default(),
779            ));
780            let signed = SignedSubBlock {
781                inner: SubBlock {
782                    version: SubBlockVersion::V1,
783                    parent_hash: B256::random(),
784                    fee_recipient: Address::random(),
785                    transactions: vec![tx],
786                },
787                signature: Bytes::new(),
788            };
789            Self::new_unchecked(signed, vec![Address::ZERO], B256::ZERO)
790        }
791    }
792
793    fn payload_with_metadata(count: usize) -> TempoBuiltPayload {
794        let metadata: Vec<_> = (0..count).map(|_| SubBlockMetadata::random()).collect();
795        let input: Bytes = alloy_rlp::encode(&metadata).into();
796        let tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
797            TxLegacy {
798                chain_id: None,
799                nonce: 0,
800                gas_price: 0,
801                gas_limit: 0,
802                to: Address::random().into(),
803                value: U256::ZERO,
804                input,
805            },
806            Signature::test_signature(),
807        ));
808        let block = Block {
809            header: TempoHeader::default(),
810            body: BlockBody {
811                transactions: vec![tx],
812                ommers: vec![],
813                withdrawals: None,
814            },
815        };
816        let sealed = Arc::new(SealedBlock::seal_slow(block));
817        let eth = EthBuiltPayload::new(PayloadId::default(), sealed, U256::ZERO, None);
818        TempoBuiltPayload::new(eth, None)
819    }
820
821    #[test]
822    fn test_is_more_subblocks() {
823        // None payload always returns false
824        assert!(!is_more_subblocks(None, &[]));
825        assert!(!is_more_subblocks(None, &[RecoveredSubBlock::random()]));
826
827        // Equal count returns false (1 == 1)
828        let payload = payload_with_metadata(1);
829        assert!(!is_more_subblocks(
830            Some(&payload),
831            &[RecoveredSubBlock::random()]
832        ));
833
834        // More subblocks returns true (2 > 1)
835        assert!(is_more_subblocks(
836            Some(&payload),
837            &[RecoveredSubBlock::random(), RecoveredSubBlock::random()]
838        ));
839
840        // Fewer subblocks returns false (1 < 2)
841        let payload = payload_with_metadata(2);
842        assert!(!is_more_subblocks(
843            Some(&payload),
844            &[RecoveredSubBlock::random()]
845        ));
846
847        // Empty metadata, empty subblocks returns false (0 > 0 is false)
848        let payload = payload_with_metadata(0);
849        assert!(!is_more_subblocks(Some(&payload), &[]));
850
851        // Empty metadata, one subblock returns true (1 > 0)
852        assert!(is_more_subblocks(
853            Some(&payload),
854            &[RecoveredSubBlock::random()]
855        ));
856    }
857
858    #[test]
859    fn test_extra_data_flow_in_attributes() {
860        // Test that extra_data in attributes can be accessed correctly
861        let extra_data = Bytes::from(vec![42, 43, 44, 45, 46]);
862
863        let attrs = TempoPayloadBuilderAttributes::new(
864            PayloadId::default(),
865            B256::default(),
866            Address::default(),
867            1000,
868            extra_data.clone(),
869            Vec::new,
870        );
871
872        assert_eq!(attrs.extra_data(), &extra_data);
873
874        // Verify the data is as expected
875        let injected_data = attrs.extra_data().clone();
876
877        assert_eq!(injected_data, extra_data);
878    }
879
880    #[test]
881    fn test_has_expired_transactions_boundary() {
882        // valid_before == timestamp → expired
883        let subblock = RecoveredSubBlock::with_valid_before(Some(1000));
884        assert!(has_expired_transactions(&subblock, 1000));
885
886        // valid_before < timestamp → expired
887        assert!(has_expired_transactions(&subblock, 1001));
888
889        // valid_before > timestamp → NOT expired
890        assert!(!has_expired_transactions(&subblock, 999));
891
892        // No valid_before → NOT expired
893        let subblock_no_expiry = RecoveredSubBlock::with_valid_before(None);
894        assert!(!has_expired_transactions(&subblock_no_expiry, 1000));
895    }
896}