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