Skip to content

Commit 6996e91

Browse files
committed
feat(fast-inbox): archiver syncs Inbox buckets (A-1379)
Part of the Fast Inbox stack (AZIP-22). Makes the archiver track the L1 Inbox rolling-hash buckets introduced in A-1377, validate the full-width consensus rolling-hash chain, and expose the bucket queries the sequencer/validator will consume (A-1382/A-1383). Purely additive: the legacy per-checkpoint `getL1ToL2Messages` path is unchanged. ## What's added - `InboxMessage` gains `inboxRollingHash: Fr`, `bucketSeq`, and `bucketTimestamp` (the L1 block timestamp that keys the bucket). `mapLogInboxMessage` propagates them; the ethereum `MessageSent` log now also carries the emitting L1 block's timestamp. - `MessageStore` persists a per-bucket snapshot (`{ inboxRollingHash, totalMsgCount, timestamp, msgCount, lastMessageIndex }`) keyed by bucket sequence, plus a timestamp→sequence index for at-or-before lookups. Snapshots are written as messages are inserted and rewound (deleted / boundary-bucket recomputed) on reorg removal, all inside the existing kv-store transactions. - `addL1ToL2Messages` now validates the full-width consensus rolling hash (`updateInboxRollingHash`) alongside the legacy 128-bit keccak chain. Both run until the streaming inbox flips on (A-1388 removes the legacy one). - Three queries on `L1ToL2MessageSource` (implemented by the archiver, supported by the mock, exposed over the archiver RPC schema): `getLatestInboxBucketAtOrBefore(timestamp)`, `getInboxBucket(seq)`, and `getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive)`. New `InboxBucket` type + zod schema in stdlib messaging. ## Notes - No migration: `ARCHIVER_DB_VERSION` is bumped 7 → 8, so nodes resync their L1→L2 messages from L1. Archiver message stores are rebuildable from L1, so this is acceptable on this release line. - Folds in A-1344: `InboxMessage.l1BlockNumber` is widened from UInt32 to UInt64 (the serialization was being rewritten anyway). - Field naming: the full-width hash is `inboxRollingHash: Fr` to match the `updateInboxRollingHash` mirror, `CheckpointHeader.inboxRollingHash`, and the decoded `MessageSent` event — the value is a truncated-sha256-to-field element. (The plan's provisional `consensusRollingHash: Buffer32` predates the implemented event.) The legacy `rollingHash: Buffer16` stays until A-1388. ## Out of scope - Reorg-driven bucket invalidation (a reorg that replays the identical message leaves but re-buckets them across different L1 blocks changes neither the 128-bit nor the full-width leaf-based hash, so `localStateMatches` cannot detect it, and the allowed message-reinsertion path leaves its bucket snapshot stale). This is handled by A-1389, and bucket consumption is gated off by the `streamingInbox` flag pre-flip. - Sequencer/validator consumption (A-1382/A-1383), proposal bucket-reference types (A-1381), and removing the legacy 128-bit hash (A-1388). ## Testing - `message_store.test.ts`: bucket snapshotting (multi-message, cross-batch, rollover), full-width chain-mismatch detection, reorg rewinding, and the three queries incl. boundary cases. - `archiver-sync.test.ts`: end-to-end sync now exercises the full-width chain validation and asserts the bucket queries against the synced state (the fake L1 state mirrors the on-chain bucket assignment). - `stdlib` archiver interface RPC round-trip tests for the three new methods. Replaces #24784.
1 parent 7977123 commit 6996e91

20 files changed

Lines changed: 977 additions & 78 deletions

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,34 @@ describe('Archiver Sync', () => {
259259
expect(
260260
(await archiver.getCheckpoints({ from: CheckpointNumber(1), limit: 100 })).map(b => b.checkpoint.number),
261261
).toEqual([1, 2, 3]);
262+
263+
// Inbox buckets: each of the three L1 message blocks opened its own bucket, in insertion order.
264+
const t1 = fake.getTimestampAtL1Block(98n);
265+
const t2 = fake.getTimestampAtL1Block(2504n);
266+
const t3 = fake.getTimestampAtL1Block(2511n);
267+
268+
expect(await archiver.getInboxBucket(1n)).toMatchObject({
269+
seq: 1n,
270+
msgCount: 3,
271+
totalMsgCount: 3n,
272+
timestamp: t1,
273+
});
274+
expect(await archiver.getInboxBucket(3n)).toMatchObject({
275+
seq: 3n,
276+
msgCount: 3,
277+
totalMsgCount: 9n,
278+
timestamp: t3,
279+
});
280+
281+
// At-or-before lookups resolve the latest bucket not opened after the given timestamp.
282+
expect((await archiver.getLatestInboxBucketAtOrBefore(t3))!.seq).toEqual(3n);
283+
expect((await archiver.getLatestInboxBucketAtOrBefore(t2))!.seq).toEqual(2n);
284+
expect(await archiver.getLatestInboxBucketAtOrBefore(t1 - 1n)).toBeUndefined();
285+
286+
// Messages between buckets, in insertion order.
287+
expect(await archiver.getL1ToL2MessagesBetweenBuckets(0n, 3n)).toEqual([...msgs1, ...msgs2, ...msgs3]);
288+
expect(await archiver.getL1ToL2MessagesBetweenBuckets(1n, 2n)).toEqual(msgs2);
289+
expect(await archiver.getL1ToL2MessagesBetweenBuckets(2n, 3n)).toEqual(msgs3);
262290
}, 30_000);
263291

264292
it('ignores checkpoint 3 because it has been pruned', async () => {

yarn-project/archiver/src/errors.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ export class L1ToL2MessagesNotReadyError extends Error {
116116
}
117117
}
118118

119+
/**
120+
* Thrown when a query names an Inbox bucket this archiver has not synced. Distinguishes "not synced yet, retry once
121+
* L1 sync catches up" from a genuinely empty result.
122+
*/
123+
export class InboxBucketNotSyncedError extends Error {
124+
constructor(public readonly bucketSeq: bigint) {
125+
super(`Inbox bucket ${bucketSeq} has not been synced`);
126+
this.name = 'InboxBucketNotSyncedError';
127+
}
128+
}
129+
119130
/** Thrown when a proposed checkpoint number is stale (already processed). */
120131
export class ProposedCheckpointStaleError extends Error {
121132
constructor(

yarn-project/archiver/src/l1/data_retrieval.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,9 @@ function mapLogInboxMessage(log: MessageSentLog): InboxMessage {
387387
l1BlockHash: log.l1BlockHash,
388388
checkpointNumber: log.args.checkpointNumber,
389389
rollingHash: log.args.rollingHash,
390+
inboxRollingHash: log.args.inboxRollingHash,
391+
bucketSeq: log.args.bucketSeq,
392+
bucketTimestamp: log.l1BlockTimestamp,
390393
};
391394
}
392395

yarn-project/archiver/src/modules/data_source_base.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
} from '@aztec/stdlib/epoch-helpers';
4141
import type { L2LogsSource } from '@aztec/stdlib/interfaces/server';
4242
import type { LogResult, PrivateLogsQuery, PublicLogsQuery } from '@aztec/stdlib/logs';
43-
import type { L1ToL2MessageSource, L2ToL1MembershipWitness } from '@aztec/stdlib/messaging';
43+
import type { InboxBucket, L1ToL2MessageSource, L2ToL1MembershipWitness } from '@aztec/stdlib/messaging';
4444
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
4545
import type { BlockHeader, IndexedTxEffect, TxHash } from '@aztec/stdlib/tx';
4646
import type { UInt64 } from '@aztec/stdlib/types';
@@ -324,6 +324,18 @@ export abstract class ArchiverDataSourceBase
324324
return this.stores.messages.getL1ToL2MessageIndex(l1ToL2Message);
325325
}
326326

327+
public getLatestInboxBucketAtOrBefore(timestamp: bigint): Promise<InboxBucket | undefined> {
328+
return this.stores.messages.getLatestInboxBucketAtOrBefore(timestamp);
329+
}
330+
331+
public getInboxBucket(seq: bigint): Promise<InboxBucket | undefined> {
332+
return this.stores.messages.getInboxBucket(seq);
333+
}
334+
335+
public getL1ToL2MessagesBetweenBuckets(fromExclusive: bigint, toInclusive: bigint): Promise<Fr[]> {
336+
return this.stores.messages.getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive);
337+
}
338+
327339
private async getPublishedCheckpointFromCheckpointData(checkpoint: CheckpointData): Promise<PublishedCheckpoint> {
328340
const blocksForCheckpoint = await this.stores.blocks.getBlocksForCheckpoint(checkpoint.checkpointNumber);
329341
if (!blocksForCheckpoint) {

yarn-project/archiver/src/modules/l1_synchronizer.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,10 @@ export class ArchiverL1Synchronizer implements Traceable {
495495
);
496496
}
497497

498-
/** Retrieves L1 to L2 messages from L1 in batches and stores them. */
498+
/**
499+
* Retrieves L1 to L2 messages from L1 in batches and stores them. Batches must span whole L1 blocks so that every
500+
* message of an Inbox bucket is stored in a single call, which the message store requires.
501+
*/
499502
private async retrieveAndStoreMessages(fromL1Block: bigint, toL1Block: bigint): Promise<void> {
500503
let searchStartBlock: bigint = 0n;
501504
let searchEndBlock: bigint = fromL1Block;
@@ -508,7 +511,7 @@ export class ArchiverL1Synchronizer implements Traceable {
508511
this.log.trace(`Retrieving L1 to L2 messages in L1 blocks ${searchStartBlock}-${searchEndBlock}`);
509512
const messages = await retrieveL1ToL2Messages(this.inbox, searchStartBlock, searchEndBlock);
510513
const timer = new Timer();
511-
await this.stores.messages.addL1ToL2Messages(messages);
514+
await this.stores.messages.addL1ToL2MessageBuckets(messages);
512515
const perMsg = timer.ms() / messages.length;
513516
this.instrumentation.processNewMessages(messages.length, perMsg);
514517
for (const msg of messages) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { FunctionNamesCache } from './function_names_cache.js';
1313
import { LogStore } from './log_store.js';
1414
import { MessageStore } from './message_store.js';
1515

16-
export const ARCHIVER_DB_VERSION = 7;
16+
export const ARCHIVER_DB_VERSION = 8;
1717

1818
/**
1919
* Represents the latest L1 block processed by the archiver for various objects in L2.

0 commit comments

Comments
 (0)