Skip to main content

alloy_eips/
eip2718.rs

1//! [EIP-2718] traits.
2//!
3//! [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
4
5use alloc::{borrow::Cow, vec::Vec};
6use alloy_primitives::{keccak256, Bytes, Sealable, Sealed, B256};
7use alloy_rlp::{Buf, BufMut, Header, EMPTY_STRING_CODE};
8use auto_impl::auto_impl;
9use core::fmt;
10
11// https://eips.ethereum.org/EIPS/eip-2718#transactiontype-only-goes-up-to-0x7f
12const TX_TYPE_BYTE_MAX: u8 = 0x7f;
13
14/// Identifier for legacy transaction, however a legacy tx is technically not
15/// typed.
16pub const LEGACY_TX_TYPE_ID: u8 = 0;
17
18/// Identifier for an EIP2930 transaction.
19pub const EIP2930_TX_TYPE_ID: u8 = 1;
20
21/// Identifier for an EIP1559 transaction.
22pub const EIP1559_TX_TYPE_ID: u8 = 2;
23
24/// Identifier for an EIP4844 transaction.
25pub const EIP4844_TX_TYPE_ID: u8 = 3;
26
27/// Identifier for an EIP7702 transaction.
28pub const EIP7702_TX_TYPE_ID: u8 = 4;
29
30/// [EIP-2718] decoding errors.
31///
32/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
33#[derive(Clone, Copy, Debug)]
34#[non_exhaustive] // NB: non-exhaustive allows us to add a Custom variant later
35pub enum Eip2718Error {
36    /// Rlp error from [`alloy_rlp`].
37    RlpError(alloy_rlp::Error),
38    /// Got an unexpected type flag while decoding.
39    UnexpectedType(u8),
40}
41
42/// Result type for [EIP-2718] decoding.
43pub type Eip2718Result<T, E = Eip2718Error> = core::result::Result<T, E>;
44
45impl fmt::Display for Eip2718Error {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::RlpError(err) => write!(f, "{err}"),
49            Self::UnexpectedType(t) => write!(f, "Unexpected type flag. Got {t}."),
50        }
51    }
52}
53
54impl From<alloy_rlp::Error> for Eip2718Error {
55    fn from(err: alloy_rlp::Error) -> Self {
56        Self::RlpError(err)
57    }
58}
59
60impl From<Eip2718Error> for alloy_rlp::Error {
61    fn from(err: Eip2718Error) -> Self {
62        match err {
63            Eip2718Error::RlpError(err) => err,
64            Eip2718Error::UnexpectedType(_) => Self::Custom("Unexpected type flag"),
65        }
66    }
67}
68
69impl core::error::Error for Eip2718Error {}
70
71/// Decoding trait for [EIP-2718] envelopes. These envelopes wrap a transaction
72/// or a receipt with a type flag.
73///
74/// Users should rarely import this trait, and should instead prefer letting the
75/// alloy `Provider` methods handle encoding
76///
77/// ## Implementing
78///
79/// Implement this trait when you need to make custom TransactionEnvelope
80/// and ReceiptEnvelope types for your network. These types should be enums
81/// over the accepted transaction types.
82///
83/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
84pub trait Decodable2718: Sized {
85    /// Extract the type byte from the buffer, if any. The type byte is the
86    /// first byte, provided that first byte is 0x7f or lower.
87    fn extract_type_byte(buf: &mut &[u8]) -> Option<u8> {
88        buf.first().copied().filter(|b| *b <= TX_TYPE_BYTE_MAX)
89    }
90
91    /// Decode the appropriate variant, based on the type flag.
92    ///
93    /// This function is invoked by [`Self::decode_2718`] with the type byte,
94    /// and the tail of the buffer.
95    ///
96    /// ## Implementing
97    ///
98    /// This should be a simple match block that invokes an inner type's
99    /// specific decoder.
100    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self>;
101
102    /// Decode the default variant.
103    ///
104    /// ## Implementing
105    ///
106    /// This function is invoked by [`Self::decode_2718`] when no type byte can
107    /// be extracted. It should be a simple wrapper around the default type's
108    /// decoder.
109    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self>;
110
111    /// Decode the transaction according to [EIP-2718] rules. First a 1-byte
112    /// type flag in the range 0x0-0x7f, then the body of the transaction.
113    ///
114    /// [EIP-2718] inner encodings are unspecified, and produce an opaque
115    /// bytestring.
116    ///
117    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
118    fn decode_2718(buf: &mut &[u8]) -> Eip2718Result<Self> {
119        Self::extract_type_byte(buf)
120            .map(|ty| {
121                buf.advance(1);
122                Self::typed_decode(ty, buf)
123            })
124            .unwrap_or_else(|| Self::fallback_decode(buf))
125    }
126
127    /// Decode a transaction according to [EIP-2718], ensuring no trailing bytes.
128    ///
129    /// This method decodes a single transaction from the entire buffer and ensures that the
130    /// buffer is completely consumed. If there are any trailing bytes after the transaction
131    /// data, an error is returned.
132    ///
133    /// This is different from [`decode_2718`](Self::decode_2718) which allows trailing bytes
134    /// in the buffer. This method is useful when you need to ensure that the input contains
135    /// exactly one transaction and nothing else.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if:
140    /// - The transaction data is invalid
141    /// - There are trailing bytes after the transaction
142    ///
143    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
144    fn decode_2718_exact(bytes: &[u8]) -> Eip2718Result<Self> {
145        let mut buf = bytes;
146        let tx = Self::decode_2718(&mut buf)?;
147        if !buf.is_empty() {
148            return Err(Eip2718Error::RlpError(alloy_rlp::Error::UnexpectedLength));
149        }
150        Ok(tx)
151    }
152
153    /// Decode an [EIP-2718] transaction in the network format. The network
154    /// format is used ONLY by the Ethereum p2p protocol. Do not call this
155    /// method unless you are building a p2p protocol client.
156    ///
157    /// Canonical network encoding wraps a typed envelope (`type || payload`) in an RLP string;
158    /// legacy envelopes retain their RLP list encoding.
159    ///
160    /// For backwards compatibility, the default implementation also accepts typed envelopes
161    /// without the RLP string wrapper. Successful decoding therefore does not establish canonical
162    /// network encoding, and re-encoding may produce different bytes. Requiring the wrapper would
163    /// be a breaking change for callers relying on this behavior. Use [`Self::decode_2718`] when
164    /// decoding the direct EIP-2718 format.
165    ///
166    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
167    fn network_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
168        // Keep the original buffer around by copying it.
169        let mut h_decode = *buf;
170        let h = Header::decode(&mut h_decode)?;
171
172        // If it's a list, we need to fallback to the legacy decoding.
173        if h.list {
174            return Self::fallback_decode(buf);
175        }
176        *buf = h_decode;
177
178        let remaining_len = buf.len();
179        if remaining_len == 0 || remaining_len < h.payload_length {
180            return Err(alloy_rlp::Error::InputTooShort.into());
181        }
182
183        let ty = buf.get_u8();
184        let tx = Self::typed_decode(ty, buf)?;
185
186        let bytes_consumed = remaining_len - buf.len();
187        // Header::decode also accepts a bare type byte as a one-byte RLP string. Preserve
188        // acceptance of these unwrapped typed envelopes for backwards compatibility; rejecting
189        // them here would be a breaking change.
190        if bytes_consumed != h.payload_length && h_decode[0] > EMPTY_STRING_CODE {
191            return Err(alloy_rlp::Error::UnexpectedLength.into());
192        }
193
194        Ok(tx)
195    }
196}
197
198impl<T: Decodable2718 + Sealable> Decodable2718 for Sealed<T> {
199    fn extract_type_byte(buf: &mut &[u8]) -> Option<u8> {
200        T::extract_type_byte(buf)
201    }
202
203    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
204        T::typed_decode(ty, buf).map(Self::new)
205    }
206
207    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
208        T::fallback_decode(buf).map(Self::new)
209    }
210
211    fn decode_2718(buf: &mut &[u8]) -> Eip2718Result<Self> {
212        T::decode_2718(buf).map(Self::new)
213    }
214
215    fn network_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
216        T::network_decode(buf).map(Self::new)
217    }
218}
219
220/// Encoding trait for [EIP-2718] envelopes.
221///
222/// These envelopes wrap a transaction or a receipt with a type flag. [EIP-2718] encodings are used
223/// by the `eth_sendRawTransaction` RPC call, the Ethereum block header's tries, and the
224/// peer-to-peer protocol.
225///
226/// Users should rarely import this trait, and should instead prefer letting the
227/// alloy `Provider` methods handle encoding
228///
229/// ## Implementing
230///
231/// Implement this trait when you need to make custom TransactionEnvelope
232/// and ReceiptEnvelope types for your network. These types should be enums
233/// over the accepted transaction types.
234///
235/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
236#[auto_impl(&)]
237pub trait Encodable2718: Typed2718 + Sized + Send + Sync {
238    /// Return the type flag (if any).
239    ///
240    /// This should return `None` for the default (legacy) variant of the
241    /// envelope.
242    fn type_flag(&self) -> Option<u8> {
243        match self.ty() {
244            LEGACY_TX_TYPE_ID => None,
245            ty => Some(ty),
246        }
247    }
248
249    /// The length of the 2718 encoded envelope. This is the length of the type
250    /// flag + the length of the inner encoding.
251    fn encode_2718_len(&self) -> usize;
252
253    /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
254    /// type flag in the range 0x0-0x7f, then the body of the transaction.
255    ///
256    /// [EIP-2718] inner encodings are unspecified, and produce an opaque
257    /// bytestring.
258    ///
259    /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
260    fn encode_2718(&self, out: &mut dyn BufMut);
261
262    /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
263    /// type flag in the range 0x0-0x7f, then the body of the transaction.
264    ///
265    /// This is a convenience method for encoding into a vec, and returning the
266    /// vec.
267    fn encoded_2718(&self) -> Vec<u8> {
268        let mut out = Vec::with_capacity(self.encode_2718_len());
269        self.encode_2718(&mut out);
270        out
271    }
272
273    /// Compute the hash as committed to in the MPT trie. This hash is used
274    /// ONLY by the Ethereum merkle-patricia trie and associated proofs. Do not
275    /// call this method unless you are building a full or light client.
276    ///
277    /// The trie hash is the keccak256 hash of the 2718-encoded envelope.
278    fn trie_hash(&self) -> B256 {
279        keccak256(self.encoded_2718())
280    }
281
282    /// Seal the encodable, by encoding and hashing it.
283    #[auto_impl(keep_default_for(&))]
284    fn seal(self) -> Sealed<Self> {
285        let hash = self.trie_hash();
286        Sealed::new_unchecked(self, hash)
287    }
288
289    /// A convenience function that encodes the value in the 2718 format and wraps it in a
290    /// [`WithEncoded`] wrapper.
291    ///
292    /// See also [`WithEncoded::from_2718_encodable`].
293    #[auto_impl(keep_default_for(&))]
294    fn into_encoded(self) -> WithEncoded<Self> {
295        WithEncoded::from_2718_encodable(self)
296    }
297
298    /// The length of the 2718 encoded envelope in network format. This is the
299    /// length of the header + the length of the type flag and inner encoding.
300    fn network_len(&self) -> usize {
301        let mut payload_length = self.encode_2718_len();
302        if !self.is_legacy() {
303            payload_length += Header { list: false, payload_length }.length();
304        }
305
306        payload_length
307    }
308
309    /// Encode in the network format. The network format is used ONLY by the
310    /// Ethereum p2p protocol. Do not call this method unless you are building
311    /// a p2p protocol client.
312    ///
313    /// The network encoding is the RLP encoding of the eip2718-encoded
314    /// envelope.
315    fn network_encode(&self, out: &mut dyn BufMut) {
316        if !self.is_legacy() {
317            Header { list: false, payload_length: self.encode_2718_len() }.encode(out);
318        }
319
320        self.encode_2718(out);
321    }
322}
323
324impl<T: Encodable2718> Encodable2718 for Sealed<T> {
325    fn encode_2718_len(&self) -> usize {
326        self.inner().encode_2718_len()
327    }
328
329    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
330        self.inner().encode_2718(out);
331    }
332
333    fn trie_hash(&self) -> B256 {
334        self.hash()
335    }
336}
337
338impl<T: Encodable2718 + Clone> Encodable2718 for Cow<'_, T> {
339    fn encode_2718_len(&self) -> usize {
340        (**self).encode_2718_len()
341    }
342
343    fn encode_2718(&self, out: &mut dyn BufMut) {
344        (**self).encode_2718(out)
345    }
346
347    fn trie_hash(&self) -> B256 {
348        (**self).trie_hash()
349    }
350}
351
352/// An [EIP-2718] envelope, blanket implemented for types that impl [`Encodable2718`] and
353/// [`Decodable2718`].
354///
355/// This envelope is a wrapper around a transaction, or a receipt, or any other type that is
356/// differentiated by an EIP-2718 transaction type.
357///
358/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
359pub trait Eip2718Envelope: Decodable2718 + Encodable2718 {}
360impl<T> Eip2718Envelope for T where T: Decodable2718 + Encodable2718 {}
361
362/// A trait that helps to determine the type of the transaction.
363#[auto_impl::auto_impl(&)]
364pub trait Typed2718 {
365    /// Returns the EIP-2718 type flag.
366    fn ty(&self) -> u8;
367
368    /// Returns true if the type matches the given type.
369    fn is_type(&self, ty: u8) -> bool {
370        self.ty() == ty
371    }
372
373    /// Returns true if the type is a legacy transaction.
374    fn is_legacy(&self) -> bool {
375        self.ty() == LEGACY_TX_TYPE_ID
376    }
377
378    /// Returns true if the type is an EIP-2930 transaction.
379    fn is_eip2930(&self) -> bool {
380        self.ty() == EIP2930_TX_TYPE_ID
381    }
382
383    /// Returns true if the type is an EIP-1559 transaction.
384    fn is_eip1559(&self) -> bool {
385        self.ty() == EIP1559_TX_TYPE_ID
386    }
387
388    /// Returns true if the type is an EIP-4844 transaction.
389    fn is_eip4844(&self) -> bool {
390        self.ty() == EIP4844_TX_TYPE_ID
391    }
392
393    /// Returns true if the type is an EIP-7702 transaction.
394    fn is_eip7702(&self) -> bool {
395        self.ty() == EIP7702_TX_TYPE_ID
396    }
397}
398
399impl<T: Typed2718> Typed2718 for Sealed<T> {
400    fn ty(&self) -> u8 {
401        self.inner().ty()
402    }
403}
404
405impl<T: Typed2718 + Clone> Typed2718 for Cow<'_, T> {
406    fn ty(&self) -> u8 {
407        (**self).ty()
408    }
409}
410
411#[cfg(feature = "serde")]
412impl<T: Typed2718> Typed2718 for alloy_serde::WithOtherFields<T> {
413    #[inline]
414    fn ty(&self) -> u8 {
415        self.inner.ty()
416    }
417}
418
419/// A value paired with bytes presumed to be its exact EIP-2718 encoding.
420///
421/// [`Self::new`] does not verify that the value and bytes correspond. Mapping and transforming
422/// preserve the bytes unchanged and are correct only for encoding-preserving conversions. Prefer
423/// [`Self::from_2718_encodable`] when constructing this wrapper from a value.
424#[derive(Debug, Clone, PartialEq, Eq)]
425pub struct WithEncoded<T>(Bytes, pub T);
426
427impl<T> From<(Bytes, T)> for WithEncoded<T> {
428    fn from(value: (Bytes, T)) -> Self {
429        Self(value.0, value.1)
430    }
431}
432
433impl<T> WithEncoded<T> {
434    /// Wraps the value with caller-supplied bytes without verifying that they correspond.
435    pub const fn new(bytes: Bytes, value: T) -> Self {
436        Self(bytes, value)
437    }
438
439    /// Get the encoded bytes
440    pub const fn encoded_bytes(&self) -> &Bytes {
441        &self.0
442    }
443
444    /// Returns ownership of the encoded bytes.
445    pub fn into_encoded_bytes(self) -> Bytes {
446        self.0
447    }
448
449    /// Get the underlying value
450    pub const fn value(&self) -> &T {
451        &self.1
452    }
453
454    /// Returns ownership of the underlying value.
455    pub fn into_value(self) -> T {
456        self.1
457    }
458
459    /// Transforms the value while retaining the encoded bytes unchanged.
460    pub fn transform<F: From<T>>(self) -> WithEncoded<F> {
461        WithEncoded(self.0, self.1.into())
462    }
463
464    /// Split the wrapper into [`Bytes`] and value tuple
465    pub fn split(self) -> (Bytes, T) {
466        (self.0, self.1)
467    }
468
469    /// Maps the inner value while retaining the encoded bytes unchanged.
470    pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> WithEncoded<U> {
471        WithEncoded(self.0, op(self.1))
472    }
473}
474
475impl<T> AsRef<Self> for WithEncoded<T> {
476    fn as_ref(&self) -> &Self {
477        self
478    }
479}
480
481impl<T: Encodable2718> WithEncoded<T> {
482    /// Wraps the value with the [`Encodable2718::encoded_2718`] bytes.
483    pub fn from_2718_encodable(value: T) -> Self {
484        Self(value.encoded_2718().into(), value)
485    }
486}
487
488impl<T> WithEncoded<Option<T>> {
489    /// Returns `None` if the inner value is `None`; otherwise retains the encoded bytes unchanged.
490    pub fn transpose(self) -> Option<WithEncoded<T>> {
491        self.1.map(|v| WithEncoded(self.0, v))
492    }
493}
494
495impl<L: Encodable2718, R: Encodable2718> Encodable2718 for either::Either<L, R> {
496    fn encode_2718_len(&self) -> usize {
497        match self {
498            Self::Left(l) => l.encode_2718_len(),
499            Self::Right(r) => r.encode_2718_len(),
500        }
501    }
502
503    fn encode_2718(&self, out: &mut dyn BufMut) {
504        match self {
505            Self::Left(l) => l.encode_2718(out),
506            Self::Right(r) => r.encode_2718(out),
507        }
508    }
509}
510
511impl<L: Typed2718, R: Typed2718> Typed2718 for either::Either<L, R> {
512    fn ty(&self) -> u8 {
513        match self {
514            Self::Left(l) => l.ty(),
515            Self::Right(r) => r.ty(),
516        }
517    }
518}
519
520/// Trait for checking if a transaction envelope supports a given EIP-2718 type ID.
521pub trait IsTyped2718 {
522    /// Returns true if the given type ID corresponds to a supported typed transaction.
523    fn is_type(type_id: u8) -> bool;
524}
525
526impl<L, R> IsTyped2718 for either::Either<L, R>
527where
528    L: IsTyped2718,
529    R: IsTyped2718,
530{
531    fn is_type(type_id: u8) -> bool {
532        L::is_type(type_id) || R::is_type(type_id)
533    }
534}
535
536impl<L, R> Decodable2718 for either::Either<L, R>
537where
538    L: Decodable2718 + IsTyped2718,
539    R: Decodable2718,
540{
541    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
542        if L::is_type(ty) {
543            let envelope = L::typed_decode(ty, buf)?;
544            Ok(Self::Left(envelope))
545        } else {
546            let other = R::typed_decode(ty, buf)?;
547            Ok(Self::Right(other))
548        }
549    }
550    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
551        if buf.is_empty() {
552            return Err(Eip2718Error::RlpError(alloy_rlp::Error::InputTooShort));
553        }
554        L::fallback_decode(buf).map(Self::Left)
555    }
556}