Skip to content

Commit 3e82240

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 1c0267b commit 3e82240

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,11 +245,12 @@ export class TestContext {
241245

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

@@ -272,6 +277,7 @@ export class TestContext {
272277
const checkpoint = await builder.completeCheckpoint();
273278
this.checkpoints.push(checkpoint);
274279
this.checkpointOutHashes.push(checkpoint.getCheckpointOutHash());
280+
this.currentInboxRollingHash = checkpoint.header.inboxRollingHash;
275281

276282
return {
277283
constants,
@@ -280,6 +286,7 @@ export class TestContext {
280286
blocks,
281287
l1ToL2Messages,
282288
previousBlockHeader,
289+
startInboxRollingHash,
283290
};
284291
}
285292

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

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

404414
return {
405415
constants,
@@ -409,6 +419,7 @@ export class TestContext {
409419
l1ToL2Messages,
410420
l1ToL2MessagesPerBlock,
411421
previousBlockHeader,
422+
startInboxRollingHash,
412423
};
413424
}
414425

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)