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::TempoPayloadBuilderMetrics;
9use alloy_consensus::{BlockHeader as _, Signed, Transaction, TxLegacy};
10use alloy_primitives::{Address, U256};
11use alloy_rlp::{Decodable, Encodable};
12use alloy_sol_types::SolCall;
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_errors::ConsensusError;
20use reth_evm::{
21    ConfigureEvm, Database, Evm, NextBlockEnvAttributes,
22    block::{BlockExecutionError, BlockValidationError},
23    execute::{BlockBuilder, BlockBuilderOutcome},
24};
25use reth_payload_builder::{EthBuiltPayload, PayloadBuilderError};
26use reth_payload_primitives::PayloadBuilderAttributes;
27use reth_primitives_traits::{Recovered, transaction::error::InvalidTransactionError};
28use reth_revm::{
29    State,
30    context::{Block, BlockEnv},
31    database::StateProviderDatabase,
32};
33use reth_storage_api::StateProviderFactory;
34use reth_transaction_pool::{
35    BestTransactions, BestTransactionsAttributes, TransactionPool, ValidPoolTransaction,
36    error::InvalidPoolTransactionError,
37};
38use std::{
39    sync::{
40        Arc,
41        atomic::{AtomicU64, Ordering},
42    },
43    time::Instant,
44};
45use tempo_chainspec::TempoChainSpec;
46use tempo_consensus::{TEMPO_GENERAL_GAS_DIVISOR, TEMPO_SHARED_GAS_DIVISOR};
47use tempo_evm::{TempoEvmConfig, TempoNextBlockEnvAttributes, evm::TempoEvm};
48use tempo_payload_types::TempoPayloadBuilderAttributes;
49use tempo_precompiles::{
50    STABLECOIN_EXCHANGE_ADDRESS, TIP_FEE_MANAGER_ADDRESS, TIP20_REWARDS_REGISTRY_ADDRESS,
51    stablecoin_exchange::IStablecoinExchange, tip_fee_manager::IFeeManager,
52    tip20_rewards_registry::ITIP20RewardsRegistry,
53};
54use tempo_primitives::{
55    RecoveredSubBlock, SubBlockMetadata, TempoHeader, TempoPrimitives, TempoTxEnvelope,
56    subblock::PartialValidatorKey,
57    transaction::{
58        calc_gas_balance_spending,
59        envelope::{TEMPO_SYSTEM_TX_SENDER, TEMPO_SYSTEM_TX_SIGNATURE},
60    },
61};
62use tempo_transaction_pool::{
63    TempoTransactionPool,
64    transaction::{TempoPoolTransactionError, TempoPooledTransaction},
65};
66use tracing::{Level, debug, error, info, instrument, trace, warn};
67
68#[derive(Debug, Clone)]
69pub struct TempoPayloadBuilder<Provider> {
70    pool: TempoTransactionPool<Provider>,
71    provider: Provider,
72    evm_config: TempoEvmConfig,
73    metrics: TempoPayloadBuilderMetrics,
74    /// Height at which we've seen an invalid subblock.
75    ///
76    /// We pre-validate all of the subblock transactions when collecting subblocks, so this
77    /// should never be set because subblocks with invalid transactions should never make it to the payload builder.
78    ///
79    /// However, due to disruptive nature of subblock-related bugs (invalid subblock
80    /// we're continuously failing to apply halts block building), we protect against this by tracking
81    /// last height at which we've seen an invalid subblock, and not including any subblocks
82    /// at this height for any payloads.
83    highest_invalid_subblock: Arc<AtomicU64>,
84}
85
86impl<Provider> TempoPayloadBuilder<Provider> {
87    pub fn new(
88        pool: TempoTransactionPool<Provider>,
89        provider: Provider,
90        evm_config: TempoEvmConfig,
91    ) -> Self {
92        Self {
93            pool,
94            provider,
95            evm_config,
96            metrics: TempoPayloadBuilderMetrics::default(),
97            highest_invalid_subblock: Default::default(),
98        }
99    }
100}
101
102impl<Provider: ChainSpecProvider> TempoPayloadBuilder<Provider> {
103    /// Builds system transactions to execute at the start of the block.
104    ///
105    /// Returns a vector of system transactions that must be executed at the beginning of each block:
106    /// 1. TIP20 Rewards Registry finalizeStreams - finalizes expired reward streams
107    fn build_start_block_txs(
108        &self,
109        evm: &TempoEvm<impl Database>,
110    ) -> Vec<Recovered<TempoTxEnvelope>> {
111        // No start of block system transactions after moderato.
112        if evm.ctx().cfg.spec.is_moderato() {
113            return vec![];
114        }
115
116        let chain_id = Some(self.provider.chain_spec().chain().id());
117
118        // Build rewards registry system transaction
119        let rewards_registry_input = ITIP20RewardsRegistry::finalizeStreamsCall {}
120            .abi_encode()
121            .into_iter()
122            .chain(evm.block().number.to_be_bytes_vec())
123            .collect();
124
125        let rewards_registry_tx = Recovered::new_unchecked(
126            TempoTxEnvelope::Legacy(Signed::new_unhashed(
127                TxLegacy {
128                    chain_id,
129                    nonce: 0,
130                    gas_price: 0,
131                    gas_limit: 0,
132                    to: TIP20_REWARDS_REGISTRY_ADDRESS.into(),
133                    value: U256::ZERO,
134                    input: rewards_registry_input,
135                },
136                TEMPO_SYSTEM_TX_SIGNATURE,
137            )),
138            TEMPO_SYSTEM_TX_SENDER,
139        );
140
141        vec![rewards_registry_tx]
142    }
143
144    /// Builds system transactions to seal the block.
145    ///
146    /// Returns a vector of system transactions that must be executed at the end of each block:
147    /// 1. Fee manager executeBlock - processes collected fees
148    /// 2. Stablecoin exchange executeBlock - commits pending orders
149    /// 3. Subblocks signatures - validates subblock signatures
150    fn build_seal_block_txs(
151        &self,
152        block_env: &BlockEnv,
153        subblocks: &[RecoveredSubBlock],
154    ) -> Vec<Recovered<TempoTxEnvelope>> {
155        let chain_id = Some(self.provider.chain_spec().chain().id());
156
157        // Build fee manager system transaction
158        let fee_manager_input = IFeeManager::executeBlockCall
159            .abi_encode()
160            .into_iter()
161            .chain(block_env.number.to_be_bytes_vec())
162            .collect();
163
164        let fee_manager_tx = Recovered::new_unchecked(
165            TempoTxEnvelope::Legacy(Signed::new_unhashed(
166                TxLegacy {
167                    chain_id,
168                    nonce: 0,
169                    gas_price: 0,
170                    gas_limit: 0,
171                    to: TIP_FEE_MANAGER_ADDRESS.into(),
172                    value: U256::ZERO,
173                    input: fee_manager_input,
174                },
175                TEMPO_SYSTEM_TX_SIGNATURE,
176            )),
177            TEMPO_SYSTEM_TX_SENDER,
178        );
179
180        // Build stablecoin exchange system transaction
181        let stablecoin_exchange_input = IStablecoinExchange::executeBlockCall {}
182            .abi_encode()
183            .into_iter()
184            .chain(block_env.number.to_be_bytes_vec())
185            .collect();
186
187        let stablecoin_exchange_tx = Recovered::new_unchecked(
188            TempoTxEnvelope::Legacy(Signed::new_unhashed(
189                TxLegacy {
190                    chain_id,
191                    nonce: 0,
192                    gas_price: 0,
193                    gas_limit: 0,
194                    to: STABLECOIN_EXCHANGE_ADDRESS.into(),
195                    value: U256::ZERO,
196                    input: stablecoin_exchange_input,
197                },
198                TEMPO_SYSTEM_TX_SIGNATURE,
199            )),
200            TEMPO_SYSTEM_TX_SENDER,
201        );
202
203        // Build subblocks signatures system transaction
204        let subblocks_metadata = subblocks
205            .iter()
206            .map(|s| s.metadata())
207            .collect::<Vec<SubBlockMetadata>>();
208        let subblocks_input = alloy_rlp::encode(&subblocks_metadata)
209            .into_iter()
210            .chain(block_env.number.to_be_bytes_vec())
211            .collect();
212
213        let subblocks_signatures_tx = Recovered::new_unchecked(
214            TempoTxEnvelope::Legacy(Signed::new_unhashed(
215                TxLegacy {
216                    chain_id,
217                    nonce: 0,
218                    gas_price: 0,
219                    gas_limit: 0,
220                    to: Address::ZERO.into(),
221                    value: U256::ZERO,
222                    input: subblocks_input,
223                },
224                TEMPO_SYSTEM_TX_SIGNATURE,
225            )),
226            TEMPO_SYSTEM_TX_SENDER,
227        );
228
229        vec![
230            fee_manager_tx,
231            stablecoin_exchange_tx,
232            subblocks_signatures_tx,
233        ]
234    }
235}
236
237impl<Provider> PayloadBuilder for TempoPayloadBuilder<Provider>
238where
239    Provider:
240        StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + Clone + 'static,
241{
242    type Attributes = TempoPayloadBuilderAttributes;
243    type BuiltPayload = EthBuiltPayload<TempoPrimitives>;
244
245    fn try_build(
246        &self,
247        args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
248    ) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError> {
249        self.build_payload(
250            args,
251            |attributes| self.pool.best_transactions_with_attributes(attributes),
252            false,
253        )
254    }
255
256    fn on_missing_payload(
257        &self,
258        _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
259    ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
260        MissingPayloadBehaviour::AwaitInProgress
261    }
262
263    fn build_empty_payload(
264        &self,
265        config: PayloadConfig<Self::Attributes, TempoHeader>,
266    ) -> Result<Self::BuiltPayload, PayloadBuilderError> {
267        self.build_payload(
268            BuildArguments::new(
269                Default::default(),
270                config,
271                Default::default(),
272                Default::default(),
273            ),
274            |_| core::iter::empty(),
275            true,
276        )?
277        .into_payload()
278        .ok_or_else(|| PayloadBuilderError::MissingPayload)
279    }
280}
281
282impl<Provider> TempoPayloadBuilder<Provider>
283where
284    Provider: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec>,
285{
286    #[instrument(
287        target = "payload_builder",
288        skip_all,
289        fields(
290            id = %args.config.attributes.payload_id(),
291            parent_number = %args.config.parent_header.number(),
292            parent_hash = %args.config.parent_header.hash()
293        )
294    )]
295    fn build_payload<Txs>(
296        &self,
297        args: BuildArguments<TempoPayloadBuilderAttributes, EthBuiltPayload<TempoPrimitives>>,
298        best_txs: impl FnOnce(BestTransactionsAttributes) -> Txs,
299        empty: bool,
300    ) -> Result<BuildOutcome<EthBuiltPayload<TempoPrimitives>>, PayloadBuilderError>
301    where
302        Txs: BestTransactions<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
303    {
304        let BuildArguments {
305            mut cached_reads,
306            config,
307            cancel,
308            best_payload,
309        } = args;
310        let PayloadConfig {
311            parent_header,
312            attributes,
313        } = config;
314
315        let start = Instant::now();
316
317        let block_time_millis =
318            (attributes.timestamp_millis() - parent_header.timestamp_millis()) as f64;
319        self.metrics.block_time_millis.record(block_time_millis);
320        self.metrics.block_time_millis_last.set(block_time_millis);
321
322        let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
323        let state = StateProviderDatabase::new(&state_provider);
324        let mut db = State::builder()
325            .with_database(cached_reads.as_db_mut(state))
326            .with_bundle_update()
327            .build();
328
329        let chain_spec = self.provider.chain_spec();
330        let is_osaka = self
331            .provider
332            .chain_spec()
333            .is_osaka_active_at_timestamp(attributes.timestamp());
334
335        let block_gas_limit: u64 = parent_header.gas_limit();
336        let shared_gas_limit = block_gas_limit / TEMPO_SHARED_GAS_DIVISOR;
337        let non_shared_gas_limit = block_gas_limit - shared_gas_limit;
338        let general_gas_limit =
339            (parent_header.gas_limit() - shared_gas_limit) / TEMPO_GENERAL_GAS_DIVISOR;
340
341        let mut cumulative_gas_used = 0;
342        let mut non_payment_gas_used = 0;
343        // initial block size usage - size of withdrawals plus 1Kb of overhead for the block header
344        let mut block_size_used = attributes.withdrawals().length() + 1024;
345        let mut payment_transactions = 0u64;
346        let mut total_fees = U256::ZERO;
347
348        // If building an empty payload, don't include any subblocks
349        //
350        // Also don't include any subblocks if we've seen an invalid subblock
351        // at this height or above.
352        let mut subblocks = if empty
353            || self.highest_invalid_subblock.load(Ordering::Relaxed) > parent_header.number()
354        {
355            vec![]
356        } else {
357            attributes.subblocks()
358        };
359
360        subblocks.retain(|subblock| {
361            // Edge case: remove subblocks with expired transactions
362            //
363            // We pre-validate all of the subblocks on top of parent state in subblocks service
364            // which leaves the only reason for transactions to get invalidated by expiry of
365            // `valid_before` field.
366            if subblock.transactions.iter().any(|tx| {
367                tx.as_aa().is_some_and(|tx| {
368                    tx.tx()
369                        .valid_before
370                        .is_some_and(|valid| valid < attributes.timestamp())
371                })
372            }) {
373                return false;
374            }
375
376            // Account for the subblock's size
377            block_size_used += subblock.total_tx_size();
378
379            true
380        });
381
382        let subblock_fee_recipients = subblocks
383            .iter()
384            .map(|subblock| {
385                (
386                    PartialValidatorKey::from_slice(&subblock.validator()[..15]),
387                    subblock.fee_recipient,
388                )
389            })
390            .collect();
391
392        let mut builder = self
393            .evm_config
394            .builder_for_next_block(
395                &mut db,
396                &parent_header,
397                TempoNextBlockEnvAttributes {
398                    inner: NextBlockEnvAttributes {
399                        timestamp: attributes.timestamp(),
400                        suggested_fee_recipient: attributes.suggested_fee_recipient(),
401                        prev_randao: attributes.prev_randao(),
402                        gas_limit: block_gas_limit,
403                        parent_beacon_block_root: attributes.parent_beacon_block_root(),
404                        withdrawals: Some(attributes.withdrawals().clone()),
405                    },
406                    general_gas_limit,
407                    shared_gas_limit,
408                    timestamp_millis_part: attributes.timestamp_millis_part(),
409                    extra_data: attributes.extra_data().clone(),
410                    subblock_fee_recipients,
411                },
412            )
413            .map_err(PayloadBuilderError::other)?;
414
415        builder.apply_pre_execution_changes().map_err(|err| {
416            warn!(%err, "failed to apply pre-execution changes");
417            PayloadBuilderError::Internal(err.into())
418        })?;
419
420        debug!("building new payload");
421
422        // Prepare system transactions before actual block building and account for their size.
423        let prepare_system_txs_start = Instant::now();
424        let system_txs = self.build_seal_block_txs(builder.evm().block(), &subblocks);
425        for tx in &system_txs {
426            block_size_used += tx.inner().length();
427        }
428        let prepare_system_txs_elapsed = prepare_system_txs_start.elapsed();
429        self.metrics
430            .prepare_system_transactions_duration_seconds
431            .record(prepare_system_txs_elapsed);
432
433        let base_fee = builder.evm_mut().block().basefee;
434        let mut best_txs = best_txs(BestTransactionsAttributes::new(
435            base_fee,
436            builder
437                .evm_mut()
438                .block()
439                .blob_gasprice()
440                .map(|gasprice| gasprice as u64),
441        ));
442
443        // Execute start-of-block system transactions (rewards registry finalize)
444        let start_block_txs_execution_start = Instant::now();
445        for tx in self.build_start_block_txs(builder.evm()) {
446            block_size_used += tx.inner().length();
447
448            builder
449                .execute_transaction(tx)
450                .map_err(PayloadBuilderError::evm)?;
451        }
452        let start_block_txs_execution_elapsed = start_block_txs_execution_start.elapsed();
453        self.metrics
454            .start_block_txs_execution_duration_seconds
455            .record(start_block_txs_execution_elapsed);
456
457        let execution_start = Instant::now();
458        while let Some(pool_tx) = best_txs.next() {
459            // ensure we still have capacity for this transaction
460            if cumulative_gas_used + pool_tx.gas_limit() > non_shared_gas_limit {
461                // Mark this transaction as invalid since it doesn't fit
462                // The iterator will handle lane switching internally when appropriate
463                best_txs.mark_invalid(
464                    &pool_tx,
465                    &InvalidPoolTransactionError::ExceedsGasLimit(
466                        pool_tx.gas_limit(),
467                        non_shared_gas_limit - cumulative_gas_used,
468                    ),
469                );
470                continue;
471            }
472
473            // If the tx is not a payment and will exceed the general gas limit
474            // mark the tx as invalid and continue
475            if !pool_tx.transaction.is_payment()
476                && non_payment_gas_used + pool_tx.gas_limit() > general_gas_limit
477            {
478                best_txs.mark_invalid(
479                    &pool_tx,
480                    &InvalidPoolTransactionError::Other(Box::new(
481                        TempoPoolTransactionError::ExceedsNonPaymentLimit,
482                    )),
483                );
484                continue;
485            }
486
487            // check if the job was interrupted, if so we can skip remaining transactions
488            if attributes.is_interrupted() {
489                break;
490            }
491
492            // check if the job was cancelled, if so we can exit early
493            if cancel.is_cancelled() {
494                return Ok(BuildOutcome::Cancelled);
495            }
496
497            // convert tx to a signed transaction
498            let tx = pool_tx.to_consensus();
499            let is_payment = tx.is_payment();
500
501            if is_payment {
502                payment_transactions += 1;
503            }
504
505            let tx_rlp_length = tx.inner().length();
506            let estimated_block_size_with_tx = block_size_used + tx_rlp_length;
507
508            if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
509                best_txs.mark_invalid(
510                    &pool_tx,
511                    &InvalidPoolTransactionError::OversizedData {
512                        size: estimated_block_size_with_tx,
513                        limit: MAX_RLP_BLOCK_SIZE,
514                    },
515                );
516                continue;
517            }
518
519            let tx_rlp_length = tx.inner().length();
520            let effective_gas_price = tx.effective_gas_price(Some(base_fee));
521
522            let tx_debug_repr = tracing::enabled!(Level::TRACE)
523                .then(|| format!("{tx:?}"))
524                .unwrap_or_default();
525
526            let execution_start = Instant::now();
527            let gas_used = match builder.execute_transaction(tx) {
528                Ok(gas_used) => gas_used,
529                Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
530                    error,
531                    ..
532                })) => {
533                    if error.is_nonce_too_low() {
534                        // if the nonce is too low, we can skip this transaction
535                        trace!(%error, tx = %tx_debug_repr, "skipping nonce too low transaction");
536                    } else {
537                        // if the transaction is invalid, we can skip it and all of its
538                        // descendants
539                        trace!(%error, tx = %tx_debug_repr, "skipping invalid transaction and its descendants");
540                        best_txs.mark_invalid(
541                            &pool_tx,
542                            &InvalidPoolTransactionError::Consensus(
543                                InvalidTransactionError::TxTypeNotSupported,
544                            ),
545                        );
546                    }
547                    continue;
548                }
549                // this is an error that we should treat as fatal for this attempt
550                Err(err) => return Err(PayloadBuilderError::evm(err)),
551            };
552            let elapsed = execution_start.elapsed();
553            self.metrics
554                .transaction_execution_duration_seconds
555                .record(elapsed);
556            trace!(?elapsed, "Transaction executed");
557
558            // update and add to total fees
559            total_fees += calc_gas_balance_spending(gas_used, effective_gas_price);
560            cumulative_gas_used += gas_used;
561            if !is_payment {
562                non_payment_gas_used += gas_used;
563            }
564            block_size_used += tx_rlp_length;
565        }
566
567        // check if we have a better block or received more subblocks
568        if !is_better_payload(best_payload.as_ref(), total_fees)
569            && !is_more_subblocks(best_payload.as_ref(), &subblocks)
570        {
571            // Release db
572            drop(builder);
573            // can skip building the block
574            return Ok(BuildOutcome::Aborted {
575                fees: total_fees,
576                cached_reads,
577            });
578        }
579
580        // Apply subblock transactions
581        for subblock in &subblocks {
582            for tx in subblock.transactions_recovered() {
583                if let Err(err) = builder.execute_transaction(tx.cloned()) {
584                    if let BlockExecutionError::Validation(BlockValidationError::InvalidTx {
585                        ..
586                    }) = &err
587                    {
588                        error!(
589                            ?err,
590                            "subblock transaction failed execution, aborting payload building"
591                        );
592                        self.highest_invalid_subblock
593                            .store(builder.evm().block().number.to(), Ordering::Relaxed);
594
595                        return Err(PayloadBuilderError::evm(err));
596                    } else {
597                        return Err(PayloadBuilderError::evm(err));
598                    }
599                }
600            }
601        }
602
603        let execution_elapsed = execution_start.elapsed();
604        self.metrics
605            .total_transaction_execution_duration_seconds
606            .record(execution_elapsed);
607        self.metrics
608            .payment_transactions
609            .record(payment_transactions as f64);
610        self.metrics
611            .payment_transactions_last
612            .set(payment_transactions as f64);
613
614        // Apply system transactions
615        let system_txs_execution_start = Instant::now();
616        for system_tx in system_txs {
617            builder
618                .execute_transaction(system_tx)
619                .map_err(PayloadBuilderError::evm)?;
620        }
621        let system_txs_execution_elapsed = system_txs_execution_start.elapsed();
622        self.metrics
623            .system_transactions_execution_duration_seconds
624            .record(system_txs_execution_elapsed);
625
626        let builder_finish_start = Instant::now();
627        let BlockBuilderOutcome {
628            execution_result,
629            block,
630            ..
631        } = builder.finish(&state_provider)?;
632        let builder_finish_elapsed = builder_finish_start.elapsed();
633        self.metrics
634            .payload_finalization_duration_seconds
635            .record(builder_finish_elapsed);
636
637        let total_transactions = block.transaction_count();
638        self.metrics
639            .total_transactions
640            .record(total_transactions as f64);
641        self.metrics
642            .total_transactions_last
643            .set(total_transactions as f64);
644
645        let gas_used = block.gas_used();
646        self.metrics.gas_used.record(gas_used as f64);
647        self.metrics.gas_used_last.set(gas_used as f64);
648
649        let requests = chain_spec
650            .is_prague_active_at_timestamp(attributes.timestamp())
651            .then_some(execution_result.requests);
652
653        let sealed_block = Arc::new(block.sealed_block().clone());
654
655        if is_osaka && sealed_block.rlp_length() > MAX_RLP_BLOCK_SIZE {
656            return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
657                rlp_length: sealed_block.rlp_length(),
658                max_rlp_length: MAX_RLP_BLOCK_SIZE,
659            }));
660        }
661
662        let elapsed = start.elapsed();
663        self.metrics.payload_build_duration_seconds.record(elapsed);
664        let gas_per_second = sealed_block.gas_used() as f64 / elapsed.as_secs_f64();
665        self.metrics.gas_per_second.record(gas_per_second);
666        self.metrics.gas_per_second_last.set(gas_per_second);
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            total_transactions,
677            payment_transactions,
678            ?elapsed,
679            ?execution_elapsed,
680            ?builder_finish_elapsed,
681            "Built payload"
682        );
683
684        let payload =
685            EthBuiltPayload::new(attributes.payload_id(), sealed_block, total_fees, requests);
686
687        Ok(BuildOutcome::Better {
688            payload,
689            cached_reads,
690        })
691    }
692}
693
694pub fn is_more_subblocks(
695    best_payload: Option<&EthBuiltPayload<TempoPrimitives>>,
696    subblocks: &[RecoveredSubBlock],
697) -> bool {
698    let Some(best_payload) = best_payload else {
699        return false;
700    };
701    let Some(best_metadata) = best_payload
702        .block()
703        .body()
704        .transactions
705        .iter()
706        .rev()
707        .find_map(|tx| Vec::<SubBlockMetadata>::decode(&mut tx.input().as_ref()).ok())
708    else {
709        return false;
710    };
711
712    subblocks.len() > best_metadata.len()
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use alloy_primitives::{Address, B256, Bytes};
719    use reth_payload_builder::PayloadId;
720
721    #[test]
722    fn test_extra_data_flow_in_attributes() {
723        // Test that extra_data in attributes can be accessed correctly
724        let extra_data = Bytes::from(vec![42, 43, 44, 45, 46]);
725
726        let attrs = TempoPayloadBuilderAttributes::new(
727            PayloadId::default(),
728            B256::default(),
729            Address::default(),
730            1000,
731            extra_data.clone(),
732            Vec::new,
733        );
734
735        assert_eq!(attrs.extra_data(), &extra_data);
736
737        // Verify the data is as expected
738        let injected_data = attrs.extra_data().clone();
739
740        assert_eq!(injected_data, extra_data);
741    }
742}