Skip to content
StrobeLabsPublic

Repository files navigation

eth.zig

CI Docs License: MIT Zig

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.

Read the docs at ethzig.org

Why eth.zig?

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.

Performance

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 subset

Quick Start

Derive an address from a private key

const 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"

Sign and send a transaction

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),
});

Read an ERC-20 token

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);

Resolve an ENS 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.

Simulate eth_call with state overrides

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);

Function selectors and event topics

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)")

HD wallet from mnemonic

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();

Resilient WebSocket subscriptions (production bots)

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.

Block-scoped log watching (keepers and searchers)

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, ... */ }
}

Built with eth.zig

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.

Installation

One-liner:

zig fetch --save git+https://github.com/StrobeLabs/eth.zig.git#v0.10.0

Or 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"));

Examples

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

Modules

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

Features

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

Requirements

  • Zig >= 0.17.0

Running Tests

zig build test                # Unit tests
zig build integration-test    # Integration tests (requires Anvil)

Benchmarks

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 comparison

See bench/README.md.

Contributing

Contributions are welcome. Please open an issue or pull request on GitHub.

Before submitting:

  1. Run zig build test and ensure all tests pass.
  2. Follow the existing code style -- comptime where possible.
  3. Add tests for any new functionality.

License

MIT -- see LICENSE for details.

Copyright 2025-2026 Strobe Labs

Releases

Packages

Used by

Contributors

Languages