Skip to main content

tempo_primitives/transaction/
tempo_transaction.rs

1#[cfg(feature = "serde")]
2use crate::transaction::key_authorization::serde_nonzero_quantity_opt;
3use crate::{
4    subblock::{PartialValidatorKey, has_sub_block_nonce_key_prefix},
5    transaction::{
6        AASigned, TempoSignature, TempoSignedAuthorization,
7        key_authorization::SignedKeyAuthorization,
8    },
9};
10use alloc::vec::Vec;
11use alloy_consensus::{SignableTransaction, Transaction, crypto::RecoveryError};
12use alloy_eips::{Typed2718, eip2930::AccessList, eip7702::SignedAuthorization};
13use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, keccak256};
14use alloy_rlp::{Buf, BufMut, Decodable, EMPTY_STRING_CODE, Encodable};
15use core::num::NonZeroU64;
16
17/// Tempo transaction type byte (0x76)
18pub const TEMPO_TX_TYPE_ID: u8 = 0x76;
19
20/// Magic byte for the fee payer signature
21pub const FEE_PAYER_SIGNATURE_MAGIC_BYTE: u8 = 0x78;
22
23/// Placeholder signature used to mark transactions that need fee-payer signing.
24pub const FEE_PAYER_SIGNATURE_MARKER: Signature = Signature::new(U256::ZERO, U256::ZERO, false);
25
26/// Signature type constants
27pub const SECP256K1_SIGNATURE_LENGTH: usize = 65;
28pub const P256_SIGNATURE_LENGTH: usize = 129;
29pub const MAX_WEBAUTHN_SIGNATURE_LENGTH: usize = 2048; // 2KB max
30
31/// Nonce key marking an expiring nonce transaction (uses tx hash for replay protection).
32pub const TEMPO_EXPIRING_NONCE_KEY: U256 = U256::MAX;
33
34/// Maximum allowed expiry window for expiring nonce transactions (30 seconds).
35pub const TEMPO_EXPIRING_NONCE_MAX_EXPIRY_SECS: u64 = 30;
36
37/// Signature type enumeration
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
41#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
42#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
43#[repr(u8)]
44pub enum SignatureType {
45    Secp256k1 = 0,
46    P256 = 1,
47    WebAuthn = 2,
48}
49
50impl From<SignatureType> for u8 {
51    fn from(sig_type: SignatureType) -> Self {
52        match sig_type {
53            SignatureType::Secp256k1 => 0,
54            SignatureType::P256 => 1,
55            SignatureType::WebAuthn => 2,
56        }
57    }
58}
59
60use tempo_contracts::precompiles::IAccountKeychain::SignatureType as AbiSignatureType;
61
62impl From<SignatureType> for AbiSignatureType {
63    fn from(sig_type: SignatureType) -> Self {
64        match sig_type {
65            SignatureType::Secp256k1 => Self::Secp256k1,
66            SignatureType::P256 => Self::P256,
67            SignatureType::WebAuthn => Self::WebAuthn,
68        }
69    }
70}
71
72impl TryFrom<AbiSignatureType> for SignatureType {
73    type Error = u8;
74
75    fn try_from(sig_type: AbiSignatureType) -> Result<Self, Self::Error> {
76        match sig_type {
77            AbiSignatureType::Secp256k1 => Ok(Self::Secp256k1),
78            AbiSignatureType::P256 => Ok(Self::P256),
79            AbiSignatureType::WebAuthn => Ok(Self::WebAuthn),
80            _ => Err(sig_type as u8),
81        }
82    }
83}
84
85impl alloy_rlp::Encodable for SignatureType {
86    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
87        (*self as u8).encode(out);
88    }
89
90    fn length(&self) -> usize {
91        1
92    }
93}
94
95impl alloy_rlp::Decodable for SignatureType {
96    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
97        let byte: u8 = alloy_rlp::Decodable::decode(buf)?;
98        match byte {
99            0 => Ok(Self::Secp256k1),
100            1 => Ok(Self::P256),
101            2 => Ok(Self::WebAuthn),
102            _ => Err(alloy_rlp::Error::Custom("Invalid signature type")),
103        }
104    }
105}
106
107/// Helper function to create an RLP header for a list with the given payload length
108#[inline]
109fn rlp_header(payload_length: usize) -> alloy_rlp::Header {
110    alloy_rlp::Header {
111        list: true,
112        payload_length,
113    }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
119#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
120#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
121#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
122pub struct Call {
123    /// Call target.
124    pub to: TxKind,
125
126    /// Call value.
127    pub value: U256,
128
129    /// Call input.
130    #[cfg_attr(feature = "serde", serde(flatten, with = "serde_input"))]
131    pub input: Bytes,
132}
133
134impl Call {
135    /// Returns the RLP header for this call, encapsulating both length calculation and header creation
136    #[inline]
137    fn rlp_header(&self) -> alloy_rlp::Header {
138        let payload_length = self.to.length() + self.value.length() + self.input.length();
139        alloy_rlp::Header {
140            list: true,
141            payload_length,
142        }
143    }
144
145    fn size(&self) -> usize {
146        size_of::<Self>() + self.input.len()
147    }
148}
149
150impl Encodable for Call {
151    fn encode(&self, out: &mut dyn BufMut) {
152        self.rlp_header().encode(out);
153        self.to.encode(out);
154        self.value.encode(out);
155        self.input.encode(out);
156    }
157
158    fn length(&self) -> usize {
159        self.rlp_header().length_with_payload()
160    }
161}
162
163impl Decodable for Call {
164    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
165        let header = alloy_rlp::Header::decode(buf)?;
166        if !header.list {
167            return Err(alloy_rlp::Error::UnexpectedString);
168        }
169        let remaining = buf.len();
170
171        if header.payload_length > remaining {
172            return Err(alloy_rlp::Error::InputTooShort);
173        }
174
175        let this = Self {
176            to: Decodable::decode(buf)?,
177            value: Decodable::decode(buf)?,
178            input: Decodable::decode(buf)?,
179        };
180
181        if buf.len() + header.payload_length != remaining {
182            return Err(alloy_rlp::Error::UnexpectedLength);
183        }
184
185        Ok(this)
186    }
187}
188
189/// Tempo transaction following the Tempo spec.
190///
191/// This transaction type supports:
192/// - Multiple signature types (secp256k1, P256, WebAuthn)
193/// - Parallelizable nonces via 2D nonce system (nonce_key + nonce)
194/// - Gas sponsorship via fee payer
195/// - Scheduled transactions (validBefore/validAfter)
196/// - EIP-7702 authorization lists
197#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
200#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
201pub struct TempoTransaction {
202    /// EIP-155: Simple replay attack protection
203    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
204    pub chain_id: ChainId,
205
206    /// Optional fee token preference (`None` means no preference)
207    pub fee_token: Option<Address>,
208
209    /// Max Priority fee per gas (EIP-1559)
210    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
211    pub max_priority_fee_per_gas: u128,
212
213    /// Max fee per gas (EIP-1559)
214    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
215    pub max_fee_per_gas: u128,
216
217    /// Gas limit
218    #[cfg_attr(
219        feature = "serde",
220        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
221    )]
222    pub gas_limit: u64,
223
224    /// Calls to be executed atomically
225    pub calls: Vec<Call>,
226
227    /// Access list (EIP-2930)
228    pub access_list: AccessList,
229
230    /// TT-specific fields
231
232    /// Nonce key for 2D nonce system
233    /// Key 0 is the protocol nonce, keys 1-N are user nonces for parallelization
234    pub nonce_key: U256,
235
236    /// Current nonce value for the nonce key
237    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
238    pub nonce: u64,
239
240    /// Optional features
241
242    /// Optional fee payer signature for sponsored transactions (secp256k1 only)
243    pub fee_payer_signature: Option<Signature>,
244
245    /// Upper bound for the transaction validity window, as a Unix timestamp in seconds.
246    ///
247    /// The transaction can only be included in a block with
248    /// `block.timestamp < valid_before`. For expiring nonces, this is the
249    /// `validBefore` bound defined by [TIP-1009].
250    ///
251    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
252    #[cfg_attr(feature = "serde", serde(with = "serde_nonzero_quantity_opt"))]
253    pub valid_before: Option<NonZeroU64>,
254
255    /// Lower bound for the transaction validity window, as a Unix timestamp in seconds.
256    ///
257    /// The transaction can only be included in a block with
258    /// `block.timestamp >= valid_after`. For expiring nonces, this is the
259    /// `validAfter` bound defined by [TIP-1009].
260    ///
261    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
262    #[cfg_attr(feature = "serde", serde(with = "serde_nonzero_quantity_opt"))]
263    pub valid_after: Option<NonZeroU64>,
264
265    /// Optional key authorization for provisioning a new access key
266    ///
267    /// When present, this transaction will add the specified key to the AccountKeychain precompile,
268    /// before verifying the transaction signature.
269    /// The authorization must be signed with the root key, the tx can be signed by the Keychain signature.
270    pub key_authorization: Option<SignedKeyAuthorization>,
271
272    /// Authorization list (EIP-7702 style with Tempo signatures)
273    #[cfg_attr(feature = "serde", serde(rename = "aaAuthorizationList"))]
274    pub tempo_authorization_list: Vec<TempoSignedAuthorization>,
275}
276
277/// Validates the calls list structure for Tempo transactions.
278///
279/// This is a shared validation function used by both `TempoTransaction::validate()`
280/// and the revm handler's `validate_env()` to ensure consistent validation.
281///
282/// Rules:
283/// - Calls list must not be empty
284/// - CREATE calls are not allowed when authorization list is non-empty (EIP-7702 semantics)
285/// - Only the first call can be a CREATE; all subsequent calls must be CALL
286pub fn validate_calls(calls: &[Call], has_authorization_list: bool) -> Result<(), &'static str> {
287    // Calls must not be empty (similar to EIP-7702 rejecting empty auth lists)
288    if calls.is_empty() {
289        return Err("calls list cannot be empty");
290    }
291
292    let mut calls_iter = calls.iter();
293
294    // Only the first call in the batch can be a CREATE call.
295    if let Some(call) = calls_iter.next()
296        // Authorization list validation: Can NOT have CREATE when authorization list is non-empty
297        // This follows EIP-7702 semantics - when using delegation
298        && has_authorization_list
299        && call.to.is_create()
300    {
301        return Err("calls cannot contain CREATE when 'aa_authorization_list' is non-empty");
302    }
303
304    // All subsequent calls must be CALL.
305    for call in calls_iter {
306        if call.to.is_create() {
307            return Err(
308                "only one CREATE call is allowed per transaction, and it must be the first call of the batch",
309            );
310        }
311    }
312
313    Ok(())
314}
315
316impl TempoTransaction {
317    /// Get the transaction type
318    #[doc(alias = "transaction_type")]
319    pub const fn tx_type() -> u8 {
320        TEMPO_TX_TYPE_ID
321    }
322
323    /// Returns true if this is an expiring nonce transaction.
324    ///
325    /// Expiring nonce transactions use the tx hash for replay protection instead of
326    /// sequential nonces. They are identified by `nonce_key == U256::MAX`.
327    #[inline]
328    pub fn is_expiring_nonce_tx(&self) -> bool {
329        self.nonce_key == TEMPO_EXPIRING_NONCE_KEY
330    }
331
332    /// Validates the transaction according to invariant rules.
333    ///
334    /// This performs structural validation that is always required, regardless of hardfork.
335    /// Hardfork-dependent validation (e.g., expiring nonce constraints) is performed by
336    /// the transaction pool validator and execution handler where hardfork context is available.
337    pub fn validate(&self) -> Result<(), &'static str> {
338        // Validate calls list structure using the shared function
339        validate_calls(&self.calls, !self.tempo_authorization_list.is_empty())?;
340
341        // validBefore must be greater than validAfter if both are set
342        if let Some(valid_after) = self.valid_after
343            && let Some(valid_before) = self.valid_before
344            && valid_before <= valid_after
345        {
346            return Err("valid_before must be greater than valid_after");
347        }
348
349        Ok(())
350    }
351
352    /// Calculates a heuristic for the in-memory size of the transaction
353    #[inline]
354    pub fn size(&self) -> usize {
355        size_of::<Self>()
356            + self.calls.iter().map(|call| call.size()).sum::<usize>()
357            + self.access_list.size()
358            + self.key_authorization.as_ref().map_or(0, |k| k.size())
359            + self
360                .tempo_authorization_list
361                .iter()
362                .map(|auth| auth.size())
363                .sum::<usize>()
364    }
365
366    /// Convert the transaction into a signed transaction
367    pub fn into_signed(self, signature: TempoSignature) -> AASigned {
368        AASigned::new_unhashed(self, signature)
369    }
370
371    /// Calculate the signing hash for this transaction
372    /// This is the hash that should be signed by the sender
373    pub fn signature_hash(&self) -> B256 {
374        let mut buf = Vec::new();
375        self.encode_for_signing(&mut buf);
376        keccak256(&buf)
377    }
378
379    /// Calculate the fee payer signature hash.
380    ///
381    /// This hash is signed by the fee payer to sponsor the transaction
382    pub fn fee_payer_signature_hash(&self, sender: Address) -> B256 {
383        // Use helper functions for consistent encoding
384        let payload_length = self.rlp_encoded_fields_length(|_| sender.length(), false);
385
386        let mut buf = Vec::with_capacity(1 + rlp_header(payload_length).length_with_payload());
387
388        // Magic byte for fee payer signature (like TxFeeToken)
389        buf.put_u8(FEE_PAYER_SIGNATURE_MAGIC_BYTE);
390
391        // RLP header
392        rlp_header(payload_length).encode(&mut buf);
393
394        // Encode fields using helper (skip_fee_token = false, so fee_token IS included)
395        self.rlp_encode_fields(
396            &mut buf,
397            |_, out| {
398                // Encode sender address instead of fee_payer_signature
399                sender.encode(out);
400            },
401            false, // skip_fee_token = FALSE - fee payer commits to fee_token!
402        );
403
404        keccak256(&buf)
405    }
406
407    /// Recovers the fee payer for this transaction.
408    ///
409    /// This returns the given sender if the transaction doesn't include a fee payer signature
410    pub fn recover_fee_payer(&self, sender: Address) -> Result<Address, RecoveryError> {
411        if let Some(fee_payer_signature) = &self.fee_payer_signature {
412            alloy_consensus::crypto::secp256k1::recover_signer(
413                fee_payer_signature,
414                self.fee_payer_signature_hash(sender),
415            )
416        } else {
417            Ok(sender)
418        }
419    }
420
421    /// Outputs the length of the transaction's fields, without a RLP header.
422    ///
423    /// This is the internal helper that takes closures for flexible encoding.
424    pub(crate) fn rlp_encoded_fields_length(
425        &self,
426        signature_length: impl FnOnce(&Option<Signature>) -> usize,
427        skip_fee_token: bool,
428    ) -> usize {
429        self.chain_id.length() +
430            self.max_priority_fee_per_gas.length() +
431            self.max_fee_per_gas.length() +
432            self.gas_limit.length() +
433            self.calls.length() +
434            self.access_list.length() +
435            self.nonce_key.length() +
436            self.nonce.length() +
437            self.valid_before.map_or(1, |valid_before| valid_before.length()) +
438            // valid_after (optional u64)
439            self.valid_after.map_or(1, |valid_after| valid_after.length()) +
440            // fee_token (optional Address)
441            if !skip_fee_token && let Some(addr) = self.fee_token {
442                addr.length()
443            } else {
444                1 // EMPTY_STRING_CODE
445            } +
446            signature_length(&self.fee_payer_signature) +
447            // authorization_list
448            self.tempo_authorization_list.length() +
449            // key_authorization (only included if present)
450            if let Some(key_auth) = &self.key_authorization {
451                key_auth.length()
452            } else {
453                0 // No bytes when None
454            }
455    }
456
457    pub(crate) fn rlp_encode_fields(
458        &self,
459        out: &mut dyn BufMut,
460        encode_signature: impl FnOnce(&Option<Signature>, &mut dyn BufMut),
461        skip_fee_token: bool,
462    ) {
463        self.chain_id.encode(out);
464        self.max_priority_fee_per_gas.encode(out);
465        self.max_fee_per_gas.encode(out);
466        self.gas_limit.encode(out);
467        self.calls.encode(out);
468        self.access_list.encode(out);
469        self.nonce_key.encode(out);
470        self.nonce.encode(out);
471
472        if let Some(valid_before) = self.valid_before {
473            valid_before.encode(out);
474        } else {
475            out.put_u8(EMPTY_STRING_CODE);
476        }
477
478        if let Some(valid_after) = self.valid_after {
479            valid_after.encode(out);
480        } else {
481            out.put_u8(EMPTY_STRING_CODE);
482        }
483
484        if !skip_fee_token && let Some(addr) = self.fee_token {
485            addr.encode(out);
486        } else {
487            out.put_u8(EMPTY_STRING_CODE);
488        }
489
490        encode_signature(&self.fee_payer_signature, out);
491
492        // Encode authorization_list
493        self.tempo_authorization_list.encode(out);
494
495        // Encode key_authorization (truly optional - only encoded if present)
496        if let Some(key_auth) = &self.key_authorization {
497            key_auth.encode(out);
498        }
499        // No bytes at all when None - maintains backwards compatibility
500    }
501
502    /// Public version for normal RLP encoding
503    pub(crate) fn rlp_encoded_fields_length_default(&self) -> usize {
504        self.rlp_encoded_fields_length(
505            |signature| {
506                signature.map_or(1, |s| {
507                    rlp_header(s.rlp_rs_len() + s.v().length()).length_with_payload()
508                })
509            },
510            false,
511        )
512    }
513
514    /// Public version for normal RLP encoding
515    pub(crate) fn rlp_encode_fields_default(&self, out: &mut dyn BufMut) {
516        self.rlp_encode_fields(
517            out,
518            |signature, out| {
519                if let Some(signature) = signature {
520                    let payload_length = signature.rlp_rs_len() + signature.v().length();
521                    rlp_header(payload_length).encode(out);
522                    signature.write_rlp_vrs(out, signature.v());
523                } else {
524                    out.put_u8(EMPTY_STRING_CODE);
525                }
526            },
527            false,
528        )
529    }
530
531    /// Encodes this transaction for submission to a fee-payer service.
532    ///
533    /// Fee-payer services accept an unsigned sponsorship request with `0x00` fee-payer signature.
534    /// This is a placeholder that tells the sponsor where to insert the real fee-payer signature.
535    pub fn encode_for_fee_payer_service(&self, out: &mut dyn BufMut) {
536        out.put_u8(Self::tx_type());
537
538        let payload_length = self.rlp_encoded_fields_length(|_| 1, true);
539        rlp_header(payload_length).encode(out);
540        self.rlp_encode_fields(out, |_, out| out.put_u8(0x00), true);
541    }
542
543    /// Decodes the inner TempoTransaction fields from RLP bytes
544    pub(crate) fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
545        let chain_id = Decodable::decode(buf)?;
546        let max_priority_fee_per_gas = Decodable::decode(buf)?;
547        let max_fee_per_gas = Decodable::decode(buf)?;
548        let gas_limit = Decodable::decode(buf)?;
549        let calls = Decodable::decode(buf)?;
550        let access_list = Decodable::decode(buf)?;
551        let nonce_key = Decodable::decode(buf)?;
552        let nonce = Decodable::decode(buf)?;
553
554        let valid_before = u64::decode(buf).map(NonZeroU64::new)?;
555        let valid_after = u64::decode(buf).map(NonZeroU64::new)?;
556
557        let fee_token = if let Some(first) = buf.first() {
558            if *first == EMPTY_STRING_CODE {
559                buf.advance(1);
560                None
561            } else {
562                TxKind::decode(buf)?.into_to()
563            }
564        } else {
565            return Err(alloy_rlp::Error::InputTooShort);
566        };
567
568        let fee_payer_signature = if let Some(first) = buf.first() {
569            if *first == EMPTY_STRING_CODE {
570                buf.advance(1);
571                None
572            } else {
573                let header = alloy_rlp::Header::decode(buf)?;
574                if buf.len() < header.payload_length {
575                    return Err(alloy_rlp::Error::InputTooShort);
576                }
577                if !header.list {
578                    return Err(alloy_rlp::Error::UnexpectedString);
579                }
580                Some(Signature::decode_rlp_vrs(buf, bool::decode)?)
581            }
582        } else {
583            return Err(alloy_rlp::Error::InputTooShort);
584        };
585
586        let tempo_authorization_list = Decodable::decode(buf)?;
587
588        // Decode optional key_authorization field at the end
589        // Check if the next byte looks like it could be a KeyAuthorization (RLP list)
590        // KeyAuthorization is encoded as a list, so it would start with 0xc0-0xf7 (short list) or 0xf8-0xff (long list)
591        // If it's a bytes string (0x80-0xbf for short, 0xb8-0xbf for long), it's not a
592        // KeyAuthorization and most likely a signature bytes following the AA transaction.
593        let key_authorization = if let Some(&first) = buf.first() {
594            // Check if this looks like an RLP list (KeyAuthorization is always a list)
595            if first >= 0xc0 {
596                // This could be a KeyAuthorization
597                Some(Decodable::decode(buf)?)
598            } else {
599                // This is likely not a KeyAuthorization (probably signature bytes in AASigned context)
600                None
601            }
602        } else {
603            None
604        };
605
606        let tx = Self {
607            chain_id,
608            fee_token,
609            max_priority_fee_per_gas,
610            max_fee_per_gas,
611            gas_limit,
612            calls,
613            access_list,
614            nonce_key,
615            nonce,
616            fee_payer_signature,
617            valid_before,
618            valid_after,
619            key_authorization,
620            tempo_authorization_list,
621        };
622
623        // Validate the transaction
624        tx.validate().map_err(alloy_rlp::Error::Custom)?;
625
626        Ok(tx)
627    }
628
629    /// Returns true if the nonce key of this transaction has the [`TEMPO_SUBBLOCK_NONCE_KEY_PREFIX`](crate::subblock::TEMPO_SUBBLOCK_NONCE_KEY_PREFIX).
630    pub fn has_sub_block_nonce_key_prefix(&self) -> bool {
631        has_sub_block_nonce_key_prefix(&self.nonce_key)
632    }
633
634    /// Returns the proposer of the subblock if this is a subblock transaction.
635    pub fn subblock_proposer(&self) -> Option<PartialValidatorKey> {
636        if self.has_sub_block_nonce_key_prefix() {
637            Some(PartialValidatorKey::from_slice(
638                &self.nonce_key.to_be_bytes::<32>()[1..16],
639            ))
640        } else {
641            None
642        }
643    }
644}
645
646impl Transaction for TempoTransaction {
647    #[inline]
648    fn chain_id(&self) -> Option<ChainId> {
649        Some(self.chain_id)
650    }
651
652    #[inline]
653    fn nonce(&self) -> u64 {
654        self.nonce
655    }
656
657    #[inline]
658    fn gas_limit(&self) -> u64 {
659        self.gas_limit
660    }
661
662    #[inline]
663    fn gas_price(&self) -> Option<u128> {
664        None
665    }
666
667    #[inline]
668    fn max_fee_per_gas(&self) -> u128 {
669        self.max_fee_per_gas
670    }
671
672    #[inline]
673    fn max_priority_fee_per_gas(&self) -> Option<u128> {
674        Some(self.max_priority_fee_per_gas)
675    }
676
677    #[inline]
678    fn max_fee_per_blob_gas(&self) -> Option<u128> {
679        None
680    }
681
682    #[inline]
683    fn priority_fee_or_price(&self) -> u128 {
684        self.max_priority_fee_per_gas
685    }
686
687    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
688        alloy_eips::eip1559::calc_effective_gas_price(
689            self.max_fee_per_gas,
690            self.max_priority_fee_per_gas,
691            base_fee,
692        )
693    }
694
695    #[inline]
696    fn is_dynamic_fee(&self) -> bool {
697        true
698    }
699
700    #[inline]
701    fn kind(&self) -> TxKind {
702        // Return first call's `to` or Create if empty
703        self.calls.first().map(|c| c.to).unwrap_or(TxKind::Create)
704    }
705
706    #[inline]
707    fn is_create(&self) -> bool {
708        self.kind().is_create()
709    }
710
711    #[inline]
712    fn value(&self) -> U256 {
713        // Return sum of all call values, saturating to U256::MAX on overflow
714        self.calls
715            .iter()
716            .fold(U256::ZERO, |acc, call| acc.saturating_add(call.value))
717    }
718
719    #[inline]
720    fn input(&self) -> &Bytes {
721        // Return first call's input or empty
722        static EMPTY_BYTES: Bytes = Bytes::new();
723        self.calls.first().map(|c| &c.input).unwrap_or(&EMPTY_BYTES)
724    }
725
726    #[inline]
727    fn access_list(&self) -> Option<&AccessList> {
728        Some(&self.access_list)
729    }
730
731    #[inline]
732    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
733        None
734    }
735
736    #[inline]
737    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
738        None
739    }
740}
741
742impl Typed2718 for TempoTransaction {
743    fn ty(&self) -> u8 {
744        TEMPO_TX_TYPE_ID
745    }
746}
747
748impl SignableTransaction<Signature> for TempoTransaction {
749    fn set_chain_id(&mut self, chain_id: ChainId) {
750        self.chain_id = chain_id;
751    }
752
753    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
754        // Skip fee_token if fee_payer_signature is present to ensure user doesn't commit to a specific fee token
755        let skip_fee_token = self.fee_payer_signature.is_some();
756
757        // Type byte
758        out.put_u8(Self::tx_type());
759
760        // Compute payload length using helper
761        let payload_length = self.rlp_encoded_fields_length(|_| 1, skip_fee_token);
762
763        rlp_header(payload_length).encode(out);
764
765        // Encode fields using helper
766        self.rlp_encode_fields(
767            out,
768            |signature, out| {
769                if signature.is_some() {
770                    out.put_u8(0); // placeholder byte
771                } else {
772                    out.put_u8(EMPTY_STRING_CODE);
773                }
774            },
775            skip_fee_token,
776        );
777    }
778
779    fn payload_len_for_signature(&self) -> usize {
780        let skip_fee_token = self.fee_payer_signature.is_some();
781        let payload_length = self.rlp_encoded_fields_length(|_| 1, skip_fee_token);
782
783        1 + rlp_header(payload_length).length_with_payload()
784    }
785}
786
787impl Encodable for TempoTransaction {
788    fn encode(&self, out: &mut dyn BufMut) {
789        // Encode as RLP list of fields
790        let payload_length = self.rlp_encoded_fields_length_default();
791        rlp_header(payload_length).encode(out);
792        self.rlp_encode_fields_default(out);
793    }
794
795    fn length(&self) -> usize {
796        let payload_length = self.rlp_encoded_fields_length_default();
797        rlp_header(payload_length).length_with_payload()
798    }
799}
800
801impl Decodable for TempoTransaction {
802    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
803        let header = alloy_rlp::Header::decode(buf)?;
804        if !header.list {
805            return Err(alloy_rlp::Error::UnexpectedString);
806        }
807        let remaining = buf.len();
808
809        if header.payload_length > remaining {
810            return Err(alloy_rlp::Error::InputTooShort);
811        }
812
813        let mut fields_buf = &buf[..header.payload_length];
814        let this = Self::rlp_decode_fields(&mut fields_buf)?;
815
816        if !fields_buf.is_empty() {
817            return Err(alloy_rlp::Error::UnexpectedLength);
818        }
819        buf.advance(header.payload_length);
820
821        Ok(this)
822    }
823}
824
825// Custom Arbitrary implementation to ensure calls is never empty and CREATE validation passes
826#[cfg(any(test, feature = "arbitrary"))]
827impl<'a> arbitrary::Arbitrary<'a> for TempoTransaction {
828    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
829        // Generate all fields using the default Arbitrary implementation
830        let chain_id = u.arbitrary()?;
831        let fee_token = u.arbitrary()?;
832        let max_priority_fee_per_gas = u.arbitrary()?;
833        let max_fee_per_gas = u.arbitrary()?;
834        let gas_limit = u.arbitrary()?;
835
836        // Generate calls - ensure at least one call is present and CREATE validation passes
837        // CREATE must be first (if present) and only one CREATE allowed
838        let mut calls: Vec<Call> = u.arbitrary()?;
839        if calls.is_empty() {
840            calls.push(Call {
841                to: u.arbitrary()?,
842                value: u.arbitrary()?,
843                input: u.arbitrary()?,
844            });
845        }
846
847        // Filter out CREATEs from non-first positions and ensure only one CREATE (if any)
848        let first_is_create = calls.first().map(|c| c.to.is_create()).unwrap_or(false);
849        if first_is_create {
850            // Keep the first CREATE, remove all other CREATEs
851            for call in calls.iter_mut().skip(1) {
852                if call.to.is_create() {
853                    // Replace with a CALL to a random address
854                    call.to = TxKind::Call(u.arbitrary()?);
855                }
856            }
857        } else {
858            // Remove all CREATEs (they would be at non-first positions)
859            for call in &mut calls {
860                if call.to.is_create() {
861                    call.to = TxKind::Call(u.arbitrary()?);
862                }
863            }
864        }
865
866        let access_list = u.arbitrary()?;
867
868        // For now, always set nonce_key to 0 (protocol nonce) to pass validation
869        let nonce_key = U256::ZERO;
870        let nonce = u.arbitrary()?;
871        let fee_payer_signature = u.arbitrary()?;
872
873        // Ensure valid_before > valid_after if both are set.
874        let valid_after: Option<NonZeroU64> = u.arbitrary()?;
875        let valid_before: Option<NonZeroU64> = match valid_after {
876            Some(after) => {
877                // Generate a value greater than valid_after
878                let offset: u64 = u.int_in_range(1..=1000)?;
879                Some(NonZeroU64::new(after.get().saturating_add(offset)).unwrap())
880            }
881            None => u.arbitrary()?,
882        };
883
884        Ok(Self {
885            chain_id,
886            fee_token,
887            max_priority_fee_per_gas,
888            max_fee_per_gas,
889            gas_limit,
890            calls,
891            access_list,
892            nonce_key,
893            nonce,
894            fee_payer_signature,
895            valid_before,
896            valid_after,
897            key_authorization: u.arbitrary()?,
898            tempo_authorization_list: vec![],
899        })
900    }
901}
902
903#[cfg(feature = "serde")]
904mod serde_input {
905    //! Helper module for serializing and deserializing the `input` field of a [`Call`] as either `input` or `data` fields.
906
907    use alloc::borrow::Cow;
908
909    use super::*;
910    use serde::{Deserialize, Deserializer, Serialize, Serializer};
911
912    #[derive(Serialize, Deserialize)]
913    struct SerdeHelper<'a> {
914        input: Option<Cow<'a, Bytes>>,
915        data: Option<Cow<'a, Bytes>>,
916    }
917
918    pub(super) fn serialize<S>(input: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
919    where
920        S: Serializer,
921    {
922        SerdeHelper {
923            input: Some(Cow::Borrowed(input)),
924            data: None,
925        }
926        .serialize(serializer)
927    }
928
929    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
930    where
931        D: Deserializer<'de>,
932    {
933        let helper = SerdeHelper::deserialize(deserializer)?;
934        Ok(helper
935            .input
936            .or(helper.data)
937            .ok_or(serde::de::Error::missing_field(
938                "missing `input` or `data` field",
939            ))?
940            .into_owned())
941    }
942}
943
944#[cfg(test)]
945mod tests {
946    use super::*;
947    use crate::{
948        TempoTxEnvelope,
949        transaction::{
950            KeyAuthorization, TempoSignedAuthorization,
951            tt_signature::{
952                PrimitiveSignature, SIGNATURE_TYPE_P256, SIGNATURE_TYPE_WEBAUTHN, TempoSignature,
953                derive_p256_address,
954            },
955        },
956    };
957    use alloy_eips::{Decodable2718, Encodable2718, eip7702::Authorization};
958    use alloy_primitives::{Address, Bytes, Signature, TxKind, U256, address, bytes, hex};
959    use alloy_rlp::{Decodable, Encodable};
960
961    fn nz(value: u64) -> NonZeroU64 {
962        NonZeroU64::new(value).expect("test timestamp must be non-zero")
963    }
964
965    #[test]
966    fn test_tempo_transaction_validation() {
967        // Create a dummy call to satisfy validation
968        let dummy_call = Call {
969            to: TxKind::Create,
970            value: U256::ZERO,
971            input: Bytes::new(),
972        };
973
974        // Valid: valid_before > valid_after
975        let tx1 = TempoTransaction {
976            valid_before: Some(nz(100)),
977            valid_after: Some(nz(50)),
978            tempo_authorization_list: vec![],
979            calls: vec![dummy_call.clone()],
980            ..Default::default()
981        };
982        assert!(tx1.validate().is_ok());
983
984        // Invalid: valid_before <= valid_after
985        let tx2 = TempoTransaction {
986            valid_before: Some(nz(50)),
987            valid_after: Some(nz(100)),
988            tempo_authorization_list: vec![],
989            calls: vec![dummy_call.clone()],
990            ..Default::default()
991        };
992        assert!(tx2.validate().is_err());
993
994        // Invalid: valid_before == valid_after
995        let tx3 = TempoTransaction {
996            valid_before: Some(nz(100)),
997            valid_after: Some(nz(100)),
998            tempo_authorization_list: vec![],
999            calls: vec![dummy_call.clone()],
1000            ..Default::default()
1001        };
1002        assert!(tx3.validate().is_err());
1003
1004        // Valid: no valid_after
1005        let tx4 = TempoTransaction {
1006            valid_before: Some(nz(100)),
1007            valid_after: None,
1008            tempo_authorization_list: vec![],
1009            calls: vec![dummy_call],
1010            ..Default::default()
1011        };
1012        assert!(tx4.validate().is_ok());
1013
1014        // Invalid: empty calls
1015        let tx5 = TempoTransaction {
1016            ..Default::default()
1017        };
1018        assert!(tx5.validate().is_err());
1019    }
1020
1021    #[test]
1022    fn test_tx_type() {
1023        assert_eq!(TempoTransaction::tx_type(), 0x76);
1024        assert_eq!(TEMPO_TX_TYPE_ID, 0x76);
1025    }
1026
1027    #[test]
1028    fn test_signature_type_detection() {
1029        // Secp256k1 (detected by 65-byte length, no type identifier)
1030        let sig1_bytes = vec![0u8; SECP256K1_SIGNATURE_LENGTH];
1031        let sig1 = TempoSignature::from_bytes(&sig1_bytes).unwrap();
1032        assert_eq!(sig1.signature_type(), SignatureType::Secp256k1);
1033
1034        // P256
1035        let mut sig2_bytes = vec![SIGNATURE_TYPE_P256];
1036        sig2_bytes.extend_from_slice(&[0u8; P256_SIGNATURE_LENGTH]);
1037        let sig2 = TempoSignature::from_bytes(&sig2_bytes).unwrap();
1038        assert_eq!(sig2.signature_type(), SignatureType::P256);
1039
1040        // WebAuthn
1041        let mut sig3_bytes = vec![SIGNATURE_TYPE_WEBAUTHN];
1042        sig3_bytes.extend_from_slice(&[0u8; 200]);
1043        let sig3 = TempoSignature::from_bytes(&sig3_bytes).unwrap();
1044        assert_eq!(sig3.signature_type(), SignatureType::WebAuthn);
1045    }
1046
1047    #[test]
1048    fn test_rlp_roundtrip() {
1049        let call = Call {
1050            to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1051            value: U256::from(1000),
1052            input: Bytes::from(vec![1, 2, 3, 4]),
1053        };
1054
1055        let tx = TempoTransaction {
1056            chain_id: 1,
1057            fee_token: Some(address!("0000000000000000000000000000000000000001")),
1058            max_priority_fee_per_gas: 1000000000,
1059            max_fee_per_gas: 2000000000,
1060            gas_limit: 21000,
1061            calls: vec![call.clone()],
1062            access_list: Default::default(),
1063            nonce_key: U256::ZERO,
1064            nonce: 1,
1065            fee_payer_signature: Some(Signature::test_signature()),
1066            valid_before: Some(nz(1000000)),
1067            valid_after: Some(nz(500000)),
1068            key_authorization: None,
1069            tempo_authorization_list: vec![],
1070        };
1071
1072        // Encode
1073        let mut buf = Vec::new();
1074        tx.encode(&mut buf);
1075
1076        // Decode
1077        let decoded = TempoTransaction::decode(&mut buf.as_slice()).unwrap();
1078
1079        // Verify fields
1080        assert_eq!(decoded.chain_id, tx.chain_id);
1081        assert_eq!(decoded.fee_token, tx.fee_token);
1082        assert_eq!(
1083            decoded.max_priority_fee_per_gas,
1084            tx.max_priority_fee_per_gas
1085        );
1086        assert_eq!(decoded.max_fee_per_gas, tx.max_fee_per_gas);
1087        assert_eq!(decoded.gas_limit, tx.gas_limit);
1088        assert_eq!(decoded.calls.len(), 1);
1089        assert_eq!(decoded.calls[0].to, call.to);
1090        assert_eq!(decoded.calls[0].value, call.value);
1091        assert_eq!(decoded.calls[0].input, call.input);
1092        assert_eq!(decoded.nonce_key, tx.nonce_key);
1093        assert_eq!(decoded.nonce, tx.nonce);
1094        assert_eq!(decoded.valid_before, tx.valid_before);
1095        assert_eq!(decoded.valid_after, tx.valid_after);
1096        assert_eq!(decoded.fee_payer_signature, tx.fee_payer_signature);
1097    }
1098
1099    #[test]
1100    fn test_encode_for_fee_payer_service_uses_signature_placeholder_and_skips_fee_token() {
1101        let call = Call {
1102            to: TxKind::Call(Address::random()),
1103            value: U256::ZERO,
1104            input: Bytes::from(vec![1, 2, 3, 4]),
1105        };
1106
1107        let tx = TempoTransaction {
1108            chain_id: 1,
1109            fee_token: None,
1110            max_priority_fee_per_gas: 1000000000,
1111            max_fee_per_gas: 2000000000,
1112            gas_limit: 21000,
1113            calls: vec![call],
1114            access_list: Default::default(),
1115            nonce_key: U256::ZERO,
1116            nonce: 1,
1117            fee_payer_signature: None,
1118            valid_before: Some(nz(1000000)),
1119            valid_after: Some(nz(500000)),
1120            key_authorization: None,
1121            tempo_authorization_list: vec![],
1122        };
1123
1124        let mut service_encoded = Vec::new();
1125        tx.encode_for_fee_payer_service(&mut service_encoded);
1126
1127        assert_eq!(service_encoded[0], TEMPO_TX_TYPE_ID);
1128
1129        let mut signing_tx = tx.clone();
1130        signing_tx.fee_payer_signature = Some(Signature::new(U256::ZERO, U256::ZERO, false));
1131        let mut signing_encoded = Vec::new();
1132        signing_tx.encode_for_signing(&mut signing_encoded);
1133        assert_eq!(service_encoded, signing_encoded);
1134
1135        let mut tx_with_different_fee_token = tx;
1136        tx_with_different_fee_token.fee_token = Some(Address::random());
1137        let mut different_fee_token_encoded = Vec::new();
1138        tx_with_different_fee_token.encode_for_fee_payer_service(&mut different_fee_token_encoded);
1139        assert_eq!(service_encoded, different_fee_token_encoded);
1140    }
1141
1142    #[test]
1143    fn test_rlp_roundtrip_no_optional_fields() {
1144        let call = Call {
1145            to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1146            value: U256::from(1000),
1147            input: Bytes::new(),
1148        };
1149
1150        let tx = TempoTransaction {
1151            chain_id: 1,
1152            fee_token: None,
1153            max_priority_fee_per_gas: 1000000000,
1154            max_fee_per_gas: 2000000000,
1155            gas_limit: 21000,
1156            calls: vec![call],
1157            access_list: Default::default(),
1158            nonce_key: U256::ZERO,
1159            nonce: 1,
1160            fee_payer_signature: None,
1161            valid_before: Some(nz(1000)),
1162            valid_after: None,
1163            key_authorization: None,
1164            tempo_authorization_list: vec![],
1165        };
1166
1167        // Encode
1168        let mut buf = Vec::new();
1169        tx.encode(&mut buf);
1170
1171        // Decode
1172        let decoded = TempoTransaction::decode(&mut buf.as_slice()).unwrap();
1173
1174        // Verify fields
1175        assert_eq!(decoded.chain_id, tx.chain_id);
1176        assert_eq!(decoded.fee_token, None);
1177        assert_eq!(decoded.fee_payer_signature, None);
1178        assert_eq!(decoded.valid_after, None);
1179        assert_eq!(decoded.calls.len(), 1);
1180    }
1181
1182    #[test]
1183    fn test_p256_address_derivation() {
1184        let pub_key_x =
1185            hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").into();
1186        let pub_key_y =
1187            hex!("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321").into();
1188
1189        let addr1 = derive_p256_address(&pub_key_x, &pub_key_y);
1190        let addr2 = derive_p256_address(&pub_key_x, &pub_key_y);
1191
1192        // Should be deterministic
1193        assert_eq!(addr1, addr2);
1194
1195        // Should not be zero address
1196        assert_ne!(addr1, Address::ZERO);
1197    }
1198
1199    #[test]
1200    fn test_nonce_system() {
1201        // Create a dummy call to satisfy validation
1202        let dummy_call = Call {
1203            to: TxKind::Create,
1204            value: U256::ZERO,
1205            input: Bytes::new(),
1206        };
1207
1208        // Test 1: Protocol nonce (key 0)
1209        let tx1 = TempoTransaction {
1210            nonce_key: U256::ZERO,
1211            nonce: 1,
1212            calls: vec![dummy_call.clone()],
1213            ..Default::default()
1214        };
1215        assert!(tx1.validate().is_ok());
1216        assert_eq!(tx1.nonce(), 1);
1217        assert_eq!(tx1.nonce_key, U256::ZERO);
1218
1219        // Test 2: User nonce (key 1, nonce 0) - first transaction in parallel sequence
1220        let tx2 = TempoTransaction {
1221            nonce_key: U256::from(1),
1222            nonce: 0,
1223            calls: vec![dummy_call.clone()],
1224            ..Default::default()
1225        };
1226        assert!(tx2.validate().is_ok());
1227        assert_eq!(tx2.nonce(), 0);
1228        assert_eq!(tx2.nonce_key, U256::from(1));
1229
1230        // Test 3: Different nonce key (key 42) - independent parallel sequence
1231        let tx3 = TempoTransaction {
1232            nonce_key: U256::from(42),
1233            nonce: 10,
1234            calls: vec![dummy_call.clone()],
1235            ..Default::default()
1236        };
1237        assert!(tx3.validate().is_ok());
1238        assert_eq!(tx3.nonce(), 10);
1239        assert_eq!(tx3.nonce_key, U256::from(42));
1240
1241        // Test 4: Verify nonce independence between different keys
1242        // Transactions with same nonce but different keys are independent
1243        let tx4a = TempoTransaction {
1244            nonce_key: U256::from(1),
1245            nonce: 100,
1246            calls: vec![dummy_call.clone()],
1247            ..Default::default()
1248        };
1249        let tx4b = TempoTransaction {
1250            nonce_key: U256::from(2),
1251            nonce: 100,
1252            calls: vec![dummy_call],
1253            ..Default::default()
1254        };
1255        assert!(tx4a.validate().is_ok());
1256        assert!(tx4b.validate().is_ok());
1257        assert_eq!(tx4a.nonce(), tx4b.nonce()); // Same nonce value
1258        assert_ne!(tx4a.nonce_key, tx4b.nonce_key); // Different keys = independent
1259    }
1260
1261    #[test]
1262    fn test_transaction_trait_impl() {
1263        let call = Call {
1264            to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1265            value: U256::from(1000),
1266            input: Bytes::new(),
1267        };
1268
1269        let tx = TempoTransaction {
1270            chain_id: 1,
1271            max_priority_fee_per_gas: 1000000000,
1272            max_fee_per_gas: 2000000000,
1273            gas_limit: 21000,
1274            calls: vec![call],
1275            ..Default::default()
1276        };
1277
1278        assert_eq!(tx.chain_id(), Some(1));
1279        assert_eq!(tx.gas_limit(), 21000);
1280        assert_eq!(tx.max_fee_per_gas(), 2000000000);
1281        assert_eq!(tx.max_priority_fee_per_gas(), Some(1000000000));
1282        assert_eq!(tx.value(), U256::from(1000));
1283        assert!(tx.is_dynamic_fee());
1284        assert!(!tx.is_create());
1285    }
1286
1287    #[test]
1288    fn test_effective_gas_price() {
1289        // Create a dummy call to satisfy validation
1290        let dummy_call = Call {
1291            to: TxKind::Create,
1292            value: U256::ZERO,
1293            input: Bytes::new(),
1294        };
1295
1296        let tx = TempoTransaction {
1297            max_priority_fee_per_gas: 1000000000,
1298            max_fee_per_gas: 2000000000,
1299            calls: vec![dummy_call],
1300            ..Default::default()
1301        };
1302
1303        // With base fee
1304        let effective1 = tx.effective_gas_price(Some(500000000));
1305        assert_eq!(effective1, 1500000000); // base_fee + max_priority_fee_per_gas
1306
1307        // Without base fee
1308        let effective2 = tx.effective_gas_price(None);
1309        assert_eq!(effective2, 2000000000); // max_fee_per_gas
1310    }
1311
1312    #[test]
1313    fn test_fee_payer_commits_to_fee_token() {
1314        // This test verifies that the fee payer signature commits to the fee_token value
1315        // i.e., changing fee_token changes the fee_payer_signature_hash
1316
1317        let sender = address!("0000000000000000000000000000000000000001");
1318        let token1 = address!("0000000000000000000000000000000000000002");
1319        let token2 = address!("0000000000000000000000000000000000000003");
1320
1321        let dummy_call = Call {
1322            to: TxKind::Create,
1323            value: U256::ZERO,
1324            input: Bytes::new(),
1325        };
1326
1327        // Transaction with fee_token = None
1328        let tx_no_token = TempoTransaction {
1329            chain_id: 1,
1330            fee_token: None,
1331            max_priority_fee_per_gas: 1000000000,
1332            max_fee_per_gas: 2000000000,
1333            gas_limit: 21000,
1334            calls: vec![dummy_call],
1335            nonce_key: U256::ZERO,
1336            nonce: 1,
1337            fee_payer_signature: Some(Signature::test_signature()),
1338            valid_before: Some(nz(1000)),
1339            valid_after: None,
1340            ..Default::default()
1341        };
1342
1343        // Transaction with fee_token = token1
1344        let tx_token1 = TempoTransaction {
1345            fee_token: Some(token1),
1346            ..tx_no_token.clone()
1347        };
1348
1349        // Transaction with fee_token = token2
1350        let tx_token2 = TempoTransaction {
1351            fee_token: Some(token2),
1352            ..tx_no_token.clone()
1353        };
1354
1355        // Calculate fee payer signature hashes
1356        let fee_payer_hash_no_token = tx_no_token.fee_payer_signature_hash(sender);
1357        let fee_payer_hash_token1 = tx_token1.fee_payer_signature_hash(sender);
1358        let fee_payer_hash_token2 = tx_token2.fee_payer_signature_hash(sender);
1359
1360        // All three fee payer hashes should be different (fee payer commits to fee_token)
1361        assert_ne!(
1362            fee_payer_hash_no_token, fee_payer_hash_token1,
1363            "Fee payer hash should change when fee_token changes from None to Some"
1364        );
1365        assert_ne!(
1366            fee_payer_hash_token1, fee_payer_hash_token2,
1367            "Fee payer hash should change when fee_token changes from token1 to token2"
1368        );
1369        assert_ne!(
1370            fee_payer_hash_no_token, fee_payer_hash_token2,
1371            "Fee payer hash should be different for None vs token2"
1372        );
1373
1374        // Calculate user signature hashes (what the sender signs)
1375        let user_hash_no_token = tx_no_token.signature_hash();
1376        let user_hash_token1 = tx_token1.signature_hash();
1377        let user_hash_token2 = tx_token2.signature_hash();
1378
1379        // All three user hashes should be THE SAME (user skips fee_token when fee_payer is present)
1380        assert_eq!(
1381            user_hash_no_token, user_hash_token1,
1382            "User hash should be the same regardless of fee_token (user skips fee_token)"
1383        );
1384        assert_eq!(
1385            user_hash_token1, user_hash_token2,
1386            "User hash should be the same regardless of fee_token (user skips fee_token)"
1387        );
1388        assert_eq!(
1389            user_hash_no_token, user_hash_token2,
1390            "User hash should be the same regardless of fee_token (user skips fee_token)"
1391        );
1392    }
1393
1394    #[test]
1395    fn test_fee_payer_signature_uses_magic_byte() {
1396        // Verify that fee payer signature hash uses the magic byte 0x78
1397
1398        let sender = address!("0000000000000000000000000000000000000001");
1399        let dummy_call = Call {
1400            to: TxKind::Create,
1401            value: U256::ZERO,
1402            input: Bytes::new(),
1403        };
1404
1405        let tx = TempoTransaction {
1406            chain_id: 1,
1407            fee_token: None,
1408            max_priority_fee_per_gas: 1000000000,
1409            max_fee_per_gas: 2000000000,
1410            gas_limit: 21000,
1411            calls: vec![dummy_call],
1412            nonce_key: U256::ZERO,
1413            nonce: 1,
1414            fee_payer_signature: Some(Signature::test_signature()),
1415            valid_before: Some(nz(1000)),
1416            ..Default::default()
1417        };
1418
1419        // The fee_payer_signature_hash should start with the magic byte
1420        // We can't directly inspect the hash construction, but we can verify it's different
1421        // from the sender signature hash which uses TEMPO_TX_TYPE_ID (0x76)
1422        let sender_hash = tx.signature_hash();
1423        let fee_payer_hash = tx.fee_payer_signature_hash(sender);
1424
1425        // These should be different because they use different type bytes
1426        assert_ne!(
1427            sender_hash, fee_payer_hash,
1428            "Sender and fee payer hashes should be different (different magic bytes)"
1429        );
1430    }
1431
1432    #[test]
1433    fn test_user_signature_without_fee_payer() {
1434        // Test that user signature hash INCLUDES fee_token when fee_payer is NOT present
1435
1436        let token1 = address!("0000000000000000000000000000000000000002");
1437        let token2 = address!("0000000000000000000000000000000000000003");
1438
1439        let dummy_call = Call {
1440            to: TxKind::Create,
1441            value: U256::ZERO,
1442            input: Bytes::new(),
1443        };
1444
1445        // Transaction WITHOUT fee_payer, fee_token = None
1446        let tx_no_payer_no_token = TempoTransaction {
1447            chain_id: 1,
1448            fee_token: None,
1449            max_priority_fee_per_gas: 1000000000,
1450            max_fee_per_gas: 2000000000,
1451            gas_limit: 21000,
1452            calls: vec![dummy_call],
1453            nonce_key: U256::ZERO,
1454            nonce: 1,
1455            fee_payer_signature: None, // No fee payer
1456            valid_before: Some(nz(1000)),
1457            valid_after: None,
1458            tempo_authorization_list: vec![],
1459            access_list: Default::default(),
1460            key_authorization: None,
1461        };
1462
1463        // Transaction WITHOUT fee_payer, fee_token = token1
1464        let tx_no_payer_token1 = TempoTransaction {
1465            fee_token: Some(token1),
1466            ..tx_no_payer_no_token.clone()
1467        };
1468
1469        // Transaction WITHOUT fee_payer, fee_token = token2
1470        let tx_no_payer_token2 = TempoTransaction {
1471            fee_token: Some(token2),
1472            ..tx_no_payer_no_token.clone()
1473        };
1474
1475        // Calculate user signature hashes
1476        let hash_no_token = tx_no_payer_no_token.signature_hash();
1477        let hash_token1 = tx_no_payer_token1.signature_hash();
1478        let hash_token2 = tx_no_payer_token2.signature_hash();
1479
1480        // All three hashes should be DIFFERENT (user includes fee_token when no fee_payer)
1481        assert_ne!(
1482            hash_no_token, hash_token1,
1483            "User hash should change when fee_token changes (no fee_payer)"
1484        );
1485        assert_ne!(
1486            hash_token1, hash_token2,
1487            "User hash should change when fee_token changes (no fee_payer)"
1488        );
1489        assert_ne!(
1490            hash_no_token, hash_token2,
1491            "User hash should change when fee_token changes (no fee_payer)"
1492        );
1493    }
1494
1495    #[test]
1496    fn test_rlp_encoding_includes_fee_token() {
1497        // Test that RLP encoding always includes fee_token in the encoded data
1498
1499        let token = address!("0000000000000000000000000000000000000002");
1500
1501        let dummy_call = Call {
1502            to: TxKind::Create,
1503            value: U256::ZERO,
1504            input: Bytes::new(),
1505        };
1506
1507        // Transaction with fee_token
1508        let tx_with_token = TempoTransaction {
1509            chain_id: 1,
1510            fee_token: Some(token),
1511            max_priority_fee_per_gas: 1000000000,
1512            max_fee_per_gas: 2000000000,
1513            gas_limit: 21000,
1514            calls: vec![dummy_call],
1515            nonce_key: U256::ZERO,
1516            nonce: 1,
1517            fee_payer_signature: Some(Signature::test_signature()),
1518            valid_before: Some(nz(1000)),
1519            valid_after: None,
1520            tempo_authorization_list: vec![],
1521            access_list: Default::default(),
1522            key_authorization: None,
1523        };
1524
1525        // Transaction without fee_token
1526        let tx_without_token = TempoTransaction {
1527            fee_token: None,
1528            ..tx_with_token.clone()
1529        };
1530
1531        // Encode both transactions
1532        let mut buf_with = Vec::new();
1533        tx_with_token.encode(&mut buf_with);
1534
1535        let mut buf_without = Vec::new();
1536        tx_without_token.encode(&mut buf_without);
1537
1538        // The encoded bytes should be different lengths
1539        assert_ne!(
1540            buf_with.len(),
1541            buf_without.len(),
1542            "RLP encoding should include fee_token in the encoded data"
1543        );
1544
1545        // The one with token should be longer (20 bytes for address vs 1 byte for empty)
1546        assert!(
1547            buf_with.len() > buf_without.len(),
1548            "Transaction with fee_token should have longer encoding"
1549        );
1550
1551        // Decode and verify
1552        let decoded_with = TempoTransaction::decode(&mut buf_with.as_slice()).unwrap();
1553        let decoded_without = TempoTransaction::decode(&mut buf_without.as_slice()).unwrap();
1554
1555        assert_eq!(decoded_with.fee_token, Some(token));
1556        assert_eq!(decoded_without.fee_token, None);
1557    }
1558
1559    #[test]
1560    fn test_signature_hash_behavior_with_and_without_fee_payer() {
1561        // Comprehensive test showing all signature hash behaviors
1562
1563        let token = address!("0000000000000000000000000000000000000002");
1564
1565        let dummy_call = Call {
1566            to: TxKind::Create,
1567            value: U256::ZERO,
1568            input: Bytes::new(),
1569        };
1570
1571        // Scenario 1: No fee payer, no token
1572        let tx_no_payer_no_token = TempoTransaction {
1573            chain_id: 1,
1574            fee_token: None,
1575            max_priority_fee_per_gas: 1000000000,
1576            max_fee_per_gas: 2000000000,
1577            gas_limit: 21000,
1578            calls: vec![dummy_call],
1579            nonce_key: U256::ZERO,
1580            nonce: 1,
1581            fee_payer_signature: None,
1582            valid_before: Some(nz(1000)),
1583            valid_after: None,
1584            tempo_authorization_list: vec![],
1585            access_list: Default::default(),
1586            key_authorization: None,
1587        };
1588
1589        // Scenario 2: No fee payer, with token
1590        let tx_no_payer_with_token = TempoTransaction {
1591            fee_token: Some(token),
1592            ..tx_no_payer_no_token.clone()
1593        };
1594
1595        // Scenario 3: With fee payer, no token
1596        let tx_with_payer_no_token = TempoTransaction {
1597            fee_payer_signature: Some(Signature::test_signature()),
1598            ..tx_no_payer_no_token.clone()
1599        };
1600
1601        // Scenario 4: With fee payer, with token
1602        let tx_with_payer_with_token = TempoTransaction {
1603            fee_token: Some(token),
1604            fee_payer_signature: Some(Signature::test_signature()),
1605            ..tx_no_payer_no_token.clone()
1606        };
1607
1608        // Calculate user signature hashes
1609        let hash1 = tx_no_payer_no_token.signature_hash();
1610        let hash2 = tx_no_payer_with_token.signature_hash();
1611        let hash3 = tx_with_payer_no_token.signature_hash();
1612        let hash4 = tx_with_payer_with_token.signature_hash();
1613
1614        // Without fee_payer: user includes fee_token, so hash1 != hash2
1615        assert_ne!(
1616            hash1, hash2,
1617            "User hash changes with fee_token when no fee_payer"
1618        );
1619
1620        // With fee_payer: user skips fee_token, so hash3 == hash4
1621        assert_eq!(
1622            hash3, hash4,
1623            "User hash ignores fee_token when fee_payer is present"
1624        );
1625
1626        // Hashes without fee_payer should differ from hashes with fee_payer
1627        // (because skip_fee_token logic changes)
1628        assert_ne!(hash1, hash3, "User hash changes when fee_payer is added");
1629    }
1630
1631    #[test]
1632    fn test_backwards_compatibility_key_authorization() {
1633        // Test that transactions without key_authorization are backwards compatible
1634        // and that the RLP encoding doesn't include any extra bytes for None
1635
1636        let call = Call {
1637            to: TxKind::Call(address!("0000000000000000000000000000000000000002")),
1638            value: U256::from(1000),
1639            input: Bytes::from(vec![1, 2, 3, 4]),
1640        };
1641
1642        // Create transaction WITHOUT key_authorization (old format)
1643        let tx_without = TempoTransaction {
1644            chain_id: 1,
1645            fee_token: Some(address!("0000000000000000000000000000000000000001")),
1646            max_priority_fee_per_gas: 1000000000,
1647            max_fee_per_gas: 2000000000,
1648            gas_limit: 21000,
1649            calls: vec![call],
1650            access_list: Default::default(),
1651            nonce_key: U256::ZERO,
1652            nonce: 1,
1653            fee_payer_signature: Some(Signature::test_signature()),
1654            valid_before: Some(nz(1000000)),
1655            valid_after: Some(nz(500000)),
1656            key_authorization: None, // No key authorization
1657            tempo_authorization_list: vec![],
1658        };
1659
1660        // Encode the transaction
1661        let mut buf_without = Vec::new();
1662        tx_without.encode(&mut buf_without);
1663
1664        // Decode it back
1665        let decoded_without = TempoTransaction::decode(&mut buf_without.as_slice()).unwrap();
1666
1667        // Verify it matches
1668        assert_eq!(decoded_without.key_authorization, None);
1669        assert_eq!(decoded_without.chain_id, tx_without.chain_id);
1670        assert_eq!(decoded_without.calls.len(), tx_without.calls.len());
1671
1672        // Create transaction WITH key_authorization (new format)
1673        let key_auth = KeyAuthorization::unrestricted(
1674            1,
1675            SignatureType::Secp256k1,
1676            address!("0000000000000000000000000000000000000004"),
1677        )
1678        .with_expiry(1234567890)
1679        .with_limits(vec![crate::transaction::TokenLimit {
1680            token: address!("0000000000000000000000000000000000000003"),
1681            limit: U256::from(10000),
1682            period: 0,
1683        }])
1684        .into_signed(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1685
1686        let tx_with = TempoTransaction {
1687            key_authorization: Some(key_auth.clone()),
1688            ..tx_without.clone()
1689        };
1690
1691        // Encode the transaction
1692        let mut buf_with = Vec::new();
1693        tx_with.encode(&mut buf_with);
1694
1695        // Decode it back
1696        let decoded_with = TempoTransaction::decode(&mut buf_with.as_slice()).unwrap();
1697
1698        // Verify the key_authorization is preserved
1699        assert!(decoded_with.key_authorization.is_some());
1700        let decoded_key_auth = decoded_with.key_authorization.unwrap();
1701        assert_eq!(decoded_key_auth.key_type, key_auth.key_type);
1702        assert_eq!(decoded_key_auth.expiry, key_auth.expiry);
1703        assert_eq!(
1704            decoded_key_auth.limits.as_ref().map(|l| l.len()),
1705            key_auth.limits.as_ref().map(|l| l.len())
1706        );
1707        assert_eq!(decoded_key_auth.key_id, key_auth.key_id);
1708
1709        // Important: The encoded transaction WITHOUT key_authorization should be shorter
1710        // This proves we're not encoding empty bytes for None
1711        assert!(
1712            buf_without.len() < buf_with.len(),
1713            "Transaction without key_authorization should have shorter encoding"
1714        );
1715
1716        // Test that an old decoder (simulated by truncating at the right position)
1717        // can still decode a transaction without key_authorization
1718        // This simulates backwards compatibility with old code that doesn't know about key_authorization
1719        let decoded_old_format = TempoTransaction::decode(&mut buf_without.as_slice()).unwrap();
1720        assert_eq!(decoded_old_format.key_authorization, None);
1721    }
1722
1723    #[test]
1724    fn test_aa_signed_rlp_direct() {
1725        // Simple test for AASigned RLP encoding/decoding without key_authorization
1726        let call = Call {
1727            to: TxKind::Create,
1728            value: U256::ZERO,
1729            input: Bytes::new(),
1730        };
1731
1732        let tx = TempoTransaction {
1733            chain_id: 0,
1734            fee_token: None,
1735            max_priority_fee_per_gas: 0,
1736            max_fee_per_gas: 0,
1737            gas_limit: 0,
1738            calls: vec![call],
1739            access_list: Default::default(),
1740            nonce_key: U256::ZERO,
1741            nonce: 0,
1742            fee_payer_signature: None,
1743            valid_before: None,
1744            valid_after: None,
1745            key_authorization: None, // No key_authorization
1746            tempo_authorization_list: vec![],
1747        };
1748
1749        let signature =
1750            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1751        let signed = AASigned::new_unhashed(tx, signature);
1752
1753        // Test direct RLP encoding/decoding
1754        let mut buf = Vec::new();
1755        signed.rlp_encode(&mut buf);
1756
1757        // Decode
1758        let decoded =
1759            AASigned::rlp_decode(&mut buf.as_slice()).expect("Should decode AASigned RLP");
1760        assert_eq!(decoded.tx().key_authorization, None);
1761    }
1762
1763    #[test]
1764    fn test_tempo_transaction_envelope_roundtrip_without_key_auth() {
1765        // Test that TempoTransaction in envelope works without key_authorization
1766        let call = Call {
1767            to: TxKind::Create,
1768            value: U256::ZERO,
1769            input: Bytes::new(),
1770        };
1771
1772        let tx = TempoTransaction {
1773            chain_id: 0,
1774            fee_token: None,
1775            max_priority_fee_per_gas: 0,
1776            max_fee_per_gas: 0,
1777            gas_limit: 0,
1778            calls: vec![call],
1779            access_list: Default::default(),
1780            nonce_key: U256::ZERO,
1781            nonce: 0,
1782            fee_payer_signature: None,
1783            valid_before: None,
1784            valid_after: None,
1785            key_authorization: None, // No key_authorization
1786            tempo_authorization_list: vec![],
1787        };
1788
1789        let signature =
1790            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1791        let signed = AASigned::new_unhashed(tx, signature);
1792        let envelope = TempoTxEnvelope::AA(signed);
1793
1794        // Encode and decode the envelope
1795        let mut buf = Vec::new();
1796        envelope.encode_2718(&mut buf);
1797        let decoded = TempoTxEnvelope::decode_2718(&mut buf.as_slice())
1798            .expect("Should decode envelope successfully");
1799
1800        // Verify it's the same
1801        if let TempoTxEnvelope::AA(aa_signed) = decoded {
1802            assert_eq!(aa_signed.tx().key_authorization, None);
1803            assert_eq!(aa_signed.tx().calls.len(), 1);
1804            assert_eq!(aa_signed.tx().chain_id, 0);
1805        } else {
1806            panic!("Expected AA envelope");
1807        }
1808    }
1809
1810    #[test]
1811    fn test_call_decode_rejects_malformed_rlp() {
1812        // Test that Call decoding rejects RLP with mismatched header length
1813        let call = Call {
1814            to: TxKind::Call(Address::random()),
1815            value: U256::random(),
1816            input: Bytes::from(vec![1, 2, 3, 4]),
1817        };
1818
1819        // Encode the call normally
1820        let mut buf = Vec::new();
1821        call.encode(&mut buf);
1822
1823        // Corrupt the header to claim less payload than actually encoded
1824        // This simulates the case where header.payload_length doesn't match actual consumed bytes
1825        let original_len = buf.len();
1826        buf.truncate(original_len - 2); // Remove 2 bytes from the end
1827
1828        let result = Call::decode(&mut buf.as_slice());
1829        assert!(
1830            result.is_err(),
1831            "Decoding should fail when header length doesn't match"
1832        );
1833        // The error could be InputTooShort or UnexpectedLength depending on what field is truncated
1834        assert!(matches!(
1835            result.unwrap_err(),
1836            alloy_rlp::Error::InputTooShort | alloy_rlp::Error::UnexpectedLength
1837        ));
1838    }
1839
1840    #[test]
1841    fn test_tempo_transaction_decode_rejects_malformed_rlp() {
1842        // Test that TempoTransaction decoding rejects RLP with mismatched header length
1843        let call = Call {
1844            to: TxKind::Call(Address::random()),
1845            value: U256::random(),
1846            input: Bytes::from(vec![1, 2, 3, 4]),
1847        };
1848
1849        let tx = TempoTransaction {
1850            chain_id: 1,
1851            fee_token: Some(Address::random()),
1852            max_priority_fee_per_gas: 1000000000,
1853            max_fee_per_gas: 2000000000,
1854            gas_limit: 21000,
1855            calls: vec![call],
1856            access_list: Default::default(),
1857            nonce_key: U256::ZERO,
1858            nonce: 1,
1859            fee_payer_signature: Some(Signature::test_signature()),
1860            valid_before: Some(nz(1000000)),
1861            valid_after: Some(nz(500000)),
1862            key_authorization: None,
1863            tempo_authorization_list: vec![],
1864        };
1865
1866        // Encode the transaction normally
1867        let mut buf = Vec::new();
1868        tx.encode(&mut buf);
1869
1870        // Corrupt by truncating - simulates header claiming more bytes than available
1871        let original_len = buf.len();
1872        buf.truncate(original_len - 5); // Remove 5 bytes from the end
1873
1874        let result = TempoTransaction::decode(&mut buf.as_slice());
1875        assert!(
1876            result.is_err(),
1877            "Decoding should fail when data is truncated"
1878        );
1879        // The error could be InputTooShort or UnexpectedLength depending on what field is truncated
1880        assert!(matches!(
1881            result.unwrap_err(),
1882            alloy_rlp::Error::InputTooShort | alloy_rlp::Error::UnexpectedLength
1883        ));
1884    }
1885
1886    #[test]
1887    #[cfg(feature = "serde")]
1888    fn call_serde() {
1889        let call: Call = serde_json::from_str(
1890            r#"{"to":"0x0000000000000000000000000000000000000002","value":"0x1","input":"0x1234"}"#,
1891        )
1892        .unwrap();
1893        assert_eq!(
1894            call.to,
1895            TxKind::Call(address!("0000000000000000000000000000000000000002"))
1896        );
1897        assert_eq!(call.value, U256::ONE);
1898        assert_eq!(call.input, bytes!("0x1234"));
1899    }
1900
1901    #[test]
1902    fn test_create_must_be_first_call() {
1903        let create_call = Call {
1904            to: TxKind::Create,
1905            value: U256::ZERO,
1906            input: Bytes::new(),
1907        };
1908        let call_call = Call {
1909            to: TxKind::Call(Address::random()),
1910            value: U256::ZERO,
1911            input: Bytes::new(),
1912        };
1913
1914        // Valid: CREATE as first call
1915        let tx_valid = TempoTransaction {
1916            calls: vec![create_call.clone(), call_call.clone()],
1917            ..Default::default()
1918        };
1919        assert!(tx_valid.validate().is_ok());
1920
1921        // Invalid: CREATE as second call
1922        let tx_invalid = TempoTransaction {
1923            calls: vec![call_call, create_call],
1924            ..Default::default()
1925        };
1926        assert!(tx_invalid.validate().is_err());
1927        assert!(tx_invalid.validate().unwrap_err().contains("first call"));
1928    }
1929
1930    #[test]
1931    fn test_only_one_create_allowed() {
1932        let create_call = Call {
1933            to: TxKind::Create,
1934            value: U256::ZERO,
1935            input: Bytes::new(),
1936        };
1937
1938        // Valid: Single CREATE
1939        let tx_valid = TempoTransaction {
1940            calls: vec![create_call.clone()],
1941            ..Default::default()
1942        };
1943        assert!(tx_valid.validate().is_ok());
1944
1945        // Invalid: Multiple CREATEs (both at first position, second one triggers error)
1946        let tx_invalid = TempoTransaction {
1947            calls: vec![create_call.clone(), create_call],
1948            ..Default::default()
1949        };
1950        assert!(tx_invalid.validate().is_err());
1951        assert!(
1952            tx_invalid
1953                .validate()
1954                .unwrap_err()
1955                .contains("only one CREATE")
1956        );
1957    }
1958
1959    #[test]
1960    fn test_create_forbidden_with_auth_list() {
1961        let create_call = Call {
1962            to: TxKind::Create,
1963            value: U256::ZERO,
1964            input: Bytes::new(),
1965        };
1966
1967        let signed_auth = TempoSignedAuthorization::new_unchecked(
1968            Authorization {
1969                chain_id: U256::ONE,
1970                address: Address::random(),
1971                nonce: 1,
1972            },
1973            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1974        );
1975
1976        // Invalid: CREATE call with auth list
1977        let tx = TempoTransaction {
1978            calls: vec![create_call],
1979            tempo_authorization_list: vec![signed_auth],
1980            ..Default::default()
1981        };
1982
1983        let result = tx.validate();
1984        assert!(result.is_err());
1985        assert!(result.unwrap_err().contains("aa_authorization_list"));
1986    }
1987
1988    #[test]
1989    fn test_create_validation_allows_call_only_batch() {
1990        // A batch with only CALL operations should be valid
1991        let call1 = Call {
1992            to: TxKind::Call(Address::random()),
1993            value: U256::ZERO,
1994            input: Bytes::new(),
1995        };
1996        let call2 = Call {
1997            to: TxKind::Call(Address::random()),
1998            value: U256::random(),
1999            input: Bytes::from(vec![1, 2, 3]),
2000        };
2001
2002        let tx = TempoTransaction {
2003            calls: vec![call1, call2],
2004            ..Default::default()
2005        };
2006        assert!(tx.validate().is_ok());
2007    }
2008
2009    #[test]
2010    fn test_value_saturates_on_overflow() {
2011        let call1 = Call {
2012            to: TxKind::Call(Address::ZERO),
2013            value: U256::MAX,
2014            input: Bytes::new(),
2015        };
2016        let call2 = Call {
2017            to: TxKind::Call(Address::ZERO),
2018            value: U256::from(1),
2019            input: Bytes::new(),
2020        };
2021
2022        let tx = TempoTransaction {
2023            calls: vec![call1, call2],
2024            ..Default::default()
2025        };
2026
2027        assert_eq!(tx.value(), U256::MAX);
2028    }
2029
2030    #[test]
2031    fn test_validate_does_not_check_expiring_nonce_constraints() {
2032        let dummy_call = Call {
2033            to: TxKind::Call(Address::ZERO),
2034            value: U256::ZERO,
2035            input: Bytes::new(),
2036        };
2037
2038        // Transaction with expiring nonce key but nonce != 0 should pass validate()
2039        // (expiring nonce constraints are hardfork-dependent and checked elsewhere)
2040        let tx_with_nonzero_nonce = TempoTransaction {
2041            nonce_key: TEMPO_EXPIRING_NONCE_KEY,
2042            nonce: 42,
2043            valid_before: None,
2044            calls: vec![dummy_call.clone()],
2045            ..Default::default()
2046        };
2047        assert!(
2048            tx_with_nonzero_nonce.validate().is_ok(),
2049            "validate() should not enforce expiring nonce constraints (hardfork-dependent)"
2050        );
2051
2052        // Transaction with expiring nonce key but no valid_before should pass validate()
2053        let tx_without_valid_before = TempoTransaction {
2054            nonce_key: TEMPO_EXPIRING_NONCE_KEY,
2055            nonce: 0,
2056            valid_before: None,
2057            calls: vec![dummy_call.clone()],
2058            ..Default::default()
2059        };
2060        assert!(
2061            tx_without_valid_before.validate().is_ok(),
2062            "validate() should not enforce expiring nonce constraints (hardfork-dependent)"
2063        );
2064
2065        // Sanity check: a fully valid expiring nonce tx should also pass
2066        let valid_expiring_tx = TempoTransaction {
2067            nonce_key: TEMPO_EXPIRING_NONCE_KEY,
2068            nonce: 0,
2069            valid_before: Some(nz(1000)),
2070            calls: vec![dummy_call],
2071            ..Default::default()
2072        };
2073        assert!(valid_expiring_tx.validate().is_ok());
2074    }
2075}
2076
2077#[cfg(all(test, feature = "reth-codec"))]
2078mod compact_tests {
2079    use super::*;
2080    use crate::transaction::{
2081        KeyAuthorization, TempoSignedAuthorization, TokenLimit,
2082        tt_signature::{P256SignatureWithPreHash, PrimitiveSignature, TempoSignature},
2083    };
2084    use alloy_eips::{eip2930::AccessListItem, eip7702::Authorization};
2085    use alloy_primitives::{Signature, U256, address, b256, bytes, hex};
2086    use reth_codecs::Compact;
2087
2088    /// Ensures backwards compatibility of compact bitflags.
2089    ///
2090    /// See reth's `HeaderExt` pattern:
2091    /// <https://github.com/paradigmxyz/reth-core/blob/0476d1bc4b71f3c3b080622be297edd91ee4e70c/crates/codecs/src/alloy/header.rs>
2092    #[test]
2093    fn compact_types_have_unused_bits() {
2094        assert_ne!(
2095            TempoTransaction::bitflag_unused_bits(),
2096            0,
2097            "TempoTransaction"
2098        );
2099        assert_ne!(Call::bitflag_unused_bits(), 0, "Call");
2100    }
2101
2102    #[test]
2103    fn call_compact_roundtrip() {
2104        let call = Call {
2105            to: TxKind::Call(address!("0x0000000000000000000000000000000000000001")),
2106            value: U256::from(1000u64),
2107            input: bytes!("deadbeef"),
2108        };
2109
2110        let expected = hex!("05000000000000000000000000000000000000000103e8deadbeef");
2111
2112        let mut buf = vec![];
2113        let len = call.to_compact(&mut buf);
2114        assert_eq!(buf, expected, "Call compact encoding changed");
2115        assert_eq!(len, expected.len());
2116
2117        let (decoded, _) = Call::from_compact(&expected, expected.len());
2118        assert_eq!(decoded, call);
2119    }
2120
2121    #[test]
2122    fn tempo_transaction_compact_roundtrip() {
2123        let tx = TempoTransaction {
2124            chain_id: 42170,
2125            fee_token: Some(address!("0x0000000000000000000000000000000000000abc")),
2126            max_priority_fee_per_gas: 1_000_000_000,
2127            max_fee_per_gas: 50_000_000_000,
2128            gas_limit: 21000,
2129            calls: vec![
2130                Call {
2131                    to: TxKind::Call(address!("0x0000000000000000000000000000000000000001")),
2132                    value: U256::from(1000u64),
2133                    input: bytes!("cafe"),
2134                },
2135                Call {
2136                    to: TxKind::Create,
2137                    value: U256::ZERO,
2138                    input: bytes!("6080604052"),
2139                },
2140            ],
2141            access_list: AccessList(vec![AccessListItem {
2142                address: address!("0x0000000000000000000000000000000000000001"),
2143                storage_keys: vec![B256::ZERO],
2144            }]),
2145            nonce_key: U256::from(7u64),
2146            nonce: 42,
2147            fee_payer_signature: Some(Signature::new(U256::from(1u64), U256::from(2u64), false)),
2148            valid_before: Some(NonZeroU64::new(1_700_001_000).unwrap()),
2149            valid_after: Some(NonZeroU64::new(1_700_000_000).unwrap()),
2150            key_authorization: Some(
2151                KeyAuthorization {
2152                    chain_id: 42170,
2153                    key_type: SignatureType::P256,
2154                    key_id: address!("0x000000000000000000000000000000000000dead"),
2155                    expiry: Some(core::num::NonZeroU64::new(1_700_100_000).unwrap()),
2156                    limits: Some(vec![TokenLimit {
2157                        token: address!("0x0000000000000000000000000000000000000042"),
2158                        limit: U256::from(1_000_000u64),
2159                        period: 86400,
2160                    }]),
2161                    allowed_calls: None,
2162                    witness: None,
2163                    is_admin: false,
2164                    account: None,
2165                }
2166                .into_signed(PrimitiveSignature::P256(P256SignatureWithPreHash {
2167                    r: b256!("0x1111111111111111111111111111111111111111111111111111111111111111"),
2168                    s: b256!("0x2222222222222222222222222222222222222222222222222222222222222222"),
2169                    pub_key_x: b256!(
2170                        "0x3333333333333333333333333333333333333333333333333333333333333333"
2171                    ),
2172                    pub_key_y: b256!(
2173                        "0x4444444444444444444444444444444444444444444444444444444444444444"
2174                    ),
2175                    pre_hash: false,
2176                })),
2177            ),
2178            tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
2179                Authorization {
2180                    chain_id: U256::from(42170u64),
2181                    address: address!("0x0000000000000000000000000000000000000099"),
2182                    nonce: 1,
2183                },
2184                TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::new(
2185                    U256::from(3u64),
2186                    U256::from(4u64),
2187                    true,
2188                ))),
2189            )],
2190        };
2191
2192        let expected = hex!(
2193            "921409e201a4ba0000000000000000000000000000000000000abc3b9aca000ba43b74005208021905000000000000000000000000000000000000000103e8cafe0600608060405201350000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000000000000000000072a0001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000046553f4e8046553f100c501f8c3f83d82a4ba0194000000000000000000000000000000000000dead84655577a0dedd940000000000000000000000000000000000000042830f424083015180b88201111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444400015ef85c82a4ba94000000000000000000000000000000000000009901b841000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000041c"
2194        );
2195
2196        let mut buf = vec![];
2197        let len = tx.to_compact(&mut buf);
2198        assert_eq!(buf, expected, "TempoTransaction compact encoding changed");
2199        assert_eq!(len, expected.len());
2200
2201        let (decoded, _) = TempoTransaction::from_compact(&expected, expected.len());
2202        assert_eq!(decoded, tx);
2203    }
2204
2205    #[test]
2206    fn signature_type_compact_roundtrip() {
2207        for (variant, expected_byte) in [
2208            (SignatureType::Secp256k1, 0x00u8),
2209            (SignatureType::P256, 0x01),
2210            (SignatureType::WebAuthn, 0x02),
2211        ] {
2212            let mut buf = vec![];
2213            let len = variant.to_compact(&mut buf);
2214            assert_eq!(
2215                buf,
2216                [expected_byte],
2217                "SignatureType::{variant:?} compact encoding changed"
2218            );
2219            assert_eq!(len, 1);
2220
2221            let (decoded, _) = SignatureType::from_compact(&[expected_byte], 1);
2222            assert_eq!(decoded, variant);
2223        }
2224    }
2225}