feat(client): validator key consistency check once per epoch#15417
Conversation
There was a problem hiding this comment.
Pull request overview
Adds validator key consistency validation at multiple lifecycle points (key file load, client startup, and epoch boundaries) to catch misconfigured validator keys early, with accompanying test-loop coverage.
Changes:
- Validate
KeyFile::from_fileby ensuringsecret_keyderives the declaredpublic_key(error on mismatch). - Add
Client::check_validator_key_for_epochand invoke it on startup and at epoch boundaries (panic on mismatch for actionable failure). - Extend test-loop setup to allow injecting a custom
validator_signer, and add new tests for mismatch scenarios.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
core/crypto/src/key_file.rs |
Adds key-file self-consistency check + unit test for mismatched keys. |
chain/client/src/client.rs |
Introduces epoch-based validator key mismatch check and hooks it at epoch boundary. |
chain/client/src/client_actor.rs |
Runs validator key check on startup for validator nodes. |
test-loop-tests/src/setup/state.rs |
Adds injectable validator_signer to NodeSetupState. |
test-loop-tests/src/setup/setup.rs |
Uses injected signer (or default) when constructing the client’s validator signer. |
test-loop-tests/src/setup/env.rs |
Initializes new NodeSetupState.validator_signer field on kill/restart paths. |
test-loop-tests/src/setup/builder.rs |
Initializes new NodeSetupState.validator_signer field in builder. |
test-loop-tests/src/tests/validator_key_check.rs |
Adds test-loop tests for startup and epoch-boundary mismatch detection. |
test-loop-tests/src/tests/mod.rs |
Registers the new validator key check tests module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let Ok(next_epoch_id) = | ||
| self.epoch_manager.get_epoch_id_from_prev_block(&block_hash) | ||
| { | ||
| self.check_validator_key_for_epoch(&next_epoch_id); |
There was a problem hiding this comment.
get_epoch_id_from_prev_block errors are silently ignored here, which can cause the validator key check to be skipped at epoch boundaries (reducing the effectiveness of this safety check) and makes diagnosing failures harder. Please at least log/warn on the error (similar to the shard layout error handling below), or propagate it if this condition should be impossible at this point.
| if let Ok(next_epoch_id) = | |
| self.epoch_manager.get_epoch_id_from_prev_block(&block_hash) | |
| { | |
| self.check_validator_key_for_epoch(&next_epoch_id); | |
| match self.epoch_manager.get_epoch_id_from_prev_block(&block_hash) { | |
| Ok(next_epoch_id) => { | |
| self.check_validator_key_for_epoch(&next_epoch_id); | |
| } | |
| Err(err) => { | |
| tracing::warn!(target: "client", ?err, "failed to get epoch id from previous block; skipping validator key check for upcoming epoch"); | |
| } |
There was a problem hiding this comment.
Seems not worth it to me, can add it if reviewer prefers to have it.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #15417 +/- ##
==========================================
+ Coverage 69.03% 69.28% +0.24%
==========================================
Files 934 938 +4
Lines 210089 211122 +1033
Branches 210089 211122 +1033
==========================================
+ Hits 145034 146266 +1232
+ Misses 59056 58819 -237
- Partials 5999 6037 +38
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
wacban
left a comment
There was a problem hiding this comment.
nice, thanks for adding this!
| let epoch_id = client.chain.head()?.epoch_id; | ||
| client.check_validator_key_for_epoch(&epoch_id); |
There was a problem hiding this comment.
Is there a case where the node is behind and wrong epoch is used for the check?
There was a problem hiding this comment.
Great point. Let me figure it out before proceeding.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
only set last_validator_key_check_epoch after get_epoch_info succeeds, so a transient store/epoch-manager error doesn't silently skip the check for the rest of the epoch. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
| /// The last epoch for which the validator key was checked. Used to ensure | ||
| /// the check runs once per epoch on the first non-syncing head block, even | ||
| /// if the epoch boundary was crossed during sync. | ||
| last_validator_key_check_epoch: Option<EpochId>, |
There was a problem hiding this comment.
I'm also happy to just check it at the beginning of an epoch to avoid adding this field to client
wacban
left a comment
There was a problem hiding this comment.
LGTM
This PR shows there is a gap in nearcore support for rotating the key on chain. It's very rare event and this PR is an improvement so I don't have any recommendations, just sharing this as an observation.
| return; | ||
| } | ||
| let validator_id = signer.validator_id(); | ||
| let epoch_info = match self.epoch_manager.get_epoch_info(epoch_id) { |
There was a problem hiding this comment.
nit: you can also use the let Ok(epoch_info) = ... else { tracing::error!(...); }; pattern here
| return; | ||
| }; | ||
| let local_key = signer.public_key(); | ||
| let epoch_key = validator_stake.public_key(); |
There was a problem hiding this comment.
epoch_key doesn't quite convince me, how about chain_key or on_chain_key?
|
Making #15469 to track the validator key rotation issue. |
KeyFile::from_file), verify the private key derives the same public key listed in the file. ReturnsInvalidDataerror on mismatch.check_validator_key_on_new_head), so it also catches the case where the epoch boundary was crossed during sync.validator_signerfield toNodeSetupStateto support injecting a custom signer for testing.