2021-03-26 06:52:01 +00:00
|
|
|
# beacon_chain
|
|
|
|
# Copyright (c) 2018-2021 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].}
|
|
|
|
|
2018-12-19 12:58:53 +00:00
|
|
|
import
|
2020-09-29 16:49:09 +00:00
|
|
|
std/[os, strutils, terminal, wordwrap, unicode],
|
2021-08-27 16:54:51 +00:00
|
|
|
chronicles, chronos, json_serialization, zxcvbn,
|
2020-07-08 12:36:03 +00:00
|
|
|
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
|
2021-10-04 19:08:31 +00:00
|
|
|
".."/spec/[eth2_merkleization, keystore],
|
|
|
|
".."/spec/datatypes/base,
|
2020-10-19 19:02:48 +00:00
|
|
|
stew/io2, libp2p/crypto/crypto as lcrypto,
|
2020-08-24 16:06:41 +00:00
|
|
|
nimcrypto/utils as ncrutils,
|
2021-10-19 14:09:26 +00:00
|
|
|
".."/[conf, filepath],
|
|
|
|
".."/networking/network_metadata,
|
|
|
|
./validator_pool
|
2018-12-19 12:58:53 +00:00
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
export
|
2021-10-19 14:09:26 +00:00
|
|
|
keystore, validator_pool
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-10-15 12:50:21 +00:00
|
|
|
when defined(windows):
|
|
|
|
import stew/[windows/acl]
|
|
|
|
|
2020-10-06 11:38:18 +00:00
|
|
|
{.localPassC: "-fno-lto".} # no LTO for crypto
|
2019-07-12 14:24:11 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
const
|
2021-10-04 19:08:31 +00:00
|
|
|
KeystoreFileName* = "keystore.json"
|
|
|
|
NetKeystoreFileName* = "network_keystore.json"
|
|
|
|
DisableFileName* = ".disable"
|
|
|
|
DisableFileContent* = "Please do not remove this file manually. " &
|
|
|
|
"This can lead to slashing of this validator's key."
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-03-24 11:13:07 +00:00
|
|
|
type
|
2020-08-21 19:36:42 +00:00
|
|
|
WalletPathPair* = object
|
|
|
|
wallet*: Wallet
|
|
|
|
path*: string
|
|
|
|
|
|
|
|
CreatedWallet* = object
|
|
|
|
walletPath*: WalletPathPair
|
2020-10-19 19:02:48 +00:00
|
|
|
seed*: KeySeed
|
2019-07-12 14:24:11 +00:00
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
AnyConf* = BeaconNodeConf | ValidatorClientConf
|
|
|
|
|
2020-08-24 16:06:41 +00:00
|
|
|
const
|
2020-10-02 15:58:08 +00:00
|
|
|
minPasswordLen = 12
|
2020-10-06 18:55:04 +00:00
|
|
|
minPasswordEntropy = 60.0
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
mostCommonPasswords = wordListArray(
|
|
|
|
currentSourcePath.parentDir /
|
2021-03-02 10:27:45 +00:00
|
|
|
"../../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
|
2020-08-24 16:06:41 +00:00
|
|
|
minWordLen = minPasswordLen)
|
|
|
|
|
2020-11-27 19:48:33 +00:00
|
|
|
proc echoP*(msg: string) =
|
2020-10-09 20:38:06 +00:00
|
|
|
## Prints a paragraph aligned to 80 columns
|
|
|
|
echo ""
|
2020-08-24 16:06:41 +00:00
|
|
|
echo wrapWords(msg, 80)
|
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
proc init*(t: typedesc[ValidatorPrivateItem], privateKey: ValidatorPrivKey,
|
|
|
|
keystore: Keystore): ValidatorPrivateItem =
|
|
|
|
ValidatorPrivateItem(
|
|
|
|
privateKey: privateKey,
|
2021-10-07 16:30:34 +00:00
|
|
|
description: if keystore.description == nil: none(string)
|
|
|
|
else: some(keystore.description[]),
|
2021-10-04 19:08:31 +00:00
|
|
|
path: some(keystore.path),
|
|
|
|
uuid: some(keystore.uuid),
|
|
|
|
version: some(uint64(keystore.version))
|
|
|
|
)
|
|
|
|
|
2020-08-26 06:42:26 +00:00
|
|
|
proc checkAndCreateDataDir*(dataDir: string): bool =
|
|
|
|
when defined(posix):
|
2020-10-30 00:36:47 +00:00
|
|
|
let requiredPerms = 0o700
|
|
|
|
if isDir(dataDir):
|
|
|
|
let currPermsRes = getPermissions(dataDir)
|
|
|
|
if currPermsRes.isErr():
|
|
|
|
fatal "Could not check data directory permissions",
|
|
|
|
data_dir = dataDir, errorCode = $currPermsRes.error,
|
|
|
|
errorMsg = ioErrorMsg(currPermsRes.error)
|
|
|
|
return false
|
2020-08-27 13:24:30 +00:00
|
|
|
else:
|
2020-10-30 00:36:47 +00:00
|
|
|
let currPerms = currPermsRes.get()
|
|
|
|
if currPerms != requiredPerms:
|
|
|
|
warn "Data directory has insecure permissions. Correcting them.",
|
|
|
|
data_dir = dataDir,
|
|
|
|
current_permissions = currPerms.toOct(4),
|
|
|
|
required_permissions = requiredPerms.toOct(4)
|
|
|
|
let newPermsRes = setPermissions(dataDir, requiredPerms)
|
|
|
|
if newPermsRes.isErr():
|
|
|
|
fatal "Could not set data directory permissions",
|
|
|
|
data_dir = dataDir,
|
|
|
|
errorCode = $newPermsRes.error,
|
|
|
|
errorMsg = ioErrorMsg(newPermsRes.error),
|
|
|
|
old_permissions = currPerms.toOct(4),
|
|
|
|
new_permissions = requiredPerms.toOct(4)
|
|
|
|
return false
|
2020-08-26 06:42:26 +00:00
|
|
|
else:
|
2020-10-27 11:04:17 +00:00
|
|
|
let res = secureCreatePath(dataDir)
|
2020-08-26 06:42:26 +00:00
|
|
|
if res.isErr():
|
2020-10-30 00:36:47 +00:00
|
|
|
fatal "Could not create data directory", data_dir = dataDir,
|
2020-08-26 06:42:26 +00:00
|
|
|
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-08-26 06:42:26 +00:00
|
|
|
elif defined(windows):
|
2020-10-30 00:36:47 +00:00
|
|
|
let amask = {AccessFlags.Read, AccessFlags.Write, AccessFlags.Execute}
|
2020-08-27 13:24:30 +00:00
|
|
|
if fileAccessible(dataDir, amask):
|
2020-10-12 13:04:21 +00:00
|
|
|
let cres = checkCurrentUserOnlyACL(dataDir)
|
|
|
|
if cres.isErr():
|
|
|
|
fatal "Could not check data folder's ACL",
|
|
|
|
data_dir = dataDir, errorCode = $cres.error,
|
|
|
|
errorMsg = ioErrorMsg(cres.error)
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-10-12 13:04:21 +00:00
|
|
|
else:
|
|
|
|
if cres.get() == false:
|
|
|
|
fatal "Data folder has insecure ACL", data_dir = dataDir
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-10-12 13:04:21 +00:00
|
|
|
else:
|
2020-10-27 11:04:17 +00:00
|
|
|
let res = secureCreatePath(dataDir)
|
|
|
|
if res.isErr():
|
|
|
|
fatal "Could not create data folder", data_dir = dataDir,
|
|
|
|
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-08-26 06:42:26 +00:00
|
|
|
else:
|
|
|
|
fatal "Unsupported operation system"
|
|
|
|
return false
|
|
|
|
|
2020-10-30 00:36:47 +00:00
|
|
|
return true
|
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
proc checkSensitivePathPermissions*(dirFilePath: string): bool =
|
|
|
|
## If ``dirFilePath`` is file, then check if file has only
|
|
|
|
##
|
|
|
|
## - "(600) rwx------" permissions on Posix (Linux, MacOS, BSD)
|
|
|
|
## - current user only ACL on Windows
|
|
|
|
##
|
|
|
|
## If ``dirFilePath`` is directory, then check if directory has only
|
|
|
|
##
|
|
|
|
## - "(700) rwx------" permissions on Posix (Linux, MacOS, BSD)
|
|
|
|
## - current user only ACL on Windows
|
|
|
|
##
|
|
|
|
## Procedure returns ``true`` if directory/file is present and all required
|
|
|
|
## permissions are set.
|
|
|
|
let r1 = isDir(dirFilePath)
|
|
|
|
let r2 = isFile(dirFilePath)
|
|
|
|
if r1 or r2:
|
|
|
|
when defined(windows):
|
|
|
|
let res = checkCurrentUserOnlyACL(dirFilePath)
|
|
|
|
if res.isErr():
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
if res.get() == false:
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
let requiredPermissions = if r1: 0o700 else: 0o600
|
|
|
|
let res = getPermissions(dirFilePath)
|
|
|
|
if res.isErr():
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
if res.get() != requiredPermissions:
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
false
|
|
|
|
|
2020-09-30 11:47:42 +00:00
|
|
|
proc checkSensitiveFilePermissions*(filePath: string): bool =
|
2020-08-27 13:24:30 +00:00
|
|
|
## Check if ``filePath`` has only "(600) rw-------" permissions.
|
2020-10-30 00:36:47 +00:00
|
|
|
## Procedure returns ``false`` if permissions are different and we can't
|
|
|
|
## correct them.
|
2020-08-27 13:24:30 +00:00
|
|
|
when defined(windows):
|
2020-10-12 13:04:21 +00:00
|
|
|
let cres = checkCurrentUserOnlyACL(filePath)
|
|
|
|
if cres.isErr():
|
|
|
|
fatal "Could not check file's ACL",
|
|
|
|
key_path = filePath, errorCode = $cres.error,
|
|
|
|
errorMsg = ioErrorMsg(cres.error)
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-10-12 13:04:21 +00:00
|
|
|
else:
|
|
|
|
if cres.get() == false:
|
|
|
|
fatal "File has insecure permissions", key_path = filePath
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-08-27 13:24:30 +00:00
|
|
|
else:
|
2020-10-30 00:36:47 +00:00
|
|
|
let requiredPerms = 0o600
|
|
|
|
let currPermsRes = getPermissions(filePath)
|
|
|
|
if currPermsRes.isErr():
|
2020-08-27 13:24:30 +00:00
|
|
|
error "Could not check file permissions",
|
2020-10-30 00:36:47 +00:00
|
|
|
key_path = filePath, errorCode = $currPermsRes.error,
|
|
|
|
errorMsg = ioErrorMsg(currPermsRes.error)
|
|
|
|
return false
|
2020-08-27 13:24:30 +00:00
|
|
|
else:
|
2020-10-30 00:36:47 +00:00
|
|
|
let currPerms = currPermsRes.get()
|
|
|
|
if currPerms != requiredPerms:
|
|
|
|
warn "File has insecure permissions. Correcting them.",
|
2020-08-27 13:24:30 +00:00
|
|
|
key_path = filePath,
|
2020-10-30 00:36:47 +00:00
|
|
|
current_permissions = currPerms.toOct(4),
|
|
|
|
required_permissions = requiredPerms.toOct(4)
|
|
|
|
let newPermsRes = setPermissions(filePath, requiredPerms)
|
|
|
|
if newPermsRes.isErr():
|
|
|
|
fatal "Could not set data directory permissions",
|
|
|
|
key_path = filePath,
|
|
|
|
errorCode = $newPermsRes.error,
|
|
|
|
errorMsg = ioErrorMsg(newPermsRes.error),
|
|
|
|
old_permissions = currPerms.toOct(4),
|
|
|
|
new_permissions = requiredPerms.toOct(4)
|
|
|
|
return false
|
|
|
|
|
|
|
|
return true
|
2020-08-27 13:24:30 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
proc keyboardCreatePassword(prompt: string,
|
|
|
|
confirm: string,
|
|
|
|
allowEmpty = false): KsResult[string] =
|
2020-09-29 16:49:09 +00:00
|
|
|
while true:
|
|
|
|
let password =
|
|
|
|
try:
|
|
|
|
readPasswordFromStdin(prompt)
|
|
|
|
except IOError:
|
|
|
|
error "Could not read password from stdin"
|
|
|
|
return err("Could not read password from stdin")
|
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
if password.len == 0 and allowEmpty:
|
|
|
|
return ok("")
|
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
# We treat `password` as UTF-8 encoded string.
|
|
|
|
if validateUtf8(password) == -1:
|
|
|
|
if runeLen(password) < minPasswordLen:
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "The entered password should be at least " & $minPasswordLen &
|
|
|
|
" characters."
|
|
|
|
echo ""
|
2020-09-29 16:49:09 +00:00
|
|
|
continue
|
2020-10-06 18:55:04 +00:00
|
|
|
elif passwordEntropy(password) < minPasswordEntropy:
|
|
|
|
echoP "The entered password has low entropy and may be easy to " &
|
|
|
|
"brute-force with automated tools. Please increase the " &
|
|
|
|
"variety of the user characters."
|
|
|
|
continue
|
2020-09-29 16:49:09 +00:00
|
|
|
elif password in mostCommonPasswords:
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "The entered password is too commonly used and it would be " &
|
|
|
|
"easy to brute-force with automated tools."
|
|
|
|
echo ""
|
2020-09-29 16:49:09 +00:00
|
|
|
continue
|
|
|
|
else:
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "Entered password is not valid UTF-8 string"
|
|
|
|
echo ""
|
2020-09-29 16:49:09 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
let confirmedPassword =
|
|
|
|
try:
|
|
|
|
readPasswordFromStdin(confirm)
|
|
|
|
except IOError:
|
|
|
|
error "Could not read password from stdin"
|
|
|
|
return err("Could not read password from stdin")
|
|
|
|
|
|
|
|
if password != confirmedPassword:
|
2020-10-09 20:38:06 +00:00
|
|
|
echo "Passwords don't match, please try again\n"
|
2020-09-29 16:49:09 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
return ok(password)
|
|
|
|
|
|
|
|
proc keyboardGetPassword[T](prompt: string, attempts: int,
|
2021-10-04 19:08:31 +00:00
|
|
|
pred: proc(p: string): KsResult[T] {.
|
|
|
|
gcsafe, raises: [Defect].}): KsResult[T] =
|
2020-09-29 16:49:09 +00:00
|
|
|
var
|
|
|
|
remainingAttempts = attempts
|
|
|
|
counter = 1
|
|
|
|
|
|
|
|
while remainingAttempts > 0:
|
|
|
|
let passphrase =
|
|
|
|
try:
|
|
|
|
readPasswordFromStdin(prompt)
|
2020-11-17 10:14:53 +00:00
|
|
|
except IOError:
|
2020-09-29 16:49:09 +00:00
|
|
|
error "Could not read password from stdin"
|
|
|
|
return
|
|
|
|
os.sleep(1000 * counter)
|
|
|
|
let res = pred(passphrase)
|
|
|
|
if res.isOk():
|
|
|
|
return res
|
|
|
|
else:
|
|
|
|
inc(counter)
|
|
|
|
dec(remainingAttempts)
|
|
|
|
err("Failed to decrypt keystore")
|
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
proc loadKeystoreFile*(path: string): KsResult[Keystore] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
try:
|
|
|
|
ok(Json.loadFile(path, Keystore))
|
|
|
|
except IOError as err:
|
|
|
|
return err("Could not read keystore file")
|
|
|
|
except SerializationError as err:
|
2021-10-07 16:30:34 +00:00
|
|
|
return err("Could not decode keystore file: " & err.formatMsg(path))
|
2021-10-04 19:08:31 +00:00
|
|
|
|
|
|
|
proc loadSecretFile*(path: string): KsResult[KeystorePass] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
try:
|
|
|
|
ok(KeystorePass.init(readFile(path)))
|
|
|
|
except IOError:
|
|
|
|
return err("Could not read password file")
|
|
|
|
|
|
|
|
proc loadKeystoreUnsafe*(validatorsDir, secretsDir,
|
|
|
|
keyName: string): KsResult[ValidatorPrivateItem] =
|
|
|
|
## Load keystore without any checks on keystore/secret permissions.
|
|
|
|
let
|
|
|
|
keystorePath = validatorsDir / keyName / KeystoreFileName
|
|
|
|
keystore = ? loadKeystoreFile(keystorePath)
|
|
|
|
|
|
|
|
let
|
|
|
|
passphrasePath = secretsDir / keyName
|
|
|
|
passphrase = ? loadSecretFile(passphrasePath)
|
|
|
|
|
|
|
|
let res = decryptKeystore(keystore, passphrase)
|
|
|
|
if res.isOk():
|
|
|
|
ok(ValidatorPrivateItem.init(res.get(), keystore))
|
|
|
|
else:
|
|
|
|
err("Failed to decrypt keystore")
|
|
|
|
|
2020-11-27 19:48:33 +00:00
|
|
|
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
|
2021-10-04 19:08:31 +00:00
|
|
|
nonInteractive: bool): Option[ValidatorPrivateItem] =
|
2020-06-01 19:48:20 +00:00
|
|
|
let
|
2021-10-04 19:08:31 +00:00
|
|
|
keystorePath = validatorsDir / keyName / KeystoreFileName
|
2020-08-02 17:26:57 +00:00
|
|
|
keystore =
|
2021-10-04 19:08:31 +00:00
|
|
|
block:
|
|
|
|
let res = loadKeystoreFile(keystorePath)
|
|
|
|
if res.isErr():
|
|
|
|
error "Failed to read keystore file", error = res.error(),
|
|
|
|
path = keystorePath
|
|
|
|
return
|
|
|
|
res.get()
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
let passphrasePath = secretsDir / keyName
|
2020-06-03 11:52:36 +00:00
|
|
|
if fileExists(passphrasePath):
|
2020-09-30 11:47:42 +00:00
|
|
|
if not(checkSensitiveFilePermissions(passphrasePath)):
|
2020-08-27 13:24:30 +00:00
|
|
|
error "Password file has insecure permissions", key_path = keyStorePath
|
|
|
|
return
|
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
let passphrase =
|
|
|
|
block:
|
|
|
|
let res = loadSecretFile(passphrasePath)
|
|
|
|
if res.isErr():
|
|
|
|
error "Failed to read passphrase file", err = res.error(),
|
|
|
|
path = passphrasePath
|
|
|
|
return
|
|
|
|
res.get()
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
let res = decryptKeystore(keystore, passphrase)
|
2021-10-04 19:08:31 +00:00
|
|
|
if res.isOk():
|
|
|
|
return some(ValidatorPrivateItem.init(res.get(), keystore))
|
2020-06-03 11:52:36 +00:00
|
|
|
else:
|
|
|
|
error "Failed to decrypt keystore", keystorePath, passphrasePath
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
if nonInteractive:
|
2020-06-01 19:48:20 +00:00
|
|
|
error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir",
|
2020-09-01 13:44:40 +00:00
|
|
|
keyName, validatorsDir, secretsDir = secretsDir
|
2020-06-01 19:48:20 +00:00
|
|
|
return
|
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let prompt = "Please enter passphrase for key \"" &
|
|
|
|
(validatorsDir / keyName) & "\": "
|
|
|
|
let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3,
|
|
|
|
proc (password: string): KsResult[ValidatorPrivKey] =
|
2020-10-02 15:46:05 +00:00
|
|
|
let decrypted = decryptKeystore(keystore, KeystorePass.init password)
|
2020-09-29 16:49:09 +00:00
|
|
|
if decrypted.isErr():
|
|
|
|
error "Keystore decryption failed. Please try again", keystorePath
|
|
|
|
decrypted
|
|
|
|
)
|
2020-10-02 15:46:05 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
if res.isOk():
|
2021-10-04 19:08:31 +00:00
|
|
|
some(ValidatorPrivateItem.init(res.get(), keystore))
|
2020-09-29 16:49:09 +00:00
|
|
|
else:
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
proc isEnabled*(validatorsDir, keyName: string): bool {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
## Returns ``true`` if specific validator with key ``keyName`` in validators
|
|
|
|
## directory ``validatorsDir`` is not disabled.
|
|
|
|
let keystorePath = validatorsDir / keyName
|
|
|
|
let disableFile = keystorePath / DisableFileName
|
|
|
|
if dirExists(keystorePath):
|
|
|
|
if fileExists(disableFile):
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
false
|
|
|
|
|
|
|
|
proc isEnabled*(conf: AnyConf, keyName: string): bool {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
## Returns ``true`` if specific validator with key ``keyName`` is not
|
|
|
|
## disabled.
|
|
|
|
isEnabled(conf.validatorsDir(), keyName)
|
|
|
|
|
|
|
|
proc isEnabled*(conf: AnyConf, publicKey: ValidatorPubKey): bool {.
|
|
|
|
raises:[Defect].} =
|
|
|
|
## Returns ``true`` if specific validator with public key ``publicKey`` is
|
|
|
|
## not disabled.
|
|
|
|
isEnabled(conf, publicKey.toHex())
|
|
|
|
|
|
|
|
iterator validatorKeysFromDirs*(validatorsDir,
|
|
|
|
secretsDir: string): ValidatorPrivateItem =
|
2020-09-01 13:44:40 +00:00
|
|
|
try:
|
|
|
|
for kind, file in walkDir(validatorsDir):
|
|
|
|
if kind == pcDir:
|
|
|
|
let keyName = splitFile(file).name
|
2021-10-04 19:08:31 +00:00
|
|
|
if isEnabled(validatorsDir, keyName):
|
|
|
|
let item = loadKeystore(validatorsDir, secretsDir, keyName, true)
|
|
|
|
if item.isSome():
|
|
|
|
yield item.get()
|
|
|
|
else:
|
|
|
|
quit 1
|
2020-09-01 13:44:40 +00:00
|
|
|
except OSError:
|
|
|
|
quit 1
|
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
iterator validatorItems*(config: AnyConf): ValidatorPrivateItem =
|
|
|
|
let validatorsDir = config.validatorsDir()
|
|
|
|
let secretsDir = config.secretsDir()
|
2020-06-01 19:48:20 +00:00
|
|
|
try:
|
|
|
|
for kind, file in walkDir(validatorsDir):
|
|
|
|
if kind == pcDir:
|
|
|
|
let keyName = splitFile(file).name
|
2021-10-04 19:08:31 +00:00
|
|
|
if isEnabled(config, keyName):
|
|
|
|
let item = loadKeystore(validatorsDir, secretsDir, keyName,
|
|
|
|
config.nonInteractive)
|
|
|
|
if item.isSome():
|
|
|
|
yield item.get()
|
|
|
|
else:
|
|
|
|
quit 1
|
2020-06-01 19:48:20 +00:00
|
|
|
except OSError as err:
|
|
|
|
error "Validator keystores directory not accessible",
|
|
|
|
path = validatorsDir, err = err.msg
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
type
|
2021-08-27 16:53:21 +00:00
|
|
|
KeystoreGenerationErrorKind = enum
|
2020-08-02 18:47:15 +00:00
|
|
|
FailedToCreateValidatorDir
|
|
|
|
FailedToCreateSecretsDir
|
2020-06-01 19:48:20 +00:00
|
|
|
FailedToCreateSecretFile
|
|
|
|
FailedToCreateKeystoreFile
|
2021-08-27 16:53:21 +00:00
|
|
|
KeystoreGenerationError* = object
|
|
|
|
case kind*: KeystoreGenerationErrorKind
|
|
|
|
of FailedToCreateValidatorDir, FailedToCreateSecretsDir,
|
|
|
|
FailedToCreateSecretFile, FailedToCreateKeystoreFile:
|
|
|
|
error*: string
|
|
|
|
|
|
|
|
proc mapErrTo[T, E](r: Result[T, E], v: static KeystoreGenerationErrorKind):
|
|
|
|
Result[T, KeystoreGenerationError] =
|
|
|
|
r.mapErr(proc (e: E): KeystoreGenerationError =
|
|
|
|
KeystoreGenerationError(kind: v, error: $e))
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-25 10:16:31 +00:00
|
|
|
proc loadNetKeystore*(keyStorePath: string,
|
|
|
|
insecurePwd: Option[string]): Option[lcrypto.PrivateKey] =
|
2020-08-24 16:06:41 +00:00
|
|
|
|
2020-09-30 11:47:42 +00:00
|
|
|
if not(checkSensitiveFilePermissions(keystorePath)):
|
2020-08-27 13:24:30 +00:00
|
|
|
error "Network keystorage file has insecure permissions",
|
|
|
|
key_path = keyStorePath
|
|
|
|
return
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
let keyStore =
|
|
|
|
try:
|
|
|
|
Json.loadFile(keystorePath, NetKeystore)
|
|
|
|
except IOError as err:
|
|
|
|
error "Failed to read network keystore", err = err.msg,
|
|
|
|
path = keystorePath
|
|
|
|
return
|
|
|
|
except SerializationError as err:
|
|
|
|
error "Invalid network keystore", err = err.formatMsg(keystorePath)
|
|
|
|
return
|
|
|
|
|
2020-08-25 10:16:31 +00:00
|
|
|
if insecurePwd.isSome():
|
|
|
|
warn "Using insecure password to unlock networking key"
|
2020-10-02 15:46:05 +00:00
|
|
|
let decrypted = decryptNetKeystore(keystore, KeystorePass.init insecurePwd.get)
|
2020-08-24 16:06:41 +00:00
|
|
|
if decrypted.isOk:
|
|
|
|
return some(decrypted.get())
|
|
|
|
else:
|
|
|
|
error "Network keystore decryption failed", key_store = keyStorePath
|
2020-08-25 10:16:31 +00:00
|
|
|
return
|
|
|
|
else:
|
2020-09-29 16:49:09 +00:00
|
|
|
let prompt = "Please enter passphrase to unlock networking key: "
|
|
|
|
let res = keyboardGetPassword[lcrypto.PrivateKey](prompt, 3,
|
|
|
|
proc (password: string): KsResult[lcrypto.PrivateKey] =
|
2020-10-02 15:46:05 +00:00
|
|
|
let decrypted = decryptNetKeystore(keystore, KeystorePass.init password)
|
2020-09-29 16:49:09 +00:00
|
|
|
if decrypted.isErr():
|
|
|
|
error "Keystore decryption failed. Please try again", keystorePath
|
|
|
|
decrypted
|
|
|
|
)
|
|
|
|
if res.isOk():
|
|
|
|
some(res.get())
|
|
|
|
else:
|
|
|
|
return
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string,
|
2020-08-25 10:16:31 +00:00
|
|
|
netKey: lcrypto.PrivateKey, insecurePwd: Option[string]
|
|
|
|
): Result[void, KeystoreGenerationError] =
|
2020-09-29 16:49:09 +00:00
|
|
|
let password =
|
|
|
|
if insecurePwd.isSome():
|
|
|
|
warn "Using insecure password to lock networking key",
|
|
|
|
key_path = keyStorePath
|
|
|
|
insecurePwd.get()
|
|
|
|
else:
|
2020-08-25 10:16:31 +00:00
|
|
|
let prompt = "Please enter NEW password to lock network key storage: "
|
2020-09-29 16:49:09 +00:00
|
|
|
let confirm = "Please confirm, network key storage password: "
|
2021-08-27 16:53:21 +00:00
|
|
|
? keyboardCreatePassword(prompt, confirm).mapErrTo(
|
|
|
|
FailedToCreateKeystoreFile)
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
let keyStore = createNetKeystore(kdfScrypt, rng, netKey,
|
2020-10-02 15:46:05 +00:00
|
|
|
KeystorePass.init password)
|
2020-08-24 16:06:41 +00:00
|
|
|
var encodedStorage: string
|
|
|
|
try:
|
|
|
|
encodedStorage = Json.encode(keyStore)
|
2021-08-27 16:53:21 +00:00
|
|
|
except SerializationError as exc:
|
2020-08-25 12:49:05 +00:00
|
|
|
error "Could not serialize network key storage", key_path = keyStorePath
|
2021-08-27 16:53:21 +00:00
|
|
|
return err(KeystoreGenerationError(
|
|
|
|
kind: FailedToCreateKeystoreFile, error: exc.msg))
|
2020-08-24 16:06:41 +00:00
|
|
|
|
2020-10-27 11:04:17 +00:00
|
|
|
let res = secureWriteFile(keyStorePath, encodedStorage)
|
2020-08-24 16:06:41 +00:00
|
|
|
if res.isOk():
|
|
|
|
ok()
|
|
|
|
else:
|
2020-10-12 13:47:59 +00:00
|
|
|
error "Could not write to network key storage file",
|
|
|
|
key_path = keyStorePath
|
2021-08-27 16:53:21 +00:00
|
|
|
res.mapErrTo(FailedToCreateKeystoreFile)
|
2020-08-24 16:06:41 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc saveKeystore(rng: var BrHmacDrbgContext,
|
|
|
|
validatorsDir, secretsDir: string,
|
2021-06-01 11:13:40 +00:00
|
|
|
signingKey: ValidatorPrivKey, signingPubKey: CookedPubKey,
|
2020-08-02 17:26:57 +00:00
|
|
|
signingKeyPath: KeyPath): Result[void, KeystoreGenerationError] =
|
|
|
|
let
|
2020-08-06 18:14:44 +00:00
|
|
|
keyName = "0x" & $signingPubKey
|
2020-08-02 17:26:57 +00:00
|
|
|
validatorDir = validatorsDir / keyName
|
|
|
|
|
|
|
|
if not existsDir(validatorDir):
|
2020-10-02 15:46:05 +00:00
|
|
|
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
|
2020-08-02 17:26:57 +00:00
|
|
|
defer: burnMem(password)
|
|
|
|
|
|
|
|
let
|
|
|
|
keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
|
|
|
|
password, signingKeyPath)
|
2021-10-04 19:08:31 +00:00
|
|
|
keystoreFile = validatorDir / KeystoreFileName
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
var encodedStorage: string
|
|
|
|
try:
|
|
|
|
encodedStorage = Json.encode(keyStore)
|
2021-08-27 16:53:21 +00:00
|
|
|
except SerializationError as e:
|
2020-08-27 13:24:30 +00:00
|
|
|
error "Could not serialize keystorage", key_path = keystoreFile
|
2021-08-27 16:53:21 +00:00
|
|
|
return err(KeystoreGenerationError(
|
|
|
|
kind: FailedToCreateKeystoreFile, error: e.msg))
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2021-08-27 16:53:21 +00:00
|
|
|
? secureCreatePath(validatorDir).mapErrTo(FailedToCreateValidatorDir)
|
|
|
|
? secureCreatePath(secretsDir).mapErrTo(FailedToCreateSecretsDir)
|
|
|
|
? secureWriteFile(secretsDir / keyName, password.str).mapErrTo(
|
|
|
|
FailedToCreateSecretFile)
|
|
|
|
? secureWriteFile(keystoreFile, encodedStorage).mapErrTo(
|
|
|
|
FailedToCreateKeystoreFile)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
ok()
|
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
proc generateDeposits*(cfg: RuntimeConfig,
|
2020-07-08 12:36:03 +00:00
|
|
|
rng: var BrHmacDrbgContext,
|
2020-10-19 19:02:48 +00:00
|
|
|
seed: KeySeed,
|
2020-08-21 19:36:42 +00:00
|
|
|
firstValidatorIdx, totalNewValidators: int,
|
2020-06-01 19:48:20 +00:00
|
|
|
validatorsDir: string,
|
2020-08-02 17:26:57 +00:00
|
|
|
secretsDir: string): Result[seq[DepositData], KeystoreGenerationError] =
|
2020-07-17 20:59:50 +00:00
|
|
|
var deposits: seq[DepositData]
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-10-01 18:56:42 +00:00
|
|
|
notice "Generating deposits", totalNewValidators, validatorsDir, secretsDir
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
# We'll reuse a single variable here to make the secret
|
|
|
|
# scrubbing (burnMem) easier to handle:
|
|
|
|
var baseKey = deriveMasterKey(seed)
|
|
|
|
defer: burnMem(baseKey)
|
|
|
|
baseKey = deriveChildKey(baseKey, baseKeyPath)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
for i in 0 ..< totalNewValidators:
|
2020-10-19 19:02:48 +00:00
|
|
|
let validatorIdx = firstValidatorIdx + i
|
|
|
|
|
|
|
|
# We'll reuse a single variable here to make the secret
|
|
|
|
# scrubbing (burnMem) easier to handle:
|
|
|
|
var derivedKey = baseKey
|
|
|
|
defer: burnMem(derivedKey)
|
|
|
|
derivedKey = deriveChildKey(derivedKey, validatorIdx)
|
|
|
|
derivedKey = deriveChildKey(derivedKey, 0) # This is witdrawal key
|
|
|
|
let withdrawalPubKey = derivedKey.toPubKey
|
|
|
|
derivedKey = deriveChildKey(derivedKey, 0) # This is the signing key
|
|
|
|
let signingPubKey = derivedKey.toPubKey
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
? saveKeystore(rng, validatorsDir, secretsDir,
|
2020-10-19 19:02:48 +00:00
|
|
|
derivedKey, signingPubKey,
|
|
|
|
makeKeyPath(validatorIdx, signingKeyKind))
|
2020-06-01 19:48:20 +00:00
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
deposits.add prepareDeposit(
|
|
|
|
cfg, withdrawalPubKey, derivedKey, signingPubKey)
|
2020-04-15 07:59:47 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
ok deposits
|
2020-04-15 07:59:47 +00:00
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
|
2020-08-27 13:24:30 +00:00
|
|
|
let walletDir = splitFile(outWalletPath).dir
|
|
|
|
var encodedWallet: string
|
|
|
|
try:
|
|
|
|
encodedWallet = Json.encode(wallet, pretty = true)
|
|
|
|
except SerializationError:
|
|
|
|
return err("Could not serialize wallet")
|
2020-10-12 13:47:59 +00:00
|
|
|
|
2021-08-27 16:53:21 +00:00
|
|
|
? secureCreatePath(walletDir).mapErr(proc(e: auto): string =
|
|
|
|
"Could not create wallet directory [" & walletDir & "]: " & $e)
|
|
|
|
|
|
|
|
? secureWriteFile(outWalletPath, encodedWallet).mapErr(proc(e: auto): string =
|
|
|
|
"Could not write wallet to file [" & outWalletPath & "]: " & $e)
|
2020-10-27 11:04:17 +00:00
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
ok()
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
proc saveWallet*(wallet: WalletPathPair): Result[void, string] =
|
|
|
|
saveWallet(wallet.wallet, wallet.path)
|
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
proc readPasswordInput(prompt: string, password: var TaintedString): bool =
|
2020-08-21 09:47:35 +00:00
|
|
|
try:
|
|
|
|
when defined(windows):
|
|
|
|
# readPasswordFromStdin() on Windows always returns `false`.
|
|
|
|
# https://github.com/nim-lang/Nim/issues/15207
|
|
|
|
discard readPasswordFromStdin(prompt, password)
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
readPasswordFromStdin(prompt, password)
|
2020-09-08 11:32:43 +00:00
|
|
|
except IOError:
|
2020-08-21 09:47:35 +00:00
|
|
|
false
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
proc setStyleNoError(styles: set[Style]) =
|
|
|
|
when defined(windows):
|
|
|
|
try: stdout.setStyle(styles)
|
|
|
|
except: discard
|
|
|
|
else:
|
|
|
|
try: stdout.setStyle(styles)
|
|
|
|
except IOError, ValueError: discard
|
|
|
|
|
|
|
|
proc setForegroundColorNoError(color: ForegroundColor) =
|
|
|
|
when defined(windows):
|
|
|
|
try: stdout.setForegroundColor(color)
|
|
|
|
except: discard
|
|
|
|
else:
|
|
|
|
try: stdout.setForegroundColor(color)
|
|
|
|
except IOError, ValueError: discard
|
|
|
|
|
|
|
|
proc resetAttributesNoError() =
|
|
|
|
when defined(windows):
|
|
|
|
try: stdout.resetAttributes()
|
|
|
|
except: discard
|
|
|
|
else:
|
|
|
|
try: stdout.resetAttributes()
|
|
|
|
except IOError: discard
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
|
|
|
|
importedDir, validatorsDir, secretsDir: string) =
|
|
|
|
var password: TaintedString
|
|
|
|
defer: burnMem(password)
|
|
|
|
|
|
|
|
try:
|
|
|
|
for file in walkDirRec(importedDir):
|
2020-10-20 13:01:21 +00:00
|
|
|
let filenameParts = splitFile(file)
|
|
|
|
if toLowerAscii(filenameParts.ext) != ".json":
|
|
|
|
continue
|
|
|
|
|
|
|
|
# In case we are importing from eth2.0-deposits-cli, the imported
|
|
|
|
# validator_keys directory will also include a "deposit_data" file
|
|
|
|
# intended for uploading to the launchpad. We'll skip it to avoid
|
|
|
|
# the "Invalid keystore" warning that it will trigger.
|
|
|
|
if filenameParts.name.startsWith("deposit_data"):
|
2020-08-02 17:26:57 +00:00
|
|
|
continue
|
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
let keystore =
|
|
|
|
try:
|
|
|
|
Json.loadFile(file, Keystore)
|
|
|
|
except SerializationError as e:
|
|
|
|
warn "Invalid keystore", err = e.formatMsg(file)
|
|
|
|
continue
|
|
|
|
except IOError as e:
|
|
|
|
warn "Failed to read keystore file", file, err = e.msg
|
|
|
|
continue
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
var firstDecryptionAttempt = true
|
|
|
|
|
|
|
|
while true:
|
2020-10-09 16:41:53 +00:00
|
|
|
var secret: seq[byte]
|
|
|
|
let status = decryptCryptoField(keystore.crypto,
|
|
|
|
KeystorePass.init password,
|
|
|
|
secret)
|
|
|
|
case status
|
|
|
|
of Success:
|
|
|
|
let privKey = ValidatorPrivKey.fromRaw(secret)
|
|
|
|
if privKey.isOk:
|
|
|
|
let pubKey = privKey.value.toPubKey
|
|
|
|
let status = saveKeystore(rng, validatorsDir, secretsDir,
|
|
|
|
privKey.value, pubKey,
|
|
|
|
keystore.path)
|
|
|
|
if status.isOk:
|
|
|
|
notice "Keystore imported", file
|
|
|
|
else:
|
2021-08-27 16:53:21 +00:00
|
|
|
error "Failed to import keystore",
|
|
|
|
file, validatorsDir, secretsDir, err = status.error
|
2020-10-09 16:41:53 +00:00
|
|
|
else:
|
|
|
|
error "Imported keystore holds invalid key", file, err = privKey.error
|
|
|
|
break
|
2020-10-15 18:45:27 +00:00
|
|
|
of InvalidKeystore:
|
|
|
|
warn "Invalid keystore", file
|
|
|
|
break
|
|
|
|
of InvalidPassword:
|
2020-08-02 17:26:57 +00:00
|
|
|
if firstDecryptionAttempt:
|
|
|
|
try:
|
2020-08-02 18:47:15 +00:00
|
|
|
const msg = "Please enter the password for decrypting '$1' " &
|
|
|
|
"or press ENTER to skip importing this keystore"
|
|
|
|
echo msg % [file]
|
2020-08-02 17:26:57 +00:00
|
|
|
except ValueError:
|
|
|
|
raiseAssert "The format string above is correct"
|
|
|
|
else:
|
|
|
|
echo "The entered password was incorrect. Please try again."
|
|
|
|
firstDecryptionAttempt = false
|
|
|
|
|
|
|
|
if not readPasswordInput("Password: ", password):
|
|
|
|
echo "System error while entering password. Please try again."
|
|
|
|
|
|
|
|
if password.len == 0:
|
|
|
|
break
|
|
|
|
except OSError:
|
|
|
|
fatal "Failed to access the imported deposits directory"
|
|
|
|
quit 1
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
template ask(prompt: string): string =
|
2020-07-14 19:00:35 +00:00
|
|
|
try:
|
2020-08-21 19:36:42 +00:00
|
|
|
stdout.write prompt, ": "
|
|
|
|
stdin.readLine()
|
|
|
|
except IOError:
|
|
|
|
return err "failure to read data from stdin"
|
|
|
|
|
|
|
|
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
|
|
|
|
config: BeaconNodeConf,
|
2020-10-19 19:02:48 +00:00
|
|
|
seed: KeySeed): Result[WalletPathPair, string] =
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "When you perform operations with your wallet such as withdrawals " &
|
2020-10-19 19:02:48 +00:00
|
|
|
"and additional deposits, you'll be asked to enter a signing " &
|
|
|
|
"password. Please note that this password is local to the current " &
|
|
|
|
"machine and you can change it at any time."
|
2020-07-14 19:00:35 +00:00
|
|
|
echo ""
|
|
|
|
|
2020-10-05 15:27:05 +00:00
|
|
|
var password =
|
2020-09-29 16:49:09 +00:00
|
|
|
block:
|
|
|
|
let prompt = "Please enter a password: "
|
|
|
|
let confirm = "Please repeat the password: "
|
2021-08-27 16:53:21 +00:00
|
|
|
? keyboardCreatePassword(prompt, confirm).mapErr(proc(e: auto): string = $e)
|
2020-10-01 19:18:56 +00:00
|
|
|
defer: burnMem(password)
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
var name: WalletName
|
|
|
|
let outWalletName = config.outWalletName
|
|
|
|
if outWalletName.isSome:
|
|
|
|
name = outWalletName.get
|
|
|
|
else:
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "For your convenience, the wallet can be identified with a name " &
|
|
|
|
"of your choice. Please enter a wallet name below or press ENTER " &
|
|
|
|
"to continue with a machine-generated name."
|
2020-10-13 12:37:25 +00:00
|
|
|
echo ""
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
while true:
|
|
|
|
var enteredName = ask "Wallet name"
|
|
|
|
if enteredName.len > 0:
|
|
|
|
name =
|
2020-07-14 19:00:35 +00:00
|
|
|
try:
|
2020-09-29 16:49:09 +00:00
|
|
|
WalletName.parseCmdArg(enteredName)
|
|
|
|
except CatchableError as err:
|
|
|
|
echo err.msg & ". Please try again."
|
|
|
|
continue
|
|
|
|
break
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
let nextAccount =
|
|
|
|
if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore:
|
|
|
|
config.restoredDepositsCount
|
|
|
|
else:
|
|
|
|
none Natural
|
2020-08-21 19:36:42 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
let wallet = createWallet(kdfPbkdf2, rng, seed,
|
2020-10-09 20:38:06 +00:00
|
|
|
name = name,
|
|
|
|
nextAccount = nextAccount,
|
|
|
|
password = KeystorePass.init password)
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
let outWalletFileFlag = config.outWalletFile
|
|
|
|
let outWalletFile =
|
|
|
|
if outWalletFileFlag.isSome:
|
|
|
|
string outWalletFileFlag.get
|
|
|
|
else:
|
|
|
|
config.walletsDir / addFileExt(string wallet.name, "json")
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
let status = saveWallet(wallet, outWalletFile)
|
|
|
|
if status.isErr:
|
|
|
|
return err("failure to create wallet file due to " & status.error)
|
2020-09-29 16:49:09 +00:00
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
echo "\nWallet file successfully written to \"", outWalletFile, "\""
|
|
|
|
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
|
|
|
|
|
2020-10-10 09:54:04 +00:00
|
|
|
when defined(windows):
|
2020-10-12 19:58:09 +00:00
|
|
|
proc clearScreen =
|
|
|
|
discard execShellCmd("cls")
|
2020-10-10 09:54:04 +00:00
|
|
|
else:
|
|
|
|
template clearScreen =
|
|
|
|
echo "\e[1;1H\e[2J\e[3J"
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
proc createWalletInteractively*(
|
|
|
|
rng: var BrHmacDrbgContext,
|
|
|
|
config: BeaconNodeConf): Result[CreatedWallet, string] =
|
|
|
|
|
|
|
|
if config.nonInteractive:
|
|
|
|
return err "not running in interactive mode"
|
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "The generated wallet is uniquely identified by a seed phrase " &
|
|
|
|
"consisting of 24 words. In case you lose your wallet and you " &
|
|
|
|
"need to restore it on a different machine, you can use the " &
|
|
|
|
"seed phrase to re-generate your signing and withdrawal keys."
|
2020-10-15 11:49:02 +00:00
|
|
|
echoP "The seed phrase should be kept secret in a safe location as if " &
|
2020-10-09 20:38:06 +00:00
|
|
|
"you are protecting a sensitive password. It can be used to withdraw " &
|
|
|
|
"funds from your wallet."
|
|
|
|
echoP "We will display the seed phrase on the next screen. Please make sure " &
|
|
|
|
"you are in a safe environment and there are no cameras or potentially " &
|
|
|
|
"unwanted eye witnesses around you. Please prepare everything necessary " &
|
|
|
|
"to copy the seed phrase to a safe location and type 'continue' in " &
|
|
|
|
"the prompt below to proceed to the next screen or 'q' to exit now."
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
while true:
|
|
|
|
let answer = ask "Action"
|
|
|
|
if answer.len > 0 and answer[0] == 'q': quit 1
|
|
|
|
if answer == "continue": break
|
|
|
|
echoP "To proceed to your seed phrase, please type 'continue' (without the quotes). " &
|
|
|
|
"Type 'q' to exit now."
|
|
|
|
echo ""
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
var mnemonic = generateMnemonic(rng)
|
|
|
|
defer: burnMem(mnemonic)
|
|
|
|
|
|
|
|
try:
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "Your seed phrase is:"
|
2020-08-21 19:36:42 +00:00
|
|
|
setStyleNoError({styleBright})
|
|
|
|
setForegroundColorNoError fgCyan
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP $mnemonic
|
2020-08-21 19:36:42 +00:00
|
|
|
resetAttributesNoError()
|
|
|
|
except IOError, ValueError:
|
|
|
|
return err "failure to write to the standard output"
|
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
echoP "Press any key to continue."
|
|
|
|
try:
|
2020-10-12 19:58:09 +00:00
|
|
|
discard getch()
|
2020-10-09 20:38:06 +00:00
|
|
|
except IOError as err:
|
|
|
|
fatal "Failed to read a key from stdin", err = err.msg
|
|
|
|
quit 1
|
2020-08-21 19:36:42 +00:00
|
|
|
|
2020-10-10 09:54:04 +00:00
|
|
|
clearScreen()
|
2020-10-09 20:38:06 +00:00
|
|
|
|
2020-10-15 11:49:02 +00:00
|
|
|
echoP "To confirm that you've saved the seed phrase, please enter the " &
|
|
|
|
"first and the last three words of it. In case you've saved the " &
|
|
|
|
"seek phrase in your clipboard, we strongly advice clearing the " &
|
|
|
|
"clipboard now."
|
2020-08-21 19:36:42 +00:00
|
|
|
echo ""
|
|
|
|
|
2020-10-09 20:38:06 +00:00
|
|
|
for i in countdown(2, 0):
|
2020-08-21 19:36:42 +00:00
|
|
|
let answer = ask "Answer"
|
2020-10-09 20:38:06 +00:00
|
|
|
let parts = answer.split(' ', maxsplit = 1)
|
|
|
|
if parts.len == 2:
|
|
|
|
if count(parts[1], ' ') == 2 and
|
|
|
|
mnemonic.string.startsWith(parts[0]) and
|
|
|
|
mnemonic.string.endsWith(parts[1]):
|
|
|
|
break
|
2020-08-21 19:36:42 +00:00
|
|
|
else:
|
2020-10-09 20:38:06 +00:00
|
|
|
doAssert parts.len == 1
|
|
|
|
|
|
|
|
if i > 0:
|
|
|
|
echo "\nYour answer was not correct. You have ", i, " more attempts"
|
|
|
|
echoP "Please enter 4 words separated with a single space " &
|
|
|
|
"(the first word from the seed phrase, followed by the last 3)"
|
|
|
|
echo ""
|
|
|
|
else:
|
|
|
|
quit 1
|
|
|
|
|
2020-10-10 09:54:04 +00:00
|
|
|
clearScreen()
|
2020-08-21 19:36:42 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
var mnenomicPassword = KeystorePass.init ""
|
|
|
|
defer: burnMem(mnenomicPassword)
|
|
|
|
|
|
|
|
echoP "The recovery of your wallet can be additionally protected by a" &
|
|
|
|
"recovery password. Since the seed phrase itself can be considered " &
|
|
|
|
"a password, setting such an additional password is optional. " &
|
|
|
|
"To ensure the strongest possible security, we recommend writing " &
|
|
|
|
"down your seed phrase and remembering your recovery password. " &
|
|
|
|
"If you don'n want to set a recovery password, just press ENTER."
|
|
|
|
|
|
|
|
var recoveryPassword = keyboardCreatePassword(
|
|
|
|
"Recovery password: ", "Confirm password: ", allowEmpty = true)
|
|
|
|
defer:
|
|
|
|
if recoveryPassword.isOk:
|
|
|
|
burnMem(recoveryPassword.get)
|
|
|
|
|
|
|
|
if recoveryPassword.isErr:
|
2021-08-27 16:53:21 +00:00
|
|
|
fatal "Failed to read password from stdin: "
|
2020-10-19 19:02:48 +00:00
|
|
|
quit 1
|
|
|
|
|
|
|
|
var keystorePass = KeystorePass.init recoveryPassword.get
|
|
|
|
defer: burnMem(keystorePass)
|
|
|
|
|
|
|
|
var seed = getSeed(mnemonic, keystorePass)
|
|
|
|
defer: burnMem(seed)
|
|
|
|
|
|
|
|
let walletPath = ? pickPasswordAndSaveWallet(rng, config, seed)
|
|
|
|
return ok CreatedWallet(walletPath: walletPath, seed: seed)
|
2020-08-21 19:36:42 +00:00
|
|
|
|
|
|
|
proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
|
|
|
|
config: BeaconNodeConf) =
|
|
|
|
var
|
|
|
|
enteredMnemonic: TaintedString
|
|
|
|
validatedMnemonic: Mnemonic
|
|
|
|
|
|
|
|
defer:
|
|
|
|
burnMem enteredMnemonic
|
|
|
|
burnMem validatedMnemonic
|
|
|
|
|
|
|
|
echo "To restore your wallet, please enter your backed-up seed phrase."
|
|
|
|
while true:
|
2020-09-29 16:49:09 +00:00
|
|
|
if not readPasswordInput("Seedphrase: ", enteredMnemonic):
|
2020-08-21 19:36:42 +00:00
|
|
|
fatal "failure to read password from stdin"
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
if validateMnemonic(enteredMnemonic, validatedMnemonic):
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
echo "The entered mnemonic was not valid. Please try again."
|
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
echoP "If your seed phrase was protected with a recovery password, " &
|
|
|
|
"please enter it below. Please ENTER to attempt to restore " &
|
|
|
|
"the wallet without a recovery password."
|
|
|
|
|
|
|
|
var recoveryPassword = keyboardCreatePassword(
|
|
|
|
"Recovery password: ", "Confirm password: ", allowEmpty = true)
|
|
|
|
defer:
|
|
|
|
if recoveryPassword.isOk:
|
|
|
|
burnMem(recoveryPassword.get)
|
|
|
|
|
|
|
|
if recoveryPassword.isErr:
|
|
|
|
fatal "Failed to read password from stdin"
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
var keystorePass = KeystorePass.init recoveryPassword.get
|
|
|
|
defer: burnMem(keystorePass)
|
|
|
|
|
|
|
|
var seed = getSeed(validatedMnemonic, keystorePass)
|
|
|
|
defer: burnMem(seed)
|
|
|
|
|
|
|
|
discard pickPasswordAndSaveWallet(rng, config, seed)
|
2020-08-21 19:36:42 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
proc unlockWalletInteractively*(wallet: Wallet): Result[KeySeed, string] =
|
2020-07-17 20:59:50 +00:00
|
|
|
echo "Please enter the password for unlocking the wallet"
|
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
let res = keyboardGetPassword[KeySeed]("Password: ", 3,
|
|
|
|
proc (password: string): KsResult[KeySeed] =
|
2020-10-09 16:41:53 +00:00
|
|
|
var secret: seq[byte]
|
|
|
|
defer: burnMem(secret)
|
|
|
|
let status = decryptCryptoField(wallet.crypto, KeystorePass.init password, secret)
|
|
|
|
case status
|
|
|
|
of Success:
|
2020-10-19 19:02:48 +00:00
|
|
|
ok(KeySeed secret)
|
2020-07-17 20:59:50 +00:00
|
|
|
else:
|
2020-10-09 16:41:53 +00:00
|
|
|
# TODO Handle InvalidKeystore in a special way here
|
2020-09-29 16:49:09 +00:00
|
|
|
let failed = "Unlocking of the wallet failed. Please try again"
|
|
|
|
echo failed
|
|
|
|
err(failed)
|
|
|
|
)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
if res.isOk():
|
|
|
|
ok(res.get())
|
|
|
|
else:
|
|
|
|
err "Unlocking of the wallet failed."
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-10-02 13:38:32 +00:00
|
|
|
proc loadWallet*(fileName: string): Result[Wallet, string] =
|
|
|
|
try:
|
|
|
|
ok Json.loadFile(fileName, Wallet)
|
|
|
|
except SerializationError as err:
|
|
|
|
err "Invalid wallet syntax: " & err.formatMsg(fileName)
|
|
|
|
except IOError as err:
|
|
|
|
err "Error accessing wallet file \"" & fileName & "\": " & err.msg
|
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
proc findWallet*(config: BeaconNodeConf,
|
2020-10-02 13:38:32 +00:00
|
|
|
name: WalletName): Result[Option[WalletPathPair], string] =
|
2020-07-17 20:59:50 +00:00
|
|
|
var walletFiles = newSeq[string]()
|
|
|
|
try:
|
|
|
|
for kind, walletFile in walkDir(config.walletsDir):
|
|
|
|
if kind != pcFile: continue
|
2020-08-13 11:32:10 +00:00
|
|
|
let walletId = splitFile(walletFile).name
|
|
|
|
if cmpIgnoreCase(walletId, name.string) == 0:
|
2020-08-21 19:36:42 +00:00
|
|
|
let wallet = ? loadWallet(walletFile)
|
2020-10-02 13:38:32 +00:00
|
|
|
return ok some WalletPathPair(wallet: wallet, path: walletFile)
|
2020-08-13 11:32:10 +00:00
|
|
|
walletFiles.add walletFile
|
2020-10-02 13:38:32 +00:00
|
|
|
except OSError as err:
|
|
|
|
return err("Error accessing the wallets directory \"" &
|
|
|
|
config.walletsDir & "\": " & err.msg)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
for walletFile in walletFiles:
|
2020-08-21 19:36:42 +00:00
|
|
|
let wallet = ? loadWallet(walletFile)
|
2020-10-01 19:18:56 +00:00
|
|
|
if cmpIgnoreCase(wallet.name.string, name.string) == 0 or
|
|
|
|
cmpIgnoreCase(wallet.uuid.string, name.string) == 0:
|
2020-10-02 13:38:32 +00:00
|
|
|
return ok some WalletPathPair(wallet: wallet, path: walletFile)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-10-02 13:38:32 +00:00
|
|
|
return ok none(WalletPathPair)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
type
|
|
|
|
# This is not particularly well-standardized yet.
|
|
|
|
# Some relevant code for generating (1) and validating (2) the data can be found below:
|
|
|
|
# 1) https://github.com/ethereum/eth2.0-deposit-cli/blob/dev/eth2deposit/credentials.py
|
|
|
|
# 2) https://github.com/ethereum/eth2.0-deposit/blob/dev/src/pages/UploadValidator/validateDepositKey.ts
|
|
|
|
LaunchPadDeposit* = object
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
withdrawal_credentials*: Eth2Digest
|
|
|
|
amount*: Gwei
|
|
|
|
signature*: ValidatorSig
|
|
|
|
deposit_message_root*: Eth2Digest
|
|
|
|
deposit_data_root*: Eth2Digest
|
|
|
|
fork_version*: Version
|
|
|
|
|
|
|
|
func init*(T: type LaunchPadDeposit,
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
cfg: RuntimeConfig, d: DepositData): T =
|
2020-07-17 20:59:50 +00:00
|
|
|
T(pubkey: d.pubkey,
|
|
|
|
withdrawal_credentials: d.withdrawal_credentials,
|
|
|
|
amount: d.amount,
|
|
|
|
signature: d.signature,
|
|
|
|
deposit_message_root: hash_tree_root(d as DepositMessage),
|
|
|
|
deposit_data_root: hash_tree_root(d),
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
fork_version: cfg.GENESIS_FORK_VERSION)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
func `as`*(copied: LaunchPadDeposit, T: type DepositData): T =
|
|
|
|
T(pubkey: copied.pubkey,
|
|
|
|
withdrawal_credentials: copied.withdrawal_credentials,
|
|
|
|
amount: copied.amount,
|
|
|
|
signature: copied.signature)
|