diff --git a/Makefile b/Makefile index 4c6346e82..601592fe8 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 0d2c4c83a..28a17b14f 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -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: diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 4bea3e0e9..9e2f26d8a 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -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" diff --git a/beacon_chain/deposit_contract.nim b/beacon_chain/deposit_contract.nim index 1198da365..8be4f1438 100644 --- a/beacon_chain/deposit_contract.nim +++ b/beacon_chain/deposit_contract.nim @@ -1,17 +1,20 @@ import - os, strutils, options, json, - chronos, confutils, web3, stint, - eth/keys + os, strutils, options, json, terminal, random, + chronos, chronicles, confutils, web3, web3/confutils_defs, stint, eth/keys, + spec/[datatypes, crypto], ssz/merkleization, keystore_management # Compiled version of /scripts/depositContract.v.py in this repo # The contract was compiled in Remix (https://remix.ethereum.org/) with vyper (remote) compiler. -const contractCode = "0x740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a052341561009857600080fd5b336003556101406000601f818352015b600061014051602081106100bb57600080fd5b600260c052602060c020015460208261016001015260208101905061014051602081106100e757600080fd5b600260c052602060c020015460208261016001015260208101905080610160526101609050602060c0825160208401600060025af161012557600080fd5b60c0519050606051600161014051018060405190131561014457600080fd5b809190121561015257600080fd5b6020811061015f57600080fd5b600260c052602060c02001555b81516001018083528114156100a8575b505061129656600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a052600015610277575b6101605261014052600061018052610140516101a0526101c060006008818352015b61018051600860008112156100da578060000360020a82046100e1565b8060020a82025b905090506101805260ff6101a051166101e052610180516101e0516101805101101561010c57600080fd5b6101e0516101805101610180526101a0517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff86000811215610155578060000360020a820461015c565b8060020a82025b905090506101a0525b81516001018083528114156100bd575b50506018600860208206610200016020828401111561019357600080fd5b60208061022082610180600060046015f15050818152809050905090508051602001806102c0828460006004600a8704601201f16101d057600080fd5b50506102c05160206001820306601f82010390506103206102c0516008818352015b826103205111156102025761021e565b6000610320516102e001535b81516001018083528114156101f2575b50505060206102a05260406102c0510160206001820306601f8201039050610280525b6000610280511115156102535761026f565b602061028051036102a001516020610280510361028052610241565b610160515650005b63c5f2892f600051141561050957341561029057600080fd5b6000610140526101405161016052600154610180526101a060006020818352015b60016001610180511614156103325760006101a051602081106102d357600080fd5b600060c052602060c02001546020826102400101526020810190506101605160208261024001015260208101905080610240526102409050602060c0825160208401600060025af161032457600080fd5b60c0519050610160526103a0565b6000610160516020826101c00101526020810190506101a0516020811061035857600080fd5b600260c052602060c02001546020826101c0010152602081019050806101c0526101c09050602060c0825160208401600060025af161039657600080fd5b60c0519050610160525b61018060026103ae57600080fd5b60028151048152505b81516001018083528114156102b1575b505060006101605160208261046001015260208101905061014051610160516101805163806732896102e05260015461030052610300516006580161009b565b506103605260006103c0525b6103605160206001820306601f82010390506103c0511015156104355761044e565b6103c05161038001526103c0516020016103c052610413565b61018052610160526101405261036060088060208461046001018260208501600060046012f150508051820191505060006018602082066103e0016020828401111561049957600080fd5b60208061040082610140600060046015f150508181528090509050905060188060208461046001018260208501600060046014f150508051820191505080610460526104609050602060c0825160208401600060025af16104f957600080fd5b60c051905060005260206000f350005b63621fd130600051141561061c57341561052257600080fd5b63806732896101405260015461016052610160516006580161009b565b506101c0526000610220525b6101c05160206001820306601f82010390506102205110151561056d57610586565b610220516101e00152610220516020016102205261054b565b6101c0805160200180610280828460006004600a8704601201f16105a957600080fd5b50506102805160206001820306601f82010390506102e0610280516008818352015b826102e05111156105db576105f7565b60006102e0516102a001535b81516001018083528114156105cb575b5050506020610260526040610280510160206001820306601f8201039050610260f350005b632289511860005114156110d957605060043560040161014037603060043560040135111561064a57600080fd5b60406024356004016101c037602060243560040135111561066a57600080fd5b608060443560040161022037606060443560040135111561068a57600080fd5b63ffffffff6001541061069c57600080fd5b633b9aca006102e0526102e0516106b257600080fd5b6102e05134046102c052633b9aca006102c05110156106d057600080fd5b603061014051146106e057600080fd5b60206101c051146106f057600080fd5b6060610220511461070057600080fd5b610140610360525b6103605151602061036051016103605261036061036051101561072a57610708565b6380673289610380526102c0516103a0526103a0516006580161009b565b50610400526000610460525b6104005160206001820306601f8201039050610460511015156107765761078f565b6104605161042001526104605160200161046052610754565b610340610360525b61036051526020610360510361036052610140610360511015156107ba57610797565b610400805160200180610300828460006004600a8704601201f16107dd57600080fd5b5050610140610480525b61048051516020610480510161048052610480610480511015610809576107e7565b63806732896104a0526001546104c0526104c0516006580161009b565b50610520526000610580525b6105205160206001820306601f8201039050610580511015156108545761086d565b6105805161054001526105805160200161058052610832565b610460610480525b610480515260206104805103610480526101406104805110151561089857610875565b6105208051602001806105a0828460006004600a8704601201f16108bb57600080fd5b505060a06106205261062051610660526101408051602001806106205161066001828460006004600a8704601201f16108f357600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516040818352015b83610600511015156109315761094e565b6000610600516020850101535b8151600101808352811415610920575b50505050602061062051610660015160206001820306601f82010390506106205101016106205261062051610680526101c08051602001806106205161066001828460006004600a8704601201f16109a557600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516020818352015b83610600511015156109e357610a00565b6000610600516020850101535b81516001018083528114156109d2575b50505050602061062051610660015160206001820306601f820103905061062051010161062052610620516106a0526103008051602001806106205161066001828460006004600a8704601201f1610a5757600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516020818352015b8361060051101515610a9557610ab2565b6000610600516020850101535b8151600101808352811415610a84575b50505050602061062051610660015160206001820306601f820103905061062051010161062052610620516106c0526102208051602001806106205161066001828460006004600a8704601201f1610b0957600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516060818352015b8361060051101515610b4757610b64565b6000610600516020850101535b8151600101808352811415610b36575b50505050602061062051610660015160206001820306601f820103905061062051010161062052610620516106e0526105a08051602001806106205161066001828460006004600a8704601201f1610bbb57600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516020818352015b8361060051101515610bf957610c16565b6000610600516020850101535b8151600101808352811415610be8575b50505050602061062051610660015160206001820306601f8201039050610620510101610620527f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c561062051610660a160006107005260006101406030806020846107c001018260208501600060046016f150508051820191505060006010602082066107400160208284011115610cad57600080fd5b60208061076082610700600060046015f15050818152809050905090506010806020846107c001018260208501600060046013f1505080518201915050806107c0526107c09050602060c0825160208401600060025af1610d0d57600080fd5b60c0519050610720526000600060406020820661086001610220518284011115610d3657600080fd5b606080610880826020602088068803016102200160006004601bf1505081815280905090509050602060c0825160208401600060025af1610d7657600080fd5b60c0519050602082610a600101526020810190506000604060206020820661092001610220518284011115610daa57600080fd5b606080610940826020602088068803016102200160006004601bf15050818152809050905090506020806020846109e001018260208501600060046015f1505080518201915050610700516020826109e0010152602081019050806109e0526109e09050602060c0825160208401600060025af1610e2757600080fd5b60c0519050602082610a6001015260208101905080610a6052610a609050602060c0825160208401600060025af1610e5e57600080fd5b60c0519050610840526000600061072051602082610b000101526020810190506101c0602080602084610b0001018260208501600060046015f150508051820191505080610b0052610b009050602060c0825160208401600060025af1610ec457600080fd5b60c0519050602082610c800101526020810190506000610300600880602084610c0001018260208501600060046012f15050805182019150506000601860208206610b800160208284011115610f1957600080fd5b602080610ba082610700600060046015f1505081815280905090509050601880602084610c0001018260208501600060046014f150508051820191505061084051602082610c0001015260208101905080610c0052610c009050602060c0825160208401600060025af1610f8c57600080fd5b60c0519050602082610c8001015260208101905080610c8052610c809050602060c0825160208401600060025af1610fc357600080fd5b60c0519050610ae052606435610ae05114610fdd57600080fd5b6001805460018254011015610ff157600080fd5b6001815401815550600154610d0052610d2060006020818352015b60016001610d005116141561104157610ae051610d20516020811061103057600080fd5b600060c052602060c02001556110d5565b6000610d20516020811061105457600080fd5b600060c052602060c0200154602082610d40010152602081019050610ae051602082610d4001015260208101905080610d4052610d409050602060c0825160208401600060025af16110a557600080fd5b60c0519050610ae052610d0060026110bc57600080fd5b60028151048152505b815160010180835281141561100c575b5050005b639890220b600051141561110d5734156110f257600080fd5b600060006000600030316003546000f161110b57600080fd5b005b60006000fd5b61018361129603610183600039610183611296036000f3" +const contractCode = staticRead "deposit_contract_code.txt" type + Eth1Address = web3.Address + StartUpCommand {.pure.} = enum deploy drain sendEth + makeDeposits CliConfig = object web3Url* {. @@ -23,21 +26,49 @@ type desc: "Private key of the controlling account" name: "private-key" }: string + askForKey {. + defaultValue: false + desc: "Ask for an Eth1 private key interactively" + name: "ask-for-key" }: bool + case cmd* {.command.}: StartUpCommand of deploy: discard of drain: - contractAddress* {. - defaultValue: "" + drainedContractAddress* {. desc: "Address of the contract to drain" - name: "deposit-contract" }: string + name: "deposit-contract" }: Eth1Address of sendEth: - toAddress {.name: "to".}: string + toAddress {.name: "to".}: Eth1Address valueEth {.name: "eth".}: string -contract(Deposit): + of makeDeposits: + depositsFile {. + desc: "A LaunchPad deposits file" + name: "deposits-file" }: InputFile + + depositContractAddress {. + desc: "Address of the deposit contract" + name: "deposit-contract" }: Eth1Address + + 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 + +contract(DepositContract): + proc deposit(pubkey: Bytes48, + withdrawalCredentials: Bytes32, + signature: Bytes96, + deposit_data_root: FixedBytes[32]) + proc drain() proc deployContract*(web3: Web3, code: string): Future[ReceiptObject] {.async.} = @@ -53,17 +84,83 @@ proc deployContract*(web3: Web3, code: string): Future[ReceiptObject] {.async.} let r = await web3.send(tr) result = await web3.getMinedTransactionReceipt(r) -proc sendEth(web3: Web3, to: string, valueEth: int): Future[TxHash] = +proc sendEth(web3: Web3, to: Eth1Address, valueEth: int): Future[TxHash] = let tr = EthSend( source: web3.defaultAccount, gas: Quantity(3000000).some, gasPrice: 1.some, value: some(valueEth.u256 * 1000000000000000000.u256), - to: Address.fromHex(to).some) + to: some(to)) web3.send(tr) +type + DelayGenerator* = proc(): chronos.Duration {.closure, gcsafe.} + +proc ethToWei(eth: UInt256): UInt256 = + eth * 1000000000000000000.u256 + +# TODO: async functions should note take `seq` inputs because +# this leads to full copies. +proc sendDeposits*(deposits: seq[LaunchPadDeposit], + web3Url, privateKey: string, + depositContractAddress: Eth1Address, + delayGenerator: DelayGenerator = nil) {.async.} = + info "Sending deposits", + web3 = web3Url, + depositContract = depositContractAddress + + 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] + + let depositContract = web3.contractSender(DepositContract, + Address depositContractAddress) + for i, dp in deposits: + let status = await depositContract.deposit( + Bytes48(dp.pubKey.toRaw()), + Bytes32(dp.withdrawal_credentials.data), + Bytes96(dp.signature.toRaw()), + FixedBytes[32](hash_tree_root(dp).data)).send(value = 32.u256.ethToWei, gasPrice = 1) + + info "Deposit sent", status = $status + + if delayGenerator != nil: + await sleepAsync(delayGenerator()) + proc main() {.async.} = - let cfg = CliConfig.load() + var cfg = CliConfig.load() + + var deposits: seq[LaunchPadDeposit] + if cfg.cmd == makeDeposits: + deposits = Json.loadFile(string cfg.depositsFile, seq[LaunchPadDeposit]) + + if cfg.askForKey: + var + privateKey: TaintedString + reasonForKey = "" + + if cfg.cmd == makeDeposits: + let + depositsWord = if deposits.len > 1: "deposits" else: "deposit" + totalEthNeeded = 32 * deposits.len + reasonForKey = " in order to make your $1 (you'll need access to $2 ETH)" % + [depositsWord, $totalEthNeeded] + + echo "Please enter your Goerli Eth1 private key in hex form (e.g. 0x1a2...f3c)" & + reasonForKey + + if not readPasswordFromStdin("> ", privateKey): + error "Failed to read an Eth1 private key from standard input" + + if privateKey.len > 0: + cfg.privateKey = privateKey.string + let web3 = await newWeb3(cfg.web3Url) if cfg.privateKey.len != 0: web3.privateKey = some(PrivateKey.fromHex(cfg.privateKey)[]) @@ -76,11 +173,26 @@ proc main() {.async.} = of StartUpCommand.deploy: let receipt = await web3.deployContract(contractCode) echo receipt.contractAddress.get, ";", receipt.blockHash + of StartUpCommand.drain: - let sender = web3.contractSender(Deposit, Address.fromHex(cfg.contractAddress)) + let sender = web3.contractSender(DepositContract, + cfg.drainedContractAddress) discard await sender.drain().send(gasPrice = 1) of StartUpCommand.sendEth: echo await sendEth(web3, cfg.toAddress, cfg.valueEth.parseInt) + of StartUpCommand.makeDeposits: + var delayGenerator: DelayGenerator + if cfg.maxDelay > 0.0: + delayGenerator = proc (): chronos.Duration {.gcsafe.} = + chronos.milliseconds (rand(cfg.minDelay..cfg.maxDelay)*1000).int + + if cfg.minDelay > cfg.maxDelay: + echo "The minimum delay should not be larger than the maximum delay" + quit 1 + + await sendDeposits(deposits, cfg.web3Url, cfg.privateKey, + cfg.depositContractAddress, delayGenerator) + when isMainModule: waitFor main() diff --git a/beacon_chain/deposit_contract_code.txt b/beacon_chain/deposit_contract_code.txt new file mode 100644 index 000000000..11ad01f1a --- /dev/null +++ b/beacon_chain/deposit_contract_code.txt @@ -0,0 +1 @@ +0x740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a052341561009857600080fd5b336003556101406000601f818352015b600061014051602081106100bb57600080fd5b600260c052602060c020015460208261016001015260208101905061014051602081106100e757600080fd5b600260c052602060c020015460208261016001015260208101905080610160526101609050602060c0825160208401600060025af161012557600080fd5b60c0519050606051600161014051018060405190131561014457600080fd5b809190121561015257600080fd5b6020811061015f57600080fd5b600260c052602060c02001555b81516001018083528114156100a8575b505061129656600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a052600015610277575b6101605261014052600061018052610140516101a0526101c060006008818352015b61018051600860008112156100da578060000360020a82046100e1565b8060020a82025b905090506101805260ff6101a051166101e052610180516101e0516101805101101561010c57600080fd5b6101e0516101805101610180526101a0517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff86000811215610155578060000360020a820461015c565b8060020a82025b905090506101a0525b81516001018083528114156100bd575b50506018600860208206610200016020828401111561019357600080fd5b60208061022082610180600060046015f15050818152809050905090508051602001806102c0828460006004600a8704601201f16101d057600080fd5b50506102c05160206001820306601f82010390506103206102c0516008818352015b826103205111156102025761021e565b6000610320516102e001535b81516001018083528114156101f2575b50505060206102a05260406102c0510160206001820306601f8201039050610280525b6000610280511115156102535761026f565b602061028051036102a001516020610280510361028052610241565b610160515650005b63c5f2892f600051141561050957341561029057600080fd5b6000610140526101405161016052600154610180526101a060006020818352015b60016001610180511614156103325760006101a051602081106102d357600080fd5b600060c052602060c02001546020826102400101526020810190506101605160208261024001015260208101905080610240526102409050602060c0825160208401600060025af161032457600080fd5b60c0519050610160526103a0565b6000610160516020826101c00101526020810190506101a0516020811061035857600080fd5b600260c052602060c02001546020826101c0010152602081019050806101c0526101c09050602060c0825160208401600060025af161039657600080fd5b60c0519050610160525b61018060026103ae57600080fd5b60028151048152505b81516001018083528114156102b1575b505060006101605160208261046001015260208101905061014051610160516101805163806732896102e05260015461030052610300516006580161009b565b506103605260006103c0525b6103605160206001820306601f82010390506103c0511015156104355761044e565b6103c05161038001526103c0516020016103c052610413565b61018052610160526101405261036060088060208461046001018260208501600060046012f150508051820191505060006018602082066103e0016020828401111561049957600080fd5b60208061040082610140600060046015f150508181528090509050905060188060208461046001018260208501600060046014f150508051820191505080610460526104609050602060c0825160208401600060025af16104f957600080fd5b60c051905060005260206000f350005b63621fd130600051141561061c57341561052257600080fd5b63806732896101405260015461016052610160516006580161009b565b506101c0526000610220525b6101c05160206001820306601f82010390506102205110151561056d57610586565b610220516101e00152610220516020016102205261054b565b6101c0805160200180610280828460006004600a8704601201f16105a957600080fd5b50506102805160206001820306601f82010390506102e0610280516008818352015b826102e05111156105db576105f7565b60006102e0516102a001535b81516001018083528114156105cb575b5050506020610260526040610280510160206001820306601f8201039050610260f350005b632289511860005114156110d957605060043560040161014037603060043560040135111561064a57600080fd5b60406024356004016101c037602060243560040135111561066a57600080fd5b608060443560040161022037606060443560040135111561068a57600080fd5b63ffffffff6001541061069c57600080fd5b633b9aca006102e0526102e0516106b257600080fd5b6102e05134046102c052633b9aca006102c05110156106d057600080fd5b603061014051146106e057600080fd5b60206101c051146106f057600080fd5b6060610220511461070057600080fd5b610140610360525b6103605151602061036051016103605261036061036051101561072a57610708565b6380673289610380526102c0516103a0526103a0516006580161009b565b50610400526000610460525b6104005160206001820306601f8201039050610460511015156107765761078f565b6104605161042001526104605160200161046052610754565b610340610360525b61036051526020610360510361036052610140610360511015156107ba57610797565b610400805160200180610300828460006004600a8704601201f16107dd57600080fd5b5050610140610480525b61048051516020610480510161048052610480610480511015610809576107e7565b63806732896104a0526001546104c0526104c0516006580161009b565b50610520526000610580525b6105205160206001820306601f8201039050610580511015156108545761086d565b6105805161054001526105805160200161058052610832565b610460610480525b610480515260206104805103610480526101406104805110151561089857610875565b6105208051602001806105a0828460006004600a8704601201f16108bb57600080fd5b505060a06106205261062051610660526101408051602001806106205161066001828460006004600a8704601201f16108f357600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516040818352015b83610600511015156109315761094e565b6000610600516020850101535b8151600101808352811415610920575b50505050602061062051610660015160206001820306601f82010390506106205101016106205261062051610680526101c08051602001806106205161066001828460006004600a8704601201f16109a557600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516020818352015b83610600511015156109e357610a00565b6000610600516020850101535b81516001018083528114156109d2575b50505050602061062051610660015160206001820306601f820103905061062051010161062052610620516106a0526103008051602001806106205161066001828460006004600a8704601201f1610a5757600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516020818352015b8361060051101515610a9557610ab2565b6000610600516020850101535b8151600101808352811415610a84575b50505050602061062051610660015160206001820306601f820103905061062051010161062052610620516106c0526102208051602001806106205161066001828460006004600a8704601201f1610b0957600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516060818352015b8361060051101515610b4757610b64565b6000610600516020850101535b8151600101808352811415610b36575b50505050602061062051610660015160206001820306601f820103905061062051010161062052610620516106e0526105a08051602001806106205161066001828460006004600a8704601201f1610bbb57600080fd5b505061062051610660015160206001820306601f8201039050610620516106600161060081516020818352015b8361060051101515610bf957610c16565b6000610600516020850101535b8151600101808352811415610be8575b50505050602061062051610660015160206001820306601f8201039050610620510101610620527f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c561062051610660a160006107005260006101406030806020846107c001018260208501600060046016f150508051820191505060006010602082066107400160208284011115610cad57600080fd5b60208061076082610700600060046015f15050818152809050905090506010806020846107c001018260208501600060046013f1505080518201915050806107c0526107c09050602060c0825160208401600060025af1610d0d57600080fd5b60c0519050610720526000600060406020820661086001610220518284011115610d3657600080fd5b606080610880826020602088068803016102200160006004601bf1505081815280905090509050602060c0825160208401600060025af1610d7657600080fd5b60c0519050602082610a600101526020810190506000604060206020820661092001610220518284011115610daa57600080fd5b606080610940826020602088068803016102200160006004601bf15050818152809050905090506020806020846109e001018260208501600060046015f1505080518201915050610700516020826109e0010152602081019050806109e0526109e09050602060c0825160208401600060025af1610e2757600080fd5b60c0519050602082610a6001015260208101905080610a6052610a609050602060c0825160208401600060025af1610e5e57600080fd5b60c0519050610840526000600061072051602082610b000101526020810190506101c0602080602084610b0001018260208501600060046015f150508051820191505080610b0052610b009050602060c0825160208401600060025af1610ec457600080fd5b60c0519050602082610c800101526020810190506000610300600880602084610c0001018260208501600060046012f15050805182019150506000601860208206610b800160208284011115610f1957600080fd5b602080610ba082610700600060046015f1505081815280905090509050601880602084610c0001018260208501600060046014f150508051820191505061084051602082610c0001015260208101905080610c0052610c009050602060c0825160208401600060025af1610f8c57600080fd5b60c0519050602082610c8001015260208101905080610c8052610c809050602060c0825160208401600060025af1610fc357600080fd5b60c0519050610ae052606435610ae05114610fdd57600080fd5b6001805460018254011015610ff157600080fd5b6001815401815550600154610d0052610d2060006020818352015b60016001610d005116141561104157610ae051610d20516020811061103057600080fd5b600060c052602060c02001556110d5565b6000610d20516020811061105457600080fd5b600060c052602060c0200154602082610d40010152602081019050610ae051602082610d4001015260208101905080610d4052610d409050602060c0825160208401600060025af16110a557600080fd5b60c0519050610ae052610d0060026110bc57600080fd5b60028151048152505b815160010180835281141561100c575b5050005b639890220b600051141561110d5734156110f257600080fd5b600060006000600030316003546000f161110b57600080fd5b005b60006000fd5b61018361129603610183600039610183611296036000f3 diff --git a/beacon_chain/keystore_management.nim b/beacon_chain/keystore_management.nim index 703873554..7828f8b85 100644 --- a/beacon_chain/keystore_management.nim +++ b/beacon_chain/keystore_management.nim @@ -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) diff --git a/beacon_chain/logtrace.nim b/beacon_chain/logtrace.nim index 1d1f5718d..508170116 100644 --- a/beacon_chain/logtrace.nim +++ b/beacon_chain/logtrace.nim @@ -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) diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index 19ecaded5..333da1232 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -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) + diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index 632ad0e9c..a60010661 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -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 diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index e74eadfad..6100bc903 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -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 diff --git a/scripts/launch_local_testnet.sh b/scripts/launch_local_testnet.sh index d71a24ac4..99f3ebe6e 100755 --- a/scripts/launch_local_testnet.sh +++ b/scripts/launch_local_testnet.sh @@ -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 & diff --git a/tests/simulation/start.sh b/tests/simulation/start.sh index c81c8df19..13fe3280c 100755 --- a/tests/simulation/start.sh +++ b/tests/simulation/start.sh @@ -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}" diff --git a/tests/simulation/vars.sh b/tests/simulation/vars.sh index 5cd28bffb..d7d011310 100644 --- a/tests/simulation/vars.sh +++ b/tests/simulation/vars.sh @@ -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" diff --git a/vendor/eth2-testnets b/vendor/eth2-testnets index f0dfca17c..6326a51b0 160000 --- a/vendor/eth2-testnets +++ b/vendor/eth2-testnets @@ -1 +1 @@ -Subproject commit f0dfca17cf2b27e390ebd18de1538d84db35f9ed +Subproject commit 6326a51b0b6e10ab5eab992c7b089c3b78e9241f diff --git a/vendor/nim-json-serialization b/vendor/nim-json-serialization index 1cf51931f..d5eb9427b 160000 --- a/vendor/nim-json-serialization +++ b/vendor/nim-json-serialization @@ -1 +1 @@ -Subproject commit 1cf51931f1037a2c44fa0912386273c01a0e0e42 +Subproject commit d5eb9427b85c7f234b2bec2d70b962dfade1fa35 diff --git a/vendor/nim-web3 b/vendor/nim-web3 index 04be80889..4fe1a81ce 160000 --- a/vendor/nim-web3 +++ b/vendor/nim-web3 @@ -1 +1 @@ -Subproject commit 04be808890ced3f47e9bb93267992f07c25acfbc +Subproject commit 4fe1a81ce7e16413b01467a2f88058127941170e