Repository navigation
Conversation
…cript The Poseidon sponge now works over BN254 Fr elements: spongefish's overwrite-mode DuplexSponge with the circom width-4 permutation (rate 3, capacity 1). Byte strings pack as their length followed by 31-byte little-endian chunks. Challenges on Poseidon are squeezed Fr elements; byte-decoded challenges stay off Poseidon. Grinding squeezes one Fr and tests its low bits.
Width 5, rate 3, capacity 2: a single element of capacity bounds the sponge at about 2^127 calls, short of the 2^128 target. Pins the full permutation state against circomlibjs and regenerates the verifier's absorb-site inventory.
Contributor
📏 PR diffNote Actual code changed: 703 lines🟢 +451 added 🔴 −252 removed Tests, docs, fixtures, and helper scripts excluded.
Every changed line is classified once. |
… transcript/j4-poseidon-native-fr # Conflicts: # crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory
quangvdao
added this pull request to stack #2098
October 21, 2026 15:49
quangvdao
marked this pull request as ready for review
October 21, 2026 15:49
Contributor
Benchmark comparison (crates)Same-runner A/B: 2 of 81 benchmarks differ by more than ±10%:
All 81 benchmarks
|
Collaborator
Author
An error occurred while trying to automatically change base from
transcript/j3-sponge-pow
to
main
October 24, 2026 09:30
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 #2099 (
transcript/j3-sponge-pow).What changed
The Poseidon transcript now works over BN254
Frelements. Before this change it worked over bytes and turned each squeezedFrinto little-endian bytes. Now it absorbs and squeezes field elements directly.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). A single element of capacity would give only about 2^127 hash calls, short of the 2^128 target. Two elements give about 2^253.Frelement, used as is: no byte decoding and no reduction bias. Poseidon does not implement the sealedUniformBytestrait, so challenges decoded from bytes, and challenges in any other field, cannot be requested on Poseidon. Asking for them is a compile error.field_challengesnow goes through a new sealed trait,ChallengeSponge<F>. Keccak and Blake2b get it from their existing byte-decoding path, and Poseidon implements it only forFr. The challenge-size cap counts 32 bytes per element and is checked before anything is squeezed.FieldMsg<F>. On Poseidon it is absorbed as one element. In the proof it is written as the element's canonical little-endian bytes, and the verifier rejects short or non-canonical bytes.Frand requires the lowbitsbits of its canonical integer to be zero. Forbits <= 25those bits are within 2^-228 of uniform. Each nonce byte is absorbed as one element, both in prover and verifier messages and in theMessageSearchprobe, so the search tests exactly what the verifier checks. Grinding still runs once per round. The unit-specific parts are in a sealed trait,PowUnit, implemented foru8andFrUnit.Transcript::checkexists only for byte sponges. The verifier therefore poisons the transcript itself: on failure it reads a message that never parses, and it never absorbs or squeezes while doing so. Every rejection still makescheck_eoffail: bad length, non-canonical element, truncated input, failed proof of work, oversized challenge request.PoseidonTranscriptis now a plain struct (no field parameter). It packs the label and framed appends as described below, andchallenge()returns a full-widthFr.How bytes are packed
Bytes enter the Poseidon sponge only in these forms:
BytesMsg, the session identifier, or either part of the transcript instance) becomes the elementn, 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. The leading length fixes how many chunks follow and how long the last one is. The map is therefore injective, and no packed string is a prefix of another.u8is one element, the byte's value. The proof-of-work nonce is sent this way, one byte per message. A[u8; N]therefore costs N elements; useBytesMsgfor byte strings.FieldMsg<Fr>is one element, the field element itself.Soundness audit
An independent auditor reviewed the change at xhigh effort. They covered capacity and rate, permutation correctness, domain separation, packing injectivity, challenge uniformity, grinding, verifier poisoning, and verifier panics. They found no exploitable bug.
poseidon(inputs, init, 5).PROTOCOL_IDwas not bumped, because that would change the Keccak and Blake2b bytes too.Validation
All runs were on a remote sandbox.
cargo fmt --all --checkcargo clippy -p jolt-transcript --all-targets -- -D warningswith--all-features, with--features bn254,solinas,binary,prover-grinding, with default features, and with--no-default-features --features transcript-poseidon,prover-grindingcargo nextest run -p jolt-transcriptwith the same four feature setscargo check --workspace,cargo clippy -p jolt-eval --all-targets -- -D warnings, andcargo check -p jolt-sdk --features host,transcript-poseidoncargo nextest run -p jolt-verifier --test fs_obligations --features fs-audit, with the absorb-site inventory regenerated for the new Poseidon codeNew Poseidon tests:
Frchallenges, byte messages and field messages;check_eof;Open questions
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.PoseidonTranscript::challenge()now returns a full 254-bitFr, where it used to decode 16 squeezed bytes (about 128 bits). The prover multiplies by wider challenges as a result.🤖 Generated with Claude Code