From 694b653757b4b39664d34ee6f95d0cbb6f26eac1 Mon Sep 17 00:00:00 2001 From: zah Date: Wed, 15 Jun 2022 05:38:27 +0300 Subject: [PATCH] Bellatrix TTD detection (#3745) * Bellatrix TTD detection * Update beacon_chain/eth1/eth1_monitor.nim Co-authored-by: Etan Kissling * Update beacon_chain/nimbus_beacon_node.nim Co-authored-by: tersec Co-authored-by: Etan Kissling Co-authored-by: tersec --- beacon_chain/beacon_node.nim | 1 + beacon_chain/eth1/eth1_monitor.nim | 129 ++++++++++++++++--- beacon_chain/nimbus_beacon_node.nim | 26 +++- beacon_chain/validators/validator_duties.nim | 14 +- 4 files changed, 139 insertions(+), 31 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 4cb9ace1c..50d007a52 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -73,6 +73,7 @@ type restKeysCache*: Table[ValidatorPubKey, ValidatorIndex] validatorMonitor*: ref ValidatorMonitor stateTtlCache*: StateTtlCache + nextExchangeTransitionConfTime*: Moment const MaxEmptySlotCount* = uint64(10*60) div SECONDS_PER_SLOT diff --git a/beacon_chain/eth1/eth1_monitor.nim b/beacon_chain/eth1/eth1_monitor.nim index e6a4cf0dc..762525ff6 100644 --- a/beacon_chain/eth1/eth1_monitor.nim +++ b/beacon_chain/eth1/eth1_monitor.nim @@ -118,6 +118,9 @@ type depositsChain: Eth1Chain eth1Progress: AsyncEvent + terminalBlockHash*: Option[BlockHash] + terminalBlockNumber*: Option[Quantity] + runFut: Future[void] stopFut: Future[void] getBeaconTime: GetBeaconTimeFn @@ -517,6 +520,50 @@ proc forkchoiceUpdated*(p: Eth1Monitor, prevRandao: FixedBytes[32] randomData, suggestedFeeRecipient: suggestedFeeRecipient))) +# TODO can't be defined within exchangeTransitionConfiguration +proc `==`(x, y: Quantity): bool {.borrow, noSideEffect.} + +proc exchangeTransitionConfiguration*(p: Eth1Monitor): Future[void] {.async.} = + # Eth1 monitor can recycle connections without (external) warning; at least, + # don't crash. + if p.isNil: + debug "exchangeTransitionConfiguration: nil Eth1Monitor" + + if p.isNil or p.dataProvider.isNil: + return + + let ccTransitionConfiguration = TransitionConfigurationV1( + terminalTotalDifficulty: p.depositsChain.cfg.TERMINAL_TOTAL_DIFFICULTY, + terminalBlockHash: + if p.terminalBlockHash.isSome: + p.terminalBlockHash.get + else: + # TODO can't use static(default(...)) in this context + default(BlockHash), + terminalBlockNumber: + if p.terminalBlockNumber.isSome: + p.terminalBlockNumber.get + else: + # TODO can't use static(default(...)) in this context + default(Quantity)) + let ecTransitionConfiguration = + await p.dataProvider.web3.provider.engine_exchangeTransitionConfigurationV1( + ccTransitionConfiguration) + if ccTransitionConfiguration != ecTransitionConfiguration: + warn "exchangeTransitionConfiguration: Configuration mismatch detected", + consensusTerminalTotalDifficulty = + ccTransitionConfiguration.terminalTotalDifficulty, + consensusTerminalBlockHash = + ccTransitionConfiguration.terminalBlockHash, + consensusTerminalBlockNumber = + ccTransitionConfiguration.terminalBlockNumber.uint64, + executionTerminalTotalDifficulty = + ecTransitionConfiguration.terminalTotalDifficulty, + executionTerminalBlockHash = + ecTransitionConfiguration.terminalBlockHash, + executionTerminalBlockNumber = + ecTransitionConfiguration.terminalBlockNumber.uint64 + template readJsonField(j: JsonNode, fieldName: string, ValueType: type): untyped = var res: ValueType fromJson(j[fieldName], fieldName, res) @@ -949,6 +996,12 @@ proc createInitialDepositSnapshot*( return ok DepositContractSnapshot(eth1Block: knownStartBlockHash) +proc currentEpoch(m: Eth1Monitor): Epoch = + if m.getBeaconTime != nil: + m.getBeaconTime().slotOrZero.epoch + else: + Epoch 0 + proc init*(T: type Eth1Monitor, cfg: RuntimeConfig, db: BeaconChainDB, @@ -1323,7 +1376,10 @@ proc startEth1Syncing(m: Eth1Monitor, delayBeforeStart: Duration) {.async.} = await m.dataProvider.onBlockHeaders(newBlockHeadersHandler, subscriptionErrorHandler) - if m.depositsChain.blocks.len == 0: + let shouldProcessDeposits = not m.depositContractAddress.isZeroMemory + var scratchMerkleizer: ref DepositsMerkleizer + var eth1SyncedTo: Eth1BlockNumber + if shouldProcessDeposits and m.depositsChain.blocks.len == 0: let startBlock = awaitWithRetries( m.dataProvider.getBlockByHash(m.depositsChain.finalizedBlockHash.asBlockHash)) @@ -1334,15 +1390,16 @@ proc startEth1Syncing(m: Eth1Monitor, delayBeforeStart: Duration) {.async.} = m.depositsChain.finalizedBlockHash, m.depositsChain.finalizedDepositsMerkleizer)) - var eth1SyncedTo = Eth1BlockNumber m.depositsChain.blocks.peekLast.number - eth1_synced_head.set eth1SyncedTo.toGaugeValue - eth1_finalized_head.set eth1SyncedTo.toGaugeValue - eth1_finalized_deposits.set( - m.depositsChain.finalizedDepositsMerkleizer.getChunkCount.toGaugeValue) + eth1SyncedTo = Eth1BlockNumber startBlock.number - var scratchMerkleizer = newClone(copy m.finalizedDepositsMerkleizer) + eth1_synced_head.set eth1SyncedTo.toGaugeValue + eth1_finalized_head.set eth1SyncedTo.toGaugeValue + eth1_finalized_deposits.set( + m.depositsChain.finalizedDepositsMerkleizer.getChunkCount.toGaugeValue) - debug "Starting Eth1 syncing", `from` = shortLog(m.depositsChain.blocks[0]) + scratchMerkleizer = newClone(copy m.finalizedDepositsMerkleizer) + + debug "Starting Eth1 syncing", `from` = shortLog(m.depositsChain.blocks[0]) while true: if bnStatus == BeaconNodeStatus.Stopping: @@ -1361,7 +1418,7 @@ proc startEth1Syncing(m: Eth1Monitor, delayBeforeStart: Duration) {.async.} = m.startIdx = 0 return - if mustUsePolling: + let nextBlock = if mustUsePolling: let blk = awaitWithRetries( m.dataProvider.web3.provider.eth_getBlockByNumber(blockId("latest"), false)) @@ -1373,26 +1430,56 @@ proc startEth1Syncing(m: Eth1Monitor, delayBeforeStart: Duration) {.async.} = continue m.latestEth1Block = some fullBlockId + blk else: awaitWithTimeout(m.eth1Progress.wait(), 5.minutes): raise newException(CorruptDataProvider, "No eth1 chain progress for too long") m.eth1Progress.clear() - if m.latestEth1BlockNumber <= m.cfg.ETH1_FOLLOW_DISTANCE: - continue + if m.latestEth1Block.isNone: + # It should not be possible for `latestEth1Block` to be none here. + # Firing the `eth1Progress` event is always done after assinging + # a value for it. + continue - let targetBlock = m.latestEth1BlockNumber - m.cfg.ETH1_FOLLOW_DISTANCE - if targetBlock <= eth1SyncedTo: - continue + awaitWithRetries m.dataProvider.getBlockByHash(m.latestEth1Block.get.hash) - let earliestBlockOfInterest = m.earliestBlockOfInterest() - await m.syncBlockRange(scratchMerkleizer, - eth1SyncedTo + 1, - targetBlock, - earliestBlockOfInterest) - eth1SyncedTo = targetBlock - eth1_synced_head.set eth1SyncedTo.toGaugeValue + if m.currentEpoch >= m.cfg.BELLATRIX_FORK_EPOCH and m.terminalBlockHash.isNone: + var terminalBlockCandidate = nextBlock + + info "startEth1Syncing: checking for merge terminal block", + currentEpoch = m.currentEpoch, + BELLATRIX_FORK_EPOCH = m.cfg.BELLATRIX_FORK_EPOCH, + totalDifficulty = nextBlock.totalDifficulty, + ttd = m.cfg.TERMINAL_TOTAL_DIFFICULTY, + terminalBlockHash = m.terminalBlockHash + + if terminalBlockCandidate.totalDifficulty >= m.cfg.TERMINAL_TOTAL_DIFFICULTY: + while not terminalBlockCandidate.parentHash.isZeroMemory: + var parentBlock = awaitWithRetries( + m.dataProvider.getBlockByHash(terminalBlockCandidate.parentHash)) + if parentBlock.totalDifficulty < m.cfg.TERMINAL_TOTAL_DIFFICULTY: + break + terminalBlockCandidate = parentBlock + m.terminalBlockHash = some terminalBlockCandidate.hash + m.terminalBlockNumber = some terminalBlockCandidate.number + + if shouldProcessDeposits: + if m.latestEth1BlockNumber <= m.cfg.ETH1_FOLLOW_DISTANCE: + continue + + let targetBlock = m.latestEth1BlockNumber - m.cfg.ETH1_FOLLOW_DISTANCE + if targetBlock <= eth1SyncedTo: + continue + + let earliestBlockOfInterest = m.earliestBlockOfInterest() + await m.syncBlockRange(scratchMerkleizer, + eth1SyncedTo + 1, + targetBlock, + earliestBlockOfInterest) + eth1SyncedTo = targetBlock + eth1_synced_head.set eth1SyncedTo.toGaugeValue proc start(m: Eth1Monitor, delayBeforeStart: Duration) {.gcsafe.} = if m.runFut.isNil: diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index ec1ce8223..ada70428b 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -9,7 +9,7 @@ import std/[os, random, sequtils, terminal, times], - bearssl, chronicles, chronos, + bearssl, chronos, chronicles, chronicles/chronos_tools, metrics, metrics/chronos_httpserver, stew/[byteutils, io2], eth/p2p/discoveryv5/[enr, random2], @@ -559,8 +559,8 @@ proc init*(T: type BeaconNode, dag = loadChainDag( config, cfg, db, eventBus, validatorMonitor, networkGenesisValidatorsRoot) - beaconClock = BeaconClock.init( - getStateField(dag.headState, genesis_time)) + genesisTime = getStateField(dag.headState, genesis_time) + beaconClock = BeaconClock.init(genesisTime) getBeaconTime = beaconClock.getBeaconTimeFn() if config.weakSubjectivityCheckpoint.isSome: @@ -665,6 +665,13 @@ proc init*(T: type BeaconNode, else: nil + bellatrixEpochTime = + genesisTime + cfg.BELLATRIX_FORK_EPOCH * SLOTS_PER_EPOCH * SECONDS_PER_SLOT + + nextExchangeTransitionConfTime = + max(Moment.init(int64 bellatrixEpochTime, Second), + Moment.now) + let node = BeaconNode( nickname: nickname, graffitiBytes: if config.graffiti.isSome: config.graffiti.get @@ -683,7 +690,8 @@ proc init*(T: type BeaconNode, gossipState: {}, beaconClock: beaconClock, validatorMonitor: validatorMonitor, - stateTtlCache: stateTtlCache) + stateTtlCache: stateTtlCache, + nextExchangeTransitionConfTime: nextExchangeTransitionConfTime) node.initLightClient( rng, cfg, dag.forkDigests, getBeaconTime, dag.genesis_validators_root) @@ -1255,7 +1263,7 @@ proc handleMissingBlocks(node: BeaconNode) = debug "Requesting detected missing blocks", blocks = shortLog(missingBlocks) node.requestManager.fetchAncestorBlocks(missingBlocks) -proc onSecond(node: BeaconNode) = +proc onSecond(node: BeaconNode, time: Moment) = ## This procedure will be called once per second. if not(node.syncManager.inProgress): node.handleMissingBlocks() @@ -1263,6 +1271,12 @@ proc onSecond(node: BeaconNode) = # Nim GC metrics (for the main thread) updateThreadMetrics() + ## This procedure will be called once per minute. + # https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.9/src/engine/specification.md#engine_exchangetransitionconfigurationv1 + if time > node.nextExchangeTransitionConfTime and not node.eth1Monitor.isNil: + node.nextExchangeTransitionConfTime = time + chronos.minutes(1) + traceAsyncErrors node.eth1Monitor.exchangeTransitionConfiguration() + if node.config.stopAtSyncedEpoch != 0 and node.dag.head.slot.epoch >= node.config.stopAtSyncedEpoch: notice "Shutting down after having reached the target synced epoch" @@ -1276,7 +1290,7 @@ proc runOnSecondLoop(node: BeaconNode) {.async.} = await chronos.sleepAsync(sleepTime) let afterSleep = chronos.now(chronos.Moment) let sleepTime = afterSleep - start - node.onSecond() + node.onSecond(start) let finished = chronos.now(chronos.Moment) let processingTime = finished - afterSleep ticks_delay.set(sleepTime.nanoseconds.float / nanosecondsIn1s) diff --git a/beacon_chain/validators/validator_duties.nim b/beacon_chain/validators/validator_duties.nim index 0c34a206b..21cc67286 100644 --- a/beacon_chain/validators/validator_duties.nim +++ b/beacon_chain/validators/validator_duties.nim @@ -571,11 +571,16 @@ proc getExecutionPayload( const GETPAYLOAD_TIMEOUT = 1.seconds let + terminalBlockHash = + if node.eth1Monitor.terminalBlockHash.isSome: + node.eth1Monitor.terminalBlockHash.get.asEth2Digest + else: + default(Eth2Digest) latestHead = if not node.dag.head.executionBlockRoot.isZero: node.dag.head.executionBlockRoot else: - default(Eth2Digest) + terminalBlockHash latestFinalized = node.dag.finalizedHead.blck.executionBlockRoot payload_id = (await forkchoice_updated( proposalState.bellatrixData.data, latestHead, latestFinalized, @@ -653,9 +658,10 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode, else: node.syncCommitteeMsgPool[].produceSyncAggregate(head.root), if slot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH or - # TODO when Eth1Monitor TTD following comes in, actually detect - # transition block directly - not is_merge_transition_complete(proposalState.bellatrixData.data): + not ( + is_merge_transition_complete(proposalState.bellatrixData.data) or + ((not node.eth1Monitor.isNil) and + node.eth1Monitor.terminalBlockHash.isSome)): default(bellatrix.ExecutionPayload) else: let pubkey = node.dag.validatorKey(validator_index)