Workaround for issue #4216.
This commit is contained in:
parent
769ed00203
commit
596006be08
|
@ -711,6 +711,11 @@ type
|
||||||
defaultValueDesc: $defaultBeaconNodeDesc
|
defaultValueDesc: $defaultBeaconNodeDesc
|
||||||
name: "rest-url" .}: string
|
name: "rest-url" .}: string
|
||||||
|
|
||||||
|
printData* {.
|
||||||
|
desc: "Print signed exit message instead of publishing it"
|
||||||
|
defaultValue: false
|
||||||
|
name: "print" .}: bool
|
||||||
|
|
||||||
of BNStartUpCmd.record:
|
of BNStartUpCmd.record:
|
||||||
case recordCmd* {.command.}: RecordCmd
|
case recordCmd* {.command.}: RecordCmd
|
||||||
of RecordCmd.create:
|
of RecordCmd.create:
|
||||||
|
|
|
@ -8,50 +8,74 @@
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[os, sequtils, times],
|
std/[os, sequtils, times],
|
||||||
|
stew/byteutils,
|
||||||
chronicles,
|
chronicles,
|
||||||
./spec/eth2_apis/rest_beacon_client,
|
./spec/eth2_apis/rest_beacon_client,
|
||||||
./spec/signatures,
|
./spec/signatures,
|
||||||
./validators/keystore_management,
|
./validators/keystore_management,
|
||||||
"."/[conf, beacon_clock, filepath]
|
"."/[conf, beacon_clock, filepath]
|
||||||
|
|
||||||
proc getSignedExitMessage(config: BeaconNodeConf,
|
type
|
||||||
validatorKeyAsStr: string,
|
ValidatorStorageKind* {.pure.} = enum
|
||||||
exitAtEpoch: Epoch,
|
Keystore, Identifier
|
||||||
validatorIdx: uint64 ,
|
|
||||||
fork: Fork,
|
|
||||||
genesis_validators_root: Eth2Digest): SignedVoluntaryExit =
|
|
||||||
let
|
|
||||||
validatorsDir = config.validatorsDir
|
|
||||||
keystoreDir = validatorsDir / validatorKeyAsStr
|
|
||||||
|
|
||||||
if not dirExists(keystoreDir):
|
ValidatorStorage* = object
|
||||||
echo "The validator keystores directory '" & validatorsDir &
|
case kind: ValidatorStorageKind
|
||||||
"' does not contain a keystore for the selected validator with public " &
|
of ValidatorStorageKind.Keystore:
|
||||||
"key '" & validatorKeyAsStr & "'."
|
privateKey: ValidatorPrivKey
|
||||||
quit 1
|
of ValidatorStorageKind.Identifier:
|
||||||
|
ident: ValidatorIdent
|
||||||
|
|
||||||
let signingItem = loadKeystore(
|
proc getSignedExitMessage(
|
||||||
validatorsDir,
|
config: BeaconNodeConf,
|
||||||
config.secretsDir,
|
storage: ValidatorStorage,
|
||||||
validatorKeyAsStr,
|
validatorKeyAsStr: string,
|
||||||
config.nonInteractive,
|
exitAtEpoch: Epoch,
|
||||||
nil)
|
validatorIdx: uint64,
|
||||||
|
fork: Fork,
|
||||||
|
genesis_validators_root: Eth2Digest
|
||||||
|
): SignedVoluntaryExit =
|
||||||
|
|
||||||
if signingItem.isNone:
|
let signingKey =
|
||||||
fatal "Unable to continue without decrypted signing key"
|
case storage.kind
|
||||||
quit 1
|
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(
|
var signedExit = SignedVoluntaryExit(
|
||||||
message: VoluntaryExit(
|
message: VoluntaryExit(
|
||||||
epoch: exitAtEpoch,
|
epoch: exitAtEpoch,
|
||||||
validator_index: validatorIdx))
|
validator_index: validatorIdx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
signedExit.signature =
|
signedExit.signature =
|
||||||
block:
|
get_voluntary_exit_signature(fork, genesis_validators_root,
|
||||||
let key = signingItem.get.privateKey
|
signedExit.message,
|
||||||
get_voluntary_exit_signature(fork, genesis_validators_root,
|
signingKey).toValidatorSig()
|
||||||
signedExit.message, key).toValidatorSig()
|
|
||||||
|
|
||||||
signedExit
|
signedExit
|
||||||
|
|
||||||
type
|
type
|
||||||
|
@ -109,6 +133,28 @@ proc askForExitConfirmation(): ClientExitAction =
|
||||||
else:
|
else:
|
||||||
ClientExitAction.quiting
|
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.} =
|
proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||||
let
|
let
|
||||||
client = RestClientRef.new(config.restUrlForExit).valueOr:
|
client = RestClientRef.new(config.restUrlForExit).valueOr:
|
||||||
|
@ -118,32 +164,29 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||||
value: StateIdentType.Head)
|
value: StateIdentType.Head)
|
||||||
blockIdentHead = BlockIdent(kind: BlockQueryKind.Named,
|
blockIdentHead = BlockIdent(kind: BlockQueryKind.Named,
|
||||||
value: BlockIdentType.Head)
|
value: BlockIdentType.Head)
|
||||||
validatorIdent = ValidatorIdent.decodeString(config.exitedValidator)
|
validator = getValidator(config.exitedValidator).valueOr:
|
||||||
|
fatal "Incorrect validator index, key or keystore path specified",
|
||||||
if validatorIdent.isErr():
|
value = config.exitedValidator, reason = error
|
||||||
fatal "Incorrect validator index or key specified",
|
quit 1
|
||||||
err = $validatorIdent.error()
|
|
||||||
quit 1
|
|
||||||
|
|
||||||
let restValidator = try:
|
let restValidator = try:
|
||||||
let response = await client.getStateValidatorPlain(stateIdHead,
|
let response = await client.getStateValidatorPlain(stateIdHead,
|
||||||
validatorIdent.get())
|
validator.getIdent())
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
let validator = decodeBytes(GetStateValidatorResponse,
|
let validatorInfo = decodeBytes(GetStateValidatorResponse,
|
||||||
response.data,
|
response.data, response.contentType)
|
||||||
response.contentType)
|
if validatorInfo.isErr():
|
||||||
if validator.isErr():
|
raise newException(RestError, $validatorInfo.error)
|
||||||
raise newException(RestError, $validator.error)
|
validatorInfo.get().data
|
||||||
validator.get().data
|
|
||||||
else:
|
else:
|
||||||
raiseGenericError(response)
|
raiseGenericError(response)
|
||||||
except CatchableError as err:
|
except CatchableError as exc:
|
||||||
fatal "Failed to obtain information for validator", err = err.msg
|
fatal "Failed to obtain information for validator", reason = exc.msg
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
let
|
let
|
||||||
validator = restValidator.validator
|
|
||||||
validatorIdx = restValidator.index.uint64
|
validatorIdx = restValidator.index.uint64
|
||||||
|
validatorKey = restValidator.validator.pubkey
|
||||||
|
|
||||||
let genesis = try:
|
let genesis = try:
|
||||||
let response = await client.getGenesisPlain()
|
let response = await client.getGenesisPlain()
|
||||||
|
@ -156,9 +199,9 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||||
genesis.get().data
|
genesis.get().data
|
||||||
else:
|
else:
|
||||||
raiseGenericError(response)
|
raiseGenericError(response)
|
||||||
except CatchableError as err:
|
except CatchableError as exc:
|
||||||
fatal "Failed to obtain the genesis validators root of the network",
|
fatal "Failed to obtain the genesis validators root of the network",
|
||||||
err = err.msg
|
reason = exc.msg
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
let exitAtEpoch = if config.exitAtEpoch.isSome:
|
let exitAtEpoch = if config.exitAtEpoch.isSome:
|
||||||
|
@ -183,56 +226,72 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||||
fork.get().data
|
fork.get().data
|
||||||
else:
|
else:
|
||||||
raiseGenericError(response)
|
raiseGenericError(response)
|
||||||
except CatchableError as err:
|
except CatchableError as exc:
|
||||||
fatal "Failed to obtain the fork id of the head state",
|
fatal "Failed to obtain the fork id of the head state",
|
||||||
err = err.msg
|
reason = exc.msg
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
let
|
let
|
||||||
genesis_validators_root = genesis.genesis_validators_root
|
genesis_validators_root = genesis.genesis_validators_root
|
||||||
validatorKeyAsStr = "0x" & $validator.pubkey
|
validatorKeyAsStr = "0x" & $validatorKey
|
||||||
signedExit = getSignedExitMessage(config,
|
signedExit = getSignedExitMessage(config,
|
||||||
|
validator,
|
||||||
validatorKeyAsStr,
|
validatorKeyAsStr,
|
||||||
exitAtEpoch,
|
exitAtEpoch,
|
||||||
validatorIdx,
|
validatorIdx,
|
||||||
fork,
|
fork,
|
||||||
genesis_validators_root)
|
genesis_validators_root)
|
||||||
|
|
||||||
try:
|
if config.printData:
|
||||||
let choice = askForExitConfirmation()
|
let bytes = encodeBytes(signedExit, "application/json").valueOr:
|
||||||
if choice == ClientExitAction.quiting:
|
fatal "Unable to serialize signed exit message", reason = error
|
||||||
quit 0
|
quit 1
|
||||||
elif choice == ClientExitAction.confirmation:
|
|
||||||
let
|
echoP "You can use following command to send voluntary exit message to " &
|
||||||
response = await client.submitPoolVoluntaryExit(signedExit)
|
"remote beacon node host:\n"
|
||||||
success = response.status == 200
|
|
||||||
if success:
|
echo "curl -X 'POST' \\"
|
||||||
echo "Successfully published voluntary exit for validator " &
|
echo " '" & config.restUrlForExit &
|
||||||
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")."
|
"/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
|
quit 0
|
||||||
else:
|
elif choice == ClientExitAction.confirmation:
|
||||||
let responseError = try:
|
let
|
||||||
Json.decode(response.data, RestErrorMessage)
|
response = await client.submitPoolVoluntaryExit(signedExit)
|
||||||
except CatchableError as err:
|
success = response.status == 200
|
||||||
fatal "Failed to decode invalid error server response on `submitPoolVoluntaryExit` request",
|
if success:
|
||||||
err = err.msg
|
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
|
quit 1
|
||||||
|
|
||||||
let
|
except CatchableError as err:
|
||||||
responseMessage = responseError.message
|
fatal "Failed to send the signed exit message", reason = err.msg
|
||||||
responseStacktraces = responseError.stacktraces
|
quit 1
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
|
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
|
||||||
await restValidatorExit(config)
|
await restValidatorExit(config)
|
||||||
|
@ -339,9 +398,10 @@ proc doDeposits*(config: BeaconNodeConf, rng: var HmacDrbgContext) {.
|
||||||
InputDir(cwd / "validator_keys")
|
InputDir(cwd / "validator_keys")
|
||||||
else:
|
else:
|
||||||
echo "The default search path for validator keys is a sub-directory " &
|
echo "The default search path for validator keys is a sub-directory " &
|
||||||
"named 'validator_keys' in the current working directory. Since " &
|
"named 'validator_keys' in the current working directory. " &
|
||||||
"no such directory exists, please either provide the correct path" &
|
" Since no such directory exists, please either provide the " &
|
||||||
"as an argument or copy the imported keys in the expected location."
|
"correct path as an argument or copy the imported keys in the " &
|
||||||
|
"expected location."
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
importKeystoresFromDir(
|
importKeystoresFromDir(
|
||||||
|
|
|
@ -1531,6 +1531,50 @@ proc resetAttributesNoError() =
|
||||||
try: stdout.resetAttributes()
|
try: stdout.resetAttributes()
|
||||||
except IOError: discard
|
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,
|
proc importKeystoresFromDir*(rng: var HmacDrbgContext, meth: ImportMethod,
|
||||||
importedDir, validatorsDir, secretsDir: string) =
|
importedDir, validatorsDir, secretsDir: string) =
|
||||||
var password: string # TODO consider using a SecretString type
|
var password: string # TODO consider using a SecretString type
|
||||||
|
|
Loading…
Reference in New Issue