Skip to content

feat(core): implement validatorapi attestation + aggregation handlers#492

Open
varex83agent wants to merge 2 commits into
bohdan/validatorapi-pr1-proxy-wiringfrom
bohdan/validatorapi-pr2-attestation-aggregation
Open

feat(core): implement validatorapi attestation + aggregation handlers#492
varex83agent wants to merge 2 commits into
bohdan/validatorapi-pr1-proxy-wiringfrom
bohdan/validatorapi-pr2-attestation-aggregation

Conversation

@varex83agent

Copy link
Copy Markdown
Collaborator

Summary

Ports the attestation & aggregation domain of the validator API from Charon v1.7.1 (core/validatorapi/{router.go,validatorapi.go,eth2types.go}). Fills the four previously unimplemented!() Component methods and wires their axum router handlers (previously todo!()).

Stacks on #488 (PR 1 — proxy + proposal/validators wiring), which laid the producer-hook scaffolding (pub_key_by_att_fn, await_agg_attestation_fn, await_agg_sig_db_fn, duty_def_fn) and the verify_partial_sig helper this PR consumes.

Scope

Endpoints wired (v1 attestation/aggregate routes intentionally stay 404, matching Go):

Route Handler
POST /eth/v2/beacon/pool/attestations submit_attestations
GET /eth/v2/validator/aggregate_attestation aggregate_attestation
POST /eth/v2/validator/aggregate_and_proofs submit_aggregate_attestations
POST /eth/v1/validator/beacon_committee_selections beacon_committee_selections

Go reference (v1.7.1, core/validatorapi/): SubmitAttestations, AggregateAttestation, SubmitAggregateAttestations, BeaconCommitteeSelections, and the matching router handlers + createAggregateAttestation.

