hive: add ethereum/engine simulator
some test with multiple client still need polishing. merge test using specially crafted blocks need to be added.
This commit is contained in:
parent
83bdde55aa
commit
4d126f2461
|
@ -0,0 +1,329 @@
|
|||
import
|
||||
std/[times, tables],
|
||||
chronicles,
|
||||
nimcrypto,
|
||||
stew/byteutils,
|
||||
eth/common, chronos,
|
||||
web3/engine_api_types,
|
||||
json_rpc/rpcclient,
|
||||
../../../nimbus/merge/mergeutils,
|
||||
../../../nimbus/debug,
|
||||
./engine_client
|
||||
|
||||
# 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
|
||||
latestFinalizedNumber*: uint64
|
||||
latestFinalizedHeader*: common.BlockHeader
|
||||
latestPayloadBuilt* : ExecutionPayloadV1
|
||||
latestExecutedPayload*: ExecutionPayloadV1
|
||||
latestForkchoice* : ForkchoiceStateV1
|
||||
|
||||
# Merge related
|
||||
firstPoSBlockNumber : Option[uint64]
|
||||
ttdReached : bool
|
||||
|
||||
client : RpcClient
|
||||
ttd : DifficultyInt
|
||||
|
||||
BlockProcessCallbacks* = object
|
||||
onPayloadProducerSelected* : proc(): bool {.gcsafe.}
|
||||
onGetPayloadID* : proc(): bool {.gcsafe.}
|
||||
onGetPayload* : proc(): bool {.gcsafe.}
|
||||
onNewPayloadBroadcast* : proc(): bool {.gcsafe.}
|
||||
onHeadBlockForkchoiceBroadcast* : proc(): bool {.gcsafe.}
|
||||
onSafeBlockForkchoiceBroadcast* : proc(): bool {.gcsafe.}
|
||||
onFinalizedBlockForkchoiceBroadcast* : proc(): bool {.gcsafe.}
|
||||
|
||||
|
||||
proc init*(cl: CLMocker, client: RpcClient, ttd: DifficultyInt) =
|
||||
cl.client = client
|
||||
cl.ttd = ttd
|
||||
|
||||
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.latestFinalizedHeader = header
|
||||
cl.ttdReached = true
|
||||
|
||||
let headerHash = BlockHash(common.blockHash(cl.latestFinalizedHeader).data)
|
||||
cl.latestForkchoice.headBlockHash = headerHash
|
||||
cl.latestForkchoice.safeBlockHash = headerHash
|
||||
cl.latestForkchoice.finalizedBlockHash = headerHash
|
||||
cl.latestFinalizedNumber = cl.latestFinalizedHeader.blockNumber.truncate(uint64)
|
||||
|
||||
let res = cl.client.forkchoiceUpdatedV1(cl.latestForkchoice)
|
||||
if res.isErr:
|
||||
error "forkchoiceUpdated error", msg=res.error
|
||||
return false
|
||||
|
||||
let s = res.get()
|
||||
if s.payloadStatus.status != PayloadExecutionStatus.valid:
|
||||
error "forkchoiceUpdated response",
|
||||
status=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.latestFinalizedNumber != lastBlockNumber:
|
||||
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.latestFinalizedHeader.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 nimcrypto.randomBytes(nextPrevRandao.data) == 32
|
||||
|
||||
let timestamp = Quantity toUnix(cl.latestFinalizedHeader.timestamp + 1.seconds)
|
||||
let payloadAttributes = PayloadAttributesV1(
|
||||
timestamp: timestamp,
|
||||
prevRandao: FixedBytes[32] nextPrevRandao.data,
|
||||
suggestedFeeRecipient: Address cl.nextFeeRecipient,
|
||||
)
|
||||
|
||||
# Save random value
|
||||
let number = cl.latestFinalizedHeader.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 blockHash = cl.latestPayloadBuilt.blockHash
|
||||
cl.latestForkchoice.headBlockHash = blockHash
|
||||
if not cl.broadcastLatestForkchoice():
|
||||
return false
|
||||
|
||||
if cb.onHeadBlockForkchoiceBroadcast != nil:
|
||||
if not cb.onHeadBlockForkchoiceBroadcast():
|
||||
return false
|
||||
|
||||
# Broadcast forkchoice updated with new SafeBlock to all clients
|
||||
cl.latestForkchoice.safeBlockHash = blockHash
|
||||
if not cl.broadcastLatestForkchoice():
|
||||
return false
|
||||
|
||||
if cb.onSafeBlockForkchoiceBroadcast != nil:
|
||||
if not cb.onSafeBlockForkchoiceBroadcast():
|
||||
return false
|
||||
|
||||
# Broadcast forkchoice updated with new FinalizedBlock to all clients
|
||||
cl.latestForkchoice.finalizedBlockHash = blockHash
|
||||
if not cl.broadcastLatestForkchoice():
|
||||
return false
|
||||
|
||||
# Save the number of the first PoS block
|
||||
if cl.firstPoSBlockNumber.isNone:
|
||||
let number = cl.latestFinalizedHeader.blockNumber.truncate(uint64) + 1
|
||||
cl.firstPoSBlockNumber = some(number)
|
||||
|
||||
# Save the header of the latest block in the PoS chain
|
||||
cl.latestFinalizedNumber = cl.latestFinalizedNumber + 1
|
||||
|
||||
# Check if any of the clients accepted the new payload
|
||||
var newHeader: common.BlockHeader
|
||||
let res = cl.client.headerByNumber(cl.latestFinalizedNumber, 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
|
||||
|
||||
cl.latestFinalizedHeader = newHeader
|
||||
|
||||
if cb.onFinalizedBlockForkchoiceBroadcast != nil:
|
||||
if not cb.onFinalizedBlockForkchoiceBroadcast():
|
||||
return false
|
||||
|
||||
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
|
|
@ -0,0 +1,191 @@
|
|||
import
|
||||
std/[times, json],
|
||||
stew/byteutils,
|
||||
eth/[common, rlp], chronos,
|
||||
web3/engine_api_types,
|
||||
json_rpc/rpcclient,
|
||||
../../../tests/rpcclient/eth_api,
|
||||
../../../premix/parser,
|
||||
../../../nimbus/rpc/hexstrings,
|
||||
../../../premix/parser
|
||||
|
||||
import web3/engine_api as web3_engine_api
|
||||
|
||||
proc forkchoiceUpdatedV1*(client: RpcClient,
|
||||
update: ForkchoiceStateV1,
|
||||
payloadAttributes = none(PayloadAttributesV1)):
|
||||
Result[ForkchoiceUpdatedResponse, string] =
|
||||
try:
|
||||
let res = waitFor client.engine_forkchoiceUpdatedV1(update, payloadAttributes)
|
||||
return ok(res)
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc getPayloadV1*(client: RpcClient, payloadId: PayloadID): Result[ExecutionPayloadV1, string] =
|
||||
try:
|
||||
let res = waitFor client.engine_getPayloadV1(payloadId)
|
||||
return ok(res)
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc newPayloadV1*(client: RpcClient,
|
||||
payload: ExecutionPayloadV1):
|
||||
Result[PayloadStatusV1, string] =
|
||||
try:
|
||||
let res = waitFor client.engine_newPayloadV1(payload)
|
||||
return ok(res)
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc toBlockNumber(n: Option[HexQuantityStr]): common.BlockNumber =
|
||||
if n.isNone:
|
||||
return 0.toBlockNumber
|
||||
toBlockNumber(hexToInt(string n.get, uint64))
|
||||
|
||||
proc toBlockNonce(n: Option[HexDataStr]): common.BlockNonce =
|
||||
if n.isNone:
|
||||
return default(BlockNonce)
|
||||
hexToByteArray(string n.get, result)
|
||||
|
||||
proc toBaseFeePerGas(n: Option[HexQuantityStr]): Option[UInt256] =
|
||||
if n.isNone:
|
||||
return none(UInt256)
|
||||
some(UInt256.fromHex(string n.get))
|
||||
|
||||
proc toBlockHeader(bc: eth_api.BlockObject): common.BlockHeader =
|
||||
common.BlockHeader(
|
||||
blockNumber: toBlockNumber(bc.number),
|
||||
parentHash : bc.parentHash,
|
||||
nonce : toBlockNonce(bc.nonce),
|
||||
ommersHash : bc.sha3Uncles,
|
||||
bloom : BloomFilter bc.logsBloom,
|
||||
txRoot : bc.transactionsRoot,
|
||||
stateRoot : bc.stateRoot,
|
||||
receiptRoot: bc.receiptsRoot,
|
||||
coinbase : bc.miner,
|
||||
difficulty : UInt256.fromHex(string bc.difficulty),
|
||||
extraData : hexToSeqByte(string bc.extraData),
|
||||
mixDigest : bc.mixHash,
|
||||
gasLimit : hexToInt(string bc.gasLimit, GasInt),
|
||||
gasUsed : hexToInt(string bc.gasUsed, GasInt),
|
||||
timestamp : initTime(hexToInt(string bc.timestamp, int64), 0),
|
||||
fee : toBaseFeePerGas(bc.baseFeePerGas)
|
||||
)
|
||||
|
||||
proc toTransactions(txs: openArray[JsonNode]): seq[Transaction] =
|
||||
for x in txs:
|
||||
result.add parseTransaction(x)
|
||||
|
||||
proc waitForTTD*(client: RpcClient,
|
||||
ttd: DifficultyInt): Future[(common.BlockHeader, bool)] {.async.} =
|
||||
let period = chronos.seconds(5)
|
||||
var loop = 0
|
||||
var emptyHeader: common.BlockHeader
|
||||
while loop < 5:
|
||||
let res = await client.eth_getBlockByNumber("latest", false)
|
||||
if res.isNone:
|
||||
return (emptyHeader, false)
|
||||
let bc = res.get()
|
||||
if hexToInt(string bc.totalDifficulty, int64).u256 >= ttd:
|
||||
return (toBlockHeader(bc), true)
|
||||
|
||||
await sleepAsync(period)
|
||||
inc loop
|
||||
|
||||
return (emptyHeader, false)
|
||||
|
||||
proc blockNumber*(client: RpcClient): Result[uint64, string] =
|
||||
try:
|
||||
let res = waitFor client.eth_blockNumber()
|
||||
return ok(hexToInt(string res, uint64))
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc headerByNumber*(client: RpcClient, number: uint64, output: var common.BlockHeader): Result[void, string] =
|
||||
try:
|
||||
let qty = encodeQuantity(number)
|
||||
let res = waitFor client.eth_getBlockByNumber(string qty, false)
|
||||
if res.isNone:
|
||||
return err("failed to get blockHeader: " & $number)
|
||||
output = toBlockHeader(res.get())
|
||||
return ok()
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc blockByNumber*(client: RpcClient, number: uint64, output: var common.EthBlock): Result[void, string] =
|
||||
try:
|
||||
let qty = encodeQuantity(number)
|
||||
let res = waitFor client.eth_getBlockByNumber(string qty, true)
|
||||
if res.isNone:
|
||||
return err("failed to get block: " & $number)
|
||||
let blk = res.get()
|
||||
output.header = toBlockHeader(blk)
|
||||
output.txs = toTransactions(blk.transactions)
|
||||
return ok()
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc latestHeader*(client: RpcClient, output: var common.BlockHeader): Result[void, string] =
|
||||
try:
|
||||
let res = waitFor client.eth_getBlockByNumber("latest", false)
|
||||
if res.isNone:
|
||||
return err("failed to get latest blockHeader")
|
||||
output = toBlockHeader(res.get())
|
||||
return ok()
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc latestBlock*(client: RpcClient, output: var common.EthBlock): Result[void, string] =
|
||||
try:
|
||||
let res = waitFor client.eth_getBlockByNumber("latest", true)
|
||||
if res.isNone:
|
||||
return err("failed to get latest blockHeader")
|
||||
let blk = res.get()
|
||||
output.header = toBlockHeader(blk)
|
||||
output.txs = toTransactions(blk.transactions)
|
||||
return ok()
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc sendTransaction*(client: RpcClient, tx: common.Transaction): Result[void, string] =
|
||||
try:
|
||||
let encodedTx = rlp.encode(tx)
|
||||
let res = waitFor client.eth_sendRawTransaction(hexDataStr(encodedTx))
|
||||
let txHash = rlpHash(tx)
|
||||
let getHash = Hash256(data: hexToByteArray[32](string res))
|
||||
if txHash != getHash:
|
||||
return err("sendTransaction: tx hash mismatch")
|
||||
return ok()
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc balanceAt*(client: RpcClient, address: EthAddress): Result[UInt256, string] =
|
||||
try:
|
||||
let res = waitFor client.eth_getBalance(ethAddressStr(address), "latest")
|
||||
return ok(UInt256.fromHex(res.string))
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc txReceipt*(client: RpcClient, txHash: Hash256): Result[eth_api.ReceiptObject, string] =
|
||||
try:
|
||||
let res = waitFor client.eth_getTransactionReceipt(txHash)
|
||||
if res.isNone:
|
||||
return err("failed to get receipt: " & txHash.data.toHex)
|
||||
return ok(res.get)
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc storageAt*(client: RpcClient, address: EthAddress, slot: UInt256): Result[UInt256, string] =
|
||||
try:
|
||||
let res = waitFor client.eth_getStorageAt(ethAddressStr(address), encodeQuantity(slot), "latest")
|
||||
return ok(UInt256.fromHex(res.string))
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
||||
|
||||
proc storageAt*(client: RpcClient, address: EthAddress, slot: UInt256, number: common.BlockNumber): Result[UInt256, string] =
|
||||
try:
|
||||
let tag = encodeQuantity(number)
|
||||
let res = waitFor client.eth_getStorageAt(ethAddressStr(address), encodeQuantity(slot), tag.string)
|
||||
return ok(UInt256.fromHex(res.string))
|
||||
except ValueError as e:
|
||||
return err(e.msg)
|
|
@ -0,0 +1,19 @@
|
|||
import
|
||||
test_env,
|
||||
engine_tests,
|
||||
chronos,
|
||||
unittest2
|
||||
|
||||
proc runTest(x: TestSpec, testStatusIMPL: var TestStatus) =
|
||||
var t = setupELClient()
|
||||
t.setRealTTD(x.ttd)
|
||||
x.run(t, testStatusIMPL)
|
||||
t.stopELClient()
|
||||
|
||||
proc main() =
|
||||
suite "Engine Tests":
|
||||
for x in engineTestList:
|
||||
test x.name:
|
||||
runTest(x, testStatusIMPL)
|
||||
|
||||
main()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"config": {
|
||||
"chainId": 7,
|
||||
"homesteadBlock": 0,
|
||||
"eip150Block": 0,
|
||||
"eip150Hash": "0x5de1ee4135274003348e80b788e5afa4b18b18d320a5622218d5c493fedf5689",
|
||||
"eip155Block": 0,
|
||||
"eip158Block": 0,
|
||||
"byzantiumBlock": 0,
|
||||
"constantinopleBlock": 0,
|
||||
"petersburgBlock": 0,
|
||||
"istanbulBlock": 0,
|
||||
"muirGlacierBlock": 0,
|
||||
"berlinBlock": 0,
|
||||
"londonBlock": 0,
|
||||
"clique": {
|
||||
"epoch": 3000,
|
||||
"period": 1
|
||||
}
|
||||
},
|
||||
"genesis": {
|
||||
"coinbase": "0x0000000000000000000000000000000000000000",
|
||||
"difficulty": "0x30000",
|
||||
"extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"gasLimit": "0x2fefd8",
|
||||
"nonce": "0x0000000000000000",
|
||||
"timestamp": "0x1234",
|
||||
"alloc": {
|
||||
"cf49fda3be353c69b41ed96333cd24302da4556f": {
|
||||
"balance": "0x123450000000000000000"
|
||||
},
|
||||
"0161e041aad467a890839d5b08b138c1e6373072": {
|
||||
"balance": "0x123450000000000000000"
|
||||
},
|
||||
"87da6a8c6e9eff15d703fc2773e32f6af8dbe301": {
|
||||
"balance": "0x123450000000000000000"
|
||||
},
|
||||
"b97de4b8c857e4f6bc354f226dc3249aaee49209": {
|
||||
"balance": "0x123450000000000000000"
|
||||
},
|
||||
"c5065c9eeebe6df2c2284d046bfc906501846c51": {
|
||||
"balance": "0x123450000000000000000"
|
||||
},
|
||||
"0000000000000000000000000000000000000314": {
|
||||
"balance": "0x0",
|
||||
"code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029",
|
||||
"storage": {
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234",
|
||||
"0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01"
|
||||
}
|
||||
},
|
||||
"0000000000000000000000000000000000000315": {
|
||||
"balance": "0x9999999999999999999999999999999",
|
||||
"code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029"
|
||||
},
|
||||
"0000000000000000000000000000000000000316": {
|
||||
"balance": "0x0",
|
||||
"code": "0x444355"
|
||||
},
|
||||
"0000000000000000000000000000000000000317": {
|
||||
"balance": "0x0",
|
||||
"code": "0x600160003555"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
import
|
||||
std/typetraits,
|
||||
test_env,
|
||||
eth/rlp
|
||||
|
||||
type
|
||||
ExecutableData* = object
|
||||
parentHash* : Hash256
|
||||
feeRecipient* : EthAddress
|
||||
stateRoot* : Hash256
|
||||
receiptsRoot* : Hash256
|
||||
logsBloom* : BloomFilter
|
||||
prevRandao* : Hash256
|
||||
number* : uint64
|
||||
gasLimit* : GasInt
|
||||
gasUsed* : GasInt
|
||||
timestamp* : EthTime
|
||||
extraData* : Blob
|
||||
baseFeePerGas*: UInt256
|
||||
blockHash* : Hash256
|
||||
transactions* : seq[Transaction]
|
||||
|
||||
CustomPayload* = object
|
||||
parentHash* : Option[Hash256]
|
||||
feeRecipient* : Option[EthAddress]
|
||||
stateRoot* : Option[Hash256]
|
||||
receiptsRoot* : Option[Hash256]
|
||||
logsBloom* : Option[BloomFilter]
|
||||
prevRandao* : Option[Hash256]
|
||||
number* : Option[uint64]
|
||||
gasLimit* : Option[GasInt]
|
||||
gasUsed* : Option[GasInt]
|
||||
timestamp* : Option[EthTime]
|
||||
extraData* : Option[Blob]
|
||||
baseFeePerGas*: Option[UInt256]
|
||||
blockHash* : Option[Hash256]
|
||||
transactions* : Option[seq[Transaction]]
|
||||
|
||||
proc customizePayload*(basePayload: ExecutableData, customData: CustomPayload): ExecutionPayloadV1 =
|
||||
let txs = if customData.transactions.isSome:
|
||||
customData.transactions.get
|
||||
else:
|
||||
basePayload.transactions
|
||||
|
||||
let txRoot = calcTxRoot(txs)
|
||||
|
||||
var customHeader = EthBlockHeader(
|
||||
parentHash: basePayload.parentHash,
|
||||
ommersHash: EMPTY_UNCLE_HASH,
|
||||
coinbase: basePayload.feeRecipient,
|
||||
stateRoot: basePayload.stateRoot,
|
||||
txRoot: txRoot,
|
||||
receiptRoot: basePayload.receiptsRoot,
|
||||
bloom: basePayload.logsBloom,
|
||||
difficulty: 0.u256,
|
||||
blockNumber: basePayload.number.toBlockNumber,
|
||||
gasLimit: basePayload.gasLimit,
|
||||
gasUsed: basePayload.gasUsed,
|
||||
timestamp: basePayload.timestamp,
|
||||
extraData: basePayload.extraData,
|
||||
mixDigest: basePayload.prevRandao,
|
||||
nonce: default(BlockNonce),
|
||||
fee: some(basePayload.baseFeePerGas)
|
||||
)
|
||||
|
||||
# Overwrite custom information
|
||||
if customData.parentHash.isSome:
|
||||
customHeader.parentHash = customData.parentHash.get
|
||||
|
||||
if customData.feeRecipient.isSome:
|
||||
customHeader.coinbase = customData.feeRecipient.get
|
||||
|
||||
if customData.stateRoot.isSome:
|
||||
customHeader.stateRoot = customData.stateRoot.get
|
||||
|
||||
if customData.receiptsRoot.isSome:
|
||||
customHeader.receiptRoot = customData.receiptsRoot.get
|
||||
|
||||
if customData.logsBloom.isSome:
|
||||
customHeader.bloom = customData.logsBloom.get
|
||||
|
||||
if customData.prevRandao.isSome:
|
||||
customHeader.mixDigest = customData.prevRandao.get
|
||||
|
||||
if customData.number.isSome:
|
||||
customHeader.blockNumber = toBlockNumber(customData.number.get)
|
||||
|
||||
if customData.gasLimit.isSome:
|
||||
customHeader.gasLimit = customData.gasLimit.get
|
||||
|
||||
if customData.gasUsed.isSome:
|
||||
customHeader.gasUsed = customData.gasUsed.get
|
||||
|
||||
if customData.timestamp.isSome:
|
||||
customHeader.timestamp = customData.timestamp.get
|
||||
|
||||
if customData.extraData.isSome:
|
||||
customHeader.extraData = customData.extraData.get
|
||||
|
||||
if customData.baseFeePerGas.isSome:
|
||||
customHeader.baseFee = customData.baseFeePerGas.get
|
||||
|
||||
# Return the new payload
|
||||
result = ExecutionPayloadV1(
|
||||
parentHash: Web3BlockHash customHeader.parentHash.data,
|
||||
feeRecipient: Web3Address customHeader.coinbase,
|
||||
stateRoot: Web3BlockHash customHeader.stateRoot.data,
|
||||
receiptsRoot: Web3BlockHash customHeader.receiptRoot.data,
|
||||
logsBloom: Web3Bloom customHeader.bloom,
|
||||
prevRandao: Web3PrevRandao customHeader.mixDigest.data,
|
||||
blockNumber: Web3Quantity customHeader.blockNumber.truncate(uint64),
|
||||
gasLimit: Web3Quantity customHeader.gasLimit,
|
||||
gasUsed: Web3Quantity customHeader.gasUsed,
|
||||
timestamp: Web3Quantity toUnix(customHeader.timestamp),
|
||||
extraData: Web3ExtraData customHeader.extraData,
|
||||
baseFeePerGas: customHeader.baseFee,
|
||||
blockHash: Web3BlockHash customHeader.blockHash.data
|
||||
)
|
||||
|
||||
for tx in txs:
|
||||
let txData = rlp.encode(tx)
|
||||
result.transactions.add TypedTransaction(txData)
|
||||
|
||||
proc hash256*(h: Web3BlockHash): Hash256 =
|
||||
Hash256(data: distinctBase h)
|
||||
|
||||
proc toExecutableData*(payload: ExecutionPayloadV1): ExecutableData =
|
||||
result = ExecutableData(
|
||||
parentHash : hash256(payload.parentHash),
|
||||
feeRecipient : distinctBase payload.feeRecipient,
|
||||
stateRoot : hash256(payload.stateRoot),
|
||||
receiptsRoot : hash256(payload.receiptsRoot),
|
||||
logsBloom : distinctBase payload.logsBloom,
|
||||
prevRandao : hash256(payload.prevRandao),
|
||||
number : uint64 payload.blockNumber,
|
||||
gasLimit : GasInt payload.gasLimit,
|
||||
gasUsed : GasInt payload.gasUsed,
|
||||
timestamp : fromUnix(int64 payload.timestamp),
|
||||
extraData : distinctBase payload.extraData,
|
||||
baseFeePerGas : payload.baseFeePerGas,
|
||||
blockHash : hash256(payload.blockHash)
|
||||
)
|
||||
|
||||
for data in payload.transactions:
|
||||
let tx = rlp.decode(distinctBase data, Transaction)
|
||||
result.transactions.add tx
|
|
@ -0,0 +1 @@
|
|||
9c647b8b7c4e7c3490668fb6c11473619db80c93704c70893d3813af4090c39c
|
|
@ -0,0 +1,151 @@
|
|||
import
|
||||
std/[os, options, json, times, math],
|
||||
eth/[common, keys],
|
||||
eth/trie/db,
|
||||
eth/p2p as eth_p2p,
|
||||
stew/[results, byteutils],
|
||||
stint,
|
||||
json_rpc/[rpcserver, rpcclient],
|
||||
../../../nimbus/[
|
||||
config,
|
||||
genesis,
|
||||
context,
|
||||
constants,
|
||||
transaction,
|
||||
utils,
|
||||
sealer,
|
||||
p2p/chain,
|
||||
db/db_chain,
|
||||
rpc/p2p,
|
||||
rpc/engine_api,
|
||||
sync/protocol_ethxx,
|
||||
utils/tx_pool
|
||||
],
|
||||
../../../tests/test_helpers,
|
||||
"."/[clmock, engine_client]
|
||||
|
||||
import web3/engine_api_types
|
||||
from web3/ethtypes as web3types import nil
|
||||
|
||||
export
|
||||
common, engine_api_types, times,
|
||||
options, results, constants, utils,
|
||||
TypedTransaction, clmock, engine_client
|
||||
|
||||
type
|
||||
EthBlockHeader* = common.BlockHeader
|
||||
|
||||
TestEnv* = ref object
|
||||
conf: NimbusConf
|
||||
ctx: EthContext
|
||||
ethNode: EthereumNode
|
||||
chainDB: BaseChainDB
|
||||
chainRef: Chain
|
||||
rpcServer: RpcSocketServer
|
||||
sealingEngine: SealingEngineRef
|
||||
rpcClient*: RpcSocketClient
|
||||
gHeader*: EthBlockHeader
|
||||
ttd*: DifficultyInt
|
||||
clMock*: CLMocker
|
||||
nonce: uint64
|
||||
vaultKey: PrivateKey
|
||||
|
||||
Web3BlockHash* = web3types.BlockHash
|
||||
Web3Address* = web3types.Address
|
||||
Web3Bloom* = web3types.FixedBytes[256]
|
||||
Web3Quantity* = web3types.Quantity
|
||||
Web3PrevRandao* = web3types.FixedBytes[32]
|
||||
Web3ExtraData* = web3types.DynamicBytes[0, 32]
|
||||
|
||||
const
|
||||
baseFolder = "hive_integration" / "nodocker" / "engine"
|
||||
genesisFile = baseFolder / "genesis.json"
|
||||
sealerKey = baseFolder / "sealer.key"
|
||||
|
||||
# This is the account that sends vault funding transactions.
|
||||
vaultAccountAddr* = hexToByteArray[20]("0xcf49fda3be353c69b41ed96333cd24302da4556f")
|
||||
vaultKeyHex = "63b508a03c3b5937ceb903af8b1b0c191012ef6eb7e9c3fb7afa94e5d214d376"
|
||||
|
||||
proc setupELClient*(t: TestEnv) =
|
||||
t.ctx = newEthContext()
|
||||
let res = t.ctx.am.importPrivateKey(sealerKey)
|
||||
if res.isErr:
|
||||
echo res.error()
|
||||
quit(QuitFailure)
|
||||
|
||||
t.ethNode = setupEthNode(t.conf, t.ctx, eth)
|
||||
t.chainDB = newBaseChainDB(
|
||||
newMemoryDb(),
|
||||
t.conf.pruneMode == PruneMode.Full,
|
||||
t.conf.networkId,
|
||||
t.conf.networkParams
|
||||
)
|
||||
t.chainRef = newChain(t.chainDB)
|
||||
|
||||
initializeEmptyDb(t.chainDB)
|
||||
let txPool = TxPoolRef.new(t.chainDB, t.conf.engineSigner)
|
||||
|
||||
t.rpcServer = newRpcSocketServer(["localhost:" & $t.conf.rpcPort])
|
||||
t.sealingEngine = SealingEngineRef.new(
|
||||
t.chainRef, t.ctx, t.conf.engineSigner,
|
||||
txPool, EngineStopped
|
||||
)
|
||||
|
||||
setupEthRpc(t.ethNode, t.ctx, t.chainDB, txPool, t.rpcServer)
|
||||
setupEngineAPI(t.sealingEngine, t.rpcServer)
|
||||
|
||||
t.sealingEngine.start()
|
||||
t.rpcServer.start()
|
||||
|
||||
t.rpcClient = newRpcSocketClient()
|
||||
waitFor t.rpcClient.connect("localhost", t.conf.rpcPort)
|
||||
t.gHeader = toGenesisHeader(t.conf.networkParams)
|
||||
|
||||
let kRes = PrivateKey.fromHex(vaultKeyHex)
|
||||
if kRes.isErr:
|
||||
echo kRes.error
|
||||
quit(QuitFailure)
|
||||
|
||||
t.vaultKey = kRes.get
|
||||
|
||||
proc setupELClient*(): TestEnv =
|
||||
result = TestEnv(
|
||||
conf: makeConfig(@["--engine-signer:658bdf435d810c91414ec09147daa6db62406379", "--custom-network:" & genesisFile])
|
||||
)
|
||||
setupELClient(result)
|
||||
|
||||
proc stopELClient*(t: TestEnv) =
|
||||
waitFor t.rpcClient.close()
|
||||
waitFor t.sealingEngine.stop()
|
||||
t.rpcServer.stop()
|
||||
waitFor t.rpcServer.closeWait()
|
||||
|
||||
# TTD is the value specified in the TestSpec + Genesis.Difficulty
|
||||
proc setRealTTD*(t: TestEnv, ttdValue: int64) =
|
||||
let realTTD = t.gHeader.difficulty + ttdValue.u256
|
||||
t.chainDB.config.terminalTotalDifficulty = some(realTTD)
|
||||
t.ttd = realTTD
|
||||
t.clmock = newCLMocker(t.rpcClient, realTTD)
|
||||
|
||||
func gwei(n: int): GasInt {.compileTime.} =
|
||||
GasInt(n * (10 ^ 9))
|
||||
|
||||
proc makeNextTransaction*(t: TestEnv, recipient: EthAddress, amount: UInt256, payload: openArray[byte] = []): Transaction =
|
||||
const
|
||||
gasLimit = 75000.GasInt
|
||||
gasPrice = 30.gwei
|
||||
|
||||
let chainId = t.conf.networkParams.config.chainId
|
||||
let tx = Transaction(
|
||||
txType : TxLegacy,
|
||||
chainId : chainId,
|
||||
nonce : AccountNonce(t.nonce),
|
||||
gasPrice: gasPrice,
|
||||
gasLimit: gasLimit,
|
||||
to : some(recipient),
|
||||
value : amount,
|
||||
payload : @payload
|
||||
)
|
||||
|
||||
inc t.nonce
|
||||
signTransaction(tx, t.vaultKey, chainId, eip155 = true)
|
|
@ -165,3 +165,15 @@ func eip1559TxNormalization*(tx: Transaction;
|
|||
result.maxFee = tx.gasPrice
|
||||
if FkLondon <= fork:
|
||||
result.gasPrice = baseFee + min(result.maxPriorityFee, result.maxFee - baseFee)
|
||||
|
||||
func effectiveGasTip*(tx: Transaction; baseFee: Option[UInt256]): GasInt =
|
||||
var
|
||||
maxPriorityFee = tx.maxPriorityFee
|
||||
maxFee = tx.maxFee
|
||||
baseFee = baseFee.get(0.u256).truncate(GasInt)
|
||||
|
||||
if tx.txType < TxEip1559:
|
||||
maxPriorityFee = tx.gasPrice
|
||||
maxFee = tx.gasPrice
|
||||
|
||||
min(maxPriorityFee, maxFee - baseFee)
|
||||
|
|
|
@ -136,7 +136,7 @@ proc parseTransaction*(n: JsonNode): Transaction =
|
|||
n.fromJson "r", tx.R
|
||||
n.fromJson "s", tx.S
|
||||
|
||||
if n["type"].kind != JNull:
|
||||
if n.hasKey("type") and n["type"].kind != JNull:
|
||||
n.fromJson "type", tx.txType
|
||||
|
||||
if tx.txType >= TxEip1559:
|
||||
|
|
Loading…
Reference in New Issue