Implementing support for emptyBlockPeriodSeconds in QBFT (Issue #3810) (#6965)

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:
amsmota
2024-09-24 12:35:08 +01:00
committed by GitHub
parent e0518c6d94
commit aed6bb0044
20 changed files with 388 additions and 154 deletions

View File

@@ -13,6 +13,8 @@
### Additions and Improvements
- Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569)
- Add Blob Transaction Metrics [#7622](https://github.com/hyperledger/besu/pull/7622)
- Implemented support for emptyBlockPeriodSeconds in QBFT [#6965](https://github.com/hyperledger/besu/pull/6965)
### Bug fixes
- Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575)

View File

@@ -288,12 +288,18 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder {
protocolContext
.getBlockchain()
.observeBlockAdded(
o ->
miningParameters.setBlockPeriodSeconds(
qbftForksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getBlockPeriodSeconds()));
o -> {
miningParameters.setBlockPeriodSeconds(
qbftForksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getBlockPeriodSeconds());
miningParameters.setEmptyBlockPeriodSeconds(
qbftForksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getEmptyBlockPeriodSeconds());
});
if (syncState.isInitialSyncPhaseDone()) {
miningCoordinator.enable();
@@ -422,8 +428,9 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder {
return block ->
LOG.info(
String.format(
"%s #%,d / %d tx / %d pending / %,d (%01.1f%%) gas / (%s)",
"%s %s #%,d / %d tx / %d pending / %,d (%01.1f%%) gas / (%s)",
block.getHeader().getCoinbase().equals(localAddress) ? "Produced" : "Imported",
block.getBody().getTransactions().size() == 0 ? "empty block" : "block",
block.getHeader().getNumber(),
block.getBody().getTransactions().size(),
transactionPool.count(),

View File

@@ -138,15 +138,18 @@ public abstract class CommandTestAbstract {
private static final Logger TEST_LOGGER = LoggerFactory.getLogger(CommandTestAbstract.class);
protected static final int POA_BLOCK_PERIOD_SECONDS = 5;
protected static final int POA_EMPTY_BLOCK_PERIOD_SECONDS = 50;
protected static final JsonObject VALID_GENESIS_QBFT_POST_LONDON =
(new JsonObject())
.put(
"config",
new JsonObject()
.put("londonBlock", 0)
.put("qbft", new JsonObject().put("blockperiodseconds", POA_BLOCK_PERIOD_SECONDS))
.put(
"qbft",
new JsonObject().put("blockperiodseconds", POA_BLOCK_PERIOD_SECONDS)));
new JsonObject()
.put("xemptyblockperiodseconds", POA_EMPTY_BLOCK_PERIOD_SECONDS)));
protected static final JsonObject VALID_GENESIS_IBFT2_POST_LONDON =
(new JsonObject())
.put(

View File

@@ -37,6 +37,13 @@ public interface BftConfigOptions {
*/
int getBlockPeriodSeconds();
/**
* Gets empty block period seconds.
*
* @return the empty block period seconds
*/
int getEmptyBlockPeriodSeconds();
/**
* Gets block period milliseconds. For TESTING only. If set then blockperiodseconds is ignored.
*

View File

@@ -41,6 +41,9 @@ public class BftFork implements Fork {
/** The constant BLOCK_PERIOD_SECONDS_KEY. */
public static final String BLOCK_PERIOD_SECONDS_KEY = "blockperiodseconds";
/** The constant EMPTY_BLOCK_PERIOD_SECONDS_KEY. */
public static final String EMPTY_BLOCK_PERIOD_SECONDS_KEY = "xemptyblockperiodseconds";
/** The constant BLOCK_PERIOD_MILLISECONDS_KEY. */
public static final String BLOCK_PERIOD_MILLISECONDS_KEY = "xblockperiodmilliseconds";
@@ -86,6 +89,16 @@ public class BftFork implements Fork {
return JsonUtil.getPositiveInt(forkConfigRoot, BLOCK_PERIOD_SECONDS_KEY);
}
/**
* Gets empty block period seconds.
*
* @return the empty block period seconds
*/
public OptionalInt getEmptyBlockPeriodSeconds() {
// It can be 0 to disable custom empty block periods
return JsonUtil.getInt(forkConfigRoot, EMPTY_BLOCK_PERIOD_SECONDS_KEY);
}
/**
* Gets block period milliseconds. Experimental for test scenarios only.
*

View File

@@ -34,6 +34,8 @@ public class JsonBftConfigOptions implements BftConfigOptions {
private static final long DEFAULT_EPOCH_LENGTH = 30_000;
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1;
// 0 keeps working as before, increase to activate it
private static final int DEFAULT_EMPTY_BLOCK_PERIOD_SECONDS = 0;
private static final int DEFAULT_BLOCK_PERIOD_MILLISECONDS = 0; // Experimental for test only
private static final int DEFAULT_ROUND_EXPIRY_SECONDS = 1;
// In a healthy network this can be very small. This default limit will allow for suitable
@@ -67,6 +69,12 @@ public class JsonBftConfigOptions implements BftConfigOptions {
bftConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
}
@Override
public int getEmptyBlockPeriodSeconds() {
return JsonUtil.getInt(
bftConfigRoot, "xemptyblockperiodseconds", DEFAULT_EMPTY_BLOCK_PERIOD_SECONDS);
}
@Override
public long getBlockPeriodMilliseconds() {
return JsonUtil.getLong(

View File

@@ -17,6 +17,7 @@ package org.hyperledger.besu.config;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.hyperledger.besu.datatypes.Address;
@@ -30,6 +31,7 @@ public class JsonBftConfigOptionsTest {
private static final int EXPECTED_DEFAULT_EPOCH_LENGTH = 30_000;
private static final int EXPECTED_DEFAULT_BLOCK_PERIOD = 1;
private static final int EXPECTED_EMPTY_DEFAULT_BLOCK_PERIOD = 0;
private static final int EXPECTED_DEFAULT_REQUEST_TIMEOUT = 1;
private static final int EXPECTED_DEFAULT_GOSSIPED_HISTORY_LIMIT = 1000;
private static final int EXPECTED_DEFAULT_MESSAGE_QUEUE_LIMIT = 1000;
@@ -61,18 +63,37 @@ public class JsonBftConfigOptionsTest {
assertThat(config.getBlockPeriodSeconds()).isEqualTo(5);
}
@Test
public void shouldGetEmptyBlockPeriodFromConfig() {
final BftConfigOptions config = fromConfigOptions(singletonMap("xemptyblockperiodseconds", 60));
assertThat(config.getEmptyBlockPeriodSeconds()).isEqualTo(60);
}
@Test
public void shouldFallbackToDefaultBlockPeriod() {
final BftConfigOptions config = fromConfigOptions(emptyMap());
assertThat(config.getBlockPeriodSeconds()).isEqualTo(EXPECTED_DEFAULT_BLOCK_PERIOD);
}
@Test
public void shouldFallbackToEmptyDefaultBlockPeriod() {
final BftConfigOptions config = fromConfigOptions(emptyMap());
assertThat(config.getEmptyBlockPeriodSeconds()).isEqualTo(EXPECTED_EMPTY_DEFAULT_BLOCK_PERIOD);
}
@Test
public void shouldGetDefaultBlockPeriodFromDefaultConfig() {
assertThat(JsonBftConfigOptions.DEFAULT.getBlockPeriodSeconds())
.isEqualTo(EXPECTED_DEFAULT_BLOCK_PERIOD);
}
@Test
public void shouldGetDefaultEmptyBlockPeriodFromDefaultConfig() {
assertThat(JsonBftConfigOptions.DEFAULT.getEmptyBlockPeriodSeconds())
.isEqualTo(EXPECTED_EMPTY_DEFAULT_BLOCK_PERIOD);
}
@Test
public void shouldThrowOnNonPositiveBlockPeriod() {
final BftConfigOptions config = fromConfigOptions(singletonMap("blockperiodseconds", -1));
@@ -80,6 +101,13 @@ public class JsonBftConfigOptionsTest {
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void shouldNotThrowOnNonPositiveEmptyBlockPeriod() {
// can be 0 to be compatible with older versions
final BftConfigOptions config = fromConfigOptions(singletonMap("xemptyblockperiodseconds", 0));
assertThatCode(() -> config.getEmptyBlockPeriodSeconds()).doesNotThrowAnyException();
}
@Test
public void shouldGetRequestTimeoutFromConfig() {
final BftConfigOptions config = fromConfigOptions(singletonMap("requesttimeoutseconds", 5));

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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)));
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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.
*

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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");

View File

@@ -131,6 +131,11 @@ public abstract class MiningParameters {
return this;
}
public MiningParameters setEmptyBlockPeriodSeconds(final int emptyBlockPeriodSeconds) {
getMutableRuntimeValues().emptyBlockPeriodSeconds = OptionalInt.of(emptyBlockPeriodSeconds);
return this;
}
@Value.Default
public boolean isStratumMiningEnabled() {
return false;
@@ -231,6 +236,8 @@ public abstract class MiningParameters {
OptionalInt getBlockPeriodSeconds();
OptionalInt getEmptyBlockPeriodSeconds();
Optional<Address> getCoinbase();
OptionalLong getTargetGasLimit();
@@ -248,6 +255,7 @@ public abstract class MiningParameters {
private volatile OptionalLong targetGasLimit;
private volatile Optional<Iterable<Long>> nonceGenerator;
private volatile OptionalInt blockPeriodSeconds;
private volatile OptionalInt emptyBlockPeriodSeconds;
private MutableRuntimeValues(final MutableInitValues initValues) {
miningEnabled = initValues.isMiningEnabled();
@@ -259,6 +267,7 @@ public abstract class MiningParameters {
targetGasLimit = initValues.getTargetGasLimit();
nonceGenerator = initValues.nonceGenerator();
blockPeriodSeconds = initValues.getBlockPeriodSeconds();
emptyBlockPeriodSeconds = initValues.getEmptyBlockPeriodSeconds();
}
@Override
@@ -274,7 +283,8 @@ public abstract class MiningParameters {
&& Objects.equals(minPriorityFeePerGas, that.minPriorityFeePerGas)
&& Objects.equals(targetGasLimit, that.targetGasLimit)
&& Objects.equals(nonceGenerator, that.nonceGenerator)
&& Objects.equals(blockPeriodSeconds, that.blockPeriodSeconds);
&& Objects.equals(blockPeriodSeconds, that.blockPeriodSeconds)
&& Objects.equals(emptyBlockPeriodSeconds, that.emptyBlockPeriodSeconds);
}
@Override

View File

@@ -218,7 +218,8 @@ public class PersistBlockTask extends AbstractEthTask<Block> {
case IMPORTED:
LOG.info(
String.format(
"Imported #%,d / %d tx / %d om / %,d (%01.1f%%) gas / (%s) in %01.3fs. Peers: %d",
"Imported %s #%,d / %d tx / %d om / %,d (%01.1f%%) gas / (%s) in %01.3fs. Peers: %d",
block.getBody().getTransactions().size() == 0 ? "empty block" : "block",
block.getHeader().getNumber(),
block.getBody().getTransactions().size(),
block.getBody().getOmmers().size(),