Visitar URL original
perf(dory): cyclotomic GT exponentiation and GT multi-exponentiation by moodlezoup · Pull Request #2174 · a16z/jolt · GitHub
Skip to content

perf(dory): cyclotomic GT exponentiation and GT multi-exponentiation - #2174

Merged
moodlezoup merged 2 commits into
mainfrom
perf/gt-cyclotomic-multiexp
Nov 3, 2026
Merged

moodlezoup merged 2 commits into
mainfrom
perf/gt-cyclotomic-multiexp

Conversation

@moodlezoup

@moodlezoup moodlezoup commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • dory-pcs (now on main). perf(arkworks): cyclotomic GT exponentiation and shared-squaring GT multi-exponentiation dory#36 scales ArkGT with cyclotomic exponentiation and adds PairingCurve::gt_msm, which shares one cyclotomic squaring chain across all terms. The Dory verifier uses gt_msm for its per-round C/D₁/D₂ updates and for the final check. It shipped in dory-pcs 0.5.0, which main picked up in chore(deps): bump dory-pcs to 0.5.0 #2236, so this PR no longer changes the dependency or lockfiles.
  • DoryScheme::combine now uses gt_msm (41 commitments at fib 2^16) instead of one exponentiation per commitment.
  • Bn254GT::scalar_mul and msm use cyclotomic_exp instead of the generic Fq12::pow.
  • Removes Bn254GT's public, unchecked From<Fq12>. It had no callers, and it was the only public way to build a Bn254GT outside GT, where cyclotomic arithmetic gives wrong results.

Group results are unchanged, so proofs are byte-identical, with no wire or transcript change.

Impact

Instructions retired under Callgrind, for jolt_verifier::verify only, single-threaded, fib. The main column is main before #2236, so the totals include the dory#36 savings that have since landed on main; on current main, combine still scales each commitment separately (the 197.0 M figure), and this PR's gt_msm brings it to 57.0 M.

main Cyclotomic scaling only This PR
Verify, 2^16 1,221.9 M 837.6 M (−31%) 483.2 M (−60%)
DoryScheme::verify 839.9 M 577.3 M 362.9 M
DoryScheme::combine 318.7 M 197.0 M 57.0 M
Stages 1–7 (unchanged) 62.9 M 62.9 M 62.9 M
Verify, 2^18 1,294.4 M 515.0 M (−60%)

Native single-threaded verify at 2^16 went from about 142 ms to 49–64 ms (a noisy measurement).

The measurement harness is not part of this PR. It wraps the single-threaded verify in profile.rs measure_verifier with Callgrind client requests and runs with --instr-atstart=no.

Soundness

