nimbus-eth2/beacon_chain/validators/keystore_management.nim

909 lines
31 KiB
Nim
Raw Normal View History

# beacon_chain
# Copyright (c) 2018-2021 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [Defect].}
import
std/[os, strutils, terminal, wordwrap, unicode],
chronicles, chronos, json_serialization, zxcvbn,
2020-07-08 12:36:03 +00:00
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
../spec/[eth2_merkleization, keystore],
../spec/datatypes/base,
stew/io2, libp2p/crypto/crypto as lcrypto,
nimcrypto/utils as ncrutils,
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
".."/[conf, filepath],
../networking/network_metadata
export
keystore
2020-10-15 12:50:21 +00:00
when defined(windows):
import stew/[windows/acl]
{.localPassC: "-fno-lto".} # no LTO for crypto
2019-07-12 14:24:11 +00:00
const
keystoreFileName* = "keystore.json"
netKeystoreFileName* = "network_keystore.json"
type
WalletPathPair* = object
wallet*: Wallet
path*: string
CreatedWallet* = object
walletPath*: WalletPathPair
seed*: KeySeed
2019-07-12 14:24:11 +00:00
const
2020-10-02 15:58:08 +00:00
minPasswordLen = 12
2020-10-06 18:55:04 +00:00
minPasswordEntropy = 60.0
mostCommonPasswords = wordListArray(
currentSourcePath.parentDir /
"../../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
minWordLen = minPasswordLen)
proc echoP*(msg: string) =
## Prints a paragraph aligned to 80 columns
echo ""
echo wrapWords(msg, 80)
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
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:
let res = secureCreatePath(dataDir)
2020-08-26 06:42:26 +00:00
if res.isErr():
2020-10-30 00:36:47 +00:00
fatal "Could not create data directory", data_dir = dataDir,
2020-08-26 06:42:26 +00:00
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
2020-10-30 00:36:47 +00:00
return false
2020-08-26 06:42:26 +00:00
elif defined(windows):
2020-10-30 00:36:47 +00:00
let amask = {AccessFlags.Read, AccessFlags.Write, AccessFlags.Execute}
if fileAccessible(dataDir, amask):
let cres = checkCurrentUserOnlyACL(dataDir)
if cres.isErr():
fatal "Could not check data folder's ACL",
data_dir = dataDir, errorCode = $cres.error,
errorMsg = ioErrorMsg(cres.error)
2020-10-30 00:36:47 +00:00
return false
else:
if cres.get() == false:
fatal "Data folder has insecure ACL", data_dir = dataDir
2020-10-30 00:36:47 +00:00
return false
else:
let res = secureCreatePath(dataDir)
if res.isErr():
fatal "Could not create data folder", data_dir = dataDir,
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
2020-10-30 00:36:47 +00:00
return false
2020-08-26 06:42:26 +00:00
else:
fatal "Unsupported operation system"
return false
2020-10-30 00:36:47 +00:00
return true
proc checkSensitiveFilePermissions*(filePath: string): bool =
## 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.
when defined(windows):
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
else:
if cres.get() == false:
fatal "File has insecure permissions", key_path = filePath
2020-10-30 00:36:47 +00:00
return false
else:
2020-10-30 00:36:47 +00:00
let requiredPerms = 0o600
let currPermsRes = getPermissions(filePath)
if currPermsRes.isErr():
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
else:
2020-10-30 00:36:47 +00:00
let currPerms = currPermsRes.get()
if currPerms != requiredPerms:
warn "File has insecure permissions. Correcting them.",
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
proc keyboardCreatePassword(prompt: string,
confirm: string,
allowEmpty = false): KsResult[string] =
while true:
let password =
try:
readPasswordFromStdin(prompt)
except IOError:
error "Could not read password from stdin"
return err("Could not read password from stdin")
if password.len == 0 and allowEmpty:
return ok("")
# We treat `password` as UTF-8 encoded string.
if validateUtf8(password) == -1:
if runeLen(password) < minPasswordLen:
echoP "The entered password should be at least " & $minPasswordLen &
" characters."
echo ""
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
elif password in mostCommonPasswords:
echoP "The entered password is too commonly used and it would be " &
"easy to brute-force with automated tools."
echo ""
continue
else:
echoP "Entered password is not valid UTF-8 string"
echo ""
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:
echo "Passwords don't match, please try again\n"
continue
return ok(password)
proc keyboardGetPassword[T](prompt: string, attempts: int,
pred: proc(p: string): KsResult[T] {.gcsafe, raises: [Defect].}): KsResult[T] =
var
remainingAttempts = attempts
counter = 1
while remainingAttempts > 0:
let passphrase =
try:
readPasswordFromStdin(prompt)
except IOError:
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")
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[ValidatorPrivKey] =
let
keystorePath = validatorsDir / keyName / keystoreFileName
keystore =
try: Json.loadFile(keystorePath, Keystore)
except IOError as err:
error "Failed to read keystore", err = err.msg, path = keystorePath
return
except SerializationError as err:
error "Invalid keystore", err = err.formatMsg(keystorePath)
return
let passphrasePath = secretsDir / keyName
if fileExists(passphrasePath):
if not(checkSensitiveFilePermissions(passphrasePath)):
error "Password file has insecure permissions", key_path = keyStorePath
return
2020-10-02 15:46:05 +00:00
let passphrase = KeystorePass.init:
try:
readFile(passphrasePath)
except IOError as err:
error "Failed to read passphrase file", err = err.msg,
path = passphrasePath
return
let res = decryptKeystore(keystore, passphrase)
if res.isOk:
return res.get.some
else:
error "Failed to decrypt keystore", keystorePath, passphrasePath
return
if nonInteractive:
error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir",
keyName, validatorsDir, secretsDir = secretsDir
return
let prompt = "Please enter passphrase for key \"" &
(validatorsDir / keyName) & "\": "
let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3,
proc (password: string): KsResult[ValidatorPrivKey] =
2020-10-02 15:46:05 +00:00
let decrypted = decryptKeystore(keystore, KeystorePass.init password)
if decrypted.isErr():
error "Keystore decryption failed. Please try again", keystorePath
decrypted
)
2020-10-02 15:46:05 +00:00
if res.isOk():
some(res.get())
else:
return
iterator validatorKeysFromDirs*(validatorsDir, secretsDir: string): ValidatorPrivKey =
try:
for kind, file in walkDir(validatorsDir):
if kind == pcDir:
let keyName = splitFile(file).name
let key = loadKeystore(validatorsDir, secretsDir, keyName, true)
if key.isSome:
yield key.get
else:
quit 1
except OSError:
quit 1
iterator validatorKeys*(config: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey =
let validatorsDir = config.validatorsDir
try:
for kind, file in walkDir(validatorsDir):
if kind == pcDir:
let keyName = splitFile(file).name
let key = loadKeystore(validatorsDir, config.secretsDir, keyName, config.nonInteractive)
if key.isSome:
yield key.get
else:
quit 1
except OSError as err:
error "Validator keystores directory not accessible",
path = validatorsDir, err = err.msg
quit 1
type
KeystoreGenerationErrorKind = enum
2020-08-02 18:47:15 +00:00
FailedToCreateValidatorDir
FailedToCreateSecretsDir
FailedToCreateSecretFile
FailedToCreateKeystoreFile
KeystoreGenerationError* = object
case kind*: KeystoreGenerationErrorKind
of FailedToCreateValidatorDir, FailedToCreateSecretsDir,
FailedToCreateSecretFile, FailedToCreateKeystoreFile:
error*: string
proc mapErrTo[T, E](r: Result[T, E], v: static KeystoreGenerationErrorKind):
Result[T, KeystoreGenerationError] =
r.mapErr(proc (e: E): KeystoreGenerationError =
KeystoreGenerationError(kind: v, error: $e))
proc loadNetKeystore*(keyStorePath: string,
insecurePwd: Option[string]): Option[lcrypto.PrivateKey] =
if not(checkSensitiveFilePermissions(keystorePath)):
error "Network keystorage file has insecure permissions",
key_path = keyStorePath
return
let keyStore =
try:
Json.loadFile(keystorePath, NetKeystore)
except IOError as err:
error "Failed to read network keystore", err = err.msg,
path = keystorePath
return
except SerializationError as err:
error "Invalid network keystore", err = err.formatMsg(keystorePath)
return
if insecurePwd.isSome():
warn "Using insecure password to unlock networking key"
2020-10-02 15:46:05 +00:00
let decrypted = decryptNetKeystore(keystore, KeystorePass.init insecurePwd.get)
if decrypted.isOk:
return some(decrypted.get())
else:
error "Network keystore decryption failed", key_store = keyStorePath
return
else:
let prompt = "Please enter passphrase to unlock networking key: "
let res = keyboardGetPassword[lcrypto.PrivateKey](prompt, 3,
proc (password: string): KsResult[lcrypto.PrivateKey] =
2020-10-02 15:46:05 +00:00
let decrypted = decryptNetKeystore(keystore, KeystorePass.init password)
if decrypted.isErr():
error "Keystore decryption failed. Please try again", keystorePath
decrypted
)
if res.isOk():
some(res.get())
else:
return
proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string,
netKey: lcrypto.PrivateKey, insecurePwd: Option[string]
): Result[void, KeystoreGenerationError] =
let password =
if insecurePwd.isSome():
warn "Using insecure password to lock networking key",
key_path = keyStorePath
insecurePwd.get()
else:
let prompt = "Please enter NEW password to lock network key storage: "
let confirm = "Please confirm, network key storage password: "
? keyboardCreatePassword(prompt, confirm).mapErrTo(
FailedToCreateKeystoreFile)
let keyStore = createNetKeystore(kdfScrypt, rng, netKey,
2020-10-02 15:46:05 +00:00
KeystorePass.init password)
var encodedStorage: string
try:
encodedStorage = Json.encode(keyStore)
except SerializationError as exc:
error "Could not serialize network key storage", key_path = keyStorePath
return err(KeystoreGenerationError(
kind: FailedToCreateKeystoreFile, error: exc.msg))
let res = secureWriteFile(keyStorePath, encodedStorage)
if res.isOk():
ok()
else:
error "Could not write to network key storage file",
key_path = keyStorePath
res.mapErrTo(FailedToCreateKeystoreFile)
proc saveKeystore(rng: var BrHmacDrbgContext,
validatorsDir, secretsDir: string,
signingKey: ValidatorPrivKey, signingPubKey: CookedPubKey,
signingKeyPath: KeyPath): Result[void, KeystoreGenerationError] =
let
keyName = "0x" & $signingPubKey
validatorDir = validatorsDir / keyName
if not existsDir(validatorDir):
2020-10-02 15:46:05 +00:00
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
defer: burnMem(password)
let
keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
password, signingKeyPath)
keystoreFile = validatorDir / keystoreFileName
var encodedStorage: string
try:
encodedStorage = Json.encode(keyStore)
except SerializationError as e:
error "Could not serialize keystorage", key_path = keystoreFile
return err(KeystoreGenerationError(
kind: FailedToCreateKeystoreFile, error: e.msg))
? secureCreatePath(validatorDir).mapErrTo(FailedToCreateValidatorDir)
? secureCreatePath(secretsDir).mapErrTo(FailedToCreateSecretsDir)
? secureWriteFile(secretsDir / keyName, password.str).mapErrTo(
FailedToCreateSecretFile)
? secureWriteFile(keystoreFile, encodedStorage).mapErrTo(
FailedToCreateKeystoreFile)
ok()
Implement split preset/config support (#2710) * Implement split preset/config support This is the initial bulk refactor to introduce runtime config values in a number of places, somewhat replacing the existing mechanism of loading network metadata. It still needs more work, this is the initial refactor that introduces runtime configuration in some of the places that need it. The PR changes the way presets and constants work, to match the spec. In particular, a "preset" now refers to the compile-time configuration while a "cfg" or "RuntimeConfig" is the dynamic part. A single binary can support either mainnet or minimal, but not both. Support for other presets has been removed completely (can be readded, in case there's need). There's a number of outstanding tasks: * `SECONDS_PER_SLOT` still needs fixing * loading custom runtime configs needs redoing * checking constants against YAML file * yeerongpilly support `build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG` * load fork epoch from config * fix fork digest sent in status * nicer error string for request failures * fix tools * one more * fixup * fixup * fixup * use "standard" network definition folder in local testnet Files are loaded from their standard locations, including genesis etc, to conform to the format used in the `eth2-networks` repo. * fix launch scripts, allow unknown config values * fix base config of rest test * cleanups * bundle mainnet config using common loader * fix spec links and names * only include supported preset in binary * drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
proc generateDeposits*(cfg: RuntimeConfig,
2020-07-08 12:36:03 +00:00
rng: var BrHmacDrbgContext,
seed: KeySeed,
firstValidatorIdx, totalNewValidators: int,
validatorsDir: string,
secretsDir: string): Result[seq[DepositData], KeystoreGenerationError] =
var deposits: seq[DepositData]
notice "Generating deposits", totalNewValidators, validatorsDir, secretsDir
# 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)
for i in 0 ..< totalNewValidators:
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
? saveKeystore(rng, validatorsDir, secretsDir,
derivedKey, signingPubKey,
makeKeyPath(validatorIdx, signingKeyKind))
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)
ok deposits
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
let walletDir = splitFile(outWalletPath).dir
var encodedWallet: string
try:
encodedWallet = Json.encode(wallet, pretty = true)
except SerializationError:
return err("Could not serialize wallet")
? 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)
ok()
proc saveWallet*(wallet: WalletPathPair): Result[void, string] =
saveWallet(wallet.wallet, wallet.path)
proc readPasswordInput(prompt: string, password: var TaintedString): bool =
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)
except IOError:
false
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
proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
importedDir, validatorsDir, secretsDir: string) =
var password: TaintedString
defer: burnMem(password)
try:
for file in walkDirRec(importedDir):
2020-10-20 13:01:21 +00:00
let filenameParts = splitFile(file)
if toLowerAscii(filenameParts.ext) != ".json":
continue
# In case we are importing from eth2.0-deposits-cli, the imported
# validator_keys directory will also include a "deposit_data" file
# intended for uploading to the launchpad. We'll skip it to avoid
# the "Invalid keystore" warning that it will trigger.
if filenameParts.name.startsWith("deposit_data"):
continue
let keystore =
try:
Json.loadFile(file, Keystore)
except SerializationError as e:
warn "Invalid keystore", err = e.formatMsg(file)
continue
except IOError as e:
warn "Failed to read keystore file", file, err = e.msg
continue
var firstDecryptionAttempt = true
while true:
var secret: seq[byte]
let status = decryptCryptoField(keystore.crypto,
KeystorePass.init password,
secret)
case status
of Success:
let privKey = ValidatorPrivKey.fromRaw(secret)
if privKey.isOk:
let pubKey = privKey.value.toPubKey
let status = saveKeystore(rng, validatorsDir, secretsDir,
privKey.value, pubKey,
keystore.path)
if status.isOk:
notice "Keystore imported", file
else:
error "Failed to import keystore",
file, validatorsDir, secretsDir, err = status.error
else:
error "Imported keystore holds invalid key", file, err = privKey.error
break
of InvalidKeystore:
warn "Invalid keystore", file
break
of InvalidPassword:
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]
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
template ask(prompt: string): string =
try:
stdout.write prompt, ": "
stdin.readLine()
except IOError:
return err "failure to read data from stdin"
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
config: BeaconNodeConf,
seed: KeySeed): Result[WalletPathPair, string] =
echoP "When you perform operations with your wallet such as withdrawals " &
"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."
echo ""
2020-10-05 15:27:05 +00:00
var password =
block:
let prompt = "Please enter a password: "
let confirm = "Please repeat the password: "
? keyboardCreatePassword(prompt, confirm).mapErr(proc(e: auto): string = $e)
2020-10-01 19:18:56 +00:00
defer: burnMem(password)
var name: WalletName
let outWalletName = config.outWalletName
if outWalletName.isSome:
name = outWalletName.get
else:
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."
echo ""
while true:
var enteredName = ask "Wallet name"
if enteredName.len > 0:
name =
try:
WalletName.parseCmdArg(enteredName)
except CatchableError as err:
echo err.msg & ". Please try again."
continue
break
let nextAccount =
if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore:
config.restoredDepositsCount
else:
none Natural
let wallet = createWallet(kdfPbkdf2, rng, seed,
name = name,
nextAccount = nextAccount,
password = KeystorePass.init password)
let outWalletFileFlag = config.outWalletFile
let outWalletFile =
if outWalletFileFlag.isSome:
string outWalletFileFlag.get
else:
config.walletsDir / addFileExt(string wallet.name, "json")
let status = saveWallet(wallet, outWalletFile)
if status.isErr:
return err("failure to create wallet file due to " & status.error)
echo "\nWallet file successfully written to \"", outWalletFile, "\""
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
when defined(windows):
2020-10-12 19:58:09 +00:00
proc clearScreen =
discard execShellCmd("cls")
else:
template clearScreen =
echo "\e[1;1H\e[2J\e[3J"
proc createWalletInteractively*(
rng: var BrHmacDrbgContext,
config: BeaconNodeConf): Result[CreatedWallet, string] =
if config.nonInteractive:
return err "not running in interactive mode"
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."
echoP "The seed phrase should be kept secret in a safe location as if " &
"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 ""
var mnemonic = generateMnemonic(rng)
defer: burnMem(mnemonic)
try:
echoP "Your seed phrase is:"
setStyleNoError({styleBright})
setForegroundColorNoError fgCyan
echoP $mnemonic
resetAttributesNoError()
except IOError, ValueError:
return err "failure to write to the standard output"
echoP "Press any key to continue."
try:
2020-10-12 19:58:09 +00:00
discard getch()
except IOError as err:
fatal "Failed to read a key from stdin", err = err.msg
quit 1
clearScreen()
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."
echo ""
for i in countdown(2, 0):
let answer = ask "Answer"
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
else:
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
clearScreen()
var mnenomicPassword = KeystorePass.init ""
defer: burnMem(mnenomicPassword)
echoP "The recovery of your wallet can be additionally protected by a" &
"recovery password. Since the seed phrase itself can be considered " &
"a password, setting such an additional password is optional. " &
"To ensure the strongest possible security, we recommend writing " &
"down your seed phrase and remembering your recovery password. " &
"If you don'n want to set a recovery password, just press ENTER."
var recoveryPassword = keyboardCreatePassword(
"Recovery password: ", "Confirm password: ", allowEmpty = true)
defer:
if recoveryPassword.isOk:
burnMem(recoveryPassword.get)
if recoveryPassword.isErr:
fatal "Failed to read password from stdin: "
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)
proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
config: BeaconNodeConf) =
var
enteredMnemonic: TaintedString
validatedMnemonic: Mnemonic
defer:
burnMem enteredMnemonic
burnMem validatedMnemonic
echo "To restore your wallet, please enter your backed-up seed phrase."
while true:
if not readPasswordInput("Seedphrase: ", enteredMnemonic):
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."
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)
proc unlockWalletInteractively*(wallet: Wallet): Result[KeySeed, string] =
echo "Please enter the password for unlocking the wallet"
let res = keyboardGetPassword[KeySeed]("Password: ", 3,
proc (password: string): KsResult[KeySeed] =
var secret: seq[byte]
defer: burnMem(secret)
let status = decryptCryptoField(wallet.crypto, KeystorePass.init password, secret)
case status
of Success:
ok(KeySeed secret)
else:
# TODO Handle InvalidKeystore in a special way here
let failed = "Unlocking of the wallet failed. Please try again"
echo failed
err(failed)
)
if res.isOk():
ok(res.get())
else:
err "Unlocking of the wallet failed."
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
proc findWallet*(config: BeaconNodeConf,
name: WalletName): Result[Option[WalletPathPair], string] =
var walletFiles = newSeq[string]()
try:
for kind, walletFile in walkDir(config.walletsDir):
if kind != pcFile: continue
let walletId = splitFile(walletFile).name
if cmpIgnoreCase(walletId, name.string) == 0:
let wallet = ? loadWallet(walletFile)
return ok some WalletPathPair(wallet: wallet, path: walletFile)
walletFiles.add walletFile
except OSError as err:
return err("Error accessing the wallets directory \"" &
config.walletsDir & "\": " & err.msg)
for walletFile in walletFiles:
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:
return ok some WalletPathPair(wallet: wallet, path: walletFile)
return ok none(WalletPathPair)
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 =
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)
func `as`*(copied: LaunchPadDeposit, T: type DepositData): T =
T(pubkey: copied.pubkey,
withdrawal_credentials: copied.withdrawal_credentials,
amount: copied.amount,
signature: copied.signature)