@aztec/slasher 4.0.0-nightly.20250907 → 4.0.0-nightly.20260108

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.
Files changed (65) hide show
  1. package/README.md +60 -11
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +8 -2
  5. package/dest/empire_slasher_client.d.ts +8 -6
  6. package/dest/empire_slasher_client.d.ts.map +1 -1
  7. package/dest/empire_slasher_client.js +11 -5
  8. package/dest/factory/create_facade.d.ts +3 -2
  9. package/dest/factory/create_facade.d.ts.map +1 -1
  10. package/dest/factory/create_implementation.d.ts +3 -3
  11. package/dest/factory/create_implementation.d.ts.map +1 -1
  12. package/dest/factory/create_implementation.js +6 -4
  13. package/dest/factory/get_settings.d.ts +2 -2
  14. package/dest/factory/get_settings.d.ts.map +1 -1
  15. package/dest/factory/index.d.ts +1 -1
  16. package/dest/index.d.ts +1 -1
  17. package/dest/null_slasher_client.d.ts +3 -2
  18. package/dest/null_slasher_client.d.ts.map +1 -1
  19. package/dest/slash_offenses_collector.d.ts +1 -1
  20. package/dest/slash_offenses_collector.d.ts.map +1 -1
  21. package/dest/slash_offenses_collector.js +1 -2
  22. package/dest/slash_round_monitor.d.ts +5 -4
  23. package/dest/slash_round_monitor.d.ts.map +1 -1
  24. package/dest/slasher_client_facade.d.ts +4 -3
  25. package/dest/slasher_client_facade.d.ts.map +1 -1
  26. package/dest/slasher_client_facade.js +1 -0
  27. package/dest/slasher_client_interface.d.ts +3 -2
  28. package/dest/slasher_client_interface.d.ts.map +1 -1
  29. package/dest/stores/offenses_store.d.ts +1 -1
  30. package/dest/stores/offenses_store.d.ts.map +1 -1
  31. package/dest/stores/offenses_store.js +1 -1
  32. package/dest/stores/payloads_store.d.ts +2 -2
  33. package/dest/stores/payloads_store.d.ts.map +1 -1
  34. package/dest/stores/schema_version.d.ts +1 -1
  35. package/dest/tally_slasher_client.d.ts +14 -8
  36. package/dest/tally_slasher_client.d.ts.map +1 -1
  37. package/dest/tally_slasher_client.js +57 -12
  38. package/dest/test/dummy_watcher.d.ts +11 -0
  39. package/dest/test/dummy_watcher.d.ts.map +1 -0
  40. package/dest/test/dummy_watcher.js +14 -0
  41. package/dest/watcher.d.ts +3 -1
  42. package/dest/watcher.d.ts.map +1 -1
  43. package/dest/watchers/attestations_block_watcher.d.ts +6 -3
  44. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  45. package/dest/watchers/attestations_block_watcher.js +37 -22
  46. package/dest/watchers/epoch_prune_watcher.d.ts +8 -7
  47. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  48. package/dest/watchers/epoch_prune_watcher.js +48 -37
  49. package/package.json +13 -12
  50. package/src/config.ts +8 -2
  51. package/src/empire_slasher_client.ts +15 -8
  52. package/src/factory/create_facade.ts +2 -1
  53. package/src/factory/create_implementation.ts +6 -1
  54. package/src/factory/get_settings.ts +1 -1
  55. package/src/null_slasher_client.ts +2 -1
  56. package/src/slash_offenses_collector.ts +1 -2
  57. package/src/slash_round_monitor.ts +3 -2
  58. package/src/slasher_client_facade.ts +4 -2
  59. package/src/slasher_client_interface.ts +2 -1
  60. package/src/stores/offenses_store.ts +1 -1
  61. package/src/tally_slasher_client.ts +80 -17
  62. package/src/test/dummy_watcher.ts +21 -0
  63. package/src/watcher.ts +4 -1
  64. package/src/watchers/attestations_block_watcher.ts +44 -26
  65. package/src/watchers/epoch_prune_watcher.ts +67 -55
@@ -1,9 +1,16 @@
1
+ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
+ import { merge, pick } from '@aztec/foundation/collection';
1
3
  import { createLogger } from '@aztec/foundation/log';
2
4
  import { L2BlockSourceEvents } from '@aztec/stdlib/block';
3
- import { OffenseType } from '@aztec/stdlib/slashing';
5
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
6
+ import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
4
7
  import { ReExFailedTxsError, ReExStateMismatchError, TransactionsNotAvailableError, ValidatorError } from '@aztec/stdlib/validators';
5
8
  import EventEmitter from 'node:events';
6
9
  import { WANT_TO_SLASH_EVENT } from '../watcher.js';
10
+ const EpochPruneWatcherPenaltiesConfigKeys = [
11
+ 'slashPrunePenalty',
12
+ 'slashDataWithholdingPenalty'
13
+ ];
7
14
  /**
8
15
  * This watcher is responsible for detecting chain prunes and creating slashing arguments for the committee.
9
16
  * It only wants to slash if:
@@ -15,12 +22,13 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
15
22
  epochCache;
16
23
  txProvider;
17
24
  blockBuilder;
18
- penalties;
19
25
  log;
20
26
  // Store bound function reference for proper listener removal
21
27
  boundHandlePruneL2Blocks;
28
+ penalties;
22
29
  constructor(l2BlockSource, l1ToL2MessageSource, epochCache, txProvider, blockBuilder, penalties){
23
- super(), this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.epochCache = epochCache, this.txProvider = txProvider, this.blockBuilder = blockBuilder, this.penalties = penalties, this.log = createLogger('epoch-prune-watcher'), this.boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
30
+ super(), this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.epochCache = epochCache, this.txProvider = txProvider, this.blockBuilder = blockBuilder, this.log = createLogger('epoch-prune-watcher'), this.boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
31
+ this.penalties = pick(penalties, ...EpochPruneWatcherPenaltiesConfigKeys);
24
32
  this.log.verbose(`EpochPruneWatcher initialized with penalties: valid epoch pruned=${penalties.slashPrunePenalty} data withholding=${penalties.slashDataWithholdingPenalty}`);
25
33
  }
26
34
  start() {
@@ -31,50 +39,52 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
31
39
  this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
32
40
  return Promise.resolve();
33
41
  }
42
+ updateConfig(config) {
43
+ this.penalties = merge(this.penalties, pick(config, ...EpochPruneWatcherPenaltiesConfigKeys));
44
+ this.log.verbose('EpochPruneWatcher config updated', this.penalties);
45
+ }
34
46
  handlePruneL2Blocks(event) {
35
47
  const { blocks, epochNumber } = event;
36
- this.log.info(`Detected chain prune. Validating epoch ${epochNumber}`);
37
- this.validateBlocks(blocks).then(async ()=>{
48
+ void this.processPruneL2Blocks(blocks, epochNumber).catch((err)=>this.log.error('Error processing pruned L2 blocks', err, {
49
+ epochNumber
50
+ }));
51
+ }
52
+ async emitSlashForEpoch(offense, epochNumber) {
53
+ const validators = await this.getValidatorsForEpoch(epochNumber);
54
+ if (validators.length === 0) {
55
+ this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
56
+ return;
57
+ }
58
+ const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
59
+ this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
60
+ this.emit(WANT_TO_SLASH_EVENT, args);
61
+ }
62
+ async processPruneL2Blocks(blocks, epochNumber) {
63
+ try {
64
+ const l1Constants = this.epochCache.getL1Constants();
65
+ const epochBlocks = blocks.filter((b)=>getEpochAtSlot(b.slot, l1Constants) === epochNumber);
66
+ this.log.info(`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, {
67
+ blocks: epochBlocks.map((b)=>b.toBlockInfo())
68
+ });
69
+ await this.validateBlocks(epochBlocks);
38
70
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
39
- const validators = await this.getValidatorsForEpoch(epochNumber);
40
- // need to specify return type to be able to return offense as undefined later on
41
- const result = {
42
- validators,
43
- offense: OffenseType.VALID_EPOCH_PRUNED
44
- };
45
- return result;
46
- }).catch(async (error)=>{
71
+ await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
72
+ } catch (error) {
47
73
  if (error instanceof TransactionsNotAvailableError) {
48
- this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, error);
49
- const validators = await this.getValidatorsForEpoch(epochNumber);
50
- return {
51
- validators,
52
- offense: OffenseType.DATA_WITHHOLDING
53
- };
74
+ this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
75
+ message: error.message
76
+ });
77
+ await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
54
78
  } else {
55
79
  this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
56
- return {
57
- validators: [],
58
- offense: undefined
59
- };
60
80
  }
61
- }).then(({ validators, offense })=>{
62
- if (validators.length === 0 || offense === undefined) {
63
- return;
64
- }
65
- const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
66
- this.log.info(`Slash for epoch ${epochNumber} created`, args);
67
- this.emit(WANT_TO_SLASH_EVENT, args);
68
- }).catch((error)=>{
69
- // This can happen if we fail to get the validators for the epoch.
70
- this.log.error('Error while creating slash for epoch', error);
71
- });
81
+ }
72
82
  }
73
83
  async validateBlocks(blocks) {
74
84
  if (blocks.length === 0) {
75
85
  return;
76
86
  }
77
- const fork = await this.blockBuilder.getFork(blocks[0].header.globalVariables.blockNumber - 1);
87
+ const fork = await this.blockBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
78
88
  try {
79
89
  for (const block of blocks){
80
90
  await this.validateBlock(block, fork);
@@ -93,7 +103,8 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
93
103
  if (missingTxs && missingTxs.length > 0) {
94
104
  throw new TransactionsNotAvailableError(missingTxs);
95
105
  }
96
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockFromL1.number);
106
+ const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
107
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
97
108
  const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, blockFromL1.header.globalVariables, {}, fork);
98
109
  if (numTxs !== txs.length) {
99
110
  // This should be detected by state mismatch, but this makes it easier to debug.
@@ -120,7 +131,7 @@ import { WANT_TO_SLASH_EVENT } from '../watcher.js';
120
131
  validator: v,
121
132
  amount: penalty,
122
133
  offenseType,
123
- epochOrSlot
134
+ epochOrSlot: BigInt(epochOrSlot)
124
135
  }));
125
136
  }
126
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "4.0.0-nightly.20250907",
3
+ "version": "4.0.0-nightly.20260108",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -10,8 +10,8 @@
10
10
  "../package.common.json"
11
11
  ],
12
12
  "scripts": {
13
- "build": "yarn clean && tsc -b",
14
- "build:dev": "tsc -b --watch",
13
+ "build": "yarn clean && ../scripts/tsc.sh",
14
+ "build:dev": "../scripts/tsc.sh --watch",
15
15
  "clean": "rm -rf ./dest .tsbuildinfo",
16
16
  "bb": "node --no-warnings ./dest/bb/index.js",
17
17
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
@@ -54,24 +54,25 @@
54
54
  ]
55
55
  },
56
56
  "dependencies": {
57
- "@aztec/epoch-cache": "4.0.0-nightly.20250907",
58
- "@aztec/ethereum": "4.0.0-nightly.20250907",
59
- "@aztec/foundation": "4.0.0-nightly.20250907",
60
- "@aztec/kv-store": "4.0.0-nightly.20250907",
61
- "@aztec/l1-artifacts": "4.0.0-nightly.20250907",
62
- "@aztec/stdlib": "4.0.0-nightly.20250907",
63
- "@aztec/telemetry-client": "4.0.0-nightly.20250907",
57
+ "@aztec/epoch-cache": "4.0.0-nightly.20260108",
58
+ "@aztec/ethereum": "4.0.0-nightly.20260108",
59
+ "@aztec/foundation": "4.0.0-nightly.20260108",
60
+ "@aztec/kv-store": "4.0.0-nightly.20260108",
61
+ "@aztec/l1-artifacts": "4.0.0-nightly.20260108",
62
+ "@aztec/stdlib": "4.0.0-nightly.20260108",
63
+ "@aztec/telemetry-client": "4.0.0-nightly.20260108",
64
64
  "source-map-support": "^0.5.21",
65
65
  "tslib": "^2.4.0",
66
- "viem": "2.23.7",
66
+ "viem": "npm:@aztec/viem@2.38.2",
67
67
  "zod": "^3.23.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@aztec/aztec.js": "4.0.0-nightly.20250907",
70
+ "@aztec/aztec.js": "4.0.0-nightly.20260108",
71
71
  "@jest/globals": "^30.0.0",
72
72
  "@types/jest": "^30.0.0",
73
73
  "@types/node": "^22.15.17",
74
74
  "@types/source-map-support": "^0.5.10",
75
+ "@typescript/native-preview": "7.0.0-dev.20251126.1",
75
76
  "jest": "^30.0.0",
76
77
  "jest-mock-extended": "^4.0.0",
77
78
  "ts-node": "^10.9.1",
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DefaultL1ContractsConfig } from '@aztec/ethereum';
1
+ import { DefaultL1ContractsConfig } from '@aztec/ethereum/config';
2
2
  import type { ConfigMappingsType } from '@aztec/foundation/config';
3
3
  import {
4
4
  bigintConfigHelper,
@@ -29,6 +29,7 @@ export const DefaultSlasherConfig: SlasherConfig = {
29
29
  slashOffenseExpirationRounds: 4,
30
30
  slashMaxPayloadSize: 50,
31
31
  slashGracePeriodL2Slots: 0,
32
+ slashExecuteRoundsLookBack: 4,
32
33
  slashSelfAllowed: false,
33
34
  };
34
35
 
@@ -83,7 +84,7 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
83
84
  },
84
85
  slashBroadcastedInvalidBlockPenalty: {
85
86
  env: 'SLASH_INVALID_BLOCK_PENALTY',
86
- description: 'Penalty amount for slashing a validator for an invalid block.',
87
+ description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
87
88
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
88
89
  },
89
90
  slashInactivityTargetPercentage: {
@@ -144,6 +145,11 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
144
145
  env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
145
146
  ...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots),
146
147
  },
148
+ slashExecuteRoundsLookBack: {
149
+ env: 'SLASH_EXECUTE_ROUNDS_LOOK_BACK',
150
+ description: 'How many rounds to look back when searching for a round to execute.',
151
+ ...numberConfigHelper(DefaultSlasherConfig.slashExecuteRoundsLookBack),
152
+ },
147
153
  slashSelfAllowed: {
148
154
  description: 'Whether to allow slashes to own validators',
149
155
  ...booleanConfigHelper(DefaultSlasherConfig.slashSelfAllowed),
@@ -1,5 +1,6 @@
1
- import { EmpireSlashingProposerContract, RollupContract } from '@aztec/ethereum';
1
+ import { EmpireSlashingProposerContract, RollupContract, SlasherContract } from '@aztec/ethereum/contracts';
2
2
  import { sumBigint } from '@aztec/foundation/bigint';
3
+ import { SlotNumber } from '@aztec/foundation/branded-types';
3
4
  import { compactArray, filterAsync, maxBy, pick } from '@aztec/foundation/collection';
4
5
  import { EthAddress } from '@aztec/foundation/eth-address';
5
6
  import { createLogger } from '@aztec/foundation/log';
@@ -121,6 +122,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
121
122
  private settings: EmpireSlasherSettings,
122
123
  private slashFactoryContract: SlashFactoryContract,
123
124
  private slashingProposer: EmpireSlashingProposerContract,
125
+ private slasher: SlasherContract,
124
126
  private rollup: RollupContract,
125
127
  watchers: Watcher[],
126
128
  private dateProvider: DateProvider,
@@ -368,7 +370,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
368
370
  * @param slotNumber - The current slot number
369
371
  * @returns The actions to take
370
372
  */
371
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
373
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
372
374
  const [executeAction, proposePayloadActions] = await Promise.all([
373
375
  this.getExecutePayloadAction(slotNumber),
374
376
  this.getProposePayloadActions(slotNumber),
@@ -378,7 +380,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
378
380
  }
379
381
 
380
382
  /** Returns an execute payload action if there are any payloads ready to be executed */
381
- protected async getExecutePayloadAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
383
+ protected async getExecutePayloadAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
382
384
  const { round } = this.roundMonitor.getRoundForSlot(slotNumber);
383
385
  const toRemove: PayloadWithRound[] = [];
384
386
 
@@ -403,12 +405,17 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
403
405
  continue;
404
406
  }
405
407
 
408
+ // Check if slashing is enabled at all
409
+ if (!(await this.slasher.isSlashingEnabled())) {
410
+ this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
411
+ return undefined;
412
+ }
413
+
406
414
  // Check if the slash payload is vetoed
407
- const slasherContract = await this.rollup.getSlasherContract();
408
- const isVetoed = await slasherContract.isPayloadVetoed(payload.payload);
415
+ const isVetoed = await this.slasher.isPayloadVetoed(payload.payload);
409
416
 
410
417
  if (isVetoed) {
411
- this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed, skipping execution`);
418
+ this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed (skipping execution)`);
412
419
  toRemove.push(payload);
