Skip to content

feat(core): validatorapi voluntary exit + validator registration handlers#491

Open
varex83agent wants to merge 2 commits into
bohdan/validatorapi-pr1-proxy-wiringfrom
bohdan/validatorapi-pr4-validator-lifecycle
Open

feat(core): validatorapi voluntary exit + validator registration handlers#491
varex83agent wants to merge 2 commits into
bohdan/validatorapi-pr1-proxy-wiringfrom
bohdan/validatorapi-pr4-validator-lifecycle

Conversation

@varex83agent

Copy link
Copy Markdown
Collaborator

Summary

Ports the validator-lifecycle validatorapi endpoints from Charon v1.7.1, wiring both the Component handler logic and the axum router so the routes are reachable end-to-end (no more todo!()/unimplemented!() for these endpoints).

  • POST /eth/v1/beacon/pool/voluntary_exitssubmit_voluntary_exit
  • POST /eth/v1/validator/register_validatorsubmit_validator_registrations

Scope

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 an Exit duty at slot = slots_per_epoch * exit.epoch, verifies the partial signature under DOMAIN_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 via slot_from_timestamp, a BuilderRegistration duty is built, the partial signature is verified under DOMAIN_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.
  • eth2api v1: derive ssz Encode/Decode on ValidatorRegistration / SignedValidatorRegistration.
  • ssz_codec::decode_signed_validator_registrations — bare 180-byte array decode with invalid buffer size guard.
  • validatorapi::types: back SignedValidatorRegistration / SignedVoluntaryExit with 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 in wrap.
  • core/validatorapi/eth2types.go: signedValidatorRegistrations (SSZ 180-byte element size).
  • core/eth2signeddata.go: exit domain DOMAIN_VOLUNTARY_EXIT / epoch = message epoch; registration domain DOMAIN_APPLICATION_BUILDER / epoch 0.

Test coverage

  • Component: exit verify+broadcast (slot = 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.
  • Router: registration JSON array, registration SSZ array (360 bytes), misaligned SSZ → 415, empty body → 400; exit JSON happy path, SSZ content type → 415, empty body → 400, invalid JSON → 400.
  • ssz_codec: registration array round-trip, misaligned-buffer reject, empty-is-empty.
  • eth2util: slot_from_timestamp normal 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-features is Docker-gated in this sandbox (eth2api integration feature spins a Lighthouse testcontainer that never readies). The functional gate is cargo test --workspace (default features); --all-features was confirmed to compile and is clippy-clean.

Stacks on PR #488 (PR 1 — proxy + proposal/validators wiring).

🤖 Generated with Claude Code

varex83agent and others added 2 commits June 17, 2026 14:03
…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>
@varex83agent

Copy link
Copy Markdown
Collaborator Author

/loop-review-pr summary

Ran 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)

  • cargo +nightly fmt --all --check — pass
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — pass
  • cargo test --workspace — pass (1614 passed, 0 failed)
  • cargo deny check — pass

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

Resolved during the loop

Major (4)

  • register_validator had no body/element cap → unbounded per-request upstream fan-out — added a per-route DefaultBodyLimit plus a REGISTRATIONS_MAX_LEN count cap — crates/core/src/validatorapi/router.rs
  • submit_voluntary_exit issued an un-timed fetch_slots_config — wrapped in UPSTREAM_REQUEST_TIMEOUTcrates/core/src/validatorapi/component.rs
  • submit_registration issued un-timed slot_from_timestamp upstream calls — wrapped in UPSTREAM_REQUEST_TIMEOUTcrates/core/src/validatorapi/component.rs
  • HelperError::FetchSlotsConfig was reused for slot arithmetic failures — added a dedicated HelperError::SlotComputation variant — crates/eth2util/src/helpers.rs

Minor (6)

  • Go parity: voluntary-exit success now logs at info! (was debug!)
  • Go parity: JSON decode failures now return "failed parsing json request body" (exit + registrations)
  • Removed Go cross-reference comments from the new router doc/inline comments
  • Removed now-stale #[allow(dead_code)] on verify_partial_sig and TestValidatorCache::arc
  • Collapsed verify_par_signed_proposal onto the shared verify_par_signed helper (dedupe)
  • Added multi-registration batch tests (in-order success + halt-on-error)

Nits (1)

  • Strengthened the slot_from_timestamp before-genesis test assertion to check the fallback approximates the current slot

Outstanding

None at bug/major severity. A couple of nits were considered and intentionally not taken: replacing the SIGNED_VALIDATOR_REGISTRATION_SSZ_SIZE = 180 constant with a compile-time ssz_fixed_len() assertion (kept the documented constant + the round-trip test that already asserts 180), and unifying the two fetch_slots_config call sites (they serve different purposes).

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