Skip to content
/ besu Public
forked from hyperledger/besu

Commit

Permalink
Implementing support for emptyBlockPeriodSeconds in QBFT (Issue hyper…
Browse files Browse the repository at this point in the history
…ledger#3810)

Signed-off-by: amsmota <[email protected]>
  • Loading branch information
amsmota committed Apr 16, 2024
1 parent cd8c857 commit 0ca261d
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,9 @@ private static MinedBlockObserver blockLogger(
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ public interface BftConfigOptions {
*/
int getBlockPeriodSeconds();

/**
* Gets empty block period seconds.
*
* @return the block period seconds
*/
int getEmptyBlockPeriodSeconds();

/**
* Gets request timeout seconds.
*
Expand Down
11 changes: 11 additions & 0 deletions config/src/main/java/org/hyperledger/besu/config/BftFork.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class BftFork {
public static final String VALIDATORS_KEY = "validators";
/** 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 = "emptyblockperiodseconds";
/** The constant BLOCK_REWARD_KEY. */
public static final String BLOCK_REWARD_KEY = "blockreward";
/** The constant MINING_BENEFICIARY_KEY. */
Expand Down Expand Up @@ -76,6 +78,15 @@ public OptionalInt getBlockPeriodSeconds() {
return JsonUtil.getPositiveInt(forkConfigRoot, BLOCK_PERIOD_SECONDS_KEY);
}

/**
* Gets empty block period seconds.
*
* @return the block empty period seconds
*/
public OptionalInt getEmptyBlockPeriodSeconds() {
return JsonUtil.getPositiveInt(forkConfigRoot, EMPTY_BLOCK_PERIOD_SECONDS_KEY);
}

/**
* Gets block reward wei.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class JsonBftConfigOptions implements BftConfigOptions {

private static final long DEFAULT_EPOCH_LENGTH = 30_000;
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1;
private static final int DEFAULT_EMPTY_BLOCK_PERIOD_SECONDS = 0;
// 0 keeps working as before, increase to activate itby default if needed
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
// protection for on a typical 20 node validator network with multiple rounds
Expand Down Expand Up @@ -65,6 +67,13 @@ public int getBlockPeriodSeconds() {
bftConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
}

@Override
public int getEmptyBlockPeriodSeconds() {
return JsonUtil.getInt(
bftConfigRoot, "emptyblockperiodseconds", DEFAULT_EMPTY_BLOCK_PERIOD_SECONDS);
}


@Override
public int getRequestTimeoutSeconds() {
return JsonUtil.getInt(bftConfigRoot, "requesttimeoutseconds", DEFAULT_ROUND_EXPIRY_SECONDS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Class for starting and keeping organised block timers */
public class BlockTimer {

private static final Logger LOG = LoggerFactory.getLogger(BlockTimer.class);
private final ForksSchedule<? extends BftConfigOptions> forksSchedule;
private final BftExecutors bftExecutors;
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
Expand All @@ -51,6 +57,8 @@ public BlockTimer(
this.bftExecutors = bftExecutors;
this.currentTimerTask = Optional.empty();
this.clock = clock;
this.blockPeriodSeconds = forksSchedule.getFork(0).getValue().getBlockPeriodSeconds();
this.emptyBlockPeriodSeconds = forksSchedule.getFork(0).getValue().getEmptyBlockPeriodSeconds();
}

/** Cancels the current running round timer if there is one */
Expand All @@ -76,16 +84,36 @@ public synchronized boolean isRunning() {
*/
public synchronized void startTimer(
final ConsensusRoundIdentifier round, final BlockHeader chainHeadHeader) {
cancelTimer();

final long now = clock.millis();

// absolute time when the timer is supposed to expire
final int blockPeriodSeconds =
forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodSeconds();
final long minimumTimeBetweenBlocksMillis = blockPeriodSeconds * 1000L;
final long expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis;

setBlockTimes(round, false);

startTimer(round, expiryTime);
}

public synchronized void startEmptyBlockTimer(
final ConsensusRoundIdentifier round, final BlockHeader chainHeadHeader) {

// absolute time when the timer is supposed to expire
final int currentBlockPeriodSeconds =
forksSchedule.getFork(round.getSequenceNumber()).getValue().getEmptyBlockPeriodSeconds();
final long minimumTimeBetweenBlocksMillis = currentBlockPeriodSeconds * 1000L;
final long expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis;

setBlockTimes(round, true);

startTimer(round, expiryTime);
}

public synchronized void startTimer(final ConsensusRoundIdentifier round, final long expiryTime) {
cancelTimer();
final long now = clock.millis();

if (expiryTime > now) {
final long delay = expiryTime - now;

Expand All @@ -98,4 +126,23 @@ public synchronized void startTimer(
queue.add(new BlockTimerExpiry(round));
}
}

private synchronized void setBlockTimes(final ConsensusRoundIdentifier round, final boolean isEmpty) {
final BftConfigOptions currentConfigOptions = forksSchedule.getFork(round.getSequenceNumber()).getValue();
this.blockPeriodSeconds = currentConfigOptions.getBlockPeriodSeconds();
this.emptyBlockPeriodSeconds = currentConfigOptions.getEmptyBlockPeriodSeconds();

long currentBlockPeriodSeconds = isEmpty && this.emptyBlockPeriodSeconds > 0
? this.emptyBlockPeriodSeconds
: this.blockPeriodSeconds;

LOG.debug("NEW CURRENTBLOCKPERIODSECONDS SET TO {}: {}", isEmpty?"EMPTYBLOCKPERIODSECONDS":"BLOCKPERIODSECONDS", currentBlockPeriodSeconds);
}

public synchronized long getBlockPeriodSeconds(){
return blockPeriodSeconds;
}
public synchronized long getEmptyBlockPeriodSeconds(){
return emptyBlockPeriodSeconds;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@
import java.util.Map;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A mutable {@link BftConfigOptions} that is used for building config for transitions in the {@link
* ForksSchedule}*.
*/
public class MutableBftConfigOptions implements BftConfigOptions {

private static final Logger LOG = LoggerFactory.getLogger(MutableBftConfigOptions.class);
private long epochLength;
private int blockPeriodSeconds;
private int emptyBlockPeriodSeconds;
private int requestTimeoutSeconds;
private int gossipedHistoryLimit;
private int messageQueueLimit;
Expand All @@ -48,6 +54,7 @@ public class MutableBftConfigOptions implements BftConfigOptions {
public MutableBftConfigOptions(final BftConfigOptions bftConfigOptions) {
this.epochLength = bftConfigOptions.getEpochLength();
this.blockPeriodSeconds = bftConfigOptions.getBlockPeriodSeconds();
this.emptyBlockPeriodSeconds = bftConfigOptions.getEmptyBlockPeriodSeconds();
this.requestTimeoutSeconds = bftConfigOptions.getRequestTimeoutSeconds();
this.gossipedHistoryLimit = bftConfigOptions.getGossipedHistoryLimit();
this.messageQueueLimit = bftConfigOptions.getMessageQueueLimit();
Expand All @@ -65,9 +72,16 @@ public long getEpochLength() {

@Override
public int getBlockPeriodSeconds() {
LOG.debug("GET BLOCKPERIODSECONDS: " + blockPeriodSeconds);
return blockPeriodSeconds;
}

@Override
public int getEmptyBlockPeriodSeconds() {
LOG.debug("GET EMPTYBLOCKPERIODSECONDS: " + emptyBlockPeriodSeconds);
return emptyBlockPeriodSeconds;
}

@Override
public int getRequestTimeoutSeconds() {
return requestTimeoutSeconds;
Expand Down Expand Up @@ -128,9 +142,20 @@ public void setEpochLength(final long epochLength) {
* @param blockPeriodSeconds the block period seconds
*/
public void setBlockPeriodSeconds(final int blockPeriodSeconds) {
LOG.info("SET BLOCKPERIODSECONDS: " + blockPeriodSeconds);
this.blockPeriodSeconds = blockPeriodSeconds;
}

/**
* Sets empty block period seconds.
*
* @param emptyBlockPeriodSeconds the empty block period seconds
*/
public void setEmptyBlockPeriodSeconds(final int emptyBlockPeriodSeconds) {
LOG.info("SET EMPTYBLOCKPERIODSECONDS: " + emptyBlockPeriodSeconds);
this.emptyBlockPeriodSeconds = emptyBlockPeriodSeconds;
}

/**
* Sets request timeout seconds.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static BlockHeaderValidator.Builder blockHeaderValidator(
new GasLimitRangeAndDeltaValidationRule(
DEFAULT_MIN_GAS_LIMIT, DEFAULT_MAX_GAS_LIMIT, baseFeeMarket))
.addRule(new TimestampBoundedByFutureParameter(1))
.addRule(new TimestampMoreRecentThanParent(secondsBetweenBlocks))
//.addRule(new TimestampMoreRecentThanParent(secondsBetweenBlocks))
.addRule(
new ConstantFieldValidationRule<>(
"MixHash", BlockHeader::getMixHash, BftHelpers.EXPECTED_MIX_HASH))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private static QbftConfigOptions createQbftConfigOptions(
new MutableQbftConfigOptions(lastSpec.getValue());

fork.getBlockPeriodSeconds().ifPresent(bftConfigOptions::setBlockPeriodSeconds);
fork.getEmptyBlockPeriodSeconds().ifPresent(bftConfigOptions::setEmptyBlockPeriodSeconds);
fork.getBlockRewardWei().ifPresent(bftConfigOptions::setBlockRewardWei);

if (fork.isMiningBeneficiaryConfigured()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;

Expand Down Expand Up @@ -130,19 +131,48 @@ public void handleBlockTimerExpiry(final ConsensusRoundIdentifier roundIdentifie

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);
finalState.isLocalNodeProposerForRound(qbftRound.getRoundIdentifier());

final long headerTimeStampSeconds = Math.round(clock.millis() / 1000D);
final Block block = qbftRound.createBlock(headerTimeStampSeconds);
final boolean blockHasTransactions = !block.getBody().getTransactions().isEmpty();
if (blockHasTransactions) {
if (isProposer) {
LOG.debug("Block Has Transactions and I AM proposer so send proposal");
qbftRound.sendProposalMessage(block);
}else{
LOG.debug("Block Has Transactions and I am NOT proposer");
}
} else {
final long emptyBlockPeriodSeconds = finalState.getBlockTimer().getEmptyBlockPeriodSeconds();
final long emptyBlockPeriodExpiryTime = parentHeader.getTimestamp() + emptyBlockPeriodSeconds;
final long nowInSeconds = finalState.getClock().millis() / 1000;
if (nowInSeconds >= emptyBlockPeriodExpiryTime) {
if (isProposer) {
LOG.debug("Block Do Not Have Transactions and I AM proposer so send proposal");
qbftRound.sendProposalMessage(block);
} else {
LOG.debug("Block Do Not Have Transactions and I am NOT proposer");
}
} else {
LOG.trace(
"Block timer expired for a round ({}) other than current ({})",
roundIdentifier,
qbftRound.getRoundIdentifier());
finalState.getRoundTimer().cancelTimer();
finalState.getBlockTimer().startEmptyBlockTimer(roundIdentifier, parentHeader);
currentRound = Optional.empty();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ public ConsensusRoundIdentifier getRoundIdentifier() {
return roundState.getRoundIdentifier();
}

/**
* Create a block
*
* @return a Block
*/
public Block createBlock(final long headerTimeStampSeconds) {
LOG.debug("Creating proposed block. round={}", roundState.getRoundIdentifier());
return blockCreator.createBlock(headerTimeStampSeconds).getBlock();
}

/**
* Send proposal message.
*
* @param headerTimeStampSeconds the header time stamp seconds
*/
public void sendProposalMessage(final Block block) {
LOG.trace("Creating proposed block blockHeader={}", block.getHeader());
updateStateWithProposalAndTransmit(block, emptyList(), emptyList());
}

/**
* Create and send proposal message.
*
Expand Down

0 comments on commit 0ca261d

Please sign in to comment.