Skip to content

feat(app): implement simnet integration#540

Draft
iamquang95 wants to merge 32 commits into
mainfrom
feat/simnet-run
Draft

feat(app): implement simnet integration#540
iamquang95 wants to merge 32 commits into
mainfrom
feat/simnet-run

Conversation

@iamquang95

@iamquang95 iamquang95 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Wires in-process simnet support into pluto run (--simnet-beacon-mock /
--simnet-validator-mock), so a full all-pluto cluster runs the complete duty
pipeline — schedule → QBFT consensus → threshold sign → broadcast — against mock
beacon/validator clients, with no real beacon node or VC. Mirrors Charon's
app.Run simnet wiring; this is what the compose smoke harness
(TestSmoke/all_pluto) drives.

Changes

  • Start the ported BeaconMock + validatormock inside run: point both beacon clients at the in-process mock, load share keystores for the validator mock, and drive it from the scheduler via a new abstract SlotTickFn seam (keeps wire.rs decoupled from the mock type).
  • --p2p-relays="" now means "no relays" (Charon parity) — enables isolated/offline runs.
  • Beacon-mock endpoint coverage for the whole flow: POST validators, block produce/publish, sync-committee duties/contribution, and attestation / sync-committee pool submits; the validator mock now sends wire-format SingleAttestation and dials loopback (not the 0.0.0.0 bind address).
  • Log-level parity with Charon so the smoke alert gate can pass: quiet simnet-only noise (libp2p relay churn, force_direct, weak-KDF keystores, expired-duty QBFT drops).

Testing

  • cargo +nightly fmt --all --check and clippy --workspace --all-targets --all-features -D warnings clean; workspace tests green.
  • New in-repo tests: single-node boot, validator resolution, produce-block, attestation-shape, and the --simnet-* bridge.
  • End-to-end duty flow (4 nodes, threshold 3) exercised via the compose smoke harness.

emlautarom1 and others added 30 commits July 7, 2026 17:18
Construct and connect the ten core duty-workflow components (scheduler,
fetcher, consensus, dutydb, validatorapi, parsigdb, parsigex, sigagg,
aggsigdb, broadcaster) into a runnable node — the Rust analog of Charon's
app.go Run/wireCoreWorkflow + core.Wire.

- node/behaviour.rs: compose parsigex + qbft + peerinfo into CoreBehaviour
  and build the libp2p Node (modeled on dkg/node.rs).
- node/wire.rs: construct and stitch the components, including the three
  deadlock-critical fetcher back-edges (agg_sig_db -> aggsigdb.wait_for,
  await_att_data -> dutydb.await_attestation, plus vapi -> aggsigdb); the
  parsigex exchange is an injectable seam (Charon's TestConfig.ParSigExFunc
  analog) so the wiring is testable without a real swarm.
- node/mod.rs: App::run with idiomatic-tokio lifecycle (JoinSet + ordered
  shutdown), cluster-lock loading, and the qbft broadcaster<->behaviour
  construction cycle resolved via OnceLock.
- node/config.rs: minimal AppConfig (subset of Charon app.Config).
- core/fetcher: re-export GraffitiBuilder so out-of-crate wiring can supply
  the required builder field.
- tests/wiring.rs: Tier 1 single-node wiring test exercising the fetcher
  back-edges (blocks-then-unblocks deadlock guard) and the full sign path
  (real-BLS threshold aggregation -> broadcaster -> beacon mock).

