From c396295ff5c8cf916de9a1e8b1074f49ebb018f1 Mon Sep 17 00:00:00 2001 From: Qian-Cheng-nju Date: Mon, 16 Mar 2026 12:52:30 +0000 Subject: [PATCH 1/3] fix: add blockchain-head guard to handleBlockTimerExpiry Signed-off-by: Qian-Cheng-nju --- .../qbft/core/statemachine/QbftController.java | 7 +++++++ .../qbft/core/statemachine/QbftControllerTest.java | 11 +++++++++++ 2 files changed, 18 insertions(+) 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..90e07dd6c81 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,13 @@ 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..c329409ba48 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,17 @@ 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 proposalForUnknownValidatorIsDiscarded() { setupProposal(roundIdentifier, unknownValidator); From ea86fc240af87b42a90d0960b57b2ef3d1ff6ac1 Mon Sep 17 00:00:00 2001 From: Qian-Cheng-nju Date: Tue, 17 Mar 2026 07:33:25 +0000 Subject: [PATCH 2/3] style: fix spotless formatting for LOG.debug call Signed-off-by: Qian-Cheng-nju --- .../besu/consensus/qbft/core/statemachine/QbftController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 90e07dd6c81..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 @@ -263,8 +263,7 @@ public void handleBlockTimerExpiry(final BlockTimerExpiry blockTimerExpiry) { // 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."); + LOG.debug("Discarding a block-timer which targets a height not above current chain height."); return; } if (isMsgForCurrentHeight(roundIdentifier, getCurrentChainHeight())) { From 16e21198a19cca5472376b77ae762ad6726eaeec Mon Sep 17 00:00:00 2001 From: Qian-Cheng-nju Date: Wed, 18 Mar 2026 03:32:39 +0000 Subject: [PATCH 3/3] test: add test for block timer below current chain height Signed-off-by: Qian-Cheng-nju --- .../qbft/core/statemachine/QbftControllerTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 c329409ba48..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 @@ -352,6 +352,17 @@ public void blockTimerForAlreadyImportedHeightIsDiscarded() { 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);