alloy_consensus/transaction/
eip1559.rs

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