Skip to content

feat(core): wire up validatorapi Component subscribe/await/verify hooks#458

Merged
varex83 merged 6 commits into
mainfrom
bohdan/validatorapi-plumbing
Jun 3, 2026
Merged

feat(core): wire up validatorapi Component subscribe/await/verify hooks#458
varex83 merged 6 commits into
mainfrom
bohdan/validatorapi-plumbing

Conversation

@varex83agent
Copy link
Copy Markdown
Collaborator

Summary

  • Adds the validatorapi Component plumbing every subsequent submit/await handler needs. No unimplemented!() arms are filled in — this PR only exposes the hooks. Mirrors Charon's core/validatorapi/validatorapi.go:196-256 and :1352.
  • New Component callback fields (six total — await_proposal_fn, await_agg_attestation_fn, await_sync_contribution_fn, await_agg_sig_db_fn, duty_def_fn, pub_key_by_att_fn) plus a subs: Vec<SubscriberFn> subscriber list. All stored as Option<Arc<…>> so registration happens before the Component is shared.
  • subscribe() wraps the user closure with a set-clone step so each subscriber receives its own ParSignedDataSet, mirroring Go's clone-before-fanout at validatorapi.go:249-256.
  • Six register_* methods overwrite on re-register, matching Go's single-function input semantics.
  • verify_partial_sig() honours insecure_test, looks up this node's public share via the existing pub_share_by_pubkey map, then delegates to pluto_eth2util::signing::verify. The Rust hook takes (domain_name, epoch, message_root, signature) directly rather than projecting them through core.Eth2SignedData — the trait hasn't been ported yet, so submit handlers in PRs 3-6 will derive the triple from their concrete signed-data wrapper.

Go reference

Go (charon/core/validatorapi/validatorapi.go) Rust (crates/core/src/validatorapi/component.rs)
subs []func(...) error field, Subscribe :249-259 subs: Vec<SubscriberFn>, Component::subscribe
awaitProposalFunc field, RegisterAwaitProposal :208 await_proposal_fn, Component::register_await_proposal
awaitAggAttFunc field, RegisterAwaitAggAttestation :238 await_agg_attestation_fn, Component::register_await_agg_attestation
awaitSyncContributionFunc field, RegisterAwaitSyncContribution :220 await_sync_contribution_fn, Component::register_await_sync_contribution
awaitAggSigDBFunc field, RegisterAwaitAggSigDB :243 await_agg_sig_db_fn, Component::register_await_agg_sig_db
dutyDefFunc field, RegisterGetDutyDefinition :232 duty_def_fn, Component::register_get_duty_definition
pubKeyByAttFunc field, RegisterPubKeyByAttestation :226 pub_key_by_att_fn, Component::register_pub_key_by_attestation
verifyPartialSig :1352 Component::verify_partial_sig

Out of scope

  • The Eth2SignedData trait + core.VerifyEth2SignedData are not ported here. verify_partial_sig instead takes the BLS domain/epoch/message-root directly. Submit handlers (PRs 3-6) compute those from their concrete signed-data wrapper before calling the hook.
  • Handler trait signatures are unchanged. No other crates touched.
  • No cluster accessor added — the existing pub_share_by_pubkey: HashMap<BLSPubKey, BLSPubKey> on Component already provides the DV-root → public-share lookup that Go's getVerifyShareFunc exposes, so no crates/cluster/ change was needed.

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 — 374/374 passing (8 new tests: subscribe fanout-clones, register_await_proposal overwrite, the remaining five register hooks overwrite, unregistered hooks default to None, verify_partial_sig accept/reject valid+tampered, reject unknown pubkey, short-circuit in insecure_test)

Base automatically changed from bohdan/validatorapi-5 to main June 2, 2026 12:56
varex83agent and others added 5 commits June 2, 2026 15:24
Adds the plumbing every subsequent submit/await handler needs without
implementing any of the unimplemented!() arms. Mirrors Charon's
core/validatorapi/validatorapi.go:196-256 (subscriber list + six
Register* hooks) plus :1352 (verifyPartialSig).

