@aztec/slasher 0.0.1-commit.b655e406 → 0.0.1-commit.b6e433891

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 (70) hide show
  1. package/README.md +24 -14
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +29 -17
  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 +5 -4
  9. package/dest/factory/create_facade.d.ts.map +1 -1
  10. package/dest/factory/create_facade.js +26 -3
  11. package/dest/factory/create_implementation.d.ts +5 -4
  12. package/dest/factory/create_implementation.d.ts.map +1 -1
  13. package/dest/factory/create_implementation.js +11 -7
  14. package/dest/factory/get_settings.d.ts +3 -3
  15. package/dest/factory/get_settings.d.ts.map +1 -1
  16. package/dest/factory/index.d.ts +1 -1
  17. package/dest/generated/slasher-defaults.d.ts +21 -0
  18. package/dest/generated/slasher-defaults.d.ts.map +1 -0
  19. package/dest/generated/slasher-defaults.js +21 -0
  20. package/dest/index.d.ts +1 -1
  21. package/dest/null_slasher_client.d.ts +3 -2
  22. package/dest/null_slasher_client.d.ts.map +1 -1
  23. package/dest/slash_offenses_collector.d.ts +5 -2
  24. package/dest/slash_offenses_collector.d.ts.map +1 -1
  25. package/dest/slash_offenses_collector.js +2 -2
  26. package/dest/slash_round_monitor.d.ts +5 -4
  27. package/dest/slash_round_monitor.d.ts.map +1 -1
  28. package/dest/slasher_client_facade.d.ts +7 -5
  29. package/dest/slasher_client_facade.d.ts.map +1 -1
  30. package/dest/slasher_client_facade.js +4 -2
  31. package/dest/slasher_client_interface.d.ts +3 -2
  32. package/dest/slasher_client_interface.d.ts.map +1 -1
  33. package/dest/stores/offenses_store.d.ts +1 -1
  34. package/dest/stores/offenses_store.d.ts.map +1 -1
  35. package/dest/stores/offenses_store.js +4 -2
  36. package/dest/stores/payloads_store.d.ts +2 -2
  37. package/dest/stores/payloads_store.d.ts.map +1 -1
  38. package/dest/stores/payloads_store.js +6 -3
  39. package/dest/stores/schema_version.d.ts +1 -1
  40. package/dest/tally_slasher_client.d.ts +6 -10
  41. package/dest/tally_slasher_client.d.ts.map +1 -1
  42. package/dest/tally_slasher_client.js +10 -5
  43. package/dest/test/dummy_watcher.d.ts +1 -1
  44. package/dest/test/dummy_watcher.d.ts.map +1 -1
  45. package/dest/watcher.d.ts +1 -1
  46. package/dest/watcher.d.ts.map +1 -1
  47. package/dest/watchers/attestations_block_watcher.d.ts +7 -6
  48. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  49. package/dest/watchers/attestations_block_watcher.js +42 -35
  50. package/dest/watchers/epoch_prune_watcher.d.ts +9 -7
  51. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  52. package/dest/watchers/epoch_prune_watcher.js +61 -17
  53. package/package.json +17 -14
  54. package/src/config.ts +32 -17
  55. package/src/empire_slasher_client.ts +6 -13
  56. package/src/factory/create_facade.ts +35 -5
  57. package/src/factory/create_implementation.ts +30 -5
  58. package/src/factory/get_settings.ts +3 -3
  59. package/src/generated/slasher-defaults.ts +23 -0
  60. package/src/null_slasher_client.ts +2 -1
  61. package/src/slash_offenses_collector.ts +8 -3
  62. package/src/slash_round_monitor.ts +3 -2
  63. package/src/slasher_client_facade.ts +6 -3
  64. package/src/slasher_client_interface.ts +2 -1
  65. package/src/stores/offenses_store.ts +4 -2
  66. package/src/stores/payloads_store.ts +7 -4
  67. package/src/tally_slasher_client.ts +22 -10
  68. package/src/watcher.ts +1 -1
  69. package/src/watchers/attestations_block_watcher.ts +59 -45
  70. package/src/watchers/epoch_prune_watcher.ts +93 -29
