alloy_consensus/transaction/
eip2930.rs

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