Skip to content

feat(client): validator key consistency check once per epoch#15417

Merged
darioush merged 10 commits into
near:masterfrom
darioush:validator-key-check
Mar 27, 2026
Merged

feat(client): validator key consistency check once per epoch#15417
darioush merged 10 commits into
near:masterfrom
darioush:validator-key-check

Conversation

@darioush

@darioush darioush commented Mar 19, 2026

Copy link
Copy Markdown
Contributor
  • At key file load time (KeyFile::from_file), verify the private key derives the same public key listed in the file. Returns InvalidData error on mismatch.
  • At each epoch transition, verify the loaded validator public key matches the key registered in the upcoming epoch's info. Panics on mismatch with an actionable message. The check is skipped during sync to avoid false positives when the node is behind and the key was correctly rotated after the local head's epoch.
  • The check fires on the first non-syncing head block of each epoch (via check_validator_key_on_new_head), so it also catches the case where the epoch boundary was crossed during sync.
  • Add validator_signer field to NodeSetupState to support injecting a custom signer for testing.
  • Add unit test for key file mismatch and a test-loop test for epoch boundary mismatch detection.

@darioush darioush requested a review from Copilot March 19, 2026 19:28
@darioush darioush changed the title feat(client): add validator key consistency checks at load and startup feat(client): validator key consistency checks at epoch change and startup Mar 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_file by ensuring secret_key derives the declared public_key (error on mismatch).
  • Add Client::check_validator_key_for_epoch and 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.

Comment thread test-loop-tests/src/tests/validator_key_check.rs Outdated
Comment thread chain/client/src/client.rs Outdated
Comment on lines +1683 to +1686
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);

Copilot AI Mar 19, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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");
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Seems not worth it to me, can add it if reviewer prefers to have it.

@darioush darioush requested a review from wacban March 19, 2026 19:36
@darioush darioush marked this pull request as ready for review March 19, 2026 19:36
@darioush darioush requested a review from a team as a code owner March 19, 2026 19:36
@codecov

codecov Bot commented Mar 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.82759% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.28%. Comparing base (70222bf) to head (5273f22).
⚠️ Report is 31 commits behind head on master.

Files with missing lines Patch % Lines
chain/client/src/client.rs 93.10% 2 Missing ⚠️
core/crypto/src/key_file.rs 95.83% 1 Missing ⚠️
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     
Flag Coverage Δ
pytests-nightly 1.15% <0.00%> (-0.01%) ⬇️
unittests 68.56% <94.82%> (+0.18%) ⬆️
unittests-nightly 68.78% <94.82%> (+0.21%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

wacban
wacban previously approved these changes Mar 20, 2026

@wacban wacban left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nice, thanks for adding this!

Comment thread chain/client/src/client_actor.rs Outdated
Comment on lines +439 to +440
let epoch_id = client.chain.head()?.epoch_id;
client.check_validator_key_for_epoch(&epoch_id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a case where the node is behind and wrong epoch is used for the check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great point. Let me figure it out before proceeding.

@darioush darioush marked this pull request as draft March 23, 2026 13:46
@darioush darioush changed the title feat(client): validator key consistency checks at epoch change and startup feat(client): validator key consistency check once per epoch Mar 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread chain/client/src/client.rs Outdated
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]>
@darioush darioush marked this pull request as ready for review March 27, 2026 02:54
@darioush darioush requested a review from wacban March 27, 2026 02:54
/// 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>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm also happy to just check it at the beginning of an epoch to avoid adding this field to client

@darioush darioush dismissed wacban’s stale review March 27, 2026 03:41

code changed

@wacban wacban left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread chain/client/src/client.rs Outdated
return;
}
let validator_id = signer.validator_id();
let epoch_info = match self.epoch_manager.get_epoch_info(epoch_id) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: you can also use the let Ok(epoch_info) = ... else { tracing::error!(...); }; pattern here

Comment thread chain/client/src/client.rs Outdated
return;
};
let local_key = signer.public_key();
let epoch_key = validator_stake.public_key();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

epoch_key doesn't quite convince me, how about chain_key or on_chain_key?

@darioush

Copy link
Copy Markdown
Contributor Author

Making #15469 to track the validator key rotation issue.

@darioush darioush enabled auto-merge March 27, 2026 12:20
@darioush darioush added this pull request to the merge queue Mar 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 27, 2026
@darioush darioush added this pull request to the merge queue Mar 27, 2026
@darioush darioush removed this pull request from the merge queue due to a manual request Mar 27, 2026
@darioush darioush enabled auto-merge March 27, 2026 12:40
@darioush darioush added this pull request to the merge queue Mar 27, 2026
Merged via the queue into near:master with commit c7267a3 Mar 27, 2026
32 checks passed
@darioush darioush deleted the validator-key-check branch March 27, 2026 13:16
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