Skip to content

fix(cache): serve prepared points only for the points they came from - #35

Merged
markosg04 merged 3 commits into
a16z:mainfrom
quangvdao:fix/prepared-cache-match-points
Nov 2, 2026
Merged

markosg04 merged 3 commits into
a16z:mainfrom
quangvdao:fix/prepared-cache-match-points

Conversation

@quangvdao

@quangvdao quangvdao commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With the cache feature, the setup multi-pairings in ark_pairing.rs use the global prepared-point cache for any input no longer than the cache. They never check that the input is the cached points. So:

  • Wrong pairings. If the cache was built from another setup, a pairing silently returns a wrong result. This includes a larger setup generated separately: setups use OS randomness, so two of them share no points.
  • A panic. An input longer than the cache panics on the slice.
  • No recovery. init_cache keeps any cache at least as large as the input, so a second init_cache call cannot fix a wrong cache.

This is the same problem #29 addressed. #29 was closed because jolt-dory never fills the cache, so Jolt cannot hit the problem. This PR is for dory itself:

  • Dory's own tests. tests/arkworks/cache.rs fills the cache with random points. Under plain cargo test, every test in the test_arkworks binary shares one process, so later tests in that binary pair against those points. Their proofs then fail to verify (on this sandbox, five evaluation tests failed). CI uses nextest, which runs each test in its own process, so CI never sees this.
  • Other users. Any other user of cache who creates more than one setup in a process gets wrong results instead of an error.

Changes

  • PreparedCache stores the source points beside their prepared forms. New methods g1_prepared_for(points) and g2_prepared_for(points) return the prepared prefix only when points is exactly a prefix of the cached points. Otherwise they return None.
  • The four pairing paths in ark_pairing.rs (G1 and G2, sequential and parallel) use these lookups. On None, they prepare the input themselves, exactly as they do with no cache.
  • init_cache keeps the existing cache only when that cache covers the new points. Otherwise it replaces the cache.
  • Tests:
    • The tests that write the global cache now run one at a time, behind a shared lock.
    • foreign_cache_falls_back checks setup pairings against a naive pairing, with the cache filled from other points. It covers inputs both shorter and longer than the cache, from the G1 side and the G2 side.
    • reinit_with_other_points_replaces checks that init_cache replaces a cache built from other points.
    • proof_verifies_after_foreign_cache runs prove and verify end to end after a foreign cache.
    • cache_hit_matches_naive_pairing pairs prefixes of length 1, n/2 and n of the cached points (the cache-hit path) and checks them against a naive pairing.
    • non_prefix_points_fall_back checks that an offset slice of the cached points, and a prefix with its last point changed, miss the cache and still pair correctly.
  • CHANGELOG.md: Unreleased entries under Fixed, Added and Changed.

Breaking: all of PreparedCache's fields are now private, including g1_prepared and g2_prepared. Prepared points are reachable only through g1_prepared_for and g2_prepared_for, so downstream code can't index them without the prefix check. New g1_len and g2_len return the cache size. Code outside the crate can't build a PreparedCache with a struct literal. init_cache builds it.

Cost: each lookup compares the input with the cached points (n projective equality checks, in parallel). This is not free. In one release-mode measurement on a Mac, the check for 1024 G2 points took about 8 ms, against about 101 ms for the cache-hit multi_pair_g2_setup it guards, so roughly 8% of that pairing. The prover repeats it for each setup pairing in every reduce round. Tagging the cache with a setup identity would remove most of this cost; this PR does not do that.

Validation

On a Linux sandbox, against main at 5246da5:

  • On main, foreign_cache_falls_back and proof_verifies_after_foreign_cache fail, under nextest as well. They pass on this branch.
  • All CI steps pass on this branch:
    • cargo fmt --check;
    • clippy with the transparent features, with zk, and with --no-default-features;
    • cargo doc with -D warnings;
    • all five examples;
    • cargo nextest with the transparent features (88 passed);
    • cargo nextest with zk: 4 of 5 cold runs passed (145 tests each). One run failed test_workflow_without_precommitment and test_zero_polynomial. Those tests don't use the cache, and the failure did not reproduce. I didn't capture its output. The likely cause is the disk-persisted setup race fixed in fix(setup): publish setup files atomically and keep the first one saved #34.
  • Plain cargo test still fails on this branch with only this change, because of that setup-file race. With fix(setup): publish setup files atomically and keep the first one saved #34 also applied, plain cargo test --features backends,parallel,cache,disk-persistence,zk passed:
    • 3 runs out of 3 with parallel threads;
    • 1 run with --test-threads=1;
    • 1 run with the transparent features.

🤖 Generated with Claude Code

The setup multi-pairings took cached prepared points for any input no
longer than the cache, without checking the input. A cache built from
another setup gave wrong pairings, and an input longer than the cache
panicked. The cache now keeps its source points. A lookup serves a prefix
of them only, and the pairing otherwise prepares its input. init_cache
replaces a cache that does not cover its input.

The cache tests take a shared lock, and new tests pin the fallback, the
replacement, and a proof that verifies after a foreign cache.
g1_prepared and g2_prepared were still public, so downstream code could
index them without the prefix check that g1_prepared_for and
g2_prepared_for apply. Make them private and add g1_len and g2_len for
callers that only need the cache size.

Add tests that pair prefixes of the cached points (the cache-hit path)
against a naive pairing, and that offset or altered point sets miss the
cache and still pair correctly.
@quangvdao
quangvdao marked this pull request as ready for review October 29, 2026 21:09
@markosg04
markosg04 merged commit 45d8d18 into a16z:main Nov 2, 2026
8 checks passed
moodlezoup pushed a commit to a16z/jolt that referenced this pull request Nov 2, 2026
* chore(deps): bump dory-pcs to 0.5.0

Picks up atomic first-writer-wins setup files (a16z/dory#34), the
prepared-point cache prefix check (a16z/dory#35) and cyclotomic GT
exponentiation in the verifier (a16z/dory#36). Refreshes the two
scheme.rs comments that described the old dory behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(fuzz): bump dory-pcs to 0.5.0 in fuzz lockfiles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.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.

2 participants