@aztec/prover-node 5.0.0-rc.2 → 5.0.1
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 +53 -60
- package/dest/checkpoint-store.d.ts +39 -38
- package/dest/checkpoint-store.d.ts.map +1 -1
- package/dest/checkpoint-store.js +62 -79
- package/dest/job/checkpoint-prover.d.ts +6 -16
- package/dest/job/checkpoint-prover.d.ts.map +1 -1
- package/dest/job/checkpoint-prover.js +11 -34
- package/dest/job/epoch-session.d.ts +4 -2
- package/dest/job/epoch-session.d.ts.map +1 -1
- package/dest/job/epoch-session.js +4 -2
- package/dest/job/top-tree-job.d.ts +2 -2
- package/dest/job/top-tree-job.d.ts.map +1 -1
- package/dest/job/top-tree-job.js +4 -4
- package/dest/metrics.js +1 -1
- package/dest/prover-node.d.ts +1 -1
- package/dest/prover-node.d.ts.map +1 -1
- package/dest/prover-node.js +5 -7
- package/dest/session-manager.d.ts +2 -2
- package/dest/session-manager.d.ts.map +1 -1
- package/dest/session-manager.js +43 -9
- package/package.json +23 -23
- package/src/checkpoint-store.ts +62 -86
- package/src/job/checkpoint-prover.ts +13 -39
- package/src/job/epoch-session.ts +4 -2
- package/src/job/top-tree-job.ts +4 -4
- package/src/metrics.ts +1 -1
- package/src/prover-node.ts +6 -7
- package/src/session-manager.ts +41 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/prover-node",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -56,28 +56,28 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aztec/archiver": "5.0.
|
|
60
|
-
"@aztec/bb-prover": "5.0.
|
|
61
|
-
"@aztec/blob-client": "5.0.
|
|
62
|
-
"@aztec/blob-lib": "5.0.
|
|
63
|
-
"@aztec/constants": "5.0.
|
|
64
|
-
"@aztec/epoch-cache": "5.0.
|
|
65
|
-
"@aztec/ethereum": "5.0.
|
|
66
|
-
"@aztec/foundation": "5.0.
|
|
67
|
-
"@aztec/kv-store": "5.0.
|
|
68
|
-
"@aztec/l1-artifacts": "5.0.
|
|
69
|
-
"@aztec/native": "5.0.
|
|
70
|
-
"@aztec/node-keystore": "5.0.
|
|
71
|
-
"@aztec/node-lib": "5.0.
|
|
72
|
-
"@aztec/noir-protocol-circuits-types": "5.0.
|
|
73
|
-
"@aztec/p2p": "5.0.
|
|
74
|
-
"@aztec/protocol-contracts": "5.0.
|
|
75
|
-
"@aztec/prover-client": "5.0.
|
|
76
|
-
"@aztec/sequencer-client": "5.0.
|
|
77
|
-
"@aztec/simulator": "5.0.
|
|
78
|
-
"@aztec/stdlib": "5.0.
|
|
79
|
-
"@aztec/telemetry-client": "5.0.
|
|
80
|
-
"@aztec/world-state": "5.0.
|
|
59
|
+
"@aztec/archiver": "5.0.1",
|
|
60
|
+
"@aztec/bb-prover": "5.0.1",
|
|
61
|
+
"@aztec/blob-client": "5.0.1",
|
|
62
|
+
"@aztec/blob-lib": "5.0.1",
|
|
63
|
+
"@aztec/constants": "5.0.1",
|
|
64
|
+
"@aztec/epoch-cache": "5.0.1",
|
|
65
|
+
"@aztec/ethereum": "5.0.1",
|
|
66
|
+
"@aztec/foundation": "5.0.1",
|
|
67
|
+
"@aztec/kv-store": "5.0.1",
|
|
68
|
+
"@aztec/l1-artifacts": "5.0.1",
|
|
69
|
+
"@aztec/native": "5.0.1",
|
|
70
|
+
"@aztec/node-keystore": "5.0.1",
|
|
71
|
+
"@aztec/node-lib": "5.0.1",
|
|
72
|
+
"@aztec/noir-protocol-circuits-types": "5.0.1",
|
|
73
|
+
"@aztec/p2p": "5.0.1",
|
|
74
|
+
"@aztec/protocol-contracts": "5.0.1",
|
|
75
|
+
"@aztec/prover-client": "5.0.1",
|
|
76
|
+
"@aztec/sequencer-client": "5.0.1",
|
|
77
|
+
"@aztec/simulator": "5.0.1",
|
|
78
|
+
"@aztec/stdlib": "5.0.1",
|
|
79
|
+
"@aztec/telemetry-client": "5.0.1",
|
|
80
|
+
"@aztec/world-state": "5.0.1",
|
|
81
81
|
"source-map-support": "^0.5.21",
|
|
82
82
|
"tslib": "^2.4.0",
|
|
83
83
|
"viem": "npm:@aztec/viem@2.38.2"
|
package/src/checkpoint-store.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
2
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
3
|
-
import { RunningPromise } from '@aztec/foundation/promise';
|
|
4
3
|
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
5
4
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
6
5
|
import { type L1RollupConstants, getEpochAtSlot, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
@@ -19,54 +18,64 @@ export type CheckpointProverFactory = (args: CheckpointProverArgs, deps: Checkpo
|
|
|
19
18
|
*
|
|
20
19
|
* The store survives every epoch / session boundary. A prover lives from its first
|
|
21
20
|
* `addOrUpdate` call until either:
|
|
22
|
-
* -
|
|
21
|
+
* - its checkpoint is pruned by an L1 reorg (`cancelAndRemoveAboveBlock`), or
|
|
23
22
|
* - its epoch's proof-submission window has closed (`reapExpired`), so the proof could no
|
|
24
23
|
* longer be accepted on L1 even if produced.
|
|
25
24
|
*
|
|
26
|
-
* A
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* A prover's sub-tree work forks world-state per block and does not survive a prune of a base
|
|
26
|
+
* block, so there is nothing to preserve across a reorg: a pruned prover is cancelled and
|
|
27
|
+
* dropped, and a re-add (even of identical content) constructs a fresh prover.
|
|
29
28
|
*/
|
|
30
29
|
export class CheckpointStore {
|
|
31
30
|
private readonly provers = new Map<string, CheckpointProver>();
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Teardowns of provers already removed from `provers` (by prune or reap), awaited on `stop()`.
|
|
33
|
+
* Keyed by a monotonic id rather than the prover's content id: a prune-then-re-add can leave two
|
|
34
|
+
* teardowns for the same content id in flight at once, which a content-id key would clobber.
|
|
35
|
+
*/
|
|
36
|
+
private readonly pendingTeardowns = new Map<number, Promise<void>>();
|
|
37
|
+
private nextTeardownId = 0;
|
|
33
38
|
private readonly log: Logger;
|
|
34
39
|
|
|
35
40
|
constructor(
|
|
36
|
-
private readonly l2BlockSource: Pick<L2BlockSource, '
|
|
41
|
+
private readonly l2BlockSource: Pick<L2BlockSource, 'getL1Constants'>,
|
|
37
42
|
private readonly proverDeps: Omit<CheckpointProverDeps, 'log'>,
|
|
38
|
-
private readonly options: { slotWatcherPollIntervalMs: number },
|
|
39
43
|
bindings?: LoggerBindings,
|
|
40
44
|
private readonly proverFactoryFn: CheckpointProverFactory = (args, deps) => new CheckpointProver(args, deps),
|
|
41
45
|
) {
|
|
42
46
|
this.log = createLogger('prover-node:checkpoint-store', bindings);
|
|
43
|
-
this.slotWatcher = new RunningPromise(
|
|
44
|
-
() => this.reapPrunedPastSlot(),
|
|
45
|
-
this.log,
|
|
46
|
-
this.options.slotWatcherPollIntervalMs,
|
|
47
|
-
);
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
public start(): Promise<void> {
|
|
51
|
-
this.slotWatcher.start();
|
|
52
50
|
return Promise.resolve();
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
public async stop(): Promise<void> {
|
|
56
|
-
await
|
|
57
|
-
//
|
|
54
|
+
// Cancel every live prover, then await both their teardown and any still in flight for provers
|
|
55
|
+
// already removed by a prune or reap.
|
|
58
56
|
const provers = Array.from(this.provers.values());
|
|
59
57
|
this.provers.clear();
|
|
60
58
|
for (const prover of provers) {
|
|
61
59
|
prover.cancel();
|
|
62
60
|
}
|
|
63
|
-
await Promise.allSettled(provers.map(p => p.whenDone()));
|
|
61
|
+
await Promise.allSettled([...provers.map(p => p.whenDone()), ...this.pendingTeardowns.values()]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tracks the teardown of a prover just removed from the store so `stop()` can await it. The entry
|
|
66
|
+
* removes itself once teardown settles, so the map stays bounded by the number in flight.
|
|
67
|
+
*/
|
|
68
|
+
private trackTeardown(prover: CheckpointProver): void {
|
|
69
|
+
const id = this.nextTeardownId++;
|
|
70
|
+
const done = prover.whenDone();
|
|
71
|
+
this.pendingTeardowns.set(id, done);
|
|
72
|
+
void done.finally(() => this.pendingTeardowns.delete(id));
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
/**
|
|
67
76
|
* Registers a checkpoint with the store. If a prover already exists for the
|
|
68
|
-
* `(number, slot, archive root)` content key
|
|
69
|
-
* otherwise a new prover is constructed.
|
|
77
|
+
* `(number, slot, archive root)` content key it is reused (an at-least-once re-registration of
|
|
78
|
+
* still-canonical content); otherwise a new prover is constructed.
|
|
70
79
|
*/
|
|
71
80
|
public async addOrUpdate(checkpoint: Checkpoint, data: RegisterCheckpointData): Promise<CheckpointProver> {
|
|
72
81
|
const l1Constants = await this.l2BlockSource.getL1Constants();
|
|
@@ -75,18 +84,18 @@ export class CheckpointStore {
|
|
|
75
84
|
|
|
76
85
|
const existing = this.provers.get(id);
|
|
77
86
|
if (existing) {
|
|
78
|
-
existing.markCanonical();
|
|
79
87
|
return existing;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
// At most one canonical checkpoint per slot. A different
|
|
83
|
-
//
|
|
84
|
-
//
|
|
90
|
+
// At most one canonical checkpoint per slot. A different checkpoint at the same slot means the
|
|
91
|
+
// caller forgot to prune the old chain before adding the replacement — surface it rather than
|
|
92
|
+
// silently creating a parallel canonical chain. A pruned checkpoint has already been removed,
|
|
93
|
+
// so every prover still in the store is canonical.
|
|
85
94
|
for (const prover of this.provers.values()) {
|
|
86
|
-
if (prover.slotNumber === checkpoint.header.slotNumber
|
|
95
|
+
if (prover.slotNumber === checkpoint.header.slotNumber) {
|
|
87
96
|
throw new Error(
|
|
88
97
|
`Cannot add checkpoint ${checkpoint.number} (archive ${checkpoint.archive.root}) at slot ${checkpoint.header.slotNumber}: ` +
|
|
89
|
-
`a different
|
|
98
|
+
`a different checkpoint already occupies this slot. Prune it first.`,
|
|
90
99
|
);
|
|
91
100
|
}
|
|
92
101
|
}
|
|
@@ -97,20 +106,25 @@ export class CheckpointStore {
|
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
109
|
+
* Cancels and removes every prover that holds a block above the prune target. A checkpoint is orphaned by a prune to
|
|
110
|
+
* block `targetBlockNumber` iff its last block sits above the target — including a checkpoint whose range straddles
|
|
111
|
+
* the target (partially orphaned), which block-range marking catches without boundary ambiguity. Keying off the
|
|
112
|
+
* surviving block number (rather than a checkpoint number) is correct even when the source has already
|
|
104
113
|
* re-checkpointed past the divergence: the prune event reports the highest surviving block, which by construction
|
|
105
114
|
* survives on the source, whereas the source's current checkpointed tip can sit above the prune target.
|
|
106
|
-
*
|
|
115
|
+
*
|
|
116
|
+
* The prover's in-flight sub-tree work forks world-state per block and faults once its base block is pruned, so it
|
|
117
|
+
* cannot be reused; it is cancelled (aborting the fork reads) and dropped. A subsequent re-add constructs a fresh
|
|
118
|
+
* prover. Returns the removed provers.
|
|
107
119
|
*/
|
|
108
|
-
public
|
|
120
|
+
public cancelAndRemoveAboveBlock(targetBlockNumber: BlockNumber): CheckpointProver[] {
|
|
109
121
|
const affected: CheckpointProver[] = [];
|
|
110
|
-
for (const prover of this.provers.
|
|
122
|
+
for (const [id, prover] of Array.from(this.provers.entries())) {
|
|
111
123
|
const lastBlockNumber = prover.checkpoint.blocks.at(-1)!.number;
|
|
112
|
-
if (lastBlockNumber > targetBlockNumber
|
|
113
|
-
prover.
|
|
124
|
+
if (lastBlockNumber > targetBlockNumber) {
|
|
125
|
+
prover.cancel();
|
|
126
|
+
this.trackTeardown(prover);
|
|
127
|
+
this.provers.delete(id);
|
|
114
128
|
affected.push(prover);
|
|
115
129
|
}
|
|
116
130
|
}
|
|
@@ -118,20 +132,17 @@ export class CheckpointStore {
|
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
/**
|
|
121
|
-
* Drops
|
|
122
|
-
*
|
|
123
|
-
*
|
|
135
|
+
* Drops provers whose epoch is at or below the supplied expired epoch. Once an epoch's
|
|
136
|
+
* proof-submission window has closed, its proof can no longer be accepted on L1, so the
|
|
137
|
+
* prover is no longer needed.
|
|
124
138
|
*/
|
|
125
139
|
public reapExpired(expiredEpoch: EpochNumber): void {
|
|
126
140
|
const reaped: { id: string; checkpointNumber: CheckpointNumber; epochNumber: EpochNumber }[] = [];
|
|
127
141
|
for (const [id, prover] of Array.from(this.provers.entries())) {
|
|
128
|
-
if (prover.isPruned()) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
142
|
if (prover.epochNumber <= expiredEpoch) {
|
|
132
143
|
reaped.push({ id, checkpointNumber: prover.checkpoint.number, epochNumber: prover.epochNumber });
|
|
133
144
|
prover.cancel({ routine: true });
|
|
134
|
-
|
|
145
|
+
this.trackTeardown(prover);
|
|
135
146
|
this.provers.delete(id);
|
|
136
147
|
}
|
|
137
148
|
}
|
|
@@ -154,63 +165,28 @@ export class CheckpointStore {
|
|
|
154
165
|
return this.provers.get(CheckpointProver.idFor(checkpoint));
|
|
155
166
|
}
|
|
156
167
|
|
|
157
|
-
/** Every prover currently in the store
|
|
168
|
+
/** Every prover currently in the store, in insertion order. */
|
|
158
169
|
public listAll(): CheckpointProver[] {
|
|
159
170
|
return Array.from(this.provers.values());
|
|
160
171
|
}
|
|
161
172
|
|
|
162
|
-
/**
|
|
163
|
-
public
|
|
164
|
-
return Array.from(this.provers.values())
|
|
165
|
-
.filter(p => !p.isPruned())
|
|
166
|
-
.sort((a, b) => a.checkpoint.number - b.checkpoint.number);
|
|
173
|
+
/** Provers in the store, sorted by checkpoint number. */
|
|
174
|
+
public list(): CheckpointProver[] {
|
|
175
|
+
return Array.from(this.provers.values()).sort((a, b) => a.checkpoint.number - b.checkpoint.number);
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
/**
|
|
170
|
-
*
|
|
171
|
-
* checkpoint number.
|
|
179
|
+
* Provers whose slot is in the supplied epoch's slot range, sorted by checkpoint number.
|
|
172
180
|
*/
|
|
173
|
-
public async
|
|
181
|
+
public async listForEpoch(epoch: EpochNumber): Promise<CheckpointProver[]> {
|
|
174
182
|
const l1Constants = await this.l2BlockSource.getL1Constants();
|
|
175
183
|
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, l1Constants);
|
|
176
|
-
return this.
|
|
184
|
+
return this.listInSlotRange(fromSlot, toSlot);
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
/**
|
|
180
|
-
public
|
|
181
|
-
return this.
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* SlotWatcher tick: reap pruned provers whose slot has passed the chain's synced
|
|
186
|
-
* slot. Once the chain has moved past, no re-add can revive the prover and its
|
|
187
|
-
* content key is unique enough that an actual re-add would create a new entry.
|
|
188
|
-
*
|
|
189
|
-
* Protected so unit tests can drive a single tick without spinning up the
|
|
190
|
-
* `RunningPromise` and waiting on its interval.
|
|
191
|
-
*/
|
|
192
|
-
protected async reapPrunedPastSlot(): Promise<void> {
|
|
193
|
-
let syncedSlot: SlotNumber | undefined;
|
|
194
|
-
try {
|
|
195
|
-
syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
196
|
-
} catch (err) {
|
|
197
|
-
this.log.debug(`SlotWatcher could not read synced slot`, { error: `${err}` });
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (syncedSlot === undefined) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
for (const [id, prover] of Array.from(this.provers.entries())) {
|
|
204
|
-
if (prover.isPruned() && prover.slotNumber < syncedSlot) {
|
|
205
|
-
this.log.info(`Reaping pruned CheckpointProver ${id}: slot ${prover.slotNumber} < synced ${syncedSlot}`, {
|
|
206
|
-
checkpointNumber: prover.checkpoint.number,
|
|
207
|
-
slotNumber: prover.slotNumber,
|
|
208
|
-
});
|
|
209
|
-
prover.cancel();
|
|
210
|
-
void prover.whenDone();
|
|
211
|
-
this.provers.delete(id);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
187
|
+
/** Provers whose slot falls within `[fromSlot, toSlot]`, sorted by checkpoint number. */
|
|
188
|
+
public listInSlotRange(fromSlot: SlotNumber, toSlot: SlotNumber): CheckpointProver[] {
|
|
189
|
+
return this.list().filter(p => p.slotNumber >= fromSlot && p.slotNumber <= toSlot);
|
|
214
190
|
}
|
|
215
191
|
}
|
|
216
192
|
|
|
@@ -59,16 +59,16 @@ export type CheckpointProverArgs = {
|
|
|
59
59
|
* The store creates a CheckpointProver once per content-key. Keying on the checkpoint's
|
|
60
60
|
* own archive root (its post-state) means two checkpoints are "the same" iff they
|
|
61
61
|
* produce the same archive — so a reorg branch, or a replacement built on the same
|
|
62
|
-
* predecessor but with different content, keys to a distinct prover
|
|
63
|
-
* re-add keys to the same one and reuses its in-flight sub-tree work.
|
|
62
|
+
* predecessor but with different content, keys to a distinct prover.
|
|
64
63
|
*
|
|
65
64
|
* The prover eagerly starts its own tx gather and sub-tree work in the constructor, so
|
|
66
65
|
* callers only need to call `whenBlockProofsReady()` to obtain the resulting block-rollup
|
|
67
66
|
* proofs.
|
|
68
67
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* a
|
|
68
|
+
* A CheckpointProver does not survive a prune: its sub-tree work forks world-state per
|
|
69
|
+
* block, and an L1 prune of a base block faults those reads. The store therefore cancels and
|
|
70
|
+
* discards a prover when its checkpoint is pruned, and a re-add (even of identical content)
|
|
71
|
+
* constructs a fresh prover.
|
|
72
72
|
*
|
|
73
73
|
* `cancel()` is idempotent. It aborts the gather + sub-tree, rejects the block-proof
|
|
74
74
|
* promise, and exposes a `whenDone()` that resolves once teardown has unwound.
|
|
@@ -92,8 +92,6 @@ export class CheckpointProver {
|
|
|
92
92
|
private cancelled = false;
|
|
93
93
|
private subTree?: CheckpointSubTreeOrchestrator;
|
|
94
94
|
private completed = false;
|
|
95
|
-
/** Pruned in the canonical chain but not yet reaped — sub-tree continues running. */
|
|
96
|
-
private pruned = false;
|
|
97
95
|
private readonly abortController = new AbortController();
|
|
98
96
|
|
|
99
97
|
/** Tracks the eager gather+execute task so `cancel()` and `whenDone()` can await its unwind. */
|
|
@@ -146,37 +144,6 @@ export class CheckpointProver {
|
|
|
146
144
|
return this.completed;
|
|
147
145
|
}
|
|
148
146
|
|
|
149
|
-
public isPruned(): boolean {
|
|
150
|
-
return this.pruned;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Mark this prover as no longer present in the canonical chain. Sub-tree proving keeps
|
|
155
|
-
* running so the work survives if the checkpoint is re-added. Idempotent.
|
|
156
|
-
*/
|
|
157
|
-
public markPruned(): void {
|
|
158
|
-
if (this.pruned) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
this.pruned = true;
|
|
162
|
-
this.deps.log.info(`Marking CheckpointProver ${this.id} as pruned`, {
|
|
163
|
-
checkpointNumber: this.checkpoint.number,
|
|
164
|
-
slotNumber: this.slotNumber,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Mark this prover as part of the canonical chain again after a re-add. Idempotent. */
|
|
169
|
-
public markCanonical(): void {
|
|
170
|
-
if (!this.pruned) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
this.pruned = false;
|
|
174
|
-
this.deps.log.info(`Marking CheckpointProver ${this.id} as canonical`, {
|
|
175
|
-
checkpointNumber: this.checkpoint.number,
|
|
176
|
-
slotNumber: this.slotNumber,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
147
|
/** AbortSignal that fires on cancel — for callers that want to wire their own tasks. */
|
|
181
148
|
public getAbortSignal(): AbortSignal {
|
|
182
149
|
return this.abortController.signal;
|
|
@@ -432,7 +399,14 @@ export class CheckpointProver {
|
|
|
432
399
|
}
|
|
433
400
|
|
|
434
401
|
private async processTxs(publicProcessor: PublicProcessor, txs: Tx[]): Promise<ProcessedTx[]> {
|
|
435
|
-
|
|
402
|
+
// Pass the abort signal so a prune-driven cancel stops the current block's public execution
|
|
403
|
+
// immediately, rather than running it to completion before the next `signal.aborted` check.
|
|
404
|
+
// On abort `process` returns a partial result, the length check below throws, and
|
|
405
|
+
// `gatherAndExecute` swallows it via its `cancelled` guard.
|
|
406
|
+
const [processedTxs, failedTxs] = await publicProcessor.process(txs, {
|
|
407
|
+
deadline: this.deps.deadline,
|
|
408
|
+
signal: this.abortController.signal,
|
|
409
|
+
});
|
|
436
410
|
|
|
437
411
|
if (failedTxs.length) {
|
|
438
412
|
const failedTxHashes = await Promise.all(failedTxs.map(({ tx }) => tx.getTxHash()));
|
package/src/job/epoch-session.ts
CHANGED
|
@@ -228,7 +228,7 @@ export class EpochSession implements Traceable {
|
|
|
228
228
|
* Cancels the session. Idempotent. Withdraws any submitted candidate from the
|
|
229
229
|
* publishing service so the in-flight publisher (if any) is interrupted.
|
|
230
230
|
*/
|
|
231
|
-
public async cancel(reason = 'cancelled'): Promise<void> {
|
|
231
|
+
public async cancel(reason = 'cancelled', { abortJobs = true }: { abortJobs?: boolean } = {}): Promise<void> {
|
|
232
232
|
if (this.isTerminal()) {
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
@@ -247,7 +247,9 @@ export class EpochSession implements Traceable {
|
|
|
247
247
|
if (this.topTreeJob && !this.topTreeJob.isCancelled()) {
|
|
248
248
|
const job = this.topTreeJob;
|
|
249
249
|
this.topTreeJob = undefined;
|
|
250
|
-
|
|
250
|
+
// On a clean shutdown we leave the in-flight broker jobs alone so a restart can reuse them;
|
|
251
|
+
// other cancellations (reorg, supersede, deadline) abort them since their inputs are stale.
|
|
252
|
+
job.cancel(abortJobs);
|
|
251
253
|
this.pendingTopTreeCleanups.push(job);
|
|
252
254
|
}
|
|
253
255
|
await this.teardownTopTreeIfNeeded();
|
package/src/job/top-tree-job.ts
CHANGED
|
@@ -128,7 +128,7 @@ export class TopTreeJob {
|
|
|
128
128
|
* via `whenDone()` — the parent collects the cancelled job and awaits all
|
|
129
129
|
* pending top-tree teardowns at the end of the epoch.
|
|
130
130
|
*/
|
|
131
|
-
public cancel(): void {
|
|
131
|
+
public cancel(abortJobs = true): void {
|
|
132
132
|
if (this.cancelled) {
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
@@ -146,7 +146,7 @@ export class TopTreeJob {
|
|
|
146
146
|
// Fire and forget: parent awaits the cancel-driven teardown via whenDone(); the
|
|
147
147
|
// chained .catch swallows rejections so the unawaited promise doesn't surface
|
|
148
148
|
// as an unhandled rejection.
|
|
149
|
-
this.cancelPromise = this.runCancel().catch(() => {});
|
|
149
|
+
this.cancelPromise = this.runCancel(abortJobs).catch(() => {});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/** Resolves once the cancel-driven teardown of the underlying orchestrator has unwound. */
|
|
@@ -156,9 +156,9 @@ export class TopTreeJob {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
private async runCancel(): Promise<void> {
|
|
159
|
+
private async runCancel(abortJobs: boolean): Promise<void> {
|
|
160
160
|
try {
|
|
161
|
-
this.topTree.cancel({ abortJobs
|
|
161
|
+
this.topTree.cancel({ abortJobs });
|
|
162
162
|
} catch (err) {
|
|
163
163
|
this.deps.log.error('Error cancelling top tree', err);
|
|
164
164
|
}
|
package/src/metrics.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class ProverNodeJobMetrics {
|
|
|
96
96
|
this.activeCheckpoints = this.meter.createObservableGauge(Metrics.PROVER_NODE_ACTIVE_CHECKPOINTS);
|
|
97
97
|
this.activeEpochSessions = this.meter.createObservableGauge(Metrics.PROVER_NODE_ACTIVE_EPOCH_SESSIONS);
|
|
98
98
|
this.stateObserver = (observer: BatchObservableResult) => {
|
|
99
|
-
observer.observe(this.activeCheckpoints!, checkpointStore.
|
|
99
|
+
observer.observe(this.activeCheckpoints!, checkpointStore.listAll().length);
|
|
100
100
|
let full = 0;
|
|
101
101
|
let partial = 0;
|
|
102
102
|
for (const session of sessionManager.allSessions()) {
|
package/src/prover-node.ts
CHANGED
|
@@ -12,9 +12,9 @@ import { getLastSiblingPath } from '@aztec/prover-client/helpers';
|
|
|
12
12
|
import { ChonkCache } from '@aztec/prover-client/orchestrator';
|
|
13
13
|
import { PublicProcessorFactory } from '@aztec/simulator/server';
|
|
14
14
|
import {
|
|
15
|
+
EventDrivenL2BlockStream,
|
|
15
16
|
type L2BlockId,
|
|
16
17
|
type L2BlockSource,
|
|
17
|
-
L2BlockStream,
|
|
18
18
|
type L2BlockStreamEvent,
|
|
19
19
|
type L2BlockStreamEventHandler,
|
|
20
20
|
L2TipsMemoryStore,
|
|
@@ -87,7 +87,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
87
87
|
/** In-memory store for the L2BlockStream's local data provider. */
|
|
88
88
|
private tipsStore: L2TipsMemoryStore;
|
|
89
89
|
/** Block stream for checkpoint and reorg detection. */
|
|
90
|
-
private blockStream:
|
|
90
|
+
private blockStream: EventDrivenL2BlockStream | undefined;
|
|
91
91
|
/**
|
|
92
92
|
* Highest epoch whose proof-submission window has passed. Monotonic high-water mark.
|
|
93
93
|
* Seeded from the last fully-proven epoch at start(); advanced on every block-stream
|
|
@@ -169,7 +169,6 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
169
169
|
txGatheringTimeoutMs: this.config.txGatheringTimeoutMs,
|
|
170
170
|
deadline: undefined,
|
|
171
171
|
},
|
|
172
|
-
{ slotWatcherPollIntervalMs: this.config.proverNodePollingIntervalMs },
|
|
173
172
|
this.log.getBindings(),
|
|
174
173
|
);
|
|
175
174
|
}
|
|
@@ -372,8 +371,8 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
372
371
|
private async handlePruneEvent(prunedToBlock: L2BlockId) {
|
|
373
372
|
this.log.warn(`Chain pruned to block ${prunedToBlock.number}`, { prunedToBlock });
|
|
374
373
|
|
|
375
|
-
// Resolve the cursor floor BEFORE
|
|
376
|
-
// throw after
|
|
374
|
+
// Resolve the cursor floor BEFORE removing provers: cancelAndRemoveAboveBlock returns only the provers it removed,
|
|
375
|
+
// so a throw after removing would leave a retry pass with nothing to act on. Resolving first means a throw leaves
|
|
377
376
|
// everything untouched and the next pass retries the whole handler (the tips cursor only advances on success).
|
|
378
377
|
let cursorFloor: CheckpointNumber;
|
|
379
378
|
if (prunedToBlock.number === 0) {
|
|
@@ -391,7 +390,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
391
390
|
cursorFloor = CheckpointNumber(Math.max(0, Number(targetData.checkpointNumber) - 1));
|
|
392
391
|
}
|
|
393
392
|
|
|
394
|
-
const affected = this.checkpointStore.
|
|
393
|
+
const affected = this.checkpointStore.cancelAndRemoveAboveBlock(prunedToBlock.number);
|
|
395
394
|
|
|
396
395
|
if (this.lastProcessedCheckpoint > cursorFloor) {
|
|
397
396
|
this.lastProcessedCheckpoint = cursorFloor;
|
|
@@ -510,7 +509,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
510
509
|
const { lastFullyProvenEpoch } = await this.resolveLastFullyProvenEpoch();
|
|
511
510
|
this.lastExpiredEpoch = lastFullyProvenEpoch;
|
|
512
511
|
this.lastProcessedCheckpoint = await this.computeStartingCheckpoint(lastFullyProvenEpoch);
|
|
513
|
-
this.blockStream = new
|
|
512
|
+
this.blockStream = new EventDrivenL2BlockStream(this.l2BlockSource, this.tipsStore, this, this.log, {
|
|
514
513
|
pollIntervalMS: this.config.proverNodePollingIntervalMs,
|
|
515
514
|
tipsOnly: true,
|
|
516
515
|
});
|
package/src/session-manager.ts
CHANGED
|
@@ -185,7 +185,7 @@ export class SessionManager {
|
|
|
185
185
|
* slot. Dedupes against any existing session covering the same range, returning its id.
|
|
186
186
|
*/
|
|
187
187
|
public async startProof(epoch: EpochNumber): Promise<string> {
|
|
188
|
-
const canonical = await this.deps.checkpointStore.
|
|
188
|
+
const canonical = await this.deps.checkpointStore.listForEpoch(epoch);
|
|
189
189
|
if (canonical.length === 0) {
|
|
190
190
|
throw new EmptyEpochError(epoch);
|
|
191
191
|
}
|
|
@@ -227,7 +227,9 @@ export class SessionManager {
|
|
|
227
227
|
await this.epochTicker?.stop();
|
|
228
228
|
await this.reconcileQueue.cancel();
|
|
229
229
|
const sessions = this.allSessions();
|
|
230
|
-
|
|
230
|
+
// A clean shutdown is just a restart, so preserve the in-flight broker jobs (abortJobs: false)
|
|
231
|
+
// for the restarted node to reuse rather than re-proving the epoch from scratch.
|
|
232
|
+
await Promise.allSettled(sessions.map(s => s.cancel('prover-node stopping', { abortJobs: false })));
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
// ---------------- reconcile ----------------
|
|
@@ -268,7 +270,7 @@ export class SessionManager {
|
|
|
268
270
|
this.fullSessions.delete(key);
|
|
269
271
|
continue;
|
|
270
272
|
}
|
|
271
|
-
const canonical = this.
|
|
273
|
+
const canonical = this.checkpointsForSpec(session.getSpec());
|
|
272
274
|
if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
|
|
273
275
|
this.fireAndForgetCancel(session, 'canonical content changed');
|
|
274
276
|
this.fullSessions.delete(key);
|
|
@@ -284,7 +286,7 @@ export class SessionManager {
|
|
|
284
286
|
this.partialSessions.delete(key);
|
|
285
287
|
continue;
|
|
286
288
|
}
|
|
287
|
-
const canonical = this.
|
|
289
|
+
const canonical = this.checkpointsForSpec(session.getSpec());
|
|
288
290
|
if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
|
|
289
291
|
this.fireAndForgetCancel(session, 'canonical content changed');
|
|
290
292
|
this.partialSessions.delete(key);
|
|
@@ -298,6 +300,8 @@ export class SessionManager {
|
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
private async openFullSessionIfReady(epoch: EpochNumber): Promise<void> {
|
|
303
|
+
// `recreateInvalidSessions` runs at the top of every reconcile and deletes terminal sessions
|
|
304
|
+
// before this is called, so a session present here is live and already covers the epoch.
|
|
301
305
|
if (this.fullSessions.has(epoch)) {
|
|
302
306
|
return;
|
|
303
307
|
}
|
|
@@ -314,7 +318,7 @@ export class SessionManager {
|
|
|
314
318
|
return;
|
|
315
319
|
}
|
|
316
320
|
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, l1Constants);
|
|
317
|
-
const canonical = this.deps.checkpointStore.
|
|
321
|
+
const canonical = this.deps.checkpointStore.listInSlotRange(fromSlot, toSlot);
|
|
318
322
|
if (!this.archiverFullyCovered(archiverCps, canonical)) {
|
|
319
323
|
this.log.debug(`Skipping full-session open for epoch ${epoch}: archiver checkpoints not all in store`, {
|
|
320
324
|
archiverCount: archiverCps.length,
|
|
@@ -329,7 +333,7 @@ export class SessionManager {
|
|
|
329
333
|
}
|
|
330
334
|
|
|
331
335
|
private openPartialSession(spec: SessionSpec): void {
|
|
332
|
-
const canonical = this.deps.checkpointStore.
|
|
336
|
+
const canonical = this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
|
|
333
337
|
if (canonical.length === 0) {
|
|
334
338
|
return;
|
|
335
339
|
}
|
|
@@ -403,6 +407,17 @@ export class SessionManager {
|
|
|
403
407
|
const state = await session.start();
|
|
404
408
|
this.log.info(`Session ${session.getId()} exited with state ${state}`);
|
|
405
409
|
if (state === 'failed' && this.deps.onSessionFailed) {
|
|
410
|
+
// Best-effort suppression of the spurious post-mortem upload a prune produces: if the session's
|
|
411
|
+
// checkpoints no longer match the store's current set, the failure was caused by the content
|
|
412
|
+
// changing under it, not a genuine proving fault, so skip the upload. This is inherently racy —
|
|
413
|
+
// the store lags the world-state unwind, so a fault observed before the prune is reconciled here
|
|
414
|
+
// still uploads. The epoch is recovered regardless by recreating the session on re-add.
|
|
415
|
+
if (!this.checkpointsMatch(session.getCheckpoints(), this.checkpointsForSpec(session.getSpec()))) {
|
|
416
|
+
this.log.info(`Skipping failure upload for session ${session.getId()}: canonical content changed`, {
|
|
417
|
+
...session.getSpec(),
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
406
421
|
try {
|
|
407
422
|
await this.deps.onSessionFailed(session);
|
|
408
423
|
} catch (err) {
|
|
@@ -447,6 +462,24 @@ export class SessionManager {
|
|
|
447
462
|
return live >= max;
|
|
448
463
|
}
|
|
449
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Maps a reconcile trigger to the epochs whose full session should be (re)opened.
|
|
467
|
+
*
|
|
468
|
+
* This is where the "don't retry a genuinely-failed epoch, but do recover a pruned one" invariant
|
|
469
|
+
* lives — enforced by which triggers are gated by `lastTickEpoch`:
|
|
470
|
+
*
|
|
471
|
+
* - The periodic `tick` IS gated: once a tick has opened a session for an epoch, `lastTickEpoch`
|
|
472
|
+
* advances to it and later ticks skip it (`epoch <= lastTickEpoch`). So a failed attempt is never
|
|
473
|
+
* resubmitted on a loop by the tick.
|
|
474
|
+
* - `checkpoint` and `prune` are deliberately NOT gated. They only fire when the epoch's canonical
|
|
475
|
+
* content actually changes — a checkpoint arrives, or a reorg prunes/replaces one — which is
|
|
476
|
+
* exactly when re-attempting is correct.
|
|
477
|
+
*
|
|
478
|
+
* A genuine proving failure produces no content change, hence no checkpoint/prune event, so only
|
|
479
|
+
* the gated tick could reopen it — and it won't. A prune + re-add fires ungated events, so the
|
|
480
|
+
* epoch is reopened through this path (and `openFullSessionIfReady` rebuilds over the fresh
|
|
481
|
+
* provers). See the "onTick does not retry ... but recovers ... re-added" test.
|
|
482
|
+
*/
|
|
450
483
|
private async epochsForTrigger(trigger: ReconcileTrigger): Promise<EpochNumber[]> {
|
|
451
484
|
switch (trigger.kind) {
|
|
452
485
|
case 'checkpoint':
|
|
@@ -481,8 +514,8 @@ export class SessionManager {
|
|
|
481
514
|
return getEpochAtSlot(header.getSlot(), await this.getL1Constants());
|
|
482
515
|
}
|
|
483
516
|
|
|
484
|
-
private
|
|
485
|
-
return this.deps.checkpointStore.
|
|
517
|
+
private checkpointsForSpec(spec: SessionSpec): CheckpointProver[] {
|
|
518
|
+
return this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
|
|
486
519
|
}
|
|
487
520
|
|
|
488
521
|
private fireAndForgetCancel(session: EpochSession, reason: string): void {
|