Skip to main content

tempo_revm/
tx.rs

1use crate::TempoInvalidTransaction;
2use alloy_consensus::{Typed2718, crypto::secp256k1};
3use alloy_evm::{FromRecoveredTx, FromTxWithEncoded, IntoTxEnv, TransactionEnvMut};
4use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
5use core::num::NonZeroU64;
6use revm::context::{
7    Transaction, TxEnv,
8    either::Either,
9    result::InvalidTransaction,
10    transaction::{
11        AccessList, AccessListItem, RecoveredAuthority, RecoveredAuthorization, SignedAuthorization,
12    },
13};
14use tempo_primitives::{
15    AASigned, TempoSignature, TempoTransaction, TempoTxEnvelope,
16    transaction::{
17        Call, RecoveredTempoAuthorization, SignedKeyAuthorization, calc_gas_balance_spending,
18    },
19};
20
21/// Tempo transaction environment for AA features.
22#[derive(Debug, Clone, Default)]
23pub struct TempoBatchCallEnv {
24    /// Signature bytes for Tempo transactions
25    pub signature: TempoSignature,
26
27    /// validBefore timestamp
28    pub valid_before: Option<u64>,
29
30    /// validAfter timestamp
31    pub valid_after: Option<u64>,
32
33    /// Multiple calls for Tempo transactions
34    pub aa_calls: Vec<Call>,
35
36    /// Authorization list (EIP-7702 with Tempo signatures)
37    ///
38    /// Each authorization lazily recovers the authority on first access and caches the result.
39    /// The signature is preserved for gas calculation.
40    pub tempo_authorization_list: Vec<RecoveredTempoAuthorization>,
41
42    /// Nonce key for 2D nonce system
43    pub nonce_key: U256,
44
45    /// Whether the transaction is a subblock transaction.
46    pub subblock_transaction: bool,
47
48    /// Optional key authorization for provisioning access keys
49    pub key_authorization: Option<SignedKeyAuthorization>,
50
51    /// Transaction signature hash (for signature verification)
52    pub signature_hash: B256,
53
54    /// Transaction hash
55    pub tx_hash: B256,
56
57    /// Optional access key ID override for gas estimation.
58    /// When provided in eth_call/eth_estimateGas, enables spending limits simulation
59    /// This is not used in actual transaction execution - the key_id is recovered from the signature.
60    pub override_key_id: Option<Address>,
61
62    /// Perf optimization for expiring nonce transactions.
63    ///
64    /// Stores how many other expiring nonce transactions are there in the block before this one.
65    pub expiring_nonce_idx: Option<usize>,
66}
67/// Tempo transaction environment.
68#[derive(Debug, Clone, Default, derive_more::Deref, derive_more::DerefMut)]
69pub struct TempoTxEnv {
70    /// Inner Ethereum [`TxEnv`].
71    #[deref]
72    #[deref_mut]
73    pub inner: TxEnv,
74
75    /// Optional fee token preference specified for the transaction.
76    pub fee_token: Option<Address>,
77
78    /// Whether the transaction is a system transaction.
79    pub is_system_tx: bool,
80
81    /// Sender-scoped transaction identifier used for replay-sensitive features.
82    ///
83    /// Synthetic transaction environments used by tests and simulations may leave this unset.
84    pub unique_tx_identifier: Option<B256>,
85
86    /// Optional fee payer specified for the transaction.
87    ///
88    /// - Some(Some(address)) corresponds to a successfully recovered fee payer
89    /// - Some(None) corresponds to a failed recovery and means that transaction is invalid
90    /// - None corresponds to a transaction without a fee payer
91    pub fee_payer: Option<Option<Address>>,
92
93    /// AA-specific transaction environment (boxed to keep TempoTxEnv lean for non-AA tx)
94    pub tempo_tx_env: Option<Box<TempoBatchCallEnv>>,
95}
96
97impl TempoTxEnv {
98    /// Resolves fee payer from the signature.
99    pub fn fee_payer(&self) -> Result<Address, TempoInvalidTransaction> {
100        if let Some(fee_payer) = self.fee_payer {
101            fee_payer.ok_or(TempoInvalidTransaction::InvalidFeePayerSignature)
102        } else {
103            Ok(self.caller())
104        }
105    }
106
107    /// Returns true if transaction carries a fee payer signature.
108    pub fn has_fee_payer_signature(&self) -> bool {
109        self.fee_payer.is_some()
110    }
111
112    /// Returns true if the transaction is a subblock transaction.
113    pub fn is_subblock_transaction(&self) -> bool {
114        self.tempo_tx_env
115            .as_ref()
116            .is_some_and(|aa| aa.subblock_transaction)
117    }
118
119    /// Returns the sender-scoped transaction identifier.
120    ///
121    /// This is `keccak256(encode_for_signing || sender)` for every real transaction type. For
122    /// Tempo AA transactions, this matches the existing expiring nonce hash helper.
123    pub fn unique_tx_identifier(&self) -> Option<B256> {
124        self.unique_tx_identifier
125    }
126
127    /// Returns the replay-protected hash used to derive channel reserve IDs for `open`.
128    pub fn channel_open_context_hash(&self) -> Option<B256> {
129        self.unique_tx_identifier()
130    }
131
132    /// Returns the first top-level call in the transaction.
133    pub fn first_call(&self) -> Option<(&TxKind, &[u8])> {
134        if let Some(aa) = self.tempo_tx_env.as_ref() {
135            aa.aa_calls
136                .first()
137                .map(|call| (&call.to, call.input.as_ref()))
138        } else {
139            Some((&self.inner.kind, &self.inner.data))
140        }
141    }
142
143    /// Returns an iterator over the top-level calls in the transaction.
144    ///
145    /// For AA transactions, iterates over `aa_calls`. For non-AA transactions,
146    /// returns a single-element iterator with the inner transaction's kind and data.
147    pub fn calls(&self) -> impl Iterator<Item = (&TxKind, &[u8])> {
148        if let Some(aa) = self.tempo_tx_env.as_ref() {
149            Either::Left(
150                aa.aa_calls
151                    .iter()
152                    .map(|call| (&call.to, call.input.as_ref())),
153            )
154        } else {
155            Either::Right(core::iter::once((
156                &self.inner.kind,
157                self.inner.input().as_ref(),
158            )))
159        }
160    }
161}
162
163impl From<TxEnv> for TempoTxEnv {
164    fn from(inner: TxEnv) -> Self {
165        Self {
166            inner,
167            ..Default::default()
168        }
169    }
170}
171
172impl Transaction for TempoTxEnv {
173    type AccessListItem<'a> = &'a AccessListItem;
174    type Authorization<'a> = &'a Either<SignedAuthorization, RecoveredAuthorization>;
175
176    fn tx_type(&self) -> u8 {
177        self.inner.tx_type()
178    }
179
180    fn kind(&self) -> TxKind {
181        self.inner.kind()
182    }
183
184    fn caller(&self) -> Address {
185        self.inner.caller()
186    }
187
188    fn gas_limit(&self) -> u64 {
189        self.inner.gas_limit()
190    }
191
192    fn gas_price(&self) -> u128 {
193        self.inner.gas_price()
194    }
195
196    fn value(&self) -> U256 {
197        self.inner.value()
198    }
199
200    fn nonce(&self) -> u64 {
201        Transaction::nonce(&self.inner)
202    }
203
204    fn chain_id(&self) -> Option<u64> {
205        self.inner.chain_id()
206    }
207
208    fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>> {
209        self.inner.access_list()
210    }
211
212    fn max_fee_per_gas(&self) -> u128 {
213        self.inner.max_fee_per_gas()
214    }
215
216    fn max_fee_per_blob_gas(&self) -> u128 {
217        self.inner.max_fee_per_blob_gas()
218    }
219
220    fn authorization_list_len(&self) -> usize {
221        self.inner.authorization_list_len()
222    }
223
224    fn authorization_list(&self) -> impl Iterator<Item = Self::Authorization<'_>> {
225        self.inner.authorization_list()
226    }
227
228    fn input(&self) -> &Bytes {
229        self.inner.input()
230    }
231
232    fn blob_versioned_hashes(&self) -> &[B256] {
233        self.inner.blob_versioned_hashes()
234    }
235
236    fn max_priority_fee_per_gas(&self) -> Option<u128> {
237        self.inner.max_priority_fee_per_gas()
238    }
239
240    fn max_balance_spending(&self) -> Result<U256, InvalidTransaction> {
241        calc_gas_balance_spending(self.gas_limit(), self.max_fee_per_gas())
242            .checked_add(self.value())
243            .ok_or(InvalidTransaction::OverflowPaymentInTransaction)
244    }
245
246    fn effective_balance_spending(
247        &self,
248        base_fee: u128,
249        _blob_price: u128,
250    ) -> Result<U256, InvalidTransaction> {
251        calc_gas_balance_spending(self.gas_limit(), self.effective_gas_price(base_fee))
252            .checked_add(self.value())
253            .ok_or(InvalidTransaction::OverflowPaymentInTransaction)
254    }
255}
256
257impl TransactionEnvMut for TempoTxEnv {
258    fn set_gas_limit(&mut self, gas_limit: u64) {
259        self.inner.set_gas_limit(gas_limit);
260    }
261
262    fn set_nonce(&mut self, nonce: u64) {
263        self.inner.set_nonce(nonce);
264    }
265
266    fn set_access_list(&mut self, access_list: AccessList) {
267        self.inner.set_access_list(access_list);
268    }
269}
270
271impl IntoTxEnv<Self> for TempoTxEnv {
272    fn into_tx_env(self) -> Self {
273        self
274    }
275}
276
277impl FromRecoveredTx<AASigned> for TempoTxEnv {
278    fn from_recovered_tx(aa_signed: &AASigned, caller: Address) -> Self {
279        let tx = aa_signed.tx();
280        let signature = aa_signed.signature();
281
282        // Populate the key_id cache for Keychain signatures before cloning
283        // This parallelizes recovery during Tx->TxEnv conversion, and the cache is preserved when cloned
284        if let Some(keychain_sig) = signature.as_keychain() {
285            let _ = keychain_sig.key_id(&aa_signed.signature_hash());
286        }
287
288        let TempoTransaction {
289            chain_id,
290            fee_token,
291            max_priority_fee_per_gas,
292            max_fee_per_gas,
293            gas_limit,
294            calls,
295            access_list,
296            nonce_key,
297            nonce,
298            fee_payer_signature,
299            valid_before,
300            valid_after,
301            key_authorization,
302            tempo_authorization_list,
303        } = tx;
304
305        // Extract to/value/input from calls (use first call or defaults)
306        let (to, value, input) = if let Some(first_call) = calls.first() {
307            (first_call.to, first_call.value, first_call.input.clone())
308        } else {
309            (
310                alloy_primitives::TxKind::Create,
311                alloy_primitives::U256::ZERO,
312                alloy_primitives::Bytes::new(),
313            )
314        };
315
316        Self {
317            inner: TxEnv {
318                tx_type: tx.ty(),
319                caller,
320                gas_limit: *gas_limit,
321                gas_price: *max_fee_per_gas,
322                kind: to,
323                value,
324                data: input,
325                nonce: *nonce, // AA: nonce maps to TxEnv.nonce
326                chain_id: Some(*chain_id),
327                gas_priority_fee: Some(*max_priority_fee_per_gas),
328                access_list: access_list.clone(),
329                // Convert Tempo authorization list to RecoveredAuthorization upfront
330                authorization_list: tempo_authorization_list
331                    .iter()
332                    .map(|auth| {
333                        let authority = auth
334                            .recover_authority()
335                            .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
336                        Either::Right(RecoveredAuthorization::new_unchecked(
337                            auth.inner().clone(),
338                            authority,
339                        ))
340                    })
341                    .collect(),
342                ..Default::default()
343            },
344            fee_token: *fee_token,
345            is_system_tx: false,
346            unique_tx_identifier: Some(aa_signed.expiring_nonce_hash(caller)),
347            fee_payer: fee_payer_signature.map(|sig| {
348                secp256k1::recover_signer(&sig, tx.fee_payer_signature_hash(caller)).ok()
349            }),
350            // Bundle AA-specific fields into TempoBatchCallEnv
351            tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
352                signature: signature.clone(),
353                valid_before: valid_before.map(NonZeroU64::get),
354                valid_after: valid_after.map(NonZeroU64::get),
355                aa_calls: calls.clone(),
356                // Recover authorizations upfront to avoid recovery during execution
357                tempo_authorization_list: tempo_authorization_list
358                    .iter()
359                    .map(|auth| RecoveredTempoAuthorization::recover(auth.clone()))
360                    .collect(),
361                nonce_key: *nonce_key,
362                subblock_transaction: aa_signed.tx().subblock_proposer().is_some(),
363                key_authorization: key_authorization.clone(),
364                signature_hash: aa_signed.signature_hash(),
365                tx_hash: *aa_signed.hash(),
366                // override_key_id is only used for gas estimation, not actual execution
367                override_key_id: None,
368                // can only be derived when given an entire block
369                expiring_nonce_idx: None,
370            })),
371        }
372    }
373}
374
375impl FromRecoveredTx<TempoTxEnvelope> for TempoTxEnv {
376    fn from_recovered_tx(tx: &TempoTxEnvelope, sender: Address) -> Self {
377        match tx {
378            tx @ TempoTxEnvelope::Legacy(inner) => Self {
379                inner: TxEnv::from_recovered_tx(inner.tx(), sender),
380                fee_token: None,
381                is_system_tx: tx.is_system_tx(),
382                unique_tx_identifier: Some(tx.unique_tx_identifier(sender)),
383                fee_payer: None,
384                tempo_tx_env: None, // Non-AA transaction
385            },
386            TempoTxEnvelope::Eip2930(inner) => Self {
387                inner: TxEnv::from_recovered_tx(inner.tx(), sender),
388                unique_tx_identifier: Some(tx.unique_tx_identifier(sender)),
389                ..Default::default()
390            },
391            TempoTxEnvelope::Eip1559(inner) => Self {
392                inner: TxEnv::from_recovered_tx(inner.tx(), sender),
393                unique_tx_identifier: Some(tx.unique_tx_identifier(sender)),
394                ..Default::default()
395            },
396            TempoTxEnvelope::Eip7702(inner) => Self {
397                inner: TxEnv::from_recovered_tx(inner.tx(), sender),
398                unique_tx_identifier: Some(tx.unique_tx_identifier(sender)),
399                ..Default::default()
400            },
401            TempoTxEnvelope::AA(tx) => Self::from_recovered_tx(tx, sender),
402        }
403    }
404}
405
406impl FromTxWithEncoded<AASigned> for TempoTxEnv {
407    fn from_encoded_tx(tx: &AASigned, sender: Address, _encoded: Bytes) -> Self {
408        Self::from_recovered_tx(tx, sender)
409    }
410}
411
412impl FromTxWithEncoded<TempoTxEnvelope> for TempoTxEnv {
413    fn from_encoded_tx(tx: &TempoTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
414        Self::from_recovered_tx(tx, sender)
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use alloy_consensus::{Signed, TxLegacy, transaction::TxHashRef};
421    use alloy_evm::FromRecoveredTx;
422    use alloy_primitives::{Address, Bytes, Signature, TxKind, U256, keccak256};
423    use core::num::NonZeroU64;
424    use proptest::prelude::*;
425    use revm::context::{Transaction, TxEnv, result::InvalidTransaction};
426    use tempo_primitives::{
427        TempoTxEnvelope,
428        transaction::{
429            Call, calc_gas_balance_spending,
430            tempo_transaction::TEMPO_EXPIRING_NONCE_KEY,
431            tt_signature::{PrimitiveSignature, TempoSignature},
432            tt_signed::AASigned,
433            validate_calls,
434        },
435    };
436
437    use crate::{TempoInvalidTransaction, TempoTxEnv};
438
439    fn create_call(to: TxKind) -> Call {
440        Call {
441            to,
442            value: alloy_primitives::U256::ZERO,
443            input: alloy_primitives::Bytes::new(),
444        }
445    }
446
447    #[test]
448    fn test_validate_empty_calls_list() {
449        let result = validate_calls(&[], false);
450        assert!(result.is_err());
451        assert!(result.unwrap_err().contains("empty"));
452    }
453
454    #[test]
455    fn test_validate_single_call_ok() {
456        let calls = vec![create_call(TxKind::Call(alloy_primitives::Address::ZERO))];
457        assert!(validate_calls(&calls, false).is_ok());
458    }
459
460    #[test]
461    fn test_validate_single_create_ok() {
462        let calls = vec![create_call(TxKind::Create)];
463        assert!(validate_calls(&calls, false).is_ok());
464    }
465
466    #[test]
467    fn test_validate_create_with_authorization_list_fails() {
468        let calls = vec![create_call(TxKind::Create)];
469        let result = validate_calls(&calls, true); // has_authorization_list = true
470        assert!(result.is_err());
471        assert!(result.unwrap_err().contains("CREATE"));
472    }
473
474    #[test]
475    fn test_validate_create_not_first_call_fails() {
476        let calls = vec![
477            create_call(TxKind::Call(alloy_primitives::Address::ZERO)),
478            create_call(TxKind::Create), // CREATE as second call - should fail
479        ];
480        let result = validate_calls(&calls, false);
481        assert!(result.is_err());
482        assert!(result.unwrap_err().contains("first call"));
483    }
484
485    #[test]
486    fn test_validate_multiple_creates_fails() {
487        let calls = vec![
488            create_call(TxKind::Create),
489            create_call(TxKind::Create), // Second CREATE - should fail
490        ];
491        let result = validate_calls(&calls, false);
492        assert!(result.is_err());
493        assert!(result.unwrap_err().contains("first call"));
494    }
495
496    #[test]
497    fn test_validate_create_first_then_calls_ok() {
498        let calls = vec![
499            create_call(TxKind::Create),
500            create_call(TxKind::Call(alloy_primitives::Address::ZERO)),
501            create_call(TxKind::Call(alloy_primitives::Address::random())),
502        ];
503        // No auth list, so CREATE is allowed
504        assert!(validate_calls(&calls, false).is_ok());
505    }
506
507    #[test]
508    fn test_validate_multiple_calls_ok() {
509        let calls = vec![
510            create_call(TxKind::Call(alloy_primitives::Address::ZERO)),
511            create_call(TxKind::Call(alloy_primitives::Address::random())),
512            create_call(TxKind::Call(alloy_primitives::Address::random())),
513        ];
514        assert!(validate_calls(&calls, false).is_ok());
515    }
516
517    #[test]
518    fn test_from_recovered_tx_expiring_nonce_hash() {
519        let caller = Address::repeat_byte(0xAA);
520
521        let make_aa_signed = |nonce_key: U256| -> AASigned {
522            let tx = tempo_primitives::transaction::TempoTransaction {
523                chain_id: 1,
524                gas_limit: 1_000_000,
525                nonce_key,
526                nonce: 0,
527                valid_before: Some(NonZeroU64::new(100).unwrap()),
528                calls: vec![Call {
529                    to: TxKind::Call(Address::repeat_byte(0x42)),
530                    value: U256::ZERO,
531                    input: Bytes::new(),
532                }],
533                ..Default::default()
534            };
535            let sig = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
536                Signature::test_signature(),
537            ));
538            AASigned::new_unhashed(tx, sig)
539        };
540
541        // Expiring nonce txs and channel opens share the same encode_for_signing||sender hash.
542        let expiring_signed = make_aa_signed(TEMPO_EXPIRING_NONCE_KEY);
543        let expiring_env = TempoTxEnv::from_recovered_tx(&expiring_signed, caller);
544        let expected_identifier = expiring_signed.expiring_nonce_hash(caller);
545        assert_eq!(
546            expiring_env.channel_open_context_hash(),
547            Some(expected_identifier),
548            "expiring nonce channel opens must use the sender-scoped transaction identifier"
549        );
550
551        // Regular 2D nonce txs still use the same encode_for_signing||sender construction.
552        let regular_signed = make_aa_signed(U256::from(42));
553        let regular_env = super::TempoTxEnv::from_recovered_tx(&regular_signed, caller);
554        assert_eq!(
555            regular_env.channel_open_context_hash(),
556            Some(regular_signed.expiring_nonce_hash(caller)),
557            "non-expiring AA channel opens must use encode_for_signing||sender"
558        );
559    }
560
561    #[test]
562    fn test_legacy_channel_open_context_hash_uses_encoded_signing_payload_and_sender() {
563        let caller = Address::repeat_byte(0xAA);
564        let tx = TxLegacy {
565            chain_id: Some(1),
566            nonce: 7,
567            gas_price: 1,
568            gas_limit: 21_000,
569            to: TxKind::Call(Address::repeat_byte(0x42)),
570            value: U256::ZERO,
571            input: Bytes::new(),
572        };
573        let envelope =
574            TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()));
575        let tx_hash = *envelope.tx_hash();
576        let TempoTxEnvelope::Legacy(signed) = &envelope else {
577            unreachable!()
578        };
579
580        let tx_env = super::TempoTxEnv::from_recovered_tx(&envelope, caller);
581        let signature_hash = signed.signature_hash();
582        assert_ne!(
583            signature_hash, tx_hash,
584            "legacy signature hash is the unsigned signing hash, not the signed tx hash"
585        );
586
587        let mut signature_hash_and_sender = [0u8; 52];
588        signature_hash_and_sender[..32].copy_from_slice(signature_hash.as_slice());
589        signature_hash_and_sender[32..].copy_from_slice(caller.as_slice());
590        let signature_hash_context = keccak256(signature_hash_and_sender);
591        let encoded_payload_context = envelope.unique_tx_identifier(caller);
592        assert_ne!(
593            encoded_payload_context, signature_hash_context,
594            "channel opens must use the encoded signing payload, not signature_hash||sender"
595        );
596        assert_eq!(
597            tx_env.channel_open_context_hash(),
598            Some(encoded_payload_context)
599        );
600    }
601
602    #[test]
603    fn test_tx_env() {
604        let tx_env = super::TempoTxEnv::default();
605
606        // Test default values
607        assert_eq!(tx_env.inner.nonce, 0);
608        assert!(tx_env.inner.access_list.is_empty());
609        assert!(tx_env.fee_token.is_none());
610        assert!(!tx_env.is_system_tx);
611        assert!(tx_env.fee_payer.is_none());
612        assert!(tx_env.tempo_tx_env.is_none());
613    }
614
615    #[test]
616    fn test_fee_payer_without_signature_uses_caller() {
617        let caller = Address::repeat_byte(0xAB);
618        let tx_env = super::TempoTxEnv {
619            inner: TxEnv {
620                caller,
621                ..Default::default()
622            },
623            fee_payer: None,
624            ..Default::default()
625        };
626
627        assert_eq!(tx_env.fee_payer(), Ok(caller));
628    }
629
630    #[test]
631    fn test_fee_payer_invalid_signature_rejected() {
632        let tx_env = super::TempoTxEnv {
633            fee_payer: Some(None),
634            ..Default::default()
635        };
636
637        assert!(matches!(
638            tx_env.fee_payer(),
639            Err(TempoInvalidTransaction::InvalidFeePayerSignature)
640        ));
641    }
642
643    #[test]
644    fn test_fee_payer_resolving_to_sender_is_allowed_in_tx_env() {
645        let caller = Address::repeat_byte(0xAB);
646        let tx_env = super::TempoTxEnv {
647            inner: TxEnv {
648                caller,
649                ..Default::default()
650            },
651            fee_payer: Some(Some(caller)),
652            ..Default::default()
653        };
654
655        assert_eq!(tx_env.fee_payer(), Ok(caller));
656    }
657
658    #[test]
659    fn test_has_fee_payer_signature() {
660        let without_sig = super::TempoTxEnv {
661            fee_payer: None,
662            ..Default::default()
663        };
664        assert!(!without_sig.has_fee_payer_signature());
665
666        let with_sig = super::TempoTxEnv {
667            fee_payer: Some(Some(Address::repeat_byte(0xAB))),
668            ..Default::default()
669        };
670        assert!(with_sig.has_fee_payer_signature());
671    }
672
673    #[test]
674    fn test_transaction_env_set_gas_limit() {
675        use alloy_evm::TransactionEnvMut;
676
677        let mut tx_env = super::TempoTxEnv::default();
678
679        tx_env.set_gas_limit(21000);
680        assert_eq!(tx_env.inner.gas_limit, 21000);
681
682        tx_env.set_gas_limit(1_000_000);
683        assert_eq!(tx_env.inner.gas_limit, 1_000_000);
684    }
685
686    #[test]
687    fn test_transaction_env_nonce() {
688        use alloy_evm::TransactionEnvMut;
689        use revm::context::Transaction;
690
691        let mut tx_env = super::TempoTxEnv::default();
692        assert_eq!(Transaction::nonce(&tx_env), 0);
693
694        tx_env.set_nonce(42);
695        assert_eq!(Transaction::nonce(&tx_env), 42);
696
697        tx_env.set_nonce(u64::MAX);
698        assert_eq!(Transaction::nonce(&tx_env), u64::MAX);
699    }
700
701    #[test]
702    fn test_transaction_env_set_access_list() {
703        use alloy_evm::TransactionEnvMut;
704        use revm::context::transaction::{AccessList, AccessListItem};
705
706        let mut tx_env = super::TempoTxEnv::default();
707        assert!(tx_env.inner.access_list.is_empty());
708
709        let access_list = AccessList(vec![
710            AccessListItem {
711                address: alloy_primitives::Address::ZERO,
712                storage_keys: vec![alloy_primitives::B256::ZERO],
713            },
714            AccessListItem {
715                address: alloy_primitives::Address::repeat_byte(0x01),
716                storage_keys: vec![
717                    alloy_primitives::B256::repeat_byte(0x01),
718                    alloy_primitives::B256::repeat_byte(0x02),
719                ],
720            },
721        ]);
722
723        tx_env.set_access_list(access_list);
724        assert_eq!(tx_env.inner.access_list.0.len(), 2);
725        assert_eq!(
726            tx_env.inner.access_list.0[0].address,
727            alloy_primitives::Address::ZERO
728        );
729        assert_eq!(tx_env.inner.access_list.0[0].storage_keys.len(), 1);
730        assert_eq!(tx_env.inner.access_list.0[1].storage_keys.len(), 2);
731    }
732
733    #[test]
734    fn test_transaction_env_combined_operations() {
735        use alloy_evm::TransactionEnvMut;
736        use revm::context::transaction::{AccessList, AccessListItem};
737
738        let mut tx_env = super::TempoTxEnv::default();
739
740        // Set all values
741        tx_env.set_gas_limit(50_000);
742        tx_env.set_nonce(100);
743        tx_env.set_access_list(AccessList(vec![AccessListItem {
744            address: alloy_primitives::Address::repeat_byte(0xAB),
745            storage_keys: vec![],
746        }]));
747
748        // Verify all values are set correctly
749        assert_eq!(tx_env.inner.gas_limit, 50_000);
750        assert_eq!(revm::context::Transaction::nonce(&tx_env), 100);
751        assert_eq!(tx_env.inner.access_list.0.len(), 1);
752        assert_eq!(
753            tx_env.inner.access_list.0[0].address,
754            alloy_primitives::Address::repeat_byte(0xAB)
755        );
756    }
757
758    #[test]
759    fn test_transaction_env_from_tx_env() {
760        use revm::context::{Transaction, TxEnv};
761
762        let inner = TxEnv {
763            gas_limit: 75_000,
764            nonce: 55,
765            ..Default::default()
766        };
767
768        let tx_env: super::TempoTxEnv = inner.into();
769
770        assert_eq!(tx_env.inner.gas_limit, 75_000);
771        assert_eq!(Transaction::nonce(&tx_env), 55);
772        assert!(tx_env.fee_token.is_none());
773        assert!(!tx_env.is_system_tx);
774        assert!(tx_env.fee_payer.is_none());
775        assert!(tx_env.tempo_tx_env.is_none());
776    }
777
778    #[test]
779    fn test_first_call_without_aa() {
780        use alloy_primitives::{Address, Bytes};
781        use revm::context::TxEnv;
782
783        // Test without tempo_tx_env (non-AA transaction)
784        let addr = Address::repeat_byte(0x42);
785        let data = Bytes::from(vec![0x01, 0x02, 0x03]);
786
787        let tx_env = super::TempoTxEnv {
788            inner: TxEnv {
789                kind: TxKind::Call(addr),
790                data: data.clone(),
791                ..Default::default()
792            },
793            ..Default::default()
794        };
795
796        let first_call = tx_env.first_call();
797        assert!(first_call.is_some());
798        let (kind, input) = first_call.unwrap();
799        assert_eq!(*kind, TxKind::Call(addr));
800        assert_eq!(input, data.as_ref());
801    }
802
803    #[test]
804    fn test_first_call_with_aa() {
805        use alloy_primitives::{Address, Bytes, U256};
806        use tempo_primitives::transaction::Call;
807
808        // Test with tempo_tx_env (AA transaction)
809        let addr1 = Address::repeat_byte(0x11);
810        let addr2 = Address::repeat_byte(0x22);
811        let input1 = Bytes::from(vec![0xAA, 0xBB]);
812        let input2 = Bytes::from(vec![0xCC, 0xDD]);
813
814        let tx_env = super::TempoTxEnv {
815            tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
816                aa_calls: vec![
817                    Call {
818                        to: TxKind::Call(addr1),
819                        value: U256::ZERO,
820                        input: input1.clone(),
821                    },
822                    Call {
823                        to: TxKind::Call(addr2),
824                        value: U256::from(100),
825                        input: input2,
826                    },
827                ],
828                ..Default::default()
829            })),
830            ..Default::default()
831        };
832
833        let first_call = tx_env.first_call();
834        assert!(first_call.is_some());
835        let (kind, input) = first_call.unwrap();
836        assert_eq!(*kind, TxKind::Call(addr1));
837        assert_eq!(input, input1.as_ref());
838    }
839
840    #[test]
841    fn test_first_call_with_empty_aa_calls() {
842        // Test with tempo_tx_env but empty calls list
843        let tx_env = super::TempoTxEnv {
844            tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
845                aa_calls: vec![],
846                ..Default::default()
847            })),
848            ..Default::default()
849        };
850
851        assert!(tx_env.first_call().is_none());
852    }
853
854    #[test]
855    fn test_calls() {
856        use alloy_primitives::{Address, Bytes, U256};
857        use revm::context::TxEnv;
858        use tempo_primitives::transaction::Call;
859
860        let addr1 = Address::repeat_byte(0x11);
861        let addr2 = Address::repeat_byte(0x22);
862        let input1 = Bytes::from(vec![0x01]);
863        let input2 = Bytes::from(vec![0x02, 0x03]);
864        let input3 = Bytes::from(vec![0x04, 0x05, 0x06]);
865
866        // Non-AA transaction: returns single call from inner TxEnv
867        let non_aa_tx = super::TempoTxEnv {
868            inner: TxEnv {
869                kind: TxKind::Call(addr1),
870                data: input1.clone(),
871                ..Default::default()
872            },
873            ..Default::default()
874        };
875        let calls: Vec<_> = non_aa_tx.calls().collect();
876        assert_eq!(calls.len(), 1);
877        assert_eq!(*calls[0].0, TxKind::Call(addr1));
878        assert_eq!(calls[0].1, input1.as_ref());
879
880        // AA transaction with multiple calls
881        let aa_tx = super::TempoTxEnv {
882            tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
883                aa_calls: vec![
884                    Call {
885                        to: TxKind::Call(addr1),
886                        value: U256::ZERO,
887                        input: input1.clone(),
888                    },
889                    Call {
890                        to: TxKind::Call(addr2),
891                        value: U256::from(50),
892                        input: input2.clone(),
893                    },
894                    Call {
895                        to: TxKind::Create,
896                        value: U256::from(100),
897                        input: input3.clone(),
898                    },
899                ],
900                ..Default::default()
901            })),
902            ..Default::default()
903        };
904        let calls: Vec<_> = aa_tx.calls().collect();
905        assert_eq!(calls.len(), 3);
906        assert_eq!(*calls[0].0, TxKind::Call(addr1));
907        assert_eq!(calls[0].1, input1.as_ref());
908        assert_eq!(*calls[1].0, TxKind::Call(addr2));
909        assert_eq!(calls[1].1, input2.as_ref());
910        assert_eq!(*calls[2].0, TxKind::Create);
911        assert_eq!(calls[2].1, input3.as_ref());
912
913        // AA transaction with empty calls list
914        let empty_aa_tx = super::TempoTxEnv {
915            tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
916                aa_calls: vec![],
917                ..Default::default()
918            })),
919            ..Default::default()
920        };
921        let calls: Vec<_> = empty_aa_tx.calls().collect();
922        assert!(calls.is_empty());
923    }
924
925    /// Strategy for random U256 values.
926    fn arb_u256() -> impl Strategy<Value = alloy_primitives::U256> {
927        any::<[u64; 4]>().prop_map(alloy_primitives::U256::from_limbs)
928    }
929
930    /// Helper to create a TempoTxEnv with the given gas/fee/value parameters.
931    fn make_tx_env(
932        gas_limit: u64,
933        gas_price: u128,
934        value: alloy_primitives::U256,
935    ) -> super::TempoTxEnv {
936        super::TempoTxEnv {
937            inner: revm::context::TxEnv {
938                gas_limit,
939                gas_price,
940                value,
941                ..Default::default()
942            },
943            ..Default::default()
944        }
945    }
946
947    proptest! {
948        #![proptest_config(ProptestConfig::with_cases(500))]
949
950        /// Property: max_balance_spending never panics, returns Ok or OverflowPaymentInTransaction
951        #[test]
952        fn proptest_max_balance_spending_no_panic(
953            gas_limit in any::<u64>(),
954            max_fee_per_gas in any::<u128>(),
955            value in arb_u256(),
956        ) {
957            let tx_env = make_tx_env(gas_limit, max_fee_per_gas, value);
958            let result = tx_env.max_balance_spending();
959            prop_assert!(
960                result.is_ok()
961                    || result == Err(InvalidTransaction::OverflowPaymentInTransaction)
962            );
963        }
964
965        /// Property: max_balance_spending returns overflow when gas*price + value overflows U256
966        #[test]
967        fn proptest_max_balance_spending_overflow_detection(
968            gas_limit in any::<u64>(),
969            max_fee_per_gas in any::<u128>(),
970            value in arb_u256(),
971        ) {
972            let tx_env = make_tx_env(gas_limit, max_fee_per_gas, value);
973            let gas_spending = calc_gas_balance_spending(gas_limit, max_fee_per_gas);
974            let result = tx_env.max_balance_spending();
975
976            match gas_spending.checked_add(value) {
977                Some(expected) => prop_assert_eq!(result, Ok(expected)),
978                None => prop_assert_eq!(result, Err(InvalidTransaction::OverflowPaymentInTransaction)),
979            }
980        }
981
982        /// Property: effective_balance_spending <= max_balance_spending (when both succeed)
983        /// Uses constrained ranges to ensure we don't overflow and actually test the property.
984        #[test]
985        fn proptest_effective_le_max_balance_spending(
986            gas_limit in 0u64..30_000_000u64,  // realistic gas limits
987            max_fee_per_gas in 0u128..1_000_000_000_000u128,  // up to 1000 gwei
988            max_priority_fee in 0u128..100_000_000_000u128,   // up to 100 gwei
989            base_fee in 0u128..500_000_000_000u128,           // up to 500 gwei
990            value in 0u128..10_000_000_000_000_000_000_000u128,  // up to 10k ETH in wei
991        ) {
992            let mut tx_env = make_tx_env(gas_limit, max_fee_per_gas, alloy_primitives::U256::from(value));
993            tx_env.inner.gas_priority_fee = Some(max_priority_fee);
994
995            let max_result = tx_env.max_balance_spending();
996            let effective_result = tx_env.effective_balance_spending(base_fee, 0);
997
998            // With constrained inputs, both should succeed
999            let max_spending = max_result.expect("max_balance_spending should succeed with constrained inputs");
1000            let effective_spending = effective_result.expect("effective_balance_spending should succeed with constrained inputs");
1001
1002            prop_assert!(
1003                effective_spending <= max_spending,
1004                "effective_balance_spending ({}) should be <= max_balance_spending ({})",
1005                effective_spending,
1006                max_spending
1007            );
1008        }
1009
1010        /// Property: effective_balance_spending with base_fee=0 uses only priority fee (EIP-1559)
1011        ///
1012        /// For EIP-1559 transactions with base_fee=0:
1013        /// effective_gas_price = min(max_fee_per_gas, base_fee + priority_fee) = min(max_fee, priority_fee)
1014        /// This test verifies the computation matches expectations.
1015        #[test]
1016        fn proptest_effective_balance_spending_zero_base_fee(
1017            gas_limit in 0u64..30_000_000u64,
1018            max_fee_per_gas in 0u128..1_000_000_000_000u128,
1019            priority_fee in 0u128..500_000_000_000u128,
1020            value in 0u128..10_000_000_000_000_000_000_000u128,
1021        ) {
1022            use revm::context::Transaction;
1023
1024            let mut tx_env = make_tx_env(gas_limit, max_fee_per_gas, alloy_primitives::U256::from(value));
1025            // Set tx_type to EIP-1559 and priority fee
1026            tx_env.inner.tx_type = 2; // EIP-1559
1027            tx_env.inner.gas_priority_fee = Some(priority_fee);
1028
1029            let result = tx_env.effective_balance_spending(0, 0);
1030
1031            // For EIP-1559: effective_gas_price = min(max_fee, 0 + priority_fee) = min(max_fee, priority_fee)
1032            let effective_price = std::cmp::min(max_fee_per_gas, priority_fee);
1033            let expected_gas_spending = calc_gas_balance_spending(gas_limit, effective_price);
1034            let expected = expected_gas_spending.checked_add(alloy_primitives::U256::from(value));
1035
1036            match expected {
1037                Some(expected_val) => prop_assert_eq!(result, Ok(expected_val)),
1038                None => prop_assert_eq!(result, Err(InvalidTransaction::OverflowPaymentInTransaction)),
1039            }
1040        }
1041
1042        /// Property: calls() returns exactly aa_calls.len() for AA transactions
1043        #[test]
1044        fn proptest_calls_count_aa_tx(num_calls in 0usize..20) {
1045            let aa_tx = super::TempoTxEnv {
1046                tempo_tx_env: Some(Box::new(super::TempoBatchCallEnv {
1047                    aa_calls: (0..num_calls)
1048                        .map(|_| Call {
1049                            to: TxKind::Call(alloy_primitives::Address::ZERO),
1050                            value: alloy_primitives::U256::ZERO,
1051                            input: alloy_primitives::Bytes::new(),
1052                        })
1053                        .collect(),
1054                    ..Default::default()
1055                })),
1056                ..Default::default()
1057            };
1058            prop_assert_eq!(aa_tx.calls().count(), num_calls);
1059        }
1060
1061    }
1062
1063    #[test]
1064    fn test_calls_count_non_aa_tx() {
1065        let non_aa_tx = make_tx_env(21_000, 0, alloy_primitives::U256::ZERO);
1066        assert_eq!(non_aa_tx.calls().count(), 1);
1067    }
1068}