Motivation
eth.zig is measurably the fastest Ethereum library on most of the surface we benchmark (18/26 vs alloy.rs), but today that speed is only reachable from Zig. Zig is a small pool; the people who would benefit most from a 24x faster RLP encoder or a 4.23x faster sign+recover are the Python, Node, and Go teams running signing and decoding in hot loops, and they will never write Zig.
A stable C ABI turns eth.zig from "a Zig library" into "the fast native core other ecosystems bind to" -- the same path that made libsecp256k1, blst, and rust cryptography ubiquitous. This is the single highest-leverage adoption change available to the project, and Zig's C interop makes it nearly free: export fn plus a hand-written header.
Scope
A new src/c_api.zig plus a generated include/eth.h, exposing a deliberately small, allocation-free-where-possible surface:
- Hashing / primitives:
eth_keccak256(const uint8_t *data, size_t len, uint8_t out[32]), eth_address_from_pubkey, eth_address_checksum(const uint8_t addr[20], char out[43])
- secp256k1:
eth_sign(const uint8_t key[32], const uint8_t hash[32], uint8_t out_sig[65]), eth_recover(const uint8_t sig[65], const uint8_t hash[32], uint8_t out_addr[20])
- Transactions:
eth_tx_sign(...) over a C struct mirroring transaction.Eip1559Transaction, returning the signed RLP into a caller-provided buffer plus a written length; eth_tx_hash(...)
- ABI:
eth_abi_selector(const char *signature, uint8_t out[4]), and encode/decode entry points over a caller-provided buffer
- RLP:
eth_rlp_encode / eth_rlp_decode over caller buffers
Design rules that keep this maintainable:
- Caller-allocates. Every function writes into a caller-provided buffer and returns a written length or a negative error code. No
eth_free, no ownership questions across the FFI boundary, no allocator smuggled through an opaque handle. Provide eth_*_max_len helpers so callers can size buffers.
- A single flat error enum (
typedef enum { ETH_OK = 0, ETH_ERR_BUFFER_TOO_SMALL = -1, ... }) mapped from Zig error sets in one place, so the mapping is auditable.
- Network calls stay out of v1. Signing, hashing, encoding, decoding only -- the pure-compute paths where we actually win. Transports drag in
std.Io, TLS, and lifetime questions that would triple the surface for a fraction of the value. Bindings can do their own HTTP.
- No breaking changes without a soname bump. The header is a public contract from the day someone links it.
Deliverables:
zig build c-lib producing libethzig.a and libethzig.so/.dylib for the host target, plus include/eth.h
- A round-trip test suite driven from C (compiled in CI) asserting the C surface matches the Zig one on the same vectors
- A
examples/ffi/ directory with a minimal C program and one Python ctypes script that signs a transaction, so the README can show the payoff in ten lines
- CI job cross-compiling the shared library for linux-x86_64, linux-aarch64, macos-aarch64
Follow-ups (deliberately not in this issue)
Published bindings packages (PyPI / npm) belong in their own repos and their own issues once the ABI is stable. Same for a cgo wrapper.
Pointers
src/root.zig is the module map; the functions worth exporting first are the ones in bench/RESULTS.md where we win by the widest margin. build.zig currently has no b.option calls and unconditionally sets link_libc = true, so adding a library artifact is additive. Keep src/c_api.zig free of any logic -- it should be a pure translation layer over the existing modules, so the Zig API stays the source of truth.
Motivation
eth.zig is measurably the fastest Ethereum library on most of the surface we benchmark (18/26 vs alloy.rs), but today that speed is only reachable from Zig. Zig is a small pool; the people who would benefit most from a 24x faster RLP encoder or a 4.23x faster
sign+recoverare the Python, Node, and Go teams running signing and decoding in hot loops, and they will never write Zig.A stable C ABI turns eth.zig from "a Zig library" into "the fast native core other ecosystems bind to" -- the same path that made libsecp256k1, blst, and rust
cryptographyubiquitous. This is the single highest-leverage adoption change available to the project, and Zig's C interop makes it nearly free:export fnplus a hand-written header.Scope
A new
src/c_api.zigplus a generatedinclude/eth.h, exposing a deliberately small, allocation-free-where-possible surface:eth_keccak256(const uint8_t *data, size_t len, uint8_t out[32]),eth_address_from_pubkey,eth_address_checksum(const uint8_t addr[20], char out[43])eth_sign(const uint8_t key[32], const uint8_t hash[32], uint8_t out_sig[65]),eth_recover(const uint8_t sig[65], const uint8_t hash[32], uint8_t out_addr[20])eth_tx_sign(...)over a C struct mirroringtransaction.Eip1559Transaction, returning the signed RLP into a caller-provided buffer plus a written length;eth_tx_hash(...)eth_abi_selector(const char *signature, uint8_t out[4]), and encode/decode entry points over a caller-provided buffereth_rlp_encode/eth_rlp_decodeover caller buffersDesign rules that keep this maintainable:
eth_free, no ownership questions across the FFI boundary, no allocator smuggled through an opaque handle. Provideeth_*_max_lenhelpers so callers can size buffers.typedef enum { ETH_OK = 0, ETH_ERR_BUFFER_TOO_SMALL = -1, ... }) mapped from Zig error sets in one place, so the mapping is auditable.std.Io, TLS, and lifetime questions that would triple the surface for a fraction of the value. Bindings can do their own HTTP.Deliverables:
zig build c-libproducinglibethzig.aandlibethzig.so/.dylibfor the host target, plusinclude/eth.hexamples/ffi/directory with a minimal C program and one Pythonctypesscript that signs a transaction, so the README can show the payoff in ten linesFollow-ups (deliberately not in this issue)
Published bindings packages (PyPI / npm) belong in their own repos and their own issues once the ABI is stable. Same for a
cgowrapper.Pointers
src/root.zigis the module map; the functions worth exporting first are the ones inbench/RESULTS.mdwhere we win by the widest margin.build.zigcurrently has nob.optioncalls and unconditionally setslink_libc = true, so adding a library artifact is additive. Keepsrc/c_api.zigfree of any logic -- it should be a pure translation layer over the existing modules, so the Zig API stays the source of truth.