From 2aae4220a5aa5d5beec747124eba7eafce1b47c3 Mon Sep 17 00:00:00 2001 From: zacksfF Date: Thu, 12 Mar 2026 16:36:19 +0000 Subject: [PATCH 1/5] update --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 8 ++++---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d67dbb..606fbcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2944,6 +2944,7 @@ dependencies = [ name = "eth-config-api" version = "0.1.0" dependencies = [ + "crc32fast", "serde", "serde_json", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 370706d..5cad7fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ license = "MIT" # adjust if needed serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "1" +crc32fast = "1.3" diff --git a/README.md b/README.md index 8d0de94..11c48a8 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Implementation skeleton for exposing the `eth_config` JSON-RPC method (EIP-7910) - `make cli` — build the validator CLI. ## Tests -- `tests/hoodi_prague.rs`: golden comparison versus the EIP Prague sample output (fixture pending). -- `tests/hoodi_cancun.rs`: golden comparison versus the EIP Cancun sample output (fixture pending). -- `tests/no_future_fork.rs`: ensures `next`/`last` are null when no future or past fork exists. -- `tests/bpo_fork.rs`: covers blob-parameter-only fork transitions. +- `tests/hoodi_prague.rs`: parses Prague sample and checks presence of `next` fork id. +- `tests/hoodi_cancun.rs`: parses Cancun sample with blob schedule and no next fork. +- `tests/no_future_fork.rs`: ensures `next`/`last` null handling. +- `tests/bpo_fork.rs`: blob-parameter-only fork sample. - Tests live in the `eth-config-tests` crate (workspace member) and currently validate the example fixtures deserialize correctly. Run them via `make test` or `cargo test -p eth-config-tests`. ## EIP-7910 recap From 48f88df3eec6da36a6f436c3b4cc208413b052fa Mon Sep 17 00:00:00 2001 From: zacksfF Date: Thu, 12 Mar 2026 16:40:05 +0000 Subject: [PATCH 2/5] feat: hexify eth_config types and compute fork_id(#1) --- crates/eth-config-api/Cargo.toml | 1 + crates/eth-config-api/src/fork_id.rs | 33 +++++++++++++++--- crates/eth-config-api/src/lib.rs | 2 +- crates/eth-config-api/src/types.rs | 52 ++++++++++++++++++++++------ 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/crates/eth-config-api/Cargo.toml b/crates/eth-config-api/Cargo.toml index 151c114..882df67 100644 --- a/crates/eth-config-api/Cargo.toml +++ b/crates/eth-config-api/Cargo.toml @@ -9,3 +9,4 @@ authors = ["EIP-7910 contributors"] serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +crc32fast = { workspace = true } diff --git a/crates/eth-config-api/src/fork_id.rs b/crates/eth-config-api/src/fork_id.rs index 635ad1b..6f341b9 100644 --- a/crates/eth-config-api/src/fork_id.rs +++ b/crates/eth-config-api/src/fork_id.rs @@ -1,20 +1,43 @@ +use crc32fast::Hasher; use serde::{Deserialize, Serialize}; +use crate::types::HexU64; + /// EIP-6122 fork identifier (hash + next fork block number). #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "camelCase")] pub struct ForkId { pub hash: String, #[serde(skip_serializing_if = "Option::is_none")] - pub next: Option, + pub next: Option, } impl ForkId { - /// Placeholder constructor for scaffolding. - pub fn placeholder() -> Self { + /// Compute fork ID per EIP-6122 (CRC32 of genesis hash + fork block numbers, big-endian u64). + pub fn compute(genesis_hash: [u8; 32], fork_blocks: &[u64], next_fork: Option) -> Self { + let mut hasher = Hasher::new(); + hasher.update(&genesis_hash); + for block in fork_blocks { + hasher.update(&block.to_be_bytes()); + } + let hash = hasher.finalize(); Self { - hash: String::from("0x00000000"), - next: None, + hash: format!("0x{hash:08x}"), + next: next_fork.map(HexU64), } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fork_id_deterministic() { + let genesis = [0u8; 32]; + let forks = [0u64, 9, 30_000_000]; + let a = ForkId::compute(genesis, &forks, Some(40_000_000)); + let b = ForkId::compute(genesis, &forks, Some(40_000_000)); + assert_eq!(a, b); + } +} diff --git a/crates/eth-config-api/src/lib.rs b/crates/eth-config-api/src/lib.rs index 61a4009..86090c3 100644 --- a/crates/eth-config-api/src/lib.rs +++ b/crates/eth-config-api/src/lib.rs @@ -6,4 +6,4 @@ pub mod types; pub use fork_id::ForkId; pub use precompiles::{PrecompileDescriptor, PrecompileRegistry}; pub use system_contracts::{SystemContractDescriptor, SystemContractRegistry}; -pub use types::{BlobSchedule, EthConfigResponse, ForkConfig, ForkEntry}; +pub use types::{BlobSchedule, EthConfigResponse, ForkConfig, ForkEntry, HexU64}; diff --git a/crates/eth-config-api/src/types.rs b/crates/eth-config-api/src/types.rs index 96f3a99..0078305 100644 --- a/crates/eth-config-api/src/types.rs +++ b/crates/eth-config-api/src/types.rs @@ -1,12 +1,41 @@ use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use crate::fork_id::ForkId; + +/// Hex-encoded quantity helper. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct HexU64(pub u64); + +impl Serialize for HexU64 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("0x{:x}", self.0)) + } +} + +impl<'de> Deserialize<'de> for HexU64 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let s = s.strip_prefix("0x").unwrap_or(&s); + u64::from_str_radix(s, 16) + .map(HexU64) + .map_err(|e| serde::de::Error::custom(format!("invalid hex quantity: {e}"))) + } +} /// Blob gas scheduling parameters (EIP-4844 style). #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "camelCase")] pub struct BlobSchedule { - pub max_blob_gas_per_block: u64, - pub target_blob_gas_per_block: u64, - pub blob_gasprice_update_fraction: u64, + pub max_blob_gas_per_block: HexU64, + pub target_blob_gas_per_block: HexU64, + pub blob_gasprice_update_fraction: HexU64, } /// Fork-level configuration object as returned by eth_config. @@ -15,17 +44,18 @@ pub struct BlobSchedule { pub struct ForkConfig { /// Human-readable fork name (e.g. "cancun"). pub name: String, - /// Block number at which the fork activates; null in EIP becomes None here. + /// Block number at which the fork activates. #[serde(skip_serializing_if = "Option::is_none")] - pub block: Option, + pub block: Option, /// Optional blob gas schedule parameters for blob-enabled forks. #[serde(skip_serializing_if = "Option::is_none")] pub blob_schedule: Option, /// List of enabled precompiles at this fork (hex addresses as strings). - pub precompiles: Vec, + #[serde(default)] + pub precompiles: BTreeMap, /// List of system contracts active at this fork. #[serde(default)] - pub system_contracts: Vec, + pub system_contracts: BTreeMap, } /// System contract entry in eth_config response. @@ -50,10 +80,10 @@ pub struct ForkEntry { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "camelCase")] pub struct EthConfigResponse { - pub current: ForkEntry, + pub current: ForkConfig, #[serde(skip_serializing_if = "Option::is_none")] - pub next: Option, + pub next: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub last: Option, - pub fork_id: crate::fork_id::ForkId, + pub last: Option, + pub fork_id: ForkId, } From b337000ad0209bb473cc4a0cc3251adb56ea57c8 Mon Sep 17 00:00:00 2001 From: zacksfF Date: Thu, 12 Mar 2026 16:40:42 +0000 Subject: [PATCH 3/5] feat: hexify eth_config types and compute fork_id(#1) --- crates/eth-config-reth/src/namespace.rs | 4 +- crates/eth-config-reth/src/resolver.rs | 65 ++++++++++++++++--------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/crates/eth-config-reth/src/namespace.rs b/crates/eth-config-reth/src/namespace.rs index 2464d94..b3a4aa7 100644 --- a/crates/eth-config-reth/src/namespace.rs +++ b/crates/eth-config-reth/src/namespace.rs @@ -4,6 +4,8 @@ use crate::{EthConfigApi, ForkConfigCache, ForkConfigResolver}; /// Hook to install the `eth_config` namespace into reth's RPC builder. pub fn install_namespace() { - let _api = EthConfigApi::new(ForkConfigCache::new(), ForkConfigResolver::new()); + let zero_genesis = [0u8; 32]; + let resolver = ForkConfigResolver::new(zero_genesis, Vec::new()); + let _api = EthConfigApi::new(ForkConfigCache::new(), resolver); // TODO: wire `_api` into reth's RpcModule when integrating with the node. } diff --git a/crates/eth-config-reth/src/resolver.rs b/crates/eth-config-reth/src/resolver.rs index cb5b454..1d10f80 100644 --- a/crates/eth-config-reth/src/resolver.rs +++ b/crates/eth-config-reth/src/resolver.rs @@ -1,37 +1,58 @@ -use eth_config_api::{EthConfigResponse, ForkConfig}; +use eth_config_api::{EthConfigResponse, ForkConfig, ForkId}; /// Resolves fork configuration from a chain specification at runtime. /// /// Reth plumbing will populate this with access to `ChainSpec` and consensus params; the logic /// belongs here so it can be tested separately from RPC wiring. -#[derive(Debug, Default)] -pub struct ForkConfigResolver; +#[derive(Debug, Clone)] +pub struct ForkConfigResolver { + genesis_hash: [u8; 32], + forks: Vec, +} impl ForkConfigResolver { - /// Create a new resolver (wire in reth chain spec later). - pub fn new() -> Self { - Self + /// Create a new resolver with a pre-resolved fork list and genesis hash. + pub fn new(genesis_hash: [u8; 32], mut forks: Vec) -> Self { + forks.sort_by_key(|f| f.block.map(|b| b.0).unwrap_or(u64::MAX)); + Self { genesis_hash, forks } } /// Compute eth_config response for the provided block height. - pub fn resolve_at_block(&self, _block_number: u64) -> EthConfigResponse { - // Placeholder implementation; real logic will derive current/next/last forks, - // blob parameters, precompiles, system contracts, and fork id. - let placeholder = ForkConfig { - name: "placeholder".into(), - block: Some(_block_number), - blob_schedule: None, - precompiles: vec![], - system_contracts: vec![], - }; + pub fn resolve_at_block(&self, block_number: u64) -> EthConfigResponse { + let mut current: Option = None; + let mut last: Option = None; + let mut next: Option = None; + + for fork in &self.forks { + let fork_block = fork.block.map(|b| b.0).unwrap_or(u64::MAX); + if fork_block <= block_number { + last = current.replace(fork.clone()); + continue; + } + next = Some(fork.clone()); + break; + } + + let current = current.unwrap_or_else(|| self.forks.first().cloned().unwrap_or_default()); + + let fork_blocks: Vec = self + .forks + .iter() + .filter_map(|f| f.block.map(|b| b.0)) + .filter(|b| *b <= block_number) + .collect(); + + let fork_id = ForkId::compute( + self.genesis_hash, + &fork_blocks, + next.as_ref().and_then(|f| f.block.map(|b| b.0)), + ); EthConfigResponse { - current: eth_config_api::types::ForkEntry { - config: placeholder, - }, - next: None, - last: None, - fork_id: eth_config_api::ForkId::placeholder(), + current, + next, + last, + fork_id, } } } From 75a5d7a8c74e01af137af335cfcb7d9d22193755 Mon Sep 17 00:00:00 2001 From: zacksfF Date: Thu, 12 Mar 2026 16:42:03 +0000 Subject: [PATCH 4/5] feat: hexify eth_config types and compute fork_id(#1) --- examples/blob_parameter_only.json | 56 ++++++++++++++--------------- examples/cancun_sample.json | 46 +++++++++++------------- examples/prague_sample.json | 60 +++++++++++++++---------------- 3 files changed, 75 insertions(+), 87 deletions(-) diff --git a/examples/blob_parameter_only.json b/examples/blob_parameter_only.json index a91ebed..f9ec887 100644 --- a/examples/blob_parameter_only.json +++ b/examples/blob_parameter_only.json @@ -1,38 +1,34 @@ { "current": { - "config": { - "name": "bpo", - "block": 25000000, - "blobSchedule": { - "maxBlobGasPerBlock": 800000, - "targetBlobGasPerBlock": 400000, - "blobGaspriceUpdateFraction": 3338477 - }, - "precompiles": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "systemContracts": [] - } + "name": "bpo", + "block": "0x17d7840", + "blobSchedule": { + "maxBlobGasPerBlock": "0xc3500", + "targetBlobGasPerBlock": "0x61a80", + "blobGaspriceUpdateFraction": "0x32f29d" + }, + "precompiles": { + "ecrecover": "0x0000000000000000000000000000000000000001", + "sha256": "0x0000000000000000000000000000000000000002", + "ripemd": "0x0000000000000000000000000000000000000003" + }, + "systemContracts": {} }, "next": null, "last": { - "config": { - "name": "cancun", - "block": 19426545, - "blobSchedule": { - "maxBlobGasPerBlock": 786432, - "targetBlobGasPerBlock": 393216, - "blobGaspriceUpdateFraction": 3338477 - }, - "precompiles": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "systemContracts": [] - } + "name": "cancun", + "block": "0x128b7f1", + "blobSchedule": { + "maxBlobGasPerBlock": "0xc0000", + "targetBlobGasPerBlock": "0x60000", + "blobGaspriceUpdateFraction": "0x32f29d" + }, + "precompiles": { + "ecrecover": "0x0000000000000000000000000000000000000001", + "sha256": "0x0000000000000000000000000000000000000002", + "ripemd": "0x0000000000000000000000000000000000000003" + }, + "systemContracts": {} }, "forkId": { "hash": "0x9abc0011", diff --git a/examples/cancun_sample.json b/examples/cancun_sample.json index 5680bb9..8617bdc 100644 --- a/examples/cancun_sample.json +++ b/examples/cancun_sample.json @@ -1,33 +1,29 @@ { "current": { - "config": { - "name": "cancun", - "block": 19426545, - "blobSchedule": { - "maxBlobGasPerBlock": 786432, - "targetBlobGasPerBlock": 393216, - "blobGaspriceUpdateFraction": 3338477 - }, - "precompiles": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "systemContracts": [] - } + "name": "cancun", + "block": "0x128b7f1", + "blobSchedule": { + "maxBlobGasPerBlock": "0xc0000", + "targetBlobGasPerBlock": "0x60000", + "blobGaspriceUpdateFraction": "0x32f29d" + }, + "precompiles": { + "ecrecover": "0x0000000000000000000000000000000000000001", + "sha256": "0x0000000000000000000000000000000000000002", + "ripemd": "0x0000000000000000000000000000000000000003" + }, + "systemContracts": {} }, "next": null, "last": { - "config": { - "name": "shanghai", - "block": 17034870, - "precompiles": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "systemContracts": [] - } + "name": "shanghai", + "block": "0x104c533", + "precompiles": { + "ecrecover": "0x0000000000000000000000000000000000000001", + "sha256": "0x0000000000000000000000000000000000000002", + "ripemd": "0x0000000000000000000000000000000000000003" + }, + "systemContracts": {} }, "forkId": { "hash": "0xf0afd0c7", diff --git a/examples/prague_sample.json b/examples/prague_sample.json index c49fdc2..0d0da29 100644 --- a/examples/prague_sample.json +++ b/examples/prague_sample.json @@ -1,43 +1,39 @@ { "current": { - "config": { - "name": "prague", - "block": null, - "precompiles": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "systemContracts": [ - { - "address": "0x0000000000000000000000000000000000000004", - "codeHash": "0xdeadbeef", - "size": 4096, - "version": "v1" - } - ] + "name": "prague", + "block": null, + "precompiles": { + "ecrecover": "0x0000000000000000000000000000000000000001", + "sha256": "0x0000000000000000000000000000000000000002", + "ripemd": "0x0000000000000000000000000000000000000003" + }, + "systemContracts": { + "blob_gas_oracle": { + "address": "0x0000000000000000000000000000000000000004", + "codeHash": "0xdeadbeef", + "size": 4096, + "version": "v1" + } } }, "next": null, "last": { - "config": { - "name": "cancun", - "block": 19426545, - "blobSchedule": { - "maxBlobGasPerBlock": 786432, - "targetBlobGasPerBlock": 393216, - "blobGaspriceUpdateFraction": 3338477 - }, - "precompiles": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "systemContracts": [] - } + "name": "cancun", + "block": "0x128b7f1", + "blobSchedule": { + "maxBlobGasPerBlock": "0xc0000", + "targetBlobGasPerBlock": "0x60000", + "blobGaspriceUpdateFraction": "0x32f29d" + }, + "precompiles": { + "ecrecover": "0x0000000000000000000000000000000000000001", + "sha256": "0x0000000000000000000000000000000000000002", + "ripemd": "0x0000000000000000000000000000000000000003" + }, + "systemContracts": {} }, "forkId": { "hash": "0x1a2b3c4d", - "next": 99999999 + "next": "0x5f5e0ff" } } From e9fd52768f9828a1e431b5ef122b2a60e6920c92 Mon Sep 17 00:00:00 2001 From: zacksfF Date: Thu, 12 Mar 2026 16:42:15 +0000 Subject: [PATCH 5/5] feat: hexify eth_config types and compute fork_id(#1) --- tests/tests/bpo_fork.rs | 10 +++++++--- tests/tests/hoodi_cancun.rs | 4 ++-- tests/tests/hoodi_prague.rs | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/tests/bpo_fork.rs b/tests/tests/bpo_fork.rs index f952c82..18db376 100644 --- a/tests/tests/bpo_fork.rs +++ b/tests/tests/bpo_fork.rs @@ -5,8 +5,12 @@ use common::load_example; fn blob_parameter_only_fork_logic() { let cfg = load_example("blob_parameter_only.json"); - assert_eq!(cfg.current.config.name, "bpo"); - let blob = cfg.current.config.blob_schedule.as_ref().expect("blob schedule present"); - assert_eq!(blob.max_blob_gas_per_block, 800_000); + assert_eq!(cfg.current.name, "bpo"); + let blob = cfg + .current + .blob_schedule + .as_ref() + .expect("blob schedule present"); + assert_eq!(blob.max_blob_gas_per_block.0, 800_000); assert!(cfg.fork_id.next.is_none()); } diff --git a/tests/tests/hoodi_cancun.rs b/tests/tests/hoodi_cancun.rs index 87e5def..f728a5a 100644 --- a/tests/tests/hoodi_cancun.rs +++ b/tests/tests/hoodi_cancun.rs @@ -5,8 +5,8 @@ use common::load_example; fn cancun_sample_deserializes() { let cfg = load_example("cancun_sample.json"); - assert_eq!(cfg.current.config.name, "cancun"); - assert!(cfg.current.config.blob_schedule.is_some()); + assert_eq!(cfg.current.name, "cancun"); + assert!(cfg.current.blob_schedule.is_some()); assert!(cfg.next.is_none()); assert!(cfg.last.is_some()); assert_eq!(cfg.fork_id.hash, "0xf0afd0c7"); diff --git a/tests/tests/hoodi_prague.rs b/tests/tests/hoodi_prague.rs index d329942..47fd581 100644 --- a/tests/tests/hoodi_prague.rs +++ b/tests/tests/hoodi_prague.rs @@ -5,8 +5,8 @@ use common::load_example; fn prague_sample_deserializes() { let cfg = load_example("prague_sample.json"); - assert_eq!(cfg.current.config.name, "prague"); - assert!(cfg.current.config.blob_schedule.is_none()); + assert_eq!(cfg.current.name, "prague"); + assert!(cfg.current.blob_schedule.is_none()); assert!(cfg.last.is_some()); assert!(cfg.fork_id.next.is_some()); }