@lodestar/validator 1.35.0-dev.f80d2d52da → 1.35.0-dev.fcf8d024ea
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/lib/buckets.d.ts.map +1 -0
- package/lib/defaults.d.ts.map +1 -0
- package/lib/genesis.d.ts.map +1 -0
- package/lib/index.d.ts +7 -7
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +5 -5
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts.map +1 -0
- package/lib/metrics.js +14 -14
- package/lib/metrics.js.map +1 -1
- package/lib/repositories/index.d.ts.map +1 -0
- package/lib/repositories/metaDataRepository.d.ts.map +1 -0
- package/lib/repositories/metaDataRepository.js +4 -3
- package/lib/repositories/metaDataRepository.js.map +1 -1
- package/lib/services/attestation.d.ts.map +1 -0
- package/lib/services/attestation.js +77 -60
- package/lib/services/attestation.js.map +1 -1
- package/lib/services/attestationDuties.d.ts.map +1 -0
- package/lib/services/attestationDuties.js +105 -98
- package/lib/services/attestationDuties.js.map +1 -1
- package/lib/services/block.d.ts.map +1 -0
- package/lib/services/block.js +64 -56
- package/lib/services/block.js.map +1 -1
- package/lib/services/blockDuties.d.ts +2 -2
- package/lib/services/blockDuties.d.ts.map +1 -0
- package/lib/services/blockDuties.js +35 -26
- package/lib/services/blockDuties.js.map +1 -1
- package/lib/services/chainHeaderTracker.d.ts.map +1 -0
- package/lib/services/chainHeaderTracker.js +30 -27
- package/lib/services/chainHeaderTracker.js.map +1 -1
- package/lib/services/doppelgangerService.d.ts.map +1 -0
- package/lib/services/doppelgangerService.js +52 -45
- package/lib/services/doppelgangerService.js.map +1 -1
- package/lib/services/emitter.d.ts +1 -1
- package/lib/services/emitter.d.ts.map +1 -0
- package/lib/services/externalSignerSync.d.ts.map +1 -0
- package/lib/services/externalSignerSync.js.map +1 -1
- package/lib/services/indices.d.ts.map +1 -0
- package/lib/services/indices.js +8 -5
- package/lib/services/indices.js.map +1 -1
- package/lib/services/prepareBeaconProposer.d.ts.map +1 -0
- package/lib/services/prepareBeaconProposer.js.map +1 -1
- package/lib/services/syncCommittee.d.ts.map +1 -0
- package/lib/services/syncCommittee.js +80 -61
- package/lib/services/syncCommittee.js.map +1 -1
- package/lib/services/syncCommitteeDuties.d.ts.map +1 -0
- package/lib/services/syncCommitteeDuties.js +28 -23
- package/lib/services/syncCommitteeDuties.js.map +1 -1
- package/lib/services/syncingStatusTracker.d.ts.map +1 -0
- package/lib/services/syncingStatusTracker.js +32 -27
- package/lib/services/syncingStatusTracker.js.map +1 -1
- package/lib/services/utils.d.ts.map +1 -0
- package/lib/services/validatorStore.d.ts.map +1 -0
- package/lib/services/validatorStore.js +9 -3
- package/lib/services/validatorStore.js.map +1 -1
- package/lib/slashingProtection/attestation/attestationByTargetRepository.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/attestationByTargetRepository.js +7 -3
- package/lib/slashingProtection/attestation/attestationByTargetRepository.js.map +1 -1
- package/lib/slashingProtection/attestation/attestationLowerBoundRepository.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js +5 -3
- package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js.map +1 -1
- package/lib/slashingProtection/attestation/errors.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/index.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/index.js +3 -0
- package/lib/slashingProtection/attestation/index.js.map +1 -1
- package/lib/slashingProtection/block/blockBySlotRepository.d.ts.map +1 -0
- package/lib/slashingProtection/block/blockBySlotRepository.js +7 -3
- package/lib/slashingProtection/block/blockBySlotRepository.js.map +1 -1
- package/lib/slashingProtection/block/errors.d.ts.map +1 -0
- package/lib/slashingProtection/block/index.d.ts.map +1 -0
- package/lib/slashingProtection/block/index.js +1 -0
- package/lib/slashingProtection/block/index.js.map +1 -1
- package/lib/slashingProtection/index.d.ts +1 -1
- package/lib/slashingProtection/index.d.ts.map +1 -0
- package/lib/slashingProtection/index.js +3 -0
- package/lib/slashingProtection/index.js.map +1 -1
- package/lib/slashingProtection/interchange/errors.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/formats/completeV4.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/formats/index.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/formats/v5.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/index.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/parseInterchange.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/serializeInterchange.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/types.d.ts.map +1 -0
- package/lib/slashingProtection/interface.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js +8 -0
- package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js.map +1 -1
- package/lib/slashingProtection/minMaxSurround/errors.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/index.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/interface.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/minMaxSurround.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/minMaxSurround.js +2 -0
- package/lib/slashingProtection/minMaxSurround/minMaxSurround.js.map +1 -1
- package/lib/slashingProtection/types.d.ts.map +1 -0
- package/lib/slashingProtection/utils.d.ts +1 -1
- package/lib/slashingProtection/utils.d.ts.map +1 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/util/batch.d.ts.map +1 -0
- package/lib/util/clock.d.ts +3 -0
- package/lib/util/clock.d.ts.map +1 -0
- package/lib/util/clock.js +9 -1
- package/lib/util/clock.js.map +1 -1
- package/lib/util/difference.d.ts.map +1 -0
- package/lib/util/externalSignerClient.d.ts.map +1 -0
- package/lib/util/format.d.ts.map +1 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/logger.d.ts.map +1 -0
- package/lib/util/params.d.ts.map +1 -0
- package/lib/util/params.js +18 -2
- package/lib/util/params.js.map +1 -1
- package/lib/util/url.d.ts.map +1 -0
- package/lib/validator.d.ts.map +1 -0
- package/lib/validator.js +15 -0
- package/lib/validator.js.map +1 -1
- package/package.json +19 -16
- package/src/buckets.ts +30 -0
- package/src/defaults.ts +8 -0
- package/src/genesis.ts +19 -0
- package/src/index.ts +22 -0
- package/src/metrics.ts +417 -0
- package/src/repositories/index.ts +1 -0
- package/src/repositories/metaDataRepository.ts +42 -0
- package/src/services/attestation.ts +362 -0
- package/src/services/attestationDuties.ts +406 -0
- package/src/services/block.ts +261 -0
- package/src/services/blockDuties.ts +217 -0
- package/src/services/chainHeaderTracker.ts +89 -0
- package/src/services/doppelgangerService.ts +286 -0
- package/src/services/emitter.ts +43 -0
- package/src/services/externalSignerSync.ts +81 -0
- package/src/services/indices.ts +165 -0
- package/src/services/prepareBeaconProposer.ts +119 -0
- package/src/services/syncCommittee.ts +338 -0
- package/src/services/syncCommitteeDuties.ts +337 -0
- package/src/services/syncingStatusTracker.ts +74 -0
- package/src/services/utils.ts +58 -0
- package/src/services/validatorStore.ts +830 -0
- package/src/slashingProtection/attestation/attestationByTargetRepository.ts +77 -0
- package/src/slashingProtection/attestation/attestationLowerBoundRepository.ts +44 -0
- package/src/slashingProtection/attestation/errors.ts +66 -0
- package/src/slashingProtection/attestation/index.ts +171 -0
- package/src/slashingProtection/block/blockBySlotRepository.ts +78 -0
- package/src/slashingProtection/block/errors.ts +28 -0
- package/src/slashingProtection/block/index.ts +94 -0
- package/src/slashingProtection/index.ts +95 -0
- package/src/slashingProtection/interchange/errors.ts +15 -0
- package/src/slashingProtection/interchange/formats/completeV4.ts +125 -0
- package/src/slashingProtection/interchange/formats/index.ts +7 -0
- package/src/slashingProtection/interchange/formats/v5.ts +120 -0
- package/src/slashingProtection/interchange/index.ts +5 -0
- package/src/slashingProtection/interchange/parseInterchange.ts +55 -0
- package/src/slashingProtection/interchange/serializeInterchange.ts +35 -0
- package/src/slashingProtection/interchange/types.ts +18 -0
- package/src/slashingProtection/interface.ts +28 -0
- package/src/slashingProtection/minMaxSurround/distanceStoreRepository.ts +57 -0
- package/src/slashingProtection/minMaxSurround/errors.ts +27 -0
- package/src/slashingProtection/minMaxSurround/index.ts +4 -0
- package/src/slashingProtection/minMaxSurround/interface.ts +23 -0
- package/src/slashingProtection/minMaxSurround/minMaxSurround.ts +104 -0
- package/src/slashingProtection/types.ts +12 -0
- package/src/slashingProtection/utils.ts +42 -0
- package/src/types.ts +31 -0
- package/src/util/batch.ts +15 -0
- package/src/util/clock.ts +170 -0
- package/src/util/difference.ts +10 -0
- package/src/util/externalSignerClient.ts +277 -0
- package/src/util/format.ts +3 -0
- package/src/util/index.ts +6 -0
- package/src/util/logger.ts +51 -0
- package/src/util/params.ts +320 -0
- package/src/util/url.ts +16 -0
- package/src/validator.ts +418 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import {SecretKey} from "@chainsafe/blst";
|
|
2
|
+
import {BitArray} from "@chainsafe/ssz";
|
|
3
|
+
import {routes} from "@lodestar/api";
|
|
4
|
+
import {BeaconConfig} from "@lodestar/config";
|
|
5
|
+
import {
|
|
6
|
+
DOMAIN_AGGREGATE_AND_PROOF,
|
|
7
|
+
DOMAIN_APPLICATION_BUILDER,
|
|
8
|
+
DOMAIN_BEACON_ATTESTER,
|
|
9
|
+
DOMAIN_BEACON_PROPOSER,
|
|
10
|
+
DOMAIN_CONTRIBUTION_AND_PROOF,
|
|
11
|
+
DOMAIN_RANDAO,
|
|
12
|
+
DOMAIN_SELECTION_PROOF,
|
|
13
|
+
DOMAIN_SYNC_COMMITTEE,
|
|
14
|
+
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF,
|
|
15
|
+
ForkSeq,
|
|
16
|
+
} from "@lodestar/params";
|
|
17
|
+
import {
|
|
18
|
+
ZERO_HASH,
|
|
19
|
+
blindedOrFullBlockHashTreeRoot,
|
|
20
|
+
computeDomain,
|
|
21
|
+
computeEpochAtSlot,
|
|
22
|
+
computeSigningRoot,
|
|
23
|
+
computeStartSlotAtEpoch,
|
|
24
|
+
} from "@lodestar/state-transition";
|
|
25
|
+
import {
|
|
26
|
+
AggregateAndProof,
|
|
27
|
+
Attestation,
|
|
28
|
+
BLSPubkey,
|
|
29
|
+
BLSSignature,
|
|
30
|
+
BeaconBlock,
|
|
31
|
+
BlindedBeaconBlock,
|
|
32
|
+
Epoch,
|
|
33
|
+
Root,
|
|
34
|
+
SignedAggregateAndProof,
|
|
35
|
+
SignedBeaconBlock,
|
|
36
|
+
SignedBlindedBeaconBlock,
|
|
37
|
+
SingleAttestation,
|
|
38
|
+
Slot,
|
|
39
|
+
ValidatorIndex,
|
|
40
|
+
altair,
|
|
41
|
+
bellatrix,
|
|
42
|
+
phase0,
|
|
43
|
+
ssz,
|
|
44
|
+
} from "@lodestar/types";
|
|
45
|
+
import {fromHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
|
|
46
|
+
import {Metrics} from "../metrics.js";
|
|
47
|
+
import {ISlashingProtection} from "../slashingProtection/index.js";
|
|
48
|
+
import {PubkeyHex} from "../types.js";
|
|
49
|
+
import {SignableMessage, SignableMessageType, externalSignerPostSignature} from "../util/externalSignerClient.js";
|
|
50
|
+
import {isValidatePubkeyHex} from "../util/format.js";
|
|
51
|
+
import {LoggerVc} from "../util/logger.js";
|
|
52
|
+
import {DoppelgangerService} from "./doppelgangerService.js";
|
|
53
|
+
import {IndicesService} from "./indices.js";
|
|
54
|
+
|
|
55
|
+
type BLSPubkeyMaybeHex = BLSPubkey | PubkeyHex;
|
|
56
|
+
type Eth1Address = string;
|
|
57
|
+
|
|
58
|
+
export enum SignerType {
|
|
59
|
+
Local,
|
|
60
|
+
Remote,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SignerLocal = {
|
|
64
|
+
type: SignerType.Local;
|
|
65
|
+
secretKey: SecretKey;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type SignerRemote = {
|
|
69
|
+
type: SignerType.Remote;
|
|
70
|
+
url: string;
|
|
71
|
+
pubkey: PubkeyHex;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type DefaultProposerConfig = {
|
|
75
|
+
graffiti?: string;
|
|
76
|
+
strictFeeRecipientCheck: boolean;
|
|
77
|
+
feeRecipient: Eth1Address;
|
|
78
|
+
builder: {
|
|
79
|
+
gasLimit: number;
|
|
80
|
+
selection: routes.validator.BuilderSelection;
|
|
81
|
+
boostFactor: bigint;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type ProposerConfig = {
|
|
86
|
+
graffiti?: string;
|
|
87
|
+
strictFeeRecipientCheck?: boolean;
|
|
88
|
+
feeRecipient?: Eth1Address;
|
|
89
|
+
builder?: {
|
|
90
|
+
gasLimit?: number;
|
|
91
|
+
selection?: routes.validator.BuilderSelection;
|
|
92
|
+
boostFactor?: bigint;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type ValidatorProposerConfig = {
|
|
97
|
+
proposerConfig: {[index: PubkeyHex]: ProposerConfig};
|
|
98
|
+
defaultConfig: ProposerConfig;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type ValidatorStoreModules = {
|
|
102
|
+
config: BeaconConfig;
|
|
103
|
+
slashingProtection: ISlashingProtection;
|
|
104
|
+
indicesService: IndicesService;
|
|
105
|
+
doppelgangerService: DoppelgangerService | null;
|
|
106
|
+
metrics: Metrics | null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* This cache stores SignedValidatorRegistrationV1 data for a validator so that
|
|
111
|
+
* we do not create and send new registration objects to avoid DOSing the builder
|
|
112
|
+
*
|
|
113
|
+
* See: https://github.com/ChainSafe/lodestar/issues/4208
|
|
114
|
+
*/
|
|
115
|
+
type BuilderData = {
|
|
116
|
+
validatorRegistration: bellatrix.SignedValidatorRegistrationV1;
|
|
117
|
+
regFullKey: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validator entity capable of producing signatures. Either:
|
|
122
|
+
* - local: With BLS secret key
|
|
123
|
+
* - remote: With data to contact a remote signer
|
|
124
|
+
*/
|
|
125
|
+
export type Signer = SignerLocal | SignerRemote;
|
|
126
|
+
|
|
127
|
+
type ValidatorData = ProposerConfig & {
|
|
128
|
+
signer: Signer;
|
|
129
|
+
builderData?: BuilderData;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const defaultOptions = {
|
|
133
|
+
suggestedFeeRecipient: "0x0000000000000000000000000000000000000000",
|
|
134
|
+
defaultGasLimit: 45_000_000,
|
|
135
|
+
builderSelection: routes.validator.BuilderSelection.ExecutionOnly,
|
|
136
|
+
builderAliasSelection: routes.validator.BuilderSelection.Default,
|
|
137
|
+
builderBoostFactor: BigInt(100),
|
|
138
|
+
// spec asks for gossip validation by default
|
|
139
|
+
broadcastValidation: routes.beacon.BroadcastValidation.gossip,
|
|
140
|
+
// should request fetching the locally produced block in blinded format
|
|
141
|
+
blindedLocal: false,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const MAX_BUILDER_BOOST_FACTOR = 2n ** 64n - 1n;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Service that sets up and handles validator attester duties.
|
|
148
|
+
*/
|
|
149
|
+
export class ValidatorStore {
|
|
150
|
+
private readonly config: BeaconConfig;
|
|
151
|
+
private readonly slashingProtection: ISlashingProtection;
|
|
152
|
+
private readonly indicesService: IndicesService;
|
|
153
|
+
private readonly doppelgangerService: DoppelgangerService | null;
|
|
154
|
+
private readonly metrics: Metrics | null;
|
|
155
|
+
|
|
156
|
+
private readonly validators = new Map<PubkeyHex, ValidatorData>();
|
|
157
|
+
/** Initially true because there are no validators */
|
|
158
|
+
private pubkeysToDiscover: PubkeyHex[] = [];
|
|
159
|
+
private readonly defaultProposerConfig: DefaultProposerConfig;
|
|
160
|
+
|
|
161
|
+
constructor(modules: ValidatorStoreModules, valProposerConfig: ValidatorProposerConfig) {
|
|
162
|
+
const {config, slashingProtection, indicesService, doppelgangerService, metrics} = modules;
|
|
163
|
+
this.config = config;
|
|
164
|
+
this.slashingProtection = slashingProtection;
|
|
165
|
+
this.indicesService = indicesService;
|
|
166
|
+
this.doppelgangerService = doppelgangerService;
|
|
167
|
+
this.metrics = metrics;
|
|
168
|
+
|
|
169
|
+
const defaultConfig = valProposerConfig.defaultConfig;
|
|
170
|
+
const builderBoostFactor = defaultConfig.builder?.boostFactor ?? defaultOptions.builderBoostFactor;
|
|
171
|
+
if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
|
|
172
|
+
throw Error(`Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR for defaultConfig`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.defaultProposerConfig = {
|
|
176
|
+
graffiti: defaultConfig.graffiti,
|
|
177
|
+
strictFeeRecipientCheck: defaultConfig.strictFeeRecipientCheck ?? false,
|
|
178
|
+
feeRecipient: defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient,
|
|
179
|
+
builder: {
|
|
180
|
+
gasLimit: defaultConfig.builder?.gasLimit ?? defaultOptions.defaultGasLimit,
|
|
181
|
+
selection: defaultConfig.builder?.selection ?? defaultOptions.builderSelection,
|
|
182
|
+
boostFactor: builderBoostFactor,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (metrics) {
|
|
187
|
+
metrics.signers.addCollect(() => metrics.signers.set(this.validators.size));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a validator store with initial signers
|
|
193
|
+
*/
|
|
194
|
+
static async init(
|
|
195
|
+
modules: ValidatorStoreModules,
|
|
196
|
+
initialSigners: Signer[],
|
|
197
|
+
valProposerConfig: ValidatorProposerConfig = {defaultConfig: {}, proposerConfig: {}}
|
|
198
|
+
): Promise<ValidatorStore> {
|
|
199
|
+
const validatorStore = new ValidatorStore(modules, valProposerConfig);
|
|
200
|
+
|
|
201
|
+
await Promise.all(initialSigners.map((signer) => validatorStore.addSigner(signer, valProposerConfig)));
|
|
202
|
+
|
|
203
|
+
return validatorStore;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Return all known indices from the validatorStore pubkeys */
|
|
207
|
+
getAllLocalIndices(): ValidatorIndex[] {
|
|
208
|
+
return this.indicesService.getAllLocalIndices();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getPubkeyOfIndex(index: ValidatorIndex): PubkeyHex | undefined {
|
|
212
|
+
return this.indicesService.index2pubkey.get(index);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pollValidatorIndices(): Promise<ValidatorIndex[]> {
|
|
216
|
+
// Consumers will call this function every epoch forever. If everyone has been discovered, skip
|
|
217
|
+
return this.indicesService.indexCount >= this.validators.size
|
|
218
|
+
? Promise.resolve([])
|
|
219
|
+
: this.indicesService.pollValidatorIndices(Array.from(this.validators.keys()));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getFeeRecipient(pubkeyHex: PubkeyHex): Eth1Address {
|
|
223
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
224
|
+
if (validatorData === undefined) {
|
|
225
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
226
|
+
}
|
|
227
|
+
return validatorData.feeRecipient ?? this.defaultProposerConfig.feeRecipient;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getFeeRecipientByIndex(index: ValidatorIndex): Eth1Address {
|
|
231
|
+
const pubkey = this.indicesService.index2pubkey.get(index);
|
|
232
|
+
return pubkey ? this.getFeeRecipient(pubkey) : this.defaultProposerConfig.feeRecipient;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setFeeRecipient(pubkeyHex: PubkeyHex, feeRecipient: Eth1Address): void {
|
|
236
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
237
|
+
if (validatorData === undefined) {
|
|
238
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
239
|
+
}
|
|
240
|
+
// This should directly modify data in the map
|
|
241
|
+
validatorData.feeRecipient = feeRecipient;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
deleteFeeRecipient(pubkeyHex: PubkeyHex): void {
|
|
245
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
246
|
+
if (validatorData === undefined) {
|
|
247
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
248
|
+
}
|
|
249
|
+
// This should directly modify data in the map
|
|
250
|
+
delete validatorData.feeRecipient;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getGraffiti(pubkeyHex: PubkeyHex): string | undefined {
|
|
254
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
255
|
+
if (validatorData === undefined) {
|
|
256
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
257
|
+
}
|
|
258
|
+
return validatorData.graffiti ?? this.defaultProposerConfig.graffiti;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setGraffiti(pubkeyHex: PubkeyHex, graffiti: string): void {
|
|
262
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
263
|
+
if (validatorData === undefined) {
|
|
264
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
265
|
+
}
|
|
266
|
+
validatorData.graffiti = graffiti;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
deleteGraffiti(pubkeyHex: PubkeyHex): void {
|
|
270
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
271
|
+
if (validatorData === undefined) {
|
|
272
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
273
|
+
}
|
|
274
|
+
delete validatorData.graffiti;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getBuilderSelectionParams(pubkeyHex: PubkeyHex): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
|
|
278
|
+
const selection =
|
|
279
|
+
this.validators.get(pubkeyHex)?.builder?.selection ?? this.defaultProposerConfig.builder.selection;
|
|
280
|
+
|
|
281
|
+
let boostFactor: bigint;
|
|
282
|
+
switch (selection) {
|
|
283
|
+
case routes.validator.BuilderSelection.Default:
|
|
284
|
+
// Default value slightly favors local block to improve censorship resistance of Ethereum
|
|
285
|
+
// The people have spoken and so it shall be https://x.com/lodestar_eth/status/1772679499928191044
|
|
286
|
+
boostFactor = BigInt(90);
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case routes.validator.BuilderSelection.MaxProfit:
|
|
290
|
+
boostFactor =
|
|
291
|
+
this.validators.get(pubkeyHex)?.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case routes.validator.BuilderSelection.BuilderAlways:
|
|
295
|
+
case routes.validator.BuilderSelection.BuilderOnly:
|
|
296
|
+
boostFactor = MAX_BUILDER_BOOST_FACTOR;
|
|
297
|
+
break;
|
|
298
|
+
|
|
299
|
+
case routes.validator.BuilderSelection.ExecutionAlways:
|
|
300
|
+
case routes.validator.BuilderSelection.ExecutionOnly:
|
|
301
|
+
boostFactor = BigInt(0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {selection, boostFactor};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
strictFeeRecipientCheck(pubkeyHex: PubkeyHex): boolean {
|
|
308
|
+
return (
|
|
309
|
+
this.validators.get(pubkeyHex)?.strictFeeRecipientCheck ?? this.defaultProposerConfig?.strictFeeRecipientCheck
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getGasLimit(pubkeyHex: PubkeyHex): number {
|
|
314
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
315
|
+
if (validatorData === undefined) {
|
|
316
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
317
|
+
}
|
|
318
|
+
return validatorData?.builder?.gasLimit ?? this.defaultProposerConfig.builder.gasLimit;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
setGasLimit(pubkeyHex: PubkeyHex, gasLimit: number): void {
|
|
322
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
323
|
+
if (validatorData === undefined) {
|
|
324
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
325
|
+
}
|
|
326
|
+
validatorData.builder = {...validatorData.builder, gasLimit};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
deleteGasLimit(pubkeyHex: PubkeyHex): void {
|
|
330
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
331
|
+
if (validatorData === undefined) {
|
|
332
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
333
|
+
}
|
|
334
|
+
delete validatorData.builder?.gasLimit;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getBuilderBoostFactor(pubkeyHex: PubkeyHex): bigint {
|
|
338
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
339
|
+
if (validatorData === undefined) {
|
|
340
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
341
|
+
}
|
|
342
|
+
return validatorData?.builder?.boostFactor ?? this.defaultProposerConfig.builder.boostFactor;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
setBuilderBoostFactor(pubkeyHex: PubkeyHex, boostFactor: bigint): void {
|
|
346
|
+
if (boostFactor > MAX_BUILDER_BOOST_FACTOR) {
|
|
347
|
+
throw Error(`Invalid builderBoostFactor=${boostFactor} > MAX_BUILDER_BOOST_FACTOR`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
351
|
+
if (validatorData === undefined) {
|
|
352
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
353
|
+
}
|
|
354
|
+
validatorData.builder = {...validatorData.builder, boostFactor};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
deleteBuilderBoostFactor(pubkeyHex: PubkeyHex): void {
|
|
358
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
359
|
+
if (validatorData === undefined) {
|
|
360
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
361
|
+
}
|
|
362
|
+
delete validatorData.builder?.boostFactor;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Return true if `index` is active part of this validator client */
|
|
366
|
+
hasValidatorIndex(index: ValidatorIndex): boolean {
|
|
367
|
+
return this.indicesService.index2pubkey.has(index);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
getProposerConfig(pubkeyHex: PubkeyHex): ProposerConfig | null {
|
|
371
|
+
let proposerConfig: ProposerConfig | null = null;
|
|
372
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
373
|
+
if (validatorData === undefined) {
|
|
374
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const {graffiti, strictFeeRecipientCheck, feeRecipient, builder} = validatorData;
|
|
378
|
+
|
|
379
|
+
// if anything is set , i.e not default then return
|
|
380
|
+
if (
|
|
381
|
+
graffiti !== undefined ||
|
|
382
|
+
strictFeeRecipientCheck !== undefined ||
|
|
383
|
+
feeRecipient !== undefined ||
|
|
384
|
+
builder?.gasLimit !== undefined ||
|
|
385
|
+
builder?.selection !== undefined ||
|
|
386
|
+
builder?.boostFactor !== undefined
|
|
387
|
+
) {
|
|
388
|
+
proposerConfig = {graffiti, strictFeeRecipientCheck, feeRecipient, builder};
|
|
389
|
+
}
|
|
390
|
+
return proposerConfig;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async addSigner(signer: Signer, valProposerConfig?: ValidatorProposerConfig): Promise<void> {
|
|
394
|
+
const pubkey = getSignerPubkeyHex(signer);
|
|
395
|
+
const proposerConfig = valProposerConfig?.proposerConfig?.[pubkey];
|
|
396
|
+
const builderBoostFactor = proposerConfig?.builder?.boostFactor;
|
|
397
|
+
if (builderBoostFactor !== undefined && builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
|
|
398
|
+
throw Error(`Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR for pubkey=${pubkey}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!this.validators.has(pubkey)) {
|
|
402
|
+
// Doppelganger registration must be done before adding validator to signers
|
|
403
|
+
await this.doppelgangerService?.registerValidator(pubkey);
|
|
404
|
+
|
|
405
|
+
this.pubkeysToDiscover.push(pubkey);
|
|
406
|
+
this.validators.set(pubkey, {
|
|
407
|
+
signer,
|
|
408
|
+
...proposerConfig,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getSigner(pubkeyHex: PubkeyHex): Signer | undefined {
|
|
414
|
+
return this.validators.get(pubkeyHex)?.signer;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
removeSigner(pubkeyHex: PubkeyHex): boolean {
|
|
418
|
+
this.doppelgangerService?.unregisterValidator(pubkeyHex);
|
|
419
|
+
|
|
420
|
+
return this.indicesService.removeForKey(pubkeyHex) || this.validators.delete(pubkeyHex);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Return true if there is at least 1 pubkey registered */
|
|
424
|
+
hasSomeValidators(): boolean {
|
|
425
|
+
return this.validators.size > 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
votingPubkeys(): PubkeyHex[] {
|
|
429
|
+
return Array.from(this.validators.keys());
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
hasVotingPubkey(pubkeyHex: PubkeyHex): boolean {
|
|
433
|
+
return this.validators.has(pubkeyHex);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
getRemoteSignerPubkeys(signerUrl: string): PubkeyHex[] {
|
|
437
|
+
const pubkeysHex = [];
|
|
438
|
+
for (const {signer} of this.validators.values()) {
|
|
439
|
+
if (signer.type === SignerType.Remote && signer.url === signerUrl) {
|
|
440
|
+
pubkeysHex.push(signer.pubkey);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return pubkeysHex;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async signBlock(
|
|
447
|
+
pubkey: BLSPubkey,
|
|
448
|
+
blindedOrFull: BeaconBlock | BlindedBeaconBlock,
|
|
449
|
+
currentSlot: Slot,
|
|
450
|
+
logger?: LoggerVc
|
|
451
|
+
): Promise<SignedBeaconBlock | SignedBlindedBeaconBlock> {
|
|
452
|
+
// Make sure the block slot is not higher than the current slot to avoid potential attacks.
|
|
453
|
+
if (blindedOrFull.slot > currentSlot) {
|
|
454
|
+
throw Error(`Not signing block with slot ${blindedOrFull.slot} greater than current slot ${currentSlot}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Duties are filtered before-hard by doppelganger-safe, this assert should never throw
|
|
458
|
+
this.assertDoppelgangerSafe(pubkey);
|
|
459
|
+
|
|
460
|
+
const signingSlot = blindedOrFull.slot;
|
|
461
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_BEACON_PROPOSER);
|
|
462
|
+
const blockRoot = blindedOrFullBlockHashTreeRoot(this.config, blindedOrFull);
|
|
463
|
+
// Don't use `computeSigningRoot()` here to compute the objectRoot in typesafe function blindedOrFullBlockHashTreeRoot()
|
|
464
|
+
const signingRoot = ssz.phase0.SigningData.hashTreeRoot({objectRoot: blockRoot, domain});
|
|
465
|
+
|
|
466
|
+
logger?.debug("Signing the block proposal", {
|
|
467
|
+
slot: signingSlot,
|
|
468
|
+
blockRoot: toRootHex(blockRoot),
|
|
469
|
+
signingRoot: toRootHex(signingRoot),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
await this.slashingProtection.checkAndInsertBlockProposal(pubkey, {slot: signingSlot, signingRoot});
|
|
474
|
+
} catch (e) {
|
|
475
|
+
this.metrics?.slashingProtectionBlockError.inc();
|
|
476
|
+
throw e;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const signableMessage: SignableMessage = {
|
|
480
|
+
type: SignableMessageType.BLOCK_V2,
|
|
481
|
+
data: blindedOrFull,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
message: blindedOrFull,
|
|
486
|
+
signature: await this.getSignature(pubkey, signingRoot, signingSlot, signableMessage),
|
|
487
|
+
} as SignedBeaconBlock | SignedBlindedBeaconBlock;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async signRandao(pubkey: BLSPubkey, slot: Slot): Promise<BLSSignature> {
|
|
491
|
+
const signingSlot = slot;
|
|
492
|
+
const domain = this.config.getDomain(slot, DOMAIN_RANDAO);
|
|
493
|
+
const epoch = computeEpochAtSlot(slot);
|
|
494
|
+
const signingRoot = computeSigningRoot(ssz.Epoch, epoch, domain);
|
|
495
|
+
|
|
496
|
+
const signableMessage: SignableMessage = {
|
|
497
|
+
type: SignableMessageType.RANDAO_REVEAL,
|
|
498
|
+
data: {epoch},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return this.getSignature(pubkey, signingRoot, signingSlot, signableMessage);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async signAttestation(
|
|
505
|
+
duty: routes.validator.AttesterDuty,
|
|
506
|
+
attestationData: phase0.AttestationData,
|
|
507
|
+
currentEpoch: Epoch
|
|
508
|
+
): Promise<SingleAttestation> {
|
|
509
|
+
// Make sure the target epoch is not higher than the current epoch to avoid potential attacks.
|
|
510
|
+
if (attestationData.target.epoch > currentEpoch) {
|
|
511
|
+
throw Error(
|
|
512
|
+
`Not signing attestation with target epoch ${attestationData.target.epoch} greater than current epoch ${currentEpoch}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Duties are filtered before-hard by doppelganger-safe, this assert should never throw
|
|
517
|
+
this.assertDoppelgangerSafe(duty.pubkey);
|
|
518
|
+
|
|
519
|
+
this.validateAttestationDuty(duty, attestationData);
|
|
520
|
+
const signingSlot = computeStartSlotAtEpoch(attestationData.target.epoch);
|
|
521
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_BEACON_ATTESTER);
|
|
522
|
+
const signingRoot = computeSigningRoot(ssz.phase0.AttestationData, attestationData, domain);
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await this.slashingProtection.checkAndInsertAttestation(duty.pubkey, {
|
|
526
|
+
sourceEpoch: attestationData.source.epoch,
|
|
527
|
+
targetEpoch: attestationData.target.epoch,
|
|
528
|
+
signingRoot,
|
|
529
|
+
});
|
|
530
|
+
} catch (e) {
|
|
531
|
+
this.metrics?.slashingProtectionAttestationError.inc();
|
|
532
|
+
throw e;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const signableMessage: SignableMessage = {
|
|
536
|
+
type: SignableMessageType.ATTESTATION,
|
|
537
|
+
data: attestationData,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
if (this.config.getForkSeq(signingSlot) >= ForkSeq.electra) {
|
|
541
|
+
return {
|
|
542
|
+
committeeIndex: duty.committeeIndex,
|
|
543
|
+
attesterIndex: duty.validatorIndex,
|
|
544
|
+
data: attestationData,
|
|
545
|
+
signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex),
|
|
551
|
+
data: attestationData,
|
|
552
|
+
signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
|
|
553
|
+
} as phase0.Attestation;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async signAggregateAndProof(
|
|
557
|
+
duty: routes.validator.AttesterDuty,
|
|
558
|
+
selectionProof: BLSSignature,
|
|
559
|
+
aggregate: Attestation
|
|
560
|
+
): Promise<SignedAggregateAndProof> {
|
|
561
|
+
this.validateAttestationDuty(duty, aggregate.data);
|
|
562
|
+
|
|
563
|
+
const aggregateAndProof: AggregateAndProof = {
|
|
564
|
+
aggregate,
|
|
565
|
+
aggregatorIndex: duty.validatorIndex,
|
|
566
|
+
selectionProof,
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const signingSlot = aggregate.data.slot;
|
|
570
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_AGGREGATE_AND_PROOF);
|
|
571
|
+
const isPostElectra = this.config.getForkSeq(signingSlot) >= ForkSeq.electra;
|
|
572
|
+
const signingRoot = isPostElectra
|
|
573
|
+
? computeSigningRoot(ssz.electra.AggregateAndProof, aggregateAndProof, domain)
|
|
574
|
+
: computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain);
|
|
575
|
+
|
|
576
|
+
const signableMessage: SignableMessage = {
|
|
577
|
+
type: isPostElectra ? SignableMessageType.AGGREGATE_AND_PROOF_V2 : SignableMessageType.AGGREGATE_AND_PROOF,
|
|
578
|
+
data: aggregateAndProof,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
message: aggregateAndProof,
|
|
583
|
+
signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async signSyncCommitteeSignature(
|
|
588
|
+
pubkey: BLSPubkeyMaybeHex,
|
|
589
|
+
validatorIndex: ValidatorIndex,
|
|
590
|
+
slot: Slot,
|
|
591
|
+
beaconBlockRoot: Root
|
|
592
|
+
): Promise<altair.SyncCommitteeMessage> {
|
|
593
|
+
const signingSlot = slot;
|
|
594
|
+
const domain = this.config.getDomain(slot, DOMAIN_SYNC_COMMITTEE);
|
|
595
|
+
const signingRoot = computeSigningRoot(ssz.Root, beaconBlockRoot, domain);
|
|
596
|
+
const signableMessage: SignableMessage = {
|
|
597
|
+
type: SignableMessageType.SYNC_COMMITTEE_MESSAGE,
|
|
598
|
+
data: {beaconBlockRoot, slot},
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
slot,
|
|
603
|
+
validatorIndex,
|
|
604
|
+
beaconBlockRoot,
|
|
605
|
+
signature: await this.getSignature(pubkey, signingRoot, signingSlot, signableMessage),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async signContributionAndProof(
|
|
610
|
+
duty: {pubkey: BLSPubkeyMaybeHex; validatorIndex: number},
|
|
611
|
+
selectionProof: BLSSignature,
|
|
612
|
+
contribution: altair.SyncCommitteeContribution
|
|
613
|
+
): Promise<altair.SignedContributionAndProof> {
|
|
614
|
+
const contributionAndProof: altair.ContributionAndProof = {
|
|
615
|
+
contribution,
|
|
616
|
+
aggregatorIndex: duty.validatorIndex,
|
|
617
|
+
selectionProof,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const signingSlot = contribution.slot;
|
|
621
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_CONTRIBUTION_AND_PROOF);
|
|
622
|
+
const signingRoot = computeSigningRoot(ssz.altair.ContributionAndProof, contributionAndProof, domain);
|
|
623
|
+
|
|
624
|
+
const signableMessage: SignableMessage = {
|
|
625
|
+
type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF,
|
|
626
|
+
data: contributionAndProof,
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
message: contributionAndProof,
|
|
631
|
+
signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async signAttestationSelectionProof(pubkey: BLSPubkeyMaybeHex, slot: Slot): Promise<BLSSignature> {
|
|
636
|
+
const signingSlot = slot;
|
|
637
|
+
const domain = this.config.getDomain(slot, DOMAIN_SELECTION_PROOF);
|
|
638
|
+
const signingRoot = computeSigningRoot(ssz.Slot, slot, domain);
|
|
639
|
+
|
|
640
|
+
const signableMessage: SignableMessage = {
|
|
641
|
+
type: SignableMessageType.AGGREGATION_SLOT,
|
|
642
|
+
data: {slot},
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
return this.getSignature(pubkey, signingRoot, signingSlot, signableMessage);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async signSyncCommitteeSelectionProof(
|
|
649
|
+
pubkey: BLSPubkeyMaybeHex,
|
|
650
|
+
slot: Slot,
|
|
651
|
+
subcommitteeIndex: number
|
|
652
|
+
): Promise<BLSSignature> {
|
|
653
|
+
const signingSlot = slot;
|
|
654
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF);
|
|
655
|
+
const signingData: altair.SyncAggregatorSelectionData = {
|
|
656
|
+
slot,
|
|
657
|
+
subcommitteeIndex,
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const signingRoot = computeSigningRoot(ssz.altair.SyncAggregatorSelectionData, signingData, domain);
|
|
661
|
+
|
|
662
|
+
const signableMessage: SignableMessage = {
|
|
663
|
+
type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF,
|
|
664
|
+
data: {slot, subcommitteeIndex},
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
return this.getSignature(pubkey, signingRoot, signingSlot, signableMessage);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async signVoluntaryExit(
|
|
671
|
+
pubkey: BLSPubkeyMaybeHex,
|
|
672
|
+
validatorIndex: number,
|
|
673
|
+
exitEpoch: Epoch
|
|
674
|
+
): Promise<phase0.SignedVoluntaryExit> {
|
|
675
|
+
const signingSlot = computeStartSlotAtEpoch(exitEpoch);
|
|
676
|
+
const domain = this.config.getDomainForVoluntaryExit(signingSlot);
|
|
677
|
+
|
|
678
|
+
const voluntaryExit: phase0.VoluntaryExit = {epoch: exitEpoch, validatorIndex};
|
|
679
|
+
const signingRoot = computeSigningRoot(ssz.phase0.VoluntaryExit, voluntaryExit, domain);
|
|
680
|
+
|
|
681
|
+
const signableMessage: SignableMessage = {
|
|
682
|
+
type: SignableMessageType.VOLUNTARY_EXIT,
|
|
683
|
+
data: voluntaryExit,
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
message: voluntaryExit,
|
|
688
|
+
signature: await this.getSignature(pubkey, signingRoot, signingSlot, signableMessage),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
isDoppelgangerSafe(pubkeyHex: PubkeyHex): boolean {
|
|
693
|
+
// If doppelganger is not enabled we assumed all keys to be safe for use
|
|
694
|
+
return !this.doppelgangerService || this.doppelgangerService.isDoppelgangerSafe(pubkeyHex);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async signValidatorRegistration(
|
|
698
|
+
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
699
|
+
regAttributes: {feeRecipient: Eth1Address; gasLimit: number},
|
|
700
|
+
_slot: Slot
|
|
701
|
+
): Promise<bellatrix.SignedValidatorRegistrationV1> {
|
|
702
|
+
const pubkey = typeof pubkeyMaybeHex === "string" ? fromHex(pubkeyMaybeHex) : pubkeyMaybeHex;
|
|
703
|
+
const feeRecipient = fromHex(regAttributes.feeRecipient);
|
|
704
|
+
const {gasLimit} = regAttributes;
|
|
705
|
+
|
|
706
|
+
const validatorRegistration: bellatrix.ValidatorRegistrationV1 = {
|
|
707
|
+
feeRecipient,
|
|
708
|
+
gasLimit,
|
|
709
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
710
|
+
pubkey,
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const signingSlot = 0;
|
|
714
|
+
const domain = computeDomain(DOMAIN_APPLICATION_BUILDER, this.config.GENESIS_FORK_VERSION, ZERO_HASH);
|
|
715
|
+
const signingRoot = computeSigningRoot(ssz.bellatrix.ValidatorRegistrationV1, validatorRegistration, domain);
|
|
716
|
+
|
|
717
|
+
const signableMessage: SignableMessage = {
|
|
718
|
+
type: SignableMessageType.VALIDATOR_REGISTRATION,
|
|
719
|
+
data: validatorRegistration,
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
message: validatorRegistration,
|
|
724
|
+
signature: await this.getSignature(pubkeyMaybeHex, signingRoot, signingSlot, signableMessage),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async getValidatorRegistration(
|
|
729
|
+
pubkeyMaybeHex: BLSPubkeyMaybeHex,
|
|
730
|
+
regAttributes: {feeRecipient: Eth1Address; gasLimit: number},
|
|
731
|
+
slot: Slot
|
|
732
|
+
): Promise<bellatrix.SignedValidatorRegistrationV1> {
|
|
733
|
+
const pubkeyHex = typeof pubkeyMaybeHex === "string" ? pubkeyMaybeHex : toPubkeyHex(pubkeyMaybeHex);
|
|
734
|
+
const {feeRecipient, gasLimit} = regAttributes;
|
|
735
|
+
const regFullKey = `${feeRecipient}-${gasLimit}`;
|
|
736
|
+
const validatorData = this.validators.get(pubkeyHex);
|
|
737
|
+
const builderData = validatorData?.builderData;
|
|
738
|
+
if (builderData?.regFullKey === regFullKey) {
|
|
739
|
+
return builderData.validatorRegistration;
|
|
740
|
+
}
|
|
741
|
+
const validatorRegistration = await this.signValidatorRegistration(pubkeyMaybeHex, regAttributes, slot);
|
|
742
|
+
// If pubkeyHex was actually registered, then update the regData
|
|
743
|
+
if (validatorData !== undefined) {
|
|
744
|
+
validatorData.builderData = {validatorRegistration, regFullKey};
|
|
745
|
+
this.validators.set(pubkeyHex, validatorData);
|
|
746
|
+
}
|
|
747
|
+
return validatorRegistration;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private async getSignature(
|
|
751
|
+
pubkey: BLSPubkeyMaybeHex,
|
|
752
|
+
signingRoot: Uint8Array,
|
|
753
|
+
signingSlot: Slot,
|
|
754
|
+
signableMessage: SignableMessage
|
|
755
|
+
): Promise<BLSSignature> {
|
|
756
|
+
// TODO: Refactor indexing to not have to run toHex() on the pubkey every time
|
|
757
|
+
const pubkeyHex = typeof pubkey === "string" ? pubkey : toPubkeyHex(pubkey);
|
|
758
|
+
|
|
759
|
+
const signer = this.validators.get(pubkeyHex)?.signer;
|
|
760
|
+
if (!signer) {
|
|
761
|
+
throw Error(`Validator pubkey ${pubkeyHex} not known`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
switch (signer.type) {
|
|
765
|
+
case SignerType.Local: {
|
|
766
|
+
const timer = this.metrics?.localSignTime.startTimer();
|
|
767
|
+
const signature = signer.secretKey.sign(signingRoot).toBytes();
|
|
768
|
+
timer?.();
|
|
769
|
+
return signature;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
case SignerType.Remote: {
|
|
773
|
+
const timer = this.metrics?.remoteSignTime.startTimer();
|
|
774
|
+
try {
|
|
775
|
+
const signatureHex = await externalSignerPostSignature(
|
|
776
|
+
this.config,
|
|
777
|
+
signer.url,
|
|
778
|
+
pubkeyHex,
|
|
779
|
+
signingRoot,
|
|
780
|
+
signingSlot,
|
|
781
|
+
signableMessage
|
|
782
|
+
);
|
|
783
|
+
return fromHex(signatureHex);
|
|
784
|
+
} catch (e) {
|
|
785
|
+
this.metrics?.remoteSignErrors.inc();
|
|
786
|
+
throw e;
|
|
787
|
+
} finally {
|
|
788
|
+
timer?.();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Prevent signing bad data sent by the Beacon node */
|
|
795
|
+
private validateAttestationDuty(duty: routes.validator.AttesterDuty, data: phase0.AttestationData): void {
|
|
796
|
+
if (duty.slot !== data.slot) {
|
|
797
|
+
throw Error(`Inconsistent duties during signing: duty.slot ${duty.slot} != att.slot ${data.slot}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const isPostElectra = this.config.getForkSeq(data.slot) >= ForkSeq.electra;
|
|
801
|
+
if (!isPostElectra && duty.committeeIndex !== data.index) {
|
|
802
|
+
throw Error(
|
|
803
|
+
`Inconsistent duties during signing: duty.committeeIndex ${duty.committeeIndex} != att.committeeIndex ${data.index}`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (isPostElectra && data.index !== 0) {
|
|
807
|
+
throw Error(`Non-zero committee index post-electra during signing: att.committeeIndex ${data.index}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void {
|
|
812
|
+
const pubkeyHex = typeof pubKey === "string" ? pubKey : toPubkeyHex(pubKey);
|
|
813
|
+
if (!this.isDoppelgangerSafe(pubkeyHex)) {
|
|
814
|
+
throw new Error(`Doppelganger state for key ${pubkeyHex} is not safe`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function getSignerPubkeyHex(signer: Signer): PubkeyHex {
|
|
820
|
+
switch (signer.type) {
|
|
821
|
+
case SignerType.Local:
|
|
822
|
+
return toPubkeyHex(signer.secretKey.toPublicKey().toBytes());
|
|
823
|
+
|
|
824
|
+
case SignerType.Remote:
|
|
825
|
+
if (!isValidatePubkeyHex(signer.pubkey)) {
|
|
826
|
+
throw Error(`Bad format in RemoteSigner.pubkey ${signer.pubkey}`);
|
|
827
|
+
}
|
|
828
|
+
return signer.pubkey;
|
|
829
|
+
}
|
|
830
|
+
}
|