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::{keccak256, Bytes, ChainId, Signature, TxKind, B256, U256};
8use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result};
9use core::mem;
10
11/// Legacy transaction.
12#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
13#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
16#[doc(alias = "LegacyTransaction", alias = "TransactionLegacy", alias = "LegacyTx")]
17pub struct TxLegacy {
18    /// Added as EIP-155: Simple replay attack protection
19    #[cfg_attr(
20        feature = "serde",
21        serde(
22            default,
23            with = "alloy_serde::quantity::opt",
24            skip_serializing_if = "Option::is_none",
25        )
26    )]
27    pub chain_id: Option<ChainId>,
28    /// A scalar value equal to the number of transactions sent by the sender; formally Tn.
29    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
30    pub nonce: u64,
31    /// A scalar value equal to the number of
32    /// Wei to be paid per unit of gas for all computation
33    /// costs incurred as a result of the execution of this transaction; formally Tp.
34    ///
35    /// As ethereum circulation is around 120mil eth as of 2022 that is around
36    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
37    /// 340282366920938463463374607431768211455
38    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
39    pub gas_price: u128,
40    /// A scalar value equal to the maximum
41    /// amount of gas that should be used in executing
42    /// this transaction. This is paid up-front, before any
43    /// computation is done and may not be increased
44    /// later; formally Tg.
45    #[cfg_attr(
46        feature = "serde",
47        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
48    )]
49    pub gas_limit: u64,
50    /// The 160-bit address of the message call’s recipient or, for a contract creation
51    /// transaction, ∅, used here to denote the only member of B0 ; formally Tt.
52    #[cfg_attr(feature = "serde", serde(default))]
53    pub to: TxKind,
54    /// A scalar value equal to the number of Wei to
55    /// be transferred to the message call’s recipient or,
56    /// in the case of contract creation, as an endowment
57    /// to the newly created account; formally Tv.
58    pub value: U256,
59    /// Input has two uses depending if `to` field is Create or Call.
60    /// pub init: An unlimited size byte array specifying the
61    /// EVM-code for the account initialisation procedure CREATE,
62    /// data: An unlimited size byte array specifying the
63    /// input data of the message call, formally Td.
64    pub input: Bytes,
65}
66
67impl TxLegacy {
68    /// The EIP-2718 transaction type.
69    pub const TX_TYPE: isize = 0;
70
71    /// Calculates a heuristic for the in-memory size of the [TxLegacy] transaction.
72    #[inline]
73    pub fn size(&self) -> usize {
74        mem::size_of::<Option<ChainId>>() + // chain_id
75        mem::size_of::<u64>() + // nonce
76        mem::size_of::<u128>() + // gas_price
77        mem::size_of::<u64>() + // gas_limit
78        self.to.size() + // to
79        mem::size_of::<U256>() + // value
80        self.input.len() // input
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 Encodable for TxLegacy {
352    fn encode(&self, out: &mut dyn BufMut) {
353        self.encode_for_signing(out)
354    }
355
356    fn length(&self) -> usize {
357        let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
358        // 'header length' + 'payload length'
359        length_of_length(payload_length) + payload_length
360    }
361}
362
363impl Decodable for TxLegacy {
364    fn decode(data: &mut &[u8]) -> Result<Self> {
365        let header = Header::decode(data)?;
366        let remaining_len = data.len();
367
368        let transaction_payload_len = header.payload_length;
369
370        if transaction_payload_len > remaining_len {
371            return Err(alloy_rlp::Error::InputTooShort);
372        }
373
374        let mut transaction = Self::rlp_decode_fields(data)?;
375
376        // If we still have data, it should be an eip-155 encoded chain_id
377        if !data.is_empty() {
378            transaction.chain_id = Some(Decodable::decode(data)?);
379            let _: U256 = Decodable::decode(data)?; // r
380            let _: U256 = Decodable::decode(data)?; // s
381        }
382
383        let decoded = remaining_len - data.len();
384        if decoded != transaction_payload_len {
385            return Err(alloy_rlp::Error::UnexpectedLength);
386        }
387
388        Ok(transaction)
389    }
390}
391
392/// Helper for encoding `y_parity` boolean and optional `chain_id` into EIP-155 `v` value.
393pub const fn to_eip155_value(y_parity: bool, chain_id: Option<ChainId>) -> u128 {
394    match chain_id {
395        Some(id) => 35 + id as u128 * 2 + y_parity as u128,
396        None => 27 + y_parity as u128,
397    }
398}
399
400/// Helper for decoding EIP-155 `v` value into `y_parity` boolean and optional `chain_id`.
401pub const fn from_eip155_value(value: u128) -> Option<(bool, Option<ChainId>)> {
402    match value {
403        27 => Some((false, None)),
404        28 => Some((true, None)),
405        v @ 35.. => {
406            let y_parity = ((v - 35) % 2) != 0;
407            let chain_id = (v - 35) / 2;
408
409            if chain_id > u64::MAX as u128 {
410                return None;
411            }
412            Some((y_parity, Some(chain_id as u64)))
413        }
414        _ => None,
415    }
416}
417
418#[cfg(feature = "serde")]
419pub mod signed_legacy_serde {
420    //! Helper module for encoding signatures of transactions wrapped into [`Signed`] in legacy
421    //! format.
422    //!
423    //! By default, signatures are encoded as a single boolean under `yParity` key. However, for
424    //! legacy transactions parity byte is encoded as `v` key respecting EIP-155 format.
425    use super::*;
426    use alloc::borrow::Cow;
427    use alloy_primitives::U128;
428    use serde::{Deserialize, Serialize};
429
430    struct LegacySignature {
431        r: U256,
432        s: U256,
433        v: U128,
434    }
435
436    #[derive(Serialize, Deserialize)]
437    struct HumanReadableRepr {
438        r: U256,
439        s: U256,
440        v: U128,
441    }
442
443    #[derive(Serialize, Deserialize)]
444    #[serde(transparent)]
445    struct NonHumanReadableRepr((U256, U256, U128));
446
447    impl Serialize for LegacySignature {
448        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
449        where
450            S: serde::Serializer,
451        {
452            if serializer.is_human_readable() {
453                HumanReadableRepr { r: self.r, s: self.s, v: self.v }.serialize(serializer)
454            } else {
455                NonHumanReadableRepr((self.r, self.s, self.v)).serialize(serializer)
456            }
457        }
458    }
459
460    impl<'de> Deserialize<'de> for LegacySignature {
461        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
462        where
463            D: serde::Deserializer<'de>,
464        {
465            if deserializer.is_human_readable() {
466                HumanReadableRepr::deserialize(deserializer).map(|repr| Self {
467                    r: repr.r,
468                    s: repr.s,
469                    v: repr.v,
470                })
471            } else {
472                NonHumanReadableRepr::deserialize(deserializer).map(|repr| Self {
473                    r: repr.0 .0,
474                    s: repr.0 .1,
475                    v: repr.0 .2,
476                })
477            }
478        }
479    }
480
481    #[derive(Serialize, Deserialize)]
482    struct SignedLegacy<'a> {
483        #[serde(flatten)]
484        tx: Cow<'a, TxLegacy>,
485        #[serde(flatten)]
486        signature: LegacySignature,
487        hash: B256,
488    }
489
490    /// Serializes signed transaction with `v` key for signature parity.
491    pub fn serialize<S>(signed: &crate::Signed<TxLegacy>, serializer: S) -> Result<S::Ok, S::Error>
492    where
493        S: serde::Serializer,
494    {
495        SignedLegacy {
496            tx: Cow::Borrowed(signed.tx()),
497            signature: LegacySignature {
498                v: U128::from(to_eip155_value(signed.signature().v(), signed.tx().chain_id())),
499                r: signed.signature().r(),
500                s: signed.signature().s(),
501            },
502            hash: *signed.hash(),
503        }
504        .serialize(serializer)
505    }
506
507    /// Deserializes signed transaction expecting `v` key for signature parity.
508    pub fn deserialize<'de, D>(deserializer: D) -> Result<crate::Signed<TxLegacy>, D::Error>
509    where
510        D: serde::Deserializer<'de>,
511    {
512        let SignedLegacy { tx, signature, hash } = SignedLegacy::deserialize(deserializer)?;
513        let (parity, chain_id) = from_eip155_value(signature.v.to())
514            .ok_or_else(|| serde::de::Error::custom("invalid EIP-155 signature parity value"))?;
515
516        // Note: some implementations always set the chain id in the response, so we only check if
517        // they differ if both are set.
518        if let Some((tx_chain_id, chain_id)) = tx.chain_id().zip(chain_id) {
519            if tx_chain_id != chain_id {
520                return Err(serde::de::Error::custom("chain id mismatch"));
521            }
522        }
523        let mut tx = tx.into_owned();
524        tx.chain_id = chain_id;
525        Ok(Signed::new_unchecked(tx, Signature::new(signature.r, signature.s, parity), hash))
526    }
527}
528
529/// Bincode-compatible [`TxLegacy`] serde implementation.
530#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
531pub(super) mod serde_bincode_compat {
532    use alloc::borrow::Cow;
533    use alloy_primitives::{Bytes, ChainId, TxKind, U256};
534    use serde::{Deserialize, Deserializer, Serialize, Serializer};
535    use serde_with::{DeserializeAs, SerializeAs};
536
537    /// Bincode-compatible [`super::TxLegacy`] serde implementation.
538    ///
539    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
540    /// ```rust
541    /// use alloy_consensus::{serde_bincode_compat, TxLegacy};
542    /// use serde::{Deserialize, Serialize};
543    /// use serde_with::serde_as;
544    ///
545    /// #[serde_as]
546    /// #[derive(Serialize, Deserialize)]
547    /// struct Data {
548    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxLegacy")]
549    ///     header: TxLegacy,
550    /// }
551    /// ```
552    #[derive(Debug, Serialize, Deserialize)]
553    pub struct TxLegacy<'a> {
554        #[serde(default, with = "alloy_serde::quantity::opt")]
555        chain_id: Option<ChainId>,
556        nonce: u64,
557        gas_price: u128,
558        gas_limit: u64,
559        #[serde(default)]
560        to: TxKind,
561        value: U256,
562        input: Cow<'a, Bytes>,
563    }
564
565    impl<'a> From<&'a super::TxLegacy> for TxLegacy<'a> {
566        fn from(value: &'a super::TxLegacy) -> Self {
567            Self {
568                chain_id: value.chain_id,
569                nonce: value.nonce,
570                gas_price: value.gas_price,
571                gas_limit: value.gas_limit,
572                to: value.to,
573                value: value.value,
574                input: Cow::Borrowed(&value.input),
575            }
576        }
577    }
578
579    impl<'a> From<TxLegacy<'a>> for super::TxLegacy {
580        fn from(value: TxLegacy<'a>) -> Self {
581            Self {
582                chain_id: value.chain_id,
583                nonce: value.nonce,
584                gas_price: value.gas_price,
585                gas_limit: value.gas_limit,
586                to: value.to,
587                value: value.value,
588                input: value.input.into_owned(),
589            }
590        }
591    }
592
593    impl SerializeAs<super::TxLegacy> for TxLegacy<'_> {
594        fn serialize_as<S>(source: &super::TxLegacy, serializer: S) -> Result<S::Ok, S::Error>
595        where
596            S: Serializer,
597        {
598            TxLegacy::from(source).serialize(serializer)
599        }
600    }
601
602    impl<'de> DeserializeAs<'de, super::TxLegacy> for TxLegacy<'de> {
603        fn deserialize_as<D>(deserializer: D) -> Result<super::TxLegacy, D::Error>
604        where
605            D: Deserializer<'de>,
606        {
607            TxLegacy::deserialize(deserializer).map(Into::into)
608        }
609    }
610
611    #[cfg(test)]
612    mod tests {
613        use arbitrary::Arbitrary;
614        use bincode::config;
615        use rand::Rng;
616        use serde::{Deserialize, Serialize};
617        use serde_with::serde_as;
618
619        use super::super::{serde_bincode_compat, TxLegacy};
620
621        #[test]
622        fn test_tx_legacy_bincode_roundtrip() {
623            #[serde_as]
624            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
625            struct Data {
626                #[serde_as(as = "serde_bincode_compat::TxLegacy")]
627                transaction: TxLegacy,
628            }
629
630            let mut bytes = [0u8; 1024];
631            rand::thread_rng().fill(bytes.as_mut_slice());
632            let data = Data {
633                transaction: TxLegacy::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
634                    .unwrap(),
635            };
636
637            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
638            let (decoded, _) =
639                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).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::{address, b256, hex, Address, Signature, TxKind, B256, U256};
652
653    #[test]
654    fn recover_signer_legacy() {
655        let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into();
656        let hash: B256 =
657            hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into();
658
659        let tx = TxLegacy {
660            chain_id: Some(1),
661            nonce: 0x18,
662            gas_price: 0xfa56ea00,
663            gas_limit: 119902,
664            to: TxKind::Call(hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()),
665            value: U256::from(0x1c6bf526340000u64),
666            input:  hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(),
667        };
668
669        let sig = Signature::from_scalars_and_parity(
670            b256!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031"),
671            b256!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5"),
672            false,
673        );
674
675        let signed_tx = tx.into_signed(sig);
676
677        assert_eq!(*signed_tx.hash(), hash, "Expected same hash");
678        assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass.");
679    }
680
681    #[test]
682    // Test vector from https://github.com/alloy-rs/alloy/issues/125
683    fn decode_legacy_and_recover_signer() {
684        use crate::transaction::RlpEcdsaDecodableTx;
685        let raw_tx = alloy_primitives::bytes!("f9015482078b8505d21dba0083022ef1947a250d5630b4cf539739df2c5dacb4c659f2488d880c46549a521b13d8b8e47ff36ab50000000000000000000000000000000000000000000066ab5a608bd00a23f2fe000000000000000000000000000000000000000000000000000000000000008000000000000000000000000048c04ed5691981c42154c6167398f95e8f38a7ff00000000000000000000000000000000000000000000000000000000632ceac70000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006c6ee5e31d828de241282b9606c8e98ea48526e225a0c9077369501641a92ef7399ff81c21639ed4fd8fc69cb793cfa1dbfab342e10aa0615facb2f1bcf3274a354cfe384a38d0cc008a11c2dd23a69111bc6930ba27a8");
686
687        let tx = TxLegacy::rlp_decode_signed(&mut raw_tx.as_ref()).unwrap();
688
689        let recovered = tx.recover_signer().unwrap();
690        let expected = address!("a12e1462d0ceD572f396F58B6E2D03894cD7C8a4");
691
692        assert_eq!(tx.tx().chain_id, Some(1), "Expected same chain id");
693        assert_eq!(expected, recovered, "Expected same signer");
694    }
695
696    #[test]
697    fn eip155_roundtrip() {
698        assert_eq!(from_eip155_value(to_eip155_value(false, None)), Some((false, None)));
699        assert_eq!(from_eip155_value(to_eip155_value(true, None)), Some((true, None)));
700
701        for chain_id in [0, 1, 10, u64::MAX] {
702            assert_eq!(
703                from_eip155_value(to_eip155_value(false, Some(chain_id))),
704                Some((false, Some(chain_id)))
705            );
706            assert_eq!(
707                from_eip155_value(to_eip155_value(true, Some(chain_id))),
708                Some((true, Some(chain_id)))
709            );
710        }
711    }
712}