@aztec/aztec-node 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.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/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 +57 -37
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +163 -65
- 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 +23 -21
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +86 -80
- 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 +230 -119
- package/src/sentinel/config.ts +18 -0
- package/src/sentinel/factory.ts +5 -1
- package/src/sentinel/sentinel.ts +123 -106
- 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,20 +1,27 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import {
|
|
2
|
+
import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
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';
|
|
5
6
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
6
7
|
import { L2TipsMemoryStore, type L2TipsStore } from '@aztec/kv-store/stores';
|
|
7
8
|
import type { P2PClient } from '@aztec/p2p';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
OffenseType,
|
|
11
|
+
WANT_TO_SLASH_EVENT,
|
|
12
|
+
type WantToSlashArgs,
|
|
13
|
+
type Watcher,
|
|
14
|
+
type WatcherEmitter,
|
|
15
|
+
} from '@aztec/slasher';
|
|
9
16
|
import type { SlasherConfig } from '@aztec/slasher/config';
|
|
10
17
|
import {
|
|
11
18
|
type L2BlockSource,
|
|
12
19
|
L2BlockStream,
|
|
13
20
|
type L2BlockStreamEvent,
|
|
14
21
|
type L2BlockStreamEventHandler,
|
|
15
|
-
|
|
22
|
+
getAttestationInfoFromPublishedL2Block,
|
|
16
23
|
} from '@aztec/stdlib/block';
|
|
17
|
-
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
|
+
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
18
25
|
import type {
|
|
19
26
|
SingleValidatorStats,
|
|
20
27
|
ValidatorStats,
|
|
@@ -34,9 +41,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
34
41
|
protected blockStream!: L2BlockStream;
|
|
35
42
|
protected l2TipsStore: L2TipsStore;
|
|
36
43
|
|
|
37
|
-
protected initialSlot:
|
|
38
|
-
protected lastProcessedSlot:
|
|
39
|
-
|
|
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[] }> =
|
|
40
48
|
new Map();
|
|
41
49
|
|
|
42
50
|
constructor(
|
|
@@ -68,7 +76,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
68
76
|
/** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
|
|
69
77
|
protected async init() {
|
|
70
78
|
this.initialSlot = this.epochCache.getEpochAndSlotNow().slot;
|
|
71
|
-
const startingBlock = await this.archiver.getBlockNumber();
|
|
79
|
+
const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
|
|
72
80
|
this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
|
|
73
81
|
this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
|
|
74
82
|
}
|
|
@@ -83,9 +91,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
83
91
|
// Store mapping from slot to archive, block number, and attestors
|
|
84
92
|
for (const block of event.blocks) {
|
|
85
93
|
this.slotNumberToBlock.set(block.block.header.getSlot(), {
|
|
86
|
-
blockNumber: block.block.number,
|
|
94
|
+
blockNumber: BlockNumber(block.block.number),
|
|
87
95
|
archive: block.block.archive.root.toString(),
|
|
88
|
-
attestors:
|
|
96
|
+
attestors: getAttestationInfoFromPublishedL2Block(block)
|
|
97
|
+
.filter(a => a.status === 'recovered-from-signature')
|
|
98
|
+
.map(a => a.address!),
|
|
89
99
|
});
|
|
90
100
|
}
|
|
91
101
|
|
|
@@ -108,60 +118,49 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
108
118
|
if (event.type !== 'chain-proven') {
|
|
109
119
|
return;
|
|
110
120
|
}
|
|
111
|
-
const blockNumber = event.block.number;
|
|
121
|
+
const blockNumber = BlockNumber(event.block.number);
|
|
112
122
|
const block = await this.archiver.getBlock(blockNumber);
|
|
113
123
|
if (!block) {
|
|
114
124
|
this.logger.error(`Failed to get block ${blockNumber}`, { block });
|
|
115
125
|
return;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
// TODO(palla/slash): We should only be computing proven performance if this is
|
|
129
|
+
// a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
|
|
130
|
+
const epoch = getEpochAtSlot(block.header.getSlot(), this.epochCache.getL1Constants());
|
|
119
131
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
120
132
|
const performance = await this.computeProvenPerformance(epoch);
|
|
121
133
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
122
134
|
|
|
123
|
-
await this.updateProvenPerformance(epoch, performance);
|
|
135
|
+
await this.store.updateProvenPerformance(epoch, performance);
|
|
124
136
|
await this.handleProvenPerformance(epoch, performance);
|
|
125
137
|
}
|
|
126
138
|
|
|
127
|
-
protected async computeProvenPerformance(epoch:
|
|
128
|
-
const
|
|
129
|
-
const provenSlots = headers.map(h => h.getSlot());
|
|
130
|
-
const fromSlot = provenSlots[0];
|
|
131
|
-
const toSlot = provenSlots[provenSlots.length - 1];
|
|
139
|
+
protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
|
|
140
|
+
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
132
141
|
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
133
142
|
if (!committee) {
|
|
134
143
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
135
144
|
return {};
|
|
136
145
|
}
|
|
137
|
-
const stats = await this.computeStats({ fromSlot, toSlot });
|
|
138
|
-
this.logger.debug(`Stats for epoch ${epoch}`, stats);
|
|
139
|
-
|
|
140
|
-
const performance: ValidatorsEpochPerformance = {};
|
|
141
|
-
for (const validator of Object.keys(stats.stats)) {
|
|
142
|
-
let address;
|
|
143
|
-
try {
|
|
144
|
-
address = EthAddress.fromString(validator);
|
|
145
|
-
} catch (e) {
|
|
146
|
-
this.logger.error(`Invalid validator address ${validator}`, e);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (!committee.find(v => v.equals(address))) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let missed = 0;
|
|
153
|
-
for (const history of stats.stats[validator].history) {
|
|
154
|
-
if (provenSlots.includes(history.slot) && history.status === 'attestation-missed') {
|
|
155
|
-
missed++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
performance[address.toString()] = { missed, total: provenSlots.length };
|
|
159
|
-
}
|
|
160
|
-
return performance;
|
|
161
|
-
}
|
|
162
146
|
|
|
163
|
-
|
|
164
|
-
|
|
147
|
+
const stats = await this.computeStats({
|
|
148
|
+
fromSlot,
|
|
149
|
+
toSlot,
|
|
150
|
+
validators: committee,
|
|
151
|
+
});
|
|
152
|
+
this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
|
|
153
|
+
|
|
154
|
+
// Note that we are NOT using the total slots in the epoch as `total` here, since we only
|
|
155
|
+
// compute missed attestations over the blocks that had a proposal in them. So, let's say
|
|
156
|
+
// we have an epoch with 10 slots, but only 5 had a block proposal. A validator that was
|
|
157
|
+
// offline, assuming they were not picked as proposer, will then be reported as having missed
|
|
158
|
+
// 5/5 attestations. If we used the total, they'd be reported as 5/10, which would probably
|
|
159
|
+
// allow them to avoid being slashed.
|
|
160
|
+
return mapValues(stats.stats, stat => ({
|
|
161
|
+
missed: stat.missedAttestations.count + stat.missedProposals.count,
|
|
162
|
+
total: stat.missedAttestations.total + stat.missedProposals.total,
|
|
163
|
+
}));
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
/**
|
|
@@ -172,7 +171,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
172
171
|
*/
|
|
173
172
|
protected async checkPastInactivity(
|
|
174
173
|
validator: EthAddress,
|
|
175
|
-
currentEpoch:
|
|
174
|
+
currentEpoch: EpochNumber,
|
|
176
175
|
requiredConsecutiveEpochs: number,
|
|
177
176
|
): Promise<boolean> {
|
|
178
177
|
if (requiredConsecutiveEpochs === 0) {
|
|
@@ -182,28 +181,31 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
182
181
|
// Get all historical performance for this validator
|
|
183
182
|
const allPerformance = await this.store.getProvenPerformance(validator);
|
|
184
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
|
+
|
|
185
187
|
// If we don't have enough historical data, don't slash
|
|
186
|
-
if (
|
|
188
|
+
if (pastEpochs.length < requiredConsecutiveEpochs) {
|
|
187
189
|
this.logger.debug(
|
|
188
190
|
`Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
|
|
189
191
|
);
|
|
190
192
|
return false;
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
//
|
|
194
|
-
return
|
|
195
|
-
.sort((a, b) => Number(b.epoch - a.epoch))
|
|
196
|
-
.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
|
|
197
197
|
.slice(0, requiredConsecutiveEpochs)
|
|
198
198
|
.every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
protected async handleProvenPerformance(epoch:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
201
|
+
protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
|
|
202
|
+
if (this.config.slashInactivityPenalty === 0n) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const inactiveValidators = getEntries(performance)
|
|
207
|
+
.filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
|
|
208
|
+
.map(([address]) => address);
|
|
207
209
|
|
|
208
210
|
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
209
211
|
inactiveValidators,
|
|
@@ -216,15 +218,15 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
216
218
|
this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1),
|
|
217
219
|
);
|
|
218
220
|
|
|
219
|
-
const args = criminals.map(address => ({
|
|
221
|
+
const args: WantToSlashArgs[] = criminals.map(address => ({
|
|
220
222
|
validator: EthAddress.fromString(address),
|
|
221
223
|
amount: this.config.slashInactivityPenalty,
|
|
222
224
|
offenseType: OffenseType.INACTIVITY,
|
|
223
|
-
epochOrSlot: epoch,
|
|
225
|
+
epochOrSlot: BigInt(epoch),
|
|
224
226
|
}));
|
|
225
227
|
|
|
226
228
|
if (criminals.length > 0) {
|
|
227
|
-
this.logger.
|
|
229
|
+
this.logger.verbose(
|
|
228
230
|
`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
|
|
229
231
|
{ ...args, epochThreshold },
|
|
230
232
|
);
|
|
@@ -261,8 +263,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
261
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.
|
|
262
264
|
* Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
|
|
263
265
|
*/
|
|
264
|
-
protected async isReadyToProcess(currentSlot:
|
|
265
|
-
|
|
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);
|
|
266
273
|
if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
|
|
267
274
|
this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
|
|
268
275
|
return false;
|
|
@@ -279,7 +286,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
const archiverSlot = await this.archiver.getL2SlotNumber();
|
|
282
|
-
if (archiverSlot < targetSlot) {
|
|
289
|
+
if (archiverSlot === undefined || archiverSlot < targetSlot) {
|
|
283
290
|
this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, { archiverSlot, targetSlot });
|
|
284
291
|
return false;
|
|
285
292
|
}
|
|
@@ -299,7 +306,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
299
306
|
* Gathers committee and proposer data for a given slot, computes slot stats,
|
|
300
307
|
* and updates overall stats.
|
|
301
308
|
*/
|
|
302
|
-
protected async processSlot(slot:
|
|
309
|
+
protected async processSlot(slot: SlotNumber) {
|
|
303
310
|
const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
|
|
304
311
|
if (!committee || committee.length === 0) {
|
|
305
312
|
this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
|
|
@@ -315,7 +322,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
/** Computes activity for a given slot. */
|
|
318
|
-
protected async getSlotActivity(slot:
|
|
325
|
+
protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
|
|
319
326
|
this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
|
|
320
327
|
|
|
321
328
|
// Check if there is an L2 block in L1 for this L2 slot
|
|
@@ -326,15 +333,20 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
326
333
|
// (contains the ones synced from mined blocks, which we may have missed from p2p).
|
|
327
334
|
const block = this.slotNumberToBlock.get(slot);
|
|
328
335
|
const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
336
|
+
// Filter out attestations with invalid signatures
|
|
337
|
+
const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
|
|
338
|
+
const attestors = new Set(
|
|
339
|
+
[...p2pAttestors.map(a => a.toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
340
|
+
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
341
|
+
),
|
|
342
|
+
);
|
|
333
343
|
|
|
334
|
-
// We assume that there was a block proposal if at least one of the validators attested to it.
|
|
344
|
+
// We assume that there was a block proposal if at least one of the validators (other than the proposer) attested to it.
|
|
335
345
|
// It could be the case that every single validator failed, and we could differentiate it by having
|
|
336
346
|
// this node re-execute every block proposal it sees and storing it in the attestation pool.
|
|
337
347
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
348
|
+
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
349
|
+
// since they will attest to their own proposal it even if it's not re-executable.
|
|
338
350
|
const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
|
|
339
351
|
this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, { ...block, slot });
|
|
340
352
|
|
|
@@ -372,26 +384,30 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
372
384
|
}
|
|
373
385
|
|
|
374
386
|
/** Push the status for each slot for each validator. */
|
|
375
|
-
protected updateValidators(slot:
|
|
387
|
+
protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
|
|
376
388
|
return this.store.updateValidators(slot, stats);
|
|
377
389
|
}
|
|
378
390
|
|
|
379
391
|
/** Computes stats to be returned based on stored data. */
|
|
380
392
|
public async computeStats({
|
|
381
|
-
fromSlot
|
|
382
|
-
toSlot
|
|
383
|
-
|
|
384
|
-
|
|
393
|
+
fromSlot,
|
|
394
|
+
toSlot,
|
|
395
|
+
validators,
|
|
396
|
+
}: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
|
|
397
|
+
const histories = validators
|
|
398
|
+
? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
|
|
399
|
+
: await this.store.getHistories();
|
|
400
|
+
|
|
385
401
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
402
|
+
fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
403
|
+
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
404
|
+
|
|
405
|
+
const stats = mapValues(histories, (history, address) =>
|
|
406
|
+
this.computeStatsForValidator(address, history ?? [], fromSlot, toSlot),
|
|
407
|
+
);
|
|
408
|
+
|
|
393
409
|
return {
|
|
394
|
-
stats
|
|
410
|
+
stats,
|
|
395
411
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
396
412
|
initialSlot: this.initialSlot,
|
|
397
413
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -401,8 +417,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
401
417
|
/** Computes stats for a single validator. */
|
|
402
418
|
public async getValidatorStats(
|
|
403
419
|
validatorAddress: EthAddress,
|
|
404
|
-
fromSlot?:
|
|
405
|
-
toSlot?:
|
|
420
|
+
fromSlot?: SlotNumber,
|
|
421
|
+
toSlot?: SlotNumber,
|
|
406
422
|
): Promise<SingleValidatorStats | undefined> {
|
|
407
423
|
const history = await this.store.getHistory(validatorAddress);
|
|
408
424
|
|
|
@@ -411,13 +427,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
411
427
|
}
|
|
412
428
|
|
|
413
429
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
414
|
-
const effectiveFromSlot =
|
|
430
|
+
const effectiveFromSlot =
|
|
431
|
+
fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
415
432
|
const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
|
|
416
433
|
|
|
417
434
|
const historyLength = BigInt(this.store.getHistoryLength());
|
|
418
|
-
if (effectiveToSlot - effectiveFromSlot > historyLength) {
|
|
435
|
+
if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
|
|
419
436
|
throw new Error(
|
|
420
|
-
`Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
|
|
437
|
+
`Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
|
|
421
438
|
`Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
|
|
422
439
|
);
|
|
423
440
|
}
|
|
@@ -428,11 +445,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
428
445
|
effectiveFromSlot,
|
|
429
446
|
effectiveToSlot,
|
|
430
447
|
);
|
|
431
|
-
const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
|
|
432
448
|
|
|
433
449
|
return {
|
|
434
450
|
validator,
|
|
435
|
-
allTimeProvenPerformance,
|
|
451
|
+
allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
|
|
436
452
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
437
453
|
initialSlot: this.initialSlot,
|
|
438
454
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -442,39 +458,40 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
442
458
|
protected computeStatsForValidator(
|
|
443
459
|
address: `0x${string}`,
|
|
444
460
|
allHistory: ValidatorStatusHistory,
|
|
445
|
-
fromSlot?:
|
|
446
|
-
toSlot?:
|
|
461
|
+
fromSlot?: SlotNumber,
|
|
462
|
+
toSlot?: SlotNumber,
|
|
447
463
|
): ValidatorStats {
|
|
448
|
-
let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
|
|
449
|
-
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;
|
|
466
|
+
const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
|
|
467
|
+
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
450
468
|
return {
|
|
451
469
|
address: EthAddress.fromString(address),
|
|
452
|
-
lastProposal: this.computeFromSlot(
|
|
453
|
-
|
|
454
|
-
),
|
|
455
|
-
lastAttestation: this.computeFromSlot(history.filter(h => h.status === 'attestation-sent').at(-1)?.slot),
|
|
470
|
+
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
471
|
+
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
456
472
|
totalSlots: history.length,
|
|
457
|
-
missedProposals: this.computeMissed(history, 'block', 'block-missed'),
|
|
458
|
-
missedAttestations: this.computeMissed(history, 'attestation', 'attestation-missed'),
|
|
473
|
+
missedProposals: this.computeMissed(history, 'block', ['block-missed']),
|
|
474
|
+
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
459
475
|
history,
|
|
460
476
|
};
|
|
461
477
|
}
|
|
462
478
|
|
|
463
479
|
protected computeMissed(
|
|
464
480
|
history: ValidatorStatusHistory,
|
|
465
|
-
computeOverPrefix: ValidatorStatusType,
|
|
466
|
-
filter: ValidatorStatusInSlot,
|
|
481
|
+
computeOverPrefix: ValidatorStatusType | undefined,
|
|
482
|
+
filter: ValidatorStatusInSlot[],
|
|
467
483
|
) {
|
|
468
|
-
const relevantHistory = history.filter(h => h.status.startsWith(computeOverPrefix));
|
|
469
|
-
const filteredHistory = relevantHistory.filter(h => h.status
|
|
484
|
+
const relevantHistory = history.filter(h => !computeOverPrefix || h.status.startsWith(computeOverPrefix));
|
|
485
|
+
const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
|
|
470
486
|
return {
|
|
471
|
-
currentStreak: countWhile([...relevantHistory].reverse(), h => h.status
|
|
487
|
+
currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
|
|
472
488
|
rate: relevantHistory.length === 0 ? undefined : filteredHistory.length / relevantHistory.length,
|
|
473
489
|
count: filteredHistory.length,
|
|
490
|
+
total: relevantHistory.length,
|
|
474
491
|
};
|
|
475
492
|
}
|
|
476
493
|
|
|
477
|
-
protected computeFromSlot(slot:
|
|
494
|
+
protected computeFromSlot(slot: SlotNumber | undefined) {
|
|
478
495
|
if (slot === undefined) {
|
|
479
496
|
return undefined;
|
|
480
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
|
}
|