alloy_consensus/transaction/
eip1559.rs

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