diff --git a/src/getBatchPosters.integration.test.ts b/src/getBatchPosters.integration.test.ts new file mode 100644 index 00000000..b846d00f --- /dev/null +++ b/src/getBatchPosters.integration.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect } from 'vitest'; +import { Address, createPublicClient, http } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; + +import { nitroTestnodeL2 } from './chains'; +import { sequencerInboxActions } from './decorators/sequencerInboxActions'; +import { getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts } from './testHelpers'; +import { getBatchPosters } from './getBatchPosters'; + +const { l3RollupOwner } = getNitroTestnodePrivateKeyAccounts(); +const { l3Rollup, l3UpgradeExecutor, l3SequencerInbox } = getInformationFromTestnode(); + +const client = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), +}).extend( + sequencerInboxActions({ + sequencerInbox: l3SequencerInbox, + }), +); + +async function setBatchPoster(batchPoster: Address, state: boolean) { + const tx = await client.sequencerInboxPrepareTransactionRequest({ + functionName: 'setIsBatchPoster', + args: [batchPoster, state], + account: l3RollupOwner.address, + upgradeExecutor: l3UpgradeExecutor, + sequencerInbox: l3SequencerInbox, + }); + + const txHash = await client.sendRawTransaction({ + serializedTransaction: await l3RollupOwner.signTransaction(tx), + }); + + await client.waitForTransactionReceipt({ + hash: txHash, + }); +} + +// Tests can be enabled once we run one node per integration test +describe.skip('successfully get batch posters', () => { + it('when disabling the same batch posters multiple time', async () => { + const randomAccount = privateKeyToAccount(generatePrivateKey()).address; + + const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } = + await getBatchPosters(client, { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }); + + // By default, chains from nitro testnode has 1 batch poster + expect(initialBatchPosters).toHaveLength(1); + expect(isAccurateInitially).toBeTruthy(); + + await setBatchPoster(randomAccount, false); + await setBatchPoster(randomAccount, false); + + const { isAccurate: isStillAccurate, batchPosters: newBatchPosters } = await getBatchPosters( + client, + { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }, + ); + // Setting the same batch poster multiple time to false doesn't add new batch posters + expect(newBatchPosters).toEqual(initialBatchPosters); + expect(isStillAccurate).toBeTruthy(); + + await setBatchPoster(randomAccount, true); + const { batchPosters, isAccurate } = await getBatchPosters(client, { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }); + + expect(batchPosters).toEqual(initialBatchPosters.concat(randomAccount)); + expect(isAccurate).toBeTruthy(); + + // Reset state for future tests + await setBatchPoster(randomAccount, false); + const { isAccurate: isAccurateFinal, batchPosters: batchPostersFinal } = await getBatchPosters( + client, + { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }, + ); + expect(batchPostersFinal).toEqual(initialBatchPosters); + expect(isAccurateFinal).toBeTruthy(); + }); + + it('when enabling the same batch poster multiple time', async () => { + const randomAccount = privateKeyToAccount(generatePrivateKey()).address; + + const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } = + await getBatchPosters(client, { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }); + // By default, chains from nitro testnode has 1 batch poster + expect(initialBatchPosters).toHaveLength(1); + expect(isAccurateInitially).toBeTruthy(); + + await setBatchPoster(randomAccount, true); + await setBatchPoster(randomAccount, true); + const { isAccurate: isStillAccurate, batchPosters: newBatchPosters } = await getBatchPosters( + client, + { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }, + ); + + expect(newBatchPosters).toEqual(initialBatchPosters.concat(randomAccount)); + expect(isStillAccurate).toBeTruthy(); + + // Reset state for futures tests + await setBatchPoster(randomAccount, false); + const { batchPosters, isAccurate } = await getBatchPosters(client, { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }); + expect(batchPosters).toEqual(initialBatchPosters); + expect(isAccurate).toBeTruthy(); + }); + + it('when adding an existing batch poster', async () => { + const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } = + await getBatchPosters(client, { rollup: l3Rollup, sequencerInbox: l3SequencerInbox }); + expect(initialBatchPosters).toHaveLength(1); + expect(isAccurateInitially).toBeTruthy(); + + const firstBatchPoster = initialBatchPosters[0]; + await setBatchPoster(firstBatchPoster, true); + + const { isAccurate, batchPosters } = await getBatchPosters(client, { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }); + expect(batchPosters).toEqual(initialBatchPosters); + expect(isAccurate).toBeTruthy(); + }); + + it('when removing an existing batch poster', async () => { + const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } = + await getBatchPosters(client, { rollup: l3Rollup, sequencerInbox: l3SequencerInbox }); + expect(initialBatchPosters).toHaveLength(1); + expect(isAccurateInitially).toBeTruthy(); + + const lastBatchPoster = initialBatchPosters[initialBatchPosters.length - 1]; + await setBatchPoster(lastBatchPoster, false); + const { isAccurate, batchPosters } = await getBatchPosters(client, { + rollup: l3Rollup, + sequencerInbox: l3SequencerInbox, + }); + expect(batchPosters).toEqual(initialBatchPosters.slice(0, -1)); + expect(isAccurate).toBeTruthy(); + + await setBatchPoster(lastBatchPoster, true); + const { isAccurate: isAccurateFinal, batchPosters: batchPostersFinal } = await getBatchPosters( + client, + { rollup: l3Rollup, sequencerInbox: l3SequencerInbox }, + ); + expect(batchPostersFinal).toEqual(initialBatchPosters); + expect(isAccurateFinal).toBeTruthy(); + }); +}); diff --git a/src/getBatchPosters.ts b/src/getBatchPosters.ts new file mode 100644 index 00000000..7dd3e50c --- /dev/null +++ b/src/getBatchPosters.ts @@ -0,0 +1,181 @@ +import { + Address, + Chain, + Hex, + PublicClient, + Transport, + decodeFunctionData, + getAbiItem, + getFunctionSelector, +} from 'viem'; +import { rollupCreator, upgradeExecutor } from './contracts'; +import { safeL2ABI, sequencerInboxABI } from './abi'; +import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash'; + +const createRollupABI = getAbiItem({ abi: rollupCreator.abi, name: 'createRollup' }); +const createRollupFunctionSelector = getFunctionSelector(createRollupABI); + +const setIsBatchPosterABI = getAbiItem({ abi: sequencerInboxABI, name: 'setIsBatchPoster' }); +const setIsBatchPosterFunctionSelector = getFunctionSelector(setIsBatchPosterABI); + +const executeCallABI = getAbiItem({ abi: upgradeExecutor.abi, name: 'executeCall' }); +const upgradeExecutorExecuteCallFunctionSelector = getFunctionSelector(executeCallABI); + +const execTransactionABI = getAbiItem({ abi: safeL2ABI, name: 'execTransaction' }); +const safeL2FunctionSelector = getFunctionSelector(execTransactionABI); + +const ownerFunctionCalledEventAbi = getAbiItem({ + abi: sequencerInboxABI, + name: 'OwnerFunctionCalled', +}); + +function getBatchPostersFromFunctionData< + TAbi extends (typeof createRollupABI)[] | (typeof setIsBatchPosterABI)[], +>({ abi, data }: { abi: TAbi; data: Hex }) { + const { args } = decodeFunctionData({ + abi, + data, + }); + return args; +} + +function updateAccumulator(acc: Set
, input: Hex) { + const [batchPoster, isAdd] = getBatchPostersFromFunctionData({ + abi: [setIsBatchPosterABI], + data: input, + }); + + if (isAdd) { + acc.add(batchPoster); + } else { + acc.delete(batchPoster); + } + + return acc; +} + +export type GetBatchPostersParams = { + /** Address of the rollup we're getting list of batch posters from */ + rollup: Address; + /** Address of the sequencerInbox we're getting logs from */ + sequencerInbox: Address; +}; +export type GetBatchPostersReturnType = { + /** + * If logs contain unknown signature, batch posters list might: + * - contain false positives (batch posters that were removed, but returned as batch poster) + * - contain false negatives (batch posters that were added, but not present in the list) + */ + isAccurate: boolean; + /** List of batch posters for the given rollup */ + batchPosters: Address[]; +}; + +/** + * + * @param {PublicClient} publicClient - The chain Viem Public Client + * @param {GetBatchPostersParams} GetBatchPostersParams {@link GetBatchPostersParams} + * + * @returns Promise<{@link GetBatchPostersReturnType}> + * + * @remarks Batch posters list is not guaranteed to be exhaustive if the `isAccurate` flag is false. + * It might contain false positive (batch posters that were removed, but returned as batch poster) + * or false negative (batch posters that were added, but not present in the list) + * + * @example + * const { isAccurate, batchPosters } = getBatchPosters(client, { + * rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336', + * sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1' + * }); + * + * if (isAccurate) { + * // batch posters were all fetched properly + * } else { + * // batch posters list is not guaranteed to be accurate + * } + */ +export async function getBatchPosters( + publicClient: PublicClient, + { rollup, sequencerInbox }: GetBatchPostersParams, +): Promise { + let blockNumber: bigint | 'earliest'; + let createRollupTransactionHash: Address | null = null; + try { + createRollupTransactionHash = await createRollupFetchTransactionHash({ + rollup, + publicClient, + }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash: createRollupTransactionHash, + }); + blockNumber = receipt.blockNumber; + } catch (e) { + blockNumber = 'earliest'; + } + + const sequencerInboxEvents = await publicClient.getLogs({ + address: sequencerInbox, + event: ownerFunctionCalledEventAbi, + args: { id: 1n }, + fromBlock: blockNumber, + toBlock: 'latest', + }); + + const events = createRollupTransactionHash + ? [{ transactionHash: createRollupTransactionHash }, ...sequencerInboxEvents] + : sequencerInboxEvents; + const txs = await Promise.all( + events.map((event) => + publicClient.getTransaction({ + hash: event.transactionHash, + }), + ), + ); + + let isAccurate = true; + const batchPosters = txs.reduce((acc, tx) => { + const txSelectedFunction = tx.input.slice(0, 10); + + switch (txSelectedFunction) { + case createRollupFunctionSelector: { + const [{ batchPoster }] = getBatchPostersFromFunctionData({ + abi: [createRollupABI], + data: tx.input, + }); + + return new Set([...acc, batchPoster]); + } + case setIsBatchPosterFunctionSelector: { + return updateAccumulator(acc, tx.input); + } + case upgradeExecutorExecuteCallFunctionSelector: { + const { args: executeCallCalldata } = decodeFunctionData({ + abi: [executeCallABI], + data: tx.input, + }); + return updateAccumulator(acc, executeCallCalldata[1]); + } + case safeL2FunctionSelector: { + const { args: execTransactionCalldata } = decodeFunctionData({ + abi: [execTransactionABI], + data: tx.input, + }); + const { args: executeCallCalldata } = decodeFunctionData({ + abi: [executeCallABI], + data: execTransactionCalldata[2], + }); + return updateAccumulator(acc, executeCallCalldata[1]); + } + default: { + console.warn(`[getBatchPosters] unknown 4bytes, tx id: ${tx.hash}`); + isAccurate = false; + return acc; + } + } + }, new Set
()); + + return { + isAccurate, + batchPosters: [...batchPosters], + }; +} diff --git a/src/getBatchPosters.unit.test.ts b/src/getBatchPosters.unit.test.ts new file mode 100644 index 00000000..1dd965bf --- /dev/null +++ b/src/getBatchPosters.unit.test.ts @@ -0,0 +1,588 @@ +import { + Address, + EIP1193RequestFn, + Hex, + createPublicClient, + createTransport, + encodeFunctionData, + http, +} from 'viem'; +import { arbitrum, arbitrumSepolia } from 'viem/chains'; +import { it, expect, vi, describe } from 'vitest'; +import { safeL2ABI, sequencerInboxABI } from './abi'; +import { sequencerInboxPrepareFunctionData } from './sequencerInboxPrepareTransactionRequest'; +import { getBatchPosters } from './getBatchPosters'; + +const client = createPublicClient({ + chain: arbitrum, + transport: http(), +}); + +function mockLog(transactionHash: string) { + return { + address: '0x193e2887031c148ab54f5e856ea51ae521661200', + args: { id: 1n }, + blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f', + blockNumber: 55635757n, + data: '0x', + eventName: 'OwnerFunctionCalled', + logIndex: 42, + removed: false, + topics: [ + '0xea8787f128d10b2cc0317b0c3960f9ad447f7f6c1ed189db1083ccffd20f456e', + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + transactionHash, + transactionIndex: 3, + }; +} +function mockTransaction(data: Hex) { + return { + accessList: [], + blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f', + blockNumber: 55635757n, + chainId: 421614, + from: '0xfd5735380689a53e6b048e980f34cb94be9fd0c7', + gas: 7149526n, + gasPrice: 147390000n, + hash: '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10', + input: data, + maxFeePerGas: 174300000n, + maxPriorityFeePerGas: 0n, + nonce: 82, + r: '0x49cabfb3327a9f8c0fb8da0d8b3b81daf2f46d09ebc3c640b09d788f51159b7f', + s: '0x2ee5ae568a892beed3a763c33c8da4a3368be0b2422f2785fe00be76ecad08f8', + to: '0x06e341073b2749e0bb9912461351f716decda9b0', + transactionIndex: 3, + type: 'eip1559', + typeHex: '0x2', + v: 1n, + value: 0n, + yParity: 1, + }; +} +function mockData({ + logs, + method, + params, +}: { + logs: { + [transactionHash: string]: Hex; + }; + method: + | 'eth_getLogs' + | 'eth_getTransactionByHash' + | 'eth_getTransactionReceipt' + | 'eth_blockNumber'; + params: string; +}) { + if (method === 'eth_getLogs') { + return Object.keys(logs).map((transactionHash) => mockLog(transactionHash)); + } + + if (method === 'eth_getTransactionByHash') { + return mockTransaction(logs[params]); + } + + if (method === 'eth_getTransactionReceipt') { + return { + blockNumber: 36723964, + }; + } + + if (method === 'eth_blockNumber') { + return 36723964; + } + + return null; +} + +// Taken from https://sepolia.arbiscan.io/tx/0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10 (Input data) +const validInput = + '0xcb73d6e2000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000725d217057e509dd284ee0e13ac846cfea0b7bb100000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000019999000000000000000000000000d1fe5b0a963a0e557aabb59cb61ffdd568b4605c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a00000754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17000000000000000000000000fd5735380689a53e6b048e980f34cb94be9fd0c700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe22e119e00000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001680000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e1000000000000000000000000000000000000000000000000000000000000002757b22686f6d657374656164426c6f636b223a302c2264616f466f726b426c6f636b223a6e756c6c2c2264616f466f726b537570706f7274223a747275652c22656970313530426c6f636b223a302c2265697031353048617368223a22307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030222c22656970313535426c6f636b223a302c22656970313538426c6f636b223a302c2262797a616e7469756d426c6f636b223a302c22636f6e7374616e74696e6f706c65426c6f636b223a302c2270657465727362757267426c6f636b223a302c22697374616e62756c426c6f636b223a302c226d756972476c6163696572426c6f636b223a302c226265726c696e426c6f636b223a302c226c6f6e646f6e426c6f636b223a302c22636c69717565223a7b22706572696f64223a302c2265706f6368223a307d2c22617262697472756d223a7b22456e61626c654172624f53223a747275652c22416c6c6f774465627567507265636f6d70696c6573223a66616c73652c2244617461417661696c6162696c697479436f6d6d6974746565223a747275652c22496e697469616c4172624f5356657273696f6e223a31312c2247656e65736973426c6f636b4e756d223a302c224d6178436f646553697a65223a32343537362c224d6178496e6974436f646553697a65223a34393135322c22496e697469616c436861696e4f776e6572223a22307846643537333533383036383941353365364230343865393830463334634239346265396644306337227d2c22636861696e4964223a36383231393137393432327d000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006a23ccc1c36d2aaa98aef2a4471cf807dd22e45b'; +const upgradeExecutorAddress = '0x314f3c2f9803f8828a1050127be5bb29a4940f29'; +const rollupAddress = '0xe0875cbd144fe66c015a95e5b2d2c15c3b612179'; +const sequencerInboxAddress = '0x041f85dd87c46b941dc9b15c6628b19ee5358485'; + +function setBatchPosterHelper(args: [Address, boolean]) { + return encodeFunctionData({ + abi: sequencerInboxABI, + functionName: 'setIsBatchPoster', + args, + }); +} +function upgradeExecutorSetBatchPosterHelper(args: [Address, boolean]) { + return sequencerInboxPrepareFunctionData({ + sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1', + functionName: 'setIsBatchPoster', + args, + abi: sequencerInboxABI, + upgradeExecutor: upgradeExecutorAddress, + }).data; +} +function safeSetBatchPosterHelper(args: [Address, boolean]) { + const bytes = upgradeExecutorSetBatchPosterHelper(args); + return encodeFunctionData({ + abi: safeL2ABI, + functionName: 'execTransaction', + args: [ + rollupAddress, + 0n, + bytes, + 0, + 0n, + 0n, + 0n, + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x000000000000000000000000AD46AD093FD26B4464B756AC1B56985AF87399E7000000000000000000000000000000000000000000000000000000000000000001ABFD0989138206FEC57AE925D0B8CC27ECBB4484DC4CE1133D90E2BA4A644E6179F6640360B48976145461BBC820378F733421ADFCF78730FD20408BB10C284F1B', + ], + }); +} + +it('getBatchPosters returns all batch posters (Xai)', async () => { + const { isAccurate, batchPosters } = await getBatchPosters(client, { + rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336', + sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1', + }); + expect(batchPosters).toEqual(['0x7F68dba68E72a250004812fe04F1123Fca89aBa9']); + expect(isAccurate).toBeTruthy(); +}); + +describe('createRollupFunctionSelector', () => { + it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': validInput, + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + chain: arbitrumSepolia, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: '0x995a9d3ca121d48d21087ede20bc8acb2398c8b1', + }); + + expect(batchPosters).toEqual(['0x725D217057e509Dd284eE0e13aC846Cfea0B7BB1']); + expect(isAccurate).toBeTruthy(); + }); + + it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': validInput, + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef', + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual(['0x725D217057e509Dd284eE0e13aC846Cfea0B7BB1']); + expect(isAccurate).toBeFalsy(); + }); +}); + +describe('setBatchPosterFunctionSelector', () => { + it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + setBatchPosterHelper(['0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8', true]), + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': + setBatchPosterHelper(['0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', true]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([ + '0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8', + '0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', + ]); + expect(isAccurate).toBeTruthy(); + }); + + it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + setBatchPosterHelper(['0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8', true]), + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef', + '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a': + setBatchPosterHelper(['0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', true]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([ + '0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8', + '0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', + ]); + expect(isAccurate).toBeFalsy(); + }); +}); + +describe('upgradeExecutorExecuteCallFunctionSelector', () => { + it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + upgradeExecutorSetBatchPosterHelper([ + '0x81209B63188f27339441B741518fF73F18b4Efd4', + true, + ]), + '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a': + upgradeExecutorSetBatchPosterHelper([ + '0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', + true, + ]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([ + '0x81209B63188f27339441B741518fF73F18b4Efd4', + '0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', + ]); + expect(isAccurate).toBeTruthy(); + }); + + it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef', + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + upgradeExecutorSetBatchPosterHelper([ + '0x81209B63188f27339441B741518fF73F18b4Efd4', + true, + ]), + '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a': + upgradeExecutorSetBatchPosterHelper([ + '0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', + false, + ]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual(['0x81209B63188f27339441B741518fF73F18b4Efd4']); + expect(isAccurate).toBeFalsy(); + }); +}); + +describe('safeL2FunctionSelector', () => { + it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + safeSetBatchPosterHelper(['0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962', true]), + '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a': + safeSetBatchPosterHelper(['0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', false]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual(['0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962']); + expect(isAccurate).toBeTruthy(); + }); + + it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => { + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef', + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + safeSetBatchPosterHelper(['0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962', true]), + '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a': + safeSetBatchPosterHelper(['0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', true]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([ + '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962', + '0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', + ]); + expect(isAccurate).toBeFalsy(); + }); +}); + +describe('Detect batch posters added or removed multiple times', () => { + it('when disabling the same batch poster multiple time', async () => { + const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962'; + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + safeSetBatchPosterHelper([batchPoster, false]), + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': + safeSetBatchPosterHelper([batchPoster, false]), + '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a': + safeSetBatchPosterHelper([batchPoster, true]), + '0x1acada0382b0ae715c41365c71b780871ec5adcfced8e57f0ae009c4b8738e2a': + safeSetBatchPosterHelper([batchPoster, false]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([]); + expect(isAccurate).toBeTruthy(); + }); + it('when enabling the same batch posters multiple time', async () => { + const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962'; + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + safeSetBatchPosterHelper([batchPoster, true]), + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': + safeSetBatchPosterHelper([batchPoster, true]), + '0x1acada0382b0ae715c41365c71b780871ec5adcfced8e57f0ae009c4b8738e2a': + safeSetBatchPosterHelper([batchPoster, false]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([]); + expect(isAccurate).toBeTruthy(); + }); + it('when adding an existing batch poster', async () => { + const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962'; + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + safeSetBatchPosterHelper([batchPoster, true]), + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': + safeSetBatchPosterHelper([batchPoster, true]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([batchPoster]); + expect(isAccurate).toBeTruthy(); + }); + it('when removing an existing batchPoster', async () => { + const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962'; + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return mockData({ + logs: { + '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': + safeSetBatchPosterHelper([batchPoster, false]), + '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': + safeSetBatchPosterHelper([batchPoster, false]), + '0x1acada0382b0ae715c41365c71b780871ec5adcfced8e57f0ae009c4b8738e2a': + safeSetBatchPosterHelper([batchPoster, true]), + }, + method, + params, + }); + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + }); + + const { batchPosters, isAccurate } = await getBatchPosters(mockClient, { + rollup: rollupAddress, + sequencerInbox: sequencerInboxAddress, + }); + + expect(batchPosters).toEqual([batchPoster]); + expect(isAccurate).toBeTruthy(); + }); +}); diff --git a/src/getValidators.unit.test.ts b/src/getValidators.unit.test.ts index e98fbb34..18186056 100644 --- a/src/getValidators.unit.test.ts +++ b/src/getValidators.unit.test.ts @@ -7,7 +7,7 @@ import { encodeFunctionData, http, } from 'viem'; -import { arbitrum } from 'viem/chains'; +import { arbitrum, arbitrumSepolia } from 'viem/chains'; import { it, expect, vi, describe } from 'vitest'; import { getValidators } from './getValidators'; import { rollupAdminLogicABI, safeL2ABI } from './abi'; @@ -23,7 +23,7 @@ function mockLog(transactionHash: string) { address: '0x193e2887031c148ab54f5e856ea51ae521661200', args: { id: 6n }, blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f', - blockNumber: 36723964n, + blockNumber: 35635757n, data: '0x', eventName: 'OwnerFunctionCalled', logIndex: 42, @@ -40,7 +40,7 @@ function mockTransaction(data: Hex) { return { accessList: [], blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f', - blockNumber: 36723964n, + blockNumber: 35635757n, chainId: 421614, from: '0xfd5735380689a53e6b048e980f34cb94be9fd0c7', gas: 7149526n, @@ -69,7 +69,11 @@ function mockData({ logs: { [transactionHash: string]: Hex; }; - method: 'eth_getLogs' | 'eth_getTransactionByHash'; + method: + | 'eth_getLogs' + | 'eth_getTransactionByHash' + | 'eth_getTransactionReceipt' + | 'eth_blockNumber'; params: string; }) { if (method === 'eth_getLogs') { @@ -80,6 +84,16 @@ function mockData({ return mockTransaction(logs[params]); } + if (method === 'eth_getTransactionReceipt') { + return { + blockNumber: 35635757, + }; + } + + if (method === 'eth_blockNumber') { + return 35635757; + } + return null; } @@ -133,7 +147,7 @@ it('getValidators return all validators (Xai)', async () => { }); describe('createRollupFunctionSelector', () => { - it('getValidators return all validators with complete flag set to true', async () => { + it('getValidators return all validators with isAccurate flag set to true', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -152,6 +166,7 @@ describe('createRollupFunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -162,7 +177,7 @@ describe('createRollupFunctionSelector', () => { expect(isAccurate).toBeTruthy(); }); - it('getValidators return all validators with complete flag set to false', async () => { + it('getValidators return all validators with isAccurate flag set to false', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -182,6 +197,7 @@ describe('createRollupFunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -194,7 +210,7 @@ describe('createRollupFunctionSelector', () => { }); describe('setValidatorFunctionSelector', () => { - it('getValidators return all validators with complete flag set to true', async () => { + it('getValidators return all validators with isAccurate flag set to true', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -222,6 +238,7 @@ describe('setValidatorFunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -235,7 +252,7 @@ describe('setValidatorFunctionSelector', () => { expect(isAccurate).toBeTruthy(); }); - it('getValidators return all validators with complete flag set to false', async () => { + it('getValidators return all validators with isAccurate flag set to false', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -264,6 +281,7 @@ describe('setValidatorFunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -279,7 +297,7 @@ describe('setValidatorFunctionSelector', () => { }); describe('upgradeExecutorExecuteCallFunctionSelector', () => { - it('getValidators return all validators with complete flag set to true', async () => { + it('getValidators return all validators with isAccurate flag set to true', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -310,6 +328,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -324,7 +343,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => { expect(isAccurate).toBeTruthy(); }); - it('getValidators return all validators with complete flag set to false', async () => { + it('getValidators return all validators with isAccurate flag set to false', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -356,6 +375,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -368,7 +388,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => { }); describe('safeL2FunctionSelector', () => { - it('getValidators return all validators with complete flag set to true', async () => { + it('getValidators return all validators with isAccurate flag set to true', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -396,6 +416,7 @@ describe('safeL2FunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -409,7 +430,7 @@ describe('safeL2FunctionSelector', () => { expect(isAccurate).toBeTruthy(); }); - it('getValidators return all validators with complete flag set to false', async () => { + it('getValidators return all validators with isAccurate flag set to false', async () => { const mockTransport = () => createTransport({ key: 'mock', @@ -438,6 +459,7 @@ describe('safeL2FunctionSelector', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -480,6 +502,7 @@ describe('Detect validators added or removed multiple times', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -514,6 +537,7 @@ describe('Detect validators added or removed multiple times', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -546,6 +570,7 @@ describe('Detect validators added or removed multiple times', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { @@ -580,6 +605,7 @@ describe('Detect validators added or removed multiple times', () => { const mockClient = createPublicClient({ transport: mockTransport, + chain: arbitrumSepolia, }); const { validators, isAccurate } = await getValidators(mockClient, { diff --git a/src/index.ts b/src/index.ts index 44df6553..e53666c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,11 @@ import { SequencerInboxMaxTimeVariation, } from './getDefaultSequencerInboxMaxTimeVariation'; import { getValidators, GetValidatorsParams, GetValidatorsReturnType } from './getValidators'; +import { + getBatchPosters, + GetBatchPostersParams, + GetBatchPostersReturnType, +} from './getBatchPosters'; export { arbOwnerPublicActions, @@ -186,4 +191,8 @@ export { getValidators, GetValidatorsParams, GetValidatorsReturnType, + // + getBatchPosters, + GetBatchPostersParams, + GetBatchPostersReturnType, }; diff --git a/src/sequencerInboxPrepareTransactionRequest.ts b/src/sequencerInboxPrepareTransactionRequest.ts index 241f430f..2e539566 100644 --- a/src/sequencerInboxPrepareTransactionRequest.ts +++ b/src/sequencerInboxPrepareTransactionRequest.ts @@ -36,7 +36,7 @@ type SequencerInboxPrepareFunctionDataParameters( +export function sequencerInboxPrepareFunctionData( params: SequencerInboxPrepareFunctionDataParameters, ) { const { upgradeExecutor } = params;