Skip to main content

tempo_revm/
tx.rs

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