The fastest Ethereum library.
A complete Ethereum client library written in Zig -- ABI encoding, RLP serialization, secp256k1 signing, Keccak-256 hashing, HD wallets, ERC-20/721 tokens, JSON-RPC, ENS, and more.
Fast, and kept fast -- vendored bitcoin-core secp256k1, XKCP Keccak, limb-native u256 math and allocation-light ABI/RLP codecs. Ten hot-path benchmarks are tracked release over release against eth.zig's own past versions; see Performance.
Comptime-first -- Function selectors and event topics are computed at compile time with zero runtime cost. The compiler does the hashing so your program doesn't have to.
Complete -- ABI, RLP, secp256k1, Keccak-256, BIP-32/39/44 HD wallets, EIP-712, JSON-RPC, WebSocket, ENS, ERC-20/721 -- everything you need for Ethereum in one package.
The benchmark suite covers the ten operations that dominate a trading bot, liquidator or searcher hot loop: Keccak-256 (32 B and 1 KB), secp256k1 sign and recover, EIP-1559 transaction hashing, ABI encode and decode, u256 mulDiv, a Uniswap V4 swap step, and Universal Router calldata decoding. Each run reports the median ns/op over 31 batches, and versions are compared by interleaving their runs so machine noise cancels out. See bench/README.md for the method and the latest results against previous releases.
zig build bench # run the suite
zig build bench -- --filter keccak # run a subsetconst eth = @import("eth");
const private_key = try eth.hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
const signer = eth.signer.Signer.init(private_key);
const addr = try signer.address();
const checksum = eth.primitives.addressToChecksum(&addr);
// "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"const eth = @import("eth");
var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com");
defer transport.deinit();
var provider = eth.provider.Provider.init(allocator, &transport);
var wallet = eth.wallet.Wallet.initLocal(allocator, private_key, &provider);
const tx_hash = try wallet.sendTransaction(.{
.to = recipient_address,
.value = eth.units.parseEther(1.0),
});const eth = @import("eth");
// Comptime selectors -- zero runtime cost
const balance_sel = eth.erc20.selectors.balanceOf;
// Or use the typed wrapper
var token = eth.erc20.ERC20.init(allocator, token_addr, &provider);
const balance = try token.balanceOf(holder_addr);
const name = try token.name();
defer allocator.free(name);Forward resolution goes through the ENSIP-10 Universal Resolver (0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe on mainnet and Sepolia), which also handles wildcard/CCIP-Read (EIP-3668) names automatically for the parts of the flow that resolve on-chain:
const eth = @import("eth");
var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com", eth.runtime.blockingIo());
defer transport.deinit();
var provider = eth.provider.Provider.init(allocator, &transport);
// Resolve vitalik.eth -> 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
const addr = try eth.ens_resolver.resolve(allocator, &provider, "vitalik.eth");Names are normalized per ENSIP-15 before resolution; names that fail normalization are rejected and MUST be treated as invalid for payment or identity use. If the name requires an off-chain CCIP-Read gateway round-trip that resolve does not perform, it returns error.OffchainLookupRequired rather than silently returning a wrong or stale answer.
Lets searchers answer "what if?" questions without forking a node: what if this token balance were larger, what if this storage slot held a different value, what if this address ran alternative bytecode? Maps to the geth-style third argument to eth_call.
const eth = @import("eth");
var overrides = eth.state_overrides.StateOverrides.init(allocator);
defer overrides.deinit();
// Pretend this account is rich, has nonce 0, and the pool has a hand-tuned reserve.
try overrides.setBalance(searcher_addr, 1_000_000 * std.math.pow(u256, 10, 18));
try overrides.setNonce(searcher_addr, 0);
try overrides.setStorageAt(pool_addr, reserves_slot, new_reserves_word);
const result = try provider.callWithOverrides(target_contract, calldata, &overrides);
defer allocator.free(result);const eth = @import("eth");
// Computed at compile time -- zero runtime cost
const transfer_sel = comptime eth.keccak.selector("transfer(address,uint256)");
// transfer_sel == [4]u8{ 0xa9, 0x05, 0x9c, 0xbb }
// Same function works at runtime too
const runtime_sel = eth.keccak.selector(runtime_signature);
const transfer_topic = comptime eth.keccak.hash("Transfer(address,address,uint256)");
// transfer_topic == keccak256("Transfer(address,address,uint256)")const eth = @import("eth");
const words = [_][]const u8{
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "about",
};
const seed = try eth.mnemonic.toSeed(&words, "");
const key = try eth.hd_wallet.deriveEthAccount(seed, 0);
const addr = key.toAddress();ws_client.WsClient wraps ws_transport with three features bots need: transparent reconnect with exponential backoff + jitter, multiplexed subscriptions on a single socket, and an application-layer ping keepalive. Subscription handles stay valid across reconnects -- the underlying server-side sub-id is swapped automatically.
const eth = @import("eth");
const client = try eth.ws_client.WsClient.connect(allocator, "wss://mainnet.example.com/ws", .{});
defer client.deinit();
const heads = try client.subscribe(.{ .new_heads = {} });
const transfers = try client.subscribe(.{ .logs = .{
.address = usdc_address,
.topics = &.{transfer_event_topic},
} });
// MEV searchers: stream full pending transactions (geth-style).
const pending = try client.subscribe(.{ .new_pending_transactions = .{ .full = true } });
while (true) {
const event = try client.next();
defer allocator.free(event.payload);
if (event.sub == heads) {
const header = try eth.subscription.parseBlockFromNotification(allocator, event.payload);
// ... handle new block
} else if (event.sub == transfers) {
const log = try eth.subscription.parseLogFromNotification(allocator, event.payload);
// ... handle Transfer log
} else if (event.sub == pending) {
const tx = try eth.subscription.parseTransactionFromNotification(allocator, event.payload);
defer eth.rpc_transaction.freeRpcTransaction(allocator, tx);
// ... evaluate the pending tx (sandwich, backrun, ...)
}
}Lower-level building blocks remain available: ws_transport.WsTransport for raw frames and ws_transport.connectWithReconnect() for a callback-style reconnect loop without subscription state.
log_watcher.LogWatcher packages the canonical bot loop -- on each new head, fetch filtered logs for that block -- on top of WsClient + eth_getLogs. Blocks missed across reconnects are back-filled and reorged ranges are re-fetched automatically:
var watcher = try eth.log_watcher.LogWatcher.init(allocator, &provider, client, .{
.address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
}, .{});
defer watcher.deinit();
while (true) {
const logs = try watcher.pollOnce(); // blocks until the next head
defer eth.log_watcher.freeLogs(allocator, logs);
for (logs) |log| { /* liquidate, arb, ... */ }
}Real production bots and SDKs built on eth.zig:
| Project | Description | Language |
|---|---|---|
| perpcity-zig-sdk | High-performance SDK for the PerpCity perpetual futures protocol on Base. Comptime ABI encoding, lock-free nonce management, 2-tier price cache. | Zig |
| gator-liquidators | Production liquidation keeper for PerpCity. Batch-checks positions via Multicall3, pre-signs liquidation txs, submits to Base sequencer. | Zig |
eth.zig
└── perpcity-zig-sdk (protocol SDK with comptime contract ABIs)
└── gator-liquidators (production liquidation bot on Base)
Built something with eth.zig? Open a PR to add it here.
One-liner:
zig fetch --save git+https://github.com/StrobeLabs/eth.zig.git#v0.10.0Or add manually to your build.zig.zon:
.dependencies = .{
.eth = .{
.url = "git+https://github.com/StrobeLabs/eth.zig.git#v0.10.0",
.hash = "...", // run `zig build` and it will tell you the expected hash
},
},Then import in your build.zig:
const eth_dep = b.dependency("eth", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("eth", eth_dep.module("eth"));The examples/ directory contains self-contained programs demonstrating each major feature:
| Example | Description | Requires RPC |
|---|---|---|
01_derive_address |
Derive address from private key | No |
02_check_balance |
Query ETH balance via JSON-RPC | Yes |
03_sign_message |
EIP-191 personal message signing | No |
04_send_transaction |
Send ETH with Wallet | Yes (Anvil) |
05_read_erc20 |
ERC-20 module API showcase | Yes |
06_hd_wallet |
BIP-44 HD wallet derivation | No |
07_comptime_selectors |
Comptime function selectors | No |
08_mev_share_backrunner |
MEV-Share backrunner bot (SSE stream + bundle) | No (dry-run) |
Run any example:
cd examples && zig build && ./zig-out/bin/01_derive_address| Layer | Modules | Description |
|---|---|---|
| Primitives | primitives, uint256, hex |
Address, Hash, Bytes32, u256, hex encoding |
| Encoding | rlp, abi_encode, abi_decode, abi_types |
RLP and ABI encoding/decoding |
| Crypto | secp256k1, signer, signature, keccak, eip155, kzg |
ECDSA signing (RFC 6979), Keccak-256, EIP-155, EIP-4844 KZG |
| Types | transaction, receipt, block, blob, access_list |
Legacy, EIP-2930, EIP-1559, EIP-4844 transactions |
| Accounts | mnemonic, hd_wallet |
BIP-32/39/44 HD wallets and mnemonic generation |
| Transport | http_transport, ws_transport, sse_transport, json_rpc, provider, subscription, ws_client |
HTTP, WebSocket, and SSE transports; resilient WS client with auto-reconnect |
| ENS | ens_namehash, ens_resolver, ens_reverse, ens_normalize, ens_contenthash |
ENSIP-15 normalization, Universal Resolver forward/reverse resolution, contenthash decoding |
| Client | wallet, contract, multicall, event, erc20, erc721 |
Signing wallet, contract interaction, Multicall3, token wrappers |
| Standards | eip712, abi_json |
EIP-712 typed data signing, Solidity JSON ABI parsing |
| Chains | chains |
Ethereum, Arbitrum, Optimism, Base, Polygon definitions |
| Feature | Status |
|---|---|
| Primitives (Address, Hash, u256) | Complete |
| RLP encoding/decoding | Complete |
| ABI encoding/decoding (all Solidity types) | Complete |
| Keccak-256 hashing | Complete |
| secp256k1 ECDSA signing (RFC 6979, EIP-2 low-S) | Complete |
| Transaction types (Legacy, EIP-2930, EIP-1559, EIP-4844) | Complete |
| EIP-4844 KZG (blob commitments/proofs, vendored c-kzg-4844 + blst) | Complete |
| EIP-155 replay protection | Complete |
| EIP-191 personal message signing | Complete |
| EIP-712 typed structured data signing | Complete |
| EIP-55 address checksums | Complete |
| BIP-32/39/44 HD wallets | Complete |
| HTTP transport | Complete |
| WebSocket transport (with TLS) | Complete |
| Resilient WS client (auto-reconnect + resubscribe + keepalive) | Complete |
| JSON-RPC provider (24+ methods) | Complete |
| ENS resolution (forward + reverse) | Complete |
| Contract read/write helpers | Complete |
| Multicall3 batch calls | Complete |
| Event log decoding and filtering | Complete |
| Chain definitions (5 networks) | Complete |
| Unit conversions (Wei/Gwei/Ether) | Complete |
| ERC-20 typed wrapper | Complete |
| ERC-721 typed wrapper | Complete |
| JSON ABI parsing | Complete |
| EIP-7702 transactions | Planned |
| IPC transport | Planned |
| Provider middleware (retry, caching) | Planned |
| Hardware wallet signers | Planned |
- Zig >= 0.17.0
zig build test # Unit tests
zig build integration-test # Integration tests (requires Anvil)zig build bench # the ten tracked benchmarks
zig build bench -- --list # what each one measures
python3 bench/ab.py OLD_BENCH_BIN NEW_BENCH_BIN # interleaved version comparisonSee bench/README.md.
Contributions are welcome. Please open an issue or pull request on GitHub.
Before submitting:
- Run
zig build testand ensure all tests pass. - Follow the existing code style -- comptime where possible.
- Add tests for any new functionality.
MIT -- see LICENSE for details.
Copyright 2025-2026 Strobe Labs