- New Component fields: subs, await_proposal_fn, await_agg_attestation_fn,
  await_sync_contribution_fn, await_agg_sig_db_fn, duty_def_fn,
  pub_key_by_att_fn. All Option<Arc<…>> so registration before the
  Component is shared in an Arc, then read-only thereafter.
- subscribe() wraps the user closure with a set-clone step so each
  subscriber receives its own ParSignedDataSet — matches Go's
  Subscribe clone-before-fanout at validatorapi.go:249-256.
- register_* methods replace any prior registration, matching Go's
  single-function input semantics.
- verify_partial_sig() honours insecure_test, looks up this node's
  public share from pub_share_by_pubkey, then delegates to
  pluto_eth2util::signing::verify. Unlike Go — which projects domain /
  epoch / message-root through the core.Eth2SignedData interface — the
  Rust hook takes those three values directly so we don't have to port
  the Eth2SignedData trait in this plumbing PR; submit handlers in PRs
  3-6 will derive the triple from their concrete signed-data wrapper.

Tests: subscribe fanout clones per subscriber; the six register hooks
all overwrite on re-register; unregistered hooks default to None;
verify_partial_sig accepts a real BLS signature, rejects a tampered
one, rejects an unknown DV pubkey, and short-circuits in insecure_test
mode.

Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
SubscriberFn took ParSignedDataSet by value, so the wrapper closure had
to call set.clone() before invoking the user fn even though the caller
had already cloned to produce the owned argument. The fanout loop that
future submit handlers will run (`for sub in &subs { sub(&duty,
set.clone()).await? }`) would thus pay two HashMap-backed clones per
subscriber where Charon's `Subscribe` (validatorapi.go:249-256) pays one.

Take the set by reference: the wrapper now performs the sole clone
before handing the owned copy to the user closure. Fanout sites pass
`&set` to every subscriber, matching Go's per-subscriber cost.

The existing subscribe_fanouts_clones_to_every_subscriber test (which
exercises the clone-isolation invariant via a mutating first subscriber
and a pristine-set assertion on the second) is updated to the new
call-by-reference pattern and still passes — confirming each subscriber
continues to observe its own copy.
…ck aliases

The validatorapi Component callback type aliases (SubscriberFn,
AwaitProposalFn, AwaitAggAttestationFn, AwaitSyncContributionFn,
AwaitAggSigDBFn, DutyDefFn, PubKeyByAttFn) used a locally-defined
`CallbackFuture<'a, T> = Pin<Box<dyn Future<...>>>` alias instead of the
workspace-standard `futures::future::BoxFuture`. The codebase already
uses BoxFuture in pluto-core (consensus/qbft/transport.rs) and in
pluto-dkg / pluto-peerinfo for the same shape.

Replace inline with BoxFuture, drop the local CallbackFuture alias, and
drop the now-unused `pin::Pin` import. No behaviour change.
Rust naming convention (RFC 430) treats acronyms in PascalCase as single
capitalized words: `Db`, not `DB`. The companion error variant in this
crate already uses `DutyDbError` (dutydb::Error). `BLSPubKey` is an
unrelated upstream type from `pluto_eth2api` and stays as-is.
The tests module had a second `use` block in the middle of the file
(line ~1259, just before the plumbing-tests section) for std::sync::Mutex,
pluto_crypto/testutil/serde_json, and a re-import of SignedRandao /
SyncContribution / VersionedAggregatedAttestation / PubKey from `crate`.
Rust style places all `use` declarations at the top of each module.

Fold the mid-block imports into the existing top-of-module `use` block.
Drop the now-redundant `pluto_eth2util::signing::DomainName` test-mod
import — the parent module already brings it in via `use super::*`.
@varex83agent varex83agent force-pushed the bohdan/validatorapi-plumbing branch from f5c3b49 to 8eedb3f Compare June 2, 2026 14:09
@varex83agent
Copy link
Copy Markdown
Collaborator Author

/loop-review-pr summary

Ran 2 review-and-fix iteration(s) against this PR.
Terminated by: completion_promise.

Pre-loop housekeeping

The branch was rebased onto origin/main via git rebase --onto origin/main 5da9093 bohdan/validatorapi-plumbing. Reason: PR #451 was squash-merged into main as e75a114, so the 5 ancestor commits this branch carried (5da9093, 26675fe, 11ad940, 6fe5785, 0767f40) were already in main and a plain git rebase origin/main produced spurious conflicts against the squashed version. After the --onto rebase the PR diff is a focused +627 / -4 in crates/core/src/validatorapi/component.rs.

