use eth_valkyoth_codec::{DecodeError, DecodeLimits, RlpInteger, RlpItem, RlpList, RlpScalar};
use eth_valkyoth_primitives::{Address, B256, Gas, 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,
};
mod error;
pub use error::{ReceiptDecodeError, ReceiptDecodeErrorCategory, ReceiptField};
const ADDRESS_BYTES: usize = 20;
const B256_BYTES: usize = 32;
const BLOOM_BYTES: usize = 256;
const LOG_FIELD_COUNT: usize = 3;
pub const RECEIPT_FIELD_COUNT: usize = 4;
pub const EIP_2718_TYPED_ZERO_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_TYPED_ZERO_PREFIX;
pub const EIP_2718_MAX_TYPED_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_MAX_TYPED_PREFIX;
pub const EIP_2718_RECEIPT_SCALAR_PREFIX_START: u8 = SHARED_EIP_2718_SCALAR_PREFIX_START;
pub const LEGACY_RECEIPT_PREFIX_START: u8 = SHARED_LEGACY_PREFIX_START;
pub const EIP_2718_RESERVED_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_RESERVED_PREFIX;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReceiptEnvelope<'a> {
Legacy(RlpList<'a>),
Typed(TypedReceiptEnvelope<'a>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TypedReceiptEnvelope<'a> {
pub receipt_type: TransactionType,
pub payload: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UnvalidatedReceipt<'a> {
encoded_payload: &'a [u8],
pub envelope: ReceiptKind,
pub status_or_state_root: ReceiptStatusOrStateRoot,
pub cumulative_gas_used: Gas,
pub logs_bloom: ReceiptLogsBloom,
pub logs: ReceiptLogs<'a>,
}
impl UnvalidatedReceipt<'_> {
#[must_use]
pub const fn encoded_payload(&self) -> &[u8] {
self.encoded_payload
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReceiptKind {
Legacy,
Typed(TransactionType),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReceiptStatusOrStateRoot {
Failure,
Success,
StateRoot(B256),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReceiptLogsBloom([u8; BLOOM_BYTES]);
impl ReceiptLogsBloom {
#[must_use]
pub const fn from_bytes(bytes: [u8; BLOOM_BYTES]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn to_bytes(self) -> [u8; BLOOM_BYTES] {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReceiptLogs<'a> {
list: RlpList<'a>,
}
impl<'a> ReceiptLogs<'a> {
#[must_use]
pub const fn len(self) -> usize {
self.list.item_count()
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.list.is_empty()
}
#[must_use]
pub const fn entries(self) -> ReceiptLogEntries<'a> {
ReceiptLogEntries {
items: self.list.items(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReceiptLog<'a> {
pub address: Address,
pub topics: ReceiptLogTopics<'a>,
pub data: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReceiptLogTopics<'a> {
list: RlpList<'a>,
}
impl<'a> ReceiptLogTopics<'a> {
#[must_use]
pub const fn len(self) -> usize {
self.list.item_count()
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.list.is_empty()
}
#[must_use]
pub const fn topics(self) -> ReceiptLogTopicItems<'a> {
ReceiptLogTopicItems {
items: self.list.items(),
}
}
}
#[derive(Clone, Debug)]
pub struct ReceiptLogEntries<'a> {
items: eth_valkyoth_codec::RlpListItems<'a>,
}
impl<'a> Iterator for ReceiptLogEntries<'a> {
type Item = Result<ReceiptLog<'a>, ReceiptDecodeError>;
fn next(&mut self) -> Option<Self::Item> {
self.items.next().map(decode_log_item)
}
}
impl core::iter::FusedIterator for ReceiptLogEntries<'_> {}
#[derive(Clone, Debug)]
pub struct ReceiptLogTopicItems<'a> {
items: eth_valkyoth_codec::RlpListItems<'a>,
}
impl Iterator for ReceiptLogTopicItems<'_> {
type Item = Result<B256, ReceiptDecodeError>;
fn next(&mut self) -> Option<Self::Item> {
self.items.next().map(decode_topic_item)
}
}
impl core::iter::FusedIterator for ReceiptLogTopicItems<'_> {}
pub fn decode_receipt_envelope<'a>(
input: &'a [u8],
limits: DecodeLimits,
) -> Result<ReceiptEnvelope<'a>, ReceiptDecodeError> {
limits
.check_input_len(input.len())
.map_err(|source| field_error(ReceiptField::Payload, source))?;
let Some(prefix) = classify_eip2718_prefix(input) else {
return Err(ReceiptDecodeError::EmptyInput);
};
match prefix {
Eip2718Prefix::TypedZero => Err(ReceiptDecodeError::UnsupportedReceiptType {
type_byte: EIP_2718_TYPED_ZERO_RECEIPT_PREFIX,
}),
Eip2718Prefix::Typed { type_byte, payload } => {
let receipt_type = TransactionType::try_new_typed(type_byte)
.map_err(|_| ReceiptDecodeError::UnsupportedReceiptType { type_byte })?;
Ok(ReceiptEnvelope::Typed(TypedReceiptEnvelope {
receipt_type,
payload,
}))
}
Eip2718Prefix::ScalarPrefix { prefix } => Err(ReceiptDecodeError::ScalarPrefix { prefix }),
Eip2718Prefix::Legacy => {
let list = eth_valkyoth_codec::decode_rlp_list(input, limits)
.map_err(|source| field_error(ReceiptField::Payload, source))?;
Ok(ReceiptEnvelope::Legacy(list))
}
Eip2718Prefix::Reserved => Err(ReceiptDecodeError::ReservedPrefix),
}
}
pub fn decode_receipt<'a>(
input: &'a [u8],
limits: DecodeLimits,
) -> Result<UnvalidatedReceipt<'a>, ReceiptDecodeError> {
match decode_receipt_envelope(input, limits)? {
ReceiptEnvelope::Legacy(list) => decode_receipt_list(list, input, ReceiptKind::Legacy),
ReceiptEnvelope::Typed(typed) => {
let list = eth_valkyoth_codec::decode_rlp_list(typed.payload, limits)
.map_err(|source| field_error(ReceiptField::Payload, source))?;
decode_receipt_list(list, typed.payload, ReceiptKind::Typed(typed.receipt_type))
}
}
}
fn decode_receipt_list<'a>(
list: RlpList<'a>,
encoded_payload: &'a [u8],
envelope: ReceiptKind,
) -> Result<UnvalidatedReceipt<'a>, ReceiptDecodeError> {
if list.item_count() != RECEIPT_FIELD_COUNT {
return Err(ReceiptDecodeError::WrongFieldCount {
expected: RECEIPT_FIELD_COUNT,
found: list.item_count(),
});
}
let mut fields = list.items();
let status_or_state_root =
decode_status_or_root(next_scalar(&mut fields, ReceiptField::StatusOrStateRoot)?)?;
let cumulative_gas_used = Gas::new(decode_u64(&mut fields, ReceiptField::CumulativeGasUsed)?);
let logs_bloom = ReceiptLogsBloom::from_bytes(decode_fixed::<BLOOM_BYTES>(
&mut fields,
ReceiptField::LogsBloom,
)?);
let logs = decode_logs(next_list(&mut fields, ReceiptField::Logs)?)?;
Ok(UnvalidatedReceipt {
encoded_payload,
envelope,
status_or_state_root,
cumulative_gas_used,
logs_bloom,
logs,
})
}
fn decode_status_or_root(
scalar: RlpScalar<'_>,
) -> Result<ReceiptStatusOrStateRoot, ReceiptDecodeError> {
let payload = scalar.payload();
match payload {
[] => Ok(ReceiptStatusOrStateRoot::Failure),
[1] => Ok(ReceiptStatusOrStateRoot::Success),
bytes if bytes.len() == B256_BYTES => Ok(ReceiptStatusOrStateRoot::StateRoot(
B256::from_bytes(bytes.try_into().map_err(|_| {
ReceiptDecodeError::InvalidStatusOrStateRoot { found: bytes.len() }
})?),
)),
_ => Err(ReceiptDecodeError::InvalidStatusOrStateRoot {
found: payload.len(),
}),
}
}
fn decode_logs(list: RlpList<'_>) -> Result<ReceiptLogs<'_>, ReceiptDecodeError> {
for item in list.items() {
let _ = decode_log_item(item)?;
}
Ok(ReceiptLogs { list })
}
fn decode_log_item(
item: Result<RlpItem<'_>, DecodeError>,
) -> Result<ReceiptLog<'_>, ReceiptDecodeError> {
let item = item.map_err(|source| field_error(ReceiptField::Logs, source))?;
let RlpItem::List(list) = item else {
return Err(field_error(
ReceiptField::Logs,
DecodeError::UnexpectedScalar,
));
};
if list.item_count() != LOG_FIELD_COUNT {
return Err(ReceiptDecodeError::InvalidLogFieldCount {
found: list.item_count(),
});
}
let mut fields = list.items();
let address = Address::from_bytes(decode_fixed::<ADDRESS_BYTES>(
&mut fields,
ReceiptField::LogAddress,
)?);
let topics = decode_topics(next_list(&mut fields, ReceiptField::LogTopics)?)?;
let data = next_scalar(&mut fields, ReceiptField::LogData)?.payload();
Ok(ReceiptLog {
address,
topics,
data,
})
}
fn decode_topics(list: RlpList<'_>) -> Result<ReceiptLogTopics<'_>, ReceiptDecodeError> {
for item in list.items() {
let _ = decode_topic_item(item)?;
}
Ok(ReceiptLogTopics { list })
}
fn decode_topic_item(item: Result<RlpItem<'_>, DecodeError>) -> Result<B256, ReceiptDecodeError> {
let item = item.map_err(|source| field_error(ReceiptField::LogTopics, source))?;
let RlpItem::Scalar(scalar) = item else {
return Err(field_error(
ReceiptField::LogTopics,
DecodeError::UnexpectedList,
));
};
let found = scalar.payload().len();
let bytes: [u8; B256_BYTES] = scalar
.payload()
.try_into()
.map_err(|_| ReceiptDecodeError::InvalidLogTopicLength { found })?;
Ok(B256::from_bytes(bytes))
}
fn decode_u64<'a>(
fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
field: ReceiptField,
) -> Result<u64, ReceiptDecodeError> {
RlpInteger::try_from_scalar(next_scalar(fields, field)?)
.and_then(RlpInteger::to_u64)
.map_err(|source| field_error(field, source))
}
fn decode_fixed<'a, const N: usize>(
fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
field: ReceiptField,
) -> Result<[u8; N], ReceiptDecodeError> {
let scalar = next_scalar(fields, field)?;
let found = scalar.payload().len();
scalar.payload().try_into().map_err(|_| match field {
ReceiptField::LogsBloom => ReceiptDecodeError::InvalidLogsBloomLength { found },
ReceiptField::LogAddress => ReceiptDecodeError::InvalidLogAddressLength { found },
_ => field_error(field, DecodeError::Malformed),
})
}
fn next_scalar<'a>(
fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
field: ReceiptField,
) -> Result<RlpScalar<'a>, ReceiptDecodeError> {
let item = fields
.next()
.ok_or(field_error(field, DecodeError::Malformed))?
.map_err(|source| field_error(field, source))?;
match item {
RlpItem::Scalar(scalar) => Ok(scalar),
RlpItem::List(_) => Err(field_error(field, DecodeError::UnexpectedList)),
}
}
fn next_list<'a>(
fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
field: ReceiptField,
) -> Result<RlpList<'a>, ReceiptDecodeError> {
let item = fields
.next()
.ok_or(field_error(field, DecodeError::Malformed))?
.map_err(|source| field_error(field, source))?;
match item {
RlpItem::List(list) => Ok(list),
RlpItem::Scalar(_) => Err(field_error(field, DecodeError::UnexpectedScalar)),
}
}
const fn field_error(field: ReceiptField, source: DecodeError) -> ReceiptDecodeError {
ReceiptDecodeError::FieldDecode { field, source }
}
#[cfg(test)]
#[path = "receipt_tests.rs"]
mod tests;