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 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
65fn 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 highest_invalid_subblock: Arc<AtomicU64>,
89 state_provider_metrics: bool,
91 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 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 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 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 let mut block_size_used = attributes.withdrawals().length() + 1024;
286 let mut payment_transactions = 0u64;
287 let mut total_fees = U256::ZERO;
288
289 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 if has_expired_transactions(subblock, attributes.timestamp()) {
308 self.metrics.inc_subblocks_expired();
309 return false;
310 }
311
312 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 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 if cumulative_gas_used + pool_tx.gas_limit() > non_shared_gas_limit {
390 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 !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 if attributes.is_interrupted() {
422 break;
423 }
424
425 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 trace!(%error, tx = %tx_debug_repr, "skipping nonce too low transaction");
467 self.metrics.inc_pool_tx_skipped("nonce_too_low");
468 } else {
469 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 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 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 if !is_better_payload(best_payload.as_ref(), total_fees)
513 && !is_more_subblocks(best_payload.as_ref(), &subblocks)
514 {
515 drop(builder);
517 drop(db);
518 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 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 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 assert!(!is_more_subblocks(None, &[]));
825 assert!(!is_more_subblocks(None, &[RecoveredSubBlock::random()]));
826
827 let payload = payload_with_metadata(1);
829 assert!(!is_more_subblocks(
830 Some(&payload),
831 &[RecoveredSubBlock::random()]
832 ));
833
834 assert!(is_more_subblocks(
836 Some(&payload),
837 &[RecoveredSubBlock::random(), RecoveredSubBlock::random()]
838 ));
839
840 let payload = payload_with_metadata(2);
842 assert!(!is_more_subblocks(
843 Some(&payload),
844 &[RecoveredSubBlock::random()]
845 ));
846
847 let payload = payload_with_metadata(0);
849 assert!(!is_more_subblocks(Some(&payload), &[]));
850
851 assert!(is_more_subblocks(
853 Some(&payload),
854 &[RecoveredSubBlock::random()]
855 ));
856 }
857
858 #[test]
859 fn test_extra_data_flow_in_attributes() {
860 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 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 let subblock = RecoveredSubBlock::with_valid_before(Some(1000));
884 assert!(has_expired_transactions(&subblock, 1000));
885
886 assert!(has_expired_transactions(&subblock, 1001));
888
889 assert!(!has_expired_transactions(&subblock, 999));
891
892 let subblock_no_expiry = RecoveredSubBlock::with_valid_before(None);
894 assert!(!has_expired_transactions(&subblock_no_expiry, 1000));
895 }
896}