Make block txs selection max time aware of PoA transitions (#6676)

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>
pull/6698/head
Fabio Di Fabio 8 months ago committed by GitHub
parent 246cce41e1
commit 65f8880fb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 4
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  3. 17
      besu/src/main/java/org/hyperledger/besu/cli/options/MiningOptions.java
  4. 12
      besu/src/main/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilder.java
  5. 11
      besu/src/main/java/org/hyperledger/besu/controller/IbftBesuControllerBuilder.java
  6. 11
      besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
  7. 11
      besu/src/test/java/org/hyperledger/besu/cli/options/AbstractCLIOptionsTest.java
  8. 23
      besu/src/test/java/org/hyperledger/besu/cli/options/MiningOptionsTest.java
  9. 204
      besu/src/test/java/org/hyperledger/besu/controller/AbstractBftBesuControllerBuilderTest.java
  10. 231
      besu/src/test/java/org/hyperledger/besu/controller/CliqueBesuControllerBuilderTest.java
  11. 73
      besu/src/test/java/org/hyperledger/besu/controller/IbftBesuControllerBuilderTest.java
  12. 158
      besu/src/test/java/org/hyperledger/besu/controller/QbftBesuControllerBuilderTest.java
  13. 1
      consensus/clique/src/test/java/org/hyperledger/besu/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java
  14. 2
      ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java
  15. 29
      ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningParameters.java

@ -24,6 +24,7 @@
- Transaction call object to accept both `input` and `data` field simultaneously if they are set to equal values [#6702](https://github.com/hyperledger/besu/pull/6702)
### Bug fixes
- Make block transaction selection max time aware of PoA transitions [#6676](https://github.com/hyperledger/besu/pull/6676)
### Download Links

@ -2149,10 +2149,10 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private MiningParameters getMiningParameters() {
if (miningParameters == null) {
miningOptions.setGenesisBlockPeriodSeconds(
getGenesisBlockPeriodSeconds(getActualGenesisConfigOptions()));
miningOptions.setTransactionSelectionService(transactionSelectionServiceImpl);
miningParameters = miningOptions.toDomainObject();
getGenesisBlockPeriodSeconds(getActualGenesisConfigOptions())
.ifPresent(miningParameters::setBlockPeriodSeconds);
initMiningParametersMetrics(miningParameters);
}
return miningParameters;

@ -42,7 +42,6 @@ import org.hyperledger.besu.plugin.services.TransactionSelectionService;
import org.hyperledger.besu.util.number.PositiveNumber;
import java.util.List;
import java.util.OptionalInt;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
@ -191,7 +190,6 @@ public class MiningOptions implements CLIOptions<MiningParameters> {
DEFAULT_POS_BLOCK_CREATION_REPETITION_MIN_DURATION;
}
private OptionalInt maybeGenesisBlockPeriodSeconds;
private TransactionSelectionService transactionSelectionService;
private MiningOptions() {}
@ -205,16 +203,6 @@ public class MiningOptions implements CLIOptions<MiningParameters> {
return new MiningOptions();
}
/**
* Set the optional genesis block period per seconds
*
* @param genesisBlockPeriodSeconds if the network is PoA then the block period in seconds
* specified in the genesis file, otherwise empty.
*/
public void setGenesisBlockPeriodSeconds(final OptionalInt genesisBlockPeriodSeconds) {
maybeGenesisBlockPeriodSeconds = genesisBlockPeriodSeconds;
}
/**
* Set the transaction selection service
*
@ -311,7 +299,6 @@ public class MiningOptions implements CLIOptions<MiningParameters> {
static MiningOptions fromConfig(final MiningParameters miningParameters) {
final MiningOptions miningOptions = MiningOptions.create();
miningOptions.setGenesisBlockPeriodSeconds(miningParameters.getGenesisBlockPeriodSeconds());
miningOptions.setTransactionSelectionService(miningParameters.getTransactionSelectionService());
miningOptions.isMiningEnabled = miningParameters.isMiningEnabled();
miningOptions.iStratumMiningEnabled = miningParameters.isStratumMiningEnabled();
@ -347,9 +334,6 @@ public class MiningOptions implements CLIOptions<MiningParameters> {
@Override
public MiningParameters toDomainObject() {
checkNotNull(
maybeGenesisBlockPeriodSeconds,
"genesisBlockPeriodSeconds must be set before using this object");
checkNotNull(
transactionSelectionService,
"transactionSelectionService must be set before using this object");
@ -370,7 +354,6 @@ public class MiningOptions implements CLIOptions<MiningParameters> {
}
return ImmutableMiningParameters.builder()
.genesisBlockPeriodSeconds(maybeGenesisBlockPeriodSeconds)
.transactionSelectionService(transactionSelectionService)
.mutableInitValues(updatableInitValuesBuilder.build())
.isStratumMiningEnabled(iStratumMiningEnabled)

@ -102,6 +102,18 @@ public class CliqueBesuControllerBuilder extends BesuControllerBuilder {
miningExecutor,
syncState,
new CliqueMiningTracker(localAddress, protocolContext));
// Update the next block period in seconds according to the transition schedule
protocolContext
.getBlockchain()
.observeBlockAdded(
o ->
miningParameters.setBlockPeriodSeconds(
forksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getBlockPeriodSeconds()));
miningCoordinator.addMinedBlockObserver(ethProtocolManager);
// Clique mining is implicitly enabled.

@ -234,6 +234,17 @@ public class IbftBesuControllerBuilder extends BftBesuControllerBuilder {
blockchain,
bftEventQueue);
// Update the next block period in seconds according to the transition schedule
protocolContext
.getBlockchain()
.observeBlockAdded(
o ->
miningParameters.setBlockPeriodSeconds(
forksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getBlockPeriodSeconds()));
if (syncState.isInitialSyncPhaseDone()) {
LOG.info("Starting IBFT mining coordinator");
ibftMiningCoordinator.enable();

@ -274,6 +274,17 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder {
blockchain,
bftEventQueue);
// Update the next block period in seconds according to the transition schedule
protocolContext
.getBlockchain()
.observeBlockAdded(
o ->
miningParameters.setBlockPeriodSeconds(
qbftForksSchedule
.getFork(o.getBlock().getHeader().getNumber() + 1)
.getValue()
.getBlockPeriodSeconds()));
if (syncState.isInitialSyncPhaseDone()) {
LOG.info("Starting QBFT mining coordinator");
miningCoordinator.enable();

@ -22,6 +22,7 @@ import org.hyperledger.besu.cli.CommandTestAbstract;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
@ -113,10 +114,18 @@ public abstract class AbstractCLIOptionsTest<D, T extends CLIOptions<D>>
protected abstract T getOptionsFromBesuCommand(final TestBesuCommand besuCommand);
protected void internalTestSuccess(final Consumer<D> assertion, final String... args) {
internalTestSuccess((bc, conf) -> conf, assertion, args);
}
protected void internalTestSuccess(
final BiFunction<TestBesuCommand, D, D> runtimeConf,
final Consumer<D> assertion,
final String... args) {
final TestBesuCommand cmd = parseCommand(args);
final T options = getOptionsFromBesuCommand(cmd);
final D config = options.toDomainObject();
final D config = runtimeConf.apply(cmd, options.toDomainObject());
assertion.accept(config);
assertThat(commandOutput.toString(UTF_8)).isEmpty();

@ -34,7 +34,6 @@ import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;
import java.util.OptionalInt;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.Test;
@ -316,6 +315,7 @@ public class MiningOptionsTest extends AbstractCLIOptionsTest<MiningParameters,
@Test
public void blockTxsSelectionMaxTimeDefaultValue() {
internalTestSuccess(
this::runtimeConfiguration,
miningParams ->
assertThat(miningParams.getNonPoaBlockTxsSelectionMaxTime())
.isEqualTo(DEFAULT_NON_POA_BLOCK_TXS_SELECTION_MAX_TIME));
@ -324,6 +324,7 @@ public class MiningOptionsTest extends AbstractCLIOptionsTest<MiningParameters,
@Test
public void blockTxsSelectionMaxTimeOption() {
internalTestSuccess(
this::runtimeConfiguration,
miningParams -> assertThat(miningParams.getBlockTxsSelectionMaxTime()).isEqualTo(1700L),
"--block-txs-selection-max-time",
"1700");
@ -343,6 +344,7 @@ public class MiningOptionsTest extends AbstractCLIOptionsTest<MiningParameters,
@Test
public void poaBlockTxsSelectionMaxTimeDefaultValue() {
internalTestSuccess(
this::runtimeConfiguration,
miningParams ->
assertThat(miningParams.getPoaBlockTxsSelectionMaxTime())
.isEqualTo(DEFAULT_POA_BLOCK_TXS_SELECTION_MAX_TIME));
@ -352,6 +354,7 @@ public class MiningOptionsTest extends AbstractCLIOptionsTest<MiningParameters,
public void poaBlockTxsSelectionMaxTimeOption() throws IOException {
final Path genesisFileIBFT2 = createFakeGenesisFile(VALID_GENESIS_IBFT2_POST_LONDON);
internalTestSuccess(
this::runtimeConfiguration,
miningParams ->
assertThat(miningParams.getPoaBlockTxsSelectionMaxTime())
.isEqualTo(PositiveNumber.fromInt(80)),
@ -365,6 +368,7 @@ public class MiningOptionsTest extends AbstractCLIOptionsTest<MiningParameters,
public void poaBlockTxsSelectionMaxTimeOptionOver100Percent() throws IOException {
final Path genesisFileClique = createFakeGenesisFile(VALID_GENESIS_CLIQUE_POST_LONDON);
internalTestSuccess(
this::runtimeConfiguration,
miningParams -> {
assertThat(miningParams.getPoaBlockTxsSelectionMaxTime())
.isEqualTo(PositiveNumber.fromInt(200));
@ -412,16 +416,19 @@ public class MiningOptionsTest extends AbstractCLIOptionsTest<MiningParameters,
@Override
protected MiningOptions getOptionsFromBesuCommand(final TestBesuCommand besuCommand) {
final var miningOptions = besuCommand.getMiningOptions();
miningOptions.setGenesisBlockPeriodSeconds(
besuCommand.getActualGenesisConfigOptions().isPoa()
? OptionalInt.of(POA_BLOCK_PERIOD_SECONDS)
: OptionalInt.empty());
return miningOptions;
return besuCommand.getMiningOptions();
}
@Override
protected String[] getNonOptionFields() {
return new String[] {"maybeGenesisBlockPeriodSeconds", "transactionSelectionService"};
return new String[] {"transactionSelectionService"};
}
private MiningParameters runtimeConfiguration(
final TestBesuCommand besuCommand, final MiningParameters miningParameters) {
if (besuCommand.getActualGenesisConfigOptions().isPoa()) {
miningParameters.setBlockPeriodSeconds(POA_BLOCK_PERIOD_SECONDS);
}
return miningParameters;
}
}

@ -0,0 +1,204 @@
/*
* 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.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import org.hyperledger.besu.config.CheckpointConfigOptions;
import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockBody;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderFunctions;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.ethereum.p2p.config.NetworkingConfiguration;
import org.hyperledger.besu.ethereum.storage.StorageProvider;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage;
import org.hyperledger.besu.ethereum.storage.keyvalue.VariablesKeyValueStorage;
import org.hyperledger.besu.ethereum.trie.forest.storage.ForestWorldStateKeyValueStorage;
import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration;
import org.hyperledger.besu.ethereum.worldstate.WorldStatePreimageStorage;
import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.evm.log.LogsBloomFilter;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage;
import java.math.BigInteger;
import java.nio.file.Path;
import java.time.Clock;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Range;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public abstract class AbstractBftBesuControllerBuilderTest {
protected BesuControllerBuilder bftBesuControllerBuilder;
@Mock protected GenesisConfigFile genesisConfigFile;
@Mock protected GenesisConfigOptions genesisConfigOptions;
@Mock private SynchronizerConfiguration synchronizerConfiguration;
@Mock private EthProtocolConfiguration ethProtocolConfiguration;
@Mock CheckpointConfigOptions checkpointConfigOptions;
@Mock private PrivacyParameters privacyParameters;
@Mock private Clock clock;
@Mock private StorageProvider storageProvider;
@Mock private GasLimitCalculator gasLimitCalculator;
@Mock private WorldStatePreimageStorage worldStatePreimageStorage;
private static final BigInteger networkId = BigInteger.ONE;
private static final NodeKey nodeKey = NodeKeyUtils.generate();
private final TransactionPoolConfiguration poolConfiguration =
TransactionPoolConfiguration.DEFAULT;
private final ObservableMetricsSystem observableMetricsSystem = new NoOpMetricsSystem();
protected final ObjectMapper objectMapper = new ObjectMapper();
private final MiningParameters miningParameters = MiningParameters.newDefault();
@TempDir Path tempDir;
@BeforeEach
public void setup() throws JsonProcessingException {
// besu controller setup
final ForestWorldStateKeyValueStorage worldStateKeyValueStorage =
mock(ForestWorldStateKeyValueStorage.class);
final WorldStateStorageCoordinator worldStateStorageCoordinator =
new WorldStateStorageCoordinator(worldStateKeyValueStorage);
lenient().when(genesisConfigFile.getParentHash()).thenReturn(Hash.ZERO.toHexString());
lenient().when(genesisConfigFile.getDifficulty()).thenReturn(Bytes.of(0).toHexString());
lenient().when(genesisConfigFile.getMixHash()).thenReturn(Hash.ZERO.toHexString());
lenient().when(genesisConfigFile.getNonce()).thenReturn(Long.toHexString(1));
lenient().when(genesisConfigFile.getConfigOptions(any())).thenReturn(genesisConfigOptions);
lenient().when(genesisConfigFile.getConfigOptions()).thenReturn(genesisConfigOptions);
lenient().when(genesisConfigOptions.getCheckpointOptions()).thenReturn(checkpointConfigOptions);
lenient()
.when(storageProvider.createBlockchainStorage(any(), any()))
.thenReturn(
new KeyValueStoragePrefixedKeyBlockchainStorage(
new InMemoryKeyValueStorage(),
new VariablesKeyValueStorage(new InMemoryKeyValueStorage()),
new MainnetBlockHeaderFunctions()));
lenient()
.when(
storageProvider.createWorldStateStorageCoordinator(
DataStorageConfiguration.DEFAULT_FOREST_CONFIG))
.thenReturn(worldStateStorageCoordinator);
lenient().when(worldStateKeyValueStorage.isWorldStateAvailable(any())).thenReturn(true);
lenient()
.when(worldStateKeyValueStorage.updater())
.thenReturn(mock(ForestWorldStateKeyValueStorage.Updater.class));
lenient()
.when(worldStatePreimageStorage.updater())
.thenReturn(mock(WorldStatePreimageStorage.Updater.class));
lenient()
.when(storageProvider.createWorldStatePreimageStorage())
.thenReturn(worldStatePreimageStorage);
lenient().when(synchronizerConfiguration.getDownloaderParallelism()).thenReturn(1);
lenient().when(synchronizerConfiguration.getTransactionsParallelism()).thenReturn(1);
lenient().when(synchronizerConfiguration.getComputationParallelism()).thenReturn(1);
lenient()
.when(synchronizerConfiguration.getBlockPropagationRange())
.thenReturn(Range.closed(1L, 2L));
setupBftGenesisConfigOptions();
bftBesuControllerBuilder =
createBftControllerBuilder()
.genesisConfigFile(genesisConfigFile)
.synchronizerConfiguration(synchronizerConfiguration)
.ethProtocolConfiguration(ethProtocolConfiguration)
.networkId(networkId)
.miningParameters(miningParameters)
.metricsSystem(observableMetricsSystem)
.privacyParameters(privacyParameters)
.dataDirectory(tempDir)
.clock(clock)
.transactionPoolConfiguration(poolConfiguration)
.dataStorageConfiguration(DataStorageConfiguration.DEFAULT_FOREST_CONFIG)
.nodeKey(nodeKey)
.storageProvider(storageProvider)
.gasLimitCalculator(gasLimitCalculator)
.evmConfiguration(EvmConfiguration.DEFAULT)
.networkConfiguration(NetworkingConfiguration.create());
}
protected abstract void setupBftGenesisConfigOptions() throws JsonProcessingException;
protected abstract BesuControllerBuilder createBftControllerBuilder();
@Test
public void miningParametersBlockPeriodSecondsIsUpdatedOnTransition() {
final var besuController = bftBesuControllerBuilder.build();
final var protocolContext = besuController.getProtocolContext();
final BlockHeader header1 =
new BlockHeader(
protocolContext.getBlockchain().getChainHeadHash(),
Hash.EMPTY_TRIE_HASH,
Address.ZERO,
Hash.EMPTY_TRIE_HASH,
Hash.EMPTY_TRIE_HASH,
Hash.EMPTY_TRIE_HASH,
LogsBloomFilter.builder().build(),
Difficulty.ONE,
1,
0,
0,
0,
protocolContext.getBlockchain().getChainHead().getBlockHeader().getExtraData(),
Wei.ZERO,
Hash.EMPTY,
0,
null,
null,
null,
null,
null,
getBlockHeaderFunctions());
final Block block1 = new Block(header1, BlockBody.empty());
protocolContext.getBlockchain().appendBlock(block1, List.of());
assertThat(miningParameters.getBlockPeriodSeconds()).isNotEmpty().hasValue(2);
assertThat(miningParameters.getBlockTxsSelectionMaxTime()).isEqualTo(2000 * 75 / 100);
}
protected abstract BlockHeaderFunctions getBlockHeaderFunctions();
}

@ -0,0 +1,231 @@
/*
* 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.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.config.CheckpointConfigOptions;
import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.ImmutableCliqueConfigOptions;
import org.hyperledger.besu.config.TransitionsConfigOptions;
import org.hyperledger.besu.consensus.clique.CliqueBlockHeaderFunctions;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockBody;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.ethereum.p2p.config.NetworkingConfiguration;
import org.hyperledger.besu.ethereum.storage.StorageProvider;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage;
import org.hyperledger.besu.ethereum.storage.keyvalue.VariablesKeyValueStorage;
import org.hyperledger.besu.ethereum.trie.forest.storage.ForestWorldStateKeyValueStorage;
import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration;
import org.hyperledger.besu.ethereum.worldstate.WorldStatePreimageStorage;
import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.evm.log.LogsBloomFilter;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage;
import java.math.BigInteger;
import java.nio.file.Path;
import java.time.Clock;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Range;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class CliqueBesuControllerBuilderTest {
private BesuControllerBuilder cliqueBesuControllerBuilder;
@Mock private GenesisConfigFile genesisConfigFile;
@Mock private GenesisConfigOptions genesisConfigOptions;
@Mock private SynchronizerConfiguration synchronizerConfiguration;
@Mock private EthProtocolConfiguration ethProtocolConfiguration;
@Mock private CheckpointConfigOptions checkpointConfigOptions;
@Mock private PrivacyParameters privacyParameters;
@Mock private Clock clock;
@Mock private StorageProvider storageProvider;
@Mock private GasLimitCalculator gasLimitCalculator;
@Mock private WorldStatePreimageStorage worldStatePreimageStorage;
private static final BigInteger networkId = BigInteger.ONE;
private static final NodeKey nodeKey = NodeKeyUtils.generate();
private final TransactionPoolConfiguration poolConfiguration =
TransactionPoolConfiguration.DEFAULT;
private final ObservableMetricsSystem observableMetricsSystem = new NoOpMetricsSystem();
private final ObjectMapper objectMapper = new ObjectMapper();
private final MiningParameters miningParameters = MiningParameters.newDefault();
@TempDir Path tempDir;
@BeforeEach
public void setup() throws JsonProcessingException {
// Clique Besu controller setup
final ForestWorldStateKeyValueStorage worldStateKeyValueStorage =
mock(ForestWorldStateKeyValueStorage.class);
final WorldStateStorageCoordinator worldStateStorageCoordinator =
new WorldStateStorageCoordinator(worldStateKeyValueStorage);
lenient().when(genesisConfigFile.getParentHash()).thenReturn(Hash.ZERO.toHexString());
lenient().when(genesisConfigFile.getDifficulty()).thenReturn(Bytes.of(0).toHexString());
when(genesisConfigFile.getExtraData())
.thenReturn(
"0x0000000000000000000000000000000000000000000000000000000000000000b9b81ee349c3807e46bc71aa2632203c5b4620340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
lenient().when(genesisConfigFile.getMixHash()).thenReturn(Hash.ZERO.toHexString());
lenient().when(genesisConfigFile.getNonce()).thenReturn(Long.toHexString(1));
lenient().when(genesisConfigFile.getConfigOptions(any())).thenReturn(genesisConfigOptions);
lenient().when(genesisConfigFile.getConfigOptions()).thenReturn(genesisConfigOptions);
lenient().when(genesisConfigOptions.getCheckpointOptions()).thenReturn(checkpointConfigOptions);
lenient()
.when(storageProvider.createBlockchainStorage(any(), any()))
.thenReturn(
new KeyValueStoragePrefixedKeyBlockchainStorage(
new InMemoryKeyValueStorage(),
new VariablesKeyValueStorage(new InMemoryKeyValueStorage()),
new MainnetBlockHeaderFunctions()));
lenient()
.when(
storageProvider.createWorldStateStorageCoordinator(
DataStorageConfiguration.DEFAULT_FOREST_CONFIG))
.thenReturn(worldStateStorageCoordinator);
lenient().when(worldStateKeyValueStorage.isWorldStateAvailable(any())).thenReturn(true);
lenient()
.when(worldStateKeyValueStorage.updater())
.thenReturn(mock(ForestWorldStateKeyValueStorage.Updater.class));
lenient()
.when(worldStatePreimageStorage.updater())
.thenReturn(mock(WorldStatePreimageStorage.Updater.class));
lenient()
.when(storageProvider.createWorldStatePreimageStorage())
.thenReturn(worldStatePreimageStorage);
lenient().when(synchronizerConfiguration.getDownloaderParallelism()).thenReturn(1);
lenient().when(synchronizerConfiguration.getTransactionsParallelism()).thenReturn(1);
lenient().when(synchronizerConfiguration.getComputationParallelism()).thenReturn(1);
lenient()
.when(synchronizerConfiguration.getBlockPropagationRange())
.thenReturn(Range.closed(1L, 2L));
// clique prepForBuild setup
lenient()
.when(genesisConfigOptions.getCliqueConfigOptions())
.thenReturn(
ImmutableCliqueConfigOptions.builder()
.epochLength(30)
.createEmptyBlocks(true)
.blockPeriodSeconds(1)
.build());
final var jsonTransitions =
(ObjectNode)
objectMapper.readTree(
"""
{"clique": [
{
"block": 2,
"blockperiodseconds": 2
}
]}
""");
lenient()
.when(genesisConfigOptions.getTransitions())
.thenReturn(new TransitionsConfigOptions(jsonTransitions));
cliqueBesuControllerBuilder =
new CliqueBesuControllerBuilder()
.genesisConfigFile(genesisConfigFile)
.synchronizerConfiguration(synchronizerConfiguration)
.ethProtocolConfiguration(ethProtocolConfiguration)
.networkId(networkId)
.miningParameters(miningParameters)
.metricsSystem(observableMetricsSystem)
.privacyParameters(privacyParameters)
.dataDirectory(tempDir)
.clock(clock)
.transactionPoolConfiguration(poolConfiguration)
.dataStorageConfiguration(DataStorageConfiguration.DEFAULT_FOREST_CONFIG)
.nodeKey(nodeKey)
.storageProvider(storageProvider)
.gasLimitCalculator(gasLimitCalculator)
.evmConfiguration(EvmConfiguration.DEFAULT)
.networkConfiguration(NetworkingConfiguration.create());
}
@Test
public void miningParametersBlockPeriodSecondsIsUpdatedOnTransition() {
final var besuController = cliqueBesuControllerBuilder.build();
final var protocolContext = besuController.getProtocolContext();
final BlockHeader header1 =
new BlockHeader(
protocolContext.getBlockchain().getChainHeadHash(),
Hash.EMPTY_TRIE_HASH,
Address.ZERO,
Hash.EMPTY_TRIE_HASH,
Hash.EMPTY_TRIE_HASH,
Hash.EMPTY_TRIE_HASH,
LogsBloomFilter.builder().build(),
Difficulty.ONE,
1,
0,
0,
0,
Bytes.EMPTY,
Wei.ZERO,
Hash.EMPTY,
0,
null,
null,
null,
null,
null,
new CliqueBlockHeaderFunctions());
final Block block1 = new Block(header1, BlockBody.empty());
protocolContext.getBlockchain().appendBlock(block1, List.of());
assertThat(miningParameters.getBlockPeriodSeconds()).isNotEmpty().hasValue(2);
assertThat(miningParameters.getBlockTxsSelectionMaxTime()).isEqualTo(2000 * 75 / 100);
}
}

@ -0,0 +1,73 @@
/*
* 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.controller;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.config.JsonBftConfigOptions;
import org.hyperledger.besu.config.TransitionsConfigOptions;
import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions;
import org.hyperledger.besu.consensus.common.bft.MutableBftConfigOptions;
import org.hyperledger.besu.consensus.ibft.IbftExtraDataCodec;
import org.hyperledger.besu.ethereum.core.BlockHeaderFunctions;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class IbftBesuControllerBuilderTest extends AbstractBftBesuControllerBuilderTest {
@Override
public void setupBftGenesisConfigOptions() throws JsonProcessingException {
// Ibft prepForBuild setup
lenient()
.when(genesisConfigOptions.getBftConfigOptions())
.thenReturn(new MutableBftConfigOptions(JsonBftConfigOptions.DEFAULT));
final var jsonTransitions =
(ObjectNode)
objectMapper.readTree(
"""
{"ibft2": [
{
"block": 2,
"blockperiodseconds": 2
}
]}
""");
lenient()
.when(genesisConfigOptions.getTransitions())
.thenReturn(new TransitionsConfigOptions(jsonTransitions));
when(genesisConfigFile.getExtraData())
.thenReturn(
"0xf83ea00000000000000000000000000000000000000000000000000000000000000000d594c2ab482b506de561668e07f04547232a72897daf808400000000c0");
}
@Override
protected BesuControllerBuilder createBftControllerBuilder() {
return new IbftBesuControllerBuilder();
}
@Override
protected BlockHeaderFunctions getBlockHeaderFunctions() {
return BftBlockHeaderFunctions.forOnchainBlock(new IbftExtraDataCodec());
}
}

@ -16,167 +16,77 @@ package org.hyperledger.besu.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.config.CheckpointConfigOptions;
import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.JsonQbftConfigOptions;
import org.hyperledger.besu.config.TransitionsConfigOptions;
import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions;
import org.hyperledger.besu.consensus.common.bft.BftContext;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.consensus.qbft.MutableQbftConfigOptions;
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
import org.hyperledger.besu.consensus.qbft.validator.ForkingValidatorProvider;
import org.hyperledger.besu.cryptoservices.NodeKey;
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.ProtocolContext;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.ethereum.p2p.config.NetworkingConfiguration;
import org.hyperledger.besu.ethereum.storage.StorageProvider;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStoragePrefixedKeyBlockchainStorage;
import org.hyperledger.besu.ethereum.storage.keyvalue.VariablesKeyValueStorage;
import org.hyperledger.besu.ethereum.trie.forest.storage.ForestWorldStateKeyValueStorage;
import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration;
import org.hyperledger.besu.ethereum.worldstate.WorldStatePreimageStorage;
import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage;
import java.math.BigInteger;
import java.nio.file.Path;
import java.time.Clock;
import org.hyperledger.besu.ethereum.core.BlockHeaderFunctions;
import java.util.List;
import com.google.common.collect.Range;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class QbftBesuControllerBuilderTest {
private BesuControllerBuilder qbftBesuControllerBuilder;
@Mock private GenesisConfigFile genesisConfigFile;
@Mock private GenesisConfigOptions genesisConfigOptions;
@Mock private SynchronizerConfiguration synchronizerConfiguration;
@Mock private EthProtocolConfiguration ethProtocolConfiguration;
@Mock CheckpointConfigOptions checkpointConfigOptions;
@Mock private MiningParameters miningParameters;
@Mock private PrivacyParameters privacyParameters;
@Mock private Clock clock;
@Mock private StorageProvider storageProvider;
@Mock private GasLimitCalculator gasLimitCalculator;
@Mock private WorldStatePreimageStorage worldStatePreimageStorage;
private static final BigInteger networkId = BigInteger.ONE;
private static final NodeKey nodeKey = NodeKeyUtils.generate();
private final TransactionPoolConfiguration poolConfiguration =
TransactionPoolConfiguration.DEFAULT;
private final ObservableMetricsSystem observableMetricsSystem = new NoOpMetricsSystem();
@TempDir Path tempDir;
@BeforeEach
public void setup() {
// besu controller setup
final ForestWorldStateKeyValueStorage worldStateKeyValueStorage =
mock(ForestWorldStateKeyValueStorage.class);
final WorldStateStorageCoordinator worldStateStorageCoordinator =
new WorldStateStorageCoordinator(worldStateKeyValueStorage);
lenient().when(genesisConfigFile.getParentHash()).thenReturn(Hash.ZERO.toHexString());
lenient().when(genesisConfigFile.getDifficulty()).thenReturn(Bytes.of(0).toHexString());
when(genesisConfigFile.getExtraData()).thenReturn(Bytes.EMPTY.toHexString());
lenient().when(genesisConfigFile.getMixHash()).thenReturn(Hash.ZERO.toHexString());
lenient().when(genesisConfigFile.getNonce()).thenReturn(Long.toHexString(1));
lenient().when(genesisConfigFile.getConfigOptions(any())).thenReturn(genesisConfigOptions);
lenient().when(genesisConfigFile.getConfigOptions()).thenReturn(genesisConfigOptions);
lenient().when(genesisConfigOptions.getCheckpointOptions()).thenReturn(checkpointConfigOptions);
lenient()
.when(storageProvider.createBlockchainStorage(any(), any()))
.thenReturn(
new KeyValueStoragePrefixedKeyBlockchainStorage(
new InMemoryKeyValueStorage(),
new VariablesKeyValueStorage(new InMemoryKeyValueStorage()),
new MainnetBlockHeaderFunctions()));
public class QbftBesuControllerBuilderTest extends AbstractBftBesuControllerBuilderTest {
lenient()
.when(
storageProvider.createWorldStateStorageCoordinator(
DataStorageConfiguration.DEFAULT_FOREST_CONFIG))
.thenReturn(worldStateStorageCoordinator);
lenient().when(worldStateKeyValueStorage.isWorldStateAvailable(any())).thenReturn(true);
lenient()
.when(worldStateKeyValueStorage.updater())
.thenReturn(mock(ForestWorldStateKeyValueStorage.Updater.class));
lenient()
.when(worldStatePreimageStorage.updater())
.thenReturn(mock(WorldStatePreimageStorage.Updater.class));
lenient()
.when(storageProvider.createWorldStatePreimageStorage())
.thenReturn(worldStatePreimageStorage);
lenient().when(synchronizerConfiguration.getDownloaderParallelism()).thenReturn(1);
lenient().when(synchronizerConfiguration.getTransactionsParallelism()).thenReturn(1);
lenient().when(synchronizerConfiguration.getComputationParallelism()).thenReturn(1);
lenient()
.when(synchronizerConfiguration.getBlockPropagationRange())
.thenReturn(Range.closed(1L, 2L));
@Override
public void setupBftGenesisConfigOptions() throws JsonProcessingException {
// qbft prepForBuild setup
lenient()
.when(genesisConfigOptions.getQbftConfigOptions())
.thenReturn(new MutableQbftConfigOptions(JsonQbftConfigOptions.DEFAULT));
final var jsonTransitions =
(ObjectNode)
objectMapper.readTree(
"""
{"qbft": [
{
"block": 2,
"blockperiodseconds": 2
}
]}
""");
lenient()
.when(genesisConfigOptions.getTransitions())
.thenReturn(mock(TransitionsConfigOptions.class));
.thenReturn(new TransitionsConfigOptions(jsonTransitions));
lenient()
.when(genesisConfigFile.getExtraData())
.thenReturn(
QbftExtraDataCodec.createGenesisExtraDataString(List.of(Address.fromHexString("1"))));
}
@Override
protected BesuControllerBuilder createBftControllerBuilder() {
return new QbftBesuControllerBuilder();
}
qbftBesuControllerBuilder =
new QbftBesuControllerBuilder()
.genesisConfigFile(genesisConfigFile)
.synchronizerConfiguration(synchronizerConfiguration)
.ethProtocolConfiguration(ethProtocolConfiguration)
.networkId(networkId)
.miningParameters(miningParameters)
.metricsSystem(observableMetricsSystem)
.privacyParameters(privacyParameters)
.dataDirectory(tempDir)
.clock(clock)
.transactionPoolConfiguration(poolConfiguration)
.dataStorageConfiguration(DataStorageConfiguration.DEFAULT_FOREST_CONFIG)
.nodeKey(nodeKey)
.storageProvider(storageProvider)
.gasLimitCalculator(gasLimitCalculator)
.evmConfiguration(EvmConfiguration.DEFAULT)
.networkConfiguration(NetworkingConfiguration.create());
@Override
protected BlockHeaderFunctions getBlockHeaderFunctions() {
return BftBlockHeaderFunctions.forOnchainBlock(new QbftExtraDataCodec());
}
@Test
public void forkingValidatorProviderIsAvailableOnBftContext() {
final BesuController besuController = qbftBesuControllerBuilder.build();
final BesuController besuController = bftBesuControllerBuilder.build();
final ValidatorProvider validatorProvider =
besuController
@ -192,7 +102,7 @@ public class QbftBesuControllerBuilderTest {
when(protocolContext.getBlockchain()).thenReturn(mock(MutableBlockchain.class));
assertThatThrownBy(
() -> qbftBesuControllerBuilder.createAdditionalJsonRpcMethodFactory(protocolContext))
() -> bftBesuControllerBuilder.createAdditionalJsonRpcMethodFactory(protocolContext))
.isInstanceOf(NullPointerException.class)
.hasMessage("transactionValidatorProvider should have been initialised");
}

@ -42,7 +42,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class CliqueBlockSchedulerTest {
private final KeyPair proposerKeyPair = SignatureAlgorithmFactory.getInstance().generateKeyPair();
private Address localAddr;

@ -1405,9 +1405,9 @@ public abstract class AbstractBlockTransactionSelectorTest {
MutableInitValues.builder()
.minTransactionGasPrice(minGasPrice)
.minBlockOccupancyRatio(minBlockOccupancyRatio)
.blockPeriodSeconds(genesisBlockPeriodSeconds)
.build())
.transactionSelectionService(transactionSelectionService)
.genesisBlockPeriodSeconds(genesisBlockPeriodSeconds)
.poaBlockTxsSelectionMaxTime(minBlockTimePercentage)
.build();
}

@ -122,6 +122,15 @@ public abstract class MiningParameters {
return this;
}
public OptionalInt getBlockPeriodSeconds() {
return getMutableRuntimeValues().blockPeriodSeconds;
}
public MiningParameters setBlockPeriodSeconds(final int blockPeriodSeconds) {
getMutableRuntimeValues().blockPeriodSeconds = OptionalInt.of(blockPeriodSeconds);
return this;
}
@Value.Default
public boolean isStratumMiningEnabled() {
return false;
@ -161,12 +170,10 @@ public abstract class MiningParameters {
};
}
public abstract OptionalInt getGenesisBlockPeriodSeconds();
@Value.Derived
public long getBlockTxsSelectionMaxTime() {
if (getGenesisBlockPeriodSeconds().isPresent()) {
return (TimeUnit.SECONDS.toMillis(getGenesisBlockPeriodSeconds().getAsInt())
final var maybeBlockPeriodSeconds = getMutableRuntimeValues().blockPeriodSeconds;
if (maybeBlockPeriodSeconds.isPresent()) {
return (TimeUnit.SECONDS.toMillis(maybeBlockPeriodSeconds.getAsInt())
* getPoaBlockTxsSelectionMaxTime().getValue())
/ 100;
}
@ -222,6 +229,8 @@ public abstract class MiningParameters {
return DEFAULT_MIN_BLOCK_OCCUPANCY_RATIO;
}
OptionalInt getBlockPeriodSeconds();
Optional<Address> getCoinbase();
OptionalLong getTargetGasLimit();
@ -238,6 +247,7 @@ public abstract class MiningParameters {
private volatile Optional<Address> coinbase;
private volatile OptionalLong targetGasLimit;
private volatile Optional<Iterable<Long>> nonceGenerator;
private volatile OptionalInt blockPeriodSeconds;
private MutableRuntimeValues(final MutableInitValues initValues) {
miningEnabled = initValues.isMiningEnabled();
@ -248,6 +258,7 @@ public abstract class MiningParameters {
coinbase = initValues.getCoinbase();
targetGasLimit = initValues.getTargetGasLimit();
nonceGenerator = initValues.nonceGenerator();
blockPeriodSeconds = initValues.getBlockPeriodSeconds();
}
@Override
@ -262,7 +273,8 @@ public abstract class MiningParameters {
&& Objects.equals(coinbase, that.coinbase)
&& Objects.equals(minPriorityFeePerGas, that.minPriorityFeePerGas)
&& Objects.equals(targetGasLimit, that.targetGasLimit)
&& Objects.equals(nonceGenerator, that.nonceGenerator);
&& Objects.equals(nonceGenerator, that.nonceGenerator)
&& Objects.equals(blockPeriodSeconds, that.blockPeriodSeconds);
}
@Override
@ -275,7 +287,8 @@ public abstract class MiningParameters {
minBlockOccupancyRatio,
coinbase,
targetGasLimit,
nonceGenerator);
nonceGenerator,
blockPeriodSeconds);
}
@Override
@ -297,6 +310,8 @@ public abstract class MiningParameters {
+ targetGasLimit
+ ", nonceGenerator="
+ nonceGenerator
+ ", blockPeriodSeconds="
+ blockPeriodSeconds
+ '}';
}
}

Loading…
Cancel
Save