1#![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 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 fn build_start_block_txs(
108 &self,
109 evm: &TempoEvm<impl Database>,
110 ) -> Vec<Recovered<TempoTxEnvelope>> {
111 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 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 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 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 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 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 let mut block_size_used = attributes.withdrawals().length() + 1024;
345 let mut payment_transactions = 0u64;
346 let mut total_fees = U256::ZERO;
347
348 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 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 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 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 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 if cumulative_gas_used + pool_tx.gas_limit() > non_shared_gas_limit {
461 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 !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 if attributes.is_interrupted() {
489 break;
490 }
491
492 if cancel.is_cancelled() {
494 return Ok(BuildOutcome::Cancelled);
495 }
496
497 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 trace!(%error, tx = %tx_debug_repr, "skipping nonce too low transaction");
536 } else {
537 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 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 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 if !is_better_payload(best_payload.as_ref(), total_fees)
569 && !is_more_subblocks(best_payload.as_ref(), &subblocks)
570 {
571 drop(builder);
573 return Ok(BuildOutcome::Aborted {
575 fees: total_fees,
576 cached_reads,
577 });
578 }
579
580 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 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 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 let injected_data = attrs.extra_data().clone();
739
740 assert_eq!(injected_data, extra_data);
741 }
742}