Skip to content

feat(core): implement validatorapi validators handler#457

Open
varex83agent wants to merge 10 commits into
mainfrom
bohdan/validatorapi-validators
Open

feat(core): implement validatorapi validators handler#457
varex83agent wants to merge 10 commits into
mainfrom
bohdan/validatorapi-validators

Conversation

@varex83agent
Copy link
Copy Markdown
Collaborator

Summary

  • Implements Component::validators in crates/core/src/validatorapi/component.rs, replacing the previous unimplemented!() stub. Port of Validators in core/validatorapi/validatorapi.go (Charon v1.7.1, lines 1218–1296).
  • Adds a convert_validators helper that mirrors convertValidators (validatorapi.go lines 1305–1332), rewriting each upstream validator's inner.pubkey from the cluster's root key to this node's public share. The ignore_not_found flag follows the Go contract — pass-through when the request filtered by indices only, error otherwise.
  • Adds invert_pub_share_map, used once per request to translate the VC-supplied pubshares back into root pubkeys before forwarding to the upstream POST /eth/v1/beacon/states/{state_id}/validators endpoint.
  • Aliases Validator to the auto-generated GetStateValidatorsResponseResponseDatum in validatorapi/types.rs, matching the established pattern used by the other duty types.
  • Bounds the upstream call with UPSTREAM_REQUEST_TIMEOUT (504 on elapse), maps transport failures to 502, and propagates upstream 400 / 404 without leaking the upstream body into the client-visible message. Reuses the existing upstream_timeout / upstream_call_failed / upstream_status_error / upstream_unexpected helpers.

Go reference

Pluto (Rust) Charon (Go)
Component::validators (component.rs) Component.Validators (validatorapi.go:1218–1296)
convert_validators (component.rs) Component.convertValidators (validatorapi.go:1305–1332)
invert_pub_share_map (component.rs) keysByShare map built in NewComponent (Go derives once at construction; we build per request since it is small and only the validators handler needs it)
Input pubshare → root pubkey rejection (400 unknown validator public key in request) getPubKeyFunc "unknown public key" branch in validatorapi.go

Notable simplification vs Go: Pluto's EthBeaconNodeApiClient is the plain auto-generated client and has no CompleteValidators cache, so the Go cache-then-fetch fast path (lines 1234–1286) collapses to a single upstream call.

Test plan

  • cargo +nightly fmt --all --check
  • cargo clippy -p pluto-core --all-targets --all-features -- -D warnings
  • cargo test -p pluto-core --all-features — 379/379 passing (11 new tests covering the validators handler and its helpers: convert_validators happy-path / ignore-not-found pass-through / unknown-without-ignore rejection / malformed upstream pubkey; invert_pub_share_map round-trip; end-to-end happy path; pubkey-only filter pass-through of strangers; index-filter rejection of strangers; unknown input pubshare rejection (400); upstream timeout (504); malformed upstream pubkey (502); upstream 400 propagation without body leakage).

