Skip to content

fix(validator): reject a manifest that reuses one key for both signing roles - #1185

Merged
tcoratger merged 2 commits into
leanEthereum:mainfrom
adust09:fix/validator-dual-key-separation
Jul 6, 2026
Merged

fix(validator): reject a manifest that reuses one key for both signing roles#1185
tcoratger merged 2 commits into
leanEthereum:mainfrom
adust09:fix/validator-dual-key-separation

Conversation

@adust09

@adust09 adust09 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

ValidatorEntry documents 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, and from_yaml never 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 WellFormed distinctness 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_gen calls, so their public keys always differ.

Testing

  • Added 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.
  • Updated the shared manifest-entry test helper so its two public keys differ, as the loader now requires.
  • uv run pytest tests/node/validator/ — 78 passed.
  • ruff lint + format, ty typecheck, codespell all pass.

Closes #1184

adust09 added 2 commits July 7, 2026 00:32
…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
tcoratger merged commit ae104fa into leanEthereum:main Jul 6, 2026
14 checks passed
@adust09
adust09 deleted the fix/validator-dual-key-separation branch July 7, 2026 01:07
adust09 added a commit to NyxFoundation/formal-leanSpec that referenced this pull request Jul 20, 2026
…#59)

Upstream leanEthereum/leanSpec#1185 (closes #1184) now rejects a
manifest whose attestation and proposal public keys coincide, at load
time and without touching secret bytes. The model's addChecked
previously compared PRF seeds as a placeholder for the invited fix;
re-align it with the merged shape:

- addChecked now compares public keys, with the Arklib-side derivation
  entering as the publicKeyOf parameter (the repo's crypto-as-parameter
  pattern).
- addChecked_wellFormed holds for every derivation: distinct public
  keys imply distinct secret keys by congruence alone.
- addChecked_seed_distinct keeps the OTS-reuse core of #1184: for
  seed-fingerprinting derivations an accepted entry's keys have
  distinct master seeds.
- Registry.lean and catalog docstrings drop the stale 'upstream does
  not enforce' caveat: WellFormed is established by construction since
  #1185.
adust09 added a commit to NyxFoundation/formal-leanSpec that referenced this pull request Jul 20, 2026
* feat(val): re-align VAL-2 checked insertion with merged leanSpec#1185

Upstream leanEthereum/leanSpec#1185 (closes #1184) now rejects a
manifest whose attestation and proposal public keys coincide, at load
time and without touching secret bytes. The model's addChecked
previously compared PRF seeds as a placeholder for the invited fix;
re-align it with the merged shape:

- addChecked now compares public keys, with the Arklib-side derivation
  entering as the publicKeyOf parameter (the repo's crypto-as-parameter
  pattern).
- addChecked_wellFormed holds for every derivation: distinct public
  keys imply distinct secret keys by congruence alone.
- addChecked_seed_distinct keeps the OTS-reuse core of #1184: for
  seed-fingerprinting derivations an accepted entry's keys have
  distinct master seeds.
- Registry.lean and catalog docstrings drop the stale 'upstream does
  not enforce' caveat: WellFormed is established by construction since
  #1185.

* feat(st): prove ST-7 checkpoint replacement is strictly forward

ST-3/ST-6 bound only the checkpoint slots, so they still admitted a
transition that swaps latest_justified / latest_finalized to a different
root at the same slot. ST-7 closes that gap at the STF level: across a
successful transition each checkpoint is either unchanged as a whole
value (root included) or replaced by one at a strictly higher slot,
mirroring Checkpoint.advance_to's strict comparison and the
finalized < source.slot finalization guard.

- New LeanSpec/Forks/Lstar/CheckpointForward.lean with the per-phase
  chain applyJustification_forward -> processAttestation_forward ->
  foldlM -> processAttestations_forward, plus
  processBlockHeader_checkpoints_of_ne_zero (past genesis anchoring the
  header stage leaves both checkpoints untouched) and the top-level
  checkpoint_forward.
- Genesis anchoring (the first block filling in its parent root at
  slot 0) is the one designed same-slot replacement; it is excluded by
  the latestBlockHeader.slot != 0 hypothesis and documented in the
  catalog entry.
- Root ancestry across branches stays a store invariant per upstream
  leanSpec#1182's Checkpoint.advance_to / Store.latest_finalized notes;
  it is future FC work, not an STF property.
- Catalog: ST-7 entry added, progress table now 31 proved / 1 axiom.
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.

Validator dual-key separation is maintained only by convention (a same-key manifest silently reuses XMSS OTS state)

2 participants