Repository navigation
Conversation
Contributor
📏 PR diffNote Actual code changed: 707 lines🟢 +450 added 🔴 −257 removed Tests, docs, fixtures, and helper scripts excluded.
Every changed line is classified once. |
This was referenced Oct 21, 2026
quangvdao
force-pushed
the
transcript/j7-sponge-port
branch
from
October 21, 2026 17:36
23e3189 to
af6d719
Compare
…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.
quangvdao
force-pushed
the
transcript/j7-sponge-port
branch
from
October 21, 2026 18:15
af6d719 to
d04df1b
Compare
…verState/VerifierState Replace the byte-based Poseidon sponge (capacity about 2^127) with the width-5 circom BN254 Poseidon over FrUnit: rate 3, capacity 2. - SpongeUnit maps each message's byte encoding to sponge units: the identity for u8 (Keccak and Blake2b bytes are unchanged), and for FrUnit a length element followed by 31-byte little-endian chunks. - ChallengeSponge<F> is the single challenge path; Poseidon draws Fr challenges as squeezed elements and implements it only for Fr. - Proof of work on Poseidon tests the low bits of one squeezed element; the MessageSearch probe absorbs exactly the packed nonce bytes. - Verifier rejections poison the state through a message that never parses, since spongefish's check exists only for byte sponges. - Re-enable transcript-poseidon in jolt-sdk and the examples; Keccak stays the default, and Blake2b with Poseidon is a compile error. - Re-bless the Fiat-Shamir absorb-site inventory.
quangvdao
force-pushed
the
transcript/j4b-poseidon-native-fr
branch
from
October 21, 2026 19:52
49f2f3f to
3cfbd10
Compare
quangvdao
force-pushed
the
transcript/j7-sponge-port
branch
from
October 21, 2026 20:58
d04df1b to
d4e949c
Compare
…n-native-fr Brings in J6's grind search, main via J2 (7931b22), and J7's short scalar challenges. challenge_short_scalar now reads its integer through SpongeUnit::short_word: byte sponges keep ceil(b/8) squeezed bytes unchanged, and PoseidonSponge keeps the low b bits of x0 + p*x1 from two squeezed elements.
…short challenges against num-bigint, refresh docs The carry out of x0 + p*x1 is discarded on purpose (the word is taken mod 2^256); binding it keeps -D warnings green. The Poseidon short challenge test now checks b = 128, 130 and 192 against unbounded integers, and a new test checks that a zero loss factor is refused and poisons the verifier. Docs in challenge.rs, poseidon.rs, lib.rs, transcript.rs and the fuzz targets now match the ChallengeSponge and short-challenge paths.
# Conflicts: # crates/jolt-transcript/tests/pow_tests.rs
…o transcript/j4b-poseidon-native-fr
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.
Stacked on #2110 (
transcript/j7-sponge-port). Supersedes #2103.Why a new PR instead of retargeting #2103. #2103 was built on J3 and edits the legacy facade, including
legacy.rs, which #2110 deletes. The two conflict in 13 files. Moving #2103 onto #2110 would mean rewriting its history and force-pushing it. This branch ports the audited Poseidon design onto #2110's API. It now merges #2110's head 6cea4bc, which contains J6's adaptive grind search,main(through J2, 7931b22) and #2110's short scalar challenges. The update is an ordinary merge commit, with no rebase or force-push. #2103 is closed.What changed
#2110 still ships the older byte-based Poseidon sponge. Its capacity gives only about 2^127 hash calls to break, below the 2^128 target. This PR replaces it with the width-5 sponge over BN254
Frthat the J4 audit approved, so the crate has exactly one Poseidon sponge.PoseidonSpongeis spongefish's overwrite-modeDuplexSponge, withFrUnit(oneFr) as its unit. The permutation is circomlib's width-5 Poseidon (x^5 S-box, 8 full and 60 partial rounds), built from the constants light-poseidon ships. Rate is 3 elements (slots 0–2) and capacity is 2 elements (slots 3–4), about 2^253 generic security.Encoding. A new sealed trait,SpongeUnit, maps a message's bytes into the sponge's units: foru8it is the identity, and forFrUnitit packs the bytes as described below. A small internal adapter,Units<M>, feeds the result to spongefish'sProverState/VerifierState. TheTranscriptsealed core gainstype Unit, andProverTranscript::Spongeis tied to it (DuplexSpongeInterface<U = Self::Unit>).FieldTranscript<F>now draws through one sealed trait,ChallengeSponge<F>. Keccak and Blake2b get it from their existing byte-decoding path (UniformBytes+ChallengeCodec), unchanged. Poseidon implements it only forFr: a challenge is a squeezed element, used as is, with no byte decoding and no reduction bias. Asking Poseidon for challenges in any other field is a compile error. The challenge-size cap counts 32 bytes per element and is checked before anything is squeezed.challenge_short_scalardraws an integer ofbbits (bfrom the step's loss factor, at most 192 bits for au64loss). It now reads that integer throughSpongeUnit::short_word, so it runs on any sponge unit:ceil(b / 8)bytes and clears the bits aboveb, exactly as refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110 does.x0andx1and keeps the lowbbits ofx0 + p * x1. That integer is uniform on[0, p^2), so its lowbbits are within2^b / p^2 <= 2^-253of uniform. One element alone would be about2^-64from uniform atb = 192, which misses the2^-160target.jolt-dory's label-based short Dory widths come from refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110 unchanged, and run on Poseidon too.Frand requires the lowbitsbits of its canonical integer to be zero; forbits <= 25those bits are within 2^-228 of uniform. The nonce is still sent one byte per prover message. On Poseidon each byte is packed as a message of its own (2 elements), and theMessageSearchprobe absorbs exactly the same units, so the search tests what the verifier checks. Grinding still runs once per round.Transcript::checkexists only for byte sponges. The verifier therefore poisons itself: on failure it reads a message type that never parses and absorbs nothing. Every rejection still makescheck_eoffail: truncated input, failed proof of work, bad difficulty, oversized challenge request.jolt-sdkand thefibonacciandmerkle-tree-saveexamples regain thetranscript-poseidonfeature, which setsProtocolSponge = PoseidonSponge. Keccak stays the default. Enablingtranscript-blake2bandtranscript-poseidontogether is a compile error.ScriptedTranscriptstays byte-only (it wraps transcripts whose unit isu8), because its script checks bytes.jolt-prover/src/blindfold.rsnow compares 32 squeezed units of the transcript's own unit type, so it works on any sponge.How bytes are packed on Poseidon
Every message, the session identifier and the transcript instance reach the Poseidon sponge as byte strings, and each one is packed on its own:
n, followed byceil(n / 31)elements. Each of those is the little-endian integer of the next 31 bytes, and the last chunk may be shorter. Every chunk is below 2^248, which is below the field modulus, so every chunk is a canonical element.Soundness audit
Merge with 6cea4bc. A second independent audit at xhigh effort covered the merge. Verdict: sound. It found the following:
(x0 + p * x1) mod 2^256correctly. The auditor checked this against exact integer arithmetic on 200,100 input pairs for everybfrom 1 to 253. Its exact distance from uniform is2^-381.7atb = 128,2^-326.7atb = 192, and at most2^-256.8for anyb <= 253.-D warnings, and stale docs inchallenge.rs,poseidon.rs,lib.rsand the two fuzz targets.Original port. An independent auditor reviewed the port at xhigh effort. They covered capacity and rate, the permutation, packing injectivity and domain separation, byte-sponge equivalence with #2110, challenge uniformity and the cap, grinding, verifier poisoning and verifier panics. They found no exploitable issue.
Fr's challenge set is so large thatgrind_bits_for_lossreturns 0. The path is correct and tested.constants()hasassert/expectcalls on fixed library data. Input cannot reach them, and refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110 already hadexpecton this path.The capacity and permutation checks from the J4 audit were re-run on this branch:
poseidon.rsunit tests pin all five input and output slots of the permutation against circomlibjs 0.1.7poseidon(inputs, init, 5), and output slot 0 against light-poseidon.poseidon_testsreplays a sample transcript on a bare sponge from the documented packing and checks that the challenges match, and pins a known-answer vector for two native challenges.Validation
All runs were on a remote sandbox at this branch's head after the merge with 6cea4bc.
cargo fmt --all --check;python3 scripts/check_style_invariants.py --base 6cea4bc85;typos.cargo clippy -p jolt-transcript --all-targets -- -D warningswith--all-features, default features,--no-default-features --features transcript-poseidon,prover-grinding,--features bn254,solinas,binary,prover-grinding, and--features test-utils.cargo nextest run -p jolt-transcriptwith--all-features, default features (Keccak only), and--no-default-features --features transcript-poseidon,prover-grinding. All pass.cargo nextest run -p jolt-transcript --features bn254,transcript-blake2b --test keccak_tests --test blake2b_tests. They pass on the files refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110 ships.jolt-dory:cargo clippy --all-targets -- -D warningsandcargo nextest run.jolt-sumcheck:cargo clippy -p jolt-sumcheck -p jolt-blindfold --all-targets -- -D warnings,cargo clippy -p jolt-sumcheck --all-targets --features committed,prover -- -D warnings, andcargo nextest run -p jolt-sumcheck --features committed,prover.jolt-prover:cargo clippy --all-targets -- -D warningswith default features and with--features prover-fixtures,zk;cargo nextest run -p jolt-prover(default features).cargo nextest run -p jolt-verifier --test fs_obligations --features fs-audit, with and withoutjolt-transcript/transcript-poseidon. The inventory needed no change from the merge.cargo clippy -p jolt-sdk --all-targets --features host,transcript-poseidon -- -D warnings;cargo clippy -p jolt-eval --all-targets -- -D warnings;cargo checkof thejolt-transcriptfuzz crate.cargo clippy --all --all-targets -- -D warningswith--features allocative,host,--features allocative,host,zkand--no-default-features, pluscargo check --workspace.cargo check -p fibonacci --features transcript-poseidon, and a one-off run of thejolt-sumcheckclear-sumcheck example switched toPoseidonSpongeoverFr, which proved and verified (not committed).cargo doc -p jolt-transcript --all-features --no-depswith-D warningsreports two broken intra-doc links that refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110 already has ([fail]inchallenge.rsand[1600]inlib.rs); this PR adds none. CI does not run rustdoc.jolt-proverfixture tests (--features prover-fixtures[,zk]). They need the guest toolchain, so CI runs them.transcript-poseidon, which also needs the guest toolchain. CI's Keccak end-to-end jobs cover the shared code paths.Test changes against #2110
Keccak and Blake2b known-answer tests are byte-identical.
keccak_tests.rsandblake2b_tests.rsare not touched by this PR and pass unchanged, so those transcripts produce the same bytes as on #2110.challenge_tests.rs,narg_eof_tests.rsandnative_traits_tests.rsare also untouched.Against #2110's head d04df1b, no assertion on a byte sponge was removed or loosened. Every removed or rewritten assertion is listed below. #2110's
search_is_independent_of_the_thread_counttest inpow.rsis kept as #2110 has it. The search it checks is now generic over the sponge unit. #2110's short-challenge tests (test_challenge_short_scalarfor Keccak and Blake2b, andshort_scalar_refuses_a_field_without_room) run unchanged.New for Poseidon short challenges:
short_challenge_keeps_the_low_bits_of_two_elementschecksb = 128,130and192againstx0 + p * x1computed withnum-bigint. It also checks that the next challenge is the third squeezed element, and that the verifier draws the same value.short_challenge_refuses_zero_loss_and_poisonschecks that a zero loss factor is refused before anything is squeezed, and that it poisons the verifier.Removed or replaced (all Poseidon, whose sponge this PR replaces):
poseidon_tests::test_poseidon_known_vectorpinned 32 squeezed bytes from the old byte sponge. That vector cannot hold once the sponge changes. It is replaced bynative_known_vector, which pins the new sponge's NARG bytes and twoFrchallenges, and by a test that replays the transcript on a bare sponge from the documented packing.test_poseidon_determinism_and_ordersqueezed bytes, which Poseidon no longer offers. Its two checks move tochallenges_bind_messages_session_and_instance: the same messages give the same challenges, and swapping two messages changes them.test_poseidon_prover_and_verifier_agreeis covered byround_trips_byte_and_scalar_messages, which appends a public message on both sides and compares every challenge.poseidon.rsunit tests of the hand-written byte sponge are gone with that sponge: empty versus zero absorb, clone resume, clone divergence and ratchet. The new sponge is spongefish'sDuplexSponge, so its clone and ratchet code is spongefish's. New unit tests cover:[0]pack to different lengths;[0]versus[0, 0]) and for distinct sessions;Rewritten without loss:
pow_tests::sponge_checksandverify_noncenow run on any sponge unit. They still compare 16 squeezed units after the grind and 40 units for the probe, so on Keccak and Blake2b they compare the same bytes as before. One new assertion checks that the probe's check units pass and that its next 16 units equal the prover's challenge.jolt-transcriptfuzz harnesses observe 32 units for states and 16 per challenge, the same byte counts as on refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110.jolt-evaltranscript-symmetry invariant compares challenges asFrvalues drawn throughFieldTranscript, not as 32 raw squeezed bytes, so the same invariant also runs on Poseidon. On Keccak and Blake2b eachFris reduced from at least 32 squeezed bytes, so the comparison still covers about 254 bits.pow.rs's layout test reads the check length fromu8::check_len, which gives the same value as the removedword_len.Open questions
FieldMsg<F>, which absorbed a field element as one element. This PR drops it: field elements go through their byte encoding like every other message, costing 3 elements instead of 1. That keeps a single message path through refactor(transcript): run Jolt on spongefish ProverState/VerifierState and delete the legacy facade #2110's byteEncodingAPI. A native one-element path can come back if recursion cost needs it.DuplexSpongeputs the rate first. circomlib and the Poseidon reference keep the capacity in slot 0. The ideal-permutation bound does not depend on position, but a circuit verifier has to use the same order.🤖 Generated with Claude Code