Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 91 additions & 48 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## Test coverage

`cargo test --workspace` runs the unit tests and the FFI differential
tests, but not the `circom-vk`-gated codegen tests: no workspace member
enables that feature as a normal dependency, so they need the explicit
`--features` invocation below. The mollusk suites need `cargo build-sbf`
first, and `tests/rust-vk` needs its npm build. CI runs `just ci` with
`cargo test --workspace` runs the unit tests, the codegen tests and the
FFI differential tests. The `circom-vk`-gated codegen tests only run
there because `tests/rust-vk` enables `circom-vk` through a
dev-dependency; the all-features invocation below does not depend on
that. The mollusk suites need `cargo build-sbf` first, and
`tests/rust-vk` needs its npm build. CI runs `just ci` with
`PROPTEST_CASES=1000000`: lint (fmt check, clippy, the feature-matrix
compile, README sync via cargo-rdme), the no_std compile check
(`check-nostd`), circuit build, workspace build and tests, the
Expand All @@ -25,55 +26,83 @@ check.
6. `groth16::tests::test_is_less_than_bn254_field_size_be`
7. `groth16::tests::fr_modulus_constant_matches_ark` — pins
`FR_MODULUS_BE` to `ark_bn254::Fr::MODULUS`
8. `groth16::tests::bsb22_e2e::bsb22_e2e_verifies` — deterministic
8. `groth16::tests::delta_equal_gamma_vk_accepts_forged_proof` — the
test vk has delta == gamma (no phase-2 contribution), so
`A = α, B = β, C = -L(x)` verifies for arbitrary inputs; the same
forgery fails once delta differs
9. `groth16::tests::bsb22_e2e::bsb22_e2e_verifies` — deterministic
gnark fixture, positive path
9. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_mutated_public_input`
10. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_public_input_greater_than_field_size`
11. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_substituted_on_curve_commitment`
12. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_substituted_on_curve_pok`
13. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_swapped_commitment_and_pok`
14. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_identity_commitment_and_pok`
15. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_not_on_curve_commitment`
16. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_not_on_curve_pok`
17. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_tampered_commitment_key`
18. `groth16::tests::bsb22_e2e::bsb22_e2e_new_rejects_bsb22_vk`
19. `groth16::tests::bsb22_e2e::bsb22_e2e_new_with_commitment_rejects_standard_vk`
20. `groth16::tests::bsb22_e2e::bsb22_e2e_new_with_commitment_rejects_short_vk_ic`
21. `hash_to_field::tests::matches_gnark_empty` — gnark-crypto golden
10. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_mutated_public_input`
11. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_public_input_greater_than_field_size`
12. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_substituted_on_curve_commitment`
13. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_substituted_on_curve_pok`
14. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_swapped_commitment_and_pok`
15. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_identity_commitment_and_pok`
16. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_not_on_curve_commitment`
17. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_not_on_curve_pok`
18. `groth16::tests::bsb22_e2e::bsb22_e2e_rejects_tampered_commitment_key`
19. `groth16::tests::bsb22_e2e::bsb22_e2e_new_rejects_bsb22_vk`
20. `groth16::tests::bsb22_e2e::bsb22_e2e_new_with_commitment_rejects_standard_vk`
21. `groth16::tests::bsb22_e2e::bsb22_e2e_new_with_commitment_rejects_short_vk_ic`
22. `hash_to_field::tests::matches_gnark_empty` — gnark-crypto golden
vector
22. `hash_to_field::tests::matches_gnark_abc` — golden vector
23. `hash_to_field::tests::matches_gnark_zero_g1` — golden vector
24. `hash_to_field::tests::matches_gnark_sequential` — golden vector
25. `hash_to_field::tests::reference_matches_rfc9380_vectors` —
23. `hash_to_field::tests::matches_gnark_abc` — golden vector
24. `hash_to_field::tests::matches_gnark_zero_g1` — golden vector
25. `hash_to_field::tests::matches_gnark_sequential` — golden vector
26. `hash_to_field::tests::reference_matches_rfc9380_vectors` —
validates the RustCrypto reference expander against all 10 RFC
9380 CFRG vectors
26. `hash_to_field::tests::expander_matches_reference_grid` —
27. `hash_to_field::tests::expander_matches_reference_grid` —
deterministic msg/dst-length grid vs that reference at L = 48
27. `hash_to_field::tests::prop_expander_bsb22_shape` — proptest,
28. `hash_to_field::tests::prop_expander_bsb22_shape` — proptest,
64/16-byte shape (`PROPTEST_CASES`, default 1000)
28. `hash_to_field::tests::prop_expander_max_msg` — proptest,
29. `hash_to_field::tests::prop_expander_max_msg` — proptest,
187/1-byte shape
29. `hash_to_field::tests::prop_reduce_be_l48_matches_single_buffer`
30. `hash_to_field::tests::prop_reduce_be_l48_matches_single_buffer`
— proptest, production halved reduction vs arkworks'
whole-buffer reduction
30. `hash_to_field::tests::reduce_be_l48_matches_single_buffer_at_bounds`
31. `hash_to_field::tests::reduce_be_l48_wraps_on_final_add` — r,
31. `hash_to_field::tests::reduce_be_l48_matches_single_buffer_at_bounds`
32. `hash_to_field::tests::reduce_be_l48_wraps_on_final_add` — r,
r + 1 and the largest multiple of r below 2^384, where the final
add wraps past r
32. `vk::gnark::tests::parse_bsb22_vk_shape`
33. `vk::gnark::tests::rejects_truncated_input`
34. `vk::gnark::tests::rejects_trailing_bytes`
35. `vk::gnark::tests::rejects_oversized_nb_k_without_allocating`
36. `vk::gnark::tests::rejects_multi_commitment`
37. `vk::gnark::tests::rejects_multi_commitment_keys`
38. `vk::gnark::tests::rejects_lockstep_mismatch`
39. `vk::gnark::tests::rejects_committed_public_inputs`
40. `vk::gnark::tests::bsb22_vk_to_rust_const_roundtrip`
41. `vk::gnark::tests::generate_bsb22_vk_file_reports_io_error_for_missing_input`
42. `circom_vk_codegen::generates_const_with_vk_commitment_none`
43. `circom_vk_codegen::rejects_empty_ic`
44. `circom_vk_codegen::rejects_empty_point_coordinates`
45. `readme_benchmarks::crate_docs_cu_table_matches_benchmarks` — pins
33. `vk::gnark::tests::parse_bsb22_vk_shape`
34. `vk::gnark::tests::rejects_truncated_input`
35. `vk::gnark::tests::rejects_trailing_bytes`
36. `vk::gnark::tests::rejects_oversized_nb_k_without_allocating`
37. `vk::gnark::tests::rejects_multi_commitment`
38. `vk::gnark::tests::rejects_multi_commitment_keys`
39. `vk::gnark::tests::rejects_lockstep_mismatch`
40. `vk::gnark::tests::rejects_committed_public_inputs`
41. `vk::gnark::tests::bsb22_vk_to_rust_const_roundtrip` — includes the
insecure-test-setup header, consts and compile guard
42. `vk::gnark::tests::bsb22_vk_to_rust_const_production_has_no_guard`
43. `vk::gnark::tests::bsb22_vk_to_rust_const_rejects_production_vk_with_delta_equal_gamma`
44. `vk::gnark::tests::bsb22_vk_to_rust_const_accepts_insecure_test_vk_with_delta_equal_gamma`
45. `vk::gnark::tests::generate_bsb22_vk_file_pins_proving_key_sha256`
— the SHA-256 of an "abc" proving key file appears in the
generated const
46. `vk::gnark::tests::generate_bsb22_vk_file_reports_io_error_for_missing_proving_key`
47. `vk::gnark::tests::generate_bsb22_vk_file_reports_io_error_for_missing_input`
48. `vk::setup::tests::proving_key_sha256_matches_known_vector` — FIPS
180-2 "abc"
49. `vk::setup::tests::proving_key_sha256_streams_across_chunks` —
multi-chunk file vs one-shot SHA-256
50. `vk::setup::tests::proving_key_sha256_reports_missing_file`
51. `vk::setup::tests::metadata_marks_insecure_test_setup_with_compile_guard`
52. `vk::setup::tests::metadata_production_has_no_compile_guard`
53. `vk::setup::tests::metadata_export_name_depends_on_vk_source` —
two vks with the same const name export different symbols
54. `vk::setup::tests::find_setup_txts_reads_markers_between_other_bytes`
55. `vk::setup::tests::find_setup_txts_returns_empty_without_markers`
56. `vk::setup::tests::find_setup_txts_rejects_unterminated_marker`
57. `vk::setup::tests::find_setup_txts_rejects_malformed_fields`
58. `circom_vk_codegen::generates_const_with_vk_commitment_none`
59. `circom_vk_codegen::insecure_test_setup_emits_flag_header_and_compile_guard`
60. `circom_vk_codegen::production_setup_emits_flag_without_compile_guard`
61. `circom_vk_codegen::rejects_production_vk_with_delta_equal_gamma`
62. `circom_vk_codegen::rejects_empty_ic`
63. `circom_vk_codegen::rejects_empty_point_coordinates`
64. `readme_benchmarks::crate_docs_cu_table_matches_benchmarks` — pins
the CU table in the src/lib.rs crate docs (rendered into README.md
by cargo-rdme) to the BENCHMARKS.md totals

Expand Down Expand Up @@ -116,7 +145,8 @@ first.
compile/prove/verify smoke test
12. Go: `TestHashToFieldGoldenVectors` — prints the golden vectors
baked into `src/hash_to_field.rs`
13. Go: `TestGenerateIsDeterministic` — bench-generator determinism
13. Go: `TestGenerateIsDeterministic` — bench-generator determinism,
proving keys included

### Mollusk program tests (tests/program, requires Go + Solana toolchain)

Expand All @@ -125,11 +155,14 @@ overwrites the profiled build:

```sh
cargo build-sbf --manifest-path tests/program/Cargo.toml -- --features profile-program
cargo test -p bsb22-integration-program --test failing
cargo test -p bsb22-integration-program --test failing --test setup_txt
cargo test -p bsb22-integration-program --test bench_cu -- --ignored --nocapture
```

Each `failing.rs` test pins the exact `ProgramError` the client sees.
The baked vks come from a seeded gnark setup, so they are generated
as `SetupKind::InsecureTest` and the crate enables its
`insecure-test-setup` feature by default. Each `failing.rs` test pins
the exact `ProgramError` the client sees.

1. `rejects_unknown_selector`
2. `rejects_empty_instruction_data`
Expand All @@ -150,15 +183,25 @@ Each `failing.rs` test pins the exact `ProgramError` the client sees.
15. `rejects_swapped_commitment_and_pok`
16. `offsets_match_program_layout` — guards the test-side offsets
against instruction-format drift
17. `bench_cu` (`--ignored`) — executes all 8 variants successfully
17. `setup_txt_readable_from_program_binary` (`setup_txt.rs`) — reads
the exported `*_SETUP_TXT` markers back from the built .so: one
per vk, all flagged insecure, each with the SHA-256 of its
fixture pk.bin
18. `bench_cu` (`--ignored`) — executes all 8 variants successfully
under mollusk and regenerates BENCHMARKS.md

Deploy tooling reads the same markers with
`vk::setup::find_setup_txts`, from a local build or a
`solana program dump`; `strings program.so | grep -A7 "BEGIN GROTH16 VK SETUP"`
shows them by hand.

### Circom end-to-end (tests/rust-vk, requires npm + circom)

`npm install && npm run build-all` in `tests/rust-vk`, then
`cargo test -p rust-vk-integration-test`.

1. `test_compressed_account_proof_with_groth16_solana` — builds a
1. `test_compressed_account_proof_with_groth16_solana` — checks the
zkey's SHA-256 against `VERIFYINGKEY_PROVING_KEY_SHA256`, builds a
compressed-account Merkle-proof witness, proves with circom-prover,
and verifies with the vk const generated by `vk::circom` in
`build.rs`
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ check-cfg = ['cfg(target_os, values("solana"))']

[package]
name = "groth16-solana"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
description = "groth16 verification with solana alt_bn128 syscalls"
repository = "https://github.com/Lightprotocol/groth16-solana"
Expand Down Expand Up @@ -57,10 +57,13 @@ proptest = "1"

[features]
# snarkjs verification_key.json parser + Rust-const generator (vk::circom).
circom-vk = ["serde", "serde_json", "dep:num-bigint"]
# `sha2` hashes the proving key for the generated checksum const.
circom-vk = ["serde", "serde_json", "dep:num-bigint", "sha2"]
# gnark vk.WriteRawTo parser + Rust-const generator (vk::gnark).
# Independent of `bsb22`: parses plain and BSB22 vks alike.
gnark-vk = []
# Independent of `bsb22`: parses plain and BSB22 vks alike. `sha2`
# hashes the proving key; the activation is a no-op on the solana
# target, where the generator is compiled out.
gnark-vk = ["sha2"]
circom = ["circom-prover", "dep:ark-bn254", "dep:ark-ff"]
bsb22 = ["sha2", "dep:ark-bn254", "dep:ark-ff"]
# Test-only: re-exports `hash_to_field_bn254_fr` so the differential
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ at your own risk.
- `bsb22-test` — re-exports the hash-to-field internals for the
differential FFI tests. Test-only, not a stable API.

## Verifying key setup metadata

Both vk generators take a `SetupKind` and the proving key, and write
next to the vk const:

- `<NAME>_PROVING_KEY_SHA256` — a prover hashes the proving key it is
about to load (`vk::setup::proving_key_sha256`) and compares, so a
stale or mismatched key fails before proving.
- `<NAME>_INSECURE_TEST_SETUP` — `true` for `SetupKind::InsecureTest`
(seeded or otherwise untrusted setup randomness: whoever knows it
can make the vk accept a proof for any public inputs). Such a vk
only compiles in a crate that enables an `insecure-test-setup`
feature. Declaring `SetupKind::Production` for a vk whose delta
equals its gamma (no phase-2 contribution) is an error.
- `<NAME>_SETUP_TXT` — both values as an exported, delimited string
(the `security.txt` technique), so they can be read back from a
program binary after deployment with `vk::setup::find_setup_txts`,
or by hand:

```sh
solana program dump <PROGRAM_ID> program.so
strings program.so | grep -A7 "BEGIN GROTH16 VK SETUP"
```

## Benchmarks

End-to-end verification cost (proof parsing, verifier construction,
Expand Down
11 changes: 6 additions & 5 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ check-benchmarks:

# === Tests (per-suite test list in CLAUDE.md) ===

# The workspace run misses the `circom-vk`-gated codegen tests (no
# member enables that feature as a normal dependency), so `test` chains
# the all-features unit run, mirroring CI.
# The workspace run reaches the `circom-vk`-gated codegen tests only
# through tests/rust-vk's dev-dependency, so `test` chains the
# all-features unit run, mirroring CI.

# Workspace tests plus the all-features unit run
test: test-workspace test-unit
Expand All @@ -61,9 +61,10 @@ test-ffi:
test-go:
cd tests/gnark-ffi/gnark-fixture && go test ./...

# Mollusk negative tests for every verifier variant
# Mollusk negative tests for every verifier variant, plus the vk setup
# markers read back from the .so
test-program: build-program
cargo test -p bsb22-integration-program --test failing
cargo test -p bsb22-integration-program --test failing --test setup_txt

# === SBF program (tests/program) ===

Expand Down
15 changes: 12 additions & 3 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ pub enum Groth16Error {
#[cfg(feature = "gnark-vk")]
#[error("Bsb22CommittedPublicInputsUnsupported")]
Bsb22CommittedPublicInputsUnsupported,
/// Reading the vk.bin or writing the generated Rust file failed in
/// `generate_bsb22_vk_file` — a filesystem problem (wrong path,
/// permissions), not a malformed verifying key.
/// Reading the vk.bin or proving key, or writing the generated Rust
/// file, failed in `generate_bsb22_vk_file` — a filesystem problem
/// (wrong path, permissions), not a malformed verifying key.
#[cfg(feature = "gnark-vk")]
#[error("Bsb22VkFileIoFailed")]
Bsb22VkFileIoFailed,
Expand All @@ -71,6 +71,13 @@ pub enum Groth16Error {
#[cfg(feature = "bsb22")]
#[error("Bsb22InconsistentCommitmentState")]
Bsb22InconsistentCommitmentState,
/// The vk was declared `SetupKind::Production` but its delta equals
/// its gamma: the setup had no phase-2 contribution, so anyone can
/// build a proof for any public inputs from the vk alone. Declare
/// it `SetupKind::InsecureTest` if it is a test setup.
#[cfg(feature = "gnark-vk")]
#[error("ForgeableProductionVk")]
ForgeableProductionVk,
}

#[cfg(feature = "circom")]
Expand Down Expand Up @@ -116,6 +123,8 @@ impl From<Groth16Error> for u32 {
Groth16Error::Bsb22InvalidCommitmentPoint => 20,
#[cfg(feature = "bsb22")]
Groth16Error::Bsb22InconsistentCommitmentState => 21,
#[cfg(feature = "gnark-vk")]
Groth16Error::ForgeableProductionVk => 22,
}
}
}
39 changes: 39 additions & 0 deletions src/groth16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,45 @@ mod tests {
assert_eq!(FR_MODULUS_BE.to_vec(), ark_bn254::Fr::MODULUS.to_bytes_be());
}

/// `VERIFYING_KEY` is a snarkjs key exported before any phase-2
/// contribution: delta is still the G2 generator, equal to gamma.
/// Then `e(L, γ)·e(C, δ)` equals `e(L + C, δ)`, so
/// `A = α, B = β, C = -L(x)` verifies for any public inputs `x`,
/// and the vk generators reject `SetupKind::Production` for such a key.
#[test]
fn delta_equal_gamma_vk_accepts_forged_proof() {
assert_eq!(VERIFYING_KEY.vk_delta_g2, VERIFYING_KEY.vk_gamma_g2);
// Arbitrary public inputs, unrelated to any real witness.
let inputs: [[u8; 32]; 9] = core::array::from_fn(|i| {
let mut input = [0u8; 32];
if let Some(last) = input.last_mut() {
*last = i as u8 + 1;
}
input
});
let forge = |vk: &Groth16Verifyingkey| {
let mut prepare =
Groth16Verifier::new(&[0u8; 64], &[0u8; 128], &[0u8; 64], &inputs, vk).unwrap();
prepare.prepare_inputs::<true>().unwrap();
let proof_a = negate_g1_be(&vk.vk_alpha_g1);
let proof_c = negate_g1_be(&prepare.prepared_public_inputs);
Groth16Verifier::new(&proof_a, &vk.vk_beta_g2, &proof_c, &inputs, vk)
.unwrap()
.verify()
};
assert_eq!(forge(&VERIFYING_KEY), Ok(()));

// The same forgery fails once delta differs from gamma.
let contributed = Groth16Verifyingkey {
vk_delta_g2: VERIFYING_KEY.vk_beta_g2,
..VERIFYING_KEY
};
assert_eq!(
forge(&contributed),
Err(Groth16Error::ProofVerificationFailed)
);
}

#[test]
fn test_is_less_than_bn254_field_size_be() {
let bytes = [0u8; 32];
Expand Down
Loading
Loading