@aztec/aztec-node 2.0.0-nightly.20250903 → 2.0.0-rc.2
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/server.js +1 -1
- package/dest/sentinel/sentinel.d.ts +14 -6
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +84 -63
- package/package.json +24 -24
- package/src/aztec-node/server.ts +1 -1
- package/src/sentinel/sentinel.ts +122 -74
|
@@ -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
|
|
@@ -14,7 +14,7 @@ export declare class Sentinel extends Sentinel_base implements L2BlockStreamEven
|
|
|
14
14
|
protected archiver: L2BlockSource;
|
|
15
15
|
protected p2p: P2PClient;
|
|
16
16
|
protected store: SentinelStore;
|
|
17
|
-
protected config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>;
|
|
17
|
+
protected config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashInactivityConsecutiveEpochThreshold'>;
|
|
18
18
|
protected logger: import("@aztec/foundation/log").Logger;
|
|
19
19
|
protected runningPromise: RunningPromise;
|
|
20
20
|
protected blockStream: L2BlockStream;
|
|
@@ -26,7 +26,7 @@ export declare class Sentinel extends Sentinel_base implements L2BlockStreamEven
|
|
|
26
26
|
archive: string;
|
|
27
27
|
attestors: EthAddress[];
|
|
28
28
|
}>;
|
|
29
|
-
constructor(epochCache: EpochCache, archiver: L2BlockSource, p2p: P2PClient, store: SentinelStore, config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>, logger?: import("@aztec/foundation/log").Logger);
|
|
29
|
+
constructor(epochCache: EpochCache, archiver: L2BlockSource, p2p: P2PClient, store: SentinelStore, config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashInactivityConsecutiveEpochThreshold'>, logger?: import("@aztec/foundation/log").Logger);
|
|
30
30
|
updateConfig(config: Partial<SlasherConfig>): void;
|
|
31
31
|
start(): Promise<void>;
|
|
32
32
|
/** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
|
|
@@ -35,8 +35,14 @@ 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
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
|
|
40
|
+
* @param validator The validator address to check
|
|
41
|
+
* @param currentEpoch Epochs strictly before the current one are evaluated only
|
|
42
|
+
* @param requiredConsecutiveEpochs Number of consecutive epochs required for slashing
|
|
43
|
+
*/
|
|
44
|
+
protected checkPastInactivity(validator: EthAddress, currentEpoch: bigint, requiredConsecutiveEpochs: number): Promise<boolean>;
|
|
45
|
+
protected handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): Promise<void>;
|
|
40
46
|
/**
|
|
41
47
|
* Process data for two L2 slots ago.
|
|
42
48
|
* Note that we do not process historical data, since we rely on p2p data for processing,
|
|
@@ -61,17 +67,19 @@ export declare class Sentinel extends Sentinel_base implements L2BlockStreamEven
|
|
|
61
67
|
/** Push the status for each slot for each validator. */
|
|
62
68
|
protected updateValidators(slot: bigint, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>): Promise<void>;
|
|
63
69
|
/** Computes stats to be returned based on stored data. */
|
|
64
|
-
computeStats({ fromSlot
|
|
70
|
+
computeStats({ fromSlot, toSlot, validators, }?: {
|
|
65
71
|
fromSlot?: bigint;
|
|
66
72
|
toSlot?: bigint;
|
|
73
|
+
validators?: EthAddress[];
|
|
67
74
|
}): Promise<ValidatorsStats>;
|
|
68
75
|
/** Computes stats for a single validator. */
|
|
69
76
|
getValidatorStats(validatorAddress: EthAddress, fromSlot?: bigint, toSlot?: bigint): Promise<SingleValidatorStats | undefined>;
|
|
70
77
|
protected computeStatsForValidator(address: `0x${string}`, allHistory: ValidatorStatusHistory, fromSlot?: bigint, toSlot?: bigint): ValidatorStats;
|
|
71
|
-
protected computeMissed(history: ValidatorStatusHistory, computeOverPrefix: ValidatorStatusType, filter: ValidatorStatusInSlot): {
|
|
78
|
+
protected computeMissed(history: ValidatorStatusHistory, computeOverPrefix: ValidatorStatusType | undefined, filter: ValidatorStatusInSlot[]): {
|
|
72
79
|
currentStreak: number;
|
|
73
80
|
rate: number | undefined;
|
|
74
81
|
count: number;
|
|
82
|
+
total: number;
|
|
75
83
|
};
|
|
76
84
|
protected computeFromSlot(slot: bigint | undefined): {
|
|
77
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 } 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);
|
|
90
|
-
this.handleProvenPerformance(epoch, performance);
|
|
91
|
+
await this.store.updateProvenPerformance(epoch, performance);
|
|
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,41 +100,54 @@ 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;
|
|
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
|
+
}));
|
|
131
122
|
}
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
|
|
125
|
+
* @param validator The validator address to check
|
|
126
|
+
* @param currentEpoch Epochs strictly before the current one are evaluated only
|
|
127
|
+
* @param requiredConsecutiveEpochs Number of consecutive epochs required for slashing
|
|
128
|
+
*/ async checkPastInactivity(validator, currentEpoch, requiredConsecutiveEpochs) {
|
|
129
|
+
if (requiredConsecutiveEpochs === 0) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// Get all historical performance for this validator
|
|
133
|
+
const allPerformance = await this.store.getProvenPerformance(validator);
|
|
134
|
+
// If we don't have enough historical data, don't slash
|
|
135
|
+
if (allPerformance.length < requiredConsecutiveEpochs) {
|
|
136
|
+
this.logger.debug(`Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
|
|
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);
|
|
134
141
|
}
|
|
135
|
-
handleProvenPerformance(epoch, performance) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
async handleProvenPerformance(epoch, performance) {
|
|
143
|
+
const inactiveValidators = getEntries(performance).filter(([_, { missed, total }])=>missed / total >= this.config.slashInactivityTargetPercentage).map(([address])=>address);
|
|
144
|
+
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
145
|
+
inactiveValidators,
|
|
146
|
+
epoch,
|
|
147
|
+
inactivityTargetPercentage: this.config.slashInactivityTargetPercentage
|
|
148
|
+
});
|
|
149
|
+
const epochThreshold = this.config.slashInactivityConsecutiveEpochThreshold;
|
|
150
|
+
const criminals = await filterAsync(inactiveValidators, (address)=>this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1));
|
|
139
151
|
const args = criminals.map((address)=>({
|
|
140
152
|
validator: EthAddress.fromString(address),
|
|
141
153
|
amount: this.config.slashInactivityPenalty,
|
|
@@ -143,8 +155,9 @@ export class Sentinel extends EventEmitter {
|
|
|
143
155
|
epochOrSlot: epoch
|
|
144
156
|
}));
|
|
145
157
|
if (criminals.length > 0) {
|
|
146
|
-
this.logger.info(`Identified ${criminals.length} validators to slash due to inactivity`, {
|
|
147
|
-
args
|
|
158
|
+
this.logger.info(`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`, {
|
|
159
|
+
...args,
|
|
160
|
+
epochThreshold
|
|
148
161
|
});
|
|
149
162
|
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
150
163
|
}
|
|
@@ -243,11 +256,13 @@ export class Sentinel extends EventEmitter {
|
|
|
243
256
|
const attestors = new Set([
|
|
244
257
|
...p2pAttested.map((a)=>a.getSender().toString()),
|
|
245
258
|
...block?.attestors.map((a)=>a.toString()) ?? []
|
|
246
|
-
]);
|
|
247
|
-
// 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.
|
|
248
261
|
// It could be the case that every single validator failed, and we could differentiate it by having
|
|
249
262
|
// this node re-execute every block proposal it sees and storing it in the attestation pool.
|
|
250
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.
|
|
251
266
|
const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
|
|
252
267
|
this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, {
|
|
253
268
|
...block,
|
|
@@ -288,18 +303,17 @@ export class Sentinel extends EventEmitter {
|
|
|
288
303
|
/** Push the status for each slot for each validator. */ updateValidators(slot, stats) {
|
|
289
304
|
return this.store.updateValidators(slot, stats);
|
|
290
305
|
}
|
|
291
|
-
/** Computes stats to be returned based on stored data. */ async computeStats({ fromSlot
|
|
292
|
-
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();
|
|
293
311
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
for (const [address, history] of Object.entries(histories)){
|
|
298
|
-
const validatorAddress = address;
|
|
299
|
-
result[validatorAddress] = this.computeStatsForValidator(validatorAddress, history, fromSlot, toSlot);
|
|
300
|
-
}
|
|
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));
|
|
301
315
|
return {
|
|
302
|
-
stats
|
|
316
|
+
stats,
|
|
303
317
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
304
318
|
initialSlot: this.initialSlot,
|
|
305
319
|
slotWindow: this.store.getHistoryLength()
|
|
@@ -330,25 +344,32 @@ export class Sentinel extends EventEmitter {
|
|
|
330
344
|
computeStatsForValidator(address, allHistory, fromSlot, toSlot) {
|
|
331
345
|
let history = fromSlot ? allHistory.filter((h)=>h.slot >= fromSlot) : allHistory;
|
|
332
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);
|
|
333
349
|
return {
|
|
334
350
|
address: EthAddress.fromString(address),
|
|
335
|
-
lastProposal: this.computeFromSlot(
|
|
336
|
-
lastAttestation: this.computeFromSlot(
|
|
351
|
+
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
352
|
+
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
337
353
|
totalSlots: history.length,
|
|
338
|
-
missedProposals: this.computeMissed(history, 'block',
|
|
339
|
-
|
|
354
|
+
missedProposals: this.computeMissed(history, 'block', [
|
|
355
|
+
'block-missed'
|
|
356
|
+
]),
|
|
357
|
+
missedAttestations: this.computeMissed(history, 'attestation', [
|
|
358
|
+
'attestation-missed'
|
|
359
|
+
]),
|
|
340
360
|
history
|
|
341
361
|
};
|
|
342
362
|
}
|
|
343
363
|
computeMissed(history, computeOverPrefix, filter) {
|
|
344
|
-
const relevantHistory = history.filter((h)
|
|
345
|
-
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));
|
|
346
366
|
return {
|
|
347
367
|
currentStreak: countWhile([
|
|
348
368
|
...relevantHistory
|
|
349
|
-
].reverse(), (h)=>h.status
|
|
369
|
+
].reverse(), (h)=>filter.includes(h.status)),
|
|
350
370
|
rate: relevantHistory.length === 0 ? undefined : filteredHistory.length / relevantHistory.length,
|
|
351
|
-
count: filteredHistory.length
|
|
371
|
+
count: filteredHistory.length,
|
|
372
|
+
total: relevantHistory.length
|
|
352
373
|
};
|
|
353
374
|
}
|
|
354
375
|
computeFromSlot(slot) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/aztec-node",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-rc.2",
|
|
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": "2.0.0-
|
|
70
|
-
"@aztec/bb-prover": "2.0.0-
|
|
71
|
-
"@aztec/blob-sink": "2.0.0-
|
|
72
|
-
"@aztec/constants": "2.0.0-
|
|
73
|
-
"@aztec/epoch-cache": "2.0.0-
|
|
74
|
-
"@aztec/ethereum": "2.0.0-
|
|
75
|
-
"@aztec/foundation": "2.0.0-
|
|
76
|
-
"@aztec/kv-store": "2.0.0-
|
|
77
|
-
"@aztec/l1-artifacts": "2.0.0-
|
|
78
|
-
"@aztec/merkle-tree": "2.0.0-
|
|
79
|
-
"@aztec/node-keystore": "2.0.0-
|
|
80
|
-
"@aztec/node-lib": "2.0.0-
|
|
81
|
-
"@aztec/noir-protocol-circuits-types": "2.0.0-
|
|
82
|
-
"@aztec/p2p": "2.0.0-
|
|
83
|
-
"@aztec/protocol-contracts": "2.0.0-
|
|
84
|
-
"@aztec/prover-client": "2.0.0-
|
|
85
|
-
"@aztec/sequencer-client": "2.0.0-
|
|
86
|
-
"@aztec/simulator": "2.0.0-
|
|
87
|
-
"@aztec/slasher": "2.0.0-
|
|
88
|
-
"@aztec/stdlib": "2.0.0-
|
|
89
|
-
"@aztec/telemetry-client": "2.0.0-
|
|
90
|
-
"@aztec/validator-client": "2.0.0-
|
|
91
|
-
"@aztec/world-state": "2.0.0-
|
|
69
|
+
"@aztec/archiver": "2.0.0-rc.2",
|
|
70
|
+
"@aztec/bb-prover": "2.0.0-rc.2",
|
|
71
|
+
"@aztec/blob-sink": "2.0.0-rc.2",
|
|
72
|
+
"@aztec/constants": "2.0.0-rc.2",
|
|
73
|
+
"@aztec/epoch-cache": "2.0.0-rc.2",
|
|
74
|
+
"@aztec/ethereum": "2.0.0-rc.2",
|
|
75
|
+
"@aztec/foundation": "2.0.0-rc.2",
|
|
76
|
+
"@aztec/kv-store": "2.0.0-rc.2",
|
|
77
|
+
"@aztec/l1-artifacts": "2.0.0-rc.2",
|
|
78
|
+
"@aztec/merkle-tree": "2.0.0-rc.2",
|
|
79
|
+
"@aztec/node-keystore": "2.0.0-rc.2",
|
|
80
|
+
"@aztec/node-lib": "2.0.0-rc.2",
|
|
81
|
+
"@aztec/noir-protocol-circuits-types": "2.0.0-rc.2",
|
|
82
|
+
"@aztec/p2p": "2.0.0-rc.2",
|
|
83
|
+
"@aztec/protocol-contracts": "2.0.0-rc.2",
|
|
84
|
+
"@aztec/prover-client": "2.0.0-rc.2",
|
|
85
|
+
"@aztec/sequencer-client": "2.0.0-rc.2",
|
|
86
|
+
"@aztec/simulator": "2.0.0-rc.2",
|
|
87
|
+
"@aztec/slasher": "2.0.0-rc.2",
|
|
88
|
+
"@aztec/stdlib": "2.0.0-rc.2",
|
|
89
|
+
"@aztec/telemetry-client": "2.0.0-rc.2",
|
|
90
|
+
"@aztec/validator-client": "2.0.0-rc.2",
|
|
91
|
+
"@aztec/world-state": "2.0.0-rc.2",
|
|
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 } 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,
|
|
@@ -44,7 +50,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
44
50
|
protected archiver: L2BlockSource,
|
|
45
51
|
protected p2p: P2PClient,
|
|
46
52
|
protected store: SentinelStore,
|
|
47
|
-
protected config: Pick<
|
|
53
|
+
protected config: Pick<
|
|
54
|
+
SlasherConfig,
|
|
55
|
+
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashInactivityConsecutiveEpochThreshold'
|
|
56
|
+
>,
|
|
48
57
|
protected logger = createLogger('node:sentinel'),
|
|
49
58
|
) {
|
|
50
59
|
super();
|
|
@@ -112,63 +121,91 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
112
121
|
return;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
|
|
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());
|
|
116
127
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
117
128
|
const performance = await this.computeProvenPerformance(epoch);
|
|
118
129
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
119
130
|
|
|
120
|
-
await this.updateProvenPerformance(epoch, performance);
|
|
121
|
-
this.handleProvenPerformance(epoch, performance);
|
|
131
|
+
await this.store.updateProvenPerformance(epoch, performance);
|
|
132
|
+
await this.handleProvenPerformance(epoch, performance);
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
protected async computeProvenPerformance(epoch: bigint) {
|
|
125
|
-
const
|
|
126
|
-
const provenSlots = headers.map(h => h.getSlot());
|
|
127
|
-
const fromSlot = provenSlots[0];
|
|
128
|
-
const toSlot = provenSlots[provenSlots.length - 1];
|
|
135
|
+
protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
|
|
136
|
+
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
129
137
|
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
130
138
|
if (!committee) {
|
|
131
139
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
132
140
|
return {};
|
|
133
141
|
}
|
|
134
|
-
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
let missed = 0;
|
|
150
|
-
for (const history of stats.stats[validator].history) {
|
|
151
|
-
if (provenSlots.includes(history.slot) && history.status === 'attestation-missed') {
|
|
152
|
-
missed++;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
performance[address.toString()] = { missed, total: provenSlots.length };
|
|
156
|
-
}
|
|
157
|
-
return performance;
|
|
142
|
+
|
|
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
|
+
}));
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
|
|
160
|
+
* @param validator The validator address to check
|
|
161
|
+
* @param currentEpoch Epochs strictly before the current one are evaluated only
|
|
162
|
+
* @param requiredConsecutiveEpochs Number of consecutive epochs required for slashing
|
|
163
|
+
*/
|
|
164
|
+
protected async checkPastInactivity(
|
|
165
|
+
validator: EthAddress,
|
|
166
|
+
currentEpoch: bigint,
|
|
167
|
+
requiredConsecutiveEpochs: number,
|
|
168
|
+
): Promise<boolean> {
|
|
169
|
+
if (requiredConsecutiveEpochs === 0) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get all historical performance for this validator
|
|
174
|
+
const allPerformance = await this.store.getProvenPerformance(validator);
|
|
175
|
+
|
|
176
|
+
// If we don't have enough historical data, don't slash
|
|
177
|
+
if (allPerformance.length < requiredConsecutiveEpochs) {
|
|
178
|
+
this.logger.debug(
|
|
179
|
+
`Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
|
|
180
|
+
);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
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
|
+
return allPerformance
|
|
186
|
+
.sort((a, b) => Number(b.epoch - a.epoch))
|
|
187
|
+
.filter(p => p.epoch < currentEpoch)
|
|
188
|
+
.slice(0, requiredConsecutiveEpochs)
|
|
189
|
+
.every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
|
|
162
190
|
}
|
|
163
191
|
|
|
164
|
-
protected handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
|
|
165
|
-
const
|
|
166
|
-
.filter(([_, { missed, total }]) =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
192
|
+
protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
|
|
193
|
+
const inactiveValidators = getEntries(performance)
|
|
194
|
+
.filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
|
|
195
|
+
.map(([address]) => address);
|
|
196
|
+
|
|
197
|
+
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
198
|
+
inactiveValidators,
|
|
199
|
+
epoch,
|
|
200
|
+
inactivityTargetPercentage: this.config.slashInactivityTargetPercentage,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const epochThreshold = this.config.slashInactivityConsecutiveEpochThreshold;
|
|
204
|
+
const criminals: string[] = await filterAsync(inactiveValidators, address =>
|
|
205
|
+
this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1),
|
|
206
|
+
);
|
|
170
207
|
|
|
171
|
-
const args = criminals.map(address => ({
|
|
208
|
+
const args: WantToSlashArgs[] = criminals.map(address => ({
|
|
172
209
|
validator: EthAddress.fromString(address),
|
|
173
210
|
amount: this.config.slashInactivityPenalty,
|
|
174
211
|
offenseType: OffenseType.INACTIVITY,
|
|
@@ -176,7 +213,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
176
213
|
}));
|
|
177
214
|
|
|
178
215
|
if (criminals.length > 0) {
|
|
179
|
-
this.logger.info(
|
|
216
|
+
this.logger.info(
|
|
217
|
+
`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
|
|
218
|
+
{ ...args, epochThreshold },
|
|
219
|
+
);
|
|
180
220
|
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
181
221
|
}
|
|
182
222
|
}
|
|
@@ -275,15 +315,18 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
275
315
|
// (contains the ones synced from mined blocks, which we may have missed from p2p).
|
|
276
316
|
const block = this.slotNumberToBlock.get(slot);
|
|
277
317
|
const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
|
|
278
|
-
const attestors = new Set(
|
|
279
|
-
...p2pAttested.map(a => a.getSender().toString()),
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
);
|
|
282
323
|
|
|
283
|
-
// 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.
|
|
284
325
|
// It could be the case that every single validator failed, and we could differentiate it by having
|
|
285
326
|
// this node re-execute every block proposal it sees and storing it in the attestation pool.
|
|
286
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.
|
|
287
330
|
const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
|
|
288
331
|
this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, { ...block, slot });
|
|
289
332
|
|
|
@@ -327,20 +370,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
327
370
|
|
|
328
371
|
/** Computes stats to be returned based on stored data. */
|
|
329
372
|
public async computeStats({
|
|
330
|
-
fromSlot
|
|
331
|
-
toSlot
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
|
|
334
381
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
|
|
342
389
|
return {
|
|
343
|
-
stats
|
|
390
|
+
stats,
|
|
344
391
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
345
392
|
initialSlot: this.initialSlot,
|
|
346
393
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -396,30 +443,31 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
396
443
|
): ValidatorStats {
|
|
397
444
|
let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
|
|
398
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);
|
|
399
448
|
return {
|
|
400
449
|
address: EthAddress.fromString(address),
|
|
401
|
-
lastProposal: this.computeFromSlot(
|
|
402
|
-
|
|
403
|
-
),
|
|
404
|
-
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),
|
|
405
452
|
totalSlots: history.length,
|
|
406
|
-
missedProposals: this.computeMissed(history, 'block', 'block-missed'),
|
|
407
|
-
missedAttestations: this.computeMissed(history, 'attestation', 'attestation-missed'),
|
|
453
|
+
missedProposals: this.computeMissed(history, 'block', ['block-missed']),
|
|
454
|
+
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
408
455
|
history,
|
|
409
456
|
};
|
|
410
457
|
}
|
|
411
458
|
|
|
412
459
|
protected computeMissed(
|
|
413
460
|
history: ValidatorStatusHistory,
|
|
414
|
-
computeOverPrefix: ValidatorStatusType,
|
|
415
|
-
filter: ValidatorStatusInSlot,
|
|
461
|
+
computeOverPrefix: ValidatorStatusType | undefined,
|
|
462
|
+
filter: ValidatorStatusInSlot[],
|
|
416
463
|
) {
|
|
417
|
-
const relevantHistory = history.filter(h => h.status.startsWith(computeOverPrefix));
|
|
418
|
-
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));
|
|
419
466
|
return {
|
|
420
|
-
currentStreak: countWhile([...relevantHistory].reverse(), h => h.status
|
|
467
|
+
currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
|
|
421
468
|
rate: relevantHistory.length === 0 ? undefined : filteredHistory.length / relevantHistory.length,
|
|
422
469
|
count: filteredHistory.length,
|
|
470
|
+
total: relevantHistory.length,
|
|
423
471
|
};
|
|
424
472
|
}
|
|
425
473
|
|