Skip to content

Feature/bls12 381 dory pc#96

Open
Zyra-V21 wants to merge 23 commits intomicrosoft:mainfrom
Zyra-V21:feature/bls12-381-dory-pc
Open

Feature/bls12 381 dory pc#96
Zyra-V21 wants to merge 23 commits intomicrosoft:mainfrom
Zyra-V21:feature/bls12-381-dory-pc

Conversation

@Zyra-V21
Copy link

Add BLS12-381 Curve Provider and Dory-PC Backend

Summary

This PR adds support for:

  1. BLS12-381 curve as a new curve provider
  2. Dory-PC as an alternative polynomial commitment scheme

Motivation

BLS12-381 is widely used in production systems (Ethereum 2.0, Zcash) and offers strong security guarantees. Dory-PC provides O(1) commitment size and O(log n) verification.

Changes

New Files

  • src/provider/bls12_381.rs - BLS12-381 curve implementation via halo2curves
  • src/provider/pcs/dory_pc.rs - Dory-PC adapter wrapping quarks-zk crate
  • benches/pcs_comparison.rs - Benchmark comparing Hyrax vs Dory
  • examples/sha256_bls12_381_dory.rs - SHA-256 circuit example with Dory-PC
  • examples/BENCHMARK_RESULTS.md - Benchmark results

Modified Files

  • src/provider/mod.rs - Register BLS12381HyraxEngine and BLS12381DoryEngine
  • src/provider/pcs/mod.rs - Export dory_pc module
  • Cargo.toml - Add dependencies (quarks-zk, ark-* crates)

New Engines

pub struct BLS12381HyraxEngine;  // BLS12-381 with Hyrax-PC
pub struct BLS12381DoryEngine;   // BLS12-381 with Dory-PC

Benchmark Results (Simple Circuit)

Phase Hyrax Dory Notes
Setup 104.58 ms 376.75 µs Dory 278x faster (O(1))
Prove 32.12 ms 502.53 ms Hyrax 15.6x faster
Verify 30.77 ms 254.45 ms Hyrax 8.3x faster

SHA-256 Circuit Results (Heavy Circuit, 131K+ constraints)

Message Setup Prove Verify
256 bytes 856 ms 14,777 ms 1,852 ms
512 bytes 1,413 ms 14,649 ms 1,666 ms

Performance Observations

What works well:

  • Setup scales well - Dory O(1) commitment size means setup doesn't grow proportionally with circuit size
  • Verification is reasonable - O(log n) verification works as expected

Current bottlenecks:

  • Prove time is the main bottleneck - ~65% of prove time (9.5s of 14.7s) is spent in PCS prove due to pairing operations in BLS12-381
  • BLS12-381 pairing operations are inherently slower than secp256r1 group operations (used by T256HyraxEngine)

Potential optimizations (not in scope for this PR):

  • GPU acceleration for MSM and pairing operations
  • Better parallelism in commitment computation
  • Batched pairing verification

Assessment

This is a research-grade implementation. The numbers are expected for:

  • A heavy circuit (SHA-256 with 131K+ constraints)
  • BLS12-381 curve with pairing operations
  • No GPU or hardware acceleration

For production use, significant optimization work would be needed.

Testing

  • 16 new tests for BLS12-381 provider
  • 11 new tests for Dory-PC adapter (correctness + soundness)
cargo test --features dory
cargo bench --bench pcs_comparison --features dory
cargo run --release --example sha256_bls12_381_dory --features dory

Dependencies

New optional

@Zyra-V21
Copy link
Author

@microsoft-github-policy-service agree

Ok(DoryBlind)
}

