@aztec/slasher 0.0.1-commit.24de95ac
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 +218 -0
- package/dest/config.d.ts +6 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +134 -0
- package/dest/empire_slasher_client.d.ts +189 -0
- package/dest/empire_slasher_client.d.ts.map +1 -0
- package/dest/empire_slasher_client.js +572 -0
- package/dest/factory/create_facade.d.ts +15 -0
- package/dest/factory/create_facade.d.ts.map +1 -0
- package/dest/factory/create_facade.js +23 -0
- package/dest/factory/create_implementation.d.ts +17 -0
- package/dest/factory/create_implementation.d.ts.map +1 -0
- package/dest/factory/create_implementation.js +73 -0
- 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 +3 -0
- package/dest/factory/index.d.ts.map +1 -0
- package/dest/factory/index.js +2 -0
- package/dest/index.d.ts +11 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +10 -0
- package/dest/null_slasher_client.d.ts +16 -0
- package/dest/null_slasher_client.d.ts.map +1 -0
- package/dest/null_slasher_client.js +33 -0
- package/dest/slash_offenses_collector.d.ts +45 -0
- package/dest/slash_offenses_collector.d.ts.map +1 -0
- package/dest/slash_offenses_collector.js +94 -0
- package/dest/slash_round_monitor.d.ts +29 -0
- package/dest/slash_round_monitor.d.ts.map +1 -0
- package/dest/slash_round_monitor.js +52 -0
- package/dest/slasher_client_facade.d.ts +43 -0
- package/dest/slasher_client_facade.d.ts.map +1 -0
- package/dest/slasher_client_facade.js +76 -0
- package/dest/slasher_client_interface.d.ts +38 -0
- package/dest/slasher_client_interface.d.ts.map +1 -0
- package/dest/slasher_client_interface.js +4 -0
- package/dest/stores/offenses_store.d.ts +37 -0
- package/dest/stores/offenses_store.d.ts.map +1 -0
- package/dest/stores/offenses_store.js +105 -0
- package/dest/stores/payloads_store.d.ts +29 -0
- package/dest/stores/payloads_store.d.ts.map +1 -0
- package/dest/stores/payloads_store.js +125 -0
- package/dest/stores/schema_version.d.ts +2 -0
- package/dest/stores/schema_version.d.ts.map +1 -0
- package/dest/stores/schema_version.js +1 -0
- package/dest/tally_slasher_client.d.ts +129 -0
- package/dest/tally_slasher_client.d.ts.map +1 -0
- package/dest/tally_slasher_client.js +349 -0
- 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 +21 -0
- package/dest/watcher.d.ts.map +1 -0
- package/dest/watcher.js +1 -0
- package/dest/watchers/attestations_block_watcher.d.ts +33 -0
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -0
- package/dest/watchers/attestations_block_watcher.js +135 -0
- package/dest/watchers/epoch_prune_watcher.d.ts +37 -0
- package/dest/watchers/epoch_prune_watcher.d.ts.map +1 -0
- package/dest/watchers/epoch_prune_watcher.js +135 -0
- package/package.json +89 -0
- package/src/config.ts +157 -0
- package/src/empire_slasher_client.ts +656 -0
- package/src/factory/create_facade.ts +52 -0
- package/src/factory/create_implementation.ts +159 -0
- package/src/factory/get_settings.ts +58 -0
- package/src/factory/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/null_slasher_client.ts +40 -0
- package/src/slash_offenses_collector.ts +118 -0
- package/src/slash_round_monitor.ts +61 -0
- package/src/slasher_client_facade.ts +100 -0
- package/src/slasher_client_interface.ts +45 -0
- package/src/stores/offenses_store.ts +145 -0
- package/src/stores/payloads_store.ts +146 -0
- package/src/stores/schema_version.ts +1 -0
- package/src/tally_slasher_client.ts +436 -0
- package/src/test/dummy_watcher.ts +21 -0
- package/src/watcher.ts +27 -0
- package/src/watchers/attestations_block_watcher.ts +180 -0
- package/src/watchers/epoch_prune_watcher.ts +192 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { sumBigint } from '@aztec/foundation/bigint';
|
|
2
|
+
import { compactArray, filterAsync, maxBy, pick } from '@aztec/foundation/collection';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { sleep } from '@aztec/foundation/sleep';
|
|
6
|
+
import { getFirstEligibleRoundForOffense, getOffenseIdentifiersFromPayload, getPenaltyForOffense, isOffenseUncontroversial, offenseDataComparator, offensesToValidatorSlash } from '@aztec/stdlib/slashing';
|
|
7
|
+
import { SlashOffensesCollector } from './slash_offenses_collector.js';
|
|
8
|
+
import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
9
|
+
/**
|
|
10
|
+
* The Empire Slasher client is responsible for managing slashable offenses and slash payloads
|
|
11
|
+
* using the Empire slashing model where fixed payloads are created and voted on.
|
|
12
|
+
*
|
|
13
|
+
* The client subscribes to several slash watchers that emit offenses and tracks them. When the slasher is the
|
|
14
|
+
* proposer, it aggregates pending offenses from previous rounds and creates slash payloads, or votes for previous
|
|
15
|
+
* slash payloads.
|
|
16
|
+
* Voting is handled by the sequencer publisher, the slasher client does not interact with L1 directly.
|
|
17
|
+
* The client also monitors slash payloads created by other nodes, and executes them when they become submittable.
|
|
18
|
+
*
|
|
19
|
+
* Payload creation and selection
|
|
20
|
+
* - At each L2 slot in a slashing round, the proposer for that L2 slot may vote for an existing slashing payload or
|
|
21
|
+
* create one of their own. Note that anyone can create a slash payload on L1, but nodes will only follow payloads
|
|
22
|
+
* from proposers; we could enforce this on L1, but we do not want to make any changes there if we can avoid it.
|
|
23
|
+
* - If it is the first L2 slot in the slashing round, there is nothing to vote for, so the proposer creates a slash
|
|
24
|
+
* payload and votes for it.
|
|
25
|
+
* - On their turn, each proposer computes a score for each payload in the round. This score is a function of the
|
|
26
|
+
* total offences slashed, how many votes it has received so far, and how far into the round we are. The score for a
|
|
27
|
+
* payload is zero if the proposer disagrees with it (see "agreement" below).
|
|
28
|
+
* - The proposer also computes the score for the payload they would create. If the resulting score is higher than
|
|
29
|
+
* any existing payload, it creates the payload. Otherwise, it votes for the one with the highest score.
|
|
30
|
+
*
|
|
31
|
+
* Collecting offences
|
|
32
|
+
* - Whenever a node spots a slashable offence, they store it and add it to a local collection of "pending
|
|
33
|
+
* offences". When a proposer needs to create a slash payload, they include all pending offences from previous
|
|
34
|
+
* rounds. This means an offence is **only slashable in the next round it happened** (or a future one).
|
|
35
|
+
* - Each offence also carries an epoch or block identifier, so we can differentiate two offences of the same kind by
|
|
36
|
+
* the same validator.
|
|
37
|
+
* - When a slash payload is flagged as executable (as in it got enough votes to be executed), nodes remove all
|
|
38
|
+
* slashed offences in the payload from their collection of pending offences.
|
|
39
|
+
* - Pending offences expire after a configurable time. This is to minimize divergences. For instance, a validator
|
|
40
|
+
* that has to be slashed due to inactivity 50 epochs ago will only be considered for slashing by nodes that were
|
|
41
|
+
* online 50 epochs ago. We propose using the validator exit window as expiration time, any value higher means that
|
|
42
|
+
* we may try slashing validators that have exited the set already.
|
|
43
|
+
*
|
|
44
|
+
* Agreement and scoring
|
|
45
|
+
* - A proposer will *agree* with a slash payload if it *agrees* with every offence in the payload, all
|
|
46
|
+
* *uncontroversial* offences from the past round are included, and it is below a configurable maximum size.
|
|
47
|
+
* - An *uncontroversial* offence is one where every node agrees that a slash is in order, regardless of any p2p
|
|
48
|
+
* network partitions. The only uncontroversial offence we have now is "proposing a block on L1 with invalid
|
|
49
|
+
* attestations".
|
|
50
|
+
* - A proposer will *agree* with a given offence if it is present in its list of "pending offences", and the
|
|
51
|
+
* slashing amount is within a configurable min-max range.
|
|
52
|
+
* - Slash payloads need a maximum size to ensure they can don't exceed the maximum L1 gas per tx when executed.
|
|
53
|
+
* This is configurable but depends on the L1 contracts implementation. When creating a payload, if there are too
|
|
54
|
+
* many pending offences to fit, proposers favor the offences with the highest slashing amount first, tie-breaking by
|
|
55
|
+
* choosing the most recent ones.
|
|
56
|
+
* - The scoring function will boost proposals with more agreed slashes, as well as proposals with more votes, and
|
|
57
|
+
* will disincentivize the creation of new proposals as the end of the round nears. This function will NOT be
|
|
58
|
+
* enforced on L1.
|
|
59
|
+
*
|
|
60
|
+
* Execution
|
|
61
|
+
* - Once a slash payload becomes executable, the next proposer is expected to execute it. If they don't, the
|
|
62
|
+
* following does, and so on. No gas rebate is given.
|
|
63
|
+
*/ export class EmpireSlasherClient {
|
|
64
|
+
config;
|
|
65
|
+
settings;
|
|
66
|
+
slashFactoryContract;
|
|
67
|
+
slashingProposer;
|
|
68
|
+
slasher;
|
|
69
|
+
rollup;
|
|
70
|
+
dateProvider;
|
|
71
|
+
offensesStore;
|
|
72
|
+
payloadsStore;
|
|
73
|
+
log;
|
|
74
|
+
executablePayloads;
|
|
75
|
+
unwatchCallbacks;
|
|
76
|
+
overridePayloadActive;
|
|
77
|
+
offensesCollector;
|
|
78
|
+
roundMonitor;
|
|
79
|
+
constructor(config, settings, slashFactoryContract, slashingProposer, slasher, rollup, watchers, dateProvider, offensesStore, payloadsStore, log = createLogger('slasher:empire')){
|
|
80
|
+
this.config = config;
|
|
81
|
+
this.settings = settings;
|
|
82
|
+
this.slashFactoryContract = slashFactoryContract;
|
|
83
|
+
this.slashingProposer = slashingProposer;
|
|
84
|
+
this.slasher = slasher;
|
|
85
|
+
this.rollup = rollup;
|
|
86
|
+
this.dateProvider = dateProvider;
|
|
87
|
+
this.offensesStore = offensesStore;
|
|
88
|
+
this.payloadsStore = payloadsStore;
|
|
89
|
+
this.log = log;
|
|
90
|
+
this.executablePayloads = [];
|
|
91
|
+
this.unwatchCallbacks = [];
|
|
92
|
+
this.overridePayloadActive = false;
|
|
93
|
+
this.overridePayloadActive = config.slashOverridePayload !== undefined && !config.slashOverridePayload.isZero();
|
|
94
|
+
this.roundMonitor = new SlashRoundMonitor(this.settings, this.dateProvider);
|
|
95
|
+
this.offensesCollector = new SlashOffensesCollector(config, this.settings, watchers, offensesStore);
|
|
96
|
+
}
|
|
97
|
+
async start() {
|
|
98
|
+
this.log.debug('Starting Empire Slasher client...');
|
|
99
|
+
// Start the offenses collector
|
|
100
|
+
await this.offensesCollector.start();
|
|
101
|
+
// TODO(palla/slash): Sync any events since the last time we were offline, or since the current round started.
|
|
102
|
+
// Detect when a payload wins voting via PayloadSubmittable event
|
|
103
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToSubmittablePayloads(({ payload, round })=>void this.handleProposalExecutable(EthAddress.fromString(payload), round).catch((err)=>this.log.error('Error handling proposalExecutable', err, {
|
|
104
|
+
payload,
|
|
105
|
+
round
|
|
106
|
+
}))));
|
|
107
|
+
// Detect when a payload is submitted via PayloadSubmitted event
|
|
108
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToPayloadSubmitted(({ payload, round })=>void this.handleProposalExecuted(EthAddress.fromString(payload), round).catch((err)=>this.log.error('Error handling payloadSubmitted', err, {
|
|
109
|
+
payload,
|
|
110
|
+
round
|
|
111
|
+
}))));
|
|
112
|
+
// Detect when a payload is signalled via SignalCast event
|
|
113
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToSignalCasted(({ payload, round, signaler })=>void this.handleProposalSignalled(EthAddress.fromString(payload), round, EthAddress.fromString(signaler)).catch((err)=>this.log.error('Error handling proposalSignalled', err, {
|
|
114
|
+
payload,
|
|
115
|
+
round,
|
|
116
|
+
signaler
|
|
117
|
+
}))));
|
|
118
|
+
// Check for round changes
|
|
119
|
+
this.unwatchCallbacks.push(this.roundMonitor.listenToNewRound((round)=>this.handleNewRound(round)));
|
|
120
|
+
this.log.info(`Started empire slasher client`);
|
|
121
|
+
return Promise.resolve();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Allows consumers to stop the instance of the slasher client.
|
|
125
|
+
* 'ready' will now return 'false' and the running promise that keeps the client synced is interrupted.
|
|
126
|
+
*/ async stop() {
|
|
127
|
+
this.log.debug('Stopping Empire Slasher client...');
|
|
128
|
+
for (const unwatchCallback of this.unwatchCallbacks){
|
|
129
|
+
unwatchCallback();
|
|
130
|
+
}
|
|
131
|
+
this.roundMonitor.stop();
|
|
132
|
+
await this.offensesCollector.stop();
|
|
133
|
+
// Viem calls eth_uninstallFilter under the hood when uninstalling event watchers, but these calls are not awaited,
|
|
134
|
+
// meaning that any error that happens during the uninstallation will not be caught. This causes errors during jest teardowns,
|
|
135
|
+
// where we stop anvil after all other processes are stopped, so sometimes the eth_uninstallFilter call fails because anvil
|
|
136
|
+
// is already stopped. We add a sleep here to give the uninstallation some time to complete, but the proper fix is for
|
|
137
|
+
// viem to await the eth_uninstallFilter calls, or to catch any errors that happen during the uninstallation.
|
|
138
|
+
// See https://github.com/wevm/viem/issues/3714.
|
|
139
|
+
await sleep(2000);
|
|
140
|
+
this.log.info('Empire Slasher client stopped');
|
|
141
|
+
}
|
|
142
|
+
/** Returns the current config */ getConfig() {
|
|
143
|
+
return this.config;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Update the config of the slasher client
|
|
147
|
+
* @param config - The new config
|
|
148
|
+
*/ updateConfig(config) {
|
|
149
|
+
const newConfig = {
|
|
150
|
+
...this.config,
|
|
151
|
+
...config
|
|
152
|
+
};
|
|
153
|
+
// We keep this separate flag to tell us if we should be signal for the override payload: after the override payload is executed,
|
|
154
|
+
// the slasher goes back to using the monitored payloads to inform the sequencer publisher what payload to signal for.
|
|
155
|
+
// So we only want to flip back "on" the voting for override payload if config we just passed in re-set the override payload.
|
|
156
|
+
this.overridePayloadActive = config.slashOverridePayload !== undefined && !config.slashOverridePayload.isZero();
|
|
157
|
+
this.config = newConfig;
|
|
158
|
+
}
|
|
159
|
+
getSlashPayloads() {
|
|
160
|
+
return this.payloadsStore.getPayloadsForRound(this.roundMonitor.getCurrentRound().round);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Triggered on a time basis when we enter a new slashing round.
|
|
164
|
+
* Clears expired payloads and offenses from stores.
|
|
165
|
+
*/ async handleNewRound(round) {
|
|
166
|
+
this.log.info(`Starting new slashing round ${round}`);
|
|
167
|
+
await this.payloadsStore.clearExpiredPayloads(round);
|
|
168
|
+
await this.offensesCollector.handleNewRound(round);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Called when we see a PayloadSubmittable event on the SlashProposer.
|
|
172
|
+
* Adds the proposal to the list of executable ones.
|
|
173
|
+
*/ async handleProposalExecutable(payloadAddress, round) {
|
|
174
|
+
// Track this payload for execution later
|
|
175
|
+
this.executablePayloads.push({
|
|
176
|
+
payload: payloadAddress,
|
|
177
|
+
round
|
|
178
|
+
});
|
|
179
|
+
this.log.verbose(`Proposal ${payloadAddress.toString()} is executable for round ${round}`, {
|
|
180
|
+
payloadAddress,
|
|
181
|
+
round
|
|
182
|
+
});
|
|
183
|
+
// Stop signaling for the override payload if it was elected
|
|
184
|
+
if (this.overridePayloadActive && this.config.slashOverridePayload && this.config.slashOverridePayload.equals(payloadAddress)) {
|
|
185
|
+
this.overridePayloadActive = false;
|
|
186
|
+
}
|
|
187
|
+
// Load the payload to unflag all offenses to be slashed as pending
|
|
188
|
+
const payload = await this.payloadsStore.getPayload(payloadAddress) ?? await this.slashFactoryContract.getSlashPayloadFromEvents(payloadAddress, this.settings);
|
|
189
|
+
if (!payload) {
|
|
190
|
+
this.log.warn(`No payload found for ${payloadAddress.toString()} in round ${round}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const offenses = getOffenseIdentifiersFromPayload(payload);
|
|
194
|
+
await this.offensesCollector.markAsSlashed(offenses);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Called when we see a PayloadSubmitted event on the SlashProposer.
|
|
198
|
+
* Removes the proposal from the list of executable ones.
|
|
199
|
+
*/ handleProposalExecuted(payload, round) {
|
|
200
|
+
this.log.verbose(`Proposal ${payload.toString()} on round ${round} has been executed`);
|
|
201
|
+
const index = this.executablePayloads.findIndex((p)=>p.payload.equals(payload));
|
|
202
|
+
if (index !== -1) {
|
|
203
|
+
this.executablePayloads.splice(index, 1);
|
|
204
|
+
}
|
|
205
|
+
return Promise.resolve();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Called when we see a SignalCast event on the SlashProposer.
|
|
209
|
+
* Adds a vote for the given payload in the round.
|
|
210
|
+
* Retrieves the proposal if we have not seen it before.
|
|
211
|
+
*/ async handleProposalSignalled(payloadAddress, round, signaller) {
|
|
212
|
+
const payload = await this.payloadsStore.getPayload(payloadAddress);
|
|
213
|
+
if (!payload) {
|
|
214
|
+
this.log.debug(`Fetching payload for signal at ${payloadAddress.toString()}`);
|
|
215
|
+
const payload = await this.slashFactoryContract.getSlashPayloadFromEvents(payloadAddress, this.settings);
|
|
216
|
+
if (!payload) {
|
|
217
|
+
this.log.warn(`No payload found for signal at ${payloadAddress.toString()}`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const votes = await this.slashingProposer.getPayloadSignals(this.rollup.address, round, payloadAddress.toString());
|
|
221
|
+
await this.payloadsStore.addPayload({
|
|
222
|
+
...payload,
|
|
223
|
+
votes,
|
|
224
|
+
round
|
|
225
|
+
});
|
|
226
|
+
this.log.verbose(`Added payload at ${payloadAddress}`, {
|
|
227
|
+
...payload,
|
|
228
|
+
votes,
|
|
229
|
+
round,
|
|
230
|
+
signaller
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
const votes = await this.payloadsStore.incrementPayloadVotes(payloadAddress, round);
|
|
234
|
+
this.log.verbose(`Added vote for payload at ${payloadAddress}`, {
|
|
235
|
+
votes,
|
|
236
|
+
signaller,
|
|
237
|
+
round
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Create a slash payload for the given round from pending offenses
|
|
243
|
+
* @param round - The round to create the payload for, defaults to the current round
|
|
244
|
+
* @returns The payload data or undefined if no offenses to slash
|
|
245
|
+
*/ async gatherOffensesForRound(round) {
|
|
246
|
+
round ??= this.roundMonitor.getCurrentRound().round;
|
|
247
|
+
// Filter pending offenses to those that can be included in this round
|
|
248
|
+
const pendingOffenses = await this.offensesStore.getPendingOffenses();
|
|
249
|
+
const eligibleOffenses = pendingOffenses.filter((offense)=>this.isOffenseForRound(offense, round));
|
|
250
|
+
// Sort by uncontroversial first, then slash amount (descending), then detection time (ascending)
|
|
251
|
+
const sortedOffenses = [
|
|
252
|
+
...eligibleOffenses
|
|
253
|
+
].sort(offenseDataComparator);
|
|
254
|
+
// Take up to maxPayloadSize offenses
|
|
255
|
+
const { slashMaxPayloadSize } = this.config;
|
|
256
|
+
const selectedOffenses = sortedOffenses.slice(0, slashMaxPayloadSize);
|
|
257
|
+
if (selectedOffenses.length !== sortedOffenses.length) {
|
|
258
|
+
this.log.warn(`Offense list of ${sortedOffenses.length} truncated to max size of ${slashMaxPayloadSize}`);
|
|
259
|
+
}
|
|
260
|
+
return selectedOffenses;
|
|
261
|
+
}
|
|
262
|
+
/** Returns all pending offenses stored */ getPendingOffenses() {
|
|
263
|
+
return this.offensesStore.getPendingOffenses();
|
|
264
|
+
}
|
|
265
|
+
/** Get uncontroversial offenses that are expected to be present on the given round. */ async getPendingUncontroversialOffensesForRound(round) {
|
|
266
|
+
const pendingOffenses = await this.offensesStore.getPendingOffenses();
|
|
267
|
+
const filteredOffenses = pendingOffenses.filter((offense)=>isOffenseUncontroversial(offense.offenseType) && this.isOffenseForRound(offense, round)).sort(offenseDataComparator);
|
|
268
|
+
return filteredOffenses.slice(0, this.config.slashMaxPayloadSize);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Calculate score for a slash payload, bumping the votes by one, so we get the score as if we voted for it.
|
|
272
|
+
* @param payload - The payload to score
|
|
273
|
+
* @param votes - Number of votes the payload has received
|
|
274
|
+
* @returns The score for the payload
|
|
275
|
+
*/ calculatePayloadScore(payload) {
|
|
276
|
+
// TODO: Update this function to something smarter
|
|
277
|
+
return (payload.votes + 1n) * sumBigint(payload.slashes.map((o)=>o.amount));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get the actions the proposer should take for slashing
|
|
281
|
+
* @param slotNumber - The current slot number
|
|
282
|
+
* @returns The actions to take
|
|
283
|
+
*/ async getProposerActions(slotNumber) {
|
|
284
|
+
const [executeAction, proposePayloadActions] = await Promise.all([
|
|
285
|
+
this.getExecutePayloadAction(slotNumber),
|
|
286
|
+
this.getProposePayloadActions(slotNumber)
|
|
287
|
+
]);
|
|
288
|
+
return compactArray([
|
|
289
|
+
executeAction,
|
|
290
|
+
...proposePayloadActions
|
|
291
|
+
]);
|
|
292
|
+
}
|
|
293
|
+
/** Returns an execute payload action if there are any payloads ready to be executed */ async getExecutePayloadAction(slotNumber) {
|
|
294
|
+
const { round } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
295
|
+
const toRemove = [];
|
|
296
|
+
let toExecute;
|
|
297
|
+
for (const payload of this.executablePayloads){
|
|
298
|
+
const executableRound = payload.round + BigInt(this.settings.slashingExecutionDelayInRounds) + 1n;
|
|
299
|
+
if (round < executableRound) {
|
|
300
|
+
this.log.debug(`Payload ${payload.payload} for round ${payload.round} is not executable yet`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (payload.round + BigInt(this.settings.slashingPayloadLifetimeInRounds) < round) {
|
|
304
|
+
this.log.verbose(`Payload ${payload.payload} for round ${payload.round} has expired`);
|
|
305
|
+
toRemove.push(payload);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const roundInfo = await this.slashingProposer.getRoundInfo(this.rollup.address, payload.round);
|
|
309
|
+
if (roundInfo.executed) {
|
|
310
|
+
this.log.verbose(`Payload ${payload.payload} for round ${payload.round} has already been executed`);
|
|
311
|
+
toRemove.push(payload);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Check if slashing is enabled at all
|
|
315
|
+
if (!await this.slasher.isSlashingEnabled()) {
|
|
316
|
+
this.log.warn(`Slashing is disabled in the Slasher contract (skipping execution)`);
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
// Check if the slash payload is vetoed
|
|
320
|
+
const isVetoed = await this.slasher.isPayloadVetoed(payload.payload);
|
|
321
|
+
if (isVetoed) {
|
|
322
|
+
this.log.info(`Payload ${payload.payload} from round ${payload.round} is vetoed (skipping execution)`);
|
|
323
|
+
toRemove.push(payload);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
this.log.info(`Executing payload ${payload.payload} from round ${payload.round}`);
|
|
327
|
+
toExecute = payload;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
// Clean up expired or executed payloads
|
|
331
|
+
this.executablePayloads = this.executablePayloads.filter((p)=>!toRemove.includes(p));
|
|
332
|
+
return toExecute ? {
|
|
333
|
+
type: 'execute-empire-payload',
|
|
334
|
+
round: toExecute.round
|
|
335
|
+
} : undefined;
|
|
336
|
+
}
|
|
337
|
+
/** Returns a vote or create payload action based on payload scoring */ async getProposePayloadActions(slotNumber) {
|
|
338
|
+
// Compute what round we are in based on the slot number
|
|
339
|
+
const { round, votingSlot } = this.roundMonitor.getRoundForSlot(slotNumber);
|
|
340
|
+
const { slashingRoundSize: roundSize, slashingQuorumSize: quorumSize } = this.settings;
|
|
341
|
+
const logData = {
|
|
342
|
+
round,
|
|
343
|
+
votingSlot,
|
|
344
|
+
slotNumber
|
|
345
|
+
};
|
|
346
|
+
// If override payload is active, vote for it
|
|
347
|
+
if (this.overridePayloadActive && this.config.slashOverridePayload && !this.config.slashOverridePayload.isZero()) {
|
|
348
|
+
this.log.info(`Overriding slash payload to ${this.config.slashOverridePayload.toString()}`, logData);
|
|
349
|
+
return [
|
|
350
|
+
{
|
|
351
|
+
type: 'vote-empire-payload',
|
|
352
|
+
payload: this.config.slashOverridePayload
|
|
353
|
+
}
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
// Check if there is a payload that has already won, if so, no need to do anything
|
|
357
|
+
const existingPayloads = await this.payloadsStore.getPayloadsForRound(round);
|
|
358
|
+
const winningPayload = existingPayloads.find((p)=>p.votes >= quorumSize);
|
|
359
|
+
if (winningPayload) {
|
|
360
|
+
this.log.verbose(`No need to vote as payload ${winningPayload.address} has already won`, logData);
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
// Check if we should create a new payload at this stage
|
|
364
|
+
// We define an initial "nomination phase" at the beginning, which depends on the number of votes needed,
|
|
365
|
+
// and only allow for new proposals to be created then. This ensures that no payloads are created that will
|
|
366
|
+
// not be able to pass. The invariant here is that a payload can be created only if there are enough slots
|
|
367
|
+
// left such that if half of the remaining votes are cast for it, then it will be able to pass.
|
|
368
|
+
const nominationPhaseDurationInSlots = BigInt(Math.floor((roundSize - quorumSize) / 2));
|
|
369
|
+
// Create our ideal payload from the pending offenses we have in store
|
|
370
|
+
let idealPayload = undefined;
|
|
371
|
+
if (votingSlot <= nominationPhaseDurationInSlots) {
|
|
372
|
+
const idealOffenses = await this.gatherOffensesForRound(round);
|
|
373
|
+
idealPayload = idealOffenses.length === 0 ? undefined : {
|
|
374
|
+
slashes: offensesToValidatorSlash(idealOffenses),
|
|
375
|
+
votes: 0n,
|
|
376
|
+
address: EthAddress.ZERO
|
|
377
|
+
};
|
|
378
|
+
this.log.debug(`Collected offenses for ideal payload for round ${round}`, {
|
|
379
|
+
...logData,
|
|
380
|
+
idealOffenses
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
this.log.debug(`Past nomination phase, will not create new payloads for round ${round}`, logData);
|
|
384
|
+
}
|
|
385
|
+
// Find the best existing payload. We filter out those that have no chance of winning given how many voting
|
|
386
|
+
// slots are left in the round, and then filter by those we agree with.
|
|
387
|
+
const feasiblePayloads = existingPayloads.filter((p)=>BigInt(quorumSize) - p.votes <= BigInt(roundSize) - votingSlot);
|
|
388
|
+
const requiredOffenses = await this.getPendingUncontroversialOffensesForRound(round);
|
|
389
|
+
const agreedPayloads = await filterAsync(feasiblePayloads, (p)=>this.agreeWithPayload(p, round, requiredOffenses));
|
|
390
|
+
const bestPayload = maxBy([
|
|
391
|
+
...agreedPayloads,
|
|
392
|
+
idealPayload
|
|
393
|
+
], (p)=>p ? this.calculatePayloadScore(p) : 0);
|
|
394
|
+
// Debug all payloads info
|
|
395
|
+
if (this.log.isLevelEnabled('debug')) {
|
|
396
|
+
this.log.debug(`Scored payloads for round ${round}`, {
|
|
397
|
+
...logData,
|
|
398
|
+
idealPayload,
|
|
399
|
+
existingPayloads: existingPayloads.map((p)=>({
|
|
400
|
+
...pick(p, 'address', 'votes', 'round', 'timestamp'),
|
|
401
|
+
score: this.calculatePayloadScore(p)
|
|
402
|
+
})),
|
|
403
|
+
feasiblePayloads: feasiblePayloads.map((p)=>({
|
|
404
|
+
...pick(p, 'address', 'votes', 'round', 'timestamp'),
|
|
405
|
+
score: this.calculatePayloadScore(p)
|
|
406
|
+
})),
|
|
407
|
+
agreedPayloads: agreedPayloads.map((p)=>({
|
|
408
|
+
...pick(p, 'address', 'votes', 'round', 'timestamp'),
|
|
409
|
+
score: this.calculatePayloadScore(p)
|
|
410
|
+
}))
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (bestPayload === undefined || this.calculatePayloadScore(bestPayload) === 0n) {
|
|
414
|
+
// No payloads to vote for, do nothing
|
|
415
|
+
this.log.verbose(`No suitable slash payloads to vote for in round ${round}`, {
|
|
416
|
+
...logData,
|
|
417
|
+
existingPayloadsCount: existingPayloads.length,
|
|
418
|
+
feasiblePayloadsCount: feasiblePayloads.length,
|
|
419
|
+
agreedPayloadsCount: agreedPayloads.length,
|
|
420
|
+
idealPayloadSlashesCount: idealPayload?.slashes.length
|
|
421
|
+
});
|
|
422
|
+
return [];
|
|
423
|
+
} else if (bestPayload === idealPayload) {
|
|
424
|
+
// If our ideal payload is the best, we create it (if not deployed yet) and vote for it
|
|
425
|
+
const { address, isDeployed } = await this.slashFactoryContract.getAddressAndIsDeployed(idealPayload.slashes);
|
|
426
|
+
this.log.info(`Proposing and voting for payload ${address.toString()} in round ${round}`, {
|
|
427
|
+
...logData,
|
|
428
|
+
payload: bestPayload
|
|
429
|
+
});
|
|
430
|
+
const createAction = isDeployed ? undefined : {
|
|
431
|
+
type: 'create-empire-payload',
|
|
432
|
+
data: idealPayload.slashes
|
|
433
|
+
};
|
|
434
|
+
const voteAction = {
|
|
435
|
+
type: 'vote-empire-payload',
|
|
436
|
+
payload: address
|
|
437
|
+
};
|
|
438
|
+
return compactArray([
|
|
439
|
+
createAction,
|
|
440
|
+
voteAction
|
|
441
|
+
]);
|
|
442
|
+
} else {
|
|
443
|
+
// Otherwise, vote for our favorite payload
|
|
444
|
+
this.log.info(`Voting for existing payload ${bestPayload.address.toString()} in round ${round}`, {
|
|
445
|
+
...logData,
|
|
446
|
+
payload: bestPayload
|
|
447
|
+
});
|
|
448
|
+
return [
|
|
449
|
+
{
|
|
450
|
+
type: 'vote-empire-payload',
|
|
451
|
+
payload: bestPayload.address
|
|
452
|
+
}
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Check if we agree with a payload:
|
|
458
|
+
* - We must agree with every offense in the payload
|
|
459
|
+
* - All uncontroversial offenses from past rounds must be included
|
|
460
|
+
* - Payload must be below maximum size
|
|
461
|
+
* - Slash amounts must be within acceptable ranges
|
|
462
|
+
*/ async agreeWithPayload(payload, round, cachedUncontroversialOffenses) {
|
|
463
|
+
// Check size limit
|
|
464
|
+
const maxPayloadSize = this.config.slashMaxPayloadSize;
|
|
465
|
+
if (payload.slashes.length > maxPayloadSize) {
|
|
466
|
+
this.log.verbose(`Rejecting payload ${payload.address} since size ${payload.slashes.length} exceeds maximum ${maxPayloadSize}`, {
|
|
467
|
+
payload,
|
|
468
|
+
maxPayloadSize
|
|
469
|
+
});
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
// Check we agree with all offenses and proposed slash amounts, and all offenses are from past rounds
|
|
473
|
+
for (const slash of payload.slashes){
|
|
474
|
+
for (const { offenseType, epochOrSlot } of slash.offenses){
|
|
475
|
+
const offense = {
|
|
476
|
+
validator: slash.validator,
|
|
477
|
+
offenseType,
|
|
478
|
+
epochOrSlot
|
|
479
|
+
};
|
|
480
|
+
if (!await this.offensesStore.hasPendingOffense(offense)) {
|
|
481
|
+
this.log.debug(`Rejecting payload ${payload.address} due to offense not found`, {
|
|
482
|
+
offense,
|
|
483
|
+
payload
|
|
484
|
+
});
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
const [minRound, maxRound] = this.getRoundRangeForOffense(offense);
|
|
488
|
+
if (round < minRound || round > maxRound) {
|
|
489
|
+
this.log.debug(`Rejecting payload ${payload.address} due to offense not from valid round`, {
|
|
490
|
+
offense,
|
|
491
|
+
payload,
|
|
492
|
+
round,
|
|
493
|
+
minRound,
|
|
494
|
+
maxRound
|
|
495
|
+
});
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const [minSlashAmount, maxSlashAmount] = this.getSlashAmountValidRange(slash.offenses);
|
|
500
|
+
if (slash.amount < minSlashAmount || slash.amount > maxSlashAmount) {
|
|
501
|
+
this.log.debug(`Rejecting payload ${payload.address} due to slash amount out of range`, {
|
|
502
|
+
amount: slash.amount,
|
|
503
|
+
minSlashAmount,
|
|
504
|
+
maxSlashAmount,
|
|
505
|
+
payload
|
|
506
|
+
});
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Check that all uncontroversial offenses from past rounds are included
|
|
511
|
+
const uncontroversialOffenses = cachedUncontroversialOffenses ?? await this.getPendingUncontroversialOffensesForRound(round);
|
|
512
|
+
for (const requiredOffense of uncontroversialOffenses){
|
|
513
|
+
const validatorOffenses = payload.slashes.filter((slash)=>slash.validator.equals(requiredOffense.validator)).flatMap((slash)=>slash.offenses);
|
|
514
|
+
if (!validatorOffenses.some((o)=>o.offenseType === requiredOffense.offenseType && o.epochOrSlot === requiredOffense.epochOrSlot)) {
|
|
515
|
+
this.log.debug(`Rejecting payload due to missing uncontroversial offense for validator ${requiredOffense.validator}`, {
|
|
516
|
+
requiredOffense,
|
|
517
|
+
validatorOffenses,
|
|
518
|
+
payload
|
|
519
|
+
});
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Returns whether the given offense can be included in the given round.
|
|
527
|
+
* Depends on the offense round range and whether we include offenses from past rounds.
|
|
528
|
+
*/ isOffenseForRound(offense, round) {
|
|
529
|
+
const [minRound, maxRound] = this.getRoundRangeForOffense(offense);
|
|
530
|
+
const match = round >= minRound && round <= maxRound;
|
|
531
|
+
this.log.trace(`Offense ${offense.offenseType} for ${offense.validator} ${match ? 'is' : 'is not'} for round ${round}`, {
|
|
532
|
+
minRound,
|
|
533
|
+
maxRound,
|
|
534
|
+
round,
|
|
535
|
+
offense
|
|
536
|
+
});
|
|
537
|
+
return match;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Returns the range (inclusive) of rounds in which we could expect an offense to be found.
|
|
541
|
+
* Lower bound is determined by all offenses that should have been captured before the start of a round,
|
|
542
|
+
* which depends on the offense type (eg INACTIVITY is captured once an epoch ends, DATA_WITHHOLDING is
|
|
543
|
+
* captured after the epoch proof submission window for the epoch for which the data was withheld).
|
|
544
|
+
* Upper bound is determined by the expiration rounds for an offense, which is a config setting.
|
|
545
|
+
*/ getRoundRangeForOffense(offense) {
|
|
546
|
+
const minRound = getFirstEligibleRoundForOffense(offense, this.settings);
|
|
547
|
+
return [
|
|
548
|
+
minRound,
|
|
549
|
+
minRound + BigInt(this.config.slashOffenseExpirationRounds)
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
/** Returns the acceptable range for slash amount given a set of offenses. */ getSlashAmountValidRange(offenses) {
|
|
553
|
+
if (offenses.length === 0) {
|
|
554
|
+
return [
|
|
555
|
+
0n,
|
|
556
|
+
0n
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
const minAmount = sumBigint(offenses.map((o)=>this.getMinAmountForOffense(o.offenseType)));
|
|
560
|
+
const maxAmount = sumBigint(offenses.map((o)=>this.getMaxAmountForOffense(o.offenseType)));
|
|
561
|
+
return [
|
|
562
|
+
minAmount,
|
|
563
|
+
maxAmount
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
/** Get minimum acceptable amount for an offense type */ getMinAmountForOffense(offense) {
|
|
567
|
+
return getPenaltyForOffense(offense, this.config) * BigInt(this.config.slashMinPenaltyPercentage * 1000) / 1000n;
|
|
568
|
+
}
|
|
569
|
+
/** Get maximum acceptable amount for an offense type */ getMaxAmountForOffense(offense) {
|
|
570
|
+
return getPenaltyForOffense(offense, this.config) * BigInt(this.config.slashMaxPenaltyPercentage * 1000) / 1000n;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import type { L1ReaderConfig, ViemClient } from '@aztec/ethereum';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
|
+
import type { DataStoreConfig } from '@aztec/kv-store/config';
|
|
6
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
7
|
+
import type { SlasherClientInterface } from '../slasher_client_interface.js';
|
|
8
|
+
import type { Watcher } from '../watcher.js';
|
|
9
|
+
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
|
|
10
|
+
export declare function createSlasherFacade(config: SlasherConfig & DataStoreConfig & {
|
|
11
|
+
ethereumSlotDuration: number;
|
|
12
|
+
}, l1Contracts: Pick<L1ReaderConfig['l1Contracts'], 'rollupAddress' | 'slashFactoryAddress'>, l1Client: ViemClient, watchers: Watcher[], dateProvider: DateProvider, epochCache: EpochCache,
|
|
13
|
+
/** List of own validator addresses to add to the slashValidatorNever list unless slashSelfAllowed is true */
|
|
14
|
+
validatorAddresses?: EthAddress[], logger?: import("@aztec/foundation/log").Logger): Promise<SlasherClientInterface>;
|
|
15
|
+
//# sourceMappingURL=create_facade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create_facade.d.ts","sourceRoot":"","sources":["../../src/factory/create_facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,8FAA8F;AAC9F,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;IAAE,oBAAoB,EAAE,MAAM,CAAA;CAAE,EAC1E,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,EAAE,eAAe,GAAG,qBAAqB,CAAC,EACzF,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU;AACtB,6GAA6G;AAC7G,kBAAkB,GAAE,UAAU,EAAO,EACrC,MAAM,yCAA0B,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAwBjC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
2
|
+
import { unique } from '@aztec/foundation/collection';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { createStore } from '@aztec/kv-store/lmdb-v2';
|
|
6
|
+
import { SlasherClientFacade } from '../slasher_client_facade.js';
|
|
7
|
+
import { SCHEMA_VERSION } from '../stores/schema_version.js';
|
|
8
|
+
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */ export async function createSlasherFacade(config, l1Contracts, l1Client, watchers, dateProvider, epochCache, /** List of own validator addresses to add to the slashValidatorNever list unless slashSelfAllowed is true */ validatorAddresses = [], logger = createLogger('slasher')) {
|
|
9
|
+
if (!l1Contracts.rollupAddress || l1Contracts.rollupAddress.equals(EthAddress.ZERO)) {
|
|
10
|
+
throw new Error('Cannot initialize SlasherClient without a Rollup address');
|
|
11
|
+
}
|
|
12
|
+
const kvStore = await createStore('slasher', SCHEMA_VERSION, config, createLogger('slasher:lmdb'));
|
|
13
|
+
const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
|
|
14
|
+
const slashValidatorsNever = config.slashSelfAllowed ? config.slashValidatorsNever : unique([
|
|
15
|
+
...config.slashValidatorsNever,
|
|
16
|
+
...validatorAddresses
|
|
17
|
+
].map((a)=>a.toString())).map(EthAddress.fromString);
|
|
18
|
+
const updatedConfig = {
|
|
19
|
+
...config,
|
|
20
|
+
slashValidatorsNever
|
|
21
|
+
};
|
|
22
|
+
return new SlasherClientFacade(updatedConfig, rollup, l1Client, l1Contracts.slashFactoryAddress, watchers, epochCache, dateProvider, kvStore, logger);
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import type { ViemClient } from '@aztec/ethereum';
|
|
3
|
+
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
|
+
import type { DataStoreConfig } from '@aztec/kv-store/config';
|
|
7
|
+
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
8
|
+
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
9
|
+
import { EmpireSlasherClient } from '../empire_slasher_client.js';
|
|
10
|
+
import { NullSlasherClient } from '../null_slasher_client.js';
|
|
11
|
+
import { TallySlasherClient } from '../tally_slasher_client.js';
|
|
12
|
+
import type { Watcher } from '../watcher.js';
|
|
13
|
+
/** Creates a slasher client implementation (either tally or empire) based on the slasher proposer type in the rollup */
|
|
14
|
+
export declare function createSlasherImplementation(config: SlasherConfig & DataStoreConfig & {
|
|
15
|
+
ethereumSlotDuration: number;
|
|
16
|
+
}, rollup: RollupContract, l1Client: ViemClient, slashFactoryAddress: EthAddress | undefined, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, kvStore: AztecLMDBStoreV2, logger?: import("@aztec/foundation/log").Logger): Promise<EmpireSlasherClient | TallySlasherClient | NullSlasherClient>;
|
|
17
|
+
//# sourceMappingURL=create_implementation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create_implementation.d.ts","sourceRoot":"","sources":["../../src/factory/create_implementation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAEL,cAAc,EAEf,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAA8B,MAAM,6BAA6B,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,wHAAwH;AACxH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;IAAE,oBAAoB,EAAE,MAAM,CAAA;CAAE,EAC1E,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,UAAU,EACpB,mBAAmB,EAAE,UAAU,GAAG,SAAS,EAC3C,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,gBAAgB,EACzB,MAAM,yCAA0B,yEAcjC"}
|