Round change upon f+1 RC messages (experimental option) (#7838)

* Fix incorrect duration for THREE_MINUTES from 1 minute to 3 minutes

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* Round change upon f+1 RC messages (experimental option)

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* Update besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java

Co-authored-by: Matt Whitehead <matthew1001@hotmail.com>
Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com>

* revert an unrelated fix already merged to main

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* update return value description

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* fix logging level for couple of logs

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* Review changes , added tests

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* Merge and fix controller builder test context

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* minor fix to import missing class after merging main

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

* Add missing function header comments

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>

---------

Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>
Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com>
Co-authored-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io>
Co-authored-by: Matt Whitehead <matthew1001@hotmail.com>
Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com>
Co-authored-by: Jason Frame <jason.frame@consensys.net>
Co-authored-by: Matt Whitehead <matthew.whitehead@kaleido.io>
This commit is contained in:
Bhanu Pulluri
2025-01-31 11:41:19 -05:00
committed by GitHub
parent ac0265f3bd
commit 81e1ab9bf4
12 changed files with 385 additions and 51 deletions

View File

@@ -69,6 +69,7 @@ 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;
@@ -301,6 +302,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private final EvmOptions unstableEvmOptions = EvmOptions.create();
private final IpcOptions unstableIpcOptions = IpcOptions.create();
private final ChainPruningOptions unstableChainPruningOptions = ChainPruningOptions.create();
private final QBFTOptions unstableQbftOptions = QBFTOptions.create();
// stable CLI options
final DataStorageOptions dataStorageOptions = DataStorageOptions.create();
@@ -1162,6 +1164,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
.put("EVM Options", unstableEvmOptions)
.put("IPC Options", unstableIpcOptions)
.put("Chain Data Pruning Options", unstableChainPruningOptions)
.put("QBFT Options", unstableQbftOptions)
.build();
UnstableOptionsSubCommand.createUnstableOptions(commandLine, unstableOptions);
@@ -1794,6 +1797,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
.clock(Clock.systemUTC())
.isRevertReasonEnabled(isRevertReasonEnabled)
.storageProvider(storageProvider)
.isEarlyRoundChangeEnabled(unstableQbftOptions.isEarlyRoundChangeEnabled())
.gasLimitCalculator(
miningParametersSupplier.get().getTargetGasLimit().isPresent()
? new FrontierTargetingGasLimitCalculator()

View File

@@ -0,0 +1,49 @@
/*
* 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.cli.options.unstable;
import picocli.CommandLine;
/** Handles configuration options for QBFT consensus */
public class QBFTOptions {
/** Default constructor */
private QBFTOptions() {}
/**
* Create a new instance of QBFTOptions
*
* @return a new instance of QBFTOptions
*/
public static QBFTOptions create() {
return new QBFTOptions();
}
@CommandLine.Option(
names = {"--Xqbft-enable-early-round-change"},
description =
"Enable early round change upon receiving f+1 valid future Round Change messages from different validators (experimental)",
hidden = true)
private boolean enableEarlyRoundChange = false;
/**
* Is early round change enabled boolean.
*
* @return true if early round change is enabled
*/
public boolean isEarlyRoundChangeEnabled() {
return enableEarlyRoundChange;
}
}

View File

@@ -219,6 +219,9 @@ public abstract class BesuControllerBuilder implements MiningParameterOverrides
/** The transaction simulator */
protected TransactionSimulator transactionSimulator;
/** When enabled, round changes on f+1 RC messages from higher rounds */
protected boolean isEarlyRoundChangeEnabled = false;
/** Instantiates a new Besu controller builder. */
protected BesuControllerBuilder() {}
@@ -553,6 +556,17 @@ public abstract class BesuControllerBuilder implements MiningParameterOverrides
return this;
}
/**
* check if early round change is enabled when f+1 RC messages from higher rounds are received
*
* @param isEarlyRoundChangeEnabled whether to enable early round change
* @return the besu controller
*/
public BesuControllerBuilder isEarlyRoundChangeEnabled(final boolean isEarlyRoundChangeEnabled) {
this.isEarlyRoundChangeEnabled = isEarlyRoundChangeEnabled;
return this;
}
/**
* Build besu controller.
*

View File

@@ -245,23 +245,28 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder {
final MessageFactory messageFactory = new MessageFactory(nodeKey);
QbftBlockHeightManagerFactory qbftBlockHeightManagerFactory =
new QbftBlockHeightManagerFactory(
finalState,
new QbftRoundFactory(
finalState,
protocolContext,
bftProtocolSchedule,
minedBlockObservers,
messageValidatorFactory,
messageFactory,
bftExtraDataCodec().get()),
messageValidatorFactory,
messageFactory,
new ValidatorModeTransitionLogger(qbftForksSchedule));
qbftBlockHeightManagerFactory.isEarlyRoundChangeEnabled(isEarlyRoundChangeEnabled);
final BftEventHandler qbftController =
new QbftController(
blockchain,
finalState,
new QbftBlockHeightManagerFactory(
finalState,
new QbftRoundFactory(
finalState,
protocolContext,
bftProtocolSchedule,
minedBlockObservers,
messageValidatorFactory,
messageFactory,
bftExtraDataCodec().get()),
messageValidatorFactory,
messageFactory,
new ValidatorModeTransitionLogger(qbftForksSchedule)),
qbftBlockHeightManagerFactory,
gossiper,
duplicateMessageTracker,
futureMessageBuffer,

View File

@@ -280,6 +280,7 @@ public abstract class CommandTestAbstract {
when(mockControllerBuilder.isRevertReasonEnabled(false)).thenReturn(mockControllerBuilder);
when(mockControllerBuilder.isParallelTxProcessingEnabled(false))
.thenReturn(mockControllerBuilder);
when(mockControllerBuilder.isEarlyRoundChangeEnabled(false)).thenReturn(mockControllerBuilder);
when(mockControllerBuilder.storageProvider(any())).thenReturn(mockControllerBuilder);
when(mockControllerBuilder.gasLimitCalculator(any())).thenReturn(mockControllerBuilder);
when(mockControllerBuilder.requiredBlocks(any())).thenReturn(mockControllerBuilder);

View File

@@ -43,6 +43,16 @@ public class BftHelpers {
return Util.fastDivCeiling(2 * validatorCount, 3);
}
/**
* Calculate required future RC messages count quorum for a round change.
*
* @param validatorCount the validator count
* @return Required number of future round change messages to reach quorum for a round change.
*/
public static int calculateRequiredFutureRCQuorum(final int validatorCount) {
return (validatorCount - 1) / 3 + 1;
}
/**
* Prepare message count for quorum.
*

View File

@@ -83,8 +83,7 @@ public class RoundTimer {
// Once we are up to round 2 start logging round expiries
if (round.getRoundNumber() >= 2) {
LOG.info(
"BFT round {} expired. Moved to round {} which will expire in {} seconds",
round.getRoundNumber() - 1,
"Moved to round {} which will expire in {} seconds",
round.getRoundNumber(),
(expiryTime / 1000));
}

View File

@@ -63,4 +63,39 @@ public class BftHelpersTest {
public void calculateRequiredValidatorQuorum20Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredValidatorQuorum(20)).isEqualTo(14);
}
@Test
public void calculateRequiredFutureRCQuorum4Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(4)).isEqualTo(2);
}
@Test
public void calculateRequiredFutureRCQuorum6Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(6)).isEqualTo(2);
}
@Test
public void calculateRequiredFutureRCQuorum7Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(7)).isEqualTo(3);
}
@Test
public void calculateRequiredFutureRCQuorum9Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(9)).isEqualTo(3);
}
@Test
public void calculateRequiredFutureRCQuorum10Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(10)).isEqualTo(4);
}
@Test
public void calculateRequiredFutureRCQuorum13Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(13)).isEqualTo(5);
}
@Test
public void calculateRequiredFutureRCQuorum15Validator() {
Assertions.assertThat(BftHelpers.calculateRequiredFutureRCQuorum(15)).isEqualTo(5);
}
}

View File

@@ -68,6 +68,7 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
private Optional<PreparedCertificate> latestPreparedCertificate = Optional.empty();
private Optional<QbftRound> currentRound = Optional.empty();
private boolean isEarlyRoundChangeEnabled = false;
/**
* Instantiates a new Qbft block height manager.
@@ -115,6 +116,39 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
finalState.getBlockTimer().startTimer(roundIdentifier, parentHeader);
}
/**
* 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 BlockHeader parentHeader,
final BftFinalState 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()) {
@@ -227,23 +261,36 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
return;
}
doRoundChange(qbftRound.getRoundIdentifier().getRoundNumber() + 1);
}
private synchronized void doRoundChange(final int newRoundNumber) {
if (currentRound.isPresent()
&& currentRound.get().getRoundIdentifier().getRoundNumber() >= newRoundNumber) {
return;
}
LOG.debug(
"Round has expired, creating PreparedCertificate and notifying peers. round={}",
qbftRound.getRoundIdentifier());
"Round has expired or changing based on RC quorum, creating PreparedCertificate and notifying peers. round={}",
currentRound.get().getRoundIdentifier());
final Optional<PreparedCertificate> preparedCertificate =
qbftRound.constructPreparedCertificate();
currentRound.get().constructPreparedCertificate();
if (preparedCertificate.isPresent()) {
latestPreparedCertificate = preparedCertificate;
}
startNewRound(qbftRound.getRoundIdentifier().getRoundNumber() + 1);
qbftRound = currentRound.get();
startNewRound(newRoundNumber);
if (currentRound.isEmpty()) {
LOG.info("Failed to start round ");
return;
}
QbftRound qbftRoundNew = currentRound.get();
try {
final RoundChange localRoundChange =
messageFactory.createRoundChange(
qbftRound.getRoundIdentifier(), latestPreparedCertificate);
qbftRoundNew.getRoundIdentifier(), latestPreparedCertificate);
// Its possible the locally created RoundChange triggers the transmission of a NewRound
// message - so it must be handled accordingly.
@@ -252,7 +299,7 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
LOG.warn("Failed to create signed RoundChange message.", e);
}
transmitter.multicastRoundChange(qbftRound.getRoundIdentifier(), latestPreparedCertificate);
transmitter.multicastRoundChange(qbftRoundNew.getRoundIdentifier(), latestPreparedCertificate);
}
@Override
@@ -333,24 +380,55 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
final Optional<Collection<RoundChange>> result =
roundChangeManager.appendRoundChangeMessage(message);
if (result.isPresent()) {
LOG.debug(
"Received sufficient RoundChange messages to change round to targetRound={}",
targetRound);
if (messageAge == MessageAge.FUTURE_ROUND) {
startNewRound(targetRound.getRoundNumber());
}
final RoundChangeArtifacts roundChangeMetadata = RoundChangeArtifacts.create(result.get());
if (finalState.isLocalNodeProposerForRound(targetRound)) {
if (currentRound.isEmpty()) {
startNewRound(0);
if (!isEarlyRoundChangeEnabled) {
if (result.isPresent()) {
LOG.debug(
"Received sufficient RoundChange messages to change round to targetRound={}",
targetRound);
if (messageAge == MessageAge.FUTURE_ROUND) {
startNewRound(targetRound.getRoundNumber());
}
final RoundChangeArtifacts roundChangeMetadata = RoundChangeArtifacts.create(result.get());
if (finalState.isLocalNodeProposerForRound(targetRound)) {
if (currentRound.isEmpty()) {
startNewRound(0);
}
currentRound
.get()
.startRoundWith(roundChangeMetadata, TimeUnit.MILLISECONDS.toSeconds(clock.millis()));
}
}
} else {
if (currentRound.isEmpty()) {
startNewRound(0);
}
int currentRoundNumber = currentRound.get().getRoundIdentifier().getRoundNumber();
// If this node is proposer for the current round, check if quorum is achieved for RC messages
// aiming this round
if (targetRound.getRoundNumber() == currentRoundNumber
&& finalState.isLocalNodeProposerForRound(targetRound)
&& result.isPresent()) {
final RoundChangeArtifacts roundChangeMetadata = RoundChangeArtifacts.create(result.get());
currentRound
.get()
.startRoundWith(roundChangeMetadata, TimeUnit.MILLISECONDS.toSeconds(clock.millis()));
}
// check if f+1 RC messages for future rounds are received
QbftRound qbftRound = currentRound.get();
Optional<Integer> nextHigherRound =
roundChangeManager.futureRCQuorumReceived(qbftRound.getRoundIdentifier());
if (nextHigherRound.isPresent()) {
LOG.info(
"Received sufficient RoundChange messages to change round to targetRound={}",
nextHigherRound.get());
doRoundChange(nextHigherRound.get());
}
}
}

View File

@@ -34,6 +34,7 @@ public class QbftBlockHeightManagerFactory {
private final MessageValidatorFactory messageValidatorFactory;
private final MessageFactory messageFactory;
private final ValidatorModeTransitionLogger validatorModeTransitionLogger;
private boolean isEarlyRoundChangeEnabled = false;
/**
* Instantiates a new Qbft block height manager factory.
@@ -75,22 +76,60 @@ public class QbftBlockHeightManagerFactory {
}
}
/**
* Sets early round change enabled.
*
* @param isEarlyRoundChangeEnabled the is early round change enabled
*/
public void isEarlyRoundChangeEnabled(final boolean isEarlyRoundChangeEnabled) {
this.isEarlyRoundChangeEnabled = isEarlyRoundChangeEnabled;
}
private BaseQbftBlockHeightManager createNoOpBlockHeightManager(final BlockHeader parentHeader) {
return new NoOpBlockHeightManager(parentHeader);
}
private BaseQbftBlockHeightManager createFullBlockHeightManager(final BlockHeader parentHeader) {
return new QbftBlockHeightManager(
parentHeader,
finalState,
new RoundChangeManager(
BftHelpers.calculateRequiredValidatorQuorum(finalState.getValidators().size()),
messageValidatorFactory.createRoundChangeMessageValidator(
parentHeader.getNumber() + 1L, parentHeader),
finalState.getLocalAddress()),
roundFactory,
finalState.getClock(),
messageValidatorFactory,
messageFactory);
QbftBlockHeightManager qbftBlockHeightManager;
RoundChangeManager roundChangeManager;
if (isEarlyRoundChangeEnabled) {
roundChangeManager =
new RoundChangeManager(
BftHelpers.calculateRequiredValidatorQuorum(finalState.getValidators().size()),
BftHelpers.calculateRequiredFutureRCQuorum(finalState.getValidators().size()),
messageValidatorFactory.createRoundChangeMessageValidator(
parentHeader.getNumber() + 1L, parentHeader),
finalState.getLocalAddress());
qbftBlockHeightManager =
new QbftBlockHeightManager(
parentHeader,
finalState,
roundChangeManager,
roundFactory,
finalState.getClock(),
messageValidatorFactory,
messageFactory,
true);
} else {
roundChangeManager =
new RoundChangeManager(
BftHelpers.calculateRequiredValidatorQuorum(finalState.getValidators().size()),
messageValidatorFactory.createRoundChangeMessageValidator(
parentHeader.getNumber() + 1L, parentHeader),
finalState.getLocalAddress());
qbftBlockHeightManager =
new QbftBlockHeightManager(
parentHeader,
finalState,
roundChangeManager,
roundFactory,
finalState.getClock(),
messageValidatorFactory,
messageFactory);
}
return qbftBlockHeightManager;
}
}

