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, 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 an [EIP-2718] transaction in the network format. The network
128 /// format is used ONLY by the Ethereum p2p protocol. Do not call this
129 /// method unless you are building a p2p protocol client.
130 ///
131 /// The network encoding is the RLP encoding of the eip2718-encoded
132 /// envelope.
133 ///
134 /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
135 fn network_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
136 // Keep the original buffer around by copying it.
137 let mut h_decode = *buf;
138 let h = Header::decode(&mut h_decode)?;
139
140 // If it's a list, we need to fallback to the legacy decoding.
141 if h.list {
142 return Self::fallback_decode(buf);
143 }
144 *buf = h_decode;
145
146 let remaining_len = buf.len();
147 if remaining_len == 0 || remaining_len < h.payload_length {
148 return Err(alloy_rlp::Error::InputTooShort.into());
149 }
150
151 let ty = buf.get_u8();
152 let tx = Self::typed_decode(ty, buf)?;
153
154 let bytes_consumed = remaining_len - buf.len();
155 // because Header::decode works for single bytes (including the tx type), returning a
156 // string Header with payload_length of 1, we need to make sure this check is only
157 // performed for transactions with a string header
158 if bytes_consumed != h.payload_length && h_decode[0] > EMPTY_STRING_CODE {
159 return Err(alloy_rlp::Error::UnexpectedLength.into());
160 }
161
162 Ok(tx)
163 }
164}
165
166impl<T: Decodable2718 + Sealable> Decodable2718 for Sealed<T> {
167 fn extract_type_byte(buf: &mut &[u8]) -> Option<u8> {
168 T::extract_type_byte(buf)
169 }
170
171 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
172 T::typed_decode(ty, buf).map(Self::new)
173 }
174
175 fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
176 T::fallback_decode(buf).map(Self::new)
177 }
178
179 fn decode_2718(buf: &mut &[u8]) -> Eip2718Result<Self> {
180 T::decode_2718(buf).map(Self::new)
181 }
182
183 fn network_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
184 T::network_decode(buf).map(Self::new)
185 }
186}
187
188/// Encoding trait for [EIP-2718] envelopes.
189///
190/// These envelopes wrap a transaction or a receipt with a type flag. [EIP-2718] encodings are used
191/// by the `eth_sendRawTransaction` RPC call, the Ethereum block header's tries, and the
192/// peer-to-peer protocol.
193///
194/// Users should rarely import this trait, and should instead prefer letting the
195/// alloy `Provider` methods handle encoding
196///
197/// ## Implementing
198///
199/// Implement this trait when you need to make custom TransactionEnvelope
200/// and ReceiptEnvelope types for your network. These types should be enums
201/// over the accepted transaction types.
202///
203/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
204#[auto_impl(&)]
205pub trait Encodable2718: Typed2718 + Sized + Send + Sync {
206 /// Return the type flag (if any).
207 ///
208 /// This should return `None` for the default (legacy) variant of the
209 /// envelope.
210 fn type_flag(&self) -> Option<u8> {
211 match self.ty() {
212 LEGACY_TX_TYPE_ID => None,
213 ty => Some(ty),
214 }
215 }
216
217 /// The length of the 2718 encoded envelope. This is the length of the type
218 /// flag + the length of the inner encoding.
219 fn encode_2718_len(&self) -> usize;
220
221 /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
222 /// type flag in the range 0x0-0x7f, then the body of the transaction.
223 ///
224 /// [EIP-2718] inner encodings are unspecified, and produce an opaque
225 /// bytestring.
226 ///
227 /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
228 fn encode_2718(&self, out: &mut dyn BufMut);
229
230 /// Encode the transaction according to [EIP-2718] rules. First a 1-byte
231 /// type flag in the range 0x0-0x7f, then the body of the transaction.
232 ///
233 /// This is a convenience method for encoding into a vec, and returning the
234 /// vec.
235 fn encoded_2718(&self) -> Vec<u8> {
236 let mut out = Vec::with_capacity(self.encode_2718_len());
237 self.encode_2718(&mut out);
238 out
239 }
240
241 /// Compute the hash as committed to in the MPT trie. This hash is used
242 /// ONLY by the Ethereum merkle-patricia trie and associated proofs. Do not
243 /// call this method unless you are building a full or light client.
244 ///
245 /// The trie hash is the keccak256 hash of the 2718-encoded envelope.
246 fn trie_hash(&self) -> B256 {
247 keccak256(self.encoded_2718())
248 }
249
250 /// Seal the encodable, by encoding and hashing it.
251 #[auto_impl(keep_default_for(&))]
252 fn seal(self) -> Sealed<Self> {
253 let hash = self.trie_hash();
254 Sealed::new_unchecked(self, hash)
255 }
256
257 /// A convenience function that encodes the value in the 2718 format and wraps it in a
258 /// [`WithEncoded`] wrapper.
259 ///
260 /// See also [`WithEncoded::from_2718_encodable`].
261 #[auto_impl(keep_default_for(&))]
262 fn into_encoded(self) -> WithEncoded<Self> {
263 WithEncoded::from_2718_encodable(self)
264 }
265
266 /// The length of the 2718 encoded envelope in network format. This is the
267 /// length of the header + the length of the type flag and inner encoding.
268 fn network_len(&self) -> usize {
269 let mut payload_length = self.encode_2718_len();
270 if !self.is_legacy() {
271 payload_length += Header { list: false, payload_length }.length();
272 }
273
274 payload_length
275 }
276
277 /// Encode in the network format. The network format is used ONLY by the
278 /// Ethereum p2p protocol. Do not call this method unless you are building
279 /// a p2p protocol client.
280 ///
281 /// The network encoding is the RLP encoding of the eip2718-encoded
282 /// envelope.
283 fn network_encode(&self, out: &mut dyn BufMut) {
284 if !self.is_legacy() {
285 Header { list: false, payload_length: self.encode_2718_len() }.encode(out);
286 }
287
288 self.encode_2718(out);
289 }
290}
291
292impl<T: Encodable2718> Encodable2718 for Sealed<T> {
293 fn encode_2718_len(&self) -> usize {
294 self.inner().encode_2718_len()
295 }
296
297 fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
298 self.inner().encode_2718(out);
299 }
300
301 fn trie_hash(&self) -> B256 {
302 self.hash()
303 }
304}
305
306/// An [EIP-2718] envelope, blanket implemented for types that impl [`Encodable2718`] and
307/// [`Decodable2718`].
308///
309/// This envelope is a wrapper around a transaction, or a receipt, or any other type that is
310/// differentiated by an EIP-2718 transaction type.
311///
312/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718
313pub trait Eip2718Envelope: Decodable2718 + Encodable2718 {}
314impl<T> Eip2718Envelope for T where T: Decodable2718 + Encodable2718 {}
315
316/// A trait that helps to determine the type of the transaction.
317#[auto_impl::auto_impl(&)]
318pub trait Typed2718 {
319 /// Returns the EIP-2718 type flag.
320 fn ty(&self) -> u8;
321
322 /// Returns true if the type matches the given type.
323 fn is_type(&self, ty: u8) -> bool {
324 self.ty() == ty
325 }
326
327 /// Returns true if the type is a legacy transaction.
328 fn is_legacy(&self) -> bool {
329 self.ty() == LEGACY_TX_TYPE_ID
330 }
331
332 /// Returns true if the type is an EIP-2930 transaction.
333 fn is_eip2930(&self) -> bool {
334 self.ty() == EIP2930_TX_TYPE_ID
335 }
336
337 /// Returns true if the type is an EIP-1559 transaction.
338 fn is_eip1559(&self) -> bool {
339 self.ty() == EIP1559_TX_TYPE_ID
340 }
341
342 /// Returns true if the type is an EIP-4844 transaction.
343 fn is_eip4844(&self) -> bool {
344 self.ty() == EIP4844_TX_TYPE_ID
345 }
346
347 /// Returns true if the type is an EIP-7702 transaction.
348 fn is_eip7702(&self) -> bool {
349 self.ty() == EIP7702_TX_TYPE_ID
350 }
351}
352
353impl<T: Typed2718> Typed2718 for Sealed<T> {
354 fn ty(&self) -> u8 {
355 self.inner().ty()
356 }
357}
358
359#[cfg(feature = "serde")]
360impl<T: Typed2718> Typed2718 for alloy_serde::WithOtherFields<T> {
361 #[inline]
362 fn ty(&self) -> u8 {
363 self.inner.ty()
364 }
365}
366
367/// Generic wrapper with encoded Bytes, such as transaction data.
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct WithEncoded<T>(Bytes, pub T);
370
371impl<T> From<(Bytes, T)> for WithEncoded<T> {
372 fn from(value: (Bytes, T)) -> Self {
373 Self(value.0, value.1)
374 }
375}
376
377impl<T> WithEncoded<T> {
378 /// Wraps the value with the bytes.
379 pub const fn new(bytes: Bytes, value: T) -> Self {
380 Self(bytes, value)
381 }
382
383 /// Get the encoded bytes
384 pub const fn encoded_bytes(&self) -> &Bytes {
385 &self.0
386 }
387
388 /// Returns ownership of the encoded bytes.
389 pub fn into_encoded_bytes(self) -> Bytes {
390 self.0
391 }
392
393 /// Get the underlying value
394 pub const fn value(&self) -> &T {
395 &self.1
396 }
397
398 /// Returns ownership of the underlying value.
399 pub fn into_value(self) -> T {
400 self.1
401 }
402
403 /// Transform the value
404 pub fn transform<F: From<T>>(self) -> WithEncoded<F> {
405 WithEncoded(self.0, self.1.into())
406 }
407
408 /// Split the wrapper into [`Bytes`] and value tuple
409 pub fn split(self) -> (Bytes, T) {
410 (self.0, self.1)
411 }
412
413 /// Maps the inner value to a new value using the given function.
414 pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> WithEncoded<U> {
415 WithEncoded(self.0, op(self.1))
416 }
417}
418
419impl<T: Encodable2718> WithEncoded<T> {
420 /// Wraps the value with the [`Encodable2718::encoded_2718`] bytes.
421 pub fn from_2718_encodable(value: T) -> Self {
422 Self(value.encoded_2718().into(), value)
423 }
424}
425
426impl<T> WithEncoded<Option<T>> {
427 /// returns `None` if the inner value is `None`, otherwise returns `Some(WithEncoded<T>)`.
428 pub fn transpose(self) -> Option<WithEncoded<T>> {
429 self.1.map(|v| WithEncoded(self.0, v))
430 }
431}
432
433impl<L: Encodable2718, R: Encodable2718> Encodable2718 for either::Either<L, R> {
434 fn encode_2718_len(&self) -> usize {
435 match self {
436 Self::Left(l) => l.encode_2718_len(),
437 Self::Right(r) => r.encode_2718_len(),
438 }
439 }
440
441 fn encode_2718(&self, out: &mut dyn BufMut) {
442 match self {
443 Self::Left(l) => l.encode_2718(out),
444 Self::Right(r) => r.encode_2718(out),
445 }
446 }
447}
448
449impl<L: Typed2718, R: Typed2718> Typed2718 for either::Either<L, R> {
450 fn ty(&self) -> u8 {
451 match self {
452 Self::Left(l) => l.ty(),
453 Self::Right(r) => r.ty(),
454 }
455 }
456}
457
458/// Trait for checking if a transaction envelope supports a given EIP-2718 type ID.
459pub trait IsTyped2718 {
460 /// Returns true if the given type ID corresponds to a supported typed transaction.
461 fn is_type(type_id: u8) -> bool;
462}
463
464impl<L, R> IsTyped2718 for either::Either<L, R>
465where
466 L: IsTyped2718,
467 R: IsTyped2718,
468{
469 fn is_type(type_id: u8) -> bool {
470 L::is_type(type_id) || R::is_type(type_id)
471 }
472}
473
474impl<L, R> Decodable2718 for either::Either<L, R>
475where
476 L: Decodable2718 + IsTyped2718,
477 R: Decodable2718,
478{
479 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
480 if L::is_type(ty) {
481 let envelope = L::typed_decode(ty, buf)?;
482 Ok(Self::Left(envelope))
483 } else {
484 let other = R::typed_decode(ty, buf)?;
485 Ok(Self::Right(other))
486 }
487 }
488 fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
489 if buf.is_empty() {
490 return Err(Eip2718Error::RlpError(alloy_rlp::Error::InputTooShort));
491 }
492 L::fallback_decode(buf).map(Self::Left)
493 }
494}