732 lines
26 KiB
Nim
732 lines
26 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/[json, options, times],
|
|
chronos, bearssl/rand, chronicles, confutils, stint, json_serialization,
|
|
web3, eth/keys, eth/p2p/discoveryv5/random2,
|
|
stew/[io2, byteutils], json_rpc/jsonmarshal,
|
|
../beacon_chain/conf,
|
|
../beacon_chain/el/el_manager,
|
|
../beacon_chain/networking/eth2_network,
|
|
../beacon_chain/spec/eth2_merkleization,
|
|
../beacon_chain/spec/datatypes/base,
|
|
../beacon_chain/spec/eth2_apis/eth2_rest_serialization,
|
|
../beacon_chain/validators/keystore_management
|
|
|
|
from std/os import changeFileExt, fileExists
|
|
from std/times import toUnix
|
|
from ../beacon_chain/el/engine_api_conversions import asEth2Digest
|
|
from ../beacon_chain/spec/beaconstate import initialize_beacon_state_from_eth1
|
|
from ../tests/mocking/mock_genesis import mockEth1BlockHash
|
|
|
|
# For nim-confutils, which uses this kind of init(Type, value) pattern
|
|
func init(T: type IpAddress, ip: IpAddress): T = ip
|
|
|
|
type
|
|
Eth1Address = web3.Address
|
|
|
|
StartUpCommand {.pure.} = enum
|
|
generateDeposits
|
|
createTestnet
|
|
createTestnetEnr
|
|
run
|
|
sendDeposits
|
|
deployDepositContract
|
|
sendEth
|
|
|
|
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 StartUpCommand.deployDepositContract:
|
|
discard
|
|
|
|
of StartUpCommand.sendEth:
|
|
toAddress* {.name: "to".}: Eth1Address
|
|
valueEth* {.name: "eth".}: string
|
|
|
|
of StartUpCommand.generateDeposits:
|
|
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 StartUpCommand.createTestnet:
|
|
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"
|
|
name: "total-validators" .}: uint64
|
|
|
|
bootstrapAddress* {.
|
|
desc: "The public IP address that will be advertised as a bootstrap node for the testnet"
|
|
defaultValue: defaultAdminListenAddress
|
|
defaultValueDesc: $defaultAdminListenAddressDesc
|
|
name: "bootstrap-address" .}: IpAddress
|
|
|
|
bootstrapPort* {.
|
|
desc: "The TCP/UDP port that will be used by the bootstrap node"
|
|
defaultValue: defaultEth2TcpPort
|
|
defaultValueDesc: $defaultEth2TcpPortDesc
|
|
name: "bootstrap-port" .}: Port
|
|
|
|
dataDir* {.
|
|
desc: "Nimbus data directory where the keys of the bootstrap node will be placed"
|
|
name: "data-dir" .}: OutDir
|
|
|
|
netKeyFile* {.
|
|
desc: "Source of network (secp256k1) private key file"
|
|
name: "netkey-file" .}: OutFile
|
|
|
|
netKeyInsecurePassword* {.
|
|
desc: "Use pre-generated INSECURE password for network private key file"
|
|
defaultValue: false,
|
|
name: "insecure-netkey-password" .}: bool
|
|
|
|
genesisTime* {.
|
|
desc: "Unix epoch time of the network genesis"
|
|
name: "genesis-time" .}: Option[uint64]
|
|
|
|
genesisOffset* {.
|
|
desc: "Seconds from now to add to genesis time"
|
|
name: "genesis-offset" .}: Option[int]
|
|
|
|
executionGenesisBlock* {.
|
|
desc: "The execution genesis block in a merged testnet"
|
|
name: "execution-genesis-block" .}: Option[InputFile]
|
|
|
|
capellaForkEpoch* {.
|
|
defaultValue: FAR_FUTURE_EPOCH
|
|
desc: "The epoch of the Capella hard-fork"
|
|
name: "capella-fork-epoch" .}: Epoch
|
|
|
|
denebForkEpoch* {.
|
|
defaultValue: FAR_FUTURE_EPOCH
|
|
desc: "The epoch of the Deneb hard-fork"
|
|
name: "deneb-fork-epoch" .}: Epoch
|
|
|
|
electraForkEpoch* {.
|
|
defaultValue: FAR_FUTURE_EPOCH
|
|
desc: "The epoch of the Electra hard-fork"
|
|
name: "electra-fork-epoch" .}: Epoch
|
|
|
|
fuluForkEpoch* {.
|
|
defaultValue: FAR_FUTURE_EPOCH
|
|
desc: "The epoch of the Fulu hard-fork"
|
|
name: "fulu-fork-epoch" .}: Epoch
|
|
|
|
outputGenesis* {.
|
|
desc: "Output file where to write the initial state snapshot"
|
|
name: "output-genesis" .}: OutFile
|
|
|
|
outputDepositTreeSnapshot* {.
|
|
desc: "Output file where to write the initial deposit tree snapshot"
|
|
name: "output-deposit-tree-snapshot" .}: OutFile
|
|
|
|
outputBootstrapFile* {.
|
|
desc: "Output file with list of bootstrap nodes for the network"
|
|
name: "output-bootstrap-file" .}: OutFile
|
|
|
|
of StartUpCommand.createTestnetEnr:
|
|
inputBootstrapEnr* {.
|
|
desc: "Path to the bootstrap ENR"
|
|
name: "bootstrap-enr" .}: InputFile
|
|
|
|
enrDataDir* {.
|
|
desc: "Nimbus data directory where the keys of the node will be placed"
|
|
name: "data-dir" .}: OutDir
|
|
|
|
enrNetKeyFile* {.
|
|
desc: "Source of network (secp256k1) private key file"
|
|
name: "enr-netkey-file" .}: OutFile
|
|
|
|
enrNetKeyInsecurePassword* {.
|
|
desc: "Use pre-generated INSECURE password for network private key file"
|
|
defaultValue: false,
|
|
name: "insecure-netkey-password" .}: bool
|
|
|
|
enrAddress* {.
|
|
desc: "The public IP address of that ENR"
|
|
defaultValue: defaultAdminListenAddress
|
|
defaultValueDesc: $defaultAdminListenAddressDesc
|
|
name: "enr-address" .}: IpAddress
|
|
|
|
enrPort* {.
|
|
desc: "The TCP/UDP port of that ENR"
|
|
defaultValue: defaultEth2TcpPort
|
|
defaultValueDesc: $defaultEth2TcpPortDesc
|
|
name: "enr-port" .}: Port
|
|
|
|
of StartUpCommand.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
|
|
|
|
of StartUpCommand.run:
|
|
discard
|
|
|
|
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: web3.FixedBytes[32])
|
|
|
|
template `as`(address: Eth1Address, T: type bellatrix.ExecutionAddress): T =
|
|
T(data: distinctBase(address))
|
|
|
|
template `as`(address: BlockHash, T: type Eth2Digest): T =
|
|
asEth2Digest(address)
|
|
|
|
func getOrDefault[T](x: Opt[T]): T =
|
|
if x.isSome:
|
|
x.get
|
|
else:
|
|
default T
|
|
|
|
func `as`(blk: BlockObject, T: type bellatrix.ExecutionPayloadHeader): T =
|
|
T(parent_hash: blk.parentHash as Eth2Digest,
|
|
fee_recipient: blk.miner as ExecutionAddress,
|
|
state_root: blk.stateRoot as Eth2Digest,
|
|
receipts_root: blk.receiptsRoot as Eth2Digest,
|
|
logs_bloom: BloomLogs(data: distinctBase(blk.logsBloom)),
|
|
prev_randao: Eth2Digest(data: blk.difficulty.toByteArrayBE), # Is BE correct here?
|
|
block_number: uint64 blk.number,
|
|
gas_limit: uint64 blk.gasLimit,
|
|
gas_used: uint64 blk.gasUsed,
|
|
timestamp: uint64 blk.timestamp,
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(blk.extraData.bytes),
|
|
base_fee_per_gas: blk.baseFeePerGas.getOrDefault(),
|
|
block_hash: blk.hash as Eth2Digest,
|
|
transactions_root: blk.transactionsRoot as Eth2Digest)
|
|
|
|
func `as`(blk: BlockObject, T: type capella.ExecutionPayloadHeader): T =
|
|
T(parent_hash: blk.parentHash as Eth2Digest,
|
|
fee_recipient: blk.miner as ExecutionAddress,
|
|
state_root: blk.stateRoot as Eth2Digest,
|
|
receipts_root: blk.receiptsRoot as Eth2Digest,
|
|
logs_bloom: BloomLogs(data: distinctBase(blk.logsBloom)),
|
|
prev_randao: Eth2Digest(data: blk.difficulty.toByteArrayBE),
|
|
block_number: uint64 blk.number,
|
|
gas_limit: uint64 blk.gasLimit,
|
|
gas_used: uint64 blk.gasUsed,
|
|
timestamp: uint64 blk.timestamp,
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(blk.extraData.bytes),
|
|
base_fee_per_gas: blk.baseFeePerGas.getOrDefault(),
|
|
block_hash: blk.hash as Eth2Digest,
|
|
transactions_root: blk.transactionsRoot as Eth2Digest,
|
|
withdrawals_root: blk.withdrawalsRoot.getOrDefault() as Eth2Digest)
|
|
|
|
func `as`(blk: BlockObject, T: type deneb.ExecutionPayloadHeader): T =
|
|
T(parent_hash: blk.parentHash as Eth2Digest,
|
|
fee_recipient: blk.miner as ExecutionAddress,
|
|
state_root: blk.stateRoot as Eth2Digest,
|
|
receipts_root: blk.receiptsRoot as Eth2Digest,
|
|
logs_bloom: BloomLogs(data: distinctBase(blk.logsBloom)),
|
|
prev_randao: Eth2Digest(data: blk.difficulty.toByteArrayBE),
|
|
block_number: uint64 blk.number,
|
|
gas_limit: uint64 blk.gasLimit,
|
|
gas_used: uint64 blk.gasUsed,
|
|
timestamp: uint64 blk.timestamp,
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(blk.extraData.bytes),
|
|
base_fee_per_gas: blk.baseFeePerGas.getOrDefault(),
|
|
block_hash: blk.hash as Eth2Digest,
|
|
transactions_root: blk.transactionsRoot as Eth2Digest,
|
|
withdrawals_root: blk.withdrawalsRoot.getOrDefault() as Eth2Digest,
|
|
blob_gas_used: uint64 blk.blobGasUsed.getOrDefault(),
|
|
excess_blob_gas: uint64 blk.excessBlobGas.getOrDefault())
|
|
|
|
func `as`(blk: BlockObject, T: type electra.ExecutionPayloadHeader): T =
|
|
T(parent_hash: blk.parentHash as Eth2Digest,
|
|
fee_recipient: blk.miner as ExecutionAddress,
|
|
state_root: blk.stateRoot as Eth2Digest,
|
|
receipts_root: blk.receiptsRoot as Eth2Digest,
|
|
logs_bloom: BloomLogs(data: distinctBase(blk.logsBloom)),
|
|
prev_randao: Eth2Digest(data: blk.difficulty.toByteArrayBE),
|
|
block_number: uint64 blk.number,
|
|
gas_limit: uint64 blk.gasLimit,
|
|
gas_used: uint64 blk.gasUsed,
|
|
timestamp: uint64 blk.timestamp,
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(blk.extraData.bytes),
|
|
base_fee_per_gas: blk.baseFeePerGas.getOrDefault(),
|
|
block_hash: blk.hash as Eth2Digest,
|
|
transactions_root: blk.transactionsRoot as Eth2Digest,
|
|
withdrawals_root: blk.withdrawalsRoot.getOrDefault() as Eth2Digest,
|
|
blob_gas_used: uint64 blk.blobGasUsed.getOrDefault(),
|
|
excess_blob_gas: uint64 blk.excessBlobGas.getOrDefault())
|
|
|
|
func `as`(blk: BlockObject, T: type fulu.ExecutionPayloadHeader): T =
|
|
T(parent_hash: blk.parentHash as Eth2Digest,
|
|
fee_recipient: blk.miner as ExecutionAddress,
|
|
state_root: blk.stateRoot as Eth2Digest,
|
|
receipts_root: blk.receiptsRoot as Eth2Digest,
|
|
logs_bloom: BloomLogs(data: distinctBase(blk.logsBloom)),
|
|
prev_randao: Eth2Digest(data: blk.difficulty.toByteArrayBE),
|
|
block_number: uint64 blk.number,
|
|
gas_limit: uint64 blk.gasLimit,
|
|
gas_used: uint64 blk.gasUsed,
|
|
timestamp: uint64 blk.timestamp,
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(blk.extraData.bytes),
|
|
base_fee_per_gas: blk.baseFeePerGas.getOrDefault(),
|
|
block_hash: blk.hash as Eth2Digest,
|
|
transactions_root: blk.transactionsRoot as Eth2Digest,
|
|
withdrawals_root: blk.withdrawalsRoot.getOrDefault() as Eth2Digest,
|
|
blob_gas_used: uint64 blk.blobGasUsed.getOrDefault(),
|
|
excess_blob_gas: uint64 blk.excessBlobGas.getOrDefault())
|
|
|
|
func createDepositContractSnapshot(
|
|
deposits: seq[DepositData],
|
|
blockHash: Eth2Digest,
|
|
blockHeight: uint64): DepositContractSnapshot =
|
|
var merkleizer = DepositsMerkleizer.init()
|
|
for i, deposit in deposits:
|
|
let htr = hash_tree_root(deposit)
|
|
merkleizer.addChunk(htr.data)
|
|
|
|
DepositContractSnapshot(
|
|
eth1Block: blockHash,
|
|
depositContractState: merkleizer.toDepositContractState,
|
|
blockHeight: blockHeight)
|
|
|
|
proc writeValue*(writer: var JsonWriter, value: DateTime) {.
|
|
raises: [IOError].} =
|
|
writer.writeValue($value)
|
|
|
|
proc readValue*(reader: var JsonReader, value: var DateTime) {.
|
|
raises: [IOError, SerializationError].} =
|
|
let s = reader.readValue(string)
|
|
try:
|
|
value = parse(s, "YYYY-MM-dd HH:mm:ss'.'fffzzz", utc())
|
|
except CatchableError:
|
|
raiseUnexpectedValue(reader, "Invalid date time")
|
|
|
|
proc writeValue*(writer: var JsonWriter, value: IoErrorCode) {.
|
|
raises: [IOError].} =
|
|
writer.writeValue(distinctBase value)
|
|
|
|
proc readValue*(reader: var JsonReader, value: var IoErrorCode) {.
|
|
raises: [IOError, SerializationError].} =
|
|
IoErrorCode reader.readValue(distinctBase IoErrorCode)
|
|
|
|
proc createEnr(rng: var HmacDrbgContext,
|
|
dataDir: string,
|
|
netKeyFile: string,
|
|
netKeyInsecurePassword: bool,
|
|
cfg: RuntimeConfig,
|
|
forkId: seq[byte],
|
|
address: IpAddress,
|
|
port: Port): enr.Record
|
|
{.raises: [CatchableError].} =
|
|
type MetaData = altair.MetaData
|
|
let
|
|
networkKeys = rng.getPersistentNetKeys(
|
|
dataDir, netKeyFile, netKeyInsecurePassword, allowLoadExisting = false)
|
|
|
|
netMetadata = MetaData()
|
|
bootstrapEnr = enr.Record.init(
|
|
1, # sequence number
|
|
networkKeys.seckey.asEthKey,
|
|
Opt.some(address),
|
|
Opt.some(port),
|
|
Opt.some(port),
|
|
[
|
|
toFieldPair(enrForkIdField, forkId),
|
|
toFieldPair(enrAttestationSubnetsField, SSZ.encode(netMetadata.attnets))
|
|
])
|
|
bootstrapEnr.tryGet()
|
|
|
|
proc doCreateTestnet*(config: CliConfig,
|
|
rng: var HmacDrbgContext)
|
|
{.raises: [CatchableError].} =
|
|
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
|
|
|
|
var deposits: seq[DepositData]
|
|
for i in 0 ..< launchPadDeposits.len:
|
|
deposits.add(launchPadDeposits[i] as DepositData)
|
|
|
|
let
|
|
startTime = if config.genesisTime.isSome:
|
|
config.genesisTime.get
|
|
else:
|
|
uint64(times.toUnix(times.getTime()) + config.genesisOffset.get(0))
|
|
outGenesis = config.outputGenesis.string
|
|
eth1Hash = mockEth1BlockHash # TODO: Can we set a more appropriate value?
|
|
cfg = getRuntimeConfig(config.eth2Network)
|
|
|
|
# This is intentionally left default initialized, when the user doesn't
|
|
# provide an execution genesis block. The generated genesis state will
|
|
# then be considered non-finalized merged state according to the spec.
|
|
var genesisBlock = BlockObject()
|
|
|
|
if config.executionGenesisBlock.isSome:
|
|
logScope:
|
|
path = config.executionGenesisBlock.get.string
|
|
|
|
if not fileExists(config.executionGenesisBlock.get.string):
|
|
error "The specified execution genesis block file doesn't exist"
|
|
quit 1
|
|
|
|
let genesisBlockContents = readAllChars(config.executionGenesisBlock.get.string)
|
|
if genesisBlockContents.isErr:
|
|
error "Failed to read the specified execution genesis block file",
|
|
err = genesisBlockContents.error
|
|
quit 1
|
|
|
|
try:
|
|
let blockAsJson = genesisBlockContents.get
|
|
genesisBlock = JrpcConv.decode(blockAsJson, BlockObject)
|
|
except CatchableError as err:
|
|
error "Failed to load the genesis block from json",
|
|
err = err.msg
|
|
quit 1
|
|
|
|
template createAndSaveState(genesisExecutionPayloadHeader: auto): Eth2Digest =
|
|
var initialState = newClone(initialize_beacon_state_from_eth1(
|
|
cfg, eth1Hash, startTime, deposits, genesisExecutionPayloadHeader,
|
|
{skipBlsValidation}))
|
|
# https://github.com/ethereum/eth2.0-pm/tree/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start#create-genesis-state
|
|
initialState.genesis_time = startTime
|
|
|
|
doAssert initialState.validators.len > 0
|
|
|
|
# let outGenesisExt = splitFile(outGenesis).ext
|
|
#if cmpIgnoreCase(outGenesisExt, ".json") == 0:
|
|
|
|
# let outGenesisJson = outGenesis & ".json"
|
|
# RestJson.saveFile(outGenesisJson, initialState, pretty = true)
|
|
# info "JSON genesis file written", path = outGenesisJson
|
|
|
|
let outSszGenesis = outGenesis.changeFileExt "ssz"
|
|
SSZ.saveFile(outSszGenesis, initialState[])
|
|
info "SSZ genesis file written",
|
|
path = outSszGenesis, fork = kind(typeof initialState[])
|
|
|
|
SSZ.saveFile(
|
|
config.outputDepositTreeSnapshot.string,
|
|
createDepositContractSnapshot(
|
|
deposits,
|
|
genesisExecutionPayloadHeader.block_hash,
|
|
genesisExecutionPayloadHeader.block_number).getTreeSnapshot())
|
|
|
|
initialState[].genesis_validators_root
|
|
|
|
let genesisValidatorsRoot =
|
|
if config.fuluForkEpoch == 0:
|
|
createAndSaveState(genesisBlock as fulu.ExecutionPayloadHeader)
|
|
elif config.electraForkEpoch == 0:
|
|
createAndSaveState(genesisBlock as electra.ExecutionPayloadHeader)
|
|
elif config.denebForkEpoch == 0:
|
|
createAndSaveState(genesisBlock as deneb.ExecutionPayloadHeader)
|
|
elif config.capellaForkEpoch == 0:
|
|
createAndSaveState(genesisBlock as capella.ExecutionPayloadHeader)
|
|
else:
|
|
createAndSaveState(genesisBlock as bellatrix.ExecutionPayloadHeader)
|
|
|
|
let bootstrapFile = string config.outputBootstrapFile
|
|
if bootstrapFile.len > 0:
|
|
let
|
|
forkId = getENRForkID(
|
|
cfg,
|
|
Epoch(0),
|
|
genesisValidatorsRoot)
|
|
enr =
|
|
createEnr(rng, string config.dataDir, string config.netKeyFile,
|
|
config.netKeyInsecurePassword, cfg, SSZ.encode(forkId),
|
|
config.bootstrapAddress, config.bootstrapPort)
|
|
writeFile(bootstrapFile, enr.toURI)
|
|
echo "Wrote ", bootstrapFile
|
|
|
|
type
|
|
DelayGenerator = proc(): chronos.Duration {.gcsafe, raises: [].}
|
|
|
|
func 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(keys.PrivateKey.fromHex(privateKey)[])
|
|
else:
|
|
let accounts = await result.provider.eth_accounts()
|
|
doAssert(accounts.len > 0)
|
|
result.defaultAccount = accounts[0]
|
|
|
|
{.pop.} # TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError
|
|
|
|
when isMainModule:
|
|
import
|
|
web3/confutils_defs,
|
|
../beacon_chain/filepath
|
|
|
|
from std/sequtils import mapIt, toSeq
|
|
from std/terminal import readPasswordFromStdin
|
|
|
|
# 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 depositContractCode =
|
|
hexToSeqByte staticRead "../beacon_chain/el/deposit_contract_code.txt"
|
|
|
|
proc doCreateTestnetEnr(config: CliConfig,
|
|
rng: var HmacDrbgContext)
|
|
{.raises: [CatchableError].} =
|
|
let
|
|
cfg = getRuntimeConfig(config.eth2Network)
|
|
bootstrapEnr = parseBootstrapAddress(toSeq(lines(string config.inputBootstrapEnr))[0]).get()
|
|
forkIdField = bootstrapEnr.tryGet(enrForkIdField, seq[byte]).get()
|
|
enr =
|
|
createEnr(rng, string config.enrDataDir, string config.enrNetKeyFile,
|
|
config.enrNetKeyInsecurePassword, cfg, forkIdField,
|
|
config.enrAddress, config.enrPort)
|
|
stderr.writeLine(enr.toURI)
|
|
|
|
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)
|
|
|
|
# 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, 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(chronos.seconds 60)
|
|
web3 = await initWeb3(web3Url, privateKey)
|
|
|
|
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.generateDeposits:
|
|
let
|
|
mnemonic = generateMnemonic(rng[])
|
|
seed = getSeed(mnemonic, KeystorePass.init "")
|
|
cfg = getRuntimeConfig(conf.eth2Network)
|
|
|
|
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,
|
|
conf.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
|
|
|
|
case conf.cmd
|
|
of StartUpCommand.createTestnet:
|
|
let rng = HmacDrbgContext.new()
|
|
doCreateTestnet(conf, rng[])
|
|
|
|
of StartUpCommand.createTestnetEnr:
|
|
let rng = HmacDrbgContext.new()
|
|
doCreateTestnetEnr(conf, rng[])
|
|
|
|
of StartUpCommand.deployDepositContract:
|
|
let web3 = await initWeb3(conf.web3Url, conf.privateKey)
|
|
let receipt = await web3.deployContract(depositContractCode)
|
|
echo receipt.contractAddress.get, ";", receipt.blockHash
|
|
|
|
of StartUpCommand.sendEth:
|
|
let web3 = await initWeb3(conf.web3Url, conf.privateKey)
|
|
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.run:
|
|
discard
|
|
|
|
of StartUpCommand.generateDeposits:
|
|
# This is handled above before the case statement
|
|
discard
|
|
|
|
waitFor main()
|