@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.
@@ -788,7 +788,7 @@ import { NodeMetrics } from './node_metrics.js';
788
788
  ...this.config,
789
789
  ...config
790
790
  };
791
- this.sequencer?.updateSequencerConfig(config);
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
- protected updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): Promise<void>;
39
- protected handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): void;
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: _fromSlot, toSlot: _toSlot, }?: {
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":"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"}
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,EAIL,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,KAAK,aAAa,EAClB,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAE/B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,oBAAoB,EACpB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,EAC1B,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;6BAEI,UAAU,cAAc;AAAvE,qBAAa,QAAS,SAAQ,aAA2C,YAAW,yBAAyB,EAAE,OAAO;IAWlH,SAAS,CAAC,UAAU,EAAE,UAAU;IAChC,SAAS,CAAC,QAAQ,EAAE,aAAa;IACjC,SAAS,CAAC,GAAG,EAAE,SAAS;IACxB,SAAS,CAAC,KAAK,EAAE,aAAa;IAC9B,SAAS,CAAC,MAAM,EAAE,IAAI,CACpB,aAAa,EACb,iCAAiC,GAAG,wBAAwB,GAAG,0CAA0C,CAC1G;IACD,SAAS,CAAC,MAAM;IAlBlB,SAAS,CAAC,cAAc,EAAE,cAAc,CAAC;IACzC,SAAS,CAAC,WAAW,EAAG,aAAa,CAAC;IACtC,SAAS,CAAC,WAAW,EAAE,WAAW,CAAC;IAEnC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,SAAS,CAAC,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IAChD,SAAS,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAC/F;gBAGA,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,SAAS,EACd,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,IAAI,CACpB,aAAa,EACb,iCAAiC,GAAG,wBAAwB,GAAG,0CAA0C,CAC1G,EACS,MAAM,yCAAgC;IAQ3C,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC;IAIrC,KAAK;IAKlB,kHAAkH;cAClG,IAAI;IAOb,IAAI;IAIE,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;cA2B7D,iBAAiB,CAAC,KAAK,EAAE,kBAAkB;cAsB3C,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC;IAuB5F;;;;;OAKG;cACa,mBAAmB,CACjC,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,MAAM,EACpB,yBAAyB,EAAE,MAAM,GAChC,OAAO,CAAC,OAAO,CAAC;cAwBH,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B;IAgC9F;;;;OAIG;IACU,IAAI;IAmBjB;;;;OAIG;cACa,gBAAgB,CAAC,WAAW,EAAE,MAAM;IAkCpD;;;OAGG;cACa,WAAW,CAAC,IAAI,EAAE,MAAM;IAexC,0CAA0C;cAC1B,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE;;;IA2D1G,wDAAwD;IACxD,SAAS,CAAC,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,MAAM,EAAE,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAIxG,0DAA0D;IAC7C,YAAY,CAAC,EACxB,QAAQ,EACR,MAAM,EACN,UAAU,GACX,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,UAAU,EAAE,CAAA;KAAO,GAAG,OAAO,CAAC,eAAe,CAAC;IAqBpG,6CAA6C;IAChC,iBAAiB,CAC5B,gBAAgB,EAAE,UAAU,EAC5B,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAoC5C,SAAS,CAAC,wBAAwB,CAChC,OAAO,EAAE,KAAK,MAAM,EAAE,EACtB,UAAU,EAAE,sBAAsB,EAClC,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,cAAc;IAgBjB,SAAS,CAAC,aAAa,CACrB,OAAO,EAAE,sBAAsB,EAC/B,iBAAiB,EAAE,mBAAmB,GAAG,SAAS,EAClD,MAAM,EAAE,qBAAqB,EAAE;;;;;;IAYjC,SAAS,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS;;;;;CAOnD"}
@@ -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
- const epoch = getEpochAtSlot(block.header.getSlot(), await this.archiver.getL1Constants());
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 headers = await this.archiver.getBlockHeadersForEpoch(epoch);
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}`, stats);
107
- const performance = {};
108
- for (const validator of Object.keys(stats.stats)){
109
- let address;
110
- try {
111
- address = EthAddress.fromString(validator);
112
- } catch (e) {
113
- this.logger.error(`Invalid validator address ${validator}`, e);
114
- continue;
115
- }
116
- if (!committee.find((v)=>v.equals(address))) {
117
- continue;
118
- }
119
- let missed = 0;
120
- for (const history of stats.stats[validator].history){
121
- if (provenSlots.includes(history.slot) && history.status === 'attestation-missed') {
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
- updateProvenPerformance(epoch, performance) {
133
- return this.store.updateProvenPerformance(epoch, performance);
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 criminals = Object.entries(performance).filter(([_, { missed, total }])=>{
137
- return missed / total >= this.config.slashInactivityTargetPercentage;
138
- }).map(([address])=>address);
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: _fromSlot, toSlot: _toSlot } = {}) {
292
- const histories = await this.store.getHistories();
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
- const fromSlot = _fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
295
- const toSlot = _toSlot ?? this.lastProcessedSlot ?? slotNow;
296
- const result = {};
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: result,
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(history.filter((h)=>h.status === 'block-proposed' || h.status === 'block-mined').at(-1)?.slot),
336
- lastAttestation: this.computeFromSlot(history.filter((h)=>h.status === 'attestation-sent').at(-1)?.slot),
351
+ lastProposal: this.computeFromSlot(lastProposal?.slot),
352
+ lastAttestation: this.computeFromSlot(lastAttestation?.slot),
337
353
  totalSlots: history.length,
338
- missedProposals: this.computeMissed(history, 'block', 'block-missed'),
339
- missedAttestations: this.computeMissed(history, 'attestation', 'attestation-missed'),
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)=>h.status.startsWith(computeOverPrefix));
345
- const filteredHistory = relevantHistory.filter((h)=>h.status === filter);
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 === filter),
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-nightly.20250903",
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-nightly.20250903",
70
- "@aztec/bb-prover": "2.0.0-nightly.20250903",
71
- "@aztec/blob-sink": "2.0.0-nightly.20250903",
72
- "@aztec/constants": "2.0.0-nightly.20250903",
73
- "@aztec/epoch-cache": "2.0.0-nightly.20250903",
74
- "@aztec/ethereum": "2.0.0-nightly.20250903",
75
- "@aztec/foundation": "2.0.0-nightly.20250903",
76
- "@aztec/kv-store": "2.0.0-nightly.20250903",
77
- "@aztec/l1-artifacts": "2.0.0-nightly.20250903",
78
- "@aztec/merkle-tree": "2.0.0-nightly.20250903",
79
- "@aztec/node-keystore": "2.0.0-nightly.20250903",
80
- "@aztec/node-lib": "2.0.0-nightly.20250903",
81
- "@aztec/noir-protocol-circuits-types": "2.0.0-nightly.20250903",
82
- "@aztec/p2p": "2.0.0-nightly.20250903",
83
- "@aztec/protocol-contracts": "2.0.0-nightly.20250903",
84
- "@aztec/prover-client": "2.0.0-nightly.20250903",
85
- "@aztec/sequencer-client": "2.0.0-nightly.20250903",
86
- "@aztec/simulator": "2.0.0-nightly.20250903",
87
- "@aztec/slasher": "2.0.0-nightly.20250903",
88
- "@aztec/stdlib": "2.0.0-nightly.20250903",
89
- "@aztec/telemetry-client": "2.0.0-nightly.20250903",
90
- "@aztec/validator-client": "2.0.0-nightly.20250903",
91
- "@aztec/world-state": "2.0.0-nightly.20250903",
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",
@@ -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?.updateSequencerConfig(config);
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
@@ -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 { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
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<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>,
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
- const epoch = getEpochAtSlot(block.header.getSlot(), await this.archiver.getL1Constants());
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 headers = await this.archiver.getBlockHeadersForEpoch(epoch);
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
- const stats = await this.computeStats({ fromSlot, toSlot });
135
- this.logger.debug(`Stats for epoch ${epoch}`, stats);
136
-
137
- const performance: ValidatorsEpochPerformance = {};
138
- for (const validator of Object.keys(stats.stats)) {
139
- let address;
140
- try {
141
- address = EthAddress.fromString(validator);
142
- } catch (e) {
143
- this.logger.error(`Invalid validator address ${validator}`, e);
144
- continue;
145
- }
146
- if (!committee.find(v => v.equals(address))) {
147
- continue;
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
- protected updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
161
- return this.store.updateProvenPerformance(epoch, performance);
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 criminals = Object.entries(performance)
166
- .filter(([_, { missed, total }]) => {
167
- return missed / total >= this.config.slashInactivityTargetPercentage;
168
- })
169
- .map(([address]) => address as `0x${string}`);
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(`Identified ${criminals.length} validators to slash due to inactivity`, { args });
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
- ...(block?.attestors.map(a => a.toString()) ?? []),
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: _fromSlot,
331
- toSlot: _toSlot,
332
- }: { fromSlot?: bigint; toSlot?: bigint } = {}): Promise<ValidatorsStats> {
333
- const histories = await this.store.getHistories();
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
- const fromSlot = _fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
336
- const toSlot = _toSlot ?? this.lastProcessedSlot ?? slotNow;
337
- const result: Record<`0x${string}`, ValidatorStats> = {};
338
- for (const [address, history] of Object.entries(histories)) {
339
- const validatorAddress = address as `0x${string}`;
340
- result[validatorAddress] = this.computeStatsForValidator(validatorAddress, history, fromSlot, toSlot);
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: result,
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
- history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1)?.slot,
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 === filter);
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 === filter),
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