Skip to content

Do not write a second empty vote for a round we already timed out on - #486

Merged
yacovm merged 5 commits into
mainfrom
fix/duplicate-empty-vote-record
Aug 7, 2026
Merged

Do not write a second empty vote for a round we already timed out on#486
yacovm merged 5 commits into
mainfrom
fix/duplicate-empty-vote-record

Conversation

@samliok

@samliok samliok commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

triggerEmptyBlockNotarization is guarded only by e.round > round. It sets emptyVotes.timedOut but never reads it, even though haveWeAlreadyTimedOutOnThisRound exists, and three call sites reach it for the same round: a failed block verification, the blacklisted-proposer path in buildBlock, and proposalWaitTimeExpired.

Any two firing for one round append two empty vote records, which is what fails TestLongRunningReplication with duplicate empty vote record for round 95. The test times out on round 0, then delivers a proposal that fails verification, and asserts a second record never appears.

triggerEmptyBlockNotarization set emptyVotes.timedOut but never read it, and three call
sites reach it for the same round. Timing out on a round and then failing to verify its
late proposal appended a second empty vote record for that round.
@samliok
samliok marked this pull request as ready for review August 4, 2026 20:50
Comment thread simplex/epoch.go
return
}

// Several paths trigger the empty block agreement for the same round, so everything below,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

production code LGTM, will look at test tomorrow

Signed-off-by: Sam Liokumovich <65994425+samliok@users.noreply.github.com>
Comment thread simplex/epoch_failover_test.go Outdated
Comment thread simplex/epoch_failover_test.go Outdated
Comment thread simplex/epoch_failover_test.go Outdated
// countEmptyVoteRecords returns how many empty vote records the WAL holds for the round. It
// returns an error rather than asserting, so it is safe to call from the condition goroutine
// require.Never spawns.
func countEmptyVoteRecords(wal *testutil.TestWAL, round uint64) (int, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that in the test we only have a single round - round 0, so we don't actually need this round parameter here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, although I think a better solution is to just have our test WAL throw an error whenever we append a duplicate record. Maybe for another pr

Comment thread simplex/epoch_failover_test.go Outdated
}, nodes[0]))

// The epoch appends to the WAL after Verify returns
require.Never(t, func() bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can succeed if error is always != nil

@yacovm
yacovm merged commit 93477dc into main Aug 7, 2026
7 checks passed
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.

2 participants