From 036954709ab6707315d8fc80d506fe4686a93a03 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 6 Mar 2026 22:31:05 -0600 Subject: [PATCH 01/23] feat: dio script to generate -w config from debug_traceCall (#96) Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 5 +- dio | 303 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+), 1 deletion(-) create mode 100755 dio diff --git a/Makefile b/Makefile index 5a50ad7..b47fb0a 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,10 @@ OCFLAGS=$(filter-out $(CCSTD), $(CFLAGS)) -fmodules MKDIRS=lib bin tst/bin .pass .pass/tst/bin .make .make/bin .make/tst/bin .make/lib .pass/tst/in .pass/tst/diotst SECP256K1=secp256k1/.libs/libsecp256k1.a INCLUDE=$(addprefix -I,include) -Isecp256k1/include -EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) +EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) bin/dio +bin/dio: dio | bin + cp $< $@ + chmod +x $@ TESTS=$(patsubst tst/%.c, tst/bin/%, $(wildcard tst/*.c)) SRC=$(wildcard src/*.cpp) $(wildcard src/*.m) $(wildcard src/%.c) LIBS=$(patsubst src/%.cpp, lib/%.o, $(wildcard src/*.cpp)) $(patsubst src/%.m, lib/%.o, $(wildcard src/*.m)) $(patsubst src/%.c, lib/%.o, $(wildcard src/*.c)) diff --git a/dio b/dio new file mode 100755 index 0000000..2c16c55 --- /dev/null +++ b/dio @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +""" +dio - Generate dio config files from debug_traceCall + +Usage: + dio [provider-url] [outfile] [-o json] [--extend file] [file...] + +The call JSON (from -o, a file argument, or stdin) is the eth_call object: + {"to": "0x...", "from": "0x...", "data": "0x...", "value": "0x...", "block": "latest"} + +Only "to" is required. "block" defaults to "latest". +The generated config is written to outfile, or stdout if omitted. + +Options: + -o Call JSON inline + --extend Extend existing config file; fail on conflicting state + +The provider URL may also be set via the ETH_RPC_URL environment variable. + +Makes JSON-RPC calls to an Ethereum node and writes a dio test +configuration file for use with bin/evm -w. +""" + +import argparse +import json +import os +import sys +import urllib.request + + +def http_post(url, payload): + req = urllib.request.Request( + url, data=payload, headers={"Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req) as resp: + return resp.read() + except urllib.error.HTTPError as e: + print(f"dio: HTTP {e.code} {e.reason} from {url}", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print(f"dio: {e.reason} ({url})", file=sys.stderr) + sys.exit(1) + + +def rpc_call(url, method, params, req_id=1): + payload = json.dumps({ + "jsonrpc": "2.0", + "id": req_id, + "method": method, + "params": params, + }).encode() + resp = json.loads(http_post(url, payload)) + if "error" in resp: + err = resp["error"] + print(f"dio: {method} failed ({err.get('message', err)})", file=sys.stderr) + sys.exit(1) + return resp + + +def batch_rpc(url, requests): + payload = json.dumps(requests).encode() + result = json.loads(http_post(url, payload)) + result.sort(key=lambda r: r["id"]) + return result + + +def stack_to_address(val): + """Convert a stack value (hex string) to a 0x-prefixed lowercase address.""" + s = val[2:] if val.startswith(("0x", "0X")) else val + return "0x" + s[-40:].lower().zfill(40) + + +def normalize_key(val): + """Normalize a storage key from the stack to a 0x-prefixed 64-char hex string.""" + s = val[2:] if val.startswith(("0x", "0X")) else val + return "0x" + s.zfill(64) + + +def build_access_list(struct_logs, to_addr, from_addr): + """ + Walk structLogs and build {address: [storage_keys]}. + + Tracks the call stack to attribute SLOAD to the correct account. + """ + access_list = {} # address -> list of storage keys (ordered, unique) + call_stack = [] # current execution context (address at each depth) + + def add_account(addr): + addr = addr.lower() + if addr not in access_list: + access_list[addr] = [] + return addr + + if to_addr: + call_stack.append(add_account(to_addr)) + if from_addr: + add_account(from_addr) + + def on_sload(log): + if not call_stack: + return + key = normalize_key(log["stack"][-1]) + addr = call_stack[-1] + if key not in access_list[addr]: + access_list[addr].append(key) + + def on_call(log): + # stack (top to bottom): gas, addr, ... => addr at stack[-2] + addr = add_account(stack_to_address(log["stack"][-2])) + call_stack.append(addr) + + def on_delegatecall(log): + # Runs in caller's storage context; still track code address for eth_getCode + add_account(stack_to_address(log["stack"][-2])) + ctx = call_stack[-1] if call_stack else add_account(stack_to_address(log["stack"][-2])) + call_stack.append(ctx) + + def on_extcode(log): + # EXTCODESIZE / EXTCODECOPY: address is at stack top + add_account(stack_to_address(log["stack"][-1])) + + def on_return(_log): + if call_stack: + call_stack.pop() + + handlers = { + "SLOAD": on_sload, + "CALL": on_call, + "STATICCALL": on_call, + "DELEGATECALL": on_delegatecall, + "EXTCODESIZE": on_extcode, + "EXTCODECOPY": on_extcode, + "STOP": on_return, + "RETURN": on_return, + "REVERT": on_return, + } + + for log in struct_logs: + handler = handlers.get(log["op"]) + if handler: + handler(log) + + return access_list + + +def run(url, call_json, outfile, extend): + call = json.loads(call_json) + + if "to" not in call: + print("call JSON must include \"to\"", file=sys.stderr) + sys.exit(1) + + block = call.pop("block", "latest") + if block == "latest": + block = rpc_call(url, "eth_blockNumber", [])["result"] + + # Normalize: dio uses "input", RPC uses "data" + if "input" in call and "data" not in call: + call["data"] = call.pop("input") + + sender = call.get("from", "0x" + "0" * 40) + + trace_resp = rpc_call( + url, "debug_traceCall", + [call, block, {"disableStorage": True, "disableMemory": True}], + ) + struct_logs = trace_resp["result"]["structLogs"] + access_list = build_access_list(struct_logs, call["to"], sender) + + # Batch query balance, nonce, code, and storage for each account + batch = [] + meta = [] # (address, field, key_or_None) + bid = 1 + for addr, keys in access_list.items(): + batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getBalance", + "params": [addr, block]}) + meta.append((addr, "balance", None)); bid += 1 + + batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getTransactionCount", + "params": [addr, block]}) + meta.append((addr, "nonce", None)); bid += 1 + + batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getCode", + "params": [addr, block]}) + meta.append((addr, "code", None)); bid += 1 + + for key in keys: + batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getStorageAt", + "params": [addr, key, block]}) + meta.append((addr, "storage", key)); bid += 1 + + account_data = { + addr: {"balance": "0x0", "nonce": "0x0", "code": "0x", "storage": {}} + for addr in access_list + } + if batch: + results = batch_rpc(url, batch) + for i, resp in enumerate(results): + if "error" in resp: + err = resp["error"] + addr, field, key = meta[i] + method = "eth_getStorageAt" if field == "storage" else f"eth_get{field.capitalize()}" + print(f"dio: {method} failed ({err.get('message', err)})", file=sys.stderr) + sys.exit(1) + addr, field, key = meta[i] + val = resp["result"] + if field == "storage": + if val and val != "0x" + "0" * 64: + account_data[addr]["storage"][key] = val + else: + account_data[addr][field] = val + + # Load existing config if --extend was given + existing = [] + existing_by_addr = {} # lowercase address -> index in existing + if extend: + with open(extend) as f: + existing = json.load(f) + for i, entry in enumerate(existing): + if "address" in entry: + existing_by_addr[entry["address"].lower()] = i + + output = list(existing) + + for addr, data in account_data.items(): + if addr in existing_by_addr: + # Conflict check against existing entry + idx = existing_by_addr[addr] + ex = existing[idx] + for field, default in (("balance", "0x0"), ("nonce", "0x0"), ("code", "0x")): + new_val = data[field] + if new_val in (default, ""): + continue + ex_val = ex.get(field, default) + if ex_val != new_val: + print( + f"Conflict for {addr}.{field}: " + f"existing={ex_val!r} new={new_val!r}", + file=sys.stderr, + ) + sys.exit(1) + continue + + entry = {"address": addr} + if data["balance"] not in ("0x0", "0x"): + entry["balance"] = data["balance"] + if data["nonce"] not in ("0x0", "0x"): + entry["nonce"] = data["nonce"] + if data["code"] not in ("0x", ""): + entry["code"] = data["code"] + if data["storage"]: + entry["storage"] = data["storage"] + output.append(entry) + + # Build test entry; use "input" (dio convention) not "data" (RPC convention) + test = dict(call) + if "data" in test: + test["input"] = test.pop("data") + test["blockNumber"] = block + test.setdefault("debug", "0x20") + output.append({"tests": [test]}) + + out_json = json.dumps(output, indent=4) + + if outfile and outfile != "-": + with open(outfile, "w") as f: + f.write(out_json + "\n") + print(f"Wrote {outfile}", file=sys.stderr) + else: + print(out_json) + + +def main(): + if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"): + print(__doc__.strip()) + sys.exit(0) + + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("url", nargs="?", default=None) + parser.add_argument("outfile", nargs="?") + parser.add_argument("-o") + parser.add_argument("--extend", metavar="FILE") + parser.add_argument("files", nargs="*") + args = parser.parse_args() + + url = args.url or os.environ.get("ETH_RPC_URL") + if not url: + print("dio: provider URL required (positional arg or ETH_RPC_URL)", file=sys.stderr) + sys.exit(1) + + if args.o is not None: + run(url, args.o, args.outfile, args.extend) + elif args.files: + for path in args.files: + with open(path) as f: + run(url, f.read(), args.outfile, args.extend) + else: + run(url, sys.stdin.read(), args.outfile, args.extend) + + +if __name__ == "__main__": + main() From 60bbf701346ebebec7559b934716096688b0c79b Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 6 Mar 2026 23:45:35 -0600 Subject: [PATCH 02/23] feat: evm -nx interactive network fetch, dio fallback to evm when debug_traceCall unavailable - evm -nx: when executing with -n flag, emit JSON-RPC requests on stdout and read responses from stdin for on-demand account/storage fetching - evm -x now accepts JSON call object input (detected by leading '{') with to/from/data fields, enabling txCall instead of txCreate - src/network.c: network fetch callbacks using JSON-RPC over stdio - include/network.h: evmSetNetworkFetch() declaration - include/evm.h: evmSetFetch, evmBlockNumberIsSet, evmGetBlockNumber - dio: falls back to evm -nx proxy when debug_traceCall is unavailable Co-Authored-By: Claude Sonnet 4.6 --- dio | 222 +++++++++++++++++++++++++++++++++------------- evm.c | 103 +++++++++++++++------ include/evm.h | 6 ++ include/network.h | 3 + src/evm.c | 19 ++++ src/network.c | 135 ++++++++++++++++++++++++++++ 6 files changed, 399 insertions(+), 89 deletions(-) create mode 100644 include/network.h create mode 100644 src/network.c diff --git a/dio b/dio index 2c16c55..6943080 100755 --- a/dio +++ b/dio @@ -24,6 +24,7 @@ configuration file for use with bin/evm -w. import argparse import json import os +import subprocess import sys import urllib.request @@ -144,6 +145,154 @@ def build_access_list(struct_logs, to_addr, from_addr): return access_list +def write_config(account_data, call, block, outfile, extend): + """Build and write the dio config JSON from collected account_data.""" + existing = [] + existing_by_addr = {} # lowercase address -> index in existing + if extend: + with open(extend) as f: + existing = json.load(f) + for i, entry in enumerate(existing): + if "address" in entry: + existing_by_addr[entry["address"].lower()] = i + + output = list(existing) + + for addr, data in account_data.items(): + if addr in existing_by_addr: + idx = existing_by_addr[addr] + ex = existing[idx] + for field, default in (("balance", "0x0"), ("nonce", "0x0"), ("code", "0x")): + new_val = data[field] + if new_val in (default, ""): + continue + ex_val = ex.get(field, default) + if ex_val != new_val: + print( + f"Conflict for {addr}.{field}: " + f"existing={ex_val!r} new={new_val!r}", + file=sys.stderr, + ) + sys.exit(1) + continue + + entry = {"address": addr} + if data["balance"] not in ("0x0", "0x"): + entry["balance"] = data["balance"] + if data["nonce"] not in ("0x0", "0x"): + entry["nonce"] = data["nonce"] + if data["code"] not in ("0x", ""): + entry["code"] = data["code"] + if data["storage"]: + entry["storage"] = data["storage"] + output.append(entry) + + # Build test entry; use "input" (dio convention) not "data" (RPC convention) + test = dict(call) + if "data" in test: + test["input"] = test.pop("data") + test["blockNumber"] = block + test.setdefault("debug", "0x20") + output.append({"tests": [test]}) + + out_json = json.dumps(output, indent=4) + + if outfile and outfile != "-": + with open(outfile, "w") as f: + f.write(out_json + "\n") + print(f"Wrote {outfile}", file=sys.stderr) + else: + print(out_json) + + +def find_evm(): + script_dir = os.path.dirname(os.path.realpath(__file__)) + candidate = os.path.join(script_dir, "evm") + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return "evm" + + +def run_via_evm(url, call, block, sender, outfile, extend): + """Spawn evm -x -n with call JSON, proxy JSON-RPC on its stdin/stdout.""" + call_arg = json.dumps({ + "to": call["to"], + "from": sender, + "data": call.get("data", "0x"), + }) + proc = subprocess.Popen( + [find_evm(), "-x", "-n", "-o", call_arg], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, + ) + + account_data = {} + + def ensure_account(addr): + addr = addr.lower() + if addr not in account_data: + account_data[addr] = {"balance": "0x0", "nonce": "0x0", "code": "0x", "storage": {}} + return addr + + output_hex = None + try: + for line in proc.stdout: + line = line.strip() + if not line: + continue + msg = json.loads(line) + + if isinstance(msg, list): + # Account batch: [{getCode,...}, {getTransactionCount,...}, {getBalance,...}] + addr = ensure_account(msg[0]["params"][0]) + id_to_method = {m["id"]: m["method"] for m in msg} + resp = json.loads(http_post(url, json.dumps(msg).encode())) + resp.sort(key=lambda r: r["id"]) + for item in resp: + val = item["result"] + method = id_to_method[item["id"]] + if method == "eth_getCode": + account_data[addr]["code"] = val + elif method == "eth_getTransactionCount": + account_data[addr]["nonce"] = val + elif method == "eth_getBalance": + account_data[addr]["balance"] = val + proc.stdin.write(json.dumps(resp) + "\n") + proc.stdin.flush() + + elif "output" in msg: + output_hex = msg["output"] + break + + elif msg.get("method") == "eth_blockNumber": + resp = {"jsonrpc": "2.0", "id": msg["id"], "result": block} + proc.stdin.write(json.dumps(resp) + "\n") + proc.stdin.flush() + + elif msg.get("method") == "eth_getStorageAt": + addr = ensure_account(msg["params"][0]) + key = msg["params"][1] + resp = json.loads(http_post(url, json.dumps(msg).encode())) + val = resp.get("result", "0x" + "0" * 64) + if val and val != "0x" + "0" * 64: + account_data[addr]["storage"][key] = val + proc.stdin.write(json.dumps(resp) + "\n") + proc.stdin.flush() + + finally: + proc.stdin.close() + proc.wait() + + if output_hex is None or proc.returncode != 0: + print("dio: evm execution failed", file=sys.stderr) + sys.exit(1) + + ensure_account(call["to"]) + if sender and sender != "0x" + "0" * 40: + ensure_account(sender) + + write_config(account_data, call, block, outfile, extend) + + def run(url, call_json, outfile, extend): call = json.loads(call_json) @@ -161,10 +310,16 @@ def run(url, call_json, outfile, extend): sender = call.get("from", "0x" + "0" * 40) - trace_resp = rpc_call( - url, "debug_traceCall", - [call, block, {"disableStorage": True, "disableMemory": True}], - ) + # Try debug_traceCall; fall back to evm -nx on any error + trace_payload = json.dumps({ + "jsonrpc": "2.0", "id": 1, "method": "debug_traceCall", + "params": [call, block, {"disableStorage": True, "disableMemory": True}], + }).encode() + trace_resp = json.loads(http_post(url, trace_payload)) + if "error" in trace_resp: + run_via_evm(url, call, block, sender, outfile, extend) + return + struct_logs = trace_resp["result"]["structLogs"] access_list = build_access_list(struct_logs, call["to"], sender) @@ -211,64 +366,7 @@ def run(url, call_json, outfile, extend): else: account_data[addr][field] = val - # Load existing config if --extend was given - existing = [] - existing_by_addr = {} # lowercase address -> index in existing - if extend: - with open(extend) as f: - existing = json.load(f) - for i, entry in enumerate(existing): - if "address" in entry: - existing_by_addr[entry["address"].lower()] = i - - output = list(existing) - - for addr, data in account_data.items(): - if addr in existing_by_addr: - # Conflict check against existing entry - idx = existing_by_addr[addr] - ex = existing[idx] - for field, default in (("balance", "0x0"), ("nonce", "0x0"), ("code", "0x")): - new_val = data[field] - if new_val in (default, ""): - continue - ex_val = ex.get(field, default) - if ex_val != new_val: - print( - f"Conflict for {addr}.{field}: " - f"existing={ex_val!r} new={new_val!r}", - file=sys.stderr, - ) - sys.exit(1) - continue - - entry = {"address": addr} - if data["balance"] not in ("0x0", "0x"): - entry["balance"] = data["balance"] - if data["nonce"] not in ("0x0", "0x"): - entry["nonce"] = data["nonce"] - if data["code"] not in ("0x", ""): - entry["code"] = data["code"] - if data["storage"]: - entry["storage"] = data["storage"] - output.append(entry) - - # Build test entry; use "input" (dio convention) not "data" (RPC convention) - test = dict(call) - if "data" in test: - test["input"] = test.pop("data") - test["blockNumber"] = block - test.setdefault("debug", "0x20") - output.append({"tests": [test]}) - - out_json = json.dumps(output, indent=4) - - if outfile and outfile != "-": - with open(outfile, "w") as f: - f.write(out_json + "\n") - print(f"Wrote {outfile}", file=sys.stderr) - else: - print(out_json) + write_config(account_data, call, block, outfile, extend) def main(): diff --git a/evm.c b/evm.c index 041099b..70c1052 100644 --- a/evm.c +++ b/evm.c @@ -1,4 +1,5 @@ #include "dio.h" +#include "network.h" #include "path.h" #include "scan.h" #include "disassemble.h" @@ -29,6 +30,7 @@ static int includeStatus = 0; static int includeLogs = 0; static const char *configFile = NULL; static int updateConfigFile = 0; +static int networkMode = 0; static void assemble(const char *contents) { op_t *programStart = &ops[CONSTRUCTOR_OFFSET]; @@ -103,45 +105,88 @@ static void disassemble(const char *contents) { disassembleFinalize(); } +// Scan json for "key":"". Strips leading "0x" if present. +// Returns pointer into json at start of hex chars, sets *len to char count. +// Returns NULL if the key is not found. +static const char *jsonStrVal(const char *json, const char *key, size_t *len) { + size_t klen = strlen(key); + const char *p = json; + for (;;) { + p = strchr(p, '"'); + if (!p) return NULL; + if (strncmp(p + 1, key, klen) == 0 && p[1 + klen] == '"') { + p += 1 + klen + 1; + while (*p == ' ' || *p == ':') p++; + if (*p != '"') return NULL; + p++; + if (p[0] == '0' && p[1] == 'x') p += 2; + const char *start = p; + while (*p && *p != '"') p++; + *len = (size_t)(p - start); + return start; + } + p++; + } +} + static void execute(const char *contents) { if (configFile == NULL) { evmInit(); } - size_t len = strlen(contents); - if (len & 1 && contents[len - 1] != '\n') { - fputs("odd-lengthed input", stderr); - _exit(1); + if (networkMode) { + evmSetNetworkFetch(); } - if (len > 2 && contents[0] == '0' && contents[1] == 'x') { - // allow 0x prefix - len -= 2; - contents += 2; + + address_t from = {{0}}; + address_t to; + int hasTo = 0; + const char *hexData = contents; + size_t hexLen; + + if (contents[0] == '{') { + size_t flen; + const char *p = jsonStrVal(contents, "to", &flen); + if (p && flen == 40) { hasTo = 1; to = AddressFromHex40(p); } + p = jsonStrVal(contents, "from", &flen); + if (p && flen == 40) from = AddressFromHex40(p); + p = jsonStrVal(contents, "data", &flen); + if (!p) p = jsonStrVal(contents, "input", &flen); + hexData = p ? p : ""; + hexLen = p ? flen : 0; + } else { + hexLen = strlen(contents); + if (hexLen & 1 && contents[hexLen - 1] != '\n') { + fputs("odd-lengthed input", stderr); + _exit(1); + } + if (hexLen > 2 && hexData[0] == '0' && hexData[1] == 'x') { + hexLen -= 2; + hexData += 2; + } } data_t input; - input.size = len / 2; - input.content = malloc(input.size); - + input.size = hexLen / 2; + input.content = input.size ? malloc(input.size) : NULL; for (size_t i = 0; i < input.size; i++) { - input.content[i] = hexString16ToUint8(contents + i * 2); + input.content[i] = hexString16ToUint8(hexData + i * 2); } - // TODO support these eth_call parameters - address_t from; uint64_t gas = 0xffffffffffffffff; - val_t value; - value[0] = 0; - value[1] = 0; - value[2] = 0; + val_t value = {0, 0, 0}; result_t result; - if (false) { - address_t to; // TODO support this parameter + if (hasTo) { result = txCall(from, gas, to, value, input, NULL); } else { result = txCreate(from, gas, value, input); } evmFinalize(); - if (outputJson) { + if (networkMode) { + fputs("{\"output\":\"0x", stdout); + for (size_t i = 0; i < result.returnData.size; i++) + printf("%02x", result.returnData.content[i]); + fputs("\"}\n", stdout); + } else if (outputJson) { fputs("{\"", stdout); if (includeGas) { printf("gasUsed\":%" PRIu64 ",\"", gas - result.gasRemaining); @@ -161,16 +206,17 @@ static void execute(const char *contents) { ); } fputs("returnData\":\"0x", stdout); - } - for (;result.returnData.size--;) printf("%02x", *result.returnData.content++); - if (outputJson) { + for (;result.returnData.size--;) printf("%02x", *result.returnData.content++); fputs("\"}", stdout); + putchar('\n'); + } else { + for (;result.returnData.size--;) printf("%02x", *result.returnData.content++); + putchar('\n'); } - putchar('\n'); } -#define USAGE fputs("usage: evm [ [-w json-file [-u] ] [-x [-gs] ] | [-c | -C] [-j] | -d ] [-o input] [file...]\n", stderr) +#define USAGE fputs("usage: evm [ [-w json-file [-u] ] [-x [-n] [-gs] ] | [-c | -C] [-j] | -d ] [-o input] [file...]\n", stderr) static const struct option long_options[] = { {"version", no_argument, NULL, 'v'}, @@ -182,7 +228,7 @@ int main(int argc, char *const argv[]) { int option; char *contents = NULL; - while ((option = getopt_long(argc, argv, "cCdgjlo:suvw:x", long_options, NULL)) != -1) + while ((option = getopt_long(argc, argv, "cCdgjlo:nsuvw:x", long_options, NULL)) != -1) switch (option) { case 'c': wrapMinConstructor = 1; @@ -205,6 +251,9 @@ int main(int argc, char *const argv[]) { case 'g': includeGas = 1; break; + case 'n': + networkMode = 1; + break; case 's': includeStatus = 1; break; diff --git a/include/evm.h b/include/evm.h index ec8c128..5a781bd 100644 --- a/include/evm.h +++ b/include/evm.h @@ -101,6 +101,12 @@ typedef struct callResult { void evmInit(); void evmFinalize(); +typedef void (*account_fetch_t)(address_t address); +typedef void (*storage_fetch_t)(address_t address, const uint256_t *key, uint256_t *value_out); +void evmSetFetch(account_fetch_t, storage_fetch_t); +bool evmBlockNumberIsSet(void); +uint64_t evmGetBlockNumber(void); + #define EVM_DEBUG_STACK 1 #define EVM_DEBUG_MEMORY 2 #define EVM_DEBUG_OPS (4 + 8 + 16) diff --git a/include/network.h b/include/network.h new file mode 100644 index 0000000..5adcb17 --- /dev/null +++ b/include/network.h @@ -0,0 +1,3 @@ +// Register network fetch callbacks that fulfill missing account/storage state +// by emitting JSON-RPC requests on stdout and reading responses from stdin. +void evmSetNetworkFetch(void); diff --git a/src/evm.c b/src/evm.c index f7f7518..ab83dc5 100644 --- a/src/evm.c +++ b/src/evm.c @@ -323,10 +323,13 @@ static uint64_t evmIteration = 0; static uint16_t logIndex = 0; static uint64_t refundCounter = 0; static uint64_t blockNumber = 20587048; +static bool blockNumberSet = false; static uint64_t timestamp = 0x65712600; static address_t coinbase; static uint64_t debugFlags = 0; static account_t knownPrecompiles[KNOWN_PRECOMPILES]; +static account_fetch_t accountFetch = NULL; +static storage_fetch_t storageFetch = NULL; uint16_t depthOf(context_t *context) { return context - callstack.bottom; @@ -342,6 +345,20 @@ void fRepeat(FILE *file, const char *str, uint16_t times) { void evmSetBlockNumber(uint64_t _blockNumber) { blockNumber = _blockNumber; + blockNumberSet = true; +} + +bool evmBlockNumberIsSet(void) { + return blockNumberSet; +} + +uint64_t evmGetBlockNumber(void) { + return blockNumber; +} + +void evmSetFetch(account_fetch_t af, storage_fetch_t sf) { + accountFetch = af; + storageFetch = sf; } void evmSetTimestamp(uint64_t _timestamp) { @@ -383,6 +400,7 @@ static account_t *getAccount(const address_t address) { result->balance[0] = 0; result->balance[1] = 0; result->balance[2] = 0; + if (accountFetch) accountFetch(address); } return result; } @@ -535,6 +553,7 @@ static storage_t *getAccountStorage(account_t *account, const uint256_t *key) { } *storage = calloc(1, sizeof(storage_t)); copy256(&(*storage)->key, key); + if (storageFetch) storageFetch(account->address, key, &(*storage)->value); return *storage; } diff --git a/src/network.c b/src/network.c new file mode 100644 index 0000000..2b9f892 --- /dev/null +++ b/src/network.c @@ -0,0 +1,135 @@ +#include "network.h" +#include "evm.h" + +#include +#include +#include +#include +#include + +static uint32_t rpcId = 0; +static char rpcBuf[131072]; +static char networkBlockHex[20]; + +// Scan forward in *p to the next "result":"0x" value. +// Returns a pointer to the first hex character (past "0x"). +// Advances *p to the same position; caller scans to '"' for the end. +static const char *nextResultHex(const char **p) { + *p = strstr(*p, "\"result\""); + if (!*p) return NULL; + *p += 8; + *p = strchr(*p, ':'); + if (!*p) return NULL; + (*p)++; + while (**p == ' ') (*p)++; + if (**p != '"') return NULL; + (*p)++; + if ((*p)[0] == '0' && (*p)[1] == 'x') (*p) += 2; + return *p; +} + +static void ensureNetworkBlock(void) { + if (evmBlockNumberIsSet()) { + snprintf(networkBlockHex, sizeof(networkBlockHex), "0x%" PRIx64, evmGetBlockNumber()); + return; + } + printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_blockNumber\",\"params\":[]}\n", ++rpcId); + fflush(stdout); + if (!fgets(rpcBuf, sizeof(rpcBuf), stdin)) { + fputs("evm: network: no response for eth_blockNumber\n", stderr); + _exit(1); + } + const char *p = rpcBuf; + const char *hex = nextResultHex(&p); + if (!hex) { + fputs("evm: network: bad eth_blockNumber response\n", stderr); + _exit(1); + } + uint64_t block = 0; + while (*p != '"' && *p) block = (block << 4) | hexString8ToUint8(*p++); + snprintf(networkBlockHex, sizeof(networkBlockHex), "0x%" PRIx64, block); + evmSetBlockNumber(block); +} + +static void networkFetchAccount(address_t address) { + ensureNetworkBlock(); + uint32_t base = ++rpcId; rpcId += 2; + printf("["); + printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getCode\",\"params\":[\"", base); + fprintAddress(stdout, address); + printf("\",\"%s\"]},", networkBlockHex); + printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getTransactionCount\",\"params\":[\"", base + 1); + fprintAddress(stdout, address); + printf("\",\"%s\"]},", networkBlockHex); + printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getBalance\",\"params\":[\"", base + 2); + fprintAddress(stdout, address); + printf("\",\"%s\"]}", networkBlockHex); + printf("]\n"); + fflush(stdout); + + if (!fgets(rpcBuf, sizeof(rpcBuf), stdin)) { + fputs("evm: network: no response for account fetch\n", stderr); + _exit(1); + } + const char *p = rpcBuf; + + // code + const char *hex = nextResultHex(&p); + if (!hex) { fputs("evm: network: bad eth_getCode response\n", stderr); _exit(1); } + const char *codeStart = p; + while (*p != '"' && *p) p++; + data_t code; + code.size = (p - codeStart) / 2; + code.content = code.size ? malloc(code.size) : NULL; + for (size_t i = 0; i < code.size; i++) + code.content[i] = hexString16ToUint8(codeStart + i * 2); + evmMockCode(address, code); + if (*p == '"') p++; + + // nonce + hex = nextResultHex(&p); + if (!hex) { fputs("evm: network: bad eth_getTransactionCount response\n", stderr); _exit(1); } + uint64_t nonce = 0; + while (*p != '"' && *p) nonce = (nonce << 4) | hexString8ToUint8(*p++); + evmMockNonce(address, nonce); + if (*p == '"') p++; + + // balance + hex = nextResultHex(&p); + if (!hex) { fputs("evm: network: bad eth_getBalance response\n", stderr); _exit(1); } + val_t balance = {0, 0, 0}; + while (*p != '"' && *p) { + balance[0] = (balance[0] << 4) | (balance[1] >> 28); + balance[1] = (balance[1] << 4) | (balance[2] >> 28); + balance[2] = (balance[2] << 4) | hexString8ToUint8(*p++); + } + evmMockBalance(address, balance); +} + +static void networkFetchStorage(address_t address, const uint256_t *key, uint256_t *value_out) { + ensureNetworkBlock(); + printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getStorageAt\",\"params\":[\"", ++rpcId); + fprintAddress(stdout, address); + printf("\",\"0x%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "\",\"%s\"]}\n", + UPPER(UPPER_P(key)), LOWER(UPPER_P(key)), + UPPER(LOWER_P(key)), LOWER(LOWER_P(key)), + networkBlockHex); + fflush(stdout); + + if (!fgets(rpcBuf, sizeof(rpcBuf), stdin)) { + fputs("evm: network: no response for storage fetch\n", stderr); + _exit(1); + } + const char *p = rpcBuf; + nextResultHex(&p); + if (!p) { fputs("evm: network: bad eth_getStorageAt response\n", stderr); _exit(1); } + clear256(value_out); + while (*p != '"' && *p) { + shiftl256(value_out, 4, value_out); + LOWER(LOWER_P(value_out)) |= hexString8ToUint8(*p++); + } +} + +void evmSetNetworkFetch(void) { + evmSetFetch(networkFetchAccount, networkFetchStorage); +} From 84173b33fb65af86c680dd90bfd1e198eb253871 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 6 Mar 2026 23:51:31 -0600 Subject: [PATCH 03/23] fix: include output in dio-generated test cases Co-Authored-By: Claude Sonnet 4.6 --- dio | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dio b/dio index 6943080..c8720e5 100755 --- a/dio +++ b/dio @@ -145,7 +145,7 @@ def build_access_list(struct_logs, to_addr, from_addr): return access_list -def write_config(account_data, call, block, outfile, extend): +def write_config(account_data, call, block, outfile, extend, result=None): """Build and write the dio config JSON from collected account_data.""" existing = [] existing_by_addr = {} # lowercase address -> index in existing @@ -193,6 +193,8 @@ def write_config(account_data, call, block, outfile, extend): test["input"] = test.pop("data") test["blockNumber"] = block test.setdefault("debug", "0x20") + if result is not None: + test["output"] = result output.append({"tests": [test]}) out_json = json.dumps(output, indent=4) @@ -290,7 +292,7 @@ def run_via_evm(url, call, block, sender, outfile, extend): if sender and sender != "0x" + "0" * 40: ensure_account(sender) - write_config(account_data, call, block, outfile, extend) + write_config(account_data, call, block, outfile, extend, result=output_hex) def run(url, call_json, outfile, extend): @@ -320,7 +322,9 @@ def run(url, call_json, outfile, extend): run_via_evm(url, call, block, sender, outfile, extend) return - struct_logs = trace_resp["result"]["structLogs"] + trace_result = trace_resp["result"] + struct_logs = trace_result["structLogs"] + return_value = "0x" + trace_result.get("returnValue", "") access_list = build_access_list(struct_logs, call["to"], sender) # Batch query balance, nonce, code, and storage for each account @@ -366,7 +370,7 @@ def run(url, call_json, outfile, extend): else: account_data[addr][field] = val - write_config(account_data, call, block, outfile, extend) + write_config(account_data, call, block, outfile, extend, result=return_value) def main(): From 5ede5e0774b43d43ca53a16ba659e4511fd2bd18 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 6 Mar 2026 23:58:17 -0600 Subject: [PATCH 04/23] fix: trim leading zeros from storage keys in dio output Co-Authored-By: Claude Sonnet 4.6 --- dio | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dio b/dio index c8720e5..a957fdf 100755 --- a/dio +++ b/dio @@ -73,9 +73,9 @@ def stack_to_address(val): def normalize_key(val): - """Normalize a storage key from the stack to a 0x-prefixed 64-char hex string.""" - s = val[2:] if val.startswith(("0x", "0X")) else val - return "0x" + s.zfill(64) + """Normalize a storage key to a trimmed 0x-prefixed hex string.""" + s = val[2:] if val.startswith("0x") else val + return "0x" + (s.lstrip("0") or "0") def build_access_list(struct_logs, to_addr, from_addr): @@ -272,7 +272,7 @@ def run_via_evm(url, call, block, sender, outfile, extend): elif msg.get("method") == "eth_getStorageAt": addr = ensure_account(msg["params"][0]) - key = msg["params"][1] + key = normalize_key(msg["params"][1]) resp = json.loads(http_post(url, json.dumps(msg).encode())) val = resp.get("result", "0x" + "0" * 64) if val and val != "0x" + "0" * 64: From 4cdeba6a81cdf50790204b87578e499372efe76c Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sat, 7 Mar 2026 00:01:10 -0600 Subject: [PATCH 05/23] refactor: rename dio to dio.py, generalize Makefile rule for .py scripts Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 4 ++-- dio => dio.py | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename dio => dio.py (100%) diff --git a/Makefile b/Makefile index b47fb0a..3f02a40 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,8 @@ OCFLAGS=$(filter-out $(CCSTD), $(CFLAGS)) -fmodules MKDIRS=lib bin tst/bin .pass .pass/tst/bin .make .make/bin .make/tst/bin .make/lib .pass/tst/in .pass/tst/diotst SECP256K1=secp256k1/.libs/libsecp256k1.a INCLUDE=$(addprefix -I,include) -Isecp256k1/include -EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) bin/dio -bin/dio: dio | bin +EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) $(patsubst %.py, bin/%, $(wildcard *.py)) +bin/%: %.py | bin cp $< $@ chmod +x $@ TESTS=$(patsubst tst/%.c, tst/bin/%, $(wildcard tst/*.c)) diff --git a/dio b/dio.py similarity index 100% rename from dio rename to dio.py From 7df4926e3ccd2cd784fc794f1e24300b456b41c7 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sat, 7 Mar 2026 00:22:17 -0600 Subject: [PATCH 06/23] fix: allow ETH_RPC_URL with stdin input by moving early-exit into argparse flow Co-Authored-By: Claude Sonnet 4.6 --- dio.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dio.py b/dio.py index a957fdf..75f1895 100755 --- a/dio.py +++ b/dio.py @@ -374,11 +374,8 @@ def run(url, call_json, outfile, extend): def main(): - if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"): - print(__doc__.strip()) - sys.exit(0) - parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("-h", "--help", action="store_true") parser.add_argument("url", nargs="?", default=None) parser.add_argument("outfile", nargs="?") parser.add_argument("-o") @@ -386,9 +383,13 @@ def main(): parser.add_argument("files", nargs="*") args = parser.parse_args() + if args.help: + print(__doc__.strip()) + sys.exit(0) + url = args.url or os.environ.get("ETH_RPC_URL") if not url: - print("dio: provider URL required (positional arg or ETH_RPC_URL)", file=sys.stderr) + print(__doc__.strip()) sys.exit(1) if args.o is not None: From e8e84bd269eb094edfa0cb096d4a74d1aa36d4ac Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sat, 7 Mar 2026 01:03:07 -0600 Subject: [PATCH 07/23] feat: parameterize network post fn, add WebSocket support for ws:// wss:// URLs WebSocket connections are kept open for the duration of each run via async with websockets.connect(). The post function is passed through run, run_via_evm, rpc_call, and batch_rpc. HTTP path wraps http_post in an async function for uniform interface. Co-Authored-By: Claude Sonnet 4.6 --- dio.py | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/dio.py b/dio.py index 75f1895..522e5d8 100755 --- a/dio.py +++ b/dio.py @@ -22,6 +22,7 @@ """ import argparse +import asyncio import json import os import subprocess @@ -44,14 +45,14 @@ def http_post(url, payload): sys.exit(1) -def rpc_call(url, method, params, req_id=1): +async def rpc_call(post, method, params, req_id=1): payload = json.dumps({ "jsonrpc": "2.0", "id": req_id, "method": method, "params": params, }).encode() - resp = json.loads(http_post(url, payload)) + resp = json.loads(await post(payload)) if "error" in resp: err = resp["error"] print(f"dio: {method} failed ({err.get('message', err)})", file=sys.stderr) @@ -59,9 +60,9 @@ def rpc_call(url, method, params, req_id=1): return resp -def batch_rpc(url, requests): +async def batch_rpc(post, requests): payload = json.dumps(requests).encode() - result = json.loads(http_post(url, payload)) + result = json.loads(await post(payload)) result.sort(key=lambda r: r["id"]) return result @@ -215,7 +216,7 @@ def find_evm(): return "evm" -def run_via_evm(url, call, block, sender, outfile, extend): +async def run_via_evm(call, block, sender, outfile, extend, post): """Spawn evm -x -n with call JSON, proxy JSON-RPC on its stdin/stdout.""" call_arg = json.dumps({ "to": call["to"], @@ -247,7 +248,7 @@ def ensure_account(addr): # Account batch: [{getCode,...}, {getTransactionCount,...}, {getBalance,...}] addr = ensure_account(msg[0]["params"][0]) id_to_method = {m["id"]: m["method"] for m in msg} - resp = json.loads(http_post(url, json.dumps(msg).encode())) + resp = json.loads(await post(json.dumps(msg).encode())) resp.sort(key=lambda r: r["id"]) for item in resp: val = item["result"] @@ -273,7 +274,7 @@ def ensure_account(addr): elif msg.get("method") == "eth_getStorageAt": addr = ensure_account(msg["params"][0]) key = normalize_key(msg["params"][1]) - resp = json.loads(http_post(url, json.dumps(msg).encode())) + resp = json.loads(await post(json.dumps(msg).encode())) val = resp.get("result", "0x" + "0" * 64) if val and val != "0x" + "0" * 64: account_data[addr]["storage"][key] = val @@ -295,7 +296,7 @@ def ensure_account(addr): write_config(account_data, call, block, outfile, extend, result=output_hex) -def run(url, call_json, outfile, extend): +async def run(call_json, outfile, extend, post): call = json.loads(call_json) if "to" not in call: @@ -304,7 +305,7 @@ def run(url, call_json, outfile, extend): block = call.pop("block", "latest") if block == "latest": - block = rpc_call(url, "eth_blockNumber", [])["result"] + block = (await rpc_call(post, "eth_blockNumber", []))["result"] # Normalize: dio uses "input", RPC uses "data" if "input" in call and "data" not in call: @@ -317,9 +318,9 @@ def run(url, call_json, outfile, extend): "jsonrpc": "2.0", "id": 1, "method": "debug_traceCall", "params": [call, block, {"disableStorage": True, "disableMemory": True}], }).encode() - trace_resp = json.loads(http_post(url, trace_payload)) + trace_resp = json.loads(await post(trace_payload)) if "error" in trace_resp: - run_via_evm(url, call, block, sender, outfile, extend) + await run_via_evm(call, block, sender, outfile, extend, post) return trace_result = trace_resp["result"] @@ -354,7 +355,7 @@ def run(url, call_json, outfile, extend): for addr in access_list } if batch: - results = batch_rpc(url, batch) + results = await batch_rpc(post, batch) for i, resp in enumerate(results): if "error" in resp: err = resp["error"] @@ -373,6 +374,24 @@ def run(url, call_json, outfile, extend): write_config(account_data, call, block, outfile, extend, result=return_value) +async def run_main(url, call_json, outfile, extend): + if url.startswith(("ws://", "wss://")): + try: + import websockets + except ImportError: + print("dio: websockets package required for ws:// URLs (pip install websockets)", file=sys.stderr) + sys.exit(1) + async with websockets.connect(url) as ws: + async def post(payload): + await ws.send(payload.decode() if isinstance(payload, bytes) else payload) + return (await ws.recv()).encode() + await run(call_json, outfile, extend, post) + else: + async def post(payload): + return http_post(url, payload) + await run(call_json, outfile, extend, post) + + def main(): parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-h", "--help", action="store_true") @@ -393,13 +412,13 @@ def main(): sys.exit(1) if args.o is not None: - run(url, args.o, args.outfile, args.extend) + asyncio.run(run_main(url, args.o, args.outfile, args.extend)) elif args.files: for path in args.files: with open(path) as f: - run(url, f.read(), args.outfile, args.extend) + asyncio.run(run_main(url, f.read(), args.outfile, args.extend)) else: - run(url, sys.stdin.read(), args.outfile, args.extend) + asyncio.run(run_main(url, sys.stdin.read(), args.outfile, args.extend)) if __name__ == "__main__": From fc3d10da1611c1cc63b61fb2b3aad34ccc7d4817 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sat, 7 Mar 2026 22:19:43 -0600 Subject: [PATCH 08/23] feat: rewrite dio in C with HTTP and WebSocket support Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 3 + NOTICES/LIBCURL_LICENSE | 24 + dio.c | 1116 +++++++++++++++++++++++++++++++++++++++ include/ws.h | 16 + src/ws.c | 225 ++++++++ 5 files changed, 1384 insertions(+) create mode 100644 NOTICES/LIBCURL_LICENSE create mode 100644 dio.c create mode 100644 include/ws.h create mode 100644 src/ws.c diff --git a/Makefile b/Makefile index 3f02a40..19adf55 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ OCFLAGS=$(filter-out $(CCSTD), $(CFLAGS)) -fmodules MKDIRS=lib bin tst/bin .pass .pass/tst/bin .make .make/bin .make/tst/bin .make/lib .pass/tst/in .pass/tst/diotst SECP256K1=secp256k1/.libs/libsecp256k1.a INCLUDE=$(addprefix -I,include) -Isecp256k1/include +CURL_LDFLAGS := $(shell curl-config --libs 2>/dev/null || echo -lcurl) EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) $(patsubst %.py, bin/%, $(wildcard *.py)) bin/%: %.py | bin cp $< $@ @@ -76,6 +77,8 @@ bin/%: %.cpp $(CPP) $(CXXFLAGS) $(INCLUDE) $^ -o $@ bin/%: %.c $(CC) $(CFLAGS) $(INCLUDE) $^ -o $@ +bin/dio: dio.c | bin + $(CC) $(CFLAGS) $(INCLUDE) $^ $(CURL_LDFLAGS) -o $@ lib/%.o: src/%.m include/%.h | lib $(CC) -c $(OCFLAGS) $(INCLUDE) $< -o $@ lib/%.o: src/%.cpp include/%.h | lib diff --git a/NOTICES/LIBCURL_LICENSE b/NOTICES/LIBCURL_LICENSE new file mode 100644 index 0000000..d7a7a37 --- /dev/null +++ b/NOTICES/LIBCURL_LICENSE @@ -0,0 +1,24 @@ +dio.c links against libcurl (https://curl.se/libcurl/). + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1996 - 2024, Daniel Stenberg, , and many +contributors, see the THANKS file. + +All rights reserved. + +Permission to use, copy, modify, and distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall not +be used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization of the copyright holder. diff --git a/dio.c b/dio.c new file mode 100644 index 0000000..4722f6d --- /dev/null +++ b/dio.c @@ -0,0 +1,1116 @@ +/* + * dio - Generate evm test config files from on-chain state + * + * Usage: + * dio [provider-url] [outfile] [-o json] [file...] + * + * The call JSON (from -o, a file argument, or stdin) is the eth_call object: + * {"to": "0x...", "from": "0x...", "data": "0x...", "block": "latest"} + * + * Only "to" is required. "block" defaults to "latest". + * The generated config is written to outfile, or stdout if omitted. + * + * Options: + * -o Call JSON inline + * + * The provider URL may also be set via the ETH_RPC_URL environment variable. + */ + +#include "ws.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ========================================================= + * Data structures + * ========================================================= */ + +#define ADDR_LEN 43 /* "0x" + 40 hex + NUL */ +#define HEX256_LEN 68 /* "0x" + 64 hex + NUL */ +#define NONCE_LEN 22 /* "0x" + up to 18 hex + NUL */ +#define LINE_CAP 131072 /* matches evm's rpcBuf */ +#define MAX_BATCH 8192 /* max requests in one JSON-RPC batch */ + +typedef struct storage_kv { + char *key; + char *value; + struct storage_kv *next; +} storage_kv_t; + +/* Storage key recorded during trace (before value is fetched) */ +typedef struct storage_key { + char key[HEX256_LEN]; + struct storage_key *next; +} storage_key_t; + +typedef struct account { + char address[ADDR_LEN]; + char balance[HEX256_LEN]; + char nonce[NONCE_LEN]; + char *code; /* dynamically allocated "0x..." */ + storage_kv_t *storage; /* fetched key→value pairs */ + storage_key_t *keys; /* keys to fetch (from access list) */ + struct account *next; +} account_t; + +/* Metadata for one request in a batch, used to route responses */ +typedef struct { + char addr[ADDR_LEN]; + char key[HEX256_LEN]; /* empty if not a storage slot */ + int field; /* 0=balance 1=nonce 2=code 3=storage */ +} batch_meta_t; + +/* ========================================================= + * Growing string buffer + * ========================================================= */ + +typedef struct { char *buf; size_t len, cap; } strbuf_t; + +static void sbAppend(strbuf_t *sb, const char *s, size_t n) { + if (sb->len + n + 1 > sb->cap) { + size_t nc = sb->cap ? sb->cap * 2 : 4096; + while (nc < sb->len + n + 1) nc *= 2; + sb->buf = realloc(sb->buf, nc); + sb->cap = nc; + } + memcpy(sb->buf + sb->len, s, n); + sb->len += n; + sb->buf[sb->len] = '\0'; +} + +static void sbFmt(strbuf_t *sb, const char *fmt, ...) { + va_list ap, ap2; + va_start(ap, fmt); + va_copy(ap2, ap); + int n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (sb->len + (size_t)n + 1 > sb->cap) { + size_t nc = sb->cap ? sb->cap * 2 : 4096; + while (nc < sb->len + (size_t)n + 1) nc *= 2; + sb->buf = realloc(sb->buf, nc); + sb->cap = nc; + } + vsnprintf(sb->buf + sb->len, (size_t)n + 1, fmt, ap2); + va_end(ap2); + sb->len += (size_t)n; +} + +/* ========================================================= + * Minimal JSON helpers + * + * These cover only the JSON shapes produced/consumed by + * evm -n and well-formed JSON-RPC responses. + * ========================================================= */ + +static void skipWs(const char **p) { + while (**p == ' ' || **p == '\t' || **p == '\r' || **p == '\n') + (*p)++; +} + +/* + * Skip a JSON value starting at *p, advancing *p past it. + * Handles strings, objects, arrays, numbers/literals. + */ +static void jSkip(const char **p) { + skipWs(p); + if (**p == '"') { + (*p)++; + while (**p && **p != '"') { + if (**p == '\\' && (*p)[1]) (*p)++; + (*p)++; + } + if (**p == '"') (*p)++; + return; + } + if (**p == '{' || **p == '[') { + char open = **p, close = (open == '{') ? '}' : ']'; + int depth = 1; + (*p)++; + while (**p && depth > 0) { + if (**p == '"') { + (*p)++; + while (**p && **p != '"') { + if (**p == '\\' && (*p)[1]) (*p)++; + (*p)++; + } + if (**p == '"') (*p)++; + } else if (**p == open) { + depth++; (*p)++; + } else if (**p == close) { + depth--; (*p)++; + } else { + (*p)++; + } + } + return; + } + /* number, true, false, null */ + while (**p && **p != ',' && **p != '}' && **p != ']' && + **p != ' ' && **p != '\t' && **p != '\r' && **p != '\n') + (*p)++; +} + +/* + * Scan forward in p for "key": and return a pointer to the value. + * Sufficient for flat JSON-RPC messages. Returns NULL if not found. + */ +static const char *jFind(const char *p, const char *key) { + size_t klen = strlen(key); + while (*p) { + if (*p++ == '"' && strncmp(p, key, klen) == 0 && p[klen] == '"') { + p += klen + 1; + while (*p == ' ' || *p == '\t') p++; + if (*p++ != ':') continue; + while (*p == ' ' || *p == '\t') p++; + return p; + } + } + return NULL; +} + +/* + * Copy the quoted JSON string at p into buf (NUL-terminated, max buflen). + * Returns char count, or -1 if p is not a quoted string. + */ +static int jStr(const char *p, char *buf, size_t buflen) { + if (!p || *p != '"') return -1; + p++; + size_t i = 0; + while (*p && *p != '"') { + if (*p == '\\' && p[1]) p++; + if (i + 1 < buflen) buf[i++] = *p; + if (*p) p++; + } + buf[i] = '\0'; + return (int)i; +} + +/* + * Parse a JSON number or quoted "0x..." hex at p into uint64_t. + */ +static uint64_t jUint(const char *p) { + if (!p) return 0; + if (*p == '"') p++; + if (p[0] == '0' && p[1] == 'x') { + p += 2; + uint64_t v = 0; + for (unsigned c; (c = (unsigned char)*p); p++) { + if (c >= '0' && c <= '9') v = (v << 4) | (c - '0'); + else if (c >= 'a' && c <= 'f') v = (v << 4) | (c - 'a' + 10); + else if (c >= 'A' && c <= 'F') v = (v << 4) | (c - 'A' + 10); + else break; + } + return v; + } + uint64_t v = 0; + while (*p >= '0' && *p <= '9') v = v * 10 + (*p++ - '0'); + return v; +} + +/* + * Return a pointer to element n (0-based) in the JSON array at '['. + * Returns NULL if out of range. + */ +static const char *jArrayGet(const char *p, int n) { + if (!p || *p != '[') return NULL; + p++; + while (*p) { + skipWs(&p); + if (*p == ']') return NULL; + if (n == 0) return p; + jSkip(&p); + n--; + skipWs(&p); + if (*p == ',') p++; + } + return NULL; +} + +/* + * Store pointers to the last n elements (n ≤ 2) of a JSON array into + * out[0..n-1], where out[n-1] is the last element (EVM stack top). + * Returns the actual count stored (may be < n for short arrays). + */ +static int jArrayTailN(const char *arr, int n, const char **out) { + if (!arr || *arr != '[' || n <= 0 || n > 2) return 0; + const char *buf[2]; + int count = 0; + const char *p = arr + 1; + while (*p) { + skipWs(&p); + if (*p == ']') break; + buf[count % n] = p; + count++; + jSkip(&p); + skipWs(&p); + if (*p == ',') p++; + } + int found = count < n ? count : n; + for (int i = 0; i < found; i++) + out[i] = buf[(count - found + i) % n]; + return found; +} + +/* + * Return a dynamically-allocated copy of the quoted JSON string at p. + * Returns "0x" if p is NULL or not a string. Caller must free(). + */ +static char *jStrDup(const char *p) { + if (!p || *p != '"') return strdup("0x"); + p++; + const char *end = p; + while (*end && *end != '"') end++; + size_t len = end - p; + char *s = malloc(len + 1); + memcpy(s, p, len); + s[len] = '\0'; + return s; +} + +/* + * Extract "key" string from json, prepend "0x" if absent. + * Returns a dynamically-allocated string. Caller must free(). + */ +static char *jStrHex(const char *json, const char *key) { + const char *p = jFind(json, key); + if (!p || *p != '"') return strdup("0x"); + p++; + const char *end = p; + while (*end && *end != '"') end++; + size_t len = end - p; + int has0x = (len >= 2 && p[0] == '0' && p[1] == 'x'); + char *s = malloc(len + (has0x ? 1 : 3)); + if (has0x) { + memcpy(s, p, len); s[len] = '\0'; + } else { + s[0] = '0'; s[1] = 'x'; + memcpy(s + 2, p, len); s[len + 2] = '\0'; + } + return s; +} + +/* + * In a JSON-RPC batch response array, find the element with "id": targetId + * and return a newly-allocated copy of its "result" string value. + * Caller must free(). Returns NULL if not found. + */ +static char *resultById(const char *resp, uint64_t targetId) { + const char *p = resp; + if (*p == '[') p++; + while (*p) { + skipWs(&p); + if (*p == ']' || !*p) break; + const char *elem = p; + if (jUint(jFind(elem, "id")) == targetId) { + const char *rv = jFind(elem, "result"); + if (rv && *rv == '"') { + rv++; + const char *end = rv; + while (*end && *end != '"') end++; + size_t len = end - rv; + char *s = malloc(len + 1); + memcpy(s, rv, len); + s[len] = '\0'; + return s; + } + } + jSkip(&p); + skipWs(&p); + if (*p == ',') p++; + } + return NULL; +} + +/* ========================================================= + * Account management + * ========================================================= */ + +static void normalizeAddr(const char *in, char *out) { + const char *s = (in[0] == '0' && in[1] == 'x') ? in + 2 : in; + out[0] = '0'; out[1] = 'x'; + for (int i = 0; i < 40; i++) { + unsigned char c = (unsigned char)s[i]; + out[2 + i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : (char)c; + } + out[42] = '\0'; +} + +static account_t *ensureAccount(account_t **head, const char *addr) { + char norm[ADDR_LEN]; + normalizeAddr(addr, norm); + for (account_t *a = *head; a; a = a->next) + if (strcmp(a->address, norm) == 0) return a; + account_t *a = calloc(1, sizeof(account_t)); + memcpy(a->address, norm, ADDR_LEN); + strcpy(a->balance, "0x0"); + strcpy(a->nonce, "0x0"); + a->code = strdup("0x"); + a->next = *head; + *head = a; + return a; +} + +/* + * Normalize a storage key: strip leading zeros, keep at least one digit. + * "0x000...abc" -> "0xabc", "0x000...0" -> "0x0" + */ +static void normalizeKey(const char *in, char *out, size_t outlen) { + const char *s = (in[0] == '0' && in[1] == 'x') ? in + 2 : in; + while (*s == '0' && *(s + 1)) s++; + snprintf(out, outlen, "0x%s", s); +} + +static void addStorage(account_t *acct, const char *key, const char *val) { + const char *v = (val[0] == '0' && val[1] == 'x') ? val + 2 : val; + int allZero = 1; + for (const char *c = v; *c; c++) if (*c != '0') { allZero = 0; break; } + if (allZero) return; + + char normKey[HEX256_LEN]; + normalizeKey(key, normKey, sizeof(normKey)); + for (storage_kv_t *s = acct->storage; s; s = s->next) + if (strcmp(s->key, normKey) == 0) return; + + storage_kv_t *s = calloc(1, sizeof(storage_kv_t)); + s->key = strdup(normKey); + s->value = strdup(val); + s->next = acct->storage; + acct->storage = s; +} + +/* Record a storage key to be fetched for this account (deduped, normalized). */ +static void addAccessKey(account_t *acct, const char *key) { + char norm[HEX256_LEN]; + normalizeKey(key, norm, sizeof(norm)); + for (storage_key_t *k = acct->keys; k; k = k->next) + if (strcmp(k->key, norm) == 0) return; + storage_key_t *k = calloc(1, sizeof(storage_key_t)); + strncpy(k->key, norm, sizeof(k->key) - 1); + k->next = acct->keys; + acct->keys = k; +} + +/* ========================================================= + * EVM stack helpers + * ========================================================= */ + +/* + * Convert a 32-byte stack value ("0x000...address") to a + * 0x-prefixed lowercase 20-byte address string. + */ +static void stackToAddress(const char *val, char *out) { + const char *s = (val[0] == '0' && val[1] == 'x') ? val + 2 : val; + size_t len = strlen(s); + if (len > 40) s += (len - 40); + int pad = (int)(40 - (len < 40 ? len : 40)); + out[0] = '0'; out[1] = 'x'; + for (int i = 0; i < pad; i++) out[2 + i] = '0'; + for (int i = pad; i < 40; i++) { + unsigned char c = (unsigned char)s[i - pad]; + out[2 + i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : (char)c; + } + out[42] = '\0'; +} + +/* ========================================================= + * Find evm binary + * ========================================================= */ + +static char evmBinPath[4096]; + +/* + * Look for an "evm" sibling next to the running binary (argv[0]). + * Falls back to "evm" for PATH resolution if not found. + */ +static const char *findEvm(const char *self) { + const char *slash = strrchr(self, '/'); + if (slash) { + snprintf(evmBinPath, sizeof(evmBinPath), + "%.*s/evm", (int)(slash - self), self); + struct stat st; + if (stat(evmBinPath, &st) == 0 && (st.st_mode & S_IXUSR)) + return evmBinPath; + } + return "evm"; +} + +/* ========================================================= + * JSON-RPC post function type + * + * Sends a NUL-terminated JSON payload and returns a + * dynamically-allocated NUL-terminated response string. + * Caller must free(). Returns NULL on error. + * ========================================================= */ +typedef char *(*postFn)(const char *payload, void *ctx); + +/* ========================================================= + * HTTP and WebSocket post implementations + * ========================================================= */ + +typedef struct { const char *url; CURL *curl; } http_ctx_t; + +static size_t curlWrite(char *data, size_t sz, size_t n, void *userp) { + sbAppend((strbuf_t *)userp, data, sz * n); + return sz * n; +} + +static char *httpPost(const char *payload, void *ctx) { + http_ctx_t *hctx = ctx; + strbuf_t resp = {0}; + + struct curl_slist *hdrs = NULL; + hdrs = curl_slist_append(hdrs, "Content-Type: application/json"); + + curl_easy_setopt(hctx->curl, CURLOPT_URL, hctx->url); + curl_easy_setopt(hctx->curl, CURLOPT_POST, 1L); + curl_easy_setopt(hctx->curl, CURLOPT_POSTFIELDS, payload); + curl_easy_setopt(hctx->curl, CURLOPT_POSTFIELDSIZE, (long)strlen(payload)); + curl_easy_setopt(hctx->curl, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(hctx->curl, CURLOPT_WRITEFUNCTION, curlWrite); + curl_easy_setopt(hctx->curl, CURLOPT_WRITEDATA, &resp); + + CURLcode rc = curl_easy_perform(hctx->curl); + curl_slist_free_all(hdrs); + + if (rc != CURLE_OK) { + fprintf(stderr, "dio: HTTP: %s\n", curl_easy_strerror(rc)); + free(resp.buf); + return NULL; + } + return resp.buf ? resp.buf : strdup(""); +} + + +/* ========================================================= + * Subprocess proxy (evm -x -n fallback) + * ========================================================= */ + +/* + * Spawn `evm -x -n -o ` with bidirectional pipes. + * *toChild — parent writes here → child stdin + * *fromChild — parent reads here ← child stdout + * Returns the child PID. + */ +static pid_t spawnEvm(const char *evm, const char *callJson, + FILE **toChild, FILE **fromChild) { + int toFds[2], fromFds[2]; + if (pipe(toFds) == -1 || pipe(fromFds) == -1) { + perror("dio: pipe"); + _exit(1); + } + pid_t pid = fork(); + if (pid == -1) { perror("dio: fork"); _exit(1); } + if (pid == 0) { + dup2(toFds[0], STDIN_FILENO); + dup2(fromFds[1], STDOUT_FILENO); + close(toFds[0]); close(toFds[1]); + close(fromFds[0]); close(fromFds[1]); + char *args[] = { (char *)evm, "-x", "-n", "-o", (char *)callJson, NULL }; + execvp(evm, args); + perror(evm); + _exit(1); + } + close(toFds[0]); + close(fromFds[1]); + *toChild = fdopen(toFds[1], "w"); + *fromChild = fdopen(fromFds[0], "r"); + return pid; +} + +/* + * Write a sorted batch response [code, nonce, balance] to f. + * + * network.c's nextResultHex() scans for "result" values in forward + * order and assigns them: first=code, second=nonce, third=balance. + * We must therefore write the three elements sorted ascending by id. + */ +static void writeSortedBatch(FILE *f, + uint64_t codeId, const char *code, + uint64_t nonceId, const char *nonce, + uint64_t balanceId, const char *balance) { + uint64_t ids[3] = { codeId, nonceId, balanceId }; + const char *vals[3] = { code, nonce, balance }; + for (int i = 1; i < 3; i++) { + uint64_t ki = ids[i]; const char *vi = vals[i]; + int j = i - 1; + while (j >= 0 && ids[j] > ki) { + ids[j + 1] = ids[j]; vals[j + 1] = vals[j]; j--; + } + ids[j + 1] = ki; vals[j + 1] = vi; + } + fputc('[', f); + for (int i = 0; i < 3; i++) { + if (i) fputc(',', f); + fprintf(f, "{\"jsonrpc\":\"2.0\",\"id\":%" PRIu64 ",\"result\":\"%s\"}", + ids[i], vals[i] ? vals[i] : "0x"); + } + fputs("]\n", f); + fflush(f); +} + +/* + * Run the call via `evm -x -n`, proxying its JSON-RPC requests through + * the provided post function. Collects account state into *accounts. + * Returns a newly-allocated "0x..." output hex string. Caller must free(). + */ +static char *runViaEvm( + const char *self, + const char *to, const char *from, const char *data, + const char *block, + postFn post, void *ctx, + account_t **accounts) +{ + char callJson[512]; + snprintf(callJson, sizeof(callJson), + "{\"to\":\"%s\",\"from\":\"%s\",\"data\":\"%s\"}", + to, from, data); + + FILE *toChild, *fromChild; + pid_t pid = spawnEvm(findEvm(self), callJson, &toChild, &fromChild); + + char *line = malloc(LINE_CAP); + char *output = NULL; + + while (fgets(line, LINE_CAP, fromChild)) { + char *nl = line + strlen(line); + while (nl > line && (nl[-1] == '\n' || nl[-1] == '\r')) *--nl = '\0'; + if (!*line) continue; + + if (*line == '[') { + /* ---- Account batch: [getCode, getTxCount, getBalance] ---- */ + const char *elem0 = jArrayGet(line, 0); + const char *params0 = jFind(elem0, "params"); + char addr[ADDR_LEN]; + jStr(jArrayGet(params0, 0), addr, sizeof(addr)); + account_t *acct = ensureAccount(accounts, addr); + + uint64_t codeId = jUint(jFind(jArrayGet(line, 0), "id")); + uint64_t nonceId = jUint(jFind(jArrayGet(line, 1), "id")); + uint64_t balanceId = jUint(jFind(jArrayGet(line, 2), "id")); + + char *resp = post(line, ctx); + if (!resp) { + fputs("dio: RPC failed for account batch\n", stderr); + _exit(1); + } + + char *code = resultById(resp, codeId); + char *nonce = resultById(resp, nonceId); + char *balance = resultById(resp, balanceId); + free(resp); + + free(acct->code); + acct->code = code ? code : strdup("0x"); + if (nonce) strncpy(acct->nonce, nonce, sizeof(acct->nonce) - 1); + if (balance) strncpy(acct->balance, balance, sizeof(acct->balance) - 1); + free(nonce); + free(balance); + + writeSortedBatch(toChild, + codeId, acct->code, + nonceId, acct->nonce, + balanceId, acct->balance); + + } else if (strstr(line, "\"eth_blockNumber\"")) { + uint64_t id = jUint(jFind(line, "id")); + fprintf(toChild, + "{\"jsonrpc\":\"2.0\",\"id\":%" PRIu64 ",\"result\":\"%s\"}\n", + id, block); + fflush(toChild); + + } else if (strstr(line, "\"eth_getStorageAt\"")) { + const char *params = jFind(line, "params"); + char addr[ADDR_LEN], rawKey[HEX256_LEN]; + jStr(jArrayGet(params, 0), addr, sizeof(addr)); + jStr(jArrayGet(params, 1), rawKey, sizeof(rawKey)); + account_t *acct = ensureAccount(accounts, addr); + + char *resp = post(line, ctx); + if (!resp) { + fputs("dio: RPC failed for eth_getStorageAt\n", stderr); + _exit(1); + } + char val[HEX256_LEN]; + jStr(jFind(resp, "result"), val, sizeof(val)); + addStorage(acct, rawKey, val); + fprintf(toChild, "%s\n", resp); + fflush(toChild); + free(resp); + + } else { + /* ---- Final output from evm ---- */ + const char *ov = jFind(line, "output"); + if (ov && *ov == '"') { + ov++; + const char *end = ov; + while (*end && *end != '"') end++; + size_t len = end - ov; + output = malloc(len + 1); + memcpy(output, ov, len); + output[len] = '\0'; + } + break; + } + } + + fclose(toChild); + fclose(fromChild); + int status; + waitpid(pid, &status, 0); + free(line); + + if (!output || WEXITSTATUS(status) != 0) { + fputs("dio: evm execution failed\n", stderr); + _exit(1); + } + + ensureAccount(accounts, to); + if (strcmp(from, "0x0000000000000000000000000000000000000000") != 0) + ensureAccount(accounts, from); + + return output; +} + +/* ========================================================= + * debug_traceCall path + * ========================================================= */ + +/* + * Walk the structLogs array and build account entries with their + * accessed storage keys. Tracks the call stack to attribute + * SLOAD to the correct contract context. + */ +static void buildAccessList( + const char *structLogs, + const char *to, + const char *from, + account_t **accounts) +{ + static char callStack[1024][ADDR_LEN]; + static int callDepth; + callDepth = 0; + + if (to && *to) { + ensureAccount(accounts, to); + normalizeAddr(to, callStack[callDepth++]); + } + if (from && *from && + strcmp(from, "0x0000000000000000000000000000000000000000") != 0) { + ensureAccount(accounts, from); + } + + const char *p = structLogs; + if (!p || *p != '[') return; + p++; + + while (*p) { + skipWs(&p); + if (*p == ']' || !*p) break; + + const char *entry = p; + char op[32] = ""; + jStr(jFind(entry, "op"), op, sizeof(op)); + const char *stackArr = jFind(entry, "stack"); + + if (strcmp(op, "SLOAD") == 0 && callDepth > 0) { + const char *top[1]; + if (jArrayTailN(stackArr, 1, top) >= 1) { + char rawKey[HEX256_LEN]; + jStr(top[0], rawKey, sizeof(rawKey)); + account_t *acct = ensureAccount(accounts, callStack[callDepth - 1]); + addAccessKey(acct, rawKey); + } + + } else if (strcmp(op, "CALL") == 0 || strcmp(op, "STATICCALL") == 0) { + /* stack: gas(top=-1), addr(-2) */ + const char *top2[2]; + if (jArrayTailN(stackArr, 2, top2) >= 2) { + char raw[HEX256_LEN], addr[ADDR_LEN]; + jStr(top2[0], raw, sizeof(raw)); /* stack[-2] = addr */ + stackToAddress(raw, addr); + ensureAccount(accounts, addr); + if (callDepth < 1024) + normalizeAddr(addr, callStack[callDepth++]); + } + + } else if (strcmp(op, "DELEGATECALL") == 0) { + /* Executes code at stack[-2] in the CALLER's storage context */ + const char *top2[2]; + if (jArrayTailN(stackArr, 2, top2) >= 2) { + char raw[HEX256_LEN], codeAddr[ADDR_LEN]; + jStr(top2[0], raw, sizeof(raw)); /* stack[-2] = code addr */ + stackToAddress(raw, codeAddr); + ensureAccount(accounts, codeAddr); + if (callDepth < 1024) { + const char *ctx = callDepth > 0 ? callStack[callDepth - 1] : codeAddr; + strncpy(callStack[callDepth++], ctx, ADDR_LEN); + } + } + + } else if (strcmp(op, "EXTCODESIZE") == 0 || + strcmp(op, "EXTCODECOPY") == 0) { + const char *top[1]; + if (jArrayTailN(stackArr, 1, top) >= 1) { + char raw[HEX256_LEN], addr[ADDR_LEN]; + jStr(top[0], raw, sizeof(raw)); + stackToAddress(raw, addr); + ensureAccount(accounts, addr); + } + + } else if (strcmp(op, "STOP") == 0 || + strcmp(op, "RETURN") == 0 || + strcmp(op, "REVERT") == 0) { + if (callDepth > 0) callDepth--; + } + + jSkip(&p); + skipWs(&p); + if (*p == ',') p++; + } +} + +/* + * Write the dio JSON config to outfile (stdout if NULL or "-"). + */ +static void writeConfig( + account_t *accounts, + const char *to, + const char *from, + const char *input, + const char *block, + const char *outfile, + const char *output) +{ + FILE *f; + if (outfile && strcmp(outfile, "-") != 0) { + f = fopen(outfile, "w"); + if (!f) { perror(outfile); _exit(1); } + } else { + f = stdout; + } + + fputs("[\n", f); + + for (account_t *a = accounts; a; a = a->next) { + fputs(" {\n", f); + fprintf(f, " \"address\": \"%s\"", a->address); + if (strcmp(a->balance, "0x0") != 0 && strcmp(a->balance, "0x") != 0) + fprintf(f, ",\n \"balance\": \"%s\"", a->balance); + if (strcmp(a->nonce, "0x0") != 0 && strcmp(a->nonce, "0x") != 0) + fprintf(f, ",\n \"nonce\": \"%s\"", a->nonce); + if (strcmp(a->code, "0x") != 0 && strcmp(a->code, "") != 0) + fprintf(f, ",\n \"code\": \"%s\"", a->code); + if (a->storage) { + fputs(",\n \"storage\": {\n", f); + int first = 1; + for (storage_kv_t *s = a->storage; s; s = s->next) { + if (!first) fputs(",\n", f); + fprintf(f, " \"%s\": \"%s\"", s->key, s->value); + first = 0; + } + fputs("\n }", f); + } + fputs("\n },\n", f); + } + + fputs(" {\n \"tests\": [\n {\n", f); + fprintf(f, " \"to\": \"%s\"", to); + if (strcmp(from, "0x0000000000000000000000000000000000000000") != 0) + fprintf(f, ",\n \"from\": \"%s\"", from); + if (input && strcmp(input, "0x") != 0) + fprintf(f, ",\n \"input\": \"%s\"", input); + fprintf(f, ",\n \"blockNumber\": \"%s\"", block); + fputs(",\n \"debug\": \"0x20\"", f); + if (output) + fprintf(f, ",\n \"output\": \"%s\"", output); + fputs("\n }\n ]\n }\n]\n", f); + + if (f != stdout) { + fclose(f); + fprintf(stderr, "Wrote %s\n", outfile); + } +} + +/* + * Main entry point for a single call. Tries debug_traceCall first, + * falls back to runViaEvm on error or unsupported method. + */ +static void run( + const char *callJson, + const char *outfile, + postFn post, void *ctx, + const char *self) +{ + char to[ADDR_LEN] = "0x0000000000000000000000000000000000000000"; + char from[ADDR_LEN] = "0x0000000000000000000000000000000000000000"; + char block[32] = "latest"; + + char tmp[ADDR_LEN + 2]; + if (jStr(jFind(callJson, "to"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, to); + if (jStr(jFind(callJson, "from"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, from); + + const char *blkVal = jFind(callJson, "block"); + if (blkVal) jStr(blkVal, block, sizeof(block)); + + const char *dataField = jFind(callJson, "data"); + if (!dataField) dataField = jFind(callJson, "input"); + char *input = jStrDup(dataField); + if (input[0] != '0' || input[1] != 'x') { + size_t ilen = strlen(input); + char *t = malloc(ilen + 3); + t[0] = '0'; t[1] = 'x'; + memcpy(t + 2, input, ilen + 1); + free(input); + input = t; + } + + /* Resolve block number */ + if (strcmp(block, "latest") == 0) { + char *resp = post( + "{\"jsonrpc\":\"2.0\",\"id\":1," + "\"method\":\"eth_blockNumber\",\"params\":[]}", + ctx); + if (!resp) { fputs("dio: eth_blockNumber failed\n", stderr); _exit(1); } + jStr(jFind(resp, "result"), block, sizeof(block)); + free(resp); + } + + /* Try debug_traceCall */ + strbuf_t callObj = {0}; + sbFmt(&callObj, "{\"to\":\"%s\",\"from\":\"%s\"", to, from); + if (strcmp(input, "0x") != 0) + sbFmt(&callObj, ",\"data\":\"%s\"", input); + sbAppend(&callObj, "}", 1); + + strbuf_t traceReq = {0}; + sbFmt(&traceReq, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"debug_traceCall\"," + "\"params\":[%s,\"%s\"," + "{\"disableStorage\":true,\"disableMemory\":true}]}", + callObj.buf, block); + free(callObj.buf); + + char *traceResp = post(traceReq.buf, ctx); + free(traceReq.buf); + + if (!traceResp || jFind(traceResp, "error")) { + free(traceResp); + account_t *accounts = NULL; + char *output = runViaEvm(self, to, from, input, block, post, ctx, &accounts); + writeConfig(accounts, to, from, input, block, outfile, output); + free(output); + free(input); + return; + } + + /* Parse trace */ + const char *result = jFind(traceResp, "result"); + const char *logsArr = jFind(result, "structLogs"); + char *returnValue = jStrHex(result, "returnValue"); + + /* Build access list */ + account_t *accounts = NULL; + buildAccessList(logsArr, to, from, &accounts); + + /* Batch-fetch all account state */ + strbuf_t batch = {0}; + batch_meta_t *meta = malloc(MAX_BATCH * sizeof(batch_meta_t)); + int bid = 1, metaCount = 0; + + sbAppend(&batch, "[", 1); + int first = 1; + for (account_t *a = accounts; a; a = a->next) { + if (!first) sbAppend(&batch, ",", 1); first = 0; + sbFmt(&batch, + "{\"jsonrpc\":\"2.0\",\"id\":%d," + "\"method\":\"eth_getBalance\",\"params\":[\"%s\",\"%s\"]}", + bid, a->address, block); + strncpy(meta[metaCount].addr, a->address, ADDR_LEN); + meta[metaCount].key[0] = '\0'; + meta[metaCount].field = 0; + metaCount++; bid++; + + sbFmt(&batch, + ",{\"jsonrpc\":\"2.0\",\"id\":%d," + "\"method\":\"eth_getTransactionCount\",\"params\":[\"%s\",\"%s\"]}", + bid, a->address, block); + strncpy(meta[metaCount].addr, a->address, ADDR_LEN); + meta[metaCount].key[0] = '\0'; + meta[metaCount].field = 1; + metaCount++; bid++; + + sbFmt(&batch, + ",{\"jsonrpc\":\"2.0\",\"id\":%d," + "\"method\":\"eth_getCode\",\"params\":[\"%s\",\"%s\"]}", + bid, a->address, block); + strncpy(meta[metaCount].addr, a->address, ADDR_LEN); + meta[metaCount].key[0] = '\0'; + meta[metaCount].field = 2; + metaCount++; bid++; + + for (storage_key_t *k = a->keys; k; k = k->next) { + sbFmt(&batch, + ",{\"jsonrpc\":\"2.0\",\"id\":%d," + "\"method\":\"eth_getStorageAt\"," + "\"params\":[\"%s\",\"%s\",\"%s\"]}", + bid, a->address, k->key, block); + strncpy(meta[metaCount].addr, a->address, ADDR_LEN); + strncpy(meta[metaCount].key, k->key, HEX256_LEN); + meta[metaCount].field = 3; + metaCount++; bid++; + } + } + sbAppend(&batch, "]", 1); + + char *batchResp = NULL; + if (metaCount > 0) { + batchResp = post(batch.buf, ctx); + if (!batchResp) { fputs("dio: batch RPC failed\n", stderr); _exit(1); } + } + free(batch.buf); + + if (batchResp) { + const char *p = batchResp; + if (*p == '[') p++; + while (*p) { + skipWs(&p); + if (*p == ']' || !*p) break; + const char *elem = p; + int id = (int)jUint(jFind(elem, "id")); + if (id >= 1 && id < bid) { + int idx = id - 1; + account_t *a = ensureAccount(&accounts, meta[idx].addr); + const char *rv = jFind(elem, "result"); + switch (meta[idx].field) { + case 0: jStr(rv, a->balance, sizeof(a->balance)); break; + case 1: jStr(rv, a->nonce, sizeof(a->nonce)); break; + case 2: free(a->code); a->code = jStrDup(rv); break; + case 3: { + char val[HEX256_LEN]; + jStr(rv, val, sizeof(val)); + addStorage(a, meta[idx].key, val); + break; + } + } + } + jSkip(&p); + skipWs(&p); + if (*p == ',') p++; + } + free(batchResp); + } + + free(meta); + free(traceResp); + + writeConfig(accounts, to, from, input, block, outfile, returnValue); + free(returnValue); + free(input); +} + +/* ========================================================= + * main + * ========================================================= */ + +static char *readAll(FILE *f) { + strbuf_t sb = {0}; + char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) + sbAppend(&sb, buf, n); + return sb.buf ? sb.buf : strdup(""); +} + +static const char usage[] = + "dio - Generate evm test config files from on-chain state\n" + "\n" + "Usage:\n" + " dio [provider-url] [outfile] [-o json] [file...]\n" + "\n" + "The call JSON (from -o, a file argument, or stdin) is the eth_call object:\n" + " {\"to\": \"0x...\", \"from\": \"0x...\", \"data\": \"0x...\", \"block\": \"latest\"}\n" + "\n" + "Only \"to\" is required. \"block\" defaults to \"latest\".\n" + "The generated config is written to outfile, or stdout if omitted.\n" + "\n" + "Options:\n" + " -o Call JSON inline\n" + "\n" + "The provider URL may also be set via the ETH_RPC_URL environment variable.\n"; + +int main(int argc, char *const argv[]) { + static struct option longopts[] = { + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0 }, + }; + + const char *inlineJson = NULL; + int opt; + while ((opt = getopt_long(argc, (char *const *)argv, "ho:", longopts, NULL)) != -1) { + switch (opt) { + case 'h': fputs(usage, stdout); return 0; + case 'o': inlineJson = optarg; break; + default: fputs(usage, stderr); return 1; + } + } + + const char *url = (optind < argc) ? argv[optind++] : NULL; + const char *outfile = (optind < argc) ? argv[optind++] : NULL; + + if (!url) url = getenv("ETH_RPC_URL"); + if (!url) { fputs(usage, stderr); return 1; } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + int ws = strncmp(url, "ws://", 5) == 0 || + strncmp(url, "wss://", 6) == 0; + postFn post; + void *ctx; + + if (ws) { + CURL *curl = wsConnect(url); + if (!curl) { curl_global_cleanup(); return 1; } + post = wsPost; + ctx = curl; + } else { + http_ctx_t *hctx = malloc(sizeof(http_ctx_t)); + hctx->url = url; + hctx->curl = curl_easy_init(); + post = httpPost; + ctx = hctx; + } + + if (inlineJson) { + run(inlineJson, outfile, post, ctx, argv[0]); + } else if (optind < argc) { + for (; optind < argc; optind++) { + FILE *f = fopen(argv[optind], "r"); + if (!f) { perror(argv[optind]); return 1; } + char *json = readAll(f); + fclose(f); + run(json, outfile, post, ctx, argv[0]); + free(json); + } + } else { + char *json = readAll(stdin); + run(json, outfile, post, ctx, argv[0]); + free(json); + } + + if (ws) { + curl_easy_cleanup(ctx); + } else { + http_ctx_t *hctx = ctx; + curl_easy_cleanup(hctx->curl); + free(hctx); + } + curl_global_cleanup(); + return 0; +} diff --git a/include/ws.h b/include/ws.h new file mode 100644 index 0000000..97db352 --- /dev/null +++ b/include/ws.h @@ -0,0 +1,16 @@ +#pragma once +#include + +/* + * Connect to a ws:// or wss:// URL and perform the HTTP Upgrade handshake. + * Returns the ready curl handle, or NULL on error (message to stderr). + * Caller must curl_easy_cleanup() when done. + */ +CURL *wsConnect(const char *url); + +/* + * postFn-compatible: send payload as a WebSocket text frame and + * return the response as a dynamically-allocated string. + * ctx must be the CURL * returned by wsConnect. Caller must free(). + */ +char *wsPost(const char *payload, void *ctx); diff --git a/src/ws.c b/src/ws.c new file mode 100644 index 0000000..d492cd2 --- /dev/null +++ b/src/ws.c @@ -0,0 +1,225 @@ +#include "ws.h" + +#include +#include +#include +#include +#include + +/* ========================================================= + * Internal helpers + * ========================================================= */ + +/* xorshift32 PRNG for masking keys, seeded lazily from stack address + pid */ +static uint32_t wsMask(void) { + static uint32_t x = 0; + if (!x) x = (uint32_t)(uintptr_t)&x ^ (uint32_t)getpid(); + x ^= x << 13; x ^= x >> 17; x ^= x << 5; + return x; +} + +static int wsSendAll(CURL *curl, const void *buf, size_t n) { + size_t sent = 0; + while (sent < n) { + size_t chunk; + CURLcode rc; + do { rc = curl_easy_send(curl, (const char *)buf + sent, n - sent, &chunk); } + while (rc == CURLE_AGAIN); + if (rc != CURLE_OK) { + fprintf(stderr, "dio: ws send: %s\n", curl_easy_strerror(rc)); + return -1; + } + sent += chunk; + } + return 0; +} + +static int wsRecvExact(CURL *curl, void *buf, size_t n) { + size_t got = 0; + while (got < n) { + size_t chunk; + CURLcode rc; + do { rc = curl_easy_recv(curl, (char *)buf + got, n - got, &chunk); } + while (rc == CURLE_AGAIN); + if (rc != CURLE_OK) { + fprintf(stderr, "dio: ws recv: %s\n", curl_easy_strerror(rc)); + return -1; + } + got += chunk; + } + return 0; +} + +/* Append n bytes to a heap buffer, growing as needed, keeping it NUL-terminated. */ +static void wsAppend(char **buf, size_t *len, size_t *cap, const void *data, size_t n) { + if (*len + n + 1 > *cap) { + size_t nc = *cap ? *cap * 2 : 4096; + while (nc < *len + n + 1) nc *= 2; + *buf = realloc(*buf, nc); + *cap = nc; + } + memcpy(*buf + *len, data, n); + *len += n; + (*buf)[*len] = '\0'; +} + +/* ========================================================= + * HTTP Upgrade handshake + * ========================================================= */ + +/* + * Perform the WebSocket HTTP Upgrade over an already-TCP-connected curl handle. + * host: "hostname" or "hostname:port"; path: "/..." (must start with '/'). + */ +static int wsHandshake(CURL *curl, const char *host, const char *path) { + char req[4096]; + int n = snprintf(req, sizeof(req), + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Origin: null\r\n" + "\r\n", + path, host); + if (wsSendAll(curl, req, (size_t)n) != 0) return -1; + + /* Read response until \r\n\r\n */ + char *rbuf = NULL; + size_t rlen = 0, rcap = 0; + char tmp[4096]; + while (!rbuf || !strstr(rbuf, "\r\n\r\n")) { + size_t chunk; + CURLcode rc; + do { rc = curl_easy_recv(curl, tmp, sizeof(tmp), &chunk); } + while (rc == CURLE_AGAIN); + if (rc != CURLE_OK) { + fprintf(stderr, "dio: ws handshake: %s\n", curl_easy_strerror(rc)); + free(rbuf); + return -1; + } + wsAppend(&rbuf, &rlen, &rcap, tmp, chunk); + } + if (strncmp(rbuf, "HTTP/1.1 101", 12) != 0) { + fprintf(stderr, "dio: ws handshake failed: %.80s\n", rbuf); + free(rbuf); + return -1; + } + free(rbuf); + return 0; +} + +/* ========================================================= + * Public API + * ========================================================= */ + +CURL *wsConnect(const char *url) { + int secure = strncmp(url, "wss://", 6) == 0; + const char *s = url + (secure ? 6 : 5); + + char host[256], path[2048], httpUrl[2400]; + const char *slash = strchr(s, '/'); + if (slash) { + snprintf(host, sizeof(host), "%.*s", (int)(slash - s), s); + snprintf(path, sizeof(path), "%s", slash); + } else { + snprintf(host, sizeof(host), "%s", s); + path[0] = '/'; path[1] = '\0'; + } + snprintf(httpUrl, sizeof(httpUrl), "%s://%s%s", + secure ? "https" : "http", host, path); + + CURL *curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, httpUrl); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 1L); + CURLcode rc = curl_easy_perform(curl); + if (rc != CURLE_OK) { + fprintf(stderr, "dio: ws connect: %s\n", curl_easy_strerror(rc)); + curl_easy_cleanup(curl); + return NULL; + } + if (wsHandshake(curl, host, path) != 0) { + curl_easy_cleanup(curl); + return NULL; + } + return curl; +} + +/* Send a masked text frame (RFC 6455 §5.2). */ +static int wsSendFrame(CURL *curl, const char *msg, size_t len) { + uint8_t hdr[10]; + size_t hlen = 0; + hdr[hlen++] = 0x81; /* FIN | opcode=text */ + if (len < 126) { + hdr[hlen++] = 0x80 | (uint8_t)len; + } else if (len < 65536) { + hdr[hlen++] = 0x80 | 126; + hdr[hlen++] = (uint8_t)(len >> 8); + hdr[hlen++] = (uint8_t)len; + } else { + hdr[hlen++] = 0x80 | 127; + for (int i = 7; i >= 0; i--) hdr[hlen++] = (uint8_t)(len >> (i * 8)); + } + uint32_t m = wsMask(); + uint8_t mask[4] = {(uint8_t)m, (uint8_t)(m>>8), (uint8_t)(m>>16), (uint8_t)(m>>24)}; + hdr[hlen++] = mask[0]; hdr[hlen++] = mask[1]; + hdr[hlen++] = mask[2]; hdr[hlen++] = mask[3]; + if (wsSendAll(curl, hdr, hlen) != 0) return -1; + + char chunk[4096]; + size_t off = 0; + while (off < len) { + size_t n = len - off < sizeof(chunk) ? len - off : sizeof(chunk); + for (size_t i = 0; i < n; i++) + chunk[i] = (char)((uint8_t)msg[off + i] ^ mask[(off + i) & 3]); + if (wsSendAll(curl, chunk, n) != 0) return -1; + off += n; + } + return 0; +} + +/* Receive a complete WebSocket message, reassembling continuation frames. */ +static char *wsRecvMsg(CURL *curl) { + char *msg = NULL; + size_t mlen = 0, mcap = 0; + for (;;) { + uint8_t hdr[2]; + if (wsRecvExact(curl, hdr, 2) != 0) { free(msg); return NULL; } + int fin = hdr[0] & 0x80; + int opcode = hdr[0] & 0x0f; + uint64_t plen = hdr[1] & 0x7f; + if (plen == 126) { + uint8_t ext[2]; + if (wsRecvExact(curl, ext, 2) != 0) { free(msg); return NULL; } + plen = ((uint64_t)ext[0] << 8) | ext[1]; + } else if (plen == 127) { + uint8_t ext[8]; + if (wsRecvExact(curl, ext, 8) != 0) { free(msg); return NULL; } + plen = 0; + for (int i = 0; i < 8; i++) plen = (plen << 8) | ext[i]; + } + char *payload = malloc(plen + 1); + if (wsRecvExact(curl, payload, plen) != 0) { + free(payload); free(msg); return NULL; + } + payload[plen] = '\0'; + if (opcode == 0x08) { free(payload); free(msg); return NULL; } /* Close */ + if (opcode == 0x09) { /* Ping → Pong */ + uint8_t pong[2] = {0x8a, 0x00}; + wsSendAll(curl, pong, 2); + free(payload); continue; + } + wsAppend(&msg, &mlen, &mcap, payload, (size_t)plen); + free(payload); + if (fin) break; + } + return msg ? msg : strdup(""); +} + +char *wsPost(const char *payload, void *ctx) { + CURL *curl = ctx; + if (wsSendFrame(curl, payload, strlen(payload)) != 0) return NULL; + return wsRecvMsg(curl); +} From 9f62a23f7943db611c64be9a40d13fbdaa42ea8f Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sat, 7 Mar 2026 23:14:20 -0600 Subject: [PATCH 09/23] refactor: drop debug_traceCall path from dio, always use runViaEvm Removes buildAccessList and all associated dead code (storage_key_t, batch_meta_t, jArrayTailN, jStrHex, addAccessKey, stackToAddress, sbFmt) since no public RPC supports the structLog tracer for free. Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 344 +--------------------------------------------------------- 1 file changed, 3 insertions(+), 341 deletions(-) diff --git a/dio.c b/dio.c index 4722f6d..bf81470 100644 --- a/dio.c +++ b/dio.c @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -36,7 +35,6 @@ #define HEX256_LEN 68 /* "0x" + 64 hex + NUL */ #define NONCE_LEN 22 /* "0x" + up to 18 hex + NUL */ #define LINE_CAP 131072 /* matches evm's rpcBuf */ -#define MAX_BATCH 8192 /* max requests in one JSON-RPC batch */ typedef struct storage_kv { char *key; @@ -44,29 +42,15 @@ typedef struct storage_kv { struct storage_kv *next; } storage_kv_t; -/* Storage key recorded during trace (before value is fetched) */ -typedef struct storage_key { - char key[HEX256_LEN]; - struct storage_key *next; -} storage_key_t; - typedef struct account { char address[ADDR_LEN]; char balance[HEX256_LEN]; char nonce[NONCE_LEN]; char *code; /* dynamically allocated "0x..." */ storage_kv_t *storage; /* fetched key→value pairs */ - storage_key_t *keys; /* keys to fetch (from access list) */ struct account *next; } account_t; -/* Metadata for one request in a batch, used to route responses */ -typedef struct { - char addr[ADDR_LEN]; - char key[HEX256_LEN]; /* empty if not a storage slot */ - int field; /* 0=balance 1=nonce 2=code 3=storage */ -} batch_meta_t; - /* ========================================================= * Growing string buffer * ========================================================= */ @@ -85,23 +69,6 @@ static void sbAppend(strbuf_t *sb, const char *s, size_t n) { sb->buf[sb->len] = '\0'; } -static void sbFmt(strbuf_t *sb, const char *fmt, ...) { - va_list ap, ap2; - va_start(ap, fmt); - va_copy(ap2, ap); - int n = vsnprintf(NULL, 0, fmt, ap); - va_end(ap); - if (sb->len + (size_t)n + 1 > sb->cap) { - size_t nc = sb->cap ? sb->cap * 2 : 4096; - while (nc < sb->len + (size_t)n + 1) nc *= 2; - sb->buf = realloc(sb->buf, nc); - sb->cap = nc; - } - vsnprintf(sb->buf + sb->len, (size_t)n + 1, fmt, ap2); - va_end(ap2); - sb->len += (size_t)n; -} - /* ========================================================= * Minimal JSON helpers * @@ -233,31 +200,6 @@ static const char *jArrayGet(const char *p, int n) { return NULL; } -/* - * Store pointers to the last n elements (n ≤ 2) of a JSON array into - * out[0..n-1], where out[n-1] is the last element (EVM stack top). - * Returns the actual count stored (may be < n for short arrays). - */ -static int jArrayTailN(const char *arr, int n, const char **out) { - if (!arr || *arr != '[' || n <= 0 || n > 2) return 0; - const char *buf[2]; - int count = 0; - const char *p = arr + 1; - while (*p) { - skipWs(&p); - if (*p == ']') break; - buf[count % n] = p; - count++; - jSkip(&p); - skipWs(&p); - if (*p == ',') p++; - } - int found = count < n ? count : n; - for (int i = 0; i < found; i++) - out[i] = buf[(count - found + i) % n]; - return found; -} - /* * Return a dynamically-allocated copy of the quoted JSON string at p. * Returns "0x" if p is NULL or not a string. Caller must free(). @@ -274,28 +216,6 @@ static char *jStrDup(const char *p) { return s; } -/* - * Extract "key" string from json, prepend "0x" if absent. - * Returns a dynamically-allocated string. Caller must free(). - */ -static char *jStrHex(const char *json, const char *key) { - const char *p = jFind(json, key); - if (!p || *p != '"') return strdup("0x"); - p++; - const char *end = p; - while (*end && *end != '"') end++; - size_t len = end - p; - int has0x = (len >= 2 && p[0] == '0' && p[1] == 'x'); - char *s = malloc(len + (has0x ? 1 : 3)); - if (has0x) { - memcpy(s, p, len); s[len] = '\0'; - } else { - s[0] = '0'; s[1] = 'x'; - memcpy(s + 2, p, len); s[len + 2] = '\0'; - } - return s; -} - /* * In a JSON-RPC batch response array, find the element with "id": targetId * and return a newly-allocated copy of its "result" string value. @@ -385,40 +305,6 @@ static void addStorage(account_t *acct, const char *key, const char *val) { acct->storage = s; } -/* Record a storage key to be fetched for this account (deduped, normalized). */ -static void addAccessKey(account_t *acct, const char *key) { - char norm[HEX256_LEN]; - normalizeKey(key, norm, sizeof(norm)); - for (storage_key_t *k = acct->keys; k; k = k->next) - if (strcmp(k->key, norm) == 0) return; - storage_key_t *k = calloc(1, sizeof(storage_key_t)); - strncpy(k->key, norm, sizeof(k->key) - 1); - k->next = acct->keys; - acct->keys = k; -} - -/* ========================================================= - * EVM stack helpers - * ========================================================= */ - -/* - * Convert a 32-byte stack value ("0x000...address") to a - * 0x-prefixed lowercase 20-byte address string. - */ -static void stackToAddress(const char *val, char *out) { - const char *s = (val[0] == '0' && val[1] == 'x') ? val + 2 : val; - size_t len = strlen(s); - if (len > 40) s += (len - 40); - int pad = (int)(40 - (len < 40 ? len : 40)); - out[0] = '0'; out[1] = 'x'; - for (int i = 0; i < pad; i++) out[2 + i] = '0'; - for (int i = pad; i < 40; i++) { - unsigned char c = (unsigned char)s[i - pad]; - out[2 + i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : (char)c; - } - out[42] = '\0'; -} - /* ========================================================= * Find evm binary * ========================================================= */ @@ -678,104 +564,6 @@ static char *runViaEvm( return output; } -/* ========================================================= - * debug_traceCall path - * ========================================================= */ - -/* - * Walk the structLogs array and build account entries with their - * accessed storage keys. Tracks the call stack to attribute - * SLOAD to the correct contract context. - */ -static void buildAccessList( - const char *structLogs, - const char *to, - const char *from, - account_t **accounts) -{ - static char callStack[1024][ADDR_LEN]; - static int callDepth; - callDepth = 0; - - if (to && *to) { - ensureAccount(accounts, to); - normalizeAddr(to, callStack[callDepth++]); - } - if (from && *from && - strcmp(from, "0x0000000000000000000000000000000000000000") != 0) { - ensureAccount(accounts, from); - } - - const char *p = structLogs; - if (!p || *p != '[') return; - p++; - - while (*p) { - skipWs(&p); - if (*p == ']' || !*p) break; - - const char *entry = p; - char op[32] = ""; - jStr(jFind(entry, "op"), op, sizeof(op)); - const char *stackArr = jFind(entry, "stack"); - - if (strcmp(op, "SLOAD") == 0 && callDepth > 0) { - const char *top[1]; - if (jArrayTailN(stackArr, 1, top) >= 1) { - char rawKey[HEX256_LEN]; - jStr(top[0], rawKey, sizeof(rawKey)); - account_t *acct = ensureAccount(accounts, callStack[callDepth - 1]); - addAccessKey(acct, rawKey); - } - - } else if (strcmp(op, "CALL") == 0 || strcmp(op, "STATICCALL") == 0) { - /* stack: gas(top=-1), addr(-2) */ - const char *top2[2]; - if (jArrayTailN(stackArr, 2, top2) >= 2) { - char raw[HEX256_LEN], addr[ADDR_LEN]; - jStr(top2[0], raw, sizeof(raw)); /* stack[-2] = addr */ - stackToAddress(raw, addr); - ensureAccount(accounts, addr); - if (callDepth < 1024) - normalizeAddr(addr, callStack[callDepth++]); - } - - } else if (strcmp(op, "DELEGATECALL") == 0) { - /* Executes code at stack[-2] in the CALLER's storage context */ - const char *top2[2]; - if (jArrayTailN(stackArr, 2, top2) >= 2) { - char raw[HEX256_LEN], codeAddr[ADDR_LEN]; - jStr(top2[0], raw, sizeof(raw)); /* stack[-2] = code addr */ - stackToAddress(raw, codeAddr); - ensureAccount(accounts, codeAddr); - if (callDepth < 1024) { - const char *ctx = callDepth > 0 ? callStack[callDepth - 1] : codeAddr; - strncpy(callStack[callDepth++], ctx, ADDR_LEN); - } - } - - } else if (strcmp(op, "EXTCODESIZE") == 0 || - strcmp(op, "EXTCODECOPY") == 0) { - const char *top[1]; - if (jArrayTailN(stackArr, 1, top) >= 1) { - char raw[HEX256_LEN], addr[ADDR_LEN]; - jStr(top[0], raw, sizeof(raw)); - stackToAddress(raw, addr); - ensureAccount(accounts, addr); - } - - } else if (strcmp(op, "STOP") == 0 || - strcmp(op, "RETURN") == 0 || - strcmp(op, "REVERT") == 0) { - if (callDepth > 0) callDepth--; - } - - jSkip(&p); - skipWs(&p); - if (*p == ',') p++; - } -} - /* * Write the dio JSON config to outfile (stdout if NULL or "-"). */ @@ -882,136 +670,10 @@ static void run( free(resp); } - /* Try debug_traceCall */ - strbuf_t callObj = {0}; - sbFmt(&callObj, "{\"to\":\"%s\",\"from\":\"%s\"", to, from); - if (strcmp(input, "0x") != 0) - sbFmt(&callObj, ",\"data\":\"%s\"", input); - sbAppend(&callObj, "}", 1); - - strbuf_t traceReq = {0}; - sbFmt(&traceReq, - "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"debug_traceCall\"," - "\"params\":[%s,\"%s\"," - "{\"disableStorage\":true,\"disableMemory\":true}]}", - callObj.buf, block); - free(callObj.buf); - - char *traceResp = post(traceReq.buf, ctx); - free(traceReq.buf); - - if (!traceResp || jFind(traceResp, "error")) { - free(traceResp); - account_t *accounts = NULL; - char *output = runViaEvm(self, to, from, input, block, post, ctx, &accounts); - writeConfig(accounts, to, from, input, block, outfile, output); - free(output); - free(input); - return; - } - - /* Parse trace */ - const char *result = jFind(traceResp, "result"); - const char *logsArr = jFind(result, "structLogs"); - char *returnValue = jStrHex(result, "returnValue"); - - /* Build access list */ account_t *accounts = NULL; - buildAccessList(logsArr, to, from, &accounts); - - /* Batch-fetch all account state */ - strbuf_t batch = {0}; - batch_meta_t *meta = malloc(MAX_BATCH * sizeof(batch_meta_t)); - int bid = 1, metaCount = 0; - - sbAppend(&batch, "[", 1); - int first = 1; - for (account_t *a = accounts; a; a = a->next) { - if (!first) sbAppend(&batch, ",", 1); first = 0; - sbFmt(&batch, - "{\"jsonrpc\":\"2.0\",\"id\":%d," - "\"method\":\"eth_getBalance\",\"params\":[\"%s\",\"%s\"]}", - bid, a->address, block); - strncpy(meta[metaCount].addr, a->address, ADDR_LEN); - meta[metaCount].key[0] = '\0'; - meta[metaCount].field = 0; - metaCount++; bid++; - - sbFmt(&batch, - ",{\"jsonrpc\":\"2.0\",\"id\":%d," - "\"method\":\"eth_getTransactionCount\",\"params\":[\"%s\",\"%s\"]}", - bid, a->address, block); - strncpy(meta[metaCount].addr, a->address, ADDR_LEN); - meta[metaCount].key[0] = '\0'; - meta[metaCount].field = 1; - metaCount++; bid++; - - sbFmt(&batch, - ",{\"jsonrpc\":\"2.0\",\"id\":%d," - "\"method\":\"eth_getCode\",\"params\":[\"%s\",\"%s\"]}", - bid, a->address, block); - strncpy(meta[metaCount].addr, a->address, ADDR_LEN); - meta[metaCount].key[0] = '\0'; - meta[metaCount].field = 2; - metaCount++; bid++; - - for (storage_key_t *k = a->keys; k; k = k->next) { - sbFmt(&batch, - ",{\"jsonrpc\":\"2.0\",\"id\":%d," - "\"method\":\"eth_getStorageAt\"," - "\"params\":[\"%s\",\"%s\",\"%s\"]}", - bid, a->address, k->key, block); - strncpy(meta[metaCount].addr, a->address, ADDR_LEN); - strncpy(meta[metaCount].key, k->key, HEX256_LEN); - meta[metaCount].field = 3; - metaCount++; bid++; - } - } - sbAppend(&batch, "]", 1); - - char *batchResp = NULL; - if (metaCount > 0) { - batchResp = post(batch.buf, ctx); - if (!batchResp) { fputs("dio: batch RPC failed\n", stderr); _exit(1); } - } - free(batch.buf); - - if (batchResp) { - const char *p = batchResp; - if (*p == '[') p++; - while (*p) { - skipWs(&p); - if (*p == ']' || !*p) break; - const char *elem = p; - int id = (int)jUint(jFind(elem, "id")); - if (id >= 1 && id < bid) { - int idx = id - 1; - account_t *a = ensureAccount(&accounts, meta[idx].addr); - const char *rv = jFind(elem, "result"); - switch (meta[idx].field) { - case 0: jStr(rv, a->balance, sizeof(a->balance)); break; - case 1: jStr(rv, a->nonce, sizeof(a->nonce)); break; - case 2: free(a->code); a->code = jStrDup(rv); break; - case 3: { - char val[HEX256_LEN]; - jStr(rv, val, sizeof(val)); - addStorage(a, meta[idx].key, val); - break; - } - } - } - jSkip(&p); - skipWs(&p); - if (*p == ',') p++; - } - free(batchResp); - } - - free(meta); - free(traceResp); - - writeConfig(accounts, to, from, input, block, outfile, returnValue); - free(returnValue); + char *output = runViaEvm(self, to, from, input, block, post, ctx, &accounts); + writeConfig(accounts, to, from, input, block, outfile, output); + free(output); free(input); } From 1e8d416defe37e9ce6080b3ba39e0d3ed73c72a3 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 00:02:04 -0600 Subject: [PATCH 10/23] strip newlines --- dio.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dio.c b/dio.c index bf81470..ec8d8a2 100644 --- a/dio.c +++ b/dio.c @@ -370,6 +370,8 @@ static char *httpPost(const char *payload, void *ctx) { free(resp.buf); return NULL; } + while (resp.len > 0 && (resp.buf[resp.len-1] == '\n' || resp.buf[resp.len-1] == '\r')) + resp.buf[--resp.len] = '\0'; return resp.buf ? resp.buf : strdup(""); } From 7d0e6e4b1d93afd0d54580a0ba4380a3582241a6 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 01:41:49 -0600 Subject: [PATCH 11/23] no special output for network mode --- dio.c | 5 ++--- evm.c | 7 +------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/dio.c b/dio.c index ec8d8a2..e8ce3b8 100644 --- a/dio.c +++ b/dio.c @@ -400,7 +400,7 @@ static pid_t spawnEvm(const char *evm, const char *callJson, dup2(fromFds[1], STDOUT_FILENO); close(toFds[0]); close(toFds[1]); close(fromFds[0]); close(fromFds[1]); - char *args[] = { (char *)evm, "-x", "-n", "-o", (char *)callJson, NULL }; + char *args[] = { (char *)evm, "-glnsxo", (char *)callJson, NULL }; execvp(evm, args); perror(evm); _exit(1); @@ -534,7 +534,7 @@ static char *runViaEvm( } else { /* ---- Final output from evm ---- */ - const char *ov = jFind(line, "output"); + const char *ov = jFind(line, "returnData"); if (ov && *ov == '"') { ov++; const char *end = ov; @@ -617,7 +617,6 @@ static void writeConfig( if (input && strcmp(input, "0x") != 0) fprintf(f, ",\n \"input\": \"%s\"", input); fprintf(f, ",\n \"blockNumber\": \"%s\"", block); - fputs(",\n \"debug\": \"0x20\"", f); if (output) fprintf(f, ",\n \"output\": \"%s\"", output); fputs("\n }\n ]\n }\n]\n", f); diff --git a/evm.c b/evm.c index 70c1052..8c49c18 100644 --- a/evm.c +++ b/evm.c @@ -181,12 +181,7 @@ static void execute(const char *contents) { } evmFinalize(); - if (networkMode) { - fputs("{\"output\":\"0x", stdout); - for (size_t i = 0; i < result.returnData.size; i++) - printf("%02x", result.returnData.content[i]); - fputs("\"}\n", stdout); - } else if (outputJson) { + if (outputJson) { fputs("{\"", stdout); if (includeGas) { printf("gasUsed\":%" PRIu64 ",\"", gas - result.gasRemaining); From 94ce789a752c3b7e79bb5817dc08a13850b0d6af Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 03:38:32 -0500 Subject: [PATCH 12/23] code review - extract JSON helpers to src/json.c / include/json.h - add call_result_t; accumulate all calls into one writeConfig output - pass len to postFn instead of re-running strlen - compute evmPath once in main; drop self param from run/runViaEvm - skip curl cleanup on exit (OS reclaims resources) Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 341 ++++++++++++------------------------------------- include/json.h | 42 ++++++ include/ws.h | 2 +- src/json.c | 151 ++++++++++++++++++++++ src/ws.c | 4 +- 5 files changed, 281 insertions(+), 259 deletions(-) create mode 100644 include/json.h create mode 100644 src/json.c diff --git a/dio.c b/dio.c index e8ce3b8..5d73ef4 100644 --- a/dio.c +++ b/dio.c @@ -16,6 +16,7 @@ * The provider URL may also be set via the ETH_RPC_URL environment variable. */ +#include "json.h" #include "ws.h" #include #include @@ -51,6 +52,15 @@ typedef struct account { struct account *next; } account_t; +typedef struct call_result { + char to[ADDR_LEN]; + char from[ADDR_LEN]; + char block[32]; + char *input; + char *output; + struct call_result *next; +} call_result_t; + /* ========================================================= * Growing string buffer * ========================================================= */ @@ -69,185 +79,6 @@ static void sbAppend(strbuf_t *sb, const char *s, size_t n) { sb->buf[sb->len] = '\0'; } -/* ========================================================= - * Minimal JSON helpers - * - * These cover only the JSON shapes produced/consumed by - * evm -n and well-formed JSON-RPC responses. - * ========================================================= */ - -static void skipWs(const char **p) { - while (**p == ' ' || **p == '\t' || **p == '\r' || **p == '\n') - (*p)++; -} - -/* - * Skip a JSON value starting at *p, advancing *p past it. - * Handles strings, objects, arrays, numbers/literals. - */ -static void jSkip(const char **p) { - skipWs(p); - if (**p == '"') { - (*p)++; - while (**p && **p != '"') { - if (**p == '\\' && (*p)[1]) (*p)++; - (*p)++; - } - if (**p == '"') (*p)++; - return; - } - if (**p == '{' || **p == '[') { - char open = **p, close = (open == '{') ? '}' : ']'; - int depth = 1; - (*p)++; - while (**p && depth > 0) { - if (**p == '"') { - (*p)++; - while (**p && **p != '"') { - if (**p == '\\' && (*p)[1]) (*p)++; - (*p)++; - } - if (**p == '"') (*p)++; - } else if (**p == open) { - depth++; (*p)++; - } else if (**p == close) { - depth--; (*p)++; - } else { - (*p)++; - } - } - return; - } - /* number, true, false, null */ - while (**p && **p != ',' && **p != '}' && **p != ']' && - **p != ' ' && **p != '\t' && **p != '\r' && **p != '\n') - (*p)++; -} - -/* - * Scan forward in p for "key": and return a pointer to the value. - * Sufficient for flat JSON-RPC messages. Returns NULL if not found. - */ -static const char *jFind(const char *p, const char *key) { - size_t klen = strlen(key); - while (*p) { - if (*p++ == '"' && strncmp(p, key, klen) == 0 && p[klen] == '"') { - p += klen + 1; - while (*p == ' ' || *p == '\t') p++; - if (*p++ != ':') continue; - while (*p == ' ' || *p == '\t') p++; - return p; - } - } - return NULL; -} - -/* - * Copy the quoted JSON string at p into buf (NUL-terminated, max buflen). - * Returns char count, or -1 if p is not a quoted string. - */ -static int jStr(const char *p, char *buf, size_t buflen) { - if (!p || *p != '"') return -1; - p++; - size_t i = 0; - while (*p && *p != '"') { - if (*p == '\\' && p[1]) p++; - if (i + 1 < buflen) buf[i++] = *p; - if (*p) p++; - } - buf[i] = '\0'; - return (int)i; -} - -/* - * Parse a JSON number or quoted "0x..." hex at p into uint64_t. - */ -static uint64_t jUint(const char *p) { - if (!p) return 0; - if (*p == '"') p++; - if (p[0] == '0' && p[1] == 'x') { - p += 2; - uint64_t v = 0; - for (unsigned c; (c = (unsigned char)*p); p++) { - if (c >= '0' && c <= '9') v = (v << 4) | (c - '0'); - else if (c >= 'a' && c <= 'f') v = (v << 4) | (c - 'a' + 10); - else if (c >= 'A' && c <= 'F') v = (v << 4) | (c - 'A' + 10); - else break; - } - return v; - } - uint64_t v = 0; - while (*p >= '0' && *p <= '9') v = v * 10 + (*p++ - '0'); - return v; -} - -/* - * Return a pointer to element n (0-based) in the JSON array at '['. - * Returns NULL if out of range. - */ -static const char *jArrayGet(const char *p, int n) { - if (!p || *p != '[') return NULL; - p++; - while (*p) { - skipWs(&p); - if (*p == ']') return NULL; - if (n == 0) return p; - jSkip(&p); - n--; - skipWs(&p); - if (*p == ',') p++; - } - return NULL; -} - -/* - * Return a dynamically-allocated copy of the quoted JSON string at p. - * Returns "0x" if p is NULL or not a string. Caller must free(). - */ -static char *jStrDup(const char *p) { - if (!p || *p != '"') return strdup("0x"); - p++; - const char *end = p; - while (*end && *end != '"') end++; - size_t len = end - p; - char *s = malloc(len + 1); - memcpy(s, p, len); - s[len] = '\0'; - return s; -} - -/* - * In a JSON-RPC batch response array, find the element with "id": targetId - * and return a newly-allocated copy of its "result" string value. - * Caller must free(). Returns NULL if not found. - */ -static char *resultById(const char *resp, uint64_t targetId) { - const char *p = resp; - if (*p == '[') p++; - while (*p) { - skipWs(&p); - if (*p == ']' || !*p) break; - const char *elem = p; - if (jUint(jFind(elem, "id")) == targetId) { - const char *rv = jFind(elem, "result"); - if (rv && *rv == '"') { - rv++; - const char *end = rv; - while (*end && *end != '"') end++; - size_t len = end - rv; - char *s = malloc(len + 1); - memcpy(s, rv, len); - s[len] = '\0'; - return s; - } - } - jSkip(&p); - skipWs(&p); - if (*p == ',') p++; - } - return NULL; -} - /* ========================================================= * Account management * ========================================================= */ @@ -309,7 +140,8 @@ static void addStorage(account_t *acct, const char *key, const char *val) { * Find evm binary * ========================================================= */ -static char evmBinPath[4096]; +static char evmBinPath[4096]; +static const char *evmPath; /* * Look for an "evm" sibling next to the running binary (argv[0]). @@ -334,7 +166,7 @@ static const char *findEvm(const char *self) { * dynamically-allocated NUL-terminated response string. * Caller must free(). Returns NULL on error. * ========================================================= */ -typedef char *(*postFn)(const char *payload, void *ctx); +typedef char *(*postFn)(const char *payload, size_t len, void *ctx); /* ========================================================= * HTTP and WebSocket post implementations @@ -347,7 +179,7 @@ static size_t curlWrite(char *data, size_t sz, size_t n, void *userp) { return sz * n; } -static char *httpPost(const char *payload, void *ctx) { +static char *httpPost(const char *payload, size_t len, void *ctx) { http_ctx_t *hctx = ctx; strbuf_t resp = {0}; @@ -357,7 +189,7 @@ static char *httpPost(const char *payload, void *ctx) { curl_easy_setopt(hctx->curl, CURLOPT_URL, hctx->url); curl_easy_setopt(hctx->curl, CURLOPT_POST, 1L); curl_easy_setopt(hctx->curl, CURLOPT_POSTFIELDS, payload); - curl_easy_setopt(hctx->curl, CURLOPT_POSTFIELDSIZE, (long)strlen(payload)); + curl_easy_setopt(hctx->curl, CURLOPT_POSTFIELDSIZE, (long)len); curl_easy_setopt(hctx->curl, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt(hctx->curl, CURLOPT_WRITEFUNCTION, curlWrite); curl_easy_setopt(hctx->curl, CURLOPT_WRITEDATA, &resp); @@ -445,23 +277,21 @@ static void writeSortedBatch(FILE *f, /* * Run the call via `evm -x -n`, proxying its JSON-RPC requests through - * the provided post function. Collects account state into *accounts. - * Returns a newly-allocated "0x..." output hex string. Caller must free(). + * the provided post function. Collects account state into *accounts + * and writes the output into r->output. */ -static char *runViaEvm( - const char *self, - const char *to, const char *from, const char *data, - const char *block, - postFn post, void *ctx, - account_t **accounts) +static void runViaEvm( + call_result_t *r, + postFn post, void *ctx, + account_t **accounts) { char callJson[512]; snprintf(callJson, sizeof(callJson), "{\"to\":\"%s\",\"from\":\"%s\",\"data\":\"%s\"}", - to, from, data); + r->to, r->from, r->input); FILE *toChild, *fromChild; - pid_t pid = spawnEvm(findEvm(self), callJson, &toChild, &fromChild); + pid_t pid = spawnEvm(evmPath, callJson, &toChild, &fromChild); char *line = malloc(LINE_CAP); char *output = NULL; @@ -483,7 +313,7 @@ static char *runViaEvm( uint64_t nonceId = jUint(jFind(jArrayGet(line, 1), "id")); uint64_t balanceId = jUint(jFind(jArrayGet(line, 2), "id")); - char *resp = post(line, ctx); + char *resp = post(line, nl - line, ctx); if (!resp) { fputs("dio: RPC failed for account batch\n", stderr); _exit(1); @@ -510,7 +340,7 @@ static char *runViaEvm( uint64_t id = jUint(jFind(line, "id")); fprintf(toChild, "{\"jsonrpc\":\"2.0\",\"id\":%" PRIu64 ",\"result\":\"%s\"}\n", - id, block); + id, r->block); fflush(toChild); } else if (strstr(line, "\"eth_getStorageAt\"")) { @@ -520,7 +350,7 @@ static char *runViaEvm( jStr(jArrayGet(params, 1), rawKey, sizeof(rawKey)); account_t *acct = ensureAccount(accounts, addr); - char *resp = post(line, ctx); + char *resp = post(line, nl - line, ctx); if (!resp) { fputs("dio: RPC failed for eth_getStorageAt\n", stderr); _exit(1); @@ -559,24 +389,20 @@ static char *runViaEvm( _exit(1); } - ensureAccount(accounts, to); - if (strcmp(from, "0x0000000000000000000000000000000000000000") != 0) - ensureAccount(accounts, from); + ensureAccount(accounts, r->to); + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) + ensureAccount(accounts, r->from); - return output; + r->output = output; } /* * Write the dio JSON config to outfile (stdout if NULL or "-"). */ static void writeConfig( - account_t *accounts, - const char *to, - const char *from, - const char *input, - const char *block, - const char *outfile, - const char *output) + account_t *accounts, + call_result_t *results, + const char *outfile) { FILE *f; if (outfile && strcmp(outfile, "-") != 0) { @@ -610,16 +436,21 @@ static void writeConfig( fputs("\n },\n", f); } - fputs(" {\n \"tests\": [\n {\n", f); - fprintf(f, " \"to\": \"%s\"", to); - if (strcmp(from, "0x0000000000000000000000000000000000000000") != 0) - fprintf(f, ",\n \"from\": \"%s\"", from); - if (input && strcmp(input, "0x") != 0) - fprintf(f, ",\n \"input\": \"%s\"", input); - fprintf(f, ",\n \"blockNumber\": \"%s\"", block); - if (output) - fprintf(f, ",\n \"output\": \"%s\"", output); - fputs("\n }\n ]\n }\n]\n", f); + fputs(" {\n \"tests\": [\n", f); + for (call_result_t *r = results; r; r = r->next) { + if (r != results) fputs(",\n", f); + fputs(" {\n", f); + fprintf(f, " \"to\": \"%s\"", r->to); + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) + fprintf(f, ",\n \"from\": \"%s\"", r->from); + if (r->input && strcmp(r->input, "0x") != 0) + fprintf(f, ",\n \"input\": \"%s\"", r->input); + fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); + if (r->output) + fprintf(f, ",\n \"output\": \"%s\"", r->output); + fputs("\n }", f); + } + fputs("\n ]\n }\n]\n", f); if (f != stdout) { fclose(f); @@ -628,54 +459,54 @@ static void writeConfig( } /* - * Main entry point for a single call. Tries debug_traceCall first, - * falls back to runViaEvm on error or unsupported method. + * Parse callJson, resolve the block number, run via evm, and append the + * result to *results. Account state accumulates into *accounts. */ static void run( - const char *callJson, - const char *outfile, - postFn post, void *ctx, - const char *self) + const char *callJson, + postFn post, void *ctx, + account_t **accounts, + call_result_t **results) { - char to[ADDR_LEN] = "0x0000000000000000000000000000000000000000"; - char from[ADDR_LEN] = "0x0000000000000000000000000000000000000000"; - char block[32] = "latest"; + call_result_t *r = malloc(sizeof(call_result_t)); + strcpy(r->to, "0x0000000000000000000000000000000000000000"); + strcpy(r->from, "0x0000000000000000000000000000000000000000"); + strcpy(r->block, "latest"); char tmp[ADDR_LEN + 2]; - if (jStr(jFind(callJson, "to"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, to); - if (jStr(jFind(callJson, "from"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, from); + if (jStr(jFind(callJson, "to"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, r->to); + if (jStr(jFind(callJson, "from"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, r->from); const char *blkVal = jFind(callJson, "block"); - if (blkVal) jStr(blkVal, block, sizeof(block)); + if (blkVal) jStr(blkVal, r->block, sizeof(r->block)); const char *dataField = jFind(callJson, "data"); if (!dataField) dataField = jFind(callJson, "input"); - char *input = jStrDup(dataField); - if (input[0] != '0' || input[1] != 'x') { - size_t ilen = strlen(input); + r->input = jStrDup(dataField); + if (r->input[0] != '0' || r->input[1] != 'x') { + size_t ilen = strlen(r->input); char *t = malloc(ilen + 3); t[0] = '0'; t[1] = 'x'; - memcpy(t + 2, input, ilen + 1); - free(input); - input = t; + memcpy(t + 2, r->input, ilen + 1); + free(r->input); + r->input = t; } /* Resolve block number */ - if (strcmp(block, "latest") == 0) { - char *resp = post( + if (strcmp(r->block, "latest") == 0) { + static const char kBlockNumber[] = "{\"jsonrpc\":\"2.0\",\"id\":1," - "\"method\":\"eth_blockNumber\",\"params\":[]}", - ctx); + "\"method\":\"eth_blockNumber\",\"params\":[]}"; + char *resp = post(kBlockNumber, sizeof(kBlockNumber) - 1, ctx); if (!resp) { fputs("dio: eth_blockNumber failed\n", stderr); _exit(1); } - jStr(jFind(resp, "result"), block, sizeof(block)); + jStr(jFind(resp, "result"), r->block, sizeof(r->block)); free(resp); } - account_t *accounts = NULL; - char *output = runViaEvm(self, to, from, input, block, post, ctx, &accounts); - writeConfig(accounts, to, from, input, block, outfile, output); - free(output); - free(input); + runViaEvm(r, post, ctx, accounts); + + r->next = *results; + *results = r; } /* ========================================================= @@ -750,30 +581,28 @@ int main(int argc, char *const argv[]) { ctx = hctx; } + evmPath = findEvm(argv[0]); + + account_t *accounts = NULL; + call_result_t *results = NULL; + if (inlineJson) { - run(inlineJson, outfile, post, ctx, argv[0]); + run(inlineJson, post, ctx, &accounts, &results); } else if (optind < argc) { for (; optind < argc; optind++) { FILE *f = fopen(argv[optind], "r"); if (!f) { perror(argv[optind]); return 1; } char *json = readAll(f); fclose(f); - run(json, outfile, post, ctx, argv[0]); + run(json, post, ctx, &accounts, &results); free(json); } } else { char *json = readAll(stdin); - run(json, outfile, post, ctx, argv[0]); + run(json, post, ctx, &accounts, &results); free(json); } - if (ws) { - curl_easy_cleanup(ctx); - } else { - http_ctx_t *hctx = ctx; - curl_easy_cleanup(hctx->curl); - free(hctx); - } - curl_global_cleanup(); + writeConfig(accounts, results, outfile); return 0; } diff --git a/include/json.h b/include/json.h new file mode 100644 index 0000000..5ea5c74 --- /dev/null +++ b/include/json.h @@ -0,0 +1,42 @@ +#include +#include + +/* + * Minimal JSON helpers for JSON-RPC shapes produced/consumed by evm -n. + */ + +/* + * Scan forward in p for "key": and return a pointer to the value. + * Sufficient for flat JSON-RPC messages. Returns NULL if not found. + */ +const char *jFind(const char *p, const char *key); + +/* + * Copy the quoted JSON string at p into buf (NUL-terminated, max buflen). + * Returns char count, or -1 if p is not a quoted string. + */ +int jStr(const char *p, char *buf, size_t buflen); + +/* + * Parse a JSON number or quoted "0x..." hex at p into uint64_t. + */ +uint64_t jUint(const char *p); + +/* + * Return a pointer to element n (0-based) in the JSON array at '['. + * Returns NULL if out of range. + */ +const char *jArrayGet(const char *p, int n); + +/* + * Return a dynamically-allocated copy of the quoted JSON string at p. + * Returns "0x" if p is NULL or not a string. Caller must free(). + */ +char *jStrDup(const char *p); + +/* + * In a JSON-RPC batch response array, find the element with "id": targetId + * and return a newly-allocated copy of its "result" string value. + * Caller must free(). Returns NULL if not found. + */ +char *resultById(const char *resp, uint64_t targetId); diff --git a/include/ws.h b/include/ws.h index 97db352..85aca27 100644 --- a/include/ws.h +++ b/include/ws.h @@ -13,4 +13,4 @@ CURL *wsConnect(const char *url); * return the response as a dynamically-allocated string. * ctx must be the CURL * returned by wsConnect. Caller must free(). */ -char *wsPost(const char *payload, void *ctx); +char *wsPost(const char *payload, size_t len, void *ctx); diff --git a/src/json.c b/src/json.c new file mode 100644 index 0000000..bb375e5 --- /dev/null +++ b/src/json.c @@ -0,0 +1,151 @@ +#include "json.h" +#include +#include + +static void skipWs(const char **p) { + while (**p == ' ' || **p == '\t' || **p == '\r' || **p == '\n') + (*p)++; +} + +/* + * Skip a JSON value starting at *p, advancing *p past it. + * Handles strings, objects, arrays, numbers/literals. + */ +static void jSkip(const char **p) { + skipWs(p); + if (**p == '"') { + (*p)++; + while (**p && **p != '"') { + if (**p == '\\' && (*p)[1]) (*p)++; + (*p)++; + } + if (**p == '"') (*p)++; + return; + } + if (**p == '{' || **p == '[') { + char open = **p, close = (open == '{') ? '}' : ']'; + int depth = 1; + (*p)++; + while (**p && depth > 0) { + if (**p == '"') { + (*p)++; + while (**p && **p != '"') { + if (**p == '\\' && (*p)[1]) (*p)++; + (*p)++; + } + if (**p == '"') (*p)++; + } else if (**p == open) { + depth++; (*p)++; + } else if (**p == close) { + depth--; (*p)++; + } else { + (*p)++; + } + } + return; + } + /* number, true, false, null */ + while (**p && **p != ',' && **p != '}' && **p != ']' && + **p != ' ' && **p != '\t' && **p != '\r' && **p != '\n') + (*p)++; +} + +const char *jFind(const char *p, const char *key) { + size_t klen = strlen(key); + while (*p) { + if (*p++ == '"' && strncmp(p, key, klen) == 0 && p[klen] == '"') { + p += klen + 1; + while (*p == ' ' || *p == '\t') p++; + if (*p++ != ':') continue; + while (*p == ' ' || *p == '\t') p++; + return p; + } + } + return NULL; +} + +int jStr(const char *p, char *buf, size_t buflen) { + if (!p || *p != '"') return -1; + p++; + size_t i = 0; + while (*p && *p != '"') { + if (*p == '\\' && p[1]) p++; + if (i + 1 < buflen) buf[i++] = *p; + if (*p) p++; + } + buf[i] = '\0'; + return (int)i; +} + +uint64_t jUint(const char *p) { + if (!p) return 0; + if (*p == '"') p++; + if (p[0] == '0' && p[1] == 'x') { + p += 2; + uint64_t v = 0; + for (unsigned c; (c = (unsigned char)*p); p++) { + if (c >= '0' && c <= '9') v = (v << 4) | (c - '0'); + else if (c >= 'a' && c <= 'f') v = (v << 4) | (c - 'a' + 10); + else if (c >= 'A' && c <= 'F') v = (v << 4) | (c - 'A' + 10); + else break; + } + return v; + } + uint64_t v = 0; + while (*p >= '0' && *p <= '9') v = v * 10 + (*p++ - '0'); + return v; +} + +const char *jArrayGet(const char *p, int n) { + if (!p || *p != '[') return NULL; + p++; + while (*p) { + skipWs(&p); + if (*p == ']') return NULL; + if (n == 0) return p; + jSkip(&p); + n--; + skipWs(&p); + if (*p == ',') p++; + } + return NULL; +} + +char *jStrDup(const char *p) { + if (!p || *p != '"') return strdup("0x"); + p++; + const char *end = p; + while (*end && *end != '"') end++; + size_t len = end - p; + char *s = malloc(len + 1); + memcpy(s, p, len); + s[len] = '\0'; + return s; +} + +char *resultById(const char *resp, uint64_t targetId) { + const char *p = resp; + if (*p == '[') p++; + while (*p) { + skipWs(&p); + if (*p == ']' || !*p) break; + const char *elem = p; + if (jUint(jFind(elem, "id")) == targetId) { + const char *rv = jFind(elem, "result"); + if (rv && *rv == '"') { + rv++; + const char *end = rv; + while (*end && *end != '"') end++; + size_t len = end - rv; + char *s = malloc(len + 1); + memcpy(s, rv, len); + s[len] = '\0'; + return s; + } + } + jSkip(&p); + skipWs(&p); + if (*p == ',') p++; + } + return NULL; +} diff --git a/src/ws.c b/src/ws.c index d492cd2..d7215e9 100644 --- a/src/ws.c +++ b/src/ws.c @@ -218,8 +218,8 @@ static char *wsRecvMsg(CURL *curl) { return msg ? msg : strdup(""); } -char *wsPost(const char *payload, void *ctx) { +char *wsPost(const char *payload, size_t len, void *ctx) { CURL *curl = ctx; - if (wsSendFrame(curl, payload, strlen(payload)) != 0) return NULL; + if (wsSendFrame(curl, payload, len) != 0) return NULL; return wsRecvMsg(curl); } From 6253250069345ff0187d3d95f7fed0c6e9c89e3d Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 17:28:29 -0500 Subject: [PATCH 13/23] add gasUsed, logs, status to dio; logIndex to evm -w - dio: capture gasUsed, logs, status from evm output into generated configs - dio: add jValDup for copying raw JSON values - evm -w: parse logIndex from test JSON logs - evm -w: compare logIndex in LogsEqual (optional: zero means unspecified) Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 25 +++++++++++++++---------- include/evm.h | 3 +++ include/json.h | 6 ++++++ src/dio.c | 9 +++++++++ src/json.c | 11 +++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/dio.c b/dio.c index 5d73ef4..09ff2ad 100644 --- a/dio.c +++ b/dio.c @@ -58,6 +58,9 @@ typedef struct call_result { char block[32]; char *input; char *output; + char *logs; + char *status; + char *gasUsed; struct call_result *next; } call_result_t; @@ -364,16 +367,12 @@ static void runViaEvm( } else { /* ---- Final output from evm ---- */ - const char *ov = jFind(line, "returnData"); - if (ov && *ov == '"') { - ov++; - const char *end = ov; - while (*end && *end != '"') end++; - size_t len = end - ov; - output = malloc(len + 1); - memcpy(output, ov, len); - output[len] = '\0'; - } + output = jStrDup(jFind(line, "returnData")); + char gasHex[20]; + snprintf(gasHex, sizeof(gasHex), "0x%" PRIx64, jUint(jFind(line, "gasUsed"))); + r->gasUsed = strdup(gasHex); + r->status = jStrDup(jFind(line, "status")); + r->logs = jValDup(jFind(line, "logs")); break; } } @@ -446,6 +445,12 @@ static void writeConfig( if (r->input && strcmp(r->input, "0x") != 0) fprintf(f, ",\n \"input\": \"%s\"", r->input); fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); + if (r->gasUsed) + fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); + if (r->logs) + fprintf(f, ",\n \"logs\": %s", r->logs); + if (r->status) + fprintf(f, ",\n \"status\": \"%s\"", r->status); if (r->output) fprintf(f, ",\n \"output\": \"%s\"", r->output); fputs("\n }", f); diff --git a/include/evm.h b/include/evm.h index 5a781bd..9269941 100644 --- a/include/evm.h +++ b/include/evm.h @@ -52,6 +52,9 @@ typedef struct logChanges { static int LogsEqual(const logChanges_t *expectedLog, const logChanges_t *actualLog) { while (expectedLog && actualLog) { + if (expectedLog->logIndex && expectedLog->logIndex != actualLog->logIndex) { + return false; + } if (!DataEqual(&expectedLog->data, &actualLog->data)) { return false; } diff --git a/include/json.h b/include/json.h index 5ea5c74..cb4c89d 100644 --- a/include/json.h +++ b/include/json.h @@ -34,6 +34,12 @@ const char *jArrayGet(const char *p, int n); */ char *jStrDup(const char *p); +/* + * Return a dynamically-allocated copy of the raw JSON value at p + * (string, array, object, number, or literal). Caller must free(). + */ +char *jValDup(const char *p); + /* * In a JSON-RPC batch response array, find the element with "id": targetId * and return a newly-allocated copy of its "result" string value. diff --git a/src/dio.c b/src/dio.c index bbd18b0..934e802 100644 --- a/src/dio.c +++ b/src/dio.c @@ -460,6 +460,15 @@ static void jsonScanLog(const char **iter, logChanges_t **prev) { } else if (logHeadingLen == 4 && *logHeading == 'd') { // data jsonScanData(iter, &log->data); + } else if (logHeadingLen == 8 && *logHeading == 'l') { + // logIndex + const char *v = jsonScanStr(iter); + jsonSkipExpectedChar(&v, '0'); + jsonSkipExpectedChar(&v, 'x'); + while (*v != '"') { + log->logIndex = (log->logIndex << 4) | hexString8ToUint8(*v); + v++; + } } else { fprintf(stderr, "Unexpected log heading: "); for (size_t i = 0; i < logHeadingLen; i++) { diff --git a/src/json.c b/src/json.c index bb375e5..4bde7bf 100644 --- a/src/json.c +++ b/src/json.c @@ -111,6 +111,17 @@ const char *jArrayGet(const char *p, int n) { return NULL; } +char *jValDup(const char *p) { + if (!p) return NULL; + const char *start = p; + jSkip(&p); + size_t len = p - start; + char *s = malloc(len + 1); + memcpy(s, start, len); + s[len] = '\0'; + return s; +} + char *jStrDup(const char *p) { if (!p || *p != '"') return strdup("0x"); p++; From 81e8c402446b8dfb8faf65eaa21e5159d8be9d7a Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 20:41:29 -0500 Subject: [PATCH 14/23] code review - add fprintCompact256 (trims leading zeros) to uint256 - use fprintCompact256 for log topics and evm -xs status output - use fprint256 for eth_getStorageAt key in network.c - replace no-param printf/fprintf with fputs/fputc/putchar/puts Co-Authored-By: Claude Sonnet 4.6 --- evm.c | 12 ++++------- include/uint256.h | 1 + src/dio.c | 12 +++++------ src/evm.c | 52 +++++++++++++++++++---------------------------- src/network.c | 11 +++++----- src/path.c | 2 +- src/uint256.c | 21 +++++++++++++++++++ 7 files changed, 59 insertions(+), 52 deletions(-) diff --git a/evm.c b/evm.c index 8c49c18..833a2c7 100644 --- a/evm.c +++ b/evm.c @@ -38,7 +38,7 @@ static void assemble(const char *contents) { scanInit(); for (; scanValid(&contents); programLength++) { if (programLength > (PROGRAM_BUFFER_LENGTH - CONSTRUCTOR_OFFSET)) { - fprintf(stderr, "Program size exceeds limit; terminating"); + fputs("Program size exceeds limit; terminating", stderr); break; } programStart[programLength] = scanNextOp(&contents); @@ -192,13 +192,9 @@ static void execute(const char *contents) { fputs(",\"", stdout); } if (includeStatus) { - printf( - "status\":\"0x%08" PRIx64 "%08" PRIx64 "%08" PRIx64 "%08" PRIx64 "\",\"", - UPPER(UPPER(result.status)), - LOWER(UPPER(result.status)), - UPPER(LOWER(result.status)), - LOWER(LOWER(result.status)) - ); + fputs("status\":\"", stdout); + fprintCompact256(stdout, &result.status); + fputs("\",\"", stdout); } fputs("returnData\":\"0x", stdout); for (;result.returnData.size--;) printf("%02x", *result.returnData.content++); diff --git a/include/uint256.h b/include/uint256.h index 08e4e06..f5d3633 100644 --- a/include/uint256.h +++ b/include/uint256.h @@ -42,6 +42,7 @@ void dumpu256BE(const uint256_t *source, uint8_t *target); void fprint128(FILE *, const uint128_t *); void fprint256(FILE *, const uint256_t *); void fprint512(FILE *, const uint512_t *); +void fprintCompact256(FILE *, const uint256_t *); bool zero128(const uint128_t *number); bool zero256(const uint256_t *number); void copy128(uint128_t *target, const uint128_t *number); diff --git a/src/dio.c b/src/dio.c index 934e802..5b89ebc 100644 --- a/src/dio.c +++ b/src/dio.c @@ -266,9 +266,9 @@ static void reportResult(testEntry_t *test, result_t *result, uint64_t gas, cons // less actual gasUsed than expected fprintf(stderr, "gasUsed \033[0;32m%" PRIu64 "\033[0m expected %" PRIu64 " (\033[0;32m-%" PRIu64 "\033[0m)\n", gasUsed, test->gasUsed, test->gasUsed - gasUsed); } else if (testFailure) { - fprintf(stderr, "\033[0;31mfail\033[0m\n"); + fputs("\033[0;31mfail\033[0m\n", stderr); } else { - fprintf(stderr, "\033[0;32mpass\033[0m\n"); + fputs("\033[0;32mpass\033[0m\n", stderr); } } else if (testFailure) { fprintf(stderr, "\033[0;31mfail\033[0m\n"); @@ -319,9 +319,9 @@ static void verifyConstructResult(result_t *constructResult, entry_t *entry) { if (constructResult->returnData.size != entry->code.size || memcmp(constructResult->returnData.content, entry->code.content, entry->code.size)) { fputs("Code mismatch at address ", stderr); fprintAddress(stderr, (*entry->address)); - fprintf(stderr, ":\ninitcode result:\n"); + fputs(":\ninitcode result:\n", stderr); fprintData(stderr, constructResult->returnData); - fprintf(stderr, "\nexpected:\n"); + fputs("\nexpected:\n", stderr); fprintData(stderr, entry->code); fputc('\n', stderr); _exit(-1); @@ -353,7 +353,7 @@ static void applyEntry(entry_t *entry) { if (entry->constructTest) { if (!AddressZero(&entry->constructTest->from)) { if (entry->creator && !AddressEqual(&entry->constructTest->from, entry->creator)) { - fprintf(stderr, "constructTest.from conflicts with creator\n"); + fputs("constructTest.from conflicts with creator\n", stderr); _exit(1); } AddressCopy(from, entry->constructTest->from); @@ -470,7 +470,7 @@ static void jsonScanLog(const char **iter, logChanges_t **prev) { v++; } } else { - fprintf(stderr, "Unexpected log heading: "); + fputs("Unexpected log heading: ", stderr); for (size_t i = 0; i < logHeadingLen; i++) { fputc(logHeading[i], stderr); } diff --git a/src/evm.c b/src/evm.c index ab83dc5..e44f060 100644 --- a/src/evm.c +++ b/src/evm.c @@ -27,15 +27,10 @@ uint16_t fprintLog(FILE *file, const logChanges_t *log, int showLogIndex) { } fputs("\",\"topics\":[", file); for (uint8_t i = log->topicCount; i-->0;) { - fprintf(file, "\"0x%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "\"", - UPPER(UPPER(log->topics[i])), - LOWER(UPPER(log->topics[i])), - UPPER(LOWER(log->topics[i])), - LOWER(LOWER(log->topics[i])) - ); - if (i) { - fputc(',', file); - } + fputc('"', file); + fprintCompact256(file, &log->topics[i]); + fputc('"', file); + if (i) fputc(',', file); } fputs("]}", file); return items + 1; @@ -94,15 +89,10 @@ uint16_t fprintLogDiff(FILE *file, const logChanges_t *log, const logChanges_t * } } else { for (uint8_t i = log->topicCount; i-->0;) { - fprintf(file, "\"0x%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "\"", - UPPER(UPPER(log->topics[i])), - LOWER(UPPER(log->topics[i])), - UPPER(LOWER(log->topics[i])), - LOWER(LOWER(log->topics[i])) - ); - if (i) { - fputc(',', file); - } + fputc('"', file); + fprintCompact256(file, &log->topics[i]); + fputc('"', file); + if (i) fputc(',', file); } } fputs("]}", file); @@ -719,25 +709,25 @@ static result_t evmCreate2(account_t *fromAccount, uint64_t gas, val_t value, da static result_t doCall(context_t *callContext) { if (SHOW_CALLS) { INDENT; - fprintf(stderr, "from: "); + fputs("from: ", stderr); fprintAddress(stderr, callContext->caller); - fprintf(stderr, "\n"); + fputc('\n', stderr); if (callContext->account) { INDENT; - fprintf(stderr, "to: "); + fputs("to: ", stderr); fprintAddress(stderr, callContext->account->address); - fprintf(stderr, "\n"); + fputc('\n', stderr); } if (!ValueIsZero(callContext->callValue)) { INDENT; - fprintf(stderr, "value: "); + fputs("value: ", stderr); fprintVal(stderr, callContext->callValue); - fprintf(stderr, "\n"); + fputc('\n', stderr); } INDENT; - fprintf(stderr, "input: "); + fputs("input: ", stderr); dumpCallData(callContext); } @@ -1309,9 +1299,9 @@ static result_t doCall(context_t *callContext) { stateChanges_t *stateChanges = getCurrentAccountStateChanges(&result, callContext); if (SHOW_LOGS) { - fprintf(stderr, "\033[94m"); + fputs("\033[94m", stderr); fprintLog(stderr, log, true); - fprintf(stderr, "\033[0m\n"); + fputs("\033[0m\n", stderr); } log->prev = stateChanges->logChanges; stateChanges->logChanges = log; @@ -1750,13 +1740,13 @@ static result_t doCall(context_t *callContext) { if (SHOW_CALLS) { INDENT; if (zero256(&result.status)) { - fprintf(stderr, "\033[0;31m"); + fputs("\033[0;31m", stderr); } - fprintf(stderr, "output: "); + fputs("output: ", stderr); fprintData(stderr, result.returnData); - fprintf(stderr, "\n"); + fputc('\n', stderr); if (zero256(&result.status)) { - fprintf(stderr, "\033[0m"); + fputs("\033[0m", stderr); } } return result; diff --git a/src/network.c b/src/network.c index 2b9f892..bab5928 100644 --- a/src/network.c +++ b/src/network.c @@ -54,7 +54,7 @@ static void ensureNetworkBlock(void) { static void networkFetchAccount(address_t address) { ensureNetworkBlock(); uint32_t base = ++rpcId; rpcId += 2; - printf("["); + putchar('['); printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getCode\",\"params\":[\"", base); fprintAddress(stdout, address); printf("\",\"%s\"]},", networkBlockHex); @@ -64,7 +64,7 @@ static void networkFetchAccount(address_t address) { printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getBalance\",\"params\":[\"", base + 2); fprintAddress(stdout, address); printf("\",\"%s\"]}", networkBlockHex); - printf("]\n"); + puts("]"); fflush(stdout); if (!fgets(rpcBuf, sizeof(rpcBuf), stdin)) { @@ -110,10 +110,9 @@ static void networkFetchStorage(address_t address, const uint256_t *key, uint256 ensureNetworkBlock(); printf("{\"jsonrpc\":\"2.0\",\"id\":%u,\"method\":\"eth_getStorageAt\",\"params\":[\"", ++rpcId); fprintAddress(stdout, address); - printf("\",\"0x%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "%016" PRIx64 "\",\"%s\"]}\n", - UPPER(UPPER_P(key)), LOWER(UPPER_P(key)), - UPPER(LOWER_P(key)), LOWER(LOWER_P(key)), - networkBlockHex); + fputs("\",\"0x", stdout); + fprint256(stdout, key); + printf("\",\"%s\"]}\n", networkBlockHex); fflush(stdout); if (!fgets(rpcBuf, sizeof(rpcBuf), stdin)) { diff --git a/src/path.c b/src/path.c index 11d807f..f34208f 100644 --- a/src/path.c +++ b/src/path.c @@ -23,7 +23,7 @@ static const char *derivePath() { return derivedPath; } if (!selfPath) { - fprintf(stderr, "must pathInit\n"); + fputs("must pathInit\n", stderr); exit(1); } if (selfPath[0] == '/') { diff --git a/src/uint256.c b/src/uint256.c index dd51346..d3d347c 100644 --- a/src/uint256.c +++ b/src/uint256.c @@ -77,6 +77,27 @@ void fprint512(FILE *fp, const uint512_t *number) { fprint256(fp, &LOWER_P(number)); } +static int fprintCompact64(FILE *fp, uint64_t v, int started) { + if (started) { + fprintf(fp, "%016" PRIx64, v); + return 1; + } + if (v == 0) return 0; + fprintf(fp, "0x%" PRIx64, v); + return 1; +} + +static int fprintCompact128(FILE *fp, const uint128_t *number, int started) { + return fprintCompact64(fp, LOWER_P(number), + fprintCompact64(fp, UPPER_P(number), started)); +} + +void fprintCompact256(FILE *fp, const uint256_t *number) { + if (!fprintCompact128(fp, &LOWER_P(number), + fprintCompact128(fp, &UPPER_P(number), 0))) + fputs("0x0", fp); +} + bool zero128(const uint128_t *number) { return ((LOWER_P(number) == 0) && (UPPER_P(number) == 0)); } From b038606d390acc313483a4ec81246e7c56b96338 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 21:19:09 -0500 Subject: [PATCH 15/23] feat: improve RPC error messages in dio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add jArrayNext for sequential array iteration, then use it in three new helpers — rpcFailed (network failure), rpcError (RPC-level error), checkBatchErrors (per-element batch errors matched by id to method). All RPC call sites now report the method name and error object on failure instead of a generic message, and eth_blockNumber is checked for errors before its result is used, preventing "latest" from leaking into the evm subprocess. Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 65 +++++++++++++++++++++++++++++++++++++++++++------- include/json.h | 6 +++++ src/json.c | 9 +++++++ 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/dio.c b/dio.c index 09ff2ad..062087b 100644 --- a/dio.c +++ b/dio.c @@ -278,6 +278,54 @@ static void writeSortedBatch(FILE *f, fflush(f); } +static void printMethod(const char *obj) { + char method[64]; + if (jStr(jFind(obj, "method"), method, sizeof(method)) > 0) { + fputc(' ', stderr); + fputs(method, stderr); + } +} + +/* Print "dio: RPC failed: \n" and exit for a request that got NULL */ +static void rpcFailed(const char *req) { + fputs("dio: RPC failed:", stderr); + if (*req == '[') { + for (const char *elem = jArrayGet(req, 0); elem; elem = jArrayNext(elem)) + printMethod(elem); + } else { + printMethod(req); + } + fputc('\n', stderr); + _exit(1); +} + +/* Print "dio: error: \n" and exit */ +static void rpcError(const char *req, const char *errField) { + fputs("dio:", stderr); + printMethod(req); + fputs(" error: ", stderr); + char *errStr = jValDup(errField); + fputs(errStr ? errStr : "?", stderr); + free(errStr); + fputc('\n', stderr); + _exit(1); +} + +/* Check a batch resp for error objects; match method names from the batch req */ +static void checkBatchErrors(const char *resp, const char *req) { + const char *qHead = jArrayGet(req, 0); + for (const char *rElem = jArrayGet(resp, 0); rElem; rElem = jArrayNext(rElem)) { + const char *errField = jFind(rElem, "error"); + if (!errField) continue; + uint64_t id = jUint(jFind(rElem, "id")); + for (const char *qElem = qHead; qElem; qElem = jArrayNext(qElem)) { + if (jUint(jFind(qElem, "id")) == id) + rpcError(qElem, errField); + } + rpcError(qHead, errField); + } +} + /* * Run the call via `evm -x -n`, proxying its JSON-RPC requests through * the provided post function. Collects account state into *accounts @@ -317,10 +365,8 @@ static void runViaEvm( uint64_t balanceId = jUint(jFind(jArrayGet(line, 2), "id")); char *resp = post(line, nl - line, ctx); - if (!resp) { - fputs("dio: RPC failed for account batch\n", stderr); - _exit(1); - } + if (!resp) rpcFailed(line); + checkBatchErrors(resp, line); char *code = resultById(resp, codeId); char *nonce = resultById(resp, nonceId); @@ -354,10 +400,9 @@ static void runViaEvm( account_t *acct = ensureAccount(accounts, addr); char *resp = post(line, nl - line, ctx); - if (!resp) { - fputs("dio: RPC failed for eth_getStorageAt\n", stderr); - _exit(1); - } + if (!resp) rpcFailed(line); + const char *errField = jFind(resp, "error"); + if (errField) rpcError(line, errField); char val[HEX256_LEN]; jStr(jFind(resp, "result"), val, sizeof(val)); addStorage(acct, rawKey, val); @@ -503,7 +548,9 @@ static void run( "{\"jsonrpc\":\"2.0\",\"id\":1," "\"method\":\"eth_blockNumber\",\"params\":[]}"; char *resp = post(kBlockNumber, sizeof(kBlockNumber) - 1, ctx); - if (!resp) { fputs("dio: eth_blockNumber failed\n", stderr); _exit(1); } + if (!resp) rpcFailed(kBlockNumber); + const char *errField = jFind(resp, "error"); + if (errField) rpcError(kBlockNumber, errField); jStr(jFind(resp, "result"), r->block, sizeof(r->block)); free(resp); } diff --git a/include/json.h b/include/json.h index cb4c89d..3db4f35 100644 --- a/include/json.h +++ b/include/json.h @@ -28,6 +28,12 @@ uint64_t jUint(const char *p); */ const char *jArrayGet(const char *p, int n); +/* + * Advance past the element at p and return a pointer to the next element + * in the enclosing array, or NULL if the end of the array is reached. + */ +const char *jArrayNext(const char *p); + /* * Return a dynamically-allocated copy of the quoted JSON string at p. * Returns "0x" if p is NULL or not a string. Caller must free(). diff --git a/src/json.c b/src/json.c index 4bde7bf..2764feb 100644 --- a/src/json.c +++ b/src/json.c @@ -134,6 +134,15 @@ char *jStrDup(const char *p) { return s; } +const char *jArrayNext(const char *p) { + jSkip(&p); + skipWs(&p); + if (*p == ',') p++; + skipWs(&p); + if (!*p || *p == ']') return NULL; + return p; +} + char *resultById(const char *resp, uint64_t targetId) { const char *p = resp; if (*p == '[') p++; From b5a1e154b6838539e00e289a1359554904198ba2 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 21:34:08 -0500 Subject: [PATCH 16/23] code review: jArrayNext, json tests Replace sequential jArrayGet(line, N) calls in runViaEvm with jArrayNext to avoid restarting the scan from the beginning each time. Add tst/json.c covering jFind, jStr, jUint, jArrayGet, jArrayNext, jStrDup, jValDup, and resultById. Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 13 ++-- tst/json.c | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 tst/json.c diff --git a/dio.c b/dio.c index 062087b..c2f9354 100644 --- a/dio.c +++ b/dio.c @@ -360,9 +360,11 @@ static void runViaEvm( jStr(jArrayGet(params0, 0), addr, sizeof(addr)); account_t *acct = ensureAccount(accounts, addr); - uint64_t codeId = jUint(jFind(jArrayGet(line, 0), "id")); - uint64_t nonceId = jUint(jFind(jArrayGet(line, 1), "id")); - uint64_t balanceId = jUint(jFind(jArrayGet(line, 2), "id")); + const char *elem1 = jArrayNext(elem0); + const char *elem2 = jArrayNext(elem1); + uint64_t codeId = jUint(jFind(elem0, "id")); + uint64_t nonceId = jUint(jFind(elem1, "id")); + uint64_t balanceId = jUint(jFind(elem2, "id")); char *resp = post(line, nl - line, ctx); if (!resp) rpcFailed(line); @@ -395,8 +397,9 @@ static void runViaEvm( } else if (strstr(line, "\"eth_getStorageAt\"")) { const char *params = jFind(line, "params"); char addr[ADDR_LEN], rawKey[HEX256_LEN]; - jStr(jArrayGet(params, 0), addr, sizeof(addr)); - jStr(jArrayGet(params, 1), rawKey, sizeof(rawKey)); + const char *param0 = jArrayGet(params, 0); + jStr(param0, addr, sizeof(addr)); + jStr(jArrayNext(param0), rawKey, sizeof(rawKey)); account_t *acct = ensureAccount(accounts, addr); char *resp = post(line, nl - line, ctx); diff --git a/tst/json.c b/tst/json.c new file mode 100644 index 0000000..ce64ede --- /dev/null +++ b/tst/json.c @@ -0,0 +1,202 @@ +#include "json.h" + +#include +#include +#include + +void test_jFind() { + char buf[64]; + const char *obj = "{\"foo\":\"bar\",\"baz\":42}"; + + // string value + assert(jStr(jFind(obj, "foo"), buf, sizeof(buf)) == 3); + assert(strcmp(buf, "bar") == 0); + + // numeric value + assert(jUint(jFind(obj, "baz")) == 42); + + // missing key + assert(jFind(obj, "missing") == NULL); + + // nested: finds key in inner object + const char *nested = "{\"a\":{\"b\":\"c\"}}"; + assert(jStr(jFind(nested, "b"), buf, sizeof(buf)) == 1); + assert(strcmp(buf, "c") == 0); + + // no false match on prefix + const char *prefixed = "{\"foobar\":1,\"foo\":2}"; + assert(jUint(jFind(prefixed, "foo")) == 2); +} + +void test_jStr() { + char buf[64]; + + assert(jStr("\"hello\"", buf, sizeof(buf)) == 5); + assert(strcmp(buf, "hello") == 0); + + // strips outer quotes, returns length + assert(jStr("\"0xdeadbeef\"", buf, sizeof(buf)) == 10); + assert(strcmp(buf, "0xdeadbeef") == 0); + + // not a string + assert(jStr("123", buf, sizeof(buf)) == -1); + assert(jStr(NULL, buf, sizeof(buf)) == -1); + + // truncates to fit buflen (stores buflen-1 chars + NUL) + assert(jStr("\"abcde\"", buf, 4) == 3); + assert(strcmp(buf, "abc") == 0); +} + +void test_jUint() { + // decimal + assert(jUint("42") == 42); + assert(jUint("0") == 0); + + // hex string + assert(jUint("\"0xff\"") == 255); + assert(jUint("\"0x0\"") == 0); + assert(jUint("\"0x1a2b\"") == 0x1a2b); + + // unquoted hex + assert(jUint("0x10") == 16); + + // NULL + assert(jUint(NULL) == 0); +} + +void test_jArrayGet() { + char buf[8]; + const char *arr = "[\"a\",\"b\",\"c\"]"; + + assert(jStr(jArrayGet(arr, 0), buf, sizeof(buf)) == 1); + assert(strcmp(buf, "a") == 0); + assert(jStr(jArrayGet(arr, 1), buf, sizeof(buf)) == 1); + assert(strcmp(buf, "b") == 0); + assert(jStr(jArrayGet(arr, 2), buf, sizeof(buf)) == 1); + assert(strcmp(buf, "c") == 0); + + // out of range + assert(jArrayGet(arr, 3) == NULL); + + // not an array + assert(jArrayGet("{}", 0) == NULL); + assert(jArrayGet(NULL, 0) == NULL); +} + +void test_jArrayNext() { + const char *arr = "[\"a\",\"b\",\"c\"]"; + char buf[8]; + + const char *e0 = jArrayGet(arr, 0); + const char *e1 = jArrayNext(e0); + const char *e2 = jArrayNext(e1); + const char *e3 = jArrayNext(e2); + + assert(e0 != NULL); + assert(e1 != NULL); + assert(e2 != NULL); + assert(e3 == NULL); + + jStr(e0, buf, sizeof(buf)); assert(strcmp(buf, "a") == 0); + jStr(e1, buf, sizeof(buf)); assert(strcmp(buf, "b") == 0); + jStr(e2, buf, sizeof(buf)); assert(strcmp(buf, "c") == 0); + + // single-element array + const char *single = "[42]"; + assert(jArrayGet(single, 0) != NULL); + assert(jArrayNext(jArrayGet(single, 0)) == NULL); + + // object elements + const char *objarr = "[{\"id\":1},{\"id\":2}]"; + const char *o0 = jArrayGet(objarr, 0); + const char *o1 = jArrayNext(o0); + const char *o2 = jArrayNext(o1); + assert(o0 != NULL); + assert(o1 != NULL); + assert(o2 == NULL); + assert(jUint(jFind(o0, "id")) == 1); + assert(jUint(jFind(o1, "id")) == 2); +} + +void test_jStrDup() { + char *s = jStrDup("\"hello\""); + assert(strcmp(s, "hello") == 0); + free(s); + + // NULL or non-string → "0x" + s = jStrDup(NULL); + assert(strcmp(s, "0x") == 0); + free(s); + + s = jStrDup("123"); + assert(strcmp(s, "0x") == 0); + free(s); +} + +void test_jValDup() { + // string + char *v = jValDup("\"hello\" rest"); + assert(strcmp(v, "\"hello\"") == 0); + free(v); + + // number + v = jValDup("42,next"); + assert(strcmp(v, "42") == 0); + free(v); + + // object + v = jValDup("{\"a\":1},rest"); + assert(strcmp(v, "{\"a\":1}") == 0); + free(v); + + // array + v = jValDup("[1,2,3]end"); + assert(strcmp(v, "[1,2,3]") == 0); + free(v); + + // NULL + v = jValDup(NULL); + assert(v == NULL); +} + +void test_resultById() { + const char *batch = + "[{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":\"0xbb\"}," + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xaa\"}," + "{\"jsonrpc\":\"2.0\",\"id\":3,\"result\":\"0xcc\"}]"; + + char *r1 = resultById(batch, 1); + assert(r1 != NULL); + assert(strcmp(r1, "0xaa") == 0); + free(r1); + + char *r2 = resultById(batch, 2); + assert(r2 != NULL); + assert(strcmp(r2, "0xbb") == 0); + free(r2); + + char *r3 = resultById(batch, 3); + assert(r3 != NULL); + assert(strcmp(r3, "0xcc") == 0); + free(r3); + + // missing id + assert(resultById(batch, 99) == NULL); + + // element with error instead of result + const char *withErr = + "[{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32000}}]"; + assert(resultById(withErr, 1) == NULL); +} + +int main() { + test_jFind(); + test_jStr(); + test_jUint(); + test_jArrayGet(); + test_jArrayNext(); + test_jStrDup(); + test_jValDup(); + test_resultById(); + return 0; +} From 6f9a65221cbef66b097db22d97b0ea4271b7f67d Mon Sep 17 00:00:00 2001 From: William Morriss Date: Sun, 8 Mar 2026 21:49:14 -0500 Subject: [PATCH 17/23] feat: support value field in dio call JSON Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/dio.c b/dio.c index c2f9354..9fce970 100644 --- a/dio.c +++ b/dio.c @@ -56,6 +56,7 @@ typedef struct call_result { char to[ADDR_LEN]; char from[ADDR_LEN]; char block[32]; + char value[HEX256_LEN]; char *input; char *output; char *logs; @@ -336,13 +337,19 @@ static void runViaEvm( postFn post, void *ctx, account_t **accounts) { - char callJson[512]; - snprintf(callJson, sizeof(callJson), - "{\"to\":\"%s\",\"from\":\"%s\",\"data\":\"%s\"}", - r->to, r->from, r->input); + strbuf_t cj = {0}; +#define sbLit(s) sbAppend(&cj, s, sizeof(s) - 1) +#define sbStr(s) sbAppend(&cj, s, strlen(s)) + sbLit("{\"to\":\""); sbStr(r->to); + sbLit("\",\"from\":\""); sbStr(r->from); + sbLit("\",\"data\":\""); sbStr(r->input); + if (r->value[0]) { sbLit("\",\"value\":\""); sbStr(r->value); } + sbLit("\"}"); +#undef sbLit +#undef sbStr FILE *toChild, *fromChild; - pid_t pid = spawnEvm(evmPath, callJson, &toChild, &fromChild); + pid_t pid = spawnEvm(evmPath, cj.buf, &toChild, &fromChild); char *line = malloc(LINE_CAP); char *output = NULL; @@ -430,6 +437,7 @@ static void runViaEvm( int status; waitpid(pid, &status, 0); free(line); + free(cj.buf); if (!output || WEXITSTATUS(status) != 0) { fputs("dio: evm execution failed\n", stderr); @@ -490,6 +498,8 @@ static void writeConfig( fprintf(f, " \"to\": \"%s\"", r->to); if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) fprintf(f, ",\n \"from\": \"%s\"", r->from); + if (r->value[0]) + fprintf(f, ",\n \"value\": \"%s\"", r->value); if (r->input && strcmp(r->input, "0x") != 0) fprintf(f, ",\n \"input\": \"%s\"", r->input); fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); @@ -525,10 +535,12 @@ static void run( strcpy(r->to, "0x0000000000000000000000000000000000000000"); strcpy(r->from, "0x0000000000000000000000000000000000000000"); strcpy(r->block, "latest"); + r->value[0] = '\0'; char tmp[ADDR_LEN + 2]; if (jStr(jFind(callJson, "to"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, r->to); if (jStr(jFind(callJson, "from"), tmp, sizeof(tmp)) > 0) normalizeAddr(tmp, r->from); + jStr(jFind(callJson, "value"), r->value, sizeof(r->value)); const char *blkVal = jFind(callJson, "block"); if (blkVal) jStr(blkVal, r->block, sizeof(r->block)); From 8caec49740a80240aa52fb233d426695dfff943d Mon Sep 17 00:00:00 2001 From: William Morriss Date: Mon, 9 Mar 2026 03:20:08 -0500 Subject: [PATCH 18/23] feat: treat evm -x calls without `to` as contract creation - Calls with no `to` field become create entries with `initcode` and `constructTest` instead of a `tests` entry - Split results into separate `creates` and `calls` lists at creation time - Skip the `tests` entry entirely when there are no calls Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 103 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/dio.c b/dio.c index 9fce970..8be2c2e 100644 --- a/dio.c +++ b/dio.c @@ -340,8 +340,9 @@ static void runViaEvm( strbuf_t cj = {0}; #define sbLit(s) sbAppend(&cj, s, sizeof(s) - 1) #define sbStr(s) sbAppend(&cj, s, strlen(s)) - sbLit("{\"to\":\""); sbStr(r->to); - sbLit("\",\"from\":\""); sbStr(r->from); + sbLit("{"); + if (r->to[0]) { sbLit("\"to\":\""); sbStr(r->to); sbLit("\","); } + sbLit("\"from\":\""); sbStr(r->from); sbLit("\",\"data\":\""); sbStr(r->input); if (r->value[0]) { sbLit("\",\"value\":\""); sbStr(r->value); } sbLit("\"}"); @@ -444,7 +445,8 @@ static void runViaEvm( _exit(1); } - ensureAccount(accounts, r->to); + if (r->to[0]) + ensureAccount(accounts, r->to); if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) ensureAccount(accounts, r->from); @@ -456,7 +458,8 @@ static void runViaEvm( */ static void writeConfig( account_t *accounts, - call_result_t *results, + call_result_t *creates, + call_result_t *calls, const char *outfile) { FILE *f; @@ -480,40 +483,68 @@ static void writeConfig( fprintf(f, ",\n \"code\": \"%s\"", a->code); if (a->storage) { fputs(",\n \"storage\": {\n", f); - int first = 1; for (storage_kv_t *s = a->storage; s; s = s->next) { - if (!first) fputs(",\n", f); + if (s != a->storage) fputs(",\n", f); fprintf(f, " \"%s\": \"%s\"", s->key, s->value); - first = 0; } fputs("\n }", f); } fputs("\n },\n", f); } - fputs(" {\n \"tests\": [\n", f); - for (call_result_t *r = results; r; r = r->next) { - if (r != results) fputs(",\n", f); - fputs(" {\n", f); - fprintf(f, " \"to\": \"%s\"", r->to); - if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) - fprintf(f, ",\n \"from\": \"%s\"", r->from); - if (r->value[0]) - fprintf(f, ",\n \"value\": \"%s\"", r->value); - if (r->input && strcmp(r->input, "0x") != 0) - fprintf(f, ",\n \"input\": \"%s\"", r->input); - fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); + /* Create entries */ + for (call_result_t *r = creates; r; r = r->next) { + if (r != creates) fputs(",\n", f); + fputs(" {\n", f); + fprintf(f, " \"initcode\": \"%s\"", r->input); + fputs(",\n \"constructTest\": {", f); + const char *ctSep = "\n "; + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) { + fprintf(f, "%s\"from\": \"%s\"", ctSep, r->from); ctSep = ",\n "; + } + if (r->value[0]) { + fprintf(f, "%s\"value\": \"%s\"", ctSep, r->value); ctSep = ",\n "; + } + fprintf(f, "%s\"blockNumber\": \"%s\"", ctSep, r->block); if (r->gasUsed) - fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); + fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); if (r->logs) - fprintf(f, ",\n \"logs\": %s", r->logs); - if (r->status) - fprintf(f, ",\n \"status\": \"%s\"", r->status); + fprintf(f, ",\n \"logs\": %s", r->logs); + const char *ctStatus = (r->status && strcmp(r->status, "0x0") != 0) ? "0x1" : "0x0"; + fprintf(f, ",\n \"status\": \"%s\"", ctStatus); if (r->output) - fprintf(f, ",\n \"output\": \"%s\"", r->output); - fputs("\n }", f); + fprintf(f, ",\n \"output\": \"%s\"", r->output); + fputs("\n }\n }", f); + } + if (creates && calls) fputs(",\n", f); + + /* Tests entry — only if there are calls */ + if (calls) { + fputs(" {\n \"tests\": [\n", f); + for (call_result_t *r = calls; r; r = r->next) { + if (r != calls) fputs(",\n", f); + fputs(" {\n", f); + fprintf(f, " \"to\": \"%s\"", r->to); + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) + fprintf(f, ",\n \"from\": \"%s\"", r->from); + if (r->value[0]) + fprintf(f, ",\n \"value\": \"%s\"", r->value); + if (r->input && strcmp(r->input, "0x") != 0) + fprintf(f, ",\n \"input\": \"%s\"", r->input); + fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); + if (r->gasUsed) + fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); + if (r->logs) + fprintf(f, ",\n \"logs\": %s", r->logs); + if (r->status) + fprintf(f, ",\n \"status\": \"%s\"", r->status); + if (r->output) + fprintf(f, ",\n \"output\": \"%s\"", r->output); + fputs("\n }", f); + } + fputs("\n ]\n }", f); } - fputs("\n ]\n }\n]\n", f); + fputs("\n]\n", f); if (f != stdout) { fclose(f); @@ -529,10 +560,11 @@ static void run( const char *callJson, postFn post, void *ctx, account_t **accounts, - call_result_t **results) + call_result_t **creates, + call_result_t **calls) { call_result_t *r = malloc(sizeof(call_result_t)); - strcpy(r->to, "0x0000000000000000000000000000000000000000"); + r->to[0] = '\0'; strcpy(r->from, "0x0000000000000000000000000000000000000000"); strcpy(r->block, "latest"); r->value[0] = '\0'; @@ -572,8 +604,8 @@ static void run( runViaEvm(r, post, ctx, accounts); - r->next = *results; - *results = r; + if (r->to[0]) { r->next = *calls; *calls = r; } + else { r->next = *creates; *creates = r; } } /* ========================================================= @@ -651,25 +683,26 @@ int main(int argc, char *const argv[]) { evmPath = findEvm(argv[0]); account_t *accounts = NULL; - call_result_t *results = NULL; + call_result_t *creates = NULL; + call_result_t *calls = NULL; if (inlineJson) { - run(inlineJson, post, ctx, &accounts, &results); + run(inlineJson, post, ctx, &accounts, &creates, &calls); } else if (optind < argc) { for (; optind < argc; optind++) { FILE *f = fopen(argv[optind], "r"); if (!f) { perror(argv[optind]); return 1; } char *json = readAll(f); fclose(f); - run(json, post, ctx, &accounts, &results); + run(json, post, ctx, &accounts, &creates, &calls); free(json); } } else { char *json = readAll(stdin); - run(json, post, ctx, &accounts, &results); + run(json, post, ctx, &accounts, &creates, &calls); free(json); } - writeConfig(accounts, results, outfile); + writeConfig(accounts, creates, calls, outfile); return 0; } From 49491143faa3bb74af159b7162aae59bb6d2c355 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 10 Mar 2026 03:01:16 -0500 Subject: [PATCH 19/23] feat: move writeConfig to src/config.c with tests - Extract writeConfig, account_t, call_result_t, storage_kv_t and related macros from dio.c into include/config.h + src/config.c - Skip "status" in constructTest output when success (non-"0x0") - Skip "status" in tests output when "0x1" (default success) - Add tst/config.c with 13 unit tests covering accounts, creates, calls - Add tst/tst.h to share assertStderr macro; remove duplicate from tst/evm.c Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 3 - dio.c | 130 +------------- include/config.h | 42 +++++ src/config.c | 100 +++++++++++ tst/config.c | 435 +++++++++++++++++++++++++++++++++++++++++++++++ tst/evm.c | 30 +--- tst/tst.h | 34 ++++ 7 files changed, 613 insertions(+), 161 deletions(-) create mode 100644 include/config.h create mode 100644 src/config.c create mode 100644 tst/config.c create mode 100644 tst/tst.h diff --git a/Makefile b/Makefile index 19adf55..f1ef767 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,6 @@ SECP256K1=secp256k1/.libs/libsecp256k1.a INCLUDE=$(addprefix -I,include) -Isecp256k1/include CURL_LDFLAGS := $(shell curl-config --libs 2>/dev/null || echo -lcurl) EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) $(patsubst %.py, bin/%, $(wildcard *.py)) -bin/%: %.py | bin - cp $< $@ - chmod +x $@ TESTS=$(patsubst tst/%.c, tst/bin/%, $(wildcard tst/*.c)) SRC=$(wildcard src/*.cpp) $(wildcard src/*.m) $(wildcard src/%.c) LIBS=$(patsubst src/%.cpp, lib/%.o, $(wildcard src/*.cpp)) $(patsubst src/%.m, lib/%.o, $(wildcard src/*.m)) $(patsubst src/%.c, lib/%.o, $(wildcard src/*.c)) diff --git a/dio.c b/dio.c index 8be2c2e..e4da9b4 100644 --- a/dio.c +++ b/dio.c @@ -16,6 +16,7 @@ * The provider URL may also be set via the ETH_RPC_URL environment variable. */ +#include "config.h" #include "json.h" #include "ws.h" #include @@ -32,39 +33,8 @@ * Data structures * ========================================================= */ -#define ADDR_LEN 43 /* "0x" + 40 hex + NUL */ -#define HEX256_LEN 68 /* "0x" + 64 hex + NUL */ -#define NONCE_LEN 22 /* "0x" + up to 18 hex + NUL */ #define LINE_CAP 131072 /* matches evm's rpcBuf */ -typedef struct storage_kv { - char *key; - char *value; - struct storage_kv *next; -} storage_kv_t; - -typedef struct account { - char address[ADDR_LEN]; - char balance[HEX256_LEN]; - char nonce[NONCE_LEN]; - char *code; /* dynamically allocated "0x..." */ - storage_kv_t *storage; /* fetched key→value pairs */ - struct account *next; -} account_t; - -typedef struct call_result { - char to[ADDR_LEN]; - char from[ADDR_LEN]; - char block[32]; - char value[HEX256_LEN]; - char *input; - char *output; - char *logs; - char *status; - char *gasUsed; - struct call_result *next; -} call_result_t; - /* ========================================================= * Growing string buffer * ========================================================= */ @@ -453,104 +423,6 @@ static void runViaEvm( r->output = output; } -/* - * Write the dio JSON config to outfile (stdout if NULL or "-"). - */ -static void writeConfig( - account_t *accounts, - call_result_t *creates, - call_result_t *calls, - const char *outfile) -{ - FILE *f; - if (outfile && strcmp(outfile, "-") != 0) { - f = fopen(outfile, "w"); - if (!f) { perror(outfile); _exit(1); } - } else { - f = stdout; - } - - fputs("[\n", f); - - for (account_t *a = accounts; a; a = a->next) { - fputs(" {\n", f); - fprintf(f, " \"address\": \"%s\"", a->address); - if (strcmp(a->balance, "0x0") != 0 && strcmp(a->balance, "0x") != 0) - fprintf(f, ",\n \"balance\": \"%s\"", a->balance); - if (strcmp(a->nonce, "0x0") != 0 && strcmp(a->nonce, "0x") != 0) - fprintf(f, ",\n \"nonce\": \"%s\"", a->nonce); - if (strcmp(a->code, "0x") != 0 && strcmp(a->code, "") != 0) - fprintf(f, ",\n \"code\": \"%s\"", a->code); - if (a->storage) { - fputs(",\n \"storage\": {\n", f); - for (storage_kv_t *s = a->storage; s; s = s->next) { - if (s != a->storage) fputs(",\n", f); - fprintf(f, " \"%s\": \"%s\"", s->key, s->value); - } - fputs("\n }", f); - } - fputs("\n },\n", f); - } - - /* Create entries */ - for (call_result_t *r = creates; r; r = r->next) { - if (r != creates) fputs(",\n", f); - fputs(" {\n", f); - fprintf(f, " \"initcode\": \"%s\"", r->input); - fputs(",\n \"constructTest\": {", f); - const char *ctSep = "\n "; - if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) { - fprintf(f, "%s\"from\": \"%s\"", ctSep, r->from); ctSep = ",\n "; - } - if (r->value[0]) { - fprintf(f, "%s\"value\": \"%s\"", ctSep, r->value); ctSep = ",\n "; - } - fprintf(f, "%s\"blockNumber\": \"%s\"", ctSep, r->block); - if (r->gasUsed) - fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); - if (r->logs) - fprintf(f, ",\n \"logs\": %s", r->logs); - const char *ctStatus = (r->status && strcmp(r->status, "0x0") != 0) ? "0x1" : "0x0"; - fprintf(f, ",\n \"status\": \"%s\"", ctStatus); - if (r->output) - fprintf(f, ",\n \"output\": \"%s\"", r->output); - fputs("\n }\n }", f); - } - if (creates && calls) fputs(",\n", f); - - /* Tests entry — only if there are calls */ - if (calls) { - fputs(" {\n \"tests\": [\n", f); - for (call_result_t *r = calls; r; r = r->next) { - if (r != calls) fputs(",\n", f); - fputs(" {\n", f); - fprintf(f, " \"to\": \"%s\"", r->to); - if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) - fprintf(f, ",\n \"from\": \"%s\"", r->from); - if (r->value[0]) - fprintf(f, ",\n \"value\": \"%s\"", r->value); - if (r->input && strcmp(r->input, "0x") != 0) - fprintf(f, ",\n \"input\": \"%s\"", r->input); - fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); - if (r->gasUsed) - fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); - if (r->logs) - fprintf(f, ",\n \"logs\": %s", r->logs); - if (r->status) - fprintf(f, ",\n \"status\": \"%s\"", r->status); - if (r->output) - fprintf(f, ",\n \"output\": \"%s\"", r->output); - fputs("\n }", f); - } - fputs("\n ]\n }", f); - } - fputs("\n]\n", f); - - if (f != stdout) { - fclose(f); - fprintf(stderr, "Wrote %s\n", outfile); - } -} /* * Parse callJson, resolve the block number, run via evm, and append the diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..e038624 --- /dev/null +++ b/include/config.h @@ -0,0 +1,42 @@ +#include + +#define ADDR_LEN 43 /* "0x" + 40 hex + NUL */ +#define HEX256_LEN 68 /* "0x" + 64 hex + NUL */ +#define NONCE_LEN 22 /* "0x" + up to 18 hex + NUL */ + +typedef struct storage_kv { + char *key; + char *value; + struct storage_kv *next; +} storage_kv_t; + +typedef struct account { + char address[ADDR_LEN]; + char balance[HEX256_LEN]; + char nonce[NONCE_LEN]; + char *code; + storage_kv_t *storage; + struct account *next; +} account_t; + +typedef struct call_result { + char to[ADDR_LEN]; + char from[ADDR_LEN]; + char block[32]; + char value[HEX256_LEN]; + char *input; + char *output; + char *logs; + char *status; + char *gasUsed; + struct call_result *next; +} call_result_t; + +/* + * Write the dio JSON config to outfile (stdout if NULL or "-"). + */ +void writeConfig( + account_t *accounts, + call_result_t *creates, + call_result_t *calls, + const char *outfile); diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..b4c01eb --- /dev/null +++ b/src/config.c @@ -0,0 +1,100 @@ +#include "config.h" + +#include +#include +#include + +void writeConfig( + account_t *accounts, + call_result_t *creates, + call_result_t *calls, + const char *outfile) +{ + FILE *f; + if (outfile && strcmp(outfile, "-") != 0) { + f = fopen(outfile, "w"); + if (!f) { perror(outfile); _exit(1); } + } else { + f = stdout; + } + + fputs("[\n", f); + + for (account_t *a = accounts; a; a = a->next) { + if (a != accounts) fputs(",\n", f); + fputs(" {\n", f); + fprintf(f, " \"address\": \"%s\"", a->address); + if (strcmp(a->balance, "0x0") != 0 && strcmp(a->balance, "0x") != 0) + fprintf(f, ",\n \"balance\": \"%s\"", a->balance); + if (strcmp(a->nonce, "0x0") != 0 && strcmp(a->nonce, "0x") != 0) + fprintf(f, ",\n \"nonce\": \"%s\"", a->nonce); + if (strcmp(a->code, "0x") != 0 && strcmp(a->code, "") != 0) + fprintf(f, ",\n \"code\": \"%s\"", a->code); + if (a->storage) { + fputs(",\n \"storage\": {\n", f); + for (storage_kv_t *s = a->storage; s; s = s->next) { + if (s != a->storage) fputs(",\n", f); + fprintf(f, " \"%s\": \"%s\"", s->key, s->value); + } + fputs("\n }", f); + } + fputs("\n }", f); + } + + /* Create entries */ + for (call_result_t *r = creates; r; r = r->next) { + fputs(",\n {\n", f); + fprintf(f, " \"initcode\": \"%s\"", r->input); + fputs(",\n \"constructTest\": {", f); + const char *ctSep = "\n "; + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) { + fprintf(f, "%s\"from\": \"%s\"", ctSep, r->from); ctSep = ",\n "; + } + if (r->value[0]) { + fprintf(f, "%s\"value\": \"%s\"", ctSep, r->value); ctSep = ",\n "; + } + fprintf(f, "%s\"blockNumber\": \"%s\"", ctSep, r->block); + if (r->gasUsed) + fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); + if (r->logs) + fprintf(f, ",\n \"logs\": %s", r->logs); + if (strcmp(r->status, "0x0") == 0) + fprintf(f, ",\n \"status\": \"0x0\""); + if (r->output) + fprintf(f, ",\n \"output\": \"%s\"", r->output); + fputs("\n }\n }", f); + } + + /* Tests entry — only if there are calls */ + if (calls) { + fputs(",\n {\n \"tests\": [\n", f); + for (call_result_t *r = calls; r; r = r->next) { + if (r != calls) fputs(",\n", f); + fputs(" {\n", f); + fprintf(f, " \"to\": \"%s\"", r->to); + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) + fprintf(f, ",\n \"from\": \"%s\"", r->from); + if (r->value[0]) + fprintf(f, ",\n \"value\": \"%s\"", r->value); + if (r->input && strcmp(r->input, "0x") != 0) + fprintf(f, ",\n \"input\": \"%s\"", r->input); + fprintf(f, ",\n \"blockNumber\": \"%s\"", r->block); + if (r->gasUsed) + fprintf(f, ",\n \"gasUsed\": \"%s\"", r->gasUsed); + if (r->logs) + fprintf(f, ",\n \"logs\": %s", r->logs); + if (strcmp(r->status, "0x1") != 0) + fprintf(f, ",\n \"status\": \"%s\"", r->status); + if (r->output) + fprintf(f, ",\n \"output\": \"%s\"", r->output); + fputs("\n }", f); + } + fputs("\n ]\n }", f); + } + fputs("\n]\n", f); + + if (f != stdout) { + fclose(f); + fprintf(stderr, "Wrote %s\n", outfile); + } +} diff --git a/tst/config.c b/tst/config.c new file mode 100644 index 0000000..37ef554 --- /dev/null +++ b/tst/config.c @@ -0,0 +1,435 @@ +#include "config.h" +#include "tst.h" + +#include + +/* Read an entire file into a malloc'd string. Caller must free(). */ +static char *readFile(const char *path) { + FILE *f = fopen(path, "r"); + assert(f != NULL); + fseek(f, 0, SEEK_END); + long len = ftell(f); + rewind(f); + char *buf = malloc(len + 1); + fread(buf, 1, len, f); + buf[len] = '\0'; + fclose(f); + return buf; +} + +/* Write to a temp file, assert stderr, read contents, unlink, return contents. */ +static char *captureWriteConfig( + account_t *accounts, + call_result_t *creates, + call_result_t *calls) +{ + char path[] = "/tmp/config_test_XXXXXX"; + int fd = mkstemp(path); + assert(fd != -1); + close(fd); + + char expectedStderr[64]; + snprintf(expectedStderr, sizeof(expectedStderr), "Wrote %s\n", path); + assertStderr(expectedStderr, writeConfig(accounts, creates, calls, path)); + + char *out = readFile(path); + unlink(path); + return out; +} + +/* ------------------------------------------------------------------ */ + +void test_single_account_defaults() { + /* balance "0x0", nonce "0x0", code "0x" — all omitted */ + account_t a = { + .address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + char *out = captureWriteConfig(&a, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_account_with_balance() { + account_t a = { + .address = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + .balance = "0x1", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + char *out = captureWriteConfig(&a, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"," + "\n \"balance\": \"0x1\"" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_account_with_nonce() { + account_t a = { + .address = "0xcccccccccccccccccccccccccccccccccccccccc", + .balance = "0x0", + .nonce = "0x5", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + char *out = captureWriteConfig(&a, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xcccccccccccccccccccccccccccccccccccccccc\"," + "\n \"nonce\": \"0x5\"" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_account_with_code() { + account_t a = { + .address = "0xdddddddddddddddddddddddddddddddddddddddd", + .balance = "0x0", + .nonce = "0x0", + .code = "0x6001", + .storage = NULL, + .next = NULL, + }; + char *out = captureWriteConfig(&a, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xdddddddddddddddddddddddddddddddddddddddd\"," + "\n \"code\": \"0x6001\"" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_account_with_storage() { + storage_kv_t s = { .key = "0x1", .value = "0xff", .next = NULL }; + account_t a = { + .address = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = &s, + .next = NULL, + }; + char *out = captureWriteConfig(&a, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\"," + "\n \"storage\": {\n" + " \"0x1\": \"0xff\"" + "\n }" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_account_with_two_storage_slots() { + /* next pointer links slots; first in list prints first */ + storage_kv_t s2 = { .key = "0x2", .value = "0x20", .next = NULL }; + storage_kv_t s1 = { .key = "0x1", .value = "0x10", .next = &s2 }; + account_t a = { + .address = "0xffffffffffffffffffffffffffffffffffffffff", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = &s1, + .next = NULL, + }; + char *out = captureWriteConfig(&a, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xffffffffffffffffffffffffffffffffffffffff\"," + "\n \"storage\": {\n" + " \"0x1\": \"0x10\"," + "\n \"0x2\": \"0x20\"" + "\n }" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_two_accounts() { + account_t a2 = { + .address = "0x2222222222222222222222222222222222222222", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + account_t a1 = { + .address = "0x1111111111111111111111111111111111111111", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = &a2, + }; + char *out = captureWriteConfig(&a1, NULL, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0x1111111111111111111111111111111111111111\"" + "\n }," + "\n {\n" + " \"address\": \"0x2222222222222222222222222222222222222222\"" + "\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_create_entry() { + /* status is the deployed address (success) — omitted from output */ + account_t acct = { + .address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + call_result_t cr = { + .to = "", + .from = "0x0000000000000000000000000000000000000000", + .block = "latest", + .value = "", + .input = "0x6001", + .output = NULL, + .logs = NULL, + .status = "0xbd770416a3345f91e4b34576cb804a576fa48eb1", + .gasUsed = NULL, + .next = NULL, + }; + char *out = captureWriteConfig(&acct, &cr, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "\n }," + "\n {\n" + " \"initcode\": \"0x6001\"," + "\n \"constructTest\": {" + "\n \"blockNumber\": \"latest\"" + "\n }\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_create_failed() { + /* status "0x0" means revert — written explicitly */ + account_t acct = { + .address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + call_result_t cr = { + .to = "", + .from = "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead", + .block = "0x1", + .value = "", + .input = "0x", + .output = NULL, + .logs = NULL, + .status = "0x0", + .gasUsed = "0x5208", + .next = NULL, + }; + char *out = captureWriteConfig(&acct, &cr, NULL); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "\n }," + "\n {\n" + " \"initcode\": \"0x\"," + "\n \"constructTest\": {" + "\n \"from\": \"0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead\"," + "\n \"blockNumber\": \"0x1\"," + "\n \"gasUsed\": \"0x5208\"," + "\n \"status\": \"0x0\"" + "\n }\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_call_entry() { + account_t acct = { + .address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + call_result_t call = { + .to = "0x1234567890123456789012345678901234567890", + .from = "0x0000000000000000000000000000000000000000", + .block = "latest", + .value = "", + .input = "0x", + .output = NULL, + .logs = NULL, + .status = "0x1", + .gasUsed = NULL, + .next = NULL, + }; + char *out = captureWriteConfig(&acct, NULL, &call); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "\n }," + "\n {\n \"tests\": [\n" + " {\n" + " \"to\": \"0x1234567890123456789012345678901234567890\"," + "\n \"blockNumber\": \"latest\"" + "\n }" + "\n ]\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_call_with_from_input_gasused_status_output() { + account_t acct = { + .address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + call_result_t call = { + .to = "0x1234567890123456789012345678901234567890", + .from = "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead", + .block = "0xa", + .value = "", + .input = "0xdeadbeef", + .output = "0xcafe", + .logs = NULL, + .status = "0x1", + .gasUsed = "0x5208", + .next = NULL, + }; + char *out = captureWriteConfig(&acct, NULL, &call); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "\n }," + "\n {\n \"tests\": [\n" + " {\n" + " \"to\": \"0x1234567890123456789012345678901234567890\"," + "\n \"from\": \"0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead\"," + "\n \"input\": \"0xdeadbeef\"," + "\n \"blockNumber\": \"0xa\"," + "\n \"gasUsed\": \"0x5208\"," + "\n \"output\": \"0xcafe\"" + "\n }" + "\n ]\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +void test_two_calls() { + account_t acct = { + .address = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + .balance = "0x0", + .nonce = "0x0", + .code = "0x", + .storage = NULL, + .next = NULL, + }; + call_result_t call2 = { + .to = "0x2222222222222222222222222222222222222222", + .from = "0x0000000000000000000000000000000000000000", + .block = "latest", + .value = "", + .input = "0x", + .output = NULL, + .logs = NULL, + .status = "0x1", + .gasUsed = NULL, + .next = NULL, + }; + call_result_t call1 = { + .to = "0x1111111111111111111111111111111111111111", + .from = "0x0000000000000000000000000000000000000000", + .block = "latest", + .value = "", + .input = "0x", + .output = NULL, + .logs = NULL, + .status = "0x1", + .gasUsed = NULL, + .next = &call2, + }; + char *out = captureWriteConfig(&acct, NULL, &call1); + const char *expected = + "[\n" + " {\n" + " \"address\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "\n }," + "\n {\n \"tests\": [\n" + " {\n" + " \"to\": \"0x1111111111111111111111111111111111111111\"," + "\n \"blockNumber\": \"latest\"" + "\n }," + "\n {\n" + " \"to\": \"0x2222222222222222222222222222222222222222\"," + "\n \"blockNumber\": \"latest\"" + "\n }" + "\n ]\n }" + "\n]\n"; + assert(strcmp(out, expected) == 0); + free(out); +} + +int main() { + test_single_account_defaults(); + test_account_with_balance(); + test_account_with_nonce(); + test_account_with_code(); + test_account_with_storage(); + test_account_with_two_storage_slots(); + test_two_accounts(); + test_create_entry(); + test_create_failed(); + test_call_entry(); + test_call_with_from_input_gasused_status_output(); + test_two_calls(); + return 0; +} diff --git a/tst/evm.c b/tst/evm.c index be40707..3513ae4 100644 --- a/tst/evm.c +++ b/tst/evm.c @@ -7,35 +7,7 @@ #include "ops.h" #include "evm.h" -#define assertStderr(expectedErr, statement)\ - int rw[2];\ - pipe(rw);\ - int savedStderr = dup(2);\ - close(2);\ - dup2(rw[1], 2);\ - close(rw[1]);\ - statement;\ - close(2);\ - dup2(savedStderr, 2);\ - clearerr(stderr);\ - close(savedStderr);\ - size_t strSize = strlen(expectedErr) + 1;\ - char *actualErr = malloc(strSize);\ - ssize_t red = read(rw[0], actualErr, strSize);\ - if (red == -1) {\ - perror("read");\ - exit(1);\ - }\ - actualErr[red] = 0;\ - if (red != strSize - 1) {\ - fprintf(stderr, "stderr length mismatch\nexpected[%zu]: \"%s\"\nactual[%zd]: \"%s\"\n", strSize, expectedErr, red, actualErr);\ - exit(1);\ - }\ - close(rw[0]);\ - if (memcmp(expectedErr, actualErr, strSize) != 0) {\ - fprintf(stderr, "stderr mismatch\nexpected[%zu]: \"%s\"\nactual[%zd]: \"%s\"\n", strSize, expectedErr, red, actualErr);\ - exit(1);\ - } +#include "tst.h" #define assertFailedInvalid(result)\ assert(UPPER(UPPER(result.status)) == 0);\ diff --git a/tst/tst.h b/tst/tst.h new file mode 100644 index 0000000..2810380 --- /dev/null +++ b/tst/tst.h @@ -0,0 +1,34 @@ +#include +#include +#include +#include + +#define assertStderr(expectedErr, statement)\ + int rw[2];\ + pipe(rw);\ + int savedStderr = dup(2);\ + close(2);\ + dup2(rw[1], 2);\ + close(rw[1]);\ + statement;\ + close(2);\ + dup2(savedStderr, 2);\ + clearerr(stderr);\ + close(savedStderr);\ + size_t strSize = strlen(expectedErr) + 1;\ + char *actualErr = malloc(strSize);\ + ssize_t red = read(rw[0], actualErr, strSize);\ + if (red == -1) {\ + perror("read");\ + exit(1);\ + }\ + actualErr[red] = 0;\ + if (red != strSize - 1) {\ + fprintf(stderr, "stderr length mismatch\nexpected[%zu]: \"%s\"\nactual[%zd]: \"%s\"\n", strSize, expectedErr, red, actualErr);\ + exit(1);\ + }\ + close(rw[0]);\ + if (memcmp(expectedErr, actualErr, strSize) != 0) {\ + fprintf(stderr, "stderr mismatch\nexpected[%zu]: \"%s\"\nactual[%zd]: \"%s\"\n", strSize, expectedErr, red, actualErr);\ + exit(1);\ + } From 0448e64d9dd406150597c73cdbf1f713b952b584 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 10 Mar 2026 03:01:34 -0500 Subject: [PATCH 20/23] rm dio.py --- dio.py | 425 --------------------------------------------------------- 1 file changed, 425 deletions(-) delete mode 100755 dio.py diff --git a/dio.py b/dio.py deleted file mode 100755 index 522e5d8..0000000 --- a/dio.py +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env python3 -""" -dio - Generate dio config files from debug_traceCall - -Usage: - dio [provider-url] [outfile] [-o json] [--extend file] [file...] - -The call JSON (from -o, a file argument, or stdin) is the eth_call object: - {"to": "0x...", "from": "0x...", "data": "0x...", "value": "0x...", "block": "latest"} - -Only "to" is required. "block" defaults to "latest". -The generated config is written to outfile, or stdout if omitted. - -Options: - -o Call JSON inline - --extend Extend existing config file; fail on conflicting state - -The provider URL may also be set via the ETH_RPC_URL environment variable. - -Makes JSON-RPC calls to an Ethereum node and writes a dio test -configuration file for use with bin/evm -w. -""" - -import argparse -import asyncio -import json -import os -import subprocess -import sys -import urllib.request - - -def http_post(url, payload): - req = urllib.request.Request( - url, data=payload, headers={"Content-Type": "application/json"} - ) - try: - with urllib.request.urlopen(req) as resp: - return resp.read() - except urllib.error.HTTPError as e: - print(f"dio: HTTP {e.code} {e.reason} from {url}", file=sys.stderr) - sys.exit(1) - except urllib.error.URLError as e: - print(f"dio: {e.reason} ({url})", file=sys.stderr) - sys.exit(1) - - -async def rpc_call(post, method, params, req_id=1): - payload = json.dumps({ - "jsonrpc": "2.0", - "id": req_id, - "method": method, - "params": params, - }).encode() - resp = json.loads(await post(payload)) - if "error" in resp: - err = resp["error"] - print(f"dio: {method} failed ({err.get('message', err)})", file=sys.stderr) - sys.exit(1) - return resp - - -async def batch_rpc(post, requests): - payload = json.dumps(requests).encode() - result = json.loads(await post(payload)) - result.sort(key=lambda r: r["id"]) - return result - - -def stack_to_address(val): - """Convert a stack value (hex string) to a 0x-prefixed lowercase address.""" - s = val[2:] if val.startswith(("0x", "0X")) else val - return "0x" + s[-40:].lower().zfill(40) - - -def normalize_key(val): - """Normalize a storage key to a trimmed 0x-prefixed hex string.""" - s = val[2:] if val.startswith("0x") else val - return "0x" + (s.lstrip("0") or "0") - - -def build_access_list(struct_logs, to_addr, from_addr): - """ - Walk structLogs and build {address: [storage_keys]}. - - Tracks the call stack to attribute SLOAD to the correct account. - """ - access_list = {} # address -> list of storage keys (ordered, unique) - call_stack = [] # current execution context (address at each depth) - - def add_account(addr): - addr = addr.lower() - if addr not in access_list: - access_list[addr] = [] - return addr - - if to_addr: - call_stack.append(add_account(to_addr)) - if from_addr: - add_account(from_addr) - - def on_sload(log): - if not call_stack: - return - key = normalize_key(log["stack"][-1]) - addr = call_stack[-1] - if key not in access_list[addr]: - access_list[addr].append(key) - - def on_call(log): - # stack (top to bottom): gas, addr, ... => addr at stack[-2] - addr = add_account(stack_to_address(log["stack"][-2])) - call_stack.append(addr) - - def on_delegatecall(log): - # Runs in caller's storage context; still track code address for eth_getCode - add_account(stack_to_address(log["stack"][-2])) - ctx = call_stack[-1] if call_stack else add_account(stack_to_address(log["stack"][-2])) - call_stack.append(ctx) - - def on_extcode(log): - # EXTCODESIZE / EXTCODECOPY: address is at stack top - add_account(stack_to_address(log["stack"][-1])) - - def on_return(_log): - if call_stack: - call_stack.pop() - - handlers = { - "SLOAD": on_sload, - "CALL": on_call, - "STATICCALL": on_call, - "DELEGATECALL": on_delegatecall, - "EXTCODESIZE": on_extcode, - "EXTCODECOPY": on_extcode, - "STOP": on_return, - "RETURN": on_return, - "REVERT": on_return, - } - - for log in struct_logs: - handler = handlers.get(log["op"]) - if handler: - handler(log) - - return access_list - - -def write_config(account_data, call, block, outfile, extend, result=None): - """Build and write the dio config JSON from collected account_data.""" - existing = [] - existing_by_addr = {} # lowercase address -> index in existing - if extend: - with open(extend) as f: - existing = json.load(f) - for i, entry in enumerate(existing): - if "address" in entry: - existing_by_addr[entry["address"].lower()] = i - - output = list(existing) - - for addr, data in account_data.items(): - if addr in existing_by_addr: - idx = existing_by_addr[addr] - ex = existing[idx] - for field, default in (("balance", "0x0"), ("nonce", "0x0"), ("code", "0x")): - new_val = data[field] - if new_val in (default, ""): - continue - ex_val = ex.get(field, default) - if ex_val != new_val: - print( - f"Conflict for {addr}.{field}: " - f"existing={ex_val!r} new={new_val!r}", - file=sys.stderr, - ) - sys.exit(1) - continue - - entry = {"address": addr} - if data["balance"] not in ("0x0", "0x"): - entry["balance"] = data["balance"] - if data["nonce"] not in ("0x0", "0x"): - entry["nonce"] = data["nonce"] - if data["code"] not in ("0x", ""): - entry["code"] = data["code"] - if data["storage"]: - entry["storage"] = data["storage"] - output.append(entry) - - # Build test entry; use "input" (dio convention) not "data" (RPC convention) - test = dict(call) - if "data" in test: - test["input"] = test.pop("data") - test["blockNumber"] = block - test.setdefault("debug", "0x20") - if result is not None: - test["output"] = result - output.append({"tests": [test]}) - - out_json = json.dumps(output, indent=4) - - if outfile and outfile != "-": - with open(outfile, "w") as f: - f.write(out_json + "\n") - print(f"Wrote {outfile}", file=sys.stderr) - else: - print(out_json) - - -def find_evm(): - script_dir = os.path.dirname(os.path.realpath(__file__)) - candidate = os.path.join(script_dir, "evm") - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return "evm" - - -async def run_via_evm(call, block, sender, outfile, extend, post): - """Spawn evm -x -n with call JSON, proxy JSON-RPC on its stdin/stdout.""" - call_arg = json.dumps({ - "to": call["to"], - "from": sender, - "data": call.get("data", "0x"), - }) - proc = subprocess.Popen( - [find_evm(), "-x", "-n", "-o", call_arg], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, - ) - - account_data = {} - - def ensure_account(addr): - addr = addr.lower() - if addr not in account_data: - account_data[addr] = {"balance": "0x0", "nonce": "0x0", "code": "0x", "storage": {}} - return addr - - output_hex = None - try: - for line in proc.stdout: - line = line.strip() - if not line: - continue - msg = json.loads(line) - - if isinstance(msg, list): - # Account batch: [{getCode,...}, {getTransactionCount,...}, {getBalance,...}] - addr = ensure_account(msg[0]["params"][0]) - id_to_method = {m["id"]: m["method"] for m in msg} - resp = json.loads(await post(json.dumps(msg).encode())) - resp.sort(key=lambda r: r["id"]) - for item in resp: - val = item["result"] - method = id_to_method[item["id"]] - if method == "eth_getCode": - account_data[addr]["code"] = val - elif method == "eth_getTransactionCount": - account_data[addr]["nonce"] = val - elif method == "eth_getBalance": - account_data[addr]["balance"] = val - proc.stdin.write(json.dumps(resp) + "\n") - proc.stdin.flush() - - elif "output" in msg: - output_hex = msg["output"] - break - - elif msg.get("method") == "eth_blockNumber": - resp = {"jsonrpc": "2.0", "id": msg["id"], "result": block} - proc.stdin.write(json.dumps(resp) + "\n") - proc.stdin.flush() - - elif msg.get("method") == "eth_getStorageAt": - addr = ensure_account(msg["params"][0]) - key = normalize_key(msg["params"][1]) - resp = json.loads(await post(json.dumps(msg).encode())) - val = resp.get("result", "0x" + "0" * 64) - if val and val != "0x" + "0" * 64: - account_data[addr]["storage"][key] = val - proc.stdin.write(json.dumps(resp) + "\n") - proc.stdin.flush() - - finally: - proc.stdin.close() - proc.wait() - - if output_hex is None or proc.returncode != 0: - print("dio: evm execution failed", file=sys.stderr) - sys.exit(1) - - ensure_account(call["to"]) - if sender and sender != "0x" + "0" * 40: - ensure_account(sender) - - write_config(account_data, call, block, outfile, extend, result=output_hex) - - -async def run(call_json, outfile, extend, post): - call = json.loads(call_json) - - if "to" not in call: - print("call JSON must include \"to\"", file=sys.stderr) - sys.exit(1) - - block = call.pop("block", "latest") - if block == "latest": - block = (await rpc_call(post, "eth_blockNumber", []))["result"] - - # Normalize: dio uses "input", RPC uses "data" - if "input" in call and "data" not in call: - call["data"] = call.pop("input") - - sender = call.get("from", "0x" + "0" * 40) - - # Try debug_traceCall; fall back to evm -nx on any error - trace_payload = json.dumps({ - "jsonrpc": "2.0", "id": 1, "method": "debug_traceCall", - "params": [call, block, {"disableStorage": True, "disableMemory": True}], - }).encode() - trace_resp = json.loads(await post(trace_payload)) - if "error" in trace_resp: - await run_via_evm(call, block, sender, outfile, extend, post) - return - - trace_result = trace_resp["result"] - struct_logs = trace_result["structLogs"] - return_value = "0x" + trace_result.get("returnValue", "") - access_list = build_access_list(struct_logs, call["to"], sender) - - # Batch query balance, nonce, code, and storage for each account - batch = [] - meta = [] # (address, field, key_or_None) - bid = 1 - for addr, keys in access_list.items(): - batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getBalance", - "params": [addr, block]}) - meta.append((addr, "balance", None)); bid += 1 - - batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getTransactionCount", - "params": [addr, block]}) - meta.append((addr, "nonce", None)); bid += 1 - - batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getCode", - "params": [addr, block]}) - meta.append((addr, "code", None)); bid += 1 - - for key in keys: - batch.append({"jsonrpc": "2.0", "id": bid, "method": "eth_getStorageAt", - "params": [addr, key, block]}) - meta.append((addr, "storage", key)); bid += 1 - - account_data = { - addr: {"balance": "0x0", "nonce": "0x0", "code": "0x", "storage": {}} - for addr in access_list - } - if batch: - results = await batch_rpc(post, batch) - for i, resp in enumerate(results): - if "error" in resp: - err = resp["error"] - addr, field, key = meta[i] - method = "eth_getStorageAt" if field == "storage" else f"eth_get{field.capitalize()}" - print(f"dio: {method} failed ({err.get('message', err)})", file=sys.stderr) - sys.exit(1) - addr, field, key = meta[i] - val = resp["result"] - if field == "storage": - if val and val != "0x" + "0" * 64: - account_data[addr]["storage"][key] = val - else: - account_data[addr][field] = val - - write_config(account_data, call, block, outfile, extend, result=return_value) - - -async def run_main(url, call_json, outfile, extend): - if url.startswith(("ws://", "wss://")): - try: - import websockets - except ImportError: - print("dio: websockets package required for ws:// URLs (pip install websockets)", file=sys.stderr) - sys.exit(1) - async with websockets.connect(url) as ws: - async def post(payload): - await ws.send(payload.decode() if isinstance(payload, bytes) else payload) - return (await ws.recv()).encode() - await run(call_json, outfile, extend, post) - else: - async def post(payload): - return http_post(url, payload) - await run(call_json, outfile, extend, post) - - -def main(): - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("-h", "--help", action="store_true") - parser.add_argument("url", nargs="?", default=None) - parser.add_argument("outfile", nargs="?") - parser.add_argument("-o") - parser.add_argument("--extend", metavar="FILE") - parser.add_argument("files", nargs="*") - args = parser.parse_args() - - if args.help: - print(__doc__.strip()) - sys.exit(0) - - url = args.url or os.environ.get("ETH_RPC_URL") - if not url: - print(__doc__.strip()) - sys.exit(1) - - if args.o is not None: - asyncio.run(run_main(url, args.o, args.outfile, args.extend)) - elif args.files: - for path in args.files: - with open(path) as f: - asyncio.run(run_main(url, f.read(), args.outfile, args.extend)) - else: - asyncio.run(run_main(url, sys.stdin.read(), args.outfile, args.extend)) - - -if __name__ == "__main__": - main() From 664efc5d90e7ae4802618f5da190fc73a9d3633d Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 10 Mar 2026 03:18:08 -0500 Subject: [PATCH 21/23] ci: install libcurl-dev and document libcurl dependency Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/check.yml | 3 +++ .github/workflows/make.yml | 3 +++ README.md | 11 +++++++++-- make/.rme.md | 11 +++++++++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1e1abf9..0011642 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -16,5 +16,8 @@ jobs: with: submodules: recursive + - name: Install dependencies + run: sudo apt-get install -y libcurl4-openssl-dev + - name: make check run: make check diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index 89ad3cc..848445b 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -16,6 +16,9 @@ jobs: with: submodules: recursive + - name: Install dependencies + run: sudo apt-get install -y libcurl4-openssl-dev + - name: delete generated file README.md run: rm -f README.md diff --git a/README.md b/README.md index 59672ac..65f9017 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,18 @@ EVM Fast assembler, disassembler, execution, and testing for the Ethereum Virtual Machine (EVM) supporting expressive syntax. ## Installation +### Dependencies +`libcurl` is required to build `dio`. + * macOS: already installed by default + * Arch Linux: `sudo pacman -S curl` + * Debian/Ubuntu: `sudo apt-get install libcurl4-openssl-dev` + * RHEL/CentOS/Fedora: `sudo dnf install libcurl-devel` + +### Build from source ```sh -# Build from source git clone https://github.com/wjmelements/evm.git cd evm -make bin/evm +make bin/evm bin/dio # Append bin/ to your $PATH export PATH=$PATH:$pwd/bin # Install diff --git a/make/.rme.md b/make/.rme.md index 1192c4c..9cadb19 100644 --- a/make/.rme.md +++ b/make/.rme.md @@ -3,11 +3,18 @@ EVM Fast assembler, disassembler, execution, and testing for the Ethereum Virtual Machine (EVM) supporting expressive syntax. ## Installation +### Dependencies +`libcurl` is required to build `dio`. + * macOS: already installed by default + * Arch Linux: `sudo pacman -S curl` + * Debian/Ubuntu: `sudo apt-get install libcurl4-openssl-dev` + * RHEL/CentOS/Fedora: `sudo dnf install libcurl-devel` + +### Build from source ```sh -# Build from source git clone https://github.com/wjmelements/evm.git cd evm -make bin/evm +make bin/evm bin/dio # Append bin/ to your $PATH export PATH=$PATH:$pwd/bin # Install From 9d74d45150f5eaeb4b60c619197f43abd27b2091 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 10 Mar 2026 04:24:34 -0500 Subject: [PATCH 22/23] fix: resolve gcc warnings not emitted by clang Co-Authored-By: Claude Sonnet 4.6 --- dio.c | 6 +++--- tst/config.c | 2 +- tst/tst.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dio.c b/dio.c index e4da9b4..8b435f7 100644 --- a/dio.c +++ b/dio.c @@ -87,7 +87,7 @@ static account_t *ensureAccount(account_t **head, const char *addr) { * "0x000...abc" -> "0xabc", "0x000...0" -> "0x0" */ static void normalizeKey(const char *in, char *out, size_t outlen) { - const char *s = (in[0] == '0' && in[1] == 'x') ? in + 2 : in; + const char *s = in + 2; while (*s == '0' && *(s + 1)) s++; snprintf(out, outlen, "0x%s", s); } @@ -311,8 +311,8 @@ static void runViaEvm( #define sbLit(s) sbAppend(&cj, s, sizeof(s) - 1) #define sbStr(s) sbAppend(&cj, s, strlen(s)) sbLit("{"); - if (r->to[0]) { sbLit("\"to\":\""); sbStr(r->to); sbLit("\","); } - sbLit("\"from\":\""); sbStr(r->from); + if (r->to[0]) { sbLit("\"to\":\""); sbLit(r->to); sbLit("\","); } + sbLit("\"from\":\""); sbLit(r->from); sbLit("\",\"data\":\""); sbStr(r->input); if (r->value[0]) { sbLit("\",\"value\":\""); sbStr(r->value); } sbLit("\"}"); diff --git a/tst/config.c b/tst/config.c index 37ef554..a7d43e0 100644 --- a/tst/config.c +++ b/tst/config.c @@ -11,7 +11,7 @@ static char *readFile(const char *path) { long len = ftell(f); rewind(f); char *buf = malloc(len + 1); - fread(buf, 1, len, f); + assert(fread(buf, 1, len, f) == (size_t)len); buf[len] = '\0'; fclose(f); return buf; diff --git a/tst/tst.h b/tst/tst.h index 2810380..345880f 100644 --- a/tst/tst.h +++ b/tst/tst.h @@ -5,7 +5,7 @@ #define assertStderr(expectedErr, statement)\ int rw[2];\ - pipe(rw);\ + if (pipe(rw) == -1) { perror("pipe"); exit(1); }\ int savedStderr = dup(2);\ close(2);\ dup2(rw[1], 2);\ From 24d55ac313811bf57479f1134cc661e5150488bd Mon Sep 17 00:00:00 2001 From: William Morriss Date: Thu, 12 Mar 2026 01:31:35 -0500 Subject: [PATCH 23/23] feat: compute CREATE address from from+nonce when address unspecified Co-Authored-By: Claude Sonnet 4.6 --- src/dio.c | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/dio.c b/src/dio.c index 5b89ebc..eae3a08 100644 --- a/src/dio.c +++ b/src/dio.c @@ -329,18 +329,20 @@ static void verifyConstructResult(result_t *constructResult, entry_t *entry) { } } +static address_t *anonymousAddress() { + address_t *addr = calloc(1, sizeof(address_t)); + addr->address[0] = 0xaa; + addr->address[1] = 0xbb; + static uint32_t anonymousId; + *(uint32_t *)(&addr->address[15]) = anonymousId++; + return addr; +} + static void applyEntry(entry_t *entry) { if (entry->importPath) { loadConfig(entry->importPath, false); return; } - if (entry->address == NULL) { - entry->address = calloc(1, sizeof(address_t)); - entry->address->address[0] = 0xaa; - entry->address->address[1] = 0xbb; - static uint32_t anonymousId; - *(uint32_t *)(&entry->address->address[15]) = anonymousId++; - } bool constructHeaderPrinted = false; if (entry->initCode.size) { address_t from; @@ -376,7 +378,21 @@ static void applyEntry(entry_t *entry) { value[1] = 0; value[2] = 0; - result_t constructResult = evmConstruct(from, *entry->address, gas, value, entry->initCode); + result_t constructResult; + if (entry->address == NULL && !AddressZero(&from)) { + constructResult = txCreate(from, gas, value, entry->initCode); + if (!zero256(&constructResult.status)) { + entry->address = malloc(sizeof(address_t)); + *entry->address = AddressFromUint256(&constructResult.status); + } else { + entry->address = anonymousAddress(); + } + } else { + if (entry->address == NULL) { + entry->address = anonymousAddress(); + } + constructResult = evmConstruct(from, *entry->address, gas, value, entry->initCode); + } verifyConstructResult(&constructResult, entry); if (entry->constructTest) { // normalize status to 1/0 for test comparison: deployed address → 1 @@ -387,8 +403,13 @@ static void applyEntry(entry_t *entry) { runConstructTest(entry, entry->constructTest, &constructResult, gas); constructHeaderPrinted = true; } - } else if (entry->code.size) { - evmMockCode(*entry->address, entry->code); + } else { + if (entry->address == NULL) { + entry->address = anonymousAddress(); + } + if (entry->code.size) { + evmMockCode(*entry->address, entry->code); + } } evmMockNonce(*entry->address, entry->nonce); evmMockBalance(*entry->address, entry->balance);