From 31afe41f77fa9e1b10a55e6d7579843e6f75c55e Mon Sep 17 00:00:00 2001 From: jonesho <81145364+jonesho@users.noreply.github.com> Date: Thu, 10 Apr 2025 18:30:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20send-bundle=20e2e=20test=20case?= =?UTF-8?q?=20with=20workaround=20to=20support=20traces=E2=80=A6=20(#771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add send-bundle e2e test case with workaround to support traces-v1 sequencer * feat: linting and removed io.consensys in maven gradle * feat: RPC besu node to forward sendBundle to sequencer in e2e * feat: update besu nodes to devnet-9d6e914 and coordinator version update * feat: update coordinator version and l2-node-besu plugins config * feat: remove gas-limit e2e tests as already moved to 2b block gas limit for all envs * feat: remove opcode test contract related variables * feat: update coordinator version * feat: update linea-besu-package version * feat: revise test cases based on review comment * feat: skip send bundle tests explicitly if traces-v1 * feat: always pass send bundle tests if traces-v1 * feat: remove unused helper function * feat: skip send bundle tests explicitly if traces-v1 * feat: update to use l2-node-besu log4j.xml in l2-node-besu * feat: use describe.skip instead of it.skip for skipping bundle tests --- ...m.kotlin-common-minimal-conventions.gradle | 1 - docker/compose-spec-l1-services.yml | 2 +- docker/compose-spec-l2-services.yml | 20 +- docker/compose-tracing-v1-ci-extension.yml | 2 + docker/compose-tracing-v1.yml | 2 + docker/compose-tracing-v2-ci-extension.yml | 4 + .../l2-node-besu/l2-node-besu-config.toml | 2 +- .../sequencer.config.toml | 4 +- e2e/src/common/utils.ts | 226 +++++++++++------- e2e/src/config/global.d.ts | 1 + e2e/src/config/jest/global-setup.ts | 14 +- e2e/src/config/jest/setup.ts | 1 + .../config/tests-config/environments/dev.ts | 1 - .../config/tests-config/environments/local.ts | 2 - .../tests-config/environments/sepolia.ts | 1 - e2e/src/config/tests-config/setup.ts | 15 -- e2e/src/config/tests-config/types.ts | 1 - e2e/src/gas-limit.spec.ts | 104 -------- e2e/src/send-bundle.spec.ts | 139 +++++++++++ 19 files changed, 308 insertions(+), 234 deletions(-) delete mode 100644 e2e/src/gas-limit.spec.ts create mode 100644 e2e/src/send-bundle.spec.ts diff --git a/buildSrc/src/main/groovy/net.consensys.zkevm.kotlin-common-minimal-conventions.gradle b/buildSrc/src/main/groovy/net.consensys.zkevm.kotlin-common-minimal-conventions.gradle index 2229dede..3099f6a6 100644 --- a/buildSrc/src/main/groovy/net.consensys.zkevm.kotlin-common-minimal-conventions.gradle +++ b/buildSrc/src/main/groovy/net.consensys.zkevm.kotlin-common-minimal-conventions.gradle @@ -23,7 +23,6 @@ repositories { maven { url "https://artifacts.consensys.net/public/linea-besu/maven/" content { - includeGroupAndSubgroups('io.consensys') includeGroupAndSubgroups('org.hyperledger') } } diff --git a/docker/compose-spec-l1-services.yml b/docker/compose-spec-l1-services.yml index 84278dcd..54f6f927 100644 --- a/docker/compose-spec-l1-services.yml +++ b/docker/compose-spec-l1-services.yml @@ -2,7 +2,7 @@ services: l1-el-node: container_name: l1-el-node hostname: l1-el-node - image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-02eb1e1} + image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-aa00a36} profiles: [ "l1", "debug", "external-to-monorepo" ] depends_on: l1-node-genesis-generator: diff --git a/docker/compose-spec-l2-services.yml b/docker/compose-spec-l2-services.yml index ce590d99..423f6614 100644 --- a/docker/compose-spec-l2-services.yml +++ b/docker/compose-spec-l2-services.yml @@ -5,7 +5,7 @@ services: sequencer: hostname: sequencer container_name: sequencer - image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-02eb1e1} + image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-aa00a36} profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo" ] ports: - "8545:8545" @@ -82,7 +82,7 @@ services: l2-node-besu: hostname: l2-node-besu container_name: l2-node-besu - image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-02eb1e1} + image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-aa00a36} profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo" ] depends_on: sequencer: @@ -162,7 +162,7 @@ services: traces-node-v2: hostname: traces-node-v2 container_name: traces-node-v2 - image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-02eb1e1} + image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-aa00a36} profiles: [ "l2", "l2-bc", "debug", "external-to-monorepo" ] depends_on: sequencer: @@ -226,7 +226,7 @@ services: prover-v3: # prover compatible with the traces from zkbesu container_name: prover-v3 hostname: prover-v3 - image: consensys/linea-prover:${PROVER_TAG:-cd7228e} + image: consensys/linea-prover:${PROVER_TAG:-811743b} platform: linux/amd64 # to avoid spinning up on CI for now profiles: [ "l2" ] @@ -247,7 +247,7 @@ services: postman: container_name: postman hostname: postman - image: consensys/linea-postman:${POSTMAN_TAG:-cd7228e} + image: consensys/linea-postman:${POSTMAN_TAG:-811743b} profiles: [ "l2", "debug" ] platform: linux/amd64 restart: on-failure @@ -266,7 +266,7 @@ services: traces-api: hostname: traces-api container_name: traces-api - image: consensys/linea-traces-api-facade:${TRACES_API_TAG:-cd7228e} + image: consensys/linea-traces-api-facade:${TRACES_API_TAG:-811743b} profiles: [ "l2", "debug" ] restart: on-failure depends_on: @@ -287,7 +287,7 @@ services: coordinator: hostname: coordinator container_name: coordinator - image: consensys/linea-coordinator:${COORDINATOR_TAG:-29db47d} + image: consensys/linea-coordinator:${COORDINATOR_TAG:-811743b} platform: linux/amd64 profiles: [ "l2", "debug" ] depends_on: @@ -367,7 +367,7 @@ services: - l1network zkbesu-shomei: - image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-02eb1e1} + image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-aa00a36} hostname: zkbesu-shomei container_name: zkbesu-shomei profiles: [ "l2", "l2-bc", "external-to-monorepo" ] @@ -490,7 +490,7 @@ services: transaction-exclusion-api: hostname: transaction-exclusion-api container_name: transaction-exclusion-api - image: consensys/linea-transaction-exclusion-api:${TRANSACTION_EXCLUSION_API_TAG:-cd7228e} + image: consensys/linea-transaction-exclusion-api:${TRANSACTION_EXCLUSION_API_TAG:-811743b} profiles: [ "l2", "debug" ] restart: on-failure depends_on: @@ -583,7 +583,7 @@ services: ipv4_address: 10.10.10.205 zkbesu-shomei-sr: - image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-02eb1e1} + image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-devnet-aa00a36} hostname: zkbesu-shomei-sr container_name: zkbesu-shomei-sr profiles: [ "external-to-monorepo", "staterecovery" ] diff --git a/docker/compose-tracing-v1-ci-extension.yml b/docker/compose-tracing-v1-ci-extension.yml index 14ab4972..7b815641 100644 --- a/docker/compose-tracing-v1-ci-extension.yml +++ b/docker/compose-tracing-v1-ci-extension.yml @@ -12,6 +12,8 @@ services: file: compose-spec-l2-services.yml service: l2-node-besu image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-mainnet-402ebda} + environment: + BESU_PLUGINS: "LineaEstimateGasEndpointPlugin,LineaL1FinalizationTagUpdaterPlugin,LineaExtraDataPlugin,LineaTransactionPoolValidatorPlugin" volumes: - ../config/common/traces-limits-besu-v1.toml:/var/lib/besu/traces-limits.toml:ro diff --git a/docker/compose-tracing-v1.yml b/docker/compose-tracing-v1.yml index 434e989c..18e3062f 100644 --- a/docker/compose-tracing-v1.yml +++ b/docker/compose-tracing-v1.yml @@ -40,6 +40,8 @@ services: file: compose-spec-l2-services.yml service: sequencer image: consensys/linea-besu-package:${BESU_PACKAGE_TAG:-mainnet-402ebda} + environment: + BESU_PLUGINS: "LineaEstimateGasEndpointPlugin,LineaL1FinalizationTagUpdaterPlugin,LineaExtraDataPlugin,LineaTransactionPoolValidatorPlugin" volumes: - ../config/common/traces-limits-besu-v1.toml:/var/lib/besu/traces-limits.toml:ro diff --git a/docker/compose-tracing-v2-ci-extension.yml b/docker/compose-tracing-v2-ci-extension.yml index 3e406841..20b4a74b 100644 --- a/docker/compose-tracing-v2-ci-extension.yml +++ b/docker/compose-tracing-v2-ci-extension.yml @@ -11,6 +11,10 @@ services: extends: file: compose-spec-l2-services.yml service: l2-node-besu + environment: + BESU_PLUGIN_LINEA_BUNDLES_FORWARD_URLS: "http://sequencer:8545" + BESU_PLUGIN_LINEA_BUNDLES_FORWARD_RETRY_DELAY: 1000 + BESU_PLUGIN_LINEA_BUNDLES_FORWARD_TIMEOUT: 5000 volumes: - ../config/common/traces-limits-besu-v2.toml:/var/lib/besu/traces-limits.toml:ro diff --git a/docker/config/l2-node-besu/l2-node-besu-config.toml b/docker/config/l2-node-besu/l2-node-besu-config.toml index d5de6512..c1a62c41 100644 --- a/docker/config/l2-node-besu/l2-node-besu-config.toml +++ b/docker/config/l2-node-besu/l2-node-besu-config.toml @@ -36,7 +36,7 @@ metrics-port=9545 data-storage-format="BONSAI" # plugins -plugins=["LineaEstimateGasEndpointPlugin","LineaL1FinalizationTagUpdaterPlugin","LineaExtraDataPlugin", "LineaTransactionPoolValidatorPlugin"] +plugins=["LineaEstimateGasEndpointPlugin","LineaL1FinalizationTagUpdaterPlugin","LineaExtraDataPlugin","LineaTransactionPoolValidatorPlugin","LineaBundleEndpointsPlugin","ForwardBundlesPlugin"] plugin-linea-module-limit-file-path="/var/lib/besu/traces-limits.toml" plugin-linea-deny-list-path="/var/lib/besu/deny-list.txt" plugin-linea-l1l2-bridge-contract="0xe537D669CA013d86EBeF1D64e40fC74CADC91987" diff --git a/docker/config/linea-besu-sequencer/sequencer.config.toml b/docker/config/linea-besu-sequencer/sequencer.config.toml index 7bed8d46..0cec2075 100644 --- a/docker/config/linea-besu-sequencer/sequencer.config.toml +++ b/docker/config/linea-besu-sequencer/sequencer.config.toml @@ -28,7 +28,7 @@ rpc-http-max-active-connections=20000 rpc-ws-enabled=true rpc-ws-host="0.0.0.0" rpc-ws-port=8546 -rpc-ws-api=["ADMIN","DEBUG","NET","ETH","CLIQUE","MINER","WEB3","TRACE"] +rpc-ws-api=["ADMIN","DEBUG","NET","ETH","CLIQUE","MINER","WEB3","TRACE","LINEA"] rpc-ws-max-active-connections=200 # graphql @@ -47,7 +47,7 @@ api-gas-and-priority-fee-upper-bound-coefficient=300 poa-block-txs-selection-max-time=1000 # plugins -plugins=["LineaEstimateGasEndpointPlugin","LineaL1FinalizationTagUpdaterPlugin","LineaExtraDataPlugin","LineaTransactionPoolValidatorPlugin"] +plugins=["LineaEstimateGasEndpointPlugin","LineaL1FinalizationTagUpdaterPlugin","LineaExtraDataPlugin","LineaTransactionPoolValidatorPlugin","LineaBundleEndpointsPlugin","LineaSetExtraDataEndpointPlugin","LineaTransactionSelectorPlugin"] plugin-linea-module-limit-file-path="/var/lib/besu/traces-limits.toml" plugin-linea-deny-list-path="/var/lib/besu/deny-list.txt" plugin-linea-estimate-gas-compatibility-mode-enabled=false diff --git a/e2e/src/common/utils.ts b/e2e/src/common/utils.ts index 4cc633a4..5d119638 100644 --- a/e2e/src/common/utils.ts +++ b/e2e/src/common/utils.ts @@ -4,15 +4,10 @@ import { AbstractSigner, BaseContract, BlockTag, TransactionReceipt, Transaction import path from "path"; import { exec } from "child_process"; import { L2MessageServiceV1 as L2MessageService, TokenBridgeV1_1 as TokenBridge, LineaRollupV6 } from "../typechain"; -import { - PayableOverrides, - TypedContractEvent, - TypedDeferredTopicFilter, - TypedEventLog, - TypedContractMethod, -} from "../typechain/common"; +import { PayableOverrides, TypedContractEvent, TypedDeferredTopicFilter, TypedEventLog } from "../typechain/common"; import { MessageEvent, SendMessageArgs } from "./types"; import { createTestLogger } from "../config/logger"; +import { randomUUID, randomInt } from "crypto"; const logger = createTestLogger(); @@ -57,6 +52,64 @@ export const encodeData = (types: string[], values: unknown[], packed?: boolean) return ethers.AbiCoder.defaultAbiCoder().encode(types, values); }; +export async function isSendBundleMethodNotFound(rpcEndpoint: URL, targetBlockNumber = "0xffff") { + const lineaSendBundleClient = new LineaBundleClient(rpcEndpoint); + try { + await lineaSendBundleClient.lineaSendBundle([], generateRandomUUIDv4(), targetBlockNumber); + } catch (err) { + if (err instanceof Error) { + if (err.message === "Method not found") { + // Bundle request doesn't support in traces-v1 besu nodes + return true; + } + } + } + return false; +} + +export function generateRandomInt(max = 1000): number { + return randomInt(max); +} + +export function generateRandomUUIDv4(): string { + return randomUUID(); +} + +async function awaitUntil( + callback: () => Promise, + stopRetry: (a: T) => boolean, + pollingIntervalMs: number = 500, + timeoutMs: number = 2 * 60 * 1000, +): Promise { + let isExceedTimeOut = false; + setTimeout(() => { + isExceedTimeOut = true; + }, timeoutMs); + + while (!isExceedTimeOut) { + const result = await callback(); + if (stopRetry(result)) return result; + await wait(pollingIntervalMs); + } + return null; +} + +export async function pollForBlockNumber( + provider: ethers.JsonRpcProvider, + expectedBlockNumber: number, + pollingIntervalMs: number = 500, + timeoutMs: number = 2 * 60 * 1000, +): Promise { + return ( + (await awaitUntil( + async () => await provider.getBlockNumber(), + (a: number) => a >= expectedBlockNumber, + pollingIntervalMs, + timeoutMs, + )) != null + ); +} + export class RollupGetZkEVMBlockNumberClient { private endpoint: URL; private request = { @@ -65,7 +118,7 @@ export class RollupGetZkEVMBlockNumberClient { jsonrpc: "2.0", method: "rollup_getZkEVMBlockNumber", params: [], - id: 1, + id: generateRandomInt(), }), }; @@ -107,7 +160,7 @@ export class LineaEstimateGasClient { value, }, ], - id: 1, + id: generateRandomInt(), }), }; const response = await fetch(this.endpoint, request); @@ -121,6 +174,64 @@ export class LineaEstimateGasClient { } } +export class LineaBundleClient { + private endpoint: URL; + + public constructor(endpoint: URL) { + this.endpoint = endpoint; + } + + public async lineaSendBundle( + txs: string[], + replacementUUID: string, + blockNumber: string, + ): Promise<{ bundleHash: string }> { + const request = { + method: "post", + body: JSON.stringify({ + jsonrpc: "2.0", + method: "linea_sendBundle", + params: [ + { + txs, + replacementUUID, + blockNumber, + }, + ], + id: generateRandomInt(), + }), + }; + const response = await fetch(this.endpoint, request); + const responseJson = await response.json(); + if (responseJson.error?.code === -32601 && responseJson.error?.message === "Method not found") { + throw Error("Method not found"); + } + assert("result" in responseJson); + return { + bundleHash: responseJson.result.bundleHash, + }; + } + + public async lineaCancelBundle(replacementUUID: string): Promise { + const request = { + method: "post", + body: JSON.stringify({ + jsonrpc: "2.0", + method: "linea_cancelBundle", + params: [replacementUUID], + id: generateRandomInt(), + }), + }; + const response = await fetch(this.endpoint, request); + const responseJson = await response.json(); + if (responseJson.error?.code === -32601 && responseJson.error?.message === "Method not found") { + throw Error("Method not found"); + } + assert("result" in responseJson); + return responseJson.result; + } +} + export class TransactionExclusionClient { private endpoint: URL; @@ -136,7 +247,7 @@ export class TransactionExclusionClient { jsonrpc: "2.0", method: "linea_getTransactionExclusionStatusV1", params: [txHash], - id: 1, + id: generateRandomInt(), }), }; const response = await fetch(this.endpoint, request); @@ -172,7 +283,7 @@ export class TransactionExclusionClient { jsonrpc: "2.0", method: "linea_saveRejectedTransactionV1", params: params, - id: 1, + id: generateRandomInt(), }), }; const response = await fetch(this.endpoint, request); @@ -180,9 +291,13 @@ export class TransactionExclusionClient { } } -export async function getTransactionHash(txRequest: TransactionRequest, signer: Wallet): Promise { +export async function getRawTransactionHex(txRequest: TransactionRequest, signer: Wallet): Promise { const rawTransaction = await signer.populateTransaction(txRequest); - const signature = await signer.signTransaction(rawTransaction); + return await signer.signTransaction(rawTransaction); +} + +export async function getTransactionHash(txRequest: TransactionRequest, signer: Wallet): Promise { + const signature = await getRawTransactionHex(txRequest, signer); return ethers.keccak256(signature); } @@ -225,57 +340,18 @@ export async function waitForEvents< >( contract: TContract, eventFilter: TypedDeferredTopicFilter, - pollingInterval: number = 500, + pollingIntervalMs: number = 500, fromBlock?: BlockTag, toBlock?: BlockTag, criteria?: (events: TypedEventLog[]) => Promise[]>, ): Promise[]> { - let events = await getEvents(contract, eventFilter, fromBlock, toBlock, criteria); - - while (events.length === 0) { - events = await getEvents(contract, eventFilter, fromBlock, toBlock, criteria); - await wait(pollingInterval); - } - - return events; -} - -// Currently only handle simple single return types - uint256 | bytesX | string | bool -export async function pollForContractMethodReturnValue< - ExpectedReturnType extends bigint | string | boolean, - R extends [ExpectedReturnType], ->( - method: TypedContractMethod<[], R, "view">, - expectedReturnValue: ExpectedReturnType, - compareFunction: (a: ExpectedReturnType, b: ExpectedReturnType) => boolean = (a, b) => a === b, - pollingInterval: number = 500, - timeout: number = 2 * 60 * 1000, -): Promise { - let isExceedTimeOut = false; - setTimeout(() => { - isExceedTimeOut = true; - }, timeout); - - while (!isExceedTimeOut) { - const returnValue = await method(); - if (compareFunction(returnValue, expectedReturnValue)) return true; - await wait(pollingInterval); - } - - return false; -} - -// Currently only handle single uint256 return type -export async function pollForContractMethodReturnValueExceedTarget< - ExpectedReturnType extends bigint, - R extends [ExpectedReturnType], ->( - method: TypedContractMethod<[], R, "view">, - targetReturnValue: ExpectedReturnType, - pollingInterval: number = 500, - timeout: number = 2 * 60 * 1000, -): Promise { - return pollForContractMethodReturnValue(method, targetReturnValue, (a, b) => a >= b, pollingInterval, timeout); + return ( + (await awaitUntil( + async () => await getEvents(contract, eventFilter, fromBlock, toBlock, criteria), + (a: TypedEventLog[]) => a.length > 0, + pollingIntervalMs, + )) ?? [] + ); } export function getFiles(directory: string, fileRegex: RegExp[]): string[] { @@ -284,36 +360,6 @@ export function getFiles(directory: string, fileRegex: RegExp[]): string[] { return filteredFiles.map((file) => fs.readFileSync(path.join(directory, file.name), "utf-8")); } -export async function waitForFile( - directory: string, - regex: RegExp, - pollingInterval: number, - timeout: number, - criteria?: (fileName: string) => boolean, -): Promise { - const endTime = Date.now() + timeout; - - while (Date.now() < endTime) { - try { - const files = fs.readdirSync(directory); - - for (const file of files) { - if (regex.test(file) && (!criteria || criteria(file))) { - const filePath = path.join(directory, file); - const content = fs.readFileSync(filePath, "utf-8"); - return content; - } - } - } catch (err) { - throw new Error(`Error reading directory: ${(err as Error).message}`); - } - - await new Promise((resolve) => setTimeout(resolve, pollingInterval)); - } - - throw new Error("File check timed out"); -} - export async function sendTransactionsToGenerateTrafficWithInterval( signer: AbstractSigner, pollingInterval: number = 1_000, diff --git a/e2e/src/config/global.d.ts b/e2e/src/config/global.d.ts index dd0b3bd5..322a5e09 100644 --- a/e2e/src/config/global.d.ts +++ b/e2e/src/config/global.d.ts @@ -4,6 +4,7 @@ import { Logger } from "winston"; declare global { var stopL2TrafficGeneration: () => void; + var shouldSkipBundleTests: boolean; var logger: Logger; } diff --git a/e2e/src/config/jest/global-setup.ts b/e2e/src/config/jest/global-setup.ts index fb5b6b53..4c6cbe00 100644 --- a/e2e/src/config/jest/global-setup.ts +++ b/e2e/src/config/jest/global-setup.ts @@ -2,8 +2,12 @@ import { ethers } from "ethers"; import { config } from "../tests-config"; import { deployContract } from "../../common/deployments"; -import { DummyContract__factory, TestContract__factory, OpcodeTestContract__factory } from "../../typechain"; -import { etherToWei, sendTransactionsToGenerateTrafficWithInterval } from "../../common/utils"; +import { DummyContract__factory, TestContract__factory } from "../../typechain"; +import { + etherToWei, + isSendBundleMethodNotFound, + sendTransactionsToGenerateTrafficWithInterval, +} from "../../common/utils"; import { EMPTY_CONTRACT_CODE } from "../../common/constants"; import { createTestLogger } from "../logger"; @@ -18,6 +22,8 @@ export default async (): Promise => { await configureOnceOffPrerequisities(); } + process.env.SHOULD_SKIP_BUNDLE_TESTS = (await isSendBundleMethodNotFound(config.getL2BesuNodeEndpoint()!)).toString(); + logger.info("Generating L2 traffic..."); const pollingAccount = await config.getL2AccountManager().generateAccount(etherToWei("200")); const stopPolling = await sendTransactionsToGenerateTrafficWithInterval(pollingAccount, 2_000); @@ -36,11 +42,10 @@ async function configureOnceOffPrerequisities() { const to = "0x8D97689C9818892B700e27F316cc3E41e17fBeb9"; const calldata = "0x"; - const [dummyContract, l2DummyContract, l2TestContract, opcodeTestContract] = await Promise.all([ + const [dummyContract, l2DummyContract, l2TestContract] = await Promise.all([ deployContract(new DummyContract__factory(), account, [{ nonce: l1AccountNonce }]), deployContract(new DummyContract__factory(), l2Account, [{ nonce: l2AccountNonce }]), deployContract(new TestContract__factory(), l2Account, [{ nonce: l2AccountNonce + 1 }]), - deployContract(new OpcodeTestContract__factory(), l2Account, [{ nonce: l2AccountNonce + 2 }]), // Send ETH to the LineaRollup contract ( @@ -55,5 +60,4 @@ async function configureOnceOffPrerequisities() { logger.info(`L1 Dummy contract deployed. address=${await dummyContract.getAddress()}`); logger.info(`L2 Dummy contract deployed. address=${await l2DummyContract.getAddress()}`); logger.info(`L2 Test contract deployed. address=${await l2TestContract.getAddress()}`); - logger.info(`L2 OpcodeTest contract deployed. address=${await opcodeTestContract.getAddress()}`); } diff --git a/e2e/src/config/jest/setup.ts b/e2e/src/config/jest/setup.ts index 9e15b56f..42715209 100644 --- a/e2e/src/config/jest/setup.ts +++ b/e2e/src/config/jest/setup.ts @@ -1,3 +1,4 @@ import { createTestLogger } from "../logger"; global.logger = createTestLogger(); +global.shouldSkipBundleTests = process.env.SHOULD_SKIP_BUNDLE_TESTS === "true"; diff --git a/e2e/src/config/tests-config/environments/dev.ts b/e2e/src/config/tests-config/environments/dev.ts index 88bac967..ee1b3209 100644 --- a/e2e/src/config/tests-config/environments/dev.ts +++ b/e2e/src/config/tests-config/environments/dev.ts @@ -49,7 +49,6 @@ const config: Config = { L2_CHAIN_ID, ), dummyContractAddress: "", - opcodeTestContractAddress: "", }, }; diff --git a/e2e/src/config/tests-config/environments/local.ts b/e2e/src/config/tests-config/environments/local.ts index 9a6c87d9..8bc2532c 100644 --- a/e2e/src/config/tests-config/environments/local.ts +++ b/e2e/src/config/tests-config/environments/local.ts @@ -50,8 +50,6 @@ const config: Config = { shomeiFrontendEndpoint: SHOMEI_FRONTEND_ENDPOINT, sequencerEndpoint: SEQUENCER_ENDPOINT, transactionExclusionEndpoint: TRANSACTION_EXCLUSION_ENDPOINT, - // Nonce 11 - opcodeTestContractAddress: "0xFCc2155b495B6Bf6701eb322D3a97b7817898306", }, }; diff --git a/e2e/src/config/tests-config/environments/sepolia.ts b/e2e/src/config/tests-config/environments/sepolia.ts index 588bf1dc..352134bf 100644 --- a/e2e/src/config/tests-config/environments/sepolia.ts +++ b/e2e/src/config/tests-config/environments/sepolia.ts @@ -48,7 +48,6 @@ const config: Config = { L2_CHAIN_ID, ), dummyContractAddress: "", - opcodeTestContractAddress: "", }, }; diff --git a/e2e/src/config/tests-config/setup.ts b/e2e/src/config/tests-config/setup.ts index 9ceccb2b..a569489a 100644 --- a/e2e/src/config/tests-config/setup.ts +++ b/e2e/src/config/tests-config/setup.ts @@ -9,8 +9,6 @@ import { L2MessageServiceV1__factory as L2MessageService__factory, LineaRollupV6, LineaRollupV6__factory, - OpcodeTestContract, - OpcodeTestContract__factory, ProxyAdmin, ProxyAdmin__factory, TestContract, @@ -229,19 +227,6 @@ export default class TestSetup { } } - public getOpcodeTestContract(signer?: Wallet): OpcodeTestContract { - const opcodeTestContract = OpcodeTestContract__factory.connect( - this.config.L2.opcodeTestContractAddress, - this.getL2Provider(), - ); - - if (signer) { - return opcodeTestContract.connect(signer); - } - - return opcodeTestContract; - } - public getL1AccountManager(): AccountManager { return this.config.L1.accountManager; } diff --git a/e2e/src/config/tests-config/types.ts b/e2e/src/config/tests-config/types.ts index 51d781f9..460636fc 100644 --- a/e2e/src/config/tests-config/types.ts +++ b/e2e/src/config/tests-config/types.ts @@ -24,7 +24,6 @@ export type BaseL2Config = BaseConfig & { shomeiFrontendEndpoint?: URL; sequencerEndpoint?: URL; transactionExclusionEndpoint?: URL; - opcodeTestContractAddress: string; }; export type LocalL2Config = BaseL2Config & { diff --git a/e2e/src/gas-limit.spec.ts b/e2e/src/gas-limit.spec.ts deleted file mode 100644 index 37db2db6..00000000 --- a/e2e/src/gas-limit.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { pollForContractMethodReturnValueExceedTarget, wait } from "./common/utils"; -import { config } from "./config/tests-config"; -import { ContractTransactionReceipt, Wallet } from "ethers"; - -const l2AccountManager = config.getL2AccountManager(); -const l2Provider = config.getL2Provider(); - -describe("Gas limit test suite", () => { - const setGasLimit = async (account: Wallet): Promise => { - logger.debug(`setGasLimit called with account=${account.address}`); - - const opcodeTestContract = config.getOpcodeTestContract(account); - const nonce = await l2Provider.getTransactionCount(account.address, "pending"); - logger.debug(`Fetched nonce. nonce=${nonce} account=${account.address}`); - - const { maxPriorityFeePerGas, maxFeePerGas } = await l2Provider.getFeeData(); - logger.debug(`Fetched fee data. maxPriorityFeePerGas=${maxPriorityFeePerGas} maxFeePerGas=${maxFeePerGas}`); - - const tx = await opcodeTestContract.connect(account).setGasLimit({ - nonce: nonce, - maxPriorityFeePerGas: maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas, - }); - logger.debug(`setGasLimit transaction sent. transactionHash=${tx.hash}`); - - const receipt = await tx.wait(); - logger.debug(`Transaction receipt received. transactionHash=${tx.hash} status=${receipt?.status}`); - - return receipt; - }; - - const getGasLimit = async (): Promise => { - const opcodeTestContract = config.getOpcodeTestContract(); - const gasLimit = await opcodeTestContract.getGasLimit(); - - logger.debug(`Current gas limit retrieved. gasLimit=${gasLimit}`); - - return gasLimit; - }; - - it.concurrent("Should successfully invoke OpcodeTestContract.setGasLimit()", async () => { - const account = await l2AccountManager.generateAccount(); - const receipt = await setGasLimit(account); - expect(receipt?.status).toEqual(1); - }); - - it.concurrent("Should successfully finalize OpcodeTestContract.setGasLimit()", async () => { - const account = await l2AccountManager.generateAccount(); - const lineaRollupV6 = config.getLineaRollupContract(); - - const txReceipt = await setGasLimit(account); - expect(txReceipt?.status).toEqual(1); - // Ok to type assertion here, because txReceipt won't be null if it passed above assertion. - const txBlockNumber = txReceipt?.blockNumber; - - logger.debug(`Waiting for block to be finalized... blockNumber=${txBlockNumber}`); - - const isBlockFinalized = await pollForContractMethodReturnValueExceedTarget( - lineaRollupV6.currentL2BlockNumber, - BigInt(txBlockNumber), - ); - - logger.debug(`Block finalized. blockNumber=${txBlockNumber}`); - - expect(isBlockFinalized).toEqual(true); - }); - - // One-time test to test block gas limit increase from 61M -> 2B - it.skip("Should successfully reach the target gas limit, and finalize the corresponding transaction", async () => { - const targetBlockGasLimit = 2_000_000_000n; - let isTargetBlockGasLimitReached = false; - let blockNumberToCheckFinalization = 0; - const account = await l2AccountManager.generateAccount(); - const lineaRollupV6 = config.getLineaRollupContract(); - - logger.debug(`Target block gasLimit=${targetBlockGasLimit}`); - - while (!isTargetBlockGasLimitReached) { - const txReceipt = await setGasLimit(account); - expect(txReceipt?.status).toEqual(1); - const blockGasLimit = await getGasLimit(); - - if (blockGasLimit === targetBlockGasLimit) { - isTargetBlockGasLimitReached = true; - // Ok to type assertion here, because txReceipt won't be null if it passed above assertion. - blockNumberToCheckFinalization = txReceipt?.blockNumber; - } - await wait(1000); - } - - logger.debug(`Waiting for block to be finalized... blockNumber=${blockNumberToCheckFinalization}`); - - const isBlockFinalized = await pollForContractMethodReturnValueExceedTarget( - lineaRollupV6.currentL2BlockNumber, - BigInt(blockNumberToCheckFinalization), - ); - - logger.debug(`Block finalized. blockNumber=${blockNumberToCheckFinalization}`); - - expect(isBlockFinalized).toEqual(true); - // Timeout of 6 hrs - }, 21_600_000); -}); diff --git a/e2e/src/send-bundle.spec.ts b/e2e/src/send-bundle.spec.ts new file mode 100644 index 00000000..b06dfdb3 --- /dev/null +++ b/e2e/src/send-bundle.spec.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "@jest/globals"; +import { config } from "./config/tests-config"; +import { + generateRandomUUIDv4, + getRawTransactionHex, + getTransactionHash, + getWallet, + LineaBundleClient, + pollForBlockNumber, +} from "./common/utils"; +import { ethers, TransactionRequest } from "ethers"; + +const describeIf = shouldSkipBundleTests ? describe.skip : describe; +if (shouldSkipBundleTests) { + logger.info("Skip bundle tests due to tracing-v1 besu nodes"); +} + +describeIf("Send bundle test suite", () => { + const l2AccountManager = config.getL2AccountManager(); + const lineaCancelBundleClient = new LineaBundleClient(config.getSequencerEndpoint()!); + const lineaSendBundleClient = new LineaBundleClient(config.getL2BesuNodeEndpoint()!); + + it.concurrent( + "Call sendBundle to RPC node and the bundled txs should get included", + async () => { + const senderAccount = await l2AccountManager.generateAccount(); + const senderWallet = getWallet(senderAccount.privateKey, config.getL2BesuNodeProvider()!); + const recipientAccount = await l2AccountManager.generateAccount(0n); + + let senderNonce = await senderAccount.getNonce(); + const txHashes: string[] = []; + const txs: string[] = []; + for (let i = 0; i < 3; i++) { + const txRequest: TransactionRequest = { + to: recipientAccount.address, + value: ethers.parseUnits("1000", "wei"), + maxPriorityFeePerGas: ethers.parseEther("0.000000001"), // 1 Gwei + maxFeePerGas: ethers.parseEther("0.00000001"), // 10 Gwei + nonce: senderNonce++, + }; + txs.push(await getRawTransactionHex(txRequest, senderWallet)); + txHashes.push(await getTransactionHash(txRequest, senderWallet)); + } + const targetBlockNumber = (await config.getL2Provider().getBlockNumber()) + 5; + const replacementUUID = generateRandomUUIDv4(); + + await lineaSendBundleClient.lineaSendBundle(txs, replacementUUID, "0x" + targetBlockNumber.toString(16)); + + const hasReachedTargeBlockNumber = await pollForBlockNumber(config.getL2Provider(), targetBlockNumber); + + expect(hasReachedTargeBlockNumber).toBeTruthy(); + for (const tx of txHashes) { + const receipt = await config.getL2Provider().getTransactionReceipt(tx); + expect(receipt?.status).toStrictEqual(1); + } + }, + 120_000, + ); + + it.concurrent( + "Call sendBundle to RPC node but the bundled txs should not get included as not all of them is valid", + async () => { + // 1500 wei should just be enough for the first ETH transfer tx, and the second and third would fail + const senderAccount = await l2AccountManager.generateAccount(ethers.parseUnits("1500", "wei")); + const senderWallet = getWallet(senderAccount.privateKey, config.getL2BesuNodeProvider()!); + const recipientAccount = await l2AccountManager.generateAccount(0n); + + let senderNonce = await senderAccount.getNonce(); + const txHashes: string[] = []; + const txs: string[] = []; + for (let i = 0; i < 3; i++) { + const txRequest: TransactionRequest = { + to: recipientAccount.address, + value: ethers.parseUnits("1000", "wei"), + maxPriorityFeePerGas: ethers.parseEther("0.000000001"), // 1 Gwei + maxFeePerGas: ethers.parseEther("0.00000001"), // 10 Gwei + nonce: senderNonce++, + }; + txs.push(await getRawTransactionHex(txRequest, senderWallet)); + txHashes.push(await getTransactionHash(txRequest, senderWallet)); + } + const targetBlockNumber = (await config.getL2Provider().getBlockNumber()) + 5; + const replacementUUID = generateRandomUUIDv4(); + + await lineaSendBundleClient.lineaSendBundle(txs, replacementUUID, "0x" + targetBlockNumber.toString(16)); + + const hasReachedTargeBlockNumber = await pollForBlockNumber(config.getL2Provider(), targetBlockNumber); + + expect(hasReachedTargeBlockNumber).toBeTruthy(); + // None of the bundled txs should be included as not all of them is valid + for (const tx of txHashes) { + const receipt = await config.getL2Provider().getTransactionReceipt(tx); + expect(receipt?.status).toBeUndefined(); + } + }, + 120_000, + ); + + it.concurrent( + "Call sendBundle to RPC node and then cancelBundle to sequencer and no bundled txs should get included", + async () => { + const senderAccount = await l2AccountManager.generateAccount(); + const senderWallet = getWallet(senderAccount.privateKey, config.getL2BesuNodeProvider()!); + const recipientAccount = await l2AccountManager.generateAccount(0n); + + let senderNonce = await senderAccount.getNonce(); + const txHashes: string[] = []; + const txs: string[] = []; + for (let i = 0; i < 3; i++) { + const txRequest: TransactionRequest = { + to: recipientAccount.address, + value: ethers.parseUnits("1000", "wei"), + maxPriorityFeePerGas: ethers.parseEther("0.000000001"), // 1 Gwei + maxFeePerGas: ethers.parseEther("0.00000001"), // 10 Gwei + nonce: senderNonce++, + }; + txs.push(await getRawTransactionHex(txRequest, senderWallet)); + txHashes.push(await getTransactionHash(txRequest, senderWallet)); + } + const targetBlockNumber = (await config.getL2Provider().getBlockNumber()) + 10; + const replacementUUID = generateRandomUUIDv4(); + + await lineaSendBundleClient.lineaSendBundle(txs, replacementUUID, "0x" + targetBlockNumber.toString(16)); + + await pollForBlockNumber(config.getL2Provider(), targetBlockNumber - 5); + const cancelled = await lineaCancelBundleClient.lineaCancelBundle(replacementUUID); + expect(cancelled).toBeTruthy(); + + const hasReachedTargeBlockNumber = await pollForBlockNumber(config.getL2Provider(), targetBlockNumber); + + expect(hasReachedTargeBlockNumber).toBeTruthy(); + for (const tx of txHashes) { + const receipt = await config.getL2Provider().getTransactionReceipt(tx); + expect(receipt?.status).toBeUndefined(); + } + }, + 120_000, + ); +});