diff --git a/CHANGELOG.md b/CHANGELOG.md index c79c6915b2d..9a9ee8924e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Breaking Changes +- Clique consensus has been removed. Besu can no longer start or mine on pure Clique networks. Syncing networks that started as Clique and have since transitioned to PoS via `terminalTotalDifficulty` (e.g. Linea Mainnet) are still supported. [#9852](https://github.com/hyperledger/besu/pull/9852) ### Upcoming Breaking Changes - RPC changes to enhance compatibility with other ELs @@ -11,7 +12,6 @@ - Holesky network is deprecated [#9437](https://github.com/hyperledger/besu/pull/9437) - Sunsetting features - for more context on the reasoning behind the deprecation of these features, including alternative options, read [this blog post](https://www.lfdecentralizedtrust.org/blog/sunsetting-tessera-and-simplifying-hyperledger-besu) - Proof of Work consensus (PoW) - - Clique Block Production (mining) - you will still be able to sync existing Clique networks, but not be a validator or create new Clique networks. ### Bug fixes - BFT forks that change block period on time-based forks don't take effect [9681](https://github.com/hyperledger/besu/issues/9681) diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java index 1279fd44573..59849e38844 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java @@ -17,7 +17,6 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ADMIN; -import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.CLIQUE; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.IBFT; import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.PLUGINS; import static org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.ConsensusType.QBFT; @@ -36,7 +35,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.node.Node; import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode; import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory; -import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory.CliqueOptions; import java.io.File; import java.io.IOException; @@ -46,7 +44,6 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.function.UnaryOperator; import org.apache.tuweni.bytes.Bytes; @@ -117,19 +114,14 @@ public BesuNode createArchiveNode( public BesuNode createNode( final String name, final UnaryOperator configModifier) throws IOException { - final BesuNodeConfigurationBuilder configBuilder = - configModifier.apply(new BesuNodeConfigurationBuilder().name(name)); - return create(configBuilder.build()); - } + final String[] enableRpcApis = new String[] {ADMIN.name()}; - public Node createArchiveNodeThatMustNotBeTheBootnode(final String name) throws IOException { - return create( + final BesuNodeConfigurationBuilder builder = new BesuNodeConfigurationBuilder() .name(name) - .jsonRpcEnabled() - .webSocketEnabled() - .bootnodeEligible(false) - .build()); + .jsonRpcConfiguration(node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis)); + + return create(configModifier.apply(builder).build()); } public Node createQbftNodeThatMustNotBeTheBootnode(final String name) throws IOException { @@ -233,15 +225,6 @@ public BesuNode createNodeWithAuthenticationUsingEcdsaJwtPublicKey(final String .build()); } - public BesuNode createNodeWithP2pDisabled(final String name) throws IOException { - return create( - new BesuNodeConfigurationBuilder() - .name(name) - .p2pEnabled(false) - .jsonRpcConfiguration(node.createJsonRpcEnabledConfig()) - .build()); - } - public BesuNode createArchiveNodeWithRpcDisabled(final String name) throws IOException { return create(new BesuNodeConfigurationBuilder().name(name).build()); } @@ -310,40 +293,14 @@ public BesuNode createQbftNode(final String name) throws IOException { return createQbftNode(name, UnaryOperator.identity()); } - public BesuNode createCliqueNode(final String name) throws IOException { - return createCliqueNode(name, UnaryOperator.identity()); - } - - public BesuNode createCliqueNode( - final String name, final UnaryOperator configModifier) - throws IOException { - final List enableRpcApis = new ArrayList<>(Arrays.asList()); - enableRpcApis.addAll(List.of(IBFT.name(), ADMIN.name(), CLIQUE.name())); - BesuNodeConfigurationBuilder builder = - new BesuNodeConfigurationBuilder() - .name(name) - .jsonRpcConfiguration( - node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis.toArray(String[]::new))) - .webSocketConfiguration(node.createWebSocketEnabledConfig()) - .devMode(false) - .jsonRpcTxPool() - .genesisConfigProvider(GenesisConfigurationFactory::createCliqueGenesisConfig); - - builder = configModifier.apply(builder); - - return create(builder.build()); - } - public BesuNode createQbftNode( final String name, final UnaryOperator configModifier) throws IOException { - final List enableRpcApis = new ArrayList<>(Arrays.asList()); - enableRpcApis.addAll(List.of(QBFT.name(), ADMIN.name())); + final String[] enableRpcApis = new String[] {QBFT.name(), ADMIN.name()}; BesuNodeConfigurationBuilder builder = new BesuNodeConfigurationBuilder() .name(name) - .jsonRpcConfiguration( - node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis.toArray(String[]::new))) + .jsonRpcConfiguration(node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis)) .webSocketConfiguration(node.createWebSocketEnabledConfig()) .devMode(false) .jsonRpcTxPool() @@ -358,68 +315,6 @@ public BesuNode createQbftNodeWithRevertReasonEnabled(final String name) throws return createQbftNode(name, BesuNodeConfigurationBuilder::revertReasonEnabled); } - public BesuNode createCliquePluginsNode( - final String name, - final List plugins, - final List extraCLIOptions, - final String... extraRpcApis) - throws IOException { - - final List enableRpcApis = new ArrayList<>(Arrays.asList(extraRpcApis)); - enableRpcApis.addAll(List.of(IBFT.name(), ADMIN.name(), PLUGINS.name(), CLIQUE.name())); - - return create( - new BesuNodeConfigurationBuilder() - .name(name) - .jsonRpcConfiguration( - node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis.toArray(String[]::new))) - .webSocketConfiguration(node.createWebSocketEnabledConfig()) - .plugins(plugins) - .extraCLIOptions(extraCLIOptions) - .devMode(false) - .jsonRpcTxPool() - .genesisConfigProvider( - validators -> - GenesisConfigurationFactory.createCliqueGenesisConfig( - validators, CliqueOptions.DEFAULT)) - .build()); - } - - public BesuNode createCliqueNode(final String name, final CliqueOptions cliqueOptions) - throws IOException { - return createCliqueNodeWithExtraCliOptionsAndRpcApis(name, cliqueOptions, List.of()); - } - - public BesuNode createCliqueNodeWithExtraCliOptionsAndRpcApis( - final String name, final CliqueOptions cliqueOptions, final List extraCliOptions) - throws IOException { - return createCliqueNodeWithExtraCliOptionsAndRpcApis( - name, cliqueOptions, extraCliOptions, Set.of()); - } - - public BesuNode createCliqueNodeWithExtraCliOptionsAndRpcApis( - final String name, - final CliqueOptions cliqueOptions, - final List extraCliOptions, - final Set extraRpcApis) - throws IOException { - return create( - new BesuNodeConfigurationBuilder() - .name(name) - .miningEnabled() - .jsonRpcConfiguration(node.createJsonRpcWithCliqueEnabledConfig(extraRpcApis)) - .webSocketConfiguration(node.createWebSocketEnabledConfig()) - .inProcessRpcConfiguration(node.createInProcessRpcConfiguration(extraRpcApis)) - .devMode(false) - .jsonRpcTxPool() - .genesisConfigProvider( - validators -> - GenesisConfigurationFactory.createCliqueGenesisConfig( - validators, cliqueOptions)) - .extraCLIOptions(extraCliOptions) - .build()); - } - public BesuNode createIbft2NonValidatorBootnode(final String name, final String genesisFile) throws IOException { return create( @@ -463,22 +358,6 @@ public BesuNode createIbft2NodeWithLocalAccountPermissioning( .build()); } - public BesuNode createIbft2Node(final String name, final String genesisFile) throws IOException { - return create( - new BesuNodeConfigurationBuilder() - .name(name) - .miningEnabled() - .jsonRpcConfiguration(node.createJsonRpcWithIbft2AdminEnabledConfig()) - .webSocketConfiguration(node.createWebSocketEnabledConfig()) - .devMode(false) - .genesisConfigProvider( - validators -> - GenesisConfigurationFactory.createIbft2GenesisConfigFilterBootnode( - validators, genesisFile)) - .bootnodeEligible(false) - .build()); - } - public BesuNode createIbft2Node( final String name, final boolean fixedPort, final DataStorageFormat storageFormat) throws IOException { @@ -659,34 +538,6 @@ public BesuNode createExecutionEngineGenesisNode(final String name, final String .build()); } - public BesuNode createCliqueNodeWithValidators(final String name, final String... validators) - throws IOException { - return createCliqueNodeWithValidators(name, CliqueOptions.DEFAULT, validators); - } - - public BesuNode createCliqueNodeWithValidators( - final String name, final CliqueOptions cliqueOptions, final String... validators) - throws IOException { - - return create( - new BesuNodeConfigurationBuilder() - .name(name) - .miningEnabled() - .jsonRpcConfiguration(node.createJsonRpcWithCliqueEnabledConfig(Set.of())) - .webSocketConfiguration(node.createWebSocketEnabledConfig()) - .jsonRpcTxPool() - .devMode(false) - .genesisConfigProvider( - nodes -> - node.createGenesisConfigForValidators( - asList(validators), - nodes, - vs -> - GenesisConfigurationFactory.createCliqueGenesisConfig( - vs, cliqueOptions))) - .build()); - } - public BesuNode createIbft2NodeWithValidators(final String name, final String... validators) throws IOException { @@ -706,25 +557,6 @@ public BesuNode createIbft2NodeWithValidators(final String name, final String... .build()); } - public BesuNode createQbftTLSNodeWithValidators( - final String name, final String type, final String... validators) throws IOException { - - return create( - new BesuNodeConfigurationBuilder() - .name(name) - .miningEnabled() - .jsonRpcConfiguration(node.createJsonRpcWithIbft2EnabledConfig(false)) - .webSocketConfiguration(node.createWebSocketEnabledConfig()) - .devMode(false) - .genesisConfigProvider( - nodes -> - node.createGenesisConfigForValidators( - asList(validators), - nodes, - GenesisConfigurationFactory::createIbft2GenesisConfig)) - .build()); - } - public BesuNode createQbftNodeWithValidators(final String name, final String... validators) throws IOException { @@ -789,33 +621,22 @@ private BesuNodeConfigurationBuilder createConfigurationBuilderWithStaticNodes( } public BesuNode createNodeWithNonDefaultSignatureAlgorithm( - final String name, - final String genesisPath, - final KeyPair keyPair, - final List staticNodes) - throws IOException { + final String name, final KeyPair keyPair, final List staticNodes) throws IOException { BesuNodeConfigurationBuilder builder = - createNodeConfigurationWithNonDefaultSignatureAlgorithm( - name, genesisPath, keyPair, staticNodes); + createNodeConfigurationWithNonDefaultSignatureAlgorithm(name, keyPair, staticNodes); return create(builder.build()); } public BesuNodeConfigurationBuilder createNodeConfigurationWithNonDefaultSignatureAlgorithm( - final String name, - final String genesisPath, - final KeyPair keyPair, - final List staticNodes) { - - final List enableRpcApis = new ArrayList<>(Arrays.asList()); - enableRpcApis.addAll(List.of(QBFT.name(), ADMIN.name())); + final String name, final KeyPair keyPair, final List staticNodes) { + final String[] enableRpcApis = new String[] {QBFT.name(), ADMIN.name()}; BesuNodeConfigurationBuilder builder = createConfigurationBuilderWithStaticNodes(name, staticNodes); builder = builder - .jsonRpcConfiguration( - node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis.toArray(String[]::new))) + .jsonRpcConfiguration(node.createJsonRpcWithRpcApiEnabledConfig(enableRpcApis)) .webSocketConfiguration(node.createWebSocketEnabledConfig()) .devMode(false) .jsonRpcTxPool() diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java index 2354acec87d..abeeeeddad7 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/genesis/GenesisConfigurationFactory.java @@ -160,6 +160,10 @@ private static Optional updateGenesisExtraData( return Optional.of(genesis); } + public static Optional createFromResource(final String resourceName) { + return Optional.of(readGenesisFile(resourceName)); + } + private static String updateGenesisCliqueOptions( final String template, final CliqueOptions cliqueOptions) { return template diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueDiscardRpcAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueDiscardRpcAcceptanceTest.java deleted file mode 100644 index 3ac42cb056c..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueDiscardRpcAcceptanceTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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.tests.acceptance.clique; - -import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -public class CliqueDiscardRpcAcceptanceTest extends AcceptanceTestBase { - - @Test - public void shouldDiscardVotes() throws IOException { - final String[] validators = {"validator1", "validator3"}; - final BesuNode validator1 = besu.createCliqueNodeWithValidators("validator1", validators); - final BesuNode validator2 = besu.createCliqueNodeWithValidators("validator2", validators); - final BesuNode validator3 = besu.createCliqueNodeWithValidators("validator3", validators); - cluster.start(validator1, validator2, validator3); - - validator1.execute(cliqueTransactions.createAddProposal(validator2)); - validator1.execute(cliqueTransactions.createRemoveProposal(validator3)); - validator1.verify( - clique.proposalsEqual().addProposal(validator2).removeProposal(validator3).build()); - - validator1.execute(cliqueTransactions.createDiscardProposal(validator2)); - validator1.verify(clique.proposalsEqual().removeProposal(validator3).build()); - - validator1.execute(cliqueTransactions.createDiscardProposal(validator3)); - cluster.verify(clique.noProposals()); - } -} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueGetSignersRpcAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueGetSignersRpcAcceptanceTest.java deleted file mode 100644 index 25a1d335105..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueGetSignersRpcAcceptanceTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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.tests.acceptance.clique; - -import static org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions.LATEST; - -import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -@Disabled("flaky test due to hardcoded block numbers") -public class CliqueGetSignersRpcAcceptanceTest extends AcceptanceTestBase { - private BesuNode minerNode1; - private BesuNode minerNode2; - - @BeforeEach - public void setUp() throws Exception { - final String[] validators = {"miner1"}; - minerNode1 = besu.createCliqueNodeWithValidators("miner1", validators); - minerNode2 = besu.createCliqueNodeWithValidators("miner2", validators); - cluster.start(minerNode1, minerNode2); - } - - @Test - public void shouldBeAbleToGetValidatorsForBlockNumber() { - cluster.verify(clique.validatorsAtBlockEqual("0x0", minerNode1)); - minerNode1.verify(blockchain.minimumHeight(1)); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode2)); - cluster.verify(blockchain.reachesHeight(minerNode1, 1)); - cluster.verify(clique.validatorsAtBlockEqual("0x2", minerNode1, minerNode2)); - cluster.verify(clique.validatorsAtBlockEqual(LATEST, minerNode1, minerNode2)); - } - - @Test - public void shouldBeAbleToGetValidatorsForBlockHash() { - cluster.verify(clique.validatorsAtBlockHashFromBlockNumberEqual(minerNode1, 0, minerNode1)); - minerNode1.verify(blockchain.minimumHeight(1)); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode2)); - cluster.verify(blockchain.reachesHeight(minerNode1, 1)); - cluster.verify( - clique.validatorsAtBlockHashFromBlockNumberEqual(minerNode1, 2, minerNode1, minerNode2)); - } -} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueMiningAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueMiningAcceptanceTest.java deleted file mode 100644 index 60333579f6f..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueMiningAcceptanceTest.java +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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.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.AcceptanceTestBase; -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.Disabled; -import org.junit.jupiter.api.Test; -import org.web3j.protocol.core.DefaultBlockParameter; - -public class CliqueMiningAcceptanceTest extends AcceptanceTestBase { - - @Test - public void shouldMineTransactionsOnSingleNode() throws IOException { - final BesuNode minerNode = besu.createCliqueNode("miner1"); - cluster.start(minerNode); - - final Account sender = accounts.createAccount("account1"); - final Account receiver = accounts.createAccount("account2"); - - minerNode.execute(accountTransactions.createTransfer(sender, 50)); - cluster.verify(sender.balanceEquals(50)); - - minerNode.execute(accountTransactions.createIncrementalTransfers(sender, receiver, 1)); - cluster.verify(receiver.balanceEquals(1)); - - minerNode.execute(accountTransactions.createIncrementalTransfers(sender, receiver, 2)); - cluster.verify(receiver.balanceEquals(3)); - } - - @Test - public void shouldNotMineBlocksIfNoTransactionsWhenCreateEmptyBlockIsFalse() throws IOException { - final var cliqueOptionsNoEmptyBlocks = - new CliqueOptions( - CliqueOptions.DEFAULT.blockPeriodSeconds(), CliqueOptions.DEFAULT.epochLength(), false); - final BesuNode minerNode = besu.createCliqueNode("miner1", cliqueOptionsNoEmptyBlocks); - cluster.start(minerNode); - - cluster.verify(clique.noNewBlockCreated(minerNode)); - } - - @Test - public void shouldMineBlocksOnlyWhenTransactionsArePresentWhenCreateEmptyBlocksIsFalse() - throws IOException { - final var cliqueOptionsNoEmptyBlocks = - new CliqueOptions( - CliqueOptions.DEFAULT.blockPeriodSeconds(), CliqueOptions.DEFAULT.epochLength(), false); - final BesuNode minerNode = besu.createCliqueNode("miner1", cliqueOptionsNoEmptyBlocks); - cluster.start(minerNode); - - final Account sender = accounts.createAccount("account1"); - - cluster.verify(clique.noNewBlockCreated(minerNode)); - - minerNode.execute(accountTransactions.createTransfer(sender, 50)); - - minerNode.verify(clique.blockIsCreatedByProposer(minerNode)); - } - - @Test - @Disabled("flaky see https://github.com/hyperledger/besu/issues/8862") - public void shouldMineTransactionsOnMultipleNodes() throws IOException { - final BesuNode minerNode1 = besu.createCliqueNode("miner1"); - final BesuNode minerNode2 = besu.createCliqueNode("miner2"); - final BesuNode minerNode3 = besu.createCliqueNode("miner3"); - startClusterAndVerifyProducingBlocks(minerNode1, minerNode2, minerNode3); - - final Account sender = accounts.createAccount("account1"); - final Account receiver = accounts.createAccount("account2"); - - minerNode1.execute(accountTransactions.createTransfer(sender, 50)); - cluster.verify(sender.balanceEquals(50)); - - minerNode2.execute(accountTransactions.createIncrementalTransfers(sender, receiver, 1)); - cluster.verify(receiver.balanceEquals(1)); - - minerNode3.execute(accountTransactions.createIncrementalTransfers(sender, receiver, 2)); - cluster.verify(receiver.balanceEquals(3)); - } - - @Test - public void shouldStallMiningWhenInsufficientValidators() throws IOException { - final BesuNode minerNode1 = besu.createCliqueNode("miner1"); - final BesuNode minerNode2 = besu.createCliqueNode("miner2"); - final BesuNode minerNode3 = besu.createCliqueNode("miner3"); - startClusterAndVerifyProducingBlocks(minerNode1, minerNode2, minerNode3); - - cluster.stopNode(minerNode2); - cluster.stopNode(minerNode3); - minerNode1.verify(net.awaitPeerCount(0)); - minerNode1.verify(clique.blockIsCreatedByProposer(minerNode1)); - - minerNode1.verify(clique.noNewBlockCreated(minerNode1)); - } - - private void startClusterAndVerifyProducingBlocks( - final BesuNode minerNode1, final BesuNode minerNode2, final BesuNode minerNode3) { - cluster.start(minerNode1, minerNode2, minerNode3); - - // verify nodes are fully connected otherwise blocks could not be propagated - minerNode1.verify(net.awaitPeerCount(2)); - minerNode2.verify(net.awaitPeerCount(2)); - minerNode3.verify(net.awaitPeerCount(2)); - - // verify that we have started producing blocks - waitForBlockHeight(minerNode1, 1); - final var minerChainHead = minerNode1.execute(ethTransactions.block()); - minerNode2.verify(blockchain.minimumHeight(minerChainHead.getNumber().longValue())); - minerNode3.verify(blockchain.minimumHeight(minerChainHead.getNumber().longValue())); - } - - @Test - @Disabled("flaky see https://github.com/hyperledger/besu/issues/8862") - public void shouldStillMineWhenANodeFailsAndHasSufficientValidators() throws IOException { - final BesuNode minerNode1 = besu.createCliqueNode("miner1"); - final BesuNode minerNode2 = besu.createCliqueNode("miner2"); - final BesuNode minerNode3 = besu.createCliqueNode("miner3"); - startClusterAndVerifyProducingBlocks(minerNode1, minerNode2, minerNode3); - - cluster.verifyOnActiveNodes(blockchain.reachesHeight(minerNode1, 1, 85)); - - cluster.stopNode(minerNode3); - cluster.verifyOnActiveNodes(net.awaitPeerCount(1)); - - cluster.verifyOnActiveNodes(blockchain.reachesHeight(minerNode1, 2)); - cluster.verifyOnActiveNodes(clique.blockIsCreatedByProposer(minerNode1)); - cluster.verifyOnActiveNodes(clique.blockIsCreatedByProposer(minerNode2)); - } - - @Test - public void shouldMineBlocksAccordingToBlockPeriodTransitions() throws IOException { - - final var cliqueOptions = new CliqueOptions(3, CliqueOptions.DEFAULT.epochLength(), true); - final BesuNode minerNode = besu.createCliqueNode("miner1", cliqueOptions); - - // setup transitions - final Map decreasePeriodTo2_Transition = - Map.of("block", 3, "blockperiodseconds", 2); - final Map decreasePeriodTo1_Transition = - Map.of("block", 4, "blockperiodseconds", 1); - // ensure previous blockperiodseconds transition is carried over - final Map dummy_Transition = Map.of("block", 5, "createemptyblocks", true); - final Map increasePeriodTo2_Transition = - Map.of("block", 6, "blockperiodseconds", 2); - - final Optional initialGenesis = - minerNode.getGenesisConfigProvider().create(List.of(minerNode)); - final String genesisWithTransitions = - prependTransitionsToCliqueOptions( - initialGenesis.orElseThrow(), - List.of( - decreasePeriodTo2_Transition, - decreasePeriodTo1_Transition, - dummy_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)); - } - - @Test - public void shouldMineBlocksAccordingToCreateEmptyBlocksTransitions() throws IOException { - - final var cliqueOptionsEmptyBlocks = - new CliqueOptions(2, CliqueOptions.DEFAULT.epochLength(), true); - final BesuNode minerNode = besu.createCliqueNode("miner1", cliqueOptionsEmptyBlocks); - - // setup transitions - final Map noEmptyBlocks_Transition = - Map.of("block", 3, "createemptyblocks", false); - final Map emptyBlocks_Transition = - Map.of("block", 4, "createemptyblocks", true); - final Map secondNoEmptyBlocks_Transition = - Map.of("block", 6, "createemptyblocks", false); - // ensure previous createemptyblocks transition is carried over - final Map dummy_Transition = Map.of("block", 7, "blockperiodseconds", 1); - - final Optional initialGenesis = - minerNode.getGenesisConfigProvider().create(List.of(minerNode)); - final String genesisWithTransitions = - prependTransitionsToCliqueOptions( - initialGenesis.orElseThrow(), - List.of( - noEmptyBlocks_Transition, - emptyBlocks_Transition, - secondNoEmptyBlocks_Transition, - dummy_Transition)); - minerNode.setGenesisConfig(genesisWithTransitions); - - final Account sender = accounts.createAccount("account1"); - - // Mine 2 blocks - cluster.start(minerNode); - minerNode.verify(blockchain.reachesHeight(minerNode, 1)); - - // tx required to mine block - cluster.verify(clique.noNewBlockCreated(minerNode)); - minerNode.execute(accountTransactions.createTransfer(sender, 50)); - minerNode.verify(clique.blockIsCreatedByProposer(minerNode)); - - // Mine 2 more blocks so chain head is 5 - minerNode.verify(blockchain.reachesHeight(minerNode, 2)); - - // tx required to mine block 6 - cluster.verify(clique.noNewBlockCreated(minerNode)); - minerNode.execute(accountTransactions.createTransfer(sender, 50)); - minerNode.verify(clique.blockIsCreatedByProposer(minerNode)); - - // check createemptyblocks transition carried over when other transition activated... - // tx required to mine block 7 - cluster.verify(clique.noNewBlockCreated(minerNode)); - } - - 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> 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> 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 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(); - } -} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueProposalRpcAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueProposalRpcAcceptanceTest.java deleted file mode 100644 index 6cbfbbda0f0..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueProposalRpcAcceptanceTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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.tests.acceptance.clique; - -import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -public class CliqueProposalRpcAcceptanceTest extends AcceptanceTestBase { - - @Test - public void shouldReturnProposals() throws IOException { - final String[] initialValidators = {"miner1", "miner2"}; - final BesuNode minerNode1 = besu.createCliqueNodeWithValidators("miner1", initialValidators); - final BesuNode minerNode2 = besu.createCliqueNodeWithValidators("miner2", initialValidators); - final BesuNode minerNode3 = besu.createCliqueNodeWithValidators("miner3", initialValidators); - cluster.start(minerNode1, minerNode2, minerNode3); - - cluster.verify(clique.noProposals()); - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode3)); - minerNode1.execute(cliqueTransactions.createRemoveProposal(minerNode2)); - minerNode2.execute(cliqueTransactions.createRemoveProposal(minerNode3)); - - minerNode1.verify( - clique.proposalsEqual().addProposal(minerNode3).removeProposal(minerNode2).build()); - minerNode2.verify(clique.proposalsEqual().removeProposal(minerNode3).build()); - minerNode3.verify(clique.noProposals()); - } -} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueProposeRpcAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueProposeRpcAcceptanceTest.java deleted file mode 100644 index fc179925a5f..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueProposeRpcAcceptanceTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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.tests.acceptance.clique; - -import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; -import org.hyperledger.besu.tests.acceptance.dsl.condition.Condition; -import org.hyperledger.besu.tests.acceptance.dsl.condition.clique.ExpectNonceVote.CLIQUE_NONCE_VOTE; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; -import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory; - -import java.io.IOException; -import java.util.Arrays; - -import org.junit.jupiter.api.Test; - -public class CliqueProposeRpcAcceptanceTest extends AcceptanceTestBase { - private static final GenesisConfigurationFactory.CliqueOptions CLIQUE_OPTIONS = - new GenesisConfigurationFactory.CliqueOptions(5, 30000, true); - - @Test - public void shouldAddAndRemoveValidators() throws IOException { - final String[] initialValidators = {"miner1"}; - final BesuNode minerNode1 = - besu.createCliqueNodeWithValidators("miner1", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode2 = - besu.createCliqueNodeWithValidators("miner2", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode3 = - besu.createCliqueNodeWithValidators("miner3", CLIQUE_OPTIONS, initialValidators); - cluster.start(minerNode1, minerNode2, minerNode3); - - waitForNodesConnectedAndInSync(minerNode1, minerNode2, minerNode3); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode2)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2)); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode3)); - minerNode2.execute(cliqueTransactions.createAddProposal(minerNode3)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2, minerNode3)); - - final Condition cliqueValidatorsChanged = clique.awaitSignerSetChange(minerNode2); - minerNode2.execute(cliqueTransactions.createRemoveProposal(minerNode1)); - minerNode3.execute(cliqueTransactions.createRemoveProposal(minerNode1)); - cluster.verify(clique.validatorsEqual(minerNode2, minerNode3)); - cluster.verify(cliqueValidatorsChanged); - } - - @Test - public void shouldNotAddValidatorWhenInsufficientVotes() throws IOException { - final String[] initialValidators = {"miner1"}; - final BesuNode minerNode1 = - besu.createCliqueNodeWithValidators("miner1", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode2 = - besu.createCliqueNodeWithValidators("miner2", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode3 = - besu.createCliqueNodeWithValidators("miner3", CLIQUE_OPTIONS, initialValidators); - cluster.start(minerNode1, minerNode2, minerNode3); - - waitForNodesConnectedAndInSync(minerNode1, minerNode2, minerNode3); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode2)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2)); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode3)); - minerNode1.verify(blockchain.reachesHeight(minerNode1, 1)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2)); - } - - @Test - public void shouldNotRemoveValidatorWhenInsufficientVotes() throws IOException { - final String[] initialValidators = {"miner1"}; - final BesuNode minerNode1 = - besu.createCliqueNodeWithValidators("miner1", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode2 = - besu.createCliqueNodeWithValidators("miner2", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode3 = - besu.createCliqueNodeWithValidators("miner3", CLIQUE_OPTIONS, initialValidators); - cluster.start(minerNode1, minerNode2, minerNode3); - - waitForNodesConnectedAndInSync(minerNode1, minerNode2, minerNode3); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode2)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2)); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode3)); - minerNode2.execute(cliqueTransactions.createAddProposal(minerNode3)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2, minerNode3)); - - minerNode1.execute(cliqueTransactions.createRemoveProposal(minerNode3)); - minerNode1.verify(blockchain.reachesHeight(minerNode1, 1)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2, minerNode3)); - } - - @Test - public void shouldIncludeVoteInBlockHeader() throws IOException { - final String[] initialValidators = {"miner1"}; - final BesuNode minerNode1 = - besu.createCliqueNodeWithValidators("miner1", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode2 = - besu.createCliqueNodeWithValidators("miner2", CLIQUE_OPTIONS, initialValidators); - final BesuNode minerNode3 = - besu.createCliqueNodeWithValidators("miner3", CLIQUE_OPTIONS, initialValidators); - cluster.start(minerNode1, minerNode2, minerNode3); - - waitForNodesConnectedAndInSync(minerNode1, minerNode2, minerNode3); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode2)); - minerNode1.verify(blockchain.reachesHeight(minerNode1, 1)); - minerNode1.verify(blockchain.beneficiaryEquals(minerNode2)); - minerNode1.verify(clique.nonceVoteEquals(CLIQUE_NONCE_VOTE.AUTH)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2)); - - minerNode1.execute(cliqueTransactions.createAddProposal(minerNode3)); - minerNode2.execute(cliqueTransactions.createAddProposal(minerNode3)); - minerNode1.verify(blockchain.reachesHeight(minerNode1, 1)); - minerNode2.verify(blockchain.reachesHeight(minerNode1, 1)); - minerNode1.verify(blockchain.beneficiaryEquals(minerNode3)); - minerNode2.verify(blockchain.beneficiaryEquals(minerNode3)); - minerNode1.verify(clique.nonceVoteEquals(CLIQUE_NONCE_VOTE.AUTH)); - minerNode2.verify(clique.nonceVoteEquals(CLIQUE_NONCE_VOTE.AUTH)); - cluster.verify(clique.validatorsEqual(minerNode1, minerNode2, minerNode3)); - - minerNode1.execute(cliqueTransactions.createRemoveProposal(minerNode2)); - minerNode1.verify(blockchain.reachesHeight(minerNode1, 1)); - minerNode1.verify(blockchain.beneficiaryEquals(minerNode2)); - minerNode1.verify(clique.nonceVoteEquals(CLIQUE_NONCE_VOTE.DROP)); - } - - private void waitForNodesConnectedAndInSync(final BesuNode... nodes) { - // verify nodes are fully connected otherwise blocks could not be propagated - Arrays.stream(nodes).forEach(node -> node.verify(net.awaitPeerCount(nodes.length - 1))); - - // verify that the miner started producing blocks and all other nodes are syncing from it - waitForBlockHeight(nodes[0], 1); - final var firstNodeChainHead = nodes[0].execute(ethTransactions.block()); - Arrays.stream(nodes) - .skip(1) - .forEach(node -> waitForBlockHeight(node, firstNodeChainHead.getNumber().longValue())); - } -} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java new file mode 100644 index 00000000000..913173442fb --- /dev/null +++ b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueToPoSTest.java @@ -0,0 +1,379 @@ +/* + * Copyright contributors to Besu. + * + * 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.tests.acceptance.clique; + +import static org.apache.logging.log4j.util.LoaderUtil.getClassLoader; +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.eth.sync.SyncMode; +import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; +import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.methods.response.EthBlock; + +public class CliqueToPoSTest extends AcceptanceTestBase { + + private static void copyKeyFile(final BesuNode node) throws IOException { + final String resourceFileName = "clique/key"; + try (InputStream keyFileStream = getClassLoader().getResourceAsStream(resourceFileName)) { + if (keyFileStream == null) { + throw new IOException("Resource not found: " + resourceFileName); + } + Path targetPath = node.homeDirectory().resolve("key"); + Files.createDirectories(targetPath.getParent()); + Files.copy(keyFileStream, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + } + + public static void runBesuCommand(final Path dataPath) throws IOException, InterruptedException { + ProcessBuilder processBuilder = + new ProcessBuilder( + "../../build/install/besu/bin/besu", + "--genesis-file", + "src/acceptanceTest/resources/clique/clique_to_pos.json", + "--data-path", + dataPath.toString(), + "--data-storage-format", + "BONSAI", + "blocks", + "import", + "src/acceptanceTest/resources/clique/clique.blocks"); + + processBuilder.directory(new File(System.getProperty("user.dir"))); + processBuilder.inheritIO(); // This will redirect the output to the console + + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + if (exitCode == 0) { + System.out.println("Import command executed successfully."); + } else { + throw new RuntimeException("Import command execution failed with exit code: " + exitCode); + } + } + + private static final MediaType MEDIA_TYPE_JSON = + MediaType.parse("application/json; charset=utf-8"); + + @Test + public void blocksAreBuiltAndNodesSyncAfterSwitchingToPoS() throws Exception { + + final BesuNode minerNode = + besu.createNode( + "miner", + besuNodeConfigurationBuilder -> + besuNodeConfigurationBuilder + .devMode(false) + .genesisConfigProvider( + unused -> + GenesisConfigurationFactory.createFromResource( + "/clique/clique_to_pos.json")) + .dataStorageConfiguration(DataStorageConfiguration.DEFAULT_BONSAI_CONFIG) + .engineRpcEnabled(true) + .miningEnabled()); + + // First sync node uses full sync and starts fresh; it does not produce blocks + final BesuNode syncNodeFull = + besu.createNode( + "full-syncer", + besuNodeConfigurationBuilder -> + besuNodeConfigurationBuilder + .devMode(false) + .genesisConfigProvider( + unused -> + GenesisConfigurationFactory.createFromResource( + "/clique/clique_to_pos.json")) + .dataStorageConfiguration(DataStorageConfiguration.DEFAULT_BONSAI_CONFIG) + .synchronizerConfiguration( + SynchronizerConfiguration.builder() + .syncMode(SyncMode.FULL) + .syncMinimumPeerCount(1) + .build()) + .engineRpcEnabled(true)); + + // Second sync node uses snap sync and starts fresh; it does not produce blocks + final BesuNode syncNodeSnap = + besu.createNode( + "snap-syncer", + besuNodeConfigurationBuilder -> + besuNodeConfigurationBuilder + .devMode(false) + .genesisConfigProvider( + unused -> + GenesisConfigurationFactory.createFromResource( + "/clique/clique_to_pos.json")) + .dataStorageConfiguration(DataStorageConfiguration.DEFAULT_BONSAI_CONFIG) + .engineRpcEnabled(true)); + + syncNodeSnap.setSynchronizerConfiguration( + SynchronizerConfiguration.builder() + .syncMode(SyncMode.SNAP) + .syncMinimumPeerCount(1) + .build()); + + // Copy key files to the miner node datadir + copyKeyFile(minerNode); + + // Import pre-built Clique blocks into the miner's datadir (up to TTD) + runBesuCommand(minerNode.homeDirectory()); + + // Start only the miner; syncNodeFull will be added after the PoS block is produced + cluster.start(minerNode); + + // Verify the imported blocks are present on the miner + minerNode.verify(blockchain.currentHeight(4)); + + // Build PoS blocks 5-10 on minerNode via Engine API + ObjectNode latestPayload = null; + for (int i = 5; i <= 10; i++) { + latestPayload = buildNewPoSBlock(minerNode); + } + minerNode.verify(blockchain.currentHeight(10)); + + final String block10Hash = latestPayload.get("blockHash").asText(); + + // Add the full sync node to the cluster so it peers with minerNode + cluster.addNode(syncNodeFull); + + // ensure the node reached TTD first + syncNodeFull.verify(blockchain.minimumHeight(4, 10)); + + // A single forkchoiceUpdatedV1 pointing at block 10 kicks off backward sync; + // syncNodeFull does not have the block yet so it responds SYNCING and begins downloading + triggerSyncViaForkchoiceUpdate(syncNodeFull, block10Hash); + + // Wait for full sync to complete and verify the full chain is present + syncNodeFull.verify(blockchain.minimumHeight(10, 30)); + + /* TODO: Uncomment when snap sync is fixed to work for small networks + + // Add the snap sync node to the cluster so it peers with minerNode + cluster.addNode(syncNodeSnap); + + syncNodeSnap.awaitPeerDiscovery(net.awaitPeerCount(2)); + + // A single forkchoiceUpdatedV1 pointing at block 10 kicks off snap sync to pivot; + // syncNodeSnap does not have the block yet so it responds SYNCING and begins downloading + triggerSyncViaForkchoiceUpdate(syncNodeSnap, block10Hash); + + // Wait for snap sync to complete and verify the full chain is present + syncNodeSnap.verify(blockchain.minimumHeight(10, 60)); + + */ + } + + /** + * Drives PoS block building on {@code node} via the Engine API and returns the resulting + * execution payload. + * + *
    + *
  1. engine_forkchoiceUpdatedV1 (with PayloadAttributesV1) → payloadId + *
  2. engine_getPayloadV1 → executionPayload + *
  3. engine_newPayloadV1 → VALID + *
  4. engine_forkchoiceUpdatedV1 (no attributes) → canonical head advanced + *
+ */ + private ObjectNode buildNewPoSBlock(final BesuNode node) + throws IOException, InterruptedException { + final OkHttpClient httpClient = new OkHttpClient(); + final ObjectMapper mapper = new ObjectMapper(); + + // Get current head block to derive hash and next timestamp + final EthBlock.Block currentBlock = node.execute(ethTransactions.block()); + final String headHash = currentBlock.getHash(); + final long nextTimestamp = currentBlock.getTimestamp().longValue() + 1; + + // Step 1: engine_forkchoiceUpdatedV1 with payload attributes to trigger block building + final String fckWithAttributesRequest = + "{" + + "\"jsonrpc\": \"2.0\"," + + "\"method\": \"engine_forkchoiceUpdatedV1\"," + + "\"params\": [" + + " {" + + " \"headBlockHash\": \"" + + headHash + + "\"," + + " \"safeBlockHash\": \"" + + headHash + + "\"," + + " \"finalizedBlockHash\": \"0x0000000000000000000000000000000000000000000000000000000000000000\"" + + " }," + + " {" + + " \"timestamp\": \"0x" + + Long.toHexString(nextTimestamp) + + "\"," + + " \"prevRandao\": \"0x0000000000000000000000000000000000000000000000000000000000000000\"," + + " \"suggestedFeeRecipient\": \"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b\"" + + " }" + + "]," + + "\"id\": 67" + + "}"; + + final String payloadId; + try (final Response response = + engineCall(httpClient, node, fckWithAttributesRequest).execute()) { + assertThat(response.code()).isEqualTo(200); + payloadId = mapper.readTree(response.body().string()).get("result").get("payloadId").asText(); + assertThat(payloadId).isNotEmpty(); + } + + // Wait for block building to complete + Thread.sleep(500); + + // Step 2: engine_getPayloadV1 to retrieve the built execution payload + final String getPayloadRequest = + "{" + + "\"jsonrpc\": \"2.0\"," + + "\"method\": \"engine_getPayloadV1\"," + + "\"params\": [\"" + + payloadId + + "\"]," + + "\"id\": 67" + + "}"; + + final ObjectNode executionPayload; + final String newBlockHash; + try (final Response response = engineCall(httpClient, node, getPayloadRequest).execute()) { + assertThat(response.code()).isEqualTo(200); + final JsonNode result = mapper.readTree(response.body().string()).get("result"); + executionPayload = (ObjectNode) result; + newBlockHash = executionPayload.get("blockHash").asText(); + assertThat(newBlockHash).isNotEmpty(); + } + + // Step 3: engine_newPayloadV1 to validate and import the new block + importPoSBlock(node, executionPayload); + + // Step 4: engine_forkchoiceUpdatedV1 to make the new block the canonical head + advanceChainHead(node, newBlockHash); + + return executionPayload; + } + + /** + * Submits an execution payload to {@code node} via engine_newPayloadV1 and asserts it is VALID. + * Safe to call even if the node already has the block (e.g. received it via p2p). + */ + private void importPoSBlock(final BesuNode node, final ObjectNode executionPayload) + throws IOException { + final OkHttpClient httpClient = new OkHttpClient(); + final ObjectMapper mapper = new ObjectMapper(); + + final String newPayloadRequest = + "{" + + "\"jsonrpc\": \"2.0\"," + + "\"method\": \"engine_newPayloadV1\"," + + "\"params\": [" + + executionPayload + + "]," + + "\"id\": 67" + + "}"; + + try (final Response response = engineCall(httpClient, node, newPayloadRequest).execute()) { + assertThat(response.code()).isEqualTo(200); + final String status = + mapper.readTree(response.body().string()).get("result").get("status").asText(); + assertThat(status).isEqualTo("VALID"); + } + } + + /** + * Calls engine_forkchoiceUpdatedV1 (without payload attributes) on {@code node} to set {@code + * blockHash} as the canonical chain head. The node already has the block, so the expected + * response is VALID. + */ + private void advanceChainHead(final BesuNode node, final String blockHash) throws IOException { + assertThat(forkchoiceUpdatedV1Status(node, blockHash)).isEqualTo("VALID"); + } + + /** + * Calls engine_forkchoiceUpdatedV1 on {@code node} pointing at a block the node does not yet + * have. The node responds with SYNCING and immediately begins snap-syncing from its peers to + * reach that block. + */ + private void triggerSyncViaForkchoiceUpdate(final BesuNode node, final String blockHash) + throws IOException { + assertThat(forkchoiceUpdatedV1Status(node, blockHash)).isEqualTo("SYNCING"); + } + + private String forkchoiceUpdatedV1Status(final BesuNode node, final String blockHash) + throws IOException { + final OkHttpClient httpClient = new OkHttpClient(); + final ObjectMapper mapper = new ObjectMapper(); + + final String fckRequest = + "{" + + "\"jsonrpc\": \"2.0\"," + + "\"method\": \"engine_forkchoiceUpdatedV1\"," + + "\"params\": [" + + " {" + + " \"headBlockHash\": \"" + + blockHash + + "\"," + + " \"safeBlockHash\": \"" + + blockHash + + "\"," + + " \"finalizedBlockHash\": \"0x0000000000000000000000000000000000000000000000000000000000000000\"" + + " }," + + " null" + + "]," + + "\"id\": 67" + + "}"; + + try (final Response response = engineCall(httpClient, node, fckRequest).execute()) { + assertThat(response.code()).isEqualTo(200); + return mapper + .readTree(response.body().string()) + .get("result") + .get("payloadStatus") + .get("status") + .asText(); + } + } + + private Call engineCall( + final OkHttpClient httpClient, final BesuNode node, final String request) { + return httpClient.newCall( + new Request.Builder() + .url(node.engineRpcUrl().get()) + .post(RequestBody.create(request, MEDIA_TYPE_JSON)) + .build()); + } + + @Override + public void tearDownAcceptanceTestBase() { + cluster.stop(); + super.tearDownAcceptanceTestBase(); + } +} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueZeroValidatorsAcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueZeroValidatorsAcceptanceTest.java deleted file mode 100644 index e124e669d92..00000000000 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/clique/CliqueZeroValidatorsAcceptanceTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * 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.tests.acceptance.clique; - -import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; -import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; - -import java.io.IOException; - -import org.junit.jupiter.api.Test; - -public class CliqueZeroValidatorsAcceptanceTest extends AcceptanceTestBase { - - @Test - public void zeroValidatorsFormValidCluster() throws IOException { - final String[] signers = {}; - final BesuNode node1 = besu.createCliqueNodeWithValidators("node1", signers); - final BesuNode node2 = besu.createCliqueNodeWithValidators("node2", signers); - - cluster.start(node1, node2); - - cluster.verify(net.awaitPeerCount(1)); - } -} diff --git a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java index 338020d41f4..776a345b57b 100644 --- a/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java +++ b/acceptance-tests/tests/src/acceptanceTest/java/org/hyperledger/besu/tests/acceptance/crypto/SECP256R1AcceptanceTest.java @@ -40,8 +40,6 @@ public class SECP256R1AcceptanceTest extends AcceptanceTestBase { private Node otherNode; private Cluster noDiscoveryCluster; - private static final String GENESIS_FILE = "/crypto/secp256r1.json"; - private static final String MINER_NODE_PRIVATE_KEY = "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; private static final String OTHER_NODE_PRIVATE_KEY = @@ -59,13 +57,12 @@ public void setUp() throws Exception { noDiscoveryCluster = new Cluster(clusterConfiguration, net); minerNode = - besu.createNodeWithNonDefaultSignatureAlgorithm( - "minerNode", GENESIS_FILE, minerNodeKeyPair, List.of()); + besu.createNodeWithNonDefaultSignatureAlgorithm("minerNode", minerNodeKeyPair, List.of()); noDiscoveryCluster.start(minerNode); otherNode = besu.createNodeWithNonDefaultSignatureAlgorithm( - "otherNode", GENESIS_FILE, otherNodeKeyPair, List.of(minerNode)); + "otherNode", otherNodeKeyPair, List.of(minerNode)); noDiscoveryCluster.addNode(otherNode); minerNode.verify(net.awaitPeerCount(1)); diff --git a/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique.blocks b/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique.blocks new file mode 100644 index 00000000000..f6b28373108 Binary files /dev/null and b/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique.blocks differ diff --git a/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique.json.tpl b/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique_to_pos.json similarity index 68% rename from acceptance-tests/tests/src/acceptanceTest/resources/clique/clique.json.tpl rename to acceptance-tests/tests/src/acceptanceTest/resources/clique/clique_to_pos.json index 0dce4f83020..252ab9a6730 100644 --- a/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique.json.tpl +++ b/acceptance-tests/tests/src/acceptanceTest/resources/clique/clique_to_pos.json @@ -1,18 +1,26 @@ { "config": { "chainId": 4, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, "byzantiumBlock": 0, - "constantinopleBlock": 6, - "petersburgBlock": 7, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "terminalTotalDifficulty": 9, "clique": { - "blockperiodseconds": %blockperiodseconds%, - "epochlength": %epochlength%, - "createemptyblocks": %createemptyblocks% + "blockperiodseconds": 10, + "epochlength": 30000, + "createemptyblocks": true } }, "nonce": "0x0", - "timestamp": "0x58ee40ba", - "extraData": "%extraData%", + "timestamp": "0x6391BFF3", + "extraData": "0x00000000000000000000000000000000000000000000000000000000000000003b63a1ee47ffd227458746ca9bb079d0d3cbb4bd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "gasLimit": "0x47b760", "difficulty": "0x1", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", diff --git a/acceptance-tests/tests/src/acceptanceTest/resources/clique/key b/acceptance-tests/tests/src/acceptanceTest/resources/clique/key new file mode 100644 index 00000000000..7c61691398f --- /dev/null +++ b/acceptance-tests/tests/src/acceptanceTest/resources/clique/key @@ -0,0 +1 @@ +0xd9e33039bb99f3a25b787bd644d2d054c5f3eef4743795187c96e92e15ec60e6 \ No newline at end of file diff --git a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index a022e7c39f6..f3e6f2e963e 100644 --- a/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -1895,9 +1895,6 @@ private boolean hasKzgFork(final GenesisConfigOptions genesisConfigOptions) { * @return the block period in seconds, or empty if not applicable */ private OptionalInt getBlockPeriodSeconds(final GenesisConfigOptions genesisConfigOptions) { - if (genesisConfigOptions.isClique()) { - return OptionalInt.of(genesisConfigOptions.getCliqueConfigOptions().getBlockPeriodSeconds()); - } if (genesisConfigOptions.isIbft2()) { return OptionalInt.of(genesisConfigOptions.getBftConfigOptions().getBlockPeriodSeconds()); } @@ -1918,8 +1915,6 @@ private OptionalLong getPoaEpochLength(final GenesisConfigOptions genesisConfigO return OptionalLong.of(genesisConfigOptions.getBftConfigOptions().getEpochLength()); } else if (genesisConfigOptions.isQbft()) { return OptionalLong.of(genesisConfigOptions.getQbftConfigOptions().getEpochLength()); - } else if (genesisConfigOptions.isClique()) { - return OptionalLong.of(genesisConfigOptions.getCliqueConfigOptions().getEpochLength()); } return OptionalLong.empty(); } @@ -1935,8 +1930,6 @@ private String getConsensusMechanism(final GenesisConfigOptions genesisConfigOpt return "IBFT2"; } else if (genesisConfigOptions.isQbft()) { return "QBFT"; - } else if (genesisConfigOptions.isClique()) { - return "Clique"; } else if (genesisConfigOptions.isEthHash()) { return "Ethash"; } diff --git a/app/src/main/java/org/hyperledger/besu/controller/BesuController.java b/app/src/main/java/org/hyperledger/besu/controller/BesuController.java index 5183a000a8b..6e886f35a46 100644 --- a/app/src/main/java/org/hyperledger/besu/controller/BesuController.java +++ b/app/src/main/java/org/hyperledger/besu/controller/BesuController.java @@ -360,6 +360,13 @@ public BesuControllerBuilder fromGenesisFile( } else if (configOptions.isQbft()) { builder = new QbftBesuControllerBuilder(); } else if (configOptions.isClique()) { + if (configOptions.getTerminalTotalDifficulty().isEmpty()) { + throw new IllegalStateException( + """ + Clique Block Production (mining) is no longer supported. + It is still possible to sync existing Clique networks if they are migrated to PoS. + """); + } builder = new CliqueBesuControllerBuilder(); } else { throw new IllegalArgumentException("Unknown consensus mechanism defined");