Skip to content
Open
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
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,29 @@ jobs:
version: ${{ matrix.zig-version }}
- name: Run unit tests
run: zig build test
- name: Run ENS conformance vectors
- name: Run conformance vectors (ENS, KZG, go-ethereum blob tx)
run: zig build vector-test

portable-blst:
name: Portable blst backend (no assembly)
runs-on: ubuntu-latest
# blst is built with its assembly backend on x86_64 and aarch64, which is
# what both `test` runners use. The portable C backend stays the only path
# on every other target (riscv64, wasm32, aarch64_be, 32-bit), so it needs
# a job of its own or it would ship untested. It is ~7x slower, so one OS
# is enough.
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
with:
version: "0.16.0"
- name: Run unit tests against the portable backend
run: zig build test -Dblst-asm=false
- name: Build for a target that has no blst assembly
run: zig build -Dtarget=riscv64-linux-musl

fmt:
name: Format check
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ zig-pkg/
*.a
*.o
*.s
# Vendored blst assembly is source, not a build artifact
!src/crypto/blst/build/**/*.s
!src/crypto/blst/build/**/*.S

# Coverage output (make coverage / kcov)
kcov-out/
Expand Down
5 changes: 4 additions & 1 deletion 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 bench-u256 bench-keccak bench-kzg coverage docs clean

## Build the library (default)
build:
Expand Down Expand Up @@ -52,6 +52,9 @@ bench-u256:
bench-keccak:
$(ZIG) build bench-keccak -Doptimize=ReleaseFast

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

## Remove build artifacts
clean:
rm -rf zig-out zig-cache .zig-cache
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,8 @@ cd examples && zig build && ./zig-out/bin/01_derive_address
|-------|---------|-------------|
| **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 |
| **Crypto** | `secp256k1`, `signer`, `signature`, `keccak`, `eip155`, `kzg` | ECDSA signing (RFC 6979), Keccak-256, EIP-155, EIP-4844 KZG, EIP-7594 cells |
| **Types** | `transaction`, `receipt`, `block`, `blob`, `access_list` | Legacy, EIP-2930, EIP-1559, EIP-4844 transactions and blob sidecars |
| **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 |
Expand All @@ -305,7 +305,9 @@ cd examples && zig build && ./zig-out/bin/01_derive_address
| 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-4844 KZG (blob commitments/proofs, point evaluation; vendored c-kzg-4844 + blst) | Complete |
| EIP-7594 cells (cell proofs, recovery, batch verification) | Complete |
| Blob transaction network wrapper (pre-Fusaka v0 and EIP-7594 v1) | Complete |
| EIP-155 replay protection | Complete |
| EIP-191 personal message signing | Complete |
| EIP-712 typed structured data signing | Complete |
Expand Down Expand Up @@ -365,9 +367,14 @@ cd examples && zig build && ./zig-out/bin/01_derive_address

```bash
zig build test # Unit tests
zig build vector-test # Conformance vectors (ENS normalization, KZG)
zig build integration-test # Integration tests (requires Anvil)
```

The KZG vectors are the official ethereum/c-kzg-4844 cases (point evaluation
and EIP-7594 cells) plus a go-ethereum known-answer vector for the blob
transaction network encodings; see `tests/vectors/kzg/README.md`.

## Benchmarks

One command to run the full comparison (requires Zig, Rust, Python 3):
Expand All @@ -380,8 +387,13 @@ Or run individually:

```bash
zig build bench # eth.zig only
zig build bench-kzg # KZG: commitments, blob proofs, EIP-7594 cells
```

blst is built with its assembly backend on x86_64 and aarch64, which is 6-8x
faster than the portable C fallback across every KZG operation
(`-Dblst-asm=false` selects the fallback; see `docs` for the table).

## Contributing

