mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-10 14:26:26 +00:00
017f9f1103
Etan Kissling (2): remove unused `skip0xPrefix` keep the internal count helper Will (1): Bugfix/nully values (#61) Yuriy Glukhov (5): Contract constructor support Fixed compilation error in exec function Added string encoding Fixed source->from field of EthCall More flexibility to contract DSL, Async contract caller jangko (5): Reduce compiler warnings when using Nim v2 Migrate to json-serialization Add tests of json rpc marshalled types Resolve contract_dsl ambiguity Event handler passing around JsonString instead of JsonNode Share encoder between json-rpc and chronicles (#119) Simplify generic constraint of rpc and chronicles encoders Feature/execution api spec (#69) v0.3.0 bump nim-json-rpc to a6475e49b26d3afc58aaa3d67621c94eafef8efb coffeepots (1): Use nim-json-serialization for RPCs (#172) jangko (10): Add copyright to source file Remove StringOfJson Fix optional parameter parsing fails in rpc macro with generics Rename jrpc_sys module back to jsonmarshal Reenable test hhtps Add test for createRpcSigsFromNim and createSingleRpcSig Let the OS choose the port for tests Add onProcessMessage hook to client Fix example in the README.md Move errors module back to json_rpc folder Upgrade rpc router internals (#178) RPC server handle null return value correctly v0.3.0 kdeme (1): Add example test case that currently fails the Option parsing
720 lines
25 KiB
Nim
720 lines
25 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],
|
|
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,
|
|
./logtrace
|
|
|
|
from std/os import changeFileExt, fileExists
|
|
from std/sequtils import mapIt, toSeq
|
|
from std/times import toUnix
|
|
from ../beacon_chain/spec/beaconstate import initialize_beacon_state_from_eth1
|
|
from ../tests/mocking/mock_genesis import mockEth1BlockHash
|
|
|
|
# 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"
|
|
|
|
# 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
|
|
analyzeLogs
|
|
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
|
|
|
|
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
|
|
|
|
of StartUpCommand.analyzeLogs:
|
|
logFiles* {.
|
|
desc: "Specifies one or more log files",
|
|
abbr: "f",
|
|
name: "log-file" .}: seq[string]
|
|
|
|
simDir* {.
|
|
desc: "Specifies path to eth2_network_simulation directory",
|
|
defaultValue: "",
|
|
name: "sim-dir" .}: string
|
|
|
|
netDir* {.
|
|
desc: "Specifies path to network build directory",
|
|
defaultValue: "",
|
|
name: "net-dir" .}: string
|
|
|
|
logDir* {.
|
|
desc: "Specifies path with bunch of logs",
|
|
defaultValue: "",
|
|
name: "log-dir" .}: string
|
|
|
|
ignoreSerializationErrors* {.
|
|
desc: "Ignore serialization errors while parsing log files",
|
|
defaultValue: true,
|
|
name: "ignore-errors" .}: bool
|
|
|
|
dumpSerializationErrors* {.
|
|
desc: "Dump full serialization errors while parsing log files",
|
|
defaultValue: false ,
|
|
name: "dump-errors" .}: bool
|
|
|
|
nodes* {.
|
|
desc: "Specifies node names which logs will be used",
|
|
name: "nodes" .}: seq[string]
|
|
|
|
allowedLag* {.
|
|
desc: "Allowed latency lag multiplier",
|
|
defaultValue: 2.0,
|
|
name: "lag" .}: float
|
|
|
|
constPreset* {.
|
|
desc: "The const preset being used"
|
|
defaultValue: "mainnet"
|
|
name: "const-preset" .}: string
|
|
|
|
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])
|
|
|
|
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: Option[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 createDepositTreeSnapshot(deposits: seq[DepositData],
|
|
blockHash: Eth2Digest,
|
|
blockHeight: uint64): DepositTreeSnapshot =
|
|
var merkleizer = DepositsMerkleizer.init()
|
|
for i, deposit in deposits:
|
|
let htr = hash_tree_root(deposit)
|
|
merkleizer.addChunk(htr.data)
|
|
|
|
DepositTreeSnapshot(
|
|
eth1Block: blockHash,
|
|
depositContractState: merkleizer.toDepositContractState,
|
|
blockHeight: blockHeight)
|
|
|
|
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,
|
|
some(address),
|
|
some(port),
|
|
some(port),
|
|
[
|
|
toFieldPair(enrForkIdField, forkId),
|
|
toFieldPair(enrAttestationSubnetsField, SSZ.encode(netMetadata.attnets))
|
|
])
|
|
bootstrapEnr.tryGet()
|
|
|
|
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 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,
|
|
createDepositTreeSnapshot(
|
|
deposits,
|
|
genesisExecutionPayloadHeader.block_hash,
|
|
genesisExecutionPayloadHeader.block_number))
|
|
|
|
initialState[].genesis_validators_root
|
|
|
|
let genesisValidatorsRoot =
|
|
if 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
|
|
|
|
proc deployContract(web3: Web3, code: seq[byte]): Future[ReceiptObject] {.async.} =
|
|
let tr = EthSend(
|
|
`from`: web3.defaultAccount,
|
|
data: code,
|
|
gas: Quantity(3000000).some,
|
|
gasPrice: Quantity(1).some)
|
|
|
|
let r = await web3.send(tr)
|
|
result = await web3.getMinedTransactionReceipt(r)
|
|
|
|
proc sendEth(web3: Web3, to: Eth1Address, valueEth: int): Future[TxHash] =
|
|
let tr = EthSend(
|
|
`from`: web3.defaultAccount,
|
|
gas: Quantity(3000000).some,
|
|
gasPrice: Quantity(1).some,
|
|
value: some(valueEth.u256 * 1000000000000000000.u256),
|
|
to: some(to))
|
|
web3.send(tr)
|
|
|
|
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 = some(keys.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,
|
|
Eth1Address 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)
|
|
|
|
{.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/terminal import readPasswordFromStdin
|
|
|
|
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.string
|
|
|
|
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.analyzeLogs:
|
|
try:
|
|
logtrace.run(LogTraceConf(
|
|
cmd: logtrace.StartUpCommand.localSimChecks,
|
|
logFiles: conf.logFiles,
|
|
simDir: conf.simDir,
|
|
netDir: conf.netDir,
|
|
logDir: conf.logDir,
|
|
ignoreSerializationErrors: conf.ignoreSerializationErrors,
|
|
dumpSerializationErrors: conf.dumpSerializationErrors,
|
|
nodes: conf.nodes,
|
|
allowedLag: conf.allowedLag,
|
|
constPreset: conf.constPreset
|
|
))
|
|
except CatchableError as err:
|
|
fatal "Unexpected error in logtrace", err = err.msg
|
|
except Exception as exc:
|
|
# TODO: Investigate where is this coming from?
|
|
fatal "Unexpected exception in logtrace", err = exc.msg
|
|
|
|
of StartUpCommand.generateDeposits:
|
|
# This is handled above before the case statement
|
|
discard
|
|
|
|
waitFor main()
|