[High] Lack of key-epoch invalidation across DKG rotations grants indefinite signing power to retired validator coalitions; missing chainId binding in `Gatekeeper.rotate()` amplifies cross-chain impact

Re: remedy design + the bootstrap-set observation

Glad the framing landed. On the remedy:

  1. Agreed — rotate/recall/govern need a uniform fix, not a rotate-only patch

All three resolve their signing key the same way: _extractPublicKey → getRotationInfoAt → upperLookup(exodusSession). Patching rotate() alone would leave recall() and govern() able to execute under an arbitrarily old key, same as today — the vulnerability lives in the shared resolution path, not in any one caller. Whatever fix we land on should sit at that shared point, not be duplicated three times at the call sites (duplication is also how these things drift out of sync over a few years of maintenance).

  1. “Latest key only” vs “allow delay” — this is exactly what a bounded freshness window resolves

You’re right that strict latest-key-only breaks a real use case: a human signs, then the broadcast is delayed for whatever reason. The fix isn’t to remove that flexibility, it’s to bound it. Concretely (this is Mitigation D from the original report, made explicit here):

  • Track the latest successfully-applied session number per chain (the Gatekeeper already has enough state to derive this — it’s implicit in the checkpoint/bitmap history).
  • At the resolution point, reject any call citing exodusSession such that (latestAppliedSession - exodusSession) > W, for a governance-set W expressed in sessions (era-aligned, not wall-clock, so it stays meaningful regardless of era-length changes).
  • W=0 is the strict “latest only” policy you correctly identified as too rigid. W set to, say, a handful of eras gives humans a realistic grace window for delayed execution while still bounding the set of keys that remain acceptable to: only validators who were active within that window. Retired-beyond-W cohorts lose acceptance entirely, closing exactly the gap in this report.
  • This is one state read + one comparison added to the existing resolution path — it doesn’t touch the DKG/Substrate side and doesn’t require a reshare.

Separately, chain-binding (Mitigation C: include block.chainid / address(this) in computeChallenge) stays worth doing regardless of W, since it closes the cross-chain replay angle independently of session age. The two are complementary, not alternatives — D bounds “how old,” C bounds “which chain.”

A concrete value for W is really your call, since it’s a product/ops tradeoff (how long does a legitimate signer realistically need) more than a cryptography one. Happy to sanity-check whatever number you’re leaning toward against the attack model.

  1. The bootstrap-set point is a good one, and we’d like to quantify it rather than leave it qualitative

You’re right that the genesis/bootstrap committee is structurally the smallest validator set in the chain’s history, which means it has the smallest absolute t-of-n to compromise of any epoch — and per this report, it never stops being a valid signing target unless something like the W-bound above is deployed. If you can confirm the actual genesis validator count for the chain(s) in scope (we have an earlier, possibly-stale figure from recon on a staging testnet, but didn’t want to assert a specific n here without you confirming it against the chain you’re assessing severity for), we’ll fold it into the severity writeup as the concrete worst-case instance of the “attacker picks the weakest epoch” argument — i.e., not just “the attacker set grows over time” in the abstract, but “the single easiest target has existed since block 0 and remains valid today.”

— Himeko