@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?.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
@@ -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: _fromSlot, toSlot: _toSlot, }?: {
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":"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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, 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
- 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);
91
+ await this.store.updateProvenPerformance(epoch, performance);
90
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,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}`, 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;
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 = Object.entries(performance).filter(([_, { missed, total }])=>{
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: _fromSlot, toSlot: _toSlot } = {}) {
319
- 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();
320
311
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
321
- const fromSlot = _fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
322
- const toSlot = _toSlot ?? this.lastProcessedSlot ?? slotNow;
323
- const result = {};
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: result,
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(history.filter((h)=>h.status === 'block-proposed' || h.status === 'block-mined').at(-1)?.slot),
363
- 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),
364
353
  totalSlots: history.length,
365
- missedProposals: this.computeMissed(history, 'block', 'block-missed'),
366
- 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
+ ]),
367
360
  history
368
361
  };
369
362
  }
370
363
  computeMissed(history, computeOverPrefix, filter) {
371
- const relevantHistory = history.filter((h)=>h.status.startsWith(computeOverPrefix));
372
- 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));
373
366
  return {
374
367
  currentStreak: countWhile([
375
368
  ...relevantHistory
376
- ].reverse(), (h)=>h.status === filter),
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.0.0-nightly.20250905",
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": "3.0.0-nightly.20250905",
70
- "@aztec/bb-prover": "3.0.0-nightly.20250905",
71
- "@aztec/blob-sink": "3.0.0-nightly.20250905",
72
- "@aztec/constants": "3.0.0-nightly.20250905",
73
- "@aztec/epoch-cache": "3.0.0-nightly.20250905",
74
- "@aztec/ethereum": "3.0.0-nightly.20250905",
75
- "@aztec/foundation": "3.0.0-nightly.20250905",
76
- "@aztec/kv-store": "3.0.0-nightly.20250905",
77
- "@aztec/l1-artifacts": "3.0.0-nightly.20250905",
78
- "@aztec/merkle-tree": "3.0.0-nightly.20250905",
79
- "@aztec/node-keystore": "3.0.0-nightly.20250905",
80
- "@aztec/node-lib": "3.0.0-nightly.20250905",
81
- "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20250905",
82
- "@aztec/p2p": "3.0.0-nightly.20250905",
83
- "@aztec/protocol-contracts": "3.0.0-nightly.20250905",
84
- "@aztec/prover-client": "3.0.0-nightly.20250905",
85
- "@aztec/sequencer-client": "3.0.0-nightly.20250905",
86
- "@aztec/simulator": "3.0.0-nightly.20250905",
87
- "@aztec/slasher": "3.0.0-nightly.20250905",
88
- "@aztec/stdlib": "3.0.0-nightly.20250905",
89
- "@aztec/telemetry-client": "3.0.0-nightly.20250905",
90
- "@aztec/validator-client": "3.0.0-nightly.20250905",
91
- "@aztec/world-state": "3.0.0-nightly.20250905",
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",
@@ -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, 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 { 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,
@@ -115,53 +121,38 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
115
121
  return;
116
122
  }
117
123
 
118
- 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());
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 headers = await this.archiver.getBlockHeadersForEpoch(epoch);
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
- protected updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
164
- return this.store.updateProvenPerformance(epoch, performance);
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 = Object.entries(performance)
203
- .filter(([_, { missed, total }]) => {
204
- return missed / total >= this.config.slashInactivityTargetPercentage;
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
- ...(block?.attestors.map(a => a.toString()) ?? []),
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: _fromSlot,
382
- toSlot: _toSlot,
383
- }: { fromSlot?: bigint; toSlot?: bigint } = {}): Promise<ValidatorsStats> {
384
- 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
+
385
381
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
386
- const fromSlot = _fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
387
- const toSlot = _toSlot ?? this.lastProcessedSlot ?? slotNow;
388
- const result: Record<`0x${string}`, ValidatorStats> = {};
389
- for (const [address, history] of Object.entries(histories)) {
390
- const validatorAddress = address as `0x${string}`;
391
- result[validatorAddress] = this.computeStatsForValidator(validatorAddress, history, fromSlot, toSlot);
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: result,
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
- history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1)?.slot,
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 === filter);
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 === filter),
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