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