reimplement engine API rpc kiln spec v2

This commit is contained in:
jangko 2022-02-22 15:55:04 +07:00
parent 2970fc4b02
commit f782327fcf
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
15 changed files with 939 additions and 135 deletions

View File

@ -119,6 +119,10 @@ proc getBlockHash*(self: BaseChainDB, n: BlockNumber): Hash256 {.inline.} =
if not self.getHash(blockNumberToHashKey(n), result):
raise newException(BlockNotFound, "No block hash for number " & $n)
proc getCurrentBlockHash*(self: BaseChainDB): Hash256 =
if not self.getHash(blockNumberToHashKey(self.currentBlock), result):
result = Hash256()
proc getBlockHeader*(self: BaseChainDB; n: BlockNumber, output: var BlockHeader): bool =
## Returns the block header with the given number in the canonical chain.
var blockHash: Hash256
@ -133,6 +137,15 @@ proc getBlockHeader*(self: BaseChainDB; n: BlockNumber): BlockHeader =
proc getScore*(self: BaseChainDB; blockHash: Hash256): Uint256 =
rlp.decode(self.db.get(blockHashToScoreKey(blockHash).toOpenArray), Uint256)
proc getTd*(self: BaseChainDB; blockHash: Hash256, td: var Uint256): bool =
let bytes = self.db.get(blockHashToScoreKey(blockHash).toOpenArray)
if bytes.len == 0: return false
try:
td = rlp.decode(bytes, Uint256)
except RlpError:
return false
return true
proc getAncestorsHashes*(self: BaseChainDB, limit: Uint256, header: BlockHeader): seq[Hash256] =
var ancestorCount = min(header.blockNumber, limit).truncate(int)
var h = header
@ -332,6 +345,28 @@ iterator getReceipts*(self: BaseChainDB; receiptRoot: Hash256): Receipt =
break
inc receiptIdx
proc readTerminalHash*(self: BaseChainDB; h: var Hash256): bool =
let bytes = self.db.get(terminalHashKey().toOpenArray)
if bytes.len == 0:
return false
try:
h = rlp.decode(bytes, Hash256)
except RlpError:
return false
true
proc writeTerminalHash*(self: BaseChainDB; h: Hash256) =
self.db.put(terminalHashKey().toOpenArray, rlp.encode(h))
proc currentTerminalHeader*(self: BaseChainDB; header: var BlockHeader): bool =
var terminalHash: Hash256
if not self.readTerminalHash(terminalHash):
return false
if not self.getBlockHeader(terminalHash, header):
return false
true
proc persistHeaderToDb*(self: BaseChainDB; header: BlockHeader): seq[BlockHeader] =
let isGenesis = header.parentHash == GENESIS_PARENT_HASH
let headerHash = header.blockHash
@ -352,10 +387,19 @@ proc persistHeaderToDb*(self: BaseChainDB; header: BlockHeader): seq[BlockHeader
except CanonicalHeadNotFound:
return self.setAsCanonicalChainHead(headerHash)
let ttd = self.ttd()
if headScore < ttd and score >= ttd:
self.writeTerminalHash(headerHash)
if score > headScore:
self.totalDifficulty = score
result = self.setAsCanonicalChainHead(headerHash)
proc persistHeaderToDbWithoutSetHead*(self: BaseChainDB; header: BlockHeader) =
let headerHash = header.blockHash
self.addBlockNumberToHashLookup(header)
self.db.put(genericHashKey(headerHash).toOpenArray, rlp.encode(header))
proc persistUncles*(self: BaseChainDB, uncles: openarray[BlockHeader]): Hash256 =
## Persists the list of uncles to the database.
## Returns the uncles hash.

View File

@ -11,6 +11,8 @@ type
slotHashToSlot
contractHash
cliqueSnapshot
transitionStatus
terminalHash
DbKey* = object
# The first byte stores the key type. The rest are key-specific values
@ -58,6 +60,15 @@ proc cliqueSnapshotKey*(h: Hash256): DbKey {.inline.} =
result.data[1 .. 32] = h.data
result.dataEndPos = uint8 32
proc transitionStatusKey*(): DbKey =
# ETH-2 Transition Status
result.data[0] = byte ord(transitionStatus)
result.dataEndPos = uint8 1
proc terminalHashKey*(): DbKey =
result.data[0] = byte ord(terminalHash)
result.dataEndPos = uint8 1
template toOpenArray*(k: DbKey): openarray[byte] =
k.data.toOpenArray(0, int(k.dataEndPos))

72
nimbus/merge/merger.nim Normal file
View File

@ -0,0 +1,72 @@
import
chronicles,
eth/[rlp, trie/db],
../db/[storage_types, db_chain]
type
# transitionStatus describes the status of eth1/2 transition. This switch
# between modes is a one-way action which is triggered by corresponding
# consensus-layer message.
TransitionStatus = object
leftPoW : bool # The flag is set when the first NewHead message received
enteredPoS: bool # The flag is set when the first FinalisedBlock message received
# Merger is an internal help structure used to track the eth1/2 transition status.
# It's a common structure can be used in both full node and light client.
Merger* = object
db : TrieDatabaseRef
status: TransitionStatus
proc write(db: TrieDatabaseRef, status: TransitionStatus) =
db.put(transitionStatusKey().toOpenArray(), rlp.encode(status))
proc read(db: TrieDatabaseRef, status: var TransitionStatus) =
var bytes = db.get(transitionStatusKey().toOpenArray())
if bytes.len > 0:
try:
status = rlp.decode(bytes, typeof status)
except:
error "Failed to decode transition status"
proc init*(m: var Merger, db: TrieDatabaseRef) =
m.db = db
db.read(m.status)
proc init*(m: var Merger, db: BaseChainDB) =
init(m, db.db)
proc initMerger*(db: BaseChainDB): Merger =
result.init(db)
proc initMerger*(db: TrieDatabaseRef): Merger =
result.init(db)
# ReachTTD is called whenever the first NewHead message received
# from the consensus-layer.
proc reachTTD*(m: var Merger) =
if m.status.leftPoW:
return
m.status = TransitionStatus(leftPoW: true)
m.db.write(m.status)
info "Left PoW stage"
# FinalizePoS is called whenever the first FinalisedBlock message received
# from the consensus-layer.
proc finalizePoS*(m: var Merger) =
if m.status.enteredPoS:
return
m.status = TransitionStatus(leftPoW: true, enteredPoS: true)
m.db.write(m.status)
info "Entered PoS stage"
# TTDReached reports whether the chain has left the PoW stage.
proc ttdReached*(m: Merger): bool =
m.status.leftPoW
# PoSFinalized reports whether the chain has entered the PoS stage.
proc posFinalized*(m: Merger): bool =
m.status.enteredPoS

View File

@ -0,0 +1,80 @@
import
web3/engine_api_types,
../db/db_chain,
./merger
import eth/common/eth_types except BlockHeader
export merger
type
EthBlockHeader* = eth_types.BlockHeader
const
# maxTrackedPayloads is the maximum number of prepared payloads the execution
# engine tracks before evicting old ones. Ideally we should only ever track the
# latest one; but have a slight wiggle room for non-ideal conditions.
MaxTrackedPayloads = 10
# maxTrackedHeaders is the maximum number of executed payloads the execution
# engine tracks before evicting old ones. Ideally we should only ever track the
# latest one; but have a slight wiggle room for non-ideal conditions.
MaxTrackedHeaders = 10
type
QueueItem[T] = object
used: bool
data: T
SimpleQueue[M: static[int]; T] = object
list: array[M, QueueItem[T]]
PayloadItem = object
id: PayloadId
payload: ExecutionPayloadV1
HeaderItem = object
hash: Hash256
header: EthBlockHeader
EngineAPI* = ref object
merger*: Merger
payloadQueue: SimpleQueue[MaxTrackedPayloads, PayloadItem]
headerQueue: SimpleQueue[MaxTrackedHeaders, HeaderItem]
template shiftRight[M, T](x: var SimpleQueue[M, T]) =
x.list[1..^1] = x.list[0..^2]
proc put[M, T](x: var SimpleQueue[M, T], val: T) =
x.shiftRight()
x.list[0] = QueueItem[T](used: true, data: val)
iterator items[M, T](x: SimpleQueue[M, T]): T =
for z in x.list:
if z.used:
yield z.data
proc new*(_: type EngineAPI, db: BaseChainDB): EngineAPI =
new result
if not db.isNil:
result.merger.init(db)
proc put*(api: EngineAPI, hash: Hash256, header: EthBlockHeader) =
api.headerQueue.put(HeaderItem(hash: hash, header: header))
proc get*(api: EngineAPI, hash: Hash256, header: var EthBlockHeader): bool =
for x in api.headerQueue:
if x.hash == hash:
header = x.header
return true
false
proc put*(api: EngineAPI, id: PayloadId, payload: ExecutionPayloadV1) =
api.payloadQueue.put(PayloadItem(id: id, payload: payload))
proc get*(api: EngineAPI, id: PayloadId, payload: var ExecutionPayloadV1): bool =
for x in api.payloadQueue:
if x.id == id:
payload = x.payload
return true
false

View File

@ -0,0 +1,96 @@
import
std/[typetraits, times, strutils],
nimcrypto/[hash, sha2],
web3/engine_api_types,
eth/[trie, rlp, common, trie/db],
stew/[objects, results, byteutils],
../constants,
./mergetypes
import eth/common/eth_types except BlockHeader
proc computePayloadId*(headBlockHash: Hash256, params: PayloadAttributesV1): PayloadID =
var dest: Hash256
var ctx: sha256
ctx.init()
ctx.update(headBlockHash.data)
ctx.update(toBytesBE distinctBase params.timestamp)
ctx.update(distinctBase params.prevRandao)
ctx.update(distinctBase params.suggestedFeeRecipient)
ctx.finish dest.data
ctx.clear()
(distinctBase result)[0..7] = dest.data[0..7]
template unsafeQuantityToInt64(q: Quantity): int64 =
int64 q
template asEthHash*(hash: engine_api_types.BlockHash): Hash256 =
Hash256(data: distinctBase(hash))
proc calcRootHashRlp*(items: openArray[seq[byte]]): Hash256 =
var tr = initHexaryTrie(newMemoryDB())
for i, t in items:
tr.put(rlp.encode(i), t)
return tr.rootHash()
proc toBlockHeader*(payload: ExecutionPayloadV1): eth_types.BlockHeader =
let transactions = seq[seq[byte]](payload.transactions)
let txRoot = calcRootHashRlp(transactions)
EthBlockHeader(
parentHash : payload.parentHash.asEthHash,
ommersHash : EMPTY_UNCLE_HASH,
coinbase : EthAddress payload.feeRecipient,
stateRoot : payload.stateRoot.asEthHash,
txRoot : txRoot,
receiptRoot : payload.receiptsRoot.asEthHash,
bloom : distinctBase(payload.logsBloom),
difficulty : default(DifficultyInt),
blockNumber : payload.blockNumber.distinctBase.u256,
gasLimit : payload.gasLimit.unsafeQuantityToInt64,
gasUsed : payload.gasUsed.unsafeQuantityToInt64,
timestamp : fromUnix payload.timestamp.unsafeQuantityToInt64,
extraData : bytes payload.extraData,
mixDigest : payload.prevRandao.asEthHash, # EIP-4399 redefine `mixDigest` -> `prevRandao`
nonce : default(BlockNonce),
fee : some payload.baseFeePerGas
)
template toHex*(x: Hash256): string =
toHex(x.data)
template validHash*(x: Hash256): Option[BlockHash] =
some(BlockHash(x.data))
proc validate*(header: eth_types.BlockHeader, gotHash: Hash256): Result[void, string] =
let wantHash = header.blockHash
if wantHash != gotHash:
return err("blockhash mismatch, want $1, got $2" % [wantHash.toHex, gotHash.toHex])
return ok()
proc simpleFCU*(status: PayloadExecutionStatus): ForkchoiceUpdatedResponse =
ForkchoiceUpdatedResponse(payloadStatus: PayloadStatusV1(status: status))
proc simpleFCU*(status: PayloadExecutionStatus, msg: string): ForkchoiceUpdatedResponse =
ForkchoiceUpdatedResponse(payloadStatus: PayloadStatusV1(status: status, validationError: some(msg)))
proc validFCU*(id: Option[PayloadId], validHash: Hash256): ForkchoiceUpdatedResponse =
ForkchoiceUpdatedResponse(
payloadStatus: PayloadStatusV1(
status: PayloadExecutionStatus.valid,
latestValidHash: some(BlockHash validHash.data)
),
payloadId: id
)
proc invalidStatus*(validHash: Hash256, msg: string): PayloadStatusV1 =
PayloadStatusV1(
status: PayloadExecutionStatus.invalid,
latestValidHash: some(BlockHash validHash.data),
validationError: some(msg)
)
proc toBlockBody*(payload: ExecutionPayloadV1): BlockBody =
# TODO the transactions from the payload have to be converted here
discard payload.transactions

View File

@ -206,18 +206,12 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
# TODO: There should be a better place to initialize this
nimbus.chainRef.clique.authorize(conf.engineSigner, signFunc)
let initialSealingEngineState =
if conf.networkParams.config.terminalTotalDifficulty.isSome and
conf.networkParams.config.terminalTotalDifficulty.get.isZero:
nimbus.chainRef.ttdReachedAt = some(BlockNumber.zero)
EnginePostMerge
else:
EngineStopped
var initialState = EngineStopped
if chainDB.totalDifficulty > chainDB.ttd:
initialState = EnginePostMerge
nimbus.sealingEngine = SealingEngineRef.new(
# TODO: Implement the initial state correctly
nimbus.chainRef, nimbus.ctx, conf.engineSigner,
nimbus.txPool, initialSealingEngineState
nimbus.txPool, initialState
)
nimbus.sealingEngine.start()

View File

@ -63,11 +63,6 @@ type
## For non-PoA networks (when `db.config.poaEngine` is `false`),
## this descriptor is ignored.
ttdReachedAt*: Option[BlockNumber]
## The first block which difficulty was above the terminal
## total difficulty. In networks with TTD=0, this would be
## the very first block.
{.push raises: [Defect].}
# ------------------------------------------------------------------------------
@ -81,9 +76,8 @@ func toNextFork(n: BlockNumber): uint64 =
result = n.truncate(uint64)
func isBlockAfterTtd*(c: Chain, blockHeader: BlockHeader): bool =
# TODO: This should be fork aware
c.ttdReachedAt.isSome and blockHeader.blockNumber > c.ttdReachedAt.get
c.db.totalDifficulty + blockHeader.difficulty > c.db.ttd
func getNextFork(c: ChainConfig, fork: ChainFork): uint64 =
let next: array[ChainFork, uint64] = [
0'u64,

View File

@ -72,8 +72,8 @@ proc persistBlocksImpl(c: Chain; headers: openarray[BlockHeader];
if validationResult != ValidationResult.OK:
return validationResult
if c.extraValidation and c.verifyFrom <= header.blockNumber:
if c.extraValidation and c.verifyFrom <= header.blockNumber:
let isBlockAfterTtd = c.isBlockAfterTtd(header)
if c.db.config.poaEngine and not isBlockAfterTtd:
var parent = if 0 < i: @[headers[i-1]] else: @[]

View File

@ -8,136 +8,278 @@
# those terms.
import
std/[typetraits, times],
stew/[objects, results],
std/[typetraits, times, strutils],
stew/[objects, results, byteutils],
json_rpc/[rpcserver, errors],
web3/[conversions, engine_api_types],
web3/[conversions, engine_api_types], chronicles,
eth/[trie, rlp, common, trie/db],
".."/db/db_chain,
".."/p2p/chain/[chain_desc, persist_blocks],
".."/[sealer, utils, constants]
".."/[sealer, utils, constants],
".."/merge/[mergetypes, mergeutils]
import eth/common/eth_types except BlockHeader
type EthBlockHeader = eth_types.BlockHeader
# TODO move this to stew/objects
template newClone*[T: not ref](x: T): ref T =
# TODO not nil in return type: https://github.com/nim-lang/Nim/issues/14146
# TODO use only when x is a function call that returns a new instance!
let res = new typeof(x) # TODO safe to do noinit here?
res[] = x
res
template asEthHash*(hash: engine_api_types.BlockHash): Hash256 =
Hash256(data: distinctBase(hash))
template unsafeQuantityToInt64(q: Quantity): int64 =
int64 q
proc calcRootHashRlp*(items: openArray[seq[byte]]): Hash256 =
var tr = initHexaryTrie(newMemoryDB())
for i, t in items:
tr.put(rlp.encode(i), t)
return tr.rootHash()
proc toBlockHeader(payload: ExecutionPayloadV1): eth_types.BlockHeader =
discard payload.prevRandao # TODO: What should this be used for?
let transactions = seq[seq[byte]](payload.transactions)
let txRoot = calcRootHashRlp(transactions)
EthBlockHeader(
parentHash : payload.parentHash.asEthHash,
ommersHash : EMPTY_UNCLE_HASH,
coinbase : EthAddress payload.feeRecipient,
stateRoot : payload.stateRoot.asEthHash,
txRoot : txRoot,
receiptRoot : payload.receiptsRoot.asEthHash,
bloom : distinctBase(payload.logsBloom),
difficulty : default(DifficultyInt),
blockNumber : payload.blockNumber.distinctBase.u256,
gasLimit : payload.gasLimit.unsafeQuantityToInt64,
gasUsed : payload.gasUsed.unsafeQuantityToInt64,
timestamp : fromUnix payload.timestamp.unsafeQuantityToInt64,
extraData : distinctBase payload.extraData,
mixDigest : default(Hash256),
nonce : default(BlockNonce),
fee : some payload.baseFeePerGas
)
proc toBlockBody(payload: ExecutionPayloadV1): BlockBody =
# TODO the transactions from the payload have to be converted here
discard payload.transactions
proc setupEngineAPI*(
sealingEngine: SealingEngineRef,
server: RpcServer) =
var payloadsInstance = newClone(newSeq[ExecutionPayloadV1]())
template payloads: auto = payloadsInstance[]
# TODO: put it somewhere else singleton
let api = EngineAPI.new(sealingEngine.chain.db)
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.5/src/engine/specification.md#engine_getpayloadv1
server.rpc("engine_getPayloadV1") do(payloadIdBytes: FixedBytes[8]) -> ExecutionPayloadV1:
let payloadId = uint64.fromBytesBE(distinctBase payloadIdBytes)
if payloadId > payloads.len.uint64:
raise (ref InvalidRequest)(code: engineApiUnknownPayload, msg: "Unknown payload")
return payloads[int payloadId]
# 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
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.5/src/engine/specification.md#engine_executepayloadv1
#[server.rpc("engine_executePayloadV1") do(payload: ExecutionPayloadV1) -> ExecutePayloadResponse:
# TODO
if payload.transactions.len > 0:
# Give us a break, a block with transcations? instructions to execute?
# Nah, we are syncing!
return ExecutePayloadResponse(status: PayloadExecutionStatus.syncing)
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))
# TODO check whether we are syncing
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
headers = [payload.toBlockHeader]
bodies = [payload.toBlockBody]
td = db.getScore(header.parentHash)
ttd = db.ttd()
if rlpHash(headers[0]) != payload.blockHash.asEthHash:
return ExecutePayloadResponse(status: PayloadExecutionStatus.invalid,
validationError: some "payload root doesn't match its contents")
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 sealingEngine.chain.persistBlocks(headers, bodies) != ValidationResult.OK:
# TODO Provide validationError and latestValidHash
return ExecutePayloadResponse(status: PayloadExecutionStatus.invalid)
if header.timestamp <= parent.timestamp:
warn "Invalid timestamp",
parent = header.timestamp, header = header.timestamp
return invalidStatus(db.getCurrentBlockHash(), "Invalid timestamp")
return ExecutePayloadResponse(status: PayloadExecutionStatus.valid,
latestValidHash: some payload.blockHash)
trace "Inserting block without sethead",
hash = blockHash.data.toHex, number = header.blockNumber
db.persistHeaderToDbWithoutSetHead(header)
# https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.5/src/engine/specification.md#engine_forkchoiceupdatedv1
# 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
newHead = update.headBlockHash.asEthHash
blockHash = update.headBlockHash.asEthHash
# TODO Use the finalized block information to prune any alterantive
# histories that are no longer relevant
discard update.finalizedBlockHash
if blockHash == Hash256():
warn "Forkchoice requested update to zero hash"
return simpleFCU(PayloadExecutionStatus.invalid)
# TODO Check whether we are syncing
# 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)
if not db.setHead(newHead):
return ForkchoiceUpdatedResponse(status: ForkchoiceUpdatedStatus.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:
let payloadId = uint64 payloads.len
info "Creating new payload for sealing"
let payloadAttrs = payloadAttributes.get()
var payload: ExecutionPayloadV1
let generatePayloadRes = sealingEngine.generateExecutionPayload(
payloadAttributes.get,
payload)
if generatePayloadRes.isErr:
raise newException(CatchableError, generatePayloadRes.error)
let res = sealingEngine.generateExecutionPayload(payloadAttrs, payload)
payloads.add payload
if res.isErr:
error "Failed to create sealing payload", err = res.error
return simpleFCU(PayloadExecutionStatus.invalid, res.error)
return ForkchoiceUpdatedResponse(status: ForkchoiceUpdatedStatus.success,
payloadId: some payloadId.toBytesBE.PayloadID)
else:
return ForkchoiceUpdatedResponse(status: ForkchoiceUpdatedStatus.success)]#
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)

View File

@ -10,18 +10,23 @@
import
std/[times, tables, typetraits],
pkg/[chronos, stew/results, chronicles, eth/common, eth/keys],
"."/[config, db/db_chain, p2p/chain, constants, utils/header],
pkg/[chronos, stew/results, chronicles, eth/common, eth/keys, eth/rlp],
"."/[config,
db/db_chain,
p2p/chain,
constants,
utils/header],
"."/p2p/clique/[clique_defs,
clique_desc,
clique_cfg,
clique_sealer],
./p2p/[gaslimit, validate],
"."/[chain_config, utils, context],
"."/utils/tx_pool
"."/utils/tx_pool,
"."/merge/mergetypes
from web3/ethtypes as web3types import nil
from web3/engine_api_types import ExecutionPayloadV1, PayloadAttributesV1
from web3/engine_api_types import PayloadAttributesV1, ExecutionPayloadV1
type
EngineState* = enum
@ -43,7 +48,7 @@ type
signer: EthAddress
txPool: TxPoolRef
template asEthHash*(hash: Web3BlockHash): Hash256 =
template asEthHash(hash: Web3BlockHash): Hash256 =
Hash256(data: distinctBase(hash))
proc validateSealer*(conf: NimbusConf, ctx: EthContext, chain: Chain): Result[void, string] =
@ -135,8 +140,11 @@ proc generateBlock(engine: SealingEngineRef,
header: res.get()
)
if not engine.chain.isBlockAfterTtd(outBlock.header):
# TODO Post merge, Clique should not be executing
if engine.chain.isBlockAfterTtd(outBlock.header):
# Stop the block generator if we reach TTD
engine.state = EnginePostMerge
else:
# Post merge, Clique should not be executing
let sealRes = engine.chain.clique.seal(outBlock)
if sealRes.isErr:
return err("error sealing block header: " & $sealRes.error)
@ -209,6 +217,10 @@ proc sealingLoop(engine: SealingEngineRef): Future[void] {.async.} =
error "sealing engine generateBlock error", msg=blkRes.error
break
# if TTD reached during block generation, stop the sealer
if engine.state != EngineRunning:
break
let res = engine.chain.persistBlocks([blk.header], [
BlockBody(transactions: blk.txs, uncles: blk.uncles)
])
@ -243,24 +255,26 @@ proc generateExecutionPayload*(engine: SealingEngineRef,
BlockBody(transactions: blk.txs, uncles: blk.uncles)
])
if blk.header.extraData.len > 32:
return err "extraData length should not exceed 32 bytes"
payloadRes.parentHash = Web3BlockHash blk.header.parentHash.data
payloadRes.feeRecipient = Web3Address blk.header.coinbase
payloadRes.stateRoot = Web3BlockHash blk.header.stateRoot.data
payloadRes.receiptsRoot = Web3BlockHash blk.header.receiptRoot.data
payloadRes.logsBloom = Web3Bloom blk.header.bloom
# TODO Check the extra data length here
# payloadres.extraData = web3types.DynamicBytes[256] blk.header.extraData
payloadRes.prevRandao = web3types.FixedBytes[32](payloadAttrs.prevRandao)
payloadRes.blockNumber = Web3Quantity blk.header.blockNumber.truncate(uint64)
payloadRes.gasLimit = Web3Quantity blk.header.gasLimit
payloadRes.gasUsed = Web3Quantity blk.header.gasUsed
payloadRes.timestamp = payloadAttrs.timestamp
# TODO
# res.extraData
payloadres.extraData = web3types.DynamicBytes[0, 32] blk.header.extraData
payloadRes.baseFeePerGas = blk.header.fee.get(UInt256.zero)
payloadRes.blockHash = Web3BlockHash rlpHash(blk.header).data
# TODO
# res.transactions*: seq[TypedTransaction]
for tx in blk.txs:
let txData = rlp.encode(tx)
payloadRes.transactions.add web3types.TypedTransaction(txData)
return ok()

View File

@ -48,7 +48,7 @@ proc getMinerAddress(chainDB: BaseChainDB; header: BlockHeader, ttdReached: bool
{.gcsafe, raises: [Defect,CatchableError].} =
if not chainDB.config.poaEngine or ttdReached:
return header.coinbase
let account = header.ecRecover
if account.isErr:
let msg = "Could not recover account address: " & $account.error

View File

@ -43,4 +43,6 @@ cliBuilder:
./test_pow,
./test_configuration,
./test_keyed_queue_rlp,
./test_txpool
./test_txpool,
./test_merge

37
tests/merge/params.json Normal file
View File

@ -0,0 +1,37 @@
{
"config": {
"chainId":1,
"homesteadBlock":0,
"eip150Block":0,
"eip155Block":0,
"eip158Block":0,
"byzantiumBlock":0,
"constantinopleBlock":0,
"petersburgBlock":0,
"istanbulBlock":0,
"muirGlacierBlock":0,
"berlinBlock":0,
"londonBlock":0,
"clique": {
"period": 5,
"epoch": 30000
},
"terminalTotalDifficulty":0
},
"genesis": {
"nonce":"0x42",
"timestamp":"0x0",
"extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"gasLimit":"0x1C9C380",
"difficulty":"0x400000000",
"mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase":"0x0000000000000000000000000000000000000000",
"alloc":{
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b":{"balance":"0x6d6172697573766477000000"}
},
"number":"0x0",
"gasUsed":"0x0",
"parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
"baseFeePerGas":"0x7"
}
}

142
tests/merge/steps.json Normal file
View File

@ -0,0 +1,142 @@
[
{
"name": "Prepare a payload",
"method":"engine_forkchoiceUpdatedV1",
"params":[
{
"headBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"safeBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"finalizedBlockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"
},
{
"timestamp":"0x5",
"prevRandao":"0x0000000000000000000000000000000000000000000000000000000000000000",
"suggestedFeeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"
}
],
"expect": {
"payloadStatus":{
"status":"VALID",
"latestValidHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"validationError":null
},
"payloadId":"0xa247243752eb10b4"
}
},
{
"name": "Get the payload",
"method":"engine_getPayloadV1",
"params":[
"0xa247243752eb10b4"
],
"expect": {
"parentHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"feeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45",
"receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"prevRandao":"0x0000000000000000000000000000000000000000000000000000000000000000",
"blockNumber":"0x1",
"gasLimit":"0x1c95111",
"gasUsed":"0x0",
"timestamp":"0x5",
"extraData":"0x64616f2d686172642d666f726b",
"baseFeePerGas":"0x7",
"blockHash":"0x29671a05d0e18797905296bba15941c96edefc2aefe2240253cd33cf3eda80c0",
"transactions":[]
}
},
{
"name": "Execute the payload",
"method":"engine_newPayloadV1",
"params":[
{
"parentHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"feeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45",
"receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"prevRandao":"0x0000000000000000000000000000000000000000000000000000000000000000",
"blockNumber":"0x1",
"gasLimit":"0x1c9c380",
"gasUsed":"0x0",
"timestamp":"0x5",
"extraData":"0x",
"baseFeePerGas":"0x7",
"blockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"transactions":[]
}
],
"expect": {
"status":"VALID",
"latestValidHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"validationError":null
}
},
{
"name": "Update the forkchoice",
"method":"engine_forkchoiceUpdatedV1",
"params":[
{
"headBlockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"safeBlockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"finalizedBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a"
},
null
],
"expect":{
"payloadStatus":{
"status":"VALID",
"latestValidHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"validationError":null
},
"payloadId":null
}
},
{
"name": "Invalid payload length",
"method":"engine_getPayloadV1",
"params":[
"0x01"
],
"error":{
"code":-32602,
"message":"invalid argument 0: invalid payload id \"0x01\": hex string has length 2, want 16 for PayloadID"
}
},
{
"name": "Unknown paylod",
"method":"engine_getPayloadV1",
"params":[
"0x0000000000000000"
],
"error":{
"code":-32001,
"message":"Unknown payload"
}
},
{
"name": "Invalid head",
"method":"engine_forkchoiceUpdatedV1",
"params":[
{
"headBlockHash":"0x0000000000000000000000000000000000000000000000000000000000000001",
"safeBlockHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"finalizedBlockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"
},
{
"timestamp":"0x5",
"prevRandao":"0x0000000000000000000000000000000000000000000000000000000000000000",
"suggestedFeeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"
}
],
"expect":{
"payloadStatus":{
"status":"SYNCING",
"latestValidHash":null,
"validationError":null
},
"payloadId":null
}
}
]

176
tests/test_merge.nim Normal file
View File

@ -0,0 +1,176 @@
# 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/[json, os, sets, strformat, strutils, typetraits],
unittest2, nimcrypto, eth/common as eth_common,
json_rpc/[rpcserver, rpcclient], web3/[conversions, engine_api_types],
eth/[trie/db, p2p/private/p2p_types],
../nimbus/sync/protocol_eth65,
../nimbus/rpc/[common, p2p, hexstrings, rpc_types, rpc_utils, engine_api],
../nimbus/db/[db_chain],
../nimbus/[chain_config, config, context, genesis, sealer],
../nimbus/utils/[tx_pool],
../nimbus/p2p/[chain],
../nimbus/merge/mergetypes,
./test_helpers
const
baseDir = "tests" / "merge"
paramsFile = baseDir / "params.json"
stepsFile = baseDir / "steps.json"
type
Step = ref object
name: string
meth: string
params: JSonNode
expect: JsonNode
error : bool
Steps = ref object
list: seq[Step]
proc parseStep(s: Step, node: JsonNode) =
for k, v in node:
case k
of "name": s.name = v.getStr()
of "method": s.meth = v.getStr()
of "params": s.params = v
of "expect": s.expect = v
of "error": s.error = true
else:
doAssert(false, "unknown key: " & k)
proc parseSteps(node: JsonNode): Steps =
let ss = Steps(list: @[])
for n in node:
let s = Step()
s.parseStep(n)
ss.list.add s
ss
proc forkChoiceUpdate(step: Step, client: RpcClient, testStatusIMPL: var TestStatus) =
let arg = step.params[1]
if arg.kind == JNull:
step.params.elems.setLen(1)
let res = waitFor client.call(step.meth, step.params)
check toLowerAscii($res) == toLowerAscii($step.expect)
proc getPayload(step: Step, client: RpcClient, testStatusIMPL: var TestStatus) =
try:
let res = waitFor client.call(step.meth, step.params)
check toLowerAscii($res) == toLowerAscii($step.expect)
except:
check step.error == true
proc newPayload(step: Step, client: RpcClient, testStatusIMPL: var TestStatus) =
let res = waitFor client.call(step.meth, step.params)
check toLowerAscii($res) == toLowerAscii($step.expect)
proc runTest(steps: Steps) =
let
conf = makeConfig(@["--custom-network:" & paramsFile])
ctx = newEthContext()
ethNode = setupEthNode(conf, ctx, eth)
chainDB = newBaseChainDB(
newMemoryDb(),
conf.pruneMode == PruneMode.Full,
conf.networkId,
conf.networkParams
)
chainRef = newChain(chainDB)
initializeEmptyDb(chainDB)
var
rpcServer = newRpcSocketServer(["localhost:" & $conf.rpcPort])
client = newRpcSocketClient()
txPool = TxPoolRef.new(chainDB, conf.engineSigner)
sealingEngine = SealingEngineRef.new(
chainRef, ctx, conf.engineSigner,
txPool, EnginePostMerge
)
setupEthRpc(ethNode, ctx, chainDB, txPool, rpcServer)
setupEngineAPI(sealingEngine, rpcServer)
sealingEngine.start()
rpcServer.start()
waitFor client.connect("localhost", Port(conf.rpcPort))
suite "Engine API tests":
for i, step in steps.list:
test $i & " " & step.name:
case step.meth
of "engine_forkchoiceUpdatedV1":
forkChoiceUpdate(step, client, testStatusIMPL)
of "engine_getPayloadV1":
getPayload(step, client, testStatusIMPL)
of "engine_newPayloadV1":
newPayload(step, client, testStatusIMPL)
else:
doAssert(false, "unknown method: " & step.meth)
waitFor client.close()
waitFor sealingEngine.stop()
rpcServer.stop()
waitFor rpcServer.closeWait()
proc testEngineAPI() =
let node = parseJSON(readFile(stepsFile))
let steps = parseSteps(node)
runTest(steps)
proc toId(x: int): PayloadId =
var id: distinctBase PayloadId
id[^1] = x.byte
PayloadId(id)
proc `==`(a, b: Quantity): bool =
uint64(a) == uint64(b)
proc main() =
var db = newBaseChainDB(newMemoryDB())
var api = EngineAPI.new()
let
id1 = toId(1)
id2 = toId(2)
ep1 = ExecutionPayloadV1(gasLimit: Quantity 100)
ep2 = ExecutionPayloadV1(gasLimit: Quantity 101)
hdr1 = EthBlockHeader(gasLimit: 100)
hdr2 = EthBlockHeader(gasLimit: 101)
hash1 = hdr1.blockHash
hash2 = hdr2.blockHash
suite "Test engine api support":
test "test payload queue":
api.put(id1, ep1)
api.put(id2, ep2)
var eep1, eep2: ExecutionPayloadV1
check api.get(id1, eep1)
check api.get(id2, eep2)
check eep1.gasLimit == ep1.gasLimit
check eep2.gasLimit == ep2.gasLimit
test "test header queue":
api.put(hash1, hdr1)
api.put(hash2, hdr2)
var eh1, eh2: EthBlockHeader
check api.get(hash1, eh1)
check api.get(hash2, eh2)
check eh1.gasLimit == hdr1.gasLimit
check eh2.gasLimit == hdr2.gasLimit
proc mergeMain*() =
testEngineAPI()
when isMainModule:
mergeMain()