alloy_consensus/transaction/
legacy.rs

1use crate::{
2    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
3    SignableTransaction, Signed, Transaction, TxType,
4};
5use alloc::vec::Vec;
6use alloy_eips::{eip2930::AccessList, eip7702::SignedAuthorization, Typed2718};
7use alloy_primitives::{
8    keccak256, Bytes, ChainId, PrimitiveSignature as Signature, TxKind, B256, U256,
9};
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    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
41    pub gas_price: u128,
42    /// A scalar value equal to the maximum
43    /// amount of gas that should be used in executing
44    /// this transaction. This is paid up-front, before any
45    /// computation is done and may not be increased
46    /// later; formally Tg.
47    #[cfg_attr(
48        feature = "serde",
49        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
50    )]
51    pub gas_limit: u64,
52    /// The 160-bit address of the message call’s recipient or, for a contract creation
53    /// transaction, ∅, used here to denote the only member of B0 ; formally Tt.
54    #[cfg_attr(feature = "serde", serde(default))]
55    pub to: TxKind,
56    /// A scalar value equal to the number of Wei to
57    /// be transferred to the message call’s recipient or,
58    /// in the case of contract creation, as an endowment
59    /// to the newly created account; formally Tv.
60    pub value: U256,
61    /// Input has two uses depending if transaction is Create or Call (if `to` field is None or
62    /// Some). pub init: An unlimited size byte array specifying the
63    /// EVM-code for the account initialisation procedure CREATE,
64    /// data: An unlimited size byte array specifying the
65    /// input data of the message call, formally Td.
66    pub input: Bytes,
67}
68
69impl TxLegacy {
70    /// The EIP-2718 transaction type.
71    pub const TX_TYPE: isize = 0;
72
73    /// Calculates a heuristic for the in-memory size of the [TxLegacy] transaction.
74    #[inline]
75    pub fn size(&self) -> usize {
76        mem::size_of::<Option<ChainId>>() + // chain_id
77        mem::size_of::<u64>() + // nonce
78        mem::size_of::<u128>() + // gas_price
79        mem::size_of::<u64>() + // gas_limit
80        self.to.size() + // to
81        mem::size_of::<U256>() + // value
82        self.input.len() // input
83    }
84
85    /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy
86    /// transactions.
87    pub(crate) fn eip155_fields_len(&self) -> usize {
88        self.chain_id.map_or(
89            // this is either a pre-EIP-155 legacy transaction or a typed transaction
90            0,
91            // EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain
92            // ID to get the length of all 3 fields
93            // len(chain_id) + (0x00) + (0x00)
94            |id| id.length() + 2,
95        )
96    }
97
98    /// Encodes EIP-155 arguments into the desired buffer. Only encodes values
99    /// for legacy transactions.
100    pub(crate) fn encode_eip155_signing_fields(&self, out: &mut dyn BufMut) {
101        // if this is a legacy transaction without a chain ID, it must be pre-EIP-155
102        // and does not need to encode the chain ID for the signature hash encoding
103        if let Some(id) = self.chain_id {
104            // EIP-155 encodes the chain ID and two zeroes
105            id.encode(out);
106            0x00u8.encode(out);
107            0x00u8.encode(out);
108        }
109    }
110}
111
112// Legacy transaction network and 2718 encodings are identical to the RLP
113// encoding.
114impl RlpEcdsaEncodableTx for TxLegacy {
115    const DEFAULT_TX_TYPE: u8 = { Self::TX_TYPE as u8 };
116
117    fn rlp_encoded_fields_length(&self) -> usize {
118        self.nonce.length()
119            + self.gas_price.length()
120            + self.gas_limit.length()
121            + self.to.length()
122            + self.value.length()
123            + self.input.0.length()
124    }
125
126    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
127        self.nonce.encode(out);
128        self.gas_price.encode(out);
129        self.gas_limit.encode(out);
130        self.to.encode(out);
131        self.value.encode(out);
132        self.input.0.encode(out);
133    }
134
135    fn rlp_header_signed(&self, signature: &Signature) -> Header {
136        let payload_length = self.rlp_encoded_fields_length()
137            + signature.rlp_rs_len()
138            + to_eip155_value(signature.v(), self.chain_id).length();
139        Header { list: true, payload_length }
140    }
141
142    fn rlp_encoded_length_with_signature(&self, signature: &Signature) -> usize {
143        // Enforce correct parity for legacy transactions (EIP-155, 27 or 28).
144        self.rlp_header_signed(signature).length_with_payload()
145    }
146
147    fn rlp_encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) {
148        // Enforce correct parity for legacy transactions (EIP-155, 27 or 28).
149        self.rlp_header_signed(signature).encode(out);
150        self.rlp_encode_fields(out);
151        signature.write_rlp_vrs(out, to_eip155_value(signature.v(), self.chain_id));
152    }
153
154    fn eip2718_encoded_length(&self, signature: &Signature) -> usize {
155        self.rlp_encoded_length_with_signature(signature)
156    }
157
158    fn eip2718_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
159        self.rlp_encode_signed(signature, out);
160    }
161
162    fn network_header(&self, signature: &Signature) -> Header {
163        self.rlp_header_signed(signature)
164    }
165
166    fn network_encoded_length(&self, signature: &Signature) -> usize {
167        self.rlp_encoded_length_with_signature(signature)
168    }
169
170    fn network_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
171        self.rlp_encode_signed(signature, out);
172    }
173
174    fn tx_hash_with_type(&self, signature: &Signature, _ty: u8) -> alloy_primitives::TxHash {
175        let mut buf = Vec::with_capacity(self.rlp_encoded_length_with_signature(signature));
176        self.rlp_encode_signed(signature, &mut buf);
177        keccak256(&buf)
178    }
179}
180
181impl RlpEcdsaDecodableTx for TxLegacy {
182    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
183        Ok(Self {
184            nonce: Decodable::decode(buf)?,
185            gas_price: Decodable::decode(buf)?,
186            gas_limit: Decodable::decode(buf)?,
187            to: Decodable::decode(buf)?,
188            value: Decodable::decode(buf)?,
189            input: Decodable::decode(buf)?,
190            chain_id: None,
191        })
192    }
193
194    fn rlp_decode_with_signature(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Signature)> {
195        let header = Header::decode(buf)?;
196        if !header.list {
197            return Err(alloy_rlp::Error::UnexpectedString);
198        }
199
200        let remaining = buf.len();
201        let mut tx = Self::rlp_decode_fields(buf)?;
202        let signature = Signature::decode_rlp_vrs(buf, |buf| {
203            let value = Decodable::decode(buf)?;
204            let (parity, chain_id) =
205                from_eip155_value(value).ok_or(alloy_rlp::Error::Custom("invalid parity value"))?;
206            tx.chain_id = chain_id;
207            Ok(parity)
208        })?;
209
210        if buf.len() + header.payload_length != remaining {
211            return Err(alloy_rlp::Error::ListLengthMismatch {
212                expected: header.payload_length,
213                got: remaining - buf.len(),
214            });
215        }
216
217        Ok((tx, signature))
218    }
219
220    fn eip2718_decode_with_type(
221        buf: &mut &[u8],
222        _ty: u8,
223    ) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
224        Self::rlp_decode_signed(buf).map_err(Into::into)
225    }
226
227    fn eip2718_decode(buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
228        Self::rlp_decode_signed(buf).map_err(Into::into)
229    }
230
231    fn network_decode_with_type(
232        buf: &mut &[u8],
233        _ty: u8,
234    ) -> alloy_eips::eip2718::Eip2718Result<Signed<Self>> {
235        Self::rlp_decode_signed(buf).map_err(Into::into)
236    }
237}
238
239impl Transaction for TxLegacy {
240    #[inline]
241    fn chain_id(&self) -> Option<ChainId> {
242        self.chain_id
243    }
244
245    #[inline]
246    fn nonce(&self) -> u64 {
247        self.nonce
248    }
249
250    #[inline]
251    fn gas_limit(&self) -> u64 {
252        self.gas_limit
253    }
254
255    #[inline]
256    fn gas_price(&self) -> Option<u128> {
257        Some(self.gas_price)
258    }
259
260    #[inline]
261    fn max_fee_per_gas(&self) -> u128 {
262        self.gas_price
263    }
264
265    #[inline]
266    fn max_priority_fee_per_gas(&self) -> Option<u128> {
267        None
268    }
269
270    #[inline]
271    fn max_fee_per_blob_gas(&self) -> Option<u128> {
272        None
273    }
274
275    #[inline]
276    fn priority_fee_or_price(&self) -> u128 {
277        self.gas_price
278    }
279
280    fn effective_gas_price(&self, _base_fee: Option<u64>) -> u128 {
281        self.gas_price
282    }
283
284    #[inline]
285    fn is_dynamic_fee(&self) -> bool {
286        false
287    }
288
289    #[inline]
290    fn kind(&self) -> TxKind {
291        self.to
292    }
293
294    #[inline]
295    fn is_create(&self) -> bool {
296        self.to.is_create()
297    }
298
299    #[inline]
300    fn value(&self) -> U256 {
301        self.value
302    }
303
304    #[inline]
305    fn input(&self) -> &Bytes {
306        &self.input
307    }
308
309    #[inline]
310    fn access_list(&self) -> Option<&AccessList> {
311        None
312    }
313
314    #[inline]
315    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
316        None
317    }
318
319    #[inline]
320    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
321        None
322    }
323}
324
325impl SignableTransaction<Signature> for TxLegacy {
326    fn set_chain_id(&mut self, chain_id: ChainId) {
327        self.chain_id = Some(chain_id);
328    }
329
330    fn encode_for_signing(&self, out: &mut dyn BufMut) {
331        Header {
332            list: true,
333            payload_length: self.rlp_encoded_fields_length() + self.eip155_fields_len(),
334        }
335        .encode(out);
336        self.rlp_encode_fields(out);
337        self.encode_eip155_signing_fields(out);
338    }
339
340    fn payload_len_for_signature(&self) -> usize {
341        let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
342        // 'header length' + 'payload length'
343        Header { list: true, payload_length }.length_with_payload()
344    }
345}
346
347impl Typed2718 for TxLegacy {
348    fn ty(&self) -> u8 {
349        TxType::Legacy as u8
350    }
351}
352
353impl Encodable for TxLegacy {
354    fn encode(&self, out: &mut dyn BufMut) {
355        self.encode_for_signing(out)
356    }
357
358    fn length(&self) -> usize {
359        let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
360        // 'header length' + 'payload length'
361        length_of_length(payload_length) + payload_length
362    }
363}
364
365impl Decodable for TxLegacy {
366    fn decode(data: &mut &[u8]) -> Result<Self> {
367        let header = Header::decode(data)?;
368        let remaining_len = data.len();
369
370        let transaction_payload_len = header.payload_length;
371
372        if transaction_payload_len > remaining_len {
373            return Err(alloy_rlp::Error::InputTooShort);
374        }
375
376        let mut transaction = Self::rlp_decode_fields(data)?;
377
378        // If we still have data, it should be an eip-155 encoded chain_id
379        if !data.is_empty() {
380            transaction.chain_id = Some(Decodable::decode(data)?);
381            let _: U256 = Decodable::decode(data)?; // r
382            let _: U256 = Decodable::decode(data)?; // s
383        }
384
385        let decoded = remaining_len - data.len();
386        if decoded != transaction_payload_len {
387            return Err(alloy_rlp::Error::UnexpectedLength);
388        }
389
390        Ok(transaction)
391    }
392}
393
394/// Helper for encoding `y_parity` boolean and optional `chain_id` into EIP-155 `v` value.
395pub const fn to_eip155_value(y_parity: bool, chain_id: Option<ChainId>) -> u128 {
396    match chain_id {
397        Some(id) => 35 + id as u128 * 2 + y_parity as u128,
398        None => 27 + y_parity as u128,
399    }
400}
401
402/// Helper for decoding EIP-155 `v` value into `y_parity` boolean and optional `chain_id`.
403pub const fn from_eip155_value(value: u128) -> Option<(bool, Option<ChainId>)> {
404    match value {
405        27 => Some((false, None)),
406        28 => Some((true, None)),
407        v @ 35.. => {
408            let y_parity = ((v - 35) % 2) != 0;
409            let chain_id = (v - 35) / 2;
410
411            if chain_id > u64::MAX as u128 {
412                return None;
413            }
414            Some((y_parity, Some(chain_id as u64)))
415        }
416        _ => None,
417    }
418}
419
420#[cfg(feature = "serde")]
421pub mod signed_legacy_serde {
422    //! Helper module for encoding signatures of transactions wrapped into [`Signed`] in legacy
423    //! format.
424    //!
425    //! By default, signatures are encoded as a single boolean under `yParity` key. However, for
426    //! legacy transactions parity byte is encoded as `v` key respecting EIP-155 format.
427    use super::*;
428    use alloc::borrow::Cow;
429    use alloy_primitives::U128;
430    use serde::{Deserialize, Serialize};
431
432    struct LegacySignature {
433        r: U256,
434        s: U256,
435        v: U128,
436    }
437
438    #[derive(Serialize, Deserialize)]
439    struct HumanReadableRepr {
440        r: U256,
441        s: U256,
442        v: U128,
443    }
444
445    #[derive(Serialize, Deserialize)]
446    #[serde(transparent)]
447    struct NonHumanReadableRepr((U256, U256, U128));
448
449    impl Serialize for LegacySignature {
450        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
451        where
452            S: serde::Serializer,
453        {
454            if serializer.is_human_readable() {
455                HumanReadableRepr { r: self.r, s: self.s, v: self.v }.serialize(serializer)
456            } else {
457                NonHumanReadableRepr((self.r, self.s, self.v)).serialize(serializer)
458            }
459        }
460    }
461
462    impl<'de> Deserialize<'de> for LegacySignature {
463        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
464        where
465            D: serde::Deserializer<'de>,
466        {
467            if deserializer.is_human_readable() {
468                HumanReadableRepr::deserialize(deserializer).map(|repr| Self {
469                    r: repr.r,
470                    s: repr.s,
471                    v: repr.v,
472                })
473            } else {
474                NonHumanReadableRepr::deserialize(deserializer).map(|repr| Self {
475                    r: repr.0 .0,
476                    s: repr.0 .1,
477                    v: repr.0 .2,
478                })
479            }
480        }
481    }
482
483    #[derive(Serialize, Deserialize)]
484    struct SignedLegacy<'a> {
485        #[serde(flatten)]
486        tx: Cow<'a, TxLegacy>,
487        #[serde(flatten)]
488        signature: LegacySignature,
489        hash: B256,
490    }
491
492    /// Serializes signed transaction with `v` key for signature parity.
493    pub fn serialize<S>(signed: &crate::Signed<TxLegacy>, serializer: S) -> Result<S::Ok, S::Error>
494    where
495        S: serde::Serializer,
496    {
497        SignedLegacy {
498            tx: Cow::Borrowed(signed.tx()),
499            signature: LegacySignature {
500                v: U128::from(to_eip155_value(signed.signature().v(), signed.tx().chain_id())),
501                r: signed.signature().r(),
502                s: signed.signature().s(),
503            },
504            hash: *signed.hash(),
505        }
506        .serialize(serializer)
507    }
508
509    /// Deserializes signed transaction expecting `v` key for signature parity.
510    pub fn deserialize<'de, D>(deserializer: D) -> Result<crate::Signed<TxLegacy>, D::Error>
511    where
512        D: serde::Deserializer<'de>,
513    {
514        let SignedLegacy { tx, signature, hash } = SignedLegacy::deserialize(deserializer)?;
515        let (parity, chain_id) = from_eip155_value(signature.v.to())
516            .ok_or_else(|| serde::de::Error::custom("invalid EIP-155 signature parity value"))?;
517
518        // Note: some implementations always set the chain id in the response, so we only check if
519        // they differ if both are set.
520        if let Some((tx_chain_id, chain_id)) = tx.chain_id().zip(chain_id) {
521            if tx_chain_id != chain_id {
522                return Err(serde::de::Error::custom("chain id mismatch"));
523            }
524        }
525        let mut tx = tx.into_owned();
526        tx.chain_id = chain_id;
527        Ok(Signed::new_unchecked(tx, Signature::new(signature.r, signature.s, parity), hash))
528    }
529}
530
531/// Bincode-compatible [`TxLegacy`] serde implementation.
532#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
533pub(super) mod serde_bincode_compat {
534    use alloc::borrow::Cow;
535    use alloy_primitives::{Bytes, ChainId, TxKind, U256};
536    use serde::{Deserialize, Deserializer, Serialize, Serializer};
537    use serde_with::{DeserializeAs, SerializeAs};
538
539    /// Bincode-compatible [`super::TxLegacy`] serde implementation.
540    ///
541    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
542    /// ```rust
543    /// use alloy_consensus::{serde_bincode_compat, TxLegacy};
544    /// use serde::{Deserialize, Serialize};
545    /// use serde_with::serde_as;
546    ///
547    /// #[serde_as]
548    /// #[derive(Serialize, Deserialize)]
549    /// struct Data {
550    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxLegacy")]
551    ///     header: TxLegacy,
552    /// }
553    /// ```
554    #[derive(Debug, Serialize, Deserialize)]
555    pub struct TxLegacy<'a> {
556        #[serde(default, with = "alloy_serde::quantity::opt")]
557        chain_id: Option<ChainId>,
558        nonce: u64,
559        gas_price: u128,
560        gas_limit: u64,
561        #[serde(default)]
562        to: TxKind,
563        value: U256,
564        input: Cow<'a, Bytes>,
565    }
566
567    impl<'a> From<&'a super::TxLegacy> for TxLegacy<'a> {
568        fn from(value: &'a super::TxLegacy) -> Self {
569            Self {
570                chain_id: value.chain_id,
571                nonce: value.nonce,
572                gas_price: value.gas_price,
573                gas_limit: value.gas_limit,
574                to: value.to,
575                value: value.value,
576                input: Cow::Borrowed(&value.input),
577            }
578        }
579    }
580
581    impl<'a> From<TxLegacy<'a>> for super::TxLegacy {
582        fn from(value: TxLegacy<'a>) -> Self {
583            Self {
584                chain_id: value.chain_id,
585                nonce: value.nonce,
586                gas_price: value.gas_price,
587                gas_limit: value.gas_limit,
588                to: value.to,
589                value: value.value,
590                input: value.input.into_owned(),
591            }
592        }
593    }
594
595    impl SerializeAs<super::TxLegacy> for TxLegacy<'_> {
596        fn serialize_as<S>(source: &super::TxLegacy, serializer: S) -> Result<S::Ok, S::Error>
597        where
598            S: Serializer,
599        {
600            TxLegacy::from(source).serialize(serializer)
601        }
602    }
603
604    impl<'de> DeserializeAs<'de, super::TxLegacy> for TxLegacy<'de> {
605        fn deserialize_as<D>(deserializer: D) -> Result<super::TxLegacy, D::Error>
606        where
607            D: Deserializer<'de>,
608        {
609            TxLegacy::deserialize(deserializer).map(Into::into)
610        }
611    }
612
613    #[cfg(test)]
614    mod tests {
615        use arbitrary::Arbitrary;
616        use rand::Rng;
617        use serde::{Deserialize, Serialize};
618        use serde_with::serde_as;
619
620        use super::super::{serde_bincode_compat, TxLegacy};
621
622        #[test]
623        fn test_tx_legacy_bincode_roundtrip() {
624            #[serde_as]
625            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
626            struct Data {
627                #[serde_as(as = "serde_bincode_compat::TxLegacy")]
628                transaction: TxLegacy,
629            }
630
631            let mut bytes = [0u8; 1024];
632            rand::thread_rng().fill(bytes.as_mut_slice());
633            let data = Data {
634                transaction: TxLegacy::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
635                    .unwrap(),
636            };
637
638            let encoded = bincode::serialize(&data).unwrap();
639            let decoded: Data = bincode::deserialize(&encoded).unwrap();
640            assert_eq!(decoded, data);
641        }
642    }
643}
644
645#[cfg(all(test, feature = "k256"))]
646mod tests {
647    use crate::{
648        transaction::{from_eip155_value, to_eip155_value},
649        SignableTransaction, TxLegacy,
650    };
651    use alloy_primitives::{
652        address, b256, hex, Address, PrimitiveSignature as Signature, TxKind, B256, U256,
653    };
654
655    #[test]
656    fn recover_signer_legacy() {
657        let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into();
658        let hash: B256 =
659            hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into();
660
661        let tx = TxLegacy {
662            chain_id: Some(1),
663            nonce: 0x18,
664            gas_price: 0xfa56ea00,
665            gas_limit: 119902,
666            to: TxKind::Call(hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()),
667            value: U256::from(0x1c6bf526340000u64),
668            input:  hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(),
669        };
670
671        let sig = Signature::from_scalars_and_parity(
672            b256!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031"),
673            b256!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5"),
674            false,
675        );
676
677        let signed_tx = tx.into_signed(sig);
678
679        assert_eq!(*signed_tx.hash(), hash, "Expected same hash");
680        assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass.");
681    }
682
683    #[test]
684    // Test vector from https://github.com/alloy-rs/alloy/issues/125
685    fn decode_legacy_and_recover_signer() {
686        use crate::transaction::RlpEcdsaDecodableTx;
687        let raw_tx = alloy_primitives::bytes!("f9015482078b8505d21dba0083022ef1947a250d5630b4cf539739df2c5dacb4c659f2488d880c46549a521b13d8b8e47ff36ab50000000000000000000000000000000000000000000066ab5a608bd00a23f2fe000000000000000000000000000000000000000000000000000000000000008000000000000000000000000048c04ed5691981c42154c6167398f95e8f38a7ff00000000000000000000000000000000000000000000000000000000632ceac70000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006c6ee5e31d828de241282b9606c8e98ea48526e225a0c9077369501641a92ef7399ff81c21639ed4fd8fc69cb793cfa1dbfab342e10aa0615facb2f1bcf3274a354cfe384a38d0cc008a11c2dd23a69111bc6930ba27a8");
688
689        let tx = TxLegacy::rlp_decode_signed(&mut raw_tx.as_ref()).unwrap();
690
691        let recovered = tx.recover_signer().unwrap();
692        let expected = address!("a12e1462d0ceD572f396F58B6E2D03894cD7C8a4");
693
694        assert_eq!(tx.tx().chain_id, Some(1), "Expected same chain id");
695        assert_eq!(expected, recovered, "Expected same signer");
696    }
697
698    #[test]
699    fn eip155_roundtrip() {
700        assert_eq!(from_eip155_value(to_eip155_value(false, None)), Some((false, None)));
701        assert_eq!(from_eip155_value(to_eip155_value(true, None)), Some((true, None)));
702
703        for chain_id in [0, 1, 10, u64::MAX] {
704            assert_eq!(
705                from_eip155_value(to_eip155_value(false, Some(chain_id))),
706                Some((false, Some(chain_id)))
707            );
708            assert_eq!(
709                from_eip155_value(to_eip155_value(true, Some(chain_id))),
710                Some((true, Some(chain_id)))
711            );
712        }
713    }
714}