Skip to content

Commit f2322aa

Browse files
spalladinoclaude
andcommitted
test(fast-inbox): type the prover's slicing mocks and cover a retried block attempt
Replaces the `as any` stubs and the private `runPromise` reach in the checkpoint prover's message-slicing tests with typed `mock<T>()` doubles and a completion signal off the sub-tree, and asserts the bundles each block received rather than raw call counts. Adds a sequencer regression for the explicit consumption state: a first block attempt that fails on valid txs must leave the cursor untouched, so its retry re-derives the same range and the checkpoint advances the cursor exactly once. Updates the handoff-join expectation to the propagated processing error, and drops the mock builder's fallback for the now-required `l1ToL2Messages`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a314e63 commit f2322aa

3 files changed

Lines changed: 74 additions & 36 deletions

File tree

yarn-project/prover-node/src/job/checkpoint-prover.test.ts

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { promiseWithResolvers } from '@aztec/foundation/promise';
88
import { sleep } from '@aztec/foundation/sleep';
99
import { DateProvider } from '@aztec/foundation/timer';
1010
import type { EpochProverFactory } from '@aztec/prover-client';
11-
import type { ChonkCache, SubTreeResult } from '@aztec/prover-client/orchestrator';
12-
import type { PublicProcessorFactory } from '@aztec/simulator/server';
11+
import type { CheckpointSubTreeOrchestrator, ChonkCache, SubTreeResult } from '@aztec/prover-client/orchestrator';
12+
import type { PublicProcessor, PublicProcessorFactory } from '@aztec/simulator/server';
1313
import { Checkpoint } from '@aztec/stdlib/checkpoint';
14-
import type { ForkMerkleTreeOperations, ITxProvider } from '@aztec/stdlib/interfaces/server';
14+
import type { ForkMerkleTreeOperations, ITxProvider, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
1515
import { BlockHeader, type Tx } from '@aztec/stdlib/tx';
1616

1717
import { jest } from '@jest/globals';
@@ -504,7 +504,8 @@ describe('CheckpointProver', () => {
504504

505505
const prover = makeProver();
506506

507-
await expect(prover.whenSubTreeProofsReady()).rejects.toThrow(/did not complete block processing/);
507+
// The failure the loop hit wins over the early proofs, and is what the promise rejects with.
508+
await expect(prover.whenSubTreeProofsReady()).rejects.toThrow(/Unable to get meta data for block 0/);
508509
await prover.whenDone();
509510
expect(stop).toHaveBeenCalledTimes(1);
510511
expect(prover.isFailed()).toBe(true);
@@ -515,39 +516,52 @@ describe('CheckpointProver', () => {
515516
// ---------------- data-plane reorg fork fault ----------------
516517

517518
describe('streaming message slicing', () => {
518-
/** Stubs the sub-tree and forks so the execute loop runs with empty-tx blocks, recording per-block messages. */
519-
function stubExecution() {
519+
/**
520+
* Stubs the sub-tree, the forks and the public processor so the execute loop runs over empty-tx blocks, and
521+
* exposes the per-block message bundles it hands each of them. `blocksCompleted` resolves once the loop has run
522+
* `expectedBlocks` blocks to completion, which is the point at which every bundle has been handed over.
523+
*/
524+
function stubExecution(expectedBlocks: number) {
520525
txProvider.getTxsForBlock.mockReset();
521526
txProvider.getTxsForBlock.mockResolvedValue({ txs: [], missingTxs: [] });
522-
const startNewBlock = jest.fn((..._args: unknown[]) => Promise.resolve());
523-
const appendLeaves = jest.fn((..._args: unknown[]) => Promise.resolve());
524-
const subTree = {
525-
getSubTreeResult: () => new Promise<never>(() => {}),
526-
startNewBlock,
527-
startChonkVerifierCircuits: () => Promise.resolve(),
528-
addTxs: () => Promise.resolve(),
529-
setBlockCompleted: () => Promise.resolve(),
530-
cancel: () => {},
531-
stop: () => Promise.resolve(),
532-
};
533-
proverFactory.createCheckpointSubTreeOrchestrator.mockResolvedValue(subTree as any);
534-
dbProvider.fork.mockResolvedValue({ appendLeaves, close: () => Promise.resolve() } as any);
535-
publicProcessorFactory.create.mockReturnValue({ process: () => Promise.resolve([[], []]) } as any);
536-
return { startNewBlock, appendLeaves };
527+
528+
const blocksCompleted = promiseWithResolvers<void>();
529+
const subTree = mock<CheckpointSubTreeOrchestrator>();
530+
// The sub-tree's proofs never land: these tests only exercise the block loop that feeds it.
531+
subTree.getSubTreeResult.mockReturnValue(new Promise<SubTreeResult>(() => {}));
532+
subTree.setBlockCompleted.mockImplementation((_blockNumber, expectedHeader) => {
533+
if (subTree.setBlockCompleted.mock.calls.length >= expectedBlocks) {
534+
blocksCompleted.resolve();
535+
}
536+
return Promise.resolve(expectedHeader ?? BlockHeader.empty());
537+
});
538+
proverFactory.createCheckpointSubTreeOrchestrator.mockResolvedValue(subTree);
539+
540+
const fork = mock<MerkleTreeWriteOperations>();
541+
dbProvider.fork.mockResolvedValue(fork);
542+
543+
const publicProcessor = mock<PublicProcessor>();
544+
publicProcessor.process.mockResolvedValue([[], [], [], [], []]);
545+
publicProcessorFactory.create.mockReturnValue(publicProcessor);
546+
547+
const bundlesPassedToSubTree = () => subTree.startNewBlock.mock.calls.map(([, , , messages]) => messages);
548+
const bundlesAppendedToFork = () => fork.appendLeaves.mock.calls.map(([, leaves]) => leaves);
549+
return { blocksCompleted: blocksCompleted.promise, bundlesPassedToSubTree, bundlesAppendedToFork };
537550
}
538551

539552
it('slices the checkpoint messages per block by the headers leaf counts', async () => {
540553
checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, txsPerBlock: 0 });
541554
// The parent consumed 10 messages; the blocks consume 2, 0 and 1 more.
542555
pinConsumedMessageCounts(checkpoint, [12, 12, 13]);
543556
const messages = [Fr.random(), Fr.random(), Fr.random()];
544-
const { startNewBlock, appendLeaves } = stubExecution();
557+
const { blocksCompleted, bundlesPassedToSubTree, bundlesAppendedToFork } = stubExecution(3);
545558

546559
const prover = makeProver({ previousBlockHeader: makePreviousBlockHeader(10), l1ToL2Messages: messages });
547-
await (prover as any).runPromise;
560+
await blocksCompleted;
548561

549-
expect(startNewBlock.mock.calls.map(call => call[3])).toEqual([messages.slice(0, 2), [], messages.slice(2)]);
550-
expect(appendLeaves.mock.calls.map(call => call[1])).toEqual([messages.slice(0, 2), [], messages.slice(2)]);
562+
const expectedBundles = [messages.slice(0, 2), [], messages.slice(2)];
563+
expect(bundlesPassedToSubTree()).toEqual(expectedBundles);
564+
expect(bundlesAppendedToFork()).toEqual(expectedBundles);
551565
expect(prover.isFailed()).toBe(false);
552566
prover.cancel();
553567
await prover.whenDone();
@@ -556,7 +570,7 @@ describe('CheckpointProver', () => {
556570
it('fails the prover when the message list does not cover the blocks leaf count range', async () => {
557571
checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, txsPerBlock: 0 });
558572
pinConsumedMessageCounts(checkpoint, [12, 13]);
559-
const { startNewBlock } = stubExecution();
573+
const { bundlesPassedToSubTree } = stubExecution(2);
560574

561575
// The parent consumed 10, the blocks reach 13, but only two messages are supplied.
562576
const prover = makeProver({
@@ -567,7 +581,8 @@ describe('CheckpointProver', () => {
567581
await expect(prover.whenSubTreeProofsReady()).rejects.toThrow(
568582
/consumed 3 L1 to L2 messages .* but 2 were supplied/,
569583
);
570-
expect(startNewBlock).not.toHaveBeenCalled();
584+
// The mismatch is caught before any block is handed to the sub-tree, so nothing was proven on a bad slice.
585+
expect(bundlesPassedToSubTree()).toEqual([]);
571586
expect(prover.isFailed()).toBe(true);
572587
expect(onFailed).toHaveBeenCalledWith(prover);
573588
await prover.whenDone();
@@ -576,16 +591,15 @@ describe('CheckpointProver', () => {
576591
it('fails the prover when a block leaf count falls below its parent', async () => {
577592
checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, txsPerBlock: 0 });
578593
pinConsumedMessageCounts(checkpoint, [13, 12]);
579-
const { startNewBlock } = stubExecution();
594+
const messages = [Fr.random(), Fr.random()];
595+
const { bundlesPassedToSubTree } = stubExecution(2);
580596

581-
const prover = makeProver({
582-
previousBlockHeader: makePreviousBlockHeader(10),
583-
l1ToL2Messages: [Fr.random(), Fr.random()],
584-
});
597+
const prover = makeProver({ previousBlockHeader: makePreviousBlockHeader(10), l1ToL2Messages: messages });
585598

586599
await expect(prover.whenSubTreeProofsReady()).rejects.toThrow(/leaf count 12 is below its parent's 13/);
587-
// The first block was started before the second's count was found to rewind.
588-
expect(startNewBlock).toHaveBeenCalledTimes(1);
600+
// The first block was started (claiming all three messages the header says it consumed, of which only two
601+
// exist) before the second block's count was found to rewind.
602+
expect(bundlesPassedToSubTree()).toEqual([messages]);
589603
expect(prover.isFailed()).toBe(true);
590604
await prover.whenDone();
591605
});

yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1540,7 +1540,7 @@ describe('CheckpointProposalJob', () => {
15401540
return Promise.resolve();
15411541
});
15421542
};
1543-
const bundleLengths = () => checkpointBuilder.buildBlockCalls.map(call => call.opts.l1ToL2Messages?.length);
1543+
const bundleLengths = () => checkpointBuilder.buildBlockCalls.map(call => call.opts.l1ToL2Messages.length);
15441544
const signedPrefixes = () =>
15451545
validatorClient.createBlockProposal.mock.calls.map(call => call[6].inboxRollingHash.toString());
15461546
const prefixAt = (count: number) => streamingInbox.positionAt(BigInt(count)).rollingHash.toString();
@@ -1575,6 +1575,30 @@ describe('CheckpointProposalJob', () => {
15751575
expect(publisher.enqueueProposeCheckpoint.mock.calls[0][3]).toBe(7n);
15761576
});
15771577

1578+
it('re-derives the same range after a failed attempt and advances the cursor once on the retry', async () => {
1579+
// Three sub-slots. The first attempt at the checkpoint's first block fails on valid txs, so it signs nothing
1580+
// and must leave the cursor where it was: the retry in the next sub-slot has to select the same five
1581+
// messages again, and the checkpoint must end at 5 rather than at a cursor advanced twice.
1582+
mockSubslots(3);
1583+
streamingInbox.set(leaves(5));
1584+
publisher.validateCheckpointHeaderAndInbox.mockResolvedValue(7n);
1585+
checkpointBuilder.errorOnBuild = new InsufficientValidTxsError(0, 1, []);
1586+
betweenBlocks(1, () => (checkpointBuilder.errorOnBuild = undefined));
1587+
1588+
const { lastBlock } = await setupMultipleBlocks(2, [2, 1]);
1589+
validatorClient.collectAttestations.mockResolvedValue(getAttestations(lastBlock));
1590+
1591+
const checkpoint = await job.executeAndAwait();
1592+
1593+
expect(checkpoint).toBeDefined();
1594+
expect(checkpoint!.blocks).toHaveLength(2);
1595+
// Three attempts: the failed one, its retry over the same range, and the final block with nothing left.
1596+
expect(bundleLengths()).toEqual([5, 5, 0]);
1597+
// Only the two blocks that built signed a prefix, both at 5: the cursor advanced exactly once.
1598+
expect(signedPrefixes()).toEqual([prefixAt(5), prefixAt(5)]);
1599+
expect(preflightTotals()).toEqual([5n, 5n]);
1600+
});
1601+
15781602
it('produces a message-only block when messages are observed and no txs are pending', async () => {
15791603
mockSubslots(1);
15801604
streamingInbox.set(leaves(5));

yarn-project/sequencer-client/src/test/mock_checkpoint_builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
100100
throw this.errorOnBuild;
101101
}
102102

103-
this.inboxRollingHash = accumulateInboxRollingHash(this.inboxRollingHash, opts.l1ToL2Messages ?? []);
103+
this.inboxRollingHash = accumulateInboxRollingHash(this.inboxRollingHash, opts.l1ToL2Messages);
104104

105105
let block: L2Block;
106106
let usedTxs: Tx[];

0 commit comments

Comments
 (0)