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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ jobs:
- name: Run ENS conformance vectors
run: zig build vector-test

noir:
name: Noir verify / ${{ matrix.os }}
strategy:
fail-fast: false
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"
- name: Run unit tests with Barretenberg linked
# Fetches the pinned Barretenberg v5.2.0 static library for the runner
# (lazy dependency, sha256-pinned in build.zig.zon) and runs the noir
# unit + vector tests on top of the regular unit tests.
run: zig build test -Dnoir=true

fmt:
name: Format check
runs-on: ubuntu-latest
Expand Down
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ zig build
# Run tests
zig build test

# Run tests with the opt-in Noir/Barretenberg verifier linked (see README)
zig build test -Dnoir=true

# Check formatting
zig fmt --check src/ tests/

Expand All @@ -38,7 +41,7 @@ Layer 2: Encoding (-> primitives)
rlp.zig, abi_encode.zig, abi_decode.zig, abi_types.zig

Layer 3: Crypto (-> primitives)
keccak.zig, secp256k1.zig, signature.zig
keccak.zig, secp256k1.zig, signature.zig, kzg.zig, noir.zig

Layer 4: Types (-> primitives, encoding, crypto)
transaction.zig, receipt.zig, block.zig, log.zig, access_list.zig, blob.zig
Expand Down Expand Up @@ -88,7 +91,12 @@ 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
- No external dependencies in the default build -- it uses only Zig's standard
library plus the C sources vendored under `src/crypto/`. The one exception is
opt-in and default-off: `-Dnoir=true` fetches a pinned Barretenberg release
archive as a lazy package dependency (see
`src/crypto/barretenberg/VENDOR.md`). A new dependency that the default build
would resolve needs discussion first.