Minimal-runnable scope (issue #402 part A). Part-B items
(tracker/priority/infosync/monitoring/sse/recaster/vmock, eth2-based
verifiers, full proto encoders, real deadliners/gater) are marked with
inline TODO(#402 part B).

Refs: #402
- Unnecessary struct
- Rename vars
- Change visibility
- Do not import functions
- Extract common code
Adapt the core duty-workflow wiring to the pluto-core / pluto-consensus /
pluto-featureset APIs on main:

- SigAgg: the aggregate verifier is now an injectable `WireInputs.sigagg_verifier`
  (`sigagg::VerifyFn`). Production wires the eth2 verifier
  (`sigagg::new_verifier(eth2_cl)`); the Tier-1 wiring test injects a permissive
  verifier so it keeps proving the sign-path connectivity (real eth2 verification
  is exercised in part B). Mirrors Charon's `TestConfig` injection.
- Consensus: thread an `Arc<FeatureSet>` into `qbft::Config` and
  `timer::get_round_timer_func` (featureset dropped its global state); a default
  set is used for now, sourced from CLI config in part B.
- Cargo: add the `pluto-featureset` dependency; drop a duplicate `pluto-p2p` key.
- Allow `clippy::too_many_arguments` on `wire_p2p` until its inputs are grouped in
  part B (relay/priority additions).
Resolve the beacon-derived duty-workflow inputs in `run` and pass them into
`wire_core_workflow` via `WireInputs`, matching Charon `app.go:540-556`:

- Real duty gater `DutyGater::new(&eth2_cl)` (validates duties against the beacon
  chain) replaces the accept-any-valid-type stub.
- Beacon-derived `DutyDeadlineCalculator::from_client` backs every component's
  deadliner, shared as an `Arc<dyn DeadlineCalculator>` (its `deadline` method is
  sync, and `Arc<dyn _>` satisfies the trait, so one instance backs all four).
- Per-validator `GraffitiBuilder::new` from the new `AppConfig::graffiti` /
  `graffiti_disable_client_append` fields.
- `electra_slot = fork_config[Electra].epoch * slots_per_epoch`, and
  `fetch_only_comm_idx0` / `compare_attestations` read from the `AppConfig`
  feature set (`FetchOnlyCommIdx0` / `ChainSplitHalt`).

These inputs are injected via `WireInputs` so the Tier-1 test keeps its inert
defaults (never-expiring deadlines, default graffiti) and never needs the extra
beacon endpoints. `AppConfig` gains `graffiti`, `graffiti_disable_client_append`
and `feature_set`.
Wire the validator API callbacks that have no dutydb fallback (Charon
`core.Wire`):

- `register_await_agg_attestation` / `register_await_sync_contribution` /
  `register_pub_key_by_attestation` -> the local dutydb (wrapping its results in
  the `VersionedAggregatedAttestation` / `SyncContribution` newtypes).
- `register_get_duty_definition` -> the scheduler handle (type-erased across the
  callback boundary, downcast to `DutyDefinitionSet` by the component).

To supply the scheduler handle, the scheduler is now built before the validator
API (its handle is `Clone`); `await_proposal` continues to rely on the
component's built-in dutydb fallback.
Wire `parsigex::new_eth2_verifier` into the P2P behaviour, replacing the
always-accept stub. Inbound partial signatures are now verified against the
sender's public share for the duty (Charon `parsigex.NewEth2Verifier`).

`wire_p2p` takes the beacon client and a full public-share map
(`DV pubkey -> 1-based share index -> public share`), built by the new
`build_pub_shares_by_key` from every distributed validator's `pub_shares` (Part A
only extracted this node's own share).
Verify the cluster lock's hashes and signatures during `run`, gated on
`config.no_verify` (Charon `app.Run`):

- `lock.verify_hashes()` checks the lock/definition hashes.
- `lock.verify_signatures(&eth1)` checks the BLS aggregate, node and operator
  signatures. Operator-signature verification uses an execution-layer client
  built from the new `AppConfig::eth1_endpoint`; when unset, a no-op eth1 client
  is used (EIP-1271 smart-contract operator signatures are then not checked).

Adds the `pluto-eth1wrap` dependency. Manifest-file precedence is left as a TODO
until pluto-cluster gains a manifest loader.
…ODOs

The broadcaster already uses a separate submission client with the distinct
`beacon_node_submit_timeout`, so drop the stale TODO that implied otherwise.

Sharpen the two remaining TODOs to name the core-crate work they require, since
neither can be wired against the current APIs:

- Multi-`beacon_node_addrs` failover needs a multi-endpoint client in
  pluto-eth2api (`EthBeaconNodeApiClient` is single-endpoint).
- Honoring the target gas limit needs a target-gas-limit parameter on
  `validatorapi::Component::new` (Charon passes it to `NewComponent`).
Replace the local attester-only proposal encoder with
`pluto_core::unsigneddata::unsigned_data_set_to_proto` (all four duty types,
SSZ-canonical, from #508). Together with the matching all-duty-type decode on the
dutydb subscriber, proposer / sync-committee / aggregator duties now flow through
`consensus.propose` end to end, not just attester duties.
…anups

Complete the runnable-node wiring (#402 Part A):

- Wire `privkeylock::Service` into `run`/`run_lifecycle`, gated on the new
  `AppConfig::priv_key_locking` flag: the run loop is spawned as a lifecycle
  task and `close()` is invoked at the start of shutdown so the
  `<priv_key_file>.lock` sentinel is removed before the task drain. Mirrors
  Charon `app.go:161`.

- Extend the Tier-1 wiring test with the proposer and sync-committee sign paths
  (ParSigDB -> threshold -> SigAgg -> broadcaster -> beacon submit) and a
  negative test proving the real eth2 SigAgg verifier rejects a bad partial
  signature before broadcast — previously only the attester path was covered.

- Error on an invalid/empty beacon-node URL instead of silently defaulting to
  `http://127.0.0.1:5052`.

- Drop the dead `duty_gater` parameter from `wire_core_workflow`; the gater is
  still used by `wire_p2p` and consensus, just not the in-process core wiring.
- Remove invalid tasks
wire_core_workflow now builds one pubkey-scoped ValidatorCache and seeds
it into the scheduler's beacon client and the submission client, reusing
the same instance for the validator API (Charon app.go:481-482, 598).
Previously both clients kept their default empty-pubkey cache, so a real
run resolved duties against an empty (or unfiltered) validator set and
scheduled nothing. The new wiring test serves POST /states/head/validators
filtered by the request-body ids, which fails against an unseeded cache.
…aron

- verify_lock now always runs hash + signature verification and only
  downgrades failures to warnings under no_verify, matching post-#4130
  Charon (cluster/load.go) instead of skipping verification entirely.
- parsigex broadcast failures surface as AppError::ParSigEx instead of
  being mislabeled as beacon-client errors.
- drop the duplicate verify_p2p_key call in wire_p2p; run verifies once,
  matching Charon's single VerifyP2PKey in Run.
Mirrors Charon v1.7.1 wireP2P (app.go:341-400): CoreBehaviour gains the
relay client transport, RelayManager (reservation lifecycle + relay-circuit
peer routing, Charon's NewRelayReserver + NewRelayRouter) and
ForceDirectBehaviour (Charon's ForceDirectConnections). Relay endpoints
resolve in the background via bootnode::new_relays with the lock-hash-hex
Charon-Cluster header, and the connection gater now admits the resolved
relays alongside cluster peers. Without this the node was listen-only:
lock ENRs carry no addresses, so relay routing is the only peer-discovery
path.

The multiaddr->URL resolution helper moves from pluto-dkg into
pluto-p2p::bootnode (with a unit test) so the app and DKG share it.
Post-#4130's Cluster-Uuid relay header remains a follow-up.
Replaces the inline load_lock/verify_lock/verify_lock_signatures trio
with cluster::load::load_cluster_lock (#523), which mirrors Charon's
cluster.LoadClusterLock (post-#4130): read + JSON-decode the lock, then
verify hashes and signatures, downgrading failures to warnings under
no_verify. The execution-layer client is now built once in run, matching
Charon's app.Run ordering (app.go:177-178).
Replaces the unimplemented run_workflow stub with a RunConfig ->
AppConfig mapping and App::new(config).run(ct). Correctness-affecting
flags with no implementation yet (simnet mocks, custom testnets,
beacon-node headers, VC TLS, consensus protocol preference, synthetic
proposals, p2p fuzz) fail fast with a 'not yet supported' error;
observability/availability-only flags (monitoring/debug addresses,
OTLP, proc directory, fallback beacon endpoints) are ignored with a
warning. An existing cluster manifest file is warned about and ignored,
matching post-#4130 lock-only semantics. Feature flags resolve like
Charon's featureset.Init: unknown min status is a hard error, unknown
feature names are warned and skipped.
Replaces the stub-panics test with coverage of the bridge seam: field
mapping (incl. hostname validator-api addresses and empty-to-None
graffiti/eth1), fail-fast rejection of each unsupported flag group,
warn-and-continue for observability flags and existing manifests,
Charon-verbatim featureset errors, and an end-to-end run() that reaches
App::run and fails cleanly on a missing cluster lock.
run_lifecycle logged a validator-API bind or serve failure and returned
Ok(()), so the process exited 0 with the node silently degraded. Charon
fails the run instead: any lifecycle start-hook error triggers shutdown
and is returned from Manager.Run, and httpServeHook swallows only
http.ErrServerClosed (graceful shutdown).

The validator API server now runs as a fallible JoinSet task via
serve_validator_api (graceful shutdown stays a clean exit), and the
supervisor propagates the first failed task's error - constructing the
previously-unused AppError::ValidatorApi variant.
@iamquang95
iamquang95 force-pushed the feat/monitoring-api branch from f34f635 to be38ac3 Compare July 16, 2026 04:23
Base automatically changed from feat/monitoring-api to main July 16, 2026 04:30
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