Integrate the Wallet support with the Deposits creation; Produce Launchpad-compatible deposits

This commit is contained in:
Zahary Karadjov 2020-07-17 23:59:50 +03:00 committed by zah
parent fcd412f7a1
commit 40ea9e9886
16 changed files with 476 additions and 324 deletions

View File

@ -174,12 +174,20 @@ testnet0 testnet1: | beacon_node
clean-altona:
rm -rf build/data/shared_altona*
altona-deposit: | beacon_node
altona-deposit: | beacon_node deposit_contract
build/beacon_node deposits create \
--network=altona \
--count=$(VALIDATORS) \
--out-deposits-file=nbc-altona-deposits.json \
--count=$(VALIDATORS)
# TODO
# The --min-delay is needed only until we fix the invalid
# nonce generation on multiple transactions in web3
build/deposit_contract makeDeposits \
--web3-url=$(GOERLI_WEB3_URL) \
--deposit-contract=$$(cat vendor/eth2-testnets/shared/altona/deposit_contract.txt) \
--deposits-file=nbc-altona-deposits.json \
--ask-for-key \
--web3-url=$(GOERLI_WEB3_URL)
--min-delay=60
altona: | beacon_node
build/beacon_node \

View File

@ -7,12 +7,12 @@
import
# Standard library
algorithm, os, tables, strutils, times, math, terminal, bearssl, random,
algorithm, os, tables, strutils, sequtils, times, math, terminal,
# Nimble packages
stew/[objects, byteutils, endians2], stew/shims/macros,
chronos, confutils, metrics, json_rpc/[rpcserver, jsonmarshal],
chronicles,
chronicles, bearssl,
json_serialization/std/[options, sets, net], serialization/errors,
eth/[keys, async_utils],
@ -27,7 +27,7 @@ import
attestation_pool, block_pool, eth2_network, eth2_discovery,
beacon_node_common, beacon_node_types, block_pools/block_pools_types,
nimbus_binary_common, network_metadata,
mainchain_monitor, version, ssz/[merkleization], sszdump,
mainchain_monitor, version, ssz/[merkleization], sszdump, merkle_minimal,
sync_protocol, request_manager, keystore_management, interop, statusbar,
sync_manager, validator_duties, validator_api, attestation_aggregation
@ -1082,32 +1082,18 @@ programMain:
case config.cmd
of createTestnet:
var
depositDirs: seq[string]
deposits: seq[Deposit]
i = -1
for kind, dir in walkDir(config.testnetDepositsDir.string):
if kind != pcDir:
continue
let launchPadDeposits = try:
Json.loadFile(config.testnetDepositsFile.string, seq[LaunchPadDeposit])
except SerializationError as err:
error "Invalid LaunchPad deposits file",
err = formatMsg(err, config.testnetDepositsFile.string)
quit 1
inc i
if i < config.firstValidator.int:
continue
var deposits: seq[Deposit]
for i in config.firstValidator.int ..< launchPadDeposits.len:
deposits.add Deposit(data: launchPadDeposits[i] as DepositData)
depositDirs.add dir
# Add deposits, in order, to pass Merkle validation
sort(depositDirs, system.cmp)
for dir in depositDirs:
let depositFile = dir / "deposit.json"
try:
deposits.add Json.loadFile(depositFile, Deposit)
except SerializationError as err:
stderr.write "Error while loading a deposit file:\n"
stderr.write err.formatMsg(depositFile), "\n"
stderr.write "Please regenerate the deposit files by running 'beacon_node deposits create' again\n"
quit 1
attachMerkleProofs(deposits)
let
startTime = uint64(times.toUnix(times.getTime()) + config.genesisOffset)
@ -1188,18 +1174,26 @@ programMain:
of deposits:
case config.depositsCmd
of DepositsCmd.create:
if config.askForKey and config.depositPrivateKey.len == 0:
let
depositsWord = if config.totalDeposits > 1: "deposits"
else: "deposit"
totalEthNeeded = 32 * config.totalDeposits
var walletData = if config.existingWalletId.isSome:
let id = config.existingWalletId.get
let found = keystore_management.findWallet(config, id)
if found.isErr:
fatal "Unable to find wallet with the specified name/uuid",
id, err = found.error
quit 1
let unlocked = unlockWalletInteractively(found.get)
if unlocked.isOk:
unlocked.get
else:
quit 1
else:
let walletData = createWalletInteractively(rng[], config)
if walletData.isErr:
fatal "Unable to create wallet", err = walletData.error
quit 1
walletData.get
echo "Please enter your Goerli Eth1 private key in hex form " &
"(e.g. 0x1a2...f3c) in order to make your $1 (you'll need " &
"access to $2 GoETH)" % [depositsWord, $totalEthNeeded]
if not readPasswordFromStdin("> ", TaintedString config.depositPrivateKey):
error "Failed to read an Eth1 private key from standard input"
defer: burnMem(walletData.mnemonic)
createDir(config.outValidatorsDir)
createDir(config.outSecretsDir)
@ -1207,6 +1201,7 @@ programMain:
let deposits = generateDeposits(
config.runtimePreset,
rng[],
walletData,
config.totalDeposits,
config.outValidatorsDir,
config.outSecretsDir)
@ -1215,24 +1210,22 @@ programMain:
fatal "Failed to generate deposits", err = deposits.error
quit 1
if not config.dontSend:
waitFor sendDeposits(config, deposits.value)
try:
let depositDataPath = if config.outDepositsFile.isSome:
config.outDepositsFile.get.string
else:
config.outValidatorsDir / "deposit_data-" & $epochTime()
of DepositsCmd.send:
var delayGenerator: DelayGenerator
if config.maxDelay > 0.0:
delayGenerator = proc (): chronos.Duration {.gcsafe.} =
chronos.milliseconds (rand(config.minDelay..config.maxDelay)*1000).int
let launchPadDeposits =
mapIt(deposits.value, LaunchPadDeposit.init(config.runtimePreset, it))
if config.minDelay > config.maxDelay:
echo "The minimum delay should not be larger than the maximum delay"
Json.saveFile(depositDataPath, launchPadDeposits)
info "Deposit data written", filename = depositDataPath
except CatchableError as err:
error "Failed to create launchpad deposit data file", err = err.msg
quit 1
let deposits = loadDeposits(config.depositsDir)
waitFor sendDeposits(config, deposits, delayGenerator)
of DepositsCmd.status:
# TODO
echo "The status command is not implemented yet"
quit 1
@ -1241,7 +1234,7 @@ programMain:
of WalletsCmd.create:
let status = createWalletInteractively(rng[], config)
if status.isErr:
echo status.error
fatal "Unable to create wallet", err = status.error
quit 1
of WalletsCmd.list:

View File

@ -4,7 +4,7 @@ import
os, options,
chronicles, chronicles/options as chroniclesOptions,
confutils, confutils/defs, confutils/std/net,
stew/byteutils, json_serialization, web3/ethtypes,
json_serialization, web3/[ethtypes, confutils_defs],
network_metadata, spec/[crypto, keystore, digest, datatypes]
export
@ -27,7 +27,6 @@ type
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
@ -55,6 +54,10 @@ type
abbr: "d"
name: "data-dir" }: OutDir
walletsDirFlag* {.
desc: "A directory containing wallet files"
name: "wallets-dir" }: Option[InputDir]
web3Url* {.
defaultValue: ""
desc: "URL of the Web3 server to observe Eth1"
@ -126,10 +129,6 @@ type
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: "SSZ file specifying a recent state snapshot"
abbr: "s"
@ -211,9 +210,9 @@ type
name: "dump" }: bool
of createTestnet:
testnetDepositsDir* {.
desc: "Directory containing validator keystores"
name: "validators-dir" }: InputDir
testnetDepositsFile* {.
desc: "A LaunchPad deposits file for the genesis state validators"
name: "deposits-file" }: InputFile
totalValidators* {.
desc: "The number of validator deposits in the newly created chain"
@ -260,46 +259,37 @@ type
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]
outWalletsDirFlag* {.
desc: "A directory containing wallet files"
name: "wallets-dir" }: Option[OutDir]
createdWalletNameFlag* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "name"}: Option[WalletName]
createdWalletFile* {.
createdWalletFileFlag* {.
desc: "Output wallet file"
name: "out" }: Option[OutFile]
of WalletsCmd.restore:
restoredWalletName* {.
restoredWalletNameFlag* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "name"}: Option[WalletName]
restoredWalletFileFlag* {.
desc: "Output wallet file"
name: "out" }: Option[OutFile]
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:
depositPrivateKey* {.
defaultValue: ""
desc: "Private key of the controlling (sending) account",
name: "deposit-private-key" }: string
depositsDir* {.
defaultValue: "validators"
desc: "A folder with validator metadata created by the `deposits create` command"
@ -326,29 +316,17 @@ type
desc: "Output folder for randomly generated keystore passphrases"
name: "out-secrets-dir" }: string
askForKey* {.
defaultValue: false
desc: "Ask for an Eth1 private key used to fund the deposits"
name: "ask-for-key" }: bool
outDepositsFile* {.
desc: "The name of generated deposits file"
name: "out-deposits-file" }: Option[OutFile]
dontSend* {.
defaultValue: false,
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"
name: "dont-send" .}: bool
newWalletNameFlag* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "new-wallet-name"}: Option[WalletName]
of DepositsCmd.send:
minDelay* {.
defaultValue: 0.0
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)"
name: "max-delay" }: float
newWalletFileFlag* {.
desc: "Output wallet file"
name: "new-wallet-file" }: Option[OutFile]
of DepositsCmd.status:
discard
@ -439,20 +417,6 @@ proc createDumpDirs*(conf: BeaconNodeConf) =
# Dumping is mainly a debugging feature, so ignore these..
warn "Cannot create dump directories", msg = err.msg
func parseCmdArg*(T: type Eth1Address, input: TaintedString): T
{.raises: [ValueError, Defect].} =
fromHex(T, string input)
func completeCmdArg*(T: type Eth1Address, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type Eth1BlockHash, input: TaintedString): T
{.raises: [ValueError, Defect].} =
fromHex(T, string input)
func completeCmdArg*(T: type Eth1BlockHash, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type GraffitiBytes, input: TaintedString): T
{.raises: [ValueError, Defect].} =
GraffitiBytes.init(string input)
@ -478,18 +442,44 @@ func secretsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
string conf.secretsDirFlag.get(InputDir(conf.dataDir / "secrets"))
func walletsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
case conf.cmd
of noCommand:
if conf.walletsDirFlag.isSome:
return conf.walletsDirFlag.get.string
of wallets:
doAssert conf.walletsCmd == WalletsCmd.create
if conf.outWalletsDirFlag.isSome:
return conf.outWalletsDirFlag.get.string
if conf.walletsDirFlag.isSome:
conf.walletsDirFlag.get.string
else:
raiseAssert "Inappropraite call to walletsDir"
conf.dataDir / "wallets"
return conf.dataDir / "wallets"
func outWalletName*(conf: BeaconNodeConf): Option[WalletName] =
proc fail {.noReturn.} =
raiseAssert "outWalletName should be used only in the right context"
case conf.cmd
of wallets:
case conf.walletsCmd
of WalletsCmd.create: conf.createdWalletNameFlag
of WalletsCmd.restore: conf.restoredWalletNameFlag
of WalletsCmd.list: fail()
of deposits:
case conf.depositsCmd
of DepositsCmd.create: conf.newWalletNameFlag
else: fail()
else:
fail()
func outWalletFile*(conf: BeaconNodeConf): Option[OutFile] =
proc fail {.noReturn.} =
raiseAssert "outWalletName should be used only in the right context"
case conf.cmd
of wallets:
case conf.walletsCmd
of WalletsCmd.create: conf.createdWalletFileFlag
of WalletsCmd.restore: conf.restoredWalletFileFlag
of WalletsCmd.list: fail()
of deposits:
case conf.depositsCmd
of DepositsCmd.create: conf.newWalletFileFlag
else: fail()
else:
fail()
func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
conf.dataDir / "db"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,22 @@
import
std/[os, strutils, terminal, wordwrap],
std/[os, json, strutils, terminal, wordwrap],
stew/byteutils, chronicles, chronos, web3, stint, json_serialization,
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
spec/[datatypes, digest, crypto, keystore],
conf, ssz/merkleization, merkle_minimal, network_metadata
conf, ssz/merkleization, network_metadata
export
keystore
contract(DepositContract):
proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32])
{.push raises: [Defect].}
const
keystoreFileName* = "keystore.json"
depositFileName* = "deposit.json"
type
DelayGenerator* = proc(): chronos.Duration {.closure, gcsafe.}
{.push raises: [Defect].}
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
WalletDataForDeposits* = object
mnemonic*: Mnemonic
nextAccount*: Natural
proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf,
validatorsDir, keyName: string): Option[ValidatorPrivKey] =
@ -100,27 +95,40 @@ type
FailedToCreateValidatoDir
FailedToCreateSecretFile
FailedToCreateKeystoreFile
FailedToCreateDepositFile
proc generateDeposits*(preset: RuntimePreset,
rng: var BrHmacDrbgContext,
walletData: WalletDataForDeposits,
totalValidators: int,
validatorsDir: string,
secretsDir: string): Result[seq[Deposit], GenerateDepositsError] =
var deposits: seq[Deposit]
secretsDir: string): Result[seq[DepositData], GenerateDepositsError] =
var deposits: seq[DepositData]
info "Generating deposits", totalValidators, validatorsDir, secretsDir
for i in 0 ..< totalValidators:
let password = KeyStorePass getRandomBytes(rng, 32).toHex
let credentials = generateCredentials(rng, password = password)
let withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
# TODO: Explain why we are using an empty password
var withdrawalKey = keyFromPath(walletData.mnemonic, KeyStorePass"", withdrawalKeyPath)
defer: burnMem(withdrawalKey)
let withdrawalPubKey = withdrawalKey.toPubKey
for i in 0 ..< totalValidators:
let keyStoreIdx = walletData.nextAccount + i
let password = KeyStorePass getRandomBytes(rng, 32).toHex
let signingKeyPath = withdrawalKeyPath.append keyStoreIdx
var signingKey = deriveChildKey(withdrawalKey, keyStoreIdx)
defer: burnMem(signingKey)
let signingPubKey = signingKey.toPubKey
let keyStore = encryptKeystore(KdfPbkdf2, rng, signingKey,
password, signingKeyPath)
let
keyName = intToStr(i, 6) & "_" & $(credentials.signingKey.toPubKey)
keyName = $signingPubKey
validatorDir = validatorsDir / keyName
depositFile = validatorDir / depositFileName
keystoreFile = validatorDir / keystoreFileName
if existsDir(validatorDir) and existsFile(depositFile):
if existsDir(validatorDir):
continue
try: createDir validatorDir
@ -129,39 +137,13 @@ proc generateDeposits*(preset: RuntimePreset,
try: writeFile(secretsDir / keyName, password.string)
except IOError: return err FailedToCreateSecretFile
try: writeFile(keystoreFile, credentials.keyStore.string)
try: writeFile(keystoreFile, keyStore.string)
except IOError: return err FailedToCreateKeystoreFile
deposits.add credentials.prepareDeposit(preset)
# Does quadratic additional work, but fast enough, and otherwise more
# cleanly allows free intermixing of pre-existing and newly generated
# deposit and private key files. TODO: only generate new Merkle proof
# for the most recent deposit if this becomes bottleneck.
attachMerkleProofs(deposits)
try: Json.saveFile(depositFile, deposits[^1], pretty = true)
except: return err FailedToCreateDepositFile
deposits.add preset.prepareDeposit(withdrawalPubKey, signingKey, signingPubKey)
ok deposits
proc loadDeposits*(depositsDir: string): seq[Deposit] =
try:
for kind, dir in walkDir(depositsDir):
if kind == pcDir:
let depositFile = dir / depositFileName
try:
result.add Json.loadFile(depositFile, Deposit)
except IOError as err:
error "Failed to open deposit file", depositFile, err = err.msg
quit 1
except SerializationError as err:
error "Invalid deposit file", error = formatMsg(err, depositFile)
quit 1
except OSError as err:
error "Deposits directory not accessible",
path = depositsDir, err = err.msg
quit 1
const
minPasswordLen = 10
@ -170,12 +152,56 @@ const
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
minWordLength = minPasswordLen)
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
let
uuid = wallet.uuid
walletContent = WalletContent Json.encode(wallet, pretty = true)
try: createDir splitFile(outWalletPath).dir
except OSError, IOError:
let e = getCurrentException()
return err("failure to create wallet directory: " & e.msg)
try: writeFile(outWalletPath, string walletContent)
except IOError as e:
return err("failure to write file: " & e.msg)
ok()
proc readPasswordInput(prompt: string, password: var TaintedString): bool =
try: 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 createWalletInteractively*(
rng: var BrHmacDrbgContext,
conf: BeaconNodeConf): Result[OutFile, cstring] =
conf: BeaconNodeConf): Result[WalletDataForDeposits, string] =
if conf.nonInteractive:
return err "Wallets can be created only in interactive mode"
return err "not running in interactive mode"
var mnemonic = generateMnemonic(rng)
defer: burnMem(mnemonic)
@ -185,15 +211,11 @@ proc createWalletInteractively*(
stdout.write prompt, ": "
stdin.readLine()
except IOError:
return err "Failed to read data from stdin"
return err "failure to read data from stdin"
template echo80(msg: string) =
echo wrapWords(msg, 80)
proc readPasswordInput(prompt: string, password: var TaintedString): bool =
try: readPasswordFromStdin(prompt, password)
except IOError: false
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 " &
@ -201,13 +223,13 @@ proc createWalletInteractively*(
try:
echo ""
stdout.setStyle({styleBright})
stdout.setForegroundColor fgCyan
setStyleNoError({styleBright})
setForegroundColorNoError fgCyan
echo80 $mnemonic
stdout.resetAttributes()
resetAttributesNoError()
echo ""
except IOError, ValueError:
return err "Failed to write to the standard output"
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 " &
@ -220,7 +242,7 @@ proc createWalletInteractively*(
while true:
let answer = ask "Answer"
if answer == "":
return err "Wallet creation aborted"
return err "aborted wallet creation"
elif answer != "yes":
echo "To continue, please type 'yes' (without the quotes) or press enter to quit"
else:
@ -246,7 +268,7 @@ proc createWalletInteractively*(
while true:
if not readPasswordInput(prompt, password):
return err "Failed to read a password from stdin"
return err "failure to read a password from stdin"
if password.len < minPasswordLen:
try:
@ -263,15 +285,16 @@ proc createWalletInteractively*(
firstTry = false
if not readPasswordInput("Please repeat the password:", confirmedPassword):
return err "Failed to read a password from stdin"
return err "failure to read a password from stdin"
if password != confirmedPassword:
echo "Passwords don't match, please try again"
continue
var name: WalletName
if conf.createdWalletName.isSome:
name = conf.createdWalletName.get
let outWalletName = conf.outWalletName
if outWalletName.isSome:
name = outWalletName.get
else:
echo ""
echo80 "For your convenience, the wallet can be identified with a name " &
@ -287,74 +310,108 @@ proc createWalletInteractively*(
continue
break
let (uuid, walletContent) = KdfPbkdf2.createWalletContent(
rng, mnemonic,
name = name,
password = KeyStorePass password)
try:
var outWalletFile: OutFile
let wallet = KdfPbkdf2.createWallet(rng, mnemonic,
name = name,
password = KeyStorePass password)
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"))
let outWalletFileFlag = conf.outWalletFile
let outWalletFile = if outWalletFileFlag.isSome:
string outWalletFileFlag.get
else:
conf.walletsDir / addFileExt(string wallet.uuid, "json")
writeFile(string outWalletFile, string walletContent)
echo "Wallet file written to ", outWalletFile
return ok outWalletFile
except CatchableError as err:
return err "Failed to write wallet file"
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)
finally:
burnMem(password)
burnMem(confirmedPassword)
{.pop.}
proc loadWallet*(fileName: string): Result[Wallet, string] =
try:
ok Json.loadFile(fileName, Wallet)
except CatchableError as e:
err e.msg
# TODO: async functions should note take `seq` inputs because
# this leads to full copies.
proc sendDeposits*(deposits: seq[Deposit],
web3Url, privateKey: string,
depositContractAddress: Eth1Address,
delayGenerator: DelayGenerator = nil) {.async.} =
var web3 = await newWeb3(web3Url)
if privateKey.len != 0:
web3.privateKey = some(PrivateKey.fromHex(privateKey).tryGet)
else:
let accounts = await web3.provider.eth_accounts()
if accounts.len == 0:
error "No account offered by the web3 provider", web3Url
return
web3.defaultAccount = accounts[0]
proc unlockWalletInteractively*(wallet: Wallet): Result[WalletDataForDeposits, string] =
var json: JsonNode
try:
json = parseJson wallet.crypto.string
except Exception as e: # TODO: parseJson shouldn't raise general `Exception`
if e[] of Defect: raise (ref Defect)(e)
else: return err "failure to parse crypto field"
let depositContract = web3.contractSender(DepositContract,
Address depositContractAddress)
for i, dp in deposits:
let status = await depositContract.deposit(
Bytes48(dp.data.pubKey.toRaw()),
Bytes32(dp.data.withdrawal_credentials.data),
Bytes96(dp.data.signature.toRaw()),
FixedBytes[32](hash_tree_root(dp.data).data)).send(value = 32.u256.ethToWei, gasPrice = 1)
var password: TaintedString
info "Deposit sent", status = $status
echo "Please enter the password for unlocking the wallet"
if delayGenerator != nil:
await sleepAsync(delayGenerator())
for i in 1..3:
try:
if not readPasswordInput("Password: ", password):
return err "failure to read password from stdin"
proc sendDeposits*(config: BeaconNodeConf,
deposits: seq[Deposit],
delayGenerator: DelayGenerator = nil) {.async.} =
info "Sending deposits",
web3 = config.web3Url,
depositContract = config.depositContractAddress
var status = decryptoCryptoField(json, KeyStorePass password)
if status.isOk:
defer: burnMem(status.value)
return ok WalletDataForDeposits(
mnemonic: Mnemonic string.fromBytes(status.value))
else:
echo "Unlocking of the wallet failed. Please try again."
finally:
burnMem(password)
await sendDeposits(
deposits,
config.web3Url,
config.depositPrivateKey,
config.depositContractAddress.get,
delayGenerator)
return err "failure to unlock wallet"
proc findWallet*(config: BeaconNodeConf, name: WalletName): Result[Wallet, string] =
var walletFiles = newSeq[string]()
try:
for kind, walletFile in walkDir(config.walletsDir):
if kind != pcFile: continue
let fullPath = config.walletsDir / walletFile
if walletFile == name.string:
return loadWallet(fullPath)
walletFiles.add fullPath
except OSError:
return err "failure to list wallet directory"
for walletFile in walletFiles:
let wallet = loadWallet(walletFile)
if wallet.isOk and wallet.get.name == name:
return wallet
return err "failure to locate wallet file"
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,
preset: RuntimePreset, 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),
fork_version: preset.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)

View File

@ -136,16 +136,16 @@ proc readLogFileForAttsMessages(file: string): seq[SlotAttMessage] =
try:
while not(stream.atEnd()):
line = stream.readLine()
let m = Json.decode(line, LogMessage, forwardCompatible = true)
let m = Json.decode(line, LogMessage, allowUnknownFields = true)
if m.msg == "Attestation sent":
let am = Json.decode(line, AttestationSentMessage,
forwardCompatible = true)
allowUnknownFields = true)
let m = SlotAttMessage(kind: SaMessageType.AttestationSent,
asmsg: am)
res.add(m)
elif m.msg == "Slot start":
let sm = Json.decode(line, SlotStartMessage,
forwardCompatible = true)
allowUnknownFields = true)
let m = SlotAttMessage(kind: SaMessageType.SlotStart,
ssmsg: sm)
res.add(m)

View File

@ -343,3 +343,7 @@ func init*(T: typedesc[ValidatorSig], data: array[RawSigSize, byte]): T {.noInit
if v.isErr:
raise (ref ValueError)(msg: $v.error)
v[]
proc burnMem*(key: var ValidatorPrivKey) =
key = default(ValidatorPrivKey)

View File

@ -544,6 +544,11 @@ proc `<`*(x, y: ValidatorIndex) : bool {.borrow.}
proc hash*(x: ValidatorIndex): Hash {.borrow.}
func `$`*(x: ValidatorIndex): auto = $(x.int64)
func `as`*(d: DepositData, T: type DepositMessage): T =
T(pubkey: d.pubkey,
withdrawal_credentials: d.withdrawal_credentials,
amount: d.amount)
ethTimeUnit Slot
ethTimeUnit Epoch

View File

@ -13,7 +13,7 @@ import
./datatypes, ./crypto, ./digest, ./signatures
export
results
results, burnMem
{.push raises: [Defect].}
@ -127,6 +127,12 @@ WalletName.serializesAsBaseIn Json
template `$`*(m: Mnemonic): string =
string(m)
template `==`*(lhs, rhs: WalletName): bool =
string(lhs) == string(rhs)
template `$`*(x: WalletName): string =
string(x)
template burnMem*(m: var (SensitiveData|TaintedString)) =
# TODO: `burnMem` in nimcrypto could use distinctBase
# to make its usage less error-prone.
@ -450,48 +456,21 @@ proc createWalletContent*(T: type[KdfParams],
T, rng, mnemonic, name, salt, iv, password, nextAccount, pretty)
(wallet.uuid, WalletContent Json.encode(wallet, pretty = pretty))
proc restoreCredentials*(rng: var BrHmacDrbgContext,
mnemonic: Mnemonic,
password = KeyStorePass ""): Credentials =
let
withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
withdrawalKey = keyFromPath(mnemonic, password, withdrawalKeyPath)
signingKeyPath = withdrawalKeyPath.append 0
signingKey = deriveChildKey(withdrawalKey, 0)
Credentials(
mnemonic: mnemonic,
keyStore: encryptKeystore(KdfPbkdf2, rng, signingKey, password, signingKeyPath),
signingKey: signingKey,
withdrawalKey: withdrawalKey)
proc generateCredentials*(rng: var BrHmacDrbgContext,
entropy: openarray[byte] = @[],
password = KeyStorePass ""): Credentials =
let mnemonic = generateMnemonic(rng, englishWords, entropy)
restoreCredentials(rng, mnemonic, password)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/deposit-contract.md#withdrawal-credentials
proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
var bytes = eth2digest(k.toRaw())
bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8
bytes
proc prepareDeposit*(credentials: Credentials,
preset: RuntimePreset,
amount = MAX_EFFECTIVE_BALANCE.Gwei): Deposit =
let
withdrawalPubKey = credentials.withdrawalKey.toPubKey
signingPubKey = credentials.signingKey.toPubKey
proc prepareDeposit*(preset: RuntimePreset,
withdrawalPubKey: ValidatorPubKey,
signingKey: ValidatorPrivKey, signingPubKey: ValidatorPubKey,
amount = MAX_EFFECTIVE_BALANCE.Gwei): DepositData =
var res = DepositData(
amount: amount,
pubkey: signingPubKey,
withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey))
var
ret = Deposit(
data: DepositData(
amount: amount,
pubkey: signingPubKey,
withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey)))
res.signature = preset.get_deposit_signature(res, signingKey)
return res
ret.data.signature = preset.get_deposit_signature(ret.data,
credentials.signingKey)
ret

