Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions crates/net/rpc/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -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<Store>) -> 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;
Comment thread
MegaRedHand marked this conversation as resolved.
Comment thread
MegaRedHand marked this conversation as resolved.
json_response(GenesisResponse {
genesis_time,
validator_count,
})
}

pub(crate) fn routes() -> Router<Store> {
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);
}
}
2 changes: 2 additions & 0 deletions crates/net/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}

Expand Down
9 changes: 9 additions & 0 deletions docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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).
Expand Down
Loading