Skip to content

feat(akita): run Akita's opening inside Jolt's Fiat-Shamir sponge (J8) - #2111

Draft
quangvdao wants to merge 36 commits into
transcript/j7-sponge-portfrom
transcript/j8-akita-embedded
Draft

quangvdao wants to merge 36 commits into
transcript/j7-sponge-portfrom
transcript/j8-akita-embedded

Conversation

@quangvdao

@quangvdao quangvdao commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked, not mergeable yet.

What this does

Akita's opening now runs inside Jolt's single Fiat-Shamir sponge, so each message is absorbed once. Before, Jolt absorbed Akita's setup and statement into its own transcript. It then started a second Akita transcript, which absorbed them again.

  • Stage 0 (unchanged from main). For each Akita commitment, Jolt absorbs:

    • the commitment's metadata: label, flavor, layout digest, and the counts of variables, polynomials, one-hot K and coefficients;
    • the committed rows, length-prefixed.

    The wire is main's AkitaCommitment; this PR does not change it.

  • Stage 8.

    • The prover calls Akita's batched_prove_embedded on Jolt's prover state.
    • The verifier calls batched_verify_embedded on Jolt's verifier state, which reads Akita's messages out of Jolt's argument string.
    • Akita absorbs, in order:
      1. its instance descriptor: algebra, setup seed and decomposition, the selected schedule row and its schedule, the grinding plan, and the opening layout;
      2. each commitment's digest;
      3. the points;
      4. the evaluations.
  • Decoding. Before decoding a commitment's rows, the verifier runs feat(akita)!: NIST category 3 Module-SIS and 2^31 schedule coverage #2107's shape_guard. The guard resolves the selected schedule row from the trusted catalog. It then checks the declared coefficient count and the byte length against that row's profile (outer_compression_plan().terminal_coefficients()). So a forged header cannot make the decoder allocate a payload-sized buffer.

  • End of proof. verify ends with PCS::check_eof, which rejects trailing argument-string bytes. The trace batch's proof must carry an empty backend_proof, because Akita's argument now lives in Jolt's argument string.

The stage-8 API is a new trait, jolt_openings::GroupBatchOpening, with Sponge, PendingEof, prove_batch, verify_batch and check_eof.

Removed

  • bridged_akita_session: the second Akita transcript and its session handshake.
  • append_verifier_setup: setup dimensions, chunk profile, layout and catalog digest absorbed into Jolt's transcript.
  • append_batch_statement and the bind_*_statement_transcripts helpers: the statement absorbed twice.

The standalone path

The generic BatchOpeningScheme path and the single-polynomial path still run Akita on a fresh Akita session. Only tests, benches and the ZK stub use them. The session label (standalone_session) binds what Akita's own descriptor does not:

  • two challenges squeezed from the caller's transcript, which tie the opening to the caller's state;
  • the commitment's layout digest and the setup's default layout digest;
  • the trusted catalog's digest.

All parts have fixed lengths. The embedded stage-8 path does not use this label.

The regression in the earlier head, and the fix

The earlier head of this PR (933024d97) had three problems.

  • Wire and decoding. It changed the commitment wire to carry the group's whole canonical bytes, profile included. Commitments grew from 166 to 510 bytes. It also deleted shape_guard, so Akita's self-describing decoder ran before any trusted-length check. A forged header could make the verifier reserve up to about 1 GiB before rejecting. That reaches the production verifier, at stage 8.
  • Standalone path. Removing bridged_akita_session left the standalone session without the caller's transcript state and Jolt's layout and catalog digests. J7's catalog-replay and opening-layout tests caught this. The production verifier was not affected: it only uses the embedded path, where Akita's descriptor binds the selected row and the layout, and stage 0 binds the commitments.
  • Weakened tests. Instead of fixing the cause, several tests were loosened:
    • the forged-metadata pin test was removed;
    • two FS replay tests were made to accept a rejection at stage 8;
    • the serde transport test lost its setup-prefix check.

The fix:

  • Wire and guard. Main's commitment wire is back. The verifier runs feat(akita)!: NIST category 3 Module-SIS and 2^31 schedule coverage #2107's shape_guard before decoding, on both the embedded and the standalone path.
  • Standalone session. standalone_session binds the caller state and the digests.
  • Tests. Every weakened test is restored to its base form. The FS replay harness now replays absorbed messages too (below), so the frozen verifier stays honest through stage 8, and frozen.output.is_ok() holds again.