## Reporting Issues

Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,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`, `kzg` | ECDSA signing (RFC 6979), Keccak-256, EIP-155, EIP-4844 KZG |
| **Crypto** | `secp256k1`, `signer`, `signature`, `keccak`, `eip155`, `kzg`, `noir` | ECDSA signing (RFC 6979), Keccak-256, EIP-155, EIP-4844 KZG, Noir UltraHonk verification (opt-in) |
| **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 @@ -306,6 +306,7 @@ cd examples && zig build && ./zig-out/bin/01_derive_address
| 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 |
| Noir UltraHonk proof verification (opt-in `-Dnoir`, Barretenberg v5.2.0) | 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 +366,53 @@ cd examples && zig build && ./zig-out/bin/01_derive_address

```bash
zig build test # Unit tests
zig build test -Dnoir=true # Unit tests + Noir/Barretenberg verification tests (fetches the static library)
zig build integration-test # Integration tests (requires Anvil)
```

## Noir proof verification (opt-in)

`eth.noir` verifies Noir UltraHonk proofs offline through Barretenberg's C
ABI. It is off by default so the default build stays pure Zig + vendored C;
enable it with `-Dnoir=true` (or `.noir = true` in your `b.dependency` args).
That flag fetches the pinned Barretenberg **v5.2.0** static library for your
host as a lazy, hash-pinned package dependency and links it with libc++. No
C++ sources are vendored.

- Supported targets: aarch64 and x86_64 macOS, x86_64 Linux, aarch64 Linux.
Windows is out of scope.
- Verified: proofs written by `bb prove` (bb 5.2.0 / nargo 1.0.0-beta.25) in
the poseidon2 (default, `noir-recursive`) and keccak (`evm`) flavors, with
or without ZK. `Settings.fromVerifierTarget` mirrors `bb --verifier_target`
for those. IPA-accumulating rollup proofs (`noir-rollup`) are not supported:
they need a 32768-point Grumpkin CRS that `init` does not install. Proving
and VK generation are not included.
- CRS: verification needs only the BN254 G1 generator and the trusted-setup
G2 element, both embedded (192 bytes); nothing is downloaded at runtime.
Barretenberg's CRS is first-writer-wins per process, so `noir.init` installs
a verification-only CRS once and it cannot be enlarged afterwards.

```zig
const noir = eth.noir;
try noir.init();
const ok = try noir.verify(
allocator,
vk_bytes, // bb `vk` file
try noir.fieldsFromBytes(public_inputs_bytes), // bb `public_inputs` file
try noir.fieldsFromBytes(proof_bytes), // bb `proof` file
.fromVerifierTarget(.evm),
);
```

Only `true` means verified. `false` means Barretenberg returned a verdict of
`verified=false` (wrong proof, wrong public-input value, wrong size, flavor
mismatch); `error.ProofRejected` means it rejected the inputs before or during
deserialization (non-canonical field, point off the curve, public-input count
that disagrees with the VK, VK that does not deserialize). Both mean "not
verified", so callers must treat the error as a rejection rather than an
internal fault; `verifyDiag` exposes Barretenberg's message. See
`src/crypto/barretenberg/VENDOR.md` for the pinned assets and hashes.

## Benchmarks

One command to run the full comparison (requires Zig, Rust, Python 3):
Expand Down
93 changes: 93 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Opt-in Noir UltraHonk verification (src/noir.zig). Off by default: the
// Barretenberg static library is a lazy dependency that is only fetched and
// linked when this is set, and the noir tests are only collected then.
const noir = b.option(bool, "noir", "Link Barretenberg v5.2.0 and enable eth.noir (UltraHonk proof verification)") orelse false;
const build_options = b.addOptions();
build_options.addOption(bool, "noir", noir);
const build_options_module = build_options.createModule();

// Main library module
const eth_module = b.addModule("eth", .{
.root_source_file = b.path("src/root.zig"),
Expand All @@ -14,6 +22,7 @@ pub fn build(b: *std.Build) void {
addXkcp(b, eth_module, target);
addSecp256k1(b, eth_module);
addKzg(b, eth_module);
addNoir(b, eth_module, target, noir, build_options_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 @@ -31,6 +40,7 @@ pub fn build(b: *std.Build) void {
addXkcp(b, unit_test_module, target);
addSecp256k1(b, unit_test_module);
addKzg(b, unit_test_module);
addNoir(b, unit_test_module, target, noir, build_options_module);
const unit_tests = b.addTest(.{
.root_module = unit_test_module,
});
Expand All @@ -39,6 +49,24 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);

// Noir/Barretenberg interop vectors (tests/vectors/noir/, embedded from
// tests/). A separate test binary, and part of `zig build test` only when
// -Dnoir=true links the library.
if (noir) {
const noir_vector_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("tests/noir_vectors_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "eth", .module = eth_module },
},
}),
});
const run_noir_vector_tests = b.addRunArtifact(noir_vector_tests);
test_step.dependOn(&run_noir_vector_tests.step);
}

// Install the unit-test binary so coverage tooling (kcov) can run it
// out-of-band: `zig build install-test` writes it to zig-out/bin/test.
const install_unit_tests = b.addInstallArtifact(unit_tests, .{});
Expand Down Expand Up @@ -112,6 +140,7 @@ pub fn build(b: *std.Build) void {
addXkcp(b, bench_module, target);
addSecp256k1(b, bench_module);
addKzg(b, bench_module);
addNoir(b, bench_module, target, noir, build_options_module);

const bench_exe = b.addExecutable(.{
.name = "bench",
Expand Down Expand Up @@ -318,3 +347,67 @@ fn addKzg(b: *std.Build, module: *std.Build.Module) void {
.flags = ckzg_flags,
});
}

/// Expose the `noir` build option to src/root.zig and, when it is set, link the
/// pinned Barretenberg v5.2.0 static library for the target.
///
/// The library is a lazy package dependency (build.zig.zon), one archive per
/// supported host: nothing is fetched unless `-Dnoir=true`, and only the
/// archive for the target being built. It is C++ (libc++ ABI, exceptions kept
/// inside the library), so libc++ and libc are linked with it. See
/// src/crypto/barretenberg/VENDOR.md for hashes, license and update steps.
fn addNoir(
b: *std.Build,
module: *std.Build.Module,
target: std.Build.ResolvedTarget,
enabled: bool,
build_options_module: *std.Build.Module,
) void {
module.addImport("build_options", build_options_module);
if (!enabled) return;

const os = target.result.os.tag;
const arch = target.result.cpu.arch;
const dep_name: []const u8 = if (os == .macos and arch == .aarch64)
"barretenberg_arm64_darwin"
else if (os == .macos and arch == .x86_64)
"barretenberg_amd64_darwin"
else if (os == .linux and arch == .x86_64)
"barretenberg_amd64_linux"
else if (os == .linux and arch == .aarch64)
"barretenberg_arm64_linux"
else {
// No release archive for this target. Report it as an ordinary build
// error instead of aborting the runner while the graph is still being
// built: the failure is attached to the module's object-file input, so
// it fires exactly when something that needs Barretenberg is built and
// never for `zig build --help` or a step that does not link it.
const fail = b.addFail(b.fmt(
"-Dnoir=true is not supported for {s}-{s}; Barretenberg v5.2.0 static " ++
"libraries are wired up for aarch64-macos, x86_64-macos, x86_64-linux " ++
"and aarch64-linux only",
.{ @tagName(arch), @tagName(os) },
));
const unreachable_object = b.addWriteFiles();
unreachable_object.step.dependOn(&fail.step);
module.addObjectFile(unreachable_object.add("noir-unsupported-target.o", ""));
return;
};

// Returns null (after scheduling the fetch) the first time the archive is
// needed; the build runner re-executes once it is available.
const dep = b.lazyDependency(dep_name, .{}) orelse return;
module.addObjectFile(dep.path("libbb-external.a"));
module.link_libcpp = true;
module.link_libc = true;

if (os.isDarwin()) {
// Strong C11 aligned_alloc so the archive's weak, hidden definition of
// that libc-named symbol is not bound to libSystem's strict version by
// Zig's Mach-O linker. See the comment in the file.
module.addCSourceFile(.{
.file = b.path("src/crypto/barretenberg/aligned_alloc_macos.c"),
.flags = &.{"-O2"},
});
}
}
27 changes: 26 additions & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,32 @@
.version = "0.9.1", // x-release-please-version
.fingerprint = 0xd0f21900fa26f179,
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.dependencies = .{
// Barretenberg v5.2.0 prebuilt static libraries (Apache-2.0), one per
// supported host. Lazy: fetched only when building with -Dnoir=true,
// and only the entry build.zig selects for the target. Hashes and the
// update procedure are recorded in src/crypto/barretenberg/VENDOR.md.
.barretenberg_arm64_darwin = .{
.url = "https://github.com/AztecProtocol/barretenberg/releases/download/v5.2.0/barretenberg-static-arm64-darwin.tar.gz",
.hash = "N-V-__8AAKAlJwOvxF9WMTWZ6_g3tK6kxoN1XqeG0j1bIxfK",
.lazy = true,
},
.barretenberg_amd64_darwin = .{
.url = "https://github.com/AztecProtocol/barretenberg/releases/download/v5.2.0/barretenberg-static-amd64-darwin.tar.gz",
.hash = "N-V-__8AAEi5uATDH1uT4EsKM7BxD_qgy01S6bvSVntg5M_O",
.lazy = true,
},
.barretenberg_amd64_linux = .{
.url = "https://github.com/AztecProtocol/barretenberg/releases/download/v5.2.0/barretenberg-static-amd64-linux.tar.gz",
.hash = "N-V-__8AABCloQVNS5sP_eXpnSbYWfq2xQBd4DQuiwYOKmDL",
.lazy = true,
},
.barretenberg_arm64_linux = .{
.url = "https://github.com/AztecProtocol/barretenberg/releases/download/v5.2.0/barretenberg-static-arm64-linux.tar.gz",
.hash = "N-V-__8AAB5oGQRtvwJEs6U1kH632yGzq9QqF7aGtv8LDL-y",
.lazy = true,
},
},
.paths = .{
"build.zig",
"build.zig.zon",
Expand Down
Loading
Loading