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/Makefile b/Makefile index 5a50ad7..f1ef767 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +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)) +CURL_LDFLAGS := $(shell curl-config --libs 2>/dev/null || echo -lcurl) +EXECS=$(patsubst %.c, bin/%, $(wildcard *.c)) $(patsubst %.py, bin/%, $(wildcard *.py)) 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)) @@ -73,6 +74,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/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/dio.c b/dio.c new file mode 100644 index 0000000..8b435f7 --- /dev/null +++ b/dio.c @@ -0,0 +1,580 @@ +/* + * 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 "config.h" +#include "json.h" +#include "ws.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ========================================================= + * Data structures + * ========================================================= */ + +#define LINE_CAP 131072 /* matches evm's rpcBuf */ + +/* ========================================================= + * 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'; +} + +/* ========================================================= + * 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 + 2; + 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; +} + +/* ========================================================= + * Find evm binary + * ========================================================= */ + +static char evmBinPath[4096]; +static const char *evmPath; + +/* + * 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, size_t len, 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, size_t len, 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)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); + + 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; + } + 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(""); +} + + +/* ========================================================= + * 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, "-glnsxo", (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); +} + +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 + * and writes the output into r->output. + */ +static void runViaEvm( + call_result_t *r, + postFn post, void *ctx, + account_t **accounts) +{ + strbuf_t cj = {0}; +#define sbLit(s) sbAppend(&cj, s, sizeof(s) - 1) +#define sbStr(s) sbAppend(&cj, s, strlen(s)) + sbLit("{"); + 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("\"}"); +#undef sbLit +#undef sbStr + + FILE *toChild, *fromChild; + pid_t pid = spawnEvm(evmPath, cj.buf, &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); + + 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); + checkBatchErrors(resp, line); + + 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, r->block); + fflush(toChild); + + } else if (strstr(line, "\"eth_getStorageAt\"")) { + const char *params = jFind(line, "params"); + char addr[ADDR_LEN], rawKey[HEX256_LEN]; + 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); + 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); + fprintf(toChild, "%s\n", resp); + fflush(toChild); + free(resp); + + } else { + /* ---- Final output from evm ---- */ + 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; + } + } + + fclose(toChild); + fclose(fromChild); + int status; + waitpid(pid, &status, 0); + free(line); + free(cj.buf); + + if (!output || WEXITSTATUS(status) != 0) { + fputs("dio: evm execution failed\n", stderr); + _exit(1); + } + + if (r->to[0]) + ensureAccount(accounts, r->to); + if (strcmp(r->from, "0x0000000000000000000000000000000000000000") != 0) + ensureAccount(accounts, r->from); + + r->output = output; +} + + +/* + * 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, + postFn post, void *ctx, + account_t **accounts, + call_result_t **creates, + call_result_t **calls) +{ + call_result_t *r = malloc(sizeof(call_result_t)); + r->to[0] = '\0'; + 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)); + + const char *dataField = jFind(callJson, "data"); + if (!dataField) dataField = jFind(callJson, "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, r->input, ilen + 1); + free(r->input); + r->input = t; + } + + /* Resolve block number */ + if (strcmp(r->block, "latest") == 0) { + static const char kBlockNumber[] = + "{\"jsonrpc\":\"2.0\",\"id\":1," + "\"method\":\"eth_blockNumber\",\"params\":[]}"; + char *resp = post(kBlockNumber, sizeof(kBlockNumber) - 1, ctx); + 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); + } + + runViaEvm(r, post, ctx, accounts); + + if (r->to[0]) { r->next = *calls; *calls = r; } + else { r->next = *creates; *creates = r; } +} + +/* ========================================================= + * 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; + } + + evmPath = findEvm(argv[0]); + + account_t *accounts = NULL; + call_result_t *creates = NULL; + call_result_t *calls = NULL; + + if (inlineJson) { + 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, &creates, &calls); + free(json); + } + } else { + char *json = readAll(stdin); + run(json, post, ctx, &accounts, &creates, &calls); + free(json); + } + + writeConfig(accounts, creates, calls, outfile); + return 0; +} diff --git a/evm.c b/evm.c index 041099b..833a2c7 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]; @@ -36,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); @@ -103,38 +105,76 @@ 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); @@ -152,25 +192,22 @@ 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++); - 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 +219,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 +242,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/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/include/evm.h b/include/evm.h index ec8c128..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; } @@ -101,6 +104,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/json.h b/include/json.h new file mode 100644 index 0000000..3db4f35 --- /dev/null +++ b/include/json.h @@ -0,0 +1,54 @@ +#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); + +/* + * 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(). + */ +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. + * Caller must free(). Returns NULL if not found. + */ +char *resultById(const char *resp, uint64_t targetId); 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/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/include/ws.h b/include/ws.h new file mode 100644 index 0000000..85aca27 --- /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, size_t len, void *ctx); 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 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/src/dio.c b/src/dio.c index bbd18b0..eae3a08 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); @@ -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; @@ -353,7 +355,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); @@ -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); @@ -460,8 +481,17 @@ 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: "); + 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 f7f7518..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); @@ -323,10 +313,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 +335,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 +390,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 +543,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; } @@ -700,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); } @@ -1290,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; @@ -1731,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/json.c b/src/json.c new file mode 100644 index 0000000..2764feb --- /dev/null +++ b/src/json.c @@ -0,0 +1,171 @@ +#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 *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++; + 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; +} + +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++; + 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/network.c b/src/network.c new file mode 100644 index 0000000..bab5928 --- /dev/null +++ b/src/network.c @@ -0,0 +1,134 @@ +#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; + putchar('['); + 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); + puts("]"); + 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); + fputs("\",\"0x", stdout); + fprint256(stdout, key); + printf("\",\"%s\"]}\n", 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); +} 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)); } diff --git a/src/ws.c b/src/ws.c new file mode 100644 index 0000000..d7215e9 --- /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, size_t len, void *ctx) { + CURL *curl = ctx; + if (wsSendFrame(curl, payload, len) != 0) return NULL; + return wsRecvMsg(curl); +} diff --git a/tst/config.c b/tst/config.c new file mode 100644 index 0000000..a7d43e0 --- /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); + assert(fread(buf, 1, len, f) == (size_t)len); + 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/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; +} diff --git a/tst/tst.h b/tst/tst.h new file mode 100644 index 0000000..345880f --- /dev/null +++ b/tst/tst.h @@ -0,0 +1,34 @@ +#include +#include +#include +#include + +#define assertStderr(expectedErr, statement)\ + int rw[2];\ + if (pipe(rw) == -1) { perror("pipe"); exit(1); }\ + 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);\ + }