alloy_consensus/transaction/
legacy.rs

1use crate::{
2    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
3    SignableTransaction, Signed, Transaction, TxType,
4};
5use alloc::vec::Vec;
6use alloy_eips::{
7    eip2718::IsTyped2718, eip2930::AccessList, eip7702::SignedAuthorization, Typed2718,
8};
9use alloy_primitives::{keccak256, Bytes, ChainId, Signature, TxKind, B256, U256};
10use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result};
11use core::mem;
12
13/// Legacy transaction.
14#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
15#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
18#[doc(alias = "LegacyTransaction", alias = "TransactionLegacy", alias = "LegacyTx")]
19pub struct TxLegacy {
20    /// Added as EIP-155: Simple replay attack protection
21    #[cfg_attr(
22        feature = "serde",
23        serde(
24            default,
25            with = "alloy_serde::quantity::opt",
26            skip_serializing_if = "Option::is_none",
27        )
28    )]
29    pub chain_id: Option<ChainId>,
30    /// A scalar value equal to the number of transactions sent by the sender; formally Tn.
31    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
32    pub nonce: u64,
33    /// A scalar value equal to the number of
34    /// Wei to be paid per unit of gas for all computation
35    /// costs incurred as a result of the execution of this transaction; formally Tp.
36    ///
37    /// As ethereum circulation is around 120mil eth as of 2022 that is around
38    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
39    /// 340282366920938463463374607431768211455
40    ///
41    /// However, this is unfortunately not true for all chains at all points in time.
42    /// See <https://github.com/alloy-rs/alloy/issues/2842> for example.
43    /// We saturate to `u128::MAX` if the value is too large instead of failing deserialization.
44    #[cfg_attr(feature = "serde", serde(with = "gas_price"))]
45    pub gas_price: u128,
46    /// A scalar value equal to the maximum
47    /// amount of gas that should be used in executing
48    /// this transaction. This is paid up-front, before any
49    /// computation is done and may not be increased
50    /// later; formally Tg.
51    #[cfg_attr(
52        feature = "serde",
53        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
54    )]
55    pub gas_limit: u64,
56    /// The 160-bit address of the message call’s recipient or, for a contract creation
57    /// transaction, ∅, used here to denote the only member of B0 ; formally Tt.
58    #[cfg_attr(feature = "serde", serde(default))]
59    pub to: TxKind,
60    /// A scalar value equal to the number of Wei to
61    /// be transferred to the message call’s recipient or,
62    /// in the case of contract creation, as an endowment
63    /// to the newly created account; formally Tv.
64    pub value: U256,
65    /// Input has two uses depending if `to` field is Create or Call.
66    /// pub init: An unlimited size byte array specifying the
67    /// EVM-code for the account initialisation procedure CREATE,
68    /// data: An unlimited size byte array specifying the
69    /// input data of the message call, formally Td.
70    pub input: Bytes,
71}
72
73impl TxLegacy {
74    /// The EIP-2718 transaction type.
75    pub const TX_TYPE: isize = 0;
76
77    /// Calculates a heuristic for the in-memory size of the [TxLegacy] transaction.
78    #[inline]
79    pub fn size(&self) -> usize {
80        mem::size_of::<Option<ChainId>>() + // chain_id
81        mem::size_of::<u64>() + // nonce
82        mem::size_of::<u128>() + // gas_price
83        mem::size_of::<u64>() + // gas_limit
84        self.to.size() + // to
85        mem::size_of::<U256>() + // value
86        self.input.len() // input
87    }
88
89    /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy
90    /// transactions.
91    pub(crate) fn eip155_fields_len(&self) -> usize {
92        self.chain_id.map_or(
93            // this is either a pre-EIP-155 legacy transaction or a typed transaction
94            0,
95            // EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain
96            // ID to get the length of all 3 fields
97            // len(chain_id) + (0x00) + (0x00)
98            |id| id.length() + 2,
99        )
100    }
101
102    /// Encodes EIP-155 arguments into the desired buffer. Only encodes values
103    /// for legacy transactions.
104    pub(crate) fn encode_eip155_signing_fields(&self, out: &mut dyn BufMut) {
105        // if this is a legacy transaction without a chain ID, it must be pre-EIP-155
106        // and does not need to encode the chain ID for the signature hash encoding
107        if let Some(id) = self.chain_id {
108            // EIP-155 encodes the chain ID and two zeroes
109            id.encode(out);
110            0x00u8.encode(out);
111            0x00u8.encode(out);
112        }
113    }
114}
115
116// Legacy transaction network and 2718 encodings are identical to the RLP
117// encoding.
118impl RlpEcdsaEncodableTx for TxLegacy {
119    fn rlp_encoded_fields_length(&self) -> usize {
120        self.nonce.length()
121            + self.gas_price.length()
122            + self.gas_limit.length()
123            + self.to.length()
124            + self.value.length()
125            + self.input.0.length()
126    }
127
128    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
129        self.nonce.encode(out);
130        self.gas_price.encode(out);
131        self.gas_limit.encode(out);
132        self.to.encode(out);
133        self.value.encode(out);
134        self.input.0.encode(out);
135    }
136
137    fn rlp_header_signed(&self, signature: &Signature) -> Header {
138        let payload_length = self.rlp_encoded_fields_length()
139            + signature.rlp_rs_len()
140            + to_eip155_value(signature.v(), self.chain_id).length();
141        Header { list: true, payload_length }
142    }
143
144    fn rlp_encoded_length_with_signature(&self, signature: &Signature) -> usize {
145        // Enforce correct parity for legacy transactions (EIP-155, 27 or 28).
146        self.rlp_header_signed(signature).length_with_payload()
147    }
148
149    fn rlp_encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) {
150        // Enforce correct parity for legacy transactions (EIP-155, 27 or 28).
151        self.rlp_header_signed(signature).encode(out);
152        self.rlp_encode_fields(out);
153        signature.write_rlp_vrs(out, to_eip155_value(signature.v(), self.chain_id));
154    }
155
156    fn eip2718_encoded_length(&self, signature: &Signature) -> usize {
157        self.rlp_encoded_length_with_signature(signature)
158    }
159
160    fn eip2718_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
161        self.rlp_encode_signed(signature, out);
162    }
163
164    fn network_header(&self, signature: &Signature) -> Header {
165        self.rlp_header_signed(signature)
166    }
167
168    fn network_encoded_length(&self, signature: &Signature) -> usize {
169        self.rlp_encoded_length_with_signature(signature)
170    }
171
172    fn network_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
173        self.rlp_encode_signed(signature, out);
174    }
175
176    fn tx_hash_with_type(&self, signature: &Signature, _ty: u8) -> alloy_primitives::TxHash {
177        let mut buf = Vec::with_capacity(self.rlp_encoded_length_with_signature(signature));
178        self.rlp_encode_signed(signature, &mut buf);
179        keccak256(&buf)
180    }
181}
182
183impl RlpEcdsaDecodableTx for TxLegacy {
184    const DEFAULT_TX_TYPE: u8 = { Self::TX_TYPE as u8 };
185
186    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
187        Ok(Self {
188            nonce: Decodable::decode(buf)?,
189            gas_price: Decodable::decode(buf)?,
190            gas_limit: Decodable::decode(buf)?,
191            to: Decodable::decode(buf)?,
192            value: Decodable::decode(buf)?,
193            input: Decodable::decode(buf)?,
194            chain_id: None,
195        })
196    }
197
198    fn rlp_decode_with_signature(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Signature)> {
199        let header = Header::decode(buf)?;
200        if !header.list {
201            return Err(alloy_rlp::Error::UnexpectedString);
202        }
203
204        let remaining = buf.len();
205        let mut tx = Self::rlp_decode_fields(buf)?;
206        let signature = Signature::decode_rlp_vrs(buf, |buf| {
207            let value = Decodable::decode(buf)?;
208            let (parity, chain_id) =
209                from_eip155_value(value).ok_or(alloy_rlp::Error::Custom("invalid parity value"))?;
210            tx.chain_id = chain_id;
211            Ok(parity)
212        })?;
213
214        if buf.len() + header.payload_length != remaining {
215            return Err(alloy_rlp::Error::ListLengthMismatch {
216                expected: header.payload_length,
217                got: remaining - buf.len(),
218            });
219        }
220
221        Ok((tx, signature))
222    }
223
224    fn eip2718_decode_with_type(
225        buf: &mut &[u8],
226        _ty: u8,
227    ) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
228        Self::rlp_decode_signed(buf).map_err(Into::into)
229    }
230
231    fn eip2718_decode(buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
232        Self::rlp_decode_signed(buf).map_err(Into::into)
233    }
234
235    fn network_decode_with_type(
236        buf: &mut &[u8],
237        _ty: u8,
238    ) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
239        Self::rlp_decode_signed(buf).map_err(Into::into)
240    }
241}
242
243impl Transaction for TxLegacy {
244    #[inline]
245    fn chain_id(&self) -> Option<ChainId> {
246        self.chain_id
247    }
248
249    #[inline]
250    fn nonce(&self) -> u64 {
251        self.nonce
252    }
253
254    #[inline]
255    fn gas_limit(&self) -> u64 {
256        self.gas_limit
257    }
258
259    #[inline]
260    fn gas_price(&self) -> Option<u128> {
261        Some(self.gas_price)
262    }
263
264    #[inline]
265    fn max_fee_per_gas(&self) -> u128 {
266        self.gas_price
267    }
268
269    #[inline]
270    fn max_priority_fee_per_gas(&self) -> Option<u128> {
271        None
272    }
273
274    #[inline]
275    fn max_fee_per_blob_gas(&self) -> Option<u128> {
276        None
277    }
278
279    #[inline]
280    fn priority_fee_or_price(&self) -> u128 {
281        self.gas_price
282    }
283
284    fn effective_gas_price(&self, _base_fee: Option<u64>) -> u128 {
285        self.gas_price
286    }
287
288    #[inline]
289    fn is_dynamic_fee(&self) -> bool {
290        false
291    }
292
293    #[inline]
294    fn kind(&self) -> TxKind {
295        self.to
296    }
297
298    #[inline]
299    fn is_create(&self) -> bool {
300        self.to.is_create()
301    }
302
303    #[inline]
304    fn value(&self) -> U256 {
305        self.value
306    }
307
308    #[inline]
309    fn input(&self) -> &Bytes {
310        &self.input
311    }
312
313    #[inline]
314    fn access_list(&self) -> Option<&AccessList> {
315        None
316    }
317
318    #[inline]
319    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
320        None
321    }
322
323    #[inline]
324    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
325        None
326    }
327}
328
329impl SignableTransaction<Signature> for TxLegacy {
330    fn set_chain_id(&mut self, chain_id: ChainId) {
331        self.chain_id = Some(chain_id);
332    }
333
334    fn encode_for_signing(&self, out: &mut dyn BufMut) {
335        Header {
336            list: true,
337            payload_length: self.rlp_encoded_fields_length() + self.eip155_fields_len(),
338        }
339        .encode(out);
340        self.rlp_encode_fields(out);
341        self.encode_eip155_signing_fields(out);
342    }
343
344    fn payload_len_for_signature(&self) -> usize {
345        let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
346        // 'header length' + 'payload length'
347        Header { list: true, payload_length }.length_with_payload()
348    }
349}
350
351impl Typed2718 for TxLegacy {
352    fn ty(&self) -> u8 {
353        TxType::Legacy as u8
354    }
355}
356
357impl IsTyped2718 for TxLegacy {
358    fn is_type(type_id: u8) -> bool {
359        matches!(type_id, 0x00)
360    }
361}
362
363impl Encodable for TxLegacy {
364    fn encode(&self, out: &mut dyn BufMut) {
365        self.encode_for_signing(out)
366    }
367
368    fn length(&self) -> usize {
369        let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
370        // 'header length' + 'payload length'
371        length_of_length(payload_length) + payload_length
372    }
373}
374
375impl Decodable for TxLegacy {
376    fn decode(data: &mut &[u8]) -> Result<Self> {
377        let header = Header::decode(data)?;
378        let remaining_len = data.len();
379
380        let transaction_payload_len = header.payload_length;
381
382        if transaction_payload_len > remaining_len {
383            return Err(alloy_rlp::Error::InputTooShort);
384        }
385
386        let mut transaction = Self::rlp_decode_fields(data)?;
387
388        // If we still have data, it should be an eip-155 encoded chain_id
389        if !data.is_empty() {
390            transaction.chain_id = Some(Decodable::decode(data)?);
391            let _: U256 = Decodable::decode(data)?; // r
392            let _: U256 = Decodable::decode(data)?; // s
393        }
394
395        let decoded = remaining_len - data.len();
396        if decoded != transaction_payload_len {
397            return Err(alloy_rlp::Error::UnexpectedLength);
398        }
399
400        Ok(transaction)
401    }
402}
403
404/// Helper for encoding `y_parity` boolean and optional `chain_id` into EIP-155 `v` value.
405pub const fn to_eip155_value(y_parity: bool, chain_id: Option<ChainId>) -> u128 {
406    match chain_id {
407        Some(id) => 35 + id as u128 * 2 + y_parity as u128,
408        None => 27 + y_parity as u128,
409    }
410}
411
412/// Helper for decoding EIP-155 `v` value into `y_parity` boolean and optional `chain_id`.
413pub const fn from_eip155_value(value: u128) -> Option<(bool, Option<ChainId>)> {
414    match value {
415        27 => Some((false, None)),
416        28 => Some((true, None)),
417        v @ 35.. => {
418            let y_parity = ((v - 35) % 2) != 0;
419            let chain_id = (v - 35) / 2;
420
421            if chain_id > u64::MAX as u128 {
422                return None;
423            }
424            Some((y_parity, Some(chain_id as u64)))
425        }
426        _ => None,
427    }
428}
429
430#[cfg(feature = "serde")]
431mod gas_price {
432    use alloy_primitives::U256;
433    use serde::{Deserialize, Deserializer};
434
435    pub(super) use alloy_serde::quantity::serialize;
436
437    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
438    where
439        D: Deserializer<'de>,
440    {
441        U256::deserialize(deserializer).map(|x| x.saturating_to())
442    }
443
444    #[test]
445    fn test_gas_price() {
446        #[derive(Debug, PartialEq, Eq, serde::Serialize, Deserialize)]
447        struct Value {
448            #[serde(with = "self")]
449            inner: u128,
450        }
451
452        let json = r#"{"inner":"0x3e8"}"#;
453        let value = serde_json::from_str::<Value>(json).unwrap();
454        assert_eq!(value.inner, 1000);
455
456        let json =
457            r#"{"inner":"0x30783134626639633464372e3333333333333333333333333333333333333333"}"#;
458        let value = serde_json::from_str::<Value>(json).unwrap();
459        assert_eq!(value.inner, u128::MAX);
460    }
461}
462
463#[cfg(feature = "serde")]
464pub mod signed_legacy_serde {
465    //! Helper module for encoding signatures of transactions wrapped into [`Signed`] in legacy
466    //! format.
467    //!
468    //! By default, signatures are encoded as a single boolean under `yParity` key. However, for
469    //! legacy transactions parity byte is encoded as `v` key respecting EIP-155 format.
470    use super::*;
471    use alloc::borrow::Cow;
472    use alloy_primitives::U128;
473    use serde::{Deserialize, Serialize};
474
475    struct LegacySignature {
476        r: U256,
477        s: U256,
478        v: U128,
479    }
480
481    #[derive(Serialize, Deserialize)]
482    struct HumanReadableRepr {
483        r: U256,
484        s: U256,
485        v: U128,
486    }
487
488    #[derive(Serialize, Deserialize)]
489    #[serde(transparent)]
490    struct NonHumanReadableRepr((U256, U256, U128));
491
492    impl Serialize for LegacySignature {
493        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
494        where
495            S: serde::Serializer,
496        {
497            if serializer.is_human_readable() {
498                HumanReadableRepr { r: self.r, s: self.s, v: self.v }.serialize(serializer)
499            } else {
500                NonHumanReadableRepr((self.r, self.s, self.v)).serialize(serializer)
501            }
502        }
503    }
504
505    impl<'de> Deserialize<'de> for LegacySignature {
506        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
507        where
508            D: serde::Deserializer<'de>,
509        {
510            if deserializer.is_human_readable() {
511                HumanReadableRepr::deserialize(deserializer).map(|repr| Self {
512                    r: repr.r,
513                    s: repr.s,
514                    v: repr.v,
515                })
516            } else {
517                NonHumanReadableRepr::deserialize(deserializer).map(|repr| Self {
518                    r: repr.0 .0,
519                    s: repr.0 .1,
520                    v: repr.0 .2,
521                })
522            }
523        }
524    }
525
526    #[derive(Serialize, Deserialize)]
527    struct SignedLegacy<'a> {
528        #[serde(flatten)]
529        tx: Cow<'a, TxLegacy>,
530        #[serde(flatten)]
531        signature: LegacySignature,
532        hash: B256,
533    }
534
535    /// Serializes signed transaction with `v` key for signature parity.
536    pub fn serialize<S>(signed: &crate::Signed<TxLegacy>, serializer: S) -> Result<S::Ok, S::Error>
537    where
538        S: serde::Serializer,
539    {
540        SignedLegacy {
541            tx: Cow::Borrowed(signed.tx()),
542            signature: LegacySignature {
543                v: U128::from(to_eip155_value(signed.signature().v(), signed.tx().chain_id())),
544                r: signed.signature().r(),
545                s: signed.signature().s(),
546            },
547            hash: *signed.hash(),
548        }
549        .serialize(serializer)
550    }
551
552    /// Deserializes signed transaction expecting `v` key for signature parity.
553    pub fn deserialize<'de, D>(deserializer: D) -> Result<crate::Signed<TxLegacy>, D::Error>
554    where
555        D: serde::Deserializer<'de>,
556    {
557        let SignedLegacy { mut tx, signature, hash } = SignedLegacy::deserialize(deserializer)?;
558
559        // Optimism pre-Bedrock (and some other L2s) injected system transactions into the chain
560        // where the signature fields (v, r, s) are all zero.
561        // These transactions do not have a valid ECDSA signature, but are valid on-chain.
562        // See: https://github.com/alloy-rs/alloy/issues/2348
563        //
564        // Here, we detect (v=0, r=0, s=0) and treat them as system transactions,
565        // bypassing EIP-155 signature validation.
566        let is_fake_system_signature =
567            signature.r.is_zero() && signature.s.is_zero() && signature.v.is_zero();
568
569        let signature = if is_fake_system_signature {
570            Signature::new(U256::ZERO, U256::ZERO, false)
571        } else {
572            let (parity, chain_id) = from_eip155_value(signature.v.to()).ok_or_else(|| {
573                serde::de::Error::custom("invalid EIP-155 signature parity value")
574            })?;
575
576            // Note: some implementations always set the chain id in the response, so we only check
577            // if they differ if both are set.
578            if let Some((tx_chain_id, chain_id)) = tx.chain_id().zip(chain_id) {
579                if tx_chain_id != chain_id {
580                    return Err(serde::de::Error::custom("chain id mismatch"));
581                }
582            }
583
584            // update the chain id from decoding the eip155 value
585            tx.to_mut().chain_id = chain_id;
586
587            Signature::new(signature.r, signature.s, parity)
588        };
589        Ok(Signed::new_unchecked(tx.into_owned(), signature, hash))
590    }
591}
592
593#[cfg(feature = "serde")]
594pub mod untagged_legacy_serde {
595    //! Helper module for deserializing legacy transactions and ensuring that the `type` field is
596    //! not present.
597    //!
598    //! This is expected to be used as a fallback for deserializing legacy transactions without a
599    //! tag, and is needed to make sure that unknown transaction variants are explicitly rejected
600    //! instead of being treated as legacy transactions.
601
602    use super::*;
603    use serde::Deserialize;
604
605    #[derive(Deserialize)]
606    pub(crate) struct UntaggedLegacy {
607        #[serde(default, rename = "type", deserialize_with = "alloy_serde::reject_if_some")]
608        _ty: Option<()>,
609        #[serde(flatten, with = "crate::transaction::signed_legacy_serde")]
610        tx: Signed<TxLegacy>,
611    }
612
613    /// Deserializes a legacy transaction without a tag.
614    pub fn deserialize<'de, D>(deserializer: D) -> Result<Signed<TxLegacy>, D::Error>
615    where
616        D: serde::Deserializer<'de>,
617    {
618        UntaggedLegacy::deserialize(deserializer).map(|tx| tx.tx)
619    }
620}
621
622/// Bincode-compatible [`TxLegacy`] serde implementation.
623#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
624pub(super) mod serde_bincode_compat {
625    use alloc::borrow::Cow;
626    use alloy_primitives::{Bytes, ChainId, TxKind, U256};
627    use serde::{Deserialize, Deserializer, Serialize, Serializer};
628    use serde_with::{DeserializeAs, SerializeAs};
629
630    /// Bincode-compatible [`super::TxLegacy`] serde implementation.
631    ///
632    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
633    /// ```rust
634    /// use alloy_consensus::{serde_bincode_compat, TxLegacy};
635    /// use serde::{Deserialize, Serialize};
636    /// use serde_with::serde_as;
637    ///
638    /// #[serde_as]
639    /// #[derive(Serialize, Deserialize)]
640    /// struct Data {
641    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxLegacy")]
642    ///     header: TxLegacy,
643    /// }
644    /// ```
645    #[derive(Debug, Serialize, Deserialize)]
646    pub struct TxLegacy<'a> {
647        #[serde(default, with = "alloy_serde::quantity::opt")]
648        chain_id: Option<ChainId>,
649        nonce: u64,
650        gas_price: u128,
651        gas_limit: u64,
652        #[serde(default)]
653        to: TxKind,
654        value: U256,
655        input: Cow<'a, Bytes>,
656    }
657
658    impl<'a> From<&'a super::TxLegacy> for TxLegacy<'a> {
659        fn from(value: &'a super::TxLegacy) -> Self {
660            Self {
661                chain_id: value.chain_id,
662                nonce: value.nonce,
663                gas_price: value.gas_price,
664                gas_limit: value.gas_limit,
665                to: value.to,
666                value: value.value,
667                input: Cow::Borrowed(&value.input),
668            }
669        }
670    }
671
672    impl<'a> From<TxLegacy<'a>> for super::TxLegacy {
673        fn from(value: TxLegacy<'a>) -> Self {
674            Self {
675                chain_id: value.chain_id,
676                nonce: value.nonce,
677                gas_price: value.gas_price,
678                gas_limit: value.gas_limit,
679                to: value.to,
680                value: value.value,
681                input: value.input.into_owned(),
682            }
683        }
684    }
685
686    impl SerializeAs<super::TxLegacy> for TxLegacy<'_> {
687        fn serialize_as<S>(source: &super::TxLegacy, serializer: S) -> Result<S::Ok, S::Error>
688        where
689            S: Serializer,
690        {
691            TxLegacy::from(source).serialize(serializer)
692        }
693    }
694
695    impl<'de> DeserializeAs<'de, super::TxLegacy> for TxLegacy<'de> {
696        fn deserialize_as<D>(deserializer: D) -> Result<super::TxLegacy, D::Error>
697        where
698            D: Deserializer<'de>,
699        {
700            TxLegacy::deserialize(deserializer).map(Into::into)
701        }
702    }
703
704    #[cfg(test)]
705    mod tests {
706        use arbitrary::Arbitrary;
707        use bincode::config;
708        use rand::Rng;
709        use serde::{Deserialize, Serialize};
710        use serde_with::serde_as;
711
712        use super::super::{serde_bincode_compat, TxLegacy};
713
714        #[test]
715        fn test_tx_legacy_bincode_roundtrip() {
716            #[serde_as]
717            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
718            struct Data {
719                #[serde_as(as = "serde_bincode_compat::TxLegacy")]
720                transaction: TxLegacy,
721            }
722
723            let mut bytes = [0u8; 1024];
724            rand::thread_rng().fill(bytes.as_mut_slice());
725            let data = Data {
726                transaction: TxLegacy::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
727                    .unwrap(),
728            };
729
730            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
731            let (decoded, _) =
732                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
733            assert_eq!(decoded, data);
734        }
735    }
736}
737
738#[cfg(all(test, feature = "k256"))]
739mod tests {
740    use super::signed_legacy_serde;
741    use crate::{
742        transaction::{from_eip155_value, to_eip155_value},
743        SignableTransaction, TxLegacy,
744    };
745    use alloy_primitives::{address, b256, hex, Address, Signature, TxKind, B256, U256};
746
747    #[test]
748    fn recover_signer_legacy() {
749        let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into();
750        let hash: B256 =
751            hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into();
752
753        let tx = TxLegacy {
754            chain_id: Some(1),
755            nonce: 0x18,
756            gas_price: 0xfa56ea00,
757            gas_limit: 119902,
758            to: TxKind::Call(hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()),
759            value: U256::from(0x1c6bf526340000u64),
760            input:  hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(),
761        };
762
763        let sig = Signature::from_scalars_and_parity(
764            b256!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031"),
765            b256!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5"),
766            false,
767        );
768
769        let signed_tx = tx.into_signed(sig);
770
771        assert_eq!(*signed_tx.hash(), hash, "Expected same hash");
772        assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass.");
773    }
774
775    #[test]
776    // Test vector from https://github.com/alloy-rs/alloy/issues/125
777    fn decode_legacy_and_recover_signer() {
778        use crate::transaction::RlpEcdsaDecodableTx;
779        let raw_tx = alloy_primitives::bytes!("f9015482078b8505d21dba0083022ef1947a250d5630b4cf539739df2c5dacb4c659f2488d880c46549a521b13d8b8e47ff36ab50000000000000000000000000000000000000000000066ab5a608bd00a23f2fe000000000000000000000000000000000000000000000000000000000000008000000000000000000000000048c04ed5691981c42154c6167398f95e8f38a7ff00000000000000000000000000000000000000000000000000000000632ceac70000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006c6ee5e31d828de241282b9606c8e98ea48526e225a0c9077369501641a92ef7399ff81c21639ed4fd8fc69cb793cfa1dbfab342e10aa0615facb2f1bcf3274a354cfe384a38d0cc008a11c2dd23a69111bc6930ba27a8");
780
781        let tx = TxLegacy::rlp_decode_signed(&mut raw_tx.as_ref()).unwrap();
782
783        let recovered = tx.recover_signer().unwrap();
784        let expected = address!("a12e1462d0ceD572f396F58B6E2D03894cD7C8a4");
785
786        assert_eq!(tx.tx().chain_id, Some(1), "Expected same chain id");
787        assert_eq!(expected, recovered, "Expected same signer");
788    }
789
790    #[test]
791    fn eip155_roundtrip() {
792        assert_eq!(from_eip155_value(to_eip155_value(false, None)), Some((false, None)));
793        assert_eq!(from_eip155_value(to_eip155_value(true, None)), Some((true, None)));
794
795        for chain_id in [0, 1, 10, u64::MAX] {
796            assert_eq!(
797                from_eip155_value(to_eip155_value(false, Some(chain_id))),
798                Some((false, Some(chain_id)))
799            );
800            assert_eq!(
801                from_eip155_value(to_eip155_value(true, Some(chain_id))),
802                Some((true, Some(chain_id)))
803            );
804        }
805    }
806
807    #[test]
808    fn can_deserialize_system_transaction_with_zero_signature() {
809        let raw_tx = serde_json::json!({
810            "blockHash": "0x5307b5c812a067f8bc1ed1cc89d319ae6f9a0c9693848bd25c36b5191de60b85",
811            "blockNumber": "0x45a59bb",
812            "from": "0x0000000000000000000000000000000000000000",
813            "gas": "0x1e8480",
814            "gasPrice": "0x0",
815            "hash": "0x16ef68aa8f35add3a03167a12b5d1268e344f6605a64ecc3f1c3aa68e98e4e06",
816            "input": "0xcbd4ece900000000000000000000000032155c9d39084f040ba17890fe8134dbe2a0453f0000000000000000000000004a0126ee88018393b1ad2455060bc350ead9908a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000469f700000000000000000000000000000000000000000000000000000000000000644ff746f60000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002043e908a4e862aebb10e7e27db0b892b58a7e32af11d64387a414dabc327b00e200000000000000000000000000000000000000000000000000000000",
817            "nonce": "0x469f7",
818            "to": "0x4200000000000000000000000000000000000007",
819            "transactionIndex": "0x0",
820            "value": "0x0",
821            "v": "0x0",
822            "r": "0x0",
823            "s": "0x0",
824            "queueOrigin": "l1",
825            "l1TxOrigin": "0x36bde71c97b33cc4729cf772ae268934f7ab70b2",
826            "l1BlockNumber": "0xfd1a6c",
827            "l1Timestamp": "0x63e434ff",
828            "index": "0x45a59ba",
829            "queueIndex": "0x469f7",
830            "rawTransaction": "0xcbd4ece900000000000000000000000032155c9d39084f040ba17890fe8134dbe2a0453f0000000000000000000000004a0126ee88018393b1ad2455060bc350ead9908a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000469f700000000000000000000000000000000000000000000000000000000000000644ff746f60000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002043e908a4e862aebb10e7e27db0b892b58a7e32af11d64387a414dabc327b00e200000000000000000000000000000000000000000000000000000000"
831        });
832
833        let signed: crate::Signed<TxLegacy> = signed_legacy_serde::deserialize(raw_tx).unwrap();
834
835        assert_eq!(signed.signature().r(), U256::ZERO);
836        assert_eq!(signed.signature().s(), U256::ZERO);
837        assert!(!signed.signature().v());
838
839        assert_eq!(
840            signed.hash(),
841            &b256!("0x16ef68aa8f35add3a03167a12b5d1268e344f6605a64ecc3f1c3aa68e98e4e06"),
842            "hash should match the transaction hash"
843        );
844    }
845}