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