Skip to content
2 changes: 1 addition & 1 deletion src/contracts/Rollup/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// export the latest version
export * from './v2.1';
export * from './v3.1';
2 changes: 1 addition & 1 deletion src/contracts/SequencerInbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// export the latest version
export * from './v2.1';
export * from './v3.1';
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const client = createPublicClient({
}),
);

it.skip('successfully set validators', async () => {
it('successfully set validators', async () => {
const randomAccounts = [
privateKeyToAccount(generatePrivateKey()).address,
privateKeyToAccount(generatePrivateKey()).address,
Expand All @@ -31,7 +31,10 @@ it.skip('successfully set validators', async () => {
rollup: l3Rollup,
},
);
expect(initialValidators).toHaveLength(10);
// By default, chains from nitro testnode has 11 validators
// https://github.com/OffchainLabs/nitro-testnode/blob/de8cf4edec0d12e5ef1b7623e54e35ddb579ff0b/test-node.bash#L634
// https://github.com/OffchainLabs/nitro-contracts/blob/47a9c034bc082256d498f8031fab1a37c37a123c/scripts/rollupCreation.ts#L250-L257
expect(initialValidators).toHaveLength(11);
expect(isAccurateInitially).toBeTruthy();

const tx = await client.rollupAdminLogicPrepareTransactionRequest({
Expand Down
2 changes: 1 addition & 1 deletion src/getBatchPosters.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function setBatchPoster(batchPoster: Address, state: boolean) {
}

// Tests can be enabled once we run one node per integration test
describe.skip('successfully get batch posters', () => {
describe('successfully get batch posters', () => {
it('when disabling the same batch posters multiple time', async () => {
const randomAccount = privateKeyToAccount(generatePrivateKey()).address;

Expand Down
13 changes: 13 additions & 0 deletions src/getBatchPosters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getFunctionSelector,
} from 'viem';

import { rollupCreatorABI as rollupCreatorV3Dot1ABI } from './contracts/RollupCreator';
import { rollupCreatorABI as rollupCreatorV2Dot1ABI } from './contracts/RollupCreator/v2.1';
import { rollupCreatorABI as rollupCreatorV1Dot1ABI } from './contracts/RollupCreator/v1.1';
import { sequencerInboxABI } from './contracts/SequencerInbox';
Expand All @@ -18,6 +19,9 @@ import { gnosisSafeL2ABI } from './contracts/GnosisSafeL2';
import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash';
import { getLogsWithBatching } from './utils/getLogsWithBatching';

const createRollupV3Dot1ABI = getAbiItem({ abi: rollupCreatorV3Dot1ABI, name: 'createRollup' });
const createRollupV3Dot1FunctionSelector = getFunctionSelector(createRollupV3Dot1ABI);

const createRollupV2Dot1ABI = getAbiItem({ abi: rollupCreatorV2Dot1ABI, name: 'createRollup' });
const createRollupV2Dot1FunctionSelector = getFunctionSelector(createRollupV2Dot1ABI);

Expand All @@ -40,6 +44,7 @@ const ownerFunctionCalledEventAbi = getAbiItem({

function getBatchPostersFromFunctionData<
TAbi extends
| (typeof createRollupV3Dot1ABI)[]
| (typeof createRollupV2Dot1ABI)[]
| (typeof createRollupV1Dot1ABI)[]
| (typeof setIsBatchPosterABI)[],
Expand Down Expand Up @@ -148,6 +153,14 @@ export async function getBatchPosters<TChain extends Chain>(
const txSelectedFunction = tx.input.slice(0, 10);

switch (txSelectedFunction) {
case createRollupV3Dot1FunctionSelector: {
const [{ batchPosters }] = getBatchPostersFromFunctionData({
abi: [createRollupV3Dot1ABI],
data: tx.input,
});

return new Set([...acc, ...batchPosters]);
}
case createRollupV2Dot1FunctionSelector: {
const [{ batchPosters }] = getBatchPostersFromFunctionData({
abi: [createRollupV2Dot1ABI],
Expand Down
21 changes: 18 additions & 3 deletions src/getBatchPosters.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
encodeFunctionData,
http,
} from 'viem';
import { arbitrum, arbitrumSepolia } from 'viem/chains';
import { arbitrum, arbitrumSepolia, sepolia } from 'viem/chains';
import { it, expect, vi, describe } from 'vitest';

import { gnosisSafeL2ABI } from './contracts/GnosisSafeL2';
Expand All @@ -26,6 +26,11 @@ const arbitrumSepoliaClient = createPublicClient({
transport: http(),
});

const sepoliaClient = createPublicClient({
chain: sepolia,
transport: http('https://sepolia.gateway.tenderly.co'),
});

function mockLog(transactionHash: string) {
return {
address: '0x193e2887031c148ab54f5e856ea51ae521661200',
Expand Down Expand Up @@ -186,6 +191,16 @@ it('getBatchPosters returns batch posters for a chain created with RollupCreator
expect(isAccurate).toBeTruthy();
});

// https://sepolia.etherscan.io/tx/0xd79a80b7300df1bcb14e2e3ea83521d1ae37e5f171a787fb0f5377ea7f5003ad
it('getBatchPosters returns batch posters for a chain created with RollupCreator v3.1', async () => {
const { isAccurate, batchPosters } = await getBatchPosters(sepoliaClient, {
rollup: '0x5D65e18b873dD978EeE4704BC6033436aA253936',
sequencerInbox: '0x3fB778EC3e6126aF1d956A7812Eb0a28B9d25017',
});
expect(batchPosters).toEqual(['0x05c82FC99a41e417Ea6ED14e1D3f3b01BBFfba5A']);
expect(isAccurate).toBeTruthy();
});

describe('createRollupFunctionSelector', () => {
it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => {
const mockTransport = () =>
Expand Down Expand Up @@ -489,7 +504,7 @@ describe('safeL2FunctionSelector', () => {
});

describe('Detect batch posters added or removed multiple times', () => {
it('when disabling the same batch poster multiple time', async () => {
it('when disabling the same batch poster multiple times', async () => {
const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962';
const mockTransport = () =>
createTransport({
Expand Down Expand Up @@ -527,7 +542,7 @@ describe('Detect batch posters added or removed multiple times', () => {
expect(batchPosters).toEqual([]);
expect(isAccurate).toBeTruthy();
});
it('when enabling the same batch posters multiple time', async () => {
it('when enabling the same batch posters multiple times', async () => {
const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962';
const mockTransport = () =>
createTransport({
Expand Down
29 changes: 20 additions & 9 deletions src/getValidators.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ async function setValidator(validator: Address, state: boolean) {
}

// Tests can be enabled once we run one node per integration test
describe.skip('successfully get validators', () => {
it('when disabling the same validator multiple time', async () => {
describe('successfully get validators', () => {
it('when disabling the same validator multiple times', async () => {
const randomAccount = privateKeyToAccount(generatePrivateKey()).address;

const { isAccurate: isAccurateInitially, validators: initialValidators } = await getValidators(
Expand All @@ -48,8 +48,11 @@ describe.skip('successfully get validators', () => {
rollup: l3Rollup,
},
);
// By default, chains from nitro testnode has 10 validators
expect(initialValidators).toHaveLength(10);

// By default, chains from nitro testnode has 11 validators
// https://github.com/OffchainLabs/nitro-testnode/blob/de8cf4edec0d12e5ef1b7623e54e35ddb579ff0b/test-node.bash#L634
// https://github.com/OffchainLabs/nitro-contracts/blob/47a9c034bc082256d498f8031fab1a37c37a123c/scripts/rollupCreation.ts#L250-L257
expect(initialValidators).toHaveLength(11);
expect(isAccurateInitially).toBeTruthy();

await setValidator(randomAccount, false);
Expand Down Expand Up @@ -79,7 +82,7 @@ describe.skip('successfully get validators', () => {
expect(isAccurateFinal).toBeTruthy();
});

it('when enabling the same validators multiple time', async () => {
it('when enabling the same validators multiple times', async () => {
const randomAccount = privateKeyToAccount(generatePrivateKey()).address;

const { isAccurate: isAccurateInitially, validators: initialValidators } = await getValidators(
Expand All @@ -88,8 +91,10 @@ describe.skip('successfully get validators', () => {
rollup: l3Rollup,
},
);
// By default, chains from nitro testnode has 10 validators
expect(initialValidators).toHaveLength(10);
// By default, chains from nitro testnode has 11 validators
// https://github.com/OffchainLabs/nitro-testnode/blob/de8cf4edec0d12e5ef1b7623e54e35ddb579ff0b/test-node.bash#L634
// https://github.com/OffchainLabs/nitro-contracts/blob/47a9c034bc082256d498f8031fab1a37c37a123c/scripts/rollupCreation.ts#L250-L257
expect(initialValidators).toHaveLength(11);
expect(isAccurateInitially).toBeTruthy();

await setValidator(randomAccount, true);
Expand All @@ -113,7 +118,10 @@ describe.skip('successfully get validators', () => {
client,
{ rollup: l3Rollup },
);
expect(initialValidators).toHaveLength(10);
// By default, chains from nitro testnode has 11 validators
// https://github.com/OffchainLabs/nitro-testnode/blob/de8cf4edec0d12e5ef1b7623e54e35ddb579ff0b/test-node.bash#L634
// https://github.com/OffchainLabs/nitro-contracts/blob/47a9c034bc082256d498f8031fab1a37c37a123c/scripts/rollupCreation.ts#L250-L257
expect(initialValidators).toHaveLength(11);
expect(isAccurateInitially).toBeTruthy();

const firstValidator = initialValidators[0];
Expand All @@ -129,7 +137,10 @@ describe.skip('successfully get validators', () => {
client,
{ rollup: l3Rollup },
);
expect(initialValidators).toHaveLength(10);
// By default, chains from nitro testnode has 11 validators
// https://github.com/OffchainLabs/nitro-testnode/blob/de8cf4edec0d12e5ef1b7623e54e35ddb579ff0b/test-node.bash#L634
// https://github.com/OffchainLabs/nitro-contracts/blob/47a9c034bc082256d498f8031fab1a37c37a123c/scripts/rollupCreation.ts#L250-L257
expect(initialValidators).toHaveLength(11);
expect(isAccurateInitially).toBeTruthy();

const lastValidator = initialValidators[initialValidators.length - 1];
Expand Down
77 changes: 57 additions & 20 deletions src/getValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { rollupCreatorABI as rollupCreatorV2Dot1ABI } from './contracts/RollupCr
import { rollupCreatorABI as rollupCreatorV1Dot1ABI } from './contracts/RollupCreator/v1.1';
import { upgradeExecutorABI } from './contracts/UpgradeExecutor';
import { gnosisSafeL2ABI } from './contracts/GnosisSafeL2';
import { rollupABI } from './contracts/Rollup';
import { rollupABI as rollupV3Dot1ABI } from './contracts/Rollup';
import { rollupABI as rollupV2Dot1ABI } from './contracts/Rollup/v2.1';

import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash';
import { getLogsWithBatching } from './utils/getLogsWithBatching';
Expand All @@ -24,22 +25,27 @@ const createRollupV2Dot1FunctionSelector = getFunctionSelector(createRollupV2Dot
const createRollupV1Dot1ABI = getAbiItem({ abi: rollupCreatorV1Dot1ABI, name: 'createRollup' });
const createRollupV1Dot1FunctionSelector = getFunctionSelector(createRollupV1Dot1ABI);

const setValidatorABI = getAbiItem({ abi: rollupABI, name: 'setValidator' });
const setValidatorFunctionSelector = getFunctionSelector(setValidatorABI);
const setValidatorPreV3Dot1ABI = getAbiItem({ abi: rollupV2Dot1ABI, name: 'setValidator' });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't change

Suggested change
const setValidatorPreV3Dot1ABI = getAbiItem({ abi: rollupV2Dot1ABI, name: 'setValidator' });
const setValidatorABI = getAbiItem({ abi: rollupV3Dot1ABI, name: 'setValidator' });

const setValidatorPreV3Dot1FunctionSelector = getFunctionSelector(setValidatorPreV3Dot1ABI);

const executeCallABI = getAbiItem({ abi: upgradeExecutorABI, name: 'executeCall' });
const upgradeExecutorExecuteCallFunctionSelector = getFunctionSelector(executeCallABI);

const execTransactionABI = getAbiItem({ abi: gnosisSafeL2ABI, name: 'execTransaction' });
const safeL2FunctionSelector = getFunctionSelector(execTransactionABI);

const ownerFunctionCalledEventAbi = getAbiItem({ abi: rollupABI, name: 'OwnerFunctionCalled' });
const ownerFunctionCalledEventAbi = getAbiItem({
abi: rollupV2Dot1ABI,
name: 'OwnerFunctionCalled',
});

const validatorsSetEventAbi = getAbiItem({ abi: rollupV3Dot1ABI, name: 'ValidatorsSet' });

function getValidatorsFromFunctionData<
TAbi extends
| (typeof createRollupV2Dot1ABI)[]
| (typeof createRollupV1Dot1ABI)[]
| (typeof setValidatorABI)[],
| (typeof setValidatorPreV3Dot1ABI)[],
>({ abi, data }: { abi: TAbi; data: Hex }) {
const { args } = decodeFunctionData({
abi,
Expand All @@ -48,22 +54,36 @@ function getValidatorsFromFunctionData<
return args;
}

function updateAccumulator(acc: Set<Address>, input: Hex) {
const [validators, states] = getValidatorsFromFunctionData({
abi: [setValidatorABI],
data: input,
});
function iterateThroughValidatorsList(
acc: Set<Address>,
validators: Readonly<Address[]> | undefined,
enabled: Readonly<boolean[]> | undefined,
) {
if (typeof validators === 'undefined' || typeof enabled === 'undefined') {
return acc;
}

const copy = new Set<Address>(acc);

validators.forEach((validator, i) => {
const isAdd = states[i];
const isAdd = enabled[i];
if (isAdd) {
acc.add(validator);
copy.add(validator);
} else {
acc.delete(validator);
copy.delete(validator);
}
});

return acc;
return copy;
}

function updateAccumulator(acc: Set<Address>, input: Hex) {
const [validators, enabled] = getValidatorsFromFunctionData({
abi: [setValidatorPreV3Dot1ABI],
data: input,
});

return iterateThroughValidatorsList(acc, validators, enabled);
}

export type GetValidatorsParams = {
Expand Down Expand Up @@ -119,23 +139,40 @@ export async function getValidators<TChain extends Chain>(
blockNumber = 0n;
}

const events = await getLogsWithBatching(publicClient, {
const preV3Dot1Events = await getLogsWithBatching(publicClient, {
address: rollup,
event: ownerFunctionCalledEventAbi,
args: { id: 6n },
fromBlock: blockNumber,
});

const txs = await Promise.all(
events.map((event) =>
const v3Dot1ValidatorsSetEvents = await getLogsWithBatching(publicClient, {
address: rollup,
event: validatorsSetEventAbi,
fromBlock: blockNumber,
});

const validatorsFromV3Dot1Events = v3Dot1ValidatorsSetEvents
.filter((event) => event.eventName === 'ValidatorsSet')
.reduce((acc, event) => {
const { validators: _validators, enabled: _enabled } = event.args;
return iterateThroughValidatorsList(acc, _validators, _enabled);
}, new Set<Address>());

/** For pre v3.1, the OwnerFunctionCalled event is emitted when the validators list is updated
* the event is emitted without the validators list and the new states in the event args
* so we have to grab the tx and decode the calldata to get the validators list
*/
const preV3Dot1Txs = await Promise.all(
preV3Dot1Events.map((event) =>
publicClient.getTransaction({
hash: event.transactionHash,
}),
),
);

let isAccurate = true;
const validators = txs.reduce((acc, tx) => {
const validators = preV3Dot1Txs.reduce((acc, tx) => {
const txSelectedFunction = tx.input.slice(0, 10);

switch (txSelectedFunction) {
Expand All @@ -155,7 +192,7 @@ export async function getValidators<TChain extends Chain>(

return new Set([...acc, ...validators]);
}
case setValidatorFunctionSelector: {
case setValidatorPreV3Dot1FunctionSelector: {
return updateAccumulator(acc, tx.input);
}
case upgradeExecutorExecuteCallFunctionSelector: {
Expand Down Expand Up @@ -195,7 +232,7 @@ export async function getValidators<TChain extends Chain>(
return acc;
}
}
}, new Set<Address>());
}, validatorsFromV3Dot1Events);

return {
isAccurate,
Expand Down
16 changes: 15 additions & 1 deletion src/getValidators.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
encodeFunctionData,
http,
} from 'viem';
import { arbitrum, arbitrumSepolia } from 'viem/chains';
import { arbitrum, arbitrumSepolia, sepolia } from 'viem/chains';
import { it, expect, vi, describe } from 'vitest';

import { gnosisSafeL2ABI } from './contracts/GnosisSafeL2';
Expand All @@ -26,6 +26,11 @@ const arbitrumSepoliaClient = createPublicClient({
transport: http(),
});

const sepoliaClient = createPublicClient({
chain: sepolia,
transport: http('https://sepolia.gateway.tenderly.co'),
});

function mockLog(transactionHash: string) {
return {
address: '0x193e2887031c148ab54f5e856ea51ae521661200',
Expand Down Expand Up @@ -177,6 +182,15 @@ it('getValidators returns validators for a chain created with RollupCreator v2.1
expect(isAccurate).toBeTruthy();
});

// https://sepolia.etherscan.io/tx/0xd79a80b7300df1bcb14e2e3ea83521d1ae37e5f171a787fb0f5377ea7f5003ad
it('getValidators returns validators for a chain created with RollupCreator v3.1', async () => {
const { isAccurate, validators } = await getValidators(sepoliaClient, {
rollup: '0x5D65e18b873dD978EeE4704BC6033436aA253936',
});
expect(validators).toEqual(['0x776C1B18cde829C020ce1a3f75ae3B82F6a9108a']);
expect(isAccurate).toBeTruthy();
});

describe('createRollupFunctionSelector', () => {
it('getValidators return all validators with isAccurate flag set to true', async () => {
const mockTransport = () =>
Expand Down