diff --git a/Justfile b/Justfile index 985121b9a..8a5ca7ccf 100644 --- a/Justfile +++ b/Justfile @@ -32,6 +32,10 @@ lint-native: fmt-check lint-docs clippy clippy: cargo +stable clippy --workspace --all-features --all-targets -- -D warnings +# Fix clippy warnings across the workspace +clippy-fix: + cargo +stable clippy --workspace --all-features --all-targets --fix --allow-staged --allow-dirty -- -D warnings + # Check the formatting of the workspace fmt-check: cargo +nightly fmt --all -- --check diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 53d53ea2e..ce99439e0 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -12,8 +12,10 @@ extern crate alloc; #[cfg(feature = "alloy-compat")] mod alloy_compat; -mod receipt; -pub use receipt::{OpDepositReceipt, OpDepositReceiptWithBloom, OpReceiptEnvelope, OpTxReceipt}; +mod receipts; +pub use receipts::{ + OpDepositReceipt, OpDepositReceiptWithBloom, OpReceipt, OpReceiptEnvelope, OpTxReceipt, +}; pub mod transaction; pub use transaction::{ @@ -48,7 +50,7 @@ pub use transaction::serde_deposit_tx_rpc; #[cfg(all(feature = "serde", feature = "serde-bincode-compat"))] pub mod serde_bincode_compat { pub use super::{ - receipt::receipts::serde_bincode_compat::OpDepositReceipt, + receipts::deposit::serde_bincode_compat::OpDepositReceipt, transaction::{serde_bincode_compat as transaction, serde_bincode_compat::TxDeposit}, }; } diff --git a/crates/consensus/src/receipt/receipts.rs b/crates/consensus/src/receipts/deposit.rs similarity index 100% rename from crates/consensus/src/receipt/receipts.rs rename to crates/consensus/src/receipts/deposit.rs diff --git a/crates/consensus/src/receipt/envelope.rs b/crates/consensus/src/receipts/envelope.rs similarity index 100% rename from crates/consensus/src/receipt/envelope.rs rename to crates/consensus/src/receipts/envelope.rs diff --git a/crates/consensus/src/receipt/mod.rs b/crates/consensus/src/receipts/mod.rs similarity index 76% rename from crates/consensus/src/receipt/mod.rs rename to crates/consensus/src/receipts/mod.rs index bd6ae7d6c..669c9b26e 100644 --- a/crates/consensus/src/receipt/mod.rs +++ b/crates/consensus/src/receipts/mod.rs @@ -5,8 +5,11 @@ use alloy_consensus::TxReceipt; mod envelope; pub use envelope::OpReceiptEnvelope; -pub(crate) mod receipts; -pub use receipts::{OpDepositReceipt, OpDepositReceiptWithBloom}; +pub(crate) mod deposit; +pub use deposit::{OpDepositReceipt, OpDepositReceiptWithBloom}; + +mod receipt; +pub use receipt::OpReceipt; /// Receipt is the result of a transaction execution. pub trait OpTxReceipt: TxReceipt { diff --git a/crates/consensus/src/receipts/receipt.rs b/crates/consensus/src/receipts/receipt.rs new file mode 100644 index 000000000..3ceb7cbbe --- /dev/null +++ b/crates/consensus/src/receipts/receipt.rs @@ -0,0 +1,607 @@ +//! Optimism receipt type for execution and storage. + +use super::{OpDepositReceipt, OpTxReceipt}; +use crate::OpTxType; +use alloc::vec::Vec; +use alloy_consensus::{ + Eip658Value, Eip2718EncodableReceipt, Receipt, ReceiptWithBloom, RlpDecodableReceipt, + RlpEncodableReceipt, TxReceipt, Typed2718, +}; +use alloy_eips::{ + Decodable2718, Encodable2718, + eip2718::{Eip2718Result, IsTyped2718}, +}; +use alloy_primitives::{Bloom, Log}; +use alloy_rlp::{BufMut, Decodable, Encodable, Header}; + +/// Typed Optimism transaction receipt. +/// +/// Receipt containing result of transaction execution. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum OpReceipt { + /// Legacy receipt + Legacy(Receipt), + /// EIP-2930 receipt + Eip2930(Receipt), + /// EIP-1559 receipt + Eip1559(Receipt), + /// EIP-7702 receipt + Eip7702(Receipt), + /// Deposit receipt + Deposit(OpDepositReceipt), +} + +impl OpReceipt { + /// Returns [`OpTxType`] of the receipt. + pub const fn tx_type(&self) -> OpTxType { + match self { + Self::Legacy(_) => OpTxType::Legacy, + Self::Eip2930(_) => OpTxType::Eip2930, + Self::Eip1559(_) => OpTxType::Eip1559, + Self::Eip7702(_) => OpTxType::Eip7702, + Self::Deposit(_) => OpTxType::Deposit, + } + } + + /// Returns inner [`Receipt`]. + pub const fn as_receipt(&self) -> &Receipt { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => receipt, + Self::Deposit(receipt) => &receipt.inner, + } + } + + /// Returns a mutable reference to the inner [`Receipt`]. + pub const fn as_receipt_mut(&mut self) -> &mut Receipt { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => receipt, + Self::Deposit(receipt) => &mut receipt.inner, + } + } + + /// Consumes this and returns the inner [`Receipt`]. + pub fn into_receipt(self) -> Receipt { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => receipt, + Self::Deposit(receipt) => receipt.inner, + } + } + + /// Returns length of RLP-encoded receipt fields with the given [`Bloom`] without an RLP header. + pub fn rlp_encoded_fields_length(&self, bloom: &Bloom) -> usize { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => receipt.rlp_encoded_fields_length_with_bloom(bloom), + Self::Deposit(receipt) => receipt.rlp_encoded_fields_length_with_bloom(bloom), + } + } + + /// RLP-encodes receipt fields with the given [`Bloom`] without an RLP header. + pub fn rlp_encode_fields(&self, bloom: &Bloom, out: &mut dyn BufMut) { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => receipt.rlp_encode_fields_with_bloom(bloom, out), + Self::Deposit(receipt) => receipt.rlp_encode_fields_with_bloom(bloom, out), + } + } + + /// Returns RLP header for inner encoding. + pub fn rlp_header_inner(&self, bloom: &Bloom) -> Header { + Header { list: true, payload_length: self.rlp_encoded_fields_length(bloom) } + } + + /// Returns RLP header for inner encoding without bloom. + pub fn rlp_header_inner_without_bloom(&self) -> Header { + Header { list: true, payload_length: self.rlp_encoded_fields_length_without_bloom() } + } + + /// RLP-decodes the receipt from the provided buffer. This does not expect a type byte or + /// network header. + pub fn rlp_decode_inner( + buf: &mut &[u8], + tx_type: OpTxType, + ) -> alloy_rlp::Result> { + match tx_type { + OpTxType::Legacy => { + let ReceiptWithBloom { receipt, logs_bloom } = + RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; + Ok(ReceiptWithBloom { receipt: Self::Legacy(receipt), logs_bloom }) + } + OpTxType::Eip2930 => { + let ReceiptWithBloom { receipt, logs_bloom } = + RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; + Ok(ReceiptWithBloom { receipt: Self::Eip2930(receipt), logs_bloom }) + } + OpTxType::Eip1559 => { + let ReceiptWithBloom { receipt, logs_bloom } = + RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; + Ok(ReceiptWithBloom { receipt: Self::Eip1559(receipt), logs_bloom }) + } + OpTxType::Eip7702 => { + let ReceiptWithBloom { receipt, logs_bloom } = + RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; + Ok(ReceiptWithBloom { receipt: Self::Eip7702(receipt), logs_bloom }) + } + OpTxType::Deposit => { + let ReceiptWithBloom { receipt, logs_bloom } = + RlpDecodableReceipt::rlp_decode_with_bloom(buf)?; + Ok(ReceiptWithBloom { receipt: Self::Deposit(receipt), logs_bloom }) + } + } + } + + /// RLP-encodes receipt fields without an RLP header. + pub fn rlp_encode_fields_without_bloom(&self, out: &mut dyn BufMut) { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => { + receipt.status.encode(out); + receipt.cumulative_gas_used.encode(out); + receipt.logs.encode(out); + } + Self::Deposit(receipt) => { + receipt.inner.status.encode(out); + receipt.inner.cumulative_gas_used.encode(out); + receipt.inner.logs.encode(out); + if let Some(nonce) = receipt.deposit_nonce { + nonce.encode(out); + } + if let Some(version) = receipt.deposit_receipt_version { + version.encode(out); + } + } + } + } + + /// Returns length of RLP-encoded receipt fields without an RLP header. + pub fn rlp_encoded_fields_length_without_bloom(&self) -> usize { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => { + receipt.status.length() + + receipt.cumulative_gas_used.length() + + receipt.logs.length() + } + Self::Deposit(receipt) => { + receipt.inner.status.length() + + receipt.inner.cumulative_gas_used.length() + + receipt.inner.logs.length() + + receipt.deposit_nonce.map_or(0, |nonce| nonce.length()) + + receipt.deposit_receipt_version.map_or(0, |version| version.length()) + } + } + } + + /// RLP-decodes the receipt from the provided buffer without bloom. + pub fn rlp_decode_inner_without_bloom( + buf: &mut &[u8], + tx_type: OpTxType, + ) -> alloy_rlp::Result { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let remaining = buf.len(); + let status = Decodable::decode(buf)?; + let cumulative_gas_used = Decodable::decode(buf)?; + let logs = Decodable::decode(buf)?; + + let mut deposit_nonce = None; + let mut deposit_receipt_version = None; + + // For deposit receipts, try to decode nonce and version if they exist + if tx_type == OpTxType::Deposit && buf.len() + header.payload_length > remaining { + deposit_nonce = Some(Decodable::decode(buf)?); + if buf.len() + header.payload_length > remaining { + deposit_receipt_version = Some(Decodable::decode(buf)?); + } + } + + if buf.len() + header.payload_length != remaining { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + match tx_type { + OpTxType::Legacy => Ok(Self::Legacy(Receipt { status, cumulative_gas_used, logs })), + OpTxType::Eip2930 => Ok(Self::Eip2930(Receipt { status, cumulative_gas_used, logs })), + OpTxType::Eip1559 => Ok(Self::Eip1559(Receipt { status, cumulative_gas_used, logs })), + OpTxType::Eip7702 => Ok(Self::Eip7702(Receipt { status, cumulative_gas_used, logs })), + OpTxType::Deposit => Ok(Self::Deposit(OpDepositReceipt { + inner: Receipt { status, cumulative_gas_used, logs }, + deposit_nonce, + deposit_receipt_version, + })), + } + } +} + +impl Eip2718EncodableReceipt for OpReceipt { + fn eip2718_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize { + !self.tx_type().is_legacy() as usize + self.rlp_header_inner(bloom).length_with_payload() + } + + fn eip2718_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) { + if !self.tx_type().is_legacy() { + out.put_u8(self.tx_type() as u8); + } + self.rlp_header_inner(bloom).encode(out); + self.rlp_encode_fields(bloom, out); + } +} + +impl RlpEncodableReceipt for OpReceipt { + fn rlp_encoded_length_with_bloom(&self, bloom: &Bloom) -> usize { + let mut len = self.eip2718_encoded_length_with_bloom(bloom); + if !self.tx_type().is_legacy() { + len += Header { + list: false, + payload_length: self.eip2718_encoded_length_with_bloom(bloom), + } + .length(); + } + + len + } + + fn rlp_encode_with_bloom(&self, bloom: &Bloom, out: &mut dyn BufMut) { + if !self.tx_type().is_legacy() { + Header { list: false, payload_length: self.eip2718_encoded_length_with_bloom(bloom) } + .encode(out); + } + self.eip2718_encode_with_bloom(bloom, out); + } +} + +impl RlpDecodableReceipt for OpReceipt { + fn rlp_decode_with_bloom(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header_buf = &mut &**buf; + let header = Header::decode(header_buf)?; + + // Legacy receipt, reuse initial buffer without advancing + if header.list { + return Self::rlp_decode_inner(buf, OpTxType::Legacy); + } + + // Otherwise, advance the buffer and try decoding type flag followed by receipt + *buf = *header_buf; + + let remaining = buf.len(); + let tx_type = OpTxType::decode(buf)?; + let this = Self::rlp_decode_inner(buf, tx_type)?; + + if buf.len() + header.payload_length != remaining { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + Ok(this) + } +} + +impl Encodable2718 for OpReceipt { + fn encode_2718_len(&self) -> usize { + !self.tx_type().is_legacy() as usize + + self.rlp_header_inner_without_bloom().length_with_payload() + } + + fn encode_2718(&self, out: &mut dyn BufMut) { + if !self.tx_type().is_legacy() { + out.put_u8(self.tx_type() as u8); + } + self.rlp_header_inner_without_bloom().encode(out); + self.rlp_encode_fields_without_bloom(out); + } +} + +impl Decodable2718 for OpReceipt { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + Ok(Self::rlp_decode_inner_without_bloom(buf, OpTxType::try_from(ty)?)?) + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + Ok(Self::rlp_decode_inner_without_bloom(buf, OpTxType::Legacy)?) + } +} + +impl Encodable for OpReceipt { + fn encode(&self, out: &mut dyn BufMut) { + self.network_encode(out); + } + + fn length(&self) -> usize { + self.network_len() + } +} + +impl Decodable for OpReceipt { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self::network_decode(buf)?) + } +} + +impl TxReceipt for OpReceipt { + type Log = Log; + + fn status_or_post_state(&self) -> Eip658Value { + self.as_receipt().status_or_post_state() + } + + fn status(&self) -> bool { + self.as_receipt().status() + } + + fn bloom(&self) -> Bloom { + self.as_receipt().bloom() + } + + fn cumulative_gas_used(&self) -> u64 { + self.as_receipt().cumulative_gas_used() + } + + fn logs(&self) -> &[Log] { + self.as_receipt().logs() + } + + fn into_logs(self) -> Vec { + match self { + Self::Legacy(receipt) + | Self::Eip2930(receipt) + | Self::Eip1559(receipt) + | Self::Eip7702(receipt) => receipt.logs, + Self::Deposit(receipt) => receipt.inner.logs, + } + } +} + +impl Typed2718 for OpReceipt { + fn ty(&self) -> u8 { + self.tx_type().into() + } +} + +impl IsTyped2718 for OpReceipt { + fn is_type(type_id: u8) -> bool { + ::is_type(type_id) + } +} + +impl OpTxReceipt for OpReceipt { + fn deposit_nonce(&self) -> Option { + match self { + Self::Deposit(receipt) => receipt.deposit_nonce, + _ => None, + } + } + + fn deposit_receipt_version(&self) -> Option { + match self { + Self::Deposit(receipt) => receipt.deposit_receipt_version, + _ => None, + } + } +} + +impl From for OpReceipt { + fn from(envelope: super::OpReceiptEnvelope) -> Self { + match envelope { + super::OpReceiptEnvelope::Legacy(receipt) => Self::Legacy(receipt.receipt), + super::OpReceiptEnvelope::Eip2930(receipt) => Self::Eip2930(receipt.receipt), + super::OpReceiptEnvelope::Eip1559(receipt) => Self::Eip1559(receipt.receipt), + super::OpReceiptEnvelope::Eip7702(receipt) => Self::Eip7702(receipt.receipt), + super::OpReceiptEnvelope::Deposit(receipt) => Self::Deposit(OpDepositReceipt { + deposit_nonce: receipt.receipt.deposit_nonce, + deposit_receipt_version: receipt.receipt.deposit_receipt_version, + inner: receipt.receipt.inner, + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloy_primitives::{Bytes, address, b256, bytes, hex_literal::hex}; + use alloy_rlp::Encodable; + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn encode_legacy_receipt() { + let expected = hex!( + "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff" + ); + + let mut data = Vec::with_capacity(expected.length()); + let receipt = ReceiptWithBloom { + receipt: OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(false), + cumulative_gas_used: 0x1, + logs: vec![Log::new_unchecked( + address!("0x0000000000000000000000000000000000000011"), + vec![ + b256!("0x000000000000000000000000000000000000000000000000000000000000dead"), + b256!("0x000000000000000000000000000000000000000000000000000000000000beef"), + ], + bytes!("0100ff"), + )], + }), + logs_bloom: [0; 256].into(), + }; + + receipt.encode(&mut data); + + // check that the rlp length equals the length of the expected rlp + assert_eq!(receipt.length(), expected.len()); + assert_eq!(data, expected); + } + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn decode_legacy_receipt() { + let data = hex!( + "f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff" + ); + + // EIP658Receipt + let expected = ReceiptWithBloom { + receipt: OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(false), + cumulative_gas_used: 0x1, + logs: vec![Log::new_unchecked( + address!("0x0000000000000000000000000000000000000011"), + vec![ + b256!("0x000000000000000000000000000000000000000000000000000000000000dead"), + b256!("0x000000000000000000000000000000000000000000000000000000000000beef"), + ], + bytes!("0100ff"), + )], + }), + logs_bloom: [0; 256].into(), + }; + + let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + } + + #[test] + fn decode_deposit_receipt_regolith_roundtrip() { + let data = hex!( + "b901107ef9010c0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf" + ); + + // Deposit Receipt (post-regolith) + let expected = ReceiptWithBloom { + receipt: OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 46913, + logs: vec![], + }, + deposit_nonce: Some(4012991), + deposit_receipt_version: None, + }), + logs_bloom: [0; 256].into(), + }; + + let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + + let mut buf = Vec::with_capacity(data.len()); + receipt.encode(&mut buf); + assert_eq!(buf, &data[..]); + } + + #[test] + fn decode_deposit_receipt_canyon_roundtrip() { + let data = hex!( + "b901117ef9010d0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf01" + ); + + // Deposit Receipt (post-canyon) + let expected = ReceiptWithBloom { + receipt: OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 46913, + logs: vec![], + }, + deposit_nonce: Some(4012991), + deposit_receipt_version: Some(1), + }), + logs_bloom: [0; 256].into(), + }; + + let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + + let mut buf = Vec::with_capacity(data.len()); + expected.encode(&mut buf); + assert_eq!(buf, &data[..]); + } + + #[test] + fn gigantic_receipt() { + let receipt = OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 16747627, + logs: vec![ + Log::new_unchecked( + address!("0x4bf56695415f725e43c3e04354b604bcfb6dfb6e"), + vec![b256!( + "0xc69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9" + )], + Bytes::from(vec![1; 0xffffff]), + ), + Log::new_unchecked( + address!("0xfaca325c86bf9c2d5b413cd7b90b209be92229c2"), + vec![b256!( + "0x8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2" + )], + Bytes::from(vec![1; 0xffffff]), + ), + ], + }); + + let _bloom = receipt.bloom(); + let mut encoded = vec![]; + receipt.encode(&mut encoded); + + let decoded = OpReceipt::decode(&mut &encoded[..]).unwrap(); + assert_eq!(decoded, receipt); + } + + #[test] + fn test_encode_2718_length() { + let receipt = ReceiptWithBloom { + receipt: OpReceipt::Eip1559(Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 21000, + logs: vec![], + }), + logs_bloom: Bloom::default(), + }; + + let encoded = receipt.encoded_2718(); + assert_eq!( + encoded.len(), + receipt.encode_2718_len(), + "Encoded length should match the actual encoded data length" + ); + + // Test for legacy receipt as well + let legacy_receipt = ReceiptWithBloom { + receipt: OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 21000, + logs: vec![], + }), + logs_bloom: Bloom::default(), + }; + + let legacy_encoded = legacy_receipt.encoded_2718(); + assert_eq!( + legacy_encoded.len(), + legacy_receipt.encode_2718_len(), + "Encoded length for legacy receipt should match the actual encoded data length" + ); + } +} diff --git a/crates/rpc-types-engine/Cargo.toml b/crates/rpc-types-engine/Cargo.toml index 76190e840..9428a7ba1 100644 --- a/crates/rpc-types-engine/Cargo.toml +++ b/crates/rpc-types-engine/Cargo.toml @@ -44,7 +44,7 @@ derive_more = { workspace = true, features = ["as_ref", "deref_mut"] } arbtest.workspace = true serde_json.workspace = true arbitrary = { workspace = true, features = ["derive"] } -alloy-primitives = { workspace = true, features = ["arbitrary"] } +alloy-primitives = { workspace = true, features = ["arbitrary", "getrandom"] } [features] default = ["std", "serde"] diff --git a/crates/rpc-types-engine/src/envelope.rs b/crates/rpc-types-engine/src/envelope.rs index f0bd35c28..f87afb2f8 100644 --- a/crates/rpc-types-engine/src/envelope.rs +++ b/crates/rpc-types-engine/src/envelope.rs @@ -3,13 +3,17 @@ //! This module uses the `snappy` compression algorithm to decompress the payload. //! The license for snappy can be found in the `SNAPPY-LICENSE` at the root of the repository. -use crate::{OpExecutionPayload, OpExecutionPayloadSidecar, OpExecutionPayloadV4}; +use crate::{ + OpExecutionPayload, OpExecutionPayloadSidecar, OpExecutionPayloadV4, OpFlashblockError, + OpFlashblockPayload, +}; use alloc::vec::Vec; use alloy_consensus::{Block, BlockHeader, Sealable, Transaction}; use alloy_eips::{Encodable2718, eip4895::Withdrawal, eip7685::Requests}; use alloy_primitives::{B256, Signature, keccak256}; use alloy_rpc_types_engine::{ - CancunPayloadFields, ExecutionPayloadInputV2, ExecutionPayloadV3, PraguePayloadFields, + CancunPayloadFields, ExecutionPayloadInputV2, ExecutionPayloadV1, ExecutionPayloadV2, + ExecutionPayloadV3, PraguePayloadFields, }; /// A thin wrapper around [`OpExecutionPayload`] that includes the parent beacon block root. @@ -149,6 +153,121 @@ impl OpExecutionData { Self::new(payload, sidecar) } + /// Conversion from a vec of [`OpFlashblockPayload`]. Also returns the + /// [`OpExecutionPayloadSidecar`] extracted from the payloads. + /// + /// # Validation + /// + /// This method performs the following validations: + /// - At least one flashblock must be present + /// - Indices must be sequential starting from 0 + /// - First flashblock (index 0) must have a base payload + /// - Only the first flashblock may have a base payload + /// + /// # Errors + /// + /// Returns an error if any validation fails. + pub fn from_flashblocks( + flashblocks: &[OpFlashblockPayload], + ) -> Result { + // Validate we have at least one flashblock + if flashblocks.is_empty() { + return Err(OpFlashblockError::MissingPayload); + } + + // Validate indices are sequential starting from 0 + for (i, fb) in flashblocks.iter().enumerate() { + if fb.index as usize != i { + return Err(OpFlashblockError::InvalidIndex); + } + } + + // Validate first flashblock has base and extract it + let first = flashblocks.first().unwrap(); // Safe: checked empty above + if first.base.is_none() { + return Err(OpFlashblockError::MissingBasePayload); + } + + // Validate no other flashblocks have base (only first should have it) + for fb in flashblocks.iter().skip(1) { + if fb.base.is_some() { + return Err(OpFlashblockError::UnexpectedBasePayload); + } + } + + Ok(Self::from_flashblocks_unchecked(flashblocks)) + } + + /// Conversion from a vec of [`OpFlashblockPayload`] without validation. + /// + /// This is a faster alternative to [`Self::from_flashblocks`] that skips all validation + /// checks. Use this method only when you are certain the input data is valid. + /// + /// # Safety Requirements + /// + /// The caller must ensure: + /// - At least one flashblock is present + /// - Indices are sequential starting from 0 + /// - First flashblock (index 0) has a base payload + /// - Only the first flashblock has a base payload + /// + /// # Panics + /// + /// Panics if any of the safety requirements are violated. + pub fn from_flashblocks_unchecked(flashblocks: &[OpFlashblockPayload]) -> Self { + // Extract base from first flashblock + // SAFETY: Caller guarantees at least one flashblock exists with base payload + let first = flashblocks.first().expect("flashblocks must not be empty"); + let base = first.base.as_ref().expect("first flashblock must have base payload"); + + // Get the final state from the last flashblock + // SAFETY: Caller guarantees at least one flashblock exists + let diff = &flashblocks.last().expect("flashblocks must not be empty").diff; + + // Collect all transactions and withdrawals from all flashblocks + let (transactions, withdrawals) = + flashblocks.iter().fold((Vec::new(), Vec::new()), |(mut txs, mut withdrawals), p| { + txs.extend(p.diff.transactions.iter().cloned()); + withdrawals.extend(p.diff.withdrawals.iter().cloned()); + (txs, withdrawals) + }); + + let v3 = ExecutionPayloadV3 { + blob_gas_used: diff.blob_gas_used.unwrap_or(0), + excess_blob_gas: 0, + payload_inner: ExecutionPayloadV2 { + withdrawals, + payload_inner: ExecutionPayloadV1 { + parent_hash: base.parent_hash, + fee_recipient: base.fee_recipient, + state_root: diff.state_root, + receipts_root: diff.receipts_root, + logs_bloom: diff.logs_bloom, + prev_randao: base.prev_randao, + block_number: base.block_number, + gas_limit: base.gas_limit, + gas_used: diff.gas_used, + timestamp: base.timestamp, + extra_data: base.extra_data.clone(), + base_fee_per_gas: base.base_fee_per_gas, + block_hash: diff.block_hash, + transactions, + }, + }, + }; + + // Before Isthmus hardfork, withdrawals_root was not included. + // A zero withdrawals_root indicates a pre-Isthmus flashblock. + if diff.withdrawals_root == B256::ZERO { + return Self::v3(v3, Vec::new(), base.parent_beacon_block_root); + } + + let v4 = + OpExecutionPayloadV4 { withdrawals_root: diff.withdrawals_root, payload_inner: v3 }; + + Self::v4(v4, Vec::new(), base.parent_beacon_block_root, Default::default()) + } + /// Creates a new instance from args to engine API method `newPayloadV2`. /// /// Spec: @@ -604,4 +723,222 @@ mod tests { let encoded = payload_envelop.encode_v4().unwrap(); assert_eq!(data, encoded); } + + // Helper function to create a test flashblock + #[cfg(test)] + fn create_test_flashblock(index: u64, with_base: bool) -> OpFlashblockPayload { + use crate::flashblock::{ + OpFlashblockPayloadBase, OpFlashblockPayloadDelta, OpFlashblockPayloadMetadata, + }; + use alloc::collections::BTreeMap; + use alloy_primitives::{Address, Bloom, Bytes, U256}; + use alloy_rpc_types_engine::PayloadId; + + let base = if with_base { + Some(OpFlashblockPayloadBase { + parent_beacon_block_root: B256::ZERO, + parent_hash: B256::ZERO, + fee_recipient: Address::ZERO, + prev_randao: B256::ZERO, + block_number: 100, + gas_limit: 30_000_000, + timestamp: 1234567890, + extra_data: Bytes::default(), + base_fee_per_gas: U256::from(1000000000u64), + }) + } else { + None + }; + + let diff = OpFlashblockPayloadDelta { + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::ZERO, + gas_used: 21000, + block_hash: B256::ZERO, + transactions: Vec::new(), + withdrawals: Vec::new(), + withdrawals_root: B256::from([1u8; 32]), // Non-zero for Isthmus + blob_gas_used: Some(0), + }; + + let metadata = OpFlashblockPayloadMetadata { + block_number: 100, + new_account_balances: BTreeMap::new(), + receipts: BTreeMap::new(), + }; + + OpFlashblockPayload { payload_id: PayloadId::new([1u8; 8]), index, base, diff, metadata } + } + + #[test] + fn test_from_flashblocks_empty_vec() { + let result = OpExecutionData::from_flashblocks(&[]); + assert!(matches!(result, Err(OpFlashblockError::MissingPayload))); + } + + #[test] + fn test_from_flashblocks_non_sequential_indices() { + let fb1 = create_test_flashblock(0, true); + let fb2 = create_test_flashblock(2, false); // Skip index 1 + + let result = OpExecutionData::from_flashblocks(&[fb1, fb2]); + assert!(matches!(result, Err(OpFlashblockError::InvalidIndex))); + } + + #[test] + fn test_from_flashblocks_missing_base_in_first() { + let fb1 = create_test_flashblock(0, false); // First should have base + + let result = OpExecutionData::from_flashblocks(&[fb1]); + assert!(matches!(result, Err(OpFlashblockError::MissingBasePayload))); + } + + #[test] + fn test_from_flashblocks_unexpected_base_in_second() { + let fb1 = create_test_flashblock(0, true); + let fb2 = create_test_flashblock(1, true); // Should not have base + + let result = OpExecutionData::from_flashblocks(&[fb1, fb2]); + assert!(matches!(result, Err(OpFlashblockError::UnexpectedBasePayload))); + } + + #[test] + fn test_from_flashblocks_single_valid_flashblock() { + let fb1 = create_test_flashblock(0, true); + + let result = OpExecutionData::from_flashblocks(&[fb1]); + assert!(result.is_ok(), "Single valid flashblock should succeed"); + } + + #[test] + fn test_from_flashblocks_multiple_valid_flashblocks() { + let fb1 = create_test_flashblock(0, true); + let fb2 = create_test_flashblock(1, false); + let fb3 = create_test_flashblock(2, false); + + let result = OpExecutionData::from_flashblocks(&[fb1, fb2, fb3]); + assert!(result.is_ok(), "Multiple valid flashblocks should succeed"); + } + + #[test] + fn test_from_flashblocks_wrong_first_index() { + let fb1 = create_test_flashblock(1, true); // Should be index 0 + let result = OpExecutionData::from_flashblocks(&[fb1]); + assert!(matches!(result, Err(OpFlashblockError::InvalidIndex))); + } + + // Real-world test case from Unichain Sepolia + // + #[test] + #[cfg(feature = "serde")] + fn test_from_flashblocks_unichain_sepolia_block() { + use alloy_primitives::{address, b256}; + + let raw_sequence = r#"[{"payload_id":"0x03c446f063e3735a","index":0,"base":{"parent_beacon_block_root":"0xf6d335a6b2b4fd8fb539cd51a49769df4d53c31a90c54dd270e54542638ff101","parent_hash":"0x06ff95a9cd23b0328da74a984aa986b2e01d377dab1825f1029e39ece6c4a3ea","fee_recipient":"0x4200000000000000000000000000000000000011","prev_randao":"0x8beee738d20a9d77c5f27e9cb799ebe5b536f0985efad5f7d77ebff47f092c4a","block_number":"0x21e3b52","gas_limit":"0x3938700","timestamp":"0x690be89e","extra_data":"0x00000000320000000c","base_fee_per_gas":"0x33"},"diff":{"state_root":"0xb29a9bcae8cf3ae6d68985fcd70db80b3818cd629c9d5da0bb116451739b2078","receipts_root":"0x91d8ad10740ccfc1bd848fba0e02668d95769c08eeea30f10698692ba86c6159","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","gas_used":"0x10994","block_hash":"0xa66f8562a861f906a2438d7d6ba79495640d98d9c6922b9605c54b57f97a345c","transactions":["0x7ef90104a035dd2ec802504a143048c7830f8f570e0d6cf5147217af869939c6b4ba710a3694deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be000007d0000dbba0000000000000000800000000690be848000000000092042e000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000010ffd7e2fb2c36e5f27c015872ce733a7b4f3fc0f4ee668d7469c557c48f8250f0000000000000000000000004ab3387810ef500bfe05a49dc53a44c222cbab3e000000000000000000000000","0x02f87e8205158401c8ea9180338255789400000000000000000000000000000000000000008096426c6f636b204e756d6265723a203335353335363938c080a091f83058c881d9ad71c179ce680326501702eb68150d20b2bf7786e388f954a2a0180185d83e503f11bf3c265c1f9296ed8d3d7c04031cd8bb30509ad188ce7bbc"],"withdrawals":[],"withdrawals_root":"0x62ed62e0391b081bf172f287fbbe75e87d8a6c22f1d3b1f1aef4788c134633d2"},"metadata":{"block_number":35535698,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c446f063e3735a","index":1,"base":null,"diff":{"state_root":"0xfb1794f74d405b345672c57a5053c6105cc55c8e63f96fb0db5b0260df42413a","receipts_root":"0x1eaaaeb9d43bead7d32b90f1b320589174c63d2fa8f5fd366f841a205b1eb2e0","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000004040000000000000000000000000000000000000000000000000000000000000000000000","gas_used":"0x18f7d","block_hash":"0x67b0521ebfcb03d6ce2b6e1bad9c9c66795365f63ad8dc51e1e8f582a5ab7821","transactions":["0x02f86c8205158401c8ea92803382880994f878f0340bf132c28f3211e8b46c569edf81749580843fd553e8c001a0d73ce313aafea312e0b7244767e45f8b05d50305e0f4e4c3c564ddc751666815a02ee015ce2363311823c0b2e96bfb0e8090fd53c6cdd99be8cf343af123036dfc"],"withdrawals":[],"withdrawals_root":"0x62ed62e0391b081bf172f287fbbe75e87d8a6c22f1d3b1f1aef4788c134633d2"},"metadata":{"block_number":35535698,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c446f063e3735a","index":2,"base":null,"diff":{"state_root":"0x90dd105c4a2a0dd9ffe994204bfa3e2b4f70f7ea760d5cb9a4263f26a89f91b4","receipts_root":"0x0fff0488aa3732c34018b938839ab2f0caa96018221e4ffaeca011fb06ba288f","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000004040000000000000000000000000000000000000000000000000000000000000000000000","gas_used":"0x21566","block_hash":"0x720feb7457110a565b479fafbaa89cc984f5d673846a27d44bbb8cf5200b32fe","transactions":["0x02f86c8205158401c8ea93803382880994f878f0340bf132c28f3211e8b46c569edf81749580843fd553e8c001a0f8cd94080642e116bc772f36a02d002505227aa542e1c13e5129ab40b8b037fba00608318d3895388e39b218bcb275380cebc566e68f26d3d434e32b8b58366cdf"],"withdrawals":[],"withdrawals_root":"0x62ed62e0391b081bf172f287fbbe75e87d8a6c22f1d3b1f1aef4788c134633d2"},"metadata":{"block_number":35535698,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c446f063e3735a","index":3,"base":null,"diff":{"state_root":"0x71f8c60fdfdd84cffda3b0b6af7c8ff92195918f4fc2abae750a7306521ac0dc","receipts_root":"0xa62d1d98f56ffb1464a2beb185484253df68208004306e155c0bd1519137afe6","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000004040000000000000000000000000000000000000000000000000000000000000000000000","gas_used":"0x29b4f","block_hash":"0x670844e30f7325d4f290ea375e01f7e819afca317fc7db9723e6867a184984fa","transactions":["0x02f86c8205158401c8ea94803382880994f878f0340bf132c28f3211e8b46c569edf81749580843fd553e8c080a04368492ec1d087703aaf6f5fefe4427b3bf382e5cd07133f638bb6701f15fe61a05e28757fbdc7e744118be36d5a1548eb7c009eefcb5dc5c5040e09c2fc6de9d8"],"withdrawals":[],"withdrawals_root":"0x62ed62e0391b081bf172f287fbbe75e87d8a6c22f1d3b1f1aef4788c134633d2"},"metadata":{"block_number":35535698,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c446f063e3735a","index":4,"base":null,"diff":{"state_root":"0x5615e4342d231c352438f0ba6a8f0f641459f67961961764b781a909969b28ad","receipts_root":"0x588e1d47b0618d7e935b20c3945cba3b7b8c00141904f79ceed20312ea502e63","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000004040000000000000000000000000000000000000000000000000000000000000000000000","gas_used":"0x32138","block_hash":"0xc463a3120c35268f610d969f5608b479332ef10953af77c7a6be806195831196","transactions":["0x02f86c8205158401c8ea95803382880994f878f0340bf132c28f3211e8b46c569edf81749580843fd553e8c080a0802ba6d4f37e3b8de96095bd0b216144f276171d16dc62a004f1a89009af5deea00f0c6250cfd1a062a1bc2bc353a5c227a980cac0f233b7be8932f2192342ec4f"],"withdrawals":[],"withdrawals_root":"0x62ed62e0391b081bf172f287fbbe75e87d8a6c22f1d3b1f1aef4788c134633d2"},"metadata":{"block_number":35535698,"new_account_balances":{},"receipts":{}}}]"#; + + let flashblocks: Vec = serde_json::from_str(raw_sequence).unwrap(); + let execution_data = OpExecutionData::from_flashblocks(&flashblocks).unwrap(); + + // Validate against expected final block state + assert_eq!( + execution_data.payload.parent_hash(), + b256!("06ff95a9cd23b0328da74a984aa986b2e01d377dab1825f1029e39ece6c4a3ea") + ); + assert_eq!( + execution_data.payload.block_hash(), + b256!("c463a3120c35268f610d969f5608b479332ef10953af77c7a6be806195831196") + ); + assert_eq!(execution_data.payload.block_number(), 0x21E3B52); + assert_eq!(execution_data.payload.timestamp(), 0x690be89e); + assert_eq!( + execution_data.payload.fee_recipient(), + address!("4200000000000000000000000000000000000011") + ); + assert_eq!(execution_data.payload.gas_limit(), 0x3938700); + assert_eq!(execution_data.payload.as_v1().gas_used, 0x32138); + assert_eq!( + execution_data.payload.as_v1().state_root, + b256!("5615e4342d231c352438f0ba6a8f0f641459f67961961764b781a909969b28ad") + ); + assert_eq!( + execution_data.payload.as_v1().receipts_root, + b256!("588e1d47b0618d7e935b20c3945cba3b7b8c00141904f79ceed20312ea502e63") + ); + assert_eq!(execution_data.payload.transactions().len(), 6); + assert_eq!( + execution_data.payload.as_v4().unwrap().withdrawals_root, + b256!("62ed62e0391b081bf172f287fbbe75e87d8a6c22f1d3b1f1aef4788c134633d2") + ); + + // Verify parent beacon block root + assert_eq!( + execution_data.parent_beacon_block_root(), + Some(b256!("f6d335a6b2b4fd8fb539cd51a49769df4d53c31a90c54dd270e54542638ff101")) + ); + } + + // Real-world test case from Base Sepolia + // Block #33439826 with 11 flashblocks (indices 0-10) + #[test] + #[cfg(feature = "serde")] + fn test_from_flashblocks_base_sepolia_block() { + use alloy_primitives::{address, b256}; + + let raw_sequence = r#"[{"payload_id":"0x03c33cc62b81edb6","index":0,"base":{"parent_beacon_block_root":"0xf058b1e43890ed5f838bd07e77db06d075d894343d1b31f6099a345b0d8f7d1b","parent_hash":"0x6ffd2714d5af6c412c57db3f664a5a127516573bbd987fd242d06f71ea662741","fee_recipient":"0x4200000000000000000000000000000000000011","prev_randao":"0x9985c1f8ec25b468cbf2b727a8371b4554b7e7adb059c08abf7a7d51d86ceee5","block_number":"0x1fe4052","gas_limit":"0x3938700","timestamp":"0x690fdf84","extra_data":"0x000000003200000004","base_fee_per_gas":"0x34"},"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0x1b2fa5e4cbbc1f8c01a7c7204571ebe339dbdfadc666451d8e70d5c10c99830f","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","gas_used":"0xb41c","block_hash":"0x87c6775cc427caf4c0ffe0d4b6d76627536f38d77d23f105f9f104ef3e5541c7","transactions":["0x7ef90104a01c055ffd19ea027da4a8aae0a2734c6bf17c3f487d4cc22931d7dbe261409cda94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8b0098999be0000044d000a118b000000000000000400000000690fde3c00000000009252e3000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014f1595c3798e3082aa093e433bd5cbd102a11f9619d20e6e821c1a30fb56b12b000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3000000000000000000000000"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":1,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0xe38b2090ddfa6ee25b15a8ebcdd7ecc0f1ee9128ec98cb24f47909e29e11832e","logs_bloom":"0x00000000000000000000000020000000040080000000000000020005000000004000000040040000000080000000000000000000000000000002000000000000008000000000000000000000000000014000000000800000000000000000000000000000000000040100000000000000000000000100000000000380008a02000000100000400200000100800000000000000000000004001000200000000000000000000800020000000000400000000000000000008000400801080000000000005000000400000000000000000000000110000000000000000000000000100200021004400010000000010000000400000008002000004080000000000000","gas_used":"0x9d2f2","block_hash":"0x4548d5014de4883cec380838f1b225996fa3c08c176f2f63d98d8c23169fab44","transactions":["0x02f89283014a348202ea830f4275830f427583045dd594a449bc031fa0b815ca14fafd0c5edb75ccd9c80f80a4de0e9a3e000000000000000000000000000000000000000000000001236efcbcbb340000c001a0742ff606597cda39751dd369e66e9978946ce8f4eb578a8d73314535a2df4388a06a6f83c3606c32e1677f62408b8ec69b09a82f499395b26eaefea567deb83843","0x02f9101583014a34830597bd830f4240830f42aa8306aecc9442826e92e6418877459f0920cb058e462ac6a0a480b90fa4dbaa1e6400000000000000000000000000a739e4479c97289801654ec1a52a67077613c000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000691d0e7f4f6ae70adc2708ec4857d3d5ca54a11710c9ac11989b1cb3d3d8d3298a78f6a50000000000000000000000000000000000000000000000000000000000000f200000000000000000000000000000000000000000000000000000000000000e44b653f0c300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000033bea00000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004747970650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026f6b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000086f6b2e746f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003657468000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000086f6b2e74785f69640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046626173653a3078343865643835396232636630633962366261633864373134653162363436313264313232346436643a38343533323a33333433393832323a3333393131333600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000000000000000000008c00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004747970650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087769746864726177000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001977697468647261772e73656e6465722e636861696e5f7569640000000000000000000000000000000000000000000000000000000000000000000000000000046261736500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001777697468647261772e73656e6465722e61646472657373000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783438656438353962326366306339623662616338643731346531623634363132643132323464366400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e77697468647261772e746f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036574680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000f77697468647261772e616d6f756e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001431303030303030303030303030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002f77697468647261772e63726f73735f636861696e5f6164647265737365732e302e757365722e636861696e5f756964000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000077365706f6c6961000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002d77697468647261772e63726f73735f636861696e5f6164647265737365732e302e757365722e6164647265737300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307834386564383539623263663063396236626163386437313465316236343631326431323234643664000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000003977697468647261772e63726f73735f636861696e5f6164647265737365732e302e6c696d69742e6c6573735f7468616e5f6f725f657175616c0000000000000000000000000000000000000000000000000000000000000000000000000000143130303030303030303030303030303030303030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e77697468647261772e74785f69640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046626173653a3078343865643835396232636630633962366261633864373134653162363436313264313232346436643a38343533323a33333433393832323a333339313133360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041ffb578b6e9ab1699e4d9cd0078d9f28e7f0ef2136a11596aa7b6d7fe7f896dd353b7b786bf155c924f35d5099f0df90650e74a5858b75673835d24ac6dc8f1e41b00000000000000000000000000000000000000000000000000000000000000c080a09c4f42d262ed1f1bee31461fd10d8d8fbac6e340d9bc2b8035df5faa30f88d4da06d832693c1e28d4f647a6ff08f5d037d08ad2599964a9f3600396efdaec07e4a"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":2,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0xda7caba0b5682eda3aed5f47132da84aa2c2757499c23d609aa73dd3a449be1d","logs_bloom":"0x00000000000000000000000020000000040080000000000000020005000000004000000040040000000080000000000000000000000000000002000000000040008000000000000000000000000000014000000800800000000004000000000000000000000400040100000000000000000002800100000000000b80008a02000000100000400200000100800000000000000000000004001000200000000000000000000800020000000000400000000000000000008000440801080200000000005000000400000000000000000000000110000000000008000000000000100200021004400010000000110000000400000008002000004080010000000000","gas_used":"0xd6a91","block_hash":"0x17e106bfeebb2ff0123cf2e1f555e0441ed308773224513dc4ac6257d943e52c","transactions":["0x02f89283014a3482015f830f4275830f427583045dc694a449bc031fa0b815ca14fafd0c5edb75ccd9c80f80a4de0e9a3e000000000000000000000000000000000000000000000000c249fdd327780000c001a098b7dd6d4454a8d31170b5b2d1461bc8a74eed745eddc982232b2c1483cba322a07d3acfe989366b2729aa728ebca7009c15dc908954a9fb5459b75cff1bfd103f"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":3,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0xda7caba0b5682eda3aed5f47132da84aa2c2757499c23d609aa73dd3a449be1d","logs_bloom":"0x00000000000000000000000020000000040080000000000000020005000000004000000040040000000080000000000000000000000000000002000000000040008000000000000000000000000000014000000800800000000004000000000000000000000400040100000000000000000002800100000000000b80008a02000000100000400200000100800000000000000000000004001000200000000000000000000800020000000000400000000000000000008000440801080200000000005000000400000000000000000000000110000000000008000000000000100200021004400010000000110000000400000008002000004080010000000000","gas_used":"0xd6a91","block_hash":"0x17e106bfeebb2ff0123cf2e1f555e0441ed308773224513dc4ac6257d943e52c","transactions":[],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":4,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0xaff50907a173fc423a499319437afffb8abc2071ce36b6f040dc487579a5d4c3","logs_bloom":"0x0002800000000000002000012000040004008000000010000012000500000000480000004004000000918000000000000000000000000000000200821000806000800010000000000000000800000001c000000800800000000004202000000800000000000400040100020100000000000002800100000000000b90008a02000000100000480200000100800010080400000000000004001000224080000000000000008c0002040080000840000000000000000100c000c4080108020000000001500a000400000000000000000000100110000020000008000000000000100a00221004400010000000110100000400100008002100004280010000000000","gas_used":"0x1498a3","block_hash":"0x4764a20ee262986e45d29251db593320bd4bf6de1133de553b6363a5691e7644","transactions":["0x02f89283014a348203af830f4275830f427583045dd594a449bc031fa0b815ca14fafd0c5edb75ccd9c80f80a4de0e9a3e000000000000000000000000000000000000000000000002017a67f731740000c001a04ce59ff67dc25a76f3027441513f916b809f55b29d5de4fecd4aa0136a3a1a4fa02c1b32b3a1600f6bb2365130797238162cbc797843169a4cfb1ebb41465877c7","0x02f8d483014a348309087a830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b781777000000000000000000000000022e40d0a0c0bb77b570445fb59d39bcf14790b660000000000000000000000000000000000000000000000004a61b425a5ee98000000000000000000000000000000000000000000000000000006431e74449860c001a002c2402941acdc25bcaae67c62d58f1a942b32723827f77972c74b159b2c174ea04772118ec71bc7fbe0c9f1c9ef90f58927126480ca769d73704365bfbac65db3","0x02f8d483014a348308c06b830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b78177700000000000000000000000005643a7772017c8544d3841894c1f7c264cd05ffe0000000000000000000000000000000000000000000000000b035a61b2e8be000000000000000000000000000000000000000000000000000006431e7446c578c001a0ac31a5ad06a3897a0c1a909770badf8cec728abd2daf4d125a551778fa597124a013b1de6f741139d957f299bf22de0a91c1d8a4f2ade6743ddcec89bcc9e8b07d","0x02f8d483014a34830922b1830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b7817770000000000000000000000000576831e77af4b5425b39efb23528441b79ee71e20000000000000000000000000000000000000000000000002bed26c4505ca4000000000000000000000000000000000000000000000000000006431e7446f712c080a0c105ef2c930e95694d112028a642399e5a56ce6416f9b8df9ad27baa26244483a064f6e5881fa728b7afaa2e2ddd62c3182789cb247f90b6276c14f8bfc1b4f2cf"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":5,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0x6d12b13dcae85ef97ec3756b317ac9d33752bcd231a9323046ecd5a65e8ca8a2","logs_bloom":"0x0002800000000000002000012000040004008000000010000012000500000000480000004004000000918000000000000000000000000100000200821000806000800010000000000000000800000001c000000800800000000004202000000800000000000400040100020100000000000002800100000000004b90008a02000000100000480200000100800010080400000000000004001000224080000000000000008c0002040080000840000000000000000100c000c4080108020000000001500a000400000000000000000000100110004020000008000000000000100a00221004400010000000110100000400100008002100004280010000000000","gas_used":"0x153998","block_hash":"0x810679ccd05f90093eb0e88549d52ad196214f3a4a555cf0b06201f30aa61a2d","transactions":["0x02f8d483014a34830966ae830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b781777000000000000000000000000046195a8573f2610bba630bb0bd5c21c064594f3a0000000000000000000000000000000000000000000000002c94bc176f7cb4000000000000000000000000000000000000000000000000000006431e743d37eac080a053f1881c67ad8fa9838d83943afe83b6498dae96a13a019704f25e0df515dbdba05eef8e08269eaafd63ba7e14e13d73e03ec5e7fad5bcdbaaabc124da41e8e32c"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":6,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0x1b76e086c31a8a08d1c4a93b868b00238faabd4d52d9e75e55a4abf3a75e65d8","logs_bloom":"0x0002800000000000002000012000048004008000000010000012000500000000480000004004000000918000000000000000000000000100000200821000806000800010000000000000000800000001c008000800800000000004202000000800000000000400040100020100000000000002800100000000004b90008a02000000100000480200000100800010080400000000000004001000224080000000000000008c0002040080000840000000000000000100c000c4080108020000010001500a000400000000000001000000100110004021000008000000000000100a00221004400010000000110100000400100008002100004280010000000001","gas_used":"0x189dc4","block_hash":"0xfdf2cbb452a36c9c4033d1c0bc2b3dd9cee7ba91d0ca5488aa3d9a23b127b79f","transactions":["0x02f89383014a348304e447830f4240830f42a8830226b494cd997aef0b9a1d8c02a16204ccce354844edeeff80a4f7a308060000000000000000000000000000000000000000000000000000000000016636c001a07dc2c0285cd2c53657c87826a698de9ae5bb38e2580657fe1772fc08ab53a9f2a05a183dac1ed51f6aac2eff4add4510fd76d71f9dce59a3536fc00bfbb2ac750c","0x02f8d483014a3483096a27830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b7817770000000000000000000000000fde9b0be445930f929705125fe24049093e628e4000000000000000000000000000000000000000000000001517fd24c7f6670000000000000000000000000000000000000000000000000000006431e74408803c080a036f0e0df96ee863041cc41fad376f2f88364225ff6c10c2e492da014d71ab530a03cca82dd065d09a150f75103ea2e1f2867210c604fd82592ec49fae02cadc20a","0x02f8d483014a348309a03d830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b781777000000000000000000000000088c7e4701045571734e2147bad80e3d8c56500d300000000000000000000000000000000000000000000000023e284d65ede20000000000000000000000000000000000000000000000000000006431e7441d02ac080a03ee196fff4a614411f9d41431f0b174141ae6f62246df4e54117205bb19c4f64a022f123e006139ae334de3bf7b62c06b72045ba7dc0a508d137bcd056d950da33"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":7,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0x065878c1c4d88295544c04fec2e74c9dd8b5d656e196a1b7b09ce8cadbb8f979","logs_bloom":"0x0002800000000010002000052000048004008000000010000012000500000000490000004004000000918000010000000000008000000100040200821000806800800010000000000000000800000001c008000800800000000004202000000804000020000400040100020100000000020002800100000000004b90008a02000000180000480200000100800010080400000000000004011000224080000000000000008c0002040080000840000200000000000100c000c4080108020000012001500a000400000000000001000000100110004021000008000000000000100a00221104400010000000110100000400100008002100004280010000000001","gas_used":"0x1bc281","block_hash":"0xcc9c18ed55c91e97f32353e253c69766cd0d2e0acb0e7f92098d01e1d7761ce3","transactions":["0x02f8d483014a3483091e1f830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b7817770000000000000000000000000b501c0a0f800e68d980f5253650d0cf3a69d16c00000000000000000000000000000000000000000000000000b87d57d89ffe7800000000000000000000000000000000000000000000000000006431e7442365fc001a0b276c68f59bcfb78fe7905a720e9418130d5c87d60da4b6d55faf07e1b1724aba03425daae2e51a061a26bedcd89cf6ead44146ac97f831371ec36a0192728d204","0x02f8d483014a3483094dde830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b78177700000000000000000000000000097cc7164250c464fea5f9f91d1abec7718814a0000000000000000000000000000000000000000000000004c40d37c20f440000000000000000000000000000000000000000000000000000006431e744372abc001a01f3e58f3baa5e472c08097dafe1e756163c61e7200dc90751f167e796d542f20a02c10596de8b29462c0953a023a8b6c06f74fe77ea66a24598df920d542edab3b","0x02f8d483014a3483094ece830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b781777000000000000000000000000083fe74125ec8ffaeee4b2371d7ea17f6ad6f9ba2000000000000000000000000000000000000000000000000f9e4840a6e4938000000000000000000000000000000000000000000000000000006431e744362dec001a066724129c4de96e835cd1377b55541b4582bf4ebcd7c2a3faa4231ade86b14d8a03736bce9203cc0c92878fcc28ee8710961eaddde92bf6a2158c602b4d1bbdbd7","0x02f8d483014a348303750a830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b7817770000000000000000000000000414d9179c5d2207a6e0efeb0319b6c556265974600000000000000000000000000000000000000000000000033979a45ffefac000000000000000000000000000000000000000000000000000006431e74442677c001a0682d2489ba1d9666324060a006f0abe06830cecdeed4398169dc9fbf7199eb59a02e971034255d087d02b25f45a7962b31360bbed70e3aa30e69ee8f64dd6afdb4","0x02f8d483014a348308acf2830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b781777000000000000000000000000097c152d0fa30c49603e0e3e013e36c4e29bf7fea0000000000000000000000000000000000000000000000001d58bdca2addf5000000000000000000000000000000000000000000000000000006431e744447a9c001a030e423ab3697fe4ccc5ce92232d7a642a8295f489f2e52b3c3ba2f110c828e7ca057fd4d3d0e700734568b0be067deda7927188f6a67f9600bae3d6c75d201fe57"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":8,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0x7bf525f832aecc6bf7f7b7e329779640bb4477cb47bf1bde512934c5ed45519b","logs_bloom":"0x0003800000000210002000052000048004008000000010000012000500000000490000004004000000918000010000000000008000000100040200821000806800800010000000000000000800000001c00c000800802000000004202000000804000020000400040100020108000000020002800100000000044b98008a02200000180000480200000100800010080400000000002004011000224080000000000000108c0002040080000844000200000040000100c000c4080108020000012001500a000400000000000001000000104110004021000108000000000000100a00221104400010010000110100000400100008002140004280010000000001","gas_used":"0x213d0b","block_hash":"0x5f9c957cde671b50c5661b328b7f3f8a0e56e194a954d8d7cc4274eb1e014a1e","transactions":["0x02f89283014a34820392830f4275830f427583045dd594a449bc031fa0b815ca14fafd0c5edb75ccd9c80f80a4de0e9a3e000000000000000000000000000000000000000000000002017a67f731740000c001a05c4f86d9218cfab447e6ead7abb27444f7e8d3a185a1fbfb6860a36513c89d93a01d4b9b74f049bfc10feeabcb101a18a14e774e89de35ac246e6452c05e94bc98","0x02f8d483014a348309087a830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b781777000000000000000000000000022e40d0a0c0bb77b570445fb59d39bcf14790b660000000000000000000000000000000000000000000000004a61b425a5ee98000000000000000000000000000000000000000000000000000006431e74449860c001a002c2402941acdc25bcaae67c62d58f1a942b32723827f77972c74b159b2c174ea04772118ec71bc7fbe0c9f1c9ef90f58927126480ca769d73704365bfbac65db3","0x02f8d483014a348308c06b830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b78177700000000000000000000000005643a7772017c8544d3841894c1f7c264cd05ffe0000000000000000000000000000000000000000000000000b035a61b2e8be000000000000000000000000000000000000000000000000000006431e7446c578c001a0ac31a5ad06a3897a0c1a909770badf8cec728abd2daf4d125a551778fa597124a013b1de6f741139d957f299bf22de0a91c1d8a4f2ade6743ddcec89bcc9e8b07d","0x02f8d483014a34830922b1830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b7817770000000000000000000000000576831e77af4b5425b39efb23528441b79ee71e20000000000000000000000000000000000000000000000002bed26c4505ca4000000000000000000000000000000000000000000000000000006431e7446f712c080a0c105ef2c930e95694d112028a642399e5a56ce6416f9b8df9ad27baa26244483a064f6e5881fa728b7afaa2e2ddd62c3182789cb247f90b6276c14f8bfc1b4f2cf"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":9,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0xeb419bf069b8bf9738adcb7fad118724a1d4d6a83821bc532983a2949aa0910d","logs_bloom":"0x000380000000021000200005200004800400800000001000001a000500001000490000004004000000918000010000000000008000000100040200821000806800800010000000000000000800000001c00c000800802000000004202000000804000020000400040100020108000000020002800100000000044b98008a02200000180000480200000100800010080400000000002004011000224080000000000000108c0002040080000844000200000040000100c000c4080108020000012001500a000400000000000001000000104110004021000108000000000000100a00221104400010010004110100000400100008002140004280010000000001","gas_used":"0x21de0c","block_hash":"0xb802c08c65bdefdd507fe07634ea29eeaad1859b33ffac2c426dc7b620d22b19","transactions":["0x02f8d483014a3483095beb830f4240830f42a883030d4094d89f830d7795c10613e4d4769c24c05bf60932c680b864b7817770000000000000000000000000f73c129529caa024337c39e467c720cfc45874220000000000000000000000000000000000000000000000000de4f04092790e800000000000000000000000000000000000000000000000000006431e74489081c080a0a100818c4c3ec3b0bced80f81f09fc878b23274266b45e2043956562b6714dcfa023dbcbc4df92ed5817fcc9bcd238a038aad806c69585dc8cf582e6012d012d28"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}},{"payload_id":"0x03c33cc62b81edb6","index":10,"diff":{"state_root":"0x0000000000000000000000000000000000000000000000000000000000000000","receipts_root":"0xaa280e93aa4a7d3f616ad391404411abbeebe8bc8fb1ed9b3ef4d0a42bf64ccd","logs_bloom":"0x000380000000021000200005200004800400800000001000001a000500001000490000204004000000918000010000000000008000000100040200821020886800800010000000000000000800000001c00c000800802000000004202000000804000020000400040100020108000000020002800100000000044b98008a02200000180000480200000100800010080400000000002004011000224080000000020000108c0002040080000844000200000040000100c000c4080108020000012001500a000400000000000001000000104110004021000108000000000200100a10221104400010010004110100000400100008002140004280010000000001","gas_used":"0x49f43c","block_hash":"0x2b440a266840a96993d85d45d1de1e81f7a859aaac4654dcd5a990ffa2ef947b","transactions":["0x02f90fb583014a34831d4797830f4240830f42a88327fdba94ebaff6d578733e4603b99cbdbb221482f29a78e180b90f4484779f44000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005a0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006e00000000000000000000000000000000000000000000000000000000000000780000000000000000000000000000000000000000000000000000000000000082000000000000000000000000000000000000000000000000000000000000008c000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000b400000000000000000000000000000000000000000000000000000000000000be00000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000d200000000000000000000000000000000000000000000000000000000000000dc00000000000000000000000000000000000000000000000000000000000000e600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce75532be4cf5bacb01e018950b5be900eafa59f2431fed6b869799529ab39fe0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce76343a51197104ee22e37cf9c48a9eb5c99031a25196c2f1264deb5d4d3ff80000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce770a821c08f4e200bf42a148754153d78e977260a213094b521b5625618ec70000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce78bb3bcd3592df48dcd3a6383c8f61d8434b6058f61a587dfb0c37134294420000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce79b35d157e36939c03df12e39599530f615a90e624610d8d023eaf2f8329030000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce7a6370bb580180c882bf7214d1f701529ea455f8567b2be79496c9437a2ce30000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce7bf53208371925c87cacbb0bbfbf330fc8a02818e1d73c56760a9fded7f8c80000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce7cac670fbf544ec6d7360aacecd6e3fb35ea8a6ebef6161c9563a6d16a4a200000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce7d91406552fdfe569345c8561328604a63912a36d21cafa1efed0275ce6b190000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce7e6e0b5ccd73c9cea553a19e7ab6e533bc253f552e6b9145dd5470d2612f8d0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce7fc72e52aaff88c842a2092b7ce047cf47a8f56da1035142a41b6a59b856420000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce80fcaa166cc2fd1353b40f3071a491cd7ca2746c8943caaa6c024c8df0131f0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce812aabb780f12ed0c0c5dc6932220d8c5f730c54ee63384fbfe1e7fa90a5090000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce82c1dea3b99a38cf0743f31402eba0d22c4da43e715d37533da9bc5f8ca4ae0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce835d696b1a6f5089cf9bc4c2c529e181678fa2f2feb745223e7520d885a2260000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce8400f527b7b931ddfe77007be944f58173dfc1c5928eb433ae71e96f61a8420000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce85b6dcd2b462f2d1c72e4b46ea316f9183fb9ea40866724b7eef10211a83390000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce860986c742f73c595e7cf75d5014bdccde828c0fa3891f8a7e77cbaf974e7d0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce879d5711ffb11c2d9fe9737837f55726ba0609c21d62e2783cc38db59edafa0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000053ce882976c03e7cf30e96a5a578eff196e4062258f3d859abdf161bcb5fd18356000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000c080a0c7ccb6ec845a35639b2905d243be7a6cf2ee1412331d348a4bf65f53ae89cde8a06ecc40e8297c75e86332c2924b96c6bf2334a6d1b1ef803e27c9de692906b138","0x02f8b183014a3481ad830ecd10830ecdaf82b6a994af33add7918f685b2a82c1077bd8c07d220ffa0480b844095ea7b3000000000000000000000000a449bc031fa0b815ca14fafd0c5edb75ccd9c80f00000000000000000000000000000000000000000000000c6a036eb4bc740000c001a0d1877e98821074c02cf20dc84d31d70fbc00027d404fe99f3e887a33082bb6cda016f8a55aea1573b3834180e43d90eb6c4b1ffb321d2a0be8b3aa71eeaed5104a"],"withdrawals":[],"withdrawals_root":"0x77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44"},"metadata":{"block_number":33439826,"new_account_balances":{},"receipts":{}}}]"#; + + let flashblocks: Vec = serde_json::from_str(raw_sequence).unwrap(); + let execution_data = OpExecutionData::from_flashblocks(&flashblocks).unwrap(); + + // Validate against expected final block state from base payload (index 0) + assert_eq!( + execution_data.payload.parent_hash(), + b256!("6ffd2714d5af6c412c57db3f664a5a127516573bbd987fd242d06f71ea662741") + ); + assert_eq!(execution_data.payload.block_number(), 0x1fe4052); + assert_eq!(execution_data.payload.timestamp(), 0x690fdf84); + assert_eq!( + execution_data.payload.fee_recipient(), + address!("4200000000000000000000000000000000000011") + ); + assert_eq!(execution_data.payload.gas_limit(), 0x3938700); + assert_eq!(execution_data.payload.as_v1().gas_used, 0x49f43c); + + // Base skipped state root calculation thus state root is expected to be zeros. + // And subsequently the last flashblocks' block hash is not the final block's block hash. + // Real block hash: 0x0c3c3ff081d8a5ea1239bfb8a0593f641154a06b783fa142809880e011cd6a3f + assert_eq!( + execution_data.payload.as_v1().state_root, + b256!("0000000000000000000000000000000000000000000000000000000000000000") + ); + assert_eq!( + execution_data.payload.block_hash(), + // last flashblock block hash + b256!("2b440a266840a96993d85d45d1de1e81f7a859aaac4654dcd5a990ffa2ef947b") + ); + + // Verify receipts root from last flashblock (index 10) + assert_eq!( + execution_data.payload.as_v1().receipts_root, + b256!("aa280e93aa4a7d3f616ad391404411abbeebe8bc8fb1ed9b3ef4d0a42bf64ccd") + ); + + // Verify total transaction count across all 11 flashblocks + // Index 0: 1, Index 1: 2, Index 2: 1, Index 3: 0, Index 4: 4, Index 5: 1 + // Index 6: 3, Index 7: 5, Index 8: 4, Index 9: 1, Index 10: 2 + // Total: 24 transactions + assert_eq!(execution_data.payload.transactions().len(), 24); + + // Verify withdrawals root from last flashblock + assert_eq!( + execution_data.payload.as_v4().unwrap().withdrawals_root, + b256!("77b0fb1616a212bd7cf33d7c28651f19bf6093b2c5f1967e674ec861aeaf9d44") + ); + + // Verify parent beacon block root from base payload + assert_eq!( + execution_data.parent_beacon_block_root(), + Some(b256!("f058b1e43890ed5f838bd07e77db06d075d894343d1b31f6099a345b0d8f7d1b")) + ); + } } diff --git a/crates/rpc-types-engine/src/flashblock/base.rs b/crates/rpc-types-engine/src/flashblock/base.rs new file mode 100644 index 000000000..2a2469e1c --- /dev/null +++ b/crates/rpc-types-engine/src/flashblock/base.rs @@ -0,0 +1,97 @@ +//! Flashblock base execution payload types. + +use alloy_primitives::{Address, B256, Bytes, U256}; + +/// Immutable block properties shared across all flashblocks in a sequence. +/// +/// These properties remain constant throughout the block construction process +/// and are set at the beginning of the flashblock sequence. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OpFlashblockPayloadBase { + /// Parent beacon block root. + pub parent_beacon_block_root: B256, + /// Hash of the parent block. + pub parent_hash: B256, + /// Address that receives fees for this block. + pub fee_recipient: Address, + /// The previous randao value. + pub prev_randao: B256, + /// Block number. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub block_number: u64, + /// Gas limit for this block. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub gas_limit: u64, + /// Block timestamp. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub timestamp: u64, + /// Extra data for the block. + pub extra_data: Bytes, + /// Base fee per gas for this block. + pub base_fee_per_gas: U256, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + #[cfg(feature = "serde")] + fn test_base_serde_roundtrip() { + let base = OpFlashblockPayloadBase { + parent_beacon_block_root: B256::random(), + parent_hash: B256::random(), + fee_recipient: Address::random(), + prev_randao: B256::random(), + block_number: 100, + gas_limit: 30_000_000, + timestamp: 1234567890, + extra_data: Bytes::from(vec![1, 2, 3]), + base_fee_per_gas: U256::from(1000000000u64), + }; + + let json = serde_json::to_string(&base).unwrap(); + let decoded: OpFlashblockPayloadBase = serde_json::from_str(&json).unwrap(); + assert_eq!(base, decoded); + } + + #[test] + #[cfg(feature = "serde")] + fn test_base_snake_case_serialization() { + let base = OpFlashblockPayloadBase { + parent_beacon_block_root: B256::ZERO, + parent_hash: B256::ZERO, + fee_recipient: Address::ZERO, + prev_randao: B256::ZERO, + block_number: 1, + gas_limit: 30_000_000, + timestamp: 1234567890, + extra_data: Bytes::default(), + base_fee_per_gas: U256::from(1000000000u64), + }; + + let json = serde_json::to_string(&base).unwrap(); + assert!(json.contains("parent_beacon_block_root")); + assert!(json.contains("parent_hash")); + assert!(json.contains("fee_recipient")); + assert!(json.contains("prev_randao")); + assert!(json.contains("block_number")); + assert!(json.contains("gas_limit")); + assert!(json.contains("base_fee_per_gas")); + } + + #[test] + fn test_base_default() { + let base = OpFlashblockPayloadBase::default(); + assert_eq!(base.parent_beacon_block_root, B256::ZERO); + assert_eq!(base.parent_hash, B256::ZERO); + assert_eq!(base.fee_recipient, Address::ZERO); + assert_eq!(base.block_number, 0); + assert_eq!(base.gas_limit, 0); + assert_eq!(base.timestamp, 0); + assert_eq!(base.extra_data, Bytes::default()); + assert_eq!(base.base_fee_per_gas, U256::ZERO); + } +} diff --git a/crates/rpc-types-engine/src/flashblock/delta.rs b/crates/rpc-types-engine/src/flashblock/delta.rs new file mode 100644 index 000000000..1bef4af65 --- /dev/null +++ b/crates/rpc-types-engine/src/flashblock/delta.rs @@ -0,0 +1,177 @@ +//! Flashblock delta execution payload types. + +use alloc::vec::Vec; +use alloy_eips::eip4895::Withdrawal; +use alloy_primitives::{B256, Bloom, Bytes}; + +/// Represents the modified portions of an execution payload within a flashblock. +/// This structure contains only the fields that can be updated during block construction, +/// such as state root, receipts, logs, and new transactions. Other immutable block fields +/// like parent hash and block number are excluded since they remain constant throughout +/// the block's construction. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OpFlashblockPayloadDelta { + /// The state root of the block. + pub state_root: B256, + /// The receipts root of the block. + pub receipts_root: B256, + /// The logs bloom of the block. + pub logs_bloom: Bloom, + /// The gas used of the block. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub gas_used: u64, + /// The block hash of the block. + pub block_hash: B256, + /// The transactions of the block. + pub transactions: Vec, + /// Array of [`Withdrawal`] enabled with V2 + pub withdrawals: Vec, + /// The withdrawals root of the block. + pub withdrawals_root: B256, + /// The estimated cumulative blob gas used for the block. Introduced in Jovian. + /// spec: + /// Defaults to 0 if not present (for pre-Jovian blocks). + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))] + pub blob_gas_used: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + #[cfg(feature = "serde")] + fn test_delta_serde_roundtrip() { + let delta = OpFlashblockPayloadDelta { + state_root: B256::random(), + receipts_root: B256::random(), + logs_bloom: Bloom::default(), + gas_used: 21_000, + block_hash: B256::random(), + transactions: vec![Bytes::from(vec![1, 2, 3])], + withdrawals: vec![], + withdrawals_root: B256::random(), + blob_gas_used: Some(123456), + }; + + let json = serde_json::to_string(&delta).unwrap(); + let decoded: OpFlashblockPayloadDelta = serde_json::from_str(&json).unwrap(); + assert_eq!(delta, decoded); + } + + #[test] + #[cfg(feature = "serde")] + fn test_delta_snake_case_serialization() { + let delta = OpFlashblockPayloadDelta { + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::ZERO, + gas_used: 0, + block_hash: B256::ZERO, + transactions: vec![], + withdrawals: vec![], + withdrawals_root: B256::ZERO, + blob_gas_used: Some(0), + }; + + let json = serde_json::to_string(&delta).unwrap(); + assert!(json.contains("state_root")); + assert!(json.contains("receipts_root")); + assert!(json.contains("logs_bloom")); + assert!(json.contains("gas_used")); + assert!(json.contains("block_hash")); + assert!(json.contains("withdrawals_root")); + assert!(json.contains("blob_gas_used")); + } + + #[test] + #[cfg(feature = "serde")] + fn test_delta_with_withdrawals() { + let withdrawal = Withdrawal { + index: 0, + validator_index: 1, + address: alloy_primitives::Address::ZERO, + amount: 1000, + }; + + let delta = OpFlashblockPayloadDelta { + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::ZERO, + gas_used: 0, + block_hash: B256::ZERO, + transactions: vec![], + withdrawals: vec![withdrawal], + withdrawals_root: B256::ZERO, + blob_gas_used: Some(0), + }; + + let json = serde_json::to_string(&delta).unwrap(); + let decoded: OpFlashblockPayloadDelta = serde_json::from_str(&json).unwrap(); + assert_eq!(delta.withdrawals.len(), 1); + assert_eq!(decoded.withdrawals.len(), 1); + } + + #[test] + #[cfg(feature = "serde")] + fn test_delta_blob_gas_used_none_skipped() { + // Test that None blob_gas_used is skipped in serialization (pre-Jovian) + let delta = OpFlashblockPayloadDelta { + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::ZERO, + gas_used: 0, + block_hash: B256::ZERO, + transactions: vec![], + withdrawals: vec![], + withdrawals_root: B256::ZERO, + blob_gas_used: None, + }; + + let json = serde_json::to_string(&delta).unwrap(); + // Should not contain blob_gas_used when None + assert!(!json.contains("blob_gas_used")); + + // Deserialization should work and default to None + let decoded: OpFlashblockPayloadDelta = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.blob_gas_used, None); + } + + #[test] + #[cfg(feature = "serde")] + fn test_delta_blob_gas_used_some_included() { + // Test that Some blob_gas_used is included in serialization (Jovian+) + let delta = OpFlashblockPayloadDelta { + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::ZERO, + gas_used: 0, + block_hash: B256::ZERO, + transactions: vec![], + withdrawals: vec![], + withdrawals_root: B256::ZERO, + blob_gas_used: Some(12345), + }; + + let json = serde_json::to_string(&delta).unwrap(); + // Should contain blob_gas_used when Some + assert!(json.contains("blob_gas_used")); + assert!(json.contains("12345")); + } + + #[test] + fn test_delta_default() { + let delta = OpFlashblockPayloadDelta::default(); + assert_eq!(delta.state_root, B256::ZERO); + assert_eq!(delta.receipts_root, B256::ZERO); + assert_eq!(delta.logs_bloom, Bloom::ZERO); + assert_eq!(delta.gas_used, 0); + assert_eq!(delta.block_hash, B256::ZERO); + assert!(delta.transactions.is_empty()); + assert!(delta.withdrawals.is_empty()); + assert_eq!(delta.withdrawals_root, B256::ZERO); + assert_eq!(delta.blob_gas_used, None); + } +} diff --git a/crates/rpc-types-engine/src/flashblock/error.rs b/crates/rpc-types-engine/src/flashblock/error.rs new file mode 100644 index 000000000..e5fa39cf7 --- /dev/null +++ b/crates/rpc-types-engine/src/flashblock/error.rs @@ -0,0 +1,22 @@ +//! Optimism flashblock errors. + +/// Flashblock related errors. +#[derive(Debug, thiserror::Error)] +pub enum OpFlashblockError { + /// The base payload is required for the initial flashblock (index 0) but was not provided. + #[error("Missing base payload for initial flashblock")] + MissingBasePayload, + /// A base payload was provided for a non-initial flashblock, but only the first flashblock + /// should contain a base payload. + #[error("Unexpected base payload for non-initial flashblock")] + UnexpectedBasePayload, + /// The delta field is required for flashblocks but was not provided. + #[error("Missing delta for flashblock")] + MissingDelta, + /// The flashblock index is invalid or out of expected range. + #[error("Invalid index for flashblock")] + InvalidIndex, + /// The execution payload is missing from the flashblock. + #[error("Missing payload")] + MissingPayload, +} diff --git a/crates/rpc-types-engine/src/flashblock/metadata.rs b/crates/rpc-types-engine/src/flashblock/metadata.rs new file mode 100644 index 000000000..21f768e2b --- /dev/null +++ b/crates/rpc-types-engine/src/flashblock/metadata.rs @@ -0,0 +1,149 @@ +//! Flashblock metadata types. + +use alloc::collections::BTreeMap; +use alloy_primitives::{Address, B256, U256}; +use op_alloy_consensus::OpReceipt; + +/// Provides metadata about the block that may be useful for indexing or analysis. +// Note: this uses mixed camel, snake case: +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OpFlashblockPayloadMetadata { + /// The number of the block in the L2 chain. + pub block_number: u64, + /// A map of addresses to their updated balances after the block execution. + /// This represents balance changes due to transactions, rewards, or system transfers. + pub new_account_balances: BTreeMap, + /// Execution receipts for all transactions in the block. + /// Contains logs, gas usage, and other EVM-level metadata. + pub receipts: BTreeMap, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + use alloy_consensus::{Eip658Value, Receipt}; + use alloy_primitives::{Log, address}; + + fn sample_metadata() -> OpFlashblockPayloadMetadata { + let mut balances = BTreeMap::new(); + balances.insert(address!("0000000000000000000000000000000000000001"), U256::from(1000)); + + let mut receipts = BTreeMap::new(); + let receipt = OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 21000, + logs: Vec::new(), + }); + receipts.insert(B256::ZERO, receipt); + + OpFlashblockPayloadMetadata { block_number: 100, new_account_balances: balances, receipts } + } + + #[test] + #[cfg(feature = "serde")] + fn test_metadata_serde_roundtrip() { + let metadata = sample_metadata(); + + let json = serde_json::to_string(&metadata).unwrap(); + let decoded: OpFlashblockPayloadMetadata = serde_json::from_str(&json).unwrap(); + assert_eq!(metadata, decoded); + } + + #[test] + #[cfg(feature = "serde")] + fn test_metadata_snake_case_serialization() { + let metadata = sample_metadata(); + + let json = serde_json::to_string(&metadata).unwrap(); + assert!(json.contains("block_number")); + assert!(json.contains("new_account_balances")); + assert!(json.contains("receipts")); + } + + #[test] + #[cfg(feature = "serde")] + fn test_address_balance_map_serialization() { + let mut balances = BTreeMap::new(); + balances.insert(address!("0000000000000000000000000000000000000001"), U256::from(1000)); + balances.insert(address!("0000000000000000000000000000000000000002"), U256::from(2000)); + + let metadata = OpFlashblockPayloadMetadata { + block_number: 1, + new_account_balances: balances, + receipts: BTreeMap::new(), + }; + + let json = serde_json::to_value(&metadata).unwrap(); + let balances_obj = json.get("new_account_balances").unwrap(); + + // Should be serialized as an object with hex string keys + assert!(balances_obj.is_object()); + assert!(balances_obj.get("0x0000000000000000000000000000000000000001").is_some()); + } + + #[test] + #[cfg(feature = "serde")] + fn test_receipt_map_serialization() { + let mut receipts = BTreeMap::new(); + let receipt1 = OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 21000, + logs: Vec::::new(), + }); + receipts.insert(B256::ZERO, receipt1); + + let metadata = OpFlashblockPayloadMetadata { + block_number: 1, + new_account_balances: BTreeMap::new(), + receipts, + }; + + let json = serde_json::to_value(&metadata).unwrap(); + let receipts_obj = json.get("receipts").unwrap(); + + // Should be serialized as an object with hex string keys + assert!(receipts_obj.is_object()); + assert!( + receipts_obj + .get("0x0000000000000000000000000000000000000000000000000000000000000000") + .is_some() + ); + } + + #[test] + #[cfg(feature = "serde")] + fn test_receipt_json_format() { + let mut receipts = BTreeMap::new(); + let receipt = OpReceipt::Legacy(Receipt { + status: Eip658Value::Eip658(true), + cumulative_gas_used: 21000, + logs: Vec::::new(), + }); + receipts.insert(B256::ZERO, receipt); + + let metadata = OpFlashblockPayloadMetadata { + block_number: 1, + new_account_balances: BTreeMap::new(), + receipts, + }; + + let json = serde_json::to_value(&metadata).unwrap(); + let receipts_obj = json.get("receipts").unwrap(); + let receipt_entry = receipts_obj + .get("0x0000000000000000000000000000000000000000000000000000000000000000") + .unwrap(); + + // OpReceipt serializes as internally tagged enum + assert!(receipt_entry.get("Legacy").is_some()); + } + + #[test] + fn test_metadata_default() { + let metadata = OpFlashblockPayloadMetadata::default(); + assert_eq!(metadata.block_number, 0); + assert!(metadata.new_account_balances.is_empty()); + assert!(metadata.receipts.is_empty()); + } +} diff --git a/crates/rpc-types-engine/src/flashblock/mod.rs b/crates/rpc-types-engine/src/flashblock/mod.rs new file mode 100644 index 000000000..dff633d95 --- /dev/null +++ b/crates/rpc-types-engine/src/flashblock/mod.rs @@ -0,0 +1,63 @@ +//! # Flashblock Support for Optimism +//! +//! This module implements support for [Flashblocks](https://docs.base.org/chain/flashblocks), +//! which provide real-time block-like structures for faster state insight. +//! +//! ## Overview +//! +//! Flashblocks enable real-time visibility into block construction on OP Stack L2, +//! allowing users to see transaction effects before blocks are finalized. Each flashblock +//! represents a snapshot of the block's evolving state during its construction. +//! +//! ## Structure +//! +//! A flashblock sequence consists of: +//! +//! - **Base payload** ([`OpFlashblockPayloadBase`]): Immutable block properties that remain +//! constant throughout the block construction. Only present in the first flashblock (index 0). +//! +//! - **Delta payloads** ([`OpFlashblockPayloadDelta`]): Mutable/accumulating properties that change +//! as transactions are added. Present in all flashblocks. +//! +//! - **Metadata** ([`OpFlashblockPayloadMetadata`]): Additional information useful for indexing and +//! analysis. +//! +//! - **Complete payload** ([`OpFlashblockPayload`]): The envelope containing all of the above, +//! identified by a payload ID and sequential index. +//! +//! ## Usage +//! +//! Convert a sequence of flashblocks to a full execution payload: +//! +//! ```rust,ignore +//! use op_alloy_rpc_types_engine::{OpExecutionData, OpFlashblockPayload}; +//! +//! let flashblocks: Vec = vec![/* ... */]; +//! let execution_data = OpExecutionData::from_flashblocks(flashblocks)?; +//! # Ok::<(), op_alloy_rpc_types_engine::OpFlashblockError>(()) +//! ``` +//! +//! ## Validation Rules +//! +//! The [`OpExecutionData::from_flashblocks`](crate::OpExecutionData::from_flashblocks) method +//! performs comprehensive validation: +//! +//! - Indices must be sequential starting from 0 +//! - Only the first flashblock (index 0) can have a base payload +//! - All flashblocks must have delta payloads +//! - The sequence must contain at least one flashblock + +mod base; +pub use base::OpFlashblockPayloadBase; + +mod delta; +pub use delta::OpFlashblockPayloadDelta; + +mod metadata; +pub use metadata::OpFlashblockPayloadMetadata; + +mod payload; +pub use payload::OpFlashblockPayload; + +mod error; +pub use error::OpFlashblockError; diff --git a/crates/rpc-types-engine/src/flashblock/payload.rs b/crates/rpc-types-engine/src/flashblock/payload.rs new file mode 100644 index 000000000..be59b723b --- /dev/null +++ b/crates/rpc-types-engine/src/flashblock/payload.rs @@ -0,0 +1,156 @@ +//! Flashblock payload types. + +use super::{OpFlashblockPayloadBase, OpFlashblockPayloadDelta}; +use crate::flashblock::metadata::OpFlashblockPayloadMetadata; +use alloy_primitives::B256; +use alloy_rpc_types_engine::PayloadId; + +/// Flashblock payload. +/// +/// Represents a Flashblock, a real-time block-like structure emitted by the Base L2 chain. +/// A Flashblock provides a snapshot of a block's effects before finalization, +/// allowing faster insight into state transitions, balance changes, and logs. +/// +/// See: [Base Flashblocks Documentation](https://docs.base.org/chain/flashblocks) +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OpFlashblockPayload { + /// The unique payload ID as assigned by the execution engine for this block. + pub payload_id: PayloadId, + /// A sequential index that identifies the order of this Flashblock. + pub index: u64, + /// Immutable block properties shared across all flashblocks in the sequence. + /// This is `None` for all flashblocks except the first in a sequence. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub base: Option, + /// Accumulating and changing block properties for this flashblock. + pub diff: OpFlashblockPayloadDelta, + /// Additional metadata about the flashblock such as receipts and balance changes. + pub metadata: OpFlashblockPayloadMetadata, +} + +impl OpFlashblockPayload { + /// Returns the block number of this flashblock. + pub const fn block_number(&self) -> u64 { + self.metadata.block_number + } + + /// Returns the first parent hash of this flashblock. + pub fn parent_hash(&self) -> Option { + Some(self.base.as_ref()?.parent_hash) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::flashblock::{ + OpFlashblockPayloadBase, OpFlashblockPayloadDelta, OpFlashblockPayloadMetadata, + }; + use alloc::{collections::BTreeMap, vec}; + use alloy_primitives::{B256, Bloom, Bytes, U256, address}; + + fn sample_payload() -> OpFlashblockPayload { + let base = OpFlashblockPayloadBase { + parent_beacon_block_root: B256::ZERO, + parent_hash: B256::ZERO, + fee_recipient: address!("0000000000000000000000000000000000000001"), + prev_randao: B256::ZERO, + block_number: 100, + gas_limit: 30_000_000, + timestamp: 1234567890, + extra_data: Bytes::default(), + base_fee_per_gas: U256::from(1000000000u64), + }; + + let diff = OpFlashblockPayloadDelta { + state_root: B256::ZERO, + receipts_root: B256::ZERO, + logs_bloom: Bloom::ZERO, + gas_used: 21000, + block_hash: B256::ZERO, + transactions: vec![], + withdrawals: vec![], + withdrawals_root: B256::ZERO, + blob_gas_used: Some(0), + }; + + let metadata = OpFlashblockPayloadMetadata { + block_number: 100, + new_account_balances: BTreeMap::new(), + receipts: BTreeMap::new(), + }; + + OpFlashblockPayload { + payload_id: PayloadId::new([1u8; 8]), + index: 0, + base: Some(base), + diff, + metadata, + } + } + + #[test] + fn test_payload_accessors() { + let payload = sample_payload(); + + // Direct field access via public fields + assert_eq!(payload.metadata.block_number, 100); + assert_eq!(payload.base.as_ref().map(|b| b.parent_hash), Some(B256::ZERO)); + assert!(!payload.metadata.receipts.contains_key(&B256::ZERO)); + } + + #[test] + fn test_payload_without_base() { + let mut payload = sample_payload(); + payload.base = None; + + // Direct field access via public fields + assert_eq!(payload.metadata.block_number, 100); + assert_eq!(payload.base.as_ref().map(|b| b.parent_hash), None); + } + + #[test] + #[cfg(feature = "serde")] + fn test_payload_serde_roundtrip() { + let payload = sample_payload(); + + let json = serde_json::to_string(&payload).unwrap(); + let decoded: OpFlashblockPayload = serde_json::from_str(&json).unwrap(); + assert_eq!(payload, decoded); + } + + #[test] + #[cfg(feature = "serde")] + fn test_payload_snake_case_serialization() { + let payload = sample_payload(); + + let json = serde_json::to_string(&payload).unwrap(); + assert!(json.contains("payload_id")); + assert!(json.contains("\"index\"")); + assert!(json.contains("\"base\"")); + assert!(json.contains("\"diff\"")); + assert!(json.contains("\"metadata\"")); + } + + #[test] + #[cfg(feature = "serde")] + fn test_payload_base_omitted_when_none() { + let mut payload = sample_payload(); + payload.base = None; + + let json = serde_json::to_string(&payload).unwrap(); + // Base should not be serialized when None (skip_serializing_if) + assert!(!json.contains("\"base\"")); + } + + #[test] + fn test_payload_default() { + let payload = OpFlashblockPayload::default(); + assert_eq!(payload.payload_id, PayloadId::new([0u8; 8])); + assert_eq!(payload.index, 0); + assert_eq!(payload.base, None); + assert_eq!(payload.diff, OpFlashblockPayloadDelta::default()); + assert_eq!(payload.metadata, OpFlashblockPayloadMetadata::default()); + } +} diff --git a/crates/rpc-types-engine/src/lib.rs b/crates/rpc-types-engine/src/lib.rs index c7219b1ef..4b45b7b6e 100644 --- a/crates/rpc-types-engine/src/lib.rs +++ b/crates/rpc-types-engine/src/lib.rs @@ -35,3 +35,9 @@ mod superchain; pub use superchain::{ ProtocolVersion, ProtocolVersionError, ProtocolVersionFormatV0, SuperchainSignal, }; + +pub mod flashblock; +pub use flashblock::{ + OpFlashblockError, OpFlashblockPayload, OpFlashblockPayloadBase, OpFlashblockPayloadDelta, + OpFlashblockPayloadMetadata, +}; diff --git a/crates/rpc-types-engine/src/payload/v4.rs b/crates/rpc-types-engine/src/payload/v4.rs index 10524ecc5..b0eb94761 100644 --- a/crates/rpc-types-engine/src/payload/v4.rs +++ b/crates/rpc-types-engine/src/payload/v4.rs @@ -2,11 +2,9 @@ use alloc::vec::Vec; use alloy_consensus::Block; -use alloy_eips::{Decodable2718, eip4895::Withdrawal}; -use alloy_primitives::{Address, B256, Bloom, Bytes, U256}; -use alloy_rpc_types_engine::{ - BlobsBundleV1, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, PayloadError, -}; +use alloy_eips::Decodable2718; +use alloy_primitives::{B256, Bytes, U256}; +use alloy_rpc_types_engine::{BlobsBundleV1, ExecutionPayloadV3, PayloadError}; /// The Opstack execution payload for `newPayloadV4` of the engine API introduced with isthmus. /// See also @@ -89,10 +87,10 @@ impl ssz::Decode for OpExecutionPayloadV4 { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_type::()?; - builder.register_type::
()?; + builder.register_type::()?; builder.register_type::()?; builder.register_type::()?; - builder.register_type::()?; + builder.register_type::()?; builder.register_type::()?; builder.register_type::()?; builder.register_type::()?; @@ -102,7 +100,7 @@ impl ssz::Decode for OpExecutionPayloadV4 { builder.register_type::()?; builder.register_type::()?; builder.register_type::>()?; - builder.register_type::>()?; + builder.register_type::>()?; builder.register_type::()?; builder.register_type::()?; builder.register_type::()?; @@ -111,8 +109,8 @@ impl ssz::Decode for OpExecutionPayloadV4 { Ok(Self { payload_inner: ExecutionPayloadV3 { - payload_inner: ExecutionPayloadV2 { - payload_inner: ExecutionPayloadV1 { + payload_inner: alloy_rpc_types_engine::ExecutionPayloadV2 { + payload_inner: alloy_rpc_types_engine::ExecutionPayloadV1 { parent_hash: decoder.decode_next()?, fee_recipient: decoder.decode_next()?, state_root: decoder.decode_next()?, @@ -146,8 +144,8 @@ impl ssz::Encode for OpExecutionPayloadV4 { fn ssz_append(&self, buf: &mut Vec) { let offset = ::ssz_fixed_len() * 6 - +
::ssz_fixed_len() - + ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len() + ::ssz_fixed_len() * 6 + ::ssz_fixed_len() + ssz::BYTES_PER_LENGTH_OFFSET * 3;