eth_valkyoth_protocol/transaction/
envelope.rs1use core::fmt;
2
3use eth_valkyoth_codec::{
4 DecodeError, DecodeErrorCategory, DecodeLimits, RlpList, decode_rlp_list,
5};
6use eth_valkyoth_primitives::TransactionType;
7
8use crate::eip2718::{
9 EIP_2718_MAX_TYPED_PREFIX as SHARED_EIP_2718_MAX_TYPED_PREFIX,
10 EIP_2718_RESERVED_PREFIX as SHARED_EIP_2718_RESERVED_PREFIX,
11 EIP_2718_SCALAR_PREFIX_START as SHARED_EIP_2718_SCALAR_PREFIX_START,
12 EIP_2718_TYPED_ZERO_PREFIX as SHARED_EIP_2718_TYPED_ZERO_PREFIX, Eip2718Prefix,
13 LEGACY_PREFIX_START as SHARED_LEGACY_PREFIX_START, classify_eip2718_prefix,
14};
15
16pub const EIP_2718_TYPED_ZERO_PREFIX: u8 = SHARED_EIP_2718_TYPED_ZERO_PREFIX;
19
20pub const EIP_2718_MAX_TYPED_PREFIX: u8 = SHARED_EIP_2718_MAX_TYPED_PREFIX;
22
23pub const EIP_2718_SCALAR_PREFIX_START: u8 = SHARED_EIP_2718_SCALAR_PREFIX_START;
26
27pub const LEGACY_TRANSACTION_PREFIX_START: u8 = SHARED_LEGACY_PREFIX_START;
29
30pub const EIP_2718_RESERVED_PREFIX: u8 = SHARED_EIP_2718_RESERVED_PREFIX;
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum TransactionEnvelope<'a> {
40 Legacy(RlpList<'a>),
42 Typed(TypedTransactionEnvelope<'a>),
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct TypedTransactionEnvelope<'a> {
49 pub transaction_type: TransactionType,
51 pub payload: &'a [u8],
53}
54
55#[non_exhaustive]
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum TransactionEnvelopeError {
59 EmptyInput,
61 UnsupportedTransactionType {
68 type_byte: u8,
70 },
71 ScalarPrefix {
73 prefix: u8,
75 },
76 ReservedPrefix,
78 Decode(DecodeError),
80}
81
82impl TransactionEnvelopeError {
83 #[must_use]
85 pub const fn code(self) -> &'static str {
86 match self {
87 Self::EmptyInput => "ETH_TX_ENVELOPE_EMPTY_INPUT",
88 Self::UnsupportedTransactionType { .. } => "ETH_TX_ENVELOPE_UNSUPPORTED_TYPE",
89 Self::ScalarPrefix { .. } => "ETH_TX_ENVELOPE_SCALAR_PREFIX",
90 Self::ReservedPrefix => "ETH_TX_ENVELOPE_RESERVED_PREFIX",
91 Self::Decode(error) => error.code(),
92 }
93 }
94
95 #[must_use]
97 pub const fn message(self) -> &'static str {
98 match self {
99 Self::EmptyInput => "transaction envelope input is empty",
100 Self::UnsupportedTransactionType { .. } => {
101 "transaction type is not supported by this envelope shell"
102 }
103 Self::ScalarPrefix { .. } => "transaction envelope starts with an RLP scalar prefix",
104 Self::ReservedPrefix => "transaction envelope starts with the reserved 0xff prefix",
105 Self::Decode(error) => error.message(),
106 }
107 }
108
109 #[must_use]
111 pub const fn category(self) -> TransactionEnvelopeErrorCategory {
112 match self {
113 Self::EmptyInput
114 | Self::ScalarPrefix { .. }
115 | Self::ReservedPrefix
116 | Self::Decode(
117 DecodeError::TrailingBytes
118 | DecodeError::DecoderOverread
119 | DecodeError::Malformed
120 | DecodeError::UnexpectedList
121 | DecodeError::UnexpectedScalar
122 | DecodeError::LengthOverflow
123 | DecodeError::OffsetOutOfBounds,
124 ) => TransactionEnvelopeErrorCategory::MalformedInput,
125 Self::UnsupportedTransactionType { .. } => {
126 TransactionEnvelopeErrorCategory::Unsupported
127 }
128 Self::Decode(error) => match error.category() {
129 DecodeErrorCategory::MalformedInput => {
130 TransactionEnvelopeErrorCategory::MalformedInput
131 }
132 DecodeErrorCategory::ResourceExhaustion => {
133 TransactionEnvelopeErrorCategory::ResourceExhaustion
134 }
135 _ => TransactionEnvelopeErrorCategory::MalformedInput,
136 },
137 }
138 }
139}
140
141impl fmt::Display for TransactionEnvelopeError {
142 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143 formatter.write_str(self.message())
144 }
145}
146
147#[cfg(feature = "std")]
148impl std::error::Error for TransactionEnvelopeError {}
149
150#[non_exhaustive]
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub enum TransactionEnvelopeErrorCategory {
154 MalformedInput,
156 Unsupported,
158 ResourceExhaustion,
160}
161
162pub fn decode_transaction_envelope<'a>(
169 input: &'a [u8],
170 limits: DecodeLimits,
171) -> Result<TransactionEnvelope<'a>, TransactionEnvelopeError> {
172 limits
173 .check_input_len(input.len())
174 .map_err(TransactionEnvelopeError::Decode)?;
175
176 let Some(prefix) = classify_eip2718_prefix(input) else {
177 return Err(TransactionEnvelopeError::EmptyInput);
178 };
179
180 match prefix {
181 Eip2718Prefix::TypedZero => Err(TransactionEnvelopeError::UnsupportedTransactionType {
182 type_byte: EIP_2718_TYPED_ZERO_PREFIX,
183 }),
184 Eip2718Prefix::Typed { type_byte, payload } => {
185 let transaction_type = TransactionType::try_new_typed(type_byte)
186 .map_err(|_| TransactionEnvelopeError::UnsupportedTransactionType { type_byte })?;
187 Ok(TransactionEnvelope::Typed(TypedTransactionEnvelope {
188 transaction_type,
189 payload,
190 }))
191 }
192 Eip2718Prefix::ScalarPrefix { prefix } => {
193 Err(TransactionEnvelopeError::ScalarPrefix { prefix })
194 }
195 Eip2718Prefix::Legacy => {
196 let list = decode_rlp_list(input, limits).map_err(TransactionEnvelopeError::Decode)?;
197 Ok(TransactionEnvelope::Legacy(list))
198 }
199 Eip2718Prefix::Reserved => Err(TransactionEnvelopeError::ReservedPrefix),
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 extern crate std;
207 use std::string::ToString;
208
209 const TEST_LIMITS: DecodeLimits = DecodeLimits {
210 max_input_bytes: 64,
211 max_list_items: 16,
212 max_nesting_depth: 8,
213 max_total_allocation: 64,
214 max_proof_nodes: 4,
215 max_total_items: 32,
216 };
217
218 #[test]
219 fn classifies_typed_transaction_without_decoding_payload() {
220 let envelope = decode_transaction_envelope(&[0x02, 0xc0, 0x01], TEST_LIMITS);
221
222 assert!(matches!(envelope, Ok(TransactionEnvelope::Typed(_))));
223 if let Ok(TransactionEnvelope::Typed(typed)) = envelope {
224 assert_eq!(u8::from(typed.transaction_type), 0x02);
225 assert_eq!(typed.payload, &[0xc0, 0x01]);
226 }
227 }
228
229 #[test]
230 fn classifies_legacy_transaction_as_exact_rlp_list() {
231 let envelope = decode_transaction_envelope(&[0xc0], TEST_LIMITS);
232
233 assert!(matches!(envelope, Ok(TransactionEnvelope::Legacy(_))));
234 if let Ok(TransactionEnvelope::Legacy(list)) = envelope {
235 assert_eq!(list.item_count(), 0);
236 }
237 }
238
239 #[test]
240 fn rejects_legacy_transaction_with_trailing_bytes() {
241 let envelope = decode_transaction_envelope(&[0xc0, 0x80], TEST_LIMITS);
242
243 assert_eq!(
244 envelope,
245 Err(TransactionEnvelopeError::Decode(DecodeError::TrailingBytes))
246 );
247 }
248
249 #[test]
250 fn rejects_empty_input_before_prefix_classification() {
251 assert_eq!(
252 decode_transaction_envelope(&[], TEST_LIMITS),
253 Err(TransactionEnvelopeError::EmptyInput)
254 );
255 }
256
257 #[test]
258 fn rejects_typed_zero_prefix_as_unsupported() {
259 assert_eq!(
260 decode_transaction_envelope(&[0x00], TEST_LIMITS),
261 Err(TransactionEnvelopeError::UnsupportedTransactionType { type_byte: 0 })
262 );
263 }
264
265 #[test]
266 fn rejects_rlp_scalar_prefixes() {
267 assert_eq!(
268 decode_transaction_envelope(&[0x80], TEST_LIMITS),
269 Err(TransactionEnvelopeError::ScalarPrefix { prefix: 0x80 })
270 );
271 }
272
273 #[test]
274 fn rejects_reserved_extension_prefix() {
275 assert_eq!(
276 decode_transaction_envelope(&[0xff], TEST_LIMITS),
277 Err(TransactionEnvelopeError::ReservedPrefix)
278 );
279 }
280
281 #[test]
282 fn enforces_input_budget_for_typed_payloads() {
283 let limits = DecodeLimits {
284 max_input_bytes: 1,
285 ..TEST_LIMITS
286 };
287
288 assert_eq!(
289 decode_transaction_envelope(&[0x02, 0x01], limits),
290 Err(TransactionEnvelopeError::Decode(DecodeError::InputTooLarge))
291 );
292 }
293
294 #[test]
295 fn envelope_errors_have_stable_codes_and_messages() {
296 let error = TransactionEnvelopeError::ReservedPrefix;
297
298 assert_eq!(error.code(), "ETH_TX_ENVELOPE_RESERVED_PREFIX");
299 assert_eq!(
300 error.message(),
301 "transaction envelope starts with the reserved 0xff prefix"
302 );
303 assert_eq!(
304 error.category(),
305 TransactionEnvelopeErrorCategory::MalformedInput
306 );
307 assert_eq!(
308 error.to_string(),
309 "transaction envelope starts with the reserved 0xff prefix"
310 );
311 }
312}