use core::fmt;
use eth_valkyoth_codec::{
DecodeError, DecodeErrorCategory, DecodeLimits, RlpList, decode_rlp_list,
};
use eth_valkyoth_primitives::TransactionType;
use crate::eip2718::{
EIP_2718_MAX_TYPED_PREFIX as SHARED_EIP_2718_MAX_TYPED_PREFIX,
EIP_2718_RESERVED_PREFIX as SHARED_EIP_2718_RESERVED_PREFIX,
EIP_2718_SCALAR_PREFIX_START as SHARED_EIP_2718_SCALAR_PREFIX_START,
EIP_2718_TYPED_ZERO_PREFIX as SHARED_EIP_2718_TYPED_ZERO_PREFIX, Eip2718Prefix,
LEGACY_PREFIX_START as SHARED_LEGACY_PREFIX_START, classify_eip2718_prefix,
};
pub const EIP_2718_TYPED_ZERO_PREFIX: u8 = SHARED_EIP_2718_TYPED_ZERO_PREFIX;
pub const EIP_2718_MAX_TYPED_PREFIX: u8 = SHARED_EIP_2718_MAX_TYPED_PREFIX;
pub const EIP_2718_SCALAR_PREFIX_START: u8 = SHARED_EIP_2718_SCALAR_PREFIX_START;
pub const LEGACY_TRANSACTION_PREFIX_START: u8 = SHARED_LEGACY_PREFIX_START;
pub const EIP_2718_RESERVED_PREFIX: u8 = SHARED_EIP_2718_RESERVED_PREFIX;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransactionEnvelope<'a> {
Legacy(RlpList<'a>),
Typed(TypedTransactionEnvelope<'a>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TypedTransactionEnvelope<'a> {
pub transaction_type: TransactionType,
pub payload: &'a [u8],
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransactionEnvelopeError {
EmptyInput,
UnsupportedTransactionType {
type_byte: u8,
},
ScalarPrefix {
prefix: u8,
},
ReservedPrefix,
Decode(DecodeError),
}
impl TransactionEnvelopeError {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::EmptyInput => "ETH_TX_ENVELOPE_EMPTY_INPUT",
Self::UnsupportedTransactionType { .. } => "ETH_TX_ENVELOPE_UNSUPPORTED_TYPE",
Self::ScalarPrefix { .. } => "ETH_TX_ENVELOPE_SCALAR_PREFIX",
Self::ReservedPrefix => "ETH_TX_ENVELOPE_RESERVED_PREFIX",
Self::Decode(error) => error.code(),
}
}
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::EmptyInput => "transaction envelope input is empty",
Self::UnsupportedTransactionType { .. } => {
"transaction type is not supported by this envelope shell"
}
Self::ScalarPrefix { .. } => "transaction envelope starts with an RLP scalar prefix",
Self::ReservedPrefix => "transaction envelope starts with the reserved 0xff prefix",
Self::Decode(error) => error.message(),
}
}
#[must_use]
pub const fn category(self) -> TransactionEnvelopeErrorCategory {
match self {
Self::EmptyInput
| Self::ScalarPrefix { .. }
| Self::ReservedPrefix
| Self::Decode(
DecodeError::TrailingBytes
| DecodeError::DecoderOverread
| DecodeError::Malformed
| DecodeError::UnexpectedList
| DecodeError::UnexpectedScalar
| DecodeError::LengthOverflow
| DecodeError::OffsetOutOfBounds,
) => TransactionEnvelopeErrorCategory::MalformedInput,
Self::UnsupportedTransactionType { .. } => {
TransactionEnvelopeErrorCategory::Unsupported
}
Self::Decode(error) => match error.category() {
DecodeErrorCategory::MalformedInput => {
TransactionEnvelopeErrorCategory::MalformedInput
}
DecodeErrorCategory::ResourceExhaustion => {
TransactionEnvelopeErrorCategory::ResourceExhaustion
}
_ => TransactionEnvelopeErrorCategory::MalformedInput,
},
}
}
}
impl fmt::Display for TransactionEnvelopeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.message())
}
}
#[cfg(feature = "std")]
impl std::error::Error for TransactionEnvelopeError {}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransactionEnvelopeErrorCategory {
MalformedInput,
Unsupported,
ResourceExhaustion,
}
pub fn decode_transaction_envelope<'a>(
input: &'a [u8],
limits: DecodeLimits,
) -> Result<TransactionEnvelope<'a>, TransactionEnvelopeError> {
limits
.check_input_len(input.len())
.map_err(TransactionEnvelopeError::Decode)?;
let Some(prefix) = classify_eip2718_prefix(input) else {
return Err(TransactionEnvelopeError::EmptyInput);
};
match prefix {
Eip2718Prefix::TypedZero => Err(TransactionEnvelopeError::UnsupportedTransactionType {
type_byte: EIP_2718_TYPED_ZERO_PREFIX,
}),
Eip2718Prefix::Typed { type_byte, payload } => {
let transaction_type = TransactionType::try_new_typed(type_byte)
.map_err(|_| TransactionEnvelopeError::UnsupportedTransactionType { type_byte })?;
Ok(TransactionEnvelope::Typed(TypedTransactionEnvelope {
transaction_type,
payload,
}))
}
Eip2718Prefix::ScalarPrefix { prefix } => {
Err(TransactionEnvelopeError::ScalarPrefix { prefix })
}
Eip2718Prefix::Legacy => {
let list = decode_rlp_list(input, limits).map_err(TransactionEnvelopeError::Decode)?;
Ok(TransactionEnvelope::Legacy(list))
}
Eip2718Prefix::Reserved => Err(TransactionEnvelopeError::ReservedPrefix),
}
}
#[cfg(test)]
mod tests {
use super::*;
extern crate std;
use std::string::ToString;
const TEST_LIMITS: DecodeLimits = DecodeLimits {
max_input_bytes: 64,
max_list_items: 16,
max_nesting_depth: 8,
max_total_allocation: 64,
max_proof_nodes: 4,
max_total_items: 32,
};
#[test]
fn classifies_typed_transaction_without_decoding_payload() {
let envelope = decode_transaction_envelope(&[0x02, 0xc0, 0x01], TEST_LIMITS);
assert!(matches!(envelope, Ok(TransactionEnvelope::Typed(_))));
if let Ok(TransactionEnvelope::Typed(typed)) = envelope {
assert_eq!(u8::from(typed.transaction_type), 0x02);
assert_eq!(typed.payload, &[0xc0, 0x01]);
}
}
#[test]
fn classifies_legacy_transaction_as_exact_rlp_list() {
let envelope = decode_transaction_envelope(&[0xc0], TEST_LIMITS);
assert!(matches!(envelope, Ok(TransactionEnvelope::Legacy(_))));
if let Ok(TransactionEnvelope::Legacy(list)) = envelope {
assert_eq!(list.item_count(), 0);
}
}
#[test]
fn rejects_legacy_transaction_with_trailing_bytes() {
let envelope = decode_transaction_envelope(&[0xc0, 0x80], TEST_LIMITS);
assert_eq!(
envelope,
Err(TransactionEnvelopeError::Decode(DecodeError::TrailingBytes))
);
}
#[test]
fn rejects_empty_input_before_prefix_classification() {
assert_eq!(
decode_transaction_envelope(&[], TEST_LIMITS),
Err(TransactionEnvelopeError::EmptyInput)
);
}
#[test]
fn rejects_typed_zero_prefix_as_unsupported() {
assert_eq!(
decode_transaction_envelope(&[0x00], TEST_LIMITS),
Err(TransactionEnvelopeError::UnsupportedTransactionType { type_byte: 0 })
);
}
#[test]
fn rejects_rlp_scalar_prefixes() {
assert_eq!(
decode_transaction_envelope(&[0x80], TEST_LIMITS),
Err(TransactionEnvelopeError::ScalarPrefix { prefix: 0x80 })
);
}
#[test]
fn rejects_reserved_extension_prefix() {
assert_eq!(
decode_transaction_envelope(&[0xff], TEST_LIMITS),
Err(TransactionEnvelopeError::ReservedPrefix)
);
}
#[test]
fn enforces_input_budget_for_typed_payloads() {
let limits = DecodeLimits {
max_input_bytes: 1,
..TEST_LIMITS
};
assert_eq!(
decode_transaction_envelope(&[0x02, 0x01], limits),
Err(TransactionEnvelopeError::Decode(DecodeError::InputTooLarge))
);
}
#[test]
fn envelope_errors_have_stable_codes_and_messages() {
let error = TransactionEnvelopeError::ReservedPrefix;
assert_eq!(error.code(), "ETH_TX_ENVELOPE_RESERVED_PREFIX");
assert_eq!(
error.message(),
"transaction envelope starts with the reserved 0xff prefix"
);
assert_eq!(
error.category(),
TransactionEnvelopeErrorCategory::MalformedInput
);
assert_eq!(
error.to_string(),
"transaction envelope starts with the reserved 0xff prefix"
);
}
}