1#![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 reth_basic_payload_builder::{
13 BuildArguments, BuildOutcome, MissingPayloadBehaviour, PayloadBuilder, PayloadConfig,
14 is_better_payload,
15};
16use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
17use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
18use reth_engine_tree::tree::instrumented_state::InstrumentedStateProvider;
19use reth_errors::{ConsensusError, ProviderError};
20use reth_evm::{
21 ConfigureEvm, Database, Evm, NextBlockEnvAttributes,
22 block::{BlockExecutionError, BlockExecutor, BlockValidationError, TxResult},
23 execute::{BlockBuilder, BlockBuilderOutcome},
24};
25use reth_execution_types::BlockExecutionOutput;
26use reth_payload_builder::{EthBuiltPayload, PayloadBuilderError};
27use reth_payload_primitives::{BuiltPayload, BuiltPayloadExecutedBlock};
28use reth_primitives_traits::{Recovered, transaction::error::InvalidTransactionError};
29use reth_revm::{State, context::Block, database::StateProviderDatabase};
30use reth_storage_api::{StateProvider, StateProviderFactory};
31use reth_transaction_pool::{
32 BestTransactions, BestTransactionsAttributes, TransactionPool, ValidPoolTransaction,
33 error::InvalidPoolTransactionError,
34};
35use std::{
36 sync::{
37 Arc,
38 atomic::{AtomicU64, Ordering},
39 },
40 time::{Duration, Instant},
41};
42use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
43use tempo_consensus::TEMPO_SHARED_GAS_DIVISOR;
44use tempo_evm::{TempoEvmConfig, TempoNextBlockEnvAttributes, TempoStateAccess, evm::TempoEvm};
45use tempo_payload_types::{TempoBuiltPayload, TempoPayloadAttributes};
46use tempo_precompiles::{tip_fee_manager::TipFeeManager, validator_config_v2::ValidatorConfigV2};
47use tempo_primitives::{
48 RecoveredSubBlock, SubBlockMetadata, TempoHeader, TempoTxEnvelope,
49 subblock::PartialValidatorKey,
50 transaction::{
51 calc_gas_balance_spending,
52 envelope::{TEMPO_SYSTEM_TX_SENDER, TEMPO_SYSTEM_TX_SIGNATURE},
53 },
54};
55use tempo_transaction_pool::{
56 StateAwareBestTransactions, TempoTransactionPool,
57 transaction::{TempoPoolTransactionError, TempoPooledTransaction},
58};
59use tracing::{Level, debug, debug_span, error, info, instrument, trace, warn};
60
61fn has_expired_transactions(subblock: &RecoveredSubBlock, timestamp: u64) -> bool {
63 subblock.transactions.iter().any(|tx| {
64 tx.as_aa().is_some_and(|tx| {
65 tx.tx()
66 .valid_before
67 .is_some_and(|valid| valid.get() <= timestamp)
68 })
69 })
70}
71
72#[derive(Debug, Clone)]
73pub struct TempoPayloadBuilder<Provider> {
74 pool: TempoTransactionPool<Provider>,
75 provider: Provider,
76 evm_config: TempoEvmConfig,
77 metrics: TempoPayloadBuilderMetrics,
78 highest_invalid_subblock: Arc<AtomicU64>,
88 is_dev: bool,
90 state_provider_metrics: bool,
92 disable_state_cache: bool,
94}
95
96impl<Provider> TempoPayloadBuilder<Provider> {
97 pub fn new(
98 pool: TempoTransactionPool<Provider>,
99 provider: Provider,
100 evm_config: TempoEvmConfig,
101 is_dev: bool,
102 state_provider_metrics: bool,
103 disable_state_cache: bool,
104 ) -> Self {
105 Self {
106 pool,
107 provider,
108 evm_config,
109 metrics: TempoPayloadBuilderMetrics::default(),
110 highest_invalid_subblock: Default::default(),
111 is_dev,
112 state_provider_metrics,
113 disable_state_cache,
114 }
115 }
116}
117
118impl<Provider: ChainSpecProvider<ChainSpec = TempoChainSpec>> TempoPayloadBuilder<Provider> {
119 fn build_seal_block_txs(
124 &self,
125 evm: &TempoEvm<impl Database>,
126 subblocks: &[RecoveredSubBlock],
127 ) -> Vec<Recovered<TempoTxEnvelope>> {
128 if subblocks.is_empty() && evm.cfg.spec.is_t4() {
129 return vec![];
131 }
132
133 let chain_spec = self.provider.chain_spec();
134 let chain_id = Some(chain_spec.chain().id());
135
136 let subblocks_metadata = subblocks
138 .iter()
139 .map(|s| s.metadata())
140 .collect::<Vec<SubBlockMetadata>>();
141 let subblocks_input = alloy_rlp::encode(&subblocks_metadata)
142 .into_iter()
143 .chain(evm.block.number.to_be_bytes_vec())
144 .collect();
145
146 let subblocks_signatures_tx = Recovered::new_unchecked(
147 TempoTxEnvelope::Legacy(Signed::new_unhashed(
148 TxLegacy {
149 chain_id,
150 nonce: 0,
151 gas_price: 0,
152 gas_limit: 0,
153 to: Address::ZERO.into(),
154 value: U256::ZERO,
155 input: subblocks_input,
156 },
157 TEMPO_SYSTEM_TX_SIGNATURE,
158 )),
159 TEMPO_SYSTEM_TX_SENDER,
160 );
161
162 vec![subblocks_signatures_tx]
163 }
164}
165
166impl<Provider> PayloadBuilder for TempoPayloadBuilder<Provider>
167where
168 Provider:
169 StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + Clone + 'static,
170{
171 type Attributes = TempoPayloadAttributes;
172 type BuiltPayload = TempoBuiltPayload;
173
174 fn try_build(
175 &self,
176 args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
177 ) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError> {
178 self.build_payload(
179 args,
180 |attributes| self.pool.best_transactions_with_attributes(attributes),
181 false,
182 )
183 }
184
185 fn on_missing_payload(
186 &self,
187 _args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
188 ) -> MissingPayloadBehaviour<Self::BuiltPayload> {
189 MissingPayloadBehaviour::AwaitInProgress
190 }
191
192 fn build_empty_payload(
193 &self,
194 config: PayloadConfig<Self::Attributes, TempoHeader>,
195 ) -> Result<Self::BuiltPayload, PayloadBuilderError> {
196 self.build_payload(
197 BuildArguments::new(
198 Default::default(),
199 None,
200 None,
201 config,
202 Default::default(),
203 Default::default(),
204 ),
205 |_| core::iter::empty(),
206 true,
207 )?
208 .into_payload()
209 .ok_or_else(|| PayloadBuilderError::MissingPayload)
210 }
211}
212
213impl<Provider> TempoPayloadBuilder<Provider>
214where
215 Provider: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec>,
216{
217 #[instrument(
218 target = "payload_builder",
219 skip_all,
220 fields(
221 id = %args.config.payload_id,
222 parent_number = %args.config.parent_header.number(),
223 parent_hash = %args.config.parent_header.hash()
224 )
225 )]
226 fn build_payload<Txs>(
227 &self,
228 args: BuildArguments<TempoPayloadAttributes, TempoBuiltPayload>,
229 best_txs: impl FnOnce(BestTransactionsAttributes) -> Txs,
230 empty: bool,
231 ) -> Result<BuildOutcome<TempoBuiltPayload>, PayloadBuilderError>
232 where
233 Txs: BestTransactions<Item = Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
234 {
235 let BuildArguments {
236 mut cached_reads,
237 trie_handle,
238 config,
239 cancel,
240 best_payload,
241 ..
242 } = args;
243 let PayloadConfig {
244 parent_header,
245 attributes,
246 payload_id,
247 } = config;
248 let build_until_interrupt =
249 trie_handle.is_some()
251 && !self.is_dev;
253
254 macro_rules! check_cancel {
255 () => {
256 if cancel.is_cancelled() {
257 return Ok(BuildOutcome::Cancelled);
258 }
259 };
260 }
261
262 check_cancel!();
263
264 let start = Instant::now();
265
266 let block_time_millis =
267 (attributes.timestamp_millis() - parent_header.timestamp_millis()) as f64;
268 self.metrics.block_time_millis.record(block_time_millis);
269 self.metrics.block_time_millis_last.set(block_time_millis);
270
271 let state_setup_start = Instant::now();
272 let _state_setup_span = debug_span!(target: "payload_builder", "state_setup").entered();
273 let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
274 let state_provider: Box<dyn StateProvider> = if self.state_provider_metrics {
275 Box::new(InstrumentedStateProvider::new(state_provider, "builder"))
276 } else {
277 state_provider
278 };
279 let state = StateProviderDatabase::new(&state_provider);
280 let mut db = State::builder()
281 .with_database(if self.disable_state_cache {
282 Box::new(state) as Box<dyn Database<Error = ProviderError>>
283 } else {
284 Box::new(cached_reads.as_db_mut(state))
285 })
286 .with_bundle_update()
287 .build();
288 drop(_state_setup_span);
289 self.metrics
290 .state_setup_duration_seconds
291 .record(state_setup_start.elapsed());
292
293 check_cancel!();
294
295 let chain_spec = self.provider.chain_spec();
296 let is_osaka = self
297 .provider
298 .chain_spec()
299 .is_osaka_active_at_timestamp(attributes.timestamp);
300
301 let block_gas_limit: u64 = parent_header.gas_limit();
302 let shared_gas_limit = block_gas_limit / TEMPO_SHARED_GAS_DIVISOR;
303 let non_shared_gas_limit = block_gas_limit - shared_gas_limit;
306 let general_gas_limit = chain_spec.general_gas_limit_at(
307 attributes.timestamp,
308 block_gas_limit,
309 shared_gas_limit,
310 );
311
312 let mut cumulative_gas_used = 0;
313 let mut cumulative_state_gas_used = 0u64;
314 let mut non_payment_gas_used = 0;
315 let mut block_size_used = attributes
317 .withdrawals
318 .as_ref()
319 .map(|w| w.length())
320 .unwrap_or(0)
321 + 1024;
322 let mut payment_transactions = 0u64;
323 let mut total_fees = U256::ZERO;
324
325 let mut subblocks = if empty
330 || self.highest_invalid_subblock.load(Ordering::Relaxed) > parent_header.number()
331 {
332 vec![]
333 } else {
334 attributes.subblocks()
335 };
336
337 subblocks.retain(|subblock| {
338 if has_expired_transactions(subblock, attributes.timestamp) {
344 self.metrics.inc_subblocks_expired();
345 return false;
346 }
347
348 block_size_used += subblock.total_tx_size();
350
351 true
352 });
353
354 let subblock_fee_recipients = subblocks
355 .iter()
356 .map(|subblock| {
357 (
358 PartialValidatorKey::from_slice(&subblock.validator()[..15]),
359 subblock.fee_recipient,
360 )
361 })
362 .collect();
363
364 let mut builder = self
365 .evm_config
366 .builder_for_next_block(
367 &mut db,
368 &parent_header,
369 TempoNextBlockEnvAttributes {
370 inner: NextBlockEnvAttributes {
371 timestamp: attributes.timestamp,
372 suggested_fee_recipient: attributes.suggested_fee_recipient,
373 prev_randao: attributes.prev_randao,
374 gas_limit: block_gas_limit,
375 parent_beacon_block_root: attributes.parent_beacon_block_root,
376 withdrawals: attributes.withdrawals.clone().map(Into::into),
377 extra_data: attributes.extra_data().clone(),
378 slot_number: attributes.slot_number,
379 },
380 general_gas_limit,
381 shared_gas_limit,
382 timestamp_millis_part: attributes.timestamp_millis_part(),
383 consensus_context: attributes.consensus_context(),
384 subblock_fee_recipients,
385 },
386 )
387 .map_err(PayloadBuilderError::other)?;
388
389 check_cancel!();
390
391 maybe_override_fee_recipient(&mut builder, &attributes);
394
395 if let Some(ref handle) = trie_handle {
396 builder
397 .executor_mut()
398 .set_state_hook(Some(Box::new(handle.state_hook())));
399 }
400
401 builder.apply_pre_execution_changes().map_err(|err| {
402 warn!(%err, "failed to apply pre-execution changes");
403 PayloadBuilderError::Internal(err.into())
404 })?;
405
406 check_cancel!();
407
408 debug!("building new payload");
409
410 let prepare_system_txs_start = Instant::now();
412 let system_txs = self.build_seal_block_txs(builder.evm(), &subblocks);
413 for tx in &system_txs {
414 block_size_used += tx.inner().length();
415 }
416 let prepare_system_txs_elapsed = prepare_system_txs_start.elapsed();
417 self.metrics
418 .prepare_system_transactions_duration_seconds
419 .record(prepare_system_txs_elapsed);
420
421 let base_fee = builder.evm_mut().block().basefee;
422 let validator_fee_token = resolve_validator_fee_token(&mut builder)?;
423 let pool_fetch_start = Instant::now();
424 let mut best_txs =
427 StateAwareBestTransactions::new(best_txs(BestTransactionsAttributes::new(
428 base_fee,
429 builder
430 .evm_mut()
431 .block()
432 .blob_gasprice()
433 .map(|gasprice| gasprice as u64),
434 )));
435 self.metrics
436 .pool_fetch_duration_seconds
437 .record(pool_fetch_start.elapsed());
438
439 let execution_start = Instant::now();
440 let _block_fill_span = debug_span!(target: "payload_builder", "block_fill").entered();
441 loop {
442 if attributes.is_interrupted() {
443 break;
444 }
445
446 check_cancel!();
447
448 let Some(pool_tx) = best_txs.next() else {
449 if build_until_interrupt && cumulative_gas_used < non_shared_gas_limit {
450 std::thread::sleep(Duration::from_millis(1));
451 continue;
452 }
453 break;
454 };
455
456 let max_regular_gas_used = core::cmp::min(
457 pool_tx.gas_limit(),
458 builder.evm().cfg.tx_gas_limit_cap.unwrap_or(u64::MAX),
459 );
460
461 if cumulative_gas_used + max_regular_gas_used > non_shared_gas_limit {
465 best_txs.mark_invalid(
468 &pool_tx,
469 &InvalidPoolTransactionError::ExceedsGasLimit(
470 pool_tx.gas_limit(),
471 non_shared_gas_limit - cumulative_gas_used,
472 ),
473 );
474 self.metrics
475 .inc_pool_tx_skipped("exceeds_non_shared_gas_limit");
476 continue;
477 }
478
479 if !pool_tx.transaction.is_payment()
482 && non_payment_gas_used + max_regular_gas_used > general_gas_limit
483 {
484 best_txs.mark_invalid(
485 &pool_tx,
486 &InvalidPoolTransactionError::Other(Box::new(
487 TempoPoolTransactionError::ExceedsNonPaymentLimit,
488 )),
489 );
490 self.metrics
491 .inc_pool_tx_skipped("exceeds_general_gas_limit");
492 continue;
493 }
494
495 if attributes.is_interrupted() {
497 break;
498 }
499
500 check_cancel!();
501 let is_payment = pool_tx.transaction.is_payment();
502 if is_payment {
503 payment_transactions += 1;
504 }
505
506 let tx_rlp_length = pool_tx.transaction.inner().length();
507 let estimated_block_size_with_tx = block_size_used + tx_rlp_length;
508
509 if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
510 best_txs.mark_invalid(
511 &pool_tx,
512 &InvalidPoolTransactionError::OversizedData {
513 size: estimated_block_size_with_tx,
514 limit: MAX_RLP_BLOCK_SIZE,
515 },
516 );
517 self.metrics.inc_pool_tx_skipped("oversized_block");
518 continue;
519 }
520
521 let effective_gas_price = pool_tx.transaction.effective_gas_price(Some(base_fee));
522
523 let tx_debug_repr = tracing::enabled!(Level::TRACE)
524 .then(|| format!("{:?}", pool_tx.transaction))
525 .unwrap_or_default();
526
527 let tx_with_env = pool_tx.transaction.clone().into_with_tx_env();
528 let tx_execution_start = Instant::now();
529 if let Err(err) =
530 builder.execute_transaction_with_result_closure(tx_with_env, |result| {
531 cumulative_gas_used += result.block_gas_used();
532 cumulative_state_gas_used += result.state_gas_used();
533 if !is_payment {
534 non_payment_gas_used += result.block_gas_used();
535 }
536
537 let nominal_spending = calc_gas_balance_spending(
540 result.result().result.tx_gas_used(),
541 effective_gas_price,
542 );
543 if let Some(fee_token) = pool_tx.transaction.resolved_fee_token() {
544 if fee_token == validator_fee_token {
545 total_fees += nominal_spending;
546 } else {
547 total_fees +=
548 tempo_precompiles::tip_fee_manager::amm::compute_amount_out(
549 nominal_spending,
550 )
551 .expect(
552 "execution succeeded, so compute_amount_out should not fail",
553 );
554 }
555 } else {
556 warn!("no resolved fee token for a pool transaction")
557 }
558
559 best_txs.on_new_result(result);
561 })
562 {
563 if let BlockExecutionError::Validation(BlockValidationError::InvalidTx {
564 error,
565 ..
566 }) = &err
567 {
568 if error.is_nonce_too_low() {
569 trace!(%error, tx = %tx_debug_repr, "skipping nonce too low transaction");
571 self.metrics.inc_pool_tx_skipped("nonce_too_low");
572 } else {
573 trace!(%error, tx = %tx_debug_repr, "skipping invalid transaction and its descendants");
576 best_txs.mark_invalid(
577 &pool_tx,
578 &InvalidPoolTransactionError::Consensus(
579 InvalidTransactionError::TxTypeNotSupported,
580 ),
581 );
582 self.metrics.inc_pool_tx_skipped("invalid_tx");
583 }
584 continue;
585 } else {
586 return Err(PayloadBuilderError::evm(err));
587 }
588 };
589 let elapsed = tx_execution_start.elapsed();
590 self.metrics
591 .transaction_execution_duration_seconds
592 .record(elapsed);
593 trace!(?elapsed, "Transaction executed");
594
595 block_size_used += tx_rlp_length;
596 }
597 drop(_block_fill_span);
598 let total_normal_transaction_execution_elapsed = execution_start.elapsed();
599 self.metrics
600 .total_normal_transaction_execution_duration_seconds
601 .record(total_normal_transaction_execution_elapsed);
602 self.metrics
603 .payment_transactions
604 .record(payment_transactions as f64);
605 self.metrics
606 .payment_transactions_last
607 .set(payment_transactions as f64);
608
609 check_cancel!();
610
611 if !is_better_payload(best_payload.as_ref(), total_fees)
613 && !is_more_subblocks(best_payload.as_ref(), &subblocks)
614 {
615 drop(builder);
617 drop(db);
618 return Ok(BuildOutcome::Aborted {
620 fees: total_fees,
621 cached_reads,
622 });
623 }
624
625 let subblocks_start = Instant::now();
626 let _subblock_txs_span =
627 debug_span!(target: "payload_builder", "execute_subblock_txs").entered();
628 let subblocks_count = subblocks.len() as f64;
629 let mut subblock_transactions = 0f64;
630 for subblock in &subblocks {
632 let subblock_start = Instant::now();
633 let mut subblock_tx_count = 0f64;
634
635 for tx in subblock.transactions_recovered() {
636 if let Err(err) = builder.execute_transaction(tx.cloned()) {
637 if let BlockExecutionError::Validation(BlockValidationError::InvalidTx {
638 ..
639 }) = &err
640 {
641 error!(
642 ?err,
643 "subblock transaction failed execution, aborting payload building"
644 );
645 self.highest_invalid_subblock
646 .store(builder.evm().block().number.to(), Ordering::Relaxed);
647 self.metrics.inc_build_failure("subblock_invalid_tx");
648 return Err(PayloadBuilderError::evm(err));
649 } else {
650 return Err(PayloadBuilderError::evm(err));
651 }
652 }
653
654 subblock_tx_count += 1.0;
655 }
656
657 self.metrics
658 .subblock_execution_duration_seconds
659 .record(subblock_start.elapsed());
660 self.metrics
661 .subblock_transaction_count
662 .record(subblock_tx_count);
663 subblock_transactions += subblock_tx_count;
664 }
665 drop(_subblock_txs_span);
666 let total_subblock_transaction_execution_elapsed = subblocks_start.elapsed();
667 self.metrics
668 .total_subblock_transaction_execution_duration_seconds
669 .record(total_subblock_transaction_execution_elapsed);
670 self.metrics.subblocks.record(subblocks_count);
671 self.metrics.subblocks_last.set(subblocks_count);
672 self.metrics
673 .subblock_transactions
674 .record(subblock_transactions);
675 self.metrics
676 .subblock_transactions_last
677 .set(subblock_transactions);
678
679 let system_txs_execution_start = Instant::now();
681 let _system_txs_span =
682 debug_span!(target: "payload_builder", "execute_system_txs").entered();
683 for system_tx in system_txs {
684 builder
685 .execute_transaction(system_tx)
686 .map_err(PayloadBuilderError::evm)?;
687 }
688 drop(_system_txs_span);
689 let system_txs_execution_elapsed = system_txs_execution_start.elapsed();
690 self.metrics
691 .system_transactions_execution_duration_seconds
692 .record(system_txs_execution_elapsed);
693
694 let total_transaction_execution_elapsed = execution_start.elapsed();
695 self.metrics
696 .total_transaction_execution_duration_seconds
697 .record(total_transaction_execution_elapsed);
698
699 let builder_finish_start = Instant::now();
700 let _finish_span = debug_span!(target: "payload_builder", "finish_block").entered();
701 let finish_provider = || InstrumentedFinishProvider {
702 inner: &*state_provider,
703 metrics: self.metrics.clone(),
704 };
705
706 check_cancel!();
707
708 let BlockBuilderOutcome {
709 execution_result,
710 block,
711 hashed_state,
712 trie_updates,
713 } = if let Some(mut handle) = trie_handle {
714 builder.executor_mut().set_state_hook(None);
717
718 match handle.state_root() {
719 Ok(outcome) => {
720 debug!(
721 target: "payload_builder",
722 id = %payload_id,
723 state_root = ?outcome.state_root,
724 "received state root from sparse trie"
725 );
726 builder.finish(
727 finish_provider(),
728 Some((
729 outcome.state_root,
730 Arc::unwrap_or_clone(outcome.trie_updates),
731 )),
732 )?
733 }
734 Err(err) => {
735 warn!(
736 target: "payload_builder",
737 id = %payload_id,
738 %err,
739 "sparse trie failed, falling back to sync state root"
740 );
741 builder.finish(finish_provider(), None)?
742 }
743 }
744 } else {
745 builder.finish(finish_provider(), None)?
746 };
747 drop(_finish_span);
748 let builder_finish_elapsed = builder_finish_start.elapsed();
749 self.metrics
750 .payload_finalization_duration_seconds
751 .record(builder_finish_elapsed);
752
753 let total_transactions = block.transaction_count();
754 self.metrics
755 .total_transactions
756 .record(total_transactions as f64);
757 self.metrics
758 .total_transactions_last
759 .set(total_transactions as f64);
760
761 let gas_used = block.gas_used();
762 self.metrics.gas_used.record(gas_used as f64);
763 self.metrics.gas_used_last.set(gas_used as f64);
764 self.metrics
765 .state_gas_used
766 .record(cumulative_state_gas_used as f64);
767 self.metrics
768 .state_gas_used_last
769 .set(cumulative_state_gas_used as f64);
770 self.metrics
771 .general_gas_used_last
772 .set(non_payment_gas_used as f64);
773 self.metrics
774 .payment_gas_used_last
775 .set(cumulative_gas_used as f64 - non_payment_gas_used as f64);
776 self.metrics
777 .general_gas_limit_last
778 .set(general_gas_limit as f64);
779 self.metrics
780 .payment_gas_limit_last
781 .set(non_shared_gas_limit as f64 - general_gas_limit as f64);
782 self.metrics
783 .shared_gas_limit_last
784 .set(shared_gas_limit as f64);
785
786 let requests = chain_spec
787 .is_prague_active_at_timestamp(attributes.timestamp)
788 .then(|| execution_result.requests.clone());
789
790 let sealed_block = Arc::new(block.sealed_block().clone());
791 let rlp_length = sealed_block.rlp_length();
792
793 if is_osaka && rlp_length > MAX_RLP_BLOCK_SIZE {
794 return Err(PayloadBuilderError::other(ConsensusError::BlockTooLarge {
795 rlp_length,
796 max_rlp_length: MAX_RLP_BLOCK_SIZE,
797 }));
798 }
799
800 let elapsed = start.elapsed();
801 self.metrics.payload_build_duration_seconds.record(elapsed);
802 let gas_per_second = sealed_block.gas_used() as f64 / elapsed.as_secs_f64();
803 self.metrics.gas_per_second.record(gas_per_second);
804 self.metrics.gas_per_second_last.set(gas_per_second);
805 self.metrics.rlp_block_size_bytes.record(rlp_length as f64);
806 self.metrics
807 .rlp_block_size_bytes_last
808 .set(rlp_length as f64);
809
810 info!(
811 parent_hash = ?sealed_block.parent_hash(),
812 number = sealed_block.number(),
813 hash = ?sealed_block.hash(),
814 timestamp = sealed_block.timestamp_millis(),
815 gas_limit = sealed_block.gas_limit(),
816 gas_used,
817 cumulative_state_gas_used,
818 extra_data = %sealed_block.extra_data(),
819 subblocks_count,
820 payment_transactions,
821 subblock_transactions,
822 total_transactions,
823 ?elapsed,
824 ?total_normal_transaction_execution_elapsed,
825 ?total_subblock_transaction_execution_elapsed,
826 ?total_transaction_execution_elapsed,
827 ?builder_finish_elapsed,
828 "Built payload"
829 );
830
831 let eth_payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None);
832
833 let execution_output = BlockExecutionOutput {
834 result: execution_result,
835 state: db.take_bundle(),
836 };
837
838 let executed_block = BuiltPayloadExecutedBlock {
839 recovered_block: Arc::new(block),
840 execution_output: Arc::new(execution_output),
841 hashed_state: Arc::new(hashed_state),
842 trie_updates: Arc::new(trie_updates),
843 };
844
845 let payload = TempoBuiltPayload::new(eth_payload, Some(executed_block));
846
847 drop(db);
848 if build_until_interrupt {
849 Ok(BuildOutcome::Freeze(payload))
850 } else {
851 Ok(BuildOutcome::Better {
852 payload,
853 cached_reads,
854 })
855 }
856 }
857}
858
859pub fn is_more_subblocks(
860 best_payload: Option<&TempoBuiltPayload>,
861 subblocks: &[RecoveredSubBlock],
862) -> bool {
863 let Some(best_payload) = best_payload else {
864 return false;
865 };
866 let Some(best_metadata) = best_payload
867 .block()
868 .body()
869 .transactions
870 .iter()
871 .rev()
872 .find_map(|tx| Vec::<SubBlockMetadata>::decode(&mut tx.input().as_ref()).ok())
873 else {
874 return false;
875 };
876
877 subblocks.len() > best_metadata.len()
878}
879
880fn maybe_override_fee_recipient<DB: Database>(
884 builder: &mut impl BlockBuilder<Executor: BlockExecutor<Evm = TempoEvm<DB>>>,
885 attributes: &TempoPayloadAttributes,
886) {
887 let Some(public_key) = attributes.proposer_public_key() else {
888 return;
889 };
890 let ctx = builder.evm_mut().ctx_mut();
891 if !ctx.cfg.spec.is_t2() {
892 return;
893 }
894
895 match ctx.journaled_state.database.with_read_only_storage_ctx(
898 ctx.cfg.spec,
899 || -> Result<Option<Address>, PayloadBuilderError> {
900 let parent_number = ctx.block.number.saturating_to::<u64>() - 1;
901
902 let config = ValidatorConfigV2::default();
903 if !config
904 .is_initialized()
905 .map_err(PayloadBuilderError::other)?
906 {
907 return Ok(None);
908 }
909 let init_height = config
910 .get_initialized_at_height()
911 .map_err(PayloadBuilderError::other)?;
912 if init_height > parent_number {
913 return Ok(None);
914 }
915 let on_chain = config
916 .validator_by_public_key(*public_key)
917 .map(|v| v.feeRecipient)
918 .map_err(PayloadBuilderError::other)?;
919 Ok((!on_chain.is_zero()).then_some(on_chain))
920 },
921 ) {
922 Ok(Some(fee_recipient)) => {
923 debug!(%fee_recipient, "resolved fee recipient from contract");
924 builder.evm_mut().ctx_mut().block.beneficiary = fee_recipient;
925 }
926 Ok(None) => {}
927 Err(err) => {
928 warn!(%err, "failed resolving fee recipient from contract; using fallback");
929 }
930 }
931}
932
933fn resolve_validator_fee_token(
935 builder: &mut impl BlockBuilder<Executor: BlockExecutor<Evm = TempoEvm<impl Database>>>,
936) -> Result<Address, PayloadBuilderError> {
937 let ctx = builder.evm_mut().ctx_mut();
938 ctx.journaled_state
941 .database
942 .with_read_only_storage_ctx(ctx.cfg.spec, || {
943 TipFeeManager::new()
944 .get_validator_token(ctx.block.beneficiary)
945 .map_err(PayloadBuilderError::other)
946 })
947}
948
949#[cfg(test)]
950mod tests {
951 use super::*;
952 use alloy_consensus::BlockBody;
953 use alloy_primitives::{Address, B256, Bytes, Signature};
954 use core::num::NonZeroU64;
955 use reth_primitives_traits::SealedBlock;
956 use tempo_primitives::{
957 AASigned, Block, SignedSubBlock, SubBlock, SubBlockVersion, TempoSignature,
958 TempoTransaction,
959 };
960
961 fn nz(value: u64) -> NonZeroU64 {
962 NonZeroU64::new(value).expect("test valid_before must be non-zero")
963 }
964
965 trait TestExt {
966 fn random() -> Self;
967 fn with_valid_before(_: Option<NonZeroU64>) -> Self
968 where
969 Self: Sized,
970 {
971 Self::random()
972 }
973 }
974
975 impl TestExt for SubBlockMetadata {
976 fn random() -> Self {
977 Self {
978 version: SubBlockVersion::V1,
979 validator: B256::random(),
980 fee_recipient: Address::random(),
981 signature: Bytes::new(),
982 }
983 }
984 }
985
986 impl TestExt for RecoveredSubBlock {
987 fn random() -> Self {
988 Self::with_valid_before(None)
989 }
990
991 fn with_valid_before(valid_before: Option<NonZeroU64>) -> Self {
992 let tx = TempoTxEnvelope::AA(AASigned::new_unhashed(
993 TempoTransaction {
994 valid_before,
995 ..Default::default()
996 },
997 TempoSignature::default(),
998 ));
999 let signed = SignedSubBlock {
1000 inner: SubBlock {
1001 version: SubBlockVersion::V1,
1002 parent_hash: B256::random(),
1003 fee_recipient: Address::random(),
1004 transactions: vec![tx],
1005 },
1006 signature: Bytes::new(),
1007 };
1008 Self::new_unchecked(signed, vec![Address::ZERO], B256::ZERO)
1009 }
1010 }
1011
1012 fn payload_with_metadata(count: usize) -> TempoBuiltPayload {
1013 let metadata: Vec<_> = (0..count).map(|_| SubBlockMetadata::random()).collect();
1014 let input: Bytes = alloy_rlp::encode(&metadata).into();
1015 let tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
1016 TxLegacy {
1017 chain_id: None,
1018 nonce: 0,
1019 gas_price: 0,
1020 gas_limit: 0,
1021 to: Address::random().into(),
1022 value: U256::ZERO,
1023 input,
1024 },
1025 Signature::test_signature(),
1026 ));
1027 let block = Block {
1028 header: TempoHeader::default(),
1029 body: BlockBody {
1030 transactions: vec![tx],
1031 ommers: vec![],
1032 withdrawals: None,
1033 },
1034 };
1035 let sealed = Arc::new(SealedBlock::seal_slow(block));
1036 let eth = EthBuiltPayload::new(sealed, U256::ZERO, None, None);
1037 TempoBuiltPayload::new(eth, None)
1038 }
1039
1040 #[test]
1041 fn test_is_more_subblocks() {
1042 assert!(!is_more_subblocks(None, &[]));
1044 assert!(!is_more_subblocks(None, &[RecoveredSubBlock::random()]));
1045
1046 let payload = payload_with_metadata(1);
1048 assert!(!is_more_subblocks(
1049 Some(&payload),
1050 &[RecoveredSubBlock::random()]
1051 ));
1052
1053 assert!(is_more_subblocks(
1055 Some(&payload),
1056 &[RecoveredSubBlock::random(), RecoveredSubBlock::random()]
1057 ));
1058
1059 let payload = payload_with_metadata(2);
1061 assert!(!is_more_subblocks(
1062 Some(&payload),
1063 &[RecoveredSubBlock::random()]
1064 ));
1065
1066 let payload = payload_with_metadata(0);
1068 assert!(!is_more_subblocks(Some(&payload), &[]));
1069
1070 assert!(is_more_subblocks(
1072 Some(&payload),
1073 &[RecoveredSubBlock::random()]
1074 ));
1075 }
1076
1077 #[test]
1078 fn test_extra_data_flow_in_attributes() {
1079 let extra_data = Bytes::from(vec![42, 43, 44, 45, 46]);
1081
1082 let attrs = TempoPayloadAttributes::new(None, 1, 0, extra_data.clone(), None, Vec::new);
1083
1084 assert_eq!(attrs.extra_data(), &extra_data);
1085
1086 let injected_data = attrs.extra_data().clone();
1088
1089 assert_eq!(injected_data, extra_data);
1090 }
1091
1092 #[test]
1093 fn test_has_expired_transactions_boundary() {
1094 let subblock = RecoveredSubBlock::with_valid_before(Some(nz(1000)));
1096 assert!(has_expired_transactions(&subblock, 1000));
1097
1098 assert!(has_expired_transactions(&subblock, 1001));
1100
1101 assert!(!has_expired_transactions(&subblock, 999));
1103
1104 let subblock_no_expiry = RecoveredSubBlock::with_valid_before(None);
1106 assert!(!has_expired_transactions(&subblock_no_expiry, 1000));
1107 }
1108}