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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
zig-version: ["0.15.2", "master"]
zig-version: ["0.16.0", "master"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: "0.15.2"
version: "0.16.0"
- name: Check formatting
run: zig fmt --check src/ tests/

Expand All @@ -44,6 +44,6 @@ jobs:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: "0.15.2"
version: "0.16.0"
- name: Build library
run: zig build
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Minimum supported Zig version is now 0.16.0. Transports (HTTP, WebSocket, SSE), retry backoff, and receipt polling were migrated to the new `std.Io` interface; the library constructs a default blocking `std.Io` internally, so public signatures are unchanged. This is a breaking change for Zig 0.15 users
- New `eth.runtime` module exposes the library's default `std.Io` (`defaultIo()`) plus `milliTimestamp()` and `sleepMs()` helpers replacing the removed `std.time.milliTimestamp` and `std.Thread.sleep`

### Added
- `log_watcher`: block-scoped log watching (#36). `LogWatcher.pollOnce()` drives per-block `eth_getLogs` from a `newHeads` subscription, back-fills blocks missed across reconnects, and re-fetches reorged ranges on parent-hash mismatch; `watchLogs` offers a callback loop
- `provider.parseBlockHeaderObject`: parse a `BlockHeader` from a bare JSON object
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thanks for your interest in contributing to eth.zig! This document covers everyt

## Prerequisites

- [Zig >= 0.15.2](https://ziglang.org/download/)
- [Zig >= 0.16.0](https://ziglang.org/download/)
- Git

## Getting Started
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/strobelabs/eth.zig/actions/workflows/ci.yml/badge.svg)](https://github.com/strobelabs/eth.zig/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-ethzig.org-blue)](https://ethzig.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Zig](https://img.shields.io/badge/Zig-%E2%89%A5%200.15.2-orange)](https://ziglang.org/)
[![Zig](https://img.shields.io/badge/Zig-%E2%89%A5%200.16.0-orange)](https://ziglang.org/)

**The fastest Ethereum library.** Beats Rust's alloy.rs on 23 out of 26 benchmarks.

Expand Down Expand Up @@ -335,7 +335,7 @@ cd examples && zig build && ./zig-out/bin/01_derive_address

## Requirements

- Zig >= 0.15.2
- Zig >= 0.16.0

## Running Tests

Expand Down
28 changes: 25 additions & 3 deletions bench/bench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,29 @@ var precomputed_pubkey: [65]u8 = undefined;
const WARMUP_NS: u64 = 500_000_000; // 0.5s warmup
const BENCH_NS: u64 = 2_000_000_000; // 2s measurement

const Timer = std.time.Timer;
/// Minimal replacement for std.time.Timer, which was removed in Zig 0.16.
const Timer = struct {
start_ns: i96,

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

fn start() error{}!Timer {
return .{ .start_ns = now() };
}

fn reset(self: *Timer) void {
self.start_ns = now();
}

fn read(self: *Timer) u64 {
const elapsed = now() - self.start_ns;
if (elapsed < 0) return 0;
return @intCast(elapsed);
}
};

const BenchResult = struct {
ns_per_op: u64,
Expand Down Expand Up @@ -453,7 +475,7 @@ fn benchEip712Hash() void {
// ============================================================================

pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();

Expand All @@ -471,7 +493,7 @@ pub fn main() !void {
precomputed_pubkey = eth.secp256k1.derivePublicKey(TEST_PRIVKEY) catch unreachable;

var out_buf: [8192]u8 = undefined;
var w = std.fs.File.stdout().writer(&out_buf);
var w = std.Io.File.stdout().writerStreaming(std.Io.Threaded.global_single_threaded.io(), &out_buf);
const stdout = &w.interface;

try stdout.print("\n{s:<34} {s:>12} {s:>14}\n", .{ "Benchmark", "ns/op", "iters" });
Expand Down
4 changes: 2 additions & 2 deletions bench/keccak_bench_cli.zig
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const std = @import("std");
const eth = @import("eth");

pub fn main() !void {
var args = std.process.args();
pub fn main(init: std.process.Init.Minimal) !void {
var args = std.process.Args.Iterator.init(init.args);
_ = args.next(); // skip program name

const size_str = args.next() orelse "32";
Expand Down
26 changes: 24 additions & 2 deletions bench/keccak_compare.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,29 @@ const DATA_4K: [4096]u8 = @splat(0xAB);

const WARMUP_NS: u64 = 500_000_000;
const BENCH_NS: u64 = 2_000_000_000;
const Timer = std.time.Timer;
/// Minimal replacement for std.time.Timer, which was removed in Zig 0.16.
const Timer = struct {
start_ns: i96,

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

fn start() error{}!Timer {
return .{ .start_ns = now() };
}

fn reset(self: *Timer) void {
self.start_ns = now();
}

fn read(self: *Timer) u64 {
const elapsed = now() - self.start_ns;
if (elapsed < 0) return 0;
return @intCast(elapsed);
}
};

const BenchResult = struct { ns_per_op: u64, iters: u64 };

Expand Down Expand Up @@ -111,7 +133,7 @@ fn benchStdlibKeccak4k() void {

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

try stdout.print("\n{s:<30} {s:>12} {s:>14}\n", .{ "Benchmark", "ns/op", "iters" });
Expand Down
26 changes: 24 additions & 2 deletions bench/u256_bench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,29 @@ const FULL_C: u256 = 0x00000001_00000000_00000000_00000000_00000000_00000000_000
const WARMUP_NS: u64 = 500_000_000; // 0.5s warmup
const BENCH_NS: u64 = 2_000_000_000; // 2s measurement

const Timer = std.time.Timer;
/// Minimal replacement for std.time.Timer, which was removed in Zig 0.16.
const Timer = struct {
start_ns: i96,

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

fn start() error{}!Timer {
return .{ .start_ns = now() };
}

fn reset(self: *Timer) void {
self.start_ns = now();
}

fn read(self: *Timer) u64 {
const elapsed = now() - self.start_ns;
if (elapsed < 0) return 0;
return @intCast(elapsed);
}
};

const BenchResult = struct {
ns_per_op: u64,
Expand Down Expand Up @@ -232,7 +254,7 @@ fn runAndJson(comptime name: []const u8, comptime func: fn () void, stdout: anyt

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

try stdout.print("\n{s:<32} {s:>12} {s:>14}\n", .{ "Benchmark", "ns/op", "iters" });
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .eth_zig,
.version = "0.4.0",
.fingerprint = 0xd0f21900fa26f179,
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{
"build.zig",
Expand Down
2 changes: 1 addition & 1 deletion examples/01_derive_address.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

// Hardhat/Anvil account #0 private key (DO NOT use in production)
Expand Down
4 changes: 2 additions & 2 deletions examples/02_check_balance.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
Comment on lines +14 to 16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for DebugAllocator usage patterns across the codebase and verify API

echo "Searching for DebugAllocator usage patterns..."
rg -n "DebugAllocator" --type zig -C3

echo -e "\nSearching for GeneralPurposeAllocator (to see if it was removed)..."
rg -n "GeneralPurposeAllocator" --type zig -C2

Repository: StrobeLabs/eth.zig

Length of output: 1988


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Listing repo files (sanity check)..."
ls -la

echo "Verifying file exists..."
git ls-files examples/02_check_balance.zig || true
ls -la examples || true

echo "Showing examples/02_check_balance.zig (top ~80 lines)..."
sed -n '1,80p' examples/02_check_balance.zig

echo -e "\nSearching for allocator types across Zig files..."
rg -n "DebugAllocator|GeneralPurposeAllocator" --type zig -S

Repository: StrobeLabs/eth.zig

Length of output: 1958


🌐 Web query:

Zig 0.16 std.heap.DebugAllocator(.{}) .init API defer deinit usage

💡 Result:

In Zig 0.16, std.heap.GeneralPurposeAllocator has been renamed to std.heap.DebugAllocator [1][2]. To use DebugAllocator correctly, you must instantiate it, obtain an allocator interface, and ensure deinit is called to perform leak detection [3][4][5]. The standard pattern is to use defer to ensure deinit is called upon exiting the scope [3][4]. Usage Pattern: zig const std = `@import`("std"); pub fn main() !void { // 1. Instantiate DebugAllocator with your desired configuration var gpa: std.heap.DebugAllocator(.{}) = .init; // 2. Use defer to ensure deinit is called at the end of the scope. // deinit() returns a status (e.g., .ok or .leak). defer std.debug.assert(gpa.deinit() == .ok); // 3. Obtain the standard Allocator interface to perform allocations const allocator = gpa.allocator(); // Now use the allocator as usual const memory = try allocator.alloc(u8, 1024); defer allocator.free(memory); } Key Details: - Instantiation: std.heap.DebugAllocator(.{}) is a function that returns a type, which you then initialize [6]. Using .init (e.g., var gpa: ... = .init;) is the recommended way to instantiate it with default configuration [6][7]. - Deinitialization: The deinit() method performs critical cleanup and, if safety is enabled (default), checks for memory leaks [1][8][5]. It returns a std.heap.Check enum, typically .ok or .leak [9][1]. Using std.debug.assert(gpa.deinit() == .ok) is a common idiom in tests or main functions to panic if a leak is detected [3][4]. - Scope: Because DebugAllocator maintains internal state, it must remain in scope for the entire duration of any allocations performed via its allocator() interface [10]. Failure to call deinit prevents the allocator from reporting leaks and may result in improper cleanup of internal metadata [5].

Citations:


Review: std.heap.DebugAllocator(.{}) usage + deinit handling in examples/02_check_balance.zig

  • std.heap.DebugAllocator(.{}) = .init matches the standard Zig pattern for instantiating DebugAllocator (and then calling gpa.allocator()).
  • defer _ = gpa.deinit(); correctly ensures cleanup runs, but it discards the deinit() leak-check result—consider using something like defer std.debug.assert(gpa.deinit() == .ok); (or otherwise handling the returned Check) so leaks don’t go unnoticed.
  • Using DebugAllocator for an example is reasonable since it helps catch leaks/misuse during the example’s execution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/02_check_balance.zig` around lines 14 - 16, The defer currently
discards the leak-check result from gpa.deinit(); update the cleanup to assert
or otherwise handle the returned Check so memory leaks are noticed: call
gpa.deinit() in the defer and verify its result (e.g., via std.debug.assert on
the returned value) instead of ignoring it; target the DebugAllocator instance
named gpa and its gpa.deinit() call and preserve the existing gpa.allocator()
usage.


Expand Down
2 changes: 1 addition & 1 deletion examples/03_sign_message.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

// Hardhat/Anvil account #0 private key
Expand Down
6 changes: 3 additions & 3 deletions examples/04_send_transaction.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

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

Expand All @@ -38,7 +38,7 @@ pub fn main() !void {
// Send 0.1 ETH to account #1
const recipient = try eth.primitives.addressFromHex("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const recipient_checksum = eth.primitives.addressToChecksum(&recipient);
const value = eth.units.parseEther(0.1);
const value = eth.units.parseEther(0.1) orelse return error.InvalidAmount;

try stdout.print("Sending 0.1 ETH\n", .{});
try stdout.print(" From: {s}\n", .{sender_checksum});
Expand Down
4 changes: 2 additions & 2 deletions examples/05_read_erc20.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

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

Expand Down
4 changes: 2 additions & 2 deletions examples/06_hd_wallet.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

// Standard test mnemonic (DO NOT use in production)
Expand All @@ -27,7 +27,7 @@ pub fn main() !void {
// Derive first 5 accounts
for (0..5) |i| {
const key = try eth.hd_wallet.deriveEthAccount(seed, @intCast(i));
const addr = key.toAddress();
const addr = try key.toAddress();
const checksum = eth.primitives.addressToChecksum(&addr);
try stdout.print(" [{d}] {s}\n", .{ i, checksum });
}
Expand Down
2 changes: 1 addition & 1 deletion examples/07_selectors.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const eth = @import("eth");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout_impl = std.fs.File.stdout().writer(&buf);
var stdout_impl = std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), &buf);
const stdout = &stdout_impl.interface;

// Comptime: evaluated at compile time, zero runtime cost
Expand Down
2 changes: 1 addition & 1 deletion examples/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub fn build(b: *std.Build) void {
.{ "04_send_transaction", "04_send_transaction.zig" },
.{ "05_read_erc20", "05_read_erc20.zig" },
.{ "06_hd_wallet", "06_hd_wallet.zig" },
.{ "07_comptime_selectors", "07_comptime_selectors.zig" },
.{ "07_selectors", "07_selectors.zig" },
};

inline for (examples) |example| {
Expand Down
2 changes: 1 addition & 1 deletion examples/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .eth_zig_examples,
.version = "0.1.0",
.fingerprint = 0xd73bf5facd267e8e,
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.dependencies = .{
.eth = .{
.path = "..",
Expand Down
3 changes: 2 additions & 1 deletion src/flashbots.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const primitives = @import("primitives.zig");
const uint256_mod = @import("uint256.zig");
const json_rpc = @import("json_rpc.zig");
const HttpTransport = @import("http_transport.zig").HttpTransport;
const runtime = @import("runtime.zig");

// ============================================================================
// Types
Expand Down Expand Up @@ -210,7 +211,7 @@ pub const Relay = struct {
.allocator = allocator,
.url = url,
.auth_signer = signer_mod.Signer.init(auth_key),
.client = .{ .allocator = allocator },
.client = .{ .allocator = allocator, .io = runtime.defaultIo() },
.next_id = 1,
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/http_transport.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const runtime = @import("runtime.zig");

/// HTTP JSON-RPC transport layer.
///
Expand All @@ -13,7 +14,7 @@ pub const HttpTransport = struct {
return .{
.url = url,
.allocator = allocator,
.client = .{ .allocator = allocator },
.client = .{ .allocator = allocator, .io = runtime.defaultIo() },
};
}

Expand Down
5 changes: 4 additions & 1 deletion src/keccak.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ pub const Keccak256 = StdlibKeccak256;
/// Works at both comptime and runtime.
pub fn hash(data: []const u8) Hash {
if (@inComptime()) {
@setEvalBranchQuota(10000);
// Zig 0.16's stdlib Keccak permutation exceeds the default 1000-branch
// comptime quota even for a single short input (e.g. comptime function
// selectors), so raise it for the comptime path only.
@setEvalBranchQuota(100000);
var result: Hash = undefined;
StdlibKeccak256.hash(data, &result, .{});
return result;
Expand Down
9 changes: 8 additions & 1 deletion src/mnemonic.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");
const keccak = @import("keccak.zig");
const runtime = @import("runtime.zig");

/// BIP-39 mnemonic phrase generation and seed derivation.
///
Expand Down Expand Up @@ -44,7 +45,7 @@ pub const ValidEntropySize = enum(u8) {
/// Generate a random mnemonic phrase.
pub fn generate(comptime entropy_size: ValidEntropySize) [entropy_size.wordCount()][]const u8 {
var entropy: [@intFromEnum(entropy_size)]u8 = undefined;
std.crypto.random.bytes(&entropy);
runtime.defaultIo().random(&entropy);
return entropyToMnemonic(entropy_size, &entropy);
}

Expand Down Expand Up @@ -292,6 +293,12 @@ test "validate rejects bad checksum" {
try std.testing.expectError(error.InvalidChecksum, validate(&words));
}

test "generate produces a valid mnemonic" {
const words = generate(.@"128");
try std.testing.expectEqual(@as(usize, 12), words.len);
try validate(&words);
}

test "entropyToMnemonic known vector" {
// All-zero entropy should produce "abandon" x 11 + "about"
const entropy = @as([16]u8, @splat(0));
Expand Down
Loading
Loading