Refactor `handleValidatorExitCommand`

Make `validator exit command` work both with `JSON-RPC` and `REST` APIs
Fix problem with specifying rest-url using `localhost`
Change back exit error messages in `state_transition_block`
This commit is contained in:
EmilIvanichkovv 2022-01-31 13:57:16 +02:00 committed by zah
parent 3df9ffca9f
commit 336403d18b
3 changed files with 245 additions and 119 deletions

View File

@ -565,9 +565,11 @@ type
restUrlForExit* {.
desc: "URL of the beacon node REST service"
defaultValue: parseUri("http://localhost" & $DefaultEth2RestPort)
defaultValueDesc: "http://localhost:5052"
name: "rest-url" }: Uri
name: "rest-url" }: Option[Uri]
rpcUrlForExit* {.
desc: "URL of the beacon node JSON-RPC service"
name: "rpc-url" }: Option[Uri]
of BNStartUpCmd.record:
case recordCmd* {.command.}: RecordCmd

View File

@ -1523,16 +1523,197 @@ proc initStatusBar(node: BeaconNode) {.raises: [Defect, ValueError].} =
asyncSpawn statusBarUpdatesPollingLoop()
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
proc getSignedExitMessage(config: BeaconNodeConf,
validatorKeyAsStr: string,
exitAtEpoch: Epoch,
validatorIdx: uint64 ,
fork: Fork,
genesisValidatorsRoot: Eth2Digest): SignedVoluntaryExit =
let
client = RestClientRef.new(
validatorsDir = config.validatorsDir
keystoreDir = validatorsDir / validatorKeyAsStr
if not dirExists(keystoreDir):
echo "The validator keystores directory '" & validatorsDir &
"' does not contain a keystore for the selected validator with public " &
"key '" & validatorKeyAsStr & "'."
quit 1
let signingItem = loadKeystore(
validatorsDir,
config.secretsDir,
validatorKeyAsStr,
config.nonInteractive)
if signingItem.isNone:
fatal "Unable to continue without decrypted signing key"
quit 1
var signedExit = SignedVoluntaryExit(
message: VoluntaryExit(
epoch: exitAtEpoch,
validator_index: validatorIdx))
signedExit.signature =
block:
let key = signingItem.get.privateKey
get_voluntary_exit_signature(fork, genesisValidatorsRoot,
signedExit.message, key).toValidatorSig()
signedExit
type
ClientExitAction = enum
quiting = "q"
confirmation = "I understand the implications of submitting a voluntary exit"
proc askForExitConfirmation(): ClientExitAction =
template ask(prompt: string): string =
try:
resolveTAddress($config.restUrlForExit.hostname &
stdout.write prompt, ": "
stdin.readLine()
except IOError:
fatal "Failed to read user input from stdin"
quit 1
echoP "PLEASE BEWARE!"
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 " &
"has been processed. The number of epochs could be significantly " &
"higher depending on how many other validators are queued to exit."
echoP "As such, we recommend you keep track of your validator's status " &
"using an Eth2 block explorer before shutting down your beacon node."
var choice = ""
while not(choice == $ClientExitAction.confirmation or
choice == $ClientExitAction.quiting) :
echoP "To proceed to submitting your voluntary exit, please type '" &
$ClientExitAction.confirmation &
"' (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
else:
ClientExitAction.quiting
proc rpcValidatorExit(config: BeaconNodeConf) {.async.} =
warn "The JS0R-PRC API is deprecated. Consider using the REST API"
let port = try:
let value = parseInt(config.rpcUrlForExit.get.port)
if value < Port.low.int or value > Port.high.int:
raise newException(ValueError,
"The port number must be between " & $Port.low & " and " & $Port.high)
Port value
except CatchableError as err:
fatal "Invalid port number", err = err.msg
quit 1
let rpcClient = newRpcHttpClient()
try:
await connect(rpcClient, config.rpcUrlForExit.get.hostname, port,
secure = config.rpcUrlForExit.get.scheme in ["https", "wss"])
except CatchableError as err:
fatal "Failed to connect to the beacon node RPC service", err = err.msg
quit 1
let (validator, validatorIdx, _, _) = try:
await rpcClient.get_v1_beacon_states_stateId_validators_validatorId(
"head", config.exitedValidator)
except CatchableError as err:
fatal "Failed to obtain information for validator", err = err.msg
quit 1
let exitAtEpoch = if config.exitAtEpoch.isSome:
Epoch config.exitAtEpoch.get
else:
let headSlot = try:
await rpcClient.getBeaconHead()
except CatchableError as err:
fatal "Failed to obtain the current head slot", err = err.msg
quit 1
headSlot.epoch
let fork = try:
await rpcClient.get_v1_beacon_states_fork("head")
except CatchableError as err:
fatal "Failed to obtain the fork id of the head state", err = err.msg
quit 1
let genesisValidatorsRoot = try:
(await rpcClient.get_v1_beacon_genesis()).genesis_validators_root
except CatchableError as err:
fatal "Failed to obtain the genesis validators root of the network",
err = err.msg
quit 1
let
validatorKeyAsStr = "0x" & $validator.pubkey
signedExit = getSignedExitMessage(config,
validatorKeyAsStr,
exitAtEpoch,
validatorIdx,
fork,
genesisValidatorsRoot)
try:
let choice = askForExitConfirmation()
if choice == ClientExitAction.quiting:
quit 0
elif choice == ClientExitAction.confirmation:
let success = await rpcClient.post_v1_beacon_pool_voluntary_exits(signedExit)
if success:
echo "Successfully published voluntary exit for validator " &
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")."
quit 0
else:
echo "The voluntary exit was not submitted successfully. Please try again."
quit 1
except CatchableError as err:
fatal "Failed to send the signed exit message to the beacon node RPC",
err = err.msg
quit 1
proc restValidatorExit(config: BeaconNodeConf) {.async.} =
let
address = if isNone(config.restUrlForExit):
resolveTAddress("127.0.0.1", Port(DefaultEth2RestPort))[0]
else:
let taseq = try:
resolveTAddress($config.restUrlForExit.get().hostname &
":" &
$config.restUrlForExit.port)[0]
$config.restUrlForExit.get().port)
except CatchableError as err:
fatal "Failed to resolve address", err = err.msg
quit 1)
quit 1
if len(taseq) == 1:
taseq[0]
else:
taseq[1]
client = RestClientRef.new(address)
stateIdHead = StateIdent(kind: StateQueryKind.Named,
value: StateIdentType.Head)
@ -1592,27 +1773,6 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
epoch = slot.uint64 div 32
Epoch epoch
let
validatorsDir = config.validatorsDir
validatorKeyAsStr = "0x" & $validator.pubkey
keystoreDir = validatorsDir / validatorKeyAsStr
if not dirExists(keystoreDir):
echo "The validator keystores directory '" & config.validatorsDir &
"' does not contain a keystore for the selected validator with public " &
"key '" & validatorKeyAsStr & "'."
quit 1
let signingItem = loadKeystore(
validatorsDir,
config.secretsDir,
validatorKeyAsStr,
config.nonInteractive)
if signingItem.isNone:
fatal "Unable to continue without decrypted signing key"
quit 1
let fork = try:
let response = await client.getStateForkPlain(stateIdHead)
if response.status == 200:
@ -1629,66 +1789,21 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
err = err.msg
quit 1
let genesisValidatorsRoot = genesis.genesis_validators_root
var signedExit = SignedVoluntaryExit(
message: VoluntaryExit(
epoch: exitAtEpoch,
validator_index: validatorIdx))
signedExit.signature =
block:
let key = signingItem.get().privateKey
get_voluntary_exit_signature(fork, genesisValidatorsRoot,
signedExit.message, key).toValidatorSig()
template ask(prompt: string): string =
try:
stdout.write prompt, ": "
stdin.readLine()
except IOError:
fatal "Failed to read user input from stdin"
quit 1
let
genesisValidatorsRoot = genesis.genesis_validators_root
validatorKeyAsStr = "0x" & $validator.pubkey
signedExit = getSignedExitMessage(config,
validatorKeyAsStr,
exitAtEpoch,
validatorIdx,
fork,
genesisValidatorsRoot)
try:
echoP "PLEASE BEWARE!"
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 " &
"has been processed. The number of epochs could be significantly " &
"higher depending on how many other validators are queued to exit."
echoP "As such, we recommend you keep track of your validator's status " &
"using an Eth2 block explorer before shutting down your beacon node."
const
confirmation = "I understand the implications of submitting a voluntary exit"
while true:
echoP "To proceed to submitting your voluntary exit, please type '" &
confirmation & "' (without the quotes) in the prompt below and " &
"press ENTER or type 'q' to quit."
echo ""
let choice = ask "Your choice"
if choice == "q":
let choice = askForExitConfirmation()
if choice == ClientExitAction.quiting:
quit 0
elif choice == confirmation:
elif choice == ClientExitAction.confirmation:
let
response = await client.submitPoolVoluntaryExit(signedExit)
success = response.status == 200
@ -1716,10 +1831,19 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
quit 1
except CatchableError as err:
fatal "Failed to send the signed exit message to the beacon node RPC",
fatal "Failed to send the signed exit message",
err = err.msg
quit 1
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
if isSome(config.restUrlForExit):
await restValidatorExit(config)
elif isSome(config.rpcUrlForExit):
await rpcValidatorExit(config)
else:
await restValidatorExit(config)
proc loadEth2Network(config: BeaconNodeConf): Eth2NetworkMetadata {.raises: [Defect, IOError].} =
network_name.set(2, labelValues = [config.eth2Network.get(otherwise = "mainnet")])
if config.eth2Network.isSome:

View File

@ -333,37 +333,37 @@ proc check_voluntary_exit*(
# Not in spec. Check that validator_index is in range
if voluntary_exit.validator_index >= state.validators.lenu64:
return err("Invalid validator index")
return err("Exit: invalid validator index")
let validator = unsafeAddr state.validators.asSeq()[voluntary_exit.validator_index]
# Verify the validator is active
if not is_active_validator(validator[], get_current_epoch(state)):
return err("Validator not active")
return err("Exit: validator not active")
# Verify exit has not been initiated
if validator[].exit_epoch != FAR_FUTURE_EPOCH:
return err("Validator has exited")
return err("Exit: validator has exited")
# Exits must specify an epoch when they become valid; they are not valid
# before then
if not (get_current_epoch(state) >= voluntary_exit.epoch):
return err("Exit epoch not passed")
return err("Exit: exit epoch not passed")
# Verify the validator has been active long enough
if not (get_current_epoch(state) >= validator[].activation_epoch +
cfg.SHARD_COMMITTEE_PERIOD):
return err("Not in validator set long enough")
return err("Exit: not in validator set long enough")
# Verify signature
if skipBlsValidation notin flags:
if not verify_voluntary_exit_signature(
state.fork, state.genesis_validators_root, voluntary_exit,
validator[].pubkey, signed_voluntary_exit.signature):
return err("Invalid signature")
return err("Exit: invalid signature")
# Initiate exit
debug "Checking voluntary exit (validator_leaving)",
debug "Exit: checking voluntary exit (validator_leaving)",
index = voluntary_exit.validator_index,
num_validators = state.validators.len,
epoch = voluntary_exit.epoch,