Repository navigation
feat(transcript): field challenge codecs with a 2^-160 per-message bias budget (J2) - #2097
Conversation
📏 PR diffNote Actual code changed: 317 lines🟢 +315 added 🔴 −2 removed Tests, docs, fixtures, and helper scripts excluded.
Every changed line is classified once. |
eef0a00 to
6af666c
Compare
Reject unsupported lengths and messages above MAX_CHALLENGE_BYTES before touching the sponge; return early on empty messages; poison the verifier on pool exhaustion; seal ChallengeCodec; correct the BN254 width table; lint and test jolt-transcript with all features in CI.
Benchmark comparison (crates)Same-runner A/B: No differences above the ±10% noise threshold. All 81 benchmarks
|
…ected lengths PoseidonSponge squeezes the bytes of a BN254 scalar, so its output is far from uniform and the codecs' TV bound fails there. Restrict field_challenges to sponges with uniform output (sealed UniformBytes: Keccak, Blake2b512). The verifier now also poisons on a rejected length.
There was a problem hiding this comment.
Two policy issues remain: the public API precedes its first production caller without a documented external-use contract, and several added comments are inaccurate or repeat the code. No sampling or verifier-consistency defect was found. The codec-local bias bound is not a whole-proof 128-bit Fiat–Shamir security claim.
…enge-codecs # Conflicts: # .github/workflows/rust.yml
…m comments - ChallengeCodec, UniformBytes and MAX_CHALLENGE_BYTES stay public because they bound the public field_challenges methods; their docs now state that contract and name Akita's transcript as the consumer. - Remove the doc block on the crate-private message_bytes helper. - Remove the test-file heading and a comment that restated its assertions. - Correct the CI comment: default features already cover BN254 and every sponge; the all-features run adds the Solinas and binary codecs.
There was a problem hiding this comment.
The four earlier threads are addressed at d9c20cd. One new problem, inline: the external-contract doc names a consumer that does not exist yet. I found no sampling or prover/verifier consistency defect. The 2^-160 per-message bias bound is local to each codec and is not a whole-proof 128-bit Fiat–Shamir claim; this PR switches no existing challenge call site.
Stacked on #2095 (J1). Second step of moving Jolt, then Akita, onto one Keccak duplex-sponge transcript.
What this adds
jolt_transcript::ChallengeCodecandfield_challenges::<F>(len)on bothProverTranscriptandVerifierTranscript. One call squeezes a fixed number of bytes as one verifier message (verifier_message_as), then decodeslenfield elements from them.Security budget. The whole message has total variation at most 2^-161 from uniform. The duplex-sponge theorem charges twice that, so 2^-160 per message. The target is that an attacker needs about 2^128 hash calls to break the proof system. That target is priced per hash call.
Codec per field
Fp128(2^128 − 275 and others)Fp64Fp32(2^32 − 99)Ext2<Fp64>FpExt4<Fp32>Fr,Fq(BN254)F8,F64,F128,F192NUM_BYTESbytes masked tobbits, and is kept if it is below p. The codec squeezeslen + kcandidates. k is the least value with (k+1)(R − L) ≥ 161, where R = b − ⌈log₂ c⌉ and L = ⌈log₂(len + k)⌉. Kept values are exactly uniform. If the pool runs out (probability ≤ 2^-161), the call returnsVerificationError.len · DEGREEbase coordinates, assembled withExtField::from_base_fn. The codec is implemented only forExt2withTwoNr,FpExt4andFpExt8, over the nine registered bases. All nine are ≡ 5 (mod 8), so 2 is a non-residue.Prime128OffsetA7F7is left out, because its default quadratic tower is not a field.All bounds use integer arithmetic. They are a little looser than the exact minimum, which costs at most a few bytes per message.
Verifier behaviour
field_challengesrequiresH: UniformBytes, a sealed marker implemented for Keccak and Blake2b512.PoseidonSpongeis excluded: each block it squeezes is the little-endian encoding of a BN254 scalar, so its top bits are far from uniform and the codecs' TV bound would not hold. Poseidon's bytes are unchanged; it simply cannot draw field challenges this way.MAX_CHALLENGE_BYTES(16 MiB, which caps BN254 at 310,689 elements), returnsVerificationErrorbefore touching the sponge. A length taken from the input therefore cannot make the verifier allocate without bound. The verifier also poisons, so a caller that drops the error still failscheck_eof.field_challenges(0)returns an empty vector without squeezing. An empty squeeze is not a no-op for every sponge (it changes the Blake2b bridge state), so skipping it keepslen == 0neutral everywhere.spongefish::Transcript::check, so later reads andcheck_eoffail too.Compatibility
verifier_message::<T>()call sites are unchanged. Moving callers over comes in later PRs.solinasandbinaryforward tojolt-field.legacy_digest_testsnow declaresrequired-features = ["bn254", "transcript-blake2b"], so the Keccak-only build skips it.Tests
tests/challenge_tests.rscovers the following:jolt/challenge-codecs/v1, including messages that cross the 136-byte Keccak rate;CI now lints and tests
jolt-transcriptwith--all-features. The per-crate test loop used default features only, so the codecs and their tests never ran there.cargo nextest run -p jolt-transcript --all-features: 90 passed.Next
J3 (#2099) adds a proof-of-work step on the transcript sponge, with the same layout as the spongefish PR.
🤖 Generated with Claude Code