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}