Sizes

Bincode sizes of the two Akita fixture proofs. "main" is a16z/jolt main (5aa807b03). "#2107" is its head (adb9a79c8), which adds Akita #232's category-3 SIS policy. "J8" is this PR. J8's opening counts the 33-byte joint_opening_proof plus Akita's bytes in Jolt's argument string.

main #2107 J8 J8 − #2107
muldiv commitments 166 296 296 0
muldiv stages 11,677 11,677 11,942 +265
muldiv opening 65,321 74,946 74,935 −11
muldiv total 82,006 91,761 92,019 +258
advice commitments 166 296 296 0
advice stages 11,677 11,677 11,945 +268
advice opening 69,699 80,719 80,728 +9
advice total 86,582 97,862 98,143 +281

Can the verifier derive the commitment metadata?

Each field, and where the verifier could get it instead of reading it from the commitment:

  • num_vars: from the opening point's length.
  • poly_count: from the number of claims in the statement.
  • one_hot_k: from the verifier setup.
  • backend_coeff_len: from the trusted profile of the selected schedule row (outer_compression_plan).
  • layout_digest: for the trace group, from setup.default_layout_digest. Advice and program commitments choose their layout when they are committed, so the verifier cannot derive their digest without a per-object source.

This PR does not change these fields.

Tests

Restored to base

  • akita_forged_commitment_metadata_rejects_before_shape_backed_allocation (tests/pathologies.rs).
  • The fs_attacks replay tests with frozen.output.is_ok().
  • The serde-transport setup-prefix assertion (scheme.rs).
  • J7's catalog-replay and direct-opening-layout tests, including both expect_errs.
  • The preprocessing digest domains and goldens.

New

  • akita_argument_string_tampers_reject. It checks that an appended argument-string byte gives NargNotConsumed, and that a flipped byte or a truncated string rejects.
  • standalone_session_binds_jolt_metadata_and_the_caller_state. It checks that the session changes with the caller's transcript, either layout digest, and the flavor.
  • The streamed trace-batch test now also checks that each of these rejects:
    • a verifier setup with a different max_num_vars, polynomial bound, default layout digest or one-hot K;
    • one appended argument byte;
    • a forged backend_coeff_len (rejected by the shape guard, with the message matched);
    • a proof that carries a non-empty backend_proof.
  • Sponge agreement. Both embedded batch tests check that the prover's and verifier's sponges agree after the opening.

FS harness

  • Script::absorb_bytes lets the test-only ScriptedTranscript substitute absorbed bytes. The audit harness records every absorbed message and replays it, so a frozen verifier's sponge matches the honest one.
  • The FS scanner records the calls where Akita takes the transcript (batched_verify_embedded, verify_batch, check_eof).
  • The inventories are re-blessed.

Test-diff audit

Every assertion removed, loosened or rewritten against J7 and main:

  • setup_key_transcript_binds_backend_shape (removed). It checked that Jolt's transcript absorbed the backend setup shape, and append_verifier_setup is gone. Its replacements:
    • Embedded path. Akita's descriptor binds the setup seed and the selected schedule. The trace batch must match the setup's shape, layout digest and K exactly. The setup-mismatch loop in the streamed test pins each of these.
    • Standalone path. standalone_session binds the layout and catalog digests. max_num_vars and one-hot K are checked for exact equality.
  • J7's squeeze-equality checks after verify (rewritten). These are back, with the squeeze taken just before check_eof.
  • akita_field_inline_e2e: flip a backend_proof byte → flip an argument-string byte (rewritten). backend_proof is now always empty, and the new streamed check covers a non-empty one.
  • Inventory entries for deleted functions (removed). The new hand-off entries replace them.
  • The replay harness does not require every recorded absorb to be replayed. This is intentional: attack replays stop when the verifier rejects.
  • feat(akita)!: NIST category 3 Module-SIS and 2^31 schedule coverage #2107's commits. Nothing removed or loosened. Counts re-blessed after the catalog regeneration.

Soundness re-check

