json-rpc: able to query finalized block and safe block header

engine-api:
- store safe block hash and finalized block hash

engine-api test:
- fix test case related to safe block hash and finalized block hash
This commit is contained in:
jangko 2022-06-27 11:15:23 +07:00
parent cad74db423
commit b80eca0718
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
7 changed files with 155 additions and 111 deletions

View File

@ -7,7 +7,7 @@ import
web3/engine_api_types, web3/engine_api_types,
json_rpc/rpcclient, json_rpc/rpcclient,
../../../nimbus/merge/mergeutils, ../../../nimbus/merge/mergeutils,
../../../nimbus/debug, ../../../nimbus/[debug, constants],
./engine_client ./engine_client
# Consensus Layer Client Mock used to sync the Execution Clients once the TTD has been reached # Consensus Layer Client Mock used to sync the Execution Clients once the TTD has been reached
@ -34,6 +34,10 @@ type
client : RpcClient client : RpcClient
ttd : DifficultyInt ttd : DifficultyInt
slotsToSafe : int
slotsToFinalized : int
headHashHistory : seq[BlockHash]
BlockProcessCallbacks* = object BlockProcessCallbacks* = object
onPayloadProducerSelected* : proc(): bool {.gcsafe.} onPayloadProducerSelected* : proc(): bool {.gcsafe.}
onGetPayloadID* : proc(): bool {.gcsafe.} onGetPayloadID* : proc(): bool {.gcsafe.}
@ -47,6 +51,8 @@ type
proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) = proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) =
cl.client = client cl.client = client
cl.ttd = ttd cl.ttd = ttd
cl.slotsToSafe = 1
cl.slotsToFinalized = 2
proc newClMocker*(client: RpcClient, ttd: DifficultyInt): CLMocker = proc newClMocker*(client: RpcClient, ttd: DifficultyInt): CLMocker =
new result new result
@ -63,8 +69,13 @@ proc waitForTTD*(cl: CLMocker): Future[bool] {.async.} =
let headerHash = BlockHash(common.blockHash(cl.latestHeader).data) let headerHash = BlockHash(common.blockHash(cl.latestHeader).data)
cl.latestForkchoice.headBlockHash = headerHash cl.latestForkchoice.headBlockHash = headerHash
if cl.slotsToSafe == 0:
cl.latestForkchoice.safeBlockHash = headerHash cl.latestForkchoice.safeBlockHash = headerHash
if cl.slotsToFinalized == 0:
cl.latestForkchoice.finalizedBlockHash = headerHash cl.latestForkchoice.finalizedBlockHash = headerHash
cl.latestHeadNumber = cl.latestHeader.blockNumber.truncate(uint64) cl.latestHeadNumber = cl.latestHeader.blockNumber.truncate(uint64)
let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice) let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice)
@ -256,8 +267,19 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe
return false return false
# Broadcast forkchoice updated with new HeadBlock to all clients # Broadcast forkchoice updated with new HeadBlock to all clients
let blockHash = cl.latestPayloadBuilt.blockHash let previousForkchoice = cl.latestForkchoice
cl.latestForkchoice.headBlockHash = blockHash cl.headHashHistory.add cl.latestPayloadBuilt.blockHash
cl.latestForkchoice = ForkchoiceStateV1()
cl.latestForkchoice.headBlockHash = cl.latestPayloadBuilt.blockHash
let hhLen = cl.headHashHistory.len
if hhLen > cl.slotsToSafe:
cl.latestForkchoice.safeBlockHash = cl.headHashHistory[hhLen - cl.slotsToSafe - 1]
if hhLen > cl.slotsToFinalized:
cl.latestForkchoice.finalizedBlockHash = cl.headHashHistory[hhLen - cl.slotsToFinalized - 1]
if not cl.broadcastLatestForkchoice(): if not cl.broadcastLatestForkchoice():
return false return false
@ -266,19 +288,16 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe
return false return false
# Broadcast forkchoice updated with new SafeBlock to all clients # Broadcast forkchoice updated with new SafeBlock to all clients
cl.latestForkchoice.safeBlockHash = blockHash if cb.onSafeBlockChange != nil and cl.latestForkchoice.safeBlockHash != previousForkchoice.safeBlockHash:
if not cl.broadcastLatestForkchoice():
return false
if cb.onSafeBlockChange != nil:
if not cb.onSafeBlockChange(): if not cb.onSafeBlockChange():
return false return false
# Broadcast forkchoice updated with new FinalizedBlock to all clients # Broadcast forkchoice updated with new FinalizedBlock to all clients
cl.latestForkchoice.finalizedBlockHash = blockHash if cb.onFinalizedBlockChange != nil and cl.latestForkchoice.finalizedBlockHash != previousForkchoice.finalizedBlockHash:
if not cl.broadcastLatestForkchoice(): if not cb.onFinalizedBlockChange():
return false return false
# Broadcast forkchoice updated with new FinalizedBlock to all clients
# Save the number of the first PoS block # Save the number of the first PoS block
if cl.firstPoSBlockNumber.isNone: if cl.firstPoSBlockNumber.isNone:
let number = cl.latestHeader.blockNumber.truncate(uint64) + 1 let number = cl.latestHeader.blockNumber.truncate(uint64) + 1
@ -300,12 +319,37 @@ proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe
hash=newHash.toHex hash=newHash.toHex
return false return false
cl.latestHeader = newHeader # Check that the new finalized header has the correct properties
# ommersHash == 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
if cb.onFinalizedBlockChange != nil: if newHeader.ommersHash != EMPTY_UNCLE_HASH:
if not cb.onFinalizedBlockChange(): error "CLMocker: Client produced a new header with incorrect ommersHash", ommersHash = newHeader.ommersHash
return false return false
# difficulty == 0
if newHeader.difficulty != 0.u256:
error "CLMocker: Client produced a new header with incorrect difficulty", difficulty = newHeader.difficulty
return false
# mixHash == prevRandao
if newHeader.mixDigest != cl.prevRandaoHistory[cl.latestHeadNumber]:
error "CLMocker: Client produced a new header with incorrect mixHash",
get = newHeader.mixDigest.data.toHex,
expect = cl.prevRandaoHistory[cl.latestHeadNumber].data.toHex
return false
# nonce == 0x0000000000000000
if newHeader.nonce != default(BlockNonce):
error "CLMocker: Client produced a new header with incorrect nonce",
nonce = newHeader.nonce.toHex
return false
if newHeader.extraData.len > 32:
error "CLMocker: Client produced a new header with incorrect extraData (len > 32)",
len = newHeader.extraData.len
return false
cl.latestHeader = newHeader
return true return true
# Loop produce PoS blocks by using the Engine API # Loop produce PoS blocks by using the Engine API

View File

@ -158,6 +158,16 @@ proc latestBlock*(client: RpcClient, output: var common.EthBlock): Result[void,
except ValueError as e: except ValueError as e:
return err(e.msg) return err(e.msg)
proc namedHeader*(client: RpcClient, name: string, output: var common.BlockHeader): Result[void, string] =
try:
let res = waitFor client.eth_getBlockByNumber(name, false)
if res.isNone:
return err("failed to get named blockHeader")
output = toBlockHeader(res.get())
return ok()
except ValueError as e:
return err(e.msg)
proc sendTransaction*(client: RpcClient, tx: common.Transaction): Result[void, string] = proc sendTransaction*(client: RpcClient, tx: common.Transaction): Result[void, string] =
try: try:
let encodedTx = rlp.encode(tx) let encodedTx = rlp.encode(tx)

View File

@ -929,101 +929,67 @@ template blockStatusHeadBlockGen(procname: untyped, transitionBlock: bool) =
blockStatusHeadBlockGen(blockStatusHeadBlock1, false) blockStatusHeadBlockGen(blockStatusHeadBlock1, false)
blockStatusHeadBlockGen(blockStatusHeadBlock2, true) blockStatusHeadBlockGen(blockStatusHeadBlock2, true)
template blockStatusSafeBlockGen(procname: untyped, transitionBlock: bool) = proc blockStatusSafeBlock(t: TestEnv): TestStatus =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test, only if we are not testing the transition block
when not transitionBlock:
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock let clMock = t.clMock
let client = t.rpcClient let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( # On PoW mode, `safe` tag shall return error.
onPayloadProducerSelected: proc(): bool = var header: EthBlockHeader
var address: EthAddress var rr = client.namedHeader("safe", header)
testCond t.sendTx(address, 1.u256) testCond rr.isErr
shadow.hash = rlpHash(t.tx)
return true # Wait until this client catches up with latest PoS Block
, let ok = waitFor t.clMock.waitForTTD()
# Run test after a forkchoice with new HeadBlockHash has been broadcasted testCond ok
# First ForkchoiceUpdated sent was equal to 0x00..00, `safe` should return error now
rr = client.namedHeader("safe", header)
testCond rr.isErr
let pbres = clMock.produceBlocks(3, BlockProcessCallbacks(
# Run test after a forkchoice with new SafeBlockHash has been broadcasted
onSafeBlockChange: proc(): bool = onSafeBlockChange: proc(): bool =
var lastHeader: EthBlockHeader var header: EthBlockHeader
var hRes = client.latestHeader(lastHeader) let rr = client.namedHeader("safe", header)
if hRes.isErr: testCond rr.isOk
error "unable to get latest header", msg=hRes.error let safeBlockHash = hash256(clMock.latestForkchoice.safeBlockHash)
return false header.blockHash == safeBlockHash
let lastHash = BlockHash lastHeader.blockHash.data
if lastHash != clMock.latestForkchoice.headBlockHash:
error "latest block header doesn't match SafeBlock hash", hash=lastHash.toHex
return false
let rr = client.txReceipt(shadow.hash)
if rr.isErr:
error "unable to get transaction receipt"
return false
return true
)) ))
testCond produceSingleBlockRes
blockStatusSafeBlockGen(blockStatusSafeBlock1, false) testCond pbres
blockStatusSafeBlockGen(blockStatusSafeBlock2, true)
template blockStatusFinalizedBlockGen(procname: untyped, transitionBlock: bool) = proc blockStatusFinalizedBlock(t: TestEnv): TestStatus =
proc procName(t: TestEnv): TestStatus =
result = TestStatus.OK result = TestStatus.OK
# Wait until TTD is reached by this client
let ok = waitFor t.clMock.waitForTTD()
testCond ok
# Produce blocks before starting the test, only if we are not testing the transition block
when not transitionBlock:
let produce5BlockRes = t.clMock.produceBlocks(5, BlockProcessCallbacks())
testCond produce5BlockRes
let clMock = t.clMock let clMock = t.clMock
let client = t.rpcClient let client = t.rpcClient
let shadow = Shadow()
var produceSingleBlockRes = clMock.produceSingleBlock(BlockProcessCallbacks( # On PoW mode, `finalized` tag shall return error.
onPayloadProducerSelected: proc(): bool = var header: EthBlockHeader
var address: EthAddress var rr = client.namedHeader("finalized", header)
testCond t.sendTx(address, 1.u256) testCond rr.isErr
shadow.hash = rlpHash(t.tx)
return true # Wait until this client catches up with latest PoS Block
, let ok = waitFor t.clMock.waitForTTD()
# Run test after a forkchoice with new HeadBlockHash has been broadcasted testCond ok
# First ForkchoiceUpdated sent was equal to 0x00..00, `finalized` should return error now
rr = client.namedHeader("finalized", header)
testCond rr.isErr
let pbres = clMock.produceBlocks(3, BlockProcessCallbacks(
# Run test after a forkchoice with new FinalizedBlockHash has been broadcasted
onFinalizedBlockChange: proc(): bool = onFinalizedBlockChange: proc(): bool =
var lastHeader: EthBlockHeader var header: EthBlockHeader
var hRes = client.latestHeader(lastHeader) let rr = client.namedHeader("finalized", header)
if hRes.isErr: testCond rr.isOk
error "unable to get latest header", msg=hRes.error let finalizedBlockHash = hash256(clMock.latestForkchoice.finalizedBlockHash)
return false header.blockHash == finalizedBlockHash
let lastHash = BlockHash lastHeader.blockHash.data
if lastHash != clMock.latestForkchoice.headBlockHash:
error "latest block header doesn't match FinalizedBlock hash", hash=lastHash.toHex
return false
let rr = client.txReceipt(shadow.hash)
if rr.isErr:
error "unable to get transaction receipt"
return false
return true
)) ))
testCond produceSingleBlockRes
blockStatusFinalizedBlockGen(blockStatusFinalizedBlock1, false) testCond pbres
blockStatusFinalizedBlockGen(blockStatusFinalizedBlock2, true)
proc blockStatusReorg(t: TestEnv): TestStatus = proc blockStatusReorg(t: TestEnv): TestStatus =
result = TestStatus.OK result = TestStatus.OK
@ -1900,21 +1866,13 @@ const engineTestList* = [
ttd: 5, ttd: 5,
), ),
TestSpec( TestSpec(
name: "Latest Block after New SafeBlock", name: "safe Block after New SafeBlockHash",
run: blockStatusSafeBlock1, run: blockStatusSafeBlock,
),
TestSpec(
name: "Latest Block after New SafeBlock (Transition Block)",
run: blockStatusSafeBlock2,
ttd: 5, ttd: 5,
), ),
TestSpec( TestSpec(
name: "Latest Block after New FinalizedBlock", name: "finalized Block after New FinalizedBlockHash",
run: blockStatusFinalizedBlock1, run: blockStatusFinalizedBlock,
),
TestSpec(
name: "Latest Block after New FinalizedBlock (Transition Block)",
run: blockStatusFinalizedBlock2,
ttd: 5, ttd: 5,
), ),
TestSpec( TestSpec(

View File

@ -435,3 +435,21 @@ proc persistUncles*(self: BaseChainDB, uncles: openArray[BlockHeader]): Hash256
let enc = rlp.encode(uncles) let enc = rlp.encode(uncles)
result = keccakHash(enc) result = keccakHash(enc)
self.db.put(genericHashKey(result).toOpenArray, enc) self.db.put(genericHashKey(result).toOpenArray, enc)
proc safeHeaderHash*(self: BaseChainDB): Hash256 =
discard self.getHash(safeHashKey(), result)
proc safeHeaderHash*(self: BaseChainDB, headerHash: Hash256) =
self.db.put(safeHashKey().toOpenArray, rlp.encode(headerHash))
proc finalizedHeaderHash*(self: BaseChainDB): Hash256 =
discard self.getHash(finalizedHashKey(), result)
proc finalizedHeaderHash*(self: BaseChainDB, headerHash: Hash256) =
self.db.put(finalizedHashKey().toOpenArray, rlp.encode(headerHash))
proc safeHeader*(self: BaseChainDB): BlockHeader =
self.getBlockHeader(self.safeHeaderHash)
proc finalizedHeader*(self: BaseChainDB): BlockHeader =
self.getBlockHeader(self.finalizedHeaderHash)

View File

@ -13,6 +13,8 @@ type
cliqueSnapshot cliqueSnapshot
transitionStatus transitionStatus
terminalHash terminalHash
safeHash
finalizedHash
DbKey* = object DbKey* = object
# The first byte stores the key type. The rest are key-specific values # The first byte stores the key type. The rest are key-specific values
@ -69,6 +71,14 @@ proc terminalHashKey*(): DbKey =
result.data[0] = byte ord(terminalHash) result.data[0] = byte ord(terminalHash)
result.dataEndPos = uint8 1 result.dataEndPos = uint8 1
proc safeHashKey*(): DbKey {.inline.} =
result.data[0] = byte ord(safeHash)
result.dataEndPos = uint8 1
proc finalizedHashKey*(): DbKey {.inline.} =
result.data[0] = byte ord(finalizedHash)
result.dataEndPos = uint8 1
template toOpenArray*(k: DbKey): openArray[byte] = template toOpenArray*(k: DbKey): openArray[byte] =
k.data.toOpenArray(0, int(k.dataEndPos)) k.data.toOpenArray(0, int(k.dataEndPos))

View File

@ -277,6 +277,7 @@ proc setupEngineAPI*(
finalHash=finalHash.data.toHex, finalHash=finalHash.data.toHex,
finalizedBlockHash=finalizedBlockHash.data.toHex finalizedBlockHash=finalizedBlockHash.data.toHex
raise (ref InvalidRequest)(code: engineApiInvalidParams, msg: "finalilized block not canonical") raise (ref InvalidRequest)(code: engineApiInvalidParams, msg: "finalilized block not canonical")
db.finalizedHeaderHash(finalizedBlockHash)
let safeBlockHash = update.safeBlockHash.asEthHash let safeBlockHash = update.safeBlockHash.asEthHash
if safeBlockHash != Hash256(): if safeBlockHash != Hash256():
@ -295,6 +296,7 @@ proc setupEngineAPI*(
safeHash=safeHash.data.toHex, safeHash=safeHash.data.toHex,
safeBlockHash=safeBlockHash.data.toHex safeBlockHash=safeBlockHash.data.toHex
raise (ref InvalidRequest)(code: engineApiInvalidParams, msg: "safe head not canonical") raise (ref InvalidRequest)(code: engineApiInvalidParams, msg: "safe head not canonical")
db.safeHeaderHash(safeBlockHash)
# If payload generation was requested, create a new block to be potentially # 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 # sealed by the beacon client. The payload will be requested later, and we

View File

@ -35,6 +35,8 @@ proc headerFromTag*(chain: BaseChainDB, blockTag: string): BlockHeader =
case tag case tag
of "latest": result = chain.getCanonicalHead() of "latest": result = chain.getCanonicalHead()
of "earliest": result = chain.getBlockHeader(GENESIS_BLOCK_NUMBER) of "earliest": result = chain.getBlockHeader(GENESIS_BLOCK_NUMBER)
of "safe": result = chain.safeHeader()
of "finalized": result = chain.finalizedHeader()
of "pending": of "pending":
#TODO: Implement get pending block #TODO: Implement get pending block
raise newException(ValueError, "Pending tag not yet implemented") raise newException(ValueError, "Pending tag not yet implemented")