Implement 'wallets restore' and 'wallets list'; Update 'nextAccount' properly after making deposits multiple times

This commit is contained in:
Zahary Karadjov 2020-08-21 22:36:42 +03:00 committed by zah
parent 1d1e0217ca
commit 8809f8d200
5 changed files with 221 additions and 107 deletions

View File

@ -1161,26 +1161,33 @@ programMain:
of deposits:
case config.depositsCmd
of DepositsCmd.create:
var walletData = if config.existingWalletId.isSome:
var mnemonic: Mnemonic
defer: burnMem(mnemonic)
var walletPath: WalletPathPair
if config.existingWalletId.isSome:
let id = config.existingWalletId.get
let found = keystore_management.findWallet(config, id)
if found.isErr:
if found.isOk:
walletPath = found.get
else:
fatal "Unable to find wallet with the specified name/uuid",
id, err = found.error
quit 1
let unlocked = unlockWalletInteractively(found.get)
var unlocked = unlockWalletInteractively(walletPath.wallet)
if unlocked.isOk:
unlocked.get
swap(mnemonic, unlocked.get)
else:
# The failure will be reported in `unlockWalletInteractively`.
quit 1
else:
let walletData = createWalletInteractively(rng[], config)
if walletData.isErr:
fatal "Unable to create wallet", err = walletData.error
var walletRes = createWalletInteractively(rng[], config)
if walletRes.isErr:
fatal "Unable to create wallet", err = walletRes.error
quit 1
walletData.get
defer: burnMem(walletData.mnemonic)
else:
swap(mnemonic, walletRes.get.mnemonic)
walletPath = walletRes.get.walletPath
createDir(config.outValidatorsDir)
createDir(config.outSecretsDir)
@ -1188,7 +1195,8 @@ programMain:
let deposits = generateDeposits(
config.runtimePreset,
rng[],
walletData,
mnemonic,
walletPath.wallet.nextAccount,
config.totalDeposits,
config.outValidatorsDir,
config.outSecretsDir)
@ -1208,6 +1216,14 @@ programMain:
Json.saveFile(depositDataPath, launchPadDeposits)
info "Deposit data written", filename = depositDataPath
walletPath.wallet.nextAccount += deposits.value.len
let status = saveWallet(walletPath)
if status.isErr:
error "Failed to update wallet file after generating deposits",
wallet = walletPath.path,
error = status.error
quit 1
except CatchableError as err:
error "Failed to create launchpad deposit data file", err = err.msg
quit 1
@ -1225,15 +1241,22 @@ programMain:
of wallets:
case config.walletsCmd:
of WalletsCmd.create:
let status = createWalletInteractively(rng[], config)
if status.isErr:
fatal "Unable to create wallet", err = status.error
var walletRes = createWalletInteractively(rng[], config)
if walletRes.isErr:
fatal "Unable to create wallet", err = walletRes.error
quit 1
burnMem(walletRes.get.mnemonic)
of WalletsCmd.list:
# TODO
discard
for kind, walletFile in walkDir(config.walletsDir):
if kind != pcFile: continue
let walletRes = loadWallet(walletFile)
if walletRes.isOk:
echo walletRes.get.longName
else:
warn "Found corrupt wallet file",
wallet = walletFile, error = walletRes.error
of WalletsCmd.restore:
# TODO
discard
restoreWalletInteractively(rng[], config)

View File

@ -170,7 +170,7 @@ proc main() {.async.} =
if cfg.cmd == StartUpCommand.generateSimulationDeposits:
let
walletData = WalletDataForDeposits(mnemonic: generateMnemonic(rng[]))
mnemonic = generateMnemonic(rng[])
runtimePreset = defaultRuntimePreset
createDir(string cfg.outValidatorsDir)
@ -179,8 +179,8 @@ proc main() {.async.} =
let deposits = generateDeposits(
runtimePreset,
rng[],
walletData,
cfg.simulationDepositsCount,
mnemonic,
0, cfg.simulationDepositsCount,
string cfg.outValidatorsDir,
string cfg.outSecretsDir)

View File

@ -14,9 +14,13 @@ const
keystoreFileName* = "keystore.json"
type
WalletDataForDeposits* = object
WalletPathPair* = object
wallet*: Wallet
path*: string
CreatedWallet* = object
walletPath*: WalletPathPair
mnemonic*: Mnemonic
nextAccount*: Natural
proc loadKeystore(conf: BeaconNodeConf|ValidatorClientConf,
validatorsDir, keyName: string): Option[ValidatorPrivKey] =
@ -134,22 +138,22 @@ proc saveKeystore(rng: var BrHmacDrbgContext,
proc generateDeposits*(preset: RuntimePreset,
rng: var BrHmacDrbgContext,
walletData: WalletDataForDeposits,
totalValidators: int,
mnemonic: Mnemonic,
firstValidatorIdx, totalNewValidators: int,
validatorsDir: string,
secretsDir: string): Result[seq[DepositData], KeystoreGenerationError] =
var deposits: seq[DepositData]
info "Generating deposits", totalValidators, validatorsDir, secretsDir
info "Generating deposits", totalNewValidators, validatorsDir, secretsDir
let withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
# TODO: Explain why we are using an empty password
var withdrawalKey = keyFromPath(walletData.mnemonic, KeystorePass"", withdrawalKeyPath)
var withdrawalKey = keyFromPath(mnemonic, KeystorePass"", withdrawalKeyPath)
defer: burnMem(withdrawalKey)
let withdrawalPubKey = withdrawalKey.toPubKey
for i in 0 ..< totalValidators:
let keyStoreIdx = walletData.nextAccount + i
for i in 0 ..< totalNewValidators:
let keyStoreIdx = firstValidatorIdx + i
let signingKeyPath = withdrawalKeyPath.append keyStoreIdx
var signingKey = deriveChildKey(withdrawalKey, keyStoreIdx)
defer: burnMem(signingKey)
@ -168,7 +172,7 @@ const
mostCommonPasswords = wordListArray(
currentSourcePath.parentDir /
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
minWordLength = minPasswordLen)
minWordLen = minPasswordLen)
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
try: createDir splitFile(outWalletPath).dir
@ -186,6 +190,9 @@ proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
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):
@ -282,58 +289,19 @@ proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
fatal "Failed to access the imported deposits directory"
quit 1
proc createWalletInteractively*(
rng: var BrHmacDrbgContext,
conf: BeaconNodeConf): Result[WalletDataForDeposits, string] =
if conf.nonInteractive:
return err "not running in interactive mode"
var mnemonic = generateMnemonic(rng)
defer: burnMem(mnemonic)
template ask(prompt: string): string =
try:
stdout.write prompt, ": "
stdin.readLine()
except IOError:
return err "failure to read data from stdin"
template echo80(msg: string) =
echo wrapWords(msg, 80)
echo80 "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 must use the " &
"words displayed below:"
template echo80(msg: string) =
echo wrapWords(msg, 80)
template ask(prompt: string): string =
try:
echo ""
setStyleNoError({styleBright})
setForegroundColorNoError fgCyan
echo80 $mnemonic
resetAttributesNoError()
echo ""
except IOError, ValueError:
return err "failure to write to the standard output"
echo80 "Please back up the seed phrase now to a safe location as " &
"if you are protecting a sensitive password. The seed phrase " &
"can be used to withdrawl funds from your wallet."
echo ""
echo "Did you back up your seed recovery phrase?\p" &
"(please type 'yes' to continue or press enter to quit)"
while true:
let answer = ask "Answer"
if answer == "":
return err "aborted wallet creation"
elif answer != "yes":
echo "To continue, please type 'yes' (without the quotes) or press enter to quit"
else:
break
stdout.write prompt, ": "
stdin.readLine()
except IOError:
return err "failure to read data from stdin"
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
config: BeaconNodeConf,
mnemonic: Mnemonic): Result[WalletPathPair, string] =
echo ""
echo80 "When you perform operations with your wallet such as withdrawals " &
"and additional deposits, you'll be asked to enter a password. " &
@ -378,7 +346,7 @@ proc createWalletInteractively*(
continue
var name: WalletName
let outWalletName = conf.outWalletName
let outWalletName = config.outWalletName
if outWalletName.isSome:
name = outWalletName.get
else:
@ -396,33 +364,107 @@ proc createWalletInteractively*(
continue
break
let wallet = createWallet(kdfPbkdf2, rng, mnemonic,
name = name, password = KeystorePass password)
let nextAccount = if config.walletsCmd == WalletsCmd.restore:
config.restoredDepositsCount
else:
none Natural
let outWalletFileFlag = conf.outWalletFile
let wallet = createWallet(kdfPbkdf2, rng, mnemonic,
name = name,
nextAccount = nextAccount,
password = KeystorePass password)
let outWalletFileFlag = config.outWalletFile
let outWalletFile = if outWalletFileFlag.isSome:
string outWalletFileFlag.get
else:
conf.walletsDir / addFileExt(string wallet.uuid, "json")
config.walletsDir / addFileExt(string wallet.uuid, "json")
let status = saveWallet(wallet, outWalletFile)
if status.isErr:
return err("failure to create wallet file due to " & status.error)
info "Wallet file written", path = outWalletFile
return ok WalletDataForDeposits(mnemonic: mnemonic, nextAccount: 0)
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
finally:
burnMem(password)
burnMem(confirmedPassword)
proc createWalletInteractively*(
rng: var BrHmacDrbgContext,
config: BeaconNodeConf): Result[CreatedWallet, string] =
if config.nonInteractive:
return err "not running in interactive mode"
var mnemonic = generateMnemonic(rng)
defer: burnMem(mnemonic)
echo80 "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 must use the " &
"words displayed below:"
try:
echo ""
setStyleNoError({styleBright})
setForegroundColorNoError fgCyan
echo80 $mnemonic
resetAttributesNoError()
echo ""
except IOError, ValueError:
return err "failure to write to the standard output"
echo80 "Please back up the seed phrase now to a safe location as " &
"if you are protecting a sensitive password. The seed phrase " &
"can be used to withdrawl funds from your wallet."
echo ""
echo "Did you back up your seed recovery phrase?\p" &
"(please type 'yes' to continue or press enter to quit)"
while true:
let answer = ask "Answer"
if answer == "":
return err "aborted wallet creation"
elif answer != "yes":
echo "To continue, please type 'yes' (without the quotes) or press enter to quit"
else:
break
let walletPath = ? pickPasswordAndSaveWallet(rng, config, mnemonic)
return ok CreatedWallet(walletPath: walletPath, mnemonic: mnemonic)
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."
discard pickPasswordAndSaveWallet(rng, config, validatedMnemonic)
proc loadWallet*(fileName: string): Result[Wallet, string] =
try:
ok Json.loadFile(fileName, Wallet)
except CatchableError as e:
err e.msg
proc unlockWalletInteractively*(wallet: Wallet): Result[WalletDataForDeposits, string] =
proc unlockWalletInteractively*(wallet: Wallet): Result[Mnemonic, string] =
echo "Please enter the password for unlocking the wallet"
for i in 1..3:
@ -434,8 +476,7 @@ proc unlockWalletInteractively*(wallet: Wallet): Result[WalletDataForDeposits, s
var secret = decryptCryptoField(wallet.crypto, KeystorePass password)
if secret.len > 0:
defer: burnMem(secret)
return ok WalletDataForDeposits(
mnemonic: Mnemonic string.fromBytes(secret))
return ok Mnemonic(string.fromBytes(secret))
else:
echo "Unlocking of the wallet failed. Please try again."
finally:
@ -443,7 +484,7 @@ proc unlockWalletInteractively*(wallet: Wallet): Result[WalletDataForDeposits, s
return err "failure to unlock wallet"
proc findWallet*(config: BeaconNodeConf, name: WalletName): Result[Wallet, string] =
proc findWallet*(config: BeaconNodeConf, name: WalletName): Result[WalletPathPair, string] =
var walletFiles = newSeq[string]()
try:
@ -451,15 +492,16 @@ proc findWallet*(config: BeaconNodeConf, name: WalletName): Result[Wallet, strin
if kind != pcFile: continue
let walletId = splitFile(walletFile).name
if cmpIgnoreCase(walletId, name.string) == 0:
return loadWallet(walletFile)
let wallet = ? loadWallet(walletFile)
return ok WalletPathPair(wallet: wallet, path: walletFile)
walletFiles.add walletFile
except OSError:
return err "failure to list wallet directory"
for walletFile in walletFiles:
let wallet = loadWallet(walletFile)
if wallet.isOk and cmpIgnoreCase(wallet.get.name.string, name.string) == 0:
return wallet
let wallet = ? loadWallet(walletFile)
if cmpIgnoreCase(wallet.name.string, name.string) == 0:
return ok WalletPathPair(wallet: wallet, path: walletFile)
return err "failure to locate wallet file"

View File

@ -6,9 +6,9 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
math, strutils, strformat, typetraits, bearssl,
math, strutils, strformat, typetraits, algorithm,
stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros,
eth/keyfile/uuid, blscurve, faststreams/textio, json_serialization,
bearssl, eth/keyfile/uuid, blscurve, faststreams/textio, json_serialization,
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, utils, scrypt],
./datatypes, ./crypto, ./digest, ./signatures
@ -146,6 +146,7 @@ const
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
wordListLen = 2048
maxWordLen = 16
UUID.serializesAsBaseIn Json
KeyPath.serializesAsBaseIn Json
@ -170,6 +171,12 @@ template burnMem*(m: var (SensitiveData|TaintedString)) =
# to make its usage less error-prone.
utils.burnMem(string m)
func longName*(wallet: Wallet): string =
if wallet.name.string == wallet.uuid.string:
wallet.name.string
else:
wallet.name.string & " (" & wallet.uuid.string & ")"
proc getRandomBytes*(rng: var BrHmacDrbgContext, n: Natural): seq[byte]
{.raises: [Defect].} =
result = newSeq[byte](n)
@ -177,18 +184,23 @@ proc getRandomBytes*(rng: var BrHmacDrbgContext, n: Natural): seq[byte]
macro wordListArray*(filename: static string,
maxWords: static int = 0,
minWordLength: static int = 0): untyped =
minWordLen: static int = 0,
maxWordLen: static int = high(int)): untyped =
result = newTree(nnkBracket)
var words = slurp(filename).split()
for word in words:
if word.len >= minWordLength:
if word.len >= minWordLen and word.len <= maxWordLen:
result.add newCall("cstring", newLit(word))
if maxWords > 0 and result.len >= maxWords:
return
const
englishWords = wordListArray("english_word_list.txt",
maxWords = wordListLen)
maxWords = wordListLen,
maxWordLen = maxWordLen)
static:
doAssert englishWords.len == wordListLen
iterator pathNodesImpl(path: string): Natural
{.raises: [ValueError].} =
@ -228,11 +240,15 @@ func getSeed*(mnemonic: Mnemonic, password: KeystorePass): KeySeed =
let salt = "mnemonic-" & password.string
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
template add(m: var Mnemonic, s: cstring) =
m.string.add s
proc generateMnemonic*(
rng: var BrHmacDrbgContext,
words: openarray[cstring] = englishWords,
entropyParam: openarray[byte] = @[]): Mnemonic =
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
## Generates a valid BIP-0039 mnenomic:
## https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
doAssert words.len == wordListLen
var entropy: seq[byte]
@ -252,17 +268,51 @@ proc generateMnemonic*(
entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits))
var res = ""
res.add words[entropy.getBitsBE(0..10)]
# Make sure the string won't be reallocated as this may
# leave partial copies of the mnemonic in memory:
result = Mnemonic newStringOfCap(mnemonicWordCount * maxWordLen)
result.add words[entropy.getBitsBE(0..10)]
for i in 1 ..< mnemonicWordCount:
let
firstBit = i*11
lastBit = firstBit + 10
res.add " "
res.add words[entropy.getBitsBE(firstBit..lastBit)]
result.add " "
result.add words[entropy.getBitsBE(firstBit..lastBit)]
Mnemonic res
proc cmpIgnoreCase(lhs: cstring, rhs: string): int =
# TODO: This is a bit silly.
# Nim should have a `cmp` function for C strings.
cmpIgnoreCase($lhs, rhs)
proc validateMnemonic*(inputWords: TaintedString,
outputMnemonic: var Mnemonic): bool =
## Accept a case-insensitive input string and returns `true`
## if it represents a valid mnenomic. The `outputMnemonic`
## value will be populated with a normalized lower-case
## version of the mnemonic using a single space separator.
##
## The `outputMnemonic` value may be populated partially
## with sensitive data even in case of validator failure.
## Make sure to burn the received data after usage.
let words = inputWords.string.strip.split(Whitespace)
if words.len < 12 or words.len > 24 or words.len mod 3 != 0:
return false
# Make sure the string won't be re-allocated as this may
# leave partial copies of the mnemonic in memory:
outputMnemonic = Mnemonic newStringOfCap(words.len * maxWordLen)
for word in words:
let foundIdx = binarySearch(englishWords, word, cmpIgnoreCase)
if foundIdx == -1:
return false
if outputMnemonic.string.len > 0:
outputMnemonic.add " "
outputMnemonic.add englishWords[foundIdx]
return true
proc deriveChildKey*(parentKey: ValidatorPrivKey,
index: Natural): ValidatorPrivKey =
@ -549,7 +599,6 @@ proc createWallet*(kdfKind: KdfKind,
seed = getSeed(mnemonic, KeystorePass"")
crypto = createCryptoField(kdfKind, rng, distinctBase seed,
password, salt, iv)
Wallet(
uuid: uuid,
name: if name.string.len > 0: name

2
vendor/nim-libp2p vendored

@ -1 +1 @@
Subproject commit eb13845f6537a0af7564d3d4b535d88938eb104a
Subproject commit d3182c4dba5cf0eaa11a1c41f065ea3c3b436995