Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: Bench

# Benchmarks run nightly (and on demand), off the PR-blocking path: the harness
# is noisy on shared runners, so we capture numbers for trend/regression review
# rather than gate merges on them. Each run uploads the BENCH_JSON lines so a
# committed baseline + diff can be layered on later.
# rather than gate merges on them. Each run uploads the suite's JSON results;
# compare releases on one machine with bench/ab.py (see bench/README.md).
on:
schedule:
- cron: "0 7 * * *" # daily at 07:00 UTC
Expand All @@ -26,13 +26,9 @@ jobs:
with:
version: "0.17.0"
- name: Run benchmarks
run: zig build bench | tee bench-results.txt
- name: Extract machine-readable results
run: grep '^BENCH_JSON' bench-results.txt > bench-results.jsonl || true
run: zig build bench -- --save bench-results.json
- name: Upload benchmark results
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: bench-results
path: |
bench-results.txt
bench-results.jsonl
path: bench-results.json
12 changes: 3 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ZIG ?= zig
KCOV ?= kcov

.PHONY: build test fmt fmt-fix lint ci integration-test bench bench-u256 bench-keccak coverage docs clean
.PHONY: build test fmt fmt-fix lint ci integration-test bench coverage docs clean

## Build the library (default)
build:
Expand Down Expand Up @@ -42,15 +42,9 @@ ci: build fmt test
integration-test:
$(ZIG) build integration-test

## Run all benchmarks (ReleaseFast)
## Run the benchmark suite (always optimize=fast; see bench/README.md)
bench:
$(ZIG) build bench -Doptimize=ReleaseFast

bench-u256:
$(ZIG) build bench-u256 -Doptimize=ReleaseFast

bench-keccak:
$(ZIG) build bench-keccak -Doptimize=ReleaseFast
$(ZIG) build bench

