# 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)