refactor(eth2api): make ValidatorCache immutable in BeaconNodeClient - #667
Conversation
The validator set is known at node construction (from the cluster validators), so the cache no longer needs to be optional, mutable, or shared behind a lock. Build a single `ValidatorCache` in `node::run` and thread it into both the scheduler and submission `BeaconNodeClient`s at construction. - `BeaconNodeClient::new` now takes the `ValidatorCache` and stores it as a plain field (the type is already `Arc`-backed, so clones share state). - Remove the `Arc<RwLock<Option<..>>>` wrapping, the `set_validator_cache` setter, and the `NoActiveValidatorCache` error variant. - `validator_cache()` returns `&ValidatorCache`; drops the `.expect`/TODO at the scheduler read site. Closes #482 Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com>
…ore_workflow WireInputs takes the two beacon API clients. Wiring derives the cache from the cluster validators once and constructs both BeaconNodeClients, the per-epoch refresher and the validator API from it. The wiring test asserts the scheduler's first validators request carries the cluster pubkeys.
emlautarom1
left a comment
There was a problem hiding this comment.
LGTM, injecting at construction is always better than having some setter.
I pushed one extra change that makes the validator cache shared by construction instead of by convention: run is private and has no test, and wiring_seeds_shared_validator_cache sets up the clients in its own helper, so it passes even when wire_core_workflow is never called. This is solved by making wire_core_workflow build a single cache from validators and construct the clients, the refresher and the validator API from it. I also rewrote the test as wiring_seeds_validator_cache to check this behavior.
| // One pubkey-scoped validator cache shared by the scheduler's beacon | ||
| // client, the submission client, and the validator API, so every consumer | ||
| // resolves the same cluster validator set. `ValidatorCache` is `Arc`-backed, | ||
| // so the clones seeded into each client (and the one wired into the | ||
| // per-epoch refresh subscriber in `wire_core_workflow`) share state, letting | ||
| // a single refresh update every consumer at once. |
There was a problem hiding this comment.
Keep it short:
| // One pubkey-scoped validator cache shared by the scheduler's beacon | |
| // client, the submission client, and the validator API, so every consumer | |
| // resolves the same cluster validator set. `ValidatorCache` is `Arc`-backed, | |
| // so the clones seeded into each client (and the one wired into the | |
| // per-epoch refresh subscriber in `wire_core_workflow`) share state, letting | |
| // a single refresh update every consumer at once. | |
| // One pubkey-scoped validator cache shared by the scheduler's beacon | |
| // client, the submission client, and the validator API, so every consumer | |
| // resolves the same cluster validator set. |
| /// Pubkey-scoped validator cache shared by the beacon/submission clients | ||
| /// and the validator API. A clone of the same `Arc`-backed cache seeded | ||
| /// into those clients, so the per-epoch trim + refresh subscriber wired | ||
| /// below refreshes every consumer at once. |
There was a problem hiding this comment.
Remove this comment:
| /// Pubkey-scoped validator cache shared by the beacon/submission clients | |
| /// and the validator API. A clone of the same `Arc`-backed cache seeded | |
| /// into those clients, so the per-epoch trim + refresh subscriber wired | |
| /// below refreshes every consumer at once. |
| // The pubkey-scoped validator cache is built and seeded into the | ||
| // beacon/submission clients at construction (in `node::run`), and passed in | ||
| // here so the per-epoch trim + refresh subscriber registered below (and the | ||
| // validator API) share the same `Arc`-backed state. |
There was a problem hiding this comment.
Remove this comment:
| // The pubkey-scoped validator cache is built and seeded into the | |
| // beacon/submission clients at construction (in `node::run`), and passed in | |
| // here so the per-epoch trim + refresh subscriber registered below (and the | |
| // validator API) share the same `Arc`-backed state. |
# Conflicts: # Cargo.lock # crates/app/src/node/wire.rs
Summary
BeaconNodeClientheld itsValidatorCacheasArc<RwLock<Option<ValidatorCache>>>, initialized toNoneand populated later viaset_validator_cache, so every read had to handle a "not yet set" case that leaked into callers (the scheduler's.expect("validator cache is available")).The validator set is actually known at node construction (from the cluster's validators), so this optionality was never needed. This PR builds a single
ValidatorCacheonce innode::runand threads it into both the scheduler and submissionBeaconNodeClients at construction.Changes
BeaconNodeClient::newnow takes theValidatorCacheand stores it as a plain field (ValidatorCacheis alreadyArc-backed, so clones seeded into each client share state — a single per-epoch refresh still updates every consumer).Arc<RwLock<Option<..>>>wrapping, theset_validator_cachesetter, and theNoActiveValidatorCacheerror variant.validator_cache()returns&ValidatorCache; dropped the.expect/TODO at the scheduler read site.node::run/wire_core_workflow) to construct-and-seed the shared cache up front instead ofset_validator_cacheafter the fact, and all tests to the new constructor.Testing
cargo +nightly fmt --all --checkcargo clippy --workspace --all-targets --all-features -- -D warnings— cleancargo test -p pluto-eth2api -p pluto-core -p pluto-app— all pass (eth2api 133, core 605, app 141)Closes #482
🤖 Generated with Claude Code