@aztec/aztec-node 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107
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 +9 -4
- package/dest/aztec-node/config.d.ts.map +1 -1
- package/dest/aztec-node/config.js +22 -20
- package/dest/aztec-node/node_metrics.d.ts +5 -1
- package/dest/aztec-node/node_metrics.d.ts.map +1 -1
- package/dest/aztec-node/node_metrics.js +21 -0
- package/dest/aztec-node/server.d.ts +67 -48
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +584 -93
- package/dest/bin/index.d.ts +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/sentinel/config.d.ts +2 -1
- package/dest/sentinel/config.d.ts.map +1 -1
- package/dest/sentinel/config.js +16 -0
- package/dest/sentinel/factory.d.ts +1 -1
- package/dest/sentinel/factory.d.ts.map +1 -1
- package/dest/sentinel/factory.js +3 -1
- package/dest/sentinel/index.d.ts +1 -1
- package/dest/sentinel/sentinel.d.ts +20 -19
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +33 -21
- package/dest/sentinel/store.d.ts +8 -5
- package/dest/sentinel/store.d.ts.map +1 -1
- package/dest/sentinel/store.js +8 -4
- package/dest/test/index.d.ts +1 -1
- package/package.json +29 -28
- package/src/aztec-node/config.ts +36 -41
- package/src/aztec-node/node_metrics.ts +28 -0
- package/src/aztec-node/server.ts +268 -134
- package/src/sentinel/config.ts +18 -0
- package/src/sentinel/factory.ts +5 -1
- package/src/sentinel/sentinel.ts +60 -40
- package/src/sentinel/store.ts +18 -13
package/src/sentinel/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ConfigMappingsType, booleanConfigHelper, numberConfigHelper } from
|
|
|
2
2
|
|
|
3
3
|
export type SentinelConfig = {
|
|
4
4
|
sentinelHistoryLengthInEpochs: number;
|
|
5
|
+
sentinelHistoricProvenPerformanceLengthInEpochs: number;
|
|
5
6
|
sentinelEnabled: boolean;
|
|
6
7
|
};
|
|
7
8
|
|
|
@@ -11,6 +12,23 @@ export const sentinelConfigMappings: ConfigMappingsType<SentinelConfig> = {
|
|
|
11
12
|
env: 'SENTINEL_HISTORY_LENGTH_IN_EPOCHS',
|
|
12
13
|
...numberConfigHelper(24),
|
|
13
14
|
},
|
|
15
|
+
/**
|
|
16
|
+
* The number of L2 epochs kept of proven performance history for each validator.
|
|
17
|
+
* This value must be large enough so that we have proven performance for every validator
|
|
18
|
+
* for at least slashInactivityConsecutiveEpochThreshold. Assuming this value is 3,
|
|
19
|
+
* and the committee size is 48, and we have 10k validators, then we pick 48 out of 10k each draw.
|
|
20
|
+
* For any fixed element, per-draw prob = 48/10000 = 0.0048.
|
|
21
|
+
* After n draws, count ~ Binomial(n, 0.0048). We want P(X >= 3).
|
|
22
|
+
* Results (exact binomial):
|
|
23
|
+
* - 90% chance: n = 1108
|
|
24
|
+
* - 95% chance: n = 1310
|
|
25
|
+
* - 99% chance: n = 1749
|
|
26
|
+
*/
|
|
27
|
+
sentinelHistoricProvenPerformanceLengthInEpochs: {
|
|
28
|
+
description: 'The number of L2 epochs kept of proven performance history for each validator.',
|
|
29
|
+
env: 'SENTINEL_HISTORIC_PROVEN_PERFORMANCE_LENGTH_IN_EPOCHS',
|
|
30
|
+
...numberConfigHelper(2000),
|
|
31
|
+
},
|
|
14
32
|
sentinelEnabled: {
|
|
15
33
|
description: 'Whether the sentinel is enabled or not.',
|
|
16
34
|
env: 'SENTINEL_ENABLED',
|
package/src/sentinel/factory.ts
CHANGED
|
@@ -27,6 +27,10 @@ export async function createSentinel(
|
|
|
27
27
|
createLogger('node:sentinel:lmdb'),
|
|
28
28
|
);
|
|
29
29
|
const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
|
|
30
|
-
const
|
|
30
|
+
const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
|
|
31
|
+
const sentinelStore = new SentinelStore(kvStore, {
|
|
32
|
+
historyLength: storeHistoryLength,
|
|
33
|
+
historicProvenPerformanceLength: storeHistoricProvenPerformanceLength,
|
|
34
|
+
});
|
|
31
35
|
return new Sentinel(epochCache, archiver, p2p, sentinelStore, config, logger);
|
|
32
36
|
}
|
package/src/sentinel/sentinel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { BlockNumber, 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
|
+
getAttestationInfoFromPublishedL2Block,
|
|
22
23
|
} from '@aztec/stdlib/block';
|
|
23
24
|
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
25
|
import type {
|
|
@@ -40,9 +41,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
40
41
|
protected blockStream!: L2BlockStream;
|
|
41
42
|
protected l2TipsStore: L2TipsStore;
|
|
42
43
|
|
|
43
|
-
protected initialSlot:
|
|
44
|
-
protected lastProcessedSlot:
|
|
45
|
-
|
|
44
|
+
protected initialSlot: SlotNumber | undefined;
|
|
45
|
+
protected lastProcessedSlot: SlotNumber | undefined;
|
|
46
|
+
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
47
|
+
protected slotNumberToBlock: Map<SlotNumber, { blockNumber: BlockNumber; archive: string; attestors: EthAddress[] }> =
|
|
46
48
|
new Map();
|
|
47
49
|
|
|
48
50
|
constructor(
|
|
@@ -74,7 +76,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
74
76
|
/** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
|
|
75
77
|
protected async init() {
|
|
76
78
|
this.initialSlot = this.epochCache.getEpochAndSlotNow().slot;
|
|
77
|
-
const startingBlock = await this.archiver.getBlockNumber();
|
|
79
|
+
const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
|
|
78
80
|
this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
|
|
79
81
|
this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
|
|
80
82
|
}
|
|
@@ -89,9 +91,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
89
91
|
// Store mapping from slot to archive, block number, and attestors
|
|
90
92
|
for (const block of event.blocks) {
|
|
91
93
|
this.slotNumberToBlock.set(block.block.header.getSlot(), {
|
|
92
|
-
blockNumber: block.block.number,
|
|
94
|
+
blockNumber: BlockNumber(block.block.number),
|
|
93
95
|
archive: block.block.archive.root.toString(),
|
|
94
|
-
attestors:
|
|
96
|
+
attestors: getAttestationInfoFromPublishedL2Block(block)
|
|
97
|
+
.filter(a => a.status === 'recovered-from-signature')
|
|
98
|
+
.map(a => a.address!),
|
|
95
99
|
});
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -114,7 +118,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
114
118
|
if (event.type !== 'chain-proven') {
|
|
115
119
|
return;
|
|
116
120
|
}
|
|
117
|
-
const blockNumber = event.block.number;
|
|
121
|
+
const blockNumber = BlockNumber(event.block.number);
|
|
118
122
|
const block = await this.archiver.getBlock(blockNumber);
|
|
119
123
|
if (!block) {
|
|
120
124
|
this.logger.error(`Failed to get block ${blockNumber}`, { block });
|
|
@@ -132,7 +136,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
132
136
|
await this.handleProvenPerformance(epoch, performance);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
protected async computeProvenPerformance(epoch:
|
|
139
|
+
protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
|
|
136
140
|
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
137
141
|
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
138
142
|
if (!committee) {
|
|
@@ -140,7 +144,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
140
144
|
return {};
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
const stats = await this.computeStats({
|
|
147
|
+
const stats = await this.computeStats({
|
|
148
|
+
fromSlot,
|
|
149
|
+
toSlot,
|
|
150
|
+
validators: committee,
|
|
151
|
+
});
|
|
144
152
|
this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
|
|
145
153
|
|
|
146
154
|
// Note that we are NOT using the total slots in the epoch as `total` here, since we only
|
|
@@ -163,7 +171,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
163
171
|
*/
|
|
164
172
|
protected async checkPastInactivity(
|
|
165
173
|
validator: EthAddress,
|
|
166
|
-
currentEpoch:
|
|
174
|
+
currentEpoch: EpochNumber,
|
|
167
175
|
requiredConsecutiveEpochs: number,
|
|
168
176
|
): Promise<boolean> {
|
|
169
177
|
if (requiredConsecutiveEpochs === 0) {
|
|
@@ -173,23 +181,28 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
173
181
|
// Get all historical performance for this validator
|
|
174
182
|
const allPerformance = await this.store.getProvenPerformance(validator);
|
|
175
183
|
|
|
184
|
+
// Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
|
|
185
|
+
const pastEpochs = allPerformance.sort((a, b) => Number(b.epoch - a.epoch)).filter(p => p.epoch < currentEpoch);
|
|
186
|
+
|
|
176
187
|
// If we don't have enough historical data, don't slash
|
|
177
|
-
if (
|
|
188
|
+
if (pastEpochs.length < requiredConsecutiveEpochs) {
|
|
178
189
|
this.logger.debug(
|
|
179
190
|
`Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
|
|
180
191
|
);
|
|
181
192
|
return false;
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
//
|
|
185
|
-
return
|
|
186
|
-
.sort((a, b) => Number(b.epoch - a.epoch))
|
|
187
|
-
.filter(p => p.epoch < currentEpoch)
|
|
195
|
+
// Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
|
|
196
|
+
return pastEpochs
|
|
188
197
|
.slice(0, requiredConsecutiveEpochs)
|
|
189
198
|
.every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
protected async handleProvenPerformance(epoch:
|
|
201
|
+
protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
|
|
202
|
+
if (this.config.slashInactivityPenalty === 0n) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
193
206
|
const inactiveValidators = getEntries(performance)
|
|
194
207
|
.filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
|
|
195
208
|
.map(([address]) => address);
|
|
@@ -209,11 +222,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
209
222
|
validator: EthAddress.fromString(address),
|
|
210
223
|
amount: this.config.slashInactivityPenalty,
|
|
211
224
|
offenseType: OffenseType.INACTIVITY,
|
|
212
|
-
epochOrSlot: epoch,
|
|
225
|
+
epochOrSlot: BigInt(epoch),
|
|
213
226
|
}));
|
|
214
227
|
|
|
215
228
|
if (criminals.length > 0) {
|
|
216
|
-
this.logger.
|
|
229
|
+
this.logger.verbose(
|
|
217
230
|
`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
|
|
218
231
|
{ ...args, epochThreshold },
|
|
219
232
|
);
|
|
@@ -250,8 +263,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
250
263
|
* 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.
|
|
251
264
|
* Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
|
|
252
265
|
*/
|
|
253
|
-
protected async isReadyToProcess(currentSlot:
|
|
254
|
-
|
|
266
|
+
protected async isReadyToProcess(currentSlot: SlotNumber): Promise<SlotNumber | false> {
|
|
267
|
+
if (currentSlot < 2) {
|
|
268
|
+
this.logger.trace(`Current slot ${currentSlot} too early.`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const targetSlot = SlotNumber(currentSlot - 2);
|
|
255
273
|
if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
|
|
256
274
|
this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
|
|
257
275
|
return false;
|
|
@@ -268,7 +286,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
268
286
|
}
|
|
269
287
|
|
|
270
288
|
const archiverSlot = await this.archiver.getL2SlotNumber();
|
|
271
|
-
if (archiverSlot < targetSlot) {
|
|
289
|
+
if (archiverSlot === undefined || archiverSlot < targetSlot) {
|
|
272
290
|
this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, { archiverSlot, targetSlot });
|
|
273
291
|
return false;
|
|
274
292
|
}
|
|
@@ -288,7 +306,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
288
306
|
* Gathers committee and proposer data for a given slot, computes slot stats,
|
|
289
307
|
* and updates overall stats.
|
|
290
308
|
*/
|
|
291
|
-
protected async processSlot(slot:
|
|
309
|
+
protected async processSlot(slot: SlotNumber) {
|
|
292
310
|
const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
|
|
293
311
|
if (!committee || committee.length === 0) {
|
|
294
312
|
this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
|
|
@@ -304,7 +322,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
304
322
|
}
|
|
305
323
|
|
|
306
324
|
/** Computes activity for a given slot. */
|
|
307
|
-
protected async getSlotActivity(slot:
|
|
325
|
+
protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
|
|
308
326
|
this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
|
|
309
327
|
|
|
310
328
|
// Check if there is an L2 block in L1 for this L2 slot
|
|
@@ -315,8 +333,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
315
333
|
// (contains the ones synced from mined blocks, which we may have missed from p2p).
|
|
316
334
|
const block = this.slotNumberToBlock.get(slot);
|
|
317
335
|
const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
|
|
336
|
+
// Filter out attestations with invalid signatures
|
|
337
|
+
const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
|
|
318
338
|
const attestors = new Set(
|
|
319
|
-
[...
|
|
339
|
+
[...p2pAttestors.map(a => a.toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
320
340
|
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
321
341
|
),
|
|
322
342
|
);
|
|
@@ -364,7 +384,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
364
384
|
}
|
|
365
385
|
|
|
366
386
|
/** Push the status for each slot for each validator. */
|
|
367
|
-
protected updateValidators(slot:
|
|
387
|
+
protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
|
|
368
388
|
return this.store.updateValidators(slot, stats);
|
|
369
389
|
}
|
|
370
390
|
|
|
@@ -373,13 +393,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
373
393
|
fromSlot,
|
|
374
394
|
toSlot,
|
|
375
395
|
validators,
|
|
376
|
-
}: { fromSlot?:
|
|
396
|
+
}: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
|
|
377
397
|
const histories = validators
|
|
378
398
|
? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
|
|
379
399
|
: await this.store.getHistories();
|
|
380
400
|
|
|
381
401
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
382
|
-
fromSlot ??= (this.lastProcessedSlot ?? slotNow) -
|
|
402
|
+
fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
383
403
|
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
384
404
|
|
|
385
405
|
const stats = mapValues(histories, (history, address) =>
|
|
@@ -397,8 +417,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
397
417
|
/** Computes stats for a single validator. */
|
|
398
418
|
public async getValidatorStats(
|
|
399
419
|
validatorAddress: EthAddress,
|
|
400
|
-
fromSlot?:
|
|
401
|
-
toSlot?:
|
|
420
|
+
fromSlot?: SlotNumber,
|
|
421
|
+
toSlot?: SlotNumber,
|
|
402
422
|
): Promise<SingleValidatorStats | undefined> {
|
|
403
423
|
const history = await this.store.getHistory(validatorAddress);
|
|
404
424
|
|
|
@@ -407,13 +427,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
407
427
|
}
|
|
408
428
|
|
|
409
429
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
410
|
-
const effectiveFromSlot =
|
|
430
|
+
const effectiveFromSlot =
|
|
431
|
+
fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
411
432
|
const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
|
|
412
433
|
|
|
413
434
|
const historyLength = BigInt(this.store.getHistoryLength());
|
|
414
|
-
if (effectiveToSlot - effectiveFromSlot > historyLength) {
|
|
435
|
+
if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
|
|
415
436
|
throw new Error(
|
|
416
|
-
`Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
|
|
437
|
+
`Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
|
|
417
438
|
`Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
|
|
418
439
|
);
|
|
419
440
|
}
|
|
@@ -424,11 +445,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
424
445
|
effectiveFromSlot,
|
|
425
446
|
effectiveToSlot,
|
|
426
447
|
);
|
|
427
|
-
const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
|
|
428
448
|
|
|
429
449
|
return {
|
|
430
450
|
validator,
|
|
431
|
-
allTimeProvenPerformance,
|
|
451
|
+
allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
|
|
432
452
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
433
453
|
initialSlot: this.initialSlot,
|
|
434
454
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -438,11 +458,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
438
458
|
protected computeStatsForValidator(
|
|
439
459
|
address: `0x${string}`,
|
|
440
460
|
allHistory: ValidatorStatusHistory,
|
|
441
|
-
fromSlot?:
|
|
442
|
-
toSlot?:
|
|
461
|
+
fromSlot?: SlotNumber,
|
|
462
|
+
toSlot?: SlotNumber,
|
|
443
463
|
): ValidatorStats {
|
|
444
|
-
let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
|
|
445
|
-
history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
|
|
464
|
+
let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
|
|
465
|
+
history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
|
|
446
466
|
const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
|
|
447
467
|
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
448
468
|
return {
|
|
@@ -471,7 +491,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
471
491
|
};
|
|
472
492
|
}
|
|
473
493
|
|
|
474
|
-
protected computeFromSlot(slot:
|
|
494
|
+
protected computeFromSlot(slot: SlotNumber | undefined) {
|
|
475
495
|
if (slot === undefined) {
|
|
476
496
|
return undefined;
|
|
477
497
|
}
|
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';
|
|
@@ -19,7 +20,7 @@ export class SentinelStore {
|
|
|
19
20
|
|
|
20
21
|
constructor(
|
|
21
22
|
private store: AztecAsyncKVStore,
|
|
22
|
-
private config: { historyLength: number },
|
|
23
|
+
private config: { historyLength: number; historicProvenPerformanceLength: number },
|
|
23
24
|
) {
|
|
24
25
|
this.historyMap = store.openMap('sentinel-validator-status');
|
|
25
26
|
this.provenMap = store.openMap('sentinel-validator-proven');
|
|
@@ -29,7 +30,11 @@ export class SentinelStore {
|
|
|
29
30
|
return this.config.historyLength;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
public
|
|
33
|
+
public getHistoricProvenPerformanceLength() {
|
|
34
|
+
return this.config.historicProvenPerformanceLength;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public async updateProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
|
|
33
38
|
await this.store.transactionAsync(async () => {
|
|
34
39
|
for (const [who, { missed, total }] of Object.entries(performance)) {
|
|
35
40
|
await this.pushValidatorProvenPerformanceForEpoch({ who: EthAddress.fromString(who), missed, total, epoch });
|
|
@@ -37,7 +42,7 @@ export class SentinelStore {
|
|
|
37
42
|
});
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch:
|
|
45
|
+
public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch: EpochNumber }[]> {
|
|
41
46
|
const currentPerformanceBuffer = await this.provenMap.getAsync(who.toString());
|
|
42
47
|
return currentPerformanceBuffer ? this.deserializePerformance(currentPerformanceBuffer) : [];
|
|
43
48
|
}
|
|
@@ -51,7 +56,7 @@ export class SentinelStore {
|
|
|
51
56
|
who: EthAddress;
|
|
52
57
|
missed: number;
|
|
53
58
|
total: number;
|
|
54
|
-
epoch:
|
|
59
|
+
epoch: EpochNumber;
|
|
55
60
|
}) {
|
|
56
61
|
const currentPerformance = await this.getProvenPerformance(who);
|
|
57
62
|
const existingIndex = currentPerformance.findIndex(p => p.epoch === epoch);
|
|
@@ -65,13 +70,13 @@ export class SentinelStore {
|
|
|
65
70
|
// Since we keep the size small, this is not a big deal.
|
|
66
71
|
currentPerformance.sort((a, b) => Number(a.epoch - b.epoch));
|
|
67
72
|
|
|
68
|
-
// keep the most recent `
|
|
69
|
-
const performanceToKeep = currentPerformance.slice(-this.config.
|
|
73
|
+
// keep the most recent `historicProvenPerformanceLength` entries.
|
|
74
|
+
const performanceToKeep = currentPerformance.slice(-this.config.historicProvenPerformanceLength);
|
|
70
75
|
|
|
71
76
|
await this.provenMap.set(who.toString(), this.serializePerformance(performanceToKeep));
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
public async updateValidators(slot:
|
|
79
|
+
public async updateValidators(slot: SlotNumber, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
|
|
75
80
|
await this.store.transactionAsync(async () => {
|
|
76
81
|
for (const [who, status] of Object.entries(statuses)) {
|
|
77
82
|
if (status) {
|
|
@@ -83,7 +88,7 @@ export class SentinelStore {
|
|
|
83
88
|
|
|
84
89
|
private async pushValidatorStatusForSlot(
|
|
85
90
|
who: EthAddress,
|
|
86
|
-
slot:
|
|
91
|
+
slot: SlotNumber,
|
|
87
92
|
status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
|
|
88
93
|
) {
|
|
89
94
|
await this.store.transactionAsync(async () => {
|
|
@@ -106,18 +111,18 @@ export class SentinelStore {
|
|
|
106
111
|
return data && this.deserializeHistory(data);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
private serializePerformance(performance: { missed: number; total: number; epoch:
|
|
114
|
+
private serializePerformance(performance: { missed: number; total: number; epoch: EpochNumber }[]): Buffer {
|
|
110
115
|
return serializeToBuffer(
|
|
111
116
|
performance.map(p => [numToUInt32BE(Number(p.epoch)), numToUInt32BE(p.missed), numToUInt32BE(p.total)]),
|
|
112
117
|
);
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch:
|
|
120
|
+
private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: EpochNumber }[] {
|
|
116
121
|
const reader = new BufferReader(buffer);
|
|
117
|
-
const performance: { missed: number; total: number; epoch:
|
|
122
|
+
const performance: { missed: number; total: number; epoch: EpochNumber }[] = [];
|
|
118
123
|
while (!reader.isEmpty()) {
|
|
119
124
|
performance.push({
|
|
120
|
-
epoch:
|
|
125
|
+
epoch: EpochNumber(reader.readNumber()),
|
|
121
126
|
missed: reader.readNumber(),
|
|
122
127
|
total: reader.readNumber(),
|
|
123
128
|
});
|
|
@@ -135,7 +140,7 @@ export class SentinelStore {
|
|
|
135
140
|
const reader = new BufferReader(buffer);
|
|
136
141
|
const history: ValidatorStatusHistory = [];
|
|
137
142
|
while (!reader.isEmpty()) {
|
|
138
|
-
const slot =
|
|
143
|
+
const slot = SlotNumber(reader.readNumber());
|
|
139
144
|
const status = this.statusFromNumber(reader.readUInt8());
|
|
140
145
|
history.push({ slot, status });
|
|
141
146
|
}
|