413
420
  continue;
414
421
  }
@@ -424,7 +431,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
424
431
  }
425
432
 
426
433
  /** Returns a vote or create payload action based on payload scoring */
427
- protected async getProposePayloadActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
434
+ protected async getProposePayloadActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
428
435
  // Compute what round we are in based on the slot number
429
436
  const { round, votingSlot } = this.roundMonitor.getRoundForSlot(slotNumber);
430
437
  const { slashingRoundSize: roundSize, slashingQuorumSize: quorumSize } = this.settings;
@@ -467,7 +474,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
467
474
  // Find the best existing payload. We filter out those that have no chance of winning given how many voting
468
475
  // slots are left in the round, and then filter by those we agree with.
469
476
  const feasiblePayloads = existingPayloads.filter(
470
- p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - votingSlot,
477
+ p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - BigInt(votingSlot),
471
478
  );
472
479
  const requiredOffenses = await this.getPendingUncontroversialOffensesForRound(round);
473
480
  const agreedPayloads = await filterAsync(feasiblePayloads, p => this.agreeWithPayload(p, round, requiredOffenses));
@@ -1,6 +1,7 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { L1ReaderConfig, ViemClient } from '@aztec/ethereum';
3
2
  import { RollupContract } from '@aztec/ethereum/contracts';
