diff --git a/.github/workflows/test-sim-merge.yml b/.github/workflows/test-sim-merge.yml index 641d5ab2732e..6c2eb4d783e3 100644 --- a/.github/workflows/test-sim-merge.yml +++ b/.github/workflows/test-sim-merge.yml @@ -55,28 +55,9 @@ jobs: - name: Pull Geth run: docker pull $GETH_IMAGE - - name: Test Lodestar <> Geth interop - run: yarn test:sim:merge-interop - working-directory: packages/beacon-node - env: - EL_BINARY_DIR: ${{ env.GETH_IMAGE }} - EL_SCRIPT_DIR: gethdocker - ENGINE_PORT: 8551 - ETH_PORT: 8545 - TX_SCENARIOS: simple - - name: Pull Nethermind run: docker pull $NETHERMIND_IMAGE - - name: Test Lodestar <> Nethermind interop - run: yarn test:sim:merge-interop - working-directory: packages/beacon-node - env: - EL_BINARY_DIR: ${{ env.NETHERMIND_IMAGE }} - EL_SCRIPT_DIR: netherminddocker - ENGINE_PORT: 8551 - ETH_PORT: 8545 - - name: Pull mergemock run: docker pull $MERGEMOCK_IMAGE diff --git a/docs/pages/contribution/testing/integration-tests.md b/docs/pages/contribution/testing/integration-tests.md index dcf0201e4949..c93cb635afca 100644 --- a/docs/pages/contribution/testing/integration-tests.md +++ b/docs/pages/contribution/testing/integration-tests.md @@ -20,8 +20,6 @@ The images used by this test during CI are: - `GETH_WITHDRAWALS_IMAGE: g11tech/geth:withdrawalsfeb8` - `ETHEREUMJS_WITHDRAWALS_IMAGE: g11tech/ethereumjs:blobs-b6b63` -#### `test:sim:merge-interop` - #### `test:sim:mergemock` #### `yarn test:sim:blobs` diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index 7608035cb440..4908b2f4ea23 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -80,7 +80,6 @@ "test:unit": "wrapper() { yarn test:unit:minimal $@ && yarn test:unit:mainnet $@; }; wrapper", "test:e2e": "LODESTAR_PRESET=minimal vitest --run --segfaultRetry 3 --config vitest.e2e.config.ts --dir test/e2e", "test:sim": "vitest --run test/sim/**/*.test.ts", - "test:sim:merge-interop": "vitest --run test/sim/merge-interop.test.ts", "test:sim:mergemock": "vitest --run test/sim/mergemock.test.ts", "test:sim:withdrawals": "vitest --run test/sim/withdrawal-interop.test.ts", "test:sim:blobs": "vitest --run test/sim/4844-interop.test.ts", diff --git a/packages/beacon-node/test/sim/merge-interop.test.ts b/packages/beacon-node/test/sim/merge-interop.test.ts deleted file mode 100644 index 65c3381f94fb..000000000000 --- a/packages/beacon-node/test/sim/merge-interop.test.ts +++ /dev/null @@ -1,458 +0,0 @@ -import fs from "node:fs"; -import {describe, it, afterAll, afterEach, vi} from "vitest"; -import {fromHexString} from "@chainsafe/ssz"; -import {isExecutionStateType, isMergeTransitionComplete} from "@lodestar/state-transition"; -import {LogLevel, sleep} from "@lodestar/utils"; -import {TimestampFormatCode} from "@lodestar/logger"; -import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {ChainConfig} from "@lodestar/config"; -import {routes} from "@lodestar/api"; -import {Epoch} from "@lodestar/types"; -import {ValidatorProposerConfig} from "@lodestar/validator"; - -import {ExecutionPayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; -import {initializeExecutionEngine} from "../../src/execution/index.js"; -import {ClockEvent} from "../../src/util/clock.js"; -import {testLogger, TestLoggerOpts} from "../utils/logger.js"; -import {getDevBeaconNode} from "../utils/node/beacon.js"; -import {BeaconRestApiServerOpts} from "../../src/api/index.js"; -import {simTestInfoTracker} from "../utils/node/simTest.js"; -import {getAndInitDevValidators} from "../utils/node/validator.js"; -import {Eth1Provider} from "../../src/index.js"; -import {ZERO_HASH} from "../../src/constants/index.js"; -import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/utils.js"; -import {defaultExecutionEngineHttpOpts} from "../../src/execution/engine/http.js"; -import {runEL, ELStartMode, ELClient, sendTransaction, getBalance} from "../utils/runEl.js"; -import {logFilesDir} from "./params.js"; -import {shell} from "./shell.js"; - -// NOTE: Must specify -// EL_BINARY_DIR: File path to locate the EL executable -// EL_SCRIPT_DIR: Directory in packages/beacon-node for the EL client, from where to -// execute post-merge/pre-merge EL scenario scripts -// ETH_PORT: EL port on localhost hosting non auth protected eth_ methods -// ENGINE_PORT: Specify the port on which an jwt auth protected engine api is being hosted, -// typically by default at 8551 for geth. Some ELs could host it as same port as eth_ apis, -// but just with the engine_ methods protected. In that case this param can be skipped -// 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 ETH_PORT=8545 ENGINE_PORT=8551 TX_SCENARIOS=simple \ -// ../../node_modules/.bin/vitest --run test/sim/merge.test.ts -// ``` - -/* eslint-disable no-console, @typescript-eslint/naming-convention, quotes */ - -// BELLATRIX_EPOCH will happen at 2 sec * 8 slots = 16 sec -// 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec -const terminalTotalDifficultyPreMerge = 10; -const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; -const jwtSecretHex = "0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; -const retries = defaultExecutionEngineHttpOpts.retries; -const retryDelay = defaultExecutionEngineHttpOpts.retryDelay; - -describe("executionEngine / ExecutionEngineHttp", function () { - if (!process.env.EL_BINARY_DIR || !process.env.EL_SCRIPT_DIR) { - throw Error( - `EL ENV must be provided, EL_BINARY_DIR: ${process.env.EL_BINARY_DIR}, EL_SCRIPT_DIR: ${process.env.EL_SCRIPT_DIR}` - ); - } - vi.setConfig({testTimeout: 10 * 60 * 1000}); - - const dataPath = fs.mkdtempSync("lodestar-test-merge-interop"); - const elSetupConfig = { - elScriptDir: process.env.EL_SCRIPT_DIR, - elBinaryDir: process.env.EL_BINARY_DIR, - }; - const elRunOptions = { - dataPath, - jwtSecretHex, - enginePort: parseInt(process.env.ENGINE_PORT ?? "8551"), - ethPort: parseInt(process.env.ETH_PORT ?? "8545"), - }; - - const controller = new AbortController(); - afterAll(async () => { - controller?.abort(); - await shell(`sudo rm -rf ${dataPath}`); - }); - - const afterEachCallbacks: (() => Promise | void)[] = []; - afterEach(async () => { - while (afterEachCallbacks.length > 0) { - const callback = afterEachCallbacks.pop(); - if (callback) await callback(); - } - }); - - it("Send stub payloads to EL", async () => { - const {elClient, tearDownCallBack} = await runEL( - {...elSetupConfig, mode: ELStartMode.PostMerge}, - {...elRunOptions, ttd: BigInt(0)}, - controller.signal - ); - afterEachCallbacks.push(() => tearDownCallBack()); - const {genesisBlockHash, engineRpcUrl, ethRpcUrl} = elClient; - - if (TX_SCENARIOS.includes("simple")) { - await sendTransaction(ethRpcUrl, { - from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - gas: "0x76c0", - gasPrice: "0x9184e72a000", - value: "0x9184e72a", - }); - - const balance = await getBalance(ethRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - if (balance != "0x0") throw new Error("Invalid Balance: " + balance); - } - - //const controller = new AbortController(); - const executionEngine = initializeExecutionEngine( - {mode: "http", urls: [engineRpcUrl], jwtSecretHex, retries, retryDelay}, - {signal: controller.signal, logger: testLogger("Node-A-Engine")} - ); - - // 1. Prepare a payload - - /** - * curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_forkchoiceUpdatedV1","params":[{"headBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a", "safeBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a", "finalizedBlockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}, {"timestamp":"0x5", "prevRandao":"0x0000000000000000000000000000000000000000000000000000000000000000", "feeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"}],"id":67}' http://localhost:8550 - **/ - - const preparePayloadParams: PayloadAttributes = { - // Note: this is created with a pre-defined genesis.json - timestamp: quantityToNum("0x5"), - prevRandao: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), - suggestedFeeRecipient: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - }; - - const finalizedBlockHash = "0x0000000000000000000000000000000000000000000000000000000000000000"; - - const payloadId = await executionEngine.notifyForkchoiceUpdate( - ForkName.bellatrix, - genesisBlockHash, - //use finalizedBlockHash as safeBlockHash - finalizedBlockHash, - finalizedBlockHash, - preparePayloadParams - ); - - if (!payloadId) throw Error("InvalidPayloadId"); - - // 2. Get the payload - /** - * curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_getPayloadV1","params":["0xa247243752eb10b4"],"id":67}' http://localhost:8550 - **/ - - const {executionPayload: payload} = await executionEngine.getPayload(ForkName.bellatrix, 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(ethRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - 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_newPayloadV1","params":[{"parentHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a","coinbase":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b","stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45","receiptRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"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.notifyNewPayload(ForkName.bellatrix, payload); - if (payloadResult.status !== ExecutionPayloadStatus.VALID) { - throw Error("getPayload returned payload that notifyNewPayload deems invalid"); - } - - // 4. Update the fork choice - - /** - * curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_forkchoiceUpdatedV1","params":[{"headBlockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858", "safeBlockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858", "finalizedBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a"}, null],"id":67}' http://localhost:8550 - **/ - - await executionEngine.notifyForkchoiceUpdate( - ForkName.bellatrix, - bytesToData(payload.blockHash), - genesisBlockHash, - genesisBlockHash - ); - - if (TX_SCENARIOS.includes("simple")) { - const balance = await getBalance(ethRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - if (balance !== "0x9184e72a") throw new Error("Invalid Balance"); - } - - // Error cases - // 1. unknown payload - - /** - * curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_getPayload", - * "params":["0x123"] - * ,"id":67}' http://localhost:8545 - */ - - // await executionEngine.getPayload(1234567); - - // 2. unknown header - - /** - * curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_consensusValidated","params":[{ - * "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000", - * "status":"VALID" - * }],"id":67}' http://localhost:8545 - */ - }); - - it("Post-merge, run for a few blocks", async function () { - console.log("\n\nPost-merge, run for a few blocks\n\n"); - const {elClient, tearDownCallBack} = await runEL( - {...elSetupConfig, mode: ELStartMode.PostMerge}, - {...elRunOptions, ttd: BigInt(0)}, - controller.signal - ); - afterEachCallbacks.push(() => tearDownCallBack()); - - await runNodeWithEL({ - elClient, - bellatrixEpoch: 0, - testName: "post-merge", - }); - }); - - it("Pre-merge, run for a few blocks", async function () { - console.log("\n\nPre-merge, run for a few blocks\n\n"); - const {elClient, tearDownCallBack} = await runEL( - {...elSetupConfig, mode: ELStartMode.PreMerge}, - {...elRunOptions, ttd: BigInt(terminalTotalDifficultyPreMerge)}, - controller.signal - ); - afterEachCallbacks.push(() => tearDownCallBack()); - - await runNodeWithEL({ - elClient, - bellatrixEpoch: 1, - testName: "pre-merge", - }); - }); - - async function runNodeWithEL({ - elClient, - bellatrixEpoch, - testName, - }: { - elClient: ELClient; - bellatrixEpoch: Epoch; - testName: string; - }): Promise { - const {genesisBlockHash, ttd, engineRpcUrl, ethRpcUrl} = elClient; - const validatorClientCount = 1; - const validatorsPerClient = 32; - - const testParams: Pick = { - SECONDS_PER_SLOT: 2, - }; - - // Should reach justification in 6 epochs max. - // Merge block happens at epoch 2 slot 4. Then 4 epochs to finalize - const expectedEpochsToFinish = 6; - // 1 epoch of margin of error - const epochsOfMargin = 1; - const timeoutSetupMargin = 30 * 1000; // Give extra 30 seconds of margin - - // delay a bit so regular sync sees it's up to date and sync is completed from the beginning - const genesisSlotsDelay = 8; - - const timeout = - ((epochsOfMargin + expectedEpochsToFinish) * SLOTS_PER_EPOCH + genesisSlotsDelay) * - testParams.SECONDS_PER_SLOT * - 1000; - - vi.setConfig({testTimeout: timeout + 2 * timeoutSetupMargin}); - - const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * testParams.SECONDS_PER_SLOT; - - const testLoggerOpts: TestLoggerOpts = { - level: LogLevel.info, - file: { - filepath: `${logFilesDir}/merge-interop-${testName}.log`, - level: LogLevel.debug, - }, - timestampFormat: { - format: TimestampFormatCode.EpochSlot, - genesisTime, - slotsPerEpoch: SLOTS_PER_EPOCH, - secondsPerSlot: testParams.SECONDS_PER_SLOT, - }, - }; - const loggerNodeA = testLogger("Node-A", testLoggerOpts); - - const bn = await getDevBeaconNode({ - params: { - ...testParams, - ALTAIR_FORK_EPOCH: 0, - BELLATRIX_FORK_EPOCH: bellatrixEpoch, - TERMINAL_TOTAL_DIFFICULTY: ttd, - }, - options: { - api: {rest: {enabled: true} as BeaconRestApiServerOpts}, - sync: {isSingleNode: true}, - network: {allowPublishToZeroPeers: true, discv5: null}, - // Now eth deposit/merge tracker methods directly available on engine endpoints - eth1: {enabled: true, providerUrls: [engineRpcUrl], jwtSecretHex}, - executionEngine: {urls: [engineRpcUrl], jwtSecretHex}, - chain: {suggestedFeeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, - }, - validatorCount: validatorClientCount * validatorsPerClient, - logger: loggerNodeA, - genesisTime, - eth1BlockHash: fromHexString(genesisBlockHash), - }); - - afterEachCallbacks.push(async function () { - await bn.close(); - await sleep(1000); - }); - - const stopInfoTracker = simTestInfoTracker(bn, loggerNodeA); - const valProposerConfig = { - proposerConfig: { - "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c": { - graffiti: "graffiti", - strictFeeRecipientCheck: true, - feeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - builder: { - gasLimit: 30000000, - builderSelection: "executiononly", - }, - }, - "0xa4855c83d868f772a579133d9f23818008417b743e8447e235d8eb78b1d8f8a9f63f98c551beb7de254400f89592314d": { - feeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - builder: { - gasLimit: 35000000, - }, - }, - }, - defaultConfig: { - graffiti: "default graffiti", - strictFeeRecipientCheck: true, - feeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc", - builder: { - gasLimit: 30000000, - }, - }, - } as ValidatorProposerConfig; - - const {validators} = await getAndInitDevValidators({ - logPrefix: "Node-A", - node: bn, - validatorsPerClient, - validatorClientCount, - startIndex: 0, - // At least one sim test must use the REST API for beacon <-> validator comms - useRestApi: true, - testLoggerOpts, - valProposerConfig, - }); - - afterEachCallbacks.push(async function () { - await Promise.all(validators.map((v) => v.close())); - }); - - if (TX_SCENARIOS.includes("simple")) { - // If bellatrixEpoch > 0, this is the case of pre-merge transaction submission on EL pow - await sendTransaction(ethRpcUrl, { - from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - gas: "0x76c0", - gasPrice: "0x9184e72a000", - value: "0x9184e72a", - }); - } - - await new Promise((resolve, reject) => { - // Play TX_SCENARIOS - bn.chain.clock.on(ClockEvent.slot, async (slot) => { - if (slot < 2) return; - switch (slot) { - // If bellatrixEpoch > 0, this is the case of pre-merge transaction confirmation on EL pow - case 2: - if (TX_SCENARIOS.includes("simple")) { - const balance = await getBalance(ethRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - 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(); - if (!(isExecutionStateType(headState) && isMergeTransitionComplete(headState))) { - 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(ethRpcUrl, { - from: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", - to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - gas: "0x76c0", - gasPrice: "0x9184e72a000", - value: "0x9184e72a", - }); - } - break; - } - - default: - } - }); - - bn.chain.emitter.on(routes.events.EventType.finalizedCheckpoint, (checkpoint) => { - // Resolve only if the finalized checkpoint includes execution payload - const finalizedBlock = bn.chain.forkChoice.getBlockHex(checkpoint.block); - if (finalizedBlock?.executionPayloadBlockHash !== null) { - console.log(`\nGot finalized event, stopping validators and nodes\n`); - resolve(); - } - }); - }); - - // Stop chain and un-subscribe events so the execution engine won't update it's head - // Allow some time to broadcast finalized events and complete the importBlock routine - await Promise.all(validators.map((v) => v.close())); - await bn.close(); - await sleep(500); - - if (bn.chain.beaconProposerCache.get(1) !== "0xcccccccccccccccccccccccccccccccccccccccc") { - throw Error("Invalid feeRecipient set at BN"); - } - - // Assertions to make sure the end state is good - // 1. The proper head is set - const rpc = new Eth1Provider({DEPOSIT_CONTRACT_ADDRESS: ZERO_HASH}, {providerUrls: [engineRpcUrl], jwtSecretHex}); - const consensusHead = bn.chain.forkChoice.getHead(); - const executionHeadBlockNumber = await rpc.getBlockNumber(); - const executionHeadBlock = await rpc.getBlockByNumber(executionHeadBlockNumber); - if (!executionHeadBlock) throw Error("Execution has not head block"); - if (consensusHead.executionPayloadBlockHash !== executionHeadBlock.hash) { - throw Error( - "Consensus head not equal to execution head: " + - JSON.stringify({ - executionHeadBlockNumber, - executionHeadBlockHash: executionHeadBlock.hash, - consensusHeadExecutionPayloadBlockHash: consensusHead.executionPayloadBlockHash, - consensusHeadSlot: consensusHead.slot, - }) - ); - } - - if (TX_SCENARIOS.includes("simple")) { - const balance = await getBalance(ethRpcUrl, "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - // 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(); - console.log("\n\nDone\n\n"); - } -}); diff --git a/packages/cli/test/sim/mixed_client.test.ts b/packages/cli/test/sim/mixed_client.test.ts index 80c20471ede5..9d56349457e6 100644 --- a/packages/cli/test/sim/mixed_client.test.ts +++ b/packages/cli/test/sim/mixed_client.test.ts @@ -70,6 +70,7 @@ env.tracker.register({ await env.start({runTimeoutMs: estimatedTimeoutMs}); await connectAllNodes(env.nodes); -await waitForSlot(env.clock.getLastSlotOfEpoch(capellaForkEpoch + 1), env.nodes, {env, silent: true}); +// Stopping at last slot usually cause assertion to fail because of missing data as node are shutting down +await waitForSlot(env.clock.getLastSlotOfEpoch(capellaForkEpoch + 1) + 2, env.nodes, {env, silent: true}); await env.stop(); diff --git a/packages/cli/test/sim/multi_fork.test.ts b/packages/cli/test/sim/multi_fork.test.ts index 1302ad98bc82..d816e0ddae54 100644 --- a/packages/cli/test/sim/multi_fork.test.ts +++ b/packages/cli/test/sim/multi_fork.test.ts @@ -17,6 +17,8 @@ import { import {nodeAssertion} from "../utils/simulation/assertions/nodeAssertion.js"; import {mergeAssertion} from "../utils/simulation/assertions/mergeAssertion.js"; import {createForkAssertion} from "../utils/simulation/assertions/forkAssertion.js"; +import {createAccountBalanceAssertion} from "../utils/simulation/assertions/accountBalanceAssertion.js"; +import {createExecutionHeadAssertion} from "../utils/simulation/assertions/executionHeadAssertion.js"; const altairForkEpoch = 2; const bellatrixForkEpoch = 4; @@ -132,6 +134,25 @@ env.tracker.register({ }, }); +env.tracker.register( + createAccountBalanceAssertion({ + address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + sendTransactionsAtSlot: [ + env.clock.getFirstSlotOfEpoch(altairForkEpoch) + 4, + env.clock.getFirstSlotOfEpoch(bellatrixForkEpoch) + 4, + ], + validateTotalBalanceAt: [env.clock.getFirstSlotOfEpoch(bellatrixForkEpoch + 1) + 4], + targetNode: env.nodes[0], + }) +); + +env.tracker.register( + createExecutionHeadAssertion({ + // Second last slot of second bellatrix epoch + checkForSlot: [env.clock.getLastSlotOfEpoch(bellatrixForkEpoch + 1) - 1], + }) +); + await env.start({runTimeoutMs: estimatedTimeoutMs}); await connectAllNodes(env.nodes); diff --git a/packages/cli/test/utils/simulation/assertions/accountBalanceAssertion.ts b/packages/cli/test/utils/simulation/assertions/accountBalanceAssertion.ts new file mode 100644 index 000000000000..2367763b0a2f --- /dev/null +++ b/packages/cli/test/utils/simulation/assertions/accountBalanceAssertion.ts @@ -0,0 +1,74 @@ +import {EL_GENESIS_ACCOUNT} from "../constants.js"; +import {AssertionMatch, AssertionResult, NodePair, SimulationAssertion} from "../interfaces.js"; + +function hexToBigInt(num: string): bigint { + return num.startsWith("0x") ? BigInt(num) : BigInt(`0x${num}`); +} + +function bigIntToHex(num: bigint): string { + return `0x${num.toString(16)}`; +} + +const transactionAmount = BigInt(2441406250); + +export function createAccountBalanceAssertion({ + address, + sendTransactionsAtSlot, + validateTotalBalanceAt, + targetNode, +}: { + address: string; + sendTransactionsAtSlot: number[]; + validateTotalBalanceAt: number[]; + targetNode: NodePair; +}): SimulationAssertion<`accountBalance_${typeof address}`, bigint> { + return { + id: `accountBalance_${address}`, + match({slot, node}) { + if (sendTransactionsAtSlot.includes(slot) && node.id === targetNode.id) return AssertionMatch.Capture; + if (validateTotalBalanceAt.includes(slot) && node.id === targetNode.id) return AssertionMatch.Assert; + return AssertionMatch.None; + }, + async capture({node}) { + await node.execution.provider?.getRpc().fetch({ + method: "eth_sendTransaction", + params: [ + { + to: address, + from: EL_GENESIS_ACCOUNT, + gas: "0x76c0", + gasPrice: "0x9184e72a000", + value: bigIntToHex(transactionAmount), + }, + ], + }); + + // Capture the value transferred to account + return transactionAmount; + }, + async assert({node, store, slot}) { + const errors: AssertionResult[] = []; + + const expectedCaptureSlots = sendTransactionsAtSlot.filter((s) => s <= slot); + if (expectedCaptureSlots.length === 0) errors.push(`No transaction was sent to account ${address}`); + + let expectedBalanceAtCurrentSlot = BigInt(0); + for (const captureSlot of expectedCaptureSlots) { + expectedBalanceAtCurrentSlot += BigInt(store[captureSlot]); + } + + const balance = hexToBigInt( + (await node.execution.provider?.getRpc().fetch({method: "eth_getBalance", params: [address, "latest"]})) ?? + "0x0" + ); + + if (balance !== expectedBalanceAtCurrentSlot) { + errors.push( + `Account balance for ${address} does not match. Expected: ${expectedBalanceAtCurrentSlot}, got: ${balance}` + ); + } + + return errors; + }, + }; +} diff --git a/packages/cli/test/utils/simulation/assertions/executionHeadAssertion.ts b/packages/cli/test/utils/simulation/assertions/executionHeadAssertion.ts new file mode 100644 index 000000000000..595f98aea701 --- /dev/null +++ b/packages/cli/test/utils/simulation/assertions/executionHeadAssertion.ts @@ -0,0 +1,51 @@ +import {ApiError} from "@lodestar/api"; +import {toHex} from "@lodestar/utils"; +import {bellatrix} from "@lodestar/types"; +import {AssertionMatch, AssertionResult, SimulationAssertion} from "../interfaces.js"; + +export function createExecutionHeadAssertion({ + checkForSlot, +}: { + checkForSlot: number[]; +}): SimulationAssertion< + "executionHead", + {executionHead: {hash: string}; consensusHead: {executionPayload: {blockHash: string}}} +> { + return { + id: "executionHead", + match({slot}) { + if (checkForSlot.includes(slot)) return AssertionMatch.Capture | AssertionMatch.Assert; + return AssertionMatch.None; + }, + async capture({node}) { + const blockNumber = await node.execution.provider?.getBlockNumber(); + if (blockNumber == null) throw new Error("Execution provider not available"); + const executionHeadBlock = await node.execution.provider?.getBlockByNumber(blockNumber); + + const consensusHead = await node.beacon.api.beacon.getBlockV2("head"); + ApiError.assert(consensusHead); + + return { + executionHead: {hash: executionHeadBlock?.hash ?? "0x0"}, + consensusHead: { + executionPayload: { + blockHash: toHex( + (consensusHead.response.data.message as bellatrix.BeaconBlock).body.executionPayload.blockHash + ), + }, + }, + }; + }, + async assert({store, slot}) { + const errors: AssertionResult[] = []; + + if (store[slot].executionHead.hash !== store[slot].consensusHead.executionPayload.blockHash) { + errors.push( + `Execution head does not match consensus head. Expected: ${store[slot].consensusHead.executionPayload.blockHash}, got: ${store[slot].executionHead.hash}` + ); + } + + return errors; + }, + }; +} diff --git a/packages/cli/test/utils/simulation/constants.ts b/packages/cli/test/utils/simulation/constants.ts index 1d3c0f0f2c2b..b248f5109ffa 100644 --- a/packages/cli/test/utils/simulation/constants.ts +++ b/packages/cli/test/utils/simulation/constants.ts @@ -23,3 +23,6 @@ export const LODESTAR_BINARY_PATH = `${__dirname}/../../../bin/lodestar.js`; export const MOCK_ETH1_GENESIS_HASH = "0xfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfb"; export const SHARED_JWT_SECRET = "0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; export const SHARED_VALIDATOR_PASSWORD = "passwrod"; +export const EL_GENESIS_SECRET_KEY = "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"; +export const EL_GENESIS_PASSWORD = "12345678"; +export const EL_GENESIS_ACCOUNT = "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"; diff --git a/packages/cli/test/utils/simulation/execution_clients/geth.ts b/packages/cli/test/utils/simulation/execution_clients/geth.ts index 2ed9a2a91e1c..4c1b8196ab17 100644 --- a/packages/cli/test/utils/simulation/execution_clients/geth.ts +++ b/packages/cli/test/utils/simulation/execution_clients/geth.ts @@ -3,16 +3,18 @@ import {writeFile} from "node:fs/promises"; import path from "node:path"; import got from "got"; import {ZERO_HASH} from "@lodestar/state-transition"; -import {SHARED_JWT_SECRET, SIM_ENV_NETWORK_ID} from "../constants.js"; +import { + EL_GENESIS_ACCOUNT, + EL_GENESIS_PASSWORD, + EL_GENESIS_SECRET_KEY, + SHARED_JWT_SECRET, + SIM_ENV_NETWORK_ID, +} from "../constants.js"; import {Eth1ProviderWithAdmin} from "../Eth1ProviderWithAdmin.js"; import {ExecutionClient, ExecutionNodeGenerator, ExecutionStartMode, JobOptions, RunnerType} from "../interfaces.js"; import {getNodeMountedPaths} from "../utils/paths.js"; import {getNodePorts} from "../utils/ports.js"; -const SECRET_KEY = "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"; -const PASSWORD = "12345678"; -const GENESIS_ACCOUNT = "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"; - export const generateGethNode: ExecutionNodeGenerator = (opts, runner) => { if (!process.env.GETH_BINARY_DIR && !process.env.GETH_DOCKER_IMAGE) { throw new Error("GETH_BINARY_DIR or GETH_DOCKER_IMAGE must be provided"); @@ -74,8 +76,8 @@ export const generateGethNode: ExecutionNodeGenerator = (o } : undefined, bootstrap: async () => { - await writeFile(skPath, SECRET_KEY); - await writeFile(passwordPath, PASSWORD); + await writeFile(skPath, EL_GENESIS_SECRET_KEY); + await writeFile(passwordPath, EL_GENESIS_PASSWORD); }, cli: { command: binaryPath, @@ -132,7 +134,7 @@ export const generateGethNode: ExecutionNodeGenerator = (o rootDirMounted, "--allow-insecure-unlock", "--unlock", - GENESIS_ACCOUNT, + EL_GENESIS_ACCOUNT, "--password", passwordPathMounted, "--syncmode", @@ -142,7 +144,7 @@ export const generateGethNode: ExecutionNodeGenerator = (o // Logging verbosity: 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail "--verbosity", "5", - ...(mining ? ["--mine", "--miner.etherbase", GENESIS_ACCOUNT] : []), + ...(mining ? ["--mine", "--miner.etherbase", EL_GENESIS_ACCOUNT] : []), ...(mode == ExecutionStartMode.PreMerge ? ["--nodiscover"] : []), ...clientOptions, ],