@aztec/slasher 2.0.3-rc.17 → 2.0.3-rc.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,eAAO,MAAM,oBAAoB,EAAE,aAmBlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,kBAAkB,CAAC,aAAa,CAoHnE,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAQnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,eAAO,MAAM,oBAAoB,EAAE,aAoBlC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,kBAAkB,CAAC,aAAa,CAyHnE,CAAC"}
package/dest/config.js CHANGED
@@ -19,6 +19,7 @@ export const DefaultSlasherConfig = {
19
19
  slashOffenseExpirationRounds: 4,
20
20
  slashMaxPayloadSize: 50,
21
21
  slashGracePeriodL2Slots: 0,
22
+ slashExecuteRoundsLookBack: 4,
22
23
  slashSelfAllowed: false
23
24
  };
24
25
  export const slasherConfigMappings = {
@@ -62,7 +63,7 @@ export const slasherConfigMappings = {
62
63
  },
63
64
  slashBroadcastedInvalidBlockPenalty: {
64
65
  env: 'SLASH_INVALID_BLOCK_PENALTY',
65
- description: 'Penalty amount for slashing a validator for an invalid block.',
66
+ description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
66
67
  ...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty)
67
68
  },
68
69
  slashInactivityTargetPercentage: {
@@ -121,6 +122,11 @@ export const slasherConfigMappings = {
121
122
  env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
122
123
  ...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots)
123
124
  },
125
+ slashExecuteRoundsLookBack: {
126
+ env: 'SLASH_EXECUTE_ROUNDS_LOOK_BACK',
127
+ description: 'How many rounds to look back when searching for a round to execute.',
128
+ ...numberConfigHelper(DefaultSlasherConfig.slashExecuteRoundsLookBack)
129
+ },
124
130
  slashSelfAllowed: {
125
131
  description: 'Whether to allow slashes to own validators',
126
132
  ...booleanConfigHelper(DefaultSlasherConfig.slashSelfAllowed)
@@ -21,7 +21,7 @@ export type TallySlasherSettings = Prettify<SlashRoundMonitorSettings & SlashOff
21
21
  /** Committee size for block proposal */
22
22
  targetCommitteeSize: number;
23
23
  }>;
24
- export type TallySlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
24
+ export type TallySlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
25
25
  /**
26
26
  * The Tally Slasher client is responsible for managing slashable offenses using
27
27
  * the consensus-based slashing model where proposers vote on individual validator offenses.
@@ -87,8 +87,16 @@ export declare class TallySlasherClient implements ProposerSlashActionProvider,
87
87
  * @returns The actions to take
88
88
  */
89
89
  getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]>;
90
- /** Returns an execute slash action if there are any rounds ready to be executed */
90
+ /**
91
+ * Returns an execute slash action if there are any rounds ready to be executed.
92
+ * Returns the oldest slash action if there are multiple rounds pending execution.
93
+ */
91
94
  protected getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined>;
95
+ /**
96
+ * Checks if a given round is executable and returns an execute-slash action for it if so.
97
+ * Assumes round number has already been checked against lifetime and execution delay.
98
+ */
99
+ private tryGetRoundExecuteAction;
92
100
  /** Returns a vote action based on offenses from the target round (with offset applied) */
93
101
  protected getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined>;
94
102
  /** Returns the committees that were active during the timespan of a given round */
@@ -1 +1 @@
1
- {"version":3,"file":"tally_slasher_client.d.ts","sourceRoot":"","sources":["../src/tally_slasher_client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tally_slasher_client.d.ts","sourceRoot":"","sources":["../src/tally_slasher_client.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { EthAddress } from '@aztec/aztec.js';
2
+ import { maxBigint } from '@aztec/foundation/bigint';
2
3
  import { compactArray, partition, times } from '@aztec/foundation/collection';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
5
  import { sleep } from '@aztec/foundation/sleep';
@@ -120,19 +121,51 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
120
121
  voteAction
121
122
  ]);
122
123
  }
