nimbus-eth1/nimbus/beacon/api_handler/api_forkchoice.nim

206 lines
7.8 KiB
Nim

# Nimbus
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
import
std/[typetraits],
eth/common,
stew/results,
../web3_eth_conv,
../beacon_engine,
web3/execution_types,
./api_utils,
chronicles
{.push gcsafe, raises:[CatchableError].}
template validateVersion(attr, com, apiVersion) =
let
version = attr.version
timestamp = ethTime attr.timestamp
if apiVersion == Version.V3:
if version != apiVersion:
raise invalidAttr("forkChoiceUpdatedV3 expect PayloadAttributesV3" &
" but got PayloadAttributes" & $version)
if not com.isCancunOrLater(timestamp):
raise unsupportedFork(
"forkchoiceUpdatedV3 get invalid payloadAttributes timestamp")
else:
if com.isCancunOrLater(timestamp):
if version < Version.V3:
raise unsupportedFork("forkChoiceUpdated" & $apiVersion &
" doesn't support payloadAttributes" & $version)
if version >= Version.V3:
raise invalidAttr("forkChoiceUpdated" & $apiVersion &
" doesn't support PayloadAttributes" & $version)
elif com.isShanghaiOrLater(timestamp):
if version < Version.V2:
raise invalidParams("forkChoiceUpdated" & $apiVersion &
" doesn't support payloadAttributesV1")
if version > Version.V2:
raise invalidAttr("if timestamp is Shanghai or later," &
" payloadAttributes must be PayloadAttributesV2")
else:
if version != Version.V1:
raise invalidParams("if timestamp is earlier than Shanghai," &
" payloadAttributes must be PayloadAttributesV1")
proc forkchoiceUpdated*(ben: BeaconEngineRef,
apiVersion: Version,
update: ForkchoiceStateV1,
attrsOpt: Option[PayloadAttributes]):
ForkchoiceUpdatedResponse =
let
com = ben.com
db = com.db
chain = ben.chain
blockHash = ethHash update.headBlockHash
if blockHash == common.Hash256():
warn "Forkchoice requested update to zero hash"
return simpleFCU(PayloadExecutionStatus.invalid)
# Check whether we have the block yet in our database or not. If not, we'll
# need to either trigger a sync, or to reject this forkchoice update for a
# reason.
var header: common.BlockHeader
if not db.getBlockHeader(blockHash, header):
# If the head hash is unknown (was not given to us in a newPayload request),
# we cannot resolve the header, so not much to do. This could be extended in
# the future to resolve from the `eth` network, but it's an unexpected case
# that should be fixed, not papered over.
if not ben.get(blockHash, header):
warn "Forkchoice requested unknown head",
hash = blockHash.short
return simpleFCU(PayloadExecutionStatus.syncing)
# Header advertised via a past newPayload request. Start syncing to it.
# Before we do however, make sure any legacy sync in switched off so we
# don't accidentally have 2 cycles running.
if not ben.ttdReached():
ben.reachTTD()
# TODO: cancel downloader
info "Forkchoice requested sync to new head",
number = header.blockNumber,
hash = blockHash.short
# Update sync header (if any)
com.syncReqNewHead(header)
return simpleFCU(PayloadExecutionStatus.syncing)
# Block is known locally, just sanity check that the beacon client does not
# attempt to push us back to before the merge.
let blockNumber = header.blockNumber.truncate(uint64)
if header.difficulty > 0.u256 or blockNumber == 0'u64:
var
td, ptd: DifficultyInt
ttd = com.ttd.get(high(common.BlockNumber))
if not db.getTd(blockHash, td) or (blockNumber > 0'u64 and not db.getTd(header.parentHash, ptd)):
error "TDs unavailable for TTD check",
number = blockNumber,
hash = blockHash.short,
td = td,
parent = header.parentHash.short,
ptd = ptd
return simpleFCU(PayloadExecutionStatus.invalid, "TDs unavailable for TDD check")
if td < ttd or (blockNumber > 0'u64 and ptd > ttd):
error "Refusing beacon update to pre-merge",
number = blockNumber,
hash = blockHash.short,
diff = header.difficulty,
ptd = ptd,
ttd = ttd
return invalidFCU()
# If the head block is already in our canonical chain, the beacon client is
# probably resyncing. Ignore the update.
var canonHash: common.Hash256
if db.getBlockHash(header.blockNumber, canonHash) and canonHash == blockHash:
# TODO should this be possible?
# If we allow these types of reorgs, we will do lots and lots of reorgs during sync
debug "Reorg to previous block"
if chain.setCanonical(header) != ValidationResult.OK:
return invalidFCU(com, header)
elif chain.setCanonical(header) != ValidationResult.OK:
return invalidFCU(com, header)
# If the beacon client also advertised a finalized block, mark the local
# chain final and completely in PoS mode.
let finalizedBlockHash = ethHash update.finalizedBlockHash
if finalizedBlockHash != common.Hash256():
if not ben.posFinalized:
ben.finalizePoS()
# TODO: If the finalized block is not in our canonical tree, somethings wrong
var finalBlock: common.BlockHeader
if not db.getBlockHeader(finalizedBlockHash, finalBlock):
warn "Final block not available in database",
hash=finalizedBlockHash.short
raise invalidForkChoiceState("finalized block header not available")
var finalHash: common.Hash256
if not db.getBlockHash(finalBlock.blockNumber, finalHash):
warn "Final block not in canonical chain",
number=finalBlock.blockNumber,
hash=finalizedBlockHash.short
raise invalidForkChoiceState("finalized block hash not available")
if finalHash != finalizedBlockHash:
warn "Final block not in canonical chain",
number=finalBlock.blockNumber,
expect=finalizedBlockHash.short,
get=finalHash.short
raise invalidForkChoiceState("finalized block not canonical")
db.finalizedHeaderHash(finalizedBlockHash)
let safeBlockHash = ethHash update.safeBlockHash
if safeBlockHash != common.Hash256():
var safeBlock: common.BlockHeader
if not db.getBlockHeader(safeBlockHash, safeBlock):
warn "Safe block not available in database",
hash = safeBlockHash.short
raise invalidForkChoiceState("safe head not available")
var safeHash: common.Hash256
if not db.getBlockHash(safeBlock.blockNumber, safeHash):
warn "Safe block hash not available in database",
hash = safeHash.short
raise invalidForkChoiceState("safe block hash not available")
if safeHash != safeBlockHash:
warn "Safe block not in canonical chain",
blockNumber=safeBlock.blockNumber,
expect=safeBlockHash.short,
get=safeHash.short
raise invalidForkChoiceState("safe head not canonical")
db.safeHeaderHash(safeBlockHash)
# If payload generation was requested, create a new block to be potentially
# sealed by the beacon client. The payload will be requested later, and we
# might replace it arbitrarilly many times in between.
if attrsOpt.isSome:
let attrs = attrsOpt.get()
validateVersion(attrs, com, apiVersion)
let payload = ben.generatePayload(attrs).valueOr:
error "Failed to create sealing payload", err = error
raise invalidAttr(error)
let id = computePayloadId(blockHash, attrs)
ben.put(id, ben.blockValue, payload)
info "Created payload for sealing",
id = id.toHex,
hash = payload.blockHash.short,
number = payload.blockNumber
return validFCU(some(id), blockHash)
return validFCU(none(PayloadID), blockHash)