Follow-up from #17285, raised in review:
#17285 (comment)
When clearing a reloaded key's doppelganger quarantine, the validator client
runs two separate calls: checkDoppelGangerForKeys (the beacon node evaluates
liveness as of its head at that moment) and clearingEpoch (a ChainHead
request whose result caps the clearing epoch, min(wall-clock epoch, head epoch)).
The problem is that the two calls can see different heads. The liveness
verdict from the first call only covers epochs up to the head the beacon node
had at that moment. If the head advances before the second call — an epoch
boundary passes, or the node finishes catching up — clearingEpoch picks up
the newer head and uses it to end the quarantine, even though the verdict it
is acting on never looked at that newest epoch.
Example: a key quarantined at epoch 5 with a 2-epoch wait may only clear once
a clean verdict covers an epoch greater than 7.
checkDoppelGangerForKeys runs while the node's head is at epoch 7 →
the "clean" verdict covers epochs ≤ 7. Not sufficient to clear.
- The head advances to epoch 8 before the next call.
clearingEpoch returns min(8, 8) = 8 → 8 > 7, so the key clears —
with epoch 8's liveness never having been evaluated.
The skew is bounded to one epoch (the wall-clock min cap, plus the calls
running back-to-back within one slot), but the clearing epoch and the epoch
the verdict covers should be the same value by construction.
Follow-up from #17285, raised in review:
#17285 (comment)
When clearing a reloaded key's doppelganger quarantine, the validator client
runs two separate calls:
checkDoppelGangerForKeys(the beacon node evaluatesliveness as of its head at that moment) and
clearingEpoch(aChainHeadrequest whose result caps the clearing epoch,
min(wall-clock epoch, head epoch)).The problem is that the two calls can see different heads. The liveness
verdict from the first call only covers epochs up to the head the beacon node
had at that moment. If the head advances before the second call — an epoch
boundary passes, or the node finishes catching up —
clearingEpochpicks upthe newer head and uses it to end the quarantine, even though the verdict it
is acting on never looked at that newest epoch.
Example: a key quarantined at epoch 5 with a 2-epoch wait may only clear once
a clean verdict covers an epoch greater than 7.
checkDoppelGangerForKeysruns while the node's head is at epoch 7 →the "clean" verdict covers epochs ≤ 7. Not sufficient to clear.
clearingEpochreturnsmin(8, 8) = 8→8 > 7, so the key clears —with epoch 8's liveness never having been evaluated.
The skew is bounded to one epoch (the wall-clock
mincap, plus the callsrunning back-to-back within one slot), but the clearing epoch and the epoch
the verdict covers should be the same value by construction.