@aztec/aztec-node 3.0.0-nightly.20250905 → 4.0.0-nightly.20250907
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.
|
@@ -788,7 +788,7 @@ import { NodeMetrics } from './node_metrics.js';
|
|
|
788
788
|
...this.config,
|
|
789
789
|
...config
|
|
790
790
|
};
|
|
791
|
-
this.sequencer?.
|
|
791
|
+
this.sequencer?.updateConfig(config);
|
|
792
792
|
this.slasherClient?.updateConfig(config);
|
|
793
793
|
this.validatorsSentinel?.updateConfig(config);
|
|
794
794
|
// this.blockBuilder.updateConfig(config); // TODO: Spyros has a PR to add the builder to `this`, so we can do this
|
|
@@ -35,7 +35,6 @@ export declare class Sentinel extends Sentinel_base implements L2BlockStreamEven
|
|
|
35
35
|
handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void>;
|
|
36
36
|
protected handleChainProven(event: L2BlockStreamEvent): Promise<void>;
|
|
37
37
|
protected computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance>;
|
|
38
|
-
protected updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): Promise<void>;
|
|
39
38
|
/**
|
|
40
39
|
* Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
|
|
41
40
|
* @param validator The validator address to check
|
|
@@ -68,17 +67,19 @@ export declare class Sentinel extends Sentinel_base implements L2BlockStreamEven
|
|
|
68
67
|
/** Push the status for each slot for each validator. */
|
|
69
68
|
protected updateValidators(slot: bigint, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>): Promise<void>;
|
|
70
69
|
/** Computes stats to be returned based on stored data. */
|
|
71
|
-
computeStats({ fromSlot
|
|
70
|
+
computeStats({ fromSlot, toSlot, validators, }?: {
|
|
72
71
|
fromSlot?: bigint;
|
|
73
72
|
toSlot?: bigint;
|
|
73
|
+
validators?: EthAddress[];
|
|
74
74
|
}): Promise<ValidatorsStats>;
|
|
75
75
|
/** Computes stats for a single validator. */
|
|
76
76
|
getValidatorStats(validatorAddress: EthAddress, fromSlot?: bigint, toSlot?: bigint): Promise<SingleValidatorStats | undefined>;
|
|
77
77
|
protected computeStatsForValidator(address: `0x${string}`, allHistory: ValidatorStatusHistory, fromSlot?: bigint, toSlot?: bigint): ValidatorStats;
|
|
78
|
-
protected computeMissed(history: ValidatorStatusHistory, computeOverPrefix: ValidatorStatusType, filter: ValidatorStatusInSlot): {
|
|
78
|
+
protected computeMissed(history: ValidatorStatusHistory, computeOverPrefix: ValidatorStatusType | undefined, filter: ValidatorStatusInSlot[]): {
|
|
79
79
|
currentStreak: number;
|
|
80
80
|
rate: number | undefined;
|
|
81
81
|
count: number;
|
|
82
|
+
total: number;
|
|
82
83
|
};
|
|
83
84
|
protected computeFromSlot(slot: bigint | undefined): {
|
|
84
85
|
timestamp: bigint;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../../src/sentinel/sentinel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,
|
|
1
|
+
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../../src/sentinel/sentinel.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { countWhile, filterAsync } from '@aztec/foundation/collection';
|
|
1
|
+
import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection';
|
|
2
2
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
4
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
5
5
|
import { L2TipsMemoryStore } from '@aztec/kv-store/stores';
|
|
6
6
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
7
7
|
import { L2BlockStream, getAttestationsFromPublishedL2Block } from '@aztec/stdlib/block';
|
|
8
|
-
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
8
|
+
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
9
9
|
import EventEmitter from 'node:events';
|
|
10
10
|
export class Sentinel extends EventEmitter {
|
|
11
11
|
epochCache;
|
|
@@ -82,18 +82,17 @@ export class Sentinel extends EventEmitter {
|
|
|
82
82
|
});
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
// TODO(palla/slash): We should only be computing proven performance if this is
|
|
86
|
+
// a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
|
|
87
|
+
const epoch = getEpochAtSlot(block.header.getSlot(), this.epochCache.getL1Constants());
|
|
86
88
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
87
89
|
const performance = await this.computeProvenPerformance(epoch);
|
|
88
90
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
89
|
-
await this.updateProvenPerformance(epoch, performance);
|
|
91
|
+
await this.store.updateProvenPerformance(epoch, performance);
|
|
90
92
|
await this.handleProvenPerformance(epoch, performance);
|
|
91
93
|
}
|
|
92
94
|
async computeProvenPerformance(epoch) {
|
|
93
|
-
const
|
|
94
|
-
const provenSlots = headers.map((h)=>h.getSlot());
|
|
95
|
-
const fromSlot = provenSlots[0];
|
|
96
|
-
const toSlot = provenSlots[provenSlots.length - 1];
|
|
95
|
+
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
97
96
|
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
98
97
|
if (!committee) {
|
|
99
98
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
@@ -101,36 +100,25 @@ export class Sentinel extends EventEmitter {
|
|
|
101
100
|
}
|
|
102
101
|
const stats = await this.computeStats({
|
|
103
102
|
fromSlot,
|
|
104
|
-
toSlot
|
|
103
|
+
toSlot,
|
|
104
|
+
validators: committee
|
|
105
105
|
});
|
|
106
|
-
this.logger.debug(`Stats for epoch ${epoch}`,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
missed++;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
performance[address.toString()] = {
|
|
126
|
-
missed,
|
|
127
|
-
total: provenSlots.length
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
return performance;
|
|
131
|
-
}
|
|
132
|
-
updateProvenPerformance(epoch, performance) {
|
|
133
|
-
return this.store.updateProvenPerformance(epoch, performance);
|
|
106
|
+
this.logger.debug(`Stats for epoch ${epoch}`, {
|
|
107
|
+
...stats,
|
|
108
|
+
fromSlot,
|
|
109
|
+
toSlot,
|
|
110
|
+
epoch
|
|
111
|
+
});
|
|
112
|
+
// Note that we are NOT using the total slots in the epoch as `total` here, since we only
|
|
113
|
+
// compute missed attestations over the blocks that had a proposal in them. So, let's say
|
|
114
|
+
// we have an epoch with 10 slots, but only 5 had a block proposal. A validator that was
|
|
115
|
+
// offline, assuming they were not picked as proposer, will then be reported as having missed
|
|
116
|
+
// 5/5 attestations. If we used the total, they'd be reported as 5/10, which would probably
|
|
117
|
+
// allow them to avoid being slashed.
|
|
118
|
+
return mapValues(stats.stats, (stat)=>({
|
|
119
|
+
missed: stat.missedAttestations.count + stat.missedProposals.count,
|
|
120
|
+
total: stat.missedAttestations.total + stat.missedProposals.total
|
|
121
|
+
}));
|
|
134
122
|
}
|
|
135
123
|
/**
|
|
136
124
|
* Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
|
|
@@ -152,9 +140,7 @@ export class Sentinel extends EventEmitter {
|
|
|
152
140
|
return allPerformance.sort((a, b)=>Number(b.epoch - a.epoch)).filter((p)=>p.epoch < currentEpoch).slice(0, requiredConsecutiveEpochs).every((p)=>p.missed / p.total >= this.config.slashInactivityTargetPercentage);
|
|
153
141
|
}
|
|
154
142
|
async handleProvenPerformance(epoch, performance) {
|
|
155
|
-
const inactiveValidators =
|
|
156
|
-
return missed / total >= this.config.slashInactivityTargetPercentage;
|
|
157
|
-
}).map(([address])=>address);
|
|
143
|
+
const inactiveValidators = getEntries(performance).filter(([_, { missed, total }])=>missed / total >= this.config.slashInactivityTargetPercentage).map(([address])=>address);
|
|
158
144
|
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
159
145
|
inactiveValidators,
|
|
160
146
|
epoch,
|
|
@@ -270,11 +256,13 @@ export class Sentinel extends EventEmitter {
|
|
|
270
256
|
const attestors = new Set([
|
|
271
257
|
...p2pAttested.map((a)=>a.getSender().toString()),
|
|
272
258
|
...block?.attestors.map((a)=>a.toString()) ?? []
|
|
273
|
-
]);
|
|
274
|
-
// We assume that there was a block proposal if at least one of the validators attested to it.
|
|
259
|
+
].filter((addr)=>proposer.toString() !== addr));
|
|
260
|
+
// We assume that there was a block proposal if at least one of the validators (other than the proposer) attested to it.
|
|
275
261
|
// It could be the case that every single validator failed, and we could differentiate it by having
|
|
276
262
|
// this node re-execute every block proposal it sees and storing it in the attestation pool.
|
|
277
263
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
264
|
+
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
265
|
+
// since they will attest to their own proposal it even if it's not re-executable.
|
|
278
266
|
const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
|
|
279
267
|
this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, {
|
|
280
268
|
...block,
|
|
@@ -315,18 +303,17 @@ export class Sentinel extends EventEmitter {
|
|
|
315
303
|
/** Push the status for each slot for each validator. */ updateValidators(slot, stats) {
|
|
316
304
|
return this.store.updateValidators(slot, stats);
|
|
317
305
|
}
|
|
318
|
-
/** Computes stats to be returned based on stored data. */ async computeStats({ fromSlot
|
|
319
|
-
const histories = await
|
|
306
|
+
/** Computes stats to be returned based on stored data. */ async computeStats({ fromSlot, toSlot, validators } = {}) {
|
|
307
|
+
const histories = validators ? fromEntries(await Promise.all(validators.map(async (v)=>[
|
|
308
|
+
v.toString(),
|
|
309
|
+
await this.store.getHistory(v)
|
|
310
|
+
]))) : await this.store.getHistories();
|
|
320
311
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
for (const [address, history] of Object.entries(histories)){
|
|
325
|
-
const validatorAddress = address;
|
|
326
|
-
result[validatorAddress] = this.computeStatsForValidator(validatorAddress, history, fromSlot, toSlot);
|
|
327
|
-
}
|
|
312
|
+
fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
|
|
313
|
+
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
314
|
+
const stats = mapValues(histories, (history, address)=>this.computeStatsForValidator(address, history ?? [], fromSlot, toSlot));
|
|
328
315
|
return {
|
|
329
|
-
stats
|
|
316
|
+
stats,
|
|
330
317
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
331
318
|
initialSlot: this.initialSlot,
|
|
332
319
|
slotWindow: this.store.getHistoryLength()
|
|
@@ -357,25 +344,32 @@ export class Sentinel extends EventEmitter {
|
|
|
357
344
|
computeStatsForValidator(address, allHistory, fromSlot, toSlot) {
|
|
358
345
|
let history = fromSlot ? allHistory.filter((h)=>h.slot >= fromSlot) : allHistory;
|
|
359
346
|
history = toSlot ? history.filter((h)=>h.slot <= toSlot) : history;
|
|
347
|
+
const lastProposal = history.filter((h)=>h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
|
|
348
|
+
const lastAttestation = history.filter((h)=>h.status === 'attestation-sent').at(-1);
|
|
360
349
|
return {
|
|
361
350
|
address: EthAddress.fromString(address),
|
|
362
|
-
lastProposal: this.computeFromSlot(
|
|
363
|
-
lastAttestation: this.computeFromSlot(
|
|
351
|
+
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
352
|
+
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
364
353
|
totalSlots: history.length,
|
|
365
|
-
missedProposals: this.computeMissed(history, 'block',
|
|
366
|
-
|
|
354
|
+
missedProposals: this.computeMissed(history, 'block', [
|
|
355
|
+
'block-missed'
|
|
356
|
+
]),
|
|
357
|
+
missedAttestations: this.computeMissed(history, 'attestation', [
|
|
358
|
+
'attestation-missed'
|
|
359
|
+
]),
|
|
367
360
|
history
|
|
368
361
|
};
|
|
369
362
|
}
|
|
370
363
|
computeMissed(history, computeOverPrefix, filter) {
|
|
371
|
-
const relevantHistory = history.filter((h)
|
|
372
|
-
const filteredHistory = relevantHistory.filter((h)=>h.status
|
|
364
|
+
const relevantHistory = history.filter((h)=>!computeOverPrefix || h.status.startsWith(computeOverPrefix));
|
|
365
|
+
const filteredHistory = relevantHistory.filter((h)=>filter.includes(h.status));
|
|
373
366
|
return {
|
|
374
367
|
currentStreak: countWhile([
|
|
375
368
|
...relevantHistory
|
|
376
|
-
].reverse(), (h)=>h.status
|
|
369
|
+
].reverse(), (h)=>filter.includes(h.status)),
|
|
377
370
|
rate: relevantHistory.length === 0 ? undefined : filteredHistory.length / relevantHistory.length,
|
|
378
|
-
count: filteredHistory.length
|
|
371
|
+
count: filteredHistory.length,
|
|
372
|
+
total: relevantHistory.length
|
|
379
373
|
};
|
|
380
374
|
}
|
|
381
375
|
computeFromSlot(slot) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/aztec-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-nightly.20250907",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -66,29 +66,29 @@
|
|
|
66
66
|
]
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@aztec/archiver": "
|
|
70
|
-
"@aztec/bb-prover": "
|
|
71
|
-
"@aztec/blob-sink": "
|
|
72
|
-
"@aztec/constants": "
|
|
73
|
-
"@aztec/epoch-cache": "
|
|
74
|
-
"@aztec/ethereum": "
|
|
75
|
-
"@aztec/foundation": "
|
|
76
|
-
"@aztec/kv-store": "
|
|
77
|
-
"@aztec/l1-artifacts": "
|
|
78
|
-
"@aztec/merkle-tree": "
|
|
79
|
-
"@aztec/node-keystore": "
|
|
80
|
-
"@aztec/node-lib": "
|
|
81
|
-
"@aztec/noir-protocol-circuits-types": "
|
|
82
|
-
"@aztec/p2p": "
|
|
83
|
-
"@aztec/protocol-contracts": "
|
|
84
|
-
"@aztec/prover-client": "
|
|
85
|
-
"@aztec/sequencer-client": "
|
|
86
|
-
"@aztec/simulator": "
|
|
87
|
-
"@aztec/slasher": "
|
|
88
|
-
"@aztec/stdlib": "
|
|
89
|
-
"@aztec/telemetry-client": "
|
|
90
|
-
"@aztec/validator-client": "
|
|
91
|
-
"@aztec/world-state": "
|
|
69
|
+
"@aztec/archiver": "4.0.0-nightly.20250907",
|
|
70
|
+
"@aztec/bb-prover": "4.0.0-nightly.20250907",
|
|
71
|
+
"@aztec/blob-sink": "4.0.0-nightly.20250907",
|
|
72
|
+
"@aztec/constants": "4.0.0-nightly.20250907",
|
|
73
|
+
"@aztec/epoch-cache": "4.0.0-nightly.20250907",
|
|
74
|
+
"@aztec/ethereum": "4.0.0-nightly.20250907",
|
|
75
|
+
"@aztec/foundation": "4.0.0-nightly.20250907",
|
|
76
|
+
"@aztec/kv-store": "4.0.0-nightly.20250907",
|
|
77
|
+
"@aztec/l1-artifacts": "4.0.0-nightly.20250907",
|
|
78
|
+
"@aztec/merkle-tree": "4.0.0-nightly.20250907",
|
|
79
|
+
"@aztec/node-keystore": "4.0.0-nightly.20250907",
|
|
80
|
+
"@aztec/node-lib": "4.0.0-nightly.20250907",
|
|
81
|
+
"@aztec/noir-protocol-circuits-types": "4.0.0-nightly.20250907",
|
|
82
|
+
"@aztec/p2p": "4.0.0-nightly.20250907",
|
|
83
|
+
"@aztec/protocol-contracts": "4.0.0-nightly.20250907",
|
|
84
|
+
"@aztec/prover-client": "4.0.0-nightly.20250907",
|
|
85
|
+
"@aztec/sequencer-client": "4.0.0-nightly.20250907",
|
|
86
|
+
"@aztec/simulator": "4.0.0-nightly.20250907",
|
|
87
|
+
"@aztec/slasher": "4.0.0-nightly.20250907",
|
|
88
|
+
"@aztec/stdlib": "4.0.0-nightly.20250907",
|
|
89
|
+
"@aztec/telemetry-client": "4.0.0-nightly.20250907",
|
|
90
|
+
"@aztec/validator-client": "4.0.0-nightly.20250907",
|
|
91
|
+
"@aztec/world-state": "4.0.0-nightly.20250907",
|
|
92
92
|
"koa": "^2.16.1",
|
|
93
93
|
"koa-router": "^12.0.0",
|
|
94
94
|
"tslib": "^2.4.0",
|
package/src/aztec-node/server.ts
CHANGED
|
@@ -1122,7 +1122,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
1122
1122
|
|
|
1123
1123
|
public async setConfig(config: Partial<AztecNodeAdminConfig>): Promise<void> {
|
|
1124
1124
|
const newConfig = { ...this.config, ...config };
|
|
1125
|
-
this.sequencer?.
|
|
1125
|
+
this.sequencer?.updateConfig(config);
|
|
1126
1126
|
this.slasherClient?.updateConfig(config);
|
|
1127
1127
|
this.validatorsSentinel?.updateConfig(config);
|
|
1128
1128
|
// this.blockBuilder.updateConfig(config); // TODO: Spyros has a PR to add the builder to `this`, so we can do this
|
package/src/sentinel/sentinel.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import { countWhile, filterAsync } from '@aztec/foundation/collection';
|
|
2
|
+
import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection';
|
|
3
3
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
6
6
|
import { L2TipsMemoryStore, type L2TipsStore } from '@aztec/kv-store/stores';
|
|
7
7
|
import type { P2PClient } from '@aztec/p2p';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
OffenseType,
|
|
10
|
+
WANT_TO_SLASH_EVENT,
|
|
11
|
+
type WantToSlashArgs,
|
|
12
|
+
type Watcher,
|
|
13
|
+
type WatcherEmitter,
|
|
14
|
+
} from '@aztec/slasher';
|
|
9
15
|
import type { SlasherConfig } from '@aztec/slasher/config';
|
|
10
16
|
import {
|
|
11
17
|
type L2BlockSource,
|
|
@@ -14,7 +20,7 @@ import {
|
|
|
14
20
|
type L2BlockStreamEventHandler,
|
|
15
21
|
getAttestationsFromPublishedL2Block,
|
|
16
22
|
} from '@aztec/stdlib/block';
|
|
17
|
-
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
23
|
+
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
18
24
|
import type {
|
|
19
25
|
SingleValidatorStats,
|
|
20
26
|
ValidatorStats,
|
|
@@ -115,53 +121,38 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
115
121
|
return;
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
// TODO(palla/slash): We should only be computing proven performance if this is
|
|
125
|
+
// a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
|
|
126
|
+
const epoch = getEpochAtSlot(block.header.getSlot(), this.epochCache.getL1Constants());
|
|
119
127
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
120
128
|
const performance = await this.computeProvenPerformance(epoch);
|
|
121
129
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
122
130
|
|
|
123
|
-
await this.updateProvenPerformance(epoch, performance);
|
|
131
|
+
await this.store.updateProvenPerformance(epoch, performance);
|
|
124
132
|
await this.handleProvenPerformance(epoch, performance);
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
protected async computeProvenPerformance(epoch: bigint) {
|
|
128
|
-
const
|
|
129
|
-
const provenSlots = headers.map(h => h.getSlot());
|
|
130
|
-
const fromSlot = provenSlots[0];
|
|
131
|
-
const toSlot = provenSlots[provenSlots.length - 1];
|
|
135
|
+
protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
|
|
136
|
+
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
132
137
|
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
133
138
|
if (!committee) {
|
|
134
139
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
135
140
|
return {};
|
|
136
141
|
}
|
|
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
142
|
|
|
163
|
-
|
|
164
|
-
|
|
143
|
+
const stats = await this.computeStats({ fromSlot, toSlot, validators: committee });
|
|
144
|
+
this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
|
|
145
|
+
|
|
146
|
+
// Note that we are NOT using the total slots in the epoch as `total` here, since we only
|
|
147
|
+
// compute missed attestations over the blocks that had a proposal in them. So, let's say
|
|
148
|
+
// we have an epoch with 10 slots, but only 5 had a block proposal. A validator that was
|
|
149
|
+
// offline, assuming they were not picked as proposer, will then be reported as having missed
|
|
150
|
+
// 5/5 attestations. If we used the total, they'd be reported as 5/10, which would probably
|
|
151
|
+
// allow them to avoid being slashed.
|
|
152
|
+
return mapValues(stats.stats, stat => ({
|
|
153
|
+
missed: stat.missedAttestations.count + stat.missedProposals.count,
|
|
154
|
+
total: stat.missedAttestations.total + stat.missedProposals.total,
|
|
155
|
+
}));
|
|
165
156
|
}
|
|
166
157
|
|
|
167
158
|
/**
|
|
@@ -199,11 +190,9 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
199
190
|
}
|
|
200
191
|
|
|
201
192
|
protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
|
|
202
|
-
const inactiveValidators =
|
|
203
|
-
.filter(([_, { missed, total }]) =>
|
|
204
|
-
|
|
205
|
-
})
|
|
206
|
-
.map(([address]) => address as `0x${string}`);
|
|
193
|
+
const inactiveValidators = getEntries(performance)
|
|
194
|
+
.filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
|
|
195
|
+
.map(([address]) => address);
|
|
207
196
|
|
|
208
197
|
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
209
198
|
inactiveValidators,
|
|
@@ -216,7 +205,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
216
205
|
this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1),
|
|
217
206
|
);
|
|
218
207
|
|
|
219
|
-
const args = criminals.map(address => ({
|
|
208
|
+
const args: WantToSlashArgs[] = criminals.map(address => ({
|
|
220
209
|
validator: EthAddress.fromString(address),
|
|
221
210
|
amount: this.config.slashInactivityPenalty,
|
|
222
211
|
offenseType: OffenseType.INACTIVITY,
|
|
@@ -326,15 +315,18 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
326
315
|
// (contains the ones synced from mined blocks, which we may have missed from p2p).
|
|
327
316
|
const block = this.slotNumberToBlock.get(slot);
|
|
328
317
|
const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
|
|
329
|
-
const attestors = new Set(
|
|
330
|
-
...p2pAttested.map(a => a.getSender().toString()),
|
|
331
|
-
|
|
332
|
-
|
|
318
|
+
const attestors = new Set(
|
|
319
|
+
[...p2pAttested.map(a => a.getSender().toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
320
|
+
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
321
|
+
),
|
|
322
|
+
);
|
|
333
323
|
|
|
334
|
-
// We assume that there was a block proposal if at least one of the validators attested to it.
|
|
324
|
+
// We assume that there was a block proposal if at least one of the validators (other than the proposer) attested to it.
|
|
335
325
|
// It could be the case that every single validator failed, and we could differentiate it by having
|
|
336
326
|
// this node re-execute every block proposal it sees and storing it in the attestation pool.
|
|
337
327
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
328
|
+
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
329
|
+
// since they will attest to their own proposal it even if it's not re-executable.
|
|
338
330
|
const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
|
|
339
331
|
this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, { ...block, slot });
|
|
340
332
|
|
|
@@ -378,20 +370,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
378
370
|
|
|
379
371
|
/** Computes stats to be returned based on stored data. */
|
|
380
372
|
public async computeStats({
|
|
381
|
-
fromSlot
|
|
382
|
-
toSlot
|
|
383
|
-
|
|
384
|
-
|
|
373
|
+
fromSlot,
|
|
374
|
+
toSlot,
|
|
375
|
+
validators,
|
|
376
|
+
}: { fromSlot?: bigint; toSlot?: bigint; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
|
|
377
|
+
const histories = validators
|
|
378
|
+
? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
|
|
379
|
+
: await this.store.getHistories();
|
|
380
|
+
|
|
385
381
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
382
|
+
fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
|
|
383
|
+
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
384
|
+
|
|
385
|
+
const stats = mapValues(histories, (history, address) =>
|
|
386
|
+
this.computeStatsForValidator(address, history ?? [], fromSlot, toSlot),
|
|
387
|
+
);
|
|
388
|
+
|
|
393
389
|
return {
|
|
394
|
-
stats
|
|
390
|
+
stats,
|
|
395
391
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
396
392
|
initialSlot: this.initialSlot,
|
|
397
393
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -447,30 +443,31 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
447
443
|
): ValidatorStats {
|
|
448
444
|
let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
|
|
449
445
|
history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
|
|
446
|
+
const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
|
|
447
|
+
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
450
448
|
return {
|
|
451
449
|
address: EthAddress.fromString(address),
|
|
452
|
-
lastProposal: this.computeFromSlot(
|
|
453
|
-
|
|
454
|
-
),
|
|
455
|
-
lastAttestation: this.computeFromSlot(history.filter(h => h.status === 'attestation-sent').at(-1)?.slot),
|
|
450
|
+
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
451
|
+
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
456
452
|
totalSlots: history.length,
|
|
457
|
-
missedProposals: this.computeMissed(history, 'block', 'block-missed'),
|
|
458
|
-
missedAttestations: this.computeMissed(history, 'attestation', 'attestation-missed'),
|
|
453
|
+
missedProposals: this.computeMissed(history, 'block', ['block-missed']),
|
|
454
|
+
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
459
455
|
history,
|
|
460
456
|
};
|
|
461
457
|
}
|
|
462
458
|
|
|
463
459
|
protected computeMissed(
|
|
464
460
|
history: ValidatorStatusHistory,
|
|
465
|
-
computeOverPrefix: ValidatorStatusType,
|
|
466
|
-
filter: ValidatorStatusInSlot,
|
|
461
|
+
computeOverPrefix: ValidatorStatusType | undefined,
|
|
462
|
+
filter: ValidatorStatusInSlot[],
|
|
467
463
|
) {
|
|
468
|
-
const relevantHistory = history.filter(h => h.status.startsWith(computeOverPrefix));
|
|
469
|
-
const filteredHistory = relevantHistory.filter(h => h.status
|
|
464
|
+
const relevantHistory = history.filter(h => !computeOverPrefix || h.status.startsWith(computeOverPrefix));
|
|
465
|
+
const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
|
|
470
466
|
return {
|
|
471
|
-
currentStreak: countWhile([...relevantHistory].reverse(), h => h.status
|
|
467
|
+
currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
|
|
472
468
|
rate: relevantHistory.length === 0 ? undefined : filteredHistory.length / relevantHistory.length,
|
|
473
469
|
count: filteredHistory.length,
|
|
470
|
+
total: relevantHistory.length,
|
|
474
471
|
};
|
|
475
472
|
}
|
|
476
473
|
|