Repository navigation
Conversation
…, J-4, J-5) Drop labels from absorbed bytes. Byte strings absorb as a u64 length, the body and zero padding to a word; the NARG keeps length and body. Sessions are 32-byte zero-padded protocol tags. Protocol id moves to a16z/jolt-transcript/v2.
The wire nonce stays LEB128. The verifier absorbs it as a u64 LE word and checks the low bits of an 8-byte squeeze.
BN254 challenges squeeze 56 bytes per element, Fp32 candidate pools round to an even count, F8 squeezes pad to a word, and 129- and 130-bit short scalars squeeze 24 bytes.
…-2, J-11) The Jolt statement absorbs its 19 configuration values as bare u64 words, then one advice-presence word computed on every build (bit 0 untrusted, bit 1 trusted), the bare 32-byte preprocessing digest, and the inputs and outputs as length word, body and padding. Every transcript is now opened with a 32-byte protocol tag, including JOLT_SESSION. verifier.rs also carries the J-13 and J-20 label removals in the same functions; they are listed with the label sites in the next commit.
…to J-22) Dory frames byte strings with a length word and absorbs group, serde and field payloads bare; the verifier rejects any Dory proof whose sigma/nu split differs from the one num_vars fixes. Sumcheck rounds absorb one count word and the coefficients. Mid-protocol version labels and constant absorbs are gone. The Jolt-to-Akita bridge uses jolt-akita/group-batch/v5 and jolt-akita/batch/v2 tags, the grouped statement codes each role as a u64 word (0 to 5), and the Akita commitment absorbs its flavor as a word.
The program-preprocessing digest, the Spartan relation digest and the lattice packing and strategy digests switch from Blake2b-256 to Keccak-256 (sha3 0.12, the implementation jolt-inlines-keccak256 is tested against). Domain prefixes and input bytes are unchanged. The opt-in Blake2b sponge leaves these digests on Keccak. blake2 is dropped from jolt-verifier, jolt-spartan-verifier and jolt-claims. No version bump beyond a16z/jolt-transcript/v2.
Re-bless the fs_inventory absorb and challenge sites, regenerate the Dory deser seeds and the verifier fuzz bundles, and rename the ZK fixture cache keys so proofs cached under the J7 transcript regenerate instead of failing.
Contributor
📏 PR diffNote Actual code changed: 1,430 lines🟢 +627 added 🔴 −803 removed Tests, docs, fixtures, and helper scripts excluded.
Every changed line is classified once. |
append_values absorbed each value in its own call, so a group of 4-byte Fp32 or 1-byte F8 values was never padded to a word. Concatenate the encodings, zero-pad once to a word and absorb one message. For whole-word encodings (every production field, Dory's compressed groups) the Keccak and Blake2b streams are byte-identical to the per-value absorbs; a test pins that, and another pins the padding of an odd count of Fp32 values.
On a concrete ProverState, spongefish's inherent prover_message shadowed the trait method and wrote the padded sponge encoding of a BytesMsg to the NARG string, so the verifier rejected it. The setup.rs doc example hit exactly that. The trait method is now send, which nothing shadows; every caller and the doc example use it.
The verifier absorbed the folded witness opening after the inner sumcheck, but the prover never absorbs it, so the two transcripts diverged after BlindFold. Only check_eof followed, so no challenge depended on it; it cost Keccak work and would break any protocol composed after BlindFold. A test now draws one challenge on each side after BlindFold and compares them.
…(R6) The prover's CompressedRoundPoly absorbed the constant and each later coefficient as separate messages, while the verifier absorbed them as one word-padded append_values. The bytes agree for whole-word fields but not for 4-byte ones (12 bytes against 16). Both sides, and the recorder, now call append_compressed_round, which owns the encoding: the count word, then the sent coefficients [c0, c2, ..., cd] as one padded message. Adds a prover-versus-verifier transcript test over Prime32Offset99 and Fr.
Keccak absorbs by overwriting state bytes, so trailing zeros can change the next squeeze in general. Say only what holds in this fixture.
The BlindFold outer and inner sumcheck provers copied the compressed-round encoding (count word, then one append_values). Make append_compressed_round public, re-export it, and call it from both sites, so it is the only place that encodes compressed rounds. The bytes do not change.
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.
Jolt's half of the positional, word-aligned transcript: spec rows J-1 to J-22, rules R1 to R9, the Jolt rows of §6, in the §8 commit order. Every absorb and squeeze on a Jolt path is a whole number of 8-byte words, and nothing is absorbed that the verifier can derive from the statement, the schedule or the message's position. The wire format does not change.
Stacked on
transcript/j7-sponge-port(13e149195). Versions, one bump for the stack:a16z/jolt-transcript/v2(32-byte tag),jolt-akita/group-batch/v5,jolt-akita/batch/v2. Keccak stays the default sponge.Commits
47bc90099codec and tagssetup.rs(see below)95256bfeanonce and checkb08ca3cc3widthsdc9d16962statement and session tags43c4719belabel sitesd486b3be2Keccak digests86e5ed7ceknown-answer regeneration03cd79146one padded absorb inappend_values02b08e30cProverTranscript::send49d192e6fBlindFold witness-opening absorb droppedd15c3d137one shared encoding for compressed sumcheck rounds5061178f7padding-test comment narrowed0f41e8d56BlindFold prover encodes compressed rounds throughappend_compressed_roundWhat changes
PROTOCOL_IDa16z/jolt-transcript/v1, 64 bytesa16z/jolt-transcript/v2, zero-padded to 32 bytesInstance::encodeu64length + statement + paddingBytesMsgu64length + bodyLabel,LabelWithCount,U64Wordu64is the word type (see notes)append_labeled,append_valuesappend_labeleddeleted;append_valuestakes no label and absorbs the concatenated encodings, zero-padded to a word, as one message (R6)⌈bits/8⌉bytesu64LE word absorbed; check reads the low bits of one 8-byte squeeze(len + k) · 4⌈b/8⌉= 17 bytes, one-byte maskbeta2,alpha4,d3)(nu, sigma) ≠ (⌊n/2⌋, ⌈n/2⌉)spartan-clear-v1,hyperkzg-binary-bn254-v1,BlindFoldrelation_digest,policy_id,setup_idabsorbed as 32 bare bytesjolt-akita/precommitted-group-batch/v4(38 bytes),jolt-akita/batchjolt-akita/group-batch/v5,jolt-akita/batch/v2, each a 32-byte tag, then the 16-byte challenge (48 bytes)"akita/fp128", flavor stringappend_batch_statementAkitaCommitment::encodeBytesMsgsAkitaHidingCommitment::encodeBytesMsgfield_inc, 4 bytecode, 5 program image), commitment, point, evaluations. Unknown or indexed roles are rejected withOpeningsErrorappend_labeledper commitmentram_val_check_gamma,spartan-outerzerosha30.12, the hashjolt-inlines-keccak256is tested against). Prefixes and inputs unchanged; the opt-in Blake2b sponge leaves them on Keccak.blake2dropped fromjolt-verifier,jolt-spartan-verifier,jolt-claimsNotes for reviewers
u64instead of aU64Wordtype (J-4). spongefish 0.8 already encodesu64as its 8 little-endian bytes, which is exactly the word R1 asks for. A new wrapper would only forward to that impl, so every count, flag and configuration value is absorbed as a plainu64.NargMessageandsend.BytesMsghas two encodings: padded in the sponge, unpadded on the wire (NargMessage::write_narg). spongefish's inherentProverState::prover_messagewrites the sponge encoding to the NARG, so on a concreteProverStateit shadowed a trait method of the same name and wrote the padded form; the verifier then rejected (a completeness bug, never a soundness one, and thesetup.rsdoc example hit it). The trait method is nowProverTranscript::send, which nothing shadows; every caller and the doc example use it, andcargo test --doc -p jolt-transcriptpasses.prover_transcript,InstanceandBytesMsg, so crates abovejolt-transcriptcompile again only at commit 5. J-1 and J-2 land in commit 1 becauseprotocol_tagand the newInstanceare part of the codec; commit 4 updates their call sites.verifier.rsis in commit 4 for J-11 but also carries the J-13 and J-20 removals in the same functions.check_matrix_shape, tested bynon_canonical_matrix_split_rejectedin both modes) are in commit 5 with the other Dory changes.akitapath to read the same presence the statement binds. Bit 0 of the presence word is computed on every build fromproof.untrusted_advice_commitment.is_some()(CheckedInputs), andabsorb_akita_commitmentsabsorbs exactly thatOption, so the two cannot disagree. The stage-4 schedule keeps reading the same field.append_valuesis one absorb (commit 8). For every production width (BN254Fr/Fq, Fp64, Fp128, their extensions) and Dory's 32-, 64- and 384-byte compressed groups the encodings are whole words, so the Keccak and Blake2b streams are byte-identical to the old per-value absorbs: proof sizes and permutation counts below are unchanged by it, and clear-Dory absorb calls fall from 20290 to 909 per verify. A group of 4-byte Fp32 or 1-byte F8 values is now padded to a word. Poseidon binds each absorb call's length, so its transcript for a multi-valueappend_valueschanges (fewer permutations); it is not a default sponge.check_eoffollowed, so no challenge depended on it. The verifier absorb is gone, which saves 7 permutations in Dory ZK.-word-aligned.Test-diff audit (against
13e149195)No assertion is loosened. Every changed expectation follows from a spec rule.
jolt-transcriptcodec.rs:bytes_msg_is_length_prefixed→bytes_msg_is_length_prefixed_and_word_padded; newbytes_msg_narg_form_has_no_padding_and_round_tripscodec.rs:label_words_pad_and_pack→protocol_tags_pad_to_four_words;U64Wordbig-endian assertions removed with the typesetup.rs: newprotocol_id_is_the_padded_v2_tag,instance_encoding_is_the_tag_then_the_statement_as_bytes(replace the 64-byte id tests)tests/common/mod.rs:test_label_framing,test_label_too_long→test_protocol_tag_too_longpow.rs: newcheck_reads_the_low_bits_of_one_le_word;pow_tests.rsnonce helper absorbs a wordbitsof one LEu64transcript.rs: newshort_scalars_squeeze_words_and_keep_only_the_low_bitschallenge_tests.rs: BN254 width assertions 52/53 → 56; newpooled_messages_are_whole_words; known-answer vectors forPrime128OffsetA7F7,Prime64Offset59,FpExt4<Prime32Offset99>, BN254Fr,F128andF8Fralso with J-7, the Fp32 extension with J-8,F8with the R5 word paddingkeccak_tests.rs,blake2b_tests.rs,poseidon_tests.rschallenge vectorsjolt-dorytranscript.rs: four framing tests renamed and rewritten (append_bytes_absorbs_length_word_payload_and_padding,append_field_absorbs_the_little_endian_scalar,append_group_absorbs_the_bare_compressed_point,append_serde_absorbs_the_bare_compressed_value);caller_labels_do_not_reach_the_transcriptkeptjolt-dory/tests/challenge_bursts.rsjolt-dory/tests/commit_open_verify.rs: newnon_canonical_matrix_split_rejectedjolt-sumchecktests.rs:clear_round_verifier_with_label_absorbs_label,…_no_label_absorbs_a_sumcheck_round,…_no_label_binds_the_coefficient_count→clear_round_absorbs_count_word_then_coefficients,clear_round_binds_the_coefficient_count;committed_output_claims_absorb_length_and_order→…_absorb_commitments_in_orderjolt-sumcheck/tests/roundtrip.rs:labeled_round_verifier_roundtrip→full_round_verifier_roundtripram_val_check.rs:draw_challenges_absorbs_domain_separator_then_draws_gamma→draw_challenges_draws_gamma_with_no_absorbstage4/outputs.rsdraw-events testjolt-verifierverifier.rs:statement_binds_each_field_the_verifier_readsrewritten for the positional layout; new presence-word checks (1, 2, 3);validate_inputs_rejects_trace_length_below_pcs_floortakes the presence flag on every buildjolt-akitanative_batching.rs: newgroup_role_codes_are_distinct_and_unknown_roles_reject,bridge_tags_are_distinct_from_each_other_and_the_old_tagspreprocessing.rsFULL_PROGRAM_DIGEST,COMMITTED_PROGRAM_DIGEST, all four feature configurationsfs_inventory/absorb-sites.inventory,challenge-sites.inventoryverify_outer_folded_r1cs,reduce_claims,SpartanKey::begin, stage-8challenge_powers);RamValCheck::draw_challenges::challenge#1goes with its separatordeser_commitmentseeds, verifier fuzz bundles (6)challenge_tests.rs: newappend_values_is_unchanged_for_whole_word_encodings,append_values_pads_an_odd_count_of_four_byte_valuesjolt-dorytranscript.rstest helperbareappend_values, so it stays an unframed, unpadded referencejolt-transcripttests,jolt-evaltranscript_symmetry.rs:ProverTranscript::prover_message(&mut p, m)→p.send(m);jolt-evalrun_checkgains theSyncbound the trait impl needsjolt-blindfoldreal_prover.rs: newprover_and_verifier_transcripts_agree_after_blindfoldfs_inventory/absorb-sites.inventory(commits 8, 10)append_valuessite readsself.absorb(&WordAligned(bytes));verify_inner_folded_r1cs::append_vector_opening#3removedjolt-sumchecktests.rs: newcompressed_round_absorbs_like_the_verifier_for_four_byte_and_bn254_fieldsCompressedRoundPoly, the verifier's encoding and a hand-built layout (count word, then c0, c2, c3 zero-padded to a word) give the same next challenge, overPrime32Offset99(4-byte) andFrchallenge_tests.rsappend_values_pads_an_odd_count_of_four_byte_values(commit 12)fs_inventory/absorb-sites.inventory,challenge-sites.inventory(commit 11)CompressedRoundPolyabsorbs and the verifier's two absorbs become calls toappend_compressed_round, which holds the count word and the oneappend_values;verify_compressed::challenge#2→#1prover_transcript(b"...")→prover_transcript(&protocol_tag(b"..."))Proof size (same seed, bincode)
13e149195(bytes)Sizes are re-measured at
5061178f7and match86e5ed7ceexactly in every row. Dory proofs are byte-identical in size in both modes, as expected: the wire format does not change. The Akita differences are not a format change. Akita codes the terminal tail of its opening proof with Golomb-Rice (akita-params/src/golomb_rice.rs), so its length depends on the challenge values, and every challenge changes with the v2 transcript. In the advice case all 34 bytes are in the joint opening proof.Muldiv, per component (J7 → here): the joint opening proof is 74891 → 74890 (the Golomb-Rice tail above), and the stages are 11944 → 11942. Every other field is the same size: commitments 296, claims 4822, each of the ten sumcheck proofs, and the small config fields. The 2 stage bytes are all in
pow_nonces, 16 → 14. The nonces are bincode varints: a value below 251 takes 1 byte, a value up to 65535 takes 3. Grinding searches from nonce 0, so each nonce depends on the transcript state at its site. At J7 the stage-6a nonce is 419 (3 bytes); here it is 73 (1 byte). The other nine nonces are below 251 in both builds, and stage 8 has none. Base nonces: 5, 0, 1, 48, 19, 23, 4, 419, 3, 15; here: 43, 0, 1, 25, 10, 9, 42, 73, 4, 75. So the muldiv change is 1 byte of Golomb-Rice tail plus 2 bytes of nonce varint, and neither is a format change.Keccak permutations (one recorded honest verify, rate 136)
Counted from a trace of every absorb, squeeze, challenge draw, short scalar and grind in
ScriptedTranscript, replayed through a rate-136 sponge model. This includes the short scalars, grinds and verifier-side prover messages that J7's published counts missed, so the J7 column is re-measured rather than copied.The challenge draws (286 to 323 per verify) are the same in both columns, so the schedule is unchanged.
Against §7:
86e5ed7ce); clear Dory and Akita are unchanged by the review fixes.Validation
All on a remote Boat sandbox; nothing was built locally.
Run on sandbox
bx_ecgwkhfnwith-j4, per crate. The round-2 fixes were checked at tree6ecac648(git diff --binary 13e149195of5061178f7; the sumcheck and transcript suites ran on the same tree before the inventory re-bless). The round-1 fixes were checked at treee31418f3(49d192e6f); everything else at195c0876(86e5ed7ce). Commit 8 changes the sharedappend_values, so the other crates' suites were not re-run on it; the honest Dory clear, Dory ZK and Akita verifies of the size and permutation runs did pass on the fixed code, and CI covers the rest at the new head.clippy --all-targets --all-features -D warningsat0f41e8d56jolt-sumcheck,jolt-blindfold0f41e8d56jolt-sumcheck112,jolt-blindfold76fs_obligations(fs-audit) at0f41e8d56akita; re-blessing changes no inventory lineclippy --all-targets --all-features -D warningsat5061178f7jolt-sumcheck,jolt-transcript5061178f7jolt-sumcheck112,jolt-transcript92fs_obligations(fs-audit) at5061178f7akitaclippy --all-targets -D warningsat49d192e6fjolt-transcript,jolt-dory,jolt-blindfold(all features),jolt-eval49d192e6fjolt-transcript92 (all features),jolt-dory52,jolt-blindfold76,jolt-evaltranscript invariants 649d192e6fcargo test --doc -p jolt-transcript, default and all features: passfs_obligations(fs-audit) at03cd79146+akitaclippy --all-targets -D warningsat86e5ed7cejolt-verifier(plain andakita),jolt-spartan-verifier,jolt-akita,jolt-claims,jolt-hyperkzg,jolt-openings,jolt-sumcheck86e5ed7cejolt-verifierlib 79 plain / 72akita,jolt-akitalib 46,jolt-claims150,jolt-hyperkzg9,jolt-openings15,jolt-poly142,jolt-sumcheck111preprocessingdigest vectors (R9)akita, each with and withoutfield-inline)fs_attacksakita2: all passcargo checkof all ten fuzz crates; seeds and bundles regenerated by their generatorscargo macheteblake2cargo fmt --check,taplo fmt --check,typosNot run here:
jolt-prover's own tests (its library builds throughprover-fixtures) and workspace-wide jobs. CI covers those.Follow-ups (not in this PR)
jolt-dory/fuzz(Transcript),jolt-openings/fuzz(Transcript) andjolt-verifier/fuzz(ProverState).🤖 Generated with Claude Code