Skip to content

Check validator keys against the registry and reject one key for both roles - #130

Open
dimka90 wants to merge 2 commits into
grandinetech:devnet-5-leanvm-mainfrom
dimka90:fix/validator-keys-match-genesis
Open

dimka90 wants to merge 2 commits into
grandinetech:devnet-5-leanvm-mainfrom
dimka90:fix/validator-keys-match-genesis

Conversation

@dimka90

@dimka90 dimka90 commented Oct 13, 2026

Copy link
Copy Markdown

Bug

  • Keys are never checked. KeyManager (validator/src/keys.rs) decodes each validator's attestation and proposal secret keys and stores them. It never compares them with the validator's attestation_pubkey / proposal_pubkey in the state's registry. The manifest's pubkey_hex is parsed and never read.
  • One file for both roles. ValidatorConfig::load_from_file (validator/src/lib.rs) accepts a "single-key format": an entry whose file names neither an attester nor a proposer key is used for both roles.
  • Key errors are ignored. In main.rs, if keys from --hash-sig-key-dir fail to load, the node logs a warning and keeps running with zero signatures.

Problem

  • One key for both roles breaks leanSpec. ValidatorRegistry raises ValueError when a validator's attestation and proposal keys are the same (src/lean_spec/node/validator/registry.py:217-226). XMSS is a stateful one-time signature, and a validator signs a proposal and an attestation in the same slot, so sharing one key reuses one-time state.
  • A wrong key fails silently. A swapped attester/proposer file, a file under the wrong index, or a key from another network loads without error. Every signature then fails verification at peers, and the node reports nothing.
  • The failure is hidden. Because key errors only warn, a misconfigured validator keeps running with zero signatures.

Change

Commit 1: check the keys.

  • SecretKey::public_key() derives the public key using leanVM's XmssSecretKey::public_key().
  • ValidatorService::new_with_keys_and_aggregator now takes the anchor state's validators. After loading each validator's keys, it calls KeyManager::check_keys, which fails when:
    • the two keys are the same;
    • either key differs from the registry entry;
    • the validator index is outside the registry.
  • A single-key manifest entry is rejected.

Commit 2: refuse to start.

  • When --hash-sig-key-dir is given and its keys fail to load or fail a check, or the directory is missing, the node exits with the error. This follows the existing checkpoint-verification pattern ("Refusing to start").
  • Running without --hash-sig-key-dir keeps the zero-signature mode.
  • This is a separate commit, so it is easy to drop if you prefer to keep the fallback. Without it, the checks above only produce a warning.

Tests

  • check_keys_matches_registry_and_distinct_roles: matching keys pass; swapped roles and a proposal key that is not in the registry are both rejected.
  • check_keys_rejects_one_key_for_both_roles.
  • test_load_from_file_rejects_single_key_entry.
  • All three fail when the checks or the single-key rejection are removed.

Checks run:

  • cargo test -p validator --release passes.
  • cargo fmt --check passes.
  • cargo build --release --bin lean_client passes.

I did not run the full workspace make test.

Manual run against a local devnet-5 quickstart genesis (validator 3):

  • The real keys start normally.

  • With validator 3's attester and proposer key files swapped, the node exits:

    Failed to load XMSS keys: Attestation key for validator 3 does not match the validator registry. Refusing to start.
    

… roles

KeyManager loaded each validator's attestation and proposal secret keys
without checking them, and annotated_validators.yaml entries naming
neither an attester nor a proposer file were loaded as one key for both
roles.

leanSpec's ValidatorRegistry rejects one key for both roles: a validator
signs a proposal and an attestation in the same slot, and XMSS is a
stateful one-time signature. A key that differs from the registry also
makes every signature fail verification at peers.

SecretKey now derives its public key, and loading checks each
validator's two keys differ and equal its entry in the anchor state's
registry. A single-key manifest entry is rejected.
With --hash-sig-key-dir set, a key that failed to load or did not match
the validator registry, or a missing key directory, only logged a
warning and the node ran with zero signatures, so a misconfigured
validator kept running and produced nothing peers accept.

Exit with the error instead. Without --hash-sig-key-dir the node still
runs with zero signatures, as before.
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.

1 participant