Skip to content

feat(rpc): add GET /lean/v0/attestations for validator participation#459

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

feat(rpc): add GET /lean/v0/attestations for validator participation#459
MegaRedHand wants to merge 3 commits into
mainfrom
feat/lean-api-attestations

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

Adds GET /lean/v0/attestations, which returns the currently known (fork-choice-active) attestations. Optionally filterable by slot or validator index.

Enables explorer tooling to surface validator participation rates and diagnose finality stalls. 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 #459 (Attestations RPC endpoint):

Potential Compatibility Issue

  • File: crates/net/rpc/src/attestations.rs, Line 30
  • q.slot.is_none_or(|s| data.slot == s) uses Option::is_none_or, stabilized in Rust 1.82 (Oct 2024). Verify this aligns with the project's MSRV. If supporting older versions, use q.slot.map(|s| data.slot == s).unwrap_or(true) instead.

Performance/DoS Concerns

  • File: crates/net/rpc/src/attestations.rs, Lines 29-40
  • No pagination on get_attestations. With high validator counts (e.g., 1M+), extract_latest_known_attestations() could return massive datasets, causing memory pressure and RPC timeouts. Consider adding limit/offset query parameters or cursor-based pagination.
  • Line 38: In-memory sorting of potentially large vectors adds O(n log n) overhead. If the store doesn't require sorted output, consider making sorting optional via query parameter.

Code Quality

  • File: crates/net/rpc/src/attestations.rs, Lines 44-45
  • The endpoint path /lean/v0/attestations uses a non-standard prefix. Ensure this aligns with the project's API versioning strategy (this appears to be ethlambda-specific, not standard Ethereum Beacon API).

Testing

  • File: crates/net/rpc/src/attestations.rs, Lines 64-65
  • The test only validates that the response parses as JSON array. Add tests for:
    1. Slot filtering logic (when slot param provided)
    2. Empty result handling
    3. Response schema validation (checking field presence)

Security

  • No input validation on slot bounds (e.g., rejecting future slots). While this is a read-only endpoint, invalid slots could confuse clients.

Consensus Correctness

  • Data mapping appears correct for LMD GHOST vote exposure (source/target slots). No consensus logic bugs detected in this read-only path.

Recommendation: Address the MSRV issue and add pagination before merging if this targets production networks.


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/attestations.rs:29 through crates/net/rpc/src/attestations.rs:33 make ?slot= semantically wrong for anything except “current latest vote at this slot”. extract_latest_known_attestations() already collapses storage down to one latest attestation per validator (crates/storage/src/store.rs:1157 through crates/storage/src/store.rs:1162, using only the highest-slot entry per validator (crates/storage/src/store.rs:287 through crates/storage/src/store.rs:304). Filtering after that means /lean/v0/attestations?slot=N will silently omit validators that attested at slot N and then attested again later. If the API is meant to answer “show me attestations for slot N”, it needs to filter from the raw known payload set before the per-validator latest reduction, or the endpoint/param should be renamed to make the “latest only” behavior explicit.

  2. crates/net/rpc/src/attestations.rs:60 through crates/net/rpc/src/attestations.rs:77 don’t test the only nontrivial behavior in this endpoint: slot filtering over a store with real attestations. As written, the test passes on an empty anchor store and would not catch Point 1. Add at least one case where a validator has attestations at two slots and assert that ?slot=old_slot behaves as intended.

No consensus-critical logic, STF, fork choice, XMSS, or SSZ code changed in this PR beyond this observability endpoint.

I couldn’t run the crate tests in this environment because cargo/rustup need writable home directories and dependency resolution is blocked by the sandbox.


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 GET /lean/v0/attestations, a new lean-API endpoint that reads fork-choice-active per-validator attestations from the store and returns them as a JSON array, optionally filtered by slot.

  • The advertised validator_index query filter is not implemented — AttQuery only has slot, so clients sending ?validator_index=N receive the full unfiltered list silently.
  • AttestationEntry exposes only source_slot and target_slot, dropping the checkpoint roots and the head checkpoint entirely, limiting usefulness for finality-stall diagnosis.
  • The single unit test only checks for HTTP 200 + JSON array against an empty store; no test exercises slot filtering or a non-empty attestation set.

Confidence Score: 3/5

The endpoint is additive and read-only, so it cannot corrupt state, but it ships with a documented filtering capability that simply does not work.

The validator-index filter is called out in the PR description as a supported feature but is entirely absent from the implementation — any client relying on it today receives unfiltered data without any indication of the problem. This is a present gap in the delivered interface, not a speculative future concern.

crates/net/rpc/src/attestations.rs — the query struct, response shape, and test coverage all need attention before this endpoint matches its stated contract.

Important Files Changed

Filename Overview
crates/net/rpc/src/attestations.rs New endpoint GET /lean/v0/attestations; missing the advertised validator_index query filter, checkpoint roots are dropped from the response, and tests only cover the empty-store happy path.
crates/net/rpc/src/lib.rs Wires the new attestations module into the API router via a single .merge(attestations::routes()) call; straightforward, no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Router as Axum Router
    participant Handler as get_attestations
    participant Store

    Client->>Router: "GET /lean/v0/attestations[?slot=N]"
    Router->>Handler: "Query(AttQuery { slot }), State(store)"
    Handler->>Store: extract_latest_known_attestations()
    Store-->>Handler: "HashMap<validator_index, AttestationData>"
    Handler->>Handler: filter by slot (if present)
    Handler->>Handler: "map to AttestationEntry { validator_index, slot, source_slot, target_slot }"
    Handler->>Handler: sort by validator_index
    Handler-->>Client: 200 JSON array
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 Router as Axum Router
    participant Handler as get_attestations
    participant Store

    Client->>Router: "GET /lean/v0/attestations[?slot=N]"
    Router->>Handler: "Query(AttQuery { slot }), State(store)"
    Handler->>Store: extract_latest_known_attestations()
    Store-->>Handler: "HashMap<validator_index, AttestationData>"
    Handler->>Handler: filter by slot (if present)
    Handler->>Handler: "map to AttestationEntry { validator_index, slot, source_slot, target_slot }"
    Handler->>Handler: sort by validator_index
    Handler-->>Client: 200 JSON array
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/net/rpc/src/attestations.rs:12-14
**Validator-index filter is missing despite being advertised**

The PR description says the endpoint is "Optionally filterable by slot or validator index", and the handler docstring echoes this, but `AttQuery` only contains `slot`. Any client that sends `?validator_index=42` will silently receive the full unfiltered list instead of an error or a narrowed result. This is a concrete misalignment between the documented interface and the implementation. A `validator_index: Option<u64>` field should be added to `AttQuery` and applied as an additional `.filter` step alongside the slot check.

### Issue 2 of 3
crates/net/rpc/src/attestations.rs:18-24
**Checkpoint roots are dropped; only slots are returned**

`AttestationData` carries full `Checkpoint` values (both `slot` and `root`) for `source`, `target`, and `head`. `AttestationEntry` exposes only `source_slot` and `target_slot` and omits `head` entirely. Explorer tooling diagnosing finality stalls needs to correlate checkpoints against specific block roots, not just slot numbers. Consider including at least `source_root`, `target_root`, and the `head` checkpoint so consumers can cross-reference with `/lean/v0/blocks/{root}`.

### Issue 3 of 3
crates/net/rpc/src/attestations.rs:48-76
**Tests do not cover filtering or non-empty payloads**

The single test verifies only that the endpoint returns HTTP 200 with a JSON array against an empty store, so every meaningful code path — slot filtering, the sort, and the mapping of `source_slot`/`target_slot` — is exercised only in production. A test that inserts at least two attestations with different slots and then requests `?slot=<n>` would catch any filtering regression without requiring much extra setup.

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

Comment on lines +12 to +14
#[derive(Deserialize)]
struct AttQuery {
slot: Option<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 Validator-index filter is missing despite being advertised

The PR description says the endpoint is "Optionally filterable by slot or validator index", and the handler docstring echoes this, but AttQuery only contains slot. Any client that sends ?validator_index=42 will silently receive the full unfiltered list instead of an error or a narrowed result. This is a concrete misalignment between the documented interface and the implementation. A validator_index: Option<u64> field should be added to AttQuery and applied as an additional .filter step alongside the slot check.

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

Comment:
**Validator-index filter is missing despite being advertised**

The PR description says the endpoint is "Optionally filterable by slot or validator index", and the handler docstring echoes this, but `AttQuery` only contains `slot`. Any client that sends `?validator_index=42` will silently receive the full unfiltered list instead of an error or a narrowed result. This is a concrete misalignment between the documented interface and the implementation. A `validator_index: Option<u64>` field should be added to `AttQuery` and applied as an additional `.filter` step alongside the slot check.

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

Comment on lines +18 to +24
struct AttestationEntry {
validator_index: u64,
slot: u64,
source_slot: u64,
target_slot: 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 Checkpoint roots are dropped; only slots are returned

AttestationData carries full Checkpoint values (both slot and root) for source, target, and head. AttestationEntry exposes only source_slot and target_slot and omits head entirely. Explorer tooling diagnosing finality stalls needs to correlate checkpoints against specific block roots, not just slot numbers. Consider including at least source_root, target_root, and the head checkpoint so consumers can cross-reference with /lean/v0/blocks/{root}.

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

Comment:
**Checkpoint roots are dropped; only slots are returned**

`AttestationData` carries full `Checkpoint` values (both `slot` and `root`) for `source`, `target`, and `head`. `AttestationEntry` exposes only `source_slot` and `target_slot` and omits `head` entirely. Explorer tooling diagnosing finality stalls needs to correlate checkpoints against specific block roots, not just slot numbers. Consider including at least `source_root`, `target_root`, and the `head` checkpoint so consumers can cross-reference with `/lean/v0/blocks/{root}`.

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!

Comment thread crates/net/rpc/src/attestations.rs Outdated
Comment on lines +48 to +76
#[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 attestations_returns_array() {
let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), create_test_state());
let app = crate::build_api_router(store);
let resp = app
.oneshot(
Request::builder()
.uri("/lean/v0/attestations")
.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!(json.is_array());

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 Tests do not cover filtering or non-empty payloads

The single test verifies only that the endpoint returns HTTP 200 with a JSON array against an empty store, so every meaningful code path — slot filtering, the sort, and the mapping of source_slot/target_slot — is exercised only in production. A test that inserts at least two attestations with different slots and then requests ?slot=<n> would catch any filtering regression without requiring much extra setup.

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

Comment:
**Tests do not cover filtering or non-empty payloads**

The single test verifies only that the endpoint returns HTTP 200 with a JSON array against an empty store, so every meaningful code path — slot filtering, the sort, and the mapping of `source_slot`/`target_slot` — is exercised only in production. A test that inserts at least two attestations with different slots and then requests `?slot=<n>` would catch any filtering regression without requiring much extra setup.

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 data I need. Let me write the final review.


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

This PR adds a new JSON endpoint that exposes fork-choice-active attestations, optionally filtered by slot, with results sorted by validator index. The implementation is clean and follows the existing Axum handler pattern, but has three concrete issues and a test gap.


Findings

crates/net/rpc/src/attestations.rs:13–15validator_index filter promised in the PR description is not implemented

The PR body explicitly says the endpoint is "optionally filterable by slot or validator index." The AttQuery struct only has slot: Option<u64>. There is no validator_index field. Axum's Query extractor silently ignores unknown fields by default, so GET /lean/v0/attestations?validator_index=3 returns all validators unfiltered with a 200 OK — no error, no indication the filter was dropped. Either the field should be added to AttQuery and the filter applied, or the PR description must be corrected.


crates/net/rpc/src/attestations.rs:17–23AttestationEntry drops checkpoint roots, making the response semantically incomplete

AttestationData has three Checkpoint fields (head, target, source), each carrying both a slot and a root: H256. AttestationEntry exposes only source_slot and target_slot — dropping source.root, target.root, and the entire head checkpoint. Slot numbers alone are ambiguous: two validators attesting to different target roots at the same slot appear identical in the response. Equivocation detection and cross-client fork-choice debugging both require the roots. The CLAUDE.md logging convention lists target_root and source_root as standard attestation fields, reinforcing the expectation that they are included.


crates/net/rpc/src/attestations.rs:25–41 — Malformed ?slot=abc returns 422 plain text, breaking the codebase's uniform error contract

When Axum's Query<AttQuery> extractor fails (e.g. ?slot=notanumber), it returns 422 Unprocessable Entity with a plain-text body. Every other endpoint that can fail returns {"error": "..."} with 400 Bad Request (see blocks.rs:99–107). Clients that handle errors uniformly will receive an unexpected status and content type. The fix is to implement a custom rejection handler via axum::extract::rejection::QueryRejection or accept slot: Option<String> and parse/validate manually, returning the same json_response(json!({"error": "..."})) + 400 pattern.


crates/net/rpc/src/attestations.rs:57–77 — Test exercises only an empty store; slot filter and response shape are unverified

attestations_returns_array creates an anchor store (no attestations), calls the endpoint, and checks that the body is a JSON array. It cannot catch: a broken slot filter, wrong field names in AttestationEntry, missing checkpoint roots, or the 422 vs 400 error handling regression. Recommended additions: insert a synthetic attestation entry into known_payloads, assert the full serialized shape, and send ?slot=N to verify the filter path.


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

…lter, JSON errors, tests)

- Add `?validator_index=N` filter to GET /lean/v0/attestations (combinable with `?slot=`)
- Document that `?slot=` filters over latest-only attestations, not all historical ones
- Bad query params now return JSON {"error":"..."} + 400 instead of Axum's plain-text 422
- Add 5 real tests: seeded entries with field assertions, slot filter, validator_index
  filter, combined filter, and bad-param JSON 400 contract
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