View File

@@ -22,6 +22,7 @@ import org.hyperledger.besu.datatypes.Address;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
@@ -75,7 +76,7 @@ public class RoundChangeManager {
*
* @return the boolean
*/
public boolean roundChangeReady() {
public boolean roundChangeQuorumReceived() {
return receivedMessages.size() >= quorum && !actioned;
}
@@ -85,7 +86,7 @@ public class RoundChangeManager {
* @return the collection
*/
public Collection<RoundChange> createRoundChangeCertificate() {
if (roundChangeReady()) {
if (roundChangeQuorumReceived()) {
actioned = true;
return receivedMessages.values();
} else {
@@ -104,6 +105,7 @@ public class RoundChangeManager {
private final Map<Address, ConsensusRoundIdentifier> roundSummary = Maps.newHashMap();
private final long quorum;
private long rcQuorum;
private final RoundChangeMessageValidator roundChangeMessageValidator;
private final Address localAddress;
@@ -123,6 +125,23 @@ public class RoundChangeManager {
this.localAddress = localAddress;
}
/**
* Instantiates a new Round change manager.
*
* @param quorum the quorum
* @param rcQuorum quorum for round change messages
* @param roundChangeMessageValidator the round change message validator
* @param localAddress this node's address
*/
public RoundChangeManager(
final long quorum,
final long rcQuorum,
final RoundChangeMessageValidator roundChangeMessageValidator,
final Address localAddress) {
this(quorum, roundChangeMessageValidator, localAddress);
this.rcQuorum = rcQuorum;
}
/**
* Store the latest round for a node, and if chain is stalled log a summary of which round each
* address is on
@@ -130,6 +149,10 @@ public class RoundChangeManager {
* @param message the round-change message that has just been received
*/
public void storeAndLogRoundChangeSummary(final RoundChange message) {
if (!isMessageValid(message)) {
LOG.info("RoundChange message is invalid .");
return;
}
roundSummary.put(message.getAuthor(), message.getRoundIdentifier());
if (roundChangeCache.keySet().stream()
.findFirst()
@@ -147,6 +170,39 @@ public class RoundChangeManager {
}
}
/**
* Checks if a quorum of round change messages has been received for a round higher than the
* current round
*
* @param currentRoundIdentifier the current round identifier
* @return the next higher round number if quorum is reached, otherwise empty Optional
*/
public Optional<Integer> futureRCQuorumReceived(
final ConsensusRoundIdentifier currentRoundIdentifier) {
// Iterate through elements of round summary, identify ones with round number higher than
// current,
// tracking minimum of those and return the next higher round number if quorum is reached
// Filter out entries with round number greater than current round
// and collect their round numbers
Map<Address, Integer> higherRounds =
roundSummary.entrySet().stream()
.filter(entry -> isAFutureRound(entry.getValue(), currentRoundIdentifier))
.collect(
Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getRoundNumber()));
LOG.debug("Higher rounds size ={} rcquorum = {}", higherRounds.size(), rcQuorum);
// Check if we have at least f + 1 validators at higher rounds
if (higherRounds.size() >= rcQuorum) {
// Find the minimum round that is greater than the current round
return Optional.of(higherRounds.values().stream().min(Integer::compareTo).orElseThrow());
}
// If quorum is not reached, return empty Optional
return Optional.empty();
}
/**
* Adds the round message to this manager and return a certificate if it passes the threshold
*
@@ -163,7 +219,7 @@ public class RoundChangeManager {
final RoundChangeStatus roundChangeStatus = storeRoundChangeMessage(msg);
if (roundChangeStatus.roundChangeReady()) {
if (roundChangeStatus.roundChangeQuorumReceived()) {
return Optional.of(roundChangeStatus.createRoundChangeCertificate());
}
@@ -198,4 +254,9 @@ public class RoundChangeManager {
final ConsensusRoundIdentifier left, final ConsensusRoundIdentifier right) {
return left.getRoundNumber() < right.getRoundNumber();
}
private boolean isAFutureRound(
final ConsensusRoundIdentifier left, final ConsensusRoundIdentifier right) {
return left.getRoundNumber() > right.getRoundNumber();
}
}

View File

@@ -53,6 +53,7 @@ import org.hyperledger.besu.consensus.qbft.core.payload.MessageFactory;
import org.hyperledger.besu.consensus.qbft.core.validation.FutureRoundProposalMessageValidator;
import org.hyperledger.besu.consensus.qbft.core.validation.MessageValidator;
import org.hyperledger.besu.consensus.qbft.core.validation.MessageValidatorFactory;
import org.hyperledger.besu.consensus.qbft.core.validation.RoundChangeMessageValidator;
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
@@ -138,7 +139,7 @@ public class QbftBlockHeightManagerTest {
@BeforeEach
public void setup() {
for (int i = 0; i < 3; i++) {
for (int i = 0; i <= 3; i++) {
final NodeKey nodeKey = NodeKeyUtils.generate();
validators.add(Util.publicKeyToAddress(nodeKey.getPublicKey()));
validatorMessageFactory.add(new MessageFactory(nodeKey));
@@ -602,4 +603,42 @@ public class QbftBlockHeightManagerTest {
verify(blockTimer, times(0)).getEmptyBlockPeriodSeconds();
verify(blockTimer, times(0)).getBlockPeriodSeconds();
}
@Test
public void roundChangeTriggeredUponReceivingFPlusOneRoundChanges() {
final ConsensusRoundIdentifier futureRoundIdentifier1 = createFrom(roundIdentifier, 0, +2);
final ConsensusRoundIdentifier futureRoundIdentifier2 = createFrom(roundIdentifier, 0, +3);
final RoundChange roundChange1 =
validatorMessageFactory.get(0).createRoundChange(futureRoundIdentifier1, Optional.empty());
final RoundChange roundChange2 =
validatorMessageFactory.get(1).createRoundChange(futureRoundIdentifier2, Optional.empty());
RoundChangeMessageValidator roundChangeMessageValidator =
mock(RoundChangeMessageValidator.class);
when(roundChangeMessageValidator.validate(any())).thenReturn(true);
// Instantiate the real RoundChangeManager
final RoundChangeManager roundChangeManager =
new RoundChangeManager(3, 2, roundChangeMessageValidator, validators.get(2));
when(finalState.isLocalNodeProposerForRound(any())).thenReturn(false);
final QbftBlockHeightManager manager =
new QbftBlockHeightManager(
headerTestFixture.buildHeader(),
finalState,
roundChangeManager,
roundFactory,
clock,
messageValidatorFactory,
validatorMessageFactory.get(2),
true); // Enable early round change
manager.handleRoundChangePayload(roundChange1);
manager.handleRoundChangePayload(roundChange2);
verify(roundFactory, times(1))
.createNewRound(any(), eq(futureRoundIdentifier1.getRoundNumber()));
}
}