hive: pyspec simulator implementation

This commit is contained in:
jangko 2023-07-09 09:16:22 +07:00
parent 2446c22138
commit 1eca772a08
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
9 changed files with 395 additions and 40 deletions

View File

@ -13,11 +13,13 @@ ${ENV_SCRIPT} nim ${NIM_FLAGS} ${SIM_DIR}/engine/engine_sim
${ENV_SCRIPT} nim ${NIM_FLAGS} ${SIM_DIR}/consensus/consensus_sim
${ENV_SCRIPT} nim ${NIM_FLAGS} ${SIM_DIR}/graphql/graphql_sim
${ENV_SCRIPT} nim ${NIM_FLAGS} ${SIM_DIR}/rpc/rpc_sim
${ENV_SCRIPT} nim ${NIM_FLAGS} ${SIM_DIR}/pyspec/pyspec_sim
${SIM_DIR}/engine/engine_sim
${SIM_DIR}/consensus/consensus_sim
${SIM_DIR}/graphql/graphql_sim
${SIM_DIR}/rpc/rpc_sim
${SIM_DIR}/pyspec/pyspec_sim
echo "## ${1}" > simulators.md
cat engine.md consensus.md graphql.md rpc.md >> simulators.md
cat engine.md consensus.md graphql.md rpc.md pyspec.md >> simulators.md

View File

@ -0,0 +1,3 @@
import ethtypes, engine_api_types
proc engine_newPayloadV2(payload: ExecutionPayloadV1OrV2): PayloadStatusV1

View File

@ -3,7 +3,7 @@ import
stew/byteutils,
eth/[common, common/eth_types, rlp], chronos,
web3/engine_api_types,
json_rpc/[rpcclient, errors],
json_rpc/[rpcclient, errors, jsonmarshal],
../../../tests/rpcclient/eth_api,
../../../premix/parser,
../../../nimbus/rpc/hexstrings,
@ -13,6 +13,12 @@ import web3/engine_api as web3_engine_api
type Hash256 = eth_types.Hash256
from os import DirSep, AltSep
const
sourceDir = currentSourcePath.rsplit({DirSep, AltSep}, 1)[0]
createRpcSigs(RpcClient, sourceDir & "/engine_callsigs.nim")
template wrapTry(body: untyped) =
try:
body
@ -33,6 +39,13 @@ proc forkchoiceUpdatedV1*(client: RpcClient,
wrapTrySimpleRes:
client.engine_forkchoiceUpdatedV1(update, payloadAttributes)
proc forkchoiceUpdatedV2*(client: RpcClient,
update: ForkchoiceStateV1,
payloadAttributes = none(PayloadAttributesV2)):
Result[ForkchoiceUpdatedResponse, string] =
wrapTrySimpleRes:
client.engine_forkchoiceUpdatedV2(update, payloadAttributes)
proc getPayloadV1*(client: RpcClient, payloadId: PayloadID): Result[ExecutionPayloadV1, string] =
wrapTrySimpleRes:
client.engine_getPayloadV1(payloadId)
@ -49,6 +62,12 @@ proc newPayloadV2*(client: RpcClient,
wrapTrySimpleRes:
client.engine_newPayloadV2(payload)
proc newPayloadV2*(client: RpcClient,
payload: ExecutionPayloadV1OrV2):
Result[PayloadStatusV1, string] =
wrapTrySimpleRes:
client.engine_newPayloadV2(payload)
proc toBlockNumber(n: Option[HexQuantityStr]): common.BlockNumber =
if n.isNone:
return 0.toBlockNumber
@ -181,6 +200,11 @@ proc balanceAt*(client: RpcClient, address: EthAddress): Result[UInt256, string]
let res = waitFor client.eth_getBalance(ethAddressStr(address), "latest")
return ok(UInt256.fromHex(res.string))
proc nonceAt*(client: RpcClient, address: EthAddress): Result[AccountNonce, string] =
wrapTry:
let res = waitFor client.eth_getTransactionCount(ethAddressStr(address), "latest")
return ok(fromHex[AccountNonce](res.string))
proc txReceipt*(client: RpcClient, txHash: Hash256): Result[eth_api.ReceiptObject, string] =
wrapTry:
let res = waitFor client.eth_getTransactionReceipt(txHash)

View File

@ -0,0 +1,58 @@
import
eth/[common],
json_rpc/[rpcclient],
web3/ethtypes,
../../../nimbus/transaction
import eth/common/eth_types as common_eth_types
type Hash256 = common_eth_types.Hash256
import web3/engine_api_types
from web3/ethtypes as web3types import nil
type
Web3BlockHash* = web3types.BlockHash
Web3Address* = web3types.Address
Web3Bloom* = web3types.FixedBytes[256]
Web3Quantity* = web3types.Quantity
Web3PrevRandao* = web3types.FixedBytes[32]
Web3ExtraData* = web3types.DynamicBytes[0, 32]
func toWdV1(wd: Withdrawal): WithdrawalV1 =
result = WithdrawalV1(
index: Web3Quantity wd.index,
validatorIndex: Web3Quantity wd.validatorIndex,
address: Web3Address wd.address,
amount: Web3Quantity wd.amount
)
func toPayloadV1OrV2*(blk: EthBlock): ExecutionPayloadV1OrV2 =
let header = blk.header
# Return the new payload
result = ExecutionPayloadV1OrV2(
parentHash: Web3BlockHash header.parentHash.data,
feeRecipient: Web3Address header.coinbase,
stateRoot: Web3BlockHash header.stateRoot.data,
receiptsRoot: Web3BlockHash header.receiptRoot.data,
logsBloom: Web3Bloom header.bloom,
prevRandao: Web3PrevRandao header.mixDigest.data,
blockNumber: Web3Quantity header.blockNumber.truncate(uint64),
gasLimit: Web3Quantity header.gasLimit,
gasUsed: Web3Quantity header.gasUsed,
timestamp: Web3Quantity toUnix(header.timestamp),
extraData: Web3ExtraData header.extraData,
baseFeePerGas: header.baseFee,
blockHash: Web3BlockHash header.blockHash.data
)
for tx in blk.txs:
let txData = rlp.encode(tx)
result.transactions.add TypedTransaction(txData)
if blk.withdrawals.isSome:
let withdrawals = blk.withdrawals.get
var wds = newSeqOfCap[WithdrawalV1](withdrawals.len)
for wd in withdrawals:
wds.add toWdV1(wd)
result.withdrawals = some(wds)

View File

@ -0,0 +1,167 @@
# Nimbus
# Copyright (c) 2023 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/[os, json, strutils, times, typetraits, options],
stew/[byteutils, results],
eth/common,
web3/engine_api_types,
../sim_utils,
../../../tools/common/helpers as chp,
../../../tools/evmstate/helpers as ehp,
../../../tests/test_helpers,
../engine/engine_client,
./test_env,
./helpers
const
baseFolder = "hive_integration/nodocker/pyspec"
caseFolder = baseFolder & "/testcases"
supportedNetwork = ["Merge", "Shanghai", "MergeToShanghaiAtTime15k"]
type
Hash256 = common.Hash256
proc getPayload(node: JsonNode): ExecutionPayloadV1OrV2 =
let rlpBytes = hexToSeqByte(node.getStr)
toPayloadV1OrV2(rlp.decode(rlpBytes, EthBlock))
proc hash256(h: Web3BlockHash): Hash256 =
Hash256(data: distinctBase h)
proc validatePostState(node: JsonNode, t: TestEnv): bool =
# check nonce, balance & storage of accounts in final block against fixture values
for account, genesisAccount in postState(node["postState"]):
# get nonce & balance from last block (end of test execution)
let nonceRes = t.rpcClient.nonceAt(account)
if nonceRes.isErr:
echo "unable to call nonce from account: " & account.toHex
echo nonceRes.error
return false
let balanceRes = t.rpcClient.balanceAt(account)
if balanceRes.isErr:
echo "unable to call balance from account: " & account.toHex
echo balanceRes.error
return false
# check final nonce & balance matches expected in fixture
if genesisAccount.nonce != nonceRes.value:
echo "nonce recieved from account 0x",
account.toHex,
" doesn't match expected ",
genesisAccount.nonce,
" got ",
nonceRes.value
return false
if genesisAccount.balance != balanceRes.value:
echo "balance recieved from account 0x",
account.toHex,
" doesn't match expected ",
genesisAccount.balance,
" got ",
balanceRes.value
return false
# check final storage
if genesisAccount.storage.len > 0:
for slot, val in genesisAccount.storage:
let sRes = t.rpcClient.storageAt(account, slot)
if sRes.isErr:
echo "unable to call storage from account: 0x",
account.toHex,
" at slot 0x",
slot.toHex
echo sRes.error
return false
if val != sRes.value:
echo "storage recieved from account 0x",
account.toHex,
" at slot 0x",
slot.toHex,
" doesn't match expected 0x",
val.toHex,
" got 0x",
sRes.value.toHex
return false
return true
proc runTest(node: JsonNode, network: string): TestStatus =
let conf = getChainConfig(network)
var t = TestEnv(conf: makeTestConfig())
t.setupELClient(conf, node)
let blks = node["blocks"]
var latestValidHash = Hash256()
result = TestStatus.OK
for blkNode in blks:
let expectedStatus = if "expectException" in blkNode:
PayloadExecutionStatus.invalid
else:
PayloadExecutionStatus.valid
let payload = getPayload(blkNode["rlp"])
let res = t.rpcClient.newPayloadV2(payload)
if res.isErr:
result = TestStatus.Failed
echo "unable to send block ", payload.blockNumber.uint64, ": ", res.error
break
let pStatus = res.value
if pStatus.status == PayloadExecutionStatus.valid:
latestValidHash = hash256(pStatus.latestValidHash.get)
if pStatus.status != expectedStatus:
result = TestStatus.Failed
echo "payload status mismatch for block ", payload.blockNumber.uint64, ", status: ", pStatus.status
if pStatus.validationError.isSome:
echo pStatus.validationError.get
break
block:
# only update head of beacon chain if valid response occurred
if latestValidHash != Hash256():
# update with latest valid response
let fcState = ForkchoiceStateV1(headBlockHash: BlockHash latestValidHash.data)
let res = t.rpcClient.forkchoiceUpdatedV2(fcState)
if res.isErr:
result = TestStatus.Failed
echo "unable to update head of beacon chain: ", res.error
break
if not validatePostState(node, t):
result = TestStatus.Failed
break
t.stopELClient()
proc main() =
var stat: SimStat
let start = getTime()
for fileName in walkDirRec(caseFolder):
if not fileName.endsWith(".json"):
continue
let fixtureTests = json.parseFile(fileName)
for name, fixture in fixtureTests:
let network = fixture["network"].getStr
if network notin supportedNetwork:
# skip pre Merge tests
continue
let status = runTest(fixture, network)
stat.inc(name, status)
let elpd = getTime() - start
print(stat, elpd, "pyspec")
main()

View File

@ -0,0 +1,84 @@
import
std/[json],
eth/p2p as eth_p2p,
eth/trie/trie_defs,
stew/[byteutils],
json_rpc/[rpcserver, rpcclient],
../../../nimbus/[
config,
constants,
transaction,
db/accounts_cache,
core/sealer,
core/chain,
core/tx_pool,
rpc,
sync/protocol,
rpc/merge/merger,
common
],
../../../tests/test_helpers,
../../../tools/evmstate/helpers
type
TestEnv* = ref object
conf*: NimbusConf
ctx: EthContext
ethNode: EthereumNode
com: CommonRef
chainRef: ChainRef
rpcServer: RpcHttpServer
sealingEngine: SealingEngineRef
rpcClient*: RpcHttpClient
const
engineSigner = hexToByteArray[20]("0x658bdf435d810c91414ec09147daa6db62406379")
proc genesisHeader(node: JsonNode): BlockHeader =
let genesisRLP = hexToSeqByte(node["genesisRLP"].getStr)
rlp.decode(genesisRLP, EthBlock).header
proc setupELClient*(t: TestEnv, conf: ChainConfig, node: JsonNode) =
let memDB = newMemoryDb()
t.ctx = newEthContext()
t.ethNode = setupEthNode(t.conf, t.ctx, eth)
t.com = CommonRef.new(
memDB,
conf,
t.conf.pruneMode == PruneMode.Full
)
t.chainRef = newChain(t.com)
let
stateDB = AccountsCache.init(memDB, emptyRlpHash, t.conf.pruneMode == PruneMode.Full)
genesisHeader = node.genesisHeader
setupStateDB(node["pre"], stateDB)
stateDB.persist()
doAssert stateDB.rootHash == genesisHeader.stateRoot
discard t.com.db.persistHeaderToDb(genesisHeader,
t.com.consensus == ConsensusType.POS)
doAssert(t.com.db.getCanonicalHead().blockHash == genesisHeader.blockHash)
let txPool = TxPoolRef.new(t.com, engineSigner)
t.rpcServer = newRpcHttpServer(["localhost:8545"])
t.sealingEngine = SealingEngineRef.new(
t.chainRef, t.ctx, engineSigner,
txPool, EngineStopped
)
let merger = MergerRef.new(t.com.db)
setupEthRpc(t.ethNode, t.ctx, t.com, txPool, t.rpcServer)
setupEngineAPI(t.sealingEngine, t.rpcServer, merger)
#setupDebugRpc(t.com, t.rpcServer)
t.rpcServer.start()
t.rpcClient = newRpcHttpClient()
waitFor t.rpcClient.connect("localhost", 8545.Port, false)
proc stopELClient*(t: TestEnv) =
waitFor t.rpcClient.close()
waitFor t.sealingEngine.stop()
waitFor t.rpcServer.closeWait()

View File

@ -108,7 +108,7 @@ proc handle_newPayload(sealingEngine: SealingEngineRef, api: EngineApiRef, com:
else:
raise invalidParams("if timestamp is earlier than Shanghai, " &
"payload must be ExecutionPayloadV1")
var header = toBlockHeader(payload)
let blockHash = payload.blockHash.asEthHash
var res = header.validateBlockHash(blockHash)
@ -170,10 +170,11 @@ proc handle_newPayload(sealingEngine: SealingEngineRef, api: EngineApiRef, com:
td = db.getScore(header.parentHash)
ttd = com.ttd.get(high(common.BlockNumber))
if td < ttd:
warn "Ignoring pre-merge payload",
number = header.blockNumber, hash = blockHash, td, ttd
return invalidStatus()
when payload is ExecutionPayloadV1:
if (not com.forkGTE(MergeFork)) and td < ttd:
warn "Ignoring pre-merge payload",
number = header.blockNumber, hash = blockHash, td, ttd
return invalidStatus()
if header.timestamp <= parent.timestamp:
warn "Invalid timestamp",
@ -204,7 +205,7 @@ proc handle_newPayload(sealingEngine: SealingEngineRef, api: EngineApiRef, com:
# TODO: cancel downloader
return validStatus(blockHash)
# https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md#engine_getpayloadv1
proc handle_getPayload(api: EngineApiRef, payloadId: PayloadID): GetPayloadV2Response {.raises: [CatchableError].} =
trace "Engine API request received",
@ -215,7 +216,7 @@ proc handle_getPayload(api: EngineApiRef, payloadId: PayloadID): GetPayloadV2Res
raise unknownPayload("Unknown payload")
let blockValue = sumOfBlockPriorityFees(payload)
return GetPayloadV2Response(
executionPayload: payload,
blockValue: blockValue
@ -426,7 +427,7 @@ proc handle_forkchoiceUpdated(sealingEngine: SealingEngineRef, com: CommonRef, a
if res.isErr:
error "Failed to create sealing payload", err = res.error
raise invalidAttr(res.error)
let payload = res.get
let id = computePayloadId(blockHash, payloadAttrs)

View File

@ -15,6 +15,7 @@ import
eth/[trie, rlp, common, common/eth_types, trie/db],
stew/[results, byteutils],
../../constants,
../../utils/utils,
./mergetypes
type Hash256 = eth_types.Hash256
@ -55,19 +56,40 @@ proc calcRootHashRlp*(items: openArray[seq[byte]]): Hash256 =
tr.put(rlp.encode(i), t)
return tr.rootHash()
proc calcWithdrawalsRoot(withdrawals: seq[WithdrawalV1]): Hash256 =
calcRootHashRlp(withdrawals.map(writer.encode))
proc toWithdrawal*(w: WithdrawalV1): Withdrawal =
Withdrawal(
index: uint64(w.index),
validatorIndex: uint64(w.validatorIndex),
address: distinctBase(w.address),
amount: uint64(w.amount) # AARDVARK: is this wei or gwei or what?
)
func maybeWithdrawals*(payload: ExecutionPayloadV1 | ExecutionPayloadV2): Option[seq[WithdrawalV1]] =
proc toWithdrawalV1*(w: Withdrawal): WithdrawalV1 =
WithdrawalV1(
index: Quantity(w.index),
validatorIndex: Quantity(w.validatorIndex),
address: Address(w.address),
amount: Quantity(w.amount) # AARDVARK: is this wei or gwei or what?
)
proc maybeWithdrawalsRoot(payload: ExecutionPayloadV1 | ExecutionPayloadV2): Option[Hash256] =
when payload is ExecutionPayloadV1:
none[seq[WithdrawalV1]]()
none(Hash256)
else:
some(payload.withdrawals)
var wds = newSeqOfCap[Withdrawal](payload.withdrawals.len)
for wd in payload.withdrawals:
wds.add toWithdrawal(wd)
some(utils.calcWithdrawalsRoot(wds))
proc toWithdrawals(withdrawals: openArray[WithdrawalV1]): seq[WithDrawal] =
result = newSeqOfCap[Withdrawal](withdrawals.len)
for wd in withdrawals:
result.add toWithdrawal(wd)
proc toBlockHeader*(payload: ExecutionPayloadV1 | ExecutionPayloadV2): EthBlockHeader =
let transactions = seq[seq[byte]](payload.transactions)
let txRoot = calcRootHashRlp(transactions)
EthBlockHeader(
parentHash : payload.parentHash.asEthHash,
ommersHash : EMPTY_UNCLE_HASH,
@ -85,23 +107,7 @@ proc toBlockHeader*(payload: ExecutionPayloadV1 | ExecutionPayloadV2): EthBlockH
mixDigest : payload.prevRandao.asEthHash, # EIP-4399 redefine `mixDigest` -> `prevRandao`
nonce : default(BlockNonce),
fee : some payload.baseFeePerGas,
withdrawalsRoot: payload.maybeWithdrawals.map(calcWithdrawalsRoot) # EIP-4895
)
proc toWithdrawal*(w: WithdrawalV1): Withdrawal =
Withdrawal(
index: uint64(w.index),
validatorIndex: uint64(w.validatorIndex),
address: distinctBase(w.address),
amount: uint64(w.amount) # AARDVARK: is this wei or gwei or what?
)
proc toWithdrawalV1*(w: Withdrawal): WithdrawalV1 =
WithdrawalV1(
index: Quantity(w.index),
validatorIndex: Quantity(w.validatorIndex),
address: Address(w.address),
amount: Quantity(w.amount) # AARDVARK: is this wei or gwei or what?
withdrawalsRoot: payload.maybeWithdrawalsRoot # EIP-4895
)
proc toTypedTransaction*(tx: Transaction): TypedTransaction =
@ -112,12 +118,7 @@ proc toBlockBody*(payload: ExecutionPayloadV1 | ExecutionPayloadV2): BlockBody =
for i, tx in payload.transactions:
result.transactions[i] = rlp.decode(distinctBase tx, Transaction)
when payload is ExecutionPayloadV2:
let ws = payload.maybeWithdrawals
result.withdrawals =
if ws.isSome:
some(ws.get.map(toWithdrawal))
else:
none[seq[Withdrawal]]()
result.withdrawals = some(payload.withdrawals.toWithdrawals)
proc `$`*(x: BlockHash): string =
toHex(x)

View File

@ -15,7 +15,8 @@ import
stint,
stew/byteutils,
../../nimbus/transaction,
../../nimbus/db/accounts_cache
../../nimbus/db/accounts_cache,
../../nimbus/common/chain_config
template fromJson(T: type EthAddress, n: JsonNode): EthAddress =
hexToByteArray(n.getStr, sizeof(T))
@ -146,3 +147,17 @@ proc setupStateDB*(wantedState: JsonNode, stateDB: AccountsCache) =
stateDB.setNonce(account, fromJson(AccountNonce, accountData["nonce"]))
stateDB.setCode(account, fromJson(Blob, accountData["code"]))
stateDB.setBalance(account, fromJson(UInt256, accountData["balance"]))
iterator postState*(node: JsonNode): (EthAddress, GenesisAccount) =
for ac, accountData in node:
let account = hexToByteArray[20](ac)
var ga = GenesisAccount(
nonce : fromJson(AccountNonce, accountData["nonce"]),
code : fromJson(Blob, accountData["code"]),
balance: fromJson(UInt256, accountData["balance"]),
)
for slot, value in accountData{"storage"}:
ga.storage[fromHex(UInt256, slot)] = fromHex(UInt256, value.getStr)
yield (account, ga)