fix(epoch): anchor inactivity-ejection quorum floor to pre-outage snapshot - #4
Open
draevik wants to merge 1 commit into
Open
Conversation
…pshot PR lightchain-protocol#3 (bc61fed) fixed the 2026-08-11 mass-ejection incident by capping inactivity ejections at 2/3 of the *current* active count per epoch. That stops a single-epoch mass exit, but the floor is recomputed every epoch against a count that the floor itself is shrinking - so an outage lasting more than one epoch past the ejection threshold still erodes the active set epoch over epoch (2/3, then 2/3 of that, then 2/3 of that again), just slower than before instead of not at all. Fix: anchor the floor to the active-set size as of a fixed point in the past (24h ago by default) instead of the live count. This requires no new consensus state - activation_epoch/exit_epoch are already permanent per-validator fields, so 'was validator X active at epoch E' can be reconstructed for any historical E from the current state alone. 24h is chosen to comfortably exceed inactivityEjectionMinDowntimeSeconds (6h), so under normal operation the anchor epoch falls before any inactivity-driven ejection has occurred - the floor stays pinned to the genuine pre-outage set size for the duration of the incident this mechanism is meant to survive. An outage longer than 24h itself will eventually see the anchor drift into the incident window; that is a real limit of a schema-free fix and is called out in the comment rather than hidden. Test: new TestProcessRegistryUpdates_InactivityEjectionFloorDoesNotCompound constructs a second-round scenario (2 validators already ejected earlier, 4 still active and still over threshold) where a live-count floor would permit ejecting 2 more of the remaining 4, and asserts the anchored floor permits zero further ejections. Existing quorum-floor and boundary tests are unaffected since they use genesis-epoch states where anchorEpoch == currentEpoch (no lookback history to differ against). Not run in this environment: go.mod requires go1.25.1 and this sandbox has no network access to proxy.golang.org to fetch the toolchain. Verified gofmt-clean and syntactically valid; please run 'go test ./beacon-chain/core/epoch/...' before merging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
draevik
force-pushed
the
fix/anchor-inactivity-ejection-floor
branch
from
August 12, 2026 11:49
db1d634 to
e422cea
Compare
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.
What this fixes
PR #3 fixed the 2026-08-11 mass-ejection incident by capping inactivity ejections at 2/3 of the current active count per epoch. That stops a single-epoch mass exit, but the floor is recomputed every epoch against a count the floor itself is shrinking — so an outage lasting more than one epoch past the ejection threshold still erodes the active set epoch over epoch (2/3, then 2/3 of that, then 2/3 of that again), just slower than before instead of not at all.
What this adds
Anchors the floor to the active-set size as of a fixed point in the past (24h ago by default) instead of the live count. Requires no new consensus state —
activation_epoch/exit_epochare already permanent per-validator fields, so "was validator X active at epoch E" is reconstructible for any historical E from the current state alone.24h comfortably exceeds
inactivityEjectionMinDowntimeSeconds(6h), so under normal operation the anchor epoch falls before any inactivity-driven ejection has occurred. Known limit, called out in the code comment: an outage longer than 24h itself will eventually see the anchor drift into the incident window — a real trade-off of a fix that avoids a consensus-schema change.New test
TestProcessRegistryUpdates_InactivityEjectionFloorDoesNotCompoundconstructs a second-round scenario (2 validators already ejected earlier, 4 still active and still over threshold) where a live-count floor would permit ejecting 2 more of the remaining 4, and asserts the anchored floor permits zero further ejections.Verification
Ran locally (go1.26.0, WSL2/Ubuntu):
All 10 tests pass, including the new test above:
Before merging, please
This is a fix for a specific gap I found in PR #3's floor — not a final answer on the whole mechanism. Two things I'd want a second set of eyes on specifically:
inactivityEjectionFloorAnchorSeconds) is my judgment call, not something validated against LightChain's operational history. If there's a reason it should be shorter/longer, or anchored differently, I'd rather that get decided by someone with real incident context.BeaconConfig()(Ethereum's 12s slot / 32 slots-per-epoch), not LightChain's actual deployed timing (2s / 6). That's the same class of blind spot that let the original 256-threshold ship — nothing in this repo currently pins the real derived values against LightChain's real config, and this PR doesn't close that gap either. Worth a follow-up test that loads the actual chain-config file.This is a consensus-rule change, same as PR #3 — flagging accordingly.