Repository navigation
refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade - #2110
Draft
quangvdao wants to merge 17 commits into
Draft
refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade#2110quangvdao wants to merge 17 commits into
quangvdao wants to merge 17 commits into
Conversation
Contributor
📏 PR diffNote Actual code changed: 4,026 lines🟢 +1,987 added 🔴 −2,039 removed Tests, docs, fixtures, and helper scripts excluded.
Every changed line is classified once. |
This was referenced Oct 21, 2026
Draft
quangvdao
added this pull request to stack #2112
October 21, 2026 15:50
This was referenced Oct 21, 2026
4 tasks done
quangvdao
force-pushed
the
transcript/j7-sponge-port
branch
2 times, most recently
from
October 21, 2026 18:15
af6d719 to
d04df1b
Compare
quangvdao
added a commit
to LayerZero-Labs/akita
that referenced
this pull request
Oct 21, 2026
J7 (a16z/jolt#2110) was rebased, so 6691ec7cb is no longer on any ref. At d04df1bb5, jolt-transcript's default sponge is Keccak alone; no Akita crate relies on it, because every manifest sets default-features = false.
…elete the legacy facade
…ndFold over the spongefish transcript
…cript Bind the full statement as the spongefish instance, absorb commitments as labeled messages, carry the NARG string in JoltProof and check its end, and move per-round grinding onto the shared SpongePow core.
The fs-audit transcript is the production verifier state with a recording script that also replays proof-of-work check bytes, so frozen replays over changed inputs model a verifier whose every squeeze is fixed.
… the spongefish transcript Keccak is the protocol sponge, Blake2b512 behind transcript-blake2b; the Poseidon protocol selection is removed until the transcript absorbs native Fr.
jolt-prover names Fr challenges, so its transcript dependency enables bn254 itself instead of relying on feature unification. Drop an unused import in the field-inline Akita fixtures and the SDK, and avoid unwrap in the zk statistical test.
- An unlabelled UnivariatePoly round absorbs as a labelled sum-check round with its coefficient count, so a longer polynomial cannot absorb like a shorter one. - Document that every absorbed Encoding must decode uniquely, since the sponge absorbs bytes without framing. - Correct the prover-grinding comment: crates holding both sides enable yolocrypto in verifier builds; it changes visibility, not bytes. - List precommitted as unbound in the statement test, and check that every unbound edit changes the inputs. - Fix two test ports: count challenge draws rather than PoW squeezes, and squeeze the prover's tail once in the grouped-capacity loop. - Re-bless the absorb-site inventory.
Import types and traits by short name on lines this branch adds, as check_style_invariants requires, apply rustfmt, and commit the lockfiles, verifier fixtures and fuzz seeds regenerated for the new transcript bytes.
Rename the order test's label to a dictionary word of the same length, and drop jolt-sumcheck's num-traits dev-dependency, whose only user was the deleted mersenne61 compatibility test.
The proof-of-work nonce search copies the sponge state, which spongefish 0.8.0 exposes only under `yolocrypto`. jolt-sumcheck and jolt-blindfold turned `prover-grinding` on in every build, so jolt-verifier compiled the search and `yolocrypto` too. - jolt-sumcheck and jolt-blindfold gain a `prover` feature holding their prove side: the round loop, `grind_round`, the recorders' rounds, the committed builder and BlindFold's `prove`. jolt-prover, jolt-kernels and jolt-spartan-prover enable it; jolt-verifier's graph has neither the search nor `yolocrypto`. - jolt-transcript's default features are Keccak only. Blake2b and Poseidon are opt-in; the fuzz workspaces that use them, or BN254 challenges, now name those features. - CI lints the verifier-only graph, checks jolt-verifier's feature tree, and tests jolt-sumcheck and jolt-blindfold with their prover.
…ge-6b draw site Stage 6b now draws through J6's one `sequence`, so its three further `challenge` sites fold into one. Also rustfmt the stage-6b draw test.
Port J6's short Dory challenges onto the spongefish states. `FieldTranscript::challenge_short_scalar` squeezes ceil(b / 8) bytes, clears the bits above b = challenge_bits_for_loss(loss, SECURITY_BITS) and decodes the little-endian integer, as J6's legacy method did. Both states share one implementation. A width the field cannot hold returns `VerificationError` before any squeeze and poisons the verifier, as an unsupported challenge length does, so `GrindBitsError::FieldTooSmall` goes. `ScriptedTranscript` passes the draw to its script and logs it as `TranscriptEvent::ShortChallenge`, so FS audit tapes record and replay it. The Dory adapter draws beta, alpha and d short with J6's loss table; a refused draw fails `finish` like any other. challenge_bursts.rs records real transparent and zero-knowledge Dory proofs through `ScriptedTranscript`.
quangvdao
force-pushed
the
transcript/j7-sponge-port
branch
from
October 21, 2026 20:58
d04df1b to
d4e949c
Compare
…pt/j7-sponge-port # Conflicts: # crates/jolt-transcript/src/challenge.rs
quangvdao
added a commit
that referenced
this pull request
Oct 23, 2026
J7 was rebuilt on main, which now carries #2107 and J6, so J7's old head d04df1b is no longer its ancestor. This merge keeps J7's tree and re-applies J8's own change on top: Akita's opening inside Jolt's sponge, main's commitment wire behind #2107's shape guard, the standalone session binding, and the FS replay freezing. The #2107 cherry-picks on this branch are superseded by main's squash of #2107. Dory keeps J7's label-based short challenges. The only lockfile conflict was resolved by taking J7's lock and re-resolving the Akita pin.
…pt/j7-sponge-port # Conflicts: # crates/jolt-transcript/tests/pow_tests.rs
quangvdao
added a commit
that referenced
this pull request
Oct 23, 2026
quangvdao
added a commit
to LayerZero-Labs/akita
that referenced
this pull request
Oct 24, 2026
J7 contains A2's J6 pin and adds the FieldTranscript surface this branch uses.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Jolt's prover and verifier now run on spongefish 0.8.0's own
ProverStateandVerifierState, and the legacyjolt_transcript::Transcriptfacade is deleted. Stacked on #2108 (J6).TranscriptandFieldTranscript<F>are implemented once, on both spongefish states, from three sponge operations: absorb a public message, squeeze, and require. The prover and verifier run the same method bodies.ProverTranscriptadds the proof-of-work search.VerifierTranscriptadds NARG reads,check_grindandcheck_eof.Encoding. Field elements are their canonical little-endian bytes, behind a new optionalspongefishfeature on jolt-field. Commitments, claims and labels are typed messages.ChallengeCodec, one codec message per draw site.JOLT_SESSIONand aJoltStatementthat holds the protocol configuration and every public input it checks. Commitments then follow as labelled messages.JoltProofgains an emptynargfield, and the verifier callscheck_eofon it. J8 builds on this.SpongePowcore. J6's nonce plumbing, tests and site table are kept, as is its search: the calling thread scans the first 1024 nonces, the pool scans the rest only if none passes, and either way the result is the least passing nonce. Stage 6b prices its burst and squeezes it through J6's singleStage6bDraws::sequence; here the sequence is fallible, because a draw on the sealed transcript returns aResult.challenge_short_scalaris aFieldTranscriptmethod, implemented once for both spongefish states. Its width is priced from the loss as in J6. It squeezesceil(b/8)bytes, masks the top bits and decodes withfrom_bytes_le_reduced. On a field without room for the width, the verifier's state is poisoned; the prover gets an error. The Dory adapter keeps J6's loss table:beta2 (129 bits),alpha4 (130 bits),d3 (130 bits).gammaand the zero-knowledge Sigma challenges stay full width.ScriptedTranscriptrecords each short draw as its own event.challenge_burstsreads that event log.transcript-blake2b. jolt-transcript's default features are Keccak only; Blake2b and Poseidon are opt-in. The Poseidon protocol build is off (see below).yolocryptofeature. jolt-sumcheck and jolt-blindfold gain aproverfeature that holds their prove side. Only jolt-prover, jolt-kernels and jolt-spartan-prover turn it on, so jolt-verifier's dependency graph has neither the nonce search noryolocrypto. A CI step checks that graph.Who used the facade, and what each now does
Transcriptfacade,DigestTranscript,SpongeTranscript,UnpricedTranscript,domain::{Label, …}Transcript/FieldTranscriptonProverState/VerifierState.ProverInit/VerifierInitconstructors.codec::{Label, LabelWithCount, U64Word, BytesMsg}. Test-onlyScriptedTranscriptwith aScripthookT::new(b"Jolt"), thenabsorb_preambleand a second hand-written copy for the provervalidate_and_seed_transcript(…, init)builds the transcript from the statement. The stages draw throughFieldTranscriptFieldTranscriptT::new(b"Jolt"). The ZK replay comparedstate()ProverInit. The ZK replay checks against the forward transcript with probesRoundMessage::append_to_transcriptover the facade,NoncedLabelWithCountword, then its coefficients asEncoding. The nonces are checked withcheck_public_grindAppendToTranscriptimpls and bounds, oneimpl TranscriptEncodingimpls,FieldTranscriptboundsFieldTranscript. Dory still absorbs every proof value itselfAppendToTranscripton the commitmentsEncoding.bridged_akita_sessionis changed only enough to compileAppendToTranscriptsupertrait,EvaluationClaimimplEncodingProtocolTranscriptalias was used for both sidesProtocolSponge,ProofTranscript = ProverState<_>,VerifierTranscript<'a> = VerifierState<'a, _>MleAstimplementsEncoding, becauseJoltFieldnow requires it under feature unificationstate()What was deleted
jolt-transcript:
legacy.rs: the facade,SpongeTranscript, and the facade's challenge decoders (from_challenge_bytes,challenge_width);digest.rs:DigestTranscript;unpriced.rs;digest_transcript_tests.rs,legacy_digest_tests.rs);*Transcripttype aliases.One decoder per concept is left: J2's
ChallengeCodec.AppendToTranscript, everywhere.Encodingreplaces it.The facade's challenge-set declarations.
UnpricedTranscript's 2^256 declaration now comes from the test-onlyScript::UNPRICED.The duplicated preamble absorb in the verifier.
Two jolt-sumcheck tests that built a field outside jolt-field (
mersenne61_compat.rs,external-consumer/tests/external_field.rs).ChallengeCodecis sealed, so a field from another crate cannot draw challenges. That is deliberate: each codec's bias bound is argued next to its code.transcript-poseidonandtranscript-keccakon jolt-sdk, jolt-verifier and the examples.transcript-blake2bremains.Byte-format change
Every proof and fixture changes. Proofs from before this PR do not verify.
transcript-blake2b).b"Jolt"label is replaced byJOLT_SESSIONplus the encodedJoltStatement.BytesMsg. Labels are 32-byte words.JoltProof.narg(empty for now).The fixtures and FS inventories are regenerated.
Fields the verifier reads but deliberately does not absorb
The test
statement_binds_each_field_the_verifier_readshas two lists:Each unbound field, and why it is safe:
MemoryLayoutfields (input_start,output_start,output_end,stack_size,termination,program_sizeand so on).validate_inputsrejects any layout that differs from the preprocessing's, and the preprocessing digest is in the statement.trusted_advice_commitment_present. The commitment itself is absorbed under its own label right after the statement, so its presence is bound.vc_capacity. It belongs to the verifier's own setup, not to the proof.precommitted. It is derived during validation from bound fields (trace order, trace length, chunk bits, advice presence) and from the committed program, which the digest binds.Soundness audit
An independent read-only soundness review (xhigh effort) compared every transcript call on this branch with J6. Verdict: sound with notes. No critical, high or medium finding. It checked, site by site:
tau, and the missing booleanity challenges). That changes bytes, not order.JoltStatement, each with its length. The statement binds every field the verifier reads, or the field is in the unbound list above with its reason.Fixed on this branch after the review:
UnivariatePolyround message absorbed its coefficients with no count, so a longer polynomial could absorb like a shorter one. Only fuzz targets used it, but it is public. It now absorbs as a labelled sum-check round with a count.Encodingmust decode uniquely is now written on theTranscriptmodule. No type check enforces it.prover-grindingcomment said verifier builds leaveyolocryptooff, but jolt-sumcheck and jolt-blindfold turned it on in every build. They now hold their prove side behind aproverfeature, so verifier builds do leave it off.yolocryptoonly makes the sponge state visible, and it changes no bytes.precommittedis now in the binding test's unbound list. Every unbound edit is also checked to actually change the inputs.Left as notes:
ScriptedTranscriptimplementsVerifierInit, but onlytest-utilsenables it, and only dev-dependencies turn that on.pcs_setupandvc_setupare trusted setup, not Fiat-Shamir input.finish().Label's lengthassert!only sees constant labels.Poseidon
The Poseidon protocol build is off:
transcript-poseidonno longer exists on jolt-sdk, jolt-verifier or the examples. The sponge code stays in jolt-transcript, with its tests and the jolt-eval sponge invariants.The reason: J2's codecs decode challenges from raw squeezed bytes, and they need a sponge whose bytes are uniform (
UniformBytes).PoseidonSpongesqueezes the little-endian bytes of BN254 scalars, whose top bits are far from uniform. So it cannot draw Jolt's challenges until the transcript absorbs and squeezes nativeFr.Draw bursts (listed, not merged)
A burst is several draw sites in a row with no absorb between them. Each site is its own codec message, as the design asked. The table shows what merging each burst into one message would save, measured on the muldiv fixtures with the verifier's real event order. The sponge is Keccak, which permutes once per 136-byte block it squeezes.
Each row is one burst in one honest verify of the muldiv fixture. The "Burst" column gives the stage and the ordinal of the burst's first draw within that stage. Every draw in these bursts is a single challenge. A negative saving means merging would cost bytes.
For BN254
Fr, merging saves nothing: the codec uses 52 bytes per element for 1 to 8 elements and 53 from 9 on. For the Akita field (p = 2^128 − 275), a message of n elements costs (n + 1) × 16 bytes instead of 32n, so merging saves 16 bytes per extra element.Label words in the verifier's hash work
Every
LabelandLabelWithCountis one 32-byte word. Measured on the same verifies, with the Keccak permutation as the unit of hash work:The permutation counts include challenge squeezes. Labels are kept: they are what makes the absorbed stream decode uniquely across messages with no count of their own. Shrinking them (for example to a short tag) is a possible follow-up, and it would need its own unique-decoding argument.
API changes for J8 (
transcript/j8-…)Hparameter.field_challenges,public_messageandverifier_messagemoved into the sharedTranscript/FieldTranscripttraits.VerifierInit<'a>andProverInit.JoltStatementandJOLT_SESSION, plusJoltProof.narg.validate_and_seed_transcripttakes a closure that builds the transcript from the statement.verifyis nowverify<'a, F, PCS, VC, T: FieldTranscript<F> + VerifierInit<'a>>.Script::check_bytes.proverfeature:prove_batch,prove_uniskip_*,grind_round,CommittedSumcheckBuilder,CommittedSumcheckRecorder, and the recorders'absorb_roundandfinish.begin_batchandabsorb_input_claimsstay available to the verifier.prove,prove_with_row_committerand their witness and row-committer types need its newproverfeature.transcript-keccakonly. Crates that useBlake2b512orPoseidonSpongenametranscript-blake2bortranscript-poseidon.Validation
All of this ran on a Boat sandbox, not locally:
Gates.
cargo fmt --all --check,taplo fmt --check,cargo machete --with-metadata,scripts/check_style_invariants.py --base upstream/transcript/j6-protocol-grinding, andscripts/check-external-sumcheck.sh.typosreports only an existing false positive in jolt-metal.Clippy
-D warnings. Run on:allocative,host,zk, withallocative,host,zk,transcript-blake2b, and with--no-default-features;bn254,solinas,binary,prover-grinding;spongefish,spongefish,bn254andspongefish,bn254,solinas,binary;host,guest-verifier, and each withtranscript-blake2b;prover-fixtures,zk,akita,field-inlineandfs-audit;akita,prover-fixtures,zk,profiling,allocativeandfield-inline;committed,r1cs,committed,r1cs,prover,committed,prover,committed,r1cs,proverandparallel,prover, and--no-default-features;prover, and jolt-sumcheck with jolt-blindfold together without it;test-utils, withtranscript-blake2b, and with--no-default-features;Verifier feature graph.
cargo tree -p jolt-verifier -e normal,featureslists noprover,prover-grindingoryolocryptofeature. jolt-prover's graph still has all three.Nextest. Every run passes:
test-utils, all features, sequential grinding, and its doctest), jolt-field, jolt-sumcheck (default, andcommitted,prover), jolt-blindfold (default, andprover), the external sumcheck consumer, the PCS and opening crates, jolt-kernels (with and withoutzk), jolt-akita, jolt-sdk, jolt-eval and zklean-extractor;prover-fixtures,prover-fixtures,zkandakita,prover-fixtures;fs_obligations(with and withoutakita) andfs_attacks(clear, zk, akita);prover-fixtures, and withzk) and the Akita profile tests.After the rebase onto J6's 2d5e401. These all pass:
fs_obligations(with and withoutakita) andfs_attacks(clear, zk, akita);test-utils) and jolt-dory, includingchallenge_burstsand the short-draw byte tests;prover-fixtures;The challenge census was re-blessed: stage 6b has one draw site, and the Dory adapter has one full and one short draw site.
After merging J6's 223511f (which brings Jolt main 14315e9). J6's new F8 uniskip test (
crates/jolt-sumcheck/tests/f8_domain.rs) now runs on the spongefish transcript, with every assertion unchanged. It needsproverforgrind_round, so its manifest entry and the two new binary-field CI steps enablejolt-sumcheck/prover. Main's minimum trace length (validate_trace_length) is kept, and its doc comments now name J7's API. These all pass:committed,prover, and binary) clippy and tests;--lib,prover-fixtures,prover-fixtures,zk) and its lib,prover-fixturesandprover-fixtures,zktests;fs_obligationswith and withoutakita,fs_attacks(clear), and the verifier-graph check.The merge drops no test from either parent. Serialized proof sizes match J7's previous head d4e949c byte for byte on all nine verifier fixtures, clear and zk.
Regenerated. The verifier fuzz fixtures (clear and zk), the jolt-dory and jolt-crypto fuzz seeds, the fuzz lockfiles (jolt-claims' was stale and now has spongefish), and the absorb-site inventory. Every fuzz workspace passes
cargo check --bins.Left to CI (the heaviest end-to-end jobs):
🤖 Generated with Claude Code