Skip to content

Commit

Permalink
Clique block period transition (hyperledger#6596)
Browse files Browse the repository at this point in the history
Add BFT-style transitions to Clique, modelled with ForksSchedule<CliqueConfigOptions>
Add ability to transition the blockperiodseconds config.

---------

Signed-off-by: Jason Frame <[email protected]>
Signed-off-by: Simon Dudley <[email protected]>
Co-authored-by: Jason Frame <[email protected]>
  • Loading branch information
siladu and jframe authored Feb 28, 2024
1 parent 647750c commit 0e3d2f4
Show file tree
Hide file tree
Showing 27 changed files with 584 additions and 132 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

### Additions and Improvements
- Extend `Blockchain` service [#6592](https://github.com/hyperledger/besu/pull/6592)
- Add bft-style blockperiodseconds transitions to Clique [#6596](https://github.com/hyperledger/besu/pull/6596)
- RocksDB database metadata refactoring [#6555](https://github.com/hyperledger/besu/pull/6555)
- Make layered txpool aware of minGasPrice and minPriorityFeePerGas dynamic options [#6611](https://github.com/hyperledger/besu/pull/6611)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
*/
package org.hyperledger.besu.tests.acceptance.clique;

import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.data.Percentage.withPercentage;

import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBaseJunit5;
import org.hyperledger.besu.tests.acceptance.dsl.account.Account;
import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode;
import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory.CliqueOptions;

import java.io.IOException;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.web3j.protocol.core.DefaultBlockParameter;

public class CliqueMiningAcceptanceTest extends AcceptanceTestBaseJunit5 {

Expand Down Expand Up @@ -123,4 +132,117 @@ public void shouldStillMineWhenANodeFailsAndHasSufficientValidators() throws IOE
cluster.verifyOnActiveNodes(clique.blockIsCreatedByProposer(minerNode1));
cluster.verifyOnActiveNodes(clique.blockIsCreatedByProposer(minerNode2));
}

@Test
public void shouldMineBlocksWithBlockPeriodAccordingToTransitions() throws IOException {

final var cliqueOptions = new CliqueOptions(3, CliqueOptions.DEFAULT.epochLength(), true);
final BesuNode minerNode = besu.createCliqueNode("miner1", cliqueOptions);

// setup transitions
final Map<String, Object> decreasePeriodTo2_Transition =
Map.of("block", 3, "blockperiodseconds", 2);
final Map<String, Object> decreasePeriodTo1_Transition =
Map.of("block", 4, "blockperiodseconds", 1);
final Map<String, Object> increasePeriodTo2_Transition =
Map.of("block", 6, "blockperiodseconds", 2);

final Optional<String> initialGenesis =
minerNode.getGenesisConfigProvider().create(List.of(minerNode));
final String genesisWithTransitions =
prependTransitionsToCliqueOptions(
initialGenesis.orElseThrow(),
List.of(
decreasePeriodTo2_Transition,
decreasePeriodTo1_Transition,
increasePeriodTo2_Transition));
minerNode.setGenesisConfig(genesisWithTransitions);

// Mine 6 blocks
cluster.start(minerNode);
minerNode.verify(blockchain.reachesHeight(minerNode, 5));

// Assert the block period decreased/increased after each transition
final long block1Timestamp = getTimestampForBlock(minerNode, 1);
final long block2Timestamp = getTimestampForBlock(minerNode, 2);
final long block3Timestamp = getTimestampForBlock(minerNode, 3);
final long block4Timestamp = getTimestampForBlock(minerNode, 4);
final long block5Timestamp = getTimestampForBlock(minerNode, 5);
final long block6Timestamp = getTimestampForBlock(minerNode, 6);
assertThat(block2Timestamp - block1Timestamp).isCloseTo(3, withPercentage(20));
assertThat(block3Timestamp - block2Timestamp).isCloseTo(2, withPercentage(20));
assertThat(block4Timestamp - block3Timestamp).isCloseTo(1, withPercentage(20));
assertThat(block5Timestamp - block4Timestamp).isCloseTo(1, withPercentage(20));
assertThat(block6Timestamp - block5Timestamp).isCloseTo(2, withPercentage(20));
}

private long getTimestampForBlock(final BesuNode minerNode, final int blockNumber) {
return minerNode
.execute(
ethTransactions.block(DefaultBlockParameter.valueOf(BigInteger.valueOf(blockNumber))))
.getTimestamp()
.longValue();
}

private String prependTransitionsToCliqueOptions(
final String originalOptions, final List<Map<String, Object>> transitions) {
final StringBuilder stringBuilder =
new StringBuilder()
.append(formatCliqueTransitionsOptions(transitions))
.append(",\n")
.append(quote("clique"))
.append(": {");

return originalOptions.replace(quote("clique") + ": {", stringBuilder.toString());
}

private String formatCliqueTransitionsOptions(final List<Map<String, Object>> transitions) {
final StringBuilder stringBuilder = new StringBuilder();

stringBuilder.append(quote("transitions"));
stringBuilder.append(": {\n");
stringBuilder.append(quote("clique"));
stringBuilder.append(": [");
final String formattedTransitions =
transitions.stream().map(this::formatTransition).collect(joining(",\n"));
stringBuilder.append(formattedTransitions);
stringBuilder.append("\n]");
stringBuilder.append("}\n");

return stringBuilder.toString();
}

private String quote(final Object value) {
return '"' + value.toString() + '"';
}

private String formatTransition(final Map<String, Object> transition) {
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("{");
String formattedTransition =
transition.keySet().stream()
.map(key -> formatKeyValues(key, transition.get(key)))
.collect(joining(","));
stringBuilder.append(formattedTransition);
stringBuilder.append("}");
return stringBuilder.toString();
}

private String formatKeyValues(final Object... keyOrValue) {
if (keyOrValue.length % 2 == 1) {
// An odd number of strings cannot form a set of key-value pairs
throw new IllegalArgumentException("Must supply key-value pairs");
}
final StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < keyOrValue.length; i += 2) {
if (i > 0) {
stringBuilder.append(", ");
}
final String key = keyOrValue[i].toString();
final Object value = keyOrValue[i + 1];
final String valueStr = value instanceof String ? quote(value) : value.toString();
stringBuilder.append(String.format("\n%s: %s", quote(key), valueStr));
}
return stringBuilder.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.hyperledger.besu.config.CliqueConfigOptions;
import org.hyperledger.besu.consensus.clique.CliqueBlockInterface;
import org.hyperledger.besu.consensus.clique.CliqueContext;
import org.hyperledger.besu.consensus.clique.CliqueForksSchedulesFactory;
import org.hyperledger.besu.consensus.clique.CliqueMiningTracker;
import org.hyperledger.besu.consensus.clique.CliqueProtocolSchedule;
import org.hyperledger.besu.consensus.clique.blockcreation.CliqueBlockScheduler;
Expand All @@ -27,6 +28,7 @@
import org.hyperledger.besu.consensus.clique.jsonrpc.CliqueJsonRpcMethods;
import org.hyperledger.besu.consensus.common.BlockInterface;
import org.hyperledger.besu.consensus.common.EpochManager;
import org.hyperledger.besu.consensus.common.ForksSchedule;
import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.ProtocolContext;
Expand All @@ -52,19 +54,19 @@ public class CliqueBesuControllerBuilder extends BesuControllerBuilder {

private Address localAddress;
private EpochManager epochManager;
private long secondsBetweenBlocks;
private boolean createEmptyBlocks = true;
private final BlockInterface blockInterface = new CliqueBlockInterface();
private ForksSchedule<CliqueConfigOptions> forksSchedule;

@Override
protected void prepForBuild() {
localAddress = Util.publicKeyToAddress(nodeKey.getPublicKey());
final CliqueConfigOptions cliqueConfig = configOptionsSupplier.get().getCliqueConfigOptions();
final long blocksPerEpoch = cliqueConfig.getEpochLength();
secondsBetweenBlocks = cliqueConfig.getBlockPeriodSeconds();
createEmptyBlocks = cliqueConfig.getCreateEmptyBlocks();

epochManager = new EpochManager(blocksPerEpoch);
forksSchedule = CliqueForksSchedulesFactory.create(configOptionsSupplier.get());
}

@Override
Expand Down Expand Up @@ -92,7 +94,7 @@ protected MiningCoordinator createMiningCoordinator(
clock,
protocolContext.getConsensusContext(CliqueContext.class).getValidatorProvider(),
localAddress,
secondsBetweenBlocks),
forksSchedule),
epochManager,
createEmptyBlocks,
ethProtocolManager.ethContext().getScheduler());
Expand All @@ -113,6 +115,7 @@ protected MiningCoordinator createMiningCoordinator(
protected ProtocolSchedule createProtocolSchedule() {
return CliqueProtocolSchedule.create(
configOptionsSupplier.get(),
forksSchedule,
nodeKey,
privacyParameters,
isRevertReasonEnabled,
Expand Down
2 changes: 2 additions & 0 deletions config/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ dependencies {
implementation 'info.picocli:picocli'
implementation 'io.tmio:tuweni-bytes'
implementation 'io.tmio:tuweni-units'
implementation "org.immutables:value-annotations"
annotationProcessor "org.immutables:value"

testImplementation project(':testutil')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.apache.tuweni.bytes.Bytes;

/** The Bft fork. */
public class BftFork {
public class BftFork implements Fork {

/** The constant FORK_BLOCK_KEY. */
public static final String FORK_BLOCK_KEY = "block";
Expand Down Expand Up @@ -59,6 +59,7 @@ public BftFork(final ObjectNode forkConfigRoot) {
*
* @return the fork block
*/
@Override
public long getForkBlock() {
return JsonUtil.getLong(forkConfigRoot, FORK_BLOCK_KEY)
.orElseThrow(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright ConsenSys AG.
* Copyright Hyperledger Besu Contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
Expand All @@ -12,75 +12,42 @@
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.hyperledger.besu.config;

import java.util.Map;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;

/** The Clique config options. */
public class CliqueConfigOptions {

/** The constant DEFAULT. */
public static final CliqueConfigOptions DEFAULT =
new CliqueConfigOptions(JsonUtil.createEmptyObjectNode());

private static final long DEFAULT_EPOCH_LENGTH = 30_000;
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 15;
private static final boolean DEFAULT_CREATE_EMPTY_BLOCKS = true;
import org.immutables.value.Value;

private final ObjectNode cliqueConfigRoot;

/**
* Instantiates a new Clique config options.
*
* @param cliqueConfigRoot the clique config root
*/
CliqueConfigOptions(final ObjectNode cliqueConfigRoot) {
this.cliqueConfigRoot = cliqueConfigRoot;
}
/** Configuration options for the Clique consensus mechanism. */
@Value.Immutable
public interface CliqueConfigOptions {

/**
* The number of blocks in an epoch.
*
* @return the epoch length
*/
public long getEpochLength() {
return JsonUtil.getLong(cliqueConfigRoot, "epochlength", DEFAULT_EPOCH_LENGTH);
}
long getEpochLength();

/**
* Gets block period seconds.
*
* @return the block period seconds
*/
public int getBlockPeriodSeconds() {
return JsonUtil.getPositiveInt(
cliqueConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
}
int getBlockPeriodSeconds();

/**
* Whether the creation of empty blocks is allowed.
* Gets create empty blocks.
*
* @return the create empty block status
* @return whether empty blocks are permitted
*/
public boolean getCreateEmptyBlocks() {
return JsonUtil.getBoolean(cliqueConfigRoot, "createemptyblocks", DEFAULT_CREATE_EMPTY_BLOCKS);
}
boolean getCreateEmptyBlocks();

/**
* As map.
* A map of the config options.
*
* @return the map
*/
Map<String, Object> asMap() {
return ImmutableMap.of(
"epochLength",
getEpochLength(),
"blockPeriodSeconds",
getBlockPeriodSeconds(),
"createemptyblocks",
getCreateEmptyBlocks());
}
Map<String, Object> asMap();
}
66 changes: 66 additions & 0 deletions config/src/main/java/org/hyperledger/besu/config/CliqueFork.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.config;

import java.util.OptionalInt;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.node.ObjectNode;

/** The Clique fork. */
public class CliqueFork implements Fork {

/** The constant FORK_BLOCK_KEY. */
public static final String FORK_BLOCK_KEY = "block";

/** The constant BLOCK_PERIOD_SECONDS_KEY. */
public static final String BLOCK_PERIOD_SECONDS_KEY = "blockperiodseconds";

/** The Fork config root. */
protected final ObjectNode forkConfigRoot;

/**
* Instantiates a new Clique fork.
*
* @param forkConfigRoot the fork config root
*/
@JsonCreator
public CliqueFork(final ObjectNode forkConfigRoot) {
this.forkConfigRoot = forkConfigRoot;
}

/**
* Gets fork block.
*
* @return the fork block
*/
@Override
public long getForkBlock() {
return JsonUtil.getLong(forkConfigRoot, FORK_BLOCK_KEY)
.orElseThrow(
() ->
new IllegalArgumentException(
"Fork block not specified for Clique fork in custom forks"));
}

/**
* Gets block period seconds.
*
* @return the block period seconds
*/
public OptionalInt getBlockPeriodSeconds() {
return JsonUtil.getPositiveInt(forkConfigRoot, BLOCK_PERIOD_SECONDS_KEY);
}
}
Loading

0 comments on commit 0e3d2f4

Please sign in to comment.