varex83 and others added 10 commits May 28, 2026 14:08
Threads the Handler through Axum state via AppState<H> + with_state,
wires the node_version route to the real handler, and adds a TestHandler
mock that future PRs will extend per-endpoint.
Re-uses the auto-generated pluto_eth2api envelopes
(GetProposerDutiesResponseResponse, GetVersionResponseResponse) as the
on-the-wire shape rather than hand-rolling parallel types. node_version
is migrated to the same pattern; the body.rs hand-rolled wrapper module
is removed.
Drops the per-handler generic parameter and routes through
Arc<dyn Handler> via AppState. The Handler trait is object-safe
(Send + Sync + 'static + async_trait-generated methods), so this
is a pure type change with no surface impact.
Adds the Handler impl that the router has been calling through.
node_version returns the obolnetwork/pluto/{version}-{commit}/{arch}-{os}
identity string; proposer_duties calls the upstream beacon node and
rewrites known DV root public keys to this node's public share so the
validator client sees keys matching its keystore. The remaining 17
trait methods are unimplemented!() stubs that land per-PR as their
router handlers are ported.
Wires POST /eth/v1/validator/duties/attester/{epoch}: dual-format
(numeric or string-encoded) validator index body, upstream call,
pubshare swap.
Wires POST /eth/v1/validator/duties/sync/{epoch}, reusing the
ValIndexes dual-format body extractor.
Wires GET /eth/v1/validator/attestation_data. The Component now
holds an Arc<MemDB> and awaits unsigned attestation data from the
local DutyDB rather than hitting upstream.
Bug fixes (must-fix per review):

- attestation_data: wrap MemDB::await_attestation in tokio::time::timeout
  (24s) so a request for a slot that never produces consensus output
  cannot hold a handler task indefinitely. delete_duty now records
  evicted keys per duty type and notifies waiters, so await_data returns
  Error::AwaitDutyExpired immediately when the awaited duty is gone
  instead of spinning until the timeout fires. Maps to 408 on the wire.
- Stop leaking upstream BlindedBlock400Response Debug output (incl.
  stacktraces) into the client-visible ApiError.message. The variant
  payload is now attached as `source` for debug logs; the message stays
  generic.

Hardening:

- new_insecure is gated behind #[cfg(test)] so the insecure_test flag
  cannot reach production builds.
- new_router applies DefaultBodyLimit::max(64 KiB) on the two
  POST /duties/{attester,sync}/{epoch} routes — defends against the
  Vec<u64> parse amplification on the ValIndexes deserializer.
- All upstream eth2_cl calls are wrapped in tokio::time::timeout(12s)
  so a hanging beacon node cannot stall handler tasks.
- proposer_duties / attester_duties / sync_committee_duties propagate
  upstream BadRequest as 400 and ServiceUnavailable as 503 instead of
  collapsing every non-Ok variant to 502 — the VC can now back off on
  upstream syncing instead of treating it as a gateway failure.
- swap_attester_pubshares / swap_sync_committee_pubshares now return
  500 (cluster misconfig) instead of 502 when a pubshare is missing —
  the upstream returned well-formed data, the failure is local.

ValIndexes:

- Replace #[serde(untagged)] with a streaming Visitor that validates
  each element via SeqAccess::next_element. Avoids the speculative
  Vec<u64> parse and the serde Content cache. Now accepts mixed
  numeric/string elements and rejects negative integers.
- Hard cap at 8192 indices per request.

ApiError:

- with_boxed_source for sources that aren't std::error::Error (e.g.
  anyhow::Error from auto-gen request builders).

Router:

- attestation_data uses Result<Query<...>, QueryRejection> so 4xx
  responses from missing/malformed query params share the same
  { code, message } envelope as the rest of the router.

Tests (+13):

- attestation_data: timeout when data never arrives; 408 when duty is
  evicted while a waiter is parked; cancellation cleanup when the
  handler future is dropped; negative lookup on wrong committee_index.
- Status-mapping helpers: confirm upstream Debug output is never
  serialized into the message.
- Router: ApiError envelope on bad query; oversized body rejection;
  ValIndexes empty/mixed/oversized/negative cases.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
Ports `Validators` from `core/validatorapi/validatorapi.go` (lines 1218–1296)
together with the `convertValidators` helper (lines 1305–1332). The handler
translates VC-supplied pubshares back to the cluster's root pubkeys before
calling the upstream `POST /eth/v1/beacon/states/{state_id}/validators`
endpoint, then rewrites each returned validator's inner pubkey from the
root key to this node's public share so the downstream VC sees the share
it is configured to sign with.

`ignoreNotFound` follows the Go semantics: when the request filtered by
indices, an upstream validator that is not part of this cluster surfaces
as 500 (`pubshare not found`); otherwise the entry passes through with
its root pubkey unchanged. Upstream timeouts surface as 504, transport
failures as 502, and a malformed pubkey from the upstream as 502.
Upstream 400 / 404 propagate faithfully without leaking the upstream
body into the client-visible message.

`Validator` is aliased to the auto-generated
`GetStateValidatorsResponseResponseDatum`, matching the established
pattern for the other duty types in `validatorapi/types.rs`.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
Base automatically changed from bohdan/validatorapi-5 to main June 2, 2026 12:56
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