@aztec/slasher 1.2.1 → 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 +2 -9
- 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 +16 -13
- package/dest/slasher_client.d.ts.map +1 -1
- package/dest/slasher_client.js +63 -30
- package/package.json +8 -8
- package/src/attestations_block_watcher.ts +219 -0
- package/src/config.ts +34 -32
- package/src/epoch_prune_watcher.ts +19 -12
- package/src/index.ts +2 -0
- package/src/slasher_client.ts +88 -35
package/src/slasher_client.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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:
|
|
87
|
+
client: l1Client,
|
|
85
88
|
});
|
|
86
89
|
|
|
87
|
-
|
|
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,
|
|
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.
|
|
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
|
|
111
|
-
this.unwatchCallbacks.push(this.slashingProposer.
|
|
127
|
+
// detect when a payload is submittable
|
|
128
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToSubmittablePayloads(this.submitRoundIfAgree.bind(this)));
|
|
112
129
|
|
|
113
|
-
// detect when a
|
|
114
|
-
this.unwatchCallbacks.push(this.slashingProposer.
|
|
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
|
|
184
|
+
const { slasherPrivateKey: _doNotUpdate, ...configWithoutPrivateKey } = config;
|
|
185
|
+
|
|
186
|
+
const newConfig: Omit<SlasherConfig, 'slasherPrivateKey'> = {
|
|
155
187
|
...this.config,
|
|
156
|
-
...
|
|
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
|
|
215
|
-
this.log.info('
|
|
216
|
-
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);
|
|
217
249
|
// Stop signaling for the override payload if it was executed
|
|
218
|
-
if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(
|
|
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(
|
|
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
|
-
*
|
|
472
|
+
* Submit a round to the Slasher if we agree with the payload.
|
|
430
473
|
*
|
|
431
|
-
* Bound to the slashing proposer contract's
|
|
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
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
443
|
-
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`);
|
|
444
497
|
const reached = await this.slashingProposer.waitForRound(
|
|
445
|
-
|
|
498
|
+
executableRound,
|
|
446
499
|
this.config.slashProposerRoundPollingIntervalSeconds,
|
|
447
500
|
);
|
|
448
501
|
if (!reached) {
|
|
449
|
-
this.log.warn('Round not reached', {
|
|
502
|
+
this.log.warn('Round not reached', { payload, round });
|
|
450
503
|
return;
|
|
451
504
|
}
|
|
452
|
-
this.log.info('Executing round', {
|
|
505
|
+
this.log.info('Executing round', { payload, round });
|
|
453
506
|
|
|
454
507
|
await this.slashingProposer
|
|
455
508
|
.executeRound(this.l1TxUtils, round)
|