feat(core): validatorapi voluntary exit + validator registration handlers#491
Conversation
…ation handlers Port the validator-lifecycle endpoints from Charon v1.7.1: - POST /eth/v1/beacon/pool/voluntary_exits (submit_exit): JSON-only, looks up the DV root pubkey from the active-validator cache, builds an Exit duty at slot = slots_per_epoch * exit.epoch, verifies the partial signature under DOMAIN_VOLUNTARY_EXIT, and broadcasts to subscribers. - POST /eth/v1/validator/register_validator (submit_validator_registrations): JSON or SSZ array body. Empty list and builder-disabled inputs are swallowed; non-distributed-validator keys are swallowed; each managed registration maps its timestamp to a slot via slot_from_timestamp, is verified under DOMAIN_APPLICATION_BUILDER (epoch 0), and broadcast. Supporting changes: - eth2util: add helpers::slot_from_timestamp (genesis time + slot duration, with the pre-genesis current-time fallback). - eth2api v1: derive ssz Encode/Decode on (Signed)ValidatorRegistration. - core ssz_codec: decode_signed_validator_registrations for the bare 180-byte concatenation, with the invalid-buffer-size guard. - validatorapi types: back SignedValidatorRegistration / SignedVoluntaryExit with the real consensus-spec payloads. Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
- Bound register_validator: per-route body limit + REGISTRATIONS_MAX_LEN element cap so a single caller cannot drive unbounded upstream fan-out. - Wrap the previously un-timed upstream calls (fetch_slots_config in submit_voluntary_exit, slot_from_timestamp in submit_registration) in UPSTREAM_REQUEST_TIMEOUT, matching the rest of the component. - Add HelperError::SlotComputation so slot arithmetic failures are no longer reported as "fetch slots config". - Go parity: voluntary-exit success logs at info!; JSON decode failures return "failed parsing json request body". - Remove Go cross-reference comments and now-stale #[allow(dead_code)] attributes; collapse verify_par_signed_proposal onto the shared verify_par_signed helper. - Tests: multi-registration batch (in-order success + halt-on-error); stronger before-genesis slot_from_timestamp assertion. Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
/loop-review-pr summaryRan 1 review-and-fix iteration against this PR (four parallel reviewers: functional-parity, security, rust-style, code-quality). Terminated by: completion_promise (PR ideal — no remaining bug/major findings, gates green). Quality gates (final)
(Note: Resolved during the loopMajor (4)
Minor (6)
Nits (1)
OutstandingNone at bug/major severity. A couple of nits were considered and intentionally not taken: replacing the VerdictPR is ideal — all bug/major findings resolved, gates green. |
Summary
Ports the validator-lifecycle validatorapi endpoints from Charon v1.7.1, wiring both the
Componenthandler logic and the axum router so the routes are reachable end-to-end (no moretodo!()/unimplemented!()for these endpoints).POST /eth/v1/beacon/pool/voluntary_exits→submit_voluntary_exitPOST /eth/v1/validator/register_validator→submit_validator_registrationsScope
Component (
crates/core/src/validatorapi/component.rs)submit_voluntary_exit: looks up the DV root pubkey from the active-validator cache ("validator not found"→ 400 on miss), builds anExitduty atslot = slots_per_epoch * exit.epoch, verifies the partial signature underDOMAIN_VOLUNTARY_EXIT(epoch =exit.message.epoch), and fans out the partial-signed data to subscribers.submit_validator_registrations: empty list → no-op; builder mode disabled → swallow; per registration, non-distributed-validator keys are swallowed, the timestamp maps to a slot viaslot_from_timestamp, aBuilderRegistrationduty is built, the partial signature is verified underDOMAIN_APPLICATION_BUILDER(epoch 0), and the result is broadcast.Router (
router.rs)submit_exit: JSON-only; SSZ/unrecognised content types → 415, empty body → 400, malformed JSON → 400.submit_validator_registrations: JSON array or bare SSZ concatenation (180-byte fixed objects); misaligned SSZ → 415, empty body → 400.Supporting
eth2util::helpers::slot_from_timestamp— genesis time + slot duration mapping with the pre-genesis current-time fallback.eth2apiv1: derivesszEncode/DecodeonValidatorRegistration/SignedValidatorRegistration.ssz_codec::decode_signed_validator_registrations— bare 180-byte array decode withinvalid buffer sizeguard.validatorapi::types: backSignedValidatorRegistration/SignedVoluntaryExitwith the real consensus-spec payloads (were empty placeholders).Go references (v1.7.1)
core/validatorapi/validatorapi.go:SubmitVoluntaryExit,SubmitValidatorRegistrations,submitRegistration,SlotFromTimestamp,verifyPartialSig.core/validatorapi/router.go:submitExit,submitValidatorRegistrations,unmarshal, content negotiation inwrap.core/validatorapi/eth2types.go:signedValidatorRegistrations(SSZ 180-byte element size).core/eth2signeddata.go: exit domainDOMAIN_VOLUNTARY_EXIT/ epoch = message epoch; registration domainDOMAIN_APPLICATION_BUILDER/ epoch 0.Test coverage
slots_per_epoch * epoch), unknown-validator 400, bad-signature reject; registration builder-disabled swallow, empty no-op, non-DV swallow, verify+broadcast (timestamp→slot), bad-signature reject.ssz_codec: registration array round-trip, misaligned-buffer reject, empty-is-empty.eth2util:slot_from_timestampnormal mapping + pre-genesis fallback.Gates
cargo +nightly fmt --all --check✅cargo clippy --workspace --all-targets --all-features -- -D warnings✅cargo test --workspace✅ (1612 passed, 0 failed)cargo deny check✅Note:
cargo test --all-featuresis Docker-gated in this sandbox (eth2apiintegrationfeature spins a Lighthouse testcontainer that never readies). The functional gate iscargo test --workspace(default features);--all-featureswas confirmed to compile and is clippy-clean.Stacks on PR #488 (PR 1 — proxy + proposal/validators wiring).
🤖 Generated with Claude Code