Contributions are welcome. Please open an issue or pull request on [GitHub](https://github.com/StrobeLabs/eth.zig).
Expand Down
165 changes: 165 additions & 0 deletions bench/kzg_bench.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//! KZG micro-benchmark: times the c-kzg-4844 + blst operations behind
//! `eth.kzg` on one deterministic blob. Each operation is run a fixed number
//! of times and the minimum and median wall-clock times are reported in
//! milliseconds; the minimum is the number to compare across builds (blst
//! assembly vs portable C), the median shows run-to-run noise.
//!
//! Run: zig build bench-kzg

const std = @import("std");
const eth = @import("eth");

const kzg = eth.kzg;
const blob_mod = eth.blob;

fn nowNs() i96 {
const io = std.Io.Threaded.global_single_threaded.io();
return std.Io.Clock.now(.awake, io).nanoseconds;
}

const Sample = struct {
min_ns: u64,
median_ns: u64,
iters: usize,
};

/// Time `func(ctx)` `iters` times; returns min and median per-call latency.
fn measure(iters: usize, ctx: anytype, comptime func: anytype) !Sample {
var samples: [64]u64 = undefined;
const n = @min(iters, samples.len);
for (samples[0..n]) |*s| {
const t0 = nowNs();
try func(ctx);
const t1 = nowNs();
s.* = @intCast(t1 - t0);
}
std.mem.sort(u64, samples[0..n], {}, std.sort.asc(u64));
return .{ .min_ns = samples[0], .median_ns = samples[n / 2], .iters = n };
}

fn printRow(stdout: anytype, name: []const u8, s: Sample) !void {
const min_ms = @as(f64, @floatFromInt(s.min_ns)) / 1e6;
const med_ms = @as(f64, @floatFromInt(s.median_ns)) / 1e6;
try stdout.print("{s:<32} {d:>10.3} ms {d:>10.3} ms {d:>6}\n", .{ name, min_ms, med_ms, s.iters });
try stdout.print("BENCH_JSON|{{\"name\":\"{s}\",\"min_ns\":{d},\"median_ns\":{d},\"iters\":{d}}}\n", .{ name, s.min_ns, s.median_ns, s.iters });
}

const Fixture = struct {
blob: *const blob_mod.Blob,
commitment: blob_mod.KzgCommitment,
proof: blob_mod.KzgProof,
cells: *[kzg.CELLS_PER_EXT_BLOB]kzg.Cell,
cell_proofs: *[kzg.CELLS_PER_EXT_BLOB]blob_mod.KzgProof,
scratch_cells: *[kzg.CELLS_PER_EXT_BLOB]kzg.Cell,
scratch_proofs: *[kzg.CELLS_PER_EXT_BLOB]blob_mod.KzgProof,
};

fn opCommit(f: *const Fixture) !void {
_ = try kzg.blobToKzgCommitment(f.blob);
}

fn opProof(f: *const Fixture) !void {
_ = try kzg.computeBlobKzgProof(f.blob, f.commitment);
}

fn opVerify(f: *const Fixture) !void {
if (!try kzg.verifyBlobKzgProof(f.blob, f.commitment, f.proof)) return error.ProofDidNotVerify;
}

fn opVerifyBatch(f: *const Fixture) !void {
const blobs = [_]blob_mod.Blob{f.blob.*};
const commits = [_]blob_mod.KzgCommitment{f.commitment};
const proofs = [_]blob_mod.KzgProof{f.proof};
if (!try kzg.verifyBlobKzgProofBatch(&blobs, &commits, &proofs)) return error.ProofDidNotVerify;
}

fn opComputeCellsAndProofs(f: *const Fixture) !void {
try kzg.computeCellsAndKzgProofs(f.blob, f.scratch_cells, f.scratch_proofs);
}

fn opVerifyCells1(f: *const Fixture) !void {
const commitments = [_]blob_mod.KzgCommitment{f.commitment};
const indices = [_]u64{7};
if (!try kzg.verifyCellKzgProofBatch(&commitments, &indices, f.cells[7..8], f.cell_proofs[7..8])) return error.ProofDidNotVerify;
}

fn opVerifyCells128(f: *const Fixture) !void {
var commitments: [kzg.CELLS_PER_EXT_BLOB]blob_mod.KzgCommitment = undefined;
var indices: [kzg.CELLS_PER_EXT_BLOB]u64 = undefined;
for (0..kzg.CELLS_PER_EXT_BLOB) |i| {
commitments[i] = f.commitment;
indices[i] = @intCast(i);
}
if (!try kzg.verifyCellKzgProofBatch(&commitments, &indices, f.cells, f.cell_proofs)) return error.ProofDidNotVerify;
}

fn opRecoverHalf(f: *const Fixture) !void {
var indices: [kzg.CELLS_PER_EXT_BLOB / 2]u64 = undefined;
for (0..indices.len) |i| indices[i] = @intCast(i);
try kzg.recoverCellsAndKzgProofs(&indices, f.cells[0 .. kzg.CELLS_PER_EXT_BLOB / 2], f.scratch_cells, f.scratch_proofs);
}

pub fn main() !void {
var out_buf: [8192]u8 = undefined;
var w = std.Io.File.stdout().writerStreaming(std.Io.Threaded.global_single_threaded.io(), &out_buf);
const stdout = &w.interface;

var dbg: std.heap.DebugAllocator(.{}) = .init;
defer _ = dbg.deinit();
const allocator = dbg.allocator();

// Trusted-setup load time (one-shot; dominated by G1/G2 point parsing).
const t_init0 = nowNs();
try kzg.init(allocator);
const t_init1 = nowNs();
defer kzg.deinit();

// Deterministic pseudo-random blob with full-width field elements. Tiny
// scalars would let the Pippenger MSM skip most windows and understate the
// commitment cost. Masking the top byte to 6 bits keeps every 32-byte
// big-endian element below the BLS12-381 scalar modulus (0x73ed...).
const blob = try allocator.create(blob_mod.Blob);
defer allocator.destroy(blob);
var prng = std.Random.DefaultPrng.init(0x6b7a67);
prng.random().bytes(blob);
var i: usize = 0;
while (i < blob_mod.BLOB_SIZE) : (i += 32) {
blob[i] &= 0x3f;
}

const cells = try allocator.create([kzg.CELLS_PER_EXT_BLOB]kzg.Cell);
defer allocator.destroy(cells);
const cell_proofs = try allocator.create([kzg.CELLS_PER_EXT_BLOB]blob_mod.KzgProof);
defer allocator.destroy(cell_proofs);
const scratch_cells = try allocator.create([kzg.CELLS_PER_EXT_BLOB]kzg.Cell);
defer allocator.destroy(scratch_cells);
const scratch_proofs = try allocator.create([kzg.CELLS_PER_EXT_BLOB]blob_mod.KzgProof);
defer allocator.destroy(scratch_proofs);

var fx = Fixture{
.blob = blob,
.commitment = undefined,
.proof = undefined,
.cells = cells,
.cell_proofs = cell_proofs,
.scratch_cells = scratch_cells,
.scratch_proofs = scratch_proofs,
};
fx.commitment = try kzg.blobToKzgCommitment(blob);
fx.proof = try kzg.computeBlobKzgProof(blob, fx.commitment);
try kzg.computeCellsAndKzgProofs(blob, cells, cell_proofs);

try stdout.print("\n{s:<32} {s:>13} {s:>13} {s:>6}\n", .{ "kzg op", "min", "median", "iters" });
try stdout.print("{s}\n", .{"" ++ @as([68]u8, @splat('-'))});
try printRow(stdout, "init (trusted setup load)", .{ .min_ns = @intCast(t_init1 - t_init0), .median_ns = @intCast(t_init1 - t_init0), .iters = 1 });
try printRow(stdout, "blob_to_kzg_commitment", try measure(10, &fx, opCommit));
try printRow(stdout, "compute_blob_kzg_proof", try measure(10, &fx, opProof));
try printRow(stdout, "verify_blob_kzg_proof", try measure(30, &fx, opVerify));
try printRow(stdout, "verify_blob_kzg_proof_batch(1)", try measure(30, &fx, opVerifyBatch));
try printRow(stdout, "compute_cells_and_kzg_proofs", try measure(5, &fx, opComputeCellsAndProofs));
try printRow(stdout, "verify_cell_kzg_proof_batch(1)", try measure(30, &fx, opVerifyCells1));
try printRow(stdout, "verify_cell_kzg_proof_batch(128)", try measure(10, &fx, opVerifyCells128));
try printRow(stdout, "recover_cells_and_kzg_proofs(64)", try measure(5, &fx, opRecoverHalf));
try stdout.print("\n", .{});
try stdout.flush();
}
Loading
Loading