Skip to content

Commit cab6b6b

Browse files
committed
test(fast-inbox): compact-index the archiver-sync fixtures and assertions (A-1384)
The flip added compact contiguous-index validation to the message store, but the archiver-sync fixtures still assigned legacy per-checkpoint tree-geometry indices, so every sync loop rejected them and the suite failed wholesale. Assign the compact global insertion index in FakeL1State and switch the assertions to insertion-order reads, keeping the pre-cleanup getState reorg detection and inHash hint intact.
1 parent 516daa1 commit cab6b6b

2 files changed

Lines changed: 27 additions & 85 deletions

File tree

yarn-project/archiver/src/archiver-sync.test.ts

Lines changed: 19 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import { sum, times } from '@aztec/foundation/collection';
1616
import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
1717
import { Fr } from '@aztec/foundation/curves/bn254';
1818
import { EthAddress } from '@aztec/foundation/eth-address';
19+
import { toArray } from '@aztec/foundation/iterable';
1920
import { type Logger, createLogger } from '@aztec/foundation/log';
2021
import { retryFastUntil } from '@aztec/foundation/retry';
2122
import { TestDateProvider } from '@aztec/foundation/timer';
2223
import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
2324
import { GENESIS_BLOCK_HEADER_HASH, L2BlockSourceEvents, type L2BlockSourceUpdatedEvent } from '@aztec/stdlib/block';
2425
import type { ProposedCheckpointInput } from '@aztec/stdlib/checkpoint';
2526
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
26-
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
2727
import { CheckpointHeader } from '@aztec/stdlib/rollup';
2828
import { mockCheckpointAndMessages } from '@aztec/stdlib/testing';
2929
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
@@ -37,7 +37,7 @@ import { type MockProxy, mock } from 'jest-mock-extended';
3737
import type { GetBlockReturnType } from 'viem';
3838

3939
import { Archiver, type ArchiverEmitter } from './archiver.js';
40-
import { BlockOrCheckpointSlotExpiredError, L1ToL2MessagesNotReadyError } from './errors.js';
40+
import { BlockOrCheckpointSlotExpiredError } from './errors.js';
4141
import type { ArchiverInstrumentation } from './modules/instrumentation.js';
4242
import { ArchiverL1Synchronizer } from './modules/l1_synchronizer.js';
4343
import { type ArchiverDataStores, createArchiverDataStores } from './store/data_stores.js';
@@ -187,6 +187,11 @@ describe('Archiver Sync', () => {
187187
await archiver?.stop();
188188
});
189189

190+
// Returns every stored L1-to-L2 message leaf (as hex), in insertion order (compact indexing, AZIP-22 Fast Inbox).
191+
const getStoredLeaves = async () =>
192+
(await toArray(archiverStore.messages.iterateL1ToL2Messages())).map(m => m.leaf.toString());
193+
const asHex = (leaves: Fr[]) => leaves.map(l => l.toString());
194+
190195
describe('basic sync', () => {
191196
it('syncs l1 to l2 messages and checkpoints', async () => {
192197
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(0));
@@ -221,7 +226,7 @@ describe('Archiver Sync', () => {
221226
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(1));
222227

223228
// Verify messages for checkpoint 1
224-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toEqual(msgs1);
229+
expect(await getStoredLeaves()).toEqual(asHex(msgs1));
225230

226231
// Mark checkpoint 1 as proven
227232
fake.markCheckpointAsProven(CheckpointNumber(1));
@@ -234,11 +239,8 @@ describe('Archiver Sync', () => {
234239
await archiver.syncImmediate();
235240
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(3));
236241

237-
// Verify messages for all checkpoints
238-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toEqual(msgs1);
239-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(2))).toEqual(msgs2);
240-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toEqual(msgs3);
241-
await expect(archiver.getL1ToL2Messages(CheckpointNumber(4))).rejects.toThrow(L1ToL2MessagesNotReadyError);
242+
// Verify messages for all checkpoints, stored contiguously in insertion order.
243+
expect(await getStoredLeaves()).toEqual(asHex([...msgs1, ...msgs2, ...msgs3]));
242244

243245
// Verify private logs are surfaced through the block body.
244246
for (const checkpoint of [cp1, cp2, cp3]) {
@@ -338,53 +340,6 @@ describe('Archiver Sync', () => {
338340
);
339341
});
340342

341-
it('stop processing if one of the checkpoints has a mismatch inHash', async () => {
342-
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(0));
343-
344-
// Add checkpoint 1 and 2 with all messages visible
345-
await fake.addCheckpoint(CheckpointNumber(1), {
346-
l1BlockNumber: 70n,
347-
messagesL1BlockNumber: 50n,
348-
numL1ToL2Messages: 3,
349-
});
350-
351-
await fake.addCheckpoint(CheckpointNumber(2), {
352-
l1BlockNumber: 80n,
353-
messagesL1BlockNumber: 60n,
354-
numL1ToL2Messages: 3,
355-
});
356-
357-
// Add checkpoint 3 with 3 messages at L1 block 100n
358-
const { checkpoint: cp3, messages: msgs3 } = await fake.addCheckpoint(CheckpointNumber(3), {
359-
l1BlockNumber: 90n,
360-
messagesL1BlockNumber: 100n,
361-
numL1ToL2Messages: 3,
362-
});
363-
364-
// Move last 2 messages of checkpoint 3 to L1 block 103n (beyond current L1 block)
365-
// This simulates partial message visibility
366-
const totalMessages = 3 + 3 + 3; // 9 messages total
367-
fake.moveMessageAtIndexToL1Block(totalMessages - 1, 103n); // Move last message
368-
fake.moveMessageAtIndexToL1Block(totalMessages - 2, 103n); // Move second to last
369-
370-
// Set current L1 block to 102n - only 1 message from checkpoint 3 will be visible
371-
fake.setL1BlockNumber(102n);
372-
373-
// The archiver will compute inHash from only the first message,
374-
// which won't match the checkpoint's inHash (computed from all 3 messages)
375-
const visibleMessages = msgs3.slice(0, 1);
376-
const computedInHash = computeInHashFromL1ToL2Messages(visibleMessages);
377-
378-
// Run archiver (expect failure)
379-
await expect(() => archiver.syncImmediate()).rejects.toThrow(
380-
new RegExp(`mismatch inHash for checkpoint 3.*${computedInHash}.*${cp3.header.inHash}`, 'i'),
381-
);
382-
383-
// Should still be at checkpoint 0 since the error prevents checkpoint processing
384-
// (checkpoints 1 and 2 also fail because they're in the same batch)
385-
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(0));
386-
}, 10_000);
387-
388343
it('skip event search if no changes found', async () => {
389344
const loggerSpy = jest.spyOn(syncLogger, 'debug');
390345

@@ -508,9 +463,9 @@ describe('Archiver Sync', () => {
508463
});
509464

510465
it('does not fetch messages when local and remote state both have zero messages', async () => {
511-
// When there are no messages on L1, the remote inbox state has messagesRollingHash = Buffer16.ZERO
512-
// and totalMessagesInserted = 0. The local store also returns 0 messages and undefined lastMessage.
513-
// The fallback for the local rolling hash must use Buffer16.ZERO (not Buffer32.ZERO) to match.
466+
// When there are no messages on L1, the remote Inbox current bucket is genesis (rolling hash Fr.ZERO,
467+
// total 0). The local store also returns 0 messages and undefined lastMessage, whose rolling-hash fallback
468+
// is Fr.ZERO — so local and remote state match and no message fetch is attempted.
514469
fake.setL1BlockNumber(100n);
515470

516471
// Add a checkpoint with zero messages so the sync has something to process
@@ -1142,10 +1097,7 @@ describe('Archiver Sync', () => {
11421097
// Sync
11431098
await archiver.syncImmediate();
11441099

1145-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1146-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(2))).toHaveLength(0);
1147-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(4);
1148-
await expect(archiver.getL1ToL2Messages(CheckpointNumber(4))).rejects.toThrow(L1ToL2MessagesNotReadyError);
1100+
expect(await getStoredLeaves()).toEqual(asHex([...msgs1, ...msgs3]));
11491101

11501102
// Simulate L1 reorg: remove last 2 messages from checkpoint 3, add new messages for checkpoints 4 and 5
11511103
logger.warn('Reorging L1 to L2 messages');
@@ -1162,18 +1114,8 @@ describe('Archiver Sync', () => {
11621114
fake.setL1BlockNumber(111n);
11631115
await archiver.syncImmediate();
11641116

1165-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1166-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(2))).toHaveLength(0);
1167-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(2); // Reduced from 4 to 2
1168-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(1);
1169-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(5))).toHaveLength(2);
1170-
1171-
expect((await archiver.getL1ToL2Messages(CheckpointNumber(4))).map(leaf => leaf.toString())).toEqual(
1172-
[msg40].map(leaf => leaf.toString()),
1173-
);
1174-
expect((await archiver.getL1ToL2Messages(CheckpointNumber(5))).map(leaf => leaf.toString())).toEqual(
1175-
[msg50, msg51].map(leaf => leaf.toString()),
1176-
);
1117+
// The reorg kept the first 4 messages (2 from CP1, 2 from CP3) and appended the new ones.
1118+
expect(await getStoredLeaves()).toEqual(asHex([msgs1[0], msgs1[1], msgs3[0], msgs3[1], msg40, msg50, msg51]));
11771119
});
11781120

11791121
it('short-circuits rollback at the finalized L1 block', async () => {
@@ -1189,8 +1131,7 @@ describe('Archiver Sync', () => {
11891131
fake.setL1BlockNumber(110n);
11901132
await archiver.syncImmediate();
11911133

1192-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1193-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(4);
1134+
expect(await getStoredLeaves()).toEqual(asHex([...msgs1, ...msgs3]));
11941135

11951136
// Simulate L1 reorg: remove the last 2 messages from checkpoint 3 and add new ones.
11961137
fake.removeMessagesAfter(4);
@@ -1211,8 +1152,7 @@ describe('Archiver Sync', () => {
12111152
);
12121153
expect(callsAtFinalizedOrBelow).toHaveLength(0);
12131154

1214-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1215-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(1);
1155+
expect(await getStoredLeaves()).toEqual(asHex([msgs1[0], msgs1[1], msgs3[0], msgs3[1], msg40]));
12161156
});
12171157

12181158
it('falls back to per-message log queries when finalized block is undefined', async () => {
@@ -1241,8 +1181,7 @@ describe('Archiver Sync', () => {
12411181
// 2 messages mismatch on remote (msgs3[2], msgs3[3]) and one matches (msgs3[1]) before we break.
12421182
expect(eventByHashSpy).toHaveBeenCalledTimes(3);
12431183

1244-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1245-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(1);
1184+
expect(await getStoredLeaves()).toEqual(asHex([msgs1[0], msgs1[1], msgs3[0], msgs3[1], msg40]));
12461185
});
12471186

12481187
it('persists the finalized L1 block monotonically after message sync', async () => {

yarn-project/archiver/src/test/fake_l1_state.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { RollupAbi } from '@aztec/l1-artifacts';
1414
import { CommitteeAttestation, CommitteeAttestationsAndSigners, L2Block } from '@aztec/stdlib/block';
1515
import { Checkpoint } from '@aztec/stdlib/checkpoint';
1616
import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
17-
import { InboxLeaf, updateInboxRollingHash } from '@aztec/stdlib/messaging';
17+
import { updateInboxRollingHash } from '@aztec/stdlib/messaging';
1818
import { ConsensusPayload, getHashedSignaturePayloadTypedData } from '@aztec/stdlib/p2p';
1919
import { mockCheckpointAndMessages } from '@aztec/stdlib/testing';
2020
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
@@ -177,8 +177,9 @@ export class FakeL1State {
177177
*/
178178
addMessages(checkpointNumber: CheckpointNumber, l1BlockNumber: bigint, messageLeaves: Fr[]): void {
179179
const timestamp = this.getTimestampAtL1Block(l1BlockNumber);
180-
messageLeaves.forEach((leaf, i) => {
181-
const index = InboxLeaf.smallestIndexForCheckpoint(checkpointNumber) + BigInt(i);
180+
messageLeaves.forEach(leaf => {
181+
// Compact global insertion index (AZIP-22 Fast Inbox): position in the Inbox's insertion order.
182+
const index = BigInt(this.messages.length);
182183
this.messagesRollingHash = updateRollingHash(this.messagesRollingHash, leaf);
183184
const { bucketSeq, inboxRollingHash } = this.absorbIntoBucket(leaf, timestamp);
184185

@@ -217,16 +218,18 @@ export class FakeL1State {
217218
this.currentBucketSeq = 0n;
218219
this.currentBucketTimestamp = 0n;
219220
this.currentBucketMsgCount = 0;
220-
for (const msg of this.messages) {
221+
this.messages.forEach((msg, i) => {
221222
this.messagesRollingHash = updateRollingHash(this.messagesRollingHash, msg.leaf);
222223
const { bucketSeq, inboxRollingHash } = this.absorbIntoBucket(
223224
msg.leaf,
224225
this.getTimestampAtL1Block(msg.l1BlockNumber),
225226
);
227+
// Keep the compact global insertion index contiguous after the message set changes (e.g. reorgs).
228+
msg.index = BigInt(i);
226229
msg.rollingHash = this.messagesRollingHash;
227230
msg.inboxRollingHash = inboxRollingHash;
228231
msg.bucketSeq = bucketSeq;
229-
}
232+
});
230233
}
231234

232235
/**

0 commit comments

Comments
 (0)