From a98aeb00d528ebd49ba81565695b4bfc870dce91 Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Mon, 30 Mar 2026 15:24:00 +0800 Subject: [PATCH 1/3] feat: implement eth_config RPC with morph extension for morphnode compatibility Add a Morph-specific eth_config RPC handler that extends the standard EIP-7910 response with morph extension fields (useZktrie, jadeForkTime). morphnode calls eth_config at startup to determine the trie type and Jade fork timing. Without this implementation, morphnode cannot connect to morph-reth and fails to start. --- Cargo.lock | 2 + crates/node/src/add_ons.rs | 22 ++- crates/rpc/Cargo.toml | 4 + crates/rpc/src/eth_config.rs | 335 +++++++++++++++++++++++++++++++++++ crates/rpc/src/lib.rs | 2 + 5 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 crates/rpc/src/eth_config.rs diff --git a/Cargo.lock b/Cargo.lock index 7338843..cdc3cab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4992,10 +4992,12 @@ dependencies = [ "reth-rpc-convert", "reth-rpc-eth-api", "reth-rpc-eth-types", + "reth-storage-api", "reth-tasks", "reth-transaction-pool", "revm", "serde", + "serde_json", "thiserror 2.0.18", "tokio", "tracing", diff --git a/crates/node/src/add_ons.rs b/crates/node/src/add_ons.rs index 9f5b6f8..0c423b6 100644 --- a/crates/node/src/add_ons.rs +++ b/crates/node/src/add_ons.rs @@ -6,7 +6,7 @@ use crate::{ }; use morph_evm::MorphEvmConfig; use morph_primitives::{Block, MorphHeader, MorphReceipt}; -use morph_rpc::MorphEthApiBuilder; +use morph_rpc::{MorphEthApiBuilder, MorphEthConfigApiServer, MorphEthConfigHandler}; use reth_node_api::{AddOnsContext, FullNodeComponents, FullNodeTypes, NodeAddOns, NodePrimitives}; use reth_node_builder::{ NodeAdapter, @@ -103,6 +103,12 @@ where let engine_state_tracker = std::sync::Arc::new(morph_engine_api::EngineStateTracker::default()); + // Create Morph eth_config handler (EIP-7910 + morph extension) + let eth_config_handler = MorphEthConfigHandler::new( + ctx.node.provider().clone(), + ctx.node.evm_config().clone(), + ); + // Keep a local view of canonical head/forkchoice from reth engine events. let tracker_for_events = engine_state_tracker.clone(); task_executor.spawn_critical("morph engine state tracker", async move { @@ -112,13 +118,23 @@ where } }); - // Use launch_add_ons_with to register custom Engine API + // Use launch_add_ons_with to register custom Engine API and eth_config self.inner .launch_add_ons_with(ctx, move |container| { let reth_node_builder::rpc::RpcModuleContainer { - auth_module, .. + modules, + auth_module, + .. } = container; + // Register Morph eth_config handler (EIP-7910 + morph extension) + // This provides eth_config on HTTP/WS/IPC for morphnode compatibility. + tracing::debug!(target: "morph::node", "Registering Morph eth_config handler"); + modules + .merge_configured(eth_config_handler.into_rpc()) + .map_err(|e| eyre::eyre!("Failed to register eth_config handler: {}", e))?; + tracing::info!(target: "morph::node", "Morph eth_config handler registered successfully"); + // Create and register Morph L2 Engine API tracing::debug!(target: "morph::node", "Registering Morph L2 Engine API"); diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 889991a..12083cf 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -28,6 +28,7 @@ reth-rpc-eth-types.workspace = true reth-evm.workspace = true reth-provider.workspace = true reth-revm.workspace = true +reth-storage-api.workspace = true reth-tasks.workspace = true reth-errors.workspace = true reth-transaction-pool.workspace = true @@ -57,5 +58,8 @@ eyre.workspace = true tracing.workspace = true +[dev-dependencies] +serde_json.workspace = true + [features] default = [] diff --git a/crates/rpc/src/eth_config.rs b/crates/rpc/src/eth_config.rs new file mode 100644 index 0000000..5ec8159 --- /dev/null +++ b/crates/rpc/src/eth_config.rs @@ -0,0 +1,335 @@ +//! Morph-specific `eth_config` RPC handler. +//! +//! Implements the EIP-7910 `eth_config` endpoint with Morph extension fields. +//! The standard EIP-7910 response is extended with a `morph` object on each +//! fork config containing: +//! - `useZktrie`: whether the chain uses ZkTrie (pre-Jade) or MPT (post-Jade) +//! - `jadeForkTime`: the Jade hardfork activation timestamp (if configured) +//! +//! This is required by morphnode which calls `eth_config` at startup to determine +//! the trie type and Jade fork timing. + +use alloy_consensus::BlockHeader; +use alloy_eips::eip7840::BlobParams; +use alloy_primitives::Address; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use morph_chainspec::{ + hardfork::{MorphHardfork, MorphHardforks}, + spec::MorphChainSpec, +}; +use reth_chainspec::{ChainSpecProvider, EthChainSpec, ForkCondition, Hardforks, Head}; +use reth_errors::{ProviderError, RethError}; +use reth_evm::{ + precompiles::{Precompile, PrecompilesMap}, + ConfigureEvm, Evm, +}; +use reth_node_api::NodePrimitives; +use reth_primitives_traits::header::HeaderMut; +use reth_revm::db::EmptyDB; +use reth_rpc_eth_types::EthApiError; +use reth_storage_api::BlockReaderIdExt; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +// ─── Custom response types ────────────────────────────────────────────────── + +/// Response type for `eth_config` with Morph extension. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MorphEthConfig { + /// Fork configuration of the current active fork. + pub current: MorphForkConfig, + /// Fork configuration of the next scheduled fork. + #[serde(skip_serializing_if = "Option::is_none")] + pub next: Option, + /// Fork configuration of the last fork. + #[serde(skip_serializing_if = "Option::is_none")] + pub last: Option, +} + +/// A single fork configuration with Morph extension fields. +/// +/// This mirrors `alloy_eips::eip7910::EthForkConfig` but adds the `morph` extension. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MorphForkConfig { + /// The fork activation timestamp. + pub activation_time: u64, + /// Blob schedule parameters. + pub blob_schedule: BlobParams, + /// Chain ID (hex-encoded quantity string). + #[serde(with = "alloy_serde::quantity")] + pub chain_id: u64, + /// The fork hash from EIP-6122. + pub fork_id: alloy_primitives::Bytes, + /// Active precompile contracts: address -> name. + pub precompiles: BTreeMap, + /// System contracts: name -> address. + pub system_contracts: BTreeMap, + /// Morph-specific extension fields. + #[serde(skip_serializing_if = "Option::is_none")] + pub morph: Option, +} + +/// Morph-specific extension fields for the fork config. +/// +/// morphnode reads these to determine trie type and Jade fork timing. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MorphExtension { + /// Whether the chain uses ZkTrie at this fork's activation time. + /// Pre-Jade = true, post-Jade = false. + pub use_zktrie: bool, + /// The Jade hardfork activation timestamp, if configured. + #[serde(skip_serializing_if = "Option::is_none")] + pub jade_fork_time: Option, +} + +// ─── RPC trait ────────────────────────────────────────────────────────────── + +/// RPC endpoint for `eth_config` with Morph extension. +#[rpc(server, namespace = "eth")] +pub trait MorphEthConfigApi { + /// Returns an object with data about recent and upcoming fork configurations, + /// including Morph-specific extension fields. + #[method(name = "config")] + fn config(&self) -> RpcResult; +} + +// ─── Handler ──────────────────────────────────────────────────────────────── + +/// Handler for the `eth_config` RPC endpoint with Morph extensions. +#[derive(Debug, Clone)] +pub struct MorphEthConfigHandler { + provider: Provider, + evm_config: Evm, +} + +impl MorphEthConfigHandler +where + Provider: ChainSpecProvider + + BlockReaderIdExt + + 'static, + EvmConfig: + ConfigureEvm> + 'static, +{ + /// Creates a new [`MorphEthConfigHandler`]. + pub const fn new(provider: Provider, evm_config: EvmConfig) -> Self { + Self { + provider, + evm_config, + } + } + + /// Extracts the Jade fork timestamp from the chain spec, if configured. + fn jade_fork_time(&self) -> Option { + match self.provider.chain_spec().morph_fork_activation(MorphHardfork::Jade) { + ForkCondition::Timestamp(t) => Some(t), + _ => None, + } + } + + /// Returns the Morph extension for a given fork activation timestamp. + fn morph_extension_at(&self, timestamp: u64) -> MorphExtension { + let chain_spec = self.provider.chain_spec(); + // Pre-Jade uses ZkTrie, post-Jade uses MPT + let use_zktrie = !chain_spec.is_jade_active_at_timestamp(timestamp); + MorphExtension { + use_zktrie, + jade_fork_time: self.jade_fork_time(), + } + } + + /// Builds a fork config for a specific timestamp. + fn build_fork_config_at( + &self, + timestamp: u64, + precompiles: BTreeMap, + ) -> MorphForkConfig { + let chain_spec = self.provider.chain_spec(); + + // Morph L2 doesn't use standard Ethereum system contracts + // (no beacon roots, no deposit contract, etc.) + let system_contracts = BTreeMap::::new(); + + let fork_id = chain_spec + .fork_id(&Head { + timestamp, + number: u64::MAX, + ..Default::default() + }) + .hash + .0 + .into(); + + MorphForkConfig { + activation_time: timestamp, + blob_schedule: chain_spec + .blob_params_at_timestamp(timestamp) + .unwrap_or(BlobParams::cancun()), + chain_id: chain_spec.chain().id(), + fork_id, + precompiles, + system_contracts, + morph: Some(self.morph_extension_at(timestamp)), + } + } + + /// Core implementation of the `eth_config` method. + fn config_impl(&self) -> Result { + let chain_spec = self.provider.chain_spec(); + let latest = self + .provider + .latest_header()? + .ok_or_else(|| ProviderError::BestBlockNotFound)? + .into_header(); + + let current_precompiles = evm_to_precompiles_map( + self.evm_config + .evm_for_block(EmptyDB::default(), &latest) + .map_err(RethError::other)?, + ); + + let mut fork_timestamps = chain_spec + .forks_iter() + .filter_map(|(_, cond)| cond.as_timestamp()) + .collect::>(); + fork_timestamps.sort_unstable(); + fork_timestamps.dedup(); + + let (current_fork_idx, current_fork_timestamp) = fork_timestamps + .iter() + .position(|ts| &latest.timestamp() < ts) + .and_then(|idx| idx.checked_sub(1)) + .or_else(|| fork_timestamps.len().checked_sub(1)) + .and_then(|idx| fork_timestamps.get(idx).map(|ts| (idx, *ts))) + .ok_or_else(|| RethError::msg("no active timestamp fork found"))?; + + let current = + self.build_fork_config_at(current_fork_timestamp, current_precompiles); + + let mut config = MorphEthConfig { + current, + next: None, + last: None, + }; + + if let Some(next_fork_timestamp) = + fork_timestamps.get(current_fork_idx + 1).copied() + { + let fake_header = { + let mut header = latest.clone(); + header.set_timestamp(next_fork_timestamp); + header + }; + let next_precompiles = evm_to_precompiles_map( + self.evm_config + .evm_for_block(EmptyDB::default(), &fake_header) + .map_err(RethError::other)?, + ); + + config.next = + Some(self.build_fork_config_at(next_fork_timestamp, next_precompiles)); + } else { + // No future fork scheduled — no "last" either. + return Ok(config); + } + + let last_fork_timestamp = fork_timestamps.last().copied().unwrap(); + let fake_header = { + let mut header = latest; + header.set_timestamp(last_fork_timestamp); + header + }; + let last_precompiles = evm_to_precompiles_map( + self.evm_config + .evm_for_block(EmptyDB::default(), &fake_header) + .map_err(RethError::other)?, + ); + + config.last = + Some(self.build_fork_config_at(last_fork_timestamp, last_precompiles)); + + Ok(config) + } +} + +impl MorphEthConfigApiServer + for MorphEthConfigHandler +where + Provider: ChainSpecProvider + + BlockReaderIdExt + + 'static, + EvmConfig: + ConfigureEvm> + 'static, +{ + fn config(&self) -> RpcResult { + Ok(self.config_impl().map_err(EthApiError::from)?) + } +} + +/// Extracts a precompiles name -> address map from an EVM instance. +fn evm_to_precompiles_map( + evm: impl Evm, +) -> BTreeMap { + let precompiles = evm.precompiles(); + precompiles + .addresses() + .filter_map(|address| { + Some(( + precompiles.get(address)?.precompile_id().name().to_string(), + *address, + )) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_morph_extension_serialization() { + let ext = MorphExtension { + use_zktrie: true, + jade_fork_time: Some(1700000000), + }; + let json = serde_json::to_value(&ext).unwrap(); + assert_eq!(json["useZktrie"], true); + assert_eq!(json["jadeForkTime"], 1700000000); + } + + #[test] + fn test_morph_extension_without_jade() { + let ext = MorphExtension { + use_zktrie: true, + jade_fork_time: None, + }; + let json = serde_json::to_value(&ext).unwrap(); + assert_eq!(json["useZktrie"], true); + assert!(json.get("jadeForkTime").is_none()); + } + + #[test] + fn test_morph_fork_config_serialization() { + let config = MorphForkConfig { + activation_time: 0, + blob_schedule: BlobParams::cancun(), + chain_id: 0xb0a2, + fork_id: alloy_primitives::Bytes::from_static(&[0x01, 0x02, 0x03, 0x04]), + precompiles: BTreeMap::new(), + system_contracts: BTreeMap::new(), + morph: Some(MorphExtension { + use_zktrie: false, + jade_fork_time: Some(1700000000), + }), + }; + let json = serde_json::to_value(&config).unwrap(); + // chain_id should be hex-encoded quantity + assert_eq!(json["chainId"], "0xb0a2"); + // morph extension should be present + assert!(json["morph"].is_object()); + assert_eq!(json["morph"]["useZktrie"], false); + assert_eq!(json["morph"]["jadeForkTime"], 1700000000); + } +} diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index eb9cb3a..1acc122 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -4,8 +4,10 @@ pub mod error; pub mod eth; +pub mod eth_config; pub mod types; pub use error::MorphEthApiError; pub use eth::{MorphEthApi, MorphEthApiBuilder, MorphRpcConverter, MorphRpcTypes}; +pub use eth_config::{MorphEthConfigApiServer, MorphEthConfigHandler}; pub use types::*; From 49c65226fbf1049813beb331088fddc3845f55bc Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 30 Mar 2026 15:58:16 +0800 Subject: [PATCH 2/3] fix(rpc): correct precompiles doc comment and fork selection edge case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix precompiles field doc comment: "address -> name" → "name -> address" to match the actual BTreeMap type. - Replace position/checked_sub/or_else chain with rfind/find for current fork selection. The old logic incorrectly fell back to the last fork when latest timestamp was before the first timestamp fork. --- crates/rpc/src/eth_config.rs | 42 ++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/crates/rpc/src/eth_config.rs b/crates/rpc/src/eth_config.rs index 5ec8159..cf94059 100644 --- a/crates/rpc/src/eth_config.rs +++ b/crates/rpc/src/eth_config.rs @@ -20,8 +20,8 @@ use morph_chainspec::{ use reth_chainspec::{ChainSpecProvider, EthChainSpec, ForkCondition, Hardforks, Head}; use reth_errors::{ProviderError, RethError}; use reth_evm::{ - precompiles::{Precompile, PrecompilesMap}, ConfigureEvm, Evm, + precompiles::{Precompile, PrecompilesMap}, }; use reth_node_api::NodePrimitives; use reth_primitives_traits::header::HeaderMut; @@ -62,7 +62,7 @@ pub struct MorphForkConfig { pub chain_id: u64, /// The fork hash from EIP-6122. pub fork_id: alloy_primitives::Bytes, - /// Active precompile contracts: address -> name. + /// Active precompile contracts: name -> address. pub precompiles: BTreeMap, /// System contracts: name -> address. pub system_contracts: BTreeMap, @@ -110,8 +110,7 @@ where Provider: ChainSpecProvider + BlockReaderIdExt + 'static, - EvmConfig: - ConfigureEvm> + 'static, + EvmConfig: ConfigureEvm> + 'static, { /// Creates a new [`MorphEthConfigHandler`]. pub const fn new(provider: Provider, evm_config: EvmConfig) -> Self { @@ -123,7 +122,11 @@ where /// Extracts the Jade fork timestamp from the chain spec, if configured. fn jade_fork_time(&self) -> Option { - match self.provider.chain_spec().morph_fork_activation(MorphHardfork::Jade) { + match self + .provider + .chain_spec() + .morph_fork_activation(MorphHardfork::Jade) + { ForkCondition::Timestamp(t) => Some(t), _ => None, } @@ -197,16 +200,15 @@ where fork_timestamps.sort_unstable(); fork_timestamps.dedup(); - let (current_fork_idx, current_fork_timestamp) = fork_timestamps + let latest_ts = latest.timestamp(); + let current_fork_timestamp = fork_timestamps .iter() - .position(|ts| &latest.timestamp() < ts) - .and_then(|idx| idx.checked_sub(1)) - .or_else(|| fork_timestamps.len().checked_sub(1)) - .and_then(|idx| fork_timestamps.get(idx).map(|ts| (idx, *ts))) + .copied() + .rfind(|&ts| ts <= latest_ts) .ok_or_else(|| RethError::msg("no active timestamp fork found"))?; + let next_fork_timestamp = fork_timestamps.iter().copied().find(|&ts| ts > latest_ts); - let current = - self.build_fork_config_at(current_fork_timestamp, current_precompiles); + let current = self.build_fork_config_at(current_fork_timestamp, current_precompiles); let mut config = MorphEthConfig { current, @@ -214,9 +216,7 @@ where last: None, }; - if let Some(next_fork_timestamp) = - fork_timestamps.get(current_fork_idx + 1).copied() - { + if let Some(next_fork_timestamp) = next_fork_timestamp { let fake_header = { let mut header = latest.clone(); header.set_timestamp(next_fork_timestamp); @@ -228,8 +228,7 @@ where .map_err(RethError::other)?, ); - config.next = - Some(self.build_fork_config_at(next_fork_timestamp, next_precompiles)); + config.next = Some(self.build_fork_config_at(next_fork_timestamp, next_precompiles)); } else { // No future fork scheduled — no "last" either. return Ok(config); @@ -247,21 +246,18 @@ where .map_err(RethError::other)?, ); - config.last = - Some(self.build_fork_config_at(last_fork_timestamp, last_precompiles)); + config.last = Some(self.build_fork_config_at(last_fork_timestamp, last_precompiles)); Ok(config) } } -impl MorphEthConfigApiServer - for MorphEthConfigHandler +impl MorphEthConfigApiServer for MorphEthConfigHandler where Provider: ChainSpecProvider + BlockReaderIdExt + 'static, - EvmConfig: - ConfigureEvm> + 'static, + EvmConfig: ConfigureEvm> + 'static, { fn config(&self) -> RpcResult { Ok(self.config_impl().map_err(EthApiError::from)?) From 0c0950c30dd79c922db77a55220769cdbcf844ea Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 30 Mar 2026 16:29:42 +0800 Subject: [PATCH 3/3] style: fmt all --- crates/node/src/add_ons.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/node/src/add_ons.rs b/crates/node/src/add_ons.rs index 0c423b6..e2b84b2 100644 --- a/crates/node/src/add_ons.rs +++ b/crates/node/src/add_ons.rs @@ -104,10 +104,8 @@ where std::sync::Arc::new(morph_engine_api::EngineStateTracker::default()); // Create Morph eth_config handler (EIP-7910 + morph extension) - let eth_config_handler = MorphEthConfigHandler::new( - ctx.node.provider().clone(), - ctx.node.evm_config().clone(), - ); + let eth_config_handler = + MorphEthConfigHandler::new(ctx.node.provider().clone(), ctx.node.evm_config().clone()); // Keep a local view of canonical head/forkchoice from reth engine events. let tracker_for_events = engine_state_tracker.clone();