Skip to content

feat(rpc): add GET /lean/v0/genesis#455

Open
MegaRedHand wants to merge 3 commits into
mainfrom
feat/lean-api-genesis
Open

feat(rpc): add GET /lean/v0/genesis#455
MegaRedHand wants to merge 3 commits into
mainfrom
feat/lean-api-genesis

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

Adds the GET /lean/v0/genesis endpoint, which returns the genesis time and genesis validator set from the node's configuration.

Useful for clients that need to bootstrap their slot clock or verify they are on the correct network. Has unit tests and passed clippy.

Stacked on #454.

Split the monolithic lib.rs API router into focused modules: core.rs
holds the finalized-state/finalized-block/justified-checkpoint handlers
and shared response helpers; blocks.rs, fork_choice.rs, and admin.rs
each expose pub(crate) routes() -> Router<Store>. build_api_router
merges them with .with_state(store). No behavior change.
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

Review of PR 455: Add genesis RPC endpoint

The implementation is clean and follows existing patterns, but there's a critical semantic issue regarding what constitutes "genesis" data.

Critical Issue

  1. Endpoint returns head state, not genesis state (genesis.rs:14-15)
    The endpoint /lean/v0/genesis returns store.head_state().validators.len(), which gives the current validator count. This mutates as the chain processes deposits/exits. The standard Beacon API /eth/v1/beacon/genesis returns immutable genesis data.

    Fix: Either rename the endpoint to /lean/v0/head/summary (if current state is intended), or query the actual genesis state (e.g., store.genesis_state() or store.state_at_slot(0)) to return the genesis validator count and genesis_validators_root.

Medium Concerns

  1. State access pattern (genesis.rs:14)
    Verify that store.head_state() returns an Arc<BeaconState> or reference rather than cloning the entire state. If it clones, this creates a DoS vector where frequent API calls trigger expensive state copies (Ethereum states are large).

  2. Missing genesis fields
    Standard genesis endpoints return genesis_validators_root and genesis_fork_version. Consider adding these for API compatibility, or document the intentional omission in the GenesisResponse docstring.

Minor Suggestions

  1. Explicit casting (genesis.rs:15)
    The as u64 cast from usize is safe on 64-bit targets (standard for Ethereum nodes), but prefer u64::try_from(state.validators.len()).expect("validator count overflow") for explicitness, or add a comment acknowledging the assumption.

  2. Error handling (genesis.rs:13)
    Ensure store.head_state() cannot panic or hang if the state is uninitialized. If it returns Option or Result, handle it gracefully rather than unwrapping implicitly via the accessor.

Code Quality

  • Tests are well-structured using tower::ServiceExt for integration testing.
  • Axum router composition in lib.rs:104 follows existing conventions correctly.
  • The json_response helper usage maintains consistency with adjacent modules.

Recommendation: Address Item 1 (semantic mismatch) before merging. The current implementation will confuse users expecting immutable genesis data while the node is synced past genesis.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Low: get_genesis derives validator_count via store.head_state().validators.len() at crates/net/rpc/src/genesis.rs. Store::head_state() returns a full cloned State crates/storage/src/store.rs, and State carries large SSZ-backed collections like historical roots and justification bitfields crates/common/types/src/state.rs crates/common/types/src/state.rs. For a lightweight metadata endpoint, that makes each request O(size of state) in CPU/memory traffic. I’d prefer a cheap accessor such as cached validator-count metadata on Store, or another path that avoids cloning the full consensus state.

No correctness, fork-choice, attestation, finalization, XMSS, or SSZ issues are introduced by this PR beyond that performance concern; the change is otherwise straightforward route wiring plus a focused test.

I could not run the Rust test suite in this sandbox: cargo/rustup need writes under /home/runner/.rustup and /home/runner/.cargo, which are read-only here, and dependency fetches are therefore blocked.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a GET /lean/v0/genesis endpoint that returns genesis_time and validator_count, wired into the existing Axum router alongside a unit test.

  • genesis_time is sourced correctly from store.config(), which reads immutable chain configuration.
  • validator_count is sourced from store.head_state().validators.len() — the current head state — rather than the genesis/anchor state, which contradicts the stated goal of returning the "genesis validator set" and will drift as the chain evolves.
  • The test only exercises the initial anchor state where head and genesis are identical, so it does not catch this divergence.

Confidence Score: 3/5

The endpoint's genesis_time field is correct, but validator_count reads from the live head state rather than the genesis state, making it semantically wrong for any chain that changes its validator set after genesis.

The validator_count field is sourced from store.head_state(), which evolves with every block; the endpoint claims to expose genesis-stable data but will return different values depending on when it is called. The test does not detect this because it only runs against a freshly initialized store where head and genesis happen to be the same.

crates/net/rpc/src/genesis.rs — specifically the validator_count derivation on line 15.

Important Files Changed

Filename Overview
crates/net/rpc/src/genesis.rs New genesis endpoint; genesis_time is read correctly from config, but validator_count reads from the live head state instead of the genesis state, mismatching the stated semantics and making the field unstable over the chain's lifetime.
crates/net/rpc/src/lib.rs Straightforward module declaration and router merge for the new genesis module; no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant RPC as GET /lean/v0/genesis
    participant Store

    Client->>RPC: GET /lean/v0/genesis
    RPC->>Store: store.config()
    Store-->>RPC: "ChainConfig { genesis_time }"
    RPC->>Store: store.head_state()
    Store-->>RPC: State (full clone of current head)
    RPC->>RPC: .validators.len() as u64
    RPC-->>Client: "{ genesis_time, validator_count }"

    note over Store,RPC: head_state() returns the live current state, not genesis state
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant RPC as GET /lean/v0/genesis
    participant Store

    Client->>RPC: GET /lean/v0/genesis
    RPC->>Store: store.config()
    Store-->>RPC: "ChainConfig { genesis_time }"
    RPC->>Store: store.head_state()
    Store-->>RPC: State (full clone of current head)
    RPC->>RPC: .validators.len() as u64
    RPC-->>Client: "{ genesis_time, validator_count }"

    note over Store,RPC: head_state() returns the live current state, not genesis state
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/net/rpc/src/genesis.rs:14-15
**Head state used instead of genesis state for validator count**

`store.head_state()` returns the current, evolving head state, not the genesis state. The PR description says this endpoint returns the "genesis validator set," but as the chain progresses and validators are added or removed, `head_state().validators.len()` will diverge from the genesis validator count. A client relying on this to "verify they are on the correct network" could observe different counts depending on when they query, making the field unreliable for its stated purpose. The genesis validator set should be read from the anchor/genesis state, not from the live head.

### Issue 2 of 2
crates/net/rpc/src/genesis.rs:15
**Full state clone on every request to read one field**

`store.head_state()` deserializes and clones the entire `State` object just to call `.validators.len()`. Since genesis data is immutable by definition, this full-state clone on every GET is wasteful. Consider caching the genesis validator count at startup, or reading only the relevant field rather than materializing the whole state.

Reviews (1): Last reviewed commit: "feat(rpc): add GET /lean/v0/genesis" | Re-trigger Greptile

Comment on lines +14 to +15
let genesis_time = store.config().genesis_time;
let validator_count = store.head_state().validators.len() as u64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Head state used instead of genesis state for validator count

store.head_state() returns the current, evolving head state, not the genesis state. The PR description says this endpoint returns the "genesis validator set," but as the chain progresses and validators are added or removed, head_state().validators.len() will diverge from the genesis validator count. A client relying on this to "verify they are on the correct network" could observe different counts depending on when they query, making the field unreliable for its stated purpose. The genesis validator set should be read from the anchor/genesis state, not from the live head.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/src/genesis.rs
Line: 14-15

Comment:
**Head state used instead of genesis state for validator count**

`store.head_state()` returns the current, evolving head state, not the genesis state. The PR description says this endpoint returns the "genesis validator set," but as the chain progresses and validators are added or removed, `head_state().validators.len()` will diverge from the genesis validator count. A client relying on this to "verify they are on the correct network" could observe different counts depending on when they query, making the field unreliable for its stated purpose. The genesis validator set should be read from the anchor/genesis state, not from the live head.

How can I resolve this? If you propose a fix, please make it concise.


async fn get_genesis(State(store): State<Store>) -> impl IntoResponse {
let genesis_time = store.config().genesis_time;
let validator_count = store.head_state().validators.len() as u64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Full state clone on every request to read one field

store.head_state() deserializes and clones the entire State object just to call .validators.len(). Since genesis data is immutable by definition, this full-state clone on every GET is wasteful. Consider caching the genesis validator count at startup, or reading only the relevant field rather than materializing the whole state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/src/genesis.rs
Line: 15

Comment:
**Full state clone on every request to read one field**

`store.head_state()` deserializes and clones the entire `State` object just to call `.validators.len()`. Since genesis data is immutable by definition, this full-state clone on every GET is wasteful. Consider caching the genesis validator count at startup, or reading only the relevant field rather than materializing the whole state.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have all the information I need. Here is the review:


PR Review: feat(rpc): add GET /lean/v0/genesis

This PR adds a /lean/v0/genesis endpoint that returns genesis_time and validator_count from the store, stacked on #454. The implementation is small and clean, but there are a few issues worth addressing.


Findings

crates/net/rpc/src/genesis.rs:8-10 — Response shape diverges from the PR description

The PR description says the endpoint returns "the genesis time and genesis validator set." GenesisResponse only contains genesis_time and validator_count — a derived integer, not the actual validator set. If any lean client calls this endpoint to retrieve genesis pubkeys for bootstrapping (e.g. to verify committee assignments or reconstruct the genesis state), it receives only a count and silently gets nothing useful. If the spec or other client implementations define this endpoint to return actual pubkeys, ethlambda will be incompatible. Either the response struct should include the validator pubkeys, or the PR description (and probably the endpoint name/semantics) should be clarified to "genesis metadata" rather than "genesis validator set."


crates/net/rpc/src/genesis.rs:15validator_count is read from head_state, not genesis state

head_state() returns the state at the current chain head (slot N), not at genesis (slot 0). For an endpoint explicitly named /genesis, the semantically correct source is the genesis state. While lean consensus has no validator churn today so the counts happen to match, this couples the genesis endpoint to live chain state. Two nodes at different heads (e.g. one slightly behind during sync) would return the same value coincidentally but for the wrong reason. If validator registration is added to the state transition function, this endpoint silently returns a post-genesis count. A correct implementation would read the anchor/genesis state, or store the genesis validator count in ChainConfig alongside genesis_time.


crates/net/rpc/src/genesis.rs:15 — Full state deserialization for a single .len() call

store.head_state() deserializes the entire State from storage, including the full validator list with ~104 bytes of XMSS pubkeys per validator, just to call .len() and discard everything else. This is the same pattern as fork_choice.rs:51-52, but a genesis endpoint is supposed to return immutable data — the answer never changes after genesis. The validator count could be stored in ChainConfig (next to genesis_time) or cached in a metadata key, eliminating both the correctness issue above and the deserialization cost on every request.


crates/net/rpc/src/genesis.rs:55-57 — Test assertion is vacuous for validator_count

create_test_state() initializes validators: Default::default() (empty list), so assert_eq!(json["validator_count"], 0) passes trivially — it would pass even if the implementation always returned 0 regardless of actual state. A meaningful test would populate the genesis state with one or more validators and assert the count matches. As written, the test gives false confidence that the validator count logic is exercised.


crates/net/rpc/src/genesis.rs:47 — Test uses crate::build_api_router instead of routes().with_state(store)

fork_choice.rs (line 105) tests the module in isolation via routes().with_state(store), keeping tests self-contained and independent of the full router. The genesis test reaches into the parent crate with crate::build_api_router(store), coupling it to all other routes. This is a minor inconsistency but could cause unexpected interactions if other routes change.


Automated review by Claude (Anthropic) · sonnet · custom prompt

…ount 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.
Base automatically changed from feat/lean-api-router-scaffold to main June 24, 2026 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant