Repository navigation
Conversation
Documentation blast radius (advisory)These regions may need doc/spec/book updates based on changed paths. Changed files in this PR: 80
|
PCS Profile Benchmark
13 of 13 profiles passed. Times are medians of Each sample verifies the same proof first with the configured multi-threaded pool and then with one thread. Both timings reuse the same verifier setup. Merge-base comparisons are available for Benchmark shards
Public opening statements
One-hot profiles generate deterministic witnesses with one Direct evaluates the public setup contribution during Stage 2. Recursive carries the same check through a Stage 3 setup-product sumcheck. Both modes execute the complete fold schedule and terminal verification. The chunked profiles Generated profiles may select different A, B, and D ring dimensions at different fold levels. The short profile names omit those dimensions. Each sample generates deterministic witnesses and opening points, prepares setup, commits, proves, serializes the proof, checks its size, prepares verifier setup, and verifies the claimed openings. It does not test malformed proofs. Phase time
Proof size and protocol shape
Grinding retries are rejected attempts at each fold, listed in measured-run order. Zero means the first sampled nonce was accepted. Each multi-group profile has two precommitted nv16 singleton polynomials and two polynomials in the displayed final group. Native proof accounting
Memory and setup size
Deltas are shown only for profiles with a matching merge-base case. Negative is smaller or faster. The uploaded |
CI test timing
Run summary
Wall time spans 2 parallel nextest slice shards. Slowest tests
Regressions vs main
New slow testsNo new tests ≥30s vs main baseline. |
b8d0b6d to
6b61cd1
Compare
…e message Spongefish absorbs are associative, so a group sent as one message gives the same argument string and challenges as one message per atom. An empty group makes no absorb call, matching the per-atom loop it replaces.
Add embedded prover and verifier entries that run on a borrowed Spongefish state; the standalone entries build a state and call them. Akita binds its own statement in both modes: bind_instance absorbs the length-prefixed protocol tag and descriptor, and the root absorbs a 32-byte CommitmentDigest per group, computed by CommittedGroup from its canonical bytes. Grinding borrows the state; the verifier returns a must-use PendingEof whose check_eof yields acceptance, and poisons the borrowed state on any error. Drop the WitnessCommitment and TerminalTFields re-absorbs of values already absorbed when sent. Protocol version 9; known answers regenerated for both sponges.
The proof-stream guardrail forbids Validate::No in proof-input crates, tests included.
J7 replaces ProverTranscript::field_challenges with FieldTranscript::challenge_vector, which yields the same bytes. Port the two channel challenge reads and move every jolt pin, including the fuzz and recursion-profile workspaces, to the J7 rev so Jolt can patch one shared jolt-field, jolt-poly and jolt-transcript identity.
de995f4 to
bbba19b
Compare
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
J7 (a16z/jolt#2110) was rebased, so 6691ec7cb is no longer on any ref. At d04df1bb5, jolt-transcript's default sponge is Keccak alone; no Akita crate relies on it, because every manifest sets default-features = false.
Keeps this branch's J7 pin (a16z/jolt@d04df1bb5) over #253's J6 pin and takes #253's compile-time check that TRANSCRIPT_SECURITY_BITS equals jolt_transcript::SECURITY_BITS.
a16z#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.
That head merges a16z#253's latest commit, which adds a compile-time check that Akita's transcript security target equals Jolt's.
Takes A2's Jolt pin (J6, efae45b3d) and its fuzz library. The fuzz targets now use this branch's single-label channel constructors.
J7 contains A2's J6 pin and adds the FieldTranscript surface this branch uses.
send_field, receive_field, send_extension and receive_extension are gone. Use one-element field groups and the extension exchange instead.
Versions move once per stack, counted from main. Akita main is at 7 and #253 already sets 8, so this branch's tags go back from v9 to v8. The four known answers that read the tags (transcript, two commitment digests and the sumcheck cutover) are regenerated on both sponges.
…at/embedded-transcript
Merge origin/feat/jolt-transcript-j3 into hashfix-emb, preserving embedded transcript binding and J7 integration. Resolve standalone and commitment session derivation with SHAKE128. Keep J7 pins and embedded known answers pending the separate dependency and fixture updates.
Use the J7 head with jolt-transcript SHAKE128 private coins and session identifiers. Refresh all three lockfiles minimally, remove the obsolete spongefish hash dependency, and align the Book source links with the pin.
Record the exact challenge, proof, and commitment digest bytes produced by SHAKE128 session derivation for both transcript sponges. Preserve all assertions and the embedded transcript binding; descriptor digests remain Blake2b-256.
Restack (latest). This branch now merges A2's head
e1df1d07a(#253) as an ordinary merge. A2 derives the session id with SHAKE128 instead of TurboSHAKE128. A2 pins Jolt to J6 (9470792dc), but this branch usesFieldTranscript, which only J7 has, so the Jolt pin is J7bed472d3d(a16z/jolt#2110), which contains J6's SHAKE128 fix. The proof channel isjolt_transcript::ProverState<TranscriptSponge>, whose private coins also come from SHAKE128. This is FIPS 202 for the primitives only: the Fiat-Shamir duplex itself (spongefish's Keccak-f[1600] sponge or Blake2b512) is not a FIPS-approved mode. Earlier restacks merged A2'sdac235470(thectutils0.4.4 bump for RUSTSEC-2026-0385). A2's fuzz library now uses this branch's one-label channel constructors and its field and extension groups.Version (latest). Versions move once per stack, counted from
main. Akitamainis at protocol 7 and #253 already sets 8, so this PR no longer bumps:PROTOCOL_VERSION,PROTOCOL_TAG,STANDALONE_SESSION_TAG,COMMITMENT_DIGEST_TAGandCONTEXT_DOMAINgo back from v9 to v8. Four known answers read those tags, and only they were regenerated, on both sponges:transcript_has_a_known_answer(akita-transcript): the channel starts fromSTANDALONE_SESSION_TAG, andbind_instanceabsorbsPROTOCOL_TAG.commitment_digest_has_a_known_answer(akita-transcript) and the one in akita-types: the digest is a session underCOMMITMENT_DIGEST_TAG.arithmetic_cutover_preserves_proof_and_challenges(akita-sumcheck): its channel starts fromSTANDALONE_SESSION_TAG.Every other test in akita-transcript, akita-sumcheck and akita-types passed unchanged with the new tags.
CONTEXT_DOMAINis only stored in diagnostic records and never absorbed.NOT MERGEABLE: stacked on #253, which pins a16z/jolt to an unmerged commit (J5 #2106, stacked on J3 #2099 and J2 #2097). Retarget to
mainafter #253 lands.Commits 7 and 9 re-pin the jolt crates from J5 (
7e8ce196) to J7 (a16z/jolt#2110). Jolt's J8 (a16z/jolt#2111) pins this branch, and both must share onejolt-transcript. Commit 7 pinned J7 at6691ec7cb. J7 was later rebased, so that commit is on no ref any more, and commit 9 moves the pin to J7's current headd04df1bb5. Both are build-only commits, so they can be dropped or moved to #253. Before this PR merges, the pin must move to whatever J7 commit lands on a16z/jolt main.What this does
Akita used to start its own Spongefish session for every proof. Jolt, its main caller, has a session of its own. To link the two, Jolt absorbed Akita's setup, statement and commitments, squeezed a bridge value, and passed that value to Akita as session bytes. Akita then absorbed the same statement and commitments again. Each root commitment went into a sponge three times, and the two transcripts were linked only through one squeezed value.
This PR lets a caller run Akita directly on the caller's own Spongefish state:
batched_prove_embedded(onakita_proverandAkitaCommitmentScheme) andbatched_verify_embedded(onAkitaVerifierandAkitaCommitmentScheme). Akita's messages go into the caller's argument string, and its challenges come from the caller's sponge. The standalonebatched_proveandbatched_verifybuild a fresh state and call the embedded entries, so both modes run the same code. On a fresh state, an embedded proof is byte-for-byte the standalone proof.bind_instance). The root then absorbs each commitment's digest, the opening points and the claimed values. Akita's soundness does not depend on what the caller absorbed.CommitmentDigestis squeezed from a separate Spongefish session, on the selected sponge (Keccak by default), over the canonical bytes of aCommittedGroup(its profile, then its rows).CommittedGroupnow has private fields, and it computes the digest when it is built (CommittedGroup::new, now fallible) or decoded. No path accepts a digest from outside, so the verifier always hashes the rows it then uses. A caller binds a commitment early by absorbing the same 32 bytes.#[must_use] PendingEof. The caller runs its own remaining messages, then callsPendingEof::check_eof(state). That call consumes the state, checks that every proof byte was read, and is the only way to getProofAcceptance. On any error, the embedded verifier first poisons the caller's state, so every later read and the final check fail.twere absorbed twice: once when the prover sent them, and again as public values (WitnessCommitment,TerminalTFields). The second absorb added no binding, so this PR drops it on both sides.Challenges change, but the protocol version stays 8. Versions move once per stack, counted from
main: Akitamainis at 7, and #253 already sets 8 for this stack.Design record:
specs/akita-embedded-transcript.md. Book:how/transcript.md(new sections "Running inside a caller's transcript" and "Commitment digests") andhow/verification.md.Absorb map
"LP" means a
u64little-endian length prefix. "Public" means absorbed but not sent. "Sent" means a prover message, which goes into the argument string and is absorbed once.Before (#253, standalone only):
WitnessCommitment)t, already sentTerminalTFields)After (this PR, still v8):
STANDALONE_SESSION_TAG, instance LP(session bytes)CommitmentDigest(32 bytes), per grouptWhen Akita runs embedded, the "session" row is the caller's own session.
Proof size does not change: this PR sends the same messages as #253.
Soundness audits
Design (xhigh, before implementation): sound with changes, no blockers. Every required change is in this PR:
PendingEofexists;A second pass confirmed these fixes. It also confirmed that dropping both duplicate absorbs is sound, because the value used later is exactly the value received when it was sent.
Implementation (independent read-only audit of the full diff): SOUND, no blockers. It confirmed:
CommittedGroup, and no outside digest can enter;How its non-blocking notes were handled:
check_eofis not tied to the state Akita ran on. Now documented: the caller must pass the same state. A live-state check at the end would add nothing, because every Akita proof reads from the argument string and any read on a poisoned state already fails.claims.groups()in one function. A mismatch could only break completeness, not soundness. MovingCommittedGroupthrough the whole prover is listed as a follow-up.CommittedGroupalready does. That loop was already there before this PR, and it checks against the configured field bound rather than the field modulus, so it was left alone.twas suggested. The old duplicate absorb oftcaught only backend bugs, never soundness bugs. Keeping it would need the sent message passed into the terminal suffix, so it is a follow-up.Re-check of the 32-byte digest: SOUND, no blockers. It confirmed that a 256-bit output meets the 2^128 target on both sponges, that every length-dependent site was updated, and, by reading Spongefish's squeeze code, that a 32-byte squeeze is a prefix of a 64-byte one on both Keccak and Blake2b. So the truncated known answers are correct.
Digest strength: changing a bound commitment means finding a collision in the 32-byte digest, which costs about 2^128 hash calls, the transcript target. Known quantum collision searches are no cheaper than a classical parallel search once their memory is counted: Brassard-Hoyer-Tapp needs about 2^85 quantum memory, and Chailloux-Naya-Plasencia-Schrottenloher needs about 2^102 quantum time. So 256-bit outputs are the usual 128-bit collision level. Both sponges have a 512-bit capacity, so the sponge does not lower this.
Caller contract (for Jolt, J8)
CommittedGroup::digest().as_bytes()is enough. Absorbing the commitment's canonical rows, length-framed, is also enough when public data selects exactly one schedule row, so the profile is fixed. J8 does this: it keeps Jolt main's commitment wire and absorbs the metadata and the rows.PendingEof::check_eof(state).Validation
All on a Boat sandbox at this head, unless noted.
akita-pcs --test embedded_transcript: 7/7 pass withlogging-transcripton Keccak and on Blake2b. The tests:--test transcript_hardening: 6/6 pass, and--test transcript_hardening_proptest: 3/3 pass (Keccak,logging-transcript). This includes the check that prover and verifier record identical event logs.--test protocol_soundness: 5/5 pass, and--test fold_linf: 3/3 pass (Keccak,logging-transcript).akita-types --features prover,logging-transcript: 361/361 pass. This ran on the head before the digest was shortened to 32 bytes.cargo check --workspace --all-targets --features logging-transcriptpasses, andfuzz/passescargo check --bins.AGENTS.md(Keccak) pass with-D warnings.cargo fmt --check,taplo fmt --check,typos, both Rust file-line scripts, the Python script tests,check-crate-deps.shfor verifier, prover, cpu-backend, config, planner and setup,cargo machete, the shared-field identity and external schedule guards, andcheck-doc-guardrails.sh(mdbook build skipped locally).cargo check --workspace --all-targets --features logging-transcriptpasses. The akita-transcript, akita-sumcheck and akita-types tests pass on Keccak and on Blake2b, andembedded_transcriptpasses 7/7 withlogging-transcript. Two of feat(transcript)!: move the transcript onto jolt-transcript (protocol v8) #253's new sumcheck tests were moved to the two-argument channel constructors.de995f497): the akita-transcript, akita-sumcheck and akita-types tests pass on both sponges,embedded_transcriptpasses 5/5 in the default build, the threeAGENTS.mdclippy configurations and the default (Keccak) clippy pass, the profile glue tests pass (16/16) andprofile/passescargo check --release,fuzz/passescargo check, and the shared-field identity check passes for all three workspaces. The three lockfiles were regenerated.e9d85d1f7(J7 re-pin plus the merge of feat(transcript)!: move the transcript onto jolt-transcript (protocol v8) #253's head):cargo check --workspace --all-targets --features logging-transcriptpasses. The akita-params, akita-transcript and akita-sumcheck tests pass (278/278).embedded_transcriptpasses in the default build and withlogging-transcript.cargo clippy --all --all-targets --release --no-default-features --features transcript-blake2b -- -D warningspasses.check-doc-guardrails.shpasses.bfc4c2f1b(merge of feat(transcript)!: move the transcript onto jolt-transcript (protocol v8) #253'se1df1d07a, Jolt pinbed472d3d), on macpro (M4 Max):cargo metadata --lockedfor the root,fuzz/andprofile/akita-recursion/;check-shared-field-identity.sh;cargo clippy --all --all-targets --release --no-default-features --features parallel,disk-persistence,transcript-keccak -- -D warnings; the akita-transcript (prover) and akita-sumcheck library tests (29 each) and the akita-types committed-group tests (22) on both sponges. Noturboshakein any source or lockfile. Known answers re-pinned for the SHAKE128 session inbfc4c2f1b: the transcript, the commitment digest (akita-transcript and akita-types), and the standard and normalized sumcheck proofs and challenges. No assertion changed, and descriptor digests stay Blake2b-256.Known answers were regenerated for both sponges: the transcript, the commitment digest (in akita-transcript and akita-types) and the sumcheck cutover. No checked-in fixture or fuzz corpus depends on transcript bytes. The
akita-cpu-backendlibrary tests need more than 16 GB, so they run in CI only.Follow-ups (not in this PR)
akita_params::digest_descriptor_bytes), whatever the sponge. The verifier recomputes the grinding-plan, opening-layout and effective-schedule digests on every call. A recursive verifier on Keccak therefore needs a Blake2b-256 circuit too, for one compression per 128 input bytes of each digested structure. Moving them to the sponge's hash also changes the schedule row digests stored in the generated catalog artifacts, so it needs an artifact regeneration.CommittedGroupthrough the prover's opening data, so the digests and rows are one value there too.TerminalPoint,TerminalOpening), one at a time, each with its own audit.Commits
refactor(transcript): send each atom group and round polynomial as one message. The bytes stay as on feat(transcript)!: move the transcript onto jolt-transcript (protocol v8) #253.bind_instance,CommitmentDigest, borrowed grinding withPendingEof, the duplicate absorbs dropped, and the known answers.fix(transcript): the diagnostic event log (logging-transcriptonly) now restarts when a standalone prover channel is created. It used to restart when prover grinding started. That erased the instance record, so the prover's records no longer matched the verifier's, and in embedded mode it would have erased the caller's records.docs(spec): link this PR from the spec.test(types): the commitment decoding tests use onlyValidate::Yes. The proof-input guard in CI forbidsValidate::Noin proof-input crates, tests included.build: pin the jolt crates to J7 (6691ec7cb) in every manifest, the three lockfiles and the book, and use J7'sFieldTranscript::challenge_vectorin the proof-stream channel.docs(spec): caller-contract item 5 also allows binding a commitment by its rows when public data fixes its profile.build: re-pin the jolt crates to J7's rebased head (d04df1bb5). Between the two pins,jolt-fieldandjolt-polyare unchanged.jolt-transcriptchanges in two ways. Its default sponge is now Keccak alone, which no Akita crate relies on, because every manifest setsdefault-features = false. Its proof-of-work search now scans 1,024 nonces on the calling thread before it wakes the thread pool; it still returns the least accepted nonce, so proofs are unchanged. The three lockfiles were regenerated.f858d7ebf), so this branch again contains all of feat(transcript)!: move the transcript onto jolt-transcript (protocol v8) #253. This keeps feat(transcript)!: move the transcript onto jolt-transcript (protocol v8) #253's compile-time check thatTRANSCRIPT_SECURITY_BITSequalsjolt_transcript::SECURITY_BITS. The pin conflicts were resolved to this branch's J7 pin.4a3b8dc8canddac235470, the J713e149195pin, the fuzz port to the group channel API, andfix(transcript): keep the tags at v8 (c16468ac6).e1df1d07a(849bdd4c5, SHAKE128 session id), pin Jolt at J7bed472d3din every manifest, lockfile and the book (7e25e59ff), and re-pin the known answers on both sponges (bfc4c2f1b).🤖 Generated with Claude Code