mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-02-27 03:10:44 +00:00
* Part of EIP-4895: add withdrawals processing to block processing.
* Refactoring: extracted the engine API handler bodies into procs.
Intending to implement the V2 versions next. (I need the bodies to be
in separate procs so that multiple versions can use them.)
* Working on Engine API changes for Shanghai.
* Updated nim-web3, resolved ambiguity in Hash256 type.
* Updated nim-eth3 to point to master, now that I've merged that.
* I'm confused about what's going on with engine_client.
But let's try resolving this Hash256 ambiguity.
* Still trying to fix this conflict with the Hash256 types.
* Does this work now that nimbus-eth2 has been updated?
* Corrected blockValue in getPayload responses back to UInt256.
c834f67a37
* Working on getting the withdrawals-related tests to pass.
* Fixing more of those Hash256 ambiguities.
(I'm not sure why the nim-web3 library introduced a conflicting type
named Hash256, but right now I just want to get this code to compile again.)
* Bumped a couple of libraries to fix some error messages.
* Needed to get "make fluffy-tools" to pass, too.
* Getting "make nimbus_verified_proxy" to build.
382 lines
13 KiB
Nim
382 lines
13 KiB
Nim
import
|
|
std/[times, tables],
|
|
chronicles,
|
|
nimcrypto/sysrand,
|
|
stew/byteutils,
|
|
eth/common, chronos,
|
|
json_rpc/rpcclient,
|
|
../../../nimbus/rpc/merge/mergeutils,
|
|
../../../nimbus/[constants],
|
|
./engine_client
|
|
|
|
import web3/engine_api_types except Hash256 # conflict with the one from eth/common
|
|
|
|
# Consensus Layer Client Mock used to sync the Execution Clients once the TTD has been reached
|
|
type
|
|
CLMocker* = ref object
|
|
nextFeeRecipient*: EthAddress
|
|
nextPayloadID: PayloadID
|
|
|
|
# PoS Chain History Information
|
|
prevRandaoHistory*: Table[uint64, Hash256]
|
|
executedPayloadHistory*: Table[uint64, ExecutionPayloadV1]
|
|
|
|
# Latest broadcasted data using the PoS Engine API
|
|
latestHeadNumber*: uint64
|
|
latestHeader*: common.BlockHeader
|
|
latestPayloadBuilt* : ExecutionPayloadV1
|
|
latestExecutedPayload*: ExecutionPayloadV1
|
|
latestForkchoice* : ForkchoiceStateV1
|
|
|
|
# Merge related
|
|
firstPoSBlockNumber : Option[uint64]
|
|
ttdReached* : bool
|
|
|
|
client : RpcClient
|
|
ttd : DifficultyInt
|
|
|
|
slotsToSafe* : int
|
|
slotsToFinalized* : int
|
|
headHashHistory : seq[BlockHash]
|
|
|
|
BlockProcessCallbacks* = object
|
|
onPayloadProducerSelected* : proc(): bool {.gcsafe.}
|
|
onGetPayloadID* : proc(): bool {.gcsafe.}
|
|
onGetPayload* : proc(): bool {.gcsafe.}
|
|
onNewPayloadBroadcast* : proc(): bool {.gcsafe.}
|
|
onForkchoiceBroadcast* : proc(): bool {.gcsafe.}
|
|
onSafeBlockChange * : proc(): bool {.gcsafe.}
|
|
onFinalizedBlockChange* : proc(): bool {.gcsafe.}
|
|
|
|
|
|
proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) =
|
|
cl.client = client
|
|
cl.ttd = ttd
|
|
cl.slotsToSafe = 1
|
|
cl.slotsToFinalized = 2
|
|
|
|
proc newClMocker*(client: RpcClient, ttd: DifficultyInt): CLMocker =
|
|
new result
|
|
result.init(client, ttd)
|
|
|
|
proc waitForTTD*(cl: CLMocker): Future[bool] {.async.} =
|
|
let (header, waitRes) = await cl.client.waitForTTD(cl.ttd)
|
|
if not waitRes:
|
|
error "timeout while waiting for TTD"
|
|
return false
|
|
|
|
cl.latestHeader = header
|
|
cl.ttdReached = true
|
|
|
|
let headerHash = BlockHash(common.blockHash(cl.latestHeader).data)
|
|
cl.latestForkchoice.headBlockHash = headerHash
|
|
|
|
if cl.slotsToSafe == 0:
|
|
cl.latestForkchoice.safeBlockHash = headerHash
|
|
|
|
if cl.slotsToFinalized == 0:
|
|
cl.latestForkchoice.finalizedBlockHash = headerHash
|
|
|
|
cl.latestHeadNumber = cl.latestHeader.blockNumber.truncate(uint64)
|
|
|
|
let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice)
|
|
if res.isErr:
|
|
error "waitForTTD: forkchoiceUpdated error", msg=res.error
|
|
return false
|
|
|
|
let s = res.get()
|
|
if s.payloadStatus.status != PayloadExecutionStatus.valid:
|
|
error "waitForTTD: forkchoiceUpdated response unexpected",
|
|
expect = PayloadExecutionStatus.valid,
|
|
get = s.payloadStatus.status
|
|
return false
|
|
|
|
return true
|
|
|
|
proc pickNextPayloadProducer(cl: CLMocker): bool =
|
|
let nRes = cl.client.blockNumber()
|
|
if nRes.isErr:
|
|
error "CLMocker: could not get block number", msg=nRes.error
|
|
return false
|
|
|
|
let lastBlockNumber = nRes.get
|
|
if cl.latestHeadNumber != lastBlockNumber:
|
|
error "CLMocker: unexpected lastBlockNumber",
|
|
get = lastBlockNumber,
|
|
expect = cl.latestHeadNumber
|
|
return false
|
|
|
|
var header: common.BlockHeader
|
|
let hRes = cl.client.headerByNumber(lastBlockNumber, header)
|
|
if hRes.isErr:
|
|
error "CLMocker: Could not get block header", msg=hRes.error
|
|
return false
|
|
|
|
let lastBlockHash = header.blockHash
|
|
if cl.latestHeader.blockHash != lastBlockHash:
|
|
error "CLMocker: Failed to obtain a client on the latest block number"
|
|
return false
|
|
|
|
return true
|
|
|
|
proc getNextPayloadID*(cl: CLMocker): bool =
|
|
# Generate a random value for the PrevRandao field
|
|
var nextPrevRandao: Hash256
|
|
doAssert randomBytes(nextPrevRandao.data) == 32
|
|
|
|
let timestamp = Quantity toUnix(cl.latestHeader.timestamp + 1.seconds)
|
|
let payloadAttributes = PayloadAttributesV1(
|
|
timestamp: timestamp,
|
|
prevRandao: FixedBytes[32] nextPrevRandao.data,
|
|
suggestedFeeRecipient: Address cl.nextFeeRecipient,
|
|
)
|
|
|
|
# Save random value
|
|
let number = cl.latestHeader.blockNumber.truncate(uint64) + 1
|
|
cl.prevRandaoHistory[number] = nextPrevRandao
|
|
|
|
let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice, some(payloadAttributes))
|
|
if res.isErr:
|
|
error "CLMocker: Could not send forkchoiceUpdatedV1", msg=res.error
|
|
return false
|
|
|
|
let s = res.get()
|
|
if s.payloadStatus.status != PayloadExecutionStatus.valid:
|
|
error "CLMocker: Unexpected forkchoiceUpdated Response from Payload builder",
|
|
status=s.payloadStatus.status
|
|
|
|
doAssert s.payLoadID.isSome
|
|
cl.nextPayloadID = s.payloadID.get()
|
|
return true
|
|
|
|
proc getNextPayload*(cl: CLMocker): bool =
|
|
let res = cl.client.getPayloadV1(cl.nextPayloadID)
|
|
if res.isErr:
|
|
error "CLMocker: Could not getPayload",
|
|
payloadID=toHex(cl.nextPayloadID)
|
|
return false
|
|
|
|
cl.latestPayloadBuilt = res.get()
|
|
let header = toBlockHeader(cl.latestPayloadBuilt)
|
|
let blockHash = BlockHash header.blockHash.data
|
|
if blockHash != cl.latestPayloadBuilt.blockHash:
|
|
error "getNextPayload blockHash mismatch",
|
|
expected=cl.latestPayloadBuilt.blockHash.toHex,
|
|
get=blockHash.toHex
|
|
return false
|
|
|
|
return true
|
|
|
|
proc broadcastNewPayload(cl: CLMocker, payload: ExecutionPayloadV1): Result[PayloadStatusV1, string] =
|
|
let res = cl.client.newPayloadV1(payload)
|
|
return res
|
|
|
|
proc broadcastNextNewPayload(cl: CLMocker): bool =
|
|
let res = cl.broadcastNewPayload(cl.latestPayloadBuilt)
|
|
if res.isErr:
|
|
error "CLMocker: broadcastNewPayload Error", msg=res.error
|
|
return false
|
|
|
|
let s = res.get()
|
|
if s.status == PayloadExecutionStatus.valid:
|
|
# The client is synced and the payload was immediately validated
|
|
# https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md:
|
|
# - If validation succeeds, the response MUST contain {status: VALID, latestValidHash: payload.blockHash}
|
|
let blockHash = cl.latestPayloadBuilt.blockHash
|
|
if s.latestValidHash.isNone:
|
|
error "CLMocker: NewPayload returned VALID status with nil LatestValidHash",
|
|
expected=blockHash.toHex
|
|
return false
|
|
|
|
let latestValidHash = s.latestValidHash.get()
|
|
if latestValidHash != BlockHash(blockHash):
|
|
error "CLMocker: NewPayload returned VALID status with incorrect LatestValidHash",
|
|
get=latestValidHash.toHex, expected=blockHash.toHex
|
|
return false
|
|
|
|
elif s.status == PayloadExecutionStatus.accepted:
|
|
# The client is not synced but the payload was accepted
|
|
# https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md:
|
|
# - {status: ACCEPTED, latestValidHash: null, validationError: null} if the following conditions are met:
|
|
# the blockHash of the payload is valid
|
|
# the payload doesn't extend the canonical chain
|
|
# the payload hasn't been fully validated.
|
|
let nullHash = BlockHash Hash256().data
|
|
let latestValidHash = s.latestValidHash.get(nullHash)
|
|
if s.latestValidHash.isSome and latestValidHash != nullHash:
|
|
error "CLMocker: NewPayload returned ACCEPTED status with incorrect LatestValidHash",
|
|
hash=latestValidHash.toHex
|
|
return false
|
|
|
|
else:
|
|
error "CLMocker: broadcastNewPayload Response",
|
|
status=s.status
|
|
return false
|
|
|
|
cl.latestExecutedPayload = cl.latestPayloadBuilt
|
|
let number = uint64 cl.latestPayloadBuilt.blockNumber
|
|
cl.executedPayloadHistory[number] = cl.latestPayloadBuilt
|
|
return true
|
|
|
|
proc broadcastForkchoiceUpdated*(cl: CLMocker,
|
|
update: ForkchoiceStateV1): Result[ForkchoiceUpdatedResponse, string] =
|
|
let res = cl.client.forkchoiceUpdatedV1(update)
|
|
return res
|
|
|
|
proc broadcastLatestForkchoice(cl: CLMocker): bool =
|
|
let res = cl.broadcastForkchoiceUpdated(cl.latestForkchoice)
|
|
if res.isErr:
|
|
error "CLMocker: broadcastForkchoiceUpdated Error", msg=res.error
|
|
return false
|
|
|
|
let s = res.get()
|
|
if s.payloadStatus.status != PayloadExecutionStatus.valid:
|
|
error "CLMocker: broadcastForkchoiceUpdated Response",
|
|
status=s.payloadStatus.status
|
|
return false
|
|
|
|
return true
|
|
|
|
proc produceSingleBlock*(cl: CLMocker, cb: BlockProcessCallbacks): bool {.gcsafe.} =
|
|
doAssert(cl.ttdReached)
|
|
|
|
if not cl.pickNextPayloadProducer():
|
|
return false
|
|
|
|
if cb.onPayloadProducerSelected != nil:
|
|
if not cb.onPayloadProducerSelected():
|
|
return false
|
|
|
|
if not cl.getNextPayloadID():
|
|
return false
|
|
|
|
if cb.onGetPayloadID != nil:
|
|
if not cb.onGetPayloadID():
|
|
return false
|
|
|
|
# Give the client a delay between getting the payload ID and actually retrieving the payload
|
|
#time.Sleep(PayloadProductionClientDelay)
|
|
|
|
if not cl.getNextPayload():
|
|
return false
|
|
|
|
if cb.onGetPayload != nil:
|
|
if not cb.onGetPayload():
|
|
return false
|
|
|
|
if not cl.broadcastNextNewPayload():
|
|
return false
|
|
|
|
if cb.onNewPayloadBroadcast != nil:
|
|
if not cb.onNewPayloadBroadcast():
|
|
return false
|
|
|
|
# Broadcast forkchoice updated with new HeadBlock to all clients
|
|
let previousForkchoice = cl.latestForkchoice
|
|
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():
|
|
return false
|
|
|
|
if cb.onForkchoiceBroadcast != nil:
|
|
if not cb.onForkchoiceBroadcast():
|
|
return false
|
|
|
|
# Broadcast forkchoice updated with new SafeBlock to all clients
|
|
if cb.onSafeBlockChange != nil and cl.latestForkchoice.safeBlockHash != previousForkchoice.safeBlockHash:
|
|
if not cb.onSafeBlockChange():
|
|
return false
|
|
|
|
# Broadcast forkchoice updated with new FinalizedBlock to all clients
|
|
if cb.onFinalizedBlockChange != nil and cl.latestForkchoice.finalizedBlockHash != previousForkchoice.finalizedBlockHash:
|
|
if not cb.onFinalizedBlockChange():
|
|
return false
|
|
|
|
# Broadcast forkchoice updated with new FinalizedBlock to all clients
|
|
# Save the number of the first PoS block
|
|
if cl.firstPoSBlockNumber.isNone:
|
|
let number = cl.latestHeader.blockNumber.truncate(uint64) + 1
|
|
cl.firstPoSBlockNumber = some(number)
|
|
|
|
# Save the header of the latest block in the PoS chain
|
|
cl.latestHeadNumber = cl.latestHeadNumber + 1
|
|
|
|
# Check if any of the clients accepted the new payload
|
|
var newHeader: common.BlockHeader
|
|
let res = cl.client.headerByNumber(cl.latestHeadNumber, newHeader)
|
|
if res.isErr:
|
|
error "CLMock ProduceSingleBlock", msg=res.error
|
|
return false
|
|
|
|
let newHash = BlockHash newHeader.blockHash.data
|
|
if newHash != cl.latestPayloadBuilt.blockHash:
|
|
error "CLMocker: None of the clients accepted the newly constructed payload",
|
|
hash=newHash.toHex
|
|
return false
|
|
|
|
# Check that the new finalized header has the correct properties
|
|
# ommersHash == 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
|
|
if newHeader.ommersHash != EMPTY_UNCLE_HASH:
|
|
error "CLMocker: Client produced a new header with incorrect ommersHash", ommersHash = newHeader.ommersHash
|
|
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
|
|
|
|
# Loop produce PoS blocks by using the Engine API
|
|
proc produceBlocks*(cl: CLMocker, blockCount: int, cb: BlockProcessCallbacks): bool {.gcsafe.} =
|
|
# Produce requested amount of blocks
|
|
for i in 0..<blockCount:
|
|
if not cl.produceSingleBlock(cb):
|
|
return false
|
|
return true
|
|
|
|
# Check whether a block number is a PoS block
|
|
proc isBlockPoS*(cl: CLMocker, bn: common.BlockNumber): bool =
|
|
if cl.firstPoSBlockNumber.isNone:
|
|
return false
|
|
|
|
let number = cl.firstPoSBlockNumber.get()
|
|
let bn = bn.truncate(uint64)
|
|
if number > bn:
|
|
return false
|
|
|
|
return true
|
|
|
|
proc posBlockNumber*(cl: CLMocker): uint64 =
|
|
cl.firstPoSBlockNumber.get(0'u64)
|