## Remove build artifacts
clean:
Expand Down
75 changes: 14 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,28 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Zig](https://img.shields.io/badge/Zig-%E2%89%A5%200.17.0-orange)](https://ziglang.org/)

**The fastest Ethereum library.** Beats Rust's alloy.rs on 18 out of 26 benchmarks.
**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](https://ethzig.org)**

## Why eth.zig?

**Fastest Ethereum library** -- eth.zig [beats alloy.rs](bench/RESULTS.md) (Rust's leading Ethereum library, backed by Paradigm) on **18 out of 26 benchmarks**, measured 2026-06-10 against current alloy 1.6/2.0. RLP transaction encoding 24.33x faster, ABI decoding up to 8.41x, secp256k1 recovery 4.23x, ECDSA signing 2.34x, u256 division 3.43x, mulDiv 1.82x. See the [full results](bench/RESULTS.md).
**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](#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 vs alloy.rs

eth.zig wins **18/26 benchmarks** against [alloy.rs](https://alloy.rs) (alloy-primitives 1.6.0, alloy-consensus 2.0.5; run 2026-06-10). Measured on Apple Silicon, `ReleaseFast` (Zig) vs `--release` (Rust). Criterion-style harness with 0.5s warmup and 2s measurement.

| Operation | eth.zig | alloy.rs | Winner |
|-----------|---------|----------|--------|
| secp256k1 sign | 22,033 ns | 51,490 ns | **zig 2.34x** |
| secp256k1 sign+recover | 52,095 ns | 220,150 ns | **zig 4.23x** |
| Keccak-256 (32B) | 263 ns | 319 ns | **zig 1.21x** |
| Keccak-256 (4KB) | 7,732 ns | 7,838 ns | **zig 1.01x** |
| ABI encode (static) | 25 ns | 97 ns | **zig 3.88x** |
| ABI encode (dynamic) | 171 ns | 337 ns | **zig 1.97x** |
| ABI decode (uint256) | 14 ns | 51 ns | **zig 3.64x** |
| ABI decode (dynamic) | 32 ns | 269 ns | **zig 8.41x** |
| RLP encode (EIP-1559 tx) | 3 ns | 73 ns | **zig 24.33x** |
| u256 mulDiv (512-bit) | 17 ns | 31 ns | **zig 1.82x** |
| u256 division | 7 ns | 24 ns | **zig 3.43x** |
| u256 multiply | 5 ns | 10 ns | **zig 2.00x** |
| UniswapV2 getAmountOut | 24 ns | 24 ns | tie |
| UniswapV4 swap | 45 ns | 42 ns | rs 1.07x |
| TX hash (EIP-1559) | 271 ns | 361 ns | **zig 1.33x** |

alloy.rs wins on address hex parsing (1.55x -- SIMD), hex encoding (1.08x), UniswapV4 swap (1.07x), and Keccak at 256B/1KB inputs (within ~2%, measurement noise). See [full results](bench/RESULTS.md).
## 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](bench/README.md) for the method and the latest results against previous releases.

```bash
zig build bench # run the suite
zig build bench -- --filter keccak # run a subset
```

## Quick Start

Expand Down Expand Up @@ -329,34 +314,6 @@ cd examples && zig build && ./zig-out/bin/01_derive_address
| Provider middleware (retry, caching) | Planned |
| Hardware wallet signers | Planned |

## Comparison with Other Libraries

### Performance vs alloy.rs (Rust)

| Category | eth.zig | alloy.rs |
|----------|---------|----------|
| Benchmarks won | **18/26** | 5/26 |
| secp256k1 signing | Faster (2.34-4.23x) | -- |
| ABI encoding/decoding | Faster (1.96-8.41x) | -- |
| Hashing (Keccak) | Faster on small inputs (1.19-1.21x) | Within ~2% on 256B-1KB |
| u256 arithmetic | Faster on mul/div/mulDiv (1.82-3.43x) | UniswapV4 swap (1.07x) |
| Hex operations | Faster decoding (1.17x) | Faster encoding (1.08x) and parsing (1.55x, SIMD) |

### Features vs Zabi (Zig)

| Feature | eth.zig | Zabi |
|---------|---------|------|
| Comptime selectors | Yes | No |
| secp256k1 ECDSA | Yes (libsecp256k1 + Zig fallback) | Yes (C binding) |
| ABI encode/decode | Yes | Yes |
| HD wallets (BIP-32/39/44) | Yes | Yes |
| ERC-20/721 wrappers | Yes | No |
| JSON ABI parsing | Yes | Yes |
| WebSocket transport | Yes | Yes |
| ENS resolution | Yes | Yes |
| EIP-712 typed data | Yes | Yes |
| Multicall3 | Yes | No |

## Requirements

- Zig >= 0.17.0
Expand All @@ -370,17 +327,13 @@ zig build integration-test # Integration tests (requires Anvil)

## Benchmarks

One command to run the full comparison (requires Zig, Rust, Python 3):

```bash
bash bench/compare.sh
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
```

Or run individually:

```bash
zig build bench # eth.zig only
```
See [bench/README.md](bench/README.md).

## Contributing

Expand Down
20 changes: 0 additions & 20 deletions bench/BTC_SOL.md

This file was deleted.

104 changes: 104 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Benchmarks

eth.zig benchmarks itself, release over release. The suite is ten operations
that dominate the hot loop of a trading bot, liquidator or searcher; anything
not on that list is either covered by one of them or too small to measure
reliably.

| Benchmark | What it measures |
|---|---|
| `keccak256_32b` | selector, storage-slot and mapping-key hashing |
| `keccak256_1kb` | calldata- and transaction-sized hashing |
| `secp256k1_sign` | signing a transaction hash |
| `secp256k1_recover` | recovering a sender from a signature (recover only) |
| `tx_hash_eip1559` | RLP-encoding an EIP-1559 transaction and hashing it |
| `abi_encode_transfer` | encoding `transfer(address,uint256)` calldata |
| `abi_decode_dynamic` | decoding `(string,bytes)` return data |
| `u256_mulDiv` | full-precision `a*b/c` with a 512-bit intermediate |
| `u256_uniswapv4_swap` | one exact-in swap-step price update |
| `dex_decode_universal_router` | decoding a real 4-command Universal Router `execute` |

## Running

```bash
zig build bench # all ten
zig build bench -- --filter abi # a subset (substring match)
zig build bench -- --list # names and descriptions
zig build bench -- --json # machine-readable results
zig build bench -- --save results.json # write results to a file
zig build bench -- --baseline results.json # compare with a saved run
zig build bench -- --baseline results.json --fail-on-regression
```

The benchmark build is always `optimize=fast`, whatever `-Doptimize` says.

## How it measures

- Each benchmark is warmed up for 200 ms, then timed in batches sized to about
10 ms so clock overhead is negligible.
- The figure is the **median ns/op over 31 batches** (`--samples`), reported to
hundredths of a nanosecond, with the minimum and the median absolute
deviation (as a percentage of the median) alongside.
- Inputs live in module-level variables and are read through an optimizer
barrier on every call, so the compiler cannot constant-fold the work away.
(The previous harness let several sub-10 ns benchmarks fold to a near no-op,
and reported whole nanoseconds only.)
- On macOS the process asks for the user-interactive QoS class so it stays on
performance cores.
- `--baseline` flags a regression only when the median is slower than the
baseline by more than `--threshold` percent (default 5) **and** by more than
twice the two runs' combined deviation.

## Comparing against a past version

A saved baseline is only meaningful on the same machine in the same state. The
reliable way to compare two versions is to build both and interleave their
runs with `bench/ab.py`:

```bash
# in a checkout of each ref (the harness exists from v0.10.1 on)
zig build bench-install --prefix /tmp/old # -> /tmp/old/bin/bench
zig build bench-install --prefix /tmp/new

python3 bench/ab.py --rounds 7 /tmp/old/bin/bench /tmp/new/bin/bench
```

`ab.py` runs every binary once per round in a shuffled order and keeps each
benchmark's fastest per-run median. Interference (another process, a run moved
to efficiency cores) only ever makes a run slower, so the best round is the
closest estimate of what the code can do.

On a laptop doing other work, results are often bimodal: a run lands either
at full speed or roughly 1.7x slower. Use enough rounds that every binary gets
at least one fast round, and treat single-run absolute numbers with suspicion.
The nightly `Bench` workflow on a Linux runner uploads `bench-results.json`
for a longer-term trend.

## Latest results

Apple M4, macOS, 2026-10-10. `ab.py`, 7 interleaved rounds, fastest per-run
median. The baseline is v0.9.2 built with Zig 0.16.0 using this harness; the
candidate is this release built with Zig 0.17.0.

| Benchmark | v0.9.2 (Zig 0.16) | this release (Zig 0.17) | change |
|---|---:|---:|---:|
| `keccak256_32b` | 136.66 ns | 136.55 ns | -0.1% |
| `keccak256_1kb` | 1043.85 ns | 1047.39 ns | +0.3% |
| `secp256k1_sign` | 11421.14 ns | 11390.99 ns | -0.3% |
| `secp256k1_recover` | 15159.87 ns | 15158.16 ns | 0.0% |
| `tx_hash_eip1559` | 142.99 ns | 143.49 ns | +0.3% |
| `abi_encode_transfer` | 18.77 ns | 18.74 ns | -0.2% |
| `abi_decode_dynamic` | 15.77 ns | 16.95 ns | +7.5% |
| `u256_mulDiv` | 12.79 ns | 13.00 ns | +1.6% |
| `u256_uniswapv4_swap` | 24.09 ns | 21.94 ns | -8.9% |
| `dex_decode_universal_router` | (new in v0.10.0) | 154.04 ns | |

### Zig 0.17 notes

Zig 0.17 ships LLVM 22 with loop vectorization disabled to work around an LLVM
regression. Byte-at-a-time loops that 0.16 used to vectorize silently got
slower, which is what pushed `abi_decode_dynamic` 50% and Universal Router
decoding 2.3x slower right after the upgrade. The fixes read 8 bytes at a time
instead (offset/length words, zero-padding checks) and write RLP integers with
one big-endian store. The remaining `abi_decode_dynamic` gap is in code
generation for the decode loop itself, not in eth.zig's logic.
84 changes: 0 additions & 84 deletions bench/RESULTS.md

This file was deleted.

Loading
Loading