@aztec/slasher 0.0.1-commit.e3c1de76 → 0.0.1-commit.e57c76e

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 (92) hide show
  1. package/README.md +83 -76
  2. package/dest/config.d.ts +1 -1
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +41 -29
  5. package/dest/factory/create_facade.d.ts +3 -3
  6. package/dest/factory/create_facade.d.ts.map +1 -1
  7. package/dest/factory/create_facade.js +25 -2
  8. package/dest/factory/create_implementation.d.ts +6 -7
  9. package/dest/factory/create_implementation.d.ts.map +1 -1
  10. package/dest/factory/create_implementation.js +8 -56
  11. package/dest/factory/get_settings.d.ts +4 -4
  12. package/dest/factory/get_settings.d.ts.map +1 -1
  13. package/dest/factory/get_settings.js +3 -3
  14. package/dest/factory/index.d.ts +2 -2
  15. package/dest/factory/index.d.ts.map +1 -1
  16. package/dest/factory/index.js +1 -1
  17. package/dest/generated/slasher-defaults.d.ts +8 -6
  18. package/dest/generated/slasher-defaults.d.ts.map +1 -1
  19. package/dest/generated/slasher-defaults.js +7 -5
  20. package/dest/index.d.ts +6 -4
  21. package/dest/index.d.ts.map +1 -1
  22. package/dest/index.js +5 -3
  23. package/dest/null_slasher_client.d.ts +3 -4
  24. package/dest/null_slasher_client.d.ts.map +1 -1
  25. package/dest/null_slasher_client.js +1 -4
  26. package/dest/slash_offenses_collector.d.ts +10 -9
  27. package/dest/slash_offenses_collector.d.ts.map +1 -1
  28. package/dest/slash_offenses_collector.js +50 -34
  29. package/dest/slasher_client.d.ts +112 -0
  30. package/dest/slasher_client.d.ts.map +1 -0
  31. package/dest/{tally_slasher_client.js → slasher_client.js} +45 -45
  32. package/dest/slasher_client_facade.d.ts +6 -8
  33. package/dest/slasher_client_facade.d.ts.map +1 -1
  34. package/dest/slasher_client_facade.js +6 -9
  35. package/dest/slasher_client_interface.d.ts +7 -21
  36. package/dest/slasher_client_interface.d.ts.map +1 -1
  37. package/dest/slasher_client_interface.js +1 -4
  38. package/dest/stores/offenses_store.d.ts +12 -12
  39. package/dest/stores/offenses_store.d.ts.map +1 -1
  40. package/dest/stores/offenses_store.js +61 -38
  41. package/dest/watcher.d.ts +8 -1
  42. package/dest/watcher.d.ts.map +1 -1
  43. package/dest/watcher.js +1 -0
  44. package/dest/watchers/attestations_block_watcher.d.ts +26 -13
  45. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
  46. package/dest/watchers/attestations_block_watcher.js +76 -61
  47. package/dest/watchers/attested_invalid_proposal_watcher.d.ts +42 -0
  48. package/dest/watchers/attested_invalid_proposal_watcher.d.ts.map +1 -0
  49. package/dest/watchers/attested_invalid_proposal_watcher.js +117 -0
  50. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts +38 -0
  51. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts.map +1 -0
  52. package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.js +138 -0
  53. package/dest/watchers/checkpoint_equivocation_watcher.d.ts +30 -0
  54. package/dest/watchers/checkpoint_equivocation_watcher.d.ts.map +1 -0
  55. package/dest/watchers/checkpoint_equivocation_watcher.js +69 -0
  56. package/dest/watchers/data_withholding_watcher.d.ts +63 -0
  57. package/dest/watchers/data_withholding_watcher.d.ts.map +1 -0
  58. package/dest/watchers/data_withholding_watcher.js +193 -0
  59. package/package.json +10 -10
  60. package/src/config.ts +48 -29
  61. package/src/factory/create_facade.ts +32 -4
  62. package/src/factory/create_implementation.ts +24 -105
  63. package/src/factory/get_settings.ts +8 -8
  64. package/src/factory/index.ts +1 -1
  65. package/src/generated/slasher-defaults.ts +7 -5
  66. package/src/index.ts +5 -3
  67. package/src/null_slasher_client.ts +2 -6
  68. package/src/slash_offenses_collector.ts +70 -36
  69. package/src/{tally_slasher_client.ts → slasher_client.ts} +63 -54
  70. package/src/slasher_client_facade.ts +6 -11
  71. package/src/slasher_client_interface.ts +6 -21
  72. package/src/stores/offenses_store.ts +73 -47
  73. package/src/watcher.ts +8 -0
  74. package/src/watchers/attestations_block_watcher.ts +88 -82
  75. package/src/watchers/attested_invalid_proposal_watcher.ts +168 -0
  76. package/src/watchers/broadcasted_invalid_checkpoint_proposal_watcher.ts +192 -0
  77. package/src/watchers/checkpoint_equivocation_watcher.ts +96 -0
  78. package/src/watchers/data_withholding_watcher.ts +225 -0
  79. package/dest/empire_slasher_client.d.ts +0 -190
  80. package/dest/empire_slasher_client.d.ts.map +0 -1
  81. package/dest/empire_slasher_client.js +0 -564
  82. package/dest/stores/payloads_store.d.ts +0 -29
  83. package/dest/stores/payloads_store.d.ts.map +0 -1
  84. package/dest/stores/payloads_store.js +0 -128
  85. package/dest/tally_slasher_client.d.ts +0 -125
  86. package/dest/tally_slasher_client.d.ts.map +0 -1
  87. package/dest/watchers/epoch_prune_watcher.d.ts +0 -38
  88. package/dest/watchers/epoch_prune_watcher.d.ts.map +0 -1
  89. package/dest/watchers/epoch_prune_watcher.js +0 -158
  90. package/src/empire_slasher_client.ts +0 -649
  91. package/src/stores/payloads_store.ts +0 -149
  92. package/src/watchers/epoch_prune_watcher.ts +0 -221
