2021-03-26 06:52:01 +00:00
|
|
|
# beacon_chain
|
2024-01-06 14:26:56 +00:00
|
|
|
# Copyright (c) 2018-2024 Status Research & Development GmbH
|
2021-03-26 06:52:01 +00:00
|
|
|
# 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.
|
|
|
|
|
2023-01-20 14:14:37 +00:00
|
|
|
{.push raises: [].}
|
2021-03-26 06:52:01 +00:00
|
|
|
|
2018-11-26 13:33:06 +00:00
|
|
|
import
|
2024-01-17 14:26:16 +00:00
|
|
|
std/[strformat, strutils, sequtils, typetraits, uri, json],
|
2020-12-09 22:44:59 +00:00
|
|
|
# Nimble packages:
|
2023-09-12 18:16:04 +00:00
|
|
|
chronos, metrics, chronicles/timings,
|
2023-03-05 01:40:21 +00:00
|
|
|
json_rpc/[client, errors],
|
2024-01-13 01:36:17 +00:00
|
|
|
web3, web3/[engine_api, primitives, conversions],
|
2023-03-05 01:40:21 +00:00
|
|
|
eth/common/[eth_types, transaction],
|
2024-01-16 22:37:14 +00:00
|
|
|
eth/async_utils, results,
|
2024-01-19 03:20:47 +00:00
|
|
|
stew/[assign2, byteutils, objects],
|
2020-12-09 22:44:59 +00:00
|
|
|
# Local modules:
|
2024-01-17 14:26:16 +00:00
|
|
|
../spec/[eth2_merkleization, forks, helpers],
|
2021-03-05 13:12:00 +00:00
|
|
|
../networking/network_metadata,
|
2024-01-17 14:26:16 +00:00
|
|
|
".."/[beacon_node_status, future_combinators],
|
|
|
|
"."/[eth1_chain, el_conf]
|
2018-11-26 13:33:06 +00:00
|
|
|
|
2022-03-31 14:43:05 +00:00
|
|
|
from std/times import getTime, inSeconds, initTime, `-`
|
|
|
|
from ../spec/engine_authentication import getSignedIatToken
|
2023-06-10 05:39:10 +00:00
|
|
|
from ../spec/state_transition_block import kzg_commitment_to_versioned_hash
|
2022-03-31 14:43:05 +00:00
|
|
|
|
2020-06-19 17:42:28 +00:00
|
|
|
export
|
2024-01-17 14:26:16 +00:00
|
|
|
eth1_chain, el_conf, engine_api, base
|
2020-06-19 17:42:28 +00:00
|
|
|
|
2020-11-19 17:19:03 +00:00
|
|
|
logScope:
|
2024-02-07 08:44:32 +00:00
|
|
|
topics = "elman"
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2021-10-14 12:33:55 +00:00
|
|
|
type
|
|
|
|
PubKeyBytes = DynamicBytes[48, 48]
|
|
|
|
WithdrawalCredentialsBytes = DynamicBytes[32, 32]
|
|
|
|
SignatureBytes = DynamicBytes[96, 96]
|
|
|
|
Int64LeBytes = DynamicBytes[8, 8]
|
|
|
|
|
2020-03-24 11:13:07 +00:00
|
|
|
contract(DepositContract):
|
2021-10-14 12:33:55 +00:00
|
|
|
proc deposit(pubkey: PubKeyBytes,
|
|
|
|
withdrawalCredentials: WithdrawalCredentialsBytes,
|
|
|
|
signature: SignatureBytes,
|
2020-03-24 11:13:07 +00:00
|
|
|
deposit_data_root: FixedBytes[32])
|
|
|
|
|
|
|
|
proc get_deposit_root(): FixedBytes[32]
|
2021-10-14 12:33:55 +00:00
|
|
|
proc get_deposit_count(): Int64LeBytes
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2021-10-14 12:33:55 +00:00
|
|
|
proc DepositEvent(pubkey: PubKeyBytes,
|
|
|
|
withdrawalCredentials: WithdrawalCredentialsBytes,
|
|
|
|
amount: Int64LeBytes,
|
|
|
|
signature: SignatureBytes,
|
|
|
|
index: Int64LeBytes) {.event.}
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
const
|
2020-12-15 23:09:19 +00:00
|
|
|
hasDepositRootChecks = defined(has_deposit_root_checks)
|
2020-11-24 21:21:47 +00:00
|
|
|
|
2023-03-14 11:45:01 +00:00
|
|
|
targetBlocksPerLogsRequest = 1000'u64
|
|
|
|
# TODO
|
|
|
|
#
|
|
|
|
# This is currently set to 1000, because this was the default maximum
|
|
|
|
# value in Besu circa our 22.3.0 release. Previously, we've used 5000,
|
|
|
|
# but this was effectively forcing the fallback logic in `syncBlockRange`
|
|
|
|
# to always execute multiple requests before getting a successful response.
|
|
|
|
#
|
|
|
|
# Besu have raised this default to 5000 in https://github.com/hyperledger/besu/pull/5209
|
|
|
|
# which is expected to ship in their next release.
|
|
|
|
#
|
|
|
|
# Full deposits sync time with various values for this parameter:
|
|
|
|
#
|
|
|
|
# Blocks per request | Geth running on the same host | Geth running on a more distant host
|
|
|
|
# ----------------------------------------------------------------------------------------
|
|
|
|
# 1000 | 11m 20s | 22m
|
|
|
|
# 5000 | 5m 20s | 15m 40s
|
|
|
|
# 100000 | 4m 10s | not tested
|
|
|
|
#
|
|
|
|
# The number of requests scales linearly with the parameter value as you would expect.
|
|
|
|
#
|
|
|
|
# These results suggest that it would be reasonable for us to get back to 5000 once the
|
|
|
|
# Besu release is well-spread within their userbase.
|
2022-08-12 13:51:33 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
# Engine API timeouts
|
|
|
|
engineApiConnectionTimeout = 5.seconds # How much we wait before giving up connecting to the Engine API
|
|
|
|
web3RequestsTimeout* = 8.seconds # How much we wait for eth_* requests (e.g. eth_getBlockByHash)
|
|
|
|
|
2023-04-17 20:11:28 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/paris.md#request-2
|
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/shanghai.md#request-2
|
2023-03-05 01:40:21 +00:00
|
|
|
GETPAYLOAD_TIMEOUT = 1.seconds
|
|
|
|
|
2023-05-15 20:40:47 +00:00
|
|
|
connectionStateChangeHysteresisThreshold = 15
|
|
|
|
## How many unsuccesful/successful requests we must see
|
|
|
|
## before declaring the connection as degraded/restored
|
|
|
|
|
2018-11-26 13:33:06 +00:00
|
|
|
type
|
2023-03-05 01:40:21 +00:00
|
|
|
NextExpectedPayloadParams* = object
|
|
|
|
headBlockHash*: Eth2Digest
|
|
|
|
safeBlockHash*: Eth2Digest
|
|
|
|
finalizedBlockHash*: Eth2Digest
|
2023-08-15 23:00:35 +00:00
|
|
|
payloadAttributes*: PayloadAttributesV3
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
ELManager* = ref object
|
2020-12-15 21:59:29 +00:00
|
|
|
eth1Network: Option[Eth1Network]
|
2023-05-15 05:05:12 +00:00
|
|
|
## If this value is supplied the EL manager will check whether
|
2023-03-05 01:40:21 +00:00
|
|
|
## all configured EL nodes are connected to the same network.
|
|
|
|
|
2020-12-15 21:59:29 +00:00
|
|
|
depositContractAddress*: Eth1Address
|
2023-03-05 01:40:21 +00:00
|
|
|
depositContractBlockNumber: uint64
|
|
|
|
depositContractBlockHash: BlockHash
|
|
|
|
|
2022-08-12 13:51:33 +00:00
|
|
|
blocksPerLogsRequest: uint64
|
2023-03-05 01:40:21 +00:00
|
|
|
## This value is used to dynamically adjust the number of
|
|
|
|
## blocks we are trying to download at once during deposit
|
|
|
|
## syncing. By default, the value is set to the constant
|
|
|
|
## `targetBlocksPerLogsRequest`, but if the EL is failing
|
|
|
|
## to serve this number of blocks per single `eth_getLogs`
|
|
|
|
## request, we temporarily lower the value until the request
|
|
|
|
## succeeds. The failures are generally expected only in
|
|
|
|
## periods in the history for very high deposit density.
|
|
|
|
|
|
|
|
elConnections: seq[ELConnection]
|
|
|
|
## All active EL connections
|
|
|
|
|
|
|
|
eth1Chain: Eth1Chain
|
|
|
|
## At larger distances, this chain consists of all blocks
|
|
|
|
## with deposits. Within the relevant voting period, it
|
|
|
|
## also includes blocks without deposits because we must
|
|
|
|
## vote for a block only if it's part of our known history.
|
|
|
|
|
|
|
|
syncTargetBlock: Option[Eth1BlockNumber]
|
|
|
|
|
|
|
|
chainSyncingLoopFut: Future[void]
|
|
|
|
exchangeTransitionConfigurationLoopFut: Future[void]
|
|
|
|
stopFut: Future[void]
|
2020-10-14 14:04:08 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
nextExpectedPayloadParams*: Option[NextExpectedPayloadParams]
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
EtcStatus {.pure.} = enum
|
|
|
|
notExchangedYet
|
|
|
|
mismatch
|
|
|
|
match
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
DepositContractSyncStatus {.pure.} = enum
|
|
|
|
unknown
|
|
|
|
notSynced
|
|
|
|
synced
|
2022-06-15 02:38:27 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
ConnectionState = enum
|
|
|
|
NeverTested
|
|
|
|
Working
|
|
|
|
Degraded
|
|
|
|
|
|
|
|
ELConnection* = ref object
|
|
|
|
engineUrl: EngineApiUrl
|
|
|
|
|
|
|
|
web3: Option[Web3]
|
|
|
|
## This will be `none` before connecting and while we are
|
|
|
|
## reconnecting after a lost connetion. You can wait on
|
|
|
|
## the future below for the moment the connection is active.
|
|
|
|
|
|
|
|
connectingFut: Future[Result[Web3, string]]
|
|
|
|
## This future will be replaced when the connection is lost.
|
|
|
|
|
|
|
|
etcStatus: EtcStatus
|
|
|
|
## The latest status of the `exchangeTransitionConfiguration`
|
|
|
|
## exchange.
|
|
|
|
|
|
|
|
state: ConnectionState
|
2023-05-15 20:40:47 +00:00
|
|
|
hysteresisCounter: int
|
2019-11-22 13:16:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
depositContractSyncStatus: DepositContractSyncStatus
|
|
|
|
## Are we sure that this EL has synced the deposit contract?
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
lastPayloadId: Option[engine_api.PayloadID]
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2021-11-25 16:51:51 +00:00
|
|
|
FullBlockId* = object
|
|
|
|
number: Eth1BlockNumber
|
|
|
|
hash: BlockHash
|
|
|
|
|
2021-05-28 12:42:39 +00:00
|
|
|
DataProviderFailure* = object of CatchableError
|
|
|
|
CorruptDataProvider* = object of DataProviderFailure
|
|
|
|
DataProviderTimeout* = object of DataProviderFailure
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2023-08-25 09:29:07 +00:00
|
|
|
DisconnectHandler* = proc () {.gcsafe, raises: [].}
|
2020-03-24 11:13:07 +00:00
|
|
|
|
|
|
|
DepositEventHandler* = proc (
|
2021-10-14 12:33:55 +00:00
|
|
|
pubkey: PubKeyBytes,
|
|
|
|
withdrawalCredentials: WithdrawalCredentialsBytes,
|
|
|
|
amount: Int64LeBytes,
|
|
|
|
signature: SignatureBytes,
|
|
|
|
merkleTreeIndex: Int64LeBytes,
|
2023-08-25 09:29:07 +00:00
|
|
|
j: JsonNode) {.gcsafe, raises: [].}
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
BellatrixExecutionPayloadWithValue* = object
|
|
|
|
executionPayload*: ExecutionPayloadV1
|
|
|
|
blockValue*: UInt256
|
|
|
|
|
|
|
|
SomeEnginePayloadWithValue =
|
|
|
|
BellatrixExecutionPayloadWithValue |
|
|
|
|
GetPayloadV2Response |
|
2023-04-28 19:20:25 +00:00
|
|
|
GetPayloadV3Response
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2020-12-09 22:44:59 +00:00
|
|
|
declareCounter failed_web3_requests,
|
|
|
|
"Failed web3 requests"
|
|
|
|
|
|
|
|
declareGauge eth1_latest_head,
|
|
|
|
"The highest Eth1 block number observed on the network"
|
|
|
|
|
|
|
|
declareGauge eth1_synced_head,
|
|
|
|
"Block number of the highest synchronized block according to follow distance"
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
declareCounter engine_api_responses,
|
|
|
|
"Number of successful requests to the newPayload Engine API end-point",
|
|
|
|
labels = ["url", "request", "status"]
|
|
|
|
|
2023-03-09 17:24:03 +00:00
|
|
|
declareHistogram engine_api_request_duration_seconds,
|
2023-03-09 17:29:04 +00:00
|
|
|
"Time(s) used to generate signature usign remote signer",
|
2023-03-09 17:24:03 +00:00
|
|
|
buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
|
2023-03-09 17:29:04 +00:00
|
|
|
labels = ["url", "request"]
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
declareCounter engine_api_timeouts,
|
|
|
|
"Number of timed-out requests to Engine API end-point",
|
|
|
|
labels = ["url", "request"]
|
|
|
|
|
|
|
|
declareCounter engine_api_last_minute_forkchoice_updates_sent,
|
|
|
|
"Number of last minute requests to the forkchoiceUpdated Engine API end-point just before block proposals",
|
|
|
|
labels = ["url"]
|
|
|
|
|
2023-05-15 20:40:47 +00:00
|
|
|
proc close(connection: ELConnection): Future[void] {.async.} =
|
|
|
|
if connection.web3.isSome:
|
|
|
|
awaitWithTimeout(connection.web3.get.close(), 30.seconds):
|
|
|
|
debug "Failed to close data provider in time"
|
|
|
|
|
|
|
|
proc increaseCounterTowardsStateChange(connection: ELConnection): bool =
|
|
|
|
result = connection.hysteresisCounter >= connectionStateChangeHysteresisThreshold
|
|
|
|
if result:
|
|
|
|
connection.hysteresisCounter = 0
|
|
|
|
else:
|
|
|
|
inc connection.hysteresisCounter
|
|
|
|
|
|
|
|
proc decreaseCounterTowardsStateChange(connection: ELConnection) =
|
|
|
|
if connection.hysteresisCounter > 0:
|
|
|
|
# While we increase the counter by 1, we decreate it by 20% in order
|
|
|
|
# to require a steady and affirmative change instead of allowing
|
|
|
|
# the counter to drift very slowly in one direction when the ratio
|
|
|
|
# between success and failure is roughly 50:50%
|
|
|
|
connection.hysteresisCounter = connection.hysteresisCounter div 5
|
|
|
|
|
2023-03-09 17:29:04 +00:00
|
|
|
proc setDegradedState(connection: ELConnection,
|
|
|
|
requestName: string,
|
|
|
|
statusCode: int, errMsg: string) =
|
2023-05-15 20:40:47 +00:00
|
|
|
debug "Failed EL Request", requestName, statusCode, err = errMsg
|
|
|
|
|
2023-03-09 17:29:04 +00:00
|
|
|
case connection.state
|
|
|
|
of NeverTested, Working:
|
2023-05-15 20:40:47 +00:00
|
|
|
if connection.increaseCounterTowardsStateChange():
|
|
|
|
warn "Connection to EL node degraded",
|
|
|
|
url = url(connection.engineUrl),
|
|
|
|
failedRequest = requestName,
|
|
|
|
statusCode, err = errMsg
|
|
|
|
|
|
|
|
connection.state = Degraded
|
2023-03-09 23:41:28 +00:00
|
|
|
|
2023-05-15 20:40:47 +00:00
|
|
|
asyncSpawn connection.close()
|
|
|
|
connection.web3 = none[Web3]()
|
|
|
|
of Degraded:
|
|
|
|
connection.decreaseCounterTowardsStateChange()
|
2023-03-09 17:29:04 +00:00
|
|
|
|
|
|
|
proc setWorkingState(connection: ELConnection) =
|
|
|
|
case connection.state
|
2023-05-25 07:39:47 +00:00
|
|
|
of NeverTested:
|
|
|
|
connection.hysteresisCounter = 0
|
|
|
|
connection.state = Working
|
2023-03-09 17:29:04 +00:00
|
|
|
of Degraded:
|
2023-05-15 20:40:47 +00:00
|
|
|
if connection.increaseCounterTowardsStateChange():
|
|
|
|
info "Connection to EL node restored",
|
|
|
|
url = url(connection.engineUrl)
|
|
|
|
|
|
|
|
connection.state = Working
|
2023-05-25 07:39:47 +00:00
|
|
|
of Working:
|
2023-05-15 20:40:47 +00:00
|
|
|
connection.decreaseCounterTowardsStateChange()
|
2023-03-09 17:29:04 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc trackEngineApiRequest(connection: ELConnection,
|
|
|
|
request: FutureBase, requestName: string,
|
2023-03-21 22:19:36 +00:00
|
|
|
startTime: Moment, deadline: Future[void],
|
|
|
|
failureAllowed = false) =
|
2023-06-08 13:42:19 +00:00
|
|
|
request.addCallback do (udata: pointer) {.gcsafe, raises: [].}:
|
2023-03-09 17:29:04 +00:00
|
|
|
# TODO `udata` is nil here. How come?
|
|
|
|
# This forces us to create a GC cycle between the Future and the closure
|
|
|
|
if request.completed:
|
2023-03-09 20:28:46 +00:00
|
|
|
engine_api_request_duration_seconds.observe(
|
2023-03-09 17:29:04 +00:00
|
|
|
float(milliseconds(Moment.now - startTime)) / 1000.0,
|
|
|
|
[connection.engineUrl.url, requestName])
|
|
|
|
|
|
|
|
connection.setWorkingState()
|
|
|
|
|
2023-06-08 13:42:19 +00:00
|
|
|
deadline.addCallback do (udata: pointer) {.gcsafe, raises: [].}:
|
2023-03-05 01:40:21 +00:00
|
|
|
if not request.finished:
|
2023-09-22 11:06:27 +00:00
|
|
|
request.cancelSoon()
|
2023-03-05 01:40:21 +00:00
|
|
|
engine_api_timeouts.inc(1, [connection.engineUrl.url, requestName])
|
2023-05-02 13:52:26 +00:00
|
|
|
if not failureAllowed:
|
|
|
|
connection.setDegradedState(requestName, 0, "Request timed out")
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
|
|
|
let statusCode = if not request.failed:
|
|
|
|
200
|
|
|
|
elif request.error of ErrorResponse:
|
|
|
|
((ref ErrorResponse) request.error).status
|
|
|
|
else:
|
|
|
|
0
|
|
|
|
|
2023-03-21 22:19:36 +00:00
|
|
|
if request.failed and not failureAllowed:
|
2023-03-09 17:29:04 +00:00
|
|
|
connection.setDegradedState(requestName, statusCode, request.error.msg)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
engine_api_responses.inc(1, [connection.engineUrl.url, requestName, $statusCode])
|
|
|
|
|
|
|
|
template awaitOrRaiseOnTimeout[T](fut: Future[T],
|
|
|
|
timeout: Duration): T =
|
|
|
|
awaitWithTimeout(fut, timeout):
|
|
|
|
raise newException(DataProviderTimeout, "Timeout")
|
|
|
|
|
2023-03-09 17:29:04 +00:00
|
|
|
template trackedRequestWithTimeout[T](connection: ELConnection,
|
|
|
|
requestName: static string,
|
|
|
|
lazyRequestExpression: Future[T],
|
2023-03-21 22:19:36 +00:00
|
|
|
timeout: Duration,
|
|
|
|
failureAllowed = false): T =
|
2023-03-09 17:29:04 +00:00
|
|
|
let
|
|
|
|
connectionParam = connection
|
|
|
|
startTime = Moment.now
|
|
|
|
deadline = sleepAsync(timeout)
|
|
|
|
request = lazyRequestExpression
|
|
|
|
|
2023-03-21 22:19:36 +00:00
|
|
|
connectionParam.trackEngineApiRequest(
|
|
|
|
request, requestName, startTime, deadline, failureAllowed)
|
2023-03-09 17:29:04 +00:00
|
|
|
|
|
|
|
awaitWithTimeout(request, deadline):
|
|
|
|
raise newException(DataProviderTimeout, "Timeout")
|
|
|
|
|
2024-01-08 16:53:29 +00:00
|
|
|
func raiseIfNil(web3block: BlockObject): BlockObject {.raises: [ValueError].} =
|
|
|
|
if web3block == nil:
|
|
|
|
raise newException(ValueError, "EL returned 'null' result for block")
|
|
|
|
web3block
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
template cfg(m: ELManager): auto =
|
|
|
|
m.eth1Chain.cfg
|
|
|
|
|
|
|
|
func hasJwtSecret*(m: ELManager): bool =
|
|
|
|
for c in m.elConnections:
|
|
|
|
if c.engineUrl.jwtSecret.isSome:
|
|
|
|
return true
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
func isSynced*(m: ELManager): bool =
|
|
|
|
m.syncTargetBlock.isSome and
|
|
|
|
m.eth1Chain.blocks.len > 0 and
|
|
|
|
m.syncTargetBlock.get <= m.eth1Chain.blocks[^1].number
|
2020-12-03 04:30:35 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
template eth1ChainBlocks*(m: ELManager): Deque[Eth1Block] =
|
|
|
|
m.eth1Chain.blocks
|
2022-08-09 21:32:34 +00:00
|
|
|
|
2020-12-09 22:44:59 +00:00
|
|
|
template toGaugeValue(x: Quantity): int64 =
|
|
|
|
toGaugeValue(distinctBase x)
|
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
# TODO: Add cfg validation
|
2020-07-07 23:02:14 +00:00
|
|
|
# MIN_GENESIS_ACTIVE_VALIDATOR_COUNT should be larger than SLOTS_PER_EPOCH
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
# doAssert SECONDS_PER_ETH1_BLOCK * cfg.ETH1_FOLLOW_DISTANCE < GENESIS_DELAY,
|
2020-07-07 23:02:14 +00:00
|
|
|
# "Invalid configuration: GENESIS_DELAY is set too low"
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2023-01-06 21:01:10 +00:00
|
|
|
func asConsensusWithdrawal(w: WithdrawalV1): capella.Withdrawal =
|
|
|
|
capella.Withdrawal(
|
|
|
|
index: w.index.uint64,
|
|
|
|
validator_index: w.validatorIndex.uint64,
|
|
|
|
address: ExecutionAddress(data: w.address.distinctBase),
|
2023-05-10 10:20:55 +00:00
|
|
|
amount: Gwei w.amount)
|
2023-01-06 21:01:10 +00:00
|
|
|
|
|
|
|
func asEngineWithdrawal(w: capella.Withdrawal): WithdrawalV1 =
|
|
|
|
WithdrawalV1(
|
|
|
|
index: Quantity(w.index),
|
|
|
|
validatorIndex: Quantity(w.validator_index),
|
|
|
|
address: Address(w.address.data),
|
2023-01-14 17:26:57 +00:00
|
|
|
amount: Quantity(w.amount))
|
2023-01-06 21:01:10 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
func asConsensusType*(rpcExecutionPayload: ExecutionPayloadV1):
|
2022-01-12 14:50:30 +00:00
|
|
|
bellatrix.ExecutionPayload =
|
2022-04-15 12:46:56 +00:00
|
|
|
template getTransaction(tt: TypedTransaction): bellatrix.Transaction =
|
|
|
|
bellatrix.Transaction.init(tt.distinctBase)
|
2022-01-03 12:22:56 +00:00
|
|
|
|
2022-01-12 14:50:30 +00:00
|
|
|
bellatrix.ExecutionPayload(
|
2022-01-03 12:22:56 +00:00
|
|
|
parent_hash: rpcExecutionPayload.parentHash.asEth2Digest,
|
|
|
|
feeRecipient:
|
|
|
|
ExecutionAddress(data: rpcExecutionPayload.feeRecipient.distinctBase),
|
|
|
|
state_root: rpcExecutionPayload.stateRoot.asEth2Digest,
|
|
|
|
receipts_root: rpcExecutionPayload.receiptsRoot.asEth2Digest,
|
|
|
|
logs_bloom: BloomLogs(data: rpcExecutionPayload.logsBloom.distinctBase),
|
2022-03-03 16:08:14 +00:00
|
|
|
prev_randao: rpcExecutionPayload.prevRandao.asEth2Digest,
|
2022-01-03 12:22:56 +00:00
|
|
|
block_number: rpcExecutionPayload.blockNumber.uint64,
|
|
|
|
gas_limit: rpcExecutionPayload.gasLimit.uint64,
|
|
|
|
gas_used: rpcExecutionPayload.gasUsed.uint64,
|
|
|
|
timestamp: rpcExecutionPayload.timestamp.uint64,
|
2023-02-23 02:10:07 +00:00
|
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(rpcExecutionPayload.extraData.bytes),
|
2022-06-01 12:57:28 +00:00
|
|
|
base_fee_per_gas: rpcExecutionPayload.baseFeePerGas,
|
2022-01-03 12:22:56 +00:00
|
|
|
block_hash: rpcExecutionPayload.blockHash.asEth2Digest,
|
2022-01-12 14:50:30 +00:00
|
|
|
transactions: List[bellatrix.Transaction, MAX_TRANSACTIONS_PER_PAYLOAD].init(
|
2022-01-03 12:22:56 +00:00
|
|
|
mapIt(rpcExecutionPayload.transactions, it.getTransaction)))
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
func asConsensusType*(payloadWithValue: BellatrixExecutionPayloadWithValue):
|
|
|
|
bellatrix.ExecutionPayloadForSigning =
|
|
|
|
bellatrix.ExecutionPayloadForSigning(
|
|
|
|
executionPayload: payloadWithValue.executionPayload.asConsensusType,
|
|
|
|
blockValue: payloadWithValue.blockValue)
|
|
|
|
|
|
|
|
template maybeDeref[T](o: Option[T]): T = o.get
|
|
|
|
template maybeDeref[V](v: V): V = v
|
|
|
|
|
|
|
|
func asConsensusType*(rpcExecutionPayload: ExecutionPayloadV1OrV2|ExecutionPayloadV2):
|
2022-11-29 05:02:16 +00:00
|
|
|
capella.ExecutionPayload =
|
|
|
|
template getTransaction(tt: TypedTransaction): bellatrix.Transaction =
|
|
|
|
bellatrix.Transaction.init(tt.distinctBase)
|
|
|
|
|
|
|
|
capella.ExecutionPayload(
|
|
|
|
parent_hash: rpcExecutionPayload.parentHash.asEth2Digest,
|
|
|
|
feeRecipient:
|
|
|
|
ExecutionAddress(data: rpcExecutionPayload.feeRecipient.distinctBase),
|
|
|
|
state_root: rpcExecutionPayload.stateRoot.asEth2Digest,
|
|
|
|
receipts_root: rpcExecutionPayload.receiptsRoot.asEth2Digest,
|
|
|
|
logs_bloom: BloomLogs(data: rpcExecutionPayload.logsBloom.distinctBase),
|
|
|
|
prev_randao: rpcExecutionPayload.prevRandao.asEth2Digest,
|
|
|
|
block_number: rpcExecutionPayload.blockNumber.uint64,
|
|
|
|
gas_limit: rpcExecutionPayload.gasLimit.uint64,
|
|
|
|
gas_used: rpcExecutionPayload.gasUsed.uint64,
|
|
|
|
timestamp: rpcExecutionPayload.timestamp.uint64,
|
2023-02-23 02:10:07 +00:00
|
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(rpcExecutionPayload.extraData.bytes),
|
2022-11-29 05:02:16 +00:00
|
|
|
base_fee_per_gas: rpcExecutionPayload.baseFeePerGas,
|
|
|
|
block_hash: rpcExecutionPayload.blockHash.asEth2Digest,
|
|
|
|
transactions: List[bellatrix.Transaction, MAX_TRANSACTIONS_PER_PAYLOAD].init(
|
|
|
|
mapIt(rpcExecutionPayload.transactions, it.getTransaction)),
|
2022-11-29 11:04:36 +00:00
|
|
|
withdrawals: List[capella.Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD].init(
|
2023-03-05 01:40:21 +00:00
|
|
|
mapIt(maybeDeref rpcExecutionPayload.withdrawals, it.asConsensusWithdrawal)))
|
|
|
|
|
|
|
|
func asConsensusType*(payloadWithValue: engine_api.GetPayloadV2Response):
|
|
|
|
capella.ExecutionPayloadForSigning =
|
|
|
|
capella.ExecutionPayloadForSigning(
|
|
|
|
executionPayload: payloadWithValue.executionPayload.asConsensusType,
|
|
|
|
blockValue: payloadWithValue.blockValue)
|
2022-11-29 05:02:16 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
func asConsensusType*(rpcExecutionPayload: ExecutionPayloadV3):
|
2023-02-25 01:03:34 +00:00
|
|
|
deneb.ExecutionPayload =
|
2023-01-11 18:21:19 +00:00
|
|
|
template getTransaction(tt: TypedTransaction): bellatrix.Transaction =
|
|
|
|
bellatrix.Transaction.init(tt.distinctBase)
|
|
|
|
|
2023-02-25 01:03:34 +00:00
|
|
|
deneb.ExecutionPayload(
|
2023-01-11 18:21:19 +00:00
|
|
|
parent_hash: rpcExecutionPayload.parentHash.asEth2Digest,
|
|
|
|
feeRecipient:
|
|
|
|
ExecutionAddress(data: rpcExecutionPayload.feeRecipient.distinctBase),
|
|
|
|
state_root: rpcExecutionPayload.stateRoot.asEth2Digest,
|
|
|
|
receipts_root: rpcExecutionPayload.receiptsRoot.asEth2Digest,
|
|
|
|
logs_bloom: BloomLogs(data: rpcExecutionPayload.logsBloom.distinctBase),
|
|
|
|
prev_randao: rpcExecutionPayload.prevRandao.asEth2Digest,
|
|
|
|
block_number: rpcExecutionPayload.blockNumber.uint64,
|
|
|
|
gas_limit: rpcExecutionPayload.gasLimit.uint64,
|
|
|
|
gas_used: rpcExecutionPayload.gasUsed.uint64,
|
|
|
|
timestamp: rpcExecutionPayload.timestamp.uint64,
|
2023-02-23 02:10:07 +00:00
|
|
|
extra_data: List[byte, MAX_EXTRA_DATA_BYTES].init(rpcExecutionPayload.extraData.bytes),
|
2023-01-11 18:21:19 +00:00
|
|
|
base_fee_per_gas: rpcExecutionPayload.baseFeePerGas,
|
|
|
|
block_hash: rpcExecutionPayload.blockHash.asEth2Digest,
|
|
|
|
transactions: List[bellatrix.Transaction, MAX_TRANSACTIONS_PER_PAYLOAD].init(
|
|
|
|
mapIt(rpcExecutionPayload.transactions, it.getTransaction)),
|
|
|
|
withdrawals: List[capella.Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD].init(
|
2023-05-05 23:15:47 +00:00
|
|
|
mapIt(rpcExecutionPayload.withdrawals, it.asConsensusWithdrawal)),
|
2023-08-02 22:07:57 +00:00
|
|
|
blob_gas_used: rpcExecutionPayload.blobGasUsed.uint64,
|
|
|
|
excess_blob_gas: rpcExecutionPayload.excessBlobGas.uint64)
|
2023-01-11 18:21:19 +00:00
|
|
|
|
2023-04-28 19:20:25 +00:00
|
|
|
func asConsensusType*(payload: engine_api.GetPayloadV3Response):
|
2023-03-05 01:40:21 +00:00
|
|
|
deneb.ExecutionPayloadForSigning =
|
|
|
|
deneb.ExecutionPayloadForSigning(
|
2023-04-28 19:20:25 +00:00
|
|
|
executionPayload: payload.executionPayload.asConsensusType,
|
|
|
|
blockValue: payload.blockValue,
|
2023-03-05 01:40:21 +00:00
|
|
|
# TODO
|
|
|
|
# The `mapIt` calls below are necessary only because we use different distinct
|
|
|
|
# types for KZG commitments and Blobs in the `web3` and the `deneb` spec types.
|
|
|
|
# Both are defined as `array[N, byte]` under the hood.
|
2023-11-04 13:49:58 +00:00
|
|
|
blobsBundle: BlobsBundle(
|
|
|
|
commitments: KzgCommitments.init(
|
|
|
|
payload.blobsBundle.commitments.mapIt(it.bytes)),
|
|
|
|
proofs: KzgProofs.init(
|
|
|
|
payload.blobsBundle.proofs.mapIt(it.bytes)),
|
|
|
|
blobs: Blobs.init(
|
|
|
|
payload.blobsBundle.blobs.mapIt(it.bytes))))
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2022-01-12 14:50:30 +00:00
|
|
|
func asEngineExecutionPayload*(executionPayload: bellatrix.ExecutionPayload):
|
2022-01-03 12:22:56 +00:00
|
|
|
ExecutionPayloadV1 =
|
2022-04-15 12:46:56 +00:00
|
|
|
template getTypedTransaction(tt: bellatrix.Transaction): TypedTransaction =
|
|
|
|
TypedTransaction(tt.distinctBase)
|
2022-01-03 12:22:56 +00:00
|
|
|
|
|
|
|
engine_api.ExecutionPayloadV1(
|
|
|
|
parentHash: executionPayload.parent_hash.asBlockHash,
|
2022-04-08 16:22:49 +00:00
|
|
|
feeRecipient: Address(executionPayload.fee_recipient.data),
|
2022-01-03 12:22:56 +00:00
|
|
|
stateRoot: executionPayload.state_root.asBlockHash,
|
|
|
|
receiptsRoot: executionPayload.receipts_root.asBlockHash,
|
|
|
|
logsBloom:
|
|
|
|
FixedBytes[BYTES_PER_LOGS_BLOOM](executionPayload.logs_bloom.data),
|
2022-03-03 16:08:14 +00:00
|
|
|
prevRandao: executionPayload.prev_randao.asBlockHash,
|
2022-01-03 12:22:56 +00:00
|
|
|
blockNumber: Quantity(executionPayload.block_number),
|
2022-11-24 07:53:04 +00:00
|
|
|
gasLimit: Quantity(executionPayload.gas_limit),
|
|
|
|
gasUsed: Quantity(executionPayload.gas_used),
|
|
|
|
timestamp: Quantity(executionPayload.timestamp),
|
2023-02-23 02:10:07 +00:00
|
|
|
extraData: DynamicBytes[0, MAX_EXTRA_DATA_BYTES](executionPayload.extra_data),
|
2022-11-24 07:53:04 +00:00
|
|
|
baseFeePerGas: executionPayload.base_fee_per_gas,
|
|
|
|
blockHash: executionPayload.block_hash.asBlockHash,
|
|
|
|
transactions: mapIt(executionPayload.transactions, it.getTypedTransaction))
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
template toEngineWithdrawal(w: capella.Withdrawal): WithdrawalV1 =
|
|
|
|
WithdrawalV1(
|
|
|
|
index: Quantity(w.index),
|
|
|
|
validatorIndex: Quantity(w.validator_index),
|
|
|
|
address: Address(w.address.data),
|
|
|
|
amount: Quantity(w.amount))
|
|
|
|
|
2022-11-24 07:53:04 +00:00
|
|
|
func asEngineExecutionPayload*(executionPayload: capella.ExecutionPayload):
|
2022-11-29 05:02:16 +00:00
|
|
|
ExecutionPayloadV2 =
|
2022-11-24 07:53:04 +00:00
|
|
|
template getTypedTransaction(tt: bellatrix.Transaction): TypedTransaction =
|
|
|
|
TypedTransaction(tt.distinctBase)
|
2022-11-29 05:02:16 +00:00
|
|
|
engine_api.ExecutionPayloadV2(
|
2022-11-24 07:53:04 +00:00
|
|
|
parentHash: executionPayload.parent_hash.asBlockHash,
|
|
|
|
feeRecipient: Address(executionPayload.fee_recipient.data),
|
|
|
|
stateRoot: executionPayload.state_root.asBlockHash,
|
|
|
|
receiptsRoot: executionPayload.receipts_root.asBlockHash,
|
|
|
|
logsBloom:
|
|
|
|
FixedBytes[BYTES_PER_LOGS_BLOOM](executionPayload.logs_bloom.data),
|
|
|
|
prevRandao: executionPayload.prev_randao.asBlockHash,
|
|
|
|
blockNumber: Quantity(executionPayload.block_number),
|
2022-01-03 12:22:56 +00:00
|
|
|
gasLimit: Quantity(executionPayload.gas_limit),
|
|
|
|
gasUsed: Quantity(executionPayload.gas_used),
|
|
|
|
timestamp: Quantity(executionPayload.timestamp),
|
2023-02-23 02:10:07 +00:00
|
|
|
extraData: DynamicBytes[0, MAX_EXTRA_DATA_BYTES](executionPayload.extra_data),
|
2022-06-01 12:57:28 +00:00
|
|
|
baseFeePerGas: executionPayload.base_fee_per_gas,
|
2022-01-03 12:22:56 +00:00
|
|
|
blockHash: executionPayload.block_hash.asBlockHash,
|
2022-11-29 05:02:16 +00:00
|
|
|
transactions: mapIt(executionPayload.transactions, it.getTypedTransaction),
|
2023-03-05 01:40:21 +00:00
|
|
|
withdrawals: mapIt(executionPayload.withdrawals, it.toEngineWithdrawal))
|
2022-01-03 12:22:56 +00:00
|
|
|
|
2023-02-25 01:03:34 +00:00
|
|
|
func asEngineExecutionPayload*(executionPayload: deneb.ExecutionPayload):
|
2023-01-11 18:21:19 +00:00
|
|
|
ExecutionPayloadV3 =
|
|
|
|
template getTypedTransaction(tt: bellatrix.Transaction): TypedTransaction =
|
|
|
|
TypedTransaction(tt.distinctBase)
|
|
|
|
|
|
|
|
engine_api.ExecutionPayloadV3(
|
|
|
|
parentHash: executionPayload.parent_hash.asBlockHash,
|
|
|
|
feeRecipient: Address(executionPayload.fee_recipient.data),
|
|
|
|
stateRoot: executionPayload.state_root.asBlockHash,
|
|
|
|
receiptsRoot: executionPayload.receipts_root.asBlockHash,
|
|
|
|
logsBloom:
|
|
|
|
FixedBytes[BYTES_PER_LOGS_BLOOM](executionPayload.logs_bloom.data),
|
|
|
|
prevRandao: executionPayload.prev_randao.asBlockHash,
|
|
|
|
blockNumber: Quantity(executionPayload.block_number),
|
|
|
|
gasLimit: Quantity(executionPayload.gas_limit),
|
|
|
|
gasUsed: Quantity(executionPayload.gas_used),
|
|
|
|
timestamp: Quantity(executionPayload.timestamp),
|
2023-02-23 02:10:07 +00:00
|
|
|
extraData: DynamicBytes[0, MAX_EXTRA_DATA_BYTES](executionPayload.extra_data),
|
2023-01-11 18:21:19 +00:00
|
|
|
baseFeePerGas: executionPayload.base_fee_per_gas,
|
|
|
|
blockHash: executionPayload.block_hash.asBlockHash,
|
|
|
|
transactions: mapIt(executionPayload.transactions, it.getTypedTransaction),
|
2023-05-05 23:15:47 +00:00
|
|
|
withdrawals: mapIt(executionPayload.withdrawals, it.asEngineWithdrawal),
|
2023-08-02 22:07:57 +00:00
|
|
|
blobGasUsed: Quantity(executionPayload.blob_gas_used),
|
|
|
|
excessBlobGas: Quantity(executionPayload.excess_blob_gas))
|
2023-01-11 18:21:19 +00:00
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
func isConnected(connection: ELConnection): bool =
|
2023-03-05 01:40:21 +00:00
|
|
|
connection.web3.isSome
|
|
|
|
|
2023-11-27 14:48:29 +00:00
|
|
|
func getJsonRpcRequestHeaders(jwtSecret: Opt[seq[byte]]):
|
2023-03-05 01:40:21 +00:00
|
|
|
auto =
|
|
|
|
if jwtSecret.isSome:
|
|
|
|
let secret = jwtSecret.get
|
|
|
|
(proc(): seq[(string, string)] =
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc6750#section-6.1.1
|
|
|
|
@[("Authorization", "Bearer " & getSignedIatToken(
|
|
|
|
secret, (getTime() - initTime(0, 0)).inSeconds))])
|
|
|
|
else:
|
|
|
|
(proc(): seq[(string, string)] = @[])
|
|
|
|
|
|
|
|
proc newWeb3*(engineUrl: EngineApiUrl): Future[Web3] =
|
2023-05-04 23:27:26 +00:00
|
|
|
newWeb3(engineUrl.url,
|
|
|
|
getJsonRpcRequestHeaders(engineUrl.jwtSecret),
|
|
|
|
httpFlags = {HttpClientFlag.NewConnectionAlways})
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
proc establishEngineApiConnection*(url: EngineApiUrl):
|
|
|
|
Future[Result[Web3, string]] {.async.} =
|
2024-01-19 07:18:02 +00:00
|
|
|
try:
|
|
|
|
ok(await newWeb3(url).wait(engineApiConnectionTimeout))
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
err "Engine API connection timed out"
|
|
|
|
except CancelledError as exc:
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
err "Engine API connection failed: " & exc.msg
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc tryConnecting(connection: ELConnection): Future[bool] {.async.} =
|
|
|
|
if connection.isConnected:
|
|
|
|
return true
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2023-03-09 23:41:28 +00:00
|
|
|
if connection.connectingFut == nil or
|
|
|
|
connection.connectingFut.finished: # The previous attempt was not successful
|
2023-03-05 01:40:21 +00:00
|
|
|
connection.connectingFut = establishEngineApiConnection(connection.engineUrl)
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
let web3Res = await connection.connectingFut
|
|
|
|
if web3Res.isErr:
|
|
|
|
return false
|
|
|
|
else:
|
|
|
|
connection.web3 = some web3Res.get
|
|
|
|
return true
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc connectedRpcClient(connection: ELConnection): Future[RpcClient] {.async.} =
|
|
|
|
while not connection.isConnected:
|
|
|
|
if not await connection.tryConnecting():
|
|
|
|
await sleepAsync(chronos.seconds(10))
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
return connection.web3.get.provider
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc getBlockByHash(rpcClient: RpcClient, hash: BlockHash): Future[BlockObject] =
|
|
|
|
rpcClient.eth_getBlockByHash(hash, false)
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc getBlockByNumber*(rpcClient: RpcClient,
|
2022-02-04 12:12:19 +00:00
|
|
|
number: Eth1BlockNumber): Future[BlockObject] =
|
2023-03-05 01:40:21 +00:00
|
|
|
let hexNumber = try:
|
|
|
|
&"0x{number:X}" # No leading 0's!
|
|
|
|
except ValueError as exc:
|
|
|
|
# Since the format above is valid, failing here should not be possible
|
|
|
|
raiseAssert exc.msg
|
2023-01-21 00:47:38 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient.eth_getBlockByNumber(hexNumber, false)
|
|
|
|
|
|
|
|
func areSameAs(expectedParams: Option[NextExpectedPayloadParams],
|
|
|
|
latestHead, latestSafe, latestFinalized: Eth2Digest,
|
|
|
|
timestamp: uint64,
|
|
|
|
randomData: Eth2Digest,
|
|
|
|
feeRecipient: Eth1Address,
|
2023-12-01 12:24:23 +00:00
|
|
|
withdrawals: seq[WithdrawalV1]): bool =
|
2023-03-06 16:19:15 +00:00
|
|
|
expectedParams.isSome and
|
|
|
|
expectedParams.get.headBlockHash == latestHead and
|
|
|
|
expectedParams.get.safeBlockHash == latestSafe and
|
|
|
|
expectedParams.get.finalizedBlockHash == latestFinalized and
|
|
|
|
expectedParams.get.payloadAttributes.timestamp.uint64 == timestamp and
|
|
|
|
expectedParams.get.payloadAttributes.prevRandao.bytes == randomData.data and
|
|
|
|
expectedParams.get.payloadAttributes.suggestedFeeRecipient == feeRecipient and
|
2023-12-01 12:24:23 +00:00
|
|
|
expectedParams.get.payloadAttributes.withdrawals == withdrawals
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
proc forkchoiceUpdated(rpcClient: RpcClient,
|
|
|
|
state: ForkchoiceStateV1,
|
2023-04-17 14:17:52 +00:00
|
|
|
payloadAttributes: Option[PayloadAttributesV1] |
|
2023-08-15 23:00:35 +00:00
|
|
|
Option[PayloadAttributesV2] |
|
|
|
|
Option[PayloadAttributesV3]):
|
2023-03-06 16:19:15 +00:00
|
|
|
Future[ForkchoiceUpdatedResponse] =
|
2023-04-17 14:17:52 +00:00
|
|
|
when payloadAttributes is Option[PayloadAttributesV1]:
|
|
|
|
rpcClient.engine_forkchoiceUpdatedV1(state, payloadAttributes)
|
|
|
|
elif payloadAttributes is Option[PayloadAttributesV2]:
|
|
|
|
rpcClient.engine_forkchoiceUpdatedV2(state, payloadAttributes)
|
2023-08-15 23:00:35 +00:00
|
|
|
elif payloadAttributes is Option[PayloadAttributesV3]:
|
|
|
|
rpcClient.engine_forkchoiceUpdatedV3(state, payloadAttributes)
|
2023-01-06 21:01:10 +00:00
|
|
|
else:
|
2023-03-06 16:19:15 +00:00
|
|
|
static: doAssert false
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2023-08-25 09:29:07 +00:00
|
|
|
func computeBlockValue(blk: ExecutionPayloadV1): UInt256 {.raises: [RlpError].} =
|
2023-03-05 01:40:21 +00:00
|
|
|
for transactionBytes in blk.transactions:
|
|
|
|
var rlp = rlpFromBytes distinctBase(transactionBytes)
|
|
|
|
let transaction = rlp.read(eth_types.Transaction)
|
|
|
|
result += distinctBase(effectiveGasTip(transaction, blk.baseFeePerGas)).u256
|
|
|
|
|
|
|
|
proc getPayloadFromSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
GetPayloadResponseType: type,
|
|
|
|
isForkChoiceUpToDate: bool,
|
2023-08-15 23:00:35 +00:00
|
|
|
consensusHead: Eth2Digest,
|
2023-03-05 01:40:21 +00:00
|
|
|
headBlock, safeBlock, finalizedBlock: Eth2Digest,
|
|
|
|
timestamp: uint64,
|
|
|
|
randomData: Eth2Digest,
|
|
|
|
suggestedFeeRecipient: Eth1Address,
|
|
|
|
withdrawals: seq[WithdrawalV1]): Future[GetPayloadResponseType] {.async.} =
|
|
|
|
|
|
|
|
let
|
|
|
|
rpcClient = await connection.connectedRpcClient()
|
|
|
|
payloadId = if isForkChoiceUpToDate and connection.lastPayloadId.isSome:
|
|
|
|
connection.lastPayloadId.get
|
|
|
|
elif not headBlock.isZero:
|
|
|
|
engine_api_last_minute_forkchoice_updates_sent.inc(1, [connection.engineUrl.url])
|
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
when GetPayloadResponseType is BellatrixExecutionPayloadWithValue:
|
|
|
|
let response = await rpcClient.forkchoiceUpdated(
|
|
|
|
ForkchoiceStateV1(
|
|
|
|
headBlockHash: headBlock.asBlockHash,
|
|
|
|
safeBlockHash: safeBlock.asBlockHash,
|
|
|
|
finalizedBlockHash: finalizedBlock.asBlockHash),
|
2023-04-17 14:17:52 +00:00
|
|
|
some PayloadAttributesV1(
|
2023-03-06 16:19:15 +00:00
|
|
|
timestamp: Quantity timestamp,
|
|
|
|
prevRandao: FixedBytes[32] randomData.data,
|
|
|
|
suggestedFeeRecipient: suggestedFeeRecipient))
|
2023-08-15 23:00:35 +00:00
|
|
|
elif GetPayloadResponseType is engine_api.GetPayloadV2Response:
|
2023-03-06 16:19:15 +00:00
|
|
|
let response = await rpcClient.forkchoiceUpdated(
|
|
|
|
ForkchoiceStateV1(
|
|
|
|
headBlockHash: headBlock.asBlockHash,
|
|
|
|
safeBlockHash: safeBlock.asBlockHash,
|
|
|
|
finalizedBlockHash: finalizedBlock.asBlockHash),
|
2023-04-17 14:17:52 +00:00
|
|
|
some PayloadAttributesV2(
|
2023-03-06 16:19:15 +00:00
|
|
|
timestamp: Quantity timestamp,
|
|
|
|
prevRandao: FixedBytes[32] randomData.data,
|
|
|
|
suggestedFeeRecipient: suggestedFeeRecipient,
|
|
|
|
withdrawals: withdrawals))
|
2023-08-15 23:00:35 +00:00
|
|
|
elif GetPayloadResponseType is engine_api.GetPayloadV3Response:
|
|
|
|
let response = await rpcClient.forkchoiceUpdated(
|
|
|
|
ForkchoiceStateV1(
|
|
|
|
headBlockHash: headBlock.asBlockHash,
|
|
|
|
safeBlockHash: safeBlock.asBlockHash,
|
|
|
|
finalizedBlockHash: finalizedBlock.asBlockHash),
|
|
|
|
some PayloadAttributesV3(
|
|
|
|
timestamp: Quantity timestamp,
|
|
|
|
prevRandao: FixedBytes[32] randomData.data,
|
|
|
|
suggestedFeeRecipient: suggestedFeeRecipient,
|
|
|
|
withdrawals: withdrawals,
|
|
|
|
parentBeaconBlockRoot: consensusHead.asBlockHash))
|
2023-03-06 16:19:15 +00:00
|
|
|
else:
|
|
|
|
static: doAssert false
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
if response.payloadStatus.status != PayloadExecutionStatus.valid or
|
|
|
|
response.payloadId.isNone:
|
|
|
|
raise newException(CatchableError, "Head block is not a valid payload")
|
|
|
|
|
|
|
|
# Give the EL some time to assemble the block
|
|
|
|
await sleepAsync(chronos.milliseconds 500)
|
|
|
|
|
|
|
|
response.payloadId.get
|
|
|
|
else:
|
|
|
|
raise newException(CatchableError, "No confirmed execution head yet")
|
|
|
|
|
2023-04-28 19:20:25 +00:00
|
|
|
when GetPayloadResponseType is BellatrixExecutionPayloadWithValue:
|
2023-08-15 23:00:35 +00:00
|
|
|
let payload =
|
|
|
|
await engine_api.getPayload(rpcClient, ExecutionPayloadV1, payloadId)
|
2023-03-05 01:40:21 +00:00
|
|
|
return BellatrixExecutionPayloadWithValue(
|
|
|
|
executionPayload: payload,
|
|
|
|
blockValue: computeBlockValue payload)
|
|
|
|
else:
|
|
|
|
return await engine_api.getPayload(rpcClient, GetPayloadResponseType, payloadId)
|
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
func cmpGetPayloadResponses(lhs, rhs: SomeEnginePayloadWithValue): int =
|
2023-03-05 01:40:21 +00:00
|
|
|
cmp(distinctBase lhs.blockValue, distinctBase rhs.blockValue)
|
2021-05-20 10:44:13 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
template EngineApiResponseType*(T: type bellatrix.ExecutionPayloadForSigning): type =
|
|
|
|
BellatrixExecutionPayloadWithValue
|
|
|
|
|
|
|
|
template EngineApiResponseType*(T: type capella.ExecutionPayloadForSigning): type =
|
|
|
|
engine_api.GetPayloadV2Response
|
|
|
|
|
|
|
|
template EngineApiResponseType*(T: type deneb.ExecutionPayloadForSigning): type =
|
2023-04-28 19:20:25 +00:00
|
|
|
engine_api.GetPayloadV3Response
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
template toEngineWithdrawals*(withdrawals: seq[capella.Withdrawal]): seq[WithdrawalV1] =
|
|
|
|
mapIt(withdrawals, toEngineWithdrawal(it))
|
|
|
|
|
2023-09-27 15:10:28 +00:00
|
|
|
template kind(T: type ExecutionPayloadV1): ConsensusFork =
|
2023-03-05 01:40:21 +00:00
|
|
|
ConsensusFork.Bellatrix
|
|
|
|
|
2023-09-27 15:10:28 +00:00
|
|
|
template kind(T: typedesc[ExecutionPayloadV1OrV2|ExecutionPayloadV2]): ConsensusFork =
|
2023-03-05 01:40:21 +00:00
|
|
|
ConsensusFork.Capella
|
|
|
|
|
2023-09-27 15:10:28 +00:00
|
|
|
template kind(T: type ExecutionPayloadV3): ConsensusFork =
|
2023-03-05 01:40:21 +00:00
|
|
|
ConsensusFork.Deneb
|
|
|
|
|
|
|
|
proc getPayload*(m: ELManager,
|
|
|
|
PayloadType: type ForkyExecutionPayloadForSigning,
|
2023-08-15 23:00:35 +00:00
|
|
|
consensusHead: Eth2Digest,
|
2023-03-05 01:40:21 +00:00
|
|
|
headBlock, safeBlock, finalizedBlock: Eth2Digest,
|
|
|
|
timestamp: uint64,
|
|
|
|
randomData: Eth2Digest,
|
|
|
|
suggestedFeeRecipient: Eth1Address,
|
|
|
|
withdrawals: seq[capella.Withdrawal]):
|
|
|
|
Future[Opt[PayloadType]] {.async.} =
|
|
|
|
if m.elConnections.len == 0:
|
|
|
|
return err()
|
|
|
|
|
|
|
|
let
|
|
|
|
engineApiWithdrawals = toEngineWithdrawals withdrawals
|
2023-08-15 23:00:35 +00:00
|
|
|
isFcUpToDate = m.nextExpectedPayloadParams.areSameAs(
|
|
|
|
headBlock, safeBlock, finalizedBlock, timestamp,
|
2023-12-01 12:24:23 +00:00
|
|
|
randomData, suggestedFeeRecipient, engineApiWithdrawals)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2023-07-18 20:29:50 +00:00
|
|
|
# `getPayloadFromSingleEL` may introduce additional latency
|
|
|
|
const extraProcessingOverhead = 500.milliseconds
|
2023-03-05 01:40:21 +00:00
|
|
|
let
|
2023-07-18 20:29:50 +00:00
|
|
|
timeout = GETPAYLOAD_TIMEOUT + extraProcessingOverhead
|
2023-03-05 01:40:21 +00:00
|
|
|
deadline = sleepAsync(timeout)
|
|
|
|
requests = m.elConnections.mapIt(it.getPayloadFromSingleEL(
|
|
|
|
EngineApiResponseType(PayloadType),
|
2023-08-15 23:00:35 +00:00
|
|
|
isFcUpToDate, consensusHead, headBlock, safeBlock, finalizedBlock,
|
2023-03-05 01:40:21 +00:00
|
|
|
timestamp, randomData, suggestedFeeRecipient, engineApiWithdrawals
|
|
|
|
))
|
|
|
|
requestsCompleted = allFutures(requests)
|
|
|
|
|
|
|
|
await requestsCompleted or deadline
|
|
|
|
|
|
|
|
var bestPayloadIdx = none int
|
|
|
|
for idx, req in requests:
|
|
|
|
if not req.finished:
|
2023-09-22 11:06:27 +00:00
|
|
|
req.cancelSoon()
|
2023-03-05 01:40:21 +00:00
|
|
|
elif req.failed:
|
|
|
|
error "Failed to get execution payload from EL",
|
2023-03-10 09:12:29 +00:00
|
|
|
url = m.elConnections[idx].engineUrl.url,
|
|
|
|
err = req.error.msg
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
2023-09-27 15:10:28 +00:00
|
|
|
const payloadFork = PayloadType.kind
|
2023-03-05 01:40:21 +00:00
|
|
|
when payloadFork >= ConsensusFork.Capella:
|
|
|
|
when payloadFork == ConsensusFork.Capella:
|
|
|
|
# TODO: The engine_api module may offer an alternative API where it is guaranteed
|
|
|
|
# to return the correct response type (i.e. the rule below will be enforced
|
|
|
|
# during deserialization).
|
|
|
|
if req.read.executionPayload.withdrawals.isNone:
|
2023-03-10 09:12:29 +00:00
|
|
|
warn "Execution client returned a block without a 'withdrawals' field for a post-Shanghai block",
|
2023-03-05 01:40:21 +00:00
|
|
|
url = m.elConnections[idx].engineUrl.url
|
|
|
|
continue
|
|
|
|
|
|
|
|
if engineApiWithdrawals != req.read.executionPayload.withdrawals.maybeDeref:
|
2023-04-08 09:15:09 +00:00
|
|
|
# otherwise it formats as "@[(index: ..., validatorIndex: ...,
|
|
|
|
# address: ..., amount: ...), (index: ..., validatorIndex: ...,
|
|
|
|
# address: ..., amount: ...)]"
|
2023-03-05 01:40:21 +00:00
|
|
|
warn "Execution client did not return correct withdrawals",
|
2023-04-08 09:15:09 +00:00
|
|
|
withdrawals_from_cl_len = engineApiWithdrawals.len,
|
|
|
|
withdrawals_from_el_len =
|
|
|
|
req.read.executionPayload.withdrawals.maybeDeref.len,
|
|
|
|
withdrawals_from_cl =
|
|
|
|
mapIt(engineApiWithdrawals, it.asConsensusWithdrawal),
|
|
|
|
withdrawals_from_el =
|
|
|
|
mapIt(
|
|
|
|
req.read.executionPayload.withdrawals.maybeDeref,
|
|
|
|
it.asConsensusWithdrawal)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2023-03-07 04:43:27 +00:00
|
|
|
if req.read.executionPayload.extraData.len > MAX_EXTRA_DATA_BYTES:
|
|
|
|
warn "Execution client provided a block with invalid extraData (size exceeds limit)",
|
2023-03-09 17:29:04 +00:00
|
|
|
size = req.read.executionPayload.extraData.len,
|
|
|
|
limit = MAX_EXTRA_DATA_BYTES
|
2023-03-07 04:43:27 +00:00
|
|
|
continue
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
if bestPayloadIdx.isNone:
|
|
|
|
bestPayloadIdx = some idx
|
|
|
|
else:
|
|
|
|
if cmpGetPayloadResponses(req.read, requests[bestPayloadIdx.get].read) > 0:
|
|
|
|
bestPayloadIdx = some idx
|
|
|
|
|
|
|
|
if bestPayloadIdx.isSome:
|
|
|
|
return ok requests[bestPayloadIdx.get].read.asConsensusType
|
|
|
|
else:
|
|
|
|
return err()
|
|
|
|
|
|
|
|
proc waitELToSyncDeposits(connection: ELConnection,
|
|
|
|
minimalRequiredBlock: BlockHash) {.async.} =
|
|
|
|
var rpcClient = await connection.connectedRpcClient()
|
|
|
|
|
|
|
|
if connection.depositContractSyncStatus == DepositContractSyncStatus.synced:
|
|
|
|
return
|
|
|
|
|
|
|
|
var attempt = 0
|
|
|
|
|
|
|
|
while true:
|
|
|
|
try:
|
2024-01-08 16:53:29 +00:00
|
|
|
discard raiseIfNil connection.trackedRequestWithTimeout(
|
2023-03-09 17:29:04 +00:00
|
|
|
"getBlockByHash",
|
|
|
|
rpcClient.getBlockByHash(minimalRequiredBlock),
|
2023-03-21 22:19:36 +00:00
|
|
|
web3RequestsTimeout,
|
|
|
|
failureAllowed = true)
|
2023-03-05 01:40:21 +00:00
|
|
|
connection.depositContractSyncStatus = DepositContractSyncStatus.synced
|
|
|
|
return
|
|
|
|
except CancelledError as err:
|
|
|
|
trace "waitELToSyncDepositContract cancelled",
|
|
|
|
url = connection.engineUrl.url
|
|
|
|
raise err
|
|
|
|
except CatchableError as err:
|
|
|
|
connection.depositContractSyncStatus = DepositContractSyncStatus.notSynced
|
|
|
|
if attempt == 0:
|
|
|
|
warn "Failed to obtain the most recent known block from the execution " &
|
|
|
|
"layer node (the node is probably not synced)",
|
|
|
|
url = connection.engineUrl.url,
|
|
|
|
blk = minimalRequiredBlock,
|
|
|
|
err = err.msg
|
|
|
|
elif attempt mod 60 == 0:
|
|
|
|
# This warning will be produced every 30 minutes
|
|
|
|
warn "Still failing to obtain the most recent known block from the " &
|
|
|
|
"execution layer node (the node is probably still not synced)",
|
|
|
|
url = connection.engineUrl.url,
|
|
|
|
blk = minimalRequiredBlock,
|
|
|
|
err = err.msg
|
2023-03-10 10:55:55 +00:00
|
|
|
inc attempt
|
2023-03-05 01:40:21 +00:00
|
|
|
await sleepAsync(seconds(30))
|
|
|
|
rpcClient = await connection.connectedRpcClient()
|
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
func networkHasDepositContract(m: ELManager): bool =
|
2023-03-05 01:40:21 +00:00
|
|
|
not m.cfg.DEPOSIT_CONTRACT_ADDRESS.isDefaultValue
|
|
|
|
|
|
|
|
func mostRecentKnownBlock(m: ELManager): BlockHash =
|
|
|
|
if m.eth1Chain.finalizedDepositsMerkleizer.getChunkCount() > 0:
|
|
|
|
m.eth1Chain.finalizedBlockHash.asBlockHash
|
|
|
|
else:
|
|
|
|
m.depositContractBlockHash
|
|
|
|
|
|
|
|
proc selectConnectionForChainSyncing(m: ELManager): Future[ELConnection] {.async.} =
|
|
|
|
doAssert m.elConnections.len > 0
|
|
|
|
|
|
|
|
let connectionsFuts = mapIt(
|
|
|
|
m.elConnections,
|
|
|
|
if m.networkHasDepositContract:
|
|
|
|
FutureBase waitELToSyncDeposits(it, m.mostRecentKnownBlock)
|
|
|
|
else:
|
|
|
|
FutureBase connectedRpcClient(it))
|
|
|
|
|
|
|
|
# TODO: Ideally, the cancellation will be handled automatically
|
|
|
|
# by a helper like `firstCompletedFuture`
|
2023-03-10 10:55:55 +00:00
|
|
|
let firstConnected = try:
|
|
|
|
await firstCompletedFuture(connectionsFuts)
|
|
|
|
except CancelledError as err:
|
|
|
|
for future in connectionsFuts:
|
2023-09-22 11:06:27 +00:00
|
|
|
future.cancelSoon()
|
2023-03-10 10:55:55 +00:00
|
|
|
raise err
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
for future in connectionsFuts:
|
|
|
|
if future != firstConnected:
|
2023-09-22 11:06:27 +00:00
|
|
|
future.cancelSoon()
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
return m.elConnections[find(connectionsFuts, firstConnected)]
|
|
|
|
|
|
|
|
proc sendNewPayloadToSingleEL(connection: ELConnection,
|
|
|
|
payload: engine_api.ExecutionPayloadV1):
|
|
|
|
Future[PayloadStatusV1] {.async.} =
|
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
|
|
|
return await rpcClient.engine_newPayloadV1(payload)
|
|
|
|
|
|
|
|
proc sendNewPayloadToSingleEL(connection: ELConnection,
|
|
|
|
payload: engine_api.ExecutionPayloadV2):
|
|
|
|
Future[PayloadStatusV1] {.async.} =
|
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
|
|
|
return await rpcClient.engine_newPayloadV2(payload)
|
|
|
|
|
|
|
|
proc sendNewPayloadToSingleEL(connection: ELConnection,
|
2023-06-10 05:39:10 +00:00
|
|
|
payload: engine_api.ExecutionPayloadV3,
|
2023-06-30 08:14:20 +00:00
|
|
|
versioned_hashes: seq[engine_api.VersionedHash],
|
|
|
|
parent_beacon_block_root: FixedBytes[32]):
|
2023-03-05 01:40:21 +00:00
|
|
|
Future[PayloadStatusV1] {.async.} =
|
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
2023-06-30 08:14:20 +00:00
|
|
|
return await rpcClient.engine_newPayloadV3(
|
|
|
|
payload, versioned_hashes, parent_beacon_block_root)
|
2022-06-15 02:38:27 +00:00
|
|
|
|
2022-08-22 19:44:40 +00:00
|
|
|
type
|
2023-03-05 01:40:21 +00:00
|
|
|
StatusRelation = enum
|
|
|
|
newStatusIsPreferable
|
|
|
|
oldStatusIsOk
|
|
|
|
disagreement
|
|
|
|
|
2023-03-14 16:18:53 +00:00
|
|
|
func compareStatuses(newStatus, prevStatus: PayloadExecutionStatus): StatusRelation =
|
2023-03-05 01:40:21 +00:00
|
|
|
case prevStatus
|
|
|
|
of PayloadExecutionStatus.syncing:
|
|
|
|
if newStatus == PayloadExecutionStatus.syncing:
|
|
|
|
oldStatusIsOk
|
|
|
|
else:
|
|
|
|
newStatusIsPreferable
|
|
|
|
|
|
|
|
of PayloadExecutionStatus.valid:
|
|
|
|
case newStatus
|
|
|
|
of PayloadExecutionStatus.syncing,
|
|
|
|
PayloadExecutionStatus.accepted,
|
|
|
|
PayloadExecutionStatus.valid:
|
|
|
|
oldStatusIsOk
|
|
|
|
of PayloadExecutionStatus.invalid_block_hash,
|
|
|
|
PayloadExecutionStatus.invalid:
|
|
|
|
disagreement
|
|
|
|
|
|
|
|
of PayloadExecutionStatus.invalid:
|
|
|
|
case newStatus
|
|
|
|
of PayloadExecutionStatus.syncing,
|
|
|
|
PayloadExecutionStatus.invalid:
|
|
|
|
oldStatusIsOk
|
|
|
|
of PayloadExecutionStatus.valid,
|
|
|
|
PayloadExecutionStatus.accepted,
|
|
|
|
PayloadExecutionStatus.invalid_block_hash:
|
|
|
|
disagreement
|
|
|
|
|
|
|
|
of PayloadExecutionStatus.accepted:
|
|
|
|
case newStatus
|
|
|
|
of PayloadExecutionStatus.accepted,
|
|
|
|
PayloadExecutionStatus.syncing:
|
|
|
|
oldStatusIsOk
|
|
|
|
of PayloadExecutionStatus.valid:
|
|
|
|
newStatusIsPreferable
|
|
|
|
of PayloadExecutionStatus.invalid_block_hash,
|
|
|
|
PayloadExecutionStatus.invalid:
|
|
|
|
disagreement
|
|
|
|
|
|
|
|
of PayloadExecutionStatus.invalid_block_hash:
|
|
|
|
if newStatus == PayloadExecutionStatus.invalid_block_hash:
|
|
|
|
oldStatusIsOk
|
|
|
|
else:
|
|
|
|
disagreement
|
|
|
|
|
|
|
|
type
|
|
|
|
ELConsensusViolationDetector = object
|
|
|
|
selectedResponse: Option[int]
|
|
|
|
disagreementAlreadyDetected: bool
|
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
func init(T: type ELConsensusViolationDetector): T =
|
2023-03-05 01:40:21 +00:00
|
|
|
ELConsensusViolationDetector(selectedResponse: none int,
|
|
|
|
disagreementAlreadyDetected: false)
|
|
|
|
|
|
|
|
proc processResponse[ELResponseType](
|
|
|
|
d: var ELConsensusViolationDetector,
|
|
|
|
connections: openArray[ELConnection],
|
|
|
|
requests: openArray[Future[ELResponseType]],
|
|
|
|
idx: int) =
|
|
|
|
|
2023-03-21 21:35:22 +00:00
|
|
|
if not requests[idx].completed:
|
|
|
|
return
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
let status = try: requests[idx].read.status
|
|
|
|
except CatchableError: raiseAssert "checked above"
|
|
|
|
if d.selectedResponse.isNone:
|
|
|
|
d.selectedResponse = some idx
|
|
|
|
elif not d.disagreementAlreadyDetected:
|
|
|
|
let prevStatus = try: requests[d.selectedResponse.get].read.status
|
|
|
|
except CatchableError: raiseAssert "previously checked"
|
|
|
|
case compareStatuses(status, prevStatus)
|
|
|
|
of newStatusIsPreferable:
|
|
|
|
d.selectedResponse = some idx
|
|
|
|
of oldStatusIsOk:
|
|
|
|
discard
|
|
|
|
of disagreement:
|
|
|
|
d.disagreementAlreadyDetected = true
|
|
|
|
error "Execution layer consensus violation detected",
|
|
|
|
responseType = name(ELResponseType),
|
|
|
|
url1 = connections[d.selectedResponse.get].engineUrl.url,
|
|
|
|
status1 = prevStatus,
|
|
|
|
url2 = connections[idx].engineUrl.url,
|
|
|
|
status2 = status
|
|
|
|
|
2023-06-30 08:14:20 +00:00
|
|
|
proc sendNewPayload*(m: ELManager, blck: SomeForkyBeaconBlock):
|
2023-03-05 01:40:21 +00:00
|
|
|
Future[PayloadExecutionStatus] {.async.} =
|
|
|
|
let
|
|
|
|
earlyDeadline = sleepAsync(chronos.seconds 1)
|
2023-03-09 17:29:04 +00:00
|
|
|
startTime = Moment.now
|
2023-03-05 01:40:21 +00:00
|
|
|
deadline = sleepAsync(NEWPAYLOAD_TIMEOUT)
|
2023-06-30 08:14:20 +00:00
|
|
|
payload = blck.body.execution_payload.asEngineExecutionPayload
|
2023-03-05 01:40:21 +00:00
|
|
|
requests = m.elConnections.mapIt:
|
2023-06-10 05:39:10 +00:00
|
|
|
let req =
|
|
|
|
when payload is engine_api.ExecutionPayloadV3:
|
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.1/specs/deneb/beacon-chain.md#process_execution_payload
|
|
|
|
# Verify the execution payload is valid
|
|
|
|
# [Modified in Deneb] Pass `versioned_hashes` to Execution Engine
|
|
|
|
let versioned_hashes = mapIt(
|
2023-06-30 08:14:20 +00:00
|
|
|
blck.body.blob_kzg_commitments,
|
2023-06-10 05:39:10 +00:00
|
|
|
engine_api.VersionedHash(kzg_commitment_to_versioned_hash(it)))
|
2023-06-30 08:14:20 +00:00
|
|
|
sendNewPayloadToSingleEL(
|
|
|
|
it, payload, versioned_hashes,
|
|
|
|
FixedBytes[32] blck.parent_root.data)
|
2023-06-10 05:39:10 +00:00
|
|
|
elif payload is engine_api.ExecutionPayloadV1 or
|
|
|
|
payload is engine_api.ExecutionPayloadV2:
|
|
|
|
sendNewPayloadToSingleEL(it, payload)
|
|
|
|
else:
|
|
|
|
static: doAssert false
|
2023-03-09 17:29:04 +00:00
|
|
|
trackEngineApiRequest(it, req, "newPayload", startTime, deadline)
|
2023-03-05 01:40:21 +00:00
|
|
|
req
|
|
|
|
|
|
|
|
requestsCompleted = allFutures(requests)
|
|
|
|
|
|
|
|
await requestsCompleted or earlyDeadline
|
2022-08-22 19:44:40 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
var
|
|
|
|
stillPending = newSeq[Future[PayloadStatusV1]]()
|
|
|
|
responseProcessor = init ELConsensusViolationDetector
|
|
|
|
|
|
|
|
for idx, req in requests:
|
|
|
|
if not req.finished:
|
|
|
|
stillPending.add req
|
2023-03-21 21:35:22 +00:00
|
|
|
elif req.completed:
|
2023-03-05 01:40:21 +00:00
|
|
|
responseProcessor.processResponse(m.elConnections, requests, idx)
|
|
|
|
|
|
|
|
if responseProcessor.disagreementAlreadyDetected:
|
|
|
|
return PayloadExecutionStatus.invalid
|
|
|
|
elif responseProcessor.selectedResponse.isSome:
|
|
|
|
return requests[responseProcessor.selectedResponse.get].read.status
|
|
|
|
|
|
|
|
await requestsCompleted or deadline
|
|
|
|
|
|
|
|
for idx, req in requests:
|
|
|
|
if req.completed and req in stillPending:
|
|
|
|
responseProcessor.processResponse(m.elConnections, requests, idx)
|
|
|
|
|
|
|
|
return if responseProcessor.disagreementAlreadyDetected:
|
|
|
|
PayloadExecutionStatus.invalid
|
|
|
|
elif responseProcessor.selectedResponse.isSome:
|
|
|
|
requests[responseProcessor.selectedResponse.get].read.status
|
|
|
|
else:
|
|
|
|
PayloadExecutionStatus.syncing
|
|
|
|
|
|
|
|
proc forkchoiceUpdatedForSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
state: ref ForkchoiceStateV1,
|
2023-04-17 14:17:52 +00:00
|
|
|
payloadAttributes: Option[PayloadAttributesV1] |
|
2023-08-15 23:00:35 +00:00
|
|
|
Option[PayloadAttributesV2] |
|
|
|
|
Option[PayloadAttributesV3]):
|
2023-03-05 01:40:21 +00:00
|
|
|
Future[PayloadStatusV1] {.async.} =
|
|
|
|
let
|
|
|
|
rpcClient = await connection.connectedRpcClient()
|
|
|
|
response = await rpcClient.forkchoiceUpdated(state[], payloadAttributes)
|
|
|
|
|
|
|
|
if response.payloadStatus.status notin {syncing, valid, invalid}:
|
|
|
|
debug "Invalid fork-choice updated response from the EL",
|
|
|
|
payloadStatus = response.payloadStatus
|
|
|
|
return
|
|
|
|
|
|
|
|
if response.payloadStatus.status == PayloadExecutionStatus.valid and
|
|
|
|
response.payloadId.isSome:
|
|
|
|
connection.lastPayloadId = response.payloadId
|
|
|
|
|
|
|
|
return response.payloadStatus
|
|
|
|
|
|
|
|
proc forkchoiceUpdated*(m: ELManager,
|
2023-04-17 14:17:52 +00:00
|
|
|
headBlockHash, safeBlockHash,
|
|
|
|
finalizedBlockHash: Eth2Digest,
|
|
|
|
payloadAttributes: Option[PayloadAttributesV1] |
|
2023-08-15 23:00:35 +00:00
|
|
|
Option[PayloadAttributesV2] |
|
|
|
|
Option[PayloadAttributesV3]):
|
2024-01-22 16:34:54 +00:00
|
|
|
Future[(PayloadExecutionStatus, Option[BlockHash])] {.async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
doAssert not headBlockHash.isZero
|
|
|
|
|
|
|
|
# Allow finalizedBlockHash to be 0 to avoid sync deadlocks.
|
|
|
|
#
|
|
|
|
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3675.md#pos-events
|
|
|
|
# has "Before the first finalized block occurs in the system the finalized
|
|
|
|
# block hash provided by this event is stubbed with
|
|
|
|
# `0x0000000000000000000000000000000000000000000000000000000000000000`."
|
|
|
|
# and
|
2024-01-20 11:19:47 +00:00
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/bellatrix/validator.md#executionpayload
|
2023-03-05 01:40:21 +00:00
|
|
|
# notes "`finalized_block_hash` is the hash of the latest finalized execution
|
|
|
|
# payload (`Hash32()` if none yet finalized)"
|
|
|
|
|
|
|
|
if m.elConnections.len == 0:
|
|
|
|
return (PayloadExecutionStatus.syncing, none BlockHash)
|
|
|
|
|
2023-08-15 23:00:35 +00:00
|
|
|
when payloadAttributes is Option[PayloadAttributesV3]:
|
|
|
|
template payloadAttributesV3(): auto =
|
2023-04-17 14:17:52 +00:00
|
|
|
if payloadAttributes.isSome:
|
|
|
|
payloadAttributes.get
|
|
|
|
else:
|
|
|
|
# As timestamp and prevRandao are both 0, won't false-positive match
|
2023-08-15 23:00:35 +00:00
|
|
|
(static(default(PayloadAttributesV3)))
|
|
|
|
elif payloadAttributes is Option[PayloadAttributesV2]:
|
|
|
|
template payloadAttributesV3(): auto =
|
|
|
|
if payloadAttributes.isSome:
|
|
|
|
PayloadAttributesV3(
|
|
|
|
timestamp: payloadAttributes.get.timestamp,
|
|
|
|
prevRandao: payloadAttributes.get.prevRandao,
|
|
|
|
suggestedFeeRecipient: payloadAttributes.get.suggestedFeeRecipient,
|
|
|
|
withdrawals: payloadAttributes.get.withdrawals,
|
|
|
|
parentBeaconBlockRoot: static(default(FixedBytes[32])))
|
|
|
|
else:
|
|
|
|
# As timestamp and prevRandao are both 0, won't false-positive match
|
|
|
|
(static(default(PayloadAttributesV3)))
|
2023-04-17 14:17:52 +00:00
|
|
|
elif payloadAttributes is Option[PayloadAttributesV1]:
|
2023-08-15 23:00:35 +00:00
|
|
|
template payloadAttributesV3(): auto =
|
2023-04-17 14:17:52 +00:00
|
|
|
if payloadAttributes.isSome:
|
2023-08-15 23:00:35 +00:00
|
|
|
PayloadAttributesV3(
|
2023-04-17 14:17:52 +00:00
|
|
|
timestamp: payloadAttributes.get.timestamp,
|
|
|
|
prevRandao: payloadAttributes.get.prevRandao,
|
|
|
|
suggestedFeeRecipient: payloadAttributes.get.suggestedFeeRecipient,
|
2023-08-15 23:00:35 +00:00
|
|
|
withdrawals: @[],
|
|
|
|
parentBeaconBlockRoot: static(default(FixedBytes[32])))
|
2023-04-17 14:17:52 +00:00
|
|
|
else:
|
|
|
|
# As timestamp and prevRandao are both 0, won't false-positive match
|
2023-08-15 23:00:35 +00:00
|
|
|
(static(default(PayloadAttributesV3)))
|
2023-03-06 16:19:15 +00:00
|
|
|
else:
|
|
|
|
static: doAssert false
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
let
|
|
|
|
state = newClone ForkchoiceStateV1(
|
|
|
|
headBlockHash: headBlockHash.asBlockHash,
|
|
|
|
safeBlockHash: safeBlockHash.asBlockHash,
|
|
|
|
finalizedBlockHash: finalizedBlockHash.asBlockHash)
|
|
|
|
earlyDeadline = sleepAsync(chronos.seconds 1)
|
2023-03-09 17:29:04 +00:00
|
|
|
startTime = Moment.now
|
2023-03-05 01:40:21 +00:00
|
|
|
deadline = sleepAsync(FORKCHOICEUPDATED_TIMEOUT)
|
|
|
|
requests = m.elConnections.mapIt:
|
|
|
|
let req = it.forkchoiceUpdatedForSingleEL(state, payloadAttributes)
|
2023-03-09 17:29:04 +00:00
|
|
|
trackEngineApiRequest(it, req, "forkchoiceUpdated", startTime, deadline)
|
2023-03-05 01:40:21 +00:00
|
|
|
req
|
|
|
|
requestsCompleted = allFutures(requests)
|
|
|
|
|
|
|
|
await requestsCompleted or earlyDeadline
|
|
|
|
|
|
|
|
var
|
|
|
|
stillPending = newSeq[Future[PayloadStatusV1]]()
|
|
|
|
responseProcessor = init ELConsensusViolationDetector
|
|
|
|
|
|
|
|
for idx, req in requests:
|
|
|
|
if not req.finished:
|
|
|
|
stillPending.add req
|
2023-03-21 21:35:22 +00:00
|
|
|
elif req.completed:
|
2023-03-05 01:40:21 +00:00
|
|
|
responseProcessor.processResponse(m.elConnections, requests, idx)
|
|
|
|
|
2023-04-12 09:33:21 +00:00
|
|
|
template assignNextExpectedPayloadParams() =
|
|
|
|
# Ensure that there's no race condition window where getPayload's check for
|
|
|
|
# whether it needs to trigger a new fcU payload, due to cache invalidation,
|
|
|
|
# falsely suggests that the expected payload matches, and similarly that if
|
|
|
|
# the fcU fails or times out for other reasons, the expected payload params
|
|
|
|
# remain synchronized with EL state.
|
|
|
|
assign(
|
|
|
|
m.nextExpectedPayloadParams,
|
|
|
|
some NextExpectedPayloadParams(
|
|
|
|
headBlockHash: headBlockHash,
|
|
|
|
safeBlockHash: safeBlockHash,
|
|
|
|
finalizedBlockHash: finalizedBlockHash,
|
2023-08-15 23:00:35 +00:00
|
|
|
payloadAttributes: payloadAttributesV3))
|
2023-04-12 09:33:21 +00:00
|
|
|
|
2024-01-22 16:34:54 +00:00
|
|
|
template getSelected: untyped =
|
|
|
|
let
|
|
|
|
data =
|
|
|
|
try:
|
|
|
|
requests[responseProcessor.selectedResponse.get].read
|
|
|
|
except CatchableError:
|
|
|
|
raiseAssert "Only completed requests get selected"
|
|
|
|
(data.status, data.latestValidHash)
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
if responseProcessor.disagreementAlreadyDetected:
|
|
|
|
return (PayloadExecutionStatus.invalid, none BlockHash)
|
|
|
|
elif responseProcessor.selectedResponse.isSome:
|
2023-04-12 09:33:21 +00:00
|
|
|
assignNextExpectedPayloadParams()
|
2024-01-22 16:34:54 +00:00
|
|
|
return getSelected()
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
await requestsCompleted or deadline
|
|
|
|
|
|
|
|
for idx, req in requests:
|
|
|
|
if req.completed and req in stillPending:
|
|
|
|
responseProcessor.processResponse(m.elConnections, requests, idx)
|
|
|
|
|
|
|
|
return if responseProcessor.disagreementAlreadyDetected:
|
|
|
|
(PayloadExecutionStatus.invalid, none BlockHash)
|
|
|
|
elif responseProcessor.selectedResponse.isSome:
|
2023-04-12 09:33:21 +00:00
|
|
|
assignNextExpectedPayloadParams()
|
2024-01-22 16:34:54 +00:00
|
|
|
getSelected()
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
|
|
|
(PayloadExecutionStatus.syncing, none BlockHash)
|
|
|
|
|
|
|
|
# TODO can't be defined within exchangeConfigWithSingleEL
|
2023-03-06 16:19:15 +00:00
|
|
|
func `==`(x, y: Quantity): bool {.borrow.}
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
proc exchangeConfigWithSingleEL(m: ELManager, connection: ELConnection) {.async.} =
|
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
|
|
|
|
|
|
|
if m.eth1Network.isSome and
|
|
|
|
connection.etcStatus == EtcStatus.notExchangedYet:
|
2022-07-25 19:23:02 +00:00
|
|
|
try:
|
2023-03-05 01:40:21 +00:00
|
|
|
let
|
|
|
|
providerChain =
|
2023-03-09 17:29:04 +00:00
|
|
|
connection.trackedRequestWithTimeout(
|
|
|
|
"chainId",
|
|
|
|
rpcClient.eth_chainId(),
|
|
|
|
web3RequestsTimeout)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2023-12-01 13:32:45 +00:00
|
|
|
# https://chainid.network/
|
2023-03-05 01:40:21 +00:00
|
|
|
expectedChain = case m.eth1Network.get
|
|
|
|
of mainnet: 1.Quantity
|
|
|
|
of goerli: 5.Quantity
|
2023-12-01 13:32:45 +00:00
|
|
|
of sepolia: 11155111.Quantity
|
2023-09-08 05:53:27 +00:00
|
|
|
of holesky: 17000.Quantity
|
2023-03-05 01:40:21 +00:00
|
|
|
if expectedChain != providerChain:
|
|
|
|
warn "The specified EL client is connected to a different chain",
|
|
|
|
url = connection.engineUrl,
|
|
|
|
expectedChain = distinctBase(expectedChain),
|
|
|
|
actualChain = distinctBase(providerChain)
|
|
|
|
connection.etcStatus = EtcStatus.mismatch
|
|
|
|
return
|
|
|
|
except CatchableError as exc:
|
|
|
|
# Typically because it's not synced through EIP-155, assuming this Web3
|
|
|
|
# endpoint has been otherwise working.
|
|
|
|
debug "Failed to obtain eth_chainId",
|
|
|
|
error = exc.msg
|
|
|
|
|
2023-04-19 12:16:50 +00:00
|
|
|
connection.etcStatus = EtcStatus.match
|
|
|
|
|
2023-11-11 08:47:03 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/c4089414bbbe975bbc4bf1ccf0a3d31f76feb3e1/src/engine/cancun.md#deprecate-engine_exchangetransitionconfigurationv1
|
|
|
|
# Consensus layer clients MUST NOT call this method.
|
|
|
|
if m.eth1Chain.cfg.DENEB_FORK_EPOCH != FAR_FUTURE_EPOCH:
|
|
|
|
return
|
|
|
|
|
2023-04-17 20:11:28 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/paris.md#engine_exchangetransitionconfigurationv1
|
2023-03-05 01:40:21 +00:00
|
|
|
let
|
|
|
|
ourConf = TransitionConfigurationV1(
|
|
|
|
terminalTotalDifficulty: m.eth1Chain.cfg.TERMINAL_TOTAL_DIFFICULTY,
|
|
|
|
terminalBlockHash: m.eth1Chain.cfg.TERMINAL_BLOCK_HASH,
|
|
|
|
terminalBlockNumber: Quantity 0)
|
2023-04-19 12:16:50 +00:00
|
|
|
try:
|
|
|
|
discard connection.trackedRequestWithTimeout(
|
|
|
|
"exchangeTransitionConfiguration",
|
|
|
|
rpcClient.engine_exchangeTransitionConfigurationV1(ourConf),
|
|
|
|
timeout = 1.seconds)
|
|
|
|
except CatchableError as err:
|
|
|
|
warn "Failed to exchange transition configuration",
|
|
|
|
url = connection.engineUrl, err = err.msg
|
2022-06-15 02:38:27 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc exchangeTransitionConfiguration*(m: ELManager) {.async.} =
|
|
|
|
if m.elConnections.len == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
let
|
|
|
|
deadline = sleepAsync(3.seconds)
|
|
|
|
requests = m.elConnections.mapIt(m.exchangeConfigWithSingleEL(it))
|
|
|
|
requestsCompleted = allFutures(requests)
|
|
|
|
|
|
|
|
await requestsCompleted or deadline
|
|
|
|
|
2023-03-09 23:41:28 +00:00
|
|
|
var cancelled = 0
|
2023-03-05 01:40:21 +00:00
|
|
|
for idx, req in requests:
|
|
|
|
if not req.finished:
|
2023-09-22 11:06:27 +00:00
|
|
|
req.cancelSoon()
|
2023-03-09 23:41:28 +00:00
|
|
|
inc cancelled
|
|
|
|
|
|
|
|
if cancelled == requests.len:
|
|
|
|
warn "Failed to exchange configuration with the configured EL end-points"
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-01-13 01:36:17 +00:00
|
|
|
template readJsonField(logEvent, field: untyped, ValueType: type): untyped =
|
|
|
|
if logEvent.field.isNone:
|
|
|
|
raise newException(CatchableError,
|
|
|
|
"Web3 provider didn't return needed logEvent field " & astToStr(field))
|
|
|
|
logEvent.field.get
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2021-10-14 12:33:55 +00:00
|
|
|
template init[N: static int](T: type DynamicBytes[N, N]): T =
|
|
|
|
T newSeq[byte](N)
|
|
|
|
|
2023-03-09 17:29:04 +00:00
|
|
|
proc fetchTimestamp(connection: ELConnection,
|
|
|
|
rpcClient: RpcClient,
|
|
|
|
blk: Eth1Block) {.async.} =
|
|
|
|
debug "Fetching block timestamp", blockNum = blk.number
|
|
|
|
|
2024-01-08 16:53:29 +00:00
|
|
|
let web3block = raiseIfNil connection.trackedRequestWithTimeout(
|
2023-03-09 17:29:04 +00:00
|
|
|
"getBlockByHash",
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient.getBlockByHash(blk.hash.asBlockHash),
|
|
|
|
web3RequestsTimeout)
|
2023-03-09 17:29:04 +00:00
|
|
|
|
2022-08-10 12:31:10 +00:00
|
|
|
blk.timestamp = Eth1BlockTimestamp web3block.timestamp
|
|
|
|
|
2024-01-13 01:36:17 +00:00
|
|
|
func depositEventsToBlocks(depositsList: openArray[JsonString]): seq[Eth1Block] {.
|
2023-08-25 09:29:07 +00:00
|
|
|
raises: [CatchableError].} =
|
2020-06-27 12:01:19 +00:00
|
|
|
var lastEth1Block: Eth1Block
|
|
|
|
|
2024-01-13 01:36:17 +00:00
|
|
|
for logEventData in depositsList:
|
2020-06-27 12:01:19 +00:00
|
|
|
let
|
2024-01-13 01:36:17 +00:00
|
|
|
logEvent = JrpcConv.decode(logEventData.string, LogObject)
|
|
|
|
blockNumber = Eth1BlockNumber readJsonField(logEvent, blockNumber, Quantity)
|
|
|
|
blockHash = readJsonField(logEvent, blockHash, BlockHash)
|
2020-06-27 12:01:19 +00:00
|
|
|
|
|
|
|
if lastEth1Block == nil or lastEth1Block.number != blockNumber:
|
|
|
|
lastEth1Block = Eth1Block(
|
2022-08-10 12:31:10 +00:00
|
|
|
hash: blockHash.asEth2Digest,
|
|
|
|
number: blockNumber
|
|
|
|
# The `timestamp` is set in `syncBlockRange` immediately
|
|
|
|
# after calling this function, because we don't want to
|
|
|
|
# make this function `async`
|
|
|
|
)
|
2020-06-27 12:01:19 +00:00
|
|
|
|
|
|
|
result.add lastEth1Block
|
|
|
|
|
|
|
|
var
|
2021-10-14 12:33:55 +00:00
|
|
|
pubkey = init PubKeyBytes
|
|
|
|
withdrawalCredentials = init WithdrawalCredentialsBytes
|
|
|
|
amount = init Int64LeBytes
|
|
|
|
signature = init SignatureBytes
|
|
|
|
index = init Int64LeBytes
|
2020-06-27 12:01:19 +00:00
|
|
|
|
|
|
|
var offset = 0
|
2024-01-13 01:36:17 +00:00
|
|
|
offset += decode(logEvent.data, 0, offset, pubkey)
|
|
|
|
offset += decode(logEvent.data, 0, offset, withdrawalCredentials)
|
|
|
|
offset += decode(logEvent.data, 0, offset, amount)
|
|
|
|
offset += decode(logEvent.data, 0, offset, signature)
|
|
|
|
offset += decode(logEvent.data, 0, offset, index)
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2021-10-14 12:33:55 +00:00
|
|
|
if pubkey.len != 48 or
|
|
|
|
withdrawalCredentials.len != 32 or
|
|
|
|
amount.len != 8 or
|
|
|
|
signature.len != 96 or
|
|
|
|
index.len != 8:
|
|
|
|
raise newException(CorruptDataProvider, "Web3 provider supplied invalid deposit logs")
|
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
lastEth1Block.deposits.add DepositData(
|
2021-10-14 12:33:55 +00:00
|
|
|
pubkey: ValidatorPubKey.init(pubkey.toArray),
|
|
|
|
withdrawal_credentials: Eth2Digest(data: withdrawalCredentials.toArray),
|
|
|
|
amount: bytes_to_uint64(amount.toArray),
|
|
|
|
signature: ValidatorSig.init(signature.toArray))
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2020-12-01 11:14:32 +00:00
|
|
|
type
|
|
|
|
DepositContractDataStatus = enum
|
|
|
|
Fetched
|
|
|
|
VerifiedCorrect
|
|
|
|
DepositRootIncorrect
|
|
|
|
DepositRootUnavailable
|
|
|
|
DepositCountIncorrect
|
|
|
|
DepositCountUnavailable
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2020-12-01 11:14:32 +00:00
|
|
|
when hasDepositRootChecks:
|
2020-11-24 21:21:47 +00:00
|
|
|
const
|
2022-07-25 19:23:02 +00:00
|
|
|
contractCallTimeout = 60.seconds
|
2020-11-24 21:21:47 +00:00
|
|
|
|
2023-03-09 17:29:04 +00:00
|
|
|
proc fetchDepositContractData(connection: ELConnection,
|
|
|
|
rpcClient: RpcClient,
|
2024-02-06 22:03:52 +00:00
|
|
|
depositContract: Sender[DepositContract],
|
2023-03-05 01:40:21 +00:00
|
|
|
blk: Eth1Block): Future[DepositContractDataStatus] {.async.} =
|
2020-11-03 01:21:07 +00:00
|
|
|
let
|
2023-03-09 17:29:04 +00:00
|
|
|
startTime = Moment.now
|
|
|
|
deadline = sleepAsync(contractCallTimeout)
|
2023-03-05 01:40:21 +00:00
|
|
|
depositRoot = depositContract.get_deposit_root.call(blockNumber = blk.number)
|
|
|
|
rawCount = depositContract.get_deposit_count.call(blockNumber = blk.number)
|
2020-10-21 13:25:53 +00:00
|
|
|
|
2023-03-21 22:19:36 +00:00
|
|
|
# We allow failures on these requests becaues the clients
|
|
|
|
# are expected to prune the state data for historical blocks
|
2023-03-09 17:29:04 +00:00
|
|
|
connection.trackEngineApiRequest(
|
2023-03-21 22:19:36 +00:00
|
|
|
depositRoot, "get_deposit_root", startTime, deadline,
|
|
|
|
failureAllowed = true)
|
2023-03-09 17:29:04 +00:00
|
|
|
connection.trackEngineApiRequest(
|
2023-03-21 22:19:36 +00:00
|
|
|
rawCount, "get_deposit_count", startTime, deadline,
|
|
|
|
failureAllowed = true)
|
2023-03-09 17:29:04 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
try:
|
2024-02-06 22:03:52 +00:00
|
|
|
let fetchedRoot = asEth2Digest(block:
|
|
|
|
awaitWithTimeout(depositRoot, deadline):
|
|
|
|
raise newException(DataProviderTimeout,
|
|
|
|
"Request time out while obtaining deposits root"))
|
2022-08-10 12:31:10 +00:00
|
|
|
if blk.depositRoot.isZero:
|
|
|
|
blk.depositRoot = fetchedRoot
|
2020-11-03 01:21:07 +00:00
|
|
|
result = Fetched
|
2022-08-10 12:31:10 +00:00
|
|
|
elif blk.depositRoot == fetchedRoot:
|
2020-11-03 01:21:07 +00:00
|
|
|
result = VerifiedCorrect
|
|
|
|
else:
|
|
|
|
result = DepositRootIncorrect
|
|
|
|
except CatchableError as err:
|
|
|
|
debug "Failed to fetch deposits root",
|
|
|
|
blockNumber = blk.number,
|
|
|
|
err = err.msg
|
|
|
|
result = DepositRootUnavailable
|
2020-10-21 13:25:53 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
try:
|
2024-02-06 22:03:52 +00:00
|
|
|
let fetchedCount = bytes_to_uint64((block:
|
|
|
|
awaitWithTimeout(rawCount, deadline):
|
|
|
|
raise newException(DataProviderTimeout,
|
|
|
|
"Request time out while obtaining deposits count")).toArray)
|
2022-08-10 12:31:10 +00:00
|
|
|
if blk.depositCount == 0:
|
|
|
|
blk.depositCount = fetchedCount
|
|
|
|
elif blk.depositCount != fetchedCount:
|
2020-11-03 01:21:07 +00:00
|
|
|
result = DepositCountIncorrect
|
|
|
|
except CatchableError as err:
|
|
|
|
debug "Failed to fetch deposits count",
|
|
|
|
blockNumber = blk.number,
|
|
|
|
err = err.msg
|
|
|
|
result = DepositCountUnavailable
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2020-12-03 04:30:35 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
template trackFinalizedState*(m: ELManager,
|
2020-12-03 04:30:35 +00:00
|
|
|
finalizedEth1Data: Eth1Data,
|
|
|
|
finalizedStateDepositIndex: uint64): bool =
|
2023-03-05 01:40:21 +00:00
|
|
|
trackFinalizedState(m.eth1Chain, finalizedEth1Data, finalizedStateDepositIndex)
|
2020-11-24 21:21:47 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
template getBlockProposalData*(m: ELManager,
|
2021-06-11 17:51:46 +00:00
|
|
|
state: ForkedHashedBeaconState,
|
2020-12-03 04:30:35 +00:00
|
|
|
finalizedEth1Data: Eth1Data,
|
2022-03-31 14:43:05 +00:00
|
|
|
finalizedStateDepositIndex: uint64):
|
|
|
|
BlockProposalEth1Data =
|
|
|
|
getBlockProposalData(
|
2023-03-05 01:40:21 +00:00
|
|
|
m.eth1Chain, state, finalizedEth1Data, finalizedStateDepositIndex)
|
2022-03-31 14:43:05 +00:00
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
func new*(T: type ELConnection,
|
2023-03-05 01:40:21 +00:00
|
|
|
engineUrl: EngineApiUrl): T =
|
|
|
|
ELConnection(
|
|
|
|
engineUrl: engineUrl,
|
|
|
|
depositContractSyncStatus: DepositContractSyncStatus.unknown)
|
2020-11-17 19:50:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc new*(T: type ELManager,
|
|
|
|
cfg: RuntimeConfig,
|
|
|
|
depositContractBlockNumber: uint64,
|
|
|
|
depositContractBlockHash: Eth2Digest,
|
|
|
|
db: BeaconChainDB,
|
|
|
|
engineApiUrls: seq[EngineApiUrl],
|
|
|
|
eth1Network: Option[Eth1Network]): T =
|
|
|
|
let
|
|
|
|
eth1Chain = Eth1Chain.init(
|
|
|
|
cfg, db, depositContractBlockNumber, depositContractBlockHash)
|
2022-06-15 02:38:27 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
debug "Initializing ELManager",
|
2023-02-23 02:10:07 +00:00
|
|
|
depositContractBlockNumber,
|
|
|
|
depositContractBlockHash
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
T(eth1Chain: eth1Chain,
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
depositContractAddress: cfg.DEPOSIT_CONTRACT_ADDRESS,
|
2023-03-05 01:40:21 +00:00
|
|
|
depositContractBlockNumber: depositContractBlockNumber,
|
|
|
|
depositContractBlockHash: depositContractBlockHash.asBlockHash,
|
|
|
|
elConnections: mapIt(engineApiUrls, ELConnection.new(it)),
|
2020-12-15 21:59:29 +00:00
|
|
|
eth1Network: eth1Network,
|
2023-01-18 14:01:49 +00:00
|
|
|
blocksPerLogsRequest: targetBlocksPerLogsRequest)
|
2019-09-09 15:59:02 +00:00
|
|
|
|
2020-09-28 15:19:57 +00:00
|
|
|
proc safeCancel(fut: var Future[void]) =
|
|
|
|
if not fut.isNil and not fut.finished:
|
2023-09-22 11:06:27 +00:00
|
|
|
fut.cancelSoon()
|
2020-11-03 01:21:07 +00:00
|
|
|
fut = nil
|
2020-09-28 15:19:57 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc doStop(m: ELManager) {.async.} =
|
|
|
|
safeCancel m.chainSyncingLoopFut
|
|
|
|
safeCancel m.exchangeTransitionConfigurationLoopFut
|
2022-06-17 06:32:52 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.elConnections.len > 0:
|
|
|
|
let closeConnectionFutures = mapIt(m.elConnections, close(it))
|
|
|
|
await allFutures(closeConnectionFutures)
|
2022-01-31 17:28:26 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc stop(m: ELManager) {.async.} =
|
|
|
|
if not m.stopFut.isNil:
|
2020-12-15 21:59:29 +00:00
|
|
|
await m.stopFut
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
|
|
|
m.stopFut = m.doStop()
|
2020-12-15 21:59:29 +00:00
|
|
|
await m.stopFut
|
2023-03-05 01:40:21 +00:00
|
|
|
m.stopFut = nil
|
2020-12-15 21:59:29 +00:00
|
|
|
|
2020-11-19 17:19:03 +00:00
|
|
|
const
|
2020-11-24 21:21:47 +00:00
|
|
|
votedBlocksSafetyMargin = 50
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2023-03-10 17:40:27 +00:00
|
|
|
func earliestBlockOfInterest(
|
|
|
|
m: ELManager,
|
|
|
|
latestEth1BlockNumber: Eth1BlockNumber): Eth1BlockNumber =
|
|
|
|
let blocksOfInterestRange =
|
|
|
|
SLOTS_PER_ETH1_VOTING_PERIOD +
|
|
|
|
(2 * m.cfg.ETH1_FOLLOW_DISTANCE) +
|
|
|
|
votedBlocksSafetyMargin
|
|
|
|
|
|
|
|
if latestEth1BlockNumber > blocksOfInterestRange:
|
|
|
|
latestEth1BlockNumber - blocksOfInterestRange
|
|
|
|
else:
|
|
|
|
0
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc syncBlockRange(m: ELManager,
|
2023-03-09 17:29:04 +00:00
|
|
|
connection: ELConnection,
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient: RpcClient,
|
|
|
|
depositContract: Sender[DepositContract],
|
2020-11-19 17:19:03 +00:00
|
|
|
fromBlock, toBlock,
|
2020-12-15 21:59:29 +00:00
|
|
|
fullSyncFromBlock: Eth1BlockNumber) {.gcsafe, async.} =
|
2023-03-05 01:40:21 +00:00
|
|
|
doAssert m.eth1Chain.blocks.len > 0
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
var currentBlock = fromBlock
|
|
|
|
while currentBlock <= toBlock:
|
2020-11-14 20:51:50 +00:00
|
|
|
var
|
2024-01-13 01:36:17 +00:00
|
|
|
depositLogs: seq[JsonString]
|
2020-11-14 20:51:50 +00:00
|
|
|
maxBlockNumberRequested: Eth1BlockNumber
|
2020-12-01 23:35:07 +00:00
|
|
|
backoff = 100
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
while true:
|
2022-08-12 13:51:33 +00:00
|
|
|
maxBlockNumberRequested =
|
|
|
|
min(toBlock, currentBlock + m.blocksPerLogsRequest - 1)
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
debug "Obtaining deposit log events",
|
|
|
|
fromBlock = currentBlock,
|
2020-12-01 23:35:07 +00:00
|
|
|
toBlock = maxBlockNumberRequested,
|
|
|
|
backoff
|
2020-02-07 07:13:38 +00:00
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
debug.logTime "Deposit logs obtained":
|
2020-12-01 11:14:32 +00:00
|
|
|
# Reduce all request rate until we have a more general solution
|
|
|
|
# for dealing with Infura's rate limits
|
2020-12-01 23:35:07 +00:00
|
|
|
await sleepAsync(milliseconds(backoff))
|
2023-03-09 17:29:04 +00:00
|
|
|
let
|
|
|
|
startTime = Moment.now
|
|
|
|
deadline = sleepAsync 30.seconds
|
|
|
|
jsonLogsFut = depositContract.getJsonLogs(
|
|
|
|
DepositEvent,
|
|
|
|
fromBlock = some blockId(currentBlock),
|
|
|
|
toBlock = some blockId(maxBlockNumberRequested))
|
|
|
|
|
|
|
|
connection.trackEngineApiRequest(
|
|
|
|
jsonLogsFut, "getLogs", startTime, deadline)
|
2020-11-24 21:21:47 +00:00
|
|
|
|
|
|
|
depositLogs = try:
|
2022-08-12 13:51:33 +00:00
|
|
|
# Downloading large amounts of deposits may take several minutes
|
2023-03-09 17:29:04 +00:00
|
|
|
awaitWithTimeout(jsonLogsFut, deadline):
|
2022-08-12 13:51:33 +00:00
|
|
|
raise newException(DataProviderTimeout,
|
2020-11-24 21:21:47 +00:00
|
|
|
"Request time out while obtaining json logs")
|
|
|
|
except CatchableError as err:
|
2020-12-01 23:35:07 +00:00
|
|
|
debug "Request for deposit logs failed", err = err.msg
|
2020-12-09 22:44:59 +00:00
|
|
|
inc failed_web3_requests
|
2020-12-01 23:35:07 +00:00
|
|
|
backoff = (backoff * 3) div 2
|
2022-08-12 13:51:33 +00:00
|
|
|
m.blocksPerLogsRequest = m.blocksPerLogsRequest div 2
|
|
|
|
if m.blocksPerLogsRequest == 0:
|
|
|
|
m.blocksPerLogsRequest = 1
|
|
|
|
raise err
|
|
|
|
continue
|
|
|
|
m.blocksPerLogsRequest = min(
|
|
|
|
(m.blocksPerLogsRequest * 3 + 1) div 2,
|
|
|
|
targetBlocksPerLogsRequest)
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
currentBlock = maxBlockNumberRequested + 1
|
|
|
|
break
|
|
|
|
|
2020-12-01 11:14:32 +00:00
|
|
|
let blocksWithDeposits = depositEventsToBlocks(depositLogs)
|
2020-11-24 21:21:47 +00:00
|
|
|
|
|
|
|
for i in 0 ..< blocksWithDeposits.len:
|
|
|
|
let blk = blocksWithDeposits[i]
|
2020-11-19 17:19:03 +00:00
|
|
|
if blk.number > fullSyncFromBlock:
|
2023-03-14 11:46:22 +00:00
|
|
|
await fetchTimestamp(connection, rpcClient, blk)
|
2023-03-05 01:40:21 +00:00
|
|
|
let lastBlock = m.eth1Chain.blocks.peekLast
|
2020-11-19 17:19:03 +00:00
|
|
|
for n in max(lastBlock.number + 1, fullSyncFromBlock) ..< blk.number:
|
2020-12-01 23:35:07 +00:00
|
|
|
debug "Obtaining block without deposits", blockNum = n
|
2024-01-08 16:53:29 +00:00
|
|
|
let noDepositsBlock = raiseIfNil connection.trackedRequestWithTimeout(
|
2023-03-09 17:29:04 +00:00
|
|
|
"getBlockByNumber",
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient.getBlockByNumber(n),
|
|
|
|
web3RequestsTimeout)
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
m.eth1Chain.addBlock(
|
2024-01-08 16:53:29 +00:00
|
|
|
lastBlock.makeSuccessorWithoutDeposits(noDepositsBlock))
|
|
|
|
eth1_synced_head.set noDepositsBlock.number.toGaugeValue
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
m.eth1Chain.addBlock blk
|
2020-12-09 22:44:59 +00:00
|
|
|
eth1_synced_head.set blk.number.toGaugeValue
|
2020-11-24 21:21:47 +00:00
|
|
|
|
|
|
|
if blocksWithDeposits.len > 0:
|
|
|
|
let lastIdx = blocksWithDeposits.len - 1
|
|
|
|
template lastBlock: auto = blocksWithDeposits[lastIdx]
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
let status = when hasDepositRootChecks:
|
2024-02-06 22:03:52 +00:00
|
|
|
await fetchDepositContractData(
|
|
|
|
connection, rpcClient, depositContract, lastBlock)
|
2020-11-24 21:21:47 +00:00
|
|
|
else:
|
|
|
|
DepositRootUnavailable
|
2020-11-03 01:21:07 +00:00
|
|
|
|
|
|
|
when hasDepositRootChecks:
|
2020-11-24 21:21:47 +00:00
|
|
|
debug "Deposit contract state verified",
|
|
|
|
status = $status,
|
2022-08-10 12:31:10 +00:00
|
|
|
ourCount = lastBlock.depositCount,
|
|
|
|
ourRoot = lastBlock.depositRoot
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2020-11-24 21:21:47 +00:00
|
|
|
case status
|
|
|
|
of DepositRootIncorrect, DepositCountIncorrect:
|
|
|
|
raise newException(CorruptDataProvider,
|
|
|
|
"The deposit log events disagree with the deposit contract state")
|
|
|
|
else:
|
|
|
|
discard
|
2020-11-03 01:21:07 +00:00
|
|
|
|
2022-01-03 21:18:49 +00:00
|
|
|
info "Eth1 sync progress",
|
2020-11-24 21:21:47 +00:00
|
|
|
blockNumber = lastBlock.number,
|
2022-08-10 12:31:10 +00:00
|
|
|
depositsProcessed = lastBlock.depositCount
|
2020-11-17 19:50:07 +00:00
|
|
|
|
2023-04-19 19:42:30 +00:00
|
|
|
func hasConnection*(m: ELManager): bool =
|
|
|
|
m.elConnections.len > 0
|
|
|
|
|
2023-04-27 08:47:38 +00:00
|
|
|
func hasAnyWorkingConnection*(m: ELManager): bool =
|
2023-05-25 07:39:47 +00:00
|
|
|
m.elConnections.anyIt(it.state == Working or it.state == NeverTested)
|
2023-04-27 08:47:38 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
func hasProperlyConfiguredConnection*(m: ELManager): bool =
|
|
|
|
for connection in m.elConnections:
|
|
|
|
if connection.etcStatus == EtcStatus.match:
|
|
|
|
return true
|
2022-01-31 17:28:26 +00:00
|
|
|
|
2023-04-19 19:42:30 +00:00
|
|
|
false
|
2022-02-15 19:10:04 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc startExchangeTransitionConfigurationLoop(m: ELManager) {.async.} =
|
|
|
|
debug "Starting exchange transition configuration loop"
|
2020-12-15 21:59:29 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
while true:
|
2023-04-17 20:11:28 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/paris.md#specification-3
|
2023-03-05 01:40:21 +00:00
|
|
|
debug "Exchange transition configuration tick"
|
|
|
|
traceAsyncErrors m.exchangeTransitionConfiguration()
|
2023-03-09 17:29:04 +00:00
|
|
|
await sleepAsync(60.seconds)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
proc syncEth1Chain(m: ELManager, connection: ELConnection) {.async.} =
|
2023-03-09 23:41:28 +00:00
|
|
|
let rpcClient = awaitOrRaiseOnTimeout(connection.connectedRpcClient(),
|
|
|
|
1.seconds)
|
2023-03-05 01:40:21 +00:00
|
|
|
let
|
2023-03-14 16:05:21 +00:00
|
|
|
# BEWARE
|
|
|
|
# `connectedRpcClient` guarantees that connection.web3 will not be
|
|
|
|
# `none` here, but it's not safe to initialize this later (e.g closer
|
|
|
|
# to where it's used) because `connection.web3` may be set to `none`
|
|
|
|
# at any time after a failed request. Luckily, the `contractSender`
|
|
|
|
# object is very cheap to create.
|
|
|
|
depositContract = connection.web3.get.contractSender(
|
|
|
|
DepositContract, m.depositContractAddress)
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
shouldProcessDeposits = not (
|
|
|
|
m.depositContractAddress.isZeroMemory or
|
|
|
|
m.eth1Chain.finalizedBlockHash.data.isZeroMemory)
|
|
|
|
|
|
|
|
trace "Starting syncEth1Chain", shouldProcessDeposits
|
2022-01-31 17:28:26 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
logScope:
|
|
|
|
url = connection.engineUrl.url
|
2022-01-31 17:28:26 +00:00
|
|
|
|
|
|
|
# We might need to reset the chain if the new provider disagrees
|
|
|
|
# with the previous one regarding the history of the chain or if
|
|
|
|
# we have detected a conensus violation - our view disagreeing with
|
|
|
|
# the majority of the validators in the network.
|
|
|
|
#
|
|
|
|
# Consensus violations happen in practice because the web3 providers
|
|
|
|
# sometimes return incomplete or incorrect deposit log events even
|
|
|
|
# when they don't indicate any errors in the response. When this
|
|
|
|
# happens, we are usually able to download the data successfully
|
|
|
|
# on the second attempt.
|
2023-03-05 01:40:21 +00:00
|
|
|
#
|
|
|
|
# TODO
|
|
|
|
# Perhaps the above problem was manifesting only with the obsolete
|
|
|
|
# JSON-RPC data providers, which can no longer be used with Nimbus.
|
|
|
|
if m.eth1Chain.blocks.len > 0:
|
|
|
|
let needsReset = m.eth1Chain.hasConsensusViolation or (block:
|
2022-01-31 17:28:26 +00:00
|
|
|
let
|
2023-03-05 01:40:21 +00:00
|
|
|
lastKnownBlock = m.eth1Chain.blocks.peekLast
|
2024-01-08 16:53:29 +00:00
|
|
|
matchingBlockAtNewEl = raiseIfNil connection.trackedRequestWithTimeout(
|
2023-03-09 17:29:04 +00:00
|
|
|
"getBlockByNumber",
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient.getBlockByNumber(lastKnownBlock.number),
|
|
|
|
web3RequestsTimeout)
|
2022-01-31 17:28:26 +00:00
|
|
|
|
2024-01-08 16:53:29 +00:00
|
|
|
lastKnownBlock.hash.asBlockHash != matchingBlockAtNewEl.hash)
|
2022-01-31 17:28:26 +00:00
|
|
|
|
|
|
|
if needsReset:
|
2023-03-05 01:40:21 +00:00
|
|
|
trace "Resetting the Eth1 chain",
|
|
|
|
hasConsensusViolation = m.eth1Chain.hasConsensusViolation
|
|
|
|
m.eth1Chain.clear()
|
2022-12-07 10:24:51 +00:00
|
|
|
|
2022-06-15 02:38:27 +00:00
|
|
|
var eth1SyncedTo: Eth1BlockNumber
|
2022-08-12 13:48:33 +00:00
|
|
|
if shouldProcessDeposits:
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.eth1Chain.blocks.len == 0:
|
|
|
|
let finalizedBlockHash = m.eth1Chain.finalizedBlockHash.asBlockHash
|
2024-01-08 16:53:29 +00:00
|
|
|
let startBlock = raiseIfNil connection.trackedRequestWithTimeout(
|
|
|
|
"getBlockByHash",
|
|
|
|
rpcClient.getBlockByHash(finalizedBlockHash),
|
|
|
|
web3RequestsTimeout)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
m.eth1Chain.addBlock Eth1Block(
|
|
|
|
hash: m.eth1Chain.finalizedBlockHash,
|
2022-08-12 13:48:33 +00:00
|
|
|
number: Eth1BlockNumber startBlock.number,
|
|
|
|
timestamp: Eth1BlockTimestamp startBlock.timestamp)
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
eth1SyncedTo = m.eth1Chain.blocks[^1].number
|
2022-06-15 02:38:27 +00:00
|
|
|
|
|
|
|
eth1_synced_head.set eth1SyncedTo.toGaugeValue
|
|
|
|
eth1_finalized_head.set eth1SyncedTo.toGaugeValue
|
|
|
|
eth1_finalized_deposits.set(
|
2023-03-05 01:40:21 +00:00
|
|
|
m.eth1Chain.finalizedDepositsMerkleizer.getChunkCount.toGaugeValue)
|
2020-12-09 22:44:59 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
debug "Starting Eth1 syncing", `from` = shortLog(m.eth1Chain.blocks[^1])
|
2020-11-30 23:59:35 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
while true:
|
2023-03-05 01:40:21 +00:00
|
|
|
debug "syncEth1Chain tick"
|
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
if bnStatus == BeaconNodeStatus.Stopping:
|
2020-12-15 21:59:29 +00:00
|
|
|
await m.stop()
|
2020-11-03 01:21:07 +00:00
|
|
|
return
|
2020-10-14 14:04:08 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.eth1Chain.hasConsensusViolation:
|
2021-01-29 21:21:44 +00:00
|
|
|
raise newException(CorruptDataProvider, "Eth1 chain contradicts Eth2 consensus")
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
let latestBlock = try:
|
2024-01-08 16:53:29 +00:00
|
|
|
raiseIfNil connection.trackedRequestWithTimeout(
|
2023-03-09 17:29:04 +00:00
|
|
|
"getBlockByNumber",
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient.eth_getBlockByNumber(blockId("latest"), false),
|
|
|
|
web3RequestsTimeout)
|
|
|
|
except CatchableError as err:
|
2023-03-10 09:12:29 +00:00
|
|
|
warn "Failed to obtain the latest block from the EL", err = err.msg
|
2023-03-05 01:40:21 +00:00
|
|
|
raise err
|
2021-11-25 16:51:51 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
m.syncTargetBlock = some(
|
|
|
|
if Eth1BlockNumber(latestBlock.number) > m.cfg.ETH1_FOLLOW_DISTANCE:
|
|
|
|
Eth1BlockNumber(latestBlock.number) - m.cfg.ETH1_FOLLOW_DISTANCE
|
2022-08-12 19:44:55 +00:00
|
|
|
else:
|
2023-03-05 01:40:21 +00:00
|
|
|
Eth1BlockNumber(0))
|
|
|
|
if m.syncTargetBlock.get <= eth1SyncedTo:
|
|
|
|
# The chain reorged to a lower height.
|
|
|
|
# It's relatively safe to ignore that.
|
|
|
|
await sleepAsync(m.cfg.SECONDS_PER_ETH1_BLOCK.int.seconds)
|
|
|
|
continue
|
2021-11-25 16:51:51 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
eth1_latest_head.set latestBlock.number.toGaugeValue
|
2021-02-04 15:01:47 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
if shouldProcessDeposits and
|
|
|
|
latestBlock.number.uint64 > m.cfg.ETH1_FOLLOW_DISTANCE:
|
2023-03-09 17:29:04 +00:00
|
|
|
await m.syncBlockRange(connection,
|
|
|
|
rpcClient,
|
2023-03-05 01:40:21 +00:00
|
|
|
depositContract,
|
|
|
|
eth1SyncedTo + 1,
|
|
|
|
m.syncTargetBlock.get,
|
|
|
|
m.earliestBlockOfInterest(Eth1BlockNumber latestBlock.number))
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
eth1SyncedTo = m.syncTargetBlock.get
|
|
|
|
eth1_synced_head.set eth1SyncedTo.toGaugeValue
|
2022-06-15 02:38:27 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc startChainSyncingLoop(m: ELManager) {.async.} =
|
2023-07-11 06:01:56 +00:00
|
|
|
info "Starting execution layer deposit syncing",
|
2023-03-05 01:40:21 +00:00
|
|
|
contract = $m.depositContractAddress
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2023-03-10 10:55:55 +00:00
|
|
|
var syncedConnectionFut = m.selectConnectionForChainSyncing()
|
|
|
|
info "Connection attempt started"
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
while true:
|
2023-03-10 10:55:55 +00:00
|
|
|
try:
|
|
|
|
await syncedConnectionFut or sleepAsync(60.seconds)
|
|
|
|
if not syncedConnectionFut.finished:
|
2023-07-11 06:01:56 +00:00
|
|
|
notice "No synced execution layer available for deposit syncing"
|
2023-03-05 01:40:21 +00:00
|
|
|
await sleepAsync(chronos.seconds(30))
|
2022-06-15 02:38:27 +00:00
|
|
|
continue
|
|
|
|
|
2023-03-10 10:55:55 +00:00
|
|
|
await syncEth1Chain(m, syncedConnectionFut.read)
|
2023-11-01 04:53:09 +00:00
|
|
|
except CatchableError:
|
2023-03-10 10:55:55 +00:00
|
|
|
await sleepAsync(10.seconds)
|
|
|
|
|
2023-03-09 23:41:28 +00:00
|
|
|
# A more detailed error is already logged by trackEngineApiRequest
|
|
|
|
debug "Restarting the deposit syncing loop"
|
2023-03-10 10:55:55 +00:00
|
|
|
|
|
|
|
# To be extra safe, we will make a fresh connection attempt
|
|
|
|
await syncedConnectionFut.cancelAndWait()
|
|
|
|
syncedConnectionFut = m.selectConnectionForChainSyncing()
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2023-08-18 09:30:02 +00:00
|
|
|
proc start*(m: ELManager, syncChain = true) {.gcsafe.} =
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.elConnections.len == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
## Calling `ELManager.start()` on an already started ELManager is a noop
|
2023-08-18 09:30:02 +00:00
|
|
|
if syncChain and m.chainSyncingLoopFut.isNil:
|
2023-03-05 01:40:21 +00:00
|
|
|
m.chainSyncingLoopFut =
|
|
|
|
m.startChainSyncingLoop()
|
|
|
|
|
|
|
|
if m.hasJwtSecret and m.exchangeTransitionConfigurationLoopFut.isNil:
|
|
|
|
m.exchangeTransitionConfigurationLoopFut =
|
|
|
|
m.startExchangeTransitionConfigurationLoop()
|
2019-11-22 13:16:07 +00:00
|
|
|
|
2022-07-25 19:23:02 +00:00
|
|
|
func `$`(x: Quantity): string =
|
|
|
|
$(x.uint64)
|
|
|
|
|
|
|
|
func `$`(x: BlockObject): string =
|
|
|
|
$(x.number) & " [" & $(x.hash) & "]"
|
|
|
|
|
2020-12-04 16:28:42 +00:00
|
|
|
proc testWeb3Provider*(web3Url: Uri,
|
2022-03-31 14:43:05 +00:00
|
|
|
depositContractAddress: Eth1Address,
|
2023-11-27 14:48:29 +00:00
|
|
|
jwtSecret: Opt[seq[byte]]) {.async.} =
|
2022-07-25 19:23:02 +00:00
|
|
|
stdout.write "Establishing web3 connection..."
|
|
|
|
var web3: Web3
|
|
|
|
try:
|
|
|
|
web3 = awaitOrRaiseOnTimeout(
|
2023-03-09 23:41:28 +00:00
|
|
|
newWeb3($web3Url, getJsonRpcRequestHeaders(jwtSecret)),
|
|
|
|
5.seconds)
|
2022-07-25 19:23:02 +00:00
|
|
|
stdout.write "\rEstablishing web3 connection: Connected\n"
|
|
|
|
except CatchableError as err:
|
|
|
|
stdout.write "\rEstablishing web3 connection: Failure(" & err.msg & ")\n"
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
template request(actionDesc: static string,
|
|
|
|
action: untyped): untyped =
|
|
|
|
stdout.write actionDesc & "..."
|
|
|
|
stdout.flushFile()
|
|
|
|
var res: typeof(read action)
|
|
|
|
try:
|
2023-03-05 01:40:21 +00:00
|
|
|
res = awaitOrRaiseOnTimeout(action, web3RequestsTimeout)
|
2024-01-08 16:53:29 +00:00
|
|
|
when res is BlockObject:
|
|
|
|
res = raiseIfNil res
|
2022-07-25 19:23:02 +00:00
|
|
|
stdout.write "\r" & actionDesc & ": " & $res
|
2020-12-04 16:28:42 +00:00
|
|
|
except CatchableError as err:
|
2022-07-25 19:23:02 +00:00
|
|
|
stdout.write "\r" & actionDesc & ": Error(" & err.msg & ")"
|
|
|
|
stdout.write "\n"
|
|
|
|
res
|
2020-12-04 16:28:42 +00:00
|
|
|
|
2023-11-01 04:53:09 +00:00
|
|
|
discard request "Chain ID":
|
|
|
|
web3.provider.eth_chainId()
|
2022-06-27 15:02:12 +00:00
|
|
|
|
2023-11-01 04:53:09 +00:00
|
|
|
discard request "Sync status":
|
|
|
|
web3.provider.eth_syncing()
|
|
|
|
|
|
|
|
let
|
2022-07-25 19:23:02 +00:00
|
|
|
latestBlock = request "Latest block":
|
|
|
|
web3.provider.eth_getBlockByNumber(blockId("latest"), false)
|
|
|
|
|
|
|
|
ns = web3.contractSender(DepositContract, depositContractAddress)
|
|
|
|
|
2023-11-01 04:53:09 +00:00
|
|
|
discard request "Deposit root":
|
|
|
|
ns.get_deposit_root.call(blockNumber = latestBlock.number.uint64)
|