Behaviour (Go parity)

  • submit_attestations — resolves the validator index (explicit for Electra/Fulu; matched from the attester-duty set by committee index + single aggregation bit for pre-Electra), looks up the DV root pubkey via pub_key_by_att, verifies the partial attester signature (DOMAIN_BEACON_ATTESTER, epoch from the attestation's target checkpoint), and broadcasts under an attester duty.
  • aggregate_attestation — blocking await via the agg-attestation hook; returns the versioned attestation with the Eth-Consensus-Version header and { "version", "data" } body.
  • submit_aggregate_attestations — resolves the aggregator pubkey from the validator cache, verifies the inner selection proof (skipped under insecure_test, as in Go) and the outer partial signature (DOMAIN_AGGREGATE_AND_PROOF), then broadcasts under an aggregator duty.
  • beacon_committee_selections — verifies each slot signature (DOMAIN_SELECTION_PROOF), broadcasts under a prepare-aggregator duty, then awaits the aggregated selections from the AggSigDB; returns { "data": [...] }.

Router decodes versioned JSON and SSZ arrays keyed off Eth-Consensus-Version, lifts the Electra/Fulu SingleAttestation wire form into the versioned wrapper (committee index → committee bitfield, attester index → validator index), and reproduces Go's response shapes.

Shared surface (relevant to PR 3 / PR 4)

  • crates/eth2api/src/spec/electra.rs: adds SingleAttestation (SSZ + JSON, string-encoded indices).
  • crates/core/src/signeddata.rs: derives Serialize/Deserialize on AttesterDuty (so it can key a DutyDefinitionSet).
  • crates/core/src/validatorapi/types.rs: replaces the VersionedAttestation / VersionedSignedAggregateAndProof / BeaconCommitteeSelection placeholder structs with aliases to the signeddata / eth2api wrappers.
  • crates/core/src/validatorapi/testutils.rs: append-only TestHandler recording fields + setters for the four endpoints.

Tests

  • Router (router.rs): happy path + JSON/SSZ negotiation + missing-version-header + unsupported-content-type + malformed-body + v1-route-404 for each endpoint; versioned aggregate response headers/body; selections round-trip.
  • Component (component.rs): pre-Electra index resolution + broadcast, Electra explicit-index path, multi-aggregation-bit rejection, aggregate await + missing-hook 503, aggregate submit resolve+broadcast + unknown-validator rejection, selections broadcast + AggSigDB round-trip + unknown-validator rejection.

Quality gates (all green)

  • cargo +nightly fmt --all --check
  • cargo clippy --workspace --all-targets and --all-features
  • cargo test --workspace (pluto-core 426, eth2api 91, eth2util 144 — all pass)
  • cargo build --workspace --all-features
  • cargo deny check

cargo test --all-features is Docker-gated in the sandbox (eth2api integration feature spins a Lighthouse testcontainer); the functional gate is cargo test --workspace (default features), and --all-features is confirmed to compile and be clippy-clean.

🤖 Generated with Claude Code

varex83agent and others added 2 commits June 17, 2026 14:08
Port the attestation/aggregation domain of the validator API from Charon
v1.7.1 (core/validatorapi). Fills the four previously-unimplemented
Component methods and wires their axum router handlers:

- POST /eth/v2/beacon/pool/attestations -> submit_attestations
- GET  /eth/v2/validator/aggregate_attestation -> aggregate_attestation
- POST /eth/v2/validator/aggregate_and_proofs -> submit_aggregate_attestations
- POST /eth/v1/validator/beacon_committee_selections -> beacon_committee_selections

The v1 attestation/aggregate routes stay 404 (Go parity).

Component:
- submit_attestations: resolves the validator index (explicit for
  Electra/Fulu, matched from the attester-duty set + single aggregation
  bit for pre-Electra), looks up the DV root pubkey via pub_key_by_att,
  verifies the partial attester signature (DOMAIN_BEACON_ATTESTER, epoch
  from the attestation target), and broadcasts under an attester duty.
- aggregate_attestation: blocking await via the agg-attestation hook.
- submit_aggregate_attestations: looks up the aggregator pubkey from the
  validator cache, verifies the inner selection proof (skipped under
  insecure_test) and the outer partial sig (DOMAIN_AGGREGATE_AND_PROOF),
  broadcasts under an aggregator duty.
- beacon_committee_selections: verifies each slot signature
  (DOMAIN_SELECTION_PROOF), broadcasts under a prepare-aggregator duty,
  then awaits the aggregated selections from the AggSigDB.

Router decodes versioned JSON/SSZ arrays per Eth-Consensus-Version, lifts
Electra/Fulu SingleAttestation into the versioned wrapper, and returns the
versioned aggregate / selection response shapes.

Shared surface: adds electra::SingleAttestation, derives Serialize/
Deserialize on signeddata::AttesterDuty, and replaces the three attestation
placeholder types in validatorapi::types with signeddata aliases.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
- submit_attestations: match Go's no-duty-match behaviour — leave the
  validator index at 0 and let the pubkey lookup fail, instead of
  returning an early error; move the single-aggregation-bit check inside
  the committee-matching loop, mirroring validatorapi.go.
- beacon_committee_selections: resolve the AggSigDB hook up front so a
  misconfigured component fails before broadcasting partial selections.
- router: rename the non-snake-case `AttestationPayload_phase0` helper to
  `phase0_attestation_payload`.
- tests: add Electra/SSZ aggregate-and-proof decode, out-of-range
  committee-index rejection, and a multi-slot beacon-committee-selection
  broadcast test.

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 (4 parallel review agents: functional-parity vs Charon v1.7.1, security, rust-style, code-quality). Terminated by: completion (clean after fixes).

Quality gates (final)

  • cargo +nightly fmt --all --check — pass
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — pass
  • cargo test --workspace — pass (pluto-core 430, eth2api 91, eth2util 144)

Note: cargo test --all-features is Docker-gated in the sandbox (eth2api integration feature spins a Lighthouse testcontainer that never readies); the functional gate is cargo test --workspace (default features) and --all-features is confirmed to compile and be clippy-clean with -D warnings.

Resolved during the loop

Bugs (0)

Major (2)

  • Pre-Electra "no matching attester duty" returned 400 instead of Go's valIdx=0 fall-through — component.rs resolve_attestation_validator_index — restructured to mirror validatorapi.go (single-bit check moved inside the committee-matching loop; no match leaves index 0 so the pubkey lookup fails) — ad372ea
  • Non-snake-case helper AttestationPayload_phase0router.rs — renamed to phase0_attestation_payload, dropped #[allow(non_snake_case)]ad372ea

Minor (2)

  • beacon_committee_selections resolved the AggSigDB hook only after broadcasting partial selections — component.rs — hoisted the hook resolution above the broadcast loop so a misconfigured component fails before any side-effects — ad372ea
  • Test-coverage gaps — added submit_aggregate_attestations_decodes_phase0_ssz, submit_aggregate_attestations_decodes_electra_json, submit_attestations_rejects_out_of_range_committee_index (committee_index=64 → 400), and beacon_committee_selections_broadcasts_per_slotad372ea

Nits (0 changed)

Outstanding (accepted — parity-equivalent with Charon v1.7.1)

  • No element-count cap or request timeout on the new submit/await handlers. Charon has the same shape (relies on request-context cancellation; trust boundary is the local validator client). Flagged as defense-in-depth but out of scope for a functional-equivalence port; the proposal handlers' PROPOSAL_TIMEOUT precedent could be extended in a follow-up if desired.

Verdict

PR is ideal — all bug/major findings resolved, gates green.

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