1use std::{
4 cmp::Ordering,
5 fmt::Debug,
6 sync::{Arc, OnceLock},
7};
8
9use alloy_primitives::{Address, TxKind, U256};
10use reth_evm::{EvmError, EvmInternals};
11use revm::{
12 Database,
13 context::{
14 Block, Cfg, ContextTr, JournalTr, LocalContextTr, Transaction, TransactionType,
15 journaled_state::account::JournaledAccountTr,
16 result::{EVMError, ExecutionResult, InvalidTransaction, ResultGas},
17 transaction::{AccessListItem, AccessListItemTr},
18 },
19 context_interface::cfg::{GasId, GasParams},
20 handler::{
21 EvmTr, FrameResult, FrameTr, Handler, MainnetHandler, post_execution,
22 pre_execution::{self, apply_auth_list, calculate_caller_fee},
23 precompile_output_to_interpreter_result, validation,
24 },
25 inspector::{Inspector, InspectorHandler},
26 interpreter::{
27 CallOutcome, CreateOutcome, Gas, InitialAndFloorGas,
28 gas::{
29 COLD_SLOAD_COST, STANDARD_TOKEN_COST, WARM_SSTORE_RESET,
30 get_tokens_in_calldata_istanbul,
31 },
32 interpreter::EthInterpreter,
33 },
34 precompile::PrecompileError,
35};
36use tempo_chainspec::constants::gas::STORAGE_CREDIT_VALUE;
37use tempo_contracts::precompiles::{
38 IAccountKeychain::SignatureType as PrecompileSignatureType, TIPFeeAMMError,
39};
40use tempo_precompiles::{
41 ECRECOVER_GAS,
42 account_keychain::{
43 AccountKeychain, AuthorizedKey, CallScope as PrecompileCallScope, KeyRestrictions,
44 SelectorRule as PrecompileSelectorRule, TokenLimit,
45 },
46 error::TempoPrecompileError,
47 nonce::{
48 EXPIRING_NONCE_MAX_EXPIRY_SECS, EXPIRING_NONCE_SET_CAPACITY, INonce::getNonceCall,
49 NonceManager,
50 },
51 storage::{
52 Handler as _, PrecompileStorageProvider, StorageActions, StorageCtx,
53 evm::EvmPrecompileStorageProvider,
54 },
55 tip_fee_manager::TipFeeManager,
56 tip20::{ITIP20::InsufficientBalance, TIP20Error, TIP20Token},
57 tip20_channel_reserve::TIP20ChannelReserve,
58};
59use tempo_primitives::{
60 TempoAddressExt,
61 transaction::{
62 PrimitiveSignature, SignatureType, TEMPO_EXPIRING_NONCE_KEY, TempoSignature,
63 calc_gas_balance_spending, validate_calls,
64 },
65};
66
67use crate::{
68 TempoBatchCallEnv, TempoEvm, TempoInvalidTransaction, TempoTxEnv,
69 common::TempoStateAccess,
70 error::{FeePaymentError, TempoHaltReason},
71 evm::TempoContext,
72 gas_credits,
73};
74
75const P256_VERIFY_GAS: u64 = 5_000;
78
79const KEYCHAIN_VALIDATION_GAS: u64 = COLD_SLOAD_COST + 900;
81
82const KEY_AUTH_BASE_GAS: u64 = 27_000;
84
85const KEY_AUTH_PER_LIMIT_GAS: u64 = 22_000;
87
88const KEY_AUTH_EXTRA_EVENT_BUFFER: u64 = 1_500;
90
91pub const EXPIRING_NONCE_GAS: u64 = 2 * COLD_SLOAD_COST + 100 + 3 * WARM_SSTORE_RESET;
113
114#[inline]
121fn primitive_signature_verification_gas(signature: &PrimitiveSignature) -> u64 {
122 match signature {
123 PrimitiveSignature::Secp256k1(_) => 0,
124 PrimitiveSignature::P256(_) => P256_VERIFY_GAS,
125 PrimitiveSignature::WebAuthn(webauthn_sig) => {
126 let tokens = get_tokens_in_calldata_istanbul(&webauthn_sig.webauthn_data);
127 P256_VERIFY_GAS + tokens * STANDARD_TOKEN_COST
128 }
129 }
130}
131
132#[inline]
137fn tempo_signature_verification_gas(signature: &TempoSignature) -> u64 {
138 match signature {
139 TempoSignature::Primitive(prim_sig) => primitive_signature_verification_gas(prim_sig),
140 TempoSignature::Keychain(keychain_sig) => {
141 primitive_signature_verification_gas(&keychain_sig.signature) + KEYCHAIN_VALIDATION_GAS
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
148struct LoadedTxAccessKey {
149 key_id: Address,
150 key: AuthorizedKey,
151}
152
153fn call_scope_storage_slots(
160 auth: &tempo_primitives::transaction::KeyAuthorization,
161 spec: tempo_chainspec::hardfork::TempoHardfork,
162) -> u64 {
163 match auth.allowed_calls.as_ref() {
164 None => 0,
165 Some(scopes) if scopes.is_empty() => 1,
166 Some(scopes) => {
167 let is_t4 = spec.is_t4();
168 let mut selector_sets = 0u64;
169 let mut selectors = 0u64;
170 let mut constrained_selectors = 0u64;
171 let mut recipients = 0u64;
172
173 for scope in scopes {
174 if is_t4 && !scope.selector_rules.is_empty() {
175 selector_sets += 1;
176 }
177
178 selectors += scope.selector_rules.len() as u64;
179 for rule in &scope.selector_rules {
180 if !rule.recipients.is_empty() {
181 constrained_selectors += 1;
182 recipients += rule.recipients.len() as u64;
183 }
184 }
185 }
186
187 if is_t4 {
188 1 + scopes.len() as u64 * 2
196 + 1
197 + selectors * 2
198 + selector_sets
199 + constrained_selectors
200 + recipients * 2
201 } else {
202 1 + scopes.len() as u64 * 3 + selectors * 3 + constrained_selectors + recipients * 2
210 }
211 }
212 }
213}
214
215fn call_scope_extra_gas(auth: &tempo_primitives::transaction::KeyAuthorization) -> u64 {
232 const BASE_SCOPE_GAS: u64 = 5_000;
233 const TARGET_SCOPE_GAS: u64 = 7_000;
234 const SELECTOR_SCOPE_GAS: u64 = 7_000;
235 const RECIPIENT_SCOPE_GAS: u64 = 5_000;
236
237 let Some(scopes) = auth.allowed_calls.as_ref() else {
238 return BASE_SCOPE_GAS;
239 };
240
241 let num_targets = scopes.len() as u64;
242 let num_selectors = scopes
243 .iter()
244 .map(|scope| scope.selector_rules.len() as u64)
245 .sum::<u64>();
246 let num_recipients = scopes
247 .iter()
248 .flat_map(|scope| &scope.selector_rules)
249 .map(|rule| rule.recipients.len() as u64)
250 .sum::<u64>();
251
252 BASE_SCOPE_GAS
253 + TARGET_SCOPE_GAS.saturating_mul(num_targets)
254 + SELECTOR_SCOPE_GAS.saturating_mul(num_selectors)
255 + RECIPIENT_SCOPE_GAS.saturating_mul(num_recipients)
256}
257
258fn normalize_failed_batch_result_gas(
268 frame_result: &mut FrameResult,
269 final_gas_limit: u64,
270 accumulated_state_gas_spent: i64,
271) {
272 let mut corrected_gas = Gas::new_spent_with_reservoir(final_gas_limit, 0);
275 if frame_result.instruction_result().is_revert() {
276 corrected_gas.erase_cost(frame_result.gas().remaining());
277 }
278 corrected_gas.set_refund(0);
280 corrected_gas.set_state_gas_spent(0);
282 corrected_gas.set_reservoir(
284 frame_result
285 .gas()
286 .reservoir()
287 .saturating_add_signed(accumulated_state_gas_spent)
288 .saturating_add_signed(frame_result.gas().state_gas_spent()),
289 );
290 *frame_result.gas_mut() = corrected_gas;
291}
292
293fn translate_allowed_calls_for_precompile(
294 key_auth: &tempo_primitives::transaction::SignedKeyAuthorization,
295) -> Vec<PrecompileCallScope> {
296 let Some(scopes) = key_auth.allowed_calls.as_ref() else {
297 return Vec::new();
298 };
299
300 scopes
301 .iter()
302 .map(|scope| PrecompileCallScope {
303 target: scope.target,
304 selectorRules: scope
305 .selector_rules
306 .iter()
307 .map(|rule| PrecompileSelectorRule {
308 selector: rule.selector.into(),
309 recipients: rule.recipients.clone(),
310 })
311 .collect(),
312 })
313 .collect()
314}
315
316#[inline]
331fn calculate_key_authorization_gas(
332 key_auth: &tempo_primitives::transaction::SignedKeyAuthorization,
333 gas_params: &GasParams,
334 spec: tempo_chainspec::hardfork::TempoHardfork,
335) -> (u64, u64) {
336 let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
340
341 let num_limits = key_auth
342 .authorization
343 .limits
344 .as_ref()
345 .map(|limits| limits.len() as u64)
346 .unwrap_or(0);
347
348 if spec.is_t1b() {
349 const BUFFER: u64 = 2_000;
354 let sload_cost =
355 gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
356
357 let limit_slots = if spec.is_t3() {
358 num_limits.saturating_mul(2)
361 } else {
362 num_limits
363 };
364
365 let has_t5_witness = key_auth.has_witness();
366 let mut num_sstores = 1 + limit_slots;
367
368 if spec.is_t3() {
369 num_sstores += call_scope_storage_slots(&key_auth.authorization, spec);
370 }
371
372 let mut sstore_cost = gas_params.get(GasId::sstore_set_without_load_cost());
373 if spec.is_t7() {
374 sstore_cost = sstore_cost.saturating_add(STORAGE_CREDIT_VALUE);
377 }
378 let mut regular_gas = sig_gas + sload_cost + sstore_cost * num_sstores + BUFFER;
379
380 if has_t5_witness {
381 regular_gas += sload_cost + KEY_AUTH_EXTRA_EVENT_BUFFER;
382 }
383
384 if spec.is_t6() && key_auth.is_admin() {
385 regular_gas += KEY_AUTH_EXTRA_EVENT_BUFFER;
386 }
387
388 if spec.is_t4() {
390 regular_gas += call_scope_extra_gas(&key_auth.authorization);
391 }
392
393 let state_gas = gas_params
395 .get(GasId::sstore_set_state_gas())
396 .saturating_mul(num_sstores);
397
398 (regular_gas, state_gas)
399 } else {
400 (
402 KEY_AUTH_BASE_GAS + sig_gas + num_limits * KEY_AUTH_PER_LIMIT_GAS,
403 0,
404 )
405 }
406}
407
408#[derive(Debug)]
412pub struct TempoEvmHandler<DB, I> {
413 _phantom: core::marker::PhantomData<(DB, I)>,
415}
416
417impl<DB, I> TempoEvmHandler<DB, I> {
418 pub fn new() -> Self {
420 Self {
421 _phantom: core::marker::PhantomData,
422 }
423 }
424}
425
426impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
427 fn seed_precompile_tx_context(
428 &self,
429 evm: &mut TempoEvm<DB, I>,
430 ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
431 let ctx = evm.ctx_mut();
432 let channel_open_context_hash = ctx.tx.channel_open_context_hash();
433
434 StorageCtx::enter_evm(
437 &mut ctx.journaled_state,
438 &ctx.block,
439 &ctx.cfg,
440 &ctx.tx,
441 StorageActions::disabled(),
442 || {
443 let mut keychain = AccountKeychain::new();
444 keychain.set_tx_origin(ctx.tx.caller())?;
445
446 if let Some(channel_open_context_hash) = channel_open_context_hash {
447 let mut channel_reserve = TIP20ChannelReserve::new();
448 channel_reserve.set_channel_open_context_hash(channel_open_context_hash)?;
449 }
450
451 Ok::<(), TempoPrecompileError>(())
452 },
453 )
454 .map_err(|e| EVMError::Custom(e.to_string()))
455 }
456}
457
458impl<DB, I> TempoEvmHandler<DB, I>
459where
460 DB: alloy_evm::Database,
461{
462 fn prevalidate_keychain_call_scopes(
463 &self,
464 evm: &mut TempoEvm<DB, I>,
465 calls: &[tempo_primitives::transaction::Call],
466 remaining_gas: &mut u64,
467 reservoir: u64,
468 ) -> Result<Option<FrameResult>, EVMError<DB::Error, TempoInvalidTransaction>> {
469 let spec = *evm.ctx().cfg().spec();
470 if !spec.is_t3() {
471 return Ok(None);
472 }
473
474 let (access_key_addr, user_address) = {
479 let ctx = evm.ctx();
480 let tx = ctx.tx();
481 let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() else {
482 return Ok(None);
483 };
484 let Some(keychain_sig) = tempo_tx_env.signature.as_keychain() else {
485 return Ok(None);
486 };
487
488 let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
489 override_key_id
490 } else {
491 keychain_sig
492 .key_id(&tempo_tx_env.signature_hash)
493 .map_err(|_| {
494 EVMError::Custom(
495 "keychain access key recovery failed after validation".into(),
496 )
497 })?
498 };
499
500 (access_key_addr, keychain_sig.user_address)
501 };
502 let Some(kind) = calls.first().map(|call| call.to) else {
503 return Err(EVMError::Custom(
504 "AA transactions must contain at least one call".into(),
505 ));
506 };
507
508 let actions = evm.actions.clone();
510 let (validation, gas_used) = StorageCtx::enter_ctx_with_gas_limit(
511 evm.ctx_mut(),
512 *remaining_gas,
513 reservoir,
514 actions,
515 || {
516 let keychain = AccountKeychain::default();
517 for call in calls {
518 keychain.validate_call_scope_for_transaction(
519 user_address,
520 access_key_addr,
521 &call.to,
522 call.input.as_ref(),
523 )?;
524 }
525 Ok::<(), TempoPrecompileError>(())
526 },
527 );
528
529 match validation {
530 Ok(()) => {
531 *remaining_gas = remaining_gas.saturating_sub(gas_used);
532 Ok(None)
533 }
534 Err(err) => match err.into_precompile_result(gas_used, reservoir) {
535 Ok(output) => {
536 let interpreter_result =
537 precompile_output_to_interpreter_result(output, *remaining_gas);
538
539 let frame_result = if kind.is_call() {
540 FrameResult::Call(CallOutcome::new(interpreter_result, 0..0))
541 } else {
542 FrameResult::Create(CreateOutcome::new(interpreter_result, None))
543 };
544
545 Ok(Some(frame_result))
546 }
547 Err(PrecompileError::Fatal(err)) => Err(EVMError::Custom(err)),
548 Err(err) => Err(EVMError::Custom(err.to_string())),
549 },
550 }
551 }
552
553 fn execute_single_call_with<F>(
558 &mut self,
559 evm: &mut TempoEvm<DB, I>,
560 gas_limit: u64,
561 reservoir: u64,
562 mut run_loop: F,
563 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
564 where
565 F: FnMut(
566 &mut Self,
567 &mut TempoEvm<DB, I>,
568 <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
569 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
570 {
571 let first_frame_input = self.first_frame_input(evm, gas_limit, reservoir)?;
573
574 let mut frame_result = run_loop(self, evm, first_frame_input)?;
576
577 self.last_frame_result(evm, reservoir, &mut frame_result)?;
579
580 Ok(frame_result)
581 }
582
583 fn execute_single_call(
587 &mut self,
588 evm: &mut TempoEvm<DB, I>,
589 gas_limit: u64,
590 reservoir: u64,
591 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
592 self.execute_single_call_with(evm, gas_limit, reservoir, Self::run_exec_loop)
593 }
594
595 fn execute_multi_call_with<F>(
615 &mut self,
616 evm: &mut TempoEvm<DB, I>,
617 mut remaining_gas: u64,
618 mut reservoir: u64,
619 calls: Vec<tempo_primitives::transaction::Call>,
620 mut execute_single: F,
621 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
622 where
623 F: FnMut(
624 &mut Self,
625 &mut TempoEvm<DB, I>,
626 u64,
627 u64,
628 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
629 {
630 let checkpoint = evm.ctx().journal_mut().checkpoint();
632 let mut accumulated_gas_refund = 0i64;
633 let mut accumulated_state_gas_spent = 0i64;
634
635 let original_kind = evm.ctx().tx().kind();
637 let original_value = evm.ctx().tx().value();
638 let original_data = evm.ctx().tx().input().clone();
639 let original_gas_limit = evm.ctx().tx().gas_limit();
640
641 let mut final_result = None;
642
643 if let Some(mut frame_result) =
644 self.prevalidate_keychain_call_scopes(evm, &calls, &mut remaining_gas, reservoir)?
645 {
646 normalize_failed_batch_result_gas(
649 &mut frame_result,
650 evm.ctx().tx().gas_limit(),
651 accumulated_state_gas_spent,
652 );
653 return Ok(frame_result);
654 }
655
656 for call in calls.iter() {
657 {
659 let tx = &mut evm.ctx().tx;
660 tx.inner.kind = call.to;
661 tx.inner.value = call.value;
662 tx.inner.data = call.input.clone();
663 tx.inner.gas_limit = remaining_gas;
664 }
665
666 let frame_result = execute_single(self, evm, remaining_gas, reservoir);
668
669 {
671 let tx = &mut evm.ctx().tx;
672 tx.inner.kind = original_kind;
673 tx.inner.value = original_value;
674 tx.inner.data = original_data.clone();
675 tx.inner.gas_limit = original_gas_limit;
676 }
677
678 let mut frame_result = frame_result?;
679
680 if !frame_result.instruction_result().is_ok() {
682 evm.ctx().journal_mut().checkpoint_revert(checkpoint);
684
685 let uses_protocol_nonce = evm
695 .ctx()
696 .tx()
697 .tempo_tx_env
698 .as_ref()
699 .map(|aa| aa.nonce_key.is_zero())
700 .unwrap_or(true);
701
702 if uses_protocol_nonce && calls.first().map(|c| c.to.is_create()).unwrap_or(false) {
703 let caller = evm.ctx().tx().caller();
704 if let Ok(mut caller_acc) =
705 evm.ctx().journal_mut().load_account_with_code_mut(caller)
706 {
707 caller_acc.data.bump_nonce();
708 }
709 }
710
711 normalize_failed_batch_result_gas(
712 &mut frame_result,
713 evm.ctx().tx().gas_limit(),
714 accumulated_state_gas_spent,
715 );
716
717 return Ok(frame_result);
718 }
719
720 accumulated_gas_refund =
722 accumulated_gas_refund.saturating_add(frame_result.gas().refunded());
723 accumulated_state_gas_spent =
724 accumulated_state_gas_spent.saturating_add(frame_result.gas().state_gas_spent());
725
726 remaining_gas = frame_result.gas().remaining();
728 reservoir = frame_result.gas().reservoir();
729
730 final_result = Some(frame_result);
731 }
732
733 evm.ctx().journal_mut().checkpoint_commit();
735
736 let mut result =
738 final_result.ok_or_else(|| EVMError::Custom("No calls executed".into()))?;
739
740 let mut corrected_gas = Gas::new(evm.ctx().tx().gas_limit());
743 corrected_gas.set_remaining(result.gas().remaining());
744 corrected_gas.set_refund(accumulated_gas_refund);
745 corrected_gas.set_state_gas_spent(accumulated_state_gas_spent);
746 corrected_gas.set_reservoir(reservoir);
747
748 *result.gas_mut() = corrected_gas;
749
750 Ok(result)
751 }
752
753 fn execute_multi_call(
755 &mut self,
756 evm: &mut TempoEvm<DB, I>,
757 gas_limit: u64,
758 reservoir: u64,
759 calls: Vec<tempo_primitives::transaction::Call>,
760 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
761 self.execute_multi_call_with(evm, gas_limit, reservoir, calls, Self::execute_single_call)
762 }
763
764 fn inspect_execute_single_call(
769 &mut self,
770 evm: &mut TempoEvm<DB, I>,
771 gas_limit: u64,
772 reservoir: u64,
773 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
774 where
775 I: Inspector<TempoContext<DB>, EthInterpreter>,
776 {
777 self.execute_single_call_with(evm, gas_limit, reservoir, Self::inspect_run_exec_loop)
778 }
779
780 fn inspect_execute_multi_call(
785 &mut self,
786 evm: &mut TempoEvm<DB, I>,
787 gas_limit: u64,
788 reservoir: u64,
789 calls: Vec<tempo_primitives::transaction::Call>,
790 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
791 where
792 I: Inspector<TempoContext<DB>, EthInterpreter>,
793 {
794 self.execute_multi_call_with(
795 evm,
796 gas_limit,
797 reservoir,
798 calls,
799 Self::inspect_execute_single_call,
800 )
801 }
802}
803
804impl<DB, I> Default for TempoEvmHandler<DB, I> {
805 fn default() -> Self {
806 Self::new()
807 }
808}
809
810impl<DB, I> Handler for TempoEvmHandler<DB, I>
811where
812 DB: alloy_evm::Database,
813{
814 type Evm = TempoEvm<DB, I>;
815 type Error = EVMError<DB::Error, TempoInvalidTransaction>;
816 type HaltReason = TempoHaltReason;
817
818 #[inline]
824 fn execution(
825 &mut self,
826 evm: &mut Self::Evm,
827 init_and_floor_gas: &InitialAndFloorGas,
828 ) -> Result<FrameResult, Self::Error> {
829 let spec = evm.ctx_ref().cfg().spec();
830 let tx = evm.tx();
831
832 if let Some(oog) = check_gas_limit(*spec, tx, init_and_floor_gas) {
833 return Ok(oog);
834 }
835
836 let (gas_limit, reservoir) = evm.initial_gas_and_reservoir(init_and_floor_gas);
837
838 if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
839 let calls = tempo_tx_env.aa_calls.clone();
840 self.execute_multi_call(evm, gas_limit, reservoir, calls)
841 } else {
842 self.execute_single_call(evm, gas_limit, reservoir)
843 }
844 }
845
846 #[inline]
848 fn post_execution(
849 &self,
850 evm: &mut Self::Evm,
851 exec_result: &mut FrameResult,
852 init_and_floor_gas: InitialAndFloorGas,
853 eip7702_gas_refund: i64,
854 ) -> Result<ResultGas, Self::Error> {
855 if exec_result.instruction_result().is_ok() {
856 gas_credits::apply_refund(evm, exec_result.gas_mut())?;
857 }
858 self.refund(evm, exec_result, eip7702_gas_refund);
859
860 let result_gas = post_execution::build_result_gas(
861 exec_result.instruction_result().is_halt(),
862 exec_result.gas(),
863 init_and_floor_gas,
864 );
865
866 self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas);
867 self.reimburse_caller(evm, exec_result)?;
868 self.reward_beneficiary(evm, exec_result)?;
869
870 Ok(result_gas)
871 }
872
873 #[inline]
881 fn refund(&self, evm: &mut Self::Evm, exec_result: &mut FrameResult, eip7702_refund: i64) {
882 let spec = evm.ctx.cfg.spec;
883 let gas = exec_result.gas_mut();
884 if spec.is_t7() {
885 gas.record_refund(eip7702_refund);
888 } else {
889 post_execution::refund(spec.into(), gas, eip7702_refund);
890 }
891 }
892
893 #[inline]
895 fn execution_result(
896 &mut self,
897 evm: &mut Self::Evm,
898 result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
899 result_gas: ResultGas,
900 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
901 evm.clear();
902
903 MainnetHandler::default()
904 .execution_result(evm, result, result_gas)
905 .map(|result| result.map_haltreason(Into::into))
906 }
907
908 #[inline]
914 fn apply_eip7702_auth_list(
915 &self,
916 evm: &mut Self::Evm,
917 _init_and_floor_gas: &mut InitialAndFloorGas,
918 ) -> Result<u64, Self::Error> {
919 let ctx = &mut evm.ctx;
920 let spec = ctx.cfg.spec;
921
922 let has_aa_auth_list = ctx
924 .tx
925 .tempo_tx_env
926 .as_ref()
927 .map(|aa_env| !aa_env.tempo_authorization_list.is_empty())
928 .unwrap_or(false);
929
930 let refunded_accounts = if has_aa_auth_list {
931 let tempo_tx_env = ctx.tx.tempo_tx_env.as_ref().unwrap();
932
933 apply_auth_list::<_, Self::Error>(
934 ctx.cfg.chain_id,
935 tempo_tx_env
936 .tempo_authorization_list
937 .iter()
938 .filter(|auth| !(spec.is_t0() && auth.signature().is_keychain())),
940 &mut ctx.journaled_state,
941 )?
942 .0
943 } else {
944 apply_auth_list::<_, Self::Error>(
945 ctx.cfg.chain_id,
946 ctx.tx.authorization_list(),
947 &mut ctx.journaled_state,
948 )?
949 .0
950 };
951
952 let refunded_gas = ctx
953 .cfg
954 .gas_params
955 .tx_eip7702_auth_refund_regular()
956 .saturating_mul(refunded_accounts);
957
958 Ok(refunded_gas)
959 }
960
961 #[inline]
962 fn validate_against_state_and_deduct_caller(
963 &self,
964 evm: &mut Self::Evm,
965 init_gas: &mut InitialAndFloorGas,
966 ) -> Result<(), Self::Error> {
967 self.seed_precompile_tx_context(evm)?;
968
969 let actions = evm.actions.clone();
970 let block = &evm.inner.ctx.block;
971 let tx = &evm.inner.ctx.tx;
972 let cfg = &evm.inner.ctx.cfg;
973 let journal = &mut evm.inner.ctx.journaled_state;
974
975 let fee_payer = tx.fee_payer().expect("pre-validated in `validate_env`");
976 let fee_token = journal
977 .get_fee_token(tx, fee_payer, cfg.spec, actions.clone())
978 .map_err(|err| EVMError::Custom(err.to_string()))?;
979
980 evm.fee_token = Some(fee_token);
981
982 if !fee_token.is_tip20() {
985 return Err(TempoInvalidTransaction::FeeTokenNotTip20 { address: fee_token }.into());
986 }
987
988 if !tx.max_balance_spending()?.is_zero() || tx.is_subblock_transaction() {
991 journal.ensure_tip20_usd(cfg.spec, fee_token, actions.clone())?;
992 }
993
994 let account_balance = get_token_balance(journal, fee_token, fee_payer)?;
996
997 let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
999
1000 let nonce_key = tx
1001 .tempo_tx_env
1002 .as_ref()
1003 .map(|aa| aa.nonce_key)
1004 .unwrap_or_default();
1005
1006 let spec = cfg.spec();
1007
1008 let is_expiring_nonce = nonce_key == TEMPO_EXPIRING_NONCE_KEY && spec.is_t1();
1010
1011 pre_execution::validate_account_nonce_and_code(
1013 &caller_account.account().info,
1014 tx.nonce(),
1015 cfg.is_eip3607_disabled(),
1016 cfg.is_nonce_check_disabled() || !nonce_key.is_zero(),
1018 )?;
1019
1020 caller_account.touch();
1022
1023 if !nonce_key.is_zero()
1028 && tx.first_call().is_some_and(|(kind, _)| kind.is_create())
1029 && caller_account.nonce() == 0
1030 {
1031 init_gas.initial_regular_gas += cfg.gas_params.get(GasId::new_account_cost());
1032 init_gas.initial_state_gas += cfg.gas_params.new_account_state_gas();
1033
1034 if tx.gas_limit() < init_gas.initial_total_gas() {
1036 return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
1037 gas_limit: tx.gas_limit(),
1038 initial_gas: init_gas.initial_total_gas(),
1039 }
1040 .into());
1041 }
1042
1043 if cfg.is_amsterdam_eip8037_enabled()
1045 && init_gas.initial_regular_gas().max(init_gas.floor_gas) > cfg.tx_gas_limit_cap()
1046 {
1047 return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
1048 gas_floor: init_gas.initial_regular_gas(),
1049 gas_limit: cfg.tx_gas_limit_cap(),
1050 }
1051 .into());
1052 }
1053 }
1054
1055 if is_expiring_nonce {
1056 let tempo_tx_env = tx
1061 .tempo_tx_env
1062 .as_ref()
1063 .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?;
1064
1065 if tx.nonce() != 0 {
1067 return Err(TempoInvalidTransaction::ExpiringNonceNonceNotZero.into());
1068 }
1069
1070 let replay_hash = if spec.is_t1b() {
1071 tx.unique_tx_identifier()
1072 .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?
1073 } else {
1074 tempo_tx_env.tx_hash
1075 };
1076 let valid_before = tempo_tx_env
1077 .valid_before
1078 .ok_or(TempoInvalidTransaction::ExpiringNonceMissingValidBefore)?;
1079
1080 let block_timestamp = block.timestamp().saturating_to::<u64>();
1081 StorageCtx::enter_evm_without_tip1060_accounting(
1082 journal,
1083 block,
1084 cfg,
1085 tx,
1086 actions.clone(),
1087 || {
1088 let mut nonce_manager = NonceManager::new();
1089
1090 let prev_ptr = if let Some(expiring_nonce_idx) = tempo_tx_env.expiring_nonce_idx
1091 {
1092 let ptr = nonce_manager
1093 .expiring_nonce_ring_ptr
1094 .read()
1095 .map_err(|err| EVMError::Custom(err.to_string()))?;
1096
1097 let next = (ptr + expiring_nonce_idx as u32) % EXPIRING_NONCE_SET_CAPACITY;
1098
1099 nonce_manager
1100 .expiring_nonce_ring_ptr
1101 .write(next)
1102 .map_err(|err| EVMError::Custom(err.to_string()))?;
1103
1104 Some(ptr)
1105 } else {
1106 None
1107 };
1108
1109 nonce_manager
1110 .check_and_mark_expiring_nonce(replay_hash, valid_before)
1111 .map_err(|err| match err {
1112 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1113 TempoPrecompileError::NonceError(
1114 tempo_contracts::precompiles::NonceError::InvalidExpiringNonceExpiry(_),
1115 ) => {
1116 let max_allowed =
1117 block_timestamp.saturating_add(EXPIRING_NONCE_MAX_EXPIRY_SECS);
1118 if valid_before <= block_timestamp {
1119 TempoInvalidTransaction::NonceManagerError(format!(
1120 "expiring nonce transaction expired: valid_before ({valid_before}) <= block timestamp ({block_timestamp})"
1121 ))
1122 .into()
1123 } else {
1124 TempoInvalidTransaction::NonceManagerError(format!(
1125 "expiring nonce valid_before ({valid_before}) too far in the future: must be within {EXPIRING_NONCE_MAX_EXPIRY_SECS}s of block timestamp ({block_timestamp}), max allowed is {max_allowed}"
1126 ))
1127 .into()
1128 }
1129 }
1130 err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
1131 })?;
1132
1133 if let Some(prev_ptr) = prev_ptr {
1134 nonce_manager
1135 .expiring_nonce_ring_ptr
1136 .write(prev_ptr)
1137 .map_err(|err| EVMError::Custom(err.to_string()))?;
1138 }
1139
1140 Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
1141 },
1142 )?;
1143 } else if !nonce_key.is_zero() {
1144 StorageCtx::enter_evm_without_tip1060_accounting(
1146 journal,
1147 block,
1148 cfg,
1149 tx,
1150 actions.clone(),
1151 || {
1152 let mut nonce_manager = NonceManager::new();
1153
1154 if !cfg.is_nonce_check_disabled() {
1155 let tx_nonce = tx.nonce();
1156 let state = nonce_manager
1157 .get_nonce(getNonceCall {
1158 account: tx.caller(),
1159 nonceKey: nonce_key,
1160 })
1161 .map_err(|err| match err {
1162 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1163 err => TempoInvalidTransaction::NonceManagerError(err.to_string())
1164 .into(),
1165 })?;
1166
1167 match tx_nonce.cmp(&state) {
1168 Ordering::Greater => {
1169 return Err(InvalidTransaction::NonceTooHigh {
1170 tx: tx_nonce,
1171 state,
1172 }
1173 .into());
1174 }
1175 Ordering::Less => {
1176 return Err(InvalidTransaction::NonceTooLow {
1177 tx: tx_nonce,
1178 state,
1179 }
1180 .into());
1181 }
1182 _ => {}
1183 }
1184 }
1185
1186 nonce_manager
1188 .increment_nonce(tx.caller(), nonce_key)
1189 .map_err(|err| match err {
1190 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1191 err => {
1192 TempoInvalidTransaction::NonceManagerError(err.to_string()).into()
1193 }
1194 })?;
1195
1196 Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
1197 },
1198 )?;
1199 } else {
1200 if tx.kind().is_call() {
1205 caller_account.bump_nonce();
1206 }
1207 }
1208
1209 let new_balance = calculate_caller_fee(account_balance, tx, block, cfg)?;
1211 let gas_balance_spending = core::cmp::max(account_balance, new_balance) - new_balance;
1214
1215 let mut loaded_tx_access_key = None;
1222 let mut same_tx_key_authorization_use = false;
1223 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1224 && let Some(keychain_sig) = tempo_tx_env.signature.as_keychain()
1225 {
1226 let user_address = &keychain_sig.user_address;
1229
1230 if *user_address != tx.caller {
1232 return Err(TempoInvalidTransaction::KeychainUserAddressMismatch {
1233 user_address: *user_address,
1234 caller: tx.caller,
1235 }
1236 .into());
1237 }
1238
1239 let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
1241 override_key_id
1242 } else {
1243 keychain_sig
1244 .key_id(&tempo_tx_env.signature_hash)
1245 .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1246 };
1247
1248 let key_auth = tempo_tx_env.key_authorization.as_ref();
1249 same_tx_key_authorization_use =
1252 key_auth.is_some_and(|key_auth| access_key_addr == key_auth.key_id);
1253
1254 if same_tx_key_authorization_use {
1255 let key_auth = key_auth.expect("same-tx auth/use requires inline authorization");
1256
1257 if !gas_balance_spending.is_zero()
1261 && fee_payer == tx.caller
1262 && let Some(limits) = key_auth.limits.as_ref()
1263 {
1264 let remaining = limits
1265 .iter()
1266 .rev()
1267 .find(|limit| limit.token == fee_token)
1268 .map(|limit| limit.limit)
1269 .unwrap_or_default();
1270
1271 if gas_balance_spending > remaining {
1272 return Err(
1273 FeePaymentError::Other("SpendingLimitExceeded".to_string()).into()
1274 );
1275 }
1276 }
1277 } else {
1278 let loaded_key = StorageCtx::enter_precompile(
1283 journal,
1284 block,
1285 cfg,
1286 tx,
1287 actions.clone(),
1288 |mut keychain: AccountKeychain| {
1289 let tx_sig_type = keychain_sig.signature.signature_type().into();
1295 let sig_type = (key_auth.is_some() || spec.is_t1()).then_some(tx_sig_type);
1296
1297 let key = keychain
1298 .validate_keychain_authorization(
1299 *user_address,
1300 access_key_addr,
1301 block.timestamp().to::<u64>(),
1302 sig_type,
1303 )
1304 .map_err(|e| TempoInvalidTransaction::KeychainValidationFailed {
1305 reason: format!("{e:?}"),
1306 })?;
1307
1308 if key_auth.is_some() && !key.is_admin {
1311 return Err(
1312 TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys.into()
1313 );
1314 }
1315
1316 keychain
1320 .set_transaction_key(access_key_addr)
1321 .map_err(|e| EVMError::Custom(e.to_string()))?;
1322
1323 Ok::<_, EVMError<_, TempoInvalidTransaction>>(LoadedTxAccessKey {
1324 key_id: access_key_addr,
1325 key,
1326 })
1327 },
1328 )?;
1329
1330 evm.key_expiry = Some(loaded_key.key.expiry);
1331 loaded_tx_access_key = Some(loaded_key);
1332 }
1333 }
1334
1335 if cfg.spec.is_t6()
1338 && let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1339 && let Some(key_auth) = tempo_tx_env.key_authorization.as_ref()
1340 {
1341 let auth_signer = key_auth
1342 .recover_signer()
1343 .map_err(|_| TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed)?;
1344
1345 if auth_signer != tx.caller {
1346 let key_auth_sig_type: u8 = key_auth.signature.signature_type().into();
1347 let signer_is_admin = match loaded_tx_access_key {
1348 Some(loaded_key)
1349 if loaded_key.key_id == auth_signer
1350 && (loaded_key.key.signature_type as u8) == key_auth_sig_type =>
1351 {
1352 loaded_key.key.is_admin
1353 }
1354 Some(_) | None => {
1355 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1356 reason:
1357 "admin-signed key authorization must be signed by transaction key"
1358 .to_string(),
1359 }
1360 .into());
1361 }
1362 };
1363
1364 if !signer_is_admin {
1365 return Err(TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot {
1366 expected: tx.caller,
1367 actual: auth_signer,
1368 }
1369 .into());
1370 }
1371 }
1372 }
1373
1374 if !gas_balance_spending.is_zero() {
1376 let checkpoint = journal.checkpoint();
1377
1378 let skip_liquidity_check = evm.skip_liquidity_check;
1379 let result = StorageCtx::enter_evm_without_tip1060_accounting(
1380 journal,
1381 &block,
1382 cfg,
1383 tx,
1384 actions.clone(),
1385 || {
1386 TipFeeManager::new().collect_fee_pre_tx(
1387 fee_payer,
1388 fee_token,
1389 gas_balance_spending,
1390 block.beneficiary(),
1391 skip_liquidity_check,
1392 )
1393 },
1394 );
1395
1396 if let Err(err) = result {
1397 journal.checkpoint_revert(checkpoint);
1399
1400 return Err(match err {
1404 TempoPrecompileError::TIPFeeAMMError(
1405 TIPFeeAMMError::InsufficientLiquidity(_),
1406 ) => FeePaymentError::InsufficientAmmLiquidity {
1407 fee: gas_balance_spending,
1408 }
1409 .into(),
1410
1411 TempoPrecompileError::TIP20(TIP20Error::InsufficientBalance(
1412 InsufficientBalance { available, .. },
1413 )) => FeePaymentError::InsufficientFeeTokenBalance {
1414 fee: gas_balance_spending,
1415 balance: available,
1416 }
1417 .into(),
1418
1419 TempoPrecompileError::TIP20(TIP20Error::ContractPaused(_)) => {
1420 TempoInvalidTransaction::FeeTokenPaused { address: fee_token }.into()
1421 }
1422
1423 TempoPrecompileError::Fatal(e) => EVMError::Custom(e),
1424
1425 _ => FeePaymentError::Other(err.to_string()).into(),
1426 });
1427 }
1428
1429 journal.checkpoint_commit();
1430 evm.collected_fee = gas_balance_spending;
1431 }
1432
1433 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1438 && let Some(key_auth) = &tempo_tx_env.key_authorization
1439 {
1440 let keychain_checkpoint = if spec.is_t1() {
1441 Some(journal.checkpoint())
1442 } else {
1443 None
1444 };
1445
1446 let amsterdam_eip8037_enabled = cfg.enable_amsterdam_eip8037;
1447 let internals = EvmInternals::new(journal, block, cfg, tx);
1448
1449 let gas_limit = if spec.is_t1() && !spec.is_t1b() {
1456 tx.gas_limit() - init_gas.initial_total_gas()
1457 } else {
1458 u64::MAX
1459 };
1460
1461 let gas_params = if spec.is_t1() {
1463 static TABLE: OnceLock<GasParams> = OnceLock::new();
1464 TABLE
1466 .get_or_init(|| {
1467 let mut table = [0u64; 256];
1468 table[GasId::sstore_set_without_load_cost().as_usize()] =
1469 cfg.gas_params.get(GasId::sstore_set_without_load_cost());
1470 table[GasId::warm_storage_read_cost().as_usize()] =
1471 cfg.gas_params.get(GasId::warm_storage_read_cost());
1472 GasParams::new(Arc::new(table))
1473 })
1474 .clone()
1475 } else {
1476 cfg.gas_params.clone()
1477 };
1478
1479 let mut provider = EvmPrecompileStorageProvider::new(
1481 internals,
1482 gas_limit,
1483 0,
1484 cfg.spec,
1485 amsterdam_eip8037_enabled,
1486 false,
1487 gas_params,
1488 )
1489 .with_actions(actions.clone());
1490 provider.set_tip1060_storage_credits(false);
1491
1492 let out_of_gas = StorageCtx::enter(&mut provider, || {
1494 let mut keychain = AccountKeychain::default();
1495 let access_key_addr = key_auth.key_id;
1496
1497 let signature_type = match key_auth.key_type {
1500 SignatureType::Secp256k1 => PrecompileSignatureType::Secp256k1,
1501 SignatureType::P256 => PrecompileSignatureType::P256,
1502 SignatureType::WebAuthn => PrecompileSignatureType::WebAuthn,
1503 };
1504
1505 let expiry = key_auth.expiry.map_or(u64::MAX, |expiry| expiry.get());
1507
1508 let enforce_limits = key_auth.limits.is_some();
1512 let precompile_limits: Vec<TokenLimit> = key_auth
1513 .limits
1514 .as_ref()
1515 .map(|limits| {
1516 limits
1517 .iter()
1518 .map(|limit| TokenLimit {
1519 token: limit.token,
1520 amount: limit.limit,
1521 period: limit.period,
1522 })
1523 .collect()
1524 })
1525 .unwrap_or_default();
1526
1527 let allow_any_calls = key_auth.allowed_calls.is_none();
1528 let precompile_allowed_calls = translate_allowed_calls_for_precompile(key_auth);
1529
1530 let config = KeyRestrictions {
1531 expiry,
1532 enforceLimits: enforce_limits,
1533 limits: precompile_limits,
1534 allowAnyCalls: allow_any_calls,
1535 allowedCalls: precompile_allowed_calls,
1536 };
1537
1538 let result = if key_auth.is_admin() {
1540 keychain.authorize_admin_key(
1541 tx.caller,
1542 access_key_addr,
1543 signature_type,
1544 key_auth.witness(),
1545 )
1546 } else {
1547 keychain.authorize_key(
1548 tx.caller,
1549 access_key_addr,
1550 signature_type,
1551 config,
1552 key_auth.witness(),
1553 )
1554 };
1555
1556 match result {
1557 Ok(_) => Ok(false),
1559 Err(TempoPrecompileError::OutOfGas) => Ok(true),
1561 Err(TempoPrecompileError::Fatal(err)) => Err(EVMError::Custom(err)),
1562 Err(err) => Err(TempoInvalidTransaction::KeychainPrecompileError {
1563 reason: err.to_string(),
1564 }
1565 .into()),
1566 }
1567 })?;
1568
1569 let gas_used = provider.gas_used();
1570 drop(provider);
1571
1572 if let Some(expiry) = key_auth.expiry {
1574 evm.key_expiry = Some(expiry.get());
1575 }
1576
1577 if let Some(keychain_checkpoint) = keychain_checkpoint {
1582 if spec.is_t1b() {
1583 journal.checkpoint_commit();
1584 } else if out_of_gas {
1585 init_gas.initial_regular_gas = u64::MAX;
1586 journal.checkpoint_revert(keychain_checkpoint);
1587 } else {
1588 init_gas.initial_regular_gas += gas_used;
1589 journal.checkpoint_commit();
1590 };
1591 }
1592
1593 if same_tx_key_authorization_use {
1597 StorageCtx::enter_evm_without_tip1060_accounting(
1598 journal,
1599 block,
1600 cfg,
1601 tx,
1602 actions,
1603 || {
1604 let mut keychain = AccountKeychain::new();
1605 keychain
1606 .set_transaction_key(key_auth.key_id)
1607 .map_err(|e| EVMError::Custom(e.to_string()))?;
1608
1609 if evm.collected_fee.is_zero() {
1610 return Ok(());
1611 }
1612
1613 keychain
1614 .authorize_transfer(fee_payer, fee_token, evm.collected_fee)
1615 .map_err(|err| match err {
1616 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
1617 err => FeePaymentError::Other(err.to_string()).into(),
1618 })
1619 },
1620 )?;
1621 }
1622 }
1623
1624 Ok(())
1625 }
1626
1627 fn reimburse_caller(
1628 &self,
1629 evm: &mut Self::Evm,
1630 exec_result: &mut FrameResult,
1631 ) -> Result<(), Self::Error> {
1632 let actions = evm.actions.clone();
1633 let context = &mut evm.inner.ctx;
1635 let tx = context.tx();
1636 let basefee = u128::from(context.block().basefee());
1637 let effective_gas_price = tx.effective_gas_price(basefee);
1638 let gas = exec_result.gas();
1639
1640 let actual_spending = calc_gas_balance_spending(
1641 gas.used().saturating_sub(gas.reservoir()),
1642 effective_gas_price,
1643 );
1644 let refund_amount = tx.effective_balance_spending(
1645 context.block.basefee.into(),
1646 context.block.blob_gasprice().unwrap_or_default(),
1647 )? - tx.value
1648 - actual_spending;
1649
1650 if context.cfg.disable_fee_charge
1656 && evm.collected_fee.is_zero()
1657 && !actual_spending.is_zero()
1658 {
1659 return Ok(());
1660 }
1661
1662 let (journal, block, tx) = (&mut context.journaled_state, &context.block, &context.tx);
1664 let beneficiary = context.block.beneficiary();
1665
1666 let credited = StorageCtx::enter_evm_without_tip1060_accounting(
1667 &mut *journal,
1668 block,
1669 &context.cfg,
1670 tx,
1671 actions,
1672 || {
1673 let mut fee_manager = TipFeeManager::new();
1674
1675 if !actual_spending.is_zero() || !refund_amount.is_zero() {
1676 let fee_payer = tx.fee_payer().expect("pre-validated in `validate_env`");
1677 let fee_token = evm
1678 .fee_token
1679 .expect("set in `validate_against_state_and_deduct_caller`");
1680 fee_manager
1682 .collect_fee_post_tx(
1683 fee_payer,
1684 actual_spending,
1685 refund_amount,
1686 fee_token,
1687 beneficiary,
1688 )
1689 .map_err(|e| EVMError::Custom(format!("{e:?}")))
1690 } else {
1691 Ok(U256::ZERO)
1692 }
1693 },
1694 )?;
1695
1696 evm.validator_fee = credited;
1699 Ok(())
1700 }
1701
1702 #[inline]
1703 fn reward_beneficiary(
1704 &self,
1705 _evm: &mut Self::Evm,
1706 _exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
1707 ) -> Result<(), Self::Error> {
1708 Ok(())
1711 }
1712
1713 #[inline]
1719 fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
1720 evm.collected_fee = U256::ZERO;
1722 evm.validator_fee = U256::ZERO;
1723
1724 let fee_payer = evm.ctx.tx.fee_payer()?;
1726
1727 if evm.ctx.cfg.spec.is_t2()
1728 && evm.ctx.tx.has_fee_payer_signature()
1729 && fee_payer == evm.ctx.tx.caller()
1730 {
1731 return Err(TempoInvalidTransaction::SelfSponsoredFeePayer.into());
1732 }
1733
1734 if !evm.ctx.tx.value().is_zero() {
1737 return Err(TempoInvalidTransaction::ValueTransferNotAllowed.into());
1738 }
1739
1740 validation::validate_env::<_, Self::Error>(evm.ctx())?;
1743
1744 let cfg = &evm.inner.cfg;
1746 let tx = &evm.inner.tx;
1747
1748 if let Some(aa_env) = tx.tempo_tx_env.as_ref() {
1749 validate_calls(
1751 &aa_env.aa_calls,
1752 !aa_env.tempo_authorization_list.is_empty(),
1753 )
1754 .map_err(TempoInvalidTransaction::from)?;
1755
1756 if cfg.spec().is_t3()
1761 && aa_env.signature.is_keychain()
1762 && aa_env
1763 .aa_calls
1764 .first()
1765 .is_some_and(|call| call.to.is_create())
1766 {
1767 return Err(TempoInvalidTransaction::CallsValidation(
1768 "access-key transactions cannot use CREATE as the first call",
1769 )
1770 .into());
1771 }
1772
1773 aa_env
1775 .signature
1776 .validate_version(cfg.spec().is_t1c())
1777 .map_err(TempoInvalidTransaction::from)?;
1778 for auth in &aa_env.tempo_authorization_list {
1779 auth.signature()
1780 .validate_version(cfg.spec().is_t1c())
1781 .map_err(TempoInvalidTransaction::from)?;
1782 }
1783
1784 let has_keychain_fields =
1785 aa_env.key_authorization.is_some() || aa_env.signature.is_keychain();
1786
1787 if aa_env.subblock_transaction && has_keychain_fields {
1788 return Err(TempoInvalidTransaction::KeychainOpInSubblockTransaction.into());
1789 }
1790
1791 if let Some(key_auth) = &aa_env.key_authorization {
1792 let mut same_tx_auth_use = false;
1795 if let Some(keychain_sig) = aa_env.signature.as_keychain() {
1796 let access_key_addr = if let Some(override_key_id) = aa_env.override_key_id {
1798 override_key_id
1799 } else {
1800 keychain_sig
1802 .key_id(&aa_env.signature_hash)
1803 .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1804 };
1805
1806 same_tx_auth_use = access_key_addr == key_auth.key_id;
1807 if !same_tx_auth_use && !cfg.spec.is_t6() {
1808 return Err(
1809 TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys.into()
1810 );
1811 }
1812
1813 if same_tx_auth_use
1814 && cfg.spec.is_t3()
1815 && key_auth.key_type != keychain_sig.signature.signature_type()
1816 {
1817 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1818 reason: "key authorization key_type does not match the keychain signature type"
1819 .to_string(),
1820 }
1821 .into());
1822 }
1823 }
1824
1825 if (key_auth.is_admin || key_auth.account.is_some()) && !cfg.spec.is_t6() {
1826 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1827 reason: "T6 key authorization fields are not active before T6".to_string(),
1828 }
1829 .into());
1830 }
1831
1832 if cfg.spec.is_t6() && key_auth.account.is_some_and(|account| account != tx.caller)
1833 {
1834 let reason = if key_auth.is_admin() {
1839 "admin key authorization account mismatch"
1840 } else {
1841 "key authorization account mismatch"
1842 };
1843
1844 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1845 reason: reason.to_string(),
1846 }
1847 .into());
1848 }
1849
1850 if key_auth.is_admin()
1851 && (key_auth.expiry.is_some()
1852 || key_auth.limits.is_some()
1853 || key_auth.allowed_calls.is_some())
1854 {
1855 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1856 reason:
1857 "admin key authorizations cannot carry expiry, limits, or call scopes"
1858 .to_string(),
1859 }
1860 .into());
1861 }
1862
1863 if !cfg.spec.is_t6() {
1864 let auth_signer = key_auth.recover_signer().map_err(|_| {
1865 TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed
1866 })?;
1867
1868 if auth_signer != tx.caller {
1869 return Err(TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot {
1870 expected: tx.caller,
1871 actual: auth_signer,
1872 }
1873 .into());
1874 }
1875 }
1876
1877 key_auth
1881 .validate_chain_id(cfg.chain_id(), cfg.spec.is_t1c())
1882 .map_err(TempoInvalidTransaction::from)?;
1883
1884 if key_auth.has_witness() && !cfg.spec.is_t5() {
1885 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1886 reason: "key authorization witnesses are not active before T5".to_string(),
1887 }
1888 .into());
1889 }
1890
1891 if !cfg.spec.is_t3() {
1894 if key_auth.has_periodic_limits() {
1895 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1896 reason: "periodic token limits are not active before T3".to_string(),
1897 }
1898 .into());
1899 }
1900
1901 if key_auth.has_call_scopes() {
1902 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1903 reason: "call scopes are not active before T3".to_string(),
1904 }
1905 .into());
1906 }
1907 }
1908
1909 if cfg.spec.is_t6() {
1910 let auth_signer = key_auth.recover_signer().map_err(|_| {
1911 TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed
1912 })?;
1913 if auth_signer != tx.caller && key_auth.account.is_none() {
1914 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1915 reason: "admin-signed key authorization account mismatch".to_string(),
1916 }
1917 .into());
1918 }
1919
1920 if auth_signer == tx.caller
1921 && aa_env.signature.is_keychain()
1922 && !same_tx_auth_use
1923 {
1924 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1925 reason:
1926 "root-signed key authorization must use root transaction signature"
1927 .to_string(),
1928 }
1929 .into());
1930 }
1931
1932 if auth_signer != tx.caller {
1933 let Some(keychain_sig) = aa_env.signature.as_keychain() else {
1934 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1935 reason:
1936 "admin-signed key authorization must be signed by transaction key"
1937 .to_string(),
1938 }
1939 .into());
1940 };
1941
1942 let access_key_addr = if let Some(override_key_id) = aa_env.override_key_id
1943 {
1944 override_key_id
1945 } else {
1946 keychain_sig
1947 .key_id(&aa_env.signature_hash)
1948 .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1949 };
1950
1951 if access_key_addr != auth_signer {
1952 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1953 reason:
1954 "admin-signed key authorization must be signed by transaction key"
1955 .to_string(),
1956 }
1957 .into());
1958 }
1959
1960 if key_auth.signature.signature_type()
1961 != keychain_sig.signature.signature_type()
1962 {
1963 return Err(TempoInvalidTransaction::KeychainValidationFailed {
1964 reason:
1965 "admin-signed key authorization signature type does not match transaction key signature type"
1966 .to_string(),
1967 }
1968 .into());
1969 }
1970 }
1971 }
1972
1973 if let Some(expiry) = key_auth.expiry {
1975 evm.key_expiry = Some(expiry.get());
1976 }
1977 }
1978
1979 let base_fee = if cfg.is_base_fee_check_disabled() {
1981 None
1982 } else {
1983 Some(u128::from(evm.ctx_ref().block().basefee()))
1984 };
1985
1986 validation::validate_priority_fee_tx(
1987 tx.max_fee_per_gas(),
1988 tx.max_priority_fee_per_gas().unwrap_or_default(),
1989 base_fee,
1990 cfg.is_priority_fee_check_disabled(),
1991 )?;
1992
1993 let block_timestamp = evm.ctx_ref().block().timestamp().saturating_to();
1995 let valid_after = aa_env.valid_after.filter(|_| !evm.skip_valid_after_check);
1996 validate_time_window(valid_after, aa_env.valid_before, block_timestamp)?;
1997 }
1998
1999 Ok(())
2000 }
2001
2002 #[inline]
2009 fn validate_initial_tx_gas(
2010 &self,
2011 evm: &mut Self::Evm,
2012 ) -> Result<InitialAndFloorGas, Self::Error> {
2013 let tx = evm.ctx_ref().tx();
2014 let spec = evm.ctx_ref().cfg().spec();
2015 let gas_params = evm.ctx_ref().cfg().gas_params();
2016 let gas_limit = tx.gas_limit();
2017
2018 let mut init_gas = if tx.tempo_tx_env.is_some() {
2020 validate_aa_initial_tx_gas(evm)?
2022 } else {
2023 let mut acc = 0;
2024 let mut storage = 0;
2025 if tx.tx_type() != TransactionType::Legacy {
2027 (acc, storage) = tx
2028 .access_list()
2029 .map(|al| {
2030 al.fold((0, 0), |(acc, storage), item| {
2031 (acc + 1, storage + item.storage_slots().count())
2032 })
2033 })
2034 .unwrap_or_default();
2035 };
2036 let mut init_gas = gas_params.initial_tx_gas(
2037 tx.input(),
2038 tx.kind().is_create(),
2039 acc as u64,
2040 storage as u64,
2041 tx.authorization_list_len() as u64,
2042 );
2043 for auth in tx.authorization_list() {
2047 if spec.is_t1() && auth.nonce == 0 {
2048 init_gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2049 init_gas.initial_state_gas += gas_params.new_account_state_gas();
2050 }
2051 }
2052
2053 if spec.is_t1() && tx.nonce == 0 {
2056 init_gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2059 init_gas.initial_state_gas += gas_params.new_account_state_gas();
2060 }
2061
2062 if gas_limit < init_gas.initial_total_gas() {
2064 return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
2065 gas_limit,
2066 initial_gas: init_gas.initial_total_gas(),
2067 }
2068 .into());
2069 }
2070
2071 init_gas
2072 };
2073
2074 if evm.ctx.cfg.is_eip7623_disabled() {
2075 init_gas.floor_gas = 0u64;
2076 }
2077
2078 if gas_limit < init_gas.floor_gas {
2080 return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
2081 gas_limit,
2082 gas_floor: init_gas.floor_gas,
2083 }
2084 .into());
2085 }
2086
2087 if evm.ctx.cfg.is_amsterdam_eip8037_enabled()
2089 && init_gas.initial_regular_gas().max(init_gas.floor_gas)
2090 > evm.ctx.cfg.tx_gas_limit_cap()
2091 {
2092 return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
2093 gas_floor: init_gas.initial_regular_gas(),
2094 gas_limit: evm.ctx.cfg.tx_gas_limit_cap(),
2095 }
2096 .into());
2097 }
2098
2099 Ok(init_gas)
2100 }
2101
2102 fn catch_error(
2103 &self,
2104 evm: &mut Self::Evm,
2105 error: Self::Error,
2106 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
2107 evm.clear();
2108
2109 if evm.ctx.tx.is_subblock_transaction()
2111 && let Some(
2112 TempoInvalidTransaction::CollectFeePreTx(_)
2113 | TempoInvalidTransaction::FeeTokenPaused { .. }
2114 | TempoInvalidTransaction::EthInvalidTransaction(
2115 InvalidTransaction::LackOfFundForMaxFee { .. },
2116 ),
2117 ) = error.as_invalid_tx_err()
2118 {
2119 evm.ctx.journaled_state.commit_tx();
2123
2124 evm.ctx().local_mut().clear();
2125 evm.frame_stack().clear();
2126
2127 let total_spent = core::cmp::min(evm.ctx.tx.gas_limit, evm.ctx.cfg.tx_gas_limit_cap());
2129
2130 Ok(ExecutionResult::Halt {
2131 reason: TempoHaltReason::SubblockTxFeePayment,
2132 logs: Default::default(),
2133 gas: ResultGas::new_with_state_gas(total_spent, 0, 0, 0),
2134 })
2135 } else {
2136 MainnetHandler::default()
2137 .catch_error(evm, error)
2138 .map(|result| result.map_haltreason(Into::into))
2139 }
2140 }
2141}
2142
2143impl<DB, I> TempoEvmHandler<DB, I>
2144where
2145 DB: alloy_evm::Database,
2146{
2147 pub fn validate_transaction(
2151 &mut self,
2152 evm: &mut TempoEvm<DB, I>,
2153 ) -> Result<ValidationContext, EVMError<DB::Error, TempoInvalidTransaction>> {
2154 let mut init_and_floor_gas = self.validate(evm)?;
2155 self.pre_execution(evm, &mut init_and_floor_gas)?;
2156 let result = ValidationContext {
2157 fee_token: evm
2158 .fee_token
2159 .expect("set in `validate_against_state_and_deduct_caller`"),
2160 key_expiry: evm.key_expiry,
2161 };
2162 evm.clear();
2163 Ok(result)
2164 }
2165}
2166
2167#[derive(Debug, Clone)]
2170pub struct ValidationContext {
2171 pub fee_token: Address,
2173 pub key_expiry: Option<u64>,
2176}
2177
2178pub fn calculate_aa_batch_intrinsic_gas<'a>(
2195 aa_env: &TempoBatchCallEnv,
2196 gas_params: &GasParams,
2197 access_list: Option<impl Iterator<Item = &'a AccessListItem>>,
2198 spec: tempo_chainspec::hardfork::TempoHardfork,
2199) -> Result<InitialAndFloorGas, TempoInvalidTransaction> {
2200 let calls = &aa_env.aa_calls;
2201 let signature = &aa_env.signature;
2202 let authorization_list = &aa_env.tempo_authorization_list;
2203 let key_authorization = aa_env.key_authorization.as_ref();
2204 let mut gas = InitialAndFloorGas::default();
2205
2206 gas.initial_regular_gas += gas_params.tx_base_stipend();
2208
2209 gas.initial_regular_gas += tempo_signature_verification_gas(signature);
2211
2212 let cold_account_cost =
2213 gas_params.warm_storage_read_cost() + gas_params.cold_account_additional_cost();
2214
2215 gas.initial_regular_gas += cold_account_cost * calls.len().saturating_sub(1) as u64;
2218
2219 let num_auths = authorization_list.len() as u64;
2221 gas.initial_regular_gas +=
2222 num_auths * gas_params.get(GasId::tx_eip7702_per_empty_account_cost());
2223 gas.initial_state_gas += num_auths * gas_params.tx_eip7702_state_gas();
2225
2226 for auth in authorization_list {
2229 gas.initial_regular_gas += tempo_signature_verification_gas(auth.signature());
2230 if spec.is_t1() && auth.nonce == 0 {
2233 gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2234 gas.initial_state_gas += gas_params.new_account_state_gas();
2235 }
2236 }
2237
2238 if let Some(key_auth) = key_authorization {
2240 let (key_auth_regular_gas, key_auth_state_gas) =
2241 calculate_key_authorization_gas(key_auth, gas_params, spec);
2242 gas.initial_regular_gas += key_auth_regular_gas;
2243 gas.initial_state_gas += key_auth_state_gas;
2244 }
2245
2246 let mut total_tokens = 0u64;
2248
2249 for call in calls {
2250 let tokens = get_tokens_in_calldata_istanbul(&call.input);
2252 total_tokens += tokens;
2253
2254 if call.to.is_create() {
2256 gas.initial_regular_gas += gas_params.create_cost();
2258
2259 gas.initial_regular_gas += gas_params.tx_initcode_cost(call.input.len());
2261
2262 gas.initial_state_gas += gas_params.create_state_gas();
2264 }
2265
2266 if !call.value.is_zero() {
2269 return Err(TempoInvalidTransaction::ValueTransferNotAllowedInAATx);
2270 }
2271
2272 if !call.value.is_zero() && call.to.is_call() {
2275 gas.initial_regular_gas += gas_params.get(GasId::transfer_value_cost()); }
2277 }
2278
2279 gas.initial_regular_gas += total_tokens * gas_params.tx_token_cost();
2280
2281 if let Some(access_list) = access_list {
2283 let (accounts, storages) = access_list.fold((0, 0), |(acc_count, storage_count), item| {
2284 (acc_count + 1, storage_count + item.storage_slots().count())
2285 });
2286 gas.initial_regular_gas += accounts * gas_params.tx_access_list_address_cost(); gas.initial_regular_gas += storages as u64 * gas_params.tx_access_list_storage_key_cost(); }
2289
2290 gas.floor_gas = gas_params.tx_floor_cost_with_tokens(total_tokens); Ok(gas)
2294}
2295
2296fn validate_aa_initial_tx_gas<DB, I>(
2302 evm: &TempoEvm<DB, I>,
2303) -> Result<InitialAndFloorGas, EVMError<DB::Error, TempoInvalidTransaction>>
2304where
2305 DB: alloy_evm::Database,
2306{
2307 let (_, tx, cfg, _, _, _, _) = evm.ctx_ref().all();
2308 let gas_limit = tx.gas_limit();
2309 let gas_params = cfg.gas_params();
2310 let spec = *cfg.spec();
2311
2312 let aa_env = tx
2314 .tempo_tx_env
2315 .as_ref()
2316 .expect("validate_aa_initial_tx_gas called for non-AA transaction");
2317
2318 let calls = &aa_env.aa_calls;
2319
2320 let max_initcode_size = evm.ctx_ref().cfg().max_initcode_size();
2322 for call in calls {
2323 if call.to.is_create() && call.input.len() > max_initcode_size {
2324 return Err(InvalidTransaction::CreateInitCodeSizeLimit.into());
2325 }
2326 }
2327
2328 let mut batch_gas =
2330 calculate_aa_batch_intrinsic_gas(aa_env, gas_params, tx.access_list(), spec)?;
2331
2332 let mut nonce_2d_gas = 0;
2333
2334 if spec.is_t1() {
2337 if aa_env.nonce_key == TEMPO_EXPIRING_NONCE_KEY {
2338 batch_gas.initial_regular_gas += EXPIRING_NONCE_GAS;
2343 } else if tx.nonce == 0 {
2344 batch_gas.initial_regular_gas += gas_params.get(GasId::new_account_cost());
2347 batch_gas.initial_state_gas += gas_params.new_account_state_gas();
2348 } else if !aa_env.nonce_key.is_zero() {
2349 batch_gas.initial_regular_gas += spec.gas_existing_nonce_key();
2352 }
2353 } else if let Some(aa_env) = &tx.tempo_tx_env
2354 && !aa_env.nonce_key.is_zero()
2355 {
2356 nonce_2d_gas = if tx.nonce() == 0 {
2357 spec.gas_new_nonce_key()
2358 } else {
2359 spec.gas_existing_nonce_key()
2360 };
2361 };
2362
2363 if spec.is_t0() {
2368 batch_gas.initial_regular_gas += nonce_2d_gas;
2369 }
2370
2371 if gas_limit < batch_gas.initial_total_gas() {
2375 return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
2376 gas_limit,
2377 initial_gas: batch_gas.initial_total_gas(),
2378 }
2379 .into());
2380 }
2381
2382 if !spec.is_t0() {
2385 batch_gas.initial_regular_gas += nonce_2d_gas;
2386 }
2387
2388 Ok(batch_gas)
2389}
2390
2391pub fn get_token_balance<JOURNAL>(
2393 journal: &mut JOURNAL,
2394 token: Address,
2395 sender: Address,
2396) -> Result<U256, <JOURNAL::Database as Database>::Error>
2397where
2398 JOURNAL: JournalTr,
2399{
2400 journal.load_account(token)?;
2402 let balance_slot = TIP20Token::from_address(token)
2403 .expect("TIP20 prefix already validated")
2404 .balances[sender]
2405 .slot();
2406 let balance = journal.sload(token, balance_slot)?.data;
2407
2408 Ok(balance)
2409}
2410
2411impl<DB, I> InspectorHandler for TempoEvmHandler<DB, I>
2412where
2413 DB: alloy_evm::Database,
2414 I: Inspector<TempoContext<DB>>,
2415{
2416 type IT = EthInterpreter;
2417
2418 #[inline]
2420 fn inspect_execution(
2421 &mut self,
2422 evm: &mut Self::Evm,
2423 init_and_floor_gas: &InitialAndFloorGas,
2424 ) -> Result<FrameResult, Self::Error> {
2425 let spec = evm.ctx_ref().cfg().spec();
2426 let tx = evm.tx();
2427
2428 if let Some(oog) = check_gas_limit(*spec, tx, init_and_floor_gas) {
2429 return Ok(oog);
2430 }
2431
2432 let (gas_limit, reservoir) = evm.initial_gas_and_reservoir(init_and_floor_gas);
2433
2434 if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
2435 let calls = tempo_tx_env.aa_calls.clone();
2436 self.inspect_execute_multi_call(evm, gas_limit, reservoir, calls)
2437 } else {
2438 self.inspect_execute_single_call(evm, gas_limit, reservoir)
2439 }
2440 }
2441}
2442
2443#[inline]
2447fn oog_frame_result(kind: TxKind, gas_limit: u64) -> FrameResult {
2448 if kind.is_call() {
2449 FrameResult::new_call_oog(gas_limit, 0..0, 0)
2450 } else {
2451 FrameResult::new_create_oog(gas_limit, 0)
2452 }
2453}
2454
2455#[inline]
2460fn check_gas_limit(
2461 spec: tempo_chainspec::hardfork::TempoHardfork,
2462 tx: &TempoTxEnv,
2463 adjusted_gas: &InitialAndFloorGas,
2464) -> Option<FrameResult> {
2465 if spec.is_t0() && tx.gas_limit() < adjusted_gas.initial_total_gas() {
2466 let kind = *tx
2467 .first_call()
2468 .expect("we already checked that there is at least one call in aa tx")
2469 .0;
2470 return Some(oog_frame_result(kind, tx.gas_limit()));
2471 }
2472 None
2473}
2474
2475pub fn validate_time_window(
2483 valid_after: Option<u64>,
2484 valid_before: Option<u64>,
2485 block_timestamp: u64,
2486) -> Result<(), TempoInvalidTransaction> {
2487 if let Some(after) = valid_after
2489 && block_timestamp < after
2490 {
2491 return Err(TempoInvalidTransaction::ValidAfter {
2492 current: block_timestamp,
2493 valid_after: after,
2494 });
2495 }
2496
2497 if let Some(before) = valid_before
2500 && block_timestamp >= before
2501 {
2502 return Err(TempoInvalidTransaction::ValidBefore {
2503 current: block_timestamp,
2504 valid_before: before,
2505 });
2506 }
2507
2508 Ok(())
2509}
2510
2511#[cfg(test)]
2512mod tests {
2513 use super::*;
2514 use crate::{
2515 TempoBlockEnv, TempoTxEnv, evm::TempoEvm, gas_params::tempo_gas_params,
2516 tx::TempoBatchCallEnv,
2517 };
2518 use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
2519 use proptest::prelude::*;
2520 use revm::{
2521 Context, Journal, MainContext,
2522 context::CfgEnv,
2523 database::{CacheDB, EmptyDB},
2524 handler::Handler,
2525 interpreter::{
2526 InstructionResult, InterpreterResult, gas::COLD_ACCOUNT_ACCESS_COST,
2527 instructions::utility::IntoU256,
2528 },
2529 primitives::hardfork::SpecId,
2530 };
2531 use tempo_chainspec::hardfork::TempoHardfork;
2532 use tempo_contracts::precompiles::DEFAULT_FEE_TOKEN;
2533 use tempo_precompiles::{
2534 PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS, storage::ContractStorage, test_util::TIP20Setup,
2535 };
2536 use tempo_primitives::transaction::{
2537 Call, RecoveredTempoAuthorization, TempoSignature, TempoSignedAuthorization,
2538 tt_signature::{P256SignatureWithPreHash, WebAuthnSignature},
2539 };
2540
2541 fn create_test_journal() -> Journal<CacheDB<EmptyDB>> {
2542 let db = CacheDB::new(EmptyDB::default());
2543 Journal::new(db)
2544 }
2545
2546 type TestHandlerEvmResult<T> =
2547 Result<T, EVMError<<CacheDB<EmptyDB> as revm::Database>::Error, TempoInvalidTransaction>>;
2548
2549 struct TestHandlerEvm {
2550 evm: TempoEvm<CacheDB<EmptyDB>, ()>,
2551 handler: TempoEvmHandler<CacheDB<EmptyDB>, ()>,
2552 }
2553
2554 impl TestHandlerEvm {
2555 fn tx(spec: TempoHardfork, configure_tx_env: impl FnOnce(&mut TempoTxEnv)) -> Self {
2556 let mut tx_env = TempoTxEnv::default();
2557 configure_tx_env(&mut tx_env);
2558 Self::new(spec, tx_env)
2559 }
2560
2561 fn aa(
2562 spec: TempoHardfork,
2563 aa_env: TempoBatchCallEnv,
2564 configure_tx_env: impl FnOnce(&mut TempoTxEnv),
2565 ) -> Self {
2566 let mut tx_env = TempoTxEnv {
2567 tempo_tx_env: Some(Box::new(aa_env)),
2568 ..Default::default()
2569 };
2570 configure_tx_env(&mut tx_env);
2571 Self::new(spec, tx_env)
2572 }
2573
2574 fn new(spec: TempoHardfork, tx_env: TempoTxEnv) -> Self {
2575 Self::with_cfg(spec, tx_env, |_| {})
2576 }
2577
2578 fn with_cfg(
2579 spec: TempoHardfork,
2580 tx_env: TempoTxEnv,
2581 configure: impl FnOnce(&mut CfgEnv<TempoHardfork>),
2582 ) -> Self {
2583 let mut cfg = CfgEnv::<TempoHardfork>::default();
2584 cfg.spec = spec;
2585 cfg.gas_params = tempo_gas_params(spec);
2586 configure(&mut cfg);
2587
2588 let ctx = Context::mainnet()
2589 .with_db(CacheDB::new(EmptyDB::default()))
2590 .with_block(TempoBlockEnv::default())
2591 .with_cfg(cfg)
2592 .with_tx(tx_env)
2593 .with_new_journal(create_test_journal());
2594
2595 Self {
2596 evm: TempoEvm::new(ctx, ()),
2597 handler: TempoEvmHandler::new(),
2598 }
2599 }
2600
2601 fn cfg(&mut self) -> &CfgEnv<TempoHardfork> {
2602 &self.evm.ctx().cfg
2603 }
2604
2605 fn gas_params(&mut self) -> &GasParams {
2606 &self.cfg().gas_params
2607 }
2608
2609 fn validate_env(&mut self) -> TestHandlerEvmResult<()> {
2610 self.handler.validate_env(&mut self.evm)
2611 }
2612
2613 fn validate_initial_tx_gas(&mut self) -> InitialAndFloorGas {
2614 self.handler
2615 .validate_initial_tx_gas(&mut self.evm)
2616 .expect("initial gas validation should succeed")
2617 }
2618
2619 fn validate_against_state_and_deduct_caller(&mut self) -> TestHandlerEvmResult<()> {
2620 self.handler
2621 .validate_against_state_and_deduct_caller(&mut self.evm, &mut Default::default())
2622 }
2623
2624 fn execute(&mut self, init_gas: &InitialAndFloorGas) -> FrameResult {
2625 self.handler
2626 .execution(&mut self.evm, init_gas)
2627 .expect("execution should return a frame result")
2628 }
2629 }
2630
2631 #[test]
2632 fn test_invalid_fee_token_rejected() {
2633 let invalid_token = Address::random(); assert!(
2638 !invalid_token.is_tip20(),
2639 "Test requires a non-TIP20 address"
2640 );
2641
2642 let mut test = TestHandlerEvm::tx(TempoHardfork::default(), |tx_env| {
2643 tx_env.fee_token = Some(invalid_token);
2644 });
2645
2646 let result = test.validate_against_state_and_deduct_caller();
2647
2648 assert!(
2649 matches!(
2650 result,
2651 Err(EVMError::Transaction(TempoInvalidTransaction::FeeTokenNotTip20 { address })) if address == invalid_token
2652 ),
2653 "Should reject non-TIP20 fee token with FeeTokenNotTip20 error"
2654 );
2655 }
2656
2657 #[test]
2658 fn test_non_usd_fee_token_rejected() {
2659 let admin = Address::random();
2660 let mut test = TestHandlerEvm::tx(TempoHardfork::default(), |tx_env| {
2661 tx_env.inner.gas_limit = 100_000;
2662 tx_env.inner.gas_price = 1_000_000_000;
2663 tx_env.inner.gas_priority_fee = Some(1_000_000_000);
2664 });
2665
2666 let fee_token =
2667 StorageCtx::enter_ctx(&mut test.evm.inner.ctx, StorageActions::disabled(), || {
2668 TIP20Setup::create("Euro", "EUR", admin)
2669 .currency("EUR")
2670 .apply()
2671 .map(|token| token.address())
2672 })
2673 .expect("EUR token setup succeeds");
2674
2675 test.evm.inner.ctx.tx.fee_token = Some(fee_token);
2676
2677 let result = test.validate_against_state_and_deduct_caller();
2678
2679 assert!(
2680 matches!(
2681 result,
2682 Err(EVMError::Transaction(TempoInvalidTransaction::FeeTokenNotUsdCurrency {
2683 address,
2684 currency,
2685 })) if address == fee_token && currency == "EUR"
2686 ),
2687 "Should reject non-USD fee token with FeeTokenNotUsdCurrency error"
2688 );
2689 }
2690
2691 #[test]
2692 fn test_paused_fee_token_rejected() {
2693 let admin = Address::random();
2694 let fee_payer = Address::random();
2695 let fee = U256::from(100_000_000_000_000_u64);
2696 let mut test = TestHandlerEvm::tx(TempoHardfork::default(), |tx_env| {
2697 tx_env.inner.caller = fee_payer;
2698 tx_env.inner.gas_limit = 100_000;
2699 tx_env.inner.gas_price = 1_000_000_000;
2700 tx_env.inner.gas_priority_fee = Some(1_000_000_000);
2701 });
2702
2703 let fee_token =
2704 StorageCtx::enter_ctx(&mut test.evm.inner.ctx, StorageActions::disabled(), || {
2705 let mut token = TIP20Setup::create("Paused USD", "PUSD", admin)
2706 .with_issuer(admin)
2707 .with_role(admin, *tempo_precompiles::tip20::PAUSE_ROLE)
2708 .with_mint(fee_payer, fee)
2709 .apply()?;
2710 token.pause(admin, tempo_precompiles::tip20::ITIP20::pauseCall {})?;
2711 Ok::<_, TempoPrecompileError>(token.address())
2712 })
2713 .expect("paused USD token setup succeeds");
2714
2715 test.evm.inner.ctx.tx.fee_token = Some(fee_token);
2716
2717 let result = test.validate_against_state_and_deduct_caller();
2718
2719 assert!(
2720 matches!(
2721 result,
2722 Err(EVMError::Transaction(TempoInvalidTransaction::FeeTokenPaused { address })) if address == fee_token
2723 ),
2724 "Should reject paused fee token with FeeTokenPaused error"
2725 );
2726 }
2727
2728 #[test]
2729 fn test_self_sponsored_fee_payer_rejected_post_t2() {
2730 let caller = Address::random();
2731 let invalid_token = Address::random();
2732
2733 let mut test = TestHandlerEvm::tx(TempoHardfork::T2, |tx_env| {
2734 tx_env.inner.caller = caller;
2735 tx_env.fee_token = Some(invalid_token);
2736 tx_env.fee_payer = Some(Some(caller));
2737 });
2738
2739 let result = test.validate_env();
2740 assert!(matches!(
2741 result,
2742 Err(EVMError::Transaction(
2743 TempoInvalidTransaction::SelfSponsoredFeePayer
2744 ))
2745 ));
2746 }
2747
2748 #[test]
2749 fn test_self_sponsored_fee_payer_not_rejected_pre_t4() {
2750 let caller = Address::random();
2751 let invalid_token = Address::random();
2752
2753 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
2754 let mut cfg = CfgEnv::<TempoHardfork>::default();
2755 cfg.spec = TempoHardfork::T1C;
2756
2757 let tx_env = TempoTxEnv {
2758 inner: revm::context::TxEnv {
2759 caller,
2760 ..Default::default()
2761 },
2762 fee_token: Some(invalid_token),
2763 fee_payer: Some(Some(caller)),
2764 ..Default::default()
2765 };
2766
2767 let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
2768 Context::mainnet()
2769 .with_db(CacheDB::new(EmptyDB::default()))
2770 .with_block(TempoBlockEnv::default())
2771 .with_cfg(cfg)
2772 .with_tx(tx_env),
2773 (),
2774 );
2775
2776 let result = handler.validate_env(&mut evm);
2777 assert!(result.is_ok());
2778 }
2779
2780 #[test]
2781 fn test_get_token_balance() -> eyre::Result<()> {
2782 let mut journal = create_test_journal();
2783 let token = PATH_USD_ADDRESS;
2785 let account = Address::random();
2786 let expected_balance = U256::random();
2787
2788 let balance_slot = TIP20Token::from_address(token)?.balances[account].slot();
2790 journal.load_account(token)?;
2791 journal
2792 .sstore(token, balance_slot, expected_balance)
2793 .unwrap();
2794
2795 let balance = get_token_balance(&mut journal, token, account)?;
2796 assert_eq!(balance, expected_balance);
2797
2798 Ok(())
2799 }
2800
2801 #[test]
2802 fn test_get_fee_token() -> eyre::Result<()> {
2803 let journal = create_test_journal();
2804 let mut ctx: TempoContext<_> = Context::mainnet()
2805 .with_db(CacheDB::new(EmptyDB::default()))
2806 .with_block(TempoBlockEnv::default())
2807 .with_cfg(Default::default())
2808 .with_tx(TempoTxEnv::default())
2809 .with_new_journal(journal);
2810 let user = Address::random();
2811 ctx.tx.inner.caller = user;
2812 let validator = Address::random();
2813 ctx.block.beneficiary = validator;
2814 let user_fee_token = Address::random();
2815 let validator_fee_token = Address::random();
2816 let tx_fee_token = Address::random();
2817
2818 let validator_slot = TipFeeManager::new().validator_tokens[validator].slot();
2820 ctx.journaled_state.load_account(TIP_FEE_MANAGER_ADDRESS)?;
2821 ctx.journaled_state
2822 .sstore(
2823 TIP_FEE_MANAGER_ADDRESS,
2824 validator_slot,
2825 validator_fee_token.into_u256(),
2826 )
2827 .unwrap();
2828
2829 {
2830 let fee_token = ctx.journaled_state.get_fee_token(
2831 &ctx.tx,
2832 user,
2833 ctx.cfg.spec,
2834 tempo_precompiles::storage::StorageActions::disabled(),
2835 )?;
2836 assert_eq!(DEFAULT_FEE_TOKEN, fee_token);
2837 }
2838
2839 let user_slot = TipFeeManager::new().user_tokens[user].slot();
2841 ctx.journaled_state
2842 .sstore(
2843 TIP_FEE_MANAGER_ADDRESS,
2844 user_slot,
2845 user_fee_token.into_u256(),
2846 )
2847 .unwrap();
2848
2849 {
2850 let fee_token = ctx.journaled_state.get_fee_token(
2851 &ctx.tx,
2852 user,
2853 ctx.cfg.spec,
2854 tempo_precompiles::storage::StorageActions::disabled(),
2855 )?;
2856 assert_eq!(user_fee_token, fee_token);
2857 }
2858
2859 ctx.tx.fee_token = Some(tx_fee_token);
2861 let fee_token = ctx.journaled_state.get_fee_token(
2862 &ctx.tx,
2863 user,
2864 ctx.cfg.spec,
2865 tempo_precompiles::storage::StorageActions::disabled(),
2866 )?;
2867 assert_eq!(tx_fee_token, fee_token);
2868
2869 Ok(())
2870 }
2871
2872 #[test]
2873 fn test_aa_gas_single_call_vs_normal_tx() {
2874 use crate::TempoBatchCallEnv;
2875 use alloy_primitives::{Bytes, TxKind};
2876 use revm::interpreter::gas::calculate_initial_tx_gas;
2877 use tempo_primitives::transaction::{Call, TempoSignature};
2878 let gas_params = GasParams::default();
2879
2880 let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); let to = Address::random();
2883
2884 let call = Call {
2886 to: TxKind::Call(to),
2887 value: U256::ZERO,
2888 input: calldata.clone(),
2889 };
2890
2891 let aa_env = TempoBatchCallEnv {
2892 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2893 alloy_primitives::Signature::test_signature(),
2894 )), aa_calls: vec![call],
2896 key_authorization: None,
2897 signature_hash: B256::ZERO,
2898 ..Default::default()
2899 };
2900
2901 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2903 let aa_gas = calculate_aa_batch_intrinsic_gas(
2904 &aa_env,
2905 &gas_params,
2906 None::<std::iter::Empty<&AccessListItem>>, spec,
2908 )
2909 .unwrap();
2910
2911 let normal_tx_gas = calculate_initial_tx_gas(
2913 spec.into(),
2914 &calldata,
2915 false, 0, 0, 0, );
2920
2921 assert_eq!(
2923 aa_gas.initial_total_gas(),
2924 normal_tx_gas.initial_total_gas()
2925 );
2926 }
2927
2928 #[test]
2929 fn test_aa_gas_multiple_calls_overhead() {
2930 use crate::TempoBatchCallEnv;
2931 use alloy_primitives::{Bytes, TxKind};
2932 use revm::interpreter::gas::calculate_initial_tx_gas;
2933 use tempo_primitives::transaction::{Call, TempoSignature};
2934
2935 let calldata = Bytes::from(vec![1, 2, 3]); let calls = vec![
2938 Call {
2939 to: TxKind::Call(Address::random()),
2940 value: U256::ZERO,
2941 input: calldata.clone(),
2942 },
2943 Call {
2944 to: TxKind::Call(Address::random()),
2945 value: U256::ZERO,
2946 input: calldata.clone(),
2947 },
2948 Call {
2949 to: TxKind::Call(Address::random()),
2950 value: U256::ZERO,
2951 input: calldata.clone(),
2952 },
2953 ];
2954
2955 let aa_env = TempoBatchCallEnv {
2956 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2957 alloy_primitives::Signature::test_signature(),
2958 )),
2959 aa_calls: calls,
2960 key_authorization: None,
2961 signature_hash: B256::ZERO,
2962 ..Default::default()
2963 };
2964
2965 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2966 let gas = calculate_aa_batch_intrinsic_gas(
2967 &aa_env,
2968 &GasParams::default(),
2969 None::<std::iter::Empty<&AccessListItem>>,
2970 spec,
2971 )
2972 .unwrap();
2973
2974 let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
2976
2977 let expected = base_tx_gas.initial_total_gas()
2980 + 2 * (calldata.len() as u64 * 16)
2981 + 2 * COLD_ACCOUNT_ACCESS_COST;
2982 assert_eq!(gas.initial_total_gas(), expected,);
2984 }
2985
2986 #[test]
2987 fn test_aa_gas_p256_signature() {
2988 use crate::TempoBatchCallEnv;
2989 use alloy_primitives::{B256, Bytes, TxKind};
2990 use revm::interpreter::gas::calculate_initial_tx_gas;
2991 use tempo_primitives::transaction::{
2992 Call, TempoSignature, tt_signature::P256SignatureWithPreHash,
2993 };
2994
2995 let spec = SpecId::CANCUN;
2996 let calldata = Bytes::from(vec![1, 2]);
2997
2998 let call = Call {
2999 to: TxKind::Call(Address::random()),
3000 value: U256::ZERO,
3001 input: calldata.clone(),
3002 };
3003
3004 let aa_env = TempoBatchCallEnv {
3005 signature: TempoSignature::Primitive(PrimitiveSignature::P256(
3006 P256SignatureWithPreHash {
3007 r: B256::ZERO,
3008 s: B256::ZERO,
3009 pub_key_x: B256::ZERO,
3010 pub_key_y: B256::ZERO,
3011 pre_hash: false,
3012 },
3013 )),
3014 aa_calls: vec![call],
3015 key_authorization: None,
3016 signature_hash: B256::ZERO,
3017 ..Default::default()
3018 };
3019
3020 let gas = calculate_aa_batch_intrinsic_gas(
3021 &aa_env,
3022 &GasParams::default(),
3023 None::<std::iter::Empty<&AccessListItem>>,
3024 tempo_chainspec::hardfork::TempoHardfork::default(),
3025 )
3026 .unwrap();
3027
3028 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
3030
3031 let expected = base_gas.initial_total_gas() + P256_VERIFY_GAS;
3033 assert_eq!(gas.initial_total_gas(), expected,);
3034 }
3035
3036 #[test]
3037 fn test_aa_gas_create_call() {
3038 use crate::TempoBatchCallEnv;
3039 use alloy_primitives::{Bytes, TxKind};
3040 use revm::interpreter::gas::calculate_initial_tx_gas;
3041 use tempo_primitives::transaction::{Call, TempoSignature};
3042
3043 let spec = SpecId::CANCUN; let initcode = Bytes::from(vec![0x60, 0x80]); let call = Call {
3047 to: TxKind::Create,
3048 value: U256::ZERO,
3049 input: initcode.clone(),
3050 };
3051
3052 let aa_env = TempoBatchCallEnv {
3053 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3054 alloy_primitives::Signature::test_signature(),
3055 )),
3056 aa_calls: vec![call],
3057 key_authorization: None,
3058 signature_hash: B256::ZERO,
3059 ..Default::default()
3060 };
3061
3062 let gas = calculate_aa_batch_intrinsic_gas(
3063 &aa_env,
3064 &GasParams::default(),
3065 None::<std::iter::Empty<&AccessListItem>>,
3066 tempo_chainspec::hardfork::TempoHardfork::default(),
3067 )
3068 .unwrap();
3069
3070 let base_gas = calculate_initial_tx_gas(
3072 spec, &initcode, true, 0, 0, 0,
3074 );
3075
3076 assert_eq!(gas.initial_total_gas(), base_gas.initial_total_gas(),);
3078 }
3079
3080 #[test]
3081 fn test_aa_gas_value_transfer() {
3082 use crate::TempoBatchCallEnv;
3083 use alloy_primitives::{Bytes, TxKind};
3084 use tempo_primitives::transaction::{Call, TempoSignature};
3085
3086 let calldata = Bytes::from(vec![1]);
3087
3088 let call = Call {
3089 to: TxKind::Call(Address::random()),
3090 value: U256::from(1000), input: calldata,
3092 };
3093
3094 let aa_env = TempoBatchCallEnv {
3095 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3096 alloy_primitives::Signature::test_signature(),
3097 )),
3098 aa_calls: vec![call],
3099 key_authorization: None,
3100 signature_hash: B256::ZERO,
3101 ..Default::default()
3102 };
3103
3104 let res = calculate_aa_batch_intrinsic_gas(
3105 &aa_env,
3106 &GasParams::default(),
3107 None::<std::iter::Empty<&AccessListItem>>,
3108 tempo_chainspec::hardfork::TempoHardfork::default(),
3109 );
3110
3111 assert_eq!(
3112 res.unwrap_err(),
3113 TempoInvalidTransaction::ValueTransferNotAllowedInAATx
3114 );
3115 }
3116
3117 #[test]
3118 fn test_aa_gas_access_list() {
3119 use crate::TempoBatchCallEnv;
3120 use alloy_primitives::{Bytes, TxKind};
3121 use revm::interpreter::gas::calculate_initial_tx_gas;
3122 use tempo_primitives::transaction::{Call, TempoSignature};
3123
3124 let spec = SpecId::CANCUN;
3125 let calldata = Bytes::from(vec![]);
3126
3127 let call = Call {
3128 to: TxKind::Call(Address::random()),
3129 value: U256::ZERO,
3130 input: calldata.clone(),
3131 };
3132
3133 let aa_env = TempoBatchCallEnv {
3134 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3135 alloy_primitives::Signature::test_signature(),
3136 )),
3137 aa_calls: vec![call],
3138 key_authorization: None,
3139 signature_hash: B256::ZERO,
3140 ..Default::default()
3141 };
3142
3143 let gas = calculate_aa_batch_intrinsic_gas(
3145 &aa_env,
3146 &GasParams::default(),
3147 None::<std::iter::Empty<&AccessListItem>>,
3148 tempo_chainspec::hardfork::TempoHardfork::default(),
3149 )
3150 .unwrap();
3151
3152 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
3154
3155 assert_eq!(gas.initial_total_gas(), base_gas.initial_total_gas(),);
3157 }
3158
3159 #[test]
3160 fn test_key_authorization_rlp_encoding() {
3161 use alloy_primitives::{Address, U256};
3162 use tempo_primitives::transaction::{
3163 SignatureType, TokenLimit, key_authorization::KeyAuthorization,
3164 };
3165
3166 let chain_id = 1u64;
3168 let key_type = SignatureType::Secp256k1;
3169 let key_id = Address::random();
3170 let expiry = 1000u64;
3171 let limits = vec![
3172 TokenLimit {
3173 token: Address::random(),
3174 limit: U256::from(100),
3175 period: 0,
3176 },
3177 TokenLimit {
3178 token: Address::random(),
3179 limit: U256::from(200),
3180 period: 0,
3181 },
3182 ];
3183
3184 let hash1 = KeyAuthorization::unrestricted(chain_id, key_type, key_id)
3186 .with_expiry(expiry)
3187 .with_limits(limits.clone())
3188 .signature_hash();
3189
3190 let hash2 = KeyAuthorization::unrestricted(chain_id, key_type, key_id)
3192 .with_expiry(expiry)
3193 .with_limits(limits.clone())
3194 .signature_hash();
3195
3196 assert_eq!(hash1, hash2, "Hash computation should be deterministic");
3197
3198 let hash3 = KeyAuthorization::unrestricted(2, key_type, key_id)
3200 .with_expiry(expiry)
3201 .with_limits(limits)
3202 .signature_hash();
3203 assert_ne!(
3204 hash1, hash3,
3205 "Different chain_id should produce different hash"
3206 );
3207 }
3208
3209 #[test]
3210 fn test_aa_gas_floor_gas_prague() {
3211 use crate::TempoBatchCallEnv;
3212 use alloy_primitives::{Bytes, TxKind};
3213 use revm::interpreter::gas::calculate_initial_tx_gas;
3214 use tempo_primitives::transaction::{Call, TempoSignature};
3215
3216 let spec = SpecId::PRAGUE;
3217 let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); let call = Call {
3220 to: TxKind::Call(Address::random()),
3221 value: U256::ZERO,
3222 input: calldata.clone(),
3223 };
3224
3225 let aa_env = TempoBatchCallEnv {
3226 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3227 alloy_primitives::Signature::test_signature(),
3228 )),
3229 aa_calls: vec![call],
3230 key_authorization: None,
3231 signature_hash: B256::ZERO,
3232 ..Default::default()
3233 };
3234
3235 let gas = calculate_aa_batch_intrinsic_gas(
3236 &aa_env,
3237 &GasParams::default(),
3238 None::<std::iter::Empty<&AccessListItem>>,
3239 tempo_chainspec::hardfork::TempoHardfork::default(),
3240 )
3241 .unwrap();
3242
3243 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
3245
3246 assert_eq!(
3248 gas.floor_gas, base_gas.floor_gas,
3249 "Should calculate floor gas for Prague matching revm"
3250 );
3251 }
3252
3253 #[test]
3256 fn test_zero_value_transfer() -> eyre::Result<()> {
3257 use crate::TempoEvm;
3258
3259 let ctx = Context::mainnet()
3261 .with_db(CacheDB::new(EmptyDB::default()))
3262 .with_block(Default::default())
3263 .with_cfg(Default::default())
3264 .with_tx(TempoTxEnv::default());
3265 let mut evm = TempoEvm::new(ctx, ());
3266
3267 evm.ctx.tx.inner.value = U256::from(1000);
3269
3270 let handler = TempoEvmHandler::<_, ()>::new();
3272
3273 let result = handler.validate_env(&mut evm);
3275
3276 if let Err(EVMError::Transaction(err)) = result {
3277 assert_eq!(err, TempoInvalidTransaction::ValueTransferNotAllowed);
3278 } else {
3279 panic!("Expected ValueTransferNotAllowed error");
3280 }
3281
3282 Ok(())
3283 }
3284
3285 #[test]
3286 fn test_key_authorization_gas_with_limits() {
3287 use tempo_primitives::transaction::{
3288 KeyAuthorization, SignatureType, SignedKeyAuthorization, TokenLimit,
3289 };
3290
3291 let create_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
3293 let mut auth =
3294 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random());
3295 if num_limits > 0 {
3296 auth = auth.with_limits(
3297 (0..num_limits)
3298 .map(|_| TokenLimit {
3299 token: Address::random(),
3300 limit: U256::from(1000),
3301 period: 0,
3302 })
3303 .collect(),
3304 );
3305 }
3306 auth.into_signed(PrimitiveSignature::Secp256k1(
3307 alloy_primitives::Signature::test_signature(),
3308 ))
3309 };
3310
3311 let (gas_0, state_0) = calculate_key_authorization_gas(
3313 &create_key_auth(0),
3314 &GasParams::default(),
3315 tempo_chainspec::hardfork::TempoHardfork::default(),
3316 );
3317 assert_eq!(
3318 gas_0,
3319 KEY_AUTH_BASE_GAS + ECRECOVER_GAS,
3320 "0 limits should be 30,000"
3321 );
3322 assert_eq!(state_0, 0, "pre-T1B has no state gas");
3323
3324 let (gas_1, state_1) = calculate_key_authorization_gas(
3326 &create_key_auth(1),
3327 &GasParams::default(),
3328 tempo_chainspec::hardfork::TempoHardfork::default(),
3329 );
3330 assert_eq!(
3331 gas_1,
3332 KEY_AUTH_BASE_GAS + ECRECOVER_GAS + KEY_AUTH_PER_LIMIT_GAS,
3333 "1 limit should be 52,000"
3334 );
3335 assert_eq!(state_1, 0, "pre-T1B has no state gas");
3336
3337 let (gas_2, _) = calculate_key_authorization_gas(
3339 &create_key_auth(2),
3340 &GasParams::default(),
3341 tempo_chainspec::hardfork::TempoHardfork::default(),
3342 );
3343 assert_eq!(
3344 gas_2,
3345 KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS,
3346 "2 limits should be 74,000"
3347 );
3348
3349 let (gas_3, _) = calculate_key_authorization_gas(
3351 &create_key_auth(3),
3352 &GasParams::default(),
3353 tempo_chainspec::hardfork::TempoHardfork::default(),
3354 );
3355 assert_eq!(
3356 gas_3,
3357 KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 3 * KEY_AUTH_PER_LIMIT_GAS,
3358 "3 limits should be 96,000"
3359 );
3360
3361 let t1b_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
3363 let sstore =
3364 t1b_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3365 let sload =
3366 t1b_gas_params.warm_storage_read_cost() + t1b_gas_params.cold_storage_additional_cost();
3367 const BUFFER: u64 = 2_000;
3368
3369 for num_limits in 0..=3 {
3370 let (gas, state_gas) = calculate_key_authorization_gas(
3371 &create_key_auth(num_limits),
3372 &t1b_gas_params,
3373 TempoHardfork::T1B,
3374 );
3375 let expected = ECRECOVER_GAS + sload + sstore * (1 + num_limits as u64) + BUFFER;
3376 assert_eq!(gas, expected, "T1B with {num_limits} limits");
3377 assert_eq!(state_gas, 0, "T1B has no state gas");
3378 }
3379
3380 let t3_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T3);
3381 let t3_sstore =
3382 t3_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3383 let t3_sload =
3384 t3_gas_params.warm_storage_read_cost() + t3_gas_params.cold_storage_additional_cost();
3385
3386 for num_limits in 0..=3 {
3387 let num_sstores = 1 + 2 * num_limits as u64;
3388 let (gas, state_gas) = calculate_key_authorization_gas(
3389 &create_key_auth(num_limits),
3390 &t3_gas_params,
3391 TempoHardfork::T3,
3392 );
3393 let expected = ECRECOVER_GAS + t3_sload + t3_sstore * num_sstores + BUFFER;
3394 assert_eq!(gas, expected, "T3 with {num_limits} limits");
3395 assert_eq!(state_gas, 0, "T3 has no state gas");
3396 }
3397
3398 let t4_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T4);
3400 let t4_sstore =
3401 t4_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3402 let t4_sload =
3403 t4_gas_params.warm_storage_read_cost() + t4_gas_params.cold_storage_additional_cost();
3404 let t4_sstore_state =
3405 t4_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_state_gas());
3406
3407 for num_limits in 0..=3 {
3408 let num_sstores = 1 + 2 * num_limits as u64;
3409 let (gas, state_gas) = calculate_key_authorization_gas(
3410 &create_key_auth(num_limits),
3411 &t4_gas_params,
3412 TempoHardfork::T4,
3413 );
3414 let expected_state = t4_sstore_state * num_sstores;
3415 let expected = ECRECOVER_GAS
3416 + t4_sload
3417 + t4_sstore * num_sstores
3418 + BUFFER
3419 + 5_000
3420 + expected_state;
3421 assert_eq!(gas, expected, "T4 with {num_limits} limits");
3422 assert_eq!(
3423 state_gas, expected_state,
3424 "T4 state gas with {num_limits} limits"
3425 );
3426 }
3427
3428 let t5_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T5);
3429 let t5_sload =
3430 t5_gas_params.warm_storage_read_cost() + t5_gas_params.cold_storage_additional_cost();
3431 let base_t5_key_auth = create_key_auth(0);
3432 let mut witness_t5_key_auth = create_key_auth(0);
3433 witness_t5_key_auth.authorization = witness_t5_key_auth
3434 .authorization
3435 .with_witness(B256::repeat_byte(0x53));
3436
3437 let (base_t5_gas, base_t5_state_gas) =
3438 calculate_key_authorization_gas(&base_t5_key_auth, &t5_gas_params, TempoHardfork::T5);
3439 let (witness_t5_gas, witness_t5_state_gas) = calculate_key_authorization_gas(
3440 &witness_t5_key_auth,
3441 &t5_gas_params,
3442 TempoHardfork::T5,
3443 );
3444
3445 assert_eq!(
3446 witness_t5_gas - base_t5_gas,
3447 t5_sload + KEY_AUTH_EXTRA_EVENT_BUFFER,
3448 "T5 witness adds one burned-witness SLOAD and one event"
3449 );
3450 assert_eq!(
3451 witness_t5_state_gas - base_t5_state_gas,
3452 0,
3453 "T5 witness authorization does not add state gas"
3454 );
3455
3456 let t6_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T6);
3457 let base_t6_key_auth = create_key_auth(0);
3458 let mut account_bound_t6_key_auth = create_key_auth(0);
3459 account_bound_t6_key_auth.authorization = account_bound_t6_key_auth
3460 .authorization
3461 .with_account(Address::random());
3462 let mut admin_t6_key_auth = create_key_auth(0);
3463 admin_t6_key_auth.authorization = admin_t6_key_auth
3464 .authorization
3465 .into_admin(Address::random());
3466 let mut unbound_admin_t6_key_auth = create_key_auth(0);
3467 unbound_admin_t6_key_auth.authorization.is_admin = true;
3468
3469 let (base_t6_gas, base_t6_state_gas) =
3470 calculate_key_authorization_gas(&base_t6_key_auth, &t6_gas_params, TempoHardfork::T6);
3471 let (account_bound_t6_gas, account_bound_t6_state_gas) = calculate_key_authorization_gas(
3472 &account_bound_t6_key_auth,
3473 &t6_gas_params,
3474 TempoHardfork::T6,
3475 );
3476 let (admin_t6_gas, admin_t6_state_gas) =
3477 calculate_key_authorization_gas(&admin_t6_key_auth, &t6_gas_params, TempoHardfork::T6);
3478 let (unbound_admin_t6_gas, unbound_admin_t6_state_gas) = calculate_key_authorization_gas(
3479 &unbound_admin_t6_key_auth,
3480 &t6_gas_params,
3481 TempoHardfork::T6,
3482 );
3483
3484 assert_eq!(
3485 account_bound_t6_gas - base_t6_gas,
3486 0,
3487 "T6 account-bound authorization does not add key authorization gas"
3488 );
3489 assert_eq!(
3490 admin_t6_gas - base_t6_gas,
3491 KEY_AUTH_EXTRA_EVENT_BUFFER,
3492 "T6 account-bound admin authorization charges one extra event buffer"
3493 );
3494 assert_eq!(
3495 admin_t6_gas - account_bound_t6_gas,
3496 KEY_AUTH_EXTRA_EVENT_BUFFER,
3497 "T6 admin authorization pays one extra event buffer over non-admin account-bound authorization"
3498 );
3499 assert_eq!(
3500 unbound_admin_t6_gas - base_t6_gas,
3501 KEY_AUTH_EXTRA_EVENT_BUFFER,
3502 "T6 root-signed admin authorization without account charges only the extra event buffer"
3503 );
3504 assert_eq!(
3505 account_bound_t6_state_gas, base_t6_state_gas,
3506 "T6 account binding does not add state gas"
3507 );
3508 assert_eq!(
3509 admin_t6_state_gas, base_t6_state_gas,
3510 "T6 admin authorization event buffer does not add state gas"
3511 );
3512 assert_eq!(
3513 unbound_admin_t6_state_gas, base_t6_state_gas,
3514 "T6 unbound admin authorization does not add state gas"
3515 );
3516
3517 let scoped = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3518 .with_allowed_calls(vec![tempo_primitives::transaction::CallScope {
3519 target: Address::random(),
3520 selector_rules: vec![tempo_primitives::transaction::SelectorRule {
3521 selector: [0xa9, 0x05, 0x9c, 0xbb],
3522 recipients: vec![Address::random(), Address::random()],
3523 }],
3524 }])
3525 .into_signed(PrimitiveSignature::Secp256k1(
3526 alloy_primitives::Signature::test_signature(),
3527 ));
3528
3529 let (gas, state_gas) =
3530 calculate_key_authorization_gas(&scoped, &t3_gas_params, TempoHardfork::T3);
3531 let expected = ECRECOVER_GAS + t3_sload + t3_sstore * (1 + 12) + BUFFER;
3532 assert_eq!(
3533 gas, expected,
3534 "T3 scope writes should keep current main accounting"
3535 );
3536 assert_eq!(state_gas, 0, "T3 has no state gas");
3537
3538 let (gas, state_gas) =
3539 calculate_key_authorization_gas(&scoped, &t4_gas_params, TempoHardfork::T4);
3540 let num_sstores = 1 + 12;
3546 let expected_state = t4_sstore_state * num_sstores;
3547 let expected =
3548 ECRECOVER_GAS + t4_sload + t4_sstore * num_sstores + BUFFER + 29_000 + expected_state;
3549 assert_eq!(gas, expected, "T4 scope writes should be fully charged");
3550 assert_eq!(state_gas, expected_state, "T4 scope state gas");
3551 let multi_scope =
3552 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3553 .with_allowed_calls(vec![
3554 tempo_primitives::transaction::CallScope {
3555 target: Address::random(),
3556 selector_rules: vec![
3557 tempo_primitives::transaction::SelectorRule {
3558 selector: [0xa9, 0x05, 0x9c, 0xbb],
3559 recipients: vec![],
3560 },
3561 tempo_primitives::transaction::SelectorRule {
3562 selector: [0x09, 0x5e, 0xa7, 0xb3],
3563 recipients: vec![],
3564 },
3565 ],
3566 },
3567 tempo_primitives::transaction::CallScope {
3568 target: Address::random(),
3569 selector_rules: vec![],
3570 },
3571 ])
3572 .into_signed(PrimitiveSignature::Secp256k1(
3573 alloy_primitives::Signature::test_signature(),
3574 ));
3575
3576 let (gas, state_gas) =
3577 calculate_key_authorization_gas(&multi_scope, &t3_gas_params, TempoHardfork::T3);
3578 let expected = ECRECOVER_GAS + t3_sload + t3_sstore * 14 + BUFFER;
3579 assert_eq!(
3580 gas, expected,
3581 "T3 scope writes should keep current main accounting"
3582 );
3583 assert_eq!(state_gas, 0, "T3 has no state gas");
3584
3585 let (gas, state_gas) =
3586 calculate_key_authorization_gas(&multi_scope, &t4_gas_params, TempoHardfork::T4);
3587 let expected_state = t4_sstore_state * 12;
3588 let expected = ECRECOVER_GAS + t4_sload + t4_sstore * 12 + BUFFER + 33_000 + expected_state;
3589 assert_eq!(
3590 gas, expected,
3591 "T4 scope writes should only charge storage-creating rows"
3592 );
3593 assert_eq!(state_gas, expected_state, "T4 scope state gas");
3594 }
3595
3596 #[test]
3597 fn test_t4_key_authorization_matches_tip1016_sstore_regular_cost() {
3598 use tempo_primitives::transaction::{KeyAuthorization, SignatureType};
3599
3600 let key_auth =
3601 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3602 .into_signed(PrimitiveSignature::Secp256k1(
3603 alloy_primitives::Signature::test_signature(),
3604 ));
3605
3606 let gas_params =
3608 crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
3609
3610 let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
3611 let sload = gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
3612 let scope_extra_gas = call_scope_extra_gas(&key_auth.authorization);
3613 let (regular_gas, state_gas) =
3614 calculate_key_authorization_gas(&key_auth, &gas_params, TempoHardfork::T4);
3615 let helper_sstore_regular = regular_gas - sig_gas - sload - 2_000 - scope_extra_gas;
3616
3617 assert_eq!(helper_sstore_regular, 20_000);
3618 assert_eq!(state_gas, 230_000);
3619 }
3620
3621 #[test]
3622 fn test_t7_key_authorization_intrinsic_includes_storage_credit_value() {
3623 use tempo_chainspec::constants::gas::SSTORE_CREATE_COST;
3624 use tempo_primitives::transaction::{KeyAuthorization, SignatureType};
3625
3626 let key_auth =
3627 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3628 .into_signed(PrimitiveSignature::Secp256k1(
3629 alloy_primitives::Signature::test_signature(),
3630 ));
3631
3632 let gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T7);
3633 let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
3634 let sload = gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
3635 let scope_extra_gas = call_scope_extra_gas(&key_auth.authorization);
3636 let (regular_gas, state_gas) =
3637 calculate_key_authorization_gas(&key_auth, &gas_params, TempoHardfork::T7);
3638 let helper_sstore_regular = regular_gas - sig_gas - sload - 2_000 - scope_extra_gas;
3639
3640 assert_eq!(
3641 gas_params.get(GasId::sstore_set_without_load_cost()),
3642 SSTORE_CREATE_COST - STORAGE_CREDIT_VALUE,
3643 "T7 gas table should expose only the SSTORE residual"
3644 );
3645 assert_eq!(
3646 helper_sstore_regular, SSTORE_CREATE_COST,
3647 "key authorization intrinsic gas must include the TIP-1060 creditable portion"
3648 );
3649 assert_eq!(state_gas, 0, "T7 without TIP-1016 has no state gas split");
3650 }
3651
3652 #[test]
3653 fn test_translate_allowed_calls_for_precompile_preserves_empty_nested_allow_all_lists() {
3654 use tempo_primitives::transaction::{
3655 CallScope, KeyAuthorization, SelectorRule, SignatureType,
3656 };
3657
3658 let empty_selector_rules =
3659 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3660 .with_allowed_calls(vec![CallScope {
3661 target: Address::random(),
3662 selector_rules: vec![],
3663 }])
3664 .into_signed(PrimitiveSignature::Secp256k1(
3665 alloy_primitives::Signature::test_signature(),
3666 ));
3667
3668 let translated = translate_allowed_calls_for_precompile(&empty_selector_rules);
3669 assert_eq!(translated.len(), 1);
3670 assert!(translated[0].selectorRules.is_empty());
3671
3672 let empty_recipients =
3673 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3674 .with_allowed_calls(vec![CallScope {
3675 target: Address::random(),
3676 selector_rules: vec![SelectorRule {
3677 selector: [0xa9, 0x05, 0x9c, 0xbb],
3678 recipients: vec![],
3679 }],
3680 }])
3681 .into_signed(PrimitiveSignature::Secp256k1(
3682 alloy_primitives::Signature::test_signature(),
3683 ));
3684
3685 let translated = translate_allowed_calls_for_precompile(&empty_recipients);
3686 assert_eq!(translated.len(), 1);
3687 assert_eq!(translated[0].selectorRules.len(), 1);
3688 assert!(translated[0].selectorRules[0].recipients.is_empty());
3689 }
3690
3691 #[test]
3692 fn test_key_authorization_gas_in_batch() {
3693 use crate::TempoBatchCallEnv;
3694 use alloy_primitives::{Bytes, TxKind};
3695 use revm::interpreter::gas::calculate_initial_tx_gas;
3696 use tempo_primitives::transaction::{
3697 Call, KeyAuthorization, SignatureType, SignedKeyAuthorization, TempoSignature,
3698 TokenLimit,
3699 };
3700
3701 let calldata = Bytes::from(vec![1, 2, 3]);
3702
3703 let call = Call {
3704 to: TxKind::Call(Address::random()),
3705 value: U256::ZERO,
3706 input: calldata.clone(),
3707 };
3708
3709 let key_auth: SignedKeyAuthorization =
3711 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::random())
3712 .with_limits(vec![
3713 TokenLimit {
3714 token: Address::random(),
3715 limit: U256::from(1000),
3716 period: 0,
3717 },
3718 TokenLimit {
3719 token: Address::random(),
3720 limit: U256::from(2000),
3721 period: 0,
3722 },
3723 ])
3724 .into_signed(PrimitiveSignature::Secp256k1(
3725 alloy_primitives::Signature::test_signature(),
3726 ));
3727
3728 let aa_env_with_key_auth = TempoBatchCallEnv {
3729 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3730 alloy_primitives::Signature::test_signature(),
3731 )),
3732 aa_calls: vec![call.clone()],
3733 key_authorization: Some(key_auth),
3734 signature_hash: B256::ZERO,
3735 ..Default::default()
3736 };
3737
3738 let aa_env_without_key_auth = TempoBatchCallEnv {
3739 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3740 alloy_primitives::Signature::test_signature(),
3741 )),
3742 aa_calls: vec![call],
3743 key_authorization: None,
3744 signature_hash: B256::ZERO,
3745 ..Default::default()
3746 };
3747
3748 let gas_with_key_auth = calculate_aa_batch_intrinsic_gas(
3750 &aa_env_with_key_auth,
3751 &GasParams::default(),
3752 None::<std::iter::Empty<&AccessListItem>>,
3753 tempo_chainspec::hardfork::TempoHardfork::default(),
3754 )
3755 .unwrap();
3756
3757 let gas_without_key_auth = calculate_aa_batch_intrinsic_gas(
3759 &aa_env_without_key_auth,
3760 &GasParams::default(),
3761 None::<std::iter::Empty<&AccessListItem>>,
3762 tempo_chainspec::hardfork::TempoHardfork::default(),
3763 )
3764 .unwrap();
3765
3766 let expected_key_auth_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS;
3768
3769 assert_eq!(
3770 gas_with_key_auth.initial_total_gas() - gas_without_key_auth.initial_total_gas(),
3771 expected_key_auth_gas,
3772 "Key authorization should add exactly {expected_key_auth_gas} gas to batch",
3773 );
3774
3775 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
3777 let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
3778 let expected_without = base_tx_gas.initial_total_gas(); let expected_with = expected_without + expected_key_auth_gas;
3780
3781 assert_eq!(
3782 gas_without_key_auth.initial_total_gas(),
3783 expected_without,
3784 "Gas without key auth should match expected"
3785 );
3786 assert_eq!(
3787 gas_with_key_auth.initial_total_gas(),
3788 expected_with,
3789 "Gas with key auth should match expected"
3790 );
3791 }
3792
3793 #[test]
3794 fn test_2d_nonce_gas_in_intrinsic_gas() {
3795 use crate::gas_params::tempo_gas_params;
3796 use revm::{context_interface::cfg::GasId, handler::Handler};
3797
3798 const BASE_INTRINSIC_GAS: u64 = 21_000;
3799
3800 for spec in [
3801 TempoHardfork::Genesis,
3802 TempoHardfork::T0,
3803 TempoHardfork::T1,
3804 TempoHardfork::T1A,
3805 TempoHardfork::T1B,
3806 TempoHardfork::T2,
3807 ] {
3808 let gas_params = tempo_gas_params(spec);
3809
3810 let make_evm = |nonce: u64, nonce_key: U256| {
3811 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3812 let mut cfg = CfgEnv::<TempoHardfork>::default();
3813 cfg.spec = spec;
3814 cfg.gas_params = gas_params.clone();
3815 let ctx = Context::mainnet()
3816 .with_db(CacheDB::new(EmptyDB::default()))
3817 .with_block(TempoBlockEnv::default())
3818 .with_cfg(cfg)
3819 .with_tx(TempoTxEnv {
3820 inner: revm::context::TxEnv {
3821 gas_limit: 1_000_000,
3822 nonce,
3823 ..Default::default()
3824 },
3825 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3826 aa_calls: vec![Call {
3827 to: TxKind::Call(Address::random()),
3828 value: U256::ZERO,
3829 input: Bytes::new(),
3830 }],
3831 nonce_key,
3832 ..Default::default()
3833 })),
3834 ..Default::default()
3835 })
3836 .with_new_journal(journal);
3837 TempoEvm::<_, ()>::new(ctx, ())
3838 };
3839
3840 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3841
3842 {
3844 let mut evm = make_evm(5, U256::ZERO);
3845 let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
3846 assert_eq!(
3847 gas.initial_total_gas(),
3848 BASE_INTRINSIC_GAS,
3849 "{spec:?}: protocol nonce (nonce_key=0, nonce>0) should have no extra gas"
3850 );
3851 }
3852
3853 {
3855 let expected = if spec.is_t1() {
3856 BASE_INTRINSIC_GAS + gas_params.get(GasId::new_account_cost())
3858 } else {
3859 BASE_INTRINSIC_GAS + spec.gas_new_nonce_key()
3861 };
3862 let mut evm = make_evm(0, U256::ONE);
3863 let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
3864 assert_eq!(
3865 gas.initial_total_gas(),
3866 expected,
3867 "{spec:?}: nonce_key!=0, nonce==0 gas mismatch"
3868 );
3869 }
3870
3871 {
3873 let mut evm = make_evm(5, U256::ONE);
3874 let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
3875 assert_eq!(
3876 gas.initial_total_gas(),
3877 BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(),
3878 "{spec:?}: existing 2D nonce key gas mismatch"
3879 );
3880 }
3881 }
3882 }
3883
3884 #[test]
3885 fn test_2d_nonce_gas_limit_validation() {
3886 use crate::gas_params::tempo_gas_params;
3887 use revm::{context_interface::cfg::GasId, handler::Handler};
3888
3889 const BASE_INTRINSIC_GAS: u64 = 21_000;
3890
3891 for spec in [
3892 TempoHardfork::Genesis,
3893 TempoHardfork::T0,
3894 TempoHardfork::T1,
3895 TempoHardfork::T2,
3896 ] {
3897 let gas_params = tempo_gas_params(spec);
3898
3899 let nonce_zero_gas = if spec.is_t1() {
3901 gas_params.get(GasId::new_account_cost())
3902 } else {
3903 spec.gas_new_nonce_key()
3904 };
3905 let nonce_zero_state_gas = gas_params.new_account_state_gas();
3906 let nonce_zero_total = nonce_zero_gas + nonce_zero_state_gas;
3907
3908 let cases = if spec.is_t0() {
3909 let mut cases = vec![
3910 (BASE_INTRINSIC_GAS + nonce_zero_total, 0, true), (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), ];
3913 cases.push((BASE_INTRINSIC_GAS + nonce_zero_total - 1, 0u64, false));
3915 cases
3916 } else {
3917 vec![
3919 (BASE_INTRINSIC_GAS + 10_000, 0u64, true), (BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), (BASE_INTRINSIC_GAS - 1, 0, false), ]
3924 };
3925
3926 for (gas_limit, nonce, should_succeed) in cases {
3927 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3928 let mut cfg = CfgEnv::<TempoHardfork>::default();
3929 cfg.spec = spec;
3930 cfg.gas_params = gas_params.clone();
3931 let ctx = Context::mainnet()
3932 .with_db(CacheDB::new(EmptyDB::default()))
3933 .with_block(TempoBlockEnv::default())
3934 .with_cfg(cfg)
3935 .with_tx(TempoTxEnv {
3936 inner: revm::context::TxEnv {
3937 gas_limit,
3938 nonce,
3939 ..Default::default()
3940 },
3941 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3942 aa_calls: vec![Call {
3943 to: TxKind::Call(Address::random()),
3944 value: U256::ZERO,
3945 input: Bytes::new(),
3946 }],
3947 nonce_key: U256::ONE,
3948 ..Default::default()
3949 })),
3950 ..Default::default()
3951 })
3952 .with_new_journal(journal);
3953
3954 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
3955 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3956 let result = handler.validate_initial_tx_gas(&mut evm);
3957
3958 if should_succeed {
3959 assert!(
3960 result.is_ok(),
3961 "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: expected success but got error"
3962 );
3963 } else {
3964 let err = result.expect_err(&format!(
3965 "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: should fail"
3966 ));
3967 assert!(
3968 matches!(
3969 err.as_invalid_tx_err(),
3970 Some(TempoInvalidTransaction::EthInvalidTransaction(
3971 InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
3972 ))
3973 ),
3974 "Expected CallGasCostMoreThanGasLimit, got: {err:?}"
3975 );
3976 }
3977 }
3978 }
3979 }
3980
3981 #[test]
3982 fn test_t3_scope_validation_moves_to_execution() {
3983 const CALL_SCOPE_SELECTOR: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
3984
3985 let caller = Address::repeat_byte(0x11);
3986 let access_key = Address::repeat_byte(0x22);
3987 let target = DEFAULT_FEE_TOKEN;
3988
3989 let signature =
3990 TempoSignature::Keychain(tempo_primitives::transaction::KeychainSignature::new(
3991 caller,
3992 tempo_primitives::transaction::PrimitiveSignature::Secp256k1(
3993 alloy_primitives::Signature::test_signature(),
3994 ),
3995 ));
3996
3997 let mut cfg = CfgEnv::<TempoHardfork>::default();
3998 cfg.spec = TempoHardfork::T3;
3999
4000 let tx_env = TempoTxEnv {
4001 inner: revm::context::TxEnv {
4002 caller,
4003 gas_limit: 1_000_000,
4004 kind: TxKind::Call(target),
4005 ..Default::default()
4006 },
4007 fee_token: Some(DEFAULT_FEE_TOKEN),
4008 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4009 signature,
4010 aa_calls: vec![Call {
4011 to: TxKind::Call(target),
4012 value: U256::ZERO,
4013 input: Bytes::from_static(&CALL_SCOPE_SELECTOR),
4014 }],
4015 signature_hash: B256::ZERO,
4016 override_key_id: Some(access_key),
4017 ..Default::default()
4018 })),
4019 ..Default::default()
4020 };
4021
4022 let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T3, tx_env, |cfg_override| {
4023 *cfg_override = cfg;
4024 });
4025
4026 StorageCtx::enter_ctx(&mut test.evm.inner.ctx, StorageActions::disabled(), || {
4027 let mut keychain = AccountKeychain::new();
4028
4029 keychain.initialize().expect("keychain initialized");
4030 keychain
4031 .set_transaction_key(Address::ZERO)
4032 .expect("root key setup succeeds");
4033 keychain
4034 .set_tx_origin(caller)
4035 .expect("tx.origin setup succeeds");
4036 keychain
4037 .authorize_key(
4038 caller,
4039 access_key,
4040 PrecompileSignatureType::Secp256k1,
4041 KeyRestrictions {
4042 expiry: u64::MAX,
4043 enforceLimits: false,
4044 limits: vec![],
4045 allowAnyCalls: false,
4046 allowedCalls: vec![PrecompileCallScope {
4047 target,
4048 selectorRules: vec![PrecompileSelectorRule {
4049 selector: CALL_SCOPE_SELECTOR.into(),
4050 recipients: vec![],
4051 }],
4052 }],
4053 },
4054 None,
4055 )
4056 .expect("access key authorization succeeds");
4057 });
4058
4059 let init_gas = test.validate_initial_tx_gas();
4060 assert!(
4061 init_gas.floor_gas <= init_gas.initial_total_gas(),
4062 "test requires floor gas to not exceed intrinsic gas"
4063 );
4064
4065 test.evm.inner.ctx.tx.inner.gas_limit = init_gas.initial_total_gas();
4066
4067 test.validate_against_state_and_deduct_caller()
4068 .expect("scope validation no longer runs during state validation");
4069
4070 let result = test.execute(&init_gas);
4071
4072 assert!(
4073 matches!(
4074 result.instruction_result(),
4075 revm::interpreter::InstructionResult::PrecompileOOG
4076 ),
4077 "expected scope validation to fail during execution with OOG, got: {:?}",
4078 result.instruction_result()
4079 );
4080 assert_eq!(
4081 result.gas().limit(),
4082 init_gas.initial_total_gas(),
4083 "batch OOG should report the full tx gas budget"
4084 );
4085 assert_eq!(
4086 result.gas().total_gas_spent(),
4087 init_gas.initial_total_gas(),
4088 "batch OOG should consume the full tx gas budget"
4089 );
4090 assert_eq!(result.gas().refunded(), 0);
4091 }
4092
4093 #[test]
4094 fn test_t3_scope_validation_returns_call_not_allowed_revert_data() {
4095 use alloy_sol_types::SolInterface;
4096 use tempo_contracts::precompiles::AccountKeychainError;
4097
4098 const ALLOWED_SELECTOR: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
4099 const DENIED_SELECTOR: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
4100
4101 let caller = Address::repeat_byte(0x11);
4102 let access_key = Address::repeat_byte(0x22);
4103 let target = DEFAULT_FEE_TOKEN;
4104
4105 let signature =
4106 TempoSignature::Keychain(tempo_primitives::transaction::KeychainSignature::new(
4107 caller,
4108 tempo_primitives::transaction::PrimitiveSignature::Secp256k1(
4109 alloy_primitives::Signature::test_signature(),
4110 ),
4111 ));
4112
4113 let mut cfg = CfgEnv::<TempoHardfork>::default();
4114 cfg.spec = TempoHardfork::T3;
4115
4116 let tx_env = TempoTxEnv {
4117 inner: revm::context::TxEnv {
4118 caller,
4119 gas_limit: 1_000_000,
4120 kind: TxKind::Call(target),
4121 ..Default::default()
4122 },
4123 fee_token: Some(DEFAULT_FEE_TOKEN),
4124 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4125 signature,
4126 aa_calls: vec![Call {
4127 to: TxKind::Call(target),
4128 value: U256::ZERO,
4129 input: Bytes::from_static(&DENIED_SELECTOR),
4130 }],
4131 signature_hash: B256::ZERO,
4132 override_key_id: Some(access_key),
4133 ..Default::default()
4134 })),
4135 ..Default::default()
4136 };
4137
4138 let ctx = Context::mainnet()
4139 .with_db(CacheDB::new(EmptyDB::default()))
4140 .with_block(TempoBlockEnv::default())
4141 .with_cfg(cfg)
4142 .with_tx(tx_env.clone())
4143 .with_new_journal(create_test_journal());
4144
4145 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4146 let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4147
4148 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
4149 let mut keychain = AccountKeychain::new();
4150
4151 keychain.initialize().expect("keychain initialized");
4152 keychain
4153 .set_transaction_key(Address::ZERO)
4154 .expect("root key setup succeeds");
4155 keychain
4156 .set_tx_origin(caller)
4157 .expect("tx.origin setup succeeds");
4158 keychain
4159 .authorize_key(
4160 caller,
4161 access_key,
4162 PrecompileSignatureType::Secp256k1,
4163 KeyRestrictions {
4164 expiry: u64::MAX,
4165 enforceLimits: false,
4166 limits: vec![],
4167 allowAnyCalls: false,
4168 allowedCalls: vec![PrecompileCallScope {
4169 target,
4170 selectorRules: vec![PrecompileSelectorRule {
4171 selector: ALLOWED_SELECTOR.into(),
4172 recipients: vec![],
4173 }],
4174 }],
4175 },
4176 None,
4177 )
4178 .expect("access key authorization succeeds");
4179 });
4180
4181 let init_gas = handler
4182 .validate_initial_tx_gas(&mut evm)
4183 .expect("initial gas validation should succeed");
4184
4185 handler
4186 .validate_against_state_and_deduct_caller(&mut evm, &mut Default::default())
4187 .expect("scope validation no longer runs during state validation");
4188
4189 let result = handler
4190 .execution(&mut evm, &init_gas)
4191 .expect("execution should return a frame result");
4192
4193 let expected_revert: Bytes = AccountKeychainError::call_not_allowed().abi_encode().into();
4194
4195 assert_eq!(result.instruction_result(), InstructionResult::Revert);
4196 assert_eq!(result.output().data(), &expected_revert);
4197 assert!(
4198 result.gas().total_gas_spent() < tx_env.gas_limit,
4199 "prevalidate revert must not consume the full gas_limit"
4200 );
4201 }
4202
4203 #[test]
4204 fn test_t3_scope_validation_empty_calls_returns_custom_error() {
4205 let caller = Address::repeat_byte(0x11);
4206 let access_key = Address::repeat_byte(0x22);
4207
4208 let signature =
4209 TempoSignature::Keychain(tempo_primitives::transaction::KeychainSignature::new(
4210 caller,
4211 tempo_primitives::transaction::PrimitiveSignature::Secp256k1(
4212 alloy_primitives::Signature::test_signature(),
4213 ),
4214 ));
4215
4216 let mut cfg = CfgEnv::<TempoHardfork>::default();
4217 cfg.spec = TempoHardfork::T3;
4218
4219 let tx_env = TempoTxEnv {
4220 inner: revm::context::TxEnv {
4221 caller,
4222 gas_limit: 1_000_000,
4223 ..Default::default()
4224 },
4225 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4226 signature,
4227 aa_calls: vec![],
4228 signature_hash: B256::ZERO,
4229 override_key_id: Some(access_key),
4230 ..Default::default()
4231 })),
4232 ..Default::default()
4233 };
4234
4235 let ctx = Context::mainnet()
4236 .with_db(CacheDB::new(EmptyDB::default()))
4237 .with_block(TempoBlockEnv::default())
4238 .with_cfg(cfg)
4239 .with_tx(tx_env)
4240 .with_new_journal(create_test_journal());
4241
4242 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4243 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4244 let mut remaining_gas = 100_000;
4245
4246 let err = handler
4247 .prevalidate_keychain_call_scopes(&mut evm, &[], &mut remaining_gas, 0)
4248 .expect_err("empty calls should return an error instead of panicking");
4249
4250 match err {
4251 EVMError::Custom(msg) => {
4252 assert_eq!(msg, "AA transactions must contain at least one call");
4253 }
4254 other => panic!("expected custom error, got: {other:?}"),
4255 }
4256 }
4257
4258 #[test]
4260 fn test_refund_cap_removed_on_t7() {
4261 use revm::{
4262 Context, Journal,
4263 context::CfgEnv,
4264 database::{CacheDB, EmptyDB},
4265 handler::FrameResult,
4266 interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult},
4267 };
4268
4269 const SPENT: u64 = 100_000;
4271 const REFUND: i64 = 50_000;
4272 const CAPPED: i64 = (SPENT / 5) as i64;
4273
4274 let refunded_for_spec = |spec: TempoHardfork| -> i64 {
4275 let mut cfg = CfgEnv::<TempoHardfork>::default();
4276 cfg.spec = spec;
4277 let ctx = Context::mainnet()
4278 .with_db(CacheDB::new(EmptyDB::default()))
4279 .with_block(TempoBlockEnv::default())
4280 .with_cfg(cfg)
4281 .with_tx(TempoTxEnv::default())
4282 .with_new_journal(Journal::new(CacheDB::new(EmptyDB::default())));
4283 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4284 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4285
4286 let mut gas = Gas::new(SPENT);
4287 gas.set_spent(SPENT);
4288 gas.record_refund(REFUND);
4289 let mut frame_result = FrameResult::Call(CallOutcome::new(
4290 InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas),
4291 0..0,
4292 ));
4293
4294 handler.refund(&mut evm, &mut frame_result, 0);
4295 frame_result.gas().refunded()
4296 };
4297
4298 assert_eq!(
4299 refunded_for_spec(TempoHardfork::T6),
4300 CAPPED,
4301 "pre-T7 must cap the refund at one fifth of gas used"
4302 );
4303 assert_eq!(
4304 refunded_for_spec(TempoHardfork::T7),
4305 REFUND,
4306 "T7 must credit the full refund, with no EIP-3529 cap"
4307 );
4308 }
4309
4310 #[test]
4311 fn test_multicall_gas_refund_accounting() {
4312 use crate::evm::TempoEvm;
4313 use alloy_primitives::{Bytes, TxKind};
4314 use revm::{
4315 Context, Journal,
4316 context::CfgEnv,
4317 database::{CacheDB, EmptyDB},
4318 handler::FrameResult,
4319 interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult},
4320 };
4321 use tempo_primitives::transaction::Call;
4322
4323 const GAS_LIMIT: u64 = 1_000_000;
4324 const INTRINSIC_GAS: u64 = 21_000;
4325 const SPENT: (u64, u64) = (1000, 500);
4327 const REFUND: (i64, i64) = (100, 50);
4328
4329 let db = CacheDB::new(EmptyDB::default());
4331 let journal = Journal::new(db);
4332 let ctx = Context::mainnet()
4333 .with_db(CacheDB::new(EmptyDB::default()))
4334 .with_block(TempoBlockEnv::default())
4335 .with_cfg(CfgEnv::default())
4336 .with_tx(TempoTxEnv {
4337 inner: revm::context::TxEnv {
4338 gas_limit: GAS_LIMIT,
4339 ..Default::default()
4340 },
4341 ..Default::default()
4342 })
4343 .with_new_journal(journal);
4344
4345 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
4346 let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4347
4348 let calls = vec![
4350 Call {
4351 to: TxKind::Call(Address::random()),
4352 value: U256::ZERO,
4353 input: Bytes::new(),
4354 },
4355 Call {
4356 to: TxKind::Call(Address::random()),
4357 value: U256::ZERO,
4358 input: Bytes::new(),
4359 },
4360 ];
4361
4362 let (mut call_idx, calls_gas) = (0, [(SPENT.0, REFUND.0), (SPENT.1, REFUND.1)]);
4363 let result = handler.execute_multi_call_with(
4364 &mut evm,
4365 GAS_LIMIT - INTRINSIC_GAS,
4366 0,
4367 calls,
4368 |_handler, _evm, gas, _reservoir| {
4369 let (spent, refund) = calls_gas[call_idx];
4370 call_idx += 1;
4371
4372 let mut gas = Gas::new(gas);
4374 gas.set_spent(spent);
4375 gas.record_refund(refund);
4376
4377 Ok(FrameResult::Call(CallOutcome::new(
4379 InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas),
4380 0..0,
4381 )))
4382 },
4383 );
4384
4385 let result = result.expect("execute_multi_call_with should succeed");
4386 let final_gas = result.gas();
4387
4388 assert_eq!(
4389 final_gas.total_gas_spent(),
4390 INTRINSIC_GAS + SPENT.0 + SPENT.1,
4391 "Total spent should be intrinsic_gas + sum of all calls' spent values"
4392 );
4393 assert_eq!(
4394 final_gas.refunded(),
4395 REFUND.0 + REFUND.1,
4396 "Total refund should be sum of all calls' refunded values"
4397 );
4398 assert_eq!(
4399 final_gas.used(),
4400 INTRINSIC_GAS + SPENT.0 + SPENT.1 - (REFUND.0 + REFUND.1) as u64,
4401 "used() should be spent - refund"
4402 );
4403 }
4404
4405 fn arb_opt_timestamp() -> impl Strategy<Value = Option<u64>> {
4407 prop_oneof![Just(None), any::<u64>().prop_map(Some)]
4408 }
4409
4410 fn secp256k1_sig() -> TempoSignature {
4417 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
4418 alloy_primitives::Signature::test_signature(),
4419 ))
4420 }
4421
4422 fn make_aa_env(calls: Vec<Call>) -> TempoBatchCallEnv {
4424 TempoBatchCallEnv {
4425 signature: secp256k1_sig(),
4426 aa_calls: calls,
4427 key_authorization: None,
4428 signature_hash: B256::ZERO,
4429 ..Default::default()
4430 }
4431 }
4432
4433 fn make_single_call_env(calldata: Bytes) -> TempoBatchCallEnv {
4435 make_aa_env(vec![Call {
4436 to: TxKind::Call(Address::ZERO),
4437 value: U256::ZERO,
4438 input: calldata,
4439 }])
4440 }
4441
4442 fn make_multi_call_env(num_calls: usize) -> TempoBatchCallEnv {
4444 make_aa_env(
4445 (0..num_calls)
4446 .map(|_| Call {
4447 to: TxKind::Call(Address::ZERO),
4448 value: U256::ZERO,
4449 input: Bytes::new(),
4450 })
4451 .collect(),
4452 )
4453 }
4454
4455 fn compute_aa_gas(env: &TempoBatchCallEnv) -> InitialAndFloorGas {
4457 calculate_aa_batch_intrinsic_gas(
4458 env,
4459 &GasParams::default(),
4460 None::<std::iter::Empty<&AccessListItem>>,
4461 tempo_chainspec::hardfork::TempoHardfork::default(),
4462 )
4463 .unwrap()
4464 }
4465
4466 proptest! {
4467 #![proptest_config(ProptestConfig::with_cases(500))]
4468
4469 #[test]
4471 fn proptest_validate_time_window_correctness(
4472 valid_after in arb_opt_timestamp(),
4473 valid_before in arb_opt_timestamp(),
4474 block_timestamp in any::<u64>(),
4475 ) {
4476 let result = validate_time_window(valid_after, valid_before, block_timestamp);
4477
4478 let after_ok = valid_after.is_none_or(|after| block_timestamp >= after);
4479 let before_ok = valid_before.is_none_or(|before| block_timestamp < before);
4480 let expected_valid = after_ok && before_ok;
4481
4482 prop_assert_eq!(result.is_ok(), expected_valid,
4483 "valid_after={:?}, valid_before={:?}, block_ts={}, result={:?}",
4484 valid_after, valid_before, block_timestamp, result);
4485 }
4486
4487 #[test]
4489 fn proptest_validate_time_window_none_always_valid(block_timestamp in any::<u64>()) {
4490 prop_assert!(validate_time_window(None, None, block_timestamp).is_ok());
4491 }
4492
4493 #[test]
4500 fn proptest_validate_time_window_zero_after_equivalent_to_none(
4501 valid_before in arb_opt_timestamp(),
4502 block_timestamp in any::<u64>(),
4503 ) {
4504 let with_zero = validate_time_window(Some(0), valid_before, block_timestamp);
4505 let with_none = validate_time_window(None, valid_before, block_timestamp);
4506 prop_assert_eq!(with_zero.is_ok(), with_none.is_ok());
4507 }
4508
4509 #[test]
4511 fn proptest_validate_time_window_empty_window(
4512 valid_after in 1u64..=u64::MAX,
4513 offset in 0u64..1000u64,
4514 ) {
4515 let valid_before = valid_after.saturating_sub(offset);
4516 let result = validate_time_window(Some(valid_after), Some(valid_before), valid_after);
4517 prop_assert!(result.is_err(), "Empty window should reject all timestamps");
4518 }
4519
4520 #[test]
4522 fn proptest_signature_gas_ordering(webauthn_data_len in 0usize..1000) {
4523 let secp_sig = PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature());
4524 let p256_sig = PrimitiveSignature::P256(P256SignatureWithPreHash {
4525 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
4526 });
4527 let webauthn_sig = PrimitiveSignature::WebAuthn(WebAuthnSignature {
4528 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
4529 webauthn_data: Bytes::from(vec![0u8; webauthn_data_len]),
4530 });
4531
4532 let secp_gas = primitive_signature_verification_gas(&secp_sig);
4533 let p256_gas = primitive_signature_verification_gas(&p256_sig);
4534 let webauthn_gas = primitive_signature_verification_gas(&webauthn_sig);
4535
4536 prop_assert!(secp_gas <= p256_gas, "secp256k1 should be <= p256");
4537 prop_assert!(p256_gas <= webauthn_gas, "p256 should be <= webauthn");
4538 }
4539
4540 #[test]
4543 fn proptest_gas_monotonicity_calldata_nonzero(
4544 calldata_len1 in 0usize..1000,
4545 calldata_len2 in 0usize..1000,
4546 ) {
4547 let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len1])));
4548 let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len2])));
4549
4550 if calldata_len1 <= calldata_len2 {
4551 prop_assert!(gas1.initial_total_gas() <= gas2.initial_total_gas(),
4552 "More calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
4553 calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4554 } else {
4555 prop_assert!(gas1.initial_total_gas() >= gas2.initial_total_gas(),
4556 "Less calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
4557 calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4558 }
4559 }
4560
4561 #[test]
4564 fn proptest_gas_monotonicity_calldata_zero(
4565 calldata_len1 in 0usize..1000,
4566 calldata_len2 in 0usize..1000,
4567 ) {
4568 let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len1])));
4569 let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len2])));
4570
4571 if calldata_len1 <= calldata_len2 {
4572 prop_assert!(gas1.initial_total_gas() <= gas2.initial_total_gas(),
4573 "More zero-byte calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
4574 calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4575 } else {
4576 prop_assert!(gas1.initial_total_gas() >= gas2.initial_total_gas(),
4577 "Less zero-byte calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
4578 calldata_len1, gas1.initial_total_gas(), calldata_len2, gas2.initial_total_gas());
4579 }
4580 }
4581
4582 #[test]
4585 fn proptest_zero_bytes_cheaper_than_nonzero(calldata_len in 1usize..1000) {
4586 let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
4587 let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
4588
4589 prop_assert!(zero_gas.initial_total_gas() < nonzero_gas.initial_total_gas(),
4590 "Zero-byte calldata should cost less: len={}, zero_gas={}, nonzero_gas={}",
4591 calldata_len, zero_gas.initial_total_gas(), nonzero_gas.initial_total_gas());
4592 }
4593
4594 #[test]
4597 fn proptest_mixed_calldata_gas_bounded(
4598 calldata_len in 1usize..500,
4599 nonzero_ratio in 0u8..=100,
4600 ) {
4601 let calldata: Vec<u8> = (0..calldata_len)
4603 .map(|i| if (i * 100 / calldata_len) < nonzero_ratio as usize { 1u8 } else { 0u8 })
4604 .collect();
4605
4606 let mixed_gas = compute_aa_gas(&make_single_call_env(Bytes::from(calldata)));
4607 let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
4608 let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
4609
4610 prop_assert!(mixed_gas.initial_total_gas() >= zero_gas.initial_total_gas(),
4611 "Mixed calldata gas should be >= all-zero gas: mixed={}, zero={}",
4612 mixed_gas.initial_total_gas(), zero_gas.initial_total_gas());
4613 prop_assert!(mixed_gas.initial_total_gas() <= nonzero_gas.initial_total_gas(),
4614 "Mixed calldata gas should be <= all-nonzero gas: mixed={}, nonzero={}",
4615 mixed_gas.initial_total_gas(), nonzero_gas.initial_total_gas());
4616 }
4617
4618 #[test]
4620 fn proptest_gas_monotonicity_call_count(
4621 num_calls1 in 1usize..10,
4622 num_calls2 in 1usize..10,
4623 ) {
4624 let gas1 = compute_aa_gas(&make_multi_call_env(num_calls1));
4625 let gas2 = compute_aa_gas(&make_multi_call_env(num_calls2));
4626
4627 if num_calls1 <= num_calls2 {
4628 prop_assert!(gas1.initial_total_gas() <= gas2.initial_total_gas(),
4629 "More calls should mean more gas: calls1={}, gas1={}, calls2={}, gas2={}",
4630 num_calls1, gas1.initial_total_gas(), num_calls2, gas2.initial_total_gas());
4631 } else {
4632 prop_assert!(gas1.initial_total_gas() >= gas2.initial_total_gas(),
4633 "Fewer calls should mean less gas: calls1={}, gas1={}, calls2={}, gas2={}",
4634 num_calls1, gas1.initial_total_gas(), num_calls2, gas2.initial_total_gas());
4635 }
4636 }
4637
4638 #[test]
4648 fn proptest_gas_aa_secp256k1_exact_bounds(num_calls in 1usize..5) {
4649 let gas = compute_aa_gas(&make_multi_call_env(num_calls));
4650
4651 let expected = 21_000 + COLD_ACCOUNT_ACCESS_COST * (num_calls.saturating_sub(1) as u64);
4653 prop_assert_eq!(gas.initial_total_gas(), expected,
4654 "Gas {} should equal expected {} for {} calls (21k + {}*COLD_ACCOUNT_ACCESS_COST)",
4655 gas.initial_total_gas(), expected, num_calls, num_calls.saturating_sub(1));
4656 }
4657
4658 #[test]
4660 fn proptest_first_call_returns_first_for_aa(num_calls in 1usize..10) {
4661 let calls: Vec<Call> = (0..num_calls)
4662 .map(|i| Call {
4663 to: TxKind::Call(Address::with_last_byte(i as u8)),
4664 value: U256::ZERO,
4665 input: Bytes::from(vec![i as u8; i + 1]),
4666 })
4667 .collect();
4668
4669 let expected_addr = Address::with_last_byte(0);
4670 let expected_input = vec![0u8; 1];
4671
4672 let tx_env = TempoTxEnv {
4673 inner: revm::context::TxEnv::default(),
4674 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4675 aa_calls: calls,
4676 signature: secp256k1_sig(),
4677 signature_hash: B256::ZERO,
4678 ..Default::default()
4679 })),
4680 ..Default::default()
4681 };
4682
4683 let first = tx_env.first_call();
4684 prop_assert!(first.is_some(), "first_call should return Some for non-empty AA calls");
4685
4686 let (kind, input) = first.unwrap();
4687 prop_assert_eq!(*kind, TxKind::Call(expected_addr), "Should return first call's address");
4688 prop_assert_eq!(input, expected_input.as_slice(), "Should return first call's input");
4689 }
4690
4691 #[test]
4693 fn proptest_first_call_empty_aa(_dummy in 0u8..1) {
4694 let tx_env = TempoTxEnv {
4695 inner: revm::context::TxEnv::default(),
4696 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4697 aa_calls: vec![],
4698 signature: secp256k1_sig(),
4699 signature_hash: B256::ZERO,
4700 ..Default::default()
4701 })),
4702 ..Default::default()
4703 };
4704
4705 prop_assert!(tx_env.first_call().is_none(), "first_call should return None for empty AA calls");
4706 }
4707
4708 #[test]
4710 fn proptest_first_call_non_aa(calldata_len in 0usize..100) {
4711 let calldata = Bytes::from(vec![0xab_u8; calldata_len]);
4712 let target = Address::random();
4713
4714 let tx_env = TempoTxEnv {
4715 inner: revm::context::TxEnv {
4716 kind: TxKind::Call(target),
4717 data: calldata.clone(),
4718 ..Default::default()
4719 },
4720 tempo_tx_env: None,
4721 ..Default::default()
4722 };
4723
4724 let first = tx_env.first_call();
4725 prop_assert!(first.is_some(), "first_call should return Some for non-AA tx");
4726
4727 let (kind, input) = first.unwrap();
4728 prop_assert_eq!(*kind, TxKind::Call(target), "Should return inner tx kind");
4729 prop_assert_eq!(input, calldata.as_ref(), "Should return inner tx data");
4730 }
4731
4732 #[test]
4734 fn proptest_key_auth_gas_monotonic_limits(
4735 num_limits1 in 0usize..10,
4736 num_limits2 in 0usize..10,
4737 ) {
4738 use tempo_primitives::transaction::{
4739 SignatureType, SignedKeyAuthorization,
4740 key_authorization::KeyAuthorization,
4741 TokenLimit as PrimTokenLimit,
4742 };
4743
4744 let make_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
4745 let mut auth =
4746 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::ZERO);
4747 if num_limits > 0 {
4748 auth = auth.with_limits((0..num_limits).map(|i| PrimTokenLimit {
4749 token: Address::with_last_byte(i as u8),
4750 limit: U256::from(1000),
4751 period: 0,
4752 }).collect());
4753 }
4754 auth.into_signed(PrimitiveSignature::Secp256k1(
4755 alloy_primitives::Signature::test_signature(),
4756 ))
4757 };
4758
4759 for (gas_params, spec) in [
4761 (GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default()),
4762 (crate::gas_params::tempo_gas_params(TempoHardfork::T1B), TempoHardfork::T1B),
4763 ] {
4764 let (gas1, _) = calculate_key_authorization_gas(&make_key_auth(num_limits1), &gas_params, spec);
4765 let (gas2, _) = calculate_key_authorization_gas(&make_key_auth(num_limits2), &gas_params, spec);
4766
4767 if num_limits1 <= num_limits2 {
4768 prop_assert!(gas1 <= gas2,
4769 "{spec:?}: More limits should mean more gas: limits1={}, gas1={}, limits2={}, gas2={}",
4770 num_limits1, gas1, num_limits2, gas2);
4771 } else {
4772 prop_assert!(gas1 >= gas2,
4773 "{spec:?}: Fewer limits should mean less gas: limits1={}, gas1={}, limits2={}, gas2={}",
4774 num_limits1, gas1, num_limits2, gas2);
4775 }
4776 }
4777 }
4778
4779 #[test]
4781 fn proptest_key_auth_gas_minimum(
4782 sig_type in 0u8..3,
4783 num_limits in 0usize..5,
4784 ) {
4785 use tempo_primitives::transaction::{
4786 SignatureType, TokenLimit as PrimTokenLimit, key_authorization::KeyAuthorization,
4787 };
4788
4789 let signature = match sig_type {
4790 0 => PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
4791 1 => PrimitiveSignature::P256(P256SignatureWithPreHash {
4792 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
4793 }),
4794 _ => PrimitiveSignature::WebAuthn(WebAuthnSignature {
4795 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
4796 webauthn_data: Bytes::new(),
4797 }),
4798 };
4799
4800 let mut auth =
4801 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, Address::ZERO);
4802 if num_limits > 0 {
4803 auth = auth.with_limits((0..num_limits).map(|i| PrimTokenLimit {
4804 token: Address::with_last_byte(i as u8),
4805 limit: U256::from(1000),
4806 period: 0,
4807 }).collect());
4808 }
4809 let key_auth = auth.into_signed(signature);
4810
4811 let (gas, _) = calculate_key_authorization_gas(&key_auth, &GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default());
4813 let min_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS;
4814 prop_assert!(gas >= min_gas,
4815 "Pre-T1B: Key auth gas should be at least {min_gas}, got {gas}");
4816
4817 let t1b_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
4819 let (gas_t1b, _) = calculate_key_authorization_gas(&key_auth, &t1b_params, TempoHardfork::T1B);
4820 let sstore = t1b_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
4821 let sload = t1b_params.warm_storage_read_cost() + t1b_params.cold_storage_additional_cost();
4822 let min_t1b = ECRECOVER_GAS + sload + sstore;
4823 prop_assert!(gas_t1b >= min_t1b,
4824 "T1B: Key auth gas should be at least {min_t1b}, got {gas_t1b}");
4825 }
4826 }
4827
4828 #[test]
4838 fn test_t1_2d_nonce_key_charges_250k_gas() {
4839 use crate::gas_params::tempo_gas_params;
4840 use revm::{context_interface::cfg::GasId, handler::Handler};
4841
4842 const TEST_TARGET: Address = Address::new([0xAA; 20]);
4844 const TEST_NONCE_KEY: U256 = U256::from_limbs([42, 0, 0, 0]);
4845 const SPEC: TempoHardfork = TempoHardfork::T1;
4846 const NEW_NONCE_KEY_GAS: u64 = SPEC.gas_new_nonce_key();
4847 const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
4848
4849 let mut cfg = CfgEnv::<TempoHardfork>::default();
4851 cfg.spec = SPEC;
4852 cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
4853
4854 let new_account_cost = cfg.gas_params.get(GasId::new_account_cost());
4856 assert_eq!(
4857 new_account_cost, 250_000,
4858 "T1 gas params should have 250k new_account_cost"
4859 );
4860
4861 let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
4863 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
4864 let ctx = Context::mainnet()
4865 .with_db(CacheDB::new(EmptyDB::default()))
4866 .with_block(TempoBlockEnv::default())
4867 .with_cfg(cfg)
4868 .with_tx(TempoTxEnv {
4869 inner: revm::context::TxEnv {
4870 gas_limit: 1_000_000,
4871 nonce,
4872 ..Default::default()
4873 },
4874 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4875 aa_calls: vec![Call {
4876 to: TxKind::Call(TEST_TARGET),
4877 value: U256::ZERO,
4878 input: Bytes::new(),
4879 }],
4880 nonce_key,
4881 ..Default::default()
4882 })),
4883 ..Default::default()
4884 })
4885 .with_new_journal(journal);
4886 TempoEvm::<_, ()>::new(ctx, ())
4887 };
4888
4889 let mut evm_nonce_zero = make_evm(cfg.clone(), 0, TEST_NONCE_KEY);
4891 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4892 let gas_nonce_zero = handler
4893 .validate_initial_tx_gas(&mut evm_nonce_zero)
4894 .unwrap();
4895
4896 let mut evm_nonce_five = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
4899 let gas_nonce_five = handler
4900 .validate_initial_tx_gas(&mut evm_nonce_five)
4901 .unwrap();
4902
4903 let gas_delta = gas_nonce_zero.initial_total_gas() - gas_nonce_five.initial_total_gas();
4906 let expected_delta = new_account_cost - EXISTING_NONCE_KEY_GAS;
4907 assert_eq!(
4908 gas_delta, expected_delta,
4909 "T1 gas difference between nonce=0 and nonce>0 should be {expected_delta} (new_account_cost - EXISTING_NONCE_KEY_GAS), got {gas_delta}"
4910 );
4911
4912 assert_ne!(
4914 gas_delta, NEW_NONCE_KEY_GAS,
4915 "T1 should NOT use pre-T1 NEW_NONCE_KEY_GAS ({NEW_NONCE_KEY_GAS}) for nonce=0 transactions"
4916 );
4917
4918 let mut evm_regular_nonce = make_evm(cfg, 0, U256::ZERO);
4920 let gas_regular = handler
4921 .validate_initial_tx_gas(&mut evm_regular_nonce)
4922 .unwrap();
4923
4924 assert_eq!(
4925 gas_nonce_zero.initial_total_gas(),
4926 gas_regular.initial_total_gas(),
4927 "nonce=0 should charge the same regardless of nonce_key (2D vs regular)"
4928 );
4929 }
4930
4931 #[test]
4942 fn test_t1_existing_2d_nonce_key_charges_5k_gas() {
4943 use crate::gas_params::tempo_gas_params;
4944 use revm::handler::Handler;
4945
4946 const BASE_INTRINSIC_GAS: u64 = 21_000;
4947 const TEST_TARGET: Address = Address::new([0xBB; 20]);
4948 const TEST_NONCE_KEY: U256 = U256::from_limbs([99, 0, 0, 0]);
4949 const SPEC: TempoHardfork = TempoHardfork::T1;
4950 const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
4951
4952 let mut cfg = CfgEnv::<TempoHardfork>::default();
4953 cfg.spec = SPEC;
4954 cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
4955
4956 let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
4957 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
4958 let ctx = Context::mainnet()
4959 .with_db(CacheDB::new(EmptyDB::default()))
4960 .with_block(TempoBlockEnv::default())
4961 .with_cfg(cfg)
4962 .with_tx(TempoTxEnv {
4963 inner: revm::context::TxEnv {
4964 gas_limit: 1_000_000,
4965 nonce,
4966 ..Default::default()
4967 },
4968 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
4969 aa_calls: vec![Call {
4970 to: TxKind::Call(TEST_TARGET),
4971 value: U256::ZERO,
4972 input: Bytes::new(),
4973 }],
4974 nonce_key,
4975 ..Default::default()
4976 })),
4977 ..Default::default()
4978 })
4979 .with_new_journal(journal);
4980 TempoEvm::<_, ()>::new(ctx, ())
4981 };
4982
4983 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
4984
4985 let mut evm_existing_key = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
4987 let gas_existing = handler
4988 .validate_initial_tx_gas(&mut evm_existing_key)
4989 .unwrap();
4990
4991 assert_eq!(
4992 gas_existing.initial_total_gas(),
4993 BASE_INTRINSIC_GAS + EXISTING_NONCE_KEY_GAS,
4994 "T1 existing 2D nonce key (nonce>0) should charge BASE + EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
4995 );
4996
4997 let mut evm_regular = make_evm(cfg, 5, U256::ZERO);
4999 let gas_regular = handler.validate_initial_tx_gas(&mut evm_regular).unwrap();
5000
5001 assert_eq!(
5002 gas_regular.initial_total_gas(),
5003 BASE_INTRINSIC_GAS,
5004 "T1 regular nonce (nonce_key=0, nonce>0) should only charge BASE intrinsic gas"
5005 );
5006
5007 let gas_delta = gas_existing.initial_total_gas() - gas_regular.initial_total_gas();
5009 assert_eq!(
5010 gas_delta, EXISTING_NONCE_KEY_GAS,
5011 "Difference between existing 2D nonce and regular nonce should be EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
5012 );
5013 }
5014
5015 mod keychain {
5016 use super::*;
5017 use alloy_signer::SignerSync;
5018 use alloy_signer_local::PrivateKeySigner;
5019 use tempo_precompiles::ACCOUNT_KEYCHAIN_ADDRESS;
5020 use tempo_primitives::transaction::{
5021 KeychainSignature, KeychainVersion, SignatureType,
5022 key_authorization::{KeyAuthorization, TokenLimit as PrimTokenLimit},
5023 };
5024
5025 fn generate_keypair() -> (PrivateKeySigner, Address) {
5026 let signer = PrivateKeySigner::random();
5027 let addr = signer.address();
5028 (signer, addr)
5029 }
5030
5031 fn sign_key_auth(
5032 signer: &PrivateKeySigner,
5033 key_auth: KeyAuthorization,
5034 ) -> tempo_primitives::transaction::SignedKeyAuthorization {
5035 let sig = signer
5036 .sign_hash_sync(&key_auth.signature_hash())
5037 .expect("signing failed");
5038 key_auth.into_signed(PrimitiveSignature::Secp256k1(sig))
5039 }
5040
5041 fn test_sig() -> PrimitiveSignature {
5042 PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature())
5043 }
5044
5045 fn make_evm(
5052 user: Address,
5053 access_key: Address,
5054 key_auth: Option<tempo_primitives::transaction::SignedKeyAuthorization>,
5055 spec: TempoHardfork,
5056 signature: Option<TempoSignature>,
5057 seed_key: bool,
5058 ) -> (
5059 TempoEvm<CacheDB<EmptyDB>, ()>,
5060 TempoEvmHandler<CacheDB<EmptyDB>, ()>,
5061 ) {
5062 let sig = signature.unwrap_or_else(|| {
5063 TempoSignature::Keychain(KeychainSignature::new(user, test_sig()))
5064 });
5065 let mut cfg = CfgEnv::<TempoHardfork>::default();
5066 cfg.spec = spec;
5067
5068 let tx = TempoTxEnv {
5069 inner: revm::context::TxEnv {
5070 caller: user,
5071 gas_limit: 1_000_000,
5072 kind: TxKind::Call(Address::ZERO),
5073 ..Default::default()
5074 },
5075 fee_token: Some(DEFAULT_FEE_TOKEN),
5076 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
5077 signature: sig,
5078 aa_calls: vec![Call {
5079 to: TxKind::Call(Address::ZERO),
5080 value: U256::ZERO,
5081 input: Bytes::new(),
5082 }],
5083 key_authorization: key_auth,
5084 signature_hash: B256::ZERO,
5085 override_key_id: Some(access_key),
5086 ..Default::default()
5087 })),
5088 ..Default::default()
5089 };
5090
5091 let ctx = Context::mainnet()
5092 .with_db(CacheDB::new(EmptyDB::default()))
5093 .with_block(TempoBlockEnv::default())
5094 .with_cfg(cfg)
5095 .with_tx(tx)
5096 .with_new_journal(create_test_journal());
5097
5098 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
5099
5100 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5101 let mut kc = AccountKeychain::new();
5102 kc.initialize().unwrap();
5103 kc.set_transaction_key(Address::ZERO).unwrap();
5104 kc.set_tx_origin(user).unwrap();
5105 if seed_key {
5106 kc.authorize_key(
5107 user,
5108 access_key,
5109 PrecompileSignatureType::Secp256k1,
5110 KeyRestrictions {
5111 expiry: u64::MAX,
5112 enforceLimits: false,
5113 limits: vec![],
5114 allowAnyCalls: true,
5115 allowedCalls: vec![],
5116 },
5117 None,
5118 )
5119 .unwrap();
5120 }
5121 });
5122
5123 (evm, TempoEvmHandler::new())
5124 }
5125
5126 #[test]
5127 fn test_key_authorization_invalid_signature_rejected() {
5128 let (_, user) = generate_keypair();
5129 let key = Address::random();
5130 let (bad_signer, _) = generate_keypair();
5131
5132 let signed = sign_key_auth(
5133 &bad_signer,
5134 KeyAuthorization::unrestricted(1337, SignatureType::Secp256k1, key),
5135 );
5136 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T2, None, true);
5137
5138 assert!(matches!(
5139 h.validate_env(&mut evm),
5140 Err(EVMError::Transaction(
5141 TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot { .. }
5142 ))
5143 ));
5144 }
5145
5146 #[test]
5147 fn test_key_authorization_mismatched_key_id_rejected() {
5148 let (signer, user) = generate_keypair();
5149 let wrong_key = Address::random();
5150 let tx_key = Address::random();
5151
5152 let signed = sign_key_auth(
5153 &signer,
5154 KeyAuthorization::unrestricted(1337, SignatureType::Secp256k1, wrong_key),
5155 );
5156 let (mut evm, h) = make_evm(user, tx_key, Some(signed), TempoHardfork::T2, None, true);
5157
5158 assert!(matches!(
5159 h.validate_env(&mut evm),
5160 Err(EVMError::Transaction(
5161 TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys
5162 ))
5163 ));
5164 }
5165
5166 #[test]
5167 fn test_key_authorization_chain_id_wildcard() {
5168 for spec in [TempoHardfork::T1B, TempoHardfork::T2] {
5169 let (signer, user) = generate_keypair();
5170 let key = Address::random();
5171 let signed = sign_key_auth(
5172 &signer,
5173 KeyAuthorization::unrestricted(0, SignatureType::Secp256k1, key),
5174 );
5175 let (mut evm, h) = make_evm(user, key, Some(signed), spec, None, false);
5176
5177 if !spec.is_t1c()
5178 && let Some(aa_env) = evm.tx.tempo_tx_env.as_mut()
5179 && let TempoSignature::Keychain(keychain_sig) = &mut aa_env.signature
5180 {
5181 keychain_sig.version = KeychainVersion::V1;
5183 }
5184 let result = h.validate_env(&mut evm);
5185 if !spec.is_t1c() {
5186 assert!(
5187 result.is_ok(),
5188 "{spec:?}: chain_id=0 wildcard should be accepted pre-T1C, got: {result:?}"
5189 );
5190 } else {
5191 assert!(
5192 result.is_err(),
5193 "{spec:?}: chain_id=0 wildcard should be rejected post-T1C, got: {result:?}"
5194 );
5195 }
5196 }
5197 }
5198
5199 #[test]
5200 fn test_key_authorization_chain_id_wrong_and_matching() {
5201 for spec in [TempoHardfork::T1B, TempoHardfork::T2] {
5203 let (signer, user) = generate_keypair();
5205 let key = Address::random();
5206 let signed = sign_key_auth(
5207 &signer,
5208 KeyAuthorization::unrestricted(99999, SignatureType::Secp256k1, key),
5209 );
5210 let (mut evm, h) = make_evm(user, key, Some(signed), spec, None, true);
5211 assert!(
5212 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default())
5213 .is_err(),
5214 "{spec:?}: wrong chain_id should be rejected"
5215 );
5216
5217 let (signer, user) = generate_keypair();
5219 let key = Address::random();
5220 let signed = sign_key_auth(
5221 &signer,
5222 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key),
5223 );
5224 let (mut evm, h) = make_evm(user, key, Some(signed), spec, None, true);
5225 let result =
5226 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5227 assert!(
5228 !matches!(&result, Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason })) if reason.contains("chain_id")),
5229 "{spec:?}: matching chain_id should be accepted, got: {result:?}"
5230 );
5231 }
5232 }
5233
5234 #[test]
5235 fn test_key_authorization_expiry_cached_for_pool_maintenance() {
5236 let (signer, user) = generate_keypair();
5237 let key = Address::random();
5238 let expiry = u64::MAX - 1;
5239
5240 let signed = sign_key_auth(
5241 &signer,
5242 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5243 .with_expiry(expiry),
5244 );
5245 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T2, None, false);
5246
5247 let _ = h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5248 assert_eq!(evm.key_expiry, Some(expiry));
5249 }
5250
5251 #[test]
5252 fn test_key_authorization_witness_rejected_before_t5() {
5253 let (signer, user) = generate_keypair();
5254 let key = Address::random();
5255 let signed = sign_key_auth(
5256 &signer,
5257 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5258 .with_witness(B256::repeat_byte(0x53)),
5259 );
5260 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T4, None, false);
5261
5262 let result = h.validate_env(&mut evm);
5263 assert!(
5264 matches!(
5265 &result,
5266 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5267 if reason.contains("before T5")
5268 ),
5269 "witness-bearing key authorization should be rejected before T5, got: {result:?}"
5270 );
5271 }
5272
5273 #[test]
5274 fn test_t5_key_authorization_witness_is_not_burned_in_state() {
5275 use tempo_precompiles::account_keychain::isKeyAuthorizationWitnessBurnedCall;
5276
5277 let (signer, user) = generate_keypair();
5278 let key = Address::random();
5279 let witness = B256::repeat_byte(0x54);
5280 let signed = sign_key_auth(
5281 &signer,
5282 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5283 .with_witness(witness),
5284 );
5285 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T5, None, false);
5286
5287 let result =
5288 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5289 assert!(
5290 result.is_ok(),
5291 "T5 witness authorization should pass: {result:?}"
5292 );
5293
5294 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5295 let keychain = AccountKeychain::new();
5296 assert!(
5297 !keychain
5298 .is_key_authorization_witness_burned(isKeyAuthorizationWitnessBurnedCall {
5299 account: user,
5300 witness,
5301 })
5302 .expect("witness read succeeds"),
5303 "T5 key authorization must not burn its witness"
5304 );
5305 });
5306 }
5307
5308 #[test]
5309 fn test_t6_admin_key_authorization_fields_rejected_before_t6() {
5310 let (signer, user) = generate_keypair();
5311 let key = Address::random();
5312 let signed = sign_key_auth(
5313 &signer,
5314 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key).into_admin(user),
5315 );
5316 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T5, None, false);
5317
5318 let result = h.validate_env(&mut evm);
5319 assert!(
5320 matches!(
5321 &result,
5322 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5323 if reason.contains("not active before T6")
5324 ),
5325 "admin key authorization fields should be rejected before T6, got: {result:?}"
5326 );
5327 }
5328
5329 #[test]
5330 fn test_t6_admin_key_authorization_rejects_account_mismatch() {
5331 let (signer, user) = generate_keypair();
5332 let key = Address::random();
5333 let wrong_account = Address::random();
5334 let signed = sign_key_auth(
5335 &signer,
5336 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5337 .into_admin(wrong_account),
5338 );
5339 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5340
5341 let result = h.validate_env(&mut evm);
5342 assert!(
5343 matches!(
5344 &result,
5345 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5346 if reason.contains("account mismatch")
5347 ),
5348 "admin key authorization should be bound to tx.caller, got: {result:?}"
5349 );
5350 }
5351
5352 #[test]
5353 fn test_t6_root_admin_key_authorization_allows_omitted_account() {
5354 let (signer, user) = generate_keypair();
5355 let key = Address::random();
5356 let mut key_auth = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key);
5357 key_auth.is_admin = true;
5358 assert_eq!(key_auth.account, None);
5359
5360 let signed = sign_key_auth(&signer, key_auth);
5361 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5362
5363 let env_result = h.validate_env(&mut evm);
5364 assert!(
5365 env_result.is_ok(),
5366 "root-signed admin key authorization should pass stateless validation, got: {env_result:?}"
5367 );
5368
5369 let result =
5370 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5371 assert!(
5372 result.is_ok(),
5373 "root-signed admin key authorization should not require account, got: {result:?}"
5374 );
5375
5376 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5377 let keychain = AccountKeychain::new();
5378 assert!(
5379 keychain
5380 .is_admin_key(user, key)
5381 .expect("admin key status read succeeds"),
5382 "root-signed admin key should be registered as admin"
5383 );
5384 });
5385 }
5386
5387 #[test]
5388 fn test_t6_root_signed_key_authorization_rejects_admin_keychain_submission() {
5389 let (root_signer, user) = generate_keypair();
5390 let (_, admin_key) = generate_keypair();
5391 let child_key = Address::random();
5392 let signed = sign_key_auth(
5393 &root_signer,
5394 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key),
5395 );
5396 let (mut evm, h) = make_evm(
5397 user,
5398 admin_key,
5399 Some(signed),
5400 TempoHardfork::T6,
5401 None,
5402 false,
5403 );
5404
5405 let env_result = h.validate_env(&mut evm);
5406 assert!(
5407 matches!(
5408 &env_result,
5409 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5410 if reason.contains("root transaction signature")
5411 ),
5412 "root-signed key authorization should require a root transaction signature, got: {env_result:?}"
5413 );
5414 }
5415
5416 #[test]
5417 fn test_t6_root_key_authorization_rejects_account_mismatch() {
5418 let (signer, user) = generate_keypair();
5419 let key = Address::random();
5420 let wrong_account = Address::random();
5421 let signed = sign_key_auth(
5422 &signer,
5423 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5424 .with_account(wrong_account),
5425 );
5426 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5427
5428 let result = h.validate_env(&mut evm);
5429 assert!(
5430 matches!(
5431 &result,
5432 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5433 if reason.contains("key authorization account mismatch")
5434 ),
5435 "root-signed key authorization should be bound to tx.caller, got: {result:?}"
5436 );
5437 }
5438
5439 #[test]
5440 fn test_t6_admin_key_authorization_rejects_restrictions() {
5441 let (signer, user) = generate_keypair();
5442 let key = Address::random();
5443 let signed = sign_key_auth(
5444 &signer,
5445 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key)
5446 .with_expiry(u64::MAX)
5447 .into_admin(user),
5448 );
5449 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T6, None, false);
5450
5451 let result = h.validate_env(&mut evm);
5452 assert!(
5453 matches!(
5454 &result,
5455 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5456 if reason.contains("cannot carry expiry")
5457 ),
5458 "admin key authorization should reject restrictions, got: {result:?}"
5459 );
5460 }
5461
5462 #[test]
5463 fn test_t6_admin_access_key_can_authorize_different_admin_key() {
5464 let (admin_signer, admin_key) = generate_keypair();
5465 let user = Address::random();
5466 let child_key = Address::random();
5467 let signed = sign_key_auth(
5468 &admin_signer,
5469 KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, child_key)
5470 .into_admin(user),
5471 );
5472 let (mut evm, h) = make_evm(
5473 user,
5474 admin_key,
5475 Some(signed),
5476 TempoHardfork::T6,
5477 None,
5478 false,
5479 );
5480
5481 let env_result = h.validate_env(&mut evm);
5482 assert!(
5483 env_result.is_ok(),
5484 "admin access key authorization should pass stateless validation, got: {env_result:?}"
5485 );
5486
5487 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5488 let mut keychain = AccountKeychain::new();
5489 keychain
5490 .authorize_admin_key(user, admin_key, PrecompileSignatureType::Secp256k1, None)
5491 .expect("root authorizes admin key");
5492 });
5493
5494 let result =
5495 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5496 assert!(
5497 result.is_ok(),
5498 "admin access key should authorize a different admin key, got: {result:?}"
5499 );
5500
5501 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5502 let keychain = AccountKeychain::new();
5503 assert!(
5504 keychain
5505 .is_admin_key(user, child_key)
5506 .expect("admin key status read succeeds"),
5507 "child key should be registered as admin"
5508 );
5509 });
5510 }
5511
5512 #[test]
5513 fn test_t6_admin_key_authorization_rejects_different_transaction_admin_key() {
5514 let (authorization_signer, authorization_admin_key) = generate_keypair();
5515 let (_, tx_admin_key) = generate_keypair();
5516 let user = Address::random();
5517 let child_key = Address::random();
5518 let signed = sign_key_auth(
5519 &authorization_signer,
5520 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5521 .with_account(user),
5522 );
5523 let (mut evm, h) = make_evm(
5524 user,
5525 tx_admin_key,
5526 Some(signed),
5527 TempoHardfork::T6,
5528 None,
5529 false,
5530 );
5531
5532 let result = h.validate_env(&mut evm);
5533 assert!(
5534 matches!(
5535 &result,
5536 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5537 if reason.contains("must be signed by transaction key")
5538 ),
5539 "admin-signed key authorization must use the transaction admin key; auth signer {authorization_admin_key}, tx signer {tx_admin_key}, got: {result:?}"
5540 );
5541 }
5542
5543 #[test]
5544 fn test_t6_admin_access_key_non_admin_authorization_requires_account_binding() {
5545 let (admin_signer, admin_key) = generate_keypair();
5546 let user = Address::random();
5547 let child_key = Address::random();
5548 let signed = sign_key_auth(
5549 &admin_signer,
5550 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key),
5551 );
5552 let (mut evm, h) = make_evm(
5553 user,
5554 admin_key,
5555 Some(signed),
5556 TempoHardfork::T6,
5557 None,
5558 false,
5559 );
5560
5561 let result = h.validate_env(&mut evm);
5562 assert!(
5563 matches!(
5564 &result,
5565 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5566 if reason.contains("admin-signed key authorization account mismatch")
5567 ),
5568 "admin-signed non-admin authorization without account binding should fail in validate_env, got: {result:?}"
5569 );
5570 }
5571
5572 #[test]
5573 fn test_t6_admin_key_authorization_rejects_admin_signature_type_mismatch() {
5574 let (admin_signer, admin_key) = generate_keypair();
5575 let user = Address::random();
5576 let child_key = Address::random();
5577 let signed = sign_key_auth(
5578 &admin_signer,
5579 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5580 .with_account(user),
5581 );
5582 let (mut evm, h) = make_evm(
5583 user,
5584 admin_key,
5585 Some(signed),
5586 TempoHardfork::T6,
5587 None,
5588 false,
5589 );
5590
5591 let env_result = h.validate_env(&mut evm);
5592 assert!(
5593 env_result.is_ok(),
5594 "admin-signed key authorization should pass stateless validation, got: {env_result:?}"
5595 );
5596
5597 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5598 let mut keychain = AccountKeychain::new();
5599 keychain
5600 .authorize_admin_key(user, admin_key, PrecompileSignatureType::WebAuthn, None)
5601 .expect("root authorizes WebAuthn admin key");
5602 });
5603
5604 let result =
5605 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5606 assert!(
5607 matches!(
5608 &result,
5609 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5610 if reason.contains("SignatureTypeMismatch")
5611 ),
5612 "admin-signed key authorization should reject sidecar signature type mismatch, got: {result:?}"
5613 );
5614 }
5615
5616 #[test]
5617 fn test_t6_admin_access_key_non_admin_authorization_rejects_account_replay() {
5618 use tempo_precompiles::account_keychain::getKeyCall;
5619
5620 let (admin_signer, admin_key) = generate_keypair();
5621 let alice = Address::random();
5622 let bob = Address::random();
5623 let child_key = Address::random();
5624 let signed = sign_key_auth(
5625 &admin_signer,
5626 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5627 .with_account(alice),
5628 );
5629
5630 let (mut alice_evm, alice_handler) = make_evm(
5631 alice,
5632 admin_key,
5633 Some(signed.clone()),
5634 TempoHardfork::T6,
5635 None,
5636 false,
5637 );
5638 let alice_env_result = alice_handler.validate_env(&mut alice_evm);
5639 assert!(
5640 alice_env_result.is_ok(),
5641 "account-bound authorization should pass Alice stateless validation, got: {alice_env_result:?}"
5642 );
5643
5644 StorageCtx::enter_ctx(&mut alice_evm.inner.ctx, StorageActions::disabled(), || {
5645 let mut keychain = AccountKeychain::new();
5646 keychain
5647 .authorize_admin_key(alice, admin_key, PrecompileSignatureType::Secp256k1, None)
5648 .expect("root authorizes Alice admin key");
5649 });
5650
5651 let alice_result = alice_handler
5652 .validate_against_state_and_deduct_caller(&mut alice_evm, &mut Default::default());
5653 assert!(
5654 alice_result.is_ok(),
5655 "account-bound admin-signed non-admin authorization should pass for Alice, got: {alice_result:?}"
5656 );
5657 StorageCtx::enter_ctx(&mut alice_evm.inner.ctx, StorageActions::disabled(), || {
5658 let keychain = AccountKeychain::new();
5659 let key = keychain
5660 .get_key(getKeyCall {
5661 account: alice,
5662 keyId: child_key,
5663 })
5664 .expect("child key read succeeds");
5665 assert_eq!(key.keyId, child_key, "child key should be registered");
5666 assert!(
5667 !keychain
5668 .is_admin_key(alice, child_key)
5669 .expect("admin key status read succeeds"),
5670 "child key should not be admin"
5671 );
5672 });
5673
5674 let (mut bob_evm, bob_handler) =
5675 make_evm(bob, admin_key, Some(signed), TempoHardfork::T6, None, false);
5676
5677 let bob_result = bob_handler.validate_env(&mut bob_evm);
5678 assert!(
5679 matches!(
5680 &bob_result,
5681 Err(EVMError::Transaction(TempoInvalidTransaction::KeychainValidationFailed { reason }))
5682 if reason.contains("key authorization account mismatch")
5683 ),
5684 "Alice-bound authorization should not replay for Bob, got: {bob_result:?}"
5685 );
5686 }
5687
5688 #[test]
5689 fn test_t6_admin_delegation_does_not_apply_child_fee_limit() {
5690 let (admin_signer, admin_key) = generate_keypair();
5691 let user = Address::random();
5692 let child_key = Address::random();
5693 let gas_limit = 100_000;
5694 let fee = U256::from(gas_limit);
5695 let child_spending_limit = fee - U256::ONE;
5696
5697 let signed = sign_key_auth(
5698 &admin_signer,
5699 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5700 .with_limits(vec![PrimTokenLimit {
5701 token: DEFAULT_FEE_TOKEN,
5702 limit: child_spending_limit,
5703 period: 60,
5704 }])
5705 .with_account(user),
5706 );
5707 let (mut evm, h) = make_evm(
5708 user,
5709 admin_key,
5710 Some(signed),
5711 TempoHardfork::T6,
5712 None,
5713 false,
5714 );
5715 evm.inner.ctx.tx.inner.gas_limit = gas_limit;
5716 evm.inner.ctx.tx.inner.gas_price = 1_000_000_000_000;
5717 evm.inner.ctx.tx.inner.gas_priority_fee = Some(1_000_000_000_000);
5718
5719 let env_result = h.validate_env(&mut evm);
5720 assert!(
5721 env_result.is_ok(),
5722 "admin delegation should pass stateless validation, got: {env_result:?}"
5723 );
5724
5725 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5726 TIP20Setup::path_usd(user)
5727 .with_issuer(user)
5728 .with_mint(user, fee * U256::from(2))
5729 .apply()
5730 .expect("pathUSD setup succeeds");
5731
5732 let mut keychain = AccountKeychain::new();
5733 keychain
5734 .authorize_admin_key(user, admin_key, PrecompileSignatureType::Secp256k1, None)
5735 .expect("root authorizes admin key");
5736 });
5737
5738 let result =
5739 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5740 assert!(
5741 result.is_ok(),
5742 "admin delegation should not precharge fees against child key limits, got: {result:?}"
5743 );
5744 }
5745
5746 #[test]
5747 fn test_t6_admin_delegation_preserves_admin_transaction_key() {
5748 use tempo_precompiles::account_keychain::getTransactionKeyCall;
5749
5750 let (admin_signer, admin_key) = generate_keypair();
5751 let user = Address::random();
5752 let child_key = Address::random();
5753 let signed = sign_key_auth(
5754 &admin_signer,
5755 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, child_key)
5756 .with_account(user),
5757 );
5758 let (mut evm, h) = make_evm(
5759 user,
5760 admin_key,
5761 Some(signed),
5762 TempoHardfork::T6,
5763 None,
5764 false,
5765 );
5766
5767 let env_result = h.validate_env(&mut evm);
5768 assert!(
5769 env_result.is_ok(),
5770 "admin delegation should pass stateless validation, got: {env_result:?}"
5771 );
5772
5773 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5774 let mut keychain = AccountKeychain::new();
5775 keychain
5776 .authorize_admin_key(user, admin_key, PrecompileSignatureType::Secp256k1, None)
5777 .expect("root authorizes admin key");
5778 });
5779
5780 let result =
5781 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5782 assert!(
5783 result.is_ok(),
5784 "admin delegation should pass, got: {result:?}"
5785 );
5786
5787 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5788 let keychain = AccountKeychain::new();
5789 let transaction_key = keychain
5790 .get_transaction_key(getTransactionKeyCall {}, user)
5791 .expect("transaction key read succeeds");
5792 assert_eq!(
5793 transaction_key, admin_key,
5794 "admin delegation must preserve the signer key as transaction key"
5795 );
5796 });
5797 }
5798
5799 #[test]
5800 fn test_keychain_signature_with_valid_authorized_key() {
5801 let (mut evm, h) = make_evm(
5802 Address::repeat_byte(0x11),
5803 Address::repeat_byte(0x22),
5804 None,
5805 TempoHardfork::T2,
5806 None,
5807 true,
5808 );
5809
5810 let result =
5811 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5812 assert!(
5813 !matches!(
5814 result,
5815 Err(EVMError::Transaction(
5816 TempoInvalidTransaction::KeychainValidationFailed { .. }
5817 ))
5818 ),
5819 "Valid authorized key should pass, got: {result:?}"
5820 );
5821 }
5822
5823 #[test]
5824 fn test_keychain_version_rejection() {
5825 let caller = Address::random();
5826
5827 let v1 = TempoSignature::Keychain(KeychainSignature::new_v1(caller, test_sig()));
5829 let (mut evm, h) = make_evm(
5830 caller,
5831 Address::ZERO,
5832 None,
5833 TempoHardfork::T2,
5834 Some(v1),
5835 false,
5836 );
5837 assert!(matches!(
5838 h.validate_env(&mut evm),
5839 Err(EVMError::Transaction(
5840 TempoInvalidTransaction::LegacyKeychainSignature
5841 ))
5842 ));
5843
5844 let v2 = TempoSignature::Keychain(KeychainSignature::new(caller, test_sig()));
5846 let (mut evm, h) = make_evm(
5847 caller,
5848 Address::ZERO,
5849 None,
5850 TempoHardfork::T1B,
5851 Some(v2),
5852 false,
5853 );
5854 assert!(matches!(
5855 h.validate_env(&mut evm),
5856 Err(EVMError::Transaction(
5857 TempoInvalidTransaction::V2KeychainBeforeActivation
5858 ))
5859 ));
5860 }
5861
5862 #[test]
5863 fn test_key_authorization_without_existing_key_passes() {
5864 let (signer, user) = generate_keypair();
5865 let key = Address::random();
5866 let signed = sign_key_auth(
5867 &signer,
5868 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key),
5869 );
5870 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T2, None, false);
5871
5872 let result =
5873 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5874 assert!(
5875 !matches!(
5876 result,
5877 Err(EVMError::Transaction(
5878 TempoInvalidTransaction::KeychainValidationFailed { .. }
5879 | TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys
5880 | TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot { .. }
5881 | TempoInvalidTransaction::KeychainPrecompileError { .. }
5882 ))
5883 ),
5884 "Same-tx auth+use should pass when key does not exist, got: {result:?}"
5885 );
5886 }
5887
5888 #[test]
5889 fn test_same_tx_key_authorization_rejects_fee_above_new_limit_before_auth() {
5890 let (signer, user) = generate_keypair();
5891 let key = Address::random();
5892 let gas_limit = 100_000;
5893 let fee = U256::from(gas_limit);
5894 let spending_limit = fee - U256::ONE;
5895
5896 let signed = sign_key_auth(
5897 &signer,
5898 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key).with_limits(vec![
5899 PrimTokenLimit {
5900 token: DEFAULT_FEE_TOKEN,
5901 limit: spending_limit,
5902 period: 60,
5903 },
5904 ]),
5905 );
5906 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T3, None, false);
5907 evm.inner.ctx.tx.inner.gas_limit = gas_limit;
5908 evm.inner.ctx.tx.inner.gas_price = 1_000_000_000_000;
5909 evm.inner.ctx.tx.inner.gas_priority_fee = Some(1_000_000_000_000);
5910
5911 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5912 TIP20Setup::path_usd(user)
5913 .with_issuer(user)
5914 .with_mint(user, fee * U256::from(2))
5915 .apply()
5916 .expect("pathUSD setup succeeds");
5917 });
5918
5919 let result =
5920 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5921
5922 assert!(
5923 matches!(
5924 &result,
5925 Err(EVMError::Transaction(TempoInvalidTransaction::CollectFeePreTx(
5926 FeePaymentError::Other(reason)
5927 ))) if reason.contains("SpendingLimitExceeded")
5928 ),
5929 "same-tx auth+use should reject fee above the new key limit before auth, got: {result:?}"
5930 );
5931 assert_eq!(evm.collected_fee, U256::ZERO);
5932 assert!(
5933 evm.inner
5934 .ctx
5935 .journaled_state
5936 .inner
5937 .logs
5938 .iter()
5939 .all(|log| log.address != ACCOUNT_KEYCHAIN_ADDRESS),
5940 "fee-limit rejection must happen before key authorization emits events"
5941 );
5942 }
5943
5944 #[test]
5945 fn test_stale_collected_fee_not_charged_to_zero_fee_same_tx_auth_use() {
5946 let (signer, user) = generate_keypair();
5947 let key = Address::random();
5948 let stale_fee = U256::from(100_000);
5949 let spending_limit = stale_fee - U256::ONE;
5950
5951 let signed = sign_key_auth(
5952 &signer,
5953 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, key).with_limits(vec![
5954 PrimTokenLimit {
5955 token: DEFAULT_FEE_TOKEN,
5956 limit: spending_limit,
5957 period: 60,
5958 },
5959 ]),
5960 );
5961 let (mut evm, h) = make_evm(user, key, Some(signed), TempoHardfork::T3, None, false);
5962 evm.collected_fee = stale_fee;
5963 evm.inner.ctx.tx.inner.gas_limit = 100_000;
5964 evm.inner.ctx.tx.inner.gas_price = 0;
5965 evm.inner.ctx.tx.inner.gas_priority_fee = Some(0);
5966
5967 StorageCtx::enter_ctx(&mut evm.inner.ctx, StorageActions::disabled(), || {
5968 TIP20Setup::path_usd(user)
5969 .with_issuer(user)
5970 .with_mint(user, stale_fee * U256::from(2))
5971 .apply()
5972 .expect("pathUSD setup succeeds");
5973 });
5974
5975 h.validate_env(&mut evm)
5976 .expect("zero-fee same-tx auth/use env validation should pass");
5977 assert_eq!(evm.collected_fee, U256::ZERO);
5978
5979 let result =
5980 h.validate_against_state_and_deduct_caller(&mut evm, &mut Default::default());
5981
5982 assert!(
5983 result.is_ok(),
5984 "zero-fee same-tx auth/use must not charge stale fee, got: {result:?}"
5985 );
5986 assert_eq!(evm.collected_fee, U256::ZERO);
5987 }
5988 }
5989
5990 #[test]
5995 fn test_state_gas_standard_create_tx_populates_initial_state_gas() {
5996 let gas_params =
5998 crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
5999 let initcode = Bytes::from(vec![0x60, 0x80]);
6000
6001 let init_gas = gas_params.initial_tx_gas(
6002 &initcode, true, 0, 0, 0,
6004 );
6005
6006 let expected_state_gas = gas_params.create_state_gas();
6007
6008 assert!(
6009 expected_state_gas > 0,
6010 "State gas constants should be non-zero"
6011 );
6012 assert_eq!(
6013 init_gas.initial_state_gas,
6014 expected_state_gas,
6015 "CREATE tx should have initial_state_gas = create_state_gas ({})",
6016 gas_params.create_state_gas()
6017 );
6018 }
6019
6020 #[test]
6022 fn test_state_gas_standard_call_tx_zero_initial_state_gas() {
6023 let gas_params = tempo_gas_params(TempoHardfork::T4);
6024 let calldata = Bytes::from(vec![1, 2, 3]);
6025
6026 let init_gas = gas_params.initial_tx_gas(
6027 &calldata, false, 0, 0, 0,
6029 );
6030
6031 assert_eq!(
6032 init_gas.initial_state_gas, 0,
6033 "CALL tx should have zero initial_state_gas"
6034 );
6035 }
6036
6037 #[test]
6039 fn test_state_gas_aa_create_tx_populates_initial_state_gas() {
6040 let gas_params = tempo_gas_params(TempoHardfork::T4);
6041 let initcode = Bytes::from(vec![0x60, 0x80]);
6042
6043 let call = Call {
6044 to: TxKind::Create,
6045 value: U256::ZERO,
6046 input: initcode,
6047 };
6048
6049 let aa_env = TempoBatchCallEnv {
6050 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6051 alloy_primitives::Signature::test_signature(),
6052 )),
6053 aa_calls: vec![call],
6054 key_authorization: None,
6055 signature_hash: B256::ZERO,
6056 ..Default::default()
6057 };
6058
6059 let gas = calculate_aa_batch_intrinsic_gas(
6060 &aa_env,
6061 &gas_params,
6062 None::<std::iter::Empty<&AccessListItem>>,
6063 TempoHardfork::T4,
6064 )
6065 .unwrap();
6066
6067 let expected_state_gas = gas_params.create_state_gas();
6068
6069 assert_eq!(
6070 gas.initial_state_gas, expected_state_gas,
6071 "AA CREATE tx should have initial_state_gas = create_state_gas"
6072 );
6073 }
6074
6075 #[test]
6077 fn test_state_gas_aa_call_tx_zero_initial_state_gas() {
6078 let gas_params = tempo_gas_params(TempoHardfork::T4);
6079 let calldata = Bytes::from(vec![1, 2, 3]);
6080
6081 let call = Call {
6082 to: TxKind::Call(Address::random()),
6083 value: U256::ZERO,
6084 input: calldata,
6085 };
6086
6087 let aa_env = TempoBatchCallEnv {
6088 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6089 alloy_primitives::Signature::test_signature(),
6090 )),
6091 aa_calls: vec![call],
6092 key_authorization: None,
6093 signature_hash: B256::ZERO,
6094 ..Default::default()
6095 };
6096
6097 let gas = calculate_aa_batch_intrinsic_gas(
6098 &aa_env,
6099 &gas_params,
6100 None::<std::iter::Empty<&AccessListItem>>,
6101 TempoHardfork::T4,
6102 )
6103 .unwrap();
6104
6105 assert_eq!(
6106 gas.initial_state_gas, 0,
6107 "AA CALL tx should have zero initial_state_gas"
6108 );
6109 }
6110
6111 #[test]
6114 fn test_state_gas_validate_initial_tx_gas_create_t4() {
6115 let initcode = Bytes::from(vec![0x60, 0x80]);
6116 let mut test = TestHandlerEvm::tx(TempoHardfork::T4, |tx_env| {
6117 tx_env.inner.gas_limit = 60_000_000;
6118 tx_env.inner.kind = TxKind::Create;
6119 tx_env.inner.data = initcode;
6120 });
6121 let init_gas = test.validate_initial_tx_gas();
6122
6123 let expected_state_gas =
6126 test.gas_params().create_state_gas() + test.gas_params().new_account_state_gas();
6127
6128 assert_eq!(
6129 init_gas.initial_state_gas, expected_state_gas,
6130 "T4 CREATE tx with nonce==0 should have create_state_gas + new_account_state_gas"
6131 );
6132 }
6133
6134 #[test]
6137 fn test_state_gas_tx_gas_limit_above_cap_allowed() {
6138 let calldata = Bytes::from(vec![1, 2, 3]);
6139
6140 let tx_env = TempoTxEnv {
6141 inner: revm::context::TxEnv {
6142 gas_limit: 60_000_000,
6143 kind: TxKind::Call(Address::random()),
6144 data: calldata,
6145 ..Default::default()
6146 },
6147 ..Default::default()
6148 };
6149
6150 let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T4, tx_env, |cfg| {
6152 cfg.tx_gas_limit_cap = Some(30_000_000);
6153 cfg.enable_amsterdam_eip8037 = true;
6154 cfg.gas_params =
6155 crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6156 });
6157
6158 let result = test.validate_env();
6160 assert!(
6161 result.is_ok(),
6162 "With enable_amsterdam_eip8037=true, tx gas limit above cap should be allowed, got: {:?}",
6163 result.err()
6164 );
6165 }
6166
6167 #[test]
6169 fn test_state_gas_tx_gas_limit_above_cap_rejected_pre_t4() {
6170 let calldata = Bytes::from(vec![1, 2, 3]);
6171
6172 let tx_env = TempoTxEnv {
6173 inner: revm::context::TxEnv {
6174 gas_limit: 60_000_000, kind: TxKind::Call(Address::random()),
6176 data: calldata,
6177 ..Default::default()
6178 },
6179 ..Default::default()
6180 };
6181
6182 let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T1, tx_env, |cfg| {
6183 cfg.tx_gas_limit_cap = Some(30_000_000);
6184 });
6185
6186 let result = test.validate_env();
6188 assert!(
6189 result.is_err(),
6190 "With enable_amsterdam_eip8037=false, tx gas limit above cap should be rejected"
6191 );
6192 }
6193
6194 #[test]
6196 fn test_subblock_fee_payment_halt_clamps_to_gas_cap_t4() {
6197 const CAP: u64 = 30_000_000;
6198 const TX_GAS_LIMIT: u64 = 60_000_000;
6199
6200 let aa_env = TempoBatchCallEnv {
6201 subblock_transaction: true,
6202 ..Default::default()
6203 };
6204 let tx_env = TempoTxEnv {
6205 inner: revm::context::TxEnv {
6206 gas_limit: TX_GAS_LIMIT,
6207 kind: TxKind::Call(Address::random()),
6208 ..Default::default()
6209 },
6210 tempo_tx_env: Some(Box::new(aa_env)),
6211 ..Default::default()
6212 };
6213
6214 let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T4, tx_env, |cfg| {
6215 cfg.tx_gas_limit_cap = Some(CAP);
6216 cfg.enable_amsterdam_eip8037 = true;
6217 cfg.gas_params =
6218 crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6219 });
6220
6221 assert!(
6223 test.cfg().enable_amsterdam_eip8037,
6224 "T4 must enable enable_amsterdam_eip8037 for this regression to apply"
6225 );
6226
6227 let err = EVMError::Transaction(TempoInvalidTransaction::EthInvalidTransaction(
6228 InvalidTransaction::LackOfFundForMaxFee {
6229 fee: Box::new(U256::ZERO),
6230 balance: Box::new(U256::ZERO),
6231 },
6232 ));
6233
6234 let result = test
6235 .handler
6236 .catch_error(&mut test.evm, err)
6237 .expect("subblock fee-payment failure must be converted to a halt, not a hard error");
6238
6239 match result {
6240 ExecutionResult::Halt { reason, gas, .. } => {
6241 assert!(
6242 matches!(reason, TempoHaltReason::SubblockTxFeePayment),
6243 "expected SubblockTxFeePayment halt, got {reason:?}"
6244 );
6245 assert_eq!(
6246 gas.total_gas_spent(),
6247 CAP,
6248 "regular gas charged on subblock fee-payment halt must be clamped to \
6249 tx_gas_limit_cap (got {} for tx.gas_limit={} cap={})",
6250 gas.total_gas_spent(),
6251 TX_GAS_LIMIT,
6252 CAP,
6253 );
6254 assert_eq!(
6255 gas.state_gas_spent_final(),
6256 0,
6257 "halt reports zero state gas"
6258 );
6259 }
6260 other => panic!("expected ExecutionResult::Halt, got {other:?}"),
6261 }
6262 }
6263
6264 #[test]
6265 fn test_subblock_paused_fee_token_halts_as_fee_payment_failure() {
6266 let aa_env = TempoBatchCallEnv {
6267 subblock_transaction: true,
6268 ..Default::default()
6269 };
6270 let tx_env = TempoTxEnv {
6271 inner: revm::context::TxEnv {
6272 gas_limit: 100_000,
6273 kind: TxKind::Call(Address::random()),
6274 ..Default::default()
6275 },
6276 tempo_tx_env: Some(Box::new(aa_env)),
6277 ..Default::default()
6278 };
6279
6280 let mut test = TestHandlerEvm::with_cfg(TempoHardfork::T4, tx_env, |cfg| {
6281 cfg.tx_gas_limit_cap = Some(30_000_000);
6282 cfg.enable_amsterdam_eip8037 = true;
6283 cfg.gas_params =
6284 crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6285 });
6286
6287 let err = EVMError::Transaction(TempoInvalidTransaction::FeeTokenPaused {
6288 address: PATH_USD_ADDRESS,
6289 });
6290
6291 let result = test
6292 .handler
6293 .catch_error(&mut test.evm, err)
6294 .expect("subblock paused fee-token failure must be converted to a halt");
6295
6296 match result {
6297 ExecutionResult::Halt { reason, gas, .. } => {
6298 assert!(
6299 matches!(reason, TempoHaltReason::SubblockTxFeePayment),
6300 "expected SubblockTxFeePayment halt, got {reason:?}"
6301 );
6302 assert_eq!(gas.total_gas_spent(), 100_000);
6303 assert_eq!(
6304 gas.state_gas_spent_final(),
6305 0,
6306 "halt reports zero state gas"
6307 );
6308 }
6309 other => panic!("expected ExecutionResult::Halt, got {other:?}"),
6310 }
6311 }
6312
6313 #[test]
6317 fn test_state_gas_backward_compat_t1_no_state_gas_enabled() {
6318 let mut cfg = CfgEnv::<TempoHardfork>::default();
6319 cfg.spec = TempoHardfork::T1;
6320 cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
6321
6322 assert!(
6323 !cfg.enable_amsterdam_eip8037,
6324 "Pre-T4 should NOT have enable_amsterdam_eip8037"
6325 );
6326
6327 let calldata = Bytes::from(vec![1, 2, 3]);
6328
6329 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
6330 let tx_env = TempoTxEnv {
6331 inner: revm::context::TxEnv {
6332 gas_limit: 1_000_000,
6333 kind: TxKind::Call(Address::random()),
6334 data: calldata,
6335 ..Default::default()
6336 },
6337 ..Default::default()
6338 };
6339
6340 let ctx = Context::mainnet()
6341 .with_db(CacheDB::new(EmptyDB::default()))
6342 .with_block(TempoBlockEnv::default())
6343 .with_cfg(cfg)
6344 .with_tx(tx_env)
6345 .with_new_journal(journal);
6346 let mut evm = TempoEvm::<_, ()>::new(ctx, ());
6347 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
6348
6349 let init_gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
6350
6351 assert_eq!(init_gas.initial_state_gas, 0);
6353 }
6354
6355 #[test]
6358 fn test_state_gas_aa_mixed_batch_create_and_call() {
6359 let gas_params = tempo_gas_params(TempoHardfork::T4);
6360 let calldata = Bytes::from(vec![1, 2, 3]);
6361 let initcode = Bytes::from(vec![0x60, 0x80]);
6362
6363 let calls = vec![
6364 Call {
6365 to: TxKind::Call(Address::random()),
6366 value: U256::ZERO,
6367 input: calldata,
6368 },
6369 Call {
6370 to: TxKind::Create,
6371 value: U256::ZERO,
6372 input: initcode,
6373 },
6374 ];
6375
6376 let aa_env = TempoBatchCallEnv {
6377 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6378 alloy_primitives::Signature::test_signature(),
6379 )),
6380 aa_calls: calls,
6381 key_authorization: None,
6382 signature_hash: B256::ZERO,
6383 ..Default::default()
6384 };
6385
6386 let gas = calculate_aa_batch_intrinsic_gas(
6387 &aa_env,
6388 &gas_params,
6389 None::<std::iter::Empty<&AccessListItem>>,
6390 TempoHardfork::T4,
6391 )
6392 .unwrap();
6393
6394 let expected_state_gas = gas_params.create_state_gas();
6396
6397 assert_eq!(
6398 gas.initial_state_gas, expected_state_gas,
6399 "Mixed batch should have state gas only from CREATE call"
6400 );
6401 }
6402
6403 #[test]
6405 fn test_state_gas_aa_multiple_create_calls() {
6406 let gas_params = tempo_gas_params(TempoHardfork::T4);
6407 let initcode = Bytes::from(vec![0x60, 0x80]);
6408
6409 let calls = vec![
6410 Call {
6411 to: TxKind::Create,
6412 value: U256::ZERO,
6413 input: initcode.clone(),
6414 },
6415 Call {
6416 to: TxKind::Create,
6417 value: U256::ZERO,
6418 input: initcode,
6419 },
6420 ];
6421
6422 let aa_env = TempoBatchCallEnv {
6423 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6424 alloy_primitives::Signature::test_signature(),
6425 )),
6426 aa_calls: calls,
6427 key_authorization: None,
6428 signature_hash: B256::ZERO,
6429 ..Default::default()
6430 };
6431
6432 let gas = calculate_aa_batch_intrinsic_gas(
6433 &aa_env,
6434 &gas_params,
6435 None::<std::iter::Empty<&AccessListItem>>,
6436 TempoHardfork::T4,
6437 )
6438 .unwrap();
6439
6440 let per_create_state_gas = gas_params.create_state_gas();
6442
6443 assert_eq!(
6444 gas.initial_state_gas,
6445 per_create_state_gas * 2,
6446 "Multiple CREATE calls should accumulate initial_state_gas"
6447 );
6448 }
6449
6450 #[test]
6454 fn test_state_gas_multi_call_per_call_init_has_zero_state_gas() {
6455 let zero_init_gas = InitialAndFloorGas::new(0, 0);
6456 assert_eq!(
6457 zero_init_gas.initial_state_gas, 0,
6458 "Per-call init gas in multi-call must have zero initial_state_gas; \
6459 state gas is deducted once upfront, not per call"
6460 );
6461 }
6462
6463 #[test]
6467 fn test_state_gas_multi_call_corrected_gas_success_preserves_state_gas() {
6468 let gas_limit: u64 = 1_000_000;
6469 let total_gas_spent: u64 = 400_000;
6470 let accumulated_state_gas: i64 = 150_000;
6471 let accumulated_refund: i64 = 5_000;
6472
6473 let mut corrected_gas = Gas::new_spent_with_reservoir(gas_limit, 0);
6475 corrected_gas.erase_cost(gas_limit - total_gas_spent);
6476 corrected_gas.set_refund(accumulated_refund);
6477 corrected_gas.set_state_gas_spent(accumulated_state_gas);
6478
6479 assert_eq!(
6480 corrected_gas.total_gas_spent(),
6481 total_gas_spent,
6482 "Flattened gas must have correct spent"
6483 );
6484 assert_eq!(
6485 corrected_gas.used(),
6486 total_gas_spent - accumulated_refund as u64,
6487 "Flattened gas must have correct used (spent - refunded)"
6488 );
6489 assert_eq!(
6490 corrected_gas.state_gas_spent(),
6491 accumulated_state_gas,
6492 "Corrected gas must preserve accumulated state_gas_spent"
6493 );
6494 assert_eq!(
6495 corrected_gas.reservoir(),
6496 0,
6497 "Flattened gas must have zero reservoir"
6498 );
6499 }
6500
6501 #[test]
6503 fn test_state_gas_aa_auth_list_nonce_zero() {
6504 let gas_params =
6506 crate::gas_params::tempo_gas_params_with_amsterdam(TempoHardfork::T4, true);
6507
6508 let aa_env = TempoBatchCallEnv {
6509 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6510 alloy_primitives::Signature::test_signature(),
6511 )),
6512 aa_calls: vec![Call {
6513 to: TxKind::Call(Address::random()),
6514 value: U256::ZERO,
6515 input: Bytes::from(vec![1, 2, 3]),
6516 }],
6517 tempo_authorization_list: vec![RecoveredTempoAuthorization::new(
6518 TempoSignedAuthorization::new_unchecked(
6519 alloy_eips::eip7702::Authorization {
6520 chain_id: U256::ONE,
6521 address: Address::random(),
6522 nonce: 0,
6523 },
6524 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6525 alloy_primitives::Signature::test_signature(),
6526 )),
6527 ),
6528 )],
6529 ..Default::default()
6530 };
6531
6532 let gas = calculate_aa_batch_intrinsic_gas(
6533 &aa_env,
6534 &gas_params,
6535 None::<std::iter::Empty<&AccessListItem>>,
6536 TempoHardfork::T4,
6537 )
6538 .unwrap();
6539
6540 assert_eq!(
6543 gas.initial_state_gas,
6544 225_000 + 225_000,
6545 "Auth list entry should track per-auth state gas (225k) + nonce==0 account creation state gas (225k)"
6546 );
6547 }
6548
6549 #[test]
6551 fn test_state_gas_aa_nonce_zero_new_account() {
6552 let aa_env = TempoBatchCallEnv {
6553 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6554 alloy_primitives::Signature::test_signature(),
6555 )),
6556 aa_calls: vec![Call {
6557 to: TxKind::Call(Address::random()),
6558 value: U256::ZERO,
6559 input: Bytes::from(vec![1, 2, 3]),
6560 }],
6561 nonce_key: U256::ONE,
6562 ..Default::default()
6563 };
6564
6565 let mut test = TestHandlerEvm::aa(TempoHardfork::T4, aa_env, |tx_env| {
6566 tx_env.inner.gas_limit = 60_000_000;
6567 tx_env.inner.nonce = 0;
6568 });
6569 let init_gas = test.validate_initial_tx_gas();
6570
6571 assert_eq!(
6572 init_gas.initial_state_gas,
6573 test.gas_params().new_account_state_gas(),
6574 "AA tx with nonce==0 should track new_account_state_gas in T4"
6575 );
6576 }
6577
6578 #[test]
6580 fn test_state_gas_auth_list_zero_on_t1() {
6581 let gas_params = tempo_gas_params(TempoHardfork::T1);
6582 assert_eq!(
6583 gas_params.new_account_state_gas(),
6584 0,
6585 "Auth account creation state gas must be zero on T1"
6586 );
6587
6588 let aa_env = TempoBatchCallEnv {
6589 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6590 alloy_primitives::Signature::test_signature(),
6591 )),
6592 aa_calls: vec![Call {
6593 to: TxKind::Call(Address::random()),
6594 value: U256::ZERO,
6595 input: Bytes::from(vec![1, 2, 3]),
6596 }],
6597 tempo_authorization_list: vec![RecoveredTempoAuthorization::new(
6598 TempoSignedAuthorization::new_unchecked(
6599 alloy_eips::eip7702::Authorization {
6600 chain_id: U256::ONE,
6601 address: Address::random(),
6602 nonce: 0,
6603 },
6604 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6605 alloy_primitives::Signature::test_signature(),
6606 )),
6607 ),
6608 )],
6609 ..Default::default()
6610 };
6611
6612 let gas = calculate_aa_batch_intrinsic_gas(
6613 &aa_env,
6614 &gas_params,
6615 None::<std::iter::Empty<&AccessListItem>>,
6616 TempoHardfork::T1,
6617 )
6618 .unwrap();
6619
6620 assert_eq!(
6621 gas.initial_state_gas, 0,
6622 "T1 auth list nonce==0 should have zero initial_state_gas"
6623 );
6624 }
6625
6626 #[test]
6628 fn test_state_gas_standard_tx_nonce_zero_t4() {
6629 let calldata = Bytes::from(vec![1, 2, 3]);
6630 let mut test = TestHandlerEvm::tx(TempoHardfork::T4, |tx_env| {
6631 tx_env.inner.gas_limit = 60_000_000;
6632 tx_env.inner.kind = TxKind::Call(Address::random());
6633 tx_env.inner.nonce = 0;
6634 tx_env.inner.data = calldata;
6635 });
6636 let init_gas = test.validate_initial_tx_gas();
6637
6638 assert_eq!(
6639 init_gas.initial_state_gas,
6640 test.gas_params().new_account_state_gas(),
6641 "T4 standard tx with nonce==0 should track new_account_state_gas"
6642 );
6643 }
6644
6645 #[test]
6647 fn test_state_gas_standard_tx_nonce_zero_t1_no_state_gas() {
6648 let calldata = Bytes::from(vec![1, 2, 3]);
6649
6650 let mut test = TestHandlerEvm::tx(TempoHardfork::T1, |tx_env| {
6651 tx_env.inner.gas_limit = 60_000_000;
6652 tx_env.inner.kind = TxKind::Call(Address::random());
6653 tx_env.inner.nonce = 0;
6654 tx_env.inner.data = calldata;
6655 });
6656 let init_gas = test.validate_initial_tx_gas();
6657
6658 assert_eq!(
6659 init_gas.initial_state_gas, 0,
6660 "T1 standard tx with nonce==0 must NOT track state gas"
6661 );
6662 }
6663
6664 #[test]
6669 fn test_state_gas_aa_create_total_gas_includes_state_gas() {
6670 let gas_params = tempo_gas_params(TempoHardfork::T4);
6671 let initcode = Bytes::from(vec![0x60, 0x80]);
6672
6673 let call = Call {
6674 to: TxKind::Create,
6675 value: U256::ZERO,
6676 input: initcode,
6677 };
6678
6679 let aa_env = TempoBatchCallEnv {
6680 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6681 alloy_primitives::Signature::test_signature(),
6682 )),
6683 aa_calls: vec![call],
6684 key_authorization: None,
6685 signature_hash: B256::ZERO,
6686 ..Default::default()
6687 };
6688
6689 let gas = calculate_aa_batch_intrinsic_gas(
6690 &aa_env,
6691 &gas_params,
6692 None::<std::iter::Empty<&AccessListItem>>,
6693 TempoHardfork::T4,
6694 )
6695 .unwrap();
6696
6697 assert!(
6698 gas.initial_total_gas() >= gas.initial_state_gas,
6699 "invariant violated: initial_total_gas ({}) < initial_state_gas ({})",
6700 gas.initial_total_gas(),
6701 gas.initial_state_gas,
6702 );
6703 }
6704
6705 #[test]
6708 fn test_state_gas_aa_auth_nonce_zero_total_gas_includes_state_gas() {
6709 let gas_params = tempo_gas_params(TempoHardfork::T4);
6710
6711 let aa_env = TempoBatchCallEnv {
6712 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6713 alloy_primitives::Signature::test_signature(),
6714 )),
6715 aa_calls: vec![Call {
6716 to: TxKind::Call(Address::random()),
6717 value: U256::ZERO,
6718 input: Bytes::from(vec![1, 2, 3]),
6719 }],
6720 tempo_authorization_list: vec![RecoveredTempoAuthorization::new(
6721 TempoSignedAuthorization::new_unchecked(
6722 alloy_eips::eip7702::Authorization {
6723 chain_id: U256::ONE,
6724 address: Address::random(),
6725 nonce: 0,
6726 },
6727 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
6728 alloy_primitives::Signature::test_signature(),
6729 )),
6730 ),
6731 )],
6732 ..Default::default()
6733 };
6734
6735 let gas = calculate_aa_batch_intrinsic_gas(
6736 &aa_env,
6737 &gas_params,
6738 None::<std::iter::Empty<&AccessListItem>>,
6739 TempoHardfork::T4,
6740 )
6741 .unwrap();
6742
6743 assert!(
6744 gas.initial_total_gas() >= gas.initial_state_gas,
6745 "invariant violated: initial_total_gas ({}) < initial_state_gas ({})",
6746 gas.initial_total_gas(),
6747 gas.initial_state_gas,
6748 );
6749 }
6750
6751 #[test]
6753 fn test_state_gas_failed_batch_preserves_upfront_create_intrinsic_gas() {
6754 let tx_gas_limit = 1_000_000u64;
6755 let (calls, call_results) = (
6756 vec![
6757 Call {
6758 to: TxKind::Create,
6759 value: U256::ZERO,
6760 input: Bytes::from(vec![0x60, 0x80]),
6761 },
6762 Call {
6763 to: TxKind::Call(Address::random()),
6764 value: U256::ZERO,
6765 input: Bytes::new(),
6766 },
6767 ],
6768 [
6769 (InstructionResult::Stop, 10_000u64),
6770 (InstructionResult::Revert, 7_000u64),
6771 ],
6772 );
6773
6774 let aa_env = make_aa_env(calls.clone());
6775 let mut test = TestHandlerEvm::aa(TempoHardfork::T4, aa_env, |tx_env| {
6776 tx_env.inner.caller = Address::random();
6777 tx_env.inner.gas_limit = tx_gas_limit;
6778 tx_env.inner.nonce = 1;
6780 });
6781
6782 let init_gas = test.validate_initial_tx_gas();
6783 assert_eq!(
6784 init_gas.initial_state_gas,
6785 test.gas_params().create_state_gas(),
6786 "first-call CREATE should contribute create_state_gas to AA intrinsic gas"
6787 );
6788 let (gas_limit, reservoir) = test.evm.initial_gas_and_reservoir(&init_gas);
6789
6790 let mut call_idx = 0usize;
6791 let result = test
6792 .handler
6793 .execute_multi_call_with(
6794 &mut test.evm,
6795 gas_limit,
6796 reservoir,
6797 calls,
6798 |_handler, _evm, gas, _reservoir| {
6799 let (instruction_result, spent) = call_results[call_idx];
6801 call_idx += 1;
6802
6803 let mut gas = Gas::new(gas);
6804 gas.set_spent(spent);
6805
6806 Ok(FrameResult::Call(CallOutcome::new(
6807 InterpreterResult::new(instruction_result, Bytes::new(), gas),
6808 0..0,
6809 )))
6810 },
6811 )
6812 .expect("execute_multi_call_with should return a failed frame result");
6813
6814 let expected_spent =
6815 init_gas.initial_total_gas() + call_results.iter().map(|(_, spent)| spent).sum::<u64>();
6816
6817 assert_eq!(result.instruction_result(), InstructionResult::Revert);
6819 assert_eq!(result.gas().total_gas_spent(), expected_spent);
6820 assert_eq!(result.gas().remaining(), tx_gas_limit - expected_spent);
6821 assert_eq!(result.gas().state_gas_spent(), 0);
6822 assert_eq!(result.gas().reservoir(), 0);
6823 }
6824}