Repository navigation
feat(transcript): proof of work on the transcript sponge - #2099
Conversation
📏 PR diffNote Actual code changed: 424 lines🟢 +422 added 🔴 −2 removed Tests, docs, fixtures, and helper scripts excluded.
Every changed line is classified once. |
40aff20 to
b918e64
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.
a25f8b4 to
9411420
Compare
9411420 to
eaefc6f
Compare
Benchmark comparison (crates)Same-runner A/B: No differences above the ±10% noise threshold. All 81 benchmarks
|
Add grind / check_grind: a standalone proof-of-work step on the transcript sponge that protects the challenges drawn after it, up to the next prover message. The nonce is canonical LEB128 below 2^(bits+7), one byte per prover message; the check squeezes ceil(bits/8) bytes and needs the low bits zero. Difficulty is at most 25 bits. MessageSearch probes byte by byte so sponges that absorb a length per call (Poseidon) replay the real transcript. Failed checks poison the verifier. CI lints and tests the sequential search.
…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.
eaefc6f to
a59c2ab
Compare
…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.
…ge-pow # Conflicts: # .github/workflows/rust.yml
There was a problem hiding this comment.
One finding, inline: round_trips duplicates existing coverage. Nothing else needs to change at 8b49d91.
There was a problem hiding this comment.
One test-redundancy finding remains at 9b9cf9b: remove known_answers, whose cases are already covered by the integer-oracle test. The earlier round_trips finding is addressed. No production or soundness defect was found.
There was a problem hiding this comment.
No code findings remain at f5a2197: round_trips and known_answers are gone, and both threads are addressed.
This can't land yet because Prove x86-64 baseline and BMI2 arithmetic (Fp64 formal verification) did not pass at this head. GitHub cancelled it at its 45-minute limit before the HOL Light proofs ran: https://github.com/a16z/jolt/actions/runs/40039630199/job/120707813828. In Install proof dependencies, apt-get install fetched 153 MB in 40 min 32 s (63.0 kB/s); when this job passed at 9b9cf9b, the same step fetched it in 3 s. A passing rerun at this head is needed before merge.
There was a problem hiding this comment.
Prove x86-64 baseline and BMI2 arithmetic passed on rerun at f5a2197 (https://github.com/a16z/jolt/actions/runs/40039630199/job/120724532377), and all 67 checks are green. Its timeout was my previous review's only finding, so nothing remains to change at this head.
Stacked on #2097 (J2), which is stacked on #2095 (J1). Third step of moving Jolt, then Akita, onto one Keccak duplex-sponge transcript.
What this adds
A proof-of-work step on the transcript's own sponge:
ProverTranscript::grind(bits)andVerifierTranscript::check_grind(bits). It is a step of its own, not tied to one challenge call.bits == 0: does nothing.1 ≤ bits ≤ 25(MAX_POW_BITS):bitsbits are zero.The prover sends the least nonce that passes, so honest proofs are deterministic.
VerificationErrorbefore touching the sponge. Nothing panics.The layout is the one in arkworks-rs/spongefish#301 (
PowTranscriptExt::sponge_pow). There is no separate proof-of-work hash. With Keccak, each search attempt costs one permutation.Why a varint nonce
The search space is 2^(bits+7), so the honest search fails with probability at most e^-128. Most nonces are small, so LEB128 is short on average. For a run of about 400 grinding steps between 1 and 10 bits, the nonces take about 420 bytes in expectation, against about 820 for a fixed-width nonce and about 3,300 for a
u64. The cap of 25 bits keeps the nonce in au32; no protocol should grind anywhere near that.Verifier behaviour
spongefish::Transcript::checkor a failed read, so the state is poisoned andcheck_eoffails afterwards too.Prover search
MessageSearch(behindprover-grinding) copies the sponge and implementsProbe: absorb a candidate message, squeeze some bytes, without touching the transcript.PoseidonSpongeabsorbs a length with every call, so absorbing the whole nonce at once would give a different state than the transcript, and the verifier would reject.parallelsearches on all threads with rayon and returns the same least nonce as the sequential scan.Dependencies: spongefish 0.8.0 only
check_grindis always available.yolocryptofeature. That feature makes internals public and changes no computation.grindandMessageSearchtherefore sit behind a newprover-grindingfeature, which turns onspongefish/yolocrypto.cargo tree -e featuresshows noyolocryptofor defaultjolt-transcriptor forjolt-prover, since no protocol grinds yet. The oneyolocryptoinjolt-verifier's tree comes from Akita's ownakita-transcript; moving Akita over removes it.parallel(it impliesprover-grinding) searches on all threads with rayon.MessageSearchcan defer to spongefish's search primitive andyolocryptogoes away.Security note
For the Keccak duplex sponge, the reduction of Chiesa and Orrù prices each attempt per permutation call. The Blake2b hash bridge and Poseidon need their own argument; the docs say so.
Tests (
tests/pow_tests.rs, plus unit tests insrc/pow.rs)bits∈ {1, 7, 8, 9, 16} and challenge lengths {0, 1, 32, 200}. The test also checks that every smaller nonce fails, so the sent nonce really is the least one.Fp128<2^128 − 275>andFr, both orders) at bits 0, 1, 8, 9 and 16.bits = 0: byte-identical to no step.MessageSearch: probing matches sending then squeezing, and leaves the transcript untouched.check_eoffails afterwards. 26 bits andu32::MAXbits are rejected with the transcript unchanged.Validation (Linux x86 sandbox)
cargo clippy -p jolt-transcript --all-targets -- -D warningswith--all-features,--features bn254,solinas,binary,prover-grinding,--no-default-features --features transcript-keccak, and default featurescargo nextest run -p jolt-transcript: 103 passed with--all-features, 103 withbn254,solinas,binary,prover-grinding, 10 with Keccak onlyCI gains a clippy run and a test run for
bn254,solinas,binary,prover-grinding, since--all-featuresturns onparalleland would skip the sequential search. Rebased onto the updated J2, which caps message sizes, skips empty squeezes and poisons the verifier when the rejection pool runs out.🤖 Generated with Claude Code