Skip to content

Commit

Permalink
Allow exiting multiple validators at once
Browse files Browse the repository at this point in the history
  • Loading branch information
zah committed Apr 24, 2023
1 parent 58b93cc commit ebeb2e6
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 94 deletions.
13 changes: 10 additions & 3 deletions beacon_chain/conf.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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* {.
Expand Down
176 changes: 95 additions & 81 deletions beacon_chain/deposits.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 " &
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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 config.printData:
let bytes = encodeBytes(signedExit, "application/json").valueOr:
fatal "Unable to serialize signed exit message", reason = error
if not config.printData:
case askForExitConfirmation()
of ClientExitAction.abort:
quit 0
of ClientExitAction.confirm:
discard

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)
Expand Down
26 changes: 16 additions & 10 deletions beacon_chain/validators/keystore_management.nim
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ type
getValidatorAndIdxFn*: ValidatorPubKeyToDataFn
getBeaconTimeFn*: GetBeaconTimeFn

MultipleKeystoresDecryptor* = object
previouslyUsedPassword*: string

const
minPasswordLen = 12
minPasswordEntropy = 60.0
Expand All @@ -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,
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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) & "]")
Expand All @@ -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:
Expand All @@ -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) =
Expand Down
3 changes: 3 additions & 0 deletions docs/the_nimbus_book/src/voluntary-exit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down

0 comments on commit ebeb2e6

Please sign in to comment.