Skip to content
Draft
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
29 changes: 29 additions & 0 deletions internal/consensus/gossip_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ func dataGossipHandler(ps *PeerState, logger log.Logger, blockStore sm.BlockStor
return
}
}
if parts := heldBlockPartsFor(rs, prs, isValidator); parts != nil {
// rs is this handler's copy of the snapshot. The gossiper serves its
// part set and marks each part sent as the peer's under its round
// (see PeerState.SetHasProposalBlockPart), the peer's own here.
rs.ProposalBlockParts, rs.Round = parts, prs.Round
gossiper.GossipProposalBlockParts(ctx, rs, prs)
return
}

// if the peer is on a previous height that we have, help catch up
blockStoreBase := blockStore.Base()
Expand Down Expand Up @@ -267,6 +275,27 @@ func shouldProposalBeGossiped(rs cstypes.RoundState, prs *cstypes.PeerRoundState
return rs.Proposal != nil && !prs.Proposal && isValidator
}

// heldBlockPartsFor returns the complete part set of this node's valid or
// locked block when a validator peer at this height, in an earlier round, is
// collecting that block and rs.ProposalBlockParts is not it. Entering a later
// round drops the proposal part set, and a peer whose polka retargeted it at
// the block (see addVoteUpdateValidBlockMw) has nowhere else to get the parts
// from: without them it rejects every precommit for the block, counts none of
// them towards leaving its round, and stays there for good. The part set
// header is the block's identity; a round or a hash alone names no bytes.
func heldBlockPartsFor(rs cstypes.RoundState, prs *cstypes.PeerRoundState, isValidator bool) *types.PartSet {
if !isValidator || rs.Height != prs.Height || prs.Round >= rs.Round ||
rs.ProposalBlockParts.HasHeader(prs.ProposalBlockPartSetHeader) {
return nil
}
for _, parts := range []*types.PartSet{rs.ValidBlockParts, rs.LockedBlockParts} {
if parts.HasHeader(prs.ProposalBlockPartSetHeader) && parts.IsComplete() {
return parts
}
}
return nil
}

func shouldBlockPartsBeGossiped(rs cstypes.RoundState, prs *cstypes.PeerRoundState, isValidator bool) bool {
if rs.Height != prs.Height || rs.Round < prs.Round {
return false
Expand Down
103 changes: 103 additions & 0 deletions internal/consensus/gossip_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,3 +743,106 @@ func TestRoundVoteResendGossipHandler(t *testing.T) {
})
}
}

// A validator peer at this height, in an earlier round, collecting the block
// this node holds as valid or locked is served its parts from there, since the
// round that left it dropped the proposal part set. The part set header names
// the block; nothing is served on round or hash alone, to another height, or to
// a non-validator.
func (suite *GossipHandlerTestSuite) TestDataGossipHandlerServesHeldBlockToLaggingPeer() {
ctx := context.Background()
logger := log.NewTestingLogger(suite.T())
valSet, _ := factory.MockValidatorSet()
validator, nonValidator := valSet.GetProTxHashes()[0], crypto.RandProTxHash()
held := types.NewPartSetFromData(tmrand.Bytes(200), 100)
other := types.NewPartSetFromData(tmrand.Bytes(100), 100)
incomplete := types.NewPartSetFromHeader(held.Header())
_, err := incomplete.AddPart(held.GetPart(0))
suite.Require().NoError(err)
collecting := func(parts *types.PartSet, height int64, round int32) cstypes.PeerRoundState {
return cstypes.PeerRoundState{
Height: height,
Round: round,
ProposalBlockPartSetHeader: parts.Header(),
ProposalBlockParts: types.NewPartSetFromHeader(parts.Header()).BitArray(),
}
}
testCases := []struct {
name string
proTxHash crypto.ProTxHash
rs cstypes.RoundState
prs cstypes.PeerRoundState
served bool
}{
{
name: "valid block to a peer in an earlier round",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: held},
prs: collecting(held, 10, 1),
served: true,
},
{
name: "locked block to a peer in an earlier round",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, LockedRound: 1, LockedBlockParts: held},
prs: collecting(held, 10, 1),
served: true,
},
{
name: "valid block while this round proposes another",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: held, ProposalBlockParts: other},
prs: collecting(held, 10, 1),
served: true,
},
{
name: "not a block the peer collects",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: held},
prs: collecting(other, 10, 1),
},
{
name: "not to another height",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: held},
prs: collecting(held, 11, 1),
},
{
name: "not to a peer in this round or a later one",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: held},
prs: collecting(held, 10, 2),
},
{
name: "not an incomplete block",
proTxHash: validator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: incomplete},
prs: collecting(held, 10, 1),
},
{
name: "not to a non-validator",
proTxHash: nonValidator,
rs: cstypes.RoundState{Height: 10, Round: 2, ValidRound: 1, ValidBlockParts: held},
prs: collecting(held, 10, 1),
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
ps := &PeerState{ProTxHash: tc.proTxHash, PRS: tc.prs}
stateData := StateData{RoundState: tc.rs, state: sm.State{Validators: valSet}}
if tc.served {
// The part set served and the round it is marked as the peer's
// under are the peer's, on the handler's copy of the snapshot.
rsMatchedBy := mock.MatchedBy(func(rs cstypes.RoundState) bool {
return rs.ProposalBlockParts == held && rs.Round == tc.prs.Round
})
suite.fakeGossiper.On("GossipProposalBlockParts", ctx, rsMatchedBy, mock.Anything).Once()
} else {
suite.fakeBlockStore.On("Base").Return(int64(1)).Maybe()
}
hd := dataGossipHandler(ps, logger, suite.fakeBlockStore, suite.fakeGossiper)
hd(ctx, stateData)
})
}
}
Loading
Loading