You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
computeNodesConfigFromList aborts the epoch with ErrListSizeZero when the elected list is empty, even when eligible validators could fill the consensus group #132
Found while re-assessing #121 against the open PRs on 2026-08-13, baseline develop @ e48c2c2. This is not a defect in #121 itself: it is a separate failure mode in the same function, and it is what turns #121's silent drop from cosmetic into dangerous once PR #86 merges.
Problem
computeNodesConfigFromList aborts the entire epoch computation when the elected list is empty (sharding/nodesCoordinator.go:956-958):
iflen(electedList) ==0 {
returnnil, fmt.Errorf("%w elected list size is zero. No validators found", ErrListSizeZero)
}
The guard looks at electedList only. A perfectly healthy eligibleList does not save it. Verified with a throwaway test against the real function: 2 validators with List == observer plus 4 healthy eligible validators produce nil, ErrListSizeZero.
The consequence is not a skipped epoch, it is a permanently stuck node:
EpochStartPrepare logs could not compute nodes config from list - do nothing on nodesCoordinator epochStartPrepare and returns (sharding/nodesCoordinator.go:776-780).
SetNodes (:795) is therefore never reached, so nodesConfig[newEpoch] is never created.
ComputeConsensusGroup for the new epoch then returns ErrEpochNodesConfigDoesNotExist (:525-527), so every non-epoch-start header of the new epoch fails validation on all three intake paths.
That is the same end state as #90, reached through a different and worse cause. In #90 the window closes as soon as the epoch-start block commits. Here SetNodes is never called at all, so nothing closes it.
How this becomes reachable
Today the elected list can only be empty if no validator carries List == elected, which does not happen in normal operation. That is why this has never been observed.
PR #86 (validator node version attestation with observer demotion) changes exactly that. resolveVersionEnforcedList returns state.List_observer for validators without a satisfying attested version, and the PR's own tests assert demotion from List_elected as well as from List_eligible. Demoted validators then hit no case in the switch at sharding/nodesCoordinator.go:928-940 and are dropped from every list, including the elected one.
PR #86 does carry two safety guards in computeVersionEnforcement, but neither closes this hole:
supermajority: satisfied*3 >= electable*2
floor: satisfied >= minElectableNodes, skipped entirely when minElectableNodes == 0
Both count elected + eligible as a single electable population. The abort in the coordinator checks elected on its own. A distribution that passes both guards and still stalls the network:
10 elected validators, all running an outdated version
30 eligible validators, all correctly attested
electable = 40, satisfied = 30
supermajority: 30*3 = 90 >= 40*2 = 80, holds
floor: holds for any minElectableNodes <= 30
Demotion proceeds, all 10 elected validators become observers, the elected list is empty, and epoch preparation fails permanently on every node that computes it.
This is not an exotic distribution. The elected set is the incumbent, longest-running population, which is precisely the group most likely to lag behind a mandatory upgrade.
Task
Decide which side owns the invariant "the elected list is never empty at epoch preparation", then close it there:
Coordinator side: either tolerate an empty elected list when eligibleList can still fill consensusGroupSize (the shuffler runs immediately after at :789 and repopulates elected from eligible), or make the failure actionable instead of an early return that leaves the node silently frozen.
Either way the abort branch needs a test, and the interaction needs to be pinned before #86 merges. In its current shape this looks like a merge blocker for #86.
References
sharding/nodesCoordinator.go:956-958 (the abort), :776-780 (the early return), :525-527 (the downstream error), :928-940 (the switch that drops observers)
Context
Found while re-assessing #121 against the open PRs on 2026-08-13, baseline
develop@e48c2c2. This is not a defect in #121 itself: it is a separate failure mode in the same function, and it is what turns #121's silent drop from cosmetic into dangerous once PR #86 merges.Problem
computeNodesConfigFromListaborts the entire epoch computation when the elected list is empty (sharding/nodesCoordinator.go:956-958):The guard looks at
electedListonly. A perfectly healthyeligibleListdoes not save it. Verified with a throwaway test against the real function: 2 validators withList == observerplus 4 healthyeligiblevalidators producenil, ErrListSizeZero.The consequence is not a skipped epoch, it is a permanently stuck node:
EpochStartPreparelogscould not compute nodes config from list - do nothing on nodesCoordinator epochStartPrepareand returns (sharding/nodesCoordinator.go:776-780).SetNodes(:795) is therefore never reached, sonodesConfig[newEpoch]is never created.ComputeConsensusGroupfor the new epoch then returnsErrEpochNodesConfigDoesNotExist(:525-527), so every non-epoch-start header of the new epoch fails validation on all three intake paths.That is the same end state as #90, reached through a different and worse cause. In #90 the window closes as soon as the epoch-start block commits. Here
SetNodesis never called at all, so nothing closes it.How this becomes reachable
Today the elected list can only be empty if no validator carries
List == elected, which does not happen in normal operation. That is why this has never been observed.PR #86 (validator node version attestation with observer demotion) changes exactly that.
resolveVersionEnforcedListreturnsstate.List_observerfor validators without a satisfying attested version, and the PR's own tests assert demotion fromList_electedas well as fromList_eligible. Demoted validators then hit no case in the switch at sharding/nodesCoordinator.go:928-940 and are dropped from every list, including the elected one.PR #86 does carry two safety guards in
computeVersionEnforcement, but neither closes this hole:satisfied*3 >= electable*2satisfied >= minElectableNodes, skipped entirely whenminElectableNodes == 0Both count
elected + eligibleas a singleelectablepopulation. The abort in the coordinator checkselectedon its own. A distribution that passes both guards and still stalls the network:electable = 40,satisfied = 3030*3 = 90 >= 40*2 = 80, holdsminElectableNodes <= 30Demotion proceeds, all 10 elected validators become observers, the elected list is empty, and epoch preparation fails permanently on every node that computes it.
This is not an exotic distribution. The elected set is the incumbent, longest-running population, which is precisely the group most likely to lag behind a mandatory upgrade.
Task
Decide which side owns the invariant "the elected list is never empty at epoch preparation", then close it there:
eligibleListcan still fillconsensusGroupSize(the shuffler runs immediately after at :789 and repopulates elected from eligible), or make the failure actionable instead of an early return that leaves the node silently frozen.Either way the abort branch needs a test, and the interaction needs to be pinned before #86 merges. In its current shape this looks like a merge blocker for #86.
References