Workaround for issue #4216.
This commit is contained in:
parent
769ed00203
commit
596006be08
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue