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, transaction::TempoPooledTransaction,
7    tt_2d_pool::AA2dPool, validator::TempoTransactionValidator,
8};
9use alloy_consensus::Transaction;
10use alloy_primitives::{
11    Address, B256, TxHash, U256,
12    map::{AddressMap, AddressSet, HashMap},
13};
14use parking_lot::RwLock;
15use reth_chainspec::ChainSpecProvider;
16use reth_eth_wire_types::HandleMempoolData;
17use reth_provider::{ChangedAccount, StateProviderFactory};
18use reth_storage_api::StateProvider;
19use reth_transaction_pool::{
20    AddedTransactionOutcome, AllPoolTransactions, BestTransactions, BestTransactionsAttributes,
21    BlockInfo, CanonicalStateUpdate, CoinbaseTipOrdering, GetPooledTransactionLimit,
22    NewBlobSidecar, Pool, PoolResult, PoolSize, PoolTransaction, PropagatedTransactions,
23    TransactionEvents, TransactionOrigin, TransactionPool, TransactionPoolExt,
24    TransactionValidationOutcome, TransactionValidationTaskExecutor, TransactionValidator,
25    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::{
33    TempoChainSpec,
34    hardfork::{TempoHardfork, TempoHardforks},
35};
36use tempo_precompiles::{
37    DEFAULT_FEE_TOKEN, TIP_FEE_MANAGER_ADDRESS,
38    account_keychain::AccountKeychain,
39    error::Result as TempoPrecompileResult,
40    storage::Handler,
41    tip20::TIP20Token,
42    tip403_registry::{REJECT_ALL_POLICY_ID, TIP403Registry},
43};
44use tempo_primitives::Block;
45use tempo_revm::TempoStateAccess;
46
47/// Tempo transaction pool that routes based on nonce_key
48pub struct TempoTransactionPool<Client> {
49    /// Vanilla pool for all standard transactions and AA transactions with regular nonce.
50    protocol_pool: Pool<
51        TransactionValidationTaskExecutor<TempoTransactionValidator<Client>>,
52        CoinbaseTipOrdering<TempoPooledTransaction>,
53        InMemoryBlobStore,
54    >,
55    /// Minimal pool for 2D nonces (nonce_key > 0)
56    aa_2d_pool: Arc<RwLock<AA2dPool>>,
57}
58
59impl<Client> TempoTransactionPool<Client> {
60    pub fn new(
61        protocol_pool: Pool<
62            TransactionValidationTaskExecutor<TempoTransactionValidator<Client>>,
63            CoinbaseTipOrdering<TempoPooledTransaction>,
64            InMemoryBlobStore,
65        >,
66        aa_2d_pool: AA2dPool,
67    ) -> Self {
68        Self {
69            protocol_pool,
70            aa_2d_pool: Arc::new(RwLock::new(aa_2d_pool)),
71        }
72    }
73}
74impl<Client> TempoTransactionPool<Client>
75where
76    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + 'static,
77{
78    /// Obtains a clone of the shared [`AmmLiquidityCache`].
79    pub fn amm_liquidity_cache(&self) -> AmmLiquidityCache {
80        self.protocol_pool
81            .validator()
82            .validator()
83            .amm_liquidity_cache()
84    }
85
86    /// Returns the configured client
87    pub fn client(&self) -> &Client {
88        self.protocol_pool.validator().validator().client()
89    }
90
91    /// Updates the 2d nonce pool with the given state changes.
92    pub(crate) fn notify_aa_pool_on_state_updates(&self, state: &AddressMap<BundleAccount>) {
93        let (promoted, _mined) = self.aa_2d_pool.write().on_state_updates(state);
94        // Note: mined transactions are notified via the vanilla pool updates
95        self.protocol_pool
96            .inner()
97            .notify_on_transaction_updates(promoted, Vec::new());
98    }
99
100    /// Evicts transactions that are no longer valid due to on-chain events.
101    ///
102    /// This performs a single scan of all pooled transactions and checks for:
103    /// 1. **Revoked keychain keys**: AA transactions signed with keys that have been revoked
104    /// 2. **Spending limit updates**: AA transactions signed with keys whose spending limit
105    ///    changed for a token matching the transaction's fee token
106    ///    2b. **Spending limit spends**: AA transactions whose remaining spending limit (re-read
107    ///    from state) is now insufficient after included keychain txs decremented it
108    /// 3. **Validator token changes**: Transactions that would fail due to insufficient
109    ///    liquidity in the new (user_token, validator_token) AMM pool
110    /// 4. **Fee payer balance changes**: Transactions whose fee payer no longer has enough
111    ///    balance in the resolved fee token after a TIP20 transfer
112    ///
113    /// All checks are combined into one scan to avoid iterating the pool multiple times
114    /// per block.
115    pub fn evict_invalidated_transactions(
116        &self,
117        updates: &crate::maintain::TempoPoolUpdates,
118    ) -> Vec<TxHash> {
119        if !updates.has_invalidation_events() {
120            return Vec::new();
121        }
122
123        // Fetch state provider if any check needs on-chain reads:
124        // - validator token changes (liquidity check)
125        // - blacklist/whitelist (policy check)
126        // - fee payer balance changes (balance check)
127        // - spending limit spends (remaining limit check)
128        let mut state_provider = if !updates.validator_token_changes.is_empty()
129            || !updates.blacklist_additions.is_empty()
130            || !updates.whitelist_removals.is_empty()
131            || !updates.fee_balance_changes.is_empty()
132            || !updates.spending_limit_spends.is_empty()
133        {
134            self.client().latest().ok()
135        } else {
136            None
137        };
138
139        // Resolve the active hardfork for storage context.
140        let tip_timestamp = self
141            .protocol_pool
142            .validator()
143            .validator()
144            .inner
145            .fork_tracker()
146            .tip_timestamp();
147        let spec = self.client().chain_spec().tempo_hardfork_at(tip_timestamp);
148
149        // Cache policy lookups per fee token to avoid redundant storage reads.
150        // For compound policies (TIP-1015), the cache stores all sub-policy IDs
151        // so eviction matches events emitted with sub-policy IDs.
152        let mut policy_cache: AddressMap<Vec<u64>> = AddressMap::default();
153
154        // Pre-collect policy IDs where TIP_FEE_MANAGER_ADDRESS (the fee recipient) was
155        // blacklisted or un-whitelisted. This is constant across all txs so we compute
156        // it once instead of re-scanning the updates list per transaction.
157        let fee_manager_blacklisted: Vec<u64> = updates
158            .blacklist_additions
159            .iter()
160            .filter(|(_, account)| *account == TIP_FEE_MANAGER_ADDRESS)
161            .map(|(policy_id, _)| *policy_id)
162            .collect();
163        let fee_manager_unwhitelisted: Vec<u64> = updates
164            .whitelist_removals
165            .iter()
166            .filter(|(_, account)| *account == TIP_FEE_MANAGER_ADDRESS)
167            .map(|(policy_id, _)| *policy_id)
168            .collect();
169
170        // Re-check liquidity for all pooled txs when an active validator changes token.
171        // Leverages the per-tx `has_enough_liquidity` check, which passes if ANY validator pair has
172        // enough liquidity, matching admission and preventing mass-eviction of valid txs.
173        let amm_cache = self.amm_liquidity_cache();
174        let has_active_validator_token_changes = !updates.validator_token_changes.is_empty() && {
175            let active_new_tokens: Vec<_> = updates
176                .validator_token_changes
177                .iter()
178                .filter(|(validator, _)| amm_cache.is_active_validator(validator))
179                .filter(|(_, new_token)| !amm_cache.is_active_validator_token(new_token))
180                .map(|(_, &new_token)| new_token)
181                .collect();
182            amm_cache.track_tokens(&active_new_tokens)
183        };
184
185        let mut to_remove = Vec::new();
186        let mut revoked_count = 0;
187        let mut spending_limit_count = 0;
188        let mut spending_limit_spend_count = 0;
189        let mut liquidity_count = 0;
190        let mut user_token_count = 0;
191        let mut blacklisted_count = 0;
192        let mut unwhitelisted_count = 0;
193        let mut insolvent_fee_payer_count = 0;
194        let mut fee_balance_cache: HashMap<(Address, Address), U256> = HashMap::default();
195
196        let all_txs = self.all_transactions();
197        for tx in all_txs.pending.iter().chain(all_txs.queued.iter()) {
198            // Extract keychain subject once per transaction (if applicable)
199            let keychain_subject = tx.transaction.keychain_subject();
200
201            // Check 1: Revoked keychain keys
202            if !updates.revoked_keys.is_empty()
203                && let Some(ref subject) = keychain_subject
204                && subject.matches_revoked(&updates.revoked_keys)
205            {
206                to_remove.push(*tx.hash());
207                revoked_count += 1;
208                continue;
209            }
210
211            // Check 2: Spending limit updates
212            // Only evict if the transaction's fee token matches the token whose limit changed.
213            if !updates.spending_limit_changes.is_empty()
214                && let Some(ref subject) = keychain_subject
215                && subject.matches_spending_limit_update(&updates.spending_limit_changes)
216            {
217                to_remove.push(*tx.hash());
218                spending_limit_count += 1;
219                continue;
220            }
221
222            // Check 2b: Spending limit spends
223            // AccessKeySpend receipt logs identify the exact (account, key_id, token)
224            // triples whose remaining limit changed during execution. We re-read the
225            // current remaining limit from state for matching pending txs and evict if
226            // the tx's fee cost now exceeds that remaining limit.
227            if !updates.spending_limit_spends.is_empty()
228                && let Some(ref subject) = keychain_subject
229                && subject.matches_spending_limit_update(&updates.spending_limit_spends)
230                && let Some(ref mut provider) = state_provider
231                && exceeds_spending_limit(
232                    provider,
233                    subject,
234                    tx.transaction.fee_token_cost(),
235                    tip_timestamp,
236                    spec,
237                )
238            {
239                to_remove.push(*tx.hash());
240                spending_limit_spend_count += 1;
241                continue;
242            }
243
244            // Check 3: Validator token changes (re-check liquidity for all transactions)
245            // Prevents mass eviction because it only:
246            // - evicts when NO validator token has enough liquidity
247            // - considers active validators (protects from permissionless `setValidatorToken`)
248            if has_active_validator_token_changes && let Some(ref mut provider) = state_provider {
249                let user_token = tx
250                    .transaction
251                    .inner()
252                    .fee_token()
253                    .unwrap_or(tempo_precompiles::DEFAULT_FEE_TOKEN);
254                let cost = tx.transaction.fee_token_cost();
255
256                match amm_cache.has_enough_liquidity(user_token, cost, provider) {
257                    Ok(true) => {}
258                    Ok(false) => {
259                        to_remove.push(*tx.hash());
260                        liquidity_count += 1;
261                        continue;
262                    }
263                    Err(_) => continue,
264                }
265            }
266
267            // Check 3b: Fee payer balance changes.
268            // When a TIP20 transfer changes a fee payer's balance, pending transactions for that
269            // (fee_token, fee_payer) pair may no longer be executable.
270            if !updates.fee_balance_changes.is_empty()
271                && let Some(ref mut provider) = state_provider
272            {
273                let fee_token = tx.transaction.resolved_fee_token().unwrap_or_else(|| {
274                    tx.transaction
275                        .inner()
276                        .fee_token()
277                        .unwrap_or(DEFAULT_FEE_TOKEN)
278                });
279                let Ok(fee_payer) = tx.transaction.inner().fee_payer(tx.transaction.sender())
280                else {
281                    continue;
282                };
283
284                if updates
285                    .fee_balance_changes
286                    .get(&fee_token)
287                    .is_some_and(|accounts| accounts.contains(&fee_payer))
288                {
289                    let key = (fee_token, fee_payer);
290                    let balance = if let Some(balance) = fee_balance_cache.get(&key).copied() {
291                        balance
292                    } else {
293                        let Ok(balance) = provider.get_token_balance(fee_token, fee_payer, spec)
294                        else {
295                            continue;
296                        };
297                        fee_balance_cache.insert(key, balance);
298                        balance
299                    };
300
301                    if balance < tx.transaction.fee_token_cost() {
302                        to_remove.push(*tx.hash());
303                        insolvent_fee_payer_count += 1;
304                        continue;
305                    }
306                }
307            }
308
309            // Check 4: Blacklisted fee payers
310            // Only check AA transactions with a fee token (non-AA transactions don't have
311            // a fee payer that can be blacklisted via TIP403)
312            if !updates.blacklist_additions.is_empty()
313                && let Some(ref mut provider) = state_provider
314                && let Some(fee_token) = tx.transaction.inner().fee_token()
315            {
316                let fee_payer = tx
317                    .transaction
318                    .inner()
319                    .fee_payer(tx.transaction.sender())
320                    .unwrap_or(tx.transaction.sender());
321
322                // Check if any blacklist addition applies to this transaction's fee payer
323                let mut sender_evicted = false;
324                for &(blacklist_policy_id, blacklisted_account) in &updates.blacklist_additions {
325                    if fee_payer != blacklisted_account {
326                        continue;
327                    }
328
329                    let token_policies =
330                        get_sender_policy_ids(provider, fee_token, spec, &mut policy_cache);
331
332                    if token_policies
333                        .as_ref()
334                        .is_some_and(|ids| ids.contains(&blacklist_policy_id))
335                    {
336                        sender_evicted = true;
337                        break;
338                    }
339                }
340
341                // Check if the fee manager (recipient) was blacklisted on this token's
342                // recipient policy — the tx would fail at execution since the fee
343                // transfer to TIP_FEE_MANAGER_ADDRESS would be rejected.
344                let recipient_evicted = !sender_evicted
345                    && !fee_manager_blacklisted.is_empty()
346                    && get_recipient_policy_ids(provider, fee_token, spec)
347                        .is_some_and(|ids| fee_manager_blacklisted.iter().any(|p| ids.contains(p)));
348
349                if sender_evicted || recipient_evicted {
350                    to_remove.push(*tx.hash());
351                    blacklisted_count += 1;
352                }
353            }
354
355            // Check 5: Un-whitelisted fee payers
356            // When a fee payer is removed from a whitelist, their pending transactions
357            // will fail validation at execution time.
358            if !updates.whitelist_removals.is_empty()
359                && let Some(ref mut provider) = state_provider
360                && let Some(fee_token) = tx.transaction.inner().fee_token()
361            {
362                let fee_payer = tx
363                    .transaction
364                    .inner()
365                    .fee_payer(tx.transaction.sender())
366                    .unwrap_or(tx.transaction.sender());
367
368                let mut sender_evicted = false;
369                for &(whitelist_policy_id, unwhitelisted_account) in &updates.whitelist_removals {
370                    if fee_payer != unwhitelisted_account {
371                        continue;
372                    }
373
374                    let token_policies =
375                        get_sender_policy_ids(provider, fee_token, spec, &mut policy_cache);
376
377                    if token_policies
378                        .as_ref()
379                        .is_some_and(|ids| ids.contains(&whitelist_policy_id))
380                    {
381                        sender_evicted = true;
382                        break;
383                    }
384                }
385
386                // Check if the fee manager (recipient) was un-whitelisted on this
387                // token's recipient policy.
388                let recipient_evicted = !sender_evicted
389                    && !fee_manager_unwhitelisted.is_empty()
390                    && get_recipient_policy_ids(provider, fee_token, spec).is_some_and(|ids| {
391                        fee_manager_unwhitelisted.iter().any(|p| ids.contains(p))
392                    });
393
394                if sender_evicted || recipient_evicted {
395                    to_remove.push(*tx.hash());
396                    unwhitelisted_count += 1;
397                }
398            }
399
400            // Check 6: User fee token preference changes
401            // When a user changes their fee token preference via setUserToken(), transactions
402            // from that user that don't have an explicit fee_token set may now resolve to a
403            // different token at execution time, causing fee payment failures.
404            // Only evict transactions WITHOUT an explicit fee_token (those that rely on storage).
405            if !updates.user_token_changes.is_empty()
406                && tx.transaction.inner().fee_token().is_none()
407                && updates
408                    .user_token_changes
409                    .contains(&tx.transaction.sender())
410            {
411                to_remove.push(*tx.hash());
412                user_token_count += 1;
413            }
414        }
415
416        if !to_remove.is_empty() {
417            tracing::debug!(
418                target: "txpool",
419                total = to_remove.len(),
420                revoked_count,
421                spending_limit_count,
422                spending_limit_spend_count,
423                liquidity_count,
424                user_token_count,
425                blacklisted_count,
426                unwhitelisted_count,
427                insolvent_fee_payer_count,
428                "Evicting invalidated transactions"
429            );
430            self.remove_transactions(to_remove.clone());
431        }
432        to_remove
433    }
434
435    fn add_validated_transaction(
436        &self,
437        origin: TransactionOrigin,
438        transaction: TransactionValidationOutcome<TempoPooledTransaction>,
439    ) -> PoolResult<AddedTransactionOutcome> {
440        match transaction {
441            TransactionValidationOutcome::Valid {
442                balance,
443                state_nonce,
444                bytecode_hash,
445                transaction,
446                propagate,
447                authorities,
448            } => {
449                if transaction.transaction().is_aa_2d() {
450                    let transaction = transaction.into_transaction();
451                    let sender_id = self
452                        .protocol_pool
453                        .inner()
454                        .get_sender_id(transaction.sender());
455                    let transaction_id = TransactionId::new(sender_id, transaction.nonce());
456                    let tx = ValidPoolTransaction {
457                        transaction,
458                        transaction_id,
459                        propagate,
460                        timestamp: Instant::now(),
461                        origin,
462                        authority_ids: authorities
463                            .map(|auths| self.protocol_pool.inner().get_sender_ids(auths)),
464                    };
465
466                    // Get the active Tempo hardfork for expiring nonce handling
467                    let tip_timestamp = self
468                        .protocol_pool
469                        .validator()
470                        .validator()
471                        .inner
472                        .fork_tracker()
473                        .tip_timestamp();
474                    let hardfork = self.client().chain_spec().tempo_hardfork_at(tip_timestamp);
475
476                    let added = self.aa_2d_pool.write().add_transaction(
477                        Arc::new(tx),
478                        state_nonce,
479                        hardfork,
480                    )?;
481                    let hash = *added.hash();
482                    if let Some(pending) = added.as_pending() {
483                        if pending.discarded.iter().any(|tx| *tx.hash() == hash) {
484                            return Err(PoolError::new(hash, PoolErrorKind::DiscardedOnInsert));
485                        }
486                        self.protocol_pool
487                            .inner()
488                            .on_new_pending_transaction(pending);
489                    }
490
491                    let state = added.transaction_state();
492                    // notify regular event listeners from the protocol pool
493                    self.protocol_pool.inner().notify_event_listeners(&added);
494                    self.protocol_pool
495                        .inner()
496                        .on_new_transaction(added.into_new_transaction_event());
497
498                    Ok(AddedTransactionOutcome { hash, state })
499                } else {
500                    self.protocol_pool
501                        .inner()
502                        .add_transactions(
503                            origin,
504                            std::iter::once(TransactionValidationOutcome::Valid {
505                                balance,
506                                state_nonce,
507                                bytecode_hash,
508                                transaction,
509                                propagate,
510                                authorities,
511                            }),
512                        )
513                        .pop()
514                        .unwrap()
515                }
516            }
517            invalid => {
518                // this forwards for event listener updates
519                self.protocol_pool
520                    .inner()
521                    .add_transactions(origin, Some(invalid))
522                    .pop()
523                    .unwrap()
524            }
525        }
526    }
527}
528
529// Manual Clone implementation
530impl<Client> Clone for TempoTransactionPool<Client> {
531    fn clone(&self) -> Self {
532        Self {
533            protocol_pool: self.protocol_pool.clone(),
534            aa_2d_pool: Arc::clone(&self.aa_2d_pool),
535        }
536    }
537}
538
539// Manual Debug implementation
540impl<Client> std::fmt::Debug for TempoTransactionPool<Client> {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        f.debug_struct("TempoTransactionPool")
543            .field("protocol_pool", &"Pool<...>")
544            .field("aa_2d_nonce_pool", &"AA2dPool<...>")
545            .field("paused_fee_token_pool", &"PausedFeeTokenPool<...>")
546            .finish_non_exhaustive()
547    }
548}
549
550// Implement the TransactionPool trait
551impl<Client> TransactionPool for TempoTransactionPool<Client>
552where
553    Client: StateProviderFactory
554        + ChainSpecProvider<ChainSpec = TempoChainSpec>
555        + Send
556        + Sync
557        + 'static,
558    TempoPooledTransaction: reth_transaction_pool::EthPoolTransaction,
559{
560    type Transaction = TempoPooledTransaction;
561
562    fn pool_size(&self) -> PoolSize {
563        let mut size = self.protocol_pool.pool_size();
564        let (pending, queued) = self.aa_2d_pool.read().pending_and_queued_txn_count();
565        size.pending += pending;
566        size.queued += queued;
567        size
568    }
569
570    fn block_info(&self) -> BlockInfo {
571        self.protocol_pool.block_info()
572    }
573
574    async fn add_transaction_and_subscribe(
575        &self,
576        origin: TransactionOrigin,
577        transaction: Self::Transaction,
578    ) -> PoolResult<TransactionEvents> {
579        let tx = self
580            .protocol_pool
581            .validator()
582            .validate_transaction(origin, transaction)
583            .await;
584        let res = self.add_validated_transaction(origin, tx)?;
585        self.transaction_event_listener(res.hash)
586            .ok_or_else(|| PoolError::new(res.hash, PoolErrorKind::DiscardedOnInsert))
587    }
588
589    async fn add_transaction(
590        &self,
591        origin: TransactionOrigin,
592        transaction: Self::Transaction,
593    ) -> PoolResult<AddedTransactionOutcome> {
594        let tx = self
595            .protocol_pool
596            .validator()
597            .validate_transaction(origin, transaction)
598            .await;
599        self.add_validated_transaction(origin, tx)
600    }
601
602    async fn add_transactions(
603        &self,
604        origin: TransactionOrigin,
605        transactions: Vec<Self::Transaction>,
606    ) -> Vec<PoolResult<AddedTransactionOutcome>> {
607        if transactions.is_empty() {
608            return Vec::new();
609        }
610
611        // Fully delegate to protocol pool for non-2D transactions
612        if !transactions.iter().any(|tx| tx.is_aa_2d()) {
613            return self
614                .protocol_pool
615                .add_transactions(origin, transactions)
616                .await;
617        }
618
619        self.protocol_pool
620            .validator()
621            .validate_transactions_with_origin(origin, transactions)
622            .await
623            .into_iter()
624            .map(|outcome| self.add_validated_transaction(origin, outcome))
625            .collect()
626    }
627
628    async fn add_transactions_with_origins(
629        &self,
630        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
631    ) -> Vec<PoolResult<AddedTransactionOutcome>> {
632        if transactions.is_empty() {
633            return Vec::new();
634        }
635
636        // Fully delegate to protocol pool for non-2D transactions
637        if !transactions.iter().any(|(_, tx)| tx.is_aa_2d()) {
638            return self
639                .protocol_pool
640                .add_transactions_with_origins(transactions)
641                .await;
642        }
643
644        let origins = transactions
645            .iter()
646            .map(|(origin, _)| *origin)
647            .collect::<Vec<_>>();
648
649        self.protocol_pool
650            .validator()
651            .validate_transactions(transactions)
652            .await
653            .into_iter()
654            .zip(origins)
655            .map(|(outcome, origin)| self.add_validated_transaction(origin, outcome))
656            .collect()
657    }
658
659    fn transaction_event_listener(&self, tx_hash: B256) -> Option<TransactionEvents> {
660        self.protocol_pool.transaction_event_listener(tx_hash)
661    }
662
663    fn all_transactions_event_listener(
664        &self,
665    ) -> reth_transaction_pool::AllTransactionsEvents<Self::Transaction> {
666        self.protocol_pool.all_transactions_event_listener()
667    }
668
669    fn pending_transactions_listener_for(
670        &self,
671        kind: reth_transaction_pool::TransactionListenerKind,
672    ) -> tokio::sync::mpsc::Receiver<B256> {
673        self.protocol_pool.pending_transactions_listener_for(kind)
674    }
675
676    fn blob_transaction_sidecars_listener(&self) -> tokio::sync::mpsc::Receiver<NewBlobSidecar> {
677        self.protocol_pool.blob_transaction_sidecars_listener()
678    }
679
680    fn new_transactions_listener_for(
681        &self,
682        kind: reth_transaction_pool::TransactionListenerKind,
683    ) -> tokio::sync::mpsc::Receiver<reth_transaction_pool::NewTransactionEvent<Self::Transaction>>
684    {
685        self.protocol_pool.new_transactions_listener_for(kind)
686    }
687
688    fn pooled_transaction_hashes(&self) -> Vec<B256> {
689        let mut hashes = self.protocol_pool.pooled_transaction_hashes();
690        hashes.extend(self.aa_2d_pool.read().pooled_transactions_hashes_iter());
691        hashes
692    }
693
694    fn pooled_transaction_hashes_max(&self, max: usize) -> Vec<B256> {
695        let protocol_hashes = self.protocol_pool.pooled_transaction_hashes_max(max);
696        if protocol_hashes.len() >= max {
697            return protocol_hashes;
698        }
699        let remaining = max - protocol_hashes.len();
700        let mut hashes = protocol_hashes;
701        hashes.extend(
702            self.aa_2d_pool
703                .read()
704                .pooled_transactions_hashes_iter()
705                .take(remaining),
706        );
707        hashes
708    }
709
710    fn pooled_transactions(&self) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
711        let mut txs = self.protocol_pool.pooled_transactions();
712        txs.extend(self.aa_2d_pool.read().pooled_transactions_iter());
713        txs
714    }
715
716    fn pooled_transactions_max(
717        &self,
718        max: usize,
719    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
720        let mut txs = self.protocol_pool.pooled_transactions_max(max);
721        if txs.len() >= max {
722            return txs;
723        }
724
725        let remaining = max - txs.len();
726        txs.extend(
727            self.aa_2d_pool
728                .read()
729                .pooled_transactions_iter()
730                .take(remaining),
731        );
732        txs
733    }
734
735    fn get_pooled_transaction_elements(
736        &self,
737        tx_hashes: Vec<B256>,
738        limit: GetPooledTransactionLimit,
739    ) -> Vec<<Self::Transaction as PoolTransaction>::Pooled> {
740        let mut out = Vec::new();
741        self.append_pooled_transaction_elements(&tx_hashes, limit, &mut out);
742        out
743    }
744
745    fn append_pooled_transaction_elements(
746        &self,
747        tx_hashes: &[B256],
748        limit: GetPooledTransactionLimit,
749        out: &mut Vec<<Self::Transaction as PoolTransaction>::Pooled>,
750    ) {
751        let mut accumulated_size = 0;
752        self.aa_2d_pool.read().append_pooled_transaction_elements(
753            tx_hashes,
754            limit,
755            &mut accumulated_size,
756            out,
757        );
758
759        // If the limit is already exceeded, don't query the protocol pool
760        if limit.exceeds(accumulated_size) {
761            return;
762        }
763
764        // Adjust the limit for the protocol pool based on what we've already collected
765        let remaining_limit = match limit {
766            GetPooledTransactionLimit::None => GetPooledTransactionLimit::None,
767            GetPooledTransactionLimit::ResponseSizeSoftLimit(max) => {
768                GetPooledTransactionLimit::ResponseSizeSoftLimit(
769                    max.saturating_sub(accumulated_size),
770                )
771            }
772        };
773
774        self.protocol_pool
775            .append_pooled_transaction_elements(tx_hashes, remaining_limit, out);
776    }
777
778    fn get_pooled_transaction_element(
779        &self,
780        tx_hash: B256,
781    ) -> Option<reth_primitives_traits::Recovered<<Self::Transaction as PoolTransaction>::Pooled>>
782    {
783        self.protocol_pool
784            .get_pooled_transaction_element(tx_hash)
785            .or_else(|| {
786                self.aa_2d_pool
787                    .read()
788                    .get(&tx_hash)
789                    .and_then(|tx| tx.transaction.clone_into_pooled().ok())
790            })
791    }
792
793    fn best_transactions(
794        &self,
795    ) -> Box<dyn BestTransactions<Item = Arc<ValidPoolTransaction<Self::Transaction>>>> {
796        let left = self.protocol_pool.inner().best_transactions();
797        let right = self.aa_2d_pool.read().best_transactions();
798        Box::new(MergeBestTransactions::new(left, right))
799    }
800
801    fn best_transactions_with_attributes(
802        &self,
803        _attributes: BestTransactionsAttributes,
804    ) -> Box<dyn BestTransactions<Item = Arc<ValidPoolTransaction<Self::Transaction>>>> {
805        self.best_transactions()
806    }
807
808    fn pending_transactions(&self) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
809        let mut pending = self.protocol_pool.pending_transactions();
810        pending.extend(self.aa_2d_pool.read().pending_transactions());
811        pending
812    }
813
814    fn pending_transactions_max(
815        &self,
816        max: usize,
817    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
818        let protocol_txs = self.protocol_pool.pending_transactions_max(max);
819        if protocol_txs.len() >= max {
820            return protocol_txs;
821        }
822        let remaining = max - protocol_txs.len();
823        let mut txs = protocol_txs;
824        txs.extend(
825            self.aa_2d_pool
826                .read()
827                .pending_transactions()
828                .take(remaining),
829        );
830        txs
831    }
832
833    fn queued_transactions(&self) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
834        let mut queued = self.protocol_pool.queued_transactions();
835        queued.extend(self.aa_2d_pool.read().queued_transactions());
836        queued
837    }
838
839    fn pending_and_queued_txn_count(&self) -> (usize, usize) {
840        let (protocol_pending, protocol_queued) = self.protocol_pool.pending_and_queued_txn_count();
841        let (aa_pending, aa_queued) = self.aa_2d_pool.read().pending_and_queued_txn_count();
842        (protocol_pending + aa_pending, protocol_queued + aa_queued)
843    }
844
845    fn all_transactions(&self) -> AllPoolTransactions<Self::Transaction> {
846        let mut transactions = self.protocol_pool.all_transactions();
847        {
848            let aa_2d_pool = self.aa_2d_pool.read();
849            transactions
850                .pending
851                .extend(aa_2d_pool.pending_transactions());
852            transactions.queued.extend(aa_2d_pool.queued_transactions());
853        }
854        transactions
855    }
856
857    fn all_transaction_hashes(&self) -> Vec<B256> {
858        let mut hashes = self.protocol_pool.all_transaction_hashes();
859        hashes.extend(self.aa_2d_pool.read().all_transaction_hashes_iter());
860        hashes
861    }
862
863    fn remove_transactions(
864        &self,
865        hashes: Vec<B256>,
866    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
867        let mut txs = self.aa_2d_pool.write().remove_transactions(hashes.iter());
868        txs.extend(self.protocol_pool.remove_transactions(hashes));
869        txs
870    }
871
872    fn remove_transactions_and_descendants(
873        &self,
874        hashes: Vec<B256>,
875    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
876        let mut txs = self
877            .aa_2d_pool
878            .write()
879            .remove_transactions_and_descendants(hashes.iter());
880        txs.extend(
881            self.protocol_pool
882                .remove_transactions_and_descendants(hashes),
883        );
884        txs
885    }
886
887    fn remove_transactions_by_sender(
888        &self,
889        sender: Address,
890    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
891        let mut txs = self
892            .aa_2d_pool
893            .write()
894            .remove_transactions_by_sender(sender);
895        txs.extend(self.protocol_pool.remove_transactions_by_sender(sender));
896        txs
897    }
898
899    fn prune_transactions(
900        &self,
901        hashes: Vec<TxHash>,
902    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
903        let mut txs = self.aa_2d_pool.write().remove_transactions(hashes.iter());
904        txs.extend(self.protocol_pool.prune_transactions(hashes));
905        txs
906    }
907
908    fn retain_unknown<A: HandleMempoolData>(&self, announcement: &mut A) {
909        self.protocol_pool.retain_unknown(announcement);
910        if announcement.is_empty() {
911            return;
912        }
913        let aa_pool = self.aa_2d_pool.read();
914        announcement.retain_by_hash(|tx| !aa_pool.contains(tx))
915    }
916
917    fn contains(&self, tx_hash: &B256) -> bool {
918        self.protocol_pool.contains(tx_hash) || self.aa_2d_pool.read().contains(tx_hash)
919    }
920
921    fn get(&self, tx_hash: &B256) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
922        self.protocol_pool
923            .get(tx_hash)
924            .or_else(|| self.aa_2d_pool.read().get(tx_hash))
925    }
926
927    fn get_all(&self, txs: Vec<B256>) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
928        let mut result = self.aa_2d_pool.read().get_all(txs.iter());
929        result.extend(self.protocol_pool.get_all(txs));
930        result
931    }
932
933    fn on_propagated(&self, txs: PropagatedTransactions) {
934        self.protocol_pool.on_propagated(txs);
935    }
936
937    fn get_transactions_by_sender(
938        &self,
939        sender: Address,
940    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
941        let mut txs = self.protocol_pool.get_transactions_by_sender(sender);
942        txs.extend(
943            self.aa_2d_pool
944                .read()
945                .get_transactions_by_sender_iter(sender),
946        );
947        txs
948    }
949
950    fn get_pending_transactions_with_predicate(
951        &self,
952        mut predicate: impl FnMut(&ValidPoolTransaction<Self::Transaction>) -> bool,
953    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
954        let mut txs = self
955            .protocol_pool
956            .get_pending_transactions_with_predicate(&mut predicate);
957        txs.extend(
958            self.aa_2d_pool
959                .read()
960                .pending_transactions()
961                .filter(|tx| predicate(tx)),
962        );
963        txs
964    }
965
966    fn get_pending_transactions_by_sender(
967        &self,
968        sender: Address,
969    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
970        let mut txs = self
971            .protocol_pool
972            .get_pending_transactions_by_sender(sender);
973        txs.extend(
974            self.aa_2d_pool
975                .read()
976                .pending_transactions()
977                .filter(|tx| tx.sender() == sender),
978        );
979
980        txs
981    }
982
983    fn get_queued_transactions_by_sender(
984        &self,
985        sender: Address,
986    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
987        self.protocol_pool.get_queued_transactions_by_sender(sender)
988    }
989
990    fn get_highest_transaction_by_sender(
991        &self,
992        sender: Address,
993    ) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
994        // With 2D nonces, there's no concept of a single "highest" nonce across all nonce_keys
995        // Return the highest protocol nonce (nonce_key=0) only
996        self.protocol_pool.get_highest_transaction_by_sender(sender)
997    }
998
999    fn get_highest_consecutive_transaction_by_sender(
1000        &self,
1001        sender: Address,
1002        on_chain_nonce: u64,
1003    ) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
1004        // This is complex with 2D nonces - delegate to protocol pool
1005        self.protocol_pool
1006            .get_highest_consecutive_transaction_by_sender(sender, on_chain_nonce)
1007    }
1008
1009    fn get_transaction_by_sender_and_nonce(
1010        &self,
1011        sender: Address,
1012        nonce: u64,
1013    ) -> Option<Arc<ValidPoolTransaction<Self::Transaction>>> {
1014        // Only returns transactions from protocol pool (nonce_key=0)
1015        self.protocol_pool
1016            .get_transaction_by_sender_and_nonce(sender, nonce)
1017    }
1018
1019    fn get_transactions_by_origin(
1020        &self,
1021        origin: TransactionOrigin,
1022    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1023        let mut txs = self.protocol_pool.get_transactions_by_origin(origin);
1024        txs.extend(
1025            self.aa_2d_pool
1026                .read()
1027                .get_transactions_by_origin_iter(origin),
1028        );
1029        txs
1030    }
1031
1032    fn get_pending_transactions_by_origin(
1033        &self,
1034        origin: TransactionOrigin,
1035    ) -> Vec<Arc<ValidPoolTransaction<Self::Transaction>>> {
1036        let mut txs = self
1037            .protocol_pool
1038            .get_pending_transactions_by_origin(origin);
1039        txs.extend(
1040            self.aa_2d_pool
1041                .read()
1042                .get_pending_transactions_by_origin_iter(origin),
1043        );
1044        txs
1045    }
1046
1047    fn unique_senders(&self) -> AddressSet {
1048        let mut senders = self.protocol_pool.unique_senders();
1049        senders.extend(self.aa_2d_pool.read().senders_iter().copied());
1050        senders
1051    }
1052
1053    fn get_blob(
1054        &self,
1055        tx_hash: B256,
1056    ) -> Result<
1057        Option<Arc<alloy_eips::eip7594::BlobTransactionSidecarVariant>>,
1058        reth_transaction_pool::blobstore::BlobStoreError,
1059    > {
1060        self.protocol_pool.get_blob(tx_hash)
1061    }
1062
1063    fn get_all_blobs(
1064        &self,
1065        tx_hashes: Vec<B256>,
1066    ) -> Result<
1067        Vec<(
1068            B256,
1069            Arc<alloy_eips::eip7594::BlobTransactionSidecarVariant>,
1070        )>,
1071        reth_transaction_pool::blobstore::BlobStoreError,
1072    > {
1073        self.protocol_pool.get_all_blobs(tx_hashes)
1074    }
1075
1076    fn get_all_blobs_exact(
1077        &self,
1078        tx_hashes: Vec<B256>,
1079    ) -> Result<
1080        Vec<Arc<alloy_eips::eip7594::BlobTransactionSidecarVariant>>,
1081        reth_transaction_pool::blobstore::BlobStoreError,
1082    > {
1083        self.protocol_pool.get_all_blobs_exact(tx_hashes)
1084    }
1085
1086    fn get_blobs_for_versioned_hashes_v1(
1087        &self,
1088        versioned_hashes: &[B256],
1089    ) -> Result<
1090        Vec<Option<alloy_eips::eip4844::BlobAndProofV1>>,
1091        reth_transaction_pool::blobstore::BlobStoreError,
1092    > {
1093        self.protocol_pool
1094            .get_blobs_for_versioned_hashes_v1(versioned_hashes)
1095    }
1096
1097    fn get_blobs_for_versioned_hashes_v2(
1098        &self,
1099        versioned_hashes: &[B256],
1100    ) -> Result<
1101        Option<Vec<alloy_eips::eip4844::BlobAndProofV2>>,
1102        reth_transaction_pool::blobstore::BlobStoreError,
1103    > {
1104        self.protocol_pool
1105            .get_blobs_for_versioned_hashes_v2(versioned_hashes)
1106    }
1107
1108    fn get_blobs_for_versioned_hashes_v3(
1109        &self,
1110        versioned_hashes: &[B256],
1111    ) -> Result<
1112        Vec<Option<alloy_eips::eip4844::BlobAndProofV2>>,
1113        reth_transaction_pool::blobstore::BlobStoreError,
1114    > {
1115        self.protocol_pool
1116            .get_blobs_for_versioned_hashes_v3(versioned_hashes)
1117    }
1118
1119    fn get_blobs_for_versioned_hashes_v4(
1120        &self,
1121        versioned_hashes: &[B256],
1122        indices_bitarray: alloy_primitives::B128,
1123    ) -> Result<
1124        Vec<Option<alloy_eips::eip4844::BlobCellsAndProofsV1>>,
1125        reth_transaction_pool::blobstore::BlobStoreError,
1126    > {
1127        self.protocol_pool
1128            .get_blobs_for_versioned_hashes_v4(versioned_hashes, indices_bitarray)
1129    }
1130}
1131
1132impl<Client> TransactionPoolExt for TempoTransactionPool<Client>
1133where
1134    Client: StateProviderFactory + ChainSpecProvider<ChainSpec = TempoChainSpec> + 'static,
1135{
1136    type Block = Block;
1137
1138    fn set_block_info(&self, info: BlockInfo) {
1139        self.protocol_pool.set_block_info(info)
1140    }
1141
1142    fn on_canonical_state_change(&self, update: CanonicalStateUpdate<'_, Self::Block>) {
1143        self.protocol_pool.on_canonical_state_change(update)
1144    }
1145
1146    fn update_accounts(&self, accounts: Vec<ChangedAccount>) {
1147        self.protocol_pool.update_accounts(accounts)
1148    }
1149
1150    fn delete_blob(&self, tx: B256) {
1151        self.protocol_pool.delete_blob(tx)
1152    }
1153
1154    fn delete_blobs(&self, txs: Vec<B256>) {
1155        self.protocol_pool.delete_blobs(txs)
1156    }
1157
1158    fn cleanup_blobs(&self) {
1159        self.protocol_pool.cleanup_blobs()
1160    }
1161}
1162
1163/// Checks whether a pending keychain tx exceeds its effective remaining spending limit.
1164///
1165/// Re-reads the current limit from state for the tx's `(account, key_id, fee_token)` combo,
1166/// including any T3 periodic-limit rollover at `current_timestamp`. Returns true if the tx's
1167/// fee cost exceeds the effective remaining limit, meaning it should be evicted.
1168pub(crate) fn exceeds_spending_limit(
1169    provider: &mut impl StateProvider,
1170    subject: &crate::transaction::KeychainSubject,
1171    fee_token_cost: alloy_primitives::U256,
1172    current_timestamp: u64,
1173    spec: TempoHardfork,
1174) -> bool {
1175    provider
1176        .with_read_only_storage_ctx(spec, || -> TempoPrecompileResult<bool> {
1177            let keychain = AccountKeychain::new();
1178            if !keychain.keys[subject.account][subject.key_id]
1179                .read()?
1180                .enforce_limits
1181            {
1182                return Ok(false);
1183            }
1184
1185            let remaining = keychain.effective_remaining_limit(
1186                subject.account,
1187                subject.key_id,
1188                subject.fee_token,
1189                current_timestamp,
1190            )?;
1191            Ok(fee_token_cost > remaining)
1192        })
1193        .unwrap_or_default()
1194}
1195
1196/// Returns the set of policy IDs that can affect fee_payer authorization for a token.
1197///
1198/// For simple policies the set contains just the policy ID. For compound policies
1199/// (TIP-1015) it contains both the compound root and the sender sub-policy, since
1200/// fee transfer authorization checks `fee_payer` via `AuthRole::Sender`.
1201/// `recipient_policy_id` and `mint_recipient_policy_id` are excluded — they govern
1202/// other roles and cannot invalidate a fee_payer's transactions.
1203fn get_sender_policy_ids(
1204    provider: &mut impl StateProvider,
1205    fee_token: Address,
1206    spec: TempoHardfork,
1207    cache: &mut AddressMap<Vec<u64>>,
1208) -> Option<Vec<u64>> {
1209    if let Some(cached) = cache.get(&fee_token) {
1210        return Some(cached.clone());
1211    }
1212
1213    provider.with_read_only_storage_ctx(spec, || {
1214        let policy_id = TIP20Token::from_address(fee_token)
1215            .and_then(|t| t.transfer_policy_id())
1216            .ok()
1217            .filter(|&id| id != REJECT_ALL_POLICY_ID)?;
1218
1219        let mut ids = vec![policy_id];
1220
1221        // For compound policies, include only the sender sub-policy ID.
1222        let registry = TIP403Registry::new();
1223        if let Ok(data) = registry.policy_records[policy_id].base.read()
1224            && data.is_compound()
1225            && let Ok(compound) = registry.policy_records[policy_id].compound.read()
1226            && compound.sender_policy_id != REJECT_ALL_POLICY_ID
1227        {
1228            ids.push(compound.sender_policy_id);
1229        }
1230
1231        // Cache even though compound sub-policy references are immutable: avoids
1232        // redundant SLOADs when multiple transactions share the same fee token.
1233        cache.insert(fee_token, ids.clone());
1234        Some(ids)
1235    })
1236}
1237
1238/// Returns the set of policy IDs that can affect recipient authorization for a token.
1239///
1240/// For simple (non-compound) policies, the transfer policy applies symmetrically to both
1241/// sender and recipient, so the set contains just the policy ID. For compound policies
1242/// (TIP-1015) it contains both the compound root and the recipient sub-policy, since
1243/// fee transfer authorization checks the fee manager via `AuthRole::Recipient`.
1244///
1245/// Unlike `get_sender_policy_ids` this is uncached — it's only called on the rare path
1246/// where the fee manager itself is blacklisted or un-whitelisted.
1247fn get_recipient_policy_ids(
1248    provider: &mut impl StateProvider,
1249    fee_token: Address,
1250    spec: TempoHardfork,
1251) -> Option<Vec<u64>> {
1252    provider.with_read_only_storage_ctx(spec, || {
1253        let policy_id = TIP20Token::from_address(fee_token)
1254            .and_then(|t| t.transfer_policy_id())
1255            .ok()
1256            .filter(|&id| id != REJECT_ALL_POLICY_ID)?;
1257
1258        let mut ids = vec![policy_id];
1259
1260        let registry = TIP403Registry::new();
1261        if let Ok(data) = registry.policy_records[policy_id].base.read()
1262            && data.is_compound()
1263            && let Ok(compound) = registry.policy_records[policy_id].compound.read()
1264            && compound.recipient_policy_id != REJECT_ALL_POLICY_ID
1265        {
1266            ids.push(compound.recipient_policy_id);
1267        }
1268
1269        Some(ids)
1270    })
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276    use crate::{test_utils::MockProviderStorageExt, transaction::KeychainSubject};
1277    use alloy_consensus::Header;
1278    use alloy_primitives::{U256, address, uint};
1279    use alloy_signer::SignerSync;
1280    use alloy_signer_local::PrivateKeySigner;
1281    use reth_primitives_traits::Recovered;
1282    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
1283    use reth_storage_api::StateProviderFactory;
1284    use reth_transaction_pool::{
1285        PoolConfig, TransactionOrigin, TransactionPool, TransactionValidationTaskExecutor,
1286        blobstore::InMemoryBlobStore,
1287        validate::{EthTransactionValidatorBuilder, ValidTransaction},
1288    };
1289    use tempo_chainspec::{
1290        hardfork::TempoHardfork,
1291        spec::{MODERATO, TEMPO_T1_TX_GAS_LIMIT_CAP},
1292    };
1293    use tempo_contracts::precompiles::ITIP403Registry;
1294    use tempo_evm::TempoEvmConfig;
1295    use tempo_precompiles::{
1296        PATH_USD_ADDRESS,
1297        account_keychain::{AccountKeychain, AuthorizedKey, SpendingLimitState},
1298        tip20::slots as tip20_slots,
1299        tip403_registry::{CompoundPolicyData, PolicyData, TIP403Registry},
1300    };
1301    use tempo_primitives::{Block, TempoHeader, TempoPrimitives, TempoTxEnvelope};
1302
1303    fn provider_with_spending_limit(
1304        account: Address,
1305        key_id: Address,
1306        fee_token: Address,
1307        remaining_limit: alloy_primitives::U256,
1308    ) -> Box<dyn reth_storage_api::StateProvider> {
1309        provider_with_spending_limit_state(
1310            account,
1311            key_id,
1312            fee_token,
1313            SpendingLimitState {
1314                remaining: remaining_limit,
1315                ..Default::default()
1316            },
1317            TempoHardfork::default(),
1318        )
1319    }
1320
1321    fn provider_with_spending_limit_state(
1322        account: Address,
1323        key_id: Address,
1324        fee_token: Address,
1325        limit_state: SpendingLimitState,
1326        setup_spec: TempoHardfork,
1327    ) -> Box<dyn reth_storage_api::StateProvider> {
1328        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1329            tempo_chainspec::spec::MODERATO.clone(),
1330        ));
1331
1332        // Write AuthorizedKey with enforce_limits=true
1333        provider
1334            .setup_storage(setup_spec, || {
1335                let mut keychain = AccountKeychain::new();
1336                keychain.keys[account][key_id].write(AuthorizedKey {
1337                    signature_type: 0,
1338                    expiry: u64::MAX,
1339                    enforce_limits: true,
1340                    is_revoked: false,
1341                })?;
1342                let limit_key = AccountKeychain::spending_limit_key(account, key_id);
1343                keychain.spending_limits[limit_key][fee_token].write(limit_state)?;
1344                Ok::<(), tempo_precompiles::error::TempoPrecompileError>(())
1345            })
1346            .unwrap();
1347
1348        provider.latest().unwrap()
1349    }
1350
1351    fn set_fee_token_balance(
1352        provider: &MockEthProvider<TempoPrimitives, TempoChainSpec>,
1353        fee_token: Address,
1354        account: Address,
1355        balance: U256,
1356    ) {
1357        let usd_currency_value =
1358            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
1359        let transfer_policy_id_packed =
1360            uint!(0x0000000000000000000000010000000000000000000000000000000000000000_U256);
1361        let balance_slot = TIP20Token::from_address(fee_token)
1362            .expect("fee token must be a valid TIP20 token")
1363            .balances[account]
1364            .slot();
1365
1366        provider.add_account(
1367            fee_token,
1368            ExtendedAccount::new(0, U256::ZERO).extend_storage([
1369                (tip20_slots::CURRENCY.into(), usd_currency_value),
1370                (
1371                    tip20_slots::TRANSFER_POLICY_ID.into(),
1372                    transfer_policy_id_packed,
1373                ),
1374                (balance_slot.into(), balance),
1375            ]),
1376        );
1377    }
1378
1379    #[tokio::test]
1380    async fn evicts_sponsored_transactions_when_fee_payer_becomes_insolvent() {
1381        let fee_payer_signer = PrivateKeySigner::random();
1382        let fee_payer = fee_payer_signer.address();
1383        let sender = Address::random();
1384
1385        let envelope = crate::test_utils::TxBuilder::aa(sender)
1386            .fee_token(PATH_USD_ADDRESS)
1387            .build()
1388            .inner()
1389            .clone()
1390            .into_inner();
1391        let TempoTxEnvelope::AA(mut signed) = envelope else {
1392            panic!("expected AA transaction");
1393        };
1394        let fee_payer_hash = signed.tx().fee_payer_signature_hash(sender);
1395        signed.tx_mut().fee_payer_signature = Some(
1396            fee_payer_signer
1397                .sign_hash_sync(&fee_payer_hash)
1398                .expect("fee payer signing should succeed"),
1399        );
1400        let pooled = TempoPooledTransaction::new(Recovered::new_unchecked(
1401            TempoTxEnvelope::AA(signed),
1402            sender,
1403        ));
1404
1405        let provider = MockEthProvider::<TempoPrimitives>::new()
1406            .with_chain_spec(std::sync::Arc::unwrap_or_clone(MODERATO.clone()));
1407        provider.add_account(sender, ExtendedAccount::new(pooled.nonce(), *pooled.cost()));
1408        provider.add_block(
1409            B256::random(),
1410            Block {
1411                header: TempoHeader {
1412                    inner: Header {
1413                        gas_limit: TEMPO_T1_TX_GAS_LIMIT_CAP,
1414                        ..Default::default()
1415                    },
1416                    ..Default::default()
1417                },
1418                ..Default::default()
1419            },
1420        );
1421
1422        let initial_balance = pooled.fee_token_cost() + U256::from(1_u64);
1423        set_fee_token_balance(&provider, PATH_USD_ADDRESS, fee_payer, initial_balance);
1424
1425        let inner =
1426            EthTransactionValidatorBuilder::new(provider.clone(), TempoEvmConfig::mainnet())
1427                .disable_balance_check()
1428                .build(InMemoryBlobStore::default());
1429        let amm_cache =
1430            AmmLiquidityCache::new(provider.clone()).expect("failed to setup AmmLiquidityCache");
1431        let validator = TempoTransactionValidator::new(
1432            inner,
1433            crate::validator::DEFAULT_AA_VALID_AFTER_MAX_SECS,
1434            crate::validator::DEFAULT_MAX_TEMPO_AUTHORIZATIONS,
1435            amm_cache,
1436        );
1437
1438        let (executor, _task) = TransactionValidationTaskExecutor::new(validator);
1439        let protocol_pool = Pool::new(
1440            executor,
1441            CoinbaseTipOrdering::default(),
1442            InMemoryBlobStore::default(),
1443            PoolConfig::default(),
1444        );
1445        let pool = TempoTransactionPool::new(protocol_pool, AA2dPool::new(Default::default()));
1446
1447        pooled.set_resolved_fee_token(PATH_USD_ADDRESS);
1448        let validated = TransactionValidationOutcome::Valid {
1449            balance: *pooled.cost(),
1450            state_nonce: pooled.nonce(),
1451            bytecode_hash: None,
1452            transaction: ValidTransaction::new(pooled.clone(), None),
1453            propagate: true,
1454            authorities: None,
1455        };
1456        let add_result = pool.add_validated_transaction(TransactionOrigin::External, validated);
1457        assert!(
1458            add_result.is_ok(),
1459            "transaction should be admitted before sponsor drains balance: {add_result:?}"
1460        );
1461
1462        set_fee_token_balance(
1463            &provider,
1464            PATH_USD_ADDRESS,
1465            fee_payer,
1466            pooled.fee_token_cost() - U256::from(1_u64),
1467        );
1468
1469        let mut updates = crate::maintain::TempoPoolUpdates::new();
1470        updates
1471            .fee_balance_changes
1472            .entry(PATH_USD_ADDRESS)
1473            .or_default()
1474            .insert(fee_payer);
1475
1476        let evicted = pool.evict_invalidated_transactions(&updates);
1477        assert_eq!(evicted, vec![*pooled.hash()]);
1478        assert!(pool.get(pooled.hash()).is_none());
1479    }
1480
1481    /// Eviction must match sub-policy IDs against compound policies.
1482    /// When a token uses a compound policy, and a sub-policy event fires,
1483    /// the eviction comparison must detect the match.
1484    #[test]
1485    fn compound_policy_sub_policy_matches_eviction_check() {
1486        let fee_token = address!("20C0000000000000000000000000000000000001");
1487        let compound_policy_id: u64 = 5;
1488        let sender_sub_policy: u64 = 3;
1489        let recipient_sub_policy: u64 = 4;
1490
1491        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1492            tempo_chainspec::spec::MODERATO.clone(),
1493        ));
1494
1495        // Set up TIP20 token with transfer_policy_id = compound_policy_id
1496        let transfer_policy_id_packed =
1497            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
1498        provider.add_account(
1499            fee_token,
1500            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
1501                tip20_slots::TRANSFER_POLICY_ID.into(),
1502                transfer_policy_id_packed,
1503            )]),
1504        );
1505
1506        // Set up TIP403 registry with compound policy pointing to sub-policies
1507        provider
1508            .setup_storage(TempoHardfork::default(), || {
1509                let mut registry = TIP403Registry::new();
1510                registry.policy_records[compound_policy_id]
1511                    .base
1512                    .write(PolicyData {
1513                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
1514                        admin: Address::ZERO,
1515                    })?;
1516                registry.policy_records[compound_policy_id]
1517                    .compound
1518                    .write(CompoundPolicyData {
1519                        sender_policy_id: sender_sub_policy,
1520                        recipient_policy_id: recipient_sub_policy,
1521                        mint_recipient_policy_id: 0,
1522                    })
1523            })
1524            .unwrap();
1525
1526        let mut state = provider.latest().unwrap();
1527        let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
1528
1529        let ids =
1530            get_sender_policy_ids(&mut state, fee_token, TempoHardfork::default(), &mut cache)
1531                .expect("should resolve policy IDs");
1532
1533        assert!(
1534            ids.contains(&compound_policy_id),
1535            "should contain compound policy ID"
1536        );
1537        assert!(
1538            ids.contains(&sender_sub_policy),
1539            "should contain sender sub-policy"
1540        );
1541    }
1542
1543    /// fee_payer is only checked against sender sub-policy at execution time,
1544    /// so sender_policy_ids must NOT contain recipient_sub_policy.
1545    #[test]
1546    fn compound_policy_sender_ids_exclude_recipient_sub_policy() {
1547        let fee_token = address!("20C0000000000000000000000000000000000001");
1548        let compound_policy_id: u64 = 5;
1549        let sender_sub_policy: u64 = 3;
1550        let recipient_sub_policy: u64 = 4;
1551
1552        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1553            tempo_chainspec::spec::MODERATO.clone(),
1554        ));
1555
1556        let transfer_policy_id_packed =
1557            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
1558        provider.add_account(
1559            fee_token,
1560            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
1561                tip20_slots::TRANSFER_POLICY_ID.into(),
1562                transfer_policy_id_packed,
1563            )]),
1564        );
1565
1566        provider
1567            .setup_storage(TempoHardfork::default(), || {
1568                let mut registry = TIP403Registry::new();
1569                registry.policy_records[compound_policy_id]
1570                    .base
1571                    .write(PolicyData {
1572                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
1573                        admin: Address::ZERO,
1574                    })?;
1575                registry.policy_records[compound_policy_id]
1576                    .compound
1577                    .write(CompoundPolicyData {
1578                        sender_policy_id: sender_sub_policy,
1579                        recipient_policy_id: recipient_sub_policy,
1580                        mint_recipient_policy_id: 0,
1581                    })
1582            })
1583            .unwrap();
1584
1585        let mut state = provider.latest().unwrap();
1586        let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
1587
1588        let ids =
1589            get_sender_policy_ids(&mut state, fee_token, TempoHardfork::default(), &mut cache)
1590                .expect("should resolve policy IDs");
1591
1592        assert!(ids.contains(&compound_policy_id));
1593        assert!(ids.contains(&sender_sub_policy));
1594        assert!(
1595            !ids.contains(&recipient_sub_policy),
1596            "sender policy IDs should not contain recipient_sub_policy"
1597        );
1598    }
1599
1600    /// mint_recipient_policy_id is never consulted for fee transfers,
1601    /// so it must be excluded from sender policy IDs.
1602    #[test]
1603    fn compound_policy_excludes_mint_recipient() {
1604        let fee_token = address!("20C0000000000000000000000000000000000001");
1605        let compound_policy_id: u64 = 5;
1606        let sender_sub: u64 = 3;
1607        let recipient_sub: u64 = 4;
1608        let mint_recipient_sub: u64 = 6;
1609
1610        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1611            tempo_chainspec::spec::MODERATO.clone(),
1612        ));
1613
1614        let transfer_policy_id_packed =
1615            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
1616        provider.add_account(
1617            fee_token,
1618            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
1619                tip20_slots::TRANSFER_POLICY_ID.into(),
1620                transfer_policy_id_packed,
1621            )]),
1622        );
1623
1624        provider
1625            .setup_storage(TempoHardfork::default(), || {
1626                let mut registry = TIP403Registry::new();
1627                registry.policy_records[compound_policy_id]
1628                    .base
1629                    .write(PolicyData {
1630                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
1631                        admin: Address::ZERO,
1632                    })?;
1633                registry.policy_records[compound_policy_id]
1634                    .compound
1635                    .write(CompoundPolicyData {
1636                        sender_policy_id: sender_sub,
1637                        recipient_policy_id: recipient_sub,
1638                        mint_recipient_policy_id: mint_recipient_sub,
1639                    })
1640            })
1641            .unwrap();
1642
1643        let mut state = provider.latest().unwrap();
1644        let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
1645
1646        let ids =
1647            get_sender_policy_ids(&mut state, fee_token, TempoHardfork::default(), &mut cache)
1648                .expect("should resolve policy IDs");
1649
1650        assert!(
1651            !ids.contains(&mint_recipient_sub),
1652            "mint_recipient must be excluded from sender policy IDs"
1653        );
1654    }
1655
1656    /// `get_recipient_policy_ids` returns the compound root and recipient sub-policy.
1657    #[test]
1658    fn recipient_policy_ids_includes_recipient_sub_policy() {
1659        let fee_token = address!("20C0000000000000000000000000000000000001");
1660        let compound_policy_id: u64 = 5;
1661        let sender_sub: u64 = 3;
1662        let recipient_sub: u64 = 4;
1663
1664        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1665            tempo_chainspec::spec::MODERATO.clone(),
1666        ));
1667
1668        let transfer_policy_id_packed =
1669            U256::from(compound_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
1670        provider.add_account(
1671            fee_token,
1672            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
1673                tip20_slots::TRANSFER_POLICY_ID.into(),
1674                transfer_policy_id_packed,
1675            )]),
1676        );
1677
1678        provider
1679            .setup_storage(TempoHardfork::default(), || {
1680                let mut registry = TIP403Registry::new();
1681                registry.policy_records[compound_policy_id]
1682                    .base
1683                    .write(PolicyData {
1684                        policy_type: ITIP403Registry::PolicyType::COMPOUND as u8,
1685                        admin: Address::ZERO,
1686                    })?;
1687                registry.policy_records[compound_policy_id]
1688                    .compound
1689                    .write(CompoundPolicyData {
1690                        sender_policy_id: sender_sub,
1691                        recipient_policy_id: recipient_sub,
1692                        mint_recipient_policy_id: 0,
1693                    })
1694            })
1695            .unwrap();
1696
1697        let mut state = provider.latest().unwrap();
1698        let ids = get_recipient_policy_ids(&mut state, fee_token, TempoHardfork::default())
1699            .expect("should resolve policy IDs");
1700
1701        assert!(
1702            ids.contains(&compound_policy_id),
1703            "should contain compound policy ID"
1704        );
1705        assert!(
1706            ids.contains(&recipient_sub),
1707            "should contain recipient sub-policy"
1708        );
1709        assert!(
1710            !ids.contains(&sender_sub),
1711            "recipient policy IDs should not contain sender sub-policy"
1712        );
1713    }
1714
1715    /// For simple (non-compound) policies, `get_recipient_policy_ids` returns just the root.
1716    #[test]
1717    fn recipient_policy_ids_simple_policy() {
1718        let fee_token = address!("20C0000000000000000000000000000000000001");
1719        let simple_policy_id: u64 = 7;
1720
1721        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1722            tempo_chainspec::spec::MODERATO.clone(),
1723        ));
1724
1725        let transfer_policy_id_packed =
1726            U256::from(simple_policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
1727        provider.add_account(
1728            fee_token,
1729            ExtendedAccount::new(0, U256::ZERO).extend_storage([(
1730                tip20_slots::TRANSFER_POLICY_ID.into(),
1731                transfer_policy_id_packed,
1732            )]),
1733        );
1734
1735        provider
1736            .setup_storage(TempoHardfork::default(), || {
1737                let mut registry = TIP403Registry::new();
1738                registry.policy_records[simple_policy_id]
1739                    .base
1740                    .write(PolicyData {
1741                        policy_type: ITIP403Registry::PolicyType::BLACKLIST as u8,
1742                        admin: Address::ZERO,
1743                    })
1744            })
1745            .unwrap();
1746
1747        let mut state = provider.latest().unwrap();
1748        let ids = get_recipient_policy_ids(&mut state, fee_token, TempoHardfork::default())
1749            .expect("should resolve policy IDs");
1750
1751        assert_eq!(ids, vec![simple_policy_id]);
1752    }
1753
1754    #[test]
1755    fn exceeds_spending_limit_returns_true_when_cost_exceeds_remaining() {
1756        let account = Address::random();
1757        let key_id = Address::random();
1758        let fee_token = Address::random();
1759        let subject = KeychainSubject {
1760            account,
1761            key_id,
1762            fee_token,
1763        };
1764
1765        let mut state = provider_with_spending_limit(
1766            account,
1767            key_id,
1768            fee_token,
1769            alloy_primitives::U256::from(100),
1770        );
1771
1772        assert!(exceeds_spending_limit(
1773            &mut state,
1774            &subject,
1775            alloy_primitives::U256::from(200),
1776            0,
1777            TempoHardfork::default(),
1778        ));
1779    }
1780
1781    #[test]
1782    fn exceeds_spending_limit_returns_false_when_cost_within_limit() {
1783        let account = Address::random();
1784        let key_id = Address::random();
1785        let fee_token = Address::random();
1786        let subject = KeychainSubject {
1787            account,
1788            key_id,
1789            fee_token,
1790        };
1791
1792        let mut state = provider_with_spending_limit(
1793            account,
1794            key_id,
1795            fee_token,
1796            alloy_primitives::U256::from(500),
1797        );
1798
1799        assert!(!exceeds_spending_limit(
1800            &mut state,
1801            &subject,
1802            alloy_primitives::U256::from(200),
1803            0,
1804            TempoHardfork::default(),
1805        ));
1806    }
1807
1808    #[test]
1809    fn exceeds_spending_limit_returns_true_when_no_limit_set() {
1810        let account = Address::random();
1811        let key_id = Address::random();
1812        let fee_token = Address::random();
1813        let subject = KeychainSubject {
1814            account,
1815            key_id,
1816            fee_token,
1817        };
1818
1819        // Provider with AuthorizedKey (enforce_limits=true) but no spending limit slot
1820        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1821            tempo_chainspec::spec::MODERATO.clone(),
1822        ));
1823        provider
1824            .setup_storage(TempoHardfork::default(), || {
1825                AccountKeychain::new().keys[account][key_id].write(AuthorizedKey {
1826                    signature_type: 0,
1827                    expiry: u64::MAX,
1828                    enforce_limits: true,
1829                    is_revoked: false,
1830                })
1831            })
1832            .unwrap();
1833
1834        assert!(exceeds_spending_limit(
1835            &mut provider.latest().unwrap(),
1836            &subject,
1837            alloy_primitives::U256::from(1),
1838            0,
1839            TempoHardfork::default(),
1840        ));
1841    }
1842
1843    #[test]
1844    fn exceeds_spending_limit_returns_false_when_limits_not_enforced() {
1845        let account = Address::random();
1846        let key_id = Address::random();
1847        let fee_token = Address::random();
1848        let subject = KeychainSubject {
1849            account,
1850            key_id,
1851            fee_token,
1852        };
1853
1854        // Provider with AuthorizedKey (enforce_limits=false)
1855        let provider = MockEthProvider::default().with_chain_spec(std::sync::Arc::unwrap_or_clone(
1856            tempo_chainspec::spec::MODERATO.clone(),
1857        ));
1858        provider
1859            .setup_storage(TempoHardfork::default(), || {
1860                AccountKeychain::new().keys[account][key_id].write(AuthorizedKey {
1861                    signature_type: 0,
1862                    expiry: u64::MAX,
1863                    enforce_limits: false,
1864                    is_revoked: false,
1865                })
1866            })
1867            .unwrap();
1868
1869        assert!(!exceeds_spending_limit(
1870            &mut provider.latest().unwrap(),
1871            &subject,
1872            alloy_primitives::U256::from(1),
1873            0,
1874            TempoHardfork::default(),
1875        ));
1876    }
1877
1878    #[test]
1879    fn exceeds_spending_limit_uses_period_reset_after_rollover() {
1880        let account = Address::random();
1881        let key_id = Address::random();
1882        let fee_token = Address::random();
1883        let subject = KeychainSubject {
1884            account,
1885            key_id,
1886            fee_token,
1887        };
1888
1889        let mut state = provider_with_spending_limit_state(
1890            account,
1891            key_id,
1892            fee_token,
1893            SpendingLimitState {
1894                remaining: alloy_primitives::U256::ZERO,
1895                max: 100,
1896                period: 60,
1897                period_end: 10,
1898            },
1899            TempoHardfork::T3,
1900        );
1901
1902        assert!(!exceeds_spending_limit(
1903            &mut state,
1904            &subject,
1905            alloy_primitives::U256::from(50),
1906            10,
1907            TempoHardfork::T3,
1908        ));
1909        assert!(exceeds_spending_limit(
1910            &mut state,
1911            &subject,
1912            alloy_primitives::U256::from(150),
1913            10,
1914            TempoHardfork::T3,
1915        ));
1916    }
1917}