Skip to content

refactor(transcript)!: write Jolt proofs as spongefish NARG argument strings - #2000

Closed
markosg04 wants to merge 66 commits into
mainfrom
refactor/transcript-unification
Closed

markosg04 wants to merge 66 commits into
mainfrom
refactor/transcript-unification

Conversation

@markosg04

@markosg04 markosg04 commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Jolt proofs become NARG argument strings. jolt-transcript is a typed layer over spongefish's ProverState and VerifierState. Proof messages are declared by typed claims structs per stage, not by hand-written per-stage wire aggregates. Akita opens inside the Jolt transcript, so the bridge and the separate Akita proof blob are gone.

The design and its invariants are in specs/jolt-transcript-spongefish-state.md, which amends specs/jolt-transcript-narg.md. The typed messages are specified in specs/jolt-verifier-typed-messages.md. Main points:

  • Field challenges are exactly uniform: rejection sampling over fixed-width squeezes. Poseidon emits only the low 16 bytes of each permutation (within 2^-125 of uniform), so this holds at any squeeze offset.
  • No crate enables spongefish's yolocrypto; scripts/check-no-yolocrypto.sh checks the root workspace and every standalone workspace in CI. Grinding is squeeze-then-grind on seeded Forks, with a canonical LEB128 Nonce.
  • No preview and no transcript Clone. The BlindFold replay compares a 32-byte checkpoint squeezed after the stage spine on both sides.
  • The verifier rejects trailing bytes, poisons the transcript after any failed read, and checks that every byte receive_bytes returns is the byte the sponge absorbed.

