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