@aztec/prover-node 5.0.0-private.20260319 → 5.0.0-rc.2
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 +518 -0
- package/dest/actions/download-epoch-proving-job.js +1 -1
- package/dest/actions/rerun-epoch-proving-job.d.ts +4 -3
- package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
- package/dest/actions/rerun-epoch-proving-job.js +103 -21
- package/dest/bin/run-failed-epoch.js +1 -3
- package/dest/checkpoint-store.d.ts +87 -0
- package/dest/checkpoint-store.d.ts.map +1 -0
- package/dest/checkpoint-store.js +186 -0
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +1 -1
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +22 -8
- package/dest/index.d.ts +2 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -0
- package/dest/job/checkpoint-prover.d.ts +134 -0
- package/dest/job/checkpoint-prover.d.ts.map +1 -0
- package/dest/job/checkpoint-prover.js +353 -0
- package/dest/job/epoch-session.d.ts +146 -0
- package/dest/job/epoch-session.d.ts.map +1 -0
- package/dest/job/epoch-session.js +720 -0
- package/dest/job/top-tree-job.d.ts +82 -0
- package/dest/job/top-tree-job.d.ts.map +1 -0
- package/dest/job/top-tree-job.js +152 -0
- package/dest/metrics.d.ts +35 -8
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +86 -14
- package/dest/monitors/epoch-monitor.js +6 -2
- package/dest/proof-publishing-service.d.ts +161 -0
- package/dest/proof-publishing-service.d.ts.map +1 -0
- package/dest/proof-publishing-service.js +335 -0
- package/dest/prover-node-publisher.d.ts +22 -15
- package/dest/prover-node-publisher.d.ts.map +1 -1
- package/dest/prover-node-publisher.js +197 -60
- package/dest/prover-node.d.ts +105 -67
- package/dest/prover-node.d.ts.map +1 -1
- package/dest/prover-node.js +482 -224
- package/dest/prover-publisher-factory.d.ts +2 -2
- package/dest/prover-publisher-factory.d.ts.map +1 -1
- package/dest/prover-publisher-factory.js +3 -3
- package/dest/session-manager.d.ts +158 -0
- package/dest/session-manager.d.ts.map +1 -0
- package/dest/session-manager.js +452 -0
- package/dest/test/index.d.ts +7 -6
- package/dest/test/index.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/actions/download-epoch-proving-job.ts +1 -1
- package/src/actions/rerun-epoch-proving-job.ts +114 -28
- package/src/bin/run-failed-epoch.ts +1 -2
- package/src/checkpoint-store.ts +218 -0
- package/src/config.ts +2 -1
- package/src/factory.ts +18 -10
- package/src/index.ts +1 -0
- package/src/job/checkpoint-prover.ts +468 -0
- package/src/job/epoch-session.ts +436 -0
- package/src/job/top-tree-job.ts +227 -0
- package/src/metrics.ts +102 -23
- package/src/monitors/epoch-monitor.ts +2 -2
- package/src/proof-publishing-service.ts +427 -0
- package/src/prover-node-publisher.ts +231 -77
- package/src/prover-node.ts +552 -251
- package/src/prover-publisher-factory.ts +3 -3
- package/src/session-manager.ts +552 -0
- package/src/test/index.ts +6 -6
- package/dest/job/epoch-proving-job.d.ts +0 -63
- package/dest/job/epoch-proving-job.d.ts.map +0 -1
- package/dest/job/epoch-proving-job.js +0 -762
- package/src/job/epoch-proving-job.ts +0 -465
package/src/prover-node.ts
CHANGED
|
@@ -2,78 +2,123 @@ import type { Archiver } from '@aztec/archiver';
|
|
|
2
2
|
import type { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
3
|
import type { Delayer } from '@aztec/ethereum/l1-tx-utils';
|
|
4
4
|
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
5
|
-
import { assertRequired, compact, pick
|
|
6
|
-
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
|
+
import { assertRequired, compact, pick } from '@aztec/foundation/collection';
|
|
7
6
|
import { memoize } from '@aztec/foundation/decorators';
|
|
8
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
9
|
-
import {
|
|
8
|
+
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
9
|
+
import { DateProvider, executeTimeout } from '@aztec/foundation/timer';
|
|
10
|
+
import type { EpochProverFactory } from '@aztec/prover-client';
|
|
11
|
+
import { getLastSiblingPath } from '@aztec/prover-client/helpers';
|
|
12
|
+
import { ChonkCache } from '@aztec/prover-client/orchestrator';
|
|
10
13
|
import { PublicProcessorFactory } from '@aztec/simulator/server';
|
|
11
|
-
import
|
|
12
|
-
|
|
14
|
+
import {
|
|
15
|
+
type L2BlockId,
|
|
16
|
+
type L2BlockSource,
|
|
17
|
+
L2BlockStream,
|
|
18
|
+
type L2BlockStreamEvent,
|
|
19
|
+
type L2BlockStreamEventHandler,
|
|
20
|
+
L2TipsMemoryStore,
|
|
21
|
+
} from '@aztec/stdlib/block';
|
|
22
|
+
import type { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
13
23
|
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
14
24
|
import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
15
|
-
import {
|
|
25
|
+
import { type L1RollupConstants, getEpochAtSlot, getProofSubmissionDeadlineEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
16
26
|
import {
|
|
17
27
|
type EpochProverManager,
|
|
28
|
+
type EpochProvingJobState,
|
|
18
29
|
EpochProvingJobTerminalState,
|
|
19
30
|
type ITxProvider,
|
|
20
31
|
type ProverNodeApi,
|
|
21
32
|
type Service,
|
|
22
|
-
type WorldStateSyncStatus,
|
|
23
33
|
type WorldStateSynchronizer,
|
|
24
34
|
tryStop,
|
|
25
35
|
} from '@aztec/stdlib/interfaces/server';
|
|
26
36
|
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
27
37
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
28
|
-
import
|
|
38
|
+
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
29
39
|
import {
|
|
30
|
-
Attributes,
|
|
31
40
|
L1Metrics,
|
|
32
41
|
type TelemetryClient,
|
|
33
42
|
type Traceable,
|
|
34
43
|
type Tracer,
|
|
35
44
|
getTelemetryClient,
|
|
36
|
-
trackSpan,
|
|
37
45
|
} from '@aztec/telemetry-client';
|
|
38
46
|
|
|
39
47
|
import { uploadEpochProofFailure } from './actions/upload-epoch-proof-failure.js';
|
|
48
|
+
import { CheckpointStore, type RegisterCheckpointData } from './checkpoint-store.js';
|
|
40
49
|
import type { SpecificProverNodeConfig } from './config.js';
|
|
41
|
-
import type {
|
|
42
|
-
import { EpochProvingJob, type EpochProvingJobState } from './job/epoch-proving-job.js';
|
|
50
|
+
import type { EpochSession, EpochSessionHooks } from './job/epoch-session.js';
|
|
43
51
|
import { ProverNodeJobMetrics, ProverNodeRewardsMetrics } from './metrics.js';
|
|
44
|
-
import
|
|
45
|
-
import type { ProverNodePublisher } from './prover-node-publisher.js';
|
|
52
|
+
import { ProofPublishingService } from './proof-publishing-service.js';
|
|
46
53
|
import type { ProverPublisherFactory } from './prover-publisher-factory.js';
|
|
54
|
+
import { SessionManager } from './session-manager.js';
|
|
47
55
|
|
|
48
56
|
type ProverNodeOptions = SpecificProverNodeConfig & Partial<DataStoreOptions>;
|
|
49
57
|
type DataStoreOptions = Pick<DataStoreConfig, 'dataDirectory'> & Pick<ChainConfig, 'l1ChainId' | 'rollupVersion'>;
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
60
|
+
* Grace period for the proof-publishing service to settle during shutdown. The service waits for
|
|
61
|
+
* any in-flight L1 proof-submission tx to finish; that tx can take a long time to mine, so we cap
|
|
62
|
+
* the wait rather than letting `stop()` hang indefinitely.
|
|
55
63
|
*/
|
|
56
|
-
|
|
64
|
+
const PUBLISHING_SERVICE_STOP_TIMEOUT_MS = 30_000;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An Aztec Prover Node is a standalone process that monitors the chain for new checkpoints,
|
|
68
|
+
* starts proving them optimistically as they arrive, and submits epoch proofs to L1 once
|
|
69
|
+
* complete.
|
|
70
|
+
*
|
|
71
|
+
* The class is intentionally thin: it owns the long-lived collections (`CheckpointStore`,
|
|
72
|
+
* `ChonkCache`, `SessionManager`), the L2BlockStream, and a periodic ticker that nudges the
|
|
73
|
+
* manager to pick up newly-complete epochs. Every session lifecycle decision is delegated to
|
|
74
|
+
* the `SessionManager`. Each chain event is translated here into a single method call on it.
|
|
75
|
+
*/
|
|
76
|
+
export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Traceable {
|
|
57
77
|
private log = createLogger('prover-node');
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
protected readonly checkpointStore: CheckpointStore;
|
|
80
|
+
protected readonly chonkCache: ChonkCache;
|
|
81
|
+
protected sessionManager: SessionManager | undefined;
|
|
82
|
+
|
|
83
|
+
private readonly config: ProverNodeOptions;
|
|
84
|
+
private readonly jobMetrics: ProverNodeJobMetrics;
|
|
85
|
+
private readonly rewardsMetrics: ProverNodeRewardsMetrics;
|
|
86
|
+
|
|
87
|
+
/** In-memory store for the L2BlockStream's local data provider. */
|
|
88
|
+
private tipsStore: L2TipsMemoryStore;
|
|
89
|
+
/** Block stream for checkpoint and reorg detection. */
|
|
90
|
+
private blockStream: L2BlockStream | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Highest epoch whose proof-submission window has passed. Monotonic high-water mark.
|
|
93
|
+
* Seeded from the last fully-proven epoch at start(); advanced on every block-stream
|
|
94
|
+
* event by comparing the archiver's latest synced L2 slot against each epoch's
|
|
95
|
+
* submission deadline. Protected so tests can verify the start() seeding.
|
|
96
|
+
*/
|
|
97
|
+
protected lastExpiredEpoch: EpochNumber | undefined;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Highest checkpoint number whose proving-side handling has completed (or that was legitimately skipped).
|
|
101
|
+
* The catch-up loop walks from here to each `chain-checkpointed` tip event. Seeded at start() from the last
|
|
102
|
+
* checkpoint of the last fully-proven epoch (or 0), so a restart reprocesses the partially-proven epoch rather
|
|
103
|
+
* than trusting a checkpointed tip that may sit ahead of unproven checkpoints. Clamped down on a prune.
|
|
104
|
+
*/
|
|
105
|
+
protected lastProcessedCheckpoint: CheckpointNumber = CheckpointNumber.ZERO;
|
|
106
|
+
|
|
107
|
+
/** Periodic tick that runs the epoch-expiry sweep during idle periods when no block-stream events arrive. */
|
|
108
|
+
private expiryTicker: RunningPromise | undefined;
|
|
63
109
|
|
|
64
110
|
public readonly tracer: Tracer;
|
|
65
111
|
|
|
66
|
-
protected
|
|
112
|
+
protected publishingService: ProofPublishingService | undefined;
|
|
67
113
|
|
|
68
114
|
constructor(
|
|
69
|
-
protected readonly prover: EpochProverManager,
|
|
115
|
+
protected readonly prover: EpochProverManager & EpochProverFactory,
|
|
70
116
|
protected readonly publisherFactory: ProverPublisherFactory,
|
|
71
117
|
protected readonly l2BlockSource: L2BlockSource & Partial<Service>,
|
|
72
118
|
protected readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
73
119
|
protected readonly contractDataSource: ContractDataSource,
|
|
74
120
|
protected readonly worldState: WorldStateSynchronizer,
|
|
75
121
|
protected readonly p2pClient: { getTxProvider(): ITxProvider } & Partial<Service>,
|
|
76
|
-
protected readonly epochsMonitor: EpochMonitor,
|
|
77
122
|
protected readonly rollupContract: RollupContract,
|
|
78
123
|
protected readonly l1Metrics: L1Metrics,
|
|
79
124
|
config: Partial<ProverNodeOptions> = {},
|
|
@@ -100,8 +145,33 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
|
|
|
100
145
|
this.tracer = telemetryClient.getTracer('ProverNode');
|
|
101
146
|
|
|
102
147
|
this.jobMetrics = new ProverNodeJobMetrics(meter, telemetryClient.getTracer('EpochProvingJob'));
|
|
103
|
-
|
|
104
148
|
this.rewardsMetrics = new ProverNodeRewardsMetrics(meter, this.prover.getProverId(), rollupContract);
|
|
149
|
+
|
|
150
|
+
this.tipsStore = new L2TipsMemoryStore(this.l2BlockSource.getGenesisBlockHash());
|
|
151
|
+
|
|
152
|
+
this.chonkCache = new ChonkCache(this.log.getBindings());
|
|
153
|
+
this.checkpointStore = new CheckpointStore(
|
|
154
|
+
this.l2BlockSource,
|
|
155
|
+
{
|
|
156
|
+
proverFactory: this.prover,
|
|
157
|
+
chonkCache: this.chonkCache,
|
|
158
|
+
publicProcessorFactory: new PublicProcessorFactory(
|
|
159
|
+
this.contractDataSource,
|
|
160
|
+
this.dateProvider,
|
|
161
|
+
this.telemetryClient,
|
|
162
|
+
this.log.getBindings(),
|
|
163
|
+
),
|
|
164
|
+
dbProvider: this.worldState,
|
|
165
|
+
txProvider: this.p2pClient.getTxProvider(),
|
|
166
|
+
dateProvider: this.dateProvider,
|
|
167
|
+
proverId: this.prover.getProverId(),
|
|
168
|
+
metrics: this.jobMetrics,
|
|
169
|
+
txGatheringTimeoutMs: this.config.txGatheringTimeoutMs,
|
|
170
|
+
deadline: undefined,
|
|
171
|
+
},
|
|
172
|
+
{ slotWatcherPollIntervalMs: this.config.proverNodePollingIntervalMs },
|
|
173
|
+
this.log.getBindings(),
|
|
174
|
+
);
|
|
105
175
|
}
|
|
106
176
|
|
|
107
177
|
public getProverId() {
|
|
@@ -112,292 +182,527 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
|
|
|
112
182
|
return this.p2pClient;
|
|
113
183
|
}
|
|
114
184
|
|
|
115
|
-
/**
|
|
185
|
+
/** Test-only: the shared L1 tx delayer, if enabled. */
|
|
116
186
|
public getDelayer(): Delayer | undefined {
|
|
117
187
|
return this.delayer;
|
|
118
188
|
}
|
|
119
189
|
|
|
190
|
+
/** Observability summary for the ProverNodeApi. */
|
|
191
|
+
public getJobs(): Promise<{ uuid: string; status: EpochProvingJobState; epochNumber: EpochNumber }[]> {
|
|
192
|
+
return Promise.resolve(this.sessionManager?.getJobs() ?? []);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Tests inspect this when validating reconcile behaviour. */
|
|
196
|
+
public getCheckpointStore(): CheckpointStore {
|
|
197
|
+
return this.checkpointStore;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Tests inspect this to verify chonk-cache release semantics. */
|
|
201
|
+
public getChonkCache(): ChonkCache {
|
|
202
|
+
return this.chonkCache;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Tests inspect this when looking up live sessions. */
|
|
206
|
+
public getSessionManager(): SessionManager {
|
|
207
|
+
if (!this.sessionManager) {
|
|
208
|
+
throw new Error('SessionManager not yet constructed — start() must be called first.');
|
|
209
|
+
}
|
|
210
|
+
return this.sessionManager;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Returns the underlying prover instance. */
|
|
214
|
+
public getProver() {
|
|
215
|
+
return this.prover;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------- L2BlockStream handler ----------------
|
|
219
|
+
|
|
220
|
+
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
221
|
+
switch (event.type) {
|
|
222
|
+
case 'chain-checkpointed':
|
|
223
|
+
await this.processCheckpointJump(event.checkpoint.number);
|
|
224
|
+
break;
|
|
225
|
+
case 'chain-pruned':
|
|
226
|
+
await this.handlePruneEvent(event.block);
|
|
227
|
+
break;
|
|
228
|
+
case 'chain-proven':
|
|
229
|
+
this.publishingService?.onChainProven(BlockNumber(event.block.number));
|
|
230
|
+
break;
|
|
231
|
+
// The proposed tip drives only the tips store's walk-back history (recorded below); the prover-node
|
|
232
|
+
// tracks checkpoints, not proposed blocks. `blocks-added` is never emitted in tips-only mode, and
|
|
233
|
+
// `chain-finalized` carries nothing the prover-node acts on.
|
|
234
|
+
case 'chain-proposed':
|
|
235
|
+
case 'chain-finalized':
|
|
236
|
+
case 'blocks-added':
|
|
237
|
+
break;
|
|
238
|
+
default: {
|
|
239
|
+
const _: never = event;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Expiry is driven by the archiver's latest synced L2 slot
|
|
244
|
+
await this.checkEpochExpiry();
|
|
245
|
+
// Advance the local tips store only after the proving-side handling has succeeded. Any
|
|
246
|
+
// failure above propagates to the L2BlockStream (which logs and stops this poll pass) and
|
|
247
|
+
// skips this update, so the event is re-emitted on the next poll rather than skipped (A-1041).
|
|
248
|
+
await this.tipsStore.handleBlockStreamEvent(event);
|
|
249
|
+
}
|
|
250
|
+
|
|
120
251
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
252
|
+
* Walks every checkpoint between the local cursor and the newly-reported checkpointed tip, registering
|
|
253
|
+
* each one that belongs to an epoch that can still be proven. The block stream now delivers a single thin
|
|
254
|
+
* `chain-checkpointed` tip event per pass rather than one fat event per checkpoint, so this drives the
|
|
255
|
+
* catch-up itself: light metadata first (`getCheckpointsData`) to decide relevance per epoch, then a heavy
|
|
256
|
+
* `getCheckpoints` fetch only for checkpoints in provable epochs.
|
|
257
|
+
*
|
|
258
|
+
* The cursor advances one checkpoint at a time and only after that checkpoint's proving-side handling has
|
|
259
|
+
* fully succeeded, preserving the A-1041 at-least-once semantics: a mid-jump failure leaves the cursor
|
|
260
|
+
* behind so the next pass retries from the first checkpoint that did not complete.
|
|
124
261
|
*/
|
|
125
|
-
async
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
262
|
+
private async processCheckpointJump(targetCheckpoint: CheckpointNumber): Promise<void> {
|
|
263
|
+
if (targetCheckpoint <= this.lastProcessedCheckpoint) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const l1Constants = await this.getL1Constants();
|
|
267
|
+
|
|
268
|
+
// Cap the catch-up at the `(proofSubmissionEpochs + 1) * epochDuration` most recent checkpoints.
|
|
269
|
+
// When the cursor is much further behind (e.g. resyncing after a long time offline), fetching the whole gap could
|
|
270
|
+
// load thousands of checkpoints we cannot act on: anything older than the last two epochs is already past
|
|
271
|
+
// its proof-submission window, so we skip it and jump the cursor forward to the start of the capped range.
|
|
272
|
+
const maxCheckpoints = (l1Constants.proofSubmissionEpochs + 1) * l1Constants.epochDuration;
|
|
273
|
+
let from = CheckpointNumber(this.lastProcessedCheckpoint + 1);
|
|
274
|
+
if (Number(targetCheckpoint - from) + 1 > maxCheckpoints) {
|
|
275
|
+
const cappedFrom = CheckpointNumber(targetCheckpoint - maxCheckpoints + 1);
|
|
276
|
+
this.log.warn(`Skipping unprovable checkpoints during catch-up; the prover node is far behind`, {
|
|
277
|
+
from,
|
|
278
|
+
cappedFrom,
|
|
279
|
+
targetCheckpoint,
|
|
280
|
+
maxCheckpoints,
|
|
129
281
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
282
|
+
// Advance the cursor past the skipped checkpoints so they are never retried.
|
|
283
|
+
this.lastProcessedCheckpoint = CheckpointNumber(cappedFrom - 1);
|
|
284
|
+
from = cappedFrom;
|
|
285
|
+
}
|
|
286
|
+
const limit = Number(targetCheckpoint - from) + 1;
|
|
287
|
+
const metadatas = await this.l2BlockSource.getCheckpointsData({ from, limit });
|
|
288
|
+
|
|
289
|
+
// Per-epoch relevance is cached so a multi-checkpoint epoch resolves it once. Skipping is whole-epoch
|
|
290
|
+
// only: the SessionManager requires an epoch's checkpoints fully covered before it opens a session, so we
|
|
291
|
+
// never drop an individual checkpoint inside an epoch we will prove.
|
|
292
|
+
const epochSkippable = new Map<EpochNumber, boolean>();
|
|
293
|
+
for (const metadata of metadatas) {
|
|
294
|
+
const epochNumber = getEpochAtSlot(metadata.header.slotNumber, l1Constants);
|
|
295
|
+
let skippable = epochSkippable.get(epochNumber);
|
|
296
|
+
if (skippable === undefined) {
|
|
297
|
+
skippable =
|
|
298
|
+
(await this.isEpochFullyProven(epochNumber, l1Constants)) ||
|
|
299
|
+
(await this.isEpochPastProofSubmissionWindow(epochNumber, l1Constants));
|
|
300
|
+
epochSkippable.set(epochNumber, skippable);
|
|
136
301
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
} catch (err) {
|
|
140
|
-
if (err instanceof EmptyEpochError) {
|
|
141
|
-
this.log.info(`Not starting proof for ${epochNumber} since no blocks were found`);
|
|
302
|
+
if (skippable) {
|
|
303
|
+
this.log.debug(`Skipping checkpoint ${metadata.checkpointNumber} for unprovable epoch ${epochNumber}`);
|
|
142
304
|
} else {
|
|
143
|
-
this.
|
|
305
|
+
await this.registerCheckpoint(metadata.checkpointNumber, epochNumber);
|
|
144
306
|
}
|
|
145
|
-
|
|
307
|
+
// Advance only after the checkpoint's handling succeeded (or it was legitimately skipped). registerCheckpoint
|
|
308
|
+
// throws on failure, which leaves the cursor here for the next pass to retry (A-1041).
|
|
309
|
+
this.lastProcessedCheckpoint = metadata.checkpointNumber;
|
|
146
310
|
}
|
|
147
311
|
}
|
|
148
312
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
313
|
+
/** Heavy-fetch a single checkpoint, register it with the store, and notify the session manager. */
|
|
314
|
+
private async registerCheckpoint(checkpointNumber: CheckpointNumber, epochNumber: EpochNumber): Promise<void> {
|
|
315
|
+
const published = await this.l2BlockSource.getCheckpoint({ number: checkpointNumber });
|
|
316
|
+
if (!published) {
|
|
317
|
+
throw new Error(`Checkpoint ${checkpointNumber} not found in block source during catch-up`);
|
|
318
|
+
}
|
|
319
|
+
const checkpoint = published.checkpoint;
|
|
320
|
+
this.log.info(`New checkpoint ${checkpoint.number} for epoch ${epochNumber}`, {
|
|
321
|
+
checkpointNumber: checkpoint.number,
|
|
322
|
+
epochNumber,
|
|
323
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const registerData = await this.collectRegisterData(checkpoint, published.attestations);
|
|
327
|
+
await this.checkpointStore.addOrUpdate(checkpoint, registerData);
|
|
328
|
+
await this.sessionManager?.onCheckpointAdded(epochNumber);
|
|
329
|
+
|
|
330
|
+
// Tips-only mode delivers no blocks, so record one witness per checkpointed block: a reorg into the checkpoint's
|
|
331
|
+
// range then prunes at the true divergence instead of the nearest sparse tip anchor.
|
|
332
|
+
await this.tipsStore.recordBlockHashes(
|
|
333
|
+
await Promise.all(
|
|
334
|
+
checkpoint.blocks.map(async block => ({ number: block.number, hash: (await block.header.hash()).toString() })),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
160
337
|
}
|
|
161
338
|
|
|
162
339
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
340
|
+
* Gathers register-time data for a checkpoint: previous block header, L1-to-L2 messages,
|
|
341
|
+
* and the archive sibling path.
|
|
165
342
|
*/
|
|
166
|
-
async
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
await this.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
public getL2Tips() {
|
|
187
|
-
return this.l2BlockSource.getL2Tips();
|
|
343
|
+
private async collectRegisterData(
|
|
344
|
+
checkpoint: Checkpoint,
|
|
345
|
+
attestations: PublishedCheckpoint['attestations'],
|
|
346
|
+
): Promise<RegisterCheckpointData> {
|
|
347
|
+
const previousBlockNumber = BlockNumber(checkpoint.blocks[0].number - 1);
|
|
348
|
+
const previousBlockHeader = await this.gatherPreviousBlockHeader(previousBlockNumber);
|
|
349
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpoint.number);
|
|
350
|
+
const lastBlock = checkpoint.blocks.at(-1)!;
|
|
351
|
+
const lastBlockHash = await lastBlock.header.hash();
|
|
352
|
+
await this.worldState.syncImmediate(lastBlock.number, lastBlockHash);
|
|
353
|
+
const previousArchiveSiblingPath = await getLastSiblingPath(
|
|
354
|
+
MerkleTreeId.ARCHIVE,
|
|
355
|
+
this.worldState.getSnapshot(previousBlockNumber),
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
attestations,
|
|
359
|
+
previousBlockHeader,
|
|
360
|
+
l1ToL2Messages,
|
|
361
|
+
previousArchiveSiblingPath,
|
|
362
|
+
};
|
|
188
363
|
}
|
|
189
364
|
|
|
190
365
|
/**
|
|
191
|
-
*
|
|
366
|
+
* Marks every prover orphaned by the prune as pruned, clamps the catch-up cursor below the prune target's
|
|
367
|
+
* checkpoint, and notifies the session manager. Keyed off the prune target block (the highest surviving block)
|
|
368
|
+
* rather than the source's checkpointed tip, which can sit above the target after a re-checkpoint and would leave
|
|
369
|
+
* orphaned provers canonical. Throws (rather than warning) if the cursor floor cannot be resolved, so the pass
|
|
370
|
+
* fails and the prune is retried next iteration.
|
|
192
371
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
372
|
+
private async handlePruneEvent(prunedToBlock: L2BlockId) {
|
|
373
|
+
this.log.warn(`Chain pruned to block ${prunedToBlock.number}`, { prunedToBlock });
|
|
374
|
+
|
|
375
|
+
// Resolve the cursor floor BEFORE marking provers: markPrunedAboveBlock returns only newly-marked provers, so a
|
|
376
|
+
// throw after marking would leave a retry pass with nothing to act on. Resolving first means a throw leaves
|
|
377
|
+
// everything untouched and the next pass retries the whole handler (the tips cursor only advances on success).
|
|
378
|
+
let cursorFloor: CheckpointNumber;
|
|
379
|
+
if (prunedToBlock.number === 0) {
|
|
380
|
+
cursorFloor = CheckpointNumber.ZERO;
|
|
381
|
+
} else {
|
|
382
|
+
const targetData = await this.l2BlockSource.getBlockData({ number: prunedToBlock.number });
|
|
383
|
+
if (targetData === undefined) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`No block data found for prune target block ${prunedToBlock.number}; cannot clamp checkpoint cursor`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
// Clamp to `cpAtTarget - 1`: a mid-checkpoint target leaves that checkpoint partially orphaned and it must be
|
|
389
|
+
// reprocessed. Over-clamping merely re-registers a checkpoint (at-least-once by design — A-1041); under-clamping
|
|
390
|
+
// would permanently skip a rebuilt same-number checkpoint.
|
|
391
|
+
cursorFloor = CheckpointNumber(Math.max(0, Number(targetData.checkpointNumber) - 1));
|
|
392
|
+
}
|
|
197
393
|
|
|
198
|
-
|
|
199
|
-
const epochNumber = job.getEpochNumber();
|
|
200
|
-
const ctx = { id: job.getId(), epochNumber, state: undefined as EpochProvingJobState | undefined };
|
|
394
|
+
const affected = this.checkpointStore.markPrunedAboveBlock(prunedToBlock.number);
|
|
201
395
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const state = job.getState();
|
|
205
|
-
ctx.state = state;
|
|
206
|
-
|
|
207
|
-
if (state === 'reorg') {
|
|
208
|
-
this.log.warn(`Running new job for epoch ${epochNumber} due to reorg`, ctx);
|
|
209
|
-
await this.createProvingJob(epochNumber);
|
|
210
|
-
} else if (state === 'failed') {
|
|
211
|
-
this.log.error(`Job for ${epochNumber} exited with state ${state}`, ctx);
|
|
212
|
-
await this.tryUploadEpochFailure(job);
|
|
213
|
-
} else {
|
|
214
|
-
this.log.verbose(`Job for ${epochNumber} exited with state ${state}`, ctx);
|
|
215
|
-
}
|
|
216
|
-
} catch (err) {
|
|
217
|
-
this.log.error(`Error proving epoch ${epochNumber}`, err, ctx);
|
|
218
|
-
} finally {
|
|
219
|
-
this.jobs.delete(job.getId());
|
|
396
|
+
if (this.lastProcessedCheckpoint > cursorFloor) {
|
|
397
|
+
this.lastProcessedCheckpoint = cursorFloor;
|
|
220
398
|
}
|
|
221
|
-
}
|
|
222
399
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return await uploadEpochProofFailure(
|
|
226
|
-
this.config.proverNodeFailedEpochStore,
|
|
227
|
-
job.getId(),
|
|
228
|
-
job.getProvingData(),
|
|
229
|
-
this.l2BlockSource as Archiver,
|
|
230
|
-
this.worldState,
|
|
231
|
-
assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')),
|
|
232
|
-
this.log,
|
|
233
|
-
);
|
|
400
|
+
if (affected.length === 0) {
|
|
401
|
+
return;
|
|
234
402
|
}
|
|
403
|
+
const l1Constants = await this.getL1Constants();
|
|
404
|
+
const affectedEpochs = Array.from(
|
|
405
|
+
new Set(affected.map(p => Number(getEpochAtSlot(p.slotNumber, l1Constants)))),
|
|
406
|
+
).map(n => EpochNumber(n));
|
|
407
|
+
// The session manager cancels every affected session, which in turn calls
|
|
408
|
+
// publishingService.withdraw(uuid) for each candidate; no separate notification to the
|
|
409
|
+
// publishing service is needed.
|
|
410
|
+
await this.sessionManager?.onPrune(affectedEpochs);
|
|
235
411
|
}
|
|
236
412
|
|
|
237
413
|
/**
|
|
238
|
-
* Returns the
|
|
414
|
+
* Returns true once the chain has advanced past the given epoch's proof-submission window.
|
|
415
|
+
* Used to ignore checkpoints whose epoch can no longer be proven in time — chiefly while the
|
|
416
|
+
* archiver replays old blocks after a restart. Compares the archiver's latest synced L2 slot
|
|
417
|
+
* against the epoch's submission-deadline epoch; conservatively returns false if the slot can't
|
|
418
|
+
* be read yet.
|
|
239
419
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
420
|
+
private async isEpochPastProofSubmissionWindow(
|
|
421
|
+
epochNumber: EpochNumber,
|
|
422
|
+
l1Constants: L1RollupConstants,
|
|
423
|
+
): Promise<boolean> {
|
|
424
|
+
const latestSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
425
|
+
if (latestSlot === undefined) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const latestEpoch = getEpochAtSlot(latestSlot, l1Constants);
|
|
429
|
+
return latestEpoch >= getProofSubmissionDeadlineEpoch(epochNumber, l1Constants);
|
|
242
430
|
}
|
|
243
431
|
|
|
244
432
|
/**
|
|
245
|
-
*
|
|
433
|
+
* Compares the archiver's latest synced L2 slot against `lastExpiredEpoch` and, for each
|
|
434
|
+
* newly-expired epoch, releases the chonk-cache entries for its blocks and reaps any
|
|
435
|
+
* CheckpointProvers in the store. An epoch E is expired once the chain reaches the start
|
|
436
|
+
* of epoch `E + proofSubmissionEpochs + 1`. Silently no-ops if nothing has expired since
|
|
437
|
+
* the last check or the archiver's slot can't be read.
|
|
246
438
|
*/
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
439
|
+
private async checkEpochExpiry(): Promise<void> {
|
|
440
|
+
const latestSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
441
|
+
if (latestSlot === undefined) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const l1Constants = await this.getL1Constants();
|
|
445
|
+
const latestEpoch = getEpochAtSlot(latestSlot, l1Constants);
|
|
446
|
+
const offset = l1Constants.proofSubmissionEpochs + 1;
|
|
447
|
+
if (latestEpoch < offset) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const newlyExpiredUpTo = EpochNumber(latestEpoch - offset);
|
|
451
|
+
const from = this.lastExpiredEpoch === undefined ? EpochNumber(0) : EpochNumber(this.lastExpiredEpoch + 1);
|
|
452
|
+
if (newlyExpiredUpTo < from) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
for (let e = from; e <= newlyExpiredUpTo; e = EpochNumber(e + 1)) {
|
|
456
|
+
await this.expireEpoch(e);
|
|
457
|
+
}
|
|
458
|
+
this.lastExpiredEpoch = newlyExpiredUpTo;
|
|
255
459
|
}
|
|
256
460
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Releases chonk-cache entries for every block in the supplied epoch (best-effort) and
|
|
463
|
+
* reaps every CheckpointProver in the store whose epoch number matches.
|
|
464
|
+
*/
|
|
465
|
+
private async expireEpoch(epoch: EpochNumber): Promise<void> {
|
|
466
|
+
try {
|
|
467
|
+
const blocks = await this.l2BlockSource.getBlocks({ epoch, onlyCheckpointed: true });
|
|
468
|
+
if (blocks.length > 0) {
|
|
469
|
+
this.chonkCache.releaseForBlocks(blocks);
|
|
470
|
+
}
|
|
471
|
+
} catch (err) {
|
|
472
|
+
this.log.warn(`Could not release chonk-cache entries for expired epoch ${epoch}`, err);
|
|
473
|
+
}
|
|
474
|
+
this.checkpointStore.reapExpired(epoch);
|
|
262
475
|
}
|
|
263
476
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
477
|
+
// ---------------- public API ----------------
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Schedules proving for the given epoch and returns the job id without waiting for completion.
|
|
481
|
+
*/
|
|
482
|
+
public async startProof(epochNumber: EpochNumber): Promise<string> {
|
|
483
|
+
if (!this.sessionManager) {
|
|
484
|
+
throw new Error('ProverNode not started');
|
|
268
485
|
}
|
|
486
|
+
return await this.sessionManager.startProof(epochNumber);
|
|
269
487
|
}
|
|
270
488
|
|
|
271
|
-
|
|
272
|
-
private async createProvingJob(epochNumber: EpochNumber, opts: { skipEpochCheck?: boolean } = {}) {
|
|
273
|
-
this.checkMaximumPendingJobs();
|
|
274
|
-
|
|
275
|
-
this.publisher = await this.publisherFactory.create();
|
|
276
|
-
|
|
277
|
-
// Gather all data for this epoch
|
|
278
|
-
const epochData = await this.gatherEpochData(epochNumber);
|
|
279
|
-
const fromCheckpoint = epochData.checkpoints[0].number;
|
|
280
|
-
const toCheckpoint = epochData.checkpoints.at(-1)!.number;
|
|
281
|
-
const fromBlock = epochData.checkpoints[0].blocks[0].number;
|
|
282
|
-
const lastBlock = epochData.checkpoints.at(-1)!.blocks.at(-1)!;
|
|
283
|
-
const toBlock = lastBlock.number;
|
|
284
|
-
this.log.verbose(
|
|
285
|
-
`Creating proving job for epoch ${epochNumber} for checkpoint range ${fromCheckpoint} to ${toCheckpoint} and block range ${fromBlock} to ${toBlock}`,
|
|
286
|
-
);
|
|
489
|
+
// ---------------- Service lifecycle ----------------
|
|
287
490
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
await this.worldState.syncImmediate(toBlock, lastBlockHash);
|
|
491
|
+
async start() {
|
|
492
|
+
await this.checkpointStore.start();
|
|
291
493
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
this.
|
|
295
|
-
this.
|
|
296
|
-
this.
|
|
297
|
-
this.
|
|
494
|
+
await this.publisherFactory.start();
|
|
495
|
+
this.publishingService = new ProofPublishingService({
|
|
496
|
+
publisherFactory: this.publisherFactory,
|
|
497
|
+
l2BlockSource: this.l2BlockSource,
|
|
498
|
+
dateProvider: this.dateProvider,
|
|
499
|
+
config: { skipSubmitProof: !!this.config.proverNodeDisableProofPublish },
|
|
500
|
+
bindings: this.log.getBindings(),
|
|
501
|
+
});
|
|
502
|
+
this.sessionManager = this.createSessionManager(this.publishingService);
|
|
503
|
+
// SessionManager owns its own periodic tick; start it here so it begins picking up
|
|
504
|
+
// epochs that become complete by time (no fresh checkpoint event) and advances once
|
|
505
|
+
// the previous epoch is proven on L1.
|
|
506
|
+
this.sessionManager.start();
|
|
507
|
+
// Now that the store + manager exist, arm the live-state observable gauges.
|
|
508
|
+
this.jobMetrics.observeState(this.checkpointStore, this.sessionManager);
|
|
509
|
+
|
|
510
|
+
const { lastFullyProvenEpoch } = await this.resolveLastFullyProvenEpoch();
|
|
511
|
+
this.lastExpiredEpoch = lastFullyProvenEpoch;
|
|
512
|
+
this.lastProcessedCheckpoint = await this.computeStartingCheckpoint(lastFullyProvenEpoch);
|
|
513
|
+
this.blockStream = new L2BlockStream(this.l2BlockSource, this.tipsStore, this, this.log, {
|
|
514
|
+
pollIntervalMS: this.config.proverNodePollingIntervalMs,
|
|
515
|
+
tipsOnly: true,
|
|
516
|
+
});
|
|
517
|
+
this.blockStream.start();
|
|
518
|
+
|
|
519
|
+
// With thin once-per-pass tip events, the expiry sweep no longer fires once per checkpoint; drive it
|
|
520
|
+
// from a periodic tick so epochs still expire during idle/no-event periods.
|
|
521
|
+
this.expiryTicker = new RunningPromise(
|
|
522
|
+
() => this.checkEpochExpiry(),
|
|
523
|
+
this.log,
|
|
524
|
+
this.config.proverNodePollingIntervalMs,
|
|
298
525
|
);
|
|
526
|
+
this.expiryTicker.start();
|
|
299
527
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const job = this.doCreateEpochProvingJob(epochData, deadline, publicProcessorFactory, this.publisher, opts);
|
|
304
|
-
this.jobs.set(job.getId(), job);
|
|
305
|
-
return job;
|
|
528
|
+
await this.rewardsMetrics.start();
|
|
529
|
+
this.l1Metrics.start();
|
|
530
|
+
this.log.info(`Started Prover Node with prover id ${this.prover.getProverId().toString()}`, this.config);
|
|
306
531
|
}
|
|
307
532
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
533
|
+
async stop() {
|
|
534
|
+
this.log.info('Stopping ProverNode');
|
|
535
|
+
this.jobMetrics.stopObservingState();
|
|
536
|
+
await this.blockStream?.stop();
|
|
537
|
+
await this.expiryTicker?.stop();
|
|
538
|
+
if (this.sessionManager) {
|
|
539
|
+
await this.sessionManager.stop();
|
|
540
|
+
}
|
|
541
|
+
if (this.publishingService) {
|
|
542
|
+
// Bound the wait: the publishing service blocks until any in-flight L1 proof-submission tx
|
|
543
|
+
// settles, which can outlast a reasonable shutdown window. On timeout we log and move on —
|
|
544
|
+
// the tx may still mine, but shutdown must not hang on it.
|
|
545
|
+
const publishingService = this.publishingService;
|
|
546
|
+
await executeTimeout(
|
|
547
|
+
() => publishingService.stop(),
|
|
548
|
+
PUBLISHING_SERVICE_STOP_TIMEOUT_MS,
|
|
549
|
+
'prover-node publishing-service stop',
|
|
550
|
+
).catch(err => this.log.warn(`Timed out stopping proof publishing service`, err));
|
|
551
|
+
}
|
|
552
|
+
await this.checkpointStore.stop();
|
|
553
|
+
this.chonkCache.stop();
|
|
554
|
+
await this.prover.stop();
|
|
555
|
+
await tryStop(this.publisherFactory);
|
|
556
|
+
this.rewardsMetrics.stop();
|
|
557
|
+
this.l1Metrics.stop();
|
|
558
|
+
await this.telemetryClient.stop();
|
|
559
|
+
this.log.info('Stopped ProverNode');
|
|
311
560
|
}
|
|
312
561
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Constructs the session manager. Extracted so subclasses (test harness) can swap
|
|
564
|
+
* the implementation. Wired to `tryUploadSessionFailure` so failed sessions get
|
|
565
|
+
* their proving data uploaded.
|
|
566
|
+
*/
|
|
567
|
+
protected createSessionManager(publishingService: ProofPublishingService): SessionManager {
|
|
568
|
+
return new SessionManager({
|
|
569
|
+
checkpointStore: this.checkpointStore,
|
|
570
|
+
l2BlockSource: this.l2BlockSource,
|
|
571
|
+
proverFactory: this.prover,
|
|
572
|
+
proverId: this.prover.getProverId(),
|
|
573
|
+
publishingService,
|
|
574
|
+
metrics: this.jobMetrics,
|
|
575
|
+
dateProvider: this.dateProvider,
|
|
576
|
+
config: {
|
|
577
|
+
maxPendingJobs: this.config.proverNodeMaxPendingJobs,
|
|
578
|
+
tickIntervalMs: this.config.proverNodePollingIntervalMs,
|
|
579
|
+
finalizationDelayMs: this.config.proverNodeEpochProvingDelayMs,
|
|
580
|
+
},
|
|
581
|
+
onSessionFailed: async session => {
|
|
582
|
+
await this.tryUploadSessionFailure(session);
|
|
583
|
+
},
|
|
584
|
+
bindings: this.log.getBindings(),
|
|
585
|
+
});
|
|
325
586
|
}
|
|
326
587
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Installs session hooks for the e2e harness to interpose around top-tree proving
|
|
590
|
+
* (gate, override, or observe it) without monkey-patching the orchestrator factory.
|
|
591
|
+
* Applies to every session constructed after this call.
|
|
592
|
+
*/
|
|
593
|
+
public setSessionHooks(hooks: EpochSessionHooks): void {
|
|
594
|
+
if (!this.sessionManager) {
|
|
595
|
+
throw new Error('ProverNode not started; call start() before setting session hooks.');
|
|
331
596
|
}
|
|
332
|
-
|
|
597
|
+
this.sessionManager.setSessionHooks(hooks);
|
|
333
598
|
}
|
|
334
599
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const txsByBlock = await Promise.all(blocks.map(block => txProvider.getTxsForBlock(block, { deadline })));
|
|
340
|
-
const txs = txsByBlock.map(({ txs }) => txs).flat();
|
|
341
|
-
const missingTxs = txsByBlock.map(({ missingTxs }) => missingTxs).flat();
|
|
342
|
-
|
|
343
|
-
if (missingTxs.length === 0) {
|
|
344
|
-
this.log.verbose(`Gathered all ${txs.length} txs for epoch ${epochNumber}`, { epochNumber });
|
|
345
|
-
return txs;
|
|
600
|
+
/** Uploads failure snapshots when sessions exit with `failed`. Exposed as a method so tests can spy on it. */
|
|
601
|
+
public async tryUploadSessionFailure(session: EpochSession): Promise<string | undefined> {
|
|
602
|
+
if (!this.config.proverNodeFailedEpochStore) {
|
|
603
|
+
return undefined;
|
|
346
604
|
}
|
|
605
|
+
const data = SessionManager.buildSessionProvingData(session);
|
|
606
|
+
return await uploadEpochProofFailure(
|
|
607
|
+
this.config.proverNodeFailedEpochStore,
|
|
608
|
+
session.getId(),
|
|
609
|
+
data,
|
|
610
|
+
this.l2BlockSource as Archiver,
|
|
611
|
+
this.worldState,
|
|
612
|
+
assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')),
|
|
613
|
+
this.log,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ---------------- helpers ----------------
|
|
347
618
|
|
|
348
|
-
|
|
619
|
+
@memoize
|
|
620
|
+
private getL1Constants(): Promise<L1RollupConstants> {
|
|
621
|
+
return this.l2BlockSource.getL1Constants();
|
|
349
622
|
}
|
|
350
623
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Returns true if every block in the given epoch is proven on L1. An epoch is only
|
|
626
|
+
* fully proven when its *last* block is proven. Protected for direct unit-test access.
|
|
627
|
+
*/
|
|
628
|
+
protected async isEpochFullyProven(
|
|
629
|
+
epochNumber: EpochNumber,
|
|
630
|
+
l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
631
|
+
): Promise<boolean> {
|
|
632
|
+
const provenBlockNumber = await this.l2BlockSource.getBlockNumber({ tag: 'proven' });
|
|
633
|
+
if (!provenBlockNumber || provenBlockNumber <= 0) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
const provenHeader = (await this.l2BlockSource.getBlockData({ number: BlockNumber(provenBlockNumber) }))?.header;
|
|
637
|
+
if (!provenHeader) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
const provenEpoch = getEpochAtSlot(provenHeader.getSlot(), l1Constants);
|
|
641
|
+
if (epochNumber < provenEpoch) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
if (epochNumber > provenEpoch) {
|
|
645
|
+
return false;
|
|
358
646
|
}
|
|
359
|
-
return
|
|
647
|
+
return this.isProvenBlockLastOfItsEpoch(BlockNumber(provenBlockNumber), provenEpoch, l1Constants);
|
|
360
648
|
}
|
|
361
649
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
650
|
+
/** Protected for direct unit-test access. */
|
|
651
|
+
protected async isProvenBlockLastOfItsEpoch(
|
|
652
|
+
provenBlockNumber: BlockNumber,
|
|
653
|
+
provenEpoch: EpochNumber,
|
|
654
|
+
l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
655
|
+
): Promise<boolean> {
|
|
656
|
+
const nextHeader = (await this.l2BlockSource.getBlockData({ number: BlockNumber(provenBlockNumber + 1) }))?.header;
|
|
657
|
+
if (nextHeader) {
|
|
658
|
+
return getEpochAtSlot(nextHeader.getSlot(), l1Constants) > provenEpoch;
|
|
369
659
|
}
|
|
660
|
+
return this.l2BlockSource.isEpochComplete(provenEpoch);
|
|
661
|
+
}
|
|
370
662
|
|
|
371
|
-
|
|
372
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Resolves the last fully-proven epoch from L1 proven state, used to seed the catch-up cursor (via
|
|
665
|
+
* `computeStartingCheckpoint`) and `lastExpiredEpoch`. The fully-proven epoch is `provenEpoch` when the
|
|
666
|
+
* proven tip is the last block of its epoch, otherwise `provenEpoch - 1`, or `undefined` if no block is
|
|
667
|
+
* proven yet (so a restart reprocesses the partially-proven epoch rather than trusting a stale tip).
|
|
668
|
+
*/
|
|
669
|
+
protected async resolveLastFullyProvenEpoch(): Promise<{ lastFullyProvenEpoch: EpochNumber | undefined }> {
|
|
670
|
+
const provenBlockNumber = await this.l2BlockSource.getBlockNumber({ tag: 'proven' });
|
|
671
|
+
if (!provenBlockNumber || provenBlockNumber <= 0) {
|
|
672
|
+
return { lastFullyProvenEpoch: undefined };
|
|
673
|
+
}
|
|
674
|
+
const l1Constants = await this.getL1Constants();
|
|
675
|
+
const provenHeader = (await this.l2BlockSource.getBlockData({ number: BlockNumber(provenBlockNumber) }))?.header;
|
|
676
|
+
if (!provenHeader) {
|
|
677
|
+
return { lastFullyProvenEpoch: undefined };
|
|
678
|
+
}
|
|
679
|
+
const provenEpoch = getEpochAtSlot(provenHeader.getSlot(), l1Constants);
|
|
680
|
+
if (await this.isProvenBlockLastOfItsEpoch(BlockNumber(provenBlockNumber), provenEpoch, l1Constants)) {
|
|
681
|
+
return { lastFullyProvenEpoch: provenEpoch };
|
|
682
|
+
}
|
|
683
|
+
const lastFullyProvenEpoch = provenEpoch > 0 ? EpochNumber(provenEpoch - 1) : undefined;
|
|
684
|
+
return { lastFullyProvenEpoch };
|
|
373
685
|
}
|
|
374
686
|
|
|
375
|
-
/**
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
this.worldState,
|
|
387
|
-
this.prover.createEpochProver(),
|
|
388
|
-
publicProcessorFactory,
|
|
389
|
-
publisher,
|
|
390
|
-
this.l2BlockSource,
|
|
391
|
-
this.jobMetrics,
|
|
392
|
-
deadline,
|
|
393
|
-
{ parallelBlockLimit, skipSubmitProof: proverNodeDisableProofPublish, ...opts },
|
|
394
|
-
this.log.getBindings(),
|
|
395
|
-
);
|
|
687
|
+
/**
|
|
688
|
+
* Resolves the catch-up cursor seed: the last checkpoint of the last fully-proven epoch, or 0 if none. Seeding
|
|
689
|
+
* from a checkpoint (rather than a checkpointed tip) guarantees a restart reprocesses every checkpoint of the
|
|
690
|
+
* partially-proven epoch, since the checkpointed tip can sit ahead of the last fully-proven checkpoint.
|
|
691
|
+
*/
|
|
692
|
+
protected async computeStartingCheckpoint(lastFullyProvenEpoch: EpochNumber | undefined): Promise<CheckpointNumber> {
|
|
693
|
+
if (lastFullyProvenEpoch === undefined) {
|
|
694
|
+
return CheckpointNumber.ZERO;
|
|
695
|
+
}
|
|
696
|
+
const checkpoints = await this.l2BlockSource.getCheckpointsData({ epoch: lastFullyProvenEpoch });
|
|
697
|
+
return checkpoints.at(-1)?.checkpointNumber ?? CheckpointNumber.ZERO;
|
|
396
698
|
}
|
|
397
699
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
700
|
+
private async gatherPreviousBlockHeader(previousBlockNumber: number) {
|
|
701
|
+
const data = await this.l2BlockSource.getBlockData({ number: BlockNumber(previousBlockNumber) });
|
|
702
|
+
if (!data?.header) {
|
|
703
|
+
throw new Error(`Previous block header ${previousBlockNumber} not found`);
|
|
704
|
+
}
|
|
705
|
+
return data.header;
|
|
401
706
|
}
|
|
402
707
|
|
|
403
708
|
private validateConfig() {
|
|
@@ -416,9 +721,5 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
|
|
|
416
721
|
}
|
|
417
722
|
}
|
|
418
723
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
super(`No blocks found for epoch ${epochNumber}`);
|
|
422
|
-
this.name = 'EmptyEpochError';
|
|
423
|
-
}
|
|
424
|
-
}
|
|
724
|
+
// Re-export so handlers can compare states externally.
|
|
725
|
+
export { EpochProvingJobTerminalState };
|