8000 fix(consensus): prevote nil upon timeout when Proposal is missing by cason · Pull Request #2218 · cometbft/cometbft · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix(consensus): prevote nil upon timeout when Proposal is missing #2218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/consensus/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ func validatePrevote(t *testing.T, cs *State, round int32, privVal *validatorStu
panic(fmt.Sprintf("Expected prevote to be for nil, got %X", vote.BlockID.Hash))
}
} else {
if !bytes.Equal(vote.BlockID.Hash, blockHash) {
if vote.BlockID.Hash == nil {
panic(fmt.Sprintf("Expected prevote to be for %X, got <nil>", blockHash))
} else if !bytes.Equal(vote.BlockID.Hash, blockHash) {
panic(fmt.Sprintf("Expected prevote to be for %X, got %X", blockHash, vote.BlockID.Hash))
}
}
Expand Down
8 changes: 6 additions & 2 deletions internal/consensus/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -1340,8 +1340,8 @@ func (cs *State) defaultDoPrevote(height int64, round int32) {
logger := cs.Logger.With("height", height, "round", round)

// We did not receive a proposal within this round. (and thus executing this from a timeout)
if cs.ProposalBlock == nil {
logger.Debug("prevote step: ProposalBlock is nil; prevoting nil")
if cs.Proposal == nil || cs.ProposalBlock == nil {
logger.Debug("prevote step: Proposal or ProposalBlock is nil; prevoting nil")
cs.signAddVote(types.PrevoteType, nil, types.PartSetHeader{}, nil)
return
}
Expand Down Expand Up @@ -2055,6 +2055,10 @@ func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID p2p.ID) (add
cs.evsw.FireEvent(types.EventProposalBlockPart, msg)
}

count, total := cs.ProposalBlockParts.Count(), cs.ProposalBlockParts.Total()
cs.Logger.Debug("receive block part", "height", height, "round", round,
"index", part.Index, "count", count, "total", total, "from", peerID)

maxBytes := cs.state.ConsensusParams.Block.MaxBytes
if maxBytes == -1 {
maxBytes = int64(types.MaxBlockSizeBytes)
Expand Down
58 changes: 58 additions & 0 deletions internal/consensus/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ next round, so we precommit nil but maintain lock
x * TestStateLockMissingProposalWhenPOLSeenDoesNotUpdateLock - 4 vals, 1 misses proposal but sees POL.
x * TestStateLockMissingProposalWhenPOLSeenDoesNotUnlock - 4 vals, 1 misses proposal but sees POL.
* TestStateLockMissingProposalWhenPOLForLockedBlock - 4 vals, 1 misses proposal but sees POL for locked block.
x * TestStateMissingProposalValidBlockReceivedTimeout - 4 vals, 1 misses proposal but receives full block.
x * TestStateLockPOLSafety1 - 4 vals. We shouldn't change lock based on polka at earlier round
x * TestStateLockPOLSafety2 - 4 vals. After unlocking, we shouldn't relock based on polka at earlier round
x * TestStatePrevotePOLFromPreviousRound 4 vals, prevote a proposal if a POL was seen for it in a previous round.
Expand Down Expand Up @@ -1339,6 +1340,63 @@ func TestStateLockMissingProposalWhenPOLForLockedBlock(t *testing.T) {
// sees a POL for that block that matches the locked value (block).
}

// TestStateMissingProposalValidBlockReceivedTimeout tests if a node that
// misses the round's Proposal but receives a Polka for a block and the full
// block will not prevote for the valid block because the Proposal was missing.
func TestStateMissingProposalValidBlockReceivedTimeout(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cs1, vss := randState(4)
height, round := cs1.Height, cs1.Round

timeoutProposeCh := subscribe(cs1.eventBus, types.EventQueryTimeoutPropose)
voteCh := subscribe(cs1.eventBus, types.EventQueryVote)
validBlockCh := subscribe(cs1.eventBus, types.EventQueryValidBlock)

// Produce a block
block, err := cs1.createProposalBlock(ctx)
require.NoError(t, err)
blockParts, err := block.MakePartSet(types.BlockPartSizeBytes)
require.NoError(t, err)
blockID := types.BlockID{
Hash: block.Hash(),
PartSetHeader: blockParts.Header(),
}

// Skip round 0 and start consensus threads
round++
incrementRound(vss[1:]...)
startTestRound(cs1, height, round)

// Receive prevotes(height, round=1, blockID) from all other validators.
for i := 1; i < len(vss); i++ {
signAddVotes(cs1, types.PrevoteType, blockID.Hash, blockID.PartSetHeader, false, vss[i])
ensurePrevote(voteCh, height, round)
}

// We have polka for blockID so we can accept the associated full block.
for i := 0; i < int(blockParts.Total()); i++ {
err := cs1.AddProposalBlockPart(height, round, blockParts.GetPart(i), "peer")
require.NoError(t, err)
}
ensureNewValidBlock(validBlockCh, height, round)

// We don't prevote right now because we didn't receive the round's
// Proposal. Wait for the propose timeout.
ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds())

rs := cs1.GetRoundState()
assert.Equal(t, rs.ValidRound, round)
assert.Equal(t, rs.ValidBlock.Hash(), blockID.Hash)

// Since we didn't see the round's Proposal, we should prevote nil.
// NOTE: introduced by https://github.com/cometbft/cometbft/pull/1203.
// In branches v0.{34,37,38}.x, the node prevotes for the valid block.
ensurePrevote(voteCh, height, round)
validatePrevote(t, cs1, round, vss[0], nil)
}

// TestStateLockDoesNotLockOnOldProposal tests that observing
// a two thirds majority for a block does not cause a validator to lock on the
// block if a proposal was not seen for that block in the current round, but
Expand Down
0