mirror of
https://github.com/vacp2p/status-linea-besu.git
synced 2026-01-08 23:08:15 -05:00
Implemented support for emptyBlockPeriodSeconds in QBFT (Issue #3810) Introduces experimental xemptyblockperiodseconds genesis config option for producing empty blocks at a specific interval independently of the value of the existing blockperiodseconds setting. https://github.com/hyperledger/besu/issues/3810 --------- Signed-off-by: Antonio Mota <antonio.mota@citi.com> Signed-off-by: amsmota <amsmota@gmail.com>
This commit is contained in:
@@ -37,6 +37,8 @@ public class BlockTimer {
|
||||
private Optional<ScheduledFuture<?>> currentTimerTask;
|
||||
private final BftEventQueue queue;
|
||||
private final Clock clock;
|
||||
private long blockPeriodSeconds;
|
||||
private long emptyBlockPeriodSeconds;
|
||||
|
||||
/**
|
||||
* Construct a BlockTimer with primed executor service ready to start timers
|
||||
@@ -56,6 +58,8 @@ public class BlockTimer {
|
||||
this.bftExecutors = bftExecutors;
|
||||
this.currentTimerTask = Optional.empty();
|
||||
this.clock = clock;
|
||||
this.blockPeriodSeconds = 0;
|
||||
this.emptyBlockPeriodSeconds = 0;
|
||||
}
|
||||
|
||||
/** Cancels the current running round timer if there is one */
|
||||
@@ -83,13 +87,11 @@ public class BlockTimer {
|
||||
final ConsensusRoundIdentifier round, final BlockHeader chainHeadHeader) {
|
||||
cancelTimer();
|
||||
|
||||
final long now = clock.millis();
|
||||
final long expiryTime;
|
||||
|
||||
// Experimental option for test scenarios only. Not for production use.
|
||||
final long blockPeriodMilliseconds =
|
||||
forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodMilliseconds();
|
||||
|
||||
if (blockPeriodMilliseconds > 0) {
|
||||
// Experimental mode for setting < 1 second block periods e.g. for CI/CD pipelines
|
||||
// running tests against Besu
|
||||
@@ -99,12 +101,60 @@ public class BlockTimer {
|
||||
blockPeriodMilliseconds);
|
||||
} else {
|
||||
// absolute time when the timer is supposed to expire
|
||||
final int blockPeriodSeconds =
|
||||
final int currentBlockPeriodSeconds =
|
||||
forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodSeconds();
|
||||
final long minimumTimeBetweenBlocksMillis = blockPeriodSeconds * 1000L;
|
||||
final long minimumTimeBetweenBlocksMillis = currentBlockPeriodSeconds * 1000L;
|
||||
expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis;
|
||||
}
|
||||
|
||||
setBlockTimes(round);
|
||||
|
||||
startTimer(round, expiryTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the empty block timer is expired
|
||||
*
|
||||
* @param chainHeadHeader The header of the chain head
|
||||
* @param currentTimeInMillis The current time
|
||||
* @return a boolean value
|
||||
*/
|
||||
public synchronized boolean checkEmptyBlockExpired(
|
||||
final BlockHeader chainHeadHeader, final long currentTimeInMillis) {
|
||||
final long emptyBlockPeriodExpiryTime =
|
||||
(chainHeadHeader.getTimestamp() + emptyBlockPeriodSeconds) * 1000;
|
||||
|
||||
if (currentTimeInMillis > emptyBlockPeriodExpiryTime) {
|
||||
LOG.debug("Empty Block expired");
|
||||
return true;
|
||||
}
|
||||
LOG.debug("Empty Block NOT expired");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the empty block timer
|
||||
*
|
||||
* @param roundIdentifier The current round identifier
|
||||
* @param chainHeadHeader The header of the chain head
|
||||
* @param currentTimeInMillis The current time
|
||||
*/
|
||||
public void resetTimerForEmptyBlock(
|
||||
final ConsensusRoundIdentifier roundIdentifier,
|
||||
final BlockHeader chainHeadHeader,
|
||||
final long currentTimeInMillis) {
|
||||
final long emptyBlockPeriodExpiryTime =
|
||||
(chainHeadHeader.getTimestamp() + emptyBlockPeriodSeconds) * 1000;
|
||||
final long nextBlockPeriodExpiryTime = currentTimeInMillis + blockPeriodSeconds * 1000;
|
||||
|
||||
startTimer(roundIdentifier, Math.min(emptyBlockPeriodExpiryTime, nextBlockPeriodExpiryTime));
|
||||
}
|
||||
|
||||
private synchronized void startTimer(
|
||||
final ConsensusRoundIdentifier round, final long expiryTime) {
|
||||
cancelTimer();
|
||||
final long now = clock.millis();
|
||||
|
||||
if (expiryTime > now) {
|
||||
final long delay = expiryTime - now;
|
||||
|
||||
@@ -117,4 +167,29 @@ public class BlockTimer {
|
||||
queue.add(new BlockTimerExpiry(round));
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void setBlockTimes(final ConsensusRoundIdentifier round) {
|
||||
final BftConfigOptions currentConfigOptions =
|
||||
forksSchedule.getFork(round.getSequenceNumber()).getValue();
|
||||
this.blockPeriodSeconds = currentConfigOptions.getBlockPeriodSeconds();
|
||||
this.emptyBlockPeriodSeconds = currentConfigOptions.getEmptyBlockPeriodSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Block Period Seconds
|
||||
*
|
||||
* @return the Block Period Seconds
|
||||
*/
|
||||
public synchronized long getBlockPeriodSeconds() {
|
||||
return blockPeriodSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Empty Block Period Seconds
|
||||
*
|
||||
* @return the Empty Block Period Seconds
|
||||
*/
|
||||
public synchronized long getEmptyBlockPeriodSeconds() {
|
||||
return emptyBlockPeriodSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.util.Optional;
|
||||
public class MutableBftConfigOptions implements BftConfigOptions {
|
||||
private long epochLength;
|
||||
private int blockPeriodSeconds;
|
||||
private int emptyBlockPeriodSeconds;
|
||||
private long blockPeriodMilliseconds;
|
||||
private int requestTimeoutSeconds;
|
||||
private int gossipedHistoryLimit;
|
||||
@@ -49,6 +50,7 @@ public class MutableBftConfigOptions implements BftConfigOptions {
|
||||
public MutableBftConfigOptions(final BftConfigOptions bftConfigOptions) {
|
||||
this.epochLength = bftConfigOptions.getEpochLength();
|
||||
this.blockPeriodSeconds = bftConfigOptions.getBlockPeriodSeconds();
|
||||
this.emptyBlockPeriodSeconds = bftConfigOptions.getEmptyBlockPeriodSeconds();
|
||||
this.blockPeriodMilliseconds = bftConfigOptions.getBlockPeriodMilliseconds();
|
||||
this.requestTimeoutSeconds = bftConfigOptions.getRequestTimeoutSeconds();
|
||||
this.gossipedHistoryLimit = bftConfigOptions.getGossipedHistoryLimit();
|
||||
@@ -70,6 +72,11 @@ public class MutableBftConfigOptions implements BftConfigOptions {
|
||||
return blockPeriodSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEmptyBlockPeriodSeconds() {
|
||||
return emptyBlockPeriodSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBlockPeriodMilliseconds() {
|
||||
return blockPeriodMilliseconds;
|
||||
@@ -138,6 +145,15 @@ public class MutableBftConfigOptions implements BftConfigOptions {
|
||||
this.blockPeriodSeconds = blockPeriodSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets empty block period seconds.
|
||||
*
|
||||
* @param emptyBlockPeriodSeconds the empty block period seconds
|
||||
*/
|
||||
public void setEmptyBlockPeriodSeconds(final int emptyBlockPeriodSeconds) {
|
||||
this.emptyBlockPeriodSeconds = emptyBlockPeriodSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets block period milliseconds. Experimental for test scenarios. Not for use on production
|
||||
* systems.
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ForksScheduleFactoryTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void throwsErrorIfHasForkForGenesisBlock() {
|
||||
final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT;
|
||||
final BftFork fork = createFork(0, 10);
|
||||
final BftFork fork = createFork(0, 10, 30);
|
||||
final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class);
|
||||
|
||||
assertThatThrownBy(
|
||||
@@ -49,9 +49,9 @@ public class ForksScheduleFactoryTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void throwsErrorIfHasForksWithDuplicateBlock() {
|
||||
final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT;
|
||||
final BftFork fork1 = createFork(1, 10);
|
||||
final BftFork fork2 = createFork(1, 20);
|
||||
final BftFork fork3 = createFork(2, 30);
|
||||
final BftFork fork1 = createFork(1, 10, 30);
|
||||
final BftFork fork2 = createFork(1, 20, 60);
|
||||
final BftFork fork3 = createFork(2, 30, 90);
|
||||
final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class);
|
||||
|
||||
assertThatThrownBy(
|
||||
@@ -66,12 +66,12 @@ public class ForksScheduleFactoryTest {
|
||||
public void createsScheduleUsingSpecCreator() {
|
||||
final BftConfigOptions genesisConfigOptions = JsonBftConfigOptions.DEFAULT;
|
||||
final ForkSpec<BftConfigOptions> genesisForkSpec = new ForkSpec<>(0, genesisConfigOptions);
|
||||
final BftFork fork1 = createFork(1, 10);
|
||||
final BftFork fork2 = createFork(2, 20);
|
||||
final BftFork fork1 = createFork(1, 10, 20);
|
||||
final BftFork fork2 = createFork(2, 20, 40);
|
||||
final SpecCreator<BftConfigOptions, BftFork> specCreator = Mockito.mock(SpecCreator.class);
|
||||
|
||||
final BftConfigOptions configOptions1 = createBftConfigOptions(10);
|
||||
final BftConfigOptions configOptions2 = createBftConfigOptions(20);
|
||||
final BftConfigOptions configOptions1 = createBftConfigOptions(10, 30);
|
||||
final BftConfigOptions configOptions2 = createBftConfigOptions(20, 60);
|
||||
when(specCreator.create(genesisForkSpec, fork1)).thenReturn(configOptions1);
|
||||
when(specCreator.create(new ForkSpec<>(1, configOptions1), fork2)).thenReturn(configOptions2);
|
||||
|
||||
@@ -82,18 +82,25 @@ public class ForksScheduleFactoryTest {
|
||||
assertThat(schedule.getFork(2)).isEqualTo(new ForkSpec<>(2, configOptions2));
|
||||
}
|
||||
|
||||
private MutableBftConfigOptions createBftConfigOptions(final int blockPeriodSeconds) {
|
||||
private MutableBftConfigOptions createBftConfigOptions(
|
||||
final int blockPeriodSeconds, final int emptyBlockPeriodSeconds) {
|
||||
final MutableBftConfigOptions bftConfigOptions =
|
||||
new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT);
|
||||
bftConfigOptions.setBlockPeriodSeconds(blockPeriodSeconds);
|
||||
bftConfigOptions.setEmptyBlockPeriodSeconds(emptyBlockPeriodSeconds);
|
||||
return bftConfigOptions;
|
||||
}
|
||||
|
||||
private BftFork createFork(final long block, final long blockPeriodSeconds) {
|
||||
private BftFork createFork(
|
||||
final long block, final long blockPeriodSeconds, final long emptyBlockPeriodSeconds) {
|
||||
return new BftFork(
|
||||
JsonUtil.objectNodeFromMap(
|
||||
Map.of(
|
||||
BftFork.FORK_BLOCK_KEY, block,
|
||||
BftFork.BLOCK_PERIOD_SECONDS_KEY, blockPeriodSeconds)));
|
||||
BftFork.FORK_BLOCK_KEY,
|
||||
block,
|
||||
BftFork.BLOCK_PERIOD_SECONDS_KEY,
|
||||
blockPeriodSeconds,
|
||||
BftFork.EMPTY_BLOCK_PERIOD_SECONDS_KEY,
|
||||
emptyBlockPeriodSeconds)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,18 @@ public class BlockTimerTest {
|
||||
@Test
|
||||
public void startTimerSchedulesCorrectlyWhenExpiryIsInTheFuture() {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
|
||||
final long NOW_MILLIS = 505_000L;
|
||||
final long BLOCK_TIME_STAMP = 500L;
|
||||
final long EXPECTED_DELAY = 10_000L;
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS)));
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
|
||||
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
|
||||
|
||||
@@ -104,12 +110,18 @@ public class BlockTimerTest {
|
||||
@Test
|
||||
public void aBlockTimerExpiryEventIsAddedToTheQueueOnExpiry() throws InterruptedException {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 1;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 10;
|
||||
final long NOW_MILLIS = 300_500L;
|
||||
final long BLOCK_TIME_STAMP = 300;
|
||||
final long EXPECTED_DELAY = 500;
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS)));
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
when(mockClock.millis()).thenReturn(NOW_MILLIS);
|
||||
|
||||
final BlockHeader header =
|
||||
@@ -149,11 +161,17 @@ public class BlockTimerTest {
|
||||
@Test
|
||||
public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsEqualToNow() {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
|
||||
final long NOW_MILLIS = 515_000L;
|
||||
final long BLOCK_TIME_STAMP = 500;
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS)));
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
|
||||
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
|
||||
|
||||
@@ -179,11 +197,17 @@ public class BlockTimerTest {
|
||||
@Test
|
||||
public void eventIsImmediatelyAddedToTheQueueIfAbsoluteExpiryIsInThePast() {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
|
||||
final long NOW_MILLIS = 520_000L;
|
||||
final long BLOCK_TIME_STAMP = 500L;
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS)));
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
|
||||
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
|
||||
|
||||
@@ -209,11 +233,17 @@ public class BlockTimerTest {
|
||||
@Test
|
||||
public void startTimerCancelsExistingTimer() {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
|
||||
final long NOW_MILLIS = 500_000L;
|
||||
final long BLOCK_TIME_STAMP = 500L;
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS)));
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
|
||||
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
|
||||
|
||||
@@ -237,11 +267,17 @@ public class BlockTimerTest {
|
||||
@Test
|
||||
public void runningFollowsTheStateOfTheTimer() {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
|
||||
final long NOW_MILLIS = 500_000L;
|
||||
final long BLOCK_TIME_STAMP = 500L;
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(new ForkSpec<>(0, createBftFork(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS)));
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
|
||||
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
|
||||
|
||||
@@ -263,10 +299,42 @@ public class BlockTimerTest {
|
||||
assertThat(timer.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
private BftConfigOptions createBftFork(final int blockPeriodSeconds) {
|
||||
@Test
|
||||
public void checkBlockTimerEmptyAndNonEmptyPeriodSecods() {
|
||||
final int MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS = 15;
|
||||
final int MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS = 60;
|
||||
final long BLOCK_TIME_STAMP = 500L;
|
||||
final ConsensusRoundIdentifier round =
|
||||
new ConsensusRoundIdentifier(0xFEDBCA9876543210L, 0x12345678);
|
||||
final BlockHeader header =
|
||||
new BlockHeaderTestFixture().timestamp(BLOCK_TIME_STAMP).buildHeader();
|
||||
final ScheduledFuture<?> mockedFuture = mock(ScheduledFuture.class);
|
||||
Mockito.<ScheduledFuture<?>>when(
|
||||
bftExecutors.scheduleTask(any(Runnable.class), anyLong(), any()))
|
||||
.thenReturn(mockedFuture);
|
||||
|
||||
when(mockForksSchedule.getFork(anyLong()))
|
||||
.thenReturn(
|
||||
new ForkSpec<>(
|
||||
0,
|
||||
createBftFork(
|
||||
MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS,
|
||||
MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS)));
|
||||
|
||||
final BlockTimer timer = new BlockTimer(mockQueue, mockForksSchedule, bftExecutors, mockClock);
|
||||
timer.startTimer(round, header);
|
||||
|
||||
assertThat(timer.getBlockPeriodSeconds()).isEqualTo(MINIMAL_TIME_BETWEEN_BLOCKS_SECONDS);
|
||||
assertThat(timer.getEmptyBlockPeriodSeconds())
|
||||
.isEqualTo(MINIMAL_TIME_BETWEEN_EMPTY_BLOCKS_SECONDS);
|
||||
}
|
||||
|
||||
private BftConfigOptions createBftFork(
|
||||
final int blockPeriodSeconds, final int emptyBlockPeriodSeconds) {
|
||||
final MutableBftConfigOptions bftConfigOptions =
|
||||
new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT);
|
||||
bftConfigOptions.setBlockPeriodSeconds(blockPeriodSeconds);
|
||||
bftConfigOptions.setEmptyBlockPeriodSeconds(emptyBlockPeriodSeconds);
|
||||
return bftConfigOptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public class QbftForksSchedulesFactory {
|
||||
new MutableQbftConfigOptions(lastSpec.getValue());
|
||||
|
||||
fork.getBlockPeriodSeconds().ifPresent(bftConfigOptions::setBlockPeriodSeconds);
|
||||
fork.getEmptyBlockPeriodSeconds().ifPresent(bftConfigOptions::setEmptyBlockPeriodSeconds);
|
||||
fork.getBlockPeriodMilliseconds().ifPresent(bftConfigOptions::setBlockPeriodMilliseconds);
|
||||
fork.getBlockRewardWei().ifPresent(bftConfigOptions::setBlockRewardWei);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.hyperledger.besu.consensus.qbft.payload.MessageFactory;
|
||||
import org.hyperledger.besu.consensus.qbft.validation.FutureRoundProposalMessageValidator;
|
||||
import org.hyperledger.besu.consensus.qbft.validation.MessageValidatorFactory;
|
||||
import org.hyperledger.besu.datatypes.Address;
|
||||
import org.hyperledger.besu.ethereum.core.Block;
|
||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||
import org.hyperledger.besu.plugin.services.securitymodule.SecurityModuleException;
|
||||
|
||||
@@ -130,19 +131,57 @@ public class QbftBlockHeightManager implements BaseQbftBlockHeightManager {
|
||||
|
||||
logValidatorChanges(qbftRound);
|
||||
|
||||
if (roundIdentifier.equals(qbftRound.getRoundIdentifier())) {
|
||||
buildBlockAndMaybePropose(roundIdentifier, qbftRound);
|
||||
} else {
|
||||
LOG.trace(
|
||||
"Block timer expired for a round ({}) other than current ({})",
|
||||
roundIdentifier,
|
||||
qbftRound.getRoundIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
private void buildBlockAndMaybePropose(
|
||||
final ConsensusRoundIdentifier roundIdentifier, final QbftRound qbftRound) {
|
||||
|
||||
// mining will be checked against round 0 as the current round is initialised to 0 above
|
||||
final boolean isProposer =
|
||||
finalState.isLocalNodeProposerForRound(qbftRound.getRoundIdentifier());
|
||||
|
||||
if (isProposer) {
|
||||
if (roundIdentifier.equals(qbftRound.getRoundIdentifier())) {
|
||||
final long headerTimeStampSeconds = Math.round(clock.millis() / 1000D);
|
||||
qbftRound.createAndSendProposalMessage(headerTimeStampSeconds);
|
||||
if (!isProposer) {
|
||||
// nothing to do here...
|
||||
LOG.trace("This node is not a proposer so it will not send a proposal: " + roundIdentifier);
|
||||
return;
|
||||
}
|
||||
|
||||
final long headerTimeStampSeconds = Math.round(clock.millis() / 1000D);
|
||||
final Block block = qbftRound.createBlock(headerTimeStampSeconds);
|
||||
final boolean blockHasTransactions = !block.getBody().getTransactions().isEmpty();
|
||||
if (blockHasTransactions) {
|
||||
LOG.trace(
|
||||
"Block has transactions and this node is a proposer so it will send a proposal: "
|
||||
+ roundIdentifier);
|
||||
qbftRound.updateStateWithProposalAndTransmit(block);
|
||||
|
||||
} else {
|
||||
// handle the block times period
|
||||
final long currentTimeInMillis = finalState.getClock().millis();
|
||||
boolean emptyBlockExpired =
|
||||
finalState.getBlockTimer().checkEmptyBlockExpired(parentHeader, currentTimeInMillis);
|
||||
if (emptyBlockExpired) {
|
||||
LOG.trace(
|
||||
"Block has no transactions and this node is a proposer so it will send a proposal: "
|
||||
+ roundIdentifier);
|
||||
qbftRound.updateStateWithProposalAndTransmit(block);
|
||||
} else {
|
||||
LOG.trace(
|
||||
"Block timer expired for a round ({}) other than current ({})",
|
||||
roundIdentifier,
|
||||
qbftRound.getRoundIdentifier());
|
||||
"Block has no transactions but emptyBlockPeriodSeconds did not expired yet: "
|
||||
+ roundIdentifier);
|
||||
finalState
|
||||
.getBlockTimer()
|
||||
.resetTimerForEmptyBlock(roundIdentifier, parentHeader, currentTimeInMillis);
|
||||
finalState.getRoundTimer().cancelTimer();
|
||||
currentRound = Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,17 +132,14 @@ public class QbftRound {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send proposal message.
|
||||
* Create a block
|
||||
*
|
||||
* @param headerTimeStampSeconds the header time stamp seconds
|
||||
* @param headerTimeStampSeconds of the block
|
||||
* @return a Block
|
||||
*/
|
||||
public void createAndSendProposalMessage(final long headerTimeStampSeconds) {
|
||||
public Block createBlock(final long headerTimeStampSeconds) {
|
||||
LOG.debug("Creating proposed block. round={}", roundState.getRoundIdentifier());
|
||||
final Block block =
|
||||
blockCreator.createBlock(headerTimeStampSeconds, this.parentHeader).getBlock();
|
||||
|
||||
LOG.trace("Creating proposed block blockHeader={}", block.getHeader());
|
||||
updateStateWithProposalAndTransmit(block, emptyList(), emptyList());
|
||||
return blockCreator.createBlock(headerTimeStampSeconds, this.parentHeader).getBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,6 +169,15 @@ public class QbftRound {
|
||||
bestPreparedCertificate.map(PreparedCertificate::getPrepares).orElse(emptyList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state with proposal and transmit.
|
||||
*
|
||||
* @param block the block
|
||||
*/
|
||||
protected void updateStateWithProposalAndTransmit(final Block block) {
|
||||
updateStateWithProposalAndTransmit(block, emptyList(), emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state with proposal and transmit.
|
||||
*
|
||||
|
||||
@@ -37,4 +37,24 @@ public class MutableQbftConfigOptionsTest {
|
||||
|
||||
assertThat(mutableQbftConfigOptions.getValidatorContractAddress()).hasValue("0xabc");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkBlockPeriodSeconds() {
|
||||
when(qbftConfigOptions.getBlockPeriodSeconds()).thenReturn(2);
|
||||
|
||||
final MutableQbftConfigOptions mutableQbftConfigOptions =
|
||||
new MutableQbftConfigOptions(qbftConfigOptions);
|
||||
|
||||
assertThat(mutableQbftConfigOptions.getBlockPeriodSeconds()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkEmptyBlockPeriodSeconds() {
|
||||
when(qbftConfigOptions.getEmptyBlockPeriodSeconds()).thenReturn(60);
|
||||
|
||||
final MutableQbftConfigOptions mutableQbftConfigOptions =
|
||||
new MutableQbftConfigOptions(qbftConfigOptions);
|
||||
|
||||
assertThat(mutableQbftConfigOptions.getEmptyBlockPeriodSeconds()).isEqualTo(60);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,8 +157,10 @@ public class QbftBlockHeightManagerTest {
|
||||
when(messageValidator.validateCommit(any())).thenReturn(true);
|
||||
when(messageValidator.validatePrepare(any())).thenReturn(true);
|
||||
when(finalState.getBlockTimer()).thenReturn(blockTimer);
|
||||
when(finalState.getRoundTimer()).thenReturn(roundTimer);
|
||||
when(finalState.getQuorum()).thenReturn(3);
|
||||
when(finalState.getValidatorMulticaster()).thenReturn(validatorMulticaster);
|
||||
when(finalState.getClock()).thenReturn(clock);
|
||||
when(blockCreator.createBlock(anyLong(), any()))
|
||||
.thenReturn(
|
||||
new BlockCreationResult(
|
||||
@@ -267,6 +269,7 @@ public class QbftBlockHeightManagerTest {
|
||||
@Test
|
||||
public void onBlockTimerExpiryRoundTimerIsStartedAndProposalMessageIsTransmitted() {
|
||||
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true);
|
||||
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
|
||||
|
||||
final QbftBlockHeightManager manager =
|
||||
new QbftBlockHeightManager(
|
||||
@@ -290,6 +293,7 @@ public class QbftBlockHeightManagerTest {
|
||||
public void
|
||||
onBlockTimerExpiryForNonProposerRoundTimerIsStartedAndNoProposalMessageIsTransmitted() {
|
||||
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(false);
|
||||
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
|
||||
|
||||
final QbftBlockHeightManager manager =
|
||||
new QbftBlockHeightManager(
|
||||
@@ -463,6 +467,7 @@ public class QbftBlockHeightManagerTest {
|
||||
public void messagesForCurrentRoundAreBufferedAndUsedToPreloadRoundWhenItIsStarted() {
|
||||
when(finalState.getQuorum()).thenReturn(1);
|
||||
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true);
|
||||
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
|
||||
|
||||
final QbftBlockHeightManager manager =
|
||||
new QbftBlockHeightManager(
|
||||
@@ -500,6 +505,7 @@ public class QbftBlockHeightManagerTest {
|
||||
@Test
|
||||
public void preparedCertificateIncludedInRoundChangeMessageOnRoundTimeoutExpired() {
|
||||
when(finalState.isLocalNodeProposerForRound(any())).thenReturn(true);
|
||||
when(blockTimer.checkEmptyBlockExpired(any(), eq(0l))).thenReturn(true);
|
||||
|
||||
final QbftBlockHeightManager manager =
|
||||
new QbftBlockHeightManager(
|
||||
@@ -577,4 +583,24 @@ public class QbftBlockHeightManagerTest {
|
||||
manager.handleProposalPayload(futureRoundProposal);
|
||||
verify(roundFactory, never()).createNewRound(any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkOnlyEmptyBlockPeriodSecondsIsInvokedForBlocksWithNoTransactions() {
|
||||
when(finalState.isLocalNodeProposerForRound(roundIdentifier)).thenReturn(true);
|
||||
|
||||
final QbftBlockHeightManager manager =
|
||||
new QbftBlockHeightManager(
|
||||
headerTestFixture.buildHeader(),
|
||||
finalState,
|
||||
roundChangeManager,
|
||||
roundFactory,
|
||||
clock,
|
||||
messageValidatorFactory,
|
||||
messageFactory);
|
||||
|
||||
manager.handleBlockTimerExpiry(roundIdentifier);
|
||||
|
||||
verify(blockTimer, times(0)).getEmptyBlockPeriodSeconds();
|
||||
verify(blockTimer, times(0)).getBlockPeriodSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.hyperledger.besu.consensus.common.bft.BftBlockHashing;
|
||||
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||
import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
|
||||
@@ -48,7 +47,6 @@ import org.hyperledger.besu.crypto.SECPSignature;
|
||||
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
|
||||
import org.hyperledger.besu.cryptoservices.NodeKey;
|
||||
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
|
||||
import org.hyperledger.besu.datatypes.Hash;
|
||||
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.BlockCreationTiming;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.BlockCreator.BlockCreationResult;
|
||||
@@ -196,90 +194,6 @@ public class QbftRoundTest {
|
||||
verify(transmitter, never()).multicastCommit(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendsAProposalAndPrepareWhenSendProposalRequested() {
|
||||
final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);
|
||||
final QbftRound round =
|
||||
new QbftRound(
|
||||
roundState,
|
||||
blockCreator,
|
||||
protocolContext,
|
||||
protocolSchedule,
|
||||
subscribers,
|
||||
nodeKey,
|
||||
messageFactory,
|
||||
transmitter,
|
||||
roundTimer,
|
||||
bftExtraDataCodec,
|
||||
parentHeader);
|
||||
|
||||
round.createAndSendProposalMessage(15);
|
||||
verify(transmitter, times(1))
|
||||
.multicastProposal(
|
||||
roundIdentifier, proposedBlock, Collections.emptyList(), Collections.emptyList());
|
||||
verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash());
|
||||
verify(transmitter, never()).multicastCommit(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleValidatorImportBlocksImmediatelyOnProposalCreation() {
|
||||
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);
|
||||
final QbftRound round =
|
||||
new QbftRound(
|
||||
roundState,
|
||||
blockCreator,
|
||||
protocolContext,
|
||||
protocolSchedule,
|
||||
subscribers,
|
||||
nodeKey,
|
||||
messageFactory,
|
||||
transmitter,
|
||||
roundTimer,
|
||||
bftExtraDataCodec,
|
||||
parentHeader);
|
||||
round.createAndSendProposalMessage(15);
|
||||
verify(transmitter, times(1))
|
||||
.multicastProposal(
|
||||
roundIdentifier, proposedBlock, Collections.emptyList(), Collections.emptyList());
|
||||
verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash());
|
||||
verify(transmitter, times(1)).multicastCommit(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void localNodeProposesToNetworkOfTwoValidatorsImportsOnReceptionOfCommitFromPeer() {
|
||||
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
|
||||
final QbftRound round =
|
||||
new QbftRound(
|
||||
roundState,
|
||||
blockCreator,
|
||||
protocolContext,
|
||||
protocolSchedule,
|
||||
subscribers,
|
||||
nodeKey,
|
||||
messageFactory,
|
||||
transmitter,
|
||||
roundTimer,
|
||||
bftExtraDataCodec,
|
||||
parentHeader);
|
||||
|
||||
final Hash commitSealHash =
|
||||
new BftBlockHashing(new QbftExtraDataCodec())
|
||||
.calculateDataHashForCommittedSeal(proposedBlock.getHeader(), proposedExtraData);
|
||||
final SECPSignature localCommitSeal = nodeKey.sign(commitSealHash);
|
||||
|
||||
round.createAndSendProposalMessage(15);
|
||||
verify(transmitter, never()).multicastCommit(any(), any(), any());
|
||||
|
||||
round.handlePrepareMessage(
|
||||
messageFactory2.createPrepare(roundIdentifier, proposedBlock.getHash()));
|
||||
|
||||
verify(transmitter, times(1))
|
||||
.multicastCommit(roundIdentifier, proposedBlock.getHash(), localCommitSeal);
|
||||
|
||||
round.handleCommitMessage(
|
||||
messageFactory.createCommit(roundIdentifier, proposedBlock.getHash(), remoteCommitSeal));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aProposalWithAnewBlockIsSentUponReceptionOfARoundChangeWithNoCertificate() {
|
||||
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
|
||||
@@ -393,26 +307,6 @@ public class QbftRoundTest {
|
||||
assertThat(roundState.isPrepared()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creatingNewBlockNotifiesBlockMiningObservers() {
|
||||
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);
|
||||
final QbftRound round =
|
||||
new QbftRound(
|
||||
roundState,
|
||||
blockCreator,
|
||||
protocolContext,
|
||||
protocolSchedule,
|
||||
subscribers,
|
||||
nodeKey,
|
||||
messageFactory,
|
||||
transmitter,
|
||||
roundTimer,
|
||||
bftExtraDataCodec,
|
||||
parentHeader);
|
||||
round.createAndSendProposalMessage(15);
|
||||
verify(minedBlockObserver).blockMined(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockIsOnlyImportedOnceWhenCommitsAreReceivedBeforeProposal() {
|
||||
final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 0);
|
||||
|
||||
@@ -63,6 +63,8 @@ public class QbftForksSchedulesFactoryTest
|
||||
List.of("1", "2", "3"),
|
||||
BftFork.BLOCK_PERIOD_SECONDS_KEY,
|
||||
10,
|
||||
BftFork.EMPTY_BLOCK_PERIOD_SECONDS_KEY,
|
||||
60,
|
||||
BftFork.BLOCK_REWARD_KEY,
|
||||
"5",
|
||||
QbftFork.VALIDATOR_SELECTION_MODE_KEY,
|
||||
@@ -78,6 +80,7 @@ public class QbftForksSchedulesFactoryTest
|
||||
|
||||
final Map<String, Object> forkOptions = new HashMap<>(configOptions.asMap());
|
||||
forkOptions.put(BftFork.BLOCK_PERIOD_SECONDS_KEY, 10);
|
||||
forkOptions.put(BftFork.EMPTY_BLOCK_PERIOD_SECONDS_KEY, 60);
|
||||
forkOptions.put(BftFork.BLOCK_REWARD_KEY, "5");
|
||||
forkOptions.put(QbftFork.VALIDATOR_SELECTION_MODE_KEY, "5");
|
||||
forkOptions.put(QbftFork.VALIDATOR_CONTRACT_ADDRESS_KEY, "10");
|
||||
|
||||
Reference in New Issue
Block a user