1use crate::{
2 amm::AmmLiquidityCache,
3 transaction::{TempoPoolTransactionError, TempoPooledTransaction},
4};
5use alloy_consensus::Transaction;
6
7use alloy_primitives::{Address, U256};
8use reth_chainspec::{ChainSpecProvider, EthChainSpec};
9use reth_primitives_traits::{
10 GotExpected, SealedBlock, transaction::error::InvalidTransactionError,
11};
12use reth_storage_api::{StateProvider, StateProviderFactory, errors::ProviderError};
13use reth_transaction_pool::{
14 EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome,
15 TransactionValidator, error::InvalidPoolTransactionError,
16};
17use revm::context_interface::cfg::GasId;
18use tempo_chainspec::{
19 TempoChainSpec,
20 hardfork::{TempoHardfork, TempoHardforks},
21};
22use tempo_evm::TempoEvmConfig;
23#[cfg(test)]
24use tempo_precompiles::{ACCOUNT_KEYCHAIN_ADDRESS, account_keychain::AuthorizedKey};
25use tempo_precompiles::{
26 account_keychain::AccountKeychain,
27 nonce::{INonce, NonceManager},
28 storage::Handler,
29};
30use tempo_primitives::{
31 Block,
32 subblock::has_sub_block_nonce_key_prefix,
33 transaction::{
34 RecoveredTempoAuthorization, TEMPO_EXPIRING_NONCE_KEY,
35 TEMPO_EXPIRING_NONCE_MAX_EXPIRY_SECS, TempoTransaction,
36 },
37};
38use tempo_revm::{
39 TempoBatchCallEnv, TempoStateAccess, calculate_aa_batch_intrinsic_gas,
40 gas_params::{TempoGasParams, tempo_gas_params},
41 handler::EXPIRING_NONCE_GAS,
42};
43
44const AA_VALID_BEFORE_MIN_SECS: u64 = 3;
46
47pub const DEFAULT_MAX_TEMPO_AUTHORIZATIONS: usize = 16;
49
50pub const MAX_AA_CALLS: usize = 32;
52
53pub const MAX_CALL_INPUT_SIZE: usize = 128 * 1024;
55
56pub const MAX_ACCESS_LIST_ACCOUNTS: usize = 256;
58
59pub const MAX_STORAGE_KEYS_PER_ACCOUNT: usize = 256;
61
62pub const MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL: usize = 2048;
64
65pub const MAX_TOKEN_LIMITS: usize = 256;
67
68pub const DEFAULT_AA_VALID_AFTER_MAX_SECS: u64 = 120;
74
75#[derive(Debug)]
77pub struct TempoTransactionValidator<Client> {
78 pub(crate) inner: EthTransactionValidator<Client, TempoPooledTransaction, TempoEvmConfig>,
80 pub(crate) aa_valid_after_max_secs: u64,
82 pub(crate) max_tempo_authorizations: usize,
84 pub(crate) amm_liquidity_cache: AmmLiquidityCache,
86}
87
88impl<Client> TempoTransactionValidator<Client>
89where
90 Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
91{
92 pub fn new(
93 inner: EthTransactionValidator<Client, TempoPooledTransaction, TempoEvmConfig>,
94 aa_valid_after_max_secs: u64,
95 max_tempo_authorizations: usize,
96 amm_liquidity_cache: AmmLiquidityCache,
97 ) -> Self {
98 Self {
99 inner,
100 aa_valid_after_max_secs,
101 max_tempo_authorizations,
102 amm_liquidity_cache,
103 }
104 }
105
106 pub fn amm_liquidity_cache(&self) -> AmmLiquidityCache {
108 self.amm_liquidity_cache.clone()
109 }
110
111 pub fn client(&self) -> &Client {
113 self.inner.client()
114 }
115
116 fn validate_keychain_version(
119 &self,
120 transaction: &TempoPooledTransaction,
121 spec: TempoHardfork,
122 ) -> Result<(), TempoPoolTransactionError> {
123 let Some(tx) = transaction.inner().as_aa() else {
124 return Ok(());
125 };
126
127 if let Err(e) = tx.signature().validate_version(spec.is_t1c()) {
128 return Err(e.into());
129 }
130 for auth_sig in &tx.tx().tempo_authorization_list {
131 if let Err(e) = auth_sig.signature().validate_version(spec.is_t1c()) {
132 return Err(e.into());
133 }
134 }
135
136 Ok(())
137 }
138
139 fn validate_spending_limit(
140 &self,
141 transaction: &TempoPooledTransaction,
142 fee_token: Address,
143 remaining_limit: U256,
144 ) -> Result<(), TempoPoolTransactionError> {
145 let fee_cost = transaction.fee_token_cost();
146 if fee_cost > remaining_limit {
147 return Err(TempoPoolTransactionError::SpendingLimitExceeded {
148 fee_token,
149 cost: fee_cost,
150 remaining: remaining_limit,
151 });
152 }
153
154 Ok(())
155 }
156
157 fn validate_against_keychain(
163 &self,
164 transaction: &TempoPooledTransaction,
165 state_provider: &mut impl StateProvider,
166 fee_payer: Address,
167 fee_token: Address,
168 ) -> Result<Result<(), TempoPoolTransactionError>, ProviderError> {
169 let Some(tx) = transaction.inner().as_aa() else {
170 return Ok(Ok(()));
171 };
172
173 let current_time = self.inner.fork_tracker().tip_timestamp();
174 let spec = self.inner.chain_spec().tempo_hardfork_at(current_time);
175
176 let auth = tx.tx().key_authorization.as_ref();
177
178 if let Some(auth) = auth {
180 if !auth
182 .recover_signer()
183 .is_ok_and(|signer| signer == transaction.sender())
184 {
185 return Ok(Err(TempoPoolTransactionError::Keychain(
186 "Invalid KeyAuthorization signature",
187 )));
188 }
189
190 if auth
194 .validate_chain_id(self.inner.chain_spec().chain_id(), spec.is_t1c())
195 .is_err()
196 {
197 return Ok(Err(TempoPoolTransactionError::Keychain(
198 "KeyAuthorization chain_id does not match current chain",
199 )));
200 }
201
202 let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
206 if let Some(expiry) = auth.expiry
207 && expiry <= min_allowed
208 {
209 return Ok(Err(TempoPoolTransactionError::KeyAuthorizationExpired {
210 expiry,
211 min_allowed,
212 }));
213 }
214 }
215
216 let Some(sig) = tx.signature().as_keychain() else {
217 return Ok(Ok(()));
218 };
219
220 if sig.user_address != transaction.sender() {
222 return Ok(Err(TempoPoolTransactionError::Keychain(
223 "Keychain signature user_address does not match sender",
224 )));
225 }
226
227 let Ok(key_id) = sig.key_id(&tx.signature_hash()) else {
229 return Ok(Err(TempoPoolTransactionError::Keychain(
230 "Failed to recover access key ID from Keychain signature",
231 )));
232 };
233
234 let authorized_key = state_provider
235 .with_read_only_storage_ctx(spec, || {
236 AccountKeychain::new().keys[transaction.sender()][key_id].read()
237 })
238 .map_err(ProviderError::other)?;
239
240 if let Some(auth) = auth {
244 if auth.key_id != key_id {
245 return Ok(Err(TempoPoolTransactionError::Keychain(
246 "KeyAuthorization key_id does not match Keychain signature key_id",
247 )));
248 }
249
250 if authorized_key.expiry > 0 {
251 return Ok(Err(TempoPoolTransactionError::Keychain(
252 "access key already exists",
253 )));
254 }
255
256 if authorized_key.is_revoked {
257 return Ok(Err(TempoPoolTransactionError::Keychain(
258 "access key has been revoked",
259 )));
260 }
261
262 if let Some(expiry) = auth.expiry
263 && expiry < u64::MAX
264 {
265 transaction.set_key_expiry(Some(expiry));
266 }
267
268 if fee_payer == transaction.sender()
269 && let Some(limits) = &auth.limits
270 {
271 let remaining_limit = limits
272 .iter()
273 .rev()
274 .find(|limit| limit.token == fee_token)
275 .map(|limit| limit.limit)
276 .unwrap_or(U256::ZERO);
277
278 if let Err(err) =
279 self.validate_spending_limit(transaction, fee_token, remaining_limit)
280 {
281 return Ok(Err(err));
282 }
283 }
284
285 return Ok(Ok(()));
286 }
287
288 if authorized_key.is_revoked {
290 return Ok(Err(TempoPoolTransactionError::Keychain(
291 "access key has been revoked",
292 )));
293 }
294
295 if authorized_key.expiry == 0 {
297 return Ok(Err(TempoPoolTransactionError::Keychain(
298 "access key does not exist",
299 )));
300 }
301
302 let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
306 if authorized_key.expiry <= min_allowed {
307 return Ok(Err(TempoPoolTransactionError::AccessKeyExpired {
308 expiry: authorized_key.expiry,
309 min_allowed,
310 }));
311 }
312
313 if authorized_key.expiry < u64::MAX {
315 transaction.set_key_expiry(Some(authorized_key.expiry));
316 }
317
318 if fee_payer == transaction.sender() && authorized_key.enforce_limits {
321 let limit_key = AccountKeychain::spending_limit_key(transaction.sender(), key_id);
323 let remaining_limit = state_provider
324 .with_read_only_storage_ctx(spec, || {
325 AccountKeychain::new().spending_limits[limit_key][fee_token].read()
326 })
327 .map_err(ProviderError::other)?;
328
329 if let Err(err) = self.validate_spending_limit(transaction, fee_token, remaining_limit)
330 {
331 return Ok(Err(err));
332 }
333 }
334
335 Ok(Ok(()))
336 }
337
338 fn ensure_authorization_list_size(
340 &self,
341 transaction: &TempoPooledTransaction,
342 ) -> Result<(), TempoPoolTransactionError> {
343 let Some(aa_tx) = transaction.inner().as_aa() else {
344 return Ok(());
345 };
346
347 let count = aa_tx.tx().tempo_authorization_list.len();
348 if count > self.max_tempo_authorizations {
349 return Err(TempoPoolTransactionError::TooManyAuthorizations {
350 count,
351 max_allowed: self.max_tempo_authorizations,
352 });
353 }
354
355 Ok(())
356 }
357
358 fn ensure_valid_conditionals(
360 &self,
361 tx: &TempoTransaction,
362 ) -> Result<(), TempoPoolTransactionError> {
363 let current_time = self.inner.fork_tracker().tip_timestamp();
364
365 let spec = self.inner.chain_spec().tempo_hardfork_at(current_time);
367 let is_expiring_nonce = tx.is_expiring_nonce_tx() && spec.is_t1();
368
369 if is_expiring_nonce && tx.valid_before.is_none() {
371 return Err(TempoPoolTransactionError::ExpiringNonceMissingValidBefore);
372 }
373
374 if is_expiring_nonce && tx.nonce != 0 {
376 return Err(TempoPoolTransactionError::ExpiringNonceNonceNotZero);
377 }
378
379 if let Some(valid_before) = tx.valid_before {
381 let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
383 if valid_before <= min_allowed {
384 return Err(TempoPoolTransactionError::InvalidValidBefore {
385 valid_before,
386 min_allowed,
387 });
388 }
389
390 if is_expiring_nonce {
392 let max_allowed = current_time.saturating_add(TEMPO_EXPIRING_NONCE_MAX_EXPIRY_SECS);
393 if valid_before > max_allowed {
394 return Err(TempoPoolTransactionError::ExpiringNonceValidBeforeTooFar {
395 valid_before,
396 max_allowed,
397 });
398 }
399 }
400 }
401
402 if let Some(valid_after) = tx.valid_after {
404 let current_time = std::time::SystemTime::now()
406 .duration_since(std::time::UNIX_EPOCH)
407 .map(|d| d.as_secs())
408 .unwrap_or(0);
409 let max_allowed = current_time.saturating_add(self.aa_valid_after_max_secs);
410 if valid_after > max_allowed {
411 return Err(TempoPoolTransactionError::InvalidValidAfter {
412 valid_after,
413 max_allowed,
414 });
415 }
416 }
417
418 Ok(())
419 }
420
421 fn ensure_aa_intrinsic_gas(
435 &self,
436 transaction: &TempoPooledTransaction,
437 spec: TempoHardfork,
438 state_provider: &impl StateProvider,
439 ) -> Result<(), TempoPoolTransactionError> {
440 let sender = transaction.sender();
441 let Some(aa_tx) = transaction.inner().as_aa() else {
442 return Ok(());
443 };
444
445 let tx = aa_tx.tx();
446
447 let aa_env = TempoBatchCallEnv {
449 signature: aa_tx.signature().clone(),
450 valid_before: tx.valid_before,
451 valid_after: tx.valid_after,
452 aa_calls: tx.calls.clone(),
453 tempo_authorization_list: tx
454 .tempo_authorization_list
455 .iter()
456 .map(|auth| RecoveredTempoAuthorization::recover(auth.clone()))
457 .collect(),
458 nonce_key: tx.nonce_key,
459 subblock_transaction: tx.subblock_proposer().is_some(),
460 key_authorization: tx.key_authorization.clone(),
461 signature_hash: aa_tx.signature_hash(),
462 tx_hash: *aa_tx.hash(),
463 expiring_nonce_hash: tx
464 .is_expiring_nonce_tx()
465 .then(|| aa_tx.expiring_nonce_hash(sender)),
466 override_key_id: None,
467 };
468
469 let gas_params = tempo_gas_params(spec);
471
472 let mut init_and_floor_gas = calculate_aa_batch_intrinsic_gas(
473 &aa_env,
474 &gas_params,
475 Some(tx.access_list.iter()),
476 spec,
477 )
478 .map_err(|_| TempoPoolTransactionError::NonZeroValue)?;
479
480 if spec.is_t1() {
483 if tx.nonce_key == TEMPO_EXPIRING_NONCE_KEY {
485 init_and_floor_gas.initial_gas += EXPIRING_NONCE_GAS;
486 } else if tx.nonce == 0 {
487 init_and_floor_gas.initial_gas += gas_params.get(GasId::new_account_cost());
490 } else if !tx.nonce_key.is_zero() {
491 init_and_floor_gas.initial_gas += spec.gas_existing_nonce_key();
494 }
495 if !tx.nonce_key.is_zero()
498 && tx.is_create()
499 && state_provider
501 .account_nonce(&sender)
502 .ok()
503 .flatten()
504 .unwrap_or_default()
505 == 0
506 {
507 init_and_floor_gas.initial_gas += gas_params.get(GasId::new_account_cost());
508 }
509 } else if !tx.nonce_key.is_zero() {
510 if tx.nonce == 0 {
512 init_and_floor_gas.initial_gas += spec.gas_new_nonce_key();
514 } else {
515 init_and_floor_gas.initial_gas += spec.gas_existing_nonce_key();
517 }
518 }
519
520 let gas_limit = tx.gas_limit;
521
522 if gas_limit < init_and_floor_gas.initial_gas {
524 return Err(
525 TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost {
526 gas_limit,
527 intrinsic_gas: init_and_floor_gas.initial_gas,
528 },
529 );
530 }
531
532 if gas_limit < init_and_floor_gas.floor_gas {
534 return Err(
535 TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost {
536 gas_limit,
537 intrinsic_gas: init_and_floor_gas.floor_gas,
538 },
539 );
540 }
541
542 Ok(())
543 }
544
545 fn ensure_aa_field_limits(
551 &self,
552 transaction: &TempoPooledTransaction,
553 ) -> Result<(), TempoPoolTransactionError> {
554 let Some(aa_tx) = transaction.inner().as_aa() else {
555 return Ok(());
556 };
557
558 let tx = aa_tx.tx();
559
560 if tx.calls.is_empty() {
561 return Err(TempoPoolTransactionError::NoCalls);
562 }
563
564 if tx.calls.len() > MAX_AA_CALLS {
566 return Err(TempoPoolTransactionError::TooManyCalls {
567 count: tx.calls.len(),
568 max_allowed: MAX_AA_CALLS,
569 });
570 }
571
572 for (idx, call) in tx.calls.iter().enumerate() {
574 if call.to.is_create() {
575 if idx != 0 {
577 return Err(TempoPoolTransactionError::CreateCallNotFirst);
578 }
579 if !tx.tempo_authorization_list.is_empty() {
581 return Err(TempoPoolTransactionError::CreateCallWithAuthorizationList);
582 }
583 }
584
585 if call.input.len() > MAX_CALL_INPUT_SIZE {
586 return Err(TempoPoolTransactionError::CallInputTooLarge {
587 call_index: idx,
588 size: call.input.len(),
589 max_allowed: MAX_CALL_INPUT_SIZE,
590 });
591 }
592 }
593
594 if tx.access_list.len() > MAX_ACCESS_LIST_ACCOUNTS {
596 return Err(TempoPoolTransactionError::TooManyAccessListAccounts {
597 count: tx.access_list.len(),
598 max_allowed: MAX_ACCESS_LIST_ACCOUNTS,
599 });
600 }
601
602 let mut total_storage_keys = 0usize;
604 for (idx, entry) in tx.access_list.iter().enumerate() {
605 if entry.storage_keys.len() > MAX_STORAGE_KEYS_PER_ACCOUNT {
606 return Err(TempoPoolTransactionError::TooManyStorageKeysPerAccount {
607 account_index: idx,
608 count: entry.storage_keys.len(),
609 max_allowed: MAX_STORAGE_KEYS_PER_ACCOUNT,
610 });
611 }
612 total_storage_keys = total_storage_keys.saturating_add(entry.storage_keys.len());
613 }
614
615 if total_storage_keys > MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL {
616 return Err(TempoPoolTransactionError::TooManyTotalStorageKeys {
617 count: total_storage_keys,
618 max_allowed: MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL,
619 });
620 }
621
622 if let Some(ref key_auth) = tx.key_authorization
624 && let Some(ref limits) = key_auth.limits
625 && limits.len() > MAX_TOKEN_LIMITS
626 {
627 return Err(TempoPoolTransactionError::TooManyTokenLimits {
628 count: limits.len(),
629 max_allowed: MAX_TOKEN_LIMITS,
630 });
631 }
632
633 Ok(())
634 }
635
636 fn ensure_min_base_fee(
642 &self,
643 transaction: &TempoPooledTransaction,
644 spec: TempoHardfork,
645 ) -> Result<(), TempoPoolTransactionError> {
646 let min_base_fee = spec.base_fee();
647 let max_fee_per_gas = transaction.max_fee_per_gas();
648
649 if max_fee_per_gas < min_base_fee as u128 {
650 return Err(TempoPoolTransactionError::FeeCapBelowMinBaseFee {
651 max_fee_per_gas,
652 min_base_fee,
653 });
654 }
655
656 Ok(())
657 }
658
659 fn validate_one(
660 &self,
661 origin: TransactionOrigin,
662 transaction: TempoPooledTransaction,
663 mut state_provider: impl StateProvider,
664 ) -> TransactionValidationOutcome<TempoPooledTransaction> {
665 let spec = self
667 .inner
668 .chain_spec()
669 .tempo_hardfork_at(self.inner.fork_tracker().tip_timestamp());
670
671 if transaction.inner().is_system_tx() {
673 return TransactionValidationOutcome::Invalid(
674 transaction,
675 InvalidPoolTransactionError::Consensus(InvalidTransactionError::TxTypeNotSupported),
676 );
677 }
678
679 let tx_size = transaction.encoded_length();
681 let max_size = self.inner.max_tx_input_bytes();
682 if tx_size > max_size {
683 return TransactionValidationOutcome::Invalid(
684 transaction,
685 InvalidPoolTransactionError::OversizedData {
686 size: tx_size,
687 limit: max_size,
688 },
689 );
690 }
691
692 if let Err(err) = self.ensure_min_base_fee(&transaction, spec) {
694 return TransactionValidationOutcome::Invalid(
695 transaction,
696 InvalidPoolTransactionError::other(err),
697 );
698 }
699
700 if let Err(err) = self.validate_keychain_version(&transaction, spec) {
703 return TransactionValidationOutcome::Invalid(
704 transaction,
705 InvalidPoolTransactionError::other(err),
706 );
707 }
708
709 if !transaction.inner().value().is_zero() {
713 return TransactionValidationOutcome::Invalid(
714 transaction,
715 InvalidPoolTransactionError::other(TempoPoolTransactionError::NonZeroValue),
716 );
717 }
718
719 if let Some(tx) = transaction.inner().as_aa()
721 && let Err(err) = self.ensure_valid_conditionals(tx.tx())
722 {
723 return TransactionValidationOutcome::Invalid(
724 transaction,
725 InvalidPoolTransactionError::other(err),
726 );
727 }
728
729 if let Err(err) = self.ensure_authorization_list_size(&transaction) {
731 return TransactionValidationOutcome::Invalid(
732 transaction,
733 InvalidPoolTransactionError::other(err),
734 );
735 }
736
737 if transaction.inner().is_aa() {
738 if let Err(err) = self.ensure_aa_intrinsic_gas(&transaction, spec, &state_provider) {
743 return TransactionValidationOutcome::Invalid(
744 transaction,
745 InvalidPoolTransactionError::other(err),
746 );
747 }
748 } else {
749 if let Err(err) = ensure_intrinsic_gas_tempo_tx(&transaction, spec) {
751 return TransactionValidationOutcome::Invalid(transaction, err);
752 }
753 }
754
755 if let Err(err) = self.ensure_aa_field_limits(&transaction) {
758 return TransactionValidationOutcome::Invalid(
759 transaction,
760 InvalidPoolTransactionError::other(err),
761 );
762 }
763
764 let fee_payer = match transaction.inner().fee_payer(transaction.sender()) {
765 Ok(fee_payer) => fee_payer,
766 Err(_err) => {
767 return TransactionValidationOutcome::Invalid(
768 transaction,
769 InvalidPoolTransactionError::other(
770 TempoPoolTransactionError::InvalidFeePayerSignature,
771 ),
772 );
773 }
774 };
775
776 if transaction
777 .inner()
778 .as_aa()
779 .is_some_and(|aa| aa.tx().fee_payer_signature.is_some())
780 && fee_payer == transaction.sender()
781 {
782 return TransactionValidationOutcome::Invalid(
783 transaction,
784 InvalidPoolTransactionError::other(
785 TempoPoolTransactionError::SelfSponsoredFeePayer,
786 ),
787 );
788 }
789
790 let fee_token = match state_provider.get_fee_token(transaction.inner(), fee_payer, spec) {
791 Ok(fee_token) => fee_token,
792 Err(err) => {
793 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
794 }
795 };
796
797 transaction.set_resolved_fee_token(fee_token);
799
800 match state_provider.is_valid_fee_token(spec, fee_token) {
802 Ok(valid) => {
803 if !valid {
804 return TransactionValidationOutcome::Invalid(
805 transaction,
806 InvalidPoolTransactionError::other(
807 TempoPoolTransactionError::InvalidFeeToken(fee_token),
808 ),
809 );
810 }
811 }
812 Err(err) => {
813 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
814 }
815 }
816
817 match state_provider.is_fee_token_paused(spec, fee_token) {
819 Ok(paused) => {
820 if paused {
821 return TransactionValidationOutcome::Invalid(
822 transaction,
823 InvalidPoolTransactionError::other(
824 TempoPoolTransactionError::PausedFeeToken(fee_token),
825 ),
826 );
827 }
828 }
829 Err(err) => {
830 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
831 }
832 }
833
834 match state_provider.can_fee_payer_transfer(fee_token, fee_payer, spec) {
836 Ok(valid) => {
837 if !valid {
838 return TransactionValidationOutcome::Invalid(
839 transaction,
840 InvalidPoolTransactionError::other(
841 TempoPoolTransactionError::BlackListedFeePayer {
842 fee_token,
843 fee_payer,
844 },
845 ),
846 );
847 }
848 }
849 Err(err) => {
850 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
851 }
852 }
853
854 let balance = match state_provider.get_token_balance(fee_token, fee_payer, spec) {
855 Ok(balance) => balance,
856 Err(err) => {
857 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
858 }
859 };
860
861 let cost = transaction.fee_token_cost();
863 if balance < cost {
864 return TransactionValidationOutcome::Invalid(
865 transaction,
866 InvalidTransactionError::InsufficientFunds(
867 GotExpected {
868 got: balance,
869 expected: cost,
870 }
871 .into(),
872 )
873 .into(),
874 );
875 }
876
877 match self
878 .amm_liquidity_cache
879 .has_enough_liquidity(fee_token, cost, &state_provider)
880 {
881 Ok(true) => {}
882 Ok(false) => {
883 return TransactionValidationOutcome::Invalid(
884 transaction,
885 InvalidPoolTransactionError::other(
886 TempoPoolTransactionError::InsufficientLiquidity(fee_token),
887 ),
888 );
889 }
890 Err(err) => {
891 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
892 }
893 }
894
895 match self.validate_against_keychain(
897 &transaction,
898 &mut state_provider,
899 fee_payer,
900 fee_token,
901 ) {
902 Ok(Ok(())) => {}
903 Ok(Err(err)) => {
904 return TransactionValidationOutcome::Invalid(
905 transaction,
906 InvalidPoolTransactionError::other(err),
907 );
908 }
909 Err(err) => {
910 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
911 }
912 }
913
914 match self
915 .inner
916 .validate_one_with_state_provider(origin, transaction, &state_provider)
917 {
918 TransactionValidationOutcome::Valid {
919 balance,
920 mut state_nonce,
921 bytecode_hash,
922 transaction,
923 propagate,
924 authorities,
925 } => {
926 if let Some(nonce_key) = transaction.transaction().nonce_key()
928 && !nonce_key.is_zero()
929 {
930 if has_sub_block_nonce_key_prefix(&nonce_key) {
932 return TransactionValidationOutcome::Invalid(
933 transaction.into_transaction(),
934 InvalidPoolTransactionError::other(
935 TempoPoolTransactionError::SubblockNonceKey,
936 ),
937 );
938 }
939
940 let current_time = self.inner.fork_tracker().tip_timestamp();
942 let is_t1_active = self
943 .inner
944 .chain_spec()
945 .is_t1_active_at_timestamp(current_time);
946
947 if is_t1_active && nonce_key == TEMPO_EXPIRING_NONCE_KEY {
948 let replay_hash = if spec.is_t1b() {
956 transaction
957 .transaction()
958 .inner()
959 .as_aa()
960 .expect("expiring nonce tx must be AA")
961 .expiring_nonce_hash(transaction.transaction().sender())
962 } else {
963 *transaction.hash()
964 };
965
966 match state_provider.with_read_only_storage_ctx(spec, || {
971 NonceManager::new().is_expiring_nonce_seen(replay_hash, current_time)
972 }) {
973 Err(err) => {
974 return TransactionValidationOutcome::Error(
975 *transaction.hash(),
976 Box::new(err),
977 );
978 }
979 Ok(true) => {
980 return TransactionValidationOutcome::Invalid(
981 transaction.into_transaction(),
982 InvalidPoolTransactionError::other(
983 TempoPoolTransactionError::ExpiringNonceReplay,
984 ),
985 );
986 }
987 Ok(false) => (),
988 };
989 } else {
990 state_nonce = match state_provider.with_read_only_storage_ctx(spec, || {
992 NonceManager::new().get_nonce(INonce::getNonceCall {
993 account: transaction.transaction().sender(),
994 nonceKey: nonce_key,
995 })
996 }) {
997 Ok(nonce) => nonce,
998 Err(err) => {
999 return TransactionValidationOutcome::Error(
1000 *transaction.hash(),
1001 Box::new(err),
1002 );
1003 }
1004 };
1005 let tx_nonce = transaction.nonce();
1006 if tx_nonce < state_nonce {
1007 return TransactionValidationOutcome::Invalid(
1008 transaction.into_transaction(),
1009 InvalidTransactionError::NonceNotConsistent {
1010 tx: tx_nonce,
1011 state: state_nonce,
1012 }
1013 .into(),
1014 );
1015 }
1016 }
1017 }
1018
1019 transaction.transaction().prepare_tx_env();
1021
1022 TransactionValidationOutcome::Valid {
1023 balance,
1024 state_nonce,
1025 bytecode_hash,
1026 transaction,
1027 propagate,
1028 authorities,
1029 }
1030 }
1031 outcome => outcome,
1032 }
1033 }
1034}
1035
1036impl<Client> TransactionValidator for TempoTransactionValidator<Client>
1037where
1038 Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
1039{
1040 type Transaction = TempoPooledTransaction;
1041 type Block = Block;
1042
1043 async fn validate_transaction(
1044 &self,
1045 origin: TransactionOrigin,
1046 transaction: Self::Transaction,
1047 ) -> TransactionValidationOutcome<Self::Transaction> {
1048 let state_provider = match self.inner.client().latest() {
1049 Ok(provider) => provider,
1050 Err(err) => {
1051 return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
1052 }
1053 };
1054
1055 self.validate_one(origin, transaction, state_provider)
1056 }
1057
1058 async fn validate_transactions(
1059 &self,
1060 transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction), IntoIter: Send>
1061 + Send,
1062 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
1063 let transactions: Vec<_> = transactions.into_iter().collect();
1064 let state_provider = match self.inner.client().latest() {
1065 Ok(provider) => provider,
1066 Err(err) => {
1067 return transactions
1068 .into_iter()
1069 .map(|(_, tx)| {
1070 TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
1071 })
1072 .collect();
1073 }
1074 };
1075
1076 transactions
1077 .into_iter()
1078 .map(|(origin, tx)| self.validate_one(origin, tx, &state_provider))
1079 .collect()
1080 }
1081
1082 async fn validate_transactions_with_origin(
1083 &self,
1084 origin: TransactionOrigin,
1085 transactions: impl IntoIterator<Item = Self::Transaction> + Send,
1086 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
1087 let state_provider = match self.inner.client().latest() {
1088 Ok(provider) => provider,
1089 Err(err) => {
1090 return transactions
1091 .into_iter()
1092 .map(|tx| {
1093 TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
1094 })
1095 .collect();
1096 }
1097 };
1098
1099 transactions
1100 .into_iter()
1101 .map(|tx| self.validate_one(origin, tx, &state_provider))
1102 .collect()
1103 }
1104
1105 fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
1106 self.inner.on_new_head_block(new_tip_block)
1107 }
1108}
1109
1110pub fn ensure_intrinsic_gas_tempo_tx(
1112 tx: &TempoPooledTransaction,
1113 spec: TempoHardfork,
1114) -> Result<(), InvalidPoolTransactionError> {
1115 let gas_params = tempo_gas_params(spec);
1116
1117 let mut gas = gas_params.initial_tx_gas(
1118 tx.input(),
1119 tx.is_create(),
1120 tx.access_list().map(|l| l.len()).unwrap_or_default() as u64,
1121 tx.access_list()
1122 .map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
1123 .unwrap_or_default() as u64,
1124 tx.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
1125 );
1126
1127 for auth in tx.authorization_list().unwrap_or_default() {
1131 if auth.nonce == 0 {
1132 gas.initial_gas += gas_params.tx_tip1000_auth_account_creation_cost();
1133 }
1134 }
1135
1136 if spec.is_t1() && tx.nonce() == 0 {
1141 if tx.nonce_key() == Some(TEMPO_EXPIRING_NONCE_KEY) {
1142 gas.initial_gas += EXPIRING_NONCE_GAS;
1143 } else {
1144 gas.initial_gas += gas_params.get(GasId::new_account_cost());
1145 }
1146 }
1147
1148 let gas_limit = tx.gas_limit();
1149 if gas_limit < gas.initial_gas || gas_limit < gas.floor_gas {
1150 Err(InvalidPoolTransactionError::IntrinsicGasTooLow)
1151 } else {
1152 Ok(())
1153 }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159 use crate::{test_utils::TxBuilder, transaction::TempoPoolTransactionError};
1160 use alloy_consensus::{Header, Signed, Transaction, TxLegacy};
1161 use alloy_primitives::{Address, B256, TxKind, U256, address, uint};
1162 use alloy_signer::Signature;
1163 use reth_primitives_traits::SignedTransaction;
1164 use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1165 use reth_transaction_pool::{
1166 PoolTransaction, blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
1167 };
1168 use std::sync::Arc;
1169 use tempo_chainspec::spec::{MODERATO, TEMPO_T1_TX_GAS_LIMIT_CAP};
1170 use tempo_precompiles::{
1171 PATH_USD_ADDRESS, TIP403_REGISTRY_ADDRESS,
1172 tip20::{TIP20Token, slots as tip20_slots},
1173 tip403_registry::{ITIP403Registry, PolicyData, TIP403Registry},
1174 };
1175 use tempo_primitives::{
1176 Block, TempoHeader, TempoPrimitives, TempoTxEnvelope,
1177 transaction::{
1178 TempoTransaction,
1179 envelope::TEMPO_SYSTEM_TX_SIGNATURE,
1180 tempo_transaction::Call,
1181 tt_signature::{PrimitiveSignature, TempoSignature},
1182 tt_signed::AASigned,
1183 },
1184 };
1185 use tempo_revm::TempoStateAccess;
1186
1187 const TEST_VALIDITY_WINDOW: u64 = 25;
1189
1190 fn create_mock_block(timestamp: u64) -> SealedBlock<Block> {
1192 let header = TempoHeader {
1193 inner: Header {
1194 timestamp,
1195 gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1196 ..Default::default()
1197 },
1198 ..Default::default()
1199 };
1200 let block = Block {
1201 header,
1202 body: Default::default(),
1203 };
1204 SealedBlock::seal_slow(block)
1205 }
1206
1207 fn create_aa_transaction(
1210 valid_after: Option<u64>,
1211 valid_before: Option<u64>,
1212 ) -> TempoPooledTransaction {
1213 let mut builder = TxBuilder::aa(Address::random())
1214 .fee_token(address!("0000000000000000000000000000000000000002"));
1215 if let Some(va) = valid_after {
1216 builder = builder.valid_after(va);
1217 }
1218 if let Some(vb) = valid_before {
1219 builder = builder.valid_before(vb);
1220 }
1221 builder.build()
1222 }
1223
1224 fn setup_validator(
1226 transaction: &TempoPooledTransaction,
1227 tip_timestamp: u64,
1228 ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
1229 let provider = MockEthProvider::<TempoPrimitives>::new()
1230 .with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
1231 provider.add_account(
1232 transaction.sender(),
1233 ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO),
1234 );
1235 let block_with_gas = Block {
1236 header: TempoHeader {
1237 inner: Header {
1238 gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1239 ..Default::default()
1240 },
1241 ..Default::default()
1242 },
1243 ..Default::default()
1244 };
1245 provider.add_block(B256::random(), block_with_gas);
1246
1247 let usd_currency_value =
1250 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
1251 let transfer_policy_id_packed =
1254 uint!(0x0000000000000000000000010000000000000000000000000000000000000000_U256);
1255 let balance_slot = TIP20Token::from_address(PATH_USD_ADDRESS)
1257 .expect("PATH_USD_ADDRESS is a valid TIP20 token")
1258 .balances[transaction.sender()]
1259 .slot();
1260 let fee_payer_balance = U256::from(1_000_000_000_000u64); provider.add_account(
1263 PATH_USD_ADDRESS,
1264 ExtendedAccount::new(0, U256::ZERO).extend_storage([
1265 (tip20_slots::CURRENCY.into(), usd_currency_value),
1266 (
1267 tip20_slots::TRANSFER_POLICY_ID.into(),
1268 transfer_policy_id_packed,
1269 ),
1270 (balance_slot.into(), fee_payer_balance),
1271 ]),
1272 );
1273
1274 let inner =
1275 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1276 .disable_balance_check()
1277 .build(InMemoryBlobStore::default());
1278 let amm_cache =
1279 AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
1280 let validator = TempoTransactionValidator::new(
1281 inner,
1282 DEFAULT_AA_VALID_AFTER_MAX_SECS,
1283 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1284 amm_cache,
1285 );
1286
1287 let mock_block = create_mock_block(tip_timestamp);
1289 validator.on_new_head_block(&mock_block);
1290
1291 validator
1292 }
1293
1294 #[tokio::test]
1295 async fn test_some_balance() {
1296 let transaction = TxBuilder::eip1559(Address::random())
1297 .value(U256::from(1))
1298 .build_eip1559();
1299 let validator = setup_validator(&transaction, 0);
1300
1301 let outcome = validator
1302 .validate_transaction(TransactionOrigin::External, transaction.clone())
1303 .await;
1304
1305 match outcome {
1306 TransactionValidationOutcome::Invalid(_, ref err) => {
1307 assert!(matches!(
1308 err.downcast_other_ref::<TempoPoolTransactionError>(),
1309 Some(TempoPoolTransactionError::NonZeroValue)
1310 ));
1311 }
1312 _ => panic!("Expected Invalid outcome with NonZeroValue error, got: {outcome:?}"),
1313 }
1314 }
1315
1316 #[tokio::test]
1317 async fn test_system_tx_rejected_as_invalid() {
1318 let tx = TxLegacy {
1319 chain_id: Some(MODERATO.chain_id()),
1320 nonce: 0,
1321 gas_price: 0,
1322 gas_limit: 0,
1323 to: TxKind::Call(Address::ZERO),
1324 value: U256::ZERO,
1325 input: Default::default(),
1326 };
1327 let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1328 let transaction = TempoPooledTransaction::new(
1329 reth_primitives_traits::Recovered::new_unchecked(envelope, Address::ZERO),
1330 );
1331 let validator = setup_validator(&transaction, 0);
1332
1333 let outcome = validator
1334 .validate_transaction(TransactionOrigin::External, transaction)
1335 .await;
1336
1337 match outcome {
1338 TransactionValidationOutcome::Invalid(_, err) => {
1339 assert!(matches!(
1340 err,
1341 InvalidPoolTransactionError::Consensus(
1342 InvalidTransactionError::TxTypeNotSupported
1343 )
1344 ));
1345 }
1346 _ => panic!("Expected Invalid outcome with TxTypeNotSupported error, got: {outcome:?}"),
1347 }
1348 }
1349
1350 #[tokio::test]
1351 async fn test_invalid_fee_payer_signature_rejected() {
1352 let calls: Vec<Call> = vec![Call {
1353 to: TxKind::Call(Address::random()),
1354 value: U256::ZERO,
1355 input: Default::default(),
1356 }];
1357
1358 let tx = TempoTransaction {
1359 chain_id: MODERATO.chain_id(),
1360 max_priority_fee_per_gas: 1_000_000_000,
1361 max_fee_per_gas: 20_000_000_000,
1362 gas_limit: 1_000_000,
1363 calls,
1364 nonce_key: U256::ZERO,
1365 nonce: 0,
1366 fee_token: Some(PATH_USD_ADDRESS),
1367 fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
1368 ..Default::default()
1369 };
1370
1371 let signed = AASigned::new_unhashed(
1372 tx,
1373 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1374 );
1375 let transaction = TempoPooledTransaction::new(
1376 TempoTxEnvelope::from(signed).try_into_recovered().unwrap(),
1377 );
1378 let validator = setup_validator(&transaction, 0);
1379
1380 let outcome = validator
1381 .validate_transaction(TransactionOrigin::External, transaction)
1382 .await;
1383
1384 match outcome {
1385 TransactionValidationOutcome::Invalid(_, ref err) => {
1386 assert!(matches!(
1387 err.downcast_other_ref::<TempoPoolTransactionError>(),
1388 Some(TempoPoolTransactionError::InvalidFeePayerSignature)
1389 ));
1390 }
1391 _ => panic!(
1392 "Expected Invalid outcome with InvalidFeePayerSignature error, got: {outcome:?}"
1393 ),
1394 }
1395 }
1396
1397 #[tokio::test]
1398 async fn test_self_sponsored_fee_payer_rejected() {
1399 use alloy_signer::SignerSync;
1400 use alloy_signer_local::PrivateKeySigner;
1401
1402 let signer = PrivateKeySigner::random();
1403 let sender = signer.address();
1404
1405 let mut tx = TempoTransaction {
1406 chain_id: MODERATO.chain_id(),
1407 max_priority_fee_per_gas: 1_000_000_000,
1408 max_fee_per_gas: 20_000_000_000,
1409 gas_limit: 1_000_000,
1410 calls: vec![Call {
1411 to: TxKind::Call(Address::random()),
1412 value: U256::ZERO,
1413 input: Default::default(),
1414 }],
1415 nonce_key: U256::ZERO,
1416 nonce: 0,
1417 fee_token: Some(PATH_USD_ADDRESS),
1418 fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
1419 ..Default::default()
1420 };
1421
1422 let fee_payer_hash = tx.fee_payer_signature_hash(sender);
1423 tx.fee_payer_signature = Some(
1424 signer
1425 .sign_hash_sync(&fee_payer_hash)
1426 .expect("fee payer signing should succeed"),
1427 );
1428
1429 let signed = AASigned::new_unhashed(
1430 tx,
1431 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1432 );
1433
1434 let envelope: TempoTxEnvelope = signed.into();
1435 let transaction = TempoPooledTransaction::new(
1436 reth_primitives_traits::Recovered::new_unchecked(envelope, sender),
1437 );
1438 let validator = setup_validator(&transaction, 0);
1439
1440 let outcome = validator
1441 .validate_transaction(TransactionOrigin::External, transaction)
1442 .await;
1443
1444 match outcome {
1445 TransactionValidationOutcome::Invalid(_, ref err) => {
1446 assert!(matches!(
1447 err.downcast_other_ref::<TempoPoolTransactionError>(),
1448 Some(TempoPoolTransactionError::SelfSponsoredFeePayer)
1449 ));
1450 }
1451 _ => panic!(
1452 "Expected Invalid outcome with SelfSponsoredFeePayer error, got: {outcome:?}"
1453 ),
1454 }
1455 }
1456
1457 #[tokio::test]
1458 async fn test_aa_valid_before_check() {
1459 let current_time = std::time::SystemTime::now()
1461 .duration_since(std::time::UNIX_EPOCH)
1462 .unwrap()
1463 .as_secs();
1464
1465 let tx_no_valid_before = create_aa_transaction(None, None);
1467 let validator = setup_validator(&tx_no_valid_before, current_time);
1468 let outcome = validator
1469 .validate_transaction(TransactionOrigin::External, tx_no_valid_before)
1470 .await;
1471
1472 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1473 assert!(!matches!(
1474 err.downcast_other_ref::<TempoPoolTransactionError>(),
1475 Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1476 ));
1477 }
1478
1479 let tx_too_close =
1481 create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS));
1482 let validator = setup_validator(&tx_too_close, current_time);
1483 let outcome = validator
1484 .validate_transaction(TransactionOrigin::External, tx_too_close)
1485 .await;
1486
1487 match outcome {
1488 TransactionValidationOutcome::Invalid(_, ref err) => {
1489 assert!(matches!(
1490 err.downcast_other_ref::<TempoPoolTransactionError>(),
1491 Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1492 ));
1493 }
1494 _ => panic!("Expected Invalid outcome with InvalidValidBefore error, got: {outcome:?}"),
1495 }
1496
1497 let tx_valid =
1499 create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS + 1));
1500 let validator = setup_validator(&tx_valid, current_time);
1501 let outcome = validator
1502 .validate_transaction(TransactionOrigin::External, tx_valid)
1503 .await;
1504
1505 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1506 assert!(!matches!(
1507 err.downcast_other_ref::<TempoPoolTransactionError>(),
1508 Some(TempoPoolTransactionError::InvalidValidBefore { .. })
1509 ));
1510 }
1511 }
1512
1513 #[tokio::test]
1514 async fn test_aa_valid_after_check() {
1515 let current_time = std::time::SystemTime::now()
1517 .duration_since(std::time::UNIX_EPOCH)
1518 .unwrap()
1519 .as_secs();
1520
1521 let tx_no_valid_after = create_aa_transaction(None, None);
1523 let validator = setup_validator(&tx_no_valid_after, current_time);
1524 let outcome = validator
1525 .validate_transaction(TransactionOrigin::External, tx_no_valid_after)
1526 .await;
1527
1528 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1529 assert!(!matches!(
1530 err.downcast_other_ref::<TempoPoolTransactionError>(),
1531 Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1532 ));
1533 }
1534
1535 let tx_within_limit = create_aa_transaction(Some(current_time + 60), None);
1537 let validator = setup_validator(&tx_within_limit, current_time);
1538 let outcome = validator
1539 .validate_transaction(TransactionOrigin::External, tx_within_limit)
1540 .await;
1541
1542 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1543 assert!(!matches!(
1544 err.downcast_other_ref::<TempoPoolTransactionError>(),
1545 Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1546 ));
1547 }
1548
1549 let tx_too_far = create_aa_transaction(Some(current_time + 300), None);
1551 let validator = setup_validator(&tx_too_far, current_time);
1552 let outcome = validator
1553 .validate_transaction(TransactionOrigin::External, tx_too_far)
1554 .await;
1555
1556 match outcome {
1557 TransactionValidationOutcome::Invalid(_, ref err) => {
1558 assert!(matches!(
1559 err.downcast_other_ref::<TempoPoolTransactionError>(),
1560 Some(TempoPoolTransactionError::InvalidValidAfter { .. })
1561 ));
1562 }
1563 _ => panic!("Expected Invalid outcome with InvalidValidAfter error, got: {outcome:?}"),
1564 }
1565 }
1566
1567 #[tokio::test]
1568 async fn test_blacklisted_fee_payer_rejected() {
1569 let fee_token = address!("20C0000000000000000000000000000000000001");
1571 let policy_id: u64 = 2;
1572
1573 let transaction = TxBuilder::aa(Address::random())
1574 .fee_token(fee_token)
1575 .build();
1576 let fee_payer = transaction.sender();
1577
1578 let provider = MockEthProvider::<TempoPrimitives>::new()
1580 .with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
1581 provider.add_block(B256::random(), Block::default());
1582
1583 provider.add_account(
1585 transaction.sender(),
1586 ExtendedAccount::new(transaction.nonce(), U256::ZERO),
1587 );
1588
1589 let usd_currency_value =
1592 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
1593 let transfer_policy_id_packed =
1596 U256::from(policy_id) << tip20_slots::TRANSFER_POLICY_ID_OFFSET;
1597 provider.add_account(
1598 fee_token,
1599 ExtendedAccount::new(0, U256::ZERO).extend_storage([
1600 (
1601 tip20_slots::TRANSFER_POLICY_ID.into(),
1602 transfer_policy_id_packed,
1603 ),
1604 (tip20_slots::CURRENCY.into(), usd_currency_value),
1605 ]),
1606 );
1607
1608 let policy_data = PolicyData {
1610 policy_type: ITIP403Registry::PolicyType::BLACKLIST as u8,
1611 admin: Address::ZERO,
1612 };
1613 let policy_data_slot = TIP403Registry::new().policy_records[policy_id]
1614 .base
1615 .base_slot();
1616 let policy_set_slot = TIP403Registry::new().policy_set[policy_id][fee_payer].slot();
1617
1618 provider.add_account(
1619 TIP403_REGISTRY_ADDRESS,
1620 ExtendedAccount::new(0, U256::ZERO).extend_storage([
1621 (policy_data_slot.into(), policy_data.encode_to_slot()),
1622 (policy_set_slot.into(), U256::from(1)), ]),
1624 );
1625
1626 let inner =
1628 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1629 .disable_balance_check()
1630 .build(InMemoryBlobStore::default());
1631 let validator = TempoTransactionValidator::new(
1632 inner,
1633 DEFAULT_AA_VALID_AFTER_MAX_SECS,
1634 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1635 AmmLiquidityCache::new(provider).unwrap(),
1636 );
1637
1638 let outcome = validator
1639 .validate_transaction(TransactionOrigin::External, transaction)
1640 .await;
1641
1642 match outcome {
1644 TransactionValidationOutcome::Invalid(_, ref err) => {
1645 assert!(matches!(
1646 err.downcast_other_ref::<TempoPoolTransactionError>(),
1647 Some(TempoPoolTransactionError::BlackListedFeePayer { .. })
1648 ));
1649 }
1650 _ => {
1651 panic!("Expected Invalid outcome with BlackListedFeePayer error, got: {outcome:?}")
1652 }
1653 }
1654 }
1655
1656 #[tokio::test]
1659 async fn test_aa_intrinsic_gas_validation() {
1660 use alloy_primitives::{Signature, TxKind, address};
1661 use tempo_primitives::transaction::{
1662 TempoTransaction,
1663 tempo_transaction::Call,
1664 tt_signature::{PrimitiveSignature, TempoSignature},
1665 tt_signed::AASigned,
1666 };
1667
1668 let current_time = std::time::SystemTime::now()
1669 .duration_since(std::time::UNIX_EPOCH)
1670 .unwrap()
1671 .as_secs();
1672
1673 let create_aa_tx = |gas_limit: u64| {
1675 let calls: Vec<Call> = (0..10)
1676 .map(|i| Call {
1677 to: TxKind::Call(Address::from([i as u8; 20])),
1678 value: U256::ZERO,
1679 input: alloy_primitives::Bytes::from(vec![0x00; 100]),
1680 })
1681 .collect();
1682
1683 let tx = TempoTransaction {
1684 chain_id: 1,
1685 max_priority_fee_per_gas: 1_000_000_000,
1686 max_fee_per_gas: 20_000_000_000, gas_limit,
1688 calls,
1689 nonce_key: U256::ZERO,
1690 nonce: 0,
1691 fee_token: Some(address!("0000000000000000000000000000000000000002")),
1692 ..Default::default()
1693 };
1694
1695 let signed = AASigned::new_unhashed(
1696 tx,
1697 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1698 Signature::test_signature(),
1699 )),
1700 );
1701 TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1702 };
1703
1704 let tx_low_gas = create_aa_tx(30_000);
1707 let validator = setup_validator(&tx_low_gas, current_time);
1708 let outcome = validator
1709 .validate_transaction(TransactionOrigin::External, tx_low_gas)
1710 .await;
1711
1712 match outcome {
1713 TransactionValidationOutcome::Invalid(_, ref err) => {
1714 assert!(matches!(
1715 err.downcast_other_ref::<TempoPoolTransactionError>(),
1716 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1717 ));
1718 }
1719 _ => panic!(
1720 "Expected Invalid outcome with InsufficientGasForAAIntrinsicCost, got: {outcome:?}"
1721 ),
1722 }
1723
1724 let tx_high_gas = create_aa_tx(1_000_000);
1726 let validator = setup_validator(&tx_high_gas, current_time);
1727 let outcome = validator
1728 .validate_transaction(TransactionOrigin::External, tx_high_gas)
1729 .await;
1730
1731 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1732 assert!(!matches!(
1733 err.downcast_other_ref::<TempoPoolTransactionError>(),
1734 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1735 ));
1736 }
1737 }
1738
1739 #[tokio::test]
1747 async fn test_aa_create_tx_with_2d_nonce_intrinsic_gas() {
1748 use alloy_primitives::Signature;
1749 use tempo_primitives::transaction::{
1750 TempoTransaction,
1751 tempo_transaction::Call as TxCall,
1752 tt_signature::{PrimitiveSignature, TempoSignature},
1753 tt_signed::AASigned,
1754 };
1755
1756 let current_time = std::time::SystemTime::now()
1757 .duration_since(std::time::UNIX_EPOCH)
1758 .unwrap()
1759 .as_secs();
1760
1761 let create_aa_tx = |gas_limit: u64, nonce_key: U256, is_create: bool| {
1763 let calls: Vec<TxCall> = if is_create {
1764 vec![TxCall {
1765 to: TxKind::Create,
1766 value: U256::ZERO,
1767 input: alloy_primitives::Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xF3]),
1768 }]
1769 } else {
1770 (0..10)
1771 .map(|i| TxCall {
1772 to: TxKind::Call(Address::from([i as u8; 20])),
1773 value: U256::ZERO,
1774 input: alloy_primitives::Bytes::from(vec![0x00; 100]),
1775 })
1776 .collect()
1777 };
1778
1779 let valid_before = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
1780 Some(current_time + TEST_VALIDITY_WINDOW)
1781 } else {
1782 None
1783 };
1784
1785 let tx = TempoTransaction {
1786 chain_id: 1,
1787 max_priority_fee_per_gas: 1_000_000_000,
1788 max_fee_per_gas: 20_000_000_000,
1789 gas_limit,
1790 calls,
1791 nonce_key,
1792 nonce: 0,
1793 valid_before,
1794 fee_token: Some(address!("0000000000000000000000000000000000000002")),
1795 ..Default::default()
1796 };
1797
1798 let signed = AASigned::new_unhashed(
1799 tx,
1800 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1801 Signature::test_signature(),
1802 )),
1803 );
1804 TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1805 };
1806
1807 let tx_1d_low_gas = create_aa_tx(30_000, U256::ZERO, false);
1809 let validator1 = setup_validator(&tx_1d_low_gas, current_time);
1810 let outcome1 = validator1
1811 .validate_transaction(TransactionOrigin::External, tx_1d_low_gas)
1812 .await;
1813
1814 match outcome1 {
1815 TransactionValidationOutcome::Invalid(_, ref err) => {
1816 assert!(
1817 matches!(
1818 err.downcast_other_ref::<TempoPoolTransactionError>(),
1819 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1820 ),
1821 "1D nonce with low gas should fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1822 );
1823 }
1824 _ => panic!("Expected Invalid outcome, got: {outcome1:?}"),
1825 }
1826
1827 let tx_2d_low_gas = create_aa_tx(30_000, TEMPO_EXPIRING_NONCE_KEY, false);
1830 let validator2 = setup_validator(&tx_2d_low_gas, current_time);
1831 let outcome2 = validator2
1832 .validate_transaction(TransactionOrigin::External, tx_2d_low_gas)
1833 .await;
1834
1835 match outcome2 {
1836 TransactionValidationOutcome::Invalid(_, ref err) => {
1837 assert!(
1838 matches!(
1839 err.downcast_other_ref::<TempoPoolTransactionError>(),
1840 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1841 ),
1842 "2D nonce with low gas should fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1843 );
1844 }
1845 _ => panic!("Expected Invalid outcome, got: {outcome2:?}"),
1846 }
1847
1848 let tx_1d_high_gas = create_aa_tx(1_000_000, U256::ZERO, false);
1850 let validator3 = setup_validator(&tx_1d_high_gas, current_time);
1851 let outcome3 = validator3
1852 .validate_transaction(TransactionOrigin::External, tx_1d_high_gas)
1853 .await;
1854
1855 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome3 {
1857 assert!(
1858 !matches!(
1859 err.downcast_other_ref::<TempoPoolTransactionError>(),
1860 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1861 ),
1862 "1D nonce with high gas should NOT fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1863 );
1864 }
1865
1866 let tx_2d_high_gas = create_aa_tx(1_000_000, TEMPO_EXPIRING_NONCE_KEY, false);
1868 let validator4 = setup_validator(&tx_2d_high_gas, current_time);
1869 let outcome4 = validator4
1870 .validate_transaction(TransactionOrigin::External, tx_2d_high_gas)
1871 .await;
1872
1873 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome4 {
1875 assert!(
1876 !matches!(
1877 err.downcast_other_ref::<TempoPoolTransactionError>(),
1878 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1879 ),
1880 "2D nonce with high gas should NOT fail InsufficientGasForAAIntrinsicCost, got: {err:?}"
1881 );
1882 }
1883 }
1884
1885 #[tokio::test]
1886 async fn test_expiring_nonce_intrinsic_gas_uses_lower_cost() {
1887 use alloy_primitives::{Signature, TxKind, address};
1888 use tempo_primitives::transaction::{
1889 TempoTransaction,
1890 tempo_transaction::Call,
1891 tt_signature::{PrimitiveSignature, TempoSignature},
1892 tt_signed::AASigned,
1893 };
1894
1895 let current_time = std::time::SystemTime::now()
1896 .duration_since(std::time::UNIX_EPOCH)
1897 .unwrap()
1898 .as_secs();
1899
1900 let create_expiring_nonce_tx = |gas_limit: u64| {
1902 let calls: Vec<Call> = vec![Call {
1903 to: TxKind::Call(Address::from([1u8; 20])),
1904 value: U256::ZERO,
1905 input: alloy_primitives::Bytes::from(vec![0xd0, 0x9d, 0xe0, 0x8a]), }];
1907
1908 let tx = TempoTransaction {
1909 chain_id: 1,
1910 max_priority_fee_per_gas: 1_000_000_000,
1911 max_fee_per_gas: 20_000_000_000,
1912 gas_limit,
1913 calls,
1914 nonce_key: TEMPO_EXPIRING_NONCE_KEY, nonce: 0,
1916 valid_before: Some(current_time + 25), fee_token: Some(address!("0000000000000000000000000000000000000002")),
1918 ..Default::default()
1919 };
1920
1921 let signed = AASigned::new_unhashed(
1922 tx,
1923 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1924 Signature::test_signature(),
1925 )),
1926 );
1927 TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
1928 };
1929
1930 let tx = create_expiring_nonce_tx(50_000);
1934 let validator = setup_validator(&tx, current_time);
1935 let outcome = validator
1936 .validate_transaction(TransactionOrigin::External, tx)
1937 .await;
1938
1939 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
1941 let is_intrinsic_gas_error = matches!(
1942 err.downcast_other_ref::<TempoPoolTransactionError>(),
1943 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
1944 ) || matches!(
1945 err.downcast_other_ref::<InvalidPoolTransactionError>(),
1946 Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
1947 );
1948 assert!(
1949 !is_intrinsic_gas_error,
1950 "Expiring nonce tx with 50k gas should NOT fail intrinsic gas check, got: {err:?}"
1951 );
1952 }
1953 }
1954
1955 #[tokio::test]
1961 async fn test_existing_2d_nonce_key_intrinsic_gas() {
1962 use alloy_primitives::{Signature, TxKind, address};
1963 use tempo_primitives::transaction::{
1964 TempoTransaction,
1965 tempo_transaction::Call,
1966 tt_signature::{PrimitiveSignature, TempoSignature},
1967 tt_signed::AASigned,
1968 };
1969
1970 let current_time = std::time::SystemTime::now()
1971 .duration_since(std::time::UNIX_EPOCH)
1972 .unwrap()
1973 .as_secs();
1974
1975 let create_aa_tx = |gas_limit: u64, nonce_key: U256, nonce: u64| {
1977 let calls: Vec<Call> = vec![Call {
1978 to: TxKind::Call(Address::from([1u8; 20])),
1979 value: U256::ZERO,
1980 input: alloy_primitives::Bytes::from(vec![0xd0, 0x9d, 0xe0, 0x8a]), }];
1982
1983 let tx = TempoTransaction {
1984 chain_id: 1,
1985 max_priority_fee_per_gas: 1_000_000_000,
1986 max_fee_per_gas: 20_000_000_000,
1987 gas_limit,
1988 calls,
1989 nonce_key,
1990 nonce,
1991 fee_token: Some(address!("0000000000000000000000000000000000000002")),
1992 ..Default::default()
1993 };
1994
1995 let signed = AASigned::new_unhashed(
1996 tx,
1997 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
1998 Signature::test_signature(),
1999 )),
2000 );
2001 TempoPooledTransaction::new(TempoTxEnvelope::from(signed).try_into_recovered().unwrap())
2002 };
2003
2004 let tx_1d = create_aa_tx(50_000, U256::ZERO, 5);
2007 let validator = setup_validator(&tx_1d, current_time);
2008 let outcome = validator
2009 .validate_transaction(TransactionOrigin::External, tx_1d)
2010 .await;
2011
2012 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2013 let is_gas_error = matches!(
2014 err.downcast_other_ref::<TempoPoolTransactionError>(),
2015 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2016 ) || matches!(
2017 err.downcast_other_ref::<InvalidPoolTransactionError>(),
2018 Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2019 );
2020 assert!(
2021 !is_gas_error,
2022 "1D nonce with nonce>0 and 50k gas should NOT fail intrinsic gas check, got: {err:?}"
2023 );
2024 }
2025
2026 let tx_2d_ok = create_aa_tx(50_000, U256::from(1), 5);
2029 let validator = setup_validator(&tx_2d_ok, current_time);
2030 let outcome = validator
2031 .validate_transaction(TransactionOrigin::External, tx_2d_ok)
2032 .await;
2033
2034 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2035 let is_gas_error = matches!(
2036 err.downcast_other_ref::<TempoPoolTransactionError>(),
2037 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2038 ) || matches!(
2039 err.downcast_other_ref::<InvalidPoolTransactionError>(),
2040 Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2041 );
2042 assert!(
2043 !is_gas_error,
2044 "Existing 2D nonce key with 50k gas should NOT fail intrinsic gas check, got: {err:?}"
2045 );
2046 }
2047
2048 let tx_2d_low = create_aa_tx(22_000, U256::from(1), 5);
2052 let validator = setup_validator(&tx_2d_low, current_time);
2053 let outcome = validator
2054 .validate_transaction(TransactionOrigin::External, tx_2d_low)
2055 .await;
2056
2057 match outcome {
2058 TransactionValidationOutcome::Invalid(_, ref err) => {
2059 let is_gas_error = matches!(
2060 err.downcast_other_ref::<TempoPoolTransactionError>(),
2061 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2062 ) || matches!(
2063 err.downcast_other_ref::<InvalidPoolTransactionError>(),
2064 Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2065 );
2066 assert!(
2067 is_gas_error,
2068 "Existing 2D nonce key with 22k gas should fail intrinsic gas check, got: {err:?}"
2069 );
2070 }
2071 _ => panic!(
2072 "Expected Invalid outcome for existing 2D nonce with insufficient gas, got: {outcome:?}"
2073 ),
2074 }
2075
2076 let tx_1d_low = create_aa_tx(22_000, U256::ZERO, 5);
2079 let validator = setup_validator(&tx_1d_low, current_time);
2080 let outcome = validator
2081 .validate_transaction(TransactionOrigin::External, tx_1d_low)
2082 .await;
2083
2084 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2085 let is_gas_error = matches!(
2086 err.downcast_other_ref::<TempoPoolTransactionError>(),
2087 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
2088 ) || matches!(
2089 err.downcast_other_ref::<InvalidPoolTransactionError>(),
2090 Some(InvalidPoolTransactionError::IntrinsicGasTooLow)
2091 );
2092 assert!(
2093 !is_gas_error,
2094 "1D nonce with nonce>0 and 22k gas should NOT fail intrinsic gas check, got: {err:?}"
2095 );
2096 }
2097 }
2098
2099 #[tokio::test]
2100 async fn test_non_zero_value_in_eip1559_rejected() {
2101 let transaction = TxBuilder::eip1559(Address::random())
2102 .value(U256::from(1))
2103 .build_eip1559();
2104
2105 let current_time = std::time::SystemTime::now()
2106 .duration_since(std::time::UNIX_EPOCH)
2107 .unwrap()
2108 .as_secs();
2109 let validator = setup_validator(&transaction, current_time);
2110
2111 let outcome = validator
2112 .validate_transaction(TransactionOrigin::External, transaction)
2113 .await;
2114
2115 match outcome {
2116 TransactionValidationOutcome::Invalid(_, ref err) => {
2117 assert!(matches!(
2118 err.downcast_other_ref::<TempoPoolTransactionError>(),
2119 Some(TempoPoolTransactionError::NonZeroValue)
2120 ));
2121 }
2122 _ => panic!("Expected Invalid outcome with NonZeroValue error, got: {outcome:?}"),
2123 }
2124 }
2125
2126 #[tokio::test]
2127 async fn test_zero_value_passes_value_check() {
2128 let transaction = TxBuilder::eip1559(Address::random()).build_eip1559();
2130 assert!(transaction.value().is_zero(), "Test expects zero-value tx");
2131
2132 let current_time = std::time::SystemTime::now()
2133 .duration_since(std::time::UNIX_EPOCH)
2134 .unwrap()
2135 .as_secs();
2136 let validator = setup_validator(&transaction, current_time);
2137
2138 let outcome = validator
2139 .validate_transaction(TransactionOrigin::External, transaction)
2140 .await;
2141
2142 assert!(
2143 matches!(outcome, TransactionValidationOutcome::Valid { .. }),
2144 "Zero-value tx should pass validation, got: {outcome:?}"
2145 );
2146 }
2147
2148 #[tokio::test]
2149 async fn test_invalid_fee_token_rejected() {
2150 let invalid_fee_token = address!("1234567890123456789012345678901234567890");
2151
2152 let transaction = TxBuilder::aa(Address::random())
2153 .fee_token(invalid_fee_token)
2154 .build();
2155
2156 let current_time = std::time::SystemTime::now()
2157 .duration_since(std::time::UNIX_EPOCH)
2158 .unwrap()
2159 .as_secs();
2160 let validator = setup_validator(&transaction, current_time);
2161
2162 let outcome = validator
2163 .validate_transaction(TransactionOrigin::External, transaction)
2164 .await;
2165
2166 match outcome {
2167 TransactionValidationOutcome::Invalid(_, ref err) => {
2168 assert!(matches!(
2169 err.downcast_other_ref::<TempoPoolTransactionError>(),
2170 Some(TempoPoolTransactionError::InvalidFeeToken(_))
2171 ));
2172 }
2173 _ => panic!("Expected Invalid outcome with InvalidFeeToken error, got: {outcome:?}"),
2174 }
2175 }
2176
2177 #[tokio::test]
2178 async fn test_aa_valid_after_and_valid_before_both_valid() {
2179 let current_time = std::time::SystemTime::now()
2180 .duration_since(std::time::UNIX_EPOCH)
2181 .unwrap()
2182 .as_secs();
2183
2184 let valid_after = current_time + 60;
2185 let valid_before = current_time + 3600;
2186
2187 let transaction = create_aa_transaction(Some(valid_after), Some(valid_before));
2188 let validator = setup_validator(&transaction, current_time);
2189
2190 let outcome = validator
2191 .validate_transaction(TransactionOrigin::External, transaction)
2192 .await;
2193
2194 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2195 let tempo_err = err.downcast_other_ref::<TempoPoolTransactionError>();
2196 assert!(
2197 !matches!(
2198 tempo_err,
2199 Some(TempoPoolTransactionError::InvalidValidAfter { .. })
2200 | Some(TempoPoolTransactionError::InvalidValidBefore { .. })
2201 ),
2202 "Should not fail with validity window errors"
2203 );
2204 }
2205 }
2206
2207 #[tokio::test]
2208 async fn test_fee_cap_below_min_base_fee_rejected() {
2209 let current_time = std::time::SystemTime::now()
2210 .duration_since(std::time::UNIX_EPOCH)
2211 .unwrap()
2212 .as_secs();
2213
2214 let transaction = TxBuilder::aa(Address::random())
2217 .max_fee(1_000_000_000) .max_priority_fee(1_000_000_000)
2219 .build();
2220
2221 let validator = setup_validator(&transaction, current_time);
2222
2223 let outcome = validator
2224 .validate_transaction(TransactionOrigin::External, transaction)
2225 .await;
2226
2227 match outcome {
2228 TransactionValidationOutcome::Invalid(_, ref err) => {
2229 assert!(
2230 matches!(
2231 err.downcast_other_ref::<TempoPoolTransactionError>(),
2232 Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2233 ),
2234 "Expected FeeCapBelowMinBaseFee error, got: {err:?}"
2235 );
2236 }
2237 _ => panic!(
2238 "Expected Invalid outcome with FeeCapBelowMinBaseFee error, got: {outcome:?}"
2239 ),
2240 }
2241 }
2242
2243 #[tokio::test]
2244 async fn test_fee_cap_at_min_base_fee_passes() {
2245 let current_time = std::time::SystemTime::now()
2246 .duration_since(std::time::UNIX_EPOCH)
2247 .unwrap()
2248 .as_secs();
2249
2250 let active_fork = MODERATO.tempo_hardfork_at(current_time);
2252 let transaction = TxBuilder::aa(Address::random())
2253 .max_fee(active_fork.base_fee() as u128)
2254 .max_priority_fee(1_000_000_000)
2255 .build();
2256
2257 let validator = setup_validator(&transaction, current_time);
2258
2259 let outcome = validator
2260 .validate_transaction(TransactionOrigin::External, transaction)
2261 .await;
2262
2263 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2265 assert!(
2266 !matches!(
2267 err.downcast_other_ref::<TempoPoolTransactionError>(),
2268 Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2269 ),
2270 "Should not fail with FeeCapBelowMinBaseFee when fee cap equals min base fee"
2271 );
2272 }
2273 }
2274
2275 #[tokio::test]
2276 async fn test_fee_cap_above_min_base_fee_passes() {
2277 let current_time = std::time::SystemTime::now()
2278 .duration_since(std::time::UNIX_EPOCH)
2279 .unwrap()
2280 .as_secs();
2281
2282 let transaction = TxBuilder::aa(Address::random())
2285 .max_fee(20_000_000_000) .max_priority_fee(1_000_000_000)
2287 .build();
2288
2289 let validator = setup_validator(&transaction, current_time);
2290
2291 let outcome = validator
2292 .validate_transaction(TransactionOrigin::External, transaction)
2293 .await;
2294
2295 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
2297 assert!(
2298 !matches!(
2299 err.downcast_other_ref::<TempoPoolTransactionError>(),
2300 Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2301 ),
2302 "Should not fail with FeeCapBelowMinBaseFee when fee cap is above min base fee"
2303 );
2304 }
2305 }
2306
2307 #[tokio::test]
2308 async fn test_eip1559_fee_cap_below_min_base_fee_rejected() {
2309 let current_time = std::time::SystemTime::now()
2310 .duration_since(std::time::UNIX_EPOCH)
2311 .unwrap()
2312 .as_secs();
2313
2314 let transaction = TxBuilder::eip1559(Address::random())
2316 .max_fee(1_000_000_000) .max_priority_fee(1_000_000_000)
2318 .build_eip1559();
2319
2320 let validator = setup_validator(&transaction, current_time);
2321
2322 let outcome = validator
2323 .validate_transaction(TransactionOrigin::External, transaction)
2324 .await;
2325
2326 match outcome {
2327 TransactionValidationOutcome::Invalid(_, ref err) => {
2328 assert!(
2329 matches!(
2330 err.downcast_other_ref::<TempoPoolTransactionError>(),
2331 Some(TempoPoolTransactionError::FeeCapBelowMinBaseFee { .. })
2332 ),
2333 "Expected FeeCapBelowMinBaseFee error for EIP-1559 tx, got: {err:?}"
2334 );
2335 }
2336 _ => panic!(
2337 "Expected Invalid outcome with FeeCapBelowMinBaseFee error, got: {outcome:?}"
2338 ),
2339 }
2340 }
2341
2342 mod keychain_validation {
2343 use super::*;
2344 use alloy_primitives::{Signature, TxKind, address};
2345 use alloy_signer::SignerSync;
2346 use alloy_signer_local::PrivateKeySigner;
2347 use reth_chainspec::ForkCondition;
2348 use reth_transaction_pool::error::PoolTransactionError;
2349 use tempo_chainspec::hardfork::TempoHardfork;
2350 use tempo_primitives::transaction::{
2351 KeyAuthorization, SignatureType, SignedKeyAuthorization, TempoTransaction, TokenLimit,
2352 tempo_transaction::Call,
2353 tt_signature::{
2354 KeychainSignature, KeychainVersion, PrimitiveSignature, TempoSignature,
2355 },
2356 tt_signed::AASigned,
2357 };
2358
2359 fn moderato_with_t1c() -> TempoChainSpec {
2361 let mut spec = Arc::unwrap_or_clone(MODERATO.clone());
2362 spec.inner
2363 .hardforks
2364 .extend([(TempoHardfork::T1C, ForkCondition::Timestamp(0))]);
2365 spec
2366 }
2367
2368 fn generate_keypair() -> (PrivateKeySigner, Address) {
2370 let signer = PrivateKeySigner::random();
2371 let address = signer.address();
2372 (signer, address)
2373 }
2374
2375 fn create_aa_with_keychain_signature(
2377 user_address: Address,
2378 access_key_signer: &PrivateKeySigner,
2379 key_authorization: Option<SignedKeyAuthorization>,
2380 ) -> TempoPooledTransaction {
2381 create_aa_with_keychain_signature_versioned(
2382 user_address,
2383 access_key_signer,
2384 key_authorization,
2385 KeychainVersion::V2,
2386 )
2387 }
2388
2389 fn create_aa_with_v1_keychain_signature(
2391 user_address: Address,
2392 access_key_signer: &PrivateKeySigner,
2393 key_authorization: Option<SignedKeyAuthorization>,
2394 ) -> TempoPooledTransaction {
2395 create_aa_with_keychain_signature_versioned(
2396 user_address,
2397 access_key_signer,
2398 key_authorization,
2399 KeychainVersion::V1,
2400 )
2401 }
2402
2403 fn create_aa_with_keychain_signature_versioned(
2405 user_address: Address,
2406 access_key_signer: &PrivateKeySigner,
2407 key_authorization: Option<SignedKeyAuthorization>,
2408 version: KeychainVersion,
2409 ) -> TempoPooledTransaction {
2410 let tx_aa = TempoTransaction {
2411 chain_id: 42431, max_priority_fee_per_gas: 1_000_000_000,
2413 max_fee_per_gas: 20_000_000_000,
2414 gas_limit: 1_000_000,
2415 calls: vec![Call {
2416 to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
2417 value: U256::ZERO,
2418 input: alloy_primitives::Bytes::new(),
2419 }],
2420 nonce_key: U256::ZERO,
2421 nonce: 0,
2422 fee_token: Some(address!("0000000000000000000000000000000000000002")),
2423 fee_payer_signature: None,
2424 valid_after: None,
2425 valid_before: None,
2426 access_list: Default::default(),
2427 tempo_authorization_list: vec![],
2428 key_authorization,
2429 };
2430
2431 let unsigned = AASigned::new_unhashed(
2433 tx_aa.clone(),
2434 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
2435 Signature::test_signature(),
2436 )),
2437 );
2438 let sig_hash = unsigned.signature_hash();
2439
2440 let keychain_sig = match version {
2441 KeychainVersion::V1 => {
2442 let signature = access_key_signer
2443 .sign_hash_sync(&sig_hash)
2444 .expect("signing failed");
2445 TempoSignature::Keychain(KeychainSignature::new_v1(
2446 user_address,
2447 PrimitiveSignature::Secp256k1(signature),
2448 ))
2449 }
2450 KeychainVersion::V2 => {
2451 let sig_hash = KeychainSignature::signing_hash(sig_hash, user_address);
2452 let signature = access_key_signer
2453 .sign_hash_sync(&sig_hash)
2454 .expect("signing failed");
2455 TempoSignature::Keychain(KeychainSignature::new(
2456 user_address,
2457 PrimitiveSignature::Secp256k1(signature),
2458 ))
2459 }
2460 };
2461
2462 let signed_tx = AASigned::new_unhashed(tx_aa, keychain_sig);
2463 let envelope: TempoTxEnvelope = signed_tx.into();
2464 let recovered = envelope.try_into_recovered().unwrap();
2465 TempoPooledTransaction::new(recovered)
2466 }
2467
2468 fn validate_against_keychain_default_fee_context(
2469 validator: &TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>>,
2470 transaction: &TempoPooledTransaction,
2471 state_provider: &mut impl StateProvider,
2472 ) -> Result<Result<(), TempoPoolTransactionError>, ProviderError> {
2473 validator.validate_against_keychain(
2474 transaction,
2475 state_provider,
2476 transaction.sender(),
2477 transaction
2478 .inner()
2479 .fee_token()
2480 .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN),
2481 )
2482 }
2483
2484 fn setup_validator_with_keychain_storage(
2486 transaction: &TempoPooledTransaction,
2487 user_address: Address,
2488 key_id: Address,
2489 authorized_key_slot_value: Option<U256>,
2490 ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
2491 setup_validator_with_keychain_storage_spec(
2492 transaction,
2493 user_address,
2494 key_id,
2495 authorized_key_slot_value,
2496 moderato_with_t1c(),
2497 )
2498 }
2499
2500 fn setup_validator_with_keychain_storage_spec(
2501 transaction: &TempoPooledTransaction,
2502 user_address: Address,
2503 key_id: Address,
2504 authorized_key_slot_value: Option<U256>,
2505 chain_spec: TempoChainSpec,
2506 ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
2507 let provider = MockEthProvider::<TempoPrimitives>::new().with_chain_spec(chain_spec);
2508
2509 provider.add_account(
2511 transaction.sender(),
2512 ExtendedAccount::new(transaction.nonce(), U256::ZERO),
2513 );
2514 provider.add_block(B256::random(), Default::default());
2515
2516 if let Some(slot_value) = authorized_key_slot_value {
2518 let storage_slot = AccountKeychain::new().keys[user_address][key_id].base_slot();
2519 provider.add_account(
2520 ACCOUNT_KEYCHAIN_ADDRESS,
2521 ExtendedAccount::new(0, U256::ZERO)
2522 .extend_storage([(storage_slot.into(), slot_value)]),
2523 );
2524 }
2525
2526 let inner =
2527 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
2528 .disable_balance_check()
2529 .build(InMemoryBlobStore::default());
2530 let amm_cache =
2531 AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
2532 TempoTransactionValidator::new(
2533 inner,
2534 DEFAULT_AA_VALID_AFTER_MAX_SECS,
2535 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
2536 amm_cache,
2537 )
2538 }
2539
2540 #[test]
2541 fn test_non_aa_transaction_skips_keychain_validation() -> Result<(), ProviderError> {
2542 let transaction = TxBuilder::eip1559(Address::random()).build_eip1559();
2544 let validator = setup_validator(&transaction, 0);
2545 let mut state_provider = validator.inner.client().latest().unwrap();
2546
2547 let result = validate_against_keychain_default_fee_context(
2548 &validator,
2549 &transaction,
2550 &mut state_provider,
2551 )?;
2552 assert!(result.is_ok(), "Non-AA tx should skip keychain validation");
2553 Ok(())
2554 }
2555
2556 #[test]
2557 fn test_aa_with_primitive_signature_skips_keychain_validation() -> Result<(), ProviderError>
2558 {
2559 let transaction = create_aa_transaction(None, None);
2561 let validator = setup_validator(&transaction, 0);
2562 let mut state_provider = validator.inner.client().latest().unwrap();
2563
2564 let result = validate_against_keychain_default_fee_context(
2565 &validator,
2566 &transaction,
2567 &mut state_provider,
2568 )?;
2569 assert!(
2570 result.is_ok(),
2571 "AA tx with primitive signature should skip keychain validation"
2572 );
2573 Ok(())
2574 }
2575
2576 #[test]
2577 fn test_keychain_signature_with_valid_authorized_key() -> Result<(), ProviderError> {
2578 let (access_key_signer, access_key_address) = generate_keypair();
2579 let user_address = Address::random();
2580
2581 let transaction =
2582 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2583
2584 let slot_value = AuthorizedKey {
2586 signature_type: 0, expiry: u64::MAX, enforce_limits: false,
2589 is_revoked: false,
2590 }
2591 .encode_to_slot();
2592
2593 let validator = setup_validator_with_keychain_storage(
2594 &transaction,
2595 user_address,
2596 access_key_address,
2597 Some(slot_value),
2598 );
2599 let mut state_provider = validator.inner.client().latest().unwrap();
2600
2601 let result = validate_against_keychain_default_fee_context(
2602 &validator,
2603 &transaction,
2604 &mut state_provider,
2605 )?;
2606 assert!(
2607 result.is_ok(),
2608 "Valid authorized key should pass validation, got: {result:?}"
2609 );
2610 Ok(())
2611 }
2612
2613 #[test]
2614 fn test_keychain_signature_with_revoked_key_rejected() {
2615 let (access_key_signer, access_key_address) = generate_keypair();
2616 let user_address = Address::random();
2617
2618 let transaction =
2619 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2620
2621 let slot_value = AuthorizedKey {
2623 signature_type: 0,
2624 expiry: 0, enforce_limits: false,
2626 is_revoked: true,
2627 }
2628 .encode_to_slot();
2629
2630 let validator = setup_validator_with_keychain_storage(
2631 &transaction,
2632 user_address,
2633 access_key_address,
2634 Some(slot_value),
2635 );
2636 let mut state_provider = validator.inner.client().latest().unwrap();
2637
2638 let result = validate_against_keychain_default_fee_context(
2639 &validator,
2640 &transaction,
2641 &mut state_provider,
2642 );
2643 assert!(
2644 matches!(
2645 result.expect("should not be a provider error"),
2646 Err(TempoPoolTransactionError::Keychain(
2647 "access key has been revoked"
2648 ))
2649 ),
2650 "Revoked key should be rejected"
2651 );
2652 }
2653
2654 #[test]
2655 fn test_keychain_signature_with_nonexistent_key_rejected() {
2656 let (access_key_signer, access_key_address) = generate_keypair();
2657 let user_address = Address::random();
2658
2659 let transaction =
2660 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2661
2662 let slot_value = AuthorizedKey {
2664 signature_type: 0,
2665 expiry: 0, enforce_limits: false,
2667 is_revoked: false,
2668 }
2669 .encode_to_slot();
2670
2671 let validator = setup_validator_with_keychain_storage(
2672 &transaction,
2673 user_address,
2674 access_key_address,
2675 Some(slot_value),
2676 );
2677 let mut state_provider = validator.inner.client().latest().unwrap();
2678
2679 let result = validate_against_keychain_default_fee_context(
2680 &validator,
2681 &transaction,
2682 &mut state_provider,
2683 );
2684 assert!(
2685 matches!(
2686 result.expect("should not be a provider error"),
2687 Err(TempoPoolTransactionError::Keychain(
2688 "access key does not exist"
2689 ))
2690 ),
2691 "Non-existent key should be rejected"
2692 );
2693 }
2694
2695 #[test]
2696 fn test_keychain_signature_with_no_storage_rejected() {
2697 let (access_key_signer, _) = generate_keypair();
2698 let user_address = Address::random();
2699
2700 let transaction =
2701 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
2702
2703 let validator = setup_validator_with_keychain_storage(
2705 &transaction,
2706 user_address,
2707 Address::ZERO,
2708 None,
2709 );
2710 let mut state_provider = validator.inner.client().latest().unwrap();
2711
2712 let result = validate_against_keychain_default_fee_context(
2713 &validator,
2714 &transaction,
2715 &mut state_provider,
2716 );
2717 assert!(
2718 matches!(
2719 result.expect("should not be a provider error"),
2720 Err(TempoPoolTransactionError::Keychain(
2721 "access key does not exist"
2722 ))
2723 ),
2724 "Missing storage should result in non-existent key error"
2725 );
2726 }
2727
2728 #[test]
2729 fn test_key_authorization_without_existing_key_passes() -> Result<(), ProviderError> {
2730 let (access_key_signer, access_key_address) = generate_keypair();
2731 let (user_signer, user_address) = generate_keypair();
2732
2733 let key_auth = KeyAuthorization {
2735 chain_id: 42431, key_type: SignatureType::Secp256k1,
2737 key_id: access_key_address,
2738 expiry: None, limits: None, };
2741
2742 let auth_sig_hash = key_auth.signature_hash();
2743 let auth_signature = user_signer
2744 .sign_hash_sync(&auth_sig_hash)
2745 .expect("signing failed");
2746 let signed_key_auth =
2747 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2748
2749 let transaction = create_aa_with_keychain_signature(
2750 user_address,
2751 &access_key_signer,
2752 Some(signed_key_auth),
2753 );
2754
2755 let validator = setup_validator_with_keychain_storage(
2757 &transaction,
2758 user_address,
2759 access_key_address,
2760 None,
2761 );
2762 let mut state_provider = validator.inner.client().latest().unwrap();
2763
2764 let result = validate_against_keychain_default_fee_context(
2765 &validator,
2766 &transaction,
2767 &mut state_provider,
2768 )?;
2769 assert!(
2770 result.is_ok(),
2771 "Valid KeyAuthorization should pass when key does not exist, got: {result:?}"
2772 );
2773 Ok(())
2774 }
2775
2776 #[test]
2777 fn test_key_authorization_with_existing_key_rejected() {
2778 let (access_key_signer, access_key_address) = generate_keypair();
2779 let (user_signer, user_address) = generate_keypair();
2780
2781 let key_auth = KeyAuthorization {
2782 chain_id: 42431,
2783 key_type: SignatureType::Secp256k1,
2784 key_id: access_key_address,
2785 expiry: None,
2786 limits: None,
2787 };
2788
2789 let auth_sig_hash = key_auth.signature_hash();
2790 let auth_signature = user_signer
2791 .sign_hash_sync(&auth_sig_hash)
2792 .expect("signing failed");
2793 let signed_key_auth =
2794 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2795
2796 let transaction = create_aa_with_keychain_signature(
2797 user_address,
2798 &access_key_signer,
2799 Some(signed_key_auth),
2800 );
2801
2802 let existing_key_slot = AuthorizedKey {
2803 signature_type: 0,
2804 expiry: u64::MAX,
2805 enforce_limits: false,
2806 is_revoked: false,
2807 }
2808 .encode_to_slot();
2809
2810 let validator = setup_validator_with_keychain_storage(
2811 &transaction,
2812 user_address,
2813 access_key_address,
2814 Some(existing_key_slot),
2815 );
2816 let mut state_provider = validator.inner.client().latest().unwrap();
2817
2818 let result = validate_against_keychain_default_fee_context(
2819 &validator,
2820 &transaction,
2821 &mut state_provider,
2822 );
2823 assert!(
2824 matches!(
2825 result.expect("should not be a provider error"),
2826 Err(TempoPoolTransactionError::Keychain(
2827 "access key already exists"
2828 ))
2829 ),
2830 "KeyAuthorization should be rejected when key already exists"
2831 );
2832 }
2833
2834 #[test]
2835 fn test_key_authorization_spending_limit_exceeded_rejected() {
2836 let (access_key_signer, access_key_address) = generate_keypair();
2837 let (user_signer, user_address) = generate_keypair();
2838 let fee_token = address!("0000000000000000000000000000000000000002");
2839
2840 let key_auth = KeyAuthorization {
2841 chain_id: 42431,
2842 key_type: SignatureType::Secp256k1,
2843 key_id: access_key_address,
2844 expiry: None,
2845 limits: Some(vec![TokenLimit {
2846 token: fee_token,
2847 limit: U256::ZERO,
2848 }]),
2849 };
2850
2851 let auth_sig_hash = key_auth.signature_hash();
2852 let auth_signature = user_signer
2853 .sign_hash_sync(&auth_sig_hash)
2854 .expect("signing failed");
2855 let signed_key_auth =
2856 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2857
2858 let transaction = create_aa_with_keychain_signature(
2859 user_address,
2860 &access_key_signer,
2861 Some(signed_key_auth),
2862 );
2863
2864 let validator = setup_validator_with_keychain_storage(
2865 &transaction,
2866 user_address,
2867 access_key_address,
2868 None,
2869 );
2870 let mut state_provider = validator.inner.client().latest().unwrap();
2871
2872 let result = validate_against_keychain_default_fee_context(
2873 &validator,
2874 &transaction,
2875 &mut state_provider,
2876 );
2877 assert!(
2878 matches!(
2879 result.expect("should not be a provider error"),
2880 Err(TempoPoolTransactionError::SpendingLimitExceeded {
2881 fee_token: rejected_fee_token,
2882 remaining,
2883 ..
2884 }) if rejected_fee_token == fee_token && remaining == U256::ZERO
2885 ),
2886 "KeyAuthorization with insufficient fee-token limit should be rejected"
2887 );
2888 }
2889
2890 #[test]
2891 fn test_key_authorization_empty_limits_rejected() {
2892 let (access_key_signer, access_key_address) = generate_keypair();
2893 let (user_signer, user_address) = generate_keypair();
2894 let fee_token = address!("0000000000000000000000000000000000000002");
2895
2896 let key_auth = KeyAuthorization {
2897 chain_id: 42431,
2898 key_type: SignatureType::Secp256k1,
2899 key_id: access_key_address,
2900 expiry: None,
2901 limits: Some(vec![]),
2902 };
2903
2904 let auth_sig_hash = key_auth.signature_hash();
2905 let auth_signature = user_signer
2906 .sign_hash_sync(&auth_sig_hash)
2907 .expect("signing failed");
2908 let signed_key_auth =
2909 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2910
2911 let transaction = create_aa_with_keychain_signature(
2912 user_address,
2913 &access_key_signer,
2914 Some(signed_key_auth),
2915 );
2916
2917 let validator = setup_validator_with_keychain_storage(
2918 &transaction,
2919 user_address,
2920 access_key_address,
2921 None,
2922 );
2923 let mut state_provider = validator.inner.client().latest().unwrap();
2924
2925 let result = validate_against_keychain_default_fee_context(
2926 &validator,
2927 &transaction,
2928 &mut state_provider,
2929 );
2930 assert!(
2931 matches!(
2932 result.expect("should not be a provider error"),
2933 Err(TempoPoolTransactionError::SpendingLimitExceeded {
2934 fee_token: rejected_fee_token,
2935 remaining,
2936 ..
2937 }) if rejected_fee_token == fee_token && remaining == U256::ZERO
2938 ),
2939 "KeyAuthorization with empty limits should be rejected"
2940 );
2941 }
2942
2943 #[test]
2944 fn test_key_authorization_fee_token_not_in_limits_rejected() {
2945 let (access_key_signer, access_key_address) = generate_keypair();
2946 let (user_signer, user_address) = generate_keypair();
2947 let fee_token = address!("0000000000000000000000000000000000000002");
2948 let non_fee_token = Address::random();
2949 assert_ne!(non_fee_token, fee_token);
2950
2951 let key_auth = KeyAuthorization {
2952 chain_id: 42431,
2953 key_type: SignatureType::Secp256k1,
2954 key_id: access_key_address,
2955 expiry: None,
2956 limits: Some(vec![TokenLimit {
2957 token: non_fee_token,
2958 limit: U256::MAX,
2959 }]),
2960 };
2961
2962 let auth_sig_hash = key_auth.signature_hash();
2963 let auth_signature = user_signer
2964 .sign_hash_sync(&auth_sig_hash)
2965 .expect("signing failed");
2966 let signed_key_auth =
2967 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
2968
2969 let transaction = create_aa_with_keychain_signature(
2970 user_address,
2971 &access_key_signer,
2972 Some(signed_key_auth),
2973 );
2974
2975 let validator = setup_validator_with_keychain_storage(
2976 &transaction,
2977 user_address,
2978 access_key_address,
2979 None,
2980 );
2981 let mut state_provider = validator.inner.client().latest().unwrap();
2982
2983 let result = validate_against_keychain_default_fee_context(
2984 &validator,
2985 &transaction,
2986 &mut state_provider,
2987 );
2988 assert!(
2989 matches!(
2990 result.expect("should not be a provider error"),
2991 Err(TempoPoolTransactionError::SpendingLimitExceeded {
2992 fee_token: rejected_fee_token,
2993 remaining,
2994 ..
2995 }) if rejected_fee_token == fee_token && remaining == U256::ZERO
2996 ),
2997 "KeyAuthorization should reject when limits omit the fee token"
2998 );
2999 }
3000
3001 #[test]
3002 fn test_key_authorization_duplicate_token_limits_uses_last_value()
3003 -> Result<(), ProviderError> {
3004 let (access_key_signer, access_key_address) = generate_keypair();
3005 let (user_signer, user_address) = generate_keypair();
3006 let fee_token = address!("0000000000000000000000000000000000000002");
3007
3008 let probe_tx =
3009 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3010 let fee_cost = probe_tx.fee_token_cost();
3011
3012 let key_auth = KeyAuthorization {
3014 chain_id: 42431,
3015 key_type: SignatureType::Secp256k1,
3016 key_id: access_key_address,
3017 expiry: None,
3018 limits: Some(vec![
3019 TokenLimit {
3020 token: fee_token,
3021 limit: U256::ZERO,
3022 },
3023 TokenLimit {
3024 token: fee_token,
3025 limit: fee_cost + U256::from(100),
3026 },
3027 ]),
3028 };
3029
3030 let auth_sig_hash = key_auth.signature_hash();
3031 let auth_signature = user_signer
3032 .sign_hash_sync(&auth_sig_hash)
3033 .expect("signing failed");
3034 let signed_key_auth =
3035 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3036
3037 let transaction = create_aa_with_keychain_signature(
3038 user_address,
3039 &access_key_signer,
3040 Some(signed_key_auth),
3041 );
3042
3043 let validator = setup_validator_with_keychain_storage(
3044 &transaction,
3045 user_address,
3046 access_key_address,
3047 None,
3048 );
3049 let mut state_provider = validator.inner.client().latest().unwrap();
3050
3051 let result = validate_against_keychain_default_fee_context(
3052 &validator,
3053 &transaction,
3054 &mut state_provider,
3055 )?;
3056 assert!(
3057 result.is_ok(),
3058 "Inline key authorization should use the last duplicate token limit"
3059 );
3060 Ok(())
3061 }
3062
3063 #[test]
3064 fn test_key_authorization_spending_limit_uses_resolved_fee_token()
3065 -> Result<(), ProviderError> {
3066 let (access_key_signer, access_key_address) = generate_keypair();
3067 let (user_signer, user_address) = generate_keypair();
3068 let resolved_fee_token = Address::random();
3069
3070 let key_auth = KeyAuthorization {
3071 chain_id: 42431,
3072 key_type: SignatureType::Secp256k1,
3073 key_id: access_key_address,
3074 expiry: None,
3075 limits: Some(vec![TokenLimit {
3076 token: resolved_fee_token,
3077 limit: U256::MAX,
3078 }]),
3079 };
3080
3081 let auth_sig_hash = key_auth.signature_hash();
3082 let auth_signature = user_signer
3083 .sign_hash_sync(&auth_sig_hash)
3084 .expect("signing failed");
3085 let signed_key_auth =
3086 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3087
3088 let transaction = create_aa_with_keychain_signature(
3089 user_address,
3090 &access_key_signer,
3091 Some(signed_key_auth),
3092 );
3093
3094 let validator = setup_validator_with_keychain_storage(
3095 &transaction,
3096 user_address,
3097 access_key_address,
3098 None,
3099 );
3100 let mut state_provider = validator.inner.client().latest().unwrap();
3101
3102 let result = validator.validate_against_keychain(
3103 &transaction,
3104 &mut state_provider,
3105 user_address,
3106 resolved_fee_token,
3107 )?;
3108 assert!(
3109 result.is_ok(),
3110 "Inline key authorization should use the resolved fee token"
3111 );
3112 Ok(())
3113 }
3114
3115 #[test]
3116 fn test_key_authorization_spending_limit_skipped_for_sponsored_fee_payer()
3117 -> Result<(), ProviderError> {
3118 let (access_key_signer, access_key_address) = generate_keypair();
3119 let (user_signer, user_address) = generate_keypair();
3120 let fee_token = address!("0000000000000000000000000000000000000002");
3121
3122 let key_auth = KeyAuthorization {
3123 chain_id: 42431,
3124 key_type: SignatureType::Secp256k1,
3125 key_id: access_key_address,
3126 expiry: None,
3127 limits: Some(vec![TokenLimit {
3128 token: fee_token,
3129 limit: U256::ZERO,
3130 }]),
3131 };
3132
3133 let auth_sig_hash = key_auth.signature_hash();
3134 let auth_signature = user_signer
3135 .sign_hash_sync(&auth_sig_hash)
3136 .expect("signing failed");
3137 let signed_key_auth =
3138 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3139
3140 let transaction = create_aa_with_keychain_signature(
3141 user_address,
3142 &access_key_signer,
3143 Some(signed_key_auth),
3144 );
3145
3146 let validator = setup_validator_with_keychain_storage(
3147 &transaction,
3148 user_address,
3149 access_key_address,
3150 None,
3151 );
3152 let mut state_provider = validator.inner.client().latest().unwrap();
3153
3154 let sponsored_fee_payer = Address::random();
3155 assert_ne!(sponsored_fee_payer, user_address);
3156
3157 let result = validator.validate_against_keychain(
3158 &transaction,
3159 &mut state_provider,
3160 sponsored_fee_payer,
3161 fee_token,
3162 )?;
3163 assert!(
3164 result.is_ok(),
3165 "Inline key authorization spending limits should be skipped for sponsored transactions"
3166 );
3167 Ok(())
3168 }
3169
3170 fn setup_validator_with_keychain_storage_t1c(
3172 transaction: &TempoPooledTransaction,
3173 user_address: Address,
3174 key_id: Address,
3175 authorized_key_slot_value: Option<U256>,
3176 ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
3177 use tempo_chainspec::spec::DEV;
3178
3179 setup_validator_with_keychain_storage_spec(
3180 transaction,
3181 user_address,
3182 key_id,
3183 authorized_key_slot_value,
3184 Arc::unwrap_or_clone(DEV.clone()),
3185 )
3186 }
3187
3188 fn build_key_auth_tx(
3190 chain_id: u64,
3191 access_key_signer: &PrivateKeySigner,
3192 access_key_address: Address,
3193 user_signer: &PrivateKeySigner,
3194 user_address: Address,
3195 ) -> TempoPooledTransaction {
3196 build_key_auth_tx_versioned(
3197 chain_id,
3198 access_key_signer,
3199 access_key_address,
3200 user_signer,
3201 user_address,
3202 KeychainVersion::V2,
3203 )
3204 }
3205
3206 fn build_key_auth_tx_versioned(
3209 chain_id: u64,
3210 access_key_signer: &PrivateKeySigner,
3211 access_key_address: Address,
3212 user_signer: &PrivateKeySigner,
3213 user_address: Address,
3214 version: KeychainVersion,
3215 ) -> TempoPooledTransaction {
3216 let key_auth = KeyAuthorization {
3217 chain_id,
3218 key_type: SignatureType::Secp256k1,
3219 key_id: access_key_address,
3220 expiry: None,
3221 limits: None,
3222 };
3223 let auth_sig_hash = key_auth.signature_hash();
3224 let auth_signature = user_signer
3225 .sign_hash_sync(&auth_sig_hash)
3226 .expect("signing failed");
3227 let signed_key_auth =
3228 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3229 create_aa_with_keychain_signature_versioned(
3230 user_address,
3231 access_key_signer,
3232 Some(signed_key_auth),
3233 version,
3234 )
3235 }
3236
3237 #[test]
3240 fn test_key_authorization_chain_id_pre_t1c() -> Result<(), ProviderError> {
3241 let (access_key_signer, access_key_address) = generate_keypair();
3242 let (user_signer, user_address) = generate_keypair();
3243 let moderato = Arc::unwrap_or_clone(MODERATO.clone());
3244
3245 let tx = build_key_auth_tx_versioned(
3247 0,
3248 &access_key_signer,
3249 access_key_address,
3250 &user_signer,
3251 user_address,
3252 KeychainVersion::V1,
3253 );
3254 let validator = setup_validator_with_keychain_storage_spec(
3255 &tx,
3256 user_address,
3257 access_key_address,
3258 None,
3259 moderato.clone(),
3260 );
3261 let mut sp = validator.inner.client().latest().unwrap();
3262 let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp)?;
3263 assert!(
3264 result.is_ok(),
3265 "chain_id=0 should be accepted pre-T1C, got: {result:?}"
3266 );
3267
3268 let tx = build_key_auth_tx_versioned(
3270 42431,
3271 &access_key_signer,
3272 access_key_address,
3273 &user_signer,
3274 user_address,
3275 KeychainVersion::V1,
3276 );
3277 let validator = setup_validator_with_keychain_storage_spec(
3278 &tx,
3279 user_address,
3280 access_key_address,
3281 None,
3282 moderato.clone(),
3283 );
3284 let mut sp = validator.inner.client().latest().unwrap();
3285 let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp)?;
3286 assert!(
3287 result.is_ok(),
3288 "matching chain_id should be accepted pre-T1C, got: {result:?}"
3289 );
3290
3291 let tx = build_key_auth_tx_versioned(
3293 99999,
3294 &access_key_signer,
3295 access_key_address,
3296 &user_signer,
3297 user_address,
3298 KeychainVersion::V1,
3299 );
3300 let validator = setup_validator_with_keychain_storage_spec(
3301 &tx,
3302 user_address,
3303 access_key_address,
3304 None,
3305 moderato,
3306 );
3307 let mut sp = validator.inner.client().latest().unwrap();
3308 let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp);
3309 assert!(
3310 matches!(
3311 result.expect("should not be a provider error"),
3312 Err(TempoPoolTransactionError::Keychain(
3313 "KeyAuthorization chain_id does not match current chain"
3314 ))
3315 ),
3316 "wrong chain_id should be rejected pre-T1C"
3317 );
3318
3319 Ok(())
3320 }
3321
3322 #[test]
3325 fn test_key_authorization_chain_id_post_t1c() -> Result<(), ProviderError> {
3326 use tempo_chainspec::spec::DEV;
3327
3328 let (access_key_signer, access_key_address) = generate_keypair();
3329 let (user_signer, user_address) = generate_keypair();
3330
3331 let tx = build_key_auth_tx(
3333 DEV.chain_id(),
3334 &access_key_signer,
3335 access_key_address,
3336 &user_signer,
3337 user_address,
3338 );
3339 let validator = setup_validator_with_keychain_storage_t1c(
3340 &tx,
3341 user_address,
3342 access_key_address,
3343 None,
3344 );
3345 let mut sp = validator.inner.client().latest().unwrap();
3346 let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp)?;
3347 assert!(
3348 result.is_ok(),
3349 "matching chain_id should be accepted post-T1C, got: {result:?}"
3350 );
3351
3352 let tx = build_key_auth_tx(
3354 0,
3355 &access_key_signer,
3356 access_key_address,
3357 &user_signer,
3358 user_address,
3359 );
3360 let validator = setup_validator_with_keychain_storage_t1c(
3361 &tx,
3362 user_address,
3363 access_key_address,
3364 None,
3365 );
3366 let mut sp = validator.inner.client().latest().unwrap();
3367 let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp);
3368 assert!(
3369 matches!(
3370 result.expect("should not be a provider error"),
3371 Err(TempoPoolTransactionError::Keychain(
3372 "KeyAuthorization chain_id does not match current chain"
3373 ))
3374 ),
3375 "chain_id=0 wildcard should be rejected post-T1C"
3376 );
3377
3378 let tx = build_key_auth_tx(
3380 99999,
3381 &access_key_signer,
3382 access_key_address,
3383 &user_signer,
3384 user_address,
3385 );
3386 let validator = setup_validator_with_keychain_storage_t1c(
3387 &tx,
3388 user_address,
3389 access_key_address,
3390 None,
3391 );
3392 let mut sp = validator.inner.client().latest().unwrap();
3393 let result = validate_against_keychain_default_fee_context(&validator, &tx, &mut sp);
3394 assert!(
3395 matches!(
3396 result.expect("should not be a provider error"),
3397 Err(TempoPoolTransactionError::Keychain(
3398 "KeyAuthorization chain_id does not match current chain"
3399 ))
3400 ),
3401 "wrong chain_id should be rejected post-T1C"
3402 );
3403
3404 Ok(())
3405 }
3406
3407 #[test]
3408 fn test_key_authorization_mismatched_key_id_rejected() {
3409 let (access_key_signer, _access_key_address) = generate_keypair();
3410 let (user_signer, user_address) = generate_keypair();
3411 let different_key_id = Address::random();
3412
3413 let key_auth = KeyAuthorization {
3415 chain_id: 42431,
3416 key_type: SignatureType::Secp256k1,
3417 key_id: different_key_id, expiry: None,
3419 limits: None,
3420 };
3421
3422 let auth_sig_hash = key_auth.signature_hash();
3423 let auth_signature = user_signer
3424 .sign_hash_sync(&auth_sig_hash)
3425 .expect("signing failed");
3426 let signed_key_auth =
3427 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3428
3429 let transaction = create_aa_with_keychain_signature(
3431 user_address,
3432 &access_key_signer,
3433 Some(signed_key_auth),
3434 );
3435
3436 let validator = setup_validator_with_keychain_storage(
3437 &transaction,
3438 user_address,
3439 different_key_id,
3440 None,
3441 );
3442 let mut state_provider = validator.inner.client().latest().unwrap();
3443
3444 let result = validate_against_keychain_default_fee_context(
3445 &validator,
3446 &transaction,
3447 &mut state_provider,
3448 );
3449 assert!(
3450 matches!(
3451 result.expect("should not be a provider error"),
3452 Err(TempoPoolTransactionError::Keychain(
3453 "KeyAuthorization key_id does not match Keychain signature key_id"
3454 ))
3455 ),
3456 "Mismatched key_id should be rejected"
3457 );
3458 }
3459
3460 #[test]
3461 fn test_key_authorization_invalid_signature_rejected() {
3462 let (access_key_signer, access_key_address) = generate_keypair();
3463 let (_user_signer, user_address) = generate_keypair();
3464 let (random_signer, _) = generate_keypair();
3465
3466 let key_auth = KeyAuthorization {
3468 chain_id: 42431,
3469 key_type: SignatureType::Secp256k1,
3470 key_id: access_key_address,
3471 expiry: None,
3472 limits: None,
3473 };
3474
3475 let auth_sig_hash = key_auth.signature_hash();
3476 let auth_signature = random_signer
3478 .sign_hash_sync(&auth_sig_hash)
3479 .expect("signing failed");
3480 let signed_key_auth =
3481 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3482
3483 let transaction = create_aa_with_keychain_signature(
3484 user_address,
3485 &access_key_signer,
3486 Some(signed_key_auth),
3487 );
3488
3489 let validator = setup_validator_with_keychain_storage(
3490 &transaction,
3491 user_address,
3492 access_key_address,
3493 None,
3494 );
3495 let mut state_provider = validator.inner.client().latest().unwrap();
3496
3497 let result = validate_against_keychain_default_fee_context(
3498 &validator,
3499 &transaction,
3500 &mut state_provider,
3501 );
3502 assert!(
3503 matches!(
3504 result.expect("should not be a provider error"),
3505 Err(TempoPoolTransactionError::Keychain(
3506 "Invalid KeyAuthorization signature"
3507 ))
3508 ),
3509 "Invalid KeyAuthorization signature should be rejected"
3510 );
3511 }
3512
3513 #[test]
3514 fn test_keychain_user_address_mismatch_rejected() -> Result<(), ProviderError> {
3515 let (access_key_signer, access_key_address) = generate_keypair();
3516 let real_user = Address::random();
3517
3518 let tx_aa = TempoTransaction {
3520 chain_id: 42431,
3521 max_priority_fee_per_gas: 1_000_000_000,
3522 max_fee_per_gas: 20_000_000_000,
3523 gas_limit: 1_000_000,
3524 calls: vec![Call {
3525 to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
3526 value: U256::ZERO,
3527 input: alloy_primitives::Bytes::new(),
3528 }],
3529 nonce_key: U256::ZERO,
3530 nonce: 0,
3531 fee_token: Some(address!("0000000000000000000000000000000000000002")),
3532 fee_payer_signature: None,
3533 valid_after: None,
3534 valid_before: None,
3535 access_list: Default::default(),
3536 tempo_authorization_list: vec![],
3537 key_authorization: None,
3538 };
3539
3540 let unsigned = AASigned::new_unhashed(
3541 tx_aa.clone(),
3542 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
3543 Signature::test_signature(),
3544 )),
3545 );
3546 let sig_hash = KeychainSignature::signing_hash(unsigned.signature_hash(), real_user);
3548 let signature = access_key_signer
3549 .sign_hash_sync(&sig_hash)
3550 .expect("signing failed");
3551
3552 let keychain_sig = TempoSignature::Keychain(KeychainSignature::new(
3555 real_user, PrimitiveSignature::Secp256k1(signature),
3557 ));
3558
3559 let signed_tx = AASigned::new_unhashed(tx_aa, keychain_sig);
3560 let envelope: TempoTxEnvelope = signed_tx.into();
3561 let recovered = envelope.try_into_recovered().unwrap();
3562 let transaction = TempoPooledTransaction::new(recovered);
3563
3564 let slot_value = AuthorizedKey {
3571 signature_type: 0,
3572 expiry: u64::MAX,
3573 enforce_limits: false,
3574 is_revoked: false,
3575 }
3576 .encode_to_slot();
3577 let validator = setup_validator_with_keychain_storage(
3578 &transaction,
3579 real_user,
3580 access_key_address,
3581 Some(slot_value),
3582 );
3583 let mut state_provider = validator.inner.client().latest().unwrap();
3584
3585 let result = validate_against_keychain_default_fee_context(
3587 &validator,
3588 &transaction,
3589 &mut state_provider,
3590 )?;
3591 assert!(
3592 result.is_ok(),
3593 "Properly constructed keychain sig should pass, got: {result:?}"
3594 );
3595 Ok(())
3596 }
3597
3598 fn setup_validator_with_keychain_storage_and_timestamp(
3600 transaction: &TempoPooledTransaction,
3601 user_address: Address,
3602 key_id: Address,
3603 authorized_key_slot_value: Option<U256>,
3604 tip_timestamp: u64,
3605 ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
3606 let provider =
3607 MockEthProvider::<TempoPrimitives>::new().with_chain_spec(moderato_with_t1c());
3608
3609 provider.add_account(
3611 transaction.sender(),
3612 ExtendedAccount::new(transaction.nonce(), U256::ZERO),
3613 );
3614
3615 let block = Block {
3617 header: TempoHeader {
3618 inner: Header {
3619 timestamp: tip_timestamp,
3620 gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
3621 ..Default::default()
3622 },
3623 ..Default::default()
3624 },
3625 body: Default::default(),
3626 };
3627 provider.add_block(B256::random(), block);
3628
3629 if let Some(slot_value) = authorized_key_slot_value {
3631 let storage_slot = AccountKeychain::new().keys[user_address][key_id].base_slot();
3632 provider.add_account(
3633 ACCOUNT_KEYCHAIN_ADDRESS,
3634 ExtendedAccount::new(0, U256::ZERO)
3635 .extend_storage([(storage_slot.into(), slot_value)]),
3636 );
3637 }
3638
3639 let inner =
3640 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
3641 .disable_balance_check()
3642 .build(InMemoryBlobStore::default());
3643 let amm_cache =
3644 AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
3645 let validator = TempoTransactionValidator::new(
3646 inner,
3647 DEFAULT_AA_VALID_AFTER_MAX_SECS,
3648 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
3649 amm_cache,
3650 );
3651
3652 let mock_block = create_mock_block(tip_timestamp);
3654 validator.on_new_head_block(&mock_block);
3655
3656 validator
3657 }
3658
3659 #[test]
3660 fn test_stored_access_key_expired_rejected() {
3661 let (access_key_signer, access_key_address) = generate_keypair();
3662 let user_address = Address::random();
3663 let current_time = 1000u64;
3664
3665 let transaction =
3666 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3667
3668 let slot_value = AuthorizedKey {
3670 signature_type: 0,
3671 expiry: current_time - 1, enforce_limits: false,
3673 is_revoked: false,
3674 }
3675 .encode_to_slot();
3676
3677 let validator = setup_validator_with_keychain_storage_and_timestamp(
3678 &transaction,
3679 user_address,
3680 access_key_address,
3681 Some(slot_value),
3682 current_time,
3683 );
3684 let mut state_provider = validator.inner.client().latest().unwrap();
3685
3686 let result = validate_against_keychain_default_fee_context(
3687 &validator,
3688 &transaction,
3689 &mut state_provider,
3690 );
3691 assert!(
3692 matches!(
3693 result.expect("should not be a provider error"),
3694 Err(TempoPoolTransactionError::AccessKeyExpired { expiry, min_allowed: ct })
3695 if expiry == current_time - 1 && ct == current_time + AA_VALID_BEFORE_MIN_SECS
3696 ),
3697 "Expired access key should be rejected"
3698 );
3699 }
3700
3701 #[test]
3702 fn test_stored_access_key_expiry_at_current_time_rejected() {
3703 let (access_key_signer, access_key_address) = generate_keypair();
3704 let user_address = Address::random();
3705 let current_time = 1000u64;
3706
3707 let transaction =
3708 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3709
3710 let slot_value = AuthorizedKey {
3712 signature_type: 0,
3713 expiry: current_time, enforce_limits: false,
3715 is_revoked: false,
3716 }
3717 .encode_to_slot();
3718
3719 let validator = setup_validator_with_keychain_storage_and_timestamp(
3720 &transaction,
3721 user_address,
3722 access_key_address,
3723 Some(slot_value),
3724 current_time,
3725 );
3726 let mut state_provider = validator.inner.client().latest().unwrap();
3727
3728 let result = validate_against_keychain_default_fee_context(
3729 &validator,
3730 &transaction,
3731 &mut state_provider,
3732 );
3733 assert!(
3734 matches!(
3735 result.expect("should not be a provider error"),
3736 Err(TempoPoolTransactionError::AccessKeyExpired { .. })
3737 ),
3738 "Access key with expiry == current_time should be rejected"
3739 );
3740 }
3741
3742 #[test]
3743 fn test_stored_access_key_valid_expiry_accepted() -> Result<(), ProviderError> {
3744 let (access_key_signer, access_key_address) = generate_keypair();
3745 let user_address = Address::random();
3746 let current_time = 1000u64;
3747
3748 let transaction =
3749 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
3750
3751 let slot_value = AuthorizedKey {
3753 signature_type: 0,
3754 expiry: current_time + 100, enforce_limits: false,
3756 is_revoked: false,
3757 }
3758 .encode_to_slot();
3759
3760 let validator = setup_validator_with_keychain_storage_and_timestamp(
3761 &transaction,
3762 user_address,
3763 access_key_address,
3764 Some(slot_value),
3765 current_time,
3766 );
3767 let mut state_provider = validator.inner.client().latest().unwrap();
3768
3769 let result = validate_against_keychain_default_fee_context(
3770 &validator,
3771 &transaction,
3772 &mut state_provider,
3773 )?;
3774 assert!(
3775 result.is_ok(),
3776 "Access key with future expiry should be accepted, got: {result:?}"
3777 );
3778 Ok(())
3779 }
3780
3781 #[test]
3782 fn test_key_authorization_expired_rejected() {
3783 let (access_key_signer, access_key_address) = generate_keypair();
3784 let (user_signer, user_address) = generate_keypair();
3785 let current_time = 1000u64;
3786
3787 let key_auth = KeyAuthorization {
3789 chain_id: 42431,
3790 key_type: SignatureType::Secp256k1,
3791 key_id: access_key_address,
3792 expiry: Some(current_time - 1), limits: None,
3794 };
3795
3796 let auth_sig_hash = key_auth.signature_hash();
3797 let auth_signature = user_signer
3798 .sign_hash_sync(&auth_sig_hash)
3799 .expect("signing failed");
3800 let signed_key_auth =
3801 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3802
3803 let transaction = create_aa_with_keychain_signature(
3804 user_address,
3805 &access_key_signer,
3806 Some(signed_key_auth),
3807 );
3808
3809 let validator = setup_validator_with_keychain_storage_and_timestamp(
3810 &transaction,
3811 user_address,
3812 access_key_address,
3813 None,
3814 current_time,
3815 );
3816 let mut state_provider = validator.inner.client().latest().unwrap();
3817
3818 let result = validate_against_keychain_default_fee_context(
3819 &validator,
3820 &transaction,
3821 &mut state_provider,
3822 );
3823 assert!(
3824 matches!(
3825 result.expect("should not be a provider error"),
3826 Err(TempoPoolTransactionError::KeyAuthorizationExpired { expiry, min_allowed: ct })
3827 if expiry == current_time - 1 && ct == current_time + AA_VALID_BEFORE_MIN_SECS
3828 ),
3829 "Expired KeyAuthorization should be rejected"
3830 );
3831 }
3832
3833 #[test]
3834 fn test_key_authorization_expiry_at_current_time_rejected() {
3835 let (access_key_signer, access_key_address) = generate_keypair();
3836 let (user_signer, user_address) = generate_keypair();
3837 let current_time = 1000u64;
3838
3839 let key_auth = KeyAuthorization {
3841 chain_id: 42431,
3842 key_type: SignatureType::Secp256k1,
3843 key_id: access_key_address,
3844 expiry: Some(current_time), limits: None,
3846 };
3847
3848 let auth_sig_hash = key_auth.signature_hash();
3849 let auth_signature = user_signer
3850 .sign_hash_sync(&auth_sig_hash)
3851 .expect("signing failed");
3852 let signed_key_auth =
3853 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3854
3855 let transaction = create_aa_with_keychain_signature(
3856 user_address,
3857 &access_key_signer,
3858 Some(signed_key_auth),
3859 );
3860
3861 let validator = setup_validator_with_keychain_storage_and_timestamp(
3862 &transaction,
3863 user_address,
3864 access_key_address,
3865 None,
3866 current_time,
3867 );
3868 let mut state_provider = validator.inner.client().latest().unwrap();
3869
3870 let result = validate_against_keychain_default_fee_context(
3871 &validator,
3872 &transaction,
3873 &mut state_provider,
3874 );
3875 assert!(
3876 matches!(
3877 result.expect("should not be a provider error"),
3878 Err(TempoPoolTransactionError::KeyAuthorizationExpired { .. })
3879 ),
3880 "KeyAuthorization with expiry == current_time should be rejected"
3881 );
3882 }
3883
3884 #[test]
3885 fn test_key_authorization_valid_expiry_accepted() -> Result<(), ProviderError> {
3886 let (access_key_signer, access_key_address) = generate_keypair();
3887 let (user_signer, user_address) = generate_keypair();
3888 let current_time = 1000u64;
3889
3890 let key_auth = KeyAuthorization {
3892 chain_id: 42431,
3893 key_type: SignatureType::Secp256k1,
3894 key_id: access_key_address,
3895 expiry: Some(current_time + 100), limits: None,
3897 };
3898
3899 let auth_sig_hash = key_auth.signature_hash();
3900 let auth_signature = user_signer
3901 .sign_hash_sync(&auth_sig_hash)
3902 .expect("signing failed");
3903 let signed_key_auth =
3904 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3905
3906 let transaction = create_aa_with_keychain_signature(
3907 user_address,
3908 &access_key_signer,
3909 Some(signed_key_auth),
3910 );
3911
3912 let validator = setup_validator_with_keychain_storage_and_timestamp(
3913 &transaction,
3914 user_address,
3915 access_key_address,
3916 None,
3917 current_time,
3918 );
3919 let mut state_provider = validator.inner.client().latest().unwrap();
3920
3921 let result = validate_against_keychain_default_fee_context(
3922 &validator,
3923 &transaction,
3924 &mut state_provider,
3925 )?;
3926 assert!(
3927 result.is_ok(),
3928 "KeyAuthorization with future expiry should be accepted, got: {result:?}"
3929 );
3930 Ok(())
3931 }
3932
3933 #[test]
3934 fn test_key_authorization_expiry_cached_for_pool_maintenance() -> Result<(), ProviderError>
3935 {
3936 let (access_key_signer, access_key_address) = generate_keypair();
3937 let (user_signer, user_address) = generate_keypair();
3938 let current_time = 1000u64;
3939 let expiry = current_time + 100;
3940
3941 let key_auth = KeyAuthorization {
3942 chain_id: 42431,
3943 key_type: SignatureType::Secp256k1,
3944 key_id: access_key_address,
3945 expiry: Some(expiry),
3946 limits: None,
3947 };
3948
3949 let auth_sig_hash = key_auth.signature_hash();
3950 let auth_signature = user_signer
3951 .sign_hash_sync(&auth_sig_hash)
3952 .expect("signing failed");
3953 let signed_key_auth =
3954 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
3955
3956 let transaction = create_aa_with_keychain_signature(
3957 user_address,
3958 &access_key_signer,
3959 Some(signed_key_auth),
3960 );
3961
3962 let validator = setup_validator_with_keychain_storage_and_timestamp(
3963 &transaction,
3964 user_address,
3965 access_key_address,
3966 None,
3967 current_time,
3968 );
3969 let mut state_provider = validator.inner.client().latest().unwrap();
3970
3971 let result = validate_against_keychain_default_fee_context(
3972 &validator,
3973 &transaction,
3974 &mut state_provider,
3975 )?;
3976 assert!(result.is_ok(), "KeyAuthorization should be accepted");
3977 assert_eq!(
3978 transaction.key_expiry(),
3979 Some(expiry),
3980 "KeyAuthorization expiry should be cached for pool maintenance"
3981 );
3982 Ok(())
3983 }
3984
3985 #[test]
3986 fn test_key_authorization_no_expiry_accepted() -> Result<(), ProviderError> {
3987 let (access_key_signer, access_key_address) = generate_keypair();
3988 let (user_signer, user_address) = generate_keypair();
3989 let current_time = 1000u64;
3990
3991 let key_auth = KeyAuthorization {
3993 chain_id: 42431,
3994 key_type: SignatureType::Secp256k1,
3995 key_id: access_key_address,
3996 expiry: None, limits: None,
3998 };
3999
4000 let auth_sig_hash = key_auth.signature_hash();
4001 let auth_signature = user_signer
4002 .sign_hash_sync(&auth_sig_hash)
4003 .expect("signing failed");
4004 let signed_key_auth =
4005 key_auth.into_signed(PrimitiveSignature::Secp256k1(auth_signature));
4006
4007 let transaction = create_aa_with_keychain_signature(
4008 user_address,
4009 &access_key_signer,
4010 Some(signed_key_auth),
4011 );
4012
4013 let validator = setup_validator_with_keychain_storage_and_timestamp(
4014 &transaction,
4015 user_address,
4016 access_key_address,
4017 None,
4018 current_time,
4019 );
4020 let mut state_provider = validator.inner.client().latest().unwrap();
4021
4022 let result = validate_against_keychain_default_fee_context(
4023 &validator,
4024 &transaction,
4025 &mut state_provider,
4026 )?;
4027 assert!(
4028 result.is_ok(),
4029 "KeyAuthorization with no expiry should be accepted, got: {result:?}"
4030 );
4031 Ok(())
4032 }
4033
4034 fn setup_validator_with_spending_limit(
4036 transaction: &TempoPooledTransaction,
4037 user_address: Address,
4038 key_id: Address,
4039 enforce_limits: bool,
4040 spending_limit: Option<(Address, U256)>, ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
4042 let provider =
4043 MockEthProvider::<TempoPrimitives>::new().with_chain_spec(moderato_with_t1c());
4044
4045 provider.add_account(
4047 transaction.sender(),
4048 ExtendedAccount::new(transaction.nonce(), U256::ZERO),
4049 );
4050 provider.add_block(B256::random(), Default::default());
4051
4052 let slot_value = AuthorizedKey {
4054 signature_type: 0,
4055 expiry: u64::MAX,
4056 enforce_limits,
4057 is_revoked: false,
4058 }
4059 .encode_to_slot();
4060
4061 let key_storage_slot = AccountKeychain::new().keys[user_address][key_id].base_slot();
4062 let mut storage = vec![(key_storage_slot.into(), slot_value)];
4063
4064 if let Some((token, limit)) = spending_limit {
4066 let limit_key = AccountKeychain::spending_limit_key(user_address, key_id);
4067 let limit_slot: U256 =
4068 AccountKeychain::new().spending_limits[limit_key][token].slot();
4069 storage.push((limit_slot.into(), limit));
4070 }
4071
4072 provider.add_account(
4073 ACCOUNT_KEYCHAIN_ADDRESS,
4074 ExtendedAccount::new(0, U256::ZERO).extend_storage(storage),
4075 );
4076
4077 let inner =
4078 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
4079 .disable_balance_check()
4080 .build(InMemoryBlobStore::default());
4081 let amm_cache =
4082 AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
4083 TempoTransactionValidator::new(
4084 inner,
4085 DEFAULT_AA_VALID_AFTER_MAX_SECS,
4086 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
4087 amm_cache,
4088 )
4089 }
4090
4091 #[test]
4092 fn test_spending_limit_not_enforced_passes() -> Result<(), ProviderError> {
4093 let (access_key_signer, access_key_address) = generate_keypair();
4094 let user_address = Address::random();
4095
4096 let transaction =
4097 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4098
4099 let validator = setup_validator_with_spending_limit(
4101 &transaction,
4102 user_address,
4103 access_key_address,
4104 false, None, );
4107 let mut state_provider = validator.inner.client().latest().unwrap();
4108
4109 let result = validate_against_keychain_default_fee_context(
4110 &validator,
4111 &transaction,
4112 &mut state_provider,
4113 )?;
4114 assert!(
4115 result.is_ok(),
4116 "Key with enforce_limits=false should pass, got: {result:?}"
4117 );
4118 Ok(())
4119 }
4120
4121 #[test]
4122 fn test_spending_limit_sufficient_passes() -> Result<(), ProviderError> {
4123 let (access_key_signer, access_key_address) = generate_keypair();
4124 let user_address = Address::random();
4125
4126 let transaction =
4127 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4128
4129 let fee_token = transaction
4131 .inner()
4132 .fee_token()
4133 .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4134 let fee_cost = transaction.fee_token_cost();
4135
4136 let validator = setup_validator_with_spending_limit(
4138 &transaction,
4139 user_address,
4140 access_key_address,
4141 true, Some((fee_token, fee_cost + U256::from(100))), );
4144 let mut state_provider = validator.inner.client().latest().unwrap();
4145
4146 let result = validate_against_keychain_default_fee_context(
4147 &validator,
4148 &transaction,
4149 &mut state_provider,
4150 )?;
4151 assert!(
4152 result.is_ok(),
4153 "Sufficient spending limit should pass, got: {result:?}"
4154 );
4155 Ok(())
4156 }
4157
4158 #[test]
4159 fn test_spending_limit_exact_passes() -> Result<(), ProviderError> {
4160 let (access_key_signer, access_key_address) = generate_keypair();
4161 let user_address = Address::random();
4162
4163 let transaction =
4164 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4165
4166 let fee_token = transaction
4167 .inner()
4168 .fee_token()
4169 .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4170 let fee_cost = transaction.fee_token_cost();
4171
4172 let validator = setup_validator_with_spending_limit(
4174 &transaction,
4175 user_address,
4176 access_key_address,
4177 true, Some((fee_token, fee_cost)), );
4180 let mut state_provider = validator.inner.client().latest().unwrap();
4181
4182 let result = validate_against_keychain_default_fee_context(
4183 &validator,
4184 &transaction,
4185 &mut state_provider,
4186 )?;
4187 assert!(
4188 result.is_ok(),
4189 "Exact spending limit should pass, got: {result:?}"
4190 );
4191 Ok(())
4192 }
4193
4194 #[test]
4195 fn test_spending_limit_exceeded_rejected() {
4196 let (access_key_signer, access_key_address) = generate_keypair();
4197 let user_address = Address::random();
4198
4199 let transaction =
4200 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4201
4202 let fee_token = transaction
4203 .inner()
4204 .fee_token()
4205 .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4206 let fee_cost = transaction.fee_token_cost();
4207
4208 let insufficient_limit = fee_cost - U256::from(1);
4210 let validator = setup_validator_with_spending_limit(
4211 &transaction,
4212 user_address,
4213 access_key_address,
4214 true, Some((fee_token, insufficient_limit)), );
4217 let mut state_provider = validator.inner.client().latest().unwrap();
4218
4219 let result = validate_against_keychain_default_fee_context(
4220 &validator,
4221 &transaction,
4222 &mut state_provider,
4223 );
4224 assert!(
4225 matches!(
4226 result.expect("should not be a provider error"),
4227 Err(TempoPoolTransactionError::SpendingLimitExceeded { .. })
4228 ),
4229 "Insufficient spending limit should be rejected"
4230 );
4231 }
4232
4233 #[test]
4234 fn test_spending_limit_zero_rejected() {
4235 let (access_key_signer, access_key_address) = generate_keypair();
4236 let user_address = Address::random();
4237
4238 let transaction =
4239 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4240
4241 let fee_token = transaction
4242 .inner()
4243 .fee_token()
4244 .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4245
4246 let validator = setup_validator_with_spending_limit(
4248 &transaction,
4249 user_address,
4250 access_key_address,
4251 true, Some((fee_token, U256::ZERO)), );
4254 let mut state_provider = validator.inner.client().latest().unwrap();
4255
4256 let result = validate_against_keychain_default_fee_context(
4257 &validator,
4258 &transaction,
4259 &mut state_provider,
4260 );
4261 assert!(
4262 matches!(
4263 result.expect("should not be a provider error"),
4264 Err(TempoPoolTransactionError::SpendingLimitExceeded { .. })
4265 ),
4266 "Zero spending limit should be rejected"
4267 );
4268 }
4269
4270 #[test]
4271 fn test_spending_limit_wrong_token_rejected() {
4272 let (access_key_signer, access_key_address) = generate_keypair();
4273 let user_address = Address::random();
4274
4275 let transaction =
4276 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4277
4278 let fee_token = transaction
4279 .inner()
4280 .fee_token()
4281 .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
4282
4283 let different_token = Address::random();
4285 assert_ne!(fee_token, different_token); let validator = setup_validator_with_spending_limit(
4288 &transaction,
4289 user_address,
4290 access_key_address,
4291 true, Some((different_token, U256::MAX)), );
4294 let mut state_provider = validator.inner.client().latest().unwrap();
4295
4296 let result = validate_against_keychain_default_fee_context(
4297 &validator,
4298 &transaction,
4299 &mut state_provider,
4300 );
4301 assert!(
4302 matches!(
4303 result.expect("should not be a provider error"),
4304 Err(TempoPoolTransactionError::SpendingLimitExceeded { .. })
4305 ),
4306 "Wrong token spending limit should be rejected (fee token has 0 limit)"
4307 );
4308 }
4309
4310 fn moderato_without_t1c() -> TempoChainSpec {
4312 Arc::unwrap_or_clone(MODERATO.clone())
4313 }
4314
4315 fn setup_validator_with_spec(
4317 transaction: &TempoPooledTransaction,
4318 chain_spec: TempoChainSpec,
4319 tip_timestamp: u64,
4320 ) -> TempoTransactionValidator<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
4321 let provider = MockEthProvider::<TempoPrimitives>::new().with_chain_spec(chain_spec);
4322 provider.add_account(
4323 transaction.sender(),
4324 ExtendedAccount::new(transaction.nonce(), U256::ZERO),
4325 );
4326 provider.add_block(B256::random(), Default::default());
4327
4328 let inner =
4329 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
4330 .disable_balance_check()
4331 .build(InMemoryBlobStore::default());
4332 let amm_cache =
4333 AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
4334 let validator = TempoTransactionValidator::new(
4335 inner,
4336 DEFAULT_AA_VALID_AFTER_MAX_SECS,
4337 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
4338 amm_cache,
4339 );
4340
4341 let mock_block = create_mock_block(tip_timestamp);
4342 validator.on_new_head_block(&mock_block);
4343 validator
4344 }
4345
4346 #[test]
4347 fn test_legacy_v1_keychain_rejected_post_t1c() {
4348 let (access_key_signer, _) = generate_keypair();
4349 let user_address = Address::random();
4350
4351 let transaction =
4352 create_aa_with_v1_keychain_signature(user_address, &access_key_signer, None);
4353
4354 let validator = setup_validator_with_spec(&transaction, moderato_with_t1c(), 0);
4355 let spec = validator
4356 .inner
4357 .chain_spec()
4358 .tempo_hardfork_at(validator.inner.fork_tracker().tip_timestamp());
4359
4360 let result = validator.validate_keychain_version(&transaction, spec);
4361
4362 assert!(
4363 matches!(
4364 result,
4365 Err(TempoPoolTransactionError::LegacyKeychainPostT1C)
4366 ),
4367 "V1 keychain should be rejected post-T1C, got: {result:?}"
4368 );
4369 }
4370
4371 #[test]
4372 fn test_v2_keychain_accepted_post_t1c() -> Result<(), ProviderError> {
4373 let (access_key_signer, access_key_address) = generate_keypair();
4374 let user_address = Address::random();
4375
4376 let transaction =
4377 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4378
4379 let slot_value = AuthorizedKey {
4380 signature_type: 0,
4381 expiry: u64::MAX,
4382 enforce_limits: false,
4383 is_revoked: false,
4384 }
4385 .encode_to_slot();
4386
4387 let validator = setup_validator_with_keychain_storage(
4388 &transaction,
4389 user_address,
4390 access_key_address,
4391 Some(slot_value),
4392 );
4393 let mut state_provider = validator.inner.client().latest().unwrap();
4394
4395 let result = validate_against_keychain_default_fee_context(
4396 &validator,
4397 &transaction,
4398 &mut state_provider,
4399 )?;
4400 assert!(
4401 result.is_ok(),
4402 "V2 keychain should be accepted post-T1C, got: {result:?}"
4403 );
4404 Ok(())
4405 }
4406
4407 #[test]
4408 fn test_v2_keychain_rejected_pre_t1c() {
4409 let (access_key_signer, _) = generate_keypair();
4410 let user_address = Address::random();
4411
4412 let transaction =
4413 create_aa_with_keychain_signature(user_address, &access_key_signer, None);
4414
4415 let validator = setup_validator_with_spec(&transaction, moderato_without_t1c(), 0);
4416 let spec = validator
4417 .inner
4418 .chain_spec()
4419 .tempo_hardfork_at(validator.inner.fork_tracker().tip_timestamp());
4420
4421 let result = validator.validate_keychain_version(&transaction, spec);
4422
4423 assert!(
4424 matches!(result, Err(TempoPoolTransactionError::V2KeychainPreT1C)),
4425 "V2 keychain should be rejected pre-T1C, got: {result:?}"
4426 );
4427 }
4428
4429 #[test]
4430 fn test_v1_keychain_accepted_pre_t1c() -> Result<(), ProviderError> {
4431 let (access_key_signer, access_key_address) = generate_keypair();
4432 let user_address = Address::random();
4433
4434 let transaction =
4435 create_aa_with_v1_keychain_signature(user_address, &access_key_signer, None);
4436
4437 let slot_value = AuthorizedKey {
4438 signature_type: 0,
4439 expiry: u64::MAX,
4440 enforce_limits: false,
4441 is_revoked: false,
4442 }
4443 .encode_to_slot();
4444
4445 let provider =
4447 MockEthProvider::<TempoPrimitives>::new().with_chain_spec(moderato_without_t1c());
4448 provider.add_account(
4449 transaction.sender(),
4450 ExtendedAccount::new(transaction.nonce(), U256::ZERO),
4451 );
4452 provider.add_block(B256::random(), Default::default());
4453 let storage_slot =
4454 AccountKeychain::new().keys[user_address][access_key_address].base_slot();
4455 provider.add_account(
4456 ACCOUNT_KEYCHAIN_ADDRESS,
4457 ExtendedAccount::new(0, U256::ZERO)
4458 .extend_storage([(storage_slot.into(), slot_value)]),
4459 );
4460 let inner =
4461 EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
4462 .disable_balance_check()
4463 .build(InMemoryBlobStore::default());
4464 let amm_cache =
4465 AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
4466 let validator = TempoTransactionValidator::new(
4467 inner,
4468 DEFAULT_AA_VALID_AFTER_MAX_SECS,
4469 DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
4470 amm_cache,
4471 );
4472
4473 let mut state_provider = validator.inner.client().latest().unwrap();
4474 let result = validate_against_keychain_default_fee_context(
4475 &validator,
4476 &transaction,
4477 &mut state_provider,
4478 )?;
4479 assert!(
4480 result.is_ok(),
4481 "V1 keychain should be accepted pre-T1C, got: {result:?}"
4482 );
4483 Ok(())
4484 }
4485
4486 #[test]
4487 fn test_legacy_keychain_post_t1c_is_bad_transaction() {
4488 assert!(
4489 TempoPoolTransactionError::LegacyKeychainPostT1C.is_bad_transaction(),
4490 "Post-T1C V1 rejection should be a bad transaction (permanent)"
4491 );
4492 }
4493
4494 #[test]
4495 fn test_v2_keychain_pre_t1c_is_not_bad_transaction() {
4496 assert!(
4497 !TempoPoolTransactionError::V2KeychainPreT1C.is_bad_transaction(),
4498 "Pre-T1C V2 rejection should NOT be a bad transaction (transient)"
4499 );
4500 }
4501
4502 #[test]
4503 fn test_expired_access_key_is_not_bad_transaction() {
4504 assert!(
4505 !TempoPoolTransactionError::AccessKeyExpired {
4506 expiry: 1,
4507 min_allowed: 4,
4508 }
4509 .is_bad_transaction(),
4510 "Expired access key rejection should NOT be a bad transaction (timing-sensitive)"
4511 );
4512 }
4513
4514 #[test]
4515 fn test_expired_key_authorization_is_not_bad_transaction() {
4516 assert!(
4517 !TempoPoolTransactionError::KeyAuthorizationExpired {
4518 expiry: 1,
4519 min_allowed: 4,
4520 }
4521 .is_bad_transaction(),
4522 "Expired key authorization rejection should NOT be a bad transaction (timing-sensitive)"
4523 );
4524 }
4525 }
4526
4527 fn create_aa_transaction_with_authorizations(
4533 authorization_count: usize,
4534 ) -> TempoPooledTransaction {
4535 use alloy_eips::eip7702::Authorization;
4536 use alloy_primitives::{Signature, TxKind, address};
4537 use tempo_primitives::transaction::{
4538 TempoSignedAuthorization, TempoTransaction,
4539 tempo_transaction::Call,
4540 tt_signature::{PrimitiveSignature, TempoSignature},
4541 tt_signed::AASigned,
4542 };
4543
4544 let authorizations: Vec<TempoSignedAuthorization> = (0..authorization_count)
4546 .map(|i| {
4547 let auth = Authorization {
4548 chain_id: U256::from(1),
4549 nonce: i as u64,
4550 address: address!("0000000000000000000000000000000000000001"),
4551 };
4552 TempoSignedAuthorization::new_unchecked(
4553 auth,
4554 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
4555 Signature::test_signature(),
4556 )),
4557 )
4558 })
4559 .collect();
4560
4561 let tx_aa = TempoTransaction {
4562 chain_id: 1,
4563 max_priority_fee_per_gas: 1_000_000_000,
4564 max_fee_per_gas: 20_000_000_000, gas_limit: 1_000_000,
4566 calls: vec![Call {
4567 to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
4568 value: U256::ZERO,
4569 input: alloy_primitives::Bytes::new(),
4570 }],
4571 nonce_key: U256::ZERO,
4572 nonce: 0,
4573 fee_token: Some(address!("0000000000000000000000000000000000000002")),
4574 fee_payer_signature: None,
4575 valid_after: None,
4576 valid_before: None,
4577 access_list: Default::default(),
4578 tempo_authorization_list: authorizations,
4579 key_authorization: None,
4580 };
4581
4582 let signed_tx = AASigned::new_unhashed(
4583 tx_aa,
4584 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
4585 );
4586 let envelope: TempoTxEnvelope = signed_tx.into();
4587 let recovered = envelope.try_into_recovered().unwrap();
4588 TempoPooledTransaction::new(recovered)
4589 }
4590
4591 #[tokio::test]
4592 async fn test_aa_too_many_authorizations_rejected() {
4593 let current_time = std::time::SystemTime::now()
4594 .duration_since(std::time::UNIX_EPOCH)
4595 .unwrap()
4596 .as_secs();
4597
4598 let transaction =
4600 create_aa_transaction_with_authorizations(DEFAULT_MAX_TEMPO_AUTHORIZATIONS + 1);
4601 let validator = setup_validator(&transaction, current_time);
4602
4603 let outcome = validator
4604 .validate_transaction(TransactionOrigin::External, transaction)
4605 .await;
4606
4607 match &outcome {
4608 TransactionValidationOutcome::Invalid(_, err) => {
4609 let error_msg = err.to_string();
4610 assert!(
4611 error_msg.contains("Too many authorizations"),
4612 "Expected TooManyAuthorizations error, got: {error_msg}"
4613 );
4614 }
4615 other => panic!("Expected Invalid outcome, got: {other:?}"),
4616 }
4617 }
4618
4619 #[tokio::test]
4620 async fn test_aa_authorization_count_at_limit_accepted() {
4621 let current_time = std::time::SystemTime::now()
4622 .duration_since(std::time::UNIX_EPOCH)
4623 .unwrap()
4624 .as_secs();
4625
4626 let transaction =
4628 create_aa_transaction_with_authorizations(DEFAULT_MAX_TEMPO_AUTHORIZATIONS);
4629 let validator = setup_validator(&transaction, current_time);
4630
4631 let outcome = validator
4632 .validate_transaction(TransactionOrigin::External, transaction)
4633 .await;
4634
4635 if let TransactionValidationOutcome::Invalid(_, err) = &outcome {
4637 let error_msg = err.to_string();
4638 assert!(
4639 !error_msg.contains("Too many authorizations"),
4640 "Should not fail with TooManyAuthorizations at the limit, got: {error_msg}"
4641 );
4642 }
4643 }
4644
4645 #[tokio::test]
4647 async fn test_aa_no_calls_rejected() {
4648 let current_time = std::time::SystemTime::now()
4649 .duration_since(std::time::UNIX_EPOCH)
4650 .unwrap()
4651 .as_secs();
4652
4653 let transaction = TxBuilder::aa(Address::random())
4655 .fee_token(address!("0000000000000000000000000000000000000002"))
4656 .calls(vec![]) .build();
4658 let validator = setup_validator(&transaction, current_time);
4659
4660 let outcome = validator
4661 .validate_transaction(TransactionOrigin::External, transaction)
4662 .await;
4663
4664 match outcome {
4665 TransactionValidationOutcome::Invalid(_, ref err) => {
4666 assert!(
4667 matches!(
4668 err.downcast_other_ref::<TempoPoolTransactionError>(),
4669 Some(TempoPoolTransactionError::NoCalls)
4670 ),
4671 "Expected NoCalls error, got: {err:?}"
4672 );
4673 }
4674 _ => panic!("Expected Invalid outcome with NoCalls error, got: {outcome:?}"),
4675 }
4676 }
4677
4678 #[tokio::test]
4680 async fn test_aa_create_call_not_first_rejected() {
4681 let current_time = std::time::SystemTime::now()
4682 .duration_since(std::time::UNIX_EPOCH)
4683 .unwrap()
4684 .as_secs();
4685
4686 let calls = vec![
4688 Call {
4689 to: TxKind::Call(Address::random()), value: U256::ZERO,
4691 input: Default::default(),
4692 },
4693 Call {
4694 to: TxKind::Create, value: U256::ZERO,
4696 input: Default::default(),
4697 },
4698 ];
4699
4700 let transaction = TxBuilder::aa(Address::random())
4701 .fee_token(address!("0000000000000000000000000000000000000002"))
4702 .calls(calls)
4703 .build();
4704 let validator = setup_validator(&transaction, current_time);
4705
4706 let outcome = validator
4707 .validate_transaction(TransactionOrigin::External, transaction)
4708 .await;
4709
4710 match outcome {
4711 TransactionValidationOutcome::Invalid(_, ref err) => {
4712 assert!(
4713 matches!(
4714 err.downcast_other_ref::<TempoPoolTransactionError>(),
4715 Some(TempoPoolTransactionError::CreateCallNotFirst)
4716 ),
4717 "Expected CreateCallNotFirst error, got: {err:?}"
4718 );
4719 }
4720 _ => panic!("Expected Invalid outcome with CreateCallNotFirst error, got: {outcome:?}"),
4721 }
4722 }
4723
4724 #[tokio::test]
4726 async fn test_aa_create_call_first_accepted() {
4727 let current_time = std::time::SystemTime::now()
4728 .duration_since(std::time::UNIX_EPOCH)
4729 .unwrap()
4730 .as_secs();
4731
4732 let calls = vec![
4734 Call {
4735 to: TxKind::Create, value: U256::ZERO,
4737 input: Default::default(),
4738 },
4739 Call {
4740 to: TxKind::Call(Address::random()), value: U256::ZERO,
4742 input: Default::default(),
4743 },
4744 ];
4745
4746 let transaction = TxBuilder::aa(Address::random())
4747 .fee_token(address!("0000000000000000000000000000000000000002"))
4748 .calls(calls)
4749 .build();
4750 let validator = setup_validator(&transaction, current_time);
4751
4752 let outcome = validator
4753 .validate_transaction(TransactionOrigin::External, transaction)
4754 .await;
4755
4756 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
4758 assert!(
4759 !matches!(
4760 err.downcast_other_ref::<TempoPoolTransactionError>(),
4761 Some(TempoPoolTransactionError::CreateCallNotFirst)
4762 ),
4763 "CREATE call as first call should be accepted, got: {err:?}"
4764 );
4765 }
4766 }
4767
4768 #[tokio::test]
4770 async fn test_aa_multiple_creates_rejected() {
4771 let current_time = std::time::SystemTime::now()
4772 .duration_since(std::time::UNIX_EPOCH)
4773 .unwrap()
4774 .as_secs();
4775
4776 let calls = vec![
4778 Call {
4779 to: TxKind::Create, value: U256::ZERO,
4781 input: Default::default(),
4782 },
4783 Call {
4784 to: TxKind::Call(Address::random()), value: U256::ZERO,
4786 input: Default::default(),
4787 },
4788 Call {
4789 to: TxKind::Create, value: U256::ZERO,
4791 input: Default::default(),
4792 },
4793 ];
4794
4795 let transaction = TxBuilder::aa(Address::random())
4796 .fee_token(address!("0000000000000000000000000000000000000002"))
4797 .calls(calls)
4798 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
4799 .build();
4800 let validator = setup_validator(&transaction, current_time);
4801
4802 let outcome = validator
4803 .validate_transaction(TransactionOrigin::External, transaction)
4804 .await;
4805
4806 match outcome {
4807 TransactionValidationOutcome::Invalid(_, ref err) => {
4808 assert!(
4809 matches!(
4810 err.downcast_other_ref::<TempoPoolTransactionError>(),
4811 Some(TempoPoolTransactionError::CreateCallNotFirst)
4812 ),
4813 "Expected CreateCallNotFirst error, got: {err:?}"
4814 );
4815 }
4816 _ => panic!("Expected Invalid outcome with CreateCallNotFirst error, got: {outcome:?}"),
4817 }
4818 }
4819
4820 #[tokio::test]
4822 async fn test_aa_create_call_with_authorization_list_rejected() {
4823 use alloy_eips::eip7702::Authorization;
4824 use alloy_primitives::Signature;
4825 use tempo_primitives::transaction::{
4826 TempoSignedAuthorization,
4827 tt_signature::{PrimitiveSignature, TempoSignature},
4828 };
4829
4830 let current_time = std::time::SystemTime::now()
4831 .duration_since(std::time::UNIX_EPOCH)
4832 .unwrap()
4833 .as_secs();
4834
4835 let calls = vec![Call {
4837 to: TxKind::Create, value: U256::ZERO,
4839 input: Default::default(),
4840 }];
4841
4842 let auth = Authorization {
4844 chain_id: U256::from(1),
4845 nonce: 0,
4846 address: address!("0000000000000000000000000000000000000001"),
4847 };
4848 let authorization = TempoSignedAuthorization::new_unchecked(
4849 auth,
4850 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
4851 );
4852
4853 let transaction = TxBuilder::aa(Address::random())
4854 .fee_token(address!("0000000000000000000000000000000000000002"))
4855 .calls(calls)
4856 .authorization_list(vec![authorization])
4857 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
4858 .build();
4859 let validator = setup_validator(&transaction, current_time);
4860
4861 let outcome = validator
4862 .validate_transaction(TransactionOrigin::External, transaction)
4863 .await;
4864
4865 match outcome {
4866 TransactionValidationOutcome::Invalid(_, ref err) => {
4867 assert!(
4868 matches!(
4869 err.downcast_other_ref::<TempoPoolTransactionError>(),
4870 Some(TempoPoolTransactionError::CreateCallWithAuthorizationList)
4871 ),
4872 "Expected CreateCallWithAuthorizationList error, got: {err:?}"
4873 );
4874 }
4875 _ => panic!(
4876 "Expected Invalid outcome with CreateCallWithAuthorizationList error, got: {outcome:?}"
4877 ),
4878 }
4879 }
4880
4881 #[test]
4883 fn test_paused_token_is_invalid_fee_token() {
4884 let fee_token = address!("20C0000000000000000000000000000000000001");
4885
4886 let usd_currency_value =
4888 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
4889
4890 let provider =
4891 MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
4892 provider.add_account(
4893 fee_token,
4894 ExtendedAccount::new(0, U256::ZERO).extend_storage([
4895 (tip20_slots::CURRENCY.into(), usd_currency_value),
4896 (tip20_slots::PAUSED.into(), U256::from(1)),
4897 ]),
4898 );
4899
4900 let mut state = provider.latest().unwrap();
4901 let spec = provider.chain_spec().tempo_hardfork_at(0);
4902
4903 let result = state.is_fee_token_paused(spec, fee_token);
4905 assert!(result.is_ok());
4906 assert!(
4907 result.unwrap(),
4908 "Paused tokens should be detected as paused"
4909 );
4910 }
4911
4912 #[tokio::test]
4915 async fn test_non_aa_intrinsic_gas_insufficient_rejected() {
4916 let current_time = std::time::SystemTime::now()
4917 .duration_since(std::time::UNIX_EPOCH)
4918 .unwrap()
4919 .as_secs();
4920
4921 let tx = TxBuilder::eip1559(Address::random())
4923 .gas_limit(1_000) .build_eip1559();
4925
4926 let validator = setup_validator(&tx, current_time);
4927 let outcome = validator
4928 .validate_transaction(TransactionOrigin::External, tx)
4929 .await;
4930
4931 match outcome {
4932 TransactionValidationOutcome::Invalid(_, ref err) => {
4933 assert!(
4934 matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
4935 "Expected IntrinsicGasTooLow error, got: {err:?}"
4936 );
4937 }
4938 TransactionValidationOutcome::Error(_, _) => {
4939 panic!("Expected Invalid outcome, got Error - this was the bug we fixed!")
4940 }
4941 _ => panic!("Expected Invalid outcome with IntrinsicGasTooLow, got: {outcome:?}"),
4942 }
4943 }
4944
4945 #[tokio::test]
4947 async fn test_non_aa_intrinsic_gas_sufficient_passes() {
4948 let current_time = std::time::SystemTime::now()
4949 .duration_since(std::time::UNIX_EPOCH)
4950 .unwrap()
4951 .as_secs();
4952
4953 let tx = TxBuilder::eip1559(Address::random())
4955 .gas_limit(100_000) .build_eip1559();
4957
4958 let validator = setup_validator(&tx, current_time);
4959 let outcome = validator
4960 .validate_transaction(TransactionOrigin::External, tx)
4961 .await;
4962
4963 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
4965 assert!(
4966 matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
4967 "Non-AA tx with 100k gas should NOT fail intrinsic gas check, got: {err:?}"
4968 );
4969 }
4970 }
4971
4972 #[tokio::test]
4975 async fn test_non_aa_tx_does_not_trigger_aa_intrinsic_gas_error() {
4976 let current_time = std::time::SystemTime::now()
4977 .duration_since(std::time::UNIX_EPOCH)
4978 .unwrap()
4979 .as_secs();
4980
4981 let tx = TxBuilder::eip1559(Address::random())
4983 .gas_limit(1_000)
4984 .build_eip1559();
4985
4986 let validator = setup_validator(&tx, current_time);
4987 let outcome = validator
4988 .validate_transaction(TransactionOrigin::External, tx)
4989 .await;
4990
4991 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
4993 assert!(
4994 !matches!(
4995 err.downcast_other_ref::<TempoPoolTransactionError>(),
4996 Some(TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost { .. })
4997 ),
4998 "Non-AA transaction should NOT trigger AA-specific intrinsic gas error"
4999 );
5000 }
5001 }
5002
5003 #[tokio::test]
5005 async fn test_intrinsic_gas_error_contains_gas_details() {
5006 let current_time = std::time::SystemTime::now()
5007 .duration_since(std::time::UNIX_EPOCH)
5008 .unwrap()
5009 .as_secs();
5010
5011 let gas_limit = 5_000u64;
5012 let tx = TxBuilder::eip1559(Address::random())
5013 .gas_limit(gas_limit)
5014 .build_eip1559();
5015
5016 let validator = setup_validator(&tx, current_time);
5017 let outcome = validator
5018 .validate_transaction(TransactionOrigin::External, tx)
5019 .await;
5020
5021 match outcome {
5022 TransactionValidationOutcome::Invalid(_, ref err) => {
5023 assert!(
5024 matches!(err, InvalidPoolTransactionError::IntrinsicGasTooLow),
5025 "Expected IntrinsicGasTooLow error, got: {err:?}"
5026 );
5027 }
5028 _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
5029 }
5030 }
5031
5032 #[test]
5034 fn test_paused_validator_token_rejected_before_liquidity_bypass() {
5035 let paused_validator_token = address!("20C0000000000000000000000000000000000001");
5037
5038 let usd_currency_value =
5040 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
5041
5042 let provider =
5043 MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(MODERATO.clone()));
5044
5045 provider.add_account(
5047 paused_validator_token,
5048 ExtendedAccount::new(0, U256::ZERO).extend_storage([
5049 (tip20_slots::CURRENCY.into(), usd_currency_value),
5050 (tip20_slots::PAUSED.into(), U256::from(1)),
5051 ]),
5052 );
5053
5054 let mut state = provider.latest().unwrap();
5055 let spec = provider.chain_spec().tempo_hardfork_at(0);
5056
5057 let amm_cache = AmmLiquidityCache::with_unique_tokens(vec![paused_validator_token]);
5061
5062 assert!(
5064 amm_cache.is_active_validator_token(&paused_validator_token),
5065 "Token should be in unique_tokens for this test"
5066 );
5067
5068 let liquidity_result =
5071 amm_cache.has_enough_liquidity(paused_validator_token, U256::from(1000), &state);
5072 assert!(
5073 liquidity_result.is_ok() && liquidity_result.unwrap(),
5074 "Token in unique_tokens should bypass liquidity check and return true"
5075 );
5076
5077 let is_paused = state.is_fee_token_paused(spec, paused_validator_token);
5079 assert!(is_paused.is_ok());
5080 assert!(
5081 is_paused.unwrap(),
5082 "Paused validator token should be detected by is_fee_token_paused BEFORE reaching has_enough_liquidity"
5083 );
5084 }
5085
5086 #[tokio::test]
5087 async fn test_aa_exactly_max_calls_accepted() {
5088 let current_time = std::time::SystemTime::now()
5089 .duration_since(std::time::UNIX_EPOCH)
5090 .unwrap()
5091 .as_secs();
5092
5093 let calls: Vec<Call> = (0..MAX_AA_CALLS)
5094 .map(|_| Call {
5095 to: TxKind::Call(Address::random()),
5096 value: U256::ZERO,
5097 input: Default::default(),
5098 })
5099 .collect();
5100
5101 let transaction = TxBuilder::aa(Address::random())
5102 .fee_token(address!("0000000000000000000000000000000000000002"))
5103 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5104 .calls(calls)
5105 .build();
5106 let validator = setup_validator(&transaction, current_time);
5107
5108 let outcome = validator
5109 .validate_transaction(TransactionOrigin::External, transaction)
5110 .await;
5111
5112 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5113 assert!(
5114 !matches!(
5115 err.downcast_other_ref::<TempoPoolTransactionError>(),
5116 Some(TempoPoolTransactionError::TooManyCalls { .. })
5117 ),
5118 "Exactly MAX_AA_CALLS calls should not trigger TooManyCalls, got: {err:?}"
5119 );
5120 }
5121 }
5122
5123 #[tokio::test]
5124 async fn test_aa_too_many_calls_rejected() {
5125 let current_time = std::time::SystemTime::now()
5126 .duration_since(std::time::UNIX_EPOCH)
5127 .unwrap()
5128 .as_secs();
5129
5130 let calls: Vec<Call> = (0..MAX_AA_CALLS + 1)
5131 .map(|_| Call {
5132 to: TxKind::Call(Address::random()),
5133 value: U256::ZERO,
5134 input: Default::default(),
5135 })
5136 .collect();
5137
5138 let transaction = TxBuilder::aa(Address::random())
5139 .fee_token(address!("0000000000000000000000000000000000000002"))
5140 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5141 .calls(calls)
5142 .build();
5143 let validator = setup_validator(&transaction, current_time);
5144
5145 let outcome = validator
5146 .validate_transaction(TransactionOrigin::External, transaction)
5147 .await;
5148
5149 match outcome {
5150 TransactionValidationOutcome::Invalid(_, ref err) => {
5151 assert!(
5152 matches!(
5153 err.downcast_other_ref::<TempoPoolTransactionError>(),
5154 Some(TempoPoolTransactionError::TooManyCalls { .. })
5155 ),
5156 "Expected TooManyCalls error, got: {err:?}"
5157 );
5158 }
5159 _ => panic!("Expected Invalid outcome with TooManyCalls error, got: {outcome:?}"),
5160 }
5161 }
5162
5163 #[tokio::test]
5164 async fn test_aa_exactly_max_call_input_size_accepted() {
5165 let current_time = std::time::SystemTime::now()
5166 .duration_since(std::time::UNIX_EPOCH)
5167 .unwrap()
5168 .as_secs();
5169
5170 let calls = vec![Call {
5171 to: TxKind::Call(Address::random()),
5172 value: U256::ZERO,
5173 input: vec![0u8; MAX_CALL_INPUT_SIZE].into(),
5174 }];
5175
5176 let transaction = TxBuilder::aa(Address::random())
5177 .fee_token(address!("0000000000000000000000000000000000000002"))
5178 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5179 .calls(calls)
5180 .build();
5181 let validator = setup_validator(&transaction, current_time);
5182
5183 let outcome = validator
5184 .validate_transaction(TransactionOrigin::External, transaction)
5185 .await;
5186
5187 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5188 assert!(
5189 !matches!(
5190 err.downcast_other_ref::<TempoPoolTransactionError>(),
5191 Some(TempoPoolTransactionError::CallInputTooLarge { .. })
5192 ),
5193 "Exactly MAX_CALL_INPUT_SIZE input should not trigger CallInputTooLarge, got: {err:?}"
5194 );
5195 }
5196 }
5197
5198 #[tokio::test]
5199 async fn test_aa_call_input_too_large_rejected() {
5200 let current_time = std::time::SystemTime::now()
5201 .duration_since(std::time::UNIX_EPOCH)
5202 .unwrap()
5203 .as_secs();
5204
5205 let calls = vec![Call {
5206 to: TxKind::Call(Address::random()),
5207 value: U256::ZERO,
5208 input: vec![0u8; MAX_CALL_INPUT_SIZE + 1].into(),
5209 }];
5210
5211 let transaction = TxBuilder::aa(Address::random())
5212 .fee_token(address!("0000000000000000000000000000000000000002"))
5213 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5214 .calls(calls)
5215 .build();
5216 let validator = setup_validator(&transaction, current_time);
5217
5218 let outcome = validator
5219 .validate_transaction(TransactionOrigin::External, transaction)
5220 .await;
5221
5222 match outcome {
5223 TransactionValidationOutcome::Invalid(_, ref err) => {
5224 let is_oversized = matches!(err, InvalidPoolTransactionError::OversizedData { .. });
5225 let is_call_input_too_large = matches!(
5226 err.downcast_other_ref::<TempoPoolTransactionError>(),
5227 Some(TempoPoolTransactionError::CallInputTooLarge { .. })
5228 );
5229 assert!(
5230 is_oversized || is_call_input_too_large,
5231 "Expected OversizedData or CallInputTooLarge error, got: {err:?}"
5232 );
5233 }
5234 _ => panic!("Expected Invalid outcome, got: {outcome:?}"),
5235 }
5236 }
5237
5238 #[tokio::test]
5239 async fn test_aa_exactly_max_access_list_accounts_accepted() {
5240 use alloy_eips::eip2930::{AccessList, AccessListItem};
5241
5242 let current_time = std::time::SystemTime::now()
5243 .duration_since(std::time::UNIX_EPOCH)
5244 .unwrap()
5245 .as_secs();
5246
5247 let items: Vec<AccessListItem> = (0..MAX_ACCESS_LIST_ACCOUNTS)
5248 .map(|_| AccessListItem {
5249 address: Address::random(),
5250 storage_keys: vec![],
5251 })
5252 .collect();
5253
5254 let transaction = TxBuilder::aa(Address::random())
5255 .fee_token(address!("0000000000000000000000000000000000000002"))
5256 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5257 .access_list(AccessList(items))
5258 .build();
5259 let validator = setup_validator(&transaction, current_time);
5260
5261 let outcome = validator
5262 .validate_transaction(TransactionOrigin::External, transaction)
5263 .await;
5264
5265 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5266 assert!(
5267 !matches!(
5268 err.downcast_other_ref::<TempoPoolTransactionError>(),
5269 Some(TempoPoolTransactionError::TooManyAccessListAccounts { .. })
5270 ),
5271 "Exactly MAX_ACCESS_LIST_ACCOUNTS should not trigger TooManyAccessListAccounts, got: {err:?}"
5272 );
5273 }
5274 }
5275
5276 #[tokio::test]
5277 async fn test_aa_too_many_access_list_accounts_rejected() {
5278 use alloy_eips::eip2930::{AccessList, AccessListItem};
5279
5280 let current_time = std::time::SystemTime::now()
5281 .duration_since(std::time::UNIX_EPOCH)
5282 .unwrap()
5283 .as_secs();
5284
5285 let items: Vec<AccessListItem> = (0..MAX_ACCESS_LIST_ACCOUNTS + 1)
5286 .map(|_| AccessListItem {
5287 address: Address::random(),
5288 storage_keys: vec![],
5289 })
5290 .collect();
5291
5292 let transaction = TxBuilder::aa(Address::random())
5293 .fee_token(address!("0000000000000000000000000000000000000002"))
5294 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5295 .access_list(AccessList(items))
5296 .build();
5297 let validator = setup_validator(&transaction, current_time);
5298
5299 let outcome = validator
5300 .validate_transaction(TransactionOrigin::External, transaction)
5301 .await;
5302
5303 match outcome {
5304 TransactionValidationOutcome::Invalid(_, ref err) => {
5305 assert!(
5306 matches!(
5307 err.downcast_other_ref::<TempoPoolTransactionError>(),
5308 Some(TempoPoolTransactionError::TooManyAccessListAccounts { .. })
5309 ),
5310 "Expected TooManyAccessListAccounts error, got: {err:?}"
5311 );
5312 }
5313 _ => panic!(
5314 "Expected Invalid outcome with TooManyAccessListAccounts error, got: {outcome:?}"
5315 ),
5316 }
5317 }
5318
5319 #[tokio::test]
5320 async fn test_aa_exactly_max_storage_keys_per_account_accepted() {
5321 use alloy_eips::eip2930::{AccessList, AccessListItem};
5322
5323 let current_time = std::time::SystemTime::now()
5324 .duration_since(std::time::UNIX_EPOCH)
5325 .unwrap()
5326 .as_secs();
5327
5328 let items = vec![AccessListItem {
5329 address: Address::random(),
5330 storage_keys: (0..MAX_STORAGE_KEYS_PER_ACCOUNT)
5331 .map(|_| B256::random())
5332 .collect(),
5333 }];
5334
5335 let transaction = TxBuilder::aa(Address::random())
5336 .fee_token(address!("0000000000000000000000000000000000000002"))
5337 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5338 .access_list(AccessList(items))
5339 .build();
5340 let validator = setup_validator(&transaction, current_time);
5341
5342 let outcome = validator
5343 .validate_transaction(TransactionOrigin::External, transaction)
5344 .await;
5345
5346 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5347 assert!(
5348 !matches!(
5349 err.downcast_other_ref::<TempoPoolTransactionError>(),
5350 Some(TempoPoolTransactionError::TooManyStorageKeysPerAccount { .. })
5351 ),
5352 "Exactly MAX_STORAGE_KEYS_PER_ACCOUNT should not trigger TooManyStorageKeysPerAccount, got: {err:?}"
5353 );
5354 }
5355 }
5356
5357 #[tokio::test]
5358 async fn test_aa_too_many_storage_keys_per_account_rejected() {
5359 use alloy_eips::eip2930::{AccessList, AccessListItem};
5360
5361 let current_time = std::time::SystemTime::now()
5362 .duration_since(std::time::UNIX_EPOCH)
5363 .unwrap()
5364 .as_secs();
5365
5366 let items = vec![AccessListItem {
5367 address: Address::random(),
5368 storage_keys: (0..MAX_STORAGE_KEYS_PER_ACCOUNT + 1)
5369 .map(|_| B256::random())
5370 .collect(),
5371 }];
5372
5373 let transaction = TxBuilder::aa(Address::random())
5374 .fee_token(address!("0000000000000000000000000000000000000002"))
5375 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5376 .access_list(AccessList(items))
5377 .build();
5378 let validator = setup_validator(&transaction, current_time);
5379
5380 let outcome = validator
5381 .validate_transaction(TransactionOrigin::External, transaction)
5382 .await;
5383
5384 match outcome {
5385 TransactionValidationOutcome::Invalid(_, ref err) => {
5386 assert!(
5387 matches!(
5388 err.downcast_other_ref::<TempoPoolTransactionError>(),
5389 Some(TempoPoolTransactionError::TooManyStorageKeysPerAccount { .. })
5390 ),
5391 "Expected TooManyStorageKeysPerAccount error, got: {err:?}"
5392 );
5393 }
5394 _ => panic!(
5395 "Expected Invalid outcome with TooManyStorageKeysPerAccount error, got: {outcome:?}"
5396 ),
5397 }
5398 }
5399
5400 #[tokio::test]
5401 async fn test_aa_exactly_max_total_storage_keys_accepted() {
5402 use alloy_eips::eip2930::{AccessList, AccessListItem};
5403
5404 let current_time = std::time::SystemTime::now()
5405 .duration_since(std::time::UNIX_EPOCH)
5406 .unwrap()
5407 .as_secs();
5408
5409 let keys_per_account = MAX_STORAGE_KEYS_PER_ACCOUNT;
5410 let num_accounts = MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL / keys_per_account;
5411 let items: Vec<AccessListItem> = (0..num_accounts)
5412 .map(|_| AccessListItem {
5413 address: Address::random(),
5414 storage_keys: (0..keys_per_account).map(|_| B256::random()).collect(),
5415 })
5416 .collect();
5417 assert_eq!(
5418 items.iter().map(|i| i.storage_keys.len()).sum::<usize>(),
5419 MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL
5420 );
5421
5422 let transaction = TxBuilder::aa(Address::random())
5423 .fee_token(address!("0000000000000000000000000000000000000002"))
5424 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5425 .access_list(AccessList(items))
5426 .build();
5427 let validator = setup_validator(&transaction, current_time);
5428
5429 let outcome = validator
5430 .validate_transaction(TransactionOrigin::External, transaction)
5431 .await;
5432
5433 if let TransactionValidationOutcome::Invalid(_, ref err) = outcome {
5434 assert!(
5435 !matches!(
5436 err.downcast_other_ref::<TempoPoolTransactionError>(),
5437 Some(TempoPoolTransactionError::TooManyTotalStorageKeys { .. })
5438 ),
5439 "Exactly MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL should not trigger TooManyTotalStorageKeys, got: {err:?}"
5440 );
5441 }
5442 }
5443
5444 #[tokio::test]
5445 async fn test_aa_too_many_total_storage_keys_rejected() {
5446 use alloy_eips::eip2930::{AccessList, AccessListItem};
5447
5448 let current_time = std::time::SystemTime::now()
5449 .duration_since(std::time::UNIX_EPOCH)
5450 .unwrap()
5451 .as_secs();
5452
5453 let keys_per_account = MAX_STORAGE_KEYS_PER_ACCOUNT;
5454 let num_accounts = MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL / keys_per_account;
5455 let mut items: Vec<AccessListItem> = (0..num_accounts)
5456 .map(|_| AccessListItem {
5457 address: Address::random(),
5458 storage_keys: (0..keys_per_account).map(|_| B256::random()).collect(),
5459 })
5460 .collect();
5461 items.push(AccessListItem {
5462 address: Address::random(),
5463 storage_keys: vec![B256::random()],
5464 });
5465 assert_eq!(
5466 items.iter().map(|i| i.storage_keys.len()).sum::<usize>(),
5467 MAX_ACCESS_LIST_STORAGE_KEYS_TOTAL + 1
5468 );
5469
5470 let transaction = TxBuilder::aa(Address::random())
5471 .fee_token(address!("0000000000000000000000000000000000000002"))
5472 .gas_limit(TEMPO_T1_TX_GAS_LIMIT_CAP)
5473 .access_list(AccessList(items))
5474 .build();
5475 let validator = setup_validator(&transaction, current_time);
5476
5477 let outcome = validator
5478 .validate_transaction(TransactionOrigin::External, transaction)
5479 .await;
5480
5481 match outcome {
5482 TransactionValidationOutcome::Invalid(_, ref err) => {
5483 assert!(
5484 matches!(
5485 err.downcast_other_ref::<TempoPoolTransactionError>(),
5486 Some(TempoPoolTransactionError::TooManyTotalStorageKeys { .. })
5487 ),
5488 "Expected TooManyTotalStorageKeys error, got: {err:?}"
5489 );
5490 }
5491 _ => panic!(
5492 "Expected Invalid outcome with TooManyTotalStorageKeys error, got: {outcome:?}"
5493 ),
5494 }
5495 }
5496
5497 #[test]
5498 fn test_ensure_intrinsic_gas_tempo_tx_below_intrinsic_gas() {
5499 use tempo_chainspec::hardfork::TempoHardfork;
5500
5501 let tx = TxBuilder::eip1559(Address::random())
5502 .gas_limit(1)
5503 .build_eip1559();
5504
5505 let result = ensure_intrinsic_gas_tempo_tx(&tx, TempoHardfork::T1);
5506 assert!(
5507 matches!(result, Err(InvalidPoolTransactionError::IntrinsicGasTooLow)),
5508 "Expected IntrinsicGasTooLow, got: {result:?}"
5509 );
5510 }
5511
5512 #[test]
5513 fn test_ensure_intrinsic_gas_tempo_tx_exactly_at_intrinsic_gas() {
5514 use tempo_chainspec::hardfork::TempoHardfork;
5515 use tempo_revm::gas_params::tempo_gas_params;
5516
5517 let spec = TempoHardfork::T1;
5518 let tx_probe = TxBuilder::eip1559(Address::random())
5519 .gas_limit(1_000_000)
5520 .build_eip1559();
5521
5522 let gas_params = tempo_gas_params(spec);
5523 let mut gas = gas_params.initial_tx_gas(
5524 tx_probe.input(),
5525 tx_probe.is_create(),
5526 tx_probe.access_list().map(|l| l.len()).unwrap_or_default() as u64,
5527 tx_probe
5528 .access_list()
5529 .map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
5530 .unwrap_or_default() as u64,
5531 tx_probe
5532 .authorization_list()
5533 .map(|l| l.len())
5534 .unwrap_or_default() as u64,
5535 );
5536 if spec.is_t1() && tx_probe.nonce() == 0 {
5537 gas.initial_gas += gas_params.get(GasId::new_account_cost());
5538 }
5539 let intrinsic = std::cmp::max(gas.initial_gas, gas.floor_gas);
5540
5541 let tx_exact = TxBuilder::eip1559(Address::random())
5542 .gas_limit(intrinsic)
5543 .build_eip1559();
5544
5545 let result = ensure_intrinsic_gas_tempo_tx(&tx_exact, spec);
5546 assert!(
5547 result.is_ok(),
5548 "Gas limit exactly at intrinsic gas should pass, got: {result:?}"
5549 );
5550
5551 let tx_below = TxBuilder::eip1559(Address::random())
5552 .gas_limit(intrinsic - 1)
5553 .build_eip1559();
5554
5555 let result = ensure_intrinsic_gas_tempo_tx(&tx_below, spec);
5556 assert!(
5557 matches!(result, Err(InvalidPoolTransactionError::IntrinsicGasTooLow)),
5558 "Gas limit one below intrinsic gas should fail, got: {result:?}"
5559 );
5560 }
5561}