Skip to content

feat(rpc): serve states/{id}/fork, config/deposit_contract and validator/duties/sync - #642

Open
pablodeymo wants to merge 1 commit into
beacon-chain-integrationfrom
feat/beacon-api-missing-endpoints
Open

pablodeymo wants to merge 1 commit into
beacon-chain-integrationfrom
feat/beacon-api-missing-endpoints

Conversation

@pablodeymo

Copy link
Copy Markdown
Collaborator

🗒️ Description / Motivation

Validator clients call Beacon API endpoints that ethlambda beacon didn't serve. This PR adds three of the four:

  • GET /eth/v1/beacon/states/{state_id}/fork (ValidatorRequiredApi): used for fork detection and signing domains. Prysm's validator client calls it in its doppelganger check.
  • GET /eth/v1/config/deposit_contract: network sanity checks and tooling. Prysm's validator client needs it for voluntary exits and its keymanager UI.
  • POST /eth/v1/validator/duties/sync/{epoch} (ValidatorRequiredApi): a validator client's sync committee service. Without it, Lighthouse and Prysm log a warning and skip sync duties.

The fourth, POST /eth/v1/validator/liveness/{epoch}, comes in a separate PR, since it's the only one that needs new shared state.

What Changed

  • crates/net/rpc/src/beacon/states.rs: get_fork. It resolves state_id with the same load() as the other state endpoints and returns state.fork(), plus execution_optimistic and finalized.
  • crates/net/rpc/src/beacon/config.rs: get_deposit_contract. It returns Config::deposit_chain_id and deposit_contract_address, the same values /config/spec reports.
  • crates/net/rpc/src/beacon/validator.rs: post_sync_duties / sync_duties.
  • crates/blockchain/state_transition/src/beacon/helpers/altair.rs: compute_sync_committee_period, as altair's validator.md defines it.
  • Docs: the three routes in the Beacon API table in docs/rpc.md, a note on sync duties, and a new docs/spec_deviations.md entry.

Correctness / Behavior Guarantees

fork

  • A state root is a 404 with the existing "not indexed" message, the same as every other state endpoint.

duties/sync

  • Which committee: an epoch in the head state's own sync committee period reads current_sync_committee; the next period reads next_sync_committee.
  • Refused periods: any other period is a 400.
    • The period after next is refused by the spec.
    • An earlier period is refused deliberately, since it would need a historical state. That's recorded as a spec deviation. Lighthouse and Prysm serve it, but a validator client only asks about the current and next period.
  • Seats: a validator is matched by pubkey, since the committee stores pubkeys, and gets every seat it holds. The committee is drawn with replacement, so one validator can hold several. One pass over the committee builds the map, so the cost doesn't depend on how many validators are requested.
  • Errors:
    • A validator with no seat is left out of data, as the spec says.
    • An unknown index is a 400, as in both Lighthouse and Prysm.
    • 503 while syncing, as the spec and both clients do. The existing duties/attester and duties/proposer don't return 503; that's unchanged here.
  • Known gap: this node doesn't serve the sync committee message or contribution endpoints yet (planned separately). A validator client that gets sync duties here will fail to publish the messages they call for. Today it skips sync duties with a warning; neither way earns the sync rewards.

Tests Added / Run

  • fork:
    • the_fork_is_the_one_the_state_carries
    • the_finalized_states_fork_is_marked_finalized
    • a_fork_by_state_root_is_a_404
  • deposit_contract: the_deposit_contract_is_mainnets
  • duties/sync, on a fixture whose current committee is validators 0–7 and next committee validators 8–15, so reading the wrong one shows up:
    • the_current_period_reads_the_current_committee (also covers a validator in neither committee, and every seat listed)
    • the_next_period_reads_the_next_committee
    • the_period_after_next_is_a_400
    • an_earlier_period_is_a_400
    • an_unknown_validator_is_a_400
    • a_syncing_node_answers_503
  • compute_sync_committee_period: a_sync_committee_period_spans_its_epochs_and_no_more
  • CI's minimal-preset commands pass locally (cargo test -p ethlambda-types --lib --features preset-minimal and the same for ethlambda-state-transition).
  • Not yet run: a kurtosis devnet with Lighthouse's validator client, to check its sync-duties warning goes away.

