Skip to content

perf: speed up batch verification - #151

Open
sdbondi wants to merge 1 commit into
mainfrom
perf/batch-verification
Open

perf: speed up batch verification#151
sdbondi wants to merge 1 commit into
mainfrom
perf/batch-verification

Conversation

@sdbondi

@sdbondi sdbondi commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Speeds up batch verification without changing what is accepted or rejected. Medians on an M-series MacBook, 64-bit proofs (harness included as examples/verify_bench.rs):

Case main this PR Change
1 proof 0.73 ms 0.69 ms −5%
16 proofs 4.90 ms 4.34 ms −11%
64 proofs 18.7 ms 13.7 ms −27%
256 proofs 77.0 ms 50.3 ms −35%
64 proofs, RecoverOnly 8.8 ms 3.1 ms −65%
64 proofs, RecoverAndVerify 20.5 ms 14.8 ms −28%

Changes

  1. Choose the final multiscalar multiplication strategy by estimated cost. The final check always used the precomputed-Straus mixed MSM, whose per-point cost is constant (~51 curve additions per dynamic point). Each proof contributes ~16 dynamic points, so large batches ran far past the size where Pippenger (~33 additions per point, and falling with size) overtakes it. Verification now estimates both costs, mirroring the algorithm parameters in curve25519-dalek, and picks the cheaper; small batches keep the precomputed path. Both strategies compute the same sum, so acceptance is unchanged.

  2. Skip redundant generator consistency comparisons. The batch consistency check compared generator vectors element-wise between statements — tens of thousands of point-equality checks for large batches of statements cloned from the same RangeParameters. The precomputation table Arc is created exactly once alongside the generator vectors it is built from, so Arc::ptr_eq proves the vectors identical; mismatches still fall back to the full element-wise comparison.

  3. Halve the scalar work in the per-element loop. The batch weight is folded into per-proof constants, and d[i] * y^(nm−i) is tracked incrementally (one multiplication per element, with a correction at aggregation-block boundaries) instead of materializing the d vector — roughly 10 → 5 scalar multiplications per element, and one fewer O(mn) allocation per proof.

  4. RecoverOnly skips verification-only work. Point decompression (3 + 2·rounds points per proof) ran before mask recovery even though recovery never touches the points. Decompression now happens after the RecoverOnly continue, which is where the ~2.8× recovery speedup comes from. Behavior note: RecoverOnly no longer rejects proofs whose points are non-canonical encodings, since it no longer decompresses them; the structural length checks still run, and both verifying modes are unchanged.

  5. Mask recovery does one inversion per proof instead of two per proof per extension degree, by algebraically folding the and z²·y^(nm+1) divisions into mask = (d1 − η − e·d − residue·e²) · (e²·z²·y^(nm+1))⁻¹.

  6. Removed per-proof clones of commitments, d1, and minimum_value_promises.

Testing

  • Full test suite passes; cargo lints clippy --all-targets --all-features is clean; the no_std build (--no-default-features) checks; nightly cargo fmt --check is clean.
  • New test test_large_batch_invalid_proof_is_rejected exercises rejection through the plain-MSM path (a 100-proof batch crosses the dispatch threshold).
  • Reproduce the numbers with cargo run --release --example verify_bench.

🤖 Generated with Claude Code

- Choose the final multiscalar multiplication strategy by estimated
  cost: precomputed Straus for small batches, plain Pippenger for
  large ones (~35% faster at 256 proofs)
- Skip element-wise generator consistency comparisons when statements
  share the same underlying generator data (Arc pointer equality)
- Fold the batch weight into per-proof constants and track
  d[i] * y^(nm-i) incrementally instead of materializing d, roughly
  halving scalar multiplications in the per-element loop
- Defer proof point decompression until after mask recovery so
  RecoverOnly skips it entirely (~2.8x faster)
- Combine the two mask recovery inversions into one per proof
- Remove per-proof clones of commitments, d1, and minimum value
  promises
- Add examples/verify_bench.rs timing harness

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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