-
-
Notifications
You must be signed in to change notification settings - Fork 287
/
voluntaryExit.ts
177 lines (150 loc) Β· 6.16 KB
/
voluntaryExit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import inquirer from "inquirer";
import {
computeSigningRoot,
computeEpochAtSlot,
computeStartSlotAtEpoch,
getCurrentSlot,
} from "@lodestar/state-transition";
import {DOMAIN_VOLUNTARY_EXIT} from "@lodestar/params";
import {createBeaconConfig} from "@lodestar/config";
import {ssz, phase0} from "@lodestar/types";
import {toHex} from "@lodestar/utils";
import {Signer, SignerLocal, SignerType} from "@lodestar/validator";
import {Api, ApiError, getClient} from "@lodestar/api";
import {ensure0xPrefix, CliCommand, YargsError} from "../../util/index.js";
import {GlobalArgs} from "../../options/index.js";
import {getBeaconConfigFromArgs} from "../../config/index.js";
import {IValidatorCliArgs} from "./options.js";
import {getSignersFromArgs} from "./signers/index.js";
/* eslint-disable no-console */
type VoluntaryExitArgs = {
exitEpoch?: number;
pubkeys?: string[];
yes?: boolean;
};
export const voluntaryExit: CliCommand<VoluntaryExitArgs, IValidatorCliArgs & GlobalArgs> = {
command: "voluntary-exit",
describe:
"Performs a voluntary exit for a given set of validators as identified via `pubkeys`. \
If no `pubkeys` are provided, it will exit all validators that have been imported.",
examples: [
{
command: "validator voluntary-exit --pubkeys 0xF00",
description: "Perform a voluntary exit for the validator who has a public key 0xF00",
},
],
options: {
exitEpoch: {
description:
"The epoch upon which to submit the voluntary exit. If no value is provided, then we default to the currentEpoch.",
type: "number",
},
pubkeys: {
description: "Pubkeys to exit, must be available as local signers",
type: "array",
string: true, // Ensures the pubkey string is not automatically converted to numbers
coerce: (pubkeys: string[]): string[] =>
// Parse ["0x11,0x22"] to ["0x11", "0x22"]
pubkeys
.map((item) => item.split(","))
.flat(1)
.map(ensure0xPrefix),
},
yes: {
description: "Skip confirmation prompt",
type: "boolean",
},
},
handler: async (args) => {
// Fetch genesisValidatorsRoot always from beacon node
// Do not use known networks cache, it defaults to mainnet for devnets
const {config: chainForkConfig, network} = getBeaconConfigFromArgs(args);
const client = getClient({urls: args.beaconNodes}, {config: chainForkConfig});
const genesisRes = await client.beacon.getGenesis();
ApiError.assert(genesisRes, "Unable to fetch genesisValidatorsRoot from beacon node");
const {genesisValidatorsRoot, genesisTime} = genesisRes.response.data;
const config = createBeaconConfig(chainForkConfig, genesisValidatorsRoot);
// Set exitEpoch to current epoch if unspecified
const exitEpoch = args.exitEpoch ?? computeEpochAtSlot(getCurrentSlot(config, genesisTime));
// Select signers to exit
const signers = await getSignersFromArgs(args, network, {logger: console, signal: new AbortController().signal});
const signersToExit = selectSignersToExit(args, signers);
const validatorsToExit = await resolveValidatorIndexes(client, signersToExit);
if (!args.yes) {
const confirmation = await inquirer.prompt<{yes: boolean}>([
{
name: "yes",
type: "confirm",
message: `Confirm to exit pubkeys at epoch ${exitEpoch} from network ${network}?
${validatorsToExit.map((v) => `${v.pubkey} ${v.index} ${v.status}`).join("\n")}`,
},
]);
if (!confirmation.yes) {
throw new YargsError("not confirmed");
}
}
for (const [i, {index, signer, pubkey}] of validatorsToExit.entries()) {
const domain = config.getDomain(computeStartSlotAtEpoch(exitEpoch), DOMAIN_VOLUNTARY_EXIT);
const voluntaryExit: phase0.VoluntaryExit = {epoch: exitEpoch, validatorIndex: index};
const signingRoot = computeSigningRoot(ssz.phase0.VoluntaryExit, voluntaryExit, domain);
ApiError.assert(
await client.beacon.submitPoolVoluntaryExit({
message: voluntaryExit,
signature: signer.secretKey.sign(signingRoot).toBytes(),
})
);
console.log(`Submitted voluntary exit for ${pubkey} ${i + 1}/${signersToExit.length}`);
}
},
};
type SignerLocalPubkey = {signer: SignerLocal; pubkey: string};
function selectSignersToExit(args: VoluntaryExitArgs, signers: Signer[]): SignerLocalPubkey[] {
const signersWithPubkey = signers.map((signer) => ({
signer,
pubkey: getSignerPubkeyHex(signer),
}));
if (args.pubkeys) {
const signersByPubkey = new Map<string, Signer>(signersWithPubkey.map(({pubkey, signer}) => [pubkey, signer]));
const selectedSigners: SignerLocalPubkey[] = [];
for (const pubkey of args.pubkeys) {
const signer = signersByPubkey.get(pubkey);
if (!signer) {
throw new YargsError(`Unknown pubkey ${pubkey}`);
} else if (signer.type !== SignerType.Local) {
throw new YargsError(`pubkey ${pubkey} is not a local signer`);
} else {
selectedSigners.push({pubkey, signer});
}
}
return selectedSigners;
} else {
return signersWithPubkey.filter((signer): signer is SignerLocalPubkey => signer.signer.type === SignerType.Local);
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async function resolveValidatorIndexes(client: Api, signersToExit: SignerLocalPubkey[]) {
const pubkeys = signersToExit.map(({pubkey}) => pubkey);
const res = await client.beacon.getStateValidators("head", {id: pubkeys});
ApiError.assert(res, "Can not fetch state validators from beacon node");
const dataByPubkey = new Map(res.response.data.map((item) => [toHex(item.validator.pubkey), item]));
return signersToExit.map(({signer, pubkey}) => {
const item = dataByPubkey.get(pubkey);
if (!item) {
throw Error(`beacon node did not return status for pubkey ${pubkey}`);
}
return {
index: item.index,
status: item.status,
signer,
pubkey,
};
});
}
function getSignerPubkeyHex(signer: Signer): string {
switch (signer.type) {
case SignerType.Local:
return signer.secretKey.toPublicKey().toHex();
case SignerType.Remote:
return signer.pubkey;
}
}