Allow exiting multiple validators at once (#4855)
This commit is contained in:
parent
58b93ccbe0
commit
9b2c07c118
|
@ -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* {.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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).
|
||||
|
|
Loading…
Reference in New Issue