@aztec/slasher 0.0.1-fake-ceab37513c → 0.0.6-commit.a2d1860fe9
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 +73 -14
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +28 -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 +21 -0
- package/dest/generated/slasher-defaults.d.ts.map +1 -0
- package/dest/generated/slasher-defaults.js +21 -0
- package/dest/index.d.ts +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_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 +5 -3
- 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 +6 -10
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +22 -15
- 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 +58 -17
- package/package.json +17 -14
- package/src/config.ts +30 -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 +23 -0
- package/src/null_slasher_client.ts +2 -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 +5 -3
- package/src/stores/payloads_store.ts +7 -4
- package/src/tally_slasher_client.ts +37 -19
- package/src/watcher.ts +1 -1
- package/src/watchers/attestations_block_watcher.ts +59 -45
- package/src/watchers/epoch_prune_watcher.ts +90 -29
|
@@ -1,10 +1,10 @@
|
|
|
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
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;
|
|
@@ -216,7 +214,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
216
214
|
|
|
217
215
|
// Iterate over all rounds, starting from the oldest, until we find one that is executable
|
|
218
216
|
for (let roundToCheck = oldestExecutableRound; roundToCheck <= executableRound; roundToCheck++) {
|
|
219
|
-
const action = await this.tryGetRoundExecuteAction(roundToCheck);
|
|
217
|
+
const action = await this.tryGetRoundExecuteAction(roundToCheck, slotNumber);
|
|
220
218
|
if (action) {
|
|
221
219
|
return action;
|
|
222
220
|
}
|
|
@@ -231,17 +229,14 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
231
229
|
* Assumes round number has already been checked against lifetime and execution delay.
|
|
232
230
|
* @param executableRound - The round to check for execution
|
|
233
231
|
*/
|
|
234
|
-
private async tryGetRoundExecuteAction(
|
|
235
|
-
|
|
232
|
+
private async tryGetRoundExecuteAction(
|
|
233
|
+
executableRound: bigint,
|
|
234
|
+
slotNumber: SlotNumber,
|
|
235
|
+
): Promise<ProposerSlashAction | undefined> {
|
|
236
|
+
let logData: Record<string, unknown> = { executableRound, slotNumber };
|
|
236
237
|
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
237
238
|
|
|
238
239
|
try {
|
|
239
|
-
// Check if slashing is enabled at all
|
|
240
|
-
if (!(await this.slasher.isSlashingEnabled())) {
|
|
241
|
-
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`, logData);
|
|
242
|
-
return undefined;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
240
|
const roundInfo = await this.tallySlashingProposer.getRound(executableRound);
|
|
246
241
|
logData = { ...logData, roundInfo };
|
|
247
242
|
if (roundInfo.isExecuted) {
|
|
@@ -255,6 +250,16 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
255
250
|
return undefined;
|
|
256
251
|
}
|
|
257
252
|
|
|
253
|
+
// Check if round is ready to execute at the given slot
|
|
254
|
+
const isReadyToExecute = await this.tallySlashingProposer.isRoundReadyToExecute(executableRound, slotNumber);
|
|
255
|
+
if (!isReadyToExecute) {
|
|
256
|
+
this.log.warn(
|
|
257
|
+
`Round ${executableRound} is not ready to execute at slot ${slotNumber} according to contract check`,
|
|
258
|
+
logData,
|
|
259
|
+
);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
258
263
|
// Check if the round yields any slashing at all
|
|
259
264
|
const { actions: slashActions, committees } = await this.tallySlashingProposer.getTally(executableRound);
|
|
260
265
|
if (slashActions.length === 0) {
|
|
@@ -273,8 +278,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
273
278
|
return undefined;
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
const slashActionsWithAmounts = slashActions.map(action => ({
|
|
282
|
+
validator: action.validator.toString(),
|
|
283
|
+
slashAmount: action.slashAmount.toString(),
|
|
284
|
+
}));
|
|
276
285
|
this.log.info(`Round ${executableRound} is ready to execute with ${slashActions.length} slashes`, {
|
|
277
|
-
slashActions,
|
|
286
|
+
slashActions: slashActionsWithAmounts,
|
|
278
287
|
payloadAddress: payload.address.toString(),
|
|
279
288
|
...logData,
|
|
280
289
|
});
|
|
@@ -295,7 +304,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
298
|
-
protected async getVoteOffensesAction(slotNumber:
|
|
307
|
+
protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
299
308
|
// Compute what round we are in based on the slot number and what round will be slashed
|
|
300
309
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
301
310
|
const slashedRound = this.getSlashedRound(currentRound);
|
|
@@ -340,16 +349,25 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
340
349
|
return undefined;
|
|
341
350
|
}
|
|
342
351
|
|
|
352
|
+
const offensesToSlashLog = offensesToSlash.map(offense => ({
|
|
353
|
+
...offense,
|
|
354
|
+
amount: offense.amount.toString(),
|
|
355
|
+
}));
|
|
343
356
|
this.log.info(`Voting to slash ${offensesToSlash.length} offenses`, {
|
|
344
357
|
slotNumber,
|
|
345
358
|
currentRound,
|
|
346
359
|
slashedRound,
|
|
347
|
-
offensesToSlash,
|
|
360
|
+
offensesToSlash: offensesToSlashLog,
|
|
348
361
|
});
|
|
349
362
|
|
|
350
363
|
const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
|
|
351
364
|
const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
|
|
352
|
-
const votes = getSlashConsensusVotesFromOffenses(
|
|
365
|
+
const votes = getSlashConsensusVotesFromOffenses(
|
|
366
|
+
offensesToSlash,
|
|
367
|
+
committees,
|
|
368
|
+
epochsForCommittees.map(e => BigInt(e)),
|
|
369
|
+
this.settings,
|
|
370
|
+
);
|
|
353
371
|
if (votes.every(v => v === 0)) {
|
|
354
372
|
this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
|
|
355
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
|
}
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import {
|
|
2
|
+
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { chunkBy, merge, pick } from '@aztec/foundation/collection';
|
|
4
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
5
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
6
|
import {
|
|
5
7
|
EthAddress,
|
|
6
8
|
L2Block,
|
|
7
|
-
type L2BlockPruneEvent,
|
|
8
9
|
type L2BlockSourceEventEmitter,
|
|
9
10
|
L2BlockSourceEvents,
|
|
11
|
+
type L2PruneUnprovenEvent,
|
|
10
12
|
} from '@aztec/stdlib/block';
|
|
11
13
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
14
|
import type {
|
|
13
|
-
|
|
15
|
+
ICheckpointBlockBuilder,
|
|
16
|
+
ICheckpointsBuilder,
|
|
14
17
|
ITxProvider,
|
|
15
18
|
MerkleTreeWriteOperations,
|
|
16
19
|
SlasherConfig,
|
|
17
20
|
} from '@aztec/stdlib/interfaces/server';
|
|
18
|
-
import type
|
|
21
|
+
import { type L1ToL2MessageSource, computeCheckpointOutHash } from '@aztec/stdlib/messaging';
|
|
19
22
|
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
23
|
+
import type { CheckpointGlobalVariables } from '@aztec/stdlib/tx';
|
|
20
24
|
import {
|
|
21
25
|
ReExFailedTxsError,
|
|
22
26
|
ReExStateMismatchError,
|
|
@@ -51,7 +55,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
51
55
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
52
56
|
private epochCache: EpochCache,
|
|
53
57
|
private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
|
|
54
|
-
private
|
|
58
|
+
private checkpointsBuilder: ICheckpointsBuilder,
|
|
55
59
|
penalties: EpochPruneWatcherPenalties,
|
|
56
60
|
) {
|
|
57
61
|
super();
|
|
@@ -62,12 +66,12 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
public start() {
|
|
65
|
-
this.l2BlockSource.on(L2BlockSourceEvents.
|
|
69
|
+
this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
|
|
66
70
|
return Promise.resolve();
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
public stop() {
|
|
70
|
-
this.l2BlockSource.removeListener(L2BlockSourceEvents.
|
|
74
|
+
this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
|
|
71
75
|
return Promise.resolve();
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -76,34 +80,34 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
76
80
|
this.log.verbose('EpochPruneWatcher config updated', this.penalties);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
private handlePruneL2Blocks(event:
|
|
83
|
+
private handlePruneL2Blocks(event: L2PruneUnprovenEvent): void {
|
|
80
84
|
const { blocks, epochNumber } = event;
|
|
81
85
|
void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
|
|
82
86
|
this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
|
|
83
87
|
);
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
private async emitSlashForEpoch(offense: OffenseType, epochNumber:
|
|
90
|
+
private async emitSlashForEpoch(offense: OffenseType, epochNumber: EpochNumber): Promise<void> {
|
|
87
91
|
const validators = await this.getValidatorsForEpoch(epochNumber);
|
|
88
92
|
if (validators.length === 0) {
|
|
89
93
|
this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
|
|
90
94
|
return;
|
|
91
95
|
}
|
|
92
|
-
const args = this.validatorsToSlashingArgs(validators, offense,
|
|
96
|
+
const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
|
|
93
97
|
this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
|
|
94
98
|
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
private async processPruneL2Blocks(blocks: L2Block[], epochNumber:
|
|
101
|
+
private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
|
|
98
102
|
try {
|
|
99
103
|
const l1Constants = this.epochCache.getL1Constants();
|
|
100
|
-
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.
|
|
104
|
+
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
|
|
101
105
|
this.log.info(
|
|
102
106
|
`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
|
|
103
107
|
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
|
|
104
108
|
);
|
|
105
109
|
|
|
106
|
-
await this.validateBlocks(epochBlocks);
|
|
110
|
+
await this.validateBlocks(epochBlocks, epochNumber);
|
|
107
111
|
this.log.info(`Pruned epoch ${epochNumber} was valid. Want to slash committee for not having it proven.`);
|
|
108
112
|
await this.emitSlashForEpoch(OffenseType.VALID_EPOCH_PRUNED, epochNumber);
|
|
109
113
|
} catch (error) {
|
|
@@ -118,21 +122,83 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
public async validateBlocks(blocks: L2Block[]): Promise<void> {
|
|
125
|
+
public async validateBlocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
|
|
122
126
|
if (blocks.length === 0) {
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
|
|
130
|
+
// Sort blocks by block number and group by checkpoint
|
|
131
|
+
const sortedBlocks = [...blocks].sort((a, b) => a.number - b.number);
|
|
132
|
+
const blocksByCheckpoint = chunkBy(sortedBlocks, b => b.checkpointNumber);
|
|
133
|
+
|
|
134
|
+
// Get prior checkpoints in the epoch (in case this was a partial prune) to extract the out hashes
|
|
135
|
+
const priorCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(epochNumber))
|
|
136
|
+
.filter(c => c.checkpointNumber < sortedBlocks[0].checkpointNumber)
|
|
137
|
+
.map(c => c.checkpointOutHash);
|
|
138
|
+
let previousCheckpointOutHashes: Fr[] = [...priorCheckpointOutHashes];
|
|
139
|
+
|
|
140
|
+
const fork = await this.checkpointsBuilder.getFork(
|
|
141
|
+
BlockNumber(sortedBlocks[0].header.globalVariables.blockNumber - 1),
|
|
142
|
+
);
|
|
126
143
|
try {
|
|
127
|
-
for (const
|
|
128
|
-
await this.
|
|
144
|
+
for (const checkpointBlocks of blocksByCheckpoint) {
|
|
145
|
+
await this.validateCheckpoint(checkpointBlocks, previousCheckpointOutHashes, fork);
|
|
146
|
+
|
|
147
|
+
// Compute checkpoint out hash from all blocks in this checkpoint
|
|
148
|
+
const checkpointOutHash = computeCheckpointOutHash(
|
|
149
|
+
checkpointBlocks.map(b => b.body.txEffects.map(tx => tx.l2ToL1Msgs)),
|
|
150
|
+
);
|
|
151
|
+
previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
|
|
129
152
|
}
|
|
130
153
|
} finally {
|
|
131
154
|
await fork.close();
|
|
132
155
|
}
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
private async validateCheckpoint(
|
|
159
|
+
checkpointBlocks: L2Block[],
|
|
160
|
+
previousCheckpointOutHashes: Fr[],
|
|
161
|
+
fork: MerkleTreeWriteOperations,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const checkpointNumber = checkpointBlocks[0].checkpointNumber;
|
|
164
|
+
this.log.debug(`Validating pruned checkpoint ${checkpointNumber} with ${checkpointBlocks.length} blocks`);
|
|
165
|
+
|
|
166
|
+
// Get L1ToL2Messages once for the entire checkpoint
|
|
167
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
168
|
+
|
|
169
|
+
// Build checkpoint constants from first block's global variables
|
|
170
|
+
const gv = checkpointBlocks[0].header.globalVariables;
|
|
171
|
+
const constants: CheckpointGlobalVariables = {
|
|
172
|
+
chainId: gv.chainId,
|
|
173
|
+
version: gv.version,
|
|
174
|
+
slotNumber: gv.slotNumber,
|
|
175
|
+
timestamp: gv.timestamp,
|
|
176
|
+
coinbase: gv.coinbase,
|
|
177
|
+
feeRecipient: gv.feeRecipient,
|
|
178
|
+
gasFees: gv.gasFees,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Start checkpoint builder once for all blocks in this checkpoint
|
|
182
|
+
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
183
|
+
checkpointNumber,
|
|
184
|
+
constants,
|
|
185
|
+
0n, // feeAssetPriceModifier is not used for validation of the checkpoint content
|
|
186
|
+
l1ToL2Messages,
|
|
187
|
+
previousCheckpointOutHashes,
|
|
188
|
+
fork,
|
|
189
|
+
this.log.getBindings(),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Validate all blocks in the checkpoint sequentially
|
|
193
|
+
for (const block of checkpointBlocks) {
|
|
194
|
+
await this.validateBlockInCheckpoint(block, checkpointBuilder);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async validateBlockInCheckpoint(
|
|
199
|
+
blockFromL1: L2Block,
|
|
200
|
+
checkpointBuilder: ICheckpointBlockBuilder,
|
|
201
|
+
): Promise<void> {
|
|
136
202
|
this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
|
|
137
203
|
const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
|
|
138
204
|
// We load txs from the mempool directly, since the TxCollector running in the background has already been
|
|
@@ -144,14 +210,9 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
144
210
|
throw new TransactionsNotAvailableError(missingTxs);
|
|
145
211
|
}
|
|
146
212
|
|
|
147
|
-
const
|
|
148
|
-
const { block, failedTxs, numTxs } = await
|
|
149
|
-
|
|
150
|
-
l1ToL2Messages,
|
|
151
|
-
blockFromL1.header.globalVariables,
|
|
152
|
-
{},
|
|
153
|
-
fork,
|
|
154
|
-
);
|
|
213
|
+
const gv = blockFromL1.header.globalVariables;
|
|
214
|
+
const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
|
|
215
|
+
|
|
155
216
|
if (numTxs !== txs.length) {
|
|
156
217
|
// This should be detected by state mismatch, but this makes it easier to debug.
|
|
157
218
|
throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
|
|
@@ -164,7 +225,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
164
225
|
}
|
|
165
226
|
}
|
|
166
227
|
|
|
167
|
-
private async getValidatorsForEpoch(epochNumber:
|
|
228
|
+
private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
|
|
168
229
|
const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
|
|
169
230
|
if (!committee) {
|
|
170
231
|
this.log.trace(`No committee found for epoch ${epochNumber}`);
|
|
@@ -176,7 +237,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
176
237
|
private validatorsToSlashingArgs(
|
|
177
238
|
validators: EthAddress[],
|
|
178
239
|
offenseType: OffenseType,
|
|
179
|
-
epochOrSlot:
|
|
240
|
+
epochOrSlot: EpochNumber,
|
|
180
241
|
): WantToSlashArgs[] {
|
|
181
242
|
const penalty =
|
|
182
243
|
offenseType === OffenseType.DATA_WITHHOLDING
|
|
@@ -186,7 +247,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
186
247
|
validator: v,
|
|
187
248
|
amount: penalty,
|
|
188
249
|
offenseType,
|
|
189
|
-
epochOrSlot,
|
|
250
|
+
epochOrSlot: BigInt(epochOrSlot),
|
|
190
251
|
}));
|
|
191
252
|
}
|
|
192
253
|
}
|