@aztec/slasher 1.2.1 → 2.0.0-nightly.20250814

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.
@@ -1,10 +1,10 @@
1
1
  import {
2
- type ExtendedViemWalletClient,
3
2
  type L1ReaderConfig,
4
3
  L1TxUtils,
5
4
  ProposalAlreadyExecutedError,
6
5
  RollupContract,
7
6
  SlashingProposerContract,
7
+ type ViemClient,
8
8
  } from '@aztec/ethereum';
9
9
  import { EthAddress } from '@aztec/foundation/eth-address';
10
10
  import { createLogger } from '@aztec/foundation/log';
@@ -12,6 +12,7 @@ import { sleep } from '@aztec/foundation/sleep';
12
12
  import type { DateProvider } from '@aztec/foundation/timer';
13
13
  import { SlashFactoryAbi } from '@aztec/l1-artifacts';
14
14
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
15
+ import { type Offense, bigIntToOffense } from '@aztec/stdlib/slashing';
15
16
 
16
17
  import {
17
18
  type GetContractEventsReturnType,
@@ -22,7 +23,7 @@ import {
22
23
  getContract,
23
24
  } from 'viem';
24
25
 
25
- import { Offense, WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, bigIntToOffense } from './config.js';
26
+ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher } from './config.js';
26
27
 
27
28
  type MonitoredSlashPayload = {
28
29
  payloadAddress: EthAddress;
@@ -61,11 +62,13 @@ export class SlasherClient {
61
62
  private monitoredPayloads: MonitoredSlashPayload[] = [];
62
63
  private unwatchCallbacks: (() => void)[] = [];
63
64
  private overridePayloadActive = false;
65
+ private slashingExecutionDelayInRounds = 0n;
64
66
 
65
67
  static async new(
66
- config: SlasherConfig,
68
+ config: Omit<SlasherConfig, 'slasherPrivateKey'>,
67
69
  l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress'>,
68
- l1TxUtils: L1TxUtils,
70
+ l1TxUtils: L1TxUtils | undefined,
71
+ l1Client: ViemClient,
69
72
  watchers: Watcher[],
70
73
  dateProvider: DateProvider,
71
74
  ) {
@@ -76,22 +79,35 @@ export class SlasherClient {
76
79
  throw new Error('Cannot initialize SlasherClient without a slashFactory address');
77
80
  }
78
81
 
79
- const rollup = new RollupContract(l1TxUtils.client, l1Contracts.rollupAddress);
82
+ const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
80
83
  const slashingProposer = await rollup.getSlashingProposer();
81
84
  const slashFactoryContract = getContract({
82
85
  address: getAddress(l1Contracts.slashFactoryAddress.toString()),
83
86
  abi: SlashFactoryAbi,
84
- client: l1TxUtils.client,
87
+ client: l1Client,
85
88
  });
86
89
 
87
- return new SlasherClient(config, slashFactoryContract, slashingProposer, l1TxUtils, watchers, dateProvider);
90
+ const slasherClient = new SlasherClient(
91
+ config,
92
+ slashFactoryContract,
93
+ slashingProposer,
94
+ l1TxUtils,
95
+ watchers,
96
+ dateProvider,
97
+ );
98
+ rollup.listenToSlasherChanged(async () => {
99
+ const newSlashingProposer = await rollup.getSlashingProposer();
100
+ await slasherClient.setSlashingProposer(newSlashingProposer);
101
+ });
102
+
103
+ return slasherClient;
88
104
  }
89
105
 
90
106
  constructor(
91
- public config: SlasherConfig,
92
- protected slashFactoryContract: GetContractReturnType<typeof SlashFactoryAbi, ExtendedViemWalletClient>,
107
+ public config: Omit<SlasherConfig, 'slasherPrivateKey'>,
108
+ protected slashFactoryContract: GetContractReturnType<typeof SlashFactoryAbi, ViemClient>,
93
109
  private slashingProposer: SlashingProposerContract,
94
- private l1TxUtils: L1TxUtils,
110
+ private l1TxUtils: L1TxUtils | undefined,
95
111
  private watchers: Watcher[],
96
112
  private dateProvider: DateProvider,
97
113
  private log = createLogger('slasher'),
@@ -101,17 +117,18 @@ export class SlasherClient {
101
117
 
102
118
  //////////////////// Public methods ////////////////////
103
119
 
104
- public start() {
105
- this.log.info('Starting Slasher client...');
120
+ public async start() {
121
+ this.log.debug('Starting Slasher client...');
122
+ this.slashingExecutionDelayInRounds = await this.slashingProposer.getExecutionDelayInRounds();
106
123
 
107
124
  // detect when new payloads are created
108
125
  this.unwatchCallbacks.push(this.watchSlashFactoryEvents());
109
126
 
110
- // detect when a proposal is executable
111
- this.unwatchCallbacks.push(this.slashingProposer.listenToExecutableProposals(this.executeRoundIfAgree.bind(this)));
127
+ // detect when a payload is submittable
128
+ this.unwatchCallbacks.push(this.slashingProposer.listenToSubmittablePayloads(this.submitRoundIfAgree.bind(this)));
112
129
 
113
- // detect when a proposal is executed
114
- this.unwatchCallbacks.push(this.slashingProposer.listenToProposalExecuted(this.proposalExecuted.bind(this)));
130
+ // detect when a payload is submitted
131
+ this.unwatchCallbacks.push(this.slashingProposer.listenToPayloadSubmitted(this.payloadSubmitted.bind(this)));
115
132
 
116
133
  // start each watcher, who will signal the slasher client when they want to slash
117
134
  const wantToSlashCb = this.wantToSlash.bind(this);
@@ -119,6 +136,10 @@ export class SlasherClient {
119
136
  watcher.on(WANT_TO_SLASH_EVENT, wantToSlashCb);
120
137
  this.unwatchCallbacks.push(() => watcher.removeListener(WANT_TO_SLASH_EVENT, wantToSlashCb));
121
138
  }
139
+
140
+ this.log.info(
141
+ `Started Slasher client${this.l1TxUtils ? ` with publisher address ${this.l1TxUtils.client.account.address}` : ''}`,
142
+ );
122
143
  }
123
144
 
124
145
  /**
@@ -145,15 +166,26 @@ export class SlasherClient {
145
166
  this.monitoredPayloads = [];
146
167
  }
147
168
 
169
+ public async setSlashingProposer(slashingProposer: SlashingProposerContract) {
170
+ this.log.info('Slashing proposer changed');
171
+ // remove the old listeners
172
+ await this.stop();
173
+ this.slashingProposer = slashingProposer;
174
+ // start the new listeners
175
+ await this.start();
176
+ }
177
+
148
178
  /**
149
179
  * Update the config of the slasher client
150
180
  *
151
- * @param config - the new config.
181
+ * @param config - the new config. Cannot update the slasher private key.
152
182
  */
153
183
  public updateConfig(config: Partial<SlasherConfig>) {
154
- const newConfig: SlasherConfig = {
184
+ const { slasherPrivateKey: _doNotUpdate, ...configWithoutPrivateKey } = config;
185
+
186
+ const newConfig: Omit<SlasherConfig, 'slasherPrivateKey'> = {
155
187
  ...this.config,
156
- ...config,
188
+ ...configWithoutPrivateKey,
157
189
  };
158
190
 
159
191
  // We keep this separate flag to tell us if we should be signal for the override payload: after the override payload is executed,
@@ -211,15 +243,15 @@ export class SlasherClient {
211
243
  *
212
244
  * @param {round: bigint; proposal: `0x${string}`} param0
213
245
  */
214
- protected proposalExecuted({ round, proposal }: { round: bigint; proposal: `0x${string}` }) {
215
- this.log.info('Proposal executed', { round, proposal });
216
- const payload = EthAddress.fromString(proposal);
246
+ protected payloadSubmitted({ round, payload }: { round: bigint; payload: `0x${string}` }) {
247
+ this.log.info('Payload submitted', { round, payload });
248
+ const payloadAddress = EthAddress.fromString(payload);
217
249
  // Stop signaling for the override payload if it was executed
218
- if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payload)) {
250
+ if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payloadAddress)) {
219
251
  this.overridePayloadActive = false;
220
252
  }
221
253
 
222
- const index = this.monitoredPayloads.findIndex(p => p.payloadAddress.equals(payload));
254
+ const index = this.monitoredPayloads.findIndex(p => p.payloadAddress.equals(payloadAddress));
223
255
  if (index === -1) {
224
256
  return;
225
257
  }
@@ -234,6 +266,17 @@ export class SlasherClient {
234
266
  * @param args - the arguments from the watcher, including the validators, amounts, and offenses
235
267
  */
236
268
  private wantToSlash(args: WantToSlashArgs[]) {
269
+ if (!this.l1TxUtils) {
270
+ this.log.warn(
271
+ 'Cannot slash validators: no slasher private key configured. Set SLASHER_PRIVATE_KEY environment variable.',
272
+ {
273
+ validators: args.map(arg => arg.validator.toString()),
274
+ offenses: args.map(arg => arg.offense),
275
+ },
276
+ );
277
+ return;
278
+ }
279
+
237
280
  const sortedArgs = [...args].sort((a, b) => a.validator.toString().localeCompare(b.validator.toString()));
238
281
  this.log.info('Wants to slash', sortedArgs);
239
282
  this.l1TxUtils
@@ -426,30 +469,40 @@ export class SlasherClient {
426
469
  }
427
470
 
428
471
  /**
429
- * Execute a round if we agree with the proposal.
472
+ * Submit a round to the Slasher if we agree with the payload.
430
473
  *
431
- * Bound to the slashing proposer contract's listenToExecutableProposals method in the constructor.
474
+ * Bound to the slashing proposer contract's listenToSubmittablePayloads method in the constructor.
432
475
  *
433
476
  * @param {proposal: `0x${string}`; round: bigint} param0
434
477
  */
435
- private async executeRoundIfAgree({ proposal, round }: { proposal: `0x${string}`; round: bigint }) {
436
- const payload = EthAddress.fromString(proposal);
437
- if (!this.monitoredPayloads.find(p => p.payloadAddress.equals(payload))) {
438
- this.log.debug('Round executable, but we disagree', { proposal, round });
478
+ private async submitRoundIfAgree({ payload, round }: { payload: `0x${string}`; round: bigint }) {
479
+ if (!this.l1TxUtils) {
480
+ this.log.warn(
481
+ 'Cannot execute slashing proposal: no slasher private key configured. Set SLASHER_PRIVATE_KEY environment variable.',
482
+ {
483
+ payload,
484
+ round,
485
+ },
486
+ );
487
+ return;
488
+ }
489
+ const payloadAddress = EthAddress.fromString(payload);
490
+ if (!this.monitoredPayloads.find(p => p.payloadAddress.equals(payloadAddress))) {
491
+ this.log.debug('Round executable, but we disagree', { payload, round });
439
492
  return;
440
493
  }
441
494
 
442
- const nextRound = round + 1n;
443
- this.log.info(`Waiting for round ${nextRound} to be reached`);
495
+ const executableRound = round + BigInt(this.slashingExecutionDelayInRounds) + 1n;
496
+ this.log.info(`Waiting for round ${executableRound} to be reached`);
444
497
  const reached = await this.slashingProposer.waitForRound(
445
- nextRound,
498
+ executableRound,
446
499
  this.config.slashProposerRoundPollingIntervalSeconds,
447
500
  );
448
501
  if (!reached) {
449
- this.log.warn('Round not reached', { proposal, round });
502
+ this.log.warn('Round not reached', { payload, round });
450
503
  return;
451
504
  }
452
- this.log.info('Executing round', { proposal, round });
505
+ this.log.info('Executing round', { payload, round });
453
506
 
454
507
  await this.slashingProposer
455
508
  .executeRound(this.l1TxUtils, round)