From b98ddf425ad453d4a01214917a6a2fe06bbdf601 Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 26 Feb 2026 14:50:18 +0800 Subject: [PATCH 1/2] refactor(primitives): remove batch_hash from morph header Remove the `batch_hash` field from `MorphHeader` as it is no longer necessary and increases maintenance complexity. Changes: - Remove `batch_hash` field from `MorphHeader` struct - Remove `is_batch_point()` method - Remove `batch_hash` parameter from `new_l2_block` API - Remove `batch_hash` from `SafeL2Data` struct - Remove `apply_batch_hash` function from engine-api builder - Update all related documentation and tests Closes #24 --- crates/engine-api/src/api.rs | 8 +---- crates/engine-api/src/builder.rs | 35 ++----------------- crates/engine-api/src/rpc.rs | 22 ++++-------- crates/evm/src/assemble.rs | 8 ++--- crates/evm/src/engine.rs | 1 - crates/evm/src/lib.rs | 2 +- crates/payload/builder/src/builder.rs | 1 - crates/payload/types/src/safe_l2_data.rs | 15 +------- crates/primitives/src/header.rs | 44 ++++-------------------- 9 files changed, 22 insertions(+), 114 deletions(-) diff --git a/crates/engine-api/src/api.rs b/crates/engine-api/src/api.rs index cc97073..1c044d6 100644 --- a/crates/engine-api/src/api.rs +++ b/crates/engine-api/src/api.rs @@ -5,7 +5,6 @@ //! by the sequencer to produce new blocks. use crate::EngineApiResult; -use alloy_primitives::B256; use morph_payload_types::{AssembleL2BlockParams, ExecutableL2Data, GenericResponse, SafeL2Data}; use morph_primitives::MorphHeader; @@ -66,16 +65,11 @@ pub trait MorphL2EngineApi: Send + Sync { /// # Arguments /// /// * `data` - The block data to import - /// * `batch_hash` - Optional batch hash if this block is a batch point /// /// # Returns /// /// Returns `Ok(())` on success. - async fn new_l2_block( - &self, - data: ExecutableL2Data, - batch_hash: Option, - ) -> EngineApiResult<()>; + async fn new_l2_block(&self, data: ExecutableL2Data) -> EngineApiResult<()>; /// Import a safe L2 block from derivation. /// diff --git a/crates/engine-api/src/builder.rs b/crates/engine-api/src/builder.rs index 12fc9c5..fd6c038 100644 --- a/crates/engine-api/src/builder.rs +++ b/crates/engine-api/src/builder.rs @@ -291,16 +291,11 @@ where Ok(GenericResponse { success: true }) } - async fn new_l2_block( - &self, - data: ExecutableL2Data, - batch_hash: Option, - ) -> EngineApiResult<()> { + async fn new_l2_block(&self, data: ExecutableL2Data) -> EngineApiResult<()> { tracing::info!( target: "morph::engine", block_number = data.number, block_hash = %data.hash, - ?batch_hash, "importing new L2 block" ); @@ -373,9 +368,7 @@ where } let executed = cached.executed.into_executed_payload(); - let recovered_with_batch = - self.apply_batch_hash(executed.recovered_block().clone(), batch_hash); - let recovered_with_data = apply_executable_data_overrides(recovered_with_batch, &data)?; + let recovered_with_data = apply_executable_data_overrides(executed.recovered_block().clone(), &data)?; let computed_hash = recovered_with_data.hash(); if computed_hash != data.hash { return Err(MorphEngineApiError::ValidationFailed(format!( @@ -462,8 +455,7 @@ where self.cache_built_payload(&built_payload); // 3. Import the block - self.new_l2_block(executable_data.clone(), data.batch_hash) - .await?; + self.new_l2_block(executable_data.clone()).await?; // 4. Return the persisted header. let header = self @@ -602,22 +594,6 @@ impl RealMorphL2EngineApi { ); } - fn apply_batch_hash( - &self, - recovered_block: RecoveredBlock, - batch_hash: Option, - ) -> RecoveredBlock { - let Some(batch_hash) = batch_hash else { - return recovered_block; - }; - let (block, senders) = recovered_block.split(); - let block = block.map_header(|mut header: MorphHeader| { - header.batch_hash = batch_hash; - header - }); - RecoveredBlock::new_unhashed(block, senders) - } - fn current_head(&self) -> EngineApiResult where Provider: BlockReader, @@ -728,7 +704,6 @@ impl MorphL2EngineApi for StubMorphL2EngineApi { async fn new_l2_block( &self, _data: morph_payload_types::ExecutableL2Data, - _batch_hash: Option, ) -> crate::EngineApiResult<()> { tracing::warn!(target: "morph::engine", "new_l2_block called on stub implementation"); Err(crate::MorphEngineApiError::Internal( @@ -790,7 +765,6 @@ mod tests { let target_header = MorphHeader { next_l1_msg_index: 42, - batch_hash: B256::ZERO, inner: Header { parent_hash: B256::from([0x11; 32]), beneficiary: Address::from([0x22; 20]), @@ -868,7 +842,6 @@ mod tests { fn test_apply_executable_data_overrides_sets_header_fields_exactly() { let source_header = MorphHeader { next_l1_msg_index: 1, - batch_hash: B256::from([0xab; 32]), inner: Header { parent_hash: B256::from([0x01; 32]), beneficiary: Address::from([0x02; 20]), @@ -883,7 +856,6 @@ mod tests { ..Default::default() }, }; - let source_batch_hash = source_header.batch_hash; let recovered = recovered_with_header(source_header); let data = ExecutableL2Data { parent_hash: B256::from([0x11; 32]), @@ -920,7 +892,6 @@ mod tests { ); assert_eq!(header.inner.logs_bloom.as_slice(), data.logs_bloom.as_ref()); assert_eq!(header.next_l1_msg_index, data.next_l1_message_index); - assert_eq!(header.batch_hash, source_batch_hash); } #[test] diff --git a/crates/engine-api/src/rpc.rs b/crates/engine-api/src/rpc.rs index 2ab0db0..7ac35a6 100644 --- a/crates/engine-api/src/rpc.rs +++ b/crates/engine-api/src/rpc.rs @@ -4,7 +4,6 @@ //! allowing the sequencer to interact with the execution layer via RPC. use crate::{EngineApiResult, api::MorphL2EngineApi}; -use alloy_primitives::B256; use jsonrpsee::{RpcModule, core::RpcResult, proc_macros::rpc}; use morph_payload_types::{AssembleL2BlockParams, ExecutableL2Data, GenericResponse, SafeL2Data}; use morph_primitives::MorphHeader; @@ -40,8 +39,7 @@ pub trait MorphL2EngineRpc { /// /// `engine_newL2Block` #[method(name = "newL2Block")] - async fn new_l2_block(&self, data: ExecutableL2Data, batch_hash: Option) - -> RpcResult<()>; + async fn new_l2_block(&self, data: ExecutableL2Data) -> RpcResult<()>; /// Import a safe L2 block from derivation. /// @@ -106,26 +104,18 @@ where }) } - async fn new_l2_block( - &self, - data: ExecutableL2Data, - batch_hash: Option, - ) -> RpcResult<()> { + async fn new_l2_block(&self, data: ExecutableL2Data) -> RpcResult<()> { tracing::info!( target: "morph::engine", block_number = data.number, block_hash = %data.hash, - ?batch_hash, "importing new L2 block" ); - self.inner - .new_l2_block(data, batch_hash) - .await - .map_err(|e| { - tracing::error!(target: "morph::engine", error = %e, "failed to import L2 block"); - e.into() - }) + self.inner.new_l2_block(data).await.map_err(|e| { + tracing::error!(target: "morph::engine", error = %e, "failed to import L2 block"); + e.into() + }) } async fn new_safe_l2_block(&self, data: SafeL2Data) -> RpcResult { diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index 31cd2c7..8510601 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -13,7 +13,7 @@ use std::sync::Arc; /// /// This assembler builds Morph blocks from the execution output. /// Unlike `EthBlockAssembler`, it produces `MorphHeader` with proper -/// L2-specific fields (next_l1_msg_index, batch_hash). +/// L2-specific fields (next_l1_msg_index). #[derive(Debug, Clone)] pub struct MorphBlockAssembler { /// Chain specification @@ -48,12 +48,11 @@ impl BlockAssembler for MorphBlockAssembler { /// 2. **Build Logs Bloom**: Aggregates logs from all receipts /// 3. **Check Hardforks**: Determines if EIP-1559 (London) is active for base fee /// 4. **Build Header**: Creates standard Ethereum header fields - /// 5. **Wrap in MorphHeader**: Adds L2-specific fields (next_l1_msg_index, batch_hash) + /// 5. **Wrap in MorphHeader**: Adds L2-specific fields (next_l1_msg_index) /// 6. **Build Block Body**: Combines transactions and empty ommers /// /// # L2-Specific Fields /// - `next_l1_msg_index`: Inherited from parent block - /// - `batch_hash`: Set to default (will be filled by payload builder) /// /// # Arguments /// * `input` - Contains execution context, transactions, receipts, and state root @@ -117,11 +116,10 @@ impl BlockAssembler for MorphBlockAssembler { }; // Wrap in MorphHeader with L2-specific fields - // Note: next_l1_msg_index and batch_hash will be set by the payload builder + // Note: next_l1_msg_index will be updated by the payload builder let header = MorphHeader { inner, next_l1_msg_index: parent.header().next_l1_msg_index, - batch_hash: Default::default(), }; Ok(alloy_consensus::Block::new( diff --git a/crates/evm/src/engine.rs b/crates/evm/src/engine.rs index 97ec30c..f17f50f 100644 --- a/crates/evm/src/engine.rs +++ b/crates/evm/src/engine.rs @@ -120,7 +120,6 @@ mod tests { ..Default::default() }, next_l1_msg_index: 0, - batch_hash: B256::ZERO, }; let body = BlockBody { diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 5b3810d..3f8012a 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -48,7 +48,7 @@ //! requiring a custom receipt builder. //! //! 4. **L2-Specific Header**: `MorphHeader` extends standard header with -//! `next_l1_msg_index` and `batch_hash` fields. +//! the `next_l1_msg_index` field. //! //! 5. **No Blob Transactions**: EIP-4844 blob transactions are not supported. diff --git a/crates/payload/builder/src/builder.rs b/crates/payload/builder/src/builder.rs index d5146b3..4d0e2a2 100644 --- a/crates/payload/builder/src/builder.rs +++ b/crates/payload/builder/src/builder.rs @@ -700,7 +700,6 @@ where let (mut morph_block, senders) = block.split(); morph_block = morph_block.map_header(|mut header: MorphHeader| { header.next_l1_msg_index = info.next_l1_message_index; - // batch_hash remains B256::ZERO - it will be set by the batch submitter header }); block = RecoveredBlock::new_unhashed(morph_block, senders); diff --git a/crates/payload/types/src/safe_l2_data.rs b/crates/payload/types/src/safe_l2_data.rs index 13516f5..7fc654d 100644 --- a/crates/payload/types/src/safe_l2_data.rs +++ b/crates/payload/types/src/safe_l2_data.rs @@ -2,7 +2,7 @@ //! //! This type is used for NewSafeL2Block in the derivation pipeline. -use alloy_primitives::{B256, Bytes}; +use alloy_primitives::Bytes; /// Safe L2 block data, used for NewSafeL2Block (derivation). /// @@ -38,10 +38,6 @@ pub struct SafeL2Data { /// RLP-encoded transactions. #[serde(default)] pub transactions: Vec, - - /// Optional batch hash for batch association. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub batch_hash: Option, } impl SafeL2Data { @@ -59,11 +55,6 @@ impl SafeL2Data { pub fn transaction_count(&self) -> usize { self.transactions.len() } - - /// Returns true if this block is associated with a batch. - pub fn has_batch(&self) -> bool { - self.batch_hash.is_some() - } } #[cfg(test)] @@ -77,7 +68,6 @@ mod tests { assert_eq!(data.gas_limit, 0); assert!(data.base_fee_per_gas.is_none()); assert!(!data.has_transactions()); - assert!(!data.has_batch()); } #[test] @@ -88,7 +78,6 @@ mod tests { base_fee_per_gas: Some(1_000_000_000), timestamp: 1234567890, transactions: vec![Bytes::from(vec![0x01, 0x02])], - batch_hash: Some(B256::random()), }; let json = serde_json::to_string(&data).expect("serialize"); @@ -105,13 +94,11 @@ mod tests { base_fee_per_gas: None, timestamp: 1234567890, transactions: vec![], - batch_hash: None, }; let json = serde_json::to_string(&data).expect("serialize"); // Optional fields should not appear in JSON when None assert!(!json.contains("baseFeePerGas")); - assert!(!json.contains("batchHash")); let decoded: SafeL2Data = serde_json::from_str(&json).expect("deserialize"); assert_eq!(data, decoded); diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs index 51d2742..95c916b 100644 --- a/crates/primitives/src/header.rs +++ b/crates/primitives/src/header.rs @@ -2,9 +2,8 @@ //! //! This module defines the Morph-specific header type that includes: //! - `next_l1_msg_index`: The next L1 message queue index to process -//! - `batch_hash`: The batch hash (non-zero if this block is a batch point) //! -//! These fields are NOT included in the block hash calculation to maintain +//! This field is NOT included in the block hash calculation to maintain //! compatibility with standard Ethereum header hashing (matching go-ethereum's behavior). use alloy_consensus::{BlockHeader, Header, Sealable}; @@ -15,12 +14,10 @@ use alloy_rlp::{RlpDecodable, RlpEncodable}; /// /// This header extends the standard Ethereum header with Morph-specific fields: /// - `next_l1_msg_index`: Next L1 message queue index to process -/// - `batch_hash`: Non-zero if this block is a batch point /// /// **Important**: The `hash_slow()` method only hashes the inner Ethereum header, -/// excluding `next_l1_msg_index` and `batch_hash`. This matches go-ethereum's -/// `Header.Hash()` behavior where these L2-specific fields are not part of the -/// block hash calculation. +/// excluding `next_l1_msg_index`. This matches go-ethereum's `Header.Hash()` behavior +/// where L2-specific fields are not part of the block hash calculation. /// /// Note: The `inner` field must be placed last for the `Compact` derive macro, /// as fields with unknown size must come last in the struct definition. @@ -34,29 +31,17 @@ pub struct MorphHeader { #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub next_l1_msg_index: u64, - /// Batch hash - non-zero if this block is a batch point. - /// Not part of the header hash calculation. - pub batch_hash: B256, - /// Standard Ethereum header (flattened in JSON serialization). /// Must be placed last due to Compact derive requirements. #[cfg_attr(feature = "serde", serde(flatten))] pub inner: Header, } -impl MorphHeader { - /// Returns true if this block is a batch point (batch_hash is non-zero). - pub fn is_batch_point(&self) -> bool { - self.batch_hash != B256::ZERO - } -} - impl From
for MorphHeader { fn from(inner: Header) -> Self { Self { inner, next_l1_msg_index: 0, - batch_hash: B256::ZERO, } } } @@ -157,13 +142,12 @@ impl BlockHeader for MorphHeader { /// Sealable implementation for MorphHeader. /// /// **Critical**: The `hash_slow()` method only hashes the inner Ethereum header, -/// NOT the `next_l1_msg_index` and `batch_hash` fields. This matches go-ethereum's -/// `Header.Hash()` behavior which explicitly excludes these L2-specific fields -/// from the hash calculation. +/// NOT the `next_l1_msg_index` field. This matches go-ethereum's `Header.Hash()` +/// behavior which explicitly excludes L2-specific fields from the hash calculation. impl Sealable for MorphHeader { fn hash_slow(&self) -> B256 { // Only hash the inner header to match go-ethereum behavior. - // next_l1_msg_index and batch_hash are NOT part of the hash. + // next_l1_msg_index is NOT part of the hash. self.inner.hash_slow() } } @@ -175,7 +159,6 @@ impl reth_primitives_traits::InMemorySize for MorphHeader { fn size(&self) -> usize { reth_primitives_traits::InMemorySize::size(&self.inner) + core::mem::size_of::() // next_l1_msg_index - + core::mem::size_of::() // batch_hash } } @@ -262,35 +245,28 @@ mod tests { assert_eq!(header.inner, inner); assert_eq!(header.next_l1_msg_index, 0); - assert_eq!(header.batch_hash, B256::ZERO); - assert!(!header.is_batch_point()); } #[test] fn test_morph_header_with_fields() { let inner = create_test_header(); - let batch_hash = b256!("0000000000000000000000000000000000000000000000000000000000000abc"); let header = MorphHeader { inner, next_l1_msg_index: 100, - batch_hash, }; assert_eq!(header.next_l1_msg_index, 100); - assert_eq!(header.batch_hash, batch_hash); - assert!(header.is_batch_point()); } #[test] fn test_morph_header_hash_excludes_l2_fields() { let inner = create_test_header(); - // Create two headers with different L2 fields + // Create two headers with different next_l1_msg_index values let header1: MorphHeader = inner.clone().into(); let header2 = MorphHeader { inner: inner.clone(), next_l1_msg_index: 999, - batch_hash: b256!("1111111111111111111111111111111111111111111111111111111111111111"), }; // Both should have the same hash since L2 fields are excluded @@ -307,11 +283,6 @@ mod tests { header.next_l1_msg_index = 50; assert_eq!(header.next_l1_msg_index, 50); - - let batch_hash = b256!("2222222222222222222222222222222222222222222222222222222222222222"); - header.batch_hash = batch_hash; - assert_eq!(header.batch_hash, batch_hash); - assert!(header.is_batch_point()); } #[test] @@ -337,7 +308,6 @@ mod tests { let header = MorphHeader { inner, next_l1_msg_index: 42, - batch_hash: b256!("3333333333333333333333333333333333333333333333333333333333333333"), }; let json = serde_json::to_string(&header).expect("serialization failed"); From fb9676f7c14911bdead74af0b1d8f425916d031f Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 26 Feb 2026 15:00:01 +0800 Subject: [PATCH 2/2] style: format code --- crates/engine-api/src/builder.rs | 3 ++- crates/primitives/src/header.rs | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/engine-api/src/builder.rs b/crates/engine-api/src/builder.rs index fd6c038..0c60764 100644 --- a/crates/engine-api/src/builder.rs +++ b/crates/engine-api/src/builder.rs @@ -368,7 +368,8 @@ where } let executed = cached.executed.into_executed_payload(); - let recovered_with_data = apply_executable_data_overrides(executed.recovered_block().clone(), &data)?; + let recovered_with_data = + apply_executable_data_overrides(executed.recovered_block().clone(), &data)?; let computed_hash = recovered_with_data.hash(); if computed_hash != data.hash { return Err(MorphEngineApiError::ValidationFailed(format!( diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs index 95c916b..4b3d40a 100644 --- a/crates/primitives/src/header.rs +++ b/crates/primitives/src/header.rs @@ -157,8 +157,7 @@ impl reth_primitives_traits::serde_bincode_compat::RlpBincode for MorphHeader {} impl reth_primitives_traits::InMemorySize for MorphHeader { fn size(&self) -> usize { - reth_primitives_traits::InMemorySize::size(&self.inner) - + core::mem::size_of::() // next_l1_msg_index + reth_primitives_traits::InMemorySize::size(&self.inner) + core::mem::size_of::() // next_l1_msg_index } }