View File

@ -121,6 +121,8 @@ NETWORK="testnet${TESTNET}"
rm -rf "${DATA_DIR}"
DEPOSITS_FILE="${DATA_DIR}/deposits.json"
DEPOSITS_DIR="${DATA_DIR}/deposits_dir"
mkdir -p "${DEPOSITS_DIR}"
@ -154,9 +156,10 @@ NETWORK_METADATA_FILE="${DATA_DIR}/network.json"
./build/beacon_node deposits create \
--count=${TOTAL_VALIDATORS} \
--non-interactive \
--out-deposits-dir="${DEPOSITS_DIR}" \
--out-secrets-dir="${SECRETS_DIR}" \
--dont-send
--out-deposits-file="${DEPOSITS_FILE}"
if [[ $USE_GANACHE == "0" ]]; then
GENESIS_OFFSET=30
@ -197,9 +200,8 @@ else
BOOTSTRAP_TIMEOUT=$(( MAX_DELAY * TOTAL_VALIDATORS ))
./build/beacon_node deposits send \
--non-interactive \
--deposits-dir="${DEPOSITS_DIR}" \
./build/deposit_contract makeDeposits \
--deposits-file="${DEPOSITS_FILE}" \
--min-delay=$MIN_DELAY --max-delay=$MAX_DELAY \
$WEB3_ARG \
--deposit-contract=${DEPOSIT_CONTRACT_ADDRESS} > "${DATA_DIR}/log_deposit_maker.txt" 2>&1 &