@@ -1,12 +1,13 @@
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';
7
- import type { DataStoreConfig } from '@aztec/kv-store/config';
8
8
  import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
9
9
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
10
+ import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
10
11
  import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
11
12
 
12
13
  import { createSlasherImplementation } from './factory/create_implementation.js';
@@ -31,6 +32,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
31
32
  private epochCache: EpochCache,
32
33
  private dateProvider: DateProvider,
33
34
  private kvStore: AztecLMDBStoreV2,
35
+ private rollupRegisteredAtL2Slot: SlotNumber,
34
36
  private logger = createLogger('slasher'),
35
37
  ) {}
36
38
 
@@ -73,7 +75,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
73
75
  return this.client?.getPendingOffenses() ?? Promise.reject(new Error('Slasher client not initialized'));
74
76
  }
75
77
 
76
- public getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
78
+ public getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
77
79
  return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
78
80
  }
79
81
 
@@ -87,6 +89,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
87
89
  this.epochCache,
88
90
  this.dateProvider,
89
91
  this.kvStore,
92
+ this.rollupRegisteredAtL2Slot,
90
93
  this.logger,
91
94
  );
92
95
  }
@@ -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;
@@ -76,9 +76,11 @@ export class SlasherOffensesStore {
76
76
  /** Adds a new offense (defaults to pending, but will be slashed if markAsSlashed had been called for it) */
77
77
  public async addPendingOffense(offense: Offense): Promise<void> {
78
78
  const key = this.getOffenseKey(offense);
79
- await this.offenses.set(key, serializeOffense(offense));
80
79
  const round = getRoundForOffense(offense, this.settings);
81
- await this.roundsOffenses.set(this.getRoundKey(round), key);
80
+ await this.kvStore.transactionAsync(async () => {
81
+ await this.offenses.set(key, serializeOffense(offense));
82
+ await this.roundsOffenses.set(this.getRoundKey(round), key);
83
+ });
82
84
  this.log.trace(`Adding pending offense ${key} for round ${round}`);
83
85
  }
84
86
 
@@ -118,10 +118,13 @@ export class SlasherPayloadsStore {
118
118
 
119
119
  public async incrementPayloadVotes(payloadAddress: EthAddress, round: bigint): Promise<bigint> {
120
120
  const key = this.getPayloadVotesKey(round, payloadAddress);
121
- const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
122
- const newVotes = currentVotes + 1n;
123
- await this.roundPayloadVotes.set(key, newVotes);
124
- return newVotes;
121
+ let newVotes: bigint;
122
+ await this.kvStore.transactionAsync(async () => {
123
+ const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
124
+ newVotes = currentVotes + 1n;
125
+ await this.roundPayloadVotes.set(key, newVotes);
126
+ });
127
+ return newVotes!;
125
128
  }
126
129
 
127
130
  public async addPayload(payload: SlashPayloadRound): Promise<void> {
@@ -2,9 +2,9 @@ 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';
@@ -46,7 +46,10 @@ export type TallySlasherSettings = Prettify<
46
46
  >;
47
47
 
48
48
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
49
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
+ Pick<
50
+ SlasherConfig,
51
+ 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'
52
+ >;
50
53
 
51
54
  /**
52
55
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -137,8 +140,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
137
140
  this.roundMonitor.stop();
138
141
  await this.offensesCollector.stop();
139
142
 
140
- // Sleeping to sidestep viem issue with unwatching events
141
- await sleep(2000);
142
143
  this.log.info('Tally Slasher client stopped');
143
144
  }
144
145
 
@@ -169,7 +170,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
169
170
  * @param slotNumber - The current slot number
170
171
  * @returns The actions to take
171
172
  */
172
- public async getProposerActions(slotNumber: bigint): Promise<ProposerSlashAction[]> {
173
+ public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
173
174
  const [executeAction, voteAction] = await Promise.all([
174
175
  this.getExecuteSlashAction(slotNumber),
175
176
  this.getVoteOffensesAction(slotNumber),
@@ -182,7 +183,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
182
183
  * Returns an execute slash action if there are any rounds ready to be executed.
183
184
  * Returns the oldest slash action if there are multiple rounds pending execution.
184
185
  */
185
- protected async getExecuteSlashAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
186
+ protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
186
187
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
187
188
  const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
188
189
  const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
@@ -233,7 +234,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
233
234
  */
234
235
  private async tryGetRoundExecuteAction(
235
236
  executableRound: bigint,
236
- slotNumber: bigint,
237
+ slotNumber: SlotNumber,
237
238
  ): Promise<ProposerSlashAction | undefined> {
238
239
  let logData: Record<string, unknown> = { executableRound, slotNumber };
239
240
  this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
@@ -280,8 +281,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
280
281
  return undefined;
281
282
  }
282
283
 
284
+ const slashActionsWithAmounts = slashActions.map(action => ({
285
+ validator: action.validator.toString(),
286
+ slashAmount: action.slashAmount.toString(),
287
+ }));
283
288
  this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
284
- slashActions,
289
+ slashActions: slashActionsWithAmounts,
285
290
  payloadAddress: payload.address.toString(),
286
291
  ...logData,
287
292
  });
@@ -302,7 +307,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
302
307
  }
303
308
 
304
309
  /** Returns a vote action based on offenses from the target round (with offset applied) */
305
- protected async getVoteOffensesAction(slotNumber: bigint): Promise<ProposerSlashAction | undefined> {
310
+ protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
306
311
  // Compute what round we are in based on the slot number and what round will be slashed
307
312
  const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
308
313
  const slashedRound = this.getSlashedRound(currentRound);
@@ -356,7 +361,14 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
356
361
 
357
362
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
358
363
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
359
- const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees, this.settings);
364
+ const { slashMaxPayloadSize } = this.config;
365
+ const votes = getSlashConsensusVotesFromOffenses(
366
+ offensesToSlash,
367
+ committees,
368
+ epochsForCommittees.map(e => BigInt(e)),
369
+ { ...this.settings, maxSlashedValidators: slashMaxPayloadSize },
370
+ this.log,
371
+ );
360
372
  if (votes.every(v => v === 0)) {
361
373
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
362
374
  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,12 @@ 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
+ isBuildingProposal: false,
216
+ minValidTxs: 0,
217
+ });
218
+
155
219
  if (numTxs !== txs.length) {
156
220
  // This should be detected by state mismatch, but this makes it easier to debug.
157
221
  throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
@@ -164,7 +228,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
164
228
  }
165
229
  }
166
230
 
167
- private async getValidatorsForEpoch(epochNumber: bigint): Promise<EthAddress[]> {
231
+ private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
168
232
  const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
169
233
  if (!committee) {
170
234
  this.log.trace(`No committee found for epoch ${epochNumber}`);
@@ -176,7 +240,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
176
240
  private validatorsToSlashingArgs(
177
241
  validators: EthAddress[],
178
242
  offenseType: OffenseType,
179
- epochOrSlot: bigint,
243
+ epochOrSlot: EpochNumber,
180
244
  ): WantToSlashArgs[] {
181
245
  const penalty =
182
246
  offenseType === OffenseType.DATA_WITHHOLDING
@@ -186,7 +250,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
186
250
  validator: v,
187
251
  amount: penalty,
188
252
  offenseType,
189
- epochOrSlot,
253
+ epochOrSlot: BigInt(epochOrSlot),
190
254
  }));
191
255
  }
192
256
  }