tempo_primitives/transaction/
tempo_transaction.rs

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