289 lines
13 KiB
Nim
289 lines
13 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2018 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, times, strutils],
|
|
stew/[objects, results, byteutils],
|
|
json_rpc/[rpcserver, errors],
|
|
web3/[conversions, engine_api_types], chronicles,
|
|
eth/[trie, rlp, common, trie/db],
|
|
".."/db/db_chain,
|
|
".."/p2p/chain/[chain_desc, persist_blocks],
|
|
".."/[sealer, utils, constants],
|
|
".."/merge/[mergetypes, mergeutils]
|
|
|
|
import eth/common/eth_types except BlockHeader
|
|
|
|
proc setupEngineAPI*(
|
|
sealingEngine: SealingEngineRef,
|
|
server: RpcServer) =
|
|
|
|
# TODO: put it somewhere else singleton
|
|
let api = EngineAPI.new(sealingEngine.chain.db)
|
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.7/src/engine/specification.md#engine_newpayloadv1
|
|
# cannot use `params` as param name. see https:#github.com/status-im/nim-json-rpc/issues/128
|
|
server.rpc("engine_newPayloadV1") do(payload: ExecutionPayloadV1) -> PayloadStatusV1:
|
|
trace "Engine API request received",
|
|
meth = "newPayloadV1", number = $(distinctBase payload.blockNumber), hash = payload.blockHash.toHex
|
|
|
|
var header = toBlockHeader(payload)
|
|
let blockHash = payload.blockHash.asEthHash
|
|
var res = header.validate(blockHash)
|
|
if res.isErr:
|
|
return PayloadStatusV1(status: PayloadExecutionStatus.invalid_block_hash, validationError: some(res.error))
|
|
|
|
let db = sealingEngine.chain.db
|
|
|
|
# If we already have the block locally, ignore the entire execution and just
|
|
# return a fake success.
|
|
if db.getBlockHeader(blockHash, header):
|
|
warn "Ignoring already known beacon payload",
|
|
number = header.blockNumber, hash = blockHash.data.toHex
|
|
return PayloadStatusV1(status: PayloadExecutionStatus.valid, latestValidHash: validHash(blockHash))
|
|
|
|
# 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.
|
|
var parent: eth_types.BlockHeader
|
|
if not db.getBlockHeader(header.parentHash, parent):
|
|
# Stash the block away for a potential forced forckchoice update to it
|
|
# at a later time.
|
|
api.put(blockHash, header)
|
|
|
|
# Although we don't want to trigger a sync, if there is one already in
|
|
# progress, try to extend if with the current payload request to relieve
|
|
# some strain from the forkchoice update.
|
|
#if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil {
|
|
# log.Debug("Payload accepted for sync extension", "number", params.Number, "hash", params.BlockHash)
|
|
# return beacon.PayloadStatusV1{Status: beacon.SYNCING}, nil
|
|
|
|
# Either no beacon sync was started yet, or it rejected the delivered
|
|
# payload as non-integratable on top of the existing sync. We'll just
|
|
# have to rely on the beacon client to forcefully update the head with
|
|
# a forkchoice update request.
|
|
warn "Ignoring payload with missing parent",
|
|
number = header.blockNumber, hash = blockHash.data.toHex, parent = header.parentHash.data.toHex
|
|
return PayloadStatusV1(status: PayloadExecutionStatus.accepted)
|
|
|
|
# We have an existing parent, do some sanity checks to avoid the beacon client
|
|
# triggering too early
|
|
let
|
|
td = db.getScore(header.parentHash)
|
|
ttd = db.ttd()
|
|
|
|
if td < ttd:
|
|
warn "Ignoring pre-merge payload",
|
|
number = header.blockNumber, hash = blockHash.data.toHex, td, ttd
|
|
return PayloadStatusV1(status: PayloadExecutionStatus.invalid_terminal_block)
|
|
|
|
if header.timestamp <= parent.timestamp:
|
|
warn "Invalid timestamp",
|
|
parent = header.timestamp, header = header.timestamp
|
|
return invalidStatus(db.getCurrentBlockHash(), "Invalid timestamp")
|
|
|
|
trace "Inserting block without sethead",
|
|
hash = blockHash.data.toHex, number = header.blockNumber
|
|
let body = toBlockBody(payload)
|
|
let vres = sealingEngine.chain.insertBlockWithoutSetHead(header, body)
|
|
if vres != ValidationResult.OK:
|
|
return invalidStatus(db.getCurrentBlockHash(), "Failed to insert block")
|
|
|
|
# We've accepted a valid payload from the beacon client. Mark the local
|
|
# chain transitions to notify other subsystems (e.g. downloader) of the
|
|
# behavioral change.
|
|
if not api.merger.ttdReached():
|
|
api.merger.reachTTD()
|
|
# TODO: cancel downloader
|
|
|
|
return PayloadStatusV1(status: PayloadExecutionStatus.valid, latestValidHash: validHash(blockHash))
|
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.7/src/engine/specification.md#engine_getpayloadv1
|
|
server.rpc("engine_getPayloadV1") do(payloadId: PayloadID) -> ExecutionPayloadV1:
|
|
trace "Engine API request received",
|
|
meth = "GetPayload", id = payloadId.toHex
|
|
|
|
var payload: ExecutionPayloadV1
|
|
if not api.get(payloadId, payload):
|
|
raise (ref InvalidRequest)(code: engineApiUnknownPayload, msg: "Unknown payload")
|
|
#raise newException(ValueError, "Unknown payload")
|
|
|
|
return payload
|
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.7/src/engine/specification.md#engine_exchangeTransitionConfigurationV1
|
|
server.rpc("engine_exchangeTransitionConfigurationV1") do(conf: TransitionConfigurationV1) -> TransitionConfigurationV1:
|
|
trace "Engine API request received",
|
|
meth = "exchangeTransitionConfigurationV1",
|
|
ttd = conf.terminalTotalDifficulty,
|
|
number = uint64(conf.terminalBlockNumber),
|
|
blockHash = conf.terminalBlockHash.toHex
|
|
|
|
let db = sealingEngine.chain.db
|
|
let ttd = db.ttd()
|
|
|
|
if conf.terminalTotalDifficulty != ttd:
|
|
raise newException(ValueError, "invalid ttd: EL $1 CL $2" % [$ttd, $conf.terminalTotalDifficulty])
|
|
|
|
var header: EthBlockHeader
|
|
let terminalBlockNumber = uint64(conf.terminalBlockNumber)
|
|
let terminalBlockHash = conf.terminalBlockHash.asEthHash
|
|
if db.currentTerminalHeader(header):
|
|
let headerHash = header.blockHash
|
|
|
|
if terminalBlockNumber != 0'u64 and terminalBlockNumber != header.blockNumber.truncate(uint64):
|
|
raise newException(ValueError, "invalid terminal block number, got $1 want $2" % [$terminalBlockNumber, $header.blockNumber])
|
|
|
|
if terminalBlockHash != Hash256() and terminalBlockHash != headerHash:
|
|
raise newException(ValueError, "invalid terminal block hash, got $1 want $2" % [terminalBlockHash.toHex, headerHash.data.toHex])
|
|
|
|
return TransitionConfigurationV1(
|
|
terminalTotalDifficulty: ttd,
|
|
terminalBlockHash : BlockHash headerHash.data,
|
|
terminalBlockNumber : Quantity header.blockNumber.truncate(uint64)
|
|
)
|
|
|
|
if terminalBlockNumber != 0:
|
|
raise newException(ValueError, "invalid terminal block number: $1" % [$terminalBlockNumber])
|
|
|
|
if terminalBlockHash != Hash256():
|
|
raise newException(ValueError, "invalid terminal block hash, no terminal header set")
|
|
|
|
return TransitionConfigurationV1(terminalTotalDifficulty: ttd)
|
|
|
|
# ForkchoiceUpdatedV1 has several responsibilities:
|
|
# If the method is called with an empty head block:
|
|
# we return success, which can be used to check if the catalyst mode is enabled
|
|
# If the total difficulty was not reached:
|
|
# we return INVALID
|
|
# If the finalizedBlockHash is set:
|
|
# we check if we have the finalizedBlockHash in our db, if not we start a sync
|
|
# We try to set our blockchain to the headBlock
|
|
# If there are payloadAttributes:
|
|
# we try to assemble a block with the payloadAttributes and return its payloadID
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.7/src/engine/specification.md#engine_forkchoiceupdatedv1
|
|
server.rpc("engine_forkchoiceUpdatedV1") do(
|
|
update: ForkchoiceStateV1,
|
|
payloadAttributes: Option[PayloadAttributesV1]) -> ForkchoiceUpdatedResponse:
|
|
let
|
|
db = sealingEngine.chain.db
|
|
blockHash = update.headBlockHash.asEthHash
|
|
|
|
if blockHash == 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: EthBlockHeader
|
|
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 api.get(blockHash, header):
|
|
warn "Forkchoice requested unknown head",
|
|
hash = blockHash.data.toHex
|
|
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 api.merger.ttdReached():
|
|
api.merger.reachTTD()
|
|
# TODO: cancel downloader
|
|
|
|
info "Forkchoice requested sync to new head",
|
|
number = header.blockNumber,
|
|
hash = blockHash.data.toHex
|
|
|
|
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 = db.ttd()
|
|
|
|
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.data.toHex,
|
|
td = td,
|
|
parent = header.parentHash.data.toHex,
|
|
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.data.toHex,
|
|
diff = header.difficulty
|
|
|
|
return simpleFCU(PayloadExecutionStatus.invalid_terminal_block)
|
|
|
|
# If the head block is already in our canonical chain, the beacon client is
|
|
# probably resyncing. Ignore the update.
|
|
var canonHash: 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
|
|
warn "Reorg to previous block"
|
|
if not db.setHead(blockHash):
|
|
return simpleFCU(PayloadExecutionStatus.invalid)
|
|
elif not db.setHead(blockHash):
|
|
return simpleFCU(PayloadExecutionStatus.invalid)
|
|
|
|
# If the beacon client also advertised a finalized block, mark the local
|
|
# chain final and completely in PoS mode.
|
|
let finalizedBlockHash = update.finalizedBlockHash.asEthHash
|
|
if finalizedBlockHash != Hash256():
|
|
if not api.merger.posFinalized:
|
|
api.merger.finalizePoS()
|
|
|
|
# TODO: If the finalized block is not in our canonical tree, somethings wrong
|
|
var finalBlock: EthBlockHeader
|
|
if not db.getBlockHeader(finalizedBlockHash, finalBlock):
|
|
warn "Final block not available in database",
|
|
hash = finalizedBlockHash.data.toHex
|
|
return simpleFCU(PayloadExecutionStatus.invalid, "final block not available")
|
|
elif not db.getBlockHash(finalBlock.blockNumber, canonHash) or canonHash != finalizedBlockHash:
|
|
warn "Final block not in canonical chain",
|
|
number = finalBlock.blockNumber,
|
|
hash = finalizedBlockHash.data.toHex
|
|
return simpleFCU(PayloadExecutionStatus.invalid, "final block not canonical")
|
|
|
|
# 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 payloadAttributes.isSome:
|
|
info "Creating new payload for sealing"
|
|
let payloadAttrs = payloadAttributes.get()
|
|
var payload: ExecutionPayloadV1
|
|
let res = sealingEngine.generateExecutionPayload(payloadAttrs, payload)
|
|
|
|
if res.isErr:
|
|
error "Failed to create sealing payload", err = res.error
|
|
return simpleFCU(PayloadExecutionStatus.invalid, res.error)
|
|
|
|
let id = computePayloadId(blockHash, payloadAttrs)
|
|
api.put(id, payload)
|
|
|
|
info "Created payload for sealing",
|
|
id = id.toHex
|
|
|
|
return validFCU(some(id), blockHash)
|
|
|
|
return validFCU(none(PayloadId), blockHash)
|