alloy_consensus/transaction/
eip1559.rs

1use crate::{SignableTransaction, Transaction, TxType};
2use alloy_eips::{
3    eip2718::IsTyped2718, eip2930::AccessList, eip7702::SignedAuthorization, Typed2718,
4};
5use alloy_primitives::{Bytes, ChainId, Signature, TxKind, B256, U256};
6use alloy_rlp::{BufMut, Decodable, Encodable};
7use core::mem;
8
9use super::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx};
10
11/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)).
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#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
17#[doc(alias = "Eip1559Transaction", alias = "TransactionEip1559", alias = "Eip1559Tx")]
18pub struct TxEip1559 {
19    /// EIP-155: Simple replay attack protection
20    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
21    pub chain_id: ChainId,
22    /// A scalar value equal to the number of transactions sent by the sender; formally Tn.
23    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
24    pub nonce: u64,
25    /// A scalar value equal to the maximum
26    /// amount of gas that should be used in executing
27    /// this transaction. This is paid up-front, before any
28    /// computation is done and may not be increased
29    /// later; formally Tg.
30    #[cfg_attr(
31        feature = "serde",
32        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
33    )]
34    pub gas_limit: u64,
35    /// A scalar value equal to the maximum
36    /// amount of gas that should be used in executing
37    /// this transaction. This is paid up-front, before any
38    /// computation is done and may not be increased
39    /// later; formally Tg.
40    ///
41    /// As ethereum circulation is around 120mil eth as of 2022 that is around
42    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
43    /// 340282366920938463463374607431768211455
44    ///
45    /// This is also known as `GasFeeCap`
46    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
47    pub max_fee_per_gas: u128,
48    /// Max Priority fee that transaction is paying
49    ///
50    /// As ethereum circulation is around 120mil eth as of 2022 that is around
51    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
52    /// 340282366920938463463374607431768211455
53    ///
54    /// This is also known as `GasTipCap`
55    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
56    pub max_priority_fee_per_gas: u128,
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    /// The accessList specifies a list of addresses and storage keys;
67    /// these addresses and storage keys are added into the `accessed_addresses`
68    /// and `accessed_storage_keys` global sets (introduced in EIP-2929).
69    /// A gas cost is charged, though at a discount relative to the cost of
70    /// accessing outside the list.
71    // Deserialize with `alloy_serde::null_as_default` to also accept a `null` value
72    // instead of an (empty) array. This is due to certain RPC providers (e.g., Filecoin's)
73    // sometimes returning `null` instead of an empty array `[]`.
74    // More details in <https://github.com/alloy-rs/alloy/pull/2450>.
75    #[cfg_attr(feature = "serde", serde(deserialize_with = "alloy_serde::null_as_default"))]
76    pub access_list: AccessList,
77    /// Input has two uses depending if `to` field is Create or Call.
78    /// pub init: An unlimited size byte array specifying the
79    /// EVM-code for the account initialisation procedure CREATE,
80    /// data: An unlimited size byte array specifying the
81    /// input data of the message call, formally Td.
82    pub input: Bytes,
83}
84
85impl TxEip1559 {
86    /// Get the transaction type
87    #[doc(alias = "transaction_type")]
88    pub const fn tx_type() -> TxType {
89        TxType::Eip1559
90    }
91
92    /// Calculates a heuristic for the in-memory size of the [TxEip1559]
93    /// transaction.
94    #[inline]
95    pub fn size(&self) -> usize {
96        mem::size_of::<ChainId>() + // chain_id
97        mem::size_of::<u64>() + // nonce
98        mem::size_of::<u64>() + // gas_limit
99        mem::size_of::<u128>() + // max_fee_per_gas
100        mem::size_of::<u128>() + // max_priority_fee_per_gas
101        self.to.size() + // to
102        mem::size_of::<U256>() + // value
103        self.access_list.size() + // access_list
104        self.input.len() // input
105    }
106}
107
108impl RlpEcdsaEncodableTx for TxEip1559 {
109    /// Outputs the length of the transaction's fields, without a RLP header.
110    fn rlp_encoded_fields_length(&self) -> usize {
111        self.chain_id.length()
112            + self.nonce.length()
113            + self.max_priority_fee_per_gas.length()
114            + self.max_fee_per_gas.length()
115            + self.gas_limit.length()
116            + self.to.length()
117            + self.value.length()
118            + self.input.0.length()
119            + self.access_list.length()
120    }
121
122    /// Encodes only the transaction's fields into the desired buffer, without
123    /// a RLP header.
124    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
125        self.chain_id.encode(out);
126        self.nonce.encode(out);
127        self.max_priority_fee_per_gas.encode(out);
128        self.max_fee_per_gas.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        self.access_list.encode(out);
134    }
135}
136
137impl RlpEcdsaDecodableTx for TxEip1559 {
138    const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
139
140    /// Decodes the inner [TxEip1559] fields from RLP bytes.
141    ///
142    /// NOTE: This assumes a RLP header has already been decoded, and _just_
143    /// decodes the following RLP fields in the following order:
144    ///
145    /// - `chain_id`
146    /// - `nonce`
147    /// - `max_priority_fee_per_gas`
148    /// - `max_fee_per_gas`
149    /// - `gas_limit`
150    /// - `to`
151    /// - `value`
152    /// - `data` (`input`)
153    /// - `access_list`
154    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
155        Ok(Self {
156            chain_id: Decodable::decode(buf)?,
157            nonce: Decodable::decode(buf)?,
158            max_priority_fee_per_gas: Decodable::decode(buf)?,
159            max_fee_per_gas: Decodable::decode(buf)?,
160            gas_limit: Decodable::decode(buf)?,
161            to: Decodable::decode(buf)?,
162            value: Decodable::decode(buf)?,
163            input: Decodable::decode(buf)?,
164            access_list: Decodable::decode(buf)?,
165        })
166    }
167}
168
169impl Transaction for TxEip1559 {
170    #[inline]
171    fn chain_id(&self) -> Option<ChainId> {
172        Some(self.chain_id)
173    }
174
175    #[inline]
176    fn nonce(&self) -> u64 {
177        self.nonce
178    }
179
180    #[inline]
181    fn gas_limit(&self) -> u64 {
182        self.gas_limit
183    }
184
185    #[inline]
186    fn gas_price(&self) -> Option<u128> {
187        None
188    }
189
190    #[inline]
191    fn max_fee_per_gas(&self) -> u128 {
192        self.max_fee_per_gas
193    }
194
195    #[inline]
196    fn max_priority_fee_per_gas(&self) -> Option<u128> {
197        Some(self.max_priority_fee_per_gas)
198    }
199
200    #[inline]
201    fn max_fee_per_blob_gas(&self) -> Option<u128> {
202        None
203    }
204
205    #[inline]
206    fn priority_fee_or_price(&self) -> u128 {
207        self.max_priority_fee_per_gas
208    }
209
210    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
211        alloy_eips::eip1559::calc_effective_gas_price(
212            self.max_fee_per_gas,
213            self.max_priority_fee_per_gas,
214            base_fee,
215        )
216    }
217
218    #[inline]
219    fn is_dynamic_fee(&self) -> bool {
220        true
221    }
222
223    #[inline]
224    fn kind(&self) -> TxKind {
225        self.to
226    }
227
228    #[inline]
229    fn is_create(&self) -> bool {
230        self.to.is_create()
231    }
232
233    #[inline]
234    fn value(&self) -> U256 {
235        self.value
236    }
237
238    #[inline]
239    fn input(&self) -> &Bytes {
240        &self.input
241    }
242
243    #[inline]
244    fn access_list(&self) -> Option<&AccessList> {
245        Some(&self.access_list)
246    }
247
248    #[inline]
249    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
250        None
251    }
252
253    #[inline]
254    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
255        None
256    }
257}
258
259impl Typed2718 for TxEip1559 {
260    fn ty(&self) -> u8 {
261        TxType::Eip1559 as u8
262    }
263}
264
265impl IsTyped2718 for TxEip1559 {
266    fn is_type(type_id: u8) -> bool {
267        matches!(type_id, 0x02)
268    }
269}
270
271impl SignableTransaction<Signature> for TxEip1559 {
272    fn set_chain_id(&mut self, chain_id: ChainId) {
273        self.chain_id = chain_id;
274    }
275
276    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
277        out.put_u8(Self::tx_type() as u8);
278        self.encode(out)
279    }
280
281    fn payload_len_for_signature(&self) -> usize {
282        self.length() + 1
283    }
284}
285
286impl Encodable for TxEip1559 {
287    fn encode(&self, out: &mut dyn BufMut) {
288        self.rlp_encode(out);
289    }
290
291    fn length(&self) -> usize {
292        self.rlp_encoded_length()
293    }
294}
295
296impl Decodable for TxEip1559 {
297    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
298        Self::rlp_decode(buf)
299    }
300}
301
302/// Bincode-compatible [`TxEip1559`] serde implementation.
303#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
304pub(super) mod serde_bincode_compat {
305    use alloc::borrow::Cow;
306    use alloy_eips::eip2930::AccessList;
307    use alloy_primitives::{Bytes, ChainId, TxKind, U256};
308    use serde::{Deserialize, Deserializer, Serialize, Serializer};
309    use serde_with::{DeserializeAs, SerializeAs};
310
311    /// Bincode-compatible [`super::TxEip1559`] serde implementation.
312    ///
313    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
314    /// ```rust
315    /// use alloy_consensus::{serde_bincode_compat, TxEip1559};
316    /// use serde::{Deserialize, Serialize};
317    /// use serde_with::serde_as;
318    ///
319    /// #[serde_as]
320    /// #[derive(Serialize, Deserialize)]
321    /// struct Data {
322    ///     #[serde_as(as = "serde_bincode_compat::transaction::TxEip1559")]
323    ///     transaction: TxEip1559,
324    /// }
325    /// ```
326    #[derive(Debug, Serialize, Deserialize)]
327    pub struct TxEip1559<'a> {
328        chain_id: ChainId,
329        nonce: u64,
330        gas_limit: u64,
331        max_fee_per_gas: u128,
332        max_priority_fee_per_gas: u128,
333        #[serde(default)]
334        to: TxKind,
335        value: U256,
336        access_list: Cow<'a, AccessList>,
337        input: Cow<'a, Bytes>,
338    }
339
340    impl<'a> From<&'a super::TxEip1559> for TxEip1559<'a> {
341        fn from(value: &'a super::TxEip1559) -> Self {
342            Self {
343                chain_id: value.chain_id,
344                nonce: value.nonce,
345                gas_limit: value.gas_limit,
346                max_fee_per_gas: value.max_fee_per_gas,
347                max_priority_fee_per_gas: value.max_priority_fee_per_gas,
348                to: value.to,
349                value: value.value,
350                access_list: Cow::Borrowed(&value.access_list),
351                input: Cow::Borrowed(&value.input),
352            }
353        }
354    }
355
356    impl<'a> From<TxEip1559<'a>> for super::TxEip1559 {
357        fn from(value: TxEip1559<'a>) -> Self {
358            Self {
359                chain_id: value.chain_id,
360                nonce: value.nonce,
361                gas_limit: value.gas_limit,
362                max_fee_per_gas: value.max_fee_per_gas,
363                max_priority_fee_per_gas: value.max_priority_fee_per_gas,
364                to: value.to,
365                value: value.value,
366                access_list: value.access_list.into_owned(),
367                input: value.input.into_owned(),
368            }
369        }
370    }
371
372    impl SerializeAs<super::TxEip1559> for TxEip1559<'_> {
373        fn serialize_as<S>(source: &super::TxEip1559, serializer: S) -> Result<S::Ok, S::Error>
374        where
375            S: Serializer,
376        {
377            TxEip1559::from(source).serialize(serializer)
378        }
379    }
380
381    impl<'de> DeserializeAs<'de, super::TxEip1559> for TxEip1559<'de> {
382        fn deserialize_as<D>(deserializer: D) -> Result<super::TxEip1559, D::Error>
383        where
384            D: Deserializer<'de>,
385        {
386            TxEip1559::deserialize(deserializer).map(Into::into)
387        }
388    }
389
390    #[cfg(test)]
391    mod tests {
392        use arbitrary::Arbitrary;
393        use bincode::config;
394        use rand::Rng;
395        use serde::{Deserialize, Serialize};
396        use serde_with::serde_as;
397
398        use super::super::{serde_bincode_compat, TxEip1559};
399
400        #[test]
401        fn test_tx_eip1559_bincode_roundtrip() {
402            #[serde_as]
403            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
404            struct Data {
405                #[serde_as(as = "serde_bincode_compat::TxEip1559")]
406                transaction: TxEip1559,
407            }
408
409            let mut bytes = [0u8; 1024];
410            rand::thread_rng().fill(bytes.as_mut_slice());
411            let data = Data {
412                transaction: TxEip1559::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
413                    .unwrap(),
414            };
415
416            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
417            let (decoded, _) =
418                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
419            assert_eq!(decoded, data);
420        }
421    }
422}
423
424#[cfg(all(test, feature = "k256"))]
425mod tests {
426    use super::TxEip1559;
427    use crate::{
428        transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
429        SignableTransaction,
430    };
431    use alloy_eips::eip2930::AccessList;
432    use alloy_primitives::{address, b256, hex, Address, Signature, B256, U256};
433
434    #[test]
435    fn recover_signer_eip1559() {
436        let signer: Address = address!("dd6b8b3dc6b7ad97db52f08a275ff4483e024cea");
437        let hash: B256 = b256!("0ec0b6a2df4d87424e5f6ad2a654e27aaeb7dac20ae9e8385cc09087ad532ee0");
438
439        let tx =  TxEip1559 {
440                chain_id: 1,
441                nonce: 0x42,
442                gas_limit: 44386,
443                to: address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6").into(),
444                value: U256::from(0_u64),
445                input:  hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(),
446                max_fee_per_gas: 0x4a817c800,
447                max_priority_fee_per_gas: 0x3b9aca00,
448                access_list: AccessList::default(),
449            };
450
451        let sig = Signature::from_scalars_and_parity(
452            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
453            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
454            false,
455        );
456
457        assert_eq!(
458            tx.signature_hash(),
459            hex!("0d5688ac3897124635b6cf1bc0e29d6dfebceebdc10a54d74f2ef8b56535b682")
460        );
461
462        let signed_tx = tx.into_signed(sig);
463        assert_eq!(*signed_tx.hash(), hash, "Expected same hash");
464        assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass.");
465    }
466
467    #[test]
468    fn encode_decode_eip1559() {
469        let hash: B256 = b256!("0ec0b6a2df4d87424e5f6ad2a654e27aaeb7dac20ae9e8385cc09087ad532ee0");
470
471        let tx =  TxEip1559 {
472                chain_id: 1,
473                nonce: 0x42,
474                gas_limit: 44386,
475                to: address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6").into(),
476                value: U256::from(0_u64),
477                input:  hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(),
478                max_fee_per_gas: 0x4a817c800,
479                max_priority_fee_per_gas: 0x3b9aca00,
480                access_list: AccessList::default(),
481            };
482
483        let sig = Signature::from_scalars_and_parity(
484            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
485            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
486            false,
487        );
488
489        let mut buf = vec![];
490        tx.rlp_encode_signed(&sig, &mut buf);
491        let decoded = TxEip1559::rlp_decode_signed(&mut &buf[..]).unwrap();
492        assert_eq!(decoded, tx.into_signed(sig));
493        assert_eq!(*decoded.hash(), hash);
494    }
495
496    #[test]
497    fn json_decode_eip1559_null_access_list() {
498        let hash: B256 = b256!("0ec0b6a2df4d87424e5f6ad2a654e27aaeb7dac20ae9e8385cc09087ad532ee0");
499
500        let tx_json = r#"
501        {
502            "chainId": "0x1",
503            "nonce": "0x42",
504            "gas": "0xad62",
505            "to": "0x6069a6c32cf691f5982febae4faf8a6f3ab2f0f6",
506            "value": "0x0",
507            "input": "0xa22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000",
508            "maxFeePerGas": "0x4a817c800",
509            "maxPriorityFeePerGas": "0x3b9aca00",
510            "accessList": null
511        }
512        "#;
513        // Make sure that we can decode a `null` accessList
514        let tx: TxEip1559 = serde_json::from_str(tx_json).unwrap();
515        assert_eq!(tx.access_list, AccessList::default());
516
517        let sig = Signature::from_scalars_and_parity(
518            b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"),
519            b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"),
520            false,
521        );
522
523        let mut buf = vec![];
524        tx.rlp_encode_signed(&sig, &mut buf);
525        let decoded = TxEip1559::rlp_decode_signed(&mut &buf[..]).unwrap();
526        assert_eq!(decoded, tx.into_signed(sig));
527        assert_eq!(*decoded.hash(), hash);
528    }
529}