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