1use crate::{TempoBlockExecutionCtx, evm::TempoEvm};
2use alloy_consensus::{Transaction, transaction::TxHashRef};
3use alloy_evm::{
4 Database, Evm, RecoveredTx,
5 block::{
6 BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError,
7 ExecutableTx, GasOutput, TxResult,
8 },
9 eth::{
10 EthBlockExecutor, EthTxResult,
11 receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
12 },
13};
14use alloy_primitives::{Address, B256, U256};
15use alloy_rlp::Decodable;
16use commonware_codec::DecodeExt;
17use commonware_cryptography::{
18 Verifier,
19 ed25519::{PublicKey, Signature},
20};
21use reth_evm::block::StateDB;
22use reth_revm::{
23 Inspector,
24 context::result::ResultAndState,
25 state::{Account, Bytecode, EvmState},
26};
27use std::collections::{HashMap, HashSet};
28use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
29use tempo_contracts::precompiles::{
30 ADDRESS_REGISTRY_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
31 STORAGE_CREDITS_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
32};
33use tempo_primitives::{
34 SubBlock, SubBlockMetadata, TempoReceipt, TempoTxEnvelope, TempoTxType,
35 subblock::PartialValidatorKey,
36};
37use tempo_revm::{TempoHaltReason, evm::TempoContext};
38use tracing::trace;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub(crate) enum BlockSection {
42 StartOfBlock,
44 NonShared,
48 SubBlock { proposer: PartialValidatorKey },
50 GasIncentive,
52 System { seen_subblocks_signatures: bool },
54}
55
56#[derive(Debug, Clone, Copy, Default)]
58#[non_exhaustive]
59pub struct TempoReceiptBuilder;
60
61impl ReceiptBuilder for TempoReceiptBuilder {
62 type Transaction = TempoTxEnvelope;
63 type Receipt = TempoReceipt;
64
65 fn build_receipt<E: Evm>(&self, ctx: ReceiptBuilderCtx<'_, TempoTxType, E>) -> Self::Receipt {
66 let ReceiptBuilderCtx {
67 tx_type,
68 result,
69 cumulative_gas_used,
70 ..
71 } = ctx;
72 TempoReceipt {
73 tx_type,
74 success: result.is_success(),
77 cumulative_gas_used,
78 logs: result.into_logs(),
79 }
80 }
81}
82
83#[derive(Debug)]
87pub struct TempoTxResult {
88 inner: EthTxResult<TempoHaltReason, TempoTxType>,
90 next_section: BlockSection,
92 is_payment: bool,
94 tx: Option<TempoTxEnvelope>,
99 block_gas_used: u64,
101 validator_fee: U256,
106}
107
108impl TempoTxResult {
109 pub fn block_gas_used(&self) -> u64 {
111 self.block_gas_used
112 }
113
114 pub fn state_gas_used(&self) -> u64 {
116 self.inner.result.result.gas().state_gas_spent_final()
117 }
118
119 pub fn validator_fee(&self) -> U256 {
121 self.validator_fee
122 }
123}
124
125impl TxResult for TempoTxResult {
126 type HaltReason = TempoHaltReason;
127
128 fn result(&self) -> &ResultAndState<Self::HaltReason> {
129 self.inner.result()
130 }
131
132 fn into_result(self) -> ResultAndState<Self::HaltReason> {
133 self.inner.into_result()
134 }
135}
136
137pub struct TempoBlockExecutor<'a, DB: Database, I> {
143 pub(crate) inner:
144 EthBlockExecutor<'a, TempoEvm<DB, I>, &'a TempoChainSpec, TempoReceiptBuilder>,
145
146 section: BlockSection,
147 seen_subblocks: Vec<(PartialValidatorKey, Vec<TempoTxEnvelope>)>,
148 validator_set: Option<Vec<B256>>,
149 shared_gas_limit: u64,
150 subblock_fee_recipients: HashMap<PartialValidatorKey, Address>,
151
152 non_shared_gas_left: u64,
153 non_payment_gas_left: u64,
154 incentive_gas_used: u64,
155}
156
157impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
158where
159 DB: StateDB,
160 I: Inspector<TempoContext<DB>>,
161{
162 pub(crate) fn new(
163 evm: TempoEvm<DB, I>,
164 ctx: TempoBlockExecutionCtx<'a>,
165 chain_spec: &'a TempoChainSpec,
166 ) -> Self {
167 Self {
168 incentive_gas_used: 0,
169 validator_set: ctx.validator_set,
170 non_payment_gas_left: ctx.general_gas_limit,
171 non_shared_gas_left: evm.block().gas_limit.saturating_sub(ctx.shared_gas_limit),
172 shared_gas_limit: ctx.shared_gas_limit,
173 inner: EthBlockExecutor::new(
174 evm,
175 ctx.inner,
176 chain_spec,
177 TempoReceiptBuilder::default(),
178 ),
179 section: BlockSection::StartOfBlock,
180 seen_subblocks: Vec::new(),
181 subblock_fee_recipients: ctx.subblock_fee_recipients,
182 }
183 }
184
185 fn deploy_precompile_at_boundary(
190 &mut self,
191 address: Address,
192 ) -> Result<(), BlockExecutionError> {
193 let info = self
194 .inner
195 .evm
196 .db_mut()
197 .basic(address)
198 .map_err(BlockExecutionError::other)?
199 .unwrap_or_default();
200 if info.is_empty_code_hash() {
201 let mut account = Account::from(info);
202 let code = Bytecode::new_legacy([0xef].into());
203 account.info.code_hash = code.hash_slow();
204 account.info.code = Some(code);
205 account.mark_touch();
206 let state = EvmState::from_iter([(address, account)]);
207 self.inner.evm.db_mut().commit(state);
208 }
209 Ok(())
210 }
211
212 pub(crate) fn validate_system_tx(
214 &self,
215 tx: &TempoTxEnvelope,
216 ) -> Result<BlockSection, BlockValidationError> {
217 let block = self.evm().block();
218 let block_number = block.number.to_be_bytes_vec();
219 let to = tx.to().unwrap_or_default();
220
221 let mut seen_subblocks_signatures = match self.section {
223 BlockSection::System {
224 seen_subblocks_signatures,
225 } => seen_subblocks_signatures,
226 _ => false,
227 };
228
229 if to.is_zero() {
230 if seen_subblocks_signatures {
231 return Err(BlockValidationError::msg(
232 "duplicate subblocks metadata system transaction",
233 ));
234 }
235
236 if self.evm().cfg.spec.is_t4() {
237 return Err(BlockValidationError::msg("subblocks are disabled in T4+"));
238 }
239
240 if tx.input().len() < U256::BYTES
241 || tx.input()[tx.input().len() - U256::BYTES..] != block_number
242 {
243 return Err(BlockValidationError::msg(
244 "invalid subblocks metadata system transaction",
245 ));
246 }
247
248 let mut buf = &tx.input()[..tx.input().len() - U256::BYTES];
249 let Ok(metadata) = Vec::<SubBlockMetadata>::decode(&mut buf) else {
250 return Err(BlockValidationError::msg(
251 "invalid subblocks metadata system transaction",
252 ));
253 };
254
255 if !buf.is_empty() {
256 return Err(BlockValidationError::msg(
257 "invalid subblocks metadata system transaction",
258 ));
259 }
260
261 self.validate_shared_gas(&metadata)?;
262
263 seen_subblocks_signatures = true;
264 } else {
265 return Err(BlockValidationError::msg("invalid system transaction"));
266 }
267
268 Ok(BlockSection::System {
269 seen_subblocks_signatures,
270 })
271 }
272
273 pub(crate) fn validate_shared_gas(
274 &self,
275 metadata: &[SubBlockMetadata],
276 ) -> Result<(), BlockValidationError> {
277 let Some(validator_set) = &self.validator_set else {
279 return Ok(());
280 };
281 let gas_per_subblock = self
282 .shared_gas_limit
283 .checked_div(validator_set.len() as u64)
284 .expect("validator set must not be empty");
285
286 let mut incentive_gas = 0;
287 let mut seen = HashSet::new();
288 let mut next_non_empty = 0;
289 for metadata in metadata {
290 if !validator_set.contains(&metadata.validator) {
291 return Err(BlockValidationError::msg("invalid subblock validator"));
292 }
293
294 if !seen.insert(metadata.validator) {
295 return Err(BlockValidationError::msg(
296 "only one subblock per validator is allowed",
297 ));
298 }
299
300 let transactions = if let Some((validator, txs)) =
301 self.seen_subblocks.get(next_non_empty)
302 && validator.matches(metadata.validator)
303 {
304 next_non_empty += 1;
305 txs.clone()
306 } else {
307 Vec::new()
308 };
309
310 let reserved_gas = transactions
311 .iter()
312 .map(|tx| {
313 core::cmp::min(
314 tx.gas_limit(),
315 self.inner.evm.cfg.tx_gas_limit_cap.unwrap_or(u64::MAX),
316 )
317 })
318 .sum::<u64>();
319
320 let signature_hash = SubBlock {
321 version: metadata.version,
322 fee_recipient: metadata.fee_recipient,
323 parent_hash: self.inner.ctx.parent_hash,
324 transactions: transactions.clone(),
325 }
326 .signature_hash();
327
328 let Ok(validator) = PublicKey::decode(&mut metadata.validator.as_ref()) else {
329 return Err(BlockValidationError::msg("invalid subblock validator"));
330 };
331
332 let Ok(signature) = Signature::decode(&mut metadata.signature.as_ref()) else {
333 return Err(BlockValidationError::msg(
334 "invalid subblock signature encoding",
335 ));
336 };
337
338 if !validator.verify(&[], signature_hash.as_slice(), &signature) {
340 return Err(BlockValidationError::msg("invalid subblock signature"));
341 }
342
343 if reserved_gas > gas_per_subblock {
344 return Err(BlockValidationError::msg(
345 "subblock gas used exceeds gas per subblock",
346 ));
347 }
348
349 incentive_gas += gas_per_subblock - reserved_gas;
350 }
351
352 if next_non_empty != self.seen_subblocks.len() {
353 return Err(BlockValidationError::msg(
354 "failed to map all non-empty subblocks to metadata",
355 ));
356 }
357
358 if incentive_gas < self.incentive_gas_used {
359 return Err(BlockValidationError::msg("incentive gas limit exceeded"));
360 }
361
362 Ok(())
363 }
364
365 pub(crate) fn validate_tx_pre_execution(
371 &self,
372 tx: &TempoTxEnvelope,
373 ) -> Result<Option<BlockSection>, BlockValidationError> {
374 if tx.is_system_tx() {
375 self.validate_system_tx(tx).map(Some)
376 } else {
377 Ok(None)
378 }
379 }
380
381 pub(crate) fn is_payment(&self, tx: &TempoTxEnvelope) -> bool {
389 if self.evm().cfg.spec.is_t5() {
390 tx.is_payment_v2()
391 } else {
392 tx.is_payment_v1()
393 }
394 }
395
396 pub(crate) fn validate_tx(
397 &self,
398 tx: &TempoTxEnvelope,
399 gas_used: u64,
400 ) -> Result<BlockSection, BlockValidationError> {
401 if tx.is_system_tx() {
403 self.validate_system_tx(tx)
404 } else if let Some(tx_proposer) = tx.subblock_proposer() {
405 match self.section {
406 BlockSection::GasIncentive | BlockSection::System { .. } => {
407 Err(BlockValidationError::msg("subblock section already passed"))
408 }
409 BlockSection::StartOfBlock | BlockSection::NonShared => {
410 Ok(BlockSection::SubBlock {
411 proposer: tx_proposer,
412 })
413 }
414 BlockSection::SubBlock { proposer } => {
415 if proposer == tx_proposer
416 || !self.seen_subblocks.iter().any(|(p, _)| *p == tx_proposer)
417 {
418 Ok(BlockSection::SubBlock {
419 proposer: tx_proposer,
420 })
421 } else {
422 Err(BlockValidationError::msg(
423 "proposer's subblock already processed",
424 ))
425 }
426 }
427 }
428 } else {
429 match self.section {
430 BlockSection::StartOfBlock | BlockSection::NonShared => {
431 if gas_used > self.non_shared_gas_left
432 || (!self.is_payment(tx) && gas_used > self.non_payment_gas_left)
433 {
434 Ok(BlockSection::GasIncentive)
438 } else {
439 Ok(BlockSection::NonShared)
440 }
441 }
442 BlockSection::SubBlock { .. } => {
443 Ok(BlockSection::GasIncentive)
446 }
447 BlockSection::GasIncentive => Ok(BlockSection::GasIncentive),
448 BlockSection::System { .. } => {
449 trace!(target: "tempo::block", tx_hash = ?*tx.tx_hash(), "Rejecting: regular transaction after system transaction");
450 Err(BlockValidationError::msg(
451 "regular transaction can't follow system transaction",
452 ))
453 }
454 }
455 }
456 }
457}
458
459impl<'a, DB, I> BlockExecutor for TempoBlockExecutor<'a, DB, I>
460where
461 DB: StateDB,
462 I: Inspector<TempoContext<DB>>,
463{
464 type Transaction = TempoTxEnvelope;
465 type Receipt = TempoReceipt;
466 type Evm = TempoEvm<DB, I>;
467 type Result = TempoTxResult;
468
469 fn apply_pre_execution_changes(&mut self) -> Result<(), alloy_evm::block::BlockExecutionError> {
470 if self
471 .inner
472 .ctx
473 .withdrawals
474 .as_ref()
475 .is_some_and(|withdrawals| !withdrawals.is_empty())
476 {
477 return Err(BlockValidationError::msg("withdrawals are not permitted").into());
478 }
479
480 self.inner.apply_pre_execution_changes()?;
481
482 let timestamp = self.evm().block().timestamp.to::<u64>();
484 if self.inner.spec.is_t2_active_at_timestamp(timestamp) {
485 self.deploy_precompile_at_boundary(VALIDATOR_CONFIG_V2_ADDRESS)?;
486 }
487 if self.inner.spec.is_t3_active_at_timestamp(timestamp) {
488 self.deploy_precompile_at_boundary(SIGNATURE_VERIFIER_ADDRESS)?;
489 self.deploy_precompile_at_boundary(ADDRESS_REGISTRY_ADDRESS)?;
490 }
491 if self.inner.spec.is_t5_active_at_timestamp(timestamp) {
492 self.deploy_precompile_at_boundary(TIP20_CHANNEL_RESERVE_ADDRESS)?;
493 }
494 if self.inner.spec.is_t6_active_at_timestamp(timestamp) {
495 self.deploy_precompile_at_boundary(RECEIVE_POLICY_GUARD_ADDRESS)?;
496 }
497 if self.inner.spec.is_t7_active_at_timestamp(timestamp) {
498 self.deploy_precompile_at_boundary(STORAGE_CREDITS_ADDRESS)?;
499 }
500
501 Ok(())
502 }
503
504 fn receipts(&self) -> &[Self::Receipt] {
505 self.inner.receipts()
506 }
507
508 fn execute_transaction_without_commit(
509 &mut self,
510 tx: impl ExecutableTx<Self>,
511 ) -> Result<Self::Result, BlockExecutionError> {
512 let (mut tx_env, recovered) = tx.into_parts();
513 if let Some(tempo_tx_env) = tx_env.tempo_tx_env.as_mut() {
515 tempo_tx_env.expiring_nonce_idx = None;
516 }
517 let next_section = self.validate_tx_pre_execution(recovered.tx())?;
518
519 let beneficiary = self.evm_mut().ctx_mut().block.beneficiary;
520 if let Some(validator) = recovered.tx().subblock_proposer() {
522 let fee_recipient = *self
523 .subblock_fee_recipients
524 .get(&validator)
525 .ok_or(BlockExecutionError::msg("invalid subblock transaction"))?;
526
527 self.evm_mut().ctx_mut().block.beneficiary = fee_recipient;
528 }
529 let result = self
530 .inner
531 .execute_transaction_without_commit((tx_env, &recovered));
532
533 self.evm_mut().ctx_mut().block.beneficiary = beneficiary;
534
535 let inner = result?;
536
537 let block_gas_used = if self.evm().cfg.enable_amsterdam_eip8037 {
540 inner.result.result.gas().block_regular_gas_used()
541 } else {
542 inner.result.result.tx_gas_used()
543 };
544
545 let next_section = if let Some(next_section) = next_section {
546 next_section
548 } else {
549 self.validate_tx(recovered.tx(), block_gas_used)?
550 };
551 let validator_fee = self.evm().validator_fee();
553 Ok(TempoTxResult {
554 inner,
555 next_section,
556 is_payment: self.is_payment(recovered.tx()),
557 tx: matches!(next_section, BlockSection::SubBlock { .. })
558 .then(|| recovered.tx().clone()),
559 block_gas_used,
560 validator_fee,
561 })
562 }
563
564 fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
565 let TempoTxResult {
566 inner,
567 next_section,
568 is_payment,
569 tx,
570 block_gas_used,
571 validator_fee: _,
572 } = output;
573
574 let gas_output = self.inner.commit_transaction(inner);
575
576 self.section = next_section;
577
578 match self.section {
579 BlockSection::StartOfBlock => {
580 }
582 BlockSection::NonShared => {
583 self.non_shared_gas_left -= block_gas_used;
584 if !is_payment {
585 self.non_payment_gas_left -= block_gas_used;
586 }
587 }
588 BlockSection::SubBlock { proposer } => {
589 let last_subblock = if let Some(last) = self
590 .seen_subblocks
591 .last_mut()
592 .filter(|(p, _)| *p == proposer)
593 {
594 last
595 } else {
596 self.seen_subblocks.push((proposer, Vec::new()));
597 self.seen_subblocks.last_mut().unwrap()
598 };
599
600 last_subblock
601 .1
602 .push(tx.expect("missing tx for subblock transaction"));
603 }
604 BlockSection::GasIncentive => {
605 self.incentive_gas_used += block_gas_used;
606 }
607 BlockSection::System { .. } => {
608 }
610 }
611
612 gas_output
613 }
614
615 fn finish(
616 self,
617 ) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
618 let seen_subblock_signatures = match self.section {
619 BlockSection::System {
620 seen_subblocks_signatures,
621 } => seen_subblocks_signatures,
622 _ => false,
623 };
624
625 if !seen_subblock_signatures && self.evm().cfg.spec.is_t4() {
627 self.validate_shared_gas(&[])?;
628 }
629
630 let amsterdam_eip8037_enabled = self.evm().cfg.enable_amsterdam_eip8037;
631
632 let regular_gas_used = self.inner.block_regular_gas_used;
633 let (evm, mut result) = self.inner.finish()?;
634
635 if amsterdam_eip8037_enabled {
644 result.gas_used = regular_gas_used;
645 }
646
647 Ok((evm, result))
648 }
649
650 fn evm_mut(&mut self) -> &mut Self::Evm {
651 self.inner.evm_mut()
652 }
653
654 fn evm(&self) -> &Self::Evm {
655 self.inner.evm()
656 }
657}
658
659#[cfg(test)]
661impl<'a, DB, I> TempoBlockExecutor<'a, DB, I>
662where
663 DB: Database,
664 I: Inspector<TempoContext<DB>>,
665{
666 pub(crate) fn set_section_for_test(&mut self, section: BlockSection) {
668 self.section = section;
669 }
670
671 pub(crate) fn add_seen_subblock_for_test(
673 &mut self,
674 proposer: PartialValidatorKey,
675 txs: Vec<TempoTxEnvelope>,
676 ) {
677 self.seen_subblocks.push((proposer, txs));
678 }
679
680 pub(crate) fn set_incentive_gas_used_for_test(&mut self, gas: u64) {
682 self.incentive_gas_used = gas;
683 }
684
685 pub(crate) fn section(&self) -> BlockSection {
687 self.section
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694 use crate::test_utils::{TestExecutorBuilder, test_chainspec, test_evm};
695 use alloy_consensus::{Signed, TxLegacy};
696 use alloy_evm::{block::BlockExecutor, eth::receipt_builder::ReceiptBuilder};
697 use alloy_primitives::{Bytes, Log, Signature, TxKind, bytes::BytesMut};
698 use alloy_rlp::Encodable;
699 use commonware_cryptography::{Signer, ed25519::PrivateKey};
700 use reth_chainspec::EthChainSpec;
701 use reth_revm::{State, state::AccountInfo};
702 use revm::{
703 context::result::{ExecutionResult, ResultGas},
704 database::EmptyDB,
705 };
706 use std::sync::{Arc, Mutex};
707 use tempo_chainspec::spec::DEV;
708 use tempo_contracts::precompiles::PATH_USD_ADDRESS;
709 use tempo_primitives::{
710 SubBlockMetadata, TempoSignature, TempoTransaction, TempoTxType,
711 subblock::{SubBlockVersion, TEMPO_SUBBLOCK_NONCE_KEY_PREFIX},
712 transaction::{Call, envelope::TEMPO_SYSTEM_TX_SIGNATURE},
713 };
714 use tempo_revm::TempoHaltReason;
715
716 fn create_legacy_tx() -> TempoTxEnvelope {
717 let tx = TxLegacy {
718 chain_id: Some(1),
719 nonce: 0,
720 gas_price: 1,
721 gas_limit: 21000,
722 to: TxKind::Call(Address::ZERO),
723 value: U256::ZERO,
724 input: Bytes::new(),
725 };
726 TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
727 }
728
729 fn create_tip20_empty_calldata_tx() -> TempoTxEnvelope {
730 let tx = TxLegacy {
731 chain_id: Some(1),
732 nonce: 0,
733 gas_price: 1,
734 gas_limit: 21000,
735 to: TxKind::Call(PATH_USD_ADDRESS),
736 value: U256::ZERO,
737 input: Bytes::new(),
738 };
739 TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()))
740 }
741
742 #[test]
743 fn test_build_receipt() {
744 let builder = TempoReceiptBuilder;
745 let tx = create_legacy_tx();
746 let evm = test_evm(EmptyDB::default());
747
748 let logs = vec![Log::new_unchecked(
749 Address::ZERO,
750 vec![B256::ZERO],
751 Bytes::new(),
752 )];
753 let result: ExecutionResult<TempoHaltReason> = ExecutionResult::Success {
754 reason: revm::context::result::SuccessReason::Return,
755 gas: ResultGas::default().with_total_gas_spent(21000),
756 logs,
757 output: revm::context::result::Output::Call(Bytes::new()),
758 };
759
760 let cumulative_gas_used = 21000;
761
762 let receipt = builder.build_receipt(ReceiptBuilderCtx {
763 tx_type: tx.tx_type(),
764 evm: &evm,
765 result,
766 state: &Default::default(),
767 cumulative_gas_used,
768 });
769
770 assert_eq!(receipt.tx_type, TempoTxType::Legacy);
771 assert!(receipt.success);
772 assert_eq!(receipt.cumulative_gas_used, 21000);
773 assert_eq!(receipt.logs.len(), 1);
774 assert_eq!(receipt.logs[0].address, Address::ZERO);
775 }
776
777 #[test]
778 fn test_validate_system_tx() {
779 let chainspec = test_chainspec();
780 let mut db = State::builder().with_bundle_update().build();
781 let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
782
783 let signer = PrivateKey::from_seed(0);
784 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
785 let input = create_system_tx_input(metadata, 1);
786 let system_tx = create_system_tx(chainspec.chain().id(), input);
787
788 let result = executor.validate_system_tx(&system_tx);
789 assert!(
790 result.is_ok(),
791 "validate_system_tx failed: {:?}",
792 result.err()
793 );
794 assert_eq!(
795 result.unwrap(),
796 BlockSection::System {
797 seen_subblocks_signatures: true
798 }
799 );
800 }
801
802 fn create_system_tx_input(metadata: Vec<SubBlockMetadata>, block_number: u64) -> Bytes {
803 let mut input = BytesMut::new();
804 metadata.encode(&mut input);
805 input.extend_from_slice(&U256::from(block_number).to_be_bytes::<32>());
806 input.freeze().into()
807 }
808
809 fn create_system_tx(chain_id: u64, input: Bytes) -> TempoTxEnvelope {
810 TempoTxEnvelope::Legacy(Signed::new_unhashed(
811 TxLegacy {
812 chain_id: Some(chain_id),
813 nonce: 0,
814 gas_price: 0,
815 gas_limit: 0,
816 to: TxKind::Call(Address::ZERO),
817 value: U256::ZERO,
818 input,
819 },
820 TEMPO_SYSTEM_TX_SIGNATURE,
821 ))
822 }
823
824 fn create_valid_subblock_metadata(parent_hash: B256, signer: &PrivateKey) -> SubBlockMetadata {
825 let validator_key = B256::from_slice(&signer.public_key());
826 let subblock = tempo_primitives::SubBlock {
827 version: SubBlockVersion::V1,
828 parent_hash,
829 fee_recipient: Address::ZERO,
830 transactions: vec![],
831 };
832 let signature_hash = subblock.signature_hash();
833 let signature = signer.sign(&[], signature_hash.as_slice());
834
835 SubBlockMetadata {
836 version: SubBlockVersion::V1,
837 validator: validator_key,
838 fee_recipient: Address::ZERO,
839 signature: Bytes::copy_from_slice(signature.as_ref()),
840 }
841 }
842
843 #[test]
844 fn test_validate_system_tx_duplicate_subblocks_system_tx() {
845 let chainspec = test_chainspec();
846 let mut db = State::builder().with_bundle_update().build();
847 let executor = TestExecutorBuilder::default()
848 .with_section(BlockSection::System {
849 seen_subblocks_signatures: true,
850 })
851 .build(&mut db, &chainspec);
852
853 let signer = PrivateKey::from_seed(0);
854 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
855 let input = create_system_tx_input(metadata, 1);
856 let system_tx = create_system_tx(chainspec.chain().id(), input);
857
858 let result = executor.validate_system_tx(&system_tx);
859 assert!(result.is_err());
860 assert_eq!(
861 result.unwrap_err().to_string(),
862 "duplicate subblocks metadata system transaction"
863 );
864 }
865
866 #[test]
867 fn test_validate_system_tx_invalid_sublocks_metadata() {
868 let chainspec = test_chainspec();
869 let mut db = State::builder().with_bundle_update().build();
870 let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
871
872 let mut input = BytesMut::new();
873 input.extend_from_slice(&[0xff, 0xff, 0xff]); input.extend_from_slice(&U256::from(1u64).to_be_bytes::<32>());
875 let system_tx = create_system_tx(chainspec.chain().id(), input.freeze().into());
876
877 let result = executor.validate_system_tx(&system_tx);
878 assert!(result.is_err());
879 assert_eq!(
880 result.unwrap_err().to_string(),
881 "invalid subblocks metadata system transaction"
882 );
883 }
884
885 #[test]
886 fn test_validate_system_tx_invalid_system_tx() {
887 let chainspec = test_chainspec();
888 let mut db = State::builder().with_bundle_update().build();
889 let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
890
891 let system_tx = TempoTxEnvelope::Legacy(Signed::new_unhashed(
893 TxLegacy {
894 chain_id: Some(chainspec.chain().id()),
895 nonce: 0,
896 gas_price: 0,
897 gas_limit: 0,
898 to: TxKind::Call(Address::repeat_byte(0x01)), value: U256::ZERO,
900 input: Bytes::new(),
901 },
902 TEMPO_SYSTEM_TX_SIGNATURE,
903 ));
904
905 let result = executor.validate_system_tx(&system_tx);
906 assert!(result.is_err());
907 assert_eq!(
908 result.unwrap_err().to_string(),
909 "invalid system transaction"
910 );
911 }
912
913 #[test]
914 fn test_validate_system_tx_rejects_metadata_tx_in_t4() {
915 let chainspec = DEV.clone();
916 let mut db = State::builder().with_bundle_update().build();
917 let mut executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
918
919 executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T4;
921
922 let signer = PrivateKey::from_seed(0);
923 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
924 let input = create_system_tx_input(metadata, 1);
925 let system_tx = create_system_tx(chainspec.chain().id(), input);
926
927 let result = executor.validate_system_tx(&system_tx);
928 assert!(result.is_err());
929 assert_eq!(
930 result.unwrap_err().to_string(),
931 "subblocks are disabled in T4+"
932 );
933 }
934
935 #[test]
936 fn test_validate_shared_gas() {
937 let chainspec = test_chainspec();
938 let mut db = State::builder().with_bundle_update().build();
939 let signer = PrivateKey::from_seed(0);
940 let validator_key = B256::from_slice(&signer.public_key());
941 let executor = TestExecutorBuilder::default()
942 .with_validator_set(vec![validator_key])
943 .build(&mut db, &chainspec);
944
945 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
946 let result = executor.validate_shared_gas(&metadata);
947 assert!(result.is_ok());
948 }
949
950 #[test]
951 fn test_validate_shared_gas_set_does_not_contain_validator() {
952 let chainspec = test_chainspec();
953 let mut db = State::builder().with_bundle_update().build();
954 let signer = PrivateKey::from_seed(0);
955 let different_validator = B256::repeat_byte(0x42); let executor = TestExecutorBuilder::default()
957 .with_validator_set(vec![different_validator])
958 .build(&mut db, &chainspec);
959
960 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
961 let result = executor.validate_shared_gas(&metadata);
962 assert!(result.is_err());
963 assert_eq!(
964 result.unwrap_err().to_string(),
965 "invalid subblock validator"
966 );
967 }
968
969 #[test]
970 fn test_validate_shared_gas_more_than_one_subblock_per_validator() {
971 let chainspec = test_chainspec();
972 let mut db = State::builder().with_bundle_update().build();
973 let signer = PrivateKey::from_seed(0);
974 let validator_key = B256::from_slice(&signer.public_key());
975 let executor = TestExecutorBuilder::default()
976 .with_validator_set(vec![validator_key])
977 .build(&mut db, &chainspec);
978
979 let m = create_valid_subblock_metadata(B256::ZERO, &signer);
981 let metadata = vec![m.clone(), m];
982
983 let result = executor.validate_shared_gas(&metadata);
984 assert!(result.is_err());
985 assert_eq!(
986 result.unwrap_err().to_string(),
987 "only one subblock per validator is allowed"
988 );
989 }
990
991 #[test]
992 fn test_validate_shared_gas_invalid_signature_encoding() {
993 let chainspec = test_chainspec();
994 let mut db = State::builder().with_bundle_update().build();
995 let signer = PrivateKey::from_seed(0);
996 let validator_key = B256::from_slice(&signer.public_key());
997 let executor = TestExecutorBuilder::default()
998 .with_validator_set(vec![validator_key])
999 .build(&mut db, &chainspec);
1000
1001 let metadata = vec![SubBlockMetadata {
1003 version: SubBlockVersion::V1,
1004 validator: validator_key,
1005 fee_recipient: Address::ZERO,
1006 signature: Bytes::from_static(&[0x01, 0x02, 0x03]),
1007 }];
1008
1009 let result = executor.validate_shared_gas(&metadata);
1010 assert!(result.is_err());
1011 assert_eq!(
1012 result.unwrap_err().to_string(),
1013 "invalid subblock signature encoding"
1014 );
1015 }
1016
1017 #[test]
1018 fn test_validate_shared_gas_invalid_signature() {
1019 let chainspec = test_chainspec();
1020 let mut db = State::builder().with_bundle_update().build();
1021 let signer = PrivateKey::from_seed(0);
1022 let validator_key = B256::from_slice(&signer.public_key());
1023 let executor = TestExecutorBuilder::default()
1024 .with_validator_set(vec![validator_key])
1025 .build(&mut db, &chainspec);
1026
1027 let wrong_signer = PrivateKey::from_seed(1);
1029 let subblock = tempo_primitives::SubBlock {
1030 version: SubBlockVersion::V1,
1031 parent_hash: B256::ZERO,
1032 fee_recipient: Address::ZERO,
1033 transactions: vec![],
1034 };
1035 let signature_hash = subblock.signature_hash();
1036 let wrong_signature = wrong_signer.sign(&[], signature_hash.as_slice());
1037
1038 let metadata = vec![SubBlockMetadata {
1039 version: SubBlockVersion::V1,
1040 validator: validator_key, fee_recipient: Address::ZERO,
1042 signature: Bytes::copy_from_slice(wrong_signature.as_ref()), }];
1044
1045 let result = executor.validate_shared_gas(&metadata);
1046 assert!(result.is_err());
1047 assert_eq!(
1048 result.unwrap_err().to_string(),
1049 "invalid subblock signature"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_validate_shared_gas_gas_used_exceeds_gas_per_subblock() {
1055 let chainspec = test_chainspec();
1056 let mut db = State::builder().with_bundle_update().build();
1057 let signer = PrivateKey::from_seed(0);
1058 let validator_key = B256::from_slice(&signer.public_key());
1059 let tx = create_legacy_tx();
1060 let proposer = PartialValidatorKey::from_slice(&validator_key[..15]);
1061
1062 let subblock = tempo_primitives::SubBlock {
1064 version: SubBlockVersion::V1,
1065 parent_hash: B256::ZERO,
1066 fee_recipient: Address::ZERO,
1067 transactions: vec![tx.clone()],
1068 };
1069
1070 let executor = TestExecutorBuilder::default()
1071 .with_validator_set(vec![validator_key])
1072 .with_shared_gas_limit(100) .with_seen_subblock(proposer, vec![tx])
1074 .build(&mut db, &chainspec);
1075 let signature_hash = subblock.signature_hash();
1076 let signature = signer.sign(&[], signature_hash.as_slice());
1077
1078 let metadata = vec![SubBlockMetadata {
1079 version: SubBlockVersion::V1,
1080 validator: validator_key,
1081 fee_recipient: Address::ZERO,
1082 signature: Bytes::copy_from_slice(signature.as_ref()),
1083 }];
1084
1085 let result = executor.validate_shared_gas(&metadata);
1086 assert!(result.is_err());
1087 assert_eq!(
1088 result.unwrap_err().to_string(),
1089 "subblock gas used exceeds gas per subblock"
1090 );
1091 }
1092
1093 #[test]
1094 fn test_validate_shared_gas_unexpected_subblock_len() {
1095 let chainspec = test_chainspec();
1096 let mut db = State::builder().with_bundle_update().build();
1097 let signer = PrivateKey::from_seed(0);
1098 let validator_key = B256::from_slice(&signer.public_key());
1099
1100 let different_key = B256::repeat_byte(0x99);
1102 let different_proposer = PartialValidatorKey::from_slice(&different_key[..15]);
1103
1104 let executor = TestExecutorBuilder::default()
1105 .with_validator_set(vec![validator_key])
1106 .with_seen_subblock(different_proposer, vec![])
1107 .build(&mut db, &chainspec);
1108
1109 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
1111
1112 let result = executor.validate_shared_gas(&metadata);
1113 assert!(result.is_err());
1114 assert_eq!(
1115 result.unwrap_err().to_string(),
1116 "failed to map all non-empty subblocks to metadata"
1117 );
1118 }
1119
1120 #[test]
1121 fn test_validate_shared_gas_limit_exceeded() {
1122 let chainspec = test_chainspec();
1123 let mut db = State::builder().with_bundle_update().build();
1124 let signer = PrivateKey::from_seed(0);
1125 let validator_key = B256::from_slice(&signer.public_key());
1126
1127 let executor = TestExecutorBuilder::default()
1129 .with_validator_set(vec![validator_key])
1130 .with_incentive_gas_used(100_000_000)
1131 .build(&mut db, &chainspec);
1132
1133 let metadata = vec![create_valid_subblock_metadata(B256::ZERO, &signer)];
1134
1135 let result = executor.validate_shared_gas(&metadata);
1136 assert!(result.is_err());
1137 assert_eq!(
1138 result.unwrap_err().to_string(),
1139 "incentive gas limit exceeded"
1140 );
1141 }
1142
1143 #[test]
1144 fn test_is_payment_uses_v2_from_t5() {
1145 let tx = create_tip20_empty_calldata_tx();
1146 assert!(
1147 tx.is_payment_v1(),
1148 "pre-T5 prefix check accepts TIP-20 target"
1149 );
1150 assert!(
1151 !tx.is_payment_v2(),
1152 "T5 classifier rejects empty calldata per TIP-1045"
1153 );
1154
1155 let chainspec = test_chainspec();
1156 let mut db = State::builder().with_bundle_update().build();
1157 let pre_t5_executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1158 assert!(pre_t5_executor.is_payment(&tx));
1159
1160 let chainspec = DEV.clone();
1161 let mut db = State::builder().with_bundle_update().build();
1162 let mut t5_executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1163 t5_executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T5;
1164 assert!(!t5_executor.is_payment(&tx));
1165 }
1166
1167 #[test]
1168 fn test_validate_tx() {
1169 let chainspec = test_chainspec();
1170 let mut db = State::builder().with_bundle_update().build();
1171 let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1172
1173 let tx = create_legacy_tx();
1175 let result = executor.validate_tx(&tx, 21000);
1176 assert!(result.is_ok());
1177 assert_eq!(result.unwrap(), BlockSection::NonShared);
1178 }
1179
1180 fn create_subblock_tx(proposer: &PartialValidatorKey) -> TempoTxEnvelope {
1181 let mut nonce_bytes = [0u8; 32];
1182 nonce_bytes[0] = TEMPO_SUBBLOCK_NONCE_KEY_PREFIX;
1183 nonce_bytes[1..16].copy_from_slice(proposer.as_slice());
1184
1185 let tx = TempoTransaction {
1186 chain_id: 1,
1187 calls: vec![Call {
1188 to: Address::ZERO.into(),
1189 input: Default::default(),
1190 value: Default::default(),
1191 }],
1192 gas_limit: 21000,
1193 nonce_key: U256::from_be_bytes(nonce_bytes),
1194 max_fee_per_gas: 1,
1195 max_priority_fee_per_gas: 1,
1196 ..Default::default()
1197 };
1198
1199 let signature = TempoSignature::from(Signature::test_signature());
1200 TempoTxEnvelope::AA(tx.into_signed(signature))
1201 }
1202
1203 #[test]
1204 fn test_validate_tx_subblock_section_already_passed() {
1205 let chainspec = test_chainspec();
1206 let mut db = State::builder().with_bundle_update().build();
1207 let signer = PrivateKey::from_seed(0);
1208 let validator_key = B256::from_slice(&signer.public_key());
1209 let proposer = PartialValidatorKey::from_slice(&validator_key[..15]);
1210
1211 let executor = TestExecutorBuilder::default()
1213 .with_section(BlockSection::GasIncentive)
1214 .build(&mut db, &chainspec);
1215
1216 let subblock_tx = create_subblock_tx(&proposer);
1217 let result = executor.validate_tx(&subblock_tx, 21000);
1218 assert!(result.is_err());
1219 assert_eq!(
1220 result.unwrap_err().to_string(),
1221 "subblock section already passed"
1222 );
1223
1224 let mut db2 = State::builder().with_bundle_update().build();
1226 let executor2 = TestExecutorBuilder::default()
1227 .with_section(BlockSection::System {
1228 seen_subblocks_signatures: false,
1229 })
1230 .build(&mut db2, &chainspec);
1231
1232 let result = executor2.validate_tx(&subblock_tx, 21000);
1233 assert!(result.is_err());
1234 assert_eq!(
1235 result.unwrap_err().to_string(),
1236 "subblock section already passed"
1237 );
1238 }
1239
1240 #[test]
1241 fn test_validate_tx_proposer_subblock_already_processed() {
1242 let chainspec = test_chainspec();
1243 let mut db = State::builder().with_bundle_update().build();
1244 let signer1 = PrivateKey::from_seed(0);
1245 let validator_key1 = B256::from_slice(&signer1.public_key());
1246 let proposer1 = PartialValidatorKey::from_slice(&validator_key1[..15]);
1247
1248 let signer2 = PrivateKey::from_seed(1);
1249 let validator_key2 = B256::from_slice(&signer2.public_key());
1250 let proposer2 = PartialValidatorKey::from_slice(&validator_key2[..15]);
1251
1252 let executor = TestExecutorBuilder::default()
1254 .with_section(BlockSection::SubBlock {
1255 proposer: proposer2,
1256 })
1257 .with_seen_subblock(proposer1, vec![])
1258 .build(&mut db, &chainspec);
1259
1260 let subblock_tx = create_subblock_tx(&proposer1);
1262 let result = executor.validate_tx(&subblock_tx, 21000);
1263 assert!(result.is_err());
1264 assert_eq!(
1265 result.unwrap_err().to_string(),
1266 "proposer's subblock already processed"
1267 );
1268 }
1269
1270 #[test]
1271 fn test_validate_tx_regular_tx_follow_system_tx() {
1272 let chainspec = test_chainspec();
1273 let mut db = State::builder().with_bundle_update().build();
1274
1275 let executor = TestExecutorBuilder::default()
1277 .with_section(BlockSection::System {
1278 seen_subblocks_signatures: false,
1279 })
1280 .build(&mut db, &chainspec);
1281
1282 let tx = create_legacy_tx();
1284 let result = executor.validate_tx(&tx, 21000);
1285 assert!(result.is_err());
1286 assert_eq!(
1287 result.unwrap_err().to_string(),
1288 "regular transaction can't follow system transaction"
1289 );
1290 }
1291
1292 #[test]
1293 fn test_commit_transaction() {
1294 let chainspec = test_chainspec();
1295 let mut db = State::builder().with_bundle_update().build();
1296 let mut executor = TestExecutorBuilder::default()
1297 .with_general_gas_limit(30_000_000)
1298 .with_parent_beacon_block_root(B256::ZERO)
1299 .build(&mut db, &chainspec);
1300
1301 executor.apply_pre_execution_changes().unwrap();
1303
1304 let tx = create_legacy_tx();
1305 let output = TempoTxResult {
1306 inner: EthTxResult {
1307 result: ResultAndState {
1308 result: revm::context::result::ExecutionResult::Success {
1309 reason: revm::context::result::SuccessReason::Return,
1310 gas: ResultGas::default().with_total_gas_spent(21000),
1311 logs: vec![],
1312 output: revm::context::result::Output::Call(Bytes::new()),
1313 },
1314 state: Default::default(),
1315 },
1316 blob_gas_used: 0,
1317 tx_type: tx.tx_type(),
1318 },
1319 next_section: BlockSection::NonShared,
1320 is_payment: false,
1321 tx: None,
1322 block_gas_used: 21000,
1323 validator_fee: U256::ZERO,
1324 };
1325
1326 let gas_output = executor.commit_transaction(output);
1327
1328 assert_eq!(gas_output.tx_gas_used(), 21000);
1329 assert_eq!(executor.section(), BlockSection::NonShared);
1330 }
1331
1332 #[test]
1333 fn test_finish() {
1334 let chainspec = test_chainspec();
1335 let mut db = State::builder().with_bundle_update().build();
1336 let executor = TestExecutorBuilder::default().build(&mut db, &chainspec);
1337
1338 let result = executor.finish();
1339 assert!(result.is_ok());
1340 }
1341
1342 #[test]
1343 fn test_finish_t4_without_metadata_passes_when_incentive_gas_is_zero() {
1344 let chainspec = DEV.clone();
1345 let mut db = State::builder().with_bundle_update().build();
1346 let mut executor = TestExecutorBuilder::default()
1347 .with_parent_beacon_block_root(B256::ZERO)
1348 .with_validator_set(vec![B256::repeat_byte(0x01)])
1349 .build(&mut db, &chainspec);
1350
1351 executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T4;
1352 executor.apply_pre_execution_changes().unwrap();
1353
1354 assert!(executor.finish().is_ok());
1355 }
1356
1357 #[test]
1358 fn test_finish_t4_without_metadata_rejects_incentive_gas() {
1359 let chainspec = DEV.clone();
1360 let mut db = State::builder().with_bundle_update().build();
1361 let mut executor = TestExecutorBuilder::default()
1362 .with_parent_beacon_block_root(B256::ZERO)
1363 .with_validator_set(vec![B256::repeat_byte(0x01)])
1364 .with_incentive_gas_used(1)
1365 .build(&mut db, &chainspec);
1366
1367 executor.inner.evm.cfg.spec = tempo_chainspec::hardfork::TempoHardfork::T4;
1368 executor.apply_pre_execution_changes().unwrap();
1369
1370 match executor.finish() {
1371 Err(err) => assert_eq!(err.to_string(), "incentive gas limit exceeded"),
1372 Ok(_) => panic!("finish should fail when T4 block has incentive gas without metadata"),
1373 }
1374 }
1375
1376 #[test]
1377 fn test_commit_transaction_tracks_total_cumulative_gas() {
1378 let chainspec = test_chainspec();
1379 let mut db = State::builder().with_bundle_update().build();
1380 let mut executor = TestExecutorBuilder::default()
1381 .with_general_gas_limit(30_000_000)
1382 .with_parent_beacon_block_root(B256::ZERO)
1383 .build(&mut db, &chainspec);
1384
1385 executor.apply_pre_execution_changes().unwrap();
1386
1387 let tx = create_legacy_tx();
1388 let output = TempoTxResult {
1389 inner: EthTxResult {
1390 result: ResultAndState {
1391 result: revm::context::result::ExecutionResult::Success {
1392 reason: revm::context::result::SuccessReason::Return,
1393 gas: ResultGas::new_with_state_gas(21000, 0, 0, 0),
1394 logs: vec![],
1395 output: revm::context::result::Output::Call(Bytes::new()),
1396 },
1397 state: Default::default(),
1398 },
1399 blob_gas_used: 0,
1400 tx_type: tx.tx_type(),
1401 },
1402 next_section: BlockSection::NonShared,
1403 is_payment: false,
1404 tx: None,
1405 block_gas_used: 21000,
1406 validator_fee: U256::ZERO,
1407 };
1408
1409 let gas_output = executor.commit_transaction(output);
1410
1411 assert_eq!(gas_output.tx_gas_used(), 21000);
1413 }
1414
1415 #[test]
1416 fn test_cumulative_gas_accumulates_across_transactions() {
1417 let chainspec = test_chainspec();
1418 let mut db = State::builder().with_bundle_update().build();
1419 let mut executor = TestExecutorBuilder::default()
1420 .with_general_gas_limit(30_000_000)
1421 .with_parent_beacon_block_root(B256::ZERO)
1422 .build(&mut db, &chainspec);
1423
1424 executor.apply_pre_execution_changes().unwrap();
1425
1426 let tx1 = create_legacy_tx();
1428 let output1 = TempoTxResult {
1429 inner: EthTxResult {
1430 result: ResultAndState {
1431 result: revm::context::result::ExecutionResult::Success {
1432 reason: revm::context::result::SuccessReason::Return,
1433 gas: ResultGas::new_with_state_gas(21000, 0, 0, 0),
1434 logs: vec![],
1435 output: revm::context::result::Output::Call(Bytes::new()),
1436 },
1437 state: Default::default(),
1438 },
1439 blob_gas_used: 0,
1440 tx_type: tx1.tx_type(),
1441 },
1442 next_section: BlockSection::NonShared,
1443 is_payment: false,
1444 tx: None,
1445 block_gas_used: 21000,
1446 validator_fee: U256::ZERO,
1447 };
1448 executor.commit_transaction(output1);
1449
1450 let tx2 = create_legacy_tx();
1452 let output2 = TempoTxResult {
1453 inner: EthTxResult {
1454 result: ResultAndState {
1455 result: revm::context::result::ExecutionResult::Success {
1456 reason: revm::context::result::SuccessReason::Return,
1457 gas: ResultGas::new_with_state_gas(50000, 0, 0, 0),
1458 logs: vec![],
1459 output: revm::context::result::Output::Call(Bytes::new()),
1460 },
1461 state: Default::default(),
1462 },
1463 blob_gas_used: 0,
1464 tx_type: tx2.tx_type(),
1465 },
1466 next_section: BlockSection::NonShared,
1467 is_payment: false,
1468 tx: None,
1469 block_gas_used: 50000,
1470 validator_fee: U256::ZERO,
1471 };
1472 executor.commit_transaction(output2);
1473
1474 let receipts = executor.receipts();
1476 assert_eq!(receipts[0].cumulative_gas_used, 21000);
1477 assert_eq!(receipts[1].cumulative_gas_used, 71000);
1478 }
1479
1480 #[test]
1481 fn test_finish_returns_execution_gas_for_block_header() {
1482 let chainspec = test_chainspec();
1483 let mut db = State::builder().with_bundle_update().build();
1484 let mut executor = TestExecutorBuilder::default()
1485 .with_general_gas_limit(30_000_000)
1486 .with_parent_beacon_block_root(B256::ZERO)
1487 .with_section(BlockSection::NonShared)
1488 .build(&mut db, &chainspec);
1489
1490 executor.apply_pre_execution_changes().unwrap();
1491
1492 executor.inner.cumulative_tx_gas_used += 21000;
1494 executor.inner.block_regular_gas_used += 21000;
1495
1496 let (_, result) = executor.finish().unwrap();
1497 assert_eq!(result.gas_used, 21000);
1499 }
1500
1501 #[test]
1502 fn test_non_shared_gas_uses_execution_gas_only() {
1503 let chainspec = test_chainspec();
1504 let mut db = State::builder().with_bundle_update().build();
1505 let mut executor = TestExecutorBuilder::default()
1506 .with_general_gas_limit(30_000_000)
1507 .with_parent_beacon_block_root(B256::ZERO)
1508 .build(&mut db, &chainspec);
1509
1510 executor.apply_pre_execution_changes().unwrap();
1511
1512 let initial_non_shared = executor.non_shared_gas_left;
1513
1514 let tx = create_legacy_tx();
1515 let output = TempoTxResult {
1516 inner: EthTxResult {
1517 result: ResultAndState {
1518 result: revm::context::result::ExecutionResult::Success {
1519 reason: revm::context::result::SuccessReason::Return,
1520 gas: ResultGas::new_with_state_gas(50_000, 0, 0, 0),
1521 logs: vec![],
1522 output: revm::context::result::Output::Call(Bytes::new()),
1523 },
1524 state: Default::default(),
1525 },
1526 blob_gas_used: 0,
1527 tx_type: tx.tx_type(),
1528 },
1529 next_section: BlockSection::NonShared,
1530 is_payment: false,
1531 tx: None,
1532 block_gas_used: 50000,
1533 validator_fee: U256::ZERO,
1534 };
1535 executor.commit_transaction(output);
1536
1537 assert_eq!(executor.non_shared_gas_left, initial_non_shared - 50_000);
1538 }
1539
1540 #[test]
1543 fn test_t4_non_shared_gas_excludes_state_gas() {
1544 let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1545 let mut db = State::builder().with_bundle_update().build();
1546 let mut executor = TestExecutorBuilder::default()
1547 .with_general_gas_limit(30_000_000)
1548 .with_parent_beacon_block_root(B256::ZERO)
1549 .with_amsterdam_eip8037_enabled(true)
1550 .build(&mut db, &chainspec);
1551
1552 executor.apply_pre_execution_changes().unwrap();
1553
1554 let initial_non_shared = executor.non_shared_gas_left;
1555 let initial_non_payment = executor.non_payment_gas_left;
1556
1557 let tx = create_legacy_tx();
1561 let output = TempoTxResult {
1562 inner: EthTxResult {
1563 result: ResultAndState {
1564 result: revm::context::result::ExecutionResult::Success {
1565 reason: revm::context::result::SuccessReason::Return,
1566 gas: ResultGas::new_with_state_gas(300_000, 0, 0, 100_000),
1567 logs: vec![],
1568 output: revm::context::result::Output::Call(Bytes::new()),
1569 },
1570 state: Default::default(),
1571 },
1572 blob_gas_used: 0,
1573 tx_type: tx.tx_type(),
1574 },
1575 next_section: BlockSection::NonShared,
1576 is_payment: false,
1577 tx: None,
1578 block_gas_used: 200_000,
1579 validator_fee: U256::ZERO,
1580 };
1581 executor.commit_transaction(output);
1582
1583 assert_eq!(
1585 executor.non_shared_gas_left,
1586 initial_non_shared - 200_000,
1587 "T4: non_shared_gas_left should exclude state gas"
1588 );
1589 assert_eq!(
1590 executor.non_payment_gas_left,
1591 initial_non_payment - 200_000,
1592 "T4: non_payment_gas_left should exclude state gas"
1593 );
1594 }
1595
1596 #[test]
1598 fn test_t4_incentive_gas_excludes_state_gas() {
1599 let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1600 let mut db = State::builder().with_bundle_update().build();
1601 let mut executor = TestExecutorBuilder::default()
1602 .with_general_gas_limit(30_000_000)
1603 .with_parent_beacon_block_root(B256::ZERO)
1604 .with_amsterdam_eip8037_enabled(true)
1605 .build(&mut db, &chainspec);
1606
1607 executor.apply_pre_execution_changes().unwrap();
1608
1609 let tx = create_legacy_tx();
1610 let output = TempoTxResult {
1611 inner: EthTxResult {
1612 result: ResultAndState {
1613 result: revm::context::result::ExecutionResult::Success {
1614 reason: revm::context::result::SuccessReason::Return,
1615 gas: ResultGas::new_with_state_gas(300_000, 0, 0, 100_000),
1616 logs: vec![],
1617 output: revm::context::result::Output::Call(Bytes::new()),
1618 },
1619 state: Default::default(),
1620 },
1621 blob_gas_used: 0,
1622 tx_type: tx.tx_type(),
1623 },
1624 next_section: BlockSection::GasIncentive,
1625 is_payment: false,
1626 tx: None,
1627 block_gas_used: 200_000,
1628 validator_fee: U256::ZERO,
1629 };
1630 executor.commit_transaction(output);
1631
1632 assert_eq!(
1633 executor.incentive_gas_used, 200_000,
1634 "T4: incentive_gas_used should exclude state gas"
1635 );
1636 }
1637
1638 #[test]
1639 fn test_apply_pre_execution_deploys_validator_v2_code() {
1640 let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1642 let mut db = State::builder().with_bundle_update().build();
1643 let mut executor = TestExecutorBuilder::default()
1644 .with_parent_beacon_block_root(B256::ZERO)
1645 .build(&mut db, &chainspec);
1646
1647 executor.apply_pre_execution_changes().unwrap();
1648
1649 let acc = db.load_cache_account(VALIDATOR_CONFIG_V2_ADDRESS).unwrap();
1650 let info = acc.account_info().unwrap();
1651 assert!(!info.is_empty_code_hash());
1652 }
1653
1654 #[test]
1655 fn test_apply_pre_execution_deploys_signature_verifier_code() {
1656 let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1658 let mut db = State::builder().with_bundle_update().build();
1659 let mut executor = TestExecutorBuilder::default()
1660 .with_parent_beacon_block_root(B256::ZERO)
1661 .build(&mut db, &chainspec);
1662
1663 executor.apply_pre_execution_changes().unwrap();
1664
1665 let acc = db.load_cache_account(SIGNATURE_VERIFIER_ADDRESS).unwrap();
1666 let info = acc.account_info().unwrap();
1667 assert!(!info.is_empty_code_hash());
1668 }
1669
1670 #[test]
1671 fn test_apply_pre_execution_deploys_guard_code() {
1672 let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1674 let mut db = State::builder().with_bundle_update().build();
1675 let mut executor = TestExecutorBuilder::default()
1676 .with_parent_beacon_block_root(B256::ZERO)
1677 .build(&mut db, &chainspec);
1678
1679 executor.apply_pre_execution_changes().unwrap();
1680
1681 let acc = db.load_cache_account(RECEIVE_POLICY_GUARD_ADDRESS).unwrap();
1682 let info = acc.account_info().unwrap();
1683 assert!(!info.is_empty_code_hash());
1684 }
1685
1686 #[test]
1687 fn test_pre_t3_does_not_deploy_signature_verifier_code() {
1688 let chainspec = test_chainspec();
1690 let mut db = State::builder().with_bundle_update().build();
1691 let mut executor = TestExecutorBuilder::default()
1692 .with_parent_beacon_block_root(B256::ZERO)
1693 .build(&mut db, &chainspec);
1694
1695 executor.apply_pre_execution_changes().unwrap();
1696
1697 let acc = db.load_cache_account(SIGNATURE_VERIFIER_ADDRESS).unwrap();
1698 let info = acc.account_info();
1699 assert!(
1700 info.is_none() || info.unwrap().is_empty_code_hash(),
1701 "SignatureVerifier code should not be deployed before T3"
1702 );
1703 }
1704
1705 #[test]
1706 fn test_deploy_precompile_at_boundary_dispatches_state_hook() {
1707 let chainspec = test_chainspec();
1708 let mut db = State::builder().with_bundle_update().build();
1709 let mut executor = TestExecutorBuilder::default()
1710 .with_parent_beacon_block_root(B256::ZERO)
1711 .build(&mut db, &chainspec);
1712
1713 let hook_calls: Arc<Mutex<Vec<EvmState>>> = Arc::new(Mutex::new(Vec::new()));
1714 let hook_calls_clone = hook_calls.clone();
1715 executor
1716 .evm_mut()
1717 .db_mut()
1718 .set_state_hook(Some(Box::new(move |state: EvmState| {
1719 hook_calls_clone.lock().unwrap().push(state);
1720 })));
1721
1722 let addr = Address::with_last_byte(0xff);
1723 executor.deploy_precompile_at_boundary(addr).unwrap();
1724
1725 let acc = db.load_cache_account(addr).unwrap();
1727 let info = acc.account_info().unwrap();
1728 assert!(!info.is_empty_code_hash());
1729
1730 let calls = hook_calls.lock().unwrap();
1732 assert_eq!(calls.len(), 1, "state hook should be called exactly once");
1733 assert!(
1734 calls[0].contains_key(&addr),
1735 "state hook should contain the deployed address"
1736 );
1737 assert_eq!(
1738 calls[0][&addr].original_info(),
1739 Default::default(),
1740 "state hook account should preserve original_info"
1741 );
1742 }
1743
1744 #[test]
1745 fn test_deploy_precompile_at_boundary_preserves_existing_original_info() {
1746 use std::sync::{Arc, Mutex};
1747
1748 let chainspec = test_chainspec();
1749 let mut db = State::builder().with_bundle_update().build();
1750 let addr = Address::with_last_byte(0xfe);
1751 let original_info = AccountInfo {
1752 balance: U256::from(42),
1753 nonce: 7,
1754 ..Default::default()
1755 };
1756 db.insert_account(addr, original_info.clone());
1757
1758 let mut executor = TestExecutorBuilder::default()
1759 .with_parent_beacon_block_root(B256::ZERO)
1760 .build(&mut db, &chainspec);
1761
1762 let hook_calls: Arc<Mutex<Vec<EvmState>>> = Arc::new(Mutex::new(Vec::new()));
1763 let hook_calls_clone = hook_calls.clone();
1764 executor
1765 .evm_mut()
1766 .db_mut()
1767 .set_state_hook(Some(Box::new(move |state: EvmState| {
1768 hook_calls_clone.lock().unwrap().push(state);
1769 })));
1770
1771 executor.deploy_precompile_at_boundary(addr).unwrap();
1772
1773 let calls = hook_calls.lock().unwrap();
1774 assert_eq!(calls.len(), 1, "state hook should be called exactly once");
1775 assert_eq!(
1776 calls[0][&addr].original_info(),
1777 original_info,
1778 "state hook account should preserve existing original_info"
1779 );
1780 }
1781
1782 #[test]
1787 fn test_t4_finish_exempts_state_gas_from_header() {
1788 let chainspec = Arc::new(TempoChainSpec::from_genesis(DEV.genesis().clone()));
1790 let mut db = State::builder().with_bundle_update().build();
1791 let mut executor = TestExecutorBuilder::default()
1792 .with_parent_beacon_block_root(B256::ZERO)
1793 .with_amsterdam_eip8037_enabled(true)
1794 .build(&mut db, &chainspec);
1795
1796 executor.apply_pre_execution_changes().unwrap();
1797
1798 let tx_gas_used = 270_000u64;
1803 let regular_gas = 260_000u64;
1804 let state_gas = 40_000u64;
1805
1806 executor.inner.cumulative_tx_gas_used = tx_gas_used;
1807 executor.inner.block_regular_gas_used = regular_gas;
1808 executor.inner.block_state_gas_used = state_gas;
1809
1810 executor.inner.receipts.push(TempoReceipt {
1811 tx_type: TempoTxType::Legacy,
1812 success: true,
1813 cumulative_gas_used: tx_gas_used,
1814 logs: vec![],
1815 });
1816
1817 let (_evm, result) = executor.finish().expect("finish should succeed");
1818
1819 assert_eq!(
1821 result.gas_used, regular_gas,
1822 "T4 header gas_used ({}) must equal block_regular_gas_used ({})",
1823 result.gas_used, regular_gas
1824 );
1825
1826 let last_cumulative = result.receipts.last().unwrap().cumulative_gas_used;
1828 assert_eq!(last_cumulative, tx_gas_used);
1829 }
1830
1831 #[test]
1836 fn test_pre_t4_finish_uses_cumulative_gas_with_refunds() {
1837 let chainspec = test_chainspec(); let mut db = State::builder().with_bundle_update().build();
1840 let mut executor = TestExecutorBuilder::default()
1841 .with_parent_beacon_block_root(B256::ZERO)
1842 .build(&mut db, &chainspec);
1843
1844 executor.apply_pre_execution_changes().unwrap();
1845
1846 let cumulative = 273_278u64; let regular = 276_078u64; executor.inner.cumulative_tx_gas_used = cumulative;
1853 executor.inner.block_regular_gas_used = regular;
1854
1855 executor.inner.receipts.push(TempoReceipt {
1856 tx_type: TempoTxType::Legacy,
1857 success: true,
1858 cumulative_gas_used: cumulative,
1859 logs: vec![],
1860 });
1861
1862 let (_evm, result) = executor.finish().expect("finish should succeed");
1863
1864 assert_eq!(
1867 result.gas_used, cumulative,
1868 "pre-T4 header gas_used ({}) must equal cumulative_tx_gas_used ({}), \
1869 not block_regular_gas_used ({})",
1870 result.gas_used, cumulative, regular
1871 );
1872 }
1873}