feat: add send-bundle e2e test case with workaround to support traces… (#771)

* 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
This commit is contained in:
jonesho
2025-04-10 18:30:12 +08:00
committed by GitHub
parent d9d9242352
commit 31afe41f77
19 changed files with 308 additions and 234 deletions

View File

@@ -23,7 +23,6 @@ repositories {
maven {
url "https://artifacts.consensys.net/public/linea-besu/maven/"
content {
includeGroupAndSubgroups('io.consensys')
includeGroupAndSubgroups('org.hyperledger')
}
}

View File

@@ -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:

View File

@@ -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" ]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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<T>(
callback: () => Promise<T>,
stopRetry: (a: T) => boolean,
pollingIntervalMs: number = 500,
timeoutMs: number = 2 * 60 * 1000,
): Promise<T | null> {
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<boolean> {
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<boolean> {
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<string> {
export async function getRawTransactionHex(txRequest: TransactionRequest, signer: Wallet): Promise<string> {
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<string> {
const signature = await getRawTransactionHex(txRequest, signer);
return ethers.keccak256(signature);
}
@@ -225,57 +340,18 @@ export async function waitForEvents<
>(
contract: TContract,
eventFilter: TypedDeferredTopicFilter<TEvent>,
pollingInterval: number = 500,
pollingIntervalMs: number = 500,
fromBlock?: BlockTag,
toBlock?: BlockTag,
criteria?: (events: TypedEventLog<TEvent>[]) => Promise<TypedEventLog<TEvent>[]>,
): Promise<TypedEventLog<TEvent>[]> {
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<boolean> {
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<boolean> {
return pollForContractMethodReturnValue(method, targetReturnValue, (a, b) => a >= b, pollingInterval, timeout);
return (
(await awaitUntil(
async () => await getEvents(contract, eventFilter, fromBlock, toBlock, criteria),
(a: TypedEventLog<TEvent>[]) => 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<string> {
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,

View File

@@ -4,6 +4,7 @@ import { Logger } from "winston";
declare global {
var stopL2TrafficGeneration: () => void;
var shouldSkipBundleTests: boolean;
var logger: Logger;
}

View File

@@ -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<void> => {
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()}`);
}

View File

@@ -1,3 +1,4 @@
import { createTestLogger } from "../logger";
global.logger = createTestLogger();
global.shouldSkipBundleTests = process.env.SHOULD_SKIP_BUNDLE_TESTS === "true";

View File

@@ -49,7 +49,6 @@ const config: Config = {
L2_CHAIN_ID,
),
dummyContractAddress: "",
opcodeTestContractAddress: "",
},
};

View File

@@ -50,8 +50,6 @@ const config: Config = {
shomeiFrontendEndpoint: SHOMEI_FRONTEND_ENDPOINT,
sequencerEndpoint: SEQUENCER_ENDPOINT,
transactionExclusionEndpoint: TRANSACTION_EXCLUSION_ENDPOINT,
// Nonce 11
opcodeTestContractAddress: "0xFCc2155b495B6Bf6701eb322D3a97b7817898306",
},
};

View File

@@ -48,7 +48,6 @@ const config: Config = {
L2_CHAIN_ID,
),
dummyContractAddress: "",
opcodeTestContractAddress: "",
},
};

View File

@@ -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;
}

View File

@@ -24,7 +24,6 @@ export type BaseL2Config = BaseConfig & {
shomeiFrontendEndpoint?: URL;
sequencerEndpoint?: URL;
transactionExclusionEndpoint?: URL;
opcodeTestContractAddress: string;
};
export type LocalL2Config = BaseL2Config & {

View File

@@ -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<ContractTransactionReceipt | null> => {
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<bigint> => {
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 = <number>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 = <number>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);
});

139
e2e/src/send-bundle.spec.ts Normal file
View File

@@ -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,
);
});