@aztec/slasher 0.0.1-commit.f295ac2 → 0.0.1-commit.f504929

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 (49) 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 +13 -1
  5. package/dest/empire_slasher_client.d.ts +1 -1
  6. package/dest/empire_slasher_client.d.ts.map +1 -1
  7. package/dest/empire_slasher_client.js +0 -8
  8. package/dest/factory/create_facade.d.ts +2 -2
  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 +3 -2
  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 +2 -2
  15. package/dest/factory/get_settings.d.ts.map +1 -1
  16. package/dest/generated/slasher-defaults.d.ts +4 -2
  17. package/dest/generated/slasher-defaults.d.ts.map +1 -1
  18. package/dest/generated/slasher-defaults.js +3 -1
  19. package/dest/slash_offenses_collector.d.ts +5 -2
  20. package/dest/slash_offenses_collector.d.ts.map +1 -1
  21. package/dest/slash_offenses_collector.js +2 -2
  22. package/dest/slasher_client_facade.d.ts +3 -2
  23. package/dest/slasher_client_facade.d.ts.map +1 -1
  24. package/dest/slasher_client_facade.js +4 -2
  25. package/dest/stores/offenses_store.d.ts +1 -1
  26. package/dest/stores/offenses_store.d.ts.map +1 -1
  27. package/dest/stores/offenses_store.js +4 -2
  28. package/dest/stores/payloads_store.d.ts +1 -1
  29. package/dest/stores/payloads_store.d.ts.map +1 -1
  30. package/dest/stores/payloads_store.js +6 -3
  31. package/dest/tally_slasher_client.d.ts +2 -2
  32. package/dest/tally_slasher_client.d.ts.map +1 -1
  33. package/dest/tally_slasher_client.js +17 -5
  34. package/dest/watchers/epoch_prune_watcher.d.ts +7 -6
  35. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
  36. package/dest/watchers/epoch_prune_watcher.js +43 -25
  37. package/package.json +9 -9
  38. package/src/config.ts +15 -1
  39. package/src/empire_slasher_client.ts +0 -8
  40. package/src/factory/create_facade.ts +32 -3
  41. package/src/factory/create_implementation.ts +28 -3
  42. package/src/factory/get_settings.ts +2 -2
  43. package/src/generated/slasher-defaults.ts +3 -1
  44. package/src/slash_offenses_collector.ts +8 -3
  45. package/src/slasher_client_facade.ts +2 -0
  46. package/src/stores/offenses_store.ts +4 -2
  47. package/src/stores/payloads_store.ts +7 -4
  48. package/src/tally_slasher_client.ts +18 -7
  49. package/src/watchers/epoch_prune_watcher.ts +60 -27
