@aztec/slasher 0.0.1-commit.b655e406 → 0.0.1-commit.bf2612ae
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 -1
- 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/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_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/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 +5 -9
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +1 -1
- 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 +7 -6
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -1
- package/dest/watchers/epoch_prune_watcher.js +36 -13
- package/package.json +17 -14
- package/src/config.ts +17 -16
- package/src/empire_slasher_client.ts +6 -5
- package/src/factory/create_facade.ts +2 -1
- 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_round_monitor.ts +3 -2
- package/src/slasher_client_facade.ts +3 -2
- package/src/slasher_client_interface.ts +2 -1
- package/src/tally_slasher_client.ts +11 -5
- package/src/watcher.ts +1 -1
- package/src/watchers/attestations_block_watcher.ts +59 -45
- package/src/watchers/epoch_prune_watcher.ts +50 -22
|
@@ -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;
|
|
@@ -2,6 +2,7 @@ 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
8
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -169,7 +170,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
169
170
|
* @param slotNumber - The current slot number
|
|
170
171
|
* @returns The actions to take
|
|
171
172
|
*/
|
|
172
|
-
public async getProposerActions(slotNumber:
|
|
173
|
+
public async getProposerActions(slotNumber: SlotNumber): Promise<ProposerSlashAction[]> {
|
|
173
174
|
const [executeAction, voteAction] = await Promise.all([
|
|
174
175
|
this.getExecuteSlashAction(slotNumber),
|
|
175
176
|
this.getVoteOffensesAction(slotNumber),
|
|
@@ -182,7 +183,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
182
183
|
* Returns an execute slash action if there are any rounds ready to be executed.
|
|
183
184
|
* Returns the oldest slash action if there are multiple rounds pending execution.
|
|
184
185
|
*/
|
|
185
|
-
protected async getExecuteSlashAction(slotNumber:
|
|
186
|
+
protected async getExecuteSlashAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
186
187
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
187
188
|
const slashingExecutionDelayInRounds = BigInt(this.settings.slashingExecutionDelayInRounds);
|
|
188
189
|
const executableRound = currentRound - slashingExecutionDelayInRounds - 1n;
|
|
@@ -233,7 +234,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
233
234
|
*/
|
|
234
235
|
private async tryGetRoundExecuteAction(
|
|
235
236
|
executableRound: bigint,
|
|
236
|
-
slotNumber:
|
|
237
|
+
slotNumber: SlotNumber,
|
|
237
238
|
): Promise<ProposerSlashAction | undefined> {
|
|
238
239
|
let logData: Record<string, unknown> = { executableRound, slotNumber };
|
|
239
240
|
this.log.debug(`Testing if slashing round ${executableRound} is executable`, logData);
|
|
@@ -302,7 +303,7 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
/** Returns a vote action based on offenses from the target round (with offset applied) */
|
|
305
|
-
protected async getVoteOffensesAction(slotNumber:
|
|
306
|
+
protected async getVoteOffensesAction(slotNumber: SlotNumber): Promise<ProposerSlashAction | undefined> {
|
|
306
307
|
// Compute what round we are in based on the slot number and what round will be slashed
|
|
307
308
|
const { round: currentRound } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
308
309
|
const slashedRound = this.getSlashedRound(currentRound);
|
|
@@ -356,7 +357,12 @@ export class TallySlasherClient implements ProposerSlashActionProvider, SlasherC
|
|
|
356
357
|
|
|
357
358
|
const committees = await this.collectCommitteesActiveDuringRound(slashedRound);
|
|
358
359
|
const epochsForCommittees = getEpochsForRound(slashedRound, this.settings);
|
|
359
|
-
const votes = getSlashConsensusVotesFromOffenses(
|
|
360
|
+
const votes = getSlashConsensusVotesFromOffenses(
|
|
361
|
+
offensesToSlash,
|
|
362
|
+
committees,
|
|
363
|
+
epochsForCommittees.map(e => BigInt(e)),
|
|
364
|
+
this.settings,
|
|
365
|
+
);
|
|
360
366
|
if (votes.every(v => v === 0)) {
|
|
361
367
|
this.log.warn(`Computed votes for offenses are all zero. Skipping vote.`, {
|
|
362
368
|
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,25 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
2
3
|
import { 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
|
+
ICheckpointsBuilder,
|
|
14
16
|
ITxProvider,
|
|
15
17
|
MerkleTreeWriteOperations,
|
|
16
18
|
SlasherConfig,
|
|
17
19
|
} from '@aztec/stdlib/interfaces/server';
|
|
18
|
-
import type
|
|
20
|
+
import { type L1ToL2MessageSource, computeCheckpointOutHash } from '@aztec/stdlib/messaging';
|
|
19
21
|
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
22
|
+
import type { CheckpointGlobalVariables } from '@aztec/stdlib/tx';
|
|
20
23
|
import {
|
|
21
24
|
ReExFailedTxsError,
|
|
22
25
|
ReExStateMismatchError,
|
|
@@ -51,7 +54,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
51
54
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
52
55
|
private epochCache: EpochCache,
|
|
53
56
|
private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
|
|
54
|
-
private
|
|
57
|
+
private checkpointsBuilder: ICheckpointsBuilder,
|
|
55
58
|
penalties: EpochPruneWatcherPenalties,
|
|
56
59
|
) {
|
|
57
60
|
super();
|
|
@@ -62,12 +65,12 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
public start() {
|
|
65
|
-
this.l2BlockSource.on(L2BlockSourceEvents.
|
|
68
|
+
this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
|
|
66
69
|
return Promise.resolve();
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
public stop() {
|
|
70
|
-
this.l2BlockSource.removeListener(L2BlockSourceEvents.
|
|
73
|
+
this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
|
|
71
74
|
return Promise.resolve();
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -76,28 +79,28 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
76
79
|
this.log.verbose('EpochPruneWatcher config updated', this.penalties);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
private handlePruneL2Blocks(event:
|
|
82
|
+
private handlePruneL2Blocks(event: L2PruneUnprovenEvent): void {
|
|
80
83
|
const { blocks, epochNumber } = event;
|
|
81
84
|
void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
|
|
82
85
|
this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
|
|
83
86
|
);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
private async emitSlashForEpoch(offense: OffenseType, epochNumber:
|
|
89
|
+
private async emitSlashForEpoch(offense: OffenseType, epochNumber: EpochNumber): Promise<void> {
|
|
87
90
|
const validators = await this.getValidatorsForEpoch(epochNumber);
|
|
88
91
|
if (validators.length === 0) {
|
|
89
92
|
this.log.warn(`No validators found for epoch ${epochNumber} (cannot slash for ${getOffenseTypeName(offense)})`);
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
|
-
const args = this.validatorsToSlashingArgs(validators, offense,
|
|
95
|
+
const args = this.validatorsToSlashingArgs(validators, offense, epochNumber);
|
|
93
96
|
this.log.verbose(`Created slash for ${getOffenseTypeName(offense)} at epoch ${epochNumber}`, args);
|
|
94
97
|
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
private async processPruneL2Blocks(blocks: L2Block[], epochNumber:
|
|
100
|
+
private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
|
|
98
101
|
try {
|
|
99
102
|
const l1Constants = this.epochCache.getL1Constants();
|
|
100
|
-
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.
|
|
103
|
+
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
|
|
101
104
|
this.log.info(
|
|
102
105
|
`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
|
|
103
106
|
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
|
|
@@ -122,17 +125,27 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
122
125
|
if (blocks.length === 0) {
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
|
-
|
|
128
|
+
|
|
129
|
+
let previousCheckpointOutHashes: Fr[] = [];
|
|
130
|
+
const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
|
|
126
131
|
try {
|
|
127
132
|
for (const block of blocks) {
|
|
128
|
-
await this.validateBlock(block, fork);
|
|
133
|
+
await this.validateBlock(block, previousCheckpointOutHashes, fork);
|
|
134
|
+
|
|
135
|
+
// TODO(mbps): This assumes one block per checkpoint, which is only true for now.
|
|
136
|
+
const checkpointOutHash = computeCheckpointOutHash([block.body.txEffects.map(tx => tx.l2ToL1Msgs)]);
|
|
137
|
+
previousCheckpointOutHashes = [...previousCheckpointOutHashes, checkpointOutHash];
|
|
129
138
|
}
|
|
130
139
|
} finally {
|
|
131
140
|
await fork.close();
|
|
132
141
|
}
|
|
133
142
|
}
|
|
134
143
|
|
|
135
|
-
public async validateBlock(
|
|
144
|
+
public async validateBlock(
|
|
145
|
+
blockFromL1: L2Block,
|
|
146
|
+
previousCheckpointOutHashes: Fr[],
|
|
147
|
+
fork: MerkleTreeWriteOperations,
|
|
148
|
+
): Promise<void> {
|
|
136
149
|
this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
|
|
137
150
|
const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
|
|
138
151
|
// We load txs from the mempool directly, since the TxCollector running in the background has already been
|
|
@@ -144,14 +157,29 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
144
157
|
throw new TransactionsNotAvailableError(missingTxs);
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
160
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockFromL1.number);
|
|
161
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
162
|
+
const gv = blockFromL1.header.globalVariables;
|
|
163
|
+
const constants: CheckpointGlobalVariables = {
|
|
164
|
+
chainId: gv.chainId,
|
|
165
|
+
version: gv.version,
|
|
166
|
+
slotNumber: gv.slotNumber,
|
|
167
|
+
coinbase: gv.coinbase,
|
|
168
|
+
feeRecipient: gv.feeRecipient,
|
|
169
|
+
gasFees: gv.gasFees,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Use checkpoint builder to validate the block
|
|
173
|
+
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
174
|
+
checkpointNumber,
|
|
175
|
+
constants,
|
|
150
176
|
l1ToL2Messages,
|
|
151
|
-
|
|
152
|
-
{},
|
|
177
|
+
previousCheckpointOutHashes,
|
|
153
178
|
fork,
|
|
154
179
|
);
|
|
180
|
+
|
|
181
|
+
const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
|
|
182
|
+
|
|
155
183
|
if (numTxs !== txs.length) {
|
|
156
184
|
// This should be detected by state mismatch, but this makes it easier to debug.
|
|
157
185
|
throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
|
|
@@ -164,7 +192,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
164
192
|
}
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
private async getValidatorsForEpoch(epochNumber:
|
|
195
|
+
private async getValidatorsForEpoch(epochNumber: EpochNumber): Promise<EthAddress[]> {
|
|
168
196
|
const { committee } = await this.epochCache.getCommitteeForEpoch(epochNumber);
|
|
169
197
|
if (!committee) {
|
|
170
198
|
this.log.trace(`No committee found for epoch ${epochNumber}`);
|
|
@@ -176,7 +204,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
176
204
|
private validatorsToSlashingArgs(
|
|
177
205
|
validators: EthAddress[],
|
|
178
206
|
offenseType: OffenseType,
|
|
179
|
-
epochOrSlot:
|
|
207
|
+
epochOrSlot: EpochNumber,
|
|
180
208
|
): WantToSlashArgs[] {
|
|
181
209
|
const penalty =
|
|
182
210
|
offenseType === OffenseType.DATA_WITHHOLDING
|
|
@@ -186,7 +214,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
186
214
|
validator: v,
|
|
187
215
|
amount: penalty,
|
|
188
216
|
offenseType,
|
|
189
|
-
epochOrSlot,
|
|
217
|
+
epochOrSlot: BigInt(epochOrSlot),
|
|
190
218
|
}));
|
|
191
219
|
}
|
|
192
220
|
}
|