diff --git a/crates/derive/src/types/eips/deposit.rs b/crates/derive/src/types/eips/deposit.rs new file mode 100644 index 0000000000..184d15703a --- /dev/null +++ b/crates/derive/src/types/eips/deposit.rs @@ -0,0 +1,286 @@ +use crate::types::{ + network::{Signed, Transaction, TxKind}, + transaction::TxType, +}; +use alloc::vec::Vec; +use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{ + length_of_length, Buf, BufMut, Decodable, Encodable, Error as DecodeError, Header, + EMPTY_STRING_CODE, +}; +use core::mem; + +/// Deposit transactions, also known as deposits are initiated on L1, and executed on L2. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxDeposit { + /// Hash that uniquely identifies the source of the deposit. + pub source_hash: B256, + /// The address of the sender account. + pub from: Address, + /// The address of the recipient account, or the null (zero-length) address if the deposited + /// transaction is a contract creation. + pub to: TxKind, + /// The ETH value to mint on L2. + pub mint: Option, + /// The ETH value to send to the recipient account. + pub value: U256, + /// The gas limit for the L2 transaction. + pub gas_limit: u64, + /// Field indicating if this transaction is exempt from the L2 gas limit. + pub is_system_transaction: bool, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). + pub input: Bytes, +} + +impl TxDeposit { + /// Calculates a heuristic for the in-memory size of the [TxDeposit] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // source_hash + mem::size_of::
() + // from + self.to.size() + // to + mem::size_of::>() + // mint + mem::size_of::() + // value + mem::size_of::() + // gas_limit + mem::size_of::() + // is_system_transaction + self.input.len() // input + } + + /// Decodes the inner [TxDeposit] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `source_hash` + /// - `from` + /// - `to` + /// - `mint` + /// - `value` + /// - `gas_limit` + /// - `is_system_transaction` + /// - `input` + pub fn decode_inner(buf: &mut &[u8]) -> Result { + Ok(Self { + source_hash: Decodable::decode(buf)?, + from: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + mint: if *buf.first().ok_or(DecodeError::InputTooShort)? == EMPTY_STRING_CODE { + buf.advance(1); + None + } else { + Some(Decodable::decode(buf)?) + }, + value: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + is_system_transaction: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header or length of the + /// eip155 fields. + pub(crate) fn fields_len(&self) -> usize { + self.source_hash.length() + + self.from.length() + + self.to.length() + + self.mint.map_or(1, |mint| mint.length()) + + self.value.length() + + self.gas_limit.length() + + self.is_system_transaction.length() + + self.input.0.length() + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + /// + pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { + self.source_hash.encode(out); + self.from.encode(out); + self.to.encode(out); + if let Some(mint) = self.mint { + mint.encode(out); + } else { + out.put_u8(EMPTY_STRING_CODE); + } + self.value.encode(out); + self.gas_limit.encode(out); + self.is_system_transaction.encode(out); + self.input.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header. + /// + /// NOTE: Deposit transactions are not signed, so this function does not encode a signature, + /// just the header and transaction rlp. + pub(crate) fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn alloy_rlp::BufMut, + ) { + let payload_length = self.fields_len(); + let header = Header { + list: true, + payload_length, + }; + header.encode(out); + self.encode_fields(out); + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub(crate) fn payload_len(&self) -> usize { + let payload_length = self.fields_len(); + // 'tx type' + 'header length' + 'payload length' + let len = 1 + length_of_length(payload_length) + payload_length; + length_of_length(len) + len + } + + pub(crate) fn payload_len_without_header(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Get the transaction type + pub(crate) fn tx_type(&self) -> TxType { + TxType::Deposit + } +} + +impl Encodable for TxDeposit { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxDeposit { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(data) + } +} + +impl Transaction for TxDeposit { + type Signature = Signature; + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + fn into_signed(self, signature: Signature) -> Signed { + let payload_length = 1 + self.fields_len() + signature.rlp_vrs_len(); + let mut buf = Vec::with_capacity(payload_length); + buf.put_u8(TxType::Eip1559 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + // Drop any v chain id value to ensure the signature format is correct at the time of + // combination for an EIP-1559 transaction. V should indicate the y-parity of the + // signature. + Signed::new_unchecked(self, signature.with_parity_bool(), hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn alloy_rlp::BufMut) { + TxDeposit::encode_with_signature(self, signature, out) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + None + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + unreachable!("Deposit transactions do not have a chain id"); + } + + fn nonce(&self) -> u64 { + 0 + } + + fn set_nonce(&mut self, nonce: u64) { + unreachable!("Deposit transactions do not have a nonce"); + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + None + } + + fn set_gas_price(&mut self, price: U256) { + let _ = price; + } +} diff --git a/crates/derive/src/types/eips/eip2718.rs b/crates/derive/src/types/eips/eip2718.rs index 1988e17eb9..55e3b1ed15 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/eips/mod.rs b/crates/derive/src/types/eips/mod.rs index 8513380a4b..fdcbc6b107 100644 --- a/crates/derive/src/types/eips/mod.rs +++ b/crates/derive/src/types/eips/mod.rs @@ -12,4 +12,6 @@ pub mod eip4788; pub mod eip4844; pub use eip4844::{calc_blob_gasprice, calc_excess_blob_gas}; +pub mod deposit; + pub mod merge; diff --git a/crates/derive/src/types/receipt.rs b/crates/derive/src/types/receipt.rs index da40ee8085..18df6641bb 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 3b4e6b9459..023ad066c1 100644 --- a/crates/derive/src/types/transaction/envelope.rs +++ b/crates/derive/src/types/transaction/envelope.rs @@ -1,5 +1,8 @@ use crate::types::{ - eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}, + eips::{ + deposit::TxDeposit, + eip2718::{Decodable2718, Eip2718Error, Encodable2718}, + }, network::Signed, transaction::{TxEip1559, TxEip2930, TxEip4844, TxLegacy}, }; @@ -13,9 +16,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, @@ -23,6 +27,8 @@ pub enum TxType { Eip1559 = 2, /// EIP-4844 transaction type. Eip4844 = 3, + /// Optimism Deposit transaction type. + Deposit = 126, } impl TryFrom for TxType { @@ -31,7 +37,7 @@ impl TryFrom for TxType { fn try_from(value: u8) -> Result { match value { // SAFETY: repr(u8) with explicit discriminant - ..=3 => Ok(unsafe { core::mem::transmute(value) }), + ..=3 | 126 => Ok(unsafe { core::mem::transmute(value) }), _ => Err(Eip2718Error::UnexpectedType(value)), } } @@ -60,6 +66,8 @@ pub enum TxEnvelope { Eip1559(Signed), /// A [`TxEip4844`]. Eip4844(Signed), + /// A [`TxDeposit`]. + Deposit(Signed), } impl From> for TxEnvelope { @@ -82,6 +90,7 @@ impl TxEnvelope { Self::Eip2930(_) => TxType::Eip2930, Self::Eip1559(_) => TxType::Eip1559, Self::Eip4844(_) => TxType::Eip4844, + Self::Deposit(_) => TxType::Deposit, } } @@ -92,6 +101,7 @@ impl TxEnvelope { Self::Eip2930(t) => t.length(), Self::Eip1559(t) => t.length(), Self::Eip4844(t) => t.length(), + Self::Deposit(t) => t.length(), } } @@ -146,6 +156,9 @@ impl Decodable2718 for TxEnvelope { TxType::Eip4844 => Ok(Self::Eip4844( Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, )), + TxType::Deposit => Ok(Self::Deposit( + Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, + )), } } @@ -164,6 +177,7 @@ impl Encodable2718 for TxEnvelope { Self::Eip2930(_) => Some(TxType::Eip2930 as u8), Self::Eip1559(_) => Some(TxType::Eip1559 as u8), Self::Eip4844(_) => Some(TxType::Eip4844 as u8), + Self::Deposit(_) => Some(TxType::Deposit as u8), } } @@ -190,6 +204,10 @@ impl Encodable2718 for TxEnvelope { out.put_u8(TxType::Eip4844 as u8); tx.encode(out); } + TxEnvelope::Deposit(tx) => { + out.put_u8(TxType::Deposit as u8); + tx.encode(out); + } } } }