View File

@ -101,22 +101,23 @@ fi
$MAKE -j2 --no-print-directory NIMFLAGS="$CUSTOM_NIMFLAGS $DEFS" LOG_LEVEL="${LOG_LEVEL:-DEBUG}" beacon_node validator_client
count_files () {
{ ls -1q $1 2> /dev/null || true ; } | wc -l
}
EXISTING_VALIDATORS=0
if [[ -f "$DEPOSITS_FILE" ]]; then
# We count the number of deposits by counting the number of
# occurrences of the 'deposit_data_root' field:
EXISTING_VALIDATORS=$(grep -o -i deposit_data_root "$DEPOSITS_FILE" | wc -l)
fi
EXISTING_VALIDATORS=$(count_files "$VALIDATORS_DIR/*/deposit.json")
if [[ $EXISTING_VALIDATORS -lt $NUM_VALIDATORS ]]; then
if [[ $EXISTING_VALIDATORS -ne $NUM_VALIDATORS ]]; then
rm -rf "$VALIDATORS_DIR"
rm -rf "$SECRETS_DIR"
$BEACON_NODE_BIN deposits create \
--count="${NUM_VALIDATORS}" \
--non-interactive \
--new-wallet-file="${SIMULATION_DIR}/wallet.json" \
--out-deposits-dir="$VALIDATORS_DIR" \
--out-secrets-dir="$SECRETS_DIR" \
--dont-send
--out-deposits-file="$DEPOSITS_FILE"
echo "All deposits prepared"
fi
@ -128,7 +129,7 @@ if [ ! -f "${SNAPSHOT_FILE}" ]; then
--data-dir="${SIMULATION_DIR}/node-$BOOTSTRAP_NODE" \
createTestnet \
$WEB3_ARG \
--validators-dir="${VALIDATORS_DIR}" \
--deposits-file="${DEPOSITS_FILE}" \
--total-validators="${NUM_VALIDATORS}" \
--output-genesis="${SNAPSHOT_FILE}" \
--output-bootstrap-file="${NETWORK_BOOTSTRAP_FILE}" \
@ -167,7 +168,7 @@ if [ "$USE_GANACHE" != "no" ]; then
make deposit_contract
echo Deploying the validator deposit contract...
DEPLOY_CMD_OUTPUT=$($DEPLOY_DEPOSIT_CONTRACT_BIN deploy $WEB3_ARG)
DEPLOY_CMD_OUTPUT=$($DEPOSIT_CONTRACT_BIN deploy $WEB3_ARG)
# https://stackoverflow.com/questions/918886/how-do-i-split-a-string-on-a-delimiter-in-bash
OUTPUT_PIECES=(${DEPLOY_CMD_OUTPUT//;/ })
DEPOSIT_CONTRACT_ADDRESS=${OUTPUT_PIECES[0]}
@ -176,9 +177,8 @@ if [ "$USE_GANACHE" != "no" ]; then
echo Contract deployed at $DEPOSIT_CONTRACT_ADDRESS:$DEPOSIT_CONTRACT_BLOCK
if [[ "$WAIT_GENESIS" == "yes" ]]; then
run_cmd "(deposit maker)" "$BEACON_NODE_BIN deposits send \
--non-interactive \
--deposits-dir='$VALIDATORS_DIR' \
run_cmd "(deposit maker)" "$DEPOSIT_CONTRACT_BIN makeDeposits \
--deposits-file='$DEPOSITS_FILE' \
--min-delay=0 --max-delay=1 \
$WEB3_ARG \
--deposit-contract=${DEPOSIT_CONTRACT_ADDRESS}"

View File

@ -34,9 +34,10 @@ SNAPSHOT_FILE="${SIMULATION_DIR}/state_snapshot.ssz"
NETWORK_BOOTSTRAP_FILE="${SIMULATION_DIR}/bootstrap_nodes.txt"
BEACON_NODE_BIN="${GIT_ROOT}/build/beacon_node"
VALIDATOR_CLIENT_BIN="${GIT_ROOT}/build/validator_client"
DEPLOY_DEPOSIT_CONTRACT_BIN="${GIT_ROOT}/build/deposit_contract"
DEPOSIT_CONTRACT_BIN="${GIT_ROOT}/build/deposit_contract"
BOOTSTRAP_ENR_FILE="${SIMULATION_DIR}/node-${BOOTSTRAP_NODE}/beacon_node.enr"
NETWORK_METADATA_FILE="${SIMULATION_DIR}/network.json"
DEPOSITS_FILE="${SIMULATION_DIR}/deposits.json"
if [[ "$USE_GANACHE" == "yes" ]]; then
WEB3_ARG="--web3-url=ws://localhost:8545"

@ -1 +1 @@
Subproject commit f0dfca17cf2b27e390ebd18de1538d84db35f9ed
Subproject commit 6326a51b0b6e10ab5eab992c7b089c3b78e9241f

@ -1 +1 @@
Subproject commit 1cf51931f1037a2c44fa0912386273c01a0e0e42
Subproject commit d5eb9427b85c7f234b2bec2d70b962dfade1fa35

2
vendor/nim-web3 vendored

@ -1 +1 @@
Subproject commit 04be808890ced3f47e9bb93267992f07c25acfbc
Subproject commit 4fe1a81ce7e16413b01467a2f88058127941170e