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