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
44 changes: 44 additions & 0 deletions .github/workflows/c-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: C ABI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
name: C ABI / ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
with:
version: "0.16.0"
- run: zig build c-lib c-test -Doptimize=ReleaseSafe
- run: python3 examples/ffi/sign_transaction.py
- run: zig cc examples/ffi/main.c -I zig-out/include zig-out/lib/libethzig.a -o zig-out/ffi-example
- run: ./zig-out/ffi-example

cross-build:
name: C ABI / ${{ matrix.target }}
runs-on: macos-latest
strategy:
matrix:
target: [x86_64-linux-gnu, aarch64-linux-gnu, aarch64-macos]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
with:
version: "0.16.0"
- run: zig build c-lib -Dtarget=${{ matrix.target }} -Dcpu=baseline -Doptimize=ReleaseSafe
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
version: "0.16.0"
- name: Build library
run: zig build
- name: Check examples (offline)
working-directory: examples
run: zig build check

coverage:
name: Coverage (kcov)
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Optional versioned C ABI with caller-owned buffers for crypto, EIP-1559 signing, ABI and RLP; static/shared builds, C conformance tests and an offline Python example.


### Added
- abigen writes: state-changing contract calls via `*Wallet` -- `send(self, wallet, comptime name, args) ![32]u8` and `sendValue(..., value)` build the same `selector ++ encode(args)` calldata as `call` (shared `encodeCall`) and submit through `Wallet.sendTransaction`; `sendAndWait(..., max_attempts) !TransactionReceipt` waits for the receipt. Naming a `view`/`pure` function in `send` is a `@compileError` pointing you to `call`. Completes the abigen reads + events + writes surface (#68)
- `kzg` module: real EIP-4844 KZG support, vendoring the C reference implementations (`c-kzg-4844` v2.1.1 + its `blst` v0.3.14 dependency) and exposing a small Zig API for building blob-transaction sidecars. `kzg.init(allocator)`/`kzg.deinit()` load and free the mainnet trusted setup, which is `@embedFile`d (the KZG ceremony `trusted_setup.txt`) so consumers need no external file; init is idempotent and guarded by an atomic once-flag. `kzg.blobToKzgCommitment(blob)` -> `[48]u8` (c-kzg `blob_to_kzg_commitment`), `kzg.computeBlobKzgProof(blob, commitment)` -> `[48]u8` (`compute_blob_kzg_proof`), `kzg.verifyBlobKzgProof(blob, commitment, proof)` -> `bool` (`verify_blob_kzg_proof`), plus `kzg.verifyBlobKzgProofBatch(...)`. c-kzg's `C_KZG_RET` codes map to a Zig `KzgError` set. `blob.buildSidecar(allocator, raw_blob)` fills a `BlobSidecar` (blob + commitment + proof) via the above, and `blob.computeVersionedHash` derives the EIP-4844 versioned hash from a commitment. blst is built in its portable no-assembly C mode (`-D__BLST_NO_ASM__ -D__BLST_PORTABLE__`, 32-bit limbs) so the build is robust across targets with no per-arch assembly. Verified byte-for-byte against the official ethereum/c-kzg-4844 v2.1.1 test vectors (`blob_to_kzg_commitment`, `compute_blob_kzg_proof`, `verify_blob_kzg_proof` correct/incorrect) embedded under `src/crypto/c-kzg/test_vectors/`, plus round-trip and init/deinit lifecycle tests
Expand Down
6 changes: 3 additions & 3 deletions 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.16.0](https://ziglang.org/download/)
- [Zig 0.16.0](https://ziglang.org/download/)
- Git

## Getting Started
Expand Down Expand Up @@ -70,7 +70,7 @@ Layers 1-3 have zero I/O. Layers 1-5 have zero network dependencies.
2. Write your code -- follow existing patterns in the codebase
3. Add tests for new functionality
4. Run `zig fmt src/ tests/` to format your code
5. Run `zig build test` to make sure all tests pass
5. Run `make ci` to build, check formatting, and run unit, conformance, C ABI, and offline example checks
6. Open a PR against `main`

## Commit Messages
Expand All @@ -88,7 +88,7 @@ Use [Conventional Commits](https://www.conventionalcommits.org/) for PR titles o
- Keep functions focused and small
- Add doc comments (`///`) to public functions
- Prefer comptime over runtime where possible -- this is a core design principle
- No external dependencies -- everything builds on Zig's standard library
- The core uses Zig's standard library and vendored native crypto backends; discuss new dependencies before adding them.

## Reporting Issues

Expand Down
16 changes: 14 additions & 2 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 c-lib c-test examples integration-test bench bench-u256 bench-keccak coverage docs clean

## Build the library (default)
build:
Expand Down Expand Up @@ -36,7 +36,19 @@ docs:
lint: fmt test

## Full CI check: build + fmt + test (matches all CI jobs, still no Anvil)
ci: build fmt test
ci: build fmt test c-test examples

## Compile every example and run the offline examples
examples:
cd examples && $(ZIG) build check

## Build static/shared native libraries and install the C header
c-lib:
$(ZIG) build c-lib

## Test the public ABI from C, with both static and shared linkage
c-test:
$(ZIG) build c-test

## Run integration tests (requires Anvil running on localhost:8545)
integration-test:
Expand Down
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ alloy.rs wins on address hex parsing (1.55x -- SIMD), hex encoding (1.08x), Unis
const eth = @import("eth");

const private_key = try eth.hex.hexToBytesFixed(32, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
const signer = eth.signer.Signer.init(private_key);
var signer = eth.signer.Signer.fromPrivateKey(private_key);
defer signer.deinit();
const addr = try signer.address();
const checksum = eth.primitives.addressToChecksum(&addr);
// "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
Expand All @@ -62,14 +63,14 @@ const checksum = eth.primitives.addressToChecksum(&addr);
```zig
const eth = @import("eth");

var transport = eth.http_transport.HttpTransport.init(allocator, "https://rpc.example.com");
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);

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),
.value = eth.units.parseEther(1.0) orelse return error.InvalidAmount,
});
```

Expand Down Expand Up @@ -162,7 +163,7 @@ const addr = key.toAddress();
```zig
const eth = @import("eth");

const client = try eth.ws_client.WsClient.connect(allocator, "wss://mainnet.example.com/ws", .{});
const client = try eth.ws_client.WsClient.connect(allocator, "wss://mainnet.example.com/ws", eth.runtime.blockingIo(), .{});
defer client.deinit();

const heads = try client.subscribe(.{ .new_heads = {} });
Expand Down Expand Up @@ -226,13 +227,24 @@ eth.zig

Built something with eth.zig? Open a PR to add it here.

## Use from C or Python

Build the native library with Zig 0.16.0, then sign a transaction from Python without pip dependencies or an RPC connection:

```sh
zig build c-lib -Doptimize=ReleaseSafe
python3 examples/ffi/sign_transaction.py
```

See the [C header](include/eth.h) and [FFI examples](examples/ffi/) for buffer ownership, supported types, and static/shared linking.

## Installation

**One-liner:**

<!-- x-release-please-start-version -->
```bash
zig fetch --save git+https://github.com/StrobeLabs/eth.zig.git#v0.9.1
zig fetch --save=eth git+https://github.com/StrobeLabs/eth.zig.git#v0.9.1
```
<!-- x-release-please-end -->

Expand Down Expand Up @@ -271,7 +283,7 @@ The [`examples/`](examples/) directory contains self-contained programs demonstr
| `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 |
| `07_selectors` | Comptime function selectors | No |
| `08_mev_share_backrunner` | MEV-Share backrunner bot (SSE stream + bundle) | No (dry-run) |

Run any example:
Expand Down Expand Up @@ -324,9 +336,10 @@ cd examples && zig build && ./zig-out/bin/01_derive_address
| ERC-20 typed wrapper | Complete |
| ERC-721 typed wrapper | Complete |
| JSON ABI parsing | Complete |
| EIP-7702 transactions | Planned |
| EIP-7702 transactions | Done |
| IPC transport | Planned |
| Provider middleware (retry, caching) | Planned |
| Provider retry / endpoint failover | Done |
| Provider response caching | Planned |
| Hardware wallet signers | Planned |

## Comparison with Other Libraries
Expand Down Expand Up @@ -359,7 +372,7 @@ cd examples && zig build && ./zig-out/bin/01_derive_address

## Requirements

- Zig >= 0.16.0
- Zig 0.16.0

## Running Tests

Expand Down
40 changes: 40 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,46 @@ pub fn build(b: *std.Build) void {
addSecp256k1(b, eth_module);
addKzg(b, eth_module);

// Optional, versioned native ABI. Only the C surface is exported; the
// existing Zig module and its crypto backends remain unchanged.
const c_lib_step = b.step("c-lib", "Install static/shared C libraries and eth.h");
const c_test_step = b.step("c-test", "Run C ABI tests against static and shared libraries");
inline for (.{ std.builtin.LinkMode.static, std.builtin.LinkMode.dynamic }) |linkage| {
const c_module = b.createModule(.{
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = true,
});
c_module.addIncludePath(b.path("include"));
addXkcp(b, c_module, target);
addSecp256k1(b, c_module);
const lib = b.addLibrary(.{
.name = "ethzig",
.root_module = c_module,
.linkage = linkage,
.version = .{ .major = 1, .minor = 0, .patch = 0 },
});
lib.installHeader(b.path("include/eth.h"), "eth.h");
c_lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step);
const c_test_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
});
c_test_module.addCSourceFile(.{
.file = b.path("tests/c_api_test.c"),
.flags = &.{ "-std=c11", "-Wall", "-Wextra", "-Werror" },
});
c_test_module.linkLibrary(lib);
const c_tests = b.addExecutable(.{
.name = "c-api-test-" ++ @tagName(linkage),
.root_module = c_test_module,
});
c_test_step.dependOn(&b.addRunArtifact(c_tests).step);
}

// Unit tests. Root the test artifact at src/root.zig so its test block
// (which direct-imports every module file) actually collects and runs the
// per-module `test` blocks. Aggregating via `_ = eth.module` field access
Expand Down
1 change: 1 addition & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build.zig",
"build.zig.zon",
"src",
"include",
"src/crypto/blst",
"src/crypto/c-kzg",
"LICENSE",
Expand Down
52 changes: 52 additions & 0 deletions docs/content/docs/c-api.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: C and Python bindings
description: Use eth.zig signing, hashing, ABI and RLP from native applications.
---

The optional C interface exposes eth.zig's compute operations to C, Python,
and other languages with C interoperability. Build from a checkout with Zig
0.16.0:

```sh
zig build c-lib -Doptimize=ReleaseSafe
zig build c-test
python3 examples/ffi/sign_transaction.py
```

The Python example signs an EIP-1559 transaction offline using `ctypes`, with
no pip dependencies, and verifies its hash against a known vector. A minimal
C example lives in `examples/ffi/main.c`.

## Interface

| Surface | Functions |
| --- | --- |
| Hashing | `eth_keccak256`, `eth_tx_hash`, `eth_abi_selector` |
| Accounts | `eth_address_from_pubkey`, `eth_address_checksum` |
| Signing | `eth_sign`, `eth_recover`, `eth_tx_sign` |
| Encoding | `eth_abi_encode`, `eth_abi_decode`, `eth_rlp_encode`, `eth_rlp_decode` |

`zig-out/include/eth.h` documents every argument and error code. The library
installs static and versioned shared artifacts under `zig-out/lib`. The C ABI
major is 1, independent of the Zig package version; incompatible changes require
a shared-library major bump.

## Ownership

Call the operation's `*_max_len` helper, allocate a buffer, and pass its capacity.
The helper includes any temporary workspace and returns zero for invalid input
or size overflow. A successful operation returns `ETH_OK` and a written length.
On failure, the written length is zero; discard the buffer contents. Inputs and
outputs must not overlap, and no pointers are retained by the library.

Integers wider than 64 bits are big-endian byte arrays. Raw signatures use
`r[32] || s[32] || recovery_id[1]`, with recovery ID 0 or 1. Transactions support
the full EIP-1559 field set, including access lists and contract creation.

ABI encoding accepts flat tuples of up to 32 scalar, bytes, or string values;
arrays and nested tuples are outside this initial C surface. Decoded dynamic
values borrow the caller's output storage. RLP decoding unwraps one item and
reports the input consumed, allowing the caller to walk nested lists.

Networking stays in the host application. See the
[complete FFI examples and buffer contract](https://github.com/StrobeLabs/eth.zig/tree/main/examples/ffi).
6 changes: 3 additions & 3 deletions docs/content/docs/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Contributions are welcome! Here's everything you need to get started.

## Prerequisites

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

## Getting Started
Expand Down Expand Up @@ -73,7 +73,7 @@ Layers 1-3 have zero I/O. Layers 1-5 have zero network dependencies.
2. Write your code -- follow existing patterns in the codebase
3. Add tests for new functionality
4. Run `zig fmt src/ tests/` to format your code
5. Run `zig build test` to make sure all tests pass
5. Run `make ci` to build, check formatting, and run unit, conformance, C ABI, and offline example checks
6. Open a PR against `main`

## Code Style
Expand All @@ -83,7 +83,7 @@ Layers 1-3 have zero I/O. Layers 1-5 have zero network dependencies.
- Keep functions focused and small
- Add doc comments (`///`) to public functions
- Prefer comptime over runtime where possible -- this is a core design principle
- No external dependencies -- everything builds on Zig's standard library
- The core uses Zig's standard library and vendored native crypto backends; discuss new dependencies before adding them.

## Reporting Issues

Expand Down
7 changes: 4 additions & 3 deletions docs/content/docs/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The [`examples/`](https://github.com/StrobeLabs/eth.zig/tree/main/examples) dire
| `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 |
| `07_selectors` | Comptime function selectors | No |
| `08_mev_share_backrunner` | MEV-Share backrunner bot (SSE stream + bundle) | No (dry-run) |

Run any example:
Expand All @@ -34,7 +34,8 @@ const private_key = try eth.hex.hexToBytesFixed(
32,
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
);
const signer = eth.signer.Signer.init(private_key);
var signer = eth.signer.Signer.fromPrivateKey(private_key);
defer signer.deinit();
const addr = try signer.address();
const checksum = eth.primitives.addressToChecksum(&addr);
// "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
Expand All @@ -52,7 +53,7 @@ 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),
.value = eth.units.parseEther(1.0) orelse return error.InvalidAmount,
});
```

Expand Down
Loading
Loading