Quality gates (final)

  • cargo +nightly fmt --all --check — pass
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — pass
  • cargo test --workspace --all-features — pass (pluto-core 413 unit tests, exit 0)

Resolved during the loop

Major (1)

  • subscribe wrapper double-clones ParSignedDataSetcrates/core/src/validatorapi/component.rs:55 — fix in 8bfd2be. SubscriberFn now takes &ParSignedDataSet; the wrapper is the sole clone site, mirroring Go's Subscribe clone-before-fanout cost of one clone per subscriber.

Minor (3)

  • Reinvented Pin<Box<dyn Future>> alias — crates/core/src/validatorapi/component.rs:50 — fix in 9b3040c. Dropped the local CallbackFuture alias and adopted futures::future::BoxFuture across all 7 callback type aliases, matching the convention already used in consensus/qbft/transport.rs, DKG, and peerinfo.
  • AwaitAggSigDBFn naming — crates/core/src/validatorapi/component.rs:81 — fix in 0195b05. Renamed to AwaitAggSigDbFn per Rust acronym casing (RFC 430). The neighbouring DutyDbError already follows this convention; BLSPubKey is unrelated upstream and stays as-is.
  • Mid-module use statements in test module — crates/core/src/validatorapi/component.rs:1259 — fix in 8eedb3f. Hoisted the second use block at the plumbing-tests boundary up to the existing top-of-module block and dropped the redundant DomainName re-import (already in scope via super::*).

Deliberately deferred (documented in source / state file)

  • No subscriber-fanout helper yet — submit handlers don't exist in this PR; the helper lands with the first consumer PR alongside the loop's abort-on-error semantics.
  • DutyDefFn returns Box<dyn Any + Send + Sync> — already documented in source as deliberate parity with Go's untyped core.DutyDefinitionSet; revisit when the first downcast lands.
  • Callbacks lack a CancellationToken parameter — future submit handlers will wrap each callsite in tokio::time::timeout, matching the existing attestation_data pattern.
  • verify_partial_sig takes (domain_name, epoch, message_root, signature) directly instead of projecting through an Eth2SignedData trait — documented in source; tightens when the trait lands.

Outstanding

None — internal review pipeline (pluto-review / security / rust-style / code-quality, four agents in parallel) returned zero bug and zero major findings on iteration 2 after the iter-1 fixes landed.

Verdict

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

Removes the "Mirrors Go's foo" / `validatorapi.go:N` / `router.go:N`
pointers from doc comments throughout component.rs. The descriptions of
what each callback / method / error variant does remain; only the
Go-source references are dropped.

Internal cross-references (e.g. `upstream_status_error`) and the
test-fixture spec note remain — those don't point at the Charon Go
codebase.
Copy link
Copy Markdown
Collaborator

@emlautarom1 emlautarom1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond verify_partial_sig everything is plumbing so it's hard to get the full picture (and thus deciding what makes sense/what not).

I particularly do not like the callback mechanism where we have non trivial type signatures and we store them into structs. Once we start with the wiring I think thinks will get clearer.

@therustmonk wdyt about this? Any pattern you suggest?

/// [`ParSignedDataSet`] by reference; the registered wrapper clones the
/// set exactly once before invoking the user closure so every subscriber
/// observes an independent copy.
pub type SubscriberFn = Arc<
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Callbacks don't need to be Arc, Box should suffice.

/// `Subscribe` invokes every registered subscriber, each receiving its
/// own clone of the set. Mutating one clone does not affect the others.
#[tokio::test]
async fn subscribe_fanouts_clones_to_every_subscriber() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need these kind of test: subscribers get a ParSignedDataSet (which we could make a type alias instead of a separate type), so it's impossible for subscribers to alter each other from the type system perspective.

@varex83 varex83 merged commit 0f3cf70 into main Jun 3, 2026
18 checks passed
@varex83 varex83 deleted the bohdan/validatorapi-plumbing branch June 3, 2026 20:02
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.

3 participants