[Refactor] LineaRollup tests (#736)

* utils -> helpers in test

* start to refactor helpers

* did some deploy functions

* running tests with all helper functions refactored out

* slight cleanup

* did BlobSubmission.ts

* did Finalization.ts

* more comments to before

* clean up dependency order
This commit is contained in:
kyzooghost
2025-02-28 21:56:44 +11:00
committed by GitHub
parent 5c8ea31d2a
commit 14c64cf906
15 changed files with 2136 additions and 1929 deletions

View File

@@ -1,5 +1,5 @@
import { ethers, upgrades } from "hardhat";
import { getPermitData } from "../../../test/hardhat/bridging/token/utils/permitHelper";
import { getPermitData } from "../../../test/hardhat/bridging/token/helpers/permitHelper";
import { BridgedToken, MockTokenBridge } from "../../../typechain-types";
import { deployBridgedTokenBeacon } from "../test/deployBridgedTokenBeacon";
import { deployTokens } from "../test/deployTokens";

View File

@@ -4,7 +4,7 @@ The following document serves to be a guide on the best practices for the Linea
**Note:** some areas might not conform to the practices - feel free to open a PR to have them comply. Work is constantly being done to improve.
The current supported framework is Hardhat and TypeScript testing. Future improvements will include Foundry.
The current supported framework is Hardhat and TypeScript testing.
## Folder Structure and Naming Conventions

View File

@@ -4,7 +4,7 @@ import { ethers, upgrades } from "hardhat";
import { deployTokenBridgeWithMockMessaging } from "../../../../scripts/tokenBridge/test/deployTokenBridges";
import { deployTokens } from "../../../../scripts/tokenBridge/test/deployTokens";
import { BridgedToken, TestTokenBridge } from "../../../../typechain-types";
import { getPermitData } from "./utils/permitHelper";
import { getPermitData } from "./helpers/permitHelper";
import { Contract } from "ethers";
import {
ADDRESS_ZERO,

View File

@@ -0,0 +1 @@
export const FALLBACK_OPERATOR_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";

View File

@@ -8,3 +8,4 @@ export * from "./merkleTree";
export * from "./calldata";
export * from "./pauseTypes";
export * from "./roles";
export * from "./address";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,716 @@
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import * as kzg from "c-kzg";
import { expect } from "chai";
import { BaseContract, Transaction } from "ethers";
import { ethers } from "hardhat";
import blobAggregatedProof1To155 from "../../_testData/compressedDataEip4844/aggregatedProof-1-155.json";
import blobMultipleAggregatedProof1To81 from "../../_testData/compressedDataEip4844/multipleProofs/aggregatedProof-1-81.json";
import firstCompressedDataContent from "../../_testData/compressedData/blocks-1-46.json";
import secondCompressedDataContent from "../../_testData/compressedData/blocks-47-81.json";
import fourthCompressedDataContent from "../../_testData/compressedData/blocks-115-155.json";
import { TestLineaRollup } from "contracts/typechain-types";
import {
deployLineaRollupFixture,
deployRevertingVerifier,
expectSuccessfulFinalize,
getAccountsFixture,
getWalletForIndex,
sendBlobTransaction,
} from "../helpers";
import {
GENERAL_PAUSE_TYPE,
HASH_ZERO,
OPERATOR_ROLE,
TEST_PUBLIC_VERIFIER_INDEX,
BLOB_SUBMISSION_PAUSE_TYPE,
} from "../../common/constants";
import {
generateFinalizationData,
generateRandomBytes,
buildAccessErrorMessage,
expectRevertWithCustomError,
expectRevertWithReason,
generateBlobDataSubmission,
generateBlobParentShnarfData,
expectEventDirectFromReceiptData,
} from "../../common/helpers";
describe("Linea Rollup contract: EIP-4844 Blob submission tests", () => {
let lineaRollup: TestLineaRollup;
let revertingVerifier: string;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let securityCouncil: SignerWithAddress;
let operator: SignerWithAddress;
let nonAuthorizedAccount: SignerWithAddress;
const { prevShnarf, parentDataHash, parentStateRootHash } = firstCompressedDataContent;
before(async () => {
({ securityCouncil, operator, nonAuthorizedAccount } = await loadFixture(getAccountsFixture));
});
beforeEach(async () => {
({ lineaRollup } = await loadFixture(deployLineaRollupFixture));
await lineaRollup.setLastFinalizedBlock(0);
await lineaRollup.setupParentShnarf(prevShnarf);
await lineaRollup.setupParentDataShnarf(parentDataHash, prevShnarf);
await lineaRollup.setupParentFinalizedStateRoot(parentDataHash, parentStateRootHash);
});
it("Should successfully submit blobs", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
const txResponse = await ethers.provider.broadcastTransaction(signedTx);
const receipt = await ethers.provider.getTransactionReceipt(txResponse.hash);
expect(receipt).is.not.null;
const expectedEventArgs = [
parentShnarf,
finalShnarf,
blobDataSubmission[blobDataSubmission.length - 1].finalStateRootHash,
];
expectEventDirectFromReceiptData(lineaRollup as BaseContract, receipt!, "DataSubmittedV3", expectedEventArgs);
const blobShnarfExists = await lineaRollup.blobShnarfExists(finalShnarf);
expect(blobShnarfExists).to.equal(1n);
});
it("Fails the blob submission when the parent shnarf is missing", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, finalShnarf } = generateBlobDataSubmission(0, 1);
const nonExistingParentShnarf = generateRandomBytes(32);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
nonExistingParentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx),
"ParentBlobNotSubmitted",
[nonExistingParentShnarf],
);
});
it("Fails when the blob submission data is missing", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [[], parentShnarf, finalShnarf]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx),
"BlobSubmissionDataIsMissing",
);
});
it("Should revert if the caller does not have the OPERATOR_ROLE", async () => {
const { blobDataSubmission, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
await expectRevertWithReason(
lineaRollup.connect(nonAuthorizedAccount).submitBlobs(blobDataSubmission, parentShnarf, finalShnarf),
buildAccessErrorMessage(nonAuthorizedAccount, OPERATOR_ROLE),
);
});
it("Should revert if GENERAL_PAUSE_TYPE is enabled", async () => {
const { blobDataSubmission, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
await lineaRollup.connect(securityCouncil).pauseByType(GENERAL_PAUSE_TYPE);
await expectRevertWithCustomError(
lineaRollup,
lineaRollup.connect(operator).submitBlobs(blobDataSubmission, parentShnarf, finalShnarf),
"IsPaused",
[GENERAL_PAUSE_TYPE],
);
});
it("Should revert if BLOB_SUBMISSION_PAUSE_TYPE is enabled", async () => {
const { blobDataSubmission, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
await lineaRollup.connect(securityCouncil).pauseByType(BLOB_SUBMISSION_PAUSE_TYPE);
await expectRevertWithCustomError(
lineaRollup,
lineaRollup.connect(operator).submitBlobs(blobDataSubmission, parentShnarf, finalShnarf),
"IsPaused",
[BLOB_SUBMISSION_PAUSE_TYPE],
);
});
it("Should revert if the blob data is empty at any index", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 2);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: [compressedBlobs[0]],
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx),
"EmptyBlobDataAtIndex",
[1n],
);
});
it("Should fail if the final state root hash is empty", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
blobDataSubmission[0].finalStateRootHash = HASH_ZERO;
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
// TODO: Make the failure shnarf dynamic and computed
await expectRevertWithCustomError(lineaRollup, ethers.provider.broadcastTransaction(signedTx), "FinalShnarfWrong", [
finalShnarf,
"0x22f8fb954df8328627fe9c48b60192f4d970a92891417aaadea39300ca244d36",
]);
});
it("Should revert when snarkHash is zero hash", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
// Set the snarkHash to HASH_ZERO for a specific index
const emptyDataIndex = 0;
blobDataSubmission[emptyDataIndex].snarkHash = generateRandomBytes(32);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx),
"PointEvaluationFailed",
);
});
it("Should revert if the final shnarf is wrong", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 2);
const badFinalShnarf = generateRandomBytes(32);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
badFinalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(lineaRollup, ethers.provider.broadcastTransaction(signedTx), "FinalShnarfWrong", [
badFinalShnarf,
finalShnarf,
]);
});
it("Should revert if the data has already been submitted", async () => {
await sendBlobTransaction(lineaRollup, 0, 1);
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
// Try to submit the same blob data again
const encodedCall2 = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas: maxFeePerGas2, maxPriorityFeePerGas: maxPriorityFeePerGas2 } =
await ethers.provider.getFeeData();
const nonce2 = await operatorHDSigner.getNonce();
const transaction2 = Transaction.from({
data: encodedCall2,
maxPriorityFeePerGas: maxPriorityFeePerGas2!,
maxFeePerGas: maxFeePerGas2!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce: nonce2,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx2 = await operatorHDSigner.signTransaction(transaction2);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx2),
"DataAlreadySubmitted",
[finalShnarf],
);
});
it("Should revert with PointEvaluationFailed when point evaluation fails", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const { blobDataSubmission, compressedBlobs, parentShnarf, finalShnarf } = generateBlobDataSubmission(0, 1);
// Modify the kzgProof to an invalid value to trigger the PointEvaluationFailed revert
blobDataSubmission[0].kzgProof = HASH_ZERO;
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobDataSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx),
"PointEvaluationFailed",
);
});
it("Should submit 2 blobs, then submit another 2 blobs and finalize", async () => {
// Submit 2 blobs
await sendBlobTransaction(lineaRollup, 0, 2);
// Submit another 2 blobs
await sendBlobTransaction(lineaRollup, 2, 4);
// Finalize 4 blobs
await expectSuccessfulFinalize(
lineaRollup,
operator,
blobAggregatedProof1To155,
4,
fourthCompressedDataContent.finalStateRootHash,
generateBlobParentShnarfData,
);
});
it("Should revert if there is less data than blobs", async () => {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const {
blobDataSubmission: blobSubmission,
compressedBlobs: compressedBlobs,
parentShnarf: parentShnarf,
finalShnarf: finalShnarf,
} = generateBlobDataSubmission(0, 2, true);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
[blobSubmission[0]],
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce: nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
await expectRevertWithCustomError(
lineaRollup,
ethers.provider.broadcastTransaction(signedTx),
"BlobSubmissionDataEmpty",
[1],
);
});
it("Should fail to finalize with not enough gas for the rollup (pre-verifier)", async () => {
// Submit 2 blobs
await sendBlobTransaction(lineaRollup, 0, 2);
// Submit another 2 blobs
await sendBlobTransaction(lineaRollup, 2, 4);
// Finalize 4 blobs
const finalizationData = await generateFinalizationData({
l1RollingHash: blobAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(blobAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(blobAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(blobAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: blobAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(blobAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: blobAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(blobAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: blobAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: blobAggregatedProof1To155.aggregatedProof,
shnarfData: generateBlobParentShnarfData(4, false),
});
finalizationData.lastFinalizedL1RollingHash = HASH_ZERO;
finalizationData.lastFinalizedL1RollingHashMessageNumber = 0n;
await lineaRollup.setRollingHash(
blobAggregatedProof1To155.l1RollingHashMessageNumber,
blobAggregatedProof1To155.l1RollingHash,
);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(blobAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData, {
gasLimit: 50000,
});
// there is no reason
await expect(finalizeCompressedCall).to.be.reverted;
});
it("Should fail to finalize with not enough gas to verify", async () => {
// Submit 2 blobs
await sendBlobTransaction(lineaRollup, 0, 2);
// Submit another 2 blobs
await sendBlobTransaction(lineaRollup, 2, 4);
// Finalize 4 blobs
const finalizationData = await generateFinalizationData({
l1RollingHash: blobAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(blobAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(blobAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(blobAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: blobAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(blobAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: blobAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(blobAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: blobAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: blobAggregatedProof1To155.aggregatedProof,
shnarfData: generateBlobParentShnarfData(4, false),
});
finalizationData.lastFinalizedL1RollingHash = HASH_ZERO;
finalizationData.lastFinalizedL1RollingHashMessageNumber = 0n;
await lineaRollup.setRollingHash(
blobAggregatedProof1To155.l1RollingHashMessageNumber,
blobAggregatedProof1To155.l1RollingHash,
);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(blobAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData, {
gasLimit: 400000,
});
await expectRevertWithCustomError(
lineaRollup,
finalizeCompressedCall,
"InvalidProofOrProofVerificationRanOutOfGas",
["error pairing"],
);
});
const testCases = [
{ revertScenario: 0n, title: "Should fail to finalize via EMPTY_REVERT scenario with 'Unknown'" },
{ revertScenario: 1n, title: "Should fail to finalize via GAS_GUZZLE scenario with 'Unknown'" },
];
testCases.forEach(({ revertScenario, title }) => {
it(title, async () => {
revertingVerifier = await deployRevertingVerifier(revertScenario);
await lineaRollup.connect(securityCouncil).setVerifierAddress(revertingVerifier, 0);
// Submit 2 blobs
await sendBlobTransaction(lineaRollup, 0, 2);
// Submit another 2 blobs
await sendBlobTransaction(lineaRollup, 2, 4);
// Finalize 4 blobs
const finalizationData = await generateFinalizationData({
l1RollingHash: blobAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(blobAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(blobAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(blobAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: blobAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(blobAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: blobAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(blobAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: blobAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: blobAggregatedProof1To155.aggregatedProof,
shnarfData: generateBlobParentShnarfData(4, false),
});
finalizationData.lastFinalizedL1RollingHash = HASH_ZERO;
finalizationData.lastFinalizedL1RollingHashMessageNumber = 0n;
await lineaRollup.setRollingHash(
blobAggregatedProof1To155.l1RollingHashMessageNumber,
blobAggregatedProof1To155.l1RollingHash,
);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(blobAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData, {
gasLimit: 400000,
});
await expectRevertWithCustomError(
lineaRollup,
finalizeCompressedCall,
"InvalidProofOrProofVerificationRanOutOfGas",
["Unknown"],
);
});
});
it("Should successfully submit 2 blobs twice then finalize in two separate finalizations", async () => {
// Submit 2 blobs
await sendBlobTransaction(lineaRollup, 0, 2, true);
// Submit another 2 blobs
await sendBlobTransaction(lineaRollup, 2, 4, true);
// Finalize first 2 blobs
await expectSuccessfulFinalize(
lineaRollup,
operator,
blobMultipleAggregatedProof1To81,
2,
secondCompressedDataContent.finalStateRootHash,
generateBlobParentShnarfData,
true,
);
});
it("Should fail to prove if last finalized is higher than proving range", async () => {
// Submit 2 blobs
await sendBlobTransaction(lineaRollup, 0, 2, true);
// Submit another 2 blobs
await sendBlobTransaction(lineaRollup, 2, 4, true);
await lineaRollup.setLastFinalizedBlock(10_000_000);
const finalizationData = await generateFinalizationData({
l1RollingHash: blobAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(blobAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(blobAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(blobAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: HASH_ZERO, // Manipulate for bypass
finalTimestamp: BigInt(blobAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: blobAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(blobAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: blobAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: blobAggregatedProof1To155.aggregatedProof,
shnarfData: generateBlobParentShnarfData(4, false),
lastFinalizedL1RollingHash: HASH_ZERO,
lastFinalizedL1RollingHashMessageNumber: 0n,
});
await lineaRollup.setRollingHash(
blobAggregatedProof1To155.l1RollingHashMessageNumber,
blobAggregatedProof1To155.l1RollingHash,
);
await lineaRollup.setLastFinalizedBlock(10_000_000);
expectRevertWithCustomError(
lineaRollup,
lineaRollup
.connect(operator)
.finalizeBlocks(blobAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData),
"InvalidProof",
);
});
});

View File

@@ -0,0 +1,857 @@
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { loadFixture, time as networkTime } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import * as fs from "fs";
import { ethers } from "hardhat";
import aggregatedProof1To81 from "../../_testData/compressedData/multipleProofs/aggregatedProof-1-81.json";
import aggregatedProof82To153 from "../../_testData/compressedData/multipleProofs/aggregatedProof-82-153.json";
import betaV1FinalizationData from "../../_testData/betaV1/proof/7027059-7042723-d2221f5035e3dcbbc46e8a6130fef34fdec33c252b7d31fb8afa6848660260ba-getZkAggregatedProof.json";
import calldataAggregatedProof1To155 from "../../_testData/compressedData/aggregatedProof-1-155.json";
import secondCompressedDataContent from "../../_testData/compressedData/blocks-47-81.json";
import fourthCompressedDataContent from "../../_testData/compressedData/blocks-115-155.json";
import fourthMultipleCompressedDataContent from "../../_testData/compressedData/multipleProofs/blocks-120-153.json";
import { LINEA_ROLLUP_PAUSE_TYPES_ROLES, LINEA_ROLLUP_UNPAUSE_TYPES_ROLES } from "contracts/common/constants";
import { TestLineaRollup } from "contracts/typechain-types";
import {
deployPlonkVerifierSepoliaFull,
expectSuccessfulFinalize,
getAccountsFixture,
getBetaV1BlobFiles,
getRoleAddressesFixture,
deployLineaRollupFixture,
sendBlobTransactionFromFile,
} from "./../helpers";
import {
FALLBACK_OPERATOR_ADDRESS,
GENERAL_PAUSE_TYPE,
HASH_ZERO,
INITIAL_WITHDRAW_LIMIT,
ONE_DAY_IN_SECONDS,
OPERATOR_ROLE,
TEST_PUBLIC_VERIFIER_INDEX,
EMPTY_CALLDATA,
FINALIZATION_PAUSE_TYPE,
DEFAULT_LAST_FINALIZED_TIMESTAMP,
LINEA_ROLLUP_INITIALIZE_SIGNATURE,
} from "../../common/constants";
import { deployUpgradableFromFactory } from "../../common/deployment";
import {
calculateRollingHash,
generateFinalizationData,
generateRandomBytes,
generateCallDataSubmission,
generateCallDataSubmissionMultipleProofs,
generateKeccak256,
expectEvent,
buildAccessErrorMessage,
expectRevertWithCustomError,
expectRevertWithReason,
generateParentAndExpectedShnarfForIndex,
generateParentAndExpectedShnarfForMulitpleIndex,
generateParentShnarfData,
} from "../../common/helpers";
describe("Linea Rollup contract: Finalization", () => {
let lineaRollup: TestLineaRollup;
let sepoliaFullVerifier: string;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let securityCouncil: SignerWithAddress;
let operator: SignerWithAddress;
let nonAuthorizedAccount: SignerWithAddress;
let roleAddresses: { addressWithRole: string; role: string }[];
before(async () => {
({ securityCouncil, operator, nonAuthorizedAccount } = await loadFixture(getAccountsFixture));
roleAddresses = await loadFixture(getRoleAddressesFixture);
});
beforeEach(async () => {
({ lineaRollup } = await loadFixture(deployLineaRollupFixture));
});
describe("Blocks finalization with proof", () => {
const messageHash = generateRandomBytes(32);
beforeEach(async () => {
await lineaRollup.addRollingHash(10, messageHash);
await lineaRollup.setLastFinalizedBlock(0);
});
describe("With and without submission data", () => {
it("Should revert if l1 message number == 0 and l1 rolling hash is not empty", async () => {
const finalizationData = await generateFinalizationData({
l1RollingHashMessageNumber: 0n,
l1RollingHash: generateRandomBytes(32),
});
const lastFinalizedBlockNumber = await lineaRollup.currentL2BlockNumber();
const parentStateRootHash = await lineaRollup.stateRootHashes(lastFinalizedBlockNumber);
finalizationData.parentStateRootHash = parentStateRootHash;
const proof = calldataAggregatedProof1To155.aggregatedProof;
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(proof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "MissingMessageNumberForRollingHash", [
finalizationData.l1RollingHash,
]);
});
it("Should revert if l1 message number != 0 and l1 rolling hash is empty", async () => {
const finalizationData = await generateFinalizationData({
l1RollingHashMessageNumber: 1n,
l1RollingHash: HASH_ZERO,
});
const lastFinalizedBlockNumber = await lineaRollup.currentL2BlockNumber();
const parentStateRootHash = await lineaRollup.stateRootHashes(lastFinalizedBlockNumber);
finalizationData.parentStateRootHash = parentStateRootHash;
const proof = calldataAggregatedProof1To155.aggregatedProof;
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(proof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "MissingRollingHashForMessageNumber", [
finalizationData.l1RollingHashMessageNumber,
]);
});
it("Should revert if l1RollingHash does not exist on L1", async () => {
const finalizationData = await generateFinalizationData({
l1RollingHashMessageNumber: 1n,
l1RollingHash: generateRandomBytes(32),
});
const lastFinalizedBlockNumber = await lineaRollup.currentL2BlockNumber();
const parentStateRootHash = await lineaRollup.stateRootHashes(lastFinalizedBlockNumber);
finalizationData.parentStateRootHash = parentStateRootHash;
const proof = calldataAggregatedProof1To155.aggregatedProof;
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(proof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "L1RollingHashDoesNotExistOnL1", [
finalizationData.l1RollingHashMessageNumber,
finalizationData.l1RollingHash,
]);
});
it("Should revert if timestamps are not in sequence", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calculateRollingHash(HASH_ZERO, messageHash),
l1RollingHashMessageNumber: 10n,
lastFinalizedTimestamp: DEFAULT_LAST_FINALIZED_TIMESTAMP,
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
finalizationData.lastFinalizedTimestamp = finalizationData.finalTimestamp + 1n;
const expectedHashValue = generateKeccak256(
["uint256", "bytes32", "uint256"],
[
finalizationData.lastFinalizedL1RollingHashMessageNumber,
finalizationData.lastFinalizedL1RollingHash,
finalizationData.lastFinalizedTimestamp,
],
);
const actualHashValue = generateKeccak256(
["uint256", "bytes32", "uint256"],
[
finalizationData.lastFinalizedL1RollingHashMessageNumber,
finalizationData.lastFinalizedL1RollingHash,
DEFAULT_LAST_FINALIZED_TIMESTAMP,
],
);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCompressedCall, "FinalizationStateIncorrect", [
expectedHashValue,
actualHashValue,
]);
});
it("Should revert if the final shnarf does not exist", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calculateRollingHash(HASH_ZERO, messageHash),
l1RollingHashMessageNumber: 10n,
lastFinalizedTimestamp: DEFAULT_LAST_FINALIZED_TIMESTAMP,
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
finalizationData.shnarfData.snarkHash = generateRandomBytes(32);
const { dataEvaluationClaim, dataEvaluationPoint, finalStateRootHash, parentShnarf, snarkHash } =
finalizationData.shnarfData;
const expectedMissingBlobShnarf = generateKeccak256(
["bytes32", "bytes32", "bytes32", "bytes32", "bytes32"],
[parentShnarf, snarkHash, finalStateRootHash, dataEvaluationPoint, dataEvaluationClaim],
);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCompressedCall, "FinalBlobNotSubmitted", [
expectedMissingBlobShnarf,
]);
});
it("Should revert if finalizationData.finalTimestamp is greater than the block.timestamp", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calculateRollingHash(HASH_ZERO, messageHash),
l1RollingHashMessageNumber: 10n,
lastFinalizedTimestamp: DEFAULT_LAST_FINALIZED_TIMESTAMP,
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(new Date(new Date().setHours(new Date().getHours() + 2)).getTime()), // Set to 2 hours in the future
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCompressedCall, "FinalizationInTheFuture", [
finalizationData.finalTimestamp,
(await networkTime.latest()) + 1,
]);
});
});
describe("Without submission data", () => {
it("Should revert if the final block state equals the zero hash", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calculateRollingHash(HASH_ZERO, messageHash),
l1RollingHashMessageNumber: 10n,
lastFinalizedTimestamp: DEFAULT_LAST_FINALIZED_TIMESTAMP,
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
// Set the final state root hash to zero
finalizationData.shnarfData.finalStateRootHash = HASH_ZERO;
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "FinalBlockStateEqualsZeroHash");
});
});
it("Can submit blobs and finalize with Prover Beta V1", async () => {
const blobFiles = getBetaV1BlobFiles();
const finalBlobFile = JSON.parse(
fs.readFileSync(`${__dirname}/../../_testData/betaV1/${blobFiles.slice(-1)[0]}`, "utf-8"),
);
sepoliaFullVerifier = await deployPlonkVerifierSepoliaFull();
const initializationData = {
initialStateRootHash: betaV1FinalizationData.parentStateRootHash,
initialL2BlockNumber: betaV1FinalizationData.lastFinalizedBlockNumber,
genesisTimestamp: betaV1FinalizationData.parentAggregationLastBlockTimestamp,
defaultVerifier: sepoliaFullVerifier,
rateLimitPeriodInSeconds: ONE_DAY_IN_SECONDS,
rateLimitAmountInWei: INITIAL_WITHDRAW_LIMIT,
roleAddresses,
pauseTypeRoles: LINEA_ROLLUP_PAUSE_TYPES_ROLES,
unpauseTypeRoles: LINEA_ROLLUP_UNPAUSE_TYPES_ROLES,
fallbackOperator: FALLBACK_OPERATOR_ADDRESS,
defaultAdmin: securityCouncil.address,
};
const betaV1LineaRollup = (await deployUpgradableFromFactory("TestLineaRollup", [initializationData], {
initializer: LINEA_ROLLUP_INITIALIZE_SIGNATURE,
unsafeAllow: ["constructor", "incorrect-initializer-order"],
})) as unknown as TestLineaRollup;
await betaV1LineaRollup.setupParentShnarf(betaV1FinalizationData.parentAggregationFinalShnarf);
await betaV1LineaRollup.setLastFinalizedShnarf(betaV1FinalizationData.parentAggregationFinalShnarf);
for (let i = 0; i < blobFiles.length; i++) {
await sendBlobTransactionFromFile(lineaRollup, blobFiles[i], betaV1LineaRollup);
}
const finalizationData = await generateFinalizationData({
l1RollingHash: betaV1FinalizationData.l1RollingHash,
l1RollingHashMessageNumber: BigInt(betaV1FinalizationData.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(betaV1FinalizationData.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(betaV1FinalizationData.finalBlockNumber),
parentStateRootHash: betaV1FinalizationData.parentStateRootHash,
finalTimestamp: BigInt(betaV1FinalizationData.finalTimestamp),
l2MerkleRoots: betaV1FinalizationData.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(betaV1FinalizationData.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: betaV1FinalizationData.l2MessagingBlocksOffsets,
aggregatedProof: betaV1FinalizationData.aggregatedProof,
shnarfData: {
parentShnarf: finalBlobFile.prevShnarf,
snarkHash: finalBlobFile.snarkHash,
finalStateRootHash: finalBlobFile.finalStateRootHash,
dataEvaluationPoint: finalBlobFile.expectedX,
dataEvaluationClaim: finalBlobFile.expectedY,
},
});
finalizationData.lastFinalizedL1RollingHash = betaV1FinalizationData.parentAggregationLastL1RollingHash;
finalizationData.lastFinalizedL1RollingHashMessageNumber = BigInt(
betaV1FinalizationData.parentAggregationLastL1RollingHashMessageNumber,
);
await betaV1LineaRollup.setLastFinalizedState(
betaV1FinalizationData.parentAggregationLastL1RollingHashMessageNumber,
betaV1FinalizationData.parentAggregationLastL1RollingHash,
betaV1FinalizationData.parentAggregationLastBlockTimestamp,
);
await betaV1LineaRollup.setRollingHash(
betaV1FinalizationData.l1RollingHashMessageNumber,
betaV1FinalizationData.l1RollingHash,
);
const finalizeCompressedCall = betaV1LineaRollup
.connect(operator)
.finalizeBlocks(betaV1FinalizationData.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
const eventArgs = [
BigInt(betaV1FinalizationData.lastFinalizedBlockNumber) + 1n,
finalizationData.endBlockNumber,
betaV1FinalizationData.finalShnarf,
finalizationData.parentStateRootHash,
finalBlobFile.finalStateRootHash,
];
await expectEvent(betaV1LineaRollup, finalizeCompressedCall, "DataFinalizedV3", eventArgs);
const [expectedFinalStateRootHash, lastFinalizedBlockNumber, lastFinalizedState] = await Promise.all([
betaV1LineaRollup.stateRootHashes(finalizationData.endBlockNumber),
betaV1LineaRollup.currentL2BlockNumber(),
betaV1LineaRollup.currentFinalizedState(),
]);
expect(expectedFinalStateRootHash).to.equal(finalizationData.shnarfData.finalStateRootHash);
expect(lastFinalizedBlockNumber).to.equal(finalizationData.endBlockNumber);
expect(lastFinalizedState).to.equal(
generateKeccak256(
["uint256", "bytes32", "uint256"],
[
finalizationData.l1RollingHashMessageNumber,
finalizationData.l1RollingHash,
finalizationData.finalTimestamp,
],
),
);
});
});
describe("Compressed data finalization with proof", () => {
beforeEach(async () => {
await lineaRollup.setLastFinalizedBlock(0);
});
it("Should revert if the caller does not have the OPERATOR_ROLE", async () => {
const finalizationData = await generateFinalizationData();
const finalizeCall = lineaRollup
.connect(nonAuthorizedAccount)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithReason(finalizeCall, buildAccessErrorMessage(nonAuthorizedAccount, OPERATOR_ROLE));
});
it("Should revert if GENERAL_PAUSE_TYPE is enabled", async () => {
const finalizationData = await generateFinalizationData();
await lineaRollup.connect(securityCouncil).pauseByType(GENERAL_PAUSE_TYPE);
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(EMPTY_CALLDATA, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "IsPaused", [GENERAL_PAUSE_TYPE]);
});
it("Should revert if FINALIZATION_PAUSE_TYPE is enabled", async () => {
const finalizationData = await generateFinalizationData();
await lineaRollup.connect(securityCouncil).pauseByType(FINALIZATION_PAUSE_TYPE);
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(EMPTY_CALLDATA, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "IsPaused", [FINALIZATION_PAUSE_TYPE]);
});
it("Should revert if the proof is empty", async () => {
const finalizationData = await generateFinalizationData();
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(EMPTY_CALLDATA, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "ProofIsEmpty");
});
it("Should revert when finalization parentStateRootHash is different than last finalized state root hash", async () => {
// Submit 4 sets of compressed data setting the correct shnarf in storage
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
lastFinalizedTimestamp: DEFAULT_LAST_FINALIZED_TIMESTAMP,
parentStateRootHash: generateRandomBytes(32),
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
});
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData, {
gasLimit: 30_000_000,
});
await expectRevertWithCustomError(lineaRollup, finalizeCall, "StartingRootHashDoesNotMatch");
});
it("Should successfully finalize with only previously submitted data", async () => {
// Submit 4 sets of compressed data setting the correct shnarf in storage
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
await expectSuccessfulFinalize(
lineaRollup,
operator,
calldataAggregatedProof1To155,
index,
fourthCompressedDataContent.finalStateRootHash,
generateParentShnarfData,
);
});
it("Should revert when proofType is invalid", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calldataAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(calldataAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(calldataAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, 99, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "InvalidProofType");
});
it("Should revert when using a proofType index that was removed", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calldataAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(calldataAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(calldataAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
// removing the verifier index
await lineaRollup.connect(securityCouncil).unsetVerifierAddress(TEST_PUBLIC_VERIFIER_INDEX);
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "InvalidProofType");
});
it("Should fail when proof does not match", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calldataAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(calldataAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(calldataAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(index),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
// aggregatedProof1To81.aggregatedProof, wrong proof on purpose
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(aggregatedProof1To81.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "InvalidProof");
});
it("Should fail if shnarf does not exist when finalizing", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmission(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const finalizationData = await generateFinalizationData({
l1RollingHash: calldataAggregatedProof1To155.l1RollingHash,
l1RollingHashMessageNumber: BigInt(calldataAggregatedProof1To155.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(calldataAggregatedProof1To155.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(calldataAggregatedProof1To155.finalBlockNumber),
parentStateRootHash: calldataAggregatedProof1To155.parentStateRootHash,
finalTimestamp: BigInt(calldataAggregatedProof1To155.finalTimestamp),
l2MerkleRoots: calldataAggregatedProof1To155.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(calldataAggregatedProof1To155.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: calldataAggregatedProof1To155.l2MessagingBlocksOffsets,
aggregatedProof: calldataAggregatedProof1To155.aggregatedProof,
shnarfData: generateParentShnarfData(1),
});
await lineaRollup.setRollingHash(
calldataAggregatedProof1To155.l1RollingHashMessageNumber,
calldataAggregatedProof1To155.l1RollingHash,
);
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(calldataAggregatedProof1To155.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "InvalidProof");
});
it("Should successfully finalize 1-81 and then 82-153 in two separate finalizations", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmissionMultipleProofs(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForMulitpleIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
await expectSuccessfulFinalize(
lineaRollup,
operator,
aggregatedProof1To81,
2,
secondCompressedDataContent.finalStateRootHash,
generateParentShnarfData,
true,
);
await expectSuccessfulFinalize(
lineaRollup,
operator,
aggregatedProof82To153,
4,
fourthMultipleCompressedDataContent.finalStateRootHash,
generateParentShnarfData,
true,
aggregatedProof1To81.l1RollingHash,
BigInt(aggregatedProof1To81.l1RollingHashMessageNumber),
);
});
it("Should succeed when sending with pure calldata", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmissionMultipleProofs(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForMulitpleIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
await lineaRollup.setRollingHash(
aggregatedProof1To81.l1RollingHashMessageNumber,
aggregatedProof1To81.l1RollingHash,
);
const lineaRollupAddress = await lineaRollup.getAddress();
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const encodedCall =
"0x5603c65f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e00000000000000000000000000000000000000000000000000000000000000360008e483358dc0ac1d3f6a27b43f6332e88c4196151b2a1ce6c8575013cec2cbe2f666e5649165dfc5a88ede985203341f5910ec2f4e3d4e9fa2a0a2621e4b0a70a91e0f995a8d754a3ddec1698800be83da8815650488540d2ffb699eaf3518d14bff479974f1f5f8bef7687f89c6301247cd92a4ccfa76a868bd24090a670f20eb06513431e74411648ab8c02a270353146bc759451d00cf2a59477d6106b9d11df3e18bca0d3a36951e31699b21a73e22a77fb7fe43a33b717a23dd06a889a102e4143d1e024e053e4318f903c3dbd0ec0f6e49672dd0ccb10749ac00adaf311149a24bf2c2ee4cfe7ed1db713526c6f7c0893f2ec153e5f55a4b390ce135a0d3ebe372c1beea8142afebdc96fe2764b8b5a33efc0457b9ad65f678c8b9ede196eb1fe34a82e0f370fe1d53e5cfc34bc77f38a64eddbb74f6530a82feb0e0a148de4d309b179697ff8a3c97a5516a1bbffc36d8cf153e9f5ee8fdc98002fd1136a2a12603aa1c7a985b34c5dc6774a5efd6270eac180406b4f03c7ef08eee428bb45d04a3c6635a0225aae76c0522049df5080045629cd6a686c2d50d8b7fe10e6facded652c8d75d4a67d5f8f4c3c6b491f44cc23c307064ec94f7c3a44eb1d1dcd369464960e54a88ad3f9edbdd818f2ac711d6c8f49efe224fb2616f2ac0005ca0c753c801d9e2df1133cdd4de5e54554197ac3e2f42d0b140261a12796050d64ec14bb22355173d157a7a2a9a5c0a20d47430819b4c106fda54923f9bf273cbc4145962e07b853279e64e198d3da5919814935c4cf12ffc03128d0e4fc24e8f35180d257253c0ce33725cb665a81141f16418fb20cf2abb96fa65a03bc0fa8e1e842123e2a2590f423e34c1db2c51c9e872ea41942d44d94d735cf8b4b0ccc265c6bdc41dd6f1167da133450a5ea0c07ea4704c9ca377cd201d8a63f932f7d4c9edb88269008bfe08d91d576cbf1a46c2da88b13d2a38e68f66f330552114f37a226c38ed3a9acd35b75f5852e6879eb3c8746ed2676f9e3957d32bbee1493ac055f03a56e964c4ea278d323abde55b36439b5460897771bf5ef3be1992448a5479f3515d5605d403a51391c6c6db94a90b3353aa956a5ba6f7b951fee2b7657ab431f90220c84820c2db8e4b57eaa5c3ea13e8d1f4ed31b9434c5df5c083dbb24fdf9dc5a64ba7416e6d154e6b17727a3acaa9f59820e926196efd232072ead6777750dc20232d1cee8dc9a395c2d350df4bbaa5096c6f59b214dcecd00000000000000000000000000000000000000000000000000000000000000519562fa89830a0ba0063a636ca96e52ce2f032b855336c95dd788321f4e1934190eacb8ed649249b3ec6efd9fbd6ffebe1b325b53380a91f7d689bfc1aff3b6dcf0f26782f7afb93f926cacb145f55530714f20b1356725e3971dc99e0ef8b59101216d3e1700c3a0d5115686fa51caa982cb4e002a5bb9f9488c9c44e4d9a3042d2f290de42ce8ea03cf8ac09288166932f174cb069ff8147c95ed6374e4e6cb00000000000000000000000000000000000000000000000000000000645580d1000000000000000000000000000000000000000000000000000000006455a7e10000000000000000000000000000000000000000000000000000000000000000dc8e70637c1048e1e0406c4ed6fab51a7489ccb52f37ddd2c135cb1aa18ec6970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000016de197db892fd7d3fe0a389452dae0c5d0520e23d18ad20327546e2189a7e3f1000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000fffff000000000000000000000000000000000000";
const transaction = {
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: 31337,
value: 0,
gasLimit: 5_000_000,
};
await expect(operator.sendTransaction(transaction)).to.not.be.reverted;
});
it("Should fail when sending with wrong merkle root location", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmissionMultipleProofs(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForMulitpleIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
await lineaRollup.setRollingHash(
aggregatedProof1To81.l1RollingHashMessageNumber,
aggregatedProof1To81.l1RollingHash,
);
const lineaRollupAddress = await lineaRollup.getAddress();
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
// This contains the Merkle roots length for public input at 0x220 and a contrived pointer to an alternate location.
const encodedCall =
"0x5603c65f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e00000000000000000000000000000000000000000000000000000000000000360008e483358dc0ac1d3f6a27b43f6332e88c4196151b2a1ce6c8575013cec2cbe2f666e5649165dfc5a88ede985203341f5910ec2f4e3d4e9fa2a0a2621e4b0a70a91e0f995a8d754a3ddec1698800be83da8815650488540d2ffb699eaf3518d14bff479974f1f5f8bef7687f89c6301247cd92a4ccfa76a868bd24090a670f20eb06513431e74411648ab8c02a270353146bc759451d00cf2a59477d6106b9d11df3e18bca0d3a36951e31699b21a73e22a77fb7fe43a33b717a23dd06a889a102e4143d1e024e053e4318f903c3dbd0ec0f6e49672dd0ccb10749ac00adaf311149a24bf2c2ee4cfe7ed1db713526c6f7c0893f2ec153e5f55a4b390ce135a0d3ebe372c1beea8142afebdc96fe2764b8b5a33efc0457b9ad65f678c8b9ede196eb1fe34a82e0f370fe1d53e5cfc34bc77f38a64eddbb74f6530a82feb0e0a148de4d309b179697ff8a3c97a5516a1bbffc36d8cf153e9f5ee8fdc98002fd1136a2a12603aa1c7a985b34c5dc6774a5efd6270eac180406b4f03c7ef08eee428bb45d04a3c6635a0225aae76c0522049df5080045629cd6a686c2d50d8b7fe10e6facded652c8d75d4a67d5f8f4c3c6b491f44cc23c307064ec94f7c3a44eb1d1dcd369464960e54a88ad3f9edbdd818f2ac711d6c8f49efe224fb2616f2ac0005ca0c753c801d9e2df1133cdd4de5e54554197ac3e2f42d0b140261a12796050d64ec14bb22355173d157a7a2a9a5c0a20d47430819b4c106fda54923f9bf273cbc4145962e07b853279e64e198d3da5919814935c4cf12ffc03128d0e4fc24e8f35180d257253c0ce33725cb665a81141f16418fb20cf2abb96fa65a03bc0fa8e1e842123e2a2590f423e34c1db2c51c9e872ea41942d44d94d735cf8b4b0ccc265c6bdc41dd6f1167da133450a5ea0c07ea4704c9ca377cd201d8a63f932f7d4c9edb88269008bfe08d91d576cbf1a46c2da88b13d2a38e68f66f330552114f37a226c38ed3a9acd35b75f5852e6879eb3c8746ed2676f9e3957d32bbee1493ac055f03a56e964c4ea278d323abde55b36439b5460897771bf5ef3be1992448a5479f3515d5605d403a51391c6c6db94a90b3353aa956a5ba6f7b951fee2b7657ab431f90220c84820c2db8e4b57eaa5c3ea13e8d1f4ed31b9434c5df5c083dbb24fdf9dc5a64ba7416e6d154e6b17727a3acaa9f59820e926196efd232072ead6777750dc20232d1cee8dc9a395c2d350df4bbaa5096c6f59b214dcecd00000000000000000000000000000000000000000000000000000000000000519562fa89830a0ba0063a636ca96e52ce2f032b855336c95dd788321f4e1934190eacb8ed649249b3ec6efd9fbd6ffebe1b325b53380a91f7d689bfc1aff3b6dcf0f26782f7afb93f926cacb145f55530714f20b1356725e3971dc99e0ef8b59101216d3e1700c3a0d5115686fa51caa982cb4e002a5bb9f9488c9c44e4d9a3042d2f290de42ce8ea03cf8ac09288166932f174cb069ff8147c95ed6374e4e6cb00000000000000000000000000000000000000000000000000000000645580d1000000000000000000000000000000000000000000000000000000006455a7e10000000000000000000000000000000000000000000000000000000000000000dc8e70637c1048e1e0406c4ed6fab51a7489ccb52f37ddd2c135cb1aa18ec6970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000016de197db892fd7d3fe0a389452dae0c5d0520e23d18ad20327546e2189a7e3f1000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000fffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001cafecafe";
const transaction = {
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: 31337,
value: 0,
gasLimit: 5_000_000,
};
await expectRevertWithCustomError(lineaRollup, operator.sendTransaction(transaction), "InvalidProof");
});
it("Should fail to finalize with extra merkle roots", async () => {
const submissionDataBeforeFinalization = generateCallDataSubmissionMultipleProofs(0, 4);
let index = 0;
for (const data of submissionDataBeforeFinalization) {
const parentAndExpectedShnarf = generateParentAndExpectedShnarfForMulitpleIndex(index);
await lineaRollup
.connect(operator)
.submitDataAsCalldata(data, parentAndExpectedShnarf.parentShnarf, parentAndExpectedShnarf.expectedShnarf, {
gasLimit: 30_000_000,
});
index++;
}
const merkleRoots = aggregatedProof1To81.l2MerkleRoots;
merkleRoots.push(generateRandomBytes(32));
const finalizationData = await generateFinalizationData({
l1RollingHash: aggregatedProof1To81.l1RollingHash,
l1RollingHashMessageNumber: BigInt(aggregatedProof1To81.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(aggregatedProof1To81.parentAggregationLastBlockTimestamp),
parentStateRootHash: aggregatedProof1To81.parentStateRootHash,
finalTimestamp: BigInt(aggregatedProof1To81.finalTimestamp),
l2MerkleRoots: merkleRoots,
l2MerkleTreesDepth: BigInt(aggregatedProof1To81.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: aggregatedProof1To81.l2MessagingBlocksOffsets,
aggregatedProof: aggregatedProof1To81.aggregatedProof,
shnarfData: generateParentShnarfData(2, true),
});
finalizationData.lastFinalizedL1RollingHash = HASH_ZERO;
finalizationData.lastFinalizedL1RollingHashMessageNumber = 0n;
await lineaRollup.setRollingHash(
aggregatedProof1To81.l1RollingHashMessageNumber,
aggregatedProof1To81.l1RollingHash,
);
const finalizeCall = lineaRollup
.connect(operator)
.finalizeBlocks(aggregatedProof1To81.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
await expectRevertWithCustomError(lineaRollup, finalizeCall, "InvalidProof");
});
});
});

View File

@@ -0,0 +1,33 @@
/**
* Define fixtures to be loaded in the 'before' block using Hardhat 'loadFixture()' function, e.g.
before(async () => {
({ admin, securityCouncil, operator, nonAuthorizedAccount } = await loadFixture(getAccountsFixture));
roleAddresses = await loadFixture(getRoleAddressesFixture);
});
*/
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { ethers } from "hardhat";
import { LINEA_ROLLUP_ROLES } from "contracts/common/constants";
import { generateRoleAssignments } from "contracts/common/helpers";
import { OPERATOR_ROLE } from "../../common/constants";
// Use in `loadFixture(getAccountsFixture))` and not as a standalone function.
// This will ensure that the same return values will be retrieved across all invocations.
export async function getAccountsFixture() {
const [admin, securityCouncil, operator, nonAuthorizedAccount] = await ethers.getSigners();
return { admin, securityCouncil, operator, nonAuthorizedAccount };
}
export async function getRoleAddressesFixture() {
const { securityCouncil, operator } = await loadFixture(getAccountsFixture);
const roleAddresses = generateRoleAssignments(LINEA_ROLLUP_ROLES, await securityCouncil.getAddress(), [
{
role: OPERATOR_ROLE,
addresses: [operator.address],
},
]);
return roleAddresses;
}

View File

@@ -0,0 +1,183 @@
import * as kzg from "c-kzg";
import { BaseContract, Contract, Transaction } from "ethers";
import * as fs from "fs";
import { ethers } from "hardhat";
import path from "path";
import { TestLineaRollup } from "contracts/typechain-types";
import { getWalletForIndex } from "./";
import {
expectEventDirectFromReceiptData,
generateBlobDataSubmission,
generateBlobDataSubmissionFromFile,
} from "../../common/helpers";
export async function sendBlobTransaction(
lineaRollup: TestLineaRollup,
startIndex: number,
finalIndex: number,
isMultiple: boolean = false,
) {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await lineaRollup.getAddress();
const {
blobDataSubmission: blobSubmission,
compressedBlobs: compressedBlobs,
parentShnarf: parentShnarf,
finalShnarf: finalShnarf,
} = generateBlobDataSubmission(startIndex, finalIndex, isMultiple);
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce: nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
const txResponse = await ethers.provider.broadcastTransaction(signedTx);
const receipt = await ethers.provider.getTransactionReceipt(txResponse.hash);
const expectedEventArgs = [parentShnarf, finalShnarf, blobSubmission[blobSubmission.length - 1].finalStateRootHash];
expectEventDirectFromReceiptData(lineaRollup as BaseContract, receipt!, "DataSubmittedV3", expectedEventArgs);
}
export async function sendBlobTransactionFromFile(
lineaRollup: TestLineaRollup,
filePath: string,
betaV1LineaRollup: TestLineaRollup,
) {
const operatorHDSigner = getWalletForIndex(2);
const lineaRollupAddress = await betaV1LineaRollup.getAddress();
const {
blobDataSubmission: blobSubmission,
compressedBlobs: compressedBlobs,
parentShnarf: parentShnarf,
finalShnarf: finalShnarf,
} = generateBlobDataSubmissionFromFile(path.resolve(__dirname, "../../_testData/betaV1", filePath));
const encodedCall = lineaRollup.interface.encodeFunctionData("submitBlobs", [
blobSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: lineaRollupAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce: nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
const txResponse = await ethers.provider.broadcastTransaction(signedTx);
const receipt = await ethers.provider.getTransactionReceipt(txResponse.hash);
const expectedEventArgs = [parentShnarf, finalShnarf, blobSubmission[blobSubmission.length - 1].finalStateRootHash];
expectEventDirectFromReceiptData(lineaRollup as BaseContract, receipt!, "DataSubmittedV3", expectedEventArgs);
}
export async function sendBlobTransactionViaCallForwarder(
lineaRollupUpgraded: Contract,
startIndex: number,
finalIndex: number,
callforwarderAddress: string,
) {
const operatorHDSigner = getWalletForIndex(2);
const {
blobDataSubmission: blobSubmission,
compressedBlobs: compressedBlobs,
parentShnarf: parentShnarf,
finalShnarf: finalShnarf,
} = generateBlobDataSubmission(startIndex, finalIndex, false);
const encodedCall = lineaRollupUpgraded.interface.encodeFunctionData("submitBlobs", [
blobSubmission,
parentShnarf,
finalShnarf,
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: callforwarderAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 3,
nonce: nonce,
value: 0,
gasLimit: 5_000_000,
kzg,
maxFeePerBlobGas: 1n,
blobs: compressedBlobs,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
const txResponse = await ethers.provider.broadcastTransaction(signedTx);
const receipt = await ethers.provider.getTransactionReceipt(txResponse.hash);
const expectedEventArgs = [parentShnarf, finalShnarf, blobSubmission[blobSubmission.length - 1].finalStateRootHash];
expectEventDirectFromReceiptData(lineaRollupUpgraded as BaseContract, receipt!, "DataSubmittedV3", expectedEventArgs);
}
export function getBetaV1BlobFiles(): string[] {
// Read all files in the folder
const files = fs.readdirSync(path.resolve(__dirname, "../../_testData/betaV1"));
// Map files to their ranges and filter invalid ones
const filesWithRanges = files
.map((fileName) => {
const range = extractBlockRangeFromFileName(fileName);
return range ? { fileName, range } : null;
})
.filter(Boolean) as { fileName: string; range: [number, number] }[];
return filesWithRanges.sort((a, b) => a.range[0] - b.range[0]).map((f) => f.fileName);
}
// Function to extract range from the file name
function extractBlockRangeFromFileName(fileName: string): [number, number] | null {
const rangeRegex = /(\d+)-(\d+)-/;
const match = fileName.match(rangeRegex);
if (match && match.length >= 3) {
return [parseInt(match[1], 10), parseInt(match[2], 10)];
}
return null;
}

View File

@@ -0,0 +1,73 @@
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { ethers } from "hardhat";
import firstCompressedDataContent from "../../_testData/compressedData/blocks-1-46.json";
import { LINEA_ROLLUP_PAUSE_TYPES_ROLES, LINEA_ROLLUP_UNPAUSE_TYPES_ROLES } from "contracts/common/constants";
import { CallForwardingProxy, TestLineaRollup } from "contracts/typechain-types";
import { getAccountsFixture, getRoleAddressesFixture } from "./";
import {
DEFAULT_LAST_FINALIZED_TIMESTAMP,
FALLBACK_OPERATOR_ADDRESS,
INITIAL_WITHDRAW_LIMIT,
LINEA_ROLLUP_INITIALIZE_SIGNATURE,
ONE_DAY_IN_SECONDS,
} from "../../common/constants";
import { deployUpgradableFromFactory } from "../../common/deployment";
export async function deployRevertingVerifier(scenario: bigint): Promise<string> {
const revertingVerifierFactory = await ethers.getContractFactory("RevertingVerifier");
const verifier = await revertingVerifierFactory.deploy(scenario);
await verifier.waitForDeployment();
return await verifier.getAddress();
}
export async function deployPlonkVerifierSepoliaFull(): Promise<string> {
const plonkVerifierSepoliaFull = await ethers.getContractFactory("PlonkVerifierSepoliaFull");
const verifier = await plonkVerifierSepoliaFull.deploy();
await verifier.waitForDeployment();
return await verifier.getAddress();
}
export async function deployCallForwardingProxy(target: string): Promise<CallForwardingProxy> {
const callForwardingProxyFactory = await ethers.getContractFactory("CallForwardingProxy");
const callForwardingProxy = await callForwardingProxyFactory.deploy(target);
await callForwardingProxy.waitForDeployment();
return callForwardingProxy;
}
export async function deployLineaRollupFixture() {
const { securityCouncil } = await loadFixture(getAccountsFixture);
const roleAddresses = await loadFixture(getRoleAddressesFixture);
const verifier = await deployTestPlonkVerifierForDataAggregation();
const { parentStateRootHash } = firstCompressedDataContent;
const initializationData = {
initialStateRootHash: parentStateRootHash,
initialL2BlockNumber: 0,
genesisTimestamp: DEFAULT_LAST_FINALIZED_TIMESTAMP,
defaultVerifier: verifier,
rateLimitPeriodInSeconds: ONE_DAY_IN_SECONDS,
rateLimitAmountInWei: INITIAL_WITHDRAW_LIMIT,
roleAddresses,
pauseTypeRoles: LINEA_ROLLUP_PAUSE_TYPES_ROLES,
unpauseTypeRoles: LINEA_ROLLUP_UNPAUSE_TYPES_ROLES,
fallbackOperator: FALLBACK_OPERATOR_ADDRESS,
defaultAdmin: securityCouncil.address,
};
const lineaRollup = (await deployUpgradableFromFactory("TestLineaRollup", [initializationData], {
initializer: LINEA_ROLLUP_INITIALIZE_SIGNATURE,
unsafeAllow: ["constructor", "incorrect-initializer-order"],
})) as unknown as TestLineaRollup;
return { verifier, lineaRollup };
}
async function deployTestPlonkVerifierForDataAggregation(): Promise<string> {
const plonkVerifierSepoliaFull = await ethers.getContractFactory("TestPlonkVerifierForDataAggregation");
const verifier = await plonkVerifierSepoliaFull.deploy();
await verifier.waitForDeployment();
return await verifier.getAddress();
}

View File

@@ -0,0 +1,215 @@
import { expect } from "chai";
import { BaseContract, Contract, Transaction } from "ethers";
import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { TestLineaRollup } from "contracts/typechain-types";
import { getWalletForIndex } from "./";
import { HASH_ZERO, TEST_PUBLIC_VERIFIER_INDEX } from "../../common/constants";
import {
expectEvent,
expectEventDirectFromReceiptData,
generateFinalizationData,
generateKeccak256,
} from "../../common/helpers";
import { ShnarfDataGenerator } from "../../common/types";
export async function expectSuccessfulFinalize(
lineaRollup: TestLineaRollup,
operator: SignerWithAddress,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
proofData: any,
blobParentShnarfIndex: number,
finalStateRootHash: string,
shnarfDataGenerator: ShnarfDataGenerator,
isMultiple: boolean = false,
lastFinalizedRollingHash: string = HASH_ZERO,
lastFinalizedMessageNumber: bigint = 0n,
) {
const finalizationData = await generateFinalizationData({
l1RollingHash: proofData.l1RollingHash,
l1RollingHashMessageNumber: BigInt(proofData.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(proofData.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(proofData.finalBlockNumber),
parentStateRootHash: proofData.parentStateRootHash,
finalTimestamp: BigInt(proofData.finalTimestamp),
l2MerkleRoots: proofData.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(proofData.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: proofData.l2MessagingBlocksOffsets,
aggregatedProof: proofData.aggregatedProof,
shnarfData: shnarfDataGenerator(blobParentShnarfIndex, isMultiple),
});
finalizationData.lastFinalizedL1RollingHash = lastFinalizedRollingHash;
finalizationData.lastFinalizedL1RollingHashMessageNumber = lastFinalizedMessageNumber;
await lineaRollup.setRollingHash(proofData.l1RollingHashMessageNumber, proofData.l1RollingHash);
const finalizeCompressedCall = lineaRollup
.connect(operator)
.finalizeBlocks(proofData.aggregatedProof, TEST_PUBLIC_VERIFIER_INDEX, finalizationData);
const eventArgs = [
BigInt(proofData.lastFinalizedBlockNumber) + 1n,
finalizationData.endBlockNumber,
proofData.finalShnarf,
finalizationData.parentStateRootHash,
finalStateRootHash,
];
await expectEvent(lineaRollup, finalizeCompressedCall, "DataFinalizedV3", eventArgs);
const [expectedFinalStateRootHash, lastFinalizedBlockNumber, lastFinalizedState] = await Promise.all([
lineaRollup.stateRootHashes(finalizationData.endBlockNumber),
lineaRollup.currentL2BlockNumber(),
lineaRollup.currentFinalizedState(),
]);
expect(expectedFinalStateRootHash).to.equal(finalizationData.shnarfData.finalStateRootHash);
expect(lastFinalizedBlockNumber).to.equal(finalizationData.endBlockNumber);
expect(lastFinalizedState).to.equal(
generateKeccak256(
["uint256", "bytes32", "uint256"],
[finalizationData.l1RollingHashMessageNumber, finalizationData.l1RollingHash, finalizationData.finalTimestamp],
),
);
}
export async function expectSuccessfulFinalizeViaCallForwarder(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
proofData: any,
blobParentShnarfIndex: number,
finalStateRootHash: string,
shnarfDataGenerator: ShnarfDataGenerator,
isMultiple: boolean = false,
lastFinalizedRollingHash: string = HASH_ZERO,
lastFinalizedMessageNumber: bigint = 0n,
callforwarderAddress: string,
upgradedContract: Contract,
) {
const finalizationData = await generateFinalizationData({
l1RollingHash: proofData.l1RollingHash,
l1RollingHashMessageNumber: BigInt(proofData.l1RollingHashMessageNumber),
lastFinalizedTimestamp: BigInt(proofData.parentAggregationLastBlockTimestamp),
endBlockNumber: BigInt(proofData.finalBlockNumber),
parentStateRootHash: proofData.parentStateRootHash,
finalTimestamp: BigInt(proofData.finalTimestamp),
l2MerkleRoots: proofData.l2MerkleRoots,
l2MerkleTreesDepth: BigInt(proofData.l2MerkleTreesDepth),
l2MessagingBlocksOffsets: proofData.l2MessagingBlocksOffsets,
aggregatedProof: proofData.aggregatedProof,
shnarfData: shnarfDataGenerator(blobParentShnarfIndex, isMultiple),
});
finalizationData.lastFinalizedL1RollingHash = lastFinalizedRollingHash;
finalizationData.lastFinalizedL1RollingHashMessageNumber = lastFinalizedMessageNumber;
await upgradedContract.setRollingHash(proofData.l1RollingHashMessageNumber, proofData.l1RollingHash);
const shnarfData = shnarfDataGenerator(blobParentShnarfIndex, isMultiple);
const finalShnarf = generateKeccak256(
["bytes32", "bytes32", "bytes32", "bytes32", "bytes32"],
[
shnarfData.parentShnarf,
shnarfData.snarkHash,
shnarfData.finalStateRootHash,
shnarfData.dataEvaluationPoint,
shnarfData.dataEvaluationClaim,
],
);
const blobShnarfExists = await upgradedContract.blobShnarfExists(finalShnarf);
expect(blobShnarfExists).to.equal(1n);
await upgradedContract.setRollingHash(proofData.l1RollingHashMessageNumber, proofData.l1RollingHash);
const txData = [
proofData.aggregatedProof,
0,
[
proofData.parentStateRootHash,
BigInt(proofData.finalBlockNumber),
[
shnarfData.parentShnarf,
shnarfData.snarkHash,
shnarfData.finalStateRootHash,
shnarfData.dataEvaluationPoint,
shnarfData.dataEvaluationClaim,
],
proofData.parentAggregationLastBlockTimestamp,
proofData.finalTimestamp,
lastFinalizedRollingHash,
proofData.l1RollingHash,
lastFinalizedMessageNumber,
proofData.l1RollingHashMessageNumber,
proofData.l2MerkleTreesDepth,
proofData.l2MerkleRoots,
proofData.l2MessagingBlocksOffsets,
],
];
const encodedCall = ethers.concat([
"0x5603c65f",
ethers.AbiCoder.defaultAbiCoder().encode(
[
"bytes",
"uint256",
"tuple(bytes32,uint256,tuple(bytes32,bytes32,bytes32,bytes32,bytes32),uint256,uint256,bytes32,bytes32,uint256,uint256,uint256,bytes32[],bytes)",
],
txData,
),
]);
const { maxFeePerGas, maxPriorityFeePerGas } = await ethers.provider.getFeeData();
const operatorHDSigner = getWalletForIndex(2);
const nonce = await operatorHDSigner.getNonce();
const transaction = Transaction.from({
data: encodedCall,
maxPriorityFeePerGas: maxPriorityFeePerGas!,
maxFeePerGas: maxFeePerGas!,
to: callforwarderAddress,
chainId: (await ethers.provider.getNetwork()).chainId,
type: 2,
nonce: nonce,
value: 0,
gasLimit: 5_000_000,
});
const signedTx = await operatorHDSigner.signTransaction(transaction);
const txResponse = await ethers.provider.broadcastTransaction(signedTx);
const receipt = await ethers.provider.getTransactionReceipt(txResponse.hash);
expect(receipt).is.not.null;
const eventArgs = [
BigInt(proofData.lastFinalizedBlockNumber) + 1n,
finalizationData.endBlockNumber,
proofData.finalShnarf,
finalizationData.parentStateRootHash,
finalStateRootHash,
];
const dataFinalizedLogIndex = 8;
expectEventDirectFromReceiptData(
upgradedContract as BaseContract,
receipt!,
"DataFinalizedV3",
eventArgs,
dataFinalizedLogIndex,
);
const [expectedFinalStateRootHash, lastFinalizedBlockNumber, lastFinalizedState] = await Promise.all([
upgradedContract.stateRootHashes(finalizationData.endBlockNumber),
upgradedContract.currentL2BlockNumber(),
upgradedContract.currentFinalizedState(),
]);
expect(expectedFinalStateRootHash).to.equal(finalizationData.shnarfData.finalStateRootHash);
expect(lastFinalizedBlockNumber).to.equal(finalizationData.endBlockNumber);
expect(lastFinalizedState).to.equal(
generateKeccak256(
["uint256", "bytes32", "uint256"],
[finalizationData.l1RollingHashMessageNumber, finalizationData.l1RollingHash, finalizationData.finalTimestamp],
),
);
}

View File

@@ -0,0 +1,5 @@
export * from "./before";
export * from "./blob";
export * from "./deploy";
export * from "./expect";
export * from "./wallet";

View File

@@ -0,0 +1,9 @@
import { HDNodeWallet, Wallet } from "ethers";
import { config, ethers } from "hardhat";
import { HardhatNetworkHDAccountsConfig } from "hardhat/types";
export const getWalletForIndex = (index: number) => {
const accounts = config.networks.hardhat.accounts as HardhatNetworkHDAccountsConfig;
const signer = HDNodeWallet.fromPhrase(accounts.mnemonic, "", `m/44'/60'/0'/0/${index}`);
return new Wallet(signer.privateKey, ethers.provider);
};