@aztec/slasher 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.1
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 +60 -11
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -2
- package/dest/empire_slasher_client.d.ts +8 -6
- package/dest/empire_slasher_client.d.ts.map +1 -1
- package/dest/empire_slasher_client.js +11 -5
- package/dest/factory/create_facade.d.ts +3 -2
- package/dest/factory/create_facade.d.ts.map +1 -1
- package/dest/factory/create_implementation.d.ts +3 -3
- package/dest/factory/create_implementation.d.ts.map +1 -1
- package/dest/factory/create_implementation.js +8 -30
- 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 +2 -1
- package/dest/factory/index.d.ts.map +1 -1
- package/dest/factory/index.js +1 -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 +1 -2
- 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_facade.js +1 -0
- 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 +1 -1
- package/dest/stores/payloads_store.d.ts +2 -2
- package/dest/stores/payloads_store.d.ts.map +1 -1
- package/dest/stores/schema_version.d.ts +1 -1
- package/dest/tally_slasher_client.d.ts +14 -8
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +63 -11
- 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 +3 -1
- package/dest/watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.d.ts +6 -3
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.js +31 -21
- package/dest/watchers/epoch_prune_watcher.d.ts +8 -7
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
- package/dest/watchers/epoch_prune_watcher.js +48 -37
- package/package.json +13 -12
- package/src/config.ts +8 -2
- package/src/empire_slasher_client.ts +15 -8
- package/src/factory/create_facade.ts +2 -1
- package/src/factory/create_implementation.ts +9 -41
- package/src/factory/get_settings.ts +58 -0
- package/src/factory/index.ts +1 -0
- package/src/null_slasher_client.ts +2 -1
- package/src/slash_offenses_collector.ts +1 -2
- package/src/slash_round_monitor.ts +3 -2
- package/src/slasher_client_facade.ts +4 -2
- package/src/slasher_client_interface.ts +2 -1
- package/src/stores/offenses_store.ts +1 -1
- package/src/tally_slasher_client.ts +84 -16
- package/src/test/dummy_watcher.ts +21 -0
- package/src/watcher.ts +4 -1
- package/src/watchers/attestations_block_watcher.ts +38 -25
- package/src/watchers/epoch_prune_watcher.ts +67 -55
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { EthAddress } from '@aztec/aztec.js';
|
|
1
|
+
import { EthAddress } from '@aztec/aztec.js/addresses';
|
|
2
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
import { RollupContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { RollupContract, SlasherContract, TallySlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { maxBigint } from '@aztec/foundation/bigint';
|
|
5
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
6
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
5
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
8
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -45,7 +47,7 @@ export type TallySlasherSettings = Prettify<
|
|
|
45
47
|
>;
|
|
46
48
|
|
|
47
49
|
export type TallySlasherClientConfig = SlashOffensesCollectorConfig &
|
|
48
|
-
Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever'>;
|
|
50
|
+
Pick<SlasherConfig, 'slashValidatorsAlways' | 'slashValidatorsNever' | 'slashExecuteRoundsLookBack'>;
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* The Tally Slasher client is responsible for managing slashable offenses using
|
|
@@ -88,6 +90,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
88
90
|
private config: TallySlasherClientConfig,
|
|
89
91
|
private settings: TallySlasherSettings,
|
|
90
92
|
private tallySlashingProposer: TallySlashingProposerContract,
|
|
93
|
+
private slasher: SlasherContract,
|
|
91
94
|
private rollup: RollupContract,
|
|
92
95
|
watchers: Watcher[],
|
|
93
96
|
private epochCache: EpochCache,
|
|
@@ -167,7 +170,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
167
170
|
* @param slotNumber - The current slot number
|
|
168
171
|
* @returns The actions to take
|
|
169
172
|
*/
|
|
170
|
-
public async getProposerActions(slotNumber:
|
|
173
|
+
public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
171
174
|
const [executeAction, voteAction] = await Promise.all([
|
|
172
175
|
this.getExecuteSlashAction(slotNumber),
|
|
173
176
|
this.getVoteOffensesAction(slotNumber),
|
|
@@ -176,29 +179,91 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
176
179
|
return compactArray<ProposerSlashAction>([executeAction, voteAction]);
|
|
177
180
|
}
|
|
178
181
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
184
|
+
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
185
|
+
*/
|
|
186
|
+
protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
181
187
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
182
188
|
const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
|
|
183
189
|
const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
|
|
184
|
-
|
|
190
|
+
const lookBack = BigInt(this.config.slashExecuteRoundsLookBack);
|
|
191
|
+
const slashingLifetimeInRounds = BigInt(this.settings.slashingLifetimeInRounds);
|
|
192
|
+
|
|
193
|
+
// Compute the oldest executable round considering both lookBack and lifetimeInRounds
|
|
194
|
+
// A round is only executable if currentRound <= round + lifetimeInRounds
|
|
195
|
+
// So the oldest round we can execute is: currentRound - lifetimeInRounds
|
|
196
|
+
const oldestByLifetime = maxBigint(0n, currentRound - slashingLifetimeInRounds);
|
|
197
|
+
const oldestByLookBack = maxBigint(0n, executableRound - lookBack);
|
|
198
|
+
const oldestExecutableRound = maxBigint(oldestByLifetime, oldestByLookBack);
|
|
199
|
+
|
|
200
|
+
// Check if slashing is enabled at all
|
|
201
|
+
if (!(await this.slasher.isSlashingEnabled())) {
|
|
202
|
+
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
|
|
185
203
|
return undefined;
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
|
|
206
|
+
this.log.debug(`Checking slashing rounds ${oldestExecutableRound} to ${executableRound} to execute`, {
|
|
207
|
+
slotNumber,
|
|
208
|
+
currentRound,
|
|
209
|
+
oldestExecutableRound,
|
|
210
|
+
oldestByLifetime,
|
|
211
|
+
oldestByLookBack,
|
|
212
|
+
executableRound,
|
|
213
|
+
slashingExecutionDelayInRounds,
|
|
214
|
+
lookBack,
|
|
215
|
+
slashingLifetimeInRounds,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Iterate over all rounds, starting from the oldest, until we find one that is executable
|
|
219
|
+
for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
|
|
220
|
+
const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
|
|
221
|
+
if (action) {
|
|
222
|
+
return action;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// And return nothing if none are found
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Checks if a given round is executable and returns an execute-slash action for it if so.
|
|
232
|
+
* Assumes round number has already been checked against lifetime and execution delay.
|
|
233
|
+
* @param executableRound - The round to check for execution
|
|
234
|
+
*/
|
|
235
|
+
private async tryGetRoundExecuteAction(
|
|
236
|
+
executableRound: bigint,
|
|
237
|
+
slotNumber: SlotNumber,
|
|
238
|
+
): Promise<ProposerSlashAction | undefined> {
|
|
239
|
+
let logData: Record<string, unknown> = { executableRound, slotNumber };
|
|
240
|
+
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
241
|
+
|
|
189
242
|
try {
|
|
190
243
|
const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
|
|
244
|
+
logData = { ...logData, roundInfo };
|
|
191
245
|
if (roundInfo.isExecuted) {
|
|
192
246
|
this.log.verbose(`Round ${executableRound} has already been executed`, logData);
|
|
193
247
|
return undefined;
|
|
194
|
-
} else if (
|
|
195
|
-
this.log.
|
|
248
|
+
} else if (roundInfo.voteCount === 0n) {
|
|
249
|
+
this.log.debug(`Round ${executableRound} received no votes`, logData);
|
|
196
250
|
return undefined;
|
|
197
251
|
} else if (roundInfo.voteCount < this.settings.slashingQuorumSize) {
|
|
198
252
|
this.log.verbose(`Round ${executableRound} does not have enough votes to execute`, logData);
|
|
199
253
|
return undefined;
|
|
200
254
|
}
|
|
201
255
|
|
|
256
|
+
// Check if round is ready to execute at the given slot
|
|
257
|
+
const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
|
|
258
|
+
if (!isReadyToExecute) {
|
|
259
|
+
this.log.warn(
|
|
260
|
+
`Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`,
|
|
261
|
+
logData,
|
|
262
|
+
);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check if the round yields any slashing at all
|
|
202
267
|
const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
|
|
203
268
|
if (slashActions.length === 0) {
|
|
204
269
|
this.log.verbose(`Round ${executableRound} does not resolve in any slashing`, logData);
|
|
@@ -207,8 +272,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
207
272
|
|
|
208
273
|
// Check if the slash payload is vetoed
|
|
209
274
|
const payload = await this.tallySlashingProposer.getPayload(executableRound);
|
|
210
|
-
const
|
|
211
|
-
const isVetoed = await slasherContract.isPayloadVetoed(payload.address);
|
|
275
|
+
const isVetoed = await this.slasher.isPayloadVetoed(payload.address);
|
|
212
276
|
if (isVetoed) {
|
|
213
277
|
this.log.warn(`Round ${executableRound} payload is vetoed (skipping execution)`, {
|
|
214
278
|
payloadAddress: payload.address.toString(),
|
|
@@ -234,13 +298,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
234
298
|
return { type: 'execute-slash', round: executableRound, committees: slashedCommittees };
|
|
235
299
|
} catch (error) {
|
|
236
300
|
this.log.error(`Error checking round to execute ${executableRound}`, error);
|
|
301
|
+
return undefined;
|
|
237
302
|
}
|
|
238
|
-
|
|
239
|
-
return undefined;
|
|
240
303
|
}
|
|
241
304
|
|
|
242
305
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
243
|
-
protected async getVoteOffensesAction(slotNumber:
|
|
306
|
+
protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
244
307
|
// Compute what round we are in based on the slot number and what round will be slashed
|
|
245
308
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
246
309
|
const slashedRound = this.getSlashedRound(currentRound);
|
|
@@ -294,7 +357,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
294
357
|
|
|
295
358
|
const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
|
|
296
359
|
const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
|
|
297
|
-
const votes = getSlashConsensusVotesFromOffenses(
|
|
360
|
+
const votes = getSlashConsensusVotesFromOffenses(
|
|
361
|
+
offensesToSlash,
|
|
362
|
+
committees,
|
|
363
|
+
epochsForCommittees.map(e => BigInt(e)),
|
|
364
|
+
this.settings,
|
|
365
|
+
);
|
|
298
366
|
if (votes.every(v => v === 0)) {
|
|
299
367
|
this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
|
|
300
368
|
slotNumber,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
2
|
+
|
|
3
|
+
import EventEmitter from 'node:events';
|
|
4
|
+
|
|
5
|
+
import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
|
|
6
|
+
|
|
7
|
+
export class DummyWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
|
|
8
|
+
public updateConfig(_config: Partial<SlasherConfig>) {}
|
|
9
|
+
|
|
10
|
+
public start() {
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public stop() {
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public triggerSlash(args: WantToSlashArgs[]) {
|
|
19
|
+
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/watcher.ts
CHANGED
|
@@ -2,13 +2,15 @@ import { EthAddress } from '@aztec/foundation/eth-address';
|
|
|
2
2
|
import type { TypedEventEmitter } from '@aztec/foundation/types';
|
|
3
3
|
import { OffenseType } from '@aztec/stdlib/slashing';
|
|
4
4
|
|
|
5
|
+
import type { SlasherConfig } from './config.js';
|
|
6
|
+
|
|
5
7
|
export const WANT_TO_SLASH_EVENT = 'want-to-slash' as const;
|
|
6
8
|
|
|
7
9
|
export interface WantToSlashArgs {
|
|
8
10
|
validator: EthAddress;
|
|
9
11
|
amount: bigint;
|
|
10
12
|
offenseType: OffenseType;
|
|
11
|
-
epochOrSlot: bigint; // Epoch number for epoch-based offenses,
|
|
13
|
+
epochOrSlot: bigint; // Epoch number for epoch-based offenses, slot number for slot-based
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
// Event map for specific, known events of a watcher
|
|
@@ -21,4 +23,5 @@ export type WatcherEmitter = TypedEventEmitter<WatcherEventMap>;
|
|
|
21
23
|
export type Watcher = WatcherEmitter & {
|
|
22
24
|
start?: () => Promise<void>;
|
|
23
25
|
stop?: () => Promise<void>;
|
|
26
|
+
updateConfig: (config: Partial<SlasherConfig>) => void;
|
|
24
27
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { merge, pick } from '@aztec/foundation/collection';
|
|
2
4
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
3
5
|
import {
|
|
4
6
|
type InvalidBlockDetectedEvent,
|
|
7
|
+
type L2BlockInfo,
|
|
5
8
|
type L2BlockSourceEventEmitter,
|
|
6
9
|
L2BlockSourceEvents,
|
|
7
|
-
PublishedL2Block,
|
|
8
10
|
type ValidateBlockNegativeResult,
|
|
9
11
|
} from '@aztec/stdlib/block';
|
|
10
12
|
import { OffenseType } from '@aztec/stdlib/slashing';
|
|
@@ -14,6 +16,13 @@ import EventEmitter from 'node:events';
|
|
|
14
16
|
import type { SlasherConfig } from '../config.js';
|
|
15
17
|
import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
|
|
16
18
|
|
|
19
|
+
const AttestationsBlockWatcherConfigKeys = [
|
|
20
|
+
'slashAttestDescendantOfInvalidPenalty',
|
|
21
|
+
'slashProposeInvalidAttestationsPenalty',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
type AttestationsBlockWatcherConfig = Pick<SlasherConfig, (typeof AttestationsBlockWatcherConfigKeys)[number]>;
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* This watcher is responsible for detecting invalid blocks and creating slashing arguments for offenders.
|
|
19
28
|
* An invalid block is one that doesn't have enough attestations or has incorrect attestations.
|
|
@@ -29,13 +38,14 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
29
38
|
// All invalid archive roots seen
|
|
30
39
|
private invalidArchiveRoots: Set<string> = new Set();
|
|
31
40
|
|
|
41
|
+
private config: AttestationsBlockWatcherConfig;
|
|
42
|
+
|
|
32
43
|
private boundHandleInvalidBlock = (event: InvalidBlockDetectedEvent) => {
|
|
33
44
|
try {
|
|
34
45
|
this.handleInvalidBlock(event);
|
|
35
46
|
} catch (err) {
|
|
36
47
|
this.log.error('Error handling invalid block', err, {
|
|
37
|
-
...event.validationResult
|
|
38
|
-
...event.validationResult.block.l1,
|
|
48
|
+
...event.validationResult,
|
|
39
49
|
reason: event.validationResult.reason,
|
|
40
50
|
});
|
|
41
51
|
}
|
|
@@ -44,13 +54,16 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
44
54
|
constructor(
|
|
45
55
|
private l2BlockSource: L2BlockSourceEventEmitter,
|
|
46
56
|
private epochCache: EpochCache,
|
|
47
|
-
|
|
48
|
-
SlasherConfig,
|
|
49
|
-
'slashAttestDescendantOfInvalidPenalty' | 'slashProposeInvalidAttestationsPenalty'
|
|
50
|
-
>,
|
|
57
|
+
config: AttestationsBlockWatcherConfig,
|
|
51
58
|
) {
|
|
52
59
|
super();
|
|
53
|
-
this.
|
|
60
|
+
this.config = pick(config, ...AttestationsBlockWatcherConfigKeys);
|
|
61
|
+
this.log.info('AttestationsBlockWatcher initialized');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public updateConfig(newConfig: Partial<AttestationsBlockWatcherConfig>) {
|
|
65
|
+
this.config = merge(this.config, pick(newConfig, ...AttestationsBlockWatcherConfigKeys));
|
|
66
|
+
this.log.verbose('AttestationsBlockWatcher config updated', this.config);
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
public start() {
|
|
@@ -68,16 +81,16 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
68
81
|
|
|
69
82
|
private handleInvalidBlock(event: InvalidBlockDetectedEvent): void {
|
|
70
83
|
const { validationResult } = event;
|
|
71
|
-
const block = validationResult.block
|
|
84
|
+
const block = validationResult.block;
|
|
72
85
|
|
|
73
86
|
// Check if we already have processed this block, archiver may emit the same event multiple times
|
|
74
|
-
if (this.invalidArchiveRoots.has(block.archive.
|
|
75
|
-
this.log.trace(`Already processed invalid block ${block.
|
|
87
|
+
if (this.invalidArchiveRoots.has(block.archive.toString())) {
|
|
88
|
+
this.log.trace(`Already processed invalid block ${block.blockNumber}`);
|
|
76
89
|
return;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
this.log.verbose(`Detected invalid block ${block.
|
|
80
|
-
...block
|
|
92
|
+
this.log.verbose(`Detected invalid block ${block.blockNumber}`, {
|
|
93
|
+
...block,
|
|
81
94
|
reason: validationResult.valid === false ? validationResult.reason : 'unknown',
|
|
82
95
|
});
|
|
83
96
|
|
|
@@ -94,11 +107,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
94
107
|
private slashAttestorsOnAncestorInvalid(validationResult: ValidateBlockNegativeResult) {
|
|
95
108
|
const block = validationResult.block;
|
|
96
109
|
|
|
97
|
-
const parentArchive = block.
|
|
98
|
-
if (this.invalidArchiveRoots.has(
|
|
99
|
-
const attestors = validationResult.
|
|
100
|
-
this.log.info(`Want to slash attestors of block ${block.
|
|
101
|
-
...block
|
|
110
|
+
const parentArchive = block.lastArchive.toString();
|
|
111
|
+
if (this.invalidArchiveRoots.has(parentArchive)) {
|
|
112
|
+
const attestors = validationResult.attestors;
|
|
113
|
+
this.log.info(`Want to slash attestors of block ${block.blockNumber} built on invalid block`, {
|
|
114
|
+
...block,
|
|
102
115
|
...attestors,
|
|
103
116
|
parentArchive,
|
|
104
117
|
});
|
|
@@ -109,7 +122,7 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
109
122
|
validator: attestor,
|
|
110
123
|
amount: this.config.slashAttestDescendantOfInvalidPenalty,
|
|
111
124
|
offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
|
|
112
|
-
epochOrSlot: block.
|
|
125
|
+
epochOrSlot: BigInt(SlotNumber(block.slotNumber)),
|
|
113
126
|
})),
|
|
114
127
|
);
|
|
115
128
|
}
|
|
@@ -117,8 +130,8 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
117
130
|
|
|
118
131
|
private slashProposer(validationResult: ValidateBlockNegativeResult) {
|
|
119
132
|
const { reason, block } = validationResult;
|
|
120
|
-
const blockNumber = block.
|
|
121
|
-
const slot = block.
|
|
133
|
+
const blockNumber = block.blockNumber;
|
|
134
|
+
const slot = block.slotNumber;
|
|
122
135
|
const proposer = this.epochCache.getProposerFromEpochCommittee(validationResult, slot);
|
|
123
136
|
|
|
124
137
|
if (!proposer) {
|
|
@@ -132,11 +145,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
132
145
|
validator: proposer,
|
|
133
146
|
amount,
|
|
134
147
|
offenseType: offense,
|
|
135
|
-
epochOrSlot:
|
|
148
|
+
epochOrSlot: BigInt(slot),
|
|
136
149
|
};
|
|
137
150
|
|
|
138
151
|
this.log.info(`Want to slash proposer of block ${blockNumber} due to ${reason}`, {
|
|
139
|
-
...block
|
|
152
|
+
...block,
|
|
140
153
|
...args,
|
|
141
154
|
});
|
|
142
155
|
|
|
@@ -156,8 +169,8 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
|
|
159
|
-
private addInvalidBlock(block:
|
|
160
|
-
this.invalidArchiveRoots.add(block.
|
|
172
|
+
private addInvalidBlock(block: L2BlockInfo) {
|
|
173
|
+
this.invalidArchiveRoots.add(block.archive.toString());
|
|
161
174
|
|
|
162
175
|
// Prune old entries if we exceed the maximum
|
|
163
176
|
if (this.invalidArchiveRoots.size > this.maxInvalidBlocks) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Tx } from '@aztec/aztec.js';
|
|
2
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { merge, pick } from '@aztec/foundation/collection';
|
|
3
4
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import {
|
|
5
6
|
EthAddress,
|
|
@@ -8,9 +9,15 @@ import {
|
|
|
8
9
|
type L2BlockSourceEventEmitter,
|
|
9
10
|
L2BlockSourceEvents,
|
|
10
11
|
} from '@aztec/stdlib/block';
|
|
11
|
-
import
|
|
12
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
|
+
import type {
|
|
14
|
+
IFullNodeBlockBuilder,
|
|
15
|
+
ITxProvider,
|
|
16
|
+
MerkleTreeWriteOperations,
|
|
17
|
+
SlasherConfig,
|
|
18
|
+
} from '@aztec/stdlib/interfaces/server';
|
|
12
19
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
13
|
-
import { OffenseType } from '@aztec/stdlib/slashing';
|
|
20
|
+
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
14
21
|
import {
|
|
15
22
|
ReExFailedTxsError,
|
|
16
23
|
ReExStateMismatchError,
|
|
@@ -22,10 +29,9 @@ import EventEmitter from 'node:events';
|
|
|
22
29
|
|
|
23
30
|
import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '../watcher.js';
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
32
|
+
const EpochPruneWatcherPenaltiesConfigKeys = ['slashPrunePenalty', 'slashDataWithholdingPenalty'] as const;
|
|
33
|
+
|
|
34
|
+
type EpochPruneWatcherPenalties = Pick<SlasherConfig, (typeof EpochPruneWatcherPenaltiesConfigKeys)[number]>;
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* This watcher is responsible for detecting chain prunes and creating slashing arguments for the committee.
|
|
@@ -39,15 +45,18 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
39
45
|
// Store bound function reference for proper listener removal
|
|
40
46
|
private boundHandlePruneL2Blocks = this.handlePruneL2Blocks.bind(this);
|
|
41
47
|
|
|
48
|
+
private penalties: EpochPruneWatcherPenalties;
|
|
49
|
+
|
|
42
50
|
constructor(
|
|
43
51
|
private l2BlockSource: L2BlockSourceEventEmitter,
|
|
44
52
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
45
53
|
private epochCache: EpochCache,
|
|
46
54
|
private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
|
|
47
55
|
private blockBuilder: IFullNodeBlockBuilder,
|
|
48
|
-
|
|
56
|
+
penalties: EpochPruneWatcherPenalties,
|
|
49
57
|
) {
|
|
50
58
|
super();
|
|
59
|
+
this.penalties = pick(penalties, ...EpochPruneWatcherPenaltiesConfigKeys);
|
|
51
60
|
this.log.verbose(
|
|
52
61
|
`EpochPruneWatcher initialized with penalties: valid epoch pruned=${penalties.slashPrunePenalty} data withholding=${penalties.slashDataWithholdingPenalty}`,
|
|
53
62
|
);
|
|
@@ -63,56 +72,58 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
63
72
|
return Promise.resolve();
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
public updateConfig(config: Partial<SlasherConfig>): void {
|
|
76
|
+
this.penalties = merge(this.penalties, pick(config, ...EpochPruneWatcherPenaltiesConfigKeys));
|
|
77
|
+
this.log.verbose('EpochPruneWatcher config updated', this.penalties);
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
private handlePruneL2Blocks(event: L2BlockPruneEvent): void {
|
|
67
81
|
const { blocks, epochNumber } = event;
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// This can happen if we fail to get the validators for the epoch.
|
|
107
|
-
this.log.error('Error while creating slash for epoch', error);
|
|
108
|
-
});
|
|
82
|
+
void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
|
|
83
|
+
this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async emitSlashForEpoch(offense: OffenseType, epochNumber: EpochNumber): Promise<void> {
|
|
88
|
+
const validators = await this.getValidatorsForEpoch(epochNumber);
|
|
89
|
+
if (validators.length === 0) {
|
|
90
|
+
this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
|
|
94
|
+
this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
|
|
95
|
+
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
101
|
+
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber);
|
|
102
|
+
this.log.info(
|
|
103
|
+
`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
|
|
104
|
+
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await this.validateBlocks(epochBlocks);
|
|
108
|
+
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
|
|
109
|
+
await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof TransactionsNotAvailableError) {
|
|
112
|
+
this.log.info(`Data for pruned epoch ${epochNumber} was not available. Will want to slash.`, {
|
|
113
|
+
message: error.message,
|
|
114
|
+
});
|
|
115
|
+
await this.emitSlashForEpoch(OffenseType.DATA_WITHHOLDING, epochNumber);
|
|
116
|
+
} else {
|
|
117
|
+
this.log.error(`Error while validating pruned epoch ${epochNumber}. Will not want to slash.`, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
public async validateBlocks(blocks: L2Block[]): Promise<void> {
|
|
112
123
|
if (blocks.length === 0) {
|
|
113
124
|
return;
|
|
114
125
|
}
|
|
115
|
-
const fork = await this.blockBuilder.getFork(blocks[0].header.globalVariables.blockNumber - 1);
|
|
126
|
+
const fork = await this.blockBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
|
|
116
127
|
try {
|
|
117
128
|
for (const block of blocks) {
|
|
118
129
|
await this.validateBlock(block, fork);
|
|
@@ -134,9 +145,10 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
134
145
|
throw new TransactionsNotAvailableError(missingTxs);
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
const
|
|
148
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
|
|
149
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
138
150
|
const { block, failedTxs, numTxs } = await this.blockBuilder.buildBlock(
|
|
139
|
-
txs
|
|
151
|
+
txs,
|
|
140
152
|
l1ToL2Messages,
|
|
141
153
|
blockFromL1.header.globalVariables,
|
|
142
154
|
{},
|
|
@@ -154,7 +166,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
private async getValidatorsForEpoch(epochNumber:
|
|
169
|
+
private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
|
|
158
170
|
const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
|
|
159
171
|
if (!committee) {
|
|
160
172
|
this.log.trace(`No committee found for epoch ${epochNumber}`);
|
|
@@ -166,7 +178,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
166
178
|
private validatorsToSlashingArgs(
|
|
167
179
|
validators: EthAddress[],
|
|
168
180
|
offenseType: OffenseType,
|
|
169
|
-
epochOrSlot:
|
|
181
|
+
epochOrSlot: EpochNumber,
|
|
170
182
|
): WantToSlashArgs[] {
|
|
171
183
|
const penalty =
|
|
172
184
|
offenseType === OffenseType.DATA_WITHHOLDING
|
|
@@ -176,7 +188,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
176
188
|
validator: v,
|
|
177
189
|
amount: penalty,
|
|
178
190
|
offenseType,
|
|
179
|
-
epochOrSlot,
|
|
191
|
+
epochOrSlot: BigInt(epochOrSlot),
|
|
180
192
|
}));
|
|
181
193
|
}
|
|
182
194
|
}
|