Skip to main content

eth_valkyoth_protocol/
receipt.rs

1use eth_valkyoth_codec::{DecodeError, DecodeLimits, RlpInteger, RlpItem, RlpList, RlpScalar};
2use eth_valkyoth_primitives::{Address, B256, Gas, TransactionType};
3
4use crate::eip2718::{
5    EIP_2718_MAX_TYPED_PREFIX as SHARED_EIP_2718_MAX_TYPED_PREFIX,
6    EIP_2718_RESERVED_PREFIX as SHARED_EIP_2718_RESERVED_PREFIX,
7    EIP_2718_SCALAR_PREFIX_START as SHARED_EIP_2718_SCALAR_PREFIX_START,
8    EIP_2718_TYPED_ZERO_PREFIX as SHARED_EIP_2718_TYPED_ZERO_PREFIX, Eip2718Prefix,
9    LEGACY_PREFIX_START as SHARED_LEGACY_PREFIX_START, classify_eip2718_prefix,
10};
11
12mod error;
13pub use error::{ReceiptDecodeError, ReceiptDecodeErrorCategory, ReceiptField};
14
15const ADDRESS_BYTES: usize = 20;
16const B256_BYTES: usize = 32;
17const BLOOM_BYTES: usize = 256;
18const LOG_FIELD_COUNT: usize = 3;
19
20/// Number of fields in a legacy or typed receipt payload.
21pub const RECEIPT_FIELD_COUNT: usize = 4;
22/// EIP-2718 byte value reserved by this crate for the legacy receipt domain.
23pub const EIP_2718_TYPED_ZERO_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_TYPED_ZERO_PREFIX;
24/// Largest single-byte EIP-2718 typed receipt prefix.
25pub const EIP_2718_MAX_TYPED_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_MAX_TYPED_PREFIX;
26/// First RLP scalar prefix that cannot be a typed receipt or legacy list.
27pub const EIP_2718_RECEIPT_SCALAR_PREFIX_START: u8 = SHARED_EIP_2718_SCALAR_PREFIX_START;
28/// First byte used by canonical RLP short-list legacy receipts.
29pub const LEGACY_RECEIPT_PREFIX_START: u8 = SHARED_LEGACY_PREFIX_START;
30/// Prefix reserved by EIP-2718 as a future extension sentinel.
31pub const EIP_2718_RESERVED_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_RESERVED_PREFIX;
32
33/// Borrowed receipt envelope shell.
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum ReceiptEnvelope<'a> {
36    /// A legacy receipt represented as one exact RLP list.
37    Legacy(RlpList<'a>),
38    /// An EIP-2718 typed receipt with an opaque payload.
39    Typed(TypedReceiptEnvelope<'a>),
40}
41
42/// Borrowed EIP-2718 typed receipt shell.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct TypedReceiptEnvelope<'a> {
45    /// EIP-2718 receipt type byte.
46    pub receipt_type: TransactionType,
47    /// Opaque receipt payload.
48    pub payload: &'a [u8],
49}
50
51/// Borrowed receipt decoded only into field domains.
52///
53/// This type is intentionally unvalidated. It does not prove transaction
54/// execution, receipt-trie inclusion, block receipt-root membership, log
55/// semantics, cumulative-gas monotonicity, or that a typed receipt matches the
56/// transaction type at the same block index.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct UnvalidatedReceipt<'a> {
59    encoded_payload: &'a [u8],
60    /// Receipt domain.
61    pub envelope: ReceiptKind,
62    /// Status code or pre-Byzantium state root.
63    pub status_or_state_root: ReceiptStatusOrStateRoot,
64    /// Cumulative gas used after this transaction in the containing block.
65    pub cumulative_gas_used: Gas,
66    /// Logs bloom filter.
67    pub logs_bloom: ReceiptLogsBloom,
68    /// Borrowed logs list.
69    pub logs: ReceiptLogs<'a>,
70}
71
72impl UnvalidatedReceipt<'_> {
73    /// Returns the exact canonical RLP payload bytes that were decoded.
74    ///
75    /// For legacy receipts this is the whole receipt. For typed receipts this
76    /// excludes the EIP-2718 type byte.
77    #[must_use]
78    pub const fn encoded_payload(&self) -> &[u8] {
79        self.encoded_payload
80    }
81}
82
83/// Receipt domain after envelope classification.
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum ReceiptKind {
86    /// Legacy RLP receipt.
87    Legacy,
88    /// EIP-2718 typed receipt.
89    Typed(TransactionType),
90}
91
92/// Receipt status code or pre-Byzantium state root.
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum ReceiptStatusOrStateRoot {
95    /// Transaction failed.
96    Failure,
97    /// Transaction succeeded.
98    Success,
99    /// Pre-Byzantium intermediate state root.
100    StateRoot(B256),
101}
102
103/// Ethereum receipt logs bloom filter bytes.
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct ReceiptLogsBloom([u8; BLOOM_BYTES]);
106
107impl ReceiptLogsBloom {
108    /// Creates a logs bloom from raw bytes.
109    #[must_use]
110    pub const fn from_bytes(bytes: [u8; BLOOM_BYTES]) -> Self {
111        Self(bytes)
112    }
113
114    /// Returns the raw logs-bloom bytes.
115    #[must_use]
116    pub const fn to_bytes(self) -> [u8; BLOOM_BYTES] {
117        self.0
118    }
119}
120
121/// Borrowed receipt logs list.
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub struct ReceiptLogs<'a> {
124    list: RlpList<'a>,
125}
126
127impl<'a> ReceiptLogs<'a> {
128    /// Returns the number of logs.
129    #[must_use]
130    pub const fn len(self) -> usize {
131        self.list.item_count()
132    }
133
134    /// Returns true when there are no logs.
135    #[must_use]
136    pub const fn is_empty(self) -> bool {
137        self.list.is_empty()
138    }
139
140    /// Returns an iterator over decoded log entries.
141    #[must_use]
142    pub const fn entries(self) -> ReceiptLogEntries<'a> {
143        ReceiptLogEntries {
144            items: self.list.items(),
145        }
146    }
147}
148
149/// Borrowed receipt log entry.
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub struct ReceiptLog<'a> {
152    /// Contract address that emitted the log.
153    pub address: Address,
154    /// Borrowed topics list.
155    pub topics: ReceiptLogTopics<'a>,
156    /// Borrowed log data bytes.
157    pub data: &'a [u8],
158}
159
160/// Borrowed log topics list.
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub struct ReceiptLogTopics<'a> {
163    list: RlpList<'a>,
164}
165
166impl<'a> ReceiptLogTopics<'a> {
167    /// Returns the number of topics.
168    #[must_use]
169    pub const fn len(self) -> usize {
170        self.list.item_count()
171    }
172
173    /// Returns true when the log has no topics.
174    #[must_use]
175    pub const fn is_empty(self) -> bool {
176        self.list.is_empty()
177    }
178
179    /// Returns an iterator over decoded topics.
180    #[must_use]
181    pub const fn topics(self) -> ReceiptLogTopicItems<'a> {
182        ReceiptLogTopicItems {
183            items: self.list.items(),
184        }
185    }
186}
187
188/// Iterator over receipt logs.
189#[derive(Clone, Debug)]
190pub struct ReceiptLogEntries<'a> {
191    items: eth_valkyoth_codec::RlpListItems<'a>,
192}
193
194impl<'a> Iterator for ReceiptLogEntries<'a> {
195    type Item = Result<ReceiptLog<'a>, ReceiptDecodeError>;
196
197    fn next(&mut self) -> Option<Self::Item> {
198        self.items.next().map(decode_log_item)
199    }
200}
201
202impl core::iter::FusedIterator for ReceiptLogEntries<'_> {}
203
204/// Iterator over log topics.
205#[derive(Clone, Debug)]
206pub struct ReceiptLogTopicItems<'a> {
207    items: eth_valkyoth_codec::RlpListItems<'a>,
208}
209
210impl Iterator for ReceiptLogTopicItems<'_> {
211    type Item = Result<B256, ReceiptDecodeError>;
212
213    fn next(&mut self) -> Option<Self::Item> {
214        self.items.next().map(decode_topic_item)
215    }
216}
217
218impl core::iter::FusedIterator for ReceiptLogTopicItems<'_> {}
219
220/// Classifies a borrowed receipt envelope under explicit decode limits.
221pub fn decode_receipt_envelope<'a>(
222    input: &'a [u8],
223    limits: DecodeLimits,
224) -> Result<ReceiptEnvelope<'a>, ReceiptDecodeError> {
225    limits
226        .check_input_len(input.len())
227        .map_err(|source| field_error(ReceiptField::Payload, source))?;
228
229    let Some(prefix) = classify_eip2718_prefix(input) else {
230        return Err(ReceiptDecodeError::EmptyInput);
231    };
232
233    match prefix {
234        Eip2718Prefix::TypedZero => Err(ReceiptDecodeError::UnsupportedReceiptType {
235            type_byte: EIP_2718_TYPED_ZERO_RECEIPT_PREFIX,
236        }),
237        Eip2718Prefix::Typed { type_byte, payload } => {
238            let receipt_type = TransactionType::try_new_typed(type_byte)
239                .map_err(|_| ReceiptDecodeError::UnsupportedReceiptType { type_byte })?;
240            Ok(ReceiptEnvelope::Typed(TypedReceiptEnvelope {
241                receipt_type,
242                payload,
243            }))
244        }
245        Eip2718Prefix::ScalarPrefix { prefix } => Err(ReceiptDecodeError::ScalarPrefix { prefix }),
246        Eip2718Prefix::Legacy => {
247            let list = eth_valkyoth_codec::decode_rlp_list(input, limits)
248                .map_err(|source| field_error(ReceiptField::Payload, source))?;
249            Ok(ReceiptEnvelope::Legacy(list))
250        }
251        Eip2718Prefix::Reserved => Err(ReceiptDecodeError::ReservedPrefix),
252    }
253}
254
255/// Decodes a legacy or typed receipt into unvalidated field domains.
256pub fn decode_receipt<'a>(
257    input: &'a [u8],
258    limits: DecodeLimits,
259) -> Result<UnvalidatedReceipt<'a>, ReceiptDecodeError> {
260    match decode_receipt_envelope(input, limits)? {
261        ReceiptEnvelope::Legacy(list) => decode_receipt_list(list, input, ReceiptKind::Legacy),
262        ReceiptEnvelope::Typed(typed) => {
263            let list = eth_valkyoth_codec::decode_rlp_list(typed.payload, limits)
264                .map_err(|source| field_error(ReceiptField::Payload, source))?;
265            decode_receipt_list(list, typed.payload, ReceiptKind::Typed(typed.receipt_type))
266        }
267    }
268}
269
270fn decode_receipt_list<'a>(
271    list: RlpList<'a>,
272    encoded_payload: &'a [u8],
273    envelope: ReceiptKind,
274) -> Result<UnvalidatedReceipt<'a>, ReceiptDecodeError> {
275    if list.item_count() != RECEIPT_FIELD_COUNT {
276        return Err(ReceiptDecodeError::WrongFieldCount {
277            expected: RECEIPT_FIELD_COUNT,
278            found: list.item_count(),
279        });
280    }
281
282    let mut fields = list.items();
283    let status_or_state_root =
284        decode_status_or_root(next_scalar(&mut fields, ReceiptField::StatusOrStateRoot)?)?;
285    let cumulative_gas_used = Gas::new(decode_u64(&mut fields, ReceiptField::CumulativeGasUsed)?);
286    let logs_bloom = ReceiptLogsBloom::from_bytes(decode_fixed::<BLOOM_BYTES>(
287        &mut fields,
288        ReceiptField::LogsBloom,
289    )?);
290    let logs = decode_logs(next_list(&mut fields, ReceiptField::Logs)?)?;
291
292    Ok(UnvalidatedReceipt {
293        encoded_payload,
294        envelope,
295        status_or_state_root,
296        cumulative_gas_used,
297        logs_bloom,
298        logs,
299    })
300}
301
302fn decode_status_or_root(
303    scalar: RlpScalar<'_>,
304) -> Result<ReceiptStatusOrStateRoot, ReceiptDecodeError> {
305    let payload = scalar.payload();
306    match payload {
307        [] => Ok(ReceiptStatusOrStateRoot::Failure),
308        [1] => Ok(ReceiptStatusOrStateRoot::Success),
309        bytes if bytes.len() == B256_BYTES => Ok(ReceiptStatusOrStateRoot::StateRoot(
310            B256::from_bytes(bytes.try_into().map_err(|_| {
311                ReceiptDecodeError::InvalidStatusOrStateRoot { found: bytes.len() }
312            })?),
313        )),
314        _ => Err(ReceiptDecodeError::InvalidStatusOrStateRoot {
315            found: payload.len(),
316        }),
317    }
318}
319
320fn decode_logs(list: RlpList<'_>) -> Result<ReceiptLogs<'_>, ReceiptDecodeError> {
321    for item in list.items() {
322        let _ = decode_log_item(item)?;
323    }
324    Ok(ReceiptLogs { list })
325}
326
327fn decode_log_item(
328    item: Result<RlpItem<'_>, DecodeError>,
329) -> Result<ReceiptLog<'_>, ReceiptDecodeError> {
330    let item = item.map_err(|source| field_error(ReceiptField::Logs, source))?;
331    let RlpItem::List(list) = item else {
332        return Err(field_error(
333            ReceiptField::Logs,
334            DecodeError::UnexpectedScalar,
335        ));
336    };
337    if list.item_count() != LOG_FIELD_COUNT {
338        return Err(ReceiptDecodeError::InvalidLogFieldCount {
339            found: list.item_count(),
340        });
341    }
342
343    let mut fields = list.items();
344    let address = Address::from_bytes(decode_fixed::<ADDRESS_BYTES>(
345        &mut fields,
346        ReceiptField::LogAddress,
347    )?);
348    let topics = decode_topics(next_list(&mut fields, ReceiptField::LogTopics)?)?;
349    let data = next_scalar(&mut fields, ReceiptField::LogData)?.payload();
350    Ok(ReceiptLog {
351        address,
352        topics,
353        data,
354    })
355}
356
357fn decode_topics(list: RlpList<'_>) -> Result<ReceiptLogTopics<'_>, ReceiptDecodeError> {
358    for item in list.items() {
359        let _ = decode_topic_item(item)?;
360    }
361    Ok(ReceiptLogTopics { list })
362}
363
364fn decode_topic_item(item: Result<RlpItem<'_>, DecodeError>) -> Result<B256, ReceiptDecodeError> {
365    let item = item.map_err(|source| field_error(ReceiptField::LogTopics, source))?;
366    let RlpItem::Scalar(scalar) = item else {
367        return Err(field_error(
368            ReceiptField::LogTopics,
369            DecodeError::UnexpectedList,
370        ));
371    };
372    let found = scalar.payload().len();
373    let bytes: [u8; B256_BYTES] = scalar
374        .payload()
375        .try_into()
376        .map_err(|_| ReceiptDecodeError::InvalidLogTopicLength { found })?;
377    Ok(B256::from_bytes(bytes))
378}
379
380fn decode_u64<'a>(
381    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
382    field: ReceiptField,
383) -> Result<u64, ReceiptDecodeError> {
384    RlpInteger::try_from_scalar(next_scalar(fields, field)?)
385        .and_then(RlpInteger::to_u64)
386        .map_err(|source| field_error(field, source))
387}
388
389fn decode_fixed<'a, const N: usize>(
390    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
391    field: ReceiptField,
392) -> Result<[u8; N], ReceiptDecodeError> {
393    let scalar = next_scalar(fields, field)?;
394    let found = scalar.payload().len();
395    scalar.payload().try_into().map_err(|_| match field {
396        ReceiptField::LogsBloom => ReceiptDecodeError::InvalidLogsBloomLength { found },
397        ReceiptField::LogAddress => ReceiptDecodeError::InvalidLogAddressLength { found },
398        _ => field_error(field, DecodeError::Malformed),
399    })
400}
401
402fn next_scalar<'a>(
403    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
404    field: ReceiptField,
405) -> Result<RlpScalar<'a>, ReceiptDecodeError> {
406    let item = fields
407        .next()
408        .ok_or(field_error(field, DecodeError::Malformed))?
409        .map_err(|source| field_error(field, source))?;
410    match item {
411        RlpItem::Scalar(scalar) => Ok(scalar),
412        RlpItem::List(_) => Err(field_error(field, DecodeError::UnexpectedList)),
413    }
414}
415
416fn next_list<'a>(
417    fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
418    field: ReceiptField,
419) -> Result<RlpList<'a>, ReceiptDecodeError> {
420    let item = fields
421        .next()
422        .ok_or(field_error(field, DecodeError::Malformed))?
423        .map_err(|source| field_error(field, source))?;
424    match item {
425        RlpItem::List(list) => Ok(list),
426        RlpItem::Scalar(_) => Err(field_error(field, DecodeError::UnexpectedScalar)),
427    }
428}
429
430const fn field_error(field: ReceiptField, source: DecodeError) -> ReceiptDecodeError {
431    ReceiptDecodeError::FieldDecode { field, source }
432}
433
434#[cfg(test)]
435#[path = "receipt_tests.rs"]
436mod tests;