@aztec/slasher 0.0.1-commit.21caa21

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 (82) hide show
  1. package/README.md +218 -0
  2. package/dest/config.d.ts +6 -0
  3. package/dest/config.d.ts.map +1 -0
  4. package/dest/config.js +134 -0
  5. package/dest/empire_slasher_client.d.ts +190 -0
  6. package/dest/empire_slasher_client.d.ts.map +1 -0
  7. package/dest/empire_slasher_client.js +572 -0
  8. package/dest/factory/create_facade.d.ts +15 -0
  9. package/dest/factory/create_facade.d.ts.map +1 -0
  10. package/dest/factory/create_facade.js +23 -0
  11. package/dest/factory/create_implementation.d.ts +17 -0
  12. package/dest/factory/create_implementation.d.ts.map +1 -0
  13. package/dest/factory/create_implementation.js +73 -0
  14. package/dest/factory/get_settings.d.ts +4 -0
  15. package/dest/factory/get_settings.d.ts.map +1 -0
  16. package/dest/factory/get_settings.js +36 -0
  17. package/dest/factory/index.d.ts +3 -0
  18. package/dest/factory/index.d.ts.map +1 -0
  19. package/dest/factory/index.js +2 -0
  20. package/dest/index.d.ts +11 -0
  21. package/dest/index.d.ts.map +1 -0
  22. package/dest/index.js +10 -0
  23. package/dest/null_slasher_client.d.ts +17 -0
  24. package/dest/null_slasher_client.d.ts.map +1 -0
  25. package/dest/null_slasher_client.js +33 -0
  26. package/dest/slash_offenses_collector.d.ts +45 -0
  27. package/dest/slash_offenses_collector.d.ts.map +1 -0
  28. package/dest/slash_offenses_collector.js +94 -0
  29. package/dest/slash_round_monitor.d.ts +30 -0
  30. package/dest/slash_round_monitor.d.ts.map +1 -0
  31. package/dest/slash_round_monitor.js +52 -0
  32. package/dest/slasher_client_facade.d.ts +44 -0
  33. package/dest/slasher_client_facade.d.ts.map +1 -0
  34. package/dest/slasher_client_facade.js +76 -0
  35. package/dest/slasher_client_interface.d.ts +39 -0
  36. package/dest/slasher_client_interface.d.ts.map +1 -0
  37. package/dest/slasher_client_interface.js +4 -0
  38. package/dest/stores/offenses_store.d.ts +37 -0
  39. package/dest/stores/offenses_store.d.ts.map +1 -0
  40. package/dest/stores/offenses_store.js +105 -0
  41. package/dest/stores/payloads_store.d.ts +29 -0
  42. package/dest/stores/payloads_store.d.ts.map +1 -0
  43. package/dest/stores/payloads_store.js +125 -0
  44. package/dest/stores/schema_version.d.ts +2 -0
  45. package/dest/stores/schema_version.d.ts.map +1 -0
  46. package/dest/stores/schema_version.js +1 -0
  47. package/dest/tally_slasher_client.d.ts +125 -0
  48. package/dest/tally_slasher_client.d.ts.map +1 -0
  49. package/dest/tally_slasher_client.js +349 -0
  50. package/dest/test/dummy_watcher.d.ts +11 -0
  51. package/dest/test/dummy_watcher.d.ts.map +1 -0
  52. package/dest/test/dummy_watcher.js +14 -0
  53. package/dest/watcher.d.ts +21 -0
  54. package/dest/watcher.d.ts.map +1 -0
  55. package/dest/watcher.js +1 -0
  56. package/dest/watchers/attestations_block_watcher.d.ts +33 -0
  57. package/dest/watchers/attestations_block_watcher.d.ts.map +1 -0
  58. package/dest/watchers/attestations_block_watcher.js +136 -0
  59. package/dest/watchers/epoch_prune_watcher.d.ts +37 -0
  60. package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -0
  61. package/dest/watchers/epoch_prune_watcher.js +135 -0
  62. package/package.json +90 -0
  63. package/src/config.ts +157 -0
  64. package/src/empire_slasher_client.ts +657 -0
  65. package/src/factory/create_facade.ts +52 -0
  66. package/src/factory/create_implementation.ts +159 -0
  67. package/src/factory/get_settings.ts +58 -0
  68. package/src/factory/index.ts +2 -0
  69. package/src/index.ts +10 -0
  70. package/src/null_slasher_client.ts +41 -0
  71. package/src/slash_offenses_collector.ts +118 -0
  72. package/src/slash_round_monitor.ts +62 -0
  73. package/src/slasher_client_facade.ts +101 -0
  74. package/src/slasher_client_interface.ts +46 -0
  75. package/src/stores/offenses_store.ts +145 -0
  76. package/src/stores/payloads_store.ts +146 -0
  77. package/src/stores/schema_version.ts +1 -0
  78. package/src/tally_slasher_client.ts +442 -0
  79. package/src/test/dummy_watcher.ts +21 -0
  80. package/src/watcher.ts +27 -0
  81. package/src/watchers/attestations_block_watcher.ts +181 -0
  82. package/src/watchers/epoch_prune_watcher.ts +193 -0