A read-only review of this PR's diff found all seven questions sound:

  1. The standalone path binds the caller state, the commitment, the layout and catalog digests, the point and the evaluations.
  2. The embedded stage-8 path absorbs nothing beyond Akita's own statement. It rejects trailing bytes and a non-empty backend proof.
  3. On both paths, every commitment is decoded only after the trusted-shape check. No new verifier-reachable panic or unbounded allocation.
  4. Stage 0 is unchanged: one shared function, with fixed-width or length-prefixed parts.
  5. The absorbed-byte substitution exists only under test-utils, and its identity encoding absorbs exactly the original message.
  6. A catalog holds one row per layout and shape, so the selection is unambiguous.
  7. Grinding is unchanged and stays per round.

Two notes:

  • The bridge was one field element (about 127 bits). That allowed a caller-state collision in about 2^64 work. It is now two field elements.
  • The standalone opening no longer absorbs the statement into the caller's transcript. The BatchOpeningScheme docs already leave binding the statement to the caller.

Validation

All on a Linux sandbox with CI's commands (cargo nextest run --cargo-profile ci); every run passed:

  • -p jolt-transcript, and -p jolt-transcript --all-features
  • -p jolt-akita, with and without field-inline
  • -p jolt-verifier --test fs_attacks, with akita,fs-audit,prover-fixtures and with fs-audit,prover-fixtures
  • -p jolt-verifier --test fs_obligations, with fs-audit and with fs-audit,akita
  • -p jolt-verifier --features akita,prover-fixtures --test-threads 1 (84 tests)
  • -p jolt-prover --features akita,prover-fixtures (40 tests)
  • -p jolt-prover --features prover-fixtures,field-inline,akita, including akita_field_inline_e2e (37 tests)
  • -p jolt-verifier --features field-inline,akita, and --features prover-fixtures,field-inline,akita --test-threads 1
  • Dory: -p jolt-verifier --features prover-fixtures --test-threads 1 (140 tests)

Clippy passes for jolt-transcript (all features, and bn254,solinas,binary,prover-grinding), jolt-verifier and jolt-prover with akita,prover-fixtures, jolt-verifier with fs-audit,akita,prover-fixtures, and CI's field-inline crate set with -D warnings. cargo fmt, taplo fmt and check_style_invariants.py pass.

Final round at this head (ed90d31d7, Akita pin e9d85d1f7), all passing:

  • cargo nextest run --cargo-profile ci -p jolt-akita (95 tests)
  • -p jolt-verifier --test fs_obligations, with fs-audit and with fs-audit,akita
  • --cargo-profile ci -p jolt-verifier --test fs_attacks --features akita,fs-audit,prover-fixtures
  • cargo clippy -p jolt-verifier --all-targets --features akita,prover-fixtures, and cargo clippy -p jolt-akita --all-targets -- -D warnings

