@@ -7,7 +7,7 @@ import { createLogger } from '@aztec/foundation/log';
77import { promiseWithResolvers } from '@aztec/foundation/promise' ;
88import { retryUntil } from '@aztec/foundation/retry' ;
99import { sleep } from '@aztec/foundation/sleep' ;
10- import { ScopedL2ToL1Message , computeEpochOutHash } from '@aztec/stdlib/messaging' ;
10+ import { ScopedL2ToL1Message , accumulateInboxRollingHash , computeEpochOutHash } from '@aztec/stdlib/messaging' ;
1111import { makeScopedL2ToL1Message } from '@aztec/stdlib/testing' ;
1212
1313import { TestContext , makeTestDeferredJobQueue } from '../mocks/test_context.js' ;
@@ -42,20 +42,14 @@ describe('prover/orchestrator/top-tree', () => {
4242 await context . cleanup ( ) ;
4343 } ) ;
4444
45+ /** The checkpoint fixture shape shared by `makeCheckpoint` and `makeCheckpointWithMessagesPerBlock`. */
46+ type CheckpointFixture = Awaited < ReturnType < TestContext [ 'makeCheckpoint' ] > > ;
47+
4548 /**
46- * Drives a single checkpoint through `CheckpointSubTreeOrchestrator` and returns
47- * the assembled `CheckpointTopTreeData` plus the originating checkpoint metadata .
49+ * Drives a checkpoint fixture through `CheckpointSubTreeOrchestrator`, feeding block `i` the message slice
50+ * `messagesPerBlock[i]`, and returns the assembled `CheckpointTopTreeData` plus the originating fixture .
4851 */
49- async function driveSubTree ( numBlocks : number , numTxsPerBlock : number , numL1ToL2Messages = 0 , numL2ToL1Messages = 0 ) {
50- const fixture = await context . makeCheckpoint ( numBlocks , {
51- numTxsPerBlock,
52- numL1ToL2Messages,
53- makeProcessedTxOpts :
54- numL2ToL1Messages > 0
55- ? ( ) => ( { privateOnly : false , avmAccumulatedData : { l2ToL1Msgs : makeL2ToL1Messages ( numL2ToL1Messages ) } } )
56- : undefined ,
57- } ) ;
58-
52+ async function driveFixture ( fixture : CheckpointFixture , messagesPerBlock : Fr [ ] [ ] ) {
5953 const subTree = await CheckpointSubTreeOrchestrator . start (
6054 context . worldState ,
6155 context . prover ,
@@ -66,20 +60,15 @@ describe('prover/orchestrator/top-tree', () => {
6660 makeTestDeferredJobQueue ( ) ,
6761 fixture . constants ,
6862 fixture . l1ToL2Messages ,
69- Fr . ZERO ,
70- numBlocks ,
63+ fixture . startInboxRollingHash ,
64+ fixture . blocks . length ,
7165 fixture . previousBlockHeader ,
7266 ) ;
7367 const resultPromise = subTree . getSubTreeResult ( ) ;
7468
7569 for ( const [ blockIndex , block ] of fixture . blocks . entries ( ) ) {
7670 const { blockNumber, timestamp } = block . header . globalVariables ;
77- await subTree . startNewBlock (
78- blockNumber ,
79- timestamp ,
80- block . txs . length ,
81- blockIndex === 0 ? fixture . l1ToL2Messages : [ ] ,
82- ) ;
71+ await subTree . startNewBlock ( blockNumber , timestamp , block . txs . length , messagesPerBlock [ blockIndex ] ) ;
8372 if ( block . txs . length > 0 ) {
8473 await subTree . addTxs ( block . txs ) ;
8574 }
@@ -103,6 +92,33 @@ describe('prover/orchestrator/top-tree', () => {
10392 return { fixture, topTreeData } ;
10493 }
10594
95+ /**
96+ * Builds a checkpoint via `makeCheckpoint` (every message in the first block) and drives it through
97+ * `CheckpointSubTreeOrchestrator`, returning the assembled `CheckpointTopTreeData` plus the fixture.
98+ */
99+ async function driveSubTree ( numBlocks : number , numTxsPerBlock : number , numL1ToL2Messages = 0 , numL2ToL1Messages = 0 ) {
100+ const fixture = await context . makeCheckpoint ( numBlocks , {
101+ numTxsPerBlock,
102+ numL1ToL2Messages,
103+ makeProcessedTxOpts :
104+ numL2ToL1Messages > 0
105+ ? ( ) => ( { privateOnly : false , avmAccumulatedData : { l2ToL1Msgs : makeL2ToL1Messages ( numL2ToL1Messages ) } } )
106+ : undefined ,
107+ } ) ;
108+ const messagesPerBlock = fixture . blocks . map ( ( _ , i ) => ( i === 0 ? fixture . l1ToL2Messages : [ ] ) ) ;
109+ return await driveFixture ( fixture , messagesPerBlock ) ;
110+ }
111+
112+ /**
113+ * Like {@link driveSubTree} but distributes the checkpoint's messages across its blocks (streaming Inbox,
114+ * AZIP-22): block `i` carries `l1ToL2MessagesPerBlock[i]` as its own slice. A zero-tx entry in `numTxsPerBlock`
115+ * whose slice is non-empty produces a message-only block, proven by the msgs-only block root.
116+ */
117+ async function driveSubTreeWithMessageSlices ( l1ToL2MessagesPerBlock : Fr [ ] [ ] , numTxsPerBlock : number [ ] ) {
118+ const fixture = await context . makeCheckpointWithMessagesPerBlock ( l1ToL2MessagesPerBlock , { numTxsPerBlock } ) ;
119+ return await driveFixture ( fixture , l1ToL2MessagesPerBlock ) ;
120+ }
121+
106122 it ( 'produces an epoch proof for a single-checkpoint, single-block, single-tx epoch' , async ( ) => {
107123 const { topTreeData } = await driveSubTree ( 1 , 1 ) ;
108124 const challenges = await context . getFinalBlobChallenges ( ) ;
@@ -169,6 +185,39 @@ describe('prover/orchestrator/top-tree', () => {
169185 }
170186 } ) ;
171187
188+ it ( 'produces an epoch proof when messages span blocks, including a message-only block' , async ( ) => {
189+ // The streaming Inbox (AZIP-22 Fast Inbox) shapes, driven through the entire proving DAG at simulated-circuit
190+ // fidelity: a checkpoint whose messages land in a non-first block, a zero-tx message-only block (proven by the
191+ // msgs-only block root), a block merge above the three block roots, the two-input checkpoint root over per-block
192+ // bundles, the single-block checkpoint root for the follow-on checkpoints, the checkpoint merge asserting inbox
193+ // rolling-hash continuity across a message-carrying boundary, and the root rollup exposing the epoch's
194+ // rolling-hash range.
195+ const ckpt1Slices = [ [ new Fr ( 0x100 ) , new Fr ( 0x101 ) ] , [ ] , [ new Fr ( 0x102 ) , new Fr ( 0x103 ) , new Fr ( 0x104 ) ] ] ;
196+ const a = await driveSubTreeWithMessageSlices ( ckpt1Slices , [ 1 , 1 , 0 ] ) ;
197+ // A single-block checkpoint with messages after a message-carrying checkpoint: its parity chain starts from
198+ // checkpoint 0's (non-zero) end rolling hash.
199+ const b = await driveSubTree ( 1 , 1 , 2 ) ;
200+ // A message-less checkpoint after message-carrying ones: the rolling hash must pass through unchanged.
201+ const c = await driveSubTree ( 1 , 1 ) ;
202+ const challenges = await context . getFinalBlobChallenges ( ) ;
203+
204+ const topTree = new TopTreeOrchestrator ( context . prover , EthAddress . ZERO , makeTestDeferredJobQueue ( ) ) ;
205+ try {
206+ const result = await topTree . prove ( EpochNumber ( 1 ) , 3 , challenges , [ a . topTreeData , b . topTreeData , c . topTreeData ] ) ;
207+ expect ( result . proof ) . toBeDefined ( ) ;
208+ expect ( result . publicInputs ) . toBeDefined ( ) ;
209+
210+ // The epoch's rolling-hash range binds the exact message sequence consumed, in block order, across all three
211+ // checkpoints; L1 validates this range against the Inbox when the proof lands.
212+ const epochMessages = [ ...a . fixture . l1ToL2Messages , ...b . fixture . l1ToL2Messages ] ;
213+ expect ( epochMessages . length ) . toBe ( 7 ) ; // sanity: the fixtures really did carry messages
214+ expect ( result . publicInputs . previousInboxRollingHash ) . toEqual ( Fr . ZERO ) ;
215+ expect ( result . publicInputs . endInboxRollingHash ) . toEqual ( accumulateInboxRollingHash ( Fr . ZERO , epochMessages ) ) ;
216+ } finally {
217+ await topTree . stop ( ) ;
218+ }
219+ } , 300_000 ) ;
220+
172221 it ( 'pipelines: starts ckpt0 root rollup before ckpt1 sub-tree resolves' , async ( ) => {
173222 // Drive both sub-trees synchronously (still no top tree running).
174223 const a = await driveSubTree ( 1 , 1 ) ;
0 commit comments