Allow exiting multiple validators at once (#4855)

This commit is contained in:
zah 2023-04-25 09:44:01 +03:00 committed by GitHub
parent 58b93ccbe0
commit 9b2c07c118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 123 additions and 93 deletions

View File

@ -683,12 +683,19 @@ type
name: "method" .}: ImportMethod name: "method" .}: ImportMethod
of DepositsCmd.exit: of DepositsCmd.exit:
exitedValidator* {. exitedValidators* {.
name: "validator" desc: "One or more validator index, public key or a keystore path of " &
desc: "Validator index, public key or a keystore path of the exited validator" .}: string "the exited validator(s)"
name: "validator" .}: seq[string]
exitAllValidatorsFlag* {.
desc: "Exit all validators in the specified data directory or validators directory"
defaultValue: false
name: "all" .}: bool
exitAtEpoch* {. exitAtEpoch* {.
name: "epoch" name: "epoch"
defaultValueDesc: "immediately"
desc: "The desired exit epoch" .}: Option[uint64] desc: "The desired exit epoch" .}: Option[uint64]
restUrlForExit* {. restUrlForExit* {.

View File

@ -80,8 +80,8 @@ proc getSignedExitMessage(
type type
ClientExitAction = enum ClientExitAction = enum
quiting = "q" abort = "q"
confirmation = "I understand the implications of submitting a voluntary exit" confirm = "I understand the implications of submitting a voluntary exit"
proc askForExitConfirmation(): ClientExitAction = proc askForExitConfirmation(): ClientExitAction =
template ask(prompt: string): string = template ask(prompt: string): string =
@ -97,16 +97,6 @@ proc askForExitConfirmation(): ClientExitAction =
echoP "Publishing a voluntary exit is an irreversible operation! " & echoP "Publishing a voluntary exit is an irreversible operation! " &
"You won't be able to restart again with the same validator." "You won't be able to restart again with the same validator."
echoP "By requesting an exit now, you'll be exempt from penalties " &
"stemming from not performing your validator duties, but you " &
"won't be able to withdraw your deposited funds for the time " &
"being. This means that your funds will be effectively frozen " &
"until withdrawals are enabled in a future phase of Eth2."
echoP "To understand more about the Eth2 roadmap, we recommend you " &
"have a look at\n" &
"https://ethereum.org/en/eth2/#roadmap"
echoP "You must keep your validator running for at least 5 epochs " & echoP "You must keep your validator running for at least 5 epochs " &
"(32 minutes) after requesting a validator exit, as you will " & "(32 minutes) after requesting a validator exit, as you will " &
"still be required to perform validator duties until your exit " & "still be required to perform validator duties until your exit " &
@ -118,27 +108,28 @@ proc askForExitConfirmation(): ClientExitAction =
var choice = "" var choice = ""
while not(choice == $ClientExitAction.confirmation or while not(choice == $ClientExitAction.confirm or
choice == $ClientExitAction.quiting) : choice == $ClientExitAction.abort) :
echoP "To proceed to submitting your voluntary exit, please type '" & echoP "To proceed to submitting your voluntary exit, please type '" &
$ClientExitAction.confirmation & $ClientExitAction.confirm &
"' (without the quotes) in the prompt below and " & "' (without the quotes) in the prompt below and " &
"press ENTER or type 'q' to quit." "press ENTER or type 'q' to quit."
echo "" echo ""
choice = ask "Your choice" choice = ask "Your choice"
if choice == $ClientExitAction.confirmation: if choice == $ClientExitAction.confirm:
ClientExitAction.confirmation ClientExitAction.confirm
else: else:
ClientExitAction.quiting ClientExitAction.abort
proc getValidator*(name: string): Result[ValidatorStorage, string] = proc getValidator*(decryptor: var MultipleKeystoresDecryptor,
name: string): Result[ValidatorStorage, string] =
let ident = ValidatorIdent.decodeString(name) let ident = ValidatorIdent.decodeString(name)
if ident.isErr(): if ident.isErr():
if not(isFile(name)): if not(isFile(name)):
return err($ident.error) return err($ident.error)
let key = importKeystoreFromFile(name) let key = decryptor.importKeystoreFromFile(name)
if key.isErr(): if key.isErr():
return err(key.error()) return err(key.error())
ok(ValidatorStorage(kind: ValidatorStorageKind.Keystore, ok(ValidatorStorage(kind: ValidatorStorageKind.Keystore,
@ -164,29 +155,24 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
value: StateIdentType.Head) value: StateIdentType.Head)
blockIdentHead = BlockIdent(kind: BlockQueryKind.Named, blockIdentHead = BlockIdent(kind: BlockQueryKind.Named,
value: BlockIdentType.Head) value: BlockIdentType.Head)
validator = getValidator(config.exitedValidator).valueOr:
fatal "Incorrect validator index, key or keystore path specified",
value = config.exitedValidator, reason = error
quit 1
let restValidator = try: # Before making any REST requests, we'll make sure that the supplied
let response = await client.getStateValidatorPlain(stateIdHead, # inputs are correct:
validator.getIdent()) var validators: seq[ValidatorStorage]
if response.status == 200: if config.exitAllValidatorsFlag:
let validatorInfo = decodeBytes(GetStateValidatorResponse, var keystoreCache = KeystoreCacheRef.init()
response.data, response.contentType) for keystore in listLoadableKeystores(config, keystoreCache):
if validatorInfo.isErr(): validators.add ValidatorStorage(kind: ValidatorStorageKind.Keystore,
raise newException(RestError, $validatorInfo.error) privateKey: keystore.privateKey)
validatorInfo.get().data else:
else: var decryptor: MultipleKeystoresDecryptor
raiseGenericError(response) defer: dispose decryptor
except CatchableError as exc: for pubKey in config.exitedValidators:
fatal "Failed to obtain information for validator", reason = exc.msg let validatorStorage = decryptor.getValidator(pubkey).valueOr:
quit 1 fatal "Incorrect validator index, key or keystore path specified",
value = pubKey, reason = error
let quit 1
validatorIdx = restValidator.index.uint64 validators.add validatorStorage
validatorKey = restValidator.validator.pubkey
let genesis = try: let genesis = try:
let response = await client.getGenesisPlain() let response = await client.getGenesisPlain()
@ -231,67 +217,95 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
reason = exc.msg reason = exc.msg
quit 1 quit 1
let if not config.printData:
genesis_validators_root = genesis.genesis_validators_root case askForExitConfirmation()
validatorKeyAsStr = "0x" & $validatorKey of ClientExitAction.abort:
signedExit = getSignedExitMessage(config, quit 0
validator, of ClientExitAction.confirm:
validatorKeyAsStr, discard
exitAtEpoch,
validatorIdx,
fork,
genesis_validators_root)
if config.printData: var hadErrors = false
let bytes = encodeBytes(signedExit, "application/json").valueOr: for validator in validators:
fatal "Unable to serialize signed exit message", reason = error let restValidator = try:
let response = await client.getStateValidatorPlain(stateIdHead, validator.getIdent)
if response.status == 200:
let validatorInfo = decodeBytes(GetStateValidatorResponse,
response.data, response.contentType)
if validatorInfo.isErr():
raise newException(RestError, $validatorInfo.error)
validatorInfo.get().data
else:
raiseGenericError(response)
except CatchableError as exc:
fatal "Failed to obtain information for validator", reason = exc.msg
quit 1 quit 1
echoP "You can use following command to send voluntary exit message to " & let
"remote beacon node host:\n" validatorIdx = restValidator.index.uint64
validatorKey = restValidator.validator.pubkey
echo "curl -X 'POST' \\" let
echo " '" & config.restUrlForExit & genesis_validators_root = genesis.genesis_validators_root
"/eth/v1/beacon/pool/voluntary_exits' \\" validatorKeyAsStr = "0x" & $validatorKey
echo " -H 'Accept: */*' \\" signedExit = getSignedExitMessage(config,
echo " -H 'Content-Type: application/json' \\" validator,
echo " -d '" & string.fromBytes(bytes) & "'" validatorKeyAsStr,
quit 0 exitAtEpoch,
else: validatorIdx,
try: fork,
let choice = askForExitConfirmation() genesis_validators_root)
if choice == ClientExitAction.quiting:
quit 0 if config.printData:
elif choice == ClientExitAction.confirmation: let bytes = encodeBytes(signedExit, "application/json").valueOr:
error "Unable to serialize signed exit message", reason = error
hadErrors = true
continue
echoP "You can use following command to send voluntary exit message to " &
"remote beacon node host:\n"
echo "curl -X 'POST' \\"
echo " '" & config.restUrlForExit &
"/eth/v1/beacon/pool/voluntary_exits' \\"
echo " -H 'Accept: */*' \\"
echo " -H 'Content-Type: application/json' \\"
echo " -d '" & string.fromBytes(bytes) & "'"
quit 0
else:
try:
let let
validatorDesc = $validatorIdx & "(" & validatorKeyAsStr[0..9] & ")"
response = await client.submitPoolVoluntaryExit(signedExit) response = await client.submitPoolVoluntaryExit(signedExit)
success = response.status == 200 success = response.status == 200
if success: if success:
echo "Successfully published voluntary exit for validator " & echo "Successfully published voluntary exit for validator " &
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")." validatorDesc & "."
quit 0
else: else:
hadErrors = true
let responseError = try: let responseError = try:
Json.decode(response.data, RestErrorMessage) Json.decode(response.data, RestErrorMessage)
except CatchableError as exc: except CatchableError as exc:
fatal "Failed to decode invalid error server response on " & error "Failed to decode invalid error server response on " &
"`submitPoolVoluntaryExit` request", reason = exc.msg "`submitPoolVoluntaryExit` request", reason = exc.msg
quit 1 continue
let let
responseMessage = responseError.message responseMessage = responseError.message
responseStacktraces = responseError.stacktraces responseStacktraces = responseError.stacktraces
echo "The voluntary exit was not submitted successfully." echo "The voluntary exit for validator " & validatorDesc &
" was not submitted successfully."
echo responseMessage & ":" echo responseMessage & ":"
for el in responseStacktraces.get(): for el in responseStacktraces.get():
echo el echo el
echoP "Please try again." echoP "Please try again."
quit 1 except CatchableError as err:
fatal "Failed to send the signed exit message",
signedExit, reason = err.msg
hadErrors = true
except CatchableError as err: if hadErrors:
fatal "Failed to send the signed exit message", reason = err.msg quit 1
quit 1
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} = proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
await restValidatorExit(config) await restValidatorExit(config)

View File

@ -80,6 +80,9 @@ type
getValidatorAndIdxFn*: ValidatorPubKeyToDataFn getValidatorAndIdxFn*: ValidatorPubKeyToDataFn
getBeaconTimeFn*: GetBeaconTimeFn getBeaconTimeFn*: GetBeaconTimeFn
MultipleKeystoresDecryptor* = object
previouslyUsedPassword*: string
const const
minPasswordLen = 12 minPasswordLen = 12
minPasswordEntropy = 60.0 minPasswordEntropy = 60.0
@ -89,6 +92,9 @@ const
"passwords" / "10-million-password-list-top-100000.txt", "passwords" / "10-million-password-list-top-100000.txt",
minWordLen = minPasswordLen) minWordLen = minPasswordLen)
proc dispose*(decryptor: var MultipleKeystoresDecryptor) =
burnMem(decryptor.previouslyUsedPassword)
func init*(T: type KeymanagerHost, func init*(T: type KeymanagerHost,
validatorPool: ref ValidatorPool, validatorPool: ref ValidatorPool,
rng: ref HmacDrbgContext, rng: ref HmacDrbgContext,
@ -1497,6 +1503,7 @@ proc saveWallet*(wallet: WalletPathPair): Result[void, string] =
saveWallet(wallet.wallet, wallet.path) saveWallet(wallet.wallet, wallet.path)
proc readPasswordInput(prompt: string, password: var string): bool = proc readPasswordInput(prompt: string, password: var string): bool =
burnMem password
try: try:
when defined(windows): when defined(windows):
# readPasswordFromStdin() on Windows always returns `false`. # readPasswordFromStdin() on Windows always returns `false`.
@ -1533,11 +1540,9 @@ proc resetAttributesNoError() =
except IOError: discard except IOError: discard
proc importKeystoreFromFile*( proc importKeystoreFromFile*(
fileName: string decryptor: var MultipleKeystoresDecryptor,
): Result[ValidatorPrivKey, string] = fileName: string
var password: string # TODO consider using a SecretString type ): Result[ValidatorPrivKey, string] =
defer: burnMem(password)
let let
data = readAllChars(fileName).valueOr: data = readAllChars(fileName).valueOr:
return err("Unable to read keystore file [" & ioErrorMsg(error) & "]") return err("Unable to read keystore file [" & ioErrorMsg(error) & "]")
@ -1551,9 +1556,10 @@ proc importKeystoreFromFile*(
var firstDecryptionAttempt = true var firstDecryptionAttempt = true
while true: while true:
var secret: seq[byte] var secret: seq[byte]
let status = decryptCryptoField(keystore.crypto, let status = decryptCryptoField(
KeystorePass.init(password), keystore.crypto,
secret) KeystorePass.init(decryptor.previouslyUsedPassword),
secret)
case status case status
of DecryptionStatus.Success: of DecryptionStatus.Success:
let privateKey = ValidatorPrivKey.fromRaw(secret).valueOr: let privateKey = ValidatorPrivKey.fromRaw(secret).valueOr:
@ -1572,9 +1578,9 @@ proc importKeystoreFromFile*(
else: else:
echo "The entered password was incorrect. Please try again." echo "The entered password was incorrect. Please try again."
if not(readPasswordInput("Password: ", password)): if not(readPasswordInput("Password: ", decryptor.previouslyUsedPassword)):
echo "System error while entering password. Please try again." echo "System error while entering password. Please try again."
if len(password) == 0: break if len(decryptor.previouslyUsedPassword) == 0: break
proc importKeystoresFromDir*(rng: var HmacDrbgContext, meth: ImportMethod, proc importKeystoresFromDir*(rng: var HmacDrbgContext, meth: ImportMethod,
importedDir, validatorsDir, secretsDir: string) = importedDir, validatorsDir, secretsDir: string) =

View File

@ -19,6 +19,9 @@ To perform a voluntary exit, make sure your beacon node is running with the `--r
!!! note !!! note
In the command above, you must replace `<VALIDATOR_KEYSTORE_PATH>` with the file-system path of an Ethereum [ERC-2335 Keystore](https://eips.ethereum.org/EIPS/eip-2335) created by a tool such as [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) or [ethdo](https://github.com/wealdtech/ethdo). In the command above, you must replace `<VALIDATOR_KEYSTORE_PATH>` with the file-system path of an Ethereum [ERC-2335 Keystore](https://eips.ethereum.org/EIPS/eip-2335) created by a tool such as [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) or [ethdo](https://github.com/wealdtech/ethdo).
!!! tip
You can perform multiple voluntary exits at once by supplying the `--validator` option multiple times on the command-line. This is typically more convenient when the provided keystores share the same password - you'll be asked to enter it only once.
## `rest-url` parameter ## `rest-url` parameter
The `--rest-url` parameter can be used to point the exit command to a specific node for publishing the request, as long as it's compatible with the [REST API](./rest-api.md). The `--rest-url` parameter can be used to point the exit command to a specific node for publishing the request, as long as it's compatible with the [REST API](./rest-api.md).