@aztec/slasher 0.0.1-commit.24de95ac → 0.0.1-commit.2e2504e2
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/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +16 -16
- package/dest/empire_slasher_client.d.ts +6 -5
- package/dest/empire_slasher_client.d.ts.map +1 -1
- package/dest/empire_slasher_client.js +1 -9
- package/dest/factory/create_facade.d.ts +3 -2
- package/dest/factory/create_facade.d.ts.map +1 -1
- package/dest/factory/create_facade.js +1 -1
- package/dest/factory/create_implementation.d.ts +3 -3
- package/dest/factory/create_implementation.d.ts.map +1 -1
- package/dest/factory/get_settings.d.ts +2 -2
- package/dest/factory/get_settings.d.ts.map +1 -1
- package/dest/factory/index.d.ts +1 -1
- package/dest/generated/slasher-defaults.d.ts +19 -0
- package/dest/generated/slasher-defaults.d.ts.map +1 -0
- package/dest/generated/slasher-defaults.js +19 -0
- package/dest/index.d.ts +1 -1
- package/dest/null_slasher_client.d.ts +3 -2
- package/dest/null_slasher_client.d.ts.map +1 -1
- package/dest/slash_offenses_collector.d.ts +1 -1
- package/dest/slash_offenses_collector.d.ts.map +1 -1
- package/dest/slash_offenses_collector.js +5 -1
- package/dest/slash_round_monitor.d.ts +5 -4
- package/dest/slash_round_monitor.d.ts.map +1 -1
- package/dest/slasher_client_facade.d.ts +4 -3
- package/dest/slasher_client_facade.d.ts.map +1 -1
- package/dest/slasher_client_interface.d.ts +3 -2
- package/dest/slasher_client_interface.d.ts.map +1 -1
- package/dest/stores/offenses_store.d.ts +1 -1
- package/dest/stores/offenses_store.d.ts.map +1 -1
- package/dest/stores/offenses_store.js +4 -2
- package/dest/stores/payloads_store.d.ts +2 -2
- package/dest/stores/payloads_store.d.ts.map +1 -1
- package/dest/stores/payloads_store.js +6 -3
- package/dest/stores/schema_version.d.ts +1 -1
- package/dest/tally_slasher_client.d.ts +5 -9
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +11 -6
- package/dest/test/dummy_watcher.d.ts +1 -1
- package/dest/test/dummy_watcher.d.ts.map +1 -1
- package/dest/watcher.d.ts +1 -1
- package/dest/watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.d.ts +7 -6
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.js +42 -35
- package/dest/watchers/epoch_prune_watcher.d.ts +9 -7
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
- package/dest/watchers/epoch_prune_watcher.js +57 -17
- package/package.json +17 -14
- package/src/config.ts +17 -16
- package/src/empire_slasher_client.ts +6 -13
- package/src/factory/create_facade.ts +3 -2
- package/src/factory/create_implementation.ts +1 -1
- package/src/factory/get_settings.ts +1 -1
- package/src/generated/slasher-defaults.ts +21 -0
- package/src/null_slasher_client.ts +2 -1
- package/src/slash_offenses_collector.ts +5 -1
- package/src/slash_round_monitor.ts +3 -2
- package/src/slasher_client_facade.ts +3 -2
- package/src/slasher_client_interface.ts +2 -1
- package/src/stores/offenses_store.ts +4 -2
- package/src/stores/payloads_store.ts +7 -4
- package/src/tally_slasher_client.ts +21 -10
- package/src/watcher.ts +1 -1
- package/src/watchers/attestations_block_watcher.ts +59 -45
- package/src/watchers/epoch_prune_watcher.ts +88 -29
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { EmpireSlashingProposerContract, RollupContract, SlasherContract } from '@aztec/ethereum';
|
|
1
|
+
import { EmpireSlashingProposerContract, RollupContract, SlasherContract } from '@aztec/ethereum/contracts';
|
|
2
2
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
3
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
4
|
import { compactArray, filterAsync, maxBy, pick } from '@aztec/foundation/collection';
|
|
4
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
|
-
import { sleep } from '@aztec/foundation/sleep';
|
|
7
7
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
8
8
|
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
9
9
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
@@ -195,13 +195,6 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
195
195
|
this.roundMonitor.stop();
|
|
196
196
|
await this.offensesCollector.stop();
|
|
197
197
|
|
|
198
|
-
// Viem calls eth_uninstallFilter under the hood when uninstalling event watchers, but these calls are not awaited,
|
|
199
|
-
// meaning that any error that happens during the uninstallation will not be caught. This causes errors during jest teardowns,
|
|
200
|
-
// where we stop anvil after all other processes are stopped, so sometimes the eth_uninstallFilter call fails because anvil
|
|
201
|
-
// is already stopped. We add a sleep here to give the uninstallation some time to complete, but the proper fix is for
|
|
202
|
-
// viem to await the eth_uninstallFilter calls, or to catch any errors that happen during the uninstallation.
|
|
203
|
-
// See https://github.com/wevm/viem/issues/3714.
|
|
204
|
-
await sleep(2000);
|
|
205
198
|
this.log.info('Empire Slasher client stopped');
|
|
206
199
|
}
|
|
207
200
|
|
|
@@ -369,7 +362,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
369
362
|
* @param slotNumber - The current slot number
|
|
370
363
|
* @returns The actions to take
|
|
371
364
|
*/
|
|
372
|
-
public async getProposerActions(slotNumber:
|
|
365
|
+
public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
373
366
|
const [executeAction, proposePayloadActions] = await Promise.all([
|
|
374
367
|
this.getExecutePayloadAction(slotNumber),
|
|
375
368
|
this.getProposePayloadActions(slotNumber),
|
|
@@ -379,7 +372,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
379
372
|
}
|
|
380
373
|
|
|
381
374
|
/** Returns an execute payload action if there are any payloads ready to be executed */
|
|
382
|
-
protected async getExecutePayloadAction(slotNumber:
|
|
375
|
+
protected async getExecutePayloadAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
383
376
|
const { round } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
384
377
|
const toRemove: PayloadWithRound[] = [];
|
|
385
378
|
|
|
@@ -430,7 +423,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
430
423
|
}
|
|
431
424
|
|
|
432
425
|
/** Returns a vote or create payload action based on payload scoring */
|
|
433
|
-
protected async getProposePayloadActions(slotNumber:
|
|
426
|
+
protected async getProposePayloadActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
434
427
|
// Compute what round we are in based on the slot number
|
|
435
428
|
const { round, votingSlot } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
436
429
|
const { slashingRoundSize: roundSize, slashingQuorumSize: quorumSize } = this.settings;
|
|
@@ -473,7 +466,7 @@ export class EmpireSlasherClient implements ProposerSlashActionProvider, Slasher
|
|
|
473
466
|
// Find the best existing payload. We filter out those that have no chance of winning given how many voting
|
|
474
467
|
// slots are left in the round, and then filter by those we agree with.
|
|
475
468
|
const feasiblePayloads = existingPayloads.filter(
|
|
476
|
-
p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - votingSlot,
|
|
469
|
+
p => BigInt(quorumSize) - p.votes <= BigInt(roundSize) - BigInt(votingSlot),
|
|
477
470
|
);
|
|
478
471
|
const requiredOffenses = await this.getPendingUncontroversialOffensesForRound(round);
|
|
479
472
|
const agreedPayloads = await filterAsync(feasiblePayloads, p => this.agreeWithPayload(p, round, requiredOffenses));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import type { L1ReaderConfig, ViemClient } from '@aztec/ethereum';
|
|
3
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
|
|
4
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
5
|
import { unique } from '@aztec/foundation/collection';
|
|
5
6
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -30,7 +31,7 @@ export async function createSlasherFacade(
|
|
|
30
31
|
throw new Error('Cannot initialize SlasherClient without a Rollup address');
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const kvStore = await createStore('slasher', SCHEMA_VERSION, config,
|
|
34
|
+
const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
|
|
34
35
|
const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
|
|
35
36
|
|
|
36
37
|
const slashValidatorsNever = config.slashSelfAllowed
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import type { ViemClient } from '@aztec/ethereum';
|
|
3
2
|
import {
|
|
4
3
|
EmpireSlashingProposerContract,
|
|
5
4
|
RollupContract,
|
|
6
5
|
TallySlashingProposerContract,
|
|
7
6
|
} from '@aztec/ethereum/contracts';
|
|
7
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
8
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
9
|
import { createLogger } from '@aztec/foundation/log';
|
|
10
10
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Auto-generated from spartan/environments/network-defaults.yml
|
|
2
|
+
// Do not edit manually - run yarn generate to regenerate
|
|
3
|
+
|
|
4
|
+
/** Default slasher configuration values from network-defaults.yml */
|
|
5
|
+
export const slasherDefaultEnv = {
|
|
6
|
+
SLASH_MIN_PENALTY_PERCENTAGE: 0.5,
|
|
7
|
+
SLASH_MAX_PENALTY_PERCENTAGE: 2,
|
|
8
|
+
SLASH_OFFENSE_EXPIRATION_ROUNDS: 4,
|
|
9
|
+
SLASH_MAX_PAYLOAD_SIZE: 50,
|
|
10
|
+
SLASH_EXECUTE_ROUNDS_LOOK_BACK: 4,
|
|
11
|
+
SLASH_PRUNE_PENALTY: 10000000000000000000,
|
|
12
|
+
SLASH_DATA_WITHHOLDING_PENALTY: 10000000000000000000,
|
|
13
|
+
SLASH_INACTIVITY_TARGET_PERCENTAGE: 0.9,
|
|
14
|
+
SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD: 1,
|
|
15
|
+
SLASH_INACTIVITY_PENALTY: 10000000000000000000,
|
|
16
|
+
SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY: 10000000000000000000,
|
|
17
|
+
SLASH_ATTEST_DESCENDANT_OF_INVALID_PENALTY: 10000000000000000000,
|
|
18
|
+
SLASH_UNKNOWN_PENALTY: 10000000000000000000,
|
|
19
|
+
SLASH_INVALID_BLOCK_PENALTY: 10000000000000000000,
|
|
20
|
+
SLASH_GRACE_PERIOD_L2_SLOTS: 0,
|
|
21
|
+
} as const;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
2
3
|
|
|
3
4
|
import type { SlasherConfig } from './config.js';
|
|
@@ -30,7 +31,7 @@ export class NullSlasherClient implements SlasherClientInterface {
|
|
|
30
31
|
this.config = { ...this.config, ...config };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
public getProposerActions(_slotNumber:
|
|
34
|
+
public getProposerActions(_slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
34
35
|
return Promise.resolve([]);
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -85,7 +85,11 @@ export class SlashOffensesCollector {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
this.log.info(`Adding pending offense for validator ${arg.validator}`,
|
|
88
|
+
this.log.info(`Adding pending offense for validator ${arg.validator}`, {
|
|
89
|
+
...pendingOffense,
|
|
90
|
+
epochOrSlot: pendingOffense.epochOrSlot.toString(),
|
|
91
|
+
amount: pendingOffense.amount.toString(),
|
|
92
|
+
});
|
|
89
93
|
await this.offensesStore.addPendingOffense(pendingOffense);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
3
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
3
4
|
import type { Prettify } from '@aztec/foundation/types';
|
|
@@ -48,12 +49,12 @@ export class SlashRoundMonitor {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/** Returns the slashing round number and the voting slot within the round based on the L2 chain slot */
|
|
51
|
-
public getRoundForSlot(slotNumber:
|
|
52
|
+
public getRoundForSlot(slotNumber: SlotNumber): { round: bigint; votingSlot: SlotNumber } {
|
|
52
53
|
return getRoundForSlot(slotNumber, this.settings);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Returns the current slashing round and voting slot within the round */
|
|
56
|
-
public getCurrentRound(): { round: bigint; votingSlot:
|
|
57
|
+
public getCurrentRound(): { round: bigint; votingSlot: SlotNumber } {
|
|
57
58
|
const now = this.dateProvider.nowInSeconds();
|
|
58
59
|
const currentSlot = getSlotAtTimestamp(BigInt(now), this.settings);
|
|
59
60
|
return this.getRoundForSlot(currentSlot);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import type { ViemClient } from '@aztec/ethereum';
|
|
3
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
@@ -73,7 +74,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
|
|
|
73
74
|
return this.client?.getPendingOffenses() ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
public getProposerActions(slotNumber:
|
|
77
|
+
public getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
77
78
|
return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
1
2
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
2
3
|
import type { Offense, ProposerSlashAction, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
3
4
|
|
|
@@ -38,7 +39,7 @@ export interface SlasherClientInterface {
|
|
|
38
39
|
* @param slotNumber - The current slot number
|
|
39
40
|
* @returns The actions to take
|
|
40
41
|
*/
|
|
41
|
-
getProposerActions(slotNumber:
|
|
42
|
+
getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]>;
|
|
42
43
|
|
|
43
44
|
/** Returns the current config */
|
|
44
45
|
getConfig(): SlasherConfig;
|
|
@@ -76,9 +76,11 @@ export class SlasherOffensesStore {
|
|
|
76
76
|
/** Adds a new offense (defaults to pending, but will be slashed if markAsSlashed had been called for it) */
|
|
77
77
|
public async addPendingOffense(offense: Offense): Promise<void> {
|
|
78
78
|
const key = this.getOffenseKey(offense);
|
|
79
|
-
await this.offenses.set(key, serializeOffense(offense));
|
|
80
79
|
const round = getRoundForOffense(offense, this.settings);
|
|
81
|
-
await this.
|
|
80
|
+
await this.kvStore.transactionAsync(async () => {
|
|
81
|
+
await this.offenses.set(key, serializeOffense(offense));
|
|
82
|
+
await this.roundsOffenses.set(this.getRoundKey(round), key);
|
|
83
|
+
});
|
|
82
84
|
this.log.trace(`Adding pending offense ${key} for round ${round}`);
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -118,10 +118,13 @@ export class SlasherPayloadsStore {
|
|
|
118
118
|
|
|
119
119
|
public async incrementPayloadVotes(payloadAddress: EthAddress, round: bigint): Promise<bigint> {
|
|
120
120
|
const key = this.getPayloadVotesKey(round, payloadAddress);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
let newVotes: bigint;
|
|
122
|
+
await this.kvStore.transactionAsync(async () => {
|
|
123
|
+
const currentVotes = (await this.roundPayloadVotes.getAsync(key)) || 0n;
|
|
124
|
+
newVotes = currentVotes + 1n;
|
|
125
|
+
await this.roundPayloadVotes.set(key, newVotes);
|
|
126
|
+
});
|
|
127
|
+
return newVotes!;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
public async addPayload(payload: SlashPayloadRound): Promise<void> {
|
|
@@ -2,9 +2,9 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
|
|
|
2
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
3
|
import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
4
4
|
import { maxBigint } from '@aztec/foundation/bigint';
|
|
5
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
6
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
6
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
7
|
-
import { sleep } from '@aztec/foundation/sleep';
|
|
8
8
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
9
9
|
import type { Prettify } from '@aztec/foundation/types';
|
|
10
10
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
@@ -137,8 +137,6 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
137
137
|
this.roundMonitor.stop();
|
|
138
138
|
await this.offensesCollector.stop();
|
|
139
139
|
|
|
140
|
-
// Sleeping to sidestep viem issue with unwatching events
|
|
141
|
-
await sleep(2000);
|
|
142
140
|
this.log.info('Tally Slasher client stopped');
|
|
143
141
|
}
|
|
144
142
|
|
|
@@ -169,7 +167,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
169
167
|
* @param slotNumber - The current slot number
|
|
170
168
|
* @returns The actions to take
|
|
171
169
|
*/
|
|
172
|
-
public async getProposerActions(slotNumber:
|
|
170
|
+
public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
173
171
|
const [executeAction, voteAction] = await Promise.all([
|
|
174
172
|
this.getExecuteSlashAction(slotNumber),
|
|
175
173
|
this.getVoteOffensesAction(slotNumber),
|
|
@@ -182,7 +180,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
182
180
|
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
183
181
|
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
184
182
|
*/
|
|
185
|
-
protected async getExecuteSlashAction(slotNumber:
|
|
183
|
+
protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
186
184
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
187
185
|
const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
|
|
188
186
|
const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
|
|
@@ -233,7 +231,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
233
231
|
*/
|
|
234
232
|
private async tryGetRoundExecuteAction(
|
|
235
233
|
executableRound: bigint,
|
|
236
|
-
slotNumber:
|
|
234
|
+
slotNumber: SlotNumber,
|
|
237
235
|
): Promise<ProposerSlashAction | undefined> {
|
|
238
236
|
let logData: Record<string, unknown> = { executableRound, slotNumber };
|
|
239
237
|
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
@@ -280,8 +278,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
280
278
|
return undefined;
|
|
281
279
|
}
|
|
282
280
|
|
|
281
|
+
const slashActionsWithAmounts = slashActions.map(action => ({
|
|
282
|
+
validator: action.validator.toString(),
|
|
283
|
+
slashAmount: action.slashAmount.toString(),
|
|
284
|
+
}));
|
|
283
285
|
this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
|
|
284
|
-
slashActions,
|
|
286
|
+
slashActions: slashActionsWithAmounts,
|
|
285
287
|
payloadAddress: payload.address.toString(),
|
|
286
288
|
...logData,
|
|
287
289
|
});
|
|
@@ -302,7 +304,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
305
|
-
protected async getVoteOffensesAction(slotNumber:
|
|
307
|
+
protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
306
308
|
// Compute what round we are in based on the slot number and what round will be slashed
|
|
307
309
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
308
310
|
const slashedRound = this.getSlashedRound(currentRound);
|
|
@@ -347,16 +349,25 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
347
349
|
return undefined;
|
|
348
350
|
}
|
|
349
351
|
|
|
352
|
+
const offensesToSlashLog = offensesToSlash.map(offense => ({
|
|
353
|
+
...offense,
|
|
354
|
+
amount: offense.amount.toString(),
|
|
355
|
+
}));
|
|
350
356
|
this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
|
|
351
357
|
slotNumber,
|
|
352
358
|
currentRound,
|
|
353
359
|
slashedRound,
|
|
354
|
-
offensesToSlash,
|
|
360
|
+
offensesToSlash: offensesToSlashLog,
|
|
355
361
|
});
|
|
356
362
|
|
|
357
363
|
const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
|
|
358
364
|
const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
|
|
359
|
-
const votes = getSlashConsensusVotesFromOffenses(
|
|
365
|
+
const votes = getSlashConsensusVotesFromOffenses(
|
|
366
|
+
offensesToSlash,
|
|
367
|
+
committees,
|
|
368
|
+
epochsForCommittees.map(e => BigInt(e)),
|
|
369
|
+
this.settings,
|
|
370
|
+
);
|
|
360
371
|
if (votes.every(v => v === 0)) {
|
|
361
372
|
this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
|
|
362
373
|
slotNumber,
|
package/src/watcher.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface WantToSlashArgs {
|
|
|
10
10
|
validator: EthAddress;
|
|
11
11
|
amount: bigint;
|
|
12
12
|
offenseType: OffenseType;
|
|
13
|
-
epochOrSlot: bigint; // Epoch number for epoch-based offenses,
|
|
13
|
+
epochOrSlot: bigint; // Epoch number for epoch-based offenses, slot number for slot-based
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Event map for specific, known events of a watcher
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
3
|
import { merge, pick } from '@aztec/foundation/collection';
|
|
3
4
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import {
|
|
5
|
-
type
|
|
6
|
-
type L2BlockInfo,
|
|
6
|
+
type InvalidCheckpointDetectedEvent,
|
|
7
7
|
type L2BlockSourceEventEmitter,
|
|
8
8
|
L2BlockSourceEvents,
|
|
9
|
-
type
|
|
9
|
+
type ValidateCheckpointNegativeResult,
|
|
10
10
|
} from '@aztec/stdlib/block';
|
|
11
|
+
import type { CheckpointInfo } from '@aztec/stdlib/checkpoint';
|
|
11
12
|
import { OffenseType } from '@aztec/stdlib/slashing';
|
|
12
13
|
|
|
13
14
|
import EventEmitter from 'node:events';
|
|
@@ -31,19 +32,19 @@ type AttestationsBlockWatcherConfig = Pick<SlasherConfig, (typeof AttestationsBl
|
|
|
31
32
|
export class AttestationsBlockWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
|
|
32
33
|
private log: Logger = createLogger('attestations-block-watcher');
|
|
33
34
|
|
|
34
|
-
// Only keep track of the last N invalid
|
|
35
|
-
private
|
|
35
|
+
// Only keep track of the last N invalid checkpoints
|
|
36
|
+
private maxInvalidCheckpoints = 100;
|
|
36
37
|
|
|
37
38
|
// All invalid archive roots seen
|
|
38
39
|
private invalidArchiveRoots: Set<string> = new Set();
|
|
39
40
|
|
|
40
41
|
private config: AttestationsBlockWatcherConfig;
|
|
41
42
|
|
|
42
|
-
private
|
|
43
|
+
private boundHandleInvalidCheckpoint = (event: InvalidCheckpointDetectedEvent) => {
|
|
43
44
|
try {
|
|
44
|
-
this.
|
|
45
|
+
this.handleInvalidCheckpoint(event);
|
|
45
46
|
} catch (err) {
|
|
46
|
-
this.log.error('Error handling invalid
|
|
47
|
+
this.log.error('Error handling invalid checkpoint', err, {
|
|
47
48
|
...event.validationResult,
|
|
48
49
|
reason: event.validationResult.reason,
|
|
49
50
|
});
|
|
@@ -66,54 +67,61 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
public start() {
|
|
69
|
-
this.l2BlockSource.on(
|
|
70
|
+
this.l2BlockSource.events.on(
|
|
71
|
+
L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
|
|
72
|
+
this.boundHandleInvalidCheckpoint,
|
|
73
|
+
);
|
|
70
74
|
return Promise.resolve();
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
public stop() {
|
|
74
|
-
this.l2BlockSource.removeListener(
|
|
75
|
-
L2BlockSourceEvents.
|
|
76
|
-
this.
|
|
78
|
+
this.l2BlockSource.events.removeListener(
|
|
79
|
+
L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
|
|
80
|
+
this.boundHandleInvalidCheckpoint,
|
|
77
81
|
);
|
|
78
82
|
return Promise.resolve();
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
/** Event handler for invalid checkpoints as reported by the archiver. Public for testing purposes. */
|
|
86
|
+
public handleInvalidCheckpoint(event: InvalidCheckpointDetectedEvent): void {
|
|
82
87
|
const { validationResult } = event;
|
|
83
|
-
const
|
|
88
|
+
const checkpoint = validationResult.checkpoint;
|
|
84
89
|
|
|
85
|
-
// Check if we already have processed this
|
|
86
|
-
if (this.invalidArchiveRoots.has(
|
|
87
|
-
this.log.trace(`Already processed invalid
|
|
90
|
+
// Check if we already have processed this checkpoint, archiver may emit the same event multiple times
|
|
91
|
+
if (this.invalidArchiveRoots.has(checkpoint.archive.toString())) {
|
|
92
|
+
this.log.trace(`Already processed invalid checkpoint ${checkpoint.checkpointNumber}`);
|
|
88
93
|
return;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
this.log.verbose(`Detected invalid
|
|
92
|
-
...
|
|
96
|
+
this.log.verbose(`Detected invalid checkpoint ${checkpoint.checkpointNumber}`, {
|
|
97
|
+
...checkpoint,
|
|
93
98
|
reason: validationResult.valid === false ? validationResult.reason : 'unknown',
|
|
94
99
|
});
|
|
95
100
|
|
|
96
|
-
// Store the invalid
|
|
97
|
-
this.
|
|
101
|
+
// Store the invalid checkpoint
|
|
102
|
+
this.addInvalidCheckpoint(event.validationResult.checkpoint);
|
|
98
103
|
|
|
99
|
-
// Slash the proposer of the invalid
|
|
104
|
+
// Slash the proposer of the invalid checkpoint
|
|
100
105
|
this.slashProposer(event.validationResult);
|
|
101
106
|
|
|
102
|
-
// Check if the parent of this
|
|
107
|
+
// Check if the parent of this checkpoint is invalid as well, if so, we will slash its attestors as well
|
|
103
108
|
this.slashAttestorsOnAncestorInvalid(event.validationResult);
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
private slashAttestorsOnAncestorInvalid(validationResult:
|
|
107
|
-
const
|
|
111
|
+
private slashAttestorsOnAncestorInvalid(validationResult: ValidateCheckpointNegativeResult) {
|
|
112
|
+
const checkpoint = validationResult.checkpoint;
|
|
108
113
|
|
|
109
|
-
const parentArchive =
|
|
114
|
+
const parentArchive = checkpoint.lastArchive.toString();
|
|
110
115
|
if (this.invalidArchiveRoots.has(parentArchive)) {
|
|
111
116
|
const attestors = validationResult.attestors;
|
|
112
|
-
this.log.info(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
this.log.info(
|
|
118
|
+
`Want to slash attestors of checkpoint ${checkpoint.checkpointNumber} built on invalid checkpoint`,
|
|
119
|
+
{
|
|
120
|
+
...checkpoint,
|
|
121
|
+
...attestors,
|
|
122
|
+
parentArchive,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
117
125
|
|
|
118
126
|
this.emit(
|
|
119
127
|
WANT_TO_SLASH_EVENT,
|
|
@@ -121,20 +129,26 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
121
129
|
validator: attestor,
|
|
122
130
|
amount: this.config.slashAttestDescendantOfInvalidPenalty,
|
|
123
131
|
offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
|
|
124
|
-
epochOrSlot: BigInt(
|
|
132
|
+
epochOrSlot: BigInt(SlotNumber(checkpoint.slotNumber)),
|
|
125
133
|
})),
|
|
126
134
|
);
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
private slashProposer(validationResult:
|
|
131
|
-
const { reason,
|
|
132
|
-
const
|
|
133
|
-
const slot =
|
|
134
|
-
const
|
|
138
|
+
private slashProposer(validationResult: ValidateCheckpointNegativeResult) {
|
|
139
|
+
const { reason, checkpoint } = validationResult;
|
|
140
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
141
|
+
const slot = checkpoint.slotNumber;
|
|
142
|
+
const epochCommitteeInfo = {
|
|
143
|
+
committee: validationResult.committee,
|
|
144
|
+
seed: validationResult.seed,
|
|
145
|
+
epoch: validationResult.epoch,
|
|
146
|
+
isEscapeHatchOpen: false,
|
|
147
|
+
};
|
|
148
|
+
const proposer = this.epochCache.getProposerFromEpochCommittee(epochCommitteeInfo, slot);
|
|
135
149
|
|
|
136
150
|
if (!proposer) {
|
|
137
|
-
this.log.warn(`No proposer found for
|
|
151
|
+
this.log.warn(`No proposer found for checkpoint ${checkpointNumber} at slot ${slot}`);
|
|
138
152
|
return;
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -144,18 +158,18 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
144
158
|
validator: proposer,
|
|
145
159
|
amount,
|
|
146
160
|
offenseType: offense,
|
|
147
|
-
epochOrSlot: slot,
|
|
161
|
+
epochOrSlot: BigInt(slot),
|
|
148
162
|
};
|
|
149
163
|
|
|
150
|
-
this.log.info(`Want to slash proposer of
|
|
151
|
-
...
|
|
164
|
+
this.log.info(`Want to slash proposer of checkpoint ${checkpointNumber} due to ${reason}`, {
|
|
165
|
+
...checkpoint,
|
|
152
166
|
...args,
|
|
153
167
|
});
|
|
154
168
|
|
|
155
169
|
this.emit(WANT_TO_SLASH_EVENT, [args]);
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
private getOffenseFromInvalidationReason(reason:
|
|
172
|
+
private getOffenseFromInvalidationReason(reason: ValidateCheckpointNegativeResult['reason']): OffenseType {
|
|
159
173
|
switch (reason) {
|
|
160
174
|
case 'invalid-attestation':
|
|
161
175
|
return OffenseType.PROPOSED_INCORRECT_ATTESTATIONS;
|
|
@@ -168,11 +182,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
private
|
|
172
|
-
this.invalidArchiveRoots.add(
|
|
185
|
+
private addInvalidCheckpoint(checkpoint: CheckpointInfo) {
|
|
186
|
+
this.invalidArchiveRoots.add(checkpoint.archive.toString());
|
|
173
187
|
|
|
174
188
|
// Prune old entries if we exceed the maximum
|
|
175
|
-
if (this.invalidArchiveRoots.size > this.
|
|
189
|
+
if (this.invalidArchiveRoots.size > this.maxInvalidCheckpoints) {
|
|
176
190
|
const oldestKey = this.invalidArchiveRoots.keys().next().value!;
|
|
177
191
|
this.invalidArchiveRoots.delete(oldestKey);
|
|
178
192
|
}
|