The branch is merged with main (not rebased, so the commits Akita pins stay reachable). The merges port main onto the NARG transcript: the Spartan and HyperKZG crates (#1880), native Akita trace batching (#2012), backends bound to execution contexts (#2028), unified trace rows (#1996), whole-bytecode commitments in place of bytecode chunks, and the binary fields with their sum-check (#2034), which gain the same spongefish codecs as every other atom. Main's Akita chunk-profile binding (#1976) is kept. Main's clear proof-type cleanup is already covered by this branch's recorders.

Akita's runtime witness-chunking policy (LayerZero-Labs/akita#246, merged into akita#185) moves chunking out of the commitment config. Each Jolt catalog family fixes its own chunking through CatalogChunking, and gen_jolt_schedules preplans the direct rows below the setup-offloading cutover. The checked-in catalogs are unchanged.

Companion PR: LayerZero-Labs/akita#185, which pins this branch.

Since the last review round this branch also:

  • binds the exact-challenge squeeze width on Keccak and Poseidon, whose absorbs forget it (Sponge::ABSORB_FORGETS_SQUEEZE_WIDTH);
  • adds Channel::record_fork_draw, which logs accepted fork draws in the event log only;
  • squeezes word-sized remainders in one call, with byte-identical proofs.

The Keccak and Poseidon known answers are refrozen.

Changes

  • jolt-transcript: the NARG prover/verifier channel, sites, forks, grinding, and nonce. The legacy symmetric transcript, Duplex, and Preview are deleted.
  • jolt-field, jolt-crypto, jolt-dory: atoms are spongefish codecs. G1/G2 decoding accepts only canonical encodings, including for the identity.
  • Sumcheck, openings, Dory, BlindFold, HyperKZG, Spartan, the prover, and the verifier write and read the argument string. Each BlindFold, HyperKZG, and Spartan message group has one owner both roles call.
  • The SDK's ProtocolSponge is jolt_verifier::JoltSponge, the one place the sponge is chosen.
  • Tests: message/absorb/challenge/site census; frozen challenges replayed through an audit sponge (seeded forks pass through untaped); every argument-string message swept against its rejection deadline; per-sponge known answers; end to end on a swapped sponge.

Testing

  • Per-package suites after the main merge, including jolt-transcript, jolt-field (with binary), jolt-sumcheck (committed), jolt-claims, jolt-dory, jolt-blindfold, jolt-akita (91/91, and 98/98 with field-inline), jolt-kernels, tracer, and the binary-field sum-check suite ported to argument strings (19/19).
  • jolt-verifier suites with prover-fixtures,logging 93/93, +zk 88/88, +field-inline 99/99, akita,prover-fixtures,logging 79/79, prover-fixtures,field-inline,akita,logging 81/81 (these include the Fiat-Shamir attack legs and the message and truncation sweeps). The FS inventories are re-blessed for the whole-bytecode commitment and the Akita stage-8 changes.
  • jolt-prover suites: prover-fixtures 30/30, +zk 32/32, +field-inline 41/41, +field-inline,zk 26/26, akita,prover-fixtures 41/41, prover-fixtures,field-inline,akita 37/37. The verifier fuzz fixture bundles are regenerated.
  • Every cargo clippy lane in rust.yml with -D warnings; cargo fmt, taplo, cargo machete, check_style_invariants.py, check-no-yolocrypto.sh, check-shared-field-identity.sh; the jolt-verifier fuzz harness builds.
  • Not run locally: z3-verifier (no native Z3 on this machine) and the WASM verifier end to end; CI runs both.

Security Considerations

This changes the Fiat-Shamir transcript for every proof, so it affects soundness directly. Review focus:

  • Every proof byte enters the verifier only through receive*, which absorbs it. Dory parses ahead from unread(), but every parsed byte is received and compared afterwards; read_proof restates dory-pcs's message order and carries a WARNING to that effect.
  • The exact-challenge sampling and the fork construction (tag jolt-transcript/fork/v1, seed, LE32(counter)).
  • ProofHeader fields travel as byte arrays, as one [u8; 6] message for the six widths, because spongefish decodes only u32 and byte arrays.

Not addressed here: prover blindings come from the OS RNG, not spongefish's transcript-bound ProverState::rng() (now a stated non-goal of the spec).

Breaking Changes

  • Proof format: header plus argument string. Old proofs and fixtures do not verify. Poseidon proofs also change with the 16-byte squeeze.
  • jolt-transcript API: the NARG prover/verifier channel replaces the symmetric transcript.
  • SDK: the proof sponge is chosen by jolt-verifier's transcript-* features (Poseidon, then Keccak, then Blake2b).
  • Pins: Jolt pins Akita a4978d59 (akita#185), which pins this branch's merge commit f15e9b17. Merge order: merge this PR, repin Akita to Jolt main and merge it, then repin this repo to Akita main.

Transcript atoms need one fixed-width canonical codec that every message
type implements, including extension fields, which had an encoding but no
checked decoder. Move `from_bytes_le_checked` from `CanonicalEncoding` into
a new `CanonicalDecode: CanonicalBytes` supertrait and implement it for the
prime fields, `FpExt2`/`FpExt4`/`FpExt8` (coefficient-wise, rejecting any
non-canonical coefficient), unsigned integers, and byte arrays. `FpExt8`
gains the `CanonicalBytes` encoding its siblings already had.
A proof is its argument string. `ProverTranscript` appends each prover
message and absorbs exactly the appended bytes; `VerifierTranscript`
reads messages back through `receive*`, absorbs exactly the bytes read,
poisons itself on the first failure, and rejects trailing bytes in
`finish`. Code both roles run identically targets the `Channel` trait,
whose `exchange` sends for the prover and receives in place for the
verifier, so sub-protocols compose by sharing one transcript.

Atoms are the `CanonicalBytes`/`CanonicalDecode` codecs; shapes are
positional. `challenge` is exactly uniform through `Field::random`, and
`challenge_small` decodes 16 squeezed bytes through the field's small
challenge set. The sponge is a type parameter whose `Sponge::ID` is bound
into the 64-byte `ProtocolId`. `Preview` exposes a detached sponge copy for
proof-of-work search, and `grind`/`check_grind` implement low-bit-first
proof of work over canonically encoded nonces. Under `logging`, each
transcript records site-tagged events with argument-string ranges.

The unused spongefish split traits and `OptimizedChallenge` are removed;
the legacy symmetric facade stays until its consumers migrate.
G1 and G2 encode as their compressed points and GT as its compressed
`Fq12` element through `CanonicalBytes`/`CanonicalDecode`, and serde now
reuses that codec instead of a parallel one. Decoding keeps every existing
check: exact length, on-curve and subgroup membership for points, and
nonzero, unitary, r-torsion membership for GT. `JoltGroup` and vector
commitment outputs require the codec instead of `AppendToTranscript`,
`NoCommitment` decodes from the empty string, and jolt-crypto no longer
depends on jolt-transcript.
`challenges_small` draws a vector from the small challenge set and
`challenge_powers` expands one exactly uniform challenge into its powers,
so the many call sites that need either share one definition.
Callers name `Blake2b512` and `Keccak` through jolt-transcript, so no
protocol crate depends on spongefish directly.
Opening proofs now live in the caller's transcript: `open` and the batch
provers write their prover messages into a `ProverTranscript`, and
`verify` and the batch verifiers read them back from a
`VerifierTranscript`, so `CommitmentScheme::Proof`,
`BatchOpeningScheme::Proof`, and `ZkBatchOpening::proof` are gone. Each
scheme owns its commitments' wire codec through `send_commitment`,
`receive_commitment`, and `absorb_commitment`, because commitment sizes
are scheme-specific. Batch statements, prefix-packed claims, and hiding
commitments absorb as positional public atoms without labels, and RLC
coefficients come from `challenge_powers`. Transcript failures surface as
`OpeningsError::Transcript`.

jolt-poly's `transcript` feature, whose only user was this crate's
`EvaluationClaim` absorb, is removed. The Dory-backed tests compile with
the next commit, which ports jolt-dory.
Every value dory-pcs absorbs through `append_serde` is now a prover message
of the caller's transcript, so the argument string holds exactly Dory's
messages in Fiat-Shamir order and the separate `DoryProof` wire type, its
serde caps, and the round-count pre-parser are gone. dory-pcs verifies from
a proof struct handed over up front, so the verifier rebuilds that struct by
reading the messages on a scratch copy of the transcript, then runs
`dory::verify` with an adapter that receives each message from the live
transcript and requires it to equal the value dory-pcs absorbs. The Σ₁
responses, which dory-pcs never absorbs, follow Dory's messages on both
sides, so no proof byte is read without being absorbed.

`DoryCommitment` encodes as its GT atom. A byte-flip sweep over transparent
and ZK openings replaces the struct-mutation tamper tests. Tests needing the
jolt-akita dev-dependency compile once jolt-akita is ported.
…ranscript

There is no sumcheck proof type anymore. Recorders write round messages
into a `ProverTranscript` and `SumcheckVerifier` reads them back from a
`VerifierTranscript`: clear rounds as compressed polynomials, uni-skip
rounds in full, committed rounds as vector commitments. Every round travels
at its public degree bound (the prover pads instead of trimming), so the
verifier derives each message's width rather than trusting a prover-chosen
length, and committed rounds commit `degree + 1` coefficients, fixing
BlindFold's layout publicly. Output-claim commitment counts become a public
input to `verify_committed`. Clear input claims absorb as public values,
clear output claims are sent, and round challenges come from the small
challenge set as before.

`SumcheckProof`, `ClearProof`, the labeled round wrappers, the transcript
label constants, `CommittedRound`, `CommittedRoundWitness`, and the
proof-encoding and round-count errors are removed. The crate's tests are
ported in the following commit.
`BlindFoldProof` is gone: `prove` writes every BlindFold message into the
caller's `ProverTranscript` and `BlindFoldProtocol::verify` reads them back
from a `VerifierTranscript`, at counts fixed by `BlindFoldDimensions` and the
final-opening layout rather than by prover-supplied vector lengths (row
openings are read at the protocol's row width). The folding commitments are
sent as one block before the folding challenge; the relaxed instances are
no longer re-absorbed, because every commitment in them is already bound by
that block or by the stage that produced it. The folded evaluation outputs
and blindings, previously checked but never absorbed, are now sent before
the evaluation openings, and both sides send those openings in one
per-coordinate order. The outer and inner Spartan sumchecks use the shared
compressed round codec. Checks that consumed proof values before their
absorb position now run right after the corresponding receive.

The crate's tests are ported separately.
Round trips now prove into a `ProverTranscript`, verify from the argument
string, and require the verifier to finish with no bytes left. Struct
mutation tamper tests become argument-string attacks: every byte flip must
fail exactly its round's check (or decode), every truncation and any
trailing data must reject, non-canonical coefficients reject, and swapped,
replayed, or re-sessioned rounds reject. A wire pin fixes the full and
compressed round layouts, including zero padding to the degree bound.

Tests of states the fixed-width wire format can no longer express (wrong
proof encoding, round-count and degree mismatches, short compressed rounds,
labels) are removed. Extension-field round trips are removed because the
transcript paths draw round challenges through
`CanonicalEncoding::from_challenge_bytes`, which the extension types do not
implement; no production caller runs jolt-sumcheck over an extension field.
The external-consumer fixture runs over Keccak.
End-to-end tests now write each committed stage and then the BlindFold
proof into one argument string, read the stages back with
`verify_committed`, verify BlindFold from the same transcript, and require
it to finish with no bytes left. The per-field tamper tests are replaced by
a sampled byte-flip sweep across the whole argument string plus
truncation, trailing-data, and wrong-session rejections. Unit fixtures build
consistencies and output-claim commitments directly, and verifier unit tests
mutate typed messages written in the wire order. Tests of length mismatches
the verifier no longer reads from the proof are removed. The empirical
randomness test now samples the production prover.
A Jolt proof is now `JoltProof { protocol, narg }`: the protocol config
plus the argument string. The verifier reads every prover message from
`narg` in protocol order and finishes with an EOF check, so no proof
field exists outside the transcript.

- The proof header (trace length, RAM K, rw/one-hot configs, trace
  order, untrusted-advice flag) is the first message; the commitments
  follow at counts fixed by the RA layout.
- The public preamble absorbs the verifier's own protocol config.
- Stage output claims are received by shape: each member's wire
  openings in canonical order, with aliased openings filled from their
  sources. Stage 6b's runtime bytecode-RA dedup goes through the same
  helpers, and its ZK commitment count is derived from the verified
  rounds (`verify_zk_with`).
- Stage 4's staged advice/program-image openings are sent before the
  batch in clear mode, because the input claim consumes them.
- Stage 8 takes the received commitments and runs the transcript
  sequence of the shared homomorphic batch opening.
- The scalar-challenge endianness axis is gone; challenge semantics are
  fixed by the transcript.

The verifier test modules are ported in a later commit.
The prover runs every stage on one `ProverTranscript` and returns its
argument string as the proof, so it no longer assembles stage proofs,
claims, or a joint opening proof.

- Stage 0 mirrors the verifier's seeding: it validates and sends the
  proof header, absorbs the public preamble, sends the commitments, and
  absorbs the public ones.
- The stage driver returns the recorder's witness. Clear recorders send
  the round polynomials and output claims; committed recorders retain
  their openings for BlindFold.
- In clear mode, stage 4 sends the staged advice/program-image openings
  after the gamma draws and only the post-round openings after the
  rounds. In ZK mode it commits the full canonical order.
- Stage 8 writes the homomorphic batch opening to the transcript.
- BlindFold replays the argument string through the verifier's
  `verify_stages`. It checks that the replay consumed every byte and
  reached the forward sponge state, then proves onto the same
  transcript.

The packed (Akita) path is ported with jolt-akita.
…ent string

The twin tests now hand the prover's argument string to the verifier,
which must receive the proved claims, consume every byte, and reach the
prover's sponge state. The tampered-BlindFold test flips a byte of the
trailing BlindFold openings.
`ProtocolSponge` replaces `ProtocolTranscript`, `ProofTranscript`, and
`VerifierTranscript`. The `transcript-*` features now choose the sponge
(Blake2b512 by default, Keccak, or Poseidon) passed to the prover and
verifier. jolt-eval verifies with the SDK's sponge, and its transcript
symmetry invariant drives the NARG prover/verifier pair.
Messages use jolt-field's codec traits instead of an `Atom` trait. The
spec now gives the actual API and framing, the proof header, Dory's
scout-and-check integration, the BlindFold replay through
`verify_stages`, the stage-level wire decisions, the SDK sponge
selection, and the API gaps found while porting Akita.
…ifier

The stage round trips no longer hand-copy each verifier stage body. A
single `verify_through` runs the production stage verifiers over the
prover's argument string, then requires EOF and the prover's sponge
state.

- The ZK stage tests now also verify (stages 4 and 6b previously had no
  replay).
- The field-inline e2e tamper tests overwrite the FieldRdInc commitment
  bytes with another well-formed commitment and require rejection.
- The ZK BlindFold-tamper case is dropped: on an argument string it is
  zk_e2e's trailing-byte flip.
… fuzzers

A fixed script over every message, challenge, and grinding kind pins the
argument string and challenge bytes for Blake2b512, Keccak, and Poseidon.
The bench and the determinism and framing-injectivity fuzzers run on the
NARG transcript.
Akita runs on the caller's jolt-transcript, so the Jolt packed path
hands its live transcript to Akita's `batched_prove`/`batched_verify`.
Akita's messages land in Jolt's argument string, and the only `finish`
is Jolt's. The bridge that squeezed a Jolt challenge to seed a separate
Akita session is deleted, along with `AkitaBatchProof`.

- Before Akita's own messages, Jolt absorbs the setup's schedule catalog
  digest and sends the selected schedule row. Akita's root absorbs its
  descriptor, the group commitments, points, and evaluations itself.
- Akita commitments travel as a 65-byte header plus exactly
  `coeff_len * 16` payload bytes, decoded with checked deserialization.
  The hiding commitment is a fixed 16-byte evaluation.
- The prover's packed stage 0 mirrors the verifier's seeding: header,
  public preamble, commitments, then the public commitments.
- `AkitaScheduleLookupKey` follows Akita's rename to `ScheduleLookupKey`.
- The root `[patch]` redirects jolt-transcript to the workspace member,
  so Akita and Jolt share one transcript crate.

A verifier setup that differs only in `default_layout_digest` now
accepts a direct single-group opening: that digest is not an input of
such an opening.
`jolt_verifier::sites` names the protocol regions: the preamble, the
commitments, each stage, and BlindFold. The prover and verifier set the
same site on entry to each region, so under the new `logging` feature
every transcript event is attributed to its region. Sites never reach
the sponge.

This retires the `fs-audit` thread-local scopes: the `fs_scope`
attribute, the `fs_scope_guard!` macro, and `fs_audit` are deleted.
The attacker now controls the argument-string bytes directly.

- Sumcheck: the targets feed fuzzer bytes as compressed or full rounds,
  corrupt honest proofs, and differentially check compressed
  verification. Each run ends with an EOF check.
- Dory: the targets mutate honest transparent and ZK openings byte-wise
  (flip, truncate, append, negate a group element, swap or delete an
  element-width range) and require rejection. They also check that
  neither mode's verifier accepts the other mode's opening.
- Seeds are regenerated for the new encodings.
The stock prover and verifier run on Keccak by type argument alone, and
the sponge identity separates proofs: a Keccak proof does not verify
under Blake2b.
…sites

The Fiat-Shamir source census follows the NARG API. It inventories
public absorbs, prover messages, challenges, transcript delegations,
site tags, and the schema of the proof's message and public-statement
types, so moving any transcript operation needs a reviewed inventory
update. It scans source only and runs without a feature; regenerate
with JOLT_FS_BLESS=1.
Draw tests compare the production draw against the documented draw
sequence on identical transcripts (values and sponge fingerprint)
instead of recording doubles. Order tests assert the production order
functions directly. Tests of removed validators, proof-consistency
checks, and the stage-4 domain separator are deleted.
…argument strings

The fixture cache stores NARG proofs (magic JVCF0006). The ZK
completeness test pins the BlindFold protocol shape replayed from the
argument string. The tamper fuzzers mutate honest argument strings and
public statements, the deserialize and input-validation fuzzers decode
arbitrary bytes as proofs and headers, and the fuzz fixtures are
regenerated. The tamper, ZK-audit, and FS support modules are disabled
until their suites are ported.
The live Fiat-Shamir attacks run the stock verifier on an `AuditSponge`
that records the honest squeeze stream and replays it frozen. Each
attack (a preprocessing-digest flip, a stage-1 sumcheck equivocation, a
cancelling final-opening commitment pair) must verify under frozen
challenges and reject under live ones. The attacks edit
argument-string bytes, which they locate through the `logging` event
log. CI and the testing guide use `logging` in place of `fs-audit`.
…tion deadline

The typed tamper manifest is replaced by the transcript event log.

- **Sweep:** for every recorded prover message (every message, or a
  capped deterministic sample for the larger fixtures) a byte flip must
  be rejected, and the rejection must land by the message's deadline
  region:
  - clear builds: the region of the next challenge;
  - ZK builds: stage 8 or BlindFold.
- **Coverage:** message ranges must tile the argument string.
- **Structural tampers:** truncation, trailing bytes, an empty proof,
  and a protocol mismatch reject with their typed errors.
- **Statement, header, commitment-substitution, and committed-program
  tampers** carry over the old typed suites' intent.
- **ZK independence:** two ZK proofs of one statement differ in every
  hiding message, and their message bytes pass uniformity checks.

The ignored placeholder tests are deleted. CI runs the verifier suites
with `logging`.
`Transcript`, `AppendToTranscript`, the label helpers, `DigestTranscript`,
`SpongeTranscript`, the `Blake2bTranscript`/`KeccakTranscript`/
`PoseidonTranscript`/`LegacyBlake2bTranscript` aliases, and the spongefish
setup helpers have no consumers left. The crate now exposes only the
NARG channel. The `transcript-*` features now just select which sponges
are compiled in, and the blake2, digest, and rand dependencies are
dropped. The legacy test suites go with it; the NARG and known-answer
suites remain.
…permutation

PoseidonSponge emitted the 32 little-endian bytes of a field element, whose
top byte is at most 0x30. A challenge drawn at a squeeze offset that is not
32-byte aligned then saw biased bytes, costing about 2.4 bits per challenge,
while the transcript promised exact challenges. Each permutation now yields
its low 16 bytes, within 2^-125 of uniform. Poseidon proofs change; the
Poseidon known answers are regenerated.
…eads

- receive_bytes compares every block it returns with the block spongefish
  read and absorbed, so a cursor drift (an atom decoding other than its
  NUM_BYTES) fails as CursorMismatch instead of handing out unbound bytes,
  including to Dory's parse-ahead.
- receive_nonce range-checks with checked_shr, and a proof that ends
  inside a nonce reports Truncated.
- SMALL_CHALLENGE_BYTES, PROTOCOL_ID_LEN, and GRINDING_SEED_LEN are
  crate-private: nothing outside the crate uses them.
- A test pins that an exact challenge rejects an out-of-range candidate and
  resamples from the next squeeze.
- Nonce docs name the LEB128 Nonce message, not a u32.
The prover sent the six u8 widths as six 1-byte messages and the verifier
read one 6-byte message. Byte sponges absorb those identically, but a
length-binding sponge such as Poseidon does not, so every honest Poseidon
proof was rejected. Both sides now exchange one [u8; 6].
CommittedOutputClaimShape documents and debug-checks its nonzero row length.
arkworks decodes any compressed G1/G2 bytes with the infinity flag as the
identity whatever their x bits, so an identity commitment had many accepted
encodings: byte-level proof malleability, though spongefish absorbs the
re-encoding and the challenges do not move. Decoding now requires the
input to equal the point's canonical encoding.
The folded evaluation outputs and blindings, and the outer sumcheck's
Az/Bz/Cz with the error-row opening, were sent in prove.rs, read in
verify.rs, and restated by the test that builds argument strings. They are
now FoldedEvaluations and OuterClaims with send and receive, used by all
three. The Fiat-Shamir inventory is re-blessed for the moved sites and the
single header message.
…ponge

The audit sponge taped every Blake2b it built, so a seeded fork attached to
the session tape at offset zero, and forks built on Akita's worker threads
panicked outside the session: the Akita fs_attacks leg failed. A fork is a
function of a taped seed and a proof counter, so freezing the transcript
already freezes it; a sponge whose first absorb is the fork tag now runs
plain Blake2b on any thread. Transcripts are not Clone, so the clone and
scout bookkeeping goes too.
jolt-verifier's JoltSponge and the SDK's ProtocolSponge each picked the
sponge. JoltSponge now selects Poseidon, then Keccak, then Blake2b by
jolt-verifier feature, with an explicit precedence, and builds Blake2b
unconditionally so every feature set has a default; ProtocolSponge is that
alias and the SDK's transcript features forward to jolt-verifier.
The check covered only the root workspace; the fuzz harnesses and the
external sumcheck consumer resolve their own graphs. It now checks every
committed workspace whose lockfile contains spongefish and reports a failed
cargo tree instead of hiding it.
- The spongefish-state spec is implemented. It describes the LEB128 nonce,
  typed TranscriptError reporting, the empty instance every transcript
  passes, block-chunked byte messages, and Poseidon's 16-byte squeezes, and
  moves transcript-bound prover randomness, which nothing implements, from
  the invariants to the non-goals. The narg spec is marked amended where it
  still specifies previews.
- WARNINGs on the RAM output-check and booleanity draws name the draws the
  code makes; comments no longer cite challenge_scalar.
- The Dory parse-ahead carries a WARNING that it restates dory-pcs's message
  order, and verify_zk's ordering contract names stage 4's staged openings
  as the exception and why they stay sound.
- CLAUDE.md and the sumcheck README describe the sponge-parameterized
  transcript API.
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📏 PR diff

Note

Actual code changed: 15,368 lines

🟢 +6,118 added    🔴 −9,250 removed

Tests, docs, fixtures, and helper scripts excluded.

Category 🟢 Added 🔴 Removed
⌨️ Actual code +6,118 −9,250
🧪 Tests +9,391 −15,992
📚 Docs +2,061 −1,266
📦 Fixtures +2,477 −776
🔧 Helper scripts +79 −40
Total diff +20,126 −27,324

Every changed line is classified once.

Pins every Akita git dependency to LayerZero-Labs/akita 7109b3ea, the
Akita head that runs on this branch's transcript and pins Jolt 9d2eb0f,
and regenerates the lockfile. The root patch keeps Akita's jolt-* crates
on this workspace.
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark comparison (crates)

Same-runner A/B: base_run = 451daca (merge target) vs pr_run = aa5a8d6 (PR head merged onto it). Bold = faster run; Δ colored when the difference exceeds the ±10% noise threshold.

2 of 63 benchmarks differ by more than ±10%:

Benchmark base_run (merge target) pr_run (this PR) Δ
g1_serialize_bincode 104.7±1.73ns 120.9±5.62ns ${\color{red}\mathbf{+15.4\%}}$
field arithmetic/add/fp32_c99 3.2±0.03µs 2.8±0.04µs ${\color{green}\mathbf{-13.1\%}}$
All 63 benchmarks
Benchmark base_run (merge target) pr_run (this PR) Δ
EqPolynomial::evals/17 1.8±0.03ms 1.8±0.02ms +0.3%
EqPolynomial::evals/19 7.0±0.06ms 7.2±0.10ms +2.7%
EqPolynomial::evals/20 14.8±0.17ms 14.5±0.12ms -2.0%
EqPolynomial::evals/22 55.1±0.63ms 55.3±1.83ms +0.5%
EqPolynomial::evaluations/14 506.2±19.59µs 500.1±10.91µs -1.2%
EqPolynomial::evaluations/18 6.4±0.09ms 6.5±0.07ms +1.0%
EqPolynomial::evaluations/20 31.2±0.31ms 31.0±0.21ms -0.6%
F128/mul 8.4±0.23ns 8.4±0.07ns -0.4%
F128/square 6.9±0.07ns 6.9±0.11ns +0.0%
F192/mul 12.9±0.26ns 12.9±0.19ns +0.1%
F192/square 13.8±0.32ns 13.9±0.34ns +1.2%
F64/mul 2.7±0.03ns 2.7±0.06ns -3.4%
F64/square 2.6±0.02ns 2.7±0.02ns +0.1%
Polynomial::bind/14 136.7±11.38µs 137.4±11.27µs +0.5%
Polynomial::bind/18 1.8±0.02ms 1.8±0.02ms -0.5%
Polynomial::bind/20 7.2±0.04ms 7.2±0.04ms -0.1%
Polynomial::evaluate/20 42.4±0.32ms 43.2±0.49ms +1.7%
UnivariatePoly::from_evals/16 43.8±0.99µs 43.8±0.37µs +0.0%
UnivariatePoly::from_evals/3 5.3±0.07µs 5.3±0.10µs +0.2%
UnivariatePoly::from_evals/4 8.0±0.07µs 8.0±0.16µs +0.4%
UnivariatePoly::from_evals/8 19.6±0.38µs 19.6±0.18µs +0.2%
field arithmetic/add/fp128_a7f7 4.4±0.04µs 4.4±0.09µs +1.1%
field arithmetic/add/fp32_c99 3.2±0.03µs 2.8±0.04µs ${\color{green}\mathbf{-13.1\%}}$
field arithmetic/add/fp64_c59 2.9±0.04µs 2.9±0.07µs +1.2%
field arithmetic/mul/fp128_a7f7 14.6±0.23µs 14.7±0.27µs +0.7%
field arithmetic/mul/fp32_c99 5.0±0.02µs 4.9±0.09µs -1.4%
field arithmetic/mul/fp64_c59 7.6±0.11µs 7.6±0.05µs -0.2%
field arithmetic/square/fp128_a7f7 18.2±0.23µs 18.2±0.30µs +0.3%
field arithmetic/square/fp32_c99 5.3±0.05µs 5.3±0.09µs +0.2%
field arithmetic/square/fp64_c59 6.3±0.14µs 6.4±0.14µs +1.0%
field arithmetic/sub/fp128_a7f7 4.0±0.10µs 4.0±0.10µs +0.1%
field arithmetic/sub/fp32_c99 2.4±0.04µs 2.1±0.02µs -9.8%
field arithmetic/sub/fp64_c59 2.5±0.02µs 2.5±0.01µs -0.1%
fp64_prime63_offset259/ext2_packed_mul 19.4±0.56ns 19.4±0.27ns -0.1%
fp64_prime63_offset259/ext2_scalar_mul 8.1±0.12ns 8.1±0.12ns +0.1%
fp64_prime63_offset259/packed_mul 7.8±0.19ns 7.8±0.16ns -0.1%
fp64_prime63_offset259/scalar_mul 1.9±0.03ns 1.9±0.04ns +0.3%
g1_add 378.5±11.02ns 378.2±17.00ns -0.1%
g1_deserialize_bincode 8.0±0.05µs 8.2±0.10µs +1.7%
g1_double 182.9±1.12ns 185.2±5.31ns +1.2%
g1_msm/1024 11.9±0.06ms 11.7±0.05ms -1.1%
g1_msm/16 512.5±9.89µs 513.9±7.76µs +0.3%
g1_msm/256 4.0±0.01ms 3.9±0.04ms -1.1%
g1_msm/4 230.3±0.91µs 231.8±11.80µs +0.6%
g1_msm/64 1.5±0.04ms 1.4±0.02ms -0.9%
g1_scalar_mul 58.3±0.98µs 58.0±0.37µs -0.5%
g1_serialize_bincode 104.7±1.73ns 120.9±5.62ns ${\color{red}\mathbf{+15.4\%}}$
g2_msm/16 1.9±0.03ms 1.8±0.01ms -0.4%
g2_msm/256 14.9±0.12ms 14.9±0.73ms +0.5%
g2_msm/4 762.0±19.93µs 759.6±8.67µs -0.3%
g2_msm/64 5.4±0.04ms 5.4±0.03ms -0.2%
g2_scalar_mul 308.1±8.68µs 307.8±3.14µs -0.1%
gt_scalar_mul 889.3±12.40µs 893.2±13.76µs +0.4%
multi_pairing/16 5.7±0.04ms 5.6±0.03ms -0.3%
multi_pairing/2 1.2±0.01ms 1.3±0.03ms +0.4%
multi_pairing/4 1.8±0.01ms 1.8±0.02ms +0.5%
multi_pairing/8 3.1±0.02ms 3.1±0.01ms +0.1%
pairing 938.3±4.92µs 941.3±27.16µs +0.3%
pedersen_commit/1024 12.0±0.03ms 11.8±0.04ms -1.0%
pedersen_commit/16 585.7±16.05µs 585.3±8.42µs -0.1%
pedersen_commit/256 4.0±0.03ms 4.0±0.08ms -0.5%
pedersen_commit/4 291.9±12.47µs 292.3±9.80µs +0.1%
pedersen_commit/64 1.5±0.01ms 1.5±0.02ms -0.7%

24 benchmark(s) present in only one run: append_bytes/Blake2b/256B, append_bytes/Blake2b/32B, append_bytes/Keccak/256B, append_bytes/Keccak/32B, append_bytes/Poseidon/256B, append_bytes/Poseidon/32B, challenge/Blake2b, challenge/Blake2b/small, challenge/Blake2b/uniform, challenge/Keccak, …

Akita 10a13def drops akita-types' unused dependencies and gives wasm a
getrandom backend; the code Jolt builds is unchanged.
read_proof parses the Dory proof from the unread bytes before the live
transcript receives them, and reported running out of bytes as a failed
verification. The message sweeps require every cut to be rejected as
truncated, as the live transcript reports on the same bytes; a malformed
element still fails verification.
…endencies

- The fuzzing-only evaluate_mapped_expression names JoltOpeningId, which
  only that cfg imported nowhere.
- The WASM verifier template names the SDK's ProtocolSponge instead of the
  removed VerifierTranscript.
- jolt-verifier (ark-serialize), jolt-sumcheck (bincode), jolt-blindfold
  (serde), and jolt-sdk (jolt-transcript) no longer use those dependencies
  since proofs became argument strings (cargo machete); the SDK's
  transcript features forward to jolt-verifier alone.
Lines this branch adds now follow the machine-checked import rule
(scripts/check_style_invariants.py): types, traits, and constants are
imported and named by their short name, with imports gated on the same
features as their uses, and fmt::Result aliased where it would shadow
Result. The external-consumer and SDK manifests are taplo-formatted.
The checked-in bundles hold proofs from before this branch's transcript
changes, so the fuzz replay rejected them as honest fixtures. Regenerated
with the documented generate_fuzz_fixture runs, which verify each bundle
through the fuzz target's own decoding.
…ng sponges

spongefish's Keccak duplex and the byte-level Poseidon sponge squeeze by
reading their rate without permuting, so an absorb after squeezing one
byte or two leaves the same state. Exact challenges rejection-sample a
variable number of bytes, so on those sponges distinct retry histories
could reconverge at the next absorb. Sponge gains
ABSORB_FORGETS_SQUEEZE_WIDTH, and on such sponges every exact challenge
absorbs the width it squeezed as a public u64. Blake2b retains the width
and is unchanged.

A test pins each sponge's flag to its real absorb-after-squeeze behavior;
the Keccak and Poseidon known answers are refrozen. The spec documents
the binding and no longer names downstream consumers outside this
repository.
Channel::record_fork_draw logs a ForkAbsorb and a ForkSqueeze event at
the current site without touching the sponge. A prover records only the
accepted counter's draws after its search, in the order the verifier
redraws them, so off-transcript fork draws show up in both roles' event
logs and the logs still compare.
Exact challenges draw u64 words, which the squeeze helper split into
single-byte squeezes. A word-sized arm between the 32-byte blocks and
the bytes cuts those calls; every sponge's output stream is independent
of how squeezes are split, so proofs are unchanged and the known
answers still hold. The spec records the verifier cost of
squeeze-then-grind.
markosg04 added a commit to LayerZero-Labs/akita that referenced this pull request Oct 13, 2026
Picks up the exact-challenge squeeze-width binding on Keccak and
Poseidon, Channel::record_fork_draw, and word-sized squeezes.
Ports main onto the argument-string transcript: the native Akita trace
batching (#2012), backends bound to execution contexts (#2028), unified
trace rows (#1996), whole-bytecode commitments (bytecode chunking
removal), the binary fields and their sum-check (#2034 and the binary
backend), and the clear proof-type cleanup, which this branch already
subsumes. Binary fields gain the spongefish codecs every transcript atom
carries, and main's new tests are rewritten for argument strings.

Akita's runtime witness-chunking policy (LayerZero-Labs/akita#246, now
in akita#185) moves chunking out of the commitment config, so each Jolt
catalog family fixes its own through `CatalogChunking`, and the schedule
generator preplans the direct rows below the setup-offloading cutover.
LayerZero-Labs/akita#185 at a4978d59 carries Akita main, including its
runtime witness-chunking policy, and pins this branch's merge commit.
…trip

Checked decoding moved to CanonicalDecode on this branch; main's
field-inline limb test called it through CanonicalEncoding.
@quangvdao

Copy link
Copy Markdown
Collaborator

Thanks, Markos. This PR is now superseded by the transcript stack below, which moves Jolt onto spongefish 0.8.0's ProverState/VerifierState in reviewable steps:

One design difference: grinding searches nonces on a copy of the prover's sponge, so the prover alone needs spongefish's yolocrypto feature, while the verifier needs no feature and no extra permutation per grind. A squeeze-then-grind fork adds about two permutations per grind to the verifier.

Since this branch conflicts with main and the stack covers the same ground, I suggest closing it. If there's a piece the stack misses, please point to it and we'll carry it over.

@quangvdao quangvdao closed this Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

implementation PR contains implementation of a spec spec Tracking issue for a feature spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants