@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.
@@ -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,14 +115,23 @@ 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.
129
+ * @param config - the new config. Cannot update the slasher private key.
112
130
  */ updateConfig(config) {
131
+ const { slasherPrivateKey: _doNotUpdate, ...configWithoutPrivateKey } = config;
113
132
  const newConfig = {
114
133
  ...this.config,
115
- ...config
134
+ ...configWithoutPrivateKey
116
135
  };
117
136
  // We keep this separate flag to tell us if we should be signal for the override payload: after the override payload is executed,
118
137
  // the slasher goes back to using the monitored payloads to inform the sequencer publisher what payload to signal for.
@@ -158,17 +177,17 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
158
177
  * Bound to the slashing proposer contract's listenToProposalExecuted method in `.start()`.
159
178
  *
160
179
  * @param {round: bigint; proposal: `0x${string}`} param0
161
- */ proposalExecuted({ round, proposal }) {
162
- this.log.info('Proposal executed', {
180
+ */ payloadSubmitted({ round, payload }) {
181
+ this.log.info('Payload submitted', {
163
182
  round,
164
- proposal
183
+ payload
165
184
  });
166
- const payload = EthAddress.fromString(proposal);
185
+ const payloadAddress = EthAddress.fromString(payload);
167
186
  // Stop signaling for the override payload if it was executed
168
- if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payload)) {
187
+ if (this.overridePayloadActive && this.config.slashOverridePayload?.equals(payloadAddress)) {
169
188
  this.overridePayloadActive = false;
170
189
  }
171
- const index = this.monitoredPayloads.findIndex((p)=>p.payloadAddress.equals(payload));
190
+ const index = this.monitoredPayloads.findIndex((p)=>p.payloadAddress.equals(payloadAddress));
172
191
  if (index === -1) {
173
192
  return;
174
193
  }
@@ -180,6 +199,13 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
180
199
  *
181
200
  * @param args - the arguments from the watcher, including the validators, amounts, and offenses
182
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
+ }
183
209
  const sortedArgs = [
184
210
  ...args
185
211
  ].sort((a, b)=>a.validator.toString().localeCompare(b.validator.toString()));
@@ -351,32 +377,39 @@ import { WANT_TO_SLASH_EVENT, bigIntToOffense } from './config.js';
351
377
  });
352
378
  }
353
379
  /**
354
- * Execute a round if we agree with the proposal.
380
+ * Submit a round to the Slasher if we agree with the payload.
355
381
  *
356
- * 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.
357
383
  *
358
384
  * @param {proposal: `0x${string}`; round: bigint} param0
359
- */ async executeRoundIfAgree({ proposal, round }) {
360
- const payload = EthAddress.fromString(proposal);
361
- 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))) {
362
395
  this.log.debug('Round executable, but we disagree', {
363
- proposal,
396
+ payload,
364
397
  round
365
398
  });
366
399
  return;
367
400
  }
368
- const nextRound = round + 1n;
369
- this.log.info(`Waiting for round ${nextRound} to be reached`);
370
- 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);
371
404
  if (!reached) {
372
405
  this.log.warn('Round not reached', {
373
- proposal,
406
+ payload,
374
407
  round
375
408
  });
376
409
  return;
377
410
  }
378
411
  this.log.info('Executing round', {
379
- proposal,
412
+ payload,
380
413
  round
381
414
  });
382
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.1",
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.1",
58
- "@aztec/ethereum": "1.2.1",
59
- "@aztec/foundation": "1.2.1",
60
- "@aztec/l1-artifacts": "1.2.1",
61
- "@aztec/stdlib": "1.2.1",
62
- "@aztec/telemetry-client": "1.2.1",
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.1",
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,46 +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';
10
13
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
14
+ import { Offense } from '@aztec/stdlib/slashing';
11
15
 
12
- export enum Offense {
13
- UNKNOWN = 0,
14
- DATA_WITHHOLDING = 1,
15
- VALID_EPOCH_PRUNED = 2,
16
- INACTIVITY = 3,
17
- INVALID_BLOCK = 4,
18
- }
19
-
20
- export const OffenseToBigInt: Record<Offense, bigint> = {
21
- [Offense.UNKNOWN]: 0n,
22
- [Offense.DATA_WITHHOLDING]: 1n,
23
- [Offense.VALID_EPOCH_PRUNED]: 2n,
24
- [Offense.INACTIVITY]: 3n,
25
- [Offense.INVALID_BLOCK]: 4n,
26
- };
27
-
28
- export function bigIntToOffense(offense: bigint): Offense {
29
- switch (offense) {
30
- case 0n:
31
- return Offense.UNKNOWN;
32
- case 1n:
33
- return Offense.DATA_WITHHOLDING;
34
- case 2n:
35
- return Offense.VALID_EPOCH_PRUNED;
36
- case 3n:
37
- return Offense.INACTIVITY;
38
- case 4n:
39
- return Offense.INVALID_BLOCK;
40
- default:
41
- throw new Error(`Unknown offense: ${offense}`);
42
- }
43
- }
16
+ export type { SlasherConfig };
44
17
 
45
18
  export const WANT_TO_SLASH_EVENT = 'wantToSlash' as const;
46
19
 
@@ -79,7 +52,12 @@ export const DefaultSlasherConfig: SlasherConfig = {
79
52
  slashInactivitySignalTargetPercentage: 0.6,
80
53
  slashInactivityCreatePenalty: 1n,
81
54
  slashInactivityMaxPenalty: 100n,
55
+ slashProposeInvalidAttestationsPenalty: 1n,
56
+ slashProposeInvalidAttestationsMaxPenalty: 100n,
57
+ slashAttestDescendantOfInvalidPenalty: 1n,
58
+ slashAttestDescendantOfInvalidMaxPenalty: 100n,
82
59
  slashProposerRoundPollingIntervalSeconds: 12,
60
+ slasherPrivateKey: new SecretValue<string | undefined>(undefined),
83
61
  };
84
62
 
85
63
  export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
@@ -159,8 +137,32 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
159
137
  description: 'Maximum penalty amount for slashing an inactive validator.',
160
138
  ...bigintConfigHelper(DefaultSlasherConfig.slashInactivityMaxPenalty),
161
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
+ },
162
160
  slashProposerRoundPollingIntervalSeconds: {
163
161
  description: 'Polling interval for slashing proposer round in seconds.',
164
162
  ...numberConfigHelper(DefaultSlasherConfig.slashProposerRoundPollingIntervalSeconds),
165
163
  },
164
+ slasherPrivateKey: {
165
+ description: 'Private key used for creating slash payloads.',
166
+ ...secretValueConfigHelper(val => (val ? `0x${val.replace('0x', '')}` : NULL_KEY)),
167
+ },
166
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';