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