@aztec/slasher 0.0.1-fake-ceab37513c → 0.0.6-commit.a2d1860fe9

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 (66) hide show
  1. package/README.md +73 -14
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +28 -16
  5. package/dest/empire_slasher_client.d.ts +6 -5
  6. package/dest/empire_slasher_client.d.ts.map +1 -1
  7. package/dest/empire_slasher_client.js +1 -9
  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_facade.js +1 -1
  11. package/dest/factory/create_implementation.d.ts +3 -3
  12. package/dest/factory/create_implementation.d.ts.map +1 -1
  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/generated/slasher-defaults.d.ts +21 -0
  17. package/dest/generated/slasher-defaults.d.ts.map +1 -0
  18. package/dest/generated/slasher-defaults.js +21 -0
  19. package/dest/index.d.ts +1 -1
  20. package/dest/null_slasher_client.d.ts +3 -2
  21. package/dest/null_slasher_client.d.ts.map +1 -1
  22. package/dest/slash_offenses_collector.d.ts +1 -1
  23. package/dest/slash_offenses_collector.d.ts.map +1 -1
  24. package/dest/slash_round_monitor.d.ts +5 -4
  25. package/dest/slash_round_monitor.d.ts.map +1 -1
  26. package/dest/slasher_client_facade.d.ts +4 -3
  27. package/dest/slasher_client_facade.d.ts.map +1 -1
  28. package/dest/slasher_client_interface.d.ts +3 -2
  29. package/dest/slasher_client_interface.d.ts.map +1 -1
  30. package/dest/stores/offenses_store.d.ts +1 -1
  31. package/dest/stores/offenses_store.d.ts.map +1 -1
  32. package/dest/stores/offenses_store.js +5 -3
  33. package/dest/stores/payloads_store.d.ts +2 -2
  34. package/dest/stores/payloads_store.d.ts.map +1 -1
  35. package/dest/stores/payloads_store.js +6 -3
  36. package/dest/stores/schema_version.d.ts +1 -1
  37. package/dest/tally_slasher_client.d.ts +6 -10
  38. package/dest/tally_slasher_client.d.ts.map +1 -1
  39. package/dest/tally_slasher_client.js +22 -15
  40. package/dest/test/dummy_watcher.d.ts +1 -1
  41. package/dest/test/dummy_watcher.d.ts.map +1 -1
  42. package/dest/watcher.d.ts +1 -1
  43. package/dest/watcher.d.ts.map +1 -1
  44. package/dest/watchers/attestations_block_watcher.d.ts +7 -6
  45. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  46. package/dest/watchers/attestations_block_watcher.js +42 -35
  47. package/dest/watchers/epoch_prune_watcher.d.ts +9 -7
  48. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  49. package/dest/watchers/epoch_prune_watcher.js +58 -17
  50. package/package.json +17 -14
  51. package/src/config.ts +30 -16
  52. package/src/empire_slasher_client.ts +6 -13
  53. package/src/factory/create_facade.ts +3 -2
  54. package/src/factory/create_implementation.ts +1 -1
  55. package/src/factory/get_settings.ts +1 -1
  56. package/src/generated/slasher-defaults.ts +23 -0
  57. package/src/null_slasher_client.ts +2 -1
  58. package/src/slash_round_monitor.ts +3 -2
  59. package/src/slasher_client_facade.ts +3 -2
  60. package/src/slasher_client_interface.ts +2 -1
  61. package/src/stores/offenses_store.ts +5 -3
  62. package/src/stores/payloads_store.ts +7 -4
  63. package/src/tally_slasher_client.ts +37 -19
  64. package/src/watcher.ts +1 -1
  65. package/src/watchers/attestations_block_watcher.ts +59 -45
  66. package/src/watchers/epoch_prune_watcher.ts +90 -29
@@ -1,10 +1,10 @@
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
3
  import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
4
4
  import { maxBigint } from '@aztec/foundation/bigint';
5
+ import { SlotNumber } from '@aztec/foundation/branded-types';
5
6
  import { compactArray, partition, times } from '@aztec/foundation/collection';