Related Issues / PRs

  • Liveness follows in a separate PR.
  • The sync committee message and contribution endpoints are PR C/D of the validator API plan.

✅ Verification Checklist

  • Ran make fmt — clean
  • Ran make lint (clippy with -D warnings) — clean
  • Ran make test (test-consensus plus test-node, at release-fast) — all passing (1948 passed, 0 failed, 26 ignored)

@pablodeymo pablodeymo added the beacon Ethereum Beacon Chain client label Oct 1, 2026
@pablodeymo pablodeymo mentioned this pull request Oct 1, 2026
3 tasks done
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Review: PR 642 (sync duties, fork, and deposit_contract endpoints)

The change is small and well scoped, and the new endpoints are tested. I found no correctness or security bugs. The remaining points are minor.

post_sync_duties (crates/net/rpc/src/beacon/validator.rs)

  • The period selection is correct. Same period as the head uses current_sync_committee, the next period uses next_sync_committee, and anything else is a 400. head_period + 1 can't overflow in practice.
  • Matching validators by pubkey with a single pass over the committee is the right shape. Seats are returned as quoted strings and a validator with several seats gets all of them, as the spec requires.
  • The SyncStatusController extension is already layered onto the beacon router (lib.rs:215), so the handler won't hit a missing-extension 500.
  • Duplicate indices in the request body produce duplicate duty entries. Dedup the indices if you want to match other clients. It is harmless otherwise.
  • An unknown validator index returns 400, which is stricter than some clients. That is consistent with the attester-duties endpoint. Since the PR already documents the earlier-period refusal in spec_deviations.md, one line on this would help.
  • The period is derived from the head state's slot, not the wall clock. If the head lags across a period boundary (empty slots), a request for the new period gets the head's next committee. That is still correct, since it is the committee for the following period. The case is not covered by a test and is not a bug.
  • held.clone() allocates once per matching validator. That is negligible next to the positions map built per request.

get_fork (states.rs)

  • It is consistent with the other state endpoints: same load, execution_optimistic, and finalized handling, and a 0x state root gives a 404. The tests cover quoting, the finalized flag, and the root refusal.

get_deposit_contract (config.rs)

  • It reads from the same Config as /spec, so the two can't disagree. It uses HexPrefixed for the address and to_string() for the quoted chain id, as the spec expects. The test pins mainnet's values.

compute_sync_committee_period (helpers/altair.rs)

  • It is a faithful port of the spec function and has a boundary test. It will panic only if EPOCHS_PER_SYNC_COMMITTEE_PERIOD were 0, which can't happen with the presets.

Docs

  • rpc.md and spec_deviations.md are updated accurately. The note that no sync committee message or contribution endpoint exists yet is useful. A validator client that gets duties here cannot publish what they ask for.

Overall this looks good to merge.


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

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

Looks good overall. The added endpoints are small, well-scoped, and the sync-duty implementation is efficient enough for the stated constraints. I did not spot consensus-critical bugs in fork choice, finalization, state transition, XMSS, or SSZ paths from this diff.

  • No correctness issue in compute_sync_committee_period; the helper and boundary test in crates/blockchain/state_transition/src/beacon/helpers/altair.rs:173 are straightforward and spec-aligned.
  • GET /eth/v1/beacon/states/{state_id}/fork in crates/net/rpc/src/beacon/states.rs:106 correctly reuses existing state resolution and preserves the usual execution_optimistic/finalized metadata.
  • GET /eth/v1/config/deposit_contract in crates/net/rpc/src/beacon/config.rs:37 is low risk and serializes the expected fields in Beacon API format.
  • POST /eth/v1/validator/duties/sync/{epoch} in crates/net/rpc/src/beacon/validator.rs:93 has sensible gating:
    • rejects while syncing,
    • rejects pre-Altair states,
    • limits queries to current/next sync-committee period,
    • handles repeated committee seats correctly by pubkey.

One small maintainability/perf note:

  • crates/net/rpc/src/beacon/validator.rs:138 builds HashMap<BlsPubkey, Vec<String>>, converting positions to strings eagerly. Since only matched validators are returned, storing Vec<usize>/Vec<u64> first and stringifying only when building SyncDuty would avoid some unnecessary allocation. Not a blocker, just a small cleanup.

No security concerns jumped out from this PR.


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

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

I'll review this PR systematically across correctness, security, performance, and Rust best practices.

Overall Assessment

This PR adds three Beacon API endpoints (/eth/v1/config/deposit_contract, /eth/v1/beacon/states/{state_id}/fork, /eth/v1/validator/duties/sync/{epoch}) and a helper function. The code is generally well-structured with good test coverage, but I found several issues.


Detailed Findings

1. Critical: Integer Overflow Risk in compute_sync_committee_period

File: crates/blockchain/state_transition/src/beacon/helpers/altair.rs, line 173-175

pub fn compute_sync_committee_period(epoch: Epoch) -> u64 {
    epoch / preset::EPOCHS_PER_SYNC_COMMITTEE_PERIOD
}

Epoch is typically u64, and EPOCHS_PER_SYNC_COMMITTEE_PERIOD is 256 on mainnet. The division is safe, but the function lacks documentation on expected input bounds. If Epoch were ever a smaller type, this could panic. More critically, callers multiplying back (as seen in tests) can overflow:

File: crates/net/rpc/src/beacon/validator.rs, lines 1004-1005

let next_period_epoch = preset::EPOCHS_PER_SYNC_COMMITTEE_PERIOD
    * (compute_sync_committee_period(compute_epoch_at_slot(state.slot())) + 1);

This multiplication can overflow u64 if compute_sync_committee_period returns a large value. While practically unlikely with mainnet parameters, defensive coding is warranted.

Suggestion: Add checked_mul or document the invariant. Consider saturating or checked arithmetic for the multiplication in validator.rs.


2. Bug: Incorrect Error Type for Pre-Altair States

File: crates/net/rpc/src/beacon/validator.rs, line 86

let (current, next) = state
    .sync_committees()
    .map_err(|_| ApiError::BadRequest("sync committees start at altair"))?;

This returns 400 Bad Request when sync committees don't exist (pre-Altair state). Per the Beacon API specification, this should be 500 Internal Server Error or at minimum not 400—the client didn't send a bad request, the server has a state that doesn't support this feature. Actually, re-reading: if a validator client requests a sync duty for a pre-Altair epoch, that's arguably a client error. However, if the head state is pre-Altair due to the node's own sync position, this is misleading.

Suggestion: Verify against spec. If the node is on a pre-Altair fork and receives this request, 400 may be acceptable, but consider if 500 or a more specific error is appropriate.


3. Performance: Unnecessary Clone in sync_duties

File: crates/net/rpc/src/beacon/validator.rs, line 107

validator_sync_committee_indices: held.clone(),

held is &Vec<String> from the HashMap, and SyncDuty stores Vec<String>. The clone is necessary for the return value, but consider if SyncDuty could use Arc<[String]> or if the positions could be returned as slices. Given the API response needs owned data, this is acceptable but worth noting for high-throughput scenarios.

More concerning is the eager construction of the entire positions map:

File: crates/net/rpc/src/beacon/validator.rs, lines 95-100

