From e1e89965ca1bb507fef7cc1956bc3b45927b5737 Mon Sep 17 00:00:00 2001 From: Amlandeep Bhadra Date: Fri, 11 Sep 2026 01:39:22 -0400 Subject: [PATCH] feat: opt-in Noir UltraHonk proof verification via Barretenberg v5.2.0 Add eth.noir, an offline verifier for Noir UltraHonk proofs (poseidon2 and keccak/evm flavors) that talks to Barretenberg's msgpack C ABI through the pinned v5.2.0 release static library. - build.zig: -Dnoir (default false). Off, nothing changes: no download, no link, the noir tests are not collected and the default test count stays at 898. On, the matching libbb-external.a is fetched as a lazy, hash-pinned package dependency (arm64/amd64 macOS, amd64/arm64 Linux) and linked with libc++. A target with no release archive fails the step that needs the library, so informational invocations such as zig build --help still work. - src/noir.zig: init() installs the 192-byte verification CRS (the G1 generator plus the pinned G2 element) behind an atomic once-flag; verify() and verifyDiag() separate a verified=false verdict from inputs Barretenberg rejects before or during deserialization (error.ProofRejected, with the library's message), and the docs are explicit that both mean not verified; Settings.fromVerifierTarget mirrors bb --verifier_target for the flavors that work here; every bbapi call is serialized behind one mutex; responses are decoded against the pinned schema so drift is a Zig error. - IPA-accumulating rollup proofs are not supported and not offered: they are checked against a 32768-point Grumpkin CRS that init does not install. ipa_accumulation stays as a wire field, documented as unsupported, with a test pinning that no verifier target sets it. - src/noir/msgpack.zig: canonical msgpack subset encoder plus a decoder that is strict about family and bounds. - macOS: strong C11 aligned_alloc shim, compiled only with -Dnoir, because Zig's Mach-O linker binds the archive's weak hidden aligned_alloc to libSystem's strict implementation. - Tests: unit tests for the init guard, concurrent init, settings, embedded constants and the response decoder; msgpack unit tests; bb-produced vectors (hello circuit, bb 5.2.0, nargo 1.0.0-beta.25) covering both flavors plus tampered, short, wrong-input-value, wrong-input-count, undeserializable-VK, non-canonical and flavor-mismatch cases, concurrent verification across threads, and byte-for-byte request comparisons against reference requests. - CI: "Noir verify" job on ubuntu and macos running zig build test -Dnoir=true. - Docs: README section, src/crypto/barretenberg/VENDOR.md, vectors README, and the CONTRIBUTING dependency note and layer diagram. --- .github/workflows/ci.yml | 20 + CONTRIBUTING.md | 12 +- README.md | 47 +- build.zig | 93 +++ build.zig.zon | 27 +- src/crypto/barretenberg/VENDOR.md | 126 ++++ src/crypto/barretenberg/aligned_alloc_macos.c | 56 ++ src/noir.zig | 672 ++++++++++++++++++ src/noir/msgpack.zig | 587 +++++++++++++++ src/root.zig | 12 + tests/noir_vectors_test.zig | 251 +++++++ tests/vectors/noir/Prover.toml | 2 + tests/vectors/noir/README.md | 43 ++ tests/vectors/noir/keccak/proof | Bin 0 -> 4544 bytes tests/vectors/noir/keccak/public_inputs | Bin 0 -> 32 bytes tests/vectors/noir/keccak/vk | Bin 0 -> 1888 bytes tests/vectors/noir/keccak/vk_hash | Bin 0 -> 32 bytes tests/vectors/noir/main.nr | 3 + tests/vectors/noir/poseidon2/proof | Bin 0 -> 14656 bytes tests/vectors/noir/poseidon2/public_inputs | Bin 0 -> 32 bytes tests/vectors/noir/poseidon2/vk | Bin 0 -> 3680 bytes tests/vectors/noir/poseidon2/vk_hash | 1 + .../vectors/noir/requests/req_srs_g1only.bin | Bin 0 -> 242 bytes .../noir/requests/req_verify_default.bin | Bin 0 -> 19443 bytes .../vectors/noir/requests/req_verify_evm.bin | Bin 0 -> 6904 bytes 25 files changed, 1948 insertions(+), 4 deletions(-) create mode 100644 src/crypto/barretenberg/VENDOR.md create mode 100644 src/crypto/barretenberg/aligned_alloc_macos.c create mode 100644 src/noir.zig create mode 100644 src/noir/msgpack.zig create mode 100644 tests/noir_vectors_test.zig create mode 100644 tests/vectors/noir/Prover.toml create mode 100644 tests/vectors/noir/README.md create mode 100644 tests/vectors/noir/keccak/proof create mode 100644 tests/vectors/noir/keccak/public_inputs create mode 100644 tests/vectors/noir/keccak/vk create mode 100644 tests/vectors/noir/keccak/vk_hash create mode 100644 tests/vectors/noir/main.nr create mode 100644 tests/vectors/noir/poseidon2/proof create mode 100644 tests/vectors/noir/poseidon2/public_inputs create mode 100644 tests/vectors/noir/poseidon2/vk create mode 100644 tests/vectors/noir/poseidon2/vk_hash create mode 100644 tests/vectors/noir/requests/req_srs_g1only.bin create mode 100644 tests/vectors/noir/requests/req_verify_default.bin create mode 100644 tests/vectors/noir/requests/req_verify_evm.bin diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2961a16..cd9bd10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cced88..e25add1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/ @@ -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 @@ -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 diff --git a/README.md b/README.md index b7c745b..85967a3 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 | @@ -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): diff --git a/build.zig b/build.zig index d27e359..76d2a77 100644 --- a/build.zig +++ b/build.zig @@ -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"), @@ -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 @@ -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, }); @@ -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, .{}); @@ -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", @@ -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"}, + }); + } +} diff --git a/build.zig.zon b/build.zig.zon index d4eed87..c839a4e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/src/crypto/barretenberg/VENDOR.md b/src/crypto/barretenberg/VENDOR.md new file mode 100644 index 0000000..14f3cd9 --- /dev/null +++ b/src/crypto/barretenberg/VENDOR.md @@ -0,0 +1,126 @@ +# Barretenberg dependency (Noir UltraHonk verification) + +Unlike c-kzg and blst, Barretenberg is not vendored as source. It is a C++20 +code base whose static library is about 50 MB per target, so `src/noir.zig` +consumes the prebuilt release archives as lazy Zig package dependencies +declared in `build.zig.zon`. Nothing is downloaded, linked or tested unless +the build is invoked with `-Dnoir=true`, and then only the archive for the +target being built is fetched (sha256-pinned through Zig's package hash). + +## Barretenberg + +- Source: https://github.com/AztecProtocol/aztec-packages (directory + `barretenberg/`); releases are published from the mirror + https://github.com/AztecProtocol/barretenberg/releases +- Pinned release: **v5.2.0** (2026-08-17) +- License: **Apache-2.0** (`barretenberg/LICENSE` in aztec-packages). The + archives are consumed unmodified; this repository stays MIT. +- Paired Noir toolchain: nargo **1.0.0-beta.25** (the `noir-repo` submodule at + tag v5.2.0 is exactly that release). The test vectors under + `tests/vectors/noir/` were produced with this pairing. + +Each archive contains a single file, `libbb-external.a`, and no headers. Only +the C symbols `bbapi` and `bbfree` are used; the wire format they speak +(msgpack `NamedUnion` commands) is implemented in `src/noir/msgpack.zig` and +`src/noir.zig`. + +| build.zig.zon name | Release asset | Upstream SHA-256 (release digest) | Zig package hash | +|------------------------------|--------------------------------------------|--------------------------------------------------------------------|----------------------------------------------------| +| `barretenberg_arm64_darwin` | `barretenberg-static-arm64-darwin.tar.gz` | `71e4e6c904cede582374d2aaf61530090b2d7f949943648835585a931c25e054` | `N-V-__8AAKAlJwOvxF9WMTWZ6_g3tK6kxoN1XqeG0j1bIxfK` | +| `barretenberg_amd64_darwin` | `barretenberg-static-amd64-darwin.tar.gz` | `5923ae204ecee4aa53476447e7d4fd6fb6361f993dd20425ed4df9c533951f95` | `N-V-__8AAEi5uATDH1uT4EsKM7BxD_qgy01S6bvSVntg5M_O` | +| `barretenberg_amd64_linux` | `barretenberg-static-amd64-linux.tar.gz` | `3726c437ef017d6a0c0d9b5f0e854197e364bc33bf7e7ccd20aee8cac7c12e6b` | `N-V-__8AABCloQVNS5sP_eXpnSbYWfq2xQBd4DQuiwYOKmDL` | +| `barretenberg_arm64_linux` | `barretenberg-static-arm64-linux.tar.gz` | `12e4db24d1f6e6bbbaec360ff8eeab2d4665afd8f40b1302e4ddae6a4065d098` | `N-V-__8AAB5oGQRtvwJEs6U1kH632yGzq9QqF7aGtv8LDL-y` | + +The Zig package hash covers the unpacked contents (the `.a` file), so the +build fails on any content change even if the URL still resolves. The upstream +digest column is what GitHub reports for the release asset +(`gh api repos/AztecProtocol/barretenberg/releases/tags/v5.2.0 --jq +'.assets[] | "\(.name) \(.digest)"'`) and lets a reviewer cross-check the +package hash against the artifact Aztec published. + +Upstream also ships `arm64-ios`, `arm64-ios-sim`, Android and `amd64-windows` +archives; they are not wired up. Windows is out of scope (the library allocates +responses with `_aligned_malloc` there, so the `bbfree` contract differs). + +CI (`.github/workflows/ci.yml`, job "Noir verify") exercises `ubuntu-latest` +(x86_64-linux) and `macos-latest` (aarch64-macos). The `arm64-linux` and +`amd64-darwin` archives are therefore validated out-of-band: run the update +procedure's step 6 on those hosts (or under emulation) at every release bump +and record the result in the pull request. + +## Linking + +- The archive is linked as an object file input together with libc++ and libc + (`link_libcpp`, `link_libc` on the module). Exceptions are enabled inside the + library and stay inside it for every post-decode failure, which the library + reports as an `ErrorResponse`. +- macOS only: `aligned_alloc_macos.c` provides a strong C11 `aligned_alloc`. + The archive carries a weak hidden definition of that libc-named symbol and + Zig's Mach-O linker binds it to libSystem's strict implementation, which + returns NULL for the non-multiple-of-alignment sizes the library requests. + See the comment in that file. +- The archives were built by upstream with Zig's own bundled clang, not a + distribution toolchain: `strings libbb-external.a` on the amd64-linux archive + shows `clang version 20.1.2 (https://github.com/ziglang/zig-bootstrap + c6bc9398c72c7a63fe9420a9055dcfd1845bc266)`, and upstream's wrappers pin + glibc 2.35 for Linux. Zig's bundled libc++ links them cleanly. +- libc++ version delta: the archives were compiled against libc++ 20.1 headers + while Zig 0.16 links libc++ 21.1 (`_LIBCPP_VERSION 210100`). libc++ is + ABI-stable across these releases in its default configuration, and only the + C symbols `bbapi` and `bbfree` cross the boundary (C++ exceptions are caught + inside the library on every post-decode path), so no C++ type crosses it. + Re-check this delta on any release or toolchain bump. + +## CRS + +Verification needs only the BN254 G1 generator and the trusted-setup G2 +element `[x]_2`. Both are embedded as constants in `src/noir.zig`, copied from +`barretenberg/cpp/src/barretenberg/srs/factories/bn254_crs_data.hpp`, and the +G2 bytes are checked against the SHA-256 upstream pins in the same header. No +CRS file or download is involved. The BN254 CRS factory is first-writer-wins +per process (`init_bn254_mem_crs_factory` returns early if one is already +installed), so the one-point CRS installed by `noir.init` cannot be enlarged +later; proving support would have to change `init`. + +### Why IPA (rollup) proofs are not supported + +`ProofSystemSettings.ipa_accumulation` is part of the wire struct, but no +`VerifierTarget` sets it and `verify` cannot honor it. An IPA-accumulating +proof (`bb --verifier_target noir-rollup`) dispatches to `UltraFlavor, RollupIO`, +whose verifier calls `verify_ipa` and constructs a Grumpkin verifier +commitment key of `1 << CONST_ECCVM_LOG_N` = 32768 points. That reaches +`get_grumpkin_crs_factory()`, which throws unless a Grumpkin CRS was installed; +`noir.init` never sends `SrsInitGrumpkinSrs`, so such a request comes back as +`error.ProofRejected` with "You need to initialize the global CRS with a call +to init_grumpkin_crs_factory". + +Supporting it would mean shipping or fetching 32768 x 64 = 2 MB of Grumpkin +points, which cannot be an embedded constant like the 192-byte BN254 pair and +would give up the offline property for that mode. One thing makes it easier +than it looks: unlike the BN254 factory, `init_grumpkin_mem_crs_factory` has no +first-writer-wins guard, so a Grumpkin CRS can be installed later in the +process without disturbing the BN254 one already in place. + +## Update procedure + +1. Pick the new release tag on https://github.com/AztecProtocol/barretenberg/releases + and note the Noir version it pairs with (`barretenberg/bbup/bb-versions.json` + or the `noir-repo` submodule commit at that tag). +2. For each of the four targets run + `zig fetch --save=barretenberg_ https://github.com/AztecProtocol/barretenberg/releases/download//barretenberg-static-.tar.gz` + and then re-add `.lazy = true` to each entry in `build.zig.zon` (`zig fetch` + drops it). +3. Record the new upstream digests and package hashes in the table above and + bump the release tag in `src/noir.zig` (`BARRETENBERG_VERSION`). +4. Re-validate the wire schema against the new release: the `SrsInitSrs`, + `CircuitVerify`, `ProofSystemSettings` and response structs in + `bbapi/bbapi_srs.hpp`, `bbapi/bbapi_ultra_honk.hpp` and + `bbapi/bbapi_shared.hpp`, or `bb msgpack schema`. The library has no runtime + schema check and rejects requests with missing fields by terminating the + process, so every field must be present with the exact snake_case name. +5. Regenerate `tests/vectors/noir/` with the paired nargo/bb (see the README + there) and refresh the reference request bytes. +6. Run `zig build test -Dnoir=true` on every wired-up host (macOS arm64, + macOS x86_64, Linux x86_64, Linux aarch64), plus the default `make ci`, + before committing. CI covers only the first and third, so record the other + two in the pull request. diff --git a/src/crypto/barretenberg/aligned_alloc_macos.c b/src/crypto/barretenberg/aligned_alloc_macos.c new file mode 100644 index 0000000..13e88ee --- /dev/null +++ b/src/crypto/barretenberg/aligned_alloc_macos.c @@ -0,0 +1,56 @@ +/* + * Strong C11 `aligned_alloc` for macOS builds that link Barretenberg. + * + * Barretenberg's common/mem.hpp defines its own `aligned_alloc` (a + * posix_memalign wrapper) as an inline function in the global namespace on + * Apple targets. Because already declares the C11 function, that + * definition takes C linkage and is emitted into every archive member as a + * weak, hidden ("weak private external") symbol. Zig's self-hosted Mach-O + * linker resolves the archive's references to libSystem's exported + * `aligned_alloc` instead of to the archive's own definition; Apple's ld binds + * them to the archive copy. libSystem's implementation is strict C11 and + * returns NULL when `size` is not a multiple of `alignment`, and Barretenberg + * calls it with sizes such as aligned_alloc(64, 19) when packing msgpack + * responses, so the first `bbapi` call would dereference NULL. + * + * Defining a strong, exported `aligned_alloc` in the executable makes every + * reference bind here instead. The implementation rounds `size` up to a + * multiple of `alignment` (so it also satisfies strict C11 callers) and + * delegates to posix_memalign; the memory is released with free()/bbfree as + * usual. This file is compiled only for macOS targets and only when the build + * links Barretenberg (-Dnoir=true); see build.zig. + */ +#include +#include +#include +#include + +void *aligned_alloc(size_t alignment, size_t size) { + /* posix_memalign requires a power of two that is a multiple of sizeof(void *). */ + if (alignment == 0 || (alignment & (alignment - 1)) != 0) { + errno = EINVAL; + return NULL; + } + if (alignment < sizeof(void *)) { + alignment = sizeof(void *); + } + size_t remainder = size % alignment; + if (remainder != 0) { + size_t padding = alignment - remainder; + if (size > SIZE_MAX - padding) { + errno = ENOMEM; + return NULL; + } + size += padding; + } + if (size == 0) { + size = alignment; + } + void *ptr = NULL; + int rc = posix_memalign(&ptr, alignment, size); + if (rc != 0) { + errno = rc; + return NULL; + } + return ptr; +} diff --git a/src/noir.zig b/src/noir.zig new file mode 100644 index 0000000..15c5c99 --- /dev/null +++ b/src/noir.zig @@ -0,0 +1,672 @@ +//! Offline verification of Noir UltraHonk proofs through Barretenberg's C ABI. +//! +//! Opt-in: build with `-Dnoir=true`. That flag makes build.zig fetch the pinned +//! Barretenberg v5.2.0 `libbb-external.a` for the host target as a lazy +//! package dependency and link it together with libc++. Without the flag this +//! file is still part of the module, but every entry point that touches the +//! library fails to compile with a clear message when referenced, and nothing +//! here is linked or tested. Supported targets: arm64 and x86_64 macOS, +//! x86_64 Linux and arm64 Linux (the release assets that exist for those +//! hosts). +//! +//! What is verified: UltraHonk proofs written by `bb prove` (bb 5.2.0 paired +//! with nargo 1.0.0-beta.25) in the poseidon2 (`noir-recursive`, the CLI +//! default) and keccak (`evm`) oracle-hash flavors, with or without ZK. +//! IPA-accumulating rollup proofs (`bb --verifier_target noir-rollup`) are +//! NOT supported: they additionally need a 32768-point Grumpkin CRS that +//! `init` does not install (see `Settings.ipa_accumulation`). Proving, +//! verification-key computation and Solidity verifier generation are also not +//! part of this module. +//! +//! Usage: +//! ```zig +//! try noir.init(); +//! const proof = try noir.fieldsFromBytes(proof_file_bytes); // bb `proof` +//! const inputs = try noir.fieldsFromBytes(public_inputs_file_bytes); // bb `public_inputs` +//! const ok = try noir.verify(allocator, vk_file_bytes, inputs, proof, .fromVerifierTarget(.evm)); +//! ``` +//! +//! Wire protocol: `bbapi` takes one msgpack-encoded command and returns one +//! msgpack-encoded response (see noir/msgpack.zig). Requests are +//! `[[name, {field: value}]]`; responses `[name, {field: value}]`. The library +//! requires every struct field to be present and decodes requests outside its +//! exception guard, so the request encoders below emit exactly the canonical +//! bytes the library was confirmed to accept. The response decoder rejects +//! anything whose shape does not match the pinned v5.2.0 schema -- wrong +//! envelope, unknown response name, wrong field name or count, trailing bytes +//! -- as `error.UnexpectedResponse`, while accepting any msgpack encoding of +//! that shape (see noir/msgpack.zig on canonical form). +//! +//! CRS: verification needs only two trusted-setup elements, the BN254 G1 +//! generator and the G2 element `[x]_2`, both embedded below as constants +//! (192 bytes, taken from barretenberg's `srs/factories/bn254_crs_data.hpp`). +//! No CRS file is read and no network is used. Barretenberg's global CRS +//! factory is first-writer-wins for the lifetime of the process: the one-point +//! G1 CRS that `init` installs can never be replaced or enlarged afterwards, +//! so a future proving milestone must size the CRS in `init` rather than add a +//! second initializer. +//! +//! Concurrency: the library keeps unsynchronized per-process request state, +//! so every `bbapi` call is serialized behind one mutex. `init` is guarded by +//! an atomic once-flag and may be called from any number of threads. + +const std = @import("std"); +const build_options = @import("build_options"); +const msgpack = @import("noir/msgpack.zig"); +const runtime = @import("runtime.zig"); + +/// True when the build links Barretenberg (`-Dnoir=true`). +pub const enabled: bool = build_options.noir; + +/// Barretenberg release this module is pinned to. The msgpack schema used +/// below is validated against exactly this release; see +/// src/crypto/barretenberg/VENDOR.md for the update procedure. +pub const BARRETENBERG_VERSION = "v5.2.0"; + +// ============================================================================ +// Embedded CRS constants +// ============================================================================ + +/// BN254 G1 generator (x = 1, y = 2) in barretenberg's affine serialization: +/// `x || y`, each a 32-byte big-endian field element. This is the first G1 +/// element of the Aztec CRS and the only G1 point verification needs. +pub const BN254_G1_GENERATOR: [64]u8 = ([_]u8{0} ** 31) ++ [_]u8{1} ++ ([_]u8{0} ** 31) ++ [_]u8{2}; + +/// The BN254 trusted-setup G2 element `[x]_2` (128 bytes), verbatim from +/// barretenberg `srs/factories/bn254_crs_data.hpp` (`BN254_G2_ELEMENT_BYTES`). +pub const BN254_G2_ELEMENT: [128]u8 = .{ + 0x01, 0x18, 0xc4, 0xd5, 0xb8, 0x37, 0xbc, 0xc2, 0xbc, 0x89, 0xb5, 0xb3, 0x98, 0xb5, 0x97, 0x4e, + 0x9f, 0x59, 0x44, 0x07, 0x3b, 0x32, 0x07, 0x8b, 0x7e, 0x23, 0x1f, 0xec, 0x93, 0x88, 0x83, 0xb0, + 0x26, 0x0e, 0x01, 0xb2, 0x51, 0xf6, 0xf1, 0xc7, 0xe7, 0xff, 0x4e, 0x58, 0x07, 0x91, 0xde, 0xe8, + 0xea, 0x51, 0xd8, 0x7a, 0x35, 0x8e, 0x03, 0x8b, 0x4e, 0xfe, 0x30, 0xfa, 0xc0, 0x93, 0x83, 0xc1, + 0x22, 0xfe, 0xbd, 0xa3, 0xc0, 0xc0, 0x63, 0x2a, 0x56, 0x47, 0x5b, 0x42, 0x14, 0xe5, 0x61, 0x5e, + 0x11, 0xe6, 0xdd, 0x3f, 0x96, 0xe6, 0xce, 0xa2, 0x85, 0x4a, 0x87, 0xd4, 0xda, 0xcc, 0x5e, 0x55, + 0x04, 0xfc, 0x63, 0x69, 0xf7, 0x11, 0x0f, 0xe3, 0xd2, 0x51, 0x56, 0xc1, 0xbb, 0x9a, 0x72, 0x85, + 0x9c, 0xf2, 0xa0, 0x46, 0x41, 0xf9, 0x9b, 0xa4, 0xee, 0x41, 0x3c, 0x80, 0xda, 0x6a, 0x5f, 0xe4, +}; + +/// SHA-256 of `BN254_G2_ELEMENT`, pinned upstream as `BN254_G2_ELEMENT_SHA256` +/// in the same header. Checked by a unit test so a transcription error in the +/// constant above cannot go unnoticed. +pub const BN254_G2_ELEMENT_SHA256: [32]u8 = .{ + 0x01, 0x79, 0x7b, 0xfc, 0x4d, 0xe5, 0xa9, 0x6f, 0x0e, 0x51, 0x6a, 0x9e, 0xa4, 0x53, 0x7d, 0x18, + 0x78, 0x6d, 0xc3, 0x0c, 0xb9, 0x91, 0xac, 0xa4, 0x27, 0x4c, 0x95, 0x82, 0x2b, 0x69, 0xc3, 0x2f, +}; + +// ============================================================================ +// C ABI (libbb-external.a) +// ============================================================================ + +/// The single native entrypoint. `output_out`/`output_len_out` are in-out: on +/// entry they describe an optional caller-provided scratch buffer (we always +/// pass null/0), on return they describe the response, which the library +/// allocated and which must be released with `bbfree`. +extern fn bbapi(input_in: [*]const u8, input_len_in: usize, output_out: *?[*]u8, output_len_out: *usize) void; +extern fn bbfree(ptr: ?*anyopaque) void; + +// ============================================================================ +// Public types +// ============================================================================ + +pub const NoirError = error{ + /// `init` has not completed successfully in this process. + NotInitialized, + /// Barretenberg rejected the inputs before or during deserialization and + /// answered `CircuitVerify` with an `ErrorResponse`: a non-canonical field + /// element, a proof element that is not a curve point, a public-input + /// count that disagrees with the verification key, or a verification key + /// that does not deserialize. The message is available through + /// `verifyDiag`. Like `false`, this means "not verified"; it says the + /// library never got as far as a verdict, not that the proof was + /// well-formed. + ProofRejected, + /// Barretenberg answered `SrsInitSrs` with an `ErrorResponse`. + SrsInitFailed, + /// The response bytes do not match the pinned v5.2.0 schema (wrong + /// envelope, unknown response name, missing or extra fields, trailing + /// bytes). Indicates a library/binding version mismatch. + UnexpectedResponse, + /// `bbapi` returned no output buffer at all. + NoResponse, + OutOfMemory, + /// A request component exceeds the msgpack 32-bit length limit. + TooLong, +}; + +/// Oracle hash used by the prover's Fiat-Shamir transcript. Must match the +/// flavor the proof was generated with. Barretenberg also knows `starknet`, +/// but it is compiled out of the release static libraries. +pub const OracleHash = enum { + poseidon2, + keccak, +}; + +/// Mirror of Barretenberg's `ProofSystemSettings`; the field names and order +/// are the wire schema. Defaults match `bb` with no `--verifier_target`. +pub const Settings = struct { + /// Proof carries an IPA accumulator (rollup circuits). Part of the wire + /// struct, but `verify` cannot honor it: an IPA proof is checked against a + /// 32768-point Grumpkin CRS that `init` does not install, so Barretenberg + /// answers such a request with `error.ProofRejected` ("You need to + /// initialize the global CRS with a call to init_grumpkin_crs_factory"). + /// No `VerifierTarget` sets it; see src/crypto/barretenberg/VENDOR.md for + /// what supporting it would cost. + ipa_accumulation: bool = false, + oracle_hash_type: OracleHash = .poseidon2, + /// The proof was generated without blinding (`*-no-zk` targets). + disable_zk: bool = false, + /// Barretenberg's experimental Solidity-verifier layout; not used by any + /// `--verifier_target`, kept so the wire struct is complete. + optimized_solidity_verifier: bool = false, + + /// The `bb --verifier_target` values this module can verify. `default` is + /// what `bb` uses when the flag is absent (poseidon2, ZK, no IPA), which + /// is identical to `noir_recursive`. + /// + /// bb also accepts `noir-rollup`/`noir-rollup-no-zk` (IPA accumulation) + /// and `starknet`/`starknet-no-zk`. They are deliberately absent: the + /// rollup targets need a Grumpkin CRS `init` does not install (see + /// `ipa_accumulation`), and the starknet flavors are compiled out of the + /// release static libraries. + pub const VerifierTarget = enum { + default, + evm, + evm_no_zk, + noir_recursive, + noir_recursive_no_zk, + }; + + /// Same mapping as `bb`'s CLI applies to `--verifier_target`. + pub fn fromVerifierTarget(target: VerifierTarget) Settings { + return switch (target) { + .default, .noir_recursive => .{}, + .noir_recursive_no_zk => .{ .disable_zk = true }, + .evm => .{ .oracle_hash_type = .keccak }, + .evm_no_zk => .{ .oracle_hash_type = .keccak, .disable_zk = true }, + }; + } +}; + +/// Receives Barretenberg's error message when `verifyDiag` fails with +/// `error.ProofRejected`. Fixed-size so no allocator is involved. +pub const Diagnostics = struct { + pub const max_message_len = 256; + + buf: [max_message_len]u8 = undefined, + len: usize = 0, + + /// The last `ErrorResponse` message (possibly truncated), or empty. + pub fn message(self: *const Diagnostics) []const u8 { + return self.buf[0..self.len]; + } + + fn set(self: *Diagnostics, msg: []const u8) void { + const n = @min(msg.len, max_message_len); + @memcpy(self.buf[0..n], msg[0..n]); + self.len = n; + } +}; + +// ============================================================================ +// Lifecycle (process-global, init-once) +// ============================================================================ + +const State = enum(u8) { uninit, initializing, ready }; + +var init_state: std.atomic.Value(u8) = .init(@intFromEnum(State.uninit)); + +/// Serializes every `bbapi` call (the library's request state is a process +/// global with no lock of its own). The mutex only needs an `Io` for futex +/// waits on contention, so the library's blocking `Io` is used here; nothing +/// else about the call touches I/O. +var api_mutex: std.Io.Mutex = .init; + +fn loadState() State { + return @enumFromInt(init_state.load(.acquire)); +} + +/// Install the verification CRS (G1 generator + pinned G2) into Barretenberg. +/// Idempotent and thread-safe: the first caller performs the `SrsInitSrs` +/// call, concurrent callers wait for it, later callers return immediately. If +/// the winner fails, the state is rolled back so a later call can retry. +/// +/// Must complete before `verify`. The installed CRS holds a single G1 point, +/// which is sufficient for verification only (see the module docs on the +/// first-writer-wins constraint). +pub fn init() NoirError!void { + if (comptime !enabled) @compileError("eth.noir requires building with -Dnoir=true (links Barretenberg " ++ BARRETENBERG_VERSION ++ ")"); + + while (true) { + switch (loadState()) { + .ready => return, + .initializing => std.atomic.spinLoopHint(), + .uninit => { + if (init_state.cmpxchgStrong( + @intFromEnum(State.uninit), + @intFromEnum(State.initializing), + .acquire, + .acquire, + ) == null) break; + }, + } + } + + // We own initialization. Roll back on failure so another caller can retry. + errdefer init_state.store(@intFromEnum(State.uninit), .release); + + // The request is 242 bytes; encode it on the stack. + var stack: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&stack); + const request = try encodeSrsInitSrsRequest(fba.allocator(), &BN254_G1_GENERATOR, 1, &BN254_G2_ELEMENT); + + const response = try call(request); + defer response.deinit(); + try parseSrsInitResponse(response.bytes(), null); + + init_state.store(@intFromEnum(State.ready), .release); +} + +/// Whether `init` has completed successfully in this process. +pub fn isInitialized() bool { + return loadState() == .ready; +} + +// ============================================================================ +// Verification +// ============================================================================ + +/// Verify an UltraHonk proof against its verification key. +/// +/// - `vk`: the raw bytes of bb's `vk` output. +/// - `public_inputs` / `proof`: 32-byte big-endian field elements, exactly as +/// bb writes them; use `fieldsFromBytes` to view the raw files this way. +/// - `settings`: must match the flavor used at proving time +/// (`Settings.fromVerifierTarget`). +/// +/// Only `true` means verified. Both failure shapes mean "not verified", and +/// which one you get depends on how far Barretenberg got: +/// +/// - `false`: Barretenberg answered `verified=false` (a proof that does not +/// satisfy the circuit, a wrong public-input *value*, a wrong proof or VK +/// size, a flavor mismatch). +/// - `error.ProofRejected`: Barretenberg rejected the inputs before or during +/// deserialization (a non-canonical field element, a point that is not on +/// the curve, a public-input *count* that disagrees with the VK, a VK that +/// does not deserialize). `verifyDiag` exposes the library's message. +/// +/// Callers deciding whether to accept a proof must treat the error as a +/// rejection, not as an internal fault: which of the two an invalid input +/// produces is not a stable property of the input. +/// +/// `allocator` is used for one transient request buffer that is freed before +/// returning. +pub fn verify( + allocator: std.mem.Allocator, + vk: []const u8, + public_inputs: []const [32]u8, + proof: []const [32]u8, + settings: Settings, +) NoirError!bool { + return verifyDiag(allocator, vk, public_inputs, proof, settings, null); +} + +/// `verify` with an optional sink for Barretenberg's error message. +pub fn verifyDiag( + allocator: std.mem.Allocator, + vk: []const u8, + public_inputs: []const [32]u8, + proof: []const [32]u8, + settings: Settings, + diag: ?*Diagnostics, +) NoirError!bool { + if (comptime !enabled) @compileError("eth.noir requires building with -Dnoir=true (links Barretenberg " ++ BARRETENBERG_VERSION ++ ")"); + + if (diag) |d| d.len = 0; + // Guard in Zig: calling the library before the CRS is installed makes it + // throw internally, and we do not want to rely on how it reports that. + if (loadState() != .ready) return error.NotInitialized; + + const request = try encodeCircuitVerifyRequest(allocator, vk, public_inputs, proof, settings); + defer allocator.free(request); + + const response = try call(request); + defer response.deinit(); + return parseVerifyResponse(response.bytes(), diag); +} + +/// View a concatenation of 32-byte field elements (bb's `proof` and +/// `public_inputs` files) as a slice of fields, without copying. +pub fn fieldsFromBytes(bytes: []const u8) error{InvalidLength}![]const [32]u8 { + if (bytes.len % 32 != 0) return error.InvalidLength; + if (bytes.len == 0) return &[0][32]u8{}; + const fields: [*]const [32]u8 = @ptrCast(bytes.ptr); + return fields[0 .. bytes.len / 32]; +} + +// ============================================================================ +// Wire encoding (exposed for byte-for-byte tests against bb reference requests) +// ============================================================================ + +/// Encode `SrsInitSrs{points_buf, num_points, g2_point}` wrapped in the +/// single-argument tuple `bbapi` expects. Caller owns the result. +pub fn encodeSrsInitSrsRequest( + allocator: std.mem.Allocator, + points_buf: []const u8, + num_points: u32, + g2_point: *const [128]u8, +) msgpack.EncodeError![]u8 { + var enc = msgpack.Encoder.init(allocator); + errdefer enc.deinit(); + try enc.writeArrayHeader(1); // std::tuple + try enc.writeArrayHeader(2); // NamedUnion: [name, fields] + try enc.writeStr("SrsInitSrs"); + try enc.writeMapHeader(3); + try enc.writeStr("points_buf"); + try enc.writeBin(points_buf); + try enc.writeStr("num_points"); + try enc.writeUint(num_points); + try enc.writeStr("g2_point"); + try enc.writeBin(g2_point); + return enc.toOwnedSlice(); +} + +/// Encode `CircuitVerify{verification_key, public_inputs, proof, settings}` +/// wrapped in the single-argument tuple `bbapi` expects. Caller owns the result. +pub fn encodeCircuitVerifyRequest( + allocator: std.mem.Allocator, + vk: []const u8, + public_inputs: []const [32]u8, + proof: []const [32]u8, + settings: Settings, +) msgpack.EncodeError![]u8 { + var enc = msgpack.Encoder.init(allocator); + errdefer enc.deinit(); + try enc.writeArrayHeader(1); + try enc.writeArrayHeader(2); + try enc.writeStr("CircuitVerify"); + try enc.writeMapHeader(4); + try enc.writeStr("verification_key"); + try enc.writeBin(vk); + try enc.writeStr("public_inputs"); + try writeFields(&enc, public_inputs); + try enc.writeStr("proof"); + try writeFields(&enc, proof); + try enc.writeStr("settings"); + try enc.writeMapHeader(4); + try enc.writeStr("ipa_accumulation"); + try enc.writeBool(settings.ipa_accumulation); + try enc.writeStr("oracle_hash_type"); + try enc.writeStr(@tagName(settings.oracle_hash_type)); + try enc.writeStr("disable_zk"); + try enc.writeBool(settings.disable_zk); + try enc.writeStr("optimized_solidity_verifier"); + try enc.writeBool(settings.optimized_solidity_verifier); + return enc.toOwnedSlice(); +} + +/// `std::vector` encodes as an array of 32-byte bin values. +fn writeFields(enc: *msgpack.Encoder, fields: []const [32]u8) msgpack.EncodeError!void { + try enc.writeArrayHeader(fields.len); + for (fields) |*field| try enc.writeBin(field); +} + +// ============================================================================ +// Calling the library and decoding responses +// ============================================================================ + +/// A response buffer owned by Barretenberg. +const Response = struct { + ptr: [*]u8, + len: usize, + + fn bytes(self: Response) []const u8 { + return self.ptr[0..self.len]; + } + + fn deinit(self: Response) void { + bbfree(@ptrCast(self.ptr)); + } +}; + +fn call(request: []const u8) NoirError!Response { + const io = runtime.blockingIo(); + api_mutex.lockUncancelable(io); + defer api_mutex.unlock(io); + + var out: ?[*]u8 = null; + var out_len: usize = 0; + bbapi(request.ptr, request.len, &out, &out_len); + const ptr = out orelse return error.NoResponse; + return .{ .ptr = ptr, .len = out_len }; +} + +const Envelope = struct { + name: []const u8, + field_count: usize, +}; + +/// Read the `[name, {...}]` response envelope up to the map header. +fn readEnvelope(dec: *msgpack.Decoder) NoirError!Envelope { + const outer = dec.readArrayHeader() catch return error.UnexpectedResponse; + if (outer != 2) return error.UnexpectedResponse; + const name = dec.readStr() catch return error.UnexpectedResponse; + const field_count = dec.readMapHeader() catch return error.UnexpectedResponse; + return .{ .name = name, .field_count = field_count }; +} + +/// Read the single `message` field of an `ErrorResponse`, record it, and +/// return `err`. +fn readErrorResponse(dec: *msgpack.Decoder, envelope: Envelope, diag: ?*Diagnostics, err: NoirError) NoirError { + if (envelope.field_count != 1) return error.UnexpectedResponse; + const key = dec.readStr() catch return error.UnexpectedResponse; + if (!std.mem.eql(u8, key, "message")) return error.UnexpectedResponse; + const message = dec.readStr() catch return error.UnexpectedResponse; + if (!dec.finished()) return error.UnexpectedResponse; + if (diag) |d| d.set(message); + return err; +} + +/// Decode `["CircuitVerifyResponse", {"verified": bool}]` or an `ErrorResponse`. +fn parseVerifyResponse(bytes: []const u8, diag: ?*Diagnostics) NoirError!bool { + var dec = msgpack.Decoder.init(bytes); + const envelope = try readEnvelope(&dec); + if (std.mem.eql(u8, envelope.name, "ErrorResponse")) { + return readErrorResponse(&dec, envelope, diag, error.ProofRejected); + } + if (!std.mem.eql(u8, envelope.name, "CircuitVerifyResponse")) return error.UnexpectedResponse; + if (envelope.field_count != 1) return error.UnexpectedResponse; + const key = dec.readStr() catch return error.UnexpectedResponse; + if (!std.mem.eql(u8, key, "verified")) return error.UnexpectedResponse; + const verified = dec.readBool() catch return error.UnexpectedResponse; + if (!dec.finished()) return error.UnexpectedResponse; + return verified; +} + +/// Decode `["SrsInitSrsResponse", {"points_buf": bin}]` or an `ErrorResponse`. +fn parseSrsInitResponse(bytes: []const u8, diag: ?*Diagnostics) NoirError!void { + var dec = msgpack.Decoder.init(bytes); + const envelope = try readEnvelope(&dec); + if (std.mem.eql(u8, envelope.name, "ErrorResponse")) { + return readErrorResponse(&dec, envelope, diag, error.SrsInitFailed); + } + if (!std.mem.eql(u8, envelope.name, "SrsInitSrsResponse")) return error.UnexpectedResponse; + if (envelope.field_count != 1) return error.UnexpectedResponse; + const key = dec.readStr() catch return error.UnexpectedResponse; + if (!std.mem.eql(u8, key, "points_buf")) return error.UnexpectedResponse; + _ = dec.readBin() catch return error.UnexpectedResponse; + if (!dec.finished()) return error.UnexpectedResponse; +} + +// ============================================================================ +// Tests +// ============================================================================ +// +// The proof vectors and the byte-for-byte request comparisons live in +// tests/noir_vectors_test.zig (a separate test binary that embeds +// tests/vectors/noir/). The tests here cover the lifecycle guard, the +// once-flag under contention, the settings mapping, the embedded constants +// and the response decoder. They only run when the build links Barretenberg +// (root.zig gates the import on the `noir` build option). + +const testing = std.testing; + +// Must stay the first test in this file: it relies on no earlier test in this +// process having initialized the library, and the CRS cannot be uninstalled. +test "noir: verify before init returns NotInitialized without touching the library" { + try testing.expect(!isInitialized()); + try testing.expectError(error.NotInitialized, verify(testing.allocator, "", &.{}, &.{}, .{})); +} + +test "noir: init is idempotent under concurrent callers" { + const thread_count = 8; + const Worker = struct { + fn run(ok: *bool) void { + init() catch return; + ok.* = true; + } + }; + var ok: [thread_count]bool = @splat(false); + var threads: [thread_count]std.Thread = undefined; + var spawned: usize = 0; + for (0..thread_count) |i| { + threads[i] = std.Thread.spawn(.{}, Worker.run, .{&ok[i]}) catch break; + spawned += 1; + } + for (threads[0..spawned]) |t| t.join(); + for (ok[0..spawned]) |flag| try testing.expect(flag); + try testing.expect(isInitialized()); + // A later call is a no-op that still succeeds. + try init(); + try testing.expect(isInitialized()); +} + +test "noir: empty inputs after init are answered, not crashed on" { + try init(); + var diag: Diagnostics = .{}; + const result = verifyDiag(testing.allocator, "", &.{}, &.{}, .{}, &diag) catch |err| switch (err) { + error.ProofRejected => false, + else => return err, + }; + try testing.expect(!result); +} + +test "noir: Settings.fromVerifierTarget mirrors bb --verifier_target" { + const S = Settings; + try testing.expectEqual(S{}, S.fromVerifierTarget(.default)); + try testing.expectEqual(S{}, S.fromVerifierTarget(.noir_recursive)); + try testing.expectEqual(S{ .disable_zk = true }, S.fromVerifierTarget(.noir_recursive_no_zk)); + try testing.expectEqual(S{ .oracle_hash_type = .keccak }, S.fromVerifierTarget(.evm)); + try testing.expectEqual(S{ .oracle_hash_type = .keccak, .disable_zk = true }, S.fromVerifierTarget(.evm_no_zk)); + // Every target leaves the experimental Solidity layout off. + try testing.expect(!S.fromVerifierTarget(.evm).optimized_solidity_verifier); +} + +test "noir: no VerifierTarget requests IPA accumulation" { + // `init` installs a BN254 verification CRS only. An IPA proof is checked + // against a 32768-point Grumpkin CRS, which Barretenberg has no way to + // obtain here, so offering a rollup target would promise a mode that + // always fails. This pins that the gap cannot silently reopen: adding a + // rollup variant to `VerifierTarget` must fail this test (and come with a + // Grumpkin CRS in `init`). + inline for (@typeInfo(Settings.VerifierTarget).@"enum".fields) |field| { + const settings = Settings.fromVerifierTarget(@field(Settings.VerifierTarget, field.name)); + try testing.expect(!settings.ipa_accumulation); + try testing.expect(std.mem.indexOf(u8, field.name, "rollup") == null); + } + // The wire field itself stays, because it is part of the pinned schema. + try testing.expect(@hasField(Settings, "ipa_accumulation")); +} + +test "noir: embedded G2 element matches the upstream SHA-256 pin" { + var digest: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(&BN254_G2_ELEMENT, &digest, .{}); + try testing.expectEqualSlices(u8, &BN254_G2_ELEMENT_SHA256, &digest); +} + +test "noir: embedded G1 generator is (1, 2)" { + try testing.expectEqual(@as(u8, 1), BN254_G1_GENERATOR[31]); + try testing.expectEqual(@as(u8, 2), BN254_G1_GENERATOR[63]); + var nonzero: usize = 0; + for (BN254_G1_GENERATOR) |b| nonzero += @intFromBool(b != 0); + try testing.expectEqual(@as(usize, 2), nonzero); +} + +test "noir: SrsInitSrs request has the documented shape and leaks nothing" { + const request = try encodeSrsInitSrsRequest(testing.allocator, &BN254_G1_GENERATOR, 1, &BN254_G2_ELEMENT); + defer testing.allocator.free(request); + try testing.expectEqual(@as(usize, 242), request.len); + // [[ "SrsInitSrs", { "points_buf": bin8(64) ... + const prefix = "\x91\x92\xaaSrsInitSrs\x83\xaapoints_buf\xc4\x40"; + try testing.expectEqualSlices(u8, prefix, request[0..prefix.len]); + // ... "num_points": 1, "g2_point": bin8(128) <128 bytes> ]] + const tail_key = "\xaanum_points\x01\xa8g2_point\xc4\x80"; + const tail_start = prefix.len + 64; + try testing.expectEqualSlices(u8, tail_key, request[tail_start .. tail_start + tail_key.len]); + try testing.expectEqualSlices(u8, &BN254_G2_ELEMENT, request[request.len - 128 ..]); +} + +test "noir: CircuitVerify request encodes settings by name and fields as bin32" { + const vk = [_]u8{0xab} ** 3; + const fields = [_][32]u8{ [_]u8{0x11} ** 32, [_]u8{0x22} ** 32 }; + const request = try encodeCircuitVerifyRequest(testing.allocator, &vk, fields[0..1], &fields, .fromVerifierTarget(.evm_no_zk)); + defer testing.allocator.free(request); + const expected = "\x91\x92\xadCircuitVerify\x84" ++ + "\xb0verification_key\xc4\x03\xab\xab\xab" ++ + "\xadpublic_inputs\x91\xc4\x20" ++ ("\x11" ** 32) ++ + "\xa5proof\x92\xc4\x20" ++ ("\x11" ** 32) ++ "\xc4\x20" ++ ("\x22" ** 32) ++ + "\xa8settings\x84" ++ + "\xb0ipa_accumulation\xc2" ++ + "\xb0oracle_hash_type\xa6keccak" ++ + "\xaadisable_zk\xc3" ++ + "\xbboptimized_solidity_verifier\xc2"; + try testing.expectEqualSlices(u8, expected, request); +} + +test "noir: fieldsFromBytes splits on 32-byte boundaries" { + const raw = [_]u8{7} ** 96; + const fields = try fieldsFromBytes(&raw); + try testing.expectEqual(@as(usize, 3), fields.len); + try testing.expectEqualSlices(u8, raw[32..64], &fields[1]); + try testing.expectEqual(@as(usize, 0), (try fieldsFromBytes("")).len); + try testing.expectError(error.InvalidLength, fieldsFromBytes(raw[0..33])); +} + +test "noir: response decoder accepts the pinned CircuitVerifyResponse shape" { + const yes = "\x92\xb5CircuitVerifyResponse\x81\xa8verified\xc3"; + try testing.expectEqual(@as(usize, 34), yes.len); + try testing.expect(try parseVerifyResponse(yes, null)); + const no = "\x92\xb5CircuitVerifyResponse\x81\xa8verified\xc2"; + try testing.expect(!try parseVerifyResponse(no, null)); +} + +test "noir: response decoder maps ErrorResponse to ProofRejected with the message" { + const err = "\x92\xadErrorResponse\x81\xa7message\xb3point not on curve!"; + var diag: Diagnostics = .{}; + try testing.expectError(error.ProofRejected, parseVerifyResponse(err, &diag)); + try testing.expectEqualStrings("point not on curve!", diag.message()); + // SrsInitSrs errors get their own tag. + try testing.expectError(error.SrsInitFailed, parseSrsInitResponse(err, null)); +} + +test "noir: response decoder rejects schema drift instead of guessing" { + var diag: Diagnostics = .{}; + // Unknown response name. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x92\xa9Something\x80", &diag)); + // Right name, extra field. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x92\xb5CircuitVerifyResponse\x82\xa8verified\xc3\xa5extra\xc0", &diag)); + // Right name, renamed field. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x92\xb5CircuitVerifyResponse\x81\xa2ok\xc3", &diag)); + // Right name, wrong value type. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x92\xb5CircuitVerifyResponse\x81\xa8verified\x01", &diag)); + // Trailing bytes. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x92\xb5CircuitVerifyResponse\x81\xa8verified\xc3\xc0", &diag)); + // Truncated and empty. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x92\xb5CircuitVerifyResponse\x81\xa8verif", &diag)); + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("", &diag)); + // Envelope of the wrong arity. + try testing.expectError(error.UnexpectedResponse, parseVerifyResponse("\x91\xb5CircuitVerifyResponse", &diag)); + // SrsInitSrsResponse with a non-bin payload. + try testing.expectError(error.UnexpectedResponse, parseSrsInitResponse("\x92\xb2SrsInitSrsResponse\x81\xaapoints_buf\xa0", &diag)); + // A well-formed SrsInitSrsResponse. + try parseSrsInitResponse("\x92\xb2SrsInitSrsResponse\x81\xaapoints_buf\xc4\x00", &diag); +} diff --git a/src/noir/msgpack.zig b/src/noir/msgpack.zig new file mode 100644 index 0000000..d8ff28a --- /dev/null +++ b/src/noir/msgpack.zig @@ -0,0 +1,587 @@ +//! Minimal MessagePack subset used to talk to Barretenberg's `bbapi` C entrypoint. +//! +//! Only the families the bbapi request/response shapes need are implemented: +//! nil, bool, unsigned integers, str, bin, array and map (`skipValue` also +//! steps over int, float and ext values so an unexpected field can be skipped +//! deterministically). The encoder always emits the canonical shortest form, +//! exactly as msgpack-c does for these types; this matters because +//! Barretenberg v5.2.0 decodes requests outside its exception guard and a +//! malformed request terminates the host process rather than returning an +//! error. +//! +//! The decoder is strict about family and bounds, not about canonical form: +//! every typed read checks the head byte and returns `error.TypeMismatch` on +//! any other family, and every length is bounds-checked (`error.Truncated`), +//! so a schema change in a future library version surfaces as a Zig error +//! instead of undefined behavior. It does accept any encoding of the right +//! family, so a value written in a longer form than necessary (`str8` for a +//! short string, `map16` for a one-entry map) decodes normally. That is +//! deliberate: responses come from the linked library, and accepting a +//! non-minimal encoding of the expected shape cannot turn a rejection into an +//! acceptance. Only the encoder needs to be canonical. +//! +//! Spec: https://github.com/msgpack/msgpack/blob/master/spec.md + +const std = @import("std"); + +pub const EncodeError = std.mem.Allocator.Error || error{ + /// A string, binary blob, array or map is longer than 2^32 - 1 elements. + TooLong, +}; + +pub const DecodeError = error{ + /// The input ended inside a value. + Truncated, + /// The head byte does not belong to the family the caller asked for. + TypeMismatch, + /// The reserved head byte 0xc1 was encountered. + Reserved, + /// Nested containers deeper than `max_depth` while skipping a value. + TooDeep, +}; + +/// Maximum container nesting `Decoder.skipValue` will follow. +pub const max_depth: usize = 32; + +// ============================================================================ +// Encoder +// ============================================================================ + +/// Appends canonical msgpack encodings to a growable buffer. +pub const Encoder = struct { + allocator: std.mem.Allocator, + buf: std.ArrayList(u8) = .empty, + + pub fn init(allocator: std.mem.Allocator) Encoder { + return .{ .allocator = allocator }; + } + + pub fn deinit(self: *Encoder) void { + self.buf.deinit(self.allocator); + } + + /// The bytes encoded so far (still owned by the encoder). + pub fn bytes(self: *const Encoder) []const u8 { + return self.buf.items; + } + + /// Hand the encoded bytes to the caller; the encoder is left empty. + pub fn toOwnedSlice(self: *Encoder) std.mem.Allocator.Error![]u8 { + return self.buf.toOwnedSlice(self.allocator); + } + + pub fn writeNil(self: *Encoder) EncodeError!void { + try self.buf.append(self.allocator, 0xc0); + } + + pub fn writeBool(self: *Encoder, value: bool) EncodeError!void { + try self.buf.append(self.allocator, if (value) 0xc3 else 0xc2); + } + + /// Unsigned integer in its shortest form: positive fixint, uint 8/16/32/64. + pub fn writeUint(self: *Encoder, value: u64) EncodeError!void { + if (value < 0x80) { + try self.buf.append(self.allocator, @intCast(value)); + } else if (value <= std.math.maxInt(u8)) { + try self.buf.append(self.allocator, 0xcc); + try self.buf.append(self.allocator, @intCast(value)); + } else if (value <= std.math.maxInt(u16)) { + try self.buf.append(self.allocator, 0xcd); + try self.appendBigEndian(u16, @intCast(value)); + } else if (value <= std.math.maxInt(u32)) { + try self.buf.append(self.allocator, 0xce); + try self.appendBigEndian(u32, @intCast(value)); + } else { + try self.buf.append(self.allocator, 0xcf); + try self.appendBigEndian(u64, value); + } + } + + /// UTF-8 string: fixstr (< 32 bytes), str 8/16/32. + pub fn writeStr(self: *Encoder, value: []const u8) EncodeError!void { + if (value.len < 32) { + try self.buf.append(self.allocator, 0xa0 | @as(u8, @intCast(value.len))); + } else { + try self.writeLengthPrefixed(value.len, 0xd9, 0xda, 0xdb); + } + try self.buf.appendSlice(self.allocator, value); + } + + /// Raw bytes: bin 8/16/32. + pub fn writeBin(self: *Encoder, value: []const u8) EncodeError!void { + try self.writeLengthPrefixed(value.len, 0xc4, 0xc5, 0xc6); + try self.buf.appendSlice(self.allocator, value); + } + + /// Array header for `count` elements; the caller then writes the elements. + pub fn writeArrayHeader(self: *Encoder, count: usize) EncodeError!void { + if (count < 16) { + try self.buf.append(self.allocator, 0x90 | @as(u8, @intCast(count))); + } else { + try self.writeLength16or32(count, 0xdc, 0xdd); + } + } + + /// Map header for `count` key/value pairs; the caller then writes 2*count values. + pub fn writeMapHeader(self: *Encoder, count: usize) EncodeError!void { + if (count < 16) { + try self.buf.append(self.allocator, 0x80 | @as(u8, @intCast(count))); + } else { + try self.writeLength16or32(count, 0xde, 0xdf); + } + } + + fn writeLengthPrefixed(self: *Encoder, len: usize, tag8: u8, tag16: u8, tag32: u8) EncodeError!void { + if (len <= std.math.maxInt(u8)) { + try self.buf.append(self.allocator, tag8); + try self.buf.append(self.allocator, @intCast(len)); + } else { + try self.writeLength16or32(len, tag16, tag32); + } + } + + fn writeLength16or32(self: *Encoder, len: usize, tag16: u8, tag32: u8) EncodeError!void { + if (len <= std.math.maxInt(u16)) { + try self.buf.append(self.allocator, tag16); + try self.appendBigEndian(u16, @intCast(len)); + } else if (len <= std.math.maxInt(u32)) { + try self.buf.append(self.allocator, tag32); + try self.appendBigEndian(u32, @intCast(len)); + } else { + return error.TooLong; + } + } + + fn appendBigEndian(self: *Encoder, comptime T: type, value: T) EncodeError!void { + var tmp: [@sizeOf(T)]u8 = undefined; + std.mem.writeInt(T, &tmp, value, .big); + try self.buf.appendSlice(self.allocator, &tmp); + } +}; + +// ============================================================================ +// Decoder +// ============================================================================ + +/// Strict cursor over a msgpack byte string. Typed reads fail with +/// `error.TypeMismatch` unless the next value is of the requested family. +pub const Decoder = struct { + data: []const u8, + pos: usize = 0, + + pub fn init(data: []const u8) Decoder { + return .{ .data = data }; + } + + /// True once every input byte has been consumed. + pub fn finished(self: *const Decoder) bool { + return self.pos == self.data.len; + } + + pub fn remaining(self: *const Decoder) usize { + return self.data.len - self.pos; + } + + pub fn readNil(self: *Decoder) DecodeError!void { + const head = try self.peek(); + if (head != 0xc0) return error.TypeMismatch; + self.pos += 1; + } + + pub fn readBool(self: *Decoder) DecodeError!bool { + const head = try self.peek(); + switch (head) { + 0xc2 => { + self.pos += 1; + return false; + }, + 0xc3 => { + self.pos += 1; + return true; + }, + else => return error.TypeMismatch, + } + } + + pub fn readUint(self: *Decoder) DecodeError!u64 { + const head = try self.peek(); + switch (head) { + 0x00...0x7f => { + self.pos += 1; + return head; + }, + 0xcc => { + self.pos += 1; + return try self.readBigEndian(u8); + }, + 0xcd => { + self.pos += 1; + return try self.readBigEndian(u16); + }, + 0xce => { + self.pos += 1; + return try self.readBigEndian(u32); + }, + 0xcf => { + self.pos += 1; + return try self.readBigEndian(u64); + }, + else => return error.TypeMismatch, + } + } + + /// Returns a view into the input; valid as long as the input is. + pub fn readStr(self: *Decoder) DecodeError![]const u8 { + const head = try self.peek(); + const len: usize = switch (head) { + 0xa0...0xbf => blk: { + self.pos += 1; + break :blk head & 0x1f; + }, + 0xd9 => blk: { + self.pos += 1; + break :blk try self.readBigEndian(u8); + }, + 0xda => blk: { + self.pos += 1; + break :blk try self.readBigEndian(u16); + }, + 0xdb => blk: { + self.pos += 1; + break :blk try self.readBigEndian(u32); + }, + else => return error.TypeMismatch, + }; + return self.take(len); + } + + /// Returns a view into the input; valid as long as the input is. + pub fn readBin(self: *Decoder) DecodeError![]const u8 { + const head = try self.peek(); + const len: usize = switch (head) { + 0xc4 => blk: { + self.pos += 1; + break :blk try self.readBigEndian(u8); + }, + 0xc5 => blk: { + self.pos += 1; + break :blk try self.readBigEndian(u16); + }, + 0xc6 => blk: { + self.pos += 1; + break :blk try self.readBigEndian(u32); + }, + else => return error.TypeMismatch, + }; + return self.take(len); + } + + /// Number of elements that follow. + pub fn readArrayHeader(self: *Decoder) DecodeError!usize { + const head = try self.peek(); + switch (head) { + 0x90...0x9f => { + self.pos += 1; + return head & 0x0f; + }, + 0xdc => { + self.pos += 1; + return try self.readBigEndian(u16); + }, + 0xdd => { + self.pos += 1; + return try self.readBigEndian(u32); + }, + else => return error.TypeMismatch, + } + } + + /// Number of key/value pairs that follow. + pub fn readMapHeader(self: *Decoder) DecodeError!usize { + const head = try self.peek(); + switch (head) { + 0x80...0x8f => { + self.pos += 1; + return head & 0x0f; + }, + 0xde => { + self.pos += 1; + return try self.readBigEndian(u16); + }, + 0xdf => { + self.pos += 1; + return try self.readBigEndian(u32); + }, + else => return error.TypeMismatch, + } + } + + /// Step over one complete value of any family without interpreting it. + pub fn skipValue(self: *Decoder) DecodeError!void { + return self.skipValueDepth(0); + } + + fn skipValueDepth(self: *Decoder, depth: usize) DecodeError!void { + if (depth >= max_depth) return error.TooDeep; + const head = try self.peek(); + self.pos += 1; + switch (head) { + 0x00...0x7f, 0xe0...0xff, 0xc0, 0xc2, 0xc3 => {}, + 0xc1 => return error.Reserved, + 0x80...0x8f => try self.skipN(2 * @as(usize, head & 0x0f), depth), + 0x90...0x9f => try self.skipN(head & 0x0f, depth), + 0xa0...0xbf => _ = try self.take(head & 0x1f), + 0xc4, 0xd9 => _ = try self.take(try self.readBigEndian(u8)), + 0xc5, 0xda => _ = try self.take(try self.readBigEndian(u16)), + 0xc6, 0xdb => _ = try self.take(try self.readBigEndian(u32)), + // ext 8/16/32: length, then one type byte, then the payload. + 0xc7 => _ = try self.take(@as(usize, try self.readBigEndian(u8)) + 1), + 0xc8 => _ = try self.take(@as(usize, try self.readBigEndian(u16)) + 1), + 0xc9 => _ = try self.take(@as(usize, try self.readBigEndian(u32)) + 1), + 0xca, 0xcc, 0xd0, 0xcd, 0xd1, 0xcb, 0xce, 0xd2, 0xcf, 0xd3 => _ = try self.take(scalarWidth(head)), + // fixext 1/2/4/8/16: one type byte plus the payload. + 0xd4 => _ = try self.take(2), + 0xd5 => _ = try self.take(3), + 0xd6 => _ = try self.take(5), + 0xd7 => _ = try self.take(9), + 0xd8 => _ = try self.take(17), + 0xdc => try self.skipN(try self.readBigEndian(u16), depth), + 0xdd => try self.skipN(try self.readBigEndian(u32), depth), + 0xde => try self.skipN(2 * @as(usize, try self.readBigEndian(u16)), depth), + 0xdf => try self.skipN(2 * @as(usize, try self.readBigEndian(u32)), depth), + } + } + + fn scalarWidth(head: u8) usize { + return switch (head) { + 0xcc, 0xd0 => 1, + 0xcd, 0xd1 => 2, + 0xca, 0xce, 0xd2 => 4, + 0xcb, 0xcf, 0xd3 => 8, + else => unreachable, + }; + } + + fn skipN(self: *Decoder, count: usize, depth: usize) DecodeError!void { + var i: usize = 0; + while (i < count) : (i += 1) try self.skipValueDepth(depth + 1); + } + + fn peek(self: *const Decoder) DecodeError!u8 { + if (self.pos >= self.data.len) return error.Truncated; + return self.data[self.pos]; + } + + fn take(self: *Decoder, len: usize) DecodeError![]const u8 { + if (len > self.data.len - self.pos) return error.Truncated; + const out = self.data[self.pos .. self.pos + len]; + self.pos += len; + return out; + } + + fn readBigEndian(self: *Decoder, comptime T: type) DecodeError!T { + const raw = try self.take(@sizeOf(T)); + return std.mem.readInt(T, raw[0..@sizeOf(T)], .big); + } +}; + +// ============================================================================ +// Tests +// ============================================================================ + +const testing = std.testing; + +fn expectEncoded(expected: []const u8, comptime write: anytype, arg: anytype) !void { + var enc = Encoder.init(testing.allocator); + defer enc.deinit(); + try write(&enc, arg); + try testing.expectEqualSlices(u8, expected, enc.bytes()); +} + +test "msgpack: nil and bool" { + var enc = Encoder.init(testing.allocator); + defer enc.deinit(); + try enc.writeNil(); + try enc.writeBool(false); + try enc.writeBool(true); + try testing.expectEqualSlices(u8, &.{ 0xc0, 0xc2, 0xc3 }, enc.bytes()); +} + +test "msgpack: uint boundaries use the shortest encoding" { + try expectEncoded(&.{0x00}, Encoder.writeUint, @as(u64, 0)); + try expectEncoded(&.{0x7f}, Encoder.writeUint, @as(u64, 0x7f)); + try expectEncoded(&.{ 0xcc, 0x80 }, Encoder.writeUint, @as(u64, 0x80)); + try expectEncoded(&.{ 0xcc, 0xff }, Encoder.writeUint, @as(u64, 0xff)); + try expectEncoded(&.{ 0xcd, 0x01, 0x00 }, Encoder.writeUint, @as(u64, 0x100)); + try expectEncoded(&.{ 0xcd, 0xff, 0xff }, Encoder.writeUint, @as(u64, 0xffff)); + try expectEncoded(&.{ 0xce, 0x00, 0x01, 0x00, 0x00 }, Encoder.writeUint, @as(u64, 0x10000)); + try expectEncoded(&.{ 0xce, 0xff, 0xff, 0xff, 0xff }, Encoder.writeUint, @as(u64, 0xffff_ffff)); + try expectEncoded(&.{ 0xcf, 0, 0, 0, 1, 0, 0, 0, 0 }, Encoder.writeUint, @as(u64, 0x1_0000_0000)); +} + +test "msgpack: str boundaries" { + try expectEncoded(&.{0xa0}, Encoder.writeStr, ""); + try expectEncoded("\xa3abc", Encoder.writeStr, "abc"); + const s31 = "a" ** 31; + try expectEncoded("\xbf" ++ s31, Encoder.writeStr, s31); + const s32 = "a" ** 32; + try expectEncoded("\xd9\x20" ++ s32, Encoder.writeStr, s32); + const s255 = "a" ** 255; + try expectEncoded("\xd9\xff" ++ s255, Encoder.writeStr, s255); + const s256 = "a" ** 256; + try expectEncoded("\xda\x01\x00" ++ s256, Encoder.writeStr, s256); +} + +test "msgpack: bin boundaries" { + try expectEncoded(&.{ 0xc4, 0x00 }, Encoder.writeBin, ""); + try expectEncoded("\xc4\x03\x01\x02\x03", Encoder.writeBin, "\x01\x02\x03"); + const b255 = "\x5a" ** 255; + try expectEncoded("\xc4\xff" ++ b255, Encoder.writeBin, b255); + const b256 = "\x5a" ** 256; + try expectEncoded("\xc5\x01\x00" ++ b256, Encoder.writeBin, b256); + const b65536 = "\x5a" ** 65536; + try expectEncoded("\xc6\x00\x01\x00\x00" ++ b65536, Encoder.writeBin, b65536); +} + +test "msgpack: array and map headers" { + try expectEncoded(&.{0x90}, Encoder.writeArrayHeader, @as(usize, 0)); + try expectEncoded(&.{0x9f}, Encoder.writeArrayHeader, @as(usize, 15)); + try expectEncoded(&.{ 0xdc, 0x00, 0x10 }, Encoder.writeArrayHeader, @as(usize, 16)); + try expectEncoded(&.{ 0xdc, 0x01, 0xca }, Encoder.writeArrayHeader, @as(usize, 458)); + try expectEncoded(&.{ 0xdd, 0x00, 0x01, 0x00, 0x00 }, Encoder.writeArrayHeader, @as(usize, 65536)); + try expectEncoded(&.{0x80}, Encoder.writeMapHeader, @as(usize, 0)); + try expectEncoded(&.{0x84}, Encoder.writeMapHeader, @as(usize, 4)); + try expectEncoded(&.{ 0xde, 0x00, 0x10 }, Encoder.writeMapHeader, @as(usize, 16)); + try expectEncoded(&.{ 0xdf, 0x00, 0x01, 0x00, 0x00 }, Encoder.writeMapHeader, @as(usize, 65536)); +} + +test "msgpack: toOwnedSlice hands over the buffer" { + var enc = Encoder.init(testing.allocator); + defer enc.deinit(); + try enc.writeStr("hi"); + const owned = try enc.toOwnedSlice(); + defer testing.allocator.free(owned); + try testing.expectEqualSlices(u8, "\xa2hi", owned); + try testing.expectEqual(@as(usize, 0), enc.bytes().len); +} + +test "msgpack: decoder round trip" { + var enc = Encoder.init(testing.allocator); + defer enc.deinit(); + try enc.writeArrayHeader(2); + try enc.writeStr("CircuitVerifyResponse"); + try enc.writeMapHeader(3); + try enc.writeStr("verified"); + try enc.writeBool(true); + try enc.writeStr("count"); + try enc.writeUint(70000); + try enc.writeStr("blob"); + try enc.writeBin(&.{ 1, 2, 3 }); + + var dec = Decoder.init(enc.bytes()); + try testing.expectEqual(@as(usize, 2), try dec.readArrayHeader()); + try testing.expectEqualStrings("CircuitVerifyResponse", try dec.readStr()); + try testing.expectEqual(@as(usize, 3), try dec.readMapHeader()); + try testing.expectEqualStrings("verified", try dec.readStr()); + try testing.expect(try dec.readBool()); + try testing.expectEqualStrings("count", try dec.readStr()); + try testing.expectEqual(@as(u64, 70000), try dec.readUint()); + try testing.expectEqualStrings("blob", try dec.readStr()); + try testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, try dec.readBin()); + try testing.expect(dec.finished()); +} + +test "msgpack: decoder reads every uint width" { + const input = [_]u8{ 0x05, 0xcc, 0xfe, 0xcd, 0x12, 0x34, 0xce, 0, 0, 0x10, 0, 0xcf, 0, 0, 0, 1, 0, 0, 0, 0 }; + var dec = Decoder.init(&input); + try testing.expectEqual(@as(u64, 5), try dec.readUint()); + try testing.expectEqual(@as(u64, 0xfe), try dec.readUint()); + try testing.expectEqual(@as(u64, 0x1234), try dec.readUint()); + try testing.expectEqual(@as(u64, 0x1000), try dec.readUint()); + try testing.expectEqual(@as(u64, 0x1_0000_0000), try dec.readUint()); + try testing.expect(dec.finished()); +} + +test "msgpack: strict decoder rejects the wrong family" { + var dec = Decoder.init(&.{0xc3}); + try testing.expectError(error.TypeMismatch, dec.readStr()); + try testing.expectError(error.TypeMismatch, dec.readUint()); + try testing.expectError(error.TypeMismatch, dec.readArrayHeader()); + try testing.expectError(error.TypeMismatch, dec.readMapHeader()); + try testing.expectError(error.TypeMismatch, dec.readBin()); + try testing.expectError(error.TypeMismatch, dec.readNil()); + // A failed read consumes nothing. + try testing.expect(try dec.readBool()); +} + +test "msgpack: strict decoder rejects truncated input" { + var empty = Decoder.init(&.{}); + try testing.expectError(error.Truncated, empty.readBool()); + try testing.expectError(error.Truncated, empty.skipValue()); + + // fixstr claiming 5 bytes with only 2 present. + var short_str = Decoder.init("\xa5ab"); + try testing.expectError(error.Truncated, short_str.readStr()); + + // bin16 header cut off after one length byte. + var short_len = Decoder.init(&.{ 0xc5, 0x01 }); + try testing.expectError(error.Truncated, short_len.readBin()); + + // array16 header with a missing length. + var short_arr = Decoder.init(&.{0xdc}); + try testing.expectError(error.Truncated, short_arr.readArrayHeader()); +} + +test "msgpack: skipValue steps over nested containers and foreign families" { + // [ -1, 1.5f32, {"k": [nil, ext]}, "s" ] followed by a trailing bool. + const input = [_]u8{ + 0x94, + 0xff, // negative fixint -1 + 0xca, 0x3f, 0xc0, 0x00, 0x00, // float32 1.5 + 0x81, 0xa1, 'k', 0x92, 0xc0, 0xd4, 0x01, 0xaa, // {"k": [nil, fixext1(type 1, 0xaa)]} + 0xa1, 's', 0xc3, + }; + var dec = Decoder.init(&input); + try dec.skipValue(); + try testing.expect(try dec.readBool()); + try testing.expect(dec.finished()); +} + +test "msgpack: decoder accepts non-minimal encodings of the expected family" { + // Same values as the canonical forms, written in longer encodings: str8 + // instead of fixstr, array16/map16 instead of the fixed headers, and + // uint64 instead of a positive fixint. + const input = "\xdc\x00\x02" ++ // array16 with 2 elements + "\xd9\x02ok" ++ // str8 "ok" + "\xde\x00\x01" ++ // map16 with 1 entry + "\xd9\x01n" ++ // str8 "n" + "\xcf\x00\x00\x00\x00\x00\x00\x00\x07"; // uint64 7 + var dec = Decoder.init(input); + try testing.expectEqual(@as(usize, 2), try dec.readArrayHeader()); + try testing.expectEqualStrings("ok", try dec.readStr()); + try testing.expectEqual(@as(usize, 1), try dec.readMapHeader()); + try testing.expectEqualStrings("n", try dec.readStr()); + try testing.expectEqual(@as(u64, 7), try dec.readUint()); + try testing.expect(dec.finished()); + + // The encoder, by contrast, only ever emits the shortest form. + var enc = Encoder.init(testing.allocator); + defer enc.deinit(); + try enc.writeArrayHeader(2); + try enc.writeStr("ok"); + try enc.writeMapHeader(1); + try enc.writeStr("n"); + try enc.writeUint(7); + try testing.expectEqualSlices(u8, "\x92\xa2ok\x81\xa1n\x07", enc.bytes()); + try testing.expect(enc.bytes().len < input.len); +} + +test "msgpack: skipValue rejects the reserved byte and runaway nesting" { + var reserved = Decoder.init(&.{0xc1}); + try testing.expectError(error.Reserved, reserved.skipValue()); + + // 40 nested single-element arrays exceed max_depth. + const nested = [_]u8{0x91} ** 40 ++ [_]u8{0xc0}; + var deep = Decoder.init(&nested); + try testing.expectError(error.TooDeep, deep.skipValue()); +} diff --git a/src/root.zig b/src/root.zig index 3340c59..7f3f735 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,6 +1,9 @@ // eth.zig - Pure Zig Ethereum Client Library // Maintained by Strobe Labs (https://github.com/strobelabs/eth.zig) +// Build-time switches from build.zig (`-Dnoir`). +const build_options = @import("build_options"); + // -- Layer 1: Primitives -- pub const primitives = @import("primitives.zig"); pub const uint256 = @import("uint256.zig"); @@ -19,6 +22,9 @@ pub const secp256k1 = @import("secp256k1.zig"); pub const signer = @import("signer.zig"); pub const kms = @import("kms.zig"); pub const eip155 = @import("eip155.zig"); +/// Noir UltraHonk proof verification over Barretenberg. Opt-in: the entry +/// points only compile when the build is invoked with `-Dnoir=true`. +pub const noir = @import("noir.zig"); // -- Layer 4: Types -- pub const access_list = @import("access_list.zig"); @@ -123,6 +129,12 @@ test { _ = @import("blob.zig"); _ = @import("kzg.zig"); _ = @import("kzg_vectors_test.zig"); + // Layer 3, opt-in: only collected when the build links Barretenberg + // (-Dnoir=true); the default test run is unchanged. + if (build_options.noir) { + _ = @import("noir.zig"); + _ = @import("noir/msgpack.zig"); + } // Layer 5 _ = @import("mnemonic.zig"); _ = @import("hd_wallet.zig"); diff --git a/tests/noir_vectors_test.zig b/tests/noir_vectors_test.zig new file mode 100644 index 0000000..d39307b --- /dev/null +++ b/tests/noir_vectors_test.zig @@ -0,0 +1,251 @@ +// Barretenberg v5.2.0 interop vectors for eth.noir. +// +// Verifies real UltraHonk proofs written by the bb CLI (bb 5.2.0, nargo +// 1.0.0-beta.25) for the hello circuit under tests/vectors/noir/, in both the +// poseidon2 (default) and keccak ("evm") flavors, and pins the exact request +// bytes eth.noir sends to the library against reference requests produced by +// an independent encoder and confirmed against the library at runtime. +// +// Only built and run when the build links Barretenberg: +// zig build test -Dnoir=true + +const std = @import("std"); +const eth = @import("eth"); +const noir = eth.noir; + +const testing = std.testing; + +const p2_vk = @embedFile("vectors/noir/poseidon2/vk"); +const p2_proof = @embedFile("vectors/noir/poseidon2/proof"); +const p2_public_inputs = @embedFile("vectors/noir/poseidon2/public_inputs"); + +const kc_vk = @embedFile("vectors/noir/keccak/vk"); +const kc_proof = @embedFile("vectors/noir/keccak/proof"); +const kc_public_inputs = @embedFile("vectors/noir/keccak/public_inputs"); + +const req_srs_g1only = @embedFile("vectors/noir/requests/req_srs_g1only.bin"); +const req_verify_default = @embedFile("vectors/noir/requests/req_verify_default.bin"); +const req_verify_evm = @embedFile("vectors/noir/requests/req_verify_evm.bin"); + +test "noir vectors: bb artifacts have the documented sizes" { + // poseidon2 proofs are padded to CONST_PROOF_SIZE_LOG_N; keccak (evm) proofs are not. + try testing.expectEqual(@as(usize, 458), (try noir.fieldsFromBytes(p2_proof)).len); + try testing.expectEqual(@as(usize, 3680), p2_vk.len); + try testing.expectEqual(@as(usize, 142), (try noir.fieldsFromBytes(kc_proof)).len); + try testing.expectEqual(@as(usize, 1888), kc_vk.len); + // The circuit has one public input, y = 2. + const inputs = try noir.fieldsFromBytes(p2_public_inputs); + try testing.expectEqual(@as(usize, 1), inputs.len); + try testing.expectEqual(@as(u8, 2), inputs[0][31]); + try testing.expectEqualSlices(u8, p2_public_inputs, kc_public_inputs); +} + +test "noir vectors: poseidon2 ZK proof verifies" { + try noir.init(); + const ok = try noir.verify( + testing.allocator, + p2_vk, + try noir.fieldsFromBytes(p2_public_inputs), + try noir.fieldsFromBytes(p2_proof), + .fromVerifierTarget(.default), + ); + try testing.expect(ok); +} + +test "noir vectors: keccak ZK (evm) proof verifies" { + try noir.init(); + const ok = try noir.verify( + testing.allocator, + kc_vk, + try noir.fieldsFromBytes(kc_public_inputs), + try noir.fieldsFromBytes(kc_proof), + .fromVerifierTarget(.evm), + ); + try testing.expect(ok); +} + +test "noir vectors: tampered proof element is rejected with an error and a message" { + try noir.init(); + var tampered: [p2_proof.len]u8 = p2_proof.*; + // Flip the low bit of the last field (the y coordinate of the KZG opening + // point): the point is no longer on the curve, so deserialization fails. + tampered[tampered.len - 1] ^= 0x01; + var diag: noir.Diagnostics = .{}; + try testing.expectError(error.ProofRejected, noir.verifyDiag( + testing.allocator, + p2_vk, + try noir.fieldsFromBytes(p2_public_inputs), + try noir.fieldsFromBytes(&tampered), + .fromVerifierTarget(.default), + &diag, + )); + try testing.expect(diag.message().len > 0); + try testing.expect(std.mem.indexOf(u8, diag.message(), "curve") != null); +} + +test "noir vectors: short proof returns false" { + try noir.init(); + const full = try noir.fieldsFromBytes(p2_proof); + const ok = try noir.verify( + testing.allocator, + p2_vk, + try noir.fieldsFromBytes(p2_public_inputs), + full[0 .. full.len - 1], + .fromVerifierTarget(.default), + ); + try testing.expect(!ok); +} + +test "noir vectors: wrong public input returns false" { + try noir.init(); + const zero_input = [_][32]u8{[_]u8{0} ** 32}; + const ok = try noir.verify( + testing.allocator, + p2_vk, + &zero_input, + try noir.fieldsFromBytes(p2_proof), + .fromVerifierTarget(.default), + ); + try testing.expect(!ok); +} + +test "noir vectors: wrong public input count is rejected with an error" { + try noir.init(); + // The value-mismatch case above returns false, but a count that disagrees + // with the verification key is rejected before a verdict is reached. Both + // mean "not verified"; the distinction is what the docs promise. + const two_inputs = [_][32]u8{ (try noir.fieldsFromBytes(p2_public_inputs))[0], [_]u8{0} ** 32 }; + for ([_][]const [32]u8{ &.{}, &two_inputs }) |inputs| { + var diag: noir.Diagnostics = .{}; + try testing.expectError(error.ProofRejected, noir.verifyDiag( + testing.allocator, + p2_vk, + inputs, + try noir.fieldsFromBytes(p2_proof), + .fromVerifierTarget(.default), + &diag, + )); + try testing.expect(diag.message().len > 0); + } +} + +test "noir vectors: right-size verification key that does not deserialize is rejected with an error" { + try noir.init(); + // Same length as the real poseidon2 VK, so the size gate passes and the + // failure comes from deserialization rather than a verdict. + const zero_vk = [_]u8{0} ** p2_vk.len; + var diag: noir.Diagnostics = .{}; + try testing.expectError(error.ProofRejected, noir.verifyDiag( + testing.allocator, + &zero_vk, + try noir.fieldsFromBytes(p2_public_inputs), + try noir.fieldsFromBytes(p2_proof), + .fromVerifierTarget(.default), + &diag, + )); + try testing.expect(diag.message().len > 0); +} + +test "noir vectors: non-canonical field element is rejected with an error" { + try noir.init(); + var bad: [p2_proof.len]u8 = p2_proof.*; + // 0xff..ff is >= the BN254 scalar field modulus. + @memset(bad[0..32], 0xff); + var diag: noir.Diagnostics = .{}; + try testing.expectError(error.ProofRejected, noir.verifyDiag( + testing.allocator, + p2_vk, + try noir.fieldsFromBytes(p2_public_inputs), + try noir.fieldsFromBytes(&bad), + .fromVerifierTarget(.default), + &diag, + )); + try testing.expect(diag.message().len > 0); +} + +test "noir vectors: flavor mismatch returns false" { + try noir.init(); + // A keccak proof and VK checked with the poseidon2 settings. + const ok = try noir.verify( + testing.allocator, + kc_vk, + try noir.fieldsFromBytes(kc_public_inputs), + try noir.fieldsFromBytes(kc_proof), + .fromVerifierTarget(.default), + ); + try testing.expect(!ok); +} + +test "noir vectors: concurrent verification across threads" { + try noir.init(); + // Barretenberg keeps per-process request state with no lock of its own, so + // eth.noir serializes every bbapi call behind one mutex. This is what would + // catch that mutex being dropped in a later refactor: without it the + // threads race on the library's global request object. + const thread_count = 8; + const rounds = 5; + const Worker = struct { + fn run(index: usize, ok: *bool) void { + var round: usize = 0; + while (round < rounds) : (round += 1) { + // Alternate flavors so the threads do not all walk the same path. + const use_keccak = (index + round) % 2 == 1; + const verified = if (use_keccak) noir.verify( + std.testing.allocator, + kc_vk, + noir.fieldsFromBytes(kc_public_inputs) catch return, + noir.fieldsFromBytes(kc_proof) catch return, + .fromVerifierTarget(.evm), + ) catch return else noir.verify( + std.testing.allocator, + p2_vk, + noir.fieldsFromBytes(p2_public_inputs) catch return, + noir.fieldsFromBytes(p2_proof) catch return, + .fromVerifierTarget(.default), + ) catch return; + if (!verified) return; + } + ok.* = true; + } + }; + var ok: [thread_count]bool = @splat(false); + var threads: [thread_count]std.Thread = undefined; + var spawned: usize = 0; + for (0..thread_count) |i| { + threads[i] = std.Thread.spawn(.{}, Worker.run, .{ i, &ok[i] }) catch break; + spawned += 1; + } + for (threads[0..spawned]) |t| t.join(); + try testing.expect(spawned > 0); + for (ok[0..spawned]) |flag| try testing.expect(flag); +} + +test "noir vectors: SrsInitSrs request bytes match the reference" { + const request = try noir.encodeSrsInitSrsRequest(testing.allocator, &noir.BN254_G1_GENERATOR, 1, &noir.BN254_G2_ELEMENT); + defer testing.allocator.free(request); + try testing.expectEqualSlices(u8, req_srs_g1only, request); +} + +test "noir vectors: CircuitVerify request bytes match the poseidon2 reference" { + const request = try noir.encodeCircuitVerifyRequest( + testing.allocator, + p2_vk, + try noir.fieldsFromBytes(p2_public_inputs), + try noir.fieldsFromBytes(p2_proof), + .fromVerifierTarget(.default), + ); + defer testing.allocator.free(request); + try testing.expectEqualSlices(u8, req_verify_default, request); +} + +test "noir vectors: CircuitVerify request bytes match the keccak reference" { + const request = try noir.encodeCircuitVerifyRequest( + testing.allocator, + kc_vk, + try noir.fieldsFromBytes(kc_public_inputs), + try noir.fieldsFromBytes(kc_proof), + .fromVerifierTarget(.evm), + ); + defer testing.allocator.free(request); + try testing.expectEqualSlices(u8, req_verify_evm, request); +} diff --git a/tests/vectors/noir/Prover.toml b/tests/vectors/noir/Prover.toml new file mode 100644 index 0000000..8c12ebb --- /dev/null +++ b/tests/vectors/noir/Prover.toml @@ -0,0 +1,2 @@ +x = "1" +y = "2" diff --git a/tests/vectors/noir/README.md b/tests/vectors/noir/README.md new file mode 100644 index 0000000..1b13229 --- /dev/null +++ b/tests/vectors/noir/README.md @@ -0,0 +1,43 @@ +# Noir / Barretenberg test vectors + +Artifacts for `tests/noir_vectors_test.zig` (built only with `-Dnoir=true`). +They were produced with **bb 5.2.0** and **nargo 1.0.0-beta.25** +(noirc `1.0.0-beta.25+75061fab15986eedee4e7d9104ff87dd9fa4ca10`, the Noir +release that Barretenberg v5.2.0 pins) and are checked into the repo unchanged. + +## Circuit + +`main.nr` is the whole program: one private input `x`, one public input `y`, +and the constraint `x != y`. `Prover.toml` supplies `x = 1`, `y = 2`, so the +single public input is the field element 2. + +``` +nargo compile # target/hello.json +nargo execute # target/hello.gz (witness) +``` + +## Directories + +- `poseidon2/`: `bb write_vk` + `bb prove` with no `--verifier_target` (the CLI + default: poseidon2 transcript, ZK, no IPA). `proof` = 458 fields (14656 + bytes, padded to the constant proof size), `vk` = 3680 bytes. +- `keccak/`: the same circuit and witness with `--verifier_target evm` + (keccak transcript, ZK). `proof` = 142 fields (4544 bytes, unpadded), + `vk` = 1888 bytes. +- Each directory holds bb's raw outputs: `proof` and `public_inputs` + (concatenated 32-byte big-endian field elements), `vk` (raw verification + key) and `vk_hash` (32 bytes; not needed by the verifier, kept for + completeness). +- `requests/`: reference `bbapi` request bytes. They were generated by an + independent, single-purpose msgpack encoder (Python) and confirmed against + `libbb-external.a` v5.2.0 at runtime (the library answered `SrsInitSrsResponse` + and `CircuitVerifyResponse{verified: true}` respectively). The tests assert + that eth.noir's Zig encoder reproduces them byte for byte: + - `req_srs_g1only.bin`: `SrsInitSrs` with one G1 point (the generator) and + the pinned G2 element, i.e. exactly what `noir.init` sends. + - `req_verify_default.bin`: `CircuitVerify` for `poseidon2/` with the default + settings. + - `req_verify_evm.bin`: `CircuitVerify` for `keccak/` with the `evm` settings. + +`bb verify -k vk -p proof -i public_inputs [-t evm]` reports both proofs as +verified with the same bb release. diff --git a/tests/vectors/noir/keccak/proof b/tests/vectors/noir/keccak/proof new file mode 100644 index 0000000000000000000000000000000000000000..1e05a555f94de14e592e8d37e11eb98dfabc970a GIT binary patch literal 4544 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 literal 0 HcmV?d00001 diff --git a/tests/vectors/noir/keccak/public_inputs b/tests/vectors/noir/keccak/public_inputs new file mode 100644 index 0000000000000000000000000000000000000000..b68c955e77b24202666880f3d0066ec7b9dce152 GIT binary patch literal 32 LcmZQzzzdiF03rYb literal 0 HcmV?d00001 diff --git a/tests/vectors/noir/keccak/vk b/tests/vectors/noir/keccak/vk new file mode 100644 index 0000000000000000000000000000000000000000..3ac3dd6b7c4a70c3715d730fb9fce537b19a5551 GIT binary patch literal 1888 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 literal 0 HcmV?d00001 diff --git a/tests/vectors/noir/poseidon2/public_inputs b/tests/vectors/noir/poseidon2/public_inputs new file mode 100644 index 0000000000000000000000000000000000000000..b68c955e77b24202666880f3d0066ec7b9dce152 GIT binary patch literal 32 LcmZQzzzdiF03rYb literal 0 HcmV?d00001 diff --git a/tests/vectors/noir/poseidon2/vk b/tests/vectors/noir/poseidon2/vk new file mode 100644 index 0000000000000000000000000000000000000000..f3b39a355a220707cf7d0e92ad01d084c5748c02 GIT binary patch literal 3680 zcmeI!do+}390%|x8A8^WMjA6aWej6v6tmsjX}b_n)O6XyD5k+q?XKK5wp=1=D`k{h z4M~&?xvg9_L&->zE@v>xnzPtsoZLos*UsO+Ij8>IKl1+fJfHJD@B4n=^StMW&?hg< zClw*B&p6D7M%XfG^5gWbT`V&P6YrK6DtUa?Pgwr+=`>E&vYZ3>5a47Z&Fq`slbbW_ z@jgPXwCn_MglKHDl3tXg;fIsB);Gz4>qfmqY5n7AXKUJ4m^m5y>H$XxH^dm+$Vmyp zrmU0eJkbUo5IG^3<5rg&dgVAv1BKsg1CEe(_t{cmrB+&)nj4|=btCX-4<}2-WFXGw zZbHOTYjr#aI6~w{8*;Uu&xca~(roE#odE8q**j3Lo5j0w<4Ik)+5~F~xH4a!p0PPR zqhNO3)91rmxWHRV$X_=%H!aahEd9`ydA#-);L0!h&nZ*M6GP(;_PkJ#V*ty)SBoy${pPLaRqT*1o~M4!uuU)<&4l0Zwd{ zjt^t3t`7$7aF1Lferj0pvHZQxUhiCi=MUoytcM6LYIETV?{m***ua(Vf9opmPrE;=y~^t^4p7dYgbc%FAu=%J2r7;rRPzL7|w3R zZ7FbsNbb3m^cVA68ByB{qgK&?6X{ba7R0NIjpUl<#!NMt2slD(%@eNKdahQ`=DI)5 ztZxQR9*etYd*&qlqFcF#{zxMG6>x+s_C9r)E^Ay?D>}065HTM3%lO#C8(Wk1KIpQ{ zD=m~;#sEjisQ3b7P}<`gdx0k#GVKR$by-XF##OpghRHS99j#u~z!6%x;*_m4^;ObJ@#6lrU&?5s! z2+t|4sLcF<=TNqRs1+Wd;$QYga=6>I;Nb?#O+sdazo1exOQjAq*Yyo~WRyX>>(Yov)+{A1o9*8l)kU1a&Bj8 zuPB-!CDsE+h+@?;|BzwRCCiij;Y0ffT>LEQTM<2qc(Q!U9X*4)Y@B|3C=Y1zp*H=b{LqrOEc#A0@1*e?!6j7nTbRd{GVj E2UWJq2LJ#7 literal 0 HcmV?d00001 diff --git a/tests/vectors/noir/poseidon2/vk_hash b/tests/vectors/noir/poseidon2/vk_hash new file mode 100644 index 0000000..a63bb9c --- /dev/null +++ b/tests/vectors/noir/poseidon2/vk_hash @@ -0,0 +1 @@ +.u¯‚a­Ëéɵýº^Ń’‚cÏ?;³æ†XT¶p+{# \ No newline at end of file diff --git a/tests/vectors/noir/requests/req_srs_g1only.bin b/tests/vectors/noir/requests/req_srs_g1only.bin new file mode 100644 index 0000000000000000000000000000000000000000..3826382e5fead818d895a963caa0027e5fa20073 GIT binary patch literal 242 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