diff --git a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java index d43e9de4521..c049ccfe9eb 100644 --- a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java +++ b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java @@ -260,6 +260,12 @@ public void handleNewBlockEvent(final QbftNewChainHead newChainHead) { @Override public void handleBlockTimerExpiry(final BlockTimerExpiry blockTimerExpiry) { final ConsensusRoundIdentifier roundIdentifier = blockTimerExpiry.getRoundIdentifier(); + // Discard block timer events that target a height already on the blockchain (e.g., block + // was imported via peer sync while the timer was pending). Same guard as handleRoundExpiry. + if (roundIdentifier.getSequenceNumber() <= blockchain.getChainHeadBlockNumber()) { + LOG.debug("Discarding a block-timer which targets a height not above current chain height."); + return; + } if (isMsgForCurrentHeight(roundIdentifier, getCurrentChainHeight())) { getCurrentHeightManager().handleBlockTimerExpiry(roundIdentifier); } else { diff --git a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java index 0339133de3b..ecfdbe65a39 100644 --- a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java +++ b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftControllerTest.java @@ -341,6 +341,28 @@ public void blockTimerForPastHeightIsDiscarded() { verify(blockHeightManager, never()).handleBlockTimerExpiry(any()); } + @Test + public void blockTimerForAlreadyImportedHeightIsDiscarded() { + // Simulate peer sync: blockchain head advances to current height while block timer is pending + when(blockChain.getChainHeadBlockNumber()).thenReturn(4L); + final BlockTimerExpiry blockTimerExpiry = new BlockTimerExpiry(roundIdentifier); + constructQbftController(); + qbftController.start(); + qbftController.handleBlockTimerExpiry(blockTimerExpiry); + verify(blockHeightManager, never()).handleBlockTimerExpiry(any()); + } + + @Test + public void blockTimerForHeightBelowChainHeadIsDiscarded() { + // Blockchain head has advanced beyond the timer's target height + when(blockChain.getChainHeadBlockNumber()).thenReturn(5L); + final BlockTimerExpiry blockTimerExpiry = new BlockTimerExpiry(roundIdentifier); + constructQbftController(); + qbftController.start(); + qbftController.handleBlockTimerExpiry(blockTimerExpiry); + verify(blockHeightManager, never()).handleBlockTimerExpiry(any()); + } + @Test public void proposalForUnknownValidatorIsDiscarded() { setupProposal(roundIdentifier, unknownValidator);