fix(validator): reject a manifest that reuses one key for both signing roles#1185
Merged
tcoratger merged 2 commits intoJul 6, 2026
Merged
Conversation
…g roles ValidatorEntry documents that the attestation and proposal keys must be separate so a validator can sign both a proposal and an attestation in the same slot without one-time-signature state reuse. Nothing enforced it: the registry loader assigned keys without comparing them, so a manifest carrying the same key in both fields loaded silently. Both signatures then advanced their own copy of the shared key from the pre-advance state, consuming overlapping XMSS one-time state — a key-compromise-class failure that stayed invisible because both loads and both signatures verify individually. Reject the misconfiguration at load time by comparing the manifest's two public keys, which leaves the secret bytes untouched. This turns the docstring invariant into a client-checkable rule and discharges the formal model's distinctness assumption at construction. Closes leanEthereum#1184
The one-validator fixture manifest reused one placeholder public key for both roles. The registry loader now rejects that, so make the two keys differ, as a real manifest does.
tcoratger
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ValidatorEntrydocuments that the attestation and proposal keys are separate "so one validator can sign both within the same slot without OTS conflict" — but nothing enforced it. The registry loader assigned keys without comparing them, andfrom_yamlnever compared the two keys it loaded. A node whose manifest carried the same key in both fields signed a block proposal and an attestation for the same slot with the same stateful XMSS key — the one-time-signature state reuse the separation exists to prevent — and nothing surfaced it.With identical key material in both fields, each signing path advanced its own field's copy from the pre-advance state, so the two signatures consumed overlapping OTS state (a key-compromise-class failure for hash-based schemes). Both loads succeeded and both signatures verified individually, so the misconfiguration stayed invisible.
Fix
Reject the misconfiguration at load time by comparing the manifest's two public keys, which leaves the secret bytes untouched and fires before any key file is decoded. This turns the docstring invariant into a client-checkable rule and discharges the formal model's
WellFormeddistinctness assumption at construction — the same shape as #1179 did for the store invariants.Real manifests are unaffected: production key generation derives the attestation and proposal keypairs from two independent
key_gencalls, so their public keys always differ.Testing
test_same_key_for_both_roles_raises, which asserts the full error message and writes no key files, proving the check fires before any decode.uv run pytest tests/node/validator/— 78 passed.Closes #1184