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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 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
- `abigen` module: comptime contract bindings (#68). `eth.bind(@embedFile("erc20.json"))` (alias for `eth.abigen.Bind`) parses a Solidity JSON ABI **at compile time** and returns a fully typed contract struct -- zero runtime ABI parsing, with selectors and event topic0s precomputed at comptime. The ABI is read by a purpose-built comptime JSON tokenizer (std.json needs a runtime allocator and does not run at comptime in 0.16), and generated structs are reified with `@Struct` while typed reads/decodes dispatch on a comptime function name so the argument-tuple and return types are fully resolved per call site. Public surface: `Self.at(address)`; `call(self, provider, comptime name, args) !Ret` (ABI-encodes `selector ++ encode(args)`, runs `eth_call`, decodes into the mapped return type); `selectorOf(name)`/`ArgsOf(name)`/`ReturnOf(name)`; and for events `decodeEvent(name, log) !EventStruct` (indexed params from `log.topics[1..]`, non-indexed from `log.data`, validating `topics[0]`), `topicOf(name)`, and `EventOf(name)`. ABI integers map to the smallest fitting Zig integer (`uint8` -> `u8`, `uint112` -> `u112`), matching zabi's `AbiParameterToPrimative`; `address` -> `[20]u8`, `bool` -> `bool`, `bytesN` -> `[N]u8`, `bytes`/`string` -> `[]const u8`. Read functions and events this release; state-changing writes (taking a `*Wallet`) are a documented follow-up. Tuple/array params and overloaded-function redeclarations are parsed but not yet addressable, and naming an unsupported entry is a clear `@compileError`. Asserts the comptime-derived selectors against known values (`balanceOf(address)` == 0x70a08231, `transfer(address,uint256)` == 0xa9059cbb) and the ERC-20 `Transfer` topic0, and exercises calldata encoding and a full `Transfer` log decode without a network. Builds and tests green on both Zig 0.16 and 0.17-dev

## [0.7.0] - 2026-06-11
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ 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` | ECDSA signing (RFC 6979), Keccak-256, EIP-155 |
| **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 |
Expand All @@ -284,6 +284,7 @@ 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-155 replay protection | Complete |
| EIP-191 personal message signing | Complete |
| EIP-712 typed structured data signing | Complete |
Expand Down
44 changes: 44 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub fn build(b: *std.Build) void {
});
addXkcp(b, eth_module, target);
addSecp256k1(b, eth_module);
addKzg(b, eth_module);

// 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
Expand All @@ -29,6 +30,7 @@ pub fn build(b: *std.Build) void {
});
addXkcp(b, unit_test_module, target);
addSecp256k1(b, unit_test_module);
addKzg(b, unit_test_module);
const unit_tests = b.addTest(.{
.root_module = unit_test_module,
});
Expand Down Expand Up @@ -62,6 +64,7 @@ pub fn build(b: *std.Build) void {
});
addXkcp(b, bench_module, target);
addSecp256k1(b, bench_module);
addKzg(b, bench_module);

const bench_exe = b.addExecutable(.{
.name = "bench",
Expand Down Expand Up @@ -227,3 +230,44 @@ fn addSecp256k1(b: *std.Build, module: *std.Build.Module) void {
});
}
}

/// Add c-kzg-4844 + blst C sources for real EIP-4844 KZG operations.
///
/// blst is built in portable no-assembly C mode (`-D__BLST_NO_ASM__`) so it
/// uses its pure-C field arithmetic instead of per-arch assembly. This keeps
/// the build robust across targets (no GAS-vs-clang assembler conflicts) at a
/// modest performance cost, acceptable for sidecar construction. See
/// src/crypto/c-kzg/VENDOR.md for pinned versions and rationale.
fn addKzg(b: *std.Build, module: *std.Build.Module) void {
// blst: portable C backend. `__BLST_PORTABLE__` additionally disables the
// optional SHA CPU-intrinsics path in src/sha256.h. `-fno-sanitize=undefined`
// mirrors the other vendored C: blst does intentional unaligned accesses.
const blst_flags = &.{
"-D__BLST_NO_ASM__",
"-D__BLST_PORTABLE__",
"-O2",
"-fno-sanitize=undefined",
"-Wno-unused-function",
};
module.addIncludePath(b.path("src/crypto/blst/bindings"));
// server.c is blst's unity build: it #includes every other src/*.c.
module.addCSourceFile(.{
.file = b.path("src/crypto/blst/src/server.c"),
.flags = blst_flags,
});

// c-kzg-4844: ckzg.c is the unity build including every other c-kzg .c file.
// Needs its own src/ on the include path (so "common/...", "eip4844/..."
// resolve) plus blst's bindings for "blst.h".
const ckzg_flags = &.{
"-O2",
"-fno-sanitize=undefined",
"-Wno-unused-function",
};
module.addIncludePath(b.path("src/crypto/c-kzg/src"));
module.addIncludePath(b.path("src/crypto/blst/bindings"));
module.addCSourceFile(.{
.file = b.path("src/crypto/c-kzg/src/ckzg.c"),
.flags = ckzg_flags,
});
}
2 changes: 2 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"build.zig",
"build.zig.zon",
"src",
"src/crypto/blst",
"src/crypto/c-kzg",
"LICENSE",
"README.md",
},
Expand Down
76 changes: 76 additions & 0 deletions docs/content/docs/kzg.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: KZG / EIP-4844
description: Build EIP-4844 blob-transaction sidecars - KZG commitments and proofs over raw blob data - using the vendored c-kzg-4844 and blst reference implementations with the mainnet trusted setup embedded.
---

EIP-4844 ("blobs") attaches large data blobs to a transaction, each committed to
with a **KZG commitment** and proven with a **KZG proof**. `eth.kzg` provides
real, spec-correct KZG over the vendored
[c-kzg-4844](https://github.com/ethereum/c-kzg-4844) reference implementation and
its [blst](https://github.com/supranational/blst) dependency. The mainnet
trusted setup (the KZG ceremony output) is **embedded in the library** via
`@embedFile`, so you do not need to ship or locate an external setup file.

## Initialize the trusted setup

Load the trusted setup once at startup and free it at shutdown. `init` is
idempotent and thread-safe (guarded by an atomic once-flag), so calling it more
than once is harmless.

```zig
const eth = @import("eth");

try eth.kzg.init(allocator);
defer eth.kzg.deinit();
```

## Build a blob sidecar

The common task is turning raw blob bytes into a sidecar: the blob plus its KZG
commitment and proof. `blob.buildSidecar` does exactly that.

```zig
// `raw_blob` is [131072]u8 (128 KiB = 4096 field elements x 32 bytes).
// Each 32-byte field element must be a canonical BLS12-381 scalar (< modulus).
var raw_blob: eth.blob.Blob = @splat(0);
// ... fill raw_blob with your data ...

const sidecar = try eth.blob.buildSidecar(allocator, raw_blob);
// sidecar.blob, sidecar.commitment ([48]u8), sidecar.proof ([48]u8)
```

The blob-transaction `versioned hash` that goes on-chain is derived from the
commitment:

```zig
const versioned_hash = eth.blob.computeVersionedHash(sidecar.commitment);
// versioned_hash[0] == 0x01 (the KZG version byte)
```

## Lower-level API

If you want the individual operations rather than a full sidecar:

```zig
const commitment = try eth.kzg.blobToKzgCommitment(&raw_blob); // [48]u8
const proof = try eth.kzg.computeBlobKzgProof(&raw_blob, commitment); // [48]u8

const ok = try eth.kzg.verifyBlobKzgProof(&raw_blob, commitment, proof);
// ok == true

// Batch verification over equal-length slices of blobs/commitments/proofs:
const all_ok = try eth.kzg.verifyBlobKzgProofBatch(blobs, commitments, proofs);
```

Errors map c-kzg's `C_KZG_RET` codes onto a Zig `KzgError` set
(`BadArgs`, `Internal`, `OutOfMemory`), plus `NotInitialized` if you call an
operation before `kzg.init`.

## Correctness

The bindings are verified **byte-for-byte against the official ethereum/c-kzg-4844
test vectors** (the same `blob_to_kzg_commitment`, `compute_blob_kzg_proof`, and
`verify_blob_kzg_proof` vectors used by the reference implementation), in addition
to round-trip and lifecycle tests. The vendored versions are pinned in
`src/crypto/c-kzg/VENDOR.md` (c-kzg-4844 v2.1.1, blst v0.3.14, built in blst's
portable no-assembly C mode for robustness across targets).
1 change: 1 addition & 0 deletions docs/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"---Guides---",
"examples",
"transactions",
"kzg",
"contracts",
"abigen",
"tokens",
Expand Down
20 changes: 20 additions & 0 deletions src/blob.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ pub const BlobSidecar = struct {
proof: KzgProof,
};

/// Build a blob sidecar from raw blob data, computing the KZG commitment and
/// proof via the vendored c-kzg-4844 backend.
///
/// The caller must have initialized the KZG trusted setup with `kzg.init`
/// beforehand (and is responsible for `kzg.deinit`). The `allocator` is
/// accepted for API symmetry; this routine performs no heap allocation itself
/// (a `BlobSidecar` is returned by value).
pub fn buildSidecar(allocator: std.mem.Allocator, raw_blob: Blob) !BlobSidecar {
_ = allocator;
// Lazy import avoids a hard import cycle (kzg.zig imports blob.zig).
const kzg = @import("kzg.zig");
const commitment = try kzg.blobToKzgCommitment(&raw_blob);
const proof = try kzg.computeBlobKzgProof(&raw_blob, commitment);
return BlobSidecar{
.blob = raw_blob,
.commitment = commitment,
.proof = proof,
};
}

/// Compute the versioned hash from a KZG commitment.
///
/// The versioned hash is keccak256(commitment) with the first byte
Expand Down
Loading
Loading