Skip to content

Commit 3f459a5

Browse files
committed
refactor(prover-client): split orchestrator into sub-tree + top-tree pair
Introduces a sub-tree + top-tree orchestrator pair that decomposes the existing single-class proving orchestrator along the natural state-coupling boundary — per-checkpoint block-level work vs. epoch-level top-tree work — while leaving every existing API on the legacy `EpochProver` / `ProvingOrchestrator` / `EpochProvingState` path untouched. The prover-node and e2e tests build unchanged; this PR is purely additive in surface area, with structural refactors on `ProvingOrchestrator` to share scheduling and top-tree drivers with the new `TopTreeOrchestrator`. Split out from #22990 so it can land independently. ## What's new - **`CheckpointSubTreeOrchestrator`** (`checkpoint-sub-tree-orchestrator.ts`): extends `ProvingOrchestrator`, single-checkpoint by construction. Drives chonk-verifier / base / merge / block-root / block-merge for one checkpoint and resolves a `SubTreeResult` instead of escalating to the checkpoint root — the parent's `checkAndEnqueueCheckpointRootRollup` is overridden to short-circuit. The constructor calls `super.startNewEpoch(epoch, 1, empty challenges)` to set up a single-checkpoint mini-epoch; the count and challenges are never read because the override prevents the parent's finalize / root path from running. - **`TopTreeOrchestrator`** + **`TopTreeProvingState`**: self-contained driver from checkpoint-root through epoch-root rollup. Takes per-checkpoint block-proof promises and pipelines its hint chain against them. Cancellation surfaces as `TopTreeCancelledError` so callers can distinguish reorg-driven cancel from a genuine proving failure. - **`EpochProvingContext`** (`epoch-proving-context.ts`): per-epoch shared cache for chonk-verifier proofs. Survives sub-tree cancellation so a tx that gets reorged out and re-appears in a replacement checkpoint reuses the cached proof. - **`ProvingScheduler`** (`proving-scheduler.ts`): abstract base owning the `SerialQueue` deferred-job lifecycle, the `pendingProvingJobs` controller list, and a unified `deferredProving<S, T>(state, request, callback, isCancelled?)` submit envelope. The minimal `ProvingStateLike` contract is just `verifyState()` + `reject(reason)`. - **`TopTreeProvingScheduler`** (`top-tree-proving-scheduler.ts`): extends `ProvingScheduler` and holds the checkpoint-merge, padding, and root-rollup drivers (plus tree-walking helpers) shared by both orchestrators. Wraps circuit calls via a `wrapCircuitCall` hook (orchestrator overrides for spans; top-tree leaves identity) and resolves via an `onRootRollupComplete` hook to bridge the two states' differing `resolve` signatures. The per-checkpoint root driver stays subclass-specific because input-building flows differ. - **`EpochProverFactory` interface on `ProverClient`**: new factory methods `createEpochProvingContext(epochNumber)`, `createCheckpointSubTreeOrchestrator(...)`, and `createTopTreeOrchestrator()`. A single shared `BrokerCircuitProverFacade` is owned by `ProverClient` and shared across every orchestrator. ## What changes in existing code - `ProvingOrchestrator` extends `TopTreeProvingScheduler`; the inline broker-job submit envelope, queue lifecycle, and the top-tree-section drivers are inherited. `cancel()` delegates the queue-recreate + abort-jobs logic to `resetSchedulerState(this.cancelJobsOnStop)`. Three internal methods (`getOrEnqueueChonkVerifier`, `checkAndEnqueueBaseRollup`, `checkAndEnqueueCheckpointRootRollup`) become `protected` so the sub-tree can override them; `provingState` and `provingPromise` likewise become `protected` so the sub-tree can hook the parent's failure stream onto `subTreeResult`. No public API change on `ProvingOrchestrator`. - `CheckpointProvingState`: gains two read-only accessors used by the sub-tree's checkpoint-root override — `getSubTreeOutputProofs()` and `getLastArchiveSiblingPath()`. No state changes. - `ProverClient` keeps `createEpochProver()` exactly as before (each call spawns its own `BrokerCircuitProverFacade`); the new factory methods share a `getFacade()` set up in `start()` and torn down in `stop()`. `EpochProver`, `EpochProverManager`, `ServerEpochProver`, `EpochProvingState`, the integration tests in `orchestrator_*.test.ts`, `bb_prover_full_rollup.test.ts`, and `stdlib/interfaces/*` are all unchanged from `merge-train/spartan` — the prover-node and e2e tests continue to build against the existing `EpochProver` API. Migrating the prover-node onto the new factories (and the deferred-finalize flow that goes with optimistic proving) is the follow-up PR. ## Test plan - [x] 261 prover-client tests pass (full `yarn workspace @aztec/prover-client test`). - [x] `yarn build` clean against current merge-train/spartan (modulo the pre-existing `@aztec/sqlite3mc-wasm` issue inherited from baseline).
1 parent 5f8441e commit 3f459a5

