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, TxFeeToken,
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/// Tempo transaction environment.
55#[derive(Debug, Clone, Default, derive_more::Deref, derive_more::DerefMut)]
56pub struct TempoTxEnv {
57    /// Inner Ethereum [`TxEnv`].
58    #[deref]
59    #[deref_mut]
60    pub inner: TxEnv,
61
62    /// Optional fee token preference specified for the transaction.
63    pub fee_token: Option<Address>,
64
65    /// Whether the transaction is a system transaction.
66    pub is_system_tx: bool,
67
68    /// Optional fee payer specified for the transaction.
69    ///
70    /// - Some(Some(address)) corresponds to a successfully recovered fee payer
71    /// - Some(None) corresponds to a failed recovery and means that transaction is invalid
72    /// - None corresponds to a transaction without a fee payer
73    pub fee_payer: Option<Option<Address>>,
74
75    /// AA-specific transaction environment (boxed to keep TempoTxEnv lean for non-AA tx)
76    pub tempo_tx_env: Option<Box<TempoBatchCallEnv>>,
77}
78
79impl TempoTxEnv {
80    /// Resolves fee payer from the signature.
81    pub fn fee_payer(&self) -> Result<Address, TempoInvalidTransaction> {
82        if let Some(fee_payer) = self.fee_payer {
83            fee_payer.ok_or(TempoInvalidTransaction::InvalidFeePayerSignature)
84        } else {
85            Ok(self.caller())
86        }
87    }
88
89    /// Returns true if the transaction is a subblock transaction.
90    pub fn is_subblock_transaction(&self) -> bool {
91        self.tempo_tx_env
92            .as_ref()
93            .is_some_and(|aa| aa.subblock_transaction)
94    }
95
96    /// Returns the first top-level call in the transaction.
97    pub fn first_call(&self) -> Option<(&TxKind, &[u8])> {
98        if let Some(aa) = self.tempo_tx_env.as_ref() {
99            aa.aa_calls
100                .first()
101                .map(|call| (&call.to, call.input.as_ref()))
102        } else {
103            Some((&self.inner.kind, &self.inner.data))
104        }
105    }
106
107    /// Invokes the given closure for each top-level call in the transaction and
108    /// returns true if all calls returned true.
109    pub fn calls(&self) -> impl Iterator<Item = (&TxKind, &[u8])> {
110        if let Some(aa) = self.tempo_tx_env.as_ref() {
111            Either::Left(
112                aa.aa_calls
113                    .iter()
114                    .map(|call| (&call.to, call.input.as_ref())),
115            )
116        } else {
117            Either::Right(core::iter::once((
118                &self.inner.kind,
119                self.inner.input().as_ref(),
120            )))
121        }
122    }
123}
124
125impl From<TxEnv> for TempoTxEnv {
126    fn from(inner: TxEnv) -> Self {
127        Self {
128            inner,
129            ..Default::default()
130        }
131    }
132}
133
134impl Transaction for TempoTxEnv {
135    type AccessListItem<'a> = &'a AccessListItem;
136    type Authorization<'a> = &'a Either<SignedAuthorization, RecoveredAuthorization>;
137
138    fn tx_type(&self) -> u8 {
139        self.inner.tx_type()
140    }
141
142    fn kind(&self) -> TxKind {
143        self.inner.kind()
144    }
145
146    fn caller(&self) -> Address {
147        self.inner.caller()
148    }
149
150    fn gas_limit(&self) -> u64 {
151        self.inner.gas_limit()
152    }
153
154    fn gas_price(&self) -> u128 {
155        self.inner.gas_price()
156    }
157
158    fn value(&self) -> U256 {
159        self.inner.value()
160    }
161
162    fn nonce(&self) -> u64 {
163        Transaction::nonce(&self.inner)
164    }
165
166    fn chain_id(&self) -> Option<u64> {
167        self.inner.chain_id()
168    }
169
170    fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>> {
171        self.inner.access_list()
172    }
173
174    fn max_fee_per_gas(&self) -> u128 {
175        self.inner.max_fee_per_gas()
176    }
177
178    fn max_fee_per_blob_gas(&self) -> u128 {
179        self.inner.max_fee_per_blob_gas()
180    }
181
182    fn authorization_list_len(&self) -> usize {
183        self.inner.authorization_list_len()
184    }
185
186    fn authorization_list(&self) -> impl Iterator<Item = Self::Authorization<'_>> {
187        self.inner.authorization_list()
188    }
189
190    fn input(&self) -> &Bytes {
191        self.inner.input()
192    }
193
194    fn blob_versioned_hashes(&self) -> &[B256] {
195        self.inner.blob_versioned_hashes()
196    }
197
198    fn max_priority_fee_per_gas(&self) -> Option<u128> {
199        self.inner.max_priority_fee_per_gas()
200    }
201
202    fn max_balance_spending(&self) -> Result<U256, InvalidTransaction> {
203        calc_gas_balance_spending(self.gas_limit(), self.max_fee_per_gas())
204            .checked_add(self.value())
205            .ok_or(InvalidTransaction::OverflowPaymentInTransaction)
206    }
207
208    fn effective_balance_spending(
209        &self,
210        base_fee: u128,
211        _blob_price: u128,
212    ) -> Result<U256, InvalidTransaction> {
213        calc_gas_balance_spending(self.gas_limit(), self.effective_gas_price(base_fee))
214            .checked_add(self.value())
215            .ok_or(InvalidTransaction::OverflowPaymentInTransaction)
216    }
217}
218
219impl TransactionEnv for TempoTxEnv {
220    fn set_gas_limit(&mut self, gas_limit: u64) {
221        self.inner.set_gas_limit(gas_limit);
222    }
223
224    fn nonce(&self) -> u64 {
225        Transaction::nonce(&self.inner)
226    }
227
228    fn set_nonce(&mut self, nonce: u64) {
229        self.inner.set_nonce(nonce);
230    }
231
232    fn set_access_list(&mut self, access_list: AccessList) {
233        self.inner.set_access_list(access_list);
234    }
235}
236
237impl IntoTxEnv<Self> for TempoTxEnv {
238    fn into_tx_env(self) -> Self {
239        self
240    }
241}
242
243impl FromRecoveredTx<EthereumTxEnvelope<TxEip4844>> for TempoTxEnv {
244    fn from_recovered_tx(tx: &EthereumTxEnvelope<TxEip4844>, sender: Address) -> Self {
245        TxEnv::from_recovered_tx(tx, sender).into()
246    }
247}
248
249impl FromRecoveredTx<TxFeeToken> for TempoTxEnv {
250    fn from_recovered_tx(tx: &TxFeeToken, caller: Address) -> Self {
251        let TxFeeToken {
252            chain_id,
253            nonce,
254            gas_limit,
255            to,
256            value,
257            input,
258            max_fee_per_gas,
259            max_priority_fee_per_gas,
260            access_list,
261            authorization_list,
262            fee_token,
263            fee_payer_signature,
264        } = tx;
265        Self {
266            inner: TxEnv {
267                tx_type: tx.ty(),
268                caller,
269                gas_limit: *gas_limit,
270                gas_price: *max_fee_per_gas,
271                kind: *to,
272                value: *value,
273                data: input.clone(),
274                nonce: *nonce,
275                chain_id: Some(*chain_id),
276                gas_priority_fee: Some(*max_priority_fee_per_gas),
277                access_list: access_list.clone(),
278                authorization_list: authorization_list
279                    .iter()
280                    .map(|auth| {
281                        Either::Right(RecoveredAuthorization::new_unchecked(
282                            auth.inner().clone(),
283                            auth.signature()
284                                .ok()
285                                .and_then(|signature| {
286                                    secp256k1::recover_signer(&signature, auth.signature_hash())
287                                        .ok()
288                                })
289                                .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid),
290                        ))
291                    })
292                    .collect(),
293                ..Default::default()
294            },
295            fee_token: *fee_token,
296            is_system_tx: false,
297            fee_payer: fee_payer_signature.map(|sig| {
298                sig.recover_address_from_prehash(&tx.fee_payer_signature_hash(caller))
299                    .ok()
300            }),
301            tempo_tx_env: None, // Non-AA transaction
302        }
303    }
304}
305
306impl FromRecoveredTx<AASigned> for TempoTxEnv {
307    fn from_recovered_tx(aa_signed: &AASigned, caller: Address) -> Self {
308        let tx = aa_signed.tx();
309        let signature = aa_signed.signature();
310
311        // Populate the key_id cache for Keychain signatures before cloning
312        // This parallelizes recovery during Tx->TxEnv conversion, and the cache is preserved when cloned
313        if let Some(keychain_sig) = signature.as_keychain() {
314            let _ = keychain_sig.key_id(&aa_signed.signature_hash());
315        }
316
317        let TempoTransaction {
318            chain_id,
319            fee_token,
320            max_priority_fee_per_gas,
321            max_fee_per_gas,
322            gas_limit,
323            calls,
324            access_list,
325            nonce_key,
326            nonce,
327            fee_payer_signature,
328            valid_before,
329            valid_after,
330            key_authorization,
331            tempo_authorization_list,
332        } = tx;
333
334        // Extract to/value/input from calls (use first call or defaults)
335        let (to, value, input) = if let Some(first_call) = calls.first() {
336            (first_call.to, first_call.value, first_call.input.clone())
337        } else {
338            (
339                alloy_primitives::TxKind::Create,
340                alloy_primitives::U256::ZERO,
341                alloy_primitives::Bytes::new(),
342            )
343        };
344
345        Self {
346            inner: TxEnv {
347                tx_type: tx.ty(),
348                caller,
349                gas_limit: *gas_limit,
350                gas_price: *max_fee_per_gas,
351                kind: to,
352                value,
353                data: input,
354                nonce: *nonce, // AA: nonce maps to TxEnv.nonce
355                chain_id: Some(*chain_id),
356                gas_priority_fee: Some(*max_priority_fee_per_gas),
357                access_list: access_list.clone(),
358                // Convert Tempo authorization list to RecoveredAuthorization upfront
359                authorization_list: tempo_authorization_list
360                    .iter()
361                    .map(|auth| {
362                        let authority = auth
363                            .recover_authority()
364                            .map_or(RecoveredAuthority::Invalid, RecoveredAuthority::Valid);
365                        Either::Right(RecoveredAuthorization::new_unchecked(
366                            auth.inner().clone(),
367                            authority,
368                        ))
369                    })
370                    .collect(),
371                ..Default::default()
372            },
373            fee_token: *fee_token,
374            is_system_tx: false,
375            fee_payer: fee_payer_signature.map(|sig| {
376                sig.recover_address_from_prehash(&tx.fee_payer_signature_hash(caller))
377                    .ok()
378            }),
379            // Bundle AA-specific fields into TempoBatchCallEnv
380            tempo_tx_env: Some(Box::new(TempoBatchCallEnv {
381                signature: signature.clone(),
382                valid_before: *valid_before,
383                valid_after: *valid_after,
384                aa_calls: calls.clone(),
385                // Recover authorizations upfront to avoid recovery during execution
386                tempo_authorization_list: tempo_authorization_list
387                    .iter()
388                    .map(|auth| RecoveredTempoAuthorization::recover(auth.clone()))
389                    .collect(),
390                nonce_key: *nonce_key,
391                subblock_transaction: aa_signed.tx().subblock_proposer().is_some(),
392                key_authorization: key_authorization.clone(),
393                signature_hash: aa_signed.signature_hash(),
394            })),
395        }
396    }
397}
398
399impl FromRecoveredTx<TempoTxEnvelope> for TempoTxEnv {
400    fn from_recovered_tx(tx: &TempoTxEnvelope, sender: Address) -> Self {
401        match tx {
402            tx @ TempoTxEnvelope::Legacy(inner) => Self {
403                inner: TxEnv::from_recovered_tx(inner.tx(), sender),
404                fee_token: None,
405                is_system_tx: tx.is_system_tx(),
406                fee_payer: None,
407                tempo_tx_env: None, // Non-AA transaction
408            },
409            TempoTxEnvelope::Eip2930(tx) => TxEnv::from_recovered_tx(tx.tx(), sender).into(),
410            TempoTxEnvelope::Eip1559(tx) => TxEnv::from_recovered_tx(tx.tx(), sender).into(),
411            TempoTxEnvelope::Eip7702(tx) => TxEnv::from_recovered_tx(tx.tx(), sender).into(),
412            TempoTxEnvelope::AA(tx) => Self::from_recovered_tx(tx, sender),
413            TempoTxEnvelope::FeeToken(tx) => Self::from_recovered_tx(tx.tx(), sender),
414        }
415    }
416}
417
418impl FromTxWithEncoded<EthereumTxEnvelope<TxEip4844>> for TempoTxEnv {
419    fn from_encoded_tx(
420        tx: &EthereumTxEnvelope<TxEip4844>,
421        sender: Address,
422        _encoded: Bytes,
423    ) -> Self {
424        Self::from_recovered_tx(tx, sender)
425    }
426}
427
428impl FromTxWithEncoded<TxFeeToken> for TempoTxEnv {
429    fn from_encoded_tx(tx: &TxFeeToken, sender: Address, _encoded: Bytes) -> Self {
430        Self::from_recovered_tx(tx, sender)
431    }
432}
433
434impl FromTxWithEncoded<AASigned> for TempoTxEnv {
435    fn from_encoded_tx(tx: &AASigned, sender: Address, _encoded: Bytes) -> Self {
436        Self::from_recovered_tx(tx, sender)
437    }
438}
439
440impl FromTxWithEncoded<TempoTxEnvelope> for TempoTxEnv {
441    fn from_encoded_tx(tx: &TempoTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
442        Self::from_recovered_tx(tx, sender)
443    }
444}