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
of DepositsCmd.exit:
exitedValidator* {.
name: "validator"
desc: "Validator index, public key or a keystore path of the exited validator" .}: string
exitedValidators* {.
desc: "One or more validator index, public key or a keystore path of " &
"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* {.
name: "epoch"
defaultValueDesc: "immediately"
desc: "The desired exit epoch" .}: Option[uint64]
restUrlForExit* {.

View File

@ -80,8 +80,8 @@ proc getSignedExitMessage(
type
ClientExitAction = enum
quiting = "q"
confirmation = "I understand the implications of submitting a voluntary exit"
abort = "q"
confirm = "I understand the implications of submitting a voluntary exit"
proc askForExitConfirmation(): ClientExitAction =
template ask(prompt: string): string =
@ -97,16 +97,6 @@ proc askForExitConfirmation(): ClientExitAction =
echoP "Publishing a voluntary exit is an irreversible operation! " &
"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 " &
"(32 minutes) after requesting a validator exit, as you will " &
"still be required to perform validator duties until your exit " &
@ -118,27 +108,28 @@ proc askForExitConfirmation(): ClientExitAction =
var choice = ""
while not(choice == $ClientExitAction.confirmation or
choice == $ClientExitAction.quiting) :
while not(choice == $ClientExitAction.confirm or
choice == $ClientExitAction.abort) :
echoP "To proceed to submitting your voluntary exit, please type '" &
$ClientExitAction.confirmation &
$ClientExitAction.confirm &
"' (without the quotes) in the prompt below and " &
"press ENTER or type 'q' to quit."
echo ""
choice = ask "Your choice"
if choice == $ClientExitAction.confirmation:
ClientExitAction.confirmation
if choice == $ClientExitAction.confirm:
ClientExitAction.confirm
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)
if ident.isErr():
if not(isFile(name)):
return err($ident.error)
let key = importKeystoreFromFile(name)
let key = decryptor.importKeystoreFromFile(name)
if key.isErr():
return err(key.error())
ok(ValidatorStorage(kind: ValidatorStorageKind.Keystore,
@ -164,29 +155,24 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
value: StateIdentType.Head)
blockIdentHead = BlockIdent(kind: BlockQueryKind.Named,
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:
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
let
validatorIdx = restValidator.index.uint64
validatorKey = restValidator.validator.pubkey
# Before making any REST requests, we'll make sure that the supplied
# inputs are correct:
var validators: seq[ValidatorStorage]
if config.exitAllValidatorsFlag:
var keystoreCache = KeystoreCacheRef.init()
for keystore in listLoadableKeystores(config, keystoreCache):
validators.add ValidatorStorage(kind: ValidatorStorageKind.Keystore,
privateKey: keystore.privateKey)
else:
var decryptor: MultipleKeystoresDecryptor
defer: dispose decryptor
for pubKey in config.exitedValidators:
let validatorStorage = decryptor.getValidator(pubkey).valueOr:
fatal "Incorrect validator index, key or keystore path specified",
value = pubKey, reason = error
quit 1
validators.add validatorStorage
let genesis = try:
let response = await client.getGenesisPlain()
@ -231,67 +217,95 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
reason = exc.msg
quit 1
let
genesis_validators_root = genesis.genesis_validators_root
validatorKeyAsStr = "0x" & $validatorKey
signedExit = getSignedExitMessage(config,
validator,
validatorKeyAsStr,
exitAtEpoch,
validatorIdx,
fork,
genesis_validators_root)
if not config.printData:
case askForExitConfirmation()
of ClientExitAction.abort:
quit 0
of ClientExitAction.confirm:
discard
if config.printData:
let bytes = encodeBytes(signedExit, "application/json").valueOr:
fatal "Unable to serialize signed exit message", reason = error
var hadErrors = false
for validator in validators:
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
echoP "You can use following command to send voluntary exit message to " &
"remote beacon node host:\n"
let
validatorIdx = restValidator.index.uint64
validatorKey = restValidator.validator.pubkey
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 choice = askForExitConfirmation()
if choice == ClientExitAction.quiting:
quit 0
elif choice == ClientExitAction.confirmation:
let
genesis_validators_root = genesis.genesis_validators_root
validatorKeyAsStr = "0x" & $validatorKey
signedExit = getSignedExitMessage(config,
validator,
validatorKeyAsStr,
exitAtEpoch,
validatorIdx,
fork,
genesis_validators_root)
if config.printData:
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
validatorDesc = $validatorIdx & "(" & validatorKeyAsStr[0..9] & ")"
response = await client.submitPoolVoluntaryExit(signedExit)
success = response.status == 200
if success:
echo "Successfully published voluntary exit for validator " &
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")."
quit 0
validatorDesc & "."
else:
hadErrors = true
let responseError = try:
Json.decode(response.data, RestErrorMessage)
Json.decode(response.data, RestErrorMessage)
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
quit 1
continue
let
responseMessage = responseError.message
responseStacktraces = responseError.stacktraces
echo "The voluntary exit was not submitted successfully."
echo "The voluntary exit for validator " & validatorDesc &
" was not submitted successfully."
echo responseMessage & ":"
for el in responseStacktraces.get():
echo el
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:
fatal "Failed to send the signed exit message", reason = err.msg
quit 1
if hadErrors:
quit 1
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
await restValidatorExit(config)

View File

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