@aztec/slasher 0.0.1-commit.001888fc
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.
- package/README.md +228 -0
- package/dest/config.d.ts +6 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +146 -0
- package/dest/empire_slasher_client.d.ts +190 -0
- package/dest/empire_slasher_client.d.ts.map +1 -0
- package/dest/empire_slasher_client.js +564 -0
- package/dest/factory/create_facade.d.ts +16 -0
- package/dest/factory/create_facade.d.ts.map +1 -0
- package/dest/factory/create_facade.js +46 -0
- package/dest/factory/create_implementation.d.ts +18 -0
- package/dest/factory/create_implementation.d.ts.map +1 -0
- package/dest/factory/create_implementation.js +77 -0
- package/dest/factory/get_settings.d.ts +4 -0
- package/dest/factory/get_settings.d.ts.map +1 -0
- package/dest/factory/get_settings.js +36 -0
- package/dest/factory/index.d.ts +3 -0
- package/dest/factory/index.d.ts.map +1 -0
- package/dest/factory/index.js +2 -0
- package/dest/generated/slasher-defaults.d.ts +21 -0
- package/dest/generated/slasher-defaults.d.ts.map +1 -0
- package/dest/generated/slasher-defaults.js +21 -0
- package/dest/index.d.ts +11 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +10 -0
- package/dest/null_slasher_client.d.ts +17 -0
- package/dest/null_slasher_client.d.ts.map +1 -0
- package/dest/null_slasher_client.js +33 -0
- package/dest/slash_offenses_collector.d.ts +48 -0
- package/dest/slash_offenses_collector.d.ts.map +1 -0
- package/dest/slash_offenses_collector.js +94 -0
- package/dest/slash_round_monitor.d.ts +30 -0
- package/dest/slash_round_monitor.d.ts.map +1 -0
- package/dest/slash_round_monitor.js +52 -0
- package/dest/slasher_client_facade.d.ts +45 -0
- package/dest/slasher_client_facade.d.ts.map +1 -0
- package/dest/slasher_client_facade.js +78 -0
- package/dest/slasher_client_interface.d.ts +39 -0
- package/dest/slasher_client_interface.d.ts.map +1 -0
- package/dest/slasher_client_interface.js +4 -0
- package/dest/stores/offenses_store.d.ts +37 -0
- package/dest/stores/offenses_store.d.ts.map +1 -0
- package/dest/stores/offenses_store.js +107 -0
- package/dest/stores/payloads_store.d.ts +29 -0
- package/dest/stores/payloads_store.d.ts.map +1 -0
- package/dest/stores/payloads_store.js +128 -0
- package/dest/stores/schema_version.d.ts +2 -0
- package/dest/stores/schema_version.d.ts.map +1 -0
- package/dest/stores/schema_version.js +1 -0
- package/dest/tally_slasher_client.d.ts +125 -0
- package/dest/tally_slasher_client.d.ts.map +1 -0
- package/dest/tally_slasher_client.js +354 -0
- package/dest/test/dummy_watcher.d.ts +11 -0
- package/dest/test/dummy_watcher.d.ts.map +1 -0
- package/dest/test/dummy_watcher.js +14 -0
- package/dest/watcher.d.ts +21 -0
- package/dest/watcher.d.ts.map +1 -0
- package/dest/watcher.js +1 -0
- package/dest/watchers/attestations_block_watcher.d.ts +34 -0
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -0
- package/dest/watchers/attestations_block_watcher.js +142 -0
- package/dest/watchers/epoch_prune_watcher.d.ts +39 -0
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -0
- package/dest/watchers/epoch_prune_watcher.js +176 -0
- package/package.json +92 -0
- package/src/config.ts +172 -0
- package/src/empire_slasher_client.ts +649 -0
- package/src/factory/create_facade.ts +82 -0
- package/src/factory/create_implementation.ts +184 -0
- package/src/factory/get_settings.ts +58 -0
- package/src/factory/index.ts +2 -0
- package/src/generated/slasher-defaults.ts +23 -0
- package/src/index.ts +10 -0
- package/src/null_slasher_client.ts +41 -0
- package/src/slash_offenses_collector.ts +123 -0
- package/src/slash_round_monitor.ts +62 -0
- package/src/slasher_client_facade.ts +103 -0
- package/src/slasher_client_interface.ts +46 -0
- package/src/stores/offenses_store.ts +147 -0
- package/src/stores/payloads_store.ts +149 -0
- package/src/stores/schema_version.ts +1 -0
- package/src/tally_slasher_client.ts +448 -0
- package/src/test/dummy_watcher.ts +21 -0
- package/src/watcher.ts +27 -0
- package/src/watchers/attestations_block_watcher.ts +194 -0
- package/src/watchers/epoch_prune_watcher.ts +253 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
7
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
8
|
+
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
9
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
10
|
+
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
11
|
+
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
12
|
+
|
|
13
|
+
import { createSlasherImplementation } from './factory/create_implementation.js';
|
|
14
|
+
import type { SlasherClientInterface } from './slasher_client_interface.js';
|
|
15
|
+
import type { Watcher } from './watcher.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Facade for the Slasher client. This class forwards all requests to the actual Slasher client implementation.
|
|
19
|
+
* This class also monitors via the rollup contract when the underlying slasher proposer contract changes, and when it
|
|
20
|
+
* does, it stops the current slasher client, recreates a new one with the new contract address, and starts it again.
|
|
21
|
+
*/
|
|
22
|
+
export class SlasherClientFacade implements SlasherClientInterface {
|
|
23
|
+
private client: SlasherClientInterface | undefined;
|
|
24
|
+
private unwatch: (() => void) | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
|
|
28
|
+
private rollup: RollupContract,
|
|
29
|
+
private l1Client: ViemClient,
|
|
30
|
+
private slashFactoryAddress: EthAddress | undefined,
|
|
31
|
+
private watchers: Watcher[],
|
|
32
|
+
private epochCache: EpochCache,
|
|
33
|
+
private dateProvider: DateProvider,
|
|
34
|
+
private kvStore: AztecLMDBStoreV2,
|
|
35
|
+
private rollupRegisteredAtL2Slot: SlotNumber,
|
|
36
|
+
private logger = createLogger('slasher'),
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
public async start(): Promise<void> {
|
|
40
|
+
this.client = await this.createSlasherClient();
|
|
41
|
+
await this.client?.start();
|
|
42
|
+
|
|
43
|
+
this.unwatch = this.rollup.listenToSlasherChanged(() => {
|
|
44
|
+
void this.handleSlasherChange().catch(error => {
|
|
45
|
+
this.logger.error('Error handling slasher change', error);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async stop(): Promise<void> {
|
|
51
|
+
await this.client?.stop();
|
|
52
|
+
this.unwatch?.();
|
|
53
|
+
this.unwatch = undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public getConfig(): SlasherConfig {
|
|
57
|
+
return this.config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public updateConfig(config: Partial<SlasherConfig>): void {
|
|
61
|
+
this.config = { ...this.config, ...config };
|
|
62
|
+
this.client?.updateConfig(config);
|
|
63
|
+
this.watchers.forEach(watcher => watcher.updateConfig?.(config));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getSlashPayloads(): Promise<SlashPayloadRound[]> {
|
|
67
|
+
return this.client?.getSlashPayloads() ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public gatherOffensesForRound(round?: bigint): Promise<Offense[]> {
|
|
71
|
+
return this.client?.gatherOffensesForRound(round) ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public getPendingOffenses(): Promise<Offense[]> {
|
|
75
|
+
return this.client?.getPendingOffenses() ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
79
|
+
return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private createSlasherClient() {
|
|
83
|
+
return createSlasherImplementation(
|
|
84
|
+
this.config,
|
|
85
|
+
this.rollup,
|
|
86
|
+
this.l1Client,
|
|
87
|
+
this.slashFactoryAddress,
|
|
88
|
+
this.watchers,
|
|
89
|
+
this.epochCache,
|
|
90
|
+
this.dateProvider,
|
|
91
|
+
this.kvStore,
|
|
92
|
+
this.rollupRegisteredAtL2Slot,
|
|
93
|
+
this.logger,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async handleSlasherChange() {
|
|
98
|
+
this.logger.warn('Slasher contract changed, recreating slasher client');
|
|
99
|
+
await this.client?.stop();
|
|
100
|
+
this.client = await this.createSlasherClient();
|
|
101
|
+
await this.client?.start();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
3
|
+
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Common interface for slasher clients used by the Aztec node.
|
|
7
|
+
* Both Empire and Consensus slasher clients implement this interface.
|
|
8
|
+
*/
|
|
9
|
+
export interface SlasherClientInterface {
|
|
10
|
+
/** Start the slasher client */
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
|
|
13
|
+
/** Stop the slasher client */
|
|
14
|
+
stop(): Promise<void>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get slash payloads for the Empire model.
|
|
18
|
+
* The Consensus model should throw an error when this is called.
|
|
19
|
+
*/
|
|
20
|
+
getSlashPayloads(): Promise<SlashPayloadRound[]>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gather offenses for a given round, defaults to current.
|
|
24
|
+
* Used by both Empire and Consensus models.
|
|
25
|
+
*/
|
|
26
|
+
gatherOffensesForRound(round?: bigint): Promise<Offense[]>;
|
|
27
|
+
|
|
28
|
+
/** Returns all pending offenses */
|
|
29
|
+
getPendingOffenses(): Promise<Offense[]>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update the configuration.
|
|
33
|
+
* Used by both Empire and Consensus models.
|
|
34
|
+
*/
|
|
35
|
+
updateConfig(config: Partial<SlasherConfig>): void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the actions the proposer should take for slashing.
|
|
39
|
+
* @param slotNumber - The current slot number
|
|
40
|
+
* @returns The actions to take
|
|
41
|
+
*/
|
|
42
|
+
getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
|
|
43
|
+
|
|
44
|
+
/** Returns the current config */
|
|
45
|
+
getConfig(): SlasherConfig;
|
|
46
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
const round = getRoundForOffense(offense, this.settings);
|
|
80
|
+
await this.kvStore.transactionAsync(async () => {
|
|
81
|
+
await this.offenses.set(key, serializeOffense(offense));
|
|
82
|
+
await this.roundsOffenses.set(this.getRoundKey(round), key);
|
|
83
|
+
});
|
|
84
|
+
this.log.trace(`Adding pending offense ${key} for round ${round}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Marks the given offenses as slashed (regardless of whether they are known or not) */
|
|
88
|
+
public async markAsSlashed(offenses: OffenseIdentifier[]): Promise<void> {
|
|
89
|
+
await this.kvStore.transactionAsync(async () => {
|
|
90
|
+
for (const offense of offenses) {
|
|
91
|
+
const key = this.getOffenseKey(offense);
|
|
92
|
+
await this.offensesSlashed.add(key);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Prunes all offenses expired from the store */
|
|
98
|
+
public async clearExpiredOffenses(currentRound: bigint): Promise<number> {
|
|
99
|
+
const expirationRounds = this.settings.slashOffenseExpirationRounds ?? 0;
|
|
100
|
+
if (expirationRounds <= 0) {
|
|
101
|
+
return 0; // No expiration configured
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const expiredBefore = currentRound - BigInt(expirationRounds);
|
|
105
|
+
if (expiredBefore < 0) {
|
|
106
|
+
return 0; // Not enough rounds have passed to expire anything
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Collect expired offenses and rounds
|
|
110
|
+
const expiredRoundKeys = new Set<string>();
|
|
111
|
+
const expiredOffenseKeys = new Set<string>();
|
|
112
|
+
for await (const [roundKey, offenseKey] of this.roundsOffenses.entriesAsync({
|
|
113
|
+
end: this.getRoundKey(expiredBefore),
|
|
114
|
+
})) {
|
|
115
|
+
expiredOffenseKeys.add(offenseKey);
|
|
116
|
+
expiredRoundKeys.add(roundKey);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (expiredOffenseKeys.size === 0 && expiredRoundKeys.size === 0) {
|
|
120
|
+
return 0; // Nothing to clean up
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Remove expired stuff in a transaction
|
|
124
|
+
await this.kvStore.transactionAsync(async () => {
|
|
125
|
+
for (const key of expiredOffenseKeys) {
|
|
126
|
+
this.log.trace(`Deleting offense ${key}`);
|
|
127
|
+
await this.offenses.delete(key);
|
|
128
|
+
await this.offensesSlashed.delete(key);
|
|
129
|
+
}
|
|
130
|
+
for (const roundKey of expiredRoundKeys) {
|
|
131
|
+
this.log.trace(`Deleting round info for ${roundKey}`);
|
|
132
|
+
await this.roundsOffenses.delete(roundKey);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return expiredOffenseKeys.size;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Generate a unique key for an offense */
|
|
140
|
+
private getOffenseKey(offense: OffenseIdentifier): string {
|
|
141
|
+
return `${offense.validator.toString()}:${offense.offenseType}:${offense.epochOrSlot}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private getRoundKey(round: bigint): string {
|
|
145
|
+
return round.toString().padStart(16, '0');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SCHEMA_VERSION = 1;
|