From 947426cd5987017839b56974d8d08f6430f83814 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Tue, 25 Jul 2023 09:03:07 +0000 Subject: [PATCH] feat(geth-all-in-one): add ethereum test image and helper class - Add new `geth-all-in-one` test image for running ethereum tests in mainnet-like environment. Image is based on `client-go:v1.12.0` and uses Clique (PoS). There is one coinbase account with publicly available keys like in other, similar packages in cacti. - New image was introduced because currently used open-ethereum one is deprecated. - Add `geth-all-in-one-publish` CI for publishing new images. - Add `@hyperledger/cactus-test-geth-ledger` for using new geth ledger container in the tests. The class has been moved out of `cactus-test-tooling` because of conflicting `web3js` versions. Other than that, it's similar to open-ethereum test class. - Add basic tests for `@hyperledger/cactus-test-geth-ledger`. More tests are being developed right now, and should be available in subsequent PRs. Closes: #2577 Signed-off-by: Michal Bajer --- .../workflows/geth-all-in-one-publish.yaml | 60 +++ packages/cactus-test-geth-ledger/README.md | 74 ++++ packages/cactus-test-geth-ledger/package.json | 74 ++++ .../src/main/typescript/geth-test-ledger.ts | 410 ++++++++++++++++++ .../src/main/typescript/index.ts | 1 + .../src/main/typescript/index.web.ts | 1 + .../src/main/typescript/public-api.ts | 7 + .../integration/api-surface.test.ts | 5 + .../integration/geth-test-ledger.test.ts | 102 +++++ .../cactus-test-geth-ledger/tsconfig.json | 21 + tools/docker/geth-all-in-one/Dockerfile | 44 ++ tools/docker/geth-all-in-one/README.md | 65 +++ .../docker/geth-all-in-one/docker-compose.yml | 22 + .../geth-all-in-one/script-start-docker.sh | 2 + tools/docker/geth-all-in-one/src/genesis.json | 27 ++ .../docker/geth-all-in-one/src/healthcheck.sh | 14 + ...--6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0 | 1 + tsconfig.json | 3 + yarn.lock | 78 +++- 19 files changed, 999 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/geth-all-in-one-publish.yaml create mode 100644 packages/cactus-test-geth-ledger/README.md create mode 100644 packages/cactus-test-geth-ledger/package.json create mode 100644 packages/cactus-test-geth-ledger/src/main/typescript/geth-test-ledger.ts create mode 100755 packages/cactus-test-geth-ledger/src/main/typescript/index.ts create mode 100755 packages/cactus-test-geth-ledger/src/main/typescript/index.web.ts create mode 100755 packages/cactus-test-geth-ledger/src/main/typescript/public-api.ts create mode 100644 packages/cactus-test-geth-ledger/src/test/typescript/integration/api-surface.test.ts create mode 100644 packages/cactus-test-geth-ledger/src/test/typescript/integration/geth-test-ledger.test.ts create mode 100644 packages/cactus-test-geth-ledger/tsconfig.json create mode 100644 tools/docker/geth-all-in-one/Dockerfile create mode 100644 tools/docker/geth-all-in-one/README.md create mode 100644 tools/docker/geth-all-in-one/docker-compose.yml create mode 100755 tools/docker/geth-all-in-one/script-start-docker.sh create mode 100644 tools/docker/geth-all-in-one/src/genesis.json create mode 100644 tools/docker/geth-all-in-one/src/healthcheck.sh create mode 100644 tools/docker/geth-all-in-one/src/keystore/UTC--2023-07-03T14-42-00.153791517Z--6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0 diff --git a/.github/workflows/geth-all-in-one-publish.yaml b/.github/workflows/geth-all-in-one-publish.yaml new file mode 100644 index 0000000000..73d5cabc63 --- /dev/null +++ b/.github/workflows/geth-all-in-one-publish.yaml @@ -0,0 +1,60 @@ +name: geth-all-in-one-publish + +on: + push: + # Publish `main` as Docker `latest` image. + branches: + - main + + # Publish `v1.2.3` tags as releases. + tags: + - v* + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + IMAGE_NAME: cactus-geth-all-in-one + +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + build-tag-push-container: + runs-on: ubuntu-20.04 + env: + DOCKER_BUILDKIT: 1 + DOCKERFILE_PATH: ./tools/docker/geth-all-in-one/Dockerfile + DOCKER_BUILD_DIR: ./tools/docker/geth-all-in-one/ + permissions: + packages: write + contents: read + + steps: + - uses: actions/checkout@v3.5.2 + + - name: Build image + run: docker build $DOCKER_BUILD_DIR --file $DOCKERFILE_PATH --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" + + - name: Log in to registry + # This is where you will update the PAT to GITHUB_TOKEN + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push image + run: | + SHORTHASH=$(git rev-parse --short "$GITHUB_SHA") + TODAYS_DATE="$(date +%F)" + DOCKER_TAG="$TODAYS_DATE-$SHORTHASH" + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # Do not use the `latest` tag at all, tag with date + git short hash if there is no git tag + [ "$VERSION" == "main" ] && VERSION=$DOCKER_TAG + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/packages/cactus-test-geth-ledger/README.md b/packages/cactus-test-geth-ledger/README.md new file mode 100644 index 0000000000..638e1f0ca9 --- /dev/null +++ b/packages/cactus-test-geth-ledger/README.md @@ -0,0 +1,74 @@ +# `@hyperledger/cactus-test-geth-ledger` + +Helpers for running test `go-ethereum` ledger in test scripts. + +## Summary + +- [Getting Started](#getting-started) +- [Usage](#usage) +- [Runing the tests](#running-the-tests) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +## Getting Started + +Clone the git repository on your local machine. Follow these instructions that will get you a copy of the project up and running on +your local machine for development and testing purposes. + +### Prerequisites + +In the root of the project to install the dependencies execute the command: + +```sh +npm run configure +``` + +## Usage + +- In order to start the new test ledger, you must import `GethTestLedger` and `start()` it. +- Options can be modified by supplying constructor argument object. +- See tests for more complete usage examples. + +```typescript +import { GethTestLedger } from "@hyperledger/cactus-test-geth-ledger"; + +// You can supply empty object, suitable default values will be used. +const options = { + containerImageName: "cactus_geth_all_in_one", // geth AIO container name + containerImageVersion: "local-build", // geth AIO container tag + logLevel: "info" as LogLevelDesc, // log verbosity of test class, not ethereum node! + emitContainerLogs: false, // will print ethereum node logs here if `true` + envVars: [], // environment variables to provide when starting the ledger + useRunningLedger: false, // test flag to search for already running ledger instead of starting new one (only for development) +}; + +const ledger = new GethTestLedger(options); +await ledger.start(); +// await ledger.start(true); // don't pull image, use one from local storage + +// Use +const rpcApiHttpHost = await ledger.getRpcApiHttpHost(); +``` + +## Running the tests + +To check that all has been installed correctly and that the test class has no errors: + +- Run this command at the project's root: + +```sh +npx jest cactus-test-geth-ledger +``` + +## Contributing + +We welcome contributions to Hyperledger Cactus in many forms, and there’s always plenty to do! + +Please review [CONTIRBUTING.md](../../CONTRIBUTING.md) to get started. + +## License + +This distribution is published under the Apache License Version 2.0 found in the [LICENSE](../../LICENSE) file. + +## Acknowledgments diff --git a/packages/cactus-test-geth-ledger/package.json b/packages/cactus-test-geth-ledger/package.json new file mode 100644 index 0000000000..45c894f348 --- /dev/null +++ b/packages/cactus-test-geth-ledger/package.json @@ -0,0 +1,74 @@ +{ + "name": "@hyperledger/cactus-test-geth-ledger", + "version": "2.0.0-alpha.1", + "description": "Helpers for running test go-ethereum ledger in test scripts.", + "keywords": [ + "Hyperledger", + "Cactus", + "Integration", + "Blockchain", + "Distributed Ledger Technology" + ], + "homepage": "https://github.com/hyperledger/cactus#readme", + "bugs": { + "url": "https://github.com/hyperledger/cactus/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hyperledger/cactus.git" + }, + "license": "Apache-2.0", + "author": { + "name": "Hyperledger Cactus Contributors", + "email": "cactus@lists.hyperledger.org", + "url": "https://www.hyperledger.org/use/cactus" + }, + "contributors": [ + { + "name": "Please add yourself to the list of contributors", + "email": "your.name@example.com", + "url": "https://example.com" + }, + { + "name": "Michal Bajer", + "email": "michal.bajer@fujitsu.com", + "url": "https://www.fujitsu.com/global/" + } + ], + "main": "dist/lib/main/typescript/index.js", + "module": "dist/lib/main/typescript/index.js", + "browser": "dist/cactus-test-geth-ledger.web.umd.js", + "types": "dist/lib/main/typescript/index.d.ts", + "files": [ + "dist/*" + ], + "scripts": { + "watch": "npm-watch", + "webpack": "npm-run-all webpack:dev", + "webpack:dev": "npm-run-all webpack:dev:node webpack:dev:web", + "webpack:dev:node": "webpack --env=dev --target=node --config ../../webpack.config.js", + "webpack:dev:web": "webpack --env=dev --target=web --config ../../webpack.config.js" + }, + "dependencies": { + "@hyperledger/cactus-common": "2.0.0-alpha.1", + "@hyperledger/cactus-test-tooling": "2.0.0-alpha.1", + "dockerode": "3.3.0", + "internal-ip": "6.2.0", + "run-time-error": "1.4.0", + "web3": "4.0.3", + "web3-eth-accounts": "4.0.3" + }, + "devDependencies": { + "@types/dockerode": "3.2.7" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "publishConfig": { + "access": "public" + }, + "browserMinified": "dist/cactus-test-geth-ledger.web.umd.min.js", + "mainMinified": "dist/cactus-test-geth-ledger.node.umd.min.js", + "watch": {} +} diff --git a/packages/cactus-test-geth-ledger/src/main/typescript/geth-test-ledger.ts b/packages/cactus-test-geth-ledger/src/main/typescript/geth-test-ledger.ts new file mode 100644 index 0000000000..2fa9bc53e6 --- /dev/null +++ b/packages/cactus-test-geth-ledger/src/main/typescript/geth-test-ledger.ts @@ -0,0 +1,410 @@ +import { EventEmitter } from "events"; +import Docker, { Container } from "dockerode"; +import { v4 as internalIpV4 } from "internal-ip"; +import Web3, { ContractAbi, TransactionReceipt } from "web3"; +import type { Web3Account } from "web3-eth-accounts"; +import { RuntimeError } from "run-time-error"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, +} from "@hyperledger/cactus-common"; +import { Containers } from "@hyperledger/cactus-test-tooling"; + +export interface IGethTestLedgerOptions { + readonly containerImageName?: string; + readonly containerImageVersion?: string; + readonly logLevel?: LogLevelDesc; + readonly emitContainerLogs?: boolean; + readonly envVars?: string[]; + // For test development, attach to ledger that is already running, don't spin up new one + readonly useRunningLedger?: boolean; +} + +/** + * Default values used by GethTestLedger constructor. + */ +export const GETH_TEST_LEDGER_DEFAULT_OPTIONS = Object.freeze({ + containerImageName: "ghcr.io/hyperledger/cacti-geth-all-in-one", + containerImageVersion: "2023-07-27-2a8c48ed6", + logLevel: "info" as LogLevelDesc, + emitContainerLogs: false, + envVars: [], + useRunningLedger: false, +}); + +export const WHALE_ACCOUNT_PRIVATE_KEY = + "86bbf98cf5e5b1c43d2c8701764897357e0fa24982c0137efabf6dc3a6e7b69e"; +export const WHALE_ACCOUNT_ADDRESS = "6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0"; + +export class GethTestLedger { + public static readonly CLASS_NAME = "GethTestLedger"; + private readonly log: Logger; + private readonly logLevel: LogLevelDesc; + private readonly containerImageName: string; + private readonly containerImageVersion: string; + private readonly envVars: string[]; + private readonly emitContainerLogs: boolean; + public readonly useRunningLedger: boolean; + private _container: Container | undefined; + private _containerId: string | undefined; + private _web3: Web3 | undefined; + + public get fullContainerImageName(): string { + return [this.containerImageName, this.containerImageVersion].join(":"); + } + + public get className(): string { + return GethTestLedger.CLASS_NAME; + } + + public get container(): Container { + if (this._container) { + return this._container; + } else { + throw new Error(`Invalid state: _container is not set. Called start()?`); + } + } + + private get web3(): Web3 { + if (this._web3) { + return this._web3; + } else { + throw new Error( + "Invalid state: web3 client is missing, start the ledger container first.", + ); + } + } + + constructor(public readonly options: IGethTestLedgerOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + + this.logLevel = + this.options.logLevel || GETH_TEST_LEDGER_DEFAULT_OPTIONS.logLevel; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level: this.logLevel, label }); + + this.emitContainerLogs = + options?.emitContainerLogs ?? + GETH_TEST_LEDGER_DEFAULT_OPTIONS.emitContainerLogs; + this.useRunningLedger = + options?.useRunningLedger ?? + GETH_TEST_LEDGER_DEFAULT_OPTIONS.useRunningLedger; + this.containerImageName = + this.options.containerImageName || + GETH_TEST_LEDGER_DEFAULT_OPTIONS.containerImageName; + this.containerImageVersion = + this.options.containerImageVersion || + GETH_TEST_LEDGER_DEFAULT_OPTIONS.containerImageVersion; + this.envVars = + this.options.envVars || GETH_TEST_LEDGER_DEFAULT_OPTIONS.envVars; + + this.log.info( + `Created ${this.className} OK. Image FQN: ${this.fullContainerImageName}`, + ); + } + + /** + * Get container status. + * + * @returns status string + */ + public async getContainerStatus(): Promise { + if (!this.container) { + throw new Error( + "GethTestLedger#getContainerStatus(): Container not started yet!", + ); + } + + const { Status } = await Containers.getById(this.container.id); + return Status; + } + + /** + * Start a test Geth ledger. + * + * @param omitPull Don't pull docker image from upstream if true. + * @returns Promise + */ + public async start(omitPull = false): Promise { + if (this.useRunningLedger) { + this.log.info( + "Search for already running Geth Test Ledger because 'useRunningLedger' flag is enabled.", + ); + this.log.info( + "Search criteria - image name: ", + this.fullContainerImageName, + ", state: running", + ); + const containerInfo = await Containers.getByPredicate( + (ci) => + ci.Image === this.fullContainerImageName && ci.State === "healthy", + ); + const docker = new Docker(); + this._container = docker.getContainer(containerInfo.Id); + return this._container; + } + + if (this._container) { + this.log.warn("Container was already running - restarting it..."); + await this.container.stop(); + await this.container.remove(); + this._container = undefined; + } + + if (!omitPull) { + await Containers.pullImage( + this.fullContainerImageName, + {}, + this.logLevel, + ); + } + + return new Promise((resolve, reject) => { + const docker = new Docker(); + const eventEmitter: EventEmitter = docker.run( + this.fullContainerImageName, + [], + [], + { + ExposedPorts: { + ["8545/tcp"]: {}, + ["8546/tcp"]: {}, + }, + Env: this.envVars, + HostConfig: { + PublishAllPorts: true, + }, + }, + {}, + (err?: Error) => { + if (err) { + this.log.error( + `Failed to start ${this.fullContainerImageName} container; `, + err, + ); + reject(err); + } + }, + ); + + eventEmitter.once("start", async (container: Container) => { + this._container = container; + this._containerId = container.id; + + if (this.emitContainerLogs) { + const fnTag = `[${this.fullContainerImageName}]`; + await Containers.streamLogs({ + container: this.container, + tag: fnTag, + log: this.log, + }); + } + + try { + await Containers.waitForHealthCheck(this._containerId); + this._web3 = new Web3(await this.getRpcApiHttpHost()); + resolve(container); + } catch (ex) { + reject(ex); + } + }); + }); + } + + /** + * Stop a test Geth ledger. + * + * @returns Stop operation results. + */ + public async stop(): Promise { + if (this.useRunningLedger) { + this.log.info("Ignore stop request because useRunningLedger is enabled."); + return; + } else if (this.container) { + this._web3 = undefined; + return Containers.stop(this.container); + } else { + throw new Error( + `GethTestLedger#stop() Container was never created, nothing to stop.`, + ); + } + } + + /** + * Destroy a test Geth ledger. + * + * @returns Destroy operation results. + */ + public async destroy(): Promise { + if (this.useRunningLedger) { + this.log.info( + "Ignore destroy request because useRunningLedger is enabled.", + ); + return; + } else if (this.container) { + this._web3 = undefined; + return this.container.remove(); + } else { + throw new Error( + `GethTestLedger#destroy() Container was never created, nothing to destroy.`, + ); + } + } + + /** + * Creates a new ETH account from scratch on the ledger and then sends it a + * little seed money to get things started. + * + * Uses `web3.eth.accounts.create` + * + * @param [seedMoney=10e8] The amount of money to seed the new test account with. + */ + public async createEthTestAccount(seedMoney = 10e8): Promise { + const ethTestAccount = this.web3.eth.accounts.create(); + + const receipt = await this.transferAssetFromCoinbase( + ethTestAccount.address, + seedMoney, + ); + + if (receipt instanceof Error) { + throw new RuntimeError("Error in createEthTestAccount", receipt); + } else { + return ethTestAccount; + } + } + + /** + * Creates a new personal ethereum account with specified initial money and password. + * + * Uses `web3.eth.personal.newAccount` + * + * @param seedMoney Initial money to transfer to this account + * @param password Personal account password + * @returns New account address + */ + public async newEthPersonalAccount( + seedMoney = 10e8, + password = "test", + ): Promise { + const account = await this.web3.eth.personal.newAccount(password); + + const receipt = await this.transferAssetFromCoinbase(account, seedMoney); + + if (receipt instanceof Error) { + throw new RuntimeError("Error in newEthPersonalAccount", receipt); + } else { + return account; + } + } + + /** + * Seed `targetAccount` with money from coin base account. + * + * @param targetAccount Ethereum account to send money to. + * @param value Amount of money. + * @returns Transfer `TransactionReceipt` + */ + public async transferAssetFromCoinbase( + targetAccount: string, + value: number, + ): Promise { + const fnTag = `${this.className}#transferAssetFromCoinbase()`; + + const tx = await this.web3.eth.accounts.signTransaction( + { + from: WHALE_ACCOUNT_ADDRESS, + to: targetAccount, + value: value, + gas: 1000000, + }, + WHALE_ACCOUNT_PRIVATE_KEY, + ); + + if (!tx.rawTransaction) { + throw new Error(`${fnTag} Signing transaction failed, reason unknown.`); + } + + return await this.web3.eth.sendSignedTransaction(tx.rawTransaction); + } + + /** + * Deploy contract from coin base account to the ledger. + * + * @param abi - JSON interface of the contract. + * @param bytecode - Compiled code of the contract. + * @param args - Contract arguments. + * @returns Contract deployment `TransactionReceipt` + */ + public async deployContract( + abi: ContractAbi, + bytecode: string, + args?: any[], + ): Promise { + // Encode ABI + const contractProxy = new this.web3.eth.Contract(abi); + const contractTx = contractProxy.deploy({ + data: bytecode, + arguments: args as any, + }); + + // Send TX + const signedTx = await this.web3.eth.accounts.signTransaction( + { + from: WHALE_ACCOUNT_ADDRESS, + data: contractTx.encodeABI(), + gas: 8000000, // Max possible gas + nonce: await this.web3.eth.getTransactionCount(WHALE_ACCOUNT_ADDRESS), + }, + WHALE_ACCOUNT_PRIVATE_KEY, + ); + + if (!signedTx.rawTransaction) { + throw new Error(`Signing transaction failed, reason unknown.`); + } + + return await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction); + } + + public async getRpcApiHttpHost( + host?: string, + port?: number, + ): Promise { + const thePort = port || (await this.getHostPortHttp()); + const lanIpV4OrUndefined = await internalIpV4(); + const lanAddress = host || lanIpV4OrUndefined || "127.0.0.1"; // best effort... + return `http://${lanAddress}:${thePort}`; + } + + public async getRpcApiWebSocketHost( + host?: string, + port?: number, + ): Promise { + const thePort = port || (await this.getHostPortWs()); + const lanIpV4OrUndefined = await internalIpV4(); + const lanAddress = host || lanIpV4OrUndefined || "127.0.0.1"; // best effort... + return `ws://${lanAddress}:${thePort}`; + } + + private async getHostPort(port: number): Promise { + const fnTag = `${this.className}#getHostPort()`; + if (this._containerId) { + const cInfo = await Containers.getById(this._containerId); + return Containers.getPublicPort(port, cInfo); + } else { + throw new Error(`${fnTag} Container ID not set. Did you call start()?`); + } + } + + public async getHostPortHttp(): Promise { + return this.getHostPort(8545); + } + + public async getHostPortWs(): Promise { + return this.getHostPort(8546); + } +} diff --git a/packages/cactus-test-geth-ledger/src/main/typescript/index.ts b/packages/cactus-test-geth-ledger/src/main/typescript/index.ts new file mode 100755 index 0000000000..87cb558397 --- /dev/null +++ b/packages/cactus-test-geth-ledger/src/main/typescript/index.ts @@ -0,0 +1 @@ +export * from "./public-api"; diff --git a/packages/cactus-test-geth-ledger/src/main/typescript/index.web.ts b/packages/cactus-test-geth-ledger/src/main/typescript/index.web.ts new file mode 100755 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/packages/cactus-test-geth-ledger/src/main/typescript/index.web.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cactus-test-geth-ledger/src/main/typescript/public-api.ts b/packages/cactus-test-geth-ledger/src/main/typescript/public-api.ts new file mode 100755 index 0000000000..262913be47 --- /dev/null +++ b/packages/cactus-test-geth-ledger/src/main/typescript/public-api.ts @@ -0,0 +1,7 @@ +export { + IGethTestLedgerOptions, + GETH_TEST_LEDGER_DEFAULT_OPTIONS, + WHALE_ACCOUNT_PRIVATE_KEY, + WHALE_ACCOUNT_ADDRESS, + GethTestLedger, +} from "./geth-test-ledger"; diff --git a/packages/cactus-test-geth-ledger/src/test/typescript/integration/api-surface.test.ts b/packages/cactus-test-geth-ledger/src/test/typescript/integration/api-surface.test.ts new file mode 100644 index 0000000000..a65ea58247 --- /dev/null +++ b/packages/cactus-test-geth-ledger/src/test/typescript/integration/api-surface.test.ts @@ -0,0 +1,5 @@ +import * as apiSurface from "../../../main/typescript/public-api"; + +test("Library can be loaded", async () => { + expect(apiSurface).toBeTruthy(); +}); diff --git a/packages/cactus-test-geth-ledger/src/test/typescript/integration/geth-test-ledger.test.ts b/packages/cactus-test-geth-ledger/src/test/typescript/integration/geth-test-ledger.test.ts new file mode 100644 index 0000000000..9cae4bb245 --- /dev/null +++ b/packages/cactus-test-geth-ledger/src/test/typescript/integration/geth-test-ledger.test.ts @@ -0,0 +1,102 @@ +/** + * Tests of Geth helper typescript setup class. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Ledger settings +// const containerImageName = "ghcr.io/hyperledger/cactus-geth-all-in-one"; +// const containerImageVersion = "2022-10-18-06770b6c"; +// const useRunningLedger = false; + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { GethTestLedger } from "../../../main/typescript/index"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; +import { pruneDockerAllIfGithubAction } from "@hyperledger/cactus-test-tooling"; + +import "jest-extended"; +import { Web3 } from "web3"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "geth-test-ledger.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Geth Test Ledger checks", () => { + let ledger: GethTestLedger; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + log.info("Start GethTestLedger..."); + ledger = new GethTestLedger({ + emitContainerLogs: true, + logLevel: testLogLevel, + }); + log.debug("Geth image:", ledger.fullContainerImageName); + expect(ledger).toBeTruthy(); + + await ledger.start(); + }); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (ledger) { + log.info("Stop the fabric ledger..."); + await ledger.stop(); + await ledger.destroy(); + } + + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * Check if started container is still healthy. + */ + test("Started container is healthy", async () => { + const status = await ledger.getContainerStatus(); + expect(status).toEndWith("(healthy)"); + }); + + test("web3 can be attached through HTTP endpoint", async () => { + const httpRpcHost = await ledger.getRpcApiHttpHost(); + const httpWeb3 = new Web3(httpRpcHost); + const blockNumber = await httpWeb3.eth.getBlockNumber(); + expect(blockNumber.toString()).toBeTruthy(); + }); + + test("web3 can be attached through WS endpoint", async () => { + const wsRpcHost = await ledger.getRpcApiWebSocketHost(); + const wsWeb3 = new Web3(wsRpcHost); + try { + const blockNumber = await wsWeb3.eth.getBlockNumber(); + expect(blockNumber.toString()).toBeTruthy(); + } finally { + wsWeb3.provider?.disconnect(); + } + }); +}); diff --git a/packages/cactus-test-geth-ledger/tsconfig.json b/packages/cactus-test-geth-ledger/tsconfig.json new file mode 100644 index 0000000000..95cf4429f7 --- /dev/null +++ b/packages/cactus-test-geth-ledger/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist/lib/", + "declarationDir": "./dist/lib/", + "rootDir": "./src", + "tsBuildInfoFile": "../../.build-cache/cactus-test-geth-ledger.tsbuildinfo" + }, + "include": [ + "./src" + ], + "references": [ + { + "path": "../cactus-common/tsconfig.json" + }, + { + "path": "../cactus-test-tooling/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/tools/docker/geth-all-in-one/Dockerfile b/tools/docker/geth-all-in-one/Dockerfile new file mode 100644 index 0000000000..1353e2235a --- /dev/null +++ b/tools/docker/geth-all-in-one/Dockerfile @@ -0,0 +1,44 @@ +FROM ethereum/client-go:v1.12.0 + +# Init +COPY ./src/genesis.json /root/data/ +COPY ./src/keystore/ /root/data/keystore/ +RUN geth --datadir "/root/data" init "/root/data/genesis.json" + +# Setup healtcheck +COPY ./src/healthcheck.sh /bin/healthcheck +RUN chmod +x /bin/healthcheck +HEALTHCHECK --interval=5s --timeout=10s --start-period=10s --retries=30 CMD /bin/healthcheck + +# RPC - HTTP +EXPOSE 8545 +# RPC - WS +EXPOSE 8546 + +VOLUME [ "/root/data" ] + +ENTRYPOINT [ \ + "geth", \ + "--datadir", "/root/data", \ + "--nodiscover", \ + "--http", \ + "--http.addr", "0.0.0.0", \ + "--http.port", "8545", \ + "--http.corsdomain", "*", \ + "--http.vhosts", "*", \ + "--ws", \ + "--ws.addr", "0.0.0.0", \ + "--ws.port", "8546", \ + "--ws.origins", "*", \ + "--unlock", "0x6A2EC8c50BA1a9cE47c52d1cb5B7136Ee9d0cCC0", \ + "--mine", \ + "--password", "/dev/null", \ + "--allow-insecure-unlock", \ + "--miner.etherbase", "0x6A2EC8c50BA1a9cE47c52d1cb5B7136Ee9d0cCC0", \ + "--verbosity", "5" \ + ] +CMD [ \ + "--networkid", "10", \ + "--http.api", "eth,personal,web3,net,admin,debug", \ + "--ws.api", "eth,personal,web3,net,admin,debug" \ + ] \ No newline at end of file diff --git a/tools/docker/geth-all-in-one/README.md b/tools/docker/geth-all-in-one/README.md new file mode 100644 index 0000000000..529c0d209a --- /dev/null +++ b/tools/docker/geth-all-in-one/README.md @@ -0,0 +1,65 @@ +# geth-all-in-one + +An all in one ethereum/client-go (geth) docker image as described in [go-ethereum documentation](https://geth.ethereum.org/docs/fundamentals/private-network). + +- Clique (PoS) is used to mimic mainnet node. +- This docker image is for `testing` and `development` only. +- **Do NOT use in production!** +- Only use provided test account in internal network, don't use it on mainnet / testnets. + +## Usage + +### Docker Compose + +```bash +./script-start-docker.sh +``` + +or manually: + +```bash +docker-compose build && docker-compose up -d +``` + +### Docker + +```bash +# Build +DOCKER_BUILDKIT=1 docker build ./tools/docker/geth-all-in-one/ -t cactus_geth_all_in_one + +# Run +docker run --rm --name geth_aio_testnet --detach -p 8545:8545 -p 8546:8546 cactus_geth_all_in_one +``` + +### Examples + +#### Attach geth CLI + +```bash +docker exec -ti geth_aio_testnet geth --datadir "/root/data" attach + +> eth.blockNumber +24 +``` + +### Configure with Hardhat + +```javascript +module.exports = { + // ... + networks: { + geth: { + url: "http://127.0.0.1:8545", + chainId: 10, + }, + }, +}; +``` + +## Test Setup + +- Use typescript [GethTestLedger helper class](../../../packages/cactus-test-geth-ledger) to start this ledger and use it from inside of automatic test. + +## Possible improvements + +- Replace (deprecated) `eth.personal` with `Clef` - https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal diff --git a/tools/docker/geth-all-in-one/docker-compose.yml b/tools/docker/geth-all-in-one/docker-compose.yml new file mode 100644 index 0000000000..75bfb47b3c --- /dev/null +++ b/tools/docker/geth-all-in-one/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.5" + +services: + geth-aio-testnet: + container_name: ${CACTUS_GETH_LEDGER_CONTAINER_NAME:-geth_aio_testnet} + image: ${CACTUS_GETH_LEDGER_IMAGE_NAME:-cactus_geth_all_in_one:local-build} + build: + context: ./ + dockerfile: Dockerfile + ports: + - 8545:8545/tcp # RPC - HTTP + - 8546:8546/tcp # RPC - WS + expose: + - 8545/tcp # RPC - HTTP + - 8546/tcp # RPC - WS + networks: + - geth-network + +networks: + geth-network: + name: geth_aio_network + driver: bridge diff --git a/tools/docker/geth-all-in-one/script-start-docker.sh b/tools/docker/geth-all-in-one/script-start-docker.sh new file mode 100755 index 0000000000..83156688d0 --- /dev/null +++ b/tools/docker/geth-all-in-one/script-start-docker.sh @@ -0,0 +1,2 @@ +echo "[process] start docker environment for Geth testnet" +docker-compose build && docker-compose up -d diff --git a/tools/docker/geth-all-in-one/src/genesis.json b/tools/docker/geth-all-in-one/src/genesis.json new file mode 100644 index 0000000000..6066c68e57 --- /dev/null +++ b/tools/docker/geth-all-in-one/src/genesis.json @@ -0,0 +1,27 @@ +{ + "config": { + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "clique": { + "period": 5, + "epoch": 30000 + } + }, + "difficulty": "1", + "gasLimit": "800000000", + "extradata": "0x00000000000000000000000000000000000000000000000000000000000000006a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "alloc": { + "6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0": { + "balance": "100000000000000000000000000" + } + } +} diff --git a/tools/docker/geth-all-in-one/src/healthcheck.sh b/tools/docker/geth-all-in-one/src/healthcheck.sh new file mode 100644 index 0000000000..1a5b51c260 --- /dev/null +++ b/tools/docker/geth-all-in-one/src/healthcheck.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +function is_valid_block_returned() { + local rpc_endpoint="$1" + local block_number=$(geth attach --exec "eth.blockNumber" "$rpc_endpoint") + if ! echo "$block_number" | grep -Eq '^[0-9]+$'; then + echo "Invalid eth.blockNumber '${block_number}' -> UNHEALTHY" + exit 1 + fi +} + +# Check both HTTP and WS endpoints +is_valid_block_returned "http://localhost:8545" +is_valid_block_returned "ws://localhost:8546" diff --git a/tools/docker/geth-all-in-one/src/keystore/UTC--2023-07-03T14-42-00.153791517Z--6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0 b/tools/docker/geth-all-in-one/src/keystore/UTC--2023-07-03T14-42-00.153791517Z--6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0 new file mode 100644 index 0000000000..2d6576bd78 --- /dev/null +++ b/tools/docker/geth-all-in-one/src/keystore/UTC--2023-07-03T14-42-00.153791517Z--6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0 @@ -0,0 +1 @@ +{"address":"6a2ec8c50ba1a9ce47c52d1cb5b7136ee9d0ccc0","crypto":{"cipher":"aes-128-ctr","ciphertext":"2b0ea1bbdd3aa8ae3205de9428a2327e73a3fb301ea7eb36cc3b588879e15983","cipherparams":{"iv":"fbcaccd21ec623ab283aa2506055cff2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"a686699704e7f43cf2ca2421dc026a03e6532d97ab129d66f20faf1439456097"},"mac":"186797fe2b904d2d0ed9f2d839af14785326ef31c4277d6c668c7a4d3431be60"},"id":"a2cd2edd-813d-49e5-b84d-3c18b6e2524f","version":3} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 456c61d580..cdd362b869 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -94,6 +94,9 @@ { "path": "./packages/cactus-test-cmd-api-server/tsconfig.json" }, + { + "path": "./packages/cactus-test-geth-ledger/tsconfig.json" + }, { "path": "./packages/cactus-test-plugin-consortium-manual/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 9a1a721061..2683cd1491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7567,6 +7567,21 @@ __metadata: languageName: unknown linkType: soft +"@hyperledger/cactus-test-geth-ledger@workspace:packages/cactus-test-geth-ledger": + version: 0.0.0-use.local + resolution: "@hyperledger/cactus-test-geth-ledger@workspace:packages/cactus-test-geth-ledger" + dependencies: + "@hyperledger/cactus-common": 2.0.0-alpha.1 + "@hyperledger/cactus-test-tooling": 2.0.0-alpha.1 + "@types/dockerode": 3.2.7 + dockerode: 3.3.0 + internal-ip: 6.2.0 + run-time-error: 1.4.0 + web3: 4.0.3 + web3-eth-accounts: 4.0.3 + languageName: unknown + linkType: soft + "@hyperledger/cactus-test-plugin-consortium-manual@workspace:packages/cactus-test-plugin-consortium-manual": version: 0.0.0-use.local resolution: "@hyperledger/cactus-test-plugin-consortium-manual@workspace:packages/cactus-test-plugin-consortium-manual" @@ -45206,7 +45221,7 @@ __metadata: languageName: node linkType: hard -"web3-core@npm:4.1.1, web3-core@npm:^4.1.1": +"web3-core@npm:4.1.1, web3-core@npm:^4.0.3, web3-core@npm:^4.1.1": version: 4.1.1 resolution: "web3-core@npm:4.1.1" dependencies: @@ -45326,7 +45341,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-abi@npm:^4.1.1": +"web3-eth-abi@npm:^4.0.3, web3-eth-abi@npm:^4.1.1": version: 4.1.1 resolution: "web3-eth-abi@npm:4.1.1" dependencies: @@ -45470,7 +45485,22 @@ __metadata: languageName: node linkType: hard -"web3-eth-accounts@npm:^4.0.5": +"web3-eth-accounts@npm:4.0.3": + version: 4.0.3 + resolution: "web3-eth-accounts@npm:4.0.3" + dependencies: + "@ethereumjs/rlp": ^4.0.1 + crc-32: ^1.2.2 + ethereum-cryptography: ^2.0.0 + web3-errors: ^1.0.2 + web3-types: ^1.0.2 + web3-utils: ^4.0.3 + web3-validator: ^1.0.2 + checksum: 487343ecad8f37a471bb43d0a1f340ce98790bcaeec68247737c6cfb136902dc600f053636a7ceeed3c51db2862b10404fa8812d2a05a77e5d56182fb8a1f5e6 + languageName: node + linkType: hard + +"web3-eth-accounts@npm:^4.0.3, web3-eth-accounts@npm:^4.0.5": version: 4.0.5 resolution: "web3-eth-accounts@npm:4.0.5" dependencies: @@ -45597,7 +45627,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-contract@npm:^4.0.5": +"web3-eth-contract@npm:^4.0.3, web3-eth-contract@npm:^4.0.5": version: 4.0.5 resolution: "web3-eth-contract@npm:4.0.5" dependencies: @@ -45724,7 +45754,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-ens@npm:^4.0.5": +"web3-eth-ens@npm:^4.0.3, web3-eth-ens@npm:^4.0.5": version: 4.0.5 resolution: "web3-eth-ens@npm:4.0.5" dependencies: @@ -45811,7 +45841,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-iban@npm:^4.0.5": +"web3-eth-iban@npm:^4.0.3, web3-eth-iban@npm:^4.0.5": version: 4.0.5 resolution: "web3-eth-iban@npm:4.0.5" dependencies: @@ -45921,7 +45951,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-personal@npm:^4.0.5": +"web3-eth-personal@npm:^4.0.3, web3-eth-personal@npm:^4.0.5": version: 4.0.5 resolution: "web3-eth-personal@npm:4.0.5" dependencies: @@ -46075,7 +46105,7 @@ __metadata: languageName: node linkType: hard -"web3-eth@npm:4.1.1, web3-eth@npm:^4.1.1": +"web3-eth@npm:4.1.1, web3-eth@npm:^4.0.3, web3-eth@npm:^4.1.1": version: 4.1.1 resolution: "web3-eth@npm:4.1.1" dependencies: @@ -46171,7 +46201,7 @@ __metadata: languageName: node linkType: hard -"web3-net@npm:^4.0.5": +"web3-net@npm:^4.0.3, web3-net@npm:^4.0.5": version: 4.0.5 resolution: "web3-net@npm:4.0.5" dependencies: @@ -46259,7 +46289,7 @@ __metadata: languageName: node linkType: hard -"web3-providers-http@npm:^4.0.5": +"web3-providers-http@npm:^4.0.3, web3-providers-http@npm:^4.0.5": version: 4.0.5 resolution: "web3-providers-http@npm:4.0.5" dependencies: @@ -46429,7 +46459,7 @@ __metadata: languageName: node linkType: hard -"web3-providers-ws@npm:^4.0.5": +"web3-providers-ws@npm:^4.0.3, web3-providers-ws@npm:^4.0.5": version: 4.0.5 resolution: "web3-providers-ws@npm:4.0.5" dependencies: @@ -46443,7 +46473,7 @@ __metadata: languageName: node linkType: hard -"web3-rpc-methods@npm:^1.1.1": +"web3-rpc-methods@npm:^1.0.2, web3-rpc-methods@npm:^1.1.1": version: 1.1.1 resolution: "web3-rpc-methods@npm:1.1.1" dependencies: @@ -46798,6 +46828,30 @@ __metadata: languageName: node linkType: hard +"web3@npm:4.0.3": + version: 4.0.3 + resolution: "web3@npm:4.0.3" + dependencies: + web3-core: ^4.0.3 + web3-errors: ^1.0.2 + web3-eth: ^4.0.3 + web3-eth-abi: ^4.0.3 + web3-eth-accounts: ^4.0.3 + web3-eth-contract: ^4.0.3 + web3-eth-ens: ^4.0.3 + web3-eth-iban: ^4.0.3 + web3-eth-personal: ^4.0.3 + web3-net: ^4.0.3 + web3-providers-http: ^4.0.3 + web3-providers-ws: ^4.0.3 + web3-rpc-methods: ^1.0.2 + web3-types: ^1.0.2 + web3-utils: ^4.0.3 + web3-validator: ^1.0.2 + checksum: 9f96e0790a9a32bcb9d9469f65692309b38a5a0c291739555c0227da68db11d5746f1555a52cb147e96394360e5ea5797470d0769bfb60033140906c446605d2 + languageName: node + linkType: hard + "web3@npm:4.1.1": version: 4.1.1 resolution: "web3@npm:4.1.1"