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:
parent
3df9ffca9f
commit
336403d18b
|
@ -565,9 +565,11 @@ type
|
||||||
|
|
||||||
restUrlForExit* {.
|
restUrlForExit* {.
|
||||||
desc: "URL of the beacon node REST service"
|
desc: "URL of the beacon node REST service"
|
||||||
defaultValue: parseUri("http://localhost" & $DefaultEth2RestPort)
|
name: "rest-url" }: Option[Uri]
|
||||||
defaultValueDesc: "http://localhost:5052"
|
|
||||||
name: "rest-url" }: Uri
|
rpcUrlForExit* {.
|
||||||
|
desc: "URL of the beacon node JSON-RPC service"
|
||||||
|
name: "rpc-url" }: Option[Uri]
|
||||||
|
|
||||||
of BNStartUpCmd.record:
|
of BNStartUpCmd.record:
|
||||||
case recordCmd* {.command.}: RecordCmd
|
case recordCmd* {.command.}: RecordCmd
|
||||||
|
|
|
@ -1523,16 +1523,197 @@ proc initStatusBar(node: BeaconNode) {.raises: [Defect, ValueError].} =
|
||||||
|
|
||||||
asyncSpawn statusBarUpdatesPollingLoop()
|
asyncSpawn statusBarUpdatesPollingLoop()
|
||||||
|
|
||||||
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
|
proc getSignedExitMessage(config: BeaconNodeConf,
|
||||||
|
validatorKeyAsStr: string,
|
||||||
|
exitAtEpoch: Epoch,
|
||||||
|
validatorIdx: uint64 ,
|
||||||
|
fork: Fork,
|
||||||
|
genesisValidatorsRoot: Eth2Digest): SignedVoluntaryExit =
|
||||||
let
|
let
|
||||||
client = RestClientRef.new(
|
validatorsDir = config.validatorsDir
|
||||||
try:
|
keystoreDir = validatorsDir / validatorKeyAsStr
|
||||||
resolveTAddress($config.restUrlForExit.hostname &
|
|
||||||
|
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:
|
||||||
|
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:
|
except CatchableError as err:
|
||||||
fatal "Failed to resolve address", err = err.msg
|
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,
|
stateIdHead = StateIdent(kind: StateQueryKind.Named,
|
||||||
value: StateIdentType.Head)
|
value: StateIdentType.Head)
|
||||||
|
@ -1592,27 +1773,6 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
|
||||||
epoch = slot.uint64 div 32
|
epoch = slot.uint64 div 32
|
||||||
Epoch epoch
|
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 fork = try:
|
||||||
let response = await client.getStateForkPlain(stateIdHead)
|
let response = await client.getStateForkPlain(stateIdHead)
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
|
@ -1629,97 +1789,61 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
|
||||||
err = err.msg
|
err = err.msg
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
let genesisValidatorsRoot = genesis.genesis_validators_root
|
let
|
||||||
|
genesisValidatorsRoot = genesis.genesis_validators_root
|
||||||
var signedExit = SignedVoluntaryExit(
|
validatorKeyAsStr = "0x" & $validator.pubkey
|
||||||
message: VoluntaryExit(
|
signedExit = getSignedExitMessage(config,
|
||||||
epoch: exitAtEpoch,
|
validatorKeyAsStr,
|
||||||
validator_index: validatorIdx))
|
exitAtEpoch,
|
||||||
|
validatorIdx,
|
||||||
signedExit.signature =
|
fork,
|
||||||
block:
|
genesisValidatorsRoot)
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
echoP "PLEASE BEWARE!"
|
let choice = askForExitConfirmation()
|
||||||
|
if choice == ClientExitAction.quiting:
|
||||||
echoP "Publishing a voluntary exit is an irreversible operation! " &
|
quit 0
|
||||||
"You won't be able to restart again with the same validator."
|
elif choice == ClientExitAction.confirmation:
|
||||||
|
let
|
||||||
echoP "By requesting an exit now, you'll be exempt from penalties " &
|
response = await client.submitPoolVoluntaryExit(signedExit)
|
||||||
"stemming from not performing your validator duties, but you " &
|
success = response.status == 200
|
||||||
"won't be able to withdraw your deposited funds for the time " &
|
if success:
|
||||||
"being. This means that your funds will be effectively frozen " &
|
echo "Successfully published voluntary exit for validator " &
|
||||||
"until withdrawals are enabled in a future phase of Eth2."
|
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")."
|
||||||
|
|
||||||
|
|
||||||
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":
|
|
||||||
quit 0
|
quit 0
|
||||||
elif choice == confirmation:
|
else:
|
||||||
let
|
let responseError = try:
|
||||||
response = await client.submitPoolVoluntaryExit(signedExit)
|
Json.decode(response.data, RestGenericError)
|
||||||
success = response.status == 200
|
except CatchableError as err:
|
||||||
if success:
|
fatal "Failed to decode invalid error server response on `submitPoolVoluntaryExit` request",
|
||||||
echo "Successfully published voluntary exit for validator " &
|
err = err.msg
|
||||||
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")."
|
|
||||||
quit 0
|
|
||||||
else:
|
|
||||||
let responseError = try:
|
|
||||||
Json.decode(response.data, RestGenericError)
|
|
||||||
except CatchableError as err:
|
|
||||||
fatal "Failed to decode invalid error server response on `submitPoolVoluntaryExit` request",
|
|
||||||
err = err.msg
|
|
||||||
quit 1
|
|
||||||
|
|
||||||
let
|
|
||||||
responseMessage = responseError.message
|
|
||||||
responseStacktraces = responseError.stacktraces
|
|
||||||
|
|
||||||
echo "The voluntary exit was not submitted successfully."
|
|
||||||
echo responseMessage & ":"
|
|
||||||
for el in responseStacktraces.get():
|
|
||||||
echo el
|
|
||||||
echoP "Please try again."
|
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
|
let
|
||||||
|
responseMessage = responseError.message
|
||||||
|
responseStacktraces = responseError.stacktraces
|
||||||
|
|
||||||
|
echo "The voluntary exit was not submitted successfully."
|
||||||
|
echo responseMessage & ":"
|
||||||
|
for el in responseStacktraces.get():
|
||||||
|
echo el
|
||||||
|
echoP "Please try again."
|
||||||
|
quit 1
|
||||||
|
|
||||||
except CatchableError as err:
|
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
|
err = err.msg
|
||||||
quit 1
|
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].} =
|
proc loadEth2Network(config: BeaconNodeConf): Eth2NetworkMetadata {.raises: [Defect, IOError].} =
|
||||||
network_name.set(2, labelValues = [config.eth2Network.get(otherwise = "mainnet")])
|
network_name.set(2, labelValues = [config.eth2Network.get(otherwise = "mainnet")])
|
||||||
if config.eth2Network.isSome:
|
if config.eth2Network.isSome:
|
||||||
|
|
|
@ -333,37 +333,37 @@ proc check_voluntary_exit*(
|
||||||
|
|
||||||
# Not in spec. Check that validator_index is in range
|
# Not in spec. Check that validator_index is in range
|
||||||
if voluntary_exit.validator_index >= state.validators.lenu64:
|
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]
|
let validator = unsafeAddr state.validators.asSeq()[voluntary_exit.validator_index]
|
||||||
|
|
||||||
# Verify the validator is active
|
# Verify the validator is active
|
||||||
if not is_active_validator(validator[], get_current_epoch(state)):
|
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
|
# Verify exit has not been initiated
|
||||||
if validator[].exit_epoch != FAR_FUTURE_EPOCH:
|
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
|
# Exits must specify an epoch when they become valid; they are not valid
|
||||||
# before then
|
# before then
|
||||||
if not (get_current_epoch(state) >= voluntary_exit.epoch):
|
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
|
# Verify the validator has been active long enough
|
||||||
if not (get_current_epoch(state) >= validator[].activation_epoch +
|
if not (get_current_epoch(state) >= validator[].activation_epoch +
|
||||||
cfg.SHARD_COMMITTEE_PERIOD):
|
cfg.SHARD_COMMITTEE_PERIOD):
|
||||||
return err("Not in validator set long enough")
|
return err("Exit: not in validator set long enough")
|
||||||
|
|
||||||
# Verify signature
|
# Verify signature
|
||||||
if skipBlsValidation notin flags:
|
if skipBlsValidation notin flags:
|
||||||
if not verify_voluntary_exit_signature(
|
if not verify_voluntary_exit_signature(
|
||||||
state.fork, state.genesis_validators_root, voluntary_exit,
|
state.fork, state.genesis_validators_root, voluntary_exit,
|
||||||
validator[].pubkey, signed_voluntary_exit.signature):
|
validator[].pubkey, signed_voluntary_exit.signature):
|
||||||
return err("Invalid signature")
|
return err("Exit: invalid signature")
|
||||||
|
|
||||||
# Initiate exit
|
# Initiate exit
|
||||||
debug "Checking voluntary exit (validator_leaving)",
|
debug "Exit: checking voluntary exit (validator_leaving)",
|
||||||
index = voluntary_exit.validator_index,
|
index = voluntary_exit.validator_index,
|
||||||
num_validators = state.validators.len,
|
num_validators = state.validators.len,
|
||||||
epoch = voluntary_exit.epoch,
|
epoch = voluntary_exit.epoch,
|
||||||
|
|
Loading…
Reference in New Issue