diff --git a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java index 93b0ea55d84..984c1b2dab5 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java @@ -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(), diff --git a/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java index c94752b598d..5aa06828ed7 100644 --- a/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java @@ -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. * diff --git a/config/src/main/java/org/hyperledger/besu/config/BftFork.java b/config/src/main/java/org/hyperledger/besu/config/BftFork.java index 2b2031cea45..052daa042c1 100644 --- a/config/src/main/java/org/hyperledger/besu/config/BftFork.java +++ b/config/src/main/java/org/hyperledger/besu/config/BftFork.java @@ -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. */ @@ -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. * diff --git a/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java index 4dfa49fa3a0..599ef32fe0f 100644 --- a/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java @@ -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 @@ -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); diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java index 5649b69e8f1..2cba8d4b420 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java @@ -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 forksSchedule; private final BftExecutors bftExecutors; private Optional> 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 @@ -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 */ @@ -76,9 +84,6 @@ 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 = @@ -86,6 +91,29 @@ public synchronized void startTimer( 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; @@ -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; + } } diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java index ee8f546bd77..205a4231afb 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java @@ -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; @@ -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(); @@ -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; @@ -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. * diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java index 028f939d32c..8a160c741df 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java @@ -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)) diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java index cf87938cb4a..2f05ea17e9e 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java @@ -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()) { diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManager.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManager.java index 0940d35df6b..d41e8e18a47 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManager.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManager.java @@ -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; @@ -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(); } } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java index 5754592064b..7702193ae52 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java @@ -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. *