diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index ca784661b..d89070b8b 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -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 diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 990d1fac4..4a08c7473 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -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( - try: - resolveTAddress($config.restUrlForExit.hostname & + 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: + 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,97 +1789,61 @@ 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 == ClientExitAction.confirmation: + let + response = await client.submitPoolVoluntaryExit(signedExit) + success = response.status == 200 + if success: + echo "Successfully published voluntary exit for validator " & + $validatorIdx & "(" & validatorKeyAsStr[0..9] & ")." quit 0 - elif choice == confirmation: - let - response = await client.submitPoolVoluntaryExit(signedExit) - success = response.status == 200 - if success: - echo "Successfully published voluntary exit for validator " & - $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." + 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 + 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: diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index f93ef8d2b..44451a8ff 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -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,