@aztec/slasher 0.0.1-commit.24de95ac → 0.0.1-commit.2e2504e2

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.
Files changed (67) hide show
  1. package/dest/config.d.ts +1 -1
  2. package/dest/config.d.ts.map +1 -1
  3. package/dest/config.js +16 -16
  4. package/dest/empire_slasher_client.d.ts +6 -5
  5. package/dest/empire_slasher_client.d.ts.map +1 -1
  6. package/dest/empire_slasher_client.js +1 -9
  7. package/dest/factory/create_facade.d.ts +3 -2
  8. package/dest/factory/create_facade.d.ts.map +1 -1
  9. package/dest/factory/create_facade.js +1 -1
  10. package/dest/factory/create_implementation.d.ts +3 -3
  11. package/dest/factory/create_implementation.d.ts.map +1 -1
  12. package/dest/factory/get_settings.d.ts +2 -2
  13. package/dest/factory/get_settings.d.ts.map +1 -1
  14. package/dest/factory/index.d.ts +1 -1
  15. package/dest/generated/slasher-defaults.d.ts +19 -0
  16. package/dest/generated/slasher-defaults.d.ts.map +1 -0
  17. package/dest/generated/slasher-defaults.js +19 -0
  18. package/dest/index.d.ts +1 -1
  19. package/dest/null_slasher_client.d.ts +3 -2
  20. package/dest/null_slasher_client.d.ts.map +1 -1
  21. package/dest/slash_offenses_collector.d.ts +1 -1
  22. package/dest/slash_offenses_collector.d.ts.map +1 -1
  23. package/dest/slash_offenses_collector.js +5 -1
  24. package/dest/slash_round_monitor.d.ts +5 -4
  25. package/dest/slash_round_monitor.d.ts.map +1 -1
  26. package/dest/slasher_client_facade.d.ts +4 -3
  27. package/dest/slasher_client_facade.d.ts.map +1 -1
  28. package/dest/slasher_client_interface.d.ts +3 -2
  29. package/dest/slasher_client_interface.d.ts.map +1 -1
  30. package/dest/stores/offenses_store.d.ts +1 -1
  31. package/dest/stores/offenses_store.d.ts.map +1 -1
  32. package/dest/stores/offenses_store.js +4 -2
  33. package/dest/stores/payloads_store.d.ts +2 -2
  34. package/dest/stores/payloads_store.d.ts.map +1 -1
  35. package/dest/stores/payloads_store.js +6 -3
  36. package/dest/stores/schema_version.d.ts +1 -1
  37. package/dest/tally_slasher_client.d.ts +5 -9
  38. package/dest/tally_slasher_client.d.ts.map +1 -1
  39. package/dest/tally_slasher_client.js +11 -6
  40. package/dest/test/dummy_watcher.d.ts +1 -1
  41. package/dest/test/dummy_watcher.d.ts.map +1 -1
  42. package/dest/watcher.d.ts +1 -1
  43. package/dest/watcher.d.ts.map +1 -1
  44. package/dest/watchers/attestations_block_watcher.d.ts +7 -6
  45. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  46. package/dest/watchers/attestations_block_watcher.js +42 -35
  47. package/dest/watchers/epoch_prune_watcher.d.ts +9 -7
  48. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  49. package/dest/watchers/epoch_prune_watcher.js +57 -17
  50. package/package.json +17 -14
  51. package/src/config.ts +17 -16
  52. package/src/empire_slasher_client.ts +6 -13
  53. package/src/factory/create_facade.ts +3 -2
  54. package/src/factory/create_implementation.ts +1 -1
  55. package/src/factory/get_settings.ts +1 -1
  56. package/src/generated/slasher-defaults.ts +21 -0
  57. package/src/null_slasher_client.ts +2 -1
  58. package/src/slash_offenses_collector.ts +5 -1
  59. package/src/slash_round_monitor.ts +3 -2
  60. package/src/slasher_client_facade.ts +3 -2
  61. package/src/slasher_client_interface.ts +2 -1
  62. package/src/stores/offenses_store.ts +4 -2
  63. package/src/stores/payloads_store.ts +7 -4
  64. package/src/tally_slasher_client.ts +21 -10
  65. package/src/watcher.ts +1 -1
  66. package/src/watchers/attestations_block_watcher.ts +59 -45
  67. package/src/watchers/epoch_prune_watcher.ts +88 -29
@@ -1,9 +1,9 @@
1
- import { EmpireSlashingProposerContract, RollupContract, SlasherContract } from '@aztec/ethereum';
1
+ import { EmpireSlashingProposerContract, RollupContract, SlasherContract } from '@aztec/ethereum/contracts';
2
2
  import { sumBigint } from '@aztec/foundation/bigint';
3
+ import { SlotNumber } from '@aztec/foundation/branded-types';
3
4
  import { compactArray, filterAsync, maxBy, pick } from '@aztec/foundation/collection';
4
5
  import { EthAddress } from '@aztec/foundation/eth-address';
5
6
  import { createLogger } from '@aztec/foundation/log';
6
- import { sleep } from '@aztec/foundation/sleep';
7
7
  import type { DateProvider } from '@aztec/foundation/timer';
8
8
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
9
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
@@ -195,13 +195,6 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
195
195
  this.roundMonitor.stop();
196
196
  await this.offensesCollector.stop();
197
197
 
198
- // Viem calls eth_uninstallFilter under the hood when uninstalling event watchers, but these calls are not awaited,
199
- // meaning that any error that happens during the uninstallation will not be caught. This causes errors during jest teardowns,
200
- // where we stop anvil after all other processes are stopped, so sometimes the eth_uninstallFilter call fails because anvil
201
- // is already stopped. We add a sleep here to give the uninstallation some time to complete, but the proper fix is for
202
- // viem to await the eth_uninstallFilter calls, or to catch any errors that happen during the uninstallation.
203
- // See https://github.com/wevm/viem/issues/3714.
204
- await sleep(2000);
205
198
  this.log.info('Empire Slasher client stopped');
206
199
  }
207
200
 
@@ -369,7 +362,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
369
362
  * @param slotNumber - The current slot number
370
363
  * @returns The actions to take
371
364
  */
372
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
365
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
373
366
  const [executeAction, proposePayloadActions] = await Promise.all([
374
367
  this.getExecutePayloadAction(slotNumber),
375
368
  this.getProposePayloadActions(slotNumber),
@@ -379,7 +372,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
379
372
  }
380
373
 
381
374
  /** Returns an execute payload action if there are any payloads ready to be executed */
382
- protected async getExecutePayloadAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
375
+ protected async getExecutePayloadAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
383
376
  const { round } = this.roundMonitor.getRoundForSlot(slotNumber);
384
377
  const toRemove: PayloadWithRound[] = [];
385
378
 
@@ -430,7 +423,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
430
423
  }
431
424
 
432
425
  /** Returns a vote or create payload action based on payload scoring */
433
- protected async getProposePayloadActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
426
+ protected async getProposePayloadActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
434
427
  // Compute what round we are in based on the slot number
435
428
  const { round, votingSlot } = this.roundMonitor.getRoundForSlot(slotNumber);
436
429
  const { slashingRoundSize: roundSize, slashingQuorumSize: quorumSize } = this.settings;
@@ -473,7 +466,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
473
466
  // Find the best existing payload. We filter out those that have no chance of winning given how many voting
474
467
  // slots are left in the round, and then filter by those we agree with.
475
468
  const feasiblePayloads = existingPayloads.filter(
476
- p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - votingSlot,
469
+ p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - BigInt(votingSlot),
477
470
  );
478
471
  const requiredOffenses = await this.getPendingUncontroversialOffensesForRound(round);
479
472
  const agreedPayloads = await filterAsync(feasiblePayloads, p => this.agreeWithPayload(p, round, requiredOffenses));
@@ -1,6 +1,7 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { L1ReaderConfig, ViemClient } from '@aztec/ethereum';
3
2
  import { RollupContract } from '@aztec/ethereum/contracts';
3
+ import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
4
+ import type { ViemClient } from '@aztec/ethereum/types';
4
5
  import { unique } from '@aztec/foundation/collection';
5
6
  import { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { createLogger } from '@aztec/foundation/log';
@@ -30,7 +31,7 @@ export async function createSlasherFacade(
30
31
  throw new Error('Cannot initialize SlasherClient without a Rollup address');
31
32
  }
32
33
 
33
- const kvStore = await createStore('slasher', SCHEMA_VERSION, config, createLogger('slasher:lmdb'));
34
+ const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
34
35
  const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
35
36
 
36
37
  const slashValidatorsNever = config.slashSelfAllowed
@@ -1,10 +1,10 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { ViemClient } from '@aztec/ethereum';
3
2
  import {
4
3
  EmpireSlashingProposerContract,
5
4
  RollupContract,
6
5
  TallySlashingProposerContract,
7
6
  } from '@aztec/ethereum/contracts';
7
+ import type { ViemClient } from '@aztec/ethereum/types';
8
8
  import { EthAddress } from '@aztec/foundation/eth-address';
9
9
  import { createLogger } from '@aztec/foundation/log';
10
10
  import { DateProvider } from '@aztec/foundation/timer';
@@ -1,4 +1,4 @@
1
- import type { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum';
1
+ import type { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
2
2
 
3
3
  import type { TallySlasherSettings } from '../tally_slasher_client.js';
4
4
 
@@ -0,0 +1,21 @@
1
+ // Auto-generated from spartan/environments/network-defaults.yml
2
+ // Do not edit manually - run yarn generate to regenerate
3
+
4
+ /** Default slasher configuration values from network-defaults.yml */
5
+ export const slasherDefaultEnv = {
6
+ SLASH_MIN_PENALTY_PERCENTAGE: 0.5,
7
+ SLASH_MAX_PENALTY_PERCENTAGE: 2,
8
+ SLASH_OFFENSE_EXPIRATION_ROUNDS: 4,
9
+ SLASH_MAX_PAYLOAD_SIZE: 50,
10
+ SLASH_EXECUTE_ROUNDS_LOOK_BACK: 4,
11
+ SLASH_PRUNE_PENALTY: 10000000000000000000,
12
+ SLASH_DATA_WITHHOLDING_PENALTY: 10000000000000000000,
13
+ SLASH_INACTIVITY_TARGET_PERCENTAGE: 0.9,
14
+ SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD: 1,
15
+ SLASH_INACTIVITY_PENALTY: 10000000000000000000,
16
+ SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000,
17
+ SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000,
18
+ SLASH_UNKNOWN_PENALTY: 10000000000000000000,
19
+ SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
20
+ SLASH_GRACE_PERIOD_L2_SLOTS: 0,
21
+ } as const;
@@ -1,3 +1,4 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
2
3
 
3
4
  import type { SlasherConfig } from './config.js';
@@ -30,7 +31,7 @@ export class NullSlasherClient implements SlasherClientInterface {
30
31
  this.config = { ...this.config, ...config };
31
32
  }
32
33
 
33
- public getProposerActions(_slotNumber: bigint): Promise<ProposerSlashAction[]> {
34
+ public getProposerActions(_slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
34
35
  return Promise.resolve([]);
35
36
  }
36
37
 
@@ -85,7 +85,11 @@ export class SlashOffensesCollector {
85
85
  }
86
86
  }
87
87
 
88
- this.log.info(`Adding pending offense for validator ${arg.validator}`, pendingOffense);
88
+ this.log.info(`Adding pending offense for validator ${arg.validator}`, {
89
+ ...pendingOffense,
90
+ epochOrSlot: pendingOffense.epochOrSlot.toString(),
91
+ amount: pendingOffense.amount.toString(),
92
+ });
89
93
  await this.offensesStore.addPendingOffense(pendingOffense);
90
94
  }
91
95
  }
@@ -1,3 +1,4 @@
1
+ import { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import { createLogger } from '@aztec/foundation/log';
2
3
  import type { DateProvider } from '@aztec/foundation/timer';
3
4
  import type { Prettify } from '@aztec/foundation/types';
@@ -48,12 +49,12 @@ export class SlashRoundMonitor {
48
49
  }
49
50
 
50
51
  /** Returns the slashing round number and the voting slot within the round based on the L2 chain slot */
51
- public getRoundForSlot(slotNumber: bigint): { round: bigint; votingSlot: bigint } {
52
+ public getRoundForSlot(slotNumber: SlotNumber): { round: bigint; votingSlot: SlotNumber } {
52
53
  return getRoundForSlot(slotNumber, this.settings);
53
54
  }
54
55
 
55
56
  /** Returns the current slashing round and voting slot within the round */
56
- public getCurrentRound(): { round: bigint; votingSlot: bigint } {
57
+ public getCurrentRound(): { round: bigint; votingSlot: SlotNumber } {
57
58
  const now = this.dateProvider.nowInSeconds();
58
59
  const currentSlot = getSlotAtTimestamp(BigInt(now), this.settings);
59
60
  return this.getRoundForSlot(currentSlot);
@@ -1,6 +1,7 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { ViemClient } from '@aztec/ethereum';
3
2
  import { RollupContract } from '@aztec/ethereum/contracts';
3
+ import type { ViemClient } from '@aztec/ethereum/types';
4
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
4
5
  import { EthAddress } from '@aztec/foundation/eth-address';
5
6
  import { createLogger } from '@aztec/foundation/log';
6
7
  import { DateProvider } from '@aztec/foundation/timer';
@@ -73,7 +74,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
73
74
  return this.client?.getPendingOffenses() ?? Promise.reject(new Error('Slasher client not initialized'));
74
75
  }
75
76
 
76
- public getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
77
+ public getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
77
78
  return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
78
79
  }
79
80
 
@@ -1,3 +1,4 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
2
3
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
3
4
 
@@ -38,7 +39,7 @@ export interface SlasherClientInterface {
38
39
  * @param slotNumber - The current slot number
39
40
  * @returns The actions to take
40
41
  */
41
- getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]>;
42
+ getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
42
43
 
43
44
  /** Returns the current config */
44
45
  getConfig(): SlasherConfig;
@@ -76,9 +76,11 @@ export class SlasherOffensesStore {
76
76
  /** Adds a new offense (defaults to pending, but will be slashed if markAsSlashed had been called for it) */
77
77
  public async addPendingOffense(offense: Offense): Promise<void> {
78
78
  const key = this.getOffenseKey(offense);
79
- await this.offenses.set(key, serializeOffense(offense));
80
79
  const round = getRoundForOffense(offense, this.settings);
81
- await this.roundsOffenses.set(this.getRoundKey(round), key);
80
+ await this.kvStore.transactionAsync(async () => {
81
+ await this.offenses.set(key, serializeOffense(offense));
82
+ await this.roundsOffenses.set(this.getRoundKey(round), key);
83
+ });
82
84
  this.log.trace(`Adding pending offense ${key} for round ${round}`);
83
85
  }
84
86
 
@@ -118,10 +118,13 @@ export class SlasherPayloadsStore {
118
118
 
119
119
  public async incrementPayloadVotes(payloadAddress: EthAddress, round: bigint): Promise<bigint> {
120
120
  const key = this.getPayloadVotesKey(round, payloadAddress);
121
- const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
122
- const newVotes = currentVotes + 1n;
123
- await this.roundPayloadVotes.set(key, newVotes);
124
- return newVotes;
121
+ let newVotes: bigint;
122
+ await this.kvStore.transactionAsync(async () => {
123
+ const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
124
+ newVotes = currentVotes + 1n;
125
+ await this.roundPayloadVotes.set(key, newVotes);
126
+ });
127
+ return newVotes!;
125
128
  }
126
129
 
127
130
  public async addPayload(payload: SlashPayloadRound): Promise<void> {
@@ -2,9 +2,9 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
3
  import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
4
4
  import { maxBigint } from '@aztec/foundation/bigint';
5
+ import { SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { compactArray, partition, times } from '@aztec/foundation/collection';
6
7
  import { createLogger } from '@aztec/foundation/log';
7
- import { sleep } from '@aztec/foundation/sleep';
8
8
  import type { DateProvider } from '@aztec/foundation/timer';
9
9
  import type { Prettify } from '@aztec/foundation/types';
10
10
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
@@ -137,8 +137,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
137
137
  this.roundMonitor.stop();
138
138
  await this.offensesCollector.stop();
139
139
 
140
- // Sleeping to sidestep viem issue with unwatching events
141
- await sleep(2000);
142
140
  this.log.info('Tally Slasher client stopped');
143
141
  }
144
142
 
@@ -169,7 +167,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
169
167
  * @param slotNumber - The current slot number
170
168
  * @returns The actions to take
171
169
  */
172
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
170
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
173
171
  const [executeAction, voteAction] = await Promise.all([
174
172
  this.getExecuteSlashAction(slotNumber),
175
173
  this.getVoteOffensesAction(slotNumber),
@@ -182,7 +180,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
182
180
  * Returns an execute slash action if there are any rounds ready to be executed.
183
181
  * Returns the oldest slash action if there are multiple rounds pending execution.
184
182
  */
185
- protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
183
+ protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
186
184
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
187
185
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
188
186
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
@@ -233,7 +231,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
233
231
  */
234
232
  private async tryGetRoundExecuteAction(
235
233
  executableRound: bigint,
236
- slotNumber: bigint,
234
+ slotNumber: SlotNumber,
237
235
  ): Promise<ProposerSlashAction | undefined> {
238
236
  let logData: Record<string, unknown> = { executableRound, slotNumber };
239
237
  this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
@@ -280,8 +278,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
280
278
  return undefined;
281
279
  }
282
280
 
281
+ const slashActionsWithAmounts = slashActions.map(action => ({
282
+ validator: action.validator.toString(),
283
+ slashAmount: action.slashAmount.toString(),
284
+ }));
283
285
  this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
284
- slashActions,
286
+ slashActions: slashActionsWithAmounts,
285
287
  payloadAddress: payload.address.toString(),
286
288
  ...logData,
287
289
  });
@@ -302,7 +304,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
302
304
  }
303
305
 
304
306
  /** Returns a vote action based on offenses from the target round (with offset applied) */
305
- protected async getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
307
+ protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
306
308
  // Compute what round we are in based on the slot number and what round will be slashed
307
309
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
308
310
  const slashedRound = this.getSlashedRound(currentRound);
@@ -347,16 +349,25 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
347
349
  return undefined;
348
350
  }
349
351
 
352
+ const offensesToSlashLog = offensesToSlash.map(offense => ({
353
+ ...offense,
354
+ amount: offense.amount.toString(),
355
+ }));
350
356
  this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
351
357
  slotNumber,
352
358
  currentRound,
353
359
  slashedRound,
354
- offensesToSlash,
360
+ offensesToSlash: offensesToSlashLog,
355
361
  });
356
362
 
357
363
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
358
364
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
359
- const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees, this.settings);
365
+ const votes = getSlashConsensusVotesFromOffenses(
366
+ offensesToSlash,
367
+ committees,
368
+ epochsForCommittees.map(e => BigInt(e)),
369
+ this.settings,
370
+ );
360
371
  if (votes.every(v => v === 0)) {
361
372
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
362
373
  slotNumber,
package/src/watcher.ts CHANGED
@@ -10,7 +10,7 @@ export interface WantToSlashArgs {
10
10
  validator: EthAddress;
11
11
  amount: bigint;
12
12
  offenseType: OffenseType;
13
- epochOrSlot: bigint; // Epoch number for epoch-based offenses, block number for block-based
13
+ epochOrSlot: bigint; // Epoch number for epoch-based offenses, slot number for slot-based
14
14
  }
15
15
 
16
16
  // Event map for specific, known events of a watcher
@@ -1,13 +1,14 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
+ import { SlotNumber } from '@aztec/foundation/branded-types';
2
3
  import { merge, pick } from '@aztec/foundation/collection';
3
4
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
5
  import {
5
- type InvalidBlockDetectedEvent,
6
- type L2BlockInfo,
6
+ type InvalidCheckpointDetectedEvent,
7
7
  type L2BlockSourceEventEmitter,
8
8
  L2BlockSourceEvents,
9
- type ValidateBlockNegativeResult,
9
+ type ValidateCheckpointNegativeResult,
10
10
  } from '@aztec/stdlib/block';
11
+ import type { CheckpointInfo } from '@aztec/stdlib/checkpoint';
11
12
  import { OffenseType } from '@aztec/stdlib/slashing';
12
13
 
13
14
  import EventEmitter from 'node:events';
@@ -31,19 +32,19 @@ type AttestationsBlockWatcherConfig = Pick<SlasherConfig, (typeof AttestationsBl
31
32
  export class AttestationsBlockWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
32
33
  private log: Logger = createLogger('attestations-block-watcher');
33
34
 
34
- // Only keep track of the last N invalid blocks
35
- private maxInvalidBlocks = 100;
35
+ // Only keep track of the last N invalid checkpoints
36
+ private maxInvalidCheckpoints = 100;
36
37
 
37
38
  // All invalid archive roots seen
38
39
  private invalidArchiveRoots: Set<string> = new Set();
39
40
 
40
41
  private config: AttestationsBlockWatcherConfig;
41
42
 
42
- private boundHandleInvalidBlock = (event: InvalidBlockDetectedEvent) => {
43
+ private boundHandleInvalidCheckpoint = (event: InvalidCheckpointDetectedEvent) => {
43
44
  try {
44
- this.handleInvalidBlock(event);
45
+ this.handleInvalidCheckpoint(event);
45
46
  } catch (err) {
46
- this.log.error('Error handling invalid block', err, {
47
+ this.log.error('Error handling invalid checkpoint', err, {
47
48
  ...event.validationResult,
48
49
  reason: event.validationResult.reason,
49
50
  });
@@ -66,54 +67,61 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
66
67
  }
67
68
 
68
69
  public start() {
69
- this.l2BlockSource.on(L2BlockSourceEvents.InvalidAttestationsBlockDetected, this.boundHandleInvalidBlock);
70
+ this.l2BlockSource.events.on(
71
+ L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
72
+ this.boundHandleInvalidCheckpoint,
73
+ );
70
74
  return Promise.resolve();
71
75
  }
72
76
 
73
77
  public stop() {
74
- this.l2BlockSource.removeListener(
75
- L2BlockSourceEvents.InvalidAttestationsBlockDetected,
76
- this.boundHandleInvalidBlock,
78
+ this.l2BlockSource.events.removeListener(
79
+ L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
80
+ this.boundHandleInvalidCheckpoint,
77
81
  );
78
82
  return Promise.resolve();
79
83
  }
80
84
 
81
- private handleInvalidBlock(event: InvalidBlockDetectedEvent): void {
85
+ /** Event handler for invalid checkpoints as reported by the archiver. Public for testing purposes. */
86
+ public handleInvalidCheckpoint(event: InvalidCheckpointDetectedEvent): void {
82
87
  const { validationResult } = event;
83
- const block = validationResult.block;
88
+ const checkpoint = validationResult.checkpoint;
84
89
 
85
- // Check if we already have processed this block, archiver may emit the same event multiple times
86
- if (this.invalidArchiveRoots.has(block.archive.toString())) {
87
- this.log.trace(`Already processed invalid block ${block.blockNumber}`);
90
+ // Check if we already have processed this checkpoint, archiver may emit the same event multiple times
91
+ if (this.invalidArchiveRoots.has(checkpoint.archive.toString())) {
92
+ this.log.trace(`Already processed invalid checkpoint ${checkpoint.checkpointNumber}`);
88
93
  return;
89
94
  }
90
95
 
91
- this.log.verbose(`Detected invalid block ${block.blockNumber}`, {
92
- ...block,
96
+ this.log.verbose(`Detected invalid checkpoint ${checkpoint.checkpointNumber}`, {
97
+ ...checkpoint,
93
98
  reason: validationResult.valid === false ? validationResult.reason : 'unknown',
94
99
  });
95
100
 
96
- // Store the invalid block
97
- this.addInvalidBlock(event.validationResult.block);
101
+ // Store the invalid checkpoint
102
+ this.addInvalidCheckpoint(event.validationResult.checkpoint);
98
103
 
99
- // Slash the proposer of the invalid block
104
+ // Slash the proposer of the invalid checkpoint
100
105
  this.slashProposer(event.validationResult);
101
106
 
102
- // Check if the parent of this block is invalid as well, if so, we will slash its attestors as well
107
+ // Check if the parent of this checkpoint is invalid as well, if so, we will slash its attestors as well
103
108
  this.slashAttestorsOnAncestorInvalid(event.validationResult);
104
109
  }
105
110
 
106
- private slashAttestorsOnAncestorInvalid(validationResult: ValidateBlockNegativeResult) {
107
- const block = validationResult.block;
111
+ private slashAttestorsOnAncestorInvalid(validationResult: ValidateCheckpointNegativeResult) {
112
+ const checkpoint = validationResult.checkpoint;
108
113
 
109
- const parentArchive = block.lastArchive.toString();
114
+ const parentArchive = checkpoint.lastArchive.toString();
110
115
  if (this.invalidArchiveRoots.has(parentArchive)) {
111
116
  const attestors = validationResult.attestors;
112
- this.log.info(`Want to slash attestors of block ${block.blockNumber} built on invalid block`, {
113
- ...block,
114
- ...attestors,
115
- parentArchive,
116
- });
117
+ this.log.info(
118
+ `Want to slash attestors of checkpoint ${checkpoint.checkpointNumber} built on invalid checkpoint`,
119
+ {
120
+ ...checkpoint,
121
+ ...attestors,
122
+ parentArchive,
123
+ },
124
+ );
117
125
 
118
126
  this.emit(
119
127
  WANT_TO_SLASH_EVENT,
@@ -121,20 +129,26 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
121
129
  validator: attestor,
122
130
  amount: this.config.slashAttestDescendantOfInvalidPenalty,
123
131
  offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
124
- epochOrSlot: BigInt(block.slotNumber),
132
+ epochOrSlot: BigInt(SlotNumber(checkpoint.slotNumber)),
125
133
  })),
126
134
  );
127
135
  }
128
136
  }
129
137
 
130
- private slashProposer(validationResult: ValidateBlockNegativeResult) {
131
- const { reason, block } = validationResult;
132
- const blockNumber = block.blockNumber;
133
- const slot = BigInt(block.slotNumber);
134
- const proposer = this.epochCache.getProposerFromEpochCommittee(validationResult, slot);
138
+ private slashProposer(validationResult: ValidateCheckpointNegativeResult) {
139
+ const { reason, checkpoint } = validationResult;
140
+ const checkpointNumber = checkpoint.checkpointNumber;
141
+ const slot = checkpoint.slotNumber;
142
+ const epochCommitteeInfo = {
143
+ committee: validationResult.committee,
144
+ seed: validationResult.seed,
145
+ epoch: validationResult.epoch,
146
+ isEscapeHatchOpen: false,
147
+ };
148
+ const proposer = this.epochCache.getProposerFromEpochCommittee(epochCommitteeInfo, slot);
135
149
 
136
150
  if (!proposer) {
137
- this.log.warn(`No proposer found for block ${blockNumber} at slot ${slot}`);
151
+ this.log.warn(`No proposer found for checkpoint ${checkpointNumber} at slot ${slot}`);
138
152
  return;
139
153
  }
140
154
 
@@ -144,18 +158,18 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
144
158
  validator: proposer,
145
159
  amount,
146
160
  offenseType: offense,
147
- epochOrSlot: slot,
161
+ epochOrSlot: BigInt(slot),
148
162
  };
149
163
 
150
- this.log.info(`Want to slash proposer of block ${blockNumber} due to ${reason}`, {
151
- ...block,
164
+ this.log.info(`Want to slash proposer of checkpoint ${checkpointNumber} due to ${reason}`, {
165
+ ...checkpoint,
152
166
  ...args,
153
167
  });
154
168
 
155
169
  this.emit(WANT_TO_SLASH_EVENT, [args]);
156
170
  }
157
171
 
158
- private getOffenseFromInvalidationReason(reason: ValidateBlockNegativeResult['reason']): OffenseType {
172
+ private getOffenseFromInvalidationReason(reason: ValidateCheckpointNegativeResult['reason']): OffenseType {
159
173
  switch (reason) {
160
174
  case 'invalid-attestation':
161
175
  return OffenseType.PROPOSED_INCORRECT_ATTESTATIONS;
@@ -168,11 +182,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
168
182
  }
169
183
  }
170
184
 
171
- private addInvalidBlock(block: L2BlockInfo) {
172
- this.invalidArchiveRoots.add(block.archive.toString());
185
+ private addInvalidCheckpoint(checkpoint: CheckpointInfo) {
186
+ this.invalidArchiveRoots.add(checkpoint.archive.toString());
173
187
 
174
188
  // Prune old entries if we exceed the maximum
175
- if (this.invalidArchiveRoots.size > this.maxInvalidBlocks) {
189
+ if (this.invalidArchiveRoots.size > this.maxInvalidCheckpoints) {
176
190
  const oldestKey = this.invalidArchiveRoots.keys().next().value!;
177
191
  this.invalidArchiveRoots.delete(oldestKey);
178
192
  }