Skip to content

feat: BSB22 commitment verification for gnark logderivlookup proofs - #31

Merged
ananas-block merged 8 commits into
masterfrom
jorrit/feat-bs22-lookups
Jul 8, 2026
Merged

feat: BSB22 commitment verification for gnark logderivlookup proofs#31
ananas-block merged 8 commits into
masterfrom
jorrit/feat-bs22-lookups

Conversation

@ananas-block

Copy link
Copy Markdown
Contributor

Summary

Adds BSB22 Pedersen-commitment verification behind a new bsb22 feature, so this crate can verify gnark Groth16 proofs from circuits that use std/lookup/logderivlookup or api.Commit (single commitment). Also upgrades to solana-bn254 v3, moves SBF builds onto pinocchio syscalls, and reorganizes the vk parsers under src/vk/.

What the verifier does

For a proof with one BSB22 commitment, Groth16Verifier::new_with_commitment mirrors gnark backend/groth16/bn254/verify.go:

  1. Hash the 64-byte commitment to a BN254 Fr element (RFC 9380 expand_message_xmd over SHA-256, DST bsb22-commitment, byte-exact with gnark-crypto fr.Hash and pinned by golden vectors).
  2. Extend the public-input MSM with the hash times the trailing K column, then add the raw commitment point (kSum).
  3. Run the standard 4-pair Groth16 pairing check.
  4. Check the Pedersen knowledge proof: e(commitment, g_sigma_neg) * e(pok, g) == 1. For a single commitment, gnark's BatchVerifyMultiVk fold reduces to exactly this (the fold scalar for index 0 is 1), so no Fiat-Shamir challenge is needed.

Circuits that commit to public inputs (PublicAndCommitmentCommitted non-empty) and multi-commitment vks are rejected at parse time with dedicated errors.

API

  • Groth16Verifyingkey gains an un-gated vk_commitment: Option<CommitmentVerifyingKey> field. Breaking: existing baked vk consts must add vk_commitment: None. The standard new() rejects commitment vks with UnexpectedCommitmentKey in all builds.
  • vk::gnark::parse_gnark_vk_bytes parses gnark VerifyingKey.WriteRawTo binaries including the trailing commitment sections; vk::gnark::generate_bsb22_vk_file bakes a vk into a pub const from build.rs (host-only, compiled out on SBF).
  • vk::circom (renamed from vk_parser) emits the new field and returns field-naming errors instead of underflow panics on malformed JSON.
  • Removed never-constructed error variants (IncompatibleVerifyingKeyWithNrPublicInputs, InvalidG1Length, InvalidG2Length); new variants label commitment-specific failures (Bsb22InvalidCommitmentPoint, Bsb22CommittedPublicInputsUnsupported, Bsb22VkFileIoFailed).

Cost

~212k CU per BSB22 verify on Solana, measured by tests/bsb22-program/tests/litesvm_cu.rs (asserts < 350k).

Testing

Three layers, all anchored on gnark itself:

  • Lib tests (cargo test -p groth16-solana --features bsb22): committed fixture e2e, hash-to-field golden vectors from gnark-crypto v0.19.0, parser accept/reject cases.
  • FFI integration tests (tests/bsb22, requires Go): an in-repo cgo gnark fixture runs Setup/Prove/Verify per test run with fresh randomized setups across three lookup-count variants, plus negative tests for mutated public input, commitment, and PoK. gnark's own verifier is run on the same bytes as the chain-of-trust anchor.
  • SBF program test (cargo test-sbf -p bsb22-integration-program): litesvm end-to-end verify with the vk baked via the codegen path, plus a mutated-input rejection.

The port was additionally reviewed line-by-line against gnark v0.14.0 / gnark-crypto v0.19.0 sources and RFC 9380.

ananas-block and others added 8 commits March 19, 2026 22:32
…k deps

- Bump solana-bn254 from "2" to "3" — v3 uses ark-ff 0.5, eliminating
  the duplicate ark-ff 0.4 compilation from the dep graph