6
7
  import { createLogger } from '@aztec/foundation/log';
7
- import { sleep } from '@aztec/foundation/sleep';
8
8
  import type { DateProvider } from '@aztec/foundation/timer';
9
9
  import type { Prettify } from '@aztec/foundation/types';
10
10
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
@@ -137,8 +137,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
137
137
  this.roundMonitor.stop();
138
138
  await this.offensesCollector.stop();
139
139
 
140
- // Sleeping to sidestep viem issue with unwatching events
141
- await sleep(2000);
142
140
  this.log.info('Tally Slasher client stopped');
143
141
  }
144
142
 
@@ -169,7 +167,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
169
167
  * @param slotNumber - The current slot number
170
168
  * @returns The actions to take
171
169
  */
172
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
170
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
173
171
  const [executeAction, voteAction] = await Promise.all([
174
172
  this.getExecuteSlashAction(slotNumber),
175
173
  this.getVoteOffensesAction(slotNumber),
@@ -182,7 +180,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
182
180
  * Returns an execute slash action if there are any rounds ready to be executed.
183
181
  * Returns the oldest slash action if there are multiple rounds pending execution.
184
182
  */
185
- protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
183
+ protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
186
184
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
187
185
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
188
186
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
@@ -216,7 +214,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
216
214
 
217
215
  // Iterate over all rounds, starting from the oldest, until we find one that is executable
218
216
  for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
219
- const action = await this.tryGetRoundExecuteAction(roundToCheck);
217
+ const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
220
218
  if (action) {
221
219
  return action;
222
220
  }
@@ -231,17 +229,14 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
231
229
  * Assumes round number has already been checked against lifetime and execution delay.
232
230
  * @param executableRound - The round to check for execution
233
231
  */
234
- private async tryGetRoundExecuteAction(executableRound: bigint): Promise<ProposerSlashAction | undefined> {
235
- let logData: Record<string, unknown> = { executableRound };
232
+ private async tryGetRoundExecuteAction(
233
+ executableRound: bigint,
234
+ slotNumber: SlotNumber,
235
+ ): Promise<ProposerSlashAction | undefined> {
236
+ let logData: Record<string, unknown> = { executableRound, slotNumber };
236
237
  this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
237
238
 
238
239
  try {
239
- // Check if slashing is enabled at all
240
- if (!(await this.slasher.isSlashingEnabled())) {
241
- this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`, logData);
242
- return undefined;
243
- }
244
-
245
240
  const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
246
241
  logData = { ...logData, roundInfo };
247
242
  if (roundInfo.isExecuted) {
@@ -255,6 +250,16 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
255
250
  return undefined;
256
251
  }
257
252
 
253
+ // Check if round is ready to execute at the given slot
254
+ const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
255
+ if (!isReadyToExecute) {
256
+ this.log.warn(
257
+ `Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`,
258
+ logData,
259
+ );
260
+ return undefined;
261
+ }
262
+
258
263
  // Check if the round yields any slashing at all
259
264
  const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
260
265
  if (slashActions.length === 0) {
@@ -273,8 +278,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
273
278
  return undefined;
274
279
  }
275
280
 
281
+ const slashActionsWithAmounts = slashActions.map(action => ({
282
+ validator: action.validator.toString(),
283
+ slashAmount: action.slashAmount.toString(),
284
+ }));
276
285
  this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
277
- slashActions,
286
+ slashActions: slashActionsWithAmounts,
278
287
  payloadAddress: payload.address.toString(),
279
288
  ...logData,
280
289
  });
@@ -295,7 +304,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
295
304
  }
296
305
 
297
306
  /** Returns a vote action based on offenses from the target round (with offset applied) */
298
- protected async getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
307
+ protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
299
308
  // Compute what round we are in based on the slot number and what round will be slashed
300
309
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
301
310
  const slashedRound = this.getSlashedRound(currentRound);
@@ -340,16 +349,25 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
340
349
  return undefined;
341
350
  }
342
351
 
352
+ const offensesToSlashLog = offensesToSlash.map(offense => ({
353
+ ...offense,
354
+ amount: offense.amount.toString(),
355
+ }));
343
356
  this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
344
357
  slotNumber,
345
358
  currentRound,
346
359
  slashedRound,
347
- offensesToSlash,
360
+ offensesToSlash: offensesToSlashLog,
348
361
  });
349
362
 
350
363
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
351
364
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
352
- const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees, this.settings);
365
+ const votes = getSlashConsensusVotesFromOffenses(
366
+ offensesToSlash,
367
+ committees,
368
+ epochsForCommittees.map(e => BigInt(e)),
369
+ this.settings,
370
+ );
353
371
  if (votes.every(v => v === 0)) {
354
372
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
355
373
  slotNumber,
package/src/watcher.ts CHANGED
@@ -10,7 +10,7 @@ export interface WantToSlashArgs {
10
10
  validator: EthAddress;
11
11
  amount: bigint;
12
12
  offenseType: OffenseType;
13
- epochOrSlot: bigint; // Epoch number for epoch-based offenses, block number for block-based
13
+ epochOrSlot: bigint; // Epoch number for epoch-based offenses, slot number for slot-based
14
14
  }
15
15
 
16
16
  // Event map for specific, known events of a watcher
@@ -1,13 +1,14 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
+ import { SlotNumber } from '@aztec/foundation/branded-types';
2
3
  import { merge, pick } from '@aztec/foundation/collection';
3
4
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
5
  import {
5
- type InvalidBlockDetectedEvent,
6
- type L2BlockInfo,
6
+ type InvalidCheckpointDetectedEvent,
7
7
  type L2BlockSourceEventEmitter,
8
8
  L2BlockSourceEvents,
9
- type ValidateBlockNegativeResult,
9
+ type ValidateCheckpointNegativeResult,
10
10
  } from '@aztec/stdlib/block';
11
+ import type { CheckpointInfo } from '@aztec/stdlib/checkpoint';
11
12
  import { OffenseType } from '@aztec/stdlib/slashing';
12
13
 
13
14
  import EventEmitter from 'node:events';
@@ -31,19 +32,19 @@ type AttestationsBlockWatcherConfig = Pick<SlasherConfig, (typeof AttestationsBl
31
32
  export class AttestationsBlockWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
32
33
  private log: Logger = createLogger('attestations-block-watcher');
33
34
 
34
- // Only keep track of the last N invalid blocks
35
- private maxInvalidBlocks = 100;
35
+ // Only keep track of the last N invalid checkpoints
36
+ private maxInvalidCheckpoints = 100;
36
37
 
37
38
  // All invalid archive roots seen
38
39
  private invalidArchiveRoots: Set<string> = new Set();
39
40
 
40
41
  private config: AttestationsBlockWatcherConfig;
41
42
 
42
- private boundHandleInvalidBlock = (event: InvalidBlockDetectedEvent) => {
43
+ private boundHandleInvalidCheckpoint = (event: InvalidCheckpointDetectedEvent) => {
43
44
  try {
44
- this.handleInvalidBlock(event);
45
+ this.handleInvalidCheckpoint(event);
45
46
  } catch (err) {
46
- this.log.error('Error handling invalid block', err, {
47
+ this.log.error('Error handling invalid checkpoint', err, {
47
48
  ...event.validationResult,
48
49
  reason: event.validationResult.reason,
49
50
  });
@@ -66,54 +67,61 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
66
67
  }
67
68
 
68
69
  public start() {
69
- this.l2BlockSource.on(L2BlockSourceEvents.InvalidAttestationsBlockDetected, this.boundHandleInvalidBlock);
70
+ this.l2BlockSource.events.on(
71
+ L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
72
+ this.boundHandleInvalidCheckpoint,
73
+ );
70
74
  return Promise.resolve();
71
75
  }
72
76
 
73
77
  public stop() {
74
- this.l2BlockSource.removeListener(
75
- L2BlockSourceEvents.InvalidAttestationsBlockDetected,
76
- this.boundHandleInvalidBlock,
78
+ this.l2BlockSource.events.removeListener(
79
+ L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
80
+ this.boundHandleInvalidCheckpoint,
77
81
  );
78
82
  return Promise.resolve();
79
83
  }
80
84
 
81
- private handleInvalidBlock(event: InvalidBlockDetectedEvent): void {
85
+ /** Event handler for invalid checkpoints as reported by the archiver. Public for testing purposes. */
86
+ public handleInvalidCheckpoint(event: InvalidCheckpointDetectedEvent): void {
82
87
  const { validationResult } = event;
83
- const block = validationResult.block;
88
+ const checkpoint = validationResult.checkpoint;
84
89
 
85
- // Check if we already have processed this block, archiver may emit the same event multiple times
86
- if (this.invalidArchiveRoots.has(block.archive.toString())) {
87
- this.log.trace(`Already processed invalid block ${block.blockNumber}`);
90
+ // Check if we already have processed this checkpoint, archiver may emit the same event multiple times
91
+ if (this.invalidArchiveRoots.has(checkpoint.archive.toString())) {
92
+ this.log.trace(`Already processed invalid checkpoint ${checkpoint.checkpointNumber}`);
88
93
  return;
89
94
  }
90
95
 
91
- this.log.verbose(`Detected invalid block ${block.blockNumber}`, {
92
- ...block,
96
+ this.log.verbose(`Detected invalid checkpoint ${checkpoint.checkpointNumber}`, {
97
+ ...checkpoint,
93
98
  reason: validationResult.valid === false ? validationResult.reason : 'unknown',
94
99
  });
95
100
 
96
- // Store the invalid block
97
- this.addInvalidBlock(event.validationResult.block);
101
+ // Store the invalid checkpoint
102
+ this.addInvalidCheckpoint(event.validationResult.checkpoint);
98
103
 
99
- // Slash the proposer of the invalid block
104
+ // Slash the proposer of the invalid checkpoint
100
105
  this.slashProposer(event.validationResult);
101
106
 
102
- // Check if the parent of this block is invalid as well, if so, we will slash its attestors as well
107
+ // Check if the parent of this checkpoint is invalid as well, if so, we will slash its attestors as well
103
108
  this.slashAttestorsOnAncestorInvalid(event.validationResult);
104
109
  }
105
110
 
106
- private slashAttestorsOnAncestorInvalid(validationResult: ValidateBlockNegativeResult) {
107
- const block = validationResult.block;
111
+ private slashAttestorsOnAncestorInvalid(validationResult: ValidateCheckpointNegativeResult) {
112
+ const checkpoint = validationResult.checkpoint;
108
113
 
109
- const parentArchive = block.lastArchive.toString();
114
+ const parentArchive = checkpoint.lastArchive.toString();
110
115
  if (this.invalidArchiveRoots.has(parentArchive)) {
111
116
  const attestors = validationResult.attestors;
112
- this.log.info(`Want to slash attestors of block ${block.blockNumber} built on invalid block`, {
113
- ...block,
114
- ...attestors,
115
- parentArchive,
116
- });
117
+ this.log.info(
118
+ `Want to slash attestors of checkpoint ${checkpoint.checkpointNumber} built on invalid checkpoint`,
119
+ {
120
+ ...checkpoint,
121
+ ...attestors,
122
+ parentArchive,
123
+ },
124
+ );
117
125
 
118
126
  this.emit(
119
127
  WANT_TO_SLASH_EVENT,
@@ -121,20 +129,26 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
121
129
  validator: attestor,
122
130
  amount: this.config.slashAttestDescendantOfInvalidPenalty,
123
131
  offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
124
- epochOrSlot: BigInt(block.slotNumber),
132
+ epochOrSlot: BigInt(SlotNumber(checkpoint.slotNumber)),
125
133
  })),
126
134
  );
127
135
  }
128
136
  }
129
137
 
130
- private slashProposer(validationResult: ValidateBlockNegativeResult) {
131
- const { reason, block } = validationResult;
132
- const blockNumber = block.blockNumber;
133
- const slot = BigInt(block.slotNumber);
134
- const proposer = this.epochCache.getProposerFromEpochCommittee(validationResult, slot);
138
+ private slashProposer(validationResult: ValidateCheckpointNegativeResult) {
139
+ const { reason, checkpoint } = validationResult;
140
+ const checkpointNumber = checkpoint.checkpointNumber;
141
+ const slot = checkpoint.slotNumber;
142
+ const epochCommitteeInfo = {
143
+ committee: validationResult.committee,
144
+ seed: validationResult.seed,
145
+ epoch: validationResult.epoch,
146
+ isEscapeHatchOpen: false,
147
+ };
148
+ const proposer = this.epochCache.getProposerFromEpochCommittee(epochCommitteeInfo, slot);
135
149
 
136
150
  if (!proposer) {
137
- this.log.warn(`No proposer found for block ${blockNumber} at slot ${slot}`);
151
+ this.log.warn(`No proposer found for checkpoint ${checkpointNumber} at slot ${slot}`);
138
152
  return;
139
153
  }
140
154
 
@@ -144,18 +158,18 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
144
158
  validator: proposer,
145
159
  amount,
146
160
  offenseType: offense,
147
- epochOrSlot: slot,
161
+ epochOrSlot: BigInt(slot),
148
162
  };
149
163
 
150
- this.log.info(`Want to slash proposer of block ${blockNumber} due to ${reason}`, {
151
- ...block,
164
+ this.log.info(`Want to slash proposer of checkpoint ${checkpointNumber} due to ${reason}`, {
165
+ ...checkpoint,
152
166
  ...args,
153
167
  });
154
168
 
155
169
  this.emit(WANT_TO_SLASH_EVENT, [args]);
156
170
  }
157
171
 
158
- private getOffenseFromInvalidationReason(reason: ValidateBlockNegativeResult['reason']): OffenseType {
172
+ private getOffenseFromInvalidationReason(reason: ValidateCheckpointNegativeResult['reason']): OffenseType {
159
173
  switch (reason) {
160
174
  case 'invalid-attestation':
161
175
  return OffenseType.PROPOSED_INCORRECT_ATTESTATIONS;
@@ -168,11 +182,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
168
182
  }
169
183
  }
170
184
 
171
- private addInvalidBlock(block: L2BlockInfo) {
172
- this.invalidArchiveRoots.add(block.archive.toString());
185
+ private addInvalidCheckpoint(checkpoint: CheckpointInfo) {
186
+ this.invalidArchiveRoots.add(checkpoint.archive.toString());
173
187
 
174
188
  // Prune old entries if we exceed the maximum
175
- if (this.invalidArchiveRoots.size > this.maxInvalidBlocks) {
189
+ if (this.invalidArchiveRoots.size > this.maxInvalidCheckpoints) {
176
190
  const oldestKey = this.invalidArchiveRoots.keys().next().value!;
177
191
  this.invalidArchiveRoots.delete(oldestKey);
178
192
  }
@@ -1,22 +1,26 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import { merge, pick } from '@aztec/foundation/collection';
2
+ import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
+ import { chunkBy, merge, pick } from '@aztec/foundation/collection';
4
+ import type { Fr } from '@aztec/foundation/curves/bn254';
3
5
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
6
  import {
5
7
  EthAddress,
6
8
  L2Block,
7
- type L2BlockPruneEvent,
8
9
  type L2BlockSourceEventEmitter,
9
10
  L2BlockSourceEvents,
11
+ type L2PruneUnprovenEvent,
10
12
  } from '@aztec/stdlib/block';
11
13
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
12
14
  import type {
13
- IFullNodeBlockBuilder,
15
+ ICheckpointBlockBuilder,
16
+ ICheckpointsBuilder,
14
17
  ITxProvider,
15
18
  MerkleTreeWriteOperations,
16
19
  SlasherConfig,
17
20
  } from '@aztec/stdlib/interfaces/server';
18
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
21
+ import { type L1ToL2MessageSource, computeCheckpointOutHash } from '@aztec/stdlib/messaging';
19
22
  import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
23
+ import type { CheckpointGlobalVariables } from '@aztec/stdlib/tx';
20
24
  import {
21
25
  ReExFailedTxsError,
22
26
  ReExStateMismatchError,
@@ -51,7 +55,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
51
55
  private l1ToL2MessageSource: L1ToL2MessageSource,
52
56
  private epochCache: EpochCache,
53
57
  private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
54
- private blockBuilder: IFullNodeBlockBuilder,
58
+ private checkpointsBuilder: ICheckpointsBuilder,
55
59
  penalties: EpochPruneWatcherPenalties,
56
60
  ) {
57
61
  super();
@@ -62,12 +66,12 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
62
66
  }
63
67
 
64
68
  public start() {
65
- this.l2BlockSource.on(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
69
+ this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
66
70
  return Promise.resolve();
67
71
  }
68
72
 
69
73
  public stop() {
70
- this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.boundHandlePruneL2Blocks);
74
+ this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
71
75
  return Promise.resolve();
72
76
  }
73
77
 
@@ -76,34 +80,34 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
76
80
  this.log.verbose('EpochPruneWatcher config updated', this.penalties);
77
81
  }
78
82
 
79
- private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
83
+ private handlePruneL2Blocks(event: L2PruneUnprovenEvent): void {
80
84
  const { blocks, epochNumber } = event;
81
85
  void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
82
86
  this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
83
87
  );
84
88
  }
85
89
 
86
- private async emitSlashForEpoch(offense: OffenseType, epochNumber: bigint): Promise<void> {
90
+ private async emitSlashForEpoch(offense: OffenseType, epochNumber: EpochNumber): Promise<void> {
87
91
  const validators = await this.getValidatorsForEpoch(epochNumber);
88
92
  if (validators.length === 0) {
89
93
  this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
90
94
  return;
91
95
  }
92
- const args = this.validatorsToSlashingArgs(validators, offense, BigInt(epochNumber));
96
+ const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
93
97
  this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
94
98
  this.emit(WANT_TO_SLASH_EVENT, args);
95
99
  }
96
100
 
97
- private async processPruneL2Blocks(blocks: L2Block[], epochNumber: bigint): Promise<void> {
101
+ private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
98
102
  try {
99
103
  const l1Constants = this.epochCache.getL1Constants();
100
- const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber);
104
+ const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
101
105
  this.log.info(
102
106
  `Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
103
107
  { blocks: epochBlocks.map(b => b.toBlockInfo()) },
104
108
  );
105
109
 
106
- await this.validateBlocks(epochBlocks);
110
+ await this.validateBlocks(epochBlocks, epochNumber);
107
111
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
108
112
  await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
109
113
  } catch (error) {
@@ -118,21 +122,83 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
118
122
  }
119
123
  }
120
124
 
121
- public async validateBlocks(blocks: L2Block[]): Promise<void> {
125
+ public async validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
122
126
  if (blocks.length === 0) {
123
127
  return;
124
128
  }
125
- const fork = await this.blockBuilder.getFork(blocks[0].header.globalVariables.blockNumber - 1);
129
+
130
+ // Sort blocks by block number and group by checkpoint
131
+ const sortedBlocks = [...blocks].sort((a, b) => a.number - b.number);
132
+ const blocksByCheckpoint = chunkBy(sortedBlocks, b => b.checkpointNumber);
133
+
134
+ // Get prior checkpoints in the epoch (in case this was a partial prune) to extract the out hashes
135
+ const priorCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(epochNumber))
136
+ .filter(c => c.checkpointNumber < sortedBlocks[0].checkpointNumber)
137
+ .map(c => c.checkpointOutHash);
138
+ let previousCheckpointOutHashes: Fr[] = [...priorCheckpointOutHashes];
139
+
140
+ const fork = await this.checkpointsBuilder.getFork(
141
+ BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1),
142
+ );
126
143
  try {
127
- for (const block of blocks) {
128
- await this.validateBlock(block, fork);
144
+ for (const checkpointBlocks of blocksByCheckpoint) {
145
+ await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
146
+
147
+ // Compute checkpoint out hash from all blocks in this checkpoint
148
+ const checkpointOutHash = computeCheckpointOutHash(
149
+ checkpointBlocks.map(b => b.body.txEffects.map(tx => tx.l2ToL1Msgs)),
150
+ );
151
+ previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
129
152
  }
130
153
  } finally {
131
154
  await fork.close();
132
155
  }
133
156
  }
134
157
 
135
- public async validateBlock(blockFromL1: L2Block, fork: MerkleTreeWriteOperations): Promise<void> {
158
+ private async validateCheckpoint(
159
+ checkpointBlocks: L2Block[],
160
+ previousCheckpointOutHashes: Fr[],
161
+ fork: MerkleTreeWriteOperations,
162
+ ): Promise<void> {
163
+ const checkpointNumber = checkpointBlocks[0].checkpointNumber;
164
+ this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
165
+
166
+ // Get L1ToL2Messages once for the entire checkpoint
167
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
168
+
169
+ // Build checkpoint constants from first block's global variables
170
+ const gv = checkpointBlocks[0].header.globalVariables;
171
+ const constants: CheckpointGlobalVariables = {
172
+ chainId: gv.chainId,
173
+ version: gv.version,
174
+ slotNumber: gv.slotNumber,
175
+ timestamp: gv.timestamp,
176
+ coinbase: gv.coinbase,
177
+ feeRecipient: gv.feeRecipient,
178
+ gasFees: gv.gasFees,
179
+ };
180
+
181
+ // Start checkpoint builder once for all blocks in this checkpoint
182
+ const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
183
+ checkpointNumber,
184
+ constants,
185
+ 0n, // feeAssetPriceModifier is not used for validation of the checkpoint content
186
+ l1ToL2Messages,
187
+ previousCheckpointOutHashes,
188
+ fork,
189
+ this.log.getBindings(),
190
+ );
191
+
192
+ // Validate all blocks in the checkpoint sequentially
193
+ for (const block of checkpointBlocks) {
194
+ await this.validateBlockInCheckpoint(block, checkpointBuilder);
195
+ }
196
+ }
197
+
198
+ private async validateBlockInCheckpoint(
199
+ blockFromL1: L2Block,
200
+ checkpointBuilder: ICheckpointBlockBuilder,
201
+ ): Promise<void> {
136
202
  this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
137
203
  const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
138
204
  // We load txs from the mempool directly, since the TxCollector running in the background has already been
@@ -144,14 +210,9 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
144
210
  throw new TransactionsNotAvailableError(missingTxs);
145
211
  }
146
212
 
147
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockFromL1.number);
148
- const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
149
- txs,
150
- l1ToL2Messages,
151
- blockFromL1.header.globalVariables,
152
- {},
153
- fork,
154
- );
213
+ const gv = blockFromL1.header.globalVariables;
214
+ const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
215
+
155
216
  if (numTxs !== txs.length) {
156
217
  // This should be detected by state mismatch, but this makes it easier to debug.
157
218
  throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
@@ -164,7 +225,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
164
225
  }
165
226
  }
166
227
 
167
- private async getValidatorsForEpoch(epochNumber: bigint): Promise<EthAddress[]> {
228
+ private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
168
229
  const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
169
230
  if (!committee) {
170
231
  this.log.trace(`No committee found for epoch ${epochNumber}`);
@@ -176,7 +237,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
176
237
  private validatorsToSlashingArgs(
177
238
  validators: EthAddress[],
178
239
  offenseType: OffenseType,
179
- epochOrSlot: bigint,
240
+ epochOrSlot: EpochNumber,
180
241
  ): WantToSlashArgs[] {
181
242
  const penalty =
182
243
  offenseType === OffenseType.DATA_WITHHOLDING
@@ -186,7 +247,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
186
247
  validator: v,
187
248
  amount: penalty,
188
249
  offenseType,
189
- epochOrSlot,
250
+ epochOrSlot: BigInt(epochOrSlot),
190
251
  }));
191
252
  }
192
253
  }