@@ -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 ( ) ,
0 commit comments