feat: added shomei-frontend linea_getProof e2e test (#1077)

* feat: added shomei-frontend linea_getProof e2e test

* feat: revise the e2e test with comments

* feat: make linked library name human readable
This commit is contained in:
jonesho
2025-06-03 15:29:23 +08:00
committed by GitHub
parent cfd3bbd76b
commit 953e7a90a5
13 changed files with 542 additions and 9 deletions

View File

@@ -70,7 +70,7 @@ start-env-with-tracing-v2-extra:
make start-env COMPOSE_PROFILES:=l1,l2 COMPOSE_FILE:=docker/compose-tracing-v2-extra-extension.yml LINEA_PROTOCOL_CONTRACTS_ONLY=true DISABLE_JSON_RPC_PRICING_PROPAGATION=false DISABLE_TYPE2_STATE_PROOF_PROVIDER=false
start-env-with-tracing-v2-ci:
make start-env COMPOSE_FILE=docker/compose-tracing-v2-ci-extension.yml
make start-env COMPOSE_FILE=docker/compose-tracing-v2-ci-extension.yml DISABLE_TYPE2_STATE_PROOF_PROVIDER=false
start-env-with-staterecovery: COMPOSE_PROFILES:=l1,l2,staterecovery
start-env-with-staterecovery: L1_CONTRACT_VERSION:=6

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -368,7 +368,7 @@ services:
image: consensys/linea-shomei:2.3.0
hostname: shomei-frontend
container_name: shomei-frontend
profiles: [ "l2", "l2-bc" ]
profiles: [ "l2", "l2-bc", "external-to-monorepo" ]
depends_on:
zkbesu-shomei:
condition: service_started

View File

@@ -7,6 +7,11 @@ services:
file: compose-spec-l2-services.yml
service: l2-node-besu
shomei-frontend:
extends:
file: compose-spec-l2-services.yml
service: shomei-frontend
postman:
extends:
file: compose-spec-l2-services.yml

View File

@@ -74,7 +74,7 @@ export function generateRandomUUIDv4(): string {
return randomUUID();
}
async function awaitUntil<T>(
export async function awaitUntil<T>(
callback: () => Promise<T>,
stopRetry: (a: T) => boolean,
pollingIntervalMs: number = 500,
@@ -231,6 +231,68 @@ export class LineaBundleClient {
}
}
export class LineaShomeiClient {
private endpoint: URL;
public constructor(endpoint: URL) {
this.endpoint = endpoint;
}
public async rollupGetZkEVMStateMerkleProofV0(
startBlockNumber: number,
endBlockNumber: number,
zkStateManagerVersion: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> {
const request = {
method: "post",
body: JSON.stringify({
jsonrpc: "2.0",
method: "rollup_getZkEVMStateMerkleProofV0",
params: [
{
startBlockNumber,
endBlockNumber,
zkStateManagerVersion,
},
],
id: generateRandomInt(),
}),
};
const response = await fetch(this.endpoint, request);
const responseJson = await response.json();
assert("result" in responseJson);
return responseJson;
}
}
export class LineaShomeiFrontendClient {
private endpoint: URL;
public constructor(endpoint: URL) {
this.endpoint = endpoint;
}
public async lineaGetProof(
address: string,
storageKeys: string[] = [],
blockParameter: string = "latest",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> {
const request = {
method: "post",
body: JSON.stringify({
jsonrpc: "2.0",
method: "linea_getProof",
params: [address, storageKeys, blockParameter],
id: generateRandomInt(),
}),
};
const response = await fetch(this.endpoint, request);
return await response.json();
}
}
export class TransactionExclusionClient {
private endpoint: URL;
@@ -465,6 +527,11 @@ export async function execDockerCommand(command: string, containerName: string):
});
}
export async function getDockerImageTag(containerName: string, imageRepoName: string): Promise<string> {
const inspectJsonOutput = JSON.parse(await execDockerCommand("inspect", containerName));
return inspectJsonOutput[0]["Config"]["Image"].replace(imageRepoName + ":", "");
}
export function generateRoleAssignments(
roles: string[],
defaultAddress: string,

View File

@@ -2,7 +2,12 @@
import { ethers } from "ethers";
import { config } from "../tests-config";
import { deployContract } from "../../common/deployments";
import { DummyContract__factory, TestContract__factory } from "../../typechain";
import {
DummyContract__factory,
Mimc__factory,
SparseMerkleProof__factory,
TestContract__factory,
} from "../../typechain";
import { etherToWei, sendTransactionsToGenerateTrafficWithInterval } from "../../common/utils";
import { EMPTY_CONTRACT_CODE } from "../../common/constants";
import { createTestLogger } from "../logger";
@@ -36,10 +41,11 @@ async function configureOnceOffPrerequisities() {
const to = "0x8D97689C9818892B700e27F316cc3E41e17fBeb9";
const calldata = "0x";
const [dummyContract, l2DummyContract, l2TestContract] = await Promise.all([
const [dummyContract, l2DummyContract, l2TestContract, l2MimcContract] = await Promise.all([
deployContract(new DummyContract__factory(), account, [{ nonce: l1AccountNonce }]),
deployContract(new DummyContract__factory(), l2Account, [{ nonce: l2AccountNonce }]),
deployContract(new TestContract__factory(), l2Account, [{ nonce: l2AccountNonce + 1 }]),
deployContract(new Mimc__factory(), l2Account, [{ nonce: l2AccountNonce + 2 }]),
// Send ETH to the LineaRollup contract
(
@@ -51,7 +57,16 @@ async function configureOnceOffPrerequisities() {
).wait(),
]);
const l2MimcContractAddress = await l2MimcContract.getAddress();
const l2SparseMerkleProofContract = await deployContract(
new SparseMerkleProof__factory({ "contracts/Mimc.sol:Mimc": l2MimcContractAddress }),
l2Account,
[{ nonce: l2AccountNonce + 3 }],
);
logger.info(`L1 Dummy contract deployed. address=${await dummyContract.getAddress()}`);
logger.info(`L2 Dummy contract deployed. address=${await l2DummyContract.getAddress()}`);
logger.info(`L2 Test contract deployed. address=${await l2TestContract.getAddress()}`);
logger.info(`L2 Mimc contract deployed. address=${l2MimcContractAddress}`);
logger.info(`L2 SparseMerkleProof contract deployed. address=${await l2SparseMerkleProofContract.getAddress()}`);
}

View File

@@ -43,6 +43,7 @@ const config: Config = {
tokenBridgeAddress: "",
l2TokenAddress: "",
l2TestContractAddress: "",
l2SparseMerkleProofAddress: "",
accountManager: new EnvironmentBasedAccountManager(
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),
L2_WHALE_ACCOUNTS,

View File

@@ -33,12 +33,11 @@ const config: Config = {
besuNodeRpcUrl: L2_BESU_NODE_RPC_URL,
chainId: 1337,
l2MessageServiceAddress: "0xe537D669CA013d86EBeF1D64e40fC74CADC91987",
// Nonce 10
l2TestContractAddress: "0x997FC3aF1F193Cbdc013060076c67A13e218980e",
// Nonce 9
dummyContractAddress: "0xE4392c8ecC46b304C83cDB5edaf742899b1bda93",
l2TestContractAddress: "0x997FC3aF1F193Cbdc013060076c67A13e218980e", // Nonce 10
dummyContractAddress: "0xE4392c8ecC46b304C83cDB5edaf742899b1bda93", // Nonce 9
tokenBridgeAddress: "0x5C95Bcd50E6D1B4E3CDC478484C9030Ff0a7D493",
l2TokenAddress: "0xCC1B08B17301e090cbb4c1F5598Cbaa096d591FB",
l2SparseMerkleProofAddress: "0x7917AbB0cDbf3D3C4057d6a2808eE85ec16260C1", // Nonce 12
accountManager: new GenesisBasedAccountManager(
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),
path.resolve(

View File

@@ -42,6 +42,7 @@ const config: Config = {
tokenBridgeAddress: "0x93DcAdf238932e6e6a85852caC89cBd71798F463",
l2TokenAddress: "",
l2TestContractAddress: "",
l2SparseMerkleProofAddress: "",
accountManager: new EnvironmentBasedAccountManager(
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),
L2_WHALE_ACCOUNTS,

View File

@@ -11,6 +11,8 @@ import {
LineaRollupV6__factory,
ProxyAdmin,
ProxyAdmin__factory,
SparseMerkleProof,
SparseMerkleProof__factory,
TestContract,
TestContract__factory,
TestERC20,
@@ -227,6 +229,10 @@ export default class TestSetup {
}
}
public getL2SparseMerkleProofContract(): SparseMerkleProof {
return SparseMerkleProof__factory.connect(this.config.L2.l2SparseMerkleProofAddress, this.getL2Provider());
}
public getL1AccountManager(): AccountManager {
return this.config.L1.accountManager;
}

View File

@@ -20,6 +20,7 @@ export type BaseL2Config = BaseConfig & {
besuNodeRpcUrl?: URL;
tokenBridgeAddress: string;
l2TokenAddress: string;
l2SparseMerkleProofAddress: string;
shomeiEndpoint?: URL;
shomeiFrontendEndpoint?: URL;
sequencerEndpoint?: URL;

View File

@@ -0,0 +1,85 @@
import { describe, it } from "@jest/globals";
import { config } from "./config/tests-config";
import { awaitUntil, getDockerImageTag, LineaShomeiClient, LineaShomeiFrontendClient } from "./common/utils";
describe("Shomei Linea get proof test suite", () => {
const lineaRollupV6 = config.getLineaRollupContract();
const shomeiFrontendEndpoint = config.getShomeiFrontendEndpoint();
const shomeiEndpoint = config.getShomeiEndpoint();
const lineaShomeiFrontenedClient = new LineaShomeiFrontendClient(shomeiFrontendEndpoint!);
const lineaShomeiClient = new LineaShomeiClient(shomeiEndpoint!);
it.concurrent(
"Call linea_getProof to Shomei frontend node and get a valid proof",
async () => {
const shomeiImageTag = await getDockerImageTag("shomei-frontend", "consensys/linea-shomei");
logger.debug(`shomeiImageTag=${shomeiImageTag}`);
const currentL2BlockNumber = await awaitUntil(
async () => {
try {
return await lineaRollupV6.currentL2BlockNumber({
blockTag: "finalized",
});
} catch (err) {
if (!(err as Error).message.includes("could not decode result data")) {
throw err;
} // else means the currentL2BlockNumber is not ready in the L1 rollup contract yet
return -1n;
}
},
(currentL2BlockNumber: bigint) => currentL2BlockNumber > 1n,
2000,
100000,
);
expect(currentL2BlockNumber).toBeGreaterThan(1n);
logger.debug(`currentL2BlockNumber=${currentL2BlockNumber}`);
const provingAddress = "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"; // from genesis file
const getProofResponse = await awaitUntil(
async () =>
lineaShomeiFrontenedClient.lineaGetProof(provingAddress, [], "0x" + currentL2BlockNumber!.toString(16)),
(getProofResponse) => getProofResponse?.result,
2000,
100000,
);
const {
result: { zkEndStateRootHash },
} = await lineaShomeiClient.rollupGetZkEVMStateMerkleProofV0(
Number(currentL2BlockNumber),
Number(currentL2BlockNumber),
shomeiImageTag,
);
expect(zkEndStateRootHash).toBeDefined();
const l2SparseMerkleProofContract = config.getL2SparseMerkleProofContract();
const isValid = await l2SparseMerkleProofContract.verifyProof(
getProofResponse.result.accountProof.proof.proofRelatedNodes,
getProofResponse.result.accountProof.leafIndex,
zkEndStateRootHash,
);
expect(isValid).toBeTruthy();
// Modify the last hex character of the original state root hash should verify the same proof as invalid
const modifiedStateRootHash =
zkEndStateRootHash.slice(0, -1) + ((parseInt(zkEndStateRootHash.slice(-1), 16) + 1) % 16).toString(16);
logger.debug(`originalStateRootHash=${zkEndStateRootHash}`);
logger.debug(`modifiedStateRootHash=${modifiedStateRootHash}`);
const isInvalid = !(await l2SparseMerkleProofContract.verifyProof(
getProofResponse.result.accountProof.proof.proofRelatedNodes,
getProofResponse.result.accountProof.leafIndex,
modifiedStateRootHash,
));
expect(isInvalid).toBeTruthy();
},
100_000,
);
});