fn prove(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a PCS evaluation argument where claimed values are committed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current understanding:

  • Spartan stores eval_W in plaintext in SpartanSNARK struct (line 107, spartan.rs)
  • Spartan recomputes comm_eval_W = PCS::commit([eval_W]) in verifier
  • My current DoryEvaluationArgument duplicates the value in value_bytes

Question: Should I:

  1. Remove value_bytes from DoryEvaluationArgument entirely?
  2. If so, how should verify() obtain the value to call quarks-zk::verify_eval(value, ...)?
  3. Should I deserialize from comm_eval parameter, or is the value expected to come from Spartan's proof struct?

Looking at Hyrax-PC, the IPA.verify() implicitly validates the value through the inner product check. Dory's verify_eval() explicitly needs the value parameter.

Could you clarify the expected flow for getting the evaluation value to the PCS verifier?

Thanks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current implementation stores the claimed value in DoryEvaluationArgument.value (native ArkFr).

The value is implicitly committed through the comm_eval parameter:

  • Prover computes comm_eval = PCS::commit([eval_W], blind) (spartan.rs:306)
  • Verifier receives both comm_eval and arg.value
  • Verifier can check commit([arg.value]) == comm_eval for binding

Is this the pattern you're expecting, or should we handle it differently?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spartan ZK variant does not store evaluation values in plaintext.

Addresses review comment microsoft#1 from @srinathsetty

Changes:
- Updated quarks-zk to v0.1.2 with rerandomization support
- Implemented rerandomize_commitment using Vega paper approach
- Added h_gt generator to DoryCommitmentKey for rerandomization
- Added 3 rigorous tests validating unlinkability and correctness

Reference: Vega paper (eprint 2025/2094) Section 2.1
…ument

- Store DoryPCSEvaluationProof and ArkFr natively (not Vec<u8>)
- Custom serde implementation using ark_serialize
- Addresses review microsoft#3: no hacky manual serialization
Apologies for the noise. Removed:
- examples/benchmarks/** (criterion-generated files)
- PR_DESCRIPTION.md (local reference)

These were included unintentionally in the previous commit.
(n as f64).log2().ceil() is imprecise at power-of-two boundaries
(e.g. n=4096 could yield 11.999... -> 12 or 12.000... -> 12).
Use (usize::BITS - (n-1).leading_zeros()) which is exact for all
inputs and avoids any floating-point dependency in cryptographic code.
DoryPCSParams doesn't implement Serialize/Deserialize, so it can't
be stored in the commitment key. Since setup uses a fixed seed
(ChaCha20Rng::seed_from_u64(0)), params for a given num_vars are
deterministic. We cache them in a static OnceLock<Mutex<HashMap>>
keyed by num_vars, computing them once and cloning on reuse.

This eliminates redundant pairing-heavy SRS generation that was
happening on every commit(), prove(), and verify() call — the main
contributor to the ~9.5s prove bottleneck reported in benchmarks.
Replace inline QuarksDoryPCS::setup() call with get_dory_params().
commit() was regenerating the full SRS on every call — now it
hits the cache after the first invocation for a given num_vars.
Replace inline QuarksDoryPCS::setup() call with get_dory_params().
prove() was the worst offender — regenerating the full BLS12-381
SRS before every proof generation, accounting for most of the
~9.5s spent in PCS prove.
Replace inline QuarksDoryPCS::setup() call with get_dory_params().
The verifier no longer pays SRS generation cost on every verify call.
Byte concatenation produced invalid commitments that would fail
deserialization in verify(). Now we deserialize each DoryPCSCommitment,
multiply their tier2 elements in GT (additive notation in ark),
and reserialize. For single-commitment inputs (most circuits),
this is a zero-cost clone.
Old test asserted byte length grew (concat behavior). New tests:
- Single commitment: verify pass-through identity
- Multiple commitments: verify result is a valid GT element and
  differs from both inputs (non-trivial multiplication)
Was only checking bytes.is_empty(). Now attempts full
CanonicalDeserialize into DoryPCSCommitment, catching
truncated, corrupted, or malformed commitment bytes early.
Complements the empty-bytes test: verifies that arbitrary
non-GT bytes (0xDEADBEEF) are caught by deserialization validation.
Serialization failure is not an input length problem — it's an
internal error in the commitment pipeline. Use InternalError.
Deserialization failures are InvalidPCS (corrupt cryptographic data).
Serialization failure after rerandomize is InternalError (should
never happen with valid inputs).
Corrupt commitment bytes are a PCS-level error, not input length.
A failed verification is not an input length problem — it's a
proof verification error. ProofVerifyError is the canonical
variant for this in the SpartanError enum.
Machine-specific benchmark data doesn't belong in the repository.
Results are not reproducible across hardware and go stale quickly.
The PR description already contains the benchmark summary.
quarks-zk 0.1.5 depends on ark-ff/ark-serialize/ark-bls12-381 0.5.
Having 0.4 in Cargo.toml caused two incompatible versions of ArkFr
in the dependency graph, making all type conversions between
Spartan2's ark types and quarks-zk's ark types fail at compile time.
When setup was extracted to get_dory_params(), the local rng
variable was lost. prove_eval() still needs an rng for proof
generation. Restore it with the same deterministic seed.
Use explicit reassignment (a = a + b) instead of +=.
quarks-zk's Bls381GT implements Add (GT multiplication
over Fq12) but not the compound assignment variant.
@Zyra-V21
Copy link
Author

@srinathsetty Thanks for the thorough review. I've pushed a batch of fixes addressing the issues raised plus additional problems found during a deeper audit.


Addressing your review comments

Re: comment #1 — Rerandomization (Vega §2.1)

Even if Dory is hiding, we need to rerandomize it to enable reusable proving commitments to get ZK

This was addressed in b8b5931. Still in place and tested (3 rerandomization tests covering unlinkability and chained rerandomizations).

Re: comment #3 — Native types instead of pre-serialized bytes

We would like to keep unserialized items inside the proof objects so we can have strong guarantees

Addressed in fe880f2. DoryEvaluationArgument stores native DoryPCSEvaluationProof and ArkFr, with custom serde using ark_serialize::CanonicalSerialize.

Re: comment #2 — Committed evaluation values

We need a PCS evaluation argument where claimed values are committed
Spartan ZK variant does not store evaluation values in plaintext

This is the remaining architectural question. Currently DoryEvaluationArgument.value stores the evaluation as a native ArkFr. The Spartan proof flow (spartan.rs:305-306) creates comm_eval_W = PCS::commit([eval_W]) and passes it to both prove() and verify(), but the Dory adapter doesn't bind arg.value against comm_eval during verification — quarks_zk::verify_eval takes the value as an explicit parameter.

For the ZK variant where eval_W should not appear in plaintext, this would require either:

  1. The PCS verify extracting/validating the value from comm_eval internally
  2. A quarks-zk API change to support committed-value verification directly

Happy to discuss the preferred approach. This is the one open item.


Additional fixes in this push

Performance — params cache (8717cc7, d165a14, ccd76ca, 0e8f352)

QuarksDoryPCS::setup() (pairing-heavy SRS generation) was being called on every commit(), prove(), and verify() invocation. Since setup uses a fixed seed (ChaCha20Rng::seed_from_u64(0)), params for a given num_vars are deterministic. Added a thread-safe OnceLock<Mutex<HashMap<usize, DoryPCSParams>>> cache — params are computed once and reused. This was the main contributor to the ~9.5s prove bottleneck.

Correctness — combine_commitments (0effdd7, f236725)

Was concatenating serialized bytes, which produces invalid commitments that fail deserialization. Now deserializes each DoryPCSCommitment, multiplies tier2 elements in GT (additive notation in quarks-zk wraps Fq12 multiplication), and reserializes. Single-commitment inputs (most circuits) are a zero-cost clone.

Correctness — check_commitment (0c3df75, 217c7b0)

Was only checking bytes.is_empty(). Now attempts full CanonicalDeserialize into DoryPCSCommitment, catching truncated/corrupted/malformed bytes early. Added test for garbage bytes rejection.

Correctness — num_vars calculation (508a1d4)

(n as f64).log2().ceil() is imprecise at power-of-two boundaries. Replaced with usize::BITS - (n-1).leading_zeros() — exact integer arithmetic.

Correctness — error types (bb5d556, fcee309, 4a1477c, 3bb070a)

All error paths were using SpartanError::InvalidInputLength regardless of cause. Fixed:

  • Serialization failures → InternalError
  • Corrupt commitment deserialization → InvalidPCS
  • Proof verification failure → ProofVerifyError

Dependencies — ark- version bump (d52d812)*

quarks-zk 0.1.5 depends on ark-ff/ark-serialize/ark-bls12-381 0.5. Cargo.toml had 0.4, causing two incompatible ArkFr types in the dependency graph. Bumped to 0.5.

Cleanup — removed BENCHMARK_RESULTS.md (b6f7aa4)

Machine-specific data that goes stale. The PR description already has the benchmark summary.


All 36 tests pass (20 BLS12-381 provider + 16 Dory-PC adapter). Compiles clean with and without --features dory.

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.

2 participants