From 3f7e92d723983e97a0667bc03a7a1201125bc7bb Mon Sep 17 00:00:00 2001 From: clabby Date: Sat, 24 Feb 2024 14:15:26 -0700 Subject: [PATCH] feat: Add OP receipt fields --- crates/derive/src/types/eips/eip2718.rs | 1 + crates/derive/src/types/receipt.rs | 165 ++++++++++++++++-- .../derive/src/types/transaction/envelope.rs | 3 +- 3 files changed, 153 insertions(+), 16 deletions(-) diff --git a/crates/derive/src/types/eips/eip2718.rs b/crates/derive/src/types/eips/eip2718.rs index 1988e17eb..55e3b1ed1 100644 --- a/crates/derive/src/types/eips/eip2718.rs +++ b/crates/derive/src/types/eips/eip2718.rs @@ -12,6 +12,7 @@ const TX_TYPE_BYTE_MAX: u8 = 0x7f; /// [EIP-2718] decoding errors. /// /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +#[derive(Debug)] pub enum Eip2718Error { /// Rlp error from [`alloy_rlp`]. RlpError(alloy_rlp::Error), diff --git a/crates/derive/src/types/receipt.rs b/crates/derive/src/types/receipt.rs index da40ee808..18df6641b 100644 --- a/crates/derive/src/types/receipt.rs +++ b/crates/derive/src/types/receipt.rs @@ -1,12 +1,17 @@ //! This module contains the receipt types used within the derivation pipeline. +use core::cmp::Ordering; + +use crate::types::transaction::TxType; use alloc::vec::Vec; use alloy_primitives::{Bloom, Log}; -use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; +use alloy_rlp::{length_of_length, Buf, BufMut, BytesMut, Decodable, Encodable}; /// Receipt containing result of transaction execution. #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct Receipt { + /// The transaction type of the receipt. + pub tx_type: TxType, /// If transaction is executed successfully. /// /// This is the `statusCode` @@ -15,6 +20,15 @@ pub struct Receipt { pub cumulative_gas_used: u64, /// Log send from contracts. pub logs: Vec, + /// Deposit nonce for Optimism deposit transactions + pub deposit_nonce: Option, + /// Deposit receipt version for Optimism deposit transactions + /// + /// + /// The deposit receipt version was introduced in Canyon to indicate an update to how + /// receipt hashes should be computed when set. The state transition process + /// ensures this is only set for post-Canyon deposit transactions. + pub deposit_receipt_version: Option, } impl Receipt { @@ -69,10 +83,19 @@ impl ReceiptWithBloom { } fn payload_len(&self) -> usize { - self.receipt.success.length() + let mut payload_len = self.receipt.success.length() + self.receipt.cumulative_gas_used.length() + self.bloom.length() - + self.receipt.logs.len() + + self.receipt.logs.len(); + if self.receipt.tx_type == TxType::Deposit { + if let Some(deposit_nonce) = self.receipt.deposit_nonce { + payload_len += deposit_nonce.length(); + } + if let Some(deposit_receipt_version) = self.receipt.deposit_receipt_version { + payload_len += deposit_receipt_version.length(); + } + } + payload_len } /// Returns the rlp header for the receipt payload. @@ -90,10 +113,56 @@ impl ReceiptWithBloom { self.receipt.cumulative_gas_used.encode(out); self.bloom.encode(out); self.receipt.logs.encode(out); + + if self.receipt.tx_type == TxType::Deposit { + if let Some(deposit_nonce) = self.receipt.deposit_nonce { + deposit_nonce.encode(out) + } + if let Some(deposit_receipt_version) = self.receipt.deposit_receipt_version { + deposit_receipt_version.encode(out) + } + } + } + + fn encode_inner(&self, out: &mut dyn BufMut, with_header: bool) { + if matches!(self.receipt.tx_type, TxType::Legacy) { + self.encode_fields(out); + return; + } + + let mut payload = BytesMut::new(); + self.encode_fields(&mut payload); + + if with_header { + let payload_length = payload.len() + 1; + let header = alloy_rlp::Header { + list: false, + payload_length, + }; + header.encode(out); + } + + match self.receipt.tx_type { + TxType::Legacy => unreachable!("legacy already handled"), + + TxType::Eip2930 => { + out.put_u8(0x01); + } + TxType::Eip1559 => { + out.put_u8(0x02); + } + TxType::Eip4844 => { + out.put_u8(0x03); + } + TxType::Deposit => { + out.put_u8(0x7E); + } + } + out.put_slice(payload.as_ref()); } /// Decodes the receipt payload - fn decode_receipt(buf: &mut &[u8]) -> alloy_rlp::Result { + fn decode_receipt(buf: &mut &[u8], tx_type: TxType) -> alloy_rlp::Result { let b: &mut &[u8] = &mut &**buf; let rlp_head = alloy_rlp::Header::decode(b)?; if !rlp_head.list { @@ -106,10 +175,33 @@ impl ReceiptWithBloom { let bloom = Decodable::decode(b)?; let logs = Decodable::decode(b)?; - let receipt = Receipt { - success, - cumulative_gas_used, - logs, + let receipt = match tx_type { + TxType::Deposit => { + let remaining = |b: &[u8]| rlp_head.payload_length - (started_len - b.len()) > 0; + let deposit_nonce = remaining(b) + .then(|| alloy_rlp::Decodable::decode(b)) + .transpose()?; + let deposit_receipt_version = remaining(b) + .then(|| alloy_rlp::Decodable::decode(b)) + .transpose()?; + + Receipt { + tx_type, + success, + cumulative_gas_used, + logs, + deposit_nonce, + deposit_receipt_version, + } + } + _ => Receipt { + tx_type, + success, + cumulative_gas_used, + logs, + deposit_nonce: None, + deposit_receipt_version: None, + }, }; let this = Self { receipt, bloom }; @@ -127,20 +219,63 @@ impl ReceiptWithBloom { impl alloy_rlp::Encodable for ReceiptWithBloom { fn encode(&self, out: &mut dyn BufMut) { - self.encode_fields(out); + self.encode_inner(out, true) } fn length(&self) -> usize { - let payload_length = self.receipt.success.length() - + self.receipt.cumulative_gas_used.length() - + self.bloom.length() - + self.receipt.logs.length(); - payload_length + length_of_length(payload_length) + let rlp_head = self.receipt_rlp_header(); + let mut payload_len = length_of_length(rlp_head.payload_length) + rlp_head.payload_length; + // account for eip-2718 type prefix and set the list + if !matches!(self.receipt.tx_type, TxType::Legacy) { + payload_len += 1; + // we include a string header for typed receipts, so include the length here + payload_len += length_of_length(payload_len); + } + + payload_len } } impl alloy_rlp::Decodable for ReceiptWithBloom { fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - Self::decode_receipt(buf) + // a receipt is either encoded as a string (non legacy) or a list (legacy). + // We should not consume the buffer if we are decoding a legacy receipt, so let's + // check if the first byte is between 0x80 and 0xbf. + let rlp_type = *buf.first().ok_or(alloy_rlp::Error::Custom( + "cannot decode a receipt from empty bytes", + ))?; + + match rlp_type.cmp(&alloy_rlp::EMPTY_LIST_CODE) { + Ordering::Less => { + // strip out the string header + let _header = alloy_rlp::Header::decode(buf)?; + let receipt_type = *buf.first().ok_or(alloy_rlp::Error::Custom( + "typed receipt cannot be decoded from an empty slice", + ))?; + match receipt_type { + 0x01 => { + buf.advance(1); + Self::decode_receipt(buf, TxType::Eip2930) + } + 0x02 => { + buf.advance(1); + Self::decode_receipt(buf, TxType::Eip1559) + } + 0x03 => { + buf.advance(1); + Self::decode_receipt(buf, TxType::Eip4844) + } + 0x7E => { + buf.advance(1); + Self::decode_receipt(buf, TxType::Deposit) + } + _ => Err(alloy_rlp::Error::Custom("invalid receipt type")), + } + } + Ordering::Equal => Err(alloy_rlp::Error::Custom( + "an empty list is not a valid receipt encoding", + )), + Ordering::Greater => Self::decode_receipt(buf, TxType::Legacy), + } } } diff --git a/crates/derive/src/types/transaction/envelope.rs b/crates/derive/src/types/transaction/envelope.rs index 3b4e6b945..94404a7e4 100644 --- a/crates/derive/src/types/transaction/envelope.rs +++ b/crates/derive/src/types/transaction/envelope.rs @@ -13,9 +13,10 @@ use alloy_rlp::{length_of_length, Decodable, Encodable}; /// [2930]: https://eips.ethereum.org/EIPS/eip-2930 /// [4844]: https://eips.ethereum.org/EIPS/eip-4844 #[repr(u8)] -#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Default)] pub enum TxType { /// Wrapped legacy transaction type. + #[default] Legacy = 0, /// EIP-2930 transaction type. Eip2930 = 1,