diff --git a/.github/workflows/test-sim-merge.yml b/.github/workflows/test-sim-merge.yml index 6e462964f254..41496b06ed03 100644 --- a/.github/workflows/test-sim-merge.yml +++ b/.github/workflows/test-sim-merge.yml @@ -49,6 +49,7 @@ jobs: EL_BINARY_DIR: ../../go-ethereum/build/bin EL_SCRIPT_DIR: geth EL_PORT: 8545 + TX_SCENARIOS: simple # Install Nethermind merge interop - uses: actions/setup-dotnet@v1 diff --git a/kintsugi/geth/common-setup.sh b/kintsugi/geth/common-setup.sh new file mode 100755 index 000000000000..827c786e33f3 --- /dev/null +++ b/kintsugi/geth/common-setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash -x + +echo $TTD +echo $DATA_DIR +echo $scriptDir +echo $EL_BINARY_DIR + +env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json +echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json +echo "12345678" > $DATA_DIR/password.txt +pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + +$EL_BINARY_DIR/geth --catalyst --datadir $DATA_DIR init $DATA_DIR/genesis.json +$EL_BINARY_DIR/geth --catalyst --datadir $DATA_DIR account import $DATA_DIR/sk.json --password $DATA_DIR/password.txt diff --git a/kintsugi/geth/post-merge.sh b/kintsugi/geth/post-merge.sh index 3cf563551614..8e739f806660 100755 --- a/kintsugi/geth/post-merge.sh +++ b/kintsugi/geth/post-merge.sh @@ -1,12 +1,6 @@ #!/bin/bash -x scriptDir=$(dirname $0) +. $scriptDir/common-setup.sh -echo $TTD -echo $DATA_DIR -echo $scriptDir -echo $EL_BINARY_DIR - -env TTD=$TTD envsubst < $scriptDir/genesisPost.tmpl > $DATA_DIR/genesis.json -$EL_BINARY_DIR/geth --catalyst --datadir $DATA_DIR init $DATA_DIR/genesis.json -$EL_BINARY_DIR/geth --catalyst --http --ws -http.api "engine,net,eth" --datadir $DATA_DIR +$EL_BINARY_DIR/geth --catalyst --http --ws -http.api "engine,net,eth" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt diff --git a/kintsugi/geth/pre-merge.sh b/kintsugi/geth/pre-merge.sh index 4f9bdbf6d3ec..9596d6c0406d 100755 --- a/kintsugi/geth/pre-merge.sh +++ b/kintsugi/geth/pre-merge.sh @@ -1,18 +1,6 @@ #!/bin/bash -x scriptDir=$(dirname $0) +. $scriptDir/common-setup.sh -echo $TTD -echo $DATA_DIR -echo $scriptDir -echo $EL_BINARY_DIR - - -env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json -echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json -echo "12345678" > $DATA_DIR/password.txt -pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" - -$EL_BINARY_DIR/geth --catalyst --datadir $DATA_DIR init $DATA_DIR/genesis.json -$EL_BINARY_DIR/geth --catalyst --datadir $DATA_DIR account import $DATA_DIR/sk.json --password $DATA_DIR/password.txt $EL_BINARY_DIR/geth --catalyst --http --ws -http.api "engine,net,eth,miner" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --nodiscover --mine diff --git a/kintsugi/gethdocker/Dockerfile b/kintsugi/gethdocker/Dockerfile new file mode 100644 index 000000000000..d11e38f516d8 --- /dev/null +++ b/kintsugi/gethdocker/Dockerfile @@ -0,0 +1,12 @@ +# Build Geth in a stock Go builder container +FROM golang:1.17-alpine as builder + +RUN apk add --no-cache gcc musl-dev linux-headers git + +RUN git clone --depth 1 -b kintsugi-spec https://github.com/MariusVanDerWijden/go-ethereum.git /go-ethereum +RUN cd /go-ethereum && go run build/ci.go install ./cmd/geth + +FROM alpine:latest +COPY --from=builder /go-ethereum/build/bin/geth /usr/local/bin/ + +EXPOSE 8545 8546 30303 30303/udp diff --git a/kintsugi/gethdocker/README.md b/kintsugi/gethdocker/README.md new file mode 100644 index 000000000000..0db35eef4c7d --- /dev/null +++ b/kintsugi/gethdocker/README.md @@ -0,0 +1,13 @@ +# Geth Docker setup for running the sim merge tests on local machine + +###### Build geth docker image +```bash +cd kintsugi/gethdocker +docker build . --tag geth:kintsugi +``` + +###### Run test scripts +```bash +cd packages/lodestar +EL_BINARY_DIR=geth:kintsugi EL_SCRIPT_DIR=gethdocker EL_PORT=8545 TX_SCENARIOS=simple yarn mocha test/sim/merge-interop.test.ts +``` diff --git a/kintsugi/gethdocker/common-setup.sh b/kintsugi/gethdocker/common-setup.sh new file mode 100755 index 000000000000..ba60a0cdca51 --- /dev/null +++ b/kintsugi/gethdocker/common-setup.sh @@ -0,0 +1,16 @@ +#!/bin/bash -x + +echo $TTD +echo $DATA_DIR +echo $scriptDir +echo $EL_BINARY_DIR + + +env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json +echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json +echo "12345678" > $DATA_DIR/password.txt +pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + + +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) -v /mnt/code/lodestar/mergetest/packages/lodestar/$DATA_DIR:/data $EL_BINARY_DIR geth --catalyst --datadir /data init /data/genesis.json +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) -v /mnt/code/lodestar/mergetest/packages/lodestar/$DATA_DIR:/data $EL_BINARY_DIR geth --catalyst --datadir /data account import /data/sk.json --password /data/password.txt diff --git a/kintsugi/gethdocker/genesisPost.tmpl b/kintsugi/gethdocker/genesisPost.tmpl new file mode 100644 index 000000000000..988a9ecdbc60 --- /dev/null +++ b/kintsugi/gethdocker/genesisPost.tmpl @@ -0,0 +1,36 @@ +{ + "config": + { + "chainId":1, + "homesteadBlock":0, + "eip150Block":0, + "eip155Block":0, + "eip158Block":0, + "byzantiumBlock":0, + "constantinopleBlock":0, + "petersburgBlock":0, + "istanbulBlock":0, + "muirGlacierBlock":0, + "berlinBlock":0, + "londonBlock":0, + "clique": { + "period": 5, + "epoch": 30000 + }, + "terminalTotalDifficulty":${TTD} + }, + "nonce":"0x42", + "timestamp":"0x0", + "extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x1C9C380", + "difficulty":"0x400000000", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "alloc":{ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b":{"balance":"0x6d6172697573766477000000"} + }, + "number":"0x0", + "gasUsed":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" +} diff --git a/kintsugi/gethdocker/genesisPre.tmpl b/kintsugi/gethdocker/genesisPre.tmpl new file mode 100644 index 000000000000..e3e1fa4141be --- /dev/null +++ b/kintsugi/gethdocker/genesisPre.tmpl @@ -0,0 +1,36 @@ +{ + "config": { + "chainId": 1, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "clique": { + "period": 5, + "epoch": 30000 + }, + "terminalTotalDifficulty": ${TTD} + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x1c9c380", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {"balance": "0x6d6172697573766477000000"} + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x7" +} diff --git a/kintsugi/gethdocker/post-merge.sh b/kintsugi/gethdocker/post-merge.sh new file mode 100755 index 000000000000..fce3105cac49 --- /dev/null +++ b/kintsugi/gethdocker/post-merge.sh @@ -0,0 +1,6 @@ +#!/bin/bash -x + +scriptDir=$(dirname $0) +. $scriptDir/common-setup.sh + +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v /mnt/code/lodestar/mergetest/packages/lodestar/$DATA_DIR:/data $EL_BINARY_DIR geth --catalyst --http --ws -http.api "engine,net,eth" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data diff --git a/kintsugi/gethdocker/pre-merge.sh b/kintsugi/gethdocker/pre-merge.sh new file mode 100755 index 000000000000..69a763be406a --- /dev/null +++ b/kintsugi/gethdocker/pre-merge.sh @@ -0,0 +1,7 @@ +#!/bin/bash -x + +scriptDir=$(dirname $0) +. $scriptDir/common-setup.sh + +# EL_BINARY_DIR refers to the local docker image build from kintsugi/gethdocker folder +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v /mnt/code/lodestar/mergetest/packages/lodestar/$DATA_DIR:/data $EL_BINARY_DIR geth --catalyst --http --ws -http.api "engine,net,eth,miner" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --nodiscover --mine diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index d1ff0b4c955b..30bf32eeb495 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -8,6 +8,8 @@ import {LogLevel, sleep, TimestampFormatCode} from "@chainsafe/lodestar-utils"; import {SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; import {IChainConfig} from "@chainsafe/lodestar-config"; import {Epoch} from "@chainsafe/lodestar-types"; +import {merge} from "@chainsafe/lodestar-beacon-state-transition"; + import {ExecutionEngineHttp} from "../../src/executionEngine/http"; import {shell} from "./shell"; import {ChainEvent} from "../../src/chain"; @@ -25,9 +27,10 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // EL_BINARY_DIR: File path to locate the EL executable // EL_SCRIPT_DIR: Directory in kintsugi folder for the EL client, from where to execute post-merge/pre-merge EL scenario scripts // EL_PORT: EL port on localhost for hosting both engine & json rpc endpoints +// TX_SCENARIOS: comma seprated transaction scenarios this EL client build supports // Example: // ``` -// $ EL_BINARY_DIR=/home/lion/Code/eth2.0/merge-interop/go-ethereum/build/bin EL_SCRIPT_DIR=geth EL_PORT=8545 ../../node_modules/.bin/mocha test/sim/merge.test.ts +// $ EL_BINARY_DIR=/home/lion/Code/eth2.0/merge-interop/go-ethereum/build/bin EL_SCRIPT_DIR=geth EL_PORT=8545 TX_SCENARIOS=simple ../../node_modules/.bin/mocha test/sim/merge.test.ts // ``` /* eslint-disable no-console, @typescript-eslint/naming-convention, quotes */ @@ -35,6 +38,7 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // MERGE_EPOCH will happen at 2 sec * 8 slots = 16 sec // 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec const terminalTotalDifficultyPreMerge = 20; +const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; describe("executionEngine / ExecutionEngineHttp", function () { this.timeout("10min"); @@ -129,6 +133,18 @@ describe("executionEngine / ExecutionEngineHttp", function () { it("Send stub payloads to EL", async () => { const {genesisBlockHash} = await runEL("post-merge.sh", 0); + if (TX_SCENARIOS.includes("simple")) { + await sendTransaction(jsonRpcUrl, { + from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + to: "0xafa3f8684e54059998bc3a7b0d2b0da075154d66", + gas: "0x76c0", + gasPrice: "0x9184e72a000", + value: "0x9184e72a", + }); + + const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + if (balance != "0x0") throw new Error("Invalid Balance: " + balance); + } const controller = new AbortController(); const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl]}, controller.signal); @@ -162,11 +178,17 @@ describe("executionEngine / ExecutionEngineHttp", function () { **/ const payload = await executionEngine.getPayload(payloadId); + if (TX_SCENARIOS.includes("simple")) { + if (payload.transactions.length !== 1) + throw new Error("Expected a simple transaction to be in the fetched payload"); + const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + if (balance != "0x0") throw new Error("Invalid Balance: " + balance); + } // 3. Execute the payload /** * curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_executePayloadV1","params":[{"parentHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a","coinbase":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b","stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45","receiptRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","random":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","gasLimit":"0x1c9c380","gasUsed":"0x0","timestamp":"0x5","extraData":"0x","baseFeePerGas":"0x7","blockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858","transactions":[]}],"id":67}' http://localhost:8550 - * **/ + **/ const payloadResult = await executionEngine.executePayload(payload); if (!payloadResult) { @@ -181,6 +203,11 @@ describe("executionEngine / ExecutionEngineHttp", function () { await executionEngine.notifyForkchoiceUpdate(bytesToData(payload.blockHash), genesisBlockHash); + if (TX_SCENARIOS.includes("simple")) { + const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + if (balance !== "0x9184e72a") throw new Error("Invalid Balance"); + } + // Error cases // 1. unknown payload @@ -310,7 +337,53 @@ describe("executionEngine / ExecutionEngineHttp", function () { await Promise.all(validators.map((v) => v.start())); - await new Promise((resolve) => { + if (TX_SCENARIOS.includes("simple")) { + // If mergeEpoch > 0, this is the case of pre-merge transaction submission on EL pow + await sendTransaction(jsonRpcUrl, { + from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + to: "0xafa3f8684e54059998bc3a7b0d2b0da075154d66", + gas: "0x76c0", + gasPrice: "0x9184e72a000", + value: "0x9184e72a", + }); + } + + await new Promise((resolve, reject) => { + // Play TX_SCENARIOS + bn.chain.emitter.on(ChainEvent.clockSlot, async (slot) => { + if (slot < 2) return; + switch (slot) { + // If mergeEpoch > 0, this is the case of pre-merge transaction confirmation on EL pow + case 2: + if (TX_SCENARIOS.includes("simple")) { + const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + if (balance !== "0x9184e72a") reject("Invalid Balance"); + } + break; + + // By this slot, ttd should be reached and merge complete + case Number(ttd) + 3: { + const headState = bn.chain.getHeadState(); + const isMergeComplete = merge.isMergeStateType(headState) && merge.isMergeComplete(headState); + if (!isMergeComplete) reject("Merge not completed"); + + // Send another tx post-merge, total amount in destination account should be double after this is included in chain + if (TX_SCENARIOS.includes("simple")) { + await sendTransaction(jsonRpcUrl, { + from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + to: "0xafa3f8684e54059998bc3a7b0d2b0da075154d66", + gas: "0x76c0", + gasPrice: "0x9184e72a000", + value: "0x9184e72a", + }); + } + break; + } + + default: + } + }); + bn.chain.emitter.on(ChainEvent.finalized, (checkpoint) => { // Resolve only if the finalized checkpoint includes execution payload const finalizedBlock = bn.chain.forkChoice.getBlock(checkpoint.root); @@ -346,6 +419,12 @@ describe("executionEngine / ExecutionEngineHttp", function () { ); } + if (TX_SCENARIOS.includes("simple")) { + const balance = await getBalance(jsonRpcUrl, "0xafa3f8684e54059998bc3a7b0d2b0da075154d66"); + // 0x12309ce54 = 2 * 0x9184e72a + if (balance !== "0x12309ce54") throw Error("Invalid Balance"); + } + // wait for 1 slot to print current epoch stats await sleep(1 * bn.config.SECONDS_PER_SLOT * 1000); stopInfoTracker(); @@ -360,6 +439,9 @@ async function waitForELOnline(url: string, signal: AbortSignal): Promise await shell( `curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"net_version","params":[],"id":67}' ${url}` ); + + console.log("Waiting for few seconds for EL to fully setup, for e.g. unlock the account..."); + await sleep(5000, signal); return; // Done } catch (e) { await sleep(1000, signal); @@ -418,3 +500,19 @@ async function getGenesisBlockHash(url: string, signal: AbortSignal): Promise): Promise { + await shell( + `curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[${JSON.stringify( + transaction + )}],"id":67}' ${url}` + ); +} + +async function getBalance(url: string, account: string): Promise { + const response: string = await shell( + `curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["${account}","latest"],"id":67}' ${url}` + ); + const {result} = (JSON.parse(response) as unknown) as Record; + return result; +}