The runs above the final round were on the previous commit, which differs only in the Akita pin (062106ee3, before the merge of #253's head) and the re-blessed inventories.

Follow-ups

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📏 PR diff

Note

Actual code changed: 615 lines

🟢 +310 added    🔴 −305 removed

Tests, docs, fixtures, and helper scripts excluded.

Category 🟢 Added 🔴 Removed
⌨️ Actual code +310 −305
🧪 Tests +303 −172
📚 Docs +70 −37
📦 Fixtures +67 −258
🔧 Helper scripts 0 0
Total diff +750 −772

Every changed line is classified once.

…cript

Bind the full statement as the spongefish instance, absorb commitments as
labeled messages, carry the NARG string in JoltProof and check its end, and
move per-round grinding onto the shared SpongePow core.
The fs-audit transcript is the production verifier state with a recording
script that also replays proof-of-work check bytes, so frozen replays over
changed inputs model a verifier whose every squeeze is fixed.
… the spongefish transcript

Keccak is the protocol sponge, Blake2b512 behind transcript-blake2b; the
Poseidon protocol selection is removed until the transcript absorbs native Fr.
jolt-prover names Fr challenges, so its transcript dependency enables bn254
itself instead of relying on feature unification. Drop an unused import in
the field-inline Akita fixtures and the SDK, and avoid unwrap in the zk
statistical test.
- An unlabelled UnivariatePoly round absorbs as a labelled sum-check round
  with its coefficient count, so a longer polynomial cannot absorb like a
  shorter one.
- Document that every absorbed Encoding must decode uniquely, since the
  sponge absorbs bytes without framing.
- Correct the prover-grinding comment: crates holding both sides enable
  yolocrypto in verifier builds; it changes visibility, not bytes.
- List precommitted as unbound in the statement test, and check that every
  unbound edit changes the inputs.
- Fix two test ports: count challenge draws rather than PoW squeezes, and
  squeeze the prover's tail once in the grouped-capacity loop.
- Re-bless the absorb-site inventory.
Import types and traits by short name on lines this branch adds, as
check_style_invariants requires, apply rustfmt, and commit the lockfiles,
verifier fixtures and fuzz seeds regenerated for the new transcript bytes.
Rename the order test's label to a dictionary word of the same length, and
drop jolt-sumcheck's num-traits dev-dependency, whose only user was the
deleted mersenne61 compatibility test.
The proof-of-work nonce search copies the sponge state, which spongefish
0.8.0 exposes only under `yolocrypto`. jolt-sumcheck and jolt-blindfold
turned `prover-grinding` on in every build, so jolt-verifier compiled the
search and `yolocrypto` too.

- jolt-sumcheck and jolt-blindfold gain a `prover` feature holding their
  prove side: the round loop, `grind_round`, the recorders' rounds, the
  committed builder and BlindFold's `prove`. jolt-prover, jolt-kernels and
  jolt-spartan-prover enable it; jolt-verifier's graph has neither the
  search nor `yolocrypto`.
- jolt-transcript's default features are Keccak only. Blake2b and Poseidon
  are opt-in; the fuzz workspaces that use them, or BN254 challenges, now
  name those features.
- CI lints the verifier-only graph, checks jolt-verifier's feature tree,
  and tests jolt-sumcheck and jolt-blindfold with their prover.
…ge-6b draw site

Stage 6b now draws through J6's one `sequence`, so its three further
`challenge` sites fold into one. Also rustfmt the stage-6b draw test.
sumchecker and others added 12 commits October 21, 2026 14:19
Pin all Akita crates to the LayerZero-Labs/akita#232 head, which raises the
Module-SIS gate from 128 bits to an ADPS16 quantum Core-SVP score of at least
169 bits (NIST category 3) and doubles the compression ladder's terminal
image. It also carries Akita main through #189.

The commitment shape guard now derives the expected payload length from
GroupCommitPhaseParams::outer_compression_plan, Akita's single owner of the
B-image compression chain and its ladder admission rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 23f1cd2)
K=16 trace keys now cover log_T 12..=31 (arities 16..=35). The admitted
one- and two-polynomial scalar range reaches arity 44 for both K values, and
the dense producer grid reaches 35. The K=256 Single catalog also carries the
narrowest and widest native traces, derived from the same RV64 width
enumeration as K=16, at 2^30 and 2^31.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 74c695a)
…licy

gen_jolt_schedules at the pinned Akita revision replans every base and
fixture catalog under the 169-bit SIS policy and the 2^31 coverage grid.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 76192a0)
Extend the trace-geometry and field-inline coverage tests to 2^31, and add
an ignored check that plans every Jolt-scale key of the generator's family
specs and reports levels, estimated proof bytes, and setup capacity.

The cutover test now states the rule for every emitted trace key in every
packaged profile: rows below the cutover are direct, and Single rows at or
above it offload. The regenerated one-column Single rows at arities 16 and 17
terminate immediately and the 25-variable one-column row stays direct, so the
grouped capacity and serde-transport fixtures move to shapes that still model
their cases (18 and the 51-column cutover trace), and the small-arity
rejection test pins the new 12..=17 range.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d434e02)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5a46b84)
LayerZero-Labs/akita#232 merged as dfdbd767 on Akita main. Pin every Akita
crate to that commit instead of the PR head.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit df72e56)
K=256 catalogs are fixtures only, so drop the 2^30/2^31 K=256 trace rows,
which no test or fixture consumed, and keep the admitted scalar ceilings at
40 and 43: the 2^31 trace arities (35 for K=16, 39 for K=256) already fit,
and nothing uses wider scalar rows. Remove the ignored Jolt-scale planning
diagnostic; gen_jolt_schedules --check already replans every key and compares
the artifacts byte for byte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c862f4a)
DIRECT_PROGRAM_MAX_PHYSICAL_VARS still rejects 35-variable committed
programs; the new dense rows do not change that admission guard. Also
drop an arithmetic-only fixture comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit adb9a79)
Stage 0 absorbs each Akita commitment's metadata and its group's 32-byte
digest. Stage 8 runs Akita's embedded prover and verifier on Jolt's own
transcript state, and verification ends with Akita's end-of-proof check.

