Skip to content

Commit

Permalink
refactor: archiver identifies prune (#8666)
Browse files Browse the repository at this point in the history
Fixes #8620, by using the status function to figure out when a prune
have happened and unwind state. It do NOT tell the world state
synchronizer about the unwinding and that should be addressed as part of
#8665.

Adds tests to `e2e_synching` making sure that an archiver will correctly
catch the prune happening and delete the blocks and accompanying data.
Also adds e2e_synching to earthfile.

Removes the `proven_store` since we learn this from the status and it is
not more closely linked to the fetching of blocks.

Note that we are not handling L1 re-orgs specifically in here, but they
will for some cases be caught as well. But not all, so there is still
#8621 as well.

Fixes an issue where asking for block 0 would return block 1, as there
is no block 0. Explicitly return undefined if block 0 is requested now,
and gives an error if using `getBlocks` from a block that is before the
initial block.
  • Loading branch information
LHerskind authored and Rumata888 committed Sep 27, 2024
1 parent 2dfa8f7 commit 4fa7e0b
Show file tree
Hide file tree
Showing 15 changed files with 882 additions and 308 deletions.
100 changes: 93 additions & 7 deletions yarn-project/archiver/src/archiver/archiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { MemoryArchiverStore } from './memory_archiver_store/memory_archiver_sto

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

Expand Down Expand Up @@ -74,9 +73,10 @@ describe('Archiver', () => {

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

rollupRead = mock<MockRollupContractRead>({
archiveAt: (args: readonly [bigint]) => Promise.resolve(blocks[Number(args[0] - 1n)].archive.root.toString()),
});
rollupRead = mock<MockRollupContractRead>();
rollupRead.archiveAt.mockImplementation((args: readonly [bigint]) =>
Promise.resolve(blocks[Number(args[0] - 1n)].archive.root.toString()),
);

((archiver as any).rollup as any).read = rollupRead;
});
Expand All @@ -96,15 +96,19 @@ describe('Archiver', () => {

rollupRead.status
.mockResolvedValueOnce([0n, GENESIS_ROOT, 1n, blocks[0].archive.root.toString(), GENESIS_ROOT])
.mockResolvedValue([0n, GENESIS_ROOT, 3n, blocks[2].archive.root.toString(), blocks[0].archive.root.toString()]);
.mockResolvedValue([
1n,
blocks[0].archive.root.toString(),
3n,
blocks[2].archive.root.toString(),
blocks[0].archive.root.toString(),
]);

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

rollupRead.getProvenBlockNumber.mockResolvedValueOnce(1n);

mockGetLogs({
messageSent: [
makeMessageSentEvent(2504n, 2n, 0n),
Expand Down Expand Up @@ -275,6 +279,88 @@ describe('Archiver', () => {
expect(loggerSpy).toHaveBeenNthCalledWith(2, `No blocks to retrieve from ${1n} to ${50n}`);
}, 10_000);

it('Handle L2 reorg', async () => {
const loggerSpy = jest.spyOn((archiver as any).log, 'verbose');

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

const numL2BlocksInTest = 2;

const rollupTxs = blocks.map(makeRollupTx);

publicClient.getBlockNumber.mockResolvedValueOnce(50n).mockResolvedValueOnce(100n).mockResolvedValueOnce(150n);

// We will return status at first to have an empty round, then as if we have 2 pending blocks, and finally
// Just a single pending block returning a "failure" for the expected pending block
rollupRead.status
.mockResolvedValueOnce([0n, GENESIS_ROOT, 0n, GENESIS_ROOT, GENESIS_ROOT])
.mockResolvedValueOnce([0n, GENESIS_ROOT, 2n, blocks[1].archive.root.toString(), GENESIS_ROOT])
.mockResolvedValueOnce([0n, GENESIS_ROOT, 1n, blocks[0].archive.root.toString(), Fr.ZERO.toString()]);

rollupRead.archiveAt
.mockResolvedValueOnce(blocks[0].archive.root.toString())
.mockResolvedValueOnce(blocks[1].archive.root.toString())
.mockResolvedValueOnce(Fr.ZERO.toString());

// This can look slightly odd, but we will need to do an empty request for the messages, and will entirely skip
// a call to the proposed blocks because of changes with status.
mockGetLogs({
messageSent: [],
});
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({
messageSent: [],
});
mockGetLogs({
messageSent: [],
});

rollupTxs.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);

// For some reason, this is 1-indexed.
expect(loggerSpy).toHaveBeenNthCalledWith(
1,
`Retrieved no new L1 -> L2 messages between L1 blocks ${1n} and ${50}.`,
);
expect(loggerSpy).toHaveBeenNthCalledWith(2, `No blocks to retrieve from ${1n} to ${50n}`);

// Lets take a look to see if we can find re-org stuff!
await sleep(1000);

expect(loggerSpy).toHaveBeenNthCalledWith(6, `L2 prune have occurred, unwind state`);
expect(loggerSpy).toHaveBeenNthCalledWith(7, `Unwinding 1 block from block 2`);

// Should also see the block number be reduced
latestBlockNum = await archiver.getBlockNumber();
expect(latestBlockNum).toEqual(numL2BlocksInTest - 1);

const txHash = blocks[1].body.txEffects[0].txHash;
expect(await archiver.getTxEffect(txHash)).resolves.toBeUndefined;
expect(await archiver.getBlock(2)).resolves.toBeUndefined;

[LogType.NOTEENCRYPTED, LogType.ENCRYPTED, LogType.UNENCRYPTED].forEach(async t => {
expect(await archiver.getLogs(2, 1, t)).toEqual([]);
});

// The random blocks don't include contract instances nor classes we we cannot look for those here.
}, 10_000);

// logs should be created in order of how archiver syncs.
const mockGetLogs = (logs: {
messageSent?: ReturnType<typeof makeMessageSentEvent>[];
Expand Down
Loading

0 comments on commit 4fa7e0b

Please sign in to comment.