Skip to content

Commit 57d3ec8

Browse files
committed
feat(fast-inbox): sequencer streaming message selection behind STREAMING_INBOX (A-1382)
Behind the shared `streamingInbox` flag (env `STREAMING_INBOX`, default off), the sequencer selects L1-to-L2 messages from the Inbox rolling-hash buckets per block instead of consuming the whole checkpoint's messages up front. - Adds a pure `inbox_bucket_selector` that mirrors `ProposeLib.validateInboxConsumption`: newest lag-eligible bucket, per-block/per-checkpoint cap walk-back with cap-escape, and a last-block cutoff floor (cutoff = toTimestamp(slot-1) - INBOX_LAG_SECONDS). - Adds `INBOX_LAG_SECONDS` (12) to constants.nr, regenerated into the TS constants. - Threads a per-block message bundle through the checkpoint builder interface and an optional signed bucket reference onto block proposals. - Flag off is byte-identical: the new paths are gated at checkpoint start and per block.
1 parent 4e21b51 commit 57d3ec8

16 files changed

Lines changed: 660 additions & 18 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ pub global L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH: u32 =
6969
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;
72+
// 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
74+
// buckets they may not yet have seen. L2 consensus policy consumed by the sequencer and validator (and mirrored by
75+
// ProposeLib on L1); the protocol circuits do not read it.
76+
pub global INBOX_LAG_SECONDS: u32 = 12;
7277
// Maximum number of subtrees a L2ToL1Msg unbalanced tree can have. Used when calculating the out hash of a tx.
7378
pub global MAX_L2_TO_L1_MSG_SUBTREES_PER_TX: u32 = 3; // ceil(log2(MAX_L2_TO_L1_MSGS_PER_TX))
7479

yarn-project/constants/src/constants.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const NULLIFIER_SUBTREE_ROOT_SIBLING_PATH_LENGTH = 36;
3131
export const L1_TO_L2_MSG_SUBTREE_ROOT_SIBLING_PATH_LENGTH = 26;
3232
export const MAX_L1_TO_L2_MSGS_PER_BLOCK = 1024;
3333
export const MAX_L1_TO_L2_MSGS_PER_CHECKPOINT = 1024;
34+
export const INBOX_LAG_SECONDS = 12;
3435
export const MAX_L2_TO_L1_MSG_SUBTREES_PER_TX = 3;
3536
export const MAX_NOTE_HASHES_PER_TX = 64;
3637
export const MAX_NULLIFIERS_PER_TX = 64;

