From 6fe29e5dd9e23b9f730cad24bd1e768baff77e40 Mon Sep 17 00:00:00 2001 From: mark-terry <36909937+mark-terry@users.noreply.github.com> Date: Wed, 14 Apr 2021 13:15:48 +1000 Subject: [PATCH] "container-tests" module. (#1894) Signed-off-by: Mark Terry --- .circleci/config.yml | 26 +- container-tests/build.gradle | 17 + container-tests/tests/build.gradle | 38 +++ .../tests/container/ContainerTestBase.java | 297 ++++++++++++++++++ .../besu/tests/container/ContainerTests.java | 195 ++++++++++++ .../container/helpers/ContractOperations.java | 162 ++++++++++ .../test/resources/besu/config/besuAddress | 1 + .../tests/src/test/resources/besu/data/key | 1 + .../tests/src/test/resources/genesis.json | 45 +++ .../src/test/resources/goQuorum/qdata/nodeKey | 1 + .../tests/src/test/resources/ipc/.empty | 0 .../tests/src/test/resources/keys/key1.key | 6 + .../tests/src/test/resources/keys/key1.pub | 1 + .../tests/src/test/resources/keys/key2.key | 6 + .../tests/src/test/resources/keys/key2.pub | 1 + .../test/resources/tessera/tesseraConfig.json | 47 +++ .../tests/src/test/solidity/TestContract.sol | 9 + gradle/check-licenses.gradle | 1 + gradle/versions.gradle | 3 + settings.gradle | 1 + 20 files changed, 857 insertions(+), 1 deletion(-) create mode 100644 container-tests/build.gradle create mode 100644 container-tests/tests/build.gradle create mode 100644 container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTestBase.java create mode 100644 container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTests.java create mode 100644 container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/helpers/ContractOperations.java create mode 100644 container-tests/tests/src/test/resources/besu/config/besuAddress create mode 100644 container-tests/tests/src/test/resources/besu/data/key create mode 100644 container-tests/tests/src/test/resources/genesis.json create mode 100644 container-tests/tests/src/test/resources/goQuorum/qdata/nodeKey create mode 100644 container-tests/tests/src/test/resources/ipc/.empty create mode 100644 container-tests/tests/src/test/resources/keys/key1.key create mode 100644 container-tests/tests/src/test/resources/keys/key1.pub create mode 100644 container-tests/tests/src/test/resources/keys/key2.key create mode 100644 container-tests/tests/src/test/resources/keys/key2.pub create mode 100644 container-tests/tests/src/test/resources/tessera/tesseraConfig.json create mode 100644 container-tests/tests/src/test/solidity/TestContract.sol diff --git a/.circleci/config.yml b/.circleci/config.yml index eaaa736a9e..bc90f8f9e4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,6 +26,10 @@ executors: environment: GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4 + machine_executor: + machine: + image: ubuntu-2004:202010-01 + notify: webhooks: - url: $HUBOT_URL @@ -191,6 +195,19 @@ jobs: - capture_test_results - capture_test_logs + containerTests: + parallelism: 1 + executor: machine_executor + steps: + - prepare + - attach_workspace: + at: ~/project + - run: + name: Container Tests + no_output_timeout: 10m + command: ./gradlew --no-daemon containerTests -i + - capture_test_results + buildDocker: executor: besu_executor_med steps: @@ -274,7 +291,12 @@ workflows: requires: - assemble context: - - besu-dockerhub-ro + - besu-dockerhub-ro + - containerTests: + requires: + - assemble + context: + - besu-dockerhub-ro - buildDocker: requires: - unitTests @@ -289,6 +311,7 @@ workflows: requires: - integrationTests - unitTests + - containerTests - acceptanceTests - referenceTests - buildDocker @@ -304,6 +327,7 @@ workflows: requires: - integrationTests - unitTests + - containerTests - acceptanceTests - referenceTests - buildDocker diff --git a/container-tests/build.gradle b/container-tests/build.gradle new file mode 100644 index 0000000000..9cd7e616d4 --- /dev/null +++ b/container-tests/build.gradle @@ -0,0 +1,17 @@ + +/* + * 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 + */ + +jar { enabled = false } diff --git a/container-tests/tests/build.gradle b/container-tests/tests/build.gradle new file mode 100644 index 0000000000..0401e8c385 --- /dev/null +++ b/container-tests/tests/build.gradle @@ -0,0 +1,38 @@ + +/* + * 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 + */ + +jar { enabled = false } + +dependencies { + testImplementation 'junit:junit' + testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.awaitility:awaitility' + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.web3j:core' + testImplementation 'org.web3j:quorum' +} + +test.enabled = false + +task containerTests(type: Test) { + description = 'Runs GoQuorum <> Besu container tests.' + dependsOn(rootProject.distDocker) + def dockerBuildVersion = project.hasProperty('release.releaseVersion') ? project.property('release.releaseVersion') : "${rootProject.version}" + def imageName = "hyperledger/besu" + def image = "${imageName}:${dockerBuildVersion}" + systemProperty 'containertest.imagename', image +} diff --git a/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTestBase.java b/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTestBase.java new file mode 100644 index 0000000000..2e351a18c5 --- /dev/null +++ b/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTestBase.java @@ -0,0 +1,297 @@ +/* + * 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.container; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import org.awaitility.Awaitility; +import org.awaitility.core.ThrowingRunnable; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.web3j.crypto.CipherException; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthBlockNumber; +import org.web3j.protocol.core.methods.response.Web3ClientVersion; +import org.web3j.protocol.http.HttpService; +import org.web3j.quorum.Quorum; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class ContainerTestBase { + private final String besuImage = System.getProperty("containertest.imagename"); + private final String goQuorumVersion = "21.1.0"; + private final String tesseraVersion = "21.1.0"; + + protected final String goQuorumTesseraPubKey = "3XGBIf+x8IdVQOVfIsbRnHwTYOJP/Fx84G8gMmy8qDM="; + protected final String besuTesseraPubKey = "8JJLEAbq6o9m4Kqm++v0Y1n9Z2ryAFtZTyhnxSKWgws="; + + private final Network containerNetwork = Network.SHARED; + protected Quorum besuWeb3j; + protected Quorum goQuorumWeb3j; + + // General config + private final String hostGenesisPath = "/genesis.json"; + private final String hostKeyPath = "/keys/"; + private final String containerKeyPath = "/tmp/keys/"; + private final Integer networkId = 2020; + + // Besu config + private final Integer besuP2pPort = 30303; + private final Integer besuRpcPort = 8545; + private final String besuContainerGenesisPath = "/opt/besu/genesis.json"; + private final String besuNetworkAlias = "besuNode"; + private final String hostBesuKeyPath = "/besu/data/key"; + private final String containerBesuKeyPath = "/opt/besu/key"; + + // GoQuorum + Tessera shared config + private final String ipcBindDir = "/tmp/ipc/"; + private final String ipcDirPath = "/ipc/"; + private final String ipcFilePath = "test.ipc"; + private final String containerIpcPath = ipcBindDir + ipcFilePath; + + // Tessera config + private final String tesseraContainerPrivKey1Path = "/tmp/keys/key1.key"; + private final String tesseraContainerPubKey1Path = "/tmp/keys/key1.pub"; + private final String tesseraContainerPrivKey2Path = "/tmp/keys/key2.key"; + private final String tesseraContainerPubKey2Path = "/tmp/keys/key2.pub"; + private final String tesseraContainerConfigFilePath = "/tmp/tessera/tesseraConfig.json"; + protected final int tesseraRestPort = 9081; + private final int tesseraQ2TRestPort = 9003; + private final String hostTesseraResources = "/tessera/"; + private final String containerTesseraResources = "/tmp/tessera/"; + private final int tesseraP2pPort = 9001; + + // GoQuorum config + private final String goQuorumContainerDatadir = "/tmp/qdata/"; + private final String goQuorumContainerGenesis = "/tmp/qdata/genesis.json"; + private final String goQuorumContainerGethIpcPath = goQuorumContainerDatadir + "/geth.ipc"; + private final Integer goQuorumP2pPort = 30303; + private final Integer goQuorumRpcPort = 8545; + + @Rule public final GenericContainer besuContainer = buildBesuContainer(); + + @Rule + public final GenericContainer tesseraGoQuorumContainer = + buildGoQuorumTesseraContainer( + ipcDirPath, + ipcBindDir, + containerIpcPath, + tesseraContainerPrivKey1Path, + tesseraContainerPubKey1Path); + + @Rule + public final GenericContainer tesseraBesuContainer = + buildBesuTesseraContainer(tesseraContainerPrivKey2Path, tesseraContainerPubKey2Path); + + @Rule + public final GenericContainer goQuorumContainer = + buildGoQuorumContainer(ipcDirPath, ipcBindDir, containerIpcPath);; + + @Before + public void setUp() throws IOException, InterruptedException { + besuWeb3j = + buildWeb3JQuorum( + besuContainer.getContainerIpAddress(), besuContainer.getMappedPort(besuRpcPort)); + goQuorumWeb3j = + buildWeb3JQuorum( + goQuorumContainer.getContainerIpAddress(), + goQuorumContainer.getMappedPort(goQuorumRpcPort)); + + waitFor(10, () -> assertClientVersion(besuWeb3j, "dev")); + waitFor(10, () -> assertClientVersion(goQuorumWeb3j, goQuorumVersion)); + + // Tell GoQuorum to peer to Besu + goQuorumContainer.execInContainer( + "geth", + "--exec", + "admin.addPeer(\"enode://11b72d1e2fdde254a493047d4061f3b62cc2ba59f4c1b0cf41dda4ba0d77f0bfe4f932ccf9f60203b1d47753f69edf1c80e755e3159e596b1f6aa03cb0c275c4@" + + besuNetworkAlias + + ":" + + besuP2pPort + + "\")", + "attach", + goQuorumContainerGethIpcPath); + + waitFor(30, () -> assertBlockHeight(besuWeb3j, 5)); + waitFor(30, () -> assertBlockHeight(goQuorumWeb3j, 5)); + } + + @After + public void tearDown() throws IOException { + boolean fileExists = Files.deleteIfExists(Path.of(getResourcePath(ipcDirPath + ipcFilePath))); + if (!fileExists) { + System.out.println("Unable to delete tx IPC file."); + } + } + + private GenericContainer buildBesuContainer() { + return new GenericContainer(besuImage) + .withNetwork(containerNetwork) + .withNetworkAliases(besuNetworkAlias) + .withExposedPorts(besuRpcPort, besuP2pPort) + .withClasspathResourceMapping(hostBesuKeyPath, containerBesuKeyPath, BindMode.READ_ONLY) + .withClasspathResourceMapping(hostGenesisPath, besuContainerGenesisPath, BindMode.READ_ONLY) + .withClasspathResourceMapping(hostKeyPath, containerKeyPath, BindMode.READ_ONLY) + .withCommand( + "--genesis-file", + besuContainerGenesisPath, + "--network-id", + networkId.toString(), + "--p2p-port", + besuP2pPort.toString(), + "--rpc-http-enabled", + "--rpc-http-port", + besuRpcPort.toString(), + "--rpc-http-api", + "ADMIN,ETH,NET,WEB3,GOQUORUM", + "--goquorum-compatibility-enabled", + "--min-gas-price", + "0", + "--privacy-public-key-file", + "/tmp/keys/key2.pub", + "--privacy-url", + "http://" + "besuTessera" + ':' + tesseraQ2TRestPort); + } + + private GenericContainer buildGoQuorumTesseraContainer( + final String ipcPath, + final String ipcBindDir, + final String containerIpcPath, + final String privKeyPath, + final String pubKeyPath) { + return new GenericContainer("quorumengineering/tessera:" + tesseraVersion) + .withNetwork(containerNetwork) + .withNetworkAliases("goQuorumTessera") + .withClasspathResourceMapping( + hostTesseraResources, containerTesseraResources, BindMode.READ_ONLY) + .withClasspathResourceMapping(ipcPath, ipcBindDir, BindMode.READ_WRITE) + .withClasspathResourceMapping(hostKeyPath, containerKeyPath, BindMode.READ_ONLY) + .withCommand( + "--configfile " + tesseraContainerConfigFilePath, + "-o serverConfigs[0].serverAddress=http://localhost:" + tesseraRestPort, + "-o serverConfigs[1].serverAddress=unix:" + containerIpcPath, + "-o serverConfigs[2].serverAddress=http://" + "goQuorumTessera" + ":" + tesseraP2pPort, + "-o keys.keyData[0].privateKeyPath=" + privKeyPath, + "-o keys.keyData[0].publicKeyPath=" + pubKeyPath, + "-o peer[0].url=http://" + "goQuorumTessera" + ":" + tesseraP2pPort + "/", + "-o peer[1].url=http://" + "besuTessera" + ":" + tesseraP2pPort + "/") + .withExposedPorts(tesseraP2pPort, tesseraRestPort) + .waitingFor(Wait.forHttp("/upcheck")); + } + + private GenericContainer buildBesuTesseraContainer( + final String privKeyPath, final String pubKeyPath) { + return new GenericContainer("quorumengineering/tessera:" + tesseraVersion) + .withNetwork(containerNetwork) + .withNetworkAliases("besuTessera") + .withClasspathResourceMapping( + hostTesseraResources, containerTesseraResources, BindMode.READ_ONLY) + .withClasspathResourceMapping(hostKeyPath, containerKeyPath, BindMode.READ_ONLY) + .withCommand( + "--configfile " + tesseraContainerConfigFilePath, + "-o serverConfigs[0].serverAddress=http://localhost:" + tesseraRestPort, + "-o serverConfigs[1].serverAddress=http://localhost:" + tesseraQ2TRestPort, + "-o serverConfigs[2].serverAddress=http://" + "besuTessera" + ":" + tesseraP2pPort, + "-o keys.keyData[0].privateKeyPath=" + privKeyPath, + "-o keys.keyData[0].publicKeyPath=" + pubKeyPath, + "-o peer[0].url=http://" + "besuTessera" + ":" + tesseraP2pPort + "/", + "-o peer[1].url=http://" + "goQuorumTessera" + ":" + tesseraP2pPort + "/") + .withExposedPorts(tesseraP2pPort, tesseraRestPort) + .waitingFor(Wait.forHttp("/upcheck")); + } + + private GenericContainer buildGoQuorumContainer( + final String ipcPath, final String ipcBindDir, final String containerIpcPath) { + return new GenericContainer("quorumengineering/quorum:" + goQuorumVersion) + .withNetwork(containerNetwork) + .dependsOn(tesseraGoQuorumContainer) + .withExposedPorts(goQuorumRpcPort, goQuorumP2pPort) + .withClasspathResourceMapping( + "/goQuorum/qdata/", goQuorumContainerDatadir, BindMode.READ_ONLY) + .withClasspathResourceMapping(hostGenesisPath, goQuorumContainerGenesis, BindMode.READ_ONLY) + .withClasspathResourceMapping(ipcPath, ipcBindDir, BindMode.READ_WRITE) + .withEnv("PRIVATE_CONFIG", containerIpcPath) + .withCreateContainerCmdModifier( + (Consumer) + cmd -> + cmd.withEntrypoint( + "/bin/ash", + "-c", + "geth init " + + goQuorumContainerGenesis + + " --datadir " + + goQuorumContainerDatadir + + " && geth --datadir " + + goQuorumContainerDatadir + + " --networkid " + + networkId.toString() + + " --rpc" + + " --rpcaddr 0.0.0.0" + + " --rpcport " + + goQuorumRpcPort.toString() + + " --rpcapi admin,eth,debug,miner,net,shh,txpool,personal,web3,quorum,quorumExtension,clique" + + " --emitcheckpoints" + + " --port " + + goQuorumP2pPort.toString() + + " --verbosity" + + " 3" + + " --nousb" + + " --nodekey " + + goQuorumContainerDatadir + + "/nodeKey")); + } + + private Quorum buildWeb3JQuorum(final String containerIpAddress, final Integer mappedPort) { + return Quorum.build(new HttpService("http://" + containerIpAddress + ":" + mappedPort)); + } + + private void assertClientVersion(final Web3j web3, final String clientString) throws IOException { + final Web3ClientVersion clientVersionResult = web3.web3ClientVersion().send(); + assertThat(clientVersionResult.getWeb3ClientVersion()).contains(clientString); + } + + private void assertBlockHeight(final Web3j web3j, final int blockHeight) throws IOException { + final EthBlockNumber blockNumberResult = web3j.ethBlockNumber().send(); + assertThat(blockNumberResult.getBlockNumber().intValueExact()) + .isGreaterThanOrEqualTo(blockHeight); + } + + public static void waitFor(final int timeout, final ThrowingRunnable condition) { + Awaitility.await() + .ignoreExceptions() + .atMost(timeout, TimeUnit.SECONDS) + .untilAsserted(condition); + } + + private String getResourcePath(final String resourceName) { + return getClass().getResource(resourceName).getPath(); + } + + protected Credentials loadCredentials() throws IOException, CipherException { + return Credentials.create("8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"); + } +} diff --git a/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTests.java b/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTests.java new file mode 100644 index 0000000000..a1c3f17020 --- /dev/null +++ b/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/ContainerTests.java @@ -0,0 +1,195 @@ +/* + * 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.container; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hyperledger.besu.tests.container.helpers.ContractOperations.deployContractAndReturnAddress; +import static org.hyperledger.besu.tests.container.helpers.ContractOperations.generateRandomLogValue; +import static org.hyperledger.besu.tests.container.helpers.ContractOperations.getTransactionLog; +import static org.hyperledger.besu.tests.container.helpers.ContractOperations.sendLogEventAndReturnTransactionHash; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import okhttp3.OkHttpClient; +import org.junit.Before; +import org.junit.Test; +import org.web3j.crypto.CipherException; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.exceptions.TransactionException; +import org.web3j.quorum.enclave.Enclave; +import org.web3j.quorum.enclave.Tessera; +import org.web3j.quorum.enclave.protocol.EnclaveService; +import org.web3j.quorum.tx.QuorumTransactionManager; +import org.web3j.tx.response.PollingTransactionReceiptProcessor; + +public class ContainerTests extends ContainerTestBase { + + private Credentials credentials; + private Enclave besuEnclave; + private EnclaveService besuEnclaveService; + private Enclave goQuorumEnclave; + private EnclaveService goQuorumEnclaveService; + private PollingTransactionReceiptProcessor besuPollingTransactionReceiptProcessor; + private PollingTransactionReceiptProcessor goQuorumPollingTransactionReceiptProcessor; + + @Before + public void testSetUp() throws IOException, CipherException { + besuEnclaveService = + new EnclaveService( + "http://" + tesseraBesuContainer.getHost(), + tesseraBesuContainer.getMappedPort(tesseraRestPort), + new OkHttpClient()); + besuEnclave = new Tessera(besuEnclaveService, besuWeb3j); + besuPollingTransactionReceiptProcessor = + new PollingTransactionReceiptProcessor(besuWeb3j, 1000, 10); + goQuorumEnclaveService = + new EnclaveService( + "http://" + tesseraGoQuorumContainer.getHost(), + tesseraGoQuorumContainer.getMappedPort(tesseraRestPort), + new OkHttpClient()); + goQuorumEnclave = new Tessera(goQuorumEnclaveService, goQuorumWeb3j); + goQuorumPollingTransactionReceiptProcessor = + new PollingTransactionReceiptProcessor(goQuorumWeb3j, 1000, 10); + credentials = loadCredentials(); + } + + @Test + public void contractShouldBeDeployedToBothNodes() throws IOException, TransactionException { + // create a GoQuorum transaction manager + final QuorumTransactionManager qtm = + new QuorumTransactionManager( + goQuorumWeb3j, + credentials, + goQuorumTesseraPubKey, + Arrays.asList(goQuorumTesseraPubKey, besuTesseraPubKey), + goQuorumEnclave); + + // Get the deployed contract address + final String contractAddress = + deployContractAndReturnAddress( + goQuorumWeb3j, + credentials, + qtm, + besuPollingTransactionReceiptProcessor, + goQuorumPollingTransactionReceiptProcessor); + + // Generate a random value to insert into the log + final String logValue = generateRandomLogValue(); + + // Send the transaction and get the transaction hash + final String transactionHash = + sendLogEventAndReturnTransactionHash( + goQuorumWeb3j, + credentials, + contractAddress, + qtm, + besuPollingTransactionReceiptProcessor, + goQuorumPollingTransactionReceiptProcessor, + logValue); + + // Get the transaction logs + final String goQuorumResult = getTransactionLog(goQuorumWeb3j, transactionHash); + final String besuResult = getTransactionLog(besuWeb3j, transactionHash); + + assertThat(besuResult).isEqualTo(logValue); + assertThat(goQuorumResult).isEqualTo(logValue); + } + + @Test + public void contractShouldBeDeployedOnlyToGoQuorumNode() + throws IOException, TransactionException { + // create a quorum transaction manager + final QuorumTransactionManager qtm = + new QuorumTransactionManager( + goQuorumWeb3j, + credentials, + goQuorumTesseraPubKey, + List.of(goQuorumTesseraPubKey), + goQuorumEnclave); + + // Get the deployed contract address + final String contractAddress = + deployContractAndReturnAddress( + goQuorumWeb3j, + credentials, + qtm, + goQuorumPollingTransactionReceiptProcessor, + besuPollingTransactionReceiptProcessor); + + // Generate a random value to insert into the log + final String logValue = generateRandomLogValue(); + + // Send the transaction and get the transaction hash + final String transactionHash = + sendLogEventAndReturnTransactionHash( + goQuorumWeb3j, + credentials, + contractAddress, + qtm, + goQuorumPollingTransactionReceiptProcessor, + besuPollingTransactionReceiptProcessor, + logValue); + + // Assert the GoQuorum node has received the log + final String quorumResult = getTransactionLog(goQuorumWeb3j, transactionHash); + assertThat(quorumResult).isEqualTo(logValue); + + // Assert the Besu node has not received the log + assertThatThrownBy(() -> getTransactionLog(besuWeb3j, transactionHash)) + .hasMessageContaining("No log found"); + } + + @Test + public void contractShouldBeDeployedOnlyToBesuNode() throws IOException, TransactionException { + // create a GoQuorum transaction manager + final QuorumTransactionManager qtm = + new QuorumTransactionManager( + besuWeb3j, credentials, besuTesseraPubKey, List.of(besuTesseraPubKey), besuEnclave); + + // Get the deployed contract address + final String contractAddress = + deployContractAndReturnAddress( + besuWeb3j, + credentials, + qtm, + besuPollingTransactionReceiptProcessor, + goQuorumPollingTransactionReceiptProcessor); + + // Generate a random value to insert into the log + final String logValue = generateRandomLogValue(); + + // Send the transaction and get the transaction hash + final String transactionHash = + sendLogEventAndReturnTransactionHash( + besuWeb3j, + credentials, + contractAddress, + qtm, + besuPollingTransactionReceiptProcessor, + goQuorumPollingTransactionReceiptProcessor, + logValue); + + // Assert the Besu node has received the log + final String besuResult = getTransactionLog(besuWeb3j, transactionHash); + assertThat(besuResult).isEqualTo(logValue); + + // Assert the GoQuorum node has not received the log + assertThatThrownBy(() -> getTransactionLog(goQuorumWeb3j, transactionHash)) + .hasMessageContaining("No log found"); + } +} diff --git a/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/helpers/ContractOperations.java b/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/helpers/ContractOperations.java new file mode 100644 index 0000000000..e61e5bfafb --- /dev/null +++ b/container-tests/tests/src/test/java/org/hyperledger/besu/tests/container/helpers/ContractOperations.java @@ -0,0 +1,162 @@ +/* + * 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.container.helpers; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.Collections; +import java.util.List; + +import org.web3j.abi.FunctionEncoder; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.response.EthGetTransactionCount; +import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.protocol.exceptions.TransactionException; +import org.web3j.quorum.Quorum; +import org.web3j.quorum.tx.QuorumTransactionManager; +import org.web3j.tx.response.PollingTransactionReceiptProcessor; + +public class ContractOperations { + public static String deployContractAndReturnAddress( + final Quorum quorumWeb3j, + final Credentials credentials, + final QuorumTransactionManager qtm, + final PollingTransactionReceiptProcessor pollingTransactionReceiptProcessorReturn, + final PollingTransactionReceiptProcessor pollingTransactionReceiptProcessorIgnore) + throws IOException, TransactionException { + // Build smart contract transaction + final String simpleStorageContractBytecode = + "6080604052348015600f57600080fd5b5060de8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060275760003560e01c8062f88abf14602c575b600080fd5b605560048036036020811015604057600080fd5b81019080803590602001909291905050506057565b005b3373ffffffffffffffffffffffffffffffffffffffff167f748aa07c80b05bd067e3688dbb79d9f9583cd018be6a589a7c364cacd770e0a2826040518082815260200191505060405180910390a25056fea26469706673582212207df0d3ad8bced04b7bd476cc81a6233c0b575966c29b4af96450313628ee623964736f6c63430007040033"; + final String encodedConstructor = FunctionEncoder.encodeConstructor(Collections.emptyList()); + final String binaryAndInitCode = simpleStorageContractBytecode + encodedConstructor; + + final RawTransaction contractTransaction = + RawTransaction.createTransaction( + BigInteger.valueOf(getNonce(quorumWeb3j, credentials)), + BigInteger.ZERO, + BigInteger.valueOf(4300000), + "", + BigInteger.ZERO, + binaryAndInitCode); + + // Send the signed transaction to quorum + final EthSendTransaction sendContractTransactionResult = qtm.signAndSend(contractTransaction); + assertThat(sendContractTransactionResult.hasError()).isFalse(); + + pollNodeForTransactionReceipt( + pollingTransactionReceiptProcessorIgnore, + sendContractTransactionResult.getTransactionHash()); + + // Poll for the transaction receipt + final TransactionReceipt transactionReceiptResult = + pollNodeForTransactionReceiptResult( + pollingTransactionReceiptProcessorReturn, + sendContractTransactionResult.getTransactionHash()); + + return transactionReceiptResult.getContractAddress(); + } + + public static String getTransactionLog(final Quorum web3j, final String transactionHash) + throws IOException { + final EthGetTransactionReceipt transactionReceiptResult = + web3j.ethGetTransactionReceipt(transactionHash).send(); + assertThat(transactionReceiptResult.getTransactionReceipt()) + .withFailMessage("Transaction for log not found.") + .isNotEmpty(); + final List logs = transactionReceiptResult.getTransactionReceipt().get().getLogs(); + assertThat(logs.size()).withFailMessage("No log found.").isEqualTo(1); + // Remove the 0x prefix + return logs.get(0).getData().substring(2); + } + + public static String sendLogEventAndReturnTransactionHash( + final Quorum quorum, + final Credentials credentials, + final String contractAddress, + final QuorumTransactionManager qtm, + final PollingTransactionReceiptProcessor pollingTransactionReceiptProcessorReturn, + final PollingTransactionReceiptProcessor pollingTransactionReceiptProcessorIgnore, + final String param) + throws IOException, TransactionException { + final String functionSig = "0x00f88abf"; + final String data = functionSig + param; + final RawTransaction logEventTransaction = + RawTransaction.createTransaction( + BigInteger.valueOf(getNonce(quorum, credentials)), + BigInteger.ZERO, + BigInteger.valueOf(4300000), + contractAddress, + BigInteger.ZERO, + data); + + final EthSendTransaction sendTransactionResult = qtm.signAndSend(logEventTransaction); + assertThat(sendTransactionResult.hasError()).isFalse(); + + pollNodeForTransactionReceipt( + pollingTransactionReceiptProcessorIgnore, sendTransactionResult.getTransactionHash()); + + final TransactionReceipt logReceiptResult = + pollNodeForTransactionReceiptResult( + pollingTransactionReceiptProcessorReturn, sendTransactionResult.getTransactionHash()); + + return logReceiptResult.getTransactionHash(); + } + + public static String generateRandomLogValue() { + final StringBuilder randomValue = + new StringBuilder(Long.toHexString(((Double) (Math.random() * 100000000)).longValue())); + + while (randomValue.length() < 64) { + randomValue.insert(0, '0'); + } + return randomValue.toString(); + } + + public static int getNonce(final Quorum quorum, final Credentials credentials) + throws IOException { + final EthGetTransactionCount transactionCountResult = + quorum + .ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.LATEST) + .send(); + assertThat(transactionCountResult.hasError()).isFalse(); + return transactionCountResult.getTransactionCount().intValueExact(); + } + + public static TransactionReceipt pollNodeForTransactionReceiptResult( + final PollingTransactionReceiptProcessor pollingTransactionReceiptProcessor, + final String transactionHash) + throws IOException, TransactionException { + final TransactionReceipt transactionReceiptResult = + pollingTransactionReceiptProcessor.waitForTransactionReceipt(transactionHash); + assertThat(transactionReceiptResult.isStatusOK()).isTrue(); + return transactionReceiptResult; + } + + public static void pollNodeForTransactionReceipt( + final PollingTransactionReceiptProcessor pollingTransactionReceiptProcessor, + final String transactionHash) + throws IOException, TransactionException { + final TransactionReceipt transactionReceiptResult = + pollingTransactionReceiptProcessor.waitForTransactionReceipt(transactionHash); + assertThat(transactionReceiptResult.isStatusOK()).isTrue(); + } +} diff --git a/container-tests/tests/src/test/resources/besu/config/besuAddress b/container-tests/tests/src/test/resources/besu/config/besuAddress new file mode 100644 index 0000000000..ae93120b92 --- /dev/null +++ b/container-tests/tests/src/test/resources/besu/config/besuAddress @@ -0,0 +1 @@ +0x8a15eaa6e526e9b8ebdea0bfc6d4399b0ae7ba1c \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/besu/data/key b/container-tests/tests/src/test/resources/besu/data/key new file mode 100644 index 0000000000..8c5fb2ae59 --- /dev/null +++ b/container-tests/tests/src/test/resources/besu/data/key @@ -0,0 +1 @@ +0xc86c0030a86bb894c1e10c25d48d4949a288df9c6ff736b32d977f39893fd0dc \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/genesis.json b/container-tests/tests/src/test/resources/genesis.json new file mode 100644 index 0000000000..bf45d3ceb7 --- /dev/null +++ b/container-tests/tests/src/test/resources/genesis.json @@ -0,0 +1,45 @@ +{ + "config":{ + "homesteadBlock": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "chainId":2020, + "eip150Block": 0, + "eip155Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip158Block": 0, + "clique":{ + "blockperiodseconds":2, + "epochlength":30000, + "period": 2, + "epoch": 30000 + }, + "isQuorum": true + }, + "coinbase":"0x0000000000000000000000000000000000000000", + + "extraData":"0x00000000000000000000000000000000000000000000000000000000000000008a15eaa6e526e9b8ebdea0bfc6d4399b0ae7ba1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "difficulty": "0x0", + "gasLimit": "0xE0000000", + "mixhash": "0x00000000000000000000000000000000000000647572616c65787365646c6578", + "nonce": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x00" +} \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/goQuorum/qdata/nodeKey b/container-tests/tests/src/test/resources/goQuorum/qdata/nodeKey new file mode 100644 index 0000000000..8170e1a64a --- /dev/null +++ b/container-tests/tests/src/test/resources/goQuorum/qdata/nodeKey @@ -0,0 +1 @@ +c0b7ff2352175d2a43d72eca7c2f806cc7bd74a7a597630d62f5261834348f74 \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/ipc/.empty b/container-tests/tests/src/test/resources/ipc/.empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/container-tests/tests/src/test/resources/keys/key1.key b/container-tests/tests/src/test/resources/keys/key1.key new file mode 100644 index 0000000000..2dc3e9265c --- /dev/null +++ b/container-tests/tests/src/test/resources/keys/key1.key @@ -0,0 +1,6 @@ +{ + "type" : "unlocked", + "data" : { + "bytes" : "srn2w10NWdvWyHkPMWB07W5NjIPmiAHRqESL8vH3Kyw=" + } +} \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/keys/key1.pub b/container-tests/tests/src/test/resources/keys/key1.pub new file mode 100644 index 0000000000..032663d49e --- /dev/null +++ b/container-tests/tests/src/test/resources/keys/key1.pub @@ -0,0 +1 @@ +3XGBIf+x8IdVQOVfIsbRnHwTYOJP/Fx84G8gMmy8qDM= \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/keys/key2.key b/container-tests/tests/src/test/resources/keys/key2.key new file mode 100644 index 0000000000..1f24fe3f4b --- /dev/null +++ b/container-tests/tests/src/test/resources/keys/key2.key @@ -0,0 +1,6 @@ +{ + "type" : "unlocked", + "data" : { + "bytes" : "LWjzMnDP/MQIATegt45LTrMh1i9ISX64CFO0EzFDNhg=" + } +} \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/keys/key2.pub b/container-tests/tests/src/test/resources/keys/key2.pub new file mode 100644 index 0000000000..03d1d917ec --- /dev/null +++ b/container-tests/tests/src/test/resources/keys/key2.pub @@ -0,0 +1 @@ +8JJLEAbq6o9m4Kqm++v0Y1n9Z2ryAFtZTyhnxSKWgws= \ No newline at end of file diff --git a/container-tests/tests/src/test/resources/tessera/tesseraConfig.json b/container-tests/tests/src/test/resources/tessera/tesseraConfig.json new file mode 100644 index 0000000000..a1cc138a93 --- /dev/null +++ b/container-tests/tests/src/test/resources/tessera/tesseraConfig.json @@ -0,0 +1,47 @@ +{ + "useWhiteList": false, + "jdbc": { + "username": "sa", + "password": "", + "url": "jdbc:h2:/tmp/tessera;LOCK_TIMEOUT=20000", + "autoCreateTables": true + }, + "serverConfigs":[ + { + "app":"ThirdParty", + "enabled": true, + "serverAddress": "http://localhost:9081", + "communicationType" : "REST" + }, + { + "app":"Q2T", + "enabled": true, + "serverAddress": "unix:/tmp/test.ipc", + "communicationType" : "REST" + }, + { + "app":"P2P", + "enabled": true, + "serverAddress":"http://localhost:9001", + "sslConfig": { + "tls": "OFF" + }, + "communicationType" : "REST" + } + ], + "peer": [ + { + "url": "http://localhost:9001" + } + ], + "keys": { + "passwords": [], + "keyData": [ + { + "privateKeyPath": "myKey.key", + "publicKeyPath": "myKey.pub" + } + ] + }, + "alwaysSendTo": [] +} \ No newline at end of file diff --git a/container-tests/tests/src/test/solidity/TestContract.sol b/container-tests/tests/src/test/solidity/TestContract.sol new file mode 100644 index 0000000000..f44bdac601 --- /dev/null +++ b/container-tests/tests/src/test/solidity/TestContract.sol @@ -0,0 +1,9 @@ +pragma solidity >=0.6.0 < 0.8.0; + +contract TestContract { + event TestEvent(address indexed _from, int val); + + function logEvent(int randomNum) public { + emit TestEvent(msg.sender, randomNum); + } +} diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle index 50a8df9603..cbb129f188 100644 --- a/gradle/check-licenses.gradle +++ b/gradle/check-licenses.gradle @@ -46,6 +46,7 @@ ext.acceptedLicenses = [ 'Public Domain (CC0) License 1.0', 'Public Domain', 'Unicode/ICU License', + 'New BSD License' ]*.toLowerCase() /** diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 934ca2c461..3310d2b919 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -150,6 +150,7 @@ dependencyManagement { exclude group: 'com.github.jnr', name: 'jnr-unixsocket' } dependency 'org.web3j:crypto:4.5.15' + dependency 'org.web3j:quorum:4.5.15' dependency 'org.xerial.snappy:snappy-java:1.1.8.2' @@ -162,5 +163,7 @@ dependencyManagement { entry 'signing-secp256k1-api' entry 'signing-secp256k1-impl' } + + dependency 'org.testcontainers:testcontainers:1.15.2' } } diff --git a/settings.gradle b/settings.gradle index 9c6fe698e4..2dc4c6e439 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include 'consensus:common' include 'consensus:ibft' include 'consensus:ibftlegacy' include 'consensus:qbft' +include 'container-tests:tests' include 'crypto' include 'enclave' include 'errorprone-checks'