Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup {
uint256 l2ToL1TreeMinHeight = min + 1;
OUTBOX.insert(blockNumber, header.contentCommitment.outHash, l2ToL1TreeMinHeight);

emit L2BlockProposed(blockNumber);
emit L2BlockProposed(blockNumber, _archive);

// Automatically flag the block as proven if we have cheated and set assumeProvenThroughBlockNumber.
if (blockNumber <= assumeProvenThroughBlockNumber) {
Expand Down Expand Up @@ -390,7 +390,10 @@ contract Rollup is Leonidas, IRollup, ITestRollup {
* @return bytes32 - The archive root of the block
*/
function archiveAt(uint256 _blockNumber) external view override(IRollup) returns (bytes32) {
return blocks[_blockNumber].archive;
if (_blockNumber <= tips.pendingBlockNumber) {
return blocks[_blockNumber].archive;
}
return bytes32(0);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ITestRollup {
}

interface IRollup {
event L2BlockProposed(uint256 indexed blockNumber);
event L2BlockProposed(uint256 indexed blockNumber, bytes32 indexed archive);
event L2ProofVerified(uint256 indexed blockNumber, bytes32 indexed proverId);
event PrunedPending(uint256 provenBlockNumber, uint256 pendingBlockNumber);

Expand Down
99 changes: 75 additions & 24 deletions yarn-project/archiver/src/archiver/archiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Fr } from '@aztec/foundation/fields';
import { sleep } from '@aztec/foundation/sleep';
import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';

import { jest } from '@jest/globals';
import { type MockProxy, mock } from 'jest-mock-extended';
import {
type Chain,
Expand All @@ -26,6 +27,14 @@ import { type ArchiverDataStore } from './archiver_store.js';
import { type ArchiverInstrumentation } from './instrumentation.js';
import { MemoryArchiverStore } from './memory_archiver_store/memory_archiver_store.js';

interface MockRollupContractRead {
archiveAt: (args: readonly [bigint]) => Promise<`0x${string}`>;
}

class MockRollupContract {
constructor(public read: MockRollupContractRead, public address: `0x${string}`) {}
}

describe('Archiver', () => {
const rollupAddress = EthAddress.ZERO;
const inboxAddress = EthAddress.ZERO;
Expand All @@ -39,6 +48,7 @@ describe('Archiver', () => {
let now: number;

let archiver: Archiver;
let blocks: L2Block[];

beforeEach(() => {
now = +new Date();
Expand All @@ -47,16 +57,11 @@ describe('Archiver', () => {
timestamp: args.blockNumber * 1000n + BigInt(now),
})) as any,
});

instrumentation = mock({ isEnabled: () => true });
archiverStore = new MemoryArchiverStore(1000);
proverId = Fr.random();
});

afterEach(async () => {
await archiver?.stop();
});

it('can start, sync and stop and handle l1 to l2 messages and logs', async () => {
archiver = new Archiver(
publicClient,
rollupAddress,
Expand All @@ -67,18 +72,30 @@ describe('Archiver', () => {
instrumentation,
);

blocks = blockNumbers.map(x => L2Block.random(x, 4, x, x + 1, 2, 2));

const mockRollupRead = mock<MockRollupContractRead>({
archiveAt: (args: readonly [bigint]) => Promise.resolve(blocks[Number(args[0] - 1n)].archive.root.toString()),
});
(archiver as any).rollup = new MockRollupContract(mockRollupRead, rollupAddress.toString());
});

afterEach(async () => {
await archiver?.stop();
});

it('can start, sync and stop and handle l1 to l2 messages and logs', async () => {
let latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(0);

const blocks = blockNumbers.map(x => L2Block.random(x, 4, x, x + 1, 2, 2));
blocks.forEach((b, i) => (b.header.globalVariables.timestamp = new Fr(now + 1000 * (i + 1))));
const rollupTxs = blocks.map(makeRollupTx);

publicClient.getBlockNumber.mockResolvedValueOnce(2500n).mockResolvedValueOnce(2600n).mockResolvedValueOnce(2700n);

mockGetLogs({
messageSent: [makeMessageSentEvent(98n, 1n, 0n), makeMessageSentEvent(99n, 1n, 1n)],
L2BlockProposed: [makeL2BlockProposedEvent(101n, 1n)],
L2BlockProposed: [makeL2BlockProposedEvent(101n, 1n, blocks[0].archive.root.toString())],
proofVerified: [makeProofVerifiedEvent(102n, 1n, proverId)],
});

Expand All @@ -89,7 +106,10 @@ describe('Archiver', () => {
makeMessageSentEvent(2505n, 2n, 2n),
makeMessageSentEvent(2506n, 3n, 1n),
],
L2BlockProposed: [makeL2BlockProposedEvent(2510n, 2n), makeL2BlockProposedEvent(2520n, 3n)],
L2BlockProposed: [
makeL2BlockProposedEvent(2510n, 2n, blocks[1].archive.root.toString()),
makeL2BlockProposedEvent(2520n, 3n, blocks[2].archive.root.toString()),
],
});

publicClient.getTransaction.mockResolvedValueOnce(rollupTxs[0]);
Expand Down Expand Up @@ -168,45 +188,76 @@ describe('Archiver', () => {
}, 10_000);

it('does not sync past current block number', async () => {
let latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(0);

const numL2BlocksInTest = 2;
archiver = new Archiver(
publicClient,
rollupAddress,
inboxAddress,
registryAddress,
archiverStore,
1000,
instrumentation,
);

const rollupTxs = blocks.map(makeRollupTx);

// Here we set the current L1 block number to 102. L1 to L2 messages after this should not be read.
publicClient.getBlockNumber.mockResolvedValue(102n);

mockGetLogs({
messageSent: [makeMessageSentEvent(66n, 1n, 0n), makeMessageSentEvent(68n, 1n, 1n)],
L2BlockProposed: [
makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString()),
makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString()),
],
});

mockGetLogs({});

rollupTxs.slice(0, numL2BlocksInTest).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx));

await archiver.start(false);

while ((await archiver.getBlockNumber()) !== numL2BlocksInTest) {
await sleep(100);
}

latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(numL2BlocksInTest);
}, 10_000);

it('ignores block 3 because it have been pruned (simulate pruning)', async () => {
const loggerSpy = jest.spyOn((archiver as any).log, 'warn');

let latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(0);

const blocks = blockNumbers.map(x => L2Block.random(x, 4, x, x + 1, 2, 2));
const numL2BlocksInTest = 2;

const rollupTxs = blocks.map(makeRollupTx);

// Here we set the current L1 block number to 102. L1 to L2 messages after this should not be read.
publicClient.getBlockNumber.mockResolvedValue(102n);

const badArchive = Fr.random().toString();

mockGetLogs({
messageSent: [makeMessageSentEvent(66n, 1n, 0n), makeMessageSentEvent(68n, 1n, 1n)],
L2BlockProposed: [makeL2BlockProposedEvent(70n, 1n), makeL2BlockProposedEvent(80n, 2n)],
L2BlockProposed: [
makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString()),
makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString()),
makeL2BlockProposedEvent(90n, 3n, badArchive),
],
});

mockGetLogs({});

rollupTxs.slice(0, numL2BlocksInTest).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx));
rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx));

await archiver.start(false);

// Wait until block 3 is processed. If this won't happen the test will fail with timeout.
while ((await archiver.getBlockNumber()) !== numL2BlocksInTest) {
await sleep(100);
}

latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(numL2BlocksInTest);
const errorMessage = `Archive mismatch matching, ignoring block ${3} with archive: ${badArchive}, expected ${blocks[2].archive.root.toString()}`;
expect(loggerSpy).toHaveBeenCalledWith(errorMessage);
}, 10_000);

