Skip to main content

tempo_transaction_pool/
tempo_pool.rs

1// Tempo transaction pool that implements Reth's TransactionPool trait
2// Routes protocol nonces (nonce_key=0) to Reth pool
3// Routes user nonces (nonce_key>0) to minimal 2D nonce pool
4
5use crate::{
6    amm::AmmLiquidityCache, best::MergeBestTransactions, ordering::TempoTipOrdering,
7    transaction::TempoPooledTransaction, tt_2d_pool::AA2dPool,
8    validator::TempoTransactionValidator,
9};
10use alloy_consensus::Transaction;
11use alloy_primitives::{
12    Address, B256, TxHash, U256,
13    map::{AddressMap, AddressSet, Entry, HashMap},
14};
15use parking_lot::RwLock;
16use reth_chainspec::ChainSpecProvider;
17use reth_eth_wire_types::HandleMempoolData;
18use reth_provider::{ChangedAccount, StateProviderFactory};
19use reth_storage_api::StateProvider;
20use reth_transaction_pool::{
21    AddedTransactionOutcome, AllPoolTransactions, BestTransactions, BestTransactionsAttributes,
22    BlockInfo, CanonicalStateUpdate, GetPooledTransactionLimit, NewBlobSidecar, Pool, PoolResult,
23    PoolSize, PoolTransaction, PropagatedTransactions, TransactionEvents, TransactionOrigin,
24    TransactionPool, TransactionPoolExt, TransactionValidationOutcome,
25    TransactionValidationTaskExecutor, TransactionValidator, ValidPoolTransaction,
26    blobstore::InMemoryBlobStore,
27    error::{PoolError, PoolErrorKind},
28    identifier::TransactionId,
29};
30use revm::database::BundleAccount;
31use std::{sync::Arc, time::Instant};
32use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardfork};
33use tempo_precompiles::{
34    TIP_FEE_MANAGER_ADDRESS,
35    account_keychain::AccountKeychain,
36    error::Result as TempoPrecompileResult,
37    storage::{Handler, StorageActions},
38    tip20::TIP20Token,
39    tip403_registry::{REJECT_ALL_POLICY_ID, TIP403Registry},
40};
41use tempo_primitives::Block;
42use tempo_revm::TempoStateAccess;
43
44/// Tempo transaction pool that routes based on nonce_key
45pub struct TempoTransactionPool<Client> {
46    /// Vanilla pool for all standard transactions and AA transactions with regular nonce.
47    protocol_pool: Pool<
48        TransactionValidationTaskExecutor<TempoTransactionValidator<Client>>,
49        TempoTipOrdering<TempoPooledTransaction>,
50        InMemoryBlobStore,
51    >,
52    /// Minimal pool for 2D nonces (nonce_key > 0)
53    aa_2d_pool: Arc<RwLock<AA2dPool>>,
54}
55
56impl<Client> TempoTransactionPool<Client>
57where
58    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + 'static,
59{
60    pub fn new(
61        protocol_pool: Pool<
62            TransactionValidationTaskExecutor<TempoTransactionValidator<Client>>,
63            TempoTipOrdering<TempoPooledTransaction>,
64            InMemoryBlobStore,
65        >,
66        mut aa_2d_pool: AA2dPool,
67    ) -> Self {
68        aa_2d_pool.set_base_fee(protocol_pool.inner().block_info().pending_basefee);
69        Self {
70            protocol_pool,
71            aa_2d_pool: Arc::new(RwLock::new(aa_2d_pool)),
72        }
73    }
74}
75impl<Client> TempoTransactionPool<Client>
76where
77    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + 'static,
78{
79    /// Obtains a clone of the shared [`AmmLiquidityCache`].
80    pub fn amm_liquidity_cache(&self) -> AmmLiquidityCache {
81        self.protocol_pool
82            .validator()
83            .validator()
84            .amm_liquidity_cache()
85    }
86
87    /// Returns the configured client
88    pub fn client(&self) -> &Client {
89        self.protocol_pool.validator().validator().client()
90    }
91
92    /// Updates the 2d nonce pool with the given state changes.
93    ///
94    /// Returns mined AA transactions.
95    pub(crate) fn notify_aa_pool_on_state_updates(
96        &self,
97        state: &AddressMap<BundleAccount>,
98    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
99        let (promoted, mined) = self.aa_2d_pool.write().on_state_updates(state);
100        // Note: mined transactions are notified via the vanilla pool updates
101        self.protocol_pool
102            .inner()
103            .notify_on_transaction_updates(promoted, Vec::new());
104        mined
105    }
106
107    /// Evicts transactions that are no longer valid due to on-chain events.
108    ///
109    /// This performs a single scan of all pooled transactions and checks for:
110    /// 1. **Revoked keychain keys**: AA transactions signed with keys that have been revoked
111    /// 2. **Spending limit updates**: AA transactions signed with keys whose spending limit
112    ///    changed for a token matching the transaction's fee token
113    ///    2b. **Spending limit spends**: AA transactions whose remaining spending limit (re-read
114    ///    from state) is now insufficient after included keychain txs decremented it
115    ///    2c. **Key-authorization witness burns**: AA transactions with a witness-bearing
116    ///    inline key authorization whose `(account, witness)` has been manually burned
117    /// 3. **Validator token changes**: Transactions that would fail due to insufficient
118    ///    liquidity in the new (user_token, validator_token) AMM pool
119    /// 4. **Fee payer balance changes**: Transactions whose fee payer no longer has enough
120    ///    balance in the resolved fee token after a TIP20 transfer
121    ///
122    /// All checks are combined into one scan to avoid iterating the pool multiple times
123    /// per block.
124    pub fn evict_invalidated_transactions(
125        &self,
126        updates: &crate::maintain::TempoPoolUpdates,
127    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
128        if !updates.has_invalidation_events() {
129            return Vec::new();
130        }
131
132        let all_txs = self.all_transactions();
133        self.evict_invalidated_transactions_from(updates, all_txs.iter())
134    }
135
136    /// See [`Self::evict_invalidated_transactions`]; returns the removed transactions so
137    /// the caller controls when they are dropped.
138    pub(crate) fn evict_invalidated_transactions_from<'a>(
139        &self,
140        updates: &crate::maintain::TempoPoolUpdates,
141        transactions: impl IntoIterator<Item = &'a Arc<ValidPoolTransaction<TempoPooledTransaction>>>,
142    ) -> Vec<Arc<ValidPoolTransaction<TempoPooledTransaction>>> {
143        if !updates.has_invalidation_events() {
144            return Vec::new();
145        }
146
147        // Fetch state provider if any check needs on-chain reads:
148        // - validator token changes (liquidity check)
149        // - blacklist/whitelist (policy check)
150        // - fee payer balance changes (balance check)
151        // - spending limit spends (remaining limit check)
152        let mut state_provider = if !updates.validator_token_changes.is_empty()
153            || !updates.blacklist_additions.is_empty()
154            || !updates.whitelist_removals.is_empty()
155            || !updates.fee_balance_changes.is_empty()
156            || !updates.spending_limit_spends.is_empty()
157        {
158            self.client().latest().ok()
159        } else {
160            None
161        };
162
163        // Resolve the active hardfork for storage context.
164        let tip_timestamp = self
165            .protocol_pool
166            .validator()
167            .validator()
168            .inner
169            .fork_tracker()
170            .tip_timestamp();
171        let spec = self.protocol_pool.validator().validator().active_hardfork();
172
173        // Cache policy lookups per fee token to avoid redundant storage reads.
174        // For compound policies (TIP-1015), the cache stores all sub-policy IDs
175        // so eviction matches events emitted with sub-policy IDs.
176        let mut policy_cache: AddressMap<Vec<u64>> = AddressMap::default();
177
178        // Pre-collect policy IDs where TIP_FEE_MANAGER_ADDRESS (the fee recipient) was
179        // blacklisted or un-whitelisted. This is constant across all txs so we compute
180        // it once instead of re-scanning the updates list per transaction.
181        let fee_manager_blacklisted: Vec<u64> = updates
182            .blacklist_additions
183            .iter()
184            .filter(|(_, account)| *account == TIP_FEE_MANAGER_ADDRESS)
185            .map(|(policy_id, _)| *policy_id)
186            .collect();
187        let fee_manager_unwhitelisted: Vec<u64> = updates
188            .whitelist_removals
189            .iter()
190            .filter(|(_, account)| *account == TIP_FEE_MANAGER_ADDRESS)
191            .map(|(policy_id, _)| *policy_id)
192            .collect();
193
194        // Re-check liquidity for all pooled txs when an active validator changes token.
195        // Leverages the per-tx `has_enough_liquidity` check, which passes if ANY validator pair has
196        // enough liquidity, matching admission and preventing mass-eviction of valid txs.
197        let amm_cache = self.amm_liquidity_cache();
198        let has_active_validator_token_changes = !updates.validator_token_changes.is_empty() && {
199            let active_new_tokens: Vec<_> = updates
200                .validator_token_changes
201                .iter()
202                .filter(|(validator, _)| amm_cache.is_active_validator(validator))
203                .filter(|(_, new_token)| !amm_cache.is_active_validator_token(new_token))
204                .map(|(_, &new_token)| new_token)
205                .collect();
206            amm_cache.track_tokens(&active_new_tokens)
207        };
208
209        let mut to_remove = Vec::new();
210        let mut revoked_count = 0;
211        let mut key_authorization_target_count = 0;
212        let mut spending_limit_count = 0;
213        let mut spending_limit_spend_count = 0;
214        let mut key_authorization_witness_count = 0;
215        let mut liquidity_count = 0;
216        let mut user_token_count = 0;
217        let mut blacklisted_count = 0;
218        let mut unwhitelisted_count = 0;
219        let mut insolvent_fee_payer_count = 0;
220        let has_keychain_subject_updates = updates.has_keychain_subject_updates();
221        let has_key_authorization_target_updates =
222            !updates.key_authorization_target_changes.is_empty();
223        let mut fee_balance_cache: HashMap<(Address, Address), U256> = HashMap::default();
224
225        for tx in transactions {
226            // Avoid recovering key ids unless a keychain invalidation can use them.
227            if has_keychain_subject_updates || has_key_authorization_target_updates {
228                let keychain_subject = has_keychain_subject_updates
229                    .then(|| tx.transaction.keychain_subject())
230                    .flatten();
231                let key_authorization_subject = (!updates.revoked_keys.is_empty())
232                    .then(|| tx.transaction.key_authorization_signer_subject())
233                    .flatten();
234                let key_authorization_target = has_key_authorization_target_updates
235                    .then(|| tx.transaction.key_authorization_target_subject())
236                    .flatten();
237
238                // Check 1: Revoked keychain keys
239                if !updates.revoked_keys.is_empty()
240                    && (keychain_subject
241                        .as_ref()
242                        .is_some_and(|subject| subject.matches_revoked(&updates.revoked_keys))
243                        || key_authorization_subject
244                            .as_ref()
245                            .is_some_and(|subject| subject.matches_revoked(&updates.revoked_keys)))
246                {
247                    to_remove.push(*tx.hash());
248                    revoked_count += 1;
249                    continue;
250                }
251
252                // Check 1b: Inline key authorization target status changes
253                if !updates.key_authorization_target_changes.is_empty()
254                    && key_authorization_target.as_ref().is_some_and(|subject| {
255                        subject.matches_key_update(&updates.key_authorization_target_changes)
256                    })
257                {
258                    to_remove.push(*tx.hash());
259                    key_authorization_target_count += 1;
260                    continue;
261                }
262
263                // Check 2: Spending limit updates
264                // Only evict if the transaction's fee token matches the token whose limit changed.
265                if !updates.spending_limit_changes.is_empty()
266                    && let Some(ref subject) = keychain_subject
267                    && subject.matches_spending_limit_update(&updates.spending_limit_changes)
268                    && tx.transaction.is_sender_paid_fee()
269                {
270                    to_remove.push(*tx.hash());
271                    spending_limit_count += 1;
272                    continue;
273                }
274
275                // Check 2b: Spending limit spends
276                // AccessKeySpend receipt logs identify the exact (account, key_id, token)
277                // triples whose remaining limit changed during execution. We re-read the
278                // current remaining limit from state for matching pending txs and evict if
279                // the tx's fee cost now exceeds that remaining limit.
280                if !updates.spending_limit_spends.is_empty()
281                    && let Some(ref subject) = keychain_subject
282                    && subject.matches_spending_limit_update(&updates.spending_limit_spends)
283                    && tx.transaction.is_sender_paid_fee()
284                    && let Some(ref mut provider) = state_provider
285                    && exceeds_spending_limit(
286                        provider,
287                        subject,
288                        tx.transaction.fee_token_cost(),
289                        tip_timestamp,
290                        spec,
291                    )
292                {
293                    to_remove.push(*tx.hash());
294                    spending_limit_spend_count += 1;
295                    continue;
296                }
297            }
298
299            // Check 2c: TIP-1053 key-authorization witness burns
300            if !updates.key_authorization_witness_burns.is_empty()
301                && let Some(subject) = tx.transaction.key_authorization_witness_subject()
302                && updates
303                    .key_authorization_witness_burns
304                    .get(&subject.account)
305                    .is_some_and(|witnesses| witnesses.contains(&subject.witness))
306            {
307                to_remove.push(*tx.hash());
308                key_authorization_witness_count += 1;
309                continue;
310            }
311
312            // Check 3: Validator token changes (re-check liquidity for all transactions)
313            // Prevents mass eviction because it only:
314            // - evicts when NO validator token has enough liquidity
315            // - considers active validators (protects from permissionless `setValidatorToken`)
316            if has_active_validator_token_changes && let Some(ref provider) = state_provider {
317                let user_token = tx.transaction.effective_fee_token();
318                let cost = tx.transaction.fee_token_cost();
319
320                match amm_cache.has_enough_liquidity(user_token, cost, provider) {
321                    Ok(true) => {}
322                    Ok(false) => {
323                        to_remove.push(*tx.hash());
324                        liquidity_count += 1;
325                        continue;
326                    }
327                    Err(_) => continue,
328                }
329            }
330
331            // Check 3b: Fee payer balance changes.
332            // When a TIP20 transfer changes a fee payer's balance, pending transactions for that
333            // (fee_token, fee_payer) pair may no longer be executable.
334            if !updates.fee_balance_changes.is_empty()
335                && let Some(ref mut provider) = state_provider
336            {
337                let fee_token = tx.transaction.effective_fee_token();
338                // only resolve the fee payer if the fee token saw balance changes
339                if let Some(accounts) = updates.fee_balance_changes.get(&fee_token) {
340                    let Ok(fee_payer) = tx.transaction.fee_payer() else {
341                        continue;
342                    };
343
344                    if accounts.contains(&fee_payer) {
345                        let balance = match fee_balance_cache.entry((fee_token, fee_payer)) {
346                            Entry::Occupied(entry) => *entry.get(),
347                            Entry::Vacant(entry) => {
348                                let Ok(balance) = provider.get_token_balance(
349                                    fee_token,
350                                    fee_payer,
351                                    spec,
352                                    StorageActions::disabled(),
353                                ) else {
354                                    continue;
355                                };
356                                *entry.insert(balance)
357                            }
358                        };
359
360                        if balance < tx.transaction.fee_token_cost() {
361                            to_remove.push(*tx.hash());
362                            insolvent_fee_payer_count += 1;
363                            continue;
364                        }
365                    }
366                }
367            }
368
369            // Check 4: Blacklisted fee payers.
370            // AA transactions use their recovered fee payer; non-AA transactions use their sender.
371            if !updates.blacklist_additions.is_empty()
372                && let Some(ref mut provider) = state_provider
373            {
374                let fee_token = tx.transaction.effective_fee_token();
375                let fee_payer = tx
376                    .transaction
377                    .fee_payer()
378                    .unwrap_or_else(|_| tx.transaction.sender());
379
380                // Check if any blacklist addition applies to this transaction's fee payer
381                let mut sender_evicted = false;
382                for &(blacklist_policy_id, blacklisted_account) in &updates.blacklist_additions {
383                    if fee_payer != blacklisted_account {
384                        continue;
385                    }
386
387                    let token_policies =
388                        get_sender_policy_ids(provider, fee_token, spec, &mut policy_cache);
389
390                    if token_policies
391                        .as_ref()
392                        .is_some_and(|ids| ids.contains(&blacklist_policy_id))
393                    {
394                        sender_evicted = true;
395                        break;
396                    }
397                }
398
399                // Check if the fee manager (recipient) was blacklisted on this token's
400                // recipient policy — the tx would fail at execution since the fee
401                // transfer to TIP_FEE_MANAGER_ADDRESS would be rejected.
402                let recipient_evicted = !sender_evicted
403                    && !fee_manager_blacklisted.is_empty()
404                    && get_recipient_policy_ids(provider, fee_token, spec)
405                        .is_some_and(|ids| fee_manager_blacklisted.iter().any(|p| ids.contains(p)));
406
407                if sender_evicted || recipient_evicted {
408                    to_remove.push(*tx.hash());
409                    blacklisted_count += 1;
410                }
411            }
412
413            // Check 5: Un-whitelisted fee payers.
414            // When a fee payer or sender is removed from a whitelist, their pending
415            // transactions will fail validation at execution time.
416            if !updates.whitelist_removals.is_empty()
417                && let Some(ref mut provider) = state_provider
418            {
419                let fee_token = tx.transaction.effective_fee_token();
420                let fee_payer = tx
421                    .transaction
422                    .fee_payer()
423                    .unwrap_or_else(|_| tx.transaction.sender());
424
425                let mut sender_evicted = false;
426                for &(whitelist_policy_id, unwhitelisted_account) in &updates.whitelist_removals {
427                    if fee_payer != unwhitelisted_account {
428                        continue;
429                    }
430
431                    let token_policies =
432                        get_sender_policy_ids(provider, fee_token, spec, &mut policy_cache);
433
434                    if token_policies
435                        .as_ref()
436                        .is_some_and(|ids| ids.contains(&whitelist_policy_id))
437                    {
438                        sender_evicted = true;
439                        break;
440                    }
441                }
442
443                // Check if the fee manager (recipient) was un-whitelisted on this
444                // token's recipient policy.
445                let recipient_evicted = !sender_evicted
446                    && !fee_manager_unwhitelisted.is_empty()
447                    && get_recipient_policy_ids(provider, fee_token, spec).is_some_and(|ids| {
448                        fee_manager_unwhitelisted.iter().any(|p| ids.contains(p))
449                    });
450
451                if sender_evicted || recipient_evicted {
452                    to_remove.push(*tx.hash());
453                    unwhitelisted_count += 1;
454                }
455            }
456
457            // Check 6: User fee token preference changes
458            // When a fee payer changes their fee token preference via setUserToken(),
459            // transactions paid by that account that don't have an explicit fee_token set may
460            // now resolve to a different token at execution time, causing fee payment failures.
461            // Only evict transactions WITHOUT an explicit fee_token (those that rely on storage).
462            if !updates.user_token_changes.is_empty()
463                && tx.transaction.inner().fee_token().is_none()
464                && tx
465                    .transaction
466                    .fee_payer()
467                    .is_ok_and(|fee_payer| updates.user_token_changes.contains(&fee_payer))
468            {
469                to_remove.push(*tx.hash());
470                user_token_count += 1;
471            }
472        }
473
474        if to_remove.is_empty() {
475            return Vec::new();
476        }
477
478        tracing::debug!(
479            target: "txpool",
480            total = to_remove.len(),
481            revoked_count,
482            key_authorization_target_count,
483            spending_limit_count,
484            spending_limit_spend_count,
485            key_authorization_witness_count,
486            liquidity_count,
487            user_token_count,
488            blacklisted_count,
489            unwhitelisted_count,
490            insolvent_fee_payer_count,
491            "Evicting invalidated transactions"
492        );
493        self.remove_transactions(to_remove)
494    }
495
496    /// Adds a validated transaction to the subpool derived from its type and nonce key.
497    ///
498    /// [`TempoPooledTransaction::is_aa_2d`] routes AA transactions with non-zero
499    /// nonce keys, including expiring nonces, to the 2D nonce pool. Everything else
500    /// stays in the protocol pool.
501    fn add_validated_transaction(
502        &self,
503        origin: TransactionOrigin,
504        transaction: TransactionValidationOutcome<TempoPooledTransaction>,
505    ) -> PoolResult<AddedTransactionOutcome> {
506        match transaction {
507            TransactionValidationOutcome::Valid {
508                balance,
509                state_nonce,
510                bytecode_hash,
511                transaction,
512                propagate,
513                authorities,
514            } => {
515                if transaction.transaction().is_aa_2d() {
516                    let transaction = transaction.into_transaction();
517                    let sender_id = self
518                        .protocol_pool
519                        .inner()
520                        .get_sender_id(transaction.sender());
521                    let transaction_id = TransactionId::new(sender_id, transaction.nonce());
522                    let tx = ValidPoolTransaction {
523                        transaction,
524                        transaction_id,
525                        propagate,
526                        timestamp: Instant::now(),
527                        origin,
528                        authority_ids: authorities
529                            .map(|auths| self.protocol_pool.inner().get_sender_ids(auths)),
530                    };
531
532                    // Get the active Tempo hardfork for expiring nonce handling
533                    let hardfork = self.protocol_pool.validator().validator().active_hardfork();
534
535                    let tx = Arc::new(tx);
536                    let added =
537                        self.aa_2d_pool
538                            .write()
539                            .add_transaction(tx, state_nonce, hardfork)?;
540                    let hash = *added.hash();
541                    if let Some(pending) = added.as_pending() {
542                        if pending.discarded.iter().any(|tx| *tx.hash() == hash) {
543                            return Err(PoolError::new(hash, PoolErrorKind::DiscardedOnInsert));
544                        }
545                        self.protocol_pool
546                            .inner()
547                            .on_new_pending_transaction(pending);
548                    }
549
550                    let state = added.transaction_state();
551                    // notify regular event listeners from the protocol pool
552                    self.protocol_pool.inner().notify_event_listeners(&added);
553                    self.protocol_pool
554                        .inner()
555                        .on_new_transaction(added.into_new_transaction_event());
556
557                    Ok(AddedTransactionOutcome { hash, state })
558                } else {
559                    self.protocol_pool
560                        .inner()
561                        .add_transactions(
562                            origin,
563                            std::iter::once(TransactionValidationOutcome::Valid {
564                                balance,
565                                state_nonce,
566                                bytecode_hash,
567                                transaction,
568                                propagate,
569                                authorities,
570                            }),
571                        )
572                        .pop()
573                        .unwrap()
574                }
575            }
576            invalid => {
577                // this forwards for event listener updates
578                self.protocol_pool
579                    .inner()
580                    .add_transactions(origin, Some(invalid))
581                    .pop()
582                    .unwrap()
583            }
584        }
585    }
586}
587
588// Manual Clone implementation
589impl<Client> Clone for TempoTransactionPool<Client> {
590    fn clone(&self) -> Self {
591        Self {
592            protocol_pool: self.protocol_pool.clone(),
593            aa_2d_pool: Arc::clone(&self.aa_2d_pool),
594        }
595    }
596}
597
598// Manual Debug implementation
599impl<Client> std::fmt::Debug for TempoTransactionPool<Client> {
600    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
601        f.debug_struct("TempoTransactionPool")
602            .field("protocol_pool", &"Pool<...>")
603            .field("aa_2d_nonce_pool", &"AA2dPool<...>")
604            .field("paused_fee_token_pool", &"PausedFeeTokenPool<...>")
605            .finish_non_exhaustive()
606    }
607}
608
609// Implement the TransactionPool trait
610impl<Client> TransactionPool for TempoTransactionPool<Client>
611where
612    Client: StateProviderFactory
613        + ChainSpecProvider<ChainSpec = TempoChainSpec>
614        + Send
615        + Sync
616        + 'static,
617    TempoPooledTransaction: reth_transaction_pool::EthPoolTransaction,
618{
619    type Transaction = TempoPooledTransaction;
620
621    fn pool_size(&self) -> PoolSize {
622        let mut size = self.protocol_pool.pool_size();
623        let (pending, queued) = self.aa_2d_pool.read().pending_and_queued_txn_count();
624        size.pending += pending;
625        size.queued += queued;
626        size
627    }
628
629    fn block_info(&self) -> BlockInfo {
630        self.protocol_pool.block_info()
631    }
632
633    async fn add_transaction_and_subscribe(
634        &self,
635        origin: TransactionOrigin,
636        transaction: Self::Transaction,
637    ) -> PoolResult<TransactionEvents> {
638        let tx = self
639            .protocol_pool
640            .validator()
641            .validate_transaction(origin, transaction)
642            .await;
643        let res = self.add_validated_transaction(origin, tx)?;
644        self.transaction_event_listener(res.hash)
645            .ok_or_else(|| PoolError::new(res.hash, PoolErrorKind::DiscardedOnInsert))
646    }
647
648    async fn add_transaction(
649        &self,
650        origin: TransactionOrigin,
651        transaction: Self::Transaction,
652    ) -> PoolResult<AddedTransactionOutcome> {
653        let tx = self
654            .protocol_pool
655            .validator()
656            .validate_transaction(origin, transaction)
657            .await;
658        self.add_validated_transaction(origin, tx)
659    }
660
661    async fn add_transactions(
662        &self,
663        origin: TransactionOrigin,
664        transactions: Vec<Self::Transaction>,
665    ) -> Vec<PoolResult<AddedTransactionOutcome>> {
666        if transactions.is_empty() {
667            return Vec::new();
668        }
669
670        // Fully delegate to protocol pool for non-2D transactions
671        if !transactions.iter().any(|tx| tx.is_aa_2d()) {
672            return self
673                .protocol_pool
674                .add_transactions(origin, transactions)
675                .await;
676        }
677
678        self.protocol_pool
679            .validator()
680            .validate_transactions_with_origin(origin, transactions)
681            .await
682            .into_iter()
683            .map(|outcome| self.add_validated_transaction(origin, outcome))
684            .collect()
685    }
686
687    async fn add_transactions_with_origins(
688        &self,
689        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
690    ) -> Vec<PoolResult<AddedTransactionOutcome>> {
691        if transactions.is_empty() {
692            return Vec::new();
693        }
694
695        // Fully delegate to protocol pool for non-2D transactions
696        if !transactions.iter().any(|(_, tx)| tx.is_aa_2d()) {
697            return self
698                .protocol_pool
699                .add_transactions_with_origins(transactions)
700                .await;
701        }
702
703        let origins = transactions
704            .iter()
705            .map(|(origin, _)| *origin)
706            .collect::<Vec<_>>();
707
708        self.protocol_pool
709            .validator()
710            .validate_transactions(transactions)
711            .await
712            .into_iter()
713            .zip(origins)
714            .map(|(outcome, origin)| self.add_validated_transaction(origin, outcome))
715            .collect()
716    }
717
718    fn transaction_event_listener(&self, tx_hash: B256) -> Option<TransactionEvents> {
719        self.protocol_pool.transaction_event_listener(tx_hash)
720    }
721
722    fn all_transactions_event_listener(
723        &self,
724    ) -> reth_transaction_pool::AllTransactionsEvents<Self::Transaction> {
725        self.protocol_pool.all_transactions_event_listener()
726    }
727
728    fn pending_transactions_listener_for(
729        &self,
730        kind: reth_transaction_pool::TransactionListenerKind,
731    ) -> tokio::sync::mpsc::Receiver<B256> {
732        self.protocol_pool.pending_transactions_listener_for(kind)
733    }
734
735    fn blob_transaction_sidecars_listener(&self) -> tokio::sync::mpsc::Receiver<NewBlobSidecar> {
736        self.protocol_pool.blob_transaction_sidecars_listener()
737    }
738
739    fn new_transactions_listener_for(
740        &self,
741        kind: reth_transaction_pool::TransactionListenerKind,
742    ) -> tokio::sync::mpsc::Receiver<reth_transaction_pool::NewTransactionEvent<Self::Transaction>>
743    {
744        self.protocol_pool.new_transactions_listener_for(kind)
745    }
746
747    fn pooled_transaction_hashes(&self) -> Vec<B256> {
748        let mut hashes = self.protocol_pool.pooled_transaction_hashes();
749        hashes.extend(self.aa_2d_pool.read().pooled_transactions_hashes_iter());
750        hashes
751    }
752
753    fn pooled_transaction_hashes_max(&self, max: usize) -> Vec<B256> {
754        let protocol_hashes = self.protocol_pool.pooled_transaction_hashes_max(max);
755        if protocol_hashes.len() >= max {
756            return protocol_hashes;
757        }
758        let remaining = max - protocol_hashes.len();
759        let mut hashes = protocol_hashes;
760        hashes.extend(
761            self.aa_2d_pool
762                .read()
763                .pooled_transactions_hashes_iter()
764                .take(remaining),
765        );
766        hashes
767    }
768
769    fn pooled_transactions(&self) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
770        let mut txs = self.protocol_pool.pooled_transactions();
771        txs.extend(self.aa_2d_pool.read().pooled_transactions_iter());
772        txs
773    }
774
775    fn pooled_transactions_max(
776        &self,
777        max: usize,
778    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
779        let mut txs = self.protocol_pool.pooled_transactions_max(max);
780        if txs.len() >= max {
781            return txs;
782        }
783
784        let remaining = max - txs.len();
785        txs.extend(
786            self.aa_2d_pool
787                .read()
788                .pooled_transactions_iter()
789                .take(remaining),
790        );
791        txs
792    }
793
794    fn get_pooled_transaction_elements(
795        &self,
796        tx_hashes: Vec<B256>,
797        limit: GetPooledTransactionLimit,
798    ) -> Vec<<Self::Transaction as PoolTransaction>::Pooled> {
799        let mut out = Vec::new();
800        self.append_pooled_transaction_elements(&tx_hashes, limit, &mut out);
801        out
802    }
803
804    fn append_pooled_transaction_elements(
805        &self,
806        tx_hashes: &[B256],
807        limit: GetPooledTransactionLimit,
808        out: &mut Vec<<Self::Transaction as PoolTransaction>::Pooled>,
809    ) {
810        let mut accumulated_size = 0;
811        self.aa_2d_pool.read().append_pooled_transaction_elements(
812            tx_hashes,
813            limit,
814            &mut accumulated_size,
815            out,
816        );
817
818        // If the limit is already exceeded, don't query the protocol pool
819        if limit.exceeds(accumulated_size) {
820            return;
821        }
822
823        // Adjust the limit for the protocol pool based on what we've already collected
824        let remaining_limit = match limit {
825            GetPooledTransactionLimit::None => GetPooledTransactionLimit::None,
826            GetPooledTransactionLimit::ResponseSizeSoftLimit(max) => {
827                GetPooledTransactionLimit::ResponseSizeSoftLimit(
828                    max.saturating_sub(accumulated_size),
829                )
830            }
831        };
832
833        self.protocol_pool
834            .append_pooled_transaction_elements(tx_hashes, remaining_limit, out);
835    }
836
837    fn get_pooled_transaction_element(
838        &self,
839        tx_hash: B256,
840    ) -> Option<reth_primitives_traits::Recovered<<Self::Transaction as PoolTransaction>::Pooled>>
841    {
842        self.protocol_pool
843            .get_pooled_transaction_element(tx_hash)
844            .or_else(|| {
845                self.aa_2d_pool
846                    .read()
847                    .get(&tx_hash)
848                    .and_then(|tx| tx.transaction.clone_into_pooled().ok())
849            })
850    }
851
852    fn best_transactions(
853        &self,
854    ) -> Box<dyn BestTransactions<Item = Arc<ValidPoolTransaction<Self::Transaction>>>> {
855        let protocol_pool = self.protocol_pool.inner();
856        let base_fee = protocol_pool.block_info().pending_basefee;
857        let left = protocol_pool.best_transactions();
858        let right = self.aa_2d_pool.read().best_transactions();
859        Box::new(MergeBestTransactions::new(Box::new(left), right, base_fee))
860    }
861
862    fn best_transactions_with_attributes(
863        &self,
864        attributes: BestTransactionsAttributes,
865    ) -> Box<dyn BestTransactions<Item = Arc<ValidPoolTransaction<Self::Transaction>>>> {
866        let left = self
867            .protocol_pool
868            .best_transactions_with_attributes(attributes);
869        let right = self
870            .aa_2d_pool
871            .read()
872            .best_transactions_with_base_fee(attributes.basefee);
873        Box::new(MergeBestTransactions::new(left, right, attributes.basefee))
874    }
875
876    fn pending_transactions(&self) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
877        let mut pending = self.protocol_pool.pending_transactions();
878        pending.extend(self.aa_2d_pool.read().pending_transactions());
879        pending
880    }
881
882    fn pending_transactions_max(
883        &self,
884        max: usize,
885    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
886        let protocol_txs = self.protocol_pool.pending_transactions_max(max);
887        if protocol_txs.len() >= max {
888            return protocol_txs;
889        }
890        let remaining = max - protocol_txs.len();
891        let mut txs = protocol_txs;
892        txs.extend(
893            self.aa_2d_pool
894                .read()
895                .pending_transactions()
896                .take(remaining),
897        );
898        txs
899    }
900
901    fn queued_transactions(&self) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
902        let mut queued = self.protocol_pool.queued_transactions();
903        queued.extend(self.aa_2d_pool.read().queued_transactions());
904        queued
905    }
906
907    fn pending_and_queued_txn_count(&self) -> (usize, usize) {
908        let (protocol_pending, protocol_queued) = self.protocol_pool.pending_and_queued_txn_count();
909        let (aa_pending, aa_queued) = self.aa_2d_pool.read().pending_and_queued_txn_count();
910        (protocol_pending + aa_pending, protocol_queued + aa_queued)
911    }
912
913    fn all_transactions(&self) -> AllPoolTransactions<Self::Transaction> {
914        let mut transactions = self.protocol_pool.all_transactions();
915        self.aa_2d_pool
916            .read()
917            .append_all_transactions(&mut transactions);
918        transactions
919    }
920
921    fn all_transaction_hashes(&self) -> Vec<B256> {
922        let mut hashes = self.protocol_pool.all_transaction_hashes();
923        hashes.extend(self.aa_2d_pool.read().all_transaction_hashes_iter());
924        hashes
925    }
926
927    fn remove_transactions(
928        &self,
929        hashes: Vec<B256>,
930    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
931        let mut txs = self.aa_2d_pool.write().remove_transactions(hashes.iter());
932        txs.extend(self.protocol_pool.remove_transactions(hashes));
933        txs
934    }
935
936    fn remove_transactions_and_descendants(
937        &self,
938        hashes: Vec<B256>,
939    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
940        let mut txs = self
941            .aa_2d_pool
942            .write()
943            .remove_transactions_and_descendants(hashes.iter());
944        txs.extend(
945            self.protocol_pool
946                .remove_transactions_and_descendants(hashes),
947        );
948        txs
949    }
950
951    fn remove_transactions_by_sender(
952        &self,
953        sender: Address,
954    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
955        let mut txs = self
956            .aa_2d_pool
957            .write()
958            .remove_transactions_by_sender(sender);
959        txs.extend(self.protocol_pool.remove_transactions_by_sender(sender));
960        txs
961    }
962
963    fn prune_transactions(
964        &self,
965        hashes: Vec<TxHash>,
966    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
967        let mut txs = self.aa_2d_pool.write().remove_transactions(hashes.iter());
968        txs.extend(self.protocol_pool.prune_transactions(hashes));
969        txs
970    }
971
972    fn retain_unknown<A: HandleMempoolData>(&self, announcement: &mut A) {
973        self.protocol_pool.retain_unknown(announcement);
974        if announcement.is_empty() {
975            return;
976        }
977        let aa_pool = self.aa_2d_pool.read();
978        announcement.retain_by_hash(|tx| !aa_pool.contains(tx))
979    }
980
981    fn retain_contains<A>(&self, announcement: &mut A)
982    where
983        A: HandleMempoolData,
984    {
985        if announcement.is_empty() {
986            return;
987        }
988        announcement.retain_by_hash(|tx| self.contains(tx))
989    }
990
991    fn contains(&self, tx_hash: &B256) -> bool {
992        self.protocol_pool.contains(tx_hash) || self.aa_2d_pool.read().contains(tx_hash)
993    }
994
995    fn get(&self, tx_hash: &B256) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
996        self.protocol_pool
997            .get(tx_hash)
998            .or_else(|| self.aa_2d_pool.read().get(tx_hash))
999    }
1000
1001    fn get_all(&self, txs: Vec<B256>) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1002        let mut result = self.aa_2d_pool.read().get_all(txs.iter());
1003        result.extend(self.protocol_pool.get_all(txs));
1004        result
1005    }
1006
1007    fn on_propagated(&self, txs: PropagatedTransactions) {
1008        self.protocol_pool.on_propagated(txs);
1009    }
1010
1011    fn get_transactions_by_sender(
1012        &self,
1013        sender: Address,
1014    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1015        let mut txs = self.protocol_pool.get_transactions_by_sender(sender);
1016        txs.extend(
1017            self.aa_2d_pool
1018                .read()
1019                .get_transactions_by_sender_iter(sender),
1020        );
1021        txs
1022    }
1023
1024    fn get_pending_transactions_with_predicate(
1025        &self,
1026        mut predicate: impl FnMut(&ValidPoolTransaction<Self::Transaction>) -> bool,
1027    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1028        let mut txs = self
1029            .protocol_pool
1030            .get_pending_transactions_with_predicate(&mut predicate);
1031        txs.extend(
1032            self.aa_2d_pool
1033                .read()
1034                .pending_transactions()
1035                .filter(|tx| predicate(tx)),
1036        );
1037        txs
1038    }
1039
1040    fn get_pending_transactions_by_sender(
1041        &self,
1042        sender: Address,
1043    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1044        let mut txs = self
1045            .protocol_pool
1046            .get_pending_transactions_by_sender(sender);
1047        txs.extend(
1048            self.aa_2d_pool
1049                .read()
1050                .pending_transactions()
1051                .filter(|tx| tx.sender() == sender),
1052        );
1053
1054        txs
1055    }
1056
1057    fn get_queued_transactions_by_sender(
1058        &self,
1059        sender: Address,
1060    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1061        self.protocol_pool.get_queued_transactions_by_sender(sender)
1062    }
1063
1064    fn get_highest_transaction_by_sender(
1065        &self,
1066        sender: Address,
1067    ) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
1068        // With 2D nonces, there's no concept of a single "highest" nonce across all nonce_keys
1069        // Return the highest protocol nonce (nonce_key=0) only
1070        self.protocol_pool.get_highest_transaction_by_sender(sender)
1071    }
1072
1073    fn get_highest_consecutive_transaction_by_sender(
1074        &self,
1075        sender: Address,
1076        on_chain_nonce: u64,
1077    ) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
1078        // This is complex with 2D nonces - delegate to protocol pool
1079        self.protocol_pool
1080            .get_highest_consecutive_transaction_by_sender(sender, on_chain_nonce)
1081    }
1082
1083    fn get_transaction_by_sender_and_nonce(
1084        &self,
1085        sender: Address,
1086        nonce: u64,
1087    ) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
1088        // Only returns transactions from protocol pool (nonce_key=0)
1089        self.protocol_pool
1090            .get_transaction_by_sender_and_nonce(sender, nonce)
1091    }
1092
1093    fn get_transactions_by_origin(
1094        &self,
1095        origin: TransactionOrigin,
1096    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1097        let mut txs = self.protocol_pool.get_transactions_by_origin(origin);
1098        txs.extend(
1099            self.aa_2d_pool
1100                .read()
1101                .get_transactions_by_origin_iter(origin),
1102        );
1103        txs
1104    }
1105
1106    fn get_pending_transactions_by_origin(
1107        &self,
1108        origin: TransactionOrigin,
1109    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1110        let mut txs = self
1111            .protocol_pool
1112            .get_pending_transactions_by_origin(origin);
1113        txs.extend(
1114            self.aa_2d_pool
1115                .read()
1116                .get_pending_transactions_by_origin_iter(origin),
1117        );
1118        txs
1119    }
1120
1121    fn unique_senders(&self) -> AddressSet {
1122        let mut senders = self.protocol_pool.unique_senders();
1123        senders.extend(self.aa_2d_pool.read().senders_iter().copied());
1124        senders
1125    }
1126
1127    fn get_blob(
1128        &self,
1129        tx_hash: B256,
1130    ) -> Result<
1131        Option<Arc<alloy_eips::eip7594::BlobTransactionSidecarVariant>>,
1132        reth_transaction_pool::blobstore::BlobStoreError,
1133    > {
1134        self.protocol_pool.get_blob(tx_hash)
1135    }
1136
1137    fn get_all_blobs(
1138        &self,
1139        tx_hashes: Vec<B256>,
1140    ) -> Result<
1141        Vec<(
1142            B256,
1143            Arc<alloy_eips::eip7594::BlobTransactionSidecarVariant>,
1144        )>,
1145        reth_transaction_pool::blobstore::BlobStoreError,
1146    > {
1147        self.protocol_pool.get_all_blobs(tx_hashes)
1148    }
1149
1150    fn get_all_blobs_exact(
1151        &self,
1152        tx_hashes: Vec<B256>,
1153    ) -> Result<
1154        Vec<Arc<alloy_eips::eip7594::BlobTransactionSidecarVariant>>,
1155        reth_transaction_pool::blobstore::BlobStoreError,
1156    > {
1157        self.protocol_pool.get_all_blobs_exact(tx_hashes)
1158    }
1159
1160    fn get_blobs_for_versioned_hashes_v1(
1161        &self,
1162        versioned_hashes: &[B256],
1163    ) -> Result<
1164        Vec<Option<alloy_eips::eip4844::BlobAndProofV1>>,
1165        reth_transaction_pool::blobstore::BlobStoreError,
1166    > {
1167        self.protocol_pool
1168            .get_blobs_for_versioned_hashes_v1(versioned_hashes)
1169    }
1170
1171    fn get_blobs_for_versioned_hashes_v2(
1172        &self,
1173        versioned_hashes: &[B256],
1174    ) -> Result<
1175        Option<Vec<alloy_eips::eip4844::BlobAndProofV2>>,
1176        reth_transaction_pool::blobstore::BlobStoreError,
1177    > {
1178        self.protocol_pool
1179            .get_blobs_for_versioned_hashes_v2(versioned_hashes)
1180    }
1181
1182    fn get_blobs_for_versioned_hashes_v3(
1183        &self,
1184        versioned_hashes: &[B256],
1185    ) -> Result<
1186        Vec<Option<alloy_eips::eip4844::BlobAndProofV2>>,
1187        reth_transaction_pool::blobstore::BlobStoreError,
1188    > {
1189        self.protocol_pool
1190            .get_blobs_for_versioned_hashes_v3(versioned_hashes)
1191    }
1192
1193    fn get_blobs_for_versioned_hashes_v4(
1194        &self,
1195        versioned_hashes: &[B256],
1196        indices_bitarray: alloy_primitives::B128,
1197    ) -> Result<
1198        Vec<Option<alloy_eips::eip4844::BlobCellsAndProofsV1>>,
1199        reth_transaction_pool::blobstore::BlobStoreError,
1200    > {
1201        self.protocol_pool
1202            .get_blobs_for_versioned_hashes_v4(versioned_hashes, indices_bitarray)
1203    }
1204
1205    fn blob_store(&self) -> Box<dyn reth_transaction_pool::BlobStore> {
1206        TransactionPool::blob_store(&self.protocol_pool)
1207    }
1208}
1209
1210impl<Client> TransactionPoolExt for TempoTransactionPool<Client>
1211where
1212    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + 'static,
1213{
1214    type Block = Block;
1215
1216    fn set_block_info(&self, info: BlockInfo) {
1217        self.protocol_pool.set_block_info(info);
1218        self.aa_2d_pool.write().set_base_fee(info.pending_basefee);
1219    }
1220
1221    fn on_canonical_state_change(&self, update: CanonicalStateUpdate<'_, Self::Block>) {
1222        self.protocol_pool.on_canonical_state_change(update)
1223    }
1224
1225    fn update_accounts(&self, accounts: Vec<ChangedAccount>) {
1226        self.protocol_pool.update_accounts(accounts)
1227    }
1228
1229    fn delete_blob(&self, tx: B256) {
1230        self.protocol_pool.delete_blob(tx)
1231    }
1232
1233    fn delete_blobs(&self, txs: Vec<B256>) {
1234        self.protocol_pool.delete_blobs(txs)
1235    }
1236
1237    fn cleanup_blobs(&self) {
1238        self.protocol_pool.cleanup_blobs()
1239    }
1240}
1241
1242/// Checks whether a pending keychain tx exceeds its effective remaining spending limit.
1243///
1244/// Re-reads the current limit from state for the tx's `(account, key_id, fee_token)` combo,
1245/// including any T3 periodic-limit rollover at `current_timestamp`. Returns true if the tx's
1246/// fee cost exceeds the effective remaining limit, meaning it should be evicted.
1247pub(crate) fn exceeds_spending_limit(
1248    provider: &mut impl StateProvider,
1249    subject: &crate::transaction::KeychainSubject,
1250    fee_token_cost: alloy_primitives::U256,
1251    current_timestamp: u64,
1252    spec: TempoHardfork,
1253) -> bool {
1254    provider
1255        .with_read_only_storage_ctx(
1256            spec,
1257            StorageActions::disabled(),
1258            || -> TempoPrecompileResult<bool> {
1259                let keychain = AccountKeychain::new();
1260                if !keychain.keys[subject.account][subject.key_id]
1261                    .read()?
1262                    .enforce_limits
1263                {
1264                    return Ok(false);
1265                }
1266
1267                let remaining = keychain.effective_remaining_limit(
1268                    subject.account,
1269                    subject.key_id,
1270                    subject.fee_token,
1271                    current_timestamp,
1272                )?;
1273                Ok(fee_token_cost > remaining)
1274            },
1275        )
1276        .unwrap_or_default()
1277}
1278
1279/// Returns the set of policy IDs that can affect fee_payer authorization for a token.
1280///
1281/// For simple policies the set contains just the policy ID. For compound policies
1282/// (TIP-1015) it contains both the compound root and the sender sub-policy, since
1283/// fee transfer authorization checks `fee_payer` via `AuthRole::Sender`.
1284/// `recipient_policy_id` and `mint_recipient_policy_id` are excluded — they govern
1285/// other roles and cannot invalidate a fee_payer's transactions.
1286fn get_sender_policy_ids(
1287    provider: &mut impl StateProvider,
1288    fee_token: Address,
1289    spec: TempoHardfork,
1290    cache: &mut AddressMap<Vec<u64>>,
1291) -> Option<Vec<u64>> {
1292    if let Some(cached) = cache.get(&fee_token) {
1293        return Some(cached.clone());
1294    }
1295
1296    provider.with_read_only_storage_ctx(spec, StorageActions::disabled(), || {
1297        let policy_id = TIP20Token::from_address(fee_token)
1298            .and_then(|t| t.transfer_policy_id())
1299            .ok()
1300            .filter(|&id| id != REJECT_ALL_POLICY_ID)?;
1301
1302        let mut ids = vec![policy_id];
1303
1304        // For compound policies, include only the sender sub-policy ID.
1305        let registry = TIP403Registry::new();
1306        if let Ok(data) = registry.policy_records[policy_id].base.read()
1307            && data.is_compound()
1308            && let Ok(compound) = registry.policy_records[policy_id].compound.read()
1309            && compound.sender_policy_id != REJECT_ALL_POLICY_ID
1310        {
1311            ids.push(compound.sender_policy_id);
1312        }
1313
1314        // Cache even though compound sub-policy references are immutable: avoids
1315        // redundant SLOADs when multiple transactions share the same fee token.
1316        cache.insert(fee_token, ids.clone());
1317        Some(ids)
1318    })
1319}
1320
1321/// Returns the set of policy IDs that can affect recipient authorization for a token.
1322///
1323/// For simple (non-compound) policies, the transfer policy applies symmetrically to both
1324/// sender and recipient, so the set contains just the policy ID. For compound policies
1325/// (TIP-1015) it contains both the compound root and the recipient sub-policy, since
1326/// fee transfer authorization checks the fee manager via `AuthRole::Recipient`.
1327///
1328/// Unlike `get_sender_policy_ids` this is uncached — it's only called on the rare path
1329/// where the fee manager itself is blacklisted or un-whitelisted.
1330fn get_recipient_policy_ids(
1331    provider: &mut impl StateProvider,
1332    fee_token: Address,
1333    spec: TempoHardfork,
1334) -> Option<Vec<u64>> {
1335    provider.with_read_only_storage_ctx(spec, StorageActions::disabled(), || {
1336        let policy_id = TIP20Token::from_address(fee_token)
1337            .and_then(|t| t.transfer_policy_id())
1338            .ok()
1339            .filter(|&id| id != REJECT_ALL_POLICY_ID)?;
1340
1341        let mut ids = vec![policy_id];
1342
1343        let registry = TIP403Registry::new();
1344        if let Ok(data) = registry.policy_records[policy_id].base.read()
1345            && data.is_compound()
1346            && let Ok(compound) = registry.policy_records[policy_id].compound.read()
1347            && compound.recipient_policy_id != REJECT_ALL_POLICY_ID
1348        {
1349            ids.push(compound.recipient_policy_id);
1350        }
1351
1352        Some(ids)
1353    })
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358    use super::*;
1359    /// Returns the hashes of the evicted transactions.
1360    fn tx_hashes(txs: &[Arc<ValidPoolTransaction<TempoPooledTransaction>>]) -> Vec<TxHash> {
1361        txs.iter().map(|tx| *tx.hash()).collect()
1362    }
1363
1364    use crate::{test_utils::MockProviderStorageExt, transaction::KeychainSubject};
1365    use alloy_consensus::Header;
1366    use alloy_primitives::{Signature, U256, address, uint};
1367    use alloy_signer::SignerSync;
1368    use alloy_signer_local::PrivateKeySigner;
1369    use reth_primitives_traits::Recovered;
1370    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1371    use reth_storage_api::StateProviderFactory;
1372    use reth_transaction_pool::{
1373        PoolConfig, TransactionOrigin, TransactionPool, TransactionValidationTaskExecutor,
1374        blobstore::InMemoryBlobStore,
1375        validate::{EthTransactionValidatorBuilder, ValidTransaction},
1376    };
1377    use tempo_chainspec::{
1378        TempoChainSpec,
1379        hardfork::TempoHardfork,
1380        spec::{MODERATO, TEMPO_T1_TX_GAS_LIMIT_CAP},
1381    };
1382    use tempo_contracts::precompiles::ITIP403Registry;
1383    use tempo_evm::TempoEvmConfig;
1384    use tempo_precompiles::{
1385        PATH_USD_ADDRESS,
1386        account_keychain::{
1387            AccountKeychain, AuthorizedKey, SpendingLimitState, StoredSignatureType,
1388        },
1389        tip20::slots as tip20_slots,
1390        tip403_registry::{CompoundPolicyData, PolicyData, TIP403Registry},
1391    };
1392    use tempo_primitives::{
1393        Block, TempoHeader, TempoPrimitives, TempoTxEnvelope,
1394        transaction::{KeyAuthorization, PrimitiveSignature, SignatureType},
1395    };
1396
1397    fn provider_with_spending_limit(
1398        account: Address,
1399        key_id: Address,
1400        fee_token: Address,
1401        remaining_limit: alloy_primitives::U256,
1402    ) -> Box<dyn reth_storage_api::StateProvider> {
1403        provider_with_spending_limit_state(
1404            account,
1405            key_id,
1406            fee_token,
1407            SpendingLimitState {
1408                remaining: remaining_limit,
1409                ..Default::default()
1410            },
1411            TempoHardfork::default(),
1412        )
1413    }
1414
1415    fn provider_with_spending_limit_state(
1416        account: Address,
1417        key_id: Address,
1418        fee_token: Address,
1419        limit_state: SpendingLimitState,
1420        setup_spec: TempoHardfork,
1421    ) -> Box<dyn reth_storage_api::StateProvider> {
1422        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1423            tempo_chainspec::spec::MODERATO.clone(),
1424        ));
1425
1426        // Write AuthorizedKey with enforce_limits=true
1427        provider
1428            .setup_storage(setup_spec, || {
1429                let mut keychain = AccountKeychain::new();
1430                keychain.keys[account][key_id].write(AuthorizedKey {
1431                    signature_type: StoredSignatureType::Secp256k1,
1432                    expiry: u64::MAX,
1433                    enforce_limits: true,
1434                    is_revoked: false,
1435                    is_admin: false,
1436                })?;
1437                let limit_key = AccountKeychain::spending_limit_key(account, key_id);
1438                keychain.spending_limits[limit_key][fee_token].write(limit_state)?;
1439                Ok::<(), tempo_precompiles::error::TempoPrecompileError>(())
1440            })
1441            .unwrap();
1442
1443        provider.latest().unwrap()
1444    }
1445
1446    fn set_fee_token_balance(
1447        provider: &MockEthProvider<TempoPrimitives, TempoChainSpec>,
1448        fee_token: Address,
1449        account: Address,
1450        balance: U256,
1451    ) {
1452        let usd_currency_value =
1453            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
1454        let transfer_policy_id_packed =
1455            uint!(0x0000000000000000000000010000000000000000000000000000000000000000_U256);
1456        let balance_slot = TIP20Token::from_address(fee_token)
1457            .expect("fee token must be a valid TIP20 token")
1458            .balances[account]
1459            .slot();
1460
1461        provider.add_account(
1462            fee_token,
1463            ExtendedAccount::new(0, U256::ZERO).extend_storage([
1464                (tip20_slots::CURRENCY.into(), usd_currency_value),
1465                (
1466                    tip20_slots::TRANSFER_POLICY_ID.into(),
1467                    transfer_policy_id_packed,
1468                ),
1469                (balance_slot.into(), balance),
1470            ]),
1471        );
1472    }
1473
1474    fn set_transfer_policy(
1475        provider: &MockEthProvider<TempoPrimitives, TempoChainSpec>,
1476        fee_token: Address,
1477        policy_id: u64,
1478    ) {
1479        let transfer_policy_id_packed =
1480            U256::from(policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
1481
1482        provider.add_account(
1483            fee_token,
1484            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
1485                tip20_slots::TRANSFER_POLICY_ID.into(),
1486                transfer_policy_id_packed,
1487            )]),
1488        );
1489    }
1490
1491    fn set_keychain_spending_limit(
1492        provider: &MockEthProvider<TempoPrimitives, TempoChainSpec>,
1493        account: Address,
1494        key_id: Address,
1495        fee_token: Address,
1496        remaining: U256,
1497    ) {
1498        provider
1499            .setup_storage(TempoHardfork::default(), || {
1500                let mut keychain = AccountKeychain::new();
1501                keychain.keys[account][key_id].write(AuthorizedKey {
1502                    signature_type: StoredSignatureType::Secp256k1,
1503                    expiry: u64::MAX,
1504                    enforce_limits: true,
1505                    is_revoked: false,
1506                    is_admin: false,
1507                })?;
1508                let limit_key = AccountKeychain::spending_limit_key(account, key_id);
1509                keychain.spending_limits[limit_key][fee_token].write(SpendingLimitState {
1510                    remaining,
1511                    ..Default::default()
1512                })?;
1513                Ok::<(), tempo_precompiles::error::TempoPrecompileError>(())
1514            })
1515            .unwrap();
1516    }
1517
1518    fn create_test_pool(
1519        provider: MockEthProvider<TempoPrimitives, TempoChainSpec>,
1520    ) -> TempoTransactionPool<MockEthProvider<TempoPrimitives, TempoChainSpec>> {
1521        let inner =
1522            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1523                .disable_balance_check()
1524                .build(InMemoryBlobStore::default());
1525        let amm_cache =
1526            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
1527        let validator = TempoTransactionValidator::new(
1528            inner,
1529            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
1530            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1531            amm_cache,
1532        );
1533
1534        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
1535        let protocol_pool = Pool::new(
1536            executor,
1537            TempoTipOrdering::default(),
1538            InMemoryBlobStore::default(),
1539            PoolConfig::default(),
1540        );
1541        TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()))
1542    }
1543
1544    fn add_validated(
1545        pool: &TempoTransactionPool<MockEthProvider<TempoPrimitives, TempoChainSpec>>,
1546        pooled: TempoPooledTransaction,
1547    ) {
1548        let validated = TransactionValidationOutcome::Valid {
1549            balance: *pooled.cost(),
1550            state_nonce: pooled.nonce(),
1551            bytecode_hash: None,
1552            transaction: ValidTransaction::new(pooled, None),
1553            propagate: true,
1554            authorities: None,
1555        };
1556        pool.add_validated_transaction(TransactionOrigin::External, validated)
1557            .expect("transaction should be admitted");
1558    }
1559
1560    fn create_provider_with_tip() -> MockEthProvider<TempoPrimitives, TempoChainSpec> {
1561        let provider = MockEthProvider::<TempoPrimitives>::new()
1562            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
1563        provider.add_block(
1564            B256::random(),
1565            Block {
1566                header: TempoHeader {
1567                    inner: Header {
1568                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1569                        ..Default::default()
1570                    },
1571                    ..Default::default()
1572                },
1573                ..Default::default()
1574            },
1575        );
1576        provider
1577    }
1578
1579    fn sponsored_keychain_transaction(
1580        sender: Address,
1581        fee_token: Address,
1582    ) -> (TempoPooledTransaction, Address) {
1583        let access_key_signer = PrivateKeySigner::random();
1584        let key_id = access_key_signer.address();
1585        let envelope = crate::test_utils::TxBuilder::aa(sender)
1586            .fee_token(fee_token)
1587            .build_keychain(sender, &access_key_signer)
1588            .inner()
1589            .clone()
1590            .into_inner();
1591        let TempoTxEnvelope::AA(mut signed) = envelope else {
1592            panic!("expected AA transaction");
1593        };
1594
1595        let sponsor = PrivateKeySigner::random();
1596        signed.tx_mut().fee_payer_signature = Some(Signature::new(U256::ZERO, U256::ZERO, false));
1597        let fee_payer_hash = signed.tx().fee_payer_signature_hash(sender);
1598        signed.tx_mut().fee_payer_signature = Some(
1599            sponsor
1600                .sign_hash_sync(&fee_payer_hash)
1601                .expect("fee payer signing should succeed"),
1602        );
1603
1604        (
1605            TempoPooledTransaction::new(Recovered::new_unchecked(
1606                TempoTxEnvelope::AA(signed),
1607                sender,
1608            )),
1609            key_id,
1610        )
1611    }
1612
1613    fn sponsored_implicit_fee_transaction(sender: Address) -> (TempoPooledTransaction, Address) {
1614        let fee_payer_signer = loop {
1615            let signer = PrivateKeySigner::random();
1616            if signer.address() != sender {
1617                break signer;
1618            }
1619        };
1620        let fee_payer = fee_payer_signer.address();
1621        let envelope = crate::test_utils::TxBuilder::aa(sender)
1622            .build()
1623            .inner()
1624            .clone()
1625            .into_inner();
1626        let TempoTxEnvelope::AA(mut signed) = envelope else {
1627            panic!("expected AA transaction");
1628        };
1629        let fee_payer_hash = signed.tx().fee_payer_signature_hash(sender);
1630        signed.tx_mut().fee_payer_signature = Some(
1631            fee_payer_signer
1632                .sign_hash_sync(&fee_payer_hash)
1633                .expect("fee payer signing should succeed"),
1634        );
1635
1636        (
1637            TempoPooledTransaction::new(Recovered::new_unchecked(
1638                TempoTxEnvelope::AA(signed),
1639                sender,
1640            )),
1641            fee_payer,
1642        )
1643    }
1644
1645    #[tokio::test]
1646    async fn evicts_sponsored_implicit_fee_transaction_when_fee_payer_user_token_changes() {
1647        let sender = Address::random();
1648        let (pooled, fee_payer) = sponsored_implicit_fee_transaction(sender);
1649        assert_eq!(pooled.inner().fee_token(), None);
1650        assert_ne!(fee_payer, sender);
1651
1652        let provider = create_provider_with_tip();
1653        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1654        let pool = create_test_pool(provider);
1655        add_validated(&pool, pooled.clone());
1656
1657        let mut updates = crate::maintain::TempoPoolUpdates::new();
1658        updates.user_token_changes.insert(fee_payer);
1659
1660        let evicted = pool.evict_invalidated_transactions(&updates);
1661        assert_eq!(tx_hashes(&evicted), vec![*pooled.hash()]);
1662        assert!(pool.get(pooled.hash()).is_none());
1663    }
1664
1665    #[tokio::test]
1666    async fn keeps_sponsored_implicit_fee_transaction_when_sender_user_token_changes() {
1667        let sender = Address::random();
1668        let (pooled, fee_payer) = sponsored_implicit_fee_transaction(sender);
1669        assert_eq!(pooled.inner().fee_token(), None);
1670        assert_ne!(fee_payer, sender);
1671
1672        let provider = create_provider_with_tip();
1673        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1674        let pool = create_test_pool(provider);
1675        add_validated(&pool, pooled.clone());
1676
1677        let mut updates = crate::maintain::TempoPoolUpdates::new();
1678        updates.user_token_changes.insert(sender);
1679
1680        assert!(pool.evict_invalidated_transactions(&updates).is_empty());
1681        assert!(pool.get(pooled.hash()).is_some());
1682    }
1683
1684    #[tokio::test]
1685    async fn evicts_sender_paid_implicit_fee_transaction_when_sender_user_token_changes() {
1686        let sender = Address::random();
1687        let pooled = crate::test_utils::TxBuilder::aa(sender).build();
1688        assert_eq!(pooled.inner().fee_token(), None);
1689
1690        let provider = create_provider_with_tip();
1691        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1692        let pool = create_test_pool(provider);
1693        add_validated(&pool, pooled.clone());
1694
1695        let mut updates = crate::maintain::TempoPoolUpdates::new();
1696        updates.user_token_changes.insert(sender);
1697
1698        let evicted = pool.evict_invalidated_transactions(&updates);
1699        assert_eq!(tx_hashes(&evicted), vec![*pooled.hash()]);
1700        assert!(pool.get(pooled.hash()).is_none());
1701    }
1702
1703    #[tokio::test]
1704    async fn keeps_sponsored_keychain_transaction_on_spending_limit_invalidations() {
1705        let sender = Address::random();
1706        let fee_token = PATH_USD_ADDRESS;
1707        let (pooled, key_id) = sponsored_keychain_transaction(sender, fee_token);
1708
1709        let provider = create_provider_with_tip();
1710        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1711        set_keychain_spending_limit(
1712            &provider,
1713            sender,
1714            key_id,
1715            fee_token,
1716            pooled.fee_token_cost() - U256::from(1_u64),
1717        );
1718        let pool = create_test_pool(provider);
1719        add_validated(&pool, pooled.clone());
1720
1721        let mut limit_change = crate::maintain::TempoPoolUpdates::new();
1722        limit_change
1723            .spending_limit_changes
1724            .insert(sender, key_id, Some(fee_token));
1725
1726        assert!(
1727            pool.evict_invalidated_transactions(&limit_change)
1728                .is_empty()
1729        );
1730        assert!(pool.get(pooled.hash()).is_some());
1731
1732        let mut limit_spend = crate::maintain::TempoPoolUpdates::new();
1733        limit_spend
1734            .spending_limit_spends
1735            .insert(sender, key_id, Some(fee_token));
1736
1737        assert!(pool.evict_invalidated_transactions(&limit_spend).is_empty());
1738        assert!(pool.get(pooled.hash()).is_some());
1739    }
1740
1741    #[tokio::test]
1742    async fn evicts_sponsored_transactions_when_fee_payer_becomes_insolvent() {
1743        let fee_payer_signer = PrivateKeySigner::random();
1744        let fee_payer = fee_payer_signer.address();
1745        let sender = Address::random();
1746
1747        let envelope = crate::test_utils::TxBuilder::aa(sender)
1748            .fee_token(PATH_USD_ADDRESS)
1749            .build()
1750            .inner()
1751            .clone()
1752            .into_inner();
1753        let TempoTxEnvelope::AA(mut signed) = envelope else {
1754            panic!("expected AA transaction");
1755        };
1756        let fee_payer_hash = signed.tx().fee_payer_signature_hash(sender);
1757        signed.tx_mut().fee_payer_signature = Some(
1758            fee_payer_signer
1759                .sign_hash_sync(&fee_payer_hash)
1760                .expect("fee payer signing should succeed"),
1761        );
1762        let pooled = TempoPooledTransaction::new(Recovered::new_unchecked(
1763            TempoTxEnvelope::AA(signed),
1764            sender,
1765        ));
1766
1767        let provider = MockEthProvider::<TempoPrimitives>::new()
1768            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
1769        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1770        provider.add_block(
1771            B256::random(),
1772            Block {
1773                header: TempoHeader {
1774                    inner: Header {
1775                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1776                        ..Default::default()
1777                    },
1778                    ..Default::default()
1779                },
1780                ..Default::default()
1781            },
1782        );
1783
1784        let initial_balance = pooled.fee_token_cost() + U256::from(1_u64);
1785        set_fee_token_balance(&provider, PATH_USD_ADDRESS, fee_payer, initial_balance);
1786
1787        let inner =
1788            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1789                .disable_balance_check()
1790                .build(InMemoryBlobStore::default());
1791        let amm_cache =
1792            AmmLiquidityCache::new(provider.clone()).expect("failed to setup AmmLiquidityCache");
1793        let validator = TempoTransactionValidator::new(
1794            inner,
1795            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
1796            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1797            amm_cache,
1798        );
1799
1800        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
1801        let protocol_pool = Pool::new(
1802            executor,
1803            TempoTipOrdering::default(),
1804            InMemoryBlobStore::default(),
1805            PoolConfig::default(),
1806        );
1807        let pool = TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()));
1808
1809        pooled.set_resolved_fee_token(PATH_USD_ADDRESS);
1810        let validated = TransactionValidationOutcome::Valid {
1811            balance: *pooled.cost(),
1812            state_nonce: pooled.nonce(),
1813            bytecode_hash: None,
1814            transaction: ValidTransaction::new(pooled.clone(), None),
1815            propagate: true,
1816            authorities: None,
1817        };
1818        let add_result = pool.add_validated_transaction(TransactionOrigin::External, validated);
1819        assert!(
1820            add_result.is_ok(),
1821            "transaction should be admitted before sponsor drains balance: {add_result:?}"
1822        );
1823
1824        set_fee_token_balance(
1825            &provider,
1826            PATH_USD_ADDRESS,
1827            fee_payer,
1828            pooled.fee_token_cost() - U256::from(1_u64),
1829        );
1830
1831        let mut updates = crate::maintain::TempoPoolUpdates::new();
1832        updates
1833            .fee_balance_changes
1834            .entry(PATH_USD_ADDRESS)
1835            .or_default()
1836            .insert(fee_payer);
1837
1838        let evicted = pool.evict_invalidated_transactions(&updates);
1839        assert_eq!(tx_hashes(&evicted), vec![*pooled.hash()]);
1840        assert!(pool.get(pooled.hash()).is_none());
1841    }
1842
1843    #[tokio::test]
1844    async fn blacklist_eviction_uses_resolved_fee_token() {
1845        let sender = Address::random();
1846        let resolved_fee_token = address!("20C0000000000000000000000000000000000002");
1847        let policy_id = 7;
1848        let pooled = crate::test_utils::TxBuilder::aa(sender).build();
1849
1850        assert_eq!(pooled.inner().fee_token(), None);
1851        pooled.set_resolved_fee_token(resolved_fee_token);
1852
1853        let provider = MockEthProvider::<TempoPrimitives>::new()
1854            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
1855        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1856        provider.add_block(
1857            B256::random(),
1858            Block {
1859                header: TempoHeader {
1860                    inner: Header {
1861                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1862                        ..Default::default()
1863                    },
1864                    ..Default::default()
1865                },
1866                ..Default::default()
1867            },
1868        );
1869        set_transfer_policy(&provider, resolved_fee_token, policy_id);
1870
1871        let pool = create_test_pool(provider);
1872        add_validated(&pool, pooled.clone());
1873
1874        let mut updates = crate::maintain::TempoPoolUpdates::new();
1875        updates.blacklist_additions.push((policy_id, sender));
1876
1877        let evicted = pool.evict_invalidated_transactions(&updates);
1878        assert_eq!(tx_hashes(&evicted), vec![*pooled.hash()]);
1879        assert!(pool.get(pooled.hash()).is_none());
1880    }
1881
1882    #[tokio::test]
1883    async fn whitelist_eviction_uses_resolved_fee_token() {
1884        let sender = Address::random();
1885        let resolved_fee_token = address!("20C0000000000000000000000000000000000002");
1886        let policy_id = 9;
1887        let pooled = crate::test_utils::TxBuilder::aa(sender).build();
1888
1889        assert_eq!(pooled.inner().fee_token(), None);
1890        pooled.set_resolved_fee_token(resolved_fee_token);
1891
1892        let provider = MockEthProvider::<TempoPrimitives>::new()
1893            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
1894        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1895        provider.add_block(
1896            B256::random(),
1897            Block {
1898                header: TempoHeader {
1899                    inner: Header {
1900                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1901                        ..Default::default()
1902                    },
1903                    ..Default::default()
1904                },
1905                ..Default::default()
1906            },
1907        );
1908        set_transfer_policy(&provider, resolved_fee_token, policy_id);
1909
1910        let pool = create_test_pool(provider);
1911        add_validated(&pool, pooled.clone());
1912
1913        let mut updates = crate::maintain::TempoPoolUpdates::new();
1914        updates.whitelist_removals.push((policy_id, sender));
1915
1916        let evicted = pool.evict_invalidated_transactions(&updates);
1917        assert_eq!(tx_hashes(&evicted), vec![*pooled.hash()]);
1918        assert!(pool.get(pooled.hash()).is_none());
1919    }
1920
1921    #[tokio::test]
1922    async fn validator_token_change_uses_resolved_fee_token_for_liquidity_recheck() {
1923        let sender = Address::random();
1924        let validator_address = Address::random();
1925        let resolved_fee_token = address!("20C0000000000000000000000000000000000002");
1926        let pooled = crate::test_utils::TxBuilder::aa(sender).build();
1927
1928        assert_eq!(pooled.inner().fee_token(), None);
1929        pooled.set_resolved_fee_token(resolved_fee_token);
1930
1931        let provider = MockEthProvider::<TempoPrimitives>::new()
1932            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
1933        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1934        provider.add_block(
1935            B256::random(),
1936            Block {
1937                header: TempoHeader {
1938                    inner: Header {
1939                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1940                        ..Default::default()
1941                    },
1942                    ..Default::default()
1943                },
1944                ..Default::default()
1945            },
1946        );
1947
1948        let inner = EthTransactionValidatorBuilder::new(provider, TempoEvmConfig::mainnet())
1949            .disable_balance_check()
1950            .build(InMemoryBlobStore::default());
1951        let amm_cache = AmmLiquidityCache::with_unique_validators(vec![validator_address]);
1952        let validator = TempoTransactionValidator::new(
1953            inner,
1954            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
1955            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1956            amm_cache,
1957        );
1958
1959        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
1960        let protocol_pool = Pool::new(
1961            executor,
1962            TempoTipOrdering::default(),
1963            InMemoryBlobStore::default(),
1964            PoolConfig::default(),
1965        );
1966        let pool = TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()));
1967
1968        let validated = TransactionValidationOutcome::Valid {
1969            balance: *pooled.cost(),
1970            state_nonce: pooled.nonce(),
1971            bytecode_hash: None,
1972            transaction: ValidTransaction::new(pooled.clone(), None),
1973            propagate: true,
1974            authorities: None,
1975        };
1976        pool.add_validated_transaction(TransactionOrigin::External, validated)
1977            .expect("transaction should be admitted before validator token change");
1978
1979        let mut updates = crate::maintain::TempoPoolUpdates::new();
1980        updates
1981            .validator_token_changes
1982            .insert(validator_address, resolved_fee_token);
1983
1984        let evicted = pool.evict_invalidated_transactions(&updates);
1985        assert!(evicted.is_empty());
1986        assert!(pool.get(pooled.hash()).is_some());
1987    }
1988
1989    #[tokio::test]
1990    async fn evicts_transactions_with_burned_key_authorization_witness() {
1991        let sender = Address::random();
1992        let burned_witness = B256::random();
1993        let other_witness = B256::random();
1994
1995        let key_authorization = |witness| {
1996            KeyAuthorization::unrestricted(42431, SignatureType::Secp256k1, Address::random())
1997                .with_witness(witness)
1998                .into_signed(PrimitiveSignature::Secp256k1(Signature::test_signature()))
1999        };
2000
2001        let matching = crate::test_utils::TxBuilder::aa(sender)
2002            .nonce(0)
2003            .key_authorization(key_authorization(burned_witness))
2004            .build();
2005        let untouched = crate::test_utils::TxBuilder::aa(sender)
2006            .nonce(1)
2007            .key_authorization(key_authorization(other_witness))
2008            .build();
2009
2010        let provider = MockEthProvider::<TempoPrimitives>::new()
2011            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
2012        provider.add_account(sender, ExtendedAccount::new(matching.nonce(), U256::MAX));
2013        provider.add_block(
2014            B256::random(),
2015            Block {
2016                header: TempoHeader {
2017                    inner: Header {
2018                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
2019                        ..Default::default()
2020                    },
2021                    ..Default::default()
2022                },
2023                ..Default::default()
2024            },
2025        );
2026
2027        let inner =
2028            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
2029                .disable_balance_check()
2030                .build(InMemoryBlobStore::default());
2031        let amm_cache =
2032            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
2033        let validator = TempoTransactionValidator::new(
2034            inner,
2035            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
2036            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
2037            amm_cache,
2038        );
2039
2040        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
2041        let protocol_pool = Pool::new(
2042            executor,
2043            TempoTipOrdering::default(),
2044            InMemoryBlobStore::default(),
2045            PoolConfig::default(),
2046        );
2047        let pool = TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()));
2048
2049        for pooled in [&matching, &untouched] {
2050            let validated = TransactionValidationOutcome::Valid {
2051                balance: *pooled.cost(),
2052                state_nonce: pooled.nonce(),
2053                bytecode_hash: None,
2054                transaction: ValidTransaction::new(pooled.clone(), None),
2055                propagate: true,
2056                authorities: None,
2057            };
2058            pool.add_validated_transaction(TransactionOrigin::External, validated)
2059                .expect("transaction should be admitted");
2060        }
2061
2062        let mut updates = crate::maintain::TempoPoolUpdates::new();
2063        updates
2064            .key_authorization_witness_burns
2065            .entry(sender)
2066            .or_default()
2067            .insert(burned_witness);
2068
2069        let evicted = pool.evict_invalidated_transactions(&updates);
2070        assert_eq!(tx_hashes(&evicted), vec![*matching.hash()]);
2071        assert!(pool.get(matching.hash()).is_none());
2072        assert!(pool.get(untouched.hash()).is_some());
2073    }
2074
2075    #[tokio::test]
2076    async fn evicts_transactions_with_revoked_key_authorization_signer() {
2077        let sender = Address::random();
2078        let admin_signer = PrivateKeySigner::random();
2079        let admin_key = alloy_signer::Signer::address(&admin_signer);
2080        let other_signer = PrivateKeySigner::random();
2081
2082        let key_authorization = |signer: &PrivateKeySigner| {
2083            let authorization =
2084                KeyAuthorization::unrestricted(42431, SignatureType::Secp256k1, Address::random())
2085                    .with_account(sender);
2086            let signature = signer
2087                .sign_hash_sync(&authorization.signature_hash())
2088                .expect("key authorization signing should succeed");
2089            authorization.into_signed(PrimitiveSignature::Secp256k1(signature))
2090        };
2091
2092        let matching = crate::test_utils::TxBuilder::aa(sender)
2093            .nonce(0)
2094            .key_authorization(key_authorization(&admin_signer))
2095            .build();
2096        let untouched = crate::test_utils::TxBuilder::aa(sender)
2097            .nonce(1)
2098            .key_authorization(key_authorization(&other_signer))
2099            .build();
2100
2101        let provider = MockEthProvider::<TempoPrimitives>::new()
2102            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
2103        provider.add_account(sender, ExtendedAccount::new(matching.nonce(), U256::MAX));
2104        provider.add_block(
2105            B256::random(),
2106            Block {
2107                header: TempoHeader {
2108                    inner: Header {
2109                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
2110                        ..Default::default()
2111                    },
2112                    ..Default::default()
2113                },
2114                ..Default::default()
2115            },
2116        );
2117
2118        let inner =
2119            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
2120                .disable_balance_check()
2121                .build(InMemoryBlobStore::default());
2122        let amm_cache =
2123            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
2124        let validator = TempoTransactionValidator::new(
2125            inner,
2126            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
2127            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
2128            amm_cache,
2129        );
2130
2131        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
2132        let protocol_pool = Pool::new(
2133            executor,
2134            TempoTipOrdering::default(),
2135            InMemoryBlobStore::default(),
2136            PoolConfig::default(),
2137        );
2138        let pool = TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()));
2139
2140        for pooled in [&matching, &untouched] {
2141            let validated = TransactionValidationOutcome::Valid {
2142                balance: *pooled.cost(),
2143                state_nonce: pooled.nonce(),
2144                bytecode_hash: None,
2145                transaction: ValidTransaction::new(pooled.clone(), None),
2146                propagate: true,
2147                authorities: None,
2148            };
2149            pool.add_validated_transaction(TransactionOrigin::External, validated)
2150                .expect("transaction should be admitted");
2151        }
2152
2153        let mut updates = crate::maintain::TempoPoolUpdates::new();
2154        updates.revoked_keys.insert(sender, admin_key);
2155
2156        let evicted = pool.evict_invalidated_transactions(&updates);
2157        assert_eq!(tx_hashes(&evicted), vec![*matching.hash()]);
2158        assert!(pool.get(matching.hash()).is_none());
2159        assert!(pool.get(untouched.hash()).is_some());
2160    }
2161
2162    #[tokio::test]
2163    async fn evicts_transactions_with_stale_key_authorization_target() {
2164        let sender = Address::random();
2165        let signer = PrivateKeySigner::random();
2166        let target_key = Address::random();
2167        let other_key = Address::random();
2168
2169        let key_authorization = |key_id| {
2170            let authorization =
2171                KeyAuthorization::unrestricted(42431, SignatureType::Secp256k1, key_id)
2172                    .with_account(sender);
2173            let signature = signer
2174                .sign_hash_sync(&authorization.signature_hash())
2175                .expect("key authorization signing should succeed");
2176            authorization.into_signed(PrimitiveSignature::Secp256k1(signature))
2177        };
2178
2179        let matching = crate::test_utils::TxBuilder::aa(sender)
2180            .nonce(0)
2181            .key_authorization(key_authorization(target_key))
2182            .build();
2183        let untouched = crate::test_utils::TxBuilder::aa(sender)
2184            .nonce(1)
2185            .key_authorization(key_authorization(other_key))
2186            .build();
2187
2188        let provider = MockEthProvider::<TempoPrimitives>::new()
2189            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
2190        provider.add_account(sender, ExtendedAccount::new(matching.nonce(), U256::MAX));
2191        provider.add_block(
2192            B256::random(),
2193            Block {
2194                header: TempoHeader {
2195                    inner: Header {
2196                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
2197                        ..Default::default()
2198                    },
2199                    ..Default::default()
2200                },
2201                ..Default::default()
2202            },
2203        );
2204
2205        let inner =
2206            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
2207                .disable_balance_check()
2208                .build(InMemoryBlobStore::default());
2209        let amm_cache =
2210            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
2211        let validator = TempoTransactionValidator::new(
2212            inner,
2213            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
2214            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
2215            amm_cache,
2216        );
2217
2218        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
2219        let protocol_pool = Pool::new(
2220            executor,
2221            TempoTipOrdering::default(),
2222            InMemoryBlobStore::default(),
2223            PoolConfig::default(),
2224        );
2225        let pool = TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()));
2226
2227        for pooled in [&matching, &untouched] {
2228            let validated = TransactionValidationOutcome::Valid {
2229                balance: *pooled.cost(),
2230                state_nonce: pooled.nonce(),
2231                bytecode_hash: None,
2232                transaction: ValidTransaction::new(pooled.clone(), None),
2233                propagate: true,
2234                authorities: None,
2235            };
2236            pool.add_validated_transaction(TransactionOrigin::External, validated)
2237                .expect("transaction should be admitted");
2238        }
2239
2240        let mut updates = crate::maintain::TempoPoolUpdates::new();
2241        updates
2242            .key_authorization_target_changes
2243            .insert(sender, target_key);
2244
2245        let evicted = pool.evict_invalidated_transactions(&updates);
2246        assert_eq!(tx_hashes(&evicted), vec![*matching.hash()]);
2247        assert!(pool.get(matching.hash()).is_none());
2248        assert!(pool.get(untouched.hash()).is_some());
2249    }
2250
2251    /// Eviction must match sub-policy IDs against compound policies.
2252    /// When a token uses a compound policy, and a sub-policy event fires,
2253    /// the eviction comparison must detect the match.
2254    #[test]
2255    fn compound_policy_sub_policy_matches_eviction_check() {
2256        let fee_token = address!("20C0000000000000000000000000000000000001");
2257        let compound_policy_id: u64 = 5;
2258        let sender_sub_policy: u64 = 3;
2259        let recipient_sub_policy: u64 = 4;
2260
2261        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2262            tempo_chainspec::spec::MODERATO.clone(),
2263        ));
2264
2265        // Set up TIP20 token with transfer_policy_id = compound_policy_id
2266        let transfer_policy_id_packed =
2267            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
2268        provider.add_account(
2269            fee_token,
2270            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
2271                tip20_slots::TRANSFER_POLICY_ID.into(),
2272                transfer_policy_id_packed,
2273            )]),
2274        );
2275
2276        // Set up TIP403 registry with compound policy pointing to sub-policies
2277        provider
2278            .setup_storage(TempoHardfork::default(), || {
2279                let mut registry = TIP403Registry::new();
2280                registry.policy_records[compound_policy_id]
2281                    .base
2282                    .write(PolicyData {
2283                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
2284                        admin: Address::ZERO,
2285                    })?;
2286                registry.policy_records[compound_policy_id]
2287                    .compound
2288                    .write(CompoundPolicyData {
2289                        sender_policy_id: sender_sub_policy,
2290                        recipient_policy_id: recipient_sub_policy,
2291                        mint_recipient_policy_id: 0,
2292                    })
2293            })
2294            .unwrap();
2295
2296        let mut state = provider.latest().unwrap();
2297        let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
2298
2299        let ids =
2300            get_sender_policy_ids(&mut state, fee_token, TempoHardfork::default(), &mut cache)
2301                .expect("should resolve policy IDs");
2302
2303        assert!(
2304            ids.contains(&compound_policy_id),
2305            "should contain compound policy ID"
2306        );
2307        assert!(
2308            ids.contains(&sender_sub_policy),
2309            "should contain sender sub-policy"
2310        );
2311    }
2312
2313    /// fee_payer is only checked against sender sub-policy at execution time,
2314    /// so sender_policy_ids must NOT contain recipient_sub_policy.
2315    #[test]
2316    fn compound_policy_sender_ids_exclude_recipient_sub_policy() {
2317        let fee_token = address!("20C0000000000000000000000000000000000001");
2318        let compound_policy_id: u64 = 5;
2319        let sender_sub_policy: u64 = 3;
2320        let recipient_sub_policy: u64 = 4;
2321
2322        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2323            tempo_chainspec::spec::MODERATO.clone(),
2324        ));
2325
2326        let transfer_policy_id_packed =
2327            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
2328        provider.add_account(
2329            fee_token,
2330            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
2331                tip20_slots::TRANSFER_POLICY_ID.into(),
2332                transfer_policy_id_packed,
2333            )]),
2334        );
2335
2336        provider
2337            .setup_storage(TempoHardfork::default(), || {
2338                let mut registry = TIP403Registry::new();
2339                registry.policy_records[compound_policy_id]
2340                    .base
2341                    .write(PolicyData {
2342                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
2343                        admin: Address::ZERO,
2344                    })?;
2345                registry.policy_records[compound_policy_id]
2346                    .compound
2347                    .write(CompoundPolicyData {
2348                        sender_policy_id: sender_sub_policy,
2349                        recipient_policy_id: recipient_sub_policy,
2350                        mint_recipient_policy_id: 0,
2351                    })
2352            })
2353            .unwrap();
2354
2355        let mut state = provider.latest().unwrap();
2356        let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
2357
2358        let ids =
2359            get_sender_policy_ids(&mut state, fee_token, TempoHardfork::default(), &mut cache)
2360                .expect("should resolve policy IDs");
2361
2362        assert!(ids.contains(&compound_policy_id));
2363        assert!(ids.contains(&sender_sub_policy));
2364        assert!(
2365            !ids.contains(&recipient_sub_policy),
2366            "sender policy IDs should not contain recipient_sub_policy"
2367        );
2368    }
2369
2370    /// mint_recipient_policy_id is never consulted for fee transfers,
2371    /// so it must be excluded from sender policy IDs.
2372    #[test]
2373    fn compound_policy_excludes_mint_recipient() {
2374        let fee_token = address!("20C0000000000000000000000000000000000001");
2375        let compound_policy_id: u64 = 5;
2376        let sender_sub: u64 = 3;
2377        let recipient_sub: u64 = 4;
2378        let mint_recipient_sub: u64 = 6;
2379
2380        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2381            tempo_chainspec::spec::MODERATO.clone(),
2382        ));
2383
2384        let transfer_policy_id_packed =
2385            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
2386        provider.add_account(
2387            fee_token,
2388            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
2389                tip20_slots::TRANSFER_POLICY_ID.into(),
2390                transfer_policy_id_packed,
2391            )]),
2392        );
2393
2394        provider
2395            .setup_storage(TempoHardfork::default(), || {
2396                let mut registry = TIP403Registry::new();
2397                registry.policy_records[compound_policy_id]
2398                    .base
2399                    .write(PolicyData {
2400                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
2401                        admin: Address::ZERO,
2402                    })?;
2403                registry.policy_records[compound_policy_id]
2404                    .compound
2405                    .write(CompoundPolicyData {
2406                        sender_policy_id: sender_sub,
2407                        recipient_policy_id: recipient_sub,
2408                        mint_recipient_policy_id: mint_recipient_sub,
2409                    })
2410            })
2411            .unwrap();
2412
2413        let mut state = provider.latest().unwrap();
2414        let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
2415
2416        let ids =
2417            get_sender_policy_ids(&mut state, fee_token, TempoHardfork::default(), &mut cache)
2418                .expect("should resolve policy IDs");
2419
2420        assert!(
2421            !ids.contains(&mint_recipient_sub),
2422            "mint_recipient must be excluded from sender policy IDs"
2423        );
2424    }
2425
2426    /// `get_recipient_policy_ids` returns the compound root and recipient sub-policy.
2427    #[test]
2428    fn recipient_policy_ids_includes_recipient_sub_policy() {
2429        let fee_token = address!("20C0000000000000000000000000000000000001");
2430        let compound_policy_id: u64 = 5;
2431        let sender_sub: u64 = 3;
2432        let recipient_sub: u64 = 4;
2433
2434        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2435            tempo_chainspec::spec::MODERATO.clone(),
2436        ));
2437
2438        let transfer_policy_id_packed =
2439            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
2440        provider.add_account(
2441            fee_token,
2442            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
2443                tip20_slots::TRANSFER_POLICY_ID.into(),
2444                transfer_policy_id_packed,
2445            )]),
2446        );
2447
2448        provider
2449            .setup_storage(TempoHardfork::default(), || {
2450                let mut registry = TIP403Registry::new();
2451                registry.policy_records[compound_policy_id]
2452                    .base
2453                    .write(PolicyData {
2454                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
2455                        admin: Address::ZERO,
2456                    })?;
2457                registry.policy_records[compound_policy_id]
2458                    .compound
2459                    .write(CompoundPolicyData {
2460                        sender_policy_id: sender_sub,
2461                        recipient_policy_id: recipient_sub,
2462                        mint_recipient_policy_id: 0,
2463                    })
2464            })
2465            .unwrap();
2466
2467        let mut state = provider.latest().unwrap();
2468        let ids = get_recipient_policy_ids(&mut state, fee_token, TempoHardfork::default())
2469            .expect("should resolve policy IDs");
2470
2471        assert!(
2472            ids.contains(&compound_policy_id),
2473            "should contain compound policy ID"
2474        );
2475        assert!(
2476            ids.contains(&recipient_sub),
2477            "should contain recipient sub-policy"
2478        );
2479        assert!(
2480            !ids.contains(&sender_sub),
2481            "recipient policy IDs should not contain sender sub-policy"
2482        );
2483    }
2484
2485    /// For simple (non-compound) policies, `get_recipient_policy_ids` returns just the root.
2486    #[test]
2487    fn recipient_policy_ids_simple_policy() {
2488        let fee_token = address!("20C0000000000000000000000000000000000001");
2489        let simple_policy_id: u64 = 7;
2490
2491        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2492            tempo_chainspec::spec::MODERATO.clone(),
2493        ));
2494
2495        let transfer_policy_id_packed =
2496            U256::from(simple_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
2497        provider.add_account(
2498            fee_token,
2499            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
2500                tip20_slots::TRANSFER_POLICY_ID.into(),
2501                transfer_policy_id_packed,
2502            )]),
2503        );
2504
2505        provider
2506            .setup_storage(TempoHardfork::default(), || {
2507                let mut registry = TIP403Registry::new();
2508                registry.policy_records[simple_policy_id]
2509                    .base
2510                    .write(PolicyData {
2511                        policy_type: ITIP403Registry::PolicyType::BLACKLIST as u8,
2512                        admin: Address::ZERO,
2513                    })
2514            })
2515            .unwrap();
2516
2517        let mut state = provider.latest().unwrap();
2518        let ids = get_recipient_policy_ids(&mut state, fee_token, TempoHardfork::default())
2519            .expect("should resolve policy IDs");
2520
2521        assert_eq!(ids, vec![simple_policy_id]);
2522    }
2523
2524    #[test]
2525    fn exceeds_spending_limit_returns_true_when_cost_exceeds_remaining() {
2526        let account = Address::random();
2527        let key_id = Address::random();
2528        let fee_token = Address::random();
2529        let subject = KeychainSubject {
2530            account,
2531            key_id,
2532            fee_token,
2533        };
2534
2535        let mut state = provider_with_spending_limit(
2536            account,
2537            key_id,
2538            fee_token,
2539            alloy_primitives::U256::from(100),
2540        );
2541
2542        assert!(exceeds_spending_limit(
2543            &mut state,
2544            &subject,
2545            alloy_primitives::U256::from(200),
2546            0,
2547            TempoHardfork::default(),
2548        ));
2549    }
2550
2551    #[test]
2552    fn exceeds_spending_limit_returns_false_when_cost_within_limit() {
2553        let account = Address::random();
2554        let key_id = Address::random();
2555        let fee_token = Address::random();
2556        let subject = KeychainSubject {
2557            account,
2558            key_id,
2559            fee_token,
2560        };
2561
2562        let mut state = provider_with_spending_limit(
2563            account,
2564            key_id,
2565            fee_token,
2566            alloy_primitives::U256::from(500),
2567        );
2568
2569        assert!(!exceeds_spending_limit(
2570            &mut state,
2571            &subject,
2572            alloy_primitives::U256::from(200),
2573            0,
2574            TempoHardfork::default(),
2575        ));
2576    }
2577
2578    #[test]
2579    fn exceeds_spending_limit_returns_true_when_no_limit_set() {
2580        let account = Address::random();
2581        let key_id = Address::random();
2582        let fee_token = Address::random();
2583        let subject = KeychainSubject {
2584            account,
2585            key_id,
2586            fee_token,
2587        };
2588
2589        // Provider with AuthorizedKey (enforce_limits=true) but no spending limit slot
2590        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2591            tempo_chainspec::spec::MODERATO.clone(),
2592        ));
2593        provider
2594            .setup_storage(TempoHardfork::default(), || {
2595                AccountKeychain::new().keys[account][key_id].write(AuthorizedKey {
2596                    signature_type: StoredSignatureType::Secp256k1,
2597                    expiry: u64::MAX,
2598                    enforce_limits: true,
2599                    is_revoked: false,
2600                    is_admin: false,
2601                })
2602            })
2603            .unwrap();
2604
2605        assert!(exceeds_spending_limit(
2606            &mut provider.latest().unwrap(),
2607            &subject,
2608            alloy_primitives::U256::from(1),
2609            0,
2610            TempoHardfork::default(),
2611        ));
2612    }
2613
2614    #[test]
2615    fn exceeds_spending_limit_returns_false_when_limits_not_enforced() {
2616        let account = Address::random();
2617        let key_id = Address::random();
2618        let fee_token = Address::random();
2619        let subject = KeychainSubject {
2620            account,
2621            key_id,
2622            fee_token,
2623        };
2624
2625        // Provider with AuthorizedKey (enforce_limits=false)
2626        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
2627            tempo_chainspec::spec::MODERATO.clone(),
2628        ));
2629        provider
2630            .setup_storage(TempoHardfork::default(), || {
2631                AccountKeychain::new().keys[account][key_id].write(AuthorizedKey {
2632                    signature_type: StoredSignatureType::Secp256k1,
2633                    expiry: u64::MAX,
2634                    enforce_limits: false,
2635                    is_revoked: false,
2636                    is_admin: false,
2637                })
2638            })
2639            .unwrap();
2640
2641        assert!(!exceeds_spending_limit(
2642            &mut provider.latest().unwrap(),
2643            &subject,
2644            alloy_primitives::U256::from(1),
2645            0,
2646            TempoHardfork::default(),
2647        ));
2648    }
2649
2650    #[test]
2651    fn exceeds_spending_limit_uses_period_reset_after_rollover() {
2652        let account = Address::random();
2653        let key_id = Address::random();
2654        let fee_token = Address::random();
2655        let subject = KeychainSubject {
2656            account,
2657            key_id,
2658            fee_token,
2659        };
2660
2661        let mut state = provider_with_spending_limit_state(
2662            account,
2663            key_id,
2664            fee_token,
2665            SpendingLimitState {
2666                remaining: alloy_primitives::U256::ZERO,
2667                max: 100,
2668                period: 60,
2669                period_end: 10,
2670            },
2671            TempoHardfork::T3,
2672        );
2673
2674        assert!(!exceeds_spending_limit(
2675            &mut state,
2676            &subject,
2677            alloy_primitives::U256::from(50),
2678            10,
2679            TempoHardfork::T3,
2680        ));
2681        assert!(exceeds_spending_limit(
2682            &mut state,
2683            &subject,
2684            alloy_primitives::U256::from(150),
2685            10,
2686            TempoHardfork::T3,
2687        ));
2688    }
2689}