diff --git a/crates/net/rpc/src/genesis.rs b/crates/net/rpc/src/genesis.rs new file mode 100644 index 00000000..45638360 --- /dev/null +++ b/crates/net/rpc/src/genesis.rs @@ -0,0 +1,69 @@ +use axum::{Router, extract::State, response::IntoResponse, routing::get}; +use ethlambda_storage::Store; +use serde::Serialize; + +use crate::json_response; + +#[derive(Serialize)] +struct GenesisResponse { + genesis_time: u64, + validator_count: u64, +} + +async fn get_genesis(State(store): State) -> impl IntoResponse { + let genesis_time = store.config().genesis_time; + // Lean validators are fixed at genesis (no churn), so the current head + // state's validator registry always equals the genesis validator count. + let validator_count = store.head_state().validators.len() as u64; + json_response(GenesisResponse { + genesis_time, + validator_count, + }) +} + +pub(crate) fn routes() -> Router { + Router::new().route("/lean/v0/genesis", get(get_genesis)) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use ethlambda_storage::{Store, backend::InMemoryBackend}; + use ethlambda_types::state::{State, Validator}; + use http_body_util::BodyExt; + use std::sync::Arc; + use tower::ServiceExt; + + #[tokio::test] + async fn genesis_returns_time_and_validator_count() { + // Build a state with 3 validators so the assertion is non-vacuous. + let dummy_validator = |index: u64| Validator { + attestation_pubkey: [0u8; 52], + proposal_pubkey: [0u8; 52], + index, + }; + let validators = vec![dummy_validator(0), dummy_validator(1), dummy_validator(2)]; + let state = State::from_genesis(1000, validators); + + let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), state); + let app = routes().with_state(store); + let resp = app + .oneshot( + Request::builder() + .uri("/lean/v0/genesis") + .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["genesis_time"], 1000); + assert_eq!(json["validator_count"], 3); + } +} diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 99a2b44c..68ffce3b 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -12,6 +12,7 @@ mod admin; mod base; mod blocks; mod fork_choice; +mod genesis; mod heap_profiling; pub mod metrics; pub mod test_driver; @@ -100,6 +101,7 @@ fn build_api_router(store: Store) -> Router { .merge(blocks::routes()) .merge(fork_choice::routes()) .merge(admin::routes()) + .merge(genesis::routes()) .with_state(store) } diff --git a/docs/rpc.md b/docs/rpc.md index c5f94dd5..59bce8f3 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/genesis` | JSON | Genesis time and validator count | | `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,14 @@ The handler emits a fixed, compact body (no whitespace): {"status":"healthy","service":"lean-rpc-api"} ``` +### `GET /lean/v0/genesis` + +```json +{ "genesis_time": 1770407233, "validator_count": 16 } +``` + +`validator_count` is read from the head state's validator registry. Lean validators are fixed at genesis (no churn), so it always equals the size of the genesis registry. + ### `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).