Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow exiting multiple validators at once #4855

Merged
merged 1 commit into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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