From b1ac5acd6067c32a85ed78c50c67da8042087c86 Mon Sep 17 00:00:00 2001 From: Matt Whitehead Date: Tue, 11 Jun 2024 11:46:36 +0100 Subject: [PATCH] Add new acceptance test to soak test BFT chains (#7023) * Add new acceptance test to soak test BFT chains Signed-off-by: Matthew Whitehead * Spotless Signed-off-by: Matthew Whitehead * Tidy up a little with re-usable start and stop functions with built in delays Signed-off-by: Matthew Whitehead * Add shanghai version of Simple Storage contract Signed-off-by: Matthew Whitehead * Put commented gradle code back in. Fix the web3j example commands in .sol files Signed-off-by: Matthew Whitehead * Spotless Signed-off-by: Matthew Whitehead * Set publication artifacts to avoid clash Signed-off-by: Matthew Whitehead * Exclude from regular acceptance tests Signed-off-by: Matthew Whitehead * Add shanghai fork to the test. Stall the chain for less time to reduce the time taken to mine new blocks Signed-off-by: Matthew Whitehead * Tidy up Signed-off-by: Matthew Whitehead * Update acceptance-tests/tests/shanghai/build.gradle Co-authored-by: Simon Dudley Signed-off-by: Matt Whitehead * Tidy up var names Signed-off-by: Matthew Whitehead * Fix ports for IBFT2 as well as QBFT Signed-off-by: Matthew Whitehead * Remove maven publish spec, disable jar building for shanghai contract project Signed-off-by: Matthew Whitehead * web3j version Signed-off-by: Matthew Whitehead * Make fixed port optional when creating a BFT node Signed-off-by: Matthew Whitehead * Only check artifact coordinates for those starting 'org.*' Signed-off-by: Matthew Whitehead --------- Signed-off-by: Matthew Whitehead Signed-off-by: Matt Whitehead Signed-off-by: Matt Whitehead Co-authored-by: Simon Dudley Co-authored-by: Sally MacFarlane --- .../node/configuration/BesuNodeFactory.java | 49 ++- acceptance-tests/tests/build.gradle | 32 ++ .../tests/contracts/CrossContractReader.sol | 2 +- .../tests/contracts/EventEmitter.sol | 2 +- .../tests/contracts/RemoteSimpleStorage.sol | 2 +- .../tests/contracts/RevertReason.sol | 2 +- .../tests/contracts/SimpleStorage.sol | 2 +- acceptance-tests/tests/shanghai/build.gradle | 21 ++ .../SimpleStorageShanghai.sol | 31 ++ .../BftAcceptanceTestParameterization.java | 15 +- .../acceptance/bftsoak/BftMiningSoakTest.java | 351 ++++++++++++++++++ build.gradle | 2 +- settings.gradle | 1 + 13 files changed, 494 insertions(+), 18 deletions(-) create mode 100644 acceptance-tests/tests/shanghai/build.gradle create mode 100644 acceptance-tests/tests/shanghai/shanghaicontracts/SimpleStorageShanghai.sol create mode 100644 acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java 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 6ed5ee55f3..96ab9cf623 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 @@ -461,16 +461,30 @@ public class BesuNodeFactory { .build()); } - public BesuNode createIbft2Node(final String name) throws IOException { - return create( + public BesuNode createIbft2Node(final String name, final boolean fixedPort) throws IOException { + JsonRpcConfiguration rpcConfig = node.createJsonRpcWithIbft2EnabledConfig(false); + rpcConfig.addRpcApi("ADMIN,TXPOOL"); + if (fixedPort) { + rpcConfig.setPort( + Math.abs(name.hashCode() % 60000) + + 1024); // Generate a consistent port for p2p based on node name + } + BesuNodeConfigurationBuilder builder = new BesuNodeConfigurationBuilder() .name(name) .miningEnabled() - .jsonRpcConfiguration(node.createJsonRpcWithIbft2EnabledConfig(false)) + .jsonRpcConfiguration(rpcConfig) .webSocketConfiguration(node.createWebSocketEnabledConfig()) .devMode(false) - .genesisConfigProvider(GenesisConfigurationFactory::createIbft2GenesisConfig) - .build()); + .genesisConfigProvider(GenesisConfigurationFactory::createIbft2GenesisConfig); + if (fixedPort) { + builder.p2pPort( + Math.abs(name.hashCode() % 60000) + + 1024 + + 500); // Generate a consistent port for p2p based on node name (+ 500 to avoid + // clashing with RPC port or other nodes with a similar name) + } + return create(builder.build()); } public BesuNode createQbftNodeWithTLS(final String name, final String type) throws IOException { @@ -498,16 +512,31 @@ public class BesuNodeFactory { return createQbftNodeWithTLS(name, KeyStoreWrapper.KEYSTORE_TYPE_PKCS11); } - public BesuNode createQbftNode(final String name) throws IOException { - return create( + public BesuNode createQbftNode(final String name, final boolean fixedPort) throws IOException { + JsonRpcConfiguration rpcConfig = node.createJsonRpcWithQbftEnabledConfig(false); + rpcConfig.addRpcApi("ADMIN,TXPOOL"); + if (fixedPort) { + rpcConfig.setPort( + Math.abs(name.hashCode() % 60000) + + 1024); // Generate a consistent port for p2p based on node name + } + + BesuNodeConfigurationBuilder builder = new BesuNodeConfigurationBuilder() .name(name) .miningEnabled() - .jsonRpcConfiguration(node.createJsonRpcWithQbftEnabledConfig(false)) + .jsonRpcConfiguration(rpcConfig) .webSocketConfiguration(node.createWebSocketEnabledConfig()) .devMode(false) - .genesisConfigProvider(GenesisConfigurationFactory::createQbftGenesisConfig) - .build()); + .genesisConfigProvider(GenesisConfigurationFactory::createQbftGenesisConfig); + if (fixedPort) { + builder.p2pPort( + Math.abs(name.hashCode() % 60000) + + 1024 + + 500); // Generate a consistent port for p2p based on node name (+ 500 to avoid + // clashing with RPC port or other nodes with a similar name) + } + return create(builder.build()); } public BesuNode createCustomGenesisNode( diff --git a/acceptance-tests/tests/build.gradle b/acceptance-tests/tests/build.gradle index a9393b1ccf..1bc0e55567 100644 --- a/acceptance-tests/tests/build.gradle +++ b/acceptance-tests/tests/build.gradle @@ -24,6 +24,7 @@ solidity { resolvePackages = false // TODO: remove the forced version, when DEV network is upgraded to support latest forks version '0.8.19' + evmVersion 'london' } dependencies { @@ -79,6 +80,7 @@ dependencies { testImplementation 'org.web3j:besu' testImplementation 'org.web3j:core' testImplementation 'org.wiremock:wiremock' + testImplementation project(path: ':acceptance-tests:tests:shanghai') testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } @@ -153,6 +155,7 @@ task acceptanceTestMainnet(type: Test) { task acceptanceTestNotPrivacy(type: Test) { inputs.property "integration.date", LocalTime.now() // so it runs at every invocation exclude '**/privacy/**' + exclude '**/bftsoak/**' useJUnitPlatform {} @@ -205,6 +208,35 @@ task acceptanceTestCliqueBft(type: Test) { doFirst { mkdir "${buildDir}/jvmErrorLogs" } } +task acceptanceTestBftSoak(type: Test) { + inputs.property "integration.date", LocalTime.now() // so it runs at every invocation + include '**/bftsoak/**' + + useJUnitPlatform {} + + dependsOn(rootProject.installDist) + setSystemProperties(test.getSystemProperties()) + systemProperty 'acctests.runBesuAsProcess', 'true' + // Set to any time > 60 minutes to run the soak test for longer + // systemProperty 'acctests.soakTimeMins', '120' + systemProperty 'java.security.properties', "${buildDir}/resources/test/acceptanceTesting.security" + mustRunAfter rootProject.subprojects*.test + description = 'Runs BFT soak test.' + group = 'verification' + + jvmArgs "-XX:ErrorFile=${buildDir}/jvmErrorLogs/java_err_pid%p.log" + + testLogging { + exceptionFormat = 'full' + showStackTraces = true + showStandardStreams = true + showExceptions = true + showCauses = true + } + + doFirst { mkdir "${buildDir}/jvmErrorLogs" } +} + task acceptanceTestPrivacy(type: Test) { inputs.property "integration.date", LocalTime.now() // so it runs at every invocation include '**/privacy/**' diff --git a/acceptance-tests/tests/contracts/CrossContractReader.sol b/acceptance-tests/tests/contracts/CrossContractReader.sol index f43ce8b621..9524d5bde8 100644 --- a/acceptance-tests/tests/contracts/CrossContractReader.sol +++ b/acceptance-tests/tests/contracts/CrossContractReader.sol @@ -19,7 +19,7 @@ import "./EventEmitter.sol"; // compile with: // solc CrossContractReader.sol --bin --abi --optimize --overwrite -o . // then create web3j wrappers with: -// web3j solidity generate -b ./generated/CrossContractReader.bin -a ./generated/CrossContractReader.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated +// web3j generate solidity -b ./generated/CrossContractReader.bin -a ./generated/CrossContractReader.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated contract CrossContractReader { uint counter; diff --git a/acceptance-tests/tests/contracts/EventEmitter.sol b/acceptance-tests/tests/contracts/EventEmitter.sol index 2e6e29e59d..05b8868eee 100644 --- a/acceptance-tests/tests/contracts/EventEmitter.sol +++ b/acceptance-tests/tests/contracts/EventEmitter.sol @@ -17,7 +17,7 @@ pragma solidity >=0.7.0 <0.9.0; // compile with: // solc EventEmitter.sol --bin --abi --optimize --overwrite -o . // then create web3j wrappers with: -// web3j solidity generate -b ./generated/EventEmitter.bin -a ./generated/EventEmitter.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated +// web3j generate solidity -b ./generated/EventEmitter.bin -a ./generated/EventEmitter.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated contract EventEmitter { address owner; event stored(address _to, uint _amount); diff --git a/acceptance-tests/tests/contracts/RemoteSimpleStorage.sol b/acceptance-tests/tests/contracts/RemoteSimpleStorage.sol index 03c95dc2bd..f399658789 100644 --- a/acceptance-tests/tests/contracts/RemoteSimpleStorage.sol +++ b/acceptance-tests/tests/contracts/RemoteSimpleStorage.sol @@ -19,7 +19,7 @@ import "./SimpleStorage.sol"; // compile with: // solc RemoteSimpleStorage.sol --bin --abi --optimize --overwrite -o . // then create web3j wrappers with: -// web3j solidity generate -b ./generated/RemoteSimpleStorage.bin -a ./generated/RemoteSimpleStorage.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated +// web3j generate solidity -b ./generated/RemoteSimpleStorage.bin -a ./generated/RemoteSimpleStorage.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated contract RemoteSimpleStorage { SimpleStorage public simpleStorage; diff --git a/acceptance-tests/tests/contracts/RevertReason.sol b/acceptance-tests/tests/contracts/RevertReason.sol index b1270fe4cc..2d42cafe3c 100644 --- a/acceptance-tests/tests/contracts/RevertReason.sol +++ b/acceptance-tests/tests/contracts/RevertReason.sol @@ -17,7 +17,7 @@ pragma solidity >=0.7.0 <0.9.0; // compile with: // solc RevertReason.sol --bin --abi --optimize --overwrite -o . // then create web3j wrappers with: -// web3j solidity generate -b ./generated/RevertReason.bin -a ./generated/RevertReason.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated +// web3j generate solidity -b ./generated/RevertReason.bin -a ./generated/RevertReason.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated contract RevertReason { function revertWithRevertReason() public pure returns (bool) { diff --git a/acceptance-tests/tests/contracts/SimpleStorage.sol b/acceptance-tests/tests/contracts/SimpleStorage.sol index 9303def9e0..712e2a8745 100644 --- a/acceptance-tests/tests/contracts/SimpleStorage.sol +++ b/acceptance-tests/tests/contracts/SimpleStorage.sol @@ -17,7 +17,7 @@ pragma solidity >=0.7.0 <0.8.20; // compile with: // solc SimpleStorage.sol --bin --abi --optimize --overwrite -o . // then create web3j wrappers with: -// web3j solidity generate -b ./generated/SimpleStorage.bin -a ./generated/SimpleStorage.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated +// web3j generate solidity -b ./generated/SimpleStorage.bin -a ./generated/SimpleStorage.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated contract SimpleStorage { uint data; diff --git a/acceptance-tests/tests/shanghai/build.gradle b/acceptance-tests/tests/shanghai/build.gradle new file mode 100644 index 0000000000..c309d9e991 --- /dev/null +++ b/acceptance-tests/tests/shanghai/build.gradle @@ -0,0 +1,21 @@ + +plugins { + id 'org.web3j' version '4.11.3' + id 'org.web3j.solidity' version '0.4.1' +} + +jar { enabled = true } + +web3j { + generatedPackageName = 'org.hyperledger.besu.tests.web3j.generated' +} + +sourceSets.main.solidity.srcDirs = [ + "$projectDir/shanghaicontracts" +] + +solidity { + resolvePackages = false + version '0.8.25' + evmVersion 'shanghai' +} diff --git a/acceptance-tests/tests/shanghai/shanghaicontracts/SimpleStorageShanghai.sol b/acceptance-tests/tests/shanghai/shanghaicontracts/SimpleStorageShanghai.sol new file mode 100644 index 0000000000..32a7d9a2a0 --- /dev/null +++ b/acceptance-tests/tests/shanghai/shanghaicontracts/SimpleStorageShanghai.sol @@ -0,0 +1,31 @@ +/* + * Copyright contributors to Hyperledger 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 + */ +pragma solidity >=0.8.20; + +// compile with: +// solc SimpleStorageShanghai.sol --bin --abi --optimize --overwrite -o . +// then create web3j wrappers with: +// web3j generate solidity -b ./SimpleStorageShanghai.bin -a ./SimpleStorageShanghai.abi -o ../../../../../ -p org.hyperledger.besu.tests.web3j.generated +contract SimpleStorageShanghai { + uint data; + + function set(uint value) public { + data = value; + } + + function get() public view returns (uint) { + return data; + } +} diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftAcceptanceTestParameterization.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftAcceptanceTestParameterization.java index c9fcf36484..15872070d0 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftAcceptanceTestParameterization.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/BftAcceptanceTestParameterization.java @@ -38,7 +38,14 @@ public class BftAcceptanceTestParameterization { @FunctionalInterface public interface NodeCreator { - BesuNode create(BesuNodeFactory factory, String name) throws Exception; + BesuNode create(BesuNodeFactory factory, String name, boolean fixedPort) throws Exception; + } + + @FunctionalInterface + public interface FixedPortNodeCreator { + + BesuNode createFixedPort(BesuNodeFactory factory, String name, boolean fixedPort) + throws Exception; } @FunctionalInterface @@ -57,7 +64,11 @@ public class BftAcceptanceTestParameterization { } public BesuNode createNode(BesuNodeFactory factory, String name) throws Exception { - return creatorFn.create(factory, name); + return creatorFn.create(factory, name, false); + } + + public BesuNode createNodeFixedPort(BesuNodeFactory factory, String name) throws Exception { + return creatorFn.create(factory, name, true); } public BesuNode createNodeWithValidators( diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java new file mode 100644 index 0000000000..9861a7dab6 --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bftsoak/BftMiningSoakTest.java @@ -0,0 +1,351 @@ +/* + * Copyright contributors to Hyperledger 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.bftsoak; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.config.JsonUtil; +import org.hyperledger.besu.tests.acceptance.bft.BftAcceptanceTestParameterization; +import org.hyperledger.besu.tests.acceptance.bft.ParameterizedBftTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; +import org.hyperledger.besu.tests.web3j.generated.SimpleStorage; +import org.hyperledger.besu.tests.web3j.generated.SimpleStorageShanghai; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class BftMiningSoakTest extends ParameterizedBftTestBase { + + private final int NUM_STEPS = 5; + + private final int MIN_TEST_TIME_MINS = 60; + + private static final long ONE_MINUTE = Duration.of(1, ChronoUnit.MINUTES).toMillis(); + + private static final long THREE_MINUTES = Duration.of(1, ChronoUnit.MINUTES).toMillis(); + + private static final long TEN_SECONDS = Duration.of(10, ChronoUnit.SECONDS).toMillis(); + + static int getTestDurationMins() { + // Use a default soak time of 60 mins + return Integer.getInteger("acctests.soakTimeMins", 60); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("factoryFunctions") + public void shouldBeStableDuringLongTest( + final String testName, final BftAcceptanceTestParameterization nodeFactory) throws Exception { + setUp(testName, nodeFactory); + + // There is a minimum amount of time the test can be run for, due to hard coded delays + // in between certain steps. There should be no upper-limit to how long the test is run for + assertThat(getTestDurationMins()).isGreaterThanOrEqualTo(MIN_TEST_TIME_MINS); + + final BesuNode minerNode1 = nodeFactory.createNodeFixedPort(besu, "miner1"); + final BesuNode minerNode2 = nodeFactory.createNodeFixedPort(besu, "miner2"); + final BesuNode minerNode3 = nodeFactory.createNodeFixedPort(besu, "miner3"); + final BesuNode minerNode4 = nodeFactory.createNodeFixedPort(besu, "miner4"); + + // Each step should be given a minimum of 3 minutes to complete successfully. If the time + // give to run the soak test results in a time-per-step lower than this then the time + // needs to be increased. + assertThat(getTestDurationMins() / NUM_STEPS).isGreaterThanOrEqualTo(3); + + cluster.start(minerNode1, minerNode2, minerNode3, minerNode4); + + cluster.verify(blockchain.reachesHeight(minerNode1, 1, 85)); + + // Setup + // Deploy a contract that we'll invoke periodically to ensure state + // is correct during the test, especially after stopping nodes and + // applying new forks. + SimpleStorage simpleStorageContract = + minerNode1.execute(contractTransactions.createSmartContract(SimpleStorage.class)); + + // Check the contract address is as expected for this sender & nonce + contractVerifier + .validTransactionReceipt("0x42699a7612a82f1d9c36148af9c77354759b210b") + .verify(simpleStorageContract); + + // Before upgrading to newer forks, try creating a shanghai-evm contract and check that + // the transaction fails + try { + minerNode1.execute(contractTransactions.createSmartContract(SimpleStorageShanghai.class)); + Assertions.fail("Shanghai transaction should not be executed on a pre-shanghai chain"); + } catch (RuntimeException e) { + assertThat(e.getMessage()) + .contains( + "Revert reason: 'Transaction processing could not be completed due to an exception'"); + } + + // Should initially be set to 0 + assertThat(simpleStorageContract.get().send()).isEqualTo(BigInteger.valueOf(0)); + + // Set to something new + simpleStorageContract.set(BigInteger.valueOf(101)).send(); + + // Check the state of the contract has updated correctly. We'll set & get this several times + // during the test + assertThat(simpleStorageContract.get().send()).isEqualTo(BigInteger.valueOf(101)); + + // Step 1 + // Run for the configured time period, periodically checking that + // the chain is progressing as expected + BigInteger chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + assertThat(chainHeight.compareTo(BigInteger.ZERO)).isGreaterThanOrEqualTo(1); + BigInteger lastChainHeight = chainHeight; + + Instant startTime = Instant.now(); + Instant nextStepEndTime = startTime.plus(getTestDurationMins() / NUM_STEPS, ChronoUnit.MINUTES); + + while (System.currentTimeMillis() < nextStepEndTime.toEpochMilli()) { + Thread.sleep(ONE_MINUTE); + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + + // With 1-second block period chain height should have moved on by at least 50 blocks + assertThat(chainHeight.compareTo(lastChainHeight.add(BigInteger.valueOf(50)))) + .isGreaterThanOrEqualTo(1); + lastChainHeight = chainHeight; + } + Instant previousStepEndTime = Instant.now(); + + // Step 2 + // Stop one of the nodes, check that the chain continues mining + // blocks + stopNode(minerNode4); + + nextStepEndTime = + previousStepEndTime.plus(getTestDurationMins() / NUM_STEPS, ChronoUnit.MINUTES); + + while (System.currentTimeMillis() < nextStepEndTime.toEpochMilli()) { + Thread.sleep(ONE_MINUTE); + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + + // Chain height should have moved on by at least 5 blocks + assertThat(chainHeight.compareTo(lastChainHeight.add(BigInteger.valueOf(20)))) + .isGreaterThanOrEqualTo(1); + lastChainHeight = chainHeight; + } + previousStepEndTime = Instant.now(); + + // Step 3 + // Stop another one of the nodes, check that the chain now stops + // mining blocks + stopNode(minerNode3); + + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + lastChainHeight = chainHeight; + + // Leave the chain stalled for 3 minutes. Check no new blocks are mined. Then + // resume the other validators. + nextStepEndTime = previousStepEndTime.plus(3, ChronoUnit.MINUTES); + while (System.currentTimeMillis() < nextStepEndTime.toEpochMilli()) { + Thread.sleep(ONE_MINUTE); + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + + // Chain height should not have moved on + assertThat(chainHeight.equals(lastChainHeight)).isTrue(); + } + + // Step 4 + // Restart both of the stopped nodes. Check that the chain resumes + // mining blocks + startNode(minerNode3); + + startNode(minerNode4); + + previousStepEndTime = Instant.now(); + + // This step gives the stalled chain time to re-sync and agree on the next BFT round + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + nextStepEndTime = + previousStepEndTime.plus((getTestDurationMins() / NUM_STEPS), ChronoUnit.MINUTES); + lastChainHeight = chainHeight; + + while (System.currentTimeMillis() < nextStepEndTime.toEpochMilli()) { + Thread.sleep(ONE_MINUTE); + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + lastChainHeight = chainHeight; + } + previousStepEndTime = Instant.now(); + + // By this loop it should be producing blocks again + nextStepEndTime = + previousStepEndTime.plus(getTestDurationMins() / NUM_STEPS, ChronoUnit.MINUTES); + + while (System.currentTimeMillis() < nextStepEndTime.toEpochMilli()) { + Thread.sleep(ONE_MINUTE); + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + + // Chain height should have moved on by at least 1 block + assertThat(chainHeight.compareTo(lastChainHeight.add(BigInteger.valueOf(1)))) + .isGreaterThanOrEqualTo(1); + lastChainHeight = chainHeight; + } + + // Update our smart contract before upgrading from berlin to london + assertThat(simpleStorageContract.get().send()).isEqualTo(BigInteger.valueOf(101)); + simpleStorageContract.set(BigInteger.valueOf(201)).send(); + assertThat(simpleStorageContract.get().send()).isEqualTo(BigInteger.valueOf(201)); + + // Upgrade the chain from berlin to london in 120 blocks time + upgradeToLondon( + minerNode1, minerNode2, minerNode3, minerNode4, lastChainHeight.intValue() + 120); + + previousStepEndTime = Instant.now(); + + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + nextStepEndTime = + previousStepEndTime.plus(getTestDurationMins() / NUM_STEPS, ChronoUnit.MINUTES); + lastChainHeight = chainHeight; + + while (System.currentTimeMillis() < nextStepEndTime.toEpochMilli()) { + Thread.sleep(ONE_MINUTE); + chainHeight = minerNode1.execute(ethTransactions.blockNumber()); + + // Chain height should have moved on by at least 50 blocks + assertThat(chainHeight.compareTo(lastChainHeight.add(BigInteger.valueOf(50)))) + .isGreaterThanOrEqualTo(1); + lastChainHeight = chainHeight; + } + + // Check that the state of our smart contract is still correct + assertThat(simpleStorageContract.get().send()).isEqualTo(BigInteger.valueOf(201)); + + // Update it once more to check new transactions are mined OK + simpleStorageContract.set(BigInteger.valueOf(301)).send(); + assertThat(simpleStorageContract.get().send()).isEqualTo(BigInteger.valueOf(301)); + + // Upgrade the chain to shanghai in 120 seconds. Then try to deploy a shanghai contract + upgradeToShanghai( + minerNode1, minerNode2, minerNode3, minerNode4, Instant.now().getEpochSecond() + 120); + + Thread.sleep(THREE_MINUTES); + + SimpleStorageShanghai simpleStorageContractShanghai = + minerNode1.execute(contractTransactions.createSmartContract(SimpleStorageShanghai.class)); + + // Check the contract address is as expected for this sender & nonce + contractVerifier + .validTransactionReceipt("0x05d91b9031a655d08e654177336d08543ac4b711") + .verify(simpleStorageContractShanghai); + } + + private static void updateGenesisConfigToLondon( + final BesuNode minerNode, final boolean zeroBaseFeeEnabled, final int blockNumber) { + + if (minerNode.getGenesisConfig().isPresent()) { + final ObjectNode genesisConfigNode = + JsonUtil.objectNodeFromString(minerNode.getGenesisConfig().get()); + final ObjectNode config = (ObjectNode) genesisConfigNode.get("config"); + config.put("londonBlock", blockNumber); + config.put("zeroBaseFee", zeroBaseFeeEnabled); + minerNode.setGenesisConfig(genesisConfigNode.toString()); + } + } + + private static void updateGenesisConfigToShanghai( + final BesuNode minerNode, final long blockTimestamp) { + + if (minerNode.getGenesisConfig().isPresent()) { + final ObjectNode genesisConfigNode = + JsonUtil.objectNodeFromString(minerNode.getGenesisConfig().get()); + final ObjectNode config = (ObjectNode) genesisConfigNode.get("config"); + config.put("shanghaiTime", blockTimestamp); + minerNode.setGenesisConfig(genesisConfigNode.toString()); + } + } + + private void upgradeToLondon( + final BesuNode minerNode1, + final BesuNode minerNode2, + final BesuNode minerNode3, + final BesuNode minerNode4, + final int londonBlockNumber) + throws InterruptedException { + // Node 1 + stopNode(minerNode1); + updateGenesisConfigToLondon(minerNode1, true, londonBlockNumber); + startNode(minerNode1); + + // Node 2 + stopNode(minerNode2); + updateGenesisConfigToLondon(minerNode2, true, londonBlockNumber); + startNode(minerNode2); + + // Node 3 + stopNode(minerNode3); + updateGenesisConfigToLondon(minerNode3, true, londonBlockNumber); + startNode(minerNode3); + + // Node 4 + stopNode(minerNode4); + updateGenesisConfigToLondon(minerNode4, true, londonBlockNumber); + startNode(minerNode4); + } + + private void upgradeToShanghai( + final BesuNode minerNode1, + final BesuNode minerNode2, + final BesuNode minerNode3, + final BesuNode minerNode4, + final long shanghaiTime) + throws InterruptedException { + // Node 1 + stopNode(minerNode1); + updateGenesisConfigToShanghai(minerNode1, shanghaiTime); + startNode(minerNode1); + + // Node 2 + stopNode(minerNode2); + updateGenesisConfigToShanghai(minerNode2, shanghaiTime); + startNode(minerNode2); + + // Node 3 + stopNode(minerNode3); + updateGenesisConfigToShanghai(minerNode3, shanghaiTime); + startNode(minerNode3); + + // Node 4 + stopNode(minerNode4); + updateGenesisConfigToShanghai(minerNode4, shanghaiTime); + startNode(minerNode4); + } + + // Start a node with a delay before returning to give it time to start + private void startNode(final BesuNode node) throws InterruptedException { + cluster.startNode(node); + Thread.sleep(TEN_SECONDS); + } + + // Stop a node with a delay before returning to give it time to stop + private void stopNode(final BesuNode node) throws InterruptedException { + cluster.stopNode(node); + Thread.sleep(TEN_SECONDS); + } + + @Override + public void tearDownAcceptanceTestBase() { + cluster.stop(); + super.tearDownAcceptanceTestBase(); + } +} diff --git a/build.gradle b/build.gradle index 905e8b2c8a..4f34e67985 100644 --- a/build.gradle +++ b/build.gradle @@ -386,7 +386,7 @@ task checkMavenCoordinateCollisions { getAllprojects().forEach { if (it.properties.containsKey('publishing') && it.jar?.enabled) { def coordinate = it.publishing?.publications[0].coordinates - if (coordinates.containsKey(coordinate)) { + if (coordinate.toString().startsWith("org") && coordinates.containsKey(coordinate)) { throw new GradleException("Duplicate maven coordinates detected, ${coordinate} is used by " + "both ${coordinates[coordinate]} and ${it.path}.\n" + "Please add a `publishing` script block to one or both subprojects.") diff --git a/settings.gradle b/settings.gradle index 81f4cb8736..09a8d20d4c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,7 @@ rootProject.name='besu' include 'acceptance-tests:test-plugins' include 'acceptance-tests:dsl' include 'acceptance-tests:tests' +include 'acceptance-tests:tests:shanghai' include 'besu' include 'config' include 'consensus:clique'