Skip to content

feat(rpc): serve GET /eth/v2/validator/duties/proposer/{epoch} - #660

Closed
MegaRedHand wants to merge 6 commits into
feat/beacon-api-missing-endpointsfrom
feat/beacon-proposer-duties-v2
Closed

MegaRedHand wants to merge 6 commits into
feat/beacon-api-missing-endpointsfrom
feat/beacon-proposer-duties-v2

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

Stacked on #642.

Motivation

beacon-APIs #563 added GET /eth/v2/validator/duties/proposer/{epoch} and deprecated v1. The two return the same duties. They differ only in dependent_root:

dependent slot for epoch E
v1 compute_start_slot_at_epoch(E) - 1
v2 compute_start_slot_at_epoch(E - 1) - 1, genesis on underflow

v2's slot is the one fulu's proposer_lookahead depends on. process_proposer_lookahead writes epoch E's proposers during the transition into E-1. v1's later root also changes on reorgs inside E-1, which leave the duties as they were, so a validator client watching it refetches for nothing.

Changes

  • New route /eth/v2/validator/duties/proposer/{epoch}. v1 and v2 share one proposer_duties body, which takes the dependent slot as a function argument.
  • last_slot_before / last_slot_before_previous helpers. Attester duties already used v2's formula and now call the shared helper.
  • docs/rpc.md: a table row for v2, v1 marked as deprecated by the spec, and the dependent_root note rewritten for each endpoint.

Not in this PR

Testing

  • v2_dependent_root_is_the_block_before_the_previous_epoch: for both lookahead epochs, v2's data equals v1's, and its dependent_root matches the spec slot and differs from v1's. The head's epoch is 1, so this test also covers the underflow-to-genesis case.
  • an_epoch_outside_the_lookahead_is_a_400 now covers both versions.
  • cargo test -p ethlambda-rpc --lib -- beacon::validator: 25 passed. cargo clippy -p ethlambda-rpc --all-targets -D warnings is clean.

pablodeymo and others added 5 commits September 29, 2026 15:37
…eposit_contract and POST /eth/v1/validator/duties/sync/{epoch} from ethlambda beacon, three of the four Beacon API endpoints validator clients call that were missing. fork returns the state's own Fork through the same state_id resolution as the other state endpoints, so a state root is the same 404. deposit_contract returns the Config's deposit chain id and contract address. duties/sync reads the head state's current_sync_committee for an epoch in the head's own sync committee period and next_sync_committee for the next one, matches each requested validator by pubkey and returns every seat it holds (the committee is drawn with replacement), leaves out validators with no seat, and answers 400 for an unknown index or any other period and 503 while syncing. An earlier period is refused rather than answered from a historical state, recorded in docs/spec_deviations.md. compute_sync_committee_period is added to the altair helpers as validator.md defines it.
beacon-APIs #563 deprecates the v1 endpoint in favour of v2, which differs
only in `dependent_root`: the block before the previous epoch rather than
the block before the epoch. That is the block fulu's proposer lookahead
actually depends on, since an epoch's proposers are written by the epoch
transition into the epoch before it. v1's later root changes on reorgs
that leave the duties untouched, so a validator client watching it
refetches for nothing.

Both versions share one handler body, parameterised by the dependent
slot. Attester duties already used v2's formula and now share its helper.
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

I'll review this PR which adds v2 proposer duties endpoint support to ethlambda's Beacon API.

Overall Assessment

This is a well-structured, focused PR that correctly implements the Beacon API v2 proposer duties specification. The refactoring to share logic between v1 and v2 is clean. I found one minor correctness issue in test code and several code quality suggestions.


Issues Found

1. Test Bug: Potential Underflow in v2_dependent_root_is_the_block_before_the_previous_epoch (crates/net/rpc/src/beacon/validator.rs:681-682)

let dependent = compute_start_slot_at_epoch(epoch - 1).saturating_sub(1);

Problem: epoch - 1 will panic on underflow when epoch is 0. While the test loop starts at state_epoch (which is 1 in fulu_state), this is fragile. If fulu_state() ever changes to return epoch 0, this test will panic.

Fix:

