
Hanvil
A local Hedera network in one 10 MB binary, with a coding-agent harness and an x402 rail built in.
Demo
About
hanvil is a local Hedera network that runs as a single 10 MB binary. It serves JSON-RPC (7546), mirror node REST (5551) and HAPI gRPC (50211) from one in-memory chain, on the same ports and with the same 30 pre-funded accounts and keys as hiero-local-node, so @hiero-ledger/sdk, viem, hardhat and foundry connect without config changes. First JSON-RPC answer 6 ms after exec, 5.8 MB resident. Why: hiero-local-node is 18 containers, 3.8 GB of RAM and about 50 s to boot, and it is deprecated as of this month. Its replacement, Solo, runs on Kubernetes and wants 12 GB of RAM and 6 cores. hedera-harness, Hedera's coding-agent harness, only runs against testnet, so every agent attempt costs HBAR and a network round trip, and a failed attempt leaves state behind for the next one. What is in the binary: - hanvil: the network. evm_snapshot / evm_revert clone and restore the whole chain. --state FILE persists it across restarts. hiero-local-node supports neither. - hanvil run: a Rust port of hedera-harness. Reads a schema-v3 recipe, boots the chain in-process, runs GENERATE / ASSERT / CHAIN / SMOKE / EVALUATE with claude, cursor or any command as the agent. Snapshot before each attempt, revert after a failed one, chain assertions read from the Chain struct instead of a mirror round trip. No testnet account, no HBAR. 0.21 s per attempt with a fake agent. - hanvil toll: an x402 facilitator plus a metered service on the same chain, with payer, payTo and feePayer accounts preconfigured. A paid request settles in 0.081 s. The same service deployed on Railway against testnet settles in 4.91 s. hanvil_rejections: transactions Hedera refuses at precheck (duplicate transaction id, bad signature, expired valid-start window, and so on) never reach consensus, so on a real network they produce no record, no receipt and no mirror row. hanvil is the node, so it keeps them and exposes them over JSON-RPC as hanvil_rejections. The demo replays an x402 payment header and reads the DUPLICATE_TRANSACTION refusal back from that list. Recipes can also assert on decoded contract events and advance the chain clock between assertion sets. Numbers, same M-series Mac, hanvil vs hiero-local-node: - boot to first JSON-RPC answer: 6 ms vs 50.6 s - RSS: 5.8 MB vs 3,898 MB - on disk: 10.1 MB vs 9.4 GB - processes: 1 vs 18 225 tests. The command behind each number is in the README. Upstream: PRs #47 and #48 to hedera-dev/hedera-harness add network: "local" (run CHAIN against a local node) and per-attempt chain snapshots. Both are open. The demo runs the upstream TypeScript harness with those two PRs applied, green on hanvil. Not emulated: consensus, gossip, staking, the real fee schedule (flat 10,000 tinybar per HAPI transaction, 71 tinybar per unit of EVM gas), and HAPI bodies beyond the six implemented (the rest return NOT_SUPPORTED). The full list of 35 declared gaps is in the README under "What is emulated, and what is not".
How it's made
Rust. One Chain struct behind a RwLock. The JSON-RPC, mirror and gRPC listeners, hanvil run and hanvil toll all take the same lock, so a transfer submitted over JSON-RPC is visible from the mirror endpoints immediately. evm_snapshot clones the struct, evm_revert swaps it back. Nothing holds the lock across an .await. Crates: revm (EVM), alloy (ABI decoding, sender recovery), axum (the HTTP listeners), tonic + prost (HAPI gRPC over 130 vendored Hedera .proto files), clap, serde_yaml_ng (recipes), regex (secret scan). The node makes no outbound network calls. Hedera details that took the most work: - The EVM runs in tinybar, not wei. eth_getBalance and eth_gasPrice multiply by 10^10 at the JSON-RPC boundary. A value that is not a whole number of tinybar is refused with the relay's own error, -32005. - HAPI prechecks run in the order a real node runs them: node account, transaction id, valid duration, valid start, duplicate id, payer, supported body, signatures, payer balance. Signatures are verified for real: ECDSA secp256k1 over keccak256(bodyBytes), ED25519 over bodyBytes. Error codes map 1:1 to ResponseCodeEnum. - A precheck failure leaves no record and no receipt, same as Hedera, but is appended to an in-memory list served as hanvil_rejections. - Ports, account ids, EVM addresses and private keys are copied from hiero-local-node so existing configs and the SDK's forLocalNode() keep working. x402: on Hedera an x402 payment is a CryptoTransfer the client signs and the facilitator co-signs and submits as fee payer. hanvil toll implements the facilitator (:4020) and a metered service (:4021) against the in-process chain: 100000 tinybar per call, payer 0.0.1002, payTo 0.0.1003, feePayer 0.0.1004. The same code, pointed at testnet, is deployed on Railway (examples/toll, Dockerfile in the repo). The demo makes one paid request there and shows the receipt on HashScan. Harness: hanvil run reimplements hedera-harness's five stages. GENERATE spawns the agent CLI with the PRD as its prompt. ASSERT checks required and forbidden files, runs the static validators and the secret scan. CHAIN runs the recipe's deploy commands, optionally advances the chain clock, then evaluates the assertions against the Chain struct. SMOKE starts the app's dev server and drives it with @playwright/mcp in headless Chromium. EVALUATE runs a validator agent with the same MCP server. Subprocesses run in their own process groups so Ctrl-C kills the agent, the dev server and the browser together. Every attempt dumps chain state to logs/chain-state-attempt-N.json, and hanvil --state <file> boots from it. The signer's private key is written as <redacted by hanvil> in every prompt file. Process: solo, first commit 2026-09-07, written with Claude Code. The rules the code was held to are in .claude/CLAUDE.md (one Chain, one lock; ids as newtypes; no unwrap under src/; clippy -D warnings) and a pre-commit hook enforces them. Every commit carries a Co-Authored-By: Claude trailer. docs/research.md pins each claim about upstream behaviour to a file and line. CI runs cargo test on ubuntu and macos, runs the upstream TypeScript harness's CHAIN tier against hanvil with no secrets, and fails if median boot goes over 100 ms.