@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.
@@ -3,8 +3,9 @@ import { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { createLogger } from '@aztec/foundation/log';
4
4
  import { sleep } from '@aztec/foundation/sleep';
5
5
  import { SlashFactoryAbi } from '@aztec/l1-artifacts';
6
+ import { bigIntToOffense } from '@aztec/stdlib/slashing';
6
7
  import { encodeFunctionData, getAddress, getContract } from 'viem';
7
- import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
8
+ import { WANT_TO_SLASH_EVENT } from './config.js';
8
9
  /**
9
10
  * A Spartiate slasher client implementation
10
11
  *
@@ -39,21 +40,27 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
39
40
  monitoredPayloads;
40
41
  unwatchCallbacks;
41
42
  overridePayloadActive;
42
- static async new(config, l1Contracts, l1TxUtils, watchers, dateProvider) {
43
+ slashingExecutionDelayInRounds;
44
+ static async new(config, l1Contracts, l1TxUtils, l1Client, watchers, dateProvider) {
43
45
  if (!l1Contracts.rollupAddress) {
44
46
  throw new Error('Cannot initialize SlasherClient without a rollup address');
45
47
  }
46
48
  if (!l1Contracts.slashFactoryAddress) {
47
49
  throw new Error('Cannot initialize SlasherClient without a slashFactory address');
48
50
  }
49
- const rollup = new RollupContract(l1TxUtils.client, l1Contracts.rollupAddress);
51
+ const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
50
52
  const slashingProposer = await rollup.getSlashingProposer();
51
53
  const slashFactoryContract = getContract({
52
54
  address: getAddress(l1Contracts.slashFactoryAddress.toString()),
53
55
  abi: SlashFactoryAbi,
54
- client: l1TxUtils.client
56
+ client: l1Client
55
57
  });
56
- return new SlasherClient(config, slashFactoryContract, slashingProposer, l1TxUtils, watchers, dateProvider);
58
+ const slasherClient = new SlasherClient(config, slashFactoryContract, slashingProposer, l1TxUtils, watchers, dateProvider);
59
+ rollup.listenToSlasherChanged(async ()=>{
60
+ const newSlashingProposer = await rollup.getSlashingProposer();
61
+ await slasherClient.setSlashingProposer(newSlashingProposer);
62
+ });
63
+ return slasherClient;
57
64
  }
58
65
  constructor(config, slashFactoryContract, slashingProposer, l1TxUtils, watchers, dateProvider, log = createLogger('slasher')){
59
66
  this.config = config;
@@ -66,23 +73,26 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
66
73
  this.monitoredPayloads = [];
67
74
  this.unwatchCallbacks = [];
68
75
  this.overridePayloadActive = false;
76
+ this.slashingExecutionDelayInRounds = 0n;
69
77
  this.overridePayloadActive = config.slashOverridePayload !== undefined && !config.slashOverridePayload.isZero();
70
78
  }
71
79
  //////////////////// Public methods ////////////////////
72
- start() {
73
- this.log.info('Starting Slasher client...');
80
+ async start() {
81
+ this.log.debug('Starting Slasher client...');
82
+ this.slashingExecutionDelayInRounds = await this.slashingProposer.getExecutionDelayInRounds();
74
83
  // detect when new payloads are created
75
84
  this.unwatchCallbacks.push(this.watchSlashFactoryEvents());
76
- // detect when a proposal is executable
77
- this.unwatchCallbacks.push(this.slashingProposer.listenToExecutableProposals(this.executeRoundIfAgree.bind(this)));
78
- // detect when a proposal is executed
79
- this.unwatchCallbacks.push(this.slashingProposer.listenToProposalExecuted(this.proposalExecuted.bind(this)));
85
+ // detect when a payload is submittable
86
+ this.unwatchCallbacks.push(this.slashingProposer.listenToSubmittablePayloads(this.submitRoundIfAgree.bind(this)));
87
+ // detect when a payload is submitted
88
+ this.unwatchCallbacks.push(this.slashingProposer.listenToPayloadSubmitted(this.payloadSubmitted.bind(this)));
80
89
  // start each watcher, who will signal the slasher client when they want to slash
81
90
  const wantToSlashCb = this.wantToSlash.bind(this);
82
91
  for (const watcher of this.watchers){
83
92
  watcher.on(WANT_TO_SLASH_EVENT, wantToSlashCb);
84
93
  this.unwatchCallbacks.push(()=>watcher.removeListener(WANT_TO_SLASH_EVENT, wantToSlashCb));
85
94
  }
95
+ this.log.info(`Started Slasher client${this.l1TxUtils ? ` with publisher address ${this.l1TxUtils.client.account.address}` : ''}`);
86
96
  }
87
97
  /**
88
98
  * Allows consumers to stop the instance of the slasher client.
@@ -105,20 +115,27 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
105
115
  this.log.warn('Clearing monitored payloads', this.monitoredPayloads);
106
116
  this.monitoredPayloads = [];
107
117
  }
118
+ async setSlashingProposer(slashingProposer) {
119
+ this.log.info('Slashing proposer changed');
120
+ // remove the old listeners
121
+ await this.stop();
122
+ this.slashingProposer = slashingProposer;
123
+ // start the new listeners
124
+ await this.start();
125
+ }
108
126
  /**
109
127
  * Update the config of the slasher client
110
128
  *
111
- * @param config - the new config. Can only update the following fields:
112
- * - slashOverridePayload
113
- * - slashPayloadTtlSeconds
114
- * - slashProposerRoundPollingIntervalSeconds
129
+ * @param config - the new config. Cannot update the slasher private key.
115
130
  */ updateConfig(config) {
131
+ const { slasherPrivateKey: _doNotUpdate, ...configWithoutPrivateKey } = config;
116
132
  const newConfig = {
117
133
  ...this.config,
118
- slashOverridePayload: config.slashOverridePayload ?? this.config.slashOverridePayload,
119
- slashPayloadTtlSeconds: config.slashPayloadTtlSeconds ?? this.config.slashPayloadTtlSeconds,
120
- slashProposerRoundPollingIntervalSeconds: config.slashProposerRoundPollingIntervalSeconds ?? this.config.slashProposerRoundPollingIntervalSeconds
134
+ ...configWithoutPrivateKey
121
135
  };
136
+ // We keep this separate flag to tell us if we should be signal for the override payload: after the override payload is executed,
137
+ // the slasher goes back to using the monitored payloads to inform the sequencer publisher what payload to signal for.
138
+ // So we only want to flip back "on" the voting for override payload if config we just passed in re-set the override payload.
122
139
  this.overridePayloadActive = config.slashOverridePayload !== undefined && !config.slashOverridePayload.isZero();
123
140
  this.config = newConfig;
124
141
  }
@@ -160,17 +177,17 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
160
177
  * Bound to the slashing proposer contract's listenToProposalExecuted method in `.start()`.
161
178
  *
162
179
  * @param {round: bigint; proposal: `0x${string}`} param0
163
- */ proposalExecuted({ round, proposal }) {
164
- this.log.info('Proposal executed', {
180
+ */ payloadSubmitted({ round, payload }) {
181
+ this.log.info('Payload submitted', {
165
182
  round,
166
- proposal
183
+ payload
167
184
  });
168
- const payload = EthAddress.fromString(proposal);
185
+ const payloadAddress = EthAddress.fromString(payload);
169
186
  // Stop signaling for the override payload if it was executed
170
- if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payload)) {
187
+ if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payloadAddress)) {
171
188
  this.overridePayloadActive = false;
172
189
  }
173
- const index = this.monitoredPayloads.findIndex((p)=>p.payloadAddress.equals(payload));
190
+ const index = this.monitoredPayloads.findIndex((p)=>p.payloadAddress.equals(payloadAddress));
174
191
  if (index === -1) {
175
192
  return;
176
193
  }
@@ -182,6 +199,13 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
182
199
  *
183
200
  * @param args - the arguments from the watcher, including the validators, amounts, and offenses
184
201
  */ wantToSlash(args) {
202
+ if (!this.l1TxUtils) {
203
+ this.log.warn('Cannot slash validators: no slasher private key configured. Set SLASHER_PRIVATE_KEY environment variable.', {
204
+ validators: args.map((arg)=>arg.validator.toString()),
205
+ offenses: args.map((arg)=>arg.offense)
206
+ });
207
+ return;
208
+ }
185
209
  const sortedArgs = [
186
210
  ...args
187
211
  ].sort((a, b)=>a.validator.toString().localeCompare(b.validator.toString()));
@@ -353,32 +377,39 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
353
377
  });
354
378
  }
355
379
  /**
356
- * Execute a round if we agree with the proposal.
380
+ * Submit a round to the Slasher if we agree with the payload.
357
381
  *
358
- * Bound to the slashing proposer contract's listenToExecutableProposals method in the constructor.
382
+ * Bound to the slashing proposer contract's listenToSubmittablePayloads method in the constructor.
359
383
  *
360
384
  * @param {proposal: `0x${string}`; round: bigint} param0
361
- */ async executeRoundIfAgree({ proposal, round }) {
362
- const payload = EthAddress.fromString(proposal);
363
- if (!this.monitoredPayloads.find((p)=>p.payloadAddress.equals(payload))) {
385
+ */ async submitRoundIfAgree({ payload, round }) {
386
+ if (!this.l1TxUtils) {
387
+ this.log.warn('Cannot execute slashing proposal: no slasher private key configured. Set SLASHER_PRIVATE_KEY environment variable.', {
388
+ payload,
389
+ round
390
+ });
391
+ return;
392
+ }
393
+ const payloadAddress = EthAddress.fromString(payload);
394
+ if (!this.monitoredPayloads.find((p)=>p.payloadAddress.equals(payloadAddress))) {
364
395
  this.log.debug('Round executable, but we disagree', {
365
- proposal,
396
+ payload,
366
397
  round
367
398
  });
368
399
  return;
369
400
  }
370
- const nextRound = round + 1n;
371
- this.log.info(`Waiting for round ${nextRound} to be reached`);
372
- const reached = await this.slashingProposer.waitForRound(nextRound, this.config.slashProposerRoundPollingIntervalSeconds);
401
+ const executableRound = round + BigInt(this.slashingExecutionDelayInRounds) + 1n;
402
+ this.log.info(`Waiting for round ${executableRound} to be reached`);
403
+ const reached = await this.slashingProposer.waitForRound(executableRound, this.config.slashProposerRoundPollingIntervalSeconds);
373
404
  if (!reached) {
374
405
  this.log.warn('Round not reached', {
375
- proposal,
406
+ payload,
376
407
  round
377
408
  });
378
409
  return;
379
410
  }
380
411
  this.log.info('Executing round', {
381
- proposal,
412
+ payload,
382
413
  round
383
414
  });
384
415
  await this.slashingProposer.executeRound(this.l1TxUtils, round).then(({ receipt })=>{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "1.2.0",
3
+ "version": "2.0.0-nightly.20250813",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -54,19 +54,19 @@
54
54
  ]
55
55
  },
56
56
  "dependencies": {
57
- "@aztec/epoch-cache": "1.2.0",
58
- "@aztec/ethereum": "1.2.0",
59
- "@aztec/foundation": "1.2.0",
60
- "@aztec/l1-artifacts": "1.2.0",
61
- "@aztec/stdlib": "1.2.0",
62
- "@aztec/telemetry-client": "1.2.0",
57
+ "@aztec/epoch-cache": "2.0.0-nightly.20250813",
58
+ "@aztec/ethereum": "2.0.0-nightly.20250813",
59
+ "@aztec/foundation": "2.0.0-nightly.20250813",
60
+ "@aztec/l1-artifacts": "2.0.0-nightly.20250813",
61
+ "@aztec/stdlib": "2.0.0-nightly.20250813",
62
+ "@aztec/telemetry-client": "2.0.0-nightly.20250813",
63
63
  "source-map-support": "^0.5.21",
64
64
  "tslib": "^2.4.0",
65
65
  "viem": "2.23.7",
66
66
  "zod": "^3.23.8"
67
67
  },
68
68
  "devDependencies": {
69
- "@aztec/aztec.js": "1.2.0",
69
+ "@aztec/aztec.js": "2.0.0-nightly.20250813",
70
70
  "@jest/globals": "^30.0.0",
71
71
  "@types/jest": "^30.0.0",
72
72
  "@types/node": "^22.15.17",
@@ -0,0 +1,219 @@
1
+ import { EpochCache } from '@aztec/epoch-cache';
2
+ import { type Logger, createLogger } from '@aztec/foundation/log';
3
+ import {
4
+ EthAddress,
5
+ type InvalidBlockDetectedEvent,
6
+ type L2BlockSourceEventEmitter,
7
+ L2BlockSourceEvents,
8
+ PublishedL2Block,
9
+ type ValidateBlockNegativeResult,
10
+ } from '@aztec/stdlib/block';
11
+ import { Offense } from '@aztec/stdlib/slashing';
12
+
13
+ import EventEmitter from 'node:events';
14
+
15
+ import {
16
+ type SlasherConfig,
17
+ WANT_TO_SLASH_EVENT,
18
+ type WantToSlashArgs,
19
+ type Watcher,
20
+ type WatcherEmitter,
21
+ } from './config.js';
22
+
23
+ /**
24
+ * This watcher is responsible for detecting invalid blocks and creating slashing arguments for offenders.
25
+ * An invalid block is one that doesn't have enough attestations or has incorrect attestations.
26
+ * The proposer of an invalid block should be slashed.
27
+ * If there's another block consecutive to the invalid one, its proposer and attestors should also be slashed.
28
+ */
29
+ export class AttestationsBlockWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
30
+ private log: Logger = createLogger('attestations-block-watcher');
31
+
32
+ // Only keep track of the last N invalid blocks
33
+ private maxInvalidBlocks = 100;
34
+
35
+ // All invalid archive roots seen
36
+ private invalidArchiveRoots: Set<string> = new Set();
37
+
38
+ // TODO(#16140): Bad validators are never cleared even after slashing
39
+ private badAttestors: Set<string> = new Set();
40
+ private badProposers: Set<string> = new Set();
41
+
42
+ private boundHandleInvalidBlock = (event: InvalidBlockDetectedEvent) => {
43
+ try {
44
+ this.handleInvalidBlock(event);
45
+ } catch (err) {
46
+ this.log.error('Error handling invalid block', err, {
47
+ ...event.validationResult.block.block.toBlockInfo(),
48
+ ...event.validationResult.block.l1,
49
+ reason: event.validationResult.reason,
50
+ });
51
+ }
52
+ };
53
+
54
+ constructor(
55
+ private l2BlockSource: L2BlockSourceEventEmitter,
56
+ private epochCache: EpochCache,
57
+ private config: Pick<
58
+ SlasherConfig,
59
+ | 'slashAttestDescendantOfInvalidPenalty'
60
+ | 'slashAttestDescendantOfInvalidMaxPenalty'
61
+ | 'slashProposeInvalidAttestationsPenalty'
62
+ | 'slashProposeInvalidAttestationsMaxPenalty'
63
+ >,
64
+ ) {
65
+ super();
66
+ this.log.info('InvalidBlockWatcher initialized');
67
+ }
68
+
69
+ public start() {
70
+ this.l2BlockSource.on(L2BlockSourceEvents.InvalidAttestationsBlockDetected, this.boundHandleInvalidBlock);
71
+ return Promise.resolve();
72
+ }
73
+
74
+ public stop() {
75
+ this.l2BlockSource.removeListener(
76
+ L2BlockSourceEvents.InvalidAttestationsBlockDetected,
77
+ this.boundHandleInvalidBlock,
78
+ );
79
+ return Promise.resolve();
80
+ }
81
+
82
+ public shouldSlash({ amount, offense, validator }: WantToSlashArgs): Promise<boolean> {
83
+ const maxPenalty = this.getMaxPenalty(offense);
84
+ const logData = { validator, amount, offense, maxPenalty };
85
+ if (amount > maxPenalty) {
86
+ this.log.warn(`Slash amount ${amount} exceeds maximum penalty ${maxPenalty} for offense ${offense}`, logData);
87
+ return Promise.resolve(false);
88
+ }
89
+
90
+ if (this.hasOffended(offense, validator)) {
91
+ this.log.verbose(`Agreeing to slash validator ${validator} for offense ${offense}`, logData);
92
+ return Promise.resolve(true);
93
+ }
94
+
95
+ this.log.debug(`Refusing to slash validator ${validator} for offense ${offense}`, logData);
96
+ return Promise.resolve(false);
97
+ }
98
+
99
+ private handleInvalidBlock(event: InvalidBlockDetectedEvent): void {
100
+ const { validationResult } = event;
101
+ const block = validationResult.block.block;
102
+
103
+ // Check if we already have processed this block, archiver may emit the same event multiple times
104
+ if (this.invalidArchiveRoots.has(block.archive.root.toString())) {
105
+ this.log.trace(`Already processed invalid block ${block.number}`);
106
+ return;
107
+ }
108
+
109
+ this.log.verbose(`Detected invalid block ${block.number}`, {
110
+ ...block.toBlockInfo(),
111
+ reason: validationResult.valid === false ? validationResult.reason : 'unknown',
112
+ });
113
+
114
+ // Store the invalid block
115
+ this.addInvalidBlock(event.validationResult.block);
116
+
117
+ // Slash the proposer of the invalid block
118
+ this.slashProposer(event.validationResult);
119
+
120
+ // Check if the parent of this block is invalid as well, if so, we will slash its attestors as well
121
+ this.slashAttestorsOnAncestorInvalid(event.validationResult);
122
+ }
123
+
124
+ private slashAttestorsOnAncestorInvalid(validationResult: ValidateBlockNegativeResult) {
125
+ const block = validationResult.block;
126
+
127
+ const parentArchive = block.block.header.lastArchive.root.toString();
128
+ if (this.invalidArchiveRoots.has(block.block.header.lastArchive.root.toString())) {
129
+ const attestors = validationResult.attestations.map(a => a.getSender());
130
+ this.log.info(`Want to slash attestors of block ${block.block.number} built on invalid block`, {
131
+ ...block.block.toBlockInfo(),
132
+ ...attestors,
133
+ parentArchive,
134
+ });
135
+
136
+ attestors.forEach(attestor => this.badAttestors.add(attestor.toString()));
137
+
138
+ this.emit(
139
+ WANT_TO_SLASH_EVENT,
140
+ attestors.map(attestor => ({
141
+ validator: attestor,
142
+ amount: this.config.slashAttestDescendantOfInvalidPenalty,
143
+ offense: Offense.ATTESTED_DESCENDANT_OF_INVALID,
144
+ })),
145
+ );
146
+ }
147
+ }
148
+
149
+ private slashProposer(validationResult: ValidateBlockNegativeResult) {
150
+ const { reason, block } = validationResult;
151
+ const blockNumber = block.block.number;
152
+ const slot = block.block.header.getSlot();
153
+ const proposer = this.epochCache.getProposerFromEpochCommittee(validationResult, slot);
154
+
155
+ if (!proposer) {
156
+ this.log.warn(`No proposer found for block ${blockNumber} at slot ${slot}`);
157
+ return;
158
+ }
159
+
160
+ const offense = this.getOffenseFromInvalidationReason(reason);
161
+ const amount = this.config.slashProposeInvalidAttestationsPenalty;
162
+ const args: WantToSlashArgs = { validator: proposer, amount, offense };
163
+
164
+ this.log.info(`Want to slash proposer of block ${blockNumber} due to ${reason}`, {
165
+ ...block.block.toBlockInfo(),
166
+ ...args,
167
+ });
168
+
169
+ this.badProposers.add(proposer.toString());
170
+ this.emit(WANT_TO_SLASH_EVENT, [args]);
171
+ }
172
+
173
+ private getOffenseFromInvalidationReason(reason: ValidateBlockNegativeResult['reason']): Offense {
174
+ switch (reason) {
175
+ case 'invalid-attestation':
176
+ return Offense.PROPOSED_INCORRECT_ATTESTATIONS;
177
+ case 'insufficient-attestations':
178
+ return Offense.PROPOSED_INSUFFICIENT_ATTESTATIONS;
179
+ default: {
180
+ const _: never = reason;
181
+ return Offense.UNKNOWN;
182
+ }
183
+ }
184
+ }
185
+
186
+ private getMaxPenalty(offense: Offense) {
187
+ switch (offense) {
188
+ case Offense.PROPOSED_INCORRECT_ATTESTATIONS:
189
+ case Offense.PROPOSED_INSUFFICIENT_ATTESTATIONS:
190
+ return this.config.slashProposeInvalidAttestationsMaxPenalty;
191
+ case Offense.ATTESTED_DESCENDANT_OF_INVALID:
192
+ return this.config.slashProposeInvalidAttestationsMaxPenalty;
193
+ default:
194
+ return 0n;
195
+ }
196
+ }
197
+
198
+ private hasOffended(offense: Offense, validator: EthAddress): boolean {
199
+ switch (offense) {
200
+ case Offense.PROPOSED_INCORRECT_ATTESTATIONS:
201
+ case Offense.PROPOSED_INSUFFICIENT_ATTESTATIONS:
202
+ return this.badProposers.has(validator.toString());
203
+ case Offense.ATTESTED_DESCENDANT_OF_INVALID:
204
+ return this.badAttestors.has(validator.toString());
205
+ default:
206
+ return false;
207
+ }
208
+ }
209
+
210
+ private addInvalidBlock(block: PublishedL2Block) {
211
+ this.invalidArchiveRoots.add(block.block.archive.root.toString());
212
+
213
+ // Prune old entries if we exceed the maximum
214
+ if (this.invalidArchiveRoots.size > this.maxInvalidBlocks) {
215
+ const oldestKey = this.invalidArchiveRoots.keys().next().value!;
216
+ this.invalidArchiveRoots.delete(oldestKey);
217
+ }
218
+ }
219
+ }
package/src/config.ts CHANGED
@@ -1,45 +1,19 @@
1
+ import { NULL_KEY } from '@aztec/ethereum';
1
2
  import type { ConfigMappingsType } from '@aztec/foundation/config';
2
3
  import {
4
+ SecretValue,
3
5
  bigintConfigHelper,
4
6
  booleanConfigHelper,
5
7
  floatConfigHelper,
6
8
  numberConfigHelper,
9
+ secretValueConfigHelper,
7
10
  } from '@aztec/foundation/config';
8
11
  import { EthAddress } from '@aztec/foundation/eth-address';
9
12
  import type { TypedEventEmitter } from '@aztec/foundation/types';
13
+ import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
14
+ import { Offense } from '@aztec/stdlib/slashing';
10
15
 
11
- export enum Offense {
12
- UNKNOWN = 0,
13
- DATA_WITHHOLDING = 1,
14
- VALID_EPOCH_PRUNED = 2,
15
- INACTIVITY = 3,
16
- INVALID_BLOCK = 4,
17
- }
18
-
19
- export const OffenseToBigInt: Record<Offense, bigint> = {
20
- [Offense.UNKNOWN]: 0n,
21
- [Offense.DATA_WITHHOLDING]: 1n,
22
- [Offense.VALID_EPOCH_PRUNED]: 2n,
23
- [Offense.INACTIVITY]: 3n,
24
- [Offense.INVALID_BLOCK]: 4n,
25
- };
26
-
27
- export function bigIntToOffense(offense: bigint): Offense {
28
- switch (offense) {
29
- case 0n:
30
- return Offense.UNKNOWN;
31
- case 1n:
32
- return Offense.DATA_WITHHOLDING;
33
- case 2n:
34
- return Offense.VALID_EPOCH_PRUNED;
35
- case 3n:
36
- return Offense.INACTIVITY;
37
- case 4n:
38
- return Offense.INVALID_BLOCK;
39
- default:
40
- throw new Error(`Unknown offense: ${offense}`);
41
- }
42
- }
16
+ export type { SlasherConfig };
43
17
 
44
18
  export const WANT_TO_SLASH_EVENT = 'wantToSlash' as const;
45
19
 
@@ -64,25 +38,6 @@ export type Watcher = WatcherEmitter & {
64
38
  stop?: () => Promise<void>;
65
39
  };
66
40
 
67
- export interface SlasherConfig {
68
- // New configurations based on design doc
69
- slashOverridePayload?: EthAddress;
70
- slashPayloadTtlSeconds: number; // TTL for payloads, in seconds
71
- slashPruneEnabled: boolean;
72
- slashPrunePenalty: bigint;
73
- slashPruneMaxPenalty: bigint;
74
- slashInvalidBlockEnabled: boolean;
75
- slashInvalidBlockPenalty: bigint;
76
- slashInvalidBlockMaxPenalty: bigint;
77
- slashInactivityEnabled: boolean;
78
- slashInactivityCreateTargetPercentage: number; // 0-1, 0.9 means 90%. Must be greater than 0
79
- slashInactivitySignalTargetPercentage: number; // 0-1, 0.6 means 60%. Must be greater than 0
80
- slashInactivityCreatePenalty: bigint;
81
- slashInactivityMaxPenalty: bigint;
82
- slashProposerRoundPollingIntervalSeconds: number;
83
- // Consider adding: slashInactivityCreateEnabled: boolean;
84
- }
85
-
86
41
  export const DefaultSlasherConfig: SlasherConfig = {
87
42
  slashPayloadTtlSeconds: 60 * 60 * 24, // 1 day
88
43
  slashOverridePayload: undefined,
@@ -97,7 +52,12 @@ export const DefaultSlasherConfig: SlasherConfig = {
97
52
  slashInactivitySignalTargetPercentage: 0.6,
98
53
  slashInactivityCreatePenalty: 1n,
99
54
  slashInactivityMaxPenalty: 100n,
55
+ slashProposeInvalidAttestationsPenalty: 1n,
56
+ slashProposeInvalidAttestationsMaxPenalty: 100n,
57
+ slashAttestDescendantOfInvalidPenalty: 1n,
58
+ slashAttestDescendantOfInvalidMaxPenalty: 100n,
100
59
  slashProposerRoundPollingIntervalSeconds: 12,
60
+ slasherPrivateKey: new SecretValue<string | undefined>(undefined),
101
61
  };
102
62
 
103
63
  export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
@@ -177,8 +137,32 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
177
137
  description: 'Maximum penalty amount for slashing an inactive validator.',
178
138
  ...bigintConfigHelper(DefaultSlasherConfig.slashInactivityMaxPenalty),
179
139
  },
140
+ slashProposeInvalidAttestationsPenalty: {
141
+ env: 'SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY',
142
+ description: 'Penalty amount for slashing a proposer that proposed invalid attestations.',
143
+ ...bigintConfigHelper(DefaultSlasherConfig.slashProposeInvalidAttestationsPenalty),
144
+ },
145
+ slashProposeInvalidAttestationsMaxPenalty: {
146
+ env: 'SLASH_PROPOSE_INVALID_ATTESTATIONS_MAX_PENALTY',
147
+ description: 'Maximum penalty amount for slashing a proposer that proposed invalid attestations.',
148
+ ...bigintConfigHelper(DefaultSlasherConfig.slashProposeInvalidAttestationsMaxPenalty),
149
+ },
150
+ slashAttestDescendantOfInvalidPenalty: {
151
+ env: 'SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY',
152
+ description: 'Penalty amount for slashing a validator that attested to a descendant of an invalid block.',
153
+ ...bigintConfigHelper(DefaultSlasherConfig.slashAttestDescendantOfInvalidPenalty),
154
+ },
155
+ slashAttestDescendantOfInvalidMaxPenalty: {
156
+ env: 'SLASH_ATTEST_DESCENDANT_OF_INVALID_MAX_PENALTY',
157
+ description: 'Maximum penalty amount for slashing a validator that attested to a descendant of an invalid block.',
158
+ ...bigintConfigHelper(DefaultSlasherConfig.slashAttestDescendantOfInvalidMaxPenalty),
159
+ },
180
160
  slashProposerRoundPollingIntervalSeconds: {
181
161
  description: 'Polling interval for slashing proposer round in seconds.',
182
162
  ...numberConfigHelper(DefaultSlasherConfig.slashProposerRoundPollingIntervalSeconds),
183
163
  },
164
+ slasherPrivateKey: {
165
+ description: 'Private key used for creating slash payloads.',
166
+ ...secretValueConfigHelper(val => (val ? `0x${val.replace('0x', '')}` : NULL_KEY)),
167
+ },
184
168
  };
@@ -1,3 +1,4 @@
1
+ import type { Tx } from '@aztec/aztec.js';
1
2
  import { EpochCache } from '@aztec/epoch-cache';
2
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
3
4
  import {
@@ -7,8 +8,9 @@ import {
7
8
  type L2BlockSourceEventEmitter,
8
9
  L2BlockSourceEvents,
9
10
  } from '@aztec/stdlib/block';
10
- import type { IFullNodeBlockBuilder, ITxCollector, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
11
+ import type { IFullNodeBlockBuilder, ITxProvider, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
11
12
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
13
+ import { Offense } from '@aztec/stdlib/slashing';
12
14
  import {
13
15
  ReExFailedTxsError,
14
16
  ReExStateMismatchError,
@@ -18,7 +20,7 @@ import {
18
20
 
19
21
  import EventEmitter from 'node:events';
20
22
 
21
- import { Offense, WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from './config.js';
23
+ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from './config.js';
22
24
 
23
25
  /**
24
26
  * This watcher is responsible for detecting chain prunes and creating slashing arguments for the committee.
@@ -34,11 +36,14 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
34
36
  // Only keep track of the last N slashable epochs
35
37
  private maxSlashableEpochs = 100;
36
38
 
39
+ // Store bound function reference for proper listener removal
40
+ private boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
41
+
37
42
  constructor(
38
43
  private l2BlockSource: L2BlockSourceEventEmitter,
39
44
  private l1ToL2MessageSource: L1ToL2MessageSource,
40
45
  private epochCache: EpochCache,
41
- private txCollector: ITxCollector,
46
+ private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
42
47
  private blockBuilder: IFullNodeBlockBuilder,
43
48
  private penalty: bigint,
44
49
  private maxPenalty: bigint,
@@ -48,12 +53,12 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
48
53
  }
49
54
 
50
55
  public start() {
51
- this.l2BlockSource.on(L2BlockSourceEvents.L2PruneDetected, this.handlePruneL2Blocks.bind(this));
56
+ this.l2BlockSource.on(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
52
57
  return Promise.resolve();
53
58
  }
54
59
 
55
60
  public stop() {
56
- this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.handlePruneL2Blocks.bind(this));
61
+ this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
57
62
  return Promise.resolve();
58
63
  }
59
64
 
@@ -120,16 +125,18 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
120
125
  public async validateBlock(blockFromL1: L2Block, fork: MerkleTreeWriteOperations): Promise<void> {
121
126
  this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
122
127
  const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
123
- const { txs, missing } = await this.txCollector.collectTransactions(
124
- txHashes,
125
- undefined, // ask from no one in particular
126
- );
127
- if (missing && missing.length > 0) {
128
- throw new TransactionsNotAvailableError(missing);
128
+ // We load txs from the mempool directly, since the TxCollector running in the background has already been
129
+ // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
130
+ // it's likely that they are not available in the network at all.
131
+ const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
132
+
133
+ if (missingTxs && missingTxs.length > 0) {
134
+ throw new TransactionsNotAvailableError(missingTxs);
129
135
  }
136
+
130
137
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockFromL1.number);
131
138
  const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
132
- txs,
139
+ txs as Tx[],
133
140
  l1ToL2Messages,
134
141
  blockFromL1.header.globalVariables,
135
142
  {},
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './config.js';
2
2
  export * from './epoch_prune_watcher.js';
3
3
  export * from './slasher_client.js';
4
+ export * from './attestations_block_watcher.js';
5
+ export * from '@aztec/stdlib/slashing';