Skip to content

Commit dfef856

Browse files
committed
test(fast-inbox): epoch proof over per-block message slices and a msgs-only block (A-1385)
Drive the streaming Inbox shapes through the entire proving DAG (block roots incl. msgs-only, block merge, checkpoint roots, checkpoint merge, root rollup) at simulated-circuit fidelity in the top-tree orchestrator suite: a checkpoint whose messages land in a non-first block plus a zero-tx message-only block, followed by message-carrying and message-less checkpoints. Asserts the root rollup's inbox rolling-hash range matches the accumulation over the epoch's messages in block order. TestContext now threads the inbox rolling hash across the checkpoints it builds (previously hardcoded to zero), so a checkpoint built after a message-carrying one commits the correct continuation in its header and the checkpoint merge continuity assert can be exercised.
1 parent cb09f51 commit dfef856

2 files changed

Lines changed: 85 additions & 25 deletions

File tree

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export class TestContext {
5959
private nextBlockNumber = 1;
6060
private epochNumber = 1;
6161
private feePayerBalance: Fr;
62+
// Running inbox rolling hash across the checkpoints built by this context (never resets: the protocol threads it
63+
// from genesis). Each checkpoint's builder starts from it, and it advances by the checkpoint's messages, so a
64+
// checkpoint built after a message-carrying one commits the correct continuation in its header.
65+
private currentInboxRollingHash = Fr.ZERO;
6266

6367
constructor(
6468
public worldState: MerkleTreeAdminDatabase,
@@ -241,12 +245,13 @@ export class TestContext {
241245

242246
const cleanFork = await this.worldState.fork();
243247
const previousCheckpointOutHashes = this.checkpointOutHashes;
248+
const startInboxRollingHash = this.currentInboxRollingHash;
244249
const builder = await LightweightCheckpointBuilder.startNewCheckpoint(
245250
checkpointNumber,
246251
{ ...constants, timestamp },
247252
l1ToL2Messages,
248253
previousCheckpointOutHashes,
249-
Fr.ZERO,
254+
startInboxRollingHash,
250255
cleanFork,
251256
);
252257

@@ -273,6 +278,7 @@ export class TestContext {
273278
const checkpoint = await builder.completeCheckpoint();
274279
this.checkpoints.push(checkpoint);
275280
this.checkpointOutHashes.push(checkpoint.getCheckpointOutHash());
281+
this.currentInboxRollingHash = checkpoint.header.inboxRollingHash;
276282

277283
return {
278284
constants,
@@ -281,6 +287,7 @@ export class TestContext {
281287
blocks,
282288
l1ToL2Messages,
283289
previousBlockHeader,
290+
startInboxRollingHash,
284291
};
285292
}
286293

@@ -289,8 +296,9 @@ export class TestContext {
289296
* (streaming Inbox / AZIP-22 Fast Inbox): `l1ToL2MessagesPerBlock[i]` is block `i`'s own message slice,
290297
* appended at compact indices in insertion order. Lets tests exercise checkpoints whose messages span more
291298
* 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.
299+
* `makeCheckpoint` puts every message in the first block. `numTxsPerBlock` defaults to 1 because the builder
300+
* rejects a non-first block with neither txs nor messages; a zero-tx entry whose slice is non-empty is valid
301+
* and produces a message-only block.
294302
*/
295303
public async makeCheckpointWithMessagesPerBlock(
296304
l1ToL2MessagesPerBlock: Fr[][],
@@ -365,14 +373,15 @@ export class TestContext {
365373

366374
const cleanFork = await this.worldState.fork();
367375
const previousCheckpointOutHashes = this.checkpointOutHashes;
376+
const startInboxRollingHash = this.currentInboxRollingHash;
368377
// Empty checkpoint-level list + `insertMessagesPerBlock` so the builder inserts each block's slice via
369378
// `addBlock` (and accumulates them into the checkpoint's rolling hash) rather than up front.
370379
const builder = await LightweightCheckpointBuilder.startNewCheckpoint(
371380
checkpointNumber,
372381
{ ...constants, timestamp },
373382
[],
374383
previousCheckpointOutHashes,
375-
Fr.ZERO,
384+
startInboxRollingHash,
376385
cleanFork,
377386
undefined,
378387
0n,
@@ -401,6 +410,7 @@ export class TestContext {
401410
const checkpoint = await builder.completeCheckpoint();
402411
this.checkpoints.push(checkpoint);
403412
this.checkpointOutHashes.push(checkpoint.getCheckpointOutHash());
413+
this.currentInboxRollingHash = checkpoint.header.inboxRollingHash;
404414

405415
return {
406416
constants,
@@ -410,6 +420,7 @@ export class TestContext {
410420
l1ToL2Messages,
411421
l1ToL2MessagesPerBlock,
412422
previousBlockHeader,
423+
startInboxRollingHash,
413424
};
414425
}
415426

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

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createLogger } from '@aztec/foundation/log';
77
import { promiseWithResolvers } from '@aztec/foundation/promise';
88
import { retryUntil } from '@aztec/foundation/retry';
99
import { sleep } from '@aztec/foundation/sleep';
10-
import { ScopedL2ToL1Message, computeEpochOutHash } from '@aztec/stdlib/messaging';
10+
import { ScopedL2ToL1Message, accumulateInboxRollingHash, computeEpochOutHash } from '@aztec/stdlib/messaging';
1111
import { makeScopedL2ToL1Message } from '@aztec/stdlib/testing';
1212

1313
import { 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

Comments
 (0)