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