diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java index 17b28490c41..7b6bab417de 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java @@ -490,6 +490,42 @@ public BesuNode createQbftNode( return create(builder.build()); } + public BesuNode createQbftNodeWithExtraCliOptions( + final String name, + final boolean fixedPort, + final DataStorageFormat storageFormat, + final List extraCliOptions) + throws IOException { + JsonRpcConfiguration rpcConfig = node.createJsonRpcWithQbftEnabledConfig(false); + rpcConfig.addRpcApi("ADMIN,TXPOOL"); + if (fixedPort) { + rpcConfig.setPort( + Math.abs(name.hashCode() % 60000) + + 1024); // Generate a consistent port for p2p based on node name + } + BesuNodeConfigurationBuilder builder = + new BesuNodeConfigurationBuilder() + .name(name) + .miningEnabled() + .devMode(false) + .jsonRpcConfiguration(rpcConfig) + .webSocketConfiguration(node.createWebSocketEnabledConfig()) + .dataStorageConfiguration( + storageFormat == DataStorageFormat.FOREST + ? DataStorageConfiguration.DEFAULT_FOREST_CONFIG + : DataStorageConfiguration.DEFAULT_BONSAI_CONFIG) + .genesisConfigProvider(GenesisConfigurationFactory::createQbftGenesisConfig) + .extraCLIOptions(extraCliOptions); + if (fixedPort) { + builder.p2pPort( + Math.abs(name.hashCode() % 60000) + + 1024 + + 500); // Generate a consistent port for p2p based on node name (+ 500 to avoid + // clashing with RPC port or other nodes with a similar name) + } + return create(builder.build()); + } + public BesuNode createQbftMigrationNode( final String name, final boolean fixedPort, final DataStorageFormat storageFormat) throws IOException { diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftRecoveryAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftRecoveryAcceptanceTest.java new file mode 100644 index 00000000000..465b9d58156 --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftRecoveryAcceptanceTest.java @@ -0,0 +1,88 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.tests.acceptance.bft; + +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class BftRecoveryAcceptanceTest extends AcceptanceTestBase { + + private static final long THREE_MINUTES = Duration.of(3, ChronoUnit.MINUTES).toMillis(); + private static final long TEN_SECONDS = Duration.of(10, ChronoUnit.SECONDS).toMillis(); + + @Test + public void shouldBeRecoverFast() throws Exception { + + List extraCLIOptions = + List.of("--Xqbft-fast-recovery", "--Xqbft-enable-early-round-change"); + + // Create a mix of Bonsai and Forest DB nodes + final BesuNode minerNode1 = + besu.createQbftNodeWithExtraCliOptions( + "miner1", true, DataStorageFormat.BONSAI, extraCLIOptions); + final BesuNode minerNode2 = + besu.createQbftNodeWithExtraCliOptions( + "miner2", true, DataStorageFormat.BONSAI, extraCLIOptions); + final BesuNode minerNode3 = + besu.createQbftNodeWithExtraCliOptions( + "miner3", true, DataStorageFormat.BONSAI, extraCLIOptions); + final BesuNode minerNode4 = + besu.createQbftNodeWithExtraCliOptions( + "miner4", true, DataStorageFormat.BONSAI, extraCLIOptions); + + cluster.start(minerNode1, minerNode2, minerNode3, minerNode4); + + cluster.verify(blockchain.reachesHeight(minerNode1, 20, 85)); + + stopNode(minerNode3); + stopNode(minerNode4); + + /* Let the other two nodes go to round 5 */ + Thread.sleep(THREE_MINUTES); + + startNode(minerNode3); + startNode(minerNode4); + + /* Mining should start in few seconds */ + cluster.verify(blockchain.reachesHeight(minerNode3, 1, 20)); + } + + // Start a node with a delay before returning to give it time to start + private void startNode(final BesuNode node) throws InterruptedException { + cluster.startNode(node); + Thread.sleep(TEN_SECONDS); + } + + // Stop a node with a delay before returning to give it time to stop + private void stopNode(final BesuNode node) throws InterruptedException { + cluster.stopNode(node); + Thread.sleep(TEN_SECONDS); + } + + @AfterEach + @Override + public void tearDownAcceptanceTestBase() { + cluster.stop(); + super.tearDownAcceptanceTestBase(); + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index c0a50432c10..9cd0eb47e11 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -63,13 +63,13 @@ import org.hyperledger.besu.cli.options.PermissionsOptions; import org.hyperledger.besu.cli.options.PluginsConfigurationOptions; import org.hyperledger.besu.cli.options.PrivacyPluginOptions; +import org.hyperledger.besu.cli.options.QBFTOptions; import org.hyperledger.besu.cli.options.RPCOptions; import org.hyperledger.besu.cli.options.RpcWebsocketOptions; import org.hyperledger.besu.cli.options.SynchronizerOptions; import org.hyperledger.besu.cli.options.TransactionPoolOptions; import org.hyperledger.besu.cli.options.storage.DataStorageOptions; import org.hyperledger.besu.cli.options.storage.DiffBasedSubStorageOptions; -import org.hyperledger.besu.cli.options.unstable.QBFTOptions; import org.hyperledger.besu.cli.presynctasks.PreSynchronizationTaskRunner; import org.hyperledger.besu.cli.presynctasks.PrivateDatabaseMigrationPreSyncTask; import org.hyperledger.besu.cli.subcommands.PasswordSubCommand; @@ -1793,6 +1793,7 @@ public BesuControllerBuilder setupControllerBuilder() { .isRevertReasonEnabled(isRevertReasonEnabled) .storageProvider(storageProvider) .isEarlyRoundChangeEnabled(unstableQbftOptions.isEarlyRoundChangeEnabled()) + .isFastRecoveryEnabled(unstableQbftOptions.isFastRecoveryEnabled()) .requiredBlocks(requiredBlocks) .reorgLoggingThreshold(reorgLoggingThreshold) .evmConfiguration(unstableEvmOptions.toDomainObject()) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/QBFTOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/QBFTOptions.java index bb2954b5981..650cb172777 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/QBFTOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/QBFTOptions.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.cli.options.unstable; +package org.hyperledger.besu.cli.options; import picocli.CommandLine; @@ -38,6 +38,12 @@ public static QBFTOptions create() { hidden = true) private boolean enableEarlyRoundChange = false; + @CommandLine.Option( + names = {"--Xqbft-fast-recovery"}, + description = "Enable fast recovery mode for QBFT consensus (experimental)", + hidden = true) + private boolean enableFastRecovery = false; + /** * Is early round change enabled boolean. * @@ -46,4 +52,13 @@ public static QBFTOptions create() { public boolean isEarlyRoundChangeEnabled() { return enableEarlyRoundChange; } + + /** + * Is fast recovery enabled boolean. + * + * @return true if fast recovery is enabled + */ + public boolean isFastRecoveryEnabled() { + return enableFastRecovery; + } } diff --git a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java index 3af1210345e..49092f852cd 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java @@ -218,6 +218,9 @@ public abstract class BesuControllerBuilder implements MiningParameterOverrides /** When enabled, round changes on f+1 RC messages from higher rounds */ protected boolean isEarlyRoundChangeEnabled = false; + /** When enabled, qbft recovery for nodes coming up is faster */ + protected boolean isFastRecoveryEnabled = false; + /** Instantiates a new Besu controller builder. */ protected BesuControllerBuilder() {} @@ -552,6 +555,17 @@ public BesuControllerBuilder isEarlyRoundChangeEnabled(final boolean isEarlyRoun return this; } + /** + * set fast recovery enabled for QBFT consensus + * + * @param fastRecoveryEnabled whether to enable fast recovery + * @return the besu controller + */ + public BesuControllerBuilder isFastRecoveryEnabled(final boolean fastRecoveryEnabled) { + this.isFastRecoveryEnabled = fastRecoveryEnabled; + return this; + } + /** * Build besu controller. * diff --git a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java index 66f24205a44..fae86b66b1c 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java @@ -32,11 +32,13 @@ import org.hyperledger.besu.consensus.common.bft.BlockTimer; import org.hyperledger.besu.consensus.common.bft.EthSynchronizerUpdater; import org.hyperledger.besu.consensus.common.bft.EventMultiplexer; +import org.hyperledger.besu.consensus.common.bft.FrequentMessageMulticaster; import org.hyperledger.besu.consensus.common.bft.MessageTracker; import org.hyperledger.besu.consensus.common.bft.RoundTimer; import org.hyperledger.besu.consensus.common.bft.UniqueMessageMulticaster; import org.hyperledger.besu.consensus.common.bft.blockcreation.BftMiningCoordinator; import org.hyperledger.besu.consensus.common.bft.blockcreation.ProposerSelector; +import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster; import org.hyperledger.besu.consensus.common.bft.network.ValidatorPeers; import org.hyperledger.besu.consensus.common.bft.protocol.BftProtocolManager; import org.hyperledger.besu.consensus.common.bft.statemachine.BftEventHandler; @@ -242,6 +244,7 @@ protected MiningCoordinator createMiningCoordinator( // "only send once" filter applied by the UniqueMessageMulticaster. peers = new ValidatorPeers(validatorProvider, Istanbul100SubProtocol.NAME); + final ValidatorMulticaster frequentRCMulticaster = new FrequentMessageMulticaster(peers, 5000); final UniqueMessageMulticaster uniqueMessageMulticaster = new UniqueMessageMulticaster(peers, qbftConfig.getGossipedHistoryLimit()); @@ -260,7 +263,10 @@ protected MiningCoordinator createMiningCoordinator( bftExecutors), new BlockTimer(bftEventQueue, qbftForksSchedule, bftExecutors, clock), new QbftBlockCreatorFactoryAdaptor(blockCreatorFactory, qbftExtraDataCodec), - clock); + clock, + isEarlyRoundChangeEnabled, + isFastRecoveryEnabled, + frequentRCMulticaster); final MessageValidatorFactory messageValidatorFactory = new MessageValidatorFactory(proposerSelector, qbftProtocolSchedule, qbftProtocolContext); @@ -301,8 +307,6 @@ protected MiningCoordinator createMiningCoordinator( new QbftValidatorModeTransitionLoggerAdaptor( new ValidatorModeTransitionLogger(qbftForksSchedule))); - qbftBlockHeightManagerFactory.isEarlyRoundChangeEnabled(isEarlyRoundChangeEnabled); - final QbftEventHandler qbftController = new QbftController( new QbftBlockchainAdaptor(blockchain), diff --git a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index e04ec95ca1c..c86a149385a 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -283,6 +283,7 @@ public void initMocks() throws Exception { when(mockControllerBuilder.isParallelTxProcessingEnabled(false)) .thenReturn(mockControllerBuilder); when(mockControllerBuilder.isEarlyRoundChangeEnabled(false)).thenReturn(mockControllerBuilder); + when(mockControllerBuilder.isFastRecoveryEnabled(false)).thenReturn(mockControllerBuilder); when(mockControllerBuilder.storageProvider(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.requiredBlocks(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.reorgLoggingThreshold(anyLong())).thenReturn(mockControllerBuilder); diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FrequentMessageMulticaster.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FrequentMessageMulticaster.java new file mode 100644 index 00000000000..cd382973fda --- /dev/null +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FrequentMessageMulticaster.java @@ -0,0 +1,107 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.consensus.common.bft; + +import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** The Frequent message multicaster. */ +public class FrequentMessageMulticaster implements ValidatorMulticaster { + private final ValidatorMulticaster multicaster; + private final long interval; + private final ScheduledExecutorService scheduledExecutorService; + private volatile ScheduledFuture scheduledTask; + private static final Logger LOG = LoggerFactory.getLogger(FrequentMessageMulticaster.class); + + /** + * Constructor that specifies the interval at which to multicast messages + * + * @param multicaster Network connections to the remote validators + * @param multicastInterval Interval at which to multicast messages + */ + public FrequentMessageMulticaster( + final ValidatorMulticaster multicaster, final long multicastInterval) { + this.multicaster = multicaster; + this.interval = multicastInterval; + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + } + + /** + * Instantiates a new Frequent message multicaster. + * + * @param multicaster the multicaster + */ + @VisibleForTesting + public FrequentMessageMulticaster(final ValidatorMulticaster multicaster) { + this(multicaster, 5000); + } + + private Runnable createPeriodicMulticastTask( + final MessageData message, final Collection
denylist) { + return () -> { + LOG.debug( + "Broadcasting round change every {} ms on thread {}", + interval, + Thread.currentThread().threadId()); + multicaster.send(message, denylist); + }; + } + + @Override + public synchronized void send(final MessageData message) { + send(message, Collections.emptyList()); + } + + @Override + public synchronized void send(final MessageData message, final Collection
denylist) { + // Cancel the existing task before scheduling a new one + stopFrequentMulticasting(); + + // Define the periodic multicast task + final Runnable periodicMulticast = createPeriodicMulticastTask(message, denylist); + + // Schedule the periodic task + scheduledTask = + scheduledExecutorService.scheduleAtFixedRate( + periodicMulticast, 0, interval, TimeUnit.MILLISECONDS); + + LOG.debug("Scheduled new frequent multicast task for message."); + } + + /** + * Stop frequent multicasting. + * + *

Stops the current scheduled task. + */ + public synchronized void stopFrequentMulticasting() { + if (scheduledTask != null) { + LOG.debug("Cancelling existing frequent multicast task."); + scheduledTask.cancel(true); + scheduledTask = null; + } + } +} diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FrequentMessageMulticasterTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FrequentMessageMulticasterTest.java new file mode 100644 index 00000000000..fe13c32cd6d --- /dev/null +++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FrequentMessageMulticasterTest.java @@ -0,0 +1,90 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.consensus.common.bft; + +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FrequentMessageMulticasterTest { + private FrequentMessageMulticaster multicaster; + private ValidatorMulticaster mockValidatorMulticaster; + + @BeforeEach + void setUp() { + mockValidatorMulticaster = mock(ValidatorMulticaster.class); + multicaster = new FrequentMessageMulticaster(mockValidatorMulticaster, 5000); + } + + @AfterEach + void tearDown() { + multicaster.stopFrequentMulticasting(); + } + + @Test + void testMessageIsSentPeriodically() throws InterruptedException { + MessageData message = mock(MessageData.class); + + multicaster.send(message); + + // Allow time for multiple executions + TimeUnit.MILLISECONDS.sleep(12000); + + // The message should have been sent multiple times + verify(mockValidatorMulticaster, atLeast(2)).send(message, Collections.emptyList()); + } + + @Test + void testNewMessageReplacesOldTask() throws InterruptedException { + MessageData message1 = mock(MessageData.class); + MessageData message2 = mock(MessageData.class); + + multicaster.send(message1); + TimeUnit.MILLISECONDS.sleep(2000); // Allow initial message to start + + multicaster.send(message2); // This should replace the old task + + // Wait and check if message1 was stopped + TimeUnit.MILLISECONDS.sleep(7000); + + verify(mockValidatorMulticaster, atMost(2)).send(message1, Collections.emptyList()); + verify(mockValidatorMulticaster, atLeast(1)).send(message2, Collections.emptyList()); + } + + @Test + void testCancelStopsFrequentMulticasting() throws InterruptedException { + MessageData message = mock(MessageData.class); + + multicaster.send(message); + TimeUnit.MILLISECONDS.sleep(7000); + + multicaster.stopFrequentMulticasting(); + + TimeUnit.MILLISECONDS.sleep(7000); + + verify(mockValidatorMulticaster, atMost(3)).send(message, Collections.emptyList()); + } +} diff --git a/consensus/qbft-core/src/integration-test/java/org/hyperledger/besu/consensus/qbft/core/support/TestContextBuilder.java b/consensus/qbft-core/src/integration-test/java/org/hyperledger/besu/consensus/qbft/core/support/TestContextBuilder.java index e55e467de13..bf27a414d72 100644 --- a/consensus/qbft-core/src/integration-test/java/org/hyperledger/besu/consensus/qbft/core/support/TestContextBuilder.java +++ b/consensus/qbft-core/src/integration-test/java/org/hyperledger/besu/consensus/qbft/core/support/TestContextBuilder.java @@ -541,7 +541,10 @@ private static ControllerAndState createControllerAndFinalState( new RoundTimer(bftEventQueue, Duration.ofSeconds(ROUND_TIMER_SEC), bftExecutors), new BlockTimer(bftEventQueue, forksSchedule, bftExecutors, TestClock.fixed()), new QbftBlockCreatorFactoryAdaptor(blockCreatorFactory, BFT_EXTRA_DATA_ENCODER), - clock); + clock, + false, + false, + multicaster); final MessageFactory messageFactory = new MessageFactory(nodeKey, blockEncoder); diff --git a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java index 60b838c0a9b..d2debe905d1 100644 --- a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java +++ b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java @@ -134,6 +134,7 @@ public void multicastCommit( public void multicastRoundChange( final ConsensusRoundIdentifier roundIdentifier, final Optional preparedRoundCertificate) { + try { final RoundChange data = messageFactory.createRoundChange(roundIdentifier, preparedRoundCertificate); @@ -141,6 +142,7 @@ public void multicastRoundChange( final RoundChangeMessageData message = RoundChangeMessageData.create(data); multicaster.send(message); + } catch (final SecurityModuleException e) { LOG.warn("Failed to generate signature for RoundChange (not sent): {} ", e.getMessage()); } diff --git a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java index 8dd88567127..aef9f6163cd 100644 --- a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java +++ b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java @@ -68,7 +68,6 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager { private Optional latestPreparedCertificate = Optional.empty(); private Optional currentRound = Optional.empty(); - private boolean isEarlyRoundChangeEnabled = false; /** * Instantiates a new Qbft block height manager. @@ -92,7 +91,11 @@ public QbftBlockHeightManager( this.parentHeader = parentHeader; this.roundFactory = qbftRoundFactory; this.transmitter = - new QbftMessageTransmitter(messageFactory, finalState.getValidatorMulticaster()); + new QbftMessageTransmitter( + messageFactory, + (finalState.isFastRecoveryEnabled() + ? finalState.getFrequentRCMulticaster() + : finalState.getValidatorMulticaster())); this.messageFactory = messageFactory; this.clock = clock; this.roundChangeManager = roundChangeManager; @@ -116,39 +119,6 @@ public QbftBlockHeightManager( finalState.getBlockTimer().startTimer(roundIdentifier, parentHeader::getTimestamp); } - /** - * Instantiates a new Qbft block height manager. Secondary constructor with early round change - * option. - * - * @param parentHeader the parent header - * @param finalState the final state - * @param roundChangeManager the round change manager - * @param qbftRoundFactory the qbft round factory - * @param clock the clock - * @param messageValidatorFactory the message validator factory - * @param messageFactory the message factory - * @param isEarlyRoundChangeEnabled enable round change when f+1 RC messages are received - */ - public QbftBlockHeightManager( - final QbftBlockHeader parentHeader, - final QbftFinalState finalState, - final RoundChangeManager roundChangeManager, - final QbftRoundFactory qbftRoundFactory, - final Clock clock, - final MessageValidatorFactory messageValidatorFactory, - final MessageFactory messageFactory, - final boolean isEarlyRoundChangeEnabled) { - this( - parentHeader, - finalState, - roundChangeManager, - qbftRoundFactory, - clock, - messageValidatorFactory, - messageFactory); - this.isEarlyRoundChangeEnabled = isEarlyRoundChangeEnabled; - } - @Override public void handleBlockTimerExpiry(final ConsensusRoundIdentifier roundIdentifier) { if (currentRound.isPresent()) { @@ -381,7 +351,7 @@ public void handleRoundChangePayload(final RoundChange message) { final Optional> result = roundChangeManager.appendRoundChangeMessage(message); - if (!isEarlyRoundChangeEnabled) { + if (!finalState.isEarlyRoundChangeEnabled()) { if (result.isPresent()) { LOG.debug( "Received sufficient RoundChange messages to change round to targetRound={}", diff --git a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerFactory.java b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerFactory.java index b2bb40335bc..faf8450465d 100644 --- a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerFactory.java +++ b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerFactory.java @@ -34,7 +34,6 @@ public class QbftBlockHeightManagerFactory { private final MessageValidatorFactory messageValidatorFactory; private final MessageFactory messageFactory; private final QbftValidatorModeTransitionLogger validatorModeTransitionLogger; - private boolean isEarlyRoundChangeEnabled = false; /** * Instantiates a new Qbft block height manager factory. @@ -76,15 +75,6 @@ public BaseQbftBlockHeightManager create(final QbftBlockHeader parentHeader) { } } - /** - * Sets early round change enabled. - * - * @param isEarlyRoundChangeEnabled the is early round change enabled - */ - public void isEarlyRoundChangeEnabled(final boolean isEarlyRoundChangeEnabled) { - this.isEarlyRoundChangeEnabled = isEarlyRoundChangeEnabled; - } - /** * Creates a no-op height manager * @@ -102,7 +92,7 @@ private BaseQbftBlockHeightManager createFullBlockHeightManager( QbftBlockHeightManager qbftBlockHeightManager; RoundChangeManager roundChangeManager; - if (isEarlyRoundChangeEnabled) { + if (finalState.isEarlyRoundChangeEnabled()) { roundChangeManager = new RoundChangeManager( BftHelpers.calculateRequiredValidatorQuorum(finalState.getValidators().size()), @@ -110,16 +100,6 @@ private BaseQbftBlockHeightManager createFullBlockHeightManager( messageValidatorFactory.createRoundChangeMessageValidator( parentHeader.getNumber() + 1L, parentHeader), finalState.getLocalAddress()); - qbftBlockHeightManager = - new QbftBlockHeightManager( - parentHeader, - finalState, - roundChangeManager, - roundFactory, - finalState.getClock(), - messageValidatorFactory, - messageFactory, - true); } else { roundChangeManager = new RoundChangeManager( @@ -127,16 +107,16 @@ private BaseQbftBlockHeightManager createFullBlockHeightManager( messageValidatorFactory.createRoundChangeMessageValidator( parentHeader.getNumber() + 1L, parentHeader), finalState.getLocalAddress()); - qbftBlockHeightManager = - new QbftBlockHeightManager( - parentHeader, - finalState, - roundChangeManager, - roundFactory, - finalState.getClock(), - messageValidatorFactory, - messageFactory); } + qbftBlockHeightManager = + new QbftBlockHeightManager( + parentHeader, + finalState, + roundChangeManager, + roundFactory, + finalState.getClock(), + messageValidatorFactory, + messageFactory); return qbftBlockHeightManager; } 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 2471f416415..eaae7429ffb 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 @@ -15,6 +15,7 @@ package org.hyperledger.besu.consensus.qbft.core.statemachine; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; +import org.hyperledger.besu.consensus.common.bft.FrequentMessageMulticaster; import org.hyperledger.besu.consensus.common.bft.Gossiper; import org.hyperledger.besu.consensus.common.bft.MessageTracker; import org.hyperledger.besu.consensus.common.bft.SynchronizerUpdater; @@ -207,6 +208,11 @@ protected

> void consumeMessage( @Override public void handleNewBlockEvent(final QbftNewChainHead newChainHead) { + if (finalState.isFastRecoveryEnabled()) { + FrequentMessageMulticaster frequentMessageMulticaster = + (FrequentMessageMulticaster) finalState.getFrequentRCMulticaster(); + frequentMessageMulticaster.stopFrequentMulticasting(); + } final QbftBlockHeader newBlockHeader = newChainHead.newChainHeadHeader(); final QbftBlockHeader currentMiningParent = getCurrentHeightManager().getParentBlockHeader(); LOG.debug( diff --git a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftFinalState.java b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftFinalState.java index 87fbf6decef..56aa30b59b8 100644 --- a/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftFinalState.java +++ b/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftFinalState.java @@ -104,4 +104,25 @@ public interface QbftFinalState { * @return true if the local node is the proposer for the given round, false otherwise */ boolean isLocalNodeProposerForRound(ConsensusRoundIdentifier roundIdentifier); + + /** + * Gets the frequent round change multicaster. + * + * @return the frequent round change multicaster + */ + public ValidatorMulticaster getFrequentRCMulticaster(); + + /** + * Is early round change enabled boolean. + * + * @return the boolean + */ + public boolean isEarlyRoundChangeEnabled(); + + /** + * Is fast recovery enabled boolean. + * + * @return the boolean + */ + public boolean isFastRecoveryEnabled(); } diff --git a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerTest.java b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerTest.java index 6c6155f187f..7fbcdcc09c1 100644 --- a/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerTest.java +++ b/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManagerTest.java @@ -599,6 +599,7 @@ public void roundChangeTriggeredUponReceivingFPlusOneRoundChanges() { new RoundChangeManager(3, 2, roundChangeMessageValidator, validators.get(2)); when(finalState.isLocalNodeProposerForRound(any())).thenReturn(false); + when(finalState.isEarlyRoundChangeEnabled()).thenReturn(true); final QbftBlockHeightManager manager = new QbftBlockHeightManager( @@ -608,8 +609,7 @@ public void roundChangeTriggeredUponReceivingFPlusOneRoundChanges() { roundFactory, clock, messageValidatorFactory, - validatorMessageFactory.get(2), - true); // Enable early round change + validatorMessageFactory.get(2)); manager.handleRoundChangePayload(roundChange1); manager.handleRoundChangePayload(roundChange2); diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java index 5a3ff17130b..d81a35c4ffb 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftFinalStateImpl.java @@ -40,6 +40,9 @@ public class QbftFinalStateImpl implements QbftFinalState { private final BlockTimer blockTimer; private final QbftBlockCreatorFactory blockCreatorFactory; private final Clock clock; + private final boolean isEarlyRoundChangeEnabled; + private final boolean isFastRecoveryEnabled; + private final ValidatorMulticaster frequentRCMulticaster; /** * Constructs a new QBFT final state. @@ -53,6 +56,9 @@ public class QbftFinalStateImpl implements QbftFinalState { * @param blockTimer the block timer * @param blockCreatorFactory the block creator factory * @param clock the clock + * @param isEarlyRoundChangeEnabled the early round change enabled flag + * @param isFastRecoveryEnabled the fast recovery enabled flag + * @param frequentRCMulticaster the frequent round change multicaster */ public QbftFinalStateImpl( final ValidatorProvider validatorProvider, @@ -63,7 +69,10 @@ public QbftFinalStateImpl( final RoundTimer roundTimer, final BlockTimer blockTimer, final QbftBlockCreatorFactory blockCreatorFactory, - final Clock clock) { + final Clock clock, + final boolean isEarlyRoundChangeEnabled, + final boolean isFastRecoveryEnabled, + final ValidatorMulticaster frequentRCMulticaster) { this.validatorProvider = validatorProvider; this.nodeKey = nodeKey; this.localAddress = localAddress; @@ -73,6 +82,9 @@ public QbftFinalStateImpl( this.blockTimer = blockTimer; this.blockCreatorFactory = blockCreatorFactory; this.clock = clock; + this.isEarlyRoundChangeEnabled = isEarlyRoundChangeEnabled; + this.isFastRecoveryEnabled = isFastRecoveryEnabled; + this.frequentRCMulticaster = frequentRCMulticaster; } /** @@ -95,6 +107,36 @@ public ValidatorMulticaster getValidatorMulticaster() { return validatorMulticaster; } + /** + * Gets the frequent round change multicaster. + * + * @return the frequent round change multicaster + */ + @Override + public ValidatorMulticaster getFrequentRCMulticaster() { + return frequentRCMulticaster; + } + + /** + * Is early round change enabled boolean. + * + * @return the boolean + */ + @Override + public boolean isEarlyRoundChangeEnabled() { + return isEarlyRoundChangeEnabled; + } + + /** + * Is fast recovery enabled boolean. + * + * @return the boolean + */ + @Override + public boolean isFastRecoveryEnabled() { + return isFastRecoveryEnabled; + } + /** * Gets node key. *