@aztec/slasher 1.2.0 → 2.0.0-nightly.20250813

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,16 +1,18 @@
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';
11
11
  import { sleep } from '@aztec/foundation/sleep';
12
12
  import type { DateProvider } from '@aztec/foundation/timer';
13
13
  import { SlashFactoryAbi } from '@aztec/l1-artifacts';
14
+ import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
15
+ import { type Offense, bigIntToOffense } from '@aztec/stdlib/slashing';
14
16
 
15
17
  import {
16
18
  type GetContractEventsReturnType,
@@ -21,14 +23,7 @@ import {
21
23
  getContract,
22
24
  } from 'viem';
23
25
 
24
- import {
25
- Offense,
26
- type SlasherConfig,
27
- WANT_TO_SLASH_EVENT,
28
- type WantToSlashArgs,
29
- type Watcher,
30
- bigIntToOffense,
31
- } from './config.js';
26
+ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher } from './config.js';
32
27
 
33
28
  type MonitoredSlashPayload = {
34
29
  payloadAddress: EthAddress;
@@ -67,11 +62,13 @@ export class SlasherClient {
67
62
  private monitoredPayloads: MonitoredSlashPayload[] = [];
68
63
  private unwatchCallbacks: (() => void)[] = [];
69
64
  private overridePayloadActive = false;
65
+ private slashingExecutionDelayInRounds = 0n;
70
66
 
71
67
  static async new(
72
- config: SlasherConfig,
68
+ config: Omit<SlasherConfig, 'slasherPrivateKey'>,
73
69
  l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress'>,
74
- l1TxUtils: L1TxUtils,
70
+ l1TxUtils: L1TxUtils | undefined,
71
+ l1Client: ViemClient,
75
72
  watchers: Watcher[],
76
73
  dateProvider: DateProvider,
77
74
  ) {
@@ -82,22 +79,35 @@ export class SlasherClient {
82
79
  throw new Error('Cannot initialize SlasherClient without a slashFactory address');
83
80
  }
84
81
 
85
- const rollup = new RollupContract(l1TxUtils.client, l1Contracts.rollupAddress);
82
+ const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
86
83
  const slashingProposer = await rollup.getSlashingProposer();
87
84
  const slashFactoryContract = getContract({
88
85
  address: getAddress(l1Contracts.slashFactoryAddress.toString()),
89
86
  abi: SlashFactoryAbi,
90
- client: l1TxUtils.client,
87
+ client: l1Client,
91
88
  });
92
89
 
93
- 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;
94
104
  }
95
105
 
96
106
  constructor(
97
- public config: SlasherConfig,
98
- protected slashFactoryContract: GetContractReturnType<typeof SlashFactoryAbi, ExtendedViemWalletClient>,
107
+ public config: Omit<SlasherConfig, 'slasherPrivateKey'>,
108
+ protected slashFactoryContract: GetContractReturnType<typeof SlashFactoryAbi, ViemClient>,
99
109
  private slashingProposer: SlashingProposerContract,
100
- private l1TxUtils: L1TxUtils,
110
+ private l1TxUtils: L1TxUtils | undefined,
101
111
  private watchers: Watcher[],
102
112
  private dateProvider: DateProvider,
103
113
  private log = createLogger('slasher'),
@@ -107,17 +117,18 @@ export class SlasherClient {
107
117
 
108
118
  //////////////////// Public methods ////////////////////
109
119
 
110
- public start() {
111
- this.log.info('Starting Slasher client...');
120
+ public async start() {
121
+ this.log.debug('Starting Slasher client...');
122
+ this.slashingExecutionDelayInRounds = await this.slashingProposer.getExecutionDelayInRounds();
112
123
 
113
124
  // detect when new payloads are created
114
125
  this.unwatchCallbacks.push(this.watchSlashFactoryEvents());
115
126
 
116
- // detect when a proposal is executable
117
- 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)));
118
129
 
119
- // detect when a proposal is executed
120
- 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)));
121
132
 
122
133
  // start each watcher, who will signal the slasher client when they want to slash
123
134
  const wantToSlashCb = this.wantToSlash.bind(this);
@@ -125,6 +136,10 @@ export class SlasherClient {
125
136
  watcher.on(WANT_TO_SLASH_EVENT, wantToSlashCb);
126
137
  this.unwatchCallbacks.push(() => watcher.removeListener(WANT_TO_SLASH_EVENT, wantToSlashCb));
127
138
  }
139
+
140
+ this.log.info(
141
+ `Started Slasher client${this.l1TxUtils ? ` with publisher address ${this.l1TxUtils.client.account.address}` : ''}`,
142
+ );
128
143
  }
129
144
 
130
145
  /**
@@ -151,22 +166,31 @@ export class SlasherClient {
151
166
  this.monitoredPayloads = [];
152
167
  }
153
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
+
154
178
  /**
155
179
  * Update the config of the slasher client
156
180
  *
157
- * @param config - the new config. Can only update the following fields:
158
- * - slashOverridePayload
159
- * - slashPayloadTtlSeconds
160
- * - slashProposerRoundPollingIntervalSeconds
181
+ * @param config - the new config. Cannot update the slasher private key.
161
182
  */
162
183
  public updateConfig(config: Partial<SlasherConfig>) {
163
- const newConfig: SlasherConfig = {
184
+ const { slasherPrivateKey: _doNotUpdate, ...configWithoutPrivateKey } = config;
185
+
186
+ const newConfig: Omit<SlasherConfig, 'slasherPrivateKey'> = {
164
187
  ...this.config,
165
- slashOverridePayload: config.slashOverridePayload ?? this.config.slashOverridePayload,
166
- slashPayloadTtlSeconds: config.slashPayloadTtlSeconds ?? this.config.slashPayloadTtlSeconds,
167
- slashProposerRoundPollingIntervalSeconds:
168
- config.slashProposerRoundPollingIntervalSeconds ?? this.config.slashProposerRoundPollingIntervalSeconds,
188
+ ...configWithoutPrivateKey,
169
189
  };
190
+
191
+ // We keep this separate flag to tell us if we should be signal for the override payload: after the override payload is executed,
192
+ // the slasher goes back to using the monitored payloads to inform the sequencer publisher what payload to signal for.
193
+ // So we only want to flip back "on" the voting for override payload if config we just passed in re-set the override payload.
170
194
  this.overridePayloadActive = config.slashOverridePayload !== undefined && !config.slashOverridePayload.isZero();
171
195
  this.config = newConfig;
172
196
  }
@@ -219,15 +243,15 @@ export class SlasherClient {
219
243
  *
220
244
  * @param {round: bigint; proposal: `0x${string}`} param0
221
245
  */
222
- protected proposalExecuted({ round, proposal }: { round: bigint; proposal: `0x${string}` }) {
223
- this.log.info('Proposal executed', { round, proposal });
224
- 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);
225
249
  // Stop signaling for the override payload if it was executed
226
- if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payload)) {
250
+ if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payloadAddress)) {
227
251
  this.overridePayloadActive = false;
228
252
  }
229
253
 
230
- const index = this.monitoredPayloads.findIndex(p => p.payloadAddress.equals(payload));
254
+ const index = this.monitoredPayloads.findIndex(p => p.payloadAddress.equals(payloadAddress));
231
255
  if (index === -1) {
232
256
  return;
233
257
  }
@@ -242,6 +266,17 @@ export class SlasherClient {
242
266
  * @param args - the arguments from the watcher, including the validators, amounts, and offenses
243
267
  */
244
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
+
245
280
  const sortedArgs = [...args].sort((a, b) => a.validator.toString().localeCompare(b.validator.toString()));
246
281
  this.log.info('Wants to slash', sortedArgs);
247
282
  this.l1TxUtils
@@ -434,30 +469,40 @@ export class SlasherClient {
434
469
  }
435
470
 
436
471
  /**
437
- * Execute a round if we agree with the proposal.
472
+ * Submit a round to the Slasher if we agree with the payload.
438
473
  *
439
- * 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.
440
475
  *
441
476
  * @param {proposal: `0x${string}`; round: bigint} param0
442
477
  */
443
- private async executeRoundIfAgree({ proposal, round }: { proposal: `0x${string}`; round: bigint }) {
444
- const payload = EthAddress.fromString(proposal);
445
- if (!this.monitoredPayloads.find(p => p.payloadAddress.equals(payload))) {
446
- 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 });
447
492
  return;
448
493
  }
449
494
 
450
- const nextRound = round + 1n;
451
- 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`);
452
497
  const reached = await this.slashingProposer.waitForRound(
453
- nextRound,
498
+ executableRound,
454
499
  this.config.slashProposerRoundPollingIntervalSeconds,
455
500
  );
456
501
  if (!reached) {
457
- this.log.warn('Round not reached', { proposal, round });
502
+ this.log.warn('Round not reached', { payload, round });
458
503
  return;
459
504
  }
460
- this.log.info('Executing round', { proposal, round });
505
+ this.log.info('Executing round', { payload, round });
461
506
 
462
507
  await this.slashingProposer
463
508
  .executeRound(this.l1TxUtils, round)