alloy_eips/
eip2718.rs

1//! [EIP-2718] traits.
2//!
3//! [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
4
5use crate::alloc::vec::Vec;
6use alloy_primitives::{keccak256, Bytes, Sealed, B256};
7use alloy_rlp::{Buf, BufMut, Header, EMPTY_STRING_CODE};
8use core::fmt;
9
10// https://eips.ethereum.org/EIPS/eip-2718#transactiontype-only-goes-up-to-0x7f
11const TX_TYPE_BYTE_MAX: u8 = 0x7f;
12
13/// Identifier for legacy transaction, however a legacy tx is technically not
14/// typed.
15pub const LEGACY_TX_TYPE_ID: u8 = 0;
16
17/// Identifier for an EIP2930 transaction.
18pub const EIP2930_TX_TYPE_ID: u8 = 1;
19
20/// Identifier for an EIP1559 transaction.
21pub const EIP1559_TX_TYPE_ID: u8 = 2;
22
23/// Identifier for an EIP4844 transaction.
24pub const EIP4844_TX_TYPE_ID: u8 = 3;
25
26/// Identifier for an EIP7702 transaction.
27pub const EIP7702_TX_TYPE_ID: u8 = 4;
28
29/// [EIP-2718] decoding errors.
30///
31/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
32#[derive(Clone, Copy, Debug)]
33#[non_exhaustive] // NB: non-exhaustive allows us to add a Custom variant later
34pub enum Eip2718Error {
35    /// Rlp error from [`alloy_rlp`].
36    RlpError(alloy_rlp::Error),
37    /// Got an unexpected type flag while decoding.
38    UnexpectedType(u8),
39}
40
41/// Result type for [EIP-2718] decoding.
42pub type Eip2718Result<T, E = Eip2718Error> = core::result::Result<T, E>;
43
44impl fmt::Display for Eip2718Error {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::RlpError(err) => write!(f, "{err}"),
48            Self::UnexpectedType(t) => write!(f, "Unexpected type flag. Got {t}."),
49        }
50    }
51}
52
53impl From<alloy_rlp::Error> for Eip2718Error {
54    fn from(err: alloy_rlp::Error) -> Self {
55        Self::RlpError(err)
56    }
57}
58
59impl From<Eip2718Error> for alloy_rlp::Error {
60    fn from(err: Eip2718Error) -> Self {
61        match err {
62            Eip2718Error::RlpError(err) => err,
63            Eip2718Error::UnexpectedType(_) => Self::Custom("Unexpected type flag"),
64        }
65    }
66}
67
68impl core::error::Error for Eip2718Error {}
69
70/// Decoding trait for [EIP-2718] envelopes. These envelopes wrap a transaction
71/// or a receipt with a type flag.
72///
73/// Users should rarely import this trait, and should instead prefer letting the
74/// alloy `Provider` methods handle encoding
75///
76/// ## Implementing
77///
78/// Implement this trait when you need to make custom TransactionEnvelope
79/// and ReceiptEnvelope types for your network. These types should be enums
80/// over the accepted transaction types.
81///
82/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
83pub trait Decodable2718: Sized {
84    /// Extract the type byte from the buffer, if any. The type byte is the
85    /// first byte, provided that that first byte is 0x7f or lower.
86    fn extract_type_byte(buf: &mut &[u8]) -> Option<u8> {
87        buf.first().copied().filter(|b| *b <= TX_TYPE_BYTE_MAX)
88    }
89
90    /// Decode the appropriate variant, based on the type flag.
91    ///
92    /// This function is invoked by [`Self::decode_2718`] with the type byte,
93    /// and the tail of the buffer.
94    ///
95    /// ## Implementing
96    ///
97    /// This should be a simple match block that invokes an inner type's
98    /// specific decoder.
99    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self>;
100
101    /// Decode the default variant.
102    ///
103    /// ## Implementing
104    ///
105    /// This function is invoked by [`Self::decode_2718`] when no type byte can
106    /// be extracted. It should be a simple wrapper around the default type's
107    /// decoder.
108    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self>;
109
110    /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
111    /// type flag in the range 0x0-0x7f, then the body of the transaction.
112    ///
113    /// [EIP-2718] inner encodings are unspecified, and produce an opaque
114    /// bytestring.
115    ///
116    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
117    fn decode_2718(buf: &mut &[u8]) -> Eip2718Result<Self> {
118        Self::extract_type_byte(buf)
119            .map(|ty| {
120                buf.advance(1);
121                Self::typed_decode(ty, buf)
122            })
123            .unwrap_or_else(|| Self::fallback_decode(buf))
124    }
125
126    /// Decode an [EIP-2718] transaction in the network format. The network
127    /// format is used ONLY by the Ethereum p2p protocol. Do not call this
128    /// method unless you are building a p2p protocol client.
129    ///
130    /// The network encoding is the RLP encoding of the eip2718-encoded
131    /// envelope.
132    ///
133    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
134    fn network_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
135        // Keep the original buffer around by copying it.
136        let mut h_decode = *buf;
137        let h = Header::decode(&mut h_decode)?;
138
139        // If it's a list, we need to fallback to the legacy decoding.
140        if h.list {
141            return Self::fallback_decode(buf);
142        }
143        *buf = h_decode;
144
145        let remaining_len = buf.len();
146        if remaining_len == 0 || remaining_len < h.payload_length {
147            return Err(alloy_rlp::Error::InputTooShort.into());
148        }
149
150        let ty = buf.get_u8();
151        let tx = Self::typed_decode(ty, buf)?;
152
153        let bytes_consumed = remaining_len - buf.len();
154        // because Header::decode works for single bytes (including the tx type), returning a
155        // string Header with payload_length of 1, we need to make sure this check is only
156        // performed for transactions with a string header
157        if bytes_consumed != h.payload_length && h_decode[0] > EMPTY_STRING_CODE {
158            return Err(alloy_rlp::Error::UnexpectedLength.into());
159        }
160
161        Ok(tx)
162    }
163}
164
165/// Encoding trait for [EIP-2718] envelopes.
166///
167/// These envelopes wrap a transaction or a receipt with a type flag. [EIP-2718] encodings are used
168/// by the `eth_sendRawTransaction` RPC call, the Ethereum block header's tries, and the
169/// peer-to-peer protocol.
170///
171/// Users should rarely import this trait, and should instead prefer letting the
172/// alloy `Provider` methods handle encoding
173///
174/// ## Implementing
175///
176/// Implement this trait when you need to make custom TransactionEnvelope
177/// and ReceiptEnvelope types for your network. These types should be enums
178/// over the accepted transaction types.
179///
180/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
181pub trait Encodable2718: Typed2718 + Sized + Send + Sync {
182    /// Return the type flag (if any).
183    ///
184    /// This should return `None` for the default (legacy) variant of the
185    /// envelope.
186    fn type_flag(&self) -> Option<u8> {
187        match self.ty() {
188            LEGACY_TX_TYPE_ID => None,
189            ty => Some(ty),
190        }
191    }
192
193    /// The length of the 2718 encoded envelope. This is the length of the type
194    /// flag + the length of the inner encoding.
195    fn encode_2718_len(&self) -> usize;
196
197    /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
198    /// type flag in the range 0x0-0x7f, then the body of the transaction.
199    ///
200    /// [EIP-2718] inner encodings are unspecified, and produce an opaque
201    /// bytestring.
202    ///
203    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
204    fn encode_2718(&self, out: &mut dyn BufMut);
205
206    /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
207    /// type flag in the range 0x0-0x7f, then the body of the transaction.
208    ///
209    /// This is a convenience method for encoding into a vec, and returning the
210    /// vec.
211    fn encoded_2718(&self) -> Vec<u8> {
212        let mut out = Vec::with_capacity(self.encode_2718_len());
213        self.encode_2718(&mut out);
214        out
215    }
216
217    /// Compute the hash as committed to in the MPT trie. This hash is used
218    /// ONLY by the Ethereum merkle-patricia trie and associated proofs. Do not
219    /// call this method unless you are building a full or light client.
220    ///
221    /// The trie hash is the keccak256 hash of the 2718-encoded envelope.
222    fn trie_hash(&self) -> B256 {
223        keccak256(self.encoded_2718())
224    }
225
226    /// Seal the encodable, by encoding and hashing it.
227    fn seal(self) -> Sealed<Self> {
228        let hash = self.trie_hash();
229        Sealed::new_unchecked(self, hash)
230    }
231
232    /// The length of the 2718 encoded envelope in network format. This is the
233    /// length of the header + the length of the type flag and inner encoding.
234    fn network_len(&self) -> usize {
235        let mut payload_length = self.encode_2718_len();
236        if !self.is_legacy() {
237            payload_length += Header { list: false, payload_length }.length();
238        }
239
240        payload_length
241    }
242
243    /// Encode in the network format. The network format is used ONLY by the
244    /// Ethereum p2p protocol. Do not call this method unless you are building
245    /// a p2p protocol client.
246    ///
247    /// The network encoding is the RLP encoding of the eip2718-encoded
248    /// envelope.
249    fn network_encode(&self, out: &mut dyn BufMut) {
250        if !self.is_legacy() {
251            Header { list: false, payload_length: self.encode_2718_len() }.encode(out);
252        }
253
254        self.encode_2718(out);
255    }
256}
257
258/// An [EIP-2718] envelope, blanket implemented for types that impl [`Encodable2718`] and
259/// [`Decodable2718`].
260///
261/// This envelope is a wrapper around a transaction, or a receipt, or any other type that is
262/// differentiated by an EIP-2718 transaction type.
263///
264/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
265pub trait Eip2718Envelope: Decodable2718 + Encodable2718 {}
266impl<T> Eip2718Envelope for T where T: Decodable2718 + Encodable2718 {}
267
268/// A trait that helps to determine the type of the transaction.
269#[auto_impl::auto_impl(&)]
270pub trait Typed2718 {
271    /// Returns the EIP-2718 type flag.
272    fn ty(&self) -> u8;
273
274    /// Returns true if the type matches the given type.
275    fn is_type(&self, ty: u8) -> bool {
276        self.ty() == ty
277    }
278
279    /// Returns true if the type is a legacy transaction.
280    fn is_legacy(&self) -> bool {
281        self.ty() == LEGACY_TX_TYPE_ID
282    }
283
284    /// Returns true if the type is an EIP-2930 transaction.
285    fn is_eip2930(&self) -> bool {
286        self.ty() == EIP2930_TX_TYPE_ID
287    }
288
289    /// Returns true if the type is an EIP-1559 transaction.
290    fn is_eip1559(&self) -> bool {
291        self.ty() == EIP1559_TX_TYPE_ID
292    }
293
294    /// Returns true if the type is an EIP-4844 transaction.
295    fn is_eip4844(&self) -> bool {
296        self.ty() == EIP4844_TX_TYPE_ID
297    }
298
299    /// Returns true if the type is an EIP-7702 transaction.
300    fn is_eip7702(&self) -> bool {
301        self.ty() == EIP7702_TX_TYPE_ID
302    }
303}
304
305#[cfg(feature = "serde")]
306impl<T: Typed2718> Typed2718 for alloy_serde::WithOtherFields<T> {
307    #[inline]
308    fn ty(&self) -> u8 {
309        self.inner.ty()
310    }
311}
312
313/// Generic wrapper with encoded Bytes, such as transaction data.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct WithEncoded<T>(Bytes, pub T);
316
317impl<T> From<(Bytes, T)> for WithEncoded<T> {
318    fn from(value: (Bytes, T)) -> Self {
319        Self(value.0, value.1)
320    }
321}
322
323impl<T> WithEncoded<T> {
324    /// Wraps the value with the bytes.
325    pub const fn new(bytes: Bytes, value: T) -> Self {
326        Self(bytes, value)
327    }
328
329    /// Get the encoded bytes
330    pub const fn encoded_bytes(&self) -> &Bytes {
331        &self.0
332    }
333
334    /// Returns ownership of the encoded bytes.
335    pub fn into_encoded_bytes(self) -> Bytes {
336        self.0
337    }
338
339    /// Get the underlying value
340    pub const fn value(&self) -> &T {
341        &self.1
342    }
343
344    /// Returns ownership of the underlying value.
345    pub fn into_value(self) -> T {
346        self.1
347    }
348
349    /// Transform the value
350    pub fn transform<F: From<T>>(self) -> WithEncoded<F> {
351        WithEncoded(self.0, self.1.into())
352    }
353
354    /// Split the wrapper into [`Bytes`] and value tuple
355    pub fn split(self) -> (Bytes, T) {
356        (self.0, self.1)
357    }
358
359    /// Maps the inner value to a new value using the given function.
360    pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> WithEncoded<U> {
361        WithEncoded(self.0, op(self.1))
362    }
363}
364
365impl<T: Encodable2718> WithEncoded<T> {
366    /// Wraps the value with the [`Encodable2718::encoded_2718`] bytes.
367    pub fn from_2718_encodable(value: T) -> Self {
368        Self(value.encoded_2718().into(), value)
369    }
370}
371
372impl<T> WithEncoded<Option<T>> {
373    /// returns `None` if the inner value is `None`, otherwise returns `Some(WithEncoded<T>)`.
374    pub fn transpose(self) -> Option<WithEncoded<T>> {
375        self.1.map(|v| WithEncoded(self.0, v))
376    }
377}
378
379impl<L: Encodable2718, R: Encodable2718> Encodable2718 for either::Either<L, R> {
380    fn encode_2718_len(&self) -> usize {
381        match self {
382            Self::Left(l) => l.encode_2718_len(),
383            Self::Right(r) => r.encode_2718_len(),
384        }
385    }
386
387    fn encode_2718(&self, out: &mut dyn BufMut) {
388        match self {
389            Self::Left(l) => l.encode_2718(out),
390            Self::Right(r) => r.encode_2718(out),
391        }
392    }
393}
394
395impl<L: Typed2718, R: Typed2718> Typed2718 for either::Either<L, R> {
396    fn ty(&self) -> u8 {
397        match self {
398            Self::Left(l) => l.ty(),
399            Self::Right(r) => r.ty(),
400        }
401    }
402}