2021-03-26 06:52:01 +00:00
|
|
|
# beacon_chain
|
2022-03-14 12:50:23 +00:00
|
|
|
# Copyright (c) 2018-2022 Status Research & Development GmbH
|
2021-03-26 06:52:01 +00:00
|
|
|
# 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,
|
2022-03-30 22:00:03 +00:00
|
|
|
nimbus_security_resources,
|
2021-11-30 01:20:21 +00:00
|
|
|
".."/spec/[eth2_merkleization, keystore, crypto],
|
2021-10-04 19:08:31 +00:00
|
|
|
".."/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-11-30 01:20:21 +00:00
|
|
|
keystore, validator_pool, crypto
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-10-15 12:50:21 +00:00
|
|
|
when defined(windows):
|
|
|
|
import stew/[windows/acl]
|
|
|
|
|
2022-05-05 05:45:35 +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"
|
2021-11-30 01:20:21 +00:00
|
|
|
RemoteKeystoreFileName* = "remote_keystore.json"
|
2021-10-04 19:08:31 +00:00
|
|
|
NetKeystoreFileName* = "network_keystore.json"
|
|
|
|
DisableFileName* = ".disable"
|
|
|
|
DisableFileContent* = "Please do not remove this file manually. " &
|
|
|
|
"This can lead to slashing of this validator's key."
|
2022-02-07 20:36:09 +00:00
|
|
|
KeyNameSize* = 98 # 0x + hexadecimal key representation 96 characters.
|
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-12-22 12:37:31 +00:00
|
|
|
AnyConf* = BeaconNodeConf | ValidatorClientConf | SigningNodeConf
|
|
|
|
|
|
|
|
KmResult*[T] = Result[T, cstring]
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
RemoveValidatorStatus* {.pure.} = enum
|
2021-12-22 12:37:31 +00:00
|
|
|
deleted = "Deleted"
|
2022-02-07 20:36:09 +00:00
|
|
|
notFound = "Not found"
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
AddValidatorStatus* {.pure.} = enum
|
2021-12-22 12:37:31 +00:00
|
|
|
existingArtifacts = "Keystore artifacts already exists"
|
2022-02-07 20:36:09 +00:00
|
|
|
failed = "Validator not added"
|
|
|
|
|
|
|
|
AddValidatorFailure* = object
|
|
|
|
status*: AddValidatorStatus
|
|
|
|
message*: string
|
|
|
|
|
|
|
|
ImportResult*[T] = Result[T, AddValidatorFailure]
|
2021-12-22 12:37:31 +00:00
|
|
|
|
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(
|
2022-03-30 22:00:03 +00:00
|
|
|
nimbusSecurityResourcesPath /
|
|
|
|
"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-12-22 12:37:31 +00:00
|
|
|
func init*(T: type KeystoreData,
|
|
|
|
privateKey: ValidatorPrivKey,
|
|
|
|
keystore: Keystore): T =
|
|
|
|
KeystoreData(
|
|
|
|
kind: KeystoreKind.Local,
|
2021-10-04 19:08:31 +00:00
|
|
|
privateKey: privateKey,
|
2021-10-07 16:30:34 +00:00
|
|
|
description: if keystore.description == nil: none(string)
|
|
|
|
else: some(keystore.description[]),
|
2021-12-22 12:37:31 +00:00
|
|
|
path: keystore.path,
|
|
|
|
uuid: keystore.uuid,
|
|
|
|
version: uint64(keystore.version),
|
|
|
|
pubkey: privateKey.toPubKey().toPubKey()
|
2021-10-04 19:08:31 +00:00
|
|
|
)
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
func init*(T: type KeystoreData,
|
|
|
|
keystore: RemoteKeystore): Result[T, cstring] =
|
2021-11-30 01:20:21 +00:00
|
|
|
let cookedKey =
|
|
|
|
block:
|
|
|
|
let res = keystore.pubkey.load()
|
|
|
|
if res.isNone():
|
|
|
|
return err("Invalid validator's public key")
|
|
|
|
res.get()
|
2021-12-22 12:37:31 +00:00
|
|
|
ok KeystoreData(
|
|
|
|
kind: KeystoreKind.Remote,
|
|
|
|
pubkey: cookedKey.toPubKey,
|
2021-11-30 01:20:21 +00:00
|
|
|
description: keystore.description,
|
|
|
|
version: keystore.version,
|
|
|
|
remoteUrl: keystore.remote
|
2021-12-22 12:37:31 +00:00
|
|
|
)
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
func init*(T: type KeystoreData, cookedKey: CookedPubKey,
|
|
|
|
remoteUrl: HttpHostUri): T =
|
|
|
|
KeystoreData(
|
|
|
|
kind: KeystoreKind.Remote,
|
|
|
|
pubkey: cookedKey.toPubKey(),
|
|
|
|
version: 1'u64,
|
|
|
|
remoteUrl: remoteUrl
|
|
|
|
)
|
|
|
|
|
|
|
|
func init(T: type AddValidatorFailure, status: AddValidatorStatus,
|
|
|
|
msg = ""): AddValidatorFailure {.raises: [Defect].} =
|
|
|
|
AddValidatorFailure(status: status, message: msg)
|
|
|
|
|
|
|
|
func toKeystoreKind*(kind: ValidatorKind): KeystoreKind {.raises: [Defect].} =
|
|
|
|
case kind
|
|
|
|
of ValidatorKind.Local:
|
|
|
|
KeystoreKind.Local
|
|
|
|
of ValidatorKind.Remote:
|
|
|
|
KeystoreKind.Remote
|
|
|
|
|
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:
|
2022-03-22 17:06:21 +00:00
|
|
|
if (let res = secureCreatePath(dataDir); res.isErr):
|
|
|
|
fatal "Could not create data directory",
|
|
|
|
path = dataDir, err = 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",
|
2022-03-22 17:06:21 +00:00
|
|
|
path = dataDir, errorCode = $cres.error,
|
2020-10-12 13:04:21 +00:00
|
|
|
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:
|
2022-03-22 17:06:21 +00:00
|
|
|
fatal "Data folder has insecure ACL", path = dataDir
|
2020-10-30 00:36:47 +00:00
|
|
|
return false
|
2020-10-12 13:04:21 +00:00
|
|
|
else:
|
2022-03-22 17:06:21 +00:00
|
|
|
if (let res = secureCreatePath(dataDir); res.isErr):
|
|
|
|
fatal "Could not create data folder",
|
|
|
|
path = dataDir, err = 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,
|
2021-12-22 12:37:31 +00:00
|
|
|
keyName: string): KsResult[KeystoreData] =
|
2021-10-04 19:08:31 +00:00
|
|
|
## 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():
|
2021-12-22 12:37:31 +00:00
|
|
|
ok(KeystoreData.init(res.get(), keystore))
|
2021-10-04 19:08:31 +00:00
|
|
|
else:
|
|
|
|
err("Failed to decrypt keystore")
|
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
proc loadRemoteKeystoreImpl(validatorsDir,
|
2021-12-22 12:37:31 +00:00
|
|
|
keyName: string): Option[KeystoreData] =
|
2022-02-07 20:36:09 +00:00
|
|
|
let keystorePath = validatorsDir / keyName / RemoteKeystoreFileName
|
|
|
|
|
|
|
|
if not(checkSensitiveFilePermissions(keystorePath)):
|
|
|
|
error "Remote keystorage file has insecure permissions",
|
|
|
|
key_path = keystorePath
|
|
|
|
return
|
|
|
|
|
|
|
|
let keyStore =
|
2021-11-30 01:20:21 +00:00
|
|
|
block:
|
2022-02-07 20:36:09 +00:00
|
|
|
let remoteKeystore =
|
2021-11-30 01:20:21 +00:00
|
|
|
try:
|
2022-02-07 20:36:09 +00:00
|
|
|
Json.loadFile(keystorePath, RemoteKeystore)
|
|
|
|
except IOError as err:
|
|
|
|
error "Failed to read remote keystore file", err = err.msg,
|
|
|
|
path = keystorePath
|
|
|
|
return
|
2021-11-30 01:20:21 +00:00
|
|
|
except SerializationError as e:
|
2022-02-07 20:36:09 +00:00
|
|
|
error "Invalid remote keystore file",
|
|
|
|
path = keystorePath,
|
|
|
|
err_msg = e.formatMsg(keystorePath)
|
2021-11-30 01:20:21 +00:00
|
|
|
return
|
2022-02-07 20:36:09 +00:00
|
|
|
let res = init(KeystoreData, remoteKeystore)
|
2021-11-30 01:20:21 +00:00
|
|
|
if res.isErr():
|
2022-02-07 20:36:09 +00:00
|
|
|
error "Invalid remote keystore file",
|
|
|
|
path = keystorePath
|
2021-11-30 01:20:21 +00:00
|
|
|
return
|
|
|
|
res.get()
|
2022-02-07 20:36:09 +00:00
|
|
|
some(keyStore)
|
2021-11-30 01:20:21 +00:00
|
|
|
|
|
|
|
proc loadKeystoreImpl(validatorsDir, secretsDir, keyName: string,
|
2021-12-22 12:37:31 +00:00
|
|
|
nonInteractive: bool): Option[KeystoreData] =
|
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)):
|
2022-04-08 16:22:49 +00:00
|
|
|
error "Password file has insecure permissions", key_path = keystorePath
|
2020-08-27 13:24:30 +00:00
|
|
|
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():
|
2021-12-22 12:37:31 +00:00
|
|
|
return some(KeystoreData.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-12-22 12:37:31 +00:00
|
|
|
some(KeystoreData.init(res.get(), keystore))
|
2020-09-29 16:49:09 +00:00
|
|
|
else:
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
|
2021-12-22 12:37:31 +00:00
|
|
|
nonInteractive: bool): Option[KeystoreData] =
|
2021-11-30 01:20:21 +00:00
|
|
|
let
|
|
|
|
keystorePath = validatorsDir / keyName
|
|
|
|
localKeystorePath = keystorePath / KeystoreFileName
|
|
|
|
remoteKeystorePath = keystorePath / RemoteKeystoreFileName
|
|
|
|
|
|
|
|
if fileExists(localKeystorePath):
|
|
|
|
loadKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive)
|
|
|
|
elif fileExists(remoteKeystorePath):
|
|
|
|
loadRemoteKeystoreImpl(validatorsDir, keyName)
|
|
|
|
else:
|
|
|
|
error "Unable to find any keystore files", keystorePath
|
2021-12-22 12:37:31 +00:00
|
|
|
none[KeystoreData]()
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string,
|
|
|
|
kind: KeystoreKind
|
|
|
|
): KmResult[RemoveValidatorStatus] {.
|
2021-10-04 19:08:31 +00:00
|
|
|
raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
let
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile =
|
|
|
|
case kind
|
|
|
|
of KeystoreKind.Local:
|
|
|
|
keystoreDir / KeystoreFileName
|
|
|
|
of KeystoreKind.Remote:
|
|
|
|
keystoreDir / RemoteKeystoreFileName
|
|
|
|
secretFile = secretsDir / keyName
|
|
|
|
|
2022-03-24 00:38:48 +00:00
|
|
|
if not(dirExists(keystoreDir)):
|
2022-02-07 20:36:09 +00:00
|
|
|
return ok(RemoveValidatorStatus.notFound)
|
|
|
|
|
2022-03-24 00:38:48 +00:00
|
|
|
if not(fileExists(keystoreFile)):
|
2022-02-07 20:36:09 +00:00
|
|
|
return ok(RemoveValidatorStatus.notFound)
|
|
|
|
|
|
|
|
case kind
|
|
|
|
of KeystoreKind.Local:
|
|
|
|
block:
|
|
|
|
let res = io2.removeFile(keystoreFile)
|
|
|
|
if res.isErr():
|
|
|
|
return err("Could not remove keystore file")
|
|
|
|
block:
|
|
|
|
let res = io2.removeFile(secretFile)
|
2022-03-24 00:38:48 +00:00
|
|
|
if res.isErr() and fileExists(secretFile):
|
2022-02-07 20:36:09 +00:00
|
|
|
return err("Could not remove password file")
|
|
|
|
# We remove folder with all subfolders and files inside.
|
|
|
|
try:
|
|
|
|
removeDir(keystoreDir, false)
|
|
|
|
except OSError:
|
|
|
|
return err("Could not remove keystore directory")
|
|
|
|
of KeystoreKind.Remote:
|
|
|
|
block:
|
|
|
|
let res = io2.removeFile(keystoreFile)
|
|
|
|
if res.isErr():
|
|
|
|
return err("Could not remove keystore file")
|
|
|
|
# We remove folder with all subfolders and files inside.
|
|
|
|
try:
|
|
|
|
removeDir(keystoreDir, false)
|
|
|
|
except OSError:
|
|
|
|
return err("Could not remove keystore directory")
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
ok(RemoveValidatorStatus.deleted)
|
2020-09-01 13:44:40 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc removeValidatorFiles*(conf: AnyConf, keyName: string,
|
|
|
|
kind: KeystoreKind
|
|
|
|
): KmResult[RemoveValidatorStatus] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
removeValidatorFiles(conf.validatorsDir(), conf.secretsDir(), keyName, kind)
|
2021-12-22 12:37:31 +00:00
|
|
|
|
|
|
|
proc removeValidator*(pool: var ValidatorPool, conf: AnyConf,
|
2022-02-07 20:36:09 +00:00
|
|
|
publicKey: ValidatorPubKey,
|
|
|
|
kind: KeystoreKind): KmResult[RemoveValidatorStatus] {.
|
2021-12-22 12:37:31 +00:00
|
|
|
raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
let validator = pool.getValidator(publicKey)
|
|
|
|
if isNil(validator):
|
|
|
|
return ok(RemoveValidatorStatus.notFound)
|
|
|
|
if validator.kind.toKeystoreKind() != kind:
|
|
|
|
return ok(RemoveValidatorStatus.notFound)
|
|
|
|
let publicKeyName: string = "0x" & publicKey.toHex()
|
|
|
|
let res = removeValidatorFiles(conf, publicKeyName, kind)
|
2021-12-22 12:37:31 +00:00
|
|
|
if res.isErr():
|
|
|
|
return err(res.error())
|
|
|
|
pool.removeValidator(publicKey)
|
2022-02-07 20:36:09 +00:00
|
|
|
ok(res.value())
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc checkKeyName*(keyName: string): bool =
|
|
|
|
const keyAlphabet = {'a'..'f', 'A'..'F', '0'..'9'}
|
|
|
|
if len(keyName) != KeyNameSize:
|
|
|
|
return false
|
|
|
|
if keyName[0] != '0' and keyName[1] != 'x':
|
|
|
|
return false
|
|
|
|
for index in 2 ..< len(keyName):
|
|
|
|
if keyName[index] notin keyAlphabet:
|
|
|
|
return false
|
|
|
|
true
|
|
|
|
|
|
|
|
proc existsKeystore*(keystoreDir: string, keyKind: KeystoreKind): bool {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
case keyKind
|
|
|
|
of KeystoreKind.Local:
|
2022-03-24 00:38:48 +00:00
|
|
|
fileExists(keystoreDir / KeystoreFileName)
|
2022-02-07 20:36:09 +00:00
|
|
|
of KeystoreKind.Remote:
|
2022-03-24 00:38:48 +00:00
|
|
|
fileExists(keystoreDir / RemoteKeystoreFileName)
|
2022-02-07 20:36:09 +00:00
|
|
|
|
|
|
|
proc existsKeystore*(keystoreDir: string,
|
|
|
|
keysMask: set[KeystoreKind]): bool {.raises: [Defect].} =
|
|
|
|
if KeystoreKind.Local in keysMask:
|
|
|
|
if existsKeystore(keystoreDir, KeystoreKind.Local):
|
|
|
|
return true
|
|
|
|
if KeystoreKind.Remote in keysMask:
|
|
|
|
if existsKeystore(keystoreDir, KeystoreKind.Remote):
|
|
|
|
return true
|
|
|
|
false
|
|
|
|
|
|
|
|
iterator listLoadableKeystores*(validatorsDir, secretsDir: string,
|
|
|
|
nonInteractive: bool,
|
|
|
|
keysMask: set[KeystoreKind]): KeystoreData =
|
2020-06-01 19:48:20 +00:00
|
|
|
try:
|
|
|
|
for kind, file in walkDir(validatorsDir):
|
|
|
|
if kind == pcDir:
|
2021-12-22 12:37:31 +00:00
|
|
|
|
|
|
|
let
|
|
|
|
keyName = splitFile(file).name
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / KeystoreFileName
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
if not(checkKeyName(keyName)):
|
|
|
|
# Skip folders which name do not satisfy "0x[a-fA-F0-9]{96, 96}".
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not(existsKeystore(keystoreDir, keysMask)):
|
2021-12-22 12:37:31 +00:00
|
|
|
# Skip folders which do not have keystore file inside.
|
|
|
|
continue
|
|
|
|
|
|
|
|
let
|
|
|
|
secretFile = secretsDir / keyName
|
2022-02-07 20:36:09 +00:00
|
|
|
keystore = loadKeystore(validatorsDir, secretsDir, keyName,
|
|
|
|
nonInteractive)
|
2021-12-22 12:37:31 +00:00
|
|
|
if keystore.isSome():
|
2022-02-07 20:36:09 +00:00
|
|
|
yield keystore.get()
|
2021-12-22 12:37:31 +00:00
|
|
|
else:
|
2022-02-07 20:36:09 +00:00
|
|
|
fatal "Unable to load keystore", keystore = file
|
2021-12-22 12:37:31 +00:00
|
|
|
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
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
iterator listLoadableKeystores*(config: AnyConf): KeystoreData =
|
|
|
|
for el in listLoadableKeystores(config.validatorsDir(),
|
|
|
|
config.secretsDir(),
|
2022-02-07 20:36:09 +00:00
|
|
|
config.nonInteractive,
|
|
|
|
{KeystoreKind.Local, KeystoreKind.Remote}):
|
2021-12-22 12:37:31 +00:00
|
|
|
yield el
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
type
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreGenerationErrorKind* = enum
|
|
|
|
FailedToCreateValidatorsDir
|
|
|
|
FailedToCreateKeystoreDir
|
2020-08-02 18:47:15 +00:00
|
|
|
FailedToCreateSecretsDir
|
2020-06-01 19:48:20 +00:00
|
|
|
FailedToCreateSecretFile
|
|
|
|
FailedToCreateKeystoreFile
|
2022-02-07 20:36:09 +00:00
|
|
|
DuplicateKeystoreDir
|
|
|
|
DuplicateKeystoreFile
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2021-08-27 16:53:21 +00:00
|
|
|
KeystoreGenerationError* = object
|
|
|
|
case kind*: KeystoreGenerationErrorKind
|
2021-12-22 12:37:31 +00:00
|
|
|
of FailedToCreateKeystoreDir,
|
|
|
|
FailedToCreateValidatorsDir,
|
|
|
|
FailedToCreateSecretsDir,
|
|
|
|
FailedToCreateSecretFile,
|
2022-02-07 20:36:09 +00:00
|
|
|
FailedToCreateKeystoreFile,
|
|
|
|
DuplicateKeystoreDir,
|
|
|
|
DuplicateKeystoreFile:
|
2021-08-27 16:53:21 +00:00
|
|
|
error*: string
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
proc mapErrTo*[T, E](r: Result[T, E], v: static KeystoreGenerationErrorKind):
|
2021-08-27 16:53:21 +00:00
|
|
|
Result[T, KeystoreGenerationError] =
|
|
|
|
r.mapErr(proc (e: E): KeystoreGenerationError =
|
|
|
|
KeystoreGenerationError(kind: v, error: $e))
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-04-08 16:22:49 +00:00
|
|
|
proc loadNetKeystore*(keystorePath: string,
|
2020-08-25 10:16:31 +00:00
|
|
|
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",
|
2022-04-08 16:22:49 +00:00
|
|
|
key_path = keystorePath
|
2020-08-27 13:24:30 +00:00
|
|
|
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"
|
2022-04-08 16:22:49 +00:00
|
|
|
let decrypted = decryptNetKeystore(keyStore,
|
2022-02-07 20:36:09 +00:00
|
|
|
KeystorePass.init(insecurePwd.get()))
|
2020-08-24 16:06:41 +00:00
|
|
|
if decrypted.isOk:
|
|
|
|
return some(decrypted.get())
|
|
|
|
else:
|
2022-04-08 16:22:49 +00:00
|
|
|
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] =
|
2022-04-08 16:22:49 +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
|
|
|
|
2022-04-08 16:22:49 +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",
|
2022-04-08 16:22:49 +00:00
|
|
|
key_path = keystorePath
|
2020-09-29 16:49:09 +00:00
|
|
|
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:
|
2022-04-08 16:22:49 +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
|
|
|
|
2022-04-08 16:22:49 +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",
|
2022-04-08 16:22:49 +00:00
|
|
|
key_path = keystorePath
|
2021-08-27 16:53:21 +00:00
|
|
|
res.mapErrTo(FailedToCreateKeystoreFile)
|
2020-08-24 16:06:41 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc createValidatorFiles*(secretsDir, validatorsDir, keystoreDir, secretFile,
|
|
|
|
passwordAsString, keystoreFile,
|
|
|
|
encodedStorage: string
|
|
|
|
): Result[void, KeystoreGenerationError] =
|
2021-12-22 12:37:31 +00:00
|
|
|
|
|
|
|
var
|
|
|
|
success = false # becomes true when everything is created successfully
|
|
|
|
cleanupSecretsDir = true # becomes false if secretsDir already existed
|
|
|
|
cleanupValidatorsDir = true # becomes false if validatorsDir already existed
|
|
|
|
|
|
|
|
# secretsDir:
|
|
|
|
let secretsDirExisted: bool = dirExists(secretsDir)
|
|
|
|
if not(secretsDirExisted):
|
|
|
|
? secureCreatePath(secretsDir).mapErrTo(FailedToCreateSecretsDir)
|
|
|
|
defer:
|
|
|
|
if not (success or secretsDirExisted):
|
|
|
|
discard io2.removeDir(secretsDir)
|
|
|
|
|
|
|
|
# validatorsDir:
|
|
|
|
let validatorsDirExisted: bool = dirExists(validatorsDir)
|
|
|
|
if not(validatorsDirExisted):
|
|
|
|
? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir)
|
|
|
|
defer:
|
|
|
|
if not (success or validatorsDirExisted):
|
|
|
|
discard io2.removeDir(validatorsDir)
|
|
|
|
|
|
|
|
# keystoreDir:
|
|
|
|
? secureCreatePath(keystoreDir).mapErrTo(FailedToCreateKeystoreDir)
|
|
|
|
defer:
|
|
|
|
if not success:
|
|
|
|
discard io2.removeDir(keystoreDir)
|
|
|
|
|
|
|
|
# secretFile:
|
2022-02-07 20:36:09 +00:00
|
|
|
? secureWriteFile(secretFile,
|
|
|
|
passwordAsString).mapErrTo(FailedToCreateSecretFile)
|
2021-12-22 12:37:31 +00:00
|
|
|
defer:
|
|
|
|
if not success:
|
|
|
|
discard io2.removeFile(secretFile)
|
|
|
|
|
|
|
|
# keystoreFile:
|
2022-02-07 20:36:09 +00:00
|
|
|
? secureWriteFile(keystoreFile,
|
|
|
|
encodedStorage).mapErrTo(FailedToCreateKeystoreFile)
|
2021-12-22 12:37:31 +00:00
|
|
|
|
|
|
|
success = true
|
|
|
|
ok()
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc createValidatorFiles*(validatorsDir, keystoreDir, keystoreFile,
|
|
|
|
encodedStorage: string
|
|
|
|
): Result[void, KeystoreGenerationError] =
|
|
|
|
var
|
|
|
|
success = false # becomes true when everything is created successfully
|
|
|
|
|
|
|
|
# validatorsDir:
|
|
|
|
let validatorsDirExisted: bool = dirExists(validatorsDir)
|
|
|
|
if not(validatorsDirExisted):
|
|
|
|
? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir)
|
|
|
|
defer:
|
|
|
|
if not (success or validatorsDirExisted):
|
|
|
|
discard io2.removeDir(validatorsDir)
|
|
|
|
|
|
|
|
# keystoreDir:
|
|
|
|
? secureCreatePath(keystoreDir).mapErrTo(FailedToCreateKeystoreDir)
|
|
|
|
defer:
|
|
|
|
if not success:
|
|
|
|
discard io2.removeDir(keystoreDir)
|
|
|
|
|
|
|
|
# keystoreFile:
|
|
|
|
? secureWriteFile(keystoreFile,
|
|
|
|
encodedStorage).mapErrTo(FailedToCreateKeystoreFile)
|
|
|
|
success = true
|
|
|
|
ok()
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
proc saveKeystore*(rng: var BrHmacDrbgContext,
|
|
|
|
validatorsDir, secretsDir: string,
|
|
|
|
signingKey: ValidatorPrivKey,
|
|
|
|
signingPubKey: CookedPubKey,
|
|
|
|
signingKeyPath: KeyPath,
|
|
|
|
password: string,
|
|
|
|
mode = Secure): Result[void, KeystoreGenerationError] =
|
2020-08-02 17:26:57 +00:00
|
|
|
let
|
2021-12-22 12:37:31 +00:00
|
|
|
keypass = KeystorePass.init(password)
|
|
|
|
keyName = "0x" & signingPubKey.toHex()
|
|
|
|
keystoreDir = validatorsDir / keyName
|
2022-02-07 20:36:09 +00:00
|
|
|
keystoreFile = keystoreDir / KeystoreFileName
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2022-03-24 00:38:48 +00:00
|
|
|
if dirExists(keystoreDir):
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreDir,
|
|
|
|
error: "Keystore directory already exists"))
|
2022-03-24 00:38:48 +00:00
|
|
|
if fileExists(keystoreFile):
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreFile,
|
|
|
|
error: "Keystore file already exists"))
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
let keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
|
2021-12-22 12:37:31 +00:00
|
|
|
keypass, signingKeyPath,
|
|
|
|
mode = mode)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
var encodedStorage: string
|
|
|
|
try:
|
|
|
|
encodedStorage = Json.encode(keyStore)
|
|
|
|
except SerializationError as e:
|
|
|
|
error "Could not serialize keystorage", key_path = keystoreFile
|
|
|
|
return err(KeystoreGenerationError(
|
|
|
|
kind: FailedToCreateKeystoreFile, error: e.msg))
|
|
|
|
|
|
|
|
? createValidatorFiles(secretsDir, validatorsDir,
|
|
|
|
keystoreDir,
|
|
|
|
secretsDir / keyName, keypass.str,
|
|
|
|
keystoreFile, encodedStorage)
|
|
|
|
|
|
|
|
ok()
|
|
|
|
|
|
|
|
proc saveKeystore*(validatorsDir: string,
|
|
|
|
publicKey: ValidatorPubKey, url: HttpHostUri,
|
|
|
|
version = 1'u64,
|
|
|
|
flags: set[RemoteKeystoreFlag] = {},
|
|
|
|
remoteType = RemoteSignerType.Web3Signer,
|
|
|
|
desc = ""): Result[void, KeystoreGenerationError] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
let
|
|
|
|
keyName = "0x" & publicKey.toHex()
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / RemoteKeystoreFileName
|
|
|
|
keystoreDesc = if len(desc) == 0: none[string]() else: some(desc)
|
|
|
|
keyStore = RemoteKeystore(
|
|
|
|
version: version, description: keystoreDesc, remoteType: remoteType,
|
|
|
|
pubkey: publicKey, remote: url, flags: flags
|
|
|
|
)
|
|
|
|
|
2022-03-24 00:38:48 +00:00
|
|
|
if dirExists(keystoreDir):
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreDir,
|
|
|
|
error: "Keystore directory already exists"))
|
2022-03-24 00:38:48 +00:00
|
|
|
if fileExists(keystoreFile):
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreFile,
|
|
|
|
error: "Keystore file already exists"))
|
|
|
|
|
|
|
|
let encodedStorage =
|
2020-08-27 13:24:30 +00:00
|
|
|
try:
|
2022-02-07 20:36:09 +00:00
|
|
|
Json.encode(keyStore)
|
|
|
|
except SerializationError as exc:
|
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(
|
2022-02-07 20:36:09 +00:00
|
|
|
kind: FailedToCreateKeystoreFile, error: exc.msg))
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
? createValidatorFiles(validatorsDir, keystoreDir, keystoreFile,
|
|
|
|
encodedStorage)
|
2020-08-02 17:26:57 +00:00
|
|
|
ok()
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc saveKeystore*(conf: AnyConf, publicKey: ValidatorPubKey, url: HttpHostUri,
|
|
|
|
version = 1'u64,
|
|
|
|
flags: set[RemoteKeystoreFlag] = {},
|
|
|
|
remoteType = RemoteSignerType.Web3Signer,
|
|
|
|
desc = ""): Result[void, KeystoreGenerationError] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
saveKeystore(conf.validatorsDir(), publicKey, url, version, flags,
|
|
|
|
remoteType, desc)
|
|
|
|
|
|
|
|
proc importKeystore*(pool: var ValidatorPool, conf: AnyConf,
|
|
|
|
keystore: RemoteKeystore): ImportResult[KeystoreData] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
let
|
|
|
|
publicKey = keystore.pubkey
|
|
|
|
keyName = "0x" & publicKey.toHex()
|
|
|
|
validatorsDir = conf.validatorsDir()
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / RemoteKeystoreFileName
|
|
|
|
|
|
|
|
# We check `publicKey`.
|
|
|
|
let cookedKey =
|
|
|
|
block:
|
|
|
|
let res = publicKey.load()
|
|
|
|
if res.isNone():
|
|
|
|
return err(
|
|
|
|
AddValidatorFailure.init(AddValidatorStatus.failed,
|
|
|
|
"Invalid validator's public key"))
|
|
|
|
res.get()
|
|
|
|
|
|
|
|
# We check `publicKey` in memory storage first.
|
|
|
|
if publicKey in pool:
|
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts))
|
|
|
|
|
|
|
|
# We check `publicKey` in filesystem.
|
|
|
|
if existsKeystore(keystoreDir, {KeystoreKind.Local, KeystoreKind.Remote}):
|
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts))
|
|
|
|
|
|
|
|
let res = saveKeystore(conf, publicKey, keystore.remote)
|
|
|
|
if res.isErr():
|
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.failed,
|
|
|
|
$res.error()))
|
|
|
|
ok(KeystoreData.init(cookedKey, keystore.remote))
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
proc importKeystore*(pool: var ValidatorPool,
|
|
|
|
rng: var BrHmacDrbgContext,
|
|
|
|
conf: AnyConf, keystore: Keystore,
|
2022-02-07 20:36:09 +00:00
|
|
|
password: string): ImportResult[KeystoreData] {.
|
|
|
|
raises: [Defect].} =
|
2021-12-22 12:37:31 +00:00
|
|
|
let keypass = KeystorePass.init(password)
|
|
|
|
let privateKey =
|
|
|
|
block:
|
|
|
|
let res = decryptKeystore(keystore, keypass)
|
|
|
|
if res.isOk():
|
|
|
|
res.get()
|
|
|
|
else:
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(
|
|
|
|
AddValidatorFailure.init(AddValidatorStatus.failed, res.error()))
|
|
|
|
let
|
|
|
|
publicKey = privateKey.toPubKey()
|
|
|
|
keyName = "0x" & publicKey.toHex()
|
|
|
|
validatorsDir = conf.validatorsDir()
|
|
|
|
secretsDir = conf.secretsDir()
|
|
|
|
secretFile = secretsDir / keyName
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / KeystoreFileName
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
# We check `publicKey` in memory storage first.
|
|
|
|
if publicKey.toPubKey() in pool:
|
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts))
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
# We check `publicKey` in filesystem.
|
|
|
|
if existsKeystore(keystoreDir, {KeystoreKind.Local, KeystoreKind.Remote}):
|
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts))
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
let res = saveKeystore(rng, validatorsDir, secretsDir,
|
|
|
|
privateKey, publicKey, keystore.path, password)
|
2021-12-22 12:37:31 +00:00
|
|
|
|
|
|
|
if res.isErr():
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.failed,
|
|
|
|
$res.error()))
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
ok(KeystoreData.init(privateKey, keystore))
|
2021-12-22 12:37:31 +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
|
|
|
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,
|
2021-12-22 12:37:31 +00:00
|
|
|
secretsDir: string,
|
2022-02-07 20:36:09 +00:00
|
|
|
mode = Secure): 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
|
2021-12-22 12:37:31 +00:00
|
|
|
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
|
|
|
|
defer: burnMem(password)
|
2020-08-02 17:26:57 +00:00
|
|
|
? saveKeystore(rng, validatorsDir, secretsDir,
|
2020-10-19 19:02:48 +00:00
|
|
|
derivedKey, signingPubKey,
|
2021-12-22 12:37:31 +00:00
|
|
|
makeKeyPath(validatorIdx, signingKeyKind), password.str,
|
|
|
|
mode)
|
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)
|
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
proc readPasswordInput(prompt: string, password: var string): 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) =
|
2022-03-24 21:44:34 +00:00
|
|
|
var password: string # TODO consider using a SecretString type
|
2020-08-02 17:26:57 +00:00
|
|
|
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
|
2021-11-30 01:20:21 +00:00
|
|
|
of DecryptionStatus.Success:
|
2020-10-09 16:41:53 +00:00
|
|
|
let privKey = ValidatorPrivKey.fromRaw(secret)
|
|
|
|
if privKey.isOk:
|
2021-12-22 12:37:31 +00:00
|
|
|
let pubkey = privKey.value.toPubKey
|
|
|
|
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
|
|
|
|
defer: burnMem(password)
|
2020-10-09 16:41:53 +00:00
|
|
|
let status = saveKeystore(rng, validatorsDir, secretsDir,
|
2021-12-22 12:37:31 +00:00
|
|
|
privKey.value, pubkey,
|
|
|
|
keystore.path, password.str)
|
2020-10-09 16:41:53 +00:00
|
|
|
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
|
2021-11-30 01:20:21 +00:00
|
|
|
of DecryptionStatus.InvalidKeystore:
|
2020-10-15 18:45:27 +00:00
|
|
|
warn "Invalid keystore", file
|
|
|
|
break
|
2021-11-30 01:20:21 +00:00
|
|
|
of DecryptionStatus.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. " &
|
2022-03-14 12:50:23 +00:00
|
|
|
"If you don't want to set a recovery password, just press ENTER."
|
2020-10-19 19:02:48 +00:00
|
|
|
|
|
|
|
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
|
2022-03-24 21:44:34 +00:00
|
|
|
enteredMnemonic: string
|
2020-08-21 19:36:42 +00:00
|
|
|
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
|
2021-11-30 01:20:21 +00:00
|
|
|
of DecryptionStatus.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)
|