Skip to content

feat(core): implement validatorapi sync committee handlers#490

Open
varex83agent wants to merge 2 commits into
bohdan/validatorapi-pr1-proxy-wiringfrom
bohdan/validatorapi-pr3-sync-committee
Open

feat(core): implement validatorapi sync committee handlers#490
varex83agent wants to merge 2 commits into
bohdan/validatorapi-pr1-proxy-wiringfrom
bohdan/validatorapi-pr3-sync-committee

Conversation

@varex83agent

Copy link
Copy Markdown
Collaborator

Summary

Wires the four sync-committee validator-API endpoints end to end — component logic + axum router handlers — porting Charon v1.7.1 core/validatorapi. Nothing lands as dead code: each endpoint is reachable from the router and exercised by tests.

This is PR 3 of the validatorapi port (sync committee domain). It stacks on PR 1 (#488, proxy + proposal/validators wiring) and builds on the shared surface PR 1 introduced (new_router(handler, builder_enabled, upstream_base_url), AppState, TestHandler setters).

Scope

Endpoint Route Handler
submit sync messages POST /eth/v1/beacon/pool/sync_committees submit_sync_committee_messages
sync contribution GET /eth/v1/validator/sync_committee_contribution sync_committee_contribution
submit contribution+proofs POST /eth/v1/validator/contribution_and_proofs submit_sync_committee_contributions
sync selections POST /eth/v1/validator/sync_committee_selections sync_committee_selections

The previously unimplemented!() Component methods and todo!() router handlers are now implemented. The placeholder validatorapi sync types are replaced with the real altair / v1 spec types.

Behavior (Go parity, v1.7.1)

  • submit_sync_committee_messages (component.go:958): resolve each validator_index against the active-validators cache ("validator not found" on miss), build a partial SignedSyncMessage, verify against this node's share (domain DOMAIN_SYNC_COMMITTEE, message root = beacon_block_root), group by slot, broadcast SyncMessage duties.
  • submit_sync_committee_contributions (component.go:1009): verify the inner selection proof against the aggregator's full validator pubkey (domain DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, root = SyncAggregatorSelectionData), mirroring Go's VerifyEth2SignedData; then verify the outer partial signature against this node's share (domain DOMAIN_CONTRIBUTION_AND_PROOF, root = Message.HashTreeRoot); group by slot, broadcast SyncContribution duties.
  • sync_committee_selections (component.go:1072): verify each partial SyncCommitteeSelection against the share (domain DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF), broadcast PrepareSyncContribution duties, then await the aggregated selection per (duty, pubkey) from the aggsigdb ("invalid sync committee selection" if the stored type is wrong) and return them.
  • sync_committee_contribution (component.go:948): forward (slot, subcommittee_index, beacon_block_root) to the registered await_sync_contribution hook and wrap the result.
  • Signature verification is skipped under insecure_test on both the share and full-pubkey paths, matching Go's c.insecureTest guard.
  • All four endpoints are JSON-only, matching Charon's declared Encodings: [JSON]. Responses use the { "data": ... } envelope (wrapResponse).

Go references (v1.7.1)

  • core/validatorapi/validatorapi.go: SubmitSyncCommitteeMessages, SyncCommitteeContribution, SubmitSyncCommitteeContributions, SyncCommitteeSelections, verifyPartialSig.
  • core/eth2signeddata.go: VerifyEth2SignedData + the SignedSyncMessage / SignedSyncContributionAndProof / SyncCommitteeSelection domain & epoch impls.
  • core/signeddata.go: the NewPartialSignedSync* constructors.
  • core/validatorapi/router.go: submitSyncCommitteeMessages, syncCommitteeContribution, submitContributionAndProofs, syncCommitteeSelections.

Test coverage

  • Component: per-slot grouping + subscriber fan-out for messages and contributions; validator not found rejection; selection broadcast + aggsigdb collection; wrong-aggregated-type rejection; unregistered-hook 503; contribution hook forwarding.
  • Router: happy-path array forwarding; JSON data envelopes for contribution (GET query parse) and selections; 415 on non-JSON content type; 400 on empty body; 400 on missing query parameter.

Gates (run from worktree)

  • cargo +nightly fmt --all --check — clean
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean
  • cargo test --workspace — pass (--all-features test is Docker-gated in this sandbox via the eth2api integration feature; confirmed it compiles and is clippy-clean)
  • cargo deny check — ok

🤖 Generated with Claude Code

varex83agent and others added 2 commits June 17, 2026 13:37
Wire the four sync-committee validator-API endpoints end to end
(component logic + axum router handlers), porting Charon v1.7.1
core/validatorapi.

- submit_sync_committee_messages: POST /eth/v1/beacon/pool/sync_committees
- sync_committee_contribution: GET /eth/v1/validator/sync_committee_contribution
- submit_sync_committee_contributions: POST /eth/v1/validator/contribution_and_proofs
- sync_committee_selections: POST /eth/v1/validator/sync_committee_selections

Submit handlers resolve the validator index against the active-validators
cache ("validator not found" on a miss), verify partial signatures against
this node's share (sync-committee message / contribution-and-proof / sync
selection domains), and broadcast SyncMessage / SyncContribution /
PrepareSyncContribution duties to subscribers grouped by slot. The
contribution submitter additionally verifies the inner selection proof
against the aggregator's full validator pubkey, mirroring Go's
VerifyEth2SignedData. Selections then await the aggregated proof per
(duty, pubkey) from the aggsigdb. All four endpoints are JSON-only, matching
Charon's declared encodings.

Replaces the placeholder validatorapi sync types with the real altair / v1
spec types and extends the router TestHandler to record sync submissions.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
Address self-review findings on the sync-committee port:

- parse_json_array now rejects a non-UTF8 Content-Type with 415 (surfacing
  the raw header), matching the sibling request_is_ssz helper instead of
  silently defaulting to JSON.
- Drop the now-stale #[allow(dead_code)] on verify_partial_sig (it is
  consumed by the sync submit handlers in this PR) and refresh its doc.
- Add component tests: two aggregators grouped into one slot for
  contributions; subscriber failure surfaces 500 and aborts the fan-out;
  empty input arrays broadcast nothing and return an empty selections set;
  selections collects multiple (duty, pubkey) entries from the aggsigdb.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
@varex83agent

Copy link
Copy Markdown
Collaborator Author

/loop-review-pr summary

Ran 1 review-and-fix iteration against this PR (four parallel reviewers: functional-equivalence, security, rust-style, code-quality). Terminated by: completion_promise — no bug/major findings; only minor/nit items, the actionable ones fixed.

Quality gates (final)

  • cargo +nightly fmt --all --check — pass
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — pass
  • cargo test --workspace — pass (default features; --all-features is Docker-gated in CI sandbox via the eth2api integration feature, confirmed to compile + clippy-clean)
  • cargo deny check — pass

Resolved during the loop

Bugs (0) — none.

Major (0) — none.

Minor (3)

  • parse_json_array collapsed a non-UTF8 Content-Type to "" and returned a 415 that dropped the offending value — now mirrors the sibling request_is_ssz (415 surfacing the raw header). crates/core/src/validatorapi/router.rse610d16
  • Test gaps: added component tests for two aggregators grouped into one slot (contributions), subscriber-failure 500 + fan-out abort, empty-input arrays broadcasting nothing, and selections collecting multiple (duty, pubkey) entries from the aggsigdb. — e610d16
  • (rust-style) newly-added Go-cross-reference doc comments: deferred — this comment style is the pervasive established convention across these files (PR 1 merged with it); changing only the new ones would be inconsistent and out of scope.

Nits (3)

  • Removed the now-stale #[allow(dead_code, reason = "…later PRs")] on verify_partial_sig (consumed by this PR's submit handlers) and refreshed its doc. — e610d16
  • Empty selections serializing as [] vs Go's null; GET content-type gate; SigningError→400 classification — deferred as intentional parity / established patterns consistent with the existing proposal handlers.

Outstanding

None blocking. Deferred items above are documented design/parity choices, not defects.

Verdict

PR is ideal — zero bug/major findings, gates green. Confirmed functional equivalence with Charon v1.7.1 across all four sync-committee endpoints (BLS domains, message roots, epoch derivation, duty grouping, inner-vs-outer signature verification, error strings, JSON-only encoding, response envelopes).

🤖 Generated with Claude Code

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