let dependent = compute_start_slot_at_epoch(epoch.saturating_sub(1)).saturating_sub(1);

This matches the production last_slot_before_previous implementation and is defensive.


2. Inconsistent Documentation Comment (crates/net/rpc/src/beacon/validator.rs:338)

/// `GET /eth/v2/validator/duties/proposer/{epoch}`: v1's duties, with
/// `dependent_root` at `compute_start_slot_at_epoch(epoch - 1) - 1` (the
/// genesis block's on underflow).

Problem: The comment says "genesis block's on underflow" but should say "genesis block on underflow" or "the genesis block's root on underflow". Minor grammar issue.

Also, the formula compute_start_slot_at_epoch(epoch - 1) - 1 doesn't show the saturating_sub behavior explicitly, unlike the v1 comment which shows saturating_sub(1). This could mislead readers into thinking it panics.

Fix:

/// `GET /eth/v2/validator/duties/proposer/{epoch}`: v1's duties, with
/// `dependent_root` at `compute_start_slot_at_epoch(epoch.saturating_sub(1)).saturating_sub(1)`
/// (the genesis block's root on underflow).

3. Function Pointer vs. Trait Object Trade-off (crates/net/rpc/src/beacon/validator.rs:340)

fn proposer_duties(
    store: &Store,
    epoch: &str,
    dependent_slot: fn(Epoch) -> Slot,
) -> Result<serde_json::Value, ApiError> {

Observation: Using fn pointer is correct here since both last_slot_before and last_slot_before_previous are bare functions. This is zero-cost and clearer than a closure or trait object. No change needed, but good to confirm this was intentional (it appears so).


4. Missing Test Coverage for v2 Epoch 0 Edge Case (crates/net/rpc/src/beacon/validator.rs)

The v2 test only covers state_epoch and state_epoch + 1 where state_epoch = 1. The v1 test at line 650-663 tests epoch 0 explicitly:

#[tokio::test]
async fn dependent_root_is_the_block_at_the_last_slot_of_the_previous_epoch() {
    // ...
    let epoch = compute_epoch_at_slot(state.slot()); // epoch 1
    // ...
    assert_eq!(json["dependent_root"], format!("{head_root}"));
}

Suggestion: Add a test for v2 with epoch 0, or extend the existing v2 test to include epoch 0, to verify the genesis underflow behavior. The v2 dependent_root for epoch 0 should equal the genesis block root (same as v1 for epoch 0, and same as v2 for epoch 1 in the current test since both underflow).

Actually, re-reading: for epoch 0, v2's last_slot_before_previous(0) = last_slot_before(0.saturating_sub(1)) = last_slot_before(0) = compute_start_slot_at_epoch(0).saturating_sub(1) = 0.saturating_sub(1) = 0. So both v1 and v2 for epoch 0 depend on slot 0 (genesis). This is worth asserting.


5. Route Ordering (Minor) (crates/net/rpc/src/beacon/validator.rs:49-52)

.route(
    "/eth/v2/validator/duties/proposer/{epoch}",
    get(get_proposer_duties_v2),
)

Observation: The v2 route is added after v1. With axum's route matching, order shouldn't matter here since the paths are distinct. However, if there were ever a wildcard or fallback, v1 before v2 is the conventional ordering. This is fine as-is.


Security Considerations

  1. No new DoS vectors: The v2 endpoint reuses the same proposer_duties logic with identical epoch validation and lookahead bounds checking. No concern.

  2. No state mutation: Both endpoints are read-only from the store, consistent with existing design.

  3. SSZ/serialization: No changes to encoding; JSON responses use existing json_response helper.


Consensus Correctness

The v2 dependent_root definition matches the Beacon API specification:

The dependent root is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)

The implementation correctly uses saturating_sub to handle the genesis case, matching the spec's underflow behavior.

The comment at line 334-336 accurately explains why v2's definition is superior: it references the block that the proposer lookahead actually depends on, making reorg detection more precise for validators.