// logs should be created in order of how archiver syncs.
Expand All @@ -228,10 +279,10 @@ describe('Archiver', () => {
* @param l2BlockNum - L2 Block number.
* @returns An L2BlockProposed event log.
*/
function makeL2BlockProposedEvent(l1BlockNum: bigint, l2BlockNum: bigint) {
function makeL2BlockProposedEvent(l1BlockNum: bigint, l2BlockNum: bigint, archive: `0x${string}`) {
return {
blockNumber: l1BlockNum,
args: { blockNumber: l2BlockNum },
args: { blockNumber: l2BlockNum, archive },
transactionHash: `0x${l2BlockNum}`,
} as Log<bigint, number, false, undefined, true, typeof RollupAbi, 'L2BlockProposed'>;
}
Expand Down
27 changes: 20 additions & 7 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ import {
} from '@aztec/types/contracts';

import groupBy from 'lodash.groupby';
import { type Chain, type HttpTransport, type PublicClient, createPublicClient, getContract, http } from 'viem';
import {
type Chain,
type GetContractReturnType,
type HttpTransport,
type PublicClient,
createPublicClient,
getContract,
http,
} from 'viem';

import { type ArchiverDataStore } from './archiver_store.js';
import { type ArchiverConfig } from './config.js';
Expand All @@ -68,6 +76,8 @@ export class Archiver implements ArchiveSource {
*/
private runningPromise?: RunningPromise;

private rollup: GetContractReturnType<typeof RollupAbi, PublicClient<HttpTransport, Chain>>;

/**
* Creates a new instance of the Archiver.
* @param publicClient - A client for interacting with the Ethereum node.
Expand All @@ -88,7 +98,13 @@ export class Archiver implements ArchiveSource {
private readonly instrumentation: ArchiverInstrumentation,
private readonly l1StartBlock: bigint = 0n,
private readonly log: DebugLogger = createDebugLogger('aztec:archiver'),
) {}
) {
this.rollup = getContract({
address: rollupAddress.toString(),
abi: RollupAbi,
client: publicClient,
});
}

/**
* Creates a new instance of the Archiver and blocks until it syncs from chain.
Expand Down Expand Up @@ -245,17 +261,14 @@ export class Archiver implements ArchiveSource {

await this.store.addL1ToL2Messages(retrievedL1ToL2Messages);

// Read all data from chain and then write to our stores at the end
const nextExpectedL2BlockNum = BigInt((await this.store.getSynchedL2BlockNumber()) + 1);

this.log.debug(`Retrieving blocks from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
const retrievedBlocks = await retrieveBlockFromRollup(
this.rollup,
this.publicClient,
this.rollupAddress,
blockUntilSynced,
blocksSynchedTo + 1n,
currentL1BlockNumber,
nextExpectedL2BlockNum,
this.log,
);

// Add the body
Expand Down
28 changes: 17 additions & 11 deletions yarn-project/archiver/src/archiver/data_retrieval.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { type InboxLeaf, type L2Block } from '@aztec/circuit-types';
import { Fr, type Proof } from '@aztec/circuits.js';
import { type EthAddress } from '@aztec/foundation/eth-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
import { RollupAbi } from '@aztec/l1-artifacts';

import { type Hex, type PublicClient, getAbiItem } from 'viem';
import {
type Chain,
type GetContractReturnType,
type Hex,
type HttpTransport,
type PublicClient,
getAbiItem,
} from 'viem';

import {
getBlockProofFromSubmitProofTx,
Expand All @@ -27,38 +34,37 @@ import { type L1Published } from './structs/published.js';
* @returns An array of block; as well as the next eth block to search from.
*/
export async function retrieveBlockFromRollup(
rollup: GetContractReturnType<typeof RollupAbi, PublicClient<HttpTransport, Chain>>,
publicClient: PublicClient,
rollupAddress: EthAddress,
blockUntilSynced: boolean,
searchStartBlock: bigint,
searchEndBlock: bigint,
expectedNextL2BlockNum: bigint,
logger: DebugLogger = createDebugLogger('aztec:archiver'),
): Promise<L1Published<L2Block>[]> {
const retrievedBlocks: L1Published<L2Block>[] = [];
do {
if (searchStartBlock > searchEndBlock) {
break;
}
const L2BlockProposedLogs = await getL2BlockProposedLogs(
const l2BlockProposedLogs = await getL2BlockProposedLogs(
publicClient,
rollupAddress,
EthAddress.fromString(rollup.address),
searchStartBlock,
searchEndBlock,
);
if (L2BlockProposedLogs.length === 0) {

if (l2BlockProposedLogs.length === 0) {
break;
}

const lastLog = L2BlockProposedLogs[L2BlockProposedLogs.length - 1];
const lastLog = l2BlockProposedLogs[l2BlockProposedLogs.length - 1];
logger.debug(
`Got L2 block processed logs for ${L2BlockProposedLogs[0].blockNumber}-${lastLog.blockNumber} between ${searchStartBlock}-${searchEndBlock} L1 blocks`,
`Got L2 block processed logs for ${l2BlockProposedLogs[0].blockNumber}-${lastLog.blockNumber} between ${searchStartBlock}-${searchEndBlock} L1 blocks`,
);

const newBlocks = await processL2BlockProposedLogs(publicClient, expectedNextL2BlockNum, L2BlockProposedLogs);
const newBlocks = await processL2BlockProposedLogs(rollup, publicClient, l2BlockProposedLogs, logger);
retrievedBlocks.push(...newBlocks);
searchStartBlock = lastLog.blockNumber! + 1n;
expectedNextL2BlockNum += BigInt(newBlocks.length);
} while (blockUntilSynced && searchStartBlock <= searchEndBlock);
return retrievedBlocks;
}
Expand Down
Loading