Implement EIP-2386 wallets; Progress towards a CLI for interactive wallet creation
For more information:
4494da0966/EIPS/eip-2386.md (specification)
This commit is contained in:
parent
6836d41ebd
commit
384e512031
|
@ -7,7 +7,7 @@
|
|||
|
||||
import
|
||||
# Standard library
|
||||
algorithm, os, tables, random, strutils, times, math,
|
||||
algorithm, os, tables, random, strutils, times, math, terminal,
|
||||
|
||||
# Nimble packages
|
||||
stew/[objects, byteutils], stew/shims/macros,
|
||||
|
@ -899,7 +899,7 @@ func formatGwei(amount: uint64): string =
|
|||
|
||||
when hasPrompt:
|
||||
from unicode import Rune
|
||||
import terminal, prompt
|
||||
import prompt
|
||||
|
||||
proc providePromptCompletions*(line: seq[Rune], cursorPos: int): seq[string] =
|
||||
# TODO
|
||||
|
@ -1026,6 +1026,104 @@ when hasPrompt:
|
|||
# var t: Thread[ptr Prompt]
|
||||
# createThread(t, processPromptCommands, addr p)
|
||||
|
||||
proc createWalletInteractively(conf: BeaconNodeConf): OutFile {.raises: [Defect].} =
|
||||
if conf.nonInteractive:
|
||||
fatal "Wallets can be created only in interactive mode"
|
||||
quit 1
|
||||
|
||||
var mnemonic = generateMnemonic()
|
||||
defer: keystore_management.burnMem(mnemonic)
|
||||
|
||||
template readLine: string =
|
||||
try: stdin.readLine()
|
||||
except IOError:
|
||||
fatal "Failed to read data from stdin"
|
||||
quit 1
|
||||
|
||||
echo "The created wallet will be protected with a password " &
|
||||
"that applies only to the current Nimbus installation. " &
|
||||
"In case you lose your wallet and you need to restore " &
|
||||
"it on a different machine, you must use the following " &
|
||||
"seed recovery phrase: \n"
|
||||
|
||||
echo $mnemonic
|
||||
|
||||
echo "Please back up the seed phrase now to a safe location as " &
|
||||
"if you are protecting a sensitive password. The seed phrase " &
|
||||
"be used to withdrawl funds from your wallet.\n"
|
||||
|
||||
echo "Did you back up your seed recovery phrase? (please type 'yes' to continue or press enter to quit)"
|
||||
while true:
|
||||
let answer = readLine()
|
||||
if answer == "":
|
||||
quit 1
|
||||
elif answer != "yes":
|
||||
echo "To continue, please type 'yes' (without the quotes) or press enter to quit"
|
||||
else:
|
||||
break
|
||||
|
||||
echo "When you perform operations with your wallet such as withdrawals " &
|
||||
"and additional deposits, you'll be asked to enter a password. " &
|
||||
"Please note that this password is local to the current Nimbus " &
|
||||
"installation and can be changed at any time."
|
||||
|
||||
while true:
|
||||
var password, confirmedPassword: TaintedString
|
||||
try:
|
||||
let status = try:
|
||||
readPasswordFromStdin("Please enter a password:", password) and
|
||||
readPasswordFromStdin("Please repeat the password:", confirmedPassword)
|
||||
except IOError:
|
||||
fatal "Failed to read password interactively"
|
||||
quit 1
|
||||
|
||||
if status:
|
||||
if password != confirmedPassword:
|
||||
echo "Passwords don't match, please try again"
|
||||
else:
|
||||
var name: WalletName
|
||||
if conf.createdWalletName.isSome:
|
||||
name = conf.createdWalletName.get
|
||||
else:
|
||||
echo "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."
|
||||
|
||||
while true:
|
||||
var enteredName = readLine()
|
||||
if enteredName.len > 0:
|
||||
name = try: WalletName.parseCmdArg(enteredName)
|
||||
except CatchableError as err:
|
||||
echo err.msg & ". Please try again."
|
||||
continue
|
||||
break
|
||||
|
||||
let (uuid, walletContent) = KdfPbkdf2.createWalletContent(mnemonic, name)
|
||||
try:
|
||||
var outWalletFile: OutFile
|
||||
|
||||
if conf.createdWalletFile.isSome:
|
||||
outWalletFile = conf.createdWalletFile.get
|
||||
createDir splitFile(string outWalletFile).dir
|
||||
else:
|
||||
let walletsDir = conf.walletsDir
|
||||
createDir walletsDir
|
||||
outWalletFile = OutFile(walletsDir / addFileExt(string uuid, "json"))
|
||||
|
||||
writeFile(string outWalletFile, string walletContent)
|
||||
return outWalletFile
|
||||
except CatchableError as err:
|
||||
fatal "Failed to write wallet file", err = err.msg
|
||||
quit 1
|
||||
|
||||
if not status:
|
||||
fatal "Failed to read a password from stdin"
|
||||
quit 1
|
||||
|
||||
finally:
|
||||
keystore_management.burnMem(password)
|
||||
keystore_management.burnMem(confirmedPassword)
|
||||
|
||||
programMain:
|
||||
let config = makeBannerAndConfig(clientId, BeaconNodeConf)
|
||||
|
||||
|
@ -1100,22 +1198,6 @@ programMain:
|
|||
writeFile(bootstrapFile, bootstrapEnr.tryGet().toURI)
|
||||
echo "Wrote ", bootstrapFile
|
||||
|
||||
of importValidator:
|
||||
template reportFailureFor(keyExpr) =
|
||||
error "Failed to import validator key", key = keyExpr
|
||||
programResult = 1
|
||||
|
||||
if config.keyFiles.len == 0:
|
||||
stderr.write "Please specify at least one keyfile to import."
|
||||
quit 1
|
||||
|
||||
for keyFile in config.keyFiles:
|
||||
try:
|
||||
saveValidatorKey(keyFile.string.extractFilename,
|
||||
readFile(keyFile.string), config)
|
||||
except:
|
||||
reportFailureFor keyFile.string
|
||||
|
||||
of noCommand:
|
||||
debug "Launching beacon node",
|
||||
version = fullVersionStr,
|
||||
|
@ -1178,3 +1260,19 @@ programMain:
|
|||
config.depositContractAddress,
|
||||
config.depositPrivateKey,
|
||||
delayGenerator)
|
||||
|
||||
of DepositsCmd.status:
|
||||
# TODO
|
||||
echo "The status command is not implemented yet"
|
||||
quit 1
|
||||
|
||||
of wallets:
|
||||
case config.walletsCmd:
|
||||
of WalletsCmd.create:
|
||||
let walletFile = createWalletInteractively(config)
|
||||
of WalletsCmd.list:
|
||||
# TODO
|
||||
discard
|
||||
of WalletsCmd.restore:
|
||||
# TODO
|
||||
discard
|
||||
|
|
|
@ -15,16 +15,19 @@ type
|
|||
|
||||
BNStartUpCmd* = enum
|
||||
noCommand
|
||||
importValidator
|
||||
createTestnet
|
||||
deposits
|
||||
wallets
|
||||
|
||||
DepositsCmd* = enum
|
||||
create = "Create validator keystores and deposits"
|
||||
send = "Send prepared deposits to the validator deposit contract"
|
||||
WalletsCmd* {.pure.} = enum
|
||||
create = "Creates a new EIP-2386 wallet"
|
||||
restore = "Restores a wallet from cold storage"
|
||||
list = "Lists details about all wallets"
|
||||
|
||||
# TODO
|
||||
# status = "Display status information about all deposits"
|
||||
DepositsCmd* {.pure.} = enum
|
||||
create = "Creates validator keystores and deposits"
|
||||
send = "Sends prepared deposits to the validator deposit contract"
|
||||
status = "Displays status information about all deposits"
|
||||
|
||||
VCStartUpCmd* = enum
|
||||
VCNoCommand
|
||||
|
@ -38,36 +41,36 @@ type
|
|||
BeaconNodeConf* = object
|
||||
logLevel* {.
|
||||
defaultValue: "DEBUG"
|
||||
desc: "Sets the log level."
|
||||
desc: "Sets the log level"
|
||||
name: "log-level" }: string
|
||||
|
||||
eth1Network* {.
|
||||
defaultValue: goerli
|
||||
desc: "The Eth1 network tracked by the beacon node."
|
||||
desc: "The Eth1 network tracked by the beacon node"
|
||||
name: "eth1-network" }: Eth1Network
|
||||
|
||||
dataDir* {.
|
||||
defaultValue: config.defaultDataDir()
|
||||
desc: "The directory where nimbus will store all blockchain data."
|
||||
desc: "The directory where nimbus will store all blockchain data"
|
||||
abbr: "d"
|
||||
name: "data-dir" }: OutDir
|
||||
|
||||
web3Url* {.
|
||||
defaultValue: ""
|
||||
desc: "URL of the Web3 server to observe Eth1."
|
||||
desc: "URL of the Web3 server to observe Eth1"
|
||||
name: "web3-url" }: string
|
||||
|
||||
depositContractAddress* {.
|
||||
defaultValue: ""
|
||||
desc: "Address of the deposit contract."
|
||||
desc: "Address of the deposit contract"
|
||||
name: "deposit-contract" }: string
|
||||
|
||||
depositContractDeployedAt* {.
|
||||
desc: "The Eth1 block hash where the deposit contract has been deployed."
|
||||
desc: "The Eth1 block hash where the deposit contract has been deployed"
|
||||
name: "deposit-contract-block" }: Option[Eth2Digest]
|
||||
|
||||
nonInteractive* {.
|
||||
desc: "Do not display interative prompts. Quit on missing configuration."
|
||||
desc: "Do not display interative prompts. Quit on missing configuration"
|
||||
name: "non-interactive" }: bool
|
||||
|
||||
case cmd* {.
|
||||
|
@ -76,28 +79,28 @@ type
|
|||
|
||||
of noCommand:
|
||||
bootstrapNodes* {.
|
||||
desc: "Specifies one or more bootstrap nodes to use when connecting to the network."
|
||||
desc: "Specifies one or more bootstrap nodes to use when connecting to the network"
|
||||
abbr: "b"
|
||||
name: "bootstrap-node" }: seq[string]
|
||||
|
||||
bootstrapNodesFile* {.
|
||||
defaultValue: ""
|
||||
desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses."
|
||||
desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses"
|
||||
name: "bootstrap-file" }: InputFile
|
||||
|
||||
libp2pAddress* {.
|
||||
defaultValue: defaultListenAddress(config)
|
||||
desc: "Listening address for the Ethereum LibP2P traffic."
|
||||
desc: "Listening address for the Ethereum LibP2P traffic"
|
||||
name: "listen-address" }: ValidIpAddress
|
||||
|
||||
tcpPort* {.
|
||||
defaultValue: defaultEth2TcpPort
|
||||
desc: "Listening TCP port for Ethereum LibP2P traffic."
|
||||
desc: "Listening TCP port for Ethereum LibP2P traffic"
|
||||
name: "tcp-port" }: Port
|
||||
|
||||
udpPort* {.
|
||||
defaultValue: defaultEth2TcpPort
|
||||
desc: "Listening UDP port for node discovery."
|
||||
desc: "Listening UDP port for node discovery"
|
||||
name: "udp-port" }: Port
|
||||
|
||||
maxPeers* {.
|
||||
|
@ -107,7 +110,7 @@ type
|
|||
|
||||
nat* {.
|
||||
desc: "Specify method to use for determining public address. " &
|
||||
"Must be one of: any, none, upnp, pmp, extip:<IP>."
|
||||
"Must be one of: any, none, upnp, pmp, extip:<IP>"
|
||||
defaultValue: "any" }: string
|
||||
|
||||
validators* {.
|
||||
|
@ -117,52 +120,56 @@ type
|
|||
name: "validator" }: seq[ValidatorKeyPath]
|
||||
|
||||
validatorsDirFlag* {.
|
||||
desc: "A directory containing validator keystores."
|
||||
desc: "A directory containing validator keystores"
|
||||
name: "validators-dir" }: Option[InputDir]
|
||||
|
||||
secretsDirFlag* {.
|
||||
desc: "A directory containing validator keystore passwords."
|
||||
desc: "A directory containing validator keystore passwords"
|
||||
name: "secrets-dir" }: Option[InputDir]
|
||||
|
||||
walletsDirFlag* {.
|
||||
desc: "A directory containing wallet files"
|
||||
name: "wallets-dir" }: Option[InputDir]
|
||||
|
||||
stateSnapshot* {.
|
||||
desc: "Json file specifying a recent state snapshot."
|
||||
desc: "Json file specifying a recent state snapshot"
|
||||
abbr: "s"
|
||||
name: "state-snapshot" }: Option[InputFile]
|
||||
|
||||
nodeName* {.
|
||||
defaultValue: ""
|
||||
desc: "A name for this node that will appear in the logs. " &
|
||||
"If you set this to 'auto', a persistent automatically generated ID will be selected for each --dataDir folder."
|
||||
"If you set this to 'auto', a persistent automatically generated ID will be selected for each --data-dir folder"
|
||||
name: "node-name" }: string
|
||||
|
||||
verifyFinalization* {.
|
||||
defaultValue: false
|
||||
desc: "Specify whether to verify finalization occurs on schedule, for testing."
|
||||
desc: "Specify whether to verify finalization occurs on schedule, for testing"
|
||||
name: "verify-finalization" }: bool
|
||||
|
||||
stopAtEpoch* {.
|
||||
defaultValue: 0
|
||||
desc: "A positive epoch selects the epoch at which to stop."
|
||||
desc: "A positive epoch selects the epoch at which to stop"
|
||||
name: "stop-at-epoch" }: uint64
|
||||
|
||||
metricsEnabled* {.
|
||||
defaultValue: false
|
||||
desc: "Enable the metrics server."
|
||||
desc: "Enable the metrics server"
|
||||
name: "metrics" }: bool
|
||||
|
||||
metricsAddress* {.
|
||||
defaultValue: defaultAdminListenAddress(config)
|
||||
desc: "Listening address of the metrics server."
|
||||
desc: "Listening address of the metrics server"
|
||||
name: "metrics-address" }: ValidIpAddress
|
||||
|
||||
metricsPort* {.
|
||||
defaultValue: 8008
|
||||
desc: "Listening HTTP port of the metrics server."
|
||||
desc: "Listening HTTP port of the metrics server"
|
||||
name: "metrics-port" }: Port
|
||||
|
||||
statusBarEnabled* {.
|
||||
defaultValue: true
|
||||
desc: "Display a status bar at the bottom of the terminal screen."
|
||||
desc: "Display a status bar at the bottom of the terminal screen"
|
||||
name: "status-bar" }: bool
|
||||
|
||||
statusBarContents* {.
|
||||
|
@ -171,7 +178,7 @@ type
|
|||
"head: $head_root:$head_epoch:$head_epoch_slot;" &
|
||||
"time: $epoch:$epoch_slot ($slot)|" &
|
||||
"ETH: $attached_validators_balance"
|
||||
desc: "Textual template for the contents of the status bar."
|
||||
desc: "Textual template for the contents of the status bar"
|
||||
name: "status-bar-contents" }: string
|
||||
|
||||
rpcEnabled* {.
|
||||
|
@ -181,7 +188,7 @@ type
|
|||
|
||||
rpcPort* {.
|
||||
defaultValue: defaultEth2RpcPort
|
||||
desc: "HTTP port for the JSON-RPC service."
|
||||
desc: "HTTP port for the JSON-RPC service"
|
||||
name: "rpc-port" }: Port
|
||||
|
||||
rpcAddress* {.
|
||||
|
@ -196,77 +203,109 @@ type
|
|||
|
||||
of createTestnet:
|
||||
testnetDepositsDir* {.
|
||||
desc: "Directory containing validator keystores."
|
||||
desc: "Directory containing validator keystores"
|
||||
name: "validators-dir" }: InputDir
|
||||
|
||||
totalValidators* {.
|
||||
desc: "The number of validator deposits in the newly created chain."
|
||||
desc: "The number of validator deposits in the newly created chain"
|
||||
name: "total-validators" }: uint64
|
||||
|
||||
firstValidator* {.
|
||||
defaultValue: 0
|
||||
desc: "Index of first validator to add to validator list."
|
||||
desc: "Index of first validator to add to validator list"
|
||||
name: "first-validator" }: uint64
|
||||
|
||||
lastUserValidator* {.
|
||||
defaultValue: config.totalValidators - 1,
|
||||
desc: "The last validator index that will free for taking from a testnet participant."
|
||||
desc: "The last validator index that will free for taking from a testnet participant"
|
||||
name: "last-user-validator" }: uint64
|
||||
|
||||
bootstrapAddress* {.
|
||||
defaultValue: ValidIpAddress.init("127.0.0.1")
|
||||
desc: "The public IP address that will be advertised as a bootstrap node for the testnet."
|
||||
desc: "The public IP address that will be advertised as a bootstrap node for the testnet"
|
||||
name: "bootstrap-address" }: ValidIpAddress
|
||||
|
||||
bootstrapPort* {.
|
||||
defaultValue: defaultEth2TcpPort
|
||||
desc: "The TCP/UDP port that will be used by the bootstrap node."
|
||||
desc: "The TCP/UDP port that will be used by the bootstrap node"
|
||||
name: "bootstrap-port" }: Port
|
||||
|
||||
genesisOffset* {.
|
||||
defaultValue: 5
|
||||
desc: "Seconds from now to add to genesis time."
|
||||
desc: "Seconds from now to add to genesis time"
|
||||
name: "genesis-offset" }: int
|
||||
|
||||
outputGenesis* {.
|
||||
desc: "Output file where to write the initial state snapshot."
|
||||
desc: "Output file where to write the initial state snapshot"
|
||||
name: "output-genesis" }: OutFile
|
||||
|
||||
withGenesisRoot* {.
|
||||
defaultValue: false
|
||||
desc: "Include a genesis root in 'network.json'."
|
||||
desc: "Include a genesis root in 'network.json'"
|
||||
name: "with-genesis-root" }: bool
|
||||
|
||||
outputBootstrapFile* {.
|
||||
desc: "Output file with list of bootstrap nodes for the network."
|
||||
desc: "Output file with list of bootstrap nodes for the network"
|
||||
name: "output-bootstrap-file" }: OutFile
|
||||
|
||||
of importValidator:
|
||||
keyFiles* {.
|
||||
desc: "File with validator key to be imported (in hex form)."
|
||||
name: "keyfile" }: seq[ValidatorKeyPath]
|
||||
of wallets:
|
||||
case walletsCmd* {.command.}: WalletsCmd
|
||||
of WalletsCmd.create:
|
||||
createdWalletName* {.
|
||||
desc: "An easy-to-remember name for the wallet of your choice"
|
||||
name: "name"}: Option[WalletName]
|
||||
|
||||
nextAccount* {.
|
||||
desc: "Initial value for the 'nextaccount' property of the wallet"
|
||||
name: "next-account" }: Option[Natural]
|
||||
|
||||
createdWalletFile* {.
|
||||
desc: "Output wallet file"
|
||||
name: "out" }: Option[OutFile]
|
||||
|
||||
of WalletsCmd.restore:
|
||||
restoredWalletName* {.
|
||||
desc: "An easy-to-remember name for the wallet of your choice"
|
||||
name: "name"}: Option[WalletName]
|
||||
|
||||
restoredDepositsCount* {.
|
||||
desc: "Expected number of deposits to recover. If not specified, " &
|
||||
"Nimbus will try to guess the number by inspecting the latest " &
|
||||
"beacon state"
|
||||
name: "deposits".}: Option[Natural]
|
||||
|
||||
restoredWalletFile* {.
|
||||
desc: "Output wallet file"
|
||||
name: "out" }: Option[OutFile]
|
||||
|
||||
of WalletsCmd.list:
|
||||
discard
|
||||
|
||||
of deposits:
|
||||
case depositsCmd* {.command.}: DepositsCmd
|
||||
of create:
|
||||
of DepositsCmd.create:
|
||||
totalDeposits* {.
|
||||
defaultValue: 1
|
||||
desc: "Number of deposits to generate."
|
||||
desc: "Number of deposits to generate"
|
||||
name: "count" }: int
|
||||
|
||||
existingWalletId* {.
|
||||
desc: "An existing wallet ID. If not specified, a new wallet will be created"
|
||||
name: "wallet" }: Option[WalletName]
|
||||
|
||||
outValidatorsDir* {.
|
||||
defaultValue: "validators"
|
||||
desc: "Output folder for validator keystores and deposits."
|
||||
desc: "Output folder for validator keystores and deposits"
|
||||
name: "out-deposits-dir" }: string
|
||||
|
||||
outSecretsDir* {.
|
||||
defaultValue: "secrets"
|
||||
desc: "Output folder for randomly generated keystore passphrases."
|
||||
desc: "Output folder for randomly generated keystore passphrases"
|
||||
name: "out-secrets-dir" }: string
|
||||
|
||||
depositPrivateKey* {.
|
||||
defaultValue: ""
|
||||
desc: "Private key of the controlling (sending) account.",
|
||||
desc: "Private key of the controlling (sending) account",
|
||||
name: "deposit-private-key" }: string
|
||||
|
||||
dontSend* {.
|
||||
|
@ -274,25 +313,28 @@ type
|
|||
desc: "By default, all created deposits are also immediately sent " &
|
||||
"to the validator deposit contract. You can use this option to " &
|
||||
"prevent this behavior. Use the `deposits send` command to send " &
|
||||
"the deposit transactions at your convenience later."
|
||||
"the deposit transactions at your convenience later"
|
||||
name: "dont-send" .}: bool
|
||||
|
||||
of send:
|
||||
of DepositsCmd.send:
|
||||
depositsDir* {.
|
||||
defaultValue: "validators"
|
||||
desc: "A folder with validator metadata created by the `deposits create` command."
|
||||
desc: "A folder with validator metadata created by the `deposits create` command"
|
||||
name: "deposits-dir" }: string
|
||||
|
||||
minDelay* {.
|
||||
defaultValue: 0.0
|
||||
desc: "Minimum possible delay between making two deposits (in seconds)."
|
||||
desc: "Minimum possible delay between making two deposits (in seconds)"
|
||||
name: "min-delay" }: float
|
||||
|
||||
maxDelay* {.
|
||||
defaultValue: 0.0
|
||||
desc: "Maximum possible delay between making two deposits (in seconds)."
|
||||
desc: "Maximum possible delay between making two deposits (in seconds)"
|
||||
name: "max-delay" }: float
|
||||
|
||||
of DepositsCmd.status:
|
||||
discard
|
||||
|
||||
ValidatorClientConf* = object
|
||||
logLevel* {.
|
||||
defaultValue: "DEBUG"
|
||||
|
@ -301,12 +343,12 @@ type
|
|||
|
||||
dataDir* {.
|
||||
defaultValue: config.defaultDataDir()
|
||||
desc: "The directory where nimbus will store all blockchain data."
|
||||
desc: "The directory where nimbus will store all blockchain data"
|
||||
abbr: "d"
|
||||
name: "data-dir" }: OutDir
|
||||
|
||||
nonInteractive* {.
|
||||
desc: "Do not display interative prompts. Quit on missing configuration."
|
||||
desc: "Do not display interative prompts. Quit on missing configuration"
|
||||
name: "non-interactive" }: bool
|
||||
|
||||
case cmd* {.
|
||||
|
@ -316,26 +358,26 @@ type
|
|||
of VCNoCommand:
|
||||
rpcPort* {.
|
||||
defaultValue: defaultEth2RpcPort
|
||||
desc: "HTTP port of the server to connect to for RPC."
|
||||
desc: "HTTP port of the server to connect to for RPC"
|
||||
name: "rpc-port" }: Port
|
||||
|
||||
rpcAddress* {.
|
||||
defaultValue: defaultAdminListenAddress(config)
|
||||
desc: "Address of the server to connect to for RPC."
|
||||
desc: "Address of the server to connect to for RPC"
|
||||
name: "rpc-address" }: ValidIpAddress
|
||||
|
||||
validators* {.
|
||||
required
|
||||
desc: "Attach a validator by supplying a keystore path."
|
||||
desc: "Attach a validator by supplying a keystore path"
|
||||
abbr: "v"
|
||||
name: "validator" }: seq[ValidatorKeyPath]
|
||||
|
||||
validatorsDirFlag* {.
|
||||
desc: "A directory containing validator keystores."
|
||||
desc: "A directory containing validator keystores"
|
||||
name: "validators-dir" }: Option[InputDir]
|
||||
|
||||
secretsDirFlag* {.
|
||||
desc: "A directory containing validator keystore passwords."
|
||||
desc: "A directory containing validator keystore passwords"
|
||||
name: "secrets-dir" }: Option[InputDir]
|
||||
|
||||
proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
|
||||
|
@ -377,12 +419,26 @@ func parseCmdArg*(T: type Eth2Digest, input: TaintedString): T
|
|||
func completeCmdArg*(T: type Eth2Digest, input: TaintedString): seq[string] =
|
||||
return @[]
|
||||
|
||||
func parseCmdArg*(T: type WalletName, input: TaintedString): T
|
||||
{.raises: [ValueError, Defect].} =
|
||||
if input.len == 0:
|
||||
raise newException(ValueError, "The wallet name should not be empty")
|
||||
if input[0] == '_':
|
||||
raise newException(ValueError, "The wallet name should not start with an underscore")
|
||||
return T(input)
|
||||
|
||||
func completeCmdArg*(T: type WalletName, input: TaintedString): seq[string] =
|
||||
return @[]
|
||||
|
||||
func validatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
|
||||
string conf.validatorsDirFlag.get(InputDir(conf.dataDir / "validators"))
|
||||
|
||||
func secretsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
|
||||
string conf.secretsDirFlag.get(InputDir(conf.dataDir / "secrets"))
|
||||
|
||||
func walletsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
|
||||
string conf.walletsDirFlag.get(InputDir(conf.dataDir / "wallets"))
|
||||
|
||||
func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
|
||||
conf.dataDir / "db"
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ import
|
|||
web3, stint, eth/keys, confutils,
|
||||
spec/[datatypes, digest, crypto, keystore], conf, ssz/merkleization, merkle_minimal
|
||||
|
||||
export
|
||||
keystore
|
||||
|
||||
contract(DepositContract):
|
||||
proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32])
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
import
|
||||
json, math, strutils, strformat,
|
||||
json, math, strutils, strformat, typetraits,
|
||||
stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros,
|
||||
eth/keyfile/uuid, blscurve,
|
||||
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand],
|
||||
eth/keyfile/uuid, blscurve, json_serialization,
|
||||
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand, utils],
|
||||
./datatypes, ./crypto, ./digest, ./signatures
|
||||
|
||||
export
|
||||
|
@ -44,6 +44,17 @@ type
|
|||
prf: string
|
||||
salt: string
|
||||
|
||||
# https://github.com/ethereum/EIPs/blob/4494da0966afa7318ec0157948821b19c4248805/EIPS/eip-2386.md#specification
|
||||
Wallet* = object
|
||||
uuid*: UUID
|
||||
name*: WalletName
|
||||
version*: uint
|
||||
walletType* {.serializedFieldName: "type"}: string
|
||||
# TODO: The use of `JsonString` can be removed once we
|
||||
# solve the serialization problem for `Crypto[T]`
|
||||
crypto*: JsonString
|
||||
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
|
||||
|
||||
KdfParams = KdfPbkdf2 | KdfScrypt
|
||||
|
||||
Kdf[T: KdfParams] = object
|
||||
|
@ -69,12 +80,18 @@ type
|
|||
signingKeyKind # Also known as voting key
|
||||
withdrawalKeyKind
|
||||
|
||||
UUID* = distinct string
|
||||
WalletName* = distinct string
|
||||
Mnemonic* = distinct string
|
||||
KeyPath* = distinct string
|
||||
KeyStorePass* = distinct string
|
||||
KeyStoreContent* = distinct JsonString
|
||||
KeySeed* = distinct seq[byte]
|
||||
|
||||
KeyStoreContent* = distinct JsonString
|
||||
WalletContent* = distinct JsonString
|
||||
|
||||
SensitiveData = Mnemonic|KeyStorePass|KeySeed
|
||||
|
||||
Credentials* = object
|
||||
mnemonic*: Mnemonic
|
||||
keyStore*: KeyStoreContent
|
||||
|
@ -104,6 +121,17 @@ const
|
|||
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
|
||||
wordListLen = 2048
|
||||
|
||||
UUID.serializesAsBaseIn Json
|
||||
WalletName.serializesAsBaseIn Json
|
||||
|
||||
template `$`*(m: Mnemonic): string =
|
||||
string(m)
|
||||
|
||||
template burnMem*(m: var (SensitiveData|TaintedString)) =
|
||||
# TODO: `burnMem` in nimcrypto could use distinctBase
|
||||
# to make its usage less error-prone.
|
||||
utils.burnMem(string m)
|
||||
|
||||
macro wordListArray(filename: static string): array[wordListLen, cstring] =
|
||||
result = newTree(nnkBracket)
|
||||
var words = slurp(filename).split()
|
||||
|
@ -152,7 +180,7 @@ func getSeed*(mnemonic: Mnemonic, password: KeyStorePass): KeySeed =
|
|||
let salt = "mnemonic-" & password.string
|
||||
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
|
||||
|
||||
proc generateMnemonic*(words: openarray[cstring],
|
||||
proc generateMnemonic*(words: openarray[cstring] = englishWords,
|
||||
entropyParam: openarray[byte] = @[]): Mnemonic =
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
|
||||
doAssert words.len == wordListLen
|
||||
|
@ -226,9 +254,9 @@ proc shaChecksum(key, cipher: openarray[byte]): array[32, byte] =
|
|||
result = ctx.finish().data
|
||||
ctx.clear()
|
||||
|
||||
template tryJsonToCrypto(ks: JsonNode; crypto: typedesc): untyped =
|
||||
template tryJsonToCrypto(json: JsonNode; crypto: typedesc): untyped =
|
||||
try:
|
||||
ks{"crypto"}.to(Crypto[crypto])
|
||||
json.to(Crypto[crypto])
|
||||
except Exception:
|
||||
return err "ks: failed to parse crypto"
|
||||
|
||||
|
@ -238,12 +266,8 @@ template hexToBytes(data, name: string): untyped =
|
|||
except ValueError:
|
||||
return err "ks: failed to parse " & name
|
||||
|
||||
proc decryptKeystore*(data: KeyStoreContent,
|
||||
password: KeyStorePass): KsResult[ValidatorPrivKey] =
|
||||
# TODO: `parseJson` can raise a general `Exception`
|
||||
let ks = try: parseJson(data.string)
|
||||
except Exception: return err "ks: failed to parse keystore"
|
||||
|
||||
proc decryptoCryptoField*(json: JsonNode,
|
||||
password: KeyStorePass): KsResult[seq[byte]] =
|
||||
var
|
||||
decKey: seq[byte]
|
||||
salt: seq[byte]
|
||||
|
@ -251,15 +275,15 @@ proc decryptKeystore*(data: KeyStoreContent,
|
|||
cipherMsg: seq[byte]
|
||||
checksumMsg: seq[byte]
|
||||
|
||||
let kdf = ks{"crypto", "kdf", "function"}.getStr
|
||||
let kdf = json{"kdf", "function"}.getStr
|
||||
|
||||
case kdf
|
||||
of "scrypt":
|
||||
let crypto = tryJsonToCrypto(ks, KdfScrypt)
|
||||
let crypto = tryJsonToCrypto(json, KdfScrypt)
|
||||
return err "ks: scrypt not supported"
|
||||
of "pbkdf2":
|
||||
let
|
||||
crypto = tryJsonToCrypto(ks, KdfPbkdf2)
|
||||
crypto = tryJsonToCrypto(json, KdfPbkdf2)
|
||||
kdfParams = crypto.kdf.params
|
||||
|
||||
salt = hexToBytes(kdfParams.salt, "salt")
|
||||
|
@ -288,34 +312,41 @@ proc decryptKeystore*(data: KeyStoreContent,
|
|||
aesCipher.decrypt(cipherMsg, secret)
|
||||
aesCipher.clear()
|
||||
|
||||
ValidatorPrivKey.fromRaw(secret)
|
||||
ok secret
|
||||
|
||||
proc decryptKeystore*(data: KeyStoreContent,
|
||||
password: KeyStorePass): KsResult[ValidatorPrivKey] =
|
||||
# TODO: `parseJson` can raise a general `Exception`
|
||||
let
|
||||
ks = try: parseJson(data.string)
|
||||
except Exception: return err "ks: failed to parse keystore"
|
||||
secret = decryptoCryptoField(ks{"crypto"}, password)
|
||||
|
||||
ValidatorPrivKey.fromRaw(? secret)
|
||||
|
||||
proc createCryptoField(T: type[KdfParams],
|
||||
secret: openarray[byte],
|
||||
password = KeyStorePass "",
|
||||
salt: openarray[byte] = @[],
|
||||
iv: openarray[byte] = @[]): Crypto[T] =
|
||||
type AES = aes128
|
||||
|
||||
proc encryptKeystore*(T: type[KdfParams],
|
||||
privKey: ValidatorPrivkey,
|
||||
password = KeyStorePass "",
|
||||
path = KeyPath "",
|
||||
salt: openarray[byte] = @[],
|
||||
iv: openarray[byte] = @[],
|
||||
pretty = true): KeyStoreContent =
|
||||
var
|
||||
secret = privKey.toRaw[^32..^1]
|
||||
decKey: seq[byte]
|
||||
aesCipher: CTR[aes128]
|
||||
aesIv = newSeq[byte](aes128.sizeBlock)
|
||||
kdfSalt = newSeq[byte](saltSize)
|
||||
aesCipher: CTR[AES]
|
||||
cipherMsg = newSeq[byte](secret.len)
|
||||
|
||||
if salt.len > 0:
|
||||
let kdfSalt = if salt.len > 0:
|
||||
doAssert salt.len == saltSize
|
||||
kdfSalt = @salt
|
||||
@salt
|
||||
else:
|
||||
getRandomBytesOrPanic(kdfSalt)
|
||||
getRandomBytesOrPanic(saltSize)
|
||||
|
||||
if iv.len > 0:
|
||||
doAssert iv.len == aes128.sizeBlock
|
||||
aesIv = @iv
|
||||
let aesIv = if iv.len > 0:
|
||||
doAssert iv.len == AES.sizeBlock
|
||||
@iv
|
||||
else:
|
||||
getRandomBytesOrPanic(aesIv)
|
||||
getRandomBytesOrPanic(AES.sizeBlock)
|
||||
|
||||
when T is KdfPbkdf2:
|
||||
decKey = sha256.pbkdf2(password.string, kdfSalt, pbkdf2Params.c,
|
||||
|
@ -324,39 +355,83 @@ proc encryptKeystore*(T: type[KdfParams],
|
|||
var kdf = Kdf[KdfPbkdf2](function: "pbkdf2", params: pbkdf2Params, message: "")
|
||||
kdf.params.salt = byteutils.toHex(kdfSalt)
|
||||
else:
|
||||
return
|
||||
{.fatal: "Other KDFs are supported yet".}
|
||||
|
||||
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
|
||||
aesCipher.encrypt(secret, cipherMsg)
|
||||
aesCipher.clear()
|
||||
|
||||
let pubkey = privKey.toPubKey()
|
||||
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
||||
|
||||
Crypto[T](
|
||||
kdf: kdf,
|
||||
checksum: Checksum(
|
||||
function: "sha256",
|
||||
message: byteutils.toHex(sum)),
|
||||
cipher: Cipher(
|
||||
function: "aes-128-ctr",
|
||||
params: CipherParams(iv: byteutils.toHex(aesIv)),
|
||||
message: byteutils.toHex(cipherMsg)))
|
||||
|
||||
proc encryptKeystore*(T: type[KdfParams],
|
||||
privKey: ValidatorPrivkey,
|
||||
password = KeyStorePass "",
|
||||
path = KeyPath "",
|
||||
salt: openarray[byte] = @[],
|
||||
iv: openarray[byte] = @[],
|
||||
pretty = true): KeyStoreContent =
|
||||
let
|
||||
sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
||||
uuid = uuidGenerate().get
|
||||
|
||||
secret = privKey.toRaw[^32..^1]
|
||||
cryptoField = createCryptoField(T, secret, password, salt, iv)
|
||||
pubkey = privKey.toPubKey()
|
||||
uuid = uuidGenerate().expect("Random bytes should be available")
|
||||
keystore = Keystore[T](
|
||||
crypto: Crypto[T](
|
||||
kdf: kdf,
|
||||
checksum: Checksum(
|
||||
function: "sha256",
|
||||
message: byteutils.toHex(sum)
|
||||
),
|
||||
cipher: Cipher(
|
||||
function: "aes-128-ctr",
|
||||
params: CipherParams(iv: byteutils.toHex(aesIv)),
|
||||
message: byteutils.toHex(cipherMsg)
|
||||
)
|
||||
),
|
||||
crypto: cryptoField,
|
||||
pubkey: toHex(pubkey),
|
||||
path: path.string,
|
||||
uuid: $uuid,
|
||||
version: 4)
|
||||
|
||||
KeyStoreContent if pretty: json.pretty(%keystore, indent=4)
|
||||
KeyStoreContent if pretty: json.pretty(%keystore)
|
||||
else: $(%keystore)
|
||||
|
||||
proc createWallet*(T: type[KdfParams],
|
||||
mnemonic: Mnemonic,
|
||||
name = WalletName "",
|
||||
salt: openarray[byte] = @[],
|
||||
iv: openarray[byte] = @[],
|
||||
password = KeyStorePass "",
|
||||
nextAccount = none(Natural),
|
||||
pretty = true): Wallet =
|
||||
let
|
||||
uuid = UUID $(uuidGenerate().expect("Random bytes should be available"))
|
||||
# Please note that we are passing an empty password here because
|
||||
# we want the wallet restoration procedure to depend only on the
|
||||
# mnemonic (the user is asked to treat the mnemonic as a password).
|
||||
seed = getSeed(mnemonic, KeyStorePass"")
|
||||
cryptoField = %createCryptoField(T, distinctBase seed, password, salt, iv)
|
||||
|
||||
Wallet(
|
||||
uuid: uuid,
|
||||
name: if name.string.len > 0: name
|
||||
else: WalletName(uuid),
|
||||
version: 1,
|
||||
walletType: "hierarchical deterministic",
|
||||
crypto: JsonString(if pretty: json.pretty(cryptoField)
|
||||
else: $cryptoField),
|
||||
nextAccount: nextAccount.get(0))
|
||||
|
||||
proc createWalletContent*(T: type[KdfParams],
|
||||
mnemonic: Mnemonic,
|
||||
name = WalletName "",
|
||||
salt: openarray[byte] = @[],
|
||||
iv: openarray[byte] = @[],
|
||||
password = KeyStorePass "",
|
||||
nextAccount = none(Natural),
|
||||
pretty = true): (UUID, WalletContent) =
|
||||
let wallet = createWallet(T, mnemonic, name, salt, iv, password, nextAccount, pretty)
|
||||
(wallet.uuid, WalletContent Json.encode(wallet, pretty = pretty))
|
||||
|
||||
proc restoreCredentials*(mnemonic: Mnemonic,
|
||||
password = KeyStorePass ""): Credentials =
|
||||
let
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit aac25d1610a8fc15ae90fdd2a0b38ed60ea412fb
|
||||
Subproject commit a76faa5eec2454309753bbe99a68fb3cc25782f1
|
Loading…
Reference in New Issue