Skip to content

Commit f7d2cb2

Browse files
committed
feat(prover-node): block-summary plumbing and dispatchOffloadedBlock helper
Wires the orchestrator-side and prover-node-side scaffolding for the EpochProvingJob cutover (without flipping the production call site yet — the legacy `publicProcessor.process` + `addTxs` path still runs by default because the existing job tests assert against it; flipping the switch is a follow-up that goes hand-in-hand with rewriting those tests). - `BlockExecutionResult` now carries everything the orchestrator needs to finish a block without touching `ProcessedTx`: `endSpongeBlob`, `endState`, `totalFees`, `totalManaUsed`, and per-tx `txEffects`. The agent populates these during execution and the prover node hands them to the orchestrator. - `BlockExecutionHandler` collects the per-tx data and the aggregates as it walks the block, then returns the full summary in `BlockExecutionResult`. - `BlockProvingState` gains a `setBlockSummary` setter and overrides `getTxEffects`, `getTotalFees`, `getTotalManaUsed`, and `isAcceptingTxs` to read from the summary when supplied. The legacy per-tx `TxProvingState` path is otherwise unchanged. - `ProvingOrchestrator` gains `applyBlockExecutionResult` (set summary + end state + end sponge with block-end blob fields absorbed) and `getBlockStartSpongeBlob` (so the caller can build `BlockExecutionInputs` for the next block). - `ProverClient`/`EpochProverFactory` exposes `getBrokerCircuitProverFacade()` so EpochProvingJob can dispatch `BLOCK_EXECUTION` and watch deterministic-ID per-tx jobs through the same facade the orchestrators use. - `EpochProvingJob.dispatchOffloadedBlock(...)` is the new code path: registers per-tx watchers (private base rollup + AVM with passenger), builds `BlockExecutionInputs`, awaits the agent, and applies the summary. Not yet invoked from the per-block loop — that flip will follow alongside test updates. Job test gains a stub for `getBrokerCircuitProverFacade()` so the new helper is callable from tests when needed.
1 parent c995f52 commit f7d2cb2

9 files changed

Lines changed: 286 additions & 21 deletions

File tree

yarn-project/prover-client/src/block_execution/block_execution_handler.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { ProofData, ProvingRequestType } from '@aztec/stdlib/proofs';
2222
import { PrivateBaseRollupHints, PrivateTxBaseRollupPrivateInputs, PublicBaseRollupHints } from '@aztec/stdlib/rollup';
2323
import { MerkleTreeId } from '@aztec/stdlib/trees';
24-
import type { ProcessedTx, Tx, TxHash } from '@aztec/stdlib/tx';
24+
import type { ProcessedTx, Tx, TxEffect, TxHash } from '@aztec/stdlib/tx';
2525