yarn-project/foundation/src/config/env_var.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ export type EnvVar =
382382
| 'FISHERMAN_MODE'
383383
| 'MAX_ALLOWED_ETH_CLIENT_DRIFT_SECONDS'
384384
| 'MAX_BLOCKS_PER_CHECKPOINT'
385+
| 'STREAMING_INBOX'
385386
| 'LEGACY_BLS_CLI'
386387
| 'DEBUG_FORCE_TX_PROOF_VERIFICATION'
387388
| 'VALIDATOR_ALLOW_EPHEMERAL_SIGNING_PROTECTION'

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@ export class LightweightCheckpointBuilder {
7070
db: MerkleTreeWriteOperations,
7171
bindings?: LoggerBindings,
7272
feeAssetPriceModifier: bigint = 0n,
73+
// Streaming Inbox (AZIP-22 Fast Inbox): messages are inserted per block via `addBlock`, so `l1ToL2Messages` here
74+
// is empty and the up-front checkpoint-wide insertion is skipped.
75+
insertMessagesPerBlock: boolean = false,
7376
): Promise<LightweightCheckpointBuilder> {
74-
// Insert l1-to-l2 messages into the tree.
75-
await appendL1ToL2MessagesToTree(db, l1ToL2Messages);
77+
// Insert l1-to-l2 messages into the tree (legacy flow: the whole checkpoint's messages up front).
78+
if (!insertMessagesPerBlock) {
79+
await appendL1ToL2MessagesToTree(db, l1ToL2Messages);
80+
}
7681

7782
return new LightweightCheckpointBuilder(
7883
checkpointNumber,
@@ -172,7 +177,7 @@ export class LightweightCheckpointBuilder {
172177
public async addBlock(
173178
globalVariables: GlobalVariables,
174179
txs: ProcessedTx[],
175-
opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference } = {},
180+
opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference; l1ToL2Messages?: Fr[] } = {},
176181
): Promise<{ block: L2Block; timings: Record<string, number> }> {
177182
const timings: Record<string, number> = {};
178183
const isFirstBlock = this.blocks.length === 0;
@@ -203,6 +208,20 @@ export class LightweightCheckpointBuilder {
203208
timings.insertSideEffects = msInsertSideEffects;
204209
}
205210

211+
// Streaming Inbox (AZIP-22 Fast Inbox): insert this block's L1-to-L2 message bundle before reading the end state,
212+
// so the block header's L1-to-L2 tree snapshot reflects it. First-in-checkpoint bundles are padded to
213+
// NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP (matching the legacy per-checkpoint insertion and the world-state
214+
// synchronizer); non-first bundles are appended compactly. The rolling hash and inHash are then recomputed over
215+
// the accumulated logical (unpadded) messages at checkpoint completion.
216+
if (opts.l1ToL2Messages !== undefined) {
217+
if (isFirstBlock) {
218+
await appendL1ToL2MessagesToTree(this.db, opts.l1ToL2Messages);
219+
} else {
220+
await this.db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, opts.l1ToL2Messages);
221+
}
222+
this.l1ToL2Messages.push(...opts.l1ToL2Messages);
223+
}
224+
206225
const [msGetEndState, endState] = await elapsed(() => this.db.getStateReference());
207226
timings.getEndState = msGetEndState;
208227

yarn-project/sequencer-client/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const DefaultSequencerConfig = {
6868
skipPushProposedBlocksToArchiver: false,
6969
skipPublishingCheckpointsPercent: 0,
7070
maxBlocksPerCheckpoint: DEFAULT_MAX_BLOCKS_PER_CHECKPOINT,
71+
streamingInbox: false,
7172
} satisfies ResolvedSequencerConfig;
7273

7374
/**

yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
type ResolvedSequencerConfig,
4141
type WorldStateSynchronizer,
4242
} from '@aztec/stdlib/interfaces/server';
43-
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
43+
import type { InboxBucket, L1ToL2MessageSource } from '@aztec/stdlib/messaging';
4444
import { BlockProposal, CheckpointProposal, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
4545
import { CheckpointHeader } from '@aztec/stdlib/rollup';
4646
import type { ProposerTimetable, SubslotSelection } from '@aztec/stdlib/timetable';
@@ -1394,6 +1394,60 @@ describe('CheckpointProposalJob', () => {
13941394
});
13951395
});
13961396

1397+
describe('streaming inbox (flag on)', () => {
1398+
beforeEach(() => {
1399+
job.setTimetable(makeProposerTimetable({ l1Constants, blockDurationMs: 3000 }));
1400+
job.updateConfig({ streamingInbox: true });
1401+
});
1402+
1403+
it('streams per-block bucket bundles, advances the reference, and skips the bulk message fetch', async () => {
1404+
jest
1405+
.spyOn(job.getTimetable(), 'selectNextSubslot')
1406+
.mockReturnValueOnce(subslot(10, 0, false))
1407+
.mockReturnValueOnce(subslot(18, 1, true))
1408+
.mockReturnValue(noSubslot());
1409+
1410+
// The archiver returns the newest synced bucket (seq 2) for any lag/cutoff lookup, so the first block
1411+
// consumes through it and the second block (cursor already at seq 2) consumes nothing and reuses the ref.
1412+
const bucket: InboxBucket = {
1413+
seq: 2n,
1414+
inboxRollingHash: new Fr(99),
1415+
totalMsgCount: 5n,
1416+
timestamp: 0n,
1417+
msgCount: 2,
1418+
lastMessageIndex: 4n,
1419+
isOpen: false,
1420+
};
1421+
const bundle = Array.from({ length: 5 }, (_, i) => new Fr(i + 1));
1422+
l1ToL2MessageSource.getLatestInboxBucketAtOrBefore.mockResolvedValue(bucket);
1423+
l1ToL2MessageSource.getL1ToL2MessagesBetweenBuckets.mockResolvedValue(bundle);
1424+
1425+
const { lastBlock } = await setupMultipleBlocks(2, [2, 1]);
1426+
validatorClient.collectAttestations.mockResolvedValue(getAttestations(lastBlock));
1427+
1428+
const checkpoint = await job.executeAndAwait();
1429+
1430+
expect(checkpoint).toBeDefined();
1431+
1432+
// Streaming skips the bulk per-checkpoint fetch and tells the builder to insert messages per block.
1433+
expect(l1ToL2MessageSource.getL1ToL2Messages).not.toHaveBeenCalled();
1434+
expect(checkpointsBuilder.startCheckpointCalls).toHaveLength(1);
1435+
expect(checkpointsBuilder.startCheckpointCalls[0].l1ToL2Messages).toEqual([]);
1436+
expect(checkpointsBuilder.startCheckpointCalls[0].insertMessagesPerBlock).toBe(true);
1437+
1438+
// The first block consumes the selected bundle; the second consumes nothing.
1439+
expect(checkpointBuilder.buildBlockCalls).toHaveLength(2);
1440+
expect(checkpointBuilder.buildBlockCalls[0].opts.l1ToL2Messages).toEqual(bundle);
1441+
expect(checkpointBuilder.buildBlockCalls[1].opts.l1ToL2Messages).toEqual([]);
1442+
1443+
// Both block proposals carry the selected bucket reference (the second reuses the first's).
1444+
const bucketRefArgs = validatorClient.createBlockProposal.mock.calls.map(call => call[8]);
1445+
expect(bucketRefArgs).toHaveLength(2);
1446+
expect(bucketRefArgs[0]?.bucketSeq).toBe(2n);
1447+
expect(bucketRefArgs[1]?.bucketSeq).toBe(2n);
1448+
});
1449+
});
1450+
13971451
describe('build single block', () => {
13981452
it('does not build a block if not enough valid txs are collected', async () => {
13991453
// We have enough txs, but not enough valid ones

0 commit comments

Comments
 (0)