@aztec/prover-client 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import {
9
9
  type TxEffect,
10
10
  makeEmptyProcessedTx,
11
11
  makePaddingProcessedTx,
12
+ mapPublicKernelToCircuitName,
12
13
  toTxEffect,
13
14
  } from '@aztec/circuit-types';
14
15
  import {
@@ -20,6 +21,7 @@ import {
20
21
  type PublicInputsAndRecursiveProof,
21
22
  type ServerCircuitProver,
22
23
  } from '@aztec/circuit-types/interfaces';
24
+ import { type CircuitName } from '@aztec/circuit-types/stats';
23
25
  import {
24
26
  AGGREGATION_OBJECT_LENGTH,
25
27
  AvmCircuitInputs,
@@ -48,11 +50,12 @@ import {
48
50
  } from '@aztec/circuits.js';
49
51
  import { makeTuple } from '@aztec/foundation/array';
50
52
  import { padArrayEnd } from '@aztec/foundation/collection';
51
- import { AbortedError } from '@aztec/foundation/error';
53
+ import { AbortError } from '@aztec/foundation/error';
52
54
  import { createDebugLogger } from '@aztec/foundation/log';
53
55
  import { promiseWithResolvers } from '@aztec/foundation/promise';
54
56
  import { BufferReader, type Tuple } from '@aztec/foundation/serialize';
55
57
  import { pushTestData } from '@aztec/foundation/testing';
58
+ import { Attributes, type TelemetryClient, type Tracer, trackSpan, wrapCallbackInSpan } from '@aztec/telemetry-client';
56
59
  import { type MerkleTreeOperations } from '@aztec/world-state';
57
60
 
58
61
  import { inspect } from 'util';
@@ -91,7 +94,16 @@ export class ProvingOrchestrator {
91
94
  private pendingProvingJobs: AbortController[] = [];
92
95
  private paddingTx: PaddingProcessedTx | undefined = undefined;
93
96
 
94
- constructor(private db: MerkleTreeOperations, private prover: ServerCircuitProver, private initialHeader?: Header) {}
97
+ public readonly tracer: Tracer;
98
+
99
+ constructor(
100
+ private db: MerkleTreeOperations,
101
+ private prover: ServerCircuitProver,
102
+ telemetryClient: TelemetryClient,
103
+ private initialHeader?: Header,
104
+ ) {
105
+ this.tracer = telemetryClient.getTracer('ProvingOrchestrator');
106
+ }
95
107
 
96
108
  /**
97
109
  * Resets the orchestrator's cached padding tx.
@@ -108,6 +120,10 @@ export class ProvingOrchestrator {
108
120
  * @param verificationKeys - The private kernel verification keys
109
121
  * @returns A proving ticket, containing a promise notifying of proving completion
110
122
  */
123
+ @trackSpan('ProvingOrchestrator.startNewBlock', (numTxs, globalVariables) => ({
124
+ [Attributes.BLOCK_SIZE]: numTxs,
125
+ [Attributes.BLOCK_NUMBER]: globalVariables.blockNumber.toNumber(),
126
+ }))
111
127
  public async startNewBlock(
112
128
  numTxs: number,
113
129
  globalVariables: GlobalVariables,
@@ -193,6 +209,9 @@ export class ProvingOrchestrator {
193
209
  * The interface to add a simulated transaction to the scheduler
194
210
  * @param tx - The transaction to be proven
195
211
  */
212
+ @trackSpan('ProvingOrchestrator.addNewTx', tx => ({
213
+ [Attributes.TX_HASH]: tx.hash.toString(),
214
+ }))
196
215
  public async addNewTx(tx: ProcessedTx): Promise<void> {
197
216
  if (!this.provingState) {
198
217
  throw new Error(`Invalid proving state, call startNewBlock before adding transactions`);
@@ -213,6 +232,17 @@ export class ProvingOrchestrator {
213
232
  /**
214
233
  * Marks the block as full and pads it to the full power of 2 block size, no more transactions will be accepted.
215
234
  */
235
+ @trackSpan('ProvingOrchestrator.setBlockCompleted', function () {
236
+ if (!this.provingState) {
237
+ return {};
238
+ }
239
+
240
+ return {
241
+ [Attributes.BLOCK_NUMBER]: this.provingState!.globalVariables.blockNumber.toNumber(),
242
+ [Attributes.BLOCK_SIZE]: this.provingState!.totalNumTxs,
243
+ [Attributes.BLOCK_TXS_COUNT]: this.provingState!.transactionsReceived,
244
+ };
245
+ })
216
246
  public async setBlockCompleted() {
217
247
  if (!this.provingState) {
218
248
  throw new Error(`Invalid proving state, call startNewBlock before adding transactions or completing the block`);
@@ -264,18 +294,26 @@ export class ProvingOrchestrator {
264
294
  logger.debug(`Enqueuing deferred proving for padding txs to enqueue ${txInputs.length} paddings`);
265
295
  this.deferredProving(
266
296
  provingState,
267
- signal =>
268
- this.prover.getEmptyPrivateKernelProof(
269
- {
270
- // Chain id and version should not change even if the proving state does, so it's safe to use them for the padding tx
271
- // which gets cached across multiple runs of the orchestrator with different proving states. If they were to change,
272
- // we'd have to clear out the paddingTx here and regenerate it when they do.
273
- chainId: unprovenPaddingTx.data.constants.txContext.chainId,
274
- version: unprovenPaddingTx.data.constants.txContext.version,
275
- header: unprovenPaddingTx.data.constants.historicalHeader,
276
- },
277
- signal,
278
- ),
297
+ wrapCallbackInSpan(
298
+ this.tracer,
299
+ 'ProvingOrchestrator.prover.getEmptyPrivateKernelProof',
300
+ {
301
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
302
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: 'private-kernel-empty' as CircuitName,
303
+ },
304
+ signal =>
305
+ this.prover.getEmptyPrivateKernelProof(
306
+ {
307
+ // Chain id and version should not change even if the proving state does, so it's safe to use them for the padding tx
308
+ // which gets cached across multiple runs of the orchestrator with different proving states. If they were to change,
309
+ // we'd have to clear out the paddingTx here and regenerate it when they do.
310
+ chainId: unprovenPaddingTx.data.constants.txContext.chainId,
311
+ version: unprovenPaddingTx.data.constants.txContext.version,
312
+ header: unprovenPaddingTx.data.constants.historicalHeader,
313
+ },
314
+ signal,
315
+ ),
316
+ ),
279
317
  result => {
280
318
  logger.debug(`Completed proof for padding tx, now enqueuing ${txInputs.length} padding txs`);
281
319
  this.paddingTx = makePaddingProcessedTx(result);
@@ -319,6 +357,13 @@ export class ProvingOrchestrator {
319
357
  * Performs the final tree update for the block and returns the fully proven block.
320
358
  * @returns The fully proven block and proof.
321
359
  */
360
+ @trackSpan('ProvingOrchestrator.finaliseBlock', function () {
361
+ return {
362
+ [Attributes.BLOCK_NUMBER]: this.provingState!.globalVariables.blockNumber.toNumber(),
363
+ [Attributes.BLOCK_TXS_COUNT]: this.provingState!.transactionsReceived,
364
+ [Attributes.BLOCK_SIZE]: this.provingState!.totalNumTxs,
365
+ };
366
+ })
322
367
  public async finaliseBlock() {
323
368
  try {
324
369
  if (
@@ -475,7 +520,7 @@ export class ProvingOrchestrator {
475
520
 
476
521
  await callback(result);
477
522
  } catch (err) {
478
- if (err instanceof AbortedError) {
523
+ if (err instanceof AbortError) {
479
524
  // operation was cancelled, probably because the block was cancelled
480
525
  // drop this result
481
526
  return;
@@ -496,6 +541,9 @@ export class ProvingOrchestrator {
496
541
  }
497
542
 
498
543
  // Updates the merkle trees for a transaction. The first enqueued job for a transaction
544
+ @trackSpan('ProvingOrchestrator.prepareBaseRollupInputs', (_, tx) => ({
545
+ [Attributes.TX_HASH]: tx.hash.toString(),
546
+ }))
499
547
  private async prepareBaseRollupInputs(
500
548
  provingState: ProvingState | undefined,
501
549
  tx: ProcessedTx,
@@ -593,7 +641,16 @@ export class ProvingOrchestrator {
593
641
 
594
642
  this.deferredProving(
595
643
  provingState,
596
- signal => this.prover.getBaseRollupProof(tx.baseRollupInputs, signal),
644
+ wrapCallbackInSpan(
645
+ this.tracer,
646
+ 'ProvingOrchestrator.prover.getBaseRollupProof',
647
+ {
648
+ [Attributes.TX_HASH]: tx.processedTx.hash.toString(),
649
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
650
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: 'base-rollup' as CircuitName,
651
+ },
652
+ signal => this.prover.getBaseRollupProof(tx.baseRollupInputs, signal),
653
+ ),
597
654
  result => {
598
655
  logger.debug(`Completed proof for base rollup for tx ${tx.processedTx.hash.toString()}`);
599
656
  validatePartialState(result.inputs.end, tx.treeSnapshots);
@@ -622,7 +679,15 @@ export class ProvingOrchestrator {
622
679
 
623
680
  this.deferredProving(
624
681
  provingState,
625
- signal => this.prover.getMergeRollupProof(inputs, signal),
682
+ wrapCallbackInSpan(
683
+ this.tracer,
684
+ 'ProvingOrchestrator.prover.getMergeRollupProof',
685
+ {
686
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
687
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: 'merge-rollup' as CircuitName,
688
+ },
689
+ signal => this.prover.getMergeRollupProof(inputs, signal),
690
+ ),
626
691
  result => {
627
692
  this.storeAndExecuteNextMergeLevel(provingState, level, index, [
628
693
  result.inputs,
@@ -658,7 +723,15 @@ export class ProvingOrchestrator {
658
723
 
659
724
  this.deferredProving(
660
725
  provingState,
661
- signal => this.prover.getRootRollupProof(inputs, signal),
726
+ wrapCallbackInSpan(
727
+ this.tracer,
728
+ 'ProvingOrchestrator.prover.getRootRollupProof',
729
+ {
730
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
731
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: 'root-rollup' as CircuitName,
732
+ },
733
+ signal => this.prover.getRootRollupProof(inputs, signal),
734
+ ),
662
735
  result => {
663
736
  provingState.rootRollupPublicInputs = result.inputs;
664
737
  provingState.finalAggregationObject = extractAggregationObject(
@@ -680,7 +753,15 @@ export class ProvingOrchestrator {
680
753
  private enqueueBaseParityCircuit(provingState: ProvingState, inputs: BaseParityInputs, index: number) {
681
754
  this.deferredProving(
682
755
  provingState,
683
- signal => this.prover.getBaseParityProof(inputs, signal),
756
+ wrapCallbackInSpan(
757
+ this.tracer,
758
+ 'ProvingOrchestrator.prover.getBaseParityProof',
759
+ {
760
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
761
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: 'base-parity' as CircuitName,
762
+ },
763
+ signal => this.prover.getBaseParityProof(inputs, signal),
764
+ ),
684
765
  rootInput => {
685
766
  provingState.setRootParityInputs(rootInput, index);
686
767
  if (provingState.areRootParityInputsReady()) {
@@ -701,7 +782,15 @@ export class ProvingOrchestrator {
701
782
  private enqueueRootParityCircuit(provingState: ProvingState | undefined, inputs: RootParityInputs) {
702
783
  this.deferredProving(
703
784
  provingState,
704
- signal => this.prover.getRootParityProof(inputs, signal),
785
+ wrapCallbackInSpan(
786
+ this.tracer,
787
+ 'ProvingOrchestrator.prover.getRootParityProof',
788
+ {
789
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
790
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: 'root-parity' as CircuitName,
791
+ },
792
+ signal => this.prover.getRootParityProof(inputs, signal),
793
+ ),
705
794
  async rootInput => {
706
795
  provingState!.finalRootParityInput = rootInput;
707
796
  await this.checkAndEnqueueRootRollup(provingState);
@@ -770,25 +859,34 @@ export class ProvingOrchestrator {
770
859
  if (publicFunction.vmRequest) {
771
860
  // This function tries to do AVM proving. If there is a failure, it fakes the proof unless AVM_PROVING_STRICT is defined.
772
861
  // Nothing downstream depends on the AVM proof yet. So having this mode lets us incrementally build the AVM circuit.
773
- const doAvmProving = async (signal: AbortSignal) => {
774
- const inputs: AvmCircuitInputs = new AvmCircuitInputs(
775
- publicFunction.vmRequest!.bytecode,
776
- publicFunction.vmRequest!.calldata,
777
- publicFunction.vmRequest!.kernelRequest.inputs.publicCall.callStackItem.publicInputs,
778
- publicFunction.vmRequest!.avmHints,
779
- );
780
- try {
781
- return await this.prover.getAvmProof(inputs, signal);
782
- } catch (err) {
783
- if (process.env.AVM_PROVING_STRICT) {
784
- throw err;
785
- } else {
786
- logger.warn(`Error thrown when proving AVM circuit: ${err}`);
787
- logger.warn(`AVM_PROVING_STRICT is off, faking AVM proof and carrying on...`);
788
- return { proof: makeEmptyProof(), verificationKey: VerificationKeyData.makeFake() };
862
+ const doAvmProving = wrapCallbackInSpan(
863
+ this.tracer,
864
+ 'ProvingOrchestrator.prover.getAvmProof',
865
+ {
866
+ [Attributes.TX_HASH]: txProvingState.processedTx.hash.toString(),
867
+ [Attributes.APP_CIRCUIT_NAME]: publicFunction.vmRequest!.functionName,
868
+ },
869
+ async (signal: AbortSignal) => {
870
+ const inputs: AvmCircuitInputs = new AvmCircuitInputs(
871
+ publicFunction.vmRequest!.functionName,
872
+ publicFunction.vmRequest!.bytecode,
873
+ publicFunction.vmRequest!.calldata,
874
+ publicFunction.vmRequest!.kernelRequest.inputs.publicCall.callStackItem.publicInputs,
875
+ publicFunction.vmRequest!.avmHints,
876
+ );
877
+ try {
878
+ return await this.prover.getAvmProof(inputs, signal);
879
+ } catch (err) {
880
+ if (process.env.AVM_PROVING_STRICT) {
881
+ throw err;
882
+ } else {
883
+ logger.warn(`Error thrown when proving AVM circuit: ${err}`);
884
+ logger.warn(`AVM_PROVING_STRICT is off, faking AVM proof and carrying on...`);
885
+ return { proof: makeEmptyProof(), verificationKey: VerificationKeyData.makeFake() };
886
+ }
789
887
  }
790
- }
791
- };
888
+ },
889
+ );
792
890
  this.deferredProving(provingState, doAvmProving, proofAndVk => {
793
891
  logger.debug(`Proven VM for function index ${functionIndex} of tx index ${txIndex}`);
794
892
  this.checkAndEnqueuePublicKernel(provingState, txIndex, functionIndex, proofAndVk.proof);
@@ -834,13 +932,25 @@ export class ProvingOrchestrator {
834
932
 
835
933
  this.deferredProving(
836
934
  provingState,
837
- (signal): Promise<PublicInputsAndRecursiveProof<KernelCircuitPublicInputs | PublicKernelCircuitPublicInputs>> => {
838
- if (request.type === PublicKernelType.TAIL) {
839
- return this.prover.getPublicTailProof(request, signal);
840
- } else {
841
- return this.prover.getPublicKernelProof(request, signal);
842
- }
843
- },
935
+ wrapCallbackInSpan(
936
+ this.tracer,
937
+ request.type === PublicKernelType.TAIL
938
+ ? 'ProvingOrchestrator.prover.getPublicTailProof'
939
+ : 'ProvingOrchestrator.prover.getPublicKernelProof',
940
+ {
941
+ [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server',
942
+ [Attributes.PROTOCOL_CIRCUIT_NAME]: mapPublicKernelToCircuitName(request.type),
943
+ },
944
+ (
945
+ signal,
946
+ ): Promise<PublicInputsAndRecursiveProof<KernelCircuitPublicInputs | PublicKernelCircuitPublicInputs>> => {
947
+ if (request.type === PublicKernelType.TAIL) {
948
+ return this.prover.getPublicTailProof(request, signal);
949
+ } else {
950
+ return this.prover.getPublicKernelProof(request, signal);
951
+ }
952
+ },
953
+ ),
844
954
  result => {
845
955
  const nextKernelRequest = txProvingState.getNextPublicKernelFromKernelProof(
846
956
  functionIndex,
@@ -879,5 +989,9 @@ function extractAggregationObject(proof: Proof, numPublicInputs: number): Fr[] {
879
989
  Fr.SIZE_IN_BYTES * (numPublicInputs - AGGREGATION_OBJECT_LENGTH),
880
990
  Fr.SIZE_IN_BYTES * numPublicInputs,
881
991
  );
992
+ // TODO(#7159): Remove the following workaround
993
+ if (buffer.length === 0) {
994
+ return Array.from({ length: AGGREGATION_OBJECT_LENGTH }, () => Fr.ZERO);
995
+ }
882
996
  return BufferReader.asReader(buffer).readArray(AGGREGATION_OBJECT_LENGTH, Fr);
883
997
  }
@@ -27,21 +27,23 @@ import type {
27
27
  RootRollupPublicInputs,
28
28
  } from '@aztec/circuits.js';
29
29
  import { randomBytes } from '@aztec/foundation/crypto';
30
- import { AbortedError, TimeoutError } from '@aztec/foundation/error';
30
+ import { AbortError, TimeoutError } from '@aztec/foundation/error';
31
31
  import { MemoryFifo } from '@aztec/foundation/fifo';
32
32
  import { createDebugLogger } from '@aztec/foundation/log';
33
- import { type PromiseWithResolvers, promiseWithResolvers } from '@aztec/foundation/promise';
33
+ import { type PromiseWithResolvers, RunningPromise, promiseWithResolvers } from '@aztec/foundation/promise';
34
34
 
35
35
  type ProvingJobWithResolvers<T extends ProvingRequest = ProvingRequest> = {
36
36
  id: string;
37
37
  request: T;
38
38
  signal?: AbortSignal;
39
39
  attempts: number;
40
+ heartbeat: number;
40
41
  } & PromiseWithResolvers<ProvingRequestResult<T['type']>>;
41
42
 
42
43
  const MAX_RETRIES = 3;
43
44
 
44
45
  const defaultIdGenerator = () => randomBytes(4).toString('hex');
46
+ const defaultTimeSource = () => Date.now();
45
47
 
46
48
  /**
47
49
  * A helper class that sits in between services that need proofs created and agents that can create them.
@@ -52,9 +54,44 @@ export class MemoryProvingQueue implements ServerCircuitProver, ProvingJobSource
52
54
  private queue = new MemoryFifo<ProvingJobWithResolvers>();
53
55
  private jobsInProgress = new Map<string, ProvingJobWithResolvers>();
54
56
 
55
- constructor(private generateId = defaultIdGenerator) {}
57
+ private runningPromise: RunningPromise;
58
+
59
+ constructor(
60
+ /** Timeout the job if an agent doesn't report back in this time */
61
+ private jobTimeoutMs = 60 * 1000,
62
+ /** How often to check for timed out jobs */
63
+ pollingIntervalMs = 1000,
64
+ private generateId = defaultIdGenerator,
65
+ private timeSource = defaultTimeSource,
66
+ ) {
67
+ this.runningPromise = new RunningPromise(this.poll, pollingIntervalMs);
68
+ }
69
+
70
+ public start() {
71
+ if (this.runningPromise.isRunning()) {
72
+ this.log.warn('Proving queue is already running');
73
+ return;
74
+ }
75
+
76
+ this.runningPromise.start();
77
+ this.log.info('Proving queue started');
78
+ }
79
+
80
+ public async stop() {
81
+ if (!this.runningPromise.isRunning()) {
82
+ this.log.warn('Proving queue is already stopped');
83
+ return;
84
+ }
85
+
86
+ await this.runningPromise.stop();
87
+ this.log.info('Proving queue stopped');
88
+ }
89
+
90
+ public async getProvingJob({ timeoutSec = 1 } = {}): Promise<ProvingJob<ProvingRequest> | undefined> {
91
+ if (!this.runningPromise.isRunning()) {
92
+ throw new Error('Proving queue is not running. Start the queue before getting jobs.');
93
+ }
56
94
 
57
- async getProvingJob({ timeoutSec = 1 } = {}): Promise<ProvingJob<ProvingRequest> | undefined> {
58
95
  try {
59
96
  const job = await this.queue.get(timeoutSec);
60
97
  if (!job) {
@@ -62,10 +99,10 @@ export class MemoryProvingQueue implements ServerCircuitProver, ProvingJobSource
62
99
  }
63
100
 
64
101
  if (job.signal?.aborted) {
65
- this.log.debug(`Job ${job.id} type=${job.request.type} has been aborted`);
66
102
  return undefined;
67
103
  }
68
104
 
105
+ job.heartbeat = this.timeSource();
69
106
  this.jobsInProgress.set(job.id, job);
70
107
  return {
71
108
  id: job.id,
@@ -81,25 +118,33 @@ export class MemoryProvingQueue implements ServerCircuitProver, ProvingJobSource
81
118
  }
82
119
 
83
120
  resolveProvingJob<T extends ProvingRequestType>(jobId: string, result: ProvingRequestResult<T>): Promise<void> {
121
+ if (!this.runningPromise.isRunning()) {
122
+ throw new Error('Proving queue is not running.');
123
+ }
124
+
84
125
  const job = this.jobsInProgress.get(jobId);
85
126
  if (!job) {
86
- return Promise.reject(new Error('Job not found'));
127
+ this.log.warn(`Job id=${jobId} not found. Can't resolve`);
128
+ return Promise.resolve();
87
129
  }
88
130
 
89
131
  this.jobsInProgress.delete(jobId);
90
-
91
- if (job.signal?.aborted) {
92
- return Promise.resolve();
132
+ if (!job.signal?.aborted) {
133
+ job.resolve(result);
93
134
  }
94
135
 
95
- job.resolve(result);
96
136
  return Promise.resolve();
97
137
  }
98
138
 
99
139
  rejectProvingJob(jobId: string, err: any): Promise<void> {
140
+ if (!this.runningPromise.isRunning()) {
141
+ throw new Error('Proving queue is not running.');
142
+ }
143
+
100
144
  const job = this.jobsInProgress.get(jobId);
101
145
  if (!job) {
102
- return Promise.reject(new Error('Job not found'));
146
+ this.log.warn(`Job id=${jobId} not found. Can't reject`);
147
+ return Promise.resolve();
103
148
  }
104
149
 
105
150
  this.jobsInProgress.delete(jobId);
@@ -123,10 +168,50 @@ export class MemoryProvingQueue implements ServerCircuitProver, ProvingJobSource
123
168
  return Promise.resolve();
124
169
  }
125
170
 
171
+ public heartbeat(jobId: string): Promise<void> {
172
+ if (!this.runningPromise.isRunning()) {
173
+ throw new Error('Proving queue is not running.');
174
+ }
175
+
176
+ const job = this.jobsInProgress.get(jobId);
177
+ if (job) {
178
+ job.heartbeat = this.timeSource();
179
+ }
180
+
181
+ return Promise.resolve();
182
+ }
183
+
184
+ public isJobRunning(jobId: string): boolean {
185
+ return this.jobsInProgress.has(jobId);
186
+ }
187
+
188
+ private poll = () => {
189
+ const now = this.timeSource();
190
+
191
+ for (const job of this.jobsInProgress.values()) {
192
+ if (job.signal?.aborted) {
193
+ this.jobsInProgress.delete(job.id);
194
+ continue;
195
+ }
196
+
197
+ if (job.heartbeat + this.jobTimeoutMs < now) {
198
+ this.log.warn(`Job ${job.id} type=${ProvingRequestType[job.request.type]} has timed out`);
199
+
200
+ this.jobsInProgress.delete(job.id);
201
+ job.heartbeat = 0;
202
+ this.queue.put(job);
203
+ }
204
+ }
205
+ };
206
+
126
207
  private enqueue<T extends ProvingRequest>(
127
208
  request: T,
128
209
  signal?: AbortSignal,
129
210
  ): Promise<ProvingRequestResult<T['type']>> {
211
+ if (!this.runningPromise.isRunning()) {
212
+ return Promise.reject(new Error('Proving queue is not running.'));
213
+ }
214
+
130
215
  const { promise, resolve, reject } = promiseWithResolvers<ProvingRequestResult<T['type']>>();
131
216
  const item: ProvingJobWithResolvers<T> = {
132
217
  id: this.generateId(),
@@ -136,10 +221,11 @@ export class MemoryProvingQueue implements ServerCircuitProver, ProvingJobSource
136
221
  resolve,
137
222
  reject,
138
223
  attempts: 1,
224
+ heartbeat: 0,
139
225
  };
140
226
 
141
227
  if (signal) {
142
- signal.addEventListener('abort', () => reject(new AbortedError('Operation has been aborted')));
228
+ signal.addEventListener('abort', () => reject(new AbortError('Operation has been aborted')));
143
229
  }
144
230
 
145
231
  this.log.debug(
@@ -16,7 +16,7 @@ import { ProvingError } from './proving-error.js';
16
16
  * A helper class that encapsulates a circuit prover and connects it to a job source.
17
17
  */
18
18
  export class ProverAgent {
19
- private inFlightPromises = new Set<Promise<any>>();
19
+ private inFlightPromises = new Map<string, Promise<any>>();
20
20
  private runningPromise?: RunningPromise;
21
21
 
22
22
  constructor(
@@ -50,20 +50,28 @@ export class ProverAgent {
50
50
  }
51
51
 
52
52
  this.runningPromise = new RunningPromise(async () => {
53
+ for (const jobId of this.inFlightPromises.keys()) {
54
+ await jobSource.heartbeat(jobId);
55
+ }
56
+
53
57
  while (this.inFlightPromises.size < this.maxConcurrency) {
54
- const job = await jobSource.getProvingJob();
55
- if (!job) {
56
- // job source is fully drained, sleep for a bit and try again
57
- return;
58
+ try {
59
+ const job = await jobSource.getProvingJob();
60
+ if (!job) {
61
+ // job source is fully drained, sleep for a bit and try again
62
+ return;
63
+ }
64
+
65
+ const promise = this.work(jobSource, job).finally(() => this.inFlightPromises.delete(job.id));
66
+ this.inFlightPromises.set(job.id, promise);
67
+ } catch (err) {
68
+ this.log.warn(`Error processing job: ${err}`);
58
69
  }
59
-
60
- const promise = this.work(jobSource, job).finally(() => this.inFlightPromises.delete(promise));
61
- this.inFlightPromises.add(promise);
62
70
  }
63
71
  }, this.pollIntervalMs);
64
72
 
65
73
  this.runningPromise.start();
66
- this.log.info('Agent started');
74
+ this.log.info(`Agent started with concurrency=${this.maxConcurrency}`);
67
75
  }
68
76
 
69
77
  async stop(): Promise<void> {
@@ -79,14 +87,31 @@ export class ProverAgent {
79
87
 
80
88
  private async work(jobSource: ProvingJobSource, job: ProvingJob<ProvingRequest>): Promise<void> {
81
89
  try {
90
+ this.log.debug(`Picked up proving job id=${job.id} type=${ProvingRequestType[job.request.type]}`);
82
91
  const [time, result] = await elapsed(this.getProof(job.request));
83
- await jobSource.resolveProvingJob(job.id, result);
84
- this.log.debug(
85
- `Processed proving job id=${job.id} type=${ProvingRequestType[job.request.type]} duration=${time}ms`,
86
- );
92
+ if (this.isRunning()) {
93
+ this.log.debug(
94
+ `Processed proving job id=${job.id} type=${ProvingRequestType[job.request.type]} duration=${time}ms`,
95
+ );
96
+ await jobSource.resolveProvingJob(job.id, result);
97
+ } else {
98
+ this.log.debug(
99
+ `Dropping proving job id=${job.id} type=${
100
+ ProvingRequestType[job.request.type]
101
+ } duration=${time}ms: agent stopped`,
102
+ );
103
+ }
87
104
  } catch (err) {
88
- this.log.error(`Error processing proving job id=${job.id} type=${ProvingRequestType[job.request.type]}: ${err}`);
89
- await jobSource.rejectProvingJob(job.id, new ProvingError((err as any)?.message ?? String(err)));
105
+ if (this.isRunning()) {
106
+ this.log.error(
107
+ `Error processing proving job id=${job.id} type=${ProvingRequestType[job.request.type]}: ${err}`,
108
+ );
109
+ await jobSource.rejectProvingJob(job.id, new ProvingError((err as any)?.message ?? String(err)));
110
+ } else {
111
+ this.log.debug(
112
+ `Dropping proving job id=${job.id} type=${ProvingRequestType[job.request.type]}: agent stopped: ${err}`,
113
+ );
114
+ }
90
115
  }
91
116
  }
92
117