Cyclotomic squaring, and conjugation as the inverse, are valid only in the cyclotomic subgroup of Fq12, which contains GT. Every GT element exponentiated here is one of:

  • a pairing output, or a product of pairing outputs;
  • a setup element;
  • a proof element, which ArkGT deserialization subgroup-checks (dory Reorganize file structure #22);
  • a commitment, which Bn254GT deserialization subgroup-checks.

With From<Fq12> removed, Bn254GT has no unchecked public constructor.

Test plan

  • cargo fmt --check
  • cargo clippy --all --features host and host,zk, with --all-targets -D warnings
  • cargo nextest run -p jolt-crypto -p jolt-dory: 125/125
  • e2e matrix in clear mode (prover-fixtures): 10/10
  • e2e matrix in ZK mode (prover-fixtures,zk): 10/10
  • Verifier fixture standard_muldiv
  • scripts/fuzz.py check --resolve, plus the jolt-dory fuzz build
  • scripts/check_style_invariants.py
  • dory-pcs CI-equivalent checks (see perf(arkworks): cyclotomic GT exponentiation and shared-squaring GT multi-exponentiation dory#36)

Follow-ups (not in this PR)

  • G2 scalar multiplications in the Dory verifier: 113 M, about 23% of what remains. Dory calls arkworks' ArkG2::scale, which bypasses JoltG2Routines' GLV path.
  • GT subgroup checks on deserialization: generic Fq12::pow with a 254-bit exponent, roughly 100 elements per proof. The figures above don't include them.

🤖 Generated with Claude Code

Point dory-pcs at a16z/dory#36, pinned by git rev until a dory-pcs release
includes it. ArkGT scaling there uses cyclotomic exponentiation, and the Dory
verifier's multi-term GT updates use the new PairingCurve::gt_msm, which
shares one cyclotomic squaring chain across all terms.

DoryScheme::combine uses gt_msm for the commitment combination, and
Bn254GT::scalar_mul/msm use cyclotomic_exp instead of Fq12::pow.

Cyclotomic arithmetic is valid only in the cyclotomic subgroup, so remove
Bn254GT's unchecked From<Fq12>, which had no callers. Every Bn254GT is now a
pairing output, a product of them, or subgroup-checked on deserialization.

Single-threaded verification of fib at 2^16 drops from 1.222B to 0.483B
instructions retired (Callgrind, -60%).

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

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📏 PR diff

Note

Actual code changed: 27 lines

🟢 +12 added    🔴 −15 removed

Tests, docs, fixtures, and helper scripts excluded.

Category 🟢 Added 🔴 Removed
⌨️ Actual code +12 −15
🧪 Tests 0 0
📚 Docs 0 0
📦 Fixtures 0 0
🔧 Helper scripts 0 0
Total diff +12 −15

Every changed line is classified once.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark comparison (crates)

Same-runner A/B: base_run = e2c962a (merge target) vs pr_run = 68b03b5 (PR head merged onto it). Bold = faster run; Δ colored when the difference exceeds the ±10% noise threshold.

2 of 92 benchmarks differ by more than ±10%:

Benchmark base_run (merge target) pr_run (this PR) Δ
gt_scalar_mul 878.5±4.72µs 512.6±6.05µs ${\color{green}\mathbf{-41.7\%}}$
append_bytes/Blake2b/32B 68.4±3.32ns 78.3±4.67ns ${\color{red}\mathbf{+14.6\%}}$
All 92 benchmarks
Benchmark base_run (merge target) pr_run (this PR) Δ
EqPolynomial::evals/17 1.9±0.02ms 1.9±0.07ms +0.4%
EqPolynomial::evals/19 7.1±0.05ms 7.1±0.06ms -0.1%
EqPolynomial::evals/20 19.3±0.30ms 19.3±0.45ms +0.2%
EqPolynomial::evals/22 77.7±1.47ms 78.1±1.59ms +0.5%
EqPolynomial::evaluations/14 503.2±16.21µs 500.3±12.17µs -0.6%
EqPolynomial::evaluations/18 6.3±0.04ms 6.7±0.63ms +6.0%
EqPolynomial::evaluations/20 41.8±0.41ms 40.8±0.28ms -2.5%
F128/accumulator/deferred 2.3±0.03µs 2.3±0.03µs -0.2%
F128/accumulator/naive 4.4±0.06µs 4.4±0.05µs -0.1%
F128/mul 3.8±0.03ns 3.8±0.17ns +0.5%
F128/mul_slice 3.5±0.02µs 3.5±0.03µs +0.1%
F128/square 3.4±0.03ns 3.4±0.02ns +0.1%
F192/accumulator/deferred 5.4±0.03µs 5.4±0.02µs +0.0%
F192/accumulator/naive 8.7±0.06µs 8.6±0.11µs +0.0%
F192/mul 8.4±0.07ns 8.4±0.06ns +0.0%
F192/mul_slice 7.7±0.05µs 7.7±0.08µs +0.1%
F192/square 5.6±0.07ns 5.6±0.06ns +0.0%
F64/accumulator/deferred 976.6±28.24ns 970.4±6.98ns -0.6%
F64/accumulator/naive 1.9±0.03µs 1.9±0.01µs -0.7%
F64/mul 1.9±0.01ns 1.9±0.02ns +0.1%
F64/mul_slice 1.9±0.04µs 1.9±0.02µs -0.4%
F64/square 1.9±0.01ns 1.9±0.02ns +0.0%
Polynomial::bind/14 129.0±1.89µs 129.7±5.45µs +0.6%
Polynomial::bind/18 2.0±0.02ms 2.0±0.05ms +0.3%
Polynomial::bind/20 7.9±0.08ms 7.9±0.09ms -0.6%
Polynomial::bind_low_to_high_in_place/Fr/14 218.1±6.16µs 215.4±5.27µs -1.2%
Polynomial::bind_low_to_high_in_place/Fr/20 8.2±0.11ms 8.2±0.15ms -0.5%
Polynomial::bind_low_to_high_in_place/Fr/24 124.7±3.93ms 124.2±1.46ms -0.4%
Polynomial::bind_low_to_high_in_place/Prime128OffsetA7F7/14 58.5±1.04µs 58.7±0.93µs +0.3%
Polynomial::bind_low_to_high_in_place/Prime128OffsetA7F7/20 2.0±0.02ms 2.0±0.02ms -0.2%
Polynomial::bind_low_to_high_in_place/Prime128OffsetA7F7/24 28.3±1.45ms 27.9±0.15ms -1.3%
Polynomial::evaluate/20 54.2±0.35ms 53.4±0.38ms -1.5%
UnivariatePoly::from_evals/16 52.1±1.01µs 52.5±1.03µs +0.7%
UnivariatePoly::from_evals/3 6.4±0.11µs 6.4±0.13µs +0.6%
UnivariatePoly::from_evals/4 9.5±0.15µs 9.6±0.19µs +0.6%
UnivariatePoly::from_evals/8 23.3±0.39µs 23.5±0.60µs +0.9%
append_bytes/Blake2b/256B 362.7±11.27ns 362.1±5.35ns -0.2%
append_bytes/Blake2b/32B 68.4±3.32ns 78.3±4.67ns ${\color{red}\mathbf{+14.6\%}}$
append_bytes/Keccak/256B 798.1±15.14ns 793.3±12.96ns -0.6%
append_bytes/Keccak/32B 54.1±4.35ns 53.8±3.91ns -0.5%
append_bytes/Poseidon/256B 219.7±1.60µs 221.0±1.85µs +0.6%
append_bytes/Poseidon/32B 69.7±0.96µs 69.7±0.43µs -0.1%
challenge/Blake2b 855.9±22.25ns 838.0±6.16ns -2.1%
challenge/Keccak 564.6±7.94ns 567.4±11.81ns +0.5%
field arithmetic/add/fp128_a7f7 5.1±0.05µs 5.1±0.07µs +0.0%
field arithmetic/add/fp32_c99 3.1±0.05µs 3.1±0.01µs -0.4%
field arithmetic/add/fp64_c59 3.1±0.02µs 3.1±0.03µs -0.1%
field arithmetic/mul/fp128_a7f7 15.3±0.06µs 15.4±0.15µs +0.1%
field arithmetic/mul/fp32_c99 5.6±0.07µs 5.6±0.06µs -0.5%
field arithmetic/mul/fp64_c59 7.7±0.04µs 7.7±0.07µs +0.3%
field arithmetic/square/fp128_a7f7 18.0±0.18µs 18.0±0.19µs -0.1%
field arithmetic/square/fp32_c99 5.8±0.08µs 5.8±0.07µs -0.3%
field arithmetic/square/fp64_c59 6.6±0.03µs 6.7±0.13µs +0.3%
field arithmetic/sub/fp128_a7f7 3.9±0.04µs 3.9±0.06µs +0.1%
field arithmetic/sub/fp32_c99 2.3±0.01µs 2.3±0.01µs -0.2%
field arithmetic/sub/fp64_c59 2.3±0.03µs 2.3±0.01µs -0.3%
field reduction/reduce_product/fp128_a7f7 25.4±1.78µs 25.4±1.60µs -0.1%
fp64_prime63_offset259/ext2_packed_mul 15.8±0.05ns 15.8±0.10ns +0.0%
fp64_prime63_offset259/ext2_scalar_mul 7.8±0.11ns 7.8±0.06ns -0.1%
fp64_prime63_offset259/packed_mul 7.3±0.08ns 7.3±0.08ns +0.4%
fp64_prime63_offset259/scalar_mul 2.7±0.02ns 2.7±0.04ns +0.3%
fp64_prime64_offset59/ext2_dependent_mul_1024 8.2±0.01µs 8.2±0.03µs +0.0%
fp64_prime64_offset59/ext2_neg_one_scalar_mul 6.9±0.08ns 6.9±0.07ns +0.0%
fp64_prime64_offset59/ext2_packed_mul 18.8±0.05ns 18.8±0.09ns +0.0%
fp64_prime64_offset59/ext2_scalar_mul 7.8±0.10ns 7.8±0.08ns +0.1%
fp64_prime64_offset59/ext2_scalar_square 6.5±0.08ns 6.5±0.08ns +0.0%
g1_add 421.0±10.25ns 411.4±3.48ns -2.3%
g1_deserialize_bincode 8.7±0.06µs 8.8±0.47µs +1.2%
g1_double 205.2±3.63ns 203.7±1.83ns -0.7%
g1_msm/1024 12.6±0.06ms 12.7±0.03ms +0.1%
g1_msm/16 534.6±5.18µs 538.1±4.63µs +0.7%
g1_msm/256 4.2±0.02ms 4.3±0.01ms +0.7%
g1_msm/4 251.7±2.07µs 253.0±1.88µs +0.5%
g1_msm/64 1.6±0.02ms 1.6±0.01ms +0.6%
g1_scalar_mul 64.5±1.41µs 64.1±2.04µs -0.6%
g1_serialize_bincode 123.7±1.97ns 126.1±2.17ns +1.9%
g2_msm/16 1.9±0.01ms 1.9±0.02ms +0.5%
g2_msm/256 15.3±0.06ms 15.4±0.07ms +0.7%
g2_msm/4 766.9±5.36µs 771.1±5.57µs +0.6%
g2_msm/64 5.5±0.02ms 5.6±0.02ms +0.4%
g2_scalar_mul 319.2±3.27µs 319.3±1.68µs +0.0%
gt_scalar_mul 878.5±4.72µs 512.6±6.05µs ${\color{green}\mathbf{-41.7\%}}$
multi_pairing/16 5.7±0.04ms 5.7±0.03ms +0.7%
multi_pairing/2 1.2±0.01ms 1.2±0.02ms +0.8%
multi_pairing/4 1.8±0.02ms 1.8±0.01ms +0.9%
multi_pairing/8 3.1±0.01ms 3.1±0.01ms +0.9%
pairing 903.1±10.83µs 903.6±7.62µs +0.0%
pedersen_commit/1024 12.7±0.04ms 12.7±0.04ms +0.7%
pedersen_commit/16 605.0±7.46µs 608.4±4.54µs +0.6%
pedersen_commit/256 4.3±0.01ms 4.3±0.01ms +0.6%
pedersen_commit/4 314.8±2.72µs 316.4±2.33µs +0.5%
pedersen_commit/64 1.6±0.01ms 1.7±0.02ms +0.7%

quangvdao added a commit that referenced this pull request Oct 29, 2026
Drops challenge_short_scalar, challenge_bits_for_loss, the Dory loss
table and its burst test, and the exact dory-pcs pin. Curve verifier time
is not a priority, and #2174's Dory verifier optimizations more than
offset the cost.
quangvdao added a commit that referenced this pull request Oct 29, 2026
…2110)

* feat(transcript)!: one sealed transcript over spongefish 0.8 states

Replaces the transcript stack (J1-J9) with its final form in one change.

- jolt-transcript: a sealed Transcript/FieldTranscript over spongefish
  ProverState/VerifierState. Absorbs are positional and word-aligned with
  no labels; sessions are 32-byte protocol tags and a SHAKE128 session id;
  bound digests are SHA3-256 (Keccak-256 behind `keccak256-digests`).
- Challenges: full-width BN254 scalars; Dory's group-scalar challenges are
  short, sized from their loss.
- Proof-of-work mechanism only (no protocol site uses it yet): one u64 nonce
  word, MAX_POW_BITS = 57, difficulty priced from the challenge-set size,
  and a sequential search through an owned `verifier_message_oracle`.
- Sumcheck, openings, Dory, HyperKZG, Akita, BlindFold, the verifier and
  prover, SDK, examples, jolt-eval and fuzz targets run on it.
- Akita grouped statements carry a role code for main's fused-increment
  group.
- FS inventories re-blessed; verifier fuzz bundles regenerated.

* chore(jolt-eval): drop rayon from jolt-transcript in the fuzz lockfile

* ci: fix the Rust workflow's duplicated run key

Merging main left the old jolt-sumcheck command under the new
"Test jolt-blindfold with its prover" step, so the step had two run keys
and GitHub rejected the whole workflow without running it. Also drop the
removed jolt-transcript parallel feature from the every-sponge test and
update the clippy comment that referred to it.

* docs(transcript): spec and book chapter for the sealed spongefish transcript

* style: import Ordering, Encoding and Transcript; allow hex "ba" in Rust

* docs(transcript): scope the Fiat-Shamir chapter by configuration and enforcement

* test: end the misaligned-access assert with a semicolon

* refactor(transcript)!: draw Dory's challenges full width

Drops challenge_short_scalar, challenge_bits_for_loss, the Dory loss
table and its burst test, and the exact dory-pcs pin. Curve verifier time
is not a priority, and #2174's Dory verifier optimizations more than
offset the cost.

* test: re-bless FS inventories and regenerate verifier fuzz bundles

* test(verifier): bump FIXTURE_MAGIC for the new proof format

Every transcript byte changes in this PR, but the cached verifier fixtures
kept JVCF0008, the value main writes, and cached fixtures load without an
acceptance check. JVCF0011 is unused by any branch (#2137 uses JVCF0010).

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

* test(field): check the Solinas word fold against big-integer reduction

The oracle was a test-only copy of the byte-at-a-time fold that the word
fold replaced. Compare with num-bigint `% p` instead, as
bn254_differential.rs does.

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

* style(verifier): drop a duplicated cfg on Stage8BatchEntry

Left behind when the LabelWithCount import was removed.

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

* refactor(sumcheck): delete the test-only CompressedRoundPoly wrapper

Production absorbs compressed rounds through append_compressed_round and
verifies them with CompressedSumcheckProof. The wrapper had no production
caller and carried its own compressed-round checks.

honest_prove_compressed and the compressed roundtrip now absorb through
append_compressed_round and verify a CompressedSumcheckProof against the
polynomial's evaluation. The tests that only exercised the wrapper are
removed, and the FS absorb inventory loses the wrapper's site.

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

* fix(sumcheck)!: bind the committed output-claim count

The committed sumcheck's output-claim commitments were absorbed with no
count, so a public caller that accepted any count could move a commitment
across the message boundary without changing a challenge. Absorb the count
as a u64 word ahead of the commitments, as main's LabelWithCount did.

* test: regenerate verifier fuzz bundles

---------

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
quangvdao added a commit that referenced this pull request Oct 29, 2026
…ites (#2108)

* feat(transcript)!: one sealed transcript over spongefish 0.8 states

Replaces the transcript stack (J1-J9) with its final form in one change.

- jolt-transcript: a sealed Transcript/FieldTranscript over spongefish
  ProverState/VerifierState. Absorbs are positional and word-aligned with
  no labels; sessions are 32-byte protocol tags and a SHAKE128 session id;
  bound digests are SHA3-256 (Keccak-256 behind `keccak256-digests`).
- Challenges: full-width BN254 scalars; Dory's group-scalar challenges are
  short, sized from their loss.
- Proof-of-work mechanism only (no protocol site uses it yet): one u64 nonce
  word, MAX_POW_BITS = 57, difficulty priced from the challenge-set size,
  and a sequential search through an owned `verifier_message_oracle`.
- Sumcheck, openings, Dory, HyperKZG, Akita, BlindFold, the verifier and
  prover, SDK, examples, jolt-eval and fuzz targets run on it.
- Akita grouped statements carry a role code for main's fused-increment
  group.
- FS inventories re-blessed; verifier fuzz bundles regenerated.

* chore(jolt-eval): drop rayon from jolt-transcript in the fuzz lockfile

* feat(transcript): proof of work before short challenges at protocol sites

Grinds where a challenge set is too small for SECURITY_BITS on its own,
which today is only the 128-bit Akita field; BN254 sites cost 0 bits and
carry no nonces.

- Sumcheck rounds: each clear round is priced at its degree bound and its
  nonce rides in the clear proof. Committed (ZK) sumchecks and BlindFold
  carry none and require a challenge set wide enough to need none.
- Stage-level bursts (stage 1 tau, stages 1-7, stage-2 tau_high, Akita
  stage-8 selectors) share one flat loss, SITE_LOSS = 1023: 10 bits over
  the Akita field, about 4.5x the worst-case burst. Nonces live in
  `StagePowNonces`; a site the verifier skips must hold none.
- Verifier fuzz bundles regenerated.

* ci: fix the Rust workflow's duplicated run key

Merging main left the old jolt-sumcheck command under the new
"Test jolt-blindfold with its prover" step, so the step had two run keys
and GitHub rejected the whole workflow without running it. Also drop the
removed jolt-transcript parallel feature from the every-sponge test and
update the clippy comment that referred to it.

* docs(transcript): spec and book chapter for the sealed spongefish transcript

* docs(transcript): describe the proof-of-work sites

* style: import Ordering, Encoding and Transcript; allow hex "ba" in Rust

* style: import PowSite in stage 8

* docs(transcript): scope the Fiat-Shamir chapter by configuration and enforcement

* test: end the misaligned-access assert with a semicolon

* refactor(transcript)!: draw Dory's challenges full width

Drops challenge_short_scalar, challenge_bits_for_loss, the Dory loss
table and its burst test, and the exact dory-pcs pin. Curve verifier time
is not a priority, and #2174's Dory verifier optimizations more than
offset the cost.

* test: re-bless FS inventories and regenerate verifier fuzz bundles

* test(verifier): bump FIXTURE_MAGIC for the new proof format

Every transcript byte changes in this PR, but the cached verifier fixtures
kept JVCF0008, the value main writes, and cached fixtures load without an
acceptance check. JVCF0011 is unused by any branch (#2137 uses JVCF0010).

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

* test(field): check the Solinas word fold against big-integer reduction

The oracle was a test-only copy of the byte-at-a-time fold that the word
fold replaced. Compare with num-bigint `% p` instead, as
bn254_differential.rs does.

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

* style(verifier): drop a duplicated cfg on Stage8BatchEntry

Left behind when the LabelWithCount import was removed.

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

* refactor(sumcheck): delete the test-only CompressedRoundPoly wrapper

Production absorbs compressed rounds through append_compressed_round and
verifies them with CompressedSumcheckProof. The wrapper had no production
caller and carried its own compressed-round checks.

honest_prove_compressed and the compressed roundtrip now absorb through
append_compressed_round and verify a CompressedSumcheckProof against the
polynomial's evaluation. The tests that only exercised the wrapper are
removed, and the FS absorb inventory loses the wrapper's site.

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

* test: re-bless FS inventories and regenerate verifier fuzz bundles

* fix(sumcheck)!: bind the committed output-claim count

The committed sumcheck's output-claim commitments were absorbed with no
count, so a public caller that accepted any count could move a commitment
across the message boundary without changing a challenge. Absorb the count
as a u64 word ahead of the commitments, as main's LabelWithCount did.

* test: regenerate verifier fuzz bundles

* test: regenerate verifier fuzz bundles

---------

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Main bumped dory-pcs to the 0.5.0 release (#2236), which includes
a16z/dory#36. Take main's version dependency and lockfiles in place of
this branch's git-rev pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moodlezoup
moodlezoup requested a review from markosg04 November 2, 2026 17:01
@moodlezoup
moodlezoup merged commit ea85997 into main Nov 3, 2026
56 checks passed
markosg04 added a commit that referenced this pull request Nov 5, 2026
…2111)

* feat(transcript)!: one sealed transcript over spongefish 0.8 states

Replaces the transcript stack (J1-J9) with its final form in one change.

- jolt-transcript: a sealed Transcript/FieldTranscript over spongefish
  ProverState/VerifierState. Absorbs are positional and word-aligned with
  no labels; sessions are 32-byte protocol tags and a SHAKE128 session id;
  bound digests are SHA3-256 (Keccak-256 behind `keccak256-digests`).
- Challenges: full-width BN254 scalars; Dory's group-scalar challenges are
  short, sized from their loss.
- Proof-of-work mechanism only (no protocol site uses it yet): one u64 nonce
  word, MAX_POW_BITS = 57, difficulty priced from the challenge-set size,
  and a sequential search through an owned `verifier_message_oracle`.
- Sumcheck, openings, Dory, HyperKZG, Akita, BlindFold, the verifier and
  prover, SDK, examples, jolt-eval and fuzz targets run on it.
- Akita grouped statements carry a role code for main's fused-increment
  group.
- FS inventories re-blessed; verifier fuzz bundles regenerated.

* chore(jolt-eval): drop rayon from jolt-transcript in the fuzz lockfile

* feat(akita): run Akita's opening inside Jolt's Fiat-Shamir sponge (J8)

Jolt's trace batch now runs Akita's argument on Jolt's own transcript
state instead of bridging to a standalone Akita session: Akita binds its
instance descriptor and writes its messages into Jolt's argument string,
and the verifier checks end-of-string once, after the opening.

- jolt-openings: GroupBatchOpening carries the grouped prove/verify
  entry points on the concrete spongefish states, with a PendingEof the
  verifier hands back for check_eof.
- jolt-akita: the trace batch calls batched_prove_embedded /
  batched_verify_embedded. append_verifier_setup, append_batch_statement
  and bridged_akita_session go away; Akita's descriptor binds the setup,
  schedule row and layout. A trace-batch proof that also carries a
  standalone backend proof is rejected. The generic BatchOpeningScheme
  and single-polynomial paths still run Akita standalone.
- Schedule catalogs regenerated for Akita's descriptor v6 (epoch 6).
- jolt-prover / jolt-verifier stage 8 pass the state through, and the
  FS inventories are re-blessed.

The Akita pin in Cargo.toml is a placeholder (Akita main) until
LayerZero-Labs/akita#254 lands; see the TODO there.

* chore(akita): pin Akita to #254's head 4fba41edb

The Akita stack merged main (#212, #213, #223), dropped an unused
dev-dependency, kept the transcript event log across fold previews and
regenerated its planner-cost snapshots. Akita's own schedule catalogs are
unchanged.

* ci: fix the Rust workflow's duplicated run key

Merging main left the old jolt-sumcheck command under the new
"Test jolt-blindfold with its prover" step, so the step had two run keys
and GitHub rejected the whole workflow without running it. Also drop the
removed jolt-transcript parallel feature from the every-sponge test and
update the clippy comment that referred to it.

* docs(transcript): spec and book chapter for the sealed spongefish transcript

* docs(transcript): Akita's opening runs in Jolt's sponge

* style: import Ordering, Encoding and Transcript; allow hex "ba" in Rust

* style: use the imported protocol_tag in Akita's batching tests

* docs(transcript): scope the Fiat-Shamir chapter by configuration and enforcement

* test: end the misaligned-access assert with a semicolon

* refactor(transcript)!: draw Dory's challenges full width

Drops challenge_short_scalar, challenge_bits_for_loss, the Dory loss
table and its burst test, and the exact dory-pcs pin. Curve verifier time
is not a priority, and #2174's Dory verifier optimizations more than
offset the cost.

* test: re-bless FS inventories and regenerate verifier fuzz bundles

* test(verifier): bump FIXTURE_MAGIC for the new proof format

Every transcript byte changes in this PR, but the cached verifier fixtures
kept JVCF0008, the value main writes, and cached fixtures load without an
acceptance check. JVCF0011 is unused by any branch (#2137 uses JVCF0010).

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

* test(field): check the Solinas word fold against big-integer reduction

The oracle was a test-only copy of the byte-at-a-time fold that the word
fold replaced. Compare with num-bigint `% p` instead, as
bn254_differential.rs does.

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

* style(verifier): drop a duplicated cfg on Stage8BatchEntry

Left behind when the LabelWithCount import was removed.

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

* refactor(sumcheck): delete the test-only CompressedRoundPoly wrapper

Production absorbs compressed rounds through append_compressed_round and
verifies them with CompressedSumcheckProof. The wrapper had no production
caller and carried its own compressed-round checks.

honest_prove_compressed and the compressed roundtrip now absorb through
append_compressed_round and verify a CompressedSumcheckProof against the
polynomial's evaluation. The tests that only exercised the wrapper are
removed, and the FS absorb inventory loses the wrapper's site.

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

* test: re-bless FS inventories and regenerate verifier fuzz bundles

* fix(sumcheck)!: bind the committed output-claim count

The committed sumcheck's output-claim commitments were absorbed with no
count, so a public caller that accepted any count could move a commitment
across the message boundary without changing a challenge. Absorb the count
as a u64 word ahead of the commitments, as main's LabelWithCount did.

* test: regenerate verifier fuzz bundles

* test: regenerate verifier fuzz bundles

* test: regenerate verifier fuzz bundles

* chore(deps): pin Akita to the stack tip

* test(fuzz): regenerate zk verifier bundles on merged main

* chore(deps): pin Akita to the corrected stack tip

* chore(deps): pin Akita to the merged stack tip 9f8ad8a91

* fix(jolt-akita): pin the K=256 one-hot chunk size and regenerate schedules

Akita #258 switched DEFAULT_UNIT_ONEHOT_SOURCE_CHUNK_SIZE to 16.
JoltOneHotK256Direct inherited it through OneHot's committed_source_class,
so K=256 rows were priced for one hot position per 16 coefficients and the
backend rejected Jolt's K=256 sources. Declare the class explicitly, as the
K=16 policy already does, and regenerate every catalog at the new pin.

* test(jolt-verifier): regenerate fuzz bundles and the FS inventory

The clear bundles came from main under its transcript; regenerate them on
the merged tree. The inventory records the SetupPolicy argument now passed
to batched_verify_embedded.

* test(jolt-transcript): gate the whole-word absorb helper with its caller

append_values_matches_per_value_absorbs and the Encoding import were
compiled, unused, under feature sets without solinas and bn254.

* style(recursion): taplo-format the guest hash-inlines feature

* fix(akita): address review of the embedded Akita opening

- book: name the pinned Akita revision (8d5821f45), not 9f8ad8a91, which
  is not an ancestor of the pin; same for the policy-spec link.
- book: Akita's sparse ring challenges are drawn on Jolt's sponge, so
  "every challenge on Jolt's transcript is full width" is now scoped to
  Jolt's own draws.
- jolt-transcript: the `prover-grinding` comment said verifier builds
  leave `yolocrypto` off; every Akita build has it through akita-prover.
- profile: the trace batch's `backend_proof` is always empty, so the
  Akita size telemetry reported 0. Report the argument-string size and
  drop the callerless `AkitaBatchProof::unframed_payload_size`. Document
  that `backend_proof` is empty for the trace batch.
- recursion: `tamper-opening` flipped a byte of the empty `backend_proof`
  and panicked; it now flips the argument string's last byte, which is
  Akita's. Drop the guest's dead BLAKE2b jolt-transcript edge and the
  host's unused Keccak inline registration.
- jolt-verifier: `Stage8Output` is gated off the Akita build, where it had
  only a never-constructed variant. Document that the Akita stage-8
  `verify` and `GroupBatchOpening::verify_batch` return before acceptance.
- FS scanner: only `batched_verify_embedded` leaves the scanned crates.
  `verify_batch` calls between scanned crates and `check_eof` were
  recorded as absorb and challenge sites; re-bless drops those 10 entries.
- tests: `akita_schedule_selection_tampers_reject` covers the selection on
  the embedded path (flipped digests and other fixtures' valid rows);
  `proof.narg` joins the tamper manifest; the streamed-batch comment no
  longer claims the setup-mismatch cases reach Akita's descriptor.

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

---------

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: sumchecker <241190306+sumchecker@users.noreply.github.com>
Co-authored-by: Markos Georghiades <mgeorghiades@a16z.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