@lodestar/validator 1.35.0-dev.98d359db41 → 1.35.0-dev.b42a298a7c
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/package.json +15 -13
- package/lib/buckets.d.ts.map +0 -1
- package/lib/defaults.d.ts.map +0 -1
- package/lib/genesis.d.ts.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/metrics.d.ts.map +0 -1
- package/lib/repositories/index.d.ts.map +0 -1
- package/lib/repositories/metaDataRepository.d.ts.map +0 -1
- package/lib/services/attestation.d.ts.map +0 -1
- package/lib/services/attestationDuties.d.ts.map +0 -1
- package/lib/services/block.d.ts.map +0 -1
- package/lib/services/blockDuties.d.ts.map +0 -1
- package/lib/services/chainHeaderTracker.d.ts.map +0 -1
- package/lib/services/doppelgangerService.d.ts.map +0 -1
- package/lib/services/emitter.d.ts.map +0 -1
- package/lib/services/externalSignerSync.d.ts.map +0 -1
- package/lib/services/indices.d.ts.map +0 -1
- package/lib/services/prepareBeaconProposer.d.ts.map +0 -1
- package/lib/services/syncCommittee.d.ts.map +0 -1
- package/lib/services/syncCommitteeDuties.d.ts.map +0 -1
- package/lib/services/syncingStatusTracker.d.ts.map +0 -1
- package/lib/services/utils.d.ts.map +0 -1
- package/lib/services/validatorStore.d.ts.map +0 -1
- package/lib/slashingProtection/attestation/attestationByTargetRepository.d.ts.map +0 -1
- package/lib/slashingProtection/attestation/attestationLowerBoundRepository.d.ts.map +0 -1
- package/lib/slashingProtection/attestation/errors.d.ts.map +0 -1
- package/lib/slashingProtection/attestation/index.d.ts.map +0 -1
- package/lib/slashingProtection/block/blockBySlotRepository.d.ts.map +0 -1
- package/lib/slashingProtection/block/errors.d.ts.map +0 -1
- package/lib/slashingProtection/block/index.d.ts.map +0 -1
- package/lib/slashingProtection/index.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/errors.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/formats/completeV4.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/formats/index.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/formats/v5.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/index.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/parseInterchange.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/serializeInterchange.d.ts.map +0 -1
- package/lib/slashingProtection/interchange/types.d.ts.map +0 -1
- package/lib/slashingProtection/interface.d.ts.map +0 -1
- package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.d.ts.map +0 -1
- package/lib/slashingProtection/minMaxSurround/errors.d.ts.map +0 -1
- package/lib/slashingProtection/minMaxSurround/index.d.ts.map +0 -1
- package/lib/slashingProtection/minMaxSurround/interface.d.ts.map +0 -1
- package/lib/slashingProtection/minMaxSurround/minMaxSurround.d.ts.map +0 -1
- package/lib/slashingProtection/types.d.ts.map +0 -1
- package/lib/slashingProtection/utils.d.ts.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/util/batch.d.ts.map +0 -1
- package/lib/util/clock.d.ts.map +0 -1
- package/lib/util/difference.d.ts.map +0 -1
- package/lib/util/externalSignerClient.d.ts.map +0 -1
- package/lib/util/format.d.ts.map +0 -1
- package/lib/util/index.d.ts.map +0 -1
- package/lib/util/logger.d.ts.map +0 -1
- package/lib/util/params.d.ts.map +0 -1
- package/lib/util/url.d.ts.map +0 -1
- package/lib/validator.d.ts.map +0 -1
- package/src/buckets.ts +0 -30
- package/src/defaults.ts +0 -8
- package/src/genesis.ts +0 -19
- package/src/index.ts +0 -22
- package/src/metrics.ts +0 -417
- package/src/repositories/index.ts +0 -1
- package/src/repositories/metaDataRepository.ts +0 -42
- package/src/services/attestation.ts +0 -349
- package/src/services/attestationDuties.ts +0 -405
- package/src/services/block.ts +0 -261
- package/src/services/blockDuties.ts +0 -215
- package/src/services/chainHeaderTracker.ts +0 -89
- package/src/services/doppelgangerService.ts +0 -286
- package/src/services/emitter.ts +0 -43
- package/src/services/externalSignerSync.ts +0 -81
- package/src/services/indices.ts +0 -165
- package/src/services/prepareBeaconProposer.ts +0 -119
- package/src/services/syncCommittee.ts +0 -317
- package/src/services/syncCommitteeDuties.ts +0 -337
- package/src/services/syncingStatusTracker.ts +0 -74
- package/src/services/utils.ts +0 -58
- package/src/services/validatorStore.ts +0 -830
- package/src/slashingProtection/attestation/attestationByTargetRepository.ts +0 -77
- package/src/slashingProtection/attestation/attestationLowerBoundRepository.ts +0 -44
- package/src/slashingProtection/attestation/errors.ts +0 -66
- package/src/slashingProtection/attestation/index.ts +0 -171
- package/src/slashingProtection/block/blockBySlotRepository.ts +0 -78
- package/src/slashingProtection/block/errors.ts +0 -28
- package/src/slashingProtection/block/index.ts +0 -94
- package/src/slashingProtection/index.ts +0 -95
- package/src/slashingProtection/interchange/errors.ts +0 -15
- package/src/slashingProtection/interchange/formats/completeV4.ts +0 -125
- package/src/slashingProtection/interchange/formats/index.ts +0 -7
- package/src/slashingProtection/interchange/formats/v5.ts +0 -120
- package/src/slashingProtection/interchange/index.ts +0 -5
- package/src/slashingProtection/interchange/parseInterchange.ts +0 -55
- package/src/slashingProtection/interchange/serializeInterchange.ts +0 -35
- package/src/slashingProtection/interchange/types.ts +0 -18
- package/src/slashingProtection/interface.ts +0 -28
- package/src/slashingProtection/minMaxSurround/distanceStoreRepository.ts +0 -57
- package/src/slashingProtection/minMaxSurround/errors.ts +0 -27
- package/src/slashingProtection/minMaxSurround/index.ts +0 -4
- package/src/slashingProtection/minMaxSurround/interface.ts +0 -23
- package/src/slashingProtection/minMaxSurround/minMaxSurround.ts +0 -104
- package/src/slashingProtection/types.ts +0 -12
- package/src/slashingProtection/utils.ts +0 -42
- package/src/types.ts +0 -31
- package/src/util/batch.ts +0 -15
- package/src/util/clock.ts +0 -164
- package/src/util/difference.ts +0 -10
- package/src/util/externalSignerClient.ts +0 -277
- package/src/util/format.ts +0 -3
- package/src/util/index.ts +0 -6
- package/src/util/logger.ts +0 -51
- package/src/util/params.ts +0 -313
- package/src/util/url.ts +0 -16
- package/src/validator.ts +0 -418
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
-
import {ChainConfig} from "@lodestar/config";
|
|
3
|
-
import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
4
|
-
import {BLSPubkey, Epoch, RootHex, Slot} from "@lodestar/types";
|
|
5
|
-
import {sleep, toPubkeyHex} from "@lodestar/utils";
|
|
6
|
-
import {Metrics} from "../metrics.js";
|
|
7
|
-
import {PubkeyHex} from "../types.js";
|
|
8
|
-
import {IClock, LoggerVc, differenceHex} from "../util/index.js";
|
|
9
|
-
import {ValidatorStore} from "./validatorStore.js";
|
|
10
|
-
|
|
11
|
-
/** This polls block duties 1s before the next epoch */
|
|
12
|
-
// TODO: change to 6 to do it 2s before the next epoch
|
|
13
|
-
// once we have some improvement on epoch transition time
|
|
14
|
-
// see https://github.com/ChainSafe/lodestar/issues/5792#issuecomment-1647457442
|
|
15
|
-
const BLOCK_DUTIES_LOOKAHEAD_FACTOR = 12;
|
|
16
|
-
/** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch */
|
|
17
|
-
const HISTORICAL_DUTIES_EPOCHS = 2;
|
|
18
|
-
// Re-declaring to not have to depend on `lodestar-params` just for this 0
|
|
19
|
-
const GENESIS_EPOCH = 0;
|
|
20
|
-
export const GENESIS_SLOT = 0;
|
|
21
|
-
|
|
22
|
-
type BlockDutyAtEpoch = {dependentRoot: RootHex; data: routes.validator.ProposerDuty[]};
|
|
23
|
-
type NotifyBlockProductionFn = (slot: Slot, proposers: BLSPubkey[]) => void;
|
|
24
|
-
|
|
25
|
-
export class BlockDutiesService {
|
|
26
|
-
/** Notify the block service if it should produce a block. */
|
|
27
|
-
private readonly notifyBlockProductionFn: NotifyBlockProductionFn;
|
|
28
|
-
/** Maps an epoch to all *local* proposers in this epoch. Notably, this does not contain
|
|
29
|
-
proposals for any validators which are not registered locally. */
|
|
30
|
-
private readonly proposers = new Map<Epoch, BlockDutyAtEpoch>();
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
private readonly config: ChainConfig,
|
|
34
|
-
private readonly logger: LoggerVc,
|
|
35
|
-
private readonly api: ApiClient,
|
|
36
|
-
private readonly clock: IClock,
|
|
37
|
-
private readonly validatorStore: ValidatorStore,
|
|
38
|
-
private readonly metrics: Metrics | null,
|
|
39
|
-
notifyBlockProductionFn: NotifyBlockProductionFn
|
|
40
|
-
) {
|
|
41
|
-
this.notifyBlockProductionFn = notifyBlockProductionFn;
|
|
42
|
-
|
|
43
|
-
// TODO: Instead of polling every CLOCK_SLOT, poll every CLOCK_EPOCH and track re-org events
|
|
44
|
-
// only then re-fetch the block duties. Make sure most clients (including Lodestar)
|
|
45
|
-
// properly emit the re-org event
|
|
46
|
-
clock.runEverySlot(this.runBlockDutiesTask);
|
|
47
|
-
|
|
48
|
-
if (metrics) {
|
|
49
|
-
metrics.proposerDutiesEpochCount.addCollect(() => {
|
|
50
|
-
metrics.proposerDutiesEpochCount.set(this.proposers.size);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Returns the pubkeys of the validators which are assigned to propose in the given slot.
|
|
57
|
-
*
|
|
58
|
-
* It is possible that multiple validators have an identical proposal slot, however that is
|
|
59
|
-
* likely the result of heavy forking (lol) or inconsistent beacon node connections.
|
|
60
|
-
*/
|
|
61
|
-
getblockProposersAtSlot(slot: Slot): BLSPubkey[] {
|
|
62
|
-
const epoch = computeEpochAtSlot(slot);
|
|
63
|
-
const publicKeys = new Map<string, BLSPubkey>(); // pseudo-HashSet of Buffers
|
|
64
|
-
|
|
65
|
-
const dutyAtEpoch = this.proposers.get(epoch);
|
|
66
|
-
if (dutyAtEpoch) {
|
|
67
|
-
for (const proposer of dutyAtEpoch.data) {
|
|
68
|
-
if (proposer.slot === slot) {
|
|
69
|
-
publicKeys.set(toPubkeyHex(proposer.pubkey), proposer.pubkey);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return Array.from(publicKeys.values());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
removeDutiesForKey(pubkey: PubkeyHex): void {
|
|
78
|
-
for (const blockDutyAtEpoch of this.proposers.values()) {
|
|
79
|
-
blockDutyAtEpoch.data = blockDutyAtEpoch.data.filter((proposer) => {
|
|
80
|
-
return toPubkeyHex(proposer.pubkey) !== pubkey;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private runBlockDutiesTask = async (slot: Slot, signal: AbortSignal): Promise<void> => {
|
|
86
|
-
try {
|
|
87
|
-
if (slot < 0) {
|
|
88
|
-
// Before genesis, fetch the genesis duties but don't notify block production
|
|
89
|
-
// Only fetch duties once since there is not possible to re-org. TODO: Review
|
|
90
|
-
if (!this.proposers.has(GENESIS_EPOCH)) {
|
|
91
|
-
await this.pollBeaconProposers(GENESIS_EPOCH);
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
await this.pollBeaconProposersAndNotify(slot, signal);
|
|
95
|
-
}
|
|
96
|
-
} catch (e) {
|
|
97
|
-
this.logger.error("Error on pollBeaconProposers", {}, e as Error);
|
|
98
|
-
} finally {
|
|
99
|
-
this.pruneOldDuties(computeEpochAtSlot(slot));
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Download the proposer duties for the current epoch and store them in `this.proposers`.
|
|
105
|
-
* If there are any proposer for this slot, send out a notification to the block proposers.
|
|
106
|
-
*
|
|
107
|
-
* ## Note
|
|
108
|
-
*
|
|
109
|
-
* This function will potentially send *two* notifications to the `BlockService`; it will send a
|
|
110
|
-
* notification initially, then it will download the latest duties and send a *second* notification
|
|
111
|
-
* if those duties have changed. This behaviour simultaneously achieves the following:
|
|
112
|
-
*
|
|
113
|
-
* 1. Block production can happen immediately and does not have to wait for the proposer duties to
|
|
114
|
-
* download.
|
|
115
|
-
* 2. We won't miss a block if the duties for the current slot happen to change with this poll.
|
|
116
|
-
*
|
|
117
|
-
* This sounds great, but is it safe? Firstly, the additional notification will only contain block
|
|
118
|
-
* producers that were not included in the first notification. This should be safety enough.
|
|
119
|
-
* However, we also have the slashing protection as a second line of defense. These two factors
|
|
120
|
-
* provide an acceptable level of safety.
|
|
121
|
-
*
|
|
122
|
-
* It's important to note that since there is a 0-epoch look-ahead (i.e., no look-ahead) for block
|
|
123
|
-
* proposers then it's very likely that a proposal for the first slot of the epoch will need go
|
|
124
|
-
* through the slow path every time. I.e., the proposal will only happen after we've been able to
|
|
125
|
-
* download and process the duties from the BN. This means it is very important to ensure this
|
|
126
|
-
* function is as fast as possible.
|
|
127
|
-
* - Starting from Jul 2023, we poll proposers 1s before the next epoch thanks to PrepareNextSlotScheduler
|
|
128
|
-
* usually finishes in 3s.
|
|
129
|
-
*/
|
|
130
|
-
private async pollBeaconProposersAndNotify(currentSlot: Slot, signal: AbortSignal): Promise<void> {
|
|
131
|
-
const nextEpoch = computeEpochAtSlot(currentSlot) + 1;
|
|
132
|
-
const isLastSlotEpoch = computeStartSlotAtEpoch(nextEpoch) === currentSlot + 1;
|
|
133
|
-
if (isLastSlotEpoch) {
|
|
134
|
-
// no need to await for other steps, just poll proposers for next epoch
|
|
135
|
-
this.pollBeaconProposersNextEpoch(currentSlot, nextEpoch, signal).catch((e) => {
|
|
136
|
-
this.logger.error("Error on pollBeaconProposersNextEpoch", {}, e);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Notify the block proposal service for any proposals that we have in our cache.
|
|
141
|
-
const initialBlockProposers = this.getblockProposersAtSlot(currentSlot);
|
|
142
|
-
if (initialBlockProposers.length > 0) {
|
|
143
|
-
this.notifyBlockProductionFn(currentSlot, initialBlockProposers);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Poll proposers again for the same slot
|
|
147
|
-
await this.pollBeaconProposers(computeEpochAtSlot(currentSlot));
|
|
148
|
-
|
|
149
|
-
// Compute the block proposers for this slot again, now that we've received an update from the BN.
|
|
150
|
-
//
|
|
151
|
-
// Then, compute the difference between these two sets to obtain a set of block proposers
|
|
152
|
-
// which were not included in the initial notification to the `BlockService`.
|
|
153
|
-
const newBlockProducers = this.getblockProposersAtSlot(currentSlot);
|
|
154
|
-
const additionalBlockProducers = differenceHex(initialBlockProposers, newBlockProducers);
|
|
155
|
-
|
|
156
|
-
// If there are any new proposers for this slot, send a notification so they produce a block.
|
|
157
|
-
//
|
|
158
|
-
// See the function-level documentation for more reasoning about this behaviour.
|
|
159
|
-
if (additionalBlockProducers.length > 0) {
|
|
160
|
-
this.notifyBlockProductionFn(currentSlot, additionalBlockProducers);
|
|
161
|
-
this.logger.debug("Detected new block proposer", {currentSlot});
|
|
162
|
-
this.metrics?.newProposalDutiesDetected.inc();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* This is to avoid some delay on the first slot of the epoch when validators have proposal duties.
|
|
168
|
-
* See https://github.com/ChainSafe/lodestar/issues/5792
|
|
169
|
-
*/
|
|
170
|
-
private async pollBeaconProposersNextEpoch(currentSlot: Slot, nextEpoch: Epoch, signal: AbortSignal): Promise<void> {
|
|
171
|
-
const nextSlot = currentSlot + 1;
|
|
172
|
-
const lookAheadMs = (this.config.SECONDS_PER_SLOT * 1000) / BLOCK_DUTIES_LOOKAHEAD_FACTOR;
|
|
173
|
-
await sleep(this.clock.msToSlot(nextSlot) - lookAheadMs, signal);
|
|
174
|
-
this.logger.debug("Polling proposers for next epoch", {nextEpoch, nextSlot});
|
|
175
|
-
// Poll proposers for the next epoch
|
|
176
|
-
await this.pollBeaconProposers(nextEpoch);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private async pollBeaconProposers(epoch: Epoch): Promise<void> {
|
|
180
|
-
// Only download duties and push out additional block production events if we have some validators.
|
|
181
|
-
if (!this.validatorStore.hasSomeValidators()) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const res = await this.api.validator.getProposerDuties({epoch});
|
|
186
|
-
const proposerDuties = res.value();
|
|
187
|
-
const {dependentRoot} = res.meta();
|
|
188
|
-
const relevantDuties = proposerDuties.filter((duty) => {
|
|
189
|
-
const pubkeyHex = toPubkeyHex(duty.pubkey);
|
|
190
|
-
return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
this.logger.debug("Downloaded proposer duties", {epoch, dependentRoot, count: relevantDuties.length});
|
|
194
|
-
|
|
195
|
-
const prior = this.proposers.get(epoch);
|
|
196
|
-
this.proposers.set(epoch, {dependentRoot, data: relevantDuties});
|
|
197
|
-
|
|
198
|
-
if (prior && prior.dependentRoot !== dependentRoot) {
|
|
199
|
-
this.metrics?.proposerDutiesReorg.inc();
|
|
200
|
-
this.logger.warn("Proposer duties re-org. This may happen from time to time", {
|
|
201
|
-
priorDependentRoot: prior.dependentRoot,
|
|
202
|
-
dependentRoot,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Run once per epoch to prune `this.proposers` map */
|
|
208
|
-
private pruneOldDuties(currentEpoch: Epoch): void {
|
|
209
|
-
for (const epoch of this.proposers.keys()) {
|
|
210
|
-
if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
|
|
211
|
-
this.proposers.delete(epoch);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
-
import {GENESIS_SLOT} from "@lodestar/params";
|
|
3
|
-
import {Root, RootHex, Slot} from "@lodestar/types";
|
|
4
|
-
import {Logger, fromHex} from "@lodestar/utils";
|
|
5
|
-
import {ValidatorEvent, ValidatorEventEmitter} from "./emitter.js";
|
|
6
|
-
|
|
7
|
-
const {EventType} = routes.events;
|
|
8
|
-
|
|
9
|
-
export type HeadEventData = {
|
|
10
|
-
slot: Slot;
|
|
11
|
-
head: RootHex;
|
|
12
|
-
previousDutyDependentRoot: RootHex;
|
|
13
|
-
currentDutyDependentRoot: RootHex;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type RunEveryFn = (event: HeadEventData) => Promise<void>;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Track the head slot/root using the event stream api "head".
|
|
20
|
-
*/
|
|
21
|
-
export class ChainHeaderTracker {
|
|
22
|
-
private headBlockSlot: Slot = GENESIS_SLOT;
|
|
23
|
-
private headBlockRoot: Root | null = null;
|
|
24
|
-
private readonly fns: RunEveryFn[] = [];
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
private readonly logger: Logger,
|
|
28
|
-
private readonly api: ApiClient,
|
|
29
|
-
private readonly emitter: ValidatorEventEmitter
|
|
30
|
-
) {}
|
|
31
|
-
|
|
32
|
-
start(signal: AbortSignal): void {
|
|
33
|
-
this.logger.verbose("Subscribing to head event");
|
|
34
|
-
this.api.events
|
|
35
|
-
.eventstream({
|
|
36
|
-
topics: [EventType.head],
|
|
37
|
-
signal,
|
|
38
|
-
onEvent: this.onHeadUpdate,
|
|
39
|
-
onError: (e) => {
|
|
40
|
-
this.logger.error("Failed to receive head event", {}, e);
|
|
41
|
-
},
|
|
42
|
-
onClose: () => {
|
|
43
|
-
this.logger.verbose("Closed stream for head event", {});
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
.catch((e) => this.logger.error("Failed to subscribe to head event", {}, e));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
getCurrentChainHead(slot: Slot): Root | null {
|
|
50
|
-
if (slot >= this.headBlockSlot) {
|
|
51
|
-
return this.headBlockRoot;
|
|
52
|
-
}
|
|
53
|
-
// We don't know head of an old block
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
runOnNewHead(fn: RunEveryFn): void {
|
|
58
|
-
this.fns.push(fn);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private onHeadUpdate = (event: routes.events.BeaconEvent): void => {
|
|
62
|
-
if (event.type === EventType.head) {
|
|
63
|
-
const {message} = event;
|
|
64
|
-
const {slot, block, previousDutyDependentRoot, currentDutyDependentRoot} = message;
|
|
65
|
-
this.headBlockSlot = slot;
|
|
66
|
-
this.headBlockRoot = fromHex(block);
|
|
67
|
-
|
|
68
|
-
const headEventData = {
|
|
69
|
-
slot: this.headBlockSlot,
|
|
70
|
-
head: block,
|
|
71
|
-
previousDutyDependentRoot: previousDutyDependentRoot,
|
|
72
|
-
currentDutyDependentRoot: currentDutyDependentRoot,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
for (const fn of this.fns) {
|
|
76
|
-
fn(headEventData).catch((e) => this.logger.error("Error calling head event handler", e));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.emitter.emit(ValidatorEvent.chainHead, headEventData);
|
|
80
|
-
|
|
81
|
-
this.logger.verbose("Found new chain head", {
|
|
82
|
-
slot: slot,
|
|
83
|
-
head: block,
|
|
84
|
-
previousDuty: previousDutyDependentRoot,
|
|
85
|
-
currentDuty: currentDutyDependentRoot,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
-
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
3
|
-
import {Epoch, ValidatorIndex} from "@lodestar/types";
|
|
4
|
-
import {Logger, fromHex, sleep, truncBytes} from "@lodestar/utils";
|
|
5
|
-
import {Metrics} from "../metrics.js";
|
|
6
|
-
import {ISlashingProtection} from "../slashingProtection/index.js";
|
|
7
|
-
import {ProcessShutdownCallback, PubkeyHex} from "../types.js";
|
|
8
|
-
import {IClock} from "../util/index.js";
|
|
9
|
-
import {IndicesService} from "./indices.js";
|
|
10
|
-
|
|
11
|
-
// The number of epochs that must be checked before we assume that there are
|
|
12
|
-
// no other duplicate validators on the network
|
|
13
|
-
const DEFAULT_REMAINING_DETECTION_EPOCHS = 1;
|
|
14
|
-
const REMAINING_EPOCHS_IF_DOPPELGANGER = Infinity;
|
|
15
|
-
const REMAINING_EPOCHS_IF_SKIPPED = 0;
|
|
16
|
-
|
|
17
|
-
/** Liveness responses for a given epoch */
|
|
18
|
-
type EpochLivenessData = {
|
|
19
|
-
epoch: Epoch;
|
|
20
|
-
responses: routes.validator.LivenessResponseData[];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type DoppelgangerState = {
|
|
24
|
-
nextEpochToCheck: Epoch;
|
|
25
|
-
remainingEpochs: Epoch;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export enum DoppelgangerStatus {
|
|
29
|
-
/** This pubkey is known to the doppelganger service and has been verified safe */
|
|
30
|
-
VerifiedSafe = "VerifiedSafe",
|
|
31
|
-
/** This pubkey is known to the doppelganger service but has not been verified safe */
|
|
32
|
-
Unverified = "Unverified",
|
|
33
|
-
/** This pubkey is unknown to the doppelganger service */
|
|
34
|
-
Unknown = "Unknown",
|
|
35
|
-
/** This pubkey has been detected to be active on the network */
|
|
36
|
-
DoppelgangerDetected = "DoppelgangerDetected",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class DoppelgangerService {
|
|
40
|
-
private readonly doppelgangerStateByPubkey = new Map<PubkeyHex, DoppelgangerState>();
|
|
41
|
-
|
|
42
|
-
constructor(
|
|
43
|
-
private readonly logger: Logger,
|
|
44
|
-
private readonly clock: IClock,
|
|
45
|
-
private readonly api: ApiClient,
|
|
46
|
-
private readonly indicesService: IndicesService,
|
|
47
|
-
private readonly slashingProtection: ISlashingProtection,
|
|
48
|
-
private readonly processShutdownCallback: ProcessShutdownCallback,
|
|
49
|
-
private readonly metrics: Metrics | null
|
|
50
|
-
) {
|
|
51
|
-
this.clock.runEveryEpoch(this.pollLiveness);
|
|
52
|
-
|
|
53
|
-
if (metrics) {
|
|
54
|
-
metrics.doppelganger.statusCount.addCollect(() => this.onScrapeMetrics(metrics));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
this.logger.info("Doppelganger protection enabled", {detectionEpochs: DEFAULT_REMAINING_DETECTION_EPOCHS});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async registerValidator(pubkeyHex: PubkeyHex): Promise<void> {
|
|
61
|
-
const {currentEpoch} = this.clock;
|
|
62
|
-
// Disable doppelganger protection when the validator was initialized before genesis.
|
|
63
|
-
// There's no activity before genesis, so doppelganger is pointless.
|
|
64
|
-
let remainingEpochs = currentEpoch <= 0 ? REMAINING_EPOCHS_IF_SKIPPED : DEFAULT_REMAINING_DETECTION_EPOCHS;
|
|
65
|
-
const nextEpochToCheck = currentEpoch + 1;
|
|
66
|
-
|
|
67
|
-
// Log here to alert that validation won't be active until remainingEpochs == 0
|
|
68
|
-
if (remainingEpochs > 0) {
|
|
69
|
-
const previousEpoch = currentEpoch - 1;
|
|
70
|
-
const attestedInPreviousEpoch = await this.slashingProtection.hasAttestedInEpoch(
|
|
71
|
-
fromHex(pubkeyHex),
|
|
72
|
-
previousEpoch
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (attestedInPreviousEpoch) {
|
|
76
|
-
// It is safe to skip doppelganger detection
|
|
77
|
-
// https://github.com/ChainSafe/lodestar/issues/5856
|
|
78
|
-
remainingEpochs = REMAINING_EPOCHS_IF_SKIPPED;
|
|
79
|
-
this.logger.info("Doppelganger detection skipped for validator because restart was detected", {
|
|
80
|
-
pubkey: truncBytes(pubkeyHex),
|
|
81
|
-
previousEpoch,
|
|
82
|
-
});
|
|
83
|
-
} else {
|
|
84
|
-
this.logger.info("Registered validator for doppelganger detection", {
|
|
85
|
-
pubkey: truncBytes(pubkeyHex),
|
|
86
|
-
remainingEpochs,
|
|
87
|
-
nextEpochToCheck,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
this.logger.info("Doppelganger detection skipped for validator initialized before genesis", {
|
|
92
|
-
pubkey: truncBytes(pubkeyHex),
|
|
93
|
-
currentEpoch,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
this.doppelgangerStateByPubkey.set(pubkeyHex, {
|
|
98
|
-
nextEpochToCheck,
|
|
99
|
-
remainingEpochs,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
unregisterValidator(pubkeyHex: PubkeyHex): void {
|
|
104
|
-
this.doppelgangerStateByPubkey.delete(pubkeyHex);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
getStatus(pubKeyHex: PubkeyHex): DoppelgangerStatus {
|
|
108
|
-
return getStatus(this.doppelgangerStateByPubkey.get(pubKeyHex));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
isDoppelgangerSafe(pubKeyHex: PubkeyHex): boolean {
|
|
112
|
-
return getStatus(this.doppelgangerStateByPubkey.get(pubKeyHex)) === DoppelgangerStatus.VerifiedSafe;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private pollLiveness = async (currentEpoch: Epoch, signal: AbortSignal): Promise<void> => {
|
|
116
|
-
if (currentEpoch < 0) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const endSlotOfCurrentEpoch = computeStartSlotAtEpoch(currentEpoch + 1) - 1;
|
|
121
|
-
// Run the doppelganger protection check 75% through the last slot of this epoch. This
|
|
122
|
-
// *should* mean that the BN has seen the blocks and attestations for the epoch
|
|
123
|
-
await sleep(this.clock.msToSlot(endSlotOfCurrentEpoch + 3 / 4), signal);
|
|
124
|
-
|
|
125
|
-
// Collect indices that still need doppelganger checks
|
|
126
|
-
const pubkeysToCheckWithoutIndex: PubkeyHex[] = [];
|
|
127
|
-
// Collect as Map for detectDoppelganger() which needs to map back index -> pubkey
|
|
128
|
-
const indicesToCheckMap = new Map<ValidatorIndex, PubkeyHex>();
|
|
129
|
-
|
|
130
|
-
for (const [pubkeyHex, state] of this.doppelgangerStateByPubkey.entries()) {
|
|
131
|
-
if (state.remainingEpochs > 0 && state.nextEpochToCheck <= currentEpoch) {
|
|
132
|
-
const index = this.indicesService.pubkey2index.get(pubkeyHex);
|
|
133
|
-
if (index !== undefined) {
|
|
134
|
-
indicesToCheckMap.set(index, pubkeyHex);
|
|
135
|
-
} else {
|
|
136
|
-
pubkeysToCheckWithoutIndex.push(pubkeyHex);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Attempt to collect missing indexes
|
|
142
|
-
const newIndices = await this.indicesService.pollValidatorIndices(pubkeysToCheckWithoutIndex);
|
|
143
|
-
for (const index of newIndices) {
|
|
144
|
-
const pubkey = this.indicesService.index2pubkey.get(index);
|
|
145
|
-
if (pubkey) {
|
|
146
|
-
indicesToCheckMap.set(index, pubkey);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (indicesToCheckMap.size === 0) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
this.logger.info("Doppelganger liveness check", {currentEpoch, indicesCount: indicesToCheckMap.size});
|
|
155
|
-
|
|
156
|
-
// in the current epoch also request for liveness check for past epoch in case a validator index was live
|
|
157
|
-
// in the remaining 25% of the last slot of the previous epoch
|
|
158
|
-
const indicesToCheck = Array.from(indicesToCheckMap.keys());
|
|
159
|
-
const [previous, current] = await Promise.all([
|
|
160
|
-
this.getLiveness(currentEpoch - 1, indicesToCheck),
|
|
161
|
-
this.getLiveness(currentEpoch, indicesToCheck),
|
|
162
|
-
]);
|
|
163
|
-
|
|
164
|
-
this.detectDoppelganger(currentEpoch, previous, current, indicesToCheckMap);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
private async getLiveness(epoch: Epoch, indicesToCheck: ValidatorIndex[]): Promise<EpochLivenessData> {
|
|
168
|
-
if (epoch < 0) {
|
|
169
|
-
return {epoch, responses: []};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const res = await this.api.validator.getLiveness({epoch, indices: indicesToCheck});
|
|
173
|
-
if (!res.ok) {
|
|
174
|
-
this.logger.error(`Error getting liveness data for epoch ${epoch}`, {}, res.error() as Error);
|
|
175
|
-
return {epoch, responses: []};
|
|
176
|
-
}
|
|
177
|
-
return {epoch, responses: res.value()};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private detectDoppelganger(
|
|
181
|
-
currentEpoch: Epoch,
|
|
182
|
-
previousEpochLiveness: EpochLivenessData,
|
|
183
|
-
currentEpochLiveness: EpochLivenessData,
|
|
184
|
-
indicesToCheckMap: Map<ValidatorIndex, PubkeyHex>
|
|
185
|
-
): void {
|
|
186
|
-
const previousEpoch = currentEpoch - 1;
|
|
187
|
-
const violators: ValidatorIndex[] = [];
|
|
188
|
-
|
|
189
|
-
// Perform a loop through the current and previous epoch responses and detect any violators.
|
|
190
|
-
//
|
|
191
|
-
// A following loop will update the states of each validator, depending on whether or not
|
|
192
|
-
// any violators were detected here.
|
|
193
|
-
|
|
194
|
-
for (const {epoch, responses} of [previousEpochLiveness, currentEpochLiveness]) {
|
|
195
|
-
for (const response of responses) {
|
|
196
|
-
if (!response.isLive) {
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const state = this.doppelgangerStateByPubkey.get(indicesToCheckMap.get(response.index) ?? "");
|
|
201
|
-
if (!state) {
|
|
202
|
-
this.logger.error(`Inconsistent livenessResponseData unknown index ${response.index}`);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (state.nextEpochToCheck <= epoch) {
|
|
207
|
-
// Doppelganger detected
|
|
208
|
-
violators.push(response.index);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (violators.length > 0) {
|
|
214
|
-
// If a single doppelganger is detected, enable doppelganger checks on all validators forever
|
|
215
|
-
for (const state of this.doppelgangerStateByPubkey.values()) {
|
|
216
|
-
state.remainingEpochs = REMAINING_EPOCHS_IF_DOPPELGANGER;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
this.logger.error(
|
|
220
|
-
`Doppelganger(s) detected
|
|
221
|
-
A doppelganger occurs when two different validator clients run the same public key.
|
|
222
|
-
This validator client detected another instance of a local validator on the network
|
|
223
|
-
and is shutting down to prevent potential slashable offenses. Ensure that you are not
|
|
224
|
-
running a duplicate or overlapping validator client`,
|
|
225
|
-
violators
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
// Request process to shutdown
|
|
229
|
-
this.processShutdownCallback(Error("Doppelganger(s) detected"));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// If not there are no validators
|
|
233
|
-
else {
|
|
234
|
-
// Iterate through all the previous epoch responses, updating `self.doppelganger_states`.
|
|
235
|
-
//
|
|
236
|
-
// Do not bother iterating through the current epoch responses since they've already been
|
|
237
|
-
// checked for violators and they don't result in updating the state.
|
|
238
|
-
for (const response of previousEpochLiveness.responses) {
|
|
239
|
-
const state = this.doppelgangerStateByPubkey.get(indicesToCheckMap.get(response.index) ?? "");
|
|
240
|
-
if (!state) {
|
|
241
|
-
this.logger.error(`Inconsistent livenessResponseData unknown index ${response.index}`);
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!response.isLive && state.nextEpochToCheck <= previousEpoch) {
|
|
246
|
-
state.remainingEpochs--;
|
|
247
|
-
state.nextEpochToCheck = currentEpoch;
|
|
248
|
-
this.metrics?.doppelganger.epochsChecked.inc(1);
|
|
249
|
-
|
|
250
|
-
const {remainingEpochs, nextEpochToCheck} = state;
|
|
251
|
-
if (remainingEpochs <= 0) {
|
|
252
|
-
this.logger.info("Doppelganger detection complete", {index: response.index, epoch: currentEpoch});
|
|
253
|
-
} else {
|
|
254
|
-
this.logger.info("Found no doppelganger", {index: response.index, remainingEpochs, nextEpochToCheck});
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private onScrapeMetrics(metrics: Metrics): void {
|
|
262
|
-
const countByStatus = new Map<DoppelgangerStatus, number>();
|
|
263
|
-
for (const state of this.doppelgangerStateByPubkey.values()) {
|
|
264
|
-
const status = getStatus(state);
|
|
265
|
-
countByStatus.set(status, (countByStatus.get(status) ?? 0) + 1);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Loop over DoppelgangerStatus not countByStatus to zero status without counts
|
|
269
|
-
for (const status of Object.values(DoppelgangerStatus)) {
|
|
270
|
-
metrics.doppelganger.statusCount.set({status}, countByStatus.get(status) ?? 0);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function getStatus(state: DoppelgangerState | undefined): DoppelgangerStatus {
|
|
276
|
-
if (!state) {
|
|
277
|
-
return DoppelgangerStatus.Unknown;
|
|
278
|
-
}
|
|
279
|
-
if (state.remainingEpochs <= 0) {
|
|
280
|
-
return DoppelgangerStatus.VerifiedSafe;
|
|
281
|
-
}
|
|
282
|
-
if (state.remainingEpochs === REMAINING_EPOCHS_IF_DOPPELGANGER) {
|
|
283
|
-
return DoppelgangerStatus.DoppelgangerDetected;
|
|
284
|
-
}
|
|
285
|
-
return DoppelgangerStatus.Unverified;
|
|
286
|
-
}
|
package/src/services/emitter.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import {EventEmitter} from "node:events";
|
|
2
|
-
import {StrictEventEmitter} from "strict-event-emitter-types";
|
|
3
|
-
import {Slot} from "@lodestar/types";
|
|
4
|
-
import {HeadEventData} from "./chainHeaderTracker.js";
|
|
5
|
-
|
|
6
|
-
export enum ValidatorEvent {
|
|
7
|
-
/**
|
|
8
|
-
* This event signals that the node chain has a new head.
|
|
9
|
-
*/
|
|
10
|
-
chainHead = "chainHead",
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type ValidatorEvents = {
|
|
14
|
-
[ValidatorEvent.chainHead]: (head: HeadEventData) => void;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Emit important validator events.
|
|
19
|
-
*/
|
|
20
|
-
export class ValidatorEventEmitter extends (EventEmitter as {
|
|
21
|
-
new (): StrictEventEmitter<EventEmitter, ValidatorEvents>;
|
|
22
|
-
}) {
|
|
23
|
-
/**
|
|
24
|
-
* Wait for the first block to come with slot >= provided slot.
|
|
25
|
-
*/
|
|
26
|
-
async waitForBlockSlot(slot: Slot): Promise<void> {
|
|
27
|
-
let headListener: (head: HeadEventData) => void;
|
|
28
|
-
|
|
29
|
-
const onDone = (): void => {
|
|
30
|
-
this.off(ValidatorEvent.chainHead, headListener);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
headListener = (head: HeadEventData): void => {
|
|
35
|
-
if (head.slot >= slot) {
|
|
36
|
-
onDone();
|
|
37
|
-
resolve();
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
this.on(ValidatorEvent.chainHead, headListener);
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|