213 lines
7.9 KiB
Nim
213 lines
7.9 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
|
|
results,
|
|
../web3_eth_conv,
|
|
eth/common/hashes,
|
|
../beacon_engine,
|
|
web3/[execution_types, primitives],
|
|
../payload_conv,
|
|
./api_utils,
|
|
chronicles
|
|
|
|
{.push gcsafe, raises:[CatchableError].}
|
|
|
|
func validateVersionedHashed(payload: ExecutionPayload,
|
|
expected: openArray[Hash32]): bool =
|
|
var versionedHashes: seq[VersionedHash]
|
|
for x in payload.transactions:
|
|
let tx = rlp.decode(distinctBase(x), Transaction)
|
|
versionedHashes.add tx.versionedHashes
|
|
|
|
if versionedHashes.len != expected.len:
|
|
return false
|
|
|
|
for i, x in expected:
|
|
if distinctBase(x) != versionedHashes[i].data:
|
|
return false
|
|
true
|
|
|
|
template validateVersion(com, timestamp, payloadVersion, apiVersion) =
|
|
if apiVersion == Version.V4:
|
|
if not com.isPragueOrLater(timestamp):
|
|
raise unsupportedFork("newPayloadV4 expect payload timestamp fall within Prague")
|
|
|
|
if com.isPragueOrLater(timestamp):
|
|
if payloadVersion != Version.V3:
|
|
raise invalidParams("if timestamp is Prague or later, " &
|
|
"payload must be ExecutionPayloadV3, got ExecutionPayload" & $payloadVersion)
|
|
|
|
if apiVersion == Version.V3:
|
|
if not com.isCancunOrLater(timestamp):
|
|
raise unsupportedFork("newPayloadV3 expect payload timestamp fall within Cancun")
|
|
|
|
if com.isCancunOrLater(timestamp):
|
|
if payloadVersion != Version.V3:
|
|
raise invalidParams("if timestamp is Cancun or later, " &
|
|
"payload must be ExecutionPayloadV3, got ExecutionPayload" & $payloadVersion)
|
|
|
|
elif com.isShanghaiOrLater(timestamp):
|
|
if payloadVersion != Version.V2:
|
|
raise invalidParams("if timestamp is Shanghai or later, " &
|
|
"payload must be ExecutionPayloadV2, got ExecutionPayload" & $payloadVersion)
|
|
|
|
elif payloadVersion != Version.V1:
|
|
raise invalidParams("if timestamp is earlier than Shanghai, " &
|
|
"payload must be ExecutionPayloadV1, got ExecutionPayload" & $payloadVersion)
|
|
|
|
if apiVersion == Version.V3 or apiVersion == Version.V4:
|
|
# both newPayloadV3 and newPayloadV4 expect ExecutionPayloadV3
|
|
if payloadVersion != Version.V3:
|
|
raise invalidParams("newPayload" & $apiVersion &
|
|
" expect ExecutionPayload3" &
|
|
" but got ExecutionPayload" & $payloadVersion)
|
|
|
|
template validatePayload(apiVersion, payloadVersion, payload) =
|
|
if payloadVersion >= Version.V2:
|
|
if payload.withdrawals.isNone:
|
|
raise invalidParams("newPayload" & $apiVersion &
|
|
"withdrawals is expected from execution payload")
|
|
|
|
if apiVersion >= Version.V3 or payloadVersion >= Version.V3:
|
|
if payload.blobGasUsed.isNone:
|
|
raise invalidParams("newPayload" & $apiVersion &
|
|
"blobGasUsed is expected from execution payload")
|
|
if payload.excessBlobGas.isNone:
|
|
raise invalidParams("newPayload" & $apiVersion &
|
|
"excessBlobGas is expected from execution payload")
|
|
|
|
proc newPayload*(ben: BeaconEngineRef,
|
|
apiVersion: Version,
|
|
payload: ExecutionPayload,
|
|
versionedHashes = Opt.none(seq[Hash32]),
|
|
beaconRoot = Opt.none(Hash32),
|
|
executionRequests = Opt.none(array[3, seq[byte]])): PayloadStatusV1 =
|
|
|
|
trace "Engine API request received",
|
|
meth = "newPayload",
|
|
number = payload.blockNumber,
|
|
hash = payload.blockHash
|
|
|
|
if apiVersion >= Version.V3:
|
|
if beaconRoot.isNone:
|
|
raise invalidParams("newPayloadV3 expect beaconRoot but got none")
|
|
|
|
if apiVersion >= Version.V4:
|
|
if executionRequests.isNone:
|
|
raise invalidParams("newPayload" & $apiVersion &
|
|
": executionRequests is expected from execution payload")
|
|
|
|
let
|
|
com = ben.com
|
|
db = com.db
|
|
timestamp = ethTime payload.timestamp
|
|
version = payload.version
|
|
requestsHash = calcRequestsHash(executionRequests)
|
|
|
|
validatePayload(apiVersion, version, payload)
|
|
validateVersion(com, timestamp, version, apiVersion)
|
|
|
|
var blk = ethBlock(payload, beaconRoot, requestsHash)
|
|
template header: Header = blk.header
|
|
|
|
if apiVersion >= Version.V3:
|
|
if versionedHashes.isNone:
|
|
raise invalidParams("newPayload" & $apiVersion &
|
|
" expect blobVersionedHashes but got none")
|
|
if not validateVersionedHashed(payload, versionedHashes.get):
|
|
return invalidStatus(header.parentHash, "invalid blob versionedHashes")
|
|
|
|
let blockHash = payload.blockHash
|
|
header.validateBlockHash(blockHash, version).isOkOr:
|
|
return error
|
|
|
|
# If we already have the block locally, ignore the entire execution and just
|
|
# return a fake success.
|
|
if ben.chain.haveBlockLocally(blockHash):
|
|
warn "Ignoring already known beacon payload",
|
|
number = header.number, hash = blockHash.short
|
|
return validStatus(blockHash)
|
|
|
|
# If this block was rejected previously, keep rejecting it
|
|
let res = ben.checkInvalidAncestor(blockHash, blockHash)
|
|
if res.isSome:
|
|
return res.get
|
|
|
|
# If the parent is missing, we - in theory - could trigger a sync, but that
|
|
# would also entail a reorg. That is problematic if multiple sibling blocks
|
|
# are being fed to us, and even moreso, if some semi-distant uncle shortens
|
|
# our live chain. As such, payload execution will not permit reorgs and thus
|
|
# will not trigger a sync cycle. That is fine though, if we get a fork choice
|
|
# update after legit payload executions.
|
|
let parent = ben.chain.headerByHash(header.parentHash).valueOr:
|
|
return ben.delayPayloadImport(header)
|
|
|
|
# We have an existing parent, do some sanity checks to avoid the beacon client
|
|
# triggering too early
|
|
let ttd = com.ttd.get(high(UInt256))
|
|
|
|
if version == Version.V1:
|
|
let ptd = db.getScore(header.parentHash).valueOr:
|
|
0.u256
|
|
let gptd = db.getScore(parent.parentHash)
|
|
if ptd < ttd:
|
|
warn "Ignoring pre-merge payload",
|
|
number = header.number, hash = blockHash.short, ptd, ttd
|
|
return invalidStatus()
|
|
if parent.difficulty > 0.u256 and gptd.isSome and gptd.value >= ttd:
|
|
warn "Ignoring pre-merge parent block",
|
|
number = header.number, hash = blockHash.short, ptd, ttd
|
|
return invalidStatus()
|
|
|
|
if header.timestamp <= parent.timestamp:
|
|
warn "Invalid timestamp",
|
|
number = header.number, parentNumber = parent.number,
|
|
parent = parent.timestamp, header = header.timestamp
|
|
return invalidStatus(parent.blockHash, "Invalid timestamp")
|
|
|
|
# Another corner case: if the node is in snap sync mode, but the CL client
|
|
# tries to make it import a block. That should be denied as pushing something
|
|
# into the database directly will conflict with the assumptions of snap sync
|
|
# that it has an empty db that it can fill itself.
|
|
when false:
|
|
if api.eth.SyncMode() != downloader.FullSync:
|
|
return api.delayPayloadImport(header)
|
|
|
|
if not ben.chain.haveBlockAndState(header.parentHash):
|
|
ben.put(blockHash, header)
|
|
warn "State not available, ignoring new payload",
|
|
hash = blockHash,
|
|
number = header.number
|
|
let blockHash = latestValidHash(db, parent, ttd)
|
|
return acceptedStatus(blockHash)
|
|
|
|
trace "Inserting block without sethead",
|
|
hash = blockHash, number = header.number
|
|
let vres = ben.chain.importBlock(blk)
|
|
if vres.isErr:
|
|
warn "Error importing block",
|
|
number = header.number,
|
|
hash = blockHash.short,
|
|
parent = header.parentHash.short,
|
|
error = vres.error()
|
|
ben.setInvalidAncestor(header, blockHash)
|
|
let blockHash = latestValidHash(db, parent, ttd)
|
|
return invalidStatus(blockHash, vres.error())
|
|
|
|
info "New payload received and validated",
|
|
number = header.number,
|
|
hash = blockHash.short,
|
|
parent = header.parentHash.short,
|
|
txs = blk.transactions.len,
|
|
gasUsed = header.gasUsed,
|
|
blobGas = header.blobGasUsed.get(0'u64)
|
|
|
|
return validStatus(blockHash)
|