Adam Spitz d8a1adacaa
More work on withdrawals (#1482)
* 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.
2023-03-09 18:40:55 -05:00

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)