Skip to content

fix(epoch): anchor inactivity-ejection quorum floor to pre-outage snapshot - #4

Open
draevik wants to merge 1 commit into
lightchain-protocol:mainfrom
draevik:fix/anchor-inactivity-ejection-floor
Open

fix(epoch): anchor inactivity-ejection quorum floor to pre-outage snapshot#4
draevik wants to merge 1 commit into
lightchain-protocol:mainfrom
draevik:fix/anchor-inactivity-ejection-floor

Conversation

@draevik

@draevik draevik commented Aug 12, 2026

Copy link
Copy Markdown

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_epoch are 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_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.

Verification

Ran locally (go1.26.0, WSL2/Ubuntu):

go test ./beacon-chain/core/epoch/... -v

All 10 tests pass, including the new test above:

--- PASS: TestProcessRegistryUpdates_NoRotation (0.00s)
--- PASS: TestProcessRegistryUpdates_EligibleToActivate (0.00s)
--- PASS: TestProcessRegistryUpdates_EligibleToActivate_Cancun (0.00s)
--- PASS: TestProcessRegistryUpdates_ActivationCompletes (0.00s)
--- PASS: TestProcessRegistryUpdates_ValidatorsEjected (0.00s)
--- PASS: TestProcessRegistryUpdates_CanExits (0.00s)
--- PASS: TestProcessRegistryUpdates_HighInactivityScoreEjected (0.00s)
--- PASS: TestProcessRegistryUpdates_InactivityEjectionQuorumFloor (0.00s)
--- PASS: TestProcessRegistryUpdates_InactivityEjectionFloorDoesNotCompound (0.00s)
--- PASS: TestProcessRegistryUpdates_Phase0NoInactivityScores (0.00s)
ok  github.com/OffchainLabs/prysm/v7/beacon-chain/core/epoch  3.892s

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:

  • The 24h anchor window (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.
  • Tests here run against Go's default 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.

…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
draevik force-pushed the fix/anchor-inactivity-ejection-floor branch from db1d634 to e422cea Compare August 12, 2026 11:49
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.

1 participant