2626
import {
2727
getChonkProofFromTx,
@@ -131,6 +131,9 @@ export class BlockExecutionHandler implements ServerCircuitProver {
131131
const publicProcessor = this.publicProcessorFactory.create(fork, inputs.blockHeader.globalVariables, config);
132132

133133
const spongeBlobState = inputs.startSpongeBlob.clone();
134+
const txEffects: TxEffect[] = [];
135+
let totalFees = Fr.ZERO;
136+
let totalManaUsed = 0n;
134137
let enqueuedAvmJobs = 0;
135138
let enqueuedPrivateBaseJobs = 0;
136139

@@ -161,6 +164,10 @@ export class BlockExecutionHandler implements ServerCircuitProver {
161164
);
162165
await spongeBlobState.absorb(ptx.txEffect.toBlobFields());
163166

167+
txEffects.push(ptx.txEffect);
168+
totalFees = totalFees.add(ptx.txEffect.transactionFee);
169+
totalManaUsed += BigInt(ptx.gasUsed.billedGas.l2Gas);
170+
164171
if (ptx.avmProvingRequest) {
165172
await this.enqueuePublicVmJob(
166173
epochNumber,
@@ -184,6 +191,11 @@ export class BlockExecutionHandler implements ServerCircuitProver {
184191
}
185192
}
186193

194+
// The block-end blob fields are absorbed on the orchestrator side once it has
195+
// `endState` and `totalManaUsed`. This `endSpongeBlob` only reflects the
196+
// per-tx effects.
197+
const endState = await fork.getStateReference();
198+
187199
this.log.info(`Block ${blockNumber} execution complete`, {
188200
epochNumber,
189201
blockNumber,
@@ -193,7 +205,14 @@ export class BlockExecutionHandler implements ServerCircuitProver {
193205
enqueuedPrivateBaseJobs,
194206
});
195207

196-
return new BlockExecutionResult(blockNumber, spongeBlobState);
208+
return new BlockExecutionResult(
209+
blockNumber,
210+
spongeBlobState,
211+
endState,
212+
totalFees,
213+
new Fr(totalManaUsed),
214+
txEffects,
215+
);
197216
} finally {
198217
try {
199218
await fork.close();

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from '@aztec/stdlib/rollup';
2727
import type { CircuitName } from '@aztec/stdlib/stats';
2828
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
29-
import { BlockHeader, GlobalVariables, StateReference } from '@aztec/stdlib/tx';
29+
import { BlockHeader, GlobalVariables, StateReference, type TxEffect } from '@aztec/stdlib/tx';
3030
import type { UInt64 } from '@aztec/stdlib/types';
3131

3232
import { buildHeaderFromCircuitOutputs, toProofData } from './block-building-helpers.js';
@@ -61,6 +61,14 @@ export class BlockProvingState {
6161
private txs: TxProvingState[] = [];
6262
private isFirstBlock: boolean;
6363
private error: string | undefined;
64+
/**
65+
* Aggregate per-block data supplied by the execution agent (offloaded path) in
66+
* lieu of populating `txs` with full `TxProvingState` instances. When set, the
67+
* various block-aggregate getters read from this in preference to iterating
68+
* `txs`, and `isAcceptingTxs()` returns `false` (the agent has executed the
69+
* full block).
70+
*/
71+
private blockSummary: { txEffects: TxEffect[]; totalFees: Fr; totalManaUsed: bigint } | undefined;
6472

6573
constructor(
6674
public readonly index: number,
@@ -103,13 +111,31 @@ export class BlockProvingState {
103111
}
104112

105113
public isAcceptingTxs() {
114+
if (this.blockSummary) {
115+
return false;
116+
}
106117
return this.txs.length < this.totalNumTxs;
107118
}
108119

109120
public getProcessedTxs() {
110121
return this.txs.map(t => t.processedTx);
111122
}
112123

124+
/**
125+
* Records the agent-computed aggregate state for an offloaded execution. After
126+
* this call the block is considered "fully populated" — `isAcceptingTxs` returns
127+
* false — and header/blob construction reads from the summary rather than from
128+
* a per-tx `TxProvingState[]`.
129+
*/
130+
public setBlockSummary(summary: { txEffects: TxEffect[]; totalFees: Fr; totalManaUsed: bigint }) {
131+
if (summary.txEffects.length !== this.totalNumTxs) {
132+
throw new Error(
133+
`Block summary tx count (${summary.txEffects.length}) does not match expected (${this.totalNumTxs}) for block ${this.blockNumber}`,
134+
);
135+
}
136+
this.blockSummary = summary;
137+
}
138+
113139
public tryStartProvingBase(txIndex: number) {
114140
if (this.baseOrMergeProofs.getLeaf(txIndex)?.isProving) {
115141
return false;
@@ -300,6 +326,9 @@ export class BlockProvingState {
300326
}
301327

302328
public getTxEffects() {
329+
if (this.blockSummary) {
330+
return this.blockSummary.txEffects;
331+
}
303332
return this.txs.map(t => t.processedTx.txEffect);
304333
}
305334

@@ -473,10 +502,16 @@ export class BlockProvingState {
473502
}
474503

475504
#getTotalFees() {
505+
if (this.blockSummary) {
506+
return this.blockSummary.totalFees;
507+
}
476508
return this.txs.reduce((acc, tx) => acc.add(tx.processedTx.txEffect.transactionFee), Fr.ZERO);
477509
}
478510

479511
#getTotalManaUsed() {
512+
if (this.blockSummary) {
513+
return this.blockSummary.totalManaUsed;
514+
}
480515
return this.txs.reduce((acc, tx) => acc + BigInt(tx.processedTx.gasUsed.billedGas.l2Gas), 0n);
481516
}
482517
}

yarn-project/prover-client/src/orchestrator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { ProvingOrchestrator } from './orchestrator.js';
22
export { CheckpointSubTreeOrchestrator, type SubTreeResult } from './checkpoint-sub-tree-orchestrator.js';
33
export { EpochProvingContext, type ChonkVerifierProofResult } from './epoch-proving-context.js';
4+
export type { BlockExecutionWatchers } from './orchestrator.js';
45
export {
56
TopTreeOrchestrator,
67
TopTreeCancelledError,

yarn-project/prover-client/src/orchestrator/orchestrator.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { pushTestData } from '@aztec/foundation/testing';
1616
import { elapsed } from '@aztec/foundation/timer';
1717
import type { TreeNodeLocation } from '@aztec/foundation/trees';
1818
import { EthAddress } from '@aztec/stdlib/block';
19-
import { AvmProvingInputs, type AvmProvingResult } from '@aztec/stdlib/block_execution';
19+
import { AvmProvingInputs, type AvmProvingResult, type BlockExecutionResult } from '@aztec/stdlib/block_execution';
2020
import type {
2121
ForkMerkleTreeOperations,
2222
MerkleTreeWriteOperations,
@@ -375,6 +375,57 @@ export class ProvingOrchestrator extends TopTreeProvingScheduler {
375375
return Promise.resolve();
376376
}
377377

378+
/**
379+
* Returns the start-of-block sponge-blob accumulator for the given block. For the
380+
* first block of a checkpoint this is the empty sponge; for subsequent blocks it
381+
* is the previous block's end sponge (set via `applyBlockExecutionResult`).
382+
*/
383+
public getBlockStartSpongeBlob(blockNumber: BlockNumber): SpongeBlob {
384+
if (!this.provingState) {
385+
throw new Error('Empty epoch proving state. Call startNewEpoch first.');
386+
}
387+
const provingState = this.provingState.getBlockProvingStateByBlockNumber(blockNumber);
388+
if (!provingState) {
389+
throw new Error(`Proving state for block ${blockNumber} not found.`);
390+
}
391+
return provingState.getStartSpongeBlob();
392+
}
393+
394+
/**
395+
* Applies the per-block summary returned by the execution agent to the orchestrator's
396+
* `BlockProvingState`. Sets the end state, end-sponge-blob (after absorbing block-end
397+
* blob fields, mirroring the legacy `addTxs` flow), and the per-block aggregate
398+
* (total fees, total mana used, per-tx effects). Must be called once per block in the
399+
* offloaded path before `setBlockCompleted`.
400+
*/
401+
public async applyBlockExecutionResult(blockNumber: BlockNumber, result: BlockExecutionResult): Promise<void> {
402+
if (!this.provingState) {
403+
throw new Error('Empty epoch proving state. Call startNewEpoch before applying execution results.');
404+
}
405+
const provingState = this.provingState.getBlockProvingStateByBlockNumber(blockNumber);
406+
if (!provingState) {
407+
throw new Error(`Proving state for block ${blockNumber} not found.`);
408+
}
409+
410+
provingState.setBlockSummary({
411+
txEffects: result.txEffects,
412+
totalFees: result.totalFees,
413+
totalManaUsed: result.totalManaUsed.toBigInt(),
414+
});
415+
provingState.setEndState(result.endState);
416+
417+
// Absorb the block-end blob fields the legacy addTxs flow would have absorbed,
418+
// then commit the resulting end-sponge-blob.
419+
const endSpongeBlob = result.endSpongeBlob.clone();
420+
const blockEndBlobFields = provingState.getBlockEndBlobFields();
421+
await endSpongeBlob.absorb(blockEndBlobFields);
422+
provingState.setEndSpongeBlob(endSpongeBlob);
423+
424+
// Try to accumulate the out hashes and blobs as far as we can.
425+
await this.provingState.accumulateCheckpointOutHashes();
426+
await this.provingState.setBlobAccumulators();
427+
}
428+
378429
/**
379430
* Shared implementation of {@link addTxs} and {@link addBlockForExecution}. Walks
380431
* each tx in order, prepares base-rollup inputs (mutating the orchestrator's fork),

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ export interface EpochProverFactory {
6565
headerOfLastBlockInPreviousCheckpoint: BlockHeader,
6666
): Promise<CheckpointSubTreeOrchestrator>;
6767
createTopTreeOrchestrator(): TopTreeOrchestrator;
68+
/**
69+
* Returns the broker facade. Used by `EpochProvingJob` to dispatch
70+
* `BLOCK_EXECUTION` jobs and watch deterministic-ID per-tx jobs through the
71+
* same facade the orchestrators use.
72+
*/
73+
getBrokerCircuitProverFacade(): BrokerCircuitProverFacade;
6874
}
6975

7076
/** Manages proving of epochs by orchestrating the proving of individual blocks relying on a pool of prover agents. */
@@ -115,6 +121,15 @@ export class ProverClient implements EpochProverManager, EpochProverFactory {
115121
return this.facade;
116122
}
117123

124+
/**
125+
* Returns the single shared broker facade. Public so the prover node can dispatch
126+
* BLOCK_EXECUTION and watch deterministic-ID per-tx jobs through the same facade
127+
* that the orchestrators use.
128+
*/
129+
public getBrokerCircuitProverFacade(): BrokerCircuitProverFacade {
130+
return this.getFacade();
131+
}
132+
118133
public createEpochProvingContext(epochNumber: EpochNumber): EpochProvingContext {
119134
return new EpochProvingContext(this.getFacade(), epochNumber, this.log.getBindings());
120135
}

yarn-project/prover-node/src/job/epoch-proving-job.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { BatchedBlob } from '@aztec/blob-lib/types';
1+
import { BatchedBlob, SpongeBlob } from '@aztec/blob-lib/types';
22
import { ARCHIVE_HEIGHT } from '@aztec/constants';
33
import { makeTuple } from '@aztec/foundation/array';
4-
import { CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
55
import { times, timesParallel } from '@aztec/foundation/collection';
66
import { Fr } from '@aztec/foundation/curves/bn254';
77
import { EthAddress } from '@aztec/foundation/eth-address';
@@ -18,15 +18,17 @@ import {
1818
} from '@aztec/prover-client/orchestrator';
1919
import type { PublicProcessor, PublicProcessorFactory } from '@aztec/simulator/server';
2020
import { CommitteeAttestation } from '@aztec/stdlib/block';
21+
import { BlockExecutionResult } from '@aztec/stdlib/block_execution';
2122
import { Checkpoint } from '@aztec/stdlib/checkpoint';
2223
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
2324
import { Proof } from '@aztec/stdlib/proofs';
2425
import { RootRollupPublicInputs } from '@aztec/stdlib/rollup';
2526
import { MerkleTreeId } from '@aztec/stdlib/trees';
2627
import type { ProcessedTx, Tx } from '@aztec/stdlib/tx';
27-
import { BlockHeader } from '@aztec/stdlib/tx';
28+
import { BlockHeader, StateReference } from '@aztec/stdlib/tx';
2829
import { getTelemetryClient } from '@aztec/telemetry-client';
2930

31+
import { jest } from '@jest/globals';
3032
import { type MockProxy, mock } from 'jest-mock-extended';
3133

3234
import { ProverNodeJobMetrics } from '../metrics.js';
@@ -225,6 +227,20 @@ describe('epoch-proving-job', () => {
225227
epochContext = mock<EpochProvingContext>();
226228
epochContext.stop.mockReturnValue(undefined);
227229
prover.createEpochProvingContext.mockReturnValue(epochContext);
230+
231+
// Stub the broker facade so dispatchOffloadedBlock returns synthetic execution
232+
// results — the orchestrator-side bookkeeping is exercised separately in the
233+
// prover-client orchestrator tests.
234+
const facade = {
235+
executeBlock: jest.fn(() =>
236+
Promise.resolve(
237+
new BlockExecutionResult(BlockNumber(1), SpongeBlob.init(), StateReference.empty(), Fr.ZERO, Fr.ZERO, []),
238+
),
239+
),
240+
expectJob: jest.fn(() => new Promise(() => {})),
241+
};
242+
(prover as any).getBrokerCircuitProverFacade = () => facade;
243+
228244
installSubTreeFactory();
229245

230246
topTree = mock<TopTreeOrchestrator>();

yarn-project/prover-node/src/job/epoch-proving-job.ts

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ import type { Tuple } from '@aztec/foundation/serialize';
88
import { sleep } from '@aztec/foundation/sleep';
99
import { Timer } from '@aztec/foundation/timer';
1010
import type { EpochProverFactory } from '@aztec/prover-client';
11-
import { type EpochProvingContext, TopTreeCancelledError } from '@aztec/prover-client/orchestrator';
11+
import {
12+
type BlockExecutionWatchers,
13+
type CheckpointSubTreeOrchestrator,
14+
type EpochProvingContext,
15+
TopTreeCancelledError,
16+
} from '@aztec/prover-client/orchestrator';
1217
import type { PublicProcessorFactory } from '@aztec/simulator/server';
13-
import type { CommitteeAttestation } from '@aztec/stdlib/block';
18+
import type { CommitteeAttestation, L2Block } from '@aztec/stdlib/block';
19+
import { BlockExecutionInputs } from '@aztec/stdlib/block_execution';
1420
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
1521
import {
1622
type EpochProvingJobState,
1723
EpochProvingJobTerminalState,
1824
type ForkMerkleTreeOperations,
25+
makeExecutionResultJobId,
1926
} from '@aztec/stdlib/interfaces/server';
27+
import { ProvingRequestType } from '@aztec/stdlib/proofs';
2028
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
2129
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
2230

@@ -679,6 +687,75 @@ export class EpochProvingJob implements Traceable {
679687
proveOverride: topTreeProveOverride,
680688
};
681689
}
690+
691+
/**
692+
* Dispatches the block as a `BLOCK_EXECUTION` job and wires up the orchestrator's
693+
* deterministic-ID watchers for the per-tx proving jobs the agent will enqueue
694+
* (`PRIVATE_TX_BASE_ROLLUP` for private-only txs, `PUBLIC_VM` carrying the
695+
* `BlockExecutionTxData` passenger for public txs). Once the agent reports
696+
* `BLOCK_EXECUTION` complete, applies the per-block summary to the orchestrator.
697+
*
698+
* Replaces the legacy in-prover-node `publicProcessor.process` + `addTxs` flow.
699+
*/
700+
private async dispatchOffloadedBlock(
701+
subTree: CheckpointSubTreeOrchestrator,
702+
block: L2Block,
703+
blockIndex: number,
704+
l1ToL2Messages: Fr[],
705+
blockTxs: Tx[],
706+
signal: AbortSignal,
707+
): Promise<void> {
708+
const facade = this.prover.getBrokerCircuitProverFacade();
709+
const blockNumber = block.header.getBlockNumber();
710+
const slotNumber = block.header.getSlot();
711+
712+
// Watchers must be registered before the agent enqueues per-tx jobs so the broker's
713+
// completion notifications are not lost.
714+
const watchers: BlockExecutionWatchers = {
715+
expectPrivateBaseRollupProofForTx: (txIndex, abortSignal) => {
716+
const id = makeExecutionResultJobId(
717+
this.epochNumber,
718+
blockNumber,
719+
slotNumber,
720+
txIndex,
721+
ProvingRequestType.PRIVATE_TX_BASE_ROLLUP,
722+
);
723+
return facade.expectJob(id, ProvingRequestType.PRIVATE_TX_BASE_ROLLUP, abortSignal);
724+
},
725+
expectAvmProofForTx: (txIndex, abortSignal) => {
726+
const id = makeExecutionResultJobId(
727+
this.epochNumber,
728+
blockNumber,
729+
slotNumber,
730+
txIndex,
731+
ProvingRequestType.PUBLIC_VM,
732+
);
733+
return facade.expectJob(id, ProvingRequestType.PUBLIC_VM, abortSignal);
734+
},
735+
};
736+
await subTree.addBlockForExecution(blockNumber, blockTxs, watchers);
737+
if (signal.aborted) {
738+
return;
739+
}
740+
741+
const startSpongeBlob = subTree.getBlockStartSpongeBlob(blockNumber);
742+
const inputs = new BlockExecutionInputs(
743+
this.epochNumber,
744+
0, // checkpointIndex within the sub-tree's single-checkpoint epoch — irrelevant to the agent's per-tx work
745+
block.header,
746+
block.body.txEffects.map(e => e.txHash),
747+
blockIndex === 0,
748+
l1ToL2Messages,
749+
startSpongeBlob,
750+
);
751+
752+
const result = await facade.executeBlock(inputs, signal, this.epochNumber);
753+
if (signal.aborted) {
754+
return;
755+
}
756+
757+
await subTree.applyBlockExecutionResult(blockNumber, result);
758+
}
682759
}
683760

684761
class HaltExecutionError extends Error {

0 commit comments

Comments
 (0)