This removes the second Akita transcript (bridged_akita_session), the
setup absorb (append_verifier_setup), the duplicate statement absorbs and
shape_guard.rs. The commitment wire now carries the group's canonical bytes.

Other changes:
- pin Akita to LayerZero-Labs/akita#254 (bbba19bf4)
- add jolt-openings::GroupBatchOpening for the stage-8 batch API
- move the Akita program-preprocessing digest domains to v2 and re-pin
  the goldens
- re-bless the FS inventories; the scanner records transcript hand-offs
- add a full-verifier argument-string tamper test
- update the jolt-akita scheme tests to the new binding
- refresh the fuzz lockfiles
…d Jolt metadata

The standalone BatchOpeningScheme and CommitmentScheme paths ran Akita on a
fixed session, so a valid opening verified under a different caller
transcript, setup default layout, commitment layout digest or catalog. The
session label now binds one challenge from the caller's transcript, both
layout digests and the trusted catalog digest. J7's catalog-replay and
direct-opening rejection tests are restored unchanged. The embedded stage-8
path is unaffected.

Two tests that CI's fail-fast hid also needed the regenerated catalogs: a
single one-hot polynomial now first gets a recursive child fold at 18
variables, so grouped_capacity uses an 18-variable trace with 24-variable
advice and the scalar-guide unit test pins the new boundary. The commitment
round-trip test compares re-encoded bytes, since the decoded ring vector is
in compact mode.
A frozen replay replaced every challenge draw but let the sponge absorb the
mutated messages. Akita's embedded opening draws from that sponge directly,
so a frozen verifier rejected at stage 8 and the two Akita replay tests had
to accept that rejection.

The audit script now records the bytes of every absorbed message and a
replay absorbs the recorded bytes instead, so the sponge stays the honest
one. Both Akita replay tests again require the frozen verifier to accept.
The embedded change replaced the commitment wire with the committed
group's self-describing bytes and dropped shape_guard. Decoding those bytes
read the row count from the prover's header and could reserve about 1 GiB
before any trusted check, and the wire grew by the group's profile.

Restore main's wire: Jolt's metadata, the coefficient count, and the
serialized rows. The profile stays off the wire; the verifier resolves it
from the trusted schedule row. Stage 0 absorbs the metadata and the rows, as
on main.

- restore shape_guard from #2107, adapted to Akita's fallible
  CommittedGroup::new; both stage-8 paths decode through it again
- restore AkitaProverHint { commitment, backend, source } and main's
  program-preprocessing digest domains and goldens
- restore akita_forged_commitment_metadata_rejects_before_shape_backed_allocation
- pin the same guard on the embedded trace batch, and pin that a verifier
  setup with a different shape, layout digest or K rejects the proof
- restore #2107's serde-transport test, including its setup-prefix check
#254's new head re-pins Akita's own jolt crates to J7's rebased head and
updates a spec; Akita's code is unchanged.
One field element of about 127 bits lets two caller states collide for
about 2^64 hash calls. Two elements put that at the 2^128 target.
…ace batch

Both embedded batch tests again check that the prover's and verifier's
sponges agree after the opening, squeezed before check_eof. The streamed
test also checks that a trace-batch proof carrying standalone backend
bytes is rejected.
Jolt's statement, preprocessing digest included, is absorbed when the
transcript starts, not through a message. Replays now pass it through the
script as well, so a frozen verifier's sponge stays honest from its first
absorb and Akita's embedded opening still draws honest challenges.
The scripted transcript now passes absorbed messages and the initial
statement through its script, and the standalone Akita session draws its
caller challenges with challenge_vector.
That head merges #253's latest commit, which adds a compile-time check
that Akita's transcript security target equals Jolt's.
J7 was rebuilt on main, which now carries #2107 and J6, so J7's old head
d04df1b is no longer its ancestor. This merge keeps J7's tree and
re-applies J8's own change on top: Akita's opening inside Jolt's sponge,
main's commitment wire behind #2107's shape guard, the standalone session
binding, and the FS replay freezing. The #2107 cherry-picks on this
branch are superseded by main's squash of #2107.

Dory keeps J7's label-based short challenges. The only lockfile conflict
was resolved by taking J7's lock and re-resolving the Akita pin.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants