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,
22 pre_execution::{self, apply_auth_list, calculate_caller_fee},
23 validation,
24 },
25 inspector::{Inspector, InspectorHandler},
26 interpreter::{
27 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};
35use tempo_contracts::precompiles::{
36 IAccountKeychain::SignatureType as PrecompileSignatureType, TIPFeeAMMError,
37};
38use tempo_precompiles::{
39 ECRECOVER_GAS,
40 account_keychain::{AccountKeychain, TokenLimit, authorizeKeyCall},
41 error::TempoPrecompileError,
42 nonce::{EXPIRING_NONCE_MAX_EXPIRY_SECS, INonce::getNonceCall, NonceManager},
43 storage::{PrecompileStorageProvider, StorageCtx, evm::EvmPrecompileStorageProvider},
44 tip_fee_manager::TipFeeManager,
45 tip20::{ITIP20::InsufficientBalance, TIP20Error, TIP20Token, is_tip20_prefix},
46};
47use tempo_primitives::transaction::{
48 PrimitiveSignature, SignatureType, TEMPO_EXPIRING_NONCE_KEY, TempoSignature,
49 calc_gas_balance_spending, validate_calls,
50};
51
52use crate::{
53 TempoBatchCallEnv, TempoEvm, TempoInvalidTransaction, TempoTxEnv,
54 common::TempoStateAccess,
55 error::{FeePaymentError, TempoHaltReason},
56 evm::TempoContext,
57 gas_params::TempoGasParams,
58};
59
60const P256_VERIFY_GAS: u64 = 5_000;
63
64const KEYCHAIN_VALIDATION_GAS: u64 = COLD_SLOAD_COST + 900;
66
67const KEY_AUTH_BASE_GAS: u64 = 27_000;
69
70const KEY_AUTH_PER_LIMIT_GAS: u64 = 22_000;
72
73pub const EXPIRING_NONCE_GAS: u64 = 2 * COLD_SLOAD_COST + 100 + 3 * WARM_SSTORE_RESET;
95
96#[inline]
103fn primitive_signature_verification_gas(signature: &PrimitiveSignature) -> u64 {
104 match signature {
105 PrimitiveSignature::Secp256k1(_) => 0,
106 PrimitiveSignature::P256(_) => P256_VERIFY_GAS,
107 PrimitiveSignature::WebAuthn(webauthn_sig) => {
108 let tokens = get_tokens_in_calldata_istanbul(&webauthn_sig.webauthn_data);
109 P256_VERIFY_GAS + tokens * STANDARD_TOKEN_COST
110 }
111 }
112}
113
114#[inline]
119fn tempo_signature_verification_gas(signature: &TempoSignature) -> u64 {
120 match signature {
121 TempoSignature::Primitive(prim_sig) => primitive_signature_verification_gas(prim_sig),
122 TempoSignature::Keychain(keychain_sig) => {
123 primitive_signature_verification_gas(&keychain_sig.signature) + KEYCHAIN_VALIDATION_GAS
125 }
126 }
127}
128
129#[inline]
140fn calculate_key_authorization_gas(
141 key_auth: &tempo_primitives::transaction::SignedKeyAuthorization,
142 gas_params: &GasParams,
143 spec: tempo_chainspec::hardfork::TempoHardfork,
144) -> u64 {
145 let sig_gas = ECRECOVER_GAS + primitive_signature_verification_gas(&key_auth.signature);
149
150 let num_limits = key_auth
151 .authorization
152 .limits
153 .as_ref()
154 .map(|limits| limits.len() as u64)
155 .unwrap_or(0);
156
157 if spec.is_t1b() {
158 const BUFFER: u64 = 2_000;
162 let sstore_cost = gas_params.get(GasId::sstore_set_without_load_cost());
163 let sload_cost =
164 gas_params.warm_storage_read_cost() + gas_params.cold_storage_additional_cost();
165
166 sig_gas + sload_cost + sstore_cost * (1 + num_limits) + BUFFER
167 } else {
168 KEY_AUTH_BASE_GAS + sig_gas + num_limits * KEY_AUTH_PER_LIMIT_GAS
170 }
171}
172
173#[inline]
180fn adjusted_initial_gas(
181 spec: tempo_chainspec::hardfork::TempoHardfork,
182 evm_initial_gas: u64,
183 init_and_floor_gas: &InitialAndFloorGas,
184) -> InitialAndFloorGas {
185 if spec.is_t1() {
186 InitialAndFloorGas::new(evm_initial_gas, init_and_floor_gas.floor_gas)
187 } else {
188 *init_and_floor_gas
189 }
190}
191
192#[derive(Debug)]
196pub struct TempoEvmHandler<DB, I> {
197 fee_token: Address,
199 fee_payer: Address,
201 _phantom: core::marker::PhantomData<(DB, I)>,
203}
204
205impl<DB, I> TempoEvmHandler<DB, I> {
206 pub fn new() -> Self {
208 Self {
209 fee_token: Address::default(),
210 fee_payer: Address::default(),
211 _phantom: core::marker::PhantomData,
212 }
213 }
214}
215
216impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
217 fn seed_tx_origin(
218 &self,
219 evm: &mut TempoEvm<DB, I>,
220 ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
221 let ctx = evm.ctx_mut();
222
223 StorageCtx::enter_evm(
226 &mut ctx.journaled_state,
227 &ctx.block,
228 &ctx.cfg,
229 &ctx.tx,
230 || {
231 let mut keychain = AccountKeychain::new();
232 keychain.set_tx_origin(ctx.tx.caller())
233 },
234 )
235 .map_err(|e| EVMError::Custom(e.to_string()))
236 }
237
238 pub fn load_fee_fields(
250 &mut self,
251 evm: &mut TempoEvm<DB, I>,
252 ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
253 let ctx = evm.ctx_mut();
254
255 self.fee_payer = ctx.tx.fee_payer()?;
256 if ctx.cfg.spec.is_t2()
257 && ctx.tx.has_fee_payer_signature()
258 && self.fee_payer == ctx.tx.caller()
259 {
260 return Err(TempoInvalidTransaction::SelfSponsoredFeePayer.into());
261 }
262
263 self.fee_token = ctx
264 .journaled_state
265 .get_fee_token(&ctx.tx, self.fee_payer, ctx.cfg.spec)
266 .map_err(|err| EVMError::Custom(err.to_string()))?;
267
268 if !is_tip20_prefix(self.fee_token) {
271 return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
272 }
273
274 if (!ctx.tx.max_balance_spending()?.is_zero() || ctx.tx.is_subblock_transaction())
277 && !ctx
278 .journaled_state
279 .is_tip20_usd(ctx.cfg.spec, self.fee_token)
280 .map_err(|err| EVMError::Custom(err.to_string()))?
281 {
282 return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
283 }
284
285 Ok(())
286 }
287}
288
289impl<DB, I> TempoEvmHandler<DB, I>
290where
291 DB: alloy_evm::Database,
292{
293 fn execute_single_call_with<F>(
298 &mut self,
299 evm: &mut TempoEvm<DB, I>,
300 init_and_floor_gas: &InitialAndFloorGas,
301 mut run_loop: F,
302 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
303 where
304 F: FnMut(
305 &mut Self,
306 &mut TempoEvm<DB, I>,
307 <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
308 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
309 {
310 let gas_limit = evm.ctx().tx().gas_limit() - init_and_floor_gas.initial_gas;
311
312 let first_frame_input = self.first_frame_input(evm, gas_limit)?;
314
315 let mut frame_result = run_loop(self, evm, first_frame_input)?;
317
318 self.last_frame_result(evm, &mut frame_result)?;
320
321 Ok(frame_result)
322 }
323
324 fn execute_single_call(
328 &mut self,
329 evm: &mut TempoEvm<DB, I>,
330 init_and_floor_gas: &InitialAndFloorGas,
331 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
332 self.execute_single_call_with(evm, init_and_floor_gas, Self::run_exec_loop)
333 }
334
335 fn execute_multi_call_with<F>(
351 &mut self,
352 evm: &mut TempoEvm<DB, I>,
353 init_and_floor_gas: &InitialAndFloorGas,
354 calls: Vec<tempo_primitives::transaction::Call>,
355 mut execute_single: F,
356 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
357 where
358 F: FnMut(
359 &mut Self,
360 &mut TempoEvm<DB, I>,
361 &InitialAndFloorGas,
362 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
363 {
364 let checkpoint = evm.ctx().journal_mut().checkpoint();
366
367 let gas_limit = evm.ctx().tx().gas_limit();
368 let mut remaining_gas = gas_limit - init_and_floor_gas.initial_gas;
369 let mut accumulated_gas_refund = 0i64;
370
371 let original_kind = evm.ctx().tx().kind();
373 let original_value = evm.ctx().tx().value();
374 let original_data = evm.ctx().tx().input().clone();
375
376 let mut final_result = None;
377
378 for call in calls.iter() {
379 {
381 let tx = &mut evm.ctx().tx;
382 tx.inner.kind = call.to;
383 tx.inner.value = call.value;
384 tx.inner.data = call.input.clone();
385 tx.inner.gas_limit = remaining_gas;
386 }
387
388 let zero_init_gas = InitialAndFloorGas::new(0, 0);
390 let frame_result = execute_single(self, evm, &zero_init_gas);
391
392 {
394 let tx = &mut evm.ctx().tx;
395 tx.inner.kind = original_kind;
396 tx.inner.value = original_value;
397 tx.inner.data = original_data.clone();
398 tx.inner.gas_limit = gas_limit;
399 }
400
401 let mut frame_result = frame_result?;
402
403 let instruction_result = frame_result.instruction_result();
405 if !instruction_result.is_ok() {
406 evm.ctx().journal_mut().checkpoint_revert(checkpoint);
408
409 let uses_protocol_nonce = evm
419 .ctx()
420 .tx()
421 .tempo_tx_env
422 .as_ref()
423 .map(|aa| aa.nonce_key.is_zero())
424 .unwrap_or(true);
425
426 if uses_protocol_nonce && calls.first().map(|c| c.to.is_create()).unwrap_or(false) {
427 let caller = evm.ctx().tx().caller();
428 if let Ok(mut caller_acc) =
429 evm.ctx().journal_mut().load_account_with_code_mut(caller)
430 {
431 caller_acc.data.bump_nonce();
432 }
433 }
434
435 let gas_spent_by_failed_call = frame_result.gas().spent();
437 let total_gas_spent = (gas_limit - remaining_gas) + gas_spent_by_failed_call;
438
439 let mut corrected_gas = Gas::new(gas_limit);
442 if instruction_result.is_revert() {
443 corrected_gas.set_spent(total_gas_spent);
444 } else {
445 corrected_gas.spend_all();
446 }
447 corrected_gas.set_refund(0); *frame_result.gas_mut() = corrected_gas;
449
450 return Ok(frame_result);
451 }
452
453 let gas_spent = frame_result.gas().spent();
455 let gas_refunded = frame_result.gas().refunded();
456
457 accumulated_gas_refund = accumulated_gas_refund.saturating_add(gas_refunded);
458 remaining_gas = remaining_gas.saturating_sub(gas_spent);
460
461 final_result = Some(frame_result);
462 }
463
464 evm.ctx().journal_mut().checkpoint_commit();
466
467 let mut result =
469 final_result.ok_or_else(|| EVMError::Custom("No calls executed".into()))?;
470
471 let total_gas_spent = gas_limit - remaining_gas;
472
473 let mut corrected_gas = Gas::new(gas_limit);
476 corrected_gas.set_spent(total_gas_spent);
477 corrected_gas.set_refund(accumulated_gas_refund);
478 *result.gas_mut() = corrected_gas;
479
480 Ok(result)
481 }
482
483 fn execute_multi_call(
485 &mut self,
486 evm: &mut TempoEvm<DB, I>,
487 init_and_floor_gas: &InitialAndFloorGas,
488 calls: Vec<tempo_primitives::transaction::Call>,
489 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
490 self.execute_multi_call_with(evm, init_and_floor_gas, calls, Self::execute_single_call)
491 }
492
493 fn inspect_execute_single_call(
498 &mut self,
499 evm: &mut TempoEvm<DB, I>,
500 init_and_floor_gas: &InitialAndFloorGas,
501 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
502 where
503 I: Inspector<TempoContext<DB>, EthInterpreter>,
504 {
505 self.execute_single_call_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop)
506 }
507
508 fn inspect_execute_multi_call(
513 &mut self,
514 evm: &mut TempoEvm<DB, I>,
515 init_and_floor_gas: &InitialAndFloorGas,
516 calls: Vec<tempo_primitives::transaction::Call>,
517 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
518 where
519 I: Inspector<TempoContext<DB>, EthInterpreter>,
520 {
521 self.execute_multi_call_with(
522 evm,
523 init_and_floor_gas,
524 calls,
525 Self::inspect_execute_single_call,
526 )
527 }
528
529 pub fn inspect_execution_with<F>(
542 &mut self,
543 evm: &mut TempoEvm<DB, I>,
544 init_and_floor_gas: &InitialAndFloorGas,
545 mut exec_loop: F,
546 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
547 where
548 F: FnMut(
549 &mut Self,
550 &mut TempoEvm<DB, I>,
551 <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
552 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
553 I: Inspector<TempoContext<DB>, EthInterpreter>,
554 {
555 let spec = *evm.ctx_ref().cfg().spec();
556 let adjusted_gas = adjusted_initial_gas(spec, evm.initial_gas, init_and_floor_gas);
557
558 let tx = evm.tx();
559
560 if let Some(oog) = check_gas_limit(spec, tx, &adjusted_gas) {
561 return Ok(oog);
562 }
563
564 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() {
565 let calls = tempo_tx_env.aa_calls.clone();
566 return self.inspect_execute_multi_call(evm, &adjusted_gas, calls);
567 }
568
569 self.execute_single_call_with(evm, &adjusted_gas, &mut exec_loop)
570 }
571}
572
573impl<DB, I> Default for TempoEvmHandler<DB, I> {
574 fn default() -> Self {
575 Self::new()
576 }
577}
578
579impl<DB, I> Handler for TempoEvmHandler<DB, I>
580where
581 DB: alloy_evm::Database,
582{
583 type Evm = TempoEvm<DB, I>;
584 type Error = EVMError<DB::Error, TempoInvalidTransaction>;
585 type HaltReason = TempoHaltReason;
586
587 #[inline]
588 fn run(
589 &mut self,
590 evm: &mut Self::Evm,
591 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
592 self.load_fee_fields(evm)?;
593
594 match self.run_without_catch_error(evm) {
596 Ok(output) => Ok(output),
597 Err(err) => self.catch_error(evm, err),
598 }
599 }
600
601 #[inline]
607 fn execution(
608 &mut self,
609 evm: &mut Self::Evm,
610 init_and_floor_gas: &InitialAndFloorGas,
611 ) -> Result<FrameResult, Self::Error> {
612 let spec = evm.ctx_ref().cfg().spec();
613 let adjusted_gas = adjusted_initial_gas(*spec, evm.initial_gas, init_and_floor_gas);
614 let tx = evm.tx();
615
616 if let Some(oog) = check_gas_limit(*spec, tx, &adjusted_gas) {
617 return Ok(oog);
618 }
619
620 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref() {
621 let calls = tempo_tx_env.aa_calls.clone();
622 self.execute_multi_call(evm, &adjusted_gas, calls)
623 } else {
624 self.execute_single_call(evm, &adjusted_gas)
625 }
626 }
627
628 #[inline]
630 fn execution_result(
631 &mut self,
632 evm: &mut Self::Evm,
633 result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
634 result_gas: ResultGas,
635 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
636 evm.logs.clear();
637 evm.initial_gas = 0;
639 if !result.instruction_result().is_ok() {
640 evm.logs = evm.journal_mut().take_logs();
641 }
642
643 MainnetHandler::default()
644 .execution_result(evm, result, result_gas)
645 .map(|result| result.map_haltreason(Into::into))
646 }
647
648 #[inline]
654 fn apply_eip7702_auth_list(&self, evm: &mut Self::Evm) -> Result<u64, Self::Error> {
655 let ctx = &mut evm.ctx;
656 let spec = ctx.cfg.spec;
657
658 let has_aa_auth_list = ctx
660 .tx
661 .tempo_tx_env
662 .as_ref()
663 .map(|aa_env| !aa_env.tempo_authorization_list.is_empty())
664 .unwrap_or(false);
665
666 let refunded_gas = if has_aa_auth_list {
669 let tempo_tx_env = ctx.tx.tempo_tx_env.as_ref().unwrap();
670
671 apply_auth_list::<_, Self::Error>(
672 ctx.cfg.chain_id,
673 ctx.cfg.gas_params.tx_eip7702_auth_refund(),
674 tempo_tx_env
675 .tempo_authorization_list
676 .iter()
677 .filter(|auth| !(spec.is_t0() && auth.signature().is_keychain())),
679 &mut ctx.journaled_state,
680 )?
681 } else {
682 pre_execution::apply_eip7702_auth_list::<_, Self::Error>(evm.ctx())?
684 };
685
686 if spec.is_t1() {
689 return Ok(0);
690 }
691
692 Ok(refunded_gas)
693 }
694
695 #[inline]
696 fn validate_against_state_and_deduct_caller(
697 &self,
698 evm: &mut Self::Evm,
699 ) -> Result<(), Self::Error> {
700 self.seed_tx_origin(evm)?;
701
702 let block = &evm.inner.ctx.block;
703 let tx = &evm.inner.ctx.tx;
704 let cfg = &evm.inner.ctx.cfg;
705 let journal = &mut evm.inner.ctx.journaled_state;
706
707 if !is_tip20_prefix(self.fee_token) {
713 return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
714 }
715
716 let account_balance = get_token_balance(journal, self.fee_token, self.fee_payer)?;
718
719 let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
721
722 let nonce_key = tx
723 .tempo_tx_env
724 .as_ref()
725 .map(|aa| aa.nonce_key)
726 .unwrap_or_default();
727
728 let spec = cfg.spec();
729
730 let is_expiring_nonce = nonce_key == TEMPO_EXPIRING_NONCE_KEY && spec.is_t1();
732
733 pre_execution::validate_account_nonce_and_code(
735 &caller_account.account().info,
736 tx.nonce(),
737 cfg.is_eip3607_disabled(),
738 cfg.is_nonce_check_disabled() || !nonce_key.is_zero(),
740 )?;
741
742 caller_account.touch();
744
745 if !nonce_key.is_zero() && tx.kind().is_create() && caller_account.nonce() == 0 {
748 evm.initial_gas += cfg.gas_params().get(GasId::new_account_cost());
749
750 if tx.gas_limit() < evm.initial_gas {
752 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
753 gas_limit: tx.gas_limit(),
754 intrinsic_gas: evm.initial_gas,
755 }
756 .into());
757 }
758 }
759
760 if is_expiring_nonce {
761 let tempo_tx_env = tx
766 .tempo_tx_env
767 .as_ref()
768 .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?;
769
770 if tx.nonce() != 0 {
772 return Err(TempoInvalidTransaction::ExpiringNonceNonceNotZero.into());
773 }
774
775 let replay_hash = if spec.is_t1b() {
776 tempo_tx_env
777 .expiring_nonce_hash
778 .ok_or(TempoInvalidTransaction::ExpiringNonceMissingTxEnv)?
779 } else {
780 tempo_tx_env.tx_hash
781 };
782 let valid_before = tempo_tx_env
783 .valid_before
784 .ok_or(TempoInvalidTransaction::ExpiringNonceMissingValidBefore)?;
785
786 let block_timestamp = block.timestamp().saturating_to::<u64>();
787 StorageCtx::enter_evm(journal, block, cfg, tx, || {
788 let mut nonce_manager = NonceManager::new();
789
790 nonce_manager
791 .check_and_mark_expiring_nonce(replay_hash, valid_before)
792 .map_err(|err| match err {
793 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
794 TempoPrecompileError::NonceError(
795 tempo_contracts::precompiles::NonceError::InvalidExpiringNonceExpiry(_),
796 ) => {
797 let max_allowed =
798 block_timestamp.saturating_add(EXPIRING_NONCE_MAX_EXPIRY_SECS);
799 if valid_before <= block_timestamp {
800 TempoInvalidTransaction::NonceManagerError(format!(
801 "expiring nonce transaction expired: valid_before ({valid_before}) <= block timestamp ({block_timestamp})"
802 ))
803 .into()
804 } else {
805 TempoInvalidTransaction::NonceManagerError(format!(
806 "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}"
807 ))
808 .into()
809 }
810 }
811 err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
812 })?;
813
814 Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
815 })?;
816 } else if !nonce_key.is_zero() {
817 StorageCtx::enter_evm(journal, block, cfg, tx, || {
819 let mut nonce_manager = NonceManager::new();
820
821 if !cfg.is_nonce_check_disabled() {
822 let tx_nonce = tx.nonce();
823 let state = nonce_manager
824 .get_nonce(getNonceCall {
825 account: tx.caller(),
826 nonceKey: nonce_key,
827 })
828 .map_err(|err| match err {
829 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
830 err => {
831 TempoInvalidTransaction::NonceManagerError(err.to_string()).into()
832 }
833 })?;
834
835 match tx_nonce.cmp(&state) {
836 Ordering::Greater => {
837 return Err(InvalidTransaction::NonceTooHigh {
838 tx: tx_nonce,
839 state,
840 }
841 .into());
842 }
843 Ordering::Less => {
844 return Err(InvalidTransaction::NonceTooLow {
845 tx: tx_nonce,
846 state,
847 }
848 .into());
849 }
850 _ => {}
851 }
852 }
853
854 nonce_manager
856 .increment_nonce(tx.caller(), nonce_key)
857 .map_err(|err| match err {
858 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
859 err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
860 })?;
861
862 Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(())
863 })?;
864 } else {
865 if tx.kind().is_call() {
870 caller_account.bump_nonce();
871 }
872 }
873
874 let new_balance = calculate_caller_fee(account_balance, tx, block, cfg)?;
876 let gas_balance_spending = core::cmp::max(account_balance, new_balance) - new_balance;
879
880 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
885 && let Some(key_auth) = &tempo_tx_env.key_authorization
886 {
887 if let Some(keychain_sig) = tempo_tx_env.signature.as_keychain() {
890 let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
892 override_key_id
893 } else {
894 keychain_sig
896 .key_id(&tempo_tx_env.signature_hash)
897 .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
898 };
899
900 if access_key_addr != key_auth.key_id {
902 return Err(TempoInvalidTransaction::AccessKeyCannotAuthorizeOtherKeys.into());
903 }
904 }
905
906 let root_account = &tx.caller;
908
909 let auth_signer = key_auth
911 .recover_signer()
912 .map_err(|_| TempoInvalidTransaction::KeyAuthorizationSignatureRecoveryFailed)?;
913
914 if auth_signer != *root_account {
916 return Err(TempoInvalidTransaction::KeyAuthorizationNotSignedByRoot {
917 expected: *root_account,
918 actual: auth_signer,
919 }
920 .into());
921 }
922
923 key_auth
927 .validate_chain_id(cfg.chain_id(), spec.is_t1c())
928 .map_err(TempoInvalidTransaction::from)?;
929
930 let keychain_checkpoint = if spec.is_t1() {
931 Some(journal.checkpoint())
932 } else {
933 None
934 };
935
936 let internals = EvmInternals::new(journal, block, cfg, tx);
937
938 let gas_limit = if spec.is_t1() && !spec.is_t1b() {
945 tx.gas_limit() - evm.initial_gas
946 } else {
947 u64::MAX
948 };
949
950 let gas_params = if spec.is_t1() {
952 static TABLE: OnceLock<GasParams> = OnceLock::new();
953 TABLE
955 .get_or_init(|| {
956 let mut table = [0u64; 256];
957 table[GasId::sstore_set_without_load_cost().as_usize()] =
958 cfg.gas_params.get(GasId::sstore_set_without_load_cost());
959 table[GasId::warm_storage_read_cost().as_usize()] =
960 cfg.gas_params.get(GasId::warm_storage_read_cost());
961 GasParams::new(Arc::new(table))
962 })
963 .clone()
964 } else {
965 cfg.gas_params.clone()
966 };
967
968 let mut provider = EvmPrecompileStorageProvider::new(
969 internals, gas_limit, cfg.spec, false, gas_params,
970 );
971
972 let out_of_gas = StorageCtx::enter(&mut provider, || {
974 let mut keychain = AccountKeychain::default();
975 let access_key_addr = key_auth.key_id;
976
977 let signature_type = match key_auth.key_type {
980 SignatureType::Secp256k1 => PrecompileSignatureType::Secp256k1,
981 SignatureType::P256 => PrecompileSignatureType::P256,
982 SignatureType::WebAuthn => PrecompileSignatureType::WebAuthn,
983 };
984
985 let expiry = key_auth.expiry.unwrap_or(u64::MAX);
987
988 let current_timestamp = block.timestamp().saturating_to::<u64>();
990 if expiry <= current_timestamp {
991 return Err(TempoInvalidTransaction::AccessKeyExpiryInPast {
992 expiry,
993 current_timestamp,
994 }
995 .into());
996 }
997
998 let enforce_limits = key_auth.limits.is_some();
1002 let precompile_limits: Vec<TokenLimit> = key_auth
1003 .limits
1004 .as_ref()
1005 .map(|limits| {
1006 limits
1007 .iter()
1008 .map(|limit| TokenLimit {
1009 token: limit.token,
1010 amount: limit.limit,
1011 })
1012 .collect()
1013 })
1014 .unwrap_or_default();
1015
1016 let authorize_call = authorizeKeyCall {
1018 keyId: access_key_addr,
1019 signatureType: signature_type,
1020 expiry,
1021 enforceLimits: enforce_limits,
1022 limits: precompile_limits,
1023 };
1024
1025 match keychain.authorize_key(*root_account, authorize_call) {
1027 Ok(_) => Ok(false),
1029 Err(TempoPrecompileError::OutOfGas) => Ok(true),
1031 Err(TempoPrecompileError::Fatal(err)) => Err(EVMError::Custom(err)),
1032 Err(err) => Err(TempoInvalidTransaction::KeychainPrecompileError {
1033 reason: err.to_string(),
1034 }
1035 .into()),
1036 }
1037 })?;
1038
1039 let gas_used = provider.gas_used();
1040 drop(provider);
1041
1042 if let Some(keychain_checkpoint) = keychain_checkpoint {
1047 if spec.is_t1b() {
1048 journal.checkpoint_commit();
1049 } else if out_of_gas {
1050 evm.initial_gas = u64::MAX;
1051 journal.checkpoint_revert(keychain_checkpoint);
1052 } else {
1053 evm.initial_gas += gas_used;
1054 journal.checkpoint_commit();
1055 };
1056 }
1057 }
1058
1059 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
1062 && let Some(keychain_sig) = tempo_tx_env.signature.as_keychain()
1063 {
1064 let access_key_addr = if let Some(override_key_id) = tempo_tx_env.override_key_id {
1066 override_key_id
1067 } else {
1068 let user_address = &keychain_sig.user_address;
1071
1072 if *user_address != tx.caller {
1074 return Err(TempoInvalidTransaction::KeychainUserAddressMismatch {
1075 user_address: *user_address,
1076 caller: tx.caller,
1077 }
1078 .into());
1079 }
1080
1081 keychain_sig
1083 .key_id(&tempo_tx_env.signature_hash)
1084 .map_err(|_| TempoInvalidTransaction::AccessKeyRecoveryFailed)?
1085 };
1086
1087 let is_authorizing_this_key = tempo_tx_env
1090 .key_authorization
1091 .as_ref()
1092 .map(|key_auth| key_auth.key_id == access_key_addr)
1093 .unwrap_or(false);
1094
1095 StorageCtx::enter_precompile(
1097 journal,
1098 block,
1099 cfg,
1100 tx,
1101 |mut keychain: AccountKeychain| {
1102 if !is_authorizing_this_key {
1104 let user_address = &keychain_sig.user_address;
1106
1107 let sig_type = spec
1113 .is_t1()
1114 .then_some(keychain_sig.signature.signature_type().into());
1115
1116 keychain
1117 .validate_keychain_authorization(
1118 *user_address,
1119 access_key_addr,
1120 block.timestamp().to::<u64>(),
1121 sig_type,
1122 )
1123 .map_err(|e| TempoInvalidTransaction::KeychainValidationFailed {
1124 reason: format!("{e:?}"),
1125 })?;
1126 }
1127
1128 keychain
1132 .set_transaction_key(access_key_addr)
1133 .map_err(|e| EVMError::Custom(e.to_string()))
1134 },
1135 )?;
1136 }
1137
1138 if gas_balance_spending.is_zero() {
1141 return Ok(());
1142 }
1143
1144 let checkpoint = journal.checkpoint();
1145
1146 let result = StorageCtx::enter_evm(journal, &block, cfg, tx, || {
1147 TipFeeManager::new().collect_fee_pre_tx(
1148 self.fee_payer,
1149 self.fee_token,
1150 gas_balance_spending,
1151 block.beneficiary(),
1152 )
1153 });
1154
1155 if let Err(err) = result {
1156 journal.checkpoint_revert(checkpoint);
1158
1159 Err(match err {
1163 TempoPrecompileError::TIPFeeAMMError(TIPFeeAMMError::InsufficientLiquidity(_)) => {
1164 FeePaymentError::InsufficientAmmLiquidity {
1165 fee: gas_balance_spending,
1166 }
1167 .into()
1168 }
1169
1170 TempoPrecompileError::TIP20(TIP20Error::InsufficientBalance(
1171 InsufficientBalance { available, .. },
1172 )) => FeePaymentError::InsufficientFeeTokenBalance {
1173 fee: gas_balance_spending,
1174 balance: available,
1175 }
1176 .into(),
1177
1178 TempoPrecompileError::Fatal(e) => EVMError::Custom(e),
1179
1180 _ => FeePaymentError::Other(err.to_string()).into(),
1181 })
1182 } else {
1183 journal.checkpoint_commit();
1184 evm.collected_fee = gas_balance_spending;
1185
1186 Ok(())
1187 }
1188 }
1189
1190 fn reimburse_caller(
1191 &self,
1192 evm: &mut Self::Evm,
1193 exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
1194 ) -> Result<(), Self::Error> {
1195 let context = &mut evm.inner.ctx;
1197 let tx = context.tx();
1198 let basefee = context.block().basefee() as u128;
1199 let effective_gas_price = tx.effective_gas_price(basefee);
1200 let gas = exec_result.gas();
1201
1202 let actual_spending = calc_gas_balance_spending(gas.used(), effective_gas_price);
1203 let refund_amount = tx.effective_balance_spending(
1204 context.block.basefee.into(),
1205 context.block.blob_gasprice().unwrap_or_default(),
1206 )? - tx.value
1207 - actual_spending;
1208
1209 if context.cfg.disable_fee_charge
1215 && evm.collected_fee.is_zero()
1216 && !actual_spending.is_zero()
1217 {
1218 return Ok(());
1219 }
1220
1221 let (journal, block, tx) = (&mut context.journaled_state, &context.block, &context.tx);
1223 let beneficiary = context.block.beneficiary();
1224
1225 StorageCtx::enter_evm(&mut *journal, block, &context.cfg, tx, || {
1226 let mut fee_manager = TipFeeManager::new();
1227
1228 if !actual_spending.is_zero() || !refund_amount.is_zero() {
1229 fee_manager
1231 .collect_fee_post_tx(
1232 self.fee_payer,
1233 actual_spending,
1234 refund_amount,
1235 self.fee_token,
1236 beneficiary,
1237 )
1238 .map_err(|e| EVMError::Custom(format!("{e:?}")))?;
1239 }
1240
1241 Ok(())
1242 })
1243 }
1244
1245 #[inline]
1246 fn reward_beneficiary(
1247 &self,
1248 _evm: &mut Self::Evm,
1249 _exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
1250 ) -> Result<(), Self::Error> {
1251 Ok(())
1254 }
1255
1256 #[inline]
1262 fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
1263 if !evm.ctx.tx.value().is_zero() {
1266 return Err(TempoInvalidTransaction::ValueTransferNotAllowed.into());
1267 }
1268
1269 validation::validate_env::<_, Self::Error>(evm.ctx())?;
1272
1273 let cfg = evm.ctx_ref().cfg();
1275 let tx = evm.ctx_ref().tx();
1276
1277 if let Some(aa_env) = tx.tempo_tx_env.as_ref() {
1278 validate_calls(
1280 &aa_env.aa_calls,
1281 !aa_env.tempo_authorization_list.is_empty(),
1282 )
1283 .map_err(TempoInvalidTransaction::from)?;
1284
1285 aa_env
1287 .signature
1288 .validate_version(cfg.spec().is_t1c())
1289 .map_err(TempoInvalidTransaction::from)?;
1290 for auth in &aa_env.tempo_authorization_list {
1291 auth.signature()
1292 .validate_version(cfg.spec().is_t1c())
1293 .map_err(TempoInvalidTransaction::from)?;
1294 }
1295
1296 let has_keychain_fields =
1297 aa_env.key_authorization.is_some() || aa_env.signature.is_keychain();
1298
1299 if aa_env.subblock_transaction && has_keychain_fields {
1300 return Err(TempoInvalidTransaction::KeychainOpInSubblockTransaction.into());
1301 }
1302
1303 let base_fee = if cfg.is_base_fee_check_disabled() {
1305 None
1306 } else {
1307 Some(evm.ctx_ref().block().basefee() as u128)
1308 };
1309
1310 validation::validate_priority_fee_tx(
1311 tx.max_fee_per_gas(),
1312 tx.max_priority_fee_per_gas().unwrap_or_default(),
1313 base_fee,
1314 cfg.is_priority_fee_check_disabled(),
1315 )?;
1316
1317 let block_timestamp = evm.ctx_ref().block().timestamp().saturating_to();
1319 validate_time_window(aa_env.valid_after, aa_env.valid_before, block_timestamp)?;
1320 }
1321
1322 Ok(())
1323 }
1324
1325 #[inline]
1332 fn validate_initial_tx_gas(
1333 &self,
1334 evm: &mut Self::Evm,
1335 ) -> Result<InitialAndFloorGas, Self::Error> {
1336 let tx = evm.ctx_ref().tx();
1337 let spec = evm.ctx_ref().cfg().spec();
1338 let gas_params = evm.ctx_ref().cfg().gas_params();
1339 let gas_limit = tx.gas_limit();
1340
1341 let init_gas = if tx.tempo_tx_env.is_some() {
1343 validate_aa_initial_tx_gas(evm)?
1345 } else {
1346 let mut acc = 0;
1347 let mut storage = 0;
1348 if tx.tx_type() != TransactionType::Legacy {
1350 (acc, storage) = tx
1351 .access_list()
1352 .map(|al| {
1353 al.fold((0, 0), |(acc, storage), item| {
1354 (acc + 1, storage + item.storage_slots().count())
1355 })
1356 })
1357 .unwrap_or_default();
1358 };
1359 let mut init_gas = gas_params.initial_tx_gas(
1360 tx.input(),
1361 tx.kind().is_create(),
1362 acc as u64,
1363 storage as u64,
1364 tx.authorization_list_len() as u64,
1365 );
1366 for auth in tx.authorization_list() {
1370 if auth.nonce == 0 {
1371 init_gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost();
1372 }
1373 }
1374
1375 if spec.is_t1() && tx.nonce == 0 {
1378 init_gas.initial_gas += gas_params.get(GasId::new_account_cost());
1379 }
1380
1381 if evm.ctx.cfg.is_eip7623_disabled() {
1382 init_gas.floor_gas = 0u64;
1383 }
1384
1385 if gas_limit < init_gas.initial_gas {
1387 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1388 gas_limit,
1389 intrinsic_gas: init_gas.initial_gas,
1390 }
1391 .into());
1392 }
1393
1394 if !evm.ctx.cfg.is_eip7623_disabled() && gas_limit < init_gas.floor_gas {
1396 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1397 gas_limit,
1398 intrinsic_gas: init_gas.floor_gas,
1399 }
1400 .into());
1401 }
1402
1403 init_gas
1404 };
1405
1406 evm.initial_gas = init_gas.initial_gas;
1408
1409 Ok(init_gas)
1410 }
1411
1412 fn catch_error(
1413 &self,
1414 evm: &mut Self::Evm,
1415 error: Self::Error,
1416 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1417 evm.initial_gas = 0;
1419
1420 if evm.ctx.tx.is_subblock_transaction()
1422 && let Some(
1423 TempoInvalidTransaction::CollectFeePreTx(_)
1424 | TempoInvalidTransaction::EthInvalidTransaction(
1425 InvalidTransaction::LackOfFundForMaxFee { .. },
1426 ),
1427 ) = error.as_invalid_tx_err()
1428 {
1429 evm.ctx.journaled_state.commit_tx();
1433
1434 evm.ctx().local_mut().clear();
1435 evm.frame_stack().clear();
1436
1437 Ok(ExecutionResult::Halt {
1438 reason: TempoHaltReason::SubblockTxFeePayment,
1439 logs: Default::default(),
1440 gas: ResultGas::default().with_limit(evm.ctx.tx.gas_limit),
1441 })
1442 } else {
1443 MainnetHandler::default()
1444 .catch_error(evm, error)
1445 .map(|result| result.map_haltreason(Into::into))
1446 }
1447 }
1448}
1449
1450pub fn calculate_aa_batch_intrinsic_gas<'a>(
1467 aa_env: &TempoBatchCallEnv,
1468 gas_params: &GasParams,
1469 access_list: Option<impl Iterator<Item = &'a AccessListItem>>,
1470 spec: tempo_chainspec::hardfork::TempoHardfork,
1471) -> Result<InitialAndFloorGas, TempoInvalidTransaction> {
1472 let calls = &aa_env.aa_calls;
1473 let signature = &aa_env.signature;
1474 let authorization_list = &aa_env.tempo_authorization_list;
1475 let key_authorization = aa_env.key_authorization.as_ref();
1476 let mut gas = InitialAndFloorGas::default();
1477
1478 gas.initial_gas += gas_params.tx_base_stipend();
1480
1481 gas.initial_gas += tempo_signature_verification_gas(signature);
1483
1484 let cold_account_cost =
1485 gas_params.warm_storage_read_cost() + gas_params.cold_account_additional_cost();
1486
1487 gas.initial_gas += cold_account_cost * calls.len().saturating_sub(1) as u64;
1490
1491 gas.initial_gas +=
1493 authorization_list.len() as u64 * gas_params.tx_eip7702_per_empty_account_cost();
1494
1495 for auth in authorization_list {
1498 gas.initial_gas += tempo_signature_verification_gas(auth.signature());
1499 if auth.nonce == 0 {
1502 gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost();
1503 }
1504 }
1505
1506 if let Some(key_auth) = key_authorization {
1508 gas.initial_gas += calculate_key_authorization_gas(key_auth, gas_params, spec);
1509 }
1510
1511 let mut total_tokens = 0u64;
1513
1514 for call in calls {
1515 let tokens = get_tokens_in_calldata_istanbul(&call.input);
1517 total_tokens += tokens;
1518
1519 if call.to.is_create() {
1521 gas.initial_gas += gas_params.create_cost();
1523
1524 gas.initial_gas += gas_params.tx_initcode_cost(call.input.len());
1526 }
1527
1528 if !call.value.is_zero() {
1531 return Err(TempoInvalidTransaction::ValueTransferNotAllowedInAATx);
1532 }
1533
1534 if !call.value.is_zero() && call.to.is_call() {
1537 gas.initial_gas += gas_params.get(GasId::transfer_value_cost()); }
1539 }
1540
1541 gas.initial_gas += total_tokens * gas_params.tx_token_cost();
1542
1543 if let Some(access_list) = access_list {
1545 let (accounts, storages) = access_list.fold((0, 0), |(acc_count, storage_count), item| {
1546 (acc_count + 1, storage_count + item.storage_slots().count())
1547 });
1548 gas.initial_gas += accounts * gas_params.tx_access_list_address_cost(); gas.initial_gas += storages as u64 * gas_params.tx_access_list_storage_key_cost(); }
1551
1552 gas.floor_gas = gas_params.tx_floor_cost(total_tokens); Ok(gas)
1556}
1557
1558fn validate_aa_initial_tx_gas<DB, I>(
1564 evm: &TempoEvm<DB, I>,
1565) -> Result<InitialAndFloorGas, EVMError<DB::Error, TempoInvalidTransaction>>
1566where
1567 DB: alloy_evm::Database,
1568{
1569 let (_, tx, cfg, _, _, _, _) = evm.ctx_ref().all();
1570 let gas_limit = tx.gas_limit();
1571 let gas_params = cfg.gas_params();
1572 let spec = *cfg.spec();
1573
1574 let aa_env = tx
1576 .tempo_tx_env
1577 .as_ref()
1578 .expect("validate_aa_initial_tx_gas called for non-AA transaction");
1579
1580 let calls = &aa_env.aa_calls;
1581
1582 let max_initcode_size = evm.ctx_ref().cfg().max_initcode_size();
1584 for call in calls {
1585 if call.to.is_create() && call.input.len() > max_initcode_size {
1586 return Err(InvalidTransaction::CreateInitCodeSizeLimit.into());
1587 }
1588 }
1589
1590 let mut batch_gas =
1592 calculate_aa_batch_intrinsic_gas(aa_env, gas_params, tx.access_list(), spec)?;
1593
1594 let mut nonce_2d_gas = 0;
1595
1596 if spec.is_t1() {
1599 if aa_env.nonce_key == TEMPO_EXPIRING_NONCE_KEY {
1600 batch_gas.initial_gas += EXPIRING_NONCE_GAS;
1605 } else if tx.nonce == 0 {
1606 batch_gas.initial_gas += gas_params.get(GasId::new_account_cost());
1609 } else if !aa_env.nonce_key.is_zero() {
1610 batch_gas.initial_gas += spec.gas_existing_nonce_key();
1613 }
1614 } else if let Some(aa_env) = &tx.tempo_tx_env
1615 && !aa_env.nonce_key.is_zero()
1616 {
1617 nonce_2d_gas = if tx.nonce() == 0 {
1618 spec.gas_new_nonce_key()
1619 } else {
1620 spec.gas_existing_nonce_key()
1621 };
1622 };
1623
1624 if evm.ctx.cfg.is_eip7623_disabled() {
1625 batch_gas.floor_gas = 0u64;
1626 }
1627
1628 if spec.is_t0() {
1633 batch_gas.initial_gas += nonce_2d_gas;
1634 }
1635
1636 if gas_limit < batch_gas.initial_gas {
1638 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1639 gas_limit,
1640 intrinsic_gas: batch_gas.initial_gas,
1641 }
1642 .into());
1643 }
1644
1645 if !spec.is_t0() {
1648 batch_gas.initial_gas += nonce_2d_gas;
1649 }
1650
1651 if !evm.ctx.cfg.is_eip7623_disabled() && gas_limit < batch_gas.floor_gas {
1653 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1654 gas_limit,
1655 intrinsic_gas: batch_gas.floor_gas,
1656 }
1657 .into());
1658 }
1659
1660 Ok(batch_gas)
1661}
1662
1663pub fn get_token_balance<JOURNAL>(
1665 journal: &mut JOURNAL,
1666 token: Address,
1667 sender: Address,
1668) -> Result<U256, <JOURNAL::Database as Database>::Error>
1669where
1670 JOURNAL: JournalTr,
1671{
1672 journal.load_account(token)?;
1674 let balance_slot = TIP20Token::from_address(token)
1675 .expect("TIP20 prefix already validated")
1676 .balances[sender]
1677 .slot();
1678 let balance = journal.sload(token, balance_slot)?.data;
1679
1680 Ok(balance)
1681}
1682
1683impl<DB, I> InspectorHandler for TempoEvmHandler<DB, I>
1684where
1685 DB: alloy_evm::Database,
1686 I: Inspector<TempoContext<DB>>,
1687{
1688 type IT = EthInterpreter;
1689
1690 fn inspect_run(
1691 &mut self,
1692 evm: &mut Self::Evm,
1693 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1694 self.load_fee_fields(evm)?;
1695
1696 match self.inspect_run_without_catch_error(evm) {
1697 Ok(output) => Ok(output),
1698 Err(e) => self.catch_error(evm, e),
1699 }
1700 }
1701
1702 #[inline]
1707 fn inspect_execution(
1708 &mut self,
1709 evm: &mut Self::Evm,
1710 init_and_floor_gas: &InitialAndFloorGas,
1711 ) -> Result<FrameResult, Self::Error> {
1712 self.inspect_execution_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop)
1713 }
1714}
1715
1716#[inline]
1720fn oog_frame_result(kind: TxKind, gas_limit: u64) -> FrameResult {
1721 if kind.is_call() {
1722 FrameResult::new_call_oog(gas_limit, 0..0)
1723 } else {
1724 FrameResult::new_create_oog(gas_limit)
1725 }
1726}
1727
1728#[inline]
1733fn check_gas_limit(
1734 spec: tempo_chainspec::hardfork::TempoHardfork,
1735 tx: &TempoTxEnv,
1736 adjusted_gas: &InitialAndFloorGas,
1737) -> Option<FrameResult> {
1738 if spec.is_t0() && tx.gas_limit() < adjusted_gas.initial_gas {
1739 let kind = *tx
1740 .first_call()
1741 .expect("we already checked that there is at least one call in aa tx")
1742 .0;
1743 return Some(oog_frame_result(kind, tx.gas_limit()));
1744 }
1745 None
1746}
1747
1748pub fn validate_time_window(
1756 valid_after: Option<u64>,
1757 valid_before: Option<u64>,
1758 block_timestamp: u64,
1759) -> Result<(), TempoInvalidTransaction> {
1760 if let Some(after) = valid_after
1762 && block_timestamp < after
1763 {
1764 return Err(TempoInvalidTransaction::ValidAfter {
1765 current: block_timestamp,
1766 valid_after: after,
1767 });
1768 }
1769
1770 if let Some(before) = valid_before
1773 && block_timestamp >= before
1774 {
1775 return Err(TempoInvalidTransaction::ValidBefore {
1776 current: block_timestamp,
1777 valid_before: before,
1778 });
1779 }
1780
1781 Ok(())
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786 use super::*;
1787 use crate::{TempoBlockEnv, TempoTxEnv, evm::TempoEvm, tx::TempoBatchCallEnv};
1788 use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
1789 use proptest::prelude::*;
1790 use revm::{
1791 Context, Journal, MainContext,
1792 context::CfgEnv,
1793 database::{CacheDB, EmptyDB},
1794 handler::Handler,
1795 interpreter::{gas::COLD_ACCOUNT_ACCESS_COST, instructions::utility::IntoU256},
1796 primitives::hardfork::SpecId,
1797 };
1798 use tempo_chainspec::hardfork::TempoHardfork;
1799 use tempo_contracts::precompiles::DEFAULT_FEE_TOKEN;
1800 use tempo_precompiles::{PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS};
1801 use tempo_primitives::transaction::{
1802 Call, TempoSignature,
1803 tt_signature::{P256SignatureWithPreHash, WebAuthnSignature},
1804 };
1805
1806 fn create_test_journal() -> Journal<CacheDB<EmptyDB>> {
1807 let db = CacheDB::new(EmptyDB::default());
1808 Journal::new(db)
1809 }
1810
1811 #[test]
1812 fn test_invalid_fee_token_rejected() {
1813 let invalid_token = Address::random(); assert!(
1818 !is_tip20_prefix(invalid_token),
1819 "Test requires a non-TIP20 address"
1820 );
1821
1822 let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
1823
1824 let tx_env = TempoTxEnv {
1826 fee_token: Some(invalid_token),
1827 ..Default::default()
1828 };
1829
1830 let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
1831 Context::mainnet()
1832 .with_db(CacheDB::new(EmptyDB::default()))
1833 .with_block(TempoBlockEnv::default())
1834 .with_cfg(Default::default())
1835 .with_tx(tx_env),
1836 (),
1837 );
1838
1839 let result = handler.load_fee_fields(&mut evm);
1840
1841 assert!(
1842 matches!(
1843 result,
1844 Err(EVMError::Transaction(TempoInvalidTransaction::InvalidFeeToken(addr))) if addr == invalid_token
1845 ),
1846 "Should reject invalid fee token with InvalidFeeToken error"
1847 );
1848 }
1849
1850 #[test]
1851 fn test_self_sponsored_fee_payer_rejected_post_t2() {
1852 let caller = Address::random();
1853 let invalid_token = Address::random();
1854
1855 let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
1856 let mut cfg = CfgEnv::<TempoHardfork>::default();
1857 cfg.spec = TempoHardfork::T2;
1858
1859 let tx_env = TempoTxEnv {
1860 inner: revm::context::TxEnv {
1861 caller,
1862 ..Default::default()
1863 },
1864 fee_token: Some(invalid_token),
1865 fee_payer: Some(Some(caller)),
1866 ..Default::default()
1867 };
1868
1869 let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
1870 Context::mainnet()
1871 .with_db(CacheDB::new(EmptyDB::default()))
1872 .with_block(TempoBlockEnv::default())
1873 .with_cfg(cfg)
1874 .with_tx(tx_env),
1875 (),
1876 );
1877
1878 let result = handler.load_fee_fields(&mut evm);
1879 assert!(matches!(
1880 result,
1881 Err(EVMError::Transaction(
1882 TempoInvalidTransaction::SelfSponsoredFeePayer
1883 ))
1884 ));
1885 }
1886
1887 #[test]
1888 fn test_self_sponsored_fee_payer_not_rejected_pre_t2() {
1889 let caller = Address::random();
1890 let invalid_token = Address::random();
1891
1892 let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::default();
1893 let mut cfg = CfgEnv::<TempoHardfork>::default();
1894 cfg.spec = TempoHardfork::T1C;
1895
1896 let tx_env = TempoTxEnv {
1897 inner: revm::context::TxEnv {
1898 caller,
1899 ..Default::default()
1900 },
1901 fee_token: Some(invalid_token),
1902 fee_payer: Some(Some(caller)),
1903 ..Default::default()
1904 };
1905
1906 let mut evm: TempoEvm<CacheDB<EmptyDB>, ()> = TempoEvm::new(
1907 Context::mainnet()
1908 .with_db(CacheDB::new(EmptyDB::default()))
1909 .with_block(TempoBlockEnv::default())
1910 .with_cfg(cfg)
1911 .with_tx(tx_env),
1912 (),
1913 );
1914
1915 let result = handler.load_fee_fields(&mut evm);
1916 assert!(matches!(
1917 result,
1918 Err(EVMError::Transaction(TempoInvalidTransaction::InvalidFeeToken(addr)))
1919 if addr == invalid_token
1920 ));
1921 }
1922
1923 #[test]
1924 fn test_get_token_balance() -> eyre::Result<()> {
1925 let mut journal = create_test_journal();
1926 let token = PATH_USD_ADDRESS;
1928 let account = Address::random();
1929 let expected_balance = U256::random();
1930
1931 let balance_slot = TIP20Token::from_address(token)?.balances[account].slot();
1933 journal.load_account(token)?;
1934 journal
1935 .sstore(token, balance_slot, expected_balance)
1936 .unwrap();
1937
1938 let balance = get_token_balance(&mut journal, token, account)?;
1939 assert_eq!(balance, expected_balance);
1940
1941 Ok(())
1942 }
1943
1944 #[test]
1945 fn test_get_fee_token() -> eyre::Result<()> {
1946 let journal = create_test_journal();
1947 let mut ctx: TempoContext<_> = Context::mainnet()
1948 .with_db(CacheDB::new(EmptyDB::default()))
1949 .with_block(TempoBlockEnv::default())
1950 .with_cfg(Default::default())
1951 .with_tx(TempoTxEnv::default())
1952 .with_new_journal(journal);
1953 let user = Address::random();
1954 ctx.tx.inner.caller = user;
1955 let validator = Address::random();
1956 ctx.block.beneficiary = validator;
1957 let user_fee_token = Address::random();
1958 let validator_fee_token = Address::random();
1959 let tx_fee_token = Address::random();
1960
1961 let validator_slot = TipFeeManager::new().validator_tokens[validator].slot();
1963 ctx.journaled_state.load_account(TIP_FEE_MANAGER_ADDRESS)?;
1964 ctx.journaled_state
1965 .sstore(
1966 TIP_FEE_MANAGER_ADDRESS,
1967 validator_slot,
1968 validator_fee_token.into_u256(),
1969 )
1970 .unwrap();
1971
1972 {
1973 let fee_token = ctx
1974 .journaled_state
1975 .get_fee_token(&ctx.tx, user, ctx.cfg.spec)?;
1976 assert_eq!(DEFAULT_FEE_TOKEN, fee_token);
1977 }
1978
1979 let user_slot = TipFeeManager::new().user_tokens[user].slot();
1981 ctx.journaled_state
1982 .sstore(
1983 TIP_FEE_MANAGER_ADDRESS,
1984 user_slot,
1985 user_fee_token.into_u256(),
1986 )
1987 .unwrap();
1988
1989 {
1990 let fee_token = ctx
1991 .journaled_state
1992 .get_fee_token(&ctx.tx, user, ctx.cfg.spec)?;
1993 assert_eq!(user_fee_token, fee_token);
1994 }
1995
1996 ctx.tx.fee_token = Some(tx_fee_token);
1998 let fee_token = ctx
1999 .journaled_state
2000 .get_fee_token(&ctx.tx, user, ctx.cfg.spec)?;
2001 assert_eq!(tx_fee_token, fee_token);
2002
2003 Ok(())
2004 }
2005
2006 #[test]
2007 fn test_aa_gas_single_call_vs_normal_tx() {
2008 use crate::TempoBatchCallEnv;
2009 use alloy_primitives::{Bytes, TxKind};
2010 use revm::interpreter::gas::calculate_initial_tx_gas;
2011 use tempo_primitives::transaction::{Call, TempoSignature};
2012 let gas_params = GasParams::default();
2013
2014 let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); let to = Address::random();
2017
2018 let call = Call {
2020 to: TxKind::Call(to),
2021 value: U256::ZERO,
2022 input: calldata.clone(),
2023 };
2024
2025 let aa_env = TempoBatchCallEnv {
2026 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2027 alloy_primitives::Signature::test_signature(),
2028 )), aa_calls: vec![call],
2030 key_authorization: None,
2031 signature_hash: B256::ZERO,
2032 ..Default::default()
2033 };
2034
2035 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2037 let aa_gas = calculate_aa_batch_intrinsic_gas(
2038 &aa_env,
2039 &gas_params,
2040 None::<std::iter::Empty<&AccessListItem>>, spec,
2042 )
2043 .unwrap();
2044
2045 let normal_tx_gas = calculate_initial_tx_gas(
2047 spec.into(),
2048 &calldata,
2049 false, 0, 0, 0, );
2054
2055 assert_eq!(aa_gas.initial_gas, normal_tx_gas.initial_gas);
2057 }
2058
2059 #[test]
2060 fn test_aa_gas_multiple_calls_overhead() {
2061 use crate::TempoBatchCallEnv;
2062 use alloy_primitives::{Bytes, TxKind};
2063 use revm::interpreter::gas::calculate_initial_tx_gas;
2064 use tempo_primitives::transaction::{Call, TempoSignature};
2065
2066 let calldata = Bytes::from(vec![1, 2, 3]); let calls = vec![
2069 Call {
2070 to: TxKind::Call(Address::random()),
2071 value: U256::ZERO,
2072 input: calldata.clone(),
2073 },
2074 Call {
2075 to: TxKind::Call(Address::random()),
2076 value: U256::ZERO,
2077 input: calldata.clone(),
2078 },
2079 Call {
2080 to: TxKind::Call(Address::random()),
2081 value: U256::ZERO,
2082 input: calldata.clone(),
2083 },
2084 ];
2085
2086 let aa_env = TempoBatchCallEnv {
2087 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2088 alloy_primitives::Signature::test_signature(),
2089 )),
2090 aa_calls: calls,
2091 key_authorization: None,
2092 signature_hash: B256::ZERO,
2093 ..Default::default()
2094 };
2095
2096 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2097 let gas = calculate_aa_batch_intrinsic_gas(
2098 &aa_env,
2099 &GasParams::default(),
2100 None::<std::iter::Empty<&AccessListItem>>,
2101 spec,
2102 )
2103 .unwrap();
2104
2105 let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
2107
2108 let expected = base_tx_gas.initial_gas
2111 + 2 * (calldata.len() as u64 * 16)
2112 + 2 * COLD_ACCOUNT_ACCESS_COST;
2113 assert_eq!(gas.initial_gas, expected,);
2115 }
2116
2117 #[test]
2118 fn test_aa_gas_p256_signature() {
2119 use crate::TempoBatchCallEnv;
2120 use alloy_primitives::{B256, Bytes, TxKind};
2121 use revm::interpreter::gas::calculate_initial_tx_gas;
2122 use tempo_primitives::transaction::{
2123 Call, TempoSignature, tt_signature::P256SignatureWithPreHash,
2124 };
2125
2126 let spec = SpecId::CANCUN;
2127 let calldata = Bytes::from(vec![1, 2]);
2128
2129 let call = Call {
2130 to: TxKind::Call(Address::random()),
2131 value: U256::ZERO,
2132 input: calldata.clone(),
2133 };
2134
2135 let aa_env = TempoBatchCallEnv {
2136 signature: TempoSignature::Primitive(PrimitiveSignature::P256(
2137 P256SignatureWithPreHash {
2138 r: B256::ZERO,
2139 s: B256::ZERO,
2140 pub_key_x: B256::ZERO,
2141 pub_key_y: B256::ZERO,
2142 pre_hash: false,
2143 },
2144 )),
2145 aa_calls: vec![call],
2146 key_authorization: None,
2147 signature_hash: B256::ZERO,
2148 ..Default::default()
2149 };
2150
2151 let gas = calculate_aa_batch_intrinsic_gas(
2152 &aa_env,
2153 &GasParams::default(),
2154 None::<std::iter::Empty<&AccessListItem>>,
2155 tempo_chainspec::hardfork::TempoHardfork::default(),
2156 )
2157 .unwrap();
2158
2159 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
2161
2162 let expected = base_gas.initial_gas + P256_VERIFY_GAS;
2164 assert_eq!(gas.initial_gas, expected,);
2165 }
2166
2167 #[test]
2168 fn test_aa_gas_create_call() {
2169 use crate::TempoBatchCallEnv;
2170 use alloy_primitives::{Bytes, TxKind};
2171 use revm::interpreter::gas::calculate_initial_tx_gas;
2172 use tempo_primitives::transaction::{Call, TempoSignature};
2173
2174 let spec = SpecId::CANCUN; let initcode = Bytes::from(vec![0x60, 0x80]); let call = Call {
2178 to: TxKind::Create,
2179 value: U256::ZERO,
2180 input: initcode.clone(),
2181 };
2182
2183 let aa_env = TempoBatchCallEnv {
2184 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2185 alloy_primitives::Signature::test_signature(),
2186 )),
2187 aa_calls: vec![call],
2188 key_authorization: None,
2189 signature_hash: B256::ZERO,
2190 ..Default::default()
2191 };
2192
2193 let gas = calculate_aa_batch_intrinsic_gas(
2194 &aa_env,
2195 &GasParams::default(),
2196 None::<std::iter::Empty<&AccessListItem>>,
2197 tempo_chainspec::hardfork::TempoHardfork::default(),
2198 )
2199 .unwrap();
2200
2201 let base_gas = calculate_initial_tx_gas(
2203 spec, &initcode, true, 0, 0, 0,
2205 );
2206
2207 assert_eq!(gas.initial_gas, base_gas.initial_gas,);
2209 }
2210
2211 #[test]
2212 fn test_aa_gas_value_transfer() {
2213 use crate::TempoBatchCallEnv;
2214 use alloy_primitives::{Bytes, TxKind};
2215 use tempo_primitives::transaction::{Call, TempoSignature};
2216
2217 let calldata = Bytes::from(vec![1]);
2218
2219 let call = Call {
2220 to: TxKind::Call(Address::random()),
2221 value: U256::from(1000), input: calldata,
2223 };
2224
2225 let aa_env = TempoBatchCallEnv {
2226 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2227 alloy_primitives::Signature::test_signature(),
2228 )),
2229 aa_calls: vec![call],
2230 key_authorization: None,
2231 signature_hash: B256::ZERO,
2232 ..Default::default()
2233 };
2234
2235 let res = calculate_aa_batch_intrinsic_gas(
2236 &aa_env,
2237 &GasParams::default(),
2238 None::<std::iter::Empty<&AccessListItem>>,
2239 tempo_chainspec::hardfork::TempoHardfork::default(),
2240 );
2241
2242 assert_eq!(
2243 res.unwrap_err(),
2244 TempoInvalidTransaction::ValueTransferNotAllowedInAATx
2245 );
2246 }
2247
2248 #[test]
2249 fn test_aa_gas_access_list() {
2250 use crate::TempoBatchCallEnv;
2251 use alloy_primitives::{Bytes, TxKind};
2252 use revm::interpreter::gas::calculate_initial_tx_gas;
2253 use tempo_primitives::transaction::{Call, TempoSignature};
2254
2255 let spec = SpecId::CANCUN;
2256 let calldata = Bytes::from(vec![]);
2257
2258 let call = Call {
2259 to: TxKind::Call(Address::random()),
2260 value: U256::ZERO,
2261 input: calldata.clone(),
2262 };
2263
2264 let aa_env = TempoBatchCallEnv {
2265 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2266 alloy_primitives::Signature::test_signature(),
2267 )),
2268 aa_calls: vec![call],
2269 key_authorization: None,
2270 signature_hash: B256::ZERO,
2271 ..Default::default()
2272 };
2273
2274 let gas = calculate_aa_batch_intrinsic_gas(
2276 &aa_env,
2277 &GasParams::default(),
2278 None::<std::iter::Empty<&AccessListItem>>,
2279 tempo_chainspec::hardfork::TempoHardfork::default(),
2280 )
2281 .unwrap();
2282
2283 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
2285
2286 assert_eq!(gas.initial_gas, base_gas.initial_gas,);
2288 }
2289
2290 #[test]
2291 fn test_key_authorization_rlp_encoding() {
2292 use alloy_primitives::{Address, U256};
2293 use tempo_primitives::transaction::{
2294 SignatureType, TokenLimit, key_authorization::KeyAuthorization,
2295 };
2296
2297 let chain_id = 1u64;
2299 let key_type = SignatureType::Secp256k1;
2300 let key_id = Address::random();
2301 let expiry = 1000u64;
2302 let limits = vec![
2303 TokenLimit {
2304 token: Address::random(),
2305 limit: U256::from(100),
2306 },
2307 TokenLimit {
2308 token: Address::random(),
2309 limit: U256::from(200),
2310 },
2311 ];
2312
2313 let hash1 = KeyAuthorization {
2315 chain_id,
2316 key_type,
2317 key_id,
2318 expiry: Some(expiry),
2319 limits: Some(limits.clone()),
2320 }
2321 .signature_hash();
2322
2323 let hash2 = KeyAuthorization {
2325 chain_id,
2326 key_type,
2327 key_id,
2328 expiry: Some(expiry),
2329 limits: Some(limits.clone()),
2330 }
2331 .signature_hash();
2332
2333 assert_eq!(hash1, hash2, "Hash computation should be deterministic");
2334
2335 let hash3 = KeyAuthorization {
2337 chain_id: 2,
2338 key_type,
2339 key_id,
2340 expiry: Some(expiry),
2341 limits: Some(limits),
2342 }
2343 .signature_hash();
2344 assert_ne!(
2345 hash1, hash3,
2346 "Different chain_id should produce different hash"
2347 );
2348 }
2349
2350 #[test]
2351 fn test_aa_gas_floor_gas_prague() {
2352 use crate::TempoBatchCallEnv;
2353 use alloy_primitives::{Bytes, TxKind};
2354 use revm::interpreter::gas::calculate_initial_tx_gas;
2355 use tempo_primitives::transaction::{Call, TempoSignature};
2356
2357 let spec = SpecId::PRAGUE;
2358 let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); let call = Call {
2361 to: TxKind::Call(Address::random()),
2362 value: U256::ZERO,
2363 input: calldata.clone(),
2364 };
2365
2366 let aa_env = TempoBatchCallEnv {
2367 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2368 alloy_primitives::Signature::test_signature(),
2369 )),
2370 aa_calls: vec![call],
2371 key_authorization: None,
2372 signature_hash: B256::ZERO,
2373 ..Default::default()
2374 };
2375
2376 let gas = calculate_aa_batch_intrinsic_gas(
2377 &aa_env,
2378 &GasParams::default(),
2379 None::<std::iter::Empty<&AccessListItem>>,
2380 tempo_chainspec::hardfork::TempoHardfork::default(),
2381 )
2382 .unwrap();
2383
2384 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
2386
2387 assert_eq!(
2389 gas.floor_gas, base_gas.floor_gas,
2390 "Should calculate floor gas for Prague matching revm"
2391 );
2392 }
2393
2394 #[test]
2397 fn test_zero_value_transfer() -> eyre::Result<()> {
2398 use crate::TempoEvm;
2399
2400 let ctx = Context::mainnet()
2402 .with_db(CacheDB::new(EmptyDB::default()))
2403 .with_block(Default::default())
2404 .with_cfg(Default::default())
2405 .with_tx(TempoTxEnv::default());
2406 let mut evm = TempoEvm::new(ctx, ());
2407
2408 evm.ctx.tx.inner.value = U256::from(1000);
2410
2411 let handler = TempoEvmHandler::<_, ()>::new();
2413
2414 let result = handler.validate_env(&mut evm);
2416
2417 if let Err(EVMError::Transaction(err)) = result {
2418 assert_eq!(err, TempoInvalidTransaction::ValueTransferNotAllowed);
2419 } else {
2420 panic!("Expected ValueTransferNotAllowed error");
2421 }
2422
2423 Ok(())
2424 }
2425
2426 #[test]
2427 fn test_key_authorization_gas_with_limits() {
2428 use tempo_primitives::transaction::{
2429 KeyAuthorization, SignatureType, SignedKeyAuthorization, TokenLimit,
2430 };
2431
2432 let create_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
2434 let limits = if num_limits == 0 {
2435 None
2436 } else {
2437 Some(
2438 (0..num_limits)
2439 .map(|_| TokenLimit {
2440 token: Address::random(),
2441 limit: U256::from(1000),
2442 })
2443 .collect(),
2444 )
2445 };
2446
2447 SignedKeyAuthorization {
2448 authorization: KeyAuthorization {
2449 chain_id: 1,
2450 key_type: SignatureType::Secp256k1,
2451 key_id: Address::random(),
2452 expiry: None,
2453 limits,
2454 },
2455 signature: PrimitiveSignature::Secp256k1(
2456 alloy_primitives::Signature::test_signature(),
2457 ),
2458 }
2459 };
2460
2461 let gas_0 = calculate_key_authorization_gas(
2463 &create_key_auth(0),
2464 &GasParams::default(),
2465 tempo_chainspec::hardfork::TempoHardfork::default(),
2466 );
2467 assert_eq!(
2468 gas_0,
2469 KEY_AUTH_BASE_GAS + ECRECOVER_GAS,
2470 "0 limits should be 30,000"
2471 );
2472
2473 let gas_1 = calculate_key_authorization_gas(
2475 &create_key_auth(1),
2476 &GasParams::default(),
2477 tempo_chainspec::hardfork::TempoHardfork::default(),
2478 );
2479 assert_eq!(
2480 gas_1,
2481 KEY_AUTH_BASE_GAS + ECRECOVER_GAS + KEY_AUTH_PER_LIMIT_GAS,
2482 "1 limit should be 52,000"
2483 );
2484
2485 let gas_2 = calculate_key_authorization_gas(
2487 &create_key_auth(2),
2488 &GasParams::default(),
2489 tempo_chainspec::hardfork::TempoHardfork::default(),
2490 );
2491 assert_eq!(
2492 gas_2,
2493 KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS,
2494 "2 limits should be 74,000"
2495 );
2496
2497 let gas_3 = calculate_key_authorization_gas(
2499 &create_key_auth(3),
2500 &GasParams::default(),
2501 tempo_chainspec::hardfork::TempoHardfork::default(),
2502 );
2503 assert_eq!(
2504 gas_3,
2505 KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 3 * KEY_AUTH_PER_LIMIT_GAS,
2506 "3 limits should be 96,000"
2507 );
2508
2509 let t1b_gas_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
2511 let sstore =
2512 t1b_gas_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
2513 let sload =
2514 t1b_gas_params.warm_storage_read_cost() + t1b_gas_params.cold_storage_additional_cost();
2515 const BUFFER: u64 = 2_000;
2516
2517 for num_limits in 0..=3 {
2518 let gas = calculate_key_authorization_gas(
2519 &create_key_auth(num_limits),
2520 &t1b_gas_params,
2521 TempoHardfork::T1B,
2522 );
2523 let expected = ECRECOVER_GAS + sload + sstore * (1 + num_limits as u64) + BUFFER;
2524 assert_eq!(gas, expected, "T1B with {num_limits} limits");
2525 }
2526 }
2527
2528 #[test]
2529 fn test_key_authorization_gas_in_batch() {
2530 use crate::TempoBatchCallEnv;
2531 use alloy_primitives::{Bytes, TxKind};
2532 use revm::interpreter::gas::calculate_initial_tx_gas;
2533 use tempo_primitives::transaction::{
2534 Call, KeyAuthorization, SignatureType, SignedKeyAuthorization, TempoSignature,
2535 TokenLimit,
2536 };
2537
2538 let calldata = Bytes::from(vec![1, 2, 3]);
2539
2540 let call = Call {
2541 to: TxKind::Call(Address::random()),
2542 value: U256::ZERO,
2543 input: calldata.clone(),
2544 };
2545
2546 let key_auth: SignedKeyAuthorization = SignedKeyAuthorization {
2548 authorization: KeyAuthorization {
2549 chain_id: 1,
2550 key_type: SignatureType::Secp256k1,
2551 key_id: Address::random(),
2552 expiry: None,
2553 limits: Some(vec![
2554 TokenLimit {
2555 token: Address::random(),
2556 limit: U256::from(1000),
2557 },
2558 TokenLimit {
2559 token: Address::random(),
2560 limit: U256::from(2000),
2561 },
2562 ]),
2563 },
2564 signature: PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
2565 };
2566
2567 let aa_env_with_key_auth = TempoBatchCallEnv {
2568 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2569 alloy_primitives::Signature::test_signature(),
2570 )),
2571 aa_calls: vec![call.clone()],
2572 key_authorization: Some(key_auth),
2573 signature_hash: B256::ZERO,
2574 ..Default::default()
2575 };
2576
2577 let aa_env_without_key_auth = TempoBatchCallEnv {
2578 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2579 alloy_primitives::Signature::test_signature(),
2580 )),
2581 aa_calls: vec![call],
2582 key_authorization: None,
2583 signature_hash: B256::ZERO,
2584 ..Default::default()
2585 };
2586
2587 let gas_with_key_auth = calculate_aa_batch_intrinsic_gas(
2589 &aa_env_with_key_auth,
2590 &GasParams::default(),
2591 None::<std::iter::Empty<&AccessListItem>>,
2592 tempo_chainspec::hardfork::TempoHardfork::default(),
2593 )
2594 .unwrap();
2595
2596 let gas_without_key_auth = calculate_aa_batch_intrinsic_gas(
2598 &aa_env_without_key_auth,
2599 &GasParams::default(),
2600 None::<std::iter::Empty<&AccessListItem>>,
2601 tempo_chainspec::hardfork::TempoHardfork::default(),
2602 )
2603 .unwrap();
2604
2605 let expected_key_auth_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS + 2 * KEY_AUTH_PER_LIMIT_GAS;
2607
2608 assert_eq!(
2609 gas_with_key_auth.initial_gas - gas_without_key_auth.initial_gas,
2610 expected_key_auth_gas,
2611 "Key authorization should add exactly {expected_key_auth_gas} gas to batch",
2612 );
2613
2614 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
2616 let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
2617 let expected_without = base_tx_gas.initial_gas; let expected_with = expected_without + expected_key_auth_gas;
2619
2620 assert_eq!(
2621 gas_without_key_auth.initial_gas, expected_without,
2622 "Gas without key auth should match expected"
2623 );
2624 assert_eq!(
2625 gas_with_key_auth.initial_gas, expected_with,
2626 "Gas with key auth should match expected"
2627 );
2628 }
2629
2630 #[test]
2631 fn test_2d_nonce_gas_in_intrinsic_gas() {
2632 use crate::gas_params::tempo_gas_params;
2633 use revm::{context_interface::cfg::GasId, handler::Handler};
2634
2635 const BASE_INTRINSIC_GAS: u64 = 21_000;
2636
2637 for spec in [
2638 TempoHardfork::Genesis,
2639 TempoHardfork::T0,
2640 TempoHardfork::T1,
2641 TempoHardfork::T1A,
2642 TempoHardfork::T1B,
2643 TempoHardfork::T2,
2644 ] {
2645 let gas_params = tempo_gas_params(spec);
2646
2647 let make_evm = |nonce: u64, nonce_key: U256| {
2648 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
2649 let mut cfg = CfgEnv::<TempoHardfork>::default();
2650 cfg.spec = spec;
2651 cfg.gas_params = gas_params.clone();
2652 let ctx = Context::mainnet()
2653 .with_db(CacheDB::new(EmptyDB::default()))
2654 .with_block(TempoBlockEnv::default())
2655 .with_cfg(cfg)
2656 .with_tx(TempoTxEnv {
2657 inner: revm::context::TxEnv {
2658 gas_limit: 1_000_000,
2659 nonce,
2660 ..Default::default()
2661 },
2662 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
2663 aa_calls: vec![Call {
2664 to: TxKind::Call(Address::random()),
2665 value: U256::ZERO,
2666 input: Bytes::new(),
2667 }],
2668 nonce_key,
2669 ..Default::default()
2670 })),
2671 ..Default::default()
2672 })
2673 .with_new_journal(journal);
2674 TempoEvm::<_, ()>::new(ctx, ())
2675 };
2676
2677 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
2678
2679 {
2681 let mut evm = make_evm(5, U256::ZERO);
2682 let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
2683 assert_eq!(
2684 gas.initial_gas, BASE_INTRINSIC_GAS,
2685 "{spec:?}: protocol nonce (nonce_key=0, nonce>0) should have no extra gas"
2686 );
2687 }
2688
2689 {
2691 let expected = if spec.is_t1() {
2692 BASE_INTRINSIC_GAS + gas_params.get(GasId::new_account_cost())
2694 } else {
2695 BASE_INTRINSIC_GAS + spec.gas_new_nonce_key()
2697 };
2698 let mut evm = make_evm(0, U256::from(42));
2699 let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
2700 assert_eq!(
2701 gas.initial_gas, expected,
2702 "{spec:?}: nonce_key!=0, nonce==0 gas mismatch"
2703 );
2704 }
2705
2706 {
2708 let mut evm = make_evm(5, U256::from(42));
2709 let gas = handler.validate_initial_tx_gas(&mut evm).unwrap();
2710 assert_eq!(
2711 gas.initial_gas,
2712 BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(),
2713 "{spec:?}: existing 2D nonce key gas mismatch"
2714 );
2715 }
2716 }
2717 }
2718
2719 #[test]
2720 fn test_2d_nonce_gas_limit_validation() {
2721 use crate::gas_params::tempo_gas_params;
2722 use revm::{context_interface::cfg::GasId, handler::Handler};
2723
2724 const BASE_INTRINSIC_GAS: u64 = 21_000;
2725
2726 for spec in [
2727 TempoHardfork::Genesis,
2728 TempoHardfork::T0,
2729 TempoHardfork::T1,
2730 TempoHardfork::T2,
2731 ] {
2732 let gas_params = tempo_gas_params(spec);
2733
2734 let nonce_zero_gas = if spec.is_t1() {
2736 gas_params.get(GasId::new_account_cost())
2737 } else {
2738 spec.gas_new_nonce_key()
2739 };
2740
2741 let cases = if spec.is_t0() {
2742 vec![
2743 (BASE_INTRINSIC_GAS + 10_000, 0u64, false), (BASE_INTRINSIC_GAS + nonce_zero_gas, 0, true), (BASE_INTRINSIC_GAS + spec.gas_existing_nonce_key(), 1, true), ]
2747 } else {
2748 vec![
2750 (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), ]
2755 };
2756
2757 for (gas_limit, nonce, should_succeed) in cases {
2758 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
2759 let mut cfg = CfgEnv::<TempoHardfork>::default();
2760 cfg.spec = spec;
2761 cfg.gas_params = gas_params.clone();
2762 let ctx = Context::mainnet()
2763 .with_db(CacheDB::new(EmptyDB::default()))
2764 .with_block(TempoBlockEnv::default())
2765 .with_cfg(cfg)
2766 .with_tx(TempoTxEnv {
2767 inner: revm::context::TxEnv {
2768 gas_limit,
2769 nonce,
2770 ..Default::default()
2771 },
2772 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
2773 aa_calls: vec![Call {
2774 to: TxKind::Call(Address::random()),
2775 value: U256::ZERO,
2776 input: Bytes::new(),
2777 }],
2778 nonce_key: U256::from(1), ..Default::default()
2780 })),
2781 ..Default::default()
2782 })
2783 .with_new_journal(journal);
2784
2785 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
2786 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
2787 let result = handler.validate_initial_tx_gas(&mut evm);
2788
2789 if should_succeed {
2790 assert!(
2791 result.is_ok(),
2792 "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: expected success but got error"
2793 );
2794 } else {
2795 let err = result.expect_err(&format!(
2796 "{spec:?}: gas_limit={gas_limit}, nonce={nonce}: should fail"
2797 ));
2798 assert!(
2799 matches!(
2800 err.as_invalid_tx_err(),
2801 Some(TempoInvalidTransaction::InsufficientGasForIntrinsicCost { .. })
2802 ),
2803 "Expected InsufficientGasForIntrinsicCost, got: {err:?}"
2804 );
2805 }
2806 }
2807 }
2808 }
2809
2810 #[test]
2811 fn test_multicall_gas_refund_accounting() {
2812 use crate::evm::TempoEvm;
2813 use alloy_primitives::{Bytes, TxKind};
2814 use revm::{
2815 Context, Journal,
2816 context::CfgEnv,
2817 database::{CacheDB, EmptyDB},
2818 handler::FrameResult,
2819 interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult},
2820 };
2821 use tempo_primitives::transaction::Call;
2822
2823 const GAS_LIMIT: u64 = 1_000_000;
2824 const INTRINSIC_GAS: u64 = 21_000;
2825 const SPENT: (u64, u64) = (1000, 500);
2827 const REFUND: (i64, i64) = (100, 50);
2828
2829 let db = CacheDB::new(EmptyDB::default());
2831 let journal = Journal::new(db);
2832 let ctx = Context::mainnet()
2833 .with_db(CacheDB::new(EmptyDB::default()))
2834 .with_block(TempoBlockEnv::default())
2835 .with_cfg(CfgEnv::default())
2836 .with_tx(TempoTxEnv {
2837 inner: revm::context::TxEnv {
2838 gas_limit: GAS_LIMIT,
2839 ..Default::default()
2840 },
2841 ..Default::default()
2842 })
2843 .with_new_journal(journal);
2844
2845 let mut evm: TempoEvm<_, ()> = TempoEvm::new(ctx, ());
2846 let mut handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
2847
2848 let calls = vec![
2850 Call {
2851 to: TxKind::Call(Address::random()),
2852 value: U256::ZERO,
2853 input: Bytes::new(),
2854 },
2855 Call {
2856 to: TxKind::Call(Address::random()),
2857 value: U256::ZERO,
2858 input: Bytes::new(),
2859 },
2860 ];
2861
2862 let (mut call_idx, calls_gas) = (0, [(SPENT.0, REFUND.0), (SPENT.1, REFUND.1)]);
2863 let result = handler.execute_multi_call_with(
2864 &mut evm,
2865 &InitialAndFloorGas::new(INTRINSIC_GAS, 0),
2866 calls,
2867 |_handler, _evm, _gas| {
2868 let (spent, refund) = calls_gas[call_idx];
2869 call_idx += 1;
2870
2871 let mut gas = Gas::new(GAS_LIMIT);
2873 gas.set_spent(spent);
2874 gas.record_refund(refund);
2875
2876 Ok(FrameResult::Call(CallOutcome::new(
2878 InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas),
2879 0..0,
2880 )))
2881 },
2882 );
2883
2884 let result = result.expect("execute_multi_call_with should succeed");
2885 let final_gas = result.gas();
2886
2887 assert_eq!(
2888 final_gas.spent(),
2889 INTRINSIC_GAS + SPENT.0 + SPENT.1,
2890 "Total spent should be intrinsic_gas + sum of all calls' spent values"
2891 );
2892 assert_eq!(
2893 final_gas.refunded(),
2894 REFUND.0 + REFUND.1,
2895 "Total refund should be sum of all calls' refunded values"
2896 );
2897 assert_eq!(
2898 final_gas.used(),
2899 INTRINSIC_GAS + SPENT.0 + SPENT.1 - (REFUND.0 + REFUND.1) as u64,
2900 "used() should be spent - refund"
2901 );
2902 }
2903
2904 fn arb_opt_timestamp() -> impl Strategy<Value = Option<u64>> {
2906 prop_oneof![Just(None), any::<u64>().prop_map(Some)]
2907 }
2908
2909 fn secp256k1_sig() -> TempoSignature {
2916 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2917 alloy_primitives::Signature::test_signature(),
2918 ))
2919 }
2920
2921 fn make_aa_env(calls: Vec<Call>) -> TempoBatchCallEnv {
2923 TempoBatchCallEnv {
2924 signature: secp256k1_sig(),
2925 aa_calls: calls,
2926 key_authorization: None,
2927 signature_hash: B256::ZERO,
2928 ..Default::default()
2929 }
2930 }
2931
2932 fn make_single_call_env(calldata: Bytes) -> TempoBatchCallEnv {
2934 make_aa_env(vec![Call {
2935 to: TxKind::Call(Address::ZERO),
2936 value: U256::ZERO,
2937 input: calldata,
2938 }])
2939 }
2940
2941 fn make_multi_call_env(num_calls: usize) -> TempoBatchCallEnv {
2943 make_aa_env(
2944 (0..num_calls)
2945 .map(|_| Call {
2946 to: TxKind::Call(Address::ZERO),
2947 value: U256::ZERO,
2948 input: Bytes::new(),
2949 })
2950 .collect(),
2951 )
2952 }
2953
2954 fn compute_aa_gas(env: &TempoBatchCallEnv) -> InitialAndFloorGas {
2956 calculate_aa_batch_intrinsic_gas(
2957 env,
2958 &GasParams::default(),
2959 None::<std::iter::Empty<&AccessListItem>>,
2960 tempo_chainspec::hardfork::TempoHardfork::default(),
2961 )
2962 .unwrap()
2963 }
2964
2965 proptest! {
2966 #![proptest_config(ProptestConfig::with_cases(500))]
2967
2968 #[test]
2970 fn proptest_validate_time_window_correctness(
2971 valid_after in arb_opt_timestamp(),
2972 valid_before in arb_opt_timestamp(),
2973 block_timestamp in any::<u64>(),
2974 ) {
2975 let result = validate_time_window(valid_after, valid_before, block_timestamp);
2976
2977 let after_ok = valid_after.is_none_or(|after| block_timestamp >= after);
2978 let before_ok = valid_before.is_none_or(|before| block_timestamp < before);
2979 let expected_valid = after_ok && before_ok;
2980
2981 prop_assert_eq!(result.is_ok(), expected_valid,
2982 "valid_after={:?}, valid_before={:?}, block_ts={}, result={:?}",
2983 valid_after, valid_before, block_timestamp, result);
2984 }
2985
2986 #[test]
2988 fn proptest_validate_time_window_none_always_valid(block_timestamp in any::<u64>()) {
2989 prop_assert!(validate_time_window(None, None, block_timestamp).is_ok());
2990 }
2991
2992 #[test]
2999 fn proptest_validate_time_window_zero_after_equivalent_to_none(
3000 valid_before in arb_opt_timestamp(),
3001 block_timestamp in any::<u64>(),
3002 ) {
3003 let with_zero = validate_time_window(Some(0), valid_before, block_timestamp);
3004 let with_none = validate_time_window(None, valid_before, block_timestamp);
3005 prop_assert_eq!(with_zero.is_ok(), with_none.is_ok());
3006 }
3007
3008 #[test]
3010 fn proptest_validate_time_window_empty_window(
3011 valid_after in 1u64..=u64::MAX,
3012 offset in 0u64..1000u64,
3013 ) {
3014 let valid_before = valid_after.saturating_sub(offset);
3015 let result = validate_time_window(Some(valid_after), Some(valid_before), valid_after);
3016 prop_assert!(result.is_err(), "Empty window should reject all timestamps");
3017 }
3018
3019 #[test]
3021 fn proptest_signature_gas_ordering(webauthn_data_len in 0usize..1000) {
3022 let secp_sig = PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature());
3023 let p256_sig = PrimitiveSignature::P256(P256SignatureWithPreHash {
3024 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
3025 });
3026 let webauthn_sig = PrimitiveSignature::WebAuthn(WebAuthnSignature {
3027 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
3028 webauthn_data: Bytes::from(vec![0u8; webauthn_data_len]),
3029 });
3030
3031 let secp_gas = primitive_signature_verification_gas(&secp_sig);
3032 let p256_gas = primitive_signature_verification_gas(&p256_sig);
3033 let webauthn_gas = primitive_signature_verification_gas(&webauthn_sig);
3034
3035 prop_assert!(secp_gas <= p256_gas, "secp256k1 should be <= p256");
3036 prop_assert!(p256_gas <= webauthn_gas, "p256 should be <= webauthn");
3037 }
3038
3039 #[test]
3042 fn proptest_gas_monotonicity_calldata_nonzero(
3043 calldata_len1 in 0usize..1000,
3044 calldata_len2 in 0usize..1000,
3045 ) {
3046 let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len1])));
3047 let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len2])));
3048
3049 if calldata_len1 <= calldata_len2 {
3050 prop_assert!(gas1.initial_gas <= gas2.initial_gas,
3051 "More calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
3052 calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3053 } else {
3054 prop_assert!(gas1.initial_gas >= gas2.initial_gas,
3055 "Less calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
3056 calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3057 }
3058 }
3059
3060 #[test]
3063 fn proptest_gas_monotonicity_calldata_zero(
3064 calldata_len1 in 0usize..1000,
3065 calldata_len2 in 0usize..1000,
3066 ) {
3067 let gas1 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len1])));
3068 let gas2 = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len2])));
3069
3070 if calldata_len1 <= calldata_len2 {
3071 prop_assert!(gas1.initial_gas <= gas2.initial_gas,
3072 "More zero-byte calldata should mean more gas: len1={}, gas1={}, len2={}, gas2={}",
3073 calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3074 } else {
3075 prop_assert!(gas1.initial_gas >= gas2.initial_gas,
3076 "Less zero-byte calldata should mean less gas: len1={}, gas1={}, len2={}, gas2={}",
3077 calldata_len1, gas1.initial_gas, calldata_len2, gas2.initial_gas);
3078 }
3079 }
3080
3081 #[test]
3084 fn proptest_zero_bytes_cheaper_than_nonzero(calldata_len in 1usize..1000) {
3085 let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
3086 let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
3087
3088 prop_assert!(zero_gas.initial_gas < nonzero_gas.initial_gas,
3089 "Zero-byte calldata should cost less: len={}, zero_gas={}, nonzero_gas={}",
3090 calldata_len, zero_gas.initial_gas, nonzero_gas.initial_gas);
3091 }
3092
3093 #[test]
3096 fn proptest_mixed_calldata_gas_bounded(
3097 calldata_len in 1usize..500,
3098 nonzero_ratio in 0u8..=100,
3099 ) {
3100 let calldata: Vec<u8> = (0..calldata_len)
3102 .map(|i| if (i * 100 / calldata_len) < nonzero_ratio as usize { 1u8 } else { 0u8 })
3103 .collect();
3104
3105 let mixed_gas = compute_aa_gas(&make_single_call_env(Bytes::from(calldata)));
3106 let zero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![0u8; calldata_len])));
3107 let nonzero_gas = compute_aa_gas(&make_single_call_env(Bytes::from(vec![1u8; calldata_len])));
3108
3109 prop_assert!(mixed_gas.initial_gas >= zero_gas.initial_gas,
3110 "Mixed calldata gas should be >= all-zero gas: mixed={}, zero={}",
3111 mixed_gas.initial_gas, zero_gas.initial_gas);
3112 prop_assert!(mixed_gas.initial_gas <= nonzero_gas.initial_gas,
3113 "Mixed calldata gas should be <= all-nonzero gas: mixed={}, nonzero={}",
3114 mixed_gas.initial_gas, nonzero_gas.initial_gas);
3115 }
3116
3117 #[test]
3119 fn proptest_gas_monotonicity_call_count(
3120 num_calls1 in 1usize..10,
3121 num_calls2 in 1usize..10,
3122 ) {
3123 let gas1 = compute_aa_gas(&make_multi_call_env(num_calls1));
3124 let gas2 = compute_aa_gas(&make_multi_call_env(num_calls2));
3125
3126 if num_calls1 <= num_calls2 {
3127 prop_assert!(gas1.initial_gas <= gas2.initial_gas,
3128 "More calls should mean more gas: calls1={}, gas1={}, calls2={}, gas2={}",
3129 num_calls1, gas1.initial_gas, num_calls2, gas2.initial_gas);
3130 } else {
3131 prop_assert!(gas1.initial_gas >= gas2.initial_gas,
3132 "Fewer calls should mean less gas: calls1={}, gas1={}, calls2={}, gas2={}",
3133 num_calls1, gas1.initial_gas, num_calls2, gas2.initial_gas);
3134 }
3135 }
3136
3137 #[test]
3147 fn proptest_gas_aa_secp256k1_exact_bounds(num_calls in 1usize..5) {
3148 let gas = compute_aa_gas(&make_multi_call_env(num_calls));
3149
3150 let expected = 21_000 + COLD_ACCOUNT_ACCESS_COST * (num_calls.saturating_sub(1) as u64);
3152 prop_assert_eq!(gas.initial_gas, expected,
3153 "Gas {} should equal expected {} for {} calls (21k + {}*COLD_ACCOUNT_ACCESS_COST)",
3154 gas.initial_gas, expected, num_calls, num_calls.saturating_sub(1));
3155 }
3156
3157 #[test]
3159 fn proptest_first_call_returns_first_for_aa(num_calls in 1usize..10) {
3160 let calls: Vec<Call> = (0..num_calls)
3161 .map(|i| Call {
3162 to: TxKind::Call(Address::with_last_byte(i as u8)),
3163 value: U256::ZERO,
3164 input: Bytes::from(vec![i as u8; i + 1]),
3165 })
3166 .collect();
3167
3168 let expected_addr = Address::with_last_byte(0);
3169 let expected_input = vec![0u8; 1];
3170
3171 let tx_env = TempoTxEnv {
3172 inner: revm::context::TxEnv::default(),
3173 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3174 aa_calls: calls,
3175 signature: secp256k1_sig(),
3176 signature_hash: B256::ZERO,
3177 ..Default::default()
3178 })),
3179 ..Default::default()
3180 };
3181
3182 let first = tx_env.first_call();
3183 prop_assert!(first.is_some(), "first_call should return Some for non-empty AA calls");
3184
3185 let (kind, input) = first.unwrap();
3186 prop_assert_eq!(*kind, TxKind::Call(expected_addr), "Should return first call's address");
3187 prop_assert_eq!(input, expected_input.as_slice(), "Should return first call's input");
3188 }
3189
3190 #[test]
3192 fn proptest_first_call_empty_aa(_dummy in 0u8..1) {
3193 let tx_env = TempoTxEnv {
3194 inner: revm::context::TxEnv::default(),
3195 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3196 aa_calls: vec![],
3197 signature: secp256k1_sig(),
3198 signature_hash: B256::ZERO,
3199 ..Default::default()
3200 })),
3201 ..Default::default()
3202 };
3203
3204 prop_assert!(tx_env.first_call().is_none(), "first_call should return None for empty AA calls");
3205 }
3206
3207 #[test]
3209 fn proptest_first_call_non_aa(calldata_len in 0usize..100) {
3210 let calldata = Bytes::from(vec![0xab_u8; calldata_len]);
3211 let target = Address::random();
3212
3213 let tx_env = TempoTxEnv {
3214 inner: revm::context::TxEnv {
3215 kind: TxKind::Call(target),
3216 data: calldata.clone(),
3217 ..Default::default()
3218 },
3219 tempo_tx_env: None,
3220 ..Default::default()
3221 };
3222
3223 let first = tx_env.first_call();
3224 prop_assert!(first.is_some(), "first_call should return Some for non-AA tx");
3225
3226 let (kind, input) = first.unwrap();
3227 prop_assert_eq!(*kind, TxKind::Call(target), "Should return inner tx kind");
3228 prop_assert_eq!(input, calldata.as_ref(), "Should return inner tx data");
3229 }
3230
3231 #[test]
3233 fn proptest_key_auth_gas_monotonic_limits(
3234 num_limits1 in 0usize..10,
3235 num_limits2 in 0usize..10,
3236 ) {
3237 use tempo_primitives::transaction::{
3238 SignatureType, SignedKeyAuthorization,
3239 key_authorization::KeyAuthorization,
3240 TokenLimit as PrimTokenLimit,
3241 };
3242
3243 let make_key_auth = |num_limits: usize| -> SignedKeyAuthorization {
3244 let limits = if num_limits == 0 {
3245 None
3246 } else {
3247 Some((0..num_limits).map(|i| PrimTokenLimit {
3248 token: Address::with_last_byte(i as u8),
3249 limit: U256::from(1000),
3250 }).collect())
3251 };
3252
3253 SignedKeyAuthorization {
3254 authorization: KeyAuthorization {
3255 chain_id: 1,
3256 key_type: SignatureType::Secp256k1,
3257 key_id: Address::ZERO,
3258 expiry: None,
3259 limits,
3260 },
3261 signature: PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
3262 }
3263 };
3264
3265 for (gas_params, spec) in [
3267 (GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default()),
3268 (crate::gas_params::tempo_gas_params(TempoHardfork::T1B), TempoHardfork::T1B),
3269 ] {
3270 let gas1 = calculate_key_authorization_gas(&make_key_auth(num_limits1), &gas_params, spec);
3271 let gas2 = calculate_key_authorization_gas(&make_key_auth(num_limits2), &gas_params, spec);
3272
3273 if num_limits1 <= num_limits2 {
3274 prop_assert!(gas1 <= gas2,
3275 "{spec:?}: More limits should mean more gas: limits1={}, gas1={}, limits2={}, gas2={}",
3276 num_limits1, gas1, num_limits2, gas2);
3277 } else {
3278 prop_assert!(gas1 >= gas2,
3279 "{spec:?}: Fewer limits should mean less gas: limits1={}, gas1={}, limits2={}, gas2={}",
3280 num_limits1, gas1, num_limits2, gas2);
3281 }
3282 }
3283 }
3284
3285 #[test]
3287 fn proptest_key_auth_gas_minimum(
3288 sig_type in 0u8..3,
3289 num_limits in 0usize..5,
3290 ) {
3291 use tempo_primitives::transaction::{
3292 SignatureType, SignedKeyAuthorization,
3293 key_authorization::KeyAuthorization,
3294 TokenLimit as PrimTokenLimit,
3295 };
3296
3297 let signature = match sig_type {
3298 0 => PrimitiveSignature::Secp256k1(alloy_primitives::Signature::test_signature()),
3299 1 => PrimitiveSignature::P256(P256SignatureWithPreHash {
3300 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO, pre_hash: false,
3301 }),
3302 _ => PrimitiveSignature::WebAuthn(WebAuthnSignature {
3303 r: B256::ZERO, s: B256::ZERO, pub_key_x: B256::ZERO, pub_key_y: B256::ZERO,
3304 webauthn_data: Bytes::new(),
3305 }),
3306 };
3307
3308 let key_auth = SignedKeyAuthorization {
3309 authorization: KeyAuthorization {
3310 chain_id: 1,
3311 key_type: SignatureType::Secp256k1,
3312 key_id: Address::ZERO,
3313 expiry: None,
3314 limits: if num_limits == 0 { None } else {
3315 Some((0..num_limits).map(|i| PrimTokenLimit {
3316 token: Address::with_last_byte(i as u8),
3317 limit: U256::from(1000),
3318 }).collect())
3319 },
3320 },
3321 signature,
3322 };
3323
3324 let gas = calculate_key_authorization_gas(&key_auth, &GasParams::default(), tempo_chainspec::hardfork::TempoHardfork::default());
3326 let min_gas = KEY_AUTH_BASE_GAS + ECRECOVER_GAS;
3327 prop_assert!(gas >= min_gas,
3328 "Pre-T1B: Key auth gas should be at least {min_gas}, got {gas}");
3329
3330 let t1b_params = crate::gas_params::tempo_gas_params(TempoHardfork::T1B);
3332 let gas_t1b = calculate_key_authorization_gas(&key_auth, &t1b_params, TempoHardfork::T1B);
3333 let sstore = t1b_params.get(revm::context_interface::cfg::GasId::sstore_set_without_load_cost());
3334 let sload = t1b_params.warm_storage_read_cost() + t1b_params.cold_storage_additional_cost();
3335 let min_t1b = ECRECOVER_GAS + sload + sstore;
3336 prop_assert!(gas_t1b >= min_t1b,
3337 "T1B: Key auth gas should be at least {min_t1b}, got {gas_t1b}");
3338 }
3339 }
3340
3341 #[test]
3351 fn test_t1_2d_nonce_key_charges_250k_gas() {
3352 use crate::gas_params::tempo_gas_params;
3353 use revm::{context_interface::cfg::GasId, handler::Handler};
3354
3355 const TEST_TARGET: Address = Address::new([0xAA; 20]);
3357 const TEST_NONCE_KEY: U256 = U256::from_limbs([42, 0, 0, 0]);
3358 const SPEC: TempoHardfork = TempoHardfork::T1;
3359 const NEW_NONCE_KEY_GAS: u64 = SPEC.gas_new_nonce_key();
3360 const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
3361
3362 let mut cfg = CfgEnv::<TempoHardfork>::default();
3364 cfg.spec = SPEC;
3365 cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
3366
3367 let new_account_cost = cfg.gas_params.get(GasId::new_account_cost());
3369 assert_eq!(
3370 new_account_cost, 250_000,
3371 "T1 gas params should have 250k new_account_cost"
3372 );
3373
3374 let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
3376 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3377 let ctx = Context::mainnet()
3378 .with_db(CacheDB::new(EmptyDB::default()))
3379 .with_block(TempoBlockEnv::default())
3380 .with_cfg(cfg)
3381 .with_tx(TempoTxEnv {
3382 inner: revm::context::TxEnv {
3383 gas_limit: 1_000_000,
3384 nonce,
3385 ..Default::default()
3386 },
3387 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3388 aa_calls: vec![Call {
3389 to: TxKind::Call(TEST_TARGET),
3390 value: U256::ZERO,
3391 input: Bytes::new(),
3392 }],
3393 nonce_key,
3394 ..Default::default()
3395 })),
3396 ..Default::default()
3397 })
3398 .with_new_journal(journal);
3399 TempoEvm::<_, ()>::new(ctx, ())
3400 };
3401
3402 let mut evm_nonce_zero = make_evm(cfg.clone(), 0, TEST_NONCE_KEY);
3404 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3405 let gas_nonce_zero = handler
3406 .validate_initial_tx_gas(&mut evm_nonce_zero)
3407 .unwrap();
3408
3409 let mut evm_nonce_five = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
3412 let gas_nonce_five = handler
3413 .validate_initial_tx_gas(&mut evm_nonce_five)
3414 .unwrap();
3415
3416 let gas_delta = gas_nonce_zero.initial_gas - gas_nonce_five.initial_gas;
3419 let expected_delta = new_account_cost - EXISTING_NONCE_KEY_GAS;
3420 assert_eq!(
3421 gas_delta, expected_delta,
3422 "T1 gas difference between nonce=0 and nonce>0 should be {expected_delta} (new_account_cost - EXISTING_NONCE_KEY_GAS), got {gas_delta}"
3423 );
3424
3425 assert_ne!(
3427 gas_delta, NEW_NONCE_KEY_GAS,
3428 "T1 should NOT use pre-T1 NEW_NONCE_KEY_GAS ({NEW_NONCE_KEY_GAS}) for nonce=0 transactions"
3429 );
3430
3431 let mut evm_regular_nonce = make_evm(cfg, 0, U256::ZERO);
3433 let gas_regular = handler
3434 .validate_initial_tx_gas(&mut evm_regular_nonce)
3435 .unwrap();
3436
3437 assert_eq!(
3438 gas_nonce_zero.initial_gas, gas_regular.initial_gas,
3439 "nonce=0 should charge the same regardless of nonce_key (2D vs regular)"
3440 );
3441 }
3442
3443 #[test]
3454 fn test_t1_existing_2d_nonce_key_charges_5k_gas() {
3455 use crate::gas_params::tempo_gas_params;
3456 use revm::handler::Handler;
3457
3458 const BASE_INTRINSIC_GAS: u64 = 21_000;
3459 const TEST_TARGET: Address = Address::new([0xBB; 20]);
3460 const TEST_NONCE_KEY: U256 = U256::from_limbs([99, 0, 0, 0]);
3461 const SPEC: TempoHardfork = TempoHardfork::T1;
3462 const EXISTING_NONCE_KEY_GAS: u64 = SPEC.gas_existing_nonce_key();
3463
3464 let mut cfg = CfgEnv::<TempoHardfork>::default();
3465 cfg.spec = SPEC;
3466 cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
3467
3468 let make_evm = |cfg: CfgEnv<TempoHardfork>, nonce: u64, nonce_key: U256| {
3469 let journal = Journal::new(CacheDB::new(EmptyDB::default()));
3470 let ctx = Context::mainnet()
3471 .with_db(CacheDB::new(EmptyDB::default()))
3472 .with_block(TempoBlockEnv::default())
3473 .with_cfg(cfg)
3474 .with_tx(TempoTxEnv {
3475 inner: revm::context::TxEnv {
3476 gas_limit: 1_000_000,
3477 nonce,
3478 ..Default::default()
3479 },
3480 tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
3481 aa_calls: vec![Call {
3482 to: TxKind::Call(TEST_TARGET),
3483 value: U256::ZERO,
3484 input: Bytes::new(),
3485 }],
3486 nonce_key,
3487 ..Default::default()
3488 })),
3489 ..Default::default()
3490 })
3491 .with_new_journal(journal);
3492 TempoEvm::<_, ()>::new(ctx, ())
3493 };
3494
3495 let handler: TempoEvmHandler<CacheDB<EmptyDB>, ()> = TempoEvmHandler::new();
3496
3497 let mut evm_existing_key = make_evm(cfg.clone(), 5, TEST_NONCE_KEY);
3499 let gas_existing = handler
3500 .validate_initial_tx_gas(&mut evm_existing_key)
3501 .unwrap();
3502
3503 assert_eq!(
3504 gas_existing.initial_gas,
3505 BASE_INTRINSIC_GAS + EXISTING_NONCE_KEY_GAS,
3506 "T1 existing 2D nonce key (nonce>0) should charge BASE + EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
3507 );
3508
3509 let mut evm_regular = make_evm(cfg, 5, U256::ZERO);
3511 let gas_regular = handler.validate_initial_tx_gas(&mut evm_regular).unwrap();
3512
3513 assert_eq!(
3514 gas_regular.initial_gas, BASE_INTRINSIC_GAS,
3515 "T1 regular nonce (nonce_key=0, nonce>0) should only charge BASE intrinsic gas"
3516 );
3517
3518 let gas_delta = gas_existing.initial_gas - gas_regular.initial_gas;
3520 assert_eq!(
3521 gas_delta, EXISTING_NONCE_KEY_GAS,
3522 "Difference between existing 2D nonce and regular nonce should be EXISTING_NONCE_KEY_GAS ({EXISTING_NONCE_KEY_GAS})"
3523 );
3524 }
3525}