@@ -1,149 +0,0 @@
1
- import { EthAddress } from '@aztec/foundation/eth-address';
2
- import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
3
- import {
4
- type SlashPayload,
5
- type SlashPayloadRound,
6
- deserializeSlashPayload,
7
- serializeSlashPayload,
8
- } from '@aztec/stdlib/slashing';
9
-
10
- export class SlasherPayloadsStore {
11
- /** Map from payload address to payload data */
12
- private payloads: AztecAsyncMap<string, Buffer>;
13
-
14
- /** Map from `round:payload` to votes */
15
- private roundPayloadVotes: AztecAsyncMap<string, bigint>;
16
-
17
- constructor(
18
- private kvStore: AztecAsyncKVStore,
19
- private settings?: {
20
- slashingPayloadLifetimeInRounds?: number;
21
- },
22
- ) {
23
- this.payloads = kvStore.openMap('slash-payloads');
24
- this.roundPayloadVotes = kvStore.openMap('round-payload-votes');
25
- }
26
-
27
- public async getPayloadsForRound(round: bigint): Promise<SlashPayloadRound[]> {
28
- const payloads: SlashPayloadRound[] = [];
29
- const votes = await this.getVotesForRound(round);
30
- for (const [address, votesCount] of votes) {
31
- const payload = await this.getPayload(address);
32
- if (payload) {
33
- payloads.push({ ...payload, votes: votesCount, round });
34
- }
35
- }
36
- return payloads;
37
- }
38
-
39
- public async getPayloadAtRound(payloadAddress: EthAddress, round: bigint): Promise<SlashPayloadRound | undefined> {
40
- const address = payloadAddress.toString();
41
- const buffer = await this.payloads.getAsync(address);
42
- if (!buffer) {
43
- return undefined;
44
- }
45
-
46
- const data = deserializeSlashPayload(buffer);
47
- const votes = (await this.roundPayloadVotes.getAsync(this.getPayloadVotesKey(round, address))) ?? 0n;
48
-
49
- return { ...data, votes, round };
50
- }
51
-
52
- private async getVotesForRound(round: bigint): Promise<[string, bigint][]> {
53
- const votes: [string, bigint][] = [];
54
- for await (const [fullKey, roundVotes] of this.roundPayloadVotes.entriesAsync(
55
- this.getPayloadVotesKeyRangeForRound(round),
56
- )) {
57
- // Extract just the address part from the key (remove "round:" prefix)
58
- const address = fullKey.split(':')[1];
59
- votes.push([address, roundVotes]);
60
- }
61
- return votes;
62
- }
63
-
64
- private getRoundKey(round: bigint): string {
65
- return round.toString().padStart(16, '0');
66
- }
67
-
68
- private getPayloadVotesKey(round: bigint, payloadAddress: EthAddress | string): string {
69
- return `${this.getRoundKey(round)}:${payloadAddress.toString()}`;
70
- }
71
-
72
- private getPayloadVotesKeyRangeForRound(round: bigint): { start: string; end: string } {
73
- const start = `${this.getRoundKey(round)}:`;
74
- const end = `${this.getRoundKey(round)}:Z`; // 'Z' sorts after any hex address, 0x-prefixed or not
75
- return { start, end };
76
- }
77
-
78
- /**
79
- * Purge vote payload data for expired rounds. Does not delete actual payload data.
80
- */
81
- public async clearExpiredPayloads(currentRound: bigint): Promise<void> {
82
- const lifetimeInRounds = this.settings?.slashingPayloadLifetimeInRounds ?? 0;
83
- if (lifetimeInRounds <= 0) {
84
- return; // No lifetime configured
85
- }
86
-
87
- const expiredBefore = currentRound - BigInt(lifetimeInRounds);
88
- if (expiredBefore < 0) {
89
- return; // Not enough rounds have passed to expire anything
90
- }
91
-
92
- // Collect expired payload votes by scanning round-payload keys
93
- const expiredPayloads: string[] = [];
94
- const expiredVoteKeys: string[] = [];
95
-
96
- for await (const key of this.roundPayloadVotes.keysAsync({
97
- end: `${this.getRoundKey(expiredBefore)}:Z`,
98
- })) {
99
- const [roundStr, payloadAddress] = key.split(':');
100
- if (BigInt(roundStr) <= expiredBefore) {
101
- expiredVoteKeys.push(key);
102
- expiredPayloads.push(payloadAddress);
103
- }
104
- }
105
-
106
- if (expiredVoteKeys.length === 0) {
107
- return; // No expired payloads to clean up
108
- }
109
-
110
- // Remove expired payload vote records
111
- // Note that we do not delete payload data since these could be repurposed in future votes
112
- await this.kvStore.transactionAsync(async () => {
113
- for (const key of expiredVoteKeys) {
114
- await this.roundPayloadVotes.delete(key);
115
- }
116
- });
117
- }
118
-
119
- public async incrementPayloadVotes(payloadAddress: EthAddress, round: bigint): Promise<bigint> {
120
- const key = this.getPayloadVotesKey(round, payloadAddress);
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!;
128
- }
129
-
130
- public async addPayload(payload: SlashPayloadRound): Promise<void> {
131
- const address = payload.address.toString();
132
-
133
- await this.kvStore.transactionAsync(async () => {
134
- await this.payloads.set(address, serializeSlashPayload(payload));
135
- await this.roundPayloadVotes.set(this.getPayloadVotesKey(payload.round, address), payload.votes);
136
- });
137
- }
138
-
139
- public async getPayload(payloadAddress: EthAddress | string): Promise<SlashPayload | undefined> {
140
- const address = payloadAddress.toString();
141
- const buffer = await this.payloads.getAsync(address);
142
- return buffer ? deserializeSlashPayload(buffer) : undefined;
143
- }
144
-
145
- public async hasPayload(payload: EthAddress): Promise<boolean> {
146
- const address = payload.toString();
147
- return (await this.payloads.getAsync(address)) !== undefined;
148
- }
149
- }
@@ -1,221 +0,0 @@
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';
4
- import type { Fr } from '@aztec/foundation/curves/bn254';
5
- import { type Logger, createLogger } from '@aztec/foundation/log';
6
- import {
7
- EthAddress,
8
- L2Block,
9
- type L2BlockSourceEventEmitter,
10
- L2BlockSourceEvents,
11
- type L2PruneUnprovenEvent,
12
- } from '@aztec/stdlib/block';
13
- import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
14
- import type {
15
- ICheckpointsBuilder,
16
- ITxProvider,
17
- MerkleTreeWriteOperations,
18
- SlasherConfig,
19
- } from '@aztec/stdlib/interfaces/server';
20
- import { type L1ToL2MessageSource, computeCheckpointOutHash } from '@aztec/stdlib/messaging';
21
- import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
22
- import type { CheckpointGlobalVariables } from '@aztec/stdlib/tx';
23
- import {
24
- ReExFailedTxsError,
25
- ReExStateMismatchError,
26
- TransactionsNotAvailableError,
27
- ValidatorError,
28
- } from '@aztec/stdlib/validators';
29
-
30
- import EventEmitter from 'node:events';
31
-
32
- import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
33
-
34
- const EpochPruneWatcherPenaltiesConfigKeys = ['slashPrunePenalty', 'slashDataWithholdingPenalty'] as const;
35
-
36
- type EpochPruneWatcherPenalties = Pick<SlasherConfig, (typeof EpochPruneWatcherPenaltiesConfigKeys)[number]>;
37
-
38
- /**
39
- * This watcher is responsible for detecting chain prunes and creating slashing arguments for the committee.
40
- * It only wants to slash if:
41
- * - the transactions are not available
42
- * - OR the archive roots match when re-building all the blocks in the epoch (i.e. the epoch *could* have been proven)
43
- */
44
- export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
45
- private log: Logger = createLogger('epoch-prune-watcher');
46
-
47
- // Store bound function reference for proper listener removal
48
- private boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
49
-
50
- private penalties: EpochPruneWatcherPenalties;
51
-
52
- constructor(
53
- private l2BlockSource: L2BlockSourceEventEmitter,
54
- private l1ToL2MessageSource: L1ToL2MessageSource,
55
- private epochCache: EpochCache,
56
- private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
57
- private checkpointsBuilder: ICheckpointsBuilder,
58
- penalties: EpochPruneWatcherPenalties,
59
- ) {
60
- super();
61
- this.penalties = pick(penalties, ...EpochPruneWatcherPenaltiesConfigKeys);
62
- this.log.verbose(
63
- `EpochPruneWatcher initialized with penalties: valid epoch pruned=${penalties.slashPrunePenalty} data withholding=${penalties.slashDataWithholdingPenalty}`,
64
- );
65
- }
66
-
67
- public start() {
68
- this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
69
- return Promise.resolve();
70
- }
71
-
72
- public stop() {
73
- this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
74
- return Promise.resolve();
75
- }
76
-
77
- public updateConfig(config: Partial<SlasherConfig>): void {
78
- this.penalties = merge(this.penalties, pick(config, ...EpochPruneWatcherPenaltiesConfigKeys));
79
- this.log.verbose('EpochPruneWatcher config updated', this.penalties);
80
- }
81
-
82
- private handlePruneL2Blocks(event: L2PruneUnprovenEvent): void {
83
- const { blocks, epochNumber } = event;
84
- void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
85
- this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
86
- );
87
- }
88
-
89
- private async emitSlashForEpoch(offense: OffenseType, epochNumber: EpochNumber): Promise<void> {
90
- const validators = await this.getValidatorsForEpoch(epochNumber);
91
- if (validators.length === 0) {
92
- this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
93
- return;
94
- }
95
- const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
96
- this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
97
- this.emit(WANT_TO_SLASH_EVENT, args);
98
- }
99
-
100
- private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
101
- try {
102
- const l1Constants = this.epochCache.getL1Constants();
103
- const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
104
- this.log.info(
105
- `Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
106
- { blocks: epochBlocks.map(b => b.toBlockInfo()) },
107
- );
108
-
109
- await this.validateBlocks(epochBlocks);
110
- this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
111
- await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
112
- } catch (error) {
113
- if (error instanceof TransactionsNotAvailableError) {
114
- this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
115
- message: error.message,
116
- });
117
- await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
118
- } else {
119
- this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
120
- }
121
- }
122
- }
123
-
124
- public async validateBlocks(blocks: L2Block[]): Promise<void> {
125
- if (blocks.length === 0) {
126
- return;
127
- }
128
-
129
- let previousCheckpointOutHashes: Fr[] = [];
130
- const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
131
- try {
132
- for (const block of blocks) {
133
- await this.validateBlock(block, previousCheckpointOutHashes, fork);
134
-
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)]);
137
- previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
138
- }
139
- } finally {
140
- await fork.close();
141
- }
142
- }
143
-
144
- public async validateBlock(
145
- blockFromL1: L2Block,
146
- previousCheckpointOutHashes: Fr[],
147
- fork: MerkleTreeWriteOperations,
148
- ): 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
- }
159
-
160
- const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
161
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
162
- const gv = blockFromL1.header.globalVariables;
163
- const constants: CheckpointGlobalVariables = {
164
- chainId: gv.chainId,
165
- version: gv.version,
166
- slotNumber: gv.slotNumber,
167
- coinbase: gv.coinbase,
168
- feeRecipient: gv.feeRecipient,
169
- gasFees: gv.gasFees,
170
- };
171
-
172
- // Use checkpoint builder to validate the block
173
- const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
174
- checkpointNumber,
175
- constants,
176
- l1ToL2Messages,
177
- previousCheckpointOutHashes,
178
- fork,
179
- this.log.getBindings(),
180
- );
181
-
182
- const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
183
-
184
- if (numTxs !== txs.length) {
185
- // This should be detected by state mismatch, but this makes it easier to debug.
186
- throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
187
- }
188
- if (failedTxs.length > 0) {
189
- throw new ReExFailedTxsError(failedTxs.length);
190
- }
191
- if (!block.archive.root.equals(blockFromL1.archive.root)) {
192
- throw new ReExStateMismatchError(blockFromL1.archive.root, block.archive.root);
193
- }
194
- }
195
-
196
- private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
197
- const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
198
- if (!committee) {
199
- this.log.trace(`No committee found for epoch ${epochNumber}`);
200
- return [];
201
- }
202
- return committee;
203
- }
204
-
205
- private validatorsToSlashingArgs(
206
- validators: EthAddress[],
207
- offenseType: OffenseType,
208
- epochOrSlot: EpochNumber,
209
- ): WantToSlashArgs[] {
210
- const penalty =
211
- offenseType === OffenseType.DATA_WITHHOLDING
212
- ? this.penalties.slashDataWithholdingPenalty
213
- : this.penalties.slashPrunePenalty;
214
- return validators.map(v => ({
215
- validator: v,
216
- amount: penalty,
217
- offenseType,
218
- epochOrSlot: BigInt(epochOrSlot),
219
- }));
220
- }
221
- }