1use std::{cmp::Ordering, fmt::Debug};
4
5use alloy_primitives::{Address, B256, U256, b256};
6use reth_evm::EvmError;
7use revm::{
8 Database,
9 context::{
10 Block, Cfg, ContextTr, JournalTr, LocalContextTr, Transaction,
11 result::{EVMError, ExecutionResult, InvalidTransaction},
12 transaction::{AccessListItem, AccessListItemTr},
13 },
14 handler::{
15 EvmTr, FrameResult, FrameTr, Handler, MainnetHandler,
16 pre_execution::{self, calculate_caller_fee},
17 validation,
18 },
19 inspector::{Inspector, InspectorHandler},
20 interpreter::{
21 Gas, InitialAndFloorGas,
22 gas::{
23 ACCESS_LIST_ADDRESS, ACCESS_LIST_STORAGE_KEY, CALLVALUE, COLD_ACCOUNT_ACCESS_COST,
24 CREATE, STANDARD_TOKEN_COST, calc_tx_floor_cost, get_tokens_in_calldata, initcode_cost,
25 },
26 interpreter::EthInterpreter,
27 },
28 primitives::eip7702,
29 state::Bytecode,
30};
31use tempo_contracts::{
32 DEFAULT_7702_DELEGATE_ADDRESS,
33 precompiles::{IAccountKeychain::SignatureType as PrecompileSignatureType, TIPFeeAMMError},
34};
35use tempo_precompiles::{
36 account_keychain::{AccountKeychain, TokenLimit, authorizeKeyCall},
37 error::TempoPrecompileError,
38 nonce::{INonce::getNonceCall, NonceManager},
39 storage::StorageCtx,
40 tip_fee_manager::TipFeeManager,
41 tip20::{self, ITIP20::InsufficientBalance, TIP20Error, TIP20Token},
42};
43use tempo_primitives::transaction::{
44 PrimitiveSignature, RecoveredTempoAuthorization, SignatureType, TempoSignature,
45 calc_gas_balance_spending,
46};
47
48use crate::{
49 TempoEvm, TempoInvalidTransaction,
50 common::TempoStateAccess,
51 error::{FeePaymentError, TempoHaltReason},
52 evm::TempoContext,
53};
54
55const P256_VERIFY_GAS: u64 = 5_000;
58
59const DEFAULT_7702_DELEGATE_CODE_HASH: B256 =
61 b256!("e7b3e4597bdbdd0cc4eb42f9b799b580f23068f54e472bb802cb71efb1570482");
62
63#[inline]
70fn primitive_signature_verification_gas(signature: &PrimitiveSignature) -> u64 {
71 match signature {
72 PrimitiveSignature::Secp256k1(_) => 0,
73 PrimitiveSignature::P256(_) => P256_VERIFY_GAS,
74 PrimitiveSignature::WebAuthn(webauthn_sig) => {
75 let tokens = get_tokens_in_calldata(&webauthn_sig.webauthn_data, true);
76 P256_VERIFY_GAS + tokens * STANDARD_TOKEN_COST
77 }
78 }
79}
80
81#[inline]
86fn tempo_signature_verification_gas(signature: &TempoSignature) -> u64 {
87 match signature {
88 TempoSignature::Primitive(prim_sig) => primitive_signature_verification_gas(prim_sig),
89 TempoSignature::Keychain(keychain_sig) => {
90 primitive_signature_verification_gas(&keychain_sig.signature)
92 }
93 }
94}
95
96#[derive(Debug)]
100pub struct TempoEvmHandler<DB, I> {
101 fee_token: Address,
103 fee_payer: Address,
105 _phantom: core::marker::PhantomData<(DB, I)>,
107}
108
109impl<DB, I> TempoEvmHandler<DB, I> {
110 pub fn new() -> Self {
112 Self {
113 fee_token: Address::default(),
114 fee_payer: Address::default(),
115 _phantom: core::marker::PhantomData,
116 }
117 }
118}
119
120impl<DB: alloy_evm::Database, I> TempoEvmHandler<DB, I> {
121 fn load_fee_fields(
122 &mut self,
123 evm: &mut TempoEvm<DB, I>,
124 ) -> Result<(), EVMError<DB::Error, TempoInvalidTransaction>> {
125 let ctx = evm.ctx_mut();
126
127 self.fee_payer = ctx.tx.fee_payer()?;
128 self.fee_token = ctx.journaled_state.get_fee_token(
129 &ctx.tx,
130 ctx.block.beneficiary,
131 self.fee_payer,
132 ctx.cfg.spec,
133 )?;
134
135 if (!ctx.tx.max_balance_spending()?.is_zero() || ctx.tx.is_subblock_transaction())
137 && !ctx
138 .journaled_state
139 .is_valid_fee_token(self.fee_token, ctx.cfg.spec)?
140 {
141 return Err(TempoInvalidTransaction::InvalidFeeToken(self.fee_token).into());
142 }
143
144 Ok(())
145 }
146}
147
148impl<DB, I> TempoEvmHandler<DB, I>
149where
150 DB: alloy_evm::Database,
151{
152 fn execute_single_call_with<F>(
157 &mut self,
158 evm: &mut TempoEvm<DB, I>,
159 init_and_floor_gas: &InitialAndFloorGas,
160 mut run_loop: F,
161 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
162 where
163 F: FnMut(
164 &mut Self,
165 &mut TempoEvm<DB, I>,
166 <<TempoEvm<DB, I> as EvmTr>::Frame as FrameTr>::FrameInit,
167 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
168 {
169 let gas_limit = evm.ctx().tx().gas_limit() - init_and_floor_gas.initial_gas;
170
171 let first_frame_input = self.first_frame_input(evm, gas_limit)?;
173
174 let mut frame_result = run_loop(self, evm, first_frame_input)?;
176
177 self.last_frame_result(evm, &mut frame_result)?;
179
180 Ok(frame_result)
181 }
182
183 fn execute_single_call(
187 &mut self,
188 evm: &mut TempoEvm<DB, I>,
189 init_and_floor_gas: &InitialAndFloorGas,
190 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
191 self.execute_single_call_with(evm, init_and_floor_gas, Self::run_exec_loop)
192 }
193
194 fn execute_multi_call_with<F>(
210 &mut self,
211 evm: &mut TempoEvm<DB, I>,
212 init_and_floor_gas: &InitialAndFloorGas,
213 calls: Vec<tempo_primitives::transaction::Call>,
214 mut execute_single: F,
215 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
216 where
217 F: FnMut(
218 &mut Self,
219 &mut TempoEvm<DB, I>,
220 &InitialAndFloorGas,
221 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>,
222 {
223 let checkpoint = evm.ctx().journal_mut().checkpoint();
225
226 let gas_limit = evm.ctx().tx().gas_limit();
227 let mut remaining_gas = gas_limit - init_and_floor_gas.initial_gas;
228 let mut accumulated_gas_refund = 0i64;
229
230 let original_kind = evm.ctx().tx().kind();
232 let original_value = evm.ctx().tx().value();
233 let original_data = evm.ctx().tx().input().clone();
234
235 let mut final_result = None;
236
237 for call in calls.iter() {
238 {
240 let tx = &mut evm.ctx().tx;
241 tx.inner.kind = call.to;
242 tx.inner.value = call.value;
243 tx.inner.data = call.input.clone();
244 tx.inner.gas_limit = remaining_gas;
245 }
246
247 let zero_init_gas = InitialAndFloorGas::new(0, 0);
249 let frame_result = execute_single(self, evm, &zero_init_gas);
250
251 {
253 let tx = &mut evm.ctx().tx;
254 tx.inner.kind = original_kind;
255 tx.inner.value = original_value;
256 tx.inner.data = original_data.clone();
257 tx.inner.gas_limit = gas_limit;
258 }
259
260 let mut frame_result = frame_result?;
261
262 let instruction_result = frame_result.instruction_result();
264 if !instruction_result.is_ok() {
265 evm.ctx().journal_mut().checkpoint_revert(checkpoint);
267
268 let gas_used_by_failed_call = frame_result.gas().used();
270 let total_gas_used = (gas_limit - remaining_gas) + gas_used_by_failed_call;
271
272 let mut corrected_gas = Gas::new(gas_limit);
275 if instruction_result.is_revert() {
276 corrected_gas.set_spent(total_gas_used);
277 } else {
278 corrected_gas.spend_all();
279 }
280 corrected_gas.set_refund(0); *frame_result.gas_mut() = corrected_gas;
282
283 return Ok(frame_result);
284 }
285
286 let gas_used = frame_result.gas().used();
288 let gas_refunded = frame_result.gas().refunded();
289
290 accumulated_gas_refund = accumulated_gas_refund.saturating_add(gas_refunded);
291 remaining_gas = remaining_gas.saturating_sub(gas_used);
293
294 final_result = Some(frame_result);
295 }
296
297 evm.ctx().journal_mut().checkpoint_commit();
299
300 let mut result =
302 final_result.ok_or_else(|| EVMError::Custom("No calls executed".into()))?;
303
304 let total_gas_used = gas_limit - remaining_gas;
305
306 let mut corrected_gas = Gas::new(gas_limit);
309 corrected_gas.set_spent(total_gas_used);
310 corrected_gas.set_refund(accumulated_gas_refund);
311 *result.gas_mut() = corrected_gas;
312
313 Ok(result)
314 }
315
316 fn execute_multi_call(
318 &mut self,
319 evm: &mut TempoEvm<DB, I>,
320 init_and_floor_gas: &InitialAndFloorGas,
321 calls: Vec<tempo_primitives::transaction::Call>,
322 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>> {
323 self.execute_multi_call_with(evm, init_and_floor_gas, calls, Self::execute_single_call)
324 }
325
326 fn inspect_execute_single_call(
331 &mut self,
332 evm: &mut TempoEvm<DB, I>,
333 init_and_floor_gas: &InitialAndFloorGas,
334 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
335 where
336 I: Inspector<TempoContext<DB>, EthInterpreter>,
337 {
338 self.execute_single_call_with(evm, init_and_floor_gas, Self::inspect_run_exec_loop)
339 }
340
341 fn inspect_execute_multi_call(
346 &mut self,
347 evm: &mut TempoEvm<DB, I>,
348 init_and_floor_gas: &InitialAndFloorGas,
349 calls: Vec<tempo_primitives::transaction::Call>,
350 ) -> Result<FrameResult, EVMError<DB::Error, TempoInvalidTransaction>>
351 where
352 I: Inspector<TempoContext<DB>, EthInterpreter>,
353 {
354 self.execute_multi_call_with(
355 evm,
356 init_and_floor_gas,
357 calls,
358 Self::inspect_execute_single_call,
359 )
360 }
361}
362
363impl<DB, I> Default for TempoEvmHandler<DB, I> {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369impl<DB, I> Handler for TempoEvmHandler<DB, I>
370where
371 DB: alloy_evm::Database,
372{
373 type Evm = TempoEvm<DB, I>;
374 type Error = EVMError<DB::Error, TempoInvalidTransaction>;
375 type HaltReason = TempoHaltReason;
376
377 #[inline]
378 fn run(
379 &mut self,
380 evm: &mut Self::Evm,
381 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
382 self.load_fee_fields(evm)?;
383
384 match self.run_without_catch_error(evm) {
386 Ok(output) => Ok(output),
387 Err(err) => self.catch_error(evm, err),
388 }
389 }
390
391 #[inline]
397 fn execution(
398 &mut self,
399 evm: &mut Self::Evm,
400 init_and_floor_gas: &InitialAndFloorGas,
401 ) -> Result<FrameResult, Self::Error> {
402 if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
404 let calls = tempo_tx_env.aa_calls.clone();
406 self.execute_multi_call(evm, init_and_floor_gas, calls)
407 } else {
408 self.execute_single_call(evm, init_and_floor_gas)
410 }
411 }
412
413 #[inline]
415 fn execution_result(
416 &mut self,
417 evm: &mut Self::Evm,
418 result: <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
419 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
420 evm.logs.clear();
421 if !result.instruction_result().is_ok() {
422 evm.logs = evm.journal_mut().take_logs();
423 }
424
425 MainnetHandler::default()
426 .execution_result(evm, result)
427 .map(|result| result.map_haltreason(Into::into))
428 }
429
430 #[inline]
436 fn apply_eip7702_auth_list(&self, evm: &mut Self::Evm) -> Result<u64, Self::Error> {
437 let ctx = evm.ctx();
438
439 let has_aa_auth_list = ctx
441 .tx()
442 .tempo_tx_env
443 .as_ref()
444 .map(|aa_env| !aa_env.tempo_authorization_list.is_empty())
445 .unwrap_or(false);
446
447 if has_aa_auth_list {
450 let chain_id = ctx.cfg().chain_id();
455 let (tx, journal) = evm.ctx().tx_journal_mut();
456
457 let tempo_tx_env = tx.tempo_tx_env.as_ref().unwrap();
458 let mut refunded_accounts = 0;
459
460 for authorization in &tempo_tx_env.tempo_authorization_list {
461 let Some(authority) = authorization.authority() else {
462 continue;
464 };
465
466 let auth_chain_id = authorization.chain_id;
468 if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) {
469 continue;
470 }
471
472 if authorization.nonce == u64::MAX {
474 continue;
475 }
476
477 let mut authority_acc = journal.load_account_with_code_mut(authority)?;
479
480 if let Some(bytecode) = &authority_acc.info.code {
482 if !bytecode.is_empty() && !bytecode.is_eip7702() {
484 continue;
485 }
486 }
487
488 if authorization.nonce != authority_acc.info.nonce {
490 continue;
491 }
492
493 if !(authority_acc.is_empty()
495 && authority_acc.is_loaded_as_not_existing_not_touched())
496 {
497 refunded_accounts += 1;
498 }
499
500 authority_acc.delegate(*authorization.address());
505 }
506
507 let refunded_gas =
508 refunded_accounts * (eip7702::PER_EMPTY_ACCOUNT_COST - eip7702::PER_AUTH_BASE_COST);
509 return Ok(refunded_gas);
510 }
511
512 pre_execution::apply_eip7702_auth_list(evm.ctx())
514 }
515
516 #[inline]
517 fn validate_against_state_and_deduct_caller(
518 &self,
519 evm: &mut Self::Evm,
520 ) -> Result<(), Self::Error> {
521 let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut();
522
523 let account_balance = get_token_balance(journal, self.fee_token, self.fee_payer)?;
525
526 let mut caller_account = journal.load_account_with_code_mut(tx.caller())?.data;
528
529 if caller_account.info.has_no_code_and_nonce() {
530 caller_account.set_code(
531 DEFAULT_7702_DELEGATE_CODE_HASH,
532 Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS),
533 );
534 }
535
536 let nonce_key = tx
537 .tempo_tx_env
538 .as_ref()
539 .map(|aa| aa.nonce_key)
540 .unwrap_or_default();
541
542 pre_execution::validate_account_nonce_and_code(
544 &caller_account.info,
545 tx.nonce(),
546 cfg.is_eip3607_disabled(),
547 cfg.is_nonce_check_disabled() || !nonce_key.is_zero(),
549 )?;
550
551 caller_account.touch();
553
554 if !nonce_key.is_zero() {
555 StorageCtx::enter_evm(journal, block, cfg, || {
556 let mut nonce_manager = NonceManager::new();
557
558 if !cfg.is_nonce_check_disabled() {
559 let tx_nonce = tx.nonce();
560 let state = nonce_manager
561 .get_nonce(getNonceCall {
562 account: tx.caller(),
563 nonceKey: nonce_key,
564 })
565 .map_err(|err| match err {
566 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
567 err => {
568 TempoInvalidTransaction::NonceManagerError(err.to_string()).into()
569 }
570 })?;
571
572 match tx_nonce.cmp(&state) {
573 Ordering::Greater => {
574 return Err(TempoInvalidTransaction::EthInvalidTransaction(
575 InvalidTransaction::NonceTooHigh {
576 tx: tx_nonce,
577 state,
578 },
579 )
580 .into());
581 }
582 Ordering::Less => {
583 return Err(TempoInvalidTransaction::EthInvalidTransaction(
584 InvalidTransaction::NonceTooLow {
585 tx: tx_nonce,
586 state,
587 },
588 )
589 .into());
590 }
591 _ => {}
592 }
593 }
594
595 nonce_manager
597 .increment_nonce(tx.caller(), nonce_key)
598 .map_err(|err| match err {
599 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
600 err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
601 })?;
602
603 Result::<(), EVMError<DB::Error, TempoInvalidTransaction>>::Ok(())
604 })?;
605 } else {
606 if tx.tempo_tx_env.is_some() || tx.kind().is_call() {
610 caller_account.bump_nonce();
611 }
612 }
613
614 let new_balance = calculate_caller_fee(account_balance, tx, block, cfg)?;
616 let gas_balance_spending = core::cmp::max(account_balance, new_balance) - new_balance;
619
620 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
625 && let Some(key_auth) = &tempo_tx_env.key_authorization
626 {
627 if let Some(keychain_sig) = tempo_tx_env.signature.as_keychain() {
630 let access_key_addr =
632 keychain_sig
633 .key_id(&tempo_tx_env.signature_hash)
634 .map_err(|_| {
635 EVMError::Transaction(
636 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
637 reason:
638 "Failed to recover access key address from Keychain signature"
639 .to_string(),
640 },
641 )
642 })?;
643
644 if access_key_addr != key_auth.key_id {
646 return Err(EVMError::Transaction(
647 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
648 reason: "Access keys cannot authorize other keys. Only the root key can authorize new keys.".to_string(),
649 },
650 ));
651 }
652 }
653
654 let root_account = &tx.caller;
656
657 let auth_signer = key_auth.recover_signer().map_err(|_| {
659 EVMError::Transaction(TempoInvalidTransaction::AccessKeyAuthorizationFailed {
660 reason: "Failed to recover signer from KeyAuthorization signature".to_string(),
661 })
662 })?;
663
664 if auth_signer != *root_account {
666 return Err(EVMError::Transaction(
667 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
668 reason: format!(
669 "KeyAuthorization must be signed by root account {root_account}, but was signed by {auth_signer}",
670 ),
671 },
672 ));
673 }
674
675 let expected_chain_id = cfg.chain_id();
678 if key_auth.chain_id != 0 && key_auth.chain_id != expected_chain_id {
679 return Err(EVMError::Transaction(
680 TempoInvalidTransaction::KeyAuthorizationChainIdMismatch {
681 expected: expected_chain_id,
682 got: key_auth.chain_id,
683 },
684 ));
685 }
686
687 StorageCtx::enter_precompile(journal, block, cfg, |mut keychain: AccountKeychain| {
689 let access_key_addr = key_auth.key_id;
690
691 let signature_type = match key_auth.key_type {
694 SignatureType::Secp256k1 => PrecompileSignatureType::Secp256k1,
695 SignatureType::P256 => PrecompileSignatureType::P256,
696 SignatureType::WebAuthn => PrecompileSignatureType::WebAuthn,
697 };
698
699 let expiry = key_auth.expiry.unwrap_or(u64::MAX);
701
702 let current_timestamp = block.timestamp().saturating_to::<u64>();
704 if expiry <= current_timestamp {
705 return Err(EVMError::Transaction(
706 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
707 reason: format!(
708 "Key expiry {expiry} is in the past (current timestamp: {current_timestamp})"
709 ),
710 },
711 ));
712 }
713
714 let enforce_limits = key_auth.limits.is_some();
718 let precompile_limits: Vec<TokenLimit> = key_auth
719 .limits
720 .as_ref()
721 .map(|limits| {
722 limits
723 .iter()
724 .map(|limit| TokenLimit {
725 token: limit.token,
726 amount: limit.limit,
727 })
728 .collect()
729 })
730 .unwrap_or_default();
731
732 let authorize_call = authorizeKeyCall {
734 keyId: access_key_addr,
735 signatureType: signature_type,
736 expiry,
737 enforceLimits: enforce_limits,
738 limits: precompile_limits,
739 };
740
741 keychain
743 .authorize_key(*root_account, authorize_call)
744 .map_err(|err| match err {
745 TempoPrecompileError::Fatal(err) => EVMError::Custom(err),
746 err => TempoInvalidTransaction::AccessKeyAuthorizationFailed {
747 reason: err.to_string(),
748 }
749 .into(),
750 })
751 })?;
752 }
753
754 if let Some(tempo_tx_env) = tx.tempo_tx_env.as_ref()
757 && let Some(keychain_sig) = tempo_tx_env.signature.as_keychain()
758 {
759 let user_address = &keychain_sig.user_address;
762
763 if *user_address != tx.caller {
765 return Err(EVMError::Transaction(
766 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
767 reason: format!(
768 "Keychain user_address {} does not match transaction caller {}",
769 user_address, tx.caller
770 ),
771 },
772 ));
773 }
774
775 let access_key_addr =
777 keychain_sig
778 .key_id(&tempo_tx_env.signature_hash)
779 .map_err(|_| {
780 EVMError::Transaction(
781 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
782 reason: "Failed to recover access key address from inner signature"
783 .to_string(),
784 },
785 )
786 })?;
787
788 let is_authorizing_this_key = tempo_tx_env
791 .key_authorization
792 .as_ref()
793 .map(|key_auth| key_auth.key_id == access_key_addr)
794 .unwrap_or(false);
795
796 StorageCtx::enter_precompile(journal, block, cfg, |mut keychain: AccountKeychain| {
798 if !is_authorizing_this_key {
799 keychain
802 .validate_keychain_authorization(
803 *user_address,
804 access_key_addr,
805 block.timestamp().to::<u64>(),
806 )
807 .map_err(|e| {
808 EVMError::Transaction(
809 TempoInvalidTransaction::AccessKeyAuthorizationFailed {
810 reason: format!("Keychain validation failed: {e:?}"),
811 },
812 )
813 })?;
814 }
815
816 keychain
820 .set_transaction_key(access_key_addr)
821 .map_err(|e| EVMError::Custom(e.to_string()))
822 })?;
823 }
824
825 if gas_balance_spending.is_zero() {
826 return Ok(());
827 }
828
829 let checkpoint = journal.checkpoint();
830
831 let result = StorageCtx::enter_evm(journal, &block, cfg, || {
832 TipFeeManager::new().collect_fee_pre_tx(
833 self.fee_payer,
834 self.fee_token,
835 gas_balance_spending,
836 block.beneficiary(),
837 )
838 });
839
840 if let Err(err) = result {
841 journal.checkpoint_revert(checkpoint);
843
844 Err(match err {
848 TempoPrecompileError::TIPFeeAMMError(TIPFeeAMMError::InsufficientLiquidity(_)) => {
849 FeePaymentError::InsufficientAmmLiquidity {
850 fee: gas_balance_spending,
851 }
852 .into()
853 }
854
855 TempoPrecompileError::TIP20(TIP20Error::InsufficientBalance(
856 InsufficientBalance { available, .. },
857 )) => EVMError::Transaction(
858 FeePaymentError::InsufficientFeeTokenBalance {
859 fee: gas_balance_spending,
860 balance: available,
861 }
862 .into(),
863 ),
864
865 TempoPrecompileError::Fatal(e) => EVMError::Custom(e),
866
867 _ => EVMError::Transaction(FeePaymentError::Other(err.to_string()).into()),
868 })
869 } else {
870 journal.checkpoint_commit();
871 evm.collected_fee = gas_balance_spending;
872
873 Ok(())
874 }
875 }
876
877 fn reimburse_caller(
878 &self,
879 evm: &mut Self::Evm,
880 exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
881 ) -> Result<(), Self::Error> {
882 let context = &mut evm.inner.ctx;
884 let tx = context.tx();
885 let basefee = context.block().basefee() as u128;
886 let effective_gas_price = tx.effective_gas_price(basefee);
887 let gas = exec_result.gas();
888
889 let actual_spending = calc_gas_balance_spending(gas.used(), effective_gas_price);
891 let refund_amount = tx.effective_balance_spending(
892 context.block.basefee.into(),
893 context.block.blob_gasprice().unwrap_or_default(),
894 )? - tx.value
895 - actual_spending;
896
897 if context.cfg.disable_fee_charge
903 && evm.collected_fee.is_zero()
904 && !actual_spending.is_zero()
905 {
906 return Ok(());
907 }
908
909 let (journal, block) = (&mut context.journaled_state, &context.block);
911 let beneficiary = block.beneficiary();
912
913 StorageCtx::enter_evm(&mut *journal, block, &context.cfg, || {
914 let mut fee_manager = TipFeeManager::new();
915
916 if !actual_spending.is_zero() || !refund_amount.is_zero() {
917 fee_manager
919 .collect_fee_post_tx(
920 self.fee_payer,
921 actual_spending,
922 refund_amount,
923 self.fee_token,
924 beneficiary,
925 )
926 .map_err(|e| EVMError::Custom(format!("{e:?}")))?;
927 }
928
929 Ok(())
930 })
931 }
932
933 #[inline]
934 fn reward_beneficiary(
935 &self,
936 _evm: &mut Self::Evm,
937 _exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
938 ) -> Result<(), Self::Error> {
939 Ok(())
942 }
943
944 #[inline]
950 fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
951 if !evm.ctx.tx.value().is_zero() {
954 return Err(TempoInvalidTransaction::ValueTransferNotAllowed.into());
955 }
956
957 validation::validate_env::<_, Self::Error>(evm.ctx())?;
960
961 let cfg = evm.ctx_ref().cfg();
963 let tx = evm.ctx_ref().tx();
964
965 if let Some(aa_env) = tx.tempo_tx_env.as_ref() {
966 let has_keychain_fields =
967 aa_env.key_authorization.is_some() || aa_env.signature.is_keychain();
968
969 if has_keychain_fields && !cfg.spec.is_allegretto() {
971 return Err(TempoInvalidTransaction::KeychainOpBeforeAllegretto.into());
972 }
973
974 if aa_env.subblock_transaction {
975 if !cfg.spec.is_allegretto() {
976 if tx.max_fee_per_gas() > 0 {
977 return Err(
978 TempoInvalidTransaction::SubblockTransactionMustHaveZeroFee.into()
979 );
980 }
981 } else if has_keychain_fields {
982 return Err(TempoInvalidTransaction::KeychainOpInSubblockTransaction.into());
983 }
984 }
985
986 let base_fee = if cfg.is_base_fee_check_disabled()
990 || (aa_env.subblock_transaction && !cfg.spec.is_allegretto())
991 {
992 None
993 } else {
994 Some(evm.ctx_ref().block().basefee() as u128)
995 };
996
997 validation::validate_priority_fee_tx(
998 tx.max_fee_per_gas(),
999 tx.max_priority_fee_per_gas().unwrap_or_default(),
1000 base_fee,
1001 cfg.is_priority_fee_check_disabled(),
1002 )
1003 .map_err(TempoInvalidTransaction::EthInvalidTransaction)?;
1004
1005 let block_timestamp = evm.ctx_ref().block().timestamp().saturating_to();
1007 validate_time_window(aa_env.valid_after, aa_env.valid_before, block_timestamp)?;
1008 }
1009
1010 Ok(())
1011 }
1012
1013 #[inline]
1020 fn validate_initial_tx_gas(&self, evm: &Self::Evm) -> Result<InitialAndFloorGas, Self::Error> {
1021 let tx = evm.ctx_ref().tx();
1022
1023 if tx.tempo_tx_env.is_some() {
1025 validate_aa_initial_tx_gas(evm)
1027 } else {
1028 let spec = evm.ctx_ref().cfg().spec().into();
1030 Ok(
1031 validation::validate_initial_tx_gas(tx, spec, evm.ctx.cfg.is_eip7623_disabled())
1032 .map_err(TempoInvalidTransaction::EthInvalidTransaction)?,
1033 )
1034 }
1035 }
1036
1037 fn catch_error(
1038 &self,
1039 evm: &mut Self::Evm,
1040 error: Self::Error,
1041 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1042 if evm.ctx.tx.is_subblock_transaction()
1044 && evm.cfg.spec.is_allegretto()
1045 && let Some(
1046 TempoInvalidTransaction::CollectFeePreTx(_)
1047 | TempoInvalidTransaction::EthInvalidTransaction(
1048 InvalidTransaction::LackOfFundForMaxFee { .. },
1049 ),
1050 ) = error.as_invalid_tx_err()
1051 {
1052 evm.ctx.journaled_state.commit_tx();
1056
1057 evm.ctx().local_mut().clear();
1058 evm.frame_stack().clear();
1059
1060 Ok(ExecutionResult::Halt {
1061 reason: TempoHaltReason::SubblockTxFeePayment,
1062 gas_used: 0,
1063 })
1064 } else {
1065 MainnetHandler::default()
1066 .catch_error(evm, error)
1067 .map(|result| result.map_haltreason(Into::into))
1068 }
1069 }
1070}
1071
1072fn calculate_aa_batch_intrinsic_gas<'a>(
1086 calls: &[tempo_primitives::transaction::Call],
1087 signature: &TempoSignature,
1088 access_list: Option<impl Iterator<Item = &'a AccessListItem>>,
1089 authorization_list: &[RecoveredTempoAuthorization],
1090) -> Result<InitialAndFloorGas, TempoInvalidTransaction> {
1091 let mut gas = InitialAndFloorGas::default();
1092
1093 gas.initial_gas += 21_000;
1095
1096 gas.initial_gas += tempo_signature_verification_gas(signature);
1098
1099 gas.initial_gas += COLD_ACCOUNT_ACCESS_COST * calls.len() as u64;
1102
1103 gas.initial_gas += authorization_list.len() as u64 * eip7702::PER_EMPTY_ACCOUNT_COST;
1105 for auth in authorization_list {
1107 gas.initial_gas += tempo_signature_verification_gas(auth.signature());
1108 }
1109
1110 let mut total_tokens = 0u64;
1112
1113 for call in calls {
1114 let tokens = get_tokens_in_calldata(&call.input, true);
1116 total_tokens += tokens;
1117
1118 if call.to.is_create() {
1120 gas.initial_gas += CREATE; gas.initial_gas += initcode_cost(call.input.len());
1125 }
1126
1127 if !call.value.is_zero() {
1130 return Err(TempoInvalidTransaction::ValueTransferNotAllowedInAATx);
1131 }
1132
1133 if !call.value.is_zero() && call.to.is_call() {
1136 gas.initial_gas += CALLVALUE; }
1138 }
1139
1140 gas.initial_gas += total_tokens * STANDARD_TOKEN_COST;
1141
1142 if let Some(access_list) = access_list {
1144 let (accounts, storages) =
1145 access_list.fold((0u64, 0u64), |(acc_count, storage_count), item| {
1146 (
1147 acc_count + 1,
1148 storage_count + item.storage_slots().count() as u64,
1149 )
1150 });
1151 gas.initial_gas += accounts * ACCESS_LIST_ADDRESS; gas.initial_gas += storages * ACCESS_LIST_STORAGE_KEY; }
1154
1155 gas.floor_gas = calc_tx_floor_cost(total_tokens); Ok(gas)
1159}
1160
1161fn validate_aa_initial_tx_gas<DB, I>(
1167 evm: &TempoEvm<DB, I>,
1168) -> Result<InitialAndFloorGas, EVMError<DB::Error, TempoInvalidTransaction>>
1169where
1170 DB: alloy_evm::Database,
1171{
1172 let tx = evm.ctx_ref().tx();
1173
1174 let aa_env = tx
1176 .tempo_tx_env
1177 .as_ref()
1178 .expect("validate_aa_initial_tx_gas called for non-AA transaction");
1179
1180 let calls = &aa_env.aa_calls;
1181 let gas_limit = tx.gas_limit();
1182
1183 let max_initcode_size = evm.ctx_ref().cfg().max_initcode_size();
1185 for call in calls {
1186 if call.to.is_create() && call.input.len() > max_initcode_size {
1187 return Err(EVMError::Transaction(
1188 TempoInvalidTransaction::EthInvalidTransaction(
1189 InvalidTransaction::CreateInitCodeSizeLimit,
1190 ),
1191 ));
1192 }
1193 }
1194
1195 let mut batch_gas = calculate_aa_batch_intrinsic_gas(
1197 calls,
1198 &aa_env.signature,
1199 tx.access_list(),
1200 &aa_env.tempo_authorization_list,
1201 )?;
1202
1203 if evm.ctx.cfg.is_eip7623_disabled() {
1204 batch_gas.floor_gas = 0u64;
1205 }
1206
1207 if gas_limit < batch_gas.initial_gas {
1209 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1210 gas_limit,
1211 intrinsic_gas: batch_gas.initial_gas,
1212 }
1213 .into());
1214 }
1215
1216 if !evm.ctx.cfg.is_eip7623_disabled() && gas_limit < batch_gas.floor_gas {
1218 return Err(TempoInvalidTransaction::InsufficientGasForIntrinsicCost {
1219 gas_limit,
1220 intrinsic_gas: batch_gas.floor_gas,
1221 }
1222 .into());
1223 }
1224
1225 Ok(batch_gas)
1226}
1227
1228pub fn get_token_balance<JOURNAL>(
1230 journal: &mut JOURNAL,
1231 token: Address,
1232 sender: Address,
1233) -> Result<U256, <JOURNAL::Database as Database>::Error>
1234where
1235 JOURNAL: JournalTr,
1236{
1237 let token_id = tip20::address_to_token_id_unchecked(token);
1239
1240 journal.load_account(token)?;
1241 let balance_slot = TIP20Token::new(token_id).balances.at(sender).slot();
1242 let balance = journal.sload(token, balance_slot)?.data;
1243
1244 Ok(balance)
1245}
1246
1247impl<DB, I> InspectorHandler for TempoEvmHandler<DB, I>
1248where
1249 DB: alloy_evm::Database,
1250 I: Inspector<TempoContext<DB>>,
1251{
1252 type IT = EthInterpreter;
1253
1254 fn inspect_run(
1255 &mut self,
1256 evm: &mut Self::Evm,
1257 ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
1258 self.load_fee_fields(evm)?;
1259
1260 match self.inspect_run_without_catch_error(evm) {
1261 Ok(output) => Ok(output),
1262 Err(e) => self.catch_error(evm, e),
1263 }
1264 }
1265
1266 #[inline]
1274 fn inspect_execution(
1275 &mut self,
1276 evm: &mut Self::Evm,
1277 init_and_floor_gas: &InitialAndFloorGas,
1278 ) -> Result<FrameResult, Self::Error> {
1279 if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() {
1281 let calls = tempo_tx_env.aa_calls.clone();
1283 self.inspect_execute_multi_call(evm, init_and_floor_gas, calls)
1284 } else {
1285 self.inspect_execute_single_call(evm, init_and_floor_gas)
1287 }
1288 }
1289}
1290
1291pub fn validate_time_window(
1299 valid_after: Option<u64>,
1300 valid_before: Option<u64>,
1301 block_timestamp: u64,
1302) -> Result<(), TempoInvalidTransaction> {
1303 if let Some(after) = valid_after
1305 && block_timestamp < after
1306 {
1307 return Err(TempoInvalidTransaction::ValidAfter {
1308 current: block_timestamp,
1309 valid_after: after,
1310 });
1311 }
1312
1313 if let Some(before) = valid_before
1315 && block_timestamp >= before
1316 {
1317 return Err(TempoInvalidTransaction::ValidBefore {
1318 current: block_timestamp,
1319 valid_before: before,
1320 });
1321 }
1322
1323 Ok(())
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use super::*;
1329 use crate::{TempoBlockEnv, TempoTxEnv};
1330 use alloy_primitives::{Address, U256};
1331 use revm::{
1332 Context, Journal, MainContext,
1333 database::{CacheDB, EmptyDB},
1334 interpreter::instructions::utility::IntoU256,
1335 primitives::hardfork::SpecId,
1336 state::Account,
1337 };
1338 use tempo_chainspec::hardfork::TempoHardfork;
1339 use tempo_precompiles::{DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, TIP_FEE_MANAGER_ADDRESS};
1340
1341 fn create_test_journal() -> Journal<CacheDB<EmptyDB>> {
1342 let db = CacheDB::new(EmptyDB::default());
1343 Journal::new(db)
1344 }
1345
1346 #[test]
1347 fn test_get_token_balance() -> eyre::Result<()> {
1348 let mut journal = create_test_journal();
1349 let token = Address::random();
1350 let account = Address::random();
1351 let expected_balance = U256::random();
1352
1353 let token_id = tip20::address_to_token_id_unchecked(token);
1355 let balance_slot = TIP20Token::new(token_id).balances.at(account).slot();
1356 journal.load_account(token)?;
1357 journal
1358 .sstore(token, balance_slot, expected_balance)
1359 .unwrap();
1360
1361 let balance = get_token_balance(&mut journal, token, account)?;
1362 assert_eq!(balance, expected_balance);
1363
1364 Ok(())
1365 }
1366
1367 #[test]
1368 fn test_get_fee_token() -> eyre::Result<()> {
1369 let journal = create_test_journal();
1370 let mut ctx: TempoContext<_> = Context::mainnet()
1371 .with_db(CacheDB::new(EmptyDB::default()))
1372 .with_block(TempoBlockEnv::default())
1373 .with_cfg(Default::default())
1374 .with_tx(TempoTxEnv::default())
1375 .with_new_journal(journal);
1376 let user = Address::random();
1377 ctx.tx.inner.caller = user;
1378 let validator = Address::random();
1379 ctx.block.beneficiary = validator;
1380 let user_fee_token = Address::random();
1381 let validator_fee_token = Address::random();
1382 let tx_fee_token = Address::random();
1383
1384 let validator_slot = TipFeeManager::new().validator_tokens.at(validator).slot();
1386 ctx.journaled_state.load_account(TIP_FEE_MANAGER_ADDRESS)?;
1387 ctx.journaled_state
1388 .sstore(
1389 TIP_FEE_MANAGER_ADDRESS,
1390 validator_slot,
1391 validator_fee_token.into_u256(),
1392 )
1393 .unwrap();
1394
1395 let fee_token = ctx.journaled_state.get_fee_token(
1396 &ctx.tx,
1397 validator,
1398 user,
1399 TempoHardfork::default(),
1400 )?;
1401 assert_eq!(DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, fee_token);
1402
1403 let user_slot = TipFeeManager::new().user_tokens.at(user).slot();
1405 ctx.journaled_state
1406 .sstore(
1407 TIP_FEE_MANAGER_ADDRESS,
1408 user_slot,
1409 user_fee_token.into_u256(),
1410 )
1411 .unwrap();
1412
1413 let fee_token = ctx.journaled_state.get_fee_token(
1414 &ctx.tx,
1415 validator,
1416 user,
1417 TempoHardfork::default(),
1418 )?;
1419 assert_eq!(user_fee_token, fee_token);
1420
1421 ctx.tx.fee_token = Some(tx_fee_token);
1423 let fee_token = ctx.journaled_state.get_fee_token(
1424 &ctx.tx,
1425 validator,
1426 user,
1427 TempoHardfork::default(),
1428 )?;
1429 assert_eq!(tx_fee_token, fee_token);
1430
1431 Ok(())
1432 }
1433
1434 #[test]
1435 fn test_delegate_code_hash() {
1436 let mut account = Account::default();
1437 account
1438 .info
1439 .set_code(Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS));
1440 assert_eq!(account.info.code_hash, DEFAULT_7702_DELEGATE_CODE_HASH);
1441 }
1442
1443 #[test]
1444 fn test_aa_gas_single_call_vs_normal_tx() {
1445 use crate::TempoBatchCallEnv;
1446 use alloy_primitives::{Bytes, TxKind};
1447 use revm::interpreter::gas::calculate_initial_tx_gas;
1448 use tempo_primitives::transaction::{Call, TempoSignature};
1449
1450 let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); let to = Address::random();
1453
1454 let call = Call {
1456 to: TxKind::Call(to),
1457 value: U256::ZERO,
1458 input: calldata.clone(),
1459 };
1460
1461 let aa_env = TempoBatchCallEnv {
1462 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1463 alloy_primitives::Signature::test_signature(),
1464 )), aa_calls: vec![call],
1466 key_authorization: None,
1467 signature_hash: B256::ZERO,
1468 ..Default::default()
1469 };
1470
1471 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
1473 let aa_gas = calculate_aa_batch_intrinsic_gas(
1474 &aa_env.aa_calls,
1475 &aa_env.signature,
1476 None::<std::iter::Empty<&AccessListItem>>, &aa_env.tempo_authorization_list,
1478 )
1479 .unwrap();
1480
1481 let normal_tx_gas = calculate_initial_tx_gas(
1483 spec.into(),
1484 &calldata,
1485 false, 0, 0, 0, );
1490
1491 let expected_initial = normal_tx_gas.initial_gas + COLD_ACCOUNT_ACCESS_COST;
1493 assert_eq!(
1494 aa_gas.initial_gas, expected_initial,
1495 "AA secp256k1 single call should match normal tx + per-call overhead"
1496 );
1497 }
1498
1499 #[test]
1500 fn test_aa_gas_multiple_calls_overhead() {
1501 use crate::TempoBatchCallEnv;
1502 use alloy_primitives::{Bytes, TxKind};
1503 use revm::interpreter::gas::calculate_initial_tx_gas;
1504 use tempo_primitives::transaction::{Call, TempoSignature};
1505
1506 let calldata = Bytes::from(vec![1, 2, 3]); let calls = vec![
1509 Call {
1510 to: TxKind::Call(Address::random()),
1511 value: U256::ZERO,
1512 input: calldata.clone(),
1513 },
1514 Call {
1515 to: TxKind::Call(Address::random()),
1516 value: U256::ZERO,
1517 input: calldata.clone(),
1518 },
1519 Call {
1520 to: TxKind::Call(Address::random()),
1521 value: U256::ZERO,
1522 input: calldata.clone(),
1523 },
1524 ];
1525
1526 let aa_env = TempoBatchCallEnv {
1527 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1528 alloy_primitives::Signature::test_signature(),
1529 )),
1530 aa_calls: calls.clone(),
1531 key_authorization: None,
1532 signature_hash: B256::ZERO,
1533 ..Default::default()
1534 };
1535
1536 let spec = tempo_chainspec::hardfork::TempoHardfork::default();
1537 let gas = calculate_aa_batch_intrinsic_gas(
1538 &calls,
1539 &aa_env.signature,
1540 None::<std::iter::Empty<&AccessListItem>>,
1541 &aa_env.tempo_authorization_list,
1542 )
1543 .unwrap();
1544
1545 let base_tx_gas = calculate_initial_tx_gas(spec.into(), &calldata, false, 0, 0, 0);
1547
1548 let expected = base_tx_gas.initial_gas
1551 + 2 * (calldata.len() as u64 * 16)
1552 + 3 * COLD_ACCOUNT_ACCESS_COST;
1553 assert_eq!(
1554 gas.initial_gas, expected,
1555 "Should charge per-call overhead for each call"
1556 );
1557 }
1558
1559 #[test]
1560 fn test_aa_gas_p256_signature() {
1561 use crate::TempoBatchCallEnv;
1562 use alloy_primitives::{B256, Bytes, TxKind};
1563 use revm::interpreter::gas::calculate_initial_tx_gas;
1564 use tempo_primitives::transaction::{
1565 Call, TempoSignature, tt_signature::P256SignatureWithPreHash,
1566 };
1567
1568 let spec = SpecId::CANCUN;
1569 let calldata = Bytes::from(vec![1, 2]);
1570
1571 let call = Call {
1572 to: TxKind::Call(Address::random()),
1573 value: U256::ZERO,
1574 input: calldata.clone(),
1575 };
1576
1577 let aa_env = TempoBatchCallEnv {
1578 signature: TempoSignature::Primitive(PrimitiveSignature::P256(
1579 P256SignatureWithPreHash {
1580 r: B256::ZERO,
1581 s: B256::ZERO,
1582 pub_key_x: B256::ZERO,
1583 pub_key_y: B256::ZERO,
1584 pre_hash: false,
1585 },
1586 )),
1587 aa_calls: vec![call],
1588 key_authorization: None,
1589 signature_hash: B256::ZERO,
1590 ..Default::default()
1591 };
1592
1593 let gas = calculate_aa_batch_intrinsic_gas(
1594 &aa_env.aa_calls,
1595 &aa_env.signature,
1596 None::<std::iter::Empty<&AccessListItem>>,
1597 &aa_env.tempo_authorization_list,
1598 )
1599 .unwrap();
1600
1601 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
1603
1604 let expected = base_gas.initial_gas + P256_VERIFY_GAS + COLD_ACCOUNT_ACCESS_COST;
1606 assert_eq!(
1607 gas.initial_gas, expected,
1608 "Should include P256 verification gas"
1609 );
1610 }
1611
1612 #[test]
1613 fn test_aa_gas_create_call() {
1614 use crate::TempoBatchCallEnv;
1615 use alloy_primitives::{Bytes, TxKind};
1616 use revm::interpreter::gas::calculate_initial_tx_gas;
1617 use tempo_primitives::transaction::{Call, TempoSignature};
1618
1619 let spec = SpecId::CANCUN; let initcode = Bytes::from(vec![0x60, 0x80]); let call = Call {
1623 to: TxKind::Create,
1624 value: U256::ZERO,
1625 input: initcode.clone(),
1626 };
1627
1628 let aa_env = TempoBatchCallEnv {
1629 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1630 alloy_primitives::Signature::test_signature(),
1631 )),
1632 aa_calls: vec![call],
1633 key_authorization: None,
1634 signature_hash: B256::ZERO,
1635 ..Default::default()
1636 };
1637
1638 let gas = calculate_aa_batch_intrinsic_gas(
1639 &aa_env.aa_calls,
1640 &aa_env.signature,
1641 None::<std::iter::Empty<&AccessListItem>>,
1642 &aa_env.tempo_authorization_list,
1643 )
1644 .unwrap();
1645
1646 let base_gas = calculate_initial_tx_gas(
1648 spec, &initcode, true, 0, 0, 0,
1650 );
1651
1652 let expected = base_gas.initial_gas + COLD_ACCOUNT_ACCESS_COST;
1654 assert_eq!(gas.initial_gas, expected, "Should include CREATE costs");
1655 }
1656
1657 #[test]
1658 fn test_aa_gas_value_transfer() {
1659 use crate::TempoBatchCallEnv;
1660 use alloy_primitives::{Bytes, TxKind};
1661 use tempo_primitives::transaction::{Call, TempoSignature};
1662
1663 let calldata = Bytes::from(vec![1]);
1664
1665 let call = Call {
1666 to: TxKind::Call(Address::random()),
1667 value: U256::from(1000), input: calldata,
1669 };
1670
1671 let aa_env = TempoBatchCallEnv {
1672 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1673 alloy_primitives::Signature::test_signature(),
1674 )),
1675 aa_calls: vec![call],
1676 key_authorization: None,
1677 signature_hash: B256::ZERO,
1678 ..Default::default()
1679 };
1680
1681 let res = calculate_aa_batch_intrinsic_gas(
1682 &aa_env.aa_calls,
1683 &aa_env.signature,
1684 None::<std::iter::Empty<&AccessListItem>>,
1685 &aa_env.tempo_authorization_list,
1686 );
1687
1688 assert_eq!(
1689 res.unwrap_err(),
1690 TempoInvalidTransaction::ValueTransferNotAllowedInAATx
1691 );
1692 }
1693
1694 #[test]
1695 fn test_aa_gas_access_list() {
1696 use crate::TempoBatchCallEnv;
1697 use alloy_primitives::{Bytes, TxKind};
1698 use revm::interpreter::gas::calculate_initial_tx_gas;
1699 use tempo_primitives::transaction::{Call, TempoSignature};
1700
1701 let spec = SpecId::CANCUN;
1702 let calldata = Bytes::from(vec![]);
1703
1704 let call = Call {
1705 to: TxKind::Call(Address::random()),
1706 value: U256::ZERO,
1707 input: calldata.clone(),
1708 };
1709
1710 let aa_env = TempoBatchCallEnv {
1711 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1712 alloy_primitives::Signature::test_signature(),
1713 )),
1714 aa_calls: vec![call],
1715 key_authorization: None,
1716 signature_hash: B256::ZERO,
1717 ..Default::default()
1718 };
1719
1720 let gas = calculate_aa_batch_intrinsic_gas(
1722 &aa_env.aa_calls,
1723 &aa_env.signature,
1724 None::<std::iter::Empty<&AccessListItem>>,
1725 &aa_env.tempo_authorization_list,
1726 )
1727 .unwrap();
1728
1729 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
1731
1732 let expected = base_gas.initial_gas + COLD_ACCOUNT_ACCESS_COST;
1734 assert_eq!(
1735 gas.initial_gas, expected,
1736 "Should match normal tx + per-call overhead"
1737 );
1738 }
1739
1740 #[test]
1741 fn test_key_authorization_rlp_encoding() {
1742 use alloy_primitives::{Address, U256};
1743 use tempo_primitives::transaction::{
1744 SignatureType, TokenLimit, key_authorization::KeyAuthorization,
1745 };
1746
1747 let chain_id = 1u64;
1749 let key_type = SignatureType::Secp256k1;
1750 let key_id = Address::random();
1751 let expiry = 1000u64;
1752 let limits = vec![
1753 TokenLimit {
1754 token: Address::random(),
1755 limit: U256::from(100),
1756 },
1757 TokenLimit {
1758 token: Address::random(),
1759 limit: U256::from(200),
1760 },
1761 ];
1762
1763 let hash1 = KeyAuthorization {
1765 chain_id,
1766 key_type,
1767 key_id,
1768 expiry: Some(expiry),
1769 limits: Some(limits.clone()),
1770 }
1771 .signature_hash();
1772
1773 let hash2 = KeyAuthorization {
1775 chain_id,
1776 key_type,
1777 key_id,
1778 expiry: Some(expiry),
1779 limits: Some(limits.clone()),
1780 }
1781 .signature_hash();
1782
1783 assert_eq!(hash1, hash2, "Hash computation should be deterministic");
1784
1785 let hash3 = KeyAuthorization {
1787 chain_id: 2,
1788 key_type,
1789 key_id,
1790 expiry: Some(expiry),
1791 limits: Some(limits),
1792 }
1793 .signature_hash();
1794 assert_ne!(
1795 hash1, hash3,
1796 "Different chain_id should produce different hash"
1797 );
1798 }
1799
1800 #[test]
1801 fn test_aa_gas_floor_gas_prague() {
1802 use crate::TempoBatchCallEnv;
1803 use alloy_primitives::{Bytes, TxKind};
1804 use revm::interpreter::gas::calculate_initial_tx_gas;
1805 use tempo_primitives::transaction::{Call, TempoSignature};
1806
1807 let spec = SpecId::PRAGUE;
1808 let calldata = Bytes::from(vec![1, 2, 3, 4, 5]); let call = Call {
1811 to: TxKind::Call(Address::random()),
1812 value: U256::ZERO,
1813 input: calldata.clone(),
1814 };
1815
1816 let aa_env = TempoBatchCallEnv {
1817 signature: TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1818 alloy_primitives::Signature::test_signature(),
1819 )),
1820 aa_calls: vec![call],
1821 key_authorization: None,
1822 signature_hash: B256::ZERO,
1823 ..Default::default()
1824 };
1825
1826 let gas = calculate_aa_batch_intrinsic_gas(
1827 &aa_env.aa_calls,
1828 &aa_env.signature,
1829 None::<std::iter::Empty<&AccessListItem>>,
1830 &aa_env.tempo_authorization_list,
1831 )
1832 .unwrap();
1833
1834 let base_gas = calculate_initial_tx_gas(spec, &calldata, false, 0, 0, 0);
1836
1837 assert_eq!(
1839 gas.floor_gas, base_gas.floor_gas,
1840 "Should calculate floor gas for Prague matching revm"
1841 );
1842 }
1843
1844 #[test]
1847 fn test_zero_value_transfer() -> eyre::Result<()> {
1848 use crate::TempoEvm;
1849
1850 let ctx = Context::mainnet()
1852 .with_db(CacheDB::new(EmptyDB::default()))
1853 .with_block(Default::default())
1854 .with_cfg(Default::default())
1855 .with_tx(TempoTxEnv::default());
1856 let mut evm = TempoEvm::new(ctx, ());
1857
1858 evm.ctx.tx.inner.value = U256::from(1000);
1860
1861 let handler = TempoEvmHandler::<_, ()>::new();
1863
1864 let result = handler.validate_env(&mut evm);
1866
1867 if let Err(EVMError::Transaction(err)) = result {
1868 assert_eq!(err, TempoInvalidTransaction::ValueTransferNotAllowed);
1869 } else {
1870 panic!("Expected ValueTransferNotAllowed error");
1871 }
1872
1873 Ok(())
1874 }
1875}