Skip to content

Commit b5850ac

Browse files
committed
fix: stop throwing away all unverified proposals upon hitting a
verification error
1 parent e76a3ac commit b5850ac

File tree

2 files changed

+68
-12
lines changed

2 files changed

+68
-12
lines changed

consensus/propagation/catchup.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,30 +187,31 @@ func (blockProp *Reactor) applyCachedProposalIfAvailable() {
187187
blockProp.Logger.Info("applying cached proposal from catchup",
188188
"height", currentHeight, "round", cb.Proposal.Round, "peer", peer.peer.ID())
189189

190-
blockProp.handleCachedCompactBlock(cb)
191-
192-
// Clean up the cache entry for this peer
193-
peer.DeleteUnverifiedProposal(currentHeight)
194-
return
190+
if blockProp.handleCachedCompactBlock(cb) {
191+
// Clean up the cache entry for this peer only if we successfully applied it.
192+
peer.DeleteUnverifiedProposal(currentHeight)
193+
return
194+
}
195195
}
196196
}
197197

198198
// handleCachedCompactBlock processes a verified cached compact block.
199199
// Similar to handleCompactBlock but skips validation (already verified) and triggers immediate catchup.
200-
func (blockProp *Reactor) handleCachedCompactBlock(cb *proptypes.CompactBlock) {
200+
// Returns true if the cached block was applied.
201+
func (blockProp *Reactor) handleCachedCompactBlock(cb *proptypes.CompactBlock) bool {
201202
blockProp.Logger.Info("applying cached compact block", "height", cb.Proposal.Height, "round", cb.Proposal.Round)
202203

203204
// generate (and cache) the proofs from the partset hashes in the compact block
204205
_, err := cb.Proofs()
205206
if err != nil {
206207
blockProp.Logger.Error("cached compact block has invalid proofs", "err", err.Error())
207-
return
208+
return false
208209
}
209210

210211
// Send proposal to consensus reactor
211212
select {
212213
case <-blockProp.ctx.Done():
213-
return
214+
return false
214215
case blockProp.proposalChan <- ProposalAndSrc{
215216
Proposal: cb.Proposal,
216217
From: blockProp.self, // From self since it's from cache
@@ -221,20 +222,27 @@ func (blockProp *Reactor) handleCachedCompactBlock(cb *proptypes.CompactBlock) {
221222
added := blockProp.AddProposal(cb)
222223
if !added {
223224
blockProp.Logger.Debug("cached proposal already exists", "height", cb.Proposal.Height, "round", cb.Proposal.Round)
224-
return
225225
}
226226

227-
// Mark as catchup to skip parity requests in retryWants
227+
propFound := false
228228
blockProp.pmtx.Lock()
229-
if prop := blockProp.proposals[cb.Proposal.Height][cb.Proposal.Round]; prop != nil {
230-
prop.catchup = true
229+
if props, ok := blockProp.proposals[cb.Proposal.Height]; ok {
230+
if prop := props[cb.Proposal.Round]; prop != nil {
231+
// Mark as catchup to skip parity requests in retryWants
232+
prop.catchup = true
233+
propFound = true
234+
}
231235
}
232236
blockProp.pmtx.Unlock()
237+
if !propFound {
238+
return false
239+
}
233240

234241
// Recover any parts from mempool
235242
blockProp.recoverPartsFromMempool(cb)
236243

237244
// Immediately trigger part requests (like AddCommitment)
238245
blockProp.ticker.Reset(RetryTime)
239246
go blockProp.retryWants()
247+
return true
240248
}

consensus/propagation/catchup_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,54 @@ func TestApplyCachedProposalIfAvailable_MultiPeer(t *testing.T) {
321321
require.Nil(t, peer2Cache, "valid peer's cache entry should be deleted after successful apply")
322322
}
323323

324+
// TestApplyCachedProposalIfAvailable_KeepOnFailure ensures we don't delete a cached proposal
325+
// if handleCachedCompactBlock fails to apply it.
326+
func TestApplyCachedProposalIfAvailable_KeepOnFailure(t *testing.T) {
327+
p2pCfg := defaultTestP2PConf()
328+
nodes := 2
329+
reactors, _ := createTestReactors(nodes, p2pCfg, false, "")
330+
n1 := reactors[0] // Will have a proposal that fails in handleCachedCompactBlock
331+
n2 := reactors[1] // The node applying cached proposals
332+
333+
cleanup, _, sm := state.SetupTestCase(t)
334+
t.Cleanup(func() {
335+
cleanup(t)
336+
})
337+
338+
// n2 at height 1, round 0, with proposer set
339+
n2.SetHeightAndRound(1, 0)
340+
n2.SetProposer(mockPubKey)
341+
342+
// Create a compact block with valid signatures but invalid parts hashes.
343+
cbBad, _, _, _ := testCompactBlock(t, sm, 2, 0)
344+
badHash := make([]byte, len(cbBad.PartsHashes[0]))
345+
copy(badHash, cbBad.PartsHashes[0])
346+
badHash[0] ^= 0xFF
347+
cbBad.PartsHashes[0] = badHash
348+
cbBad.SetProofCache(nil)
349+
signBytes, err := cbBad.SignBytes()
350+
require.NoError(t, err)
351+
sig, err := mockPrivVal.SignRawBytes(TestChainID, CompactBlockUID, signBytes)
352+
require.NoError(t, err)
353+
cbBad.Signature = sig
354+
355+
// n2 receives proposal for height 2 while still at height 1 - should cache it.
356+
n2.handleCompactBlock(cbBad, n1.self, false)
357+
358+
peer1 := n2.getPeer(n1.self)
359+
require.NotNil(t, peer1.GetUnverifiedProposal(2), "peer1 should have cached proposal")
360+
361+
// Now n2 catches up to height 2 - applyCachedProposalIfAvailable runs and should fail to apply cbBad.
362+
n2.Prune(1)
363+
n2.SetHeightAndRound(2, 0)
364+
365+
_, _, has := n2.GetProposal(2, 0)
366+
require.False(t, has, "proposal should not be applied when proofs are invalid")
367+
368+
// Cached proposal should remain since handleCachedCompactBlock failed.
369+
require.NotNil(t, peer1.GetUnverifiedProposal(2), "cached proposal should remain after failed apply")
370+
}
371+
324372
// TestApplyCachedProposalIfAvailable_WrongRound tests that cached proposals for
325373
// a different round are kept cached until we advance to that round.
326374
func TestApplyCachedProposalIfAvailable_WrongRound(t *testing.T) {

0 commit comments

Comments
 (0)