mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-10 06:16:25 +00:00
8771e91d53
* Support for driving multiple EL nodes from a single Nimbus BN Full list of changes: * Eth1Monitor has been renamed to ELManager to match its current responsibilities better. * The ELManager is no longer optional in the code (it won't have a nil value under any circumstances). * The support for subscribing for headers was removed as it only worked with WebSockets and contributed significant complexity while bringing only a very minor advantage. * The `--web3-url` parameter has been deprecated in favor of a new `--el` parameter. The new parameter has a reasonable default value and supports specifying a different JWT for each connection. Each connection can also be configured with a different set of responsibilities (e.g. download deposits, validate blocks and/or produce blocks). On the command-line, these properties can be configured through URL properties stored in the #anchor part of the URL. In TOML files, they come with a very natural syntax (althrough the URL scheme is also supported). * The previously scattered EL-related state and logic is now moved to `eth1_monitor.nim` (this module will be renamed to `el_manager.nim` in a follow-up commit). State is assigned properly either to the `ELManager` or the to individual `ELConnection` objects where appropriate. The ELManager executes all Engine API requests against all attached EL nodes, in parallel. It compares their results and if there is a disagreement regarding the validity of a certain payload, this is detected and the beacon node is protected from publishing a block with a potential execution layer consensus bug in it. The BN provides metrics per EL node for the number of successful or failed requests for each type Engine API requests. If an EL node goes offline and connectivity is resoted later, we report the problem and the remedy in edge-triggered fashion. * More progress towards implementing Deneb block production in the VC and comparing the value of blocks produced by the EL and the builder API. * Adds a Makefile target for the zhejiang testnet
309 lines
9.8 KiB
Nim
309 lines
9.8 KiB
Nim
# beacon_chain
|
|
# Copyright (c) 2018-2023 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
|
|
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,
|
|
../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 = 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: string): Future[ReceiptObject] {.async.} =
|
|
var code = code
|
|
if code[1] notin {'x', 'X'}:
|
|
code = "0x" & code
|
|
let tr = EthSend(
|
|
source: web3.defaultAccount,
|
|
data: code,
|
|
gas: Quantity(3000000).some,
|
|
gasPrice: 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(
|
|
source: web3.defaultAccount,
|
|
gas: Quantity(3000000).some,
|
|
gasPrice: 1.some,
|
|
value: some(valueEth.u256 * 1000000000000000000.u256),
|
|
to: some(to))
|
|
web3.send(tr)
|
|
|
|
type
|
|
DelayGenerator* = proc(): chronos.Duration {.gcsafe, raises: [Defect].}
|
|
|
|
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 = 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 = keys.newRng()
|
|
|
|
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.string
|
|
|
|
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()
|