move wallet/deposit commands to separate files (#3372)
These commands have little to do with the "normal" beacon node operation - ergo, they deserve to live in their own module. * clean up imports/exports
This commit is contained in:
parent
3daa52ab87
commit
1760f4d7a7
|
@ -31,7 +31,7 @@ export
|
|||
beacon_chain_db, conf, attestation_pool, sync_committee_msg_pool,
|
||||
validator_pool, eth2_network, eth1_monitor, request_manager, sync_manager,
|
||||
eth2_processor, blockchain_dag, block_quarantine, base, exit_pool,
|
||||
validator_monitor
|
||||
validator_monitor, consensus_manager
|
||||
|
||||
type
|
||||
RpcServer* = RpcHttpServer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import
|
||||
std/[strutils, os, options, unicode, uri],
|
||||
metrics,
|
||||
|
||||
chronicles, chronicles/options as chroniclesOptions,
|
||||
confutils, confutils/defs, confutils/std/net, stew/shims/net as stewNet,
|
||||
|
@ -24,10 +25,12 @@ import
|
|||
./filepath
|
||||
|
||||
export
|
||||
uri,
|
||||
uri, nat, enr,
|
||||
defaultEth2TcpPort, enabledLogLevel, ValidIpAddress,
|
||||
defs, parseCmdArg, completeCmdArg, network_metadata,
|
||||
network
|
||||
network, BlockHashOrNumber
|
||||
|
||||
declareGauge network_name, "network name", ["name"]
|
||||
|
||||
const
|
||||
# TODO: How should we select between IPv4 and IPv6
|
||||
|
@ -958,3 +961,16 @@ func databaseDir*(config: AnyConf): string =
|
|||
template writeValue*(writer: var JsonWriter,
|
||||
value: TypedInputFile|InputFile|InputDir|OutPath|OutDir|OutFile) =
|
||||
writer.writeValue(string value)
|
||||
|
||||
proc loadEth2Network*(config: BeaconNodeConf): Eth2NetworkMetadata {.raises: [Defect, IOError].} =
|
||||
network_name.set(2, labelValues = [config.eth2Network.get(otherwise = "mainnet")])
|
||||
if config.eth2Network.isSome:
|
||||
getMetadataForNetwork(config.eth2Network.get)
|
||||
else:
|
||||
when const_preset == "mainnet":
|
||||
mainnetMetadata
|
||||
else:
|
||||
# Presumably other configurations can have other defaults, but for now
|
||||
# this simplifies the flow
|
||||
echo "Must specify network on non-mainnet node"
|
||||
quit 1
|
||||
|
|
|
@ -0,0 +1,448 @@
|
|||
# Copyright (c) 2018-2022 Status Research & Development GmbH
|
||||
# Licensed and distributed under either of
|
||||
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
||||
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
std/[os, sequtils, times],
|
||||
bearssl, chronicles,
|
||||
./spec/eth2_apis/[rpc_beacon_client, rest_beacon_client],
|
||||
./spec/signatures,
|
||||
./validators/keystore_management,
|
||||
"."/[conf, beacon_clock, filepath]
|
||||
|
||||
proc getSignedExitMessage(config: BeaconNodeConf,
|
||||
validatorKeyAsStr: string,
|
||||
exitAtEpoch: Epoch,
|
||||
validatorIdx: uint64 ,
|
||||
fork: Fork,
|
||||
genesisValidatorsRoot: Eth2Digest): SignedVoluntaryExit =
|
||||
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)
|
||||
|
||||
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.get().port)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to resolve address", err = err.msg
|
||||
quit 1
|
||||
if len(taseq) == 1:
|
||||
taseq[0]
|
||||
else:
|
||||
taseq[1]
|
||||
|
||||
client = RestClientRef.new(address)
|
||||
|
||||
stateIdHead = StateIdent(kind: StateQueryKind.Named,
|
||||
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
|
||||
|
||||
let restValidator = try:
|
||||
let response = await client.getStateValidatorPlain(stateIdHead,
|
||||
validatorIdent.get())
|
||||
if response.status == 200:
|
||||
let validator = decodeBytes(GetStateValidatorResponse,
|
||||
response.data,
|
||||
response.contentType)
|
||||
if validator.isErr():
|
||||
raise newException(RestError, $validator.error)
|
||||
validator.get().data
|
||||
else:
|
||||
raiseGenericError(response)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to obtain information for validator", err = err.msg
|
||||
quit 1
|
||||
|
||||
let
|
||||
validator = restValidator.validator
|
||||
validatorIdx = restValidator.index.uint64
|
||||
|
||||
let genesis = try:
|
||||
let response = await client.getGenesisPlain()
|
||||
if response.status == 200:
|
||||
let genesis = decodeBytes(GetGenesisResponse,
|
||||
response.data,
|
||||
response.contentType)
|
||||
if genesis.isErr():
|
||||
raise newException(RestError, $genesis.error)
|
||||
genesis.get().data
|
||||
else:
|
||||
raiseGenericError(response)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to obtain the genesis validators root of the network",
|
||||
err = err.msg
|
||||
quit 1
|
||||
|
||||
let exitAtEpoch = if config.exitAtEpoch.isSome:
|
||||
Epoch config.exitAtEpoch.get
|
||||
else:
|
||||
let
|
||||
genesisTime = genesis.genesis_time
|
||||
beaconClock = BeaconClock.init(genesisTime)
|
||||
time = getTime()
|
||||
slot = beaconClock.toSlot(time).slot
|
||||
epoch = slot.uint64 div 32
|
||||
Epoch epoch
|
||||
|
||||
let fork = try:
|
||||
let response = await client.getStateForkPlain(stateIdHead)
|
||||
if response.status == 200:
|
||||
let fork = decodeBytes(GetStateForkResponse,
|
||||
response.data,
|
||||
response.contentType)
|
||||
if fork.isErr():
|
||||
raise newException(RestError, $fork.error)
|
||||
fork.get().data
|
||||
else:
|
||||
raiseGenericError(response)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to obtain the fork id of the head state",
|
||||
err = err.msg
|
||||
quit 1
|
||||
|
||||
let
|
||||
genesisValidatorsRoot = genesis.genesis_validators_root
|
||||
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
|
||||
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."
|
||||
quit 1
|
||||
|
||||
except CatchableError as err:
|
||||
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 doDeposits*(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
|
||||
raises: [Defect, CatchableError].} =
|
||||
case config.depositsCmd
|
||||
of DepositsCmd.createTestnetDeposits:
|
||||
if config.eth2Network.isNone:
|
||||
fatal "Please specify the intended testnet for the deposits"
|
||||
quit 1
|
||||
let metadata = config.loadEth2Network()
|
||||
var seed: KeySeed
|
||||
defer: burnMem(seed)
|
||||
var walletPath: WalletPathPair
|
||||
|
||||
if config.existingWalletId.isSome:
|
||||
let
|
||||
id = config.existingWalletId.get
|
||||
found = findWallet(config, id).valueOr:
|
||||
fatal "Failed to locate wallet", error = error
|
||||
quit 1
|
||||
|
||||
if found.isSome:
|
||||
walletPath = found.get
|
||||
else:
|
||||
fatal "Unable to find wallet with the specified name/uuid", id
|
||||
quit 1
|
||||
|
||||
var unlocked = unlockWalletInteractively(walletPath.wallet)
|
||||
if unlocked.isOk:
|
||||
swap(seed, unlocked.get)
|
||||
else:
|
||||
# The failure will be reported in `unlockWalletInteractively`.
|
||||
quit 1
|
||||
else:
|
||||
var walletRes = createWalletInteractively(rng, config)
|
||||
if walletRes.isErr:
|
||||
fatal "Unable to create wallet", err = walletRes.error
|
||||
quit 1
|
||||
else:
|
||||
swap(seed, walletRes.get.seed)
|
||||
walletPath = walletRes.get.walletPath
|
||||
|
||||
let vres = secureCreatePath(config.outValidatorsDir)
|
||||
if vres.isErr():
|
||||
fatal "Could not create directory", path = config.outValidatorsDir
|
||||
quit QuitFailure
|
||||
|
||||
let sres = secureCreatePath(config.outSecretsDir)
|
||||
if sres.isErr():
|
||||
fatal "Could not create directory", path = config.outSecretsDir
|
||||
quit QuitFailure
|
||||
|
||||
let deposits = generateDeposits(
|
||||
metadata.cfg,
|
||||
rng,
|
||||
seed,
|
||||
walletPath.wallet.nextAccount,
|
||||
config.totalDeposits,
|
||||
config.outValidatorsDir,
|
||||
config.outSecretsDir)
|
||||
|
||||
if deposits.isErr:
|
||||
fatal "Failed to generate deposits", err = deposits.error
|
||||
quit 1
|
||||
|
||||
try:
|
||||
let depositDataPath = if config.outDepositsFile.isSome:
|
||||
config.outDepositsFile.get.string
|
||||
else:
|
||||
config.outValidatorsDir / "deposit_data-" & $epochTime() & ".json"
|
||||
|
||||
let launchPadDeposits =
|
||||
mapIt(deposits.value, LaunchPadDeposit.init(metadata.cfg, it))
|
||||
|
||||
Json.saveFile(depositDataPath, launchPadDeposits)
|
||||
echo "Deposit data written to \"", depositDataPath, "\""
|
||||
|
||||
walletPath.wallet.nextAccount += deposits.value.len
|
||||
let status = saveWallet(walletPath)
|
||||
if status.isErr:
|
||||
fatal "Failed to update wallet file after generating deposits",
|
||||
wallet = walletPath.path,
|
||||
error = status.error
|
||||
quit 1
|
||||
except CatchableError as err:
|
||||
fatal "Failed to create launchpad deposit data file", err = err.msg
|
||||
quit 1
|
||||
#[
|
||||
of DepositsCmd.status:
|
||||
echo "The status command is not implemented yet"
|
||||
quit 1
|
||||
]#
|
||||
|
||||
of DepositsCmd.`import`:
|
||||
let validatorKeysDir = if config.importedDepositsDir.isSome:
|
||||
config.importedDepositsDir.get
|
||||
else:
|
||||
let cwd = os.getCurrentDir()
|
||||
if dirExists(cwd / "validator_keys"):
|
||||
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."
|
||||
quit 1
|
||||
|
||||
importKeystoresFromDir(
|
||||
rng,
|
||||
validatorKeysDir.string,
|
||||
config.validatorsDir, config.secretsDir)
|
||||
|
||||
of DepositsCmd.exit:
|
||||
waitFor handleValidatorExitCommand(config)
|
|
@ -7,45 +7,19 @@
|
|||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
# Standard library
|
||||
std/[math, os, osproc, random, sequtils, strformat, strutils,
|
||||
tables, times, terminal],
|
||||
# Nimble packages
|
||||
stew/io2,
|
||||
spec/eth2_apis/eth2_rest_serialization,
|
||||
stew/[objects, byteutils, endians2, io2], stew/shims/macros,
|
||||
chronos, confutils, metrics, metrics/chronos_httpserver,
|
||||
chronicles, bearssl, blscurve, presto,
|
||||
json_serialization/std/[options, sets, net], serialization/errors,
|
||||
taskpools,
|
||||
|
||||
eth/keys, eth/net/nat,
|
||||
eth/p2p/discoveryv5/[protocol, enr, random2],
|
||||
|
||||
# Local modules
|
||||
"."/[
|
||||
beacon_clock, beacon_chain_db, beacon_node, beacon_node_status,
|
||||
conf, filepath, interop, nimbus_binary_common, statusbar, trusted_node_sync,
|
||||
version],
|
||||
./networking/[eth2_discovery, eth2_network, network_metadata],
|
||||
./gossip_processing/[eth2_processor, block_processor, consensus_manager],
|
||||
./validators/[
|
||||
validator_duties, validator_monitor, validator_pool,
|
||||
slashing_protection, keystore_management],
|
||||
./sync/[sync_protocol],
|
||||
std/[os, random, sequtils, terminal, times],
|
||||
bearssl, chronicles, chronos,
|
||||
metrics, metrics/chronos_httpserver,
|
||||
stew/[byteutils, io2],
|
||||
eth/p2p/discoveryv5/[enr, random2],
|
||||
eth/keys,
|
||||
./rpc/[rest_api, rpc_api, state_ttl_cache],
|
||||
./spec/datatypes/[altair, bellatrix, phase0],
|
||||
./spec/eth2_apis/rpc_beacon_client,
|
||||
./spec/[
|
||||
beaconstate, forks, helpers, network, weak_subjectivity, signatures,
|
||||
validator],
|
||||
./consensus_object_pools/[
|
||||
blockchain_dag, block_quarantine, block_clearance, attestation_pool,
|
||||
sync_committee_msg_pool, exit_pool, spec_cache],
|
||||
./eth1/eth1_monitor,
|
||||
./spec/eth2_apis/[rest_beacon_calls, rest_common]
|
||||
|
||||
from eth/common/eth_types import BlockHashOrNumber
|
||||
./spec/weak_subjectivity,
|
||||
./validators/[keystore_management, validator_duties],
|
||||
"."/[
|
||||
beacon_node, deposits, interop, nimbus_binary_common, statusbar,
|
||||
trusted_node_sync, wallets]
|
||||
|
||||
when defined(posix):
|
||||
import system/ansi_c
|
||||
|
@ -105,8 +79,6 @@ declareGauge next_action_wait,
|
|||
declareGauge versionGauge, "Nimbus version info (as metric labels)", ["version", "commit"], name = "version"
|
||||
versionGauge.set(1, labelValues=[fullVersionStr, gitRevision])
|
||||
|
||||
declareGauge network_name, "network name", ["name"]
|
||||
|
||||
logScope: topics = "beacnde"
|
||||
|
||||
const SlashingDbName = "slashing_protection"
|
||||
|
@ -1529,340 +1501,6 @@ proc initStatusBar(node: BeaconNode) {.raises: [Defect, ValueError].} =
|
|||
|
||||
asyncSpawn statusBarUpdatesPollingLoop()
|
||||
|
||||
proc getSignedExitMessage(config: BeaconNodeConf,
|
||||
validatorKeyAsStr: string,
|
||||
exitAtEpoch: Epoch,
|
||||
validatorIdx: uint64 ,
|
||||
fork: Fork,
|
||||
genesisValidatorsRoot: Eth2Digest): SignedVoluntaryExit =
|
||||
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)
|
||||
|
||||
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.get().port)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to resolve address", err = err.msg
|
||||
quit 1
|
||||
if len(taseq) == 1:
|
||||
taseq[0]
|
||||
else:
|
||||
taseq[1]
|
||||
|
||||
client = RestClientRef.new(address)
|
||||
|
||||
stateIdHead = StateIdent(kind: StateQueryKind.Named,
|
||||
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
|
||||
|
||||
let restValidator = try:
|
||||
let response = await client.getStateValidatorPlain(stateIdHead,
|
||||
validatorIdent.get())
|
||||
if response.status == 200:
|
||||
let validator = decodeBytes(GetStateValidatorResponse,
|
||||
response.data,
|
||||
response.contentType)
|
||||
if validator.isErr():
|
||||
raise newException(RestError, $validator.error)
|
||||
validator.get().data
|
||||
else:
|
||||
raiseGenericError(response)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to obtain information for validator", err = err.msg
|
||||
quit 1
|
||||
|
||||
let
|
||||
validator = restValidator.validator
|
||||
validatorIdx = restValidator.index.uint64
|
||||
|
||||
let genesis = try:
|
||||
let response = await client.getGenesisPlain()
|
||||
if response.status == 200:
|
||||
let genesis = decodeBytes(GetGenesisResponse,
|
||||
response.data,
|
||||
response.contentType)
|
||||
if genesis.isErr():
|
||||
raise newException(RestError, $genesis.error)
|
||||
genesis.get().data
|
||||
else:
|
||||
raiseGenericError(response)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to obtain the genesis validators root of the network",
|
||||
err = err.msg
|
||||
quit 1
|
||||
|
||||
let exitAtEpoch = if config.exitAtEpoch.isSome:
|
||||
Epoch config.exitAtEpoch.get
|
||||
else:
|
||||
let
|
||||
genesisTime = genesis.genesis_time
|
||||
beaconClock = BeaconClock.init(genesisTime)
|
||||
time = getTime()
|
||||
slot = beaconClock.toSlot(time).slot
|
||||
epoch = slot.uint64 div 32
|
||||
Epoch epoch
|
||||
|
||||
let fork = try:
|
||||
let response = await client.getStateForkPlain(stateIdHead)
|
||||
if response.status == 200:
|
||||
let fork = decodeBytes(GetStateForkResponse,
|
||||
response.data,
|
||||
response.contentType)
|
||||
if fork.isErr():
|
||||
raise newException(RestError, $fork.error)
|
||||
fork.get().data
|
||||
else:
|
||||
raiseGenericError(response)
|
||||
except CatchableError as err:
|
||||
fatal "Failed to obtain the fork id of the head state",
|
||||
err = err.msg
|
||||
quit 1
|
||||
|
||||
let
|
||||
genesisValidatorsRoot = genesis.genesis_validators_root
|
||||
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
|
||||
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."
|
||||
quit 1
|
||||
|
||||
except CatchableError as err:
|
||||
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:
|
||||
getMetadataForNetwork(config.eth2Network.get)
|
||||
else:
|
||||
when const_preset == "mainnet":
|
||||
mainnetMetadata
|
||||
else:
|
||||
# Presumably other configurations can have other defaults, but for now
|
||||
# this simplifies the flow
|
||||
echo "Must specify network on non-mainnet node"
|
||||
quit 1
|
||||
|
||||
proc doRunBeaconNode(config: var BeaconNodeConf, rng: ref BrHmacDrbgContext) {.raises: [Defect, CatchableError].} =
|
||||
info "Launching beacon node",
|
||||
version = fullVersionStr,
|
||||
|
@ -1982,160 +1620,6 @@ proc doCreateTestnet*(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.rais
|
|||
writeFile(bootstrapFile, bootstrapEnr.tryGet().toURI)
|
||||
echo "Wrote ", bootstrapFile
|
||||
|
||||
proc findWalletWithoutErrors(config: BeaconNodeConf,
|
||||
name: WalletName): Option[WalletPathPair] =
|
||||
let res = findWallet(config, name)
|
||||
if res.isErr:
|
||||
fatal "Failed to locate wallet", error = res.error
|
||||
quit 1
|
||||
res.get
|
||||
|
||||
proc doDeposits(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
|
||||
raises: [Defect, CatchableError].} =
|
||||
case config.depositsCmd
|
||||
of DepositsCmd.createTestnetDeposits:
|
||||
if config.eth2Network.isNone:
|
||||
fatal "Please specify the intended testnet for the deposits"
|
||||
quit 1
|
||||
let metadata = config.loadEth2Network()
|
||||
var seed: KeySeed
|
||||
defer: burnMem(seed)
|
||||
var walletPath: WalletPathPair
|
||||
|
||||
if config.existingWalletId.isSome:
|
||||
let
|
||||
id = config.existingWalletId.get
|
||||
found = findWalletWithoutErrors(config, id)
|
||||
|
||||
if found.isSome:
|
||||
walletPath = found.get
|
||||
else:
|
||||
fatal "Unable to find wallet with the specified name/uuid", id
|
||||
quit 1
|
||||
|
||||
var unlocked = unlockWalletInteractively(walletPath.wallet)
|
||||
if unlocked.isOk:
|
||||
swap(seed, unlocked.get)
|
||||
else:
|
||||
# The failure will be reported in `unlockWalletInteractively`.
|
||||
quit 1
|
||||
else:
|
||||
var walletRes = createWalletInteractively(rng, config)
|
||||
if walletRes.isErr:
|
||||
fatal "Unable to create wallet", err = walletRes.error
|
||||
quit 1
|
||||
else:
|
||||
swap(seed, walletRes.get.seed)
|
||||
walletPath = walletRes.get.walletPath
|
||||
|
||||
let vres = secureCreatePath(config.outValidatorsDir)
|
||||
if vres.isErr():
|
||||
fatal "Could not create directory", path = config.outValidatorsDir
|
||||
quit QuitFailure
|
||||
|
||||
let sres = secureCreatePath(config.outSecretsDir)
|
||||
if sres.isErr():
|
||||
fatal "Could not create directory", path = config.outSecretsDir
|
||||
quit QuitFailure
|
||||
|
||||
let deposits = generateDeposits(
|
||||
metadata.cfg,
|
||||
rng,
|
||||
seed,
|
||||
walletPath.wallet.nextAccount,
|
||||
config.totalDeposits,
|
||||
config.outValidatorsDir,
|
||||
config.outSecretsDir)
|
||||
|
||||
if deposits.isErr:
|
||||
fatal "Failed to generate deposits", err = deposits.error
|
||||
quit 1
|
||||
|
||||
try:
|
||||
let depositDataPath = if config.outDepositsFile.isSome:
|
||||
config.outDepositsFile.get.string
|
||||
else:
|
||||
config.outValidatorsDir / "deposit_data-" & $epochTime() & ".json"
|
||||
|
||||
let launchPadDeposits =
|
||||
mapIt(deposits.value, LaunchPadDeposit.init(metadata.cfg, it))
|
||||
|
||||
Json.saveFile(depositDataPath, launchPadDeposits)
|
||||
echo "Deposit data written to \"", depositDataPath, "\""
|
||||
|
||||
walletPath.wallet.nextAccount += deposits.value.len
|
||||
let status = saveWallet(walletPath)
|
||||
if status.isErr:
|
||||
fatal "Failed to update wallet file after generating deposits",
|
||||
wallet = walletPath.path,
|
||||
error = status.error
|
||||
quit 1
|
||||
except CatchableError as err:
|
||||
fatal "Failed to create launchpad deposit data file", err = err.msg
|
||||
quit 1
|
||||
#[
|
||||
of DepositsCmd.status:
|
||||
echo "The status command is not implemented yet"
|
||||
quit 1
|
||||
]#
|
||||
|
||||
of DepositsCmd.`import`:
|
||||
let validatorKeysDir = if config.importedDepositsDir.isSome:
|
||||
config.importedDepositsDir.get
|
||||
else:
|
||||
let cwd = os.getCurrentDir()
|
||||
if dirExists(cwd / "validator_keys"):
|
||||
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."
|
||||
quit 1
|
||||
|
||||
importKeystoresFromDir(
|
||||
rng,
|
||||
validatorKeysDir.string,
|
||||
config.validatorsDir, config.secretsDir)
|
||||
|
||||
of DepositsCmd.exit:
|
||||
waitFor handleValidatorExitCommand(config)
|
||||
|
||||
proc doWallets(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
|
||||
raises: [Defect, CatchableError].} =
|
||||
case config.walletsCmd:
|
||||
of WalletsCmd.create:
|
||||
if config.createdWalletNameFlag.isSome:
|
||||
let
|
||||
name = config.createdWalletNameFlag.get
|
||||
existingWallet = findWalletWithoutErrors(config, name)
|
||||
if existingWallet.isSome:
|
||||
echo "The Wallet '" & name.string & "' already exists."
|
||||
quit 1
|
||||
|
||||
var walletRes = createWalletInteractively(rng, config)
|
||||
if walletRes.isErr:
|
||||
fatal "Unable to create wallet", err = walletRes.error
|
||||
quit 1
|
||||
burnMem(walletRes.get.seed)
|
||||
|
||||
of WalletsCmd.list:
|
||||
for kind, walletFile in walkDir(config.walletsDir):
|
||||
if kind != pcFile: continue
|
||||
if checkSensitiveFilePermissions(walletFile):
|
||||
let walletRes = loadWallet(walletFile)
|
||||
if walletRes.isOk:
|
||||
echo walletRes.get.longName
|
||||
else:
|
||||
warn "Found corrupt wallet file",
|
||||
wallet = walletFile, error = walletRes.error
|
||||
else:
|
||||
warn "Found wallet file with insecure permissions",
|
||||
wallet = walletFile
|
||||
|
||||
of WalletsCmd.restore:
|
||||
restoreWalletInteractively(rng, config)
|
||||
|
||||
proc doRecord(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
|
||||
raises: [Defect, CatchableError].} =
|
||||
case config.recordCmd:
|
||||
|
|
|
@ -14,19 +14,19 @@ import
|
|||
std/[os, tables, strutils, terminal, typetraits],
|
||||
|
||||
# Nimble packages
|
||||
chronos, confutils/defs,
|
||||
chronos, confutils,
|
||||
chronicles, chronicles/helpers as chroniclesHelpers, chronicles/topics_registry,
|
||||
stew/io2,
|
||||
|
||||
# Local modules
|
||||
./spec/[helpers],
|
||||
./spec/datatypes/base,
|
||||
"."/[beacon_clock, conf, filepath, beacon_node_status]
|
||||
"."/[beacon_clock, beacon_node_status, conf, filepath]
|
||||
|
||||
when defined(posix):
|
||||
import termios
|
||||
|
||||
export beacon_node_status
|
||||
export beacon_clock, beacon_node_status, conf, confutils
|
||||
|
||||
type
|
||||
SlotStartProc*[T] = proc(node: T, wallTime: BeaconTime,
|
||||
|
|
|
@ -15,10 +15,10 @@ import
|
|||
../spec/[helpers, forks],
|
||||
../networking/[peer_pool, peer_scores, eth2_network],
|
||||
../beacon_clock,
|
||||
./sync_queue
|
||||
"."/[sync_protocol, sync_queue]
|
||||
|
||||
export phase0, altair, merge, chronos, chronicles, results,
|
||||
helpers, peer_scores, sync_queue, forks
|
||||
helpers, peer_scores, sync_queue, forks, sync_protocol
|
||||
|
||||
logScope:
|
||||
topics = "syncman"
|
||||
|
|
|
@ -21,7 +21,8 @@ import
|
|||
|
||||
export
|
||||
streams, options, keystore, phase0, altair, tables, uri, crypto,
|
||||
rest_types, eth2_rest_serialization, rest_remote_signer_calls
|
||||
rest_types, eth2_rest_serialization, rest_remote_signer_calls,
|
||||
slashing_protection
|
||||
|
||||
declareGauge validators,
|
||||
"Number of validators attached to the beacon node"
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# Copyright (c) 2018-2022 Status Research & Development GmbH
|
||||
# Licensed and distributed under either of
|
||||
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
||||
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
std/os,
|
||||
bearssl,
|
||||
./validators/keystore_management,
|
||||
./conf
|
||||
|
||||
proc doWallets*(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
|
||||
raises: [Defect, CatchableError].} =
|
||||
case config.walletsCmd:
|
||||
of WalletsCmd.create:
|
||||
if config.createdWalletNameFlag.isSome:
|
||||
let
|
||||
name = config.createdWalletNameFlag.get
|
||||
existingWallet = findWallet(config, name).valueOr:
|
||||
fatal "Failed to locate wallet", error = error
|
||||
quit 1
|
||||
if existingWallet.isSome:
|
||||
echo "The Wallet '" & name.string & "' already exists."
|
||||
quit 1
|
||||
|
||||
var wallet = createWalletInteractively(rng, config).valueOr:
|
||||
fatal "Unable to create wallet", err = error
|
||||
quit 1
|
||||
burnMem(wallet.seed)
|
||||
|
||||
of WalletsCmd.list:
|
||||
for kind, walletFile in walkDir(config.walletsDir):
|
||||
if kind != pcFile: continue
|
||||
if checkSensitiveFilePermissions(walletFile):
|
||||
let walletRes = loadWallet(walletFile)
|
||||
if walletRes.isOk:
|
||||
echo walletRes.get.longName
|
||||
else:
|
||||
warn "Found corrupt wallet file",
|
||||
wallet = walletFile, error = walletRes.error
|
||||
else:
|
||||
warn "Found wallet file with insecure permissions",
|
||||
wallet = walletFile
|
||||
|
||||
of WalletsCmd.restore:
|
||||
restoreWalletInteractively(rng, config)
|
Loading…
Reference in New Issue