tempo_transaction_pool/
validator.rs

1use crate::{
2    amm::AmmLiquidityCache,
3    transaction::{TempoPoolTransactionError, TempoPooledTransaction},
4};
5use alloy_consensus::Transaction;
6
7use alloy_primitives::{Address, U256};
8use reth_chainspec::{ChainSpecProvider, EthChainSpec};
9use reth_primitives_traits::{
10    Block, GotExpected, SealedBlock, transaction::error::InvalidTransactionError,
11};
12use reth_storage_api::{StateProvider, StateProviderFactory, errors::ProviderError};
13use reth_transaction_pool::{
14    EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome,
15    TransactionValidator, error::InvalidPoolTransactionError,
16};
17use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
18use tempo_precompiles::{
19    ACCOUNT_KEYCHAIN_ADDRESS, NONCE_PRECOMPILE_ADDRESS,
20    account_keychain::{AccountKeychain, AuthorizedKey},
21};
22use tempo_primitives::{subblock::has_sub_block_nonce_key_prefix, transaction::TempoTransaction};
23use tempo_revm::TempoStateAccess;
24
25// Reject AA txs where `valid_before` is too close to current time (or already expired) to prevent block invalidation.
26const AA_VALID_BEFORE_MIN_SECS: u64 = 3;
27
28/// Validator for Tempo transactions.
29#[derive(Debug)]
30pub struct TempoTransactionValidator<Client> {
31    /// Inner validator that performs default Ethereum tx validation.
32    pub(crate) inner: EthTransactionValidator<Client, TempoPooledTransaction>,
33    /// Maximum allowed `valid_after` offset for AA txs.
34    pub(crate) aa_valid_after_max_secs: u64,
35    /// Cache of AMM liquidity for validator tokens.
36    pub(crate) amm_liquidity_cache: AmmLiquidityCache,
37}
38
39impl<Client> TempoTransactionValidator<Client>
40where
41    Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
42{
43    pub fn new(
44        inner: EthTransactionValidator<Client, TempoPooledTransaction>,
45        aa_valid_after_max_secs: u64,
46        amm_liquidity_cache: AmmLiquidityCache,
47    ) -> Self {
48        Self {
49            inner,
50            aa_valid_after_max_secs,
51            amm_liquidity_cache,
52        }
53    }
54
55    /// Obtains a clone of the shared [`AmmLiquidityCache`].
56    pub fn amm_liquidity_cache(&self) -> AmmLiquidityCache {
57        self.amm_liquidity_cache.clone()
58    }
59
60    /// Returns the configured client
61    pub fn client(&self) -> &Client {
62        self.inner.client()
63    }
64
65    /// Check if a transaction requires keychain validation
66    ///
67    /// Returns the validation result indicating what action to take:
68    /// - ValidateKeychain: Need to validate the keychain authorization
69    /// - Skip: No validation needed (not a keychain signature, or same-tx auth is valid)
70    /// - Reject: Transaction should be rejected with the given reason
71    fn validate_against_keychain(
72        &self,
73        transaction: &TempoPooledTransaction,
74        state_provider: &impl StateProvider,
75    ) -> Result<Result<(), &'static str>, ProviderError> {
76        let Some(tx) = transaction.inner().as_aa() else {
77            return Ok(Ok(()));
78        };
79
80        let is_allegretto = self
81            .inner
82            .chain_spec()
83            .is_allegretto_active_at_timestamp(self.inner.fork_tracker().tip_timestamp());
84
85        let auth = tx.tx().key_authorization.as_ref();
86
87        if (auth.is_some() || tx.signature().is_keychain()) && !is_allegretto {
88            return Ok(Err(
89                "keychain operations are only supported after Allegretto",
90            ));
91        }
92
93        // Ensure that key auth is valid if present.
94        if let Some(auth) = auth {
95            // Validate signature
96            if !auth
97                .recover_signer()
98                .is_ok_and(|signer| signer == transaction.sender())
99            {
100                return Ok(Err("Invalid KeyAuthorization signature"));
101            }
102
103            // Validate chain_id (chain_id == 0 is wildcard, works on any chain)
104            let chain_id = self.inner.chain_spec().chain_id();
105            if auth.chain_id != 0 && auth.chain_id != chain_id {
106                return Ok(Err(
107                    "KeyAuthorization chain_id does not match current chain",
108                ));
109            }
110        }
111
112        let Some(sig) = tx.signature().as_keychain() else {
113            return Ok(Ok(()));
114        };
115
116        // This should never fail because we set sender based on the sig.
117        if sig.user_address != transaction.sender() {
118            return Ok(Err("Keychain signature user_address does not match sender"));
119        }
120
121        // This should fail happen because we validate the signature validity in `recover_signer`.
122        let Ok(key_id) = sig.key_id(&tx.signature_hash()) else {
123            return Ok(Err(
124                "Failed to recover access key ID from Keychain signature",
125            ));
126        };
127
128        // Ensure that if key auth is present, it is for the same key as the keychain signature.
129        if let Some(auth) = auth {
130            if auth.key_id != key_id {
131                return Ok(Err(
132                    "KeyAuthorization key_id does not match Keychain signature key_id",
133                ));
134            }
135
136            // KeyAuthorization is valid - skip keychain storage check (key will be authorized during execution)
137            return Ok(Ok(()));
138        }
139
140        // Compute storage slot using helper function
141        let storage_slot = AccountKeychain::new()
142            .keys
143            .at(transaction.sender())
144            .at(key_id)
145            .base_slot();
146
147        // Read storage slot from state provider
148        let slot_value = state_provider
149            .storage(ACCOUNT_KEYCHAIN_ADDRESS, storage_slot.into())?
150            .unwrap_or(U256::ZERO);
151
152        // Decode AuthorizedKey using helper
153        let authorized_key = AuthorizedKey::decode_from_slot(slot_value);
154
155        // Check if key was revoked (revoked keys cannot be used)
156        if authorized_key.is_revoked {
157            return Ok(Err("access key has been revoked"));
158        }
159
160        // Check if key exists (key exists if expiry > 0)
161        if authorized_key.expiry == 0 {
162            return Ok(Err("access key does not exist"));
163        }
164
165        // Expiry checks are skipped here, they are done in the EVM handler where block timestamp is easily available.
166        Ok(Ok(()))
167    }
168
169    /// Validates AA transaction time-bound conditionals
170    fn ensure_valid_conditionals(
171        &self,
172        tx: &TempoTransaction,
173    ) -> Result<(), TempoPoolTransactionError> {
174        // Reject AA txs where `valid_before` is too close to current time (or already expired).
175        if let Some(valid_before) = tx.valid_before {
176            // Uses tip_timestamp, as if the node is lagging lagging, the maintenance task will evict expired txs.
177            let current_time = self.inner.fork_tracker().tip_timestamp();
178            let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
179            if valid_before <= min_allowed {
180                return Err(TempoPoolTransactionError::InvalidValidBefore {
181                    valid_before,
182                    min_allowed,
183                });
184            }
185        }
186
187        // Reject AA txs where `valid_after` is too far in the future.
188        if let Some(valid_after) = tx.valid_after {
189            // Uses local time to avoid rejecting valid txs when node is lagging.
190            let current_time = std::time::SystemTime::now()
191                .duration_since(std::time::UNIX_EPOCH)
192                .map(|d| d.as_secs())
193                .unwrap_or(0);
194            let max_allowed = current_time.saturating_add(self.aa_valid_after_max_secs);
195            if valid_after > max_allowed {
196                return Err(TempoPoolTransactionError::InvalidValidAfter {
197                    valid_after,
198                    max_allowed,
199                });
200            }
201        }
202
203        Ok(())
204    }
205
206    fn validate_one(
207        &self,
208        origin: TransactionOrigin,
209        transaction: TempoPooledTransaction,
210        mut state_provider: impl StateProvider,
211    ) -> TransactionValidationOutcome<TempoPooledTransaction> {
212        // Reject system transactions, those are never allowed in the pool.
213        if transaction.inner().is_system_tx() {
214            return TransactionValidationOutcome::Error(
215                *transaction.hash(),
216                InvalidTransactionError::TxTypeNotSupported.into(),
217            );
218        }
219
220        // Validate transactions that involve keychain keys
221        match self.validate_against_keychain(&transaction, &state_provider) {
222            Ok(Ok(())) => {}
223            Ok(Err(reason)) => {
224                return TransactionValidationOutcome::Invalid(
225                    transaction,
226                    InvalidPoolTransactionError::other(TempoPoolTransactionError::Keychain(reason)),
227                );
228            }
229            Err(err) => {
230                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
231            }
232        }
233
234        // Balance transfer is not allowed as there is no balances in accounts yet.
235        // Check added in https://github.com/tempoxyz/tempo/pull/759
236        // AATx will aggregate all call values, so we dont need additional check for AA transactions.
237        if !transaction.inner().value().is_zero() {
238            return TransactionValidationOutcome::Invalid(
239                transaction,
240                InvalidPoolTransactionError::other(TempoPoolTransactionError::NonZeroValue),
241            );
242        }
243
244        // Validate AA transaction temporal conditionals (`valid_before` and `valid_after`).
245        if let Some(tx) = transaction.inner().as_aa()
246            && let Err(err) = self.ensure_valid_conditionals(tx.tx())
247        {
248            return TransactionValidationOutcome::Invalid(
249                transaction,
250                InvalidPoolTransactionError::other(err),
251            );
252        }
253
254        let fee_payer = match transaction.inner().fee_payer(transaction.sender()) {
255            Ok(fee_payer) => fee_payer,
256            Err(err) => {
257                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
258            }
259        };
260
261        let spec = self
262            .inner
263            .chain_spec()
264            .tempo_hardfork_at(self.inner.fork_tracker().tip_timestamp());
265        let fee_token =
266            match state_provider.get_fee_token(transaction.inner(), Address::ZERO, fee_payer, spec)
267            {
268                Ok(fee_token) => fee_token,
269                Err(err) => {
270                    return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
271                }
272            };
273
274        // Ensure that fee token is valid.
275        match state_provider.is_valid_fee_token(fee_token, spec) {
276            Ok(valid) => {
277                if !valid {
278                    return TransactionValidationOutcome::Invalid(
279                        transaction,
280                        InvalidPoolTransactionError::other(
281                            TempoPoolTransactionError::InvalidFeeToken(fee_token),
282                        ),
283                    );
284                }
285            }
286            Err(err) => {
287                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
288            }
289        }
290
291        // Ensure that the fee payer is not blacklisted
292        match state_provider.can_fee_payer_transfer(fee_token, fee_payer) {
293            Ok(valid) => {
294                if !valid {
295                    return TransactionValidationOutcome::Invalid(
296                        transaction,
297                        InvalidPoolTransactionError::other(
298                            TempoPoolTransactionError::BlackListedFeePayer {
299                                fee_token,
300                                fee_payer,
301                            },
302                        ),
303                    );
304                }
305            }
306            Err(err) => {
307                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
308            }
309        }
310
311        let balance = match state_provider.get_token_balance(fee_token, fee_payer) {
312            Ok(balance) => balance,
313            Err(err) => {
314                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
315            }
316        };
317
318        // Get the tx cost and adjust for fee token decimals
319        let cost = transaction.fee_token_cost();
320        if balance < cost {
321            return TransactionValidationOutcome::Invalid(
322                transaction,
323                InvalidTransactionError::InsufficientFunds(
324                    GotExpected {
325                        got: balance,
326                        expected: cost,
327                    }
328                    .into(),
329                )
330                .into(),
331            );
332        }
333
334        match self
335            .amm_liquidity_cache
336            .has_enough_liquidity(fee_token, cost, &state_provider)
337        {
338            Ok(true) => {}
339            Ok(false) => {
340                return TransactionValidationOutcome::Invalid(
341                    transaction,
342                    InvalidPoolTransactionError::other(
343                        TempoPoolTransactionError::InsufficientLiquidity(fee_token),
344                    ),
345                );
346            }
347            Err(err) => {
348                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
349            }
350        }
351
352        match self
353            .inner
354            .validate_one_with_state_provider(origin, transaction, &state_provider)
355        {
356            TransactionValidationOutcome::Valid {
357                balance,
358                mut state_nonce,
359                bytecode_hash,
360                transaction,
361                propagate,
362                authorities,
363            } => {
364                // Additional 2D nonce validations
365                // Check for 2D nonce validation (nonce_key > 0)
366                if let Some(nonce_key) = transaction.transaction().nonce_key()
367                    && !nonce_key.is_zero()
368                {
369                    // ensure the nonce key isn't prefixed with the sub-block prefix
370                    if has_sub_block_nonce_key_prefix(&nonce_key) {
371                        return TransactionValidationOutcome::Invalid(
372                            transaction.into_transaction(),
373                            InvalidPoolTransactionError::other(
374                                TempoPoolTransactionError::SubblockNonceKey,
375                            ),
376                        );
377                    }
378
379                    // This is a 2D nonce transaction - validate against 2D nonce
380                    state_nonce = match state_provider.storage(
381                        NONCE_PRECOMPILE_ADDRESS,
382                        transaction.transaction().nonce_key_slot().unwrap().into(),
383                    ) {
384                        Ok(nonce) => nonce.unwrap_or_default().saturating_to(),
385                        Err(err) => {
386                            return TransactionValidationOutcome::Error(
387                                *transaction.hash(),
388                                Box::new(err),
389                            );
390                        }
391                    };
392                    let tx_nonce = transaction.nonce();
393                    if tx_nonce < state_nonce {
394                        return TransactionValidationOutcome::Invalid(
395                            transaction.into_transaction(),
396                            InvalidTransactionError::NonceNotConsistent {
397                                tx: tx_nonce,
398                                state: state_nonce,
399                            }
400                            .into(),
401                        );
402                    }
403                }
404
405                // Pre-compute TempoTxEnv to avoid the cost during payload building.
406                transaction.transaction().prepare_tx_env();
407
408                TransactionValidationOutcome::Valid {
409                    balance,
410                    state_nonce,
411                    bytecode_hash,
412                    transaction,
413                    propagate,
414                    authorities,
415                }
416            }
417            outcome => outcome,
418        }
419    }
420}
421
422impl<Client> TransactionValidator for TempoTransactionValidator<Client>
423where
424    Client: ChainSpecProvider<ChainSpec = TempoChainSpec> + StateProviderFactory,
425{
426    type Transaction = TempoPooledTransaction;
427
428    async fn validate_transaction(
429        &self,
430        origin: TransactionOrigin,
431        transaction: Self::Transaction,
432    ) -> TransactionValidationOutcome<Self::Transaction> {
433        let state_provider = match self.inner.client().latest() {
434            Ok(provider) => provider,
435            Err(err) => {
436                return TransactionValidationOutcome::Error(*transaction.hash(), Box::new(err));
437            }
438        };
439
440        self.validate_one(origin, transaction, state_provider)
441    }
442
443    async fn validate_transactions(
444        &self,
445        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
446    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
447        let state_provider = match self.inner.client().latest() {
448            Ok(provider) => provider,
449            Err(err) => {
450                return transactions
451                    .into_iter()
452                    .map(|(_, tx)| {
453                        TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
454                    })
455                    .collect();
456            }
457        };
458
459        transactions
460            .into_iter()
461            .map(|(origin, tx)| self.validate_one(origin, tx, &state_provider))
462            .collect()
463    }
464
465    async fn validate_transactions_with_origin(
466        &self,
467        origin: TransactionOrigin,
468        transactions: impl IntoIterator<Item = Self::Transaction> + Send,
469    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
470        let state_provider = match self.inner.client().latest() {
471            Ok(provider) => provider,
472            Err(err) => {
473                return transactions
474                    .into_iter()
475                    .map(|tx| {
476                        TransactionValidationOutcome::Error(*tx.hash(), Box::new(err.clone()))
477                    })
478                    .collect();
479            }
480        };
481
482        transactions
483            .into_iter()
484            .map(|tx| self.validate_one(origin, tx, &state_provider))
485            .collect()
486    }
487
488    fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
489    where
490        B: Block,
491    {
492        self.inner.on_new_head_block(new_tip_block)
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use alloy_consensus::{Block, Transaction};
500    use alloy_eips::Decodable2718;
501    use alloy_primitives::{B256, U256, hex};
502    use reth_primitives_traits::SignedTransaction;
503    use reth_provider::test_utils::{ExtendedAccount, MockEthProvider};
504    use reth_transaction_pool::{
505        PoolTransaction, blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
506    };
507    use std::sync::Arc;
508    use tempo_chainspec::spec::ANDANTINO;
509    use tempo_precompiles::tip403_registry::TIP403Registry;
510    use tempo_primitives::TempoTxEnvelope;
511
512    /// Helper to create a mock sealed block with the given timestamp.
513    fn create_mock_block(timestamp: u64) -> SealedBlock<reth_ethereum_primitives::Block> {
514        use alloy_consensus::Header;
515        let header = Header {
516            timestamp,
517            ..Default::default()
518        };
519        let block = reth_ethereum_primitives::Block {
520            header,
521            body: Default::default(),
522        };
523        SealedBlock::seal_slow(block)
524    }
525
526    fn get_transaction(with_value: Option<U256>) -> TempoPooledTransaction {
527        let raw = "0x02f914950181ad84b2d05e0085117553845b830f7df88080b9143a6040608081523462000414576200133a803803806200001e8162000419565b9283398101608082820312620004145781516001600160401b03908181116200041457826200004f9185016200043f565b92602092838201519083821162000414576200006d9183016200043f565b8186015190946001600160a01b03821692909183900362000414576060015190805193808511620003145760038054956001938488811c9816801562000409575b89891014620003f3578190601f988981116200039d575b50899089831160011462000336576000926200032a575b505060001982841b1c191690841b1781555b8751918211620003145760049788548481811c9116801562000309575b89821014620002f457878111620002a9575b5087908784116001146200023e5793839491849260009562000232575b50501b92600019911b1c19161785555b6005556007805460ff60a01b19169055600880546001600160a01b0319169190911790553015620001f3575060025469d3c21bcecceda100000092838201809211620001de57506000917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9160025530835282815284832084815401905584519384523093a351610e889081620004b28239f35b601190634e487b7160e01b6000525260246000fd5b90606493519262461bcd60e51b845283015260248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152fd5b0151935038806200013a565b9190601f198416928a600052848a6000209460005b8c8983831062000291575050501062000276575b50505050811b0185556200014a565b01519060f884600019921b161c191690553880808062000267565b86860151895590970196948501948893500162000253565b89600052886000208880860160051c8201928b8710620002ea575b0160051c019085905b828110620002dd5750506200011d565b60008155018590620002cd565b92508192620002c4565b60228a634e487b7160e01b6000525260246000fd5b90607f16906200010b565b634e487b7160e01b600052604160045260246000fd5b015190503880620000dc565b90869350601f19831691856000528b6000209260005b8d8282106200038657505084116200036d575b505050811b018155620000ee565b015160001983861b60f8161c191690553880806200035f565b8385015186558a979095019493840193016200034c565b90915083600052896000208980850160051c8201928c8610620003e9575b918891869594930160051c01915b828110620003d9575050620000c5565b60008155859450889101620003c9565b92508192620003bb565b634e487b7160e01b600052602260045260246000fd5b97607f1697620000ae565b600080fd5b6040519190601f01601f191682016001600160401b038111838210176200031457604052565b919080601f84011215620004145782516001600160401b038111620003145760209062000475601f8201601f1916830162000419565b92818452828287010111620004145760005b8181106200049d57508260009394955001015290565b85810183015184820184015282016200048756fe608060408181526004918236101561001657600080fd5b600092833560e01c91826306fdde0314610a1c57508163095ea7b3146109f257816318160ddd146109d35781631b4c84d2146109ac57816323b872dd14610833578163313ce5671461081757816339509351146107c357816370a082311461078c578163715018a6146107685781638124f7ac146107495781638da5cb5b1461072057816395d89b411461061d578163a457c2d714610575578163a9059cbb146104e4578163c9567bf914610120575063dd62ed3e146100d557600080fd5b3461011c578060031936011261011c57806020926100f1610b5a565b6100f9610b75565b6001600160a01b0391821683526001865283832091168252845220549051908152f35b5080fd5b905082600319360112610338576008546001600160a01b039190821633036104975760079283549160ff8360a01c1661045557737a250d5630b4cf539739df2c5dacb4c659f2488d92836bffffffffffffffffffffffff60a01b8092161786553087526020938785528388205430156104065730895260018652848920828a52865280858a205584519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925863092a38554835163c45a015560e01b815290861685828581845afa9182156103dd57849187918b946103e7575b5086516315ab88c960e31b815292839182905afa9081156103dd576044879289928c916103c0575b508b83895196879586946364e329cb60e11b8652308c870152166024850152165af19081156103b6579086918991610389575b50169060065416176006558385541660604730895288865260c4858a20548860085416928751958694859363f305d71960e01b8552308a86015260248501528d60448501528d606485015260848401524260a48401525af1801561037f579084929161034c575b50604485600654169587541691888551978894859363095ea7b360e01b855284015260001960248401525af1908115610343575061030c575b5050805460ff60a01b1916600160a01b17905580f35b81813d831161033c575b6103208183610b8b565b8101031261033857518015150361011c5738806102f6565b8280fd5b503d610316565b513d86823e3d90fd5b6060809293503d8111610378575b6103648183610b8b565b81010312610374578290386102bd565b8580fd5b503d61035a565b83513d89823e3d90fd5b6103a99150863d88116103af575b6103a18183610b8b565b810190610e33565b38610256565b503d610397565b84513d8a823e3d90fd5b6103d79150843d86116103af576103a18183610b8b565b38610223565b85513d8b823e3d90fd5b6103ff919450823d84116103af576103a18183610b8b565b92386101fb565b845162461bcd60e51b81528085018790526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b6020606492519162461bcd60e51b8352820152601760248201527f74726164696e6720697320616c7265616479206f70656e0000000000000000006044820152fd5b608490602084519162461bcd60e51b8352820152602160248201527f4f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6044820152603760f91b6064820152fd5b9050346103385781600319360112610338576104fe610b5a565b9060243593303303610520575b602084610519878633610bc3565b5160018152f35b600594919454808302908382041483151715610562576127109004820391821161054f5750925080602061050b565b634e487b7160e01b815260118552602490fd5b634e487b7160e01b825260118652602482fd5b9050823461061a578260031936011261061a57610590610b5a565b918360243592338152600160205281812060018060a01b03861682526020522054908282106105c9576020856105198585038733610d31565b608490602086519162461bcd60e51b8352820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b6064820152fd5b80fd5b83833461011c578160031936011261011c57805191809380549160019083821c92828516948515610716575b6020958686108114610703578589529081156106df5750600114610687575b6106838787610679828c0383610b8b565b5191829182610b11565b0390f35b81529295507f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5b8284106106cc57505050826106839461067992820101948680610668565b80548685018801529286019281016106ae565b60ff19168887015250505050151560051b8301019250610679826106838680610668565b634e487b7160e01b845260228352602484fd5b93607f1693610649565b50503461011c578160031936011261011c5760085490516001600160a01b039091168152602090f35b50503461011c578160031936011261011c576020906005549051908152f35b833461061a578060031936011261061a57600880546001600160a01b031916905580f35b50503461011c57602036600319011261011c5760209181906001600160a01b036107b4610b5a565b16815280845220549051908152f35b82843461061a578160031936011261061a576107dd610b5a565b338252600160209081528383206001600160a01b038316845290528282205460243581019290831061054f57602084610519858533610d31565b50503461011c578160031936011261011c576020905160128152f35b83833461011c57606036600319011261011c5761084e610b5a565b610856610b75565b6044359160018060a01b0381169485815260209560018752858220338352875285822054976000198903610893575b505050906105199291610bc3565b85891061096957811561091a5733156108cc5750948481979861051997845260018a528284203385528a52039120558594938780610885565b865162461bcd60e51b8152908101889052602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b865162461bcd60e51b81529081018890526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b865162461bcd60e51b8152908101889052601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606490fd5b50503461011c578160031936011261011c5760209060ff60075460a01c1690519015158152f35b50503461011c578160031936011261011c576020906002549051908152f35b50503461011c578060031936011261011c57602090610519610a12610b5a565b6024359033610d31565b92915034610b0d5783600319360112610b0d57600354600181811c9186908281168015610b03575b6020958686108214610af05750848852908115610ace5750600114610a75575b6106838686610679828b0383610b8b565b929550600383527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5b828410610abb575050508261068394610679928201019438610a64565b8054868501880152928601928101610a9e565b60ff191687860152505050151560051b83010192506106798261068338610a64565b634e487b7160e01b845260229052602483fd5b93607f1693610a44565b8380fd5b6020808252825181830181905290939260005b828110610b4657505060409293506000838284010152601f8019910116010190565b818101860151848201604001528501610b24565b600435906001600160a01b0382168203610b7057565b600080fd5b602435906001600160a01b0382168203610b7057565b90601f8019910116810190811067ffffffffffffffff821117610bad57604052565b634e487b7160e01b600052604160045260246000fd5b6001600160a01b03908116918215610cde5716918215610c8d57600082815280602052604081205491808310610c3957604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef958760209652828652038282205586815220818154019055604051908152a3565b60405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608490fd5b60405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608490fd5b60405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b6064820152608490fd5b6001600160a01b03908116918215610de25716918215610d925760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925918360005260018252604060002085600052825280604060002055604051908152a3565b60405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b6064820152608490fd5b60405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b6064820152608490fd5b90816020910312610b7057516001600160a01b0381168103610b70579056fea2646970667358221220285c200b3978b10818ff576bb83f2dc4a2a7c98dfb6a36ea01170de792aa652764736f6c63430008140033000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000d3fd4f95820a9aa848ce716d6c200eaefb9a2e4900000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000003543131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035431310000000000000000000000000000000000000000000000000000000000c001a04e551c75810ffdfe6caff57da9f5a8732449f42f0f4c57f935b05250a76db3b6a046cd47e6d01914270c1ec0d9ac7fae7dfb240ec9a8b6ec7898c4d6aa174388f2";
528
529        let data = hex::decode(raw).unwrap();
530        let mut tx = TempoTxEnvelope::decode_2718(&mut data.as_ref()).unwrap();
531
532        if let Some(value) = with_value {
533            match &mut tx {
534                TempoTxEnvelope::Legacy(tx) => tx.tx_mut().value = value,
535                TempoTxEnvelope::Eip2930(tx) => tx.tx_mut().value = value,
536                TempoTxEnvelope::Eip1559(tx) => tx.tx_mut().value = value,
537                TempoTxEnvelope::Eip7702(tx) => tx.tx_mut().value = value,
538                // set value to first call
539                TempoTxEnvelope::AA(tx) => {
540                    if let Some(first_call) = tx.tx_mut().calls.first_mut() {
541                        first_call.value = value;
542                    }
543                }
544                TempoTxEnvelope::FeeToken(tx) => tx.tx_mut().value = value,
545            }
546        }
547
548        TempoPooledTransaction::new(tx.try_into_recovered().unwrap())
549    }
550
551    /// Helper function to create an AA transaction with the given `valid_after` and `valid_before`
552    /// timestamps
553    fn create_aa_transaction(
554        valid_after: Option<u64>,
555        valid_before: Option<u64>,
556    ) -> TempoPooledTransaction {
557        use alloy_primitives::{Signature, TxKind, address};
558        use tempo_primitives::transaction::{
559            TempoTransaction,
560            tempo_transaction::Call,
561            tt_signature::{PrimitiveSignature, TempoSignature},
562            tt_signed::AASigned,
563        };
564
565        let tx_aa = TempoTransaction {
566            chain_id: 1,
567            max_priority_fee_per_gas: 1_000_000_000,
568            max_fee_per_gas: 2_000_000_000,
569            gas_limit: 100_000,
570            calls: vec![Call {
571                to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
572                value: U256::ZERO,
573                input: alloy_primitives::Bytes::new(),
574            }],
575            nonce_key: U256::ZERO,
576            nonce: 0,
577            fee_token: Some(address!("0000000000000000000000000000000000000002")),
578            fee_payer_signature: None,
579            valid_after,
580            valid_before,
581            access_list: Default::default(),
582            tempo_authorization_list: vec![],
583            key_authorization: None,
584        };
585
586        let signed_tx = AASigned::new_unhashed(
587            tx_aa,
588            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
589        );
590        let envelope: TempoTxEnvelope = signed_tx.into();
591        let recovered = envelope.try_into_recovered().unwrap();
592        TempoPooledTransaction::new(recovered)
593    }
594
595    /// Helper function to setup validator with the given transaction and tip timestamp.
596    fn setup_validator(
597        transaction: &TempoPooledTransaction,
598        tip_timestamp: u64,
599    ) -> TempoTransactionValidator<
600        MockEthProvider<reth_ethereum_primitives::EthPrimitives, TempoChainSpec>,
601    > {
602        let provider =
603            MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(ANDANTINO.clone()));
604        provider.add_account(
605            transaction.sender(),
606            ExtendedAccount::new(transaction.nonce(), alloy_primitives::U256::ZERO),
607        );
608        provider.add_block(B256::random(), Default::default());
609
610        let inner = EthTransactionValidatorBuilder::new(provider.clone())
611            .disable_balance_check()
612            .build(InMemoryBlobStore::default());
613        let amm_cache =
614            AmmLiquidityCache::new(provider).expect("failed to setup AmmLiquidityCache");
615        let validator = TempoTransactionValidator::new(inner, 3600, amm_cache);
616
617        // Set the tip timestamp by simulating a new head block
618        let mock_block = create_mock_block(tip_timestamp);
619        validator.on_new_head_block(&mock_block);
620
621        validator
622    }
623
624    #[tokio::test]
625    async fn test_some_balance() {
626        let transaction = get_transaction(Some(U256::from(1)));
627        let validator = setup_validator(&transaction, 0);
628
629        let outcome = validator
630            .validate_transaction(TransactionOrigin::External, transaction.clone())
631            .await;
632
633        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
634            assert!(
635                err.to_string()
636                    .contains("Native transfers are not supported")
637            );
638        } else {
639            panic!("Expected Invalid outcome with InsufficientFunds error");
640        }
641    }
642
643    #[tokio::test]
644    async fn test_aa_valid_before_check() {
645        // NOTE: `setup_validator` will turn `tip_timestamp` into `current_time`
646        let current_time = std::time::SystemTime::now()
647            .duration_since(std::time::UNIX_EPOCH)
648            .unwrap()
649            .as_secs();
650
651        // Test case 1: No `valid_before`
652        let tx_no_valid_before = create_aa_transaction(None, None);
653        let validator = setup_validator(&tx_no_valid_before, current_time);
654        let outcome = validator
655            .validate_transaction(TransactionOrigin::External, tx_no_valid_before)
656            .await;
657
658        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
659            let error_msg = format!("{err}");
660            assert!(!error_msg.contains("valid_before"));
661        }
662
663        // Test case 2: `valid_before` too small (at boundary)
664        let tx_too_close =
665            create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS));
666        let validator = setup_validator(&tx_too_close, current_time);
667        let outcome = validator
668            .validate_transaction(TransactionOrigin::External, tx_too_close)
669            .await;
670
671        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
672            let error_msg = format!("{err}");
673            assert!(
674                error_msg.contains("valid_before"),
675                "Expected 'valid_before' got: {error_msg}"
676            );
677        } else {
678            panic!("Expected invalid outcome with InvalidValidBefore error");
679        }
680
681        // Test case 3: `valid_before` sufficiently in the future
682        let tx_valid =
683            create_aa_transaction(None, Some(current_time + AA_VALID_BEFORE_MIN_SECS + 1));
684        let validator = setup_validator(&tx_valid, current_time);
685        let outcome = validator
686            .validate_transaction(TransactionOrigin::External, tx_valid)
687            .await;
688
689        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
690            let error_msg = format!("{err}");
691            assert!(!error_msg.contains("valid_before"));
692        }
693    }
694
695    #[tokio::test]
696    async fn test_aa_valid_after_check() {
697        // NOTE: `setup_validator` will turn `tip_timestamp` into `current_time`
698        let current_time = std::time::SystemTime::now()
699            .duration_since(std::time::UNIX_EPOCH)
700            .unwrap()
701            .as_secs();
702
703        // Test case 1: No `valid_after`
704        let tx_no_valid_after = create_aa_transaction(None, None);
705        let validator = setup_validator(&tx_no_valid_after, current_time);
706        let outcome = validator
707            .validate_transaction(TransactionOrigin::External, tx_no_valid_after)
708            .await;
709
710        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
711            let error_msg = format!("{err}");
712            assert!(!error_msg.contains("valid_after"));
713        }
714
715        // Test case 2: `valid_after` within limit (30 minutes)
716        let tx_within_limit = create_aa_transaction(Some(current_time + 1800), None);
717        let validator = setup_validator(&tx_within_limit, current_time);
718        let outcome = validator
719            .validate_transaction(TransactionOrigin::External, tx_within_limit)
720            .await;
721
722        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
723            let error_msg = format!("{err}");
724            assert!(!error_msg.contains("valid_after"));
725        }
726
727        // Test case 3: `valid_after` beyond limit (2 hours)
728        let tx_too_far = create_aa_transaction(Some(current_time + 7200), None);
729        let validator = setup_validator(&tx_too_far, current_time);
730        let outcome = validator
731            .validate_transaction(TransactionOrigin::External, tx_too_far)
732            .await;
733
734        if let TransactionValidationOutcome::Invalid(_, err) = outcome {
735            let error_msg = format!("{err}");
736            assert!(error_msg.contains("valid_after"));
737        } else {
738            panic!("Expected invalid outcome with InvalidValidAfter error");
739        }
740    }
741
742    #[tokio::test]
743    async fn test_blacklisted_fee_payer_rejected() {
744        use alloy_primitives::{Signature, TxKind, address, uint};
745        use tempo_precompiles::{
746            TIP403_REGISTRY_ADDRESS,
747            tip20::slots as tip20_slots,
748            tip403_registry::{ITIP403Registry, PolicyData},
749        };
750        use tempo_primitives::transaction::{
751            TempoTransaction,
752            tempo_transaction::Call,
753            tt_signature::{PrimitiveSignature, TempoSignature},
754            tt_signed::AASigned,
755        };
756
757        // Use a valid TIP20 token address (PATH_USD with token_id=1)
758        let fee_token = address!("20C0000000000000000000000000000000000001");
759        let policy_id: u64 = 2;
760
761        // Create AA transaction with valid TIP20 fee_token
762        let tx_aa = TempoTransaction {
763            chain_id: 1,
764            max_priority_fee_per_gas: 1_000_000_000,
765            max_fee_per_gas: 2_000_000_000,
766            gas_limit: 100_000,
767            calls: vec![Call {
768                to: TxKind::Call(address!("0000000000000000000000000000000000000001")),
769                value: U256::ZERO,
770                input: alloy_primitives::Bytes::new(),
771            }],
772            nonce_key: U256::ZERO,
773            nonce: 0,
774            fee_token: Some(fee_token),
775            fee_payer_signature: None,
776            valid_after: None,
777            valid_before: None,
778            access_list: Default::default(),
779            tempo_authorization_list: vec![],
780            key_authorization: None,
781        };
782
783        let signed_tx = AASigned::new_unhashed(
784            tx_aa,
785            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
786        );
787        let envelope: TempoTxEnvelope = signed_tx.into();
788        let recovered = envelope.try_into_recovered().unwrap();
789        let transaction = TempoPooledTransaction::new(recovered);
790        let fee_payer = transaction.sender();
791
792        // Setup provider with storage
793        let provider =
794            MockEthProvider::default().with_chain_spec(Arc::unwrap_or_clone(ANDANTINO.clone()));
795        provider.add_block(B256::random(), Block::default());
796
797        // Add sender account
798        provider.add_account(
799            transaction.sender(),
800            ExtendedAccount::new(transaction.nonce(), U256::ZERO),
801        );
802
803        // Add TIP20 token with transfer_policy_id pointing to blacklist policy
804        // USD_CURRENCY_SLOT_VALUE: "USD" left-padded with length marker (3 bytes * 2 = 6)
805        let usd_currency_value =
806            uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
807        provider.add_account(
808            fee_token,
809            ExtendedAccount::new(0, U256::ZERO).extend_storage([
810                (
811                    tip20_slots::TRANSFER_POLICY_ID.into(),
812                    U256::from(policy_id),
813                ),
814                (tip20_slots::CURRENCY.into(), usd_currency_value),
815            ]),
816        );
817
818        // Add TIP403Registry with blacklist policy containing fee_payer
819        let policy_data = PolicyData {
820            policy_type: ITIP403Registry::PolicyType::BLACKLIST as u8,
821            admin: Address::ZERO,
822        };
823        let policy_data_slot = TIP403Registry::new().policy_data.at(policy_id).base_slot();
824        let policy_set_slot = TIP403Registry::new()
825            .policy_set
826            .at(policy_id)
827            .at(fee_payer)
828            .slot();
829
830        provider.add_account(
831            TIP403_REGISTRY_ADDRESS,
832            ExtendedAccount::new(0, U256::ZERO).extend_storage([
833                (policy_data_slot.into(), policy_data.encode_to_slot()),
834                (policy_set_slot.into(), U256::from(1)), // in blacklist = true
835            ]),
836        );
837
838        // Create validator and validate
839        let inner = EthTransactionValidatorBuilder::new(provider.clone())
840            .disable_balance_check()
841            .build(InMemoryBlobStore::default());
842        let validator =
843            TempoTransactionValidator::new(inner, 3600, AmmLiquidityCache::new(provider).unwrap());
844
845        let outcome = validator
846            .validate_transaction(TransactionOrigin::External, transaction)
847            .await;
848
849        // Assert BlackListedFeePayer error
850        match outcome {
851            TransactionValidationOutcome::Invalid(_, err) => {
852                let error_msg = err.to_string();
853                assert!(
854                    error_msg.contains("blacklisted") || error_msg.contains("BlackListed"),
855                    "Expected BlackListedFeePayer error, got: {error_msg}"
856                );
857            }
858            other => panic!("Expected Invalid outcome, got: {other:?}"),
859        }
860    }
861}