Skip to content

Commit

Permalink
feat: add getBatchPosters (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrstph-dvx authored Jun 20, 2024
1 parent 22dcaf0 commit 21093f7
Show file tree
Hide file tree
Showing 6 changed files with 983 additions and 13 deletions.
166 changes: 166 additions & 0 deletions src/getBatchPosters.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
181 changes: 181 additions & 0 deletions src/getBatchPosters.ts
Original file line number Diff line number Diff line change
@@ -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<Address>, 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<TChain extends Chain | undefined>(
publicClient: PublicClient<Transport, TChain>,
{ rollup, sequencerInbox }: GetBatchPostersParams,
): Promise<GetBatchPostersReturnType> {
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<Address>());

return {
isAccurate,
batchPosters: [...batchPosters],
};
}
Loading

0 comments on commit 21093f7

Please sign in to comment.