311 lines
10 KiB
Nim
311 lines
10 KiB
Nim
# beacon_chain
|
|
# Copyright (c) 2018-2024 Status Research & Development GmbH
|
|
# Licensed and distributed under either of
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
{.push raises: [].}
|
|
|
|
import
|
|
std/[os, sequtils, strutils, options, json, terminal],
|
|
chronos, chronicles, confutils, stint, json_serialization,
|
|
../filepath,
|
|
../networking/network_metadata,
|
|
web3, web3/confutils_defs, eth/keys, eth/p2p/discoveryv5/random2,
|
|
stew/[io2, byteutils],
|
|
../spec/eth2_merkleization,
|
|
../spec/datatypes/base,
|
|
../validators/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 =
|
|
hexToSeqByte staticRead "deposit_contract_code.txt"
|
|
|
|
type
|
|
Eth1Address = web3.Address
|
|
|
|
StartUpCommand {.pure.} = enum
|
|
deploy
|
|
sendEth
|
|
generateSimulationDeposits
|
|
sendDeposits
|
|
|
|
CliConfig = object
|
|
web3Url* {.
|
|
defaultValue: "",
|
|
desc: "URL of the Web3 server to observe Eth1"
|
|
name: "web3-url" }: string
|
|
|
|
privateKey* {.
|
|
defaultValue: ""
|
|
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
|
|
|
|
eth2Network* {.
|
|
desc: "The Eth2 network preset to use"
|
|
name: "network" }: Option[string]
|
|
|
|
case cmd* {.command.}: StartUpCommand
|
|
of deploy:
|
|
discard
|
|
|
|
of sendEth:
|
|
toAddress {.name: "to".}: Eth1Address
|
|
valueEth {.name: "eth".}: string
|
|
|
|
of generateSimulationDeposits:
|
|
simulationDepositsCount {.
|
|
desc: "The number of validator keystores to generate"
|
|
name: "count" }: Natural
|
|
|
|
outValidatorsDir {.
|
|
desc: "A directory to store the generated validator keystores"
|
|
name: "out-validators-dir" }: OutDir
|
|
|
|
outSecretsDir {.
|
|
desc: "A directory to store the generated keystore password files"
|
|
name: "out-secrets-dir" }: OutDir
|
|
|
|
outDepositsFile {.
|
|
desc: "A LaunchPad deposits file to write"
|
|
name: "out-deposits-file" }: OutFile
|
|
|
|
threshold {.
|
|
defaultValue: 1
|
|
desc: "Used to generate distributed keys"
|
|
name: "threshold" }: uint32
|
|
|
|
remoteValidatorsCount {.
|
|
defaultValue: 0
|
|
desc: "The number of distributed validators validator"
|
|
name: "remote-validators-count" }: uint32
|
|
|
|
remoteSignersUrls {.
|
|
desc: "URLs of the remote signers"
|
|
name: "remote-signer" }: seq[string]
|
|
|
|
of sendDeposits:
|
|
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
|
|
|
|
type
|
|
PubKeyBytes = DynamicBytes[48, 48]
|
|
WithdrawalCredentialsBytes = DynamicBytes[32, 32]
|
|
SignatureBytes = DynamicBytes[96, 96]
|
|
|
|
contract(DepositContract):
|
|
proc deposit(pubkey: PubKeyBytes,
|
|
withdrawalCredentials: WithdrawalCredentialsBytes,
|
|
signature: SignatureBytes,
|
|
deposit_data_root: FixedBytes[32])
|
|
|
|
proc deployContract*(web3: Web3, code: seq[byte]): Future[ReceiptObject] {.async.} =
|
|
let tr = TransactionArgs(
|
|
`from`: Opt.some web3.defaultAccount,
|
|
data: Opt.some code,
|
|
gas: Opt.some Quantity(3000000),
|
|
gasPrice: Opt.some Quantity(1))
|
|
|
|
let r = await web3.send(tr)
|
|
result = await web3.getMinedTransactionReceipt(r)
|
|
|
|
proc sendEth(web3: Web3, to: Eth1Address, valueEth: int): Future[TxHash] =
|
|
let tr = TransactionArgs(
|
|
`from`: Opt.some web3.defaultAccount,
|
|
# TODO: Force json-rpc to generate 'data' field
|
|
# should not be needed anymore, new execution-api schema
|
|
# is using `input` field
|
|
data: Opt.some(newSeq[byte]()),
|
|
gas: Opt.some Quantity(3000000),
|
|
gasPrice: Opt.some Quantity(1),
|
|
value: Opt.some(valueEth.u256 * 1000000000000000000.u256),
|
|
to: Opt.some(to))
|
|
web3.send(tr)
|
|
|
|
type
|
|
DelayGenerator* = proc(): chronos.Duration {.gcsafe, raises: [].}
|
|
|
|
proc ethToWei(eth: UInt256): UInt256 =
|
|
eth * 1000000000000000000.u256
|
|
|
|
proc initWeb3(web3Url, privateKey: string): Future[Web3] {.async.} =
|
|
result = await newWeb3(web3Url)
|
|
if privateKey.len != 0:
|
|
result.privateKey = Opt.some(PrivateKey.fromHex(privateKey)[])
|
|
else:
|
|
let accounts = await result.provider.eth_accounts()
|
|
doAssert(accounts.len > 0)
|
|
result.defaultAccount = accounts[0]
|
|
|
|
# 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.} =
|
|
notice "Sending deposits",
|
|
web3 = web3Url,
|
|
depositContract = depositContractAddress
|
|
|
|
var web3 = await initWeb3(web3Url, privateKey)
|
|
let gasPrice = int(await web3.provider.eth_gasPrice()) * 2
|
|
let depositContract = web3.contractSender(DepositContract,
|
|
Address depositContractAddress)
|
|
for i in 4200 ..< deposits.len:
|
|
let dp = deposits[i] as DepositData
|
|
|
|
while true:
|
|
try:
|
|
let tx = depositContract.deposit(
|
|
PubKeyBytes(@(dp.pubkey.toRaw())),
|
|
WithdrawalCredentialsBytes(@(dp.withdrawal_credentials.data)),
|
|
SignatureBytes(@(dp.signature.toRaw())),
|
|
FixedBytes[32](hash_tree_root(dp).data))
|
|
|
|
let status = await tx.send(value = 32.u256.ethToWei, gasPrice = gasPrice)
|
|
|
|
info "Deposit sent", tx = $status
|
|
|
|
if delayGenerator != nil:
|
|
await sleepAsync(delayGenerator())
|
|
|
|
break
|
|
except CatchableError:
|
|
await sleepAsync(60.seconds)
|
|
web3 = await initWeb3(web3Url, privateKey)
|
|
|
|
{.pop.} # TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError
|
|
proc main() {.async.} =
|
|
var conf = try: CliConfig.load()
|
|
except CatchableError as exc:
|
|
raise exc
|
|
except Exception as exc: # TODO fix confutils
|
|
raiseAssert exc.msg
|
|
|
|
let rng = HmacDrbgContext.new()
|
|
|
|
if conf.cmd == StartUpCommand.generateSimulationDeposits:
|
|
let
|
|
mnemonic = generateMnemonic(rng[])
|
|
seed = getSeed(mnemonic, KeystorePass.init "")
|
|
cfg = getRuntimeConfig(conf.eth2Network)
|
|
threshold = if conf.remoteSignersUrls.len > 0: conf.threshold
|
|
else: 0
|
|
|
|
if conf.remoteValidatorsCount > 0 and
|
|
conf.remoteSignersUrls.len == 0:
|
|
fatal "Please specify at least one remote signer URL"
|
|
quit 1
|
|
|
|
if (let res = secureCreatePath(string conf.outValidatorsDir); res.isErr):
|
|
warn "Could not create validators folder",
|
|
path = string conf.outValidatorsDir, err = ioErrorMsg(res.error)
|
|
|
|
if (let res = secureCreatePath(string conf.outSecretsDir); res.isErr):
|
|
warn "Could not create secrets folder",
|
|
path = string conf.outSecretsDir, err = ioErrorMsg(res.error)
|
|
|
|
let deposits = generateDeposits(
|
|
cfg,
|
|
rng[],
|
|
seed,
|
|
0, conf.simulationDepositsCount,
|
|
string conf.outValidatorsDir,
|
|
string conf.outSecretsDir,
|
|
conf.remoteSignersUrls,
|
|
threshold,
|
|
conf.remoteValidatorsCount,
|
|
KeystoreMode.Fast)
|
|
|
|
if deposits.isErr:
|
|
fatal "Failed to generate deposits", err = deposits.error
|
|
quit 1
|
|
|
|
let launchPadDeposits =
|
|
mapIt(deposits.value, LaunchPadDeposit.init(cfg, it))
|
|
|
|
Json.saveFile(string conf.outDepositsFile, launchPadDeposits)
|
|
notice "Deposit data written", filename = conf.outDepositsFile
|
|
quit 0
|
|
|
|
var deposits: seq[LaunchPadDeposit]
|
|
if conf.cmd == StartUpCommand.sendDeposits:
|
|
deposits = Json.loadFile(string conf.depositsFile, seq[LaunchPadDeposit])
|
|
|
|
if conf.askForKey:
|
|
var
|
|
privateKey: string # TODO consider using a SecretString type
|
|
reasonForKey = ""
|
|
|
|
if conf.cmd == StartUpCommand.sendDeposits:
|
|
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:
|
|
conf.privateKey = privateKey
|
|
|
|
let web3 = await initWeb3(conf.web3Url, conf.privateKey)
|
|
|
|
case conf.cmd
|
|
of StartUpCommand.deploy:
|
|
let receipt = await web3.deployContract(contractCode)
|
|
echo receipt.contractAddress.get, ";", receipt.blockHash
|
|
|
|
of StartUpCommand.sendEth:
|
|
echo await sendEth(web3, conf.toAddress, conf.valueEth.parseInt)
|
|
|
|
of StartUpCommand.sendDeposits:
|
|
var delayGenerator: DelayGenerator
|
|
if not (conf.maxDelay > 0.0):
|
|
conf.maxDelay = conf.minDelay
|
|
elif conf.minDelay > conf.maxDelay:
|
|
echo "The minimum delay should not be larger than the maximum delay"
|
|
quit 1
|
|
|
|
if conf.maxDelay > 0.0:
|
|
delayGenerator = proc (): chronos.Duration =
|
|
let
|
|
minDelay = (conf.minDelay*1000).int64
|
|
maxDelay = (conf.maxDelay*1000).int64
|
|
chronos.milliseconds (rng[].rand(maxDelay - minDelay) + minDelay)
|
|
|
|
await sendDeposits(deposits, conf.web3Url, conf.privateKey,
|
|
conf.depositContractAddress, delayGenerator)
|
|
|
|
of StartUpCommand.generateSimulationDeposits:
|
|
# This is handled above before the case statement
|
|
discard
|
|
|
|
when isMainModule: waitFor main()
|