Skip to content

Commit 66a5672

Browse files
committed
test(fast-inbox): prover orchestrator multi-block message-slice coverage (A-1385)
Add a checkpoint-sub-tree orchestrator test for a checkpoint whose L1-to-L2 messages span multiple blocks, including a non-first block carrying a bundle. Asserts per-block start/end L1-to-L2 snapshot continuity and slice partitioning (no gap/overlap; block slice = [prevBlockLeafCount, blockLeafCount)). Adds TestContext.makeCheckpointWithMessagesPerBlock to distribute a bundle across a checkpoint's blocks (the single-block-per-checkpoint makeCheckpoint puts every message in the first block).
1 parent bcc37cf commit 66a5672

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

yarn-project/prover-client/src/mocks/test_context.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,135 @@ export class TestContext {
283283
};
284284
}
285285

286+
/**
287+
* Like {@link makeCheckpoint} but distributes the L1-to-L2 message bundle across the checkpoint's blocks
288+
* (streaming Inbox / AZIP-22 Fast Inbox): `l1ToL2MessagesPerBlock[i]` is block `i`'s own message slice,
289+
* appended at compact indices in insertion order. Lets tests exercise checkpoints whose messages span more
290+
* than one block, including a non-first block carrying a bundle — the single-block-per-checkpoint
291+
* `makeCheckpoint` puts every message in the first block. Non-first blocks must carry at least one tx (an
292+
* empty non-first block is rejected by the builder), so `numTxsPerBlock` defaults to 1.
293+
*/
294+
public async makeCheckpointWithMessagesPerBlock(
295+
l1ToL2MessagesPerBlock: Fr[][],
296+
{
297+
numTxsPerBlock = 1,
298+
makeProcessedTxOpts = () => ({}),
299+
...constantOpts
300+
}: {
301+
numTxsPerBlock?: number | number[];
302+
makeProcessedTxOpts?: (
303+
blockGlobalVariables: GlobalVariables,
304+
txIndex: number,
305+
) => Partial<Parameters<typeof mockProcessedTx>[0]>;
306+
} & Partial<FieldsOf<CheckpointConstantData>> = {},
307+
) {
308+
const numBlocks = l1ToL2MessagesPerBlock.length;
309+
if (numBlocks === 0) {
310+
throw new Error('Cannot make a checkpoint with 0 blocks.');
311+
}
312+
313+
const checkpointIndex = this.nextCheckpointIndex++;
314+
const checkpointNumber = this.nextCheckpointNumber;
315+
this.nextCheckpointNumber++;
316+
const slotNumber = checkpointNumber * 15;
317+
318+
const constants = makeCheckpointConstants(slotNumber, constantOpts);
319+
const l1ToL2Messages = l1ToL2MessagesPerBlock.flat();
320+
321+
const fork = await this.worldState.fork();
322+
323+
const startBlockNumber = this.nextBlockNumber;
324+
const previousBlockHeader = this.getBlockHeader(BlockNumber(startBlockNumber - 1));
325+
const timestamp = BigInt(slotNumber * 26);
326+
327+
const blockGlobalVariables = times(numBlocks, i =>
328+
makeGlobals(startBlockNumber + i, slotNumber, {
329+
coinbase: constants.coinbase,
330+
feeRecipient: constants.feeRecipient,
331+
gasFees: constants.gasFees,
332+
timestamp,
333+
}),
334+
);
335+
this.nextBlockNumber += numBlocks;
336+
337+
// Build txs per block. Append the block's message slice to the fork before its txs so the per-block end
338+
// state matches the builder's (message-tree and tx-effect trees are independent, so append order does not
339+
// matter, but the cumulative message tree must reflect the slices already inserted).
340+
let totalTxs = 0;
341+
const blockEndStates: StateReference[] = [];
342+
const blockTxs = await timesAsync(numBlocks, async blockIndex => {
343+
await fork.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2MessagesPerBlock[blockIndex]);
344+
const newL1ToL2Snapshot = await getTreeSnapshot(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, fork);
345+
346+
const txIndexOffset = totalTxs;
347+
const numTxs = typeof numTxsPerBlock === 'number' ? numTxsPerBlock : numTxsPerBlock[blockIndex];
348+
totalTxs += numTxs;
349+
const txs = await timesAsync(numTxs, txIndex =>
350+
this.makeProcessedTx({
351+
seed: (txIndexOffset + txIndex + 1) * 321 + (checkpointIndex + 1) * 123456 + this.epochNumber * 0x99999,
352+
globalVariables: blockGlobalVariables[blockIndex],
353+
anchorBlockHeader: previousBlockHeader,
354+
newL1ToL2Snapshot,
355+
...makeProcessedTxOpts(blockGlobalVariables[blockIndex], txIndexOffset + txIndex),
356+
}),
357+
);
358+
359+
const endState = await this.updateTrees(txs, fork);
360+
blockEndStates.push(endState);
361+
362+
return txs;
363+
});
364+
365+
const cleanFork = await this.worldState.fork();
366+
const previousCheckpointOutHashes = this.checkpointOutHashes;
367+
// Empty checkpoint-level list + `insertMessagesPerBlock` so the builder inserts each block's slice via
368+
// `addBlock` (and accumulates them into the checkpoint's rolling hash) rather than up front.
369+
const builder = await LightweightCheckpointBuilder.startNewCheckpoint(
370+
checkpointNumber,
371+
{ ...constants, timestamp },
372+
[],
373+
previousCheckpointOutHashes,
374+
Fr.ZERO,
375+
cleanFork,
376+
undefined,
377+
0n,
378+
true,
379+
);
380+
381+
const blocks = [];
382+
for (let i = 0; i < numBlocks; i++) {
383+
const txs = blockTxs[i];
384+
const state = blockEndStates[i];
385+
386+
const { block } = await builder.addBlock(blockGlobalVariables[i], txs, {
387+
expectedEndState: state,
388+
insertTxsEffects: true,
389+
l1ToL2Messages: l1ToL2MessagesPerBlock[i],
390+
});
391+
392+
const header = block.header;
393+
this.headers.set(block.number, header);
394+
395+
await this.worldState.handleL2BlockAndMessages(block, l1ToL2MessagesPerBlock[i]);
396+
397+
blocks.push({ header, txs });
398+
}
399+
400+
const checkpoint = await builder.completeCheckpoint();
401+
this.checkpoints.push(checkpoint);
402+
this.checkpointOutHashes.push(checkpoint.getCheckpointOutHash());
403+
404+
return {
405+
constants,
406+
checkpoint,
407+
header: checkpoint.header,
408+
blocks,
409+
l1ToL2Messages,
410+
l1ToL2MessagesPerBlock,
411+
previousBlockHeader,
412+
};
413+
}
414+
286415
private async makeProcessedTx(opts: Parameters<typeof mockProcessedTx>[0] = {}): Promise<ProcessedTx> {
287416
const tx = await mockProcessedTx({
288417
vkTreeRoot: getVKTreeRoot(),

yarn-project/prover-client/src/orchestrator/checkpoint-sub-tree-orchestrator.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,77 @@ describe('prover/orchestrator/checkpoint-sub-tree', () => {
219219
await subTree.stop();
220220
}
221221
});
222+
223+
it('slices L1-to-L2 messages per block across a multi-block checkpoint', async () => {
224+
// A checkpoint whose messages span more than one block: the first block carries a bundle, a middle block
225+
// carries none (txs only), and a non-first block carries a bundle. The sub-tree must append each block's
226+
// own slice at compact indices with contiguous, non-overlapping per-block snapshots (AZIP-22 Fast Inbox).
227+
const l1ToL2MessagesPerBlock = [[new Fr(1001), new Fr(1002)], [], [new Fr(1003), new Fr(1004), new Fr(1005)]];
228+
const numBlocks = l1ToL2MessagesPerBlock.length;
229+
const { constants, blocks, l1ToL2Messages, previousBlockHeader } = await context.makeCheckpointWithMessagesPerBlock(
230+
l1ToL2MessagesPerBlock,
231+
{ numTxsPerBlock: 1 },
232+
);
233+
expect(l1ToL2Messages.length).toBe(5);
234+
235+
const subTree = await CheckpointSubTreeOrchestrator.start(
236+
context.worldState,
237+
context.prover,
238+
EthAddress.ZERO,
239+
chonkCache,
240+
EpochNumber(1),
241+
false,
242+
makeTestDeferredJobQueue(),
243+
constants,
244+
l1ToL2Messages,
245+
Fr.ZERO,
246+
numBlocks,
247+
previousBlockHeader,
248+
);
249+
try {
250+
const resultPromise = subTree.getSubTreeResult();
251+
252+
for (const [blockIndex, block] of blocks.entries()) {
253+
const { blockNumber, timestamp } = block.header.globalVariables;
254+
await subTree.startNewBlock(blockNumber, timestamp, block.txs.length, l1ToL2MessagesPerBlock[blockIndex]);
255+
if (block.txs.length > 0) {
256+
await subTree.addTxs(block.txs);
257+
}
258+
await subTree.setBlockCompleted(blockNumber, block.header);
259+
}
260+
261+
const result = await resultPromise;
262+
expect(result.blockProofOutputs).toHaveLength(numBlocks);
263+
264+
// Order the block outputs by block number (the archive tree grows by exactly one leaf per block).
265+
const ordered = [...result.blockProofOutputs].sort(
266+
(a, b) => a.inputs.previousArchive.nextAvailableLeafIndex - b.inputs.previousArchive.nextAvailableLeafIndex,
267+
);
268+
269+
// Walk the blocks in order, asserting the L1-to-L2 message tree partitions cleanly into per-block slices,
270+
// with each block's start snapshot equal to the previous block's end snapshot (the "threaded" per-block
271+
// L1-to-L2 tree state).
272+
const baseLeaf = ordered[0].inputs.startState.l1ToL2MessageTree.nextAvailableLeafIndex;
273+
let expectedStartLeaf = baseLeaf;
274+
for (const [i, output] of ordered.entries()) {
275+
const inputs = output.inputs;
276+
const startLeaf = inputs.startState.l1ToL2MessageTree.nextAvailableLeafIndex;
277+
const endLeaf = inputs.endState.l1ToL2MessageTree.nextAvailableLeafIndex;
278+
const sliceLen = l1ToL2MessagesPerBlock[i].length;
279+
280+
// Only the checkpoint's first block flags isFirstBlock.
281+
expect(inputs.isFirstBlock).toBe(i === 0);
282+
// Contiguous, non-overlapping slices: this block starts where the previous one ended (no gap/overlap).
283+
expect(startLeaf).toBe(expectedStartLeaf);
284+
// Block slice = [prevBlockLeafCount, blockLeafCount): the tree grows by exactly this block's bundle size.
285+
expect(endLeaf - startLeaf).toBe(sliceLen);
286+
expectedStartLeaf = endLeaf;
287+
}
288+
289+
// Every message is accounted for with no gap or overlap across the checkpoint's blocks.
290+
expect(expectedStartLeaf - baseLeaf).toBe(l1ToL2Messages.length);
291+
} finally {
292+
await subTree.stop();
293+
}
294+
});
222295
});

0 commit comments

Comments
 (0)