@aztec/aztec-node 2.0.0-nightly.20250902 → 3.0.0-nightly.20250904

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.
@@ -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. */
@@ -36,7 +36,14 @@ export declare class Sentinel extends Sentinel_base implements L2BlockStreamEven
36
36
  protected handleChainProven(event: L2BlockStreamEvent): Promise<void>;
37
37
  protected computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance>;
38
38
  protected updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): Promise<void>;
39
- protected handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): void;
39
+ /**
40
+ * Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
41
+ * @param validator The validator address to check
42
+ * @param currentEpoch Epochs strictly before the current one are evaluated only
43
+ * @param requiredConsecutiveEpochs Number of consecutive epochs required for slashing
44
+ */
45
+ protected checkPastInactivity(validator: EthAddress, currentEpoch: bigint, requiredConsecutiveEpochs: number): Promise<boolean>;
46
+ protected handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance): Promise<void>;
40
47
  /**
41
48
  * Process data for two L2 slots ago.
42
49
  * Note that we do not process historical data, since we rely on p2p data for processing,
@@ -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":"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"}
@@ -1,4 +1,4 @@
1
- import { countWhile } from '@aztec/foundation/collection';
1
+ import { countWhile, filterAsync } 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';
@@ -87,7 +87,7 @@ export class Sentinel extends EventEmitter {
87
87
  const performance = await this.computeProvenPerformance(epoch);
88
88
  this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
89
89
  await this.updateProvenPerformance(epoch, performance);
90
- this.handleProvenPerformance(epoch, performance);
90
+ await this.handleProvenPerformance(epoch, performance);
91
91
  }
92
92
  async computeProvenPerformance(epoch) {
93
93
  const headers = await this.archiver.getBlockHeadersForEpoch(epoch);
@@ -132,10 +132,36 @@ export class Sentinel extends EventEmitter {
132
132
  updateProvenPerformance(epoch, performance) {
133
133
  return this.store.updateProvenPerformance(epoch, performance);
134
134
  }
135
- handleProvenPerformance(epoch, performance) {
136
- const criminals = Object.entries(performance).filter(([_, { missed, total }])=>{
135
+ /**
136
+ * Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
137
+ * @param validator The validator address to check
138
+ * @param currentEpoch Epochs strictly before the current one are evaluated only
139
+ * @param requiredConsecutiveEpochs Number of consecutive epochs required for slashing
140
+ */ async checkPastInactivity(validator, currentEpoch, requiredConsecutiveEpochs) {
141
+ if (requiredConsecutiveEpochs === 0) {
142
+ return true;
143
+ }
144
+ // Get all historical performance for this validator
145
+ const allPerformance = await this.store.getProvenPerformance(validator);
146
+ // If we don't have enough historical data, don't slash
147
+ if (allPerformance.length < requiredConsecutiveEpochs) {
148
+ this.logger.debug(`Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`);
149
+ return false;
150
+ }
151
+ // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
152
+ 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
+ }
154
+ async handleProvenPerformance(epoch, performance) {
155
+ const inactiveValidators = Object.entries(performance).filter(([_, { missed, total }])=>{
137
156
  return missed / total >= this.config.slashInactivityTargetPercentage;
138
157
  }).map(([address])=>address);
158
+ this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
159
+ inactiveValidators,
160
+ epoch,
161
+ inactivityTargetPercentage: this.config.slashInactivityTargetPercentage
162
+ });
163
+ const epochThreshold = this.config.slashInactivityConsecutiveEpochThreshold;
164
+ const criminals = await filterAsync(inactiveValidators, (address)=>this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1));
139
165
  const args = criminals.map((address)=>({
140
166
  validator: EthAddress.fromString(address),
141
167
  amount: this.config.slashInactivityPenalty,
@@ -143,8 +169,9 @@ export class Sentinel extends EventEmitter {
143
169
  epochOrSlot: epoch
144
170
  }));
145
171
  if (criminals.length > 0) {
146
- this.logger.info(`Identified ${criminals.length} validators to slash due to inactivity`, {
147
- args
172
+ this.logger.info(`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`, {
173
+ ...args,
174
+ epochThreshold
148
175
  });
149
176
  this.emit(WANT_TO_SLASH_EVENT, args);
150
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/aztec-node",
3
- "version": "2.0.0-nightly.20250902",
3
+ "version": "3.0.0-nightly.20250904",
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.20250902",
70
- "@aztec/bb-prover": "2.0.0-nightly.20250902",
71
- "@aztec/blob-sink": "2.0.0-nightly.20250902",
72
- "@aztec/constants": "2.0.0-nightly.20250902",
73
- "@aztec/epoch-cache": "2.0.0-nightly.20250902",
74
- "@aztec/ethereum": "2.0.0-nightly.20250902",
75
- "@aztec/foundation": "2.0.0-nightly.20250902",
76
- "@aztec/kv-store": "2.0.0-nightly.20250902",
77
- "@aztec/l1-artifacts": "2.0.0-nightly.20250902",
78
- "@aztec/merkle-tree": "2.0.0-nightly.20250902",
79
- "@aztec/node-keystore": "2.0.0-nightly.20250902",
80
- "@aztec/node-lib": "2.0.0-nightly.20250902",
81
- "@aztec/noir-protocol-circuits-types": "2.0.0-nightly.20250902",
82
- "@aztec/p2p": "2.0.0-nightly.20250902",
83
- "@aztec/protocol-contracts": "2.0.0-nightly.20250902",
84
- "@aztec/prover-client": "2.0.0-nightly.20250902",
85
- "@aztec/sequencer-client": "2.0.0-nightly.20250902",
86
- "@aztec/simulator": "2.0.0-nightly.20250902",
87
- "@aztec/slasher": "2.0.0-nightly.20250902",
88
- "@aztec/stdlib": "2.0.0-nightly.20250902",
89
- "@aztec/telemetry-client": "2.0.0-nightly.20250902",
90
- "@aztec/validator-client": "2.0.0-nightly.20250902",
91
- "@aztec/world-state": "2.0.0-nightly.20250902",
69
+ "@aztec/archiver": "3.0.0-nightly.20250904",
70
+ "@aztec/bb-prover": "3.0.0-nightly.20250904",
71
+ "@aztec/blob-sink": "3.0.0-nightly.20250904",
72
+ "@aztec/constants": "3.0.0-nightly.20250904",
73
+ "@aztec/epoch-cache": "3.0.0-nightly.20250904",
74
+ "@aztec/ethereum": "3.0.0-nightly.20250904",
75
+ "@aztec/foundation": "3.0.0-nightly.20250904",
76
+ "@aztec/kv-store": "3.0.0-nightly.20250904",
77
+ "@aztec/l1-artifacts": "3.0.0-nightly.20250904",
78
+ "@aztec/merkle-tree": "3.0.0-nightly.20250904",
79
+ "@aztec/node-keystore": "3.0.0-nightly.20250904",
80
+ "@aztec/node-lib": "3.0.0-nightly.20250904",
81
+ "@aztec/noir-protocol-circuits-types": "3.0.0-nightly.20250904",
82
+ "@aztec/p2p": "3.0.0-nightly.20250904",
83
+ "@aztec/protocol-contracts": "3.0.0-nightly.20250904",
84
+ "@aztec/prover-client": "3.0.0-nightly.20250904",
85
+ "@aztec/sequencer-client": "3.0.0-nightly.20250904",
86
+ "@aztec/simulator": "3.0.0-nightly.20250904",
87
+ "@aztec/slasher": "3.0.0-nightly.20250904",
88
+ "@aztec/stdlib": "3.0.0-nightly.20250904",
89
+ "@aztec/telemetry-client": "3.0.0-nightly.20250904",
90
+ "@aztec/validator-client": "3.0.0-nightly.20250904",
91
+ "@aztec/world-state": "3.0.0-nightly.20250904",
92
92
  "koa": "^2.16.1",
93
93
  "koa-router": "^12.0.0",
94
94
  "tslib": "^2.4.0",
@@ -1,5 +1,5 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
- import { countWhile } from '@aztec/foundation/collection';
2
+ import { countWhile, filterAsync } 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';
@@ -44,7 +44,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
44
44
  protected archiver: L2BlockSource,
45
45
  protected p2p: P2PClient,
46
46
  protected store: SentinelStore,
47
- protected config: Pick<SlasherConfig, 'slashInactivityTargetPercentage' | 'slashInactivityPenalty'>,
47
+ protected config: Pick<
48
+ SlasherConfig,
49
+ 'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashInactivityConsecutiveEpochThreshold'
50
+ >,
48
51
  protected logger = createLogger('node:sentinel'),
49
52
  ) {
50
53
  super();
@@ -118,7 +121,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
118
121
  this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
119
122
 
120
123
  await this.updateProvenPerformance(epoch, performance);
121
- this.handleProvenPerformance(epoch, performance);
124
+ await this.handleProvenPerformance(epoch, performance);
122
125
  }
123
126
 
124
127
  protected async computeProvenPerformance(epoch: bigint) {
@@ -161,13 +164,58 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
161
164
  return this.store.updateProvenPerformance(epoch, performance);
162
165
  }
163
166
 
164
- protected handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
165
- const criminals = Object.entries(performance)
167
+ /**
168
+ * Checks if a validator has been inactive for the specified number of consecutive epochs for which we have data on it.
169
+ * @param validator The validator address to check
170
+ * @param currentEpoch Epochs strictly before the current one are evaluated only
171
+ * @param requiredConsecutiveEpochs Number of consecutive epochs required for slashing
172
+ */
173
+ protected async checkPastInactivity(
174
+ validator: EthAddress,
175
+ currentEpoch: bigint,
176
+ requiredConsecutiveEpochs: number,
177
+ ): Promise<boolean> {
178
+ if (requiredConsecutiveEpochs === 0) {
179
+ return true;
180
+ }
181
+
182
+ // Get all historical performance for this validator
183
+ const allPerformance = await this.store.getProvenPerformance(validator);
184
+
185
+ // If we don't have enough historical data, don't slash
186
+ if (allPerformance.length < requiredConsecutiveEpochs) {
187
+ this.logger.debug(
188
+ `Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
189
+ );
190
+ return false;
191
+ }
192
+
193
+ // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
194
+ return allPerformance
195
+ .sort((a, b) => Number(b.epoch - a.epoch))
196
+ .filter(p => p.epoch < currentEpoch)
197
+ .slice(0, requiredConsecutiveEpochs)
198
+ .every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
199
+ }
200
+
201
+ protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
202
+ const inactiveValidators = Object.entries(performance)
166
203
  .filter(([_, { missed, total }]) => {
167
204
  return missed / total >= this.config.slashInactivityTargetPercentage;
168
205
  })
169
206
  .map(([address]) => address as `0x${string}`);
170
207
 
208
+ this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
209
+ inactiveValidators,
210
+ epoch,
211
+ inactivityTargetPercentage: this.config.slashInactivityTargetPercentage,
212
+ });
213
+
214
+ const epochThreshold = this.config.slashInactivityConsecutiveEpochThreshold;
215
+ const criminals: string[] = await filterAsync(inactiveValidators, address =>
216
+ this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1),
217
+ );
218
+
171
219
  const args = criminals.map(address => ({
172
220
  validator: EthAddress.fromString(address),
173
221
  amount: this.config.slashInactivityPenalty,
@@ -176,7 +224,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
176
224
  }));
177
225
 
178
226
  if (criminals.length > 0) {
179
- this.logger.info(`Identified ${criminals.length} validators to slash due to inactivity`, { args });
227
+ this.logger.info(
228
+ `Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
229
+ { ...args, epochThreshold },
230
+ );
180
231
  this.emit(WANT_TO_SLASH_EVENT, args);
181
232
  }
182
233
  }