@aztec/slasher 0.0.1-commit.f2ce05ee → 0.0.1-commit.f5a9928
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 +85 -78
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +35 -29
- package/dest/factory/create_facade.d.ts +7 -4
- package/dest/factory/create_facade.d.ts.map +1 -1
- package/dest/factory/create_facade.js +29 -3
- package/dest/factory/create_implementation.d.ts +7 -7
- package/dest/factory/create_implementation.d.ts.map +1 -1
- package/dest/factory/create_implementation.js +8 -56
- package/dest/factory/get_settings.d.ts +4 -4
- package/dest/factory/get_settings.d.ts.map +1 -1
- package/dest/factory/get_settings.js +3 -3
- package/dest/factory/index.d.ts +2 -2
- package/dest/factory/index.d.ts.map +1 -1
- package/dest/factory/index.js +1 -1
- package/dest/generated/slasher-defaults.d.ts +8 -7
- package/dest/generated/slasher-defaults.d.ts.map +1 -1
- package/dest/generated/slasher-defaults.js +7 -6
- package/dest/index.d.ts +6 -4
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +5 -3
- package/dest/metrics.d.ts +23 -0
- package/dest/metrics.d.ts.map +1 -0
- package/dest/metrics.js +38 -0
- package/dest/null_slasher_client.d.ts +3 -4
- package/dest/null_slasher_client.d.ts.map +1 -1
- package/dest/null_slasher_client.js +1 -4
- package/dest/own_validator_slash_monitor.d.ts +66 -0
- package/dest/own_validator_slash_monitor.d.ts.map +1 -0
- package/dest/own_validator_slash_monitor.js +172 -0
- package/dest/slash_offenses_collector.d.ts +10 -9
- package/dest/slash_offenses_collector.d.ts.map +1 -1
- package/dest/slash_offenses_collector.js +50 -34
- package/dest/slasher_client.d.ts +118 -0
- package/dest/slasher_client.d.ts.map +1 -0
- package/dest/{tally_slasher_client.js → slasher_client.js} +65 -45
- package/dest/slasher_client_facade.d.ts +8 -8
- package/dest/slasher_client_facade.d.ts.map +1 -1
- package/dest/slasher_client_facade.js +8 -9
- package/dest/slasher_client_interface.d.ts +7 -21
- package/dest/slasher_client_interface.d.ts.map +1 -1
- package/dest/slasher_client_interface.js +1 -4
- package/dest/stores/offenses_store.d.ts +12 -12
- package/dest/stores/offenses_store.d.ts.map +1 -1
- package/dest/stores/offenses_store.js +61 -38
- package/dest/watcher.d.ts +8 -1
- package/dest/watcher.d.ts.map +1 -1
- package/dest/watcher.js +1 -0
- package/dest/watchers/attestations_block_watcher.d.ts +26 -13
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.js +76 -61
- package/dest/watchers/attested_invalid_proposal_watcher.d.ts +42 -0
- package/dest/watchers/attested_invalid_proposal_watcher.d.ts.map +1 -0
- package/dest/watchers/attested_invalid_proposal_watcher.js +117 -0
- package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts +38 -0
- package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts.map +1 -0
- package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.js +138 -0
- package/dest/watchers/checkpoint_equivocation_watcher.d.ts +30 -0
- package/dest/watchers/checkpoint_equivocation_watcher.d.ts.map +1 -0
- package/dest/watchers/checkpoint_equivocation_watcher.js +69 -0
- package/dest/watchers/data_withholding_watcher.d.ts +69 -0
- package/dest/watchers/data_withholding_watcher.d.ts.map +1 -0
- package/dest/watchers/data_withholding_watcher.js +223 -0
- package/package.json +10 -10
- package/src/config.ts +42 -29
- package/src/factory/create_facade.ts +37 -5
- package/src/factory/create_implementation.ts +29 -105
- package/src/factory/get_settings.ts +8 -8
- package/src/factory/index.ts +1 -1
- package/src/generated/slasher-defaults.ts +7 -6
- package/src/index.ts +5 -3
- package/src/metrics.ts +59 -0
- package/src/null_slasher_client.ts +2 -6
- package/src/own_validator_slash_monitor.ts +195 -0
- package/src/slash_offenses_collector.ts +70 -36
- package/src/{tally_slasher_client.ts → slasher_client.ts} +84 -54
- package/src/slasher_client_facade.ts +9 -11
- package/src/slasher_client_interface.ts +6 -21
- package/src/stores/offenses_store.ts +73 -47
- package/src/watcher.ts +8 -0
- package/src/watchers/attestations_block_watcher.ts +88 -82
- package/src/watchers/attested_invalid_proposal_watcher.ts +168 -0
- package/src/watchers/broadcasted_invalid_checkpoint_proposal_watcher.ts +192 -0
- package/src/watchers/checkpoint_equivocation_watcher.ts +96 -0
- package/src/watchers/data_withholding_watcher.ts +257 -0
- package/dest/empire_slasher_client.d.ts +0 -190
- package/dest/empire_slasher_client.d.ts.map +0 -1
- package/dest/empire_slasher_client.js +0 -564
- package/dest/stores/payloads_store.d.ts +0 -29
- package/dest/stores/payloads_store.d.ts.map +0 -1
- package/dest/stores/payloads_store.js +0 -128
- package/dest/tally_slasher_client.d.ts +0 -125
- package/dest/tally_slasher_client.d.ts.map +0 -1
- package/dest/watchers/epoch_prune_watcher.d.ts +0 -39
- package/dest/watchers/epoch_prune_watcher.d.ts.map +0 -1
- package/dest/watchers/epoch_prune_watcher.js +0 -175
- package/src/empire_slasher_client.ts +0 -649
- package/src/stores/payloads_store.ts +0 -149
- package/src/watchers/epoch_prune_watcher.ts +0 -251
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import type { Prettify } from '@aztec/foundation/types';
|
|
2
3
|
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
3
4
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
4
|
-
import { type OffenseIdentifier } from '@aztec/stdlib/slashing';
|
|
5
5
|
import type { SlasherOffensesStore } from './stores/offenses_store.js';
|
|
6
|
-
import { type WantToSlashArgs, type Watcher } from './watcher.js';
|
|
6
|
+
import { type WantToClearSlashArgs, type WantToSlashArgs, type Watcher } from './watcher.js';
|
|
7
7
|
export type SlashOffensesCollectorConfig = Prettify<Pick<SlasherConfig, 'slashGracePeriodL2Slots'>>;
|
|
8
8
|
export type SlashOffensesCollectorSettings = Prettify<Pick<L1RollupConstants, 'epochDuration'> & {
|
|
9
9
|
slashingAmounts: [bigint, bigint, bigint] | undefined;
|
|
10
|
+
/** L2 slot at which the rollup was registered as canonical in the Registry. Used to anchor the slash grace period. */
|
|
11
|
+
rollupRegisteredAtL2Slot: SlotNumber;
|
|
10
12
|
}>;
|
|
11
13
|
/**
|
|
12
14
|
* Collects and manages slashable offenses from watchers.
|
|
@@ -20,6 +22,7 @@ export declare class SlashOffensesCollector {
|
|
|
20
22
|
private readonly offensesStore;
|
|
21
23
|
private readonly log;
|
|
22
24
|
private readonly unwatchCallbacks;
|
|
25
|
+
private readonly storeMutationQueue;
|
|
23
26
|
constructor(config: SlashOffensesCollectorConfig, settings: SlashOffensesCollectorSettings, watchers: Watcher[], offensesStore: SlasherOffensesStore, log?: import("@aztec/foundation/log").Logger);
|
|
24
27
|
start(): Promise<void>;
|
|
25
28
|
stop(): Promise<void>;
|
|
@@ -29,17 +32,15 @@ export declare class SlashOffensesCollector {
|
|
|
29
32
|
* @param args - the arguments from the watcher, including the validators, amounts, and offenses
|
|
30
33
|
*/
|
|
31
34
|
handleWantToSlash(args: WantToSlashArgs[]): Promise<void>;
|
|
35
|
+
handleWantToClearSlash(args: WantToClearSlashArgs[]): Promise<void>;
|
|
32
36
|
/**
|
|
33
37
|
* Triggered on a time basis when we enter a new slashing round.
|
|
34
38
|
* Clears expired offenses from stores.
|
|
35
39
|
*/
|
|
36
40
|
handleNewRound(round: bigint): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* Marks offenses as slashed (no longer pending)
|
|
39
|
-
* @param offenses - The offenses to mark as slashed
|
|
40
|
-
*/
|
|
41
|
-
markAsSlashed(offenses: OffenseIdentifier[]): Promise<void>;
|
|
42
|
-
/** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */
|
|
41
|
+
/** Returns whether to skip an offense if it happened during the grace period after the network upgrade */
|
|
43
42
|
private shouldSkipOffense;
|
|
43
|
+
private getOffenseLogData;
|
|
44
|
+
private enqueueStoreMutation;
|
|
44
45
|
}
|
|
45
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
46
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hfb2ZmZW5zZXNfY29sbGVjdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2xhc2hfb2ZmZW5zZXNfY29sbGVjdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLFVBQVUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBR2xFLE9BQU8sS0FBSyxFQUFFLFFBQVEsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3hELE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDckUsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFHckUsT0FBTyxLQUFLLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUN2RSxPQUFPLEVBR0wsS0FBSyxvQkFBb0IsRUFDekIsS0FBSyxlQUFlLEVBQ3BCLEtBQUssT0FBTyxFQUNiLE1BQU0sY0FBYyxDQUFDO0FBRXRCLE1BQU0sTUFBTSw0QkFBNEIsR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLGFBQWEsRUFBRSx5QkFBeUIsQ0FBQyxDQUFDLENBQUM7QUFDcEcsTUFBTSxNQUFNLDhCQUE4QixHQUFHLFFBQVEsQ0FDbkQsSUFBSSxDQUFDLGlCQUFpQixFQUFFLGVBQWUsQ0FBQyxHQUFHO0lBQ3pDLGVBQWUsRUFBRSxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLEdBQUcsU0FBUyxDQUFDO0lBQ3RELHNIQUFzSDtJQUN0SCx3QkFBd0IsRUFBRSxVQUFVLENBQUM7Q0FDdEMsQ0FDRixDQUFDO0FBRUY7Ozs7R0FJRztBQUNILHFCQUFhLHNCQUFzQjtJQUsvQixPQUFPLENBQUMsUUFBUSxDQUFDLE1BQU07SUFDdkIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRO0lBQ3pCLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUTtJQUN6QixPQUFPLENBQUMsUUFBUSxDQUFDLGFBQWE7SUFDOUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFHO0lBUnRCLE9BQU8sQ0FBQyxRQUFRLENBQUMsZ0JBQWdCLENBQXNCO0lBQ3ZELE9BQU8sQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQXFCO0lBRXhELFlBQ21CLE1BQU0sRUFBRSw0QkFBNEIsRUFDcEMsUUFBUSxFQUFFLDhCQUE4QixFQUN4QyxRQUFRLEVBQUUsT0FBTyxFQUFFLEVBQ25CLGFBQWEsRUFBRSxvQkFBb0IsRUFDbkMsR0FBRyx5Q0FBNkMsRUFDL0Q7SUFFRyxLQUFLLGtCQW1CWDtJQUVZLElBQUksa0JBVWhCO0lBRUQ7Ozs7T0FJRztJQUNVLGlCQUFpQixDQUFDLElBQUksRUFBRSxlQUFlLEVBQUUsaUJBK0JyRDtJQUVZLHNCQUFzQixDQUFDLElBQUksRUFBRSxvQkFBb0IsRUFBRSxpQkFXL0Q7SUFFRDs7O09BR0c7SUFDVSxjQUFjLENBQUMsS0FBSyxFQUFFLE1BQU0saUJBS3hDO0lBRUQsMEdBQTBHO0lBQzFHLE9BQU8sQ0FBQyxpQkFBaUI7SUFLekIsT0FBTyxDQUFDLGlCQUFpQjtJQVF6QixPQUFPLENBQUMsb0JBQW9CO0NBRzdCIn0=
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash_offenses_collector.d.ts","sourceRoot":"","sources":["../src/slash_offenses_collector.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slash_offenses_collector.d.ts","sourceRoot":"","sources":["../src/slash_offenses_collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAGlE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,OAAO,EACb,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAC,CAAC;AACpG,MAAM,MAAM,8BAA8B,GAAG,QAAQ,CACnD,IAAI,CAAC,iBAAiB,EAAE,eAAe,CAAC,GAAG;IACzC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACtD,sHAAsH;IACtH,wBAAwB,EAAE,UAAU,CAAC;CACtC,CACF,CAAC;AAEF;;;;GAIG;AACH,qBAAa,sBAAsB;IAK/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsB;IACvD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IAExD,YACmB,MAAM,EAAE,4BAA4B,EACpC,QAAQ,EAAE,8BAA8B,EACxC,QAAQ,EAAE,OAAO,EAAE,EACnB,aAAa,EAAE,oBAAoB,EACnC,GAAG,yCAA6C,EAC/D;IAEG,KAAK,kBAmBX;IAEY,IAAI,kBAUhB;IAED;;;;OAIG;IACU,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,iBA+BrD;IAEY,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,EAAE,iBAW/D;IAED;;;OAGG;IACU,cAAc,CAAC,KAAK,EAAE,MAAM,iBAKxC;IAED,0GAA0G;IAC1G,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,oBAAoB;CAG7B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { SerialQueue } from '@aztec/foundation/queue';
|
|
3
|
+
import { getOffenseTypeName, getSlotForOffense } from '@aztec/stdlib/slashing';
|
|
4
|
+
import { WANT_TO_CLEAR_SLASH_EVENT, WANT_TO_SLASH_EVENT } from './watcher.js';
|
|
4
5
|
/**
|
|
5
6
|
* Collects and manages slashable offenses from watchers.
|
|
6
7
|
* This class handles the common logic for subscribing to slash watcher events,
|
|
@@ -12,6 +13,7 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
|
|
|
12
13
|
offensesStore;
|
|
13
14
|
log;
|
|
14
15
|
unwatchCallbacks;
|
|
16
|
+
storeMutationQueue;
|
|
15
17
|
constructor(config, settings, watchers, offensesStore, log = createLogger('slasher:offenses-collector')){
|
|
16
18
|
this.config = config;
|
|
17
19
|
this.settings = settings;
|
|
@@ -19,25 +21,30 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
|
|
|
19
21
|
this.offensesStore = offensesStore;
|
|
20
22
|
this.log = log;
|
|
21
23
|
this.unwatchCallbacks = [];
|
|
24
|
+
this.storeMutationQueue = new SerialQueue();
|
|
22
25
|
}
|
|
23
26
|
start() {
|
|
24
27
|
this.log.debug('Starting SlashOffensesCollector...');
|
|
25
|
-
|
|
28
|
+
this.storeMutationQueue.start();
|
|
29
|
+
// Subscribe to watcher slashing events.
|
|
26
30
|
for (const watcher of this.watchers){
|
|
27
|
-
const wantToSlashCallback = (args)=>
|
|
31
|
+
const wantToSlashCallback = (args)=>this.enqueueStoreMutation('wantToSlash', ()=>this.handleWantToSlash(args));
|
|
28
32
|
watcher.on(WANT_TO_SLASH_EVENT, wantToSlashCallback);
|
|
29
33
|
this.unwatchCallbacks.push(()=>watcher.removeListener(WANT_TO_SLASH_EVENT, wantToSlashCallback));
|
|
34
|
+
const wantToClearSlashCallback = (args)=>this.enqueueStoreMutation('wantToClearSlash', ()=>this.handleWantToClearSlash(args));
|
|
35
|
+
watcher.on(WANT_TO_CLEAR_SLASH_EVENT, wantToClearSlashCallback);
|
|
36
|
+
this.unwatchCallbacks.push(()=>watcher.removeListener(WANT_TO_CLEAR_SLASH_EVENT, wantToClearSlashCallback));
|
|
30
37
|
}
|
|
31
38
|
this.log.info('Started SlashOffensesCollector');
|
|
32
39
|
return Promise.resolve();
|
|
33
40
|
}
|
|
34
|
-
stop() {
|
|
41
|
+
async stop() {
|
|
35
42
|
this.log.debug('Stopping SlashOffensesCollector...');
|
|
36
43
|
for (const unwatchCallback of this.unwatchCallbacks){
|
|
37
44
|
unwatchCallback();
|
|
38
45
|
}
|
|
46
|
+
await this.storeMutationQueue.end();
|
|
39
47
|
this.log.info('SlashOffensesCollector stopped');
|
|
40
|
-
return Promise.resolve();
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Called when a slash watcher emits WANT_TO_SLASH_EVENT.
|
|
@@ -45,32 +52,40 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
|
|
|
45
52
|
* @param args - the arguments from the watcher, including the validators, amounts, and offenses
|
|
46
53
|
*/ async handleWantToSlash(args) {
|
|
47
54
|
for (const arg of args){
|
|
48
|
-
const
|
|
55
|
+
const offense = {
|
|
49
56
|
validator: arg.validator,
|
|
50
57
|
amount: arg.amount,
|
|
51
58
|
offenseType: arg.offenseType,
|
|
52
59
|
epochOrSlot: arg.epochOrSlot
|
|
53
60
|
};
|
|
54
|
-
if (this.shouldSkipOffense(
|
|
55
|
-
this.log.verbose('Skipping offense during grace period',
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (await this.offensesStore.hasOffense(pendingOffense)) {
|
|
59
|
-
this.log.debug('Skipping repeated offense', pendingOffense);
|
|
61
|
+
if (this.shouldSkipOffense(offense)) {
|
|
62
|
+
this.log.verbose('Skipping offense during grace period', this.getOffenseLogData(offense));
|
|
60
63
|
continue;
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
this.
|
|
65
|
+
const added = await this.offensesStore.addOffense(offense);
|
|
66
|
+
if (added) {
|
|
67
|
+
if (this.settings.slashingAmounts) {
|
|
68
|
+
const minSlash = this.settings.slashingAmounts[0];
|
|
69
|
+
if (arg.amount < minSlash) {
|
|
70
|
+
this.log.warn(`Offense amount ${arg.amount} is below minimum slashing amount ${minSlash}`, this.getOffenseLogData(offense));
|
|
71
|
+
}
|
|
66
72
|
}
|
|
73
|
+
this.log.info(`Adding pending offense for validator ${arg.validator}`, this.getOffenseLogData(offense));
|
|
74
|
+
} else {
|
|
75
|
+
this.log.debug('Skipping repeated offense', this.getOffenseLogData(offense));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async handleWantToClearSlash(args) {
|
|
80
|
+
for (const arg of args){
|
|
81
|
+
const cleared = await this.offensesStore.clearOffenses(arg);
|
|
82
|
+
if (cleared > 0) {
|
|
83
|
+
this.log.info(`Cleared ${cleared} pending offenses`, {
|
|
84
|
+
offenseType: getOffenseTypeName(arg.offenseType),
|
|
85
|
+
epochOrSlot: arg.epochOrSlot,
|
|
86
|
+
validators: arg.validators?.map((validator)=>validator.toString())
|
|
87
|
+
});
|
|
67
88
|
}
|
|
68
|
-
this.log.info(`Adding pending offense for validator ${arg.validator}`, {
|
|
69
|
-
...pendingOffense,
|
|
70
|
-
epochOrSlot: pendingOffense.epochOrSlot.toString(),
|
|
71
|
-
amount: pendingOffense.amount.toString()
|
|
72
|
-
});
|
|
73
|
-
await this.offensesStore.addPendingOffense(pendingOffense);
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
/**
|
|
@@ -82,17 +97,18 @@ import { WANT_TO_SLASH_EVENT } from './watcher.js';
|
|
|
82
97
|
this.log.debug(`Cleared ${cleared} expired offenses for round ${round}`);
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Marks offenses as slashed (no longer pending)
|
|
87
|
-
* @param offenses - The offenses to mark as slashed
|
|
88
|
-
*/ markAsSlashed(offenses) {
|
|
89
|
-
this.log.verbose(`Marking offenses as slashed`, {
|
|
90
|
-
offenses
|
|
91
|
-
});
|
|
92
|
-
return this.offensesStore.markAsSlashed(offenses);
|
|
93
|
-
}
|
|
94
|
-
/** Returns whether to skip an offense if it happened during the grace period at the beginning of the chain */ shouldSkipOffense(offense) {
|
|
100
|
+
/** Returns whether to skip an offense if it happened during the grace period after the network upgrade */ shouldSkipOffense(offense) {
|
|
95
101
|
const offenseSlot = getSlotForOffense(offense, this.settings);
|
|
96
|
-
return offenseSlot < this.config.slashGracePeriodL2Slots;
|
|
102
|
+
return offenseSlot < this.settings.rollupRegisteredAtL2Slot + this.config.slashGracePeriodL2Slots;
|
|
103
|
+
}
|
|
104
|
+
getOffenseLogData(offense) {
|
|
105
|
+
return {
|
|
106
|
+
...offense,
|
|
107
|
+
validator: offense.validator.toString(),
|
|
108
|
+
offenseType: getOffenseTypeName(offense.offenseType)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
enqueueStoreMutation(label, callback) {
|
|
112
|
+
void this.storeMutationQueue.put(callback).catch((err)=>this.log.error(`Error handling ${label}`, err));
|
|
97
113
|
}
|
|
98
114
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { EthAddress } from '@aztec/aztec.js/addresses';
|
|
2
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
+
import { RollupContract, SlasherContract, SlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import type { DateProvider } from '@aztec/foundation/timer';
|
|
6
|
+
import type { Prettify } from '@aztec/foundation/types';
|
|
7
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
8
|
+
import { type Offense, type ProposerSlashAction, type ProposerSlashActionProvider } from '@aztec/stdlib/slashing';
|
|
9
|
+
import type { Hex } from 'viem';
|
|
10
|
+
import { SlasherMetrics } from './metrics.js';
|
|
11
|
+
import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
|
|
12
|
+
import { SlashOffensesCollector, type SlashOffensesCollectorConfig, type SlashOffensesCollectorSettings } from './slash_offenses_collector.js';
|
|
13
|
+
import { SlashRoundMonitor, type SlashRoundMonitorSettings } from './slash_round_monitor.js';
|
|
14
|
+
import type { SlasherClientInterface } from './slasher_client_interface.js';
|
|
15
|
+
import type { SlasherOffensesStore } from './stores/offenses_store.js';
|
|
16
|
+
import type { Watcher } from './watcher.js';
|
|
17
|
+
/** Settings used in the slasher client, loaded from the L1 contracts during initialization */
|
|
18
|
+
export type SlasherSettings = Prettify<SlashRoundMonitorSettings & SlashOffensesCollectorSettings & {
|
|
19
|
+
slashingLifetimeInRounds: number;
|
|
20
|
+
slashingExecutionDelayInRounds: number;
|
|
21
|
+
slashingRoundSizeInEpochs: number;
|
|
22
|
+
slashingOffsetInRounds: number;
|
|
23
|
+
slashingQuorumSize: number;
|
|
24
|
+
slashingAmounts: [bigint, bigint, bigint];
|
|
25
|
+
/** Committee size for block proposal */
|
|
26
|
+
targetCommitteeSize: number;
|
|
27
|
+
}>;
|
|
28
|
+
export type SlasherClientConfig = SlashOffensesCollectorConfig & Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack' | 'slashMaxPayloadSize'>;
|
|
29
|
+
/**
|
|
30
|
+
* The Slasher client is responsible for managing slashable offenses using
|
|
31
|
+
* the consensus-based slashing model where proposers vote on individual validator offenses.
|
|
32
|
+
*
|
|
33
|
+
* The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
|
|
34
|
+
* proposer, it votes for which validators from past epochs should be slashed based on collected offenses.
|
|
35
|
+
* Voting is handled by the sequencer publisher, the slasher client does not interact with L1 directly.
|
|
36
|
+
* The client also monitors rounds and executes slashing when rounds become executable after reaching quorum.
|
|
37
|
+
*
|
|
38
|
+
* Voting and offense collection
|
|
39
|
+
* - Time is divided into rounds (ROUND_SIZE slots each). During each round, block proposers can submit votes
|
|
40
|
+
* indicating which validators from SLASH_OFFSET_IN_ROUNDS rounds ago should be slashed.
|
|
41
|
+
* - Votes are encoded as bytes where each validator's vote is represented by 2 bits indicating the slash amount (0-3 slash units)
|
|
42
|
+
* for the validator in the committee being slashed.
|
|
43
|
+
* - When gathering offenses for round N, the system looks at offenses from round N-2 (where 2 is the hardcoded
|
|
44
|
+
* offset), giving time to detect offenses and vote on them in a later round.
|
|
45
|
+
* - Each offense carries an epoch or block identifier to differentiate multiple offenses by the same validator.
|
|
46
|
+
*
|
|
47
|
+
* Quorum and execution
|
|
48
|
+
* - After a round ends, there is an execution delay period for review so the VETOER in the Slasher can veto
|
|
49
|
+
* if needed.
|
|
50
|
+
* - Once the delay passes, anyone can call executeRound() to tally votes and execute slashing.
|
|
51
|
+
* - Validators that reach the quorum threshold are slashed. A vote for slashing N units is also considered
|
|
52
|
+
* a vote for slashing N-1, N-2, ..., 1 units. The system slashes for the largest amount that reaches quorum.
|
|
53
|
+
* - The client monitors executable rounds and triggers execution when appropriate.
|
|
54
|
+
*/
|
|
55
|
+
export declare class SlasherClient implements ProposerSlashActionProvider, SlasherClientInterface {
|
|
56
|
+
private config;
|
|
57
|
+
private settings;
|
|
58
|
+
private slashingProposer;
|
|
59
|
+
private slasher;
|
|
60
|
+
private rollup;
|
|
61
|
+
private epochCache;
|
|
62
|
+
private dateProvider;
|
|
63
|
+
private offensesStore;
|
|
64
|
+
private readonly ownValidators;
|
|
65
|
+
private log;
|
|
66
|
+
private readonly metrics;
|
|
67
|
+
protected unwatchCallbacks: (() => void)[];
|
|
68
|
+
protected roundMonitor: SlashRoundMonitor;
|
|
69
|
+
protected offensesCollector: SlashOffensesCollector;
|
|
70
|
+
protected readonly ownValidatorMonitor: OwnValidatorSlashMonitor;
|
|
71
|
+
constructor(config: SlasherClientConfig, settings: SlasherSettings, slashingProposer: SlashingProposerContract, slasher: SlasherContract, rollup: RollupContract, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, offensesStore: SlasherOffensesStore, ownValidators?: EthAddress[], log?: import("@aztec/foundation/log").Logger, metrics?: SlasherMetrics);
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
/** Stop the slasher client */
|
|
74
|
+
stop(): Promise<void>;
|
|
75
|
+
/** Returns the current config */
|
|
76
|
+
getConfig(): SlasherConfig;
|
|
77
|
+
/** Update the config of the slasher client */
|
|
78
|
+
updateConfig(config: Partial<SlasherConfig>): void;
|
|
79
|
+
/** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */
|
|
80
|
+
protected handleNewRound(round: bigint): Promise<void>;
|
|
81
|
+
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */
|
|
82
|
+
protected handleRoundExecuted(round: bigint, slashCount: bigint, l1BlockHash: Hex): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Get the actions the proposer should take for slashing
|
|
85
|
+
* @param slotNumber - The current slot number
|
|
86
|
+
* @returns The actions to take
|
|
87
|
+
*/
|
|
88
|
+
getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
91
|
+
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
92
|
+
*/
|
|
93
|
+
protected getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined>;
|
|
94
|
+
private tryGetRoundExecuteAction;
|
|
95
|
+
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
96
|
+
protected getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined>;
|
|
97
|
+
/** Returns the committees that were active during the timespan of a given round */
|
|
98
|
+
private collectCommitteesActiveDuringRound;
|
|
99
|
+
/**
|
|
100
|
+
* Gather offenses to be slashed on a given round.
|
|
101
|
+
* Round N slashes validators from round N - slashOffsetInRounds.
|
|
102
|
+
* @param round - The round to get offenses for, defaults to current round
|
|
103
|
+
* @returns Array of pending offenses for the round with offset applied
|
|
104
|
+
*/
|
|
105
|
+
gatherOffensesForRound(round?: bigint): Promise<Offense[]>;
|
|
106
|
+
/** Returns all offenses stored */
|
|
107
|
+
getOffenses(): Promise<Offense[]>;
|
|
108
|
+
/**
|
|
109
|
+
* Returns the round to be slashed given the current round by applying the slash offset.
|
|
110
|
+
* During round N, we cannot slash the validators from the epochs of the same round, since the round is not over,
|
|
111
|
+
* and besides we would be asking the current validators to vote to slash themselves. So during round N we look at the
|
|
112
|
+
* epochs spanned during round N - SLASH_OFFSET_IN_ROUNDS. This offset means that the epochs we slash are complete,
|
|
113
|
+
* and also gives nodes time to detect any misbehavior (eg slashing for prunes requires the proof submission window to
|
|
114
|
+
* pass).
|
|
115
|
+
*/
|
|
116
|
+
private getSlashedRound;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlcl9jbGllbnQuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zbGFzaGVyX2NsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDdkQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDckQsT0FBTyxFQUFFLGNBQWMsRUFBRSxlQUFlLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV0RyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFHN0QsT0FBTyxLQUFLLEVBQUUsWUFBWSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDNUQsT0FBTyxLQUFLLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDeEQsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDckUsT0FBTyxFQUNMLEtBQUssT0FBTyxFQUVaLEtBQUssbUJBQW1CLEVBQ3hCLEtBQUssMkJBQTJCLEVBSWpDLE1BQU0sd0JBQXdCLENBQUM7QUFHaEMsT0FBTyxLQUFLLEVBQUUsR0FBRyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBRWhDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDOUMsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDNUUsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixLQUFLLDRCQUE0QixFQUNqQyxLQUFLLDhCQUE4QixFQUNwQyxNQUFNLCtCQUErQixDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxLQUFLLHlCQUF5QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDN0YsT0FBTyxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUM1RSxPQUFPLEtBQUssRUFBRSxvQkFBb0Is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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slasher_client.d.ts","sourceRoot":"","sources":["../src/slasher_client.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,11 +2,14 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
|
|
|
2
2
|
import { maxBigint } from '@aztec/foundation/bigint';
|
|
3
3
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
4
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
-
import { OffenseType, getEpochsForRound, getSlashConsensusVotesFromOffenses } from '@aztec/stdlib/slashing';
|
|
5
|
+
import { OffenseType, getEpochsForRound, getOffenseTypeName, getSlashConsensusVotesFromOffenses } from '@aztec/stdlib/slashing';
|
|
6
|
+
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
7
|
+
import { SlasherMetrics } from './metrics.js';
|
|
8
|
+
import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
|
|
6
9
|
import { SlashOffensesCollector } from './slash_offenses_collector.js';
|
|
7
10
|
import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
8
11
|
/**
|
|
9
|
-
* The
|
|
12
|
+
* The Slasher client is responsible for managing slashable offenses using
|
|
10
13
|
* the consensus-based slashing model where proposers vote on individual validator offenses.
|
|
11
14
|
*
|
|
12
15
|
* The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
|
|
@@ -30,60 +33,66 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
30
33
|
* - Validators that reach the quorum threshold are slashed. A vote for slashing N units is also considered
|
|
31
34
|
* a vote for slashing N-1, N-2, ..., 1 units. The system slashes for the largest amount that reaches quorum.
|
|
32
35
|
* - The client monitors executable rounds and triggers execution when appropriate.
|
|
33
|
-
|
|
34
|
-
* Differences from Empire model
|
|
35
|
-
* - No fixed slash payloads - votes are for individual validator offenses encoded in bytes
|
|
36
|
-
* - The L1 contract determines which offenses reach quorum rather than nodes agreeing on a payload
|
|
37
|
-
* - Proposers vote directly on which validators to slash and by how much
|
|
38
|
-
* - Uses a slash offset to vote on validators from past rounds (e.g., round N votes on round N-2)
|
|
39
|
-
*/ export class TallySlasherClient {
|
|
36
|
+
*/ export class SlasherClient {
|
|
40
37
|
config;
|
|
41
38
|
settings;
|
|
42
|
-
|
|
39
|
+
slashingProposer;
|
|
43
40
|
slasher;
|
|
44
41
|
rollup;
|
|
45
42
|
epochCache;
|
|
46
43
|
dateProvider;
|
|
47
44
|
offensesStore;
|
|
45
|
+
ownValidators;
|
|
48
46
|
log;
|
|
47
|
+
metrics;
|
|
49
48
|
unwatchCallbacks;
|
|
50
49
|
roundMonitor;
|
|
51
50
|
offensesCollector;
|
|
52
|
-
|
|
51
|
+
ownValidatorMonitor;
|
|
52
|
+
constructor(config, settings, slashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, ownValidators = [], log = createLogger('slasher:consensus'), metrics = new SlasherMetrics(getTelemetryClient())){
|
|
53
53
|
this.config = config;
|
|
54
54
|
this.settings = settings;
|
|
55
|
-
this.
|
|
55
|
+
this.slashingProposer = slashingProposer;
|
|
56
56
|
this.slasher = slasher;
|
|
57
57
|
this.rollup = rollup;
|
|
58
58
|
this.epochCache = epochCache;
|
|
59
59
|
this.dateProvider = dateProvider;
|
|
60
60
|
this.offensesStore = offensesStore;
|
|
61
|
+
this.ownValidators = ownValidators;
|
|
61
62
|
this.log = log;
|
|
63
|
+
this.metrics = metrics;
|
|
62
64
|
this.unwatchCallbacks = [];
|
|
63
65
|
this.roundMonitor = new SlashRoundMonitor(settings, dateProvider);
|
|
64
66
|
this.offensesCollector = new SlashOffensesCollector(config, settings, watchers, offensesStore);
|
|
67
|
+
this.ownValidatorMonitor = new OwnValidatorSlashMonitor(slashingProposer, settings, ownValidators, this.metrics);
|
|
65
68
|
}
|
|
66
69
|
async start() {
|
|
67
|
-
this.log.debug('Starting
|
|
70
|
+
this.log.debug('Starting slasher client...');
|
|
68
71
|
this.roundMonitor.start();
|
|
69
72
|
await this.offensesCollector.start();
|
|
70
73
|
// Listen for RoundExecuted events
|
|
71
|
-
this.unwatchCallbacks.push(this.
|
|
74
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToRoundExecuted(({ round, slashCount, l1BlockHash })=>void this.handleRoundExecuted(round, slashCount, l1BlockHash).catch((err)=>this.log.error('Error handling round executed', err))));
|
|
72
75
|
// Check for round changes
|
|
73
76
|
this.unwatchCallbacks.push(this.roundMonitor.listenToNewRound((round)=>this.handleNewRound(round)));
|
|
74
|
-
|
|
77
|
+
// Warn early when a slashing vote names one of our own validators. Skipped entirely when running none, so
|
|
78
|
+
// those nodes pay no extra L1 subscription. Subscribed only after the monitor's baseline read completes, so a
|
|
79
|
+
// vote cast in between is caught up by a later drain instead of being mistaken for a pre-startup vote.
|
|
80
|
+
if (this.ownValidators.length > 0) {
|
|
81
|
+
await this.ownValidatorMonitor.start(this.roundMonitor.getCurrentRound().round);
|
|
82
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToVoteCast(({ round })=>void this.ownValidatorMonitor.handleVoteCast(round)));
|
|
83
|
+
}
|
|
84
|
+
this.log.info(`Started slasher client`);
|
|
75
85
|
return Promise.resolve();
|
|
76
86
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
*/ async stop() {
|
|
80
|
-
this.log.debug('Stopping Tally Slasher client...');
|
|
87
|
+
/** Stop the slasher client */ async stop() {
|
|
88
|
+
this.log.debug('Stopping slasher client...');
|
|
81
89
|
for (const unwatchCallback of this.unwatchCallbacks){
|
|
82
90
|
unwatchCallback();
|
|
83
91
|
}
|
|
84
92
|
this.roundMonitor.stop();
|
|
93
|
+
await this.ownValidatorMonitor.stop();
|
|
85
94
|
await this.offensesCollector.stop();
|
|
86
|
-
this.log.info('
|
|
95
|
+
this.log.info('Slasher client stopped');
|
|
87
96
|
}
|
|
88
97
|
/** Returns the current config */ getConfig() {
|
|
89
98
|
return this.config;
|
|
@@ -95,14 +104,17 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
/** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */ async handleNewRound(round) {
|
|
98
|
-
this.log.info(`Starting new
|
|
107
|
+
this.log.info(`Starting new slashing round ${round}`);
|
|
108
|
+
this.ownValidatorMonitor.handleNewRound(round);
|
|
99
109
|
await this.offensesCollector.handleNewRound(round);
|
|
100
110
|
}
|
|
101
|
-
/** Called when we see a RoundExecuted event on the
|
|
111
|
+
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */ async handleRoundExecuted(round, slashCount, l1BlockHash) {
|
|
112
|
+
this.metrics.recordRoundExecuted();
|
|
102
113
|
const slashes = await this.rollup.getSlashEvents(l1BlockHash);
|
|
103
114
|
this.log.info(`Slashing round ${round} has been executed with ${slashCount} slashes`, {
|
|
104
115
|
slashes
|
|
105
116
|
});
|
|
117
|
+
this.ownValidatorMonitor.handleSlashes(round, slashes, l1BlockHash);
|
|
106
118
|
}
|
|
107
119
|
/**
|
|
108
120
|
* Get the actions the proposer should take for slashing
|
|
@@ -170,7 +182,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
170
182
|
};
|
|
171
183
|
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
172
184
|
try {
|
|
173
|
-
const roundInfo = await this.
|
|
185
|
+
const roundInfo = await this.slashingProposer.getRound(executableRound);
|
|
174
186
|
logData = {
|
|
175
187
|
...logData,
|
|
176
188
|
roundInfo
|
|
@@ -186,19 +198,19 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
186
198
|
return undefined;
|
|
187
199
|
}
|
|
188
200
|
// Check if round is ready to execute at the given slot
|
|
189
|
-
const isReadyToExecute = await this.
|
|
201
|
+
const isReadyToExecute = await this.slashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
|
|
190
202
|
if (!isReadyToExecute) {
|
|
191
203
|
this.log.warn(`Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`, logData);
|
|
192
204
|
return undefined;
|
|
193
205
|
}
|
|
194
206
|
// Check if the round yields any slashing at all
|
|
195
|
-
const { actions: slashActions, committees } = await this.
|
|
207
|
+
const { actions: slashActions, committees } = await this.slashingProposer.getTally(executableRound);
|
|
196
208
|
if (slashActions.length === 0) {
|
|
197
209
|
this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
|
|
198
210
|
return undefined;
|
|
199
211
|
}
|
|
200
212
|
// Check if the slash payload is vetoed
|
|
201
|
-
const payload = await this.
|
|
213
|
+
const payload = await this.slashingProposer.getPayload(executableRound);
|
|
202
214
|
const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
|
|
203
215
|
if (isVetoed) {
|
|
204
216
|
this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
|
|
@@ -256,7 +268,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
256
268
|
slotNumber,
|
|
257
269
|
currentRound,
|
|
258
270
|
slashedRound,
|
|
259
|
-
|
|
271
|
+
offensesFromAlwaysSlash: offensesFromAlwaysSlash.map(getOffenseLogData),
|
|
260
272
|
slashValidatorsAlways: this.config.slashValidatorsAlways
|
|
261
273
|
});
|
|
262
274
|
}
|
|
@@ -265,7 +277,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
265
277
|
slotNumber,
|
|
266
278
|
currentRound,
|
|
267
279
|
slashedRound,
|
|
268
|
-
offensesToForgive,
|
|
280
|
+
offensesToForgive: offensesToForgive.map(getOffenseLogData),
|
|
269
281
|
slashValidatorsNever: this.config.slashValidatorsNever
|
|
270
282
|
});
|
|
271
283
|
}
|
|
@@ -277,29 +289,36 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
277
289
|
});
|
|
278
290
|
return undefined;
|
|
279
291
|
}
|
|
280
|
-
|
|
281
|
-
...offense,
|
|
282
|
-
amount: offense.amount.toString()
|
|
283
|
-
}));
|
|
284
|
-
this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
|
|
292
|
+
this.log.debug(`Computing slash votes for ${offensesToSlash.length} offenses`, {
|
|
285
293
|
slotNumber,
|
|
286
294
|
currentRound,
|
|
287
295
|
slashedRound,
|
|
288
|
-
offensesToSlash:
|
|
296
|
+
offensesToSlash: offensesToSlash.map(getOffenseLogData)
|
|
289
297
|
});
|
|
290
298
|
const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
|
|
291
299
|
const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
|
|
292
|
-
const
|
|
300
|
+
const { slashMaxPayloadSize } = this.config;
|
|
301
|
+
const votes = getSlashConsensusVotesFromOffenses(offensesToSlash, committees, epochsForCommittees.map((e)=>BigInt(e)), {
|
|
302
|
+
...this.settings,
|
|
303
|
+
maxSlashedValidators: slashMaxPayloadSize
|
|
304
|
+
}, this.log);
|
|
293
305
|
if (votes.every((v)=>v === 0)) {
|
|
294
306
|
this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
|
|
295
307
|
slotNumber,
|
|
296
308
|
currentRound,
|
|
297
309
|
slashedRound,
|
|
298
|
-
offensesToSlash,
|
|
310
|
+
offensesToSlash: offensesToSlash.map(getOffenseLogData),
|
|
299
311
|
committees
|
|
300
312
|
});
|
|
301
313
|
return undefined;
|
|
302
314
|
}
|
|
315
|
+
this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
|
|
316
|
+
slotNumber,
|
|
317
|
+
slashedRound,
|
|
318
|
+
currentRound,
|
|
319
|
+
votes,
|
|
320
|
+
offensesToSlash: offensesToSlash.map(getOffenseLogData)
|
|
321
|
+
});
|
|
303
322
|
this.log.debug(`Computed votes for slashing ${offensesToSlash.length} offenses`, {
|
|
304
323
|
slashedRound,
|
|
305
324
|
currentRound,
|
|
@@ -320,14 +339,8 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
320
339
|
return Promise.all(epochsToSlash.map((epoch)=>this.epochCache.getCommitteeForEpoch(epoch).then((c)=>c.committee ?? emptyCommittee)));
|
|
321
340
|
}
|
|
322
341
|
/**
|
|
323
|
-
* Get slash payloads is NOT SUPPORTED in tally model
|
|
324
|
-
* @throws Error indicating this operation is not supported
|
|
325
|
-
*/ getSlashPayloads() {
|
|
326
|
-
return Promise.reject(new Error('Tally slashing model does not support slash payloads'));
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
342
|
* Gather offenses to be slashed on a given round.
|
|
330
|
-
*
|
|
343
|
+
* Round N slashes validators from round N - slashOffsetInRounds.
|
|
331
344
|
* @param round - The round to get offenses for, defaults to current round
|
|
332
345
|
* @returns Array of pending offenses for the round with offset applied
|
|
333
346
|
*/ async gatherOffensesForRound(round) {
|
|
@@ -337,8 +350,8 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
337
350
|
}
|
|
338
351
|
return await this.offensesStore.getOffensesForRound(targetRound);
|
|
339
352
|
}
|
|
340
|
-
/** Returns all
|
|
341
|
-
return this.offensesStore.
|
|
353
|
+
/** Returns all offenses stored */ getOffenses() {
|
|
354
|
+
return this.offensesStore.getOffenses();
|
|
342
355
|
}
|
|
343
356
|
/**
|
|
344
357
|
* Returns the round to be slashed given the current round by applying the slash offset.
|
|
@@ -352,3 +365,10 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
352
365
|
return round - BigInt(this.settings.slashingOffsetInRounds);
|
|
353
366
|
}
|
|
354
367
|
}
|
|
368
|
+
function getOffenseLogData(offense) {
|
|
369
|
+
return {
|
|
370
|
+
...offense,
|
|
371
|
+
validator: offense.validator.toString(),
|
|
372
|
+
offenseType: getOffenseTypeName(offense.offenseType)
|
|
373
|
+
};
|
|
374
|
+
}
|