alloy_consensus/transaction/
legacy.rs

1use crate::{
2    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
3    SignableTransaction, Signed, Transaction, TxType,
4};
5use alloc::vec::Vec;
6use alloy_eips::{
7    eip2718::IsTyped2718, eip2930::AccessList, eip7702::SignedAuthorization, Typed2718,
8};
9use alloy_primitives::{keccak256, Bytes, ChainId, Signature, TxKind, B256, U256};
10use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result};
11use core::mem;
12
13/// Legacy transaction.
14#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
15#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
18#[doc(alias = "LegacyTransaction", alias = "TransactionLegacy", alias = "LegacyTx")]
19pub struct TxLegacy {
20    /// Added as EIP-155: Simple replay attack protection
21    #[cfg_attr(
22        feature = "serde",
23        serde(
24            default,
25            with = "alloy_serde::quantity::opt",
26            skip_serializing_if = "Option::is_none",
27        )
28    )]
29    pub chain_id: Option<ChainId>,
30    /// A scalar value equal to the number of transactions sent by the sender; formally Tn.
31    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
32    pub nonce: u64,
33    /// A scalar value equal to the number of
34    /// Wei to be paid per unit of gas for all computation
35    /// costs incurred as a result of the execution of this transaction; formally Tp.
36    ///
37    /// As ethereum circulation is around 120mil eth as of 2022 that is around
38    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
39    /// 340282366920938463463374607431768211455
40    #[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 `to` field is Create or Call.
62    /// 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    fn rlp_encoded_fields_length(&self) -> usize {
116        self.nonce.length()
117            + self.gas_price.length()
118            + self.gas_limit.length()
119            + self.to.length()
120            + self.value.length()
121            + self.input.0.length()
122    }
123
124    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
125        self.nonce.encode(out);
126        self.gas_price.encode(out);
127        self.gas_limit.encode(out);
128        self.to.encode(out);
129        self.value.encode(out);
130        self.input.0.encode(out);
131    }
132
133    fn rlp_header_signed(&self, signature: &Signature) -> Header {
134        let payload_length = self.rlp_encoded_fields_length()
135            + signature.rlp_rs_len()
136            + to_eip155_value(signature.v(), self.chain_id).length();
137        Header { list: true, payload_length }
138    }
139
140    fn rlp_encoded_length_with_signature(&self, signature: &Signature) -> usize {
141        // Enforce correct parity for legacy transactions (EIP-155, 27 or 28).
142        self.rlp_header_signed(signature).length_with_payload()
143    }
144
145    fn rlp_encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) {
146        // Enforce correct parity for legacy transactions (EIP-155, 27 or 28).
147        self.rlp_header_signed(signature).encode(out);
148        self.rlp_encode_fields(out);
149        signature.write_rlp_vrs(out, to_eip155_value(signature.v(), self.chain_id));
150    }
151
152    fn eip2718_encoded_length(&self, signature: &Signature) -> usize {
153        self.rlp_encoded_length_with_signature(signature)
154    }
155
156    fn eip2718_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
157        self.rlp_encode_signed(signature, out);
158    }
159
160    fn network_header(&self, signature: &Signature) -> Header {
161        self.rlp_header_signed(signature)
162    }
163
164    fn network_encoded_length(&self, signature: &Signature) -> usize {
165        self.rlp_encoded_length_with_signature(signature)
166    }
167
168    fn network_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
169        self.rlp_encode_signed(signature, out);
170    }
171
172    fn tx_hash_with_type(&self, signature: &Signature, _ty: u8) -> alloy_primitives::TxHash {
173        let mut buf = Vec::with_capacity(self.rlp_encoded_length_with_signature(signature));
174        self.rlp_encode_signed(signature, &mut buf);
175        keccak256(&buf)
176    }
177}
178
179impl RlpEcdsaDecodableTx for TxLegacy {
180    const DEFAULT_TX_TYPE: u8 = { Self::TX_TYPE as u8 };
181
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 IsTyped2718 for TxLegacy {
354    fn is_type(type_id: u8) -> bool {
355        matches!(type_id, 0x00)
356    }
357}
358
359impl Encodable for TxLegacy {
360    fn encode(&self, out: &mut dyn BufMut) {
361        self.encode_for_signing(out)
362    }
363
364    fn length(&self) -> usize {
365        let payload_length = self.rlp_encoded_fields_length() + self.eip155_fields_len();
366        // 'header length' + 'payload length'
367        length_of_length(payload_length) + payload_length
368    }
369}
370
371impl Decodable for TxLegacy {
372    fn decode(data: &mut &[u8]) -> Result<Self> {
373        let header = Header::decode(data)?;
374        let remaining_len = data.len();
375
376        let transaction_payload_len = header.payload_length;
377
378        if transaction_payload_len > remaining_len {
379            return Err(alloy_rlp::Error::InputTooShort);
380        }
381
382        let mut transaction = Self::rlp_decode_fields(data)?;
383
384        // If we still have data, it should be an eip-155 encoded chain_id
385        if !data.is_empty() {
386            transaction.chain_id = Some(Decodable::decode(data)?);
387            let _: U256 = Decodable::decode(data)?; // r
388            let _: U256 = Decodable::decode(data)?; // s
389        }
390
391        let decoded = remaining_len - data.len();
392        if decoded != transaction_payload_len {
393            return Err(alloy_rlp::Error::UnexpectedLength);
394        }
395
396        Ok(transaction)
397    }
398}
399
400/// Helper for encoding `y_parity` boolean and optional `chain_id` into EIP-155 `v` value.
401pub const fn to_eip155_value(y_parity: bool, chain_id: Option<ChainId>) -> u128 {
402    match chain_id {
403        Some(id) => 35 + id as u128 * 2 + y_parity as u128,
404        None => 27 + y_parity as u128,
405    }
406}
407
408/// Helper for decoding EIP-155 `v` value into `y_parity` boolean and optional `chain_id`.
409pub const fn from_eip155_value(value: u128) -> Option<(bool, Option<ChainId>)> {
410    match value {
411        27 => Some((false, None)),
412        28 => Some((true, None)),
413        v @ 35.. => {
414            let y_parity = ((v - 35) % 2) != 0;
415            let chain_id = (v - 35) / 2;
416
417            if chain_id > u64::MAX as u128 {
418                return None;
419            }
420            Some((y_parity, Some(chain_id as u64)))
421        }
422        _ => None,
423    }
424}
425
426#[cfg(feature = "serde")]
427pub mod signed_legacy_serde {
428    //! Helper module for encoding signatures of transactions wrapped into [`Signed`] in legacy
429    //! format.
430    //!
431    //! By default, signatures are encoded as a single boolean under `yParity` key. However, for
432    //! legacy transactions parity byte is encoded as `v` key respecting EIP-155 format.
433    use super::*;
434    use alloc::borrow::Cow;
435    use alloy_primitives::U128;
436    use serde::{Deserialize, Serialize};
437
438    struct LegacySignature {
439        r: U256,
440        s: U256,
441        v: U128,
442    }
443
444    #[derive(Serialize, Deserialize)]
445    struct HumanReadableRepr {
446        r: U256,
447        s: U256,
448        v: U128,
449    }
450
451    #[derive(Serialize, Deserialize)]
452    #[serde(transparent)]
453    struct NonHumanReadableRepr((U256, U256, U128));
454
455    impl Serialize for LegacySignature {
456        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
457        where
458            S: serde::Serializer,
459        {
460            if serializer.is_human_readable() {
461                HumanReadableRepr { r: self.r, s: self.s, v: self.v }.serialize(serializer)
462            } else {
463                NonHumanReadableRepr((self.r, self.s, self.v)).serialize(serializer)
464            }
465        }
466    }
467
468    impl<'de> Deserialize<'de> for LegacySignature {
469        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
470        where
471            D: serde::Deserializer<'de>,
472        {
473            if deserializer.is_human_readable() {
474                HumanReadableRepr::deserialize(deserializer).map(|repr| Self {
475                    r: repr.r,
476                    s: repr.s,
477                    v: repr.v,
478                })
479            } else {
480                NonHumanReadableRepr::deserialize(deserializer).map(|repr| Self {
481                    r: repr.0 .0,
482                    s: repr.0 .1,
483                    v: repr.0 .2,
484                })
485            }
486        }
487    }
488
489    #[derive(Serialize, Deserialize)]
490    struct SignedLegacy<'a> {
491        #[serde(flatten)]
492        tx: Cow<'a, TxLegacy>,
493        #[serde(flatten)]
494        signature: LegacySignature,
495        hash: B256,
496    }
497
498    /// Serializes signed transaction with `v` key for signature parity.
499    pub fn serialize<S>(signed: &crate::Signed<TxLegacy>, serializer: S) -> Result<S::Ok, S::Error>
500    where
501        S: serde::Serializer,
502    {
503        SignedLegacy {
504            tx: Cow::Borrowed(signed.tx()),
505            signature: LegacySignature {
506                v: U128::from(to_eip155_value(signed.signature().v(), signed.tx().chain_id())),
507                r: signed.signature().r(),
508                s: signed.signature().s(),
509            },
510            hash: *signed.hash(),
511        }
512        .serialize(serializer)
513    }
514
515    /// Deserializes signed transaction expecting `v` key for signature parity.
516    pub fn deserialize<'de, D>(deserializer: D) -> Result<crate::Signed<TxLegacy>, D::Error>
517    where
518        D: serde::Deserializer<'de>,
519    {
520        let SignedLegacy { mut tx, signature, hash } = SignedLegacy::deserialize(deserializer)?;
521
522        // Optimism pre-Bedrock (and some other L2s) injected system transactions into the chain
523        // where the signature fields (v, r, s) are all zero.
524        // These transactions do not have a valid ECDSA signature, but are valid on-chain.
525        // See: https://github.com/alloy-rs/alloy/issues/2348
526        //
527        // Here, we detect (v=0, r=0, s=0) and treat them as system transactions,
528        // bypassing EIP-155 signature validation.
529        let is_fake_system_signature =
530            signature.r.is_zero() && signature.s.is_zero() && signature.v.is_zero();
531
532        let signature = if is_fake_system_signature {
533            Signature::new(U256::ZERO, U256::ZERO, false)
534        } else {
535            let (parity, chain_id) = from_eip155_value(signature.v.to()).ok_or_else(|| {
536                serde::de::Error::custom("invalid EIP-155 signature parity value")
537            })?;
538
539            // Note: some implementations always set the chain id in the response, so we only check
540            // if they differ if both are set.
541            if let Some((tx_chain_id, chain_id)) = tx.chain_id().zip(chain_id) {
542                if tx_chain_id != chain_id {
543                    return Err(serde::de::Error::custom("chain id mismatch"));
544                }
545            }
546
547            // update the chain id from decoding the eip155 value
548            tx.to_mut().chain_id = chain_id;
549
550            Signature::new(signature.r, signature.s, parity)
551        };
552        Ok(Signed::new_unchecked(tx.into_owned(), signature, hash))
553    }
554}
555
556/// Bincode-compatible [`TxLegacy`] serde implementation.
557#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
558pub(super) mod serde_bincode_compat {
559    use alloc::borrow::Cow;
560    use alloy_primitives::{Bytes, ChainId, TxKind, U256};
561    use serde::{Deserialize, Deserializer, Serialize, Serializer};
562    use serde_with::{DeserializeAs, SerializeAs};
563
564    /// Bincode-compatible [`super::TxLegacy`] serde implementation.
565    ///
566    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
567    /// ```rust
568    /// use alloy_consensus::{serde_bincode_compat, TxLegacy};
569    /// use serde::{Deserialize, Serialize};
570    /// use serde_with::serde_as;
571    ///
572    /// #[serde_as]
573    /// #[derive(Serialize, Deserialize)]
574    /// struct Data {
575    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxLegacy")]
576    ///     header: TxLegacy,
577    /// }
578    /// ```
579    #[derive(Debug, Serialize, Deserialize)]
580    pub struct TxLegacy<'a> {
581        #[serde(default, with = "alloy_serde::quantity::opt")]
582        chain_id: Option<ChainId>,
583        nonce: u64,
584        gas_price: u128,
585        gas_limit: u64,
586        #[serde(default)]
587        to: TxKind,
588        value: U256,
589        input: Cow<'a, Bytes>,
590    }
591
592    impl<'a> From<&'a super::TxLegacy> for TxLegacy<'a> {
593        fn from(value: &'a super::TxLegacy) -> Self {
594            Self {
595                chain_id: value.chain_id,
596                nonce: value.nonce,
597                gas_price: value.gas_price,
598                gas_limit: value.gas_limit,
599                to: value.to,
600                value: value.value,
601                input: Cow::Borrowed(&value.input),
602            }
603        }
604    }
605
606    impl<'a> From<TxLegacy<'a>> for super::TxLegacy {
607        fn from(value: TxLegacy<'a>) -> Self {
608            Self {
609                chain_id: value.chain_id,
610                nonce: value.nonce,
611                gas_price: value.gas_price,
612                gas_limit: value.gas_limit,
613                to: value.to,
614                value: value.value,
615                input: value.input.into_owned(),
616            }
617        }
618    }
619
620    impl SerializeAs<super::TxLegacy> for TxLegacy<'_> {
621        fn serialize_as<S>(source: &super::TxLegacy, serializer: S) -> Result<S::Ok, S::Error>
622        where
623            S: Serializer,
624        {
625            TxLegacy::from(source).serialize(serializer)
626        }
627    }
628
629    impl<'de> DeserializeAs<'de, super::TxLegacy> for TxLegacy<'de> {
630        fn deserialize_as<D>(deserializer: D) -> Result<super::TxLegacy, D::Error>
631        where
632            D: Deserializer<'de>,
633        {
634            TxLegacy::deserialize(deserializer).map(Into::into)
635        }
636    }
637
638    #[cfg(test)]
639    mod tests {
640        use arbitrary::Arbitrary;
641        use bincode::config;
642        use rand::Rng;
643        use serde::{Deserialize, Serialize};
644        use serde_with::serde_as;
645
646        use super::super::{serde_bincode_compat, TxLegacy};
647
648        #[test]
649        fn test_tx_legacy_bincode_roundtrip() {
650            #[serde_as]
651            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
652            struct Data {
653                #[serde_as(as = "serde_bincode_compat::TxLegacy")]
654                transaction: TxLegacy,
655            }
656
657            let mut bytes = [0u8; 1024];
658            rand::thread_rng().fill(bytes.as_mut_slice());
659            let data = Data {
660                transaction: TxLegacy::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
661                    .unwrap(),
662            };
663
664            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
665            let (decoded, _) =
666                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
667            assert_eq!(decoded, data);
668        }
669    }
670}
671
672#[cfg(all(test, feature = "k256"))]
673mod tests {
674    use super::signed_legacy_serde;
675    use crate::{
676        transaction::{from_eip155_value, to_eip155_value},
677        SignableTransaction, TxLegacy,
678    };
679    use alloy_primitives::{address, b256, hex, Address, Signature, TxKind, B256, U256};
680
681    #[test]
682    fn recover_signer_legacy() {
683        let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into();
684        let hash: B256 =
685            hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into();
686
687        let tx = TxLegacy {
688            chain_id: Some(1),
689            nonce: 0x18,
690            gas_price: 0xfa56ea00,
691            gas_limit: 119902,
692            to: TxKind::Call(hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()),
693            value: U256::from(0x1c6bf526340000u64),
694            input:  hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(),
695        };
696
697        let sig = Signature::from_scalars_and_parity(
698            b256!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031"),
699            b256!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5"),
700            false,
701        );
702
703        let signed_tx = tx.into_signed(sig);
704
705        assert_eq!(*signed_tx.hash(), hash, "Expected same hash");
706        assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass.");
707    }
708
709    #[test]
710    // Test vector from https://github.com/alloy-rs/alloy/issues/125
711    fn decode_legacy_and_recover_signer() {
712        use crate::transaction::RlpEcdsaDecodableTx;
713        let raw_tx = alloy_primitives::bytes!("f9015482078b8505d21dba0083022ef1947a250d5630b4cf539739df2c5dacb4c659f2488d880c46549a521b13d8b8e47ff36ab50000000000000000000000000000000000000000000066ab5a608bd00a23f2fe000000000000000000000000000000000000000000000000000000000000008000000000000000000000000048c04ed5691981c42154c6167398f95e8f38a7ff00000000000000000000000000000000000000000000000000000000632ceac70000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006c6ee5e31d828de241282b9606c8e98ea48526e225a0c9077369501641a92ef7399ff81c21639ed4fd8fc69cb793cfa1dbfab342e10aa0615facb2f1bcf3274a354cfe384a38d0cc008a11c2dd23a69111bc6930ba27a8");
714
715        let tx = TxLegacy::rlp_decode_signed(&mut raw_tx.as_ref()).unwrap();
716
717        let recovered = tx.recover_signer().unwrap();
718        let expected = address!("a12e1462d0ceD572f396F58B6E2D03894cD7C8a4");
719
720        assert_eq!(tx.tx().chain_id, Some(1), "Expected same chain id");
721        assert_eq!(expected, recovered, "Expected same signer");
722    }
723
724    #[test]
725    fn eip155_roundtrip() {
726        assert_eq!(from_eip155_value(to_eip155_value(false, None)), Some((false, None)));
727        assert_eq!(from_eip155_value(to_eip155_value(true, None)), Some((true, None)));
728
729        for chain_id in [0, 1, 10, u64::MAX] {
730            assert_eq!(
731                from_eip155_value(to_eip155_value(false, Some(chain_id))),
732                Some((false, Some(chain_id)))
733            );
734            assert_eq!(
735                from_eip155_value(to_eip155_value(true, Some(chain_id))),
736                Some((true, Some(chain_id)))
737            );
738        }
739    }
740
741    #[test]
742    fn can_deserialize_system_transaction_with_zero_signature() {
743        let raw_tx = serde_json::json!({
744            "blockHash": "0x5307b5c812a067f8bc1ed1cc89d319ae6f9a0c9693848bd25c36b5191de60b85",
745            "blockNumber": "0x45a59bb",
746            "from": "0x0000000000000000000000000000000000000000",
747            "gas": "0x1e8480",
748            "gasPrice": "0x0",
749            "hash": "0x16ef68aa8f35add3a03167a12b5d1268e344f6605a64ecc3f1c3aa68e98e4e06",
750            "input": "0xcbd4ece900000000000000000000000032155c9d39084f040ba17890fe8134dbe2a0453f0000000000000000000000004a0126ee88018393b1ad2455060bc350ead9908a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000469f700000000000000000000000000000000000000000000000000000000000000644ff746f60000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002043e908a4e862aebb10e7e27db0b892b58a7e32af11d64387a414dabc327b00e200000000000000000000000000000000000000000000000000000000",
751            "nonce": "0x469f7",
752            "to": "0x4200000000000000000000000000000000000007",
753            "transactionIndex": "0x0",
754            "value": "0x0",
755            "v": "0x0",
756            "r": "0x0",
757            "s": "0x0",
758            "queueOrigin": "l1",
759            "l1TxOrigin": "0x36bde71c97b33cc4729cf772ae268934f7ab70b2",
760            "l1BlockNumber": "0xfd1a6c",
761            "l1Timestamp": "0x63e434ff",
762            "index": "0x45a59ba",
763            "queueIndex": "0x469f7",
764            "rawTransaction": "0xcbd4ece900000000000000000000000032155c9d39084f040ba17890fe8134dbe2a0453f0000000000000000000000004a0126ee88018393b1ad2455060bc350ead9908a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000469f700000000000000000000000000000000000000000000000000000000000000644ff746f60000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002043e908a4e862aebb10e7e27db0b892b58a7e32af11d64387a414dabc327b00e200000000000000000000000000000000000000000000000000000000"
765        });
766
767        let signed: crate::Signed<TxLegacy> = signed_legacy_serde::deserialize(raw_tx).unwrap();
768
769        assert_eq!(signed.signature().r(), U256::ZERO);
770        assert_eq!(signed.signature().s(), U256::ZERO);
771        assert!(!signed.signature().v());
772
773        assert_eq!(
774            signed.hash(),
775            &b256!("0x16ef68aa8f35add3a03167a12b5d1268e344f6605a64ecc3f1c3aa68e98e4e06"),
776            "hash should match the transaction hash"
777        );
778    }
779}