Positive Acknowledgments

  1. Clean refactoring: Extracting proposer_duties with a dependent_slot function parameter is elegant and DRY.
  2. Helper functions last_slot_before and last_slot_before_previous are well-named and reusable — good that attester_duties was updated to use last_slot_before_previous too.
  3. Test updates are thorough: Both v1 and v2 are tested for the out-of-lookahead 400 case.
  4. Documentation (rpc.md) is updated with accurate descriptions of the new endpoint and the dependent_root semantics.

Summary

Priority Item Location
Fix Test underflow: epoch - 1 → epoch.saturating_sub(1) validator.rs:681
Fix Grammar in doc comment validator.rs:338
Suggest Add v2 epoch 0 test case validator.rs tests
— All other code is correct and well-structured —

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

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Review: GET /eth/v2/validator/duties/proposer/{epoch}

The change looks correct and I found no blocking issues.

  • Dependent slot: v2 uses last_slot_before_previous(epoch), which is compute_start_slot_at_epoch(epoch-1) - 1, saturating to genesis. That matches beacon-APIs refactor(storage): cache ChainConfig in Store #563 and the attester-duties formula. The attester handler now calls the same helper, so the two cannot drift apart.
  • Underflow: epoch.saturating_sub(1) and compute_start_slot_at_epoch(..).saturating_sub(1) mean epochs 0 and 1 both resolve to the genesis block. No panic is possible.
  • Handler structure: proposer_duties takes dependent_slot: fn(Epoch) -> Slot, so v1 and v2 share one body and differ only in that argument. The fn pointer needs no boxing and is easy to read.
  • Lookahead window: the checked_sub and MIN_SEED_LOOKAHEAD filter bound the slice indexing, so window_start..[..SLOTS_PER_EPOCH] cannot go out of range. This logic was already there and the diff does not change it.
  • block_root_at_or_before: a slot at or past the head's slot returns head_root. An out-of-window slot returns a 500 rather than a guess. A v2 dependent slot is never further back than v1's, so the window risk is no worse than before.
  • Tests: v2_dependent_root_is_the_block_before_the_previous_epoch checks both lookahead epochs. It asserts that the duties data equals v1's and that the roots differ. The 400 path for out-of-range epochs is also run against v2 (line 969).

Minor nits:

  1. In v2_dependent_root_is_the_block_before_the_previous_epoch, assert_ne! on the two roots depends on the fixture's distinct per-slot roots (repeat_byte(slot + 1)). That holds today. A comment saying so would stop it breaking if someone changes the fixture.
  2. get_proposer_duties and get_proposer_duties_v2 are identical except for the function pointer. A small helper would remove the repetition, though two three-line handlers are acceptable.

I did not run the tests.


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

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

Looks good overall. The change is narrowly scoped, aligns with the Beacon API v2 proposer-duties semantics, and reuses the existing duty computation cleanly.

  • crates/net/rpc/src/beacon/validator.rs:321 The new v2 handler appears correct: only dependent_root changes, while duties still come from proposer_lookahead. That matches the stated intent and avoids touching fork-choice, attestation validation, justification/finalization, or state-transition logic.
  • crates/net/rpc/src/beacon/validator.rs:304 Using saturating_sub for the previous-epoch dependent slot is a good choice for epochs 0 and 1; it avoids underflow and preserves the documented “genesis block on underflow” behavior.
  • crates/net/rpc/src/beacon/validator.rs:342 Refactoring proposer_duties to take a dependent_slot function keeps the v1/v2 split explicit without duplicating proposer selection logic. No obvious performance or safety concern here.
  • crates/net/rpc/src/beacon/validator.rs:483 Reusing last_slot_before_previous for attester duties improves consistency with the already-documented semantics and reduces the chance of drift.

One small test nit:

  • crates/net/rpc/src/beacon/validator.rs:683 The test uses compute_start_slot_at_epoch(epoch - 1).saturating_sub(1). Since epoch - 1 happens before the saturating call, this can underflow in debug builds if the fixture ever changes to state_epoch == 0. The production code avoids this with last_slot_before_previous(epoch). I’d suggest mirroring that in the test, e.g. let dependent = last_slot_before_previous(epoch);.

No blockchain-security or memory-safety concerns jump out in this diff.


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

@MegaRedHand
MegaRedHand force-pushed the feat/beacon-api-missing-endpoints branch from a7f7f83 to b252fbc Compare October 5, 2026 19:01
…poser-duties-v2

#642 was squashed onto the current beacon-chain-integration, so this merge
ran against the old base and both sides re-added #642's work. Git kept the
`duties/sync` test module twice; the second copy is dropped. In docs/rpc.md
the duties paragraph keeps this branch's `dependent_root` wording.
MegaRedHand added a commit that referenced this pull request Oct 5, 2026
…3-636-638-gloas-live

# Conflicts:
#	docs/rpc.md
MegaRedHand added a commit that referenced this pull request Oct 5, 2026
…36-638-gloas-live

Brings gloas validator duties (produceBlockV4, envelope publication, PTC
duties and payload attestations, gloas attestation data and aggregates, VC
gloas support) onto the deployment branch, keeping every behavior of #626,
#633, #636, #638, #646, #647-#652, #656, #658-#660 and the sync-committee and
liveness endpoints.

Conflict resolutions keep both sides: the attestation pool stays in Store
(tmp) while the payload attestation pool is threaded through P2P and the RPC
handles (feature); the aggregate endpoints keep tmp's liveness recording and
attesting indices and add the feature's fork-header check and gloas pooling;
the VC tests and fake execution client serve both fulu blobs and gloas V6.

Semantic fixes:
 a. POST /eth/v2/beacon/blocks (gloas) calls publish_beacon_block(block,
    Vec::new()): gloas columns travel with the envelope. RecordingNetwork
    implements publish_beacon_block(block, sidecars) and both new methods.
 b. produceBlockV4 appends client versions to the graffiti exactly like
    produceBlockV3 (graffiti::execution_client_version run alongside the
    payload build, with_client_versions, Extension<OwnVersion>) and logs it.
 c. Attestation data, aggregate_attestation keep require_execution_client and
    require_validated for gloas slots; payload_attestation_data now applies
    the same two rules (503 without an execution client, or when the voted
    block's payload is unvalidated).
 d. Proposer duties v1 and v2 serve gloas epochs from a fulu or gloas state's
    proposer_lookahead; nothing refuses gloas any more; v2 keeps its
    dependent root.
 e. The VC's per-validator ProposerSettings apply to gloas proposals (graffiti
    in the BlockRequest, fee recipient compared with the bid's); a test pins
    the graffiti. VC tests updated to the ProposerSettings constructors.
 f. gloas production reads the attestation pool from Store and calls the
    stf with the ActiveBalanceCache the perf work added; fulu production and
    pack_operations are untouched (gloas blocks carry no pooled operations).
 g. Chain events are emitted by the chain actor only, so nothing on the RPC
    publish paths needed to move; gloas imports reach it unchanged.
 h. Cargo.lock unchanged; cargo check --locked passes.
@MegaRedHand

Copy link
Copy Markdown
Collaborator Author

Already included in #653

@MegaRedHand MegaRedHand closed this Oct 5, 2026
@MegaRedHand
MegaRedHand deleted the feat/beacon-proposer-duties-v2 branch October 5, 2026 22:16
MegaRedHand added a commit that referenced this pull request Oct 5, 2026
…63-64-633-636-638-gloas-live"

This reverts merge 1fef238, keeping its first parent's side.

#653 serves v2 proposer duties too, through its own handler, so tmp takes
that one instead of carrying both. #662 had since taught proposer_duties to
read a gloas head's lookahead; that stays, only the dependent-slot
parameter, the v2 route and its test go.
MegaRedHand added a commit that referenced this pull request Oct 5, 2026
…653 line

#663 landed on tmp while #660 was being reverted here, and was written
against #660's v2 helpers. The merge keeps #663's wall-clock bound, head
advance and blocking thread, with #653's DependentRoot in place of #660's
last_slot_before helpers and #653's 503-while-syncing v2 handler. The
lookahead paragraph moves from the v1 handler's doc to proposer_duties, as
#663 has it.

The tests' get helper now layers a default SyncStatusController, as the
production router always does: #663's v2 assertions went through routes()
without it, which #653's v2 handler answers with a 500.
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.

2 participants