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
20pub const RECEIPT_FIELD_COUNT: usize = 4;
22pub const EIP_2718_TYPED_ZERO_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_TYPED_ZERO_PREFIX;
24pub const EIP_2718_MAX_TYPED_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_MAX_TYPED_PREFIX;
26pub const EIP_2718_RECEIPT_SCALAR_PREFIX_START: u8 = SHARED_EIP_2718_SCALAR_PREFIX_START;
28pub const LEGACY_RECEIPT_PREFIX_START: u8 = SHARED_LEGACY_PREFIX_START;
30pub const EIP_2718_RESERVED_RECEIPT_PREFIX: u8 = SHARED_EIP_2718_RESERVED_PREFIX;
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum ReceiptEnvelope<'a> {
36 Legacy(RlpList<'a>),
38 Typed(TypedReceiptEnvelope<'a>),
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub struct TypedReceiptEnvelope<'a> {
45 pub receipt_type: TransactionType,
47 pub payload: &'a [u8],
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct UnvalidatedReceipt<'a> {
59 encoded_payload: &'a [u8],
60 pub envelope: ReceiptKind,
62 pub status_or_state_root: ReceiptStatusOrStateRoot,
64 pub cumulative_gas_used: Gas,
66 pub logs_bloom: ReceiptLogsBloom,
68 pub logs: ReceiptLogs<'a>,
70}
71
72impl UnvalidatedReceipt<'_> {
73 #[must_use]
78 pub const fn encoded_payload(&self) -> &[u8] {
79 self.encoded_payload
80 }
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum ReceiptKind {
86 Legacy,
88 Typed(TransactionType),
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum ReceiptStatusOrStateRoot {
95 Failure,
97 Success,
99 StateRoot(B256),
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct ReceiptLogsBloom([u8; BLOOM_BYTES]);
106
107impl ReceiptLogsBloom {
108 #[must_use]
110 pub const fn from_bytes(bytes: [u8; BLOOM_BYTES]) -> Self {
111 Self(bytes)
112 }
113
114 #[must_use]
116 pub const fn to_bytes(self) -> [u8; BLOOM_BYTES] {
117 self.0
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub struct ReceiptLogs<'a> {
124 list: RlpList<'a>,
125}
126
127impl<'a> ReceiptLogs<'a> {
128 #[must_use]
130 pub const fn len(self) -> usize {
131 self.list.item_count()
132 }
133
134 #[must_use]
136 pub const fn is_empty(self) -> bool {
137 self.list.is_empty()
138 }
139
140 #[must_use]
142 pub const fn entries(self) -> ReceiptLogEntries<'a> {
143 ReceiptLogEntries {
144 items: self.list.items(),
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub struct ReceiptLog<'a> {
152 pub address: Address,
154 pub topics: ReceiptLogTopics<'a>,
156 pub data: &'a [u8],
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub struct ReceiptLogTopics<'a> {
163 list: RlpList<'a>,
164}
165
166impl<'a> ReceiptLogTopics<'a> {
167 #[must_use]
169 pub const fn len(self) -> usize {
170 self.list.item_count()
171 }
172
173 #[must_use]
175 pub const fn is_empty(self) -> bool {
176 self.list.is_empty()
177 }
178
179 #[must_use]
181 pub const fn topics(self) -> ReceiptLogTopicItems<'a> {
182 ReceiptLogTopicItems {
183 items: self.list.items(),
184 }
185 }
186}
187
188#[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#[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
220pub 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
255pub 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;