@aztec-labs/prover-client 6.0.0-nightly.20260829
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.
- package/README.md +1 -0
- package/dest/config.d.ts +17 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +63 -0
- package/dest/index.d.ts +4 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +2 -0
- package/dest/light/index.d.ts +2 -0
- package/dest/light/index.d.ts.map +1 -0
- package/dest/light/index.js +1 -0
- package/dest/light/lightweight_checkpoint_builder.d.ts +58 -0
- package/dest/light/lightweight_checkpoint_builder.d.ts.map +1 -0
- package/dest/light/lightweight_checkpoint_builder.js +243 -0
- package/dest/mocks/fixtures.d.ts +20 -0
- package/dest/mocks/fixtures.d.ts.map +1 -0
- package/dest/mocks/fixtures.js +96 -0
- package/dest/mocks/test_context.d.ts +93 -0
- package/dest/mocks/test_context.d.ts.map +1 -0
- package/dest/mocks/test_context.js +354 -0
- package/dest/orchestrator/block-building-helpers.d.ts +67 -0
- package/dest/orchestrator/block-building-helpers.d.ts.map +1 -0
- package/dest/orchestrator/block-building-helpers.js +274 -0
- package/dest/orchestrator/block-proving-state.d.ts +107 -0
- package/dest/orchestrator/block-proving-state.d.ts.map +1 -0
- package/dest/orchestrator/block-proving-state.js +339 -0
- package/dest/orchestrator/checkpoint-proving-state.d.ts +67 -0
- package/dest/orchestrator/checkpoint-proving-state.d.ts.map +1 -0
- package/dest/orchestrator/checkpoint-proving-state.js +175 -0
- package/dest/orchestrator/checkpoint-sub-tree-orchestrator.d.ts +169 -0
- package/dest/orchestrator/checkpoint-sub-tree-orchestrator.d.ts.map +1 -0
- package/dest/orchestrator/checkpoint-sub-tree-orchestrator.js +1047 -0
- package/dest/orchestrator/chonk-cache.d.ts +39 -0
- package/dest/orchestrator/chonk-cache.d.ts.map +1 -0
- package/dest/orchestrator/chonk-cache.js +80 -0
- package/dest/orchestrator/index.d.ts +4 -0
- package/dest/orchestrator/index.d.ts.map +1 -0
- package/dest/orchestrator/index.js +3 -0
- package/dest/orchestrator/orchestrator_metrics.d.ts +8 -0
- package/dest/orchestrator/orchestrator_metrics.d.ts.map +1 -0
- package/dest/orchestrator/orchestrator_metrics.js +13 -0
- package/dest/orchestrator/proving-scheduler.d.ts +81 -0
- package/dest/orchestrator/proving-scheduler.d.ts.map +1 -0
- package/dest/orchestrator/proving-scheduler.js +120 -0
- package/dest/orchestrator/top-tree-orchestrator.d.ts +89 -0
- package/dest/orchestrator/top-tree-orchestrator.d.ts.map +1 -0
- package/dest/orchestrator/top-tree-orchestrator.js +233 -0
- package/dest/orchestrator/top-tree-proving-state.d.ts +61 -0
- package/dest/orchestrator/top-tree-proving-state.d.ts.map +1 -0
- package/dest/orchestrator/top-tree-proving-state.js +185 -0
- package/dest/orchestrator/tx-proving-state.d.ts +37 -0
- package/dest/orchestrator/tx-proving-state.d.ts.map +1 -0
- package/dest/orchestrator/tx-proving-state.js +77 -0
- package/dest/prover-client/factory.d.ts +6 -0
- package/dest/prover-client/factory.d.ts.map +1 -0
- package/dest/prover-client/factory.js +5 -0
- package/dest/prover-client/index.d.ts +3 -0
- package/dest/prover-client/index.d.ts.map +1 -0
- package/dest/prover-client/index.js +2 -0
- package/dest/prover-client/prover-client.d.ts +99 -0
- package/dest/prover-client/prover-client.d.ts.map +1 -0
- package/dest/prover-client/prover-client.js +163 -0
- package/dest/proving_broker/broker_prover_facade.d.ts +52 -0
- package/dest/proving_broker/broker_prover_facade.d.ts.map +1 -0
- package/dest/proving_broker/broker_prover_facade.js +360 -0
- package/dest/proving_broker/config.d.ts +40 -0
- package/dest/proving_broker/config.d.ts.map +1 -0
- package/dest/proving_broker/config.js +138 -0
- package/dest/proving_broker/factory.d.ts +5 -0
- package/dest/proving_broker/factory.d.ts.map +1 -0
- package/dest/proving_broker/factory.js +13 -0
- package/dest/proving_broker/fixtures.d.ts +6 -0
- package/dest/proving_broker/fixtures.d.ts.map +1 -0
- package/dest/proving_broker/fixtures.js +13 -0
- package/dest/proving_broker/index.d.ts +11 -0
- package/dest/proving_broker/index.d.ts.map +1 -0
- package/dest/proving_broker/index.js +10 -0
- package/dest/proving_broker/proof_store/factory.d.ts +3 -0
- package/dest/proving_broker/proof_store/factory.d.ts.map +1 -0
- package/dest/proving_broker/proof_store/factory.js +13 -0
- package/dest/proving_broker/proof_store/file_store_proof_store.d.ts +18 -0
- package/dest/proving_broker/proof_store/file_store_proof_store.d.ts.map +1 -0
- package/dest/proving_broker/proof_store/file_store_proof_store.js +60 -0
- package/dest/proving_broker/proof_store/index.d.ts +5 -0
- package/dest/proving_broker/proof_store/index.d.ts.map +1 -0
- package/dest/proving_broker/proof_store/index.js +4 -0
- package/dest/proving_broker/proof_store/inline_proof_store.d.ts +15 -0
- package/dest/proving_broker/proof_store/inline_proof_store.d.ts.map +1 -0
- package/dest/proving_broker/proof_store/inline_proof_store.js +41 -0
- package/dest/proving_broker/proof_store/proof_store.d.ts +36 -0
- package/dest/proving_broker/proof_store/proof_store.d.ts.map +1 -0
- package/dest/proving_broker/proof_store/proof_store.js +3 -0
- package/dest/proving_broker/proving_agent.d.ts +42 -0
- package/dest/proving_broker/proving_agent.d.ts.map +1 -0
- package/dest/proving_broker/proving_agent.js +155 -0
- package/dest/proving_broker/proving_broker.d.ts +77 -0
- package/dest/proving_broker/proving_broker.d.ts.map +1 -0
- package/dest/proving_broker/proving_broker.js +780 -0
- package/dest/proving_broker/proving_broker_database/memory.d.ts +20 -0
- package/dest/proving_broker/proving_broker_database/memory.d.ts.map +1 -0
- package/dest/proving_broker/proving_broker_database/memory.js +67 -0
- package/dest/proving_broker/proving_broker_database/persisted.d.ts +40 -0
- package/dest/proving_broker/proving_broker_database/persisted.d.ts.map +1 -0
- package/dest/proving_broker/proving_broker_database/persisted.js +623 -0
- package/dest/proving_broker/proving_broker_database.d.ts +66 -0
- package/dest/proving_broker/proving_broker_database.d.ts.map +1 -0
- package/dest/proving_broker/proving_broker_database.js +3 -0
- package/dest/proving_broker/proving_broker_instrumentation.d.ts +31 -0
- package/dest/proving_broker/proving_broker_instrumentation.d.ts.map +1 -0
- package/dest/proving_broker/proving_broker_instrumentation.js +97 -0
- package/dest/proving_broker/proving_job_controller.d.ts +34 -0
- package/dest/proving_broker/proving_job_controller.d.ts.map +1 -0
- package/dest/proving_broker/proving_job_controller.js +184 -0
- package/dest/proving_broker/rpc.d.ts +38 -0
- package/dest/proving_broker/rpc.d.ts.map +1 -0
- package/dest/proving_broker/rpc.js +138 -0
- package/dest/test/epoch_settlement.d.ts +36 -0
- package/dest/test/epoch_settlement.d.ts.map +1 -0
- package/dest/test/epoch_settlement.js +52 -0
- package/dest/test/index.d.ts +2 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +1 -0
- package/dest/test/mock_proof_store.d.ts +9 -0
- package/dest/test/mock_proof_store.d.ts.map +1 -0
- package/dest/test/mock_proof_store.js +10 -0
- package/dest/test/mock_prover.d.ts +38 -0
- package/dest/test/mock_prover.d.ts.map +1 -0
- package/dest/test/mock_prover.js +91 -0
- package/package.json +113 -0
- package/src/config.ts +87 -0
- package/src/index.ts +4 -0
- package/src/light/index.ts +1 -0
- package/src/light/lightweight_checkpoint_builder.ts +342 -0
- package/src/mocks/fixtures.ts +120 -0
- package/src/mocks/test_context.ts +477 -0
- package/src/orchestrator/block-building-helpers.ts +486 -0
- package/src/orchestrator/block-proving-state.ts +457 -0
- package/src/orchestrator/checkpoint-proving-state.ts +247 -0
- package/src/orchestrator/checkpoint-sub-tree-orchestrator.ts +1010 -0
- package/src/orchestrator/chonk-cache.ts +99 -0
- package/src/orchestrator/index.ts +12 -0
- package/src/orchestrator/orchestrator_metrics.ts +18 -0
- package/src/orchestrator/proving-scheduler.ts +160 -0
- package/src/orchestrator/top-tree-orchestrator.ts +394 -0
- package/src/orchestrator/top-tree-proving-state.ts +219 -0
- package/src/orchestrator/tx-proving-state.ts +121 -0
- package/src/prover-client/factory.ts +18 -0
- package/src/prover-client/index.ts +2 -0
- package/src/prover-client/prover-client.ts +286 -0
- package/src/proving_broker/broker_prover_facade.ts +626 -0
- package/src/proving_broker/config.ts +178 -0
- package/src/proving_broker/factory.ts +19 -0
- package/src/proving_broker/fixtures.ts +20 -0
- package/src/proving_broker/index.ts +10 -0
- package/src/proving_broker/proof_store/factory.ts +20 -0
- package/src/proving_broker/proof_store/file_store_proof_store.ts +78 -0
- package/src/proving_broker/proof_store/index.ts +4 -0
- package/src/proving_broker/proof_store/inline_proof_store.ts +63 -0
- package/src/proving_broker/proof_store/proof_store.ts +54 -0
- package/src/proving_broker/proving_agent.ts +210 -0
- package/src/proving_broker/proving_broker.ts +888 -0
- package/src/proving_broker/proving_broker_database/memory.ts +78 -0
- package/src/proving_broker/proving_broker_database/persisted.ts +296 -0
- package/src/proving_broker/proving_broker_database.ts +75 -0
- package/src/proving_broker/proving_broker_instrumentation.ts +133 -0
- package/src/proving_broker/proving_job_controller.ts +199 -0
- package/src/proving_broker/rpc.ts +131 -0
- package/src/test/epoch_settlement.ts +82 -0
- package/src/test/index.ts +1 -0
- package/src/test/mock_proof_store.ts +14 -0
- package/src/test/mock_prover.ts +304 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { EpochNumber } from '@aztec-labs/foundation/branded-types';
|
|
2
|
+
import { createLogger } from '@aztec-labs/foundation/log';
|
|
3
|
+
import { RunningPromise, promiseWithResolvers } from '@aztec-labs/foundation/promise';
|
|
4
|
+
import { PriorityMemoryQueue } from '@aztec-labs/foundation/queue';
|
|
5
|
+
import { Timer } from '@aztec-labs/foundation/timer';
|
|
6
|
+
import { tryStop } from '@aztec-labs/stdlib/interfaces/server';
|
|
7
|
+
import { ProvingRequestType } from '@aztec-labs/stdlib/proofs';
|
|
8
|
+
import { getTelemetryClient } from '@aztec-labs/telemetry-client';
|
|
9
|
+
import assert from 'assert';
|
|
10
|
+
import { defaultProverBrokerConfig } from './config.js';
|
|
11
|
+
import { ProvingBrokerInstrumentation } from './proving_broker_instrumentation.js';
|
|
12
|
+
function toJobMetadata(job) {
|
|
13
|
+
return {
|
|
14
|
+
id: job.id,
|
|
15
|
+
type: job.type,
|
|
16
|
+
epochNumber: job.epochNumber
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function toSettledRecord(result) {
|
|
20
|
+
return result.status === 'fulfilled' ? {
|
|
21
|
+
status: 'fulfilled'
|
|
22
|
+
} : result;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A broker that manages proof requests and distributes them to workers based on their priority.
|
|
26
|
+
* It takes a backend that is responsible for storing and retrieving proof requests and results.
|
|
27
|
+
*/ export class ProvingBroker {
|
|
28
|
+
database;
|
|
29
|
+
logger;
|
|
30
|
+
queues;
|
|
31
|
+
// Scheduling metadata for every known job (id, type, epochNumber). The large `inputsUri` is NOT held
|
|
32
|
+
// here — it lives in the database and is read on demand when a job is dispatched to an agent.
|
|
33
|
+
jobsCache;
|
|
34
|
+
// Settled status for every settled job. The large fulfilled proof `value` is NOT held here — it lives
|
|
35
|
+
// in the database (read on demand in getProvingJobStatus) and, transiently, in `pendingResults` while
|
|
36
|
+
// its database write is in flight. Small payloads (rejected reason, aborted) are kept inline.
|
|
37
|
+
resultsCache;
|
|
38
|
+
// Transient hold of a fulfilled result's full value, from the moment it settles until its database
|
|
39
|
+
// write commits, so status reads are read-your-writes without retaining every proof. Evicted once the
|
|
40
|
+
// value is durable in the database; kept only if that write failed (preserving the result in memory).
|
|
41
|
+
pendingResults;
|
|
42
|
+
// tracks when each job was enqueued
|
|
43
|
+
enqueuedAt;
|
|
44
|
+
// keeps track of which jobs are currently being processed
|
|
45
|
+
// in the event of a crash this information is lost, but that's ok
|
|
46
|
+
// the next time the broker starts it will recreate jobsCache and still
|
|
47
|
+
// accept results from the workers
|
|
48
|
+
inProgress;
|
|
49
|
+
// keep track of which proving job has been retried
|
|
50
|
+
retries;
|
|
51
|
+
// a map of promises that are resolved when a job settles. Carries no payload (settled status/results
|
|
52
|
+
// are read from resultsCache/pendingResults/DB) so it never pins a proof value in memory.
|
|
53
|
+
promises;
|
|
54
|
+
cleanupPromise;
|
|
55
|
+
msTimeSource;
|
|
56
|
+
jobTimeoutMs;
|
|
57
|
+
maxRetries;
|
|
58
|
+
instrumentation;
|
|
59
|
+
tracer;
|
|
60
|
+
completedJobNotifications;
|
|
61
|
+
/**
|
|
62
|
+
* The broker keeps track of the highest epoch its seen.
|
|
63
|
+
* This information is used for garbage collection: once it reaches the next epoch, it can start pruning the database of old state.
|
|
64
|
+
* It is important that this value is initialized to zero. This ensures that we don't delete any old jobs until the current
|
|
65
|
+
* process instance receives a job request informing it of the actual current highest epoch
|
|
66
|
+
* Example:
|
|
67
|
+
* proving epoch 11 - the broker will wipe all jobs for epochs 9 and lower
|
|
68
|
+
* finished proving epoch 11 and got first job for epoch 12 -> the broker will wipe all settled jobs for epochs 10 and lower
|
|
69
|
+
* reorged back to end of epoch 10 -> epoch 11 is skipped and epoch 12 starts -> the broker will wipe all settled jobs for epochs 10 and lower
|
|
70
|
+
*/ epochHeight;
|
|
71
|
+
maxEpochsToKeepResultsFor;
|
|
72
|
+
started;
|
|
73
|
+
debugReplayEnabled;
|
|
74
|
+
constructor(database, { proverBrokerJobTimeoutMs, proverBrokerPollIntervalMs, proverBrokerJobMaxRetries, proverBrokerMaxEpochsToKeepResultsFor, proverBrokerDebugReplayEnabled } = defaultProverBrokerConfig, client = getTelemetryClient(), logger = createLogger('prover-client:proving-broker')){
|
|
75
|
+
this.database = database;
|
|
76
|
+
this.logger = logger;
|
|
77
|
+
this.queues = {
|
|
78
|
+
[ProvingRequestType.PUBLIC_VM]: new PriorityMemoryQueue(provingJobComparator),
|
|
79
|
+
[ProvingRequestType.PUBLIC_CHONK_VERIFIER]: new PriorityMemoryQueue(provingJobComparator),
|
|
80
|
+
[ProvingRequestType.PRIVATE_TX_BASE_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
81
|
+
[ProvingRequestType.PUBLIC_TX_BASE_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
82
|
+
[ProvingRequestType.TX_MERGE_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
83
|
+
[ProvingRequestType.ROOT_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
84
|
+
[ProvingRequestType.BLOCK_MERGE_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
85
|
+
[ProvingRequestType.BLOCK_ROOT_NO_TXS_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
86
|
+
[ProvingRequestType.BLOCK_ROOT_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
87
|
+
[ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
88
|
+
[ProvingRequestType.CHECKPOINT_ROOT_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
89
|
+
[ProvingRequestType.CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
90
|
+
[ProvingRequestType.CHECKPOINT_MERGE_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
91
|
+
[ProvingRequestType.CHECKPOINT_PADDING_ROLLUP]: new PriorityMemoryQueue(provingJobComparator),
|
|
92
|
+
[ProvingRequestType.INBOX_PARITY]: new PriorityMemoryQueue(provingJobComparator)
|
|
93
|
+
};
|
|
94
|
+
this.jobsCache = new Map();
|
|
95
|
+
this.resultsCache = new Map();
|
|
96
|
+
this.pendingResults = new Map();
|
|
97
|
+
this.enqueuedAt = new Map();
|
|
98
|
+
this.inProgress = new Map();
|
|
99
|
+
this.retries = new Map();
|
|
100
|
+
this.promises = new Map();
|
|
101
|
+
this.msTimeSource = ()=>Date.now();
|
|
102
|
+
this.completedJobNotifications = [];
|
|
103
|
+
this.epochHeight = 0;
|
|
104
|
+
this.maxEpochsToKeepResultsFor = 1;
|
|
105
|
+
this.started = false;
|
|
106
|
+
this.measureQueueDepth = (type)=>{
|
|
107
|
+
return this.queues[type].length();
|
|
108
|
+
};
|
|
109
|
+
this.countActiveJobs = (type)=>{
|
|
110
|
+
let count = 0;
|
|
111
|
+
for (const { id } of this.inProgress.values()){
|
|
112
|
+
const job = this.jobsCache.get(id);
|
|
113
|
+
if (job?.type === type) {
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return count;
|
|
118
|
+
};
|
|
119
|
+
this.tracer = client.getTracer('ProvingBroker');
|
|
120
|
+
this.instrumentation = new ProvingBrokerInstrumentation(client);
|
|
121
|
+
this.cleanupPromise = new RunningPromise(this.cleanupPass.bind(this), this.logger, proverBrokerPollIntervalMs);
|
|
122
|
+
this.jobTimeoutMs = proverBrokerJobTimeoutMs;
|
|
123
|
+
this.maxRetries = proverBrokerJobMaxRetries;
|
|
124
|
+
this.maxEpochsToKeepResultsFor = proverBrokerMaxEpochsToKeepResultsFor;
|
|
125
|
+
this.debugReplayEnabled = proverBrokerDebugReplayEnabled ?? false;
|
|
126
|
+
}
|
|
127
|
+
measureQueueDepth;
|
|
128
|
+
countActiveJobs;
|
|
129
|
+
async start() {
|
|
130
|
+
if (this.started) {
|
|
131
|
+
this.logger.info('Proving Broker already started');
|
|
132
|
+
return Promise.resolve();
|
|
133
|
+
}
|
|
134
|
+
this.logger.info('Proving Broker started');
|
|
135
|
+
for await (const [item, result] of this.database.allProvingJobs()){
|
|
136
|
+
this.logger.info(`Restoring proving job id=${item.id} settled=${!!result}`, {
|
|
137
|
+
provingJobId: item.id,
|
|
138
|
+
status: result ? result.status : 'pending'
|
|
139
|
+
});
|
|
140
|
+
this.jobsCache.set(item.id, toJobMetadata(item));
|
|
141
|
+
this.promises.set(item.id, promiseWithResolvers());
|
|
142
|
+
if (result) {
|
|
143
|
+
this.promises.get(item.id).resolve();
|
|
144
|
+
this.resultsCache.set(item.id, toSettledRecord(result));
|
|
145
|
+
} else {
|
|
146
|
+
this.enqueueJobInternal(item);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.cleanupPromise.start();
|
|
150
|
+
this.instrumentation.monitorQueueDepth(this.measureQueueDepth);
|
|
151
|
+
this.instrumentation.monitorActiveJobs(this.countActiveJobs);
|
|
152
|
+
this.started = true;
|
|
153
|
+
}
|
|
154
|
+
async stop() {
|
|
155
|
+
if (!this.started) {
|
|
156
|
+
this.logger.warn('ProvingBroker not started');
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
}
|
|
159
|
+
await tryStop(this.cleanupPromise);
|
|
160
|
+
}
|
|
161
|
+
enqueueProvingJob(job) {
|
|
162
|
+
return this.#enqueueProvingJob(job);
|
|
163
|
+
}
|
|
164
|
+
cancelProvingJob(id) {
|
|
165
|
+
return this.#cancelProvingJob(id);
|
|
166
|
+
}
|
|
167
|
+
getProvingJobStatus(id) {
|
|
168
|
+
return this.#getProvingJobStatus(id);
|
|
169
|
+
}
|
|
170
|
+
getCompletedJobs(ids) {
|
|
171
|
+
return this.#getCompletedJobs(ids);
|
|
172
|
+
}
|
|
173
|
+
getProvingJob(filter) {
|
|
174
|
+
return this.#getProvingJob(filter);
|
|
175
|
+
}
|
|
176
|
+
reportProvingJobSuccess(id, value, filter) {
|
|
177
|
+
return this.#reportProvingJobSuccess(id, value, filter);
|
|
178
|
+
}
|
|
179
|
+
reportProvingJobError(id, err, retry = false, filter) {
|
|
180
|
+
return this.#reportProvingJobError(id, err, retry, filter);
|
|
181
|
+
}
|
|
182
|
+
reportProvingJobProgress(id, startedAt, filter) {
|
|
183
|
+
return this.#reportProvingJobProgress(id, startedAt, filter);
|
|
184
|
+
}
|
|
185
|
+
async replayProvingJob(jobId, type, epochNumber, inputsUri) {
|
|
186
|
+
if (!this.debugReplayEnabled) {
|
|
187
|
+
throw new Error('Debug replay not enabled. Set PROVER_BROKER_DEBUG_REPLAY_ENABLED=true');
|
|
188
|
+
}
|
|
189
|
+
this.logger.info(`Replaying proving job`, {
|
|
190
|
+
provingJobId: jobId,
|
|
191
|
+
epochNumber,
|
|
192
|
+
inputsUri
|
|
193
|
+
});
|
|
194
|
+
// Clear existing state and enqueue
|
|
195
|
+
this.cleanUpProvingJobState([
|
|
196
|
+
jobId
|
|
197
|
+
]);
|
|
198
|
+
const job = {
|
|
199
|
+
id: jobId,
|
|
200
|
+
type,
|
|
201
|
+
epochNumber,
|
|
202
|
+
inputsUri
|
|
203
|
+
};
|
|
204
|
+
this.jobsCache.set(jobId, toJobMetadata(job));
|
|
205
|
+
await this.database.addProvingJob(job);
|
|
206
|
+
this.enqueueJobInternal(job);
|
|
207
|
+
return {
|
|
208
|
+
status: 'in-queue'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async #enqueueProvingJob(job) {
|
|
212
|
+
// The status returned to the caller reflects the job's state at the start of this call: `not-found`
|
|
213
|
+
// for a brand-new job (it did not exist yet), or the cached status for one already known. Crucially
|
|
214
|
+
// this must NOT `await` before the synchronous `jobsCache`/`resultsCache` gate below, or the
|
|
215
|
+
// enqueue/revive lock would no longer hold. So the not-found default is set synchronously here and
|
|
216
|
+
// the cached branch computes the real status (which may read the DB) only when it returns.
|
|
217
|
+
let jobStatus = {
|
|
218
|
+
status: 'not-found'
|
|
219
|
+
};
|
|
220
|
+
if (this.jobsCache.has(job.id)) {
|
|
221
|
+
const existing = this.jobsCache.get(job.id);
|
|
222
|
+
// Identity check is metadata-only: `inputsUri` now lives in the database, not memory, and job ids
|
|
223
|
+
// are content-addressed (same id ⇒ same inputs), so a mismatch shows up in id/type/epochNumber.
|
|
224
|
+
assert.deepStrictEqual(toJobMetadata(job), existing, 'Duplicate proving job ID');
|
|
225
|
+
if (this.resultsCache.get(job.id)?.status === 'aborted') {
|
|
226
|
+
// The producer is re-requesting a job it previously cancelled: revive it rather than
|
|
227
|
+
// returning the cached abort, clearing the aborted state in memory and in the database so the
|
|
228
|
+
// revival survives a restart.
|
|
229
|
+
//
|
|
230
|
+
// Concurrency model: `jobsCache` is the enqueue lock. Every path that puts a job on the queue
|
|
231
|
+
// populates `jobsCache` *synchronously, before its first await* (see the "New proving job"
|
|
232
|
+
// block below), so a second concurrent enqueue of the same id observes the entry at the top
|
|
233
|
+
// of this method and takes a cached, no-op branch instead of enqueuing a duplicate. The revive
|
|
234
|
+
// must keep holding that lock: we tear down the settled state and re-set `jobsCache` in a
|
|
235
|
+
// single synchronous span (no await in between), and only then await the database. Because a
|
|
236
|
+
// concurrent re-request can only interleave at that await — by which point `jobsCache` is
|
|
237
|
+
// populated again and the aborted result is gone — it falls into the cached branch and no-ops,
|
|
238
|
+
// so the job is enqueued exactly once. (`cleanUpProvingJobState` also drops the promise, so
|
|
239
|
+
// `enqueueJobInternal` below mints a fresh one for the retry.) This holds unchanged with the
|
|
240
|
+
// slimmer caches: `jobsCache` (now metadata) and `resultsCache` (now settled status) are still
|
|
241
|
+
// synchronous in-memory maps, so the tear-down + re-set span still contains no await.
|
|
242
|
+
this.logger.info(`Reviving aborted proving job id=${job.id} epochNumber=${job.epochNumber}`, {
|
|
243
|
+
provingJobId: job.id
|
|
244
|
+
});
|
|
245
|
+
this.cleanUpProvingJobState([
|
|
246
|
+
job.id
|
|
247
|
+
]);
|
|
248
|
+
this.jobsCache.set(job.id, toJobMetadata(job));
|
|
249
|
+
await this.database.deleteProvingJobResult(job.id);
|
|
250
|
+
// The job is re-set in the cache and about to be re-enqueued below: its start status is in-queue.
|
|
251
|
+
jobStatus = {
|
|
252
|
+
status: 'in-queue'
|
|
253
|
+
};
|
|
254
|
+
} else {
|
|
255
|
+
this.logger.warn(`Cached proving job id=${job.id} epochNumber=${job.epochNumber}. Not enqueuing again`, {
|
|
256
|
+
provingJobId: job.id
|
|
257
|
+
});
|
|
258
|
+
this.instrumentation.incCachedJobs(job.type);
|
|
259
|
+
// Return the job's current status. This reads the fulfilled proof value from the DB when needed;
|
|
260
|
+
// the await is fine here because this branch does not enqueue, so it is outside the lock span.
|
|
261
|
+
return await this.#getProvingJobStatus(job.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (this.isJobStale(job)) {
|
|
265
|
+
this.logger.warn(`Tried enqueueing stale proving job id=${job.id} epochNumber=${job.epochNumber}`, {
|
|
266
|
+
provingJobId: job.id
|
|
267
|
+
});
|
|
268
|
+
throw new Error(`Epoch too old: job epoch ${job.epochNumber}, current epoch: ${this.epochHeight}`);
|
|
269
|
+
}
|
|
270
|
+
this.logger.info(`New proving job id=${job.id} epochNumber=${job.epochNumber}`, {
|
|
271
|
+
provingJobId: job.id
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
// do this first so it acts as a "lock". If this job is enqueued again while we're saving it the if at the top will catch it.
|
|
275
|
+
this.jobsCache.set(job.id, toJobMetadata(job));
|
|
276
|
+
await this.database.addProvingJob(job);
|
|
277
|
+
this.enqueueJobInternal(job);
|
|
278
|
+
this.instrumentation.incTotalJobs(job.type);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
this.logger.error(`Failed to save proving job id=${job.id}: ${err}`, err, {
|
|
281
|
+
provingJobId: job.id
|
|
282
|
+
});
|
|
283
|
+
this.jobsCache.delete(job.id);
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
return jobStatus;
|
|
287
|
+
}
|
|
288
|
+
async #cancelProvingJob(id) {
|
|
289
|
+
const job = this.jobsCache.get(id);
|
|
290
|
+
if (!job) {
|
|
291
|
+
this.logger.warn(`Can't cancel a job that doesn't exist id=${id}`, {
|
|
292
|
+
provingJobId: id
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Leave jobs that have already settled (completed or failed) alone: those results are terminal.
|
|
297
|
+
if (this.resultsCache.has(id)) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.logger.info(`Cancelling job id=${id}`, {
|
|
301
|
+
provingJobId: id
|
|
302
|
+
});
|
|
303
|
+
this.inProgress.delete(id);
|
|
304
|
+
// Record the cancellation as its own settled state and persist it, so it survives a restart and
|
|
305
|
+
// notifies the current waiter. Unlike a completion or failure this is not terminal: re-enqueuing
|
|
306
|
+
// the same job id revives it (see #enqueueProvingJob), so the abort never permanently blocks the
|
|
307
|
+
// proof.
|
|
308
|
+
this.resultsCache.set(id, {
|
|
309
|
+
status: 'aborted'
|
|
310
|
+
});
|
|
311
|
+
this.promises.get(id)?.resolve();
|
|
312
|
+
this.completedJobNotifications.push(id);
|
|
313
|
+
this.instrumentation.incAbortedJobs(job.type);
|
|
314
|
+
try {
|
|
315
|
+
await this.database.setProvingJobAborted(id);
|
|
316
|
+
} catch (saveErr) {
|
|
317
|
+
this.logger.error(`Failed to save proving job aborted status id=${id}`, saveErr, {
|
|
318
|
+
provingJobId: id
|
|
319
|
+
});
|
|
320
|
+
throw saveErr;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
cleanUpProvingJobState(ids) {
|
|
324
|
+
const idsToClean = new Set(ids);
|
|
325
|
+
for (const id of ids){
|
|
326
|
+
this.jobsCache.delete(id);
|
|
327
|
+
const deferred = this.promises.get(id);
|
|
328
|
+
if (deferred) {
|
|
329
|
+
deferred.resolve();
|
|
330
|
+
}
|
|
331
|
+
this.promises.delete(id);
|
|
332
|
+
this.resultsCache.delete(id);
|
|
333
|
+
this.pendingResults.delete(id);
|
|
334
|
+
this.inProgress.delete(id);
|
|
335
|
+
this.retries.delete(id);
|
|
336
|
+
this.enqueuedAt.delete(id);
|
|
337
|
+
}
|
|
338
|
+
this.completedJobNotifications = this.completedJobNotifications.filter((id)=>!idsToClean.has(id));
|
|
339
|
+
}
|
|
340
|
+
async #getProvingJobStatus(id) {
|
|
341
|
+
const settled = this.resultsCache.get(id);
|
|
342
|
+
if (settled) {
|
|
343
|
+
if (settled.status !== 'fulfilled') {
|
|
344
|
+
// rejected/aborted carry their (small) payload inline.
|
|
345
|
+
return settled;
|
|
346
|
+
}
|
|
347
|
+
// A fulfilled job's proof value is not held in memory: serve it from the transient hold if its
|
|
348
|
+
// database write is still in flight, otherwise read it back from the database.
|
|
349
|
+
const full = this.pendingResults.get(id) ?? await this.database.getProvingJobResult(id);
|
|
350
|
+
if (full?.status === 'fulfilled') {
|
|
351
|
+
return full;
|
|
352
|
+
}
|
|
353
|
+
// Settled as fulfilled but the value is nowhere to be found — should not happen (it is held in
|
|
354
|
+
// pendingResults until its DB write commits). Report not-found rather than a torn result.
|
|
355
|
+
this.logger.error(`Fulfilled proving job id=${id} has no retrievable result value`, {
|
|
356
|
+
provingJobId: id
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
status: 'not-found'
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// no result yet, check if we know the item
|
|
363
|
+
const item = this.jobsCache.get(id);
|
|
364
|
+
if (!item) {
|
|
365
|
+
return {
|
|
366
|
+
status: 'not-found'
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
status: this.inProgress.has(id) ? 'in-progress' : 'in-queue'
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
#getCompletedJobs(ids) {
|
|
374
|
+
const completedJobs = ids.filter((id)=>this.resultsCache.has(id));
|
|
375
|
+
const notifications = this.completedJobNotifications;
|
|
376
|
+
this.completedJobNotifications = [];
|
|
377
|
+
return Promise.resolve(notifications.concat(completedJobs));
|
|
378
|
+
}
|
|
379
|
+
async #getProvingJob(filter = {
|
|
380
|
+
allowList: []
|
|
381
|
+
}) {
|
|
382
|
+
const allowedProofs = Array.isArray(filter.allowList) && filter.allowList.length > 0 ? [
|
|
383
|
+
...filter.allowList
|
|
384
|
+
] : Object.values(ProvingRequestType).filter((x)=>typeof x === 'number');
|
|
385
|
+
allowedProofs.sort(proofTypeComparator);
|
|
386
|
+
// Select the oldest-epoch job across the allowed queues, tie-breaking by proof-type priority: an epoch's
|
|
387
|
+
// remaining jobs always outrank younger epochs' work, so the oldest epoch completes instead of starving
|
|
388
|
+
// behind the continuous arrival of new higher-priority-type jobs. The legacy L1-to-L2 tree got this ordering
|
|
389
|
+
// for free (block roots waited on parity outputs); the streaming inbox parity only gates the checkpoint
|
|
390
|
+
// root, so a purely type-major order would leave it unscheduled under sustained block production.
|
|
391
|
+
//
|
|
392
|
+
// The outer loop re-selects when the winner turns out to have no inputs: the losing candidates went back
|
|
393
|
+
// onto their queues, while the claimed winner was popped and stays off, so it cannot be re-selected.
|
|
394
|
+
for(;;){
|
|
395
|
+
let selected;
|
|
396
|
+
for (const proofType of allowedProofs){
|
|
397
|
+
const queue = this.queues[proofType];
|
|
398
|
+
let enqueuedJob;
|
|
399
|
+
let candidate;
|
|
400
|
+
// exhaust the queue and make sure we're not sending a job that's already in progress
|
|
401
|
+
// or has already been completed
|
|
402
|
+
// this can happen if the broker crashes and restarts
|
|
403
|
+
// it's possible agents will report progress or results for jobs that are in the queue (after the restart)
|
|
404
|
+
while(enqueuedJob = queue.getImmediate()){
|
|
405
|
+
const meta = this.jobsCache.get(enqueuedJob.id);
|
|
406
|
+
if (meta && !this.inProgress.has(enqueuedJob.id) && !this.resultsCache.has(enqueuedJob.id)) {
|
|
407
|
+
candidate = {
|
|
408
|
+
enqueuedJob,
|
|
409
|
+
meta
|
|
410
|
+
};
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!candidate) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (selected === undefined || candidate.enqueuedJob.epochNumber < selected.enqueuedJob.epochNumber) {
|
|
418
|
+
if (selected) {
|
|
419
|
+
this.queues[selected.proofType].put(selected.enqueuedJob);
|
|
420
|
+
}
|
|
421
|
+
selected = {
|
|
422
|
+
proofType,
|
|
423
|
+
...candidate
|
|
424
|
+
};
|
|
425
|
+
} else {
|
|
426
|
+
queue.put(candidate.enqueuedJob);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!selected) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
const { meta } = selected;
|
|
433
|
+
const time = this.msTimeSource();
|
|
434
|
+
// Claim the job synchronously (before the await below) so a concurrent dispatch can't re-pick
|
|
435
|
+
// it. The same id can sit in a queue more than once (an abort leaves a stale entry that a
|
|
436
|
+
// later revive re-enqueues alongside), so this in-progress claim — not the queue pop — is what
|
|
437
|
+
// dedups those copies across concurrently-polling agents. It must be set before the await.
|
|
438
|
+
this.inProgress.set(meta.id, {
|
|
439
|
+
id: meta.id,
|
|
440
|
+
startedAt: time,
|
|
441
|
+
lastUpdatedAt: time
|
|
442
|
+
});
|
|
443
|
+
// The large inputs are not kept in memory; read them from the database. They were durably
|
|
444
|
+
// persisted (addProvingJob's write commits) before the job became dispatchable, so this hits.
|
|
445
|
+
const inputsUri = await this.database.getProvingJobInputs(meta.id);
|
|
446
|
+
if (!inputsUri) {
|
|
447
|
+
// The job was cleaned up (or its inputs lost) after we claimed it — release the claim and
|
|
448
|
+
// select again. Nothing else was mutated yet, so there is no wait metric or enqueued-at
|
|
449
|
+
// timer to unwind.
|
|
450
|
+
this.inProgress.delete(meta.id);
|
|
451
|
+
this.logger.warn(`No inputs found for proving job id=${meta.id}; skipping dispatch`, {
|
|
452
|
+
provingJobId: meta.id
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
// The dispatch is now committed: record the queue wait and clear the enqueued-at timer.
|
|
457
|
+
const enqueuedAt = this.enqueuedAt.get(meta.id);
|
|
458
|
+
if (enqueuedAt) {
|
|
459
|
+
this.instrumentation.recordJobWait(meta.type, enqueuedAt);
|
|
460
|
+
this.enqueuedAt.delete(meta.id);
|
|
461
|
+
}
|
|
462
|
+
const job = {
|
|
463
|
+
id: meta.id,
|
|
464
|
+
type: meta.type,
|
|
465
|
+
epochNumber: meta.epochNumber,
|
|
466
|
+
inputsUri
|
|
467
|
+
};
|
|
468
|
+
return {
|
|
469
|
+
job,
|
|
470
|
+
time
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async #reportProvingJobError(id, err, retry = false, filter) {
|
|
475
|
+
const info = this.inProgress.get(id);
|
|
476
|
+
const item = this.jobsCache.get(id);
|
|
477
|
+
const retries = this.retries.get(id) ?? 0;
|
|
478
|
+
if (!item) {
|
|
479
|
+
this.logger.warn(`Can't set error on unknown proving job id=${id} err=${err}`, {
|
|
480
|
+
provingJoId: id
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!info) {
|
|
485
|
+
this.logger.warn(`Proving job id=${id} type=${ProvingRequestType[item.type]} not in the in-progress set`, {
|
|
486
|
+
provingJobId: id
|
|
487
|
+
});
|
|
488
|
+
} else {
|
|
489
|
+
this.inProgress.delete(id);
|
|
490
|
+
}
|
|
491
|
+
if (this.resultsCache.has(id)) {
|
|
492
|
+
this.logger.warn(`Proving job id=${id} is already settled, ignoring err=${err}`, {
|
|
493
|
+
provingJobId: id
|
|
494
|
+
});
|
|
495
|
+
return this.#getProvingJob(filter);
|
|
496
|
+
}
|
|
497
|
+
if (retry && retries + 1 < this.maxRetries && !this.isJobStale(item)) {
|
|
498
|
+
this.logger.info(`Retrying proving job id=${id} type=${ProvingRequestType[item.type]} retry=${retries + 1} err=${err}`, {
|
|
499
|
+
provingJobId: id
|
|
500
|
+
});
|
|
501
|
+
// assign another job to this agent
|
|
502
|
+
// do this first, before we put the failed job back in the queue
|
|
503
|
+
const maybeAnotherJob = this.#getProvingJob(filter);
|
|
504
|
+
this.retries.set(id, retries + 1);
|
|
505
|
+
this.enqueueJobInternal(item);
|
|
506
|
+
this.instrumentation.incRetriedJobs(item.type);
|
|
507
|
+
return maybeAnotherJob;
|
|
508
|
+
}
|
|
509
|
+
this.logger.info(`Marking proving job as failed id=${id} type=${ProvingRequestType[item.type]} totalAttempts=${retries + 1} err=${err}`, {
|
|
510
|
+
provingJobId: id
|
|
511
|
+
});
|
|
512
|
+
// save the (small) rejection reason to the cache and notify clients of the job status. The reason
|
|
513
|
+
// is kept in memory, so status reads work even if the database write below fails.
|
|
514
|
+
this.resultsCache.set(id, {
|
|
515
|
+
status: 'rejected',
|
|
516
|
+
reason: String(err)
|
|
517
|
+
});
|
|
518
|
+
this.promises.get(id).resolve();
|
|
519
|
+
this.completedJobNotifications.push(id);
|
|
520
|
+
this.instrumentation.incRejectedJobs(item.type);
|
|
521
|
+
if (info) {
|
|
522
|
+
const duration = this.msTimeSource() - info.startedAt;
|
|
523
|
+
this.instrumentation.recordJobDuration(item.type, duration);
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
await this.database.setProvingJobError(id, err);
|
|
527
|
+
} catch (saveErr) {
|
|
528
|
+
this.logger.error(`Failed to save proving job error status id=${id} jobErr=${err}`, saveErr, {
|
|
529
|
+
provingJobId: id
|
|
530
|
+
});
|
|
531
|
+
throw saveErr;
|
|
532
|
+
}
|
|
533
|
+
return this.#getProvingJob(filter);
|
|
534
|
+
}
|
|
535
|
+
async #reportProvingJobProgress(id, startedAt, filter) {
|
|
536
|
+
const job = this.jobsCache.get(id);
|
|
537
|
+
if (!job) {
|
|
538
|
+
this.logger.warn(`Proving job id=${id} does not exist`, {
|
|
539
|
+
provingJobId: id
|
|
540
|
+
});
|
|
541
|
+
return await this.#getProvingJob(filter);
|
|
542
|
+
}
|
|
543
|
+
if (this.resultsCache.has(id)) {
|
|
544
|
+
this.logger.warn(`Proving job id=${id} has already been completed`, {
|
|
545
|
+
provingJobId: id
|
|
546
|
+
});
|
|
547
|
+
return await this.#getProvingJob(filter);
|
|
548
|
+
}
|
|
549
|
+
const metadata = this.inProgress.get(id);
|
|
550
|
+
const now = this.msTimeSource();
|
|
551
|
+
if (!metadata) {
|
|
552
|
+
this.logger.warn(`Proving job id=${id} type=${ProvingRequestType[job.type]} not found in the in-progress cache, adding it`, {
|
|
553
|
+
provingJobId: id
|
|
554
|
+
});
|
|
555
|
+
// the queue will still contain the item at this point!
|
|
556
|
+
// we need to be careful when popping off the queue to make sure we're not sending
|
|
557
|
+
// a job that's already in progress
|
|
558
|
+
this.inProgress.set(id, {
|
|
559
|
+
id,
|
|
560
|
+
startedAt,
|
|
561
|
+
lastUpdatedAt: this.msTimeSource()
|
|
562
|
+
});
|
|
563
|
+
return undefined;
|
|
564
|
+
} else if (startedAt <= metadata.startedAt) {
|
|
565
|
+
if (startedAt < metadata.startedAt) {
|
|
566
|
+
this.logger.info(`Proving job id=${id} type=${ProvingRequestType[job.type]} startedAt=${startedAt} older agent has taken job`, {
|
|
567
|
+
provingJobId: id
|
|
568
|
+
});
|
|
569
|
+
} else {
|
|
570
|
+
this.logger.debug(`Proving job id=${id} type=${ProvingRequestType[job.type]} heartbeat`, {
|
|
571
|
+
provingJobId: id
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
metadata.startedAt = startedAt;
|
|
575
|
+
metadata.lastUpdatedAt = now;
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
this.logger.warn(`Proving job id=${id} type=${ProvingRequestType[job.type]} already being worked on by another agent. Sending new one`, {
|
|
579
|
+
provingJobId: id
|
|
580
|
+
});
|
|
581
|
+
return await this.#getProvingJob(filter);
|
|
582
|
+
}
|
|
583
|
+
async #reportProvingJobSuccess(id, value, filter) {
|
|
584
|
+
const info = this.inProgress.get(id);
|
|
585
|
+
const item = this.jobsCache.get(id);
|
|
586
|
+
const retries = this.retries.get(id) ?? 0;
|
|
587
|
+
if (!item) {
|
|
588
|
+
this.logger.warn(`Proving job id=${id} not found`, {
|
|
589
|
+
provingJobId: id
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!info) {
|
|
594
|
+
this.logger.warn(`Proving job id=${id} type=${ProvingRequestType[item.type]} not in the in-progress set`, {
|
|
595
|
+
provingJobId: id
|
|
596
|
+
});
|
|
597
|
+
} else {
|
|
598
|
+
this.inProgress.delete(id);
|
|
599
|
+
}
|
|
600
|
+
if (this.resultsCache.has(id)) {
|
|
601
|
+
this.logger.warn(`Proving job id=${id} already settled, ignoring result`, {
|
|
602
|
+
provingJobId: id
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
this.logger.info(`Proving job complete id=${id} type=${ProvingRequestType[item.type]} totalAttempts=${retries + 1}`, {
|
|
607
|
+
provingJobId: id
|
|
608
|
+
});
|
|
609
|
+
// Mark settled synchronously (the gate that makes concurrent settles no-op, unchanged from before),
|
|
610
|
+
// holding only the status in resultsCache. The large proof value is kept transiently in
|
|
611
|
+
// pendingResults so status reads are read-your-writes until the database write below commits.
|
|
612
|
+
this.resultsCache.set(id, {
|
|
613
|
+
status: 'fulfilled'
|
|
614
|
+
});
|
|
615
|
+
this.pendingResults.set(id, {
|
|
616
|
+
status: 'fulfilled',
|
|
617
|
+
value
|
|
618
|
+
});
|
|
619
|
+
this.promises.get(id).resolve();
|
|
620
|
+
this.completedJobNotifications.push(id);
|
|
621
|
+
this.instrumentation.incResolvedJobs(item.type);
|
|
622
|
+
if (info) {
|
|
623
|
+
const duration = this.msTimeSource() - info.startedAt;
|
|
624
|
+
this.instrumentation.recordJobDuration(item.type, duration);
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await this.database.setProvingJobResult(id, value);
|
|
628
|
+
// The value is durable in the database now; drop the transient in-memory copy.
|
|
629
|
+
this.pendingResults.delete(id);
|
|
630
|
+
} catch (saveErr) {
|
|
631
|
+
// Keep the value in pendingResults so status/result reads still succeed despite the failed write —
|
|
632
|
+
// this preserves the previous "works even if the database breaks" behaviour for this edge.
|
|
633
|
+
this.logger.error(`Failed to save proving job result id=${id}`, saveErr, {
|
|
634
|
+
provingJobId: id
|
|
635
|
+
});
|
|
636
|
+
throw saveErr;
|
|
637
|
+
}
|
|
638
|
+
return this.#getProvingJob(filter);
|
|
639
|
+
}
|
|
640
|
+
async cleanupPass() {
|
|
641
|
+
this.reEnqueueExpiredJobs();
|
|
642
|
+
const oldestEpochToKeep = this.oldestEpochToKeep();
|
|
643
|
+
if (oldestEpochToKeep > 0) {
|
|
644
|
+
this.cleanupJobsOlderThanEpoch(EpochNumber(oldestEpochToKeep));
|
|
645
|
+
await this.database.deleteAllProvingJobsOlderThanEpoch(EpochNumber(oldestEpochToKeep));
|
|
646
|
+
this.logger.trace(`Deleted all epochs older than ${oldestEpochToKeep}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
cleanupJobsOlderThanEpoch(epochNumber) {
|
|
650
|
+
const jobIds = Array.from(this.jobsCache.keys());
|
|
651
|
+
const jobsToClean = [];
|
|
652
|
+
for (const id of jobIds){
|
|
653
|
+
const job = this.jobsCache.get(id);
|
|
654
|
+
if (job.epochNumber < epochNumber) {
|
|
655
|
+
jobsToClean.push(id);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (jobsToClean.length > 0) {
|
|
659
|
+
this.cleanUpProvingJobState(jobsToClean);
|
|
660
|
+
this.logger.verbose(`Cleaned up proving jobs=${jobsToClean.length}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
reEnqueueExpiredJobs() {
|
|
664
|
+
const inProgressEntries = Array.from(this.inProgress.entries());
|
|
665
|
+
for (const [id, metadata] of inProgressEntries){
|
|
666
|
+
const item = this.jobsCache.get(id);
|
|
667
|
+
if (!item) {
|
|
668
|
+
this.logger.warn(`Proving job id=${id} not found. Removing it from the queue.`, {
|
|
669
|
+
provingJobId: id
|
|
670
|
+
});
|
|
671
|
+
this.inProgress.delete(id);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const now = this.msTimeSource();
|
|
675
|
+
const msSinceLastUpdate = now - metadata.lastUpdatedAt;
|
|
676
|
+
if (msSinceLastUpdate >= this.jobTimeoutMs) {
|
|
677
|
+
this.inProgress.delete(id);
|
|
678
|
+
this.instrumentation.incTimedOutJobs(item.type);
|
|
679
|
+
const retries = this.retries.get(id) ?? 0;
|
|
680
|
+
if (retries + 1 < this.maxRetries && !this.isJobStale(item)) {
|
|
681
|
+
this.logger.warn(`Proving job id=${id} timed out. Re-enqueueing (retry ${retries + 1}/${this.maxRetries}).`, {
|
|
682
|
+
provingJobId: id
|
|
683
|
+
});
|
|
684
|
+
this.retries.set(id, retries + 1);
|
|
685
|
+
this.enqueueJobInternal(item);
|
|
686
|
+
} else {
|
|
687
|
+
this.logger.error(`Proving job id=${id} timed out after ${retries + 1} attempts. Marking as failed.`, {
|
|
688
|
+
provingJobId: id
|
|
689
|
+
});
|
|
690
|
+
this.resultsCache.set(id, {
|
|
691
|
+
status: 'rejected',
|
|
692
|
+
reason: 'Timed out'
|
|
693
|
+
});
|
|
694
|
+
this.promises.get(id)?.resolve();
|
|
695
|
+
this.completedJobNotifications.push(id);
|
|
696
|
+
this.instrumentation.incRejectedJobs(item.type);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
enqueueJobInternal(job) {
|
|
702
|
+
if (!this.promises.has(job.id)) {
|
|
703
|
+
this.promises.set(job.id, promiseWithResolvers());
|
|
704
|
+
}
|
|
705
|
+
this.queues[job.type].put({
|
|
706
|
+
epochNumber: job.epochNumber,
|
|
707
|
+
id: job.id
|
|
708
|
+
});
|
|
709
|
+
this.enqueuedAt.set(job.id, new Timer());
|
|
710
|
+
this.epochHeight = Math.max(this.epochHeight, job.epochNumber);
|
|
711
|
+
}
|
|
712
|
+
isJobStale(job) {
|
|
713
|
+
return job.epochNumber < this.oldestEpochToKeep();
|
|
714
|
+
}
|
|
715
|
+
oldestEpochToKeep() {
|
|
716
|
+
return this.epochHeight - this.maxEpochsToKeepResultsFor;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Compares two proving jobs and selects which one's more important
|
|
721
|
+
* @param a - A proving job
|
|
722
|
+
* @param b - Another proving job
|
|
723
|
+
* @returns A number indicating the relative priority of the two proving jobs
|
|
724
|
+
*/ function provingJobComparator(a, b) {
|
|
725
|
+
if (a.epochNumber < b.epochNumber) {
|
|
726
|
+
return -1;
|
|
727
|
+
} else if (a.epochNumber > b.epochNumber) {
|
|
728
|
+
return 1;
|
|
729
|
+
} else {
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Compares two proofs and selects which one's more important.
|
|
735
|
+
* If some proofs does not exist in the priority array then it's considered the least important.
|
|
736
|
+
*
|
|
737
|
+
* @param a - A proof type
|
|
738
|
+
* @param b - Another proof type
|
|
739
|
+
* @returns A number indicating the relative priority of the two proof types
|
|
740
|
+
*/ function proofTypeComparator(a, b) {
|
|
741
|
+
const indexOfA = PROOF_TYPES_IN_PRIORITY_ORDER.indexOf(a);
|
|
742
|
+
const indexOfB = PROOF_TYPES_IN_PRIORITY_ORDER.indexOf(b);
|
|
743
|
+
if (indexOfA === indexOfB) {
|
|
744
|
+
return 0;
|
|
745
|
+
} else if (indexOfA === -1) {
|
|
746
|
+
// a is some new proof that didn't get added to the array
|
|
747
|
+
// b is more important because we know about it
|
|
748
|
+
return 1;
|
|
749
|
+
} else if (indexOfB === -1) {
|
|
750
|
+
// the opposite of the previous if branch
|
|
751
|
+
return -1;
|
|
752
|
+
} else if (indexOfA < indexOfB) {
|
|
753
|
+
return -1;
|
|
754
|
+
} else {
|
|
755
|
+
return 1;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Relative priority of each proof type. Proofs higher up on the list are more important and should be prioritized
|
|
760
|
+
* over proofs lower on the list.
|
|
761
|
+
*
|
|
762
|
+
* The aim is that this will speed up block proving as the closer we get to a block's root proof the more likely it
|
|
763
|
+
* is to get picked up by agents
|
|
764
|
+
*/ export const PROOF_TYPES_IN_PRIORITY_ORDER = [
|
|
765
|
+
ProvingRequestType.ROOT_ROLLUP,
|
|
766
|
+
ProvingRequestType.BLOCK_ROOT_ROLLUP,
|
|
767
|
+
ProvingRequestType.BLOCK_ROOT_SINGLE_TX_ROLLUP,
|
|
768
|
+
ProvingRequestType.BLOCK_ROOT_NO_TXS_ROLLUP,
|
|
769
|
+
ProvingRequestType.BLOCK_MERGE_ROLLUP,
|
|
770
|
+
ProvingRequestType.CHECKPOINT_ROOT_ROLLUP,
|
|
771
|
+
ProvingRequestType.CHECKPOINT_ROOT_SINGLE_BLOCK_ROLLUP,
|
|
772
|
+
ProvingRequestType.CHECKPOINT_MERGE_ROLLUP,
|
|
773
|
+
ProvingRequestType.CHECKPOINT_PADDING_ROLLUP,
|
|
774
|
+
ProvingRequestType.TX_MERGE_ROLLUP,
|
|
775
|
+
ProvingRequestType.PUBLIC_TX_BASE_ROLLUP,
|
|
776
|
+
ProvingRequestType.PRIVATE_TX_BASE_ROLLUP,
|
|
777
|
+
ProvingRequestType.PUBLIC_VM,
|
|
778
|
+
ProvingRequestType.PUBLIC_CHONK_VERIFIER,
|
|
779
|
+
ProvingRequestType.INBOX_PARITY
|
|
780
|
+
];
|