diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 99a2b44c..1f257d8d 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..27fb4cb1 --- /dev/null +++ b/crates/net/rpc/src/spec.rs @@ -0,0 +1,82 @@ +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: &'static str, +} + +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, + }) +} + +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 ethlambda_types::state::HISTORICAL_ROOTS_LIMIT; + 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["HISTORICAL_ROOTS_LIMIT"], + HISTORICAL_ROOTS_LIMIT as u64 + ); + assert_eq!(json["FORK_DIGEST"], FORK_DIGEST); + } +} 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).