@@ -0,0 +1,145 @@
1
+ import { createLogger } from '@aztec/aztec.js/log';
2
+ import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncMultiMap, AztecAsyncSet } from '@aztec/kv-store';
3
+ import {
4
+ type Offense,
5
+ type OffenseIdentifier,
6
+ deserializeOffense,
7
+ getRoundForOffense,
8
+ serializeOffense,
9
+ } from '@aztec/stdlib/slashing';
10
+
11
+ export const SCHEMA_VERSION = 1;
12
+
13
+ export class SlasherOffensesStore {
14
+ /** Map from offense key to offense data */
15
+ private offenses: AztecAsyncMap<string, Buffer>;
16
+
17
+ /** Map from offense key to whether the offense has been executed (only used for empire based slashing) */
18
+ private offensesSlashed: AztecAsyncSet<string>;
19
+
20
+ /** Multimap from round to offense keys (only used for consensus based slashing) */
21
+ private roundsOffenses: AztecAsyncMultiMap<string, string>;
22
+
23
+ private log = createLogger('slasher:store:offenses');
24
+
25
+ constructor(
26
+ private kvStore: AztecAsyncKVStore,
27
+ private settings: {
28
+ slashingRoundSize: number;
29
+ epochDuration: number;
30
+ slashOffenseExpirationRounds?: number;
31
+ },
32
+ ) {
33
+ this.offenses = kvStore.openMap('offenses');
34
+ this.roundsOffenses = kvStore.openMultiMap('rounds-offenses');
35
+ this.offensesSlashed = kvStore.openSet('offenses-slashed');
36
+ }
37
+
38
+ /** Returns all offenses not marked as slashed */
39
+ public async getPendingOffenses(): Promise<Offense[]> {
40
+ const offenses: Offense[] = [];
41
+ for await (const [key, buffer] of this.offenses.entriesAsync()) {
42
+ if (await this.offensesSlashed.hasAsync(key)) {
43
+ continue; // Skip executed offenses
44
+ }
45
+ const offense = deserializeOffense(buffer);
46
+ offenses.push(offense);
47
+ }
48
+ return offenses;
49
+ }
50
+
51
+ /** Returns all offenses tracked for the given round */
52
+ public async getOffensesForRound(round: bigint): Promise<Offense[]> {
53
+ const offenses: Offense[] = [];
54
+ for await (const key of this.roundsOffenses.getValuesAsync(this.getRoundKey(round))) {
55
+ const buffer = await this.offenses.getAsync(key);
56
+ if (buffer) {
57
+ const offense = deserializeOffense(buffer);
58
+ offenses.push(offense);
59
+ }
60
+ }
61
+ return offenses;
62
+ }
63
+
64
+ /** Returns whether an offense is pending (ie not marked as slashed) */
65
+ public async hasPendingOffense(offense: OffenseIdentifier): Promise<boolean> {
66
+ const key = this.getOffenseKey(offense);
67
+ return (await this.offenses.getAsync(key)) !== undefined && !(await this.offensesSlashed.hasAsync(key));
68
+ }
69
+
70
+ /** Returns whether we have seen this offense */
71
+ public async hasOffense(offense: OffenseIdentifier): Promise<boolean> {
72
+ const key = this.getOffenseKey(offense);
73
+ return (await this.offenses.getAsync(key)) !== undefined;
74
+ }
75
+
76
+ /** Adds a new offense (defaults to pending, but will be slashed if markAsSlashed had been called for it) */
77
+ public async addPendingOffense(offense: Offense): Promise<void> {
78
+ const key = this.getOffenseKey(offense);
79
+ await this.offenses.set(key, serializeOffense(offense));
80
+ const round = getRoundForOffense(offense, this.settings);
81
+ await this.roundsOffenses.set(this.getRoundKey(round), key);
82
+ this.log.trace(`Adding pending offense ${key} for round ${round}`);
83
+ }
84
+
85
+ /** Marks the given offenses as slashed (regardless of whether they are known or not) */
86
+ public async markAsSlashed(offenses: OffenseIdentifier[]): Promise<void> {
87
+ await this.kvStore.transactionAsync(async () => {
88
+ for (const offense of offenses) {
89
+ const key = this.getOffenseKey(offense);
90
+ await this.offensesSlashed.add(key);
91
+ }
92
+ });
93
+ }
94
+
95
+ /** Prunes all offenses expired from the store */
96
+ public async clearExpiredOffenses(currentRound: bigint): Promise<number> {
97
+ const expirationRounds = this.settings.slashOffenseExpirationRounds ?? 0;
98
+ if (expirationRounds <= 0) {
99
+ return 0; // No expiration configured
100
+ }
101
+
102
+ const expiredBefore = currentRound - BigInt(expirationRounds);
103
+ if (expiredBefore < 0) {
104
+ return 0; // Not enough rounds have passed to expire anything
105
+ }
106
+
107
+ // Collect expired offenses and rounds
108
+ const expiredRoundKeys = new Set<string>();
109
+ const expiredOffenseKeys = new Set<string>();
110
+ for await (const [roundKey, offenseKey] of this.roundsOffenses.entriesAsync({
111
+ end: this.getRoundKey(expiredBefore),
112
+ })) {
113
+ expiredOffenseKeys.add(offenseKey);
114
+ expiredRoundKeys.add(roundKey);
115
+ }
116
+
117
+ if (expiredOffenseKeys.size === 0 && expiredRoundKeys.size === 0) {
118
+ return 0; // Nothing to clean up
119
+ }
120
+
121
+ // Remove expired stuff in a transaction
122
+ await this.kvStore.transactionAsync(async () => {
123
+ for (const key of expiredOffenseKeys) {
124
+ this.log.trace(`Deleting offense ${key}`);
125
+ await this.offenses.delete(key);
126
+ await this.offensesSlashed.delete(key);
127
+ }
128
+ for (const roundKey of expiredRoundKeys) {
129
+ this.log.trace(`Deleting round info for ${roundKey}`);
130
+ await this.roundsOffenses.delete(roundKey);
131
+ }
132
+ });
133
+
134
+ return expiredOffenseKeys.size;
135
+ }
136
+
137
+ /** Generate a unique key for an offense */
138
+ private getOffenseKey(offense: OffenseIdentifier): string {
139
+ return `${offense.validator.toString()}:${offense.offenseType}:${offense.epochOrSlot}`;
140
+ }
141
+
142
+ private getRoundKey(round: bigint): string {
143
+ return round.toString().padStart(16, '0');
144
+ }
145
+ }
@@ -0,0 +1,146 @@
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
+ const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
122
+ const newVotes = currentVotes + 1n;
123
+ await this.roundPayloadVotes.set(key, newVotes);
124
+ return newVotes;
125
+ }
126
+
127
+ public async addPayload(payload: SlashPayloadRound): Promise<void> {
128
+ const address = payload.address.toString();
129
+
130
+ await this.kvStore.transactionAsync(async () => {
131
+ await this.payloads.set(address, serializeSlashPayload(payload));
132
+ await this.roundPayloadVotes.set(this.getPayloadVotesKey(payload.round, address), payload.votes);
133
+ });
134
+ }
135
+
136
+ public async getPayload(payloadAddress: EthAddress | string): Promise<SlashPayload | undefined> {
137
+ const address = payloadAddress.toString();
138
+ const buffer = await this.payloads.getAsync(address);
139
+ return buffer ? deserializeSlashPayload(buffer) : undefined;
140
+ }
141
+
142
+ public async hasPayload(payload: EthAddress): Promise<boolean> {
143
+ const address = payload.toString();
144
+ return (await this.payloads.getAsync(address)) !== undefined;
145
+ }
146
+ }
@@ -0,0 +1 @@
1
+ export const SCHEMA_VERSION = 1;