123
- /** Returns an execute slash action if there are any rounds ready to be executed */ async getExecuteSlashAction(slotNumber) {
124
+ /**
125
+ * Returns an execute slash action if there are any rounds ready to be executed.
126
+ * Returns the oldest slash action if there are multiple rounds pending execution.
127
+ */ async getExecuteSlashAction(slotNumber) {
124
128
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
125
129
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
126
130
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
127
- if (executableRound < 0n) {
131
+ const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
132
+ const oldestExecutableRound = maxBigint(0n, executableRound - lookBack);
133
+ // Check if slashing is enabled at all
134
+ if (!await this.slasher.isSlashingEnabled()) {
135
+ this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
128
136
  return undefined;
129
137
  }
130
- let logData = {
138
+ this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
139
+ slotNumber,
131
140
  currentRound,
141
+ oldestExecutableRound,
132
142
  executableRound,
133
- slotNumber
143
+ slashingExecutionDelayInRounds,
144
+ lookBack,
145
+ slashingLifetimeInRounds: this.settings.slashingLifetimeInRounds
146
+ });
147
+ // Iterate over all rounds, starting from the oldest, until we find one that is executable
148
+ for(let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++){
149
+ const action = await this.tryGetRoundExecuteAction(roundToCheck);
150
+ if (action) {
151
+ return action;
152
+ }
153
+ }
154
+ // And return nothing if none are found
155
+ return undefined;
156
+ }
157
+ /**
158
+ * Checks if a given round is executable and returns an execute-slash action for it if so.
159
+ * Assumes round number has already been checked against lifetime and execution delay.
160
+ */ async tryGetRoundExecuteAction(executableRound) {
161
+ let logData = {
162
+ executableRound
134
163
  };
164
+ this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
135
165
  try {
166
+ // Note we do not check isReadyToExecute here, since we already know that based on the
167
+ // executableRound number. Not just that, but it may be that we are building for the given slot number
168
+ // that is in the future, so the contract may think it's not yet ready to execute, whereas it is.
136
169
  const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
137
170
  logData = {
138
171
  ...logData,
@@ -141,9 +174,6 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
141
174
  if (roundInfo.isExecuted) {
142
175
  this.log.verbose(`Round ${executableRound} has already been executed`, logData);
143
176
  return undefined;
144
- } else if (!roundInfo.readyToExecute) {
145
- this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
146
- return undefined;
147
177
  } else if (roundInfo.voteCount === 0n) {
148
178
  this.log.debug(`Round ${executableRound} received no votes`, logData);
149
179
  return undefined;
@@ -151,6 +181,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
151
181
  this.log.verbose(`Round ${executableRound} does not have enough votes to execute`, logData);
152
182
  return undefined;
153
183
  }
184
+ // Check if the round yields any slashing at all
154
185
  const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
155
186
  if (slashActions.length === 0) {
156
187
  this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
@@ -184,8 +215,8 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
184
215
  };
185
216
  } catch (error) {
186
217
  this.log.error(`Error checking round to execute ${executableRound}`, error);
218
+ return undefined;
187
219
  }
188
- return undefined;
189
220
  }
190
221
  /** Returns a vote action based on offenses from the target round (with offset applied) */ async getVoteOffensesAction(slotNumber) {
191
222
  // Compute what round we are in based on the slot number and what round will be slashed
@@ -26,6 +26,8 @@ export declare class EpochPruneWatcher extends EpochPruneWatcher_base implements
26
26
  stop(): Promise<void>;
27
27
  updateConfig(config: Partial<SlasherConfig>): void;
28
28
  private handlePruneL2Blocks;
29
+ private emitSlashForEpoch;
30
+ private processPruneL2Blocks;
29
31
  validateBlocks(blocks: L2Block[]): Promise<void>;
30
32
  validateBlock(blockFromL1: L2Block, fork: MerkleTreeWriteOperations): Promise<void>;
31
33
  private getValidatorsForEpoch;
@@ -1 +1 @@
1
- {"version":3,"file":"epoch_prune_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/epoch_prune_watcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAEL,OAAO,EAEP,KAAK,yBAAyB,EAE/B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,qBAAqB,EACrB,WAAW,EACX,yBAAyB,EACzB,aAAa,EACd,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAWnE,OAAO,EAA6C,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE7G,QAAA,MAAM,oCAAoC,+DAAgE,CAAC;AAE3G,KAAK,0BAA0B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;sCAQrD,UAAU,cAAc;AANhF;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,sBAA2C,YAAW,OAAO;IAShG,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IAZtB,OAAO,CAAC,GAAG,CAA+C;IAG1D,OAAO,CAAC,wBAAwB,CAAuC;IAEvE,OAAO,CAAC,SAAS,CAA6B;gBAGpC,aAAa,EAAE,yBAAyB,EACxC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAChD,YAAY,EAAE,qBAAqB,EAC3C,SAAS,EAAE,0BAA0B;IAShC,KAAK;IAKL,IAAI;IAKJ,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAKzD,OAAO,CAAC,mBAAmB;IA+Cd,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD,aAAa,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAgClF,qBAAqB;IASnC,OAAO,CAAC,wBAAwB;CAgBjC"}
1
+ {"version":3,"file":"epoch_prune_watcher.d.ts","sourceRoot":"","sources":["../../src/watchers/epoch_prune_watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAEL,OAAO,EAEP,KAAK,yBAAyB,EAE/B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,qBAAqB,EACrB,WAAW,EACX,yBAAyB,EACzB,aAAa,EACd,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAWnE,OAAO,EAA6C,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE7G,QAAA,MAAM,oCAAoC,+DAAgE,CAAC;AAE3G,KAAK,0BAA0B,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;sCAQrD,UAAU,cAAc;AANhF;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,sBAA2C,YAAW,OAAO;IAShG,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IAZtB,OAAO,CAAC,GAAG,CAA+C;IAG1D,OAAO,CAAC,wBAAwB,CAAuC;IAEvE,OAAO,CAAC,SAAS,CAA6B;gBAGpC,aAAa,EAAE,yBAAyB,EACxC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAChD,YAAY,EAAE,qBAAqB,EAC3C,SAAS,EAAE,0BAA0B;IAShC,KAAK;IAKL,IAAI;IAKJ,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAKzD,OAAO,CAAC,mBAAmB;YAOb,iBAAiB;YAWjB,oBAAoB;IAwBrB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAchD,aAAa,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAgClF,qBAAqB;IASnC,OAAO,CAAC,wBAAwB;CAgBjC"}
@@ -1,7 +1,8 @@
1
1
  import { merge, pick } from '@aztec/foundation/collection';
2
2
  import { createLogger } from '@aztec/foundation/log';
3
3
  import { L2BlockSourceEvents } from '@aztec/stdlib/block';
4
- import { OffenseType } from '@aztec/stdlib/slashing';
4
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
5
+ import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
5
6
  import { ReExFailedTxsError, ReExStateMismatchError, TransactionsNotAvailableError, ValidatorError } from '@aztec/stdlib/validators';
6
7
  import EventEmitter from 'node:events';
7
8
  import { WANT_TO_SLASH_EVENT } from '../watcher.js';
@@ -43,44 +44,40 @@ const EpochPruneWatcherPenaltiesConfigKeys = [
43
44
  }
44
45
  handlePruneL2Blocks(event) {
45
46
  const { blocks, epochNumber } = event;
46
- this.log.info(`Detected chain prune. Validating epoch ${epochNumber}`);
47
- this.validateBlocks(blocks).then(async ()=>{
47
+ void this.processPruneL2Blocks(blocks, epochNumber).catch((err)=>this.log.error('Error processing pruned L2 blocks', err, {
48
+ epochNumber
49
+ }));
50
+ }
51
+ async emitSlashForEpoch(offense, epochNumber) {
52
+ const validators = await this.getValidatorsForEpoch(epochNumber);
53
+ if (validators.length === 0) {
54
+ this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
55
+ return;
56
+ }
57
+ const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
58
+ this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
59
+ this.emit(WANT_TO_SLASH_EVENT, args);
60
+ }
61
+ async processPruneL2Blocks(blocks, epochNumber) {
62
+ try {
63
+ const l1Constants = this.epochCache.getL1Constants();
64
+ const epochBlocks = blocks.filter((b)=>getEpochAtSlot(b.slot, l1Constants) === epochNumber);
65
+ this.log.info(`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, {
66
+ blocks: epochBlocks.map((b)=>b.toBlockInfo())
67
+ });
68
+ await this.validateBlocks(epochBlocks);
48
69
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
49
- const validators = await this.getValidatorsForEpoch(epochNumber);
50
- // need to specify return type to be able to return offense as undefined later on
51
- const result = {
52
- validators,
53
- offense: OffenseType.VALID_EPOCH_PRUNED
54
- };
55
- return result;
56
- }).catch(async (error)=>{
70
+ await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
71
+ } catch (error) {
57
72
  if (error instanceof TransactionsNotAvailableError) {
58
73
  this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
59
74
  message: error.message
60
75
  });
61
- const validators = await this.getValidatorsForEpoch(epochNumber);
62
- return {
63
- validators,
64
- offense: OffenseType.DATA_WITHHOLDING
65
- };
76
+ await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
66
77
  } else {
67
78
  this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
68
- return {
69
- validators: [],
70
- offense: undefined
71
- };
72
79
  }
73
- }).then(({ validators, offense })=>{
74
- if (validators.length === 0 || offense === undefined) {
75
- return;
76
- }
77
- const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
78
- this.log.info(`Slash for epoch ${epochNumber} created`, args);
79
- this.emit(WANT_TO_SLASH_EVENT, args);
80
- }).catch((error)=>{
81
- // This can happen if we fail to get the validators for the epoch.
82
- this.log.error('Error while creating slash for epoch', error);
83
- });
80
+ }
84
81
  }
85
82
  async validateBlocks(blocks) {
86
83
  if (blocks.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/slasher",
3
- "version": "2.0.3-rc.17",
3
+ "version": "2.0.3-rc.18",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -54,20 +54,20 @@
54
54
  ]
55
55
  },
56
56
  "dependencies": {
57
- "@aztec/epoch-cache": "2.0.3-rc.17",
58
- "@aztec/ethereum": "2.0.3-rc.17",
59
- "@aztec/foundation": "2.0.3-rc.17",
60
- "@aztec/kv-store": "2.0.3-rc.17",
61
- "@aztec/l1-artifacts": "2.0.3-rc.17",
62
- "@aztec/stdlib": "2.0.3-rc.17",
63
- "@aztec/telemetry-client": "2.0.3-rc.17",
57
+ "@aztec/epoch-cache": "2.0.3-rc.18",
58
+ "@aztec/ethereum": "2.0.3-rc.18",
59
+ "@aztec/foundation": "2.0.3-rc.18",
60
+ "@aztec/kv-store": "2.0.3-rc.18",
61
+ "@aztec/l1-artifacts": "2.0.3-rc.18",
62
+ "@aztec/stdlib": "2.0.3-rc.18",
63
+ "@aztec/telemetry-client": "2.0.3-rc.18",
64
64
  "source-map-support": "^0.5.21",
65
65
  "tslib": "^2.4.0",
66
66
  "viem": "2.23.7",
67
67
  "zod": "^3.23.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@aztec/aztec.js": "2.0.3-rc.17",
70
+ "@aztec/aztec.js": "2.0.3-rc.18",
71
71
  "@jest/globals": "^30.0.0",
72
72
  "@types/jest": "^30.0.0",
73
73
  "@types/node": "^22.15.17",
package/src/config.ts CHANGED
@@ -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,6 +1,7 @@
1
1
  import { EthAddress } from '@aztec/aztec.js';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
3
  import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
4
+ import { maxBigint } from '@aztec/foundation/bigint';
4
5
  import { compactArray, partition, times } from '@aztec/foundation/collection';
5
6
  import { createLogger } from '@aztec/foundation/log';
6
7
  import { sleep } from '@aztec/foundation/sleep';
@@ -45,7 +46,7 @@ export type TallySlasherSettings = Prettify<
45
46
  >;
46
47
 
47
48
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
48
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
49
+ Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
50
 
50
51
  /**
51
52
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -177,26 +178,62 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
177
178
  return compactArray<ProposerSlashAction>([executeAction, voteAction]);
178
179
  }
179
180
 
180
- /** Returns an execute slash action if there are any rounds ready to be executed */
181
+ /**
182
+ * Returns an execute slash action if there are any rounds ready to be executed.
183
+ * Returns the oldest slash action if there are multiple rounds pending execution.
184
+ */
181
185
  protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
182
186
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
183
187
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
184
188
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
185
- if (executableRound < 0n) {
189
+ const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
190
+ const oldestExecutableRound = maxBigint(0n, executableRound - lookBack);
191
+
192
+ // Check if slashing is enabled at all
193
+ if (!(await this.slasher.isSlashingEnabled())) {
194
+ this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
186
195
  return undefined;
187
196
  }
188
197
 
189
- let logData: Record<string, unknown> = { currentRound, executableRound, slotNumber };
198
+ this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
199
+ slotNumber,
200
+ currentRound,
201
+ oldestExecutableRound,
202
+ executableRound,
203
+ slashingExecutionDelayInRounds,
204
+ lookBack,
205
+ slashingLifetimeInRounds: this.settings.slashingLifetimeInRounds,
206
+ });
207
+
208
+ // Iterate over all rounds, starting from the oldest, until we find one that is executable
209
+ for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
210
+ const action = await this.tryGetRoundExecuteAction(roundToCheck);
211
+ if (action) {
212
+ return action;
213
+ }
214
+ }
215
+
216
+ // And return nothing if none are found
217
+ return undefined;
218
+ }
219
+
220
+ /**
221
+ * Checks if a given round is executable and returns an execute-slash action for it if so.
222
+ * Assumes round number has already been checked against lifetime and execution delay.
223
+ */
224
+ private async tryGetRoundExecuteAction(executableRound: bigint): Promise<ProposerSlashAction | undefined> {
225
+ let logData: Record<string, unknown> = { executableRound };
226
+ this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
190
227
 
191
228
  try {
229
+ // Note we do not check isReadyToExecute here, since we already know that based on the
230
+ // executableRound number. Not just that, but it may be that we are building for the given slot number
231
+ // that is in the future, so the contract may think it's not yet ready to execute, whereas it is.
192
232
  const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
193
233
  logData = { ...logData, roundInfo };
194
234
  if (roundInfo.isExecuted) {
195
235
  this.log.verbose(`Round ${executableRound} has already been executed`, logData);
196
236
  return undefined;
197
- } else if (!roundInfo.readyToExecute) {
198
- this.log.verbose(`Round ${executableRound} is not ready to execute yet`, logData);
199
- return undefined;
200
237
  } else if (roundInfo.voteCount === 0n) {
201
238
  this.log.debug(`Round ${executableRound} received no votes`, logData);
202
239
  return undefined;
@@ -205,6 +242,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
205
242
  return undefined;
206
243
  }
207
244
 
245
+ // Check if the round yields any slashing at all
208
246
  const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
209
247
  if (slashActions.length === 0) {
210
248
  this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
@@ -239,9 +277,8 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
239
277
  return { type: 'execute-slash', round: executableRound, committees: slashedCommittees };
240
278
  } catch (error) {
241
279
  this.log.error(`Error checking round to execute ${executableRound}`, error);
280
+ return undefined;
242
281
  }
243
-
244
- return undefined;
245
282
  }
246
283
 
247
284
  /** Returns a vote action based on offenses from the target round (with offset applied) */
@@ -1,4 +1,3 @@
1
- import type { Tx } from '@aztec/aztec.js';
2
1
  import { EpochCache } from '@aztec/epoch-cache';
3
2
  import { merge, pick } from '@aztec/foundation/collection';
4
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -9,6 +8,7 @@ import {
9
8
  type L2BlockSourceEventEmitter,
10
9
  L2BlockSourceEvents,
11
10
  } from '@aztec/stdlib/block';
11
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
12
12
  import type {
13
13
  IFullNodeBlockBuilder,
14
14
  ITxProvider,
@@ -16,7 +16,7 @@ import type {
16
16
  SlasherConfig,
17
17
  } from '@aztec/stdlib/interfaces/server';
18
18
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
19
- import { OffenseType } from '@aztec/stdlib/slashing';
19
+ import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
20
20
  import {
21
21
  ReExFailedTxsError,
22
22
  ReExStateMismatchError,
@@ -78,49 +78,44 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
78
78
 
79
79
  private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
80
80
  const { blocks, epochNumber } = event;
81
- this.log.info(`Detected chain prune. Validating epoch ${epochNumber}`);
82
-
83
- this.validateBlocks(blocks)
84
- .then(async () => {
85
- this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
86
- const validators = await this.getValidatorsForEpoch(epochNumber);
87
- // need to specify return type to be able to return offense as undefined later on
88
- const result: { validators: EthAddress[]; offense: OffenseType | undefined } = {
89
- validators,
90
- offense: OffenseType.VALID_EPOCH_PRUNED,
91
- };
92
- return result;
93
- })
94
- .catch(async error => {
95
- if (error instanceof TransactionsNotAvailableError) {
96
- this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
97
- message: error.message,
98
- });
99
- const validators = await this.getValidatorsForEpoch(epochNumber);
100
- return {
101
- validators,
102
- offense: OffenseType.DATA_WITHHOLDING,
103
- };
104
- } else {
105
- this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
106
- return {
107
- validators: [],
108
- offense: undefined,
109
- };
110
- }
111
- })
112
- .then(({ validators, offense }) => {
113
- if (validators.length === 0 || offense === undefined) {
114
- return;
115
- }
116
- const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
117
- this.log.info(`Slash for epoch ${epochNumber} created`, args);
118
- this.emit(WANT_TO_SLASH_EVENT, args);
119
- })
120
- .catch(error => {
121
- // This can happen if we fail to get the validators for the epoch.
122
- this.log.error('Error while creating slash for epoch', error);
123
- });
81
+ void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
82
+ this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
83
+ );
84
+ }
85
+
86
+ private async emitSlashForEpoch(offense: OffenseType, epochNumber: bigint): Promise<void> {
87
+ const validators = await this.getValidatorsForEpoch(epochNumber);
88
+ if (validators.length === 0) {
89
+ this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
90
+ return;
91
+ }
92
+ const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
93
+ this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
94
+ this.emit(WANT_TO_SLASH_EVENT, args);
95
+ }
96
+
97
+ private async processPruneL2Blocks(blocks: L2Block[], epochNumber: bigint): Promise<void> {
98
+ try {
99
+ const l1Constants = this.epochCache.getL1Constants();
100
+ const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber);
101
+ this.log.info(
102
+ `Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
103
+ { blocks: epochBlocks.map(b => b.toBlockInfo()) },
104
+ );
105
+
106
+ await this.validateBlocks(epochBlocks);
107
+ this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
108
+ await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
109
+ } catch (error) {
110
+ if (error instanceof TransactionsNotAvailableError) {
111
+ this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
112
+ message: error.message,
113
+ });
114
+ await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
115
+ } else {
116
+ this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
117
+ }
118
+ }
124
119
  }
125
120
 
126
121
  public async validateBlocks(blocks: L2Block[]): Promise<void> {
@@ -151,7 +146,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
151
146
 
152
147
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockFromL1.number);
153
148
  const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
154
- txs as Tx[],
149
+ txs,
155
150
  l1ToL2Messages,
156
151
  blockFromL1.header.globalVariables,
157
152
  {},