From 06a1bfb05a594ac2341fa725fcd809d01c2236a9 Mon Sep 17 00:00:00 2001 From: Raoul Date: Wed, 2 Sep 2026 12:21:39 +0000 Subject: [PATCH 1/5] Add docs for the general public --- .gitignore | 2 + .vscode/launch.json | 5 +- README.md | 431 +++++++++++++++++++++++++++++++++--- src/kontrol_node/cli.py | 7 - src/kontrol_node/hello.py | 2 - src/kontrol_node/options.py | 2 - 6 files changed, 405 insertions(+), 44 deletions(-) delete mode 100644 src/kontrol_node/hello.py diff --git a/.gitignore b/.gitignore index b7377b6c..cb616a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ .coverage .vscode +.kdist +io_dir*/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 26b40f59..8687efef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,15 +1,14 @@ { "version": "0.2.0", "configurations": [ - { "name": "Kontrol Node: debug", "type": "debugpy", "request": "launch", "justMyCode": false, "module": "kontrol_node", - "args": ["run", "--steps-tracing", "--port", "8081"], + "args": ["run", "--port", "8081"], "cwd": "${workspaceFolder}" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index b97f48f0..d12515f0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,423 @@ -# kontrol-node +# ⛓️ Kontrol Node +**A local Ethereum testnet node powered by [KEVM](https://github.com/runtimeverification/evm-semantics), the K formal semantics of the EVM.** -## Build from source +[![Install](https://img.shields.io/badge/install-kup-blue)](https://kframework.org/install) +[![Discord](https://img.shields.io/badge/discord-join-7289da)](https://discord.gg/CurfmXNtbN) +[![License](https://img.shields.io/badge/license-BSD--3--Clause-green)](LICENSE) -#### K Framework +[Installation](#installation) • [Usage](#usage) • [JSON-RPC API](#json-rpc-api) • [Contribute](#for-developers) -You need to install the [K Framework] on your system, see the instructions there. -The fastest way is via the [kup package manager], with which you can do to get the correct version of K: +--- -```sh -kup install k.openssl.secp256k1 --version v$(cat deps/k_release) +## 🌟 Overview + +`kontrol-node` serves the standard +[Ethereum JSON-RPC](https://ethereum.org/en/developers/docs/apis/json-rpc/) API on chain ID `31337` +with the same ten pre-funded accounts as Anvil and Hardhat, so existing SDKs, wallets, and +Foundry-based tooling work against it unchanged — just point them at `localhost`. + +What differs is what happens *inside* a transaction. Execution is by KEVM, the same executable EVM +specification [Kontrol](https://github.com/runtimeverification/kontrol) uses for formal +verification, which buys two things an ordinary dev node cannot offer: + +- **Opcode-level traces with full state deltas.** `debug_traceTransaction` returns geth-style + `structLogs`, but each step also carries the storage, balance, nonce and code changes, the memory, + calldata, returndata and program deltas, and the full call context. That is enough to replay an + execution step by step — which is what makes it a debugger backend, and the engine behind + [Simbolik](https://simbolik.runtimeverification.com/), Runtime Verification's Solidity debugger. +- **Behavior you can read.** RPC dispatch, block production and the EVM itself are all K rewrite + rules in [`src/kontrol_node/kdist/`](src/kontrol_node/kdist/) — the specification *is* the + implementation. + +The trade-off is speed: expect seconds per transaction. This is a node for debugging individual +transactions, not for load testing. + +## 🚀 Quick Start + +### Installation + +`kontrol-node` is distributed through [`kup`](https://github.com/runtimeverification/kup), Runtime +Verification's Nix-based package manager. It pulls prebuilt binaries — including the matching K +Framework and compiled semantics — from RV's binary cache, so there is nothing to compile. + +```bash +# 1. Install the kup package manager (one time) +bash <(curl https://kframework.org/install) + +# 2. Install kontrol-node +kup install kontrol-node + +# 3. Verify the installation +kontrol-node version ``` -#### Build using the virtual environment +Upgrade with `kup update kontrol-node`, or see [Build from source](#build-from-source). + +--- + +### Usage -Prerequsites: `python >= 3.10`, [`uv`](https://docs.astral.sh/uv/). +#### Start the node + +`kontrol-node run` starts the JSON-RPC server: -In order to build `kontrol-node`, you need to build these specific targets: -```sh -uv run kdist --verbose build -j2 kontrol-node.simbolik ``` +usage: kontrol-node run [-h] [--verbose] [--debug] [--config-file CONFIG_FILE] + [--config-profile CONFIG_PROFILE] [--host ADDR] + [--port PORT] -To change the default compiler: -```sh -CXX=clang++-15 uv run kdist --verbose build -j2 kontrol-node.simbolik -f +options: + -h, --help show this help message and exit + --verbose, -v Verbose output. + --debug Debug output. + --config-file CONFIG_FILE + Path to Pyk config file. + --config-profile CONFIG_PROFILE + Config profile to be used. + --host ADDR host address (default: 127.0.0.1) + --port PORT port number (default: 8081) ``` -On Apple Silicon: -```sh -APPLE_SILICON=true uv run kdist --verbose build -j2 kontrol-node.simbolik +```bash +kontrol-node run # serve on 127.0.0.1:8081 +kontrol-node run --port 8545 # use the conventional Ethereum port +kontrol-node run --host 0.0.0.0 # accept connections from outside localhost ``` -Targets can be cleaned with: -```sh -uv run kdist clean +Tracing is always on, so `debug_traceTransaction` needs no flag. + +Chain state lives in a fresh `io_dir*` directory under the working directory and is removed on exit, +so **every run starts from genesis**. Requests are handled one at a time. + +#### Deploy and call a contract + +There is no mempool: `eth_sendTransaction` executes the transaction synchronously, mines a block for +it, and returns its hash, so the receipt is ready as soon as the call returns. Transactions are +submitted **unsigned** — name an unlocked account in `from` and the node signs for you. + +The walkthrough below deploys this contract and increments it: + +```solidity +contract Counter { + uint256 public count; // slot 0 + function increment() public { count += 1; } +} ``` -For more information, refer to `kdist --help`. +```bash +# Is the node alive? +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' +# => {"jsonrpc":"2.0","id":1,"result":31337} + +# 1. Deploy Counter — `data` is the contract's init code, `to` is omitted +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":2,"method":"eth_sendTransaction","params":[{ + "from":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas":"0x1c9c37f","gasPrice":"0x1d1a94a2000","value":"0", + "data":"0x6080604052348015600f57600080fd5b5060f78061001e6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c80633fb5c1cb1460415780638381f58a146053578063d09de08a14606d575b600080fd5b6051604c3660046083565b600055565b005b605b60005481565b60405190815260200160405180910390f35b6051600080549080607c83609b565b9190505550565b600060208284031215609457600080fd5b5035919050565b60006001820160ba57634e487b7160e01b600052601160045260246000fd5b506001019056fea2646970667358221220380554e236eca25e41ce3f83305e720d4442aaad4e06b3f2ca8b7900b823776c64736f6c634300081a0033"}]}' +# => {"jsonrpc":"2.0","id":2,"result":"0xa2a48bf94c47073aa235c34e3dfe9bd9687e7b6a6cce28a2380ed48138f51680"} + +# 2. Read the receipt to find the deployed address +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":3,"method":"eth_getTransactionReceipt", + "params":["0xa2a48bf94c47073aa235c34e3dfe9bd9687e7b6a6cce28a2380ed48138f51680"]}' +# => {..."contractAddress":"0x5fbdb2315678afecb367f032d93f642f64180aa3","gasUsed":"0x1a0e1","status":"0x1"...} + +# 3. Call increment() — selector 0xd09de08a +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":4,"method":"eth_sendTransaction","params":[{ + "from":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "to":"0x5FbDB2315678afecb367f032d93F642f64180aa3", + "gas":"0x1c9c37f","gasPrice":"0x1d1a94a2000","value":"0", + "data":"0xd09de08a"}]}' +# => {"jsonrpc":"2.0","id":4,"result":"0x8f2c14205d39e138de7b5133c260405ff4fb68f2ee7212ee521986956867bd81"} + +# 4. Confirm `count` went to 1 by reading storage slot 0 +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":5,"method":"eth_getStorageAt", + "params":["0x5FbDB2315678afecb367f032d93F642f64180aa3","0x0","latest"]}' +# => {"jsonrpc":"2.0","id":5,"result":"0x1"} +``` + +Since every run starts from genesis, the deployed address and both transaction hashes are the same +each time — the commands above can be pasted verbatim. + +Requests can be **batched**: post an array of request objects, get an array of responses in order. + +```bash +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '[{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}, + {"jsonrpc":"2.0","id":2,"method":"eth_getTransactionCount","params":["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","latest"]}]' +# => [{"jsonrpc":"2.0","id":1,"result":31337}, +# {"jsonrpc":"2.0","id":2,"result":"0x2"}] +``` + +#### Trace a transaction + +`debug_traceTransaction` returns the record of every opcode a mined transaction executed, keyed by +the hash `eth_sendTransaction` gave you — so tracing is two requests: send, then trace. The second +parameter is geth's tracer-options object, currently ignored; pass `{}`. + +```bash +curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":6,"method":"debug_traceTransaction", + "params":["0x8f2c14205d39e138de7b5133c260405ff4fb68f2ee7212ee521986956867bd81",{}]}' +``` + +The result is geth-shaped: `failed`, `gas`, `returnValue`, and one `structLogs` entry per opcode. +The `increment()` call above produces 67: + +```jsonc +{ + "jsonrpc": "2.0", + "id": 6, + "result": { + "failed": false, + "gas": 43404, + "returnValue": "", + "structLogs": [ /* 67 entries, one per opcode */ ] + } +} +``` + +Each entry snapshots the machine plus the state *changed by the previous step*. Here is the entry +right after the `SSTORE` that writes `count = 1`, showing that write in `storageChanges`: + +```jsonc +{ + "op": "POP", // opcode about to execute + "pc": 129, // program counter + "depth": 1, // call depth + "gas": 29956606, // gas remaining + "gasCost": 2000000000000, // cost of this step + "stack": ["0xd09de08a", "0x51", "0x0"], // top of stack last + "statusCode": "empty", // KEVM status code; "empty" while still running + + // State deltas — what the previous step changed. `{}` means "nothing changed". + "storageChanges": {"0x5fbdb2315678afecb367f032d93f642f64180aa3": {"0x0": "0x1"}}, + "balanceChanges": {}, + "nonceChanges": {}, + "deployedCodeChanges": {}, + "initCodeChanges": {}, + + // Byte-buffer deltas — `null` means "unchanged since the previous entry". + "memoryChange": null, + "callDataChange": null, + "returnDataChange": null, + "programChange": null, + + // Call context. Addresses in this group are decimal integers, not hex strings. + "msgSender": 1390849295786071768276380950238675083608645509734, + "txOrigin": 1390849295786071768276380950238675083608645509734, + "targetAddress": 546584486846459126461364135121053344201067465379, + "codeAddress": 546584486846459126461364135121053344201067465379, + "msgValue": 0, + "isInitCode": false, + + // Block context + "blockNumber": 2, + "blockTimestamp": 1768610547, + "coinbase": 0, + "difficulty": 0 +} +``` + +Two conventions to know before writing a consumer: + +- **Deltas, not snapshots.** The `*Changes` maps and `*Change` buffers report what the *previous* + step changed, so a write appears on the entry *after* the `SSTORE`. Maps use `{}` for "unchanged", + buffers use `null` ("reuse the last value seen"). Replaying the deltas in order reconstructs the + full state at every step. +- **Addresses are encoded inconsistently.** `*Changes` keys are hex strings, but `msgSender`, + `txOrigin`, `targetAddress`, `codeAddress` and `coinbase` are decimal integers — convert before + comparing. + +Reverting transactions are traced too: `failed` is `true` and the `structLogs` still cover the +opcodes that ran. + +--- + +## JSON-RPC API + +The implemented subset covers deploying, calling, inspecting and debugging contracts. All requests +are `POST`ed to the server root. + +| Method | Parameters | Notes | +|---|---|---| +| `eth_chainId` | — | Returns `31337` as a JSON number, not a hex quantity | +| `eth_sendTransaction` | `[tx]` | Unsigned tx object (`from`, `to`, `gas`, `gasPrice`, `value`, `data`); executes synchronously, mines a block, returns the tx hash | +| `eth_getTransactionByHash` | `[hash]` | | +| `eth_getTransactionReceipt` | `[hash]` | Includes `contractAddress` for deployments | +| `eth_getTransactionCount` | `[address, block]` | | +| `eth_getBalance` | `[address, block]` | | +| `eth_getCode` | `[address, block]` | | +| `eth_getStorageAt` | `[address, slot, block]` | | +| `eth_getBlockByNumber` | `[block, hydrated]` | | +| `eth_getBlockByHash` | `[hash, hydrated]` | | +| `anvil_dumpState` | `[""]` | Full state snapshot: latest block header, every account's balance, nonce, code and storage, the block history, and all transaction receipts | +| `anvil_setBalance` | `[address, balance]` | | +| `debug_traceTransaction` | `[hash, options]` | Opcode-level trace; `options` is accepted and ignored | + +`block` takes a hex number or the tags `"earliest"`, `"latest"`, `"safe"`, `"finalized"`, +`"pending"` — the middle two resolve to latest, since every block here is final. Errors follow +JSON-RPC: `-32601` unknown method, `-32700` malformed JSON, `-32600` malformed request, `-32603` a +request the semantics cannot process. + +### Pre-funded accounts + +Ten accounts are unlocked and funded with 10 000 ETH each, derived from the mnemonic +`test test test test test test test test test test test junk` — the same accounts, with the same +well-known keys, that Anvil and Hardhat use. + +| Address | Private key | +|---|---| +| `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` | `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` | +| `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` | `0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d` | +| `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` | `0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a` | +| `0x90F79bf6EB2c4f870365E785982E1f101E93b906` | `0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6` | +| `0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65` | `0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a` | +| `0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc` | `0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba` | +| `0x976EA74026E726554dB657fA54763abd0C3a0aa9` | `0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e` | +| `0x14dC79964da2C08b23698B3D3cc7Ca32193d9955` | `0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356` | +| `0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f` | `0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97` | +| `0xa0Ee7A142d267C1f36714E4a8F75612F20a79720` | `0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6` | + +> These keys are public. Never send real funds to these addresses. + +The first account is the default sender when a transaction omits `from`. Foundry's deterministic +CREATE2 deployer is pre-deployed at `0x4e59b44847b379578588920ca78fbf26c0b4956c`. + +### Chain configuration and behavior + +- **Chain ID** `31337`, **EVM version** Prague, **block gas limit** `30_000_000`, gas metering on. +- **One block per transaction**, mined automatically; no mempool, no separate mining step. +- **EIP-170 code size checks are disabled**, so oversized contracts deploy as-is. +- **Foundry cheatcodes** are recognized at the usual `vm` address + (`0x7109709ECfa91a80626fF3989D68f67F5b1DD12D`). A cheatcode Kontrol does not implement reverts the + transaction instead of hanging the node, and the revert shows up in the trace. + +### Current limitations + +- **State is not persistent** — each run is a fresh chain, with no flag to keep or reload one. +- **No `eth_sendRawTransaction`**, so a client that signs locally cannot submit here. +- **Block `transactions` arrays are always empty** in `eth_getBlockByNumber` and + `eth_getBlockByHash`, for both `hydrated` values; read transactions by hash instead. +- **`eth_chainId` returns a JSON number** (`31337`) where the spec requires a hex quantity + (`"0x7a69"`); a strict client may reject it. +- **No event-log or filter methods**, no `eth_call`, no `eth_estimateGas`, no WebSocket transport. +- **Throughput is low** — every request runs the K interpreter over the whole semantics. + +--- ## For Developers -Use `make` to run common tasks (see the [Makefile](Makefile) for a complete list of available targets). +Prerequisites: `python >= 3.10`, [`uv`](https://docs.astral.sh/uv/), and the K Framework. Integration +and end-to-end tests also need [Foundry](https://getfoundry.sh/), for `forge` and `anvil`. + +### Build from source + +Install the pinned K Framework version, then compile the semantics: + +```bash +# 1. Install the matching K Framework via kup +kup install k.openssl.secp256k1 --version v$(cat deps/k_release) + +# 2. Compile the K semantics (first run takes a while) +make kdist-build + +# 3. Run the node from the source checkout +uv run kontrol-node run +``` + +`make kdist-build` wraps `uv run kdist build kontrol-node.*`. To drive `kdist` directly: + +```bash +# Choose a different compiler +CXX=clang++-15 uv run kdist --verbose build -j2 kontrol-node.simbolik -f + +# Apple Silicon +APPLE_SILICON=true uv run kdist --verbose build -j2 kontrol-node.simbolik + +# Clean the build +uv run kdist clean +``` + +See `kdist --help` for all options. A Nix flake is also provided: `nix develop` gives a shell with K +and every build dependency, `nix build` builds the package. + +To build and install the wheel: + +```bash +make build +pip install dist/*.whl +``` + +### Common tasks + +Driven by `make` (see the [Makefile](Makefile) for the full list): + +| Target | Description | +|---|---| +| `make kdist-build` | Compile the K semantics (required before integration, e2e, and any real use) | +| `make build` | Build the wheel | +| `make test-unit` | Run unit tests (fast; no semantics needed) | +| `make test-integration` | Replay recorded JSON-RPC requests, singly and batched, against expected responses — and against real Anvil | +| `make test-e2e` | Compile Solidity with `forge`, deploy it, and drive it over JSON-RPC | +| `make test` | Run the full test suite | +| `make cov` | Run tests with a coverage report | +| `make check` | Run all style and type checks (flake8, mypy, autoflake, isort, black) | +| `make format` | Auto-format the codebase | + +The integration suite is golden-file based: each case in +[`src/tests/integration/test-data/`](src/tests/integration/test-data/) pairs an `.in.json` request +with an `.expected.json` response. Regenerate expectations rather than hand-editing them: + +```bash +make test-integration TEST_ARGS=--update-expected-output +``` + +That suite also includes `test_anvil_compatibility`, which sends every recorded request to both +`kontrol-node` and a real `anvil` and asserts the responses match — run it before changing any +response shape. + +### Repository layout + +| Path | Contents | +|---|---| +| [`src/kontrol_node/rpc.py`](src/kontrol_node/rpc.py) | The HTTP server and the interpreter bridge | +| [`src/kontrol_node/cli.py`](src/kontrol_node/cli.py) | Command-line interface | +| [`src/kontrol_node/genesis.json`](src/kontrol_node/genesis.json) | Genesis block and pre-funded accounts | +| [`src/kontrol_node/kdist/node.md`](src/kontrol_node/kdist/node.md) | K semantics: JSON-RPC dispatch, block production, state snapshots | +| [`src/kontrol_node/kdist/trace.md`](src/kontrol_node/kdist/trace.md) | K semantics: opcode trace collection | +| [`src/kontrol_node/kdist/trace-json.md`](src/kontrol_node/kdist/trace-json.md) | K semantics: JSON encoding of traces | +| [`src/tests/`](src/tests/) | Unit, integration, and end-to-end tests | + +### How it works + +The Python process is a thin server; all execution lives in the semantics. It creates an I/O +directory holding the genesis state, then for each request: + +1. writes the request body to `request.json`, +2. runs the compiled K definition, which loads the latest state snapshot, dispatches the method, + executes any transaction, and writes `response.json` plus the new block snapshot, receipt and + trace, +3. streams `response.json` back to the client. + +Requests are therefore serialized, and the K interpreter — not Python — defines what each method +means. Adding an RPC method means adding rules to +[`node.md`](src/kontrol_node/kdist/node.md), not Python code. + +--- + +## About -* `make build`: Build wheel -* `make check`: Check code style -* `make format`: Format code -* `make test-unit`: Run unit tests -* `make test-integration`: Run integration tests +`kontrol-node` is developed by [Runtime Verification](https://runtimeverification.com/), building on +[Kontrol](https://github.com/runtimeverification/kontrol), +[KEVM](https://github.com/runtimeverification/evm-semantics) and the +[K Framework](https://github.com/runtimeverification/k). It is the execution engine behind +[Simbolik](https://simbolik.runtimeverification.com/), RV's Solidity debugger. -[K Framework]: -[kup package manager]: +Licensed under the [BSD 3-Clause License](LICENSE). Questions and contributions welcome — find us on +[Discord](https://discord.gg/CurfmXNtbN) or open an issue. diff --git a/src/kontrol_node/cli.py b/src/kontrol_node/cli.py index 534b98ac..610fd8d2 100644 --- a/src/kontrol_node/cli.py +++ b/src/kontrol_node/cli.py @@ -106,11 +106,4 @@ def parse(s: str) -> list[T]: default=8081, help='port number', ) - run.add_argument( - '--steps-tracing', - dest='steps_tracing', - default=False, - action='store_true', - help='Enable steps tracing used for debug calls returning geth-style traces', - ) return parser diff --git a/src/kontrol_node/hello.py b/src/kontrol_node/hello.py deleted file mode 100644 index 6cab2ee4..00000000 --- a/src/kontrol_node/hello.py +++ /dev/null @@ -1,2 +0,0 @@ -def hello(name: str) -> str: - return f'Hello, {name}!' diff --git a/src/kontrol_node/options.py b/src/kontrol_node/options.py index dc1626ca..096a86b9 100644 --- a/src/kontrol_node/options.py +++ b/src/kontrol_node/options.py @@ -11,7 +11,6 @@ class VMOptions(LoggingOptions): addr: str port: int - steps_tracing: bool chain_id: int gas_price: int @@ -20,7 +19,6 @@ def default() -> dict[str, Any]: return { 'addr': '127.0.0.1', 'port': 8081, - 'steps_tracing': False, 'chain_id': 31337, 'gas_price': 0, } From 495673feb38d27427b44e4d548dce26015358e25 Mon Sep 17 00:00:00 2001 From: Raoul Date: Wed, 2 Sep 2026 15:33:51 +0000 Subject: [PATCH 2/5] Remove outdated comment --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d12515f0..21f2a756 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,6 @@ kontrol-node run --port 8545 # use the conventional Ethereum port kontrol-node run --host 0.0.0.0 # accept connections from outside localhost ``` -Tracing is always on, so `debug_traceTransaction` needs no flag. - Chain state lives in a fresh `io_dir*` directory under the working directory and is removed on exit, so **every run starts from genesis**. Requests are handled one at a time. From 109170df295ebe85a18beb7ebfc5bdd067077317 Mon Sep 17 00:00:00 2001 From: Raoul Date: Wed, 30 Sep 2026 09:53:05 +0000 Subject: [PATCH 3/5] Enable CI, disable CD Re-enable CI and remove every use of repository secrets, following runtimeverification/evm-semantics#2878 and runtimeverification/kontrol#1162. Release automation will be reinstated in a follow-up PR. - Remove the PAT-based `version-bump` job from `test.yml`, and delete `release.yml` and `update.yml`. - Pin actions to commit SHAs, scope the workflow token to `contents: read` (plus `packages: read` for jobs building the CI image), and set `persist-credentials: false` on checkouts. - `with-docker`: log in to ghcr.io with a fresh token, pass inputs via `env:` instead of interpolating them into the shell, and quote expansions. - Make Docker teardown best-effort and use `--timeout` instead of `--time`. - Add `lint-workflows.yml` running actionlint and zizmor on every PR. Co-Authored-By: Claude Opus 5.5 --- .github/actionlint.yaml | 4 ++ .github/actions/with-docker/action.yml | 32 ++++++---- .github/workflows/lint-workflows.yml | 53 +++++++++++++++++ .github/workflows/release.yml | 51 ---------------- .github/workflows/test.yml | 68 ++++++++++----------- .github/workflows/update.yml | 81 -------------------------- 6 files changed, 110 insertions(+), 179 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/lint-workflows.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/update.yml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..0a8b73ec --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +# Custom labels for the self-hosted runners; actionlint cannot discover them. +self-hosted-runner: + labels: + - normal diff --git a/.github/actions/with-docker/action.yml b/.github/actions/with-docker/action.yml index 483adb4f..d519725e 100644 --- a/.github/actions/with-docker/action.yml +++ b/.github/actions/with-docker/action.yml @@ -3,36 +3,48 @@ description: 'Run a given stage with Docker Image' inputs: container-name: description: 'Docker container name to use' - type: string required: true runs: using: 'composite' steps: + # The Dockerfile's first stage is `FROM ghcr.io/foundry-rs/foundry`, a + # third-party org's public image. Self-hosted runners can carry a stale + # ghcr.io credential in their docker config; BuildKit then sends it when + # fetching the pull token and ghcr.io rejects it ("denied: denied") instead + # of falling back to anonymous. Log in with a fresh token so the credential + # is always valid (a valid GITHUB_TOKEN can pull public images from any org). + - name: 'Log in to ghcr.io' + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} - name: 'Set up Docker' + env: + CONTAINER_NAME: ${{ inputs.container-name }} shell: bash {0} run: | set -euxo pipefail - CONTAINER_NAME=${{ inputs.container-name }} - TAG=runtimeverificationinc/${CONTAINER_NAME} + TAG="runtimeverificationinc/${CONTAINER_NAME}" K_VERSION=$(cat deps/k_release) UV_VERSION=$(cat deps/uv_release) docker build . \ --file .github/actions/with-docker/Dockerfile \ - --tag ${TAG} \ - --build-arg K_VERSION=${K_VERSION} \ - --build-arg UV_VERSION=${UV_VERSION} + --tag "${TAG}" \ + --build-arg K_VERSION="${K_VERSION}" \ + --build-arg UV_VERSION="${UV_VERSION}" docker run \ - --name ${CONTAINER_NAME} \ + --name "${CONTAINER_NAME}" \ --rm \ --interactive \ --tty \ --detach \ --user root \ --workdir /home/user \ - ${TAG} + "${TAG}" - docker cp . ${CONTAINER_NAME}:/home/user - docker exec ${CONTAINER_NAME} chown -R user:user /home/user + docker cp . "${CONTAINER_NAME}":/home/user + docker exec "${CONTAINER_NAME}" chown -R user:user /home/user diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml new file mode 100644 index 00000000..aeb0f798 --- /dev/null +++ b/.github/workflows/lint-workflows.yml @@ -0,0 +1,53 @@ +--- +name: 'Lint Workflows' +on: + pull_request: + branches: + - 'main' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + actionlint: + name: 'actionlint' + runs-on: ubuntu-latest + steps: + - name: 'Check out code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: 'Install actionlint' + env: + # Pin the release and verify it, both values should be updated together. + ACTIONLINT_VERSION: '1.7.12' + ACTIONLINT_SHA256: '8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8' + run: | + set -euxo pipefail + curl --fail --silent --show-error --location --output actionlint.tar.gz \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum --check --strict + tar --extract --gzip --file actionlint.tar.gz actionlint + - name: 'Run actionlint' + run: ./actionlint -color + + zizmor: + name: 'zizmor' + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # upload SARIF, so findings annotate the diff + steps: + - name: 'Check out code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: 'Run zizmor' + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + version: '1.29.0' + online-audits: false + advanced-security: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c50bacc1..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Release -on: - push: - branches: - - main - -jobs: - - release: - name: 'Publish Release' - runs-on: [self-hosted, linux, flyweight] - steps: - - name: 'Check out code' - uses: actions/checkout@v4 - with: - ref: ${{ github.event.push.head.sha }} - fetch-depth: 0 - - name: 'Make release' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -x - VERSION=v$(cat package/version) - gh release create ${VERSION} --target ${{ github.sha }} - - - name: 'Determine Version' - id: version - run: | - set -x - # Use version from package/version if it exists, otherwise generate one - if [ -f package/version ]; then - VERSION=$(cat package/version | tr -d '[:space:]') - else - # Generate date-based version: YYYY.MM.DD - VERSION=$(date +%Y.%m.%d) - fi - - echo "tag=${VERSION}" >> $GITHUB_OUTPUT - echo "Using version: ${VERSION}" - - - name: 'Update dependents' - run: | - set -x - TAG="${{ steps.version.outputs.tag }}" - curl --fail \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.JENKINS_GITHUB_PAT }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/runtimeverification/devops/dispatches \ - -d '{"event_type":"on-demand-test","client_payload":{"repo":"runtimeverification/kontrol-node","version":"'${TAG}'"}}' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6280a819..f3f9988f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,77 +6,71 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -jobs: +# No job here writes to the repository, so the token stays read-only regardless of +# the repository default. Jobs that build the CI image additionally need +# `packages: read` to pull `ghcr.io/foundry-rs/foundry`. +permissions: + contents: read - version-bump: - name: 'Version Bump' - runs-on: [self-hosted, linux, flyweight] - steps: - - name: 'Check out code' - uses: actions/checkout@v4 - with: - token: ${{ secrets.JENKINS_GITHUB_PAT }} - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - name: 'Configure GitHub user' - run: | - git config user.name devops - git config user.email devops@runtimeverification.com - - name: 'Update version' - run: | - og_version=$(git show origin/${GITHUB_BASE_REF}:package/version) - ./package/version.sh bump ${og_version} - ./package/version.sh sub - uv lock - new_version=$(cat package/version) - git add --update && git commit --message "Set Version: ${new_version}" || true - - name: 'Push updates' - run: git push origin HEAD:${GITHUB_HEAD_REF} +jobs: code-quality-checks: - needs: version-bump name: 'Code Quality Checks' runs-on: [self-hosted, linux, normal] + permissions: + contents: read + packages: read env: CONTAINER: kontrol-node-code-quality-${{ github.sha }} steps: - name: 'Check out code' - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - name: 'Set up Docker' uses: ./.github/actions/with-docker with: - container-name: ${CONTAINER} + container-name: ${{ env.CONTAINER }} - name: 'Run code quality checks' - run: docker exec --user user ${CONTAINER} make check + run: docker exec --user user "${CONTAINER}" make check - name: 'Run pyupgrade' - run: docker exec --user user ${CONTAINER} make pyupgrade + run: docker exec --user user "${CONTAINER}" make pyupgrade - name: 'Run unit tests' - run: docker exec --user user ${CONTAINER} make test-unit + run: docker exec --user user "${CONTAINER}" make test-unit - name: 'Tear down Docker' if: always() - run: docker stop --time=0 ${CONTAINER} + run: | + # Best effort: the container does not exist if 'Set up Docker' failed, and a + # non-zero exit here would mask the step that actually failed. + docker stop --timeout=0 "${CONTAINER}" || true tests: needs: code-quality-checks name: 'Tests' runs-on: [self-hosted, linux, normal] + permissions: + contents: read + packages: read env: CONTAINER: kontrol-node-test-${{ github.sha }} steps: - name: 'Check out code' - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - name: 'Set up Docker' uses: ./.github/actions/with-docker with: - container-name: ${CONTAINER} + container-name: ${{ env.CONTAINER }} - name: 'Build kontrol-node' - run: docker exec --user user ${CONTAINER} uv run kdist -v build kontrol-node.simbolik + run: docker exec --user user "${CONTAINER}" uv run kdist -v build kontrol-node.simbolik - name: 'Run tests' - run: docker exec --user user ${CONTAINER} make test + run: docker exec --user user "${CONTAINER}" make test - name: 'Tear down Docker' if: always() - run: docker stop --time=0 ${CONTAINER} + run: | + # Best effort: the container does not exist if 'Set up Docker' failed, and a + # non-zero exit here would mask the step that actually failed. + docker stop --timeout=0 "${CONTAINER}" || true diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml deleted file mode 100644 index e67b3bfd..00000000 --- a/.github/workflows/update.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: 'Update Version' -on: - push: - branches: - - '_update-deps/runtimeverification/kontrol' - - '_update-deps/runtimeverification/rv-nix-tools' - - '_update-deps-cron/uv2nix' - - '_update-deps-cron/pyproject-build-systems' - workflow_dispatch: -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - update-deps: - name: 'Update Dependecies' - runs-on: [self-hosted, linux, flyweight] - steps: - - name: 'Check out code' - uses: actions/checkout@v4 - with: - submodules: recursive - token: ${{ secrets.JENKINS_GITHUB_PAT }} - - name: 'Configure GitHub user' - run: | - git config user.name devops - git config user.email devops@runtimeverification.com - - name: 'Install Nix' - uses: cachix/install-nix-action@v31.2.0 - with: - install_url: https://releases.nixos.org/nix/nix-2.31.0/install - extra_nix_config: | - substituters = http://cache.nixos.org https://cache.iog.io - trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= - access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - uses: cachix/cachix-action@v16 - with: - name: k-framework - authToken: ${{ secrets.CACHIX_PUBLIC_TOKEN }} - - name: 'Update uv release tag' - id: uv_release - run: | - UV2NIX_VERSION=$(cat deps/uv2nix) - UV_VERSION=$(curl -s https://raw.githubusercontent.com/pyproject-nix/uv2nix/$(cat deps/uv2nix)/pkgs/uv-bin/srcs.json | jq -r .version) - [[ "$UV_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] - echo $UV_VERSION > deps/uv_release - git add deps/uv_release && git commit -m "Sync uv version: uv ${UV_VERSION}" || true - echo uv_version=$UV_VERSION >> "${GITHUB_OUTPUT}" - - name: 'Install Python' - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: 'Install uv' - uses: astral-sh/setup-uv@v6 - with: - version: ${{ steps.uv_release.outputs.uv_version }} - - name: 'Update uv files' - run: | - KONTROL_VERSION="$(cat deps/kontrol_release)" - sed -i 's! "kontrol@git+https://github.com/runtimeverification/kontrol.git@v[0-9\.]*",! "kontrol@git+https://github.com/runtimeverification/kontrol.git@v'${KONTROL_VERSION}'",!' pyproject.toml - uv lock --upgrade - git add . && git commit -m "Sync uv files: kontrol ${KONTROL_VERSION}" || true - - name: 'Update K release' - run: | - K_VERSION=$(uv run python3 -c 'import pyk; print(pyk.__version__)') - echo ${K_VERSION} > deps/k_release - git add deps/k_release && git commit -m "Sync release file: K ${K_VERSION}" || true - - name: 'Update Nix flake inputs' - run: | - UV2NIX_VERSION=$(cat deps/uv2nix) - PYPROJECT_BUILD_SYSTEMS_VERSION=$(cat deps/pyproject-build-systems) - KONTROL_VERSION=$(cat deps/kontrol_release) - RV_NIX_TOOLS_VERSION=$(cat deps/rv-nix-tools) - sed -i 's! uv2nix.url = "github:pyproject-nix/uv2nix/[a-z0-9\.]*"! uv2nix.url = "github:pyproject-nix/uv2nix/'"${UV2NIX_VERSION}"'"!' flake.nix - sed -i 's! pyproject-build-systems.url = "github:pyproject-nix/build-system-pkgs/[a-z0-9\.]*"! pyproject-build-systems.url = "github:pyproject-nix/build-system-pkgs/'"${PYPROJECT_BUILD_SYSTEMS_VERSION}"'"!' flake.nix - sed -i 's! kontrol.url = "github:runtimeverification/kontrol/[v0-9\.]*"! kontrol.url = "github:runtimeverification/kontrol/v'"${KONTROL_VERSION}"'"!' flake.nix - sed -i 's! rv-nix-tools.url = "github:runtimeverification/rv-nix-tools/[a-z0-9\.]*"! rv-nix-tools.url = "github:runtimeverification/rv-nix-tools/'"${RV_NIX_TOOLS_VERSION}"'"!' flake.nix - nix flake update - git add flake.nix flake.lock && git commit -m 'flake.{nix,lock}: update Nix derivations' || true - - name: 'Push updates' - run: git push From a45ba5ac0d051aeb2f7686249fad1bbe6208a073 Mon Sep 17 00:00:00 2001 From: Raoul Date: Wed, 30 Sep 2026 10:20:18 +0000 Subject: [PATCH 4/5] with-docker: copy checkout contents so Podman runners find the project On Podman-backed runners, `docker cp . :/home/user` copies the checkout directory itself to /home/user/, so `uv run` finds no project and falls back to the base image's `kdist` ("No target matches pattern: 'kontrol-node.simbolik'"). `./.` copies the directory's contents under both Docker and Podman. Co-Authored-By: Claude Opus 5.5 --- .github/actions/with-docker/action.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/with-docker/action.yml b/.github/actions/with-docker/action.yml index d519725e..3a2bc9fa 100644 --- a/.github/actions/with-docker/action.yml +++ b/.github/actions/with-docker/action.yml @@ -46,5 +46,8 @@ runs: --workdir /home/user \ "${TAG}" - docker cp . "${CONTAINER_NAME}":/home/user + # `./.` copies the checkout's contents into /home/user under both Docker and + # Podman; with a bare `.`, Podman copies the directory itself to + # /home/user/, leaving no project where `uv run` looks for one. + docker cp ./. "${CONTAINER_NAME}":/home/user docker exec "${CONTAINER_NAME}" chown -R user:user /home/user From 4fceca6b4f9b937d5a88cfa2ef6af659a8ae1719 Mon Sep 17 00:00:00 2001 From: Raoul Date: Thu, 1 Oct 2026 10:14:09 +0000 Subject: [PATCH 5/5] Rename non-standard RPC methods to the kontrol_ prefix anvil_dumpState, anvil_setBalance and debug_traceTransaction are not compatible with Anvil or geth, so their names suggested compatibility that doesn't exist. Rename them to kontrol_dumpState, kontrol_setBalance and kontrol_traceTransaction. Also remove the disabled test_anvil_compatibility test, its anvil fixture and helpers, and the README references to it. Co-Authored-By: Claude Opus 5.5 --- README.md | 20 +++--- src/kontrol_node/kdist/node.md | 38 +++++----- src/kontrol_node/rpc.py | 4 +- src/tests/e2e/test_e2e.py | 28 ++++---- src/tests/integration/conftest.py | 30 -------- ..._0.in.json => kontrol_dumpState_0.in.json} | 2 +- ..._1.in.json => kontrol_dumpState_1.in.json} | 2 +- ...0.in.json => kontrol_setBalance_0.in.json} | 2 +- ...1.in.json => kontrol_setBalance_1.in.json} | 2 +- ...son => kontrol_traceTransaction_0.in.json} | 2 +- ...son => kontrol_traceTransaction_1.in.json} | 4 +- ...=> kontrol_traceTransaction_mocks.in.json} | 4 +- ... => kontrol_traceTransaction_slow.in.json} | 4 +- ...json => kontrol_dumpState_0.expected.json} | 0 ...json => kontrol_dumpState_1.expected.json} | 0 ...son => kontrol_setBalance_0.expected.json} | 0 ...son => kontrol_setBalance_1.expected.json} | 0 ... kontrol_traceTransaction_0.expected.json} | 0 ... kontrol_traceTransaction_1.expected.json} | 0 ...trol_traceTransaction_mocks.expected.json} | 0 ...ntrol_traceTransaction_slow.expected.json} | 0 src/tests/integration/test-data/rpc-tests-all | 16 ++--- src/tests/integration/test_rpc.py | 71 ------------------- 23 files changed, 62 insertions(+), 167 deletions(-) rename src/tests/integration/test-data/input/{anvil_dumpState_0.in.json => kontrol_dumpState_0.in.json} (64%) rename src/tests/integration/test-data/input/{anvil_dumpState_1.in.json => kontrol_dumpState_1.in.json} (97%) rename src/tests/integration/test-data/input/{anvil_setBalance_0.in.json => kontrol_setBalance_0.in.json} (92%) rename src/tests/integration/test-data/input/{anvil_setBalance_1.in.json => kontrol_setBalance_1.in.json} (92%) rename src/tests/integration/test-data/input/{debug_traceTransaction_0.in.json => kontrol_traceTransaction_0.in.json} (96%) rename src/tests/integration/test-data/input/{debug_traceTransaction_1.in.json => kontrol_traceTransaction_1.in.json} (99%) rename src/tests/integration/test-data/input/{debug_traceTransaction_mocks.in.json => kontrol_traceTransaction_mocks.in.json} (99%) rename src/tests/integration/test-data/input/{debug_traceTransaction_slow.in.json => kontrol_traceTransaction_slow.in.json} (99%) rename src/tests/integration/test-data/output/{anvil_dumpState_0.expected.json => kontrol_dumpState_0.expected.json} (100%) rename src/tests/integration/test-data/output/{anvil_dumpState_1.expected.json => kontrol_dumpState_1.expected.json} (100%) rename src/tests/integration/test-data/output/{anvil_setBalance_0.expected.json => kontrol_setBalance_0.expected.json} (100%) rename src/tests/integration/test-data/output/{anvil_setBalance_1.expected.json => kontrol_setBalance_1.expected.json} (100%) rename src/tests/integration/test-data/output/{debug_traceTransaction_0.expected.json => kontrol_traceTransaction_0.expected.json} (100%) rename src/tests/integration/test-data/output/{debug_traceTransaction_1.expected.json => kontrol_traceTransaction_1.expected.json} (100%) rename src/tests/integration/test-data/output/{debug_traceTransaction_mocks.expected.json => kontrol_traceTransaction_mocks.expected.json} (100%) rename src/tests/integration/test-data/output/{debug_traceTransaction_slow.expected.json => kontrol_traceTransaction_slow.expected.json} (100%) diff --git a/README.md b/README.md index 21f2a756..7c8358f1 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ What differs is what happens *inside* a transaction. Execution is by KEVM, the s specification [Kontrol](https://github.com/runtimeverification/kontrol) uses for formal verification, which buys two things an ordinary dev node cannot offer: -- **Opcode-level traces with full state deltas.** `debug_traceTransaction` returns geth-style +- **Opcode-level traces with full state deltas.** `kontrol_traceTransaction` returns geth-style `structLogs`, but each step also carries the storage, balance, nonce and code changes, the memory, calldata, returndata and program deltas, and the full call context. That is enough to replay an execution step by step — which is what makes it a debugger backend, and the engine behind @@ -154,13 +154,13 @@ curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ #### Trace a transaction -`debug_traceTransaction` returns the record of every opcode a mined transaction executed, keyed by +`kontrol_traceTransaction` returns the record of every opcode a mined transaction executed, keyed by the hash `eth_sendTransaction` gave you — so tracing is two requests: send, then trace. The second parameter is geth's tracer-options object, currently ignored; pass `{}`. ```bash curl -s http://127.0.0.1:8081 -H 'Content-Type: application/json' \ - -d '{"jsonrpc":"2.0","id":6,"method":"debug_traceTransaction", + -d '{"jsonrpc":"2.0","id":6,"method":"kontrol_traceTransaction", "params":["0x8f2c14205d39e138de7b5133c260405ff4fb68f2ee7212ee521986956867bd81",{}]}' ``` @@ -254,9 +254,9 @@ are `POST`ed to the server root. | `eth_getStorageAt` | `[address, slot, block]` | | | `eth_getBlockByNumber` | `[block, hydrated]` | | | `eth_getBlockByHash` | `[hash, hydrated]` | | -| `anvil_dumpState` | `[""]` | Full state snapshot: latest block header, every account's balance, nonce, code and storage, the block history, and all transaction receipts | -| `anvil_setBalance` | `[address, balance]` | | -| `debug_traceTransaction` | `[hash, options]` | Opcode-level trace; `options` is accepted and ignored | +| `kontrol_dumpState` | `[""]` | Full state snapshot: latest block header, every account's balance, nonce, code and storage, the block history, and all transaction receipts | +| `kontrol_setBalance` | `[address, balance]` | | +| `kontrol_traceTransaction` | `[hash, options]` | Opcode-level trace; `options` is accepted and ignored | `block` takes a hex number or the tags `"earliest"`, `"latest"`, `"safe"`, `"finalized"`, `"pending"` — the middle two resolve to latest, since every block here is final. Errors follow @@ -312,7 +312,7 @@ CREATE2 deployer is pre-deployed at `0x4e59b44847b379578588920ca78fbf26c0b4956c` ## For Developers Prerequisites: `python >= 3.10`, [`uv`](https://docs.astral.sh/uv/), and the K Framework. Integration -and end-to-end tests also need [Foundry](https://getfoundry.sh/), for `forge` and `anvil`. +and end-to-end tests also need [Foundry](https://getfoundry.sh/), for `forge`. ### Build from source @@ -361,7 +361,7 @@ Driven by `make` (see the [Makefile](Makefile) for the full list): | `make kdist-build` | Compile the K semantics (required before integration, e2e, and any real use) | | `make build` | Build the wheel | | `make test-unit` | Run unit tests (fast; no semantics needed) | -| `make test-integration` | Replay recorded JSON-RPC requests, singly and batched, against expected responses — and against real Anvil | +| `make test-integration` | Replay recorded JSON-RPC requests, singly and batched, against expected responses | | `make test-e2e` | Compile Solidity with `forge`, deploy it, and drive it over JSON-RPC | | `make test` | Run the full test suite | | `make cov` | Run tests with a coverage report | @@ -376,10 +376,6 @@ with an `.expected.json` response. Regenerate expectations rather than hand-edit make test-integration TEST_ARGS=--update-expected-output ``` -That suite also includes `test_anvil_compatibility`, which sends every recorded request to both -`kontrol-node` and a real `anvil` and asserts the responses match — run it before changing any -response shape. - ### Repository layout | Path | Contents | diff --git a/src/kontrol_node/kdist/node.md b/src/kontrol_node/kdist/node.md index 284f416e..e6b6469b 100644 --- a/src/kontrol_node/kdist/node.md +++ b/src/kontrol_node/kdist/node.md @@ -534,11 +534,11 @@ Similarly, we save a state snapshot after the block was mined. ``` ############################################################################### -## anvil_dumpState +## kontrol_dumpState ```k - rule RPCRequest( REQ_ID, AnvilDumpState() ) + rule RPCRequest( REQ_ID, KontrolDumpState() ) => #appendFile( #responseFile, #batchSep +String "{ \"jsonrpc\": \"2.0\"" +String ", \"id\": " +String Int2String(REQ_ID) +String @@ -552,10 +552,10 @@ Similarly, we save a state snapshot after the block was mined. ``` ############################################################################### -## anvil_setBalance +## kontrol_setBalance ```k - rule RPCRequest( REQ_ID, AnvilSetBalance(ADDR, NEW_BALANCE)) + rule RPCRequest( REQ_ID, KontrolSetBalance(ADDR, NEW_BALANCE)) => #saveStateDump ~> RPCResponse( null ) ... @@ -567,7 +567,7 @@ Similarly, we save a state snapshot after the block was mined. ... - rule RPCRequest( REQ_ID, AnvilSetBalance(ADDR, NEW_BALANCE)) + rule RPCRequest( REQ_ID, KontrolSetBalance(ADDR, NEW_BALANCE)) => #saveStateDump ~> RPCResponse( null ) ... @@ -587,22 +587,22 @@ Similarly, we save a state snapshot after the block was mined. ``` ############################################################################### -## debug_traceTransaction +## kontrol_traceTransaction -At the point the debug_traceTransaction method is called, the transaction has already been +At the point the kontrol_traceTransaction method is called, the transaction has already been processed and its trace stored on disk. We just need to read the trace and return it. Since the trace can be large, we avoid loading it into memory as a JSON object, and just build the response string directly. ```k - rule RPCRequest( REQ_ID, DebugTraceTransaction( _TX_HASH ) ) + rule RPCRequest( REQ_ID, KontrolTraceTransaction( _TX_HASH ) ) => RPCResponse( null ) ... _ => REQ_ID [owise] - rule RPCRequest( REQ_ID, DebugTraceTransaction( TX_HASH ) ) + rule RPCRequest( REQ_ID, KontrolTraceTransaction( TX_HASH ) ) => #appendFile( #responseFile, #batchSep +String "{ \"jsonrpc\": \"2.0\"" +String ", \"id\": " +String Int2String(REQ_ID) +String @@ -1069,9 +1069,9 @@ This section defines an intermediate represention for JSON RPC requests. | EthGetBlockByHash( Int, Bool ) // block hash | EthGetTransactionCount( Int, Int ) // address, block number | EthGetStorageAt( Int, Int, Int ) // address, slot, block number - | AnvilDumpState() // TODO: add options - | AnvilSetBalance( Int, Int ) // address, balance - | DebugTraceTransaction( Int ) // tx hash + | KontrolDumpState() // TODO: add options + | KontrolSetBalance( Int, Int ) // address, balance + | KontrolTraceTransaction( Int ) // tx hash | UnknownMethod() | InvalidRequest() // ---------------------------------------- @@ -1178,16 +1178,16 @@ intermediate representation. #let BLOCK_INT = #parseBlockNumber( BLOCK_NUM ) #in EthGetStorageAt( ADDR_INT, SLOT_INT, BLOCK_INT ) - rule #rpcLoadParams( "anvil_dumpState", [ _ ] ) - => AnvilDumpState() + rule #rpcLoadParams( "kontrol_dumpState", [ _ ] ) + => KontrolDumpState() - rule #rpcLoadParams( "anvil_setBalance", [ ADDR:String, NEW_BALANCE:String ] ) + rule #rpcLoadParams( "kontrol_setBalance", [ ADDR:String, NEW_BALANCE:String ] ) => #let ADDR_INT = #parseAddr( ADDR ) #in #let BALANCE_INT = #parseWord( NEW_BALANCE ) #in - AnvilSetBalance( ADDR_INT, BALANCE_INT ) + KontrolSetBalance( ADDR_INT, BALANCE_INT ) - rule #rpcLoadParams( "debug_traceTransaction", [ TX_HASH:String, _OPTIONS:JSON ] ) - => DebugTraceTransaction( #parseWord( TX_HASH ) ) + rule #rpcLoadParams( "kontrol_traceTransaction", [ TX_HASH:String, _OPTIONS:JSON ] ) + => KontrolTraceTransaction( #parseWord( TX_HASH ) ) rule #rpcLoadParams( _, _ ) => UnknownMethod() [owise] @@ -1289,7 +1289,7 @@ K configuration. "value": intToHex( MSG_VALUE ), "gas_limit": MSG_GAS_LIMIT, "gas_price": MSG_GAS_PRICE - // TODO: We're currently using debug_traceTransaction for tracing + // TODO: We're currently using kontrol_traceTransaction for tracing // We may consider switching to this new format in the future } }], diff --git a/src/kontrol_node/rpc.py b/src/kontrol_node/rpc.py index 6b6587be..3c6a4f47 100644 --- a/src/kontrol_node/rpc.py +++ b/src/kontrol_node/rpc.py @@ -37,8 +37,8 @@ # eth_getBlockByHash # eth_getTransactionCount # eth_getStorageAt -# anvil_stateDump -# debug_traceTransaction +# kontrol_dumpState +# kontrol_traceTransaction # requests.json <- JSON-RPC requests # blocks/block_0.json <- The initial StateDump (genesis state) diff --git a/src/tests/e2e/test_e2e.py b/src/tests/e2e/test_e2e.py index 44d238c3..cc363da8 100644 --- a/src/tests/e2e/test_e2e.py +++ b/src/tests/e2e/test_e2e.py @@ -201,7 +201,7 @@ def test_trace_unimplemented_cheatcode( compile_solidity: Callable[[str, str], CompiledContract], rpc: RPCClient, ) -> None: - """debug_traceTransaction should return a valid trace for a transaction + """kontrol_traceTransaction should return a valid trace for a transaction that reverted due to an unimplemented cheatcode.""" compiled = compile_solidity(CHEATCODE_INTERFACE_SOL, 'CheatcodeInterfaceCaller') caller = rpc.deploy(compiled) @@ -210,7 +210,7 @@ def test_trace_unimplemented_cheatcode( receipt = rpc.receipt(tx_hash) assert receipt['status'] == '0x0' - trace = rpc._rpc('debug_traceTransaction', [tx_hash, {}]) + trace = rpc._rpc('kontrol_traceTransaction', [tx_hash, {}]) assert trace is not None assert trace['failed'] is True @@ -525,16 +525,16 @@ def test_nonexistent_account_returns_zero(self, rpc: RPCClient) -> None: # --------------------------------------------------------------------------- -# Tests: anvil_setBalance +# Tests: kontrol_setBalance # --------------------------------------------------------------------------- -class TestAnvilSetBalance: +class TestKontrolSetBalance: def test_set_existing_account_balance(self, rpc: RPCClient) -> None: account = '0x976EA74026E726554dB657fA54763abd0C3a0aa9' new_balance = 12345 - result = rpc._rpc('anvil_setBalance', [account, hex(new_balance)]) + result = rpc._rpc('kontrol_setBalance', [account, hex(new_balance)]) assert result is None # returns null on success balance = rpc.balance(account) @@ -544,20 +544,20 @@ def test_set_new_account_balance(self, rpc: RPCClient) -> None: new_account = '0x0000000000000000000000000000000000001234' new_balance = 999 - rpc._rpc('anvil_setBalance', [new_account, hex(new_balance)]) + rpc._rpc('kontrol_setBalance', [new_account, hex(new_balance)]) balance = rpc.balance(new_account) assert balance == new_balance # --------------------------------------------------------------------------- -# Tests: anvil_dumpState +# Tests: kontrol_dumpState # --------------------------------------------------------------------------- -class TestAnvilDumpState: +class TestKontrolDumpState: def test_returns_state(self, rpc: RPCClient) -> None: - result = rpc._rpc('anvil_dumpState', [None]) + result = rpc._rpc('kontrol_dumpState', [None]) assert result is not None assert isinstance(result, dict) @@ -571,7 +571,7 @@ def test_deployed_contract_in_state( compiled = compile_solidity(COUNTER_SOL, 'Counter') counter = rpc.deploy(compiled) - result = rpc._rpc('anvil_dumpState', [None]) + result = rpc._rpc('kontrol_dumpState', [None]) assert result is not None # The contract address should appear in accounts @@ -583,11 +583,11 @@ def test_deployed_contract_in_state( # --------------------------------------------------------------------------- -# Tests: debug_traceTransaction +# Tests: kontrol_traceTransaction # --------------------------------------------------------------------------- -class TestDebugTraceTransaction: +class TestKontrolTraceTransaction: def test_trace_simple_tx( self, compile_solidity: Callable[[str, str], CompiledContract], @@ -597,7 +597,7 @@ def test_trace_simple_tx( counter = rpc.deploy(compiled) tx_hash = counter.send('increment()') - trace = rpc._rpc('debug_traceTransaction', [tx_hash, {}]) + trace = rpc._rpc('kontrol_traceTransaction', [tx_hash, {}]) assert trace is not None assert 'failed' in trace @@ -608,7 +608,7 @@ def test_trace_simple_tx( def test_trace_nonexistent_returns_null(self, rpc: RPCClient) -> None: fake_hash = '0x' + '00' * 32 - result = rpc._rpc('debug_traceTransaction', [fake_hash, {}]) + result = rpc._rpc('kontrol_traceTransaction', [fake_hash, {}]) assert result is None diff --git a/src/tests/integration/conftest.py b/src/tests/integration/conftest.py index 5504e888..a3d05595 100644 --- a/src/tests/integration/conftest.py +++ b/src/tests/integration/conftest.py @@ -1,7 +1,5 @@ from __future__ import annotations -import re -import subprocess import sys import threading import time @@ -40,31 +38,3 @@ def server() -> Iterator[str]: yield f'http://{SERVER_HOST}:{server.port()}' server.shutdown() server_thread.join() - - -@pytest.fixture -def anvil() -> Iterator[str]: - """Fixture to start an Anvil instance on a dynamically assigned port. - - This fixture starts an Anvil instance for each test function, running it on a dynamically allocated - port to avoid conflicts. The fixture yields the host and port information for the Anvil instance. - - :yield: A tuple containing the host and port of the Anvil instance. - """ - cmd = ('anvil', '--port', '0', '--steps-tracing') - host_pattern = r'Listening on (.+):(\d+)' - process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - - time.sleep(2) - assert process.stdout is not None - for line in process.stdout: - match = re.search(host_pattern, line) - if match: - host, port = match.groups() - break - else: - process.terminate() - raise RuntimeError('Failed to start Anvil and retrieve host/port information.') - - yield f'http://{host}:{port}' - process.terminate() diff --git a/src/tests/integration/test-data/input/anvil_dumpState_0.in.json b/src/tests/integration/test-data/input/kontrol_dumpState_0.in.json similarity index 64% rename from src/tests/integration/test-data/input/anvil_dumpState_0.in.json rename to src/tests/integration/test-data/input/kontrol_dumpState_0.in.json index 190755b1..57e8ee23 100644 --- a/src/tests/integration/test-data/input/anvil_dumpState_0.in.json +++ b/src/tests/integration/test-data/input/kontrol_dumpState_0.in.json @@ -1,7 +1,7 @@ [ { "jsonrpc": "2.0", - "method": "anvil_dumpState", + "method": "kontrol_dumpState", "params": [], "id": 0 } diff --git a/src/tests/integration/test-data/input/anvil_dumpState_1.in.json b/src/tests/integration/test-data/input/kontrol_dumpState_1.in.json similarity index 97% rename from src/tests/integration/test-data/input/anvil_dumpState_1.in.json rename to src/tests/integration/test-data/input/kontrol_dumpState_1.in.json index 800ba1e8..79f6baf7 100644 --- a/src/tests/integration/test-data/input/anvil_dumpState_1.in.json +++ b/src/tests/integration/test-data/input/kontrol_dumpState_1.in.json @@ -30,7 +30,7 @@ }, { "jsonrpc": "2.0", - "method": "anvil_dumpState", + "method": "kontrol_dumpState", "params": [], "id": 2 } diff --git a/src/tests/integration/test-data/input/anvil_setBalance_0.in.json b/src/tests/integration/test-data/input/kontrol_setBalance_0.in.json similarity index 92% rename from src/tests/integration/test-data/input/anvil_setBalance_0.in.json rename to src/tests/integration/test-data/input/kontrol_setBalance_0.in.json index 7f4c1bc9..3406bb74 100644 --- a/src/tests/integration/test-data/input/anvil_setBalance_0.in.json +++ b/src/tests/integration/test-data/input/kontrol_setBalance_0.in.json @@ -10,7 +10,7 @@ }, { "jsonrpc": "2.0", - "method": "anvil_setBalance", + "method": "kontrol_setBalance", "params": [ "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "0x300" diff --git a/src/tests/integration/test-data/input/anvil_setBalance_1.in.json b/src/tests/integration/test-data/input/kontrol_setBalance_1.in.json similarity index 92% rename from src/tests/integration/test-data/input/anvil_setBalance_1.in.json rename to src/tests/integration/test-data/input/kontrol_setBalance_1.in.json index 530273d1..76c4c3b9 100644 --- a/src/tests/integration/test-data/input/anvil_setBalance_1.in.json +++ b/src/tests/integration/test-data/input/kontrol_setBalance_1.in.json @@ -10,7 +10,7 @@ }, { "jsonrpc": "2.0", - "method": "anvil_setBalance", + "method": "kontrol_setBalance", "params": ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92200", "300"], "id": 0 }, diff --git a/src/tests/integration/test-data/input/debug_traceTransaction_0.in.json b/src/tests/integration/test-data/input/kontrol_traceTransaction_0.in.json similarity index 96% rename from src/tests/integration/test-data/input/debug_traceTransaction_0.in.json rename to src/tests/integration/test-data/input/kontrol_traceTransaction_0.in.json index 9f2536ad..e7532890 100644 --- a/src/tests/integration/test-data/input/debug_traceTransaction_0.in.json +++ b/src/tests/integration/test-data/input/kontrol_traceTransaction_0.in.json @@ -16,7 +16,7 @@ { "jsonrpc": "2.0", "id": 1, - "method": "debug_traceTransaction", + "method": "kontrol_traceTransaction", "params": [ "0xa2a48bf94c47073aa235c34e3dfe9bd9687e7b6a6cce28a2380ed48138f51680", { diff --git a/src/tests/integration/test-data/input/debug_traceTransaction_1.in.json b/src/tests/integration/test-data/input/kontrol_traceTransaction_1.in.json similarity index 99% rename from src/tests/integration/test-data/input/debug_traceTransaction_1.in.json rename to src/tests/integration/test-data/input/kontrol_traceTransaction_1.in.json index 6c05e3dc..a7d2c496 100644 --- a/src/tests/integration/test-data/input/debug_traceTransaction_1.in.json +++ b/src/tests/integration/test-data/input/kontrol_traceTransaction_1.in.json @@ -12,7 +12,7 @@ }, { "jsonrpc": "2.0", - "method": "anvil_setBalance", + "method": "kontrol_setBalance", "params": [ "0x5fbdb2315678afecb367f032d93f642f64180aa3", "1000000000000000000000000" @@ -45,7 +45,7 @@ }, { "jsonrpc": "2.0", - "method": "debug_traceTransaction", + "method": "kontrol_traceTransaction", "params": [ "0x4d70c2db858e9574873784eebdc824b2322d850374640b33a5d42d0bfe254a85", { diff --git a/src/tests/integration/test-data/input/debug_traceTransaction_mocks.in.json b/src/tests/integration/test-data/input/kontrol_traceTransaction_mocks.in.json similarity index 99% rename from src/tests/integration/test-data/input/debug_traceTransaction_mocks.in.json rename to src/tests/integration/test-data/input/kontrol_traceTransaction_mocks.in.json index b5804ab3..ea7290f4 100644 --- a/src/tests/integration/test-data/input/debug_traceTransaction_mocks.in.json +++ b/src/tests/integration/test-data/input/kontrol_traceTransaction_mocks.in.json @@ -12,7 +12,7 @@ }, { "jsonrpc": "2.0", - "method": "anvil_setBalance", + "method": "kontrol_setBalance", "params": [ "0x5fbdb2315678afecb367f032d93f642f64180aa3", "1000000000000000000000000" @@ -33,7 +33,7 @@ }, { "jsonrpc": "2.0", - "method": "debug_traceTransaction", + "method": "kontrol_traceTransaction", "params": [ "0xb4652dde97966f090f3a891cd28856b5e52e4d40985b96a696a3e8df49e26f2b", { diff --git a/src/tests/integration/test-data/input/debug_traceTransaction_slow.in.json b/src/tests/integration/test-data/input/kontrol_traceTransaction_slow.in.json similarity index 99% rename from src/tests/integration/test-data/input/debug_traceTransaction_slow.in.json rename to src/tests/integration/test-data/input/kontrol_traceTransaction_slow.in.json index 48127571..a4ed815a 100644 --- a/src/tests/integration/test-data/input/debug_traceTransaction_slow.in.json +++ b/src/tests/integration/test-data/input/kontrol_traceTransaction_slow.in.json @@ -12,7 +12,7 @@ }, { "jsonrpc": "2.0", - "method": "anvil_setBalance", + "method": "kontrol_setBalance", "params": [ "0x5fbdb2315678afecb367f032d93f642f64180aa3", "1000000000000000000000000" @@ -45,7 +45,7 @@ }, { "jsonrpc": "2.0", - "method": "debug_traceTransaction", + "method": "kontrol_traceTransaction", "params": [ "0x16b3ff55a2953aaaeaca1d5e4324dad6b170dec5dfd2f70dec704430bdc7fc29", { diff --git a/src/tests/integration/test-data/output/anvil_dumpState_0.expected.json b/src/tests/integration/test-data/output/kontrol_dumpState_0.expected.json similarity index 100% rename from src/tests/integration/test-data/output/anvil_dumpState_0.expected.json rename to src/tests/integration/test-data/output/kontrol_dumpState_0.expected.json diff --git a/src/tests/integration/test-data/output/anvil_dumpState_1.expected.json b/src/tests/integration/test-data/output/kontrol_dumpState_1.expected.json similarity index 100% rename from src/tests/integration/test-data/output/anvil_dumpState_1.expected.json rename to src/tests/integration/test-data/output/kontrol_dumpState_1.expected.json diff --git a/src/tests/integration/test-data/output/anvil_setBalance_0.expected.json b/src/tests/integration/test-data/output/kontrol_setBalance_0.expected.json similarity index 100% rename from src/tests/integration/test-data/output/anvil_setBalance_0.expected.json rename to src/tests/integration/test-data/output/kontrol_setBalance_0.expected.json diff --git a/src/tests/integration/test-data/output/anvil_setBalance_1.expected.json b/src/tests/integration/test-data/output/kontrol_setBalance_1.expected.json similarity index 100% rename from src/tests/integration/test-data/output/anvil_setBalance_1.expected.json rename to src/tests/integration/test-data/output/kontrol_setBalance_1.expected.json diff --git a/src/tests/integration/test-data/output/debug_traceTransaction_0.expected.json b/src/tests/integration/test-data/output/kontrol_traceTransaction_0.expected.json similarity index 100% rename from src/tests/integration/test-data/output/debug_traceTransaction_0.expected.json rename to src/tests/integration/test-data/output/kontrol_traceTransaction_0.expected.json diff --git a/src/tests/integration/test-data/output/debug_traceTransaction_1.expected.json b/src/tests/integration/test-data/output/kontrol_traceTransaction_1.expected.json similarity index 100% rename from src/tests/integration/test-data/output/debug_traceTransaction_1.expected.json rename to src/tests/integration/test-data/output/kontrol_traceTransaction_1.expected.json diff --git a/src/tests/integration/test-data/output/debug_traceTransaction_mocks.expected.json b/src/tests/integration/test-data/output/kontrol_traceTransaction_mocks.expected.json similarity index 100% rename from src/tests/integration/test-data/output/debug_traceTransaction_mocks.expected.json rename to src/tests/integration/test-data/output/kontrol_traceTransaction_mocks.expected.json diff --git a/src/tests/integration/test-data/output/debug_traceTransaction_slow.expected.json b/src/tests/integration/test-data/output/kontrol_traceTransaction_slow.expected.json similarity index 100% rename from src/tests/integration/test-data/output/debug_traceTransaction_slow.expected.json rename to src/tests/integration/test-data/output/kontrol_traceTransaction_slow.expected.json diff --git a/src/tests/integration/test-data/rpc-tests-all b/src/tests/integration/test-data/rpc-tests-all index 6f822669..b5f1a047 100644 --- a/src/tests/integration/test-data/rpc-tests-all +++ b/src/tests/integration/test-data/rpc-tests-all @@ -1,7 +1,7 @@ -anvil_dumpState_0 -anvil_dumpState_1 -anvil_setBalance_0 -anvil_setBalance_1 +kontrol_dumpState_0 +kontrol_dumpState_1 +kontrol_setBalance_0 +kontrol_setBalance_1 eth_chainId_0 eth_getBalance_0 eth_getBlockByHash_0 @@ -15,9 +15,9 @@ eth_getTransactionReceipt_0 eth_getTransactionReceipt_1 eth_sendTransaction_deploy_counter eth_sendTransaction_no_sender -debug_traceTransaction_0 -debug_traceTransaction_1 -debug_traceTransaction_slow -debug_traceTransaction_mocks +kontrol_traceTransaction_0 +kontrol_traceTransaction_1 +kontrol_traceTransaction_slow +kontrol_traceTransaction_mocks method_not_found invalid_request \ No newline at end of file diff --git a/src/tests/integration/test_rpc.py b/src/tests/integration/test_rpc.py index 55b718a9..b4b06af9 100644 --- a/src/tests/integration/test_rpc.py +++ b/src/tests/integration/test_rpc.py @@ -1,6 +1,5 @@ from __future__ import annotations -import gzip import json import os from pathlib import Path @@ -67,76 +66,6 @@ def test_rpc_file_batched( ) -@pytest.mark.skip(reason='Disabled') -@pytest.mark.parametrize('test_id', RPC_TESTS_ALL) -def test_anvil_compatibility( - test_id: str, - server: str, - anvil: str, -) -> None: - """ - Test the JSON-RPC server by comparing its responses to those of an Anvil instance for a set of predefined test cases. - """ - - if test_id in RPC_TESTS_SKIPPED: - pytest.skip() - - with open(INPUT_FILES / f'{test_id}.in.json') as test_file: - payload = json.loads(test_file.read()) - if type(payload) is dict: - payload = [payload] - if type(payload) is list: - for request in payload: - kontrol_result = execute_json_rpc(server, request) - kontrol_result = json.loads(kontrol_result) - anvil_result = execute_json_rpc(anvil, request) - anvil_result = json.loads(anvil_result) - if request['method'] == 'anvil_dumpState': - print('Kontrol Node Result') - print(json.dumps(kontrol_result['result'], indent=2, sort_keys=True)) - print('Anvil Result') - anvil_snapshot = decode_snapshot(anvil_result['result'], compressed=True) - anvil_uncompressed = encode_snapshot(anvil_snapshot, compressed=False) - anvil_result['result'] = anvil_uncompressed - print(json.dumps(anvil_snapshot, indent=2, sort_keys=True)) - assert kontrol_result == anvil_result - - -### Snapshot utilities - - -def normalize_snapshot(hex_data: str) -> str: - """ - Normalize the snashot data for comparison. - 1. The RPC method `anvil_dumpState` compresses `result` with gzip, whose - output is flaky therefore decompress the result prior to comparing with - saved output, whose result was also decompressed. - 2. Additionally, json.dumps is unstable for dictionary entries therefore - sort the encoded json text dict entries - """ - snapshot_data = decode_snapshot(hex_data, compressed=True) - normalized_hex = encode_snapshot(snapshot_data, compressed=True) - return normalized_hex - - -def decode_snapshot(hex_data: str, compressed: bool = True) -> dict: - """Decode the hex-encoded gzip-compressed snapshot data.""" - if hex_data.startswith('0x'): - hex_data = hex_data[2:] - compressed_data = bytes.fromhex(hex_data) - decompressed_data = gzip.decompress(compressed_data) if compressed else compressed_data - snapshot_data = json.loads(decompressed_data.decode('utf-8')) - return snapshot_data - - -def encode_snapshot(snapshot_data: dict, compressed: bool = True) -> str: - """Encode the snapshot data as hex-encoded gzip-compressed data.""" - json_data = json.dumps(snapshot_data, sort_keys=True).encode('utf-8') - compressed_data = gzip.compress(json_data) if compressed else json_data - hex_data = '0x' + compressed_data.hex() - return hex_data - - def assert_or_update_output(actual_text: str, expected_file: Path, *, update: bool) -> None: if update: expected_file.write_text(actual_text)