@@ -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.
326374func TestApplyCachedProposalIfAvailable_WrongRound (t * testing.T ) {
0 commit comments