- Set default-features = false for thiserror, ark-serialize, ark-ec,
  ark-ff, ark-bn254, num-bigint, serde, serde_json — avoids pulling in
  std/alloc features that downstream Solana programs don't need
- Keep features = ["curve"] on ark-bn254 (required for BN254 curve ops)
Adds a `bsb22` cargo feature that extends the existing Groth16
verifier to accept gnark proofs carrying one BSB22 Pedersen
commitment plus its knowledge proof. Every circuit that touches
`std/lookup/logderivlookup` or any emulated-field range-check
helper ends up with exactly one commitment via gnark's
multicommit merging, so this unlocks on-chain verification of
those circuits without any changes to the vanilla verifier path.

New public API (gated on `bsb22`):
- `Groth16Verifier::new_with_commitment` — mirrors `new` plus the
  commitment and PoK G1 points; rejects vks that lack the
  Pedersen commitment key.
- `Groth16Verifyingkey::vk_commitment_g2` / `vk_commitment_g_sigma_neg_g2`
  as `Option<[u8; 128]>` fields; vanilla `new` refuses keys with
  these set.
- `groth16::negate_g1_be` — public helper that replaces five
  inlined copies of the gnark BE negate-and-reserialize dance.
- `hash_to_field::hash_to_field_bn254_fr` — RFC 9380
  expand_message_xmd with SHA-256, byte-exact with
  gnark-crypto's `fr.Hash`. Uses the `sol_sha256` syscall on
  chain via a raw extern binding; zero heap allocations (stack
  scratch buffer).
- `gnark_vk_parser::parse_gnark_vk_bytes` — reads gnark's
  `VerifyingKey.WriteRawTo` binary including the trailing
  `PublicAndCommitmentCommitted` / `CommitmentKeys` sections.
  Rejects multi-commitment and trailing garbage with
  `Bsb22InvalidVerifyingKeyBinary` / `Bsb22UnsupportedMultiCommitment`.
- `gnark_vk_parser::{bsb22_vk_to_rust_const, generate_bsb22_vk_file}`
  for downstream build scripts that want to bake a vk into
  `.rodata`.

Verification algorithm mirrors gnark's
`backend/groth16/bn254/verify.go:47-133` (single-commitment path):
hash-to-field → extended prepare_inputs with commitment MSM + raw
G1 addition → standard 4-pair Groth16 pairing → 2-pair Pedersen
PoK pairing. For single-commitment the gnark fold collapses to
identity so no Fiat-Shamir challenge is needed.

