Skip to content

fix(bft): guard election vote phase against nil sortition data - #589

Open
Alicepoltora wants to merge 1 commit into
canopy-network:developmentfrom
Alicepoltora:fix/bft-nil-sortition
Open

fix(bft): guard election vote phase against nil sortition data#589
Alicepoltora wants to merge 1 commit into
canopy-network:developmentfrom
Alicepoltora:fix/bft-nil-sortition

Conversation

@Alicepoltora

Copy link
Copy Markdown

Description

StartElectionPhase populates b.SortitionData only on its happy path. It has two early returns before that assignment - when ValidatorSet.GetValidator(b.PublicKey) fails, and when LoadLastProposers(b.RootHeight) fails - and both merely log and return.

HandlePhase calls SetTimerForNextPhase unconditionally after the phase handler, so an early return still advances the view to ELECTION_VOTE. NewRound sets b.SortitionData = nil, so on that path StartElectionVotePhase runs with a nil pointer and dereferences it:

  • with candidates present, in GetElectionCandidates (b.SortitionData.VotingPower);
  • with zero candidates, one line later in SelectProposerFromCandidates -> lib.WeightedPseudorandom, which reads p.LastProposerAddresses.

The result is a panic in the consensus loop. The trigger is local rather than attacker-supplied: LoadLastProposers reads the historical store (TimeMachine -> store.NewReadOnly(height)), so any pruned-version or backing-store read error reaches it. The code deliberately chooses to log-and-continue there, and then dereferences unconditionally in the next phase.

Related Issues

None found - I searched open and closed issues and PRs for sortition/election-phase nil handling and found nothing covering this.

Changes Made

  • Added a guard at the top of StartElectionVotePhase: if b.SortitionData is nil, log the error and call RoundInterrupt() instead of proceeding.
  • Added TestStartElectionVotePhaseWithNilSortitionData, which reproduces the reachable state and asserts the phase does not panic and recovers into RoundInterrupt.

I used RoundInterrupt() rather than a bare return because it is the established recovery path in this file (it is used at a dozen other bail-out sites): it stops the VDF service, resets the FSM, and gossips a pacemaker message so the node moves cleanly into the next round instead of sitting in a half-initialized view.

Checklist

  • I have tested the changes locally and verified they work as intended.
  • I have appropriately titled my branch issue-#<issue-number>. (No issue exists for this; happy to rename if you'd like one opened first.)
  • I have run re-built the web-wallet and/or block explorer (if applicable). (Not applicable - Go only.)
  • I have run npm run prettier to format the web-wallet and/or block explorer (if applicable). (Not applicable.)
  • I have updated documentation (if applicable). (Not applicable.)
  • I have included tests for the changes (if applicable).

Additional Notes

Verification on development:

  • The new test panics before the fix (invalid memory address or nil pointer dereference) and passes after it.
  • go test ./bft/ passes in full.
  • go test $(go list ./... | grep -v cmd/auto-update) - the CI test command - passes across the repository.
  • go build ./... succeeds; gofmt reports no changes for either touched file.

If you would prefer the fix on the other side - having StartElectionPhase populate SortitionData before its fallible calls, or not advancing the phase when it bails - I am happy to rework it that way; this version was chosen as the smallest change consistent with the file's existing error handling.

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