15 files changed

Lines changed: 1820 additions & 212 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ export class TestContext {
284284

285285
return {
286286
constants,
287+
checkpoint,
287288
header: checkpoint.header,
288289
blocks,
289290
l1ToL2Messages,

yarn-project/prover-client/src/orchestrator/checkpoint-proving-state.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,17 @@ export class CheckpointProvingState {
346346
? [this.blockProofs.getNode(rootLocation)?.provingOutput] // If there's only 1 block, its proof will be stored at the root.
347347
: this.blockProofs.getChildren(rootLocation).map(c => c?.provingOutput);
348348
}
349+
350+
/**
351+
* Returns the block-level proof outputs that feed into the checkpoint root rollup.
352+
* Used by `CheckpointSubTreeOrchestrator` to surface its sub-tree result.
353+
*/
354+
public getSubTreeOutputProofs() {
355+
return this.#getChildProofsForRoot();
356+
}
357+
358+
/** Sibling path of the archive tree captured before any block in this checkpoint landed. */
359+
public getLastArchiveSiblingPath() {
360+
return this.lastArchiveSiblingPath;
361+
}
349362
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { FinalBlobBatchingChallenges } from '@aztec/blob-lib';
2+
import { EpochNumber } from '@aztec/foundation/branded-types';
3+
import { EthAddress } from '@aztec/foundation/eth-address';
4+
import { createLogger } from '@aztec/foundation/log';
5+
6+
import { TestContext } from '../mocks/test_context.js';
7+
import { CheckpointSubTreeOrchestrator } from './checkpoint-sub-tree-orchestrator.js';
8+
import { EpochProvingContext } from './epoch-proving-context.js';
9+
10+
const logger = createLogger('prover-client:test:checkpoint-sub-tree-orchestrator');
11+
12+
describe('prover/orchestrator/checkpoint-sub-tree', () => {
13+
let context: TestContext;
14+
let epochContext: EpochProvingContext;
15+
16+
beforeEach(async () => {
17+
context = await TestContext.new(logger);
18+
epochContext = new EpochProvingContext(context.prover, EpochNumber(1));
19+
});
20+
21+
afterEach(async () => {
22+
epochContext.stop();
23+
await context.cleanup();
24+
});
25+
26+
it('resolves the sub-tree result with block-level proofs for a single-block checkpoint', async () => {
27+
const numBlocks = 1;
28+
const numTxsPerBlock = 1;
29+
const { constants, blocks, l1ToL2Messages, previousBlockHeader } = await context.makeCheckpoint(numBlocks, {
30+
numTxsPerBlock,
31+
});
32+
33+
const subTree = await CheckpointSubTreeOrchestrator.start(
34+
context.worldState,
35+
context.prover,
36+
EthAddress.ZERO,
37+
epochContext,
38+
false,
39+
10,
40+
constants,
41+
l1ToL2Messages,
42+
numBlocks,
43+
previousBlockHeader,
44+
);
45+
try {
46+
const resultPromise = subTree.getSubTreeResult();
47+
48+
for (const block of blocks) {
49+
const { blockNumber, timestamp } = block.header.globalVariables;
50+
await subTree.startNewBlock(blockNumber, timestamp, block.txs.length);
51+
if (block.txs.length > 0) {
52+
await subTree.addTxs(block.txs);
53+
}
54+
await subTree.setBlockCompleted(blockNumber, block.header);
55+
}
56+
57+
const result = await resultPromise;
58+
expect(result.blockProofOutputs).toHaveLength(1);
59+
expect(result.blockProofOutputs[0].proof).toBeDefined();
60+
expect(result.previousArchiveSiblingPath).toBeDefined();
61+
} finally {
62+
await subTree.stop();
63+
}
64+
});
65+
66+
it('resolves with two block proofs for a two-block checkpoint', async () => {
67+
const numBlocks = 2;
68+
const numTxsPerBlock = 1;
69+
const { constants, blocks, l1ToL2Messages, previousBlockHeader } = await context.makeCheckpoint(numBlocks, {
70+
numTxsPerBlock,
71+
});
72+
73+
const subTree = await CheckpointSubTreeOrchestrator.start(
74+
context.worldState,
75+
context.prover,
76+
EthAddress.ZERO,
77+
epochContext,
78+
false,
79+
10,
80+
constants,
81+
l1ToL2Messages,
82+
numBlocks,
83+
previousBlockHeader,
84+
);
85+
try {
86+
const resultPromise = subTree.getSubTreeResult();
87+
88+
for (const block of blocks) {
89+
const { blockNumber, timestamp } = block.header.globalVariables;
90+
await subTree.startNewBlock(blockNumber, timestamp, block.txs.length);
91+
if (block.txs.length > 0) {
92+
await subTree.addTxs(block.txs);
93+
}
94+
await subTree.setBlockCompleted(blockNumber, block.header);
95+
}
96+
97+
const result = await resultPromise;
98+
expect(result.blockProofOutputs).toHaveLength(2);
99+
} finally {
100+
await subTree.stop();
101+
}
102+
});
103+
104+
it('throws when startNewEpoch is called explicitly', async () => {
105+
const { constants, l1ToL2Messages, previousBlockHeader } = await context.makeCheckpoint(1, { numTxsPerBlock: 0 });
106+
const subTree = await CheckpointSubTreeOrchestrator.start(
107+
context.worldState,
108+
context.prover,
109+
EthAddress.ZERO,
110+
epochContext,
111+
false,
112+
10,
113+
constants,
114+
l1ToL2Messages,
115+
1,
116+
previousBlockHeader,
117+
);
118+
try {
119+
expect(() => subTree.startNewEpoch(EpochNumber(2), 1, FinalBlobBatchingChallenges.empty())).toThrow(
120+
/starts its epoch in the constructor/,
121+
);
122+
} finally {
123+
await subTree.stop();
124+
}
125+
});
126+
127+
it('throws when startNewCheckpoint is called explicitly', async () => {
128+
const { constants, l1ToL2Messages, previousBlockHeader } = await context.makeCheckpoint(1, { numTxsPerBlock: 0 });
129+
const subTree = await CheckpointSubTreeOrchestrator.start(
130+
context.worldState,
131+
context.prover,
132+
EthAddress.ZERO,
133+
epochContext,
134+
false,
135+
10,
136+
constants,
137+
l1ToL2Messages,
138+
1,
139+
previousBlockHeader,
140+
);
141+
try {
142+
await expect(subTree.startNewCheckpoint(0, constants, l1ToL2Messages, 1, previousBlockHeader)).rejects.toThrow(
143+
/drives its single checkpoint in `start`/,
144+
);
145+
} finally {
146+
await subTree.stop();
147+
}
148+
});
149+
});

0 commit comments

Comments
 (0)