From 4af7780c3ef9d697a6640c8fe5d87fc811e522a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:49:50 -0300 Subject: [PATCH 1/4] feat(rpc): add GET /lean/v0/config/spec --- crates/net/rpc/src/lib.rs | 2 + crates/net/rpc/src/spec.rs | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 crates/net/rpc/src/spec.rs diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 09268765..5e3907a0 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -14,6 +14,7 @@ mod blocks; mod fork_choice; mod heap_profiling; pub mod metrics; +mod spec; pub mod test_driver; pub(crate) use base::json_response; @@ -100,6 +101,7 @@ fn build_api_router(store: Store) -> Router { .merge(blocks::routes()) .merge(fork_choice::routes()) .merge(admin::routes()) + .merge(spec::routes()) .with_state(store) } diff --git a/crates/net/rpc/src/spec.rs b/crates/net/rpc/src/spec.rs new file mode 100644 index 00000000..5a5ec0b9 --- /dev/null +++ b/crates/net/rpc/src/spec.rs @@ -0,0 +1,77 @@ +use axum::{Router, response::IntoResponse, routing::get}; +use ethlambda_blockchain::{INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT}; +use ethlambda_storage::Store; +use ethlambda_types::state::HISTORICAL_ROOTS_LIMIT; +use serde::Serialize; + +use crate::json_response; + +// Dummy fork digest; keep in sync with ethlambda_p2p::gossipsub::messages::FORK_DIGEST until it's centralized in a shared crate. +const FORK_DIGEST: &str = "12345678"; + +#[derive(Serialize)] +struct SpecResponse { + #[serde(rename = "MILLISECONDS_PER_SLOT")] + ms_per_slot: u64, + #[serde(rename = "INTERVALS_PER_SLOT")] + intervals_per_slot: u64, + #[serde(rename = "MILLISECONDS_PER_INTERVAL")] + ms_per_interval: u64, + #[serde(rename = "HISTORICAL_ROOTS_LIMIT")] + historical_roots_limit: u64, + #[serde(rename = "FORK_DIGEST")] + fork_digest: String, +} + +async fn get_spec() -> impl IntoResponse { + json_response(SpecResponse { + ms_per_slot: MILLISECONDS_PER_SLOT, + intervals_per_slot: INTERVALS_PER_SLOT, + ms_per_interval: MILLISECONDS_PER_INTERVAL, + historical_roots_limit: HISTORICAL_ROOTS_LIMIT as u64, + fork_digest: FORK_DIGEST.to_string(), + }) +} + +pub(crate) fn routes() -> Router { + Router::new().route("/lean/v0/config/spec", get(get_spec)) +} + +#[cfg(test)] +mod tests { + use super::FORK_DIGEST; + use crate::test_utils::create_test_state; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use ethlambda_blockchain::{ + INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, + }; + use ethlambda_storage::{Store, backend::InMemoryBackend}; + use http_body_util::BodyExt; + use std::sync::Arc; + use tower::ServiceExt; + + #[tokio::test] + async fn spec_returns_lean_constants() { + let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), create_test_state()); + let app = crate::build_api_router(store); + let resp = app + .oneshot( + Request::builder() + .uri("/lean/v0/config/spec") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + let body = resp.into_body().collect().await.unwrap().to_bytes(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(json["MILLISECONDS_PER_SLOT"], MILLISECONDS_PER_SLOT); + assert_eq!(json["INTERVALS_PER_SLOT"], INTERVALS_PER_SLOT); + assert_eq!(json["MILLISECONDS_PER_INTERVAL"], MILLISECONDS_PER_INTERVAL); + assert_eq!(json["FORK_DIGEST"], FORK_DIGEST); + } +} From 72216912320df30d4f9c15ea9cd3e8237f62c494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:38:06 -0300 Subject: [PATCH 2/4] fix(rpc): address review feedback on config/spec (test assertion, static fork_digest) --- crates/net/rpc/src/spec.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/net/rpc/src/spec.rs b/crates/net/rpc/src/spec.rs index 5a5ec0b9..27fb4cb1 100644 --- a/crates/net/rpc/src/spec.rs +++ b/crates/net/rpc/src/spec.rs @@ -20,7 +20,7 @@ struct SpecResponse { #[serde(rename = "HISTORICAL_ROOTS_LIMIT")] historical_roots_limit: u64, #[serde(rename = "FORK_DIGEST")] - fork_digest: String, + fork_digest: &'static str, } async fn get_spec() -> impl IntoResponse { @@ -29,7 +29,7 @@ async fn get_spec() -> impl IntoResponse { intervals_per_slot: INTERVALS_PER_SLOT, ms_per_interval: MILLISECONDS_PER_INTERVAL, historical_roots_limit: HISTORICAL_ROOTS_LIMIT as u64, - fork_digest: FORK_DIGEST.to_string(), + fork_digest: FORK_DIGEST, }) } @@ -49,6 +49,7 @@ mod tests { INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, }; use ethlambda_storage::{Store, backend::InMemoryBackend}; + use ethlambda_types::state::HISTORICAL_ROOTS_LIMIT; use http_body_util::BodyExt; use std::sync::Arc; use tower::ServiceExt; @@ -72,6 +73,10 @@ mod tests { assert_eq!(json["MILLISECONDS_PER_SLOT"], MILLISECONDS_PER_SLOT); assert_eq!(json["INTERVALS_PER_SLOT"], INTERVALS_PER_SLOT); assert_eq!(json["MILLISECONDS_PER_INTERVAL"], MILLISECONDS_PER_INTERVAL); + assert_eq!( + json["HISTORICAL_ROOTS_LIMIT"], + HISTORICAL_ROOTS_LIMIT as u64 + ); assert_eq!(json["FORK_DIGEST"], FORK_DIGEST); } } From e5404289379ddfe7d231c197f060efa9c34e6776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:55:24 -0300 Subject: [PATCH 3/4] docs(rpc): document GET /lean/v0/config/spec --- docs/rpc.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/rpc.md b/docs/rpc.md index c5f94dd5..df263afd 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -23,6 +23,7 @@ If `--api-port` and `--metrics-port` are equal, all routers are merged onto a si | Method | Path | Response | Description | |--------|------|----------|-------------| | `GET` | `/lean/v0/health` | JSON | Liveness check | +| `GET` | `/lean/v0/config/spec` | JSON | Protocol constants the node runs with | | `GET` | `/lean/v0/states/finalized` | SSZ | Latest finalized `State` | | `GET` | `/lean/v0/blocks/finalized` | SSZ | Latest finalized `SignedBlock` | | `GET` | `/lean/v0/checkpoints/justified` | JSON | Latest justified `Checkpoint` | @@ -41,6 +42,22 @@ The handler emits a fixed, compact body (no whitespace): {"status":"healthy","service":"lean-rpc-api"} ``` +### `GET /lean/v0/config/spec` + +Protocol constants the node was built with. Keys mirror the leanSpec constant names: + +```json +{ + "MILLISECONDS_PER_SLOT": 4000, + "INTERVALS_PER_SLOT": 5, + "MILLISECONDS_PER_INTERVAL": 800, + "HISTORICAL_ROOTS_LIMIT": 262144, + "FORK_DIGEST": "12345678" +} +``` + +`FORK_DIGEST` is the 4-byte hex string (no `0x` prefix) embedded in gossipsub topic names. + ### `GET /lean/v0/states/finalized` SSZ-encoded `State` at the latest finalized checkpoint (`Content-Type: application/octet-stream`). The served state has its `latest_block_header.state_root` zeroed to match the canonical post-state representation the state transition produces, so checkpoint-sync peers reconstruct an identical state root. See [Checkpoint Sync](./checkpoint_sync.md). From e08af638dc4abd6eaf5cf46875c8d5389ac4ca94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:39:56 -0300 Subject: [PATCH 4/4] refactor(types): centralize FORK_DIGEST in a shared constants module The dummy fork digest was duplicated as a private const in the p2p gossipsub topic builder and again in the new config/spec RPC handler, each with a comment warning the other must be kept in sync. Move it to ethlambda_types::constants::FORK_DIGEST so both re-use one definition. --- crates/common/types/src/constants.rs | 10 ++++++++++ crates/common/types/src/lib.rs | 1 + crates/net/p2p/src/gossipsub/messages.rs | 9 +-------- crates/net/rpc/src/spec.rs | 5 +---- 4 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 crates/common/types/src/constants.rs diff --git a/crates/common/types/src/constants.rs b/crates/common/types/src/constants.rs new file mode 100644 index 00000000..3066b344 --- /dev/null +++ b/crates/common/types/src/constants.rs @@ -0,0 +1,10 @@ +//! Protocol constants shared across crates. + +/// Fork digest embedded in every gossipsub topic string, as lowercase hex +/// without a `0x` prefix. +/// +/// The [leanSpec](https://github.com/leanEthereum/leanSpec/pull/622) +/// currently mandates a dummy value shared across all clients; this will +/// eventually be derived from the fork version and genesis validators root. +// TODO: derive dynamically once the spec defines fork identification. +pub const FORK_DIGEST: &str = "12345678"; diff --git a/crates/common/types/src/lib.rs b/crates/common/types/src/lib.rs index aa180c98..6db9a590 100644 --- a/crates/common/types/src/lib.rs +++ b/crates/common/types/src/lib.rs @@ -2,6 +2,7 @@ pub mod aggregator; pub mod attestation; pub mod block; pub mod checkpoint; +pub mod constants; pub mod genesis; pub mod primitives; pub mod signature; diff --git a/crates/net/p2p/src/gossipsub/messages.rs b/crates/net/p2p/src/gossipsub/messages.rs index 4a47f201..11664750 100644 --- a/crates/net/p2p/src/gossipsub/messages.rs +++ b/crates/net/p2p/src/gossipsub/messages.rs @@ -1,11 +1,4 @@ -/// Fork digest embedded in every gossipsub topic string, as lowercase hex -/// without a `0x` prefix. -/// -/// The [leanSpec](https://github.com/leanEthereum/leanSpec/pull/622) -/// currently mandates a dummy value shared across all clients; this will -/// eventually be derived from the fork version and genesis validators root. -// TODO: derive dynamically once the spec defines fork identification. -pub const FORK_DIGEST: &str = "12345678"; +pub use ethlambda_types::constants::FORK_DIGEST; /// Topic kind for block gossip pub const BLOCK_TOPIC_KIND: &str = "block"; diff --git a/crates/net/rpc/src/spec.rs b/crates/net/rpc/src/spec.rs index 27fb4cb1..579b0d52 100644 --- a/crates/net/rpc/src/spec.rs +++ b/crates/net/rpc/src/spec.rs @@ -1,14 +1,11 @@ use axum::{Router, response::IntoResponse, routing::get}; use ethlambda_blockchain::{INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT}; use ethlambda_storage::Store; -use ethlambda_types::state::HISTORICAL_ROOTS_LIMIT; +use ethlambda_types::{constants::FORK_DIGEST, state::HISTORICAL_ROOTS_LIMIT}; use serde::Serialize; use crate::json_response; -// Dummy fork digest; keep in sync with ethlambda_p2p::gossipsub::messages::FORK_DIGEST until it's centralized in a shared crate. -const FORK_DIGEST: &str = "12345678"; - #[derive(Serialize)] struct SpecResponse { #[serde(rename = "MILLISECONDS_PER_SLOT")]