@aztec/aztec-node 0.0.1-commit.b655e406 → 0.0.1-commit.c0b82b2
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/dest/aztec-node/config.d.ts +11 -5
- package/dest/aztec-node/config.d.ts.map +1 -1
- package/dest/aztec-node/config.js +17 -3
- package/dest/aztec-node/node_metrics.d.ts +1 -1
- package/dest/aztec-node/node_metrics.d.ts.map +1 -1
- package/dest/aztec-node/node_metrics.js +9 -16
- package/dest/aztec-node/server.d.ts +68 -125
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +846 -268
- package/dest/bin/index.d.ts +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/sentinel/config.d.ts +1 -1
- package/dest/sentinel/factory.d.ts +1 -1
- package/dest/sentinel/factory.d.ts.map +1 -1
- package/dest/sentinel/factory.js +1 -1
- package/dest/sentinel/index.d.ts +1 -1
- package/dest/sentinel/sentinel.d.ts +22 -20
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +101 -63
- package/dest/sentinel/store.d.ts +6 -5
- package/dest/sentinel/store.d.ts.map +1 -1
- package/dest/sentinel/store.js +14 -9
- package/dest/test/index.d.ts +1 -1
- package/package.json +31 -28
- package/src/aztec-node/config.ts +34 -14
- package/src/aztec-node/node_metrics.ts +6 -17
- package/src/aztec-node/server.ts +595 -351
- package/src/sentinel/factory.ts +1 -6
- package/src/sentinel/sentinel.ts +136 -82
- package/src/sentinel/store.ts +22 -21
package/src/sentinel/factory.ts
CHANGED
|
@@ -20,12 +20,7 @@ export async function createSentinel(
|
|
|
20
20
|
if (!config.sentinelEnabled) {
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
-
const kvStore = await createStore(
|
|
24
|
-
'sentinel',
|
|
25
|
-
SentinelStore.SCHEMA_VERSION,
|
|
26
|
-
config,
|
|
27
|
-
createLogger('node:sentinel:lmdb'),
|
|
28
|
-
);
|
|
23
|
+
const kvStore = await createStore('sentinel', SentinelStore.SCHEMA_VERSION, config, logger.getBindings());
|
|
29
24
|
const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
|
|
30
25
|
const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
|
|
31
26
|
const sentinelStore = new SentinelStore(kvStore, {
|
package/src/sentinel/sentinel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
3
|
import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection';
|
|
3
4
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
5
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
L2BlockStream,
|
|
19
20
|
type L2BlockStreamEvent,
|
|
20
21
|
type L2BlockStreamEventHandler,
|
|
21
|
-
|
|
22
|
+
getAttestationInfoFromPublishedCheckpoint,
|
|
22
23
|
} from '@aztec/stdlib/block';
|
|
23
24
|
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
25
|
import type {
|
|
@@ -35,15 +36,29 @@ import EventEmitter from 'node:events';
|
|
|
35
36
|
|
|
36
37
|
import { SentinelStore } from './store.js';
|
|
37
38
|
|
|
39
|
+
/** Maps a validator status to its category: proposer or attestation. */
|
|
40
|
+
function statusToCategory(status: ValidatorStatusInSlot): ValidatorStatusType {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case 'attestation-sent':
|
|
43
|
+
case 'attestation-missed':
|
|
44
|
+
return 'attestation';
|
|
45
|
+
default:
|
|
46
|
+
return 'proposer';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
export class Sentinel extends (EventEmitter as new () => WatcherEmitter) implements L2BlockStreamEventHandler, Watcher {
|
|
39
51
|
protected runningPromise: RunningPromise;
|
|
40
52
|
protected blockStream!: L2BlockStream;
|
|
41
53
|
protected l2TipsStore: L2TipsStore;
|
|
42
54
|
|
|
43
|
-
protected initialSlot:
|
|
44
|
-
protected lastProcessedSlot:
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
protected initialSlot: SlotNumber | undefined;
|
|
56
|
+
protected lastProcessedSlot: SlotNumber | undefined;
|
|
57
|
+
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
58
|
+
protected slotNumberToCheckpoint: Map<
|
|
59
|
+
SlotNumber,
|
|
60
|
+
{ checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }
|
|
61
|
+
> = new Map();
|
|
47
62
|
|
|
48
63
|
constructor(
|
|
49
64
|
protected epochCache: EpochCache,
|
|
@@ -74,7 +89,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
74
89
|
/** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
|
|
75
90
|
protected async init() {
|
|
76
91
|
this.initialSlot = this.epochCache.getEpochAndSlotNow().slot;
|
|
77
|
-
const startingBlock = await this.archiver.getBlockNumber();
|
|
92
|
+
const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
|
|
78
93
|
this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
|
|
79
94
|
this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
|
|
80
95
|
}
|
|
@@ -85,47 +100,54 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
85
100
|
|
|
86
101
|
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
87
102
|
await this.l2TipsStore.handleBlockStreamEvent(event);
|
|
88
|
-
if (event.type === '
|
|
89
|
-
|
|
90
|
-
for (const block of event.blocks) {
|
|
91
|
-
this.slotNumberToBlock.set(block.block.header.getSlot(), {
|
|
92
|
-
blockNumber: block.block.number,
|
|
93
|
-
archive: block.block.archive.root.toString(),
|
|
94
|
-
attestors: getAttestationInfoFromPublishedL2Block(block)
|
|
95
|
-
.filter(a => a.status === 'recovered-from-signature')
|
|
96
|
-
.map(a => a.address!),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Prune the archive map to only keep at most N entries
|
|
101
|
-
const historyLength = this.store.getHistoryLength();
|
|
102
|
-
if (this.slotNumberToBlock.size > historyLength) {
|
|
103
|
-
const toDelete = Array.from(this.slotNumberToBlock.keys())
|
|
104
|
-
.sort((a, b) => Number(a - b))
|
|
105
|
-
.slice(0, this.slotNumberToBlock.size - historyLength);
|
|
106
|
-
for (const key of toDelete) {
|
|
107
|
-
this.slotNumberToBlock.delete(key);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
103
|
+
if (event.type === 'chain-checkpointed') {
|
|
104
|
+
this.handleCheckpoint(event);
|
|
110
105
|
} else if (event.type === 'chain-proven') {
|
|
111
106
|
await this.handleChainProven(event);
|
|
112
107
|
}
|
|
113
108
|
}
|
|
114
109
|
|
|
110
|
+
protected handleCheckpoint(event: L2BlockStreamEvent) {
|
|
111
|
+
if (event.type !== 'chain-checkpointed') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const checkpoint = event.checkpoint;
|
|
115
|
+
|
|
116
|
+
// Store mapping from slot to archive, checkpoint number, and attestors
|
|
117
|
+
this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, {
|
|
118
|
+
checkpointNumber: checkpoint.checkpoint.number,
|
|
119
|
+
archive: checkpoint.checkpoint.archive.root.toString(),
|
|
120
|
+
attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint)
|
|
121
|
+
.filter(a => a.status === 'recovered-from-signature')
|
|
122
|
+
.map(a => a.address!),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Prune the archive map to only keep at most N entries
|
|
126
|
+
const historyLength = this.store.getHistoryLength();
|
|
127
|
+
if (this.slotNumberToCheckpoint.size > historyLength) {
|
|
128
|
+
const toDelete = Array.from(this.slotNumberToCheckpoint.keys())
|
|
129
|
+
.sort((a, b) => Number(a - b))
|
|
130
|
+
.slice(0, this.slotNumberToCheckpoint.size - historyLength);
|
|
131
|
+
for (const key of toDelete) {
|
|
132
|
+
this.slotNumberToCheckpoint.delete(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
protected async handleChainProven(event: L2BlockStreamEvent) {
|
|
116
138
|
if (event.type !== 'chain-proven') {
|
|
117
139
|
return;
|
|
118
140
|
}
|
|
119
141
|
const blockNumber = event.block.number;
|
|
120
|
-
const
|
|
121
|
-
if (!
|
|
122
|
-
this.logger.error(`Failed to get block ${blockNumber}
|
|
142
|
+
const header = await this.archiver.getBlockHeader(blockNumber);
|
|
143
|
+
if (!header) {
|
|
144
|
+
this.logger.error(`Failed to get block header ${blockNumber}`);
|
|
123
145
|
return;
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
// TODO(palla/slash): We should only be computing proven performance if this is
|
|
127
149
|
// a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
|
|
128
|
-
const epoch = getEpochAtSlot(
|
|
150
|
+
const epoch = getEpochAtSlot(header.getSlot(), this.epochCache.getL1Constants());
|
|
129
151
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
130
152
|
const performance = await this.computeProvenPerformance(epoch);
|
|
131
153
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
@@ -134,15 +156,23 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
134
156
|
await this.handleProvenPerformance(epoch, performance);
|
|
135
157
|
}
|
|
136
158
|
|
|
137
|
-
protected async computeProvenPerformance(epoch:
|
|
159
|
+
protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
|
|
138
160
|
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
139
|
-
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
161
|
+
const { committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(fromSlot);
|
|
162
|
+
if (isEscapeHatchOpen) {
|
|
163
|
+
this.logger.info(`Skipping proven performance for epoch ${epoch} - escape hatch is open`);
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
140
166
|
if (!committee) {
|
|
141
167
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
142
168
|
return {};
|
|
143
169
|
}
|
|
144
170
|
|
|
145
|
-
const stats = await this.computeStats({
|
|
171
|
+
const stats = await this.computeStats({
|
|
172
|
+
fromSlot,
|
|
173
|
+
toSlot,
|
|
174
|
+
validators: committee,
|
|
175
|
+
});
|
|
146
176
|
this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
|
|
147
177
|
|
|
148
178
|
// Note that we are NOT using the total slots in the epoch as `total` here, since we only
|
|
@@ -165,7 +195,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
165
195
|
*/
|
|
166
196
|
protected async checkPastInactivity(
|
|
167
197
|
validator: EthAddress,
|
|
168
|
-
currentEpoch:
|
|
198
|
+
currentEpoch: EpochNumber,
|
|
169
199
|
requiredConsecutiveEpochs: number,
|
|
170
200
|
): Promise<boolean> {
|
|
171
201
|
if (requiredConsecutiveEpochs === 0) {
|
|
@@ -175,23 +205,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
175
205
|
// Get all historical performance for this validator
|
|
176
206
|
const allPerformance = await this.store.getProvenPerformance(validator);
|
|
177
207
|
|
|
208
|
+
// Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
|
|
209
|
+
const pastEpochs = allPerformance.sort((a, b) => Number(b.epoch - a.epoch)).filter(p => p.epoch < currentEpoch);
|
|
210
|
+
|
|
178
211
|
// If we don't have enough historical data, don't slash
|
|
179
|
-
if (
|
|
212
|
+
if (pastEpochs.length < requiredConsecutiveEpochs) {
|
|
180
213
|
this.logger.debug(
|
|
181
214
|
`Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
|
|
182
215
|
);
|
|
183
216
|
return false;
|
|
184
217
|
}
|
|
185
218
|
|
|
186
|
-
//
|
|
187
|
-
return
|
|
188
|
-
.sort((a, b) => Number(b.epoch - a.epoch))
|
|
189
|
-
.filter(p => p.epoch < currentEpoch)
|
|
219
|
+
// Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
|
|
220
|
+
return pastEpochs
|
|
190
221
|
.slice(0, requiredConsecutiveEpochs)
|
|
191
222
|
.every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
|
|
192
223
|
}
|
|
193
224
|
|
|
194
|
-
protected async handleProvenPerformance(epoch:
|
|
225
|
+
protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
|
|
195
226
|
if (this.config.slashInactivityPenalty === 0n) {
|
|
196
227
|
return;
|
|
197
228
|
}
|
|
@@ -215,7 +246,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
215
246
|
validator: EthAddress.fromString(address),
|
|
216
247
|
amount: this.config.slashInactivityPenalty,
|
|
217
248
|
offenseType: OffenseType.INACTIVITY,
|
|
218
|
-
epochOrSlot: epoch,
|
|
249
|
+
epochOrSlot: BigInt(epoch),
|
|
219
250
|
}));
|
|
220
251
|
|
|
221
252
|
if (criminals.length > 0) {
|
|
@@ -256,8 +287,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
256
287
|
* We also don't move past the archiver last synced L2 slot, as we don't want to process data that is not yet available.
|
|
257
288
|
* Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
|
|
258
289
|
*/
|
|
259
|
-
protected async isReadyToProcess(currentSlot:
|
|
260
|
-
|
|
290
|
+
protected async isReadyToProcess(currentSlot: SlotNumber): Promise<SlotNumber | false> {
|
|
291
|
+
if (currentSlot < 2) {
|
|
292
|
+
this.logger.trace(`Current slot ${currentSlot} too early.`);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const targetSlot = SlotNumber(currentSlot - 2);
|
|
261
297
|
if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
|
|
262
298
|
this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
|
|
263
299
|
return false;
|
|
@@ -279,8 +315,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
279
315
|
return false;
|
|
280
316
|
}
|
|
281
317
|
|
|
282
|
-
const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.
|
|
283
|
-
const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.
|
|
318
|
+
const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.proposed.hash);
|
|
319
|
+
const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.proposed.hash);
|
|
284
320
|
const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash;
|
|
285
321
|
if (!isP2pSynced) {
|
|
286
322
|
this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash });
|
|
@@ -294,8 +330,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
294
330
|
* Gathers committee and proposer data for a given slot, computes slot stats,
|
|
295
331
|
* and updates overall stats.
|
|
296
332
|
*/
|
|
297
|
-
protected async processSlot(slot:
|
|
298
|
-
const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
|
|
333
|
+
protected async processSlot(slot: SlotNumber) {
|
|
334
|
+
const { epoch, seed, committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(slot);
|
|
335
|
+
if (isEscapeHatchOpen) {
|
|
336
|
+
this.logger.info(`Skipping slot ${slot} at epoch ${epoch} - escape hatch is open`);
|
|
337
|
+
this.lastProcessedSlot = slot;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
299
340
|
if (!committee || committee.length === 0) {
|
|
300
341
|
this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
|
|
301
342
|
this.lastProcessedSlot = slot;
|
|
@@ -310,21 +351,21 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
310
351
|
}
|
|
311
352
|
|
|
312
353
|
/** Computes activity for a given slot. */
|
|
313
|
-
protected async getSlotActivity(slot:
|
|
354
|
+
protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
|
|
314
355
|
this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
|
|
315
356
|
|
|
316
357
|
// Check if there is an L2 block in L1 for this L2 slot
|
|
317
358
|
|
|
318
|
-
// Here we get all attestations for the
|
|
319
|
-
// or all attestations for all proposals in the slot if no
|
|
359
|
+
// Here we get all checkpoint attestations for the checkpoint at the given slot,
|
|
360
|
+
// or all checkpoint attestations for all proposals in the slot if no checkpoint was mined.
|
|
320
361
|
// We gather from both p2p (contains the ones seen on the p2p layer) and archiver
|
|
321
|
-
// (contains the ones synced from mined
|
|
322
|
-
const
|
|
323
|
-
const p2pAttested = await this.p2p.
|
|
362
|
+
// (contains the ones synced from mined checkpoints, which we may have missed from p2p).
|
|
363
|
+
const checkpoint = this.slotNumberToCheckpoint.get(slot);
|
|
364
|
+
const p2pAttested = await this.p2p.getCheckpointAttestationsForSlot(slot, checkpoint?.archive);
|
|
324
365
|
// Filter out attestations with invalid signatures
|
|
325
366
|
const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
|
|
326
367
|
const attestors = new Set(
|
|
327
|
-
[...p2pAttestors.map(a => a.toString()), ...(
|
|
368
|
+
[...p2pAttestors.map(a => a.toString()), ...(checkpoint?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
328
369
|
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
329
370
|
),
|
|
330
371
|
);
|
|
@@ -335,20 +376,29 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
335
376
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
336
377
|
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
337
378
|
// since they will attest to their own proposal it even if it's not re-executable.
|
|
338
|
-
|
|
339
|
-
|
|
379
|
+
let status: 'checkpoint-mined' | 'checkpoint-proposed' | 'checkpoint-missed' | 'blocks-missed';
|
|
380
|
+
if (checkpoint) {
|
|
381
|
+
status = 'checkpoint-mined';
|
|
382
|
+
} else if (attestors.size > 0) {
|
|
383
|
+
status = 'checkpoint-proposed';
|
|
384
|
+
} else {
|
|
385
|
+
// No checkpoint on L1 and no checkpoint attestations seen. Check if block proposals were sent for this slot.
|
|
386
|
+
const hasBlockProposals = await this.p2p.hasBlockProposalsForSlot(slot);
|
|
387
|
+
status = hasBlockProposals ? 'checkpoint-missed' : 'blocks-missed';
|
|
388
|
+
}
|
|
389
|
+
this.logger.debug(`Checkpoint status for slot ${slot}: ${status}`, { ...checkpoint, slot });
|
|
340
390
|
|
|
341
|
-
// Get attestors that failed their
|
|
391
|
+
// Get attestors that failed their checkpoint attestation duties, but only if there was a checkpoint proposed or mined
|
|
342
392
|
const missedAttestors = new Set(
|
|
343
|
-
|
|
393
|
+
status === 'blocks-missed' || status === 'checkpoint-missed'
|
|
344
394
|
? []
|
|
345
395
|
: committee.filter(v => !attestors.has(v.toString()) && !proposer.equals(v)).map(v => v.toString()),
|
|
346
396
|
);
|
|
347
397
|
|
|
348
398
|
this.logger.debug(`Retrieved ${attestors.size} attestors out of ${committee.length} for slot ${slot}`, {
|
|
349
|
-
|
|
399
|
+
status,
|
|
350
400
|
proposer: proposer.toString(),
|
|
351
|
-
...
|
|
401
|
+
...checkpoint,
|
|
352
402
|
slot,
|
|
353
403
|
attestors: [...attestors],
|
|
354
404
|
missedAttestors: [...missedAttestors],
|
|
@@ -358,7 +408,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
358
408
|
// Compute the status for each validator in the committee
|
|
359
409
|
const statusFor = (who: `0x${string}`): ValidatorStatusInSlot | undefined => {
|
|
360
410
|
if (who === proposer.toString()) {
|
|
361
|
-
return
|
|
411
|
+
return status;
|
|
362
412
|
} else if (attestors.has(who)) {
|
|
363
413
|
return 'attestation-sent';
|
|
364
414
|
} else if (missedAttestors.has(who)) {
|
|
@@ -372,7 +422,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
372
422
|
}
|
|
373
423
|
|
|
374
424
|
/** Push the status for each slot for each validator. */
|
|
375
|
-
protected updateValidators(slot:
|
|
425
|
+
protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
|
|
376
426
|
return this.store.updateValidators(slot, stats);
|
|
377
427
|
}
|
|
378
428
|
|
|
@@ -381,13 +431,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
381
431
|
fromSlot,
|
|
382
432
|
toSlot,
|
|
383
433
|
validators,
|
|
384
|
-
}: { fromSlot?:
|
|
434
|
+
}: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
|
|
385
435
|
const histories = validators
|
|
386
436
|
? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
|
|
387
437
|
: await this.store.getHistories();
|
|
388
438
|
|
|
389
439
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
390
|
-
fromSlot ??= (this.lastProcessedSlot ?? slotNow) -
|
|
440
|
+
fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
391
441
|
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
392
442
|
|
|
393
443
|
const stats = mapValues(histories, (history, address) =>
|
|
@@ -405,8 +455,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
405
455
|
/** Computes stats for a single validator. */
|
|
406
456
|
public async getValidatorStats(
|
|
407
457
|
validatorAddress: EthAddress,
|
|
408
|
-
fromSlot?:
|
|
409
|
-
toSlot?:
|
|
458
|
+
fromSlot?: SlotNumber,
|
|
459
|
+
toSlot?: SlotNumber,
|
|
410
460
|
): Promise<SingleValidatorStats | undefined> {
|
|
411
461
|
const history = await this.store.getHistory(validatorAddress);
|
|
412
462
|
|
|
@@ -415,13 +465,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
415
465
|
}
|
|
416
466
|
|
|
417
467
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
418
|
-
const effectiveFromSlot =
|
|
468
|
+
const effectiveFromSlot =
|
|
469
|
+
fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
419
470
|
const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
|
|
420
471
|
|
|
421
472
|
const historyLength = BigInt(this.store.getHistoryLength());
|
|
422
|
-
if (effectiveToSlot - effectiveFromSlot > historyLength) {
|
|
473
|
+
if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
|
|
423
474
|
throw new Error(
|
|
424
|
-
`Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
|
|
475
|
+
`Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
|
|
425
476
|
`Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
|
|
426
477
|
);
|
|
427
478
|
}
|
|
@@ -432,11 +483,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
432
483
|
effectiveFromSlot,
|
|
433
484
|
effectiveToSlot,
|
|
434
485
|
);
|
|
435
|
-
const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
|
|
436
486
|
|
|
437
487
|
return {
|
|
438
488
|
validator,
|
|
439
|
-
allTimeProvenPerformance,
|
|
489
|
+
allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
|
|
440
490
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
441
491
|
initialSlot: this.initialSlot,
|
|
442
492
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -446,19 +496,21 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
446
496
|
protected computeStatsForValidator(
|
|
447
497
|
address: `0x${string}`,
|
|
448
498
|
allHistory: ValidatorStatusHistory,
|
|
449
|
-
fromSlot?:
|
|
450
|
-
toSlot?:
|
|
499
|
+
fromSlot?: SlotNumber,
|
|
500
|
+
toSlot?: SlotNumber,
|
|
451
501
|
): ValidatorStats {
|
|
452
|
-
let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
|
|
453
|
-
history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
|
|
454
|
-
const lastProposal = history
|
|
502
|
+
let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
|
|
503
|
+
history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
|
|
504
|
+
const lastProposal = history
|
|
505
|
+
.filter(h => h.status === 'checkpoint-proposed' || h.status === 'checkpoint-mined')
|
|
506
|
+
.at(-1);
|
|
455
507
|
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
456
508
|
return {
|
|
457
509
|
address: EthAddress.fromString(address),
|
|
458
510
|
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
459
511
|
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
460
512
|
totalSlots: history.length,
|
|
461
|
-
missedProposals: this.computeMissed(history, '
|
|
513
|
+
missedProposals: this.computeMissed(history, 'proposer', ['checkpoint-missed', 'blocks-missed']),
|
|
462
514
|
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
463
515
|
history,
|
|
464
516
|
};
|
|
@@ -466,10 +518,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
466
518
|
|
|
467
519
|
protected computeMissed(
|
|
468
520
|
history: ValidatorStatusHistory,
|
|
469
|
-
|
|
521
|
+
computeOverCategory: ValidatorStatusType | undefined,
|
|
470
522
|
filter: ValidatorStatusInSlot[],
|
|
471
523
|
) {
|
|
472
|
-
const relevantHistory = history.filter(
|
|
524
|
+
const relevantHistory = history.filter(
|
|
525
|
+
h => !computeOverCategory || statusToCategory(h.status) === computeOverCategory,
|
|
526
|
+
);
|
|
473
527
|
const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
|
|
474
528
|
return {
|
|
475
529
|
currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
|
|
@@ -479,7 +533,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
479
533
|
};
|
|
480
534
|
}
|
|
481
535
|
|
|
482
|
-
protected computeFromSlot(slot:
|
|
536
|
+
protected computeFromSlot(slot: SlotNumber | undefined) {
|
|
483
537
|
if (slot === undefined) {
|
|
484
538
|
return undefined;
|
|
485
539
|
}
|
package/src/sentinel/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
2
3
|
import { BufferReader, numToUInt8, numToUInt32BE, serializeToBuffer } from '@aztec/foundation/serialize';
|
|
3
4
|
import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
|
|
@@ -8,7 +9,7 @@ import type {
|
|
|
8
9
|
} from '@aztec/stdlib/validators';
|
|
9
10
|
|
|
10
11
|
export class SentinelStore {
|
|
11
|
-
public static readonly SCHEMA_VERSION =
|
|
12
|
+
public static readonly SCHEMA_VERSION = 3;
|
|
12
13
|
|
|
13
14
|
// a map from validator address to their ValidatorStatusHistory
|
|
14
15
|
private readonly historyMap: AztecAsyncMap<`0x${string}`, Buffer>;
|
|
@@ -33,7 +34,7 @@ export class SentinelStore {
|
|
|
33
34
|
return this.config.historicProvenPerformanceLength;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
public async updateProvenPerformance(epoch:
|
|
37
|
+
public async updateProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
|
|
37
38
|
await this.store.transactionAsync(async () => {
|
|
38
39
|
for (const [who, { missed, total }] of Object.entries(performance)) {
|
|
39
40
|
await this.pushValidatorProvenPerformanceForEpoch({ who: EthAddress.fromString(who), missed, total, epoch });
|
|
@@ -41,7 +42,7 @@ export class SentinelStore {
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch:
|
|
45
|
+
public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch: EpochNumber }[]> {
|
|
45
46
|
const currentPerformanceBuffer = await this.provenMap.getAsync(who.toString());
|
|
46
47
|
return currentPerformanceBuffer ? this.deserializePerformance(currentPerformanceBuffer) : [];
|
|
47
48
|
}
|
|
@@ -55,7 +56,7 @@ export class SentinelStore {
|
|
|
55
56
|
who: EthAddress;
|
|
56
57
|
missed: number;
|
|
57
58
|
total: number;
|
|
58
|
-
epoch:
|
|
59
|
+
epoch: EpochNumber;
|
|
59
60
|
}) {
|
|
60
61
|
const currentPerformance = await this.getProvenPerformance(who);
|
|
61
62
|
const existingIndex = currentPerformance.findIndex(p => p.epoch === epoch);
|
|
@@ -75,7 +76,7 @@ export class SentinelStore {
|
|
|
75
76
|
await this.provenMap.set(who.toString(), this.serializePerformance(performanceToKeep));
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
public async updateValidators(slot:
|
|
79
|
+
public async updateValidators(slot: SlotNumber, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
|
|
79
80
|
await this.store.transactionAsync(async () => {
|
|
80
81
|
for (const [who, status] of Object.entries(statuses)) {
|
|
81
82
|
if (status) {
|
|
@@ -85,11 +86,7 @@ export class SentinelStore {
|
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
private async pushValidatorStatusForSlot(
|
|
89
|
-
who: EthAddress,
|
|
90
|
-
slot: bigint,
|
|
91
|
-
status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
|
|
92
|
-
) {
|
|
89
|
+
private async pushValidatorStatusForSlot(who: EthAddress, slot: SlotNumber, status: ValidatorStatusInSlot) {
|
|
93
90
|
await this.store.transactionAsync(async () => {
|
|
94
91
|
const currentHistory = (await this.getHistory(who)) ?? [];
|
|
95
92
|
const newHistory = [...currentHistory, { slot, status }].slice(-this.config.historyLength);
|
|
@@ -110,18 +107,18 @@ export class SentinelStore {
|
|
|
110
107
|
return data && this.deserializeHistory(data);
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
private serializePerformance(performance: { missed: number; total: number; epoch:
|
|
110
|
+
private serializePerformance(performance: { missed: number; total: number; epoch: EpochNumber }[]): Buffer {
|
|
114
111
|
return serializeToBuffer(
|
|
115
112
|
performance.map(p => [numToUInt32BE(Number(p.epoch)), numToUInt32BE(p.missed), numToUInt32BE(p.total)]),
|
|
116
113
|
);
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch:
|
|
116
|
+
private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: EpochNumber }[] {
|
|
120
117
|
const reader = new BufferReader(buffer);
|
|
121
|
-
const performance: { missed: number; total: number; epoch:
|
|
118
|
+
const performance: { missed: number; total: number; epoch: EpochNumber }[] = [];
|
|
122
119
|
while (!reader.isEmpty()) {
|
|
123
120
|
performance.push({
|
|
124
|
-
epoch:
|
|
121
|
+
epoch: EpochNumber(reader.readNumber()),
|
|
125
122
|
missed: reader.readNumber(),
|
|
126
123
|
total: reader.readNumber(),
|
|
127
124
|
});
|
|
@@ -139,7 +136,7 @@ export class SentinelStore {
|
|
|
139
136
|
const reader = new BufferReader(buffer);
|
|
140
137
|
const history: ValidatorStatusHistory = [];
|
|
141
138
|
while (!reader.isEmpty()) {
|
|
142
|
-
const slot =
|
|
139
|
+
const slot = SlotNumber(reader.readNumber());
|
|
143
140
|
const status = this.statusFromNumber(reader.readUInt8());
|
|
144
141
|
history.push({ slot, status });
|
|
145
142
|
}
|
|
@@ -148,16 +145,18 @@ export class SentinelStore {
|
|
|
148
145
|
|
|
149
146
|
private statusToNumber(status: ValidatorStatusInSlot): number {
|
|
150
147
|
switch (status) {
|
|
151
|
-
case '
|
|
148
|
+
case 'checkpoint-mined':
|
|
152
149
|
return 1;
|
|
153
|
-
case '
|
|
150
|
+
case 'checkpoint-proposed':
|
|
154
151
|
return 2;
|
|
155
|
-
case '
|
|
152
|
+
case 'checkpoint-missed':
|
|
156
153
|
return 3;
|
|
157
154
|
case 'attestation-sent':
|
|
158
155
|
return 4;
|
|
159
156
|
case 'attestation-missed':
|
|
160
157
|
return 5;
|
|
158
|
+
case 'blocks-missed':
|
|
159
|
+
return 6;
|
|
161
160
|
default: {
|
|
162
161
|
const _exhaustive: never = status;
|
|
163
162
|
throw new Error(`Unknown status: ${status}`);
|
|
@@ -168,15 +167,17 @@ export class SentinelStore {
|
|
|
168
167
|
private statusFromNumber(status: number): ValidatorStatusInSlot {
|
|
169
168
|
switch (status) {
|
|
170
169
|
case 1:
|
|
171
|
-
return '
|
|
170
|
+
return 'checkpoint-mined';
|
|
172
171
|
case 2:
|
|
173
|
-
return '
|
|
172
|
+
return 'checkpoint-proposed';
|
|
174
173
|
case 3:
|
|
175
|
-
return '
|
|
174
|
+
return 'checkpoint-missed';
|
|
176
175
|
case 4:
|
|
177
176
|
return 'attestation-sent';
|
|
178
177
|
case 5:
|
|
179
178
|
return 'attestation-missed';
|
|
179
|
+
case 6:
|
|
180
|
+
return 'blocks-missed';
|
|
180
181
|
default:
|
|
181
182
|
throw new Error(`Unknown status: ${status}`);
|
|
182
183
|
}
|