3
+ import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
4
+ import type { ViemClient } from '@aztec/ethereum/types';
4
5
  import { unique } from '@aztec/foundation/collection';
5
6
  import { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { createLogger } from '@aztec/foundation/log';
@@ -1,10 +1,10 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { ViemClient } from '@aztec/ethereum';
3
2
  import {
4
3
  EmpireSlashingProposerContract,
5
4
  RollupContract,
6
5
  TallySlashingProposerContract,
7
6
  } from '@aztec/ethereum/contracts';
7
+ import type { ViemClient } from '@aztec/ethereum/types';
8
8
  import { EthAddress } from '@aztec/foundation/eth-address';
9
9
  import { createLogger } from '@aztec/foundation/log';
10
10
  import { DateProvider } from '@aztec/foundation/timer';
@@ -71,6 +71,7 @@ async function createEmpireSlasher(
71
71
  l1GenesisTime,
72
72
  slotDuration,
73
73
  l1StartBlock,
74
+ slasher,
74
75
  ] = await Promise.all([
75
76
  slashingProposer.getExecutionDelayInRounds(),
76
77
  slashingProposer.getLifetimeInRounds(),
@@ -81,6 +82,7 @@ async function createEmpireSlasher(
81
82
  rollup.getL1GenesisTime(),
82
83
  rollup.getSlotDuration(),
83
84
  rollup.getL1StartBlock(),
85
+ rollup.getSlasherContract(),
84
86
  ]);
85
87
 
86
88
  const settings: EmpireSlasherSettings = {
@@ -110,6 +112,7 @@ async function createEmpireSlasher(
110
112
  settings,
111
113
  slashFactoryContract,
112
114
  slashingProposer,
115
+ slasher!,
113
116
  rollup,
114
117
  watchers,
115
118
  dateProvider,
@@ -134,6 +137,7 @@ async function createTallySlasher(
134
137
  }
135
138
 
136
139
  const settings = await getTallySlasherSettings(rollup, slashingProposer);
140
+ const slasher = await rollup.getSlasherContract();
137
141
 
138
142
  const offensesStore = new SlasherOffensesStore(kvStore, {
139
143
  ...settings,
@@ -144,6 +148,7 @@ async function createTallySlasher(
144
148
  config,
145
149
  settings,
146
150
  slashingProposer,
151
+ slasher!,
147
152
  rollup,
148
153
  watchers,
149
154
  epochCache,
@@ -1,4 +1,4 @@
1
- import type { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum';
1
+ import type { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
2
2
 
3
3
  import type { TallySlasherSettings } from '../tally_slasher_client.js';
4
4
 
@@ -1,3 +1,4 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
2
3
 
3
4
  import type { SlasherConfig } from './config.js';
@@ -30,7 +31,7 @@ export class NullSlasherClient implements SlasherClientInterface {
30
31
  this.config = { ...this.config, ...config };
31
32
  }
32
33
 
33
- public getProposerActions(_slotNumber: bigint): Promise<ProposerSlashAction[]> {
34
+ public getProposerActions(_slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
34
35
  return Promise.resolve([]);
35
36
  }
36
37
 
@@ -95,10 +95,9 @@ export class SlashOffensesCollector {
95
95
  * Clears expired offenses from stores.
96
96
  */
97
97
  public async handleNewRound(round: bigint) {
98
- this.log.verbose(`Clearing expired offenses for new slashing round ${round}`);
99
98
  const cleared = await this.offensesStore.clearExpiredOffenses(round);
100
99
  if (cleared && cleared > 0) {
101
- this.log.verbose(`Cleared ${cleared} expired offenses`);
100
+ this.log.debug(`Cleared ${cleared} expired offenses for round ${round}`);
102
101
  }
103
102
  }
104
103
 
@@ -1,3 +1,4 @@
1
+ import { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import { createLogger } from '@aztec/foundation/log';
2
3
  import type { DateProvider } from '@aztec/foundation/timer';
3
4
  import type { Prettify } from '@aztec/foundation/types';
@@ -48,12 +49,12 @@ export class SlashRoundMonitor {
48
49
  }
49
50
 
50
51
  /** Returns the slashing round number and the voting slot within the round based on the L2 chain slot */
51
- public getRoundForSlot(slotNumber: bigint): { round: bigint; votingSlot: bigint } {
52
+ public getRoundForSlot(slotNumber: SlotNumber): { round: bigint; votingSlot: SlotNumber } {
52
53
  return getRoundForSlot(slotNumber, this.settings);
53
54
  }
54
55
 
55
56
  /** Returns the current slashing round and voting slot within the round */
56
- public getCurrentRound(): { round: bigint; votingSlot: bigint } {
57
+ public getCurrentRound(): { round: bigint; votingSlot: SlotNumber } {
57
58
  const now = this.dateProvider.nowInSeconds();
58
59
  const currentSlot = getSlotAtTimestamp(BigInt(now), this.settings);
59
60
  return this.getRoundForSlot(currentSlot);
@@ -1,6 +1,7 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import type { ViemClient } from '@aztec/ethereum';
3
2
  import { RollupContract } from '@aztec/ethereum/contracts';
3
+ import type { ViemClient } from '@aztec/ethereum/types';
4
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
4
5
  import { EthAddress } from '@aztec/foundation/eth-address';
5
6
  import { createLogger } from '@aztec/foundation/log';
6
7
  import { DateProvider } from '@aztec/foundation/timer';
@@ -58,6 +59,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
58
59
  public updateConfig(config: Partial<SlasherConfig>): void {
59
60
  this.config = { ...this.config, ...config };
60
61
  this.client?.updateConfig(config);
62
+ this.watchers.forEach(watcher => watcher.updateConfig?.(config));
61
63
  }
62
64
 
63
65
  public getSlashPayloads(): Promise<SlashPayloadRound[]> {
@@ -72,7 +74,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
72
74
  return this.client?.getPendingOffenses() ?? Promise.reject(new Error('Slasher client not initialized'));
73
75
  }
74
76
 
75
- public getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
77
+ public getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
76
78
  return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
77
79
  }
78
80
 
@@ -1,3 +1,4 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
2
3
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
3
4
 
@@ -38,7 +39,7 @@ export interface SlasherClientInterface {
38
39
  * @param slotNumber - The current slot number
39
40
  * @returns The actions to take
40
41
  */
41
- getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]>;
42
+ getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
42
43
 
43
44
  /** Returns the current config */
44
45
  getConfig(): SlasherConfig;
@@ -1,4 +1,4 @@
1
- import { createLogger } from '@aztec/aztec.js';
1
+ import { createLogger } from '@aztec/aztec.js/log';
2
2
  import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap, AztecAsyncSet } from '@aztec/kv-store';
3
3
  import {
4
4
  type Offense,
@@ -1,6 +1,8 @@
1
- import { EthAddress } from '@aztec/aztec.js';
1
+ import { EthAddress } from '@aztec/aztec.js/addresses';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
- import { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
3
+ import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
4
+ import { maxBigint } from '@aztec/foundation/bigint';
5
+ import { SlotNumber } from '@aztec/foundation/branded-types';
4
6
  import { compactArray, partition, times } from '@aztec/foundation/collection';
5
7
  import { createLogger } from '@aztec/foundation/log';
6
8
  import { sleep } from '@aztec/foundation/sleep';
@@ -45,7 +47,7 @@ export type TallySlasherSettings = Prettify<
45
47
  >;
46
48
 
47
49
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
48
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
50
+ Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
51
 
50
52
  /**
51
53
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -88,6 +90,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
88
90
  private config: TallySlasherClientConfig,
89
91
  private settings: TallySlasherSettings,
90
92
  private tallySlashingProposer: TallySlashingProposerContract,
93
+ private slasher: SlasherContract,
91
94
  private rollup: RollupContract,
92
95
  watchers: Watcher[],
93
96
  private epochCache: EpochCache,
@@ -167,7 +170,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
167
170
  * @param slotNumber - The current slot number
168
171
  * @returns The actions to take
169
172
  */
170
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
173
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
171
174
  const [executeAction, voteAction] = await Promise.all([
172
175
  this.getExecuteSlashAction(slotNumber),
173
176
  this.getVoteOffensesAction(slotNumber),
@@ -176,16 +179,65 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
176
179
  return compactArray<ProposerSlashAction>([executeAction, voteAction]);
177
180
  }
178
181
 
179
- /** Returns an execute slash action if there are any rounds ready to be executed */
180
- protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
182
+ /**
183
+ * Returns an execute slash action if there are any rounds ready to be executed.
184
+ * Returns the oldest slash action if there are multiple rounds pending execution.
185
+ */
186
+ protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
181
187
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
182
188
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
183
189
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
184
- if (executableRound < 0n) {
190
+ const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
191
+ const slashingLifetimeInRounds = BigInt(this.settings.slashingLifetimeInRounds);
192
+
193
+ // Compute the oldest executable round considering both lookBack and lifetimeInRounds
194
+ // A round is only executable if currentRound <= round + lifetimeInRounds
195
+ // So the oldest round we can execute is: currentRound - lifetimeInRounds
196
+ const oldestByLifetime = maxBigint(0n, currentRound - slashingLifetimeInRounds);
197
+ const oldestByLookBack = maxBigint(0n, executableRound - lookBack);
198
+ const oldestExecutableRound = maxBigint(oldestByLifetime, oldestByLookBack);
199
+
200
+ // Check if slashing is enabled at all
201
+ if (!(await this.slasher.isSlashingEnabled())) {
202
+ this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
185
203
  return undefined;
186
204
  }
187
205
 
188
- let logData: Record<string, unknown> = { currentRound, executableRound, slotNumber };
206
+ this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
207
+ slotNumber,
208
+ currentRound,
209
+ oldestExecutableRound,
210
+ oldestByLifetime,
211
+ oldestByLookBack,
212
+ executableRound,
213
+ slashingExecutionDelayInRounds,
214
+ lookBack,
215
+ slashingLifetimeInRounds,
216
+ });
217
+
218
+ // Iterate over all rounds, starting from the oldest, until we find one that is executable
219
+ for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
220
+ const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
221
+ if (action) {
222
+ return action;
223
+ }
224
+ }
225
+
226
+ // And return nothing if none are found
227
+ return undefined;
228
+ }
229
+
230
+ /**
231
+ * Checks if a given round is executable and returns an execute-slash action for it if so.
232
+ * Assumes round number has already been checked against lifetime and execution delay.
233
+ * @param executableRound - The round to check for execution
234
+ */
235
+ private async tryGetRoundExecuteAction(
236
+ executableRound: bigint,
237
+ slotNumber: SlotNumber,
238
+ ): Promise<ProposerSlashAction | undefined> {
239
+ let logData: Record<string, unknown> = { executableRound, slotNumber };
240
+ this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
189
241
 
190
242
  try {
191
243
  const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
@@ -193,9 +245,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
193
245
  if (roundInfo.isExecuted) {
194
246
  this.log.verbose(`Round ${executableRound} has already been executed`, logData);
195
247
  return undefined;
196
- } else if (!roundInfo.readyToExecute) {
197
- this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
198
- return undefined;
199
248
  } else if (roundInfo.voteCount === 0n) {
200
249
  this.log.debug(`Round ${executableRound} received no votes`, logData);
201
250
  return undefined;
@@ -204,6 +253,17 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
204
253
  return undefined;
205
254
  }
206
255
 
256
+ // Check if round is ready to execute at the given slot
257
+ const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
258
+ if (!isReadyToExecute) {
259
+ this.log.warn(
260
+ `Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`,
261
+ logData,
262
+ );
263
+ return undefined;
264
+ }
265
+
266
+ // Check if the round yields any slashing at all
207
267
  const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
208
268
  if (slashActions.length === 0) {
209
269
  this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
@@ -212,8 +272,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
212
272
 
213
273
  // Check if the slash payload is vetoed
214
274
  const payload = await this.tallySlashingProposer.getPayload(executableRound);
215
- const slasherContract = await this.rollup.getSlasherContract();
216
- const isVetoed = await slasherContract.isPayloadVetoed(payload.address);
275
+ const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
217
276
  if (isVetoed) {
218
277
  this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
219
278
  payloadAddress: payload.address.toString(),
@@ -239,13 +298,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
239
298
  return { type: 'execute-slash', round: executableRound, committees: slashedCommittees };
240
299
  } catch (error) {
241
300
  this.log.error(`Error checking round to execute ${executableRound}`, error);
301
+ return undefined;
242
302
  }
243
-
244
- return undefined;
245
303
  }
246
304
 
247
305
  /** Returns a vote action based on offenses from the target round (with offset applied) */
248
- protected async getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
306
+ protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
249
307
  // Compute what round we are in based on the slot number and what round will be slashed
250
308
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
251
309
  const slashedRound = this.getSlashedRound(currentRound);
@@ -299,7 +357,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
299
357
 
300
358
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
301
359
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
302
- const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees, this.settings);
360
+ const votes = getSlashConsensusVotesFromOffenses(
361
+ offensesToSlash,
362
+ committees,
363
+ epochsForCommittees.map(e => BigInt(e)),
364
+ this.settings,
365
+ );
303
366
  if (votes.every(v => v === 0)) {
304
367
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
305
368
  slotNumber,