Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion e2e/scripts/generateAbi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const ABI_INPUT_DIRS = [
];

const INCLUDE_FILES: string[] = [
"LineaRollupV6",
"LineaRollupV8",
"L2MessageServiceV1",
"TokenBridgeV1_1",
"ProxyAdmin",
Expand All @@ -22,6 +22,7 @@ const INCLUDE_FILES: string[] = [
"LineaSequencerUptimeFeed",
"OpcodeTester",
"Mimc",
"ForcedTransactionGateway",
];

async function main() {
Expand Down
4 changes: 2 additions & 2 deletions e2e/src/bridge-tokens.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { waitForEvents, getMessageSentEventFromLogs, estimateLineaGas } from "./
import { L2RpcEndpoint } from "./config/clients/l2-client";
import { getBridgedTokenContract } from "./config/contracts/contracts";
import { createTestContext } from "./config/setup";
import { L2MessageServiceV1Abi, LineaRollupV6Abi, TestERC20Abi, TokenBridgeV1_1Abi } from "./generated";
import { L2MessageServiceV1Abi, LineaRollupV8Abi, TestERC20Abi, TokenBridgeV1_1Abi } from "./generated";

const context = createTestContext();
const l1AccountManager = context.getL1AccountManager();
Expand Down Expand Up @@ -244,7 +244,7 @@ describe("Bridge ERC20 Tokens L1 -> L2 and L2 -> L1", () => {
logger.debug("Waiting for L1 MessageClaimed event.");

const [claimedEvent] = await waitForEvents(l1PublicClient, {
abi: LineaRollupV6Abi,
abi: LineaRollupV8Abi,
address: context.l1Contracts.lineaRollup(l1PublicClient).address,
eventName: "MessageClaimed",
args: {
Expand Down
7 changes: 7 additions & 0 deletions e2e/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { etherToWei, generateKeccak256 } from "@consensys/linea-shared-utils";
import path from "path";
import { zeroHash } from "viem";

export const ROLLING_HASH_UPDATED_EVENT_SIGNATURE =
Expand All @@ -16,6 +17,12 @@ export const MINIMUM_FEE_IN_WEI = etherToWei("0.0001");
export const DEPLOYER_ACCOUNT_INDEX = 0;
export const LIVENESS_ACCOUNT_INDEX = 1;

export const GENESIS_TIMESTAMP_FILE_PATH = path.resolve(
__dirname,
"../../..",
"docker/config/l2-genesis-initialization/fork-timestamp.txt",
);

export const PAUSE_ALL_ROLE = generateKeccak256(["string"], ["PAUSE_ALL_ROLE"], true);
export const UNPAUSE_ALL_ROLE = generateKeccak256(["string"], ["UNPAUSE_ALL_ROLE"], true);
export const PAUSE_L1_L2_ROLE = generateKeccak256(["string"], ["PAUSE_L1_L2_ROLE"], true);
Expand Down
203 changes: 203 additions & 0 deletions e2e/src/common/test-helpers/forced-transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { TestContext } from "e2e/src/config/setup";
import { readFileSync, existsSync } from "fs";
import {
type Address,
type Client,
type Hash,
type Hex,
encodeAbiParameters,
keccak256,
parseTransaction,
zeroHash,
} from "viem";
import { type PrivateKeyAccount } from "viem/accounts";

import { getLineaRollupContract } from "../../config/contracts/contracts";
import { createTestLogger } from "../../config/logger";
import { LineaRollupV8Abi } from "../../generated";
import { GENESIS_TIMESTAMP_FILE_PATH } from "../constants";
import { getRawTransactionHex, waitForEvents } from "../utils";

const logger = createTestLogger();

export function getDefaultLastFinalizedTimestamp() {
const filePath = GENESIS_TIMESTAMP_FILE_PATH;
if (!existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const timestamp = readFileSync(filePath, "utf-8");

if (!Number(timestamp)) {
throw new Error(`Invalid timestamp value in file: ${filePath}`);
}

return BigInt(timestamp);
}

export type LastFinalizedState = {
timestamp: bigint;
messageNumber: bigint;
messageRollingHash: Hex;
forcedTransactionNumber: bigint;
forcedTransactionRollingHash: Hex;
};

export type BuildForcedTransactionParams = {
l2Account: PrivateKeyAccount;
to: Address;
nonce: bigint;
gasLimit: bigint;
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
value?: bigint;
data?: Hex;
};

export type ForcedTransactionStruct = {
nonce: bigint;
maxPriorityFeePerGas: bigint;
maxFeePerGas: bigint;
gasLimit: bigint;
to: Address;
value: bigint;
input: Hex;
accessList: { contractAddress: Address; storageKeys: Hex[] }[];
yParity: number;
r: bigint;
s: bigint;
};

export function computeFinalizedStateHash(state: LastFinalizedState): Hex {
return keccak256(
encodeAbiParameters(
[{ type: "uint256" }, { type: "bytes32" }, { type: "uint256" }, { type: "bytes32" }, { type: "uint256" }],
[
state.messageNumber,
state.messageRollingHash,
state.forcedTransactionNumber,
state.forcedTransactionRollingHash,
state.timestamp,
],
),
);
}

export async function resolveLastFinalizedState(
lineaRollup: ReturnType<typeof getLineaRollupContract>,
l1PublicClient: Client,
genesisTimestamp: bigint,
): Promise<LastFinalizedState> {
const onChainStateHash: Hex = await lineaRollup.read.currentFinalizedState();

const defaultState: LastFinalizedState = {
timestamp: genesisTimestamp,
messageNumber: 0n,
messageRollingHash: zeroHash,
forcedTransactionNumber: 0n,
forcedTransactionRollingHash: zeroHash,
};

const defaultHash = computeFinalizedStateHash(defaultState);

if (onChainStateHash === defaultHash) {
logger.debug("Finalized state matches default genesis state.");
return defaultState;
}

logger.debug("Finalized state differs from default — reconstructing from on-chain events...");

const currentL2BlockNumber: bigint = await lineaRollup.read.currentL2BlockNumber();

const events = await waitForEvents(l1PublicClient, {
abi: LineaRollupV8Abi,
address: lineaRollup.address as Address,
eventName: "FinalizedStateUpdated",
fromBlock: 0n,
toBlock: "latest",
args: { blockNumber: currentL2BlockNumber },
pollingIntervalMs: 500,
timeoutMs: 5_000,
strict: true,
});

const latestEvent = events[events.length - 1];
const timestamp = latestEvent.args.timestamp;
const messageNumber = latestEvent.args.messageNumber;
const forcedTransactionNumber = latestEvent.args.forcedTransactionNumber;

const [messageRollingHash, forcedTransactionRollingHash] = await Promise.all([
lineaRollup.read.rollingHashes([messageNumber]),
lineaRollup.read.forcedTransactionRollingHashes([forcedTransactionNumber]),
]);

const reconstructed: LastFinalizedState = {
timestamp,
messageNumber,
messageRollingHash: messageRollingHash as Hex,
forcedTransactionNumber,
forcedTransactionRollingHash: forcedTransactionRollingHash as Hex,
};

const reconstructedHash = computeFinalizedStateHash(reconstructed);
if (reconstructedHash !== onChainStateHash) {
throw new Error(
`Reconstructed state hash (${reconstructedHash}) does not match on-chain hash (${onChainStateHash}).`,
);
}

logger.debug("Reconstructed finalized state validated successfully.");
return reconstructed;
}

export type BuildForcedTransactionResult = {
forcedTransaction: ForcedTransactionStruct;
l2TxHash: Hash;
};

export async function buildSignedForcedTransaction(
context: TestContext,
params: BuildForcedTransactionParams,
): Promise<BuildForcedTransactionResult> {
const { l2Account, to, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, value = 0n, data = "0x" } = params;

const client = context.l2PublicClient();

const txRequest = await client.prepareTransactionRequest({
type: "eip1559",
account: l2Account,
to,
value,
maxPriorityFeePerGas,
maxFeePerGas,
gas: gasLimit,
data,
nonce: Number(nonce),
accessList: [],
});

const signedTxHex = await getRawTransactionHex(client, txRequest);
const l2TxHash = keccak256(signedTxHex);

const parsed = parseTransaction(signedTxHex);

if (parsed.yParity === undefined || parsed.r === undefined || parsed.s === undefined) {
throw new Error("Parsed transaction is missing signature fields (yParity, r, s).");
}

return {
forcedTransaction: {
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
to,
value,
input: data as Hex,
accessList: [] as { contractAddress: Address; storageKeys: Hex[] }[],
yParity: parsed.yParity,
r: BigInt(parsed.r),
s: BigInt(parsed.s),
},
l2TxHash,
};
}
6 changes: 4 additions & 2 deletions e2e/src/config/contracts/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { type Abi, type Address, type Client, type Transport, type Chain, type A
import {
BridgedTokenAbi,
DummyContractAbi,
ForcedTransactionGatewayAbi,
L2MessageServiceV1Abi,
LineaRollupV6Abi,
LineaRollupV8Abi,
LineaSequencerUptimeFeedAbi,
OpcodeTesterAbi,
ProxyAdminAbi,
Expand All @@ -25,7 +26,7 @@ function createContractGetter<const TAbi extends Abi>(abi: TAbi) {
) => getContract({ abi, address, client });
}

export const getLineaRollupContract = createContractGetter(LineaRollupV6Abi);
export const getLineaRollupContract = createContractGetter(LineaRollupV8Abi);
export const getLineaRollupProxyAdminContract = createContractGetter(ProxyAdminAbi);
export const getTestERC20Contract = createContractGetter(TestERC20Abi);
export const getTokenBridgeContract = createContractGetter(TokenBridgeV1_1Abi);
Expand All @@ -36,3 +37,4 @@ export const getLineaSequencerUpTimeFeedContract = createContractGetter(LineaSeq
export const getOpcodeTesterContract = createContractGetter(OpcodeTesterAbi);
export const getTestContract = createContractGetter(TestContractAbi);
export const getBridgedTokenContract = createContractGetter(BridgedTokenAbi);
export const getForcedTransactionGatewayContract = createContractGetter(ForcedTransactionGatewayAbi);
5 changes: 5 additions & 0 deletions e2e/src/config/contracts/l1-contract-registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
getDummyContract,
getForcedTransactionGatewayContract,
getLineaRollupContract,
getLineaRollupProxyAdminContract,
getTestERC20Contract,
Expand Down Expand Up @@ -30,6 +31,10 @@ export function createL1ContractRegistry(cfg: L1Config) {
dummyContract: <T extends Transport, C extends Chain | undefined, A extends Account | undefined>(
client: Client<T, C, A>,
) => getDummyContract(client, cfg.dummyContractAddress),

forcedTransactionGateway: <T extends Transport, C extends Chain | undefined, A extends Account | undefined>(
client: Client<T, C, A>,
) => getForcedTransactionGatewayContract(client, cfg.forcedTransactionGatewayAddress),
};
}

Expand Down
Loading
Loading