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