From fc1b23fd722c6e4d629b92a77b7e20f04c9af675 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:48:20 -0300 Subject: [PATCH 1/2] feat(rpc): add GET /lean/v0/genesis --- crates/net/rpc/src/genesis.rs | 58 +++++++++++++++++++++++++++++++++++ crates/net/rpc/src/lib.rs | 2 ++ 2 files changed, 60 insertions(+) create mode 100644 crates/net/rpc/src/genesis.rs diff --git a/crates/net/rpc/src/genesis.rs b/crates/net/rpc/src/genesis.rs new file mode 100644 index 00000000..8efbbbd2 --- /dev/null +++ b/crates/net/rpc/src/genesis.rs @@ -0,0 +1,58 @@ +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; + 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 crate::test_utils::create_test_state; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use ethlambda_storage::{Store, backend::InMemoryBackend}; + use http_body_util::BodyExt; + use std::sync::Arc; + use tower::ServiceExt; + + #[tokio::test] + async fn genesis_returns_time_and_validator_count() { + let state = create_test_state(); // genesis_time = 1000 + let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), state); + let app = crate::build_api_router(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"], 0); + } +} diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 09268765..a24fc102 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) } From b9422c3e9f4911ea5033ebe5c9422946c8559d3f 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:39:18 -0300 Subject: [PATCH 2/2] fix(rpc): address review feedback on genesis (non-vacuous validator_count test) Build a 3-validator anchor state in the genesis test so asserting validator_count == 3 actually exercises the handler. Also adds a clarifying comment explaining why head_state().validators.len() equals the genesis count (no churn in lean), and switches the test router to routes().with_state(store) for isolation. --- crates/net/rpc/src/genesis.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/net/rpc/src/genesis.rs b/crates/net/rpc/src/genesis.rs index 8efbbbd2..45638360 100644 --- a/crates/net/rpc/src/genesis.rs +++ b/crates/net/rpc/src/genesis.rs @@ -12,6 +12,8 @@ struct GenesisResponse { 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, @@ -25,21 +27,30 @@ pub(crate) fn routes() -> Router { #[cfg(test)] mod tests { - use crate::test_utils::create_test_state; + 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() { - let state = create_test_state(); // genesis_time = 1000 + // 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 = crate::build_api_router(store); + let app = routes().with_state(store); let resp = app .oneshot( Request::builder() @@ -53,6 +64,6 @@ mod tests { 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"], 0); + assert_eq!(json["validator_count"], 3); } }