@aztec/slasher 0.0.1-commit.9b94fc1 → 0.0.1-commit.b468ad8
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 +2 -2
- package/dest/empire_slasher_client.d.ts.map +1 -1
- package/dest/empire_slasher_client.js +0 -8
- 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 +2 -2
- 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/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/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/slasher_client_facade.d.ts +2 -2
- package/dest/slasher_client_facade.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 +1 -1
- package/dest/stores/payloads_store.d.ts.map +1 -1
- package/dest/stores/payloads_store.js +6 -3
- package/dest/tally_slasher_client.d.ts +1 -1
- package/dest/tally_slasher_client.d.ts.map +1 -1
- package/dest/tally_slasher_client.js +10 -5
- 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 +40 -34
- 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 +34 -11
- package/package.json +16 -14
- package/src/config.ts +17 -16
- package/src/empire_slasher_client.ts +1 -9
- 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/slash_offenses_collector.ts +5 -1
- package/src/slasher_client_facade.ts +1 -1
- package/src/stores/offenses_store.ts +4 -2
- package/src/stores/payloads_store.ts +7 -4
- package/src/tally_slasher_client.ts +10 -5
- package/src/watcher.ts +1 -1
- package/src/watchers/attestations_block_watcher.ts +57 -44
- package/src/watchers/epoch_prune_watcher.ts +45 -17
|
@@ -3,12 +3,12 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
|
3
3
|
import { merge, pick } from '@aztec/foundation/collection';
|
|
4
4
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import {
|
|
6
|
-
type
|
|
7
|
-
type L2BlockInfo,
|
|
6
|
+
type InvalidCheckpointDetectedEvent,
|
|
8
7
|
type L2BlockSourceEventEmitter,
|
|
9
8
|
L2BlockSourceEvents,
|
|
10
|
-
type
|
|
9
|
+
type ValidateCheckpointNegativeResult,
|
|
11
10
|
} from '@aztec/stdlib/block';
|
|
11
|
+
import type { CheckpointInfo } from '@aztec/stdlib/checkpoint';
|
|
12
12
|
import { OffenseType } from '@aztec/stdlib/slashing';
|
|
13
13
|
|
|
14
14
|
import EventEmitter from 'node:events';
|
|
@@ -32,19 +32,19 @@ type AttestationsBlockWatcherConfig = Pick<SlasherConfig, (typeof AttestationsBl
|
|
|
32
32
|
export class AttestationsBlockWatcher extends (EventEmitter as new () => WatcherEmitter) implements Watcher {
|
|
33
33
|
private log: Logger = createLogger('attestations-block-watcher');
|
|
34
34
|
|
|
35
|
-
// Only keep track of the last N invalid
|
|
36
|
-
private
|
|
35
|
+
// Only keep track of the last N invalid checkpoints
|
|
36
|
+
private maxInvalidCheckpoints = 100;
|
|
37
37
|
|
|
38
38
|
// All invalid archive roots seen
|
|
39
39
|
private invalidArchiveRoots: Set<string> = new Set();
|
|
40
40
|
|
|
41
41
|
private config: AttestationsBlockWatcherConfig;
|
|
42
42
|
|
|
43
|
-
private
|
|
43
|
+
private boundHandleInvalidCheckpoint = (event: InvalidCheckpointDetectedEvent) => {
|
|
44
44
|
try {
|
|
45
|
-
this.
|
|
45
|
+
this.handleInvalidCheckpoint(event);
|
|
46
46
|
} catch (err) {
|
|
47
|
-
this.log.error('Error handling invalid
|
|
47
|
+
this.log.error('Error handling invalid checkpoint', err, {
|
|
48
48
|
...event.validationResult,
|
|
49
49
|
reason: event.validationResult.reason,
|
|
50
50
|
});
|
|
@@ -67,54 +67,61 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
public start() {
|
|
70
|
-
this.l2BlockSource.on(
|
|
70
|
+
this.l2BlockSource.events.on(
|
|
71
|
+
L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
|
|
72
|
+
this.boundHandleInvalidCheckpoint,
|
|
73
|
+
);
|
|
71
74
|
return Promise.resolve();
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
public stop() {
|
|
75
|
-
this.l2BlockSource.removeListener(
|
|
76
|
-
L2BlockSourceEvents.
|
|
77
|
-
this.
|
|
78
|
+
this.l2BlockSource.events.removeListener(
|
|
79
|
+
L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
|
|
80
|
+
this.boundHandleInvalidCheckpoint,
|
|
78
81
|
);
|
|
79
82
|
return Promise.resolve();
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
/** Event handler for invalid checkpoints as reported by the archiver. Public for testing purposes. */
|
|
86
|
+
public handleInvalidCheckpoint(event: InvalidCheckpointDetectedEvent): void {
|
|
83
87
|
const { validationResult } = event;
|
|
84
|
-
const
|
|
88
|
+
const checkpoint = validationResult.checkpoint;
|
|
85
89
|
|
|
86
|
-
// Check if we already have processed this
|
|
87
|
-
if (this.invalidArchiveRoots.has(
|
|
88
|
-
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}`);
|
|
89
93
|
return;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
this.log.verbose(`Detected invalid
|
|
93
|
-
...
|
|
96
|
+
this.log.verbose(`Detected invalid checkpoint ${checkpoint.checkpointNumber}`, {
|
|
97
|
+
...checkpoint,
|
|
94
98
|
reason: validationResult.valid === false ? validationResult.reason : 'unknown',
|
|
95
99
|
});
|
|
96
100
|
|
|
97
|
-
// Store the invalid
|
|
98
|
-
this.
|
|
101
|
+
// Store the invalid checkpoint
|
|
102
|
+
this.addInvalidCheckpoint(event.validationResult.checkpoint);
|
|
99
103
|
|
|
100
|
-
// Slash the proposer of the invalid
|
|
104
|
+
// Slash the proposer of the invalid checkpoint
|
|
101
105
|
this.slashProposer(event.validationResult);
|
|
102
106
|
|
|
103
|
-
// 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
|
|
104
108
|
this.slashAttestorsOnAncestorInvalid(event.validationResult);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
private slashAttestorsOnAncestorInvalid(validationResult:
|
|
108
|
-
const
|
|
111
|
+
private slashAttestorsOnAncestorInvalid(validationResult: ValidateCheckpointNegativeResult) {
|
|
112
|
+
const checkpoint = validationResult.checkpoint;
|
|
109
113
|
|
|
110
|
-
const parentArchive =
|
|
114
|
+
const parentArchive = checkpoint.lastArchive.toString();
|
|
111
115
|
if (this.invalidArchiveRoots.has(parentArchive)) {
|
|
112
116
|
const attestors = validationResult.attestors;
|
|
113
|
-
this.log.info(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
);
|
|
118
125
|
|
|
119
126
|
this.emit(
|
|
120
127
|
WANT_TO_SLASH_EVENT,
|
|
@@ -122,20 +129,26 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
122
129
|
validator: attestor,
|
|
123
130
|
amount: this.config.slashAttestDescendantOfInvalidPenalty,
|
|
124
131
|
offenseType: OffenseType.ATTESTED_DESCENDANT_OF_INVALID,
|
|
125
|
-
epochOrSlot: BigInt(SlotNumber(
|
|
132
|
+
epochOrSlot: BigInt(SlotNumber(checkpoint.slotNumber)),
|
|
126
133
|
})),
|
|
127
134
|
);
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
private slashProposer(validationResult:
|
|
132
|
-
const { reason,
|
|
133
|
-
const
|
|
134
|
-
const slot =
|
|
135
|
-
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);
|
|
136
149
|
|
|
137
150
|
if (!proposer) {
|
|
138
|
-
this.log.warn(`No proposer found for
|
|
151
|
+
this.log.warn(`No proposer found for checkpoint ${checkpointNumber} at slot ${slot}`);
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
141
154
|
|
|
@@ -148,15 +161,15 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
148
161
|
epochOrSlot: BigInt(slot),
|
|
149
162
|
};
|
|
150
163
|
|
|
151
|
-
this.log.info(`Want to slash proposer of
|
|
152
|
-
...
|
|
164
|
+
this.log.info(`Want to slash proposer of checkpoint ${checkpointNumber} due to ${reason}`, {
|
|
165
|
+
...checkpoint,
|
|
153
166
|
...args,
|
|
154
167
|
});
|
|
155
168
|
|
|
156
169
|
this.emit(WANT_TO_SLASH_EVENT, [args]);
|
|
157
170
|
}
|
|
158
171
|
|
|
159
|
-
private getOffenseFromInvalidationReason(reason:
|
|
172
|
+
private getOffenseFromInvalidationReason(reason: ValidateCheckpointNegativeResult['reason']): OffenseType {
|
|
160
173
|
switch (reason) {
|
|
161
174
|
case 'invalid-attestation':
|
|
162
175
|
return OffenseType.PROPOSED_INCORRECT_ATTESTATIONS;
|
|
@@ -169,11 +182,11 @@ export class AttestationsBlockWatcher extends (EventEmitter as new () => Watcher
|
|
|
169
182
|
}
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
private
|
|
173
|
-
this.invalidArchiveRoots.add(
|
|
185
|
+
private addInvalidCheckpoint(checkpoint: CheckpointInfo) {
|
|
186
|
+
this.invalidArchiveRoots.add(checkpoint.archive.toString());
|
|
174
187
|
|
|
175
188
|
// Prune old entries if we exceed the maximum
|
|
176
|
-
if (this.invalidArchiveRoots.size > this.
|
|
189
|
+
if (this.invalidArchiveRoots.size > this.maxInvalidCheckpoints) {
|
|
177
190
|
const oldestKey = this.invalidArchiveRoots.keys().next().value!;
|
|
178
191
|
this.invalidArchiveRoots.delete(oldestKey);
|
|
179
192
|
}
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import { EpochNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
3
3
|
import { merge, pick } from '@aztec/foundation/collection';
|
|
4
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
5
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
5
6
|
import {
|
|
6
7
|
EthAddress,
|
|
7
8
|
L2Block,
|
|
8
|
-
type L2BlockPruneEvent,
|
|
9
9
|
type L2BlockSourceEventEmitter,
|
|
10
10
|
L2BlockSourceEvents,
|
|
11
|
+
type L2PruneUnprovenEvent,
|
|
11
12
|
} from '@aztec/stdlib/block';
|
|
12
13
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
14
|
import type {
|
|
14
|
-
|
|
15
|
+
ICheckpointsBuilder,
|
|
15
16
|
ITxProvider,
|
|
16
17
|
MerkleTreeWriteOperations,
|
|
17
18
|
SlasherConfig,
|
|
18
19
|
} from '@aztec/stdlib/interfaces/server';
|
|
19
|
-
import type
|
|
20
|
+
import { type L1ToL2MessageSource, computeCheckpointOutHash } from '@aztec/stdlib/messaging';
|
|
20
21
|
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
22
|
+
import type { CheckpointGlobalVariables } from '@aztec/stdlib/tx';
|
|
21
23
|
import {
|
|
22
24
|
ReExFailedTxsError,
|
|
23
25
|
ReExStateMismatchError,
|
|
@@ -52,7 +54,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
52
54
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
53
55
|
private epochCache: EpochCache,
|
|
54
56
|
private txProvider: Pick<ITxProvider, 'getAvailableTxs'>,
|
|
55
|
-
private
|
|
57
|
+
private checkpointsBuilder: ICheckpointsBuilder,
|
|
56
58
|
penalties: EpochPruneWatcherPenalties,
|
|
57
59
|
) {
|
|
58
60
|
super();
|
|
@@ -63,12 +65,12 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
public start() {
|
|
66
|
-
this.l2BlockSource.on(L2BlockSourceEvents.
|
|
68
|
+
this.l2BlockSource.events.on(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
|
|
67
69
|
return Promise.resolve();
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
public stop() {
|
|
71
|
-
this.l2BlockSource.removeListener(L2BlockSourceEvents.
|
|
73
|
+
this.l2BlockSource.events.removeListener(L2BlockSourceEvents.L2PruneUnproven, this.boundHandlePruneL2Blocks);
|
|
72
74
|
return Promise.resolve();
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -77,7 +79,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
77
79
|
this.log.verbose('EpochPruneWatcher config updated', this.penalties);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
private handlePruneL2Blocks(event:
|
|
82
|
+
private handlePruneL2Blocks(event: L2PruneUnprovenEvent): void {
|
|
81
83
|
const { blocks, epochNumber } = event;
|
|
82
84
|
void this.processPruneL2Blocks(blocks, epochNumber).catch(err =>
|
|
83
85
|
this.log.error('Error processing pruned L2 blocks', err, { epochNumber }),
|
|
@@ -98,7 +100,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
98
100
|
private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise<void> {
|
|
99
101
|
try {
|
|
100
102
|
const l1Constants = this.epochCache.getL1Constants();
|
|
101
|
-
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.
|
|
103
|
+
const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber);
|
|
102
104
|
this.log.info(
|
|
103
105
|
`Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`,
|
|
104
106
|
{ blocks: epochBlocks.map(b => b.toBlockInfo()) },
|
|
@@ -123,17 +125,27 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
123
125
|
if (blocks.length === 0) {
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
126
|
-
|
|
128
|
+
|
|
129
|
+
let previousCheckpointOutHashes: Fr[] = [];
|
|
130
|
+
const fork = await this.checkpointsBuilder.getFork(BlockNumber(blocks[0].header.globalVariables.blockNumber - 1));
|
|
127
131
|
try {
|
|
128
132
|
for (const block of blocks) {
|
|
129
|
-
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];
|
|
130
138
|
}
|
|
131
139
|
} finally {
|
|
132
140
|
await fork.close();
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
public async validateBlock(
|
|
144
|
+
public async validateBlock(
|
|
145
|
+
blockFromL1: L2Block,
|
|
146
|
+
previousCheckpointOutHashes: Fr[],
|
|
147
|
+
fork: MerkleTreeWriteOperations,
|
|
148
|
+
): Promise<void> {
|
|
137
149
|
this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`);
|
|
138
150
|
const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash);
|
|
139
151
|
// We load txs from the mempool directly, since the TxCollector running in the background has already been
|
|
@@ -145,14 +157,30 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter
|
|
|
145
157
|
throw new TransactionsNotAvailableError(missingTxs);
|
|
146
158
|
}
|
|
147
159
|
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
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,
|
|
151
176
|
l1ToL2Messages,
|
|
152
|
-
|
|
153
|
-
{},
|
|
177
|
+
previousCheckpointOutHashes,
|
|
154
178
|
fork,
|
|
179
|
+
this.log.getBindings(),
|
|
155
180
|
);
|
|
181
|
+
|
|
182
|
+
const { block, failedTxs, numTxs } = await checkpointBuilder.buildBlock(txs, gv.blockNumber, gv.timestamp, {});
|
|
183
|
+
|
|
156
184
|
if (numTxs !== txs.length) {
|
|
157
185
|
// This should be detected by state mismatch, but this makes it easier to debug.
|
|
158
186
|
throw new ValidatorError(`Built block with ${numTxs} txs, expected ${txs.length}`);
|