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-05-22 19:57:05 +00:00
|
|
|
std/[options, strformat, typetraits, json, sequtils],
|
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],
|
2024-04-04 04:33:08 +00:00
|
|
|
eth/common/eth_types,
|
2024-05-14 18:03:30 +00:00
|
|
|
results,
|
2024-09-02 11:47:00 +00:00
|
|
|
kzg4844/[kzg_abi, kzg],
|
2024-01-19 03:20:47 +00:00
|
|
|
stew/[assign2, byteutils, objects],
|
2020-12-09 22:44:59 +00:00
|
|
|
# Local modules:
|
2024-04-04 04:33:08 +00:00
|
|
|
../spec/[eth2_merkleization, forks],
|
2021-03-05 13:12:00 +00:00
|
|
|
../networking/network_metadata,
|
2024-05-14 18:03:30 +00:00
|
|
|
".."/beacon_node_status,
|
2024-08-22 14:12:03 +00:00
|
|
|
"."/[el_conf, engine_api_conversions, eth1_chain]
|
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
|
2024-04-04 04:33:08 +00:00
|
|
|
from ../spec/helpers import bytes_to_uint64
|
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
|
|
|
|
2024-10-23 00:42:46 +00:00
|
|
|
const
|
|
|
|
SleepDurations =
|
|
|
|
[100.milliseconds, 200.milliseconds, 500.milliseconds, 1.seconds]
|
|
|
|
|
2021-10-14 12:33:55 +00:00
|
|
|
type
|
2024-09-29 11:32:55 +00:00
|
|
|
FixedBytes[N: static int] = web3.FixedBytes[N]
|
2021-10-14 12:33:55 +00:00
|
|
|
PubKeyBytes = DynamicBytes[48, 48]
|
|
|
|
WithdrawalCredentialsBytes = DynamicBytes[32, 32]
|
|
|
|
SignatureBytes = DynamicBytes[96, 96]
|
|
|
|
Int64LeBytes = DynamicBytes[8, 8]
|
2024-05-14 18:03:30 +00:00
|
|
|
WithoutTimeout* = distinct int
|
2021-10-14 12:33:55 +00:00
|
|
|
|
2024-10-23 00:42:46 +00:00
|
|
|
DeadlineObject* = object
|
|
|
|
# TODO (cheatfate): This object declaration could be removed when
|
|
|
|
# `Raising()` macro starts to support procedure arguments.
|
|
|
|
future*: Future[void].Raising([CancelledError])
|
2021-10-14 12:33:55 +00:00
|
|
|
|
2024-08-22 14:12:03 +00:00
|
|
|
SomeEnginePayloadWithValue =
|
|
|
|
BellatrixExecutionPayloadWithValue |
|
|
|
|
GetPayloadV2Response |
|
|
|
|
GetPayloadV3Response |
|
|
|
|
GetPayloadV4Response
|
|
|
|
|
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
|
2024-05-14 18:03:30 +00:00
|
|
|
noTimeout = WithoutTimeout(0)
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
ELManagerState* {.pure.} = enum
|
|
|
|
Running, Closing, Closed
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
ELManager* = ref object
|
2024-06-12 01:46:39 +00:00
|
|
|
eth1Network: Opt[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
|
2024-11-04 11:19:43 +00:00
|
|
|
depositContractBlockHash: Hash32
|
2023-03-05 01:40:21 +00:00
|
|
|
|
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.
|
|
|
|
|
2024-06-12 01:46:39 +00:00
|
|
|
syncTargetBlock: Opt[Eth1BlockNumber]
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
chainSyncingLoopFut: Future[void]
|
|
|
|
exchangeTransitionConfigurationLoopFut: Future[void]
|
2024-05-14 18:03:30 +00:00
|
|
|
managerState: ELManagerState
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
ELConnectionState {.pure.} = enum
|
2023-03-05 01:40:21 +00:00
|
|
|
NeverTested
|
|
|
|
Working
|
|
|
|
Degraded
|
|
|
|
|
|
|
|
ELConnection* = ref object
|
|
|
|
engineUrl: EngineApiUrl
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
web3: Opt[Web3]
|
2023-03-05 01:40:21 +00:00
|
|
|
## 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.
|
|
|
|
|
2024-02-17 09:15:02 +00:00
|
|
|
connectingFut: Future[Result[Web3, string]].Raising([CancelledError])
|
2023-03-05 01:40:21 +00:00
|
|
|
## This future will be replaced when the connection is lost.
|
|
|
|
|
|
|
|
etcStatus: EtcStatus
|
|
|
|
## The latest status of the `exchangeTransitionConfiguration`
|
|
|
|
## exchange.
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
state: ELConnectionState
|
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
|
|
|
|
2024-06-12 01:46:39 +00:00
|
|
|
lastPayloadId: Opt[PayloadID]
|
2020-03-24 11:13:07 +00:00
|
|
|
|
2021-11-25 16:51:51 +00:00
|
|
|
FullBlockId* = object
|
|
|
|
number: Eth1BlockNumber
|
2024-11-04 11:19:43 +00:00
|
|
|
hash: Hash32
|
2021-11-25 16:51:51 +00:00
|
|
|
|
2021-05-28 12:42:39 +00:00
|
|
|
DataProviderFailure* = object of CatchableError
|
|
|
|
CorruptDataProvider* = object of DataProviderFailure
|
|
|
|
DataProviderTimeout* = object of DataProviderFailure
|
2024-05-14 18:03:30 +00:00
|
|
|
DataProviderConnectionFailure* = 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
|
|
|
|
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"]
|
|
|
|
|
2024-10-23 00:42:46 +00:00
|
|
|
proc init*(t: typedesc[DeadlineObject], d: Duration): DeadlineObject =
|
|
|
|
DeadlineObject(future: sleepAsync(d))
|
|
|
|
|
|
|
|
proc variedSleep*(
|
|
|
|
counter: var int,
|
|
|
|
durations: openArray[Duration]
|
|
|
|
): Future[void] {.async: (raises: [CancelledError], raw: true).} =
|
|
|
|
doAssert(len(durations) > 0, "Empty durations array!")
|
|
|
|
let index =
|
|
|
|
if (counter < 0) or (counter > high(durations)):
|
|
|
|
high(durations)
|
|
|
|
else:
|
|
|
|
counter
|
|
|
|
inc(counter)
|
|
|
|
sleepAsync(durations[index])
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc close(connection: ELConnection): Future[void] {.async: (raises: []).} =
|
2023-05-15 20:40:47 +00:00
|
|
|
if connection.web3.isSome:
|
2024-05-14 18:03:30 +00:00
|
|
|
try:
|
|
|
|
let web3 = connection.web3.get
|
|
|
|
await noCancel web3.close().wait(30.seconds)
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
debug "Failed to close execution layer data provider in time",
|
|
|
|
timeout = 30.seconds
|
|
|
|
except CatchableError as exc:
|
|
|
|
# TODO (cheatfate): This handler should be removed when `nim-web3` will
|
|
|
|
# adopt `asyncraises`.
|
|
|
|
debug "Failed to close execution layer", error = $exc.name,
|
|
|
|
reason = $exc.msg
|
2023-05-15 20:40:47 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc setDegradedState(
|
|
|
|
connection: ELConnection,
|
|
|
|
requestName: string,
|
|
|
|
statusCode: int,
|
|
|
|
errMsg: string
|
|
|
|
): Future[void] {.async: (raises: []).} =
|
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
|
2024-05-14 18:03:30 +00:00
|
|
|
of ELConnectionState.NeverTested, ELConnectionState.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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
await connection.close()
|
|
|
|
connection.web3 = Opt.none(Web3)
|
|
|
|
of ELConnectionState.Degraded:
|
2023-05-15 20:40:47 +00:00
|
|
|
connection.decreaseCounterTowardsStateChange()
|
2023-03-09 17:29:04 +00:00
|
|
|
|
|
|
|
proc setWorkingState(connection: ELConnection) =
|
|
|
|
case connection.state
|
2024-05-14 18:03:30 +00:00
|
|
|
of ELConnectionState.NeverTested:
|
2023-05-25 07:39:47 +00:00
|
|
|
connection.hysteresisCounter = 0
|
|
|
|
connection.state = Working
|
2024-05-14 18:03:30 +00:00
|
|
|
of ELConnectionState.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
|
2024-05-14 18:03:30 +00:00
|
|
|
of ELConnectionState.Working:
|
2023-05-15 20:40:47 +00:00
|
|
|
connection.decreaseCounterTowardsStateChange()
|
2023-03-09 17:29:04 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc engineApiRequest[T](
|
|
|
|
connection: ELConnection,
|
|
|
|
request: Future[T],
|
|
|
|
requestName: string,
|
|
|
|
startTime: Moment,
|
|
|
|
deadline: Future[void] | Duration | WithoutTimeout,
|
|
|
|
failureAllowed = false
|
|
|
|
): Future[T] {.async: (raises: [CatchableError]).} =
|
|
|
|
## This procedure raises `CancelledError` and `DataProviderTimeout`
|
|
|
|
## exceptions, and everything which `request` could raise.
|
|
|
|
try:
|
|
|
|
let res =
|
|
|
|
when deadline is WithoutTimeout:
|
|
|
|
await request
|
|
|
|
else:
|
|
|
|
await request.wait(deadline)
|
|
|
|
engine_api_request_duration_seconds.observe(
|
|
|
|
float(milliseconds(Moment.now - startTime)) / 1000.0,
|
2023-03-09 17:29:04 +00:00
|
|
|
[connection.engineUrl.url, requestName])
|
2024-05-14 18:03:30 +00:00
|
|
|
engine_api_responses.inc(
|
|
|
|
1, [connection.engineUrl.url, requestName, "200"])
|
|
|
|
connection.setWorkingState()
|
|
|
|
res
|
2024-05-15 04:44:52 +00:00
|
|
|
except AsyncTimeoutError:
|
2024-05-14 18:03:30 +00:00
|
|
|
engine_api_timeouts.inc(1, [connection.engineUrl.url, requestName])
|
|
|
|
if not(failureAllowed):
|
|
|
|
await connection.setDegradedState(requestName, 0, "Request timed out")
|
|
|
|
raise newException(DataProviderTimeout, "Request timed out")
|
|
|
|
except CancelledError as exc:
|
|
|
|
when deadline is WithoutTimeout:
|
|
|
|
# When `deadline` is set to `noTimeout`, we usually get cancelled on
|
|
|
|
# timeout which was handled by caller.
|
2023-03-05 01:40:21 +00:00
|
|
|
engine_api_timeouts.inc(1, [connection.engineUrl.url, requestName])
|
2024-05-14 18:03:30 +00:00
|
|
|
if not(failureAllowed):
|
|
|
|
await connection.setDegradedState(requestName, 0, "Request timed out")
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
2024-05-14 18:03:30 +00:00
|
|
|
if not(failureAllowed):
|
|
|
|
await connection.setDegradedState(requestName, 0, "Request interrupted")
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
let statusCode =
|
|
|
|
if request.error of ErrorResponse:
|
2023-03-05 01:40:21 +00:00
|
|
|
((ref ErrorResponse) request.error).status
|
|
|
|
else:
|
|
|
|
0
|
2024-05-14 18:03:30 +00:00
|
|
|
engine_api_responses.inc(
|
|
|
|
1, [connection.engineUrl.url, requestName, $statusCode])
|
|
|
|
if not(failureAllowed):
|
|
|
|
await connection.setDegradedState(
|
|
|
|
requestName, statusCode, request.error.msg)
|
|
|
|
raise exc
|
2023-03-09 17:29:04 +00:00
|
|
|
|
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
|
|
|
|
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-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,
|
2024-05-14 18:03:30 +00:00
|
|
|
getJsonRpcRequestHeaders(engineUrl.jwtSecret), httpFlags = {})
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-02-17 09:15:02 +00:00
|
|
|
proc establishEngineApiConnection(url: EngineApiUrl):
|
|
|
|
Future[Result[Web3, string]] {.
|
|
|
|
async: (raises: [CancelledError]).} =
|
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:
|
2024-02-17 09:15:02 +00:00
|
|
|
err exc.msg
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2024-02-17 09:15:02 +00:00
|
|
|
proc tryConnecting(connection: ELConnection): Future[bool] {.
|
2024-05-14 18:03:30 +00:00
|
|
|
async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
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
|
2024-05-14 18:03:30 +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:
|
2024-02-17 09:15:02 +00:00
|
|
|
warn "Engine API connection failed", err = web3Res.error
|
2024-05-14 18:03:30 +00:00
|
|
|
false
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
2024-05-14 18:03:30 +00:00
|
|
|
connection.web3 = Opt.some(web3Res.get)
|
|
|
|
true
|
2020-12-01 21:20:28 +00:00
|
|
|
|
2024-02-17 09:15:02 +00:00
|
|
|
proc connectedRpcClient(connection: ELConnection): Future[RpcClient] {.
|
2024-05-14 18:03:30 +00:00
|
|
|
async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
while not connection.isConnected:
|
2024-05-14 18:03:30 +00:00
|
|
|
if not(await connection.tryConnecting()):
|
2023-03-05 01:40:21 +00:00
|
|
|
await sleepAsync(chronos.seconds(10))
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
connection.web3.get.provider
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc getBlockByHash(
|
|
|
|
rpcClient: RpcClient,
|
2024-11-04 11:19:43 +00:00
|
|
|
hash: Hash32
|
2024-05-16 11:54:51 +00:00
|
|
|
): Future[BlockObject] {.async: (raises: [CatchableError]).} =
|
|
|
|
await rpcClient.eth_getBlockByHash(hash, false)
|
2020-06-27 12:01:19 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc getBlockByNumber*(
|
|
|
|
rpcClient: RpcClient,
|
|
|
|
number: Eth1BlockNumber
|
2024-05-16 11:54:51 +00:00
|
|
|
): Future[BlockObject] {.async: (raises: [CatchableError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
let hexNumber = try:
|
2024-03-19 21:14:08 +00:00
|
|
|
let num = distinctBase(number)
|
|
|
|
&"0x{num:X}" # No leading 0's!
|
2023-03-05 01:40:21 +00:00
|
|
|
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
|
|
|
|
2024-05-16 11:54:51 +00:00
|
|
|
await rpcClient.eth_getBlockByNumber(hexNumber, false)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
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,
|
2024-06-12 01:46:39 +00:00
|
|
|
payloadAttributes: Opt[PayloadAttributesV1] |
|
|
|
|
Opt[PayloadAttributesV2] |
|
|
|
|
Opt[PayloadAttributesV3]):
|
2023-03-06 16:19:15 +00:00
|
|
|
Future[ForkchoiceUpdatedResponse] =
|
2024-06-12 01:46:39 +00:00
|
|
|
when payloadAttributes is Opt[PayloadAttributesV1]:
|
2023-04-17 14:17:52 +00:00
|
|
|
rpcClient.engine_forkchoiceUpdatedV1(state, payloadAttributes)
|
2024-06-12 01:46:39 +00:00
|
|
|
elif payloadAttributes is Opt[PayloadAttributesV2]:
|
2023-04-17 14:17:52 +00:00
|
|
|
rpcClient.engine_forkchoiceUpdatedV2(state, payloadAttributes)
|
2024-06-12 01:46:39 +00:00
|
|
|
elif payloadAttributes is Opt[PayloadAttributesV3]:
|
2023-08-15 23:00:35 +00:00
|
|
|
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
|
|
|
|
|
|
|
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,
|
2024-05-14 18:03:30 +00:00
|
|
|
withdrawals: seq[WithdrawalV1]
|
|
|
|
): Future[GetPayloadResponseType] {.async: (raises: [CatchableError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
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),
|
2024-06-12 01:46:39 +00:00
|
|
|
Opt.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),
|
2024-06-12 01:46:39 +00:00
|
|
|
Opt.some PayloadAttributesV2(
|
2023-03-06 16:19:15 +00:00
|
|
|
timestamp: Quantity timestamp,
|
|
|
|
prevRandao: FixedBytes[32] randomData.data,
|
|
|
|
suggestedFeeRecipient: suggestedFeeRecipient,
|
|
|
|
withdrawals: withdrawals))
|
2024-04-06 13:11:47 +00:00
|
|
|
elif GetPayloadResponseType is engine_api.GetPayloadV3Response or
|
|
|
|
GetPayloadResponseType is engine_api.GetPayloadV4Response:
|
|
|
|
# https://github.com/ethereum/execution-apis/blob/90a46e9137c89d58e818e62fa33a0347bba50085/src/engine/prague.md
|
|
|
|
# does not define any new forkchoiceUpdated, so reuse V3 from Dencun
|
2023-08-15 23:00:35 +00:00
|
|
|
let response = await rpcClient.forkchoiceUpdated(
|
|
|
|
ForkchoiceStateV1(
|
|
|
|
headBlockHash: headBlock.asBlockHash,
|
|
|
|
safeBlockHash: safeBlock.asBlockHash,
|
|
|
|
finalizedBlockHash: finalizedBlock.asBlockHash),
|
2024-06-12 01:46:39 +00:00
|
|
|
Opt.some PayloadAttributesV3(
|
2023-08-15 23:00:35 +00:00
|
|
|
timestamp: Quantity timestamp,
|
|
|
|
prevRandao: FixedBytes[32] randomData.data,
|
|
|
|
suggestedFeeRecipient: suggestedFeeRecipient,
|
|
|
|
withdrawals: withdrawals,
|
2024-10-02 13:26:11 +00:00
|
|
|
parentBeaconBlockRoot: consensusHead.to(Hash32)))
|
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(
|
2024-04-04 04:33:08 +00:00
|
|
|
executionPayload: payload, blockValue: Wei.zero)
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
2024-04-10 09:38:38 +00:00
|
|
|
return await engine_api.getPayload(
|
|
|
|
rpcClient, GetPayloadResponseType, payloadId)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
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
|
|
|
|
2024-04-03 14:43:43 +00:00
|
|
|
template EngineApiResponseType*(T: type electra.ExecutionPayloadForSigning): type =
|
|
|
|
engine_api.GetPayloadV4Response
|
|
|
|
|
2024-11-13 03:29:14 +00:00
|
|
|
template EngineApiResponseType*(T: type fulu.ExecutionPayloadForSigning): type =
|
|
|
|
engine_api.GetPayloadV4Response
|
|
|
|
|
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
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc getPayload*(
|
|
|
|
m: ELManager,
|
|
|
|
PayloadType: type ForkyExecutionPayloadForSigning,
|
|
|
|
consensusHead: Eth2Digest,
|
|
|
|
headBlock, safeBlock, finalizedBlock: Eth2Digest,
|
|
|
|
timestamp: uint64,
|
|
|
|
randomData: Eth2Digest,
|
|
|
|
suggestedFeeRecipient: Eth1Address,
|
|
|
|
withdrawals: seq[capella.Withdrawal]
|
|
|
|
): Future[Opt[PayloadType]] {.async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
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)
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
var bestPayloadIdx = Opt.none(int)
|
2023-03-07 04:43:27 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
while true:
|
|
|
|
let requests =
|
|
|
|
m.elConnections.mapIt(
|
|
|
|
it.getPayloadFromSingleEL(EngineApiResponseType(PayloadType),
|
|
|
|
isFcUpToDate, consensusHead, headBlock, safeBlock, finalizedBlock,
|
|
|
|
timestamp, randomData, suggestedFeeRecipient, engineApiWithdrawals))
|
|
|
|
|
|
|
|
let timeoutExceeded =
|
|
|
|
try:
|
|
|
|
await allFutures(requests).wait(deadline)
|
|
|
|
false
|
2024-05-16 11:54:51 +00:00
|
|
|
except AsyncTimeoutError:
|
2024-05-14 18:03:30 +00:00
|
|
|
true
|
|
|
|
except CancelledError as exc:
|
|
|
|
let pending =
|
|
|
|
requests.filterIt(not(it.finished())).mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
for idx, req in requests:
|
|
|
|
if not(req.finished()):
|
|
|
|
warn "Timeout while getting execution payload",
|
|
|
|
url = m.elConnections[idx].engineUrl.url
|
|
|
|
elif req.failed():
|
|
|
|
warn "Failed to get execution payload from EL",
|
|
|
|
url = m.elConnections[idx].engineUrl.url,
|
|
|
|
reason = req.error.msg
|
2023-03-05 01:40:21 +00:00
|
|
|
else:
|
2024-05-14 18:03:30 +00:00
|
|
|
const payloadFork = PayloadType.kind
|
|
|
|
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.value().executionPayload.withdrawals.isNone:
|
|
|
|
warn "Execution client returned a block without a " &
|
|
|
|
"'withdrawals' field for a post-Shanghai block",
|
|
|
|
url = m.elConnections[idx].engineUrl.url
|
|
|
|
continue
|
|
|
|
|
|
|
|
if engineApiWithdrawals !=
|
|
|
|
req.value().executionPayload.withdrawals.maybeDeref:
|
|
|
|
# otherwise it formats as "@[(index: ..., validatorIndex: ...,
|
|
|
|
# address: ..., amount: ...), (index: ..., validatorIndex: ...,
|
|
|
|
# address: ..., amount: ...)]"
|
|
|
|
# TODO (cheatfate): should we have `continue` statement at the
|
|
|
|
# end of this branch. If no such payload could be choosen as
|
|
|
|
# best one.
|
|
|
|
warn "Execution client did not return correct withdrawals",
|
|
|
|
withdrawals_from_cl_len = engineApiWithdrawals.len,
|
|
|
|
withdrawals_from_el_len =
|
|
|
|
req.value().executionPayload.withdrawals.maybeDeref.len,
|
|
|
|
withdrawals_from_cl =
|
|
|
|
mapIt(engineApiWithdrawals, it.asConsensusWithdrawal),
|
|
|
|
withdrawals_from_el =
|
|
|
|
mapIt(
|
|
|
|
req.value().executionPayload.withdrawals.maybeDeref,
|
|
|
|
it.asConsensusWithdrawal),
|
|
|
|
url = m.elConnections[idx].engineUrl.url
|
|
|
|
# If we have more than one EL connection we consider this as
|
|
|
|
# a failure.
|
|
|
|
if len(requests) > 1:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if req.value().executionPayload.extraData.len > MAX_EXTRA_DATA_BYTES:
|
|
|
|
warn "Execution client provided a block with invalid extraData " &
|
|
|
|
"(size exceeds limit)",
|
|
|
|
url = m.elConnections[idx].engineUrl.url,
|
|
|
|
size = req.value().executionPayload.extraData.len,
|
|
|
|
limit = MAX_EXTRA_DATA_BYTES
|
|
|
|
continue
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
if bestPayloadIdx.isNone:
|
|
|
|
bestPayloadIdx = Opt.some(idx)
|
|
|
|
else:
|
|
|
|
if cmpGetPayloadResponses(
|
|
|
|
req.value(), requests[bestPayloadIdx.get].value()) > 0:
|
|
|
|
bestPayloadIdx = Opt.some(idx)
|
2024-02-17 09:15:02 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let pending =
|
|
|
|
requests.filterIt(not(it.finished())).mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
|
2024-11-13 03:29:14 +00:00
|
|
|
when PayloadType.kind == ConsensusFork.Fulu:
|
|
|
|
if bestPayloadIdx.isSome():
|
|
|
|
return ok(requests[bestPayloadIdx.get()].value().asConsensusTypeFulu)
|
|
|
|
else:
|
|
|
|
if bestPayloadIdx.isSome():
|
|
|
|
return ok(requests[bestPayloadIdx.get()].value().asConsensusType)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
if timeoutExceeded:
|
|
|
|
break
|
|
|
|
|
|
|
|
err()
|
|
|
|
|
|
|
|
proc waitELToSyncDeposits(
|
|
|
|
connection: ELConnection,
|
2024-11-04 11:19:43 +00:00
|
|
|
minimalRequiredBlock: Hash32
|
2024-05-14 18:03:30 +00:00
|
|
|
) {.async: (raises: [CancelledError]).} =
|
|
|
|
var rpcClient: RpcClient = nil
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
if connection.depositContractSyncStatus == DepositContractSyncStatus.synced:
|
|
|
|
return
|
|
|
|
|
|
|
|
var attempt = 0
|
|
|
|
|
|
|
|
while true:
|
2024-05-14 18:03:30 +00:00
|
|
|
if isNil(rpcClient):
|
|
|
|
rpcClient = await connection.connectedRpcClient()
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
try:
|
2024-05-14 18:03:30 +00:00
|
|
|
discard raiseIfNil await connection.engineApiRequest(
|
2023-03-09 17:29:04 +00:00
|
|
|
rpcClient.getBlockByHash(minimalRequiredBlock),
|
2024-05-14 18:03:30 +00:00
|
|
|
"getBlockByHash", Moment.now(),
|
|
|
|
web3RequestsTimeout, failureAllowed = true)
|
2023-03-05 01:40:21 +00:00
|
|
|
connection.depositContractSyncStatus = DepositContractSyncStatus.synced
|
|
|
|
return
|
2024-05-14 18:03:30 +00:00
|
|
|
except CancelledError as exc:
|
|
|
|
trace "waitELToSyncDepositContract interrupted",
|
2023-03-05 01:40:21 +00:00
|
|
|
url = connection.engineUrl.url
|
2024-05-14 18:03:30 +00:00
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
2023-03-05 01:40:21 +00:00
|
|
|
connection.depositContractSyncStatus = DepositContractSyncStatus.notSynced
|
|
|
|
if attempt == 0:
|
2024-05-14 18:03:30 +00:00
|
|
|
warn "Failed to obtain the most recent known block from the " &
|
|
|
|
"execution layer node (the node is probably not synced)",
|
2023-03-05 01:40:21 +00:00
|
|
|
url = connection.engineUrl.url,
|
|
|
|
blk = minimalRequiredBlock,
|
2024-05-14 18:03:30 +00:00
|
|
|
reason = exc.msg
|
2023-03-05 01:40:21 +00:00
|
|
|
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,
|
2024-05-14 18:03:30 +00:00
|
|
|
reason = exc.msg
|
|
|
|
inc(attempt)
|
2023-03-05 01:40:21 +00:00
|
|
|
await sleepAsync(seconds(30))
|
2024-05-14 18:03:30 +00:00
|
|
|
rpcClient = nil
|
2023-03-05 01:40:21 +00:00
|
|
|
|
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
|
|
|
|
|
2024-11-04 11:19:43 +00:00
|
|
|
func mostRecentKnownBlock(m: ELManager): Hash32 =
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.eth1Chain.finalizedDepositsMerkleizer.getChunkCount() > 0:
|
|
|
|
m.eth1Chain.finalizedBlockHash.asBlockHash
|
|
|
|
else:
|
|
|
|
m.depositContractBlockHash
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc selectConnectionForChainSyncing(
|
|
|
|
m: ELManager
|
|
|
|
): Future[ELConnection] {.async: (raises: [CancelledError,
|
|
|
|
DataProviderConnectionFailure]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
doAssert m.elConnections.len > 0
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let pendingConnections = m.elConnections.mapIt(
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.networkHasDepositContract:
|
|
|
|
FutureBase waitELToSyncDeposits(it, m.mostRecentKnownBlock)
|
|
|
|
else:
|
|
|
|
FutureBase connectedRpcClient(it))
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
while true:
|
|
|
|
var pendingFutures = pendingConnections
|
|
|
|
try:
|
|
|
|
discard await race(pendingFutures)
|
2024-05-15 04:44:52 +00:00
|
|
|
except ValueError:
|
2024-05-14 18:03:30 +00:00
|
|
|
raiseAssert "pendingFutures should not be empty at this moment"
|
|
|
|
except CancelledError as exc:
|
|
|
|
let pending = pendingConnections.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
pendingFutures.reset()
|
|
|
|
for index, future in pendingConnections.pairs():
|
|
|
|
if future.completed():
|
|
|
|
let pending = pendingConnections.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
return m.elConnections[index]
|
|
|
|
elif not(future.finished()):
|
|
|
|
pendingFutures.add(future)
|
|
|
|
|
|
|
|
if len(pendingFutures) == 0:
|
|
|
|
raise newException(DataProviderConnectionFailure,
|
|
|
|
"Unable to establish connection for chain syncing")
|
|
|
|
|
|
|
|
proc sendNewPayloadToSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
payload: engine_api.ExecutionPayloadV1
|
|
|
|
): Future[PayloadStatusV1] {.async: (raises: [CatchableError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
2024-05-14 18:03:30 +00:00
|
|
|
await rpcClient.engine_newPayloadV1(payload)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc sendNewPayloadToSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
payload: engine_api.ExecutionPayloadV2
|
|
|
|
): Future[PayloadStatusV1] {.async: (raises: [CatchableError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
2024-05-14 18:03:30 +00:00
|
|
|
await rpcClient.engine_newPayloadV2(payload)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc sendNewPayloadToSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
payload: engine_api.ExecutionPayloadV3,
|
|
|
|
versioned_hashes: seq[engine_api.VersionedHash],
|
|
|
|
parent_beacon_block_root: FixedBytes[32]
|
|
|
|
): Future[PayloadStatusV1] {.async: (raises: [CatchableError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
2024-05-14 18:03:30 +00:00
|
|
|
await rpcClient.engine_newPayloadV3(
|
2024-10-02 13:26:11 +00:00
|
|
|
payload, versioned_hashes, Hash32 parent_beacon_block_root)
|
2022-06-15 02:38:27 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc sendNewPayloadToSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
payload: engine_api.ExecutionPayloadV4,
|
|
|
|
versioned_hashes: seq[engine_api.VersionedHash],
|
2024-10-17 14:53:37 +00:00
|
|
|
parent_beacon_block_root: FixedBytes[32],
|
|
|
|
executionRequests: array[3, seq[byte]]
|
2024-05-14 18:03:30 +00:00
|
|
|
): Future[PayloadStatusV1] {.async: (raises: [CatchableError]).} =
|
2024-04-09 10:44:03 +00:00
|
|
|
let rpcClient = await connection.connectedRpcClient()
|
2024-05-14 18:03:30 +00:00
|
|
|
await rpcClient.engine_newPayloadV4(
|
2024-10-17 14:53:37 +00:00
|
|
|
payload, versioned_hashes, Hash32 parent_beacon_block_root,
|
|
|
|
executionRequests)
|
2024-04-09 10:44:03 +00:00
|
|
|
|
2022-08-22 19:44:40 +00:00
|
|
|
type
|
2023-03-05 01:40:21 +00:00
|
|
|
StatusRelation = enum
|
|
|
|
newStatusIsPreferable
|
|
|
|
oldStatusIsOk
|
|
|
|
disagreement
|
|
|
|
|
2024-05-14 18:03:30 +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
|
2024-05-14 18:03:30 +00:00
|
|
|
selectedResponse: Opt[int]
|
2023-03-05 01:40:21 +00:00
|
|
|
disagreementAlreadyDetected: bool
|
|
|
|
|
2023-03-06 16:19:15 +00:00
|
|
|
func init(T: type ELConsensusViolationDetector): T =
|
2024-05-14 18:03:30 +00:00
|
|
|
ELConsensusViolationDetector(
|
|
|
|
selectedResponse: Opt.none(int),
|
|
|
|
disagreementAlreadyDetected: false
|
|
|
|
)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc processResponse(
|
2023-03-05 01:40:21 +00:00
|
|
|
d: var ELConsensusViolationDetector,
|
2024-05-14 18:03:30 +00:00
|
|
|
elResponseType: typedesc,
|
2023-03-05 01:40:21 +00:00
|
|
|
connections: openArray[ELConnection],
|
2024-05-14 18:03:30 +00:00
|
|
|
requests: auto,
|
2023-03-05 01:40:21 +00:00
|
|
|
idx: int) =
|
|
|
|
|
2023-03-21 21:35:22 +00:00
|
|
|
if not requests[idx].completed:
|
|
|
|
return
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let status = requests[idx].value().status
|
2023-03-05 01:40:21 +00:00
|
|
|
if d.selectedResponse.isNone:
|
2024-05-14 18:03:30 +00:00
|
|
|
d.selectedResponse = Opt.some(idx)
|
2023-03-05 01:40:21 +00:00
|
|
|
elif not d.disagreementAlreadyDetected:
|
2024-05-14 18:03:30 +00:00
|
|
|
let prevStatus = requests[d.selectedResponse.get].value().status
|
2023-03-05 01:40:21 +00:00
|
|
|
case compareStatuses(status, prevStatus)
|
|
|
|
of newStatusIsPreferable:
|
2024-05-14 18:03:30 +00:00
|
|
|
d.selectedResponse = Opt.some(idx)
|
2023-03-05 01:40:21 +00:00
|
|
|
of oldStatusIsOk:
|
|
|
|
discard
|
|
|
|
of disagreement:
|
|
|
|
d.disagreementAlreadyDetected = true
|
|
|
|
error "Execution layer consensus violation detected",
|
2024-05-14 18:03:30 +00:00
|
|
|
responseType = name(elResponseType),
|
2023-03-05 01:40:21 +00:00
|
|
|
url1 = connections[d.selectedResponse.get].engineUrl.url,
|
|
|
|
status1 = prevStatus,
|
|
|
|
url2 = connections[idx].engineUrl.url,
|
|
|
|
status2 = status
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc lazyWait(futures: seq[FutureBase]) {.async: (raises: []).} =
|
|
|
|
block:
|
|
|
|
let pending = futures.filterIt(not(it.finished()))
|
|
|
|
if len(pending) > 0:
|
|
|
|
try:
|
|
|
|
await allFutures(pending).wait(30.seconds)
|
|
|
|
except CancelledError:
|
|
|
|
discard
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
discard
|
|
|
|
|
|
|
|
block:
|
|
|
|
let pending = futures.filterIt(not(it.finished())).mapIt(it.cancelAndWait())
|
|
|
|
if len(pending) > 0:
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
|
|
|
|
proc sendNewPayload*(
|
|
|
|
m: ELManager,
|
2024-10-23 00:42:46 +00:00
|
|
|
blck: SomeForkyBeaconBlock,
|
|
|
|
deadlineObj: DeadlineObject,
|
|
|
|
maxRetriesCount: int
|
2024-05-14 18:03:30 +00:00
|
|
|
): Future[PayloadExecutionStatus] {.async: (raises: [CancelledError]).} =
|
2024-10-23 00:42:46 +00:00
|
|
|
doAssert maxRetriesCount > 0
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
let
|
2024-05-14 18:03:30 +00:00
|
|
|
startTime = Moment.now()
|
2024-10-23 00:42:46 +00:00
|
|
|
deadline = deadlineObj.future
|
2024-09-20 19:32:16 +00:00
|
|
|
payload = blck.body.asEngineExecutionPayload
|
2023-03-05 01:40:21 +00:00
|
|
|
var
|
2024-05-14 18:03:30 +00:00
|
|
|
responseProcessor = ELConsensusViolationDetector.init()
|
2024-10-23 00:42:46 +00:00
|
|
|
sleepCounter = 0
|
|
|
|
retriesCount = 0
|
2024-05-14 18:03:30 +00:00
|
|
|
|
|
|
|
while true:
|
|
|
|
block mainLoop:
|
|
|
|
let
|
|
|
|
requests = m.elConnections.mapIt:
|
|
|
|
let req =
|
2024-11-13 03:29:14 +00:00
|
|
|
when typeof(blck).kind >= ConsensusFork.Electra:
|
2024-10-17 14:53:37 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/4140e528360fea53c34a766d86a000c6c039100e/src/engine/prague.md#engine_newpayloadv4
|
|
|
|
let versioned_hashes = mapIt(
|
|
|
|
blck.body.blob_kzg_commitments,
|
|
|
|
engine_api.VersionedHash(kzg_commitment_to_versioned_hash(it)))
|
|
|
|
sendNewPayloadToSingleEL(
|
|
|
|
it, payload, versioned_hashes,
|
|
|
|
FixedBytes[32] blck.parent_root.data,
|
|
|
|
[SSZ.encode(blck.body.execution_requests.deposits),
|
|
|
|
SSZ.encode(blck.body.execution_requests.withdrawals),
|
|
|
|
SSZ.encode(blck.body.execution_requests.consolidations)])
|
|
|
|
elif typeof(blck).kind == ConsensusFork.Deneb:
|
2024-05-14 18:03:30 +00:00
|
|
|
# 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(
|
|
|
|
blck.body.blob_kzg_commitments,
|
|
|
|
engine_api.VersionedHash(kzg_commitment_to_versioned_hash(it)))
|
|
|
|
sendNewPayloadToSingleEL(
|
|
|
|
it, payload, versioned_hashes,
|
|
|
|
FixedBytes[32] blck.parent_root.data)
|
2024-10-17 14:53:37 +00:00
|
|
|
elif typeof(blck).kind in [ConsensusFork.Bellatrix, ConsensusFork.Capella]:
|
2024-05-14 18:03:30 +00:00
|
|
|
sendNewPayloadToSingleEL(it, payload)
|
|
|
|
else:
|
|
|
|
static: doAssert false
|
|
|
|
engineApiRequest(it, req, "newPayload", startTime, noTimeout)
|
|
|
|
|
|
|
|
var pendingRequests = requests
|
|
|
|
|
|
|
|
while true:
|
|
|
|
let timeoutExceeded =
|
|
|
|
try:
|
|
|
|
discard await race(pendingRequests).wait(deadline)
|
|
|
|
false
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
true
|
|
|
|
except ValueError:
|
|
|
|
raiseAssert "pendingRequests should not be empty!"
|
|
|
|
except CancelledError as exc:
|
|
|
|
let pending =
|
|
|
|
requests.filterIt(not(it.finished())).mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
var stillPending: type(pendingRequests)
|
|
|
|
for request in pendingRequests:
|
|
|
|
if not(request.finished()):
|
|
|
|
stillPending.add(request)
|
|
|
|
elif request.completed():
|
|
|
|
let index = requests.find(request)
|
|
|
|
doAssert(index >= 0)
|
|
|
|
responseProcessor.processResponse(type(payload),
|
|
|
|
m.elConnections, requests, index)
|
|
|
|
pendingRequests = stillPending
|
|
|
|
|
|
|
|
if responseProcessor.disagreementAlreadyDetected:
|
|
|
|
let pending =
|
|
|
|
pendingRequests.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
return PayloadExecutionStatus.invalid
|
|
|
|
elif responseProcessor.selectedResponse.isSome():
|
|
|
|
# We spawn task which will wait for all other responses which are
|
|
|
|
# still pending, after 30.seconds all pending requests will be
|
|
|
|
# cancelled.
|
|
|
|
asyncSpawn lazyWait(pendingRequests.mapIt(FutureBase(it)))
|
|
|
|
return requests[responseProcessor.selectedResponse.get].value().status
|
|
|
|
|
|
|
|
if timeoutExceeded:
|
|
|
|
# Timeout exceeded, cancelling all pending requests.
|
|
|
|
let pending =
|
|
|
|
pendingRequests.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
return PayloadExecutionStatus.syncing
|
|
|
|
|
|
|
|
if len(pendingRequests) == 0:
|
2024-10-23 00:42:46 +00:00
|
|
|
# All requests failed.
|
|
|
|
inc(retriesCount)
|
|
|
|
if retriesCount == maxRetriesCount:
|
|
|
|
return PayloadExecutionStatus.syncing
|
2024-07-23 22:49:52 +00:00
|
|
|
|
|
|
|
# To avoid continous spam of requests when EL node is offline we
|
2024-10-23 00:42:46 +00:00
|
|
|
# going to sleep until next attempt.
|
|
|
|
await variedSleep(sleepCounter, SleepDurations)
|
2024-05-14 18:03:30 +00:00
|
|
|
break mainLoop
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-10-23 00:42:46 +00:00
|
|
|
proc sendNewPayload*(
|
|
|
|
m: ELManager,
|
|
|
|
blck: SomeForkyBeaconBlock
|
|
|
|
): Future[PayloadExecutionStatus] {.
|
|
|
|
async: (raises: [CancelledError], raw: true).} =
|
|
|
|
sendNewPayload(m, blck, DeadlineObject.init(NEWPAYLOAD_TIMEOUT), high(int))
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
proc forkchoiceUpdatedForSingleEL(
|
|
|
|
connection: ELConnection,
|
|
|
|
state: ref ForkchoiceStateV1,
|
2024-06-12 01:46:39 +00:00
|
|
|
payloadAttributes: Opt[PayloadAttributesV1] |
|
|
|
|
Opt[PayloadAttributesV2] |
|
|
|
|
Opt[PayloadAttributesV3]
|
2024-05-14 18:03:30 +00:00
|
|
|
): Future[PayloadStatusV1] {.async: (raises: [CatchableError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
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
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc forkchoiceUpdated*(
|
|
|
|
m: ELManager,
|
|
|
|
headBlockHash, safeBlockHash, finalizedBlockHash: Eth2Digest,
|
2024-06-12 01:46:39 +00:00
|
|
|
payloadAttributes: Opt[PayloadAttributesV1] |
|
|
|
|
Opt[PayloadAttributesV2] |
|
2024-10-23 00:42:46 +00:00
|
|
|
Opt[PayloadAttributesV3],
|
|
|
|
deadlineObj: DeadlineObject,
|
|
|
|
maxRetriesCount: int
|
2024-11-04 11:19:43 +00:00
|
|
|
): Future[(PayloadExecutionStatus, Opt[Hash32])] {.
|
2024-05-14 18:03:30 +00:00
|
|
|
async: (raises: [CancelledError]).} =
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
doAssert not headBlockHash.isZero
|
2024-10-23 00:42:46 +00:00
|
|
|
doAssert maxRetriesCount > 0
|
2023-03-05 01:40:21 +00:00
|
|
|
|
|
|
|
# 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-11-24 12:16:09 +00:00
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.9/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:
|
2024-11-04 11:19:43 +00:00
|
|
|
return (PayloadExecutionStatus.syncing, Opt.none Hash32)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-06-12 01:46:39 +00:00
|
|
|
when payloadAttributes is Opt[PayloadAttributesV3]:
|
2023-08-15 23:00:35 +00:00
|
|
|
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)))
|
2024-06-12 01:46:39 +00:00
|
|
|
elif payloadAttributes is Opt[PayloadAttributesV2]:
|
2023-08-15 23:00:35 +00:00
|
|
|
template payloadAttributesV3(): auto =
|
|
|
|
if payloadAttributes.isSome:
|
|
|
|
PayloadAttributesV3(
|
|
|
|
timestamp: payloadAttributes.get.timestamp,
|
|
|
|
prevRandao: payloadAttributes.get.prevRandao,
|
|
|
|
suggestedFeeRecipient: payloadAttributes.get.suggestedFeeRecipient,
|
|
|
|
withdrawals: payloadAttributes.get.withdrawals,
|
2024-10-02 13:26:11 +00:00
|
|
|
parentBeaconBlockRoot: default(Hash32))
|
2023-08-15 23:00:35 +00:00
|
|
|
else:
|
|
|
|
# As timestamp and prevRandao are both 0, won't false-positive match
|
|
|
|
(static(default(PayloadAttributesV3)))
|
2024-06-12 01:46:39 +00:00
|
|
|
elif payloadAttributes is Opt[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: @[],
|
2024-10-02 13:26:11 +00:00
|
|
|
parentBeaconBlockRoot: default(Hash32))
|
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)
|
2023-03-09 17:29:04 +00:00
|
|
|
startTime = Moment.now
|
2024-10-23 00:42:46 +00:00
|
|
|
deadline = deadlineObj.future
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
var
|
2024-05-14 18:03:30 +00:00
|
|
|
responseProcessor = ELConsensusViolationDetector.init()
|
2024-10-23 00:42:46 +00:00
|
|
|
sleepCounter = 0
|
|
|
|
retriesCount = 0
|
2024-05-14 18:03:30 +00:00
|
|
|
|
|
|
|
while true:
|
|
|
|
block mainLoop:
|
|
|
|
let requests =
|
|
|
|
m.elConnections.mapIt:
|
|
|
|
let req = it.forkchoiceUpdatedForSingleEL(state, payloadAttributes)
|
|
|
|
engineApiRequest(it, req, "forkchoiceUpdated", startTime, noTimeout)
|
|
|
|
|
|
|
|
var pendingRequests = requests
|
|
|
|
|
|
|
|
while true:
|
|
|
|
let timeoutExceeded =
|
|
|
|
try:
|
|
|
|
discard await race(pendingRequests).wait(deadline)
|
|
|
|
false
|
|
|
|
except ValueError:
|
|
|
|
raiseAssert "pendingRequests should not be empty!"
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
true
|
|
|
|
except CancelledError as exc:
|
|
|
|
let pending =
|
|
|
|
pendingRequests.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
var stillPending: type(pendingRequests)
|
|
|
|
for request in pendingRequests:
|
|
|
|
if not(request.finished()):
|
|
|
|
stillPending.add(request)
|
|
|
|
elif request.completed():
|
|
|
|
let index = requests.find(request)
|
|
|
|
doAssert(index >= 0)
|
|
|
|
responseProcessor.processResponse(
|
|
|
|
PayloadStatusV1, m.elConnections, requests, index)
|
|
|
|
pendingRequests = stillPending
|
|
|
|
|
|
|
|
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,
|
|
|
|
payloadAttributes: payloadAttributesV3))
|
|
|
|
|
|
|
|
template getSelected: untyped =
|
|
|
|
let data = requests[responseProcessor.selectedResponse.get].value()
|
|
|
|
(data.status, data.latestValidHash)
|
|
|
|
|
|
|
|
if responseProcessor.disagreementAlreadyDetected:
|
|
|
|
let pending =
|
|
|
|
pendingRequests.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
2024-11-04 11:19:43 +00:00
|
|
|
return (PayloadExecutionStatus.invalid, Opt.none Hash32)
|
2024-05-14 18:03:30 +00:00
|
|
|
elif responseProcessor.selectedResponse.isSome:
|
|
|
|
# We spawn task which will wait for all other responses which are
|
|
|
|
# still pending, after 30.seconds all pending requests will be
|
|
|
|
# cancelled.
|
|
|
|
asyncSpawn lazyWait(pendingRequests.mapIt(FutureBase(it)))
|
|
|
|
assignNextExpectedPayloadParams()
|
|
|
|
return getSelected()
|
|
|
|
|
|
|
|
if timeoutExceeded:
|
|
|
|
# Timeout exceeded, cancelling all pending requests.
|
|
|
|
let pending =
|
|
|
|
pendingRequests.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
2024-11-04 11:19:43 +00:00
|
|
|
return (PayloadExecutionStatus.syncing, Opt.none Hash32)
|
2024-05-14 18:03:30 +00:00
|
|
|
|
|
|
|
if len(pendingRequests) == 0:
|
|
|
|
# All requests failed, we will continue our attempts until deadline
|
|
|
|
# is not finished.
|
2024-10-23 00:42:46 +00:00
|
|
|
inc(retriesCount)
|
|
|
|
if retriesCount == maxRetriesCount:
|
2024-11-04 11:19:43 +00:00
|
|
|
return (PayloadExecutionStatus.syncing, Opt.none Hash32)
|
2024-07-23 22:49:52 +00:00
|
|
|
|
|
|
|
# To avoid continous spam of requests when EL node is offline we
|
2024-10-23 00:42:46 +00:00
|
|
|
# going to sleep until next attempt.
|
|
|
|
await variedSleep(sleepCounter, SleepDurations)
|
2024-05-14 18:03:30 +00:00
|
|
|
break mainLoop
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-10-23 00:42:46 +00:00
|
|
|
proc forkchoiceUpdated*(
|
|
|
|
m: ELManager,
|
|
|
|
headBlockHash, safeBlockHash, finalizedBlockHash: Eth2Digest,
|
|
|
|
payloadAttributes: Opt[PayloadAttributesV1] |
|
|
|
|
Opt[PayloadAttributesV2] |
|
|
|
|
Opt[PayloadAttributesV3]
|
2024-11-04 11:19:43 +00:00
|
|
|
): Future[(PayloadExecutionStatus, Opt[Hash32])] {.
|
2024-10-23 00:42:46 +00:00
|
|
|
async: (raises: [CancelledError], raw: true).} =
|
|
|
|
forkchoiceUpdated(
|
|
|
|
m, headBlockHash, safeBlockHash, finalizedBlockHash,
|
|
|
|
payloadAttributes, DeadlineObject.init(FORKCHOICEUPDATED_TIMEOUT),
|
|
|
|
high(int))
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
# 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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc exchangeConfigWithSingleEL(
|
|
|
|
m: ELManager,
|
|
|
|
connection: ELConnection
|
|
|
|
) {.async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
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
|
2024-05-14 18:03:30 +00:00
|
|
|
providerChain = await connection.engineApiRequest(
|
|
|
|
rpcClient.eth_chainId(), "chainId", Moment.now(),
|
|
|
|
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
|
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
|
2024-05-14 18:03:30 +00:00
|
|
|
except CancelledError as exc:
|
|
|
|
debug "Configuration exchange was interrupted"
|
|
|
|
raise exc
|
2023-03-05 01:40:21 +00:00
|
|
|
except CatchableError as exc:
|
|
|
|
# Typically because it's not synced through EIP-155, assuming this Web3
|
|
|
|
# endpoint has been otherwise working.
|
2024-05-14 18:03:30 +00:00
|
|
|
debug "Failed to obtain eth_chainId", reason = exc.msg
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2023-04-19 12:16:50 +00:00
|
|
|
connection.etcStatus = EtcStatus.match
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc exchangeTransitionConfiguration*(
|
|
|
|
m: ELManager
|
|
|
|
) {.async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
if m.elConnections.len == 0:
|
|
|
|
return
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let requests = m.elConnections.mapIt(m.exchangeConfigWithSingleEL(it))
|
|
|
|
try:
|
|
|
|
await allFutures(requests).wait(3.seconds)
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
discard
|
|
|
|
except CancelledError as exc:
|
|
|
|
let pending = requests.filterIt(not(it.finished())).
|
|
|
|
mapIt(it.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
raise exc
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let (pending, failed, finished) =
|
|
|
|
block:
|
|
|
|
var
|
|
|
|
failed = 0
|
|
|
|
done = 0
|
|
|
|
pending: seq[Future[void]]
|
|
|
|
for req in requests:
|
|
|
|
if not req.finished():
|
|
|
|
pending.add(req.cancelAndWait())
|
|
|
|
else:
|
|
|
|
if req.completed():
|
|
|
|
inc(done)
|
|
|
|
else:
|
|
|
|
inc(failed)
|
|
|
|
(pending, failed, done)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
await noCancel allFutures(pending)
|
2023-03-09 23:41:28 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
if (len(pending) > 0) or (failed != 0):
|
|
|
|
warn "Failed to exchange configuration with the configured EL end-points",
|
|
|
|
completed = finished, failed = failed, timed_out = len(pending)
|
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)
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc fetchTimestamp(
|
|
|
|
connection: ELConnection,
|
|
|
|
rpcClient: RpcClient,
|
|
|
|
blk: Eth1Block
|
|
|
|
) {.async: (raises: [CatchableError]).} =
|
2023-03-09 17:29:04 +00:00
|
|
|
debug "Fetching block timestamp", blockNum = blk.number
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let web3block = raiseIfNil await connection.engineApiRequest(
|
2023-03-05 01:40:21 +00:00
|
|
|
rpcClient.getBlockByHash(blk.hash.asBlockHash),
|
2024-05-14 18:03:30 +00:00
|
|
|
"getBlockByHash", Moment.now(), web3RequestsTimeout)
|
2023-03-09 17:29:04 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
blk.timestamp = Eth1BlockTimestamp(web3block.timestamp)
|
2022-08-10 12:31:10 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
func depositEventsToBlocks(
|
|
|
|
depositsList: openArray[JsonString]
|
|
|
|
): seq[Eth1Block] {.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)
|
2024-11-04 11:19:43 +00:00
|
|
|
blockHash = readJsonField(logEvent, blockHash, Hash32)
|
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:
|
2024-05-14 18:03:30 +00:00
|
|
|
raise newException(CorruptDataProvider,
|
|
|
|
"Web3 provider supplied invalid deposit logs")
|
2021-10-14 12:33:55 +00:00
|
|
|
|
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),
|
2024-03-19 13:22:07 +00:00
|
|
|
amount: bytes_to_uint64(amount.toArray).Gwei,
|
2021-10-14 12:33:55 +00:00
|
|
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc fetchDepositContractData(
|
|
|
|
connection: ELConnection,
|
|
|
|
rpcClient: RpcClient,
|
|
|
|
depositContract: Sender[DepositContract],
|
|
|
|
blk: Eth1Block
|
|
|
|
): Future[DepositContractDataStatus] {.async: (raises: [CancelledError]).} =
|
2020-11-03 01:21:07 +00:00
|
|
|
let
|
2024-05-14 18:03:30 +00:00
|
|
|
startTime = Moment.now()
|
2023-03-09 17:29:04 +00:00
|
|
|
deadline = sleepAsync(contractCallTimeout)
|
2024-05-14 18:03:30 +00:00
|
|
|
depositRootFut =
|
|
|
|
depositContract.get_deposit_root.call(blockNumber = blk.number)
|
|
|
|
rawCountFut =
|
|
|
|
depositContract.get_deposit_count.call(blockNumber = blk.number)
|
|
|
|
engineFut1 = connection.engineApiRequest(
|
|
|
|
depositRootFut, "get_deposit_root", startTime, deadline,
|
|
|
|
failureAllowed = true)
|
|
|
|
engineFut2 = connection.engineApiRequest(
|
|
|
|
rawCountFut, "get_deposit_count", startTime, deadline,
|
|
|
|
failureAllowed = true)
|
|
|
|
|
|
|
|
try:
|
|
|
|
await allFutures(engineFut1, engineFut2)
|
|
|
|
except CancelledError as exc:
|
|
|
|
var pending: seq[Future[void]]
|
|
|
|
if not(engineFut1.finished()):
|
|
|
|
pending.add(engineFut1.cancelAndWait())
|
|
|
|
if not(engineFut2.finished()):
|
|
|
|
pending.add(engineFut2.cancelAndWait())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
var res: DepositContractDataStatus
|
2023-03-09 17:29:04 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
try:
|
2024-05-14 18:03:30 +00:00
|
|
|
# `engineFut1` could hold timeout exception `DataProviderTimeout`.
|
|
|
|
discard engineFut1.read()
|
|
|
|
let fetchedRoot = asEth2Digest(depositRootFut.read())
|
2022-08-10 12:31:10 +00:00
|
|
|
if blk.depositRoot.isZero:
|
|
|
|
blk.depositRoot = fetchedRoot
|
2024-05-14 18:03:30 +00:00
|
|
|
res = Fetched
|
2022-08-10 12:31:10 +00:00
|
|
|
elif blk.depositRoot == fetchedRoot:
|
2024-05-14 18:03:30 +00:00
|
|
|
res = VerifiedCorrect
|
2020-11-03 01:21:07 +00:00
|
|
|
else:
|
2024-05-14 18:03:30 +00:00
|
|
|
res = DepositRootIncorrect
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "Failed to fetch deposits root", block_number = blk.number,
|
|
|
|
reason = exc.msg
|
|
|
|
res = DepositRootUnavailable
|
2020-10-21 13:25:53 +00:00
|
|
|
|
2020-11-03 01:21:07 +00:00
|
|
|
try:
|
2024-05-14 18:03:30 +00:00
|
|
|
# `engineFut2` could hold timeout exception `DataProviderTimeout`.
|
|
|
|
discard engineFut2.read()
|
|
|
|
let fetchedCount = bytes_to_uint64(rawCountFut.read().toArray)
|
2022-08-10 12:31:10 +00:00
|
|
|
if blk.depositCount == 0:
|
|
|
|
blk.depositCount = fetchedCount
|
|
|
|
elif blk.depositCount != fetchedCount:
|
2024-05-14 18:03:30 +00:00
|
|
|
res = DepositCountIncorrect
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "Failed to fetch deposits count", block_number = blk.number,
|
|
|
|
reason = exc.msg
|
|
|
|
res = DepositCountUnavailable
|
|
|
|
res
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
func new*(T: type ELConnection, engineUrl: EngineApiUrl): T =
|
2023-03-05 01:40:21 +00:00
|
|
|
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],
|
2024-06-12 01:46:39 +00:00
|
|
|
eth1Network: Opt[Eth1Network]): T =
|
2023-03-05 01:40:21 +00:00
|
|
|
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,
|
2024-05-14 18:03:30 +00:00
|
|
|
blocksPerLogsRequest: targetBlocksPerLogsRequest,
|
|
|
|
managerState: ELManagerState.Running)
|
|
|
|
|
|
|
|
proc stop(m: ELManager) {.async: (raises: []).} =
|
|
|
|
if m.managerState notin {ELManagerState.Closing, ELManagerState.Closed}:
|
|
|
|
m.managerState = ELManagerState.Closing
|
|
|
|
var pending: seq[Future[void].Raising([])]
|
|
|
|
if not(m.chainSyncingLoopFut.isNil()) and
|
|
|
|
not(m.chainSyncingLoopFut.finished()):
|
|
|
|
pending.add(m.chainSyncingLoopFut.cancelAndWait())
|
|
|
|
if not(m.exchangeTransitionConfigurationLoopFut.isNil()) and
|
|
|
|
not(m.exchangeTransitionConfigurationLoopFut.finished()):
|
|
|
|
pending.add(m.exchangeTransitionConfigurationLoopFut.cancelAndWait())
|
|
|
|
for connection in m.elConnections:
|
|
|
|
pending.add(connection.close())
|
|
|
|
await noCancel allFutures(pending)
|
|
|
|
m.managerState = ELManagerState.Closed
|
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
|
|
|
|
|
2024-03-19 13:21:47 +00:00
|
|
|
if latestEth1BlockNumber > blocksOfInterestRange.Eth1BlockNumber:
|
2023-03-10 17:40:27 +00:00
|
|
|
latestEth1BlockNumber - blocksOfInterestRange
|
|
|
|
else:
|
2024-03-19 13:21:47 +00:00
|
|
|
0.Eth1BlockNumber
|
2020-11-19 17:19:03 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc syncBlockRange(
|
|
|
|
m: ELManager,
|
|
|
|
connection: ELConnection,
|
|
|
|
rpcClient: RpcClient,
|
|
|
|
depositContract: Sender[DepositContract],
|
|
|
|
fromBlock, toBlock,
|
|
|
|
fullSyncFromBlock: Eth1BlockNumber
|
|
|
|
) {.async: (raises: [CatchableError]).} =
|
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))
|
2024-05-14 18:03:30 +00:00
|
|
|
|
2024-05-16 11:54:51 +00:00
|
|
|
depositLogs =
|
2024-05-14 18:03:30 +00:00
|
|
|
try:
|
|
|
|
await connection.engineApiRequest(
|
|
|
|
depositContract.getJsonLogs(
|
|
|
|
DepositEvent,
|
2024-06-12 01:46:39 +00:00
|
|
|
fromBlock = Opt.some blockId(currentBlock),
|
|
|
|
toBlock = Opt.some blockId(maxBlockNumberRequested)),
|
2024-05-14 18:03:30 +00:00
|
|
|
"getLogs", Moment.now(), 30.seconds)
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "Request for deposit logs was interrupted"
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "Request for deposit logs failed", reason = exc.msg
|
|
|
|
inc failed_web3_requests
|
|
|
|
backoff = (backoff * 3) div 2
|
|
|
|
m.blocksPerLogsRequest = m.blocksPerLogsRequest div 2
|
|
|
|
if m.blocksPerLogsRequest == 0:
|
|
|
|
m.blocksPerLogsRequest = 1
|
|
|
|
raise exc
|
|
|
|
continue
|
2022-08-12 13:51:33 +00:00
|
|
|
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:
|
2024-05-14 18:03:30 +00:00
|
|
|
try:
|
|
|
|
await fetchTimestamp(connection, rpcClient, blk)
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "Request for block timestamp was interrupted",
|
|
|
|
block_number = blk.number
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "Request for block timestamp failed",
|
|
|
|
block_number = blk.number, reason = exc.msg
|
|
|
|
|
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-05-14 18:03:30 +00:00
|
|
|
let noDepositsBlock =
|
|
|
|
try:
|
|
|
|
raiseIfNil await connection.engineApiRequest(
|
|
|
|
rpcClient.getBlockByNumber(n),
|
|
|
|
"getBlockByNumber", Moment.now(), web3RequestsTimeout)
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "The process of obtaining the block was interrupted",
|
|
|
|
block_number = n
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "Request for block failed", block_number = n,
|
|
|
|
reason = exc.msg
|
|
|
|
raise exc
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
let status =
|
|
|
|
when hasDepositRootChecks:
|
|
|
|
await fetchDepositContractData(
|
|
|
|
connection, rpcClient, depositContract, lastBlock)
|
|
|
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc startExchangeTransitionConfigurationLoop(
|
|
|
|
m: ELManager
|
|
|
|
) {.async: (raises: [CancelledError]).} =
|
2023-03-05 01:40:21 +00:00
|
|
|
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
|
2024-05-14 18:03:30 +00:00
|
|
|
await m.exchangeTransitionConfiguration()
|
2023-03-09 17:29:04 +00:00
|
|
|
await sleepAsync(60.seconds)
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc syncEth1Chain(
|
|
|
|
m: ELManager,
|
|
|
|
connection: ELConnection
|
|
|
|
) {.async: (raises: [CatchableError]).} =
|
|
|
|
let rpcClient =
|
|
|
|
try:
|
|
|
|
await connection.connectedRpcClient().wait(1.seconds)
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
raise newException(DataProviderTimeout, "Connection timed out")
|
|
|
|
|
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-05-14 18:03:30 +00:00
|
|
|
matchingBlockAtNewEl =
|
|
|
|
try:
|
|
|
|
raiseIfNil await connection.engineApiRequest(
|
|
|
|
rpcClient.getBlockByNumber(lastKnownBlock.number),
|
|
|
|
"getBlockByNumber", Moment.now(), web3RequestsTimeout)
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "getBlockByNumber request has been interrupted",
|
|
|
|
last_known_block_number = lastKnownBlock.number
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "getBlockByNumber request failed",
|
|
|
|
last_known_block_number = lastKnownBlock.number,
|
|
|
|
reason = exc.msg
|
|
|
|
raise exc
|
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-05-14 18:03:30 +00:00
|
|
|
let startBlock =
|
|
|
|
try:
|
|
|
|
raiseIfNil await connection.engineApiRequest(
|
|
|
|
rpcClient.getBlockByHash(finalizedBlockHash),
|
|
|
|
"getBlockByHash", Moment.now(), web3RequestsTimeout)
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "getBlockByHash() request has been interrupted",
|
|
|
|
finalized_block_hash = finalizedBlockHash
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "getBlockByHash() request has failed",
|
|
|
|
finalized_block_hash = finalizedBlockHash,
|
|
|
|
reason = exc.msg
|
|
|
|
raise exc
|
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
|
|
|
|
2024-03-08 18:27:45 +00:00
|
|
|
var latestBlockNumber: Eth1BlockNumber
|
2020-11-03 01:21:07 +00:00
|
|
|
while true:
|
2024-03-08 18:27:45 +00:00
|
|
|
debug "syncEth1Chain tick",
|
|
|
|
shouldProcessDeposits, latestBlockNumber, eth1SyncedTo
|
2023-03-05 01:40:21 +00:00
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
# TODO (cheatfate): This should be removed
|
2020-11-03 01:21:07 +00:00
|
|
|
if bnStatus == BeaconNodeStatus.Stopping:
|
2024-05-14 18:03:30 +00:00
|
|
|
await noCancel 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:
|
2024-05-14 18:03:30 +00:00
|
|
|
raise newException(CorruptDataProvider,
|
|
|
|
"Eth1 chain contradicts Eth2 consensus")
|
|
|
|
|
|
|
|
let latestBlock =
|
|
|
|
try:
|
|
|
|
raiseIfNil await connection.engineApiRequest(
|
|
|
|
rpcClient.eth_getBlockByNumber(blockId("latest"), false),
|
|
|
|
"getBlockByNumber", Moment.now(), web3RequestsTimeout)
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "Latest block request has been interrupted"
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
warn "Failed to obtain the latest block from the EL", reason = exc.msg
|
|
|
|
raise exc
|
2021-01-29 21:21:44 +00:00
|
|
|
|
2024-03-19 13:21:47 +00:00
|
|
|
latestBlockNumber = latestBlock.number
|
2021-11-25 16:51:51 +00:00
|
|
|
|
2024-06-12 01:46:39 +00:00
|
|
|
m.syncTargetBlock = Opt.some(
|
2024-03-19 13:21:47 +00:00
|
|
|
if latestBlock.number > m.cfg.ETH1_FOLLOW_DISTANCE.Eth1BlockNumber:
|
|
|
|
latestBlock.number - m.cfg.ETH1_FOLLOW_DISTANCE
|
2022-08-12 19:44:55 +00:00
|
|
|
else:
|
2024-03-19 13:21:47 +00:00
|
|
|
0.Eth1BlockNumber)
|
2023-03-05 01:40:21 +00:00
|
|
|
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:
|
2024-05-14 18:03:30 +00:00
|
|
|
try:
|
|
|
|
await m.syncBlockRange(connection,
|
|
|
|
rpcClient,
|
|
|
|
depositContract,
|
|
|
|
eth1SyncedTo + 1,
|
|
|
|
m.syncTargetBlock.get,
|
|
|
|
m.earliestBlockOfInterest(latestBlock.number))
|
|
|
|
except CancelledError as exc:
|
|
|
|
debug "Syncing block range process has been interrupted"
|
|
|
|
raise exc
|
|
|
|
except CatchableError as exc:
|
|
|
|
debug "Syncing block range process has been failed", reason = exc.msg
|
|
|
|
raise exc
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc startChainSyncingLoop(
|
|
|
|
m: ELManager
|
|
|
|
) {.async: (raises: []).} =
|
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"
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
var runLoop = true
|
|
|
|
while runLoop:
|
2023-03-10 10:55:55 +00:00
|
|
|
try:
|
2024-05-14 18:03:30 +00:00
|
|
|
let connection = await syncedConnectionFut.wait(60.seconds)
|
|
|
|
await syncEth1Chain(m, connection)
|
|
|
|
except AsyncTimeoutError:
|
|
|
|
notice "No synced EL nodes available for deposit syncing"
|
|
|
|
try:
|
2023-03-05 01:40:21 +00:00
|
|
|
await sleepAsync(chronos.seconds(30))
|
2024-05-14 18:03:30 +00:00
|
|
|
except CancelledError:
|
|
|
|
runLoop = false
|
|
|
|
except CancelledError:
|
|
|
|
runLoop = false
|
2024-05-15 04:44:52 +00:00
|
|
|
except CatchableError:
|
2024-05-14 18:03:30 +00:00
|
|
|
try:
|
|
|
|
await sleepAsync(10.seconds)
|
|
|
|
except CancelledError:
|
|
|
|
runLoop = false
|
|
|
|
break
|
2023-03-09 23:41:28 +00:00
|
|
|
debug "Restarting the deposit syncing loop"
|
2024-05-14 18:03:30 +00:00
|
|
|
# A more detailed error is already logged by trackEngineApiRequest
|
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
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
debug "EL chain syncing process has been stopped"
|
|
|
|
|
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) & "]"
|
|
|
|
|
2024-05-14 18:03:30 +00:00
|
|
|
proc testWeb3Provider*(
|
|
|
|
web3Url: Uri,
|
|
|
|
depositContractAddress: Eth1Address,
|
|
|
|
jwtSecret: Opt[seq[byte]]
|
|
|
|
) {.async: (raises: [CatchableError]).} =
|
|
|
|
|
2022-07-25 19:23:02 +00:00
|
|
|
stdout.write "Establishing web3 connection..."
|
2024-05-14 18:03:30 +00:00
|
|
|
let web3 =
|
|
|
|
try:
|
|
|
|
await newWeb3($web3Url,
|
|
|
|
getJsonRpcRequestHeaders(jwtSecret)).wait(5.seconds)
|
|
|
|
except CatchableError as exc:
|
|
|
|
stdout.write "\rEstablishing web3 connection: Failure(" & exc.msg & ")\n"
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
stdout.write "\rEstablishing web3 connection: Connected\n"
|
2022-07-25 19:23:02 +00:00
|
|
|
|
|
|
|
template request(actionDesc: static string,
|
|
|
|
action: untyped): untyped =
|
|
|
|
stdout.write actionDesc & "..."
|
|
|
|
stdout.flushFile()
|
|
|
|
var res: typeof(read action)
|
|
|
|
try:
|
2024-05-14 18:03:30 +00:00
|
|
|
let fut = action
|
|
|
|
res = await fut.wait(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":
|
2024-03-19 13:21:47 +00:00
|
|
|
ns.get_deposit_root.call(blockNumber = latestBlock.number)
|