Skip to content

Commit e5258bd

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 de56dbe commit e5258bd

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
@@ -284,6 +284,135 @@ export class TestContext {
284284
};
285285
}
286286

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