From ec59819953ba802e175829726d086f5f8b96d367 Mon Sep 17 00:00:00 2001 From: jangko Date: Wed, 26 Oct 2022 22:46:13 +0700 Subject: [PATCH] implement evmstate CLI for evmlab --- Makefile | 9 + tools/evmstate/config.nim | 80 ++++++++ tools/evmstate/evmstate.nim | 309 +++++++++++++++++++++++++++++++ tools/evmstate/evmstate_test.nim | 57 ++++++ tools/evmstate/helpers.nim | 164 ++++++++++++++++ tools/evmstate/readme.md | 49 +++++ 6 files changed, 668 insertions(+) create mode 100644 tools/evmstate/config.nim create mode 100644 tools/evmstate/evmstate.nim create mode 100644 tools/evmstate/evmstate_test.nim create mode 100644 tools/evmstate/helpers.nim create mode 100644 tools/evmstate/readme.md diff --git a/Makefile b/Makefile index b986a7528..fb17f2003 100644 --- a/Makefile +++ b/Makefile @@ -245,10 +245,19 @@ t8n: | build deps t8n_test: | build deps t8n $(ENV_SCRIPT) nim c -r $(NIM_PARAMS) -d:chronicles_default_output_device=stderr "tools/t8n/$@.nim" +# builds evm state test tool +evmstate: | build deps + $(ENV_SCRIPT) nim c $(NIM_PARAMS) -d:chronicles_enabled=off "tools/evmstate/$@.nim" + +# builds and runs evm state tool test suite +evmstate_test: | build deps evmstate + $(ENV_SCRIPT) nim c -r $(NIM_PARAMS) "tools/evmstate/$@.nim" + # usual cleaning clean: | clean-common rm -rf build/{nimbus,fluffy,lc_proxy,$(TOOLS_CSV),all_tests,test_kvstore_rocksdb,test_rpc,all_fluffy_tests,all_fluffy_portal_spec_tests,test_portal_testnet,portalcli,blockwalk,eth_data_exporter,utp_test_app,utp_test,*.dSYM} rm -rf tools/t8n/{t8n,t8n_test} + rm -rf tools/evmstate/{evmstate,evmstate_test} ifneq ($(USE_LIBBACKTRACE), 0) + $(MAKE) -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT) endif diff --git a/tools/evmstate/config.nim b/tools/evmstate/config.nim new file mode 100644 index 000000000..182e97826 --- /dev/null +++ b/tools/evmstate/config.nim @@ -0,0 +1,80 @@ +import + std/[os, options], + confutils, confutils/defs + +export + confutils, defs + +type + StateConf* = object of RootObj + dumpEnabled* {. + desc: "dumps the state after the run" + defaultValue: false + name: "dump" }: bool + + jsonEnabled* {. + desc: "output trace logs in machine readable format (json)" + defaultValue: false + name: "json" }: bool + + debugEnabled* {. + desc: "output full trace logs" + defaultValue: false + name: "debug" }: bool + + disableMemory* {. + desc: "disable memory output" + defaultValue: true + name: "nomemory" }: bool + + disableStack* {. + desc: "disable stack output" + defaultValue: false + name: "nostack" }: bool + + disableStorage* {. + desc: "disable storage output" + defaultValue: false + name: "nostorage" }: bool + + disableReturnData* {. + desc: "enable return data output" + defaultValue: true + name: "noreturndata" }: bool + + fork* {. + desc: "choose which fork to be tested" + defaultValue: "" + name: "fork" }: string + + index* {. + desc: "if index is unset, all subtest in the fork will be tested" + defaultValue: none(int) + name: "index" }: Option[int] + + pretty* {. + desc: "pretty print the trace result" + defaultValue: false + name: "pretty" }: bool + + verbosity* {. + desc: "sets the verbosity level" + defaultValue: 0 + name: "verbosity" }: int + + inputFile* {. + desc: "json file contains state test data" + argument }: string + +const + Copyright = "Copyright (c) 2022 Status Research & Development GmbH" + Version = "Nimbus-evmstate 0.1.0" + +proc init*(_: type StateConf, cmdLine = commandLineParams()): StateConf = + {.push warning[ProveInit]: off.} + result = StateConf.load( + cmdLine, + version = Version, + copyrightBanner = Version & "\n" & Copyright + ) + {.pop.} diff --git a/tools/evmstate/evmstate.nim b/tools/evmstate/evmstate.nim new file mode 100644 index 000000000..0dcaa1d9f --- /dev/null +++ b/tools/evmstate/evmstate.nim @@ -0,0 +1,309 @@ +import + std/[json, strutils, sets, tables, options], + eth/[common, keys], + stew/[results, byteutils], + stint, + eth/trie/[db, trie_defs], + ../../nimbus/[forks, vm_types, chain_config, vm_state], + ../../nimbus/db/[db_chain, accounts_cache], + ../../nimbus/transaction, + ../../nimbus/p2p/executor, + "."/[config, helpers] + +type + StateContext = object + name: string + header: BlockHeader + tx: Transaction + expectedHash: Hash256 + expectedLogs: Hash256 + fork: Fork + index: int + tracerFlags: set[TracerFlags] + error: string + + DumpAccount = ref object + balance : UInt256 + nonce : AccountNonce + root : Hash256 + codeHash: Hash256 + code : Blob + key : Hash256 + storage : Table[UInt256, UInt256] + + StateDump = ref object + root: Hash256 + accounts: Table[EthAddress, DumpAccount] + + StateResult = object + name : string + pass : bool + fork : string + error: string + state: StateDump + +proc extractNameAndFixture(ctx: var StateContext, n: JsonNode): JsonNode = + for label, child in n: + result = child + ctx.name = label + return + doAssert(false, "unreachable") + +proc parseTx(txData, index: JsonNode): Transaction = + let + dataIndex = index["data"].getInt + gasIndex = index["gas"].getInt + valIndex = index["value"].getInt + parseTx(txData, dataIndex, gasIndex, valIndex) + +proc toBytes(x: string): seq[byte] = + result = newSeq[byte](x.len) + for i in 0..= vmState.blockNumber: + return + elif blockNumber < 0: + return + elif blockNumber < vmState.blockNumber - 256: + return + else: + return keccakHash(toBytes($blockNumber)) + +proc verifyResult(ctx: var StateContext, vmState: BaseVMState) = + ctx.error = "" + let obtainedHash = vmState.readOnlyStateDB.rootHash + if obtainedHash != ctx.expectedHash: + ctx.error = "post state root mismatch: got $1, want $2" % + [$obtainedHash, $ctx.expectedHash] + return + + let logEntries = vmState.getAndClearLogEntries() + let actualLogsHash = rlpHash(logEntries) + if actualLogsHash != ctx.expectedLogs: + ctx.error = "post state log hash mismatch: got $1, want $2" % + [$actualLogsHash, $ctx.expectedLogs] + return + +proc `%`(x: UInt256): JsonNode = + %("0x" & x.toHex) + +proc `%`(x: Blob): JsonNode = + %("0x" & x.toHex) + +proc `%`(x: Hash256): JsonNode = + %("0x" & x.data.toHex) + +proc `%`(x: AccountNonce): JsonNode = + %("0x" & x.toHex) + +proc `%`(x: Table[UInt256, UInt256]): JsonNode = + result = newJObject() + for k, v in x: + result["0x" & k.toHex] = %(v) + +proc `%`(x: DumpAccount): JsonNode = + result = %{ + "balance" : %(x.balance), + "nonce" : %(x.nonce), + "root" : %(x.root), + "codeHash": %(x.codeHash), + "code" : %(x.code), + "key" : %(x.key) + } + if x.storage.len > 0: + result["storage"] = %(x.storage) + +proc `%`(x: Table[EthAddress, DumpAccount]): JsonNode = + result = newJObject() + for k, v in x: + result["0x" & k.toHex] = %(v) + +proc `%`(x: StateDump): JsonNode = + result = %{ + "root": %(x.root), + "accounts": %(x.accounts) + } + +proc writeResultToStdout(stateRes: seq[StateResult]) = + var n = newJArray() + for res in stateRes: + let z = %{ + "name" : %(res.name), + "pass" : %(res.pass), + "fork" : %(res.fork), + "error": %(res.error) + } + if res.state.isNil.not: + z["state"] = %(res.state) + n.add(z) + + stdout.write(n.pretty) + stdout.write("\n") + +proc dumpAccounts(db: AccountsCache): Table[EthAddress, DumpAccount] = + for accAddr in db.addresses(): + let acc = DumpAccount( + balance : db.getBalance(accAddr), + nonce : db.getNonce(accAddr), + root : db.getStorageRoot(accAddr), + codeHash: db.getCodeHash(accAddr), + code : db.getCode(accAddr), + key : keccakHash(accAddr) + ) + for k, v in db.storage(accAddr): + acc.storage[k] = v + result[accAddr] = acc + +proc dumpState(vmState: BaseVMState): StateDump = + StateDump( + root: vmState.readOnlyStateDB.rootHash, + accounts: dumpAccounts(vmState.stateDB) + ) + +proc writeTraceToStderr(vmState: BaseVMState, pretty: bool) = + let trace = vmState.getTracingResult() + if pretty: + stderr.writeLine(trace.pretty) + else: + let logs = trace["structLogs"] + trace.delete("structLogs") + for x in logs: + stderr.writeLine($x) + stderr.writeLine($trace) + +proc runExecution(ctx: var StateContext, conf: StateConf, pre: JsonNode): StateResult = + let + chainParams = NetworkParams(config: chainConfigForNetwork(MainNet)) + chainDB = newBaseChainDB(newMemoryDB(), pruneTrie = false, params = chainParams) + parent = BlockHeader(stateRoot: emptyRlpHash) + + # set total difficulty + chainDB.setScore(parent.blockHash, 0.u256) + + if ctx.fork >= FkParis: + chainDB.config.terminalTotalDifficulty = some(0.u256) + + let vmState = BaseVMState.new( + parent = parent, + header = ctx.header, + chainDB = chainDB, + tracerFlags = ctx.tracerFlags, + pruneTrie = chainDB.pruneTrie) + + var gasUsed: GasInt + let sender = ctx.tx.getSender() + + vmState.mutateStateDB: + setupStateDB(pre, db) + db.persist() # settle accounts storage + + defer: + ctx.verifyResult(vmState) + result = StateResult( + name : ctx.name, + pass : ctx.error.len == 0, + fork : toString(ctx.fork), + error: ctx.error + ) + if conf.dumpEnabled: + result.state = dumpState(vmState) + if conf.jsonEnabled: + writeTraceToStderr(vmState, conf.pretty) + + let rc = vmState.processTransaction( + ctx.tx, sender, ctx.header, ctx.fork) + if rc.isOk: + gasUsed = rc.value + + let miner = ctx.header.coinbase + if miner in vmState.selfDestructs: + vmState.mutateStateDB: + db.addBalance(miner, 0.u256) + if ctx.fork >= FkSpurious: + if db.isEmptyAccount(miner): + db.deleteAccount(miner) + db.persist() + +proc toTracerFlags(conf: Stateconf): set[TracerFlags] = + result = { + TracerFlags.DisableStateDiff, + TracerFlags.EnableTracing + } + + if conf.disableMemory : result.incl TracerFlags.DisableMemory + if conf.disablestack : result.incl TracerFlags.DisableStack + if conf.disableReturnData: result.incl TracerFlags.DisableReturnData + if conf.disableStorage : result.incl TracerFlags.DisableStorage + +template hasError(ctx: StateContext): bool = + ctx.error.len > 0 + +proc prepareAndRun(ctx: var StateContext, conf: StateConf): bool = + let + fixture = json.parseFile(conf.inputFile) + n = ctx.extractNameAndFixture(fixture) + txData = n["transaction"] + post = n["post"] + pre = n["pre"] + + ctx.header = parseHeader(n["env"]) + + if conf.debugEnabled or conf.jsonEnabled: + ctx.tracerFlags = toTracerFlags(conf) + + var + stateRes = newSeqOfCap[StateResult](post.len) + index = 1 + hasError = false + + template prepareFork(forkName: string) = + let fork = parseFork(forkName) + doAssert(fork.isSome, "unsupported fork: " & forkName) + ctx.fork = fork.get() + ctx.index = index + inc index + + template runSubTest(subTest: JsonNode) = + ctx.expectedHash = Hash256.fromJson(subTest["hash"]) + ctx.expectedLogs = Hash256.fromJson(subTest["logs"]) + ctx.tx = parseTx(txData, subTest["indexes"]) + let res = ctx.runExecution(conf, pre) + stateRes.add res + hasError = hasError or ctx.hasError + + if conf.fork.len > 0: + if not post.hasKey(conf.fork): + stdout.writeLine("selected fork not available: " & conf.fork) + return false + + let forkData = post[conf.fork] + prepareFork(conf.fork) + if conf.index.isNone: + for subTest in forkData: + runSubTest(subTest) + else: + let index = conf.index.get() + if index > forkData.len or index < 0: + stdout.writeLine("selected index out of range(0-$1), requested $2" % + [$forkData.len, $index]) + return false + + let subTest = forkData[index] + runSubTest(subTest) + else: + for forkName, forkData in post: + prepareFork(forkName) + for subTest in forkData: + runSubTest(subTest) + + writeResultToStdout(stateRes) + not hasError + +proc main() = + let conf = StateConf.init() + var ctx: StateContext + if not ctx.prepareAndRun(conf): + quit(QuitFailure) + +main() diff --git a/tools/evmstate/evmstate_test.nim b/tools/evmstate/evmstate_test.nim new file mode 100644 index 000000000..3e2742af0 --- /dev/null +++ b/tools/evmstate/evmstate_test.nim @@ -0,0 +1,57 @@ +import + std/[os, osproc, strutils, tables], + unittest2, + testutils/markdown_reports, + ../../tests/test_allowed_to_fail + +const + inputFolder = "tests" / "fixtures" / "eth_tests" / "GeneralStateTests" + +proc runTest(filename: string): bool = + let appDir = getAppDir() + let cmd = appDir / ("evmstate " & filename) + let (res, exitCode) = execCmdEx(cmd) + if exitCode != QuitSuccess: + echo res + return false + + true + +template skipTest(folder, name: untyped): bool = + skipNewGSTTests(folder, name) + +proc main() = + suite "evmstate test suite": + var status = initOrderedTable[string, OrderedTable[string, Status]]() + var filenames: seq[string] = @[] + for filename in walkDirRec(inputFolder): + if not filename.endsWith(".json"): + continue + + let (folder, name) = filename.splitPath() + let last = folder.splitPath().tail + if not status.hasKey(last): + status[last] = initOrderedTable[string, Status]() + status[last][name] = Status.Skip + if skipTest(last, name): + continue + + filenames.add filename + + for inputFile in filenames: + let testName = substr(inputFile, inputFolder.len+1) + test testName: + let (folder, name) = inputFile.splitPath() + let last = folder.splitPath().tail + status[last][name] = Status.Fail + let res = runTest(inputFile) + check true == res + if res: + status[last][name] = Status.OK + + status.sort do (a: (string, OrderedTable[string, Status]), + b: (string, OrderedTable[string, Status])) -> int: cmp(a[0], b[0]) + + generateReport("evmstate", status) + +main() diff --git a/tools/evmstate/helpers.nim b/tools/evmstate/helpers.nim new file mode 100644 index 000000000..4e76d660f --- /dev/null +++ b/tools/evmstate/helpers.nim @@ -0,0 +1,164 @@ +import + std/[options, json, strutils], + eth/[common, keys], + eth/trie/trie_defs, + stint, + stew/byteutils, + ../../nimbus/[transaction, forks], + ../../nimbus/db/accounts_cache + +template fromJson(T: type EthAddress, n: JsonNode): EthAddress = + hexToByteArray(n.getStr, sizeof(T)) + +proc fromJson(T: type UInt256, n: JsonNode): UInt256 = + # stTransactionTest/ValueOverflow.json + # prevent parsing exception and subtitute it with max uint256 + let hex = n.getStr + if ':' in hex: + high(UInt256) + else: + UInt256.fromHex(hex) + +template fromJson*(T: type Hash256, n: JsonNode): Hash256 = + Hash256(data: hexToByteArray(n.getStr, 32)) + +proc fromJson(T: type Blob, n: JsonNode): Blob = + let hex = n.getStr + if hex.len == 0: + @[] + else: + hexToSeqByte(hex) + +template fromJson(T: type GasInt, n: JsonNode): GasInt = + fromHex[GasInt](n.getStr) + +template fromJson(T: type AccountNonce, n: JsonNode): AccountNonce = + fromHex[AccountNonce](n.getStr) + +template fromJson(T: type EthTime, n: JsonNode): EthTime = + fromUnix(fromHex[int64](n.getStr)) + +proc fromJson(T: type PrivateKey, n: JsonNode): PrivateKey = + var secretKey = n.getStr + removePrefix(secretKey, "0x") + PrivateKey.fromHex(secretKey).tryGet() + +proc fromJson(T: type AccessList, n: JsonNode): AccessList = + if n.kind == JNull: + return + + for x in n: + var ap = AccessPair( + address: EthAddress.fromJson(x["address"]) + ) + let sks = x["storageKeys"] + for sk in sks: + ap.storageKeys.add hexToByteArray(sk.getStr, 32) + result.add ap + +template required(T: type, nField: string): auto = + fromJson(T, n[nField]) + +template required(T: type, nField: string, index: int): auto = + fromJson(T, n[nField][index]) + +template omitZero(T: type, nField: string): auto = + if n.hasKey(nField): + fromJson(T, n[nField]) + else: + default(T) + +template omitZero(T: type, nField: string, index: int): auto = + if n.hasKey(nField): + fromJson(T, n[nField][index]) + else: + default(T) + +template optional(T: type, nField: string): auto = + if n.hasKey(nField): + some(T.fromJson(n[nField])) + else: + none(T) + +proc txType(n: JsonNode): TxType = + if "gasPrice" notin n: + return TxEip1559 + if "accessLists" in n: + return TxEip2930 + TxLegacy + +proc parseHeader*(n: JsonNode): BlockHeader = + BlockHeader( + coinbase : required(EthAddress, "currentCoinbase"), + difficulty : required(DifficultyInt, "currentDifficulty"), + blockNumber: required(BlockNumber, "currentNumber"), + gasLimit : required(GasInt, "currentGasLimit"), + timestamp : required(EthTime, "currentTimestamp"), + stateRoot : emptyRlpHash, + mixDigest : omitZero(Hash256, "currentRandom"), + fee : optional(UInt256, "currentBaseFee") + ) + +proc parseTx*(n: JsonNode, dataIndex, gasIndex, valueIndex: int): Transaction = + var tx = Transaction( + txType : txType(n), + nonce : required(AccountNonce, "nonce"), + gasLimit: required(GasInt, "gasLimit", gasIndex), + value : required(UInt256, "value", valueIndex), + payload : required(Blob, "data", dataIndex), + chainId : ChainId(1), + gasPrice: omitZero(GasInt, "gasPrice"), + maxFee : omitZero(GasInt, "maxFeePerGas"), + accessList: omitZero(AccessList, "accessLists", dataIndex), + maxPriorityFee: omitZero(GasInt, "maxPriorityFeePerGas") + ) + + let rawTo = n["to"].getStr + if rawTo != "": + tx.to = some(hexToByteArray(rawTo, 20)) + + let secretKey = required(PrivateKey, "secretKey") + signTransaction(tx, secretKey, tx.chainId, false) + +proc setupStateDB*(wantedState: JsonNode, stateDB: AccountsCache) = + for ac, accountData in wantedState: + let account = hexToByteArray[20](ac) + for slot, value in accountData{"storage"}: + stateDB.setStorage(account, fromHex(UInt256, slot), fromHex(UInt256, value.getStr)) + + stateDB.setNonce(account, fromJson(AccountNonce, accountData["nonce"])) + stateDB.setCode(account, fromJson(Blob, accountData["code"])) + stateDB.setBalance(account, fromJson(UInt256, accountData["balance"])) + +proc parseFork*(x: string): Option[Fork] = + case x + of "Frontier" : some(FkFrontier) + of "Homestead" : some(FkHomestead) + of "EIP150" : some(FkTangerine) + of "EIP158" : some(FkSpurious) + of "Byzantium" : some(FkByzantium) + of "Constantinople" : some(FkConstantinople) + of "ConstantinopleFix": some(FkPetersburg) + of "Istanbul" : some(FkIstanbul) + of "Berlin" : some(FkBerlin) + of "London" : some(FkLondon) + of "Merge" : some(FkParis) + of "Shanghai" : some(FkShanghai) + of "Cancun" : some(FkCancun) + else: none(Fork) + +proc toString*(x: Fork): string = + case x + of FkFrontier : "Frontier" + of FkHomestead : "Homestead" + of FkTangerine : "EIP150" + of FkSpurious : "EIP158" + of FkByzantium : "Byzantium" + of FkConstantinople: "Constantinople" + of FkPetersburg : "ConstantinopleFix" + of FkIstanbul : "Istanbul" + of FkBerlin : "Berlin" + of FkLondon : "London" + of FkParis : "Merge" + of FkShanghai : "Shanghai" + of FkCancun : "Cancun" diff --git a/tools/evmstate/readme.md b/tools/evmstate/readme.md new file mode 100644 index 000000000..dd890d58f --- /dev/null +++ b/tools/evmstate/readme.md @@ -0,0 +1,49 @@ +## EVM state test tool + +The `evmstate` tool to execute state test. + +### Build instructions + +There are few options to build `evmstate` tool like any other nimbus tools. + +1. Use nimble to install dependencies and your system Nim compiler(version <= 1.6.0). + ``` + $> nimble install -y --depsOnly + $> nim c -d:release -d:chronicles_enabled=off tools/evmstate/evmstate + $> nim c -r -d:release tools/evmstate/evmstate_test + ``` +2. Use nimbus shipped Nim compiler and dependencies. + ``` + $> make update + $> ./env.sh nim c -d:release -d:chronicles_enabled=off tools/evmstate/evmstate + $> ./env.sh nim c -r -d:release tools/evmstate/evmstate_test + ``` +3. Use nimbus makefile. + ``` + $> make update + $> make evmstate + $> make evmstate_test + ``` + +### Command line params + +Available command line params +``` +Usage: + +evmstate [OPTIONS]... + + json file contains state test data. + +The following options are available: + + --dump dumps the state after the run [=false]. + --json output trace logs in machine readable format (json) [=false]. + --debug output full trace logs [=false]. + --nomemory disable memory output [=true]. + --nostack disable stack output [=false]. + --nostorage disable storage output [=false]. + --noreturndata enable return data output [=true]. + --verbosity sets the verbosity level [=0]. + +```