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