Test infrastructure:
- `tests/bsb22/gnark-fixture/` — single-file Go module exposing
  Setup/Prove/NativeVerify via cgo for Lookups1/2/3Circuit
  (1, 2, 3 logderivlookup queries, all merged into one
  commitment by gnark's multicommit).
- `tests/bsb22/` — integration test crate that compiles the Go
  fixture to a C archive via bindgen and runs the verifier
  against real proofs from each variant; 7 positive+negative
  tests.
- `tests/bsb22-program/` — tiny cdylib Solana program + litesvm
  test that measures empirical on-chain CU.
- `tests/fixtures/bsb22/` — committed binary snapshot (vk +
  proof + public input) shared between the lib-internal
  end-to-end test, the SBF program's build.rs, and the parser
  unit tests.

Empirical on-chain CU: ~223,558 per BSB22 verify (vanilla
Groth16 is ~170k; BSB22 adds one hash-to-field, one G1 mul, two
G1 additions, and one 2-pair pairing).

The vanilla Groth16 path is byte-identical to before — 6/6 existing
tests pass unchanged with default features, all new code is gated.

Transitive dep pin required for the SBF toolchain (rustc 1.84):
blake3 is pinned to =1.8.2 in `[workspace.dependencies]` to keep
`constant_time_eq` on 0.3.x, which doesn't require edition2024.
alt_bn128_addition       -> alt_bn128_g1_addition_be
alt_bn128_multiplication -> alt_bn128_g1_multiplication_be
alt_bn128_pairing        -> alt_bn128_pairing_be
alt_bn128_g1_decompress  -> alt_bn128_g1_decompress_be
alt_bn128_g2_decompress  -> alt_bn128_g2_decompress_be
- Add src/syscalls.rs re-exporting g1_addition_be / g1_multiplication_be /
  pairing_be from pinocchio when targeting Solana and from solana-bn254 v3
  on host. Lets the crate build under pinocchio without pulling solana-bn254
  into SBF builds.
- Replace .concat() input assembly in groth16.rs with stack-allocated fixed
  buffers (96 / 128 / 384 / 768 bytes), removing per-call Vec allocations.
- is_less_than_bn254_field_size_be uses ark BigInt::cmp directly; num-bigint
  becomes optional and is gated behind the existing 'vk' feature.
- Bump thiserror 1 -> 2.

Test and VK-parser updates carry the corresponding API and feature-gate
changes.
A bare extern "C" sol_sha256 symbol does not resolve under the
static-syscalls SBF ABI (platform-tools v1.54+); the call silently
leaves the output buffer zeroed. That zeroes the BSB22
commitment-derived public input, so every committed (merge / P256)
on-chain proof verification fails while host verification passes.
Use pinocchio's define_syscall!-bound sol_sha256, matching how the
alt_bn128 ops are already bound.
- Merge the two cfg-gated commitment-key fields into a single ungated
  vk_commitment: Option<CommitmentVerifyingKey>; the type now enforces
  that both G2 points are set together, and vk consts no longer need
  feature-conditional fields. Standard new() rejects commitment vks in
  all builds. Breaking for baked consts: add vk_commitment: None.
- Move vk generators to src/vk/ as vk::gnark and vk::circom; codegen is
  now private, host-only (cfg(not(target_os = "solana"))), and emits
  the merged field. circom codegen returns meaningful errors instead of
  underflow panics on malformed JSON (new integration tests).
- Extract g1_mul_add/g1_add and verify_commitment_pok helpers; delete
  statically dead length checks; specialize expand_message_xmd to L=48;
  unify Cursor bounds checks with checked_add.
- Target-gate sha2 to host builds; syscalls and hash_to_field are now
  pub(crate).
- Collapse the three Go fixture circuits into one parameterized
  LookupsCircuit; drop go mod tidy from the build script; delete
  superseded TestDumpVkBytes.
- Naming pass: rename generic/abbreviated locals, replace vanilla with
  standard Groth16, migrate deprecated alt_bn128 compress calls to _be.
- Docs: fix stale field names, CU figures (~212k measured), dangling
  fixture-README pointer, machine-local path in README.
- Add UnexpectedCommitmentKey (ungated), Bsb22CommittedPublicInputsUnsupported,
  Bsb22VkFileIoFailed, and Bsb22InvalidCommitmentPoint; delete the never-
  constructed IncompatibleVerifyingKeyWithNrPublicInputs, InvalidG1Length,
  and InvalidG2Length (codes 0/4/5 stay unassigned).
- Check vk_commitment before the vk_ic length check in both constructors so
  the commitment mismatch is reported instead of being shadowed by
  InvalidPublicInputsLength.
- Label an off-curve prover-supplied commitment as Bsb22InvalidCommitmentPoint
  instead of PreparingInputsG1AdditionFailed; tighten the mutated-commitment
  FFI assertion from four accepted variants to three.
- Split Bsb22UnsupportedMultiCommitment: committed public inputs now report
  Bsb22CommittedPublicInputsUnsupported, lockstep mismatch reports
  Bsb22InvalidVerifyingKeyBinary; generate_bsb22_vk_file IO failures report
  Bsb22VkFileIoFailed instead of invalid-binary.
- Prose pass over new docs/comments: fix the wrong fail-closed claim on
  verify_commitment_pok, name concrete error variants in comments, drop
  filler wording.
The bsb22-integration-program litesvm test loads a prebuilt
bsb22_integration_program.so from target/deploy, produced by
cargo build-sbf. cargo test --workspace does not build it, so the
test panicked with a missing-.so error. Install the Solana toolchain
and run cargo build-sbf for the program before the workspace test so
the .so exists at the path the test expects.
@ananas-block
ananas-block merged commit c4cb0f4 into master Jul 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant