Skip to content

Commit 7e29709

Browse files
committed
fix(fast-inbox): synthesize the genesis Inbox bucket on read (A-1383)
The archiver only writes bucket snapshots for ingested messages, whose first bucket is sequence 1, so `getInboxBucket(0)` / `getInboxBucketByTotalMsgCount(0)` returned undefined. That broke streaming resolution of a genesis parent (first block would reject `parent_bucket_unresolved`), of an empty genesis block's own bucket reference, and let an empty checkpoint skip the last-block censorship enforcement. Synthesize the sequence-0 sentinel (rolling hash 0, total 0) on read, mirroring the on-chain Inbox's base case, with a store-level test exercising it through a populated MessageStore.
1 parent af4a3f2 commit 7e29709

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

yarn-project/archiver/src/store/message_store.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,14 @@ describe('MessageStore', () => {
403403
expect(await messageStore.getInboxBucketByTotalMsgCount(7n)).toBeUndefined();
404404
});
405405

406+
it('synthesizes the genesis sentinel bucket (sequence 0, total 0) which is never ingested', async () => {
407+
// With real messages present but no ingested sequence-0 snapshot, both lookups still resolve genesis.
408+
await messageStore.addL1ToL2Messages(makeBucketedMessages(threeBucketSpec));
409+
410+
expect(await messageStore.getInboxBucket(0n)).toMatchObject({ seq: 0n, totalMsgCount: 0n, msgCount: 0 });
411+
expect(await messageStore.getInboxBucketByTotalMsgCount(0n)).toMatchObject({ seq: 0n, totalMsgCount: 0n });
412+
});
413+
406414
it('continues a bucket that spans two insertion batches', async () => {
407415
const msgs = makeBucketedMessages(threeBucketSpec);
408416
await messageStore.addL1ToL2Messages(msgs.slice(0, 2));

yarn-project/archiver/src/store/message_store.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ function deserializeBucketSnapshot(buffer: Buffer): BucketSnapshot {
5454
return { inboxRollingHash, totalMsgCount, timestamp, msgCount, lastMessageIndex };
5555
}
5656

57+
// The genesis sentinel bucket (AZIP-22 Fast Inbox): sequence 0 with a zero rolling hash and no messages, mirroring the
58+
// on-chain Inbox's base case. The archiver never ingests a snapshot for it (no message is absorbed into sequence 0), so
59+
// it is synthesized on read. Consumers use its sequence number and zero total; its deploy-time timestamp is not tracked
60+
// here and is unused.
61+
const GENESIS_INBOX_BUCKET: InboxBucket = {
62+
seq: 0n,
63+
inboxRollingHash: Fr.ZERO,
64+
totalMsgCount: 0n,
65+
timestamp: 0n,
66+
msgCount: 0,
67+
lastMessageIndex: 0n,
68+
isOpen: false,
69+
};
70+
5771
export class MessageStoreError extends Error {
5872
constructor(
5973
message: string,
@@ -403,11 +417,16 @@ export class MessageStore {
403417

404418
/**
405419
* Returns the Inbox bucket with the given sequence number, or undefined if it has not been synced (AZIP-22 Fast
406-
* Inbox).
420+
* Inbox). Sequence 0 is the genesis sentinel: the on-chain Inbox reserves it as the "consumed nothing" base case
421+
* and never absorbs a message into it, so the archiver ingests no snapshot for it; it is synthesized here (rolling
422+
* hash 0, total 0) so streaming consumers can resolve a genesis parent or an empty checkpoint's last-consumed bucket.
407423
*/
408424
public async getInboxBucket(seq: bigint): Promise<InboxBucket | undefined> {
409425
const snapshot = await this.getBucketSnapshotBySeq(seq);
410-
return snapshot && this.toInboxBucket(seq, snapshot);
426+
if (snapshot !== undefined) {
427+
return this.toInboxBucket(seq, snapshot);
428+
}
429+
return seq === 0n ? GENESIS_INBOX_BUCKET : undefined;
411430
}
412431

413432
/**

0 commit comments

Comments
 (0)