let mut positions: HashMap<BlsPubkey, Vec<String>> = HashMap::new();
for (position, pubkey) in committee.pubkeys.iter().enumerate() {
    positions
        .entry(*pubkey)
        .or_default()
        .push(position.to_string());
}

This builds a map for all validators in the committee (512 on mainnet) even if only a few indices are requested. For typical validator clients requesting 1-100 duties, this is wasteful.

Suggestion: For small request sets, linear scan the committee per validator instead. The break-even depends on typical request sizes, but consider benchmarking or using a threshold-based approach.


4. Correctness: compute_sync_committee_period Not Using Spec's uint64 Division Semantics

File: crates/blockchain/state_transition/src/beacon/helpers/altair.rs, line 173-175

The spec defines compute_sync_committee_period as epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD using Python's floor division. Rust's / on unsigned integers is floor division, so this matches. However, the function should explicitly handle or document behavior for Epoch types that might be signed.

Acknowledgment: The test at lines 606-612 correctly verifies boundary conditions. Good coverage.


5. Security: Potential DoS via Large Request Body

File: crates/net/rpc/src/beacon/validator.rs, line 72-76

async fn post_sync_duties(
    // ...
    Json(indices): Json<Vec<String>>,
) -> Response {

There's no apparent limit on indices length. A malicious client could send a very large array, causing:

  • Excessive memory allocation in JSON parsing
  • Long iteration time in sync_duties
  • Large response construction

Suggestion: Add a maximum length check for indices, or use Axum's body size limits. Compare with existing post_attester_duties for consistency.


6. Error Handling: Inconsistent Validator Lookup Error

File: crates/net/rpc/src/beacon/validator.rs, line 104-106

let validator = state
    .validator(validator_index)
    .map_err(|_| ApiError::BadRequest("unknown validator index"))?;

This returns 400 for an unknown validator index. However, the Beacon API specification typically expects 404 Not Found or 400 depending on context. Verify consistency with other endpoints in this file.

Looking at post_attester_duties for comparison—check if it uses the same pattern. If inconsistent, standardize.


7. Code Quality: Default::default() for SyncStatusController in Tests

File: crates/net/rpc/src/beacon/validator.rs, multiple test lines (e.g., 978, 993, 1007, etc.)

post_sync(state.clone(), epoch, &["3", "9", "20"], Default::default()).await;

Default::default() for SyncStatusController presumably creates a "not syncing" state. This is implicit and fragile—if Default changes, tests break semantically without compilation errors.

Suggestion: Use an explicit constructor like SyncStatusController::new(SyncStatus::NotSyncing) for clarity, matching the a_syncing_node_answers_503 test pattern.


8. Documentation: fork Endpoint Version Inconsistency

File: docs/rpc.md, line 233

| `GET` | `/eth/v1/beacon/states/{state_id}/fork` | JSON | That state's `Fork`: previous and current version, and the epoch it changed |

The implementation uses v1:

File: crates/net/rpc/src/beacon/states.rs, line 40

.route("/eth/v1/beacon/states/{state_id}/fork", get(get_fork))

But the standard Beacon API has /eth/v1/beacon/states/{state_id}/fork in the spec—this appears correct. However, verify the response format matches spec exactly, including the execution_optimistic and finalized metadata fields.


9. Test Coverage: Missing Test for execution_optimistic in get_fork

File: crates/net/rpc/src/beacon/states.rs, lines 457-489

The the_fork_is_the_one_the_state_carries test checks execution_optimistic is a boolean, but doesn't verify it matches store.is_beacon_optimistic(root) correctly. The the_finalized_states_fork_is_marked_finalized test only checks finalized.

Suggestion: Add a test verifying execution_optimistic propagates correctly from the store, or at least matches the expected value for the test fixture.


10. Rust Idiom: Unnecessary format! in Test Assertion

File: crates/net/rpc/src/beacon/validator.rs, line 983

assert_eq!(duties[0]["pubkey"], format!("0x{}", hex::encode(pubkey.0)));

This creates a hex string manually. If pubkey implements Serialize with a hex format, use that instead. If not, consider whether serde_json::to_value(pubkey) would work, or if a helper exists.

Actually, looking more carefully: pubkey.0 suggests accessing internal bytes. This is fragile if the struct layout changes.

Suggestion: Use pubkey.to_string() or a dedicated formatting method if available, or serde_json::to_value(&pubkey) to match the JSON serialization path.


11. Memory Safety: hex::encode(pubkey.0) Assumes Fixed-Size Array

File: crates/net/rpc/src/beacon/validator.rs, line 983

pubkey.0 is presumably a fixed-size byte array (48 bytes for BLS12-381). hex::encode handles this correctly, but this pattern appears ad-hoc.

Suggestion: Ensure BlsPubkey has a consistent serialization approach. If serde with serialize_with is used elsewhere, use that for test assertions to avoid drift.


12. Spec Compliance: get_deposit_contract Response Format

File: crates/net/rpc/src/beacon/config.rs, lines 38-46

crate::json_response(serde_json::json!({
    "data": {
        "chain_id": config.deposit_chain_id.to_string(),
        "address": HexPrefixed(&config.deposit_contract_address).to_string(),
    }
}))

Verify chain_id should be string or number per spec. The .to_string() makes it a string. Check Beacon API spec—typically chain_id is a Uint64 quoted string in JSON, which this matches.

The address uses HexPrefixed—verify this produces lowercase hex with 0x prefix, as spec requires.


13. Concurrency: sync_status.get() Race Condition?

File: crates/net/rpc/src/beacon/validator.rs, line 78

if sync_status.get() == SyncStatus::Syncing {

This reads a shared atomic or lock. The value could change immediately after this check, before sync_duties executes. This is a TOCTOU (time-of-check-time-of-use) issue, but likely benign here since:

  • Worst case: node starts syncing during request, serves slightly stale data
  • The spec doesn't require strict linearizability for this endpoint

Acknowledgment: This pattern is common in this codebase and acceptable for this use case.


14. Missing Import Check

File: crates/net/rpc/src/beacon/validator.rs, line 17

use ethlambda_blockchain::{SyncStatusController, metrics::SyncStatus};

Verify SyncStatusController is Clone or Copy as needed for the Extension layer. The tests use Default::default() and explicit construction, suggesting it implements appropriate traits.


Positive Observations

  1. Good test coverage for boundary conditions in compute_sync_committee_period
  2. Clear documentation of intentional spec deviations in docs/spec_deviations.md
  3. Consistent error handling pattern with ApiError
  4. Efficient committee lookup using HashMap instead of O(n×m) scanning (though see Point 3 for trade-off)
  5. Proper SSZ/JSON serialization helpers used consistently

Summary Table

Priority Item File Line
High Overflow risk in period multiplication validator.rs ~1004
Medium Potential DoS via unbounded indices validator.rs 72-76
Medium Inefficient eager HashMap construction validator.rs 95-100
Low Implicit Default::default() in tests validator.rs multiple
Low hex::encode(pubkey.0) fragility validator.rs 983
Low Missing execution_optimistic test states.rs 457-489

The PR is generally well-crafted and ready for merge after addressing the overflow risk (Item 1) and considering the DoS limit (Item 2).


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

@MegaRedHand
MegaRedHand added this pull request to stack #654 October 2, 2026 19:56

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few comments, nothing blocking. The period bound is the only behavior change I'd consider; the rest are small cleanups and tighter tests.

let requested_period = compute_sync_committee_period(epoch);
let committee = if requested_period == head_period {
current
} else if requested_period == head_period + 1 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: the allowed periods are measured from the head state's slot, not the clock. When the head is still in period P but the clock is already in P+1 (an empty first slot of the new period, or a head a few slots behind), a request for P+2 is a valid "next period" request and gets a 400 here.

Lighthouse covers this by advancing a lagging head state to the current period before answering (duties_from_state_load in http_api/src/sync_committees.rs). Here that would mean advancing through fork_choice::checkpoint_state, as attestation_data already does, since P+2's committee only exists in the advanced state's next_sync_committee.

for validator_index in indices {
let validator = state
.validator(validator_index)
.map_err(|_| ApiError::BadRequest("unknown validator index"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: a 400 for an index the state doesn't know matches Lighthouse, but attester_duties in this same file skips unknown indices and answers 200. Worth picking one behavior for both duty endpoints, so a validator client sending the same index set to both gets consistent answers?

indices: &[String],
) -> Result<serde_json::Value, ApiError> {
let epoch = parse_epoch(epoch)?;
let indices = indices

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: this parse block is the same as attester_duties', which collects into a HashSet and so also drops duplicates. Here ["3", "3"] returns validator 3's duty twice. A shared parse_indices helper next to parse_epoch would keep both in line.


// One pass over the committee rather than one per requested validator:
// the committee stores pubkeys, so that is what a validator is matched by.
let mut positions: HashMap<BlsPubkey, Vec<String>> = HashMap::new();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: this allocates a String per seat and then clones the Vec for each duty. Storing Vec<u64> positions and serializing the field with serialize_with = "ethlambda_types::beacon::serde_helpers::quoted_u64_seq::serialize" (as the altair containers do) avoids the per-seat allocations; once the requested indices are deduplicated, remove instead of get + clone avoids the copy too.

Comment thread docs/rpc.md
state; see `docs/spec_deviations.md`). A validator is matched by pubkey and
gets every seat it holds, since the committee is drawn with replacement; one
with no seat is left out. An unknown index is a `400`, and the endpoint is a
`503` while the node is syncing. This node serves no sync committee message

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: besides the message and contribution endpoints, POST /eth/v1/validator/sync_committee_subscriptions is also unserved. Once a validator client gets sync duties it starts calling it, so for a VC with a sync committee member the visible effect of this PR is more 404s (subscriptions per epoch, messages and contributions per slot) instead of one 404 on duties/sync. Worth listing the subscriptions gap here too.


/// `GET /eth/v1/beacon/states/{state_id}/fork`: the `Fork` the state carries,
/// which is what a validator client builds its signing domains from.
async fn get_fork(Path(state_id): Path<String>, State(store): State<Store>) -> Response {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: the load, error and execution_optimistic + finalized envelope here is the same as get_finality_checkpoints'. A small helper that takes a closure over the state (state_json(store, state_id, |state| data)) would keep the state sub-resources consistent as more are added.

crate::json_response(serde_json::json!({
"data": {
"chain_id": config.deposit_chain_id.to_string(),
"address": HexPrefixed(&config.deposit_contract_address).to_string(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: hex_string(config.deposit_contract_address), defined further down in this file, does the same thing.

let response = app.oneshot(request).await.unwrap();
let status = response.status();
let body = response.into_body().collect().await.unwrap().to_bytes();
(status, serde_json::from_slice(&body).unwrap_or_default())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Test nit: unwrap_or_default() turns a non-JSON body into Null, and the 400 tests only check the status code, so they would still pass if a different 400 path fired (epoch parse, body rejection). Unwrapping the parse and asserting on json["message"] would pin each test to the check it names.

}

#[tokio::test]
async fn the_current_period_reads_the_current_committee() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Test gap: every current-period test asks for exactly the head's epoch. A case for a later epoch in the same period (for example head_epoch + 1) would catch a regression to comparing epochs instead of periods.

…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.
@MegaRedHand
MegaRedHand force-pushed the feat/beacon-api-missing-endpoints branch from a7f7f83 to b252fbc Compare October 5, 2026 19:01
MegaRedHand added a commit that referenced this pull request Oct 5, 2026
…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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beacon Ethereum Beacon Chain client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants