Skip to content

Commit c732d00

Browse files
committed
feat(fast-inbox): validator streaming acceptance conditions, flag off (A-1383)
Implements A-1383: the validator's streaming-Inbox acceptance conditions (AZIP-22 Fast Inbox), behind the shared `streamingInbox` flag (default off). Flag off ⇒ byte-identical behavior. Stacked on #24787 (A-1382). ## The four block-proposal checks (`validator-client/src/streaming_inbox_checks.ts`, wired in `proposal_handler.handleBlockProposal`) - **Exists**: the referenced bucket resolves in the node's own Inbox view and its consensus rolling hash matches the reference. An unknown bucket is an immediate reject (`bucket_unknown`); the bounded-wait soft path is A-1393. The reference is trusted only as a `bucketSeq` lookup hint — timestamp/counts are read from the locally resolved bucket. - **Moves forward**: the bucket's cumulative total is at least the parent block's (equal ⇒ empty bundle). - **Not too new**: the bucket is at least `INBOX_LAG_SECONDS` old at validation time (`dateProvider.now()`, inclusive boundary). - **Caps**: the per-block count and the running per-checkpoint total fit `MAX_L1_TO_L2_MSGS_PER_BLOCK` / `MAX_L1_TO_L2_MSGS_PER_CHECKPOINT`. The derived bundle is fed into per-block re-execution (`insertMessagesPerBlock` threaded through `openCheckpoint` → `reexecuteTransactions`); the existing state-ref comparison after re-execution stays the final arbiter. ## Last-block censorship check (`validateCheckpointProposal`) Before attesting, the minimum-consumption rule runs via the shared `isInboxConsumptionSufficient` predicate: the first unconsumed bucket must be absent, past the cutoff, or a cap-escape. A mandatory unconsumed bucket rejects the checkpoint (no attestation, `inbox_consumption_insufficient`). ## Shared-predicate extraction (`refactor` commit) The cutoff formula and the minimum-consumption/cap-escape rule moved to `stdlib/messaging/inbox_consumption`, so the sequencer's bucket selection and the validator share one source of truth. The sequencer's `selectStreamingBundle` now calls `getInboxCutoffTimestamp` instead of inlining it — behavior unchanged (selector suite still green). ## Corrected cutoff anchor The cutoff is `getTimestampForSlot(slot - 1) - INBOX_LAG_SECONDS` (mirroring `ProposeLib.validateInboxConsumption`), **not** `getBuildFrameStart`. The A-1371 §13 cross-layer vectors are pinned in `inbox_consumption.test.ts`. ## Parent-ref derivation Parent/last-consumed buckets are resolved from L1-to-L2 tree leaf counts (compact post-flip indexing == bucket cumulative total) via a new `getInboxBucketByTotalMsgCount` archiver lookup. A leaf count that does not land on a bucket boundary (a pre-flip padded parent) is rejected/deferred; the flip (A-1384) closes that gap. ## CI must typecheck what can't be verified locally `tsc -b` cannot complete locally due to pre-existing `noir-protocol-circuits-types` breakage (missing artifacts / stale generated types), which cascades to `archiver`/`validator-client`. stdlib builds clean; no errors in the changed files outside the broken zone. Runnable unit suites are green: `streaming_inbox_checks` (14), `inbox_consumption` (10), `proposal_handler` (34, incl. flag-off unchanged + new streaming/censorship tests), `message_store` (35), stdlib `archiver` interface (RPC schema). CI is the typecheck of record for the unbuildable zone. Out of scope: bounded-wait (A-1393), the flip (A-1384), reorg rejection (A-1389), the full streaming checkpoint rebuild (A-1385). Replaces #24788.
1 parent 2d6dee2 commit c732d00

29 files changed

Lines changed: 1230 additions & 54 deletions

noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub global MAX_L1_TO_L2_MSGS_PER_BLOCK: u32 = 1024;
7070
// Cap on L1-to-L2 messages consumed by a single checkpoint. Semantically today's NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP.
7171
pub global MAX_L1_TO_L2_MSGS_PER_CHECKPOINT: u32 = 1024;
7272
// Minimum bucket age, in seconds, at the start of a checkpoint's build frame for its consumption to be mandatory under
73-
// the streaming Inbox censorship cutoff (AZIP-22 Fast Inbox). One L1 slot: validators cannot be required to act on
73+
// the streaming Inbox censorship cutoff. One L1 slot: validators cannot be required to act on
7474
// buckets they may not yet have seen. L2 consensus policy consumed by the sequencer and validator (and mirrored by
7575
// ProposeLib on L1); the protocol circuits do not read it.
7676
pub global INBOX_LAG_SECONDS: u32 = 12;

yarn-project/archiver/src/errors.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ export class InboxBucketNotSyncedError extends Error {
127127
}
128128
}
129129

130+
/**
131+
* Thrown when a cumulative Inbox message count does not resolve to a bucket boundary this archiver has synced, either
132+
* because the count sits inside a bucket or because the bucket is not synced yet.
133+
*/
134+
export class InboxBucketBoundaryNotSyncedError extends Error {
135+
constructor(public readonly totalMsgCount: bigint) {
136+
super(`No synced Inbox bucket ends at cumulative message count ${totalMsgCount}`);
137+
this.name = 'InboxBucketBoundaryNotSyncedError';
138+
}
139+
}
140+
130141
/** Thrown when a proposed checkpoint number is stale (already processed). */
131142
export class ProposedCheckpointStaleError extends Error {
132143
constructor(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,18 @@ export abstract class ArchiverDataSourceBase
332332
return this.stores.messages.getInboxBucket(seq);
333333
}
334334

335+
public getInboxBucketByTotalMsgCount(totalMsgCount: bigint): Promise<InboxBucket | undefined> {
336+
return this.stores.messages.getInboxBucketByTotalMsgCount(totalMsgCount);
337+
}
338+
335339
public getL1ToL2MessagesBetweenBuckets(fromExclusive: bigint, toInclusive: bigint): Promise<Fr[]> {
336340
return this.stores.messages.getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive);
337341
}
338342

343+
public getL1ToL2MessagesBetweenLeafCounts(startLeafCount: bigint, endLeafCount: bigint): Promise<Fr[]> {
344+
return this.stores.messages.getL1ToL2MessagesBetweenLeafCounts(startLeafCount, endLeafCount);
345+
}
346+
339347
private async getPublishedCheckpointFromCheckpointData(checkpoint: CheckpointData): Promise<PublishedCheckpoint> {
340348
const blocksForCheckpoint = await this.stores.blocks.getBlocksForCheckpoint(checkpoint.checkpointNumber);
341349
if (!blocksForCheckpoint) {

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { Checkpoint, type PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
88
import { updateInboxRollingHash } from '@aztec/stdlib/messaging';
99
import '@aztec/stdlib/testing/jest';
1010

11-
import { InboxBucketNotSyncedError, L1ToL2MessagesNotReadyError } from '../errors.js';
11+
import {
12+
InboxBucketBoundaryNotSyncedError,
13+
InboxBucketNotSyncedError,
14+
L1ToL2MessagesNotReadyError,
15+
} from '../errors.js';
1216
import { type InboxMessage, updateRollingHash } from '../structs/inbox_message.js';
1317
import {
1418
makeInboxMessage,
@@ -401,6 +405,28 @@ describe('MessageStore', () => {
401405
expect(await messageStore.getInboxBucket(4n)).toBeUndefined();
402406
});
403407

408+
it('resolves a bucket by its cumulative message total', async () => {
409+
const msgs = makeBucketedMessages(threeBucketSpec);
410+
await messageStore.addL1ToL2MessageBuckets(msgs);
411+
412+
// Each bucket boundary (cumulative totals 3, 5, 6) resolves to its bucket.
413+
expect((await messageStore.getInboxBucketByTotalMsgCount(3n))?.seq).toEqual(1n);
414+
expect((await messageStore.getInboxBucketByTotalMsgCount(5n))?.seq).toEqual(2n);
415+
expect((await messageStore.getInboxBucketByTotalMsgCount(6n))?.seq).toEqual(3n);
416+
// A total inside a bucket (not on a boundary) does not resolve.
417+
expect(await messageStore.getInboxBucketByTotalMsgCount(4n)).toBeUndefined();
418+
// A total past the last synced bucket does not resolve.
419+
expect(await messageStore.getInboxBucketByTotalMsgCount(7n)).toBeUndefined();
420+
});
421+
422+
it('synthesizes the genesis sentinel bucket (sequence 0, total 0) which is never ingested', async () => {
423+
// With real messages present but no ingested sequence-0 snapshot, both lookups still resolve genesis.
424+
await messageStore.addL1ToL2MessageBuckets(makeBucketedMessages(threeBucketSpec));
425+
426+
expect(await messageStore.getInboxBucket(0n)).toMatchObject({ seq: 0n, totalMsgCount: 0n, msgCount: 0 });
427+
expect(await messageStore.getInboxBucketByTotalMsgCount(0n)).toMatchObject({ seq: 0n, totalMsgCount: 0n });
428+
});
429+
404430
it('rejects a bucket delivered without the messages already stored for it', async () => {
405431
const msgs = makeBucketedMessages(threeBucketSpec);
406432
await messageStore.addL1ToL2MessageBuckets(msgs.slice(0, 2));
@@ -492,6 +518,40 @@ describe('MessageStore', () => {
492518
await expect(messageStore.getL1ToL2MessagesBetweenBuckets(4n, 3n)).rejects.toThrow(/Invalid Inbox bucket range/);
493519
});
494520

521+
it('returns messages between cumulative leaf counts', async () => {
522+
const msgs = makeBucketedMessages(threeBucketSpec);
523+
await messageStore.addL1ToL2MessageBuckets(msgs);
524+
const leaves = msgs.map(m => m.leaf);
525+
526+
// Bucket boundaries sit at cumulative counts 0 (genesis), 3, 5 and 6.
527+
expect(await messageStore.getL1ToL2MessagesBetweenLeafCounts(0n, 6n)).toEqual(leaves);
528+
expect(await messageStore.getL1ToL2MessagesBetweenLeafCounts(0n, 3n)).toEqual(leaves.slice(0, 3));
529+
expect(await messageStore.getL1ToL2MessagesBetweenLeafCounts(3n, 5n)).toEqual(leaves.slice(3, 5));
530+
expect(await messageStore.getL1ToL2MessagesBetweenLeafCounts(5n, 6n)).toEqual(leaves.slice(5));
531+
// An empty range consumes nothing, at a bucket boundary or at genesis.
532+
expect(await messageStore.getL1ToL2MessagesBetweenLeafCounts(5n, 5n)).toEqual([]);
533+
expect(await messageStore.getL1ToL2MessagesBetweenLeafCounts(0n, 0n)).toEqual([]);
534+
});
535+
536+
it('throws when a leaf count does not land on a synced bucket boundary', async () => {
537+
const msgs = makeBucketedMessages(threeBucketSpec);
538+
await messageStore.addL1ToL2MessageBuckets(msgs);
539+
540+
// Counts inside a bucket and past the last synced bucket both fail rather than returning a partial range.
541+
await expect(messageStore.getL1ToL2MessagesBetweenLeafCounts(0n, 4n)).rejects.toThrow(
542+
InboxBucketBoundaryNotSyncedError,
543+
);
544+
await expect(messageStore.getL1ToL2MessagesBetweenLeafCounts(4n, 6n)).rejects.toThrow(
545+
InboxBucketBoundaryNotSyncedError,
546+
);
547+
await expect(messageStore.getL1ToL2MessagesBetweenLeafCounts(3n, 9n)).rejects.toThrow(
548+
InboxBucketBoundaryNotSyncedError,
549+
);
550+
await expect(messageStore.getL1ToL2MessagesBetweenLeafCounts(5n, 3n)).rejects.toThrow(
551+
/Invalid Inbox leaf count range/,
552+
);
553+
});
554+
495555
it('rewinds buckets when messages are removed', async () => {
496556
const msgs = makeBucketedMessages(threeBucketSpec);
497557
await messageStore.addL1ToL2MessageBuckets(msgs);

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

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
} from '@aztec/kv-store';
1616
import { type InboxBucket, InboxLeaf, updateInboxRollingHash } from '@aztec/stdlib/messaging';
1717

18-
import { InboxBucketNotSyncedError, L1ToL2MessagesNotReadyError } from '../errors.js';
18+
import {
19+
InboxBucketBoundaryNotSyncedError,
20+
InboxBucketNotSyncedError,
21+
L1ToL2MessagesNotReadyError,
22+
} from '../errors.js';
1923
import {
2024
type InboxMessage,
2125
deserializeInboxMessage,
@@ -85,6 +89,19 @@ function groupMessagesByBucket(messages: InboxMessage[]): IncomingBucket[] {
8589
return buckets;
8690
}
8791

92+
// The genesis sentinel bucket: sequence 0 with a zero rolling hash and no messages, mirroring the
93+
// on-chain Inbox's base case. The archiver never ingests a snapshot for it (no message is absorbed into sequence 0), so
94+
// it is synthesized on read. Consumers use its sequence number and zero total; its deploy-time timestamp is not tracked
95+
// here and is unused.
96+
const GENESIS_INBOX_BUCKET: InboxBucket = {
97+
seq: 0n,
98+
inboxRollingHash: Fr.ZERO,
99+
totalMsgCount: 0n,
100+
timestamp: 0n,
101+
msgCount: 0,
102+
lastMessageIndex: 0n,
103+
};
104+
88105
export class MessageStoreError extends Error {
89106
constructor(
90107
message: string,
@@ -487,17 +504,65 @@ export class MessageStore {
487504
}
488505

489506
/**
490-
* Returns the Inbox bucket with the given sequence number, or undefined if it has not been synced (AZIP-22 Fast
491-
* Inbox).
507+
* Returns the Inbox bucket with the given sequence number, or undefined if it has not been synced. Sequence 0 is
508+
* the genesis sentinel: the on-chain Inbox reserves it as the "consumed nothing" base case
509+
* and never absorbs a message into it, so the archiver ingests no snapshot for it; it is synthesized here (rolling
510+
* hash 0, total 0) so streaming consumers can resolve a genesis parent or an empty checkpoint's last-consumed bucket.
492511
*/
493512
public async getInboxBucket(seq: bigint): Promise<InboxBucket | undefined> {
494513
const snapshot = await this.getBucketSnapshotBySeq(seq);
495-
return snapshot && this.toInboxBucket(seq, snapshot);
514+
if (snapshot !== undefined) {
515+
return this.toInboxBucket(seq, snapshot);
516+
}
517+
return seq === 0n ? GENESIS_INBOX_BUCKET : undefined;
518+
}
519+
520+
/**
521+
* Returns the Inbox bucket whose cumulative message total equals `totalMsgCount`, or undefined if no synced bucket
522+
* sits on that boundary. Sequence 0 (total 0) is the genesis sentinel base case; otherwise the
523+
* message at global index `totalMsgCount - 1` is the last message of the bucket with that cumulative total, so its
524+
* `bucketSeq` resolves the bucket. A total that does not land on a bucket boundary returns undefined.
525+
*/
526+
public async getInboxBucketByTotalMsgCount(totalMsgCount: bigint): Promise<InboxBucket | undefined> {
527+
if (totalMsgCount === 0n) {
528+
return this.getInboxBucket(0n);
529+
}
530+
const buffer = await this.#l1ToL2Messages.getAsync(this.indexToKey(totalMsgCount - 1n));
531+
if (buffer === undefined) {
532+
return undefined;
533+
}
534+
const bucket = await this.getInboxBucket(deserializeInboxMessage(buffer).bucketSeq);
535+
return bucket !== undefined && bucket.totalMsgCount === totalMsgCount ? bucket : undefined;
536+
}
537+
538+
/**
539+
* Returns the message leaves in the cumulative Inbox message-count range `[startLeafCount, endLeafCount)`, in
540+
* insertion order. The bounds are compact L1-to-L2 tree leaf counts, which every block header
541+
* carries, so consumers can ask for the messages a block or checkpoint consumed without resolving buckets
542+
* themselves. Both bounds must land on a bucket boundary this archiver has synced; it throws otherwise, since a
543+
* caller asking for a range always expects the messages in it.
544+
*/
545+
public async getL1ToL2MessagesBetweenLeafCounts(startLeafCount: bigint, endLeafCount: bigint): Promise<Fr[]> {
546+
if (startLeafCount > endLeafCount) {
547+
throw new Error(`Invalid Inbox leaf count range [${startLeafCount}, ${endLeafCount})`);
548+
}
549+
const startBucket = await this.getBucketAtBoundary(startLeafCount);
550+
const endBucket = await this.getBucketAtBoundary(endLeafCount);
551+
return this.getL1ToL2MessagesBetweenBuckets(startBucket.seq, endBucket.seq);
552+
}
553+
554+
/** Resolves the bucket ending at the given cumulative message count, failing loudly if there is none. */
555+
private async getBucketAtBoundary(totalMsgCount: bigint): Promise<InboxBucket> {
556+
const bucket = await this.getInboxBucketByTotalMsgCount(totalMsgCount);
557+
if (bucket === undefined) {
558+
throw new InboxBucketBoundaryNotSyncedError(totalMsgCount);
559+
}
560+
return bucket;
496561
}
497562

498563
/**
499564
* Returns the latest Inbox bucket opened at or before the given L1 timestamp, or undefined if every synced bucket
500-
* was opened strictly after it (AZIP-22 Fast Inbox).
565+
* was opened strictly after it.
501566
*/
502567
public async getLatestInboxBucketAtOrBefore(timestamp: bigint): Promise<InboxBucket | undefined> {
503568
// Bucket timestamps are non-decreasing in sequence number, so the bucket we want is the highest sequence indexed
@@ -519,8 +584,8 @@ export class MessageStore {
519584
}
520585

521586
/**
522-
* Returns the message leaves absorbed into buckets in the range `(fromExclusive, toInclusive]`, in insertion order
523-
* (AZIP-22 Fast Inbox). Both bounds must name buckets this archiver has synced, so that an empty result means the
587+
* Returns the message leaves absorbed into buckets in the range `(fromExclusive, toInclusive]`, in insertion order.
588+
* Both bounds must name buckets this archiver has synced, so that an empty result means the
524589
* range holds no messages rather than hiding an unsynced bound; callers route the
525590
* `InboxBucketNotSyncedError` to their own catch-up handling. Sequence 0 is the genesis base case and always
526591
* resolves: the range then starts at the first message of the Inbox.

yarn-project/archiver/src/structs/inbox_message.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export type InboxMessage = {
1212
l1BlockHash: Buffer32;
1313
/** Legacy 128-bit keccak rolling hash of all messages inserted up to and including this one. */
1414
rollingHash: Buffer16;
15-
/** Consensus rolling hash (truncated sha256 chain) of all messages up to and including this one (AZIP-22 Fast Inbox). */
15+
/** Consensus rolling hash (truncated sha256 chain) of all messages up to and including this one. */
1616
inboxRollingHash: Fr;
17-
/** Sequence number of the Inbox bucket this message was absorbed into (AZIP-22 Fast Inbox). */
17+
/** Sequence number of the Inbox bucket this message was absorbed into. */
1818
bucketSeq: bigint;
1919
/** L1 block timestamp at which this message's bucket was opened; the bucket's recency key, in seconds. */
2020
bucketTimestamp: bigint;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class FakeL1State {
143143
private checkpoints: CheckpointData[] = [];
144144
private messages: MessageData[] = [];
145145
private messagesRollingHash: Buffer16 = Buffer16.ZERO;
146-
// Consensus rolling-hash and bucket-ring state, mirroring the on-chain Inbox (AZIP-22 Fast Inbox).
146+
// Consensus rolling-hash and bucket-ring state, mirroring the on-chain Inbox.
147147
private messagesConsensusRollingHash: Fr = Fr.ZERO;
148148
private currentBucketSeq: bigint = 0n;
149149
private currentBucketTimestamp: bigint = 0n;

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,17 @@ export class MockArchiver extends MockL2BlockSource implements L2BlockSource, L1
3737
return this.messageSource.getInboxBucket(seq);
3838
}
3939

40+
getInboxBucketByTotalMsgCount(totalMsgCount: bigint): Promise<InboxBucket | undefined> {
41+
return this.messageSource.getInboxBucketByTotalMsgCount(totalMsgCount);
42+
}
43+
4044
getL1ToL2MessagesBetweenBuckets(fromExclusive: bigint, toInclusive: bigint): Promise<Fr[]> {
4145
return this.messageSource.getL1ToL2MessagesBetweenBuckets(fromExclusive, toInclusive);
4246
}
47+
48+
getL1ToL2MessagesBetweenLeafCounts(startLeafCount: bigint, endLeafCount: bigint): Promise<Fr[]> {
49+
return this.messageSource.getL1ToL2MessagesBetweenLeafCounts(startLeafCount, endLeafCount);
50+
}
4351
}
4452

4553
/**

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource {
3838
return Promise.resolve(this.buckets.get(seq));
3939
}
4040

41+
getInboxBucketByTotalMsgCount(totalMsgCount: bigint): Promise<InboxBucket | undefined> {
42+
if (totalMsgCount === 0n) {
43+
return Promise.resolve(this.buckets.get(0n));
44+
}
45+
return Promise.resolve([...this.buckets.values()].find(bucket => bucket.totalMsgCount === totalMsgCount));
46+
}
47+
4148
getLatestInboxBucketAtOrBefore(timestamp: bigint): Promise<InboxBucket | undefined> {
4249
const atOrBefore = [...this.buckets.values()]
4350
.filter(bucket => bucket.timestamp <= timestamp)
@@ -52,6 +59,15 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource {
5259
return Promise.resolve(seqs.flatMap(seq => this.messagesPerBucket.get(seq) ?? []));
5360
}
5461

62+
async getL1ToL2MessagesBetweenLeafCounts(startLeafCount: bigint, endLeafCount: bigint): Promise<Fr[]> {
63+
const startBucket = await this.getInboxBucketByTotalMsgCount(startLeafCount);
64+
const endBucket = await this.getInboxBucketByTotalMsgCount(endLeafCount);
65+
if (startBucket === undefined || endBucket === undefined) {
66+
throw new Error(`No mocked Inbox bucket boundary at ${startLeafCount} or ${endLeafCount}`);
67+
}
68+
return this.getL1ToL2MessagesBetweenBuckets(startBucket.seq, endBucket.seq);
69+
}
70+
5571
getBlockNumber() {
5672
return Promise.resolve(BlockNumber(this.blockNumber));
5773
}

yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export class LightweightCheckpointBuilder {
208208
timings.insertSideEffects = msInsertSideEffects;
209209
}
210210

211-
// Streaming Inbox (AZIP-22 Fast Inbox): insert this block's L1-to-L2 message bundle before reading the end state,
211+
// Streaming Inbox: insert this block's L1-to-L2 message bundle before reading the end state,
212212
// so the block header's L1-to-L2 tree snapshot reflects it. First-in-checkpoint bundles are padded to
213213
// NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP (matching the legacy per-checkpoint insertion and the world-state
214214
// synchronizer); non-first bundles are appended compactly. The logical (unpadded) messages are accumulated only

0 commit comments

Comments
 (0)