@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.
- package/dest/attestations_block_watcher.d.ts +34 -0
- package/dest/attestations_block_watcher.d.ts.map +1 -0
- package/dest/attestations_block_watcher.js +170 -0
- package/dest/config.d.ts +3 -25
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +32 -33
- package/dest/epoch_prune_watcher.d.ts +4 -3
- package/dest/epoch_prune_watcher.d.ts.map +1 -1
- package/dest/epoch_prune_watcher.js +15 -9
- package/dest/index.d.ts +2 -0
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -0
- package/dest/slasher_client.d.ts +17 -16
- package/dest/slasher_client.d.ts.map +1 -1
- package/dest/slasher_client.js +66 -35
- package/package.json +8 -8
- package/src/attestations_block_watcher.ts +219 -0
- package/src/config.ts +35 -51
- package/src/epoch_prune_watcher.ts +19 -12
- package/src/index.ts +2 -0
- package/src/slasher_client.ts +93 -48
package/src/slasher_client.ts
CHANGED
|
@@ -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(
|
|
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:
|
|
87
|
+
client: l1Client,
|
|
91
88
|
});
|
|
92
89
|
|
|
93
|
-
|
|
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,
|
|
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.
|
|
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
|
|
117
|
-
this.unwatchCallbacks.push(this.slashingProposer.
|
|
127
|
+
// detect when a payload is submittable
|
|
128
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToSubmittablePayloads(this.submitRoundIfAgree.bind(this)));
|
|
118
129
|
|
|
119
|
-
// detect when a
|
|
120
|
-
this.unwatchCallbacks.push(this.slashingProposer.
|
|
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.
|
|
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
|
|
184
|
+
const { slasherPrivateKey: _doNotUpdate, ...configWithoutPrivateKey } = config;
|
|
185
|
+
|
|
186
|
+
const newConfig: Omit<SlasherConfig, 'slasherPrivateKey'> = {
|
|
164
187
|
...this.config,
|
|
165
|
-
|
|
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
|
|
223
|
-
this.log.info('
|
|
224
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
472
|
+
* Submit a round to the Slasher if we agree with the payload.
|
|
438
473
|
*
|
|
439
|
-
* Bound to the slashing proposer contract's
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
451
|
-
this.log.info(`Waiting for round ${
|
|
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
|
-
|
|
498
|
+
executableRound,
|
|
454
499
|
this.config.slashProposerRoundPollingIntervalSeconds,
|
|
455
500
|
);
|
|
456
501
|
if (!reached) {
|
|
457
|
-
this.log.warn('Round not reached', {
|
|
502
|
+
this.log.warn('Round not reached', { payload, round });
|
|
458
503
|
return;
|
|
459
504
|
}
|
|
460
|
-
this.log.info('Executing round', {
|
|
505
|
+
this.log.info('Executing round', { payload, round });
|
|
461
506
|
|
|
462
507
|
await this.slashingProposer
|
|
463
508
|
.executeRound(this.l1TxUtils, round)
|