@@ -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> {
@@ -5,7 +5,6 @@ import { maxBigint } from '@aztec/foundation/bigint';
5
5
  import { SlotNumber } from '@aztec/foundation/branded-types';
6
6
  import { compactArray, partition, times } from '@aztec/foundation/collection';
7
7
  import { createLogger } from '@aztec/foundation/log';
8
- import { sleep } from '@aztec/foundation/sleep';
9
8
  import type { DateProvider } from '@aztec/foundation/timer';
10
9
  import type { Prettify } from '@aztec/foundation/types';
11
10
  import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
@@ -47,7 +46,10 @@ export type TallySlasherSettings = Prettify<
47
46
  >;
48
47
 
49
48
  export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
50
- Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
49
+ Pick<
50
+ SlasherConfig,
51
+ 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'
52
+ >;
51
53
 
52
54
  /**
53
55
  * The Tally Slasher client is responsible for managing slashable offenses using
@@ -138,8 +140,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
138
140
  this.roundMonitor.stop();
139
141
  await this.offensesCollector.stop();
140
142
 
141
- // Sleeping to sidestep viem issue with unwatching events
142
- await sleep(2000);
143
143
  this.log.info('Tally Slasher client stopped');
144
144
  }
145
145
 
@@ -281,8 +281,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
281
281
  return undefined;
282
282
  }
283
283
 
284
+ const slashActionsWithAmounts = slashActions.map(action => ({
285
+ validator: action.validator.toString(),
286
+ slashAmount: action.slashAmount.toString(),
287
+ }));
284
288
  this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
285
- slashActions,
289
+ slashActions: slashActionsWithAmounts,
286
290
  payloadAddress: payload.address.toString(),
287
291
  ...logData,
288
292
  });
@@ -357,12 +361,19 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
357
361
 
358
362
  const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
359
363
  const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
360
- const votes = getSlashConsensusVotesFromOffenses(
364
+ const { slashMaxPayloadSize } = this.config;
365
+ const { votes, truncatedCount } = getSlashConsensusVotesFromOffenses(
361
366
  offensesToSlash,
362
367
  committees,
363
368
  epochsForCommittees.map(e => BigInt(e)),
364
- this.settings,
369
+ { ...this.settings, maxSlashedValidators: slashMaxPayloadSize },
365
370
  );
371
+ if (truncatedCount > 0) {
372
+ this.log.warn(
373
+ `Vote truncated: ${truncatedCount} validator-epoch pairs dropped to stay within gas limit of ${slashMaxPayloadSize}`,
374
+ { slotNumber, currentRound, slashedRound },
375
+ );
376
+ }
366
377
  if (votes.every(v => v === 0)) {
367
378
  this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
368
379
  slotNumber,
@@ -1,17 +1,18 @@
1
1
  import { EpochCache } from '@aztec/epoch-cache';
2
- import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
- 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
4
  import type { Fr } from '@aztec/foundation/curves/bn254';
5
5
  import { type Logger, createLogger } from '@aztec/foundation/log';
6
6
  import {
7
7
  EthAddress,
8
- L2BlockNew,
8
+ L2Block,
9
9
  type L2BlockSourceEventEmitter,
10
10
  L2BlockSourceEvents,
11
11
  type L2PruneUnprovenEvent,
12
12
  } from '@aztec/stdlib/block';
13
13
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
14
14
  import type {
15
+ ICheckpointBlockBuilder,
15
16
  ICheckpointsBuilder,
16
17
  ITxProvider,
17
18
  MerkleTreeWriteOperations,
@@ -97,7 +98,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
97
98
  this.emit(WANT_TO_SLASH_EVENT, args);
98
99
  }
99
100
 
100
- private async processPruneL2Blocks(blocks: L2BlockNew[], epochNumber: EpochNumber): Promise<void> {
101
+ private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
101
102
  try {
102
103
  const l1Constants = this.epochCache.getL1Constants();
103
104
  const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
@@ -106,7 +107,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
106
107
  { blocks: epochBlocks.map(b => b.toBlockInfo()) },
107
108
  );
108
109
 
109
- await this.validateBlocks(epochBlocks);
110
+ await this.validateBlocks(epochBlocks, epochNumber);
110
111
  this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
111
112
  await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
112
113
  } catch (error) {
@@ -121,19 +122,32 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
121
122
  }
122
123
  }
123
124
 
124
- public async validateBlocks(blocks: L2BlockNew[]): Promise<void> {
125
+ public async validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
125
126
  if (blocks.length === 0) {
126
127
  return;
127
128
  }
128
129
 
129
- let previousCheckpointOutHashes: Fr[] = [];
130
- const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
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
+ );
131
143
  try {
132
- for (const block of blocks) {
133
- await this.validateBlock(block, previousCheckpointOutHashes, fork);
144
+ for (const checkpointBlocks of blocksByCheckpoint) {
145
+ await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
134
146
 
135
- // TODO(mbps): This assumes one block per checkpoint, which is only true for now.
136
- const checkpointOutHash = computeCheckpointOutHash([block.body.txEffects.map(tx => tx.l2ToL1Msgs)]);
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
+ );
137
151
  previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
138
152
  }
139
153
  } finally {
@@ -141,43 +155,62 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
141
155
  }
142
156
  }
143
157
 
144
- public async validateBlock(
145
- blockFromL1: L2BlockNew,
158
+ private async validateCheckpoint(
159
+ checkpointBlocks: L2Block[],
146
160
  previousCheckpointOutHashes: Fr[],
147
161
  fork: MerkleTreeWriteOperations,
148
162
  ): Promise<void> {
149
- this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
150
- const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
151
- // We load txs from the mempool directly, since the TxCollector running in the background has already been
152
- // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
153
- // it's likely that they are not available in the network at all.
154
- const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
155
-
156
- if (missingTxs && missingTxs.length > 0) {
157
- throw new TransactionsNotAvailableError(missingTxs);
158
- }
163
+ const checkpointNumber = checkpointBlocks[0].checkpointNumber;
164
+ this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
159
165
 
160
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
166
+ // Get L1ToL2Messages once for the entire checkpoint
161
167
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
162
- const gv = blockFromL1.header.globalVariables;
168
+
169
+ // Build checkpoint constants from first block's global variables
170
+ const gv = checkpointBlocks[0].header.globalVariables;
163
171
  const constants: CheckpointGlobalVariables = {
164
172
  chainId: gv.chainId,
165
173
  version: gv.version,
166
174
  slotNumber: gv.slotNumber,
175
+ timestamp: gv.timestamp,
167
176
  coinbase: gv.coinbase,
168
177
  feeRecipient: gv.feeRecipient,
169
178
  gasFees: gv.gasFees,
170
179
  };
171
180
 
172
- // Use checkpoint builder to validate the block
181
+ // Start checkpoint builder once for all blocks in this checkpoint
173
182
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
174
183
  checkpointNumber,
175
184
  constants,
185
+ 0n, // feeAssetPriceModifier is not used for validation of the checkpoint content
176
186
  l1ToL2Messages,
177
187
  previousCheckpointOutHashes,
178
188
  fork,
189
+ this.log.getBindings(),
179
190
  );
180
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> {
202
+ this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
203
+ const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
204
+ // We load txs from the mempool directly, since the TxCollector running in the background has already been
205
+ // trying to fetch them from nodes or via reqresp. If we haven't managed to collect them by now,
206
+ // it's likely that they are not available in the network at all.
207
+ const { txs, missingTxs } = await this.txProvider.getAvailableTxs(txHashes);
208
+
209
+ if (missingTxs && missingTxs.length > 0) {
210
+ throw new TransactionsNotAvailableError(missingTxs);
211
+ }
212
+
213
+ const gv = blockFromL1.header.globalVariables;
181
214
  const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
182
215
 
183
216
  if (numTxs !== txs.length) {