Skip to content

feat(rpc): add GET /lean/v0/config/spec#456

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

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

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

Adds the GET /lean/v0/config/spec endpoint, which exposes chain configuration parameters (slot duration, committee sizes, finality constants, etc.) as JSON.

Allows external tools and cross-client integrations to discover runtime config without out-of-band coordination. 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

File: crates/net/rpc/src/spec.rs

Critical: Hardcoded Fork Digest

  • Line 12: The FORK_DIGEST constant is hardcoded as "12345678" (dummy value). Fork digests are critical for chain identification in consensus networking—incorrect values will cause peers to reject messages or partition the node. This should be dynamically derived from genesis_validators_root and the current fork version per the consensus spec, not hardcoded. If this is intentional for early prototyping, add a TODO or compile-time warning to prevent production usage.

Safety: Unchecked Type Cast

  • Line 24: HISTORICAL_ROOTS_LIMIT as u64 assumes the source constant fits in u64. If the upstream type changes to usize (e.g., on 32-bit targets) or a larger integer, this silently truncates. Use u64::try_from() or ensure the source crate exports the constant as u64 explicitly.

Maintenance Burden

  • Line 10-11: The comment indicates FORK_DIGEST must be kept in sync with ethlambda_p2p. This violates DRY and risks drift. Move this constant to a shared configuration crate (e.g., ethlambda_types::config) that both rpc and p2p can import.

Testing Gap

  • Lines 70-72: The test asserts on timing constants but omits HISTORICAL_ROOTS_LIMIT. Add:
    assert_eq!(json["HISTORICAL_ROOTS_LIMIT"], HISTORICAL_ROOTS_LIMIT as u64);

Minor: Unnecessary Allocation

  • Line 31: FORK_DIGEST.to_string() allocates on every request. Since the response owns the data anyway, consider changing the struct field to &'static str or using Cow<'static, str> to avoid the allocation. Given this is a low-frequency config endpoint, this is merely stylistic.

Architecture Note
Consider grouping these constants into a structured ChainConfig type in the blockchain crate rather than importing them piecemeal from different modules (blockchain, types). This centralizes the protocol configuration and makes it easier to support multiple networks (mainnet, testnets, devnets).

File: crates/net/rpc/src/lib.rs

  • Line 101: Route registration looks correct. Ensure the path /lean/v0/config/spec aligns with your API versioning strategy.

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

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/net/rpc/src/spec.rs:9-10 introduces a second hard-coded FORK_DIGEST, while the networking stack already defines the real source used for gossipsub topics at crates/net/p2p/src/gossipsub/messages.rs:8. That creates a split-brain config surface: when the digest changes in p2p, /lean/v0/config/spec can silently keep returning the old value, and the new test at crates/net/rpc/src/spec.rs:56-75 would still pass because it only checks the duplicate. For a consensus client, exposing stale network identity is an interoperability bug. This should come from a shared constant/module, not a copied literal.

  2. crates/net/rpc/src/spec.rs:72-75 never asserts the new HISTORICAL_ROOTS_LIMIT field even though it is part of the added response at crates/net/rpc/src/spec.rs:20-21,31. That leaves one of the endpoint’s exported consensus constants untested and makes regressions easy to miss. Add an assertion for json["HISTORICAL_ROOTS_LIMIT"].

No consensus-path code was changed here, so I did not find issues in fork choice, attestation validation, STF, XMSS, or SSZ logic from this diff itself. I could not run the targeted Rust test in this sandbox because cargo needs write access to the default Cargo/Rustup locations and git-fetched dependencies.


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

This PR adds the GET /lean/v0/config/spec endpoint, which serialises compile-time consensus constants (MILLISECONDS_PER_SLOT, INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, HISTORICAL_ROOTS_LIMIT, FORK_DIGEST) as JSON, enabling external tools and clients to discover chain parameters at runtime.

  • spec.rs introduces the new handler, SpecResponse struct, and an integration test; the route is wired into build_api_router in lib.rs.
  • FORK_DIGEST is manually duplicated from ethlambda_p2p::gossipsub::messages with only a prose "keep in sync" comment as a guard — since ethlambda-p2p is not a dependency of ethlambda-rpc, a future change to the P2P constant will silently leave the spec endpoint returning a stale value.
  • The unit test covers four of the five response fields but omits an assertion for HISTORICAL_ROOTS_LIMIT.

Confidence Score: 3/5

The endpoint is broadly correct, but the duplicated FORK_DIGEST constant has no compile-time guard and will silently diverge from the P2P layer when the value is eventually derived dynamically.

The duplicated FORK_DIGEST without an import-based sync guard is the key concern: the whole purpose of this endpoint is for clients to discover the fork digest reliably, but a future update to the P2P crate will leave the spec endpoint returning a stale value with no compile-time or test-time warning. The rest of the implementation — serialisation, routing, test structure — is straightforward and correct.

crates/net/rpc/src/spec.rs — the FORK_DIGEST duplication and the incomplete test assertions both need attention before this can serve as a trustworthy discovery endpoint.

Important Files Changed

Filename Overview
crates/net/rpc/src/spec.rs New file adding GET /lean/v0/config/spec; duplicates FORK_DIGEST without a compile-time sync guard, creating silent divergence risk when the P2P crate updates it.
crates/net/rpc/src/lib.rs One-line module declaration and router merge for spec::routes(); change is minimal and correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant RPC as ethlambda-rpc
    participant BC as ethlambda-blockchain
    participant Types as ethlambda-types

    Client->>RPC: GET /lean/v0/config/spec
    RPC->>BC: read MILLISECONDS_PER_SLOT, INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL
    RPC->>Types: read HISTORICAL_ROOTS_LIMIT
    Note over RPC: FORK_DIGEST read from local hardcoded constant (duplicated from ethlambda-p2p)
    RPC-->>Client: "200 OK { MILLISECONDS_PER_SLOT, INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, HISTORICAL_ROOTS_LIMIT, FORK_DIGEST }"
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 ethlambda-rpc
    participant BC as ethlambda-blockchain
    participant Types as ethlambda-types

    Client->>RPC: GET /lean/v0/config/spec
    RPC->>BC: read MILLISECONDS_PER_SLOT, INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL
    RPC->>Types: read HISTORICAL_ROOTS_LIMIT
    Note over RPC: FORK_DIGEST read from local hardcoded constant (duplicated from ethlambda-p2p)
    RPC-->>Client: "200 OK { MILLISECONDS_PER_SLOT, INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, HISTORICAL_ROOTS_LIMIT, FORK_DIGEST }"
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/spec.rs:9-10
**Duplicated constant with no compile-time guard**

`FORK_DIGEST` is manually copied from `ethlambda_p2p::gossipsub::messages::FORK_DIGEST` with a prose comment asking authors to "keep in sync." Because `ethlambda-p2p` is not a dependency of `ethlambda-rpc`, there is no import that would force a compile error on divergence. The `messages.rs` doc already notes that this value will eventually be derived from the fork version and genesis validators root. When that change lands, the spec endpoint will silently return the old placeholder while the network uses the new digest — exactly the out-of-band coordination problem this endpoint is meant to eliminate. The fix is to move the constant into a shared crate (`ethlambda-types` or `ethlambda-blockchain`) and import it in both `ethlambda-p2p` and `ethlambda-rpc`.

### Issue 2 of 2
crates/net/rpc/src/spec.rs:72-75
The test never asserts `HISTORICAL_ROOTS_LIMIT`, so a wrong cast or accidental removal of that field would go undetected. Adding the assertion keeps the test in sync with the full response shape.

```suggestion
        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"],
            ethlambda_types::state::HISTORICAL_ROOTS_LIMIT as u64
        );
        assert_eq!(json["FORK_DIGEST"], FORK_DIGEST);
```

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

Comment on lines +9 to +10
// 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";

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 Duplicated constant with no compile-time guard

FORK_DIGEST is manually copied from ethlambda_p2p::gossipsub::messages::FORK_DIGEST with a prose comment asking authors to "keep in sync." Because ethlambda-p2p is not a dependency of ethlambda-rpc, there is no import that would force a compile error on divergence. The messages.rs doc already notes that this value will eventually be derived from the fork version and genesis validators root. When that change lands, the spec endpoint will silently return the old placeholder while the network uses the new digest — exactly the out-of-band coordination problem this endpoint is meant to eliminate. The fix is to move the constant into a shared crate (ethlambda-types or ethlambda-blockchain) and import it in both ethlambda-p2p and ethlambda-rpc.

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

Comment:
**Duplicated constant with no compile-time guard**

`FORK_DIGEST` is manually copied from `ethlambda_p2p::gossipsub::messages::FORK_DIGEST` with a prose comment asking authors to "keep in sync." Because `ethlambda-p2p` is not a dependency of `ethlambda-rpc`, there is no import that would force a compile error on divergence. The `messages.rs` doc already notes that this value will eventually be derived from the fork version and genesis validators root. When that change lands, the spec endpoint will silently return the old placeholder while the network uses the new digest — exactly the out-of-band coordination problem this endpoint is meant to eliminate. The fix is to move the constant into a shared crate (`ethlambda-types` or `ethlambda-blockchain`) and import it in both `ethlambda-p2p` and `ethlambda-rpc`.

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

Comment on lines +72 to +75
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["FORK_DIGEST"], FORK_DIGEST);

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 The test never asserts HISTORICAL_ROOTS_LIMIT, so a wrong cast or accidental removal of that field would go undetected. Adding the assertion keeps the test in sync with the full response shape.

Suggested change
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["FORK_DIGEST"], FORK_DIGEST);
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"],
ethlambda_types::state::HISTORICAL_ROOTS_LIMIT as u64
);
assert_eq!(json["FORK_DIGEST"], FORK_DIGEST);
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/src/spec.rs
Line: 72-75

Comment:
The test never asserts `HISTORICAL_ROOTS_LIMIT`, so a wrong cast or accidental removal of that field would go undetected. Adding the assertion keeps the test in sync with the full response shape.

```suggestion
        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"],
            ethlambda_types::state::HISTORICAL_ROOTS_LIMIT as u64
        );
        assert_eq!(json["FORK_DIGEST"], FORK_DIGEST);
```

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

Here is the review:


Review: feat(rpc): add GET /lean/v0/config/spec

This PR adds a stateless GET /lean/v0/config/spec endpoint that serializes compile-time consensus constants (slot timing, HISTORICAL_ROOTS_LIMIT, fork digest) as JSON, enabling external tools and cross-client integrations to discover runtime configuration. The implementation is clean and follows the existing handler/router pattern correctly. Two confirmed issues warrant fixes before merge.


Findings

crates/net/rpc/src/spec.rs:10FORK_DIGEST is silently duplicated from ethlambda-p2p; no compile-time check enforces equality

ethlambda_p2p::gossipsub::messages::FORK_DIGEST is the canonical pub const and already has a doc-comment tracking the TODO. spec.rs declares its own private copy with a // keep in sync comment. The rpc crate does not depend on ethlambda-p2p, so a direct import isn't available today — but ethlambda-types is already a shared dependency of both crates, making it the right home for this constant. If someone updates the p2p constant when the real fork digest is introduced and misses this copy, the API endpoint will advertise a stale FORK_DIGEST while gossipsub topics use the updated one, silently breaking cross-client topic subscriptions with no build error.

Suggested fix: move FORK_DIGEST to ethlambda-types and import it from there in both ethlambda-p2p and ethlambda-rpc.


crates/net/rpc/src/spec.rs:72 — test spec_returns_lean_constants omits an assertion for HISTORICAL_ROOTS_LIMIT

The test checks MILLISECONDS_PER_SLOT, INTERVALS_PER_SLOT, MILLISECONDS_PER_INTERVAL, and FORK_DIGEST, but never asserts json["HISTORICAL_ROOTS_LIMIT"]. The field is present in SpecResponse and populated in get_spec, but dropping or mistyping the serde(rename) attribute would leave the test green while the response silently regresses.

Suggested fix: add assert_eq!(json["HISTORICAL_ROOTS_LIMIT"], HISTORICAL_ROOTS_LIMIT as u64); at line 75.


crates/net/rpc/src/spec.rs:22fork_digest: String allocates per request for a compile-time constant (low severity)

FORK_DIGEST.to_string() allocates a new heap String on every request for a value that is a &'static str. serde serializes &str and String identically in JSON. Changing the field type to fork_digest: &'static str and assigning FORK_DIGEST directly eliminates the allocation with no behavioral change.


The HISTORICAL_ROOTS_LIMIT as u64 cast is safe — the project targets 64-bit only and the value (262,144) is far below any overflow threshold. The async fn get_spec() without .await is correct; Axum's Handler trait requires async fn even for synchronous responses.


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

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