From 596006be0883737fe15b6c9f731f211594cd739b Mon Sep 17 00:00:00 2001 From: cheatfate Date: Mon, 20 Mar 2023 13:58:54 +0200 Subject: [PATCH] Workaround for issue #4216. --- beacon_chain/conf.nim | 5 + beacon_chain/deposits.nim | 230 +++++++++++------- .../validators/keystore_management.nim | 44 ++++ 3 files changed, 194 insertions(+), 85 deletions(-) diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 2c1322bd3..5795b1ec1 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -711,6 +711,11 @@ type defaultValueDesc: $defaultBeaconNodeDesc name: "rest-url" .}: string + printData* {. + desc: "Print signed exit message instead of publishing it" + defaultValue: false + name: "print" .}: bool + of BNStartUpCmd.record: case recordCmd* {.command.}: RecordCmd of RecordCmd.create: diff --git a/beacon_chain/deposits.nim b/beacon_chain/deposits.nim index 8cfe3b844..111d3b6da 100644 --- a/beacon_chain/deposits.nim +++ b/beacon_chain/deposits.nim @@ -8,50 +8,74 @@ import std/[os, sequtils, times], + stew/byteutils, chronicles, ./spec/eth2_apis/rest_beacon_client, ./spec/signatures, ./validators/keystore_management, "."/[conf, beacon_clock, filepath] -proc getSignedExitMessage(config: BeaconNodeConf, - validatorKeyAsStr: string, - exitAtEpoch: Epoch, - validatorIdx: uint64 , - fork: Fork, - genesis_validators_root: Eth2Digest): SignedVoluntaryExit = - let - validatorsDir = config.validatorsDir - keystoreDir = validatorsDir / validatorKeyAsStr +type + ValidatorStorageKind* {.pure.} = enum + Keystore, Identifier - 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 + ValidatorStorage* = object + case kind: ValidatorStorageKind + of ValidatorStorageKind.Keystore: + privateKey: ValidatorPrivKey + of ValidatorStorageKind.Identifier: + ident: ValidatorIdent - let signingItem = loadKeystore( - validatorsDir, - config.secretsDir, - validatorKeyAsStr, - config.nonInteractive, - nil) +proc getSignedExitMessage( + config: BeaconNodeConf, + storage: ValidatorStorage, + validatorKeyAsStr: string, + exitAtEpoch: Epoch, + validatorIdx: uint64, + fork: Fork, + genesis_validators_root: Eth2Digest + ): SignedVoluntaryExit = - if signingItem.isNone: - fatal "Unable to continue without decrypted signing key" - quit 1 + let signingKey = + case storage.kind + of ValidatorStorageKind.Identifier: + let + 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, + nil) + + if signingItem.isNone: + fatal "Unable to continue without decrypted signing key" + quit 1 + + signingItem.get().privateKey + + of ValidatorStorageKind.Keystore: + storage.privateKey var signedExit = SignedVoluntaryExit( message: VoluntaryExit( epoch: exitAtEpoch, - validator_index: validatorIdx)) + validator_index: validatorIdx + ) + ) signedExit.signature = - block: - let key = signingItem.get.privateKey - get_voluntary_exit_signature(fork, genesis_validators_root, - signedExit.message, key).toValidatorSig() - + get_voluntary_exit_signature(fork, genesis_validators_root, + signedExit.message, + signingKey).toValidatorSig() signedExit type @@ -109,6 +133,28 @@ proc askForExitConfirmation(): ClientExitAction = else: ClientExitAction.quiting +proc getValidator*(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) + if key.isErr(): + return err(key.error()) + ok(ValidatorStorage(kind: ValidatorStorageKind.Keystore, + privateKey: key.get())) + else: + ok(ValidatorStorage(kind: ValidatorStorageKind.Identifier, + ident: ident.get())) + +proc getIdent*(storage: ValidatorStorage): ValidatorIdent = + case storage.kind + of ValidatorStorageKind.Keystore: + ValidatorIdent(kind: ValidatorQueryKind.Key, + key: storage.privateKey.toPubKey().toPubKey()) + of ValidatorStorageKind.Identifier: + storage.ident + proc restValidatorExit(config: BeaconNodeConf) {.async.} = let client = RestClientRef.new(config.restUrlForExit).valueOr: @@ -118,32 +164,29 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} = value: StateIdentType.Head) blockIdentHead = BlockIdent(kind: BlockQueryKind.Named, value: BlockIdentType.Head) - validatorIdent = ValidatorIdent.decodeString(config.exitedValidator) - - if validatorIdent.isErr(): - fatal "Incorrect validator index or key specified", - err = $validatorIdent.error() - quit 1 + 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, - validatorIdent.get()) + validator.getIdent()) if response.status == 200: - let validator = decodeBytes(GetStateValidatorResponse, - response.data, - response.contentType) - if validator.isErr(): - raise newException(RestError, $validator.error) - validator.get().data + 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 err: - fatal "Failed to obtain information for validator", err = err.msg + except CatchableError as exc: + fatal "Failed to obtain information for validator", reason = exc.msg quit 1 let - validator = restValidator.validator validatorIdx = restValidator.index.uint64 + validatorKey = restValidator.validator.pubkey let genesis = try: let response = await client.getGenesisPlain() @@ -156,9 +199,9 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} = genesis.get().data else: raiseGenericError(response) - except CatchableError as err: + except CatchableError as exc: fatal "Failed to obtain the genesis validators root of the network", - err = err.msg + reason = exc.msg quit 1 let exitAtEpoch = if config.exitAtEpoch.isSome: @@ -183,56 +226,72 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} = fork.get().data else: raiseGenericError(response) - except CatchableError as err: + except CatchableError as exc: fatal "Failed to obtain the fork id of the head state", - err = err.msg + reason = exc.msg quit 1 let genesis_validators_root = genesis.genesis_validators_root - validatorKeyAsStr = "0x" & $validator.pubkey + validatorKeyAsStr = "0x" & $validatorKey signedExit = getSignedExitMessage(config, + validator, validatorKeyAsStr, exitAtEpoch, validatorIdx, fork, genesis_validators_root) - try: - 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] & ")." + if config.printData: + let bytes = encodeBytes(signedExit, "application/json").valueOr: + fatal "Unable to serialize signed exit message", reason = error + quit 1 + + 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 choice = askForExitConfirmation() + if choice == ClientExitAction.quiting: quit 0 - else: - let responseError = try: - Json.decode(response.data, RestErrorMessage) - except CatchableError as err: - fatal "Failed to decode invalid error server response on `submitPoolVoluntaryExit` request", - err = err.msg + 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 + else: + let responseError = try: + Json.decode(response.data, RestErrorMessage) + except CatchableError as exc: + fatal "Failed to decode invalid error server response on " & + "`submitPoolVoluntaryExit` request", reason = exc.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 - 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", - err = err.msg - quit 1 + except CatchableError as err: + fatal "Failed to send the signed exit message", reason = err.msg + quit 1 proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} = await restValidatorExit(config) @@ -339,9 +398,10 @@ proc doDeposits*(config: BeaconNodeConf, rng: var HmacDrbgContext) {. InputDir(cwd / "validator_keys") else: echo "The default search path for validator keys is a sub-directory " & - "named 'validator_keys' in the current working directory. Since " & - "no such directory exists, please either provide the correct path" & - "as an argument or copy the imported keys in the expected location." + "named 'validator_keys' in the current working directory. " & + " Since no such directory exists, please either provide the " & + "correct path as an argument or copy the imported keys in the " & + "expected location." quit 1 importKeystoresFromDir( diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index 3d55b0f8a..d5814959d 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -1531,6 +1531,50 @@ proc resetAttributesNoError() = try: stdout.resetAttributes() except IOError: discard +proc importKeystoreFromFile*( + fileName: string + ): Result[ValidatorPrivKey, string] = + var password: string # TODO consider using a SecretString type + defer: burnMem(password) + + let + data = readAllChars(fileName).valueOr: + return err("Unable to read keystore file [" & ioErrorMsg(error) & "]") + keystore = + try: + parseKeystore(data) + except SerializationError as e: + return err("Invalid keystore file format [" & + e.formatMsg(fileName) & "]") + + var firstDecryptionAttempt = true + while true: + var secret: seq[byte] + let status = decryptCryptoField(keystore.crypto, + KeystorePass.init(password), + secret) + case status + of DecryptionStatus.Success: + let privateKey = ValidatorPrivKey.fromRaw(secret).valueOr: + return err("Keystore holds invalid private key [" & $error & "]") + return ok(privateKey) + of DecryptionStatus.InvalidKeystore: + return err("Invalid keystore format") + of DecryptionStatus.InvalidPassword: + if firstDecryptionAttempt: + try: + const msg = "Please enter the password for decrypting '$1'" + echo msg % [fileName] + except ValueError: + raiseAssert "The format string above is correct" + firstDecryptionAttempt = false + else: + echo "The entered password was incorrect. Please try again." + + if not(readPasswordInput("Password: ", password)): + echo "System error while entering password. Please try again." + if len(password) == 0: break + proc importKeystoresFromDir*(rng: var HmacDrbgContext, meth: ImportMethod, importedDir, validatorsDir, secretsDir: string) = var password: string # TODO consider using a SecretString type