@aztec/slasher 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.1

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 (69) hide show
  1. package/README.md +60 -11
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +8 -2
  5. package/dest/empire_slasher_client.d.ts +8 -6
  6. package/dest/empire_slasher_client.d.ts.map +1 -1
  7. package/dest/empire_slasher_client.js +11 -5
  8. package/dest/factory/create_facade.d.ts +3 -2
  9. package/dest/factory/create_facade.d.ts.map +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/create_implementation.js +8 -30
  13. package/dest/factory/get_settings.d.ts +4 -0
  14. package/dest/factory/get_settings.d.ts.map +1 -0
  15. package/dest/factory/get_settings.js +36 -0
  16. package/dest/factory/index.d.ts +2 -1
  17. package/dest/factory/index.d.ts.map +1 -1
  18. package/dest/factory/index.js +1 -0
  19. package/dest/index.d.ts +1 -1
  20. package/dest/null_slasher_client.d.ts +3 -2
  21. package/dest/null_slasher_client.d.ts.map +1 -1
  22. package/dest/slash_offenses_collector.d.ts +1 -1
  23. package/dest/slash_offenses_collector.d.ts.map +1 -1
  24. package/dest/slash_offenses_collector.js +1 -2
  25. package/dest/slash_round_monitor.d.ts +5 -4
  26. package/dest/slash_round_monitor.d.ts.map +1 -1
  27. package/dest/slasher_client_facade.d.ts +4 -3
  28. package/dest/slasher_client_facade.d.ts.map +1 -1
  29. package/dest/slasher_client_facade.js +1 -0
  30. package/dest/slasher_client_interface.d.ts +3 -2
  31. package/dest/slasher_client_interface.d.ts.map +1 -1
  32. package/dest/stores/offenses_store.d.ts +1 -1
  33. package/dest/stores/offenses_store.d.ts.map +1 -1
  34. package/dest/stores/offenses_store.js +1 -1
  35. package/dest/stores/payloads_store.d.ts +2 -2
  36. package/dest/stores/payloads_store.d.ts.map +1 -1
  37. package/dest/stores/schema_version.d.ts +1 -1
  38. package/dest/tally_slasher_client.d.ts +14 -8
  39. package/dest/tally_slasher_client.d.ts.map +1 -1
  40. package/dest/tally_slasher_client.js +63 -11
  41. package/dest/test/dummy_watcher.d.ts +11 -0
  42. package/dest/test/dummy_watcher.d.ts.map +1 -0
  43. package/dest/test/dummy_watcher.js +14 -0
  44. package/dest/watcher.d.ts +3 -1
  45. package/dest/watcher.d.ts.map +1 -1
  46. package/dest/watchers/attestations_block_watcher.d.ts +6 -3
  47. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  48. package/dest/watchers/attestations_block_watcher.js +31 -21
  49. package/dest/watchers/epoch_prune_watcher.d.ts +8 -7
  50. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  51. package/dest/watchers/epoch_prune_watcher.js +48 -37
  52. package/package.json +13 -12
  53. package/src/config.ts +8 -2
  54. package/src/empire_slasher_client.ts +15 -8
  55. package/src/factory/create_facade.ts +2 -1
  56. package/src/factory/create_implementation.ts +9 -41
  57. package/src/factory/get_settings.ts +58 -0
  58. package/src/factory/index.ts +1 -0
  59. package/src/null_slasher_client.ts +2 -1
  60. package/src/slash_offenses_collector.ts +1 -2
  61. package/src/slash_round_monitor.ts +3 -2
  62. package/src/slasher_client_facade.ts +4 -2
  63. package/src/slasher_client_interface.ts +2 -1
  64. package/src/stores/offenses_store.ts +1 -1
  65. package/src/tally_slasher_client.ts +84 -16
  66. package/src/test/dummy_watcher.ts +21 -0
  67. package/src/watcher.ts +4 -1
  68. package/src/watchers/attestations_block_watcher.ts +38 -25
  69. package/src/watchers/epoch_prune_watcher.ts +67 -55
@@ -1,6 +1,8 @@
1
- import { EthAddress } from '@aztec/aztec.js';
1
+ import { EthAddress } from '@aztec/aztec.js/addresses';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
- import { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
3
+ import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
4
+ import { maxBigint } from '@aztec/foundation/bigint';
5
+ import { SlotNumber } from '@aztec/foundation/branded-types';
4
6
  import { compactArray, partition, times } from '@aztec/foundation/collection';
5
7
  import { createLogger } from '@aztec/foundation/log';
6
8
  import { sleep } from '@aztec/foundation/sleep';
@@ -45,7 +47,7 @@ export type TallySlasherSettings = Prettify<
45
47
  >;
46
48
 
47
49
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
48
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
50
+ Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
51
 
50
52
  /**
51
53
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -88,6 +90,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
88
90
  private config: TallySlasherClientConfig,
89
91
  private settings: TallySlasherSettings,
90
92
  private tallySlashingProposer: TallySlashingProposerContract,
93
+ private slasher: SlasherContract,
91
94
  private rollup: RollupContract,
92
95
  watchers: Watcher[],
93
96
  private epochCache: EpochCache,
@@ -167,7 +170,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
167
170
  * @param slotNumber - The current slot number
168
171
  * @returns The actions to take
169
172
  */
170
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
173
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
171
174
  const [executeAction, voteAction] = await Promise.all([
172
175
  this.getExecuteSlashAction(slotNumber),
173
176
  this.getVoteOffensesAction(slotNumber),
@@ -176,29 +179,91 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
176
179
  return compactArray<ProposerSlashAction>([executeAction, voteAction]);
177
180
  }
178
181
 
179
- /** Returns an execute slash action if there are any rounds ready to be executed */
180
- protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
182
+ /**
183
+ * Returns an execute slash action if there are any rounds ready to be executed.
184
+ * Returns the oldest slash action if there are multiple rounds pending execution.
185
+ */
186
+ protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
181
187
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
182
188
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
183
189
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
184
- if (executableRound < 0n) {
190
+ const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
191
+ const slashingLifetimeInRounds = BigInt(this.settings.slashingLifetimeInRounds);
192
+
193
+ // Compute the oldest executable round considering both lookBack and lifetimeInRounds
194
+ // A round is only executable if currentRound <= round + lifetimeInRounds
195
+ // So the oldest round we can execute is: currentRound - lifetimeInRounds
196
+ const oldestByLifetime = maxBigint(0n, currentRound - slashingLifetimeInRounds);
197
+ const oldestByLookBack = maxBigint(0n, executableRound - lookBack);
198
+ const oldestExecutableRound = maxBigint(oldestByLifetime, oldestByLookBack);
199
+
200
+ // Check if slashing is enabled at all
201
+ if (!(await this.slasher.isSlashingEnabled())) {
202
+ this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
185
203
  return undefined;
186
204
  }
187
205
 
188
- const logData = { currentRound, executableRound, slotNumber };
206
+ this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
207
+ slotNumber,
208
+ currentRound,
209
+ oldestExecutableRound,
210
+ oldestByLifetime,
211
+ oldestByLookBack,
212
+ executableRound,
213
+ slashingExecutionDelayInRounds,
214
+ lookBack,
215
+ slashingLifetimeInRounds,
216
+ });
217
+
218
+ // Iterate over all rounds, starting from the oldest, until we find one that is executable
219
+ for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
220
+ const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
221
+ if (action) {
222
+ return action;
223
+ }
224
+ }
225
+
226
+ // And return nothing if none are found
227
+ return undefined;
228
+ }
229
+
230
+ /**
231
+ * Checks if a given round is executable and returns an execute-slash action for it if so.
232
+ * Assumes round number has already been checked against lifetime and execution delay.
233
+ * @param executableRound - The round to check for execution
234
+ */
235
+ private async tryGetRoundExecuteAction(
236
+ executableRound: bigint,
237
+ slotNumber: SlotNumber,
238
+ ): Promise<ProposerSlashAction | undefined> {
239
+ let logData: Record<string, unknown> = { executableRound, slotNumber };
240
+ this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
241
+
189
242
  try {
190
243
  const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
244
+ logData = { ...logData, roundInfo };
191
245
  if (roundInfo.isExecuted) {
192
246
  this.log.verbose(`Round ${executableRound} has already been executed`, logData);
193
247
  return undefined;
194
- } else if (!roundInfo.readyToExecute) {
195
- this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
248
+ } else if (roundInfo.voteCount === 0n) {
249
+ this.log.debug(`Round ${executableRound} received no votes`, logData);
196
250
  return undefined;
197
251
  } else if (roundInfo.voteCount < this.settings.slashingQuorumSize) {
198
252
  this.log.verbose(`Round ${executableRound} does not have enough votes to execute`, logData);
199
253
  return undefined;
200
254
  }
201
255
 
256
+ // Check if round is ready to execute at the given slot
257
+ const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
258
+ if (!isReadyToExecute) {
259
+ this.log.warn(
260
+ `Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`,
261
+ logData,
262
+ );
263
+ return undefined;
264
+ }
265
+
266
+ // Check if the round yields any slashing at all
202
267
  const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
203
268
  if (slashActions.length === 0) {
204
269
  this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
@@ -207,8 +272,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
207
272
 
208
273
  // Check if the slash payload is vetoed
209
274
  const payload = await this.tallySlashingProposer.getPayload(executableRound);
210
- const slasherContract = await this.rollup.getSlasherContract();
211
- const isVetoed = await slasherContract.isPayloadVetoed(payload.address);
275
+ const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
212
276
  if (isVetoed) {
213
277
  this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
214
278
  payloadAddress: payload.address.toString(),
@@ -234,13 +298,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
234
298
  return { type: 'execute-slash', round: executableRound, committees: slashedCommittees };
235
299
  } catch (error) {
236
300
  this.log.error(`Error checking round to execute ${executableRound}`, error);
301
+ return undefined;
237
302
  }
238
-
239
- return undefined;
240
303
  }
241
304
 
242
305
  /** Returns a vote action based on offenses from the target round (with offset applied) */
243
- protected async getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
306
+ protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
244
307
  // Compute what round we are in based on the slot number and what round will be slashed
245
308
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
246
309
  const slashedRound = this.getSlashedRound(currentRound);
@@ -294,7 +357,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
294
357
 
295
358
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
296
359
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
297
- const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees, this.settings);
360
+ const votes = getSlashConsensusVotesFromOffenses(
361
+ offensesToSlash,
362
+ committees,
363
+ epochsForCommittees.map(e => BigInt(e)),
364
+ this.settings,
365
+ );
298
366
  if (votes.every(v => v === 0)) {
299
367
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
300
368
  slotNumber,
@@ -0,0 +1,21 @@
1
+ import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
2
+
3
+ import EventEmitter from 'node:events';
4
+
5
+ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
6
+
7
+ export class DummyWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
8
+ public updateConfig(_config: Partial<SlasherConfig>) {}
9
+
10
+ public start() {
11
+ return Promise.resolve();
12
+ }
13
+
14
+ public stop() {
15
+ return Promise.resolve();
16
+ }
17
+
18
+ public triggerSlash(args: WantToSlashArgs[]) {
19
+ this.emit(WANT_TO_SLASH_EVENT, args);
20
+ }
21
+ }
package/src/watcher.ts CHANGED
@@ -2,13 +2,15 @@ import { EthAddress } from '@aztec/foundation/eth-address';
2
2
  import type { TypedEventEmitter } from '@aztec/foundation/types';
3
3
  import { OffenseType } from '@aztec/stdlib/slashing';
4
4
 
5
+ import type { SlasherConfig } from './config.js';
6
+
5
7
  export const WANT_TO_SLASH_EVENT = 'want-to-slash' as const;
6
8
 
7
9
  export interface WantToSlashArgs {
8
10
  validator: EthAddress;
9
11
  amount: bigint;
10
12
  offenseType: OffenseType;
11
- 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
12
14
  }
13
15
 
14
16
  // Event map for specific, known events of a watcher
@@ -21,4 +23,5 @@ export type WatcherEmitter = TypedEventEmitter<WatcherEventMap>;
21
23
  export type Watcher = WatcherEmitter & {
22
24
  start?: () => Promise<void>;
23
25
  stop?: () => Promise<void>;
26
+ updateConfig: (config: Partial<SlasherConfig>) => void;
24
27
  };
@@ -1,10 +1,12 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
+ import { SlotNumber } from '@aztec/foundation/branded-types';
3
+ import { merge, pick } from '@aztec/foundation/collection';
2
4
  import { type Logger, createLogger } from '@aztec/foundation/log';
3
5
  import {
4
6
  type InvalidBlockDetectedEvent,
7
+ type L2BlockInfo,
5
8
  type L2BlockSourceEventEmitter,
6
9
  L2BlockSourceEvents,
7
- PublishedL2Block,
8
10
  type ValidateBlockNegativeResult,
9
11
  } from '@aztec/stdlib/block';
10
12
  import { OffenseType } from '@aztec/stdlib/slashing';
@@ -14,6 +16,13 @@ import EventEmitter from 'node:events';
14
16
  import type { SlasherConfig } from '../config.js';
15
17
  import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
16
18
 
19
+ const AttestationsBlockWatcherConfigKeys = [
20
+ 'slashAttestDescendantOfInvalidPenalty',
21
+ 'slashProposeInvalidAttestationsPenalty',
22
+ ] as const;
23
+
24
+ type AttestationsBlockWatcherConfig = Pick<SlasherConfig, (typeof AttestationsBlockWatcherConfigKeys)[number]>;
25
+
17
26
  /**
18
27
  * This watcher is responsible for detecting invalid blocks and creating slashing arguments for offenders.
19
28
  * An invalid block is one that doesn't have enough attestations or has incorrect attestations.
@@ -29,13 +38,14 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
29
38
  // All invalid archive roots seen
30
39
  private invalidArchiveRoots: Set<string> = new Set();
31
40
 
41
+ private config: AttestationsBlockWatcherConfig;
42
+
32
43
  private boundHandleInvalidBlock = (event: InvalidBlockDetectedEvent) => {
33
44
  try {
34
45
  this.handleInvalidBlock(event);
35
46
  } catch (err) {
36
47
  this.log.error('Error handling invalid block', err, {
37
- ...event.validationResult.block.block.toBlockInfo(),
38
- ...event.validationResult.block.l1,
48
+ ...event.validationResult,
39
49
  reason: event.validationResult.reason,
40
50
  });
41
51
  }
@@ -44,13 +54,16 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
44
54
  constructor(
45
55
  private l2BlockSource: L2BlockSourceEventEmitter,
46
56
  private epochCache: EpochCache,
47
- private config: Pick<
48
- SlasherConfig,
49
- 'slashAttestDescendantOfInvalidPenalty' | 'slashProposeInvalidAttestationsPenalty'
50
- >,
57
+ config: AttestationsBlockWatcherConfig,
51
58
  ) {
52
59
  super();
53
- this.log.info('InvalidBlockWatcher initialized');
60
+ this.config = pick(config, ...AttestationsBlockWatcherConfigKeys);
61
+ this.log.info('AttestationsBlockWatcher initialized');
62
+ }
63
+
64
+ public updateConfig(newConfig: Partial<AttestationsBlockWatcherConfig>) {
65
+ this.config = merge(this.config, pick(newConfig, ...AttestationsBlockWatcherConfigKeys));
66
+ this.log.verbose('AttestationsBlockWatcher config updated', this.config);
54
67
  }
55
68
 
56
69
  public start() {
@@ -68,16 +81,16 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
68
81
 
69
82
  private handleInvalidBlock(event: InvalidBlockDetectedEvent): void {
70
83
  const { validationResult } = event;
71
- const block = validationResult.block.block;
84
+ const block = validationResult.block;
72
85
 
73
86
  // Check if we already have processed this block, archiver may emit the same event multiple times
74
- if (this.invalidArchiveRoots.has(block.archive.root.toString())) {
75
- this.log.trace(`Already processed invalid block ${block.number}`);
87
+ if (this.invalidArchiveRoots.has(block.archive.toString())) {
88
+ this.log.trace(`Already processed invalid block ${block.blockNumber}`);
76
89
  return;
77
90
  }
78
91
 
79
- this.log.verbose(`Detected invalid block ${block.number}`, {
80
- ...block.toBlockInfo(),
92
+ this.log.verbose(`Detected invalid block ${block.blockNumber}`, {
93
+ ...block,
81
94
  reason: validationResult.valid === false ? validationResult.reason : 'unknown',
82
95
  });
83
96
 
@@ -94,11 +107,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
94
107
  private slashAttestorsOnAncestorInvalid(validationResult: ValidateBlockNegativeResult) {
95
108
  const block = validationResult.block;
96
109
 
97
- const parentArchive = block.block.header.lastArchive.root.toString();
98
- if (this.invalidArchiveRoots.has(block.block.header.lastArchive.root.toString())) {
99
- const attestors = validationResult.attestations.map(a => a.getSender());
100
- this.log.info(`Want to slash attestors of block ${block.block.number} built on invalid block`, {
101
- ...block.block.toBlockInfo(),
110
+ const parentArchive = block.lastArchive.toString();
111
+ if (this.invalidArchiveRoots.has(parentArchive)) {
112
+ const attestors = validationResult.attestors;
113
+ this.log.info(`Want to slash attestors of block ${block.blockNumber} built on invalid block`, {
114
+ ...block,
102
115
  ...attestors,
103
116
  parentArchive,
104
117
  });
@@ -109,7 +122,7 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
109
122
  validator: attestor,
110
123
  amount: this.config.slashAttestDescendantOfInvalidPenalty,
111
124
  offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
112
- epochOrSlot: block.block.slot,
125
+ epochOrSlot: BigInt(SlotNumber(block.slotNumber)),
113
126
  })),
114
127
  );
115
128
  }
@@ -117,8 +130,8 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
117
130
 
118
131
  private slashProposer(validationResult: ValidateBlockNegativeResult) {
119
132
  const { reason, block } = validationResult;
120
- const blockNumber = block.block.number;
121
- const slot = block.block.header.getSlot();
133
+ const blockNumber = block.blockNumber;
134
+ const slot = block.slotNumber;
122
135
  const proposer = this.epochCache.getProposerFromEpochCommittee(validationResult, slot);
123
136
 
124
137
  if (!proposer) {
@@ -132,11 +145,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
132
145
  validator: proposer,
133
146
  amount,
134
147
  offenseType: offense,
135
- epochOrSlot: block.block.slot,
148
+ epochOrSlot: BigInt(slot),
136
149
  };
137
150
 
138
151
  this.log.info(`Want to slash proposer of block ${blockNumber} due to ${reason}`, {
139
- ...block.block.toBlockInfo(),
152
+ ...block,
140
153
  ...args,
141
154
  });
142
155
 
@@ -156,8 +169,8 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
156
169
  }
157
170
  }
158
171
 
159
- private addInvalidBlock(block: PublishedL2Block) {
160
- this.invalidArchiveRoots.add(block.block.archive.root.toString());
172
+ private addInvalidBlock(block: L2BlockInfo) {
173
+ this.invalidArchiveRoots.add(block.archive.toString());
161
174
 
162
175
  // Prune old entries if we exceed the maximum
163
176
  if (this.invalidArchiveRoots.size > this.maxInvalidBlocks) {
@@ -1,5 +1,6 @@
1
- import type { Tx } from '@aztec/aztec.js';
2
1
  import { EpochCache } from '@aztec/epoch-cache';
2
+ import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
+ import { merge, pick } from '@aztec/foundation/collection';
3
4
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
5
  import {
5
6
  EthAddress,
@@ -8,9 +9,15 @@ import {
8
9
  type L2BlockSourceEventEmitter,
9
10
  L2BlockSourceEvents,
10
11
  } from '@aztec/stdlib/block';
11
- import type { IFullNodeBlockBuilder, ITxProvider, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
12
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
13
+ import type {
14
+ IFullNodeBlockBuilder,
15
+ ITxProvider,
16
+ MerkleTreeWriteOperations,
17
+ SlasherConfig,
18
+ } from '@aztec/stdlib/interfaces/server';
12
19
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
13
- import { OffenseType } from '@aztec/stdlib/slashing';
20
+ import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
14
21
  import {
15
22
  ReExFailedTxsError,
16
23
  ReExStateMismatchError,
@@ -22,10 +29,9 @@ import EventEmitter from 'node:events';
22
29
 
23
30
  import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
24
31
 
25
- type EpochPruneWatcherPenalties = {
26
- slashPrunePenalty: bigint;
27
- slashDataWithholdingPenalty: bigint;
28
- };
32
+ const EpochPruneWatcherPenaltiesConfigKeys = ['slashPrunePenalty', 'slashDataWithholdingPenalty'] as const;
33
+
34
+ type EpochPruneWatcherPenalties = Pick<SlasherConfig, (typeof EpochPruneWatcherPenaltiesConfigKeys)[number]>;
29
35
 
30
36
  /**
31
37
  * This watcher is responsible for detecting chain prunes and creating slashing arguments for the committee.
@@ -39,15 +45,18 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
39
45
  // Store bound function reference for proper listener removal
40
46
  private boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
41
47
 
48
+ private penalties: EpochPruneWatcherPenalties;
49
+
42
50
  constructor(
43
51
  private l2BlockSource: L2BlockSourceEventEmitter,
44
52
  private l1ToL2MessageSource: L1ToL2MessageSource,
45
53
  private epochCache: EpochCache,
46
54
  private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
47
55
  private blockBuilder: IFullNodeBlockBuilder,
48
- private penalties: EpochPruneWatcherPenalties,
56
+ penalties: EpochPruneWatcherPenalties,
49
57
  ) {
50
58
  super();
59
+ this.penalties = pick(penalties, ...EpochPruneWatcherPenaltiesConfigKeys);
51
60
  this.log.verbose(
52
61
  `EpochPruneWatcher initialized with penalties: valid epoch pruned=${penalties.slashPrunePenalty} data withholding=${penalties.slashDataWithholdingPenalty}`,
53
62
  );
@@ -63,56 +72,58 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
63
72
  return Promise.resolve();
64
73
  }
65
74
 
75
+ public updateConfig(config: Partial<SlasherConfig>): void {
76
+ this.penalties = merge(this.penalties, pick(config, ...EpochPruneWatcherPenaltiesConfigKeys));
77
+ this.log.verbose('EpochPruneWatcher config updated', this.penalties);
78
+ }
79
+
66
80
  private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
67
81
  const { blocks, epochNumber } = event;
68
- this.log.info(`Detected chain prune. Validating epoch ${epochNumber}`);
69
-
70
- this.validateBlocks(blocks)
71
- .then(async () => {
72
- this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
73
- const validators = await this.getValidatorsForEpoch(epochNumber);
74
- // need to specify return type to be able to return offense as undefined later on
75
- const result: { validators: EthAddress[]; offense: OffenseType | undefined } = {
76
- validators,
77
- offense: OffenseType.VALID_EPOCH_PRUNED,
78
- };
79
- return result;
80
- })
81
- .catch(async error => {
82
- if (error instanceof TransactionsNotAvailableError) {
83
- this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, error);
84
- const validators = await this.getValidatorsForEpoch(epochNumber);
85
- return {
86
- validators,
87
- offense: OffenseType.DATA_WITHHOLDING,
88
- };
89
- } else {
90
- this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
91
- return {
92
- validators: [],
93
- offense: undefined,
94
- };
95
- }
96
- })
97
- .then(({ validators, offense }) => {
98
- if (validators.length === 0 || offense === undefined) {
99
- return;
100
- }
101
- const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
102
- this.log.info(`Slash for epoch ${epochNumber} created`, args);
103
- this.emit(WANT_TO_SLASH_EVENT, args);
104
- })
105
- .catch(error => {
106
- // This can happen if we fail to get the validators for the epoch.
107
- this.log.error('Error while creating slash for epoch', error);
108
- });
82
+ void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
83
+ this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
84
+ );
85
+ }
86
+
87
+ private async emitSlashForEpoch(offense: OffenseType, epochNumber: EpochNumber): Promise<void> {
88
+ const validators = await this.getValidatorsForEpoch(epochNumber);
89
+ if (validators.length === 0) {
90
+ this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
91
+ return;
92
+ }
93
+ const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
94
+ this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
95
+ this.emit(WANT_TO_SLASH_EVENT, args);
96
+ }
97
+
98
+ private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
99
+ try {
100
+ const l1Constants = this.epochCache.getL1Constants();
101
+ const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber);
102
+ this.log.info(
103
+ `Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
104
+ { blocks: epochBlocks.map(b => b.toBlockInfo()) },
105
+ );
106
+
107
+ await this.validateBlocks(epochBlocks);
108
+ this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
109
+ await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
110
+ } catch (error) {
111
+ if (error instanceof TransactionsNotAvailableError) {
112
+ this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
113
+ message: error.message,
114
+ });
115
+ await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
116
+ } else {
117
+ this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
118
+ }
119
+ }
109
120
  }
110
121
 
111
122
  public async validateBlocks(blocks: L2Block[]): Promise<void> {
112
123
  if (blocks.length === 0) {
113
124
  return;
114
125
  }
115
- const fork = await this.blockBuilder.getFork(blocks[0].header.globalVariables.blockNumber - 1);
126
+ const fork = await this.blockBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
116
127
  try {
117
128
  for (const block of blocks) {
118
129
  await this.validateBlock(block, fork);
@@ -134,9 +145,10 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
134
145
  throw new TransactionsNotAvailableError(missingTxs);
135
146
  }
136
147
 
137
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockFromL1.number);
148
+ const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
149
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
138
150
  const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
139
- txs as Tx[],
151
+ txs,
140
152
  l1ToL2Messages,
141
153
  blockFromL1.header.globalVariables,
142
154
  {},
@@ -154,7 +166,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
154
166
  }
155
167
  }
156
168
 
157
- private async getValidatorsForEpoch(epochNumber: bigint): Promise<EthAddress[]> {
169
+ private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
158
170
  const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
159
171
  if (!committee) {
160
172
  this.log.trace(`No committee found for epoch ${epochNumber}`);
@@ -166,7 +178,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
166
178
  private validatorsToSlashingArgs(
167
179
  validators: EthAddress[],
168
180
  offenseType: OffenseType,
169
- epochOrSlot: bigint,
181
+ epochOrSlot: EpochNumber,
170
182
  ): WantToSlashArgs[] {
171
183
  const penalty =
172
184
  offenseType === OffenseType.DATA_WITHHOLDING
@@ -176,7 +188,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
176
188
  validator: v,
177
189
  amount: penalty,
178
190
  offenseType,
179
- epochOrSlot,
191
+ epochOrSlot: BigInt(epochOrSlot),
180
192
  }));
181
193
  }
182
194
  }