2021-03-26 06:52:01 +00:00
|
|
|
# beacon_chain
|
2023-01-20 14:14:37 +00:00
|
|
|
# Copyright (c) 2018-2023 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.
|
|
|
|
|
2023-01-20 14:14:37 +00:00
|
|
|
{.push raises: [].}
|
2021-03-26 06:52:01 +00:00
|
|
|
|
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,
|
2022-06-21 08:29:16 +00:00
|
|
|
bearssl/rand,
|
|
|
|
serialization, blscurve, eth/common/eth_types, eth/keys, confutils,
|
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,
|
2022-08-19 10:30:07 +00:00
|
|
|
".."/[conf, filepath, beacon_clock],
|
2021-10-19 14:09:26 +00:00
|
|
|
".."/networking/network_metadata,
|
|
|
|
./validator_pool
|
2018-12-19 12:58:53 +00:00
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
export
|
2022-06-21 08:29:16 +00:00
|
|
|
keystore, validator_pool, crypto, rand
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-10-15 12:50:21 +00:00
|
|
|
when defined(windows):
|
|
|
|
import stew/[windows/acl]
|
|
|
|
|
2022-07-06 10:33:02 +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"
|
2022-07-13 14:45:04 +00:00
|
|
|
FeeRecipientFilename* = "suggested_fee_recipient.hex"
|
2023-02-15 15:10:31 +00:00
|
|
|
GasLimitFilename* = "suggested_gas_limit.json"
|
2022-02-07 20:36:09 +00:00
|
|
|
KeyNameSize* = 98 # 0x + hexadecimal key representation 96 characters.
|
2022-08-07 21:53:20 +00:00
|
|
|
MaxKeystoreFileSize* = 65536
|
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
|
|
|
KmResult*[T] = Result[T, cstring]
|
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
AnyKeystore* = RemoteKeystore | Keystore
|
|
|
|
|
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
|
|
|
|
2022-11-20 13:55:43 +00:00
|
|
|
ValidatorPubKeyToDataFn* =
|
|
|
|
proc (pubkey: ValidatorPubKey): Opt[ValidatorAndIndex]
|
2022-08-19 10:30:07 +00:00
|
|
|
{.raises: [Defect], gcsafe.}
|
|
|
|
|
|
|
|
KeymanagerHost* = object
|
|
|
|
validatorPool*: ref ValidatorPool
|
|
|
|
rng*: ref HmacDrbgContext
|
|
|
|
keymanagerToken*: string
|
|
|
|
validatorsDir*: string
|
|
|
|
secretsDir*: string
|
2023-05-17 04:56:37 +00:00
|
|
|
defaultFeeRecipient*: Opt[Eth1Address]
|
2023-02-15 15:10:31 +00:00
|
|
|
defaultGasLimit*: uint64
|
2022-11-20 13:55:43 +00:00
|
|
|
getValidatorAndIdxFn*: ValidatorPubKeyToDataFn
|
2022-08-19 10:30:07 +00:00
|
|
|
getBeaconTimeFn*: GetBeaconTimeFn
|
|
|
|
|
2023-04-25 06:44:01 +00:00
|
|
|
MultipleKeystoresDecryptor* = object
|
|
|
|
previouslyUsedPassword*: string
|
|
|
|
|
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)
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func dispose*(decryptor: var MultipleKeystoresDecryptor) =
|
2023-04-25 06:44:01 +00:00
|
|
|
burnMem(decryptor.previouslyUsedPassword)
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
func init*(T: type KeymanagerHost,
|
|
|
|
validatorPool: ref ValidatorPool,
|
|
|
|
rng: ref HmacDrbgContext,
|
|
|
|
keymanagerToken: string,
|
|
|
|
validatorsDir: string,
|
|
|
|
secretsDir: string,
|
2023-05-17 04:56:37 +00:00
|
|
|
defaultFeeRecipient: Opt[Eth1Address],
|
2023-02-15 15:10:31 +00:00
|
|
|
defaultGasLimit: uint64,
|
2022-11-20 13:55:43 +00:00
|
|
|
getValidatorAndIdxFn: ValidatorPubKeyToDataFn,
|
2022-08-19 10:30:07 +00:00
|
|
|
getBeaconTimeFn: GetBeaconTimeFn): T =
|
|
|
|
T(validatorPool: validatorPool,
|
|
|
|
rng: rng,
|
|
|
|
keymanagerToken: keymanagerToken,
|
|
|
|
validatorsDir: validatorsDir,
|
|
|
|
secretsDir: secretsDir,
|
|
|
|
defaultFeeRecipient: defaultFeeRecipient,
|
2023-02-15 15:10:31 +00:00
|
|
|
defaultGasLimit: defaultGasLimit,
|
2022-11-20 13:55:43 +00:00
|
|
|
getValidatorAndIdxFn: getValidatorAndIdxFn,
|
2022-08-19 10:30:07 +00:00
|
|
|
getBeaconTimeFn: getBeaconTimeFn)
|
|
|
|
|
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,
|
2022-08-07 21:53:20 +00:00
|
|
|
keystore: Keystore, handle: FileLockHandle): T {.raises: [Defect].} =
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreData(
|
|
|
|
kind: KeystoreKind.Local,
|
2021-10-04 19:08:31 +00:00
|
|
|
privateKey: privateKey,
|
2022-08-19 10:30:07 +00:00
|
|
|
description: keystore.description,
|
2021-12-22 12:37:31 +00:00
|
|
|
path: keystore.path,
|
|
|
|
uuid: keystore.uuid,
|
2022-08-07 21:53:20 +00:00
|
|
|
handle: handle,
|
2021-12-22 12:37:31 +00:00
|
|
|
version: uint64(keystore.version),
|
|
|
|
pubkey: privateKey.toPubKey().toPubKey()
|
2021-10-04 19:08:31 +00:00
|
|
|
)
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func init(T: type KeystoreData, keystore: RemoteKeystore,
|
|
|
|
handle: FileLockHandle): Result[T, cstring] {.raises: [Defect].} =
|
2023-01-11 12:29:21 +00:00
|
|
|
let cookedKey = keystore.pubkey.load().valueOr:
|
2021-11-30 01:20:21 +00:00
|
|
|
return err("Invalid validator's public key")
|
2023-01-11 12:29:21 +00:00
|
|
|
|
2023-05-09 08:16:43 +00:00
|
|
|
ok case keystore.remoteType
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
KeystoreData(
|
|
|
|
kind: KeystoreKind.Remote,
|
|
|
|
handle: handle,
|
|
|
|
pubkey: cookedKey.toPubKey,
|
|
|
|
description: keystore.description,
|
|
|
|
version: keystore.version,
|
|
|
|
remotes: keystore.remotes,
|
|
|
|
threshold: keystore.threshold,
|
|
|
|
remoteType: RemoteSignerType.Web3Signer)
|
|
|
|
of RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
KeystoreData(
|
|
|
|
kind: KeystoreKind.Remote,
|
|
|
|
handle: handle,
|
|
|
|
pubkey: cookedKey.toPubKey,
|
|
|
|
description: keystore.description,
|
|
|
|
version: keystore.version,
|
|
|
|
remotes: keystore.remotes,
|
|
|
|
threshold: keystore.threshold,
|
|
|
|
remoteType: RemoteSignerType.VerifyingWeb3Signer,
|
|
|
|
provenBlockProperties: keystore.provenBlockProperties)
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func init(T: type KeystoreData, cookedKey: CookedPubKey,
|
|
|
|
remotes: seq[RemoteSignerInfo], threshold: uint32,
|
|
|
|
handle: FileLockHandle): T =
|
2022-02-07 20:36:09 +00:00
|
|
|
KeystoreData(
|
|
|
|
kind: KeystoreKind.Remote,
|
2022-08-07 21:53:20 +00:00
|
|
|
handle: handle,
|
2022-02-07 20:36:09 +00:00
|
|
|
pubkey: cookedKey.toPubKey(),
|
2022-05-10 00:32:12 +00:00
|
|
|
version: 2'u64,
|
|
|
|
remotes: remotes,
|
2022-08-07 21:53:20 +00:00
|
|
|
threshold: threshold,
|
2022-02-07 20:36:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func init(T: type AddValidatorFailure, status: AddValidatorStatus,
|
|
|
|
msg = ""): AddValidatorFailure {.raises: [Defect].} =
|
|
|
|
AddValidatorFailure(status: status, message: msg)
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func toKeystoreKind(kind: ValidatorKind): KeystoreKind {.raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
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
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc checkSensitivePathPermissions(dirFilePath: string): bool =
|
2021-10-04 19:08:31 +00:00
|
|
|
## 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
|
2022-07-18 19:17:11 +00:00
|
|
|
elif cstring(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")
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc loadSecretFile(path: string): KsResult[KeystorePass] {.
|
2021-10-04 19:08:31 +00:00
|
|
|
raises: [Defect].} =
|
2022-08-07 21:53:20 +00:00
|
|
|
let res = readAllChars(path)
|
|
|
|
if res.isErr():
|
|
|
|
return err(ioErrorMsg(res.error()))
|
|
|
|
ok(KeystorePass.init(res.get()))
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
proc loadRemoteKeystoreImpl(validatorsDir,
|
2023-02-16 17:25:48 +00:00
|
|
|
keyName: string): Opt[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
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-02-07 20:36:09 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
let handle =
|
|
|
|
block:
|
|
|
|
let res = openLockedFile(keystorePath)
|
|
|
|
if res.isErr():
|
|
|
|
error "Unable to lock keystore file", key_path = keystorePath,
|
|
|
|
error_msg = ioErrorMsg(res.error())
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-08-07 21:53:20 +00:00
|
|
|
res.get()
|
|
|
|
|
|
|
|
var success = false
|
|
|
|
defer:
|
|
|
|
if not(success):
|
|
|
|
discard handle.closeLockedFile()
|
|
|
|
|
|
|
|
let keystore =
|
2021-11-30 01:20:21 +00:00
|
|
|
block:
|
2022-08-07 21:53:20 +00:00
|
|
|
let gres = handle.getData(MaxKeystoreFileSize)
|
|
|
|
if gres.isErr():
|
|
|
|
error "Could not read remote keystore file", key_path = keystorePath,
|
|
|
|
error_msg = ioErrorMsg(gres.error())
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-08-07 21:53:20 +00:00
|
|
|
let buffer = gres.get()
|
|
|
|
let data =
|
2021-11-30 01:20:21 +00:00
|
|
|
try:
|
2022-08-19 10:30:07 +00:00
|
|
|
parseRemoteKeystore(buffer)
|
2021-11-30 01:20:21 +00:00
|
|
|
except SerializationError as e:
|
2022-08-07 21:53:20 +00:00
|
|
|
error "Invalid remote keystore file", key_path = keystorePath,
|
|
|
|
error_msg = e.formatMsg(keystorePath)
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-08-07 21:53:20 +00:00
|
|
|
let kres = KeystoreData.init(data, handle)
|
|
|
|
if kres.isErr():
|
|
|
|
error "Invalid remote keystore file", key_path = keystorePath,
|
|
|
|
error_msg = kres.error()
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-08-07 21:53:20 +00:00
|
|
|
kres.get()
|
|
|
|
|
|
|
|
success = true
|
2023-02-16 17:25:48 +00:00
|
|
|
Opt.some(keystore)
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
|
2023-02-16 17:25:48 +00:00
|
|
|
nonInteractive: bool,
|
|
|
|
cache: KeystoreCacheRef): Opt[KeystoreData] =
|
2020-06-01 19:48:20 +00:00
|
|
|
let
|
2021-10-04 19:08:31 +00:00
|
|
|
keystorePath = validatorsDir / keyName / KeystoreFileName
|
2022-08-07 21:53:20 +00:00
|
|
|
passphrasePath = secretsDir / keyName
|
|
|
|
handle =
|
2021-10-04 19:08:31 +00:00
|
|
|
block:
|
2022-08-07 21:53:20 +00:00
|
|
|
let res = openLockedFile(keystorePath)
|
2021-10-04 19:08:31 +00:00
|
|
|
if res.isErr():
|
2022-08-07 21:53:20 +00:00
|
|
|
error "Unable to lock keystore file", key_path = keystorePath,
|
|
|
|
error_msg = ioErrorMsg(res.error())
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2021-10-04 19:08:31 +00:00
|
|
|
res.get()
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
var success = false
|
|
|
|
defer:
|
|
|
|
if not(success):
|
|
|
|
discard handle.closeLockedFile()
|
|
|
|
|
|
|
|
let
|
|
|
|
keystore =
|
|
|
|
block:
|
|
|
|
let gres = handle.getData(MaxKeystoreFileSize)
|
|
|
|
if gres.isErr():
|
|
|
|
error "Could not read local keystore file", key_path = keystorePath,
|
|
|
|
error_msg = ioErrorMsg(gres.error())
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-08-07 21:53:20 +00:00
|
|
|
let buffer = gres.get()
|
|
|
|
let data =
|
|
|
|
try:
|
2022-08-19 10:30:07 +00:00
|
|
|
parseKeystore(buffer)
|
2022-08-07 21:53:20 +00:00
|
|
|
except SerializationError as e:
|
|
|
|
error "Invalid local keystore file", key_path = keystorePath,
|
|
|
|
error_msg = e.formatMsg(keystorePath)
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2022-08-07 21:53:20 +00:00
|
|
|
data
|
|
|
|
|
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
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2020-08-27 13:24:30 +00:00
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
let passphrase =
|
|
|
|
block:
|
|
|
|
let res = loadSecretFile(passphrasePath)
|
|
|
|
if res.isErr():
|
2022-08-07 21:53:20 +00:00
|
|
|
error "Failed to read passphrase file", error_msg = res.error(),
|
2021-10-04 19:08:31 +00:00
|
|
|
path = passphrasePath
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2021-10-04 19:08:31 +00:00
|
|
|
res.get()
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
let res = decryptKeystore(keystore, passphrase, cache)
|
2021-10-04 19:08:31 +00:00
|
|
|
if res.isOk():
|
2022-08-07 21:53:20 +00:00
|
|
|
success = true
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.some(KeystoreData.init(res.get(), keystore, handle))
|
2020-06-03 11:52:36 +00:00
|
|
|
else:
|
2022-08-07 21:53:20 +00:00
|
|
|
error "Failed to decrypt keystore", key_path = keystorePath,
|
|
|
|
secure_path = passphrasePath
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
if nonInteractive:
|
2022-08-07 21:53:20 +00:00
|
|
|
error "Unable to load validator key store. Please ensure matching " &
|
|
|
|
"passphrase exists in the secrets dir", key_path = keystorePath,
|
|
|
|
key_name = keyName, validatorsDir, secretsDir = secretsDir
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
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] =
|
2023-02-16 17:25:48 +00:00
|
|
|
let decrypted = decryptKeystore(keystore, KeystorePass.init password,
|
|
|
|
cache)
|
2020-09-29 16:49:09 +00:00
|
|
|
if decrypted.isErr():
|
2022-08-07 21:53:20 +00:00
|
|
|
error "Keystore decryption failed. Please try again",
|
|
|
|
keystore_path = keystorePath
|
2020-09-29 16:49:09 +00:00
|
|
|
decrypted
|
|
|
|
)
|
2020-10-02 15:46:05 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
if res.isErr():
|
2023-02-16 17:25:48 +00:00
|
|
|
return Opt.none(KeystoreData)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
success = true
|
2023-02-16 17:25:48 +00:00
|
|
|
Opt.some(KeystoreData.init(res.get(), keystore, handle))
|
2022-08-07 21:53:20 +00:00
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
|
2023-02-16 17:25:48 +00:00
|
|
|
nonInteractive: bool,
|
|
|
|
cache: KeystoreCacheRef): Opt[KeystoreData] =
|
2021-11-30 01:20:21 +00:00
|
|
|
let
|
|
|
|
keystorePath = validatorsDir / keyName
|
|
|
|
localKeystorePath = keystorePath / KeystoreFileName
|
|
|
|
remoteKeystorePath = keystorePath / RemoteKeystoreFileName
|
|
|
|
|
|
|
|
if fileExists(localKeystorePath):
|
2023-02-16 17:25:48 +00:00
|
|
|
loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive,
|
|
|
|
cache)
|
2021-11-30 01:20:21 +00:00
|
|
|
elif fileExists(remoteKeystorePath):
|
|
|
|
loadRemoteKeystoreImpl(validatorsDir, keyName)
|
|
|
|
else:
|
|
|
|
error "Unable to find any keystore files", keystorePath
|
2023-02-16 17:25:48 +00:00
|
|
|
Opt.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-07-13 14:45:04 +00:00
|
|
|
func fsName(pubkey: ValidatorPubKey|CookedPubKey): string =
|
|
|
|
"0x" & pubkey.toHex()
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
proc removeValidator*(pool: var ValidatorPool,
|
|
|
|
validatorsDir, secretsDir: string,
|
2022-02-07 20:36:09 +00:00
|
|
|
publicKey: ValidatorPubKey,
|
2022-08-07 21:53:20 +00:00
|
|
|
kind: KeystoreKind): KmResult[RemoveValidatorStatus] {.
|
|
|
|
raises: [Defect].} =
|
2023-02-20 11:28:56 +00:00
|
|
|
let validator = pool.getValidator(publicKey).valueOr:
|
2022-02-07 20:36:09 +00:00
|
|
|
return ok(RemoveValidatorStatus.notFound)
|
|
|
|
if validator.kind.toKeystoreKind() != kind:
|
|
|
|
return ok(RemoveValidatorStatus.notFound)
|
2022-08-07 21:53:20 +00:00
|
|
|
let cres = validator.data.handle.closeLockedFile()
|
|
|
|
if cres.isErr():
|
|
|
|
return err("Could not unlock validator keystore file")
|
2022-08-19 10:30:07 +00:00
|
|
|
let res = removeValidatorFiles(validatorsDir, secretsDir, publicKey.fsName, 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
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func checkKeyName(keyName: string): bool =
|
2022-02-07 20:36:09 +00:00
|
|
|
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
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc existsKeystore(keystoreDir: string, keyKind: KeystoreKind): bool {.
|
2022-02-07 20:36:09 +00:00
|
|
|
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
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc existsKeystore(keystoreDir: string,
|
|
|
|
keysMask: set[KeystoreKind]): bool {.raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
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
|
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
iterator listLoadableKeys*(validatorsDir, secretsDir: string,
|
|
|
|
keysMask: set[KeystoreKind]): CookedPubKey =
|
|
|
|
try:
|
|
|
|
for kind, file in walkDir(validatorsDir):
|
|
|
|
if kind == pcDir:
|
|
|
|
let
|
|
|
|
keyName = splitFile(file).name
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
|
|
|
|
if not(checkKeyName(keyName)):
|
|
|
|
# Skip folders which name do not satisfy "0x[a-fA-F0-9]{96, 96}".
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not(existsKeystore(keystoreDir, keysMask)):
|
|
|
|
# Skip folder which do not satisfy `keysMask`.
|
|
|
|
continue
|
|
|
|
|
|
|
|
let kres = ValidatorPubKey.fromHex(keyName)
|
|
|
|
if kres.isErr():
|
|
|
|
# Skip folders which could not be decoded to ValidatorPubKey.
|
|
|
|
continue
|
|
|
|
let publicKey = kres.get()
|
|
|
|
|
2023-01-11 12:29:21 +00:00
|
|
|
let cres = publicKey.load().valueOr:
|
2022-08-07 21:53:20 +00:00
|
|
|
# Skip folders which has invalid ValidatorPubKey
|
|
|
|
# (point is not on curve).
|
|
|
|
continue
|
|
|
|
|
2023-01-11 12:29:21 +00:00
|
|
|
yield cres
|
2022-08-07 21:53:20 +00:00
|
|
|
|
|
|
|
except OSError as err:
|
|
|
|
error "Validator keystores directory not accessible",
|
|
|
|
path = validatorsDir, err = err.msg
|
|
|
|
quit 1
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
iterator listLoadableKeystores*(validatorsDir, secretsDir: string,
|
|
|
|
nonInteractive: bool,
|
2023-02-16 17:25:48 +00:00
|
|
|
keysMask: set[KeystoreKind],
|
|
|
|
cache: KeystoreCacheRef): 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,
|
2023-02-16 17:25:48 +00:00
|
|
|
nonInteractive, cache).valueOr:
|
|
|
|
fatal "Unable to load keystore", keystore = file
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
yield keystore
|
2021-12-22 12:37:31 +00:00
|
|
|
|
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
|
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
iterator listLoadableKeystores*(config: AnyConf,
|
|
|
|
cache: KeystoreCacheRef): KeystoreData =
|
2021-12-22 12:37:31 +00:00
|
|
|
for el in listLoadableKeystores(config.validatorsDir(),
|
|
|
|
config.secretsDir(),
|
2022-02-07 20:36:09 +00:00
|
|
|
config.nonInteractive,
|
2023-02-16 17:25:48 +00:00
|
|
|
{KeystoreKind.Local, KeystoreKind.Remote},
|
|
|
|
cache):
|
2021-12-22 12:37:31 +00:00
|
|
|
yield el
|
|
|
|
|
2022-09-17 05:30:07 +00:00
|
|
|
type
|
2023-02-15 15:10:31 +00:00
|
|
|
ValidatorConfigFileStatus* = enum
|
2022-09-17 05:30:07 +00:00
|
|
|
noSuchValidator
|
2023-02-15 15:10:31 +00:00
|
|
|
malformedConfigFile
|
2022-09-17 05:30:07 +00:00
|
|
|
|
|
|
|
func validatorKeystoreDir(
|
|
|
|
validatorsDir: string, pubkey: ValidatorPubKey): string =
|
|
|
|
validatorsDir / pubkey.fsName
|
|
|
|
|
|
|
|
func feeRecipientPath(validatorsDir: string,
|
|
|
|
pubkey: ValidatorPubKey): string =
|
|
|
|
validatorsDir.validatorKeystoreDir(pubkey) / FeeRecipientFilename
|
|
|
|
|
2023-02-15 15:10:31 +00:00
|
|
|
func gasLimitPath(validatorsDir: string,
|
|
|
|
pubkey: ValidatorPubKey): string =
|
|
|
|
validatorsDir.validatorKeystoreDir(pubkey) / GasLimitFilename
|
|
|
|
|
2022-09-17 05:30:07 +00:00
|
|
|
proc getSuggestedFeeRecipient*(
|
2023-05-17 04:56:37 +00:00
|
|
|
validatorsDir: string, pubkey: ValidatorPubKey,
|
|
|
|
defaultFeeRecipient: Eth1Address):
|
|
|
|
Result[Eth1Address, ValidatorConfigFileStatus] =
|
2022-09-17 05:30:07 +00:00
|
|
|
# In this particular case, an error might be by design. If the file exists,
|
|
|
|
# but doesn't load or parse that's a more urgent matter to fix. Many people
|
|
|
|
# people might prefer, however, not to override their default suggested fee
|
|
|
|
# recipients per validator, so don't warn very loudly, if at all.
|
|
|
|
if not dirExists(validatorsDir.validatorKeystoreDir(pubkey)):
|
|
|
|
return err noSuchValidator
|
|
|
|
|
|
|
|
let feeRecipientPath = validatorsDir.feeRecipientPath(pubkey)
|
|
|
|
if not fileExists(feeRecipientPath):
|
|
|
|
return ok defaultFeeRecipient
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Avoid being overly flexible initially. Trailing whitespace is common
|
|
|
|
# enough it probably should be allowed, but it is reasonable to simply
|
|
|
|
# disallow the mostly-pointless flexibility of leading whitespace.
|
|
|
|
ok Eth1Address.fromHex(strutils.strip(
|
|
|
|
readFile(feeRecipientPath), leading = false, trailing = true))
|
|
|
|
except CatchableError as exc:
|
|
|
|
# Because the nonexistent validator case was already checked, any failure
|
|
|
|
# at this point is serious enough to alert the user.
|
2023-02-15 15:10:31 +00:00
|
|
|
warn "Failed to load fee recipient file; falling back to default fee recipient",
|
|
|
|
feeRecipientPath, defaultFeeRecipient,
|
|
|
|
err = exc.msg
|
|
|
|
err malformedConfigFile
|
|
|
|
|
|
|
|
proc getSuggestedGasLimit*(
|
|
|
|
validatorsDir: string,
|
|
|
|
pubkey: ValidatorPubKey,
|
|
|
|
defaultGasLimit: uint64): Result[uint64, ValidatorConfigFileStatus] =
|
|
|
|
# In this particular case, an error might be by design. If the file exists,
|
|
|
|
# but doesn't load or parse that's a more urgent matter to fix. Many people
|
|
|
|
# people might prefer, however, not to override their default suggested gas
|
|
|
|
# limit per validator, so don't warn very loudly, if at all.
|
|
|
|
if not dirExists(validatorsDir.validatorKeystoreDir(pubkey)):
|
|
|
|
return err noSuchValidator
|
|
|
|
|
|
|
|
let gasLimitPath = validatorsDir.gasLimitPath(pubkey)
|
|
|
|
if not fileExists(gasLimitPath):
|
|
|
|
return ok defaultGasLimit
|
|
|
|
try:
|
|
|
|
ok parseBiggestUInt(strutils.strip(
|
|
|
|
readFile(gasLimitPath), leading = false, trailing = true))
|
|
|
|
except SerializationError as e:
|
|
|
|
warn "Invalid local gas limit file", gasLimitPath,
|
|
|
|
err= e.formatMsg(gasLimitPath)
|
|
|
|
err malformedConfigFile
|
|
|
|
except CatchableError as exc:
|
|
|
|
warn "Failed to load gas limit file; falling back to default gas limit",
|
|
|
|
gasLimitPath, defaultGasLimit,
|
2022-09-17 05:30:07 +00:00
|
|
|
err = exc.msg
|
2023-02-15 15:10:31 +00:00
|
|
|
err malformedConfigFile
|
2022-09-17 05:30:07 +00:00
|
|
|
|
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
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func 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,
|
2022-07-13 21:26:16 +00:00
|
|
|
insecurePwd: Option[string]): Opt[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:
|
2022-09-19 22:09:56 +00:00
|
|
|
Json.loadFile(keystorePath, NetKeystore,
|
|
|
|
requireAllFields = true,
|
|
|
|
allowUnknownFields = true)
|
2020-08-24 16:06:41 +00:00
|
|
|
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:
|
2022-07-13 21:26:16 +00:00
|
|
|
return ok(decrypted.get())
|
2020-08-24 16:06:41 +00:00
|
|
|
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():
|
2022-07-13 21:26:16 +00:00
|
|
|
ok(res.get())
|
2020-09-29 16:49:09 +00:00
|
|
|
else:
|
|
|
|
return
|
2020-08-24 16:06:41 +00:00
|
|
|
|
2022-06-21 08:29:16 +00:00
|
|
|
proc saveNetKeystore*(rng: var HmacDrbgContext, 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-08-07 21:53:20 +00:00
|
|
|
proc createLocalValidatorFiles*(
|
|
|
|
secretsDir, validatorsDir, keystoreDir,
|
|
|
|
secretFile, passwordAsString, keystoreFile,
|
|
|
|
encodedStorage: string
|
|
|
|
): Result[void, KeystoreGenerationError] {.raises: [Defect].} =
|
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:
|
2022-08-07 21:53:20 +00:00
|
|
|
if not (success or secretsDirExisted):
|
2021-12-22 12:37:31 +00:00
|
|
|
discard io2.removeDir(secretsDir)
|
|
|
|
|
|
|
|
# validatorsDir:
|
|
|
|
let validatorsDirExisted: bool = dirExists(validatorsDir)
|
|
|
|
if not(validatorsDirExisted):
|
|
|
|
? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir)
|
|
|
|
defer:
|
2022-08-07 21:53:20 +00:00
|
|
|
if not (success or validatorsDirExisted):
|
2021-12-22 12:37:31 +00:00
|
|
|
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()
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc createLockedLocalValidatorFiles(
|
2022-08-07 21:53:20 +00:00
|
|
|
secretsDir, validatorsDir, keystoreDir,
|
|
|
|
secretFile, passwordAsString, keystoreFile,
|
|
|
|
encodedStorage: string
|
|
|
|
): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} =
|
|
|
|
|
|
|
|
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:
|
|
|
|
? secureWriteFile(secretFile,
|
|
|
|
passwordAsString).mapErrTo(FailedToCreateSecretFile)
|
|
|
|
defer:
|
|
|
|
if not success:
|
|
|
|
discard io2.removeFile(secretFile)
|
|
|
|
|
|
|
|
# keystoreFile:
|
|
|
|
let lock =
|
|
|
|
? secureWriteLockedFile(keystoreFile,
|
|
|
|
encodedStorage).mapErrTo(FailedToCreateKeystoreFile)
|
|
|
|
|
|
|
|
success = true
|
|
|
|
ok(lock)
|
|
|
|
|
|
|
|
proc createRemoteValidatorFiles*(
|
|
|
|
validatorsDir, keystoreDir, keystoreFile, encodedStorage: string
|
|
|
|
): Result[void, KeystoreGenerationError] {.raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
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()
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc createLockedRemoteValidatorFiles(
|
2022-08-07 21:53:20 +00:00
|
|
|
validatorsDir, keystoreDir, keystoreFile, encodedStorage: string
|
|
|
|
): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} =
|
|
|
|
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:
|
|
|
|
let lock = ? secureWriteLockedFile(
|
|
|
|
keystoreFile, encodedStorage).mapErrTo(FailedToCreateKeystoreFile)
|
|
|
|
success = true
|
|
|
|
ok(lock)
|
|
|
|
|
|
|
|
proc saveKeystore*(
|
|
|
|
rng: var HmacDrbgContext,
|
|
|
|
validatorsDir, secretsDir: string,
|
|
|
|
signingKey: ValidatorPrivKey,
|
|
|
|
signingPubKey: CookedPubKey,
|
|
|
|
signingKeyPath: KeyPath,
|
|
|
|
password: string,
|
2023-02-16 17:25:48 +00:00
|
|
|
salt: openArray[byte] = @[],
|
2022-08-07 21:53:20 +00:00
|
|
|
mode = Secure
|
|
|
|
): Result[void, KeystoreGenerationError] {.raises: [Defect].} =
|
2020-08-02 17:26:57 +00:00
|
|
|
let
|
2021-12-22 12:37:31 +00:00
|
|
|
keypass = KeystorePass.init(password)
|
2022-07-13 14:45:04 +00:00
|
|
|
keyName = signingPubKey.fsName
|
2021-12-22 12:37:31 +00:00
|
|
|
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,
|
2023-02-16 17:25:48 +00:00
|
|
|
mode = mode, salt = salt)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
let encodedStorage =
|
|
|
|
try:
|
|
|
|
Json.encode(keyStore)
|
|
|
|
except SerializationError as e:
|
|
|
|
error "Could not serialize keystorage", key_path = keystoreFile
|
|
|
|
return err(KeystoreGenerationError(
|
|
|
|
kind: FailedToCreateKeystoreFile, error: e.msg))
|
2022-02-07 20:36:09 +00:00
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
? createLocalValidatorFiles(secretsDir, validatorsDir,
|
|
|
|
keystoreDir,
|
|
|
|
secretsDir / keyName, keypass.str,
|
|
|
|
keystoreFile, encodedStorage)
|
2022-02-07 20:36:09 +00:00
|
|
|
ok()
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc saveLockedKeystore(
|
2022-08-07 21:53:20 +00:00
|
|
|
rng: var HmacDrbgContext,
|
|
|
|
validatorsDir, secretsDir: string,
|
|
|
|
signingKey: ValidatorPrivKey,
|
|
|
|
signingPubKey: CookedPubKey,
|
|
|
|
signingKeyPath: KeyPath,
|
|
|
|
password: string,
|
|
|
|
mode = Secure
|
|
|
|
): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} =
|
|
|
|
let
|
|
|
|
keypass = KeystorePass.init(password)
|
|
|
|
keyName = signingPubKey.fsName
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / KeystoreFileName
|
|
|
|
|
|
|
|
if dirExists(keystoreDir):
|
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreDir,
|
|
|
|
error: "Keystore directory already exists"))
|
|
|
|
if fileExists(keystoreFile):
|
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreFile,
|
|
|
|
error: "Keystore file already exists"))
|
|
|
|
|
|
|
|
let keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
|
|
|
|
keypass, signingKeyPath,
|
|
|
|
mode = mode)
|
|
|
|
|
|
|
|
let encodedStorage =
|
|
|
|
try:
|
|
|
|
Json.encode(keyStore)
|
|
|
|
except SerializationError as e:
|
|
|
|
error "Could not serialize keystorage", key_path = keystoreFile
|
|
|
|
return err(KeystoreGenerationError(
|
|
|
|
kind: FailedToCreateKeystoreFile, error: e.msg))
|
|
|
|
|
|
|
|
let lock = ? createLockedLocalValidatorFiles(secretsDir, validatorsDir,
|
|
|
|
keystoreDir,
|
|
|
|
secretsDir / keyName,
|
|
|
|
keypass.str,
|
|
|
|
keystoreFile, encodedStorage)
|
|
|
|
ok(lock)
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc saveKeystore(
|
2022-08-07 21:53:20 +00:00
|
|
|
validatorsDir: string,
|
|
|
|
publicKey: ValidatorPubKey,
|
|
|
|
urls: seq[RemoteSignerInfo],
|
|
|
|
threshold: uint32,
|
|
|
|
flags: set[RemoteKeystoreFlag] = {},
|
|
|
|
remoteType = RemoteSignerType.Web3Signer,
|
|
|
|
desc = ""
|
|
|
|
): Result[void, KeystoreGenerationError] {.raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
let
|
2022-07-13 14:45:04 +00:00
|
|
|
keyName = publicKey.fsName
|
2022-02-07 20:36:09 +00:00
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / RemoteKeystoreFileName
|
|
|
|
keystoreDesc = if len(desc) == 0: none[string]() else: some(desc)
|
|
|
|
keyStore = RemoteKeystore(
|
2022-05-10 00:32:12 +00:00
|
|
|
version: 2'u64,
|
|
|
|
description: keystoreDesc,
|
|
|
|
remoteType: remoteType,
|
|
|
|
pubkey: publicKey,
|
|
|
|
threshold: threshold,
|
|
|
|
remotes: urls,
|
|
|
|
flags: flags)
|
2022-02-07 20:36:09 +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"))
|
|
|
|
|
|
|
|
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-08-07 21:53:20 +00:00
|
|
|
? createRemoteValidatorFiles(validatorsDir, keystoreDir, keystoreFile,
|
|
|
|
encodedStorage)
|
2020-08-02 17:26:57 +00:00
|
|
|
ok()
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc saveLockedKeystore(
|
2022-08-07 21:53:20 +00:00
|
|
|
validatorsDir: string,
|
|
|
|
publicKey: ValidatorPubKey,
|
|
|
|
urls: seq[RemoteSignerInfo],
|
|
|
|
threshold: uint32,
|
|
|
|
flags: set[RemoteKeystoreFlag] = {},
|
|
|
|
remoteType = RemoteSignerType.Web3Signer,
|
|
|
|
desc = ""
|
|
|
|
): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} =
|
|
|
|
let
|
|
|
|
keyName = publicKey.fsName
|
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / RemoteKeystoreFileName
|
|
|
|
keystoreDesc = if len(desc) == 0: none[string]() else: some(desc)
|
|
|
|
keyStore = RemoteKeystore(
|
|
|
|
version: 2'u64,
|
|
|
|
description: keystoreDesc,
|
|
|
|
remoteType: remoteType,
|
|
|
|
pubkey: publicKey,
|
|
|
|
threshold: threshold,
|
|
|
|
remotes: urls,
|
|
|
|
flags: flags)
|
|
|
|
|
|
|
|
if dirExists(keystoreDir):
|
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreDir,
|
|
|
|
error: "Keystore directory already exists"))
|
|
|
|
if fileExists(keystoreFile):
|
|
|
|
return err(KeystoreGenerationError(kind: DuplicateKeystoreFile,
|
|
|
|
error: "Keystore file already exists"))
|
|
|
|
|
|
|
|
let encodedStorage =
|
|
|
|
try:
|
|
|
|
Json.encode(keyStore)
|
|
|
|
except SerializationError as exc:
|
|
|
|
error "Could not serialize keystorage", key_path = keystoreFile
|
|
|
|
return err(KeystoreGenerationError(
|
|
|
|
kind: FailedToCreateKeystoreFile, error: exc.msg))
|
|
|
|
|
|
|
|
let lock = ? createLockedRemoteValidatorFiles(validatorsDir, keystoreDir,
|
|
|
|
keystoreFile, encodedStorage)
|
|
|
|
ok(lock)
|
|
|
|
|
|
|
|
proc saveKeystore*(
|
|
|
|
validatorsDir: string,
|
|
|
|
publicKey: ValidatorPubKey,
|
|
|
|
url: HttpHostUri
|
|
|
|
): Result[void, KeystoreGenerationError] {.raises: [Defect].} =
|
2022-05-10 00:32:12 +00:00
|
|
|
let remoteInfo = RemoteSignerInfo(url: url, id: 0)
|
|
|
|
saveKeystore(validatorsDir, publicKey, @[remoteInfo], 1)
|
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
proc saveLockedKeystore(
|
2022-08-07 21:53:20 +00:00
|
|
|
validatorsDir: string,
|
|
|
|
publicKey: ValidatorPubKey,
|
|
|
|
url: HttpHostUri
|
|
|
|
): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} =
|
|
|
|
let remoteInfo = RemoteSignerInfo(url: url, id: 0)
|
|
|
|
saveLockedKeystore(validatorsDir, publicKey, @[remoteInfo], 1)
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
proc importKeystore*(pool: var ValidatorPool,
|
|
|
|
validatorsDir: string,
|
2022-05-10 00:32:12 +00:00
|
|
|
keystore: RemoteKeystore): ImportResult[KeystoreData]
|
|
|
|
{.raises: [Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
let
|
|
|
|
publicKey = keystore.pubkey
|
2022-07-13 14:45:04 +00:00
|
|
|
keyName = publicKey.fsName
|
2022-02-07 20:36:09 +00:00
|
|
|
keystoreDir = validatorsDir / keyName
|
|
|
|
keystoreFile = keystoreDir / RemoteKeystoreFileName
|
|
|
|
|
|
|
|
# We check `publicKey`.
|
2023-01-11 12:29:21 +00:00
|
|
|
let cookedKey = publicKey.load().valueOr:
|
2022-02-07 20:36:09 +00:00
|
|
|
return err(
|
|
|
|
AddValidatorFailure.init(AddValidatorStatus.failed,
|
|
|
|
"Invalid validator's public key"))
|
|
|
|
|
|
|
|
# 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))
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
let res = saveLockedKeystore(validatorsDir, publicKey, keystore.remotes,
|
2022-08-07 21:53:20 +00:00
|
|
|
keystore.threshold)
|
2022-02-07 20:36:09 +00:00
|
|
|
if res.isErr():
|
|
|
|
return err(AddValidatorFailure.init(AddValidatorStatus.failed,
|
|
|
|
$res.error()))
|
2022-08-07 21:53:20 +00:00
|
|
|
ok(KeystoreData.init(cookedKey, keystore.remotes, keystore.threshold,
|
|
|
|
res.get()))
|
2022-02-07 20:36:09 +00:00
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
proc importKeystore*(pool: var ValidatorPool,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2022-08-19 10:30:07 +00:00
|
|
|
validatorsDir, secretsDir: string,
|
|
|
|
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()
|
2022-07-13 14:45:04 +00:00
|
|
|
keyName = publicKey.fsName
|
2022-02-07 20:36:09 +00:00
|
|
|
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-08-07 21:53:20 +00:00
|
|
|
let res = saveLockedKeystore(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-08-07 21:53:20 +00:00
|
|
|
ok(KeystoreData.init(privateKey, keystore, res.get()))
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
proc generateDistributedStore*(rng: var HmacDrbgContext,
|
2022-05-10 00:32:12 +00:00
|
|
|
shares: seq[SecretShare],
|
|
|
|
pubKey: ValidatorPubKey,
|
|
|
|
validatorIdx: Natural,
|
|
|
|
shareSecretsDir: string,
|
|
|
|
shareValidatorDir: string,
|
|
|
|
remoteValidatorDir: string,
|
|
|
|
remoteSignersUrls: seq[string],
|
2023-01-13 10:31:39 +00:00
|
|
|
threshold: uint32,
|
|
|
|
mode = KeystoreMode.Secure): Result[void, KeystoreGenerationError] =
|
2022-05-10 00:32:12 +00:00
|
|
|
var signers: seq[RemoteSignerInfo]
|
2022-05-17 13:50:49 +00:00
|
|
|
for idx, share in shares:
|
2022-06-21 08:29:16 +00:00
|
|
|
var password = KeystorePass.init ncrutils.toHex(rng.generateBytes(32))
|
2022-05-10 00:32:12 +00:00
|
|
|
# remote signer shares
|
|
|
|
defer: burnMem(password)
|
|
|
|
? saveKeystore(rng,
|
2022-05-17 13:50:49 +00:00
|
|
|
shareValidatorDir / $share.id,
|
|
|
|
shareSecretsDir / $share.id,
|
2023-03-05 01:40:21 +00:00
|
|
|
share.key,
|
|
|
|
share.key.toPubKey,
|
2022-05-10 00:32:12 +00:00
|
|
|
makeKeyPath(validatorIdx, signingKeyKind),
|
2023-03-05 01:40:21 +00:00
|
|
|
password.str,
|
|
|
|
@[],
|
2023-01-13 10:31:39 +00:00
|
|
|
mode)
|
2022-05-10 00:32:12 +00:00
|
|
|
|
|
|
|
signers.add RemoteSignerInfo(
|
|
|
|
url: HttpHostUri(parseUri(remoteSignersUrls[idx])),
|
|
|
|
id: share.id,
|
|
|
|
pubkey: share.key.toPubKey.toPubKey)
|
|
|
|
|
|
|
|
# actual validator
|
|
|
|
saveKeystore(remoteValidatorDir, pubKey, signers, threshold)
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
func validatorKeystoreDir(host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey): string =
|
2022-09-17 05:30:07 +00:00
|
|
|
host.validatorsDir.validatorKeystoreDir(pubkey)
|
2022-07-13 14:45:04 +00:00
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func feeRecipientPath(host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey): string =
|
2022-09-17 05:30:07 +00:00
|
|
|
host.validatorsDir.feeRecipientPath(pubkey)
|
2022-07-13 14:45:04 +00:00
|
|
|
|
2023-06-02 11:06:33 +00:00
|
|
|
func gasLimitPath(host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey): string =
|
2023-02-15 15:10:31 +00:00
|
|
|
host.validatorsDir.gasLimitPath(pubkey)
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
proc removeFeeRecipientFile*(host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey): Result[void, string] =
|
|
|
|
let path = host.feeRecipientPath(pubkey)
|
2022-07-13 14:45:04 +00:00
|
|
|
if fileExists(path):
|
|
|
|
let res = io2.removeFile(path)
|
|
|
|
if res.isErr:
|
|
|
|
return err res.error.ioErrorMsg
|
|
|
|
|
|
|
|
return ok()
|
|
|
|
|
2023-02-15 15:10:31 +00:00
|
|
|
proc removeGasLimitFile*(host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey): Result[void, string] =
|
|
|
|
let path = host.gasLimitPath(pubkey)
|
|
|
|
if fileExists(path):
|
|
|
|
let res = io2.removeFile(path)
|
|
|
|
if res.isErr:
|
|
|
|
return err res.error.ioErrorMsg
|
|
|
|
|
|
|
|
return ok()
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
proc setFeeRecipient*(host: KeymanagerHost, pubkey: ValidatorPubKey, feeRecipient: Eth1Address): Result[void, string] =
|
|
|
|
let validatorKeystoreDir = host.validatorKeystoreDir(pubkey)
|
2022-07-13 14:45:04 +00:00
|
|
|
|
|
|
|
? secureCreatePath(validatorKeystoreDir).mapErr(proc(e: auto): string =
|
|
|
|
"Could not create wallet directory [" & validatorKeystoreDir & "]: " & $e)
|
|
|
|
|
|
|
|
io2.writeFile(validatorKeystoreDir / FeeRecipientFilename, $feeRecipient)
|
|
|
|
.mapErr(proc(e: auto): string = "Failed to write fee recipient file: " & $e)
|
|
|
|
|
2023-02-15 15:10:31 +00:00
|
|
|
proc setGasLimit*(host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey,
|
|
|
|
gasLimit: uint64): Result[void, string] =
|
|
|
|
let validatorKeystoreDir = host.validatorKeystoreDir(pubkey)
|
|
|
|
|
|
|
|
? secureCreatePath(validatorKeystoreDir).mapErr(proc(e: auto): string =
|
|
|
|
"Could not create wallet directory [" & validatorKeystoreDir & "]: " & $e)
|
|
|
|
|
|
|
|
io2.writeFile(validatorKeystoreDir / GasLimitFilename, $gasLimit)
|
|
|
|
.mapErr(proc(e: auto): string = "Failed to write gas limit file: " & $e)
|
|
|
|
|
2023-05-17 04:56:37 +00:00
|
|
|
from ".."/spec/beaconstate import has_eth1_withdrawal_credential
|
|
|
|
|
|
|
|
proc getValidatorWithdrawalAddress*(
|
|
|
|
host: KeymanagerHost, pubkey: ValidatorPubKey): Opt[Eth1Address] =
|
|
|
|
if host.getValidatorAndIdxFn.isNil:
|
|
|
|
Opt.none Eth1Address
|
|
|
|
else:
|
|
|
|
let validatorAndIndex = host.getValidatorAndIdxFn(pubkey)
|
|
|
|
if validatorAndIndex.isNone:
|
|
|
|
Opt.none Eth1Address
|
|
|
|
else:
|
|
|
|
template validator: auto = validatorAndIndex.get.validator
|
|
|
|
if has_eth1_withdrawal_credential(validator):
|
|
|
|
var address: distinctBase(Eth1Address)
|
|
|
|
address[0..^1] =
|
|
|
|
validator.withdrawal_credentials.data[12..^1]
|
|
|
|
Opt.some Eth1Address address
|
|
|
|
else:
|
|
|
|
Opt.none Eth1Address
|
|
|
|
|
|
|
|
func getPerValidatorDefaultFeeRecipient*(
|
|
|
|
defaultFeeRecipient: Opt[Eth1Address],
|
|
|
|
withdrawalAddress: Opt[Eth1Address]): Eth1Address =
|
|
|
|
defaultFeeRecipient.valueOr:
|
|
|
|
withdrawalAddress.valueOr:
|
|
|
|
(static(default(Eth1Address)))
|
|
|
|
|
2022-07-13 14:45:04 +00:00
|
|
|
proc getSuggestedFeeRecipient*(
|
2023-05-17 04:56:37 +00:00
|
|
|
host: KeymanagerHost, pubkey: ValidatorPubKey,
|
|
|
|
defaultFeeRecipient: Eth1Address):
|
2023-05-20 12:18:51 +00:00
|
|
|
Result[Eth1Address, ValidatorConfigFileStatus] =
|
2023-05-17 04:56:37 +00:00
|
|
|
host.validatorsDir.getSuggestedFeeRecipient(pubkey, defaultFeeRecipient)
|
|
|
|
|
|
|
|
proc getSuggestedFeeRecipient(
|
|
|
|
host: KeyManagerHost, pubkey: ValidatorPubKey,
|
|
|
|
withdrawalAddress: Opt[Eth1Address]): Eth1Address =
|
|
|
|
# Enforce the gsfr(foo).valueOr(foo) pattern where feasible
|
|
|
|
let perValidatorDefaultFeeRecipient = getPerValidatorDefaultFeeRecipient(
|
|
|
|
host.defaultFeeRecipient, withdrawalAddress)
|
|
|
|
host.getSuggestedFeeRecipient(
|
|
|
|
pubkey, perValidatorDefaultFeeRecipient).valueOr:
|
|
|
|
perValidatorDefaultFeeRecipient
|
2022-07-13 14:45:04 +00:00
|
|
|
|
2023-02-15 15:10:31 +00:00
|
|
|
proc getSuggestedGasLimit*(
|
|
|
|
host: KeymanagerHost,
|
|
|
|
pubkey: ValidatorPubKey): Result[uint64, ValidatorConfigFileStatus] =
|
|
|
|
host.validatorsDir.getSuggestedGasLimit(pubkey, host.defaultGasLimit)
|
|
|
|
|
2023-05-17 04:56:37 +00:00
|
|
|
proc addValidator*(
|
|
|
|
host: KeymanagerHost, keystore: KeystoreData,
|
|
|
|
withdrawalAddress: Opt[Eth1Address]) =
|
2022-09-17 05:30:07 +00:00
|
|
|
let
|
2023-05-17 04:56:37 +00:00
|
|
|
feeRecipient = host.getSuggestedFeeRecipient(
|
|
|
|
keystore.pubkey, withdrawalAddress)
|
2023-02-15 15:10:31 +00:00
|
|
|
gasLimit = host.getSuggestedGasLimit(keystore.pubkey).valueOr(
|
|
|
|
host.defaultGasLimit)
|
|
|
|
v = host.validatorPool[].addValidator(keystore, feeRecipient, gasLimit)
|
2022-11-20 13:55:43 +00:00
|
|
|
|
2023-02-07 14:53:36 +00:00
|
|
|
if not isNil(host.getValidatorAndIdxFn):
|
|
|
|
let data = host.getValidatorAndIdxFn(keystore.pubkey)
|
|
|
|
v.updateValidator(data)
|
2022-07-13 14:45:04 +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,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
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-05-10 00:32:12 +00:00
|
|
|
remoteSignersUrls: seq[string] = @[],
|
|
|
|
threshold: uint32 = 1,
|
|
|
|
remoteValidatorsCount: uint32 = 0,
|
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
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
var
|
|
|
|
salt = rng.generateKeystoreSalt()
|
|
|
|
password = KeystorePass.init ncrutils.toHex(rng.generateBytes(32))
|
|
|
|
|
|
|
|
defer:
|
|
|
|
burnMem(salt)
|
|
|
|
burnMem(password)
|
|
|
|
|
2022-05-10 00:32:12 +00:00
|
|
|
let localValidatorsCount = totalNewValidators - int(remoteValidatorsCount)
|
|
|
|
for i in 0 ..< localValidatorsCount:
|
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
|
2022-05-10 00:32:12 +00:00
|
|
|
|
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,
|
2023-02-16 17:25:48 +00:00
|
|
|
salt, 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
|
|
|
|
2022-05-10 00:32:12 +00:00
|
|
|
for i in 0 ..< remoteValidatorsCount:
|
|
|
|
let validatorIdx = int(firstValidatorIdx) + localValidatorsCount + int(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
|
|
|
|
|
|
|
|
let sharesCount = uint32 len(remoteSignersUrls)
|
|
|
|
|
|
|
|
let shares = generateSecretShares(derivedKey, rng, threshold, sharesCount)
|
|
|
|
if shares.isErr():
|
|
|
|
error "Failed to generate distributed key: ", threshold, sharesCount
|
|
|
|
continue
|
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
? generateDistributedStore(rng,
|
2022-05-10 00:32:12 +00:00
|
|
|
shares.get,
|
|
|
|
signingPubKey.toPubKey,
|
|
|
|
validatorIdx,
|
|
|
|
secretsDir & "_shares",
|
|
|
|
validatorsDir & "_shares",
|
|
|
|
validatorsDir,
|
|
|
|
remoteSignersUrls,
|
2023-01-13 10:31:39 +00:00
|
|
|
threshold,
|
|
|
|
mode)
|
2022-05-10 00:32:12 +00:00
|
|
|
|
|
|
|
deposits.add prepareDeposit(
|
|
|
|
cfg, withdrawalPubKey, derivedKey, signingPubKey)
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
ok deposits
|
2020-04-15 07:59:47 +00:00
|
|
|
|
2023-06-02 11:06:33 +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 =
|
2023-04-25 06:44:01 +00:00
|
|
|
burnMem password
|
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
|
|
|
|
|
2023-03-20 11:58:54 +00:00
|
|
|
proc importKeystoreFromFile*(
|
2023-04-25 06:44:01 +00:00
|
|
|
decryptor: var MultipleKeystoresDecryptor,
|
|
|
|
fileName: string
|
|
|
|
): Result[ValidatorPrivKey, string] =
|
2023-03-20 11:58:54 +00:00
|
|
|
let
|
|
|
|
data = readAllChars(fileName).valueOr:
|
|
|
|
return err("Unable to read keystore file [" & ioErrorMsg(error) & "]")
|
|
|
|
keystore =
|
|
|
|
try:
|
|
|
|
parseKeystore(data)
|
|
|
|
except SerializationError as e:
|
|
|
|
return err("Invalid keystore file format [" &
|
|
|
|
e.formatMsg(fileName) & "]")
|
|
|
|
|
|
|
|
var firstDecryptionAttempt = true
|
|
|
|
while true:
|
|
|
|
var secret: seq[byte]
|
2023-04-25 06:44:01 +00:00
|
|
|
let status = decryptCryptoField(
|
|
|
|
keystore.crypto,
|
|
|
|
KeystorePass.init(decryptor.previouslyUsedPassword),
|
|
|
|
secret)
|
2023-03-20 11:58:54 +00:00
|
|
|
case status
|
|
|
|
of DecryptionStatus.Success:
|
|
|
|
let privateKey = ValidatorPrivKey.fromRaw(secret).valueOr:
|
|
|
|
return err("Keystore holds invalid private key [" & $error & "]")
|
|
|
|
return ok(privateKey)
|
|
|
|
of DecryptionStatus.InvalidKeystore:
|
|
|
|
return err("Invalid keystore format")
|
|
|
|
of DecryptionStatus.InvalidPassword:
|
|
|
|
if firstDecryptionAttempt:
|
|
|
|
try:
|
|
|
|
const msg = "Please enter the password for decrypting '$1'"
|
|
|
|
echo msg % [fileName]
|
|
|
|
except ValueError:
|
|
|
|
raiseAssert "The format string above is correct"
|
|
|
|
firstDecryptionAttempt = false
|
|
|
|
else:
|
|
|
|
echo "The entered password was incorrect. Please try again."
|
|
|
|
|
2023-04-25 06:44:01 +00:00
|
|
|
if not(readPasswordInput("Password: ", decryptor.previouslyUsedPassword)):
|
2023-03-20 11:58:54 +00:00
|
|
|
echo "System error while entering password. Please try again."
|
2023-04-25 06:44:01 +00:00
|
|
|
if len(decryptor.previouslyUsedPassword) == 0: break
|
2023-03-20 11:58:54 +00:00
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
proc importKeystoresFromDir*(rng: var HmacDrbgContext, meth: ImportMethod,
|
2020-08-02 17:26:57 +00:00
|
|
|
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)
|
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
var (singleSaltPassword, singleSaltSalt) =
|
|
|
|
case meth
|
|
|
|
of ImportMethod.Normal:
|
|
|
|
var defaultSeq: seq[byte]
|
|
|
|
(KeystorePass.init(""), defaultSeq)
|
|
|
|
of ImportMethod.SingleSalt:
|
|
|
|
(KeystorePass.init(ncrutils.toHex(rng.generateBytes(32))),
|
|
|
|
rng.generateBytes(32))
|
|
|
|
|
|
|
|
defer:
|
|
|
|
burnMem(singleSaltPassword)
|
|
|
|
burnMem(singleSaltSalt)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
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:
|
2022-09-19 22:09:56 +00:00
|
|
|
Json.loadFile(file, Keystore,
|
|
|
|
requireAllFields = true,
|
|
|
|
allowUnknownFields = true)
|
2020-08-27 13:24:30 +00:00
|
|
|
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
|
2023-02-16 17:25:48 +00:00
|
|
|
var (password, salt) =
|
|
|
|
case meth
|
|
|
|
of ImportMethod.Normal:
|
|
|
|
var defaultSeq: seq[byte]
|
|
|
|
(KeystorePass.init ncrutils.toHex(rng.generateBytes(32)),
|
|
|
|
defaultSeq)
|
|
|
|
of ImportMethod.SingleSalt:
|
|
|
|
(singleSaltPassword, singleSaltSalt)
|
|
|
|
|
|
|
|
defer:
|
|
|
|
burnMem(password)
|
|
|
|
burnMem(salt)
|
|
|
|
|
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,
|
2023-02-16 17:25:48 +00:00
|
|
|
keystore.path, password.str,
|
|
|
|
salt)
|
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"
|
|
|
|
|
2022-06-21 08:29:16 +00:00
|
|
|
proc pickPasswordAndSaveWallet(rng: var HmacDrbgContext,
|
2020-08-21 19:36:42 +00:00
|
|
|
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: "
|
2022-07-13 14:43:57 +00:00
|
|
|
? keyboardCreatePassword(prompt, confirm)
|
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:
|
2022-12-06 12:40:13 +00:00
|
|
|
let enteredName = ask "Wallet name"
|
2020-09-29 16:49:09 +00:00
|
|
|
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*(
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-08-21 19:36:42 +00:00
|
|
|
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
|
|
|
|
2022-06-21 08:29:16 +00:00
|
|
|
proc restoreWalletInteractively*(rng: var HmacDrbgContext,
|
2020-08-21 19:36:42 +00:00
|
|
|
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)
|