From 7d3ad39e59e628f7bab201cf8bbf8a6940f244e6 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:10:26 -0400 Subject: [PATCH 01/62] docs(specs): specify one NARG transcript for Jolt and Akita --- specs/jolt-transcript-narg.md | 383 ++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 specs/jolt-transcript-narg.md diff --git a/specs/jolt-transcript-narg.md b/specs/jolt-transcript-narg.md new file mode 100644 index 0000000000..a24719637e --- /dev/null +++ b/specs/jolt-transcript-narg.md @@ -0,0 +1,383 @@ +# Spec: One NARG transcript for Jolt and Akita + +| Field | Value | +| --------- | ------------------ | +| Author(s) | @markosg04 | +| Created | 2026-10-01 | +| Status | proposed | +| PR | (Jolt) / (Akita) | + +## Summary + +Jolt and Akita each own a Fiat-Shamir transcript. Jolt's production path is a +symmetric absorb/squeeze facade over a Blake2b-256 digest chain +(`LegacyBlake2bTranscript`), with proof data carried in a typed `JoltProof` +struct that the verifier must remember to absorb field by field. Akita moved to +a spongefish NARG proof stream (#37) with exact field sampling, grinding, and +site diagnostics, in its own `akita-transcript`. Jolt wraps Akita's opening in a +nested Akita channel whose session is seeded by one squeezed "bridge" challenge +(`bridged_akita_session`, `jolt-akita/src/adapters.rs:1323`). The bridge exists only because the two +transcripts cannot compose. + +This spec replaces both with one `jolt-transcript` built on the NARG model: +the proof *is* the argument string, every prover message is absorbed exactly as +transmitted, and protocols compose by sharing a channel. Jolt's proof becomes +the NARG. Akita runs on the caller's channel, so it nests inside Jolt with no +bridge and `akita-transcript` is deleted. The sponge is a type parameter; +choosing a different one is a one-line alias change. + +Delivery is two co-designed PRs, one per repository, with no compatibility +layer and no follow-up migration PR. + +## Intent + +### Goal + +`jolt-transcript` provides a role-generic proof channel, prover and verifier +ends, typed atoms, exact and small-set challenges, a preview handle for proof +of work, and feature-gated site diagnostics. It is expressive enough that Akita +uses it unchanged, and Jolt's prover, verifier, BlindFold, Dory, and Akita +integration all run on it. + +### Ownership + +| Component | Owner crate | Notes | +| --- | --- | --- | +| `Sponge` trait, `Prover`, `Verifier<'a, H>`, `Channel`, `Atom`, `Preview`, grinding, `TranscriptError`, `SiteId` | `jolt-transcript` | Depends on `jolt-field` and `spongefish` only. | +| Sponge permutations / hash duplexes | `spongefish` (Blake2b512, Keccak, ...) and `jolt-transcript::PoseidonSponge` | One workspace pin for both repos. | +| Exact uniform sampling contract | `jolt-field` (`Field::random`) | The channel feeds it a sponge-backed `RngCore`. | +| Small-set challenge decoding | `jolt-field` (`from_challenge_bytes`) | Unchanged semantics. | +| Canonical encode/checked decode of fields | `jolt-field` (`CanonicalEncoding`) | Extension fields gain `CanonicalEncoding` (checked per coefficient). | +| Atom impls for group elements, commitments | `jolt-crypto`, `jolt-dory`, Akita crates | Each type's owner implements `Atom` locally. | +| Round-message shapes, protocol order | `jolt-sumcheck`, `jolt-verifier`, Akita protocol crates | Shapes are positional, derived from public parameters. | +| Akita site coordinates, descriptor bytes | `akita-types` | Plain data; converted to `SiteId` for diagnostics. | + +### Invariants + +1. **Absorbed bytes are transmitted bytes.** Every prover message is absorbed + with exactly the bytes written to (prover) or consumed from (verifier) the + NARG. `Atom::read` accepts only canonical encodings of valid values (field + elements below the modulus, on-curve and in-subgroup points), so a verifier + cannot accept two byte strings for one value. +2. **No unabsorbed proof data.** The verifier obtains proof data only through + `Verifier::receive*`. There is no hint channel and no side struct. The + absence of a hint API is the enforcement mechanism. +3. **Exact consumption.** `Verifier::finish(self)` fails unless the NARG is + fully consumed. Composite verifiers (Jolt calling Akita) call it once, at the + outermost boundary. +4. **Domain separation.** Each channel starts from a 64-byte protocol id that + binds the protocol name, version, proof mode (clear or ZK), and + `Sponge::ID`, followed by a length-framed session value. A proof produced + under one sponge or mode cannot verify under another. +5. **Role symmetry.** For any operation sequence, a verifier replaying it over + the prover's NARG reproduces every challenge and every received value. + `jolt-eval`'s `transcript_prover_verifier_consistency` invariant checks this + over `Prover`/`Verifier` for every sponge. +6. **Previews cannot mutate the proof.** `Preview` holds only a clone of the + public sponge state. It cannot write the NARG or advance the live sponge. +7. **Sites never affect bytes.** `SiteId` is diagnostic. It is recorded under + the `logging` feature and compiled out otherwise. Toggling the feature does + not change a proof or a challenge. +8. **Challenge distributions are explicit per call site.** `challenge::()` + is exactly uniform over `F` (`Field::random` rejection contract). + `challenge_small::()` is the field's small-set draw + (`from_challenge_bytes` over 16 squeezed bytes; 125 bits for BN254). No other + draw exists. + +### Non-Goals + +- Changing `dory-pcs` (crates.io). Dory keeps its `DoryTranscript` trait; see + Design. +- Unifying `jolt-sumcheck` and `akita-sumcheck`. +- Byte-compatibility with any existing proof, fixture digest, or EVM/recursion + transcript. Every pinned proof digest is regenerated. +- Updating out-of-tree consumers that reproduce the Blake2b-256 digest chain + (EVM verifier #1890, Blake fast recursion #1892/#1911, wrapper constraints + #1879/#1886). They port to the new transcript in their own PRs. +- Changing what any protocol stage proves. Message order is preserved stage by + stage. Only transport, framing, and the challenge mapping in invariant 8 + change. + +## Design + +### API + +```rust +pub trait Sponge: DuplexSpongeInterface + Default + Clone + Send + Sync + 'static { + /// Bound into every protocol id derived for this sponge. + const ID: &'static str; +} + +/// Fixed-width canonical value. Shapes are positional: callers send and receive +/// exactly the counts their public parameters imply. +pub trait Atom: Sized { + const SIZE: usize; + fn write(&self, out: &mut [u8]); // exactly SIZE bytes + fn read(bytes: &[u8]) -> Result; // canonical-only +} +impl Atom for F { /* LE canonical bytes */ } +// Local atoms: U32, U64, Bytes. Group elements and commitments implement +// Atom in their owner crates. + +pub struct ProtocolId([u8; 64]); +impl ProtocolId { + pub fn new(name: &str, version: u32, mode: ProofMode) -> Self; +} + +pub struct Prover { /* sponge, narg, logging */ } +impl Prover { + pub fn new(protocol: &ProtocolId, session: &[u8]) -> Self; + pub fn send(&mut self, value: &A); + pub fn send_all(&mut self, values: &[A]); + pub fn send_bytes(&mut self, bytes: &[u8], max_len: usize) -> Result<(), TranscriptError>; + pub fn grind(&mut self, bits: u8) -> Result<(), TranscriptError>; + pub fn finish(self) -> Vec; +} + +pub struct Verifier<'a, H: Sponge> { /* sponge, remaining narg, logging */ } +impl<'a, H: Sponge> Verifier<'a, H> { + pub fn new(protocol: &ProtocolId, session: &[u8], narg: &'a [u8]) -> Self; + pub fn receive(&mut self) -> Result; + pub fn receive_n(&mut self, count: usize) -> Result, TranscriptError>; + pub fn receive_bytes(&mut self, max_len: usize) -> Result, TranscriptError>; + pub fn check_grind(&mut self, bits: u8) -> Result<(), TranscriptError>; + pub fn finish(self) -> Result<(), TranscriptError>; +} + +/// Operations both roles perform identically; shared protocol code takes `&mut impl Channel`. +pub trait Channel { + type Sponge: Sponge; + fn public(&mut self, value: &A); + fn public_all(&mut self, values: &[A]); + fn public_bytes(&mut self, bytes: &[u8]); // length-framed + /// Prover: send `*value`. Verifier: overwrite `*value` with the received atom. + fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError>; + fn challenge(&mut self) -> F; + fn challenge_small(&mut self) -> F; + fn challenge_bytes(&mut self) -> [u8; N]; + fn preview(&self) -> Preview; + fn site(&mut self, site: SiteId); +} +``` + +`Prover` and `Verifier` implement `Channel`. `public_*` and `challenge*` are +inherent on both as well, so role-specific code needs no trait import. + +Example: one sumcheck round of degree `d`. The verifier knows `d` from public +parameters, so the poly's length is never transmitted. + +```rust +// prover // verifier +prover.send_all(round_poly.coeffs()); let c = verifier.receive_n::(d + 1)?; +let r = prover.challenge_small::(); let r = verifier.challenge_small::(); +``` + +### Wire format + +The NARG is the concatenation of atom encodings and length-framed byte payloads +(8-byte LE length, then bytes, with `len <= max_len` checked before any +allocation). It carries no labels or tags. `JoltProof` becomes a +`Vec` newtype with serde. Prover-chosen parameters the verifier needs before +reading shapes (`trace_length`, `ram_K`, configs, advice presence) are the first +prover messages. The verifier receives and validates each one before using it +to size a later receive. The proof mode is in the protocol id, and the verifier +also checks it against its build before reading, so a mismatch surfaces as a +typed error rather than a challenge mismatch. + +Byte order is little-endian canonical (`CanonicalBytes`). The legacy +big-endian reversal disappears. + +### Composition: Jolt calls Akita + +Akita's prove and verify entry points take `&mut Prover` / `&mut Verifier` +(or `&mut impl Channel` for shared sites) and absorb their instance descriptor +with `public_bytes` as their first operation. Akita's standalone API constructs +its own channel with an Akita `ProtocolId` and calls the same inner function, so +there is one code path. Inside Jolt, stage 8 passes its live channel. Akita's +messages land in Jolt's NARG, and Jolt's transcript state binds everything +Akita squeezes. `bridged_akita_session`, the nested session label, and +`AkitaBatchProof`'s opaque proof bytes are deleted. + +Akita's current instance binding (`DomainSeparator.instance(descriptor)`) +becomes a public message. Soundness is unchanged: the descriptor is absorbed +before any Akita challenge in both cases. + +### Dory + +`dory-pcs` exposes a symmetric `DoryTranscript` and its own proof struct. +`jolt-dory` implements `DoryTranscript` for `&mut impl Channel`: `append_*` maps +to `public_*` and `challenge_scalar` to `challenge`. The serialized Dory proof is +sent with `send_bytes` and decoded with `DorySerialize`, which rejects trailing +and non-canonical data. Dory therefore absorbs its group elements twice, once +as NARG bytes and once through its own transcript calls. That costs a few +hundred group-element absorptions per proof, and it keeps invariant 2: there is +no hint path. A Dory-native NARG integration belongs to a `dory-pcs` release +and is out of scope. + +### Grinding and previews + +`Preview` clones the sponge and exposes `absorb` and +`squeeze(&mut [u8])`. `grind(bits)` searches `U32` nonces against a preview, +sends the winning nonce, then squeezes and checks the predicate on the live +channel. `check_grind` receives the nonce and checks the same predicate. The +predicate (leading zero bits of 8 squeezed bytes, little-endian bit order) and +the nonce bound move from `akita-transcript/src/grinding.rs`. Akita's +fold-response nonce search builds on `Preview` inside Akita. + +### Diagnostics + +Under `logging`, each channel records `(SiteId, op kind, NARG byte range)`. Akita +converts `ProtocolSiteId` to `SiteId([u8; 32])`. Jolt's verifier tags each stage +with a `SiteId` and retires `fs_audit`'s thread-local scope. Tests use the +recorded ranges for two purposes: + +- **Equivalence:** the prover's and verifier's site streams are identical. + This replaces Akita's `transcript_hardening` and Jolt's `fs_inventory`. +- **Tamper sweep:** flipping one byte in every recorded prover-message range + makes verification fail. This is the NARG form of the existing per-field + tamper sweeps. + +### BlindFold + +The ZK prover finishes stage 8 on its `Prover`, then runs the verifier's own +stage functions over a `Verifier` built on the NARG prefix to obtain the stage +outputs that BlindFold lowers. The replay has to end with the prefix fully +consumed and a 32-byte sponge fingerprint (squeezed from a clone) equal to the +prover's. Otherwise proving fails with the existing +`ProverError::InvariantViolation`. BlindFold then continues on the original `Prover`. ZK recorders send +round commitments instead of round polynomials. The choice is made by message +type, not by a second transcript. + +### Sponge selection + +Protocol code is generic over `H: Sponge`. That replaces today's +`T: Transcript` one for one, so generic arity is unchanged. Jolt fixes its +default in one place +(`pub type JoltSponge = spongefish::instantiations::Blake2b512` in +`jolt-verifier`). Akita drops the `transcript-blake2b`/`transcript-keccak` +features and the `compile_error!` that enforces exactly one of them. The sponge +reaches Akita through its caller's `H`. `PoseidonSponge` stays behind the +`transcript-poseidon` feature because it pulls in BN254. + +### Alternatives considered + +- **Keep spongefish's `ProverState`/`VerifierState` and its codec traits.** + Rejected for four reasons: + - Typed errors: spongefish's `VerificationError` is a unit type, which + conflicts with the verifier-closure lints. + - Previews need the sponge state, which spongefish exposes only through its + `yolocrypto` feature. + - Its encode-then-serialize split permits absorbed bytes and transmitted + bytes to differ, which invariant 1 forbids. + - There is no logging hook. + + `jolt-transcript` uses spongefish for sponges only. +- **Keep labels and absorb them per message.** Rejected. Order is fixed by + public parameters, so labels add no binding, and they cost one absorption per + message. The diagnostic need is met by `SiteId` at zero release cost. +- **Ship the Dory proof as a hint, absorbed only through `DoryTranscript`.** + Rejected. It reintroduces unabsorbed proof data, so soundness would depend on + `dory-pcs` absorbing every element it reads. +- **One exactly uniform challenge everywhere.** Rejected for this change. The + 125-bit draw is a measured prover optimization (cheaper binding + multiplications in BN254 Montgomery form). Changing it is a performance + decision, not a transport one. The mapping is: today's `challenge()` becomes + `challenge_small`, and today's `challenge_scalar()` and Akita's field + challenges become exact `challenge`. Each draw moves to an equal or larger + sample set. + +## Evaluation + +### Acceptance criteria + +- [ ] `jolt-transcript` exposes only the API above. `Transcript`, + `AppendToTranscript`, `Label`, `LabelWithCount`, `U64Word`, + `DigestTranscript`, `LegacyBlake2bTranscript`, `SpongeTranscript`, and + the split `ProverTranscript`/`VerifierTranscript` traits are deleted. +- [ ] `akita-transcript` is deleted. No Akita crate depends on `spongefish` + directly. Akita depends on `jolt-transcript` at the Jolt PR's revision. +- [ ] `jolt-akita` contains no nested transcript. Akita's messages are in + Jolt's NARG, and the only `finish()` call is Jolt's. +- [ ] `JoltProof` is a NARG newtype. No verifier stage reads proof data except + through `Verifier`. +- [ ] Changing `JoltSponge` to `Keccak` compiles, and the prover acceptance + suites pass (exercised once in CI on one suite). +- [ ] Tamper sweep: in the clear, ZK, and Akita fixture proofs, every recorded + prover-message range rejects under a one-byte flip. +- [ ] Every pinned proof digest and fixture is regenerated. The fixture test + decodes and verifies them. + +### Testing strategy + +- `jolt-transcript`: + - Frozen known-answer vectors per sponge for protocol-id derivation, each + atom kind, framed bytes, `challenge`, `challenge_small`, and grinding. + - Round-trip symmetry through the `jolt-eval` invariant (updated to the new + API). + - Rejection of truncated, trailing, non-canonical, and over-length input, + leaving the cursor unmoved on error. + - Preview non-mutation. +- `jolt-field`: checked decoding for extension fields rejects any + non-canonical coefficient. +- Workspace: the prover acceptance suites in CLAUDE.md (clear, ZK, Akita), the + verifier fixtures, and `cargo clippy` in both modes. +- Akita: the existing PCS suites, with `transcript_hardening` rewritten + against `logging` site streams. + +### Performance + +- No regression above noise on `transcript_ops`. +- No regression in end-to-end prover time on `fibonacci` at 2^16 and 2^20 + (`telemetry:fibonacci:prover_time_s`), Dory and Akita. +- Proof size is reported before and after. It should shrink: labels and + 32-byte framing words disappear, and the double-absorbed Dory bytes are not + transmitted twice. + +## Execution + +### Implementation slices (both PRs developed together) + +1. Pin spongefish `ef97413` in Jolt. Add the new `jolt-transcript`, and add + `CanonicalEncoding` for extension fields. +2. Port the crates bottom-up: `jolt-crypto`, `jolt-poly`, `jolt-sumcheck`, + `jolt-openings`, `jolt-dory`, `jolt-blindfold`. +3. Port `jolt-verifier` (stages read from `Verifier`, `JoltProof` becomes the + NARG) and `jolt-prover` (stages write to `Prover`, BlindFold replays the + NARG). +4. Akita PR: rebase on Akita `main`, delete `akita-transcript`, and make the + entry points channel-taking. Port `akita-sumcheck`, `-types`, `-prover`, + `-verifier`, `-pcs`, `-challenges`, `-config`, and `-cpu-backend`. +5. Bump the Akita pin in the Jolt PR to the Akita PR head. Port `jolt-akita` + (Jolt already pins a post-#37 Akita, `1cc7a2a`, 12 commits behind `main`, and compiles against Akita `main` unchanged) and delete the bridge. + Extend `[patch."https://github.com/a16z/jolt"]` to `jolt-transcript` and + `jolt-poly`. +6. Regenerate fixtures and digests. Update `jolt-sdk`, `jolt-eval`, and + `examples/recursion`. + +### Merge order + +1. The Akita PR merges first, pinning the Jolt PR's head revision (retained + through its PR ref). +2. The Jolt PR repins Akita to the merge commit and merges. + +After the Akita PR merges, `jolt-transcript`, `jolt-field`, and `jolt-poly` must +not change in the Jolt PR without a repin. Akita's next routine Jolt bump moves +it onto Jolt `main`. + +## Unverified / risks + +- **Akita API churn.** 17 open Akita + PRs will conflict with the channel-taking signatures. +- **Recursion cycle count.** `examples/recursion` verifies in-guest with + software Blake2b512 duplexing. The cycle-count change is not yet measured. +- **Dory parsing.** That `DorySerialize` rejects every non-canonical proof + encoding is assumed from `jolt-dory`'s existing serde tests, not re-audited. +- **Small-challenge wrapper constraints.** Whether any wrapper constraint + depends on 125-bit challenges is out of scope here. It matters for the + out-of-tree ports. + +## References + +- `specs/jolt-transcript-spongefish.md` (#1455): the prior staged port this supersedes. +- Akita #37 (spongefish proof stream), `akita-transcript` on Akita `main`. +- spongefish `ef97413` (`DuplexSpongeInterface`, `instantiations`). From ecac95d1212805c82a8d25619806d0a7bcd25d1f Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:18:59 -0400 Subject: [PATCH 02/62] feat(field)!: own checked decoding in a CanonicalDecode codec trait 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. --- crates/jolt-field/src/algebra.rs | 71 ++++++++++++++++--- crates/jolt-field/src/bn254/mod.rs | 24 ++++--- crates/jolt-field/src/lib.rs | 11 +-- crates/jolt-field/src/ops.rs | 2 +- crates/jolt-field/src/solinas/ext.rs | 48 ++++++++++++- crates/jolt-field/src/solinas/fp128.rs | 18 ++--- crates/jolt-field/src/solinas/word.rs | 18 +++-- .../tests/solinas_ext_differential.rs | 16 ++++- .../tests/solinas_fp128_differential.rs | 10 +-- .../tests/solinas_words_differential.rs | 8 +-- crates/jolt-field/tests/spine.rs | 15 ++-- .../jolt-sumcheck/tests/mersenne61_compat.rs | 16 +++-- zklean-extractor/src/mle_ast.rs | 42 +++++------ 13 files changed, 217 insertions(+), 82 deletions(-) diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index a37433cd5c..b49fa27e73 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -280,18 +280,75 @@ pub trait CanonicalBytes { } } +/// Checked decoding half of a fixed-width canonical codec. +/// +/// Proof transport and wire serialization decode through this trait, so a +/// verifier accepts exactly one byte string per value. +/// +/// # Invariants +/// +/// - `from_bytes_le_checked(&x.to_bytes_le_vec()) == Some(x)` for every `x`. +/// - Every other input, including any input whose length is not +/// [`CanonicalBytes::NUM_BYTES`], decodes to `None`. +pub trait CanonicalDecode: CanonicalBytes + Sized { + /// Decodes exactly [`CanonicalBytes::NUM_BYTES`] canonical bytes; + /// `None` on wrong length or a non-canonical value. + fn from_bytes_le_checked(bytes: &[u8]) -> Option; +} + +/// Fixed-width little-endian codecs for unsigned integers, so protocol +/// counters and nonces travel as transcript atoms like field elements do. +macro_rules! impl_uint_codec { + ($($t:ty),*) => {$( + impl CanonicalBytes for $t { + const NUM_BYTES: usize = <$t>::BITS as usize / 8; + + #[inline] + fn to_bytes_le(&self, out: &mut [u8]) { + out.copy_from_slice(&self.to_le_bytes()); + } + } + + impl CanonicalDecode for $t { + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + bytes.try_into().ok().map(<$t>::from_le_bytes) + } + } + )*}; +} + +impl_uint_codec!(u8, u16, u32, u64, u128); + +/// Raw bytes are their own canonical encoding. +impl CanonicalBytes for [u8; N] { + const NUM_BYTES: usize = N; + + #[inline] + fn to_bytes_le(&self, out: &mut [u8]) { + out.copy_from_slice(self); + } +} + +impl CanonicalDecode for [u8; N] { + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + bytes.try_into().ok() + } +} + /// Canonical decode-and-introspect surface of a field element, on top of the -/// [`CanonicalBytes`] encoding: the single source of canonicity for wire -/// serialization. +/// [`CanonicalBytes`]/[`CanonicalDecode`] codec: the single source of +/// canonicity for wire serialization. /// /// Transcript absorption and challenge derivation use the explicit /// [`CanonicalBytes`] encoding so the hashed byte stream is specified /// independently of any serialization library. Proof/wire serialization goes /// through serde + bincode, reusing -/// [`from_bytes_le_checked`](Self::from_bytes_le_checked) so non-canonical -/// encodings are rejected uniformly. +/// [`from_bytes_le_checked`](CanonicalDecode::from_bytes_le_checked) so +/// non-canonical encodings are rejected uniformly. pub trait CanonicalEncoding: - CanonicalBytes + Sized + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static + CanonicalDecode + Copy + Default + PartialEq + Eq + Debug + Hash + Send + Sync + 'static { /// Bit length of the field order `|F|` (for prime fields, the modulus). const MODULUS_BITS: u32; @@ -299,10 +356,6 @@ pub trait CanonicalEncoding: /// Decodes little-endian bytes of any length by reducing into the field. fn from_bytes_le_reduced(bytes: &[u8]) -> Self; - /// Decodes exactly [`CanonicalBytes::NUM_BYTES`] canonical bytes; - /// `None` on wrong length or a non-canonical value. - fn from_bytes_le_checked(bytes: &[u8]) -> Option; - /// Returns the canonical representative if it fits in a `u128`. /// /// For extension fields: the constant coefficient, when all higher diff --git a/crates/jolt-field/src/bn254/mod.rs b/crates/jolt-field/src/bn254/mod.rs index 9633738b5d..ec64e20177 100644 --- a/crates/jolt-field/src/bn254/mod.rs +++ b/crates/jolt-field/src/bn254/mod.rs @@ -10,7 +10,10 @@ mod mont; pub use mont::{FrSignedProductAccumulator, FrSmallScalarAccumulator, WideAccumulator}; -use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{ + CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, +}; use ark_ff::{BigInteger, PrimeField, UniformRand}; use rand_core::RngCore; @@ -123,14 +126,7 @@ macro_rules! wrap_bn254 { } } - impl CanonicalEncoding for $ty { - const MODULUS_BITS: u32 = 254; - - #[inline] - fn from_bytes_le_reduced(bytes: &[u8]) -> Self { - $ty(<$inner>::from_le_bytes_mod_order(bytes)) - } - + impl CanonicalDecode for $ty { #[inline] fn from_bytes_le_checked(bytes: &[u8]) -> Option { use ark_serialize::CanonicalDeserialize; @@ -139,6 +135,16 @@ macro_rules! wrap_bn254 { } <$inner>::deserialize_compressed(bytes).ok().map($ty) } + } + + impl CanonicalEncoding for $ty { + const MODULUS_BITS: u32 = 254; + + #[inline] + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + $ty(<$inner>::from_le_bytes_mod_order(bytes)) + } + #[inline] fn to_u128_checked(&self) -> Option { diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index ac742f162e..cbe8cfd771 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -1,8 +1,9 @@ //! Field and ring abstractions for the Jolt zkVM. //! //! A slim algebraic ladder — [`AdditiveGroup`] → [`Ring`] → [`Field`] — with -//! orthogonal capabilities: [`CanonicalBytes`]/[`CanonicalEncoding`] (the -//! Fiat-Shamir transcript surface and the field decode surface on top of it) +//! orthogonal capabilities: [`CanonicalBytes`]/[`CanonicalDecode`] (the +//! fixed-width canonical codec every transcript atom implements), +//! [`CanonicalEncoding`] (the field decode-and-introspect surface on top of it) //! and [`WithAccumulator`] (deferred-reduction fused multiply-add). //! [`JoltField`] is the blanket-implemented bundle of everything Jolt's //! protocol stack requires of a scalar field: `Field + CanonicalEncoding + @@ -64,7 +65,7 @@ //! - Proof/wire serialization is serde + bincode over canonical //! little-endian bytes (see [`impl_serde_bytes!`]); deserialization //! rejects non-canonical encodings uniformly via -//! [`CanonicalEncoding::from_bytes_le_checked`]. +//! [`CanonicalDecode::from_bytes_le_checked`]. //! - Fiat-Shamir transcript bytes use the explicit little-endian encoding //! ([`CanonicalBytes::to_bytes_le`]) and never go through a serialization //! library. @@ -105,8 +106,8 @@ pub mod solinas; mod unreduced; pub use algebra::{ - Accumulator, AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, JoltField, - MaybeAllocative, NaiveAccumulator, PseudoMersenne, Ring, WithAccumulator, + Accumulator, AdditiveGroup, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, + JoltField, MaybeAllocative, NaiveAccumulator, PseudoMersenne, Ring, WithAccumulator, }; #[cfg(feature = "bn254")] pub use bn254::{Fq, Fr, FrSignedProductAccumulator, FrSmallScalarAccumulator, WideAccumulator}; diff --git a/crates/jolt-field/src/ops.rs b/crates/jolt-field/src/ops.rs index 11792ac5b9..ddb1684686 100644 --- a/crates/jolt-field/src/ops.rs +++ b/crates/jolt-field/src/ops.rs @@ -179,7 +179,7 @@ macro_rules! impl_serde_bytes { impl<'de, $($g)*> ::serde::Deserialize<'de> for $ty { fn deserialize>(deserializer: D) -> Result { let buf = <[u8; $n]>::deserialize(deserializer)?; - <$ty as $crate::CanonicalEncoding>::from_bytes_le_checked(&buf) + <$ty as $crate::CanonicalDecode>::from_bytes_le_checked(&buf) .ok_or_else(|| ::serde::de::Error::custom("non-canonical field element encoding")) } } diff --git a/crates/jolt-field/src/solinas/ext.rs b/crates/jolt-field/src/solinas/ext.rs index ceb027d9f8..18c430d077 100644 --- a/crates/jolt-field/src/solinas/ext.rs +++ b/crates/jolt-field/src/solinas/ext.rs @@ -26,7 +26,9 @@ )] use crate::solinas::pseudo_mersenne_modulus; -use crate::{CanonicalBytes, Ext2Config, ExtField, Field, FieldError, PseudoMersenne, Ring}; +use crate::{ + CanonicalBytes, CanonicalDecode, Ext2Config, ExtField, Field, FieldError, PseudoMersenne, Ring, +}; use num_traits::Zero; use rand_core::RngCore; use std::marker::PhantomData; @@ -140,6 +142,13 @@ impl> CanonicalBytes for FpExt2> CanonicalDecode for FpExt2 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let [c0, c1] = decode_coeffs(bytes)?; + Some(Self::new(c0, c1)) + } +} + crate::impl_ring_ops!(impl[F: Field, C: Ext2Config] FpExt2 { add(a, b): FpExt2::new(a.coeffs[0] + b.coeffs[0], a.coeffs[1] + b.coeffs[1]), sub(a, b): FpExt2::new(a.coeffs[0] - b.coeffs[0], a.coeffs[1] - b.coeffs[1]), @@ -302,6 +311,12 @@ impl CanonicalBytes for FpExt4 { } } +impl CanonicalDecode for FpExt4 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + decode_coeffs(bytes).map(Self::new) + } +} + crate::impl_ring_ops!(impl[F: PseudoMersenne] FpExt4 { add(a, b): FpExt4::new(std::array::from_fn(|i| a.coeffs[i] + b.coeffs[i])), sub(a, b): FpExt4::new(std::array::from_fn(|i| a.coeffs[i] - b.coeffs[i])), @@ -428,6 +443,37 @@ impl std::fmt::Display for FpExt8 { } } +/// Encodes coefficients in basis order as `c0 || ... || c7`. +impl CanonicalBytes for FpExt8 { + const NUM_BYTES: usize = F::NUM_BYTES * 8; + + fn to_bytes_le(&self, out: &mut [u8]) { + assert_eq!(out.len(), Self::NUM_BYTES); + for (coefficient, bytes) in self.coeffs.iter().zip(out.chunks_exact_mut(F::NUM_BYTES)) { + coefficient.to_bytes_le(bytes); + } + } +} + +impl CanonicalDecode for FpExt8 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + decode_coeffs(bytes).map(Self::new) + } +} + +/// Decodes `N` canonical base coefficients laid out in basis order, rejecting +/// any wrong length or non-canonical coefficient. +fn decode_coeffs(bytes: &[u8]) -> Option<[F; N]> { + if bytes.len() != F::NUM_BYTES * N { + return None; + } + let mut coeffs = [F::zero(); N]; + for (coefficient, chunk) in coeffs.iter_mut().zip(bytes.chunks_exact(F::NUM_BYTES)) { + *coefficient = F::from_bytes_le_checked(chunk)?; + } + Some(coeffs) +} + crate::impl_ring_ops!(impl[F: PseudoMersenne] FpExt8 { add(a, b): FpExt8::new(std::array::from_fn(|i| a.coeffs[i] + b.coeffs[i])), sub(a, b): FpExt8::new(std::array::from_fn(|i| a.coeffs[i] - b.coeffs[i])), diff --git a/crates/jolt-field/src/solinas/fp128.rs b/crates/jolt-field/src/solinas/fp128.rs index 3e2850c7ca..9d7277220b 100644 --- a/crates/jolt-field/src/solinas/fp128.rs +++ b/crates/jolt-field/src/solinas/fp128.rs @@ -20,8 +20,8 @@ use super::word::mul64_wide; use crate::PseudoMersenne; use crate::{ - CanonicalBytes, CanonicalEncoding, Field, Fp128Accumulator, Fp128SignedAccumulator, Ring, - WithAccumulator, + CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, Fp128Accumulator, + Fp128SignedAccumulator, Ring, WithAccumulator, }; #[cfg(feature = "bytemuck")] use bytemuck::{CheckedBitPattern, NoUninit, Zeroable}; @@ -1132,6 +1132,14 @@ impl CanonicalBytes for Fp128

{ } } +impl CanonicalDecode for Fp128

{ + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; 16] = bytes.try_into().ok()?; + Self::from_u128_checked(u128::from_le_bytes(arr)) + } +} + impl CanonicalEncoding for Fp128

{ // C < 2^32 implies p > 2^127, so the modulus is exactly 128 bits. const MODULUS_BITS: u32 = 128; @@ -1146,12 +1154,6 @@ impl CanonicalEncoding for Fp128

{ crate::solinas::reduce_le_bytes_mod_order(bytes) } - #[inline] - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - let arr: [u8; 16] = bytes.try_into().ok()?; - Self::from_u128_checked(u128::from_le_bytes(arr)) - } - #[inline] fn to_u128_checked(&self) -> Option { Some(join(self.0)) diff --git a/crates/jolt-field/src/solinas/word.rs b/crates/jolt-field/src/solinas/word.rs index b4855a8288..085fd3ca7c 100644 --- a/crates/jolt-field/src/solinas/word.rs +++ b/crates/jolt-field/src/solinas/word.rs @@ -9,7 +9,10 @@ //! path for sub-word primes, with a BMI2 variant on x86-64). use crate::PseudoMersenne; -use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{ + CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, +}; use rand_core::RngCore; /// Trial-division primality check, cheap enough for CTFE at u32 scale. @@ -308,6 +311,14 @@ macro_rules! define_solinas_prime { } } + impl CanonicalDecode for $name

{ + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; (<$word>::BITS / 8) as usize] = bytes.try_into().ok()?; + Self::from_u128_checked(<$word>::from_le_bytes(arr) as u128) + } + } + impl CanonicalEncoding for $name

{ const MODULUS_BITS: u32 = Self::BITS; @@ -321,11 +332,6 @@ macro_rules! define_solinas_prime { $crate::solinas::reduce_le_bytes_mod_order(bytes) } - #[inline] - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - let arr: [u8; (<$word>::BITS / 8) as usize] = bytes.try_into().ok()?; - Self::from_u128_checked(<$word>::from_le_bytes(arr) as u128) - } #[inline] fn to_u128_checked(&self) -> Option { diff --git a/crates/jolt-field/tests/solinas_ext_differential.rs b/crates/jolt-field/tests/solinas_ext_differential.rs index d217d3bbc3..149016d782 100644 --- a/crates/jolt-field/tests/solinas_ext_differential.rs +++ b/crates/jolt-field/tests/solinas_ext_differential.rs @@ -22,7 +22,9 @@ use jolt_field as two; use num_traits::{One, Zero}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{CanonicalBytes, CanonicalEncoding, ExtField, Field, PseudoMersenne, Ring}; +use two::{ + CanonicalBytes, CanonicalDecode, CanonicalEncoding, ExtField, Field, PseudoMersenne, Ring, +}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xE87_D1FF) @@ -388,6 +390,18 @@ macro_rules! check_ext { bincode::serde::decode_from_slice::<$E2, _>(&bad[..nb * d - 1], cfg).is_err(), "truncated encoding must be rejected" ); + let x = mk2(&sample($rng)); + let encoded = x.to_bytes_le_vec(); + assert_eq!(encoded.len(), <$E2 as CanonicalBytes>::NUM_BYTES); + assert_eq!( + <$E2 as CanonicalDecode>::from_bytes_le_checked(&encoded), + Some(x) + ); + assert_eq!(<$E2 as CanonicalDecode>::from_bytes_le_checked(&bad), None); + assert_eq!( + <$E2 as CanonicalDecode>::from_bytes_le_checked(&encoded[..nb * d - 1]), + None + ); // Random sampling spec: with the same seed, `random` draws the `d` // base-field coefficients in order. diff --git a/crates/jolt-field/tests/solinas_fp128_differential.rs b/crates/jolt-field/tests/solinas_fp128_differential.rs index daf4e1c1e3..5c5a53d342 100644 --- a/crates/jolt-field/tests/solinas_fp128_differential.rs +++ b/crates/jolt-field/tests/solinas_fp128_differential.rs @@ -12,8 +12,8 @@ use jolt_field as two; use rand::{Rng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use two::{ - Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, - Ring, + Accumulator as _, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field as _, JoltField, + PseudoMersenne, Ring, }; fn rng() -> ChaCha20Rng { @@ -302,16 +302,16 @@ macro_rules! check_prime128 { // the CanonicalEncoding surface and through serde. let p_bytes = p.to_le_bytes(); assert_eq!( - <$two as CanonicalEncoding>::from_bytes_le_checked(&p_bytes), + <$two as CanonicalDecode>::from_bytes_le_checked(&p_bytes), None ); assert_eq!( - <$two as CanonicalEncoding>::from_bytes_le_checked(&p_bytes[..15]), + <$two as CanonicalDecode>::from_bytes_le_checked(&p_bytes[..15]), None, "wrong length rejected" ); assert_eq!( - <$two as CanonicalEncoding>::from_bytes_le_checked(&[0u8; 17]), + <$two as CanonicalDecode>::from_bytes_le_checked(&[0u8; 17]), None, "over-length rejected" ); diff --git a/crates/jolt-field/tests/solinas_words_differential.rs b/crates/jolt-field/tests/solinas_words_differential.rs index 2af4f38f7b..75642107be 100644 --- a/crates/jolt-field/tests/solinas_words_differential.rs +++ b/crates/jolt-field/tests/solinas_words_differential.rs @@ -12,8 +12,8 @@ use num_bigint::BigUint; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use two::{ - Accumulator as _, CanonicalBytes, CanonicalEncoding, Field as _, JoltField, PseudoMersenne, - Ring, + Accumulator as _, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field as _, JoltField, + PseudoMersenne, Ring, }; fn rng() -> ChaCha20Rng { @@ -196,11 +196,11 @@ macro_rules! check_prime { let n = <$two as CanonicalBytes>::NUM_BYTES; let p_bytes = &p.to_le_bytes()[..n]; assert_eq!( - <$two as CanonicalEncoding>::from_bytes_le_checked(p_bytes), + <$two as CanonicalDecode>::from_bytes_le_checked(p_bytes), None ); assert_eq!( - <$two as CanonicalEncoding>::from_bytes_le_checked(&p.to_le_bytes()[..n - 1]), + <$two as CanonicalDecode>::from_bytes_le_checked(&p.to_le_bytes()[..n - 1]), None, "wrong length rejected" ); diff --git a/crates/jolt-field/tests/spine.rs b/crates/jolt-field/tests/spine.rs index febf2d2e53..106aec726e 100644 --- a/crates/jolt-field/tests/spine.rs +++ b/crates/jolt-field/tests/spine.rs @@ -7,8 +7,8 @@ #![expect(clippy::unwrap_used, reason = "test code")] use jolt_field::{ - impl_ring_ops, impl_serde_bytes, Accumulator, CanonicalBytes, CanonicalEncoding, Field, - JoltField, NaiveAccumulator, One, Ring, WithAccumulator, Zero, + impl_ring_ops, impl_serde_bytes, Accumulator, CanonicalBytes, CanonicalDecode, + CanonicalEncoding, Field, JoltField, NaiveAccumulator, One, Ring, WithAccumulator, Zero, }; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -88,6 +88,13 @@ impl CanonicalBytes for M61 { } } +impl CanonicalDecode for M61 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; 8] = bytes.try_into().ok()?; + Self::from_u128_checked(u64::from_le_bytes(arr) as u128) + } +} + impl CanonicalEncoding for M61 { const MODULUS_BITS: u32 = 61; fn from_bytes_le_reduced(bytes: &[u8]) -> Self { @@ -97,10 +104,6 @@ impl CanonicalEncoding for M61 { .rev() .fold(M61(0), |acc, &b| acc * base + M61::from_u64(b as u64)) } - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - let arr: [u8; 8] = bytes.try_into().ok()?; - Self::from_u128_checked(u64::from_le_bytes(arr) as u128) - } fn to_u128_checked(&self) -> Option { Some(self.0 as u128) } diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index c4aad11897..b041713177 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -15,8 +15,8 @@ use std::{ }; use jolt_field::{ - AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, - WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, + Ring, WithAccumulator, }; use jolt_sumcheck::{ BooleanHypercube, ClearRound, EvaluationClaim, RoundMessage, SumcheckClaim, SumcheckVerifier, @@ -247,6 +247,13 @@ impl CanonicalBytes for Mersenne61 { } } +impl CanonicalDecode for Mersenne61 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + let arr: [u8; 8] = bytes.try_into().ok()?; + Self::from_u128_checked(u64::from_le_bytes(arr) as u128) + } +} + impl CanonicalEncoding for Mersenne61 { const MODULUS_BITS: u32 = 61; @@ -257,11 +264,6 @@ impl CanonicalEncoding for Mersenne61 { Self::from_u128(u128::from_le_bytes(buf)) } - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - let arr: [u8; 8] = bytes.try_into().ok()?; - Self::from_u128_checked(u64::from_le_bytes(arr) as u128) - } - fn to_u128_checked(&self) -> Option { Some(self.0 as u128) } diff --git a/zklean-extractor/src/mle_ast.rs b/zklean-extractor/src/mle_ast.rs index acdbea8bf8..4478fae8c8 100644 --- a/zklean-extractor/src/mle_ast.rs +++ b/zklean-extractor/src/mle_ast.rs @@ -18,8 +18,8 @@ use serde::{Deserialize, Serialize}; #[cfg(test)] use jolt_field::JoltField; use jolt_field::{ - AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, - WithAccumulator, + AdditiveGroup, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, + Ring, WithAccumulator, }; #[cfg(test)] @@ -1411,24 +1411,7 @@ impl CanonicalBytes for MleAst { } } -impl CanonicalEncoding for MleAst { - const MODULUS_BITS: u32 = 254; - - fn from_bytes_le_reduced(bytes: &[u8]) -> Self { - if let Some(challenge) = take_pending_challenge() { - return challenge; - } - - let value = BigUint::from_bytes_le(bytes) - % BigUint::from_bytes_le(&BN254_MODULUS.map(u64::to_le_bytes).concat()); - let digits = value.to_u64_digits(); - let mut limbs = [0u64; 4]; - for (dst, src) in limbs.iter_mut().zip(digits) { - *dst = src; - } - Self::new_scalar(limbs) - } - +impl CanonicalDecode for MleAst { fn from_bytes_le_checked(bytes: &[u8]) -> Option { if let Some(challenge) = take_pending_challenge() { return Some(challenge); @@ -1449,6 +1432,25 @@ impl CanonicalEncoding for MleAst { } Some(Self::new_scalar(limbs)) } +} + +impl CanonicalEncoding for MleAst { + const MODULUS_BITS: u32 = 254; + + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + if let Some(challenge) = take_pending_challenge() { + return challenge; + } + + let value = BigUint::from_bytes_le(bytes) + % BigUint::from_bytes_le(&BN254_MODULUS.map(u64::to_le_bytes).concat()); + let digits = value.to_u64_digits(); + let mut limbs = [0u64; 4]; + for (dst, src) in limbs.iter_mut().zip(digits) { + *dst = src; + } + Self::new_scalar(limbs) + } fn to_u128_checked(&self) -> Option { match get_node(self.root) { From d94a549e16398bd002595542686d367c036a1f83 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:27:16 -0400 Subject: [PATCH 03/62] feat(transcript): add the NARG prover/verifier channel 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. --- Cargo.lock | 2 + crates/jolt-transcript/Cargo.toml | 12 +- crates/jolt-transcript/src/channel.rs | 64 ++++ crates/jolt-transcript/src/duplex.rs | 144 +++++++++ crates/jolt-transcript/src/error.rs | 38 +++ crates/jolt-transcript/src/grinding.rs | 149 +++++++++ crates/jolt-transcript/src/lib.rs | 62 ++-- crates/jolt-transcript/src/preview.rs | 44 +++ crates/jolt-transcript/src/protocol.rs | 58 ++++ crates/jolt-transcript/src/prover.rs | 226 ++++++++++---- crates/jolt-transcript/src/site.rs | 113 +++++++ crates/jolt-transcript/src/sponge.rs | 29 ++ crates/jolt-transcript/src/verifier.rs | 284 ++++++++++++++---- crates/jolt-transcript/tests/narg.rs | 266 ++++++++++++++++ .../tests/native_traits_tests.rs | 46 --- 15 files changed, 1353 insertions(+), 184 deletions(-) create mode 100644 crates/jolt-transcript/src/channel.rs create mode 100644 crates/jolt-transcript/src/duplex.rs create mode 100644 crates/jolt-transcript/src/error.rs create mode 100644 crates/jolt-transcript/src/grinding.rs create mode 100644 crates/jolt-transcript/src/preview.rs create mode 100644 crates/jolt-transcript/src/protocol.rs create mode 100644 crates/jolt-transcript/src/site.rs create mode 100644 crates/jolt-transcript/src/sponge.rs create mode 100644 crates/jolt-transcript/tests/narg.rs delete mode 100644 crates/jolt-transcript/tests/native_traits_tests.rs diff --git a/Cargo.lock b/Cargo.lock index a3fb907763..cfab841730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3802,7 +3802,9 @@ dependencies = [ "light-poseidon", "num-traits", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.20", ] [[package]] diff --git a/crates/jolt-transcript/Cargo.toml b/crates/jolt-transcript/Cargo.toml index 7f3773f386..cfd995eb4f 100644 --- a/crates/jolt-transcript/Cargo.toml +++ b/crates/jolt-transcript/Cargo.toml @@ -13,13 +13,15 @@ workspace = true [features] default = ["transcript-blake2b", "transcript-keccak", "transcript-poseidon"] -spongefish = ["dep:rand", "dep:spongefish", "spongefish/sha3"] +# Records every transcript operation with its site and argument-string range. +logging = [] +spongefish = ["dep:rand"] digest = ["dep:digest"] bn254 = [ "dep:ark-bn254", "dep:ark-ff", "jolt-field/bn254", - "spongefish?/ark-ff", + "spongefish/ark-ff", ] transcript-blake2b = ["spongefish", "digest", "spongefish/blake2", "dep:blake2"] transcript-keccak = ["spongefish", "spongefish/keccak"] @@ -29,7 +31,11 @@ transcript-poseidon = ["spongefish", "bn254", "dep:light-poseidon"] ark-bn254 = { workspace = true, optional = true } ark-ff = { workspace = true, optional = true } light-poseidon = { workspace = true, optional = true } -spongefish = { workspace = true, optional = true } +# spongefish 0.7 references its sha3 `StdHash` even when another sponge is +# selected, so the feature is required to build at all. +spongefish = { workspace = true, features = ["sha3"] } +rand_core = { workspace = true } +thiserror = { workspace = true } blake2 = { workspace = true, optional = true } digest = { workspace = true, optional = true } jolt-field = { path = "../jolt-field", default-features = false } diff --git a/crates/jolt-transcript/src/channel.rs b/crates/jolt-transcript/src/channel.rs new file mode 100644 index 0000000000..75c304317d --- /dev/null +++ b/crates/jolt-transcript/src/channel.rs @@ -0,0 +1,64 @@ +//! Operations both transcript roles perform identically. + +use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; + +use crate::{Preview, SiteId, Sponge, TranscriptError}; + +/// One party's end of a Fiat-Shamir proof channel. +/// +/// Protocol code that is the same for prover and verifier (absorbing public +/// statements, drawing challenges, exchanging messages whose shape both sides +/// know) is written once against this trait. A prover message is exchanged in +/// place: the prover sends the value it holds, the verifier overwrites the +/// slot with the value it receives. +/// +/// Message shapes are positional. Neither role transmits lengths or labels for +/// atoms; both derive every count from public parameters. +pub trait Channel { + /// The sponge this transcript runs on. + type Sponge: Sponge; + + /// Tags subsequent operations with `site` in the event log. Never absorbed. + fn site(&mut self, site: SiteId); + + /// Absorbs a public atom both sides already hold. + fn public(&mut self, value: &A); + + /// Absorbs public atoms both sides already hold, in order. + fn public_all(&mut self, values: &[A]); + + /// Absorbs a public byte string, framed by its `u64` little-endian length. + fn public_bytes(&mut self, bytes: &[u8]); + + /// Sends (prover) or receives into (verifier) one prover message. + /// + /// # Errors + /// + /// The verifier fails when the argument string is exhausted or the bytes + /// are not a canonical encoding. The prover never fails. + fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError>; + + /// Sends or receives one prover message per slot, in order. + /// + /// # Errors + /// + /// As [`exchange`](Self::exchange). + fn exchange_all(&mut self, values: &mut [A]) + -> Result<(), TranscriptError>; + + /// Draws an exactly uniform element of `F`, per [`Field::random`]'s + /// rejection-sampling contract. + fn challenge(&mut self) -> F; + + /// Draws a challenge from `F`'s small challenge set, decoded from 16 + /// squeezed bytes by [`CanonicalEncoding::from_challenge_bytes`]. Cheaper to + /// multiply by than [`challenge`](Self::challenge); its soundness error is + /// set by the small set's size, not `|F|`. + fn challenge_small(&mut self) -> F; + + /// Squeezes `N` raw challenge bytes. + fn challenge_bytes(&mut self) -> [u8; N]; + + /// A detached copy of the current sponge state. + fn preview(&self) -> Preview; +} diff --git a/crates/jolt-transcript/src/duplex.rs b/crates/jolt-transcript/src/duplex.rs new file mode 100644 index 0000000000..fd5137c8e4 --- /dev/null +++ b/crates/jolt-transcript/src/duplex.rs @@ -0,0 +1,144 @@ +//! Sponge state shared by both transcript roles. + +use jolt_field::{CanonicalBytes, CanonicalEncoding, Field}; +use rand_core::RngCore; + +#[cfg(feature = "logging")] +use crate::site::TranscriptEvent; +use crate::site::{Log, TranscriptOp}; +use crate::{Preview, ProtocolId, SiteId, Sponge}; + +/// Bytes squeezed for a [`Channel::challenge_small`](crate::Channel::challenge_small). +pub const SMALL_CHALLENGE_BYTES: usize = 16; + +/// Width of the little-endian length prefix on framed public bytes. +const FRAME_LEN_BYTES: usize = 8; + +/// The sponge plus its event log. Both roles run every absorb and squeeze +/// through here, so their sponge transitions cannot drift apart. +#[derive(Clone, Debug)] +pub(crate) struct Duplex { + sponge: H, + log: Log, +} + +impl Duplex { + /// Absorbs the protocol id, then the length-framed session. + pub(crate) fn new(protocol: &ProtocolId, session: &[u8]) -> Self { + let mut sponge = H::default(); + let _ = sponge.absorb(protocol.as_bytes()); + absorb_framed(&mut sponge, session); + Self { + sponge, + log: Log::default(), + } + } + + pub(crate) fn set_site(&mut self, site: SiteId) { + self.log.set_site(site); + } + + pub(crate) fn absorb_public(&mut self, bytes: &[u8]) { + let _ = self.sponge.absorb(bytes); + self.log.record(TranscriptOp::Public, bytes.len(), None); + } + + pub(crate) fn absorb_public_atoms(&mut self, values: &[A]) { + let mut bytes = vec![0u8; A::NUM_BYTES * values.len()]; + for (value, out) in values.iter().zip(bytes.chunks_exact_mut(A::NUM_BYTES)) { + value.to_bytes_le(out); + } + self.absorb_public(&bytes); + } + + pub(crate) fn absorb_public_framed(&mut self, bytes: &[u8]) { + absorb_framed(&mut self.sponge, bytes); + self.log + .record(TranscriptOp::Public, FRAME_LEN_BYTES + bytes.len(), None); + } + + /// Absorbs prover-message bytes found at `narg_start` in the argument string. + pub(crate) fn absorb_message(&mut self, bytes: &[u8], narg_start: usize) { + let _ = self.sponge.absorb(bytes); + self.log.record( + TranscriptOp::Message, + bytes.len(), + Some(narg_start..narg_start + bytes.len()), + ); + } + + pub(crate) fn squeeze(&mut self, out: &mut [u8]) { + let _ = self.sponge.squeeze(out); + self.log.record(TranscriptOp::Challenge, out.len(), None); + } + + pub(crate) fn squeeze_array(&mut self) -> [u8; N] { + let mut out = [0u8; N]; + self.squeeze(&mut out); + out + } + + pub(crate) fn challenge(&mut self) -> F { + let mut rng = SqueezeRng { + sponge: &mut self.sponge, + squeezed: 0, + }; + let value = F::random(&mut rng); + let squeezed = rng.squeezed; + self.log.record(TranscriptOp::Challenge, squeezed, None); + value + } + + pub(crate) fn challenge_small(&mut self) -> F { + let mut bytes = [0u8; SMALL_CHALLENGE_BYTES]; + self.squeeze(&mut bytes); + F::from_challenge_bytes(&bytes) + } + + pub(crate) fn preview(&self) -> Preview { + Preview::new(self.sponge.clone()) + } + + #[cfg(feature = "logging")] + pub(crate) fn events(&self) -> &[TranscriptEvent] { + self.log.events() + } +} + +/// Absorbs `len(bytes)` as a `u64` little-endian prefix, then `bytes`, so +/// consecutive variable-length values cannot run into each other. +fn absorb_framed(sponge: &mut H, bytes: &[u8]) { + let _ = sponge.absorb(&(bytes.len() as u64).to_le_bytes()); + let _ = sponge.absorb(bytes); +} + +/// Feeds squeezed sponge output to [`Field::random`], whose rejection +/// sampling contract defines exactly uniform challenges. +struct SqueezeRng<'a, H> { + sponge: &'a mut H, + squeezed: usize, +} + +impl RngCore for SqueezeRng<'_, H> { + fn next_u32(&mut self) -> u32 { + let mut bytes = [0u8; 4]; + self.fill_bytes(&mut bytes); + u32::from_le_bytes(bytes) + } + + fn next_u64(&mut self) -> u64 { + let mut bytes = [0u8; 8]; + self.fill_bytes(&mut bytes); + u64::from_le_bytes(bytes) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let _ = self.sponge.squeeze(dest); + self.squeezed += dest.len(); + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } +} diff --git a/crates/jolt-transcript/src/error.rs b/crates/jolt-transcript/src/error.rs new file mode 100644 index 0000000000..da3e05ab92 --- /dev/null +++ b/crates/jolt-transcript/src/error.rs @@ -0,0 +1,38 @@ +//! Typed transcript failures. + +use thiserror::Error; + +/// Why a transcript operation failed. +/// +/// Every verifier-side variant means the proof is rejected. A +/// [`VerifierTranscript`](crate::VerifierTranscript) that has returned an error +/// is poisoned: later receives fail with [`TranscriptError::Poisoned`] and +/// [`finish`](crate::VerifierTranscript::finish) fails, so a caller that +/// swallows an error cannot go on to accept. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] +pub enum TranscriptError { + /// The argument string ended before a prover message. + #[error("proof ended before a prover message")] + Truncated, + /// Prover-message bytes are not the canonical encoding of any value. + #[error("prover message is not canonically encoded")] + NonCanonical, + /// A length, count, or nonce exceeds the bound the protocol states for it. + #[error("prover message exceeds its public bound")] + OutOfBounds, + /// Bytes remain after the protocol's last prover message. + #[error("proof has trailing bytes")] + TrailingBytes, + /// The proof-of-work predicate rejected the received nonce. + #[error("proof-of-work predicate rejected the nonce")] + GrindingRejected, + /// The requested proof-of-work difficulty is outside the supported range. + #[error("unsupported proof-of-work difficulty")] + UnsupportedGrinding, + /// No nonce in the bounded search range satisfied the predicate. + #[error("proof-of-work search exhausted its nonce range")] + GrindingExhausted, + /// An earlier receive failed, so this transcript can no longer accept. + #[error("transcript is poisoned by an earlier failure")] + Poisoned, +} diff --git a/crates/jolt-transcript/src/grinding.rs b/crates/jolt-transcript/src/grinding.rs new file mode 100644 index 0000000000..5b15107477 --- /dev/null +++ b/crates/jolt-transcript/src/grinding.rs @@ -0,0 +1,149 @@ +//! Proof of work and its nonce codec. +//! +//! A prover grinds `bits` bits by finding a nonce whose absorption makes the +//! next 32 squeezed bytes start with `bits` zero bits, low bit first. Nonces +//! are searched over `bits + GRINDING_NONCE_SLACK_BITS` bits, so an honest +//! search exhausts its range with probability about `exp(-2^7)`. + +use std::num::NonZeroU8; + +/// Extra nonce bits searched beyond the difficulty. +pub const GRINDING_NONCE_SLACK_BITS: u8 = 7; +/// Largest supported difficulty; the nonce range then fills a `u32`. +pub const MAX_GRINDING_BITS: u8 = u32::BITS as u8 - GRINDING_NONCE_SLACK_BITS; +/// Byte length of the squeezed proof-of-work predicate. +pub const GRINDING_PREDICATE_LEN: usize = 32; + +/// Largest encoded nonce: a `u32` in 7-bit groups. +const NONCE_MAX_BYTES: usize = 5; + +/// Returns whether the low `bits` bits of `predicate`, low byte first and low +/// bit first within each byte, are all zero. +#[must_use] +pub fn grinding_predicate_accepts( + predicate: &[u8; GRINDING_PREDICATE_LEN], + bits: NonZeroU8, +) -> bool { + let bits = usize::from(bits.get()); + let (whole, partial) = predicate.split_at(bits / 8); + let remaining = bits % 8; + whole.iter().all(|&byte| byte == 0) + && (remaining == 0 + || partial + .first() + .is_some_and(|&byte| byte & ((1u8 << remaining) - 1) == 0)) +} + +/// Nonce search width for difficulty `bits`. +pub(crate) fn nonce_bits(bits: NonZeroU8) -> Option { + (bits.get() <= MAX_GRINDING_BITS).then(|| bits.get() + GRINDING_NONCE_SLACK_BITS) +} + +/// The canonical encoding of a nonce: little-endian base-128 groups with a +/// continuation bit and no redundant trailing zero group. +pub(crate) struct NonceBytes { + bytes: [u8; NONCE_MAX_BYTES], + len: usize, +} + +impl NonceBytes { + pub(crate) fn as_slice(&self) -> &[u8] { + self.bytes.split_at(self.len).0 + } +} + +pub(crate) fn encode_nonce(mut value: u32) -> NonceBytes { + let mut encoded = NonceBytes { + bytes: [0; NONCE_MAX_BYTES], + len: 0, + }; + for slot in &mut encoded.bytes { + let mut byte = (value & 0x7f) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + *slot = byte; + encoded.len += 1; + if value == 0 { + break; + } + } + encoded +} + +/// Decodes a canonical nonce below `2^nonce_bits` from the front of `bytes`, +/// returning it with its encoded length. Rejects non-minimal encodings. +pub(crate) fn decode_nonce(bytes: &[u8], nonce_bits: u8) -> Option<(u32, usize)> { + let mut value = 0u64; + for (index, &byte) in bytes.iter().take(NONCE_MAX_BYTES).enumerate() { + let payload = byte & 0x7f; + value |= u64::from(payload) << (7 * index); + if byte & 0x80 == 0 { + if index != 0 && payload == 0 { + return None; + } + let value = u32::try_from(value).ok()?; + return (u64::from(value) >> nonce_bits == 0).then_some((value, index + 1)); + } + } + None +} + +/// Searches `0..2^nonce_bits` for the first nonce whose predicate accepts. +pub(crate) fn search_nonce( + bits: NonZeroU8, + nonce_bits: u8, + mut predicate_for: impl FnMut(u32) -> [u8; GRINDING_PREDICATE_LEN], +) -> Option { + let attempts = 1u64 << nonce_bits; + (0..attempts) + .filter_map(|candidate| u32::try_from(candidate).ok()) + .find(|&nonce| grinding_predicate_accepts(&predicate_for(nonce), bits)) +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "test code")] +mod tests { + use super::*; + + #[test] + fn predicate_checks_low_bits_at_byte_boundaries() { + for bits in [1, 7, 8, 9, 16, 17, MAX_GRINDING_BITS] { + let nonzero = NonZeroU8::new(bits).unwrap(); + let mut predicate = [0u8; GRINDING_PREDICATE_LEN]; + assert!(grinding_predicate_accepts(&predicate, nonzero)); + let last = usize::from(bits - 1); + predicate[last / 8] = 1 << (last % 8); + assert!(!grinding_predicate_accepts(&predicate, nonzero)); + predicate[last / 8] = 0; + let first_free = usize::from(bits); + predicate[first_free / 8] |= 1 << (first_free % 8); + assert!(grinding_predicate_accepts(&predicate, nonzero)); + } + } + + #[test] + fn nonce_codec_is_canonical() { + for value in [0, 1, 0x7f, 0x80, 0x3fff, 0x4000, u32::MAX] { + let encoded = encode_nonce(value); + assert_eq!( + decode_nonce(encoded.as_slice(), 32), + Some((value, encoded.as_slice().len())) + ); + } + assert_eq!( + decode_nonce(&[0x80, 0x00], 32), + None, + "redundant zero group" + ); + assert_eq!(decode_nonce(&[0x80], 32), None, "truncated"); + assert_eq!( + decode_nonce(&[0xff, 0xff, 0xff, 0xff, 0x1f], 32), + None, + "exceeds u32" + ); + assert_eq!(decode_nonce(&[0x80, 0x01], 7), None, "exceeds nonce_bits"); + assert_eq!(decode_nonce(&[0x7f], 7), Some((0x7f, 1))); + } +} diff --git a/crates/jolt-transcript/src/lib.rs b/crates/jolt-transcript/src/lib.rs index d80980fcac..52fc10f774 100644 --- a/crates/jolt-transcript/src/lib.rs +++ b/crates/jolt-transcript/src/lib.rs @@ -1,19 +1,20 @@ -//! Fiat-Shamir transcripts for Jolt, backed by spongefish. +//! Fiat-Shamir transcripts for Jolt in the NARG model. //! -//! Two surfaces: +//! A proof is its argument string. [`ProverTranscript`] appends every prover +//! message and absorbs exactly the appended bytes; [`VerifierTranscript`] +//! reads messages back and absorbs exactly the bytes it read. Code that both +//! roles run identically is written against [`Channel`]. Protocols compose by +//! sharing one transcript: a sub-protocol takes the caller's transcript rather +//! than starting its own. //! -//! - **Split spongefish-native traits** ([`ProverTranscript`], -//! [`VerifierTranscript`], [`OptimizedChallenge`]) — implemented directly -//! on `spongefish::ProverState` / `spongefish::VerifierState`. Use these -//! for new code. -//! - **Source-compatible facade** ([`Transcript`], [`AppendToTranscript`], -//! [`Blake2bTranscript`], [`KeccakTranscript`], [`PoseidonTranscript`]) — -//! preserved for `jolt-sumcheck`, `jolt-openings`, and `jolt-crypto` while -//! those crates migrate to the split-trait surface. +//! Message atoms are the [`CanonicalBytes`](jolt_field::CanonicalBytes) / +//! [`CanonicalDecode`](jolt_field::CanonicalDecode) codecs owned by each +//! type's crate. Challenges are exactly uniform ([`Channel::challenge`]) or +//! drawn from a field's small challenge set ([`Channel::challenge_small`]). +//! The sponge is a type parameter ([`Sponge`]) bound into the [`ProtocolId`]. //! -//! Three sponges feature-gated: `transcript-blake2b` (spongefish -//! `Blake2b512`), `transcript-keccak` (spongefish `Keccak`), -//! `transcript-poseidon` (local Circom-compatible BN254 [`PoseidonSponge`]). +//! The legacy symmetric facade ([`Transcript`], [`AppendToTranscript`], +//! [`DigestTranscript`]) remains until every consumer moves to the channel. #![deny(missing_docs)] // In the jolt-verifier runtime closure: stricter panic and unsafe discipline @@ -32,20 +33,45 @@ clippy::wildcard_enum_match_arm )] +mod channel; #[cfg(feature = "spongefish")] mod codec; #[cfg(feature = "digest")] mod digest; +mod duplex; +mod error; +mod grinding; mod legacy; #[cfg(feature = "transcript-poseidon")] mod poseidon; -#[cfg(feature = "spongefish")] +mod preview; +mod protocol; mod prover; #[cfg(feature = "spongefish")] mod setup; -#[cfg(feature = "spongefish")] +mod site; +mod sponge; mod verifier; +pub use channel::Channel; +pub use duplex::SMALL_CHALLENGE_BYTES; +pub use error::TranscriptError; +pub use grinding::{ + grinding_predicate_accepts, GRINDING_NONCE_SLACK_BITS, GRINDING_PREDICATE_LEN, + MAX_GRINDING_BITS, +}; +pub use preview::Preview; +pub use protocol::{ProtocolId, PROTOCOL_ID_LEN}; +pub use prover::ProverTranscript; +#[cfg(feature = "logging")] +pub use site::TranscriptEvent; +pub use site::{SiteId, TranscriptOp}; +pub use sponge::Sponge; +/// The duplex interface every [`Sponge`] implements; re-exported so a custom +/// sponge needs no direct spongefish dependency. +pub use spongefish::DuplexSpongeInterface; +pub use verifier::VerifierTranscript; + #[cfg(feature = "spongefish")] pub use codec::BytesMsg; #[cfg(feature = "digest")] @@ -68,12 +94,6 @@ pub mod domain { #[cfg(feature = "transcript-poseidon")] pub use poseidon::PoseidonSponge; -#[cfg(all(feature = "bn254", feature = "spongefish"))] -pub use prover::OptimizedChallenge; -#[cfg(feature = "spongefish")] -pub use prover::ProverTranscript; -#[cfg(feature = "spongefish")] -pub use verifier::VerifierTranscript; #[cfg(feature = "transcript-blake2b")] use blake2::{digest::consts::U32, Blake2b}; diff --git a/crates/jolt-transcript/src/preview.rs b/crates/jolt-transcript/src/preview.rs new file mode 100644 index 0000000000..6462403154 --- /dev/null +++ b/crates/jolt-transcript/src/preview.rs @@ -0,0 +1,44 @@ +//! Look-ahead over a copy of the public sponge state. + +use jolt_field::CanonicalBytes; + +use crate::grinding::encode_nonce; +use crate::Sponge; + +/// A detached copy of a transcript's sponge. +/// +/// A preview absorbs and squeezes exactly as the live transcript would, but it +/// holds no argument string and no reference to the transcript, so it cannot +/// change the proof or advance the live sponge. Proof-of-work searches use it +/// to test candidate nonces before committing the winner. +#[derive(Clone, Debug)] +pub struct Preview(H); + +impl Preview { + pub(crate) fn new(sponge: H) -> Self { + Self(sponge) + } + + /// Absorbs `value` as a prover message or public atom would be absorbed. + pub fn absorb(&mut self, value: &A) { + let _ = self.0.absorb(&value.to_bytes_le_vec()); + } + + /// Absorbs raw bytes, as an exact-length byte message would be absorbed. + pub fn absorb_bytes(&mut self, bytes: &[u8]) { + let _ = self.0.absorb(bytes); + } + + /// Absorbs `nonce` in the encoding [`send_nonce`](crate::ProverTranscript::send_nonce) uses. + pub fn absorb_nonce(&mut self, nonce: u32) { + let _ = self.0.absorb(encode_nonce(nonce).as_slice()); + } + + /// Squeezes `N` bytes, as a challenge would. + #[must_use] + pub fn squeeze(&mut self) -> [u8; N] { + let mut out = [0u8; N]; + let _ = self.0.squeeze(&mut out); + out + } +} diff --git a/crates/jolt-transcript/src/protocol.rs b/crates/jolt-transcript/src/protocol.rs new file mode 100644 index 0000000000..ec68c556d5 --- /dev/null +++ b/crates/jolt-transcript/src/protocol.rs @@ -0,0 +1,58 @@ +//! Domain separation: the identity a transcript starts from. + +use crate::Sponge; + +/// Byte width of a [`ProtocolId`]. +pub const PROTOCOL_ID_LEN: usize = 64; + +/// The 64-byte domain separator every transcript absorbs first. +/// +/// Encodes `len(name) || name || len(Sponge::ID) || Sponge::ID`, zero padded. +/// Both lengths are one byte, so the encoding is injective in `(name, sponge)`. +/// Callers put everything that must separate proofs into `name`: protocol, +/// version, and proof mode (for example `"jolt/v1/zk"`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ProtocolId([u8; PROTOCOL_ID_LEN]); + +impl ProtocolId { + /// Derives the identity of protocol `name` running on sponge `H`. + /// + /// # Panics + /// + /// Panics if `name` and `H::ID` do not fit in 62 bytes together. Evaluate + /// it in a `const` item to turn that into a compile error. + #[must_use] + #[expect( + clippy::indexing_slicing, + reason = "every index is below the asserted total length, which fits the buffer" + )] + pub const fn new(name: &str) -> Self { + let name = name.as_bytes(); + let sponge = H::ID.as_bytes(); + assert!( + name.len() + sponge.len() + 2 <= PROTOCOL_ID_LEN, + "protocol name and sponge id exceed the protocol id width" + ); + let mut id = [0u8; PROTOCOL_ID_LEN]; + id[0] = name.len() as u8; + let mut i = 0; + while i < name.len() { + id[1 + i] = name[i]; + i += 1; + } + let sponge_start = 1 + name.len(); + id[sponge_start] = sponge.len() as u8; + let mut j = 0; + while j < sponge.len() { + id[sponge_start + 1 + j] = sponge[j]; + j += 1; + } + Self(id) + } + + /// The encoded identity. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; PROTOCOL_ID_LEN] { + &self.0 + } +} diff --git a/crates/jolt-transcript/src/prover.rs b/crates/jolt-transcript/src/prover.rs index ca2a7da51f..ac3a7e621a 100644 --- a/crates/jolt-transcript/src/prover.rs +++ b/crates/jolt-transcript/src/prover.rs @@ -1,79 +1,185 @@ -//! Spongefish-native [`ProverTranscript`] surface. -//! -//! Implemented directly on `spongefish::ProverState` via the orphan -//! rule. Methods are positional, matching spongefish-native usage -//! (WhiR, sigma-rs). - -#[cfg(feature = "bn254")] -use jolt_field::Fr; -use rand::{CryptoRng, RngCore}; -use spongefish::{Decoding, DuplexSpongeInterface, Encoding, NargSerialize, ProverState}; - -/// Prover-side spongefish transcript. -/// -/// `H::U` is the sponge alphabet (`u8` for every sponge in this crate). -pub trait ProverTranscript { - /// Absorbs `msg` symmetrically with the verifier; emits no NARG bytes. - fn public_message + ?Sized>(&mut self, msg: &T); +//! The prover's end: absorb every message exactly as it is written. + +use std::num::NonZeroU8; - /// Absorbs `msg` and appends its NARG-serialized form for verifier replay. - fn prover_message + NargSerialize + ?Sized>(&mut self, msg: &T); +use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; - /// Squeezes a verifier challenge. - fn verifier_message>(&mut self) -> T; +use crate::duplex::Duplex; +use crate::grinding::{encode_nonce, nonce_bits, search_nonce, GRINDING_PREDICATE_LEN}; +#[cfg(feature = "logging")] +use crate::TranscriptEvent; +use crate::{Channel, Preview, ProtocolId, SiteId, Sponge, TranscriptError}; - /// Bytes accumulated in the NARG so far. - fn narg_string(&self) -> &[u8]; +/// Prover transcript: a sponge plus the argument string it writes. +/// +/// Every prover message is appended to the argument string and the appended +/// bytes are absorbed, so the bytes the verifier reads are exactly the bytes +/// that bound the following challenges. +#[derive(Clone, Debug)] +pub struct ProverTranscript { + duplex: Duplex, + narg: Vec, } -impl ProverTranscript for ProverState -where - H: DuplexSpongeInterface, - R: RngCore + CryptoRng, -{ - fn public_message + ?Sized>(&mut self, msg: &T) { - ProverState::public_message(self, msg); +impl ProverTranscript { + /// Starts a transcript for `protocol`, bound to `session`. + #[must_use] + pub fn new(protocol: &ProtocolId, session: &[u8]) -> Self { + Self { + duplex: Duplex::new(protocol, session), + narg: Vec::new(), + } } - fn prover_message + NargSerialize + ?Sized>(&mut self, msg: &T) { - ProverState::prover_message(self, msg); + /// Sends one atom. + pub fn send(&mut self, value: &A) { + let start = self.narg.len(); + self.narg.resize(start + A::NUM_BYTES, 0); + value.to_bytes_le(self.narg.split_at_mut(start).1); + self.absorb_written(start); } - fn verifier_message>(&mut self) -> T { - ProverState::verifier_message::(self) + /// Sends atoms in order. + pub fn send_all(&mut self, values: &[A]) { + let start = self.narg.len(); + self.narg.resize(start + A::NUM_BYTES * values.len(), 0); + for (value, out) in values.iter().zip( + self.narg + .split_at_mut(start) + .1 + .chunks_exact_mut(A::NUM_BYTES), + ) { + value.to_bytes_le(out); + } + self.absorb_written(start); } - fn narg_string(&self) -> &[u8] { - ProverState::narg_string(self) + /// Sends bytes whose length the verifier already knows. + pub fn send_bytes(&mut self, bytes: &[u8]) { + let start = self.narg.len(); + self.narg.extend_from_slice(bytes); + self.absorb_written(start); } -} -/// 128-bit-truncating challenge decoder. Implemented for sponges where the -/// optimization is sound (Blake2b, Keccak); deliberately not implemented -/// for [`PoseidonSponge`](crate::PoseidonSponge), so calling it on a -/// Poseidon-backed state is a compile error. -#[cfg(feature = "bn254")] -pub trait OptimizedChallenge { - /// Squeezes a 128-bit-truncated challenge as an [`Fr`]. - fn challenge_128(&mut self) -> Fr; -} + /// Sends a byte string of at most `max_len` bytes, prefixed by its `u32` + /// little-endian length. + /// + /// # Errors + /// + /// [`TranscriptError::OutOfBounds`] if `bytes` exceeds `max_len` or `u32`. + pub fn send_bounded_bytes( + &mut self, + bytes: &[u8], + max_len: usize, + ) -> Result<(), TranscriptError> { + if bytes.len() > max_len { + return Err(TranscriptError::OutOfBounds); + } + let len = u32::try_from(bytes.len()).map_err(|_| TranscriptError::OutOfBounds)?; + self.send(&len); + self.send_bytes(bytes); + Ok(()) + } + + /// Sends a nonce in its canonical variable-length encoding. + pub fn send_nonce(&mut self, nonce: u32) { + self.send_bytes(encode_nonce(nonce).as_slice()); + } + + /// Grinds `bits` bits of proof of work and returns the committed nonce. + /// A zero difficulty sends nothing. + /// + /// # Errors + /// + /// [`TranscriptError::UnsupportedGrinding`] above + /// [`MAX_GRINDING_BITS`](crate::MAX_GRINDING_BITS), and + /// [`TranscriptError::GrindingExhausted`] if no nonce in range succeeds. + pub fn grind(&mut self, bits: u8) -> Result { + let Some(bits) = NonZeroU8::new(bits) else { + return Ok(0); + }; + let nonce_bits = nonce_bits(bits).ok_or(TranscriptError::UnsupportedGrinding)?; + let nonce = search_nonce(bits, nonce_bits, |nonce| { + let mut preview = self.preview(); + preview.absorb_nonce(nonce); + preview.squeeze::() + }) + .ok_or(TranscriptError::GrindingExhausted)?; + self.send_nonce(nonce); + let _predicate: [u8; GRINDING_PREDICATE_LEN] = self.challenge_bytes(); + Ok(nonce) + } + + /// The argument string written so far. + #[must_use] + pub fn narg(&self) -> &[u8] { + &self.narg + } + + /// Recorded operations, in order. + #[cfg(feature = "logging")] + #[must_use] + pub fn events(&self) -> &[TranscriptEvent] { + self.duplex.events() + } -#[cfg(all(feature = "bn254", feature = "transcript-blake2b"))] -impl OptimizedChallenge for ProverState -where - R: RngCore + CryptoRng, -{ - fn challenge_128(&mut self) -> Fr { - Fr::from(ProverState::verifier_message::(self)) + /// Ends the transcript, returning the proof. + #[must_use] + pub fn finish(self) -> Vec { + self.narg + } + + fn absorb_written(&mut self, start: usize) { + let written = self.narg.split_at(start).1; + self.duplex.absorb_message(written, start); } } -#[cfg(all(feature = "bn254", feature = "transcript-keccak"))] -impl OptimizedChallenge for ProverState -where - R: RngCore + CryptoRng, -{ - fn challenge_128(&mut self) -> Fr { - Fr::from(ProverState::verifier_message::(self)) +impl Channel for ProverTranscript { + type Sponge = H; + + fn site(&mut self, site: SiteId) { + self.duplex.set_site(site); + } + + fn public(&mut self, value: &A) { + self.duplex.absorb_public_atoms(std::slice::from_ref(value)); + } + + fn public_all(&mut self, values: &[A]) { + self.duplex.absorb_public_atoms(values); + } + + fn public_bytes(&mut self, bytes: &[u8]) { + self.duplex.absorb_public_framed(bytes); + } + + fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError> { + self.send(value); + Ok(()) + } + + fn exchange_all( + &mut self, + values: &mut [A], + ) -> Result<(), TranscriptError> { + self.send_all(values); + Ok(()) + } + + fn challenge(&mut self) -> F { + self.duplex.challenge() + } + + fn challenge_small(&mut self) -> F { + self.duplex.challenge_small() + } + + fn challenge_bytes(&mut self) -> [u8; N] { + self.duplex.squeeze_array() + } + + fn preview(&self) -> Preview { + self.duplex.preview() } } diff --git a/crates/jolt-transcript/src/site.rs b/crates/jolt-transcript/src/site.rs new file mode 100644 index 0000000000..5dcade41ed --- /dev/null +++ b/crates/jolt-transcript/src/site.rs @@ -0,0 +1,113 @@ +//! Diagnostic protocol sites and the transcript event log. +//! +//! Sites never reach the sponge: a proof and its challenges are identical with +//! and without the `logging` feature. Under `logging`, each transcript records +//! one [`TranscriptEvent`] per operation, tagged with the site set by the most +//! recent [`Channel::site`](crate::Channel::site) call. + +/// Opaque 32-byte name of a protocol site, chosen by the protocol. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct SiteId(pub [u8; 32]); + +impl SiteId { + /// A site named by an ASCII label of at most 32 bytes, zero padded. + /// + /// # Panics + /// + /// Panics if `label` exceeds 32 bytes. Evaluate it in a `const` item to + /// turn that into a compile error. + #[must_use] + #[expect( + clippy::indexing_slicing, + reason = "the loop index is below label.len(), which is asserted to fit" + )] + pub const fn label(label: &str) -> Self { + let label = label.as_bytes(); + assert!(label.len() <= 32, "site label exceeds 32 bytes"); + let mut id = [0u8; 32]; + let mut i = 0; + while i < label.len() { + id[i] = label[i]; + i += 1; + } + Self(id) + } +} + +/// Kind of transcript operation recorded in the event log. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TranscriptOp { + /// Absorbed a public value. + Public, + /// Sent (prover) or received (verifier) a prover message. + Message, + /// Squeezed a verifier challenge. + Challenge, +} + +/// One recorded transcript operation. +#[cfg(feature = "logging")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TranscriptEvent { + /// Site active when the operation ran. + pub site: SiteId, + /// What the operation did. + pub op: TranscriptOp, + /// Bytes absorbed or squeezed. + pub len: usize, + /// Argument-string byte range of a prover message. + pub narg: Option>, +} + +/// Event log of one transcript; empty and free without the `logging` feature. +#[derive(Clone, Debug, Default)] +pub(crate) struct Log { + #[cfg(feature = "logging")] + site: SiteId, + #[cfg(feature = "logging")] + events: Vec, +} + +#[cfg(feature = "logging")] +impl Log { + pub(crate) fn set_site(&mut self, site: SiteId) { + self.site = site; + } + + pub(crate) fn record( + &mut self, + op: TranscriptOp, + len: usize, + narg: Option>, + ) { + self.events.push(TranscriptEvent { + site: self.site, + op, + len, + narg, + }); + } + + pub(crate) fn events(&self) -> &[TranscriptEvent] { + &self.events + } +} + +#[cfg(not(feature = "logging"))] +#[expect( + clippy::unused_self, + reason = "the event log compiles to nothing without the logging feature" +)] +impl Log { + #[inline(always)] + pub(crate) fn set_site(&mut self, _site: SiteId) {} + + #[inline(always)] + pub(crate) fn record( + &mut self, + _op: TranscriptOp, + _len: usize, + _narg: Option>, + ) { + } +} diff --git a/crates/jolt-transcript/src/sponge.rs b/crates/jolt-transcript/src/sponge.rs new file mode 100644 index 0000000000..6fec2ac528 --- /dev/null +++ b/crates/jolt-transcript/src/sponge.rs @@ -0,0 +1,29 @@ +//! Duplex sponges a transcript can run on. + +use spongefish::DuplexSpongeInterface; + +/// A byte-oriented duplex sponge with a stable identity. +/// +/// Protocol code is generic over `H: Sponge`; swapping the sponge is a change +/// of type argument. [`ID`](Self::ID) is bound into every +/// [`ProtocolId`](crate::ProtocolId), so a proof produced under one sponge +/// never verifies under another. +pub trait Sponge: DuplexSpongeInterface + Default + Clone + Send + Sync + 'static { + /// Stable name of this sponge construction. + const ID: &'static str; +} + +#[cfg(feature = "transcript-blake2b")] +impl Sponge for spongefish::instantiations::Blake2b512 { + const ID: &'static str = "blake2b512"; +} + +#[cfg(feature = "transcript-keccak")] +impl Sponge for spongefish::instantiations::Keccak { + const ID: &'static str = "keccak-f1600"; +} + +#[cfg(feature = "transcript-poseidon")] +impl Sponge for crate::PoseidonSponge { + const ID: &'static str = "poseidon-bn254-circom-t4"; +} diff --git a/crates/jolt-transcript/src/verifier.rs b/crates/jolt-transcript/src/verifier.rs index 37d7656798..edd2eda115 100644 --- a/crates/jolt-transcript/src/verifier.rs +++ b/crates/jolt-transcript/src/verifier.rs @@ -1,79 +1,255 @@ -//! Spongefish-native [`VerifierTranscript`] surface. - -#[cfg(all( - feature = "bn254", - any(feature = "transcript-blake2b", feature = "transcript-keccak") -))] -use jolt_field::Fr; -use spongefish::{ - Decoding, DuplexSpongeInterface, Encoding, NargDeserialize, VerificationResult, VerifierState, +//! The verifier's end: read every message from the argument string, absorb +//! exactly the bytes read. + +use std::num::NonZeroU8; + +use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; + +use crate::duplex::Duplex; +use crate::grinding::{ + decode_nonce, grinding_predicate_accepts, nonce_bits, GRINDING_PREDICATE_LEN, }; +#[cfg(feature = "logging")] +use crate::TranscriptEvent; +use crate::{Channel, Preview, ProtocolId, SiteId, Sponge, TranscriptError}; -#[cfg(all( - feature = "bn254", - any(feature = "transcript-blake2b", feature = "transcript-keccak") -))] -use crate::prover::OptimizedChallenge; +/// Verifier transcript: a sponge plus a cursor over the proof bytes. +/// +/// The only way to obtain proof data is a `receive*` call, which absorbs the +/// bytes it consumed. After any failed receive the transcript is poisoned: +/// later receives fail and [`finish`](Self::finish) fails. +#[derive(Clone, Debug)] +pub struct VerifierTranscript<'a, H> { + duplex: Duplex, + narg: &'a [u8], + consumed: usize, + poisoned: bool, +} -/// Verifier-side spongefish transcript. -pub trait VerifierTranscript { - /// Absorbs `msg` symmetrically with the prover. - fn public_message + ?Sized>(&mut self, msg: &T); +impl<'a, H: Sponge> VerifierTranscript<'a, H> { + /// Starts a transcript for `protocol`, bound to `session`, over the proof `narg`. + #[must_use] + pub fn new(protocol: &ProtocolId, session: &[u8], narg: &'a [u8]) -> Self { + Self { + duplex: Duplex::new(protocol, session), + narg, + consumed: 0, + poisoned: false, + } + } - /// Reads a prover message from the NARG, absorbing it into the sponge. - fn prover_message + NargDeserialize>(&mut self) -> VerificationResult; + /// Receives one atom. + /// + /// # Errors + /// + /// [`TranscriptError::Truncated`] or [`TranscriptError::NonCanonical`]. + pub fn receive(&mut self) -> Result { + let bytes = self.peek(A::NUM_BYTES)?; + let value = A::from_bytes_le_checked(bytes).ok_or(TranscriptError::NonCanonical); + let value = self.check(value)?; + self.advance(A::NUM_BYTES); + Ok(value) + } - /// Squeezes a verifier challenge. - fn verifier_message>(&mut self) -> T; + /// Receives `count` atoms. Checks that the proof holds them before allocating. + /// + /// # Errors + /// + /// [`TranscriptError::Truncated`] or [`TranscriptError::NonCanonical`]. + pub fn receive_n( + &mut self, + count: usize, + ) -> Result, TranscriptError> { + let len = A::NUM_BYTES + .checked_mul(count) + .ok_or(TranscriptError::Truncated); + let len = self.check(len)?; + let bytes = self.peek(len)?; + let values = bytes + .chunks_exact(A::NUM_BYTES) + .map(|chunk| A::from_bytes_le_checked(chunk).ok_or(TranscriptError::NonCanonical)) + .collect::, _>>(); + let values = self.check(values)?; + self.advance(len); + Ok(values) + } - /// Asserts the NARG was fully consumed. + /// Receives `len` bytes whose length the verifier already knows. /// - /// Soundness-critical: without this check, `valid_proof || garbage` - /// verifies as if the trailing bytes were absent, making the top-level - /// proof bytes malleable. Call once at the end of the top-level verify - /// function on the success path; error paths skip it (the proof is - /// already rejected). Composite verifiers follow the same rule at - /// their own finalize boundary. + /// # Errors /// - /// Takes `self` by value to prevent reuse. + /// [`TranscriptError::Truncated`]. + pub fn receive_bytes(&mut self, len: usize) -> Result<&'a [u8], TranscriptError> { + let bytes = self.peek(len)?; + self.advance(len); + Ok(bytes) + } + + /// Receives a length-prefixed byte string of at most `max_len` bytes, + /// checking the length before reading the body. /// /// # Errors /// - /// Returns `Err` if the NARG has unread bytes. - fn check_eof(self) -> VerificationResult<()>; -} + /// [`TranscriptError::OutOfBounds`] if the length exceeds `max_len`, else as + /// [`receive_bytes`](Self::receive_bytes). + pub fn receive_bounded_bytes(&mut self, max_len: usize) -> Result<&'a [u8], TranscriptError> { + let len = self.receive::()?; + let len = usize::try_from(len) + .ok() + .filter(|&len| len <= max_len) + .ok_or(TranscriptError::OutOfBounds); + let len = self.check(len)?; + self.receive_bytes(len) + } -impl VerifierTranscript for VerifierState<'_, H> -where - H: DuplexSpongeInterface, -{ - fn public_message + ?Sized>(&mut self, msg: &T) { - VerifierState::public_message(self, msg); + /// Receives a canonically encoded nonce below `2^nonce_bits`. + /// + /// # Errors + /// + /// [`TranscriptError::NonCanonical`] for a malformed, non-minimal, or + /// out-of-range encoding. + pub fn receive_nonce(&mut self, nonce_bits: u8) -> Result { + self.check(Ok(()))?; + let rest = self.narg.split_at(self.consumed).1; + let decoded = decode_nonce(rest, nonce_bits).ok_or(TranscriptError::NonCanonical); + let (nonce, len) = self.check(decoded)?; + self.advance(len); + Ok(nonce) } - fn prover_message + NargDeserialize>(&mut self) -> VerificationResult { - VerifierState::prover_message::(self) + /// Checks `bits` bits of proof of work and returns the received nonce. A + /// zero difficulty reads nothing. + /// + /// # Errors + /// + /// [`TranscriptError::UnsupportedGrinding`] above + /// [`MAX_GRINDING_BITS`](crate::MAX_GRINDING_BITS), + /// [`TranscriptError::GrindingRejected`] if the predicate fails, else as + /// [`receive_nonce`](Self::receive_nonce). + pub fn check_grind(&mut self, bits: u8) -> Result { + let Some(bits) = NonZeroU8::new(bits) else { + return Ok(0); + }; + let nonce_bits = nonce_bits(bits).ok_or(TranscriptError::UnsupportedGrinding); + let nonce_bits = self.check(nonce_bits)?; + let nonce = self.receive_nonce(nonce_bits)?; + let predicate: [u8; GRINDING_PREDICATE_LEN] = self.challenge_bytes(); + let accepted = if grinding_predicate_accepts(&predicate, bits) { + Ok(nonce) + } else { + Err(TranscriptError::GrindingRejected) + }; + self.check(accepted) } - fn verifier_message>(&mut self) -> T { - VerifierState::verifier_message::(self) + /// Bytes of the proof not yet received. + #[must_use] + pub fn remaining(&self) -> usize { + self.narg.len() - self.consumed } - fn check_eof(self) -> VerificationResult<()> { - VerifierState::check_eof(self) + /// Recorded operations, in order. + #[cfg(feature = "logging")] + #[must_use] + pub fn events(&self) -> &[TranscriptEvent] { + self.duplex.events() } -} -#[cfg(all(feature = "bn254", feature = "transcript-blake2b"))] -impl OptimizedChallenge for VerifierState<'_, spongefish::instantiations::Blake2b512> { - fn challenge_128(&mut self) -> Fr { - Fr::from(VerifierState::verifier_message::(self)) + /// Ends the transcript. Call once, at the outermost verifier boundary, + /// after the protocol's last message. + /// + /// # Errors + /// + /// [`TranscriptError::Poisoned`] after an earlier failure, and + /// [`TranscriptError::TrailingBytes`] if proof bytes remain: without this + /// check `proof || garbage` would verify. + pub fn finish(self) -> Result<(), TranscriptError> { + if self.poisoned { + Err(TranscriptError::Poisoned) + } else if self.remaining() != 0 { + Err(TranscriptError::TrailingBytes) + } else { + Ok(()) + } + } + + /// The next `len` unread bytes, without consuming them. + fn peek(&mut self, len: usize) -> Result<&'a [u8], TranscriptError> { + self.check(Ok(()))?; + let narg = self.narg; + let bytes = narg + .get(self.consumed..) + .and_then(|rest| rest.get(..len)) + .ok_or(TranscriptError::Truncated); + self.check(bytes) + } + + /// Consumes and absorbs the next `len` bytes, which `peek` already bounded. + fn advance(&mut self, len: usize) { + let narg = self.narg; + let (_, rest) = narg.split_at(self.consumed); + let (bytes, _) = rest.split_at(len); + self.duplex.absorb_message(bytes, self.consumed); + self.consumed += len; + } + + /// Poisons the transcript on failure; fails immediately once poisoned. + fn check(&mut self, result: Result) -> Result { + if self.poisoned { + return Err(TranscriptError::Poisoned); + } + result.inspect_err(|_| self.poisoned = true) } } -#[cfg(all(feature = "bn254", feature = "transcript-keccak"))] -impl OptimizedChallenge for VerifierState<'_, spongefish::instantiations::Keccak> { - fn challenge_128(&mut self) -> Fr { - Fr::from(VerifierState::verifier_message::(self)) +impl Channel for VerifierTranscript<'_, H> { + type Sponge = H; + + fn site(&mut self, site: SiteId) { + self.duplex.set_site(site); + } + + fn public(&mut self, value: &A) { + self.duplex.absorb_public_atoms(std::slice::from_ref(value)); + } + + fn public_all(&mut self, values: &[A]) { + self.duplex.absorb_public_atoms(values); + } + + fn public_bytes(&mut self, bytes: &[u8]) { + self.duplex.absorb_public_framed(bytes); + } + + fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError> { + *value = self.receive()?; + Ok(()) + } + + fn exchange_all( + &mut self, + values: &mut [A], + ) -> Result<(), TranscriptError> { + let received = self.receive_n(values.len())?; + for (slot, value) in values.iter_mut().zip(received) { + *slot = value; + } + Ok(()) + } + + fn challenge(&mut self) -> F { + self.duplex.challenge() + } + + fn challenge_small(&mut self) -> F { + self.duplex.challenge_small() + } + + fn challenge_bytes(&mut self) -> [u8; N] { + self.duplex.squeeze_array() + } + + fn preview(&self) -> Preview { + self.duplex.preview() } } diff --git a/crates/jolt-transcript/tests/narg.rs b/crates/jolt-transcript/tests/narg.rs new file mode 100644 index 0000000000..3676b32f08 --- /dev/null +++ b/crates/jolt-transcript/tests/narg.rs @@ -0,0 +1,266 @@ +//! Prover/verifier symmetry, rejection, and wire-format pins for the NARG +//! transcript. + +#![cfg(all( + feature = "bn254", + feature = "transcript-blake2b", + feature = "transcript-keccak", + feature = "transcript-poseidon" +))] +#![expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "tests")] + +use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr, Ring}; +use jolt_transcript::{ + Channel, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, TranscriptError, + VerifierTranscript, SMALL_CHALLENGE_BYTES, +}; +use spongefish::instantiations::{Blake2b512, Keccak}; + +const SESSION: &[u8] = b"narg-tests"; + +fn protocol() -> ProtocolId { + ProtocolId::new::("jolt-transcript/tests") +} + +/// A toy protocol exercising every message kind. The verifier's only +/// acceptance check is the final product, so tampering is detected only +/// through the challenges the tampered bytes feed. +fn prove() -> Vec { + let mut prover = ProverTranscript::::new(&protocol::(), SESSION); + prover.public(&Fr::from_u64(11)); + prover.public_bytes(b"public statement"); + let x = Fr::from_u64(5); + prover.send(&x); + prover.send_all(&[Fr::from_u64(6), Fr::from_u64(7)]); + prover.send_bytes(&[1, 2, 3]); + prover.send_bounded_bytes(&[9; 10], 16).unwrap(); + let r: Fr = prover.challenge(); + let s: Fr = prover.challenge_small(); + let _nonce = prover.grind(6).unwrap(); + prover.send(&(x * r + s)); + prover.finish() +} + +fn verify(narg: &[u8]) -> Result<(), TranscriptError> { + let mut verifier = VerifierTranscript::::new(&protocol::(), SESSION, narg); + verifier.public(&Fr::from_u64(11)); + verifier.public_bytes(b"public statement"); + let x: Fr = verifier.receive()?; + let _pair: Vec = verifier.receive_n(2)?; + let _bytes = verifier.receive_bytes(3)?; + let _bounded = verifier.receive_bounded_bytes(16)?; + let r: Fr = verifier.challenge(); + let s: Fr = verifier.challenge_small(); + let _nonce = verifier.check_grind(6)?; + let y: Fr = verifier.receive()?; + if y != x * r + s { + return Err(TranscriptError::NonCanonical); + } + verifier.finish() +} + +fn for_each_sponge( + check: impl Fn(&dyn Fn() -> Vec, &dyn Fn(&[u8]) -> Result<(), TranscriptError>), +) { + check(&prove::, &|narg| verify::(narg)); + check(&prove::, &|narg| verify::(narg)); + check(&prove::, &|narg| { + verify::(narg) + }); +} + +#[test] +fn honest_proof_verifies() { + for_each_sponge(|prove, verify| assert_eq!(verify(&prove()), Ok(()))); +} + +#[test] +fn every_byte_flip_rejects() { + for_each_sponge(|prove, verify| { + let narg = prove(); + for index in 0..narg.len() { + let mut tampered = narg.clone(); + tampered[index] ^= 1; + assert!(verify(&tampered).is_err(), "flip at byte {index} accepted"); + } + }); +} + +#[test] +fn trailing_and_truncated_proofs_reject() { + for_each_sponge(|prove, verify| { + let mut narg = prove(); + narg.push(0); + assert_eq!(verify(&narg), Err(TranscriptError::TrailingBytes)); + narg.truncate(narg.len() - 2); + assert_eq!(verify(&narg), Err(TranscriptError::Truncated)); + }); +} + +#[test] +fn sponges_and_protocol_names_separate_domains() { + fn first_challenge(protocol: &ProtocolId, session: &[u8]) -> Fr { + ProverTranscript::::new(protocol, session).challenge() + } + let blake = first_challenge::(&protocol::(), SESSION); + assert_ne!( + blake, + first_challenge::(&protocol::(), SESSION) + ); + assert_ne!( + blake, + first_challenge::(&ProtocolId::new::("other"), SESSION) + ); + assert_ne!( + blake, + first_challenge::(&protocol::(), b"other") + ); + assert_ne!( + ProtocolId::new::("ab").as_bytes(), + ProtocolId::new::("a").as_bytes() + ); +} + +#[test] +fn public_bytes_are_framed() { + let protocol = protocol::(); + let mut split = ProverTranscript::::new(&protocol, SESSION); + split.public_bytes(b"ab"); + split.public_bytes(b"c"); + let mut joined = ProverTranscript::::new(&protocol, SESSION); + joined.public_bytes(b"a"); + joined.public_bytes(b"bc"); + assert_ne!( + split.challenge_bytes::<32>(), + joined.challenge_bytes::<32>() + ); +} + +#[test] +fn failed_receive_poisons_without_consuming() { + let mut prover = ProverTranscript::::new(&protocol::(), SESSION); + prover.send(&7u64); + let narg = prover.finish(); + let mut verifier = + VerifierTranscript::::new(&protocol::(), SESSION, &narg); + assert_eq!(verifier.receive::(), Err(TranscriptError::Truncated)); + assert_eq!(verifier.remaining(), narg.len()); + assert_eq!(verifier.receive::(), Err(TranscriptError::Poisoned)); + assert_eq!(verifier.finish(), Err(TranscriptError::Poisoned)); +} + +#[test] +fn non_canonical_field_element_rejects() { + let modulus_bytes = { + let minus_one = -Fr::from_u64(1); + let mut bytes = minus_one.to_bytes_le_vec(); + bytes[0] += 1; + bytes + }; + let mut verifier = + VerifierTranscript::::new(&protocol::(), SESSION, &modulus_bytes); + assert_eq!(verifier.receive::(), Err(TranscriptError::NonCanonical)); +} + +#[test] +fn bounds_are_checked_before_reading() { + let mut prover = ProverTranscript::::new(&protocol::(), SESSION); + assert_eq!( + prover.send_bounded_bytes(&[0; 5], 4), + Err(TranscriptError::OutOfBounds) + ); + prover.send_bounded_bytes(&[0; 5], 5).unwrap(); + let narg = prover.finish(); + let mut verifier = + VerifierTranscript::::new(&protocol::(), SESSION, &narg); + assert_eq!( + verifier.receive_bounded_bytes(4), + Err(TranscriptError::OutOfBounds) + ); + + let mut verifier = + VerifierTranscript::::new(&protocol::(), SESSION, &narg); + assert_eq!( + verifier.receive_n::(usize::MAX / Fr::NUM_BYTES), + Err(TranscriptError::Truncated) + ); +} + +#[test] +fn preview_does_not_advance_the_transcript() { + let mut prover = ProverTranscript::::new(&protocol::(), SESSION); + let mut untouched = prover.clone(); + let mut preview = prover.preview(); + preview.absorb(&Fr::from_u64(1)); + let _ = preview.squeeze::<32>(); + assert_eq!( + prover.challenge_bytes::<32>(), + untouched.challenge_bytes::<32>() + ); + assert!(prover.narg().is_empty()); +} + +#[test] +fn grinding_rejects_a_stronger_claim_and_unsupported_difficulty() { + let protocol = protocol::(); + let mut prover = ProverTranscript::::new(&protocol, SESSION); + let nonce = prover.grind(4).unwrap(); + let narg = prover.finish(); + + let mut verifier = VerifierTranscript::::new(&protocol, SESSION, &narg); + assert_eq!(verifier.check_grind(4), Ok(nonce)); + assert_eq!(verifier.finish(), Ok(())); + + let mut prover = ProverTranscript::::new(&protocol, SESSION); + assert_eq!(prover.grind(26), Err(TranscriptError::UnsupportedGrinding)); + assert_eq!(prover.grind(0), Ok(0)); + assert!(prover.narg().is_empty()); +} + +#[test] +fn small_challenge_decodes_the_next_squeezed_bytes() { + let mut prover = ProverTranscript::::new(&protocol::(), SESSION); + for _ in 0..8 { + let expected = + Fr::from_challenge_bytes(&prover.preview().squeeze::()); + assert_eq!(prover.challenge_small::(), expected); + } +} + +#[test] +fn exchange_is_send_for_the_prover_and_receive_for_the_verifier() { + fn shared(channel: &mut C, value: &mut Fr) -> Fr { + channel.exchange(value).unwrap(); + channel.challenge() + } + let protocol = protocol::(); + let mut prover = ProverTranscript::::new(&protocol, SESSION); + let prover_challenge = shared(&mut prover, &mut Fr::from_u64(42)); + let narg = prover.finish(); + let mut verifier = VerifierTranscript::::new(&protocol, SESSION, &narg); + let mut slot = Fr::from_u64(0); + assert_eq!(shared(&mut verifier, &mut slot), prover_challenge); + assert_eq!(slot, Fr::from_u64(42)); + assert_eq!(verifier.finish(), Ok(())); +} + +#[cfg(feature = "logging")] +#[test] +fn prover_and_verifier_log_identical_sites() { + use jolt_transcript::SiteId; + let protocol = protocol::(); + let mut prover = ProverTranscript::::new(&protocol, SESSION); + prover.site(SiteId::label("statement")); + prover.public(&Fr::from_u64(3)); + prover.site(SiteId::label("message")); + prover.send(&Fr::from_u64(4)); + let _: Fr = prover.challenge(); + let mut verifier = VerifierTranscript::::new(&protocol, SESSION, prover.narg()); + verifier.site(SiteId::label("statement")); + verifier.public(&Fr::from_u64(3)); + verifier.site(SiteId::label("message")); + let _: Fr = verifier.receive().unwrap(); + let _: Fr = verifier.challenge(); + assert_eq!(prover.events(), verifier.events()); + assert_eq!(prover.events()[1].narg, Some(0..32)); +} diff --git a/crates/jolt-transcript/tests/native_traits_tests.rs b/crates/jolt-transcript/tests/native_traits_tests.rs deleted file mode 100644 index 790cc0b99f..0000000000 --- a/crates/jolt-transcript/tests/native_traits_tests.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Native transcript domain separation and challenge bounds. - -#![cfg(all(feature = "bn254", feature = "transcript-blake2b"))] - -use jolt_field::{CanonicalBytes, Fr}; -use jolt_transcript::{prover_transcript, verifier_transcript, OptimizedChallenge}; -use spongefish::instantiations::Blake2b512; - -const SESSION: &[u8] = b"native-traits"; -const INSTANCE: [u8; 32] = [0x77; 32]; - -#[test] -fn optimized_challenge_is_128_bit_truncated() { - let mut prover = prover_transcript(SESSION, INSTANCE, Blake2b512::default()); - let mut verifier = verifier_transcript(SESSION, INSTANCE, Blake2b512::default(), &[]); - - for challenge in [prover.challenge_128(), verifier.challenge_128()] { - let mut bytes = [0u8; 32]; - challenge.to_bytes_le(&mut bytes); - assert!(bytes.iter().skip(16).all(|&byte| byte == 0)); - } -} - -#[test] -fn distinct_sessions_diverge() { - let mut prover_a = prover_transcript(b"a", INSTANCE, Blake2b512::default()); - let mut prover_b = prover_transcript(b"b", INSTANCE, Blake2b512::default()); - let a: Fr = prover_a.challenge_128(); - let b: Fr = prover_b.challenge_128(); - assert_ne!(a, b); - - let mut verifier_a = verifier_transcript(b"a", INSTANCE, Blake2b512::default(), &[]); - let mut verifier_b = verifier_transcript(b"b", INSTANCE, Blake2b512::default(), &[]); - assert_ne!(verifier_a.challenge_128(), verifier_b.challenge_128()); -} - -#[test] -fn distinct_instances_diverge() { - let mut prover_a = prover_transcript(SESSION, [0x11; 32], Blake2b512::default()); - let mut prover_b = prover_transcript(SESSION, [0x22; 32], Blake2b512::default()); - assert_ne!(prover_a.challenge_128(), prover_b.challenge_128()); - - let mut verifier_a = verifier_transcript(SESSION, [0x11; 32], Blake2b512::default(), &[]); - let mut verifier_b = verifier_transcript(SESSION, [0x22; 32], Blake2b512::default(), &[]); - assert_ne!(verifier_a.challenge_128(), verifier_b.challenge_128()); -} From df2b3934024219389f4337c58369bf3f15320106 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:30:23 -0400 Subject: [PATCH 04/62] refactor(crypto)!: encode group elements as canonical transcript atoms 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. --- Cargo.lock | 1 - crates/jolt-crypto/Cargo.toml | 5 -- crates/jolt-crypto/fuzz/Cargo.toml | 5 +- crates/jolt-crypto/src/commitment.rs | 11 ++- crates/jolt-crypto/src/ec/bn254/g1.rs | 1 + crates/jolt-crypto/src/ec/bn254/g2.rs | 1 + crates/jolt-crypto/src/ec/bn254/gt.rs | 87 +++++++++---------- crates/jolt-crypto/src/ec/bn254/mod.rs | 114 ++++++++++++------------- crates/jolt-crypto/src/ec/group.rs | 11 +-- 9 files changed, 115 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfab841730..88db9063ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3218,7 +3218,6 @@ dependencies = [ "criterion", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint 0.4.6", "num-integer", "num-traits", diff --git a/crates/jolt-crypto/Cargo.toml b/crates/jolt-crypto/Cargo.toml index d773fb9a1b..a1e92313d4 100644 --- a/crates/jolt-crypto/Cargo.toml +++ b/crates/jolt-crypto/Cargo.toml @@ -36,7 +36,6 @@ parallel = ["dep:rayon", "jolt-poly/parallel"] [dependencies] jolt-field = { path = "../jolt-field", default-features = false } jolt-poly = { path = "../jolt-poly", default-features = false } -jolt-transcript = { path = "../jolt-transcript", default-features = false } serde = { workspace = true, features = ["derive", "alloc"] } rand_core = { workspace = true } @@ -57,10 +56,6 @@ rand_chacha = { workspace = true } serde_json = { workspace = true, features = ["std"] } bincode = { workspace = true } criterion = { workspace = true } -jolt-transcript = { workspace = true, features = [ - "bn254", - "transcript-blake2b", -] } [[test]] name = "coverage" diff --git a/crates/jolt-crypto/fuzz/Cargo.toml b/crates/jolt-crypto/fuzz/Cargo.toml index d111de49f3..e6568a65d0 100644 --- a/crates/jolt-crypto/fuzz/Cargo.toml +++ b/crates/jolt-crypto/fuzz/Cargo.toml @@ -1,9 +1,8 @@ [workspace] # The fuzz sub-workspace does not inherit the root workspace's `[patch.crates-io]`. -# `light-poseidon` (via jolt-transcript) pulls unpatched `ark-ff` from crates.io; -# jolt-crypto's types are built against the patched fork. Apply the same patches -# here so both sides see the same `ark-ff`. +# jolt-crypto's types are built against the patched arkworks fork; apply the same +# patches here so every crate sees the same `ark-ff`. [patch.crates-io] ark-bn254 = { git = "https://github.com/a16z/arkworks-algebra", branch = "dev/twist-shout" } ark-ec = { git = "https://github.com/a16z/arkworks-algebra", branch = "dev/twist-shout" } diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index b30ae0ac56..f1b5d8558b 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -4,9 +4,8 @@ use std::{ marker::PhantomData, }; -use jolt_field::{Accumulator, CanonicalBytes, JoltField, WithAccumulator}; +use jolt_field::{Accumulator, CanonicalBytes, CanonicalDecode, JoltField, WithAccumulator}; use jolt_poly::EqPolynomial; -use jolt_transcript::AppendToTranscript; use serde::{de::DeserializeOwned, Deserialize, Serialize}; #[cfg(feature = "parallel")] @@ -56,6 +55,12 @@ impl CanonicalBytes for NoCommitment { fn to_bytes_le(&self, _out: &mut [u8]) {} } +impl CanonicalDecode for NoCommitment { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + bytes.is_empty().then_some(Self) + } +} + impl HomomorphicCommitment for NoCommitment { fn add(_c1: &Self, _c2: &Self) -> Self { Self @@ -106,7 +111,7 @@ impl VectorCommitment for NoVectorCommitment { /// elements with a blinding factor. Uses `Self::Output` from the supertrait /// as the commitment value type. pub trait VectorCommitment: - Commitment + Commitment { type Field: JoltField; diff --git a/crates/jolt-crypto/src/ec/bn254/g1.rs b/crates/jolt-crypto/src/ec/bn254/g1.rs index aa12921381..70709b44b7 100644 --- a/crates/jolt-crypto/src/ec/bn254/g1.rs +++ b/crates/jolt-crypto/src/ec/bn254/g1.rs @@ -4,5 +4,6 @@ super::impl_jolt_group_wrapper!( Bn254G1, G1Projective, G1Affine, + 32, "BN254 G1 group element (projective coordinates)." ); diff --git a/crates/jolt-crypto/src/ec/bn254/g2.rs b/crates/jolt-crypto/src/ec/bn254/g2.rs index c790b959a3..cdb6f57098 100644 --- a/crates/jolt-crypto/src/ec/bn254/g2.rs +++ b/crates/jolt-crypto/src/ec/bn254/g2.rs @@ -4,6 +4,7 @@ super::impl_jolt_group_wrapper!( Bn254G2, G2Projective, G2Affine, + 64, "BN254 G2 group element (projective coordinates)." ); diff --git a/crates/jolt-crypto/src/ec/bn254/gt.rs b/crates/jolt-crypto/src/ec/bn254/gt.rs index eaf2dc9033..c895db06ab 100644 --- a/crates/jolt-crypto/src/ec/bn254/gt.rs +++ b/crates/jolt-crypto/src/ec/bn254/gt.rs @@ -4,9 +4,7 @@ use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use ark_bn254::{Fq12, Fr}; use ark_ff::{AdditiveGroup, Field as ArkField, PrimeField}; use ark_serialize::{CanonicalSerialize, Compress, SerializationError, Write}; -use jolt_field::JoltField; - -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField}; use crate::JoltGroup; @@ -150,24 +148,6 @@ impl MulAssign for Bn254GT { } } -#[expect(clippy::expect_used)] -impl AppendToTranscript for Bn254GT { - fn append_to_transcript(&self, transcript: &mut T) { - use ark_serialize::CanonicalSerialize; - let mut buf = Vec::with_capacity(self.0.uncompressed_size()); - self.0 - .serialize_uncompressed(&mut buf) - .expect("GT serialization cannot fail"); - buf.reverse(); - transcript.append_bytes(&buf); - } - - fn transcript_payload_len(&self) -> Option { - use ark_serialize::CanonicalSerialize; - Some(self.0.uncompressed_size() as u64) - } -} - impl JoltGroup for Bn254GT { #[inline(always)] fn identity() -> Self { @@ -211,35 +191,34 @@ impl JoltGroup for Bn254GT { impl serde::Serialize for Bn254GT { fn serialize(&self, serializer: S) -> Result { - use ark_serialize::CanonicalSerialize; - let mut buf = Vec::with_capacity(self.0.compressed_size()); - self.0 - .serialize_compressed(&mut buf) - .map_err(serde::ser::Error::custom)?; - serializer.serialize_bytes(&buf) + serializer.serialize_bytes(&self.to_bytes_le_vec()) } } impl<'de> serde::Deserialize<'de> for Bn254GT { fn deserialize>(deserializer: D) -> Result { - use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; let buf = >::deserialize(deserializer)?; - // Exact-size gate: rejects oversized/truncated payloads before any - // field parsing and makes the encoding canonical (no trailing bytes). - let expected_len = Fq12::ONE.compressed_size(); - if buf.len() != expected_len { - return Err(serde::de::Error::custom(format!( + Self::decode_compressed(&buf).map_err(serde::de::Error::custom) + } +} + +impl Bn254GT { + /// Decodes exactly one compressed `Fq12` element and checks that it lies + /// in the r-torsion subgroup GT. + fn decode_compressed(bytes: &[u8]) -> Result { + use ark_serialize::CanonicalDeserialize; + let expected_len = ::NUM_BYTES; + if bytes.len() != expected_len { + return Err(format!( "GT element encoding must be exactly {expected_len} bytes, got {}", - buf.len() - ))); + bytes.len() + )); } - let inner = Fq12::deserialize_compressed(&buf[..]).map_err(serde::de::Error::custom)?; + let inner = Fq12::deserialize_compressed(bytes).map_err(|error| error.to_string())?; // Reject Fq12::ZERO: not in any multiplicative subgroup, and later // Neg/Sub/SubAssign would call .inverse().expect(...) and panic. if inner == Fq12::ZERO { - return Err(serde::de::Error::custom( - "GT element is zero (not in r-torsion subgroup)", - )); + return Err("GT element is zero (not in r-torsion subgroup)".to_owned()); } // Unitarity pre-filter: GT ⊂ the norm-1 (unitary) subgroup of Fq12 // over Fq6, i.e. x^(q^6+1) = conj(x)·x = 1. A non-GT Fq12 element is @@ -250,21 +229,39 @@ impl<'de> serde::Deserialize<'de> for Bn254GT { let mut conj = inner; let _ = conj.conjugate_in_place(); if conj * inner != Fq12::ONE { - return Err(serde::de::Error::custom( - "GT element is not unitary (not in the r-torsion subgroup)", - )); + return Err("GT element is not unitary (not in the r-torsion subgroup)".to_owned()); } // Subgroup membership: GT is the r-torsion subgroup, so x^r == 1. // Unitarity is necessary but not sufficient; this check is exact. if inner.pow(Fr::MODULUS) != Fq12::ONE { - return Err(serde::de::Error::custom( - "GT element is not in the r-torsion subgroup", - )); + return Err("GT element is not in the r-torsion subgroup".to_owned()); } Ok(Self(inner)) } } +/// The compressed arkworks `Fq12` encoding. +impl CanonicalBytes for Bn254GT { + const NUM_BYTES: usize = 384; + + #[expect( + clippy::expect_used, + reason = "serializing into an exact-size buffer cannot fail" + )] + fn to_bytes_le(&self, out: &mut [u8]) { + assert_eq!(out.len(), Self::NUM_BYTES); + self.0 + .serialize_compressed(out) + .expect("GT serialization cannot fail"); + } +} + +impl CanonicalDecode for Bn254GT { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Self::decode_compressed(bytes).ok() + } +} + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "tests may fail loudly")] mod tests { diff --git a/crates/jolt-crypto/src/ec/bn254/mod.rs b/crates/jolt-crypto/src/ec/bn254/mod.rs index a3268f0eef..2d236f29d9 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -5,14 +5,14 @@ //! the public API — all conversions happen internally. /// Generates a `#[repr(transparent)]` wrapper over an arkworks projective curve type, -/// with all operator impls, serde, `AppendToTranscript`, `JoltGroup`, compile-time -/// size assertions, and a safe `into_inner` accessor. +/// with all operator impls, serde, the canonical compressed transcript codec, +/// `JoltGroup`, compile-time size assertions, and a safe `into_inner` accessor. /// /// Paths are fully qualified so the macro does not inject `use` items into the caller's /// module scope — callers can expand the macro multiple times in the same module or /// alongside unrelated imports without conflicts. macro_rules! impl_jolt_group_wrapper { - ($wrapper:ident, $projective:ty, $affine:ty, $doc:literal) => { + ($wrapper:ident, $projective:ty, $affine:ty, $compressed_len:expr, $doc:literal) => { #[doc = $doc] #[derive(Clone, Copy, Default, Eq, PartialEq)] #[repr(transparent)] @@ -138,12 +138,7 @@ macro_rules! impl_jolt_group_wrapper { impl ::serde::Serialize for $wrapper { fn serialize(&self, serializer: S) -> Result { - use ::ark_serialize::CanonicalSerialize; - let mut buf = Vec::with_capacity(self.0.compressed_size()); - self.0 - .serialize_compressed(&mut buf) - .map_err(::serde::ser::Error::custom)?; - serializer.serialize_bytes(&buf) + serializer.serialize_bytes(&::jolt_field::CanonicalBytes::to_bytes_le_vec(self)) } } @@ -151,33 +146,49 @@ macro_rules! impl_jolt_group_wrapper { fn deserialize>( deserializer: D, ) -> Result { - use ::ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; let buf = >::deserialize(deserializer)?; - // Exact-size gate (as for `Bn254GT`): `deserialize_compressed` - // stops after one point and would silently accept trailing - // bytes, giving one group element many wire encodings. - let expected_len = <$projective>::default().compressed_size(); - if buf.len() != expected_len { - return Err(::serde::de::Error::custom(format!( + Self::decode_compressed(&buf).map_err(::serde::de::Error::custom) + } + } + + impl $wrapper { + /// Decodes exactly one compressed point, validating it is on the + /// curve and in the prime-order subgroup. + fn decode_compressed(bytes: &[u8]) -> Result { + use ::ark_serialize::CanonicalDeserialize; + // Exact-size gate: `deserialize_compressed` stops after one + // point and would silently accept trailing bytes, giving one + // group element many encodings. + let expected_len = ::NUM_BYTES; + if bytes.len() != expected_len { + return Err(format!( "{} encoding must be exactly {expected_len} bytes, got {}", stringify!($wrapper), - buf.len() - ))); + bytes.len() + )); } - let inner = <$projective>::deserialize_compressed(&buf[..]) - .map_err(::serde::de::Error::custom)?; - Ok(Self(inner)) + <$projective>::deserialize_compressed(bytes) + .map(Self) + .map_err(|error| error.to_string()) } } - impl ::jolt_transcript::AppendToTranscript for $wrapper { - fn append_to_transcript(&self, transcript: &mut T) { + /// The compressed arkworks encoding. + impl ::jolt_field::CanonicalBytes for $wrapper { + const NUM_BYTES: usize = $compressed_len; + + fn to_bytes_le(&self, out: &mut [u8]) { use ::ark_serialize::CanonicalSerialize; - let mut buf = Vec::with_capacity(self.0.compressed_size()); + assert_eq!(out.len(), Self::NUM_BYTES); self.0 - .serialize_compressed(&mut buf) + .serialize_compressed(out) .expect(concat!(stringify!($wrapper), " serialization cannot fail")); - transcript.append_bytes(&buf); + } + } + + impl ::jolt_field::CanonicalDecode for $wrapper { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Self::decode_compressed(bytes).ok() } } @@ -333,43 +344,28 @@ pub(crate) fn field_to_fr(f: &F) -> ark_bn254::Fr { #[expect(clippy::expect_used, reason = "tests may fail loudly")] mod tests { use ark_serialize::CanonicalSerialize; - use jolt_field::Fr; - use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; + use jolt_field::{CanonicalBytes, CanonicalDecode}; use super::{Bn254, Bn254G1, Bn254G2}; #[test] - fn g1_transcript_encoding_uses_compressed_commitment_bytes() { - let point = Bn254::g1_generator(); - let mut actual = Blake2bTranscript::::new(b"test"); - point.append_to_transcript(&mut actual); - - let mut expected = Blake2bTranscript::::new(b"test"); - let mut bytes = Vec::new(); - point - .0 - .serialize_compressed(&mut bytes) - .expect("serialize G1"); - expected.append_bytes(&bytes); - - assert_eq!(actual.state(), expected.state()); - } - - #[test] - fn g2_transcript_encoding_uses_compressed_commitment_bytes() { - let point = Bn254::g2_generator(); - let mut actual = Blake2bTranscript::::new(b"test"); - point.append_to_transcript(&mut actual); - - let mut expected = Blake2bTranscript::::new(b"test"); - let mut bytes = Vec::new(); - point - .0 - .serialize_compressed(&mut bytes) - .expect("serialize G2"); - expected.append_bytes(&bytes); - - assert_eq!(actual.state(), expected.state()); + fn canonical_codec_is_the_compressed_encoding() { + fn check( + point: P, + compressed: &impl CanonicalSerialize, + ) { + let mut expected = Vec::new(); + compressed + .serialize_compressed(&mut expected) + .expect("serialize point"); + assert_eq!(P::NUM_BYTES, compressed.compressed_size()); + assert_eq!(point.to_bytes_le_vec(), expected); + assert_eq!(P::from_bytes_le_checked(&expected), Some(point)); + } + let g1 = Bn254::g1_generator(); + check(g1, &g1.0); + let g2 = Bn254::g2_generator(); + check(g2, &g2.0); } fn encode_with_trailing_byte(point: &P) -> Vec { diff --git a/crates/jolt-crypto/src/ec/group.rs b/crates/jolt-crypto/src/ec/group.rs index 376c0b5353..84ba00c877 100644 --- a/crates/jolt-crypto/src/ec/group.rs +++ b/crates/jolt-crypto/src/ec/group.rs @@ -1,8 +1,7 @@ use std::fmt::Debug; use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; -use jolt_field::JoltField; -use jolt_transcript::AppendToTranscript; +use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField}; use serde::{Deserialize, Serialize}; /// Cryptographic group suitable for commitments. @@ -22,8 +21,9 @@ use serde::{Deserialize, Serialize}; /// seeds accumulators with `Default::default()`; a non-identity default would /// silently offset every aggregate. /// -/// Requires [`AppendToTranscript`] so group elements can be absorbed into -/// Fiat-Shamir transcripts (e.g., Pedersen commitments in ZK sumcheck). +/// Requires the canonical [`CanonicalBytes`]/[`CanonicalDecode`] codec so +/// group elements travel as transcript atoms (e.g., Pedersen commitments in +/// ZK sumcheck). Decoding must accept only valid group elements. pub trait JoltGroup: Clone + Copy @@ -42,7 +42,8 @@ pub trait JoltGroup: + SubAssign + Serialize + for<'de> Deserialize<'de> - + AppendToTranscript + + CanonicalBytes + + CanonicalDecode { /// Group identity element. #[must_use] From 26e352b76733091b38ec48cdb6067d1db9b368b2 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:35:30 -0400 Subject: [PATCH 05/62] feat(transcript): derive small-challenge vectors and challenge powers `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. --- crates/jolt-transcript/src/channel.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/jolt-transcript/src/channel.rs b/crates/jolt-transcript/src/channel.rs index 75c304317d..70e17e0574 100644 --- a/crates/jolt-transcript/src/channel.rs +++ b/crates/jolt-transcript/src/channel.rs @@ -56,6 +56,19 @@ pub trait Channel { /// set by the small set's size, not `|F|`. fn challenge_small(&mut self) -> F; + /// Draws `len` challenges from `F`'s small challenge set, in order. + fn challenges_small(&mut self, len: usize) -> Vec { + (0..len).map(|_| self.challenge_small()).collect() + } + + /// Draws one exactly uniform `gamma` and returns `[1, gamma, ..., gamma^(len-1)]`. + fn challenge_powers(&mut self, len: usize) -> Vec { + let gamma: F = self.challenge(); + std::iter::successors(Some(F::one()), |power| Some(*power * gamma)) + .take(len) + .collect() + } + /// Squeezes `N` raw challenge bytes. fn challenge_bytes(&mut self) -> [u8; N]; From 43aa1b4c6a40add9a5a3df2f2c9b7a2d5207be30 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:38:42 -0400 Subject: [PATCH 06/62] feat(transcript): re-export the spongefish sponges it implements Callers name `Blake2b512` and `Keccak` through jolt-transcript, so no protocol crate depends on spongefish directly. --- crates/jolt-transcript/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/jolt-transcript/src/lib.rs b/crates/jolt-transcript/src/lib.rs index 52fc10f774..781f1db14d 100644 --- a/crates/jolt-transcript/src/lib.rs +++ b/crates/jolt-transcript/src/lib.rs @@ -67,6 +67,10 @@ pub use prover::ProverTranscript; pub use site::TranscriptEvent; pub use site::{SiteId, TranscriptOp}; pub use sponge::Sponge; +#[cfg(feature = "transcript-blake2b")] +pub use spongefish::instantiations::Blake2b512; +#[cfg(feature = "transcript-keccak")] +pub use spongefish::instantiations::Keccak; /// The duplex interface every [`Sponge`] implements; re-exported so a custom /// sponge needs no direct spongefish dependency. pub use spongefish::DuplexSpongeInterface; @@ -106,10 +110,6 @@ use blake2::{digest::consts::U32, Blake2b}; ) ))] use jolt_field::Fr; -#[cfg(feature = "transcript-blake2b")] -use spongefish::instantiations::Blake2b512; -#[cfg(feature = "transcript-keccak")] -use spongefish::instantiations::Keccak; /// Fiat-Shamir transcript backed by Blake2b-512 (spongefish duplex sponge). #[cfg(all(feature = "transcript-blake2b", feature = "bn254"))] From b8e1ac0db5269ba3a45c0067da4c223ffb25b90c Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:49:22 -0400 Subject: [PATCH 07/62] refactor(openings)!: run PCS openings on the NARG transcript 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. --- Cargo.lock | 5 - crates/jolt-openings/Cargo.toml | 4 +- .../dory_homomorphic_batch_must_reject.rs | 72 ++++--- crates/jolt-openings/src/claims.rs | 44 ++-- crates/jolt-openings/src/error.rs | 5 + crates/jolt-openings/src/lib.rs | 2 +- crates/jolt-openings/src/prefix.rs | 22 +- crates/jolt-openings/src/schemes.rs | 201 ++++++++---------- .../jolt-openings/tests/homomorphic_dory.rs | 94 ++++---- .../tests/prefix_packed_layout.rs | 12 +- crates/jolt-openings/tests/schemes.rs | 45 ++-- crates/jolt-openings/tests/support/common.rs | 18 ++ crates/jolt-openings/tests/support/mock.rs | 101 +++++---- crates/jolt-poly/Cargo.toml | 2 - crates/jolt-poly/src/evaluation_claim.rs | 18 -- 15 files changed, 341 insertions(+), 304 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88db9063ee..eb57ab6693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3232,15 +3232,11 @@ dependencies = [ name = "jolt-dory" version = "0.1.0" dependencies = [ - "akita-config", - "akita-pcs", - "akita-types", "ark-bn254", "ark-ec", "ark-serialize 0.5.0", "criterion", "dory-pcs", - "jolt-akita", "jolt-crypto", "jolt-field", "jolt-openings", @@ -3601,7 +3597,6 @@ dependencies = [ "bincode 2.0.1", "criterion", "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand 0.8.7", diff --git a/crates/jolt-openings/Cargo.toml b/crates/jolt-openings/Cargo.toml index a6b1386691..cedcf1206e 100644 --- a/crates/jolt-openings/Cargo.toml +++ b/crates/jolt-openings/Cargo.toml @@ -11,9 +11,7 @@ workspace = true [dependencies] jolt-crypto = { path = "../jolt-crypto", default-features = false } jolt-field = { path = "../jolt-field", default-features = false } -jolt-poly = { path = "../jolt-poly", default-features = false, features = [ - "transcript", -] } +jolt-poly = { path = "../jolt-poly", default-features = false } jolt-transcript = { path = "../jolt-transcript", default-features = false } serde.workspace = true thiserror.workspace = true diff --git a/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs b/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs index 694d2684ec..bbce2e6482 100644 --- a/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs +++ b/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs @@ -16,7 +16,15 @@ use jolt_openings::{ ZkBatchOpeningScheme, ZkOpeningScheme, }; use jolt_poly::{MultilinearPoly, Point, Polynomial, HIGH_TO_LOW}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; + +fn prover(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&ProtocolId::new::("jolt-openings/fuzz"), session) +} + +fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new(&ProtocolId::new::("jolt-openings/fuzz"), session, narg) +} use libfuzzer_sys::fuzz_target; const MAX_NUM_VARS: usize = 4; @@ -102,8 +110,8 @@ fn prove_clear( claims: Vec, polynomials: &[Polynomial], hints: Vec, -) -> ::Proof { - let mut transcript = Blake2bTranscript::new(CLEAR_LABEL); +) -> Vec { + let mut transcript = prover(CLEAR_LABEL); ::prove_batch( setup, claims, @@ -111,7 +119,8 @@ fn prove_clear( hints, &mut transcript, ) - .unwrap_or_else(|error| panic!("Dory clear batch proof failed: {error}")) + .unwrap_or_else(|error| panic!("Dory clear batch proof failed: {error}")); + transcript.finish() } fn shifted_point(point: &Point) -> Point { @@ -148,8 +157,8 @@ fuzz_target!(|data: &[u8]| { match data[2] % 10 { 0 => { let (claims, hints) = commit_clear(&polynomials, &point, prover_setup); - let mut prover_transcript = Blake2bTranscript::new(CLEAR_LABEL); - let proof = ::prove_batch( + let mut prover_transcript = prover(CLEAR_LABEL); + ::prove_batch( prover_setup, claims.clone(), sources(&polynomials), @@ -158,27 +167,32 @@ fuzz_target!(|data: &[u8]| { ) .unwrap_or_else(|error| panic!("Dory clear batch proof failed: {error}")); - let mut verifier_transcript = Blake2bTranscript::new(CLEAR_LABEL); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(CLEAR_LABEL, &narg); ::verify_batch( verifier_setup, &claims, - &proof, &mut verifier_transcript, ) .unwrap_or_else(|error| panic!("honest clear batch verification failed: {error}")); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + prover_transcript.preview().squeeze::<32>(), + verifier_transcript.preview().squeeze::<32>() + ); + verifier_transcript + .finish() + .unwrap_or_else(|error| panic!("honest proof left bytes unread: {error}")); } 1 => { let (mut claims, hints) = commit_clear(&polynomials, &point, prover_setup); let proof = prove_clear(prover_setup, claims.clone(), &polynomials, hints); claims[0].evaluation.value += Fr::from_u64(1); - let mut transcript = Blake2bTranscript::new(CLEAR_LABEL); + let mut transcript = verifier(CLEAR_LABEL, &proof); assert!( ::verify_batch( verifier_setup, &claims, - &proof, &mut transcript, ) .is_err(), @@ -197,12 +211,11 @@ fuzz_target!(|data: &[u8]| { let mut tampered = claims; tampered[0].commitment = wrong_commitment; - let mut transcript = Blake2bTranscript::new(CLEAR_LABEL); + let mut transcript = verifier(CLEAR_LABEL, &proof); assert!( ::verify_batch( verifier_setup, &tampered, - &proof, &mut transcript, ) .is_err(), @@ -212,7 +225,7 @@ fuzz_target!(|data: &[u8]| { 3 => { let (mut claims, hints) = commit_clear(&polynomials, &point, prover_setup); claims[1].evaluation.point = shifted_point(&point); - let mut transcript = Blake2bTranscript::new(CLEAR_LABEL); + let mut transcript = prover(CLEAR_LABEL); assert!( ::prove_batch( prover_setup, @@ -230,7 +243,7 @@ fuzz_target!(|data: &[u8]| { let wrong = Polynomial::new(vec![Fr::from_u64(1); 1usize << (num_vars + 1)]); let mut polynomial_sources = sources(&polynomials); polynomial_sources[0] = &wrong; - let mut transcript = Blake2bTranscript::new(CLEAR_LABEL); + let mut transcript = prover(CLEAR_LABEL); assert!( ::prove_batch( prover_setup, @@ -248,7 +261,7 @@ fuzz_target!(|data: &[u8]| { let mut polynomial_sources = sources(&polynomials); polynomial_sources.pop(); hints.pop(); - let mut transcript = Blake2bTranscript::new(CLEAR_LABEL); + let mut transcript = prover(CLEAR_LABEL); assert!( ::prove_batch( prover_setup, @@ -264,12 +277,11 @@ fuzz_target!(|data: &[u8]| { 6 => { let (claims, hints) = commit_clear(&polynomials, &point, prover_setup); let proof = prove_clear(prover_setup, claims.clone(), &polynomials, hints); - let mut transcript = Blake2bTranscript::new(b"fuzz-openings-clear-alt"); + let mut transcript = verifier(b"fuzz-openings-clear-alt", &proof); assert!( ::verify_batch( verifier_setup, &claims, - &proof, &mut transcript, ) .is_err(), @@ -278,7 +290,7 @@ fuzz_target!(|data: &[u8]| { } 7 => { let (commitments, hints, evaluations) = commit_zk(&polynomials, &point, prover_setup); - let mut prover_transcript = Blake2bTranscript::new(ZK_LABEL); + let mut prover_transcript = prover(ZK_LABEL); let opening = ::prove_batch_zk( prover_setup, point.clone(), @@ -290,17 +302,23 @@ fuzz_target!(|data: &[u8]| { ) .unwrap_or_else(|error| panic!("Dory ZK batch proof failed: {error}")); - let mut verifier_transcript = Blake2bTranscript::new(ZK_LABEL); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(ZK_LABEL, &narg); let verifier_hiding = ::verify_batch_zk( verifier_setup, point, commitments, - &opening.proof, &mut verifier_transcript, ) .unwrap_or_else(|error| panic!("honest ZK batch verification failed: {error}")); assert_eq!(opening.hiding_commitment, verifier_hiding); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + prover_transcript.preview().squeeze::<32>(), + verifier_transcript.preview().squeeze::<32>() + ); + verifier_transcript + .finish() + .unwrap_or_else(|error| panic!("honest proof left bytes unread: {error}")); } 8 => { let (commitments, mut hints, mut evaluations) = @@ -309,7 +327,7 @@ fuzz_target!(|data: &[u8]| { polynomial_sources.pop(); hints.pop(); evaluations.pop(); - let mut transcript = Blake2bTranscript::new(ZK_LABEL); + let mut transcript = prover(ZK_LABEL); assert!( ::prove_batch_zk( prover_setup, @@ -327,8 +345,8 @@ fuzz_target!(|data: &[u8]| { _ => { let (mut commitments, hints, evaluations) = commit_zk(&polynomials, &point, prover_setup); - let mut prover_transcript = Blake2bTranscript::new(ZK_LABEL); - let opening = ::prove_batch_zk( + let mut prover_transcript = prover(ZK_LABEL); + let _opening = ::prove_batch_zk( prover_setup, point.clone(), commitments.clone(), @@ -338,6 +356,7 @@ fuzz_target!(|data: &[u8]| { &mut prover_transcript, ) .unwrap_or_else(|error| panic!("Dory ZK batch proof failed: {error}")); + let narg = prover_transcript.finish(); let alternate = alternate_polynomial(&polynomials[0]); let (wrong_commitment, _) = @@ -348,13 +367,12 @@ fuzz_target!(|data: &[u8]| { } commitments[0] = wrong_commitment; - let mut verifier_transcript = Blake2bTranscript::new(ZK_LABEL); + let mut verifier_transcript = verifier(ZK_LABEL, &narg); assert!( ::verify_batch_zk( verifier_setup, point, commitments, - &opening.proof, &mut verifier_transcript, ) .is_err(), diff --git a/crates/jolt-openings/src/claims.rs b/crates/jolt-openings/src/claims.rs index a61fc4a68b..ba02b7c773 100644 --- a/crates/jolt-openings/src/claims.rs +++ b/crates/jolt-openings/src/claims.rs @@ -1,8 +1,8 @@ //! Stateless claim types for PCS operations. -use jolt_field::JoltField; +use jolt_field::{CanonicalBytes, JoltField}; use jolt_poly::EvaluationClaim; -use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; +use jolt_transcript::Channel; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ZkEvaluationClaim<'a, F, C> { @@ -19,21 +19,11 @@ impl<'a, F, C> ZkEvaluationClaim<'a, F, C> { } } -impl AppendToTranscript for ZkEvaluationClaim<'_, F, C> -where - F: JoltField, - C: AppendToTranscript, -{ - fn append_to_transcript(&self, transcript: &mut T) { - transcript.append(&LabelWithCount( - b"zk_opening_point", - self.point.len() as u64, - )); - for coordinate in self.point { - coordinate.append_to_transcript(transcript); - } - transcript.append(&Label(b"zk_eval_commitment")); - self.hiding_commitment.append_to_transcript(transcript); +impl ZkEvaluationClaim<'_, F, C> { + /// Absorbs the opening point, then the hiding commitment to the evaluation. + pub fn absorb(&self, channel: &mut Ch) { + channel.public_all(self.point); + channel.public(self.hiding_commitment); } } @@ -44,16 +34,12 @@ pub struct VerifierOpeningClaim { pub evaluation: EvaluationClaim, } -pub(crate) struct VerifierRlcClaims<'a, F: JoltField, C>(pub &'a [VerifierOpeningClaim]); - -impl AppendToTranscript for VerifierRlcClaims<'_, F, C> -where - F: JoltField, -{ - fn append_to_transcript(&self, transcript: &mut T) { - transcript.append(&LabelWithCount(b"rlc_claims", self.0.len() as u64)); - for claim in self.0 { - claim.evaluation.value.append_to_transcript(transcript); - } - } +/// Absorbs an opening claim's point, then its value. +pub(crate) fn absorb_evaluation( + channel: &mut Ch, + point: &[F], + value: &F, +) { + channel.public_all(point); + channel.public(value); } diff --git a/crates/jolt-openings/src/error.rs b/crates/jolt-openings/src/error.rs index 1f6dc231ef..a23f5d06be 100644 --- a/crates/jolt-openings/src/error.rs +++ b/crates/jolt-openings/src/error.rs @@ -1,5 +1,7 @@ //! PCS error types. +use jolt_transcript::TranscriptError; + #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum OpeningsError { #[error("opening proof verification failed")] @@ -22,4 +24,7 @@ pub enum OpeningsError { #[error("polynomial size {poly_size} exceeds setup max {setup_max}")] PolynomialTooLarge { poly_size: usize, setup_max: usize }, + + #[error("opening proof transcript: {0}")] + Transcript(#[from] TranscriptError), } diff --git a/crates/jolt-openings/src/lib.rs b/crates/jolt-openings/src/lib.rs index bc400d3383..4e4746f37f 100644 --- a/crates/jolt-openings/src/lib.rs +++ b/crates/jolt-openings/src/lib.rs @@ -20,7 +20,7 @@ //! ```text //! Commitment (jolt-crypto: Output type) //! │ -//! CommitmentScheme (+ Field, Proof, commit/open/verify) +//! CommitmentScheme (+ Field, commitment codec, commit/open/verify) //! ╱ │ ╲ //! Additively Streaming ZkOpeningScheme //! Homomorphic Commitment │ diff --git a/crates/jolt-openings/src/prefix.rs b/crates/jolt-openings/src/prefix.rs index 4239b19813..683ef55c87 100644 --- a/crates/jolt-openings/src/prefix.rs +++ b/crates/jolt-openings/src/prefix.rs @@ -17,7 +17,7 @@ use std::collections::BTreeMap; use jolt_field::JoltField; use jolt_poly::{eq_index_msb, Point, HIGH_TO_LOW}; -use jolt_transcript::{Label, Transcript, U64Word}; +use jolt_transcript::Channel; use crate::{EvaluationClaim, OpeningsError}; @@ -185,14 +185,14 @@ where /// Binds the semantic statement, samples the selector, and returns the /// corresponding claim on the physical polynomial. - pub fn reduce_claims( + pub fn reduce_claims( &self, claims: &PrefixPackedClaims, - transcript: &mut T, + transcript: &mut C, ) -> Result, OpeningsError> where F: JoltField, - T: Transcript, + C: Channel, { if claims.point.len() != self.logical_num_vars { return Err(OpeningsError::InvalidBatch(format!( @@ -209,15 +209,13 @@ where ))); } - transcript.append(&Label(b"prefix_packed_claim")); - transcript.append(&U64Word(self.logical_num_vars as u64)); - transcript.append(&U64Word(self.slot_capacity as u64)); - transcript.append(&Label(b"prefix_pack_layout")); - transcript.append_bytes(&claims.layout_digest); - transcript.append_values(b"prefix_pack_point", claims.point.as_slice()); - transcript.append_values(b"prefix_pack_evals", &claims.evaluations); + transcript.public(&(self.logical_num_vars as u64)); + transcript.public(&(self.slot_capacity as u64)); + transcript.public(&claims.layout_digest); + transcript.public_all(claims.point.as_slice()); + transcript.public_all(&claims.evaluations); - let selector = transcript.challenge_vector(self.selector_num_vars); + let selector = transcript.challenges_small(self.selector_num_vars); let point = self.pack_point(&selector, claims.point.as_slice())?; let evaluation = self.reduce_evaluations(&selector, &claims.evaluations)?; Ok(EvaluationClaim::new(point, evaluation)) diff --git a/crates/jolt-openings/src/schemes.rs b/crates/jolt-openings/src/schemes.rs index 77d720d9e3..efd21283b5 100644 --- a/crates/jolt-openings/src/schemes.rs +++ b/crates/jolt-openings/src/schemes.rs @@ -9,14 +9,13 @@ use std::{fmt::Debug, marker::PhantomData}; use jolt_crypto::{Commitment, HomomorphicCommitment}; -use jolt_field::{JoltField, Ring}; +use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField, Ring}; use jolt_poly::{MultilinearPoly, Point, RlcSource, HIGH_TO_LOW}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use serde::{de::DeserializeOwned, Serialize}; -use crate::claims::{VerifierOpeningClaim, VerifierRlcClaims, ZkEvaluationClaim}; +use crate::claims::{absorb_evaluation, VerifierOpeningClaim, ZkEvaluationClaim}; use crate::error::OpeningsError; -use crate::EvaluationClaim; /// Self-describing metadata a group commitment carries: the backend flavor, /// the protocol-owned layout digest binding the ordered member identities, @@ -47,9 +46,13 @@ pub trait GroupSetupMetadata { } /// Commit to f: F^n -> F, then prove f(r) = v for verifier-chosen r. +/// +/// Opening proofs live in the caller's transcript: `open` writes its prover +/// messages and `verify` reads them back. The scheme owns its commitments' +/// wire codec through [`send_commitment`](Self::send_commitment) and +/// [`receive_commitment`](Self::receive_commitment). pub trait CommitmentScheme: Commitment { type Field: JoltField; - type Proof: Clone + Debug + Eq + Send + Sync + 'static + Serialize + DeserializeOwned; type ProverSetup: Clone + Send + Sync; type VerifierSetup: Clone + Send + Sync + Serialize + DeserializeOwned; @@ -69,22 +72,35 @@ pub trait CommitmentScheme: Commitment { setup: &Self::ProverSetup, ) -> Result<(Self::Output, Self::OpeningHint), OpeningsError>; - fn open + ?Sized>( + /// Writes `commitment` into the proof as a prover message. + fn send_commitment(commitment: &Self::Output, transcript: &mut ProverTranscript); + + /// Reads one commitment written by [`send_commitment`](Self::send_commitment), + /// rejecting any encoding that is not a valid commitment under `setup`. + fn receive_commitment( + setup: &Self::VerifierSetup, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result; + + /// Absorbs a commitment both sides already hold, such as one fixed by + /// preprocessing. + fn absorb_commitment(commitment: &Self::Output, channel: &mut C); + + fn open + ?Sized, H: Sponge>( poly: &P, point: &[Self::Field], eval: Self::Field, setup: &Self::ProverSetup, hint: Option, - transcript: &mut impl Transcript, - ) -> Result; + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError>; - fn verify( + fn verify( commitment: &Self::Output, point: &[Self::Field], eval: Self::Field, - proof: &Self::Proof, setup: &Self::VerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError>; /// Commits a group of polynomials as one commitment object whose members @@ -108,13 +124,13 @@ pub trait CommitmentScheme: Commitment { // TODO(#1782): fold the retained-state contract into a first-class // committed-object type on this trait instead of the `OpeningHint` // side-channel. - fn prove_batch( + fn prove_batch( _setup: &Self::ProverSetup, _groups: Vec>, _final_group: GroupOpeningClaim, _final_hint: Self::OpeningHint, - _transcript: &mut impl Transcript, - ) -> Result { + _transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { Err(OpeningsError::InvalidBatch( "this commitment scheme has no native group-batch opening".to_owned(), )) @@ -122,12 +138,11 @@ pub trait CommitmentScheme: Commitment { /// Verifies one proof opening zero or more independently committed groups /// at their group-local points, followed by a final commitment group. - fn verify_batch( + fn verify_batch( _setup: &Self::VerifierSetup, _groups: &[TaggedGroupOpeningClaim], _final_group: &GroupOpeningClaim, - _proof: &Self::Proof, - _transcript: &mut impl Transcript, + _transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { Err(OpeningsError::InvalidBatch( "this commitment scheme has no native group-batch opening".to_owned(), @@ -330,7 +345,8 @@ pub trait ZkOpeningScheme: CommitmentScheme { + 'static + Serialize + DeserializeOwned - + AppendToTranscript; + + CanonicalBytes + + CanonicalDecode; type Blind: Clone + Send + Sync; @@ -342,27 +358,22 @@ pub trait ZkOpeningScheme: CommitmentScheme { /// Open a ZK/hiding commitment using the opening hint returned by /// [`commit_zk`](Self::commit_zk). - #[expect( - clippy::type_complexity, - reason = "ZK openings return the native proof, hiding commitment, and blind" - )] - fn open_zk + ?Sized>( + fn open_zk + ?Sized, H: Sponge>( poly: &P, point: &[Self::Field], eval: Self::Field, setup: &Self::ProverSetup, hint: Self::OpeningHint, - transcript: &mut impl Transcript, - ) -> Result<(Self::Proof, Self::HidingCommitment, Self::Blind), OpeningsError>; + transcript: &mut ProverTranscript, + ) -> Result<(Self::HidingCommitment, Self::Blind), OpeningsError>; /// Verify a ZK opening proof and return the hiding commitment to the /// evaluation that the proof binds internally. - fn verify_zk( + fn verify_zk( commitment: &Self::Output, point: &[Self::Field], - proof: &Self::Proof, setup: &Self::VerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result; } @@ -415,36 +426,30 @@ pub trait BatchOpeningScheme { Self: 'a; /// Commit-time auxiliary data reused by the PCS when opening. type Hints; - type Proof; - fn prove_batch<'a, T>( + fn prove_batch<'a, H: Sponge>( setup: &Self::ProverSetup, statement: Self::Statement, polynomials: Self::Polynomials<'a>, hints: Self::Hints, - transcript: &mut T, - ) -> Result + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> where - Self: 'a, - T: Transcript; + Self: 'a; - fn verify_batch( + fn verify_batch( setup: &Self::VerifierSetup, statement: &Self::Statement, - proof: &Self::Proof, - transcript: &mut T, - ) -> Result<(), OpeningsError> - where - T: Transcript; + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result<(), OpeningsError>; } -/// The prover-side outputs of a hiding batch opening: the native proof, the -/// hiding commitment binding the joint evaluation, that commitment's blind, -/// and the joint evaluation itself (`Σ γⁱ · evalᵢ`) — the last two are the -/// secrets a downstream ZK layer (BlindFold's final-opening binding) opens -/// the hiding commitment with. +/// The prover-side outputs of a hiding batch opening: the hiding commitment +/// binding the joint evaluation, that commitment's blind, and the joint +/// evaluation itself (`Σ γⁱ · evalᵢ`) — the last two are the secrets a +/// downstream ZK layer (BlindFold's final-opening binding) opens the hiding +/// commitment with. pub struct ZkBatchOpening { - pub proof: S::Proof, pub hiding_commitment: S::HidingCommitment, pub blind: S::Blind, pub joint_evaluation: S::Field, @@ -461,28 +466,24 @@ pub trait ZkBatchOpeningScheme: BatchOpeningScheme { type HidingCommitment; type Blind; - fn prove_batch_zk<'a, T>( + fn prove_batch_zk<'a, H: Sponge>( setup: &Self::ProverSetup, point: Point, commitments: Vec, polynomials: Self::Polynomials<'a>, hints: Self::Hints, evaluations: Vec, - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, OpeningsError> where - Self: 'a, - T: Transcript; + Self: 'a; - fn verify_batch_zk( + fn verify_batch_zk( setup: &Self::VerifierSetup, point: Point, commitments: Vec, - proof: &Self::Proof, - transcript: &mut T, - ) -> Result - where - T: Transcript; + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result; } // Batching strategies are zero-sized marker types rather than impls on the @@ -507,23 +508,21 @@ where where Self: 'a; type Hints = Vec; - type Proof = PCS::Proof; #[tracing::instrument( skip_all, name = "HomomorphicBatch::prove_batch", fields(claims = claims.len()) )] - fn prove_batch<'a, T>( + fn prove_batch<'a, H: Sponge>( setup: &Self::ProverSetup, claims: Self::Statement, polynomials: Self::Polynomials<'a>, hints: Self::Hints, - transcript: &mut T, - ) -> Result + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> where Self: 'a, - T: Transcript, { let statement = HomomorphicBatchStatement::new(&claims)?; if polynomials.len() != statement.claims.len() || hints.len() != statement.claims.len() { @@ -535,13 +534,13 @@ where ))); } - statement.append_to_transcript(transcript); - let scalars = transcript.challenge_scalar_powers(statement.claims.len()); + statement.absorb_values(transcript); + let scalars = transcript.challenge_powers(statement.claims.len()); let joint_eval = statement.joint_eval(&scalars); let joint_polynomial = Self::combine_polynomials(polynomials, &scalars, statement.point.len())?; let combined_hint = PCS::combine_hints(hints, &scalars); - let proof = PCS::open( + PCS::open( &joint_polynomial, statement.point.as_slice(), joint_eval, @@ -549,22 +548,18 @@ where Some(combined_hint), transcript, )?; - EvaluationClaim::new(statement.point.clone(), joint_eval).append_to_transcript(transcript); - Ok(proof) + absorb_evaluation(transcript, statement.point.as_slice(), &joint_eval); + Ok(()) } - fn verify_batch( + fn verify_batch( setup: &Self::VerifierSetup, claims: &Self::Statement, - proof: &Self::Proof, - transcript: &mut T, - ) -> Result<(), OpeningsError> - where - T: Transcript, - { + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result<(), OpeningsError> { let statement = HomomorphicBatchStatement::new(claims)?; - statement.append_to_transcript(transcript); - let scalars = transcript.challenge_scalar_powers(statement.claims.len()); + statement.absorb_values(transcript); + let scalars = transcript.challenge_powers(statement.claims.len()); let joint_eval = statement.joint_eval(&scalars); let commitments = statement.commitments(); let joint_commitment = PCS::combine(&commitments, &scalars); @@ -572,11 +567,10 @@ where &joint_commitment, statement.point.as_slice(), joint_eval, - proof, setup, transcript, )?; - EvaluationClaim::new(statement.point.clone(), joint_eval).append_to_transcript(transcript); + absorb_evaluation(transcript, statement.point.as_slice(), &joint_eval); Ok(()) } } @@ -646,12 +640,16 @@ where } } -impl AppendToTranscript for HomomorphicBatchStatement<'_, F, C> -where - F: JoltField, -{ - fn append_to_transcript(&self, transcript: &mut T) { - VerifierRlcClaims(self.claims).append_to_transcript(transcript); +impl HomomorphicBatchStatement<'_, F, C> { + /// Absorbs the claimed values. The commitments and the shared point are + /// already bound by the caller; see [`BatchOpeningScheme`]. + fn absorb_values(&self, channel: &mut Ch) { + let values: Vec = self + .claims + .iter() + .map(|claim| claim.evaluation.value) + .collect(); + channel.public_all(&values); } } @@ -669,18 +667,17 @@ where name = "HomomorphicBatch::prove_batch_zk", fields(claims = commitments.len()) )] - fn prove_batch_zk<'a, T>( + fn prove_batch_zk<'a, H: Sponge>( setup: &Self::ProverSetup, point: Point, commitments: Vec, polynomials: Self::Polynomials<'a>, hints: Self::Hints, evaluations: Vec, - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, OpeningsError> where Self: 'a, - T: Transcript, { if commitments.is_empty() { return Err(OpeningsError::InvalidBatch( @@ -699,7 +696,7 @@ where commitments.len() ))); } - let scalars = transcript.challenge_scalar_powers(commitments.len()); + let scalars = transcript.challenge_powers(commitments.len()); let joint_eval = evaluations .iter() @@ -709,7 +706,7 @@ where }); let joint_polynomial = Self::combine_polynomials(polynomials, &scalars, point.len())?; let combined_hint = PCS::combine_hints(hints, &scalars); - let (proof, hiding_commitment, blind) = PCS::open_zk( + let (hiding_commitment, blind) = PCS::open_zk( &joint_polynomial, point.as_slice(), joint_eval, @@ -717,42 +714,30 @@ where combined_hint, transcript, )?; - ZkEvaluationClaim::new(point.as_slice(), &hiding_commitment) - .append_to_transcript(transcript); + ZkEvaluationClaim::new(point.as_slice(), &hiding_commitment).absorb(transcript); Ok(ZkBatchOpening { - proof, hiding_commitment, blind, joint_evaluation: joint_eval, }) } - fn verify_batch_zk( + fn verify_batch_zk( setup: &Self::VerifierSetup, point: Point, commitments: Vec, - proof: &Self::Proof, - transcript: &mut T, - ) -> Result - where - T: Transcript, - { + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { if commitments.is_empty() { return Err(OpeningsError::InvalidBatch( "batch opening requires at least one commitment".to_owned(), )); } - let scalars = transcript.challenge_scalar_powers(commitments.len()); + let scalars = transcript.challenge_powers(commitments.len()); let joint_commitment = PCS::combine(&commitments, &scalars); - let hiding_commitment = PCS::verify_zk( - &joint_commitment, - point.as_slice(), - proof, - setup, - transcript, - )?; - ZkEvaluationClaim::new(point.as_slice(), &hiding_commitment) - .append_to_transcript(transcript); + let hiding_commitment = + PCS::verify_zk(&joint_commitment, point.as_slice(), setup, transcript)?; + ZkEvaluationClaim::new(point.as_slice(), &hiding_commitment).absorb(transcript); Ok(hiding_commitment) } } diff --git a/crates/jolt-openings/tests/homomorphic_dory.rs b/crates/jolt-openings/tests/homomorphic_dory.rs index 2d034d3304..59dc1ddf26 100644 --- a/crates/jolt-openings/tests/homomorphic_dory.rs +++ b/crates/jolt-openings/tests/homomorphic_dory.rs @@ -4,18 +4,17 @@ reason = "tests assert successful proof paths" )] -use jolt_dory::{DoryCommitment, DoryProof, DoryScheme, DoryVerifierSetup}; +use jolt_dory::{DoryCommitment, DoryScheme, DoryVerifierSetup}; use jolt_field::Fr; use jolt_openings::{ BatchOpeningScheme, HomomorphicBatch, OpeningsError, ZkBatchOpeningScheme, ZkOpeningScheme, }; use jolt_poly::{Point, Polynomial, HIGH_TO_LOW}; -use jolt_transcript::{Blake2bTranscript, Transcript}; #[path = "support/common.rs"] pub mod common; -use common::{clear_claims, fr, homomorphic_polynomials, sources}; +use common::{clear_claims, fingerprint, fr, homomorphic_polynomials, prover, sources, verifier}; type HomomorphicDoryBatch = HomomorphicBatch; @@ -26,8 +25,8 @@ fn dory_homomorphic_batch_roundtrip_clear_many_polynomials() { let verifier_setup = DoryScheme::setup_verifier(point.len()); let (claims, hints) = clear_claims::(&polynomials, &point, &prover_setup); - let mut prover_transcript = Blake2bTranscript::new(b"dory-batch"); - let proof = ::prove_batch( + let mut prover_transcript = prover(b"dory-batch"); + ::prove_batch( &prover_setup, claims.clone(), sources(&polynomials), @@ -36,16 +35,20 @@ fn dory_homomorphic_batch_roundtrip_clear_many_polynomials() { ) .expect("Dory homomorphic batch proof should be produced"); - let mut verifier_transcript = Blake2bTranscript::new(b"dory-batch"); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"dory-batch", &narg); ::verify_batch( &verifier_setup, &claims, - &proof, &mut verifier_transcript, ) .expect("Dory homomorphic batch proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + fingerprint(&prover_transcript), + fingerprint(&verifier_transcript) + ); + verifier_transcript.finish().expect("proof fully consumed"); } #[test] @@ -55,8 +58,8 @@ fn dory_homomorphic_batch_rejects_tampered_value() { let verifier_setup = DoryScheme::setup_verifier(point.len()); let (claims, hints) = clear_claims::(&polynomials, &point, &prover_setup); - let mut prover_transcript = Blake2bTranscript::new(b"dory-batch-tamper"); - let proof = ::prove_batch( + let mut prover_transcript = prover(b"dory-batch-tamper"); + ::prove_batch( &prover_setup, claims.clone(), sources(&polynomials), @@ -68,11 +71,11 @@ fn dory_homomorphic_batch_rejects_tampered_value() { let mut tampered = claims; tampered[1].evaluation.value += fr(1); - let mut verifier_transcript = Blake2bTranscript::new(b"dory-batch-tamper"); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"dory-batch-tamper", &narg); let result = ::verify_batch( &verifier_setup, &tampered, - &proof, &mut verifier_transcript, ); assert!(result.is_err(), "tampered Dory batch value should fail"); @@ -85,7 +88,7 @@ fn dory_homomorphic_batch_rejects_mismatched_points() { let (mut claims, hints) = clear_claims::(&polynomials, &point, &prover_setup); claims[2].evaluation.point = Point::new(vec![fr(2), fr(3), fr(5)]); - let mut transcript = Blake2bTranscript::new(b"dory-batch-point-mismatch"); + let mut transcript = prover(b"dory-batch-point-mismatch"); let result = ::prove_batch( &prover_setup, claims, @@ -105,7 +108,7 @@ fn dory_homomorphic_batch_rejects_witness_count_mismatch() { let _dropped = polynomial_sources.pop(); let _dropped_hint = hints.pop(); - let mut transcript = Blake2bTranscript::new(b"dory-batch-witness-count"); + let mut transcript = prover(b"dory-batch-witness-count"); let result = ::prove_batch( &prover_setup, claims, @@ -125,7 +128,7 @@ fn dory_homomorphic_batch_rejects_wrong_witness_dimension() { let mut polynomial_sources = sources(&polynomials); polynomial_sources[0] = &wrong_witness; - let mut transcript = Blake2bTranscript::new(b"dory-batch-witness-dim"); + let mut transcript = prover(b"dory-batch-witness-dim"); let result = ::prove_batch( &prover_setup, claims, @@ -152,7 +155,7 @@ fn dory_homomorphic_zk_batch_roundtrip() { evaluations.push(polynomial.evaluate(&point)); } - let mut prover_transcript = Blake2bTranscript::new(b"dory-batch-zk"); + let mut prover_transcript = prover(b"dory-batch-zk"); let opening = ::prove_batch_zk( &prover_setup, point.clone(), @@ -164,27 +167,37 @@ fn dory_homomorphic_zk_batch_roundtrip() { ) .expect("Dory ZK homomorphic batch proof should be produced"); - let mut verifier_transcript = Blake2bTranscript::new(b"dory-batch-zk"); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"dory-batch-zk", &narg); let verifier_hiding = ::verify_batch_zk( &verifier_setup, point, commitments, - &opening.proof, &mut verifier_transcript, ) .expect("Dory ZK homomorphic batch proof should verify"); assert_eq!(opening.hiding_commitment, verifier_hiding); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + fingerprint(&prover_transcript), + fingerprint(&verifier_transcript) + ); + verifier_transcript.finish().expect("proof fully consumed"); } struct ZkBatchFixture { point: Point, commitments: Vec, verifier_setup: DoryVerifierSetup, - proof: DoryProof, + narg: Vec, } +/// Argument-string offset of Dory's `y_com`: the batch draws its coefficients +/// without a prover message, so Dory's VMV message (c, d2 in GT; e1 in G1) +/// and e2 (G2) come first. +const Y_COM_OFFSET: usize = 2 * 384 + 32 + 64; +const G1_BYTES: usize = 32; + /// Produces an honest ZK batch proof over two random 2-variable polynomials, /// bound to a caller-chosen transcript label so each test can replay the /// exact prover transcript. @@ -203,8 +216,8 @@ fn zk_batch_fixture(seed: u64, label: &'static [u8]) -> ZkBatchFixture { evaluations.push(polynomial.evaluate(&point)); } - let mut transcript = Blake2bTranscript::new(label); - let opening = ::prove_batch_zk( + let mut transcript = prover(label); + let _opening = ::prove_batch_zk( &prover_setup, point.clone(), commitments.clone(), @@ -219,7 +232,7 @@ fn zk_batch_fixture(seed: u64, label: &'static [u8]) -> ZkBatchFixture { point, commitments, verifier_setup, - proof: opening.proof, + narg: transcript.finish(), } } @@ -228,16 +241,17 @@ fn verify_zk_fixture( label: &'static [u8], point: Point, commitments: Vec, - proof: &DoryProof, + narg: &[u8], ) -> Result<::HidingCommitment, OpeningsError> { - let mut transcript = Blake2bTranscript::new(label); - ::verify_batch_zk( + let mut transcript = verifier(label, narg); + let hiding = ::verify_batch_zk( &fixture.verifier_setup, point, commitments, - proof, &mut transcript, - ) + )?; + transcript.finish()?; + Ok(hiding) } #[test] @@ -260,7 +274,7 @@ fn dory_homomorphic_zk_batch_rejects_tampered_commitment() { label, fixture.point.clone(), tampered, - &fixture.proof, + &fixture.narg, ); assert!( matches!(result, Err(OpeningsError::VerificationFailed)), @@ -273,7 +287,7 @@ fn dory_homomorphic_zk_batch_rejects_tampered_commitment() { label, fixture.point.clone(), fixture.commitments.clone(), - &fixture.proof, + &fixture.narg, ) .expect("control: the untampered ZK batch proof must verify"); } @@ -292,7 +306,7 @@ fn dory_homomorphic_zk_batch_rejects_wrong_opening_point() { label, wrong_point, fixture.commitments.clone(), - &fixture.proof, + &fixture.narg, ); assert!( matches!(result, Err(OpeningsError::VerificationFailed)), @@ -305,7 +319,7 @@ fn dory_homomorphic_zk_batch_rejects_wrong_opening_point() { label, fixture.point.clone(), fixture.commitments.clone(), - &fixture.proof, + &fixture.narg, ) .expect("control: the original opening point must verify"); } @@ -319,12 +333,14 @@ fn dory_homomorphic_zk_batch_rejects_tampered_hiding_commitment() { // Graft a well-formed but wrong evaluation commitment (y_com) from an // unrelated proof: the value the proof binds no longer matches the // proven evaluation. - let mut grafted = fixture.proof.clone(); + let y_com = Y_COM_OFFSET..Y_COM_OFFSET + G1_BYTES; + let mut grafted = fixture.narg.clone(); assert_ne!( - grafted.0.y_com, donor.proof.0.y_com, + grafted[y_com.clone()], + donor.narg[y_com.clone()], "the donor proof must supply a different hiding commitment" ); - grafted.0.y_com = donor.proof.0.y_com; + grafted[y_com.clone()].copy_from_slice(&donor.narg[y_com.clone()]); let result = verify_zk_fixture( &fixture, @@ -340,8 +356,8 @@ fn dory_homomorphic_zk_batch_rejects_tampered_hiding_commitment() { // Stripping the hiding commitment entirely must also be rejected rather // than falling back to a non-hiding verification path. - let mut stripped = fixture.proof.clone(); - stripped.0.y_com = None; + let mut stripped = fixture.narg.clone(); + let _removed: Vec = stripped.drain(y_com).collect(); let result = verify_zk_fixture( &fixture, label, @@ -360,7 +376,7 @@ fn dory_homomorphic_zk_batch_rejects_tampered_hiding_commitment() { label, fixture.point.clone(), fixture.commitments.clone(), - &fixture.proof, + &fixture.narg, ) .expect("control: the intact ZK batch proof must verify"); } @@ -384,7 +400,7 @@ fn dory_homomorphic_zk_batch_rejects_witness_count_mismatch() { let _dropped_hint = hints.pop(); let _dropped_eval = evaluations.pop(); - let mut transcript = Blake2bTranscript::new(b"dory-batch-zk-witness-count"); + let mut transcript = prover(b"dory-batch-zk-witness-count"); let result = ::prove_batch_zk( &prover_setup, point, diff --git a/crates/jolt-openings/tests/prefix_packed_layout.rs b/crates/jolt-openings/tests/prefix_packed_layout.rs index b01cf37b89..ad37f2e367 100644 --- a/crates/jolt-openings/tests/prefix_packed_layout.rs +++ b/crates/jolt-openings/tests/prefix_packed_layout.rs @@ -3,7 +3,7 @@ use jolt_field::{Fr, Ring}; use jolt_openings::{OpeningsError, PrefixPackedClaims, PrefixPackedLayout}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript}; fn fr(value: u64) -> Fr { Fr::from_u64(value) @@ -82,7 +82,10 @@ fn claim_reduction_binds_statement_before_drawing_selector() { let layout = PrefixPackedLayout::new(2, 4, [0_u64, 1, 2]).unwrap(); let claims = PrefixPackedClaims::new([9_u8; 32], vec![fr(3), fr(5)], vec![fr(7), fr(11), fr(13)]); - let mut transcript = Blake2bTranscript::new(b"prefix-packed-claim"); + let mut transcript = ProverTranscript::::new( + &ProtocolId::new::("jolt-openings/tests"), + b"prefix-packed-claim", + ); let reduced = layout.reduce_claims(&claims, &mut transcript).unwrap(); assert_eq!(reduced.point.len(), layout.packed_num_vars()); @@ -100,7 +103,10 @@ fn claim_reduction_binds_statement_before_drawing_selector() { .unwrap() ); - let mut changed_transcript = Blake2bTranscript::new(b"prefix-packed-claim"); + let mut changed_transcript = ProverTranscript::::new( + &ProtocolId::new::("jolt-openings/tests"), + b"prefix-packed-claim", + ); let changed = PrefixPackedClaims::new([9_u8; 32], vec![fr(3), fr(5)], vec![fr(7), fr(11), fr(17)]); let changed_reduced = layout diff --git a/crates/jolt-openings/tests/schemes.rs b/crates/jolt-openings/tests/schemes.rs index 8da84601df..78ec20e8e7 100644 --- a/crates/jolt-openings/tests/schemes.rs +++ b/crates/jolt-openings/tests/schemes.rs @@ -9,14 +9,13 @@ use jolt_openings::{ BatchOpeningScheme, HomomorphicBatch, OpeningsError, ZkBatchOpeningScheme, ZkOpeningScheme, }; use jolt_poly::{Point, Polynomial, HIGH_TO_LOW}; -use jolt_transcript::{Blake2bTranscript, Transcript}; #[path = "support/common.rs"] pub mod common; #[path = "support/mock.rs"] pub mod mock; -use common::{clear_claims, fr, sources}; +use common::{clear_claims, fingerprint, fr, prover, sources, verifier}; use mock::{MockCommitment, MockCommitmentScheme}; type MockPCS = MockCommitmentScheme; @@ -40,8 +39,8 @@ fn homomorphic_batch_opening_roundtrip_clear() { let (polynomials, point) = batch_polynomials(); let (claims, _) = clear_claims::(&polynomials, &point, &()); - let mut prover_transcript = Blake2bTranscript::new(b"batch-clear"); - let proof = ::prove_batch( + let mut prover_transcript = prover(b"batch-clear"); + ::prove_batch( &(), claims.clone(), sources(&polynomials), @@ -50,16 +49,20 @@ fn homomorphic_batch_opening_roundtrip_clear() { ) .expect("batch proof should be produced"); - let mut verifier_transcript = Blake2bTranscript::new(b"batch-clear"); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"batch-clear", &narg); ::verify_batch( &(), &claims, - &proof, &mut verifier_transcript, ) .expect("batch proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + fingerprint(&prover_transcript), + fingerprint(&verifier_transcript) + ); + verifier_transcript.finish().expect("proof fully consumed"); } #[test] @@ -67,8 +70,8 @@ fn homomorphic_batch_opening_rejects_tampered_clear_claim() { let (polynomials, point) = batch_polynomials(); let (claims, _) = clear_claims::(&polynomials, &point, &()); - let mut prover_transcript = Blake2bTranscript::new(b"batch-clear-tampered"); - let proof = ::prove_batch( + let mut prover_transcript = prover(b"batch-clear-tampered"); + ::prove_batch( &(), claims.clone(), sources(&polynomials), @@ -80,11 +83,11 @@ fn homomorphic_batch_opening_rejects_tampered_clear_claim() { let mut tampered = claims; tampered[1].evaluation.value += fr(1); - let mut verifier_transcript = Blake2bTranscript::new(b"batch-clear-tampered"); + let narg = prover_transcript.finish(); + let mut verifier_transcript = verifier(b"batch-clear-tampered", &narg); let result = ::verify_batch( &(), &tampered, - &proof, &mut verifier_transcript, ); assert!(result.is_err(), "tampered claim should fail"); @@ -92,7 +95,7 @@ fn homomorphic_batch_opening_rejects_tampered_clear_claim() { #[test] fn homomorphic_batch_opening_rejects_empty_claims() { - let mut transcript = Blake2bTranscript::new(b"batch-empty"); + let mut transcript = prover(b"batch-empty"); let result = ::prove_batch( &(), Vec::new(), @@ -109,7 +112,7 @@ fn homomorphic_batch_opening_rejects_mismatched_points() { let (mut claims, _) = clear_claims::(&polynomials, &point, &()); claims[1].evaluation.point = Point::new(vec![fr(8), fr(13), fr(21)]); - let mut transcript = Blake2bTranscript::new(b"batch-point-mismatch"); + let mut transcript = prover(b"batch-point-mismatch"); let result = ::prove_batch( &(), claims, @@ -125,7 +128,7 @@ fn homomorphic_batch_opening_rejects_mismatched_witness_count() { let (polynomials, point) = batch_polynomials(); let (claims, _) = clear_claims::(&polynomials, &point, &()); - let mut transcript = Blake2bTranscript::new(b"batch-mismatch"); + let mut transcript = prover(b"batch-mismatch"); let result = ::prove_batch( &(), claims, @@ -159,7 +162,7 @@ fn homomorphic_batch_opening_roundtrip_zk() { let (polynomials, point) = batch_polynomials(); let commitments = zk_commitments(&polynomials); - let mut prover_transcript = Blake2bTranscript::new(b"batch-zk"); + let mut prover_transcript = prover(b"batch-zk"); let opening = ::prove_batch_zk( &(), point.clone(), @@ -171,18 +174,22 @@ fn homomorphic_batch_opening_roundtrip_zk() { ) .expect("ZK batch proof should be produced"); - let mut verifier_transcript = Blake2bTranscript::new(b"batch-zk"); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"batch-zk", &narg); let verifier_hiding = ::verify_batch_zk( &(), point, commitments, - &opening.proof, &mut verifier_transcript, ) .expect("ZK batch proof should verify"); assert_eq!(verifier_hiding, opening.hiding_commitment); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + fingerprint(&prover_transcript), + fingerprint(&verifier_transcript) + ); + verifier_transcript.finish().expect("proof fully consumed"); } #[test] @@ -190,7 +197,7 @@ fn homomorphic_zk_batch_opening_rejects_witness_count_mismatch() { let (polynomials, point) = batch_polynomials(); let commitments = zk_commitments(&polynomials); - let mut transcript = Blake2bTranscript::new(b"batch-zk-mismatch"); + let mut transcript = prover(b"batch-zk-mismatch"); let result = ::prove_batch_zk( &(), point.clone(), diff --git a/crates/jolt-openings/tests/support/common.rs b/crates/jolt-openings/tests/support/common.rs index 5c9d6353b1..0439b110b3 100644 --- a/crates/jolt-openings/tests/support/common.rs +++ b/crates/jolt-openings/tests/support/common.rs @@ -1,9 +1,27 @@ use jolt_field::{Field, Fr, Ring}; use jolt_openings::{CommitmentScheme, EvaluationClaim, VerifierOpeningClaim}; use jolt_poly::{MultilinearPoly, Point, Polynomial, HIGH_TO_LOW}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; +pub fn protocol() -> ProtocolId { + ProtocolId::new::("jolt-openings/tests") +} + +pub fn prover(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&protocol(), session) +} + +pub fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new(&protocol(), session, narg) +} + +/// Sponge state after both sides finish, which must agree for an honest run. +pub fn fingerprint(channel: &C) -> [u8; 32] { + channel.preview().squeeze() +} + pub fn fr(value: u64) -> Fr { Fr::from_u64(value) } diff --git a/crates/jolt-openings/tests/support/mock.rs b/crates/jolt-openings/tests/support/mock.rs index 832bb72271..1263554618 100644 --- a/crates/jolt-openings/tests/support/mock.rs +++ b/crates/jolt-openings/tests/support/mock.rs @@ -1,10 +1,10 @@ use std::marker::PhantomData; use jolt_crypto::{Commitment, HomomorphicCommitment}; -use jolt_field::JoltField; +use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError, ZkOpeningScheme}; use jolt_poly::{MultilinearPoly, Polynomial}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -25,18 +25,21 @@ impl Default for MockCommitment { } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))] -pub struct MockProof { - evaluations: Vec, +/// The mock "opening proof" is the polynomial itself, sent in the clear. +fn send_evaluations( + evaluations: &[F], + transcript: &mut ProverTranscript, +) { + transcript.send(&(evaluations.len() as u64)); + transcript.send_all(evaluations); } -impl AppendToTranscript for MockCommitment { - fn append_to_transcript(&self, transcript: &mut T) { - for evaluation in &self.evaluations { - evaluation.append_to_transcript(transcript); - } - } +fn receive_evaluations( + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, OpeningsError> { + let len = usize::try_from(transcript.receive::()?) + .map_err(|_| OpeningsError::VerificationFailed)?; + Ok(transcript.receive_n(len)?) } impl Commitment for MockCommitmentScheme @@ -51,7 +54,6 @@ where F: JoltField + Serialize + DeserializeOwned, { type Field = F; - type Proof = MockProof; type ProverSetup = (); type VerifierSetup = (); type OpeningHint = (); @@ -71,31 +73,47 @@ where Ok((MockCommitment { evaluations }, ())) } - fn open + ?Sized>( + fn send_commitment(commitment: &Self::Output, transcript: &mut ProverTranscript) { + send_evaluations(&commitment.evaluations, transcript); + } + + fn receive_commitment( + _setup: &Self::VerifierSetup, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(MockCommitment { + evaluations: receive_evaluations(transcript)?, + }) + } + + fn absorb_commitment(commitment: &Self::Output, channel: &mut C) { + channel.public_all(&commitment.evaluations); + } + + fn open + ?Sized, H: Sponge>( poly: &P, _point: &[Self::Field], _eval: Self::Field, _setup: &Self::ProverSetup, _hint: Option<()>, - _transcript: &mut impl Transcript, - ) -> Result { - let evaluations = poly.to_dense().into_owned(); - Ok(MockProof { evaluations }) + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { + send_evaluations(&poly.to_dense(), transcript); + Ok(()) } - fn verify( + fn verify( commitment: &Self::Output, point: &[Self::Field], eval: Self::Field, - proof: &Self::Proof, _setup: &Self::VerifierSetup, - _transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { - if commitment.evaluations != proof.evaluations { + let evaluations = receive_evaluations(transcript)?; + if commitment.evaluations != evaluations { return Err(OpeningsError::VerificationFailed); } - let poly = Polynomial::new(proof.evaluations.clone()); - if poly.evaluate(point) != eval { + if Polynomial::new(evaluations).evaluate(point) != eval { return Err(OpeningsError::VerificationFailed); } Ok(()) @@ -146,9 +164,17 @@ pub struct MockHidingCommitment { pub eval: F, } -impl AppendToTranscript for MockHidingCommitment { - fn append_to_transcript(&self, transcript: &mut T) { - self.eval.append_to_transcript(transcript); +impl CanonicalBytes for MockHidingCommitment { + const NUM_BYTES: usize = F::NUM_BYTES; + + fn to_bytes_le(&self, out: &mut [u8]) { + self.eval.to_bytes_le(out); + } +} + +impl CanonicalDecode for MockHidingCommitment { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + F::from_bytes_le_checked(bytes).map(|eval| Self { eval }) } } @@ -166,31 +192,30 @@ where Self::commit(poly, setup) } - fn open_zk + ?Sized>( + fn open_zk + ?Sized, H: Sponge>( poly: &P, _point: &[Self::Field], eval: Self::Field, _setup: &Self::ProverSetup, _hint: Self::OpeningHint, - _transcript: &mut impl Transcript, - ) -> Result<(Self::Proof, Self::HidingCommitment, Self::Blind), OpeningsError> { - let evaluations = poly.to_dense().into_owned(); - Ok((MockProof { evaluations }, MockHidingCommitment { eval }, ())) + transcript: &mut ProverTranscript, + ) -> Result<(Self::HidingCommitment, Self::Blind), OpeningsError> { + send_evaluations(&poly.to_dense(), transcript); + Ok((MockHidingCommitment { eval }, ())) } - fn verify_zk( + fn verify_zk( commitment: &Self::Output, point: &[Self::Field], - proof: &Self::Proof, _setup: &Self::VerifierSetup, - _transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result { - if commitment.evaluations != proof.evaluations { + let evaluations = receive_evaluations(transcript)?; + if commitment.evaluations != evaluations { return Err(OpeningsError::VerificationFailed); } - let poly = Polynomial::new(proof.evaluations.clone()); Ok(MockHidingCommitment { - eval: poly.evaluate(point), + eval: Polynomial::new(evaluations).evaluate(point), }) } } diff --git a/crates/jolt-poly/Cargo.toml b/crates/jolt-poly/Cargo.toml index 4d2f30d40b..3e16d5d5ed 100644 --- a/crates/jolt-poly/Cargo.toml +++ b/crates/jolt-poly/Cargo.toml @@ -15,7 +15,6 @@ workspace = true allocative = { workspace = true, optional = true } jolt-field = { path = "../jolt-field", default-features = false } jolt-utils = { workspace = true } -jolt-transcript = { path = "../jolt-transcript", default-features = false, optional = true } serde = { workspace = true, features = ["derive", "alloc"] } tracing.workspace = true rayon = { workspace = true, optional = true } @@ -44,4 +43,3 @@ parallel = ["rayon"] # Forwards to jolt-field so `F: JoltField` implies `F: Allocative`; without # it this crate's field-generic derives cannot discharge their scalar bound. allocative = ["dep:allocative", "jolt-field/allocative"] -transcript = ["dep:jolt-transcript"] diff --git a/crates/jolt-poly/src/evaluation_claim.rs b/crates/jolt-poly/src/evaluation_claim.rs index 68e12a89c2..03ff6478af 100644 --- a/crates/jolt-poly/src/evaluation_claim.rs +++ b/crates/jolt-poly/src/evaluation_claim.rs @@ -2,9 +2,6 @@ use serde::{Deserialize, Serialize}; use crate::{Point, HIGH_TO_LOW}; -#[cfg(feature = "transcript")] -use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; - /// An evaluation of a multilinear polynomial at a point. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct EvaluationClaim { @@ -20,18 +17,3 @@ impl EvaluationClaim { } } } - -#[cfg(feature = "transcript")] -impl AppendToTranscript for EvaluationClaim -where - F: AppendToTranscript, -{ - fn append_to_transcript(&self, transcript: &mut T) { - transcript.append(&LabelWithCount(b"opening_point", self.point.len() as u64)); - for coordinate in self.point.as_slice() { - coordinate.append_to_transcript(transcript); - } - transcript.append(&Label(b"opening_eval")); - self.value.append_to_transcript(transcript); - } -} From 3f9d45f221af0dc08c19be155e18df3b87556280 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:49:36 -0400 Subject: [PATCH 08/62] refactor(dory)!: run Dory openings inside the NARG transcript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/jolt-dory/benches/dory.rs | 55 ++- crates/jolt-dory/src/lib.rs | 5 +- crates/jolt-dory/src/scheme.rs | 111 ++--- crates/jolt-dory/src/streaming.rs | 57 +-- crates/jolt-dory/src/test_support.rs | 15 + crates/jolt-dory/src/transcript.rs | 462 ++++++++++--------- crates/jolt-dory/src/types.rs | 236 +--------- crates/jolt-dory/tests/commit_open_verify.rs | 287 ++++++------ 8 files changed, 559 insertions(+), 669 deletions(-) create mode 100644 crates/jolt-dory/src/test_support.rs diff --git a/crates/jolt-dory/benches/dory.rs b/crates/jolt-dory/benches/dory.rs index d4e474f01b..09e93a9d9d 100644 --- a/crates/jolt-dory/benches/dory.rs +++ b/crates/jolt-dory/benches/dory.rs @@ -10,7 +10,19 @@ use jolt_dory::{DoryScheme, DoryVerifierSetup}; use jolt_field::{Field, Fr}; use jolt_openings::{CommitmentScheme, StreamingCommitment, ZkOpeningScheme}; use jolt_poly::{OneHotPolynomial, Polynomial}; -use jolt_transcript::Transcript; +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript, VerifierTranscript}; + +fn prover(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&ProtocolId::new::("jolt-dory/bench"), session) +} + +fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new( + &ProtocolId::new::("jolt-dory/bench"), + session, + narg, + ) +} use rand_chacha::ChaCha20Rng; use rand_core::{RngCore, SeedableRng}; @@ -68,7 +80,7 @@ fn bench_open(c: &mut Criterion) { (poly, point, eval) }, |(poly, point, eval)| { - let mut transcript = jolt_transcript::Blake2bTranscript::new(b"bench-open"); + let mut transcript = prover(b"bench-open"); DoryScheme::open(&poly, &point, eval, &setup, None, &mut transcript) }, criterion::BatchSize::SmallInput, @@ -97,21 +109,17 @@ fn bench_verify(c: &mut Criterion) { let eval = poly.evaluate(&point); let (commitment, _) = DoryScheme::commit(poly.evaluations(), &setup).unwrap(); - let mut transcript = - jolt_transcript::Blake2bTranscript::new(b"bench-verify"); - let proof = - DoryScheme::open(&poly, &point, eval, &setup, None, &mut transcript) - .unwrap(); - (commitment, point, eval, proof) + let mut transcript = prover(b"bench-verify"); + DoryScheme::open(&poly, &point, eval, &setup, None, &mut transcript) + .unwrap(); + (commitment, point, eval, transcript.finish()) }, - |(commitment, point, eval, proof)| { - let mut transcript = - jolt_transcript::Blake2bTranscript::new(b"bench-verify"); + |(commitment, point, eval, narg)| { + let mut transcript = verifier(b"bench-verify", &narg); DoryScheme::verify( &commitment, &point, eval, - &proof, &verifier_setup, &mut transcript, ) @@ -236,8 +244,7 @@ fn bench_open_zk(c: &mut Criterion) { (poly, point, eval, hint) }, |(poly, point, eval, hint)| { - let mut transcript = - jolt_transcript::Blake2bTranscript::new(b"bench-open-zk"); + let mut transcript = prover(b"bench-open-zk"); DoryScheme::open_zk(&poly, &point, eval, &setup, hint, &mut transcript) }, criterion::BatchSize::SmallInput, @@ -267,23 +274,15 @@ fn bench_verify_zk(c: &mut Criterion) { let (commitment, hint) = ::commit_zk(poly.evaluations(), &setup) .unwrap(); - let mut transcript = - jolt_transcript::Blake2bTranscript::new(b"bench-verify-zk"); - let (proof, _eval_com, _blind) = + let mut transcript = prover(b"bench-verify-zk"); + let (_eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &setup, hint, &mut transcript) .unwrap(); - (commitment, point, proof) + (commitment, point, transcript.finish()) }, - |(commitment, point, proof)| { - let mut transcript = - jolt_transcript::Blake2bTranscript::new(b"bench-verify-zk"); - DoryScheme::verify_zk( - &commitment, - &point, - &proof, - &verifier_setup, - &mut transcript, - ) + |(commitment, point, narg)| { + let mut transcript = verifier(b"bench-verify-zk", &narg); + DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut transcript) }, criterion::BatchSize::SmallInput, ); diff --git a/crates/jolt-dory/src/lib.rs b/crates/jolt-dory/src/lib.rs index df95e19f09..86fe2b20f9 100644 --- a/crates/jolt-dory/src/lib.rs +++ b/crates/jolt-dory/src/lib.rs @@ -20,7 +20,6 @@ //! [`PedersenSetup`](jolt_crypto::PedersenSetup) (use //! `PedersenSetup::derive(&prover_setup, capacity)`). //! - [`DoryCommitment`] — BN254 pairing target element (GT). -//! - [`DoryProof`] — single opening proof. //! - [`DoryProverSetup`] / [`DoryVerifierSetup`] — prover and verifier SRS. //! - [`DoryPartialCommitment`] — intermediate state for streaming commitment. //! - [`DoryHint`] — row commitments and commitment blind reusable as opening proof hint. @@ -44,6 +43,8 @@ mod routines; mod scheme; mod streaming; +#[cfg(test)] +mod test_support; mod transcript; mod types; #[cfg(not(target_arch = "wasm32"))] @@ -51,5 +52,5 @@ mod urs_lock; pub use scheme::DoryScheme; pub use types::{ - DoryCommitment, DoryHint, DoryPartialCommitment, DoryProof, DoryProverSetup, DoryVerifierSetup, + DoryCommitment, DoryHint, DoryPartialCommitment, DoryProverSetup, DoryVerifierSetup, }; diff --git a/crates/jolt-dory/src/scheme.rs b/crates/jolt-dory/src/scheme.rs index 85ca7cd5ab..7d3b892099 100644 --- a/crates/jolt-dory/src/scheme.rs +++ b/crates/jolt-dory/src/scheme.rs @@ -16,12 +16,12 @@ use jolt_crypto::{Bn254G1, Bn254GT, Commitment, DeriveSetup, JoltGroup, Pedersen use jolt_field::Fr; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError, ZkOpeningScheme}; use jolt_poly::MultilinearPoly; -use jolt_transcript::Transcript; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use rayon::prelude::*; use crate::routines::{JoltG1Routines, JoltG2Routines}; -use crate::transcript::JoltToDoryTranscript; -use crate::types::{DoryCommitment, DoryHint, DoryProof, DoryProverSetup, DoryVerifierSetup}; +use crate::transcript::{read_proof, DoryProverChannel, DoryVerifierChannel}; +use crate::types::{DoryCommitment, DoryHint, DoryProverSetup, DoryVerifierSetup}; // All jolt types below are #[repr(transparent)] over the same arkworks // inner type as their dory-pcs counterpart, guaranteeing identical layout. @@ -202,7 +202,6 @@ impl Commitment for DoryScheme { impl CommitmentScheme for DoryScheme { type Field = Fr; - type Proof = DoryProof; type ProverSetup = DoryProverSetup; type VerifierSetup = DoryVerifierSetup; type OpeningHint = DoryHint; @@ -228,15 +227,30 @@ impl CommitmentScheme for DoryScheme { Self::commit_with_mode::(poly, setup) } + fn send_commitment(commitment: &Self::Output, transcript: &mut ProverTranscript) { + transcript.send(commitment); + } + + fn receive_commitment( + _setup: &Self::VerifierSetup, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(transcript.receive()?) + } + + fn absorb_commitment(commitment: &Self::Output, channel: &mut C) { + channel.public(commitment); + } + #[tracing::instrument(skip_all, name = "DoryScheme::open")] - fn open + ?Sized>( + fn open + ?Sized, H: Sponge>( poly: &P, point: &[Fr], _eval: Fr, setup: &Self::ProverSetup, hint: Option, - transcript: &mut impl Transcript, - ) -> Result { + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { let num_vars = point.len(); let adapter = DorySourceAdapter::new(poly); let sigma = num_vars.div_ceil(2); @@ -255,9 +269,10 @@ impl CommitmentScheme for DoryScheme { ); let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); - let mut dory_transcript = JoltToDoryTranscript::new(transcript); + let mut dory_transcript = DoryProverChannel::new(transcript); - let (proof, _blind) = + // Every message of the returned proof is already in the transcript. + let (_proof, _blind) = dory::prove::( &adapter, &ark_point, @@ -269,42 +284,33 @@ impl CommitmentScheme for DoryScheme { &mut dory_transcript, ) .map_err(|e| OpeningsError::ProveFailed(format!("dory::prove failed: {e:?}")))?; - - Ok(DoryProof(proof)) + Ok(()) } #[tracing::instrument(skip_all, name = "DoryScheme::verify")] - fn verify( + fn verify( commitment: &Self::Output, point: &[Fr], eval: Fr, - proof: &Self::Proof, setup: &Self::VerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { + let proof = read_proof(transcript.clone(), point.len(), false)?; let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); let ark_eval = jolt_fr_to_ark(&eval); let ark_commitment = jolt_gt_to_ark(&commitment.0); - let mut dory_transcript = JoltToDoryTranscript::new(transcript); - - if proof.0.e2.is_some() - || proof.0.y_com.is_some() - || proof.0.sigma1_proof.is_some() - || proof.0.sigma2_proof.is_some() - || proof.0.scalar_product_proof.is_some() - { - return Err(OpeningsError::VerificationFailed); - } + let mut dory_transcript = DoryVerifierChannel::new(transcript); dory::verify::( ark_commitment, ark_eval, &ark_point, - &proof.0, + &proof, setup.0.clone().into_inner(), &mut dory_transcript, ) - .map_err(|_| OpeningsError::VerificationFailed) + .map_err(|_| OpeningsError::VerificationFailed)?; + dory_transcript.finish(&proof) } } @@ -371,19 +377,15 @@ impl ZkOpeningScheme for DoryScheme { Self::commit_with_mode::(poly, setup) } - #[expect( - clippy::type_complexity, - reason = "ZK openings return the native proof, hiding commitment, and blind" - )] #[tracing::instrument(skip_all, name = "DoryScheme::open_zk")] - fn open_zk + ?Sized>( + fn open_zk + ?Sized, H: Sponge>( poly: &P, point: &[Fr], _eval: Fr, setup: &Self::ProverSetup, hint: Self::OpeningHint, - transcript: &mut impl Transcript, - ) -> Result<(Self::Proof, Self::HidingCommitment, Self::Blind), OpeningsError> { + transcript: &mut ProverTranscript, + ) -> Result<(Self::HidingCommitment, Self::Blind), OpeningsError> { let num_vars = point.len(); let adapter = DorySourceAdapter::new(poly); let sigma = num_vars.div_ceil(2); @@ -391,7 +393,7 @@ impl ZkOpeningScheme for DoryScheme { let (row_commitments, commit_blind) = hint.into_ark_parts(); let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); - let mut dory_transcript = JoltToDoryTranscript::new(transcript); + let mut dory_transcript = DoryProverChannel::new(transcript); let (proof, y_blinding) = dory::prove::( @@ -405,6 +407,7 @@ impl ZkOpeningScheme for DoryScheme { &mut dory_transcript, ) .map_err(|e| OpeningsError::ProveFailed(format!("dory::prove (ZK) failed: {e:?}")))?; + dory_transcript.send_sigma1_responses(&proof)?; let y_com = proof .y_com @@ -414,38 +417,38 @@ impl ZkOpeningScheme for DoryScheme { OpeningsError::ProveFailed("ZK proof must return y_blinding".to_owned()) })?; - Ok((DoryProof(proof), y_com, blinding)) + Ok((y_com, blinding)) } #[tracing::instrument(skip_all, name = "DoryScheme::verify_zk")] - fn verify_zk( + fn verify_zk( commitment: &Self::Output, point: &[Fr], - proof: &Self::Proof, setup: &Self::VerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result { + let proof = read_proof(transcript.clone(), point.len(), true)?; let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); // In ZK mode dory::verify reads the evaluation commitment from `proof.y_com`, // so the caller-side eval is unused here. let dummy_eval = ::zero(); let ark_commitment = jolt_gt_to_ark(&commitment.0); - let mut dory_transcript = JoltToDoryTranscript::new(transcript); let hiding_commitment = proof - .0 .y_com .map(ark_to_jolt_g1) .ok_or(OpeningsError::VerificationFailed)?; + let mut dory_transcript = DoryVerifierChannel::new(transcript); dory::verify::( ark_commitment, dummy_eval, &ark_point, - &proof.0, + &proof, setup.0.clone().into_inner(), &mut dory_transcript, ) .map_err(|_| OpeningsError::VerificationFailed)?; + dory_transcript.finish(&proof)?; Ok(hiding_commitment) } @@ -644,8 +647,8 @@ mod tests { let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"test"); - let proof = DoryScheme::open( + let mut prove_transcript = crate::test_support::prover(b"test"); + DoryScheme::open( &poly, &point, eval, @@ -655,12 +658,13 @@ mod tests { ) .unwrap(); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"test"); + let narg = prove_transcript.finish(); + + let mut verify_transcript = crate::test_support::verifier(b"test", &narg); let result = DoryScheme::verify( &commitment, &point, eval, - &proof, &verifier_setup, &mut verify_transcript, ); @@ -729,8 +733,8 @@ mod tests { let (commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"zk-test"); - let (proof, _eval_com, _blinding) = DoryScheme::open_zk( + let mut prove_transcript = crate::test_support::prover(b"zk-test"); + let (_hiding, _blind) = DoryScheme::open_zk( &poly, &point, eval, @@ -740,14 +744,11 @@ mod tests { ) .unwrap(); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"zk-test"); - let result = DoryScheme::verify_zk( - &commitment, - &point, - &proof, - &verifier_setup, - &mut verify_transcript, - ); + let narg = prove_transcript.finish(); + + let mut verify_transcript = crate::test_support::verifier(b"zk-test", &narg); + let result = + DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut verify_transcript); assert!(result.is_ok(), "ZK verification failed: {result:?}"); } diff --git a/crates/jolt-dory/src/streaming.rs b/crates/jolt-dory/src/streaming.rs index 3e81ade5f8..96d79a65f2 100644 --- a/crates/jolt-dory/src/streaming.rs +++ b/crates/jolt-dory/src/streaming.rs @@ -467,7 +467,6 @@ mod tests { CommitmentScheme, StreamingCommitment, ZkOpeningScheme, ZkStreamingCommitment, }; use jolt_poly::{MultilinearPoly, OneHotIndexOrder, OneHotPolynomial}; - use jolt_transcript::Transcript; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -506,8 +505,8 @@ mod tests { .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"stream-open"); - let proof = DoryScheme::open( + let mut prove_transcript = crate::test_support::prover(b"stream-open"); + DoryScheme::open( &poly, &point, eval, @@ -517,12 +516,12 @@ mod tests { ) .unwrap(); let verifier_setup = DoryScheme::verifier_setup(&prover_setup); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"stream-open"); + let narg = prove_transcript.finish(); + let mut verify_transcript = crate::test_support::verifier(b"stream-open", &narg); let result = DoryScheme::verify( &streamed, &point, eval, - &proof, &verifier_setup, &mut verify_transcript, ); @@ -563,8 +562,8 @@ mod tests { .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"u64-stream-open"); - let proof = DoryScheme::open( + let mut prove_transcript = crate::test_support::prover(b"u64-stream-open"); + DoryScheme::open( &poly, &point, eval, @@ -574,12 +573,12 @@ mod tests { ) .unwrap(); let verifier_setup = DoryScheme::verifier_setup(&prover_setup); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"u64-stream-open"); + let narg = prove_transcript.finish(); + let mut verify_transcript = crate::test_support::verifier(b"u64-stream-open", &narg); let result = DoryScheme::verify( &streamed, &point, eval, - &proof, &verifier_setup, &mut verify_transcript, ); @@ -628,8 +627,8 @@ mod tests { .map(|_| ::random(&mut rng)) .collect::>(); let eval = Fr::from_u64(0); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"zero-zk-open"); - let (proof, _, _) = DoryScheme::open_zk( + let mut prove_transcript = crate::test_support::prover(b"zero-zk-open"); + let (_hiding, _blind) = DoryScheme::open_zk( &poly, &point, eval, @@ -638,14 +637,10 @@ mod tests { &mut prove_transcript, ) .unwrap(); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"zero-zk-open"); - let result = DoryScheme::verify_zk( - &commitment, - &point, - &proof, - &verifier_setup, - &mut verify_transcript, - ); + let narg = prove_transcript.finish(); + let mut verify_transcript = crate::test_support::verifier(b"zero-zk-open", &narg); + let result = + DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut verify_transcript); assert!(result.is_ok(), "zero-row ZK streaming hint should open"); } @@ -735,8 +730,8 @@ mod tests { .map(|_| ::random(&mut rng)) .collect::>(); let eval = poly.evaluate(&point); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"one-hot-zk-open"); - let (proof, _, _) = DoryScheme::open_zk( + let mut prove_transcript = crate::test_support::prover(b"one-hot-zk-open"); + let (_hiding, _blind) = DoryScheme::open_zk( &poly, &point, eval, @@ -745,14 +740,10 @@ mod tests { &mut prove_transcript, ) .unwrap(); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"one-hot-zk-open"); - let result = DoryScheme::verify_zk( - &commitment, - &point, - &proof, - &verifier_setup, - &mut verify_transcript, - ); + let narg = prove_transcript.finish(); + let mut verify_transcript = crate::test_support::verifier(b"one-hot-zk-open", &narg); + let result = + DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut verify_transcript); assert!(result.is_ok(), "one-hot ZK streaming hint should open"); } @@ -798,8 +789,8 @@ mod tests { .map(|_| ::random(&mut rng)) .collect(); let eval = poly.evaluate(&point); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"i128-stream-open"); - let proof = DoryScheme::open( + let mut prove_transcript = crate::test_support::prover(b"i128-stream-open"); + DoryScheme::open( &poly, &point, eval, @@ -809,12 +800,12 @@ mod tests { ) .unwrap(); let verifier_setup = DoryScheme::verifier_setup(&prover_setup); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"i128-stream-open"); + let narg = prove_transcript.finish(); + let mut verify_transcript = crate::test_support::verifier(b"i128-stream-open", &narg); let result = DoryScheme::verify( &streamed, &point, eval, - &proof, &verifier_setup, &mut verify_transcript, ); diff --git a/crates/jolt-dory/src/test_support.rs b/crates/jolt-dory/src/test_support.rs new file mode 100644 index 0000000000..09f01333c8 --- /dev/null +++ b/crates/jolt-dory/src/test_support.rs @@ -0,0 +1,15 @@ +//! Transcripts for the crate's unit tests. + +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript, VerifierTranscript}; + +fn protocol() -> ProtocolId { + ProtocolId::new::("jolt-dory/tests") +} + +pub(crate) fn prover(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&protocol(), session) +} + +pub(crate) fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new(&protocol(), session, narg) +} diff --git a/crates/jolt-dory/src/transcript.rs b/crates/jolt-dory/src/transcript.rs index 6e39ff04a1..885e5aca7f 100644 --- a/crates/jolt-dory/src/transcript.rs +++ b/crates/jolt-dory/src/transcript.rs @@ -1,73 +1,79 @@ -//! Bridges the `jolt-transcript` framework into dory-pcs's `DoryTranscript` trait. +//! Runs dory-pcs inside the caller's NARG transcript. //! -//! Prover/verifier parity within `jolt-dory` is by construction: both sides -//! traverse this adapter. The surrounding Jolt transcript is responsible for -//! matching the core Fiat-Shamir byte layout before this adapter is entered. - -use dory::backends::arkworks::BN254; +//! dory-pcs absorbs every prover message it relies on through +//! [`DoryTranscript::append_serde`], interleaved with its challenges in an +//! order fixed by the proof's round count. The prover adapter sends each such +//! value as a prover message, so the argument string holds exactly Dory's +//! messages in Fiat-Shamir order. dory-pcs verifies from a proof struct it +//! receives up front, so the verifier first rebuilds that struct by reading the +//! messages on a scratch copy of the transcript, then runs `dory::verify` with +//! an adapter whose every `append_serde` receives the next message from the +//! live transcript and checks it equals the value dory-pcs absorbs. +//! +//! The Σ₁ responses of a ZK proof are the one part of the proof dory-pcs never +//! absorbs. Both sides place them after the Dory messages, so the argument +//! string carries no byte the verifier reads without absorbing. + +use dory::backends::arkworks::{ArkDoryProof, ArkG2, BN254}; +use dory::messages::{ + FirstReduceMessage, ScalarProductMessage, ScalarProductProof, SecondReduceMessage, Sigma1Proof, + Sigma2Proof, VMVMessage, +}; use dory::primitives::arithmetic::Group as DoryGroup; use dory::primitives::transcript::Transcript as DoryTranscript; -use dory::primitives::DorySerialize; +use dory::primitives::{DoryDeserialize, DorySerialize}; use jolt_field::Fr; -use jolt_transcript::domain::{Label, LabelWithCount}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_openings::OpeningsError; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; -use crate::scheme::{ark_to_jolt_fr, jolt_fr_to_ark, ArkFr}; +use crate::scheme::{jolt_fr_to_ark, ArkFr, ArkG1, ArkGT}; -pub struct JoltToDoryTranscript<'a, T: Transcript> { - transcript: &'a mut T, +/// Prover side: every absorbed value becomes a prover message. +pub(crate) struct DoryProverChannel<'a, H> { + transcript: &'a mut ProverTranscript, } -impl<'a, T: Transcript> JoltToDoryTranscript<'a, T> { - pub fn new(transcript: &'a mut T) -> Self { +impl<'a, H: Sponge> DoryProverChannel<'a, H> { + pub(crate) fn new(transcript: &'a mut ProverTranscript) -> Self { Self { transcript } } + + /// Sends the Σ₁ responses dory-pcs does not absorb, after its messages. + pub(crate) fn send_sigma1_responses( + &mut self, + proof: &ArkDoryProof, + ) -> Result<(), OpeningsError> { + let sigma1 = proof.sigma1_proof.as_ref().ok_or_else(|| { + OpeningsError::ProveFailed("ZK proof must contain a Σ₁ proof".to_owned()) + })?; + for response in [&sigma1.z1, &sigma1.z2, &sigma1.z3] { + self.transcript.send_bytes(&compressed(response)); + } + Ok(()) + } } -impl> DoryTranscript for JoltToDoryTranscript<'_, T> { +impl DoryTranscript for DoryProverChannel<'_, H> { type Curve = BN254; fn append_bytes(&mut self, _label: &[u8], bytes: &[u8]) { - self.transcript - .append(&LabelWithCount(b"dory_bytes", bytes.len() as u64)); - self.transcript.append_bytes(bytes); + self.transcript.send_bytes(bytes); } fn append_field(&mut self, _label: &[u8], x: &ArkFr) { - let jolt_scalar = ark_to_jolt_fr(x); - self.transcript.append(&Label(b"dory_field")); - jolt_scalar.append_to_transcript(self.transcript); + self.transcript.send_bytes(&compressed(x)); } - #[expect( - clippy::expect_used, - reason = "transcript serialization failures are fatal" - )] fn append_group(&mut self, _label: &[u8], g: &G) { - let mut buffer = Vec::new(); - g.serialize_compressed(&mut buffer) - .expect("group serialization should not fail"); - self.transcript - .append(&LabelWithCount(b"dory_group", buffer.len() as u64)); - self.transcript.append_bytes(&buffer); + self.transcript.send_bytes(&compressed(g)); } - #[expect( - clippy::expect_used, - reason = "transcript serialization failures are fatal" - )] fn append_serde(&mut self, _label: &[u8], s: &S) { - let mut buffer = Vec::new(); - s.serialize_compressed(&mut buffer) - .expect("DorySerialize serialization should not fail"); - self.transcript - .append(&LabelWithCount(b"dory_serde", buffer.len() as u64)); - self.transcript.append_bytes(&buffer); + self.transcript.send_bytes(&compressed(s)); } fn challenge_scalar(&mut self, _label: &[u8]) -> ArkFr { - let challenge: Fr = self.transcript.challenge_scalar(); - jolt_fr_to_ark(&challenge) + jolt_fr_to_ark(&self.transcript.challenge::()) } fn reset(&mut self, _domain_label: &[u8]) { @@ -75,192 +81,232 @@ impl> DoryTranscript for JoltToDoryTranscript<'_, } } -#[cfg(test)] -mod tests { - #![expect( - clippy::expect_used, - clippy::indexing_slicing, - reason = "tests unwrap infallible serialization of well-formed group elements" - )] +/// Verifier side: every absorbed value must be the next prover message. +pub(crate) struct DoryVerifierChannel<'a, 'p, H> { + transcript: &'a mut VerifierTranscript<'p, H>, + mismatch: bool, +} - use super::*; - use crate::scheme::jolt_fr_to_ark; - use ark_bn254::{Fr as ArkBn254Fr, G1Projective}; - use ark_ec::PrimeGroup; - use ark_serialize::CanonicalSerialize; - use dory::backends::arkworks::{ArkFr as DoryArkFr, ArkG1}; - use jolt_field::Ring; - use jolt_transcript::Blake2bTranscript; - - // The framing words below are reconstructed from the documented layout - // (legacy.rs `Label` / `LabelWithCount`), NOT by calling those helpers: - // a `Label` is one 32-byte zero-padded word; a `LabelWithCount` packs the - // label into bytes 0..24 and the count big-endian into bytes 24..32. Each - // word and each payload is absorbed as its own `append_bytes` call. - - fn label_word(label: &[u8]) -> [u8; 32] { - assert!(label.len() <= 32); - let mut word = [0u8; 32]; - word[..label.len()].copy_from_slice(label); - word +impl<'a, 'p, H: Sponge> DoryVerifierChannel<'a, 'p, H> { + pub(crate) fn new(transcript: &'a mut VerifierTranscript<'p, H>) -> Self { + Self { + transcript, + mismatch: false, + } } - fn label_with_count_word(label: &[u8], count: u64) -> [u8; 32] { - assert!(label.len() <= 24); - let mut word = [0u8; 32]; - word[..label.len()].copy_from_slice(label); - word[24..].copy_from_slice(&count.to_be_bytes()); - word + /// Receives the Σ₁ responses, which must match the ones `proof` was + /// rebuilt with, then reports whether every absorbed value matched. + pub(crate) fn finish(mut self, proof: &ArkDoryProof) -> Result<(), OpeningsError> { + if let Some(sigma1) = &proof.sigma1_proof { + for response in [&sigma1.z1, &sigma1.z2, &sigma1.z3] { + self.expect(&compressed(response)); + } + } + if self.mismatch { + return Err(OpeningsError::VerificationFailed); + } + Ok(()) } - fn transcript() -> Blake2bTranscript { - Blake2bTranscript::new(b"dory-adapter-framing") + fn expect(&mut self, bytes: &[u8]) { + match self.transcript.receive_bytes(bytes.len()) { + Ok(received) if received == bytes => {} + Ok(_) | Err(_) => self.mismatch = true, + } } +} - /// `ArkFr` in scope is a type alias, which cannot be used in constructor - /// position; this builds the dory wrapper explicitly. - fn dory_ark_fr(inner: ArkBn254Fr) -> ArkFr { - DoryArkFr(inner) - } +impl DoryTranscript for DoryVerifierChannel<'_, '_, H> { + type Curve = BN254; - #[test] - fn append_bytes_frames_as_dory_bytes_count_word_then_payload() { - let payload = [0xaa, 0xbb, 0xcc, 0xdd, 0xee]; - - let mut actual = transcript(); - JoltToDoryTranscript::new(&mut actual).append_bytes(b"caller-label", &payload); - - let mut expected = transcript(); - expected.append_bytes(&label_with_count_word(b"dory_bytes", 5)); - expected.append_bytes(&payload); - assert_eq!(actual.state(), expected.state()); - - // The count is load-bearing: the same payload under a wrong count - // must diverge, otherwise the golden comparison proves nothing. - let mut wrong_count = transcript(); - wrong_count.append_bytes(&label_with_count_word(b"dory_bytes", 4)); - wrong_count.append_bytes(&payload); - assert_ne!(actual.state(), wrong_count.state()); - - // Word and payload are separate absorptions, not one concatenated - // buffer (the sponge length-prefixes each `append_bytes` call). - let mut merged = transcript(); - let mut buffer = label_with_count_word(b"dory_bytes", 5).to_vec(); - buffer.extend_from_slice(&payload); - merged.append_bytes(&buffer); - assert_ne!(actual.state(), merged.state()); + fn append_bytes(&mut self, _label: &[u8], bytes: &[u8]) { + self.expect(bytes); } - #[test] - fn append_field_frames_as_dory_field_label_then_big_endian_scalar() { - let mut actual = transcript(); - JoltToDoryTranscript::new(&mut actual).append_field( - b"caller-label", - &dory_ark_fr(ArkBn254Fr::from(0xdead_beefu64)), - ); - - // Fr absorbs as its 32-byte big-endian canonical form: 24 zero bytes - // then the value, reconstructed here without CanonicalBytes. - let mut scalar_be = [0u8; 32]; - scalar_be[24..].copy_from_slice(&0xdead_beefu64.to_be_bytes()); - - let mut expected = transcript(); - expected.append_bytes(&label_word(b"dory_field")); - expected.append_bytes(&scalar_be); - assert_eq!(actual.state(), expected.state()); - - // Little-endian absorption would be an invisible-to-roundtrip bug. - let mut little_endian = transcript(); - let mut scalar_le = [0u8; 32]; - scalar_le[..8].copy_from_slice(&0xdead_beefu64.to_le_bytes()); - little_endian.append_bytes(&label_word(b"dory_field")); - little_endian.append_bytes(&scalar_le); - assert_ne!(actual.state(), little_endian.state()); + fn append_field(&mut self, _label: &[u8], x: &ArkFr) { + self.expect(&compressed(x)); } - #[test] - fn append_group_frames_as_dory_group_count_word_then_compressed_point() { - let generator = ArkG1(G1Projective::generator()); - - let mut actual = transcript(); - JoltToDoryTranscript::new(&mut actual).append_group(b"caller-label", &generator); - - // Payload reconstructed via arkworks compressed serialization of the - // inner point; the adapter's framing is the labeled count word. - let mut payload = Vec::new(); - generator - .0 - .serialize_compressed(&mut payload) - .expect("compressed G1 serialization should not fail"); - assert_eq!(payload.len(), 32); - - let mut expected = transcript(); - expected.append_bytes(&label_with_count_word(b"dory_group", payload.len() as u64)); - expected.append_bytes(&payload); - assert_eq!(actual.state(), expected.state()); + fn append_group(&mut self, _label: &[u8], g: &G) { + self.expect(&compressed(g)); } - #[test] - fn append_serde_frames_as_dory_serde_count_word_then_compressed_value() { - let value = dory_ark_fr(ArkBn254Fr::from(42u64)); - - let mut actual = transcript(); - JoltToDoryTranscript::new(&mut actual).append_serde(b"caller-label", &value); - - let mut payload = Vec::new(); - value - .0 - .serialize_compressed(&mut payload) - .expect("compressed Fr serialization should not fail"); - assert_eq!(payload.len(), 32); - - let mut expected = transcript(); - expected.append_bytes(&label_with_count_word(b"dory_serde", payload.len() as u64)); - expected.append_bytes(&payload); - assert_eq!(actual.state(), expected.state()); + fn append_serde(&mut self, _label: &[u8], s: &S) { + self.expect(&compressed(s)); } - /// Domain separation comes from the fixed `dory_*` labels; the caller's - /// dory-side label is deliberately dropped by the adapter. - #[test] - fn caller_labels_do_not_reach_the_transcript() { - let payload = [1u8, 2, 3]; + fn challenge_scalar(&mut self, _label: &[u8]) -> ArkFr { + jolt_fr_to_ark(&self.transcript.challenge::()) + } - let mut first = transcript(); - JoltToDoryTranscript::new(&mut first).append_bytes(b"label-one", &payload); + fn reset(&mut self, _domain_label: &[u8]) { + unreachable!("reset is not invoked by dory-pcs and is intentionally unsupported") + } +} - let mut second = transcript(); - JoltToDoryTranscript::new(&mut second).append_bytes(b"label-two", &payload); +#[expect( + clippy::expect_used, + reason = "serializing a group or field element into a Vec cannot fail" +)] +fn compressed(value: &S) -> Vec { + let mut bytes = Vec::with_capacity(value.compressed_size()); + value + .serialize_compressed(&mut bytes) + .expect("Dory element serialization cannot fail"); + bytes +} - assert_eq!(first.state(), second.state()); - assert_ne!(first.state(), transcript().state()); +/// Rebuilds the Dory proof for a `num_vars`-variable opening from the next +/// prover messages, reading them from `scout`, a scratch copy of the live +/// verifier transcript. Message order mirrors `dory::verify`. +pub(crate) fn read_proof( + mut scout: VerifierTranscript<'_, H>, + num_vars: usize, + zk: bool, +) -> Result { + let sigma = num_vars.div_ceil(2); + let nu = num_vars - sigma; + let reader = &mut scout; + + let vmv_message = VMVMessage { + c: read::(reader)?, + d2: read(reader)?, + e1: read::(reader)?, + }; + let zk_prefix = if zk { + let e2 = read::(reader)?; + let y_com = read::(reader)?; + let sigma1_commitments = (read::(reader)?, read::(reader)?); + let sigma2 = Sigma2Proof { + a: read::(reader)?, + z1: read::(reader)?, + z2: read(reader)?, + }; + Some((e2, y_com, sigma1_commitments, sigma2)) + } else { + None + }; + + let mut first_messages = Vec::with_capacity(sigma); + let mut second_messages = Vec::with_capacity(sigma); + for _ in 0..sigma { + first_messages.push(FirstReduceMessage { + d1_left: read::(reader)?, + d1_right: read(reader)?, + d2_left: read(reader)?, + d2_right: read(reader)?, + e1_beta: read::(reader)?, + e2_beta: read::(reader)?, + }); + second_messages.push(SecondReduceMessage { + c_plus: read::(reader)?, + c_minus: read(reader)?, + e1_plus: read::(reader)?, + e1_minus: read(reader)?, + e2_plus: read::(reader)?, + e2_minus: read(reader)?, + }); } - /// The adapter's challenge is the Jolt transcript's scalar challenge, - /// converted — so after identical absorptions both sides must squeeze the - /// same scalar, and the adapter's state advances exactly like the direct - /// transcript's. + let Some((e2, y_com, (a1, a2), sigma2)) = zk_prefix else { + let final_message = ScalarProductMessage { + e1: read::(reader)?, + e2: read::(reader)?, + }; + return Ok(ArkDoryProof { + vmv_message, + first_messages, + second_messages, + final_message: Some(final_message), + nu, + sigma, + e2: None, + y_com: None, + sigma1_proof: None, + sigma2_proof: None, + scalar_product_proof: None, + }); + }; + + let scalar_product = ScalarProductProof { + p1: read::(reader)?, + p2: read(reader)?, + q: read(reader)?, + r: read(reader)?, + e1: read::(reader)?, + e2: read::(reader)?, + r1: read::(reader)?, + r2: read(reader)?, + r3: read(reader)?, + }; + let sigma1 = Sigma1Proof { + a1, + a2, + z1: read::(reader)?, + z2: read(reader)?, + z3: read(reader)?, + }; + Ok(ArkDoryProof { + vmv_message, + first_messages, + second_messages, + final_message: None, + nu, + sigma, + e2: Some(e2), + y_com: Some(y_com), + sigma1_proof: Some(sigma1), + sigma2_proof: Some(sigma2), + scalar_product_proof: Some(scalar_product), + }) +} + +/// Compressed width of each element type a Dory proof carries. +trait CompressedWidth { + const BYTES: usize; +} + +impl CompressedWidth for ArkFr { + const BYTES: usize = 32; +} + +impl CompressedWidth for ArkG1 { + const BYTES: usize = 32; +} + +impl CompressedWidth for ArkG2 { + const BYTES: usize = 64; +} + +impl CompressedWidth for ArkGT { + const BYTES: usize = 384; +} + +/// Reads one compressed element. +fn read(scout: &mut VerifierTranscript<'_, H>) -> Result +where + T: DoryDeserialize + CompressedWidth, + H: Sponge, +{ + let bytes = scout.receive_bytes(T::BYTES)?; + T::deserialize_compressed(bytes).map_err(|_| OpeningsError::VerificationFailed) +} + +#[cfg(test)] +mod tests { + use dory::primitives::arithmetic::Field as DoryField; + + use super::*; + #[test] - fn challenge_scalar_matches_underlying_jolt_transcript() { - let mut adapted = transcript(); - let mut adapter = JoltToDoryTranscript::new(&mut adapted); - adapter.append_bytes(b"caller-label", b"shared-absorption"); - let adapter_challenge = adapter.challenge_scalar(b"caller-label"); - - let mut direct = transcript(); - direct.append_bytes(&label_with_count_word(b"dory_bytes", 17)); - direct.append_bytes(b"shared-absorption"); - let direct_challenge: Fr = direct.challenge_scalar(); - - assert_eq!(adapter_challenge, jolt_fr_to_ark(&direct_challenge)); - assert_ne!(adapter_challenge, dory_ark_fr(ArkBn254Fr::from(0u64))); - assert_eq!(adapted.state(), direct.state()); - - // Fr conversion sanity: the transmute-based bridge is the identity on - // canonical values. - assert_eq!( - jolt_fr_to_ark(&Fr::from_u64(7)), - dory_ark_fr(ArkBn254Fr::from(7u64)), - ); + fn compressed_widths_match_arkworks() { + fn width(value: &T) { + assert_eq!(value.compressed_size(), T::BYTES); + } + width(&::one()); + width(&::identity()); + width(&::identity()); + width(&::identity()); } } diff --git a/crates/jolt-dory/src/types.rs b/crates/jolt-dory/src/types.rs index 5fc516ff37..0822867960 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -3,25 +3,15 @@ use std::io::Cursor; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use dory::backends::arkworks::{ - ArkDoryProof, ArkG1, ArkG2, ArkGT, ArkworksProverSetup, ArkworksVerifierSetup, -}; +use dory::backends::arkworks::{ArkG1, ArkG2, ArkGT, ArkworksProverSetup, ArkworksVerifierSetup}; use jolt_crypto::{Bn254G1, Bn254GT, HomomorphicCommitment}; -use jolt_field::Fr; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_field::{CanonicalBytes, CanonicalDecode, Fr}; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; -/// Caps the upstream `Vec::with_capacity(num_rounds)` allocation against -/// attacker-supplied round counts during proof deserialization. Real Dory -/// proofs use `num_rounds = ceil(log2(N/2))` for an N-coefficient polynomial, -/// so 64 covers polynomials up to 2^65 evaluations. -pub const MAX_SERIALIZED_PROOF_ROUNDS: usize = 64; - -/// Byte-size cap on a serialized proof, checked before any parsing. A -/// well-formed proof is ~4.7 KiB per round (12 group elements, GT-dominated) -/// plus a small fixed prefix/suffix, so `MAX_SERIALIZED_PROOF_ROUNDS` rounds -/// stay well under 512 KiB. -pub const MAX_SERIALIZED_PROOF_BYTES: usize = 512 * 1024; +/// Bounds the rounds any supported proof can use, and so the verifier setup's +/// per-round tables. Dory runs `ceil(num_vars / 2)` rounds, so 64 covers +/// polynomials up to 2^128 evaluations. +const MAX_PROOF_ROUNDS: usize = 64; #[derive(Clone, Debug, PartialEq, Eq)] pub struct DoryCommitment(pub Bn254GT); @@ -48,13 +38,18 @@ impl<'de> Deserialize<'de> for DoryCommitment { } } -impl AppendToTranscript for DoryCommitment { - fn append_to_transcript(&self, transcript: &mut T) { - self.0.append_to_transcript(transcript); +/// The commitment's GT element is its transcript atom. +impl CanonicalBytes for DoryCommitment { + const NUM_BYTES: usize = Bn254GT::NUM_BYTES; + + fn to_bytes_le(&self, out: &mut [u8]) { + self.0.to_bytes_le(out); } +} - fn transcript_payload_len(&self) -> Option { - self.0.transcript_payload_len() +impl CanonicalDecode for DoryCommitment { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Bn254GT::from_bytes_le_checked(bytes).map(Self) } } @@ -70,37 +65,6 @@ impl HomomorphicCommitment for DoryCommitment { } } -#[derive(Clone, Debug, PartialEq)] -pub struct DoryProof(pub ArkDoryProof); - -impl Eq for DoryProof {} - -impl Serialize for DoryProof { - fn serialize(&self, serializer: S) -> Result { - canonical_serialize(&self.0, serializer) - } -} - -impl<'de> Deserialize<'de> for DoryProof { - fn deserialize>(deserializer: D) -> Result { - let buf: Vec = Deserialize::deserialize(deserializer)?; - if buf.len() > MAX_SERIALIZED_PROOF_BYTES { - return Err(Error::custom(format!( - "Dory proof ({} bytes) exceeds maximum ({MAX_SERIALIZED_PROOF_BYTES})", - buf.len() - ))); - } - validate_proof_round_count(&buf).map_err(Error::custom)?; - let mut cursor = Cursor::new(&buf[..]); - let proof = ArkDoryProof::deserialize_compressed(&mut cursor).map_err(Error::custom)?; - // Canonical encoding: a valid parse must consume the entire buffer. - if cursor.position() != buf.len() as u64 { - return Err(Error::custom("Dory proof encoding has trailing bytes")); - } - Ok(Self(proof)) - } -} - #[derive(Clone)] pub struct DoryProverSetup(pub ArkworksProverSetup); @@ -180,9 +144,8 @@ fn canonical_serialize( } /// Caps each GT vector in a serialized verifier setup. The delta/chi tables -/// hold `max_num_rounds + 1` entries, and `MAX_SERIALIZED_PROOF_ROUNDS` -/// bounds the rounds any supported proof can use. -const MAX_SETUP_GT_VECTOR_LEN: usize = MAX_SERIALIZED_PROOF_ROUNDS + 1; +/// hold `max_num_rounds + 1` entries. +const MAX_SETUP_GT_VECTOR_LEN: usize = MAX_PROOF_ROUNDS + 1; /// Pre-validates a serialized `ArkworksVerifierSetup` before delegating to /// the upstream parser, whose `Vec` deserialization reads a u64 length @@ -227,48 +190,17 @@ fn validate_verifier_setup_structure(buf: &[u8]) -> Result<(), String> { Ok(()) } -/// Pre-validates the round count from the proof's wire bytes before invoking -/// the upstream `CanonicalDeserialize`, which calls `Vec::with_capacity(num_rounds)` -/// and would OOM on attacker-supplied lengths near `u32::MAX`. -/// -/// The prefix elements are parsed with `Validate::No`: this scan only needs -/// their wire width to locate the round count, and the real parse that -/// follows re-reads them with full (expensive, for GT) subgroup validation. -fn validate_proof_round_count(buf: &[u8]) -> Result<(), String> { - use ark_serialize::{Compress, Validate}; - let mut cursor = Cursor::new(buf); - let _: ArkGT = - CanonicalDeserialize::deserialize_with_mode(&mut cursor, Compress::Yes, Validate::No) - .map_err(|e| format!("invalid Dory proof VMV.c: {e}"))?; - let _: ArkGT = - CanonicalDeserialize::deserialize_with_mode(&mut cursor, Compress::Yes, Validate::No) - .map_err(|e| format!("invalid Dory proof VMV.d2: {e}"))?; - let _: ArkG1 = - CanonicalDeserialize::deserialize_with_mode(&mut cursor, Compress::Yes, Validate::No) - .map_err(|e| format!("invalid Dory proof VMV.e1: {e}"))?; - let num_rounds: u32 = CanonicalDeserialize::deserialize_compressed(&mut cursor) - .map_err(|e| format!("invalid Dory proof round count: {e}"))?; - if num_rounds as usize > MAX_SERIALIZED_PROOF_ROUNDS { - return Err(format!( - "Dory proof round count ({num_rounds}) exceeds maximum ({MAX_SERIALIZED_PROOF_ROUNDS})" - )); - } - Ok(()) -} - #[cfg(test)] #[expect( clippy::expect_used, clippy::unwrap_used, reason = "tests may panic on assertion failures" )] -#[expect(clippy::indexing_slicing, reason = "tests index fixture data")] mod tests { use super::*; use jolt_field::Field; use jolt_openings::CommitmentScheme; use jolt_poly::Polynomial; - use jolt_transcript::Transcript; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; @@ -310,8 +242,8 @@ mod tests { let (commitment, hint) = crate::DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - let mut prove_transcript = jolt_transcript::Blake2bTranscript::new(b"serde-vs"); - let proof = crate::DoryScheme::open( + let mut prove_transcript = crate::test_support::prover(b"serde-vs"); + crate::DoryScheme::open( &poly, &point, eval, @@ -321,12 +253,12 @@ mod tests { ) .unwrap(); - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"serde-vs"); + let narg = prove_transcript.finish(); + let mut verify_transcript = crate::test_support::verifier(b"serde-vs", &narg); let result = crate::DoryScheme::verify( &commitment, &point, eval, - &proof, &deserialized, &mut verify_transcript, ); @@ -369,128 +301,4 @@ mod tests { bytes.push(0); assert_rejected_with::(&bytes, "length mismatch"); } - - #[test] - fn dory_proof_serde_round_trip() { - let num_vars = 2; - let mut rng = ChaCha20Rng::seed_from_u64(402); - - let prover_setup = crate::DoryScheme::setup_prover(num_vars); - - let poly = Polynomial::::random(num_vars, &mut rng); - let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) - .collect(); - let eval = poly.evaluate(&point); - - let mut transcript = jolt_transcript::Blake2bTranscript::new(b"serde-bp"); - let proof = - crate::DoryScheme::open(&poly, &point, eval, &prover_setup, None, &mut transcript) - .unwrap(); - - let serialized = serde_json::to_vec(&proof).expect("serialize proof"); - let deserialized: DoryProof = - serde_json::from_slice(&serialized).expect("deserialize proof"); - - let verifier_setup = DoryVerifierSetup(prover_setup.0.to_verifier_setup()); - let (commitment, _) = crate::DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - - let mut verify_transcript = jolt_transcript::Blake2bTranscript::new(b"serde-bp"); - let result = crate::DoryScheme::verify( - &commitment, - &point, - eval, - &deserialized, - &verifier_setup, - &mut verify_transcript, - ); - assert!(result.is_ok(), "deserialized proof must verify correctly"); - } - - #[test] - fn dory_proof_rejects_oversized_buffer() { - let bytes = vec![0u8; MAX_SERIALIZED_PROOF_BYTES + 1]; - assert_rejected_with::(&bytes, "exceeds maximum"); - } - - #[test] - fn dory_proof_rejects_trailing_bytes() { - let num_vars = 2; - let mut rng = ChaCha20Rng::seed_from_u64(404); - - let prover_setup = crate::DoryScheme::setup_prover(num_vars); - let poly = Polynomial::::random(num_vars, &mut rng); - let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) - .collect(); - let eval = poly.evaluate(&point); - - let mut transcript = jolt_transcript::Blake2bTranscript::new(b"serde-trailing"); - let proof = - crate::DoryScheme::open(&poly, &point, eval, &prover_setup, None, &mut transcript) - .unwrap(); - - let mut bytes = Vec::new(); - proof - .0 - .serialize_compressed(&mut bytes) - .expect("serialize proof"); - bytes.push(0); - assert_rejected_with::(&bytes, "trailing bytes"); - } - - #[test] - fn dory_proof_rejects_oversized_round_count() { - let num_vars = 2; - let mut rng = ChaCha20Rng::seed_from_u64(403); - - let prover_setup = crate::DoryScheme::setup_prover(num_vars); - let poly = Polynomial::::random(num_vars, &mut rng); - let point: Vec = (0..num_vars) - .map(|_| ::random(&mut rng)) - .collect(); - let eval = poly.evaluate(&point); - - let mut transcript = jolt_transcript::Blake2bTranscript::new(b"serde-oversized"); - let proof = - crate::DoryScheme::open(&poly, &point, eval, &prover_setup, None, &mut transcript) - .unwrap(); - - let mut bytes = Vec::new(); - proof - .0 - .serialize_compressed(&mut bytes) - .expect("serialize proof"); - - let mut prefix = Vec::new(); - proof - .0 - .vmv_message - .c - .serialize_compressed(&mut prefix) - .expect("serialize VMV.c"); - proof - .0 - .vmv_message - .d2 - .serialize_compressed(&mut prefix) - .expect("serialize VMV.d2"); - proof - .0 - .vmv_message - .e1 - .serialize_compressed(&mut prefix) - .expect("serialize VMV.e1"); - - let mut oversized_rounds = Vec::new(); - u32::MAX - .serialize_compressed(&mut oversized_rounds) - .expect("serialize round count"); - bytes[prefix.len()..prefix.len() + oversized_rounds.len()] - .copy_from_slice(&oversized_rounds); - - let encoded = serde_json::to_vec(&bytes).expect("encode proof bytes"); - let result = serde_json::from_slice::(&encoded); - assert!(result.is_err(), "oversized round count must be rejected"); - } } diff --git a/crates/jolt-dory/tests/commit_open_verify.rs b/crates/jolt-dory/tests/commit_open_verify.rs index 41771c0651..602a726919 100644 --- a/crates/jolt-dory/tests/commit_open_verify.rs +++ b/crates/jolt-dory/tests/commit_open_verify.rs @@ -10,17 +10,28 @@ )] use dory::backends::arkworks::ArkG1; +use dory::primitives::DorySerialize; use jolt_dory::DoryScheme; use jolt_field::{Field, Fr, Ring}; use jolt_openings::{ AdditivelyHomomorphic, CommitmentScheme, StreamingCommitment, ZkOpeningScheme, }; use jolt_poly::{OneHotPolynomial, Polynomial}; -use jolt_transcript::{Blake2bTranscript, KeccakTranscript, Transcript}; +use jolt_transcript::{ + Blake2b512, Keccak, ProtocolId, ProverTranscript, Sponge, VerifierTranscript, +}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; -fn round_trip>(num_vars: usize, seed: u64, label: &'static [u8]) { +fn prover(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&ProtocolId::new::("jolt-dory/tests"), session) +} + +fn verifier<'a, H: Sponge>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, H> { + VerifierTranscript::new(&ProtocolId::new::("jolt-dory/tests"), session, narg) +} + +fn round_trip(num_vars: usize, seed: u64, label: &'static [u8]) { let mut rng = ChaCha20Rng::seed_from_u64(seed); let prover_setup = DoryScheme::setup_prover(num_vars); let verifier_setup = DoryScheme::setup_verifier(num_vars); @@ -32,41 +43,36 @@ fn round_trip>(num_vars: usize, seed: u64, label: let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); // With hint - let mut pt = T::new(label); - let proof = DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); + let mut pt = prover::(label); + DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); - let mut vt = T::new(label); - DoryScheme::verify(&commitment, &point, eval, &proof, &verifier_setup, &mut vt) + let narg = pt.finish(); + let mut vt = verifier::(label, &narg); + DoryScheme::verify(&commitment, &point, eval, &verifier_setup, &mut vt) .expect("round-trip verification (with hint) must succeed"); // Without hint - let mut pt2 = T::new(label); - let proof2 = DoryScheme::open(&poly, &point, eval, &prover_setup, None, &mut pt2).unwrap(); + let mut pt2 = prover::(label); + DoryScheme::open(&poly, &point, eval, &prover_setup, None, &mut pt2).unwrap(); - let mut vt2 = T::new(label); - DoryScheme::verify( - &commitment, - &point, - eval, - &proof2, - &verifier_setup, - &mut vt2, - ) - .expect("round-trip verification (without hint) must succeed"); + let narg2 = pt2.finish(); + let mut vt2 = verifier::(label, &narg2); + DoryScheme::verify(&commitment, &point, eval, &verifier_setup, &mut vt2) + .expect("round-trip verification (without hint) must succeed"); } #[test] fn commit_open_verify_various_sizes() { for num_vars in [2, 3, 4, 6] { - round_trip::(num_vars, 100 + num_vars as u64, b"cov-sizes"); + round_trip::(num_vars, 100 + num_vars as u64, b"cov-sizes"); } } #[test] fn commit_open_verify_both_transcripts() { let num_vars = 4; - round_trip::(num_vars, 200, b"blake2b-rt"); - round_trip::(num_vars, 200, b"keccak-rt"); + round_trip::(num_vars, 200, b"blake2b-rt"); + round_trip::(num_vars, 200, b"keccak-rt"); } #[test] @@ -150,14 +156,14 @@ fn streaming_zk_commitment_is_blinded_and_verifies() { "streaming ZK commitments must use fresh blinding" ); - let mut pt = Blake2bTranscript::new(b"stream-zk"); - let (proof, eval_com, _blind) = + let mut pt = prover::(b"stream-zk"); + let (eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); - let mut vt = Blake2bTranscript::new(b"stream-zk"); - let verified_eval_com = - DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt) - .expect("streaming ZK commitment must verify"); + let narg = pt.finish(); + let mut vt = verifier::(b"stream-zk", &narg); + let verified_eval_com = DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut vt) + .expect("streaming ZK commitment must verify"); assert_eq!(verified_eval_com, eval_com); } @@ -175,19 +181,13 @@ fn wrong_eval_rejected() { let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"wrong-eval"); - let proof = DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); + let mut pt = prover::(b"wrong-eval"); + DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); let tampered_eval = eval + Fr::from_u64(1); - let mut vt = Blake2bTranscript::new(b"wrong-eval"); - let result = DoryScheme::verify( - &commitment, - &point, - tampered_eval, - &proof, - &verifier_setup, - &mut vt, - ); + let narg = pt.finish(); + let mut vt = verifier::(b"wrong-eval", &narg); + let result = DoryScheme::verify(&commitment, &point, tampered_eval, &verifier_setup, &mut vt); assert!(result.is_err(), "tampered eval must be rejected"); } @@ -205,20 +205,14 @@ fn wrong_point_rejected() { let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"wrong-point"); - let proof = DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); + let mut pt = prover::(b"wrong-point"); + DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); let mut tampered_point = point.clone(); tampered_point[0] += Fr::from_u64(1); - let mut vt = Blake2bTranscript::new(b"wrong-point"); - let result = DoryScheme::verify( - &commitment, - &tampered_point, - eval, - &proof, - &verifier_setup, - &mut vt, - ); + let narg = pt.finish(); + let mut vt = verifier::(b"wrong-point", &narg); + let result = DoryScheme::verify(&commitment, &tampered_point, eval, &verifier_setup, &mut vt); assert!(result.is_err(), "tampered point must be rejected"); } @@ -299,8 +293,8 @@ fn wrong_commitment_rejected() { let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"wrong-commit"); - let proof = DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); + let mut pt = prover::(b"wrong-commit"); + DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); // Commit to a different polynomial let wrong_poly = Polynomial::::random(num_vars, &mut rng); @@ -308,15 +302,9 @@ fn wrong_commitment_rejected() { DoryScheme::commit(wrong_poly.evaluations(), &prover_setup).unwrap(); assert_ne!(commitment, wrong_commitment); - let mut vt = Blake2bTranscript::new(b"wrong-commit"); - let result = DoryScheme::verify( - &wrong_commitment, - &point, - eval, - &proof, - &verifier_setup, - &mut vt, - ); + let narg = pt.finish(); + let mut vt = verifier::(b"wrong-commit", &narg); + let result = DoryScheme::verify(&wrong_commitment, &point, eval, &verifier_setup, &mut vt); assert!(result.is_err(), "wrong commitment must be rejected"); } @@ -334,11 +322,12 @@ fn wrong_transcript_domain_rejected() { let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"correct-domain"); - let proof = DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); + let mut pt = prover::(b"correct-domain"); + DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); - let mut vt = Blake2bTranscript::new(b"wrong-domain"); - let result = DoryScheme::verify(&commitment, &point, eval, &proof, &verifier_setup, &mut vt); + let narg = pt.finish(); + let mut vt = verifier::(b"wrong-domain", &narg); + let result = DoryScheme::verify(&commitment, &point, eval, &verifier_setup, &mut vt); assert!(result.is_err(), "wrong transcript domain must be rejected"); } @@ -346,11 +335,11 @@ fn wrong_transcript_domain_rejected() { fn property_based_round_trip() { for seed in 0..10u64 { let num_vars = 2 + (seed as usize % 4); // 2..5 - round_trip::(num_vars, 800 + seed, b"prop-rt"); + round_trip::(num_vars, 800 + seed, b"prop-rt"); } } -fn zk_round_trip>(num_vars: usize, seed: u64, label: &'static [u8]) { +fn zk_round_trip(num_vars: usize, seed: u64, label: &'static [u8]) { let mut rng = ChaCha20Rng::seed_from_u64(seed); let prover_setup = DoryScheme::setup_prover(num_vars); let verifier_setup = DoryScheme::setup_verifier(num_vars); @@ -362,29 +351,29 @@ fn zk_round_trip>(num_vars: usize, seed: u64, labe let (commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = T::new(label); - let (proof, eval_com, _blind) = + let mut pt = prover::(label); + let (eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); - let mut vt = T::new(label); - let verified_eval_com = - DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt) - .expect("ZK round-trip verification must succeed"); + let narg = pt.finish(); + let mut vt = verifier::(label, &narg); + let verified_eval_com = DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut vt) + .expect("ZK round-trip verification must succeed"); assert_eq!(verified_eval_com, eval_com); } #[test] fn zk_round_trip_various_sizes() { for num_vars in [2, 3, 4, 6] { - zk_round_trip::(num_vars, 1100 + num_vars as u64, b"zk-cov-sizes"); + zk_round_trip::(num_vars, 1100 + num_vars as u64, b"zk-cov-sizes"); } } #[test] fn zk_round_trip_both_transcripts() { let num_vars = 4; - zk_round_trip::(num_vars, 1200, b"zk-blake2b-rt"); - zk_round_trip::(num_vars, 1200, b"zk-keccak-rt"); + zk_round_trip::(num_vars, 1200, b"zk-blake2b-rt"); + zk_round_trip::(num_vars, 1200, b"zk-keccak-rt"); } #[test] @@ -402,12 +391,13 @@ fn transparent_verify_rejects_zk_opening_proof() { let (commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"zk-proof-transparent-verify"); - let (proof, _eval_com, _blind) = + let mut pt = prover::(b"zk-proof-transparent-verify"); + let (_eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); - let mut vt = Blake2bTranscript::new(b"zk-proof-transparent-verify"); - let result = DoryScheme::verify(&commitment, &point, eval, &proof, &verifier_setup, &mut vt); + let narg = pt.finish(); + let mut vt = verifier::(b"zk-proof-transparent-verify", &narg); + let result = DoryScheme::verify(&commitment, &point, eval, &verifier_setup, &mut vt); assert!( result.is_err(), "transparent verification must reject ZK opening proofs" @@ -430,8 +420,8 @@ fn zk_wrong_commitment_rejected() { let (commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"zk-wrong-commit"); - let (proof, _eval_com, _blind) = + let mut pt = prover::(b"zk-wrong-commit"); + let (_eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); let wrong_poly = Polynomial::::random(num_vars, &mut rng); @@ -440,8 +430,9 @@ fn zk_wrong_commitment_rejected() { .unwrap(); assert_ne!(commitment, wrong_commitment); - let mut vt = Blake2bTranscript::new(b"zk-wrong-commit"); - let result = DoryScheme::verify_zk(&wrong_commitment, &point, &proof, &verifier_setup, &mut vt); + let narg = pt.finish(); + let mut vt = verifier::(b"zk-wrong-commit", &narg); + let result = DoryScheme::verify_zk(&wrong_commitment, &point, &verifier_setup, &mut vt); assert!(result.is_err(), "ZK: wrong commitment must be rejected"); } @@ -463,18 +454,13 @@ fn transparent_commitment_rejected_for_zk_blinded_proof() { let (_zk_commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"zk-transparent-reject"); - let (proof, _eval_com, _blind) = + let mut pt = prover::(b"zk-transparent-reject"); + let (_eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); - let mut vt = Blake2bTranscript::new(b"zk-transparent-reject"); - let result = DoryScheme::verify_zk( - &transparent_commitment, - &point, - &proof, - &verifier_setup, - &mut vt, - ); + let narg = pt.finish(); + let mut vt = verifier::(b"zk-transparent-reject", &narg); + let result = DoryScheme::verify_zk(&transparent_commitment, &point, &verifier_setup, &mut vt); assert!( result.is_err(), "transparent commitment must not verify against a proof using a ZK commit blind" @@ -513,8 +499,8 @@ fn zk_combined_commitment_and_hint_verify() { .collect(); let eval = weighted_poly.evaluate(&point); - let mut pt = Blake2bTranscript::new(b"zk-combined"); - let (proof, eval_com, _blind) = DoryScheme::open_zk( + let mut pt = prover::(b"zk-combined"); + let (eval_com, _blind) = DoryScheme::open_zk( &weighted_poly, &point, eval, @@ -524,15 +510,11 @@ fn zk_combined_commitment_and_hint_verify() { ) .unwrap(); - let mut vt = Blake2bTranscript::new(b"zk-combined"); - let verified_eval_com = DoryScheme::verify_zk( - &combined_commitment, - &point, - &proof, - &verifier_setup, - &mut vt, - ) - .expect("combined ZK commitment and hint must verify"); + let narg = pt.finish(); + let mut vt = verifier::(b"zk-combined", &narg); + let verified_eval_com = + DoryScheme::verify_zk(&combined_commitment, &point, &verifier_setup, &mut vt) + .expect("combined ZK commitment and hint must verify"); assert_eq!(verified_eval_com, eval_com); } @@ -551,17 +533,23 @@ fn wrong_eval_commitment_rejected_zk() { let (commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"zk-tampered-y-com"); - let (mut proof, _eval_com, _blind) = + let mut pt = prover::(b"zk-tampered-y-com"); + let (_eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); - // Replace proof.y_com (the hiding commitment to the evaluation) with a - // different valid G1. dory::verify must reject because the Σ₁/Σ₂ sub-proofs - // bind y_com cryptographically to the rest of the proof. - proof.0.y_com = Some(ArkG1::default()); - - let mut vt = Blake2bTranscript::new(b"zk-tampered-y-com"); - let result = DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt); + // Replace y_com (the hiding commitment to the evaluation) with a different + // valid G1. dory::verify must reject because the Σ₁/Σ₂ sub-proofs bind + // y_com cryptographically to the rest of the proof. y_com follows the VMV + // message (c, d2 in GT; e1 in G1) and e2 (G2) in the argument string. + let mut narg = pt.finish(); + let y_com_offset = 2 * 384 + 32 + 64; + let mut identity = Vec::new(); + ArkG1::default() + .serialize_compressed(&mut identity) + .expect("serialize G1"); + narg[y_com_offset..y_com_offset + identity.len()].copy_from_slice(&identity); + let mut vt = verifier::(b"zk-tampered-y-com", &narg); + let result = DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut vt); assert!(result.is_err(), "tampered proof.y_com must be rejected"); } @@ -580,12 +568,13 @@ fn zk_wrong_transcript_domain_rejected() { let (commitment, hint) = ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); - let mut pt = Blake2bTranscript::new(b"zk-correct-domain"); - let (proof, _eval_com, _blind) = + let mut pt = prover::(b"zk-correct-domain"); + let (_eval_com, _blind) = DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); - let mut vt = Blake2bTranscript::new(b"zk-wrong-domain"); - let result = DoryScheme::verify_zk(&commitment, &point, &proof, &verifier_setup, &mut vt); + let narg = pt.finish(); + let mut vt = verifier::(b"zk-wrong-domain", &narg); + let result = DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut vt); assert!( result.is_err(), "ZK: wrong transcript domain must be rejected" @@ -639,8 +628,8 @@ fn ragged_hint_combination_verifies() { .collect(); let eval = joint.evaluate(&point); - let mut pt = Blake2bTranscript::new(b"ragged-hints"); - let proof = DoryScheme::open( + let mut pt = prover::(b"ragged-hints"); + DoryScheme::open( &joint, &point, eval, @@ -650,14 +639,54 @@ fn ragged_hint_combination_verifies() { ) .unwrap(); - let mut vt = Blake2bTranscript::new(b"ragged-hints"); - DoryScheme::verify( - &combined_commitment, - &point, - eval, - &proof, - &verifier_setup, - &mut vt, - ) - .expect("ragged hint combination must verify"); + let narg = pt.finish(); + let mut vt = verifier::(b"ragged-hints", &narg); + DoryScheme::verify(&combined_commitment, &point, eval, &verifier_setup, &mut vt) + .expect("ragged hint combination must verify"); +} + +/// Every Dory message is absorbed: flipping a byte anywhere in a transparent or +/// ZK opening's argument string must make verification fail. +#[test] +fn byte_flips_across_the_argument_string_reject() { + let num_vars = 3; + let mut rng = ChaCha20Rng::seed_from_u64(1700); + let prover_setup = DoryScheme::setup_prover(num_vars); + let verifier_setup = DoryScheme::setup_verifier(num_vars); + let poly = Polynomial::::random(num_vars, &mut rng); + let point: Vec = (0..num_vars) + .map(|_| ::random(&mut rng)) + .collect(); + let eval = poly.evaluate(&point); + + let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); + let mut pt = prover::(b"flips"); + DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt).unwrap(); + let narg = pt.finish(); + for index in (0..narg.len()).step_by(61) { + let mut tampered = narg.clone(); + tampered[index] ^= 1; + let mut vt = verifier::(b"flips", &tampered); + let verdict = DoryScheme::verify(&commitment, &point, eval, &verifier_setup, &mut vt) + .and_then(|()| vt.finish().map_err(Into::into)); + assert!( + verdict.is_err(), + "transparent flip at byte {index} accepted" + ); + } + + let (commitment, hint) = + ::commit_zk(poly.evaluations(), &prover_setup).unwrap(); + let mut pt = prover::(b"zk-flips"); + let (_eval_com, _blind) = + DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut pt).unwrap(); + let narg = pt.finish(); + for index in (0..narg.len()).step_by(61) { + let mut tampered = narg.clone(); + tampered[index] ^= 1; + let mut vt = verifier::(b"zk-flips", &tampered); + let verdict = DoryScheme::verify_zk(&commitment, &point, &verifier_setup, &mut vt) + .and_then(|_| vt.finish().map_err(Into::into)); + assert!(verdict.is_err(), "ZK flip at byte {index} accepted"); + } } From 3ffb55add70a273401680c50f07be2aff8b0f6ad Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 13:54:36 -0400 Subject: [PATCH 09/62] refactor(sumcheck)!: send and read sumcheck rounds through the NARG transcript 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. --- .../src/optimized/inc_claim_reduction.rs | 2 +- .../src/optimized/registers_val_evaluation.rs | 2 +- crates/jolt-sumcheck/src/committed.rs | 164 +++-------- crates/jolt-sumcheck/src/domain.rs | 27 +- crates/jolt-sumcheck/src/error.rs | 36 +-- crates/jolt-sumcheck/src/lib.rs | 75 ++--- crates/jolt-sumcheck/src/proof.rs | 200 -------------- crates/jolt-sumcheck/src/prover.rs | 117 +++----- crates/jolt-sumcheck/src/recorder.rs | 206 ++++++-------- crates/jolt-sumcheck/src/round_proof.rs | 258 +++++++----------- crates/jolt-sumcheck/src/verifier.rs | 223 +++++---------- 11 files changed, 369 insertions(+), 941 deletions(-) delete mode 100644 crates/jolt-sumcheck/src/proof.rs diff --git a/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs b/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs index 37369ce390..4abbf50432 100644 --- a/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs +++ b/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs @@ -330,7 +330,7 @@ impl ProveRounds for IncKernel { let group = group(y)?; acc[0] += group[0]; acc[1] += group[1]; - Ok(acc) + Ok::<_, SumcheckError>(acc) }, ) .try_reduce(|| vec![F::zero(); 2], |a, b| Ok(merge_evals(a, b)))?; diff --git a/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs b/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs index 40ec78e837..232a67914f 100644 --- a/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs +++ b/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs @@ -268,7 +268,7 @@ impl ProveRounds for ValEvaluationKernel { || [F::Accumulator::default(); 3], |mut acc, y| { group(y, &mut acc)?; - Ok(acc) + Ok::<_, SumcheckError>(acc) }, ) .map(|acc| acc.map(|acc| acc.map(F::Accumulator::reduce))) diff --git a/crates/jolt-sumcheck/src/committed.rs b/crates/jolt-sumcheck/src/committed.rs index b57decf360..863fdbaad5 100644 --- a/crates/jolt-sumcheck/src/committed.rs +++ b/crates/jolt-sumcheck/src/committed.rs @@ -1,69 +1,36 @@ //! Committed sumcheck round messages. +//! +//! A committed round sends a vector commitment to the round polynomial's +//! `degree + 1` coefficients (padded to the round's public degree bound, which +//! fixes BlindFold's coefficient layout) instead of the coefficients. After the +//! rounds, the output-claim values are row-committed in chunks of the setup's +//! capacity. #[cfg(feature = "committed")] use jolt_crypto::VectorCommitment; -use jolt_field::{Field, JoltField}; +#[cfg(feature = "committed")] +use jolt_field::CanonicalEncoding; +use jolt_field::Field; +#[cfg(feature = "committed")] +use jolt_field::JoltField; #[cfg(feature = "committed")] use jolt_poly::UnivariatePoly; -use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; +#[cfg(feature = "committed")] +use jolt_transcript::{Channel, ProverTranscript, Sponge}; #[cfg(feature = "committed")] use rand_core::RngCore; use serde::{Deserialize, Serialize}; use crate::error::SumcheckError; #[cfg(feature = "committed")] -use crate::proof::SumcheckProof; -use crate::round_proof::RoundMessage; - -const SUMCHECK_COMMITMENT_LABEL: &[u8] = b"sumcheck_commitment"; -const OUTPUT_CLAIMS_LABEL: &[u8] = b"output_claims_coms"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct CommittedRound { - pub commitment: C, - pub degree: usize, -} - -impl RoundMessage for CommittedRound { - fn degree(&self) -> usize { - self.degree - } - - fn append_to_transcript(&self, transcript: &mut T) { - // The degree is packed into the label word so the round's degree — - // which fixes the BlindFold R1CS coefficient layout — is - // Fiat-Shamir-bound, not free prover-chosen wire data. - transcript.append(&LabelWithCount( - SUMCHECK_COMMITMENT_LABEL, - self.degree as u64, - )); - self.commitment.append_to_transcript(transcript); - } -} +use crate::round_proof::padded_coefficients; +/// Row commitments to a committed sumcheck's flattened output-claim values. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct CommittedOutputClaims { pub commitments: Vec, } -impl AppendToTranscript for CommittedOutputClaims { - fn append_to_transcript(&self, transcript: &mut T) { - transcript.append(&LabelWithCount( - OUTPUT_CLAIMS_LABEL, - self.commitments.len() as u64, - )); - for commitment in &self.commitments { - commitment.append_to_transcript(transcript); - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CommittedSumcheckProof { - pub rounds: Vec>, - pub output_claims: CommittedOutputClaims, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct VerifiedCommittedRound { pub commitment: C, @@ -202,12 +169,12 @@ impl CommittedSumcheckWitness { } } -/// Incrementally assembles a [`CommittedSumcheckProof`]: per round, commit the -/// round polynomial's coefficients with a fresh blinding, absorb the -/// commitment, and squeeze the round challenge; at the end, row-commit the -/// flattened output-claim values and absorb those commitments. Blindings are -/// drawn from the caller-supplied `rng` and retained in the witness — the -/// caller owns the randomness source, so a fixed seed reproduces the proof. +/// Records a committed sumcheck into the prover transcript: per round, commit +/// the padded round polynomial with a fresh blinding, send the commitment, and +/// draw the round challenge; at the end, row-commit the flattened output-claim +/// values and send those commitments. Blindings are drawn from the +/// caller-supplied `rng` and retained in the witness, so a fixed seed +/// reproduces the proof. #[cfg(feature = "committed")] pub struct CommittedSumcheckBuilder<'a, F, VC, R> where @@ -217,14 +184,13 @@ where { setup: &'a VC::Setup, rng: R, - rounds: Vec>, witness: CommittedSumcheckWitness, } #[cfg(feature = "committed")] impl<'a, F, VC, R> CommittedSumcheckBuilder<'a, F, VC, R> where - F: JoltField, + F: JoltField + CanonicalEncoding, VC: VectorCommitment, R: RngCore, { @@ -235,22 +201,19 @@ where Ok(Self { setup, rng, - rounds: Vec::new(), witness: CommittedSumcheckWitness::new(), }) } - /// Commit one round polynomial, absorb the commitment, and squeeze the - /// round challenge. - pub fn commit_round( + /// Commit one round polynomial of degree bound `degree`, send the + /// commitment, and draw the round challenge. + pub fn commit_round( &mut self, round_poly: &UnivariatePoly, - transcript: &mut T, - ) -> Result> - where - T: Transcript, - { - let coefficients = round_poly.coefficients().to_vec(); + degree: usize, + transcript: &mut ProverTranscript, + ) -> Result> { + let coefficients = padded_coefficients(round_poly, degree)?; if coefficients.len() > VC::capacity(self.setup) { return Err(SumcheckError::RoundExceedsCommitmentCapacity { coefficients: coefficients.len(), @@ -259,79 +222,30 @@ where } let blinding = F::random(&mut self.rng); - let witness = CommittedRoundWitness { - coefficients: coefficients.clone(), - blinding, - }; - let round = witness.commit::(self.setup)?; - round.append_to_transcript(transcript); - let challenge = transcript.challenge(); + let commitment = VC::commit(self.setup, &coefficients, &blinding); + transcript.send(&commitment); + let challenge = transcript.challenge_small(); - self.rounds.push(round); self.witness.round_coefficients.push(coefficients); self.witness.round_blindings.push(blinding); Ok(challenge) } /// Row-commit the flattened output-claim values (chunked to the setup's - /// capacity), absorb the commitments, and assemble the proof, returning it - /// with the prover-retained witness that opens it. - #[expect( - clippy::type_complexity, - reason = "a proof paired with the witness that opens it, not worth a named pair type" - )] - pub fn finish( + /// capacity), send the commitments, and return the prover-retained + /// witness that opens every commitment sent. + pub fn finish( mut self, output_claim_values: &[F], - transcript: &mut T, - ) -> Result<(SumcheckProof, CommittedSumcheckWitness), SumcheckError> - where - T: Transcript, - { + transcript: &mut ProverTranscript, + ) -> Result, SumcheckError> { let capacity = VC::capacity(self.setup); - let mut commitments = Vec::with_capacity(output_claim_values.len().div_ceil(capacity)); for row in output_claim_values.chunks(capacity) { let blinding = F::random(&mut self.rng); - let commitment = VC::commit(self.setup, row, &blinding); - commitments.push(commitment); + transcript.send(&VC::commit(self.setup, row, &blinding)); self.witness.output_claim_rows.push(row.to_vec()); self.witness.output_claim_blindings.push(blinding); } - - let output_claims = CommittedOutputClaims { commitments }; - output_claims.append_to_transcript(transcript); - Ok(( - SumcheckProof::Committed(CommittedSumcheckProof { - rounds: self.rounds, - output_claims, - }), - self.witness, - )) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct CommittedRoundWitness { - pub coefficients: Vec, - pub blinding: F, -} - -impl CommittedRoundWitness { - #[cfg(feature = "committed")] - pub fn commit( - &self, - setup: &VC::Setup, - ) -> Result, SumcheckError> - where - VC: VectorCommitment, - { - if self.coefficients.is_empty() { - return Err(SumcheckError::EmptyRoundCoefficients); - } - - Ok(CommittedRound { - commitment: VC::commit(setup, &self.coefficients, &self.blinding), - degree: self.coefficients.len() - 1, - }) + Ok(self.witness) } } diff --git a/crates/jolt-sumcheck/src/domain.rs b/crates/jolt-sumcheck/src/domain.rs index 21dc26a89a..0dc4fcff3f 100644 --- a/crates/jolt-sumcheck/src/domain.rs +++ b/crates/jolt-sumcheck/src/domain.rs @@ -1,25 +1,25 @@ //! Sumcheck round domains. use crate::error::SumcheckError; -use crate::round_proof::ClearRound; use jolt_field::Field; use jolt_poly::lagrange::{centered_domain_start, centered_power_sums, CenteredIntegerDomainError}; +use jolt_poly::UnivariatePoly; pub trait SumcheckDomain { fn round_sum_coefficients(&self, degree: usize) -> Result, SumcheckError>; - fn check_round_sum( + /// Checks that `round`'s sum over this domain equals `running_sum`. + fn check_round_sum( &self, round_index: usize, running_sum: F, - round: &R, - ) -> Result<(), SumcheckError> - where - R: ClearRound, - { - round.check_round_well_formed(round_index)?; - let coefficients = self.round_sum_coefficients(round.degree())?; - let expected = round.degree() + 1; + round: &UnivariatePoly, + ) -> Result<(), SumcheckError> { + let expected = round.coefficients().len(); + let degree = expected + .checked_sub(1) + .ok_or(SumcheckError::EmptyRoundCoefficients)?; + let coefficients = self.round_sum_coefficients(degree)?; if coefficients.len() != expected { return Err(SumcheckError::RoundSumCoefficientCountMismatch { round: round_index, @@ -28,7 +28,12 @@ pub trait SumcheckDomain { }); } - let actual = round.coefficient_linear_combination(&coefficients); + let actual = round + .coefficients() + .iter() + .zip(&coefficients) + .map(|(&coefficient, &scale)| coefficient * scale) + .sum(); if actual != running_sum { return Err(SumcheckError::RoundCheckFailed { round: round_index, diff --git a/crates/jolt-sumcheck/src/error.rs b/crates/jolt-sumcheck/src/error.rs index a449c90cbe..f0cad7ac79 100644 --- a/crates/jolt-sumcheck/src/error.rs +++ b/crates/jolt-sumcheck/src/error.rs @@ -1,6 +1,7 @@ //! Error types for sumcheck protocol verification failures. use jolt_field::Field; +use jolt_transcript::TranscriptError; /// Errors that can occur during sumcheck verification. /// @@ -10,6 +11,10 @@ use jolt_field::Field; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum SumcheckError { + /// The proof's transcript rejected a round message. + #[error("sumcheck transcript: {0}")] + Transcript(#[from] TranscriptError), + /// Round check failed: the domain sum did not match the expected value /// carried forward from the previous round. #[error("round {round}: expected sum {expected}, got {actual}")] @@ -79,17 +84,6 @@ pub enum SumcheckError { #[error("sumcheck degree {degree} cannot be represented with its coefficient count")] DegreeOverflow { degree: usize }, - /// A round polynomial encoded in compressed form had fewer than two - /// coefficients, so there is no linear term to omit. Any valid - /// compressed sumcheck round polynomial has degree ≥ 1. - #[error("round {round}: compressed round polynomial requires >= 2 coefficients, got {got}")] - CompressedPolynomialTooShort { - /// Zero-indexed round number where the malformed polynomial appeared. - round: usize, - /// Actual number of coefficients received. - got: usize, - }, - /// The domain-sum coefficient vector must have exactly one scalar per /// round-polynomial coefficient. #[error("round {round}: expected {expected} round-sum coefficients, got {got}")] @@ -109,16 +103,6 @@ pub enum SumcheckError { domain_size: usize, }, - /// The number of round polynomials in the proof does not match - /// the number of variables in the claim. - #[error("expected {expected} rounds, proof contains {got}")] - WrongNumberOfRounds { - /// Expected number of rounds (equal to `num_vars`). - expected: usize, - /// Actual number of round polynomials in the proof. - got: usize, - }, - /// A round witness did not contain any coefficients. #[error("round polynomial must contain at least one coefficient")] EmptyRoundCoefficients, @@ -203,16 +187,6 @@ pub enum SumcheckError { members: usize, }, - /// The caller selected a verifier path that is incompatible with the proof - /// wire encoding. - #[error("wrong sumcheck proof encoding: expected {expected}, got {got}")] - WrongProofEncoding { - /// Expected proof encoding. - expected: &'static str, - /// Actual proof encoding. - got: &'static str, - }, - /// A batched committed consistency was asked for an impossible point slice. #[error("batched point range overflow: offset {offset}, num_vars {num_vars}")] BatchedPointRangeOverflow { diff --git a/crates/jolt-sumcheck/src/lib.rs b/crates/jolt-sumcheck/src/lib.rs index 32930fb713..fdc3e9dc12 100644 --- a/crates/jolt-sumcheck/src/lib.rs +++ b/crates/jolt-sumcheck/src/lib.rs @@ -16,14 +16,13 @@ //! |--------|---------| //! | [`claim`] | [`SumcheckClaim`] (input statement) and [`EvaluationClaim`] (reduction output) | //! | [`batch`] | [`BatchPrelude`] — the batched head shared by verify and prove drivers | -//! | [`proof`] | [`ClearProof`], [`ClearSumcheckProof`], [`CompressedSumcheckProof`], and [`SumcheckProof`] — serializable proofs | -//! | [`verifier`] | [`SumcheckVerifier`] engine | +//! | [`verifier`] | [`SumcheckVerifier`] engine, reading rounds from the proof transcript | //! | [`prover`] | [`ProveRounds`], [`prove_batch`], and the uni-skip provers — the prove-side engine | //! | [`prover`] | [`RoundScheduler`] / [`SequentialRounds`] — the per-round member-traversal seam | //! | [`recorder`] | [`SumcheckRecorder`] — the clear/ZK proof-recording seam | //! | [`domain`] | [`SumcheckDomain`] implementations for round-sum checks | //! | `r1cs` | R1CS lowering for sumcheck verifier equations (`r1cs` feature) | -//! | [`round_proof`] | [`RoundMessage`] and [`ClearRound`] traits | +//! | [`round_proof`] | Round polynomials on the wire: fixed-width full and compressed forms | //! | [`committed`] | Commitment-backed round messages | //! | [`error`] | [`SumcheckError`] variants | //! @@ -34,27 +33,19 @@ //! - [`SumcheckStatement`] — round count and degree bound without a claimed sum. //! - [`EvaluationClaim`] — the oracle evaluation claim `g(r) = v` produced by a //! successful reduction; the caller MUST discharge it against the polynomial oracle. -//! - [`ClearProof`] — clear proof wire representation, either full or compressed. -//! - [`ClearSumcheckProof`] — a sequence of full univariate round polynomials, one per variable. -//! - [`CompressedSumcheckProof`] — owned wire form omitting each linear coefficient. -//! - [`SumcheckProof`] — clear or committed sumcheck proof data. -//! - [`CommittedSumcheckProof`] — committed round messages and output-claim commitments. +//! - [`CommittedOutputClaims`] — row commitments to a committed sumcheck's output claims. //! - [`BooleanHypercube`] — the standard `{0,1}` sumcheck round domain. //! - [`CenteredIntegerDomain`] — centered consecutive-integer sumcheck round domain. -//! - [`SumcheckError`] — error variants: `RoundCheckFailed`, `DegreeBoundExceeded`, -//! `WrongNumberOfRounds`. +//! - [`SumcheckError`] — error variants, including `RoundCheckFailed`, +//! `DegreeBoundExceeded`, and `Transcript`. //! -//! ## Verifiers -//! - [`SumcheckVerifier`] — single-instance verifier. Replays the Fiat-Shamir -//! transcript and checks each round. +//! ## Proofs live in the transcript //! -//! ## Per-round proof types -//! - [`RoundMessage`] — degree bound and transcript absorption. -//! - [`ClearRound`] — clear round polynomial evaluation and well-formedness. -//! - [`UnivariatePoly`](jolt_poly::UnivariatePoly) — raw, unlabelled absorb. -//! - [`LabeledRoundPoly`] — borrowed wrapper adding a `LabelWithCount` prefix. -//! - [`CompressedLabeledRoundPoly`] — borrowed wrapper using the compressed -//! wire format (omits the linear coefficient). +//! There is no sumcheck proof type. Provers write round polynomials (or their +//! commitments) into a [`ProverTranscript`](jolt_transcript::ProverTranscript) +//! through a [`SumcheckRecorder`]; [`SumcheckVerifier`] reads them back from a +//! [`VerifierTranscript`](jolt_transcript::VerifierTranscript). Every round is +//! sent at its public degree bound, so no length or label travels with it. //! //! # Dependency position //! @@ -67,10 +58,10 @@ //! ``` //! //! Polynomial and clear sumcheck arithmetic is generic over -//! [`Field`](jolt_field::Field). Stock clear transcript adapters additionally -//! require [`AppendToTranscript`](jolt_transcript::AppendToTranscript) where -//! field values are absorbed. Optimized Jolt kernels and commitment backends -//! retain their stronger capability bounds at their own integration points. +//! [`Field`](jolt_field::Field); transcript paths additionally require +//! [`CanonicalEncoding`](jolt_field::CanonicalEncoding) to send and draw field +//! elements. Optimized Jolt kernels and commitment backends retain their +//! stronger capability bounds at their own integration points. //! // In the jolt-verifier runtime closure: stricter panic and unsafe discipline @@ -94,7 +85,6 @@ pub mod claim; pub mod committed; pub mod domain; pub mod error; -pub mod proof; pub mod prover; #[cfg(feature = "r1cs")] pub mod r1cs; @@ -107,42 +97,22 @@ mod round_scheduler_tests; #[cfg(all(test, feature = "committed"))] mod tests; -/// Transcript label used for ordinary sumcheck round polynomials. -pub const SUMCHECK_ROUND_TRANSCRIPT_LABEL: &[u8] = b"sumcheck_poly"; -/// Transcript label used for univariate-skip round polynomials. -pub const UNISKIP_ROUND_TRANSCRIPT_LABEL: &[u8] = b"uniskip_poly"; -/// Transcript label used when a sumcheck claim scalar is absorbed before batching. -pub const SUMCHECK_CLAIM_TRANSCRIPT_LABEL: &[u8] = b"sumcheck_claim"; -/// Transcript label used when a produced opening claim is absorbed in the clear. -pub const OPENING_CLAIM_TRANSCRIPT_LABEL: &[u8] = b"opening_claim"; - -/// Absorbs a sumcheck claim scalar using Jolt's canonical transcript label. -pub fn append_sumcheck_claim(transcript: &mut T, claim: &A) -where - A: jolt_transcript::AppendToTranscript, - T: jolt_transcript::Transcript, -{ - transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, claim); -} - pub use batch::{BatchMember, BatchPrelude}; pub use claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; #[cfg(feature = "committed")] pub use committed::CommittedSumcheckBuilder; pub use committed::{ - BatchedCommittedSumcheckConsistency, CommittedOutputClaims, CommittedRound, - CommittedRoundWitness, CommittedSumcheckConsistency, CommittedSumcheckProof, + BatchedCommittedSumcheckConsistency, CommittedOutputClaims, CommittedSumcheckConsistency, CommittedSumcheckWitness, VerifiedCommittedRound, }; pub use domain::{BooleanHypercube, CenteredIntegerDomain, SumcheckDomain, SumcheckDomainSpec}; pub use error::SumcheckError; -pub use proof::{ClearProof, ClearSumcheckProof, CompressedSumcheckProof, SumcheckProof}; -#[cfg(feature = "committed")] -pub use prover::prove_uniskip_committed; pub use prover::{ prove_batch, prove_uniskip_clear, MemberFinish, MemberRound, ProveRounds, ProvedBatch, - ProvedUniskip, ProvedUniskipCommitted, RoundScheduler, SequentialRounds, + ProvedUniskip, RoundScheduler, SequentialRounds, }; +#[cfg(feature = "committed")] +pub use prover::{prove_uniskip_committed, ProvedUniskipCommitted}; #[cfg(feature = "r1cs")] pub use r1cs::{ allocate_sumcheck_r1cs_layout, append_sumcheck_r1cs_constraints, @@ -151,6 +121,9 @@ pub use r1cs::{ }; #[cfg(feature = "committed")] pub use recorder::CommittedSumcheckRecorder; -pub use recorder::{ClearSumcheckRecorder, RecordedSumcheck, SumcheckRecorder}; -pub use round_proof::{ClearRound, CompressedLabeledRoundPoly, LabeledRoundPoly, RoundMessage}; +pub use recorder::{ClearSumcheckRecorder, SumcheckRecorder}; +pub use round_proof::{ + padded_coefficients, receive_compressed_round, receive_full_round, send_compressed_round, + send_full_round, +}; pub use verifier::SumcheckVerifier; diff --git a/crates/jolt-sumcheck/src/proof.rs b/crates/jolt-sumcheck/src/proof.rs deleted file mode 100644 index d77c5c677b..0000000000 --- a/crates/jolt-sumcheck/src/proof.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! Proof structures for single and batched sumcheck protocols. - -use crate::{ - claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}, - committed::{CommittedSumcheckConsistency, CommittedSumcheckProof}, - domain::{BooleanHypercube, SumcheckDomain}, - error::SumcheckError, - round_proof::LabeledRoundPoly, - verifier::SumcheckVerifier, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, -}; -use jolt_field::Field; -use jolt_poly::{CompressedPoly, UnivariatePoly}; -use jolt_transcript::{AppendToTranscript, Transcript}; -use serde::{Deserialize, Serialize}; - -/// A sumcheck proof consisting of one univariate round polynomial per variable. -/// -/// In round $i$ the prover sends a univariate polynomial $s_i(X)$ of degree -/// at most $d$ (the claim's degree bound). The verifier checks that -/// $s_i(0) + s_i(1)$ equals the running sum, then sets the next challenge -/// $r_i$ and updates the running sum to $s_i(r_i)$. -/// -/// The proof is complete when all $n$ round polynomials have been sent; -/// the verifier is left with a single evaluation claim at the point -/// $(r_1, \ldots, r_n)$. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: Deserialize<'de>"))] -pub struct ClearSumcheckProof { - /// Round polynomials $s_1, \ldots, s_n$ in the order they were generated. - pub round_polynomials: Vec>, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: Deserialize<'de>"))] -pub struct CompressedSumcheckProof { - /// Boolean-hypercube round polynomials with the linear coefficient omitted. - pub round_polynomials: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: Deserialize<'de>"))] -pub enum ClearProof { - Full(ClearSumcheckProof), - Compressed(CompressedSumcheckProof), -} - -impl Default for ClearProof { - fn default() -> Self { - Self::Full(ClearSumcheckProof::default()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound( - serialize = "F: Serialize, C: Serialize", - deserialize = "F: Deserialize<'de>, C: Deserialize<'de>" -))] -pub enum SumcheckProof { - Clear(ClearProof), - Committed(CommittedSumcheckProof), -} - -impl SumcheckProof { - pub fn is_committed(&self) -> bool { - matches!(self, Self::Committed(_)) - } - - pub fn is_clear(&self) -> bool { - matches!(self, Self::Clear(_)) - } - - pub fn as_clear(&self) -> Option<&ClearProof> { - match self { - Self::Clear(proof) => Some(proof), - Self::Committed(_) => None, - } - } - - pub fn as_committed(&self) -> Option<&CommittedSumcheckProof> { - match self { - Self::Clear(_) => None, - Self::Committed(proof) => Some(proof), - } - } - - /// Verifies a full-round clear sumcheck proof over `domain`. - pub fn verify( - &self, - claim: &SumcheckClaim, - domain: D, - round_label: &'static [u8], - transcript: &mut T, - ) -> Result, SumcheckError> - where - F: AppendToTranscript, - T: Transcript, - D: SumcheckDomain, - { - match self { - Self::Clear(ClearProof::Full(proof)) => { - let rounds = proof - .round_polynomials - .iter() - .map(|poly| LabeledRoundPoly::new(poly, round_label)) - .collect::>(); - SumcheckVerifier::verify(claim, &rounds, domain, transcript) - } - Self::Clear(ClearProof::Compressed(_)) => Err(SumcheckError::WrongProofEncoding { - expected: "full clear", - got: "compressed clear", - }), - Self::Committed(_) => Err(SumcheckError::WrongProofEncoding { - expected: "full clear", - got: "committed", - }), - } - } - - /// Verifies a compressed clear Boolean-hypercube sumcheck proof against a - /// combined claim given as plain `(num_vars, degree, claimed_sum)`. - /// - /// Takes the claim dimensions directly rather than a [`SumcheckClaim`] so a - /// batched-verify driver that has already reduced its instances to one combined - /// claim can call it without constructing a claim value. - pub fn verify_compressed_boolean( - &self, - num_vars: usize, - degree: usize, - claimed_sum: F, - transcript: &mut T, - ) -> Result, SumcheckError> - where - F: AppendToTranscript, - T: Transcript, - { - match self { - Self::Clear(ClearProof::Compressed(proof)) => SumcheckVerifier::verify_compressed( - &SumcheckClaim::new(num_vars, degree, claimed_sum), - proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - transcript, - ), - Self::Clear(ClearProof::Full(_)) => Err(SumcheckError::WrongProofEncoding { - expected: "compressed clear", - got: "full clear", - }), - Self::Committed(_) => Err(SumcheckError::WrongProofEncoding { - expected: "compressed clear", - got: "committed", - }), - } - } - - /// Checks public consistency for a committed sumcheck proof. - /// - /// This path intentionally takes only a [`SumcheckStatement`]. Committed - /// proofs do not reveal the scalar claim, so claim relations are deferred - /// to the BlindFold verifier rather than represented with placeholder - /// values. - pub fn verify_committed_consistency( - &self, - statement: SumcheckStatement, - transcript: &mut T, - ) -> Result, SumcheckError> - where - T: Transcript, - C: Clone + AppendToTranscript, - { - match self { - Self::Committed(proof) => proof.verify_committed_consistency(statement, transcript), - Self::Clear(ClearProof::Full(_)) => Err(SumcheckError::WrongProofEncoding { - expected: "committed", - got: "full clear", - }), - Self::Clear(ClearProof::Compressed(_)) => Err(SumcheckError::WrongProofEncoding { - expected: "committed", - got: "compressed clear", - }), - } - } - - /// [`verify_committed_consistency`](Self::verify_committed_consistency) taking the - /// combined statement as plain `(num_vars, degree)`, for a batched-verify driver - /// that computes those dimensions itself rather than building a - /// [`SumcheckStatement`]. - pub fn verify_committed_consistency_dims( - &self, - num_vars: usize, - degree: usize, - transcript: &mut T, - ) -> Result, SumcheckError> - where - T: Transcript, - C: Clone + AppendToTranscript, - { - self.verify_committed_consistency(SumcheckStatement::new(num_vars, degree), transcript) - } -} diff --git a/crates/jolt-sumcheck/src/prover.rs b/crates/jolt-sumcheck/src/prover.rs index 71fe7f49d7..730e49c9ee 100644 --- a/crates/jolt-sumcheck/src/prover.rs +++ b/crates/jolt-sumcheck/src/prover.rs @@ -19,24 +19,21 @@ #[cfg(feature = "committed")] use jolt_crypto::VectorCommitment; -use jolt_field::Field; #[cfg(feature = "committed")] use jolt_field::JoltField; +use jolt_field::{CanonicalEncoding, Field}; use jolt_poly::{UnivariatePoly, UnivariatePolynomial}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; #[cfg(feature = "committed")] use rand_core::RngCore; use crate::batch::BatchPrelude; #[cfg(feature = "committed")] -use crate::committed::CommittedSumcheckBuilder; -use crate::committed::CommittedSumcheckWitness; +use crate::committed::{CommittedSumcheckBuilder, CommittedSumcheckWitness}; use crate::domain::{CenteredIntegerDomain, SumcheckDomain}; use crate::error::SumcheckError; -use crate::proof::{ClearProof, ClearSumcheckProof, SumcheckProof}; use crate::recorder::SumcheckRecorder; -use crate::round_proof::{LabeledRoundPoly, RoundMessage}; -use crate::OPENING_CLAIM_TRANSCRIPT_LABEL; +use crate::round_proof::{padded_coefficients, send_full_round}; /// One batch member's prove-side round interface, consumed by /// [`prove_batch`]'s round loop. Object-safe on purpose: a stage's members are @@ -162,17 +159,6 @@ pub struct ProvedBatch { pub member_claims: Vec, } -/// Drop trailing zero coefficients down to the minimum two (degree 1) the -/// compressed wire form requires. The batched polynomial is assembled over -/// `max_degree + 1` slots, so rounds where every active member's degree is -/// lower carry trailing zeros that must not reach the wire. -fn trim_round_polynomial(mut coefficients: Vec) -> UnivariatePoly { - while coefficients.len() > 2 && coefficients.last().is_some_and(|value| *value == F::zero()) { - let _ = coefficients.pop(); - } - UnivariatePoly::new(coefficients) -} - /// Prove one batched sumcheck, mirroring the generated verify drivers' /// structure: per round, combine the active members' round polynomials with /// their batching coefficients (an inactive member contributes the constant @@ -196,17 +182,17 @@ fn trim_round_polynomial(mut coefficients: Vec) -> UnivariatePoly( +pub fn prove_batch( prelude: &BatchPrelude, members: &mut [&mut dyn ProveRounds], scheduler: &mut dyn RoundScheduler, recorder: &mut R, - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, SumcheckError> where F: Field, R: SumcheckRecorder, - T: Transcript, + H: Sponge, { if members.len() != prelude.members.len() { return Err(SumcheckError::BatchMemberCountMismatch { @@ -303,7 +289,7 @@ where } } - let batched_poly = trim_round_polynomial(batched_coefficients); + let batched_poly = UnivariatePoly::new(batched_coefficients); let round_sum = batched_poly.evaluate(F::zero()) + batched_poly.evaluate(F::one()); if round_sum != running_claim { return Err(SumcheckError::RoundCheckFailed { @@ -313,7 +299,7 @@ where }); } - let challenge = recorder.absorb_round(&batched_poly, transcript)?; + let challenge = recorder.absorb_round(&batched_poly, prelude.max_degree, transcript)?; running_claim = batched_poly.evaluate(challenge); challenges.push(challenge); @@ -351,23 +337,21 @@ where }) } -/// A proved clear uni-skip round: the one-round full-coefficient wire proof, -/// the reduction challenge, and the output claim (the round polynomial at the -/// challenge — the batch driver absorbs it again as the remainder's input -/// claim). +/// A proved clear uni-skip round: the reduction challenge and the output claim +/// (the round polynomial at the challenge — the batch driver absorbs it again +/// as the remainder's input claim). #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProvedUniskip { - pub proof: SumcheckProof, +pub struct ProvedUniskip { pub challenge: F, pub output_claim: F, } -/// A proved committed uni-skip round: the committed wire proof, its retained -/// witness (for BlindFold), the reduction challenge, and the (prover-internal, -/// never absorbed) output claim. +/// A proved committed uni-skip round: its retained witness (for BlindFold), +/// the reduction challenge, and the (prover-internal, never absorbed) output +/// claim. +#[cfg(feature = "committed")] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ProvedUniskipCommitted { - pub proof: SumcheckProof, +pub struct ProvedUniskipCommitted { pub witness: CommittedSumcheckWitness, pub challenge: F, pub output_claim: F, @@ -376,91 +360,76 @@ pub struct ProvedUniskipCommitted { /// Self-check the uni-skip round polynomial against the verifier's round /// checks before anything reaches the transcript: degree bound and /// centered-integer-domain round sum. -fn check_uniskip_round( +fn check_uniskip_round( round_poly: &UnivariatePoly, input_claim: F, degree: usize, domain_size: usize, ) -> Result<(), SumcheckError> { - let round_degree = UnivariatePolynomial::degree(round_poly); - if round_degree > degree { - return Err(SumcheckError::DegreeBoundExceeded { - got: round_degree, - max: degree, - }); - } - CenteredIntegerDomain::new(domain_size).check_round_sum( - 0, - input_claim, - &LabeledRoundPoly::uniskip(round_poly), - ) + let padded = UnivariatePoly::new(padded_coefficients(round_poly, degree)?); + CenteredIntegerDomain::new(domain_size).check_round_sum(0, input_claim, &padded) } /// Prove a clear uni-skip first round, mirroring the verifier's -/// `uniskip::verify_clear`: absorb the full labeled round polynomial, squeeze -/// the reduction challenge, evaluate the output claim, and absorb it under -/// `b"opening_claim"` — before any post-uni-skip draw (the remainder batch's -/// coefficient squeeze in particular), which is why the absorb lives here and -/// not in the stage. +/// `uniskip::verify_clear`: send the full round polynomial, draw the reduction +/// challenge, evaluate the output claim, and send it — before any post-uni-skip +/// draw (the remainder batch's coefficient draw in particular), which is why +/// the send lives here and not in the stage. #[tracing::instrument(skip_all, name = "prove_uniskip_clear")] -pub fn prove_uniskip_clear( +pub fn prove_uniskip_clear( round_poly: UnivariatePoly, input_claim: F, degree: usize, domain_size: usize, - transcript: &mut T, -) -> Result, SumcheckError> + transcript: &mut ProverTranscript, +) -> Result, SumcheckError> where - F: Field + AppendToTranscript, - T: Transcript, + F: Field + CanonicalEncoding, + H: Sponge, { check_uniskip_round(&round_poly, input_claim, degree, domain_size)?; - LabeledRoundPoly::uniskip(&round_poly).append_to_transcript(transcript); - let challenge = transcript.challenge(); + send_full_round(&round_poly, degree, transcript)?; + let challenge = transcript.challenge_small(); let output_claim = round_poly.evaluate(challenge); - transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, &output_claim); + transcript.send(&output_claim); Ok(ProvedUniskip { - proof: SumcheckProof::Clear(ClearProof::Full(ClearSumcheckProof { - round_polynomials: vec![round_poly], - })), challenge, output_claim, }) } /// Prove a committed uni-skip first round, mirroring the verifier's -/// `uniskip::verify_zk`: commit the round polynomial (absorbing only the -/// commitment), squeeze the reduction challenge, then commit and absorb the -/// output claim. The claim scalar never reaches the transcript. Blindings -/// come from the caller-supplied `rng`. +/// `uniskip::verify_zk`: commit the round polynomial (sending only the +/// commitment), draw the reduction challenge, then commit and send the output +/// claim. The claim scalar never reaches the transcript. Blindings come from +/// the caller-supplied `rng`. #[tracing::instrument(skip_all, name = "prove_uniskip_committed")] #[cfg(feature = "committed")] -pub fn prove_uniskip_committed( +pub fn prove_uniskip_committed( round_poly: UnivariatePoly, input_claim: F, degree: usize, domain_size: usize, setup: &VC::Setup, rng: R, - transcript: &mut T, -) -> Result, SumcheckError> + transcript: &mut ProverTranscript, +) -> Result, SumcheckError> where F: JoltField, VC: VectorCommitment, - T: Transcript, + H: Sponge, R: RngCore, { check_uniskip_round(&round_poly, input_claim, degree, domain_size)?; let mut builder = CommittedSumcheckBuilder::::new(setup, rng)?; - let challenge = builder.commit_round(&round_poly, transcript)?; + let challenge = builder.commit_round(&round_poly, degree, transcript)?; let output_claim = round_poly.evaluate(challenge); - let (proof, witness) = builder.finish(&[output_claim], transcript)?; + let witness = builder.finish(&[output_claim], transcript)?; Ok(ProvedUniskipCommitted { - proof, witness, challenge, output_claim, diff --git a/crates/jolt-sumcheck/src/recorder.rs b/crates/jolt-sumcheck/src/recorder.rs index 3b73069685..041438730b 100644 --- a/crates/jolt-sumcheck/src/recorder.rs +++ b/crates/jolt-sumcheck/src/recorder.rs @@ -1,12 +1,12 @@ //! The clear/ZK recording seam for sumcheck proving. //! //! A batched sumcheck's round loop is identical in clear and ZK mode; only the -//! per-round recording differs (cleartext round polynomials vs. Pedersen -//! commitments) and whether input/output claims are appended to the transcript -//! in the clear. [`SumcheckRecorder`] captures exactly that difference so the -//! engine and the generated per-stage drivers are written once, generic over -//! the recorder. Whether transcript bytes are written is decided by the -//! recorder **type** — there is no runtime mode boolean to drift. +//! per-round message differs (cleartext round polynomials vs. vector +//! commitments) and whether input/output claims enter the transcript in the +//! clear. [`SumcheckRecorder`] captures exactly that difference so the engine +//! and the generated per-stage drivers are written once, generic over the +//! recorder. Whether claims are absorbed or sent is decided by the recorder +//! **type**; there is no runtime mode boolean to drift. //! //! The generated `begin_batch` drivers (`#[derive(SumcheckBatch)]` in //! `jolt-verifier`) call [`absorb_input_claims`](SumcheckRecorder::absorb_input_claims); @@ -19,147 +19,106 @@ use std::marker::PhantomData; #[cfg(feature = "committed")] use jolt_crypto::VectorCommitment; -use jolt_field::Field; #[cfg(feature = "committed")] use jolt_field::JoltField; -use jolt_poly::{CompressedPoly, UnivariatePoly}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_field::{CanonicalEncoding, Field}; +use jolt_poly::UnivariatePoly; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; #[cfg(feature = "committed")] use rand_core::RngCore; #[cfg(feature = "committed")] -use crate::committed::CommittedSumcheckBuilder; -use crate::committed::CommittedSumcheckWitness; +use crate::committed::{CommittedSumcheckBuilder, CommittedSumcheckWitness}; use crate::error::SumcheckError; -use crate::proof::{ClearProof, CompressedSumcheckProof, SumcheckProof}; -use crate::round_proof::{CompressedLabeledRoundPoly, RoundMessage}; -use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; - -/// Records one sumcheck's proof material, abstracting over clear vs. committed -/// (ZK) recording: `absorb_input_claims` once (from `begin_batch`), -/// `absorb_round` per round (returning the Fiat-Shamir challenge), then -/// `finish` with the flattened output-claim values. +use crate::round_proof::send_compressed_round; + +/// Records one sumcheck's proof material into the prover transcript, +/// abstracting over clear vs. committed (ZK) recording: `absorb_input_claims` +/// once (from `begin_batch`), `absorb_round` per round (returning the +/// Fiat-Shamir challenge), then `finish` with the flattened output-claim values. pub trait SumcheckRecorder { - /// The proof's commitment type parameter (`SumcheckProof`). Phantom - /// for a clear recorder; the vector-commitment output for a committed one. - type Commitment; + /// What the prover retains after recording: nothing for a clear recorder, + /// the openings of every commitment sent for a committed one. + type Witness; /// Absorb the batch's per-member input claims (present members, in - /// declaration order). Clear: each appended under `b"sumcheck_claim"`. - /// Committed: no-op — the claims' commitments were already absorbed by the - /// stage that produced them, so the transcript never sees the scalars. - fn absorb_input_claims(&mut self, input_claims: &[F], transcript: &mut T) - where - T: Transcript; - - /// Record one round polynomial and squeeze the round challenge. Clear: - /// appended compressed under `b"sumcheck_poly"`. Committed: Pedersen - /// commitment appended. - fn absorb_round( + /// declaration order). Clear: absorbed as public values. Committed: no-op, + /// since the claims' commitments were already sent by the stage that + /// produced them, so the transcript never sees the scalars. + fn absorb_input_claims(&mut self, input_claims: &[F], channel: &mut C); + + /// Record one round polynomial of degree bound `degree` and draw the round + /// challenge. Clear: sent compressed. Committed: its commitment is sent. + fn absorb_round( &mut self, round_poly: &UnivariatePoly, - transcript: &mut T, - ) -> Result> - where - T: Transcript; - - /// Record the flattened output-claim values (canonical absorb order) and - /// assemble the proof. Clear: each value appended under - /// `b"opening_claim"`. Committed: values are row-committed and only the - /// commitments are absorbed. - fn finish( + degree: usize, + transcript: &mut ProverTranscript, + ) -> Result>; + + /// Record the flattened output-claim values (canonical order). Clear: each + /// value is sent. Committed: the values are row-committed and only the + /// commitments are sent. + fn finish( self, output_claim_values: &[F], - transcript: &mut T, - ) -> Result, SumcheckError> - where - T: Transcript; -} - -/// A recorded sumcheck: the wire proof, plus (for a committed recorder) the -/// retained witness — round coefficients, output-claim rows, and their -/// blindings — that BlindFold later opens. `None` for a clear recorder. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RecordedSumcheck { - pub proof: SumcheckProof, - pub committed_witness: Option>, + transcript: &mut ProverTranscript, + ) -> Result>; } -/// The clear recorder: appends claims and compressed round polynomials to the -/// transcript in the clear and collects the rounds into a -/// [`CompressedSumcheckProof`]. Its transcript writes are byte-identical to -/// what the clear verifier reads back. -pub struct ClearSumcheckRecorder { - round_polynomials: Vec>, - _commitment: PhantomData, +/// The clear recorder: absorbs input claims publicly and sends compressed +/// round polynomials and output claims, exactly what the clear verifier reads +/// back. +pub struct ClearSumcheckRecorder { + _field: PhantomData, } -impl Default for ClearSumcheckRecorder { +impl Default for ClearSumcheckRecorder { fn default() -> Self { Self::new() } } -impl ClearSumcheckRecorder { +impl ClearSumcheckRecorder { pub fn new() -> Self { Self { - round_polynomials: Vec::new(), - _commitment: PhantomData, + _field: PhantomData, } } } -impl SumcheckRecorder for ClearSumcheckRecorder { - type Commitment = C; +impl SumcheckRecorder for ClearSumcheckRecorder { + type Witness = (); - fn absorb_input_claims(&mut self, input_claims: &[F], transcript: &mut T) - where - T: Transcript, - { - for input_claim in input_claims { - append_sumcheck_claim(transcript, input_claim); - } + fn absorb_input_claims(&mut self, input_claims: &[F], channel: &mut C) { + channel.public_all(input_claims); } - fn absorb_round( + fn absorb_round( &mut self, round_poly: &UnivariatePoly, - transcript: &mut T, - ) -> Result> - where - T: Transcript, - { - CompressedLabeledRoundPoly::sumcheck(round_poly).append_to_transcript(transcript); - let challenge = transcript.challenge(); - self.round_polynomials.push(round_poly.compress()); - Ok(challenge) + degree: usize, + transcript: &mut ProverTranscript, + ) -> Result> { + send_compressed_round(round_poly, degree, transcript)?; + Ok(transcript.challenge_small()) } - fn finish( + fn finish( self, output_claim_values: &[F], - transcript: &mut T, - ) -> Result, SumcheckError> - where - T: Transcript, - { - for opening_claim in output_claim_values { - transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, opening_claim); - } - Ok(RecordedSumcheck { - proof: SumcheckProof::Clear(ClearProof::Compressed(CompressedSumcheckProof { - round_polynomials: self.round_polynomials, - })), - committed_witness: None, - }) + transcript: &mut ProverTranscript, + ) -> Result<(), SumcheckError> { + transcript.send_all(output_claim_values); + Ok(()) } } -/// The committed (ZK) recorder: Pedersen-commits each round polynomial and the -/// output-claim rows, absorbing only the commitments — the transcript never -/// sees a claim or coefficient scalar. Input-claim absorbs are no-ops: the -/// claims' commitments were already absorbed by the stage that produced them. -/// The retained witness (coefficients, rows, blindings) is returned by +/// The committed (ZK) recorder: commits each round polynomial and the +/// output-claim rows, sending only the commitments — the transcript never sees +/// a claim or coefficient scalar. Input-claim absorbs are no-ops: the claims' +/// commitments were already sent by the stage that produced them. The retained +/// witness (coefficients, rows, blindings) is returned by /// [`finish`](SumcheckRecorder::finish) for BlindFold. #[cfg(feature = "committed")] pub struct CommittedSumcheckRecorder<'a, F, VC, R> @@ -192,37 +151,24 @@ where VC: VectorCommitment, R: RngCore, { - type Commitment = VC::Output; + type Witness = CommittedSumcheckWitness; - fn absorb_input_claims(&mut self, _input_claims: &[F], _transcript: &mut T) - where - T: Transcript, - { - } + fn absorb_input_claims(&mut self, _input_claims: &[F], _channel: &mut C) {} - fn absorb_round( + fn absorb_round( &mut self, round_poly: &UnivariatePoly, - transcript: &mut T, - ) -> Result> - where - T: Transcript, - { - self.builder.commit_round(round_poly, transcript) + degree: usize, + transcript: &mut ProverTranscript, + ) -> Result> { + self.builder.commit_round(round_poly, degree, transcript) } - fn finish( + fn finish( self, output_claim_values: &[F], - transcript: &mut T, - ) -> Result, SumcheckError> - where - T: Transcript, - { - let (proof, witness) = self.builder.finish(output_claim_values, transcript)?; - Ok(RecordedSumcheck { - proof, - committed_witness: Some(witness), - }) + transcript: &mut ProverTranscript, + ) -> Result, SumcheckError> { + self.builder.finish(output_claim_values, transcript) } } diff --git a/crates/jolt-sumcheck/src/round_proof.rs b/crates/jolt-sumcheck/src/round_proof.rs index c6265d4b89..a43915e38d 100644 --- a/crates/jolt-sumcheck/src/round_proof.rs +++ b/crates/jolt-sumcheck/src/round_proof.rs @@ -1,167 +1,115 @@ -//! Per-round sumcheck messages. - -use jolt_field::Field; -use jolt_poly::{UnivariatePoly, UnivariatePolynomial}; -use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; +//! Round polynomials on the wire. +//! +//! A round polynomial of degree bound `d` always travels at full width, so its +//! shape is fixed by public parameters: the full form sends the `d + 1` +//! coefficients, and the compressed Boolean-hypercube form sends the `d` +//! coefficients `[c0, c2, ..., cd]`, from which the verifier recovers `c1` +//! through `s(0) + s(1) = running_sum`. A polynomial of lower degree is padded +//! with zero coefficients. + +use jolt_field::{CanonicalEncoding, Field}; +use jolt_poly::{CompressedPoly, UnivariatePoly, UnivariatePolynomial}; +use jolt_transcript::{ProverTranscript, Sponge, VerifierTranscript}; use crate::error::SumcheckError; -use crate::{SUMCHECK_ROUND_TRANSCRIPT_LABEL, UNISKIP_ROUND_TRANSCRIPT_LABEL}; - -/// Common interface for one sumcheck round message. -pub trait RoundMessage { - fn degree(&self) -> usize; - fn append_to_transcript(&self, transcript: &mut T); +/// `poly`'s coefficients padded with zeros to `degree + 1`. +/// +/// # Errors +/// +/// [`SumcheckError::DegreeBoundExceeded`] if `poly` exceeds `degree`. +pub fn padded_coefficients( + poly: &UnivariatePoly, + degree: usize, +) -> Result, SumcheckError> { + let width = degree + .checked_add(1) + .ok_or(SumcheckError::DegreeOverflow { degree })?; + let coefficients = poly.coefficients(); + if coefficients.len() > width { + return Err(SumcheckError::DegreeBoundExceeded { + got: UnivariatePolynomial::degree(poly), + max: degree, + }); + } + let mut padded = coefficients.to_vec(); + padded.resize(width, F::zero()); + Ok(padded) } -/// A round message whose polynomial is available to the verifier. -pub trait ClearRound: RoundMessage { - fn evaluate(&self, challenge: F) -> F; - - fn coefficient_linear_combination(&self, coefficients: &[F]) -> F; - - fn check_round_well_formed(&self, _round: usize) -> Result<(), SumcheckError> { - Ok(()) - } +/// Sends `poly` as a full round message of degree bound `degree`. +/// +/// # Errors +/// +/// [`SumcheckError::DegreeBoundExceeded`] if `poly` exceeds `degree`. +pub fn send_full_round( + poly: &UnivariatePoly, + degree: usize, + transcript: &mut ProverTranscript, +) -> Result<(), SumcheckError> +where + F: Field + CanonicalEncoding, + H: Sponge, +{ + transcript.send_all(&padded_coefficients(poly, degree)?); + Ok(()) } -impl RoundMessage for UnivariatePoly { - fn degree(&self) -> usize { - UnivariatePolynomial::degree(self) - } - - fn append_to_transcript(&self, transcript: &mut T) { - for coeff in self.coefficients() { - coeff.append_to_transcript(transcript); - } - } -} - -impl ClearRound for UnivariatePoly { - fn evaluate(&self, challenge: F) -> F { - UnivariatePoly::evaluate(self, challenge) - } - - fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { - self.coefficients() - .iter() - .zip(coefficients) - .map(|(&coefficient, &scale)| coefficient * scale) - .sum() - } +/// Sends `poly` as a compressed Boolean-hypercube round message of degree +/// bound `degree >= 1`, omitting the linear coefficient. +/// +/// # Errors +/// +/// [`SumcheckError::DegreeBoundExceeded`] if `poly` exceeds `degree`. +pub fn send_compressed_round( + poly: &UnivariatePoly, + degree: usize, + transcript: &mut ProverTranscript, +) -> Result<(), SumcheckError> +where + F: Field + CanonicalEncoding, + H: Sponge, +{ + let padded = padded_coefficients(poly, degree.max(1))?; + let compressed = UnivariatePoly::new(padded).compress(); + transcript.send_all(compressed.coeffs_except_linear_term()); + Ok(()) } -/// Round polynomial paired with a Fiat-Shamir domain-separation label. -pub struct LabeledRoundPoly<'a, F: Field> { - poly: &'a UnivariatePoly, - label: &'static [u8], -} - -impl<'a, F: Field> LabeledRoundPoly<'a, F> { - pub fn new(poly: &'a UnivariatePoly, label: &'static [u8]) -> Self { - Self { poly, label } - } - - pub fn sumcheck(poly: &'a UnivariatePoly) -> Self { - Self::new(poly, SUMCHECK_ROUND_TRANSCRIPT_LABEL) - } - - pub fn uniskip(poly: &'a UnivariatePoly) -> Self { - Self::new(poly, UNISKIP_ROUND_TRANSCRIPT_LABEL) - } -} - -impl RoundMessage for LabeledRoundPoly<'_, F> { - fn degree(&self) -> usize { - as RoundMessage>::degree(self.poly) - } - - fn append_to_transcript(&self, transcript: &mut T) { - let coeffs = self.poly.coefficients(); - transcript.append(&LabelWithCount(self.label, coeffs.len() as u64)); - for coeff in coeffs { - coeff.append_to_transcript(transcript); - } - } +/// Receives a full round message of degree bound `degree`. +/// +/// # Errors +/// +/// [`SumcheckError::Transcript`] if the proof does not hold `degree + 1` +/// canonical coefficients. +pub fn receive_full_round( + degree: usize, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, SumcheckError> +where + F: Field + CanonicalEncoding, + H: Sponge, +{ + let width = degree + .checked_add(1) + .ok_or(SumcheckError::DegreeOverflow { degree })?; + Ok(UnivariatePoly::new(transcript.receive_n(width)?)) } -impl ClearRound for LabeledRoundPoly<'_, F> { - fn evaluate(&self, challenge: F) -> F { - as ClearRound>::evaluate(self.poly, challenge) - } - - fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { - as ClearRound>::coefficient_linear_combination( - self.poly, - coefficients, - ) - } -} - -/// Compressed round polynomial with label. Wire format omits the linear -/// coefficient `c_1`; the verifier recovers it from the sum-check invariant -/// `running_sum = s(0) + s(1) = 2·c_0 + c_1 + c_2 + … + c_d`. -pub struct CompressedLabeledRoundPoly<'a, F: Field> { - poly: &'a UnivariatePoly, - label: &'static [u8], -} - -impl<'a, F: Field> CompressedLabeledRoundPoly<'a, F> { - pub fn new(poly: &'a UnivariatePoly, label: &'static [u8]) -> Self { - Self { poly, label } - } - - pub fn sumcheck(poly: &'a UnivariatePoly) -> Self { - Self::new(poly, SUMCHECK_ROUND_TRANSCRIPT_LABEL) - } - - pub fn uniskip(poly: &'a UnivariatePoly) -> Self { - Self::new(poly, UNISKIP_ROUND_TRANSCRIPT_LABEL) - } -} - -impl RoundMessage for CompressedLabeledRoundPoly<'_, F> { - fn degree(&self) -> usize { - as RoundMessage>::degree(self.poly) - } - - fn append_to_transcript(&self, transcript: &mut T) { - let coeffs = self.poly.coefficients(); - // An empty round polynomial would absorb nothing (not even the - // label) and silently desynchronize the prover and verifier - // transcripts; every construction path produces >= 2 coefficients. - debug_assert!(!coeffs.is_empty(), "round polynomial has no coefficients"); - let Some((constant, rest)) = coeffs.split_first() else { - return; - }; - transcript.append(&LabelWithCount(self.label, (coeffs.len() - 1) as u64)); - constant.append_to_transcript(transcript); - for c in rest.iter().skip(1) { - c.append_to_transcript(transcript); - } - } -} - -impl ClearRound for CompressedLabeledRoundPoly<'_, F> { - fn evaluate(&self, challenge: F) -> F { - as ClearRound>::evaluate(self.poly, challenge) - } - - fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { - as ClearRound>::coefficient_linear_combination( - self.poly, - coefficients, - ) - } - - fn check_round_well_formed(&self, round: usize) -> Result<(), SumcheckError> { - let coeffs = self.poly.coefficients(); - if coeffs.len() < 2 { - return Err(SumcheckError::CompressedPolynomialTooShort { - round, - got: coeffs.len(), - }); - } - Ok(()) - } +/// Receives a compressed Boolean-hypercube round message of degree bound +/// `degree >= 1`. +/// +/// # Errors +/// +/// [`SumcheckError::Transcript`] if the proof does not hold `degree` canonical +/// coefficients. +pub fn receive_compressed_round( + degree: usize, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, SumcheckError> +where + F: Field + CanonicalEncoding, + H: Sponge, +{ + Ok(CompressedPoly::new(transcript.receive_n(degree.max(1))?)) } diff --git a/crates/jolt-sumcheck/src/verifier.rs b/crates/jolt-sumcheck/src/verifier.rs index 5ddad27901..e1cc57ee95 100644 --- a/crates/jolt-sumcheck/src/verifier.rs +++ b/crates/jolt-sumcheck/src/verifier.rs @@ -1,217 +1,116 @@ -//! Sumcheck verifier: checks round polynomials against the claimed sum. +//! Sumcheck verifier: reads round polynomials from the proof and checks them +//! against the running sum. -use jolt_field::Field; -use jolt_poly::UnivariatePolynomial; -use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; +use jolt_field::{CanonicalDecode, CanonicalEncoding, Field}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; use crate::claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; use crate::committed::{ - CommittedRound, CommittedSumcheckConsistency, CommittedSumcheckProof, VerifiedCommittedRound, + CommittedOutputClaims, CommittedSumcheckConsistency, VerifiedCommittedRound, }; -use crate::domain::{BooleanHypercube, SumcheckDomain}; +use crate::domain::SumcheckDomain; use crate::error::SumcheckError; -use crate::proof::CompressedSumcheckProof; -use crate::round_proof::{ClearRound, RoundMessage}; +use crate::round_proof::{receive_compressed_round, receive_full_round}; /// Stateless sumcheck verifier engine. pub struct SumcheckVerifier; impl SumcheckVerifier { - /// Verifies a sumcheck proof. + /// Verifies full-coefficient rounds over `domain`. /// /// For each round $i = 0, \ldots, n-1$: - /// 1. The degree bound is enforced against `claim.degree`. - /// 2. The round sum is checked against the running sum in `domain`. - /// 3. The round message is absorbed into the transcript. - /// 4. A challenge $r_i$ is squeezed from the transcript. - /// 5. The running sum is updated to the round polynomial at $r_i$. + /// 1. The round polynomial's `claim.degree + 1` coefficients are received. + /// 2. Its sum over `domain` is checked against the running sum. + /// 3. A challenge $r_i$ is drawn from the small challenge set. + /// 4. The running sum becomes the round polynomial at $r_i$. /// /// On success, returns an [`EvaluationClaim`] `{ point: r, value: v }` - /// where `v` is the final evaluation and `r = (r_1, ..., r_n)` is the - /// challenge vector. - /// - /// # Errors - /// - /// Returns [`SumcheckError`] if any round check fails, a degree bound - /// is exceeded, or the proof has the wrong number of rounds. + /// where `v` is the final evaluation and `r = (r_1, ..., r_n)`. /// /// # Soundness /// - /// When `claim.num_vars == 0`, this function performs no transcript - /// interaction and no checks: it returns + /// When `claim.num_vars == 0`, this reads nothing and returns /// `EvaluationClaim { point: Point::default(), value: claim.claimed_sum }`. - /// Sumcheck trivially reduces to a single oracle query at that point, - /// so the caller MUST verify `claim.claimed_sum` against the - /// commitment/oracle layer to retain soundness. + /// The caller MUST verify `claim.claimed_sum` against the oracle layer. #[tracing::instrument(skip_all, name = "SumcheckVerifier::verify")] - pub fn verify( + pub fn verify( claim: &SumcheckClaim, - round_proofs: &[R], domain: D, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result, SumcheckError> where - F: Field, - T: Transcript, - R: ClearRound, + F: Field + CanonicalEncoding, + H: Sponge, D: SumcheckDomain, { - if round_proofs.len() != claim.num_vars { - return Err(SumcheckError::WrongNumberOfRounds { - expected: claim.num_vars, - got: round_proofs.len(), - }); - } - let mut running_sum = claim.claimed_sum; let mut challenges = Vec::with_capacity(claim.num_vars); - - for (round, round_proof) in round_proofs.iter().enumerate() { - if round_proof.degree() > claim.degree { - return Err(SumcheckError::DegreeBoundExceeded { - got: round_proof.degree(), - max: claim.degree, - }); - } - domain.check_round_sum(round, running_sum, round_proof)?; - round_proof.append_to_transcript(transcript); - let r: F = transcript.challenge(); - running_sum = round_proof.evaluate(r); + for round in 0..claim.num_vars { + let round_poly = receive_full_round(claim.degree, transcript)?; + domain.check_round_sum(round, running_sum, &round_poly)?; + let r: F = transcript.challenge_small(); + running_sum = round_poly.evaluate(r); challenges.push(r); } - Ok(EvaluationClaim::new(challenges, running_sum)) } + /// Verifies compressed Boolean-hypercube rounds, recovering each round's + /// linear coefficient from the running sum. Soundness as for + /// [`verify`](Self::verify). #[tracing::instrument(skip_all, name = "SumcheckVerifier::verify_compressed")] - pub fn verify_compressed( + pub fn verify_compressed( claim: &SumcheckClaim, - proof: &CompressedSumcheckProof, - domain: BooleanHypercube, - round_label: &'static [u8], - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result, SumcheckError> where - F: Field + AppendToTranscript, - T: Transcript, + F: Field + CanonicalEncoding, + H: Sponge, { - if proof.round_polynomials.len() != claim.num_vars { - return Err(SumcheckError::WrongNumberOfRounds { - expected: claim.num_vars, - got: proof.round_polynomials.len(), - }); - } - let BooleanHypercube = domain; - let mut running_sum = claim.claimed_sum; let mut challenges = Vec::with_capacity(claim.num_vars); - - for (round, round_proof) in proof.round_polynomials.iter().enumerate() { - if round_proof.degree() > claim.degree { - return Err(SumcheckError::DegreeBoundExceeded { - got: round_proof.degree(), - max: claim.degree, - }); - } - let coeffs = round_proof.coeffs_except_linear_term(); - if coeffs.is_empty() { - return Err(SumcheckError::CompressedPolynomialTooShort { round, got: 0 }); - } - - transcript.append(&LabelWithCount(round_label, coeffs.len() as u64)); - for coeff in coeffs { - coeff.append_to_transcript(transcript); - } - let r: F = transcript.challenge(); - running_sum = round_proof.evaluate_with_hint(running_sum, r); + for _ in 0..claim.num_vars { + let round_poly = receive_compressed_round(claim.degree, transcript)?; + let r: F = transcript.challenge_small(); + running_sum = round_poly.evaluate_with_hint(running_sum, r); challenges.push(r); } - Ok(EvaluationClaim::new(challenges, running_sum)) } - /// Checks committed sumcheck rounds and returns the transcript-derived data. - #[tracing::instrument( - skip_all, - name = "SumcheckVerifier::verify_committed_round_consistency" + /// Reads committed rounds of degree bound `statement.degree`, then the + /// `num_output_commitments` output-claim row commitments. + /// + /// Each round commitment is followed by its challenge. Committed proofs + /// reveal no claim scalars, so the round relations are left to BlindFold; + /// this returns the commitments, degrees, and challenges it binds. + #[tracing::instrument(skip_all, name = "SumcheckVerifier::verify_committed")] + #[expect( + clippy::type_complexity, + reason = "the round consistency and the output-claim commitments are read together" )] - pub fn verify_committed_round_consistency( + pub fn verify_committed( statement: SumcheckStatement, - round_proofs: &[CommittedRound], - transcript: &mut T, - ) -> Result, SumcheckError> + num_output_commitments: usize, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result<(CommittedSumcheckConsistency, CommittedOutputClaims), SumcheckError> where - F: Field, - T: Transcript, - C: Clone + AppendToTranscript, + F: Field + CanonicalEncoding, + H: Sponge, + C: CanonicalDecode, { - if round_proofs.len() != statement.num_vars { - return Err(SumcheckError::WrongNumberOfRounds { - expected: statement.num_vars, - got: round_proofs.len(), - }); - } - let mut rounds = Vec::with_capacity(statement.num_vars); - for round_proof in round_proofs { - if round_proof.degree() > statement.degree { - return Err(SumcheckError::DegreeBoundExceeded { - got: round_proof.degree(), - max: statement.degree, - }); - } - - round_proof.append_to_transcript(transcript); + for _ in 0..statement.num_vars { + let commitment = transcript.receive()?; rounds.push(VerifiedCommittedRound { - commitment: round_proof.commitment.clone(), - degree: round_proof.degree, - challenge: transcript.challenge(), + commitment, + degree: statement.degree, + challenge: transcript.challenge_small(), }); } - - Ok(CommittedSumcheckConsistency { rounds }) - } -} - -impl CompressedSumcheckProof -where - F: Field + AppendToTranscript, -{ - pub fn verify( - &self, - claim: &SumcheckClaim, - domain: BooleanHypercube, - round_label: &'static [u8], - transcript: &mut T, - ) -> Result, SumcheckError> - where - T: Transcript, - { - SumcheckVerifier::verify_compressed(claim, self, domain, round_label, transcript) - } -} - -impl CommittedSumcheckProof { - /// Checks committed-proof consistency through the Fiat-Shamir transcript. - /// - /// This checks the round count and degree bounds, derives the round challenges, - /// and absorbs the committed output claims after the round transcript. - pub fn verify_committed_consistency( - &self, - statement: SumcheckStatement, - transcript: &mut T, - ) -> Result, SumcheckError> - where - F: Field, - T: Transcript, - C: Clone + AppendToTranscript, - { - let consistency = SumcheckVerifier::verify_committed_round_consistency( - statement, - &self.rounds, - transcript, - )?; - self.output_claims.append_to_transcript(transcript); - Ok(consistency) + let output_claims = CommittedOutputClaims { + commitments: transcript.receive_n(num_output_commitments)?, + }; + Ok((CommittedSumcheckConsistency { rounds }, output_claims)) } } From a1a58931f725de92cc2eca65b42b61f4c989ec22 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:00:14 -0400 Subject: [PATCH 10/62] refactor(blindfold)!: write BlindFold proofs into the NARG transcript `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. --- crates/jolt-blindfold/src/error.rs | 3 + crates/jolt-blindfold/src/lib.rs | 3 +- crates/jolt-blindfold/src/proof.rs | 31 -- crates/jolt-blindfold/src/prove.rs | 254 ++++----------- crates/jolt-blindfold/src/verify.rs | 487 ++++++++-------------------- crates/jolt-blindfold/src/wire.rs | 76 +++++ 6 files changed, 280 insertions(+), 574 deletions(-) delete mode 100644 crates/jolt-blindfold/src/proof.rs create mode 100644 crates/jolt-blindfold/src/wire.rs diff --git a/crates/jolt-blindfold/src/error.rs b/crates/jolt-blindfold/src/error.rs index 04ce947aad..4d065c42e0 100644 --- a/crates/jolt-blindfold/src/error.rs +++ b/crates/jolt-blindfold/src/error.rs @@ -2,6 +2,7 @@ use jolt_crypto::VectorOpeningError; use jolt_field::JoltField; use jolt_r1cs::{ClaimLoweringError, ConstraintMatrixEvalError}; use jolt_sumcheck::{SumcheckError, SumcheckR1csError}; +use jolt_transcript::TranscriptError; use thiserror::Error as ThisError; #[derive(Clone, Debug, ThisError, PartialEq, Eq)] @@ -154,6 +155,8 @@ pub enum ProverError { #[derive(Debug, ThisError)] pub enum VerificationError { + #[error("BlindFold proof transcript: {0}")] + Transcript(#[from] TranscriptError), #[error("claims have {claim_stages} stages but proof has {proof_stages}")] StageCountMismatch { claim_stages: usize, diff --git a/crates/jolt-blindfold/src/lib.rs b/crates/jolt-blindfold/src/lib.rs index deb8a0ce57..5c2c170d74 100644 --- a/crates/jolt-blindfold/src/lib.rs +++ b/crates/jolt-blindfold/src/lib.rs @@ -19,18 +19,17 @@ mod assignment; mod builder; mod error; -mod proof; pub mod protocol; mod prove; pub mod r1cs; mod relaxed; mod statements; mod verify; +mod wire; pub use assignment::AssignedBlindFoldWitness; pub use builder::{BlindFoldProtocolBuilder, BlindFoldStageBuilder}; pub use error::{Error, LayoutError, ProverError, RelaxedError, VerificationError}; -pub use proof::BlindFoldProof; pub use protocol::{ BlindFoldDimensions, BlindFoldProtocol, FinalOpeningWitnessCoordinates, RowDimensions, WitnessCoordinate, WitnessRowLayout, diff --git a/crates/jolt-blindfold/src/proof.rs b/crates/jolt-blindfold/src/proof.rs deleted file mode 100644 index 6507e3b125..0000000000 --- a/crates/jolt-blindfold/src/proof.rs +++ /dev/null @@ -1,31 +0,0 @@ -use jolt_crypto::VectorCommitmentOpening; -use jolt_field::JoltField; -use jolt_sumcheck::CompressedSumcheckProof; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound( - serialize = "F: Serialize, Com: Serialize", - deserialize = "F: for<'a> Deserialize<'a>, Com: Deserialize<'de>" -))] -pub struct BlindFoldProof { - pub auxiliary_row_commitments: Vec, - pub random_round_commitments: Vec, - pub random_output_claim_row_commitments: Vec, - pub random_auxiliary_row_commitments: Vec, - pub random_error_row_commitments: Vec, - pub random_eval_commitments: Vec, - pub random_u: F, - pub cross_term_error_row_commitments: Vec, - pub outer_sumcheck: CompressedSumcheckProof, - pub az_rx: F, - pub bz_rx: F, - pub cz_rx: F, - pub inner_sumcheck: CompressedSumcheckProof, - pub witness_opening: VectorCommitmentOpening, - pub error_opening: VectorCommitmentOpening, - pub folded_eval_outputs: Vec, - pub folded_eval_blindings: Vec, - pub folded_eval_output_openings: Vec>, - pub folded_eval_blinding_openings: Vec>, -} diff --git a/crates/jolt-blindfold/src/prove.rs b/crates/jolt-blindfold/src/prove.rs index 97616ae9ae..d085a7cbbc 100644 --- a/crates/jolt-blindfold/src/prove.rs +++ b/crates/jolt-blindfold/src/prove.rs @@ -2,16 +2,16 @@ use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpeni use jolt_field::JoltField; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial, UnivariatePoly}; use jolt_r1cs::{ConstraintMatrices, ConstraintMatrixEvalError, SparseRow}; -use jolt_sumcheck::{CompressedSumcheckProof, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; -use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript}; +use jolt_sumcheck::send_compressed_round; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; use rand_core::RngCore; use rayon::prelude::*; -use crate::{BlindFoldProof, BlindFoldProtocol, ProverError, WitnessCoordinate}; +use crate::wire::{send_opening, FoldingCommitments}; +use crate::{BlindFoldProtocol, ProverError, WitnessCoordinate}; -const OUTER_SUMCHECK_DEGREE: usize = 3; -const INNER_SUMCHECK_DEGREE: usize = 2; -const INNER_SUMCHECK_LABEL: &[u8] = b"inner_sumcheck_poly"; +pub(crate) const OUTER_SUMCHECK_DEGREE: usize = 3; +pub(crate) const INNER_SUMCHECK_DEGREE: usize = 2; #[derive(Clone, Copy, Debug)] pub struct BlindFoldWitness<'a, F: JoltField> { @@ -150,22 +150,23 @@ where } } -pub fn prove( +/// Proves the BlindFold statement, writing the proof into `transcript`. +pub fn prove( setup: &VC::Setup, protocol: &BlindFoldProtocol, - transcript: &mut T, + transcript: &mut ProverTranscript, witness: BlindFoldWitness<'_, F>, rng: &mut R, -) -> Result, ProverError> +) -> Result<(), ProverError> where - F: JoltField + AppendToTranscript, + F: JoltField, VC: VectorCommitment, - VC::Output: HomomorphicCommitment + AppendToTranscript, - T: Transcript, + VC::Output: HomomorphicCommitment, + H: Sponge, R: RngCore, { let mut row_committer = DirectBlindFoldRowCommitter; - prove_with_row_committer::( + prove_with_row_committer::( setup, protocol, transcript, @@ -175,19 +176,19 @@ where ) } -pub fn prove_with_row_committer( +pub fn prove_with_row_committer( setup: &VC::Setup, protocol: &BlindFoldProtocol, - transcript: &mut T, + transcript: &mut ProverTranscript, witness: BlindFoldWitness<'_, F>, rng: &mut R, row_committer: &mut C, -) -> Result, ProverError> +) -> Result<(), ProverError> where - F: JoltField + AppendToTranscript, + F: JoltField, VC: VectorCommitment, - VC::Output: HomomorphicCommitment + AppendToTranscript, - T: Transcript, + VC::Output: HomomorphicCommitment, + H: Sponge, R: RngCore, C: BlindFoldRowCommitter, { @@ -208,7 +209,6 @@ where )?, "auxiliary witness rows", )?; - let committed = protocol.committed_relaxed_instance(&auxiliary_row_commitments)?; for (index, ((commitment, &output), &blinding)) in protocol .eval_commitments .iter() @@ -373,14 +373,6 @@ where &random_eval_blindings, "random eval rows", )?; - let random_instance = protocol.random_relaxed_instance( - &random_round_commitments, - &random_output_claim_row_commitments, - &random_auxiliary_row_commitments, - &random_error_row_commitments, - &random_eval_commitments, - random_u, - )?; let cross_term_error_rows = row_committer.compute_cross_term_error_rows( &protocol.r1cs, @@ -407,34 +399,18 @@ where "cross-term error rows", )?; - append_relaxed_instance( - transcript, - RelaxedInstanceLabels { - u: b"bf_committed_u", - witness: b"bf_committed_w", - error: b"bf_committed_e", - eval: b"bf_committed_eval", - }, - committed.u, - &committed.witness_row_commitments, - &committed.error_row_commitments, - &committed.eval_commitments, - ); - append_relaxed_instance( - transcript, - RelaxedInstanceLabels { - u: b"bf_random_u", - witness: b"bf_random_w", - error: b"bf_random_e", - eval: b"bf_random_eval", - }, + FoldingCommitments { + auxiliary_rows: auxiliary_row_commitments, random_u, - &random_instance.witness_row_commitments, - &random_instance.error_row_commitments, - &random_instance.eval_commitments, - ); - append_values(transcript, b"bf_cross_e", &cross_term_error_row_commitments); - let folding_challenge = transcript.challenge(); + random_rounds: random_round_commitments, + random_output_claim_rows: random_output_claim_row_commitments, + random_auxiliary_rows: random_auxiliary_row_commitments, + random_error_rows: random_error_row_commitments, + random_evals: random_eval_commitments, + cross_term_error_rows: cross_term_error_row_commitments, + } + .send(transcript); + let folding_challenge: F = transcript.challenge_small(); let folded_u = F::one() + folding_challenge * random_u; let folded_witness_rows = row_committer.fold_rows( @@ -480,8 +456,8 @@ where "folded eval blindings", )?; - let mut folded_eval_output_openings = Vec::new(); - let mut folded_eval_blinding_openings = Vec::new(); + transcript.send_all(&folded_eval_outputs); + transcript.send_all(&folded_eval_blindings); for (index, (coordinates, (&folded_output, &folded_blinding))) in final_coordinates .iter() .zip(folded_eval_outputs.iter().zip(&folded_eval_blindings)) @@ -504,15 +480,7 @@ where actual: opened, }); } - // Absorb per binding (output, then blinding), in the order the - // verifier reads them. - append_vector_opening( - transcript, - b"bf_eval_out_open", - b"bf_eval_out_blind", - &opening, - ); - folded_eval_output_openings.push(opening); + send_opening(&opening, transcript); } if let Some(coordinate) = coordinates.blinding { let (opening, opened) = open_witness_coordinate::( @@ -531,17 +499,10 @@ where actual: opened, }); } - append_vector_opening( - transcript, - b"bf_eval_blind_open", - b"bf_eval_blind_bl", - &opening, - ); - folded_eval_blinding_openings.push(opening); + send_opening(&opening, transcript); } } - transcript.append(&Label(b"bf_spartan")); let outer_num_vars = log2_power_of_two("error row count", protocol.dimensions.error.row_count)? + log2_power_of_two("error row length", protocol.dimensions.error.row_len)?; if outer_num_vars == 0 { @@ -549,7 +510,7 @@ where name: "outer folded R1CS sumcheck", }); } - let tau = transcript.challenge_vector(outer_num_vars); + let tau = transcript.challenges_small(outer_num_vars); let flattened_folded_witness = flatten(&folded_witness_rows); let flattened_folded_error = flatten(&folded_error_rows); let outer_trace = prove_outer_sumcheck( @@ -578,17 +539,12 @@ where "folded error row opening", )?; - append_values(transcript, b"bf_az_bz_cz", &[az_rx, bz_rx, cz_rx]); - append_vector_opening( - transcript, - b"bf_error_opening", - b"bf_error_blind", - &error_opening, - ); + transcript.send_all(&[az_rx, bz_rx, cz_rx]); + send_opening(&error_opening, transcript); - let ra = transcript.challenge(); - let rb = transcript.challenge(); - let rc = transcript.challenge(); + let ra: F = transcript.challenge_small(); + let rb: F = transcript.challenge_small(); + let rc: F = transcript.challenge_small(); let inner_num_vars = log2_power_of_two("witness row count", protocol.dimensions.witness.row_count)? + log2_power_of_two("witness row length", protocol.dimensions.witness.row_len)?; @@ -624,27 +580,8 @@ where "folded witness row opening", )?; - Ok(BlindFoldProof { - auxiliary_row_commitments, - random_round_commitments, - random_output_claim_row_commitments, - random_auxiliary_row_commitments, - random_error_row_commitments, - random_eval_commitments, - random_u, - cross_term_error_row_commitments, - outer_sumcheck: outer_trace.proof, - az_rx, - bz_rx, - cz_rx, - inner_sumcheck: inner_trace.proof, - witness_opening, - error_opening, - folded_eval_outputs, - folded_eval_blindings, - folded_eval_output_openings, - folded_eval_blinding_openings, - }) + send_opening(&witness_opening, transcript); + Ok(()) } fn validate_witness( @@ -805,21 +742,20 @@ where #[derive(Clone, Debug)] struct SumcheckTrace { - proof: CompressedSumcheckProof, point: Vec, } -fn prove_outer_sumcheck( +fn prove_outer_sumcheck( r1cs: &ConstraintMatrices, u: F, witness: &[F], error_values: &[F], tau: &[F], - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, ProverError> where - F: JoltField + AppendToTranscript, - T: Transcript, + F: JoltField, + H: Sponge, { let num_vars = log2_power_of_two("outer folded R1CS sumcheck", error_values.len())?; ensure_len("outer challenge vector", num_vars, tau.len())?; @@ -841,7 +777,6 @@ where let mut eq_tau = Polynomial::new(EqPolynomial::::evals(tau, None)); let mut running_sum = F::zero(); - let mut rounds = Vec::with_capacity(num_vars); let mut point = Vec::with_capacity(num_vars); for _round in 0..num_vars { @@ -888,13 +823,8 @@ where actual: round_sum, }); } - let compressed = round_poly.compress(); - append_values( - transcript, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - compressed.coeffs_except_linear_term(), - ); - let challenge = transcript.challenge(); + send_compressed_round(&round_poly, OUTER_SUMCHECK_DEGREE, transcript)?; + let challenge: F = transcript.challenge_small(); running_sum = round_poly.evaluate(challenge); az.bind_with_order(challenge, BindingOrder::HighToLow); bz.bind_with_order(challenge, BindingOrder::HighToLow); @@ -902,22 +832,16 @@ where e.bind_with_order(challenge, BindingOrder::HighToLow); eq_tau.bind_with_order(challenge, BindingOrder::HighToLow); point.push(challenge); - rounds.push(compressed); } - Ok(SumcheckTrace { - proof: CompressedSumcheckProof { - round_polynomials: rounds, - }, - point, - }) + Ok(SumcheckTrace { point }) } #[expect( clippy::too_many_arguments, reason = "inner folded R1CS sumcheck is parameterized by three random matrix weights" )] -fn prove_inner_sumcheck( +fn prove_inner_sumcheck( r1cs: &ConstraintMatrices, outer_point: &[F], witness_rows: &[Vec], @@ -925,11 +849,11 @@ fn prove_inner_sumcheck( rb: F, rc: F, claim: F, - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, ProverError> where - F: JoltField + AppendToTranscript, - T: Transcript, + F: JoltField, + H: Sponge, { let witness_values = flatten(witness_rows); let num_vars = log2_power_of_two("inner folded R1CS sumcheck", witness_values.len())?; @@ -940,7 +864,6 @@ where let mut l_w = Polynomial::new(l_w); let mut witness = Polynomial::new(witness_values); let mut running_sum = claim; - let mut rounds = Vec::with_capacity(num_vars); let mut point = Vec::with_capacity(num_vars); for _round in 0..num_vars { @@ -970,26 +893,15 @@ where actual: round_sum, }); } - let compressed = round_poly.compress(); - append_values( - transcript, - INNER_SUMCHECK_LABEL, - compressed.coeffs_except_linear_term(), - ); - let challenge = transcript.challenge(); + send_compressed_round(&round_poly, INNER_SUMCHECK_DEGREE, transcript)?; + let challenge: F = transcript.challenge_small(); running_sum = round_poly.evaluate(challenge); l_w.bind_with_order(challenge, BindingOrder::HighToLow); witness.bind_with_order(challenge, BindingOrder::HighToLow); point.push(challenge); - rounds.push(compressed); } - Ok(SumcheckTrace { - proof: CompressedSumcheckProof { - round_polynomials: rounds, - }, - point, - }) + Ok(SumcheckTrace { point }) } fn matrix_vector_product(rows: &[SparseRow], vector: &[F]) -> Vec @@ -1103,58 +1015,6 @@ where ) } -#[derive(Clone, Copy, Debug)] -struct RelaxedInstanceLabels { - u: &'static [u8], - witness: &'static [u8], - error: &'static [u8], - eval: &'static [u8], -} - -fn append_relaxed_instance( - transcript: &mut T, - labels: RelaxedInstanceLabels, - u: F, - witness_commitments: &[C], - error_commitments: &[C], - eval_commitments: &[C], -) where - F: AppendToTranscript, - C: AppendToTranscript, - T: Transcript, -{ - transcript.append(&Label(labels.u)); - u.append_to_transcript(transcript); - append_values(transcript, labels.witness, witness_commitments); - append_values(transcript, labels.error, error_commitments); - append_values(transcript, labels.eval, eval_commitments); -} - -fn append_values(transcript: &mut T, label: &'static [u8], values: &[A]) -where - A: AppendToTranscript, - T: Transcript, -{ - transcript.append(&LabelWithCount(label, values.len() as u64)); - for value in values { - value.append_to_transcript(transcript); - } -} - -fn append_vector_opening( - transcript: &mut T, - row_label: &'static [u8], - blinding_label: &'static [u8], - opening: &VectorCommitmentOpening, -) where - F: AppendToTranscript, - T: Transcript, -{ - append_values(transcript, row_label, &opening.combined_vector); - transcript.append(&Label(blinding_label)); - opening.combined_blinding.append_to_transcript(transcript); -} - fn random_rows(row_count: usize, row_len: usize, rng: &mut R) -> Vec> where F: JoltField, diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs index df92b0ee70..475305e2f9 100644 --- a/crates/jolt-blindfold/src/verify.rs +++ b/crates/jolt-blindfold/src/verify.rs @@ -1,141 +1,68 @@ use jolt_crypto::{HomomorphicCommitment, VectorCommitment, VectorCommitmentOpening}; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_poly::EqPolynomial; use jolt_r1cs::{ConstraintMatrices, MatrixColumnContributions}; -use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; -use jolt_transcript::{AppendToTranscript, Label, Transcript}; +use jolt_sumcheck::{SumcheckClaim, SumcheckVerifier}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; +use crate::prove::{INNER_SUMCHECK_DEGREE, OUTER_SUMCHECK_DEGREE}; +use crate::wire::{receive_opening, FoldingCommitments}; use crate::{ - BlindFoldProof, BlindFoldProtocol, RelaxedError, RelaxedInstance, VerificationError, - WitnessCoordinate, + BlindFoldProtocol, RelaxedError, RelaxedInstance, VerificationError, WitnessCoordinate, }; -const OUTER_SUMCHECK_DEGREE: usize = 3; -const INNER_SUMCHECK_DEGREE: usize = 2; -const INNER_SUMCHECK_LABEL: &[u8] = b"inner_sumcheck_poly"; - impl BlindFoldProtocol where - F: JoltField + AppendToTranscript, - Com: Copy + HomomorphicCommitment + AppendToTranscript, + F: JoltField, + Com: Copy + HomomorphicCommitment + CanonicalDecode, { - pub fn verify( + /// Verifies the BlindFold proof read from `transcript`. + pub fn verify( &self, - proof: &BlindFoldProof, vc_setup: &VC::Setup, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), VerificationError> where VC: VectorCommitment, - T: Transcript, + H: Sponge, { - let folded = self.folded_instance_from_proof(proof, transcript)?; - ensure_len( - "folded eval outputs", - folded.eval_commitments.len(), - proof.folded_eval_outputs.len(), - )?; - ensure_len( - "folded eval blindings", - folded.eval_commitments.len(), - proof.folded_eval_blindings.len(), - )?; - proof.verify_folded_eval_commitments::(vc_setup, &folded)?; - self.verify_folded_eval_witness_bindings::(proof, vc_setup, &folded, transcript)?; - let outer = self.verify_outer_folded_r1cs::(proof, vc_setup, &folded, transcript)?; - self.verify_inner_folded_r1cs::(proof, vc_setup, &folded, &outer, transcript)?; + let folded = self.folded_instance(transcript)?; + self.verify_folded_eval_witness_bindings::(vc_setup, &folded, transcript)?; + let outer = self.verify_outer_folded_r1cs::(vc_setup, &folded, transcript)?; + self.verify_inner_folded_r1cs::(vc_setup, &folded, &outer, transcript)?; Ok(()) } -} -impl BlindFoldProtocol -where - F: JoltField + AppendToTranscript, - Com: Clone + HomomorphicCommitment + AppendToTranscript, -{ - fn folded_instance_from_proof( + fn folded_instance( &self, - proof: &BlindFoldProof, - transcript: &mut T, - ) -> Result, VerificationError> - where - T: Transcript, - { - let committed = self.committed_relaxed_instance(&proof.auxiliary_row_commitments)?; - committed.append_to_transcript( - transcript, - b"bf_committed_u", - b"bf_committed_w", - b"bf_committed_e", - b"bf_committed_eval", - ); - + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result, VerificationError> { + let sent = + FoldingCommitments::receive(&self.dimensions, self.eval_commitments.len(), transcript)?; + let committed = self.committed_relaxed_instance(&sent.auxiliary_rows)?; let random = self.random_relaxed_instance( - &proof.random_round_commitments, - &proof.random_output_claim_row_commitments, - &proof.random_auxiliary_row_commitments, - &proof.random_error_row_commitments, - &proof.random_eval_commitments, - proof.random_u, + &sent.random_rounds, + &sent.random_output_claim_rows, + &sent.random_auxiliary_rows, + &sent.random_error_rows, + &sent.random_evals, + sent.random_u, )?; - random.append_to_transcript( - transcript, - b"bf_random_u", - b"bf_random_w", - b"bf_random_e", - b"bf_random_eval", - ); - - self.validate_cross_term_error_rows(&proof.cross_term_error_row_commitments)?; - transcript.append_values(b"bf_cross_e", &proof.cross_term_error_row_commitments); - - let folding_challenge = transcript.challenge(); - Ok(committed.fold( - &random, - &proof.cross_term_error_row_commitments, - folding_challenge, - )?) - } -} + self.validate_cross_term_error_rows(&sent.cross_term_error_rows)?; -impl RelaxedInstance -where - F: AppendToTranscript, - Com: AppendToTranscript, -{ - fn append_to_transcript( - &self, - transcript: &mut T, - u_label: &'static [u8], - witness_label: &'static [u8], - error_label: &'static [u8], - eval_label: &'static [u8], - ) where - T: Transcript, - { - transcript.append(&Label(u_label)); - self.u.append_to_transcript(transcript); - transcript.append_values(witness_label, &self.witness_row_commitments); - transcript.append_values(error_label, &self.error_row_commitments); - transcript.append_values(eval_label, &self.eval_commitments); + let folding_challenge = transcript.challenge_small(); + Ok(committed.fold(&random, &sent.cross_term_error_rows, folding_challenge)?) } -} -impl BlindFoldProtocol -where - F: JoltField + AppendToTranscript, - Com: Copy + HomomorphicCommitment + AppendToTranscript, -{ - fn verify_outer_folded_r1cs( + fn verify_outer_folded_r1cs( &self, - proof: &BlindFoldProof, vc_setup: &VC::Setup, folded: &RelaxedInstance, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result, VerificationError> where VC: VectorCommitment, - T: Transcript, + H: Sponge, { let error_row_count = self.dimensions.error.row_count; if error_row_count == 0 || !error_row_count.is_power_of_two() { @@ -167,30 +94,29 @@ where }); } - transcript.append(&Label(b"bf_spartan")); - let tau = transcript.challenge_vector(num_vars); + let tau = transcript.challenges_small(num_vars); let claim = SumcheckClaim::new(num_vars, OUTER_SUMCHECK_DEGREE, F::zero()); - let outer = proof - .outer_sumcheck - .verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - transcript, - ) + let outer = SumcheckVerifier::verify_compressed(&claim, transcript) .map_err(|source| VerificationError::OuterSumcheck { source })?; + let [az_rx, bz_rx, cz_rx]: [F; 3] = [ + transcript.receive()?, + transcript.receive()?, + transcript.receive()?, + ]; + let error_opening = receive_opening(error_row_len, transcript)?; + let (row_point, entry_point) = outer.point.split_at(row_vars); let e_rx = VC::verify_committed_rows( vc_setup, &folded.error_row_commitments, row_point, entry_point, - &proof.error_opening, + &error_opening, )?; let eq_tau_rx = EqPolynomial::::mle(&tau, &outer.point); - let expected = eq_tau_rx * (proof.az_rx * proof.bz_rx - folded.u * proof.cz_rx - e_rx); + let expected = eq_tau_rx * (az_rx * bz_rx - folded.u * cz_rx - e_rx); if outer.value != expected { return Err(VerificationError::OuterFinalClaimMismatch { expected, @@ -198,38 +124,32 @@ where }); } - transcript.append_values(b"bf_az_bz_cz", &[proof.az_rx, proof.bz_rx, proof.cz_rx]); - append_vector_opening( - transcript, - b"bf_error_opening", - b"bf_error_blind", - &proof.error_opening, - ); - Ok(OuterCheck { point: outer.point.into_vec(), + az_rx, + bz_rx, + cz_rx, }) } -} -impl BlindFoldProof -where - F: JoltField, - Com: Copy + AppendToTranscript, -{ - fn verify_folded_eval_commitments( + fn verify_folded_eval_witness_bindings( &self, vc_setup: &VC::Setup, folded: &RelaxedInstance, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), VerificationError> where VC: VectorCommitment, + H: Sponge, { + let eval_count = folded.eval_commitments.len(); + let folded_eval_outputs: Vec = transcript.receive_n(eval_count)?; + let folded_eval_blindings: Vec = transcript.receive_n(eval_count)?; for (index, ((commitment, &output), &blinding)) in folded .eval_commitments .iter() - .zip(&self.folded_eval_outputs) - .zip(&self.folded_eval_blindings) + .zip(&folded_eval_outputs) + .zip(&folded_eval_blindings) .enumerate() { if !VC::verify(vc_setup, commitment, &[output], &blinding) { @@ -237,204 +157,51 @@ where } } - Ok(()) - } -} - -impl BlindFoldProtocol -where - F: JoltField + AppendToTranscript, - Com: Copy + HomomorphicCommitment + AppendToTranscript, -{ - fn verify_folded_eval_witness_bindings( - &self, - proof: &BlindFoldProof, - vc_setup: &VC::Setup, - folded: &RelaxedInstance, - transcript: &mut T, - ) -> Result<(), VerificationError> - where - VC: VectorCommitment, - T: Transcript, - { let coordinates = self.final_opening_witness_coordinates()?; - ensure_len( - "final opening bindings", - coordinates.len(), - folded.eval_commitments.len(), - )?; - - let expected_outputs = coordinates - .iter() - .filter(|coordinates| coordinates.evaluation.is_some()) - .count(); - ensure_len( - "folded eval output witness openings", - expected_outputs, - proof.folded_eval_output_openings.len(), - )?; - let expected_blindings = coordinates + ensure_len("final opening bindings", coordinates.len(), eval_count)?; + let witness_row_len = self.dimensions.witness.row_len; + for (index, (coordinates, (&output, &blinding))) in coordinates .iter() - .filter(|coordinates| coordinates.blinding.is_some()) - .count(); - ensure_len( - "folded eval blinding witness openings", - expected_blindings, - proof.folded_eval_blinding_openings.len(), - )?; - - let binding_count = coordinates.len(); - let mut output_openings = proof.folded_eval_output_openings.iter(); - let mut blinding_openings = proof.folded_eval_blinding_openings.iter(); - for (index, coordinates) in coordinates.iter().enumerate() { - if let Some(coordinate) = coordinates.evaluation { - let opening = output_openings.next().ok_or(RelaxedError::LengthMismatch { - name: "folded eval output witness openings", - expected: expected_outputs, - actual: proof.folded_eval_output_openings.len(), - })?; - let opened = coordinate.verify_opening::(vc_setup, folded, opening)?; - let expected_output = - proof - .folded_eval_outputs - .get(index) - .ok_or(RelaxedError::LengthMismatch { - name: "folded eval outputs", - expected: binding_count, - actual: proof.folded_eval_outputs.len(), - })?; - if opened != *expected_output { - return Err(VerificationError::EvalWitnessMismatch { - kind: "output", - index, - }); - } - coordinate.require_dedicated_row(opening, "output", index)?; - append_vector_opening( - transcript, - b"bf_eval_out_open", - b"bf_eval_out_blind", - opening, - ); - } - - if let Some(coordinate) = coordinates.blinding { - let opening = blinding_openings - .next() - .ok_or(RelaxedError::LengthMismatch { - name: "folded eval blinding witness openings", - expected: expected_blindings, - actual: proof.folded_eval_blinding_openings.len(), - })?; - let opened = coordinate.verify_opening::(vc_setup, folded, opening)?; - let expected_blinding = - proof - .folded_eval_blindings - .get(index) - .ok_or(RelaxedError::LengthMismatch { - name: "folded eval blindings", - expected: binding_count, - actual: proof.folded_eval_blindings.len(), - })?; - if opened != *expected_blinding { - return Err(VerificationError::EvalWitnessMismatch { - kind: "blinding", - index, - }); + .zip(folded_eval_outputs.iter().zip(&folded_eval_blindings)) + .enumerate() + { + for (coordinate, expected, kind) in [ + (coordinates.evaluation, output, "output"), + (coordinates.blinding, blinding, "blinding"), + ] { + let Some(coordinate) = coordinate else { + continue; + }; + let opening = receive_opening(witness_row_len, transcript)?; + let opened = coordinate.verify_opening::(vc_setup, folded, &opening)?; + if opened != expected { + return Err(VerificationError::EvalWitnessMismatch { kind, index }); } - coordinate.require_dedicated_row(opening, "blinding", index)?; - append_vector_opening( - transcript, - b"bf_eval_blind_open", - b"bf_eval_blind_bl", - opening, - ); + coordinate.require_dedicated_row(&opening, kind, index)?; } } Ok(()) } -} -impl WitnessCoordinate { - fn require_dedicated_row( - self, - opening: &VectorCommitmentOpening, - kind: &'static str, - index: usize, - ) -> Result<(), VerificationError> { - for (slot, value) in opening.combined_vector.iter().enumerate() { - if slot != self.column && !value.is_zero() { - return Err(VerificationError::EvalWitnessRowNotDedicated { kind, index }); - } - } - Ok(()) - } - - fn verify_opening( - self, - vc_setup: &VC::Setup, - folded: &RelaxedInstance, - opening: &VectorCommitmentOpening, - ) -> Result> - where - F: JoltField, - VC: VectorCommitment, - VC::Output: Copy + HomomorphicCommitment, - { - let witness_row_count = folded.witness_row_commitments.len(); - if witness_row_count == 0 || !witness_row_count.is_power_of_two() { - return Err(VerificationError::InvalidPowerOfTwo { - name: "witness row count", - value: witness_row_count, - }); - } - let row_vars = witness_row_count.trailing_zeros() as usize; - - let witness_row_len = opening.combined_vector.len(); - if witness_row_len == 0 || !witness_row_len.is_power_of_two() { - return Err(VerificationError::InvalidPowerOfTwo { - name: "witness row length", - value: witness_row_len, - }); - } - let entry_vars = witness_row_len.trailing_zeros() as usize; - let row_point = boolean_point::(self.row, row_vars)?; - let entry_point = boolean_point::(self.column, entry_vars)?; - Ok(VC::verify_committed_rows( - vc_setup, - &folded.witness_row_commitments, - &row_point, - &entry_point, - opening, - )?) - } -} - -impl BlindFoldProtocol -where - F: JoltField + AppendToTranscript, - Com: Copy + HomomorphicCommitment + AppendToTranscript, -{ - fn verify_inner_folded_r1cs( + fn verify_inner_folded_r1cs( &self, - proof: &BlindFoldProof, vc_setup: &VC::Setup, folded: &RelaxedInstance, outer: &OuterCheck, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), VerificationError> where VC: VectorCommitment, - T: Transcript, + H: Sponge, { - let ra = transcript.challenge(); - let rb = transcript.challenge(); - let rc = transcript.challenge(); + let ra: F = transcript.challenge_small(); + let rb: F = transcript.challenge_small(); + let rc: F = transcript.challenge_small(); let public = public_contributions(&self.r1cs, &outer.point, folded.u)?; - let claim = ra * (proof.az_rx - public.a) - + rb * (proof.bz_rx - public.b) - + rc * (proof.cz_rx - public.c); + let claim = ra * (outer.az_rx - public.a) + + rb * (outer.bz_rx - public.b) + + rc * (outer.cz_rx - public.c); let witness_row_count = self.dimensions.witness.row_count; if witness_row_count == 0 || !witness_row_count.is_power_of_two() { @@ -466,15 +233,9 @@ where }); } let inner_claim = SumcheckClaim::new(num_vars, INNER_SUMCHECK_DEGREE, claim); - let inner = proof - .inner_sumcheck - .verify( - &inner_claim, - BooleanHypercube, - INNER_SUMCHECK_LABEL, - transcript, - ) + let inner = SumcheckVerifier::verify_compressed(&inner_claim, transcript) .map_err(|source| VerificationError::InnerSumcheck { source })?; + let witness_opening = receive_opening(witness_row_len, transcript)?; let (row_point, entry_point) = inner.point.split_at(row_vars); let w_ry = VC::verify_committed_rows( @@ -482,7 +243,7 @@ where &folded.witness_row_commitments, row_point, entry_point, - &proof.witness_opening, + &witness_opening, )?; let l_w_at_ry = compute_l_w_at_ry(&self.r1cs, &outer.point, &inner.point, ra, rb, rc)?; @@ -494,34 +255,72 @@ where }); } - append_vector_opening( - transcript, - b"bf_witness_opening", - b"bf_witness_blind", - &proof.witness_opening, - ); - Ok(()) } } +/// The outer sumcheck's reduced point and the matrix evaluations received at it. #[derive(Clone, Debug, PartialEq, Eq)] struct OuterCheck { point: Vec, + az_rx: F, + bz_rx: F, + cz_rx: F, } -fn append_vector_opening( - transcript: &mut T, - row_label: &'static [u8], - blinding_label: &'static [u8], - opening: &VectorCommitmentOpening, -) where - F: AppendToTranscript, - T: Transcript, -{ - transcript.append_values(row_label, &opening.combined_vector); - transcript.append(&Label(blinding_label)); - opening.combined_blinding.append_to_transcript(transcript); +impl WitnessCoordinate { + fn require_dedicated_row( + self, + opening: &VectorCommitmentOpening, + kind: &'static str, + index: usize, + ) -> Result<(), VerificationError> { + for (slot, value) in opening.combined_vector.iter().enumerate() { + if slot != self.column && !value.is_zero() { + return Err(VerificationError::EvalWitnessRowNotDedicated { kind, index }); + } + } + Ok(()) + } + + fn verify_opening( + self, + vc_setup: &VC::Setup, + folded: &RelaxedInstance, + opening: &VectorCommitmentOpening, + ) -> Result> + where + F: JoltField, + VC: VectorCommitment, + VC::Output: Copy + HomomorphicCommitment, + { + let witness_row_count = folded.witness_row_commitments.len(); + if witness_row_count == 0 || !witness_row_count.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "witness row count", + value: witness_row_count, + }); + } + let row_vars = witness_row_count.trailing_zeros() as usize; + + let witness_row_len = opening.combined_vector.len(); + if witness_row_len == 0 || !witness_row_len.is_power_of_two() { + return Err(VerificationError::InvalidPowerOfTwo { + name: "witness row length", + value: witness_row_len, + }); + } + let entry_vars = witness_row_len.trailing_zeros() as usize; + let row_point = boolean_point::(self.row, row_vars)?; + let entry_point = boolean_point::(self.column, entry_vars)?; + Ok(VC::verify_committed_rows( + vc_setup, + &folded.witness_row_commitments, + &row_point, + &entry_point, + opening, + )?) + } } fn public_contributions( diff --git a/crates/jolt-blindfold/src/wire.rs b/crates/jolt-blindfold/src/wire.rs new file mode 100644 index 0000000000..0dad504e4e --- /dev/null +++ b/crates/jolt-blindfold/src/wire.rs @@ -0,0 +1,76 @@ +//! BlindFold's prover messages and their order in the transcript. +//! +//! Every count below is fixed by [`BlindFoldDimensions`] and the protocol's +//! final-opening layout, so no length travels with a message. The committed +//! relaxed instance needs no message of its own beyond its auxiliary rows: its +//! round, output-claim, and evaluation commitments are already in the +//! transcript from the stages that produced them, and its remaining rows are +//! fixed padding. + +use jolt_crypto::VectorCommitmentOpening; +use jolt_field::{CanonicalDecode, JoltField}; +use jolt_transcript::{ProverTranscript, Sponge, TranscriptError, VerifierTranscript}; + +use crate::BlindFoldDimensions; + +/// The commitments the prover sends before the folding challenge. +pub(crate) struct FoldingCommitments { + pub(crate) auxiliary_rows: Vec, + pub(crate) random_u: F, + pub(crate) random_rounds: Vec, + pub(crate) random_output_claim_rows: Vec, + pub(crate) random_auxiliary_rows: Vec, + pub(crate) random_error_rows: Vec, + pub(crate) random_evals: Vec, + pub(crate) cross_term_error_rows: Vec, +} + +impl FoldingCommitments { + pub(crate) fn send(&self, transcript: &mut ProverTranscript) { + transcript.send_all(&self.auxiliary_rows); + transcript.send(&self.random_u); + transcript.send_all(&self.random_rounds); + transcript.send_all(&self.random_output_claim_rows); + transcript.send_all(&self.random_auxiliary_rows); + transcript.send_all(&self.random_error_rows); + transcript.send_all(&self.random_evals); + transcript.send_all(&self.cross_term_error_rows); + } + + pub(crate) fn receive( + dimensions: &BlindFoldDimensions, + eval_count: usize, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(Self { + auxiliary_rows: transcript.receive_n(dimensions.auxiliary_rows)?, + random_u: transcript.receive()?, + random_rounds: transcript.receive_n(dimensions.coefficient_rows)?, + random_output_claim_rows: transcript.receive_n(dimensions.output_claim_rows)?, + random_auxiliary_rows: transcript.receive_n(dimensions.auxiliary_rows)?, + random_error_rows: transcript.receive_n(dimensions.error.row_count)?, + random_evals: transcript.receive_n(eval_count)?, + cross_term_error_rows: transcript.receive_n(dimensions.error.row_count)?, + }) + } +} + +/// Sends a committed-row opening: the combined row, then its blinding. +pub(crate) fn send_opening( + opening: &VectorCommitmentOpening, + transcript: &mut ProverTranscript, +) { + transcript.send_all(&opening.combined_vector); + transcript.send(&opening.combined_blinding); +} + +/// Receives a committed-row opening of a `row_len`-entry row. +pub(crate) fn receive_opening( + row_len: usize, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, TranscriptError> { + Ok(VectorCommitmentOpening { + combined_vector: transcript.receive_n(row_len)?, + combined_blinding: transcript.receive()?, + }) +} From 9b9fd9ee7a053c7c074d9739a3606396b4be2634 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:08:33 -0400 Subject: [PATCH 11/62] test(sumcheck): port the sumcheck suite to the NARG transcript 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. --- .../examples/clear_custom_transcript.rs | 87 +- .../src/round_scheduler_tests.rs | 28 +- crates/jolt-sumcheck/src/tests.rs | 1607 ++++------------- crates/jolt-sumcheck/tests/committed.rs | 299 +-- .../jolt-sumcheck/tests/extension_fields.rs | 448 ----- .../tests/external-consumer/Cargo.lock | 439 ++++- .../tests/external-consumer/Cargo.toml | 4 +- .../external-consumer/tests/external_field.rs | 111 +- .../jolt-sumcheck/tests/mersenne61_compat.rs | 94 +- crates/jolt-sumcheck/tests/roundtrip.rs | 455 ++--- crates/jolt-sumcheck/tests/solinas_fields.rs | 224 +++ crates/jolt-sumcheck/tests/soundness.rs | 708 +++----- 12 files changed, 1628 insertions(+), 2876 deletions(-) delete mode 100644 crates/jolt-sumcheck/tests/extension_fields.rs create mode 100644 crates/jolt-sumcheck/tests/solinas_fields.rs diff --git a/crates/jolt-sumcheck/examples/clear_custom_transcript.rs b/crates/jolt-sumcheck/examples/clear_custom_transcript.rs index 0ae3221369..801c7f225b 100644 --- a/crates/jolt-sumcheck/examples/clear_custom_transcript.rs +++ b/crates/jolt-sumcheck/examples/clear_custom_transcript.rs @@ -1,50 +1,15 @@ use jolt_field::{Prime64Offset59, Ring}; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{ - prove_batch, BatchMember, BatchPrelude, BooleanHypercube, ClearProof, ClearSumcheckRecorder, - ProveRounds, SequentialRounds, SumcheckClaim, SumcheckError, SumcheckProof, SumcheckRecorder, - SumcheckVerifier, OPENING_CLAIM_TRANSCRIPT_LABEL, SUMCHECK_CLAIM_TRANSCRIPT_LABEL, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, + prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, ProveRounds, SequentialRounds, + SumcheckClaim, SumcheckError, SumcheckRecorder, SumcheckVerifier, }; -use jolt_transcript::Transcript; +use jolt_transcript::{Channel, Keccak, ProtocolId, ProverTranscript, VerifierTranscript}; type F = Prime64Offset59; -#[derive(Default)] -struct FixedTranscript { - state: [u8; 32], - cursor: usize, -} - -impl Transcript for FixedTranscript { - type Challenge = F; - - fn new(label: &'static [u8]) -> Self { - let mut transcript = Self::default(); - transcript.append_bytes(label); - transcript - } - - fn append_bytes(&mut self, bytes: &[u8]) { - for byte in bytes { - let index = self.cursor % self.state.len(); - self.state[index] = self.state[index] - .wrapping_mul(31) - .wrapping_add(*byte) - .wrapping_add(1); - self.cursor += 1; - } - } - - fn challenge(&mut self) -> F { - self.append_bytes(b"challenge"); - F::from_u64(7) - } - - fn state(&self) -> [u8; 32] { - self.state - } -} +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/example"); +const SESSION: &[u8] = b"external-sumcheck-example"; struct LinearRound; @@ -65,8 +30,7 @@ impl ProveRounds for LinearRound { Ok(UnivariatePoly::new(vec![F::from_u64(3), F::from_u64(2)])) } - fn finish_rounds(&mut self, bind: F) -> Result<(), SumcheckError> { - assert_eq!(bind, F::from_u64(7)); + fn finish_rounds(&mut self, _bind: F) -> Result<(), SumcheckError> { Ok(()) } } @@ -74,10 +38,11 @@ impl ProveRounds for LinearRound { fn main() -> Result<(), SumcheckError> { // g(X) = 3 + 2X, so g(0) + g(1) = 8. let claim = SumcheckClaim::new(1, 1, F::from_u64(8)); - let mut prover_transcript = FixedTranscript::new(b"external-sumcheck-example"); + + let mut prover_transcript = ProverTranscript::::new(&PROTOCOL, SESSION); let mut recorder = ClearSumcheckRecorder::::new(); recorder.absorb_input_claims(&[claim.claimed_sum], &mut prover_transcript); - let coefficient = prover_transcript.challenge_scalar(); + let coefficient: F = prover_transcript.challenge_small(); let prelude = BatchPrelude::try_new( vec![BatchMember { input_claim: claim.claimed_sum, @@ -97,34 +62,26 @@ fn main() -> Result<(), SumcheckError> { &mut recorder, &mut prover_transcript, )?; - let recorded = recorder.finish(&proved.member_claims, &mut prover_transcript)?; - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = recorded.proof else { - unreachable!("the clear recorder always returns a compressed clear proof") - }; + recorder.finish(&proved.member_claims, &mut prover_transcript)?; + let proof = prover_transcript.finish(); - let mut verifier_transcript = FixedTranscript::new(b"external-sumcheck-example"); - verifier_transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, &claim.claimed_sum); - let verifier_coefficient = verifier_transcript.challenge_scalar(); + let mut verifier_transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, &proof); + verifier_transcript.public(&claim.claimed_sum); + let verifier_coefficient: F = verifier_transcript.challenge_small(); let combined_claim = SumcheckClaim::new( claim.num_vars, claim.degree, verifier_coefficient * claim.claimed_sum, ); - let reduced = SumcheckVerifier::verify_compressed( - &combined_claim, - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut verifier_transcript, - )?; - for opening_claim in &proved.member_claims { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, opening_claim); - } + let reduced = SumcheckVerifier::verify_compressed(&combined_claim, &mut verifier_transcript)?; + let opening_claims: Vec = verifier_transcript.receive_n(1)?; + verifier_transcript.finish()?; - assert_eq!(reduced.point.as_slice(), &[F::from_u64(7)]); - assert_eq!(proved.member_claims, vec![F::from_u64(17)]); - assert_eq!(reduced.value, coefficient * F::from_u64(17)); + let challenge = reduced.point.as_slice()[0]; + let opening = F::from_u64(3) + F::from_u64(2) * challenge; + assert_eq!(opening_claims, vec![opening]); + assert_eq!(proved.member_claims, vec![opening]); + assert_eq!(reduced.value, verifier_coefficient * opening); assert_eq!(proved.final_claim, reduced.value); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); Ok(()) } diff --git a/crates/jolt-sumcheck/src/round_scheduler_tests.rs b/crates/jolt-sumcheck/src/round_scheduler_tests.rs index 3356a83666..68e274dd33 100644 --- a/crates/jolt-sumcheck/src/round_scheduler_tests.rs +++ b/crates/jolt-sumcheck/src/round_scheduler_tests.rs @@ -6,9 +6,8 @@ reason = "tests use unwrap on fallible prove paths under assertion" )] -use jolt_crypto::Bn254G1; use jolt_field::{Fr, Ring}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, Channel, ProverTranscript}; use crate::batch::{BatchMember, BatchPrelude}; use crate::error::SumcheckError; @@ -16,7 +15,7 @@ use crate::prover::{ prove_batch, MemberFinish, MemberRound, ProveRounds, RoundScheduler, SequentialRounds, }; use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; -use crate::tests::DenseMember; +use crate::tests::{fingerprint, prover, DenseMember}; type F = Fr; @@ -53,19 +52,19 @@ fn traversal_fixture() -> ( DenseMember, DenseMember, BatchPrelude, - Blake2bTranscript, - ClearSumcheckRecorder, + ProverTranscript, + ClearSumcheckRecorder, ) { let sum_long = F::from_u64(4242); let sum_short = F::from_u64(99); let long = DenseMember::with_sum(3, sum_long, 7); let short = DenseMember::with_sum(1, sum_short, 13); - let mut transcript = Blake2bTranscript::new(b"traversal-seam"); - let mut recorder = ClearSumcheckRecorder::::new(); + let mut transcript = prover(b"traversal-seam"); + let mut recorder = ClearSumcheckRecorder::::new(); recorder.absorb_input_claims(&[sum_long, sum_short], &mut transcript); - let coeff_long: F = transcript.challenge_scalar(); - let coeff_short: F = transcript.challenge_scalar(); + let coeff_long: F = transcript.challenge_small(); + let coeff_short: F = transcript.challenge_small(); let prelude = BatchPrelude::new( vec![ BatchMember { @@ -104,18 +103,19 @@ fn proof_is_invariant_under_a_reordering_traversal() { &mut transcript, ) .unwrap(); - let recorded = recorder + recorder .finish(&proved.member_claims, &mut transcript) .unwrap(); - (proved, recorded.proof, transcript.state()) + let state = fingerprint(&transcript); + (proved, transcript.finish(), state) }; - let (sequential, sequential_proof, sequential_state) = prove(false); - let (chaotic, chaotic_proof, chaotic_state) = prove(true); + let (sequential, sequential_narg, sequential_state) = prove(false); + let (chaotic, chaotic_narg, chaotic_state) = prove(true); assert_eq!(sequential.challenges, chaotic.challenges); assert_eq!(sequential.final_claim, chaotic.final_claim); assert_eq!(sequential.member_claims, chaotic.member_claims); - assert_eq!(sequential_proof, chaotic_proof); + assert_eq!(sequential_narg, chaotic_narg); assert_eq!(sequential_state, chaotic_state); } diff --git a/crates/jolt-sumcheck/src/tests.rs b/crates/jolt-sumcheck/src/tests.rs index 5167c0eec6..4a8091beaa 100644 --- a/crates/jolt-sumcheck/src/tests.rs +++ b/crates/jolt-sumcheck/src/tests.rs @@ -1,289 +1,178 @@ -//! Unit tests for sumcheck verification. +//! Unit tests for round messages, recorders, the batched prover, and the +//! uni-skip provers against the verifier entry points. #![expect( clippy::unwrap_used, - clippy::panic, - clippy::panic_in_result_fn, - clippy::wildcard_enum_match_arm, clippy::indexing_slicing, - reason = "tests may panic on assertion failures, index directly, and match errors with catch-all arms" + clippy::panic_in_result_fn, + reason = "tests may panic on assertion failures and index fixture data" )] use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; -use jolt_field::{Fr, Ring}; -use jolt_poly::{CompressedPoly, UnivariatePoly}; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, LabelWithCount, Transcript}; +use jolt_field::{CanonicalBytes, Field, Fr, Ring}; +use jolt_poly::UnivariatePoly; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; -use crate::claim::{EvaluationClaim, SumcheckClaim, SumcheckStatement}; +use crate::batch::{BatchMember, BatchPrelude}; +use crate::claim::{SumcheckClaim, SumcheckStatement}; use crate::committed::{ - CommittedOutputClaims, CommittedRound, CommittedRoundWitness, CommittedSumcheckConsistency, - CommittedSumcheckProof, VerifiedCommittedRound, + BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency, VerifiedCommittedRound, }; +use crate::domain::{CenteredIntegerDomain, SumcheckDomain}; use crate::error::SumcheckError; -use crate::proof::{ClearProof, ClearSumcheckProof, CompressedSumcheckProof, SumcheckProof}; -use crate::round_proof::{ClearRound, CompressedLabeledRoundPoly, LabeledRoundPoly, RoundMessage}; -use crate::verifier::SumcheckVerifier; -use crate::{ - BatchedCommittedSumcheckConsistency, BooleanHypercube, CenteredIntegerDomain, SumcheckDomain, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, +use crate::prover::{ + prove_batch, prove_uniskip_clear, prove_uniskip_committed, ProveRounds, ProvedBatch, + SequentialRounds, +}; +use crate::recorder::{ClearSumcheckRecorder, CommittedSumcheckRecorder, SumcheckRecorder}; +use crate::round_proof::{ + receive_compressed_round, receive_full_round, send_compressed_round, send_full_round, }; +use crate::verifier::SumcheckVerifier; type F = Fr; +type H = Blake2b512; +type VC = Pedersen; -/// Build an honest sumcheck proof for a multilinear polynomial given -/// as evaluations over {0,1}^n. -/// -/// This is a minimal reference prover: in each round it computes the -/// round polynomial by partial evaluation, absorbs it into the -/// transcript, squeezes a challenge, and binds. -fn honest_prove( - evals: &[F], - num_vars: usize, - transcript: &mut Blake2bTranscript, -) -> ClearSumcheckProof { - let mut buf = evals.to_vec(); - let mut round_polys = Vec::with_capacity(num_vars); - - for _round in 0..num_vars { - let half = buf.len() / 2; - - // HighToLow binding: pairs (buf[i], buf[i + half]) - // s(X) = sum_i [ buf[i] * (1-X) + buf[i + half] * X ] - let mut eval_0 = F::from_u64(0); - let mut eval_1 = F::from_u64(0); - for i in 0..half { - eval_0 += buf[i]; - eval_1 += buf[i + half]; - } - - // Degree-1 round polynomial: s(X) = eval_0 + (eval_1 - eval_0) * X - let c0 = eval_0; - let c1 = eval_1 - eval_0; - let round_poly = UnivariatePoly::new(vec![c0, c1]); - - // Absorb through the same path the unlabelled verifier uses. - as RoundMessage>::append_to_transcript(&round_poly, transcript); - - let r: F = transcript.challenge(); - round_polys.push(round_poly); - - // Bind HighToLow: buf[i] = buf[i] + r * (buf[i + half] - buf[i]) - for i in 0..half { - buf[i] = buf[i] + r * (buf[i + half] - buf[i]); - } - buf.truncate(half); - } +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/unit-tests"); - ClearSumcheckProof { - round_polynomials: round_polys, - } +pub(crate) fn prover(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&PROTOCOL, session) } -fn honest_prove_compressed_labeled( - evals: &[F], - num_vars: usize, - label: &'static [u8], - transcript: &mut Blake2bTranscript, -) -> (ClearSumcheckProof, CompressedSumcheckProof) { - let mut buf = evals.to_vec(); - let mut round_polys = Vec::with_capacity(num_vars); - let mut compressed_round_polys = Vec::with_capacity(num_vars); - - for _round in 0..num_vars { - let half = buf.len() / 2; - - let mut eval_0 = F::from_u64(0); - let mut eval_1 = F::from_u64(0); - for i in 0..half { - eval_0 += buf[i]; - eval_1 += buf[i + half]; - } - - let round_poly = UnivariatePoly::new(vec![eval_0, eval_1 - eval_0]); - let compressed = CompressedLabeledRoundPoly::new(&round_poly, label); - as RoundMessage>::append_to_transcript( - &compressed, - transcript, - ); - - let r: F = transcript.challenge(); - compressed_round_polys.push(round_poly.compress()); - round_polys.push(round_poly); +fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, H> { + VerifierTranscript::new(&PROTOCOL, session, narg) +} - for i in 0..half { - buf[i] = buf[i] + r * (buf[i + half] - buf[i]); - } - buf.truncate(half); - } +/// The sponge state both roles must agree on after the same message sequence. +pub(crate) fn fingerprint(channel: &C) -> [u8; 32] { + channel.preview().squeeze() +} - ( - ClearSumcheckProof { - round_polynomials: round_polys, - }, - CompressedSumcheckProof { - round_polynomials: compressed_round_polys, - }, - ) +fn poly(coefficients: &[u64]) -> UnivariatePoly { + UnivariatePoly::new(coefficients.iter().copied().map(F::from_u64).collect()) } -/// Compute the sum over {0,1}^n of multilinear evaluations. -fn compute_sum(evals: &[F]) -> F { - evals.iter().copied().sum() +fn encode(values: &[u64]) -> Vec { + values + .iter() + .flat_map(|&value| F::from_u64(value).to_bytes_le_vec()) + .collect() } #[test] -fn verify_valid_degree1_proof() { - // f(x1, x2, x3) with known evaluations - // f(0,0,0)=1, f(1,0,0)=2, f(0,1,0)=3, f(1,1,0)=4, - // f(0,0,1)=5, f(1,0,1)=6, f(0,1,1)=7, f(1,1,1)=8 - let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - let num_vars = 3; +fn round_messages_travel_at_the_degree_bound_width() { + let quadratic = poly(&[2, 3, 5]); - let mut prover_transcript = Blake2bTranscript::new(b"sumcheck-test"); - let proof = honest_prove(&evals, num_vars, &mut prover_transcript); + let mut full = prover(b"wire"); + send_full_round(&quadratic, 3, &mut full).unwrap(); + let full = full.finish(); + assert_eq!(full, encode(&[2, 3, 5, 0])); - let claim = SumcheckClaim { - num_vars, - degree: 1, - claimed_sum: sum, - }; + let mut compressed = prover(b"wire"); + send_compressed_round(&quadratic, 3, &mut compressed).unwrap(); + let compressed = compressed.finish(); + assert_eq!(compressed, encode(&[2, 5, 0])); - let mut verifier_transcript = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify( - &claim, - &proof.round_polynomials, - BooleanHypercube, - &mut verifier_transcript, + let mut full_reader = verifier(b"wire", &full); + let received = receive_full_round::(3, &mut full_reader).unwrap(); + assert_eq!(received, poly(&[2, 3, 5, 0])); + full_reader.finish().unwrap(); + + let mut compressed_reader = verifier(b"wire", &compressed); + let received = receive_compressed_round::(3, &mut compressed_reader).unwrap(); + assert_eq!( + received.coeffs_except_linear_term(), + poly(&[2, 5, 0]).coefficients() ); - assert!(result.is_ok(), "verification failed: {:?}", result.err()); - - let EvaluationClaim { - point: challenges, - value: final_eval, - } = result.unwrap(); - assert_eq!(challenges.len(), num_vars); - - // Verify the final evaluation matches direct evaluation at the challenge point. - // evaluate_and_consume binds variables in order, matching the sumcheck bind order. - let poly = jolt_poly::Polynomial::new(evals); - let expected = poly.evaluate_and_consume(&challenges); - assert_eq!(final_eval, expected); + compressed_reader.finish().unwrap(); } #[test] -fn verify_single_variable() { - // f(x) = 3 + 7x, evals = [3, 10] - let evals = vec![F::from_u64(3), F::from_u64(10)]; - let sum = compute_sum(&evals); // 13 - - let mut pt = Blake2bTranscript::new(b"sumcheck-test"); - let proof = honest_prove(&evals, 1, &mut pt); - - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let EvaluationClaim { - point: challenges, - value: final_eval, - } = SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt) - .unwrap(); - assert_eq!(challenges.len(), 1); +fn senders_reject_rounds_above_the_degree_bound_before_writing() { + let cubic = poly(&[1, 2, 3, 4]); + let mut transcript = prover(b"over-degree"); + let before = fingerprint(&transcript); - let poly = jolt_poly::Polynomial::new(evals); - assert_eq!(final_eval, poly.evaluate_and_consume(&challenges)); + assert!(matches!( + send_full_round(&cubic, 2, &mut transcript), + Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) + )); + assert!(matches!( + send_compressed_round(&cubic, 2, &mut transcript), + Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) + )); + assert_eq!(fingerprint(&transcript), before); + assert!(transcript.finish().is_empty()); +} + +fn verify_single_round( + round: &UnivariatePoly, + degree: usize, + claimed_sum: F, + domain: CenteredIntegerDomain, +) -> Result, SumcheckError> { + let mut transcript = prover(b"integer-domain"); + send_full_round(round, degree, &mut transcript).unwrap(); + let narg = transcript.finish(); + let mut transcript = verifier(b"integer-domain", &narg); + let reduced = SumcheckVerifier::verify( + &SumcheckClaim::new(1, degree, claimed_sum), + domain, + &mut transcript, + )?; + transcript.finish()?; + Ok(reduced) } #[test] fn centered_integer_domain_verifies_round_sum() { - let round_poly = UnivariatePoly::new(vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)]); - let claim = SumcheckClaim { - num_vars: 1, - degree: 2, - claimed_sum: F::from_u64(16), - }; - - let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); - let result = SumcheckVerifier::verify( - &claim, - std::slice::from_ref(&round_poly), - CenteredIntegerDomain::new(3), - &mut transcript, - ) - .unwrap(); - - assert_eq!(result.point.len(), 1); - assert_eq!(result.value, round_poly.evaluate(result.point[0])); + // Domain {-1, 0, 1}: s(-1) + s(0) + s(1) = 4 + 2 + 10. + let round = poly(&[2, 3, 5]); + let reduced = + verify_single_round(&round, 2, F::from_u64(16), CenteredIntegerDomain::new(3)).unwrap(); + assert_eq!(reduced.point.len(), 1); + assert_eq!(reduced.value, round.evaluate(reduced.point[0])); } #[test] fn centered_integer_domain_uses_core_even_window_convention() { - let round_poly = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(1)]); - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: F::from_i64(2), - }; - - let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); - let result = SumcheckVerifier::verify( - &claim, - &[round_poly], + // Domain {-1, 0, 1, 2}: the identity sums to 2. + let reduced = verify_single_round( + &poly(&[0, 1]), + 1, + F::from_u64(2), CenteredIntegerDomain::new(4), - &mut transcript, ); - - assert!(result.is_ok(), "verification failed: {:?}", result.err()); + assert!(reduced.is_ok(), "verification failed: {:?}", reduced.err()); } #[test] fn centered_integer_domain_rejects_wrong_sum() { - let round_poly = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(1)]); - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: F::from_u64(3), - }; - - let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); - let result = SumcheckVerifier::verify( - &claim, - &[round_poly], + let result = verify_single_round( + &poly(&[0, 1]), + 1, + F::from_u64(3), CenteredIntegerDomain::new(4), - &mut transcript, ); - assert!(matches!( result, Err(SumcheckError::RoundCheckFailed { round: 0, expected, actual, - }) if expected == F::from_u64(3) && actual == F::from_i64(2) + }) if expected == F::from_u64(3) && actual == F::from_u64(2) )); } #[test] fn centered_integer_domain_rejects_empty_domain() { - let round_poly = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(1)]); - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: F::from_u64(0), - }; - - let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); - let result = SumcheckVerifier::verify( - &claim, - &[round_poly], + let result = verify_single_round( + &poly(&[0, 1]), + 1, + F::from_u64(0), CenteredIntegerDomain::new(0), - &mut transcript, ); - assert!(matches!( result, Err(SumcheckError::InvalidIntegerDomain { domain_size: 0 }) @@ -307,411 +196,6 @@ fn centered_integer_domain_exposes_power_sums() { ); } -#[test] -fn verify_round_check_failure() { - let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = Blake2bTranscript::new(b"sumcheck-test"); - let mut proof = honest_prove(&evals, 3, &mut pt); - - // Corrupt the first round polynomial - let bad_coeffs = vec![F::from_u64(999), F::from_u64(1)]; - proof.round_polynomials[0] = UnivariatePoly::new(bad_coeffs); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!(result.is_err()); - match result.unwrap_err() { - SumcheckError::RoundCheckFailed { round, .. } => assert_eq!(round, 0), - other => panic!("expected RoundCheckFailed, got {other:?}"), - } -} - -#[test] -fn verify_wrong_num_rounds() { - let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = Blake2bTranscript::new(b"sumcheck-test"); - let mut proof = honest_prove(&evals, 3, &mut pt); - - // Remove the last round - let _ = proof.round_polynomials.pop(); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - match result.unwrap_err() { - SumcheckError::WrongNumberOfRounds { expected, got } => { - assert_eq!(expected, 3); - assert_eq!(got, 2); - } - other => panic!("expected WrongNumberOfRounds, got {other:?}"), - } -} - -#[test] -fn verify_degree_exceeded() { - let evals: Vec = (1..=4).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = Blake2bTranscript::new(b"sumcheck-test"); - let mut proof = honest_prove(&evals, 2, &mut pt); - - // Replace first round poly with a degree-3 polynomial (4 coefficients) - proof.round_polynomials[0] = - UnivariatePoly::new(vec![sum, F::from_u64(1), F::from_u64(0), F::from_u64(1)]); - - let claim = SumcheckClaim { - num_vars: 2, - degree: 1, // degree bound is 1, but we gave degree 3 - claimed_sum: sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - match result.unwrap_err() { - SumcheckError::DegreeBoundExceeded { got, max } => { - assert_eq!(max, 1); - assert!(got > 1); - } - other => panic!("expected DegreeBoundExceeded, got {other:?}"), - } -} - -#[test] -fn verify_wrong_claimed_sum() { - let evals: Vec = (1..=4).map(F::from_u64).collect(); - let real_sum = compute_sum(&evals); - - let mut pt = Blake2bTranscript::new(b"sumcheck-test"); - let proof = honest_prove(&evals, 2, &mut pt); - - // Claim a different sum - let claim = SumcheckClaim { - num_vars: 2, - degree: 1, - claimed_sum: real_sum + F::from_u64(1), - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - SumcheckError::RoundCheckFailed { round: 0, .. } - )); -} - -#[test] -fn clear_round_verifier_with_label_absorbs_label() { - // poly(0) = 5, poly(1) = 5 + 3 = 8, sum = 13 - let poly = UnivariatePoly::new(vec![F::from_u64(5), F::from_u64(3)]); - let label: &[u8; 10] = b"test_label"; - let labeled = LabeledRoundPoly::new(&poly, label); - - let mut t1 = Blake2bTranscript::::new(b"sumcheck-test"); - as RoundMessage>::append_to_transcript(&labeled, &mut t1); - let c1: F = t1.challenge(); - - // Absorb manually (should match) - let mut t2 = Blake2bTranscript::new(b"sumcheck-test"); - t2.append(&LabelWithCount(label, 2)); - for coeff in poly.coefficients() { - coeff.append_to_transcript(&mut t2); - } - let c2: F = t2.challenge(); - - assert_eq!(c1, c2, "labeled absorption must match manual absorption"); -} - -#[test] -fn clear_round_verifier_no_label() { - let poly = UnivariatePoly::new(vec![F::from_u64(5), F::from_u64(3)]); - - let mut t1 = Blake2bTranscript::::new(b"sumcheck-test"); - as RoundMessage>::append_to_transcript(&poly, &mut t1); - let c1: F = t1.challenge(); - - // Manual: just coefficients, no label - let mut t2 = Blake2bTranscript::new(b"sumcheck-test"); - for coeff in poly.coefficients() { - coeff.append_to_transcript(&mut t2); - } - let c2: F = t2.challenge(); - - assert_eq!(c1, c2, "unlabeled absorption must match manual absorption"); -} - -#[test] -fn clear_round_verifier_compressed_matches_manual_absorption() { - // s(X) = 2 + 3*X + 5*X^2 ⇒ s(0) = 2, s(1) = 10, running_sum = 12. - // Sanity: 2*c0 + c1 + c2 = 2*2 + 3 + 5 = 12. - let poly = UnivariatePoly::new(vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)]); - let label: &[u8; 15] = b"compressed_test"; - let compressed = CompressedLabeledRoundPoly::new(&poly, label); - - let mut t1 = Blake2bTranscript::::new(b"sumcheck-test"); - as RoundMessage>::append_to_transcript(&compressed, &mut t1); - let ch1: F = t1.challenge(); - - // Manual absorb matching the compressed wire format: label_with_count(d), c0, c2..cd. - let mut t2 = Blake2bTranscript::new(b"sumcheck-test"); - let coeffs = poly.coefficients(); - t2.append(&LabelWithCount(label, (coeffs.len() - 1) as u64)); - coeffs[0].append_to_transcript(&mut t2); - for c in coeffs.iter().skip(2) { - c.append_to_transcript(&mut t2); - } - let ch2: F = t2.challenge(); - - assert_eq!( - ch1, ch2, - "compressed absorption must omit c1 and match manual c0+c2..cd absorption" - ); -} - -#[test] -fn clear_round_verifier_compressed_rejects_wrong_running_sum() { - // Even when the linear term is omitted from absorption, the sum check - // (s(0) + s(1) == running_sum) still binds every coefficient. - let poly = UnivariatePoly::new(vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)]); - let wrong_running_sum = F::from_u64(999); - let compressed = CompressedLabeledRoundPoly::new(&poly, b"compressed_test"); - - let result = BooleanHypercube.check_round_sum(0, wrong_running_sum, &compressed); - assert!( - matches!( - result, - Err(SumcheckError::RoundCheckFailed { round: 0, .. }) - ), - "compressed verifier must enforce s(0)+s(1)==running_sum: {result:?}" - ); -} - -#[test] -fn clear_round_verifier_compressed_rejects_short_polynomial() { - // Compressed encoding omits the linear term; a polynomial with fewer than - // two coefficients has no linear term to recover and is malformed. - let degree_zero = UnivariatePoly::new(vec![F::from_u64(7)]); - let compressed = CompressedLabeledRoundPoly::new(°ree_zero, b"compressed_test"); - - let result = BooleanHypercube.check_round_sum(3, F::from_u64(14), &compressed); - assert!( - matches!( - result, - Err(SumcheckError::CompressedPolynomialTooShort { round: 3, got: 1 }) - ), - "compressed verifier must reject degree-0 polynomials: {result:?}" - ); -} - -#[test] -fn owned_compressed_verify_matches_borrowed_compressed_rounds() { - let evals: Vec = (1..=8).map(F::from_u64).collect(); - let num_vars = 3; - let sum = compute_sum(&evals); - let label = SUMCHECK_ROUND_TRANSCRIPT_LABEL; - - let mut prover_transcript = Blake2bTranscript::new(b"sumcheck-test"); - let (clear_proof, compressed_proof) = - honest_prove_compressed_labeled(&evals, num_vars, label, &mut prover_transcript); - let claim = SumcheckClaim { - num_vars, - degree: 1, - claimed_sum: sum, - }; - - let wrapped = clear_proof - .round_polynomials - .iter() - .map(|poly| CompressedLabeledRoundPoly::new(poly, label)) - .collect::>(); - let mut borrowed_transcript = Blake2bTranscript::new(b"sumcheck-test"); - let borrowed = - SumcheckVerifier::verify(&claim, &wrapped, BooleanHypercube, &mut borrowed_transcript) - .unwrap(); - - let mut owned_transcript = Blake2bTranscript::new(b"sumcheck-test"); - let owned = compressed_proof - .verify(&claim, BooleanHypercube, label, &mut owned_transcript) - .unwrap(); - - assert_eq!(owned, borrowed); - assert_eq!(owned_transcript.state(), borrowed_transcript.state()); - - let poly = jolt_poly::Polynomial::new(evals); - assert_eq!(owned.value, poly.evaluate_and_consume(&owned.point)); -} - -#[test] -fn owned_compressed_verify_rejects_wrong_round_count() { - let proof = CompressedSumcheckProof { - round_polynomials: Vec::new(), - }; - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: F::from_u64(0), - }; - - let mut transcript = Blake2bTranscript::::new(b"sumcheck-test"); - let result = proof.verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::WrongNumberOfRounds { - expected: 1, - got: 0, - }) - )); -} - -#[test] -fn owned_compressed_verify_rejects_degree_bound_exceeded() { - let proof = CompressedSumcheckProof { - round_polynomials: vec![CompressedPoly::new(vec![F::from_u64(1), F::from_u64(2)])], - }; - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: F::from_u64(3), - }; - - let mut transcript = Blake2bTranscript::::new(b"sumcheck-test"); - let result = proof.verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }) - )); -} - -#[test] -fn owned_compressed_verify_rejects_empty_round_polynomial() { - let proof = CompressedSumcheckProof { - round_polynomials: vec![CompressedPoly::new(Vec::new())], - }; - let claim = SumcheckClaim { - num_vars: 1, - degree: 1, - claimed_sum: F::from_u64(0), - }; - - let mut transcript = Blake2bTranscript::::new(b"sumcheck-test"); - let result = proof.verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::CompressedPolynomialTooShort { round: 0, got: 0 }) - )); -} - -#[test] -fn sumcheck_proof_verify_dispatches_full_clear() { - let proof = SumcheckProof::::Clear(ClearProof::Full(ClearSumcheckProof { - round_polynomials: vec![UnivariatePoly::new(vec![F::from_u64(2), F::from_u64(3)])], - })); - let claim = SumcheckClaim::new(1, 1, F::from_u64(7)); - - let mut transcript = Blake2bTranscript::::new(b"sumcheck-proof-dispatch"); - let reduction = proof - .verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ) - .unwrap(); - - assert_eq!(reduction.point.len(), 1); -} - -#[test] -fn sumcheck_proof_verify_rejects_wrong_clear_encoding() { - let proof = SumcheckProof::::Clear(ClearProof::Compressed(CompressedSumcheckProof { - round_polynomials: Vec::new(), - })); - let claim = SumcheckClaim::new(1, 1, F::from_u64(0)); - - let mut transcript = Blake2bTranscript::::new(b"sumcheck-proof-dispatch"); - let result = proof.verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::WrongProofEncoding { - expected: "full clear", - got: "compressed clear", - }) - )); -} - -#[test] -fn sumcheck_proof_verify_rejects_committed_encoding() { - let proof = SumcheckProof::::Committed(CommittedSumcheckProof { - rounds: vec![CommittedRound { - commitment: F::from_u64(11), - degree: 1, - }], - output_claims: CommittedOutputClaims { - commitments: vec![F::from_u64(21)], - }, - }); - let claim = SumcheckClaim::new(1, 1, F::from_u64(0)); - - let mut transcript = Blake2bTranscript::::new(b"sumcheck-proof-dispatch"); - let result = proof.verify( - &claim, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::WrongProofEncoding { - expected: "full clear", - got: "committed", - }) - )); -} - #[test] fn batched_committed_consistency_accessors() { // `BatchedCommittedSumcheckConsistency` is produced by the generated ZK @@ -892,287 +376,6 @@ fn batched_instance_point_rejects_declared_width_exceeding_recorded_rounds() { )); } -#[test] -fn committed_rounds_check_transcript_and_return_public_data() { - let rounds = vec![ - CommittedRound { - commitment: F::from_u64(11), - degree: 1, - }, - CommittedRound { - commitment: F::from_u64(12), - degree: 2, - }, - CommittedRound { - commitment: F::from_u64(13), - degree: 0, - }, - ]; - - let mut manual = Blake2bTranscript::::new(b"committed-sumcheck"); - let mut expected_challenges = Vec::new(); - for round in &rounds { - manual.append(&LabelWithCount(b"sumcheck_commitment", round.degree as u64)); - round.commitment.append_to_transcript(&mut manual); - expected_challenges.push(manual.challenge()); - } - - let mut verifier = Blake2bTranscript::::new(b"committed-sumcheck"); - let consistency = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(3, 2), - &rounds, - &mut verifier, - ) - .unwrap(); - - assert_eq!(consistency.challenges(), expected_challenges); - assert_eq!(consistency.round_degrees(), vec![1, 2, 0]); - assert_eq!( - consistency.round_commitments(), - rounds - .iter() - .map(|round| round.commitment) - .collect::>() - ); - assert_eq!(verifier.state(), manual.state()); -} - -#[test] -fn committed_rounds_reject_wrong_round_count() { - let rounds = vec![CommittedRound { - commitment: F::from_u64(11), - degree: 1, - }]; - let mut transcript = Blake2bTranscript::::new(b"committed-sumcheck"); - - let result = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(2, 1), - &rounds, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::WrongNumberOfRounds { - expected: 2, - got: 1 - }) - )); -} - -#[test] -fn committed_rounds_reject_degree_bound_before_absorbing() { - let rounds = vec![CommittedRound { - commitment: F::from_u64(11), - degree: 3, - }]; - let mut transcript = Blake2bTranscript::::new(b"committed-sumcheck"); - let before = transcript.state(); - - let result = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(1, 2), - &rounds, - &mut transcript, - ); - - assert!(matches!( - result, - Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) - )); - assert_eq!(transcript.state(), before); -} - -#[test] -fn committed_output_claims_absorb_length_and_order() { - let output_claims = CommittedOutputClaims { - commitments: vec![F::from_u64(3), F::from_u64(5), F::from_u64(8)], - }; - - let mut actual = Blake2bTranscript::::new(b"committed-output"); - output_claims.append_to_transcript(&mut actual); - - let mut expected = Blake2bTranscript::::new(b"committed-output"); - expected.append(&LabelWithCount(b"output_claims_coms", 3)); - for commitment in &output_claims.commitments { - commitment.append_to_transcript(&mut expected); - } - - assert_eq!(actual.state(), expected.state()); - assert_eq!(actual.challenge(), expected.challenge()); -} - -#[test] -fn committed_proof_checks_rounds_then_output_claims() { - let proof = CommittedSumcheckProof { - rounds: vec![ - CommittedRound { - commitment: F::from_u64(11), - degree: 1, - }, - CommittedRound { - commitment: F::from_u64(12), - degree: 2, - }, - ], - output_claims: CommittedOutputClaims { - commitments: vec![F::from_u64(21), F::from_u64(34)], - }, - }; - - let mut manual = Blake2bTranscript::::new(b"committed-proof"); - let mut expected_challenges = Vec::new(); - for round in &proof.rounds { - manual.append(&LabelWithCount(b"sumcheck_commitment", round.degree as u64)); - round.commitment.append_to_transcript(&mut manual); - expected_challenges.push(manual.challenge()); - } - manual.append(&LabelWithCount(b"output_claims_coms", 2)); - for commitment in &proof.output_claims.commitments { - commitment.append_to_transcript(&mut manual); - } - - let mut verifier = Blake2bTranscript::::new(b"committed-proof"); - let consistency = proof - .verify_committed_consistency(SumcheckStatement::new(2, 2), &mut verifier) - .unwrap(); - - assert_eq!(consistency.challenges(), expected_challenges); - assert_eq!(consistency.round_degrees(), vec![1, 2]); - assert_eq!(verifier.state(), manual.state()); -} - -#[test] -fn committed_proof_rejects_bad_round_before_output_claims() { - let proof = CommittedSumcheckProof { - rounds: vec![CommittedRound { - commitment: F::from_u64(11), - degree: 3, - }], - output_claims: CommittedOutputClaims { - commitments: vec![F::from_u64(21)], - }, - }; - let mut transcript = Blake2bTranscript::::new(b"committed-proof"); - let before = transcript.state(); - - let result = proof.verify_committed_consistency(SumcheckStatement::new(1, 2), &mut transcript); - - assert!(matches!( - result, - Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) - )); - assert_eq!(transcript.state(), before); -} - -#[test] -fn committed_round_witness_commits_with_generic_vector_commitment() { - type VC = Pedersen; - - let generator = Bn254::g1_generator(); - let setup = PedersenSetup::new( - vec![ - generator, - generator.scalar_mul(&F::from_u64(2)), - generator.scalar_mul(&F::from_u64(3)), - ], - generator.scalar_mul(&F::from_u64(99)), - ); - let witness = CommittedRoundWitness { - coefficients: vec![F::from_u64(4), F::from_u64(5), F::from_u64(6)], - blinding: F::from_u64(7), - }; - - let round = witness.commit::(&setup).unwrap(); - - assert_eq!(round.degree, 2); - assert!(VC::verify( - &setup, - &round.commitment, - &witness.coefficients, - &witness.blinding - )); - assert!(!VC::verify( - &setup, - &round.commitment, - &witness.coefficients, - &(witness.blinding + F::from_u64(1)) - )); -} - -#[test] -fn committed_round_witness_rejects_empty_coefficients() { - type VC = Pedersen; - - let generator = Bn254::g1_generator(); - let setup = PedersenSetup::new(vec![generator], generator.scalar_mul(&F::from_u64(99))); - let witness = CommittedRoundWitness { - coefficients: Vec::new(), - blinding: F::from_u64(7), - }; - - let result = witness.commit::(&setup); - - assert!(matches!(result, Err(SumcheckError::EmptyRoundCoefficients))); -} - -/// A mock clear round that returns a fixed evaluation. -/// Used to verify that `SumcheckVerifier` accepts custom clear round messages. -struct MockClearRound { - fixed_sum: F, -} - -impl RoundMessage for MockClearRound { - fn degree(&self) -> usize { - 0 - } - - fn append_to_transcript(&self, transcript: &mut T) { - F::from_u64(42).append_to_transcript(transcript); - } -} - -impl ClearRound for MockClearRound { - fn evaluate(&self, _challenge: F) -> F { - self.fixed_sum - } - - fn coefficient_linear_combination(&self, coefficients: &[F]) -> F { - self.fixed_sum * coefficients[0] - } -} - -#[test] -fn verify_accepts_custom_clear_round_messages() { - let fixed = F::from_u64(0); - let round_proofs = [ - MockClearRound { fixed_sum: fixed }, - MockClearRound { fixed_sum: fixed }, - MockClearRound { fixed_sum: fixed }, - ]; - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: F::from_u64(0), - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-test"); - let result = SumcheckVerifier::verify(&claim, &round_proofs, BooleanHypercube, &mut vt); - assert!( - result.is_ok(), - "mock verifier should accept: {:?}", - result.err() - ); - - let EvaluationClaim { - point: challenges, - value: final_eval, - } = result.unwrap(); - assert_eq!(challenges.len(), 3); - // Mock always returns fixed_sum, so final eval should be fixed - assert_eq!(final_eval, fixed); -} - #[test] #[should_panic(expected = "degree >= 1")] fn sumcheck_claim_new_rejects_degree_zero() { @@ -1185,25 +388,19 @@ fn sumcheck_statement_new_rejects_degree_zero() { let _ = SumcheckStatement::new(3, 0); } -/// Drive an honest degree-1 sumcheck through the clear recorder and check the -/// assembled proof against `verify_compressed_boolean` on a twin transcript: -/// the recorder's writes (claim absorb, compressed round polys, opening-claim -/// absorb) must be byte-identical to what the verifier reads back, and the -/// verifier's reduction must land on the prover's bound value. +/// Drive an honest degree-1 sumcheck through the clear recorder: the claim +/// is absorbed publicly, the rounds and the output claim are sent, and the +/// verifier reading them back must land on the prover's bound value. #[test] fn clear_recorder_roundtrip_matches_compressed_verifier() { - use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; - use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; - let num_vars = 3; let evals: Vec = (0..1u64 << num_vars) .map(|i| F::from_u64(3 * i + 7)) .collect(); - let claimed_sum = compute_sum(&evals); + let claimed_sum: F = evals.iter().copied().sum(); - // Prover side: recorder-driven round loop (HighToLow binding, degree 1). - let mut prover_transcript = Blake2bTranscript::new(b"recorder-test"); - let mut recorder = ClearSumcheckRecorder::::new(); + let mut prover_transcript = prover(b"recorder"); + let mut recorder = ClearSumcheckRecorder::::new(); recorder.absorb_input_claims(&[claimed_sum], &mut prover_transcript); let mut buf = evals; @@ -1214,7 +411,7 @@ fn clear_recorder_roundtrip_matches_compressed_verifier() { let round_poly = UnivariatePoly::new(vec![eval_0, eval_1 - eval_0]); let r = recorder - .absorb_round(&round_poly, &mut prover_transcript) + .absorb_round(&round_poly, 1, &mut prover_transcript) .unwrap(); for i in 0..half { @@ -1223,26 +420,30 @@ fn clear_recorder_roundtrip_matches_compressed_verifier() { buf.truncate(half); } let final_eval = buf[0]; - let recorded = recorder + recorder .finish(&[final_eval], &mut prover_transcript) .unwrap(); + let prover_state = fingerprint(&prover_transcript); + let narg = prover_transcript.finish(); - // Verifier side: same transcript schedule via the public verify path. - let mut verifier_transcript = Blake2bTranscript::new(b"recorder-test"); - append_sumcheck_claim(&mut verifier_transcript, &claimed_sum); - let reduction = recorded - .proof - .verify_compressed_boolean(num_vars, 1, claimed_sum, &mut verifier_transcript) - .unwrap(); - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, &reduction.value); + let mut verifier_transcript = verifier(b"recorder", &narg); + verifier_transcript.public(&claimed_sum); + let reduction = SumcheckVerifier::verify_compressed( + &SumcheckClaim::new(num_vars, 1, claimed_sum), + &mut verifier_transcript, + ) + .unwrap(); + let output_claim: F = verifier_transcript.receive().unwrap(); assert_eq!(reduction.value, final_eval); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(output_claim, final_eval); + assert_eq!(fingerprint(&verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); } -/// A minimal dense multilinear [`ProveRounds`](crate::prover::ProveRounds) -/// member (HighToLow binding), constructible with a prescribed total sum — -/// the engine tests' stand-in for a real kernel-backed batch member. +/// A minimal dense multilinear [`ProveRounds`] member (HighToLow binding), +/// constructible with a prescribed total sum — the engine tests' stand-in for +/// a real kernel-backed batch member. pub(crate) struct DenseMember { evals: Vec, num_rounds: usize, @@ -1261,9 +462,7 @@ impl DenseMember { fn final_eval(&self) -> F { self.evals[0] } -} -impl DenseMember { /// The separate-pass bind: a full write pass over the tables, distinct /// from the eval pass in `prove_round`. fn bind(&mut self, challenge: F) { @@ -1275,7 +474,7 @@ impl DenseMember { } } -impl crate::prover::ProveRounds for DenseMember { +impl ProveRounds for DenseMember { fn num_rounds(&self) -> usize { self.num_rounds } @@ -1311,7 +510,7 @@ struct FusedDenseMember { num_rounds: usize, } -impl crate::prover::ProveRounds for FusedDenseMember { +impl ProveRounds for FusedDenseMember { fn num_rounds(&self) -> usize { self.num_rounds } @@ -1357,32 +556,44 @@ impl crate::prover::ProveRounds for FusedDenseMember { } } +/// The `begin_batch` head both roles run: input claims absorbed by the +/// recorder (publicly for the clear one), then one coefficient per member. +fn batch_head, C: Channel>( + recorder: &mut R, + input_claims: &[F], + channel: &mut C, +) -> Vec { + recorder.absorb_input_claims(input_claims, channel); + channel.challenges_small(input_claims.len()) +} + +fn prelude(members: &[(F, F, usize, usize)], max_num_vars: usize) -> BatchPrelude { + BatchPrelude::new( + members + .iter() + .map(|&(input_claim, coefficient, rounds, offset)| BatchMember { + input_claim, + coefficient, + rounds, + offset, + }) + .collect(), + max_num_vars, + 1, + ) +} + /// A member fusing bind and eval into one table pass must be byte-identical -/// to the reference member that binds and evaluates separately: same wire -/// proof, same challenges, same final claims, same transcript state. +/// to the reference member that binds and evaluates separately. #[test] fn fused_bind_eval_member_byte_matches_separate_passes() { - use crate::batch::{BatchMember, BatchPrelude}; - use crate::prover::{prove_batch, ProveRounds, SequentialRounds}; - use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; - let num_rounds = 4; let sum = F::from_u64(90210); let prove = |member: &mut dyn ProveRounds| { - let mut transcript = Blake2bTranscript::new(b"fused-vs-separate"); - let mut recorder = ClearSumcheckRecorder::::new(); - recorder.absorb_input_claims(&[sum], &mut transcript); - let coefficient: F = transcript.challenge_scalar(); - let prelude = BatchPrelude::new( - vec![BatchMember { - input_claim: sum, - coefficient, - rounds: num_rounds, - offset: 0, - }], - num_rounds, - 1, - ); + let mut transcript = prover(b"fused-vs-separate"); + let mut recorder = ClearSumcheckRecorder::::new(); + let coefficients = batch_head(&mut recorder, &[sum], &mut transcript); + let prelude = prelude(&[(sum, coefficients[0], num_rounds, 0)], num_rounds); let mut members: Vec<&mut dyn ProveRounds> = vec![member]; let proved = prove_batch( &prelude, @@ -1392,10 +603,11 @@ fn fused_bind_eval_member_byte_matches_separate_passes() { &mut transcript, ) .unwrap(); - let recorded = recorder + recorder .finish(&proved.member_claims, &mut transcript) .unwrap(); - (proved, recorded.proof, transcript.state()) + let state = fingerprint(&transcript); + (proved, transcript.finish(), state) }; let mut separate = DenseMember::with_sum(num_rounds, sum, 41); @@ -1403,11 +615,11 @@ fn fused_bind_eval_member_byte_matches_separate_passes() { evals: separate.evals.clone(), num_rounds, }; - let (separate_proved, separate_proof, separate_state) = prove(&mut separate); - let (fused_proved, fused_proof, fused_state) = prove(&mut fused); + let (separate_proved, separate_narg, separate_state) = prove(&mut separate); + let (fused_proved, fused_narg, fused_state) = prove(&mut fused); assert_eq!(separate_proved, fused_proved); - assert_eq!(separate_proof, fused_proof); + assert_eq!(separate_narg, fused_narg); assert_eq!(separate.final_eval(), fused.evals[0]); assert_eq!(separate_state, fused_state); } @@ -1425,10 +637,6 @@ fn pedersen_setup(capacity: u64) -> PedersenSetup { /// cannot carry a sumcheck round) and not panic. #[test] fn prove_batch_rejects_zero_max_degree() { - use crate::batch::{BatchMember, BatchPrelude}; - use crate::prover::{prove_batch, ProveRounds, SequentialRounds}; - use crate::recorder::ClearSumcheckRecorder; - let sum = F::from_u64(42); let mut member = DenseMember::with_sum(2, sum, 7); let prelude = BatchPrelude { @@ -1443,8 +651,8 @@ fn prove_batch_rejects_zero_max_degree() { max_degree: 0, }; let mut members: Vec<&mut dyn ProveRounds> = vec![&mut member]; - let mut transcript = Blake2bTranscript::new(b"zero-degree-batch"); - let mut recorder = ClearSumcheckRecorder::::new(); + let mut transcript = prover(b"zero-degree-batch"); + let mut recorder = ClearSumcheckRecorder::::new(); let result = prove_batch( &prelude, &mut members, @@ -1458,49 +666,27 @@ fn prove_batch_rejects_zero_max_degree() { )); } -/// Twin-transcript lock for the batched engine, padding included: a -/// 3-round and a 1-round member proved through `prove_batch` with the clear -/// recorder must be byte-identical to the verifier's head-replica + -/// `verify_compressed_boolean` + opening-claim absorbs, and the reduction must -/// land on the prover's final claim and challenges. -#[test] -fn prove_batch_clear_twin_matches_compressed_verifier_with_padding() { - use crate::batch::{BatchMember, BatchPrelude}; - use crate::prover::{prove_batch, ProveRounds, SequentialRounds}; - use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; - use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; - use jolt_field::Ring; - - let sum_long = F::from_u64(1234); - let sum_short = F::from_u64(777); - let mut long = DenseMember::with_sum(3, sum_long, 5); - let mut short = DenseMember::with_sum(1, sum_short, 91); - - // Prover: the begin_batch head choreography, the round loop, finish. - let mut prover_transcript = Blake2bTranscript::new(b"prove-batch-twin"); - let mut recorder = ClearSumcheckRecorder::::new(); - recorder.absorb_input_claims(&[sum_long, sum_short], &mut prover_transcript); - let coeff_long: F = prover_transcript.challenge_scalar(); - let coeff_short: F = prover_transcript.challenge_scalar(); - let prelude = BatchPrelude::new( - vec![ - BatchMember { - input_claim: sum_long, - coefficient: coeff_long, - rounds: 3, - offset: 0, - }, - BatchMember { - input_claim: sum_short, - coefficient: coeff_short, - rounds: 1, - offset: 2, - }, +/// Proves a 3-round and a 1-round member through the clear recorder and +/// replays the verifier's half of the protocol over the proof: head, +/// compressed rounds, then the sent member claims. +fn clear_batch_twin( + session: &[u8], + long: &mut DenseMember, + short: &mut DenseMember, + input_claims: [F; 2], + short_offset: usize, +) -> ProvedBatch { + let mut prover_transcript = prover(session); + let mut recorder = ClearSumcheckRecorder::::new(); + let coefficients = batch_head(&mut recorder, &input_claims, &mut prover_transcript); + let prelude = prelude( + &[ + (input_claims[0], coefficients[0], 3, 0), + (input_claims[1], coefficients[1], 1, short_offset), ], 3, - 1, ); - let mut members: Vec<&mut dyn ProveRounds> = vec![&mut long, &mut short]; + let mut members: Vec<&mut dyn ProveRounds> = vec![long, short]; let proved = prove_batch( &prelude, &mut members, @@ -1509,168 +695,98 @@ fn prove_batch_clear_twin_matches_compressed_verifier_with_padding() { &mut prover_transcript, ) .unwrap(); - assert_eq!( - proved.member_claims, - vec![long.final_eval(), short.final_eval()] - ); - let recorded = recorder + recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - assert!(recorded.committed_witness.is_none()); - - // Verifier twin. - let mut verifier_transcript = Blake2bTranscript::new(b"prove-batch-twin"); - append_sumcheck_claim(&mut verifier_transcript, &sum_long); - append_sumcheck_claim(&mut verifier_transcript, &sum_short); - let verifier_coeff_long: F = verifier_transcript.challenge_scalar(); - let verifier_coeff_short: F = verifier_transcript.challenge_scalar(); - assert_eq!( - (verifier_coeff_long, verifier_coeff_short), - (coeff_long, coeff_short) - ); + let prover_state = fingerprint(&prover_transcript); + let narg = prover_transcript.finish(); - let claimed_sum = - verifier_coeff_long * sum_long + verifier_coeff_short * sum_short.mul_pow_2(2); + let mut verifier_transcript = verifier(session, &narg); + let verifier_coefficients = batch_head( + &mut ClearSumcheckRecorder::::new(), + &input_claims, + &mut verifier_transcript, + ); + assert_eq!(verifier_coefficients, coefficients); + // The padded claimed sum is position-independent: the short member is + // scaled by 2^(3 - 1) whether it is head- or tail-aligned. + let claimed_sum = verifier_coefficients[0] * input_claims[0] + + verifier_coefficients[1] * input_claims[1].mul_pow_2(2); assert_eq!(claimed_sum, prelude.claimed_sum); - let reduction = recorded - .proof - .verify_compressed_boolean(3, 1, claimed_sum, &mut verifier_transcript) - .unwrap(); - for value in &proved.member_claims { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, value); - } + let reduction = SumcheckVerifier::verify_compressed( + &SumcheckClaim::new(3, 1, claimed_sum), + &mut verifier_transcript, + ) + .unwrap(); + let member_claims: Vec = verifier_transcript.receive_n(2).unwrap(); + assert_eq!(member_claims, proved.member_claims); assert_eq!(reduction.value, proved.final_claim); assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(fingerprint(&verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); + proved } -/// Twin-transcript lock for a head-aligned member: a 1-round member at -/// `offset: 0` is active in the batch's FIRST round and then halves through -/// the trailing dummy rounds. Its kernel must emit at the dummy-round padding -/// scale (the table sums to `2^(max − rounds) · input_claim`), so its final -/// batch claim is the fully-bound value with the padding halved back out. +#[test] +fn prove_batch_clear_twin_matches_compressed_verifier_with_padding() { + let input_claims = [F::from_u64(1234), F::from_u64(777)]; + let mut long = DenseMember::with_sum(3, input_claims[0], 5); + let mut short = DenseMember::with_sum(1, input_claims[1], 91); + + let proved = clear_batch_twin(b"prove-batch-twin", &mut long, &mut short, input_claims, 2); + assert_eq!( + proved.member_claims, + vec![long.final_eval(), short.final_eval()] + ); +} + +/// A 1-round member at `offset: 0` is active in the batch's FIRST round and +/// then halves through the trailing dummy rounds. Its kernel must emit at the +/// dummy-round padding scale (the table sums to `2^(max − rounds) · +/// input_claim`), so its final batch claim is the fully-bound value with the +/// padding halved back out. #[test] fn prove_batch_clear_twin_head_aligned_member() { - use crate::batch::{BatchMember, BatchPrelude}; - use crate::prover::{prove_batch, ProveRounds, SequentialRounds}; - use crate::recorder::{ClearSumcheckRecorder, SumcheckRecorder}; - use crate::{append_sumcheck_claim, OPENING_CLAIM_TRANSCRIPT_LABEL}; - use jolt_field::{Field, Ring}; - - let sum_long = F::from_u64(1234); - let sum_short = F::from_u64(777); - let mut long = DenseMember::with_sum(3, sum_long, 5); - // The head-aligned kernel's table carries the 2^(3 - 1) padding scale. - let mut short = DenseMember::with_sum(1, sum_short.mul_pow_2(2), 91); - - let mut prover_transcript = Blake2bTranscript::new(b"prove-batch-head-twin"); - let mut recorder = ClearSumcheckRecorder::::new(); - recorder.absorb_input_claims(&[sum_long, sum_short], &mut prover_transcript); - let coeff_long: F = prover_transcript.challenge_scalar(); - let coeff_short: F = prover_transcript.challenge_scalar(); - let prelude = BatchPrelude::new( - vec![ - BatchMember { - input_claim: sum_long, - coefficient: coeff_long, - rounds: 3, - offset: 0, - }, - BatchMember { - input_claim: sum_short, - coefficient: coeff_short, - rounds: 1, - offset: 0, - }, - ], - 3, - 1, + let input_claims = [F::from_u64(1234), F::from_u64(777)]; + let mut long = DenseMember::with_sum(3, input_claims[0], 5); + let mut short = DenseMember::with_sum(1, input_claims[1].mul_pow_2(2), 91); + + let proved = clear_batch_twin( + b"prove-batch-head-twin", + &mut long, + &mut short, + input_claims, + 0, ); - let mut members: Vec<&mut dyn ProveRounds> = vec![&mut long, &mut short]; - let proved = prove_batch( - &prelude, - &mut members, - &mut SequentialRounds, - &mut recorder, - &mut prover_transcript, - ) - .unwrap(); - // The short member bound the batch's FIRST challenge, then halved twice. let quarter = F::from_u64(4).inverse().unwrap(); assert_eq!( proved.member_claims, vec![long.final_eval(), short.final_eval() * quarter] ); - let recorded = recorder - .finish(&proved.member_claims, &mut prover_transcript) - .unwrap(); - - // Verifier twin: the claimed sum is position-independent — identical to - // the tail-aligned layout's. - let mut verifier_transcript = Blake2bTranscript::new(b"prove-batch-head-twin"); - append_sumcheck_claim(&mut verifier_transcript, &sum_long); - append_sumcheck_claim(&mut verifier_transcript, &sum_short); - let verifier_coeff_long: F = verifier_transcript.challenge_scalar(); - let verifier_coeff_short: F = verifier_transcript.challenge_scalar(); - let claimed_sum = - verifier_coeff_long * sum_long + verifier_coeff_short * sum_short.mul_pow_2(2); - assert_eq!(claimed_sum, prelude.claimed_sum); - let reduction = recorded - .proof - .verify_compressed_boolean(3, 1, claimed_sum, &mut verifier_transcript) - .unwrap(); - for value in &proved.member_claims { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, value); - } - - assert_eq!(reduction.value, proved.final_claim); - assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); } -/// The committed twin of the batched engine: the same members proved through -/// the committed recorder must be byte-identical to -/// `verify_committed_consistency_dims` (coefficient draws included — no claim -/// scalars are absorbed on either side), and the retained witness must open -/// every round commitment. +/// The committed twin of the batched engine: no claim scalar reaches the +/// transcript on either side, the verifier reads back the prover's +/// challenges, and the retained witness opens every commitment it read. #[test] fn prove_batch_committed_twin_matches_committed_consistency() { - use crate::batch::{BatchMember, BatchPrelude}; - use crate::prover::{prove_batch, ProveRounds, SequentialRounds}; - use crate::recorder::{CommittedSumcheckRecorder, SumcheckRecorder}; - - type VC = Pedersen; let setup = pedersen_setup(4); - let sum_long = F::from_u64(4242); - let sum_short = F::from_u64(1717); - let mut long = DenseMember::with_sum(3, sum_long, 23); - let mut short = DenseMember::with_sum(1, sum_short, 57); + let input_claims = [F::from_u64(4242), F::from_u64(1717)]; + let mut long = DenseMember::with_sum(3, input_claims[0], 23); + let mut short = DenseMember::with_sum(1, input_claims[1], 57); - let mut prover_transcript = Blake2bTranscript::new(b"prove-batch-zk-twin"); + let mut prover_transcript = prover(b"prove-batch-zk-twin"); let mut recorder = CommittedSumcheckRecorder::::new(&setup, rand_core::OsRng).unwrap(); - recorder.absorb_input_claims(&[sum_long, sum_short], &mut prover_transcript); - let coeff_long: F = prover_transcript.challenge_scalar(); - let coeff_short: F = prover_transcript.challenge_scalar(); - let prelude = BatchPrelude::new( - vec![ - BatchMember { - input_claim: sum_long, - coefficient: coeff_long, - rounds: 3, - offset: 0, - }, - BatchMember { - input_claim: sum_short, - coefficient: coeff_short, - rounds: 1, - offset: 2, - }, + let coefficients = batch_head(&mut recorder, &input_claims, &mut prover_transcript); + let prelude = prelude( + &[ + (input_claims[0], coefficients[0], 3, 0), + (input_claims[1], coefficients[1], 1, 2), ], 3, - 1, ); let mut members: Vec<&mut dyn ProveRounds> = vec![&mut long, &mut short]; let proved = prove_batch( @@ -1681,42 +797,36 @@ fn prove_batch_committed_twin_matches_committed_consistency() { &mut prover_transcript, ) .unwrap(); - let recorded = recorder + let witness = recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - - // Verifier twin: the committed path absorbs no claim scalars — only the - // coefficient squeezes and the commitments the proof carries. - let mut verifier_transcript = Blake2bTranscript::new(b"prove-batch-zk-twin"); - let _coeff_long: F = verifier_transcript.challenge_scalar(); - let _coeff_short: F = verifier_transcript.challenge_scalar(); - let consistency = recorded - .proof - .verify_committed_consistency_dims(3, 1, &mut verifier_transcript) - .unwrap(); + let prover_state = fingerprint(&prover_transcript); + let narg = prover_transcript.finish(); + + let mut verifier_transcript = verifier(b"prove-batch-zk-twin", &narg); + let verifier_coefficients: Vec = verifier_transcript.challenges_small(2); + assert_eq!(verifier_coefficients, coefficients); + let (consistency, output_claims) = SumcheckVerifier::verify_committed::( + SumcheckStatement::new(3, 1), + 1, + &mut verifier_transcript, + ) + .unwrap(); assert_eq!(consistency.challenges(), proved.challenges); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); - - // The retained witness opens every round commitment and output-claim row. - let witness = recorded.committed_witness.unwrap(); - let committed = recorded.proof.as_committed().unwrap(); - assert_eq!(witness.round_coefficients.len(), committed.rounds.len()); - for ((round, coefficients), blinding) in committed - .rounds + assert_eq!(fingerprint(&verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); + + assert_eq!(witness.round_coefficients.len(), 3); + for ((commitment, coefficients), blinding) in consistency + .round_commitments() .iter() .zip(&witness.round_coefficients) .zip(&witness.round_blindings) { - assert!(VC::verify( - &setup, - &round.commitment, - coefficients, - blinding - )); + assert!(VC::verify(&setup, commitment, coefficients, blinding)); } - for ((commitment, row), blinding) in committed - .output_claims + for ((commitment, row), blinding) in output_claims .commitments .iter() .zip(&witness.output_claim_rows) @@ -1724,86 +834,84 @@ fn prove_batch_committed_twin_matches_committed_consistency() { { assert!(VC::verify(&setup, commitment, row, blinding)); } - assert_eq!(witness.output_claim_rows.concat(), proved.member_claims,); + assert_eq!(witness.output_claim_rows.concat(), proved.member_claims); +} + +fn uniskip_round() -> (UnivariatePoly, F, usize, usize) { + let degree = 3; + let domain_size = 4; + let round_poly = poly(&[3, 1, 4, 1]); + let start = CenteredIntegerDomain::new(domain_size).start().unwrap(); + let input_claim = (start..start + domain_size as i64) + .map(|point| round_poly.evaluate(F::from_i64(point))) + .sum(); + (round_poly, input_claim, degree, domain_size) } -/// Twin-transcript lock for the clear uni-skip prover against the verify -/// choreography `jolt-verifier`'s `uniskip::verify_clear` performs: full -/// labeled round poly, challenge, output claim absorbed under -/// `b"opening_claim"` before any later draw. +/// The clear uni-skip prover against the verify choreography of +/// `jolt-verifier`'s `uniskip::verify_clear`: a full round over the centered +/// domain, its challenge, then the sent output claim. #[test] fn prove_uniskip_clear_twin_matches_uniskip_verify() { - use crate::prover::prove_uniskip_clear; - use crate::{CenteredIntegerDomain, OPENING_CLAIM_TRANSCRIPT_LABEL}; + let (round_poly, input_claim, degree, domain_size) = uniskip_round(); - let degree = 3; - let domain_size = 4; - let poly = UnivariatePoly::new(vec![ - F::from_u64(3), - F::from_u64(1), - F::from_u64(4), - F::from_u64(1), - ]); - let domain = CenteredIntegerDomain::new(domain_size); - let coefficients = domain.round_sum_coefficients(degree).unwrap(); - let input_claim = - as ClearRound>::coefficient_linear_combination(&poly, &coefficients); - - let mut prover_transcript = Blake2bTranscript::new(b"uniskip-twin"); - let proved = prove_uniskip_clear::( - poly, + let mut prover_transcript = prover(b"uniskip-twin"); + let proved = prove_uniskip_clear( + round_poly.clone(), input_claim, degree, domain_size, &mut prover_transcript, ) .unwrap(); + let prover_state = fingerprint(&prover_transcript); + let narg = prover_transcript.finish(); - let mut verifier_transcript = Blake2bTranscript::new(b"uniskip-twin"); - let reduction = proved - .proof - .verify( - &SumcheckClaim::new(1, degree, input_claim), - domain, - crate::UNISKIP_ROUND_TRANSCRIPT_LABEL, - &mut verifier_transcript, - ) - .unwrap(); - assert_eq!(reduction.value, proved.output_claim); - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, &proved.output_claim); + let mut verifier_transcript = verifier(b"uniskip-twin", &narg); + let reduction = SumcheckVerifier::verify( + &SumcheckClaim::new(1, degree, input_claim), + CenteredIntegerDomain::new(domain_size), + &mut verifier_transcript, + ) + .unwrap(); + let output_claim: F = verifier_transcript.receive().unwrap(); assert_eq!(reduction.point.as_slice(), &[proved.challenge]); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(reduction.value, round_poly.evaluate(proved.challenge)); + assert_eq!(output_claim, reduction.value); + assert_eq!(proved.output_claim, reduction.value); + assert_eq!(fingerprint(&verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); } -/// The committed uni-skip twin: `prove_uniskip_committed` must be -/// byte-identical to `verify_committed_consistency` (the transcript path of -/// `uniskip::verify_zk` — its commitment-count check is transcript-pure), and -/// the witness must open the round and output-claim commitments. #[test] -fn prove_uniskip_committed_twin_matches_committed_consistency() { - use crate::prover::prove_uniskip_committed; - use crate::CenteredIntegerDomain; +fn prove_uniskip_rejects_a_round_off_the_domain_sum_before_writing() { + let (round_poly, input_claim, degree, domain_size) = uniskip_round(); + let mut transcript = prover(b"uniskip-bad-sum"); + let result = prove_uniskip_clear( + round_poly, + input_claim + F::from_u64(1), + degree, + domain_size, + &mut transcript, + ); + assert!(matches!( + result, + Err(SumcheckError::RoundCheckFailed { round: 0, .. }) + )); + assert!(transcript.finish().is_empty()); +} - type VC = Pedersen; +/// The committed uni-skip twin: the verifier reads one round commitment and +/// one output-claim commitment, and the witness opens both. +#[test] +fn prove_uniskip_committed_twin_matches_committed_consistency() { let setup = pedersen_setup(4); + let (round_poly, input_claim, degree, domain_size) = uniskip_round(); - let degree = 3; - let domain_size = 4; - let poly = UnivariatePoly::new(vec![ - F::from_u64(2), - F::from_u64(7), - F::from_u64(1), - F::from_u64(8), - ]); - let domain = CenteredIntegerDomain::new(domain_size); - let coefficients = domain.round_sum_coefficients(degree).unwrap(); - let input_claim = - as ClearRound>::coefficient_linear_combination(&poly, &coefficients); - - let mut prover_transcript = Blake2bTranscript::new(b"uniskip-zk-twin"); + let mut prover_transcript = prover(b"uniskip-zk-twin"); let proved = prove_uniskip_committed::( - poly, + round_poly.clone(), input_claim, degree, domain_size, @@ -1812,17 +920,38 @@ fn prove_uniskip_committed_twin_matches_committed_consistency() { &mut prover_transcript, ) .unwrap(); + let prover_state = fingerprint(&prover_transcript); + let narg = prover_transcript.finish(); - let mut verifier_transcript = Blake2bTranscript::new(b"uniskip-zk-twin"); - let consistency = proved - .proof - .verify_committed_consistency(SumcheckStatement::new(1, degree), &mut verifier_transcript) - .unwrap(); + let mut verifier_transcript = verifier(b"uniskip-zk-twin", &narg); + let (consistency, output_claims) = SumcheckVerifier::verify_committed::( + SumcheckStatement::new(1, degree), + 1, + &mut verifier_transcript, + ) + .unwrap(); assert_eq!(consistency.challenges(), vec![proved.challenge]); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(fingerprint(&verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); + + let witness = &proved.witness; assert_eq!( - proved.witness.output_claim_rows.concat(), - vec![proved.output_claim], + witness.round_coefficients, + vec![round_poly.coefficients().to_vec()] ); + assert!(VC::verify( + &setup, + &consistency.round_commitments()[0], + &witness.round_coefficients[0], + &witness.round_blindings[0], + )); + assert_eq!(witness.output_claim_rows, vec![vec![proved.output_claim]]); + assert_eq!(proved.output_claim, round_poly.evaluate(proved.challenge)); + assert!(VC::verify( + &setup, + &output_claims.commitments[0], + &witness.output_claim_rows[0], + &witness.output_claim_blindings[0], + )); } diff --git a/crates/jolt-sumcheck/tests/committed.rs b/crates/jolt-sumcheck/tests/committed.rs index f5a8d37421..67a28a7904 100644 --- a/crates/jolt-sumcheck/tests/committed.rs +++ b/crates/jolt-sumcheck/tests/committed.rs @@ -1,17 +1,30 @@ -#![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] - -use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup}; -use jolt_field::{Fr, Ring}; -use jolt_sumcheck::round_proof::RoundMessage; +#![expect( + clippy::unwrap_used, + clippy::indexing_slicing, + reason = "tests may panic on assertion failures and index fixture data" +)] + +use jolt_crypto::{ + Bn254, Bn254G1, JoltGroup, NoVectorCommitment, Pedersen, PedersenSetup, VectorCommitment, +}; +use jolt_field::{CanonicalBytes, Fr, Ring}; +use jolt_poly::UnivariatePoly; use jolt_sumcheck::{ - CommittedOutputClaims, CommittedRound, CommittedRoundWitness, SumcheckError, SumcheckStatement, - SumcheckVerifier, + CommittedOutputClaims, CommittedSumcheckBuilder, CommittedSumcheckConsistency, + CommittedSumcheckWitness, SumcheckError, SumcheckStatement, SumcheckVerifier, +}; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, TranscriptError, VerifierTranscript, }; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, LabelWithCount, Transcript}; +use rand_core::OsRng; type F = Fr; type VC = Pedersen; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/tests/committed"); +const SESSION: &[u8] = b"committed-roundtrip"; +const COMMITMENT_BYTES: usize = ::NUM_BYTES; + fn pedersen_setup(capacity: usize) -> PedersenSetup { let generator = Bn254::g1_generator(); let message_generators = (1..=capacity) @@ -20,155 +33,169 @@ fn pedersen_setup(capacity: usize) -> PedersenSetup { PedersenSetup::new(message_generators, generator.scalar_mul(&F::from_u64(99))) } -fn committed_rounds( - setup: &PedersenSetup, - coefficients: &[Vec], -) -> Vec> { - coefficients - .iter() - .enumerate() - .map(|(round, coefficients)| { - CommittedRoundWitness { - coefficients: coefficients.clone(), - blinding: F::from_u64(round as u64 + 17), - } - .commit::(setup) - .unwrap() - }) - .collect() +fn poly(coefficients: &[u64]) -> UnivariatePoly { + UnivariatePoly::new(coefficients.iter().copied().map(F::from_u64).collect()) } -#[test] -fn committed_rounds_complete_with_pedersen_commitments() { - let setup = pedersen_setup(3); - let rounds = committed_rounds( - &setup, - &[ - vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)], - vec![F::from_u64(7), F::from_u64(11)], - vec![F::from_u64(13), F::from_u64(17), F::from_u64(19)], - ], - ); +struct Proved { + narg: Vec, + challenges: Vec, + witness: CommittedSumcheckWitness, + fingerprint: [u8; 32], +} - let mut prover_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); - let mut expected_challenges = Vec::new(); - for round in &rounds { - round.append_to_transcript(&mut prover_transcript); - expected_challenges.push(prover_transcript.challenge()); +/// Commits three rounds at degree bound 2 (the middle one of degree 1) and +/// four output-claim values in rows of the setup's capacity 3. +fn prove(setup: &PedersenSetup) -> Proved { + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + let mut builder = CommittedSumcheckBuilder::::new(setup, OsRng).unwrap(); + let challenges = [poly(&[2, 3, 5]), poly(&[7, 11]), poly(&[13, 17, 19])] + .iter() + .map(|round| builder.commit_round(round, 2, &mut transcript).unwrap()) + .collect(); + let outputs: Vec = [23, 29, 31, 37].map(F::from_u64).to_vec(); + let witness = builder.finish(&outputs, &mut transcript).unwrap(); + let fingerprint = transcript.preview().squeeze(); + Proved { + narg: transcript.finish(), + challenges, + witness, + fingerprint, } +} - let mut verifier_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); - let consistency = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(rounds.len(), 2), - &rounds, - &mut verifier_transcript, - ) - .unwrap(); - - assert_eq!(consistency.challenges(), expected_challenges); - assert_eq!(consistency.round_degrees(), vec![2, 1, 2]); - assert_eq!(verifier_transcript.state(), prover_transcript.state()); +type Verified = ( + CommittedSumcheckConsistency, + CommittedOutputClaims, +); + +fn verify( + narg: &[u8], + statement: SumcheckStatement, + num_output_commitments: usize, +) -> Result<(Verified, [u8; 32]), SumcheckError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let verified = + SumcheckVerifier::verify_committed(statement, num_output_commitments, &mut transcript)?; + let fingerprint = transcript.preview().squeeze(); + transcript.finish()?; + Ok((verified, fingerprint)) } #[test] -fn committed_rounds_reject_wrong_count_and_degree() { +fn committed_rounds_pad_to_the_statement_degree_and_open() { let setup = pedersen_setup(3); - let rounds = committed_rounds( - &setup, - &[ - vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)], - vec![F::from_u64(7), F::from_u64(11)], - ], + let proved = prove(&setup); + assert_eq!(proved.narg.len(), 5 * COMMITMENT_BYTES); + + let ((consistency, output_claims), fingerprint) = + verify(&proved.narg, SumcheckStatement::new(3, 2), 2).unwrap(); + assert_eq!(fingerprint, proved.fingerprint); + assert_eq!(consistency.challenges(), proved.challenges); + assert_eq!(consistency.round_degrees(), vec![2, 2, 2]); + + let witness = &proved.witness; + assert_eq!( + witness.round_coefficients[1], + poly(&[7, 11, 0]).coefficients() ); + for ((commitment, coefficients), blinding) in consistency + .round_commitments() + .iter() + .zip(&witness.round_coefficients) + .zip(&witness.round_blindings) + { + assert!(VC::verify(&setup, commitment, coefficients, blinding)); + } - let mut wrong_count_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); - let wrong_count = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(3, 2), - &rounds, - &mut wrong_count_transcript, + assert_eq!( + witness.output_claim_rows, + vec![ + [23, 29, 31].map(F::from_u64).to_vec(), + vec![F::from_u64(37)] + ] ); - assert!(matches!( - wrong_count, - Err(SumcheckError::WrongNumberOfRounds { - expected: 3, - got: 2 - }) - )); + assert_eq!(output_claims.commitments.len(), 2); + for ((commitment, row), blinding) in output_claims + .commitments + .iter() + .zip(&witness.output_claim_rows) + .zip(&witness.output_claim_blindings) + { + assert!(VC::verify(&setup, commitment, row, blinding)); + } +} - let mut degree_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); - let degree = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(2, 1), - &rounds, - &mut degree_transcript, - ); - assert!(matches!( - degree, - Err(SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }) - )); +#[test] +fn committed_proof_rejects_mismatched_public_dimensions() { + let proved = prove(&pedersen_setup(3)); + for (statement, outputs, expected) in [ + (SumcheckStatement::new(4, 2), 2, TranscriptError::Truncated), + (SumcheckStatement::new(3, 2), 3, TranscriptError::Truncated), + ( + SumcheckStatement::new(2, 2), + 2, + TranscriptError::TrailingBytes, + ), + ( + SumcheckStatement::new(3, 2), + 1, + TranscriptError::TrailingBytes, + ), + ] { + assert!( + matches!( + verify(&proved.narg, statement, outputs), + Err(SumcheckError::Transcript(error)) if error == expected + ), + "{statement:?} with {outputs} output commitments" + ); + } } #[test] -fn tampered_committed_round_changes_challenges() { +fn replaced_round_commitment_changes_later_challenges() { let setup = pedersen_setup(3); - let rounds = committed_rounds( + let proved = prove(&setup); + let replacement = VC::commit( &setup, - &[ - vec![F::from_u64(2), F::from_u64(3), F::from_u64(5)], - vec![F::from_u64(7), F::from_u64(11), F::from_u64(13)], - ], + poly(&[101, 103, 107]).coefficients(), + &F::from_u64(1), ); - let mut tampered = rounds.clone(); - tampered[1] = committed_rounds( - &setup, - &[vec![F::from_u64(101), F::from_u64(103), F::from_u64(107)]], - ) - .remove(0); - - let mut original_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); - let original = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(2, 2), - &rounds, - &mut original_transcript, - ) - .unwrap(); - - let mut tampered_transcript = Blake2bTranscript::::new(b"committed-roundtrip"); - let tampered = SumcheckVerifier::verify_committed_round_consistency( - SumcheckStatement::new(2, 2), - &tampered, - &mut tampered_transcript, - ) - .unwrap(); - - assert_ne!(tampered.challenges(), original.challenges()); - assert_ne!(tampered_transcript.state(), original_transcript.state()); + let mut tampered = proved.narg.clone(); + replacement.to_bytes_le(&mut tampered[COMMITMENT_BYTES..2 * COMMITMENT_BYTES]); + + let ((consistency, _), fingerprint) = + verify(&tampered, SumcheckStatement::new(3, 2), 2).unwrap(); + let challenges = consistency.challenges(); + assert_eq!(challenges[0], proved.challenges[0]); + assert_ne!(challenges[1], proved.challenges[1]); + assert_ne!(challenges[2], proved.challenges[2]); + assert_ne!(fingerprint, proved.fingerprint); } #[test] -fn committed_output_claims_keep_length_and_order() { - let setup = pedersen_setup(2); - let rounds = committed_rounds( - &setup, - &[ - vec![F::from_u64(2), F::from_u64(3)], - vec![F::from_u64(5), F::from_u64(7)], - ], - ); - let output_claims = CommittedOutputClaims { - commitments: rounds - .iter() - .map(|round| round.commitment) - .collect::>(), - }; - - let mut actual = Blake2bTranscript::::new(b"committed-output"); - output_claims.append_to_transcript(&mut actual); - - let mut expected = Blake2bTranscript::::new(b"committed-output"); - expected.append(&LabelWithCount(b"output_claims_coms", 2)); - for commitment in &output_claims.commitments { - commitment.append_to_transcript(&mut expected); - } +fn builder_rejects_rounds_it_cannot_commit_before_writing() { + let setup = pedersen_setup(3); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + let mut builder = CommittedSumcheckBuilder::::new(&setup, OsRng).unwrap(); + assert!(matches!( + builder.commit_round(&poly(&[1, 2, 3, 4]), 2, &mut transcript), + Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) + )); + assert!(matches!( + builder.commit_round(&poly(&[1, 2]), 3, &mut transcript), + Err(SumcheckError::RoundExceedsCommitmentCapacity { + coefficients: 4, + capacity: 3 + }) + )); + let witness = builder.finish(&[], &mut transcript).unwrap(); + assert!(witness.round_coefficients.is_empty()); + assert!(transcript.finish().is_empty()); - assert_eq!(actual.state(), expected.state()); + assert!(matches!( + CommittedSumcheckBuilder::, _>::new(&(), OsRng), + Err(SumcheckError::ZeroCommitmentCapacity) + )); } diff --git a/crates/jolt-sumcheck/tests/extension_fields.rs b/crates/jolt-sumcheck/tests/extension_fields.rs deleted file mode 100644 index b5ba93685e..0000000000 --- a/crates/jolt-sumcheck/tests/extension_fields.rs +++ /dev/null @@ -1,448 +0,0 @@ -#![expect( - clippy::panic, - clippy::unwrap_used, - reason = "tests may panic on assertion failures" -)] - -use std::marker::PhantomData; - -use jolt_field::{Ext2, Field, FpExt4, Prime128Offset275, Prime32Offset99, Prime64Offset59, Ring}; -use jolt_poly::{CompressedPoly, UnivariatePoly}; -use jolt_sumcheck::{ - prove_batch, prove_uniskip_clear, BatchMember, BatchPrelude, BooleanHypercube, - CenteredIntegerDomain, ClearProof, ClearSumcheckRecorder, CompressedSumcheckProof, ProveRounds, - SequentialRounds, SumcheckClaim, SumcheckError, SumcheckProof, SumcheckRecorder, - SumcheckVerifier, OPENING_CLAIM_TRANSCRIPT_LABEL, SUMCHECK_CLAIM_TRANSCRIPT_LABEL, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, UNISKIP_ROUND_TRANSCRIPT_LABEL, -}; -use jolt_transcript::{AppendToTranscript, Transcript}; -use num_traits::{One, Zero}; -use serde::{Deserialize, Serialize}; - -trait ChallengeField: Field + AppendToTranscript + 'static { - fn challenge_from_seed(seed: u64) -> Self; -} - -type Quadratic = Ext2; -type Quartic = FpExt4; - -impl ChallengeField for Quadratic { - fn challenge_from_seed(seed: u64) -> Self { - Self::new( - Prime64Offset59::from_u64(seed + 1), - Prime64Offset59::from_u64(seed + 11), - ) - } -} - -impl ChallengeField for Quartic { - fn challenge_from_seed(seed: u64) -> Self { - Self::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(seed + 3 * index as u64 + 1) - })) - } -} - -impl ChallengeField for Prime128Offset275 { - fn challenge_from_seed(seed: u64) -> Self { - Self::from_u64(seed + 1) - } -} - -struct ExtensionTranscript { - state: [u8; 32], - cursor: usize, - challenge_index: u64, - _field: PhantomData, -} - -impl Default for ExtensionTranscript { - fn default() -> Self { - Self { - state: [0; 32], - cursor: 0, - challenge_index: 0, - _field: PhantomData, - } - } -} - -impl Transcript for ExtensionTranscript { - type Challenge = F; - - fn new(label: &'static [u8]) -> Self { - let mut transcript = Self::default(); - transcript.append_bytes(label); - transcript - } - - fn append_bytes(&mut self, bytes: &[u8]) { - for byte in bytes { - let index = self.cursor % self.state.len(); - self.state[index] = self.state[index] - .wrapping_mul(31) - .wrapping_add(*byte) - .wrapping_add(1); - self.cursor += 1; - } - } - - fn challenge(&mut self) -> F { - self.append_bytes(b"extension_challenge"); - let state_seed = u64::from_le_bytes(self.state[..8].try_into().unwrap()); - let seed = state_seed.wrapping_add(self.challenge_index); - self.challenge_index = self.challenge_index.wrapping_add(1); - F::challenge_from_seed(seed) - } - - fn state(&self) -> [u8; 32] { - self.state - } -} - -#[derive(Default)] -struct CapturingTranscript { - absorbed: Vec, -} - -impl Transcript for CapturingTranscript { - type Challenge = Quadratic; - - fn new(label: &'static [u8]) -> Self { - Self { - absorbed: label.to_vec(), - } - } - - fn append_bytes(&mut self, bytes: &[u8]) { - self.absorbed.extend_from_slice(bytes); - } - - fn challenge(&mut self) -> Self::Challenge { - Quadratic::zero() - } - - fn state(&self) -> [u8; 32] { - [0; 32] - } -} - -struct ProductOfAffines { - num_rounds: usize, - left: Vec, - right: Vec, -} - -impl ProductOfAffines { - fn new(left: Vec, right: Vec) -> Self { - assert_eq!(left.len(), right.len()); - assert!(left.len().is_power_of_two()); - Self { - num_rounds: left.len().ilog2() as usize, - left, - right, - } - } - - fn bind(table: &mut Vec, challenge: F) { - for index in 0..table.len() / 2 { - let low = table[2 * index]; - let high = table[2 * index + 1]; - table[index] = low + challenge * (high - low); - } - table.truncate(table.len() / 2); - } -} - -impl ProveRounds for ProductOfAffines { - fn num_rounds(&self) -> usize { - self.num_rounds - } - - fn prove_round( - &mut self, - bind: Option, - _round: usize, - previous_claim: F, - ) -> Result, SumcheckError> { - if let Some(challenge) = bind { - Self::bind(&mut self.left, challenge); - Self::bind(&mut self.right, challenge); - } - - let mut coefficients = [F::zero(); 3]; - for (left, right) in self.left.chunks_exact(2).zip(self.right.chunks_exact(2)) { - let left_delta = left[1] - left[0]; - let right_delta = right[1] - right[0]; - coefficients[0] += left[0] * right[0]; - coefficients[1] += left[0] * right_delta + left_delta * right[0]; - coefficients[2] += left_delta * right_delta; - } - let polynomial = UnivariatePoly::new(coefficients.to_vec()); - assert_eq!( - polynomial.evaluate(F::zero()) + polynomial.evaluate(F::one()), - previous_claim - ); - Ok(polynomial) - } - - fn finish_rounds(&mut self, bind: F) -> Result<(), SumcheckError> { - Self::bind(&mut self.left, bind); - Self::bind(&mut self.right, bind); - Ok(()) - } -} - -fn affine(coefficients: [F; 3], point: [F; 2]) -> F { - coefficients[0] + coefficients[1] * point[0] + coefficients[2] * point[1] -} - -fn affine_table(coefficients: [F; 3]) -> Vec { - [ - [F::zero(), F::zero()], - [F::one(), F::zero()], - [F::zero(), F::one()], - [F::one(), F::one()], - ] - .into_iter() - .map(|point| affine(coefficients, point)) - .collect() -} - -fn run_batched_roundtrip(left_coefficients: [F; 3], right_coefficients: [F; 3]) -where - F: ChallengeField + Serialize + for<'de> Deserialize<'de>, -{ - let left = affine_table(left_coefficients); - let right = affine_table(right_coefficients); - let input_claim: F = left.iter().zip(&right).map(|(&a, &b)| a * b).sum(); - - let mut prover_transcript = ExtensionTranscript::::new(b"extension-sumcheck"); - let mut recorder = ClearSumcheckRecorder::::new(); - recorder.absorb_input_claims(&[input_claim], &mut prover_transcript); - let coefficient = prover_transcript.challenge_scalar(); - let prelude = BatchPrelude::try_new( - vec![BatchMember { - input_claim, - coefficient, - rounds: 2, - offset: 0, - }], - 2, - 2, - ) - .unwrap(); - let mut product = ProductOfAffines::new(left, right); - let mut members: [&mut dyn ProveRounds; 1] = [&mut product]; - let proved = prove_batch( - &prelude, - &mut members, - &mut SequentialRounds, - &mut recorder, - &mut prover_transcript, - ) - .unwrap(); - let recorded = recorder - .finish(&proved.member_claims, &mut prover_transcript) - .unwrap(); - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = recorded.proof else { - panic!("clear recorder returned a non-compressed proof") - }; - - let wire = bincode::serde::encode_to_vec(&proof, bincode::config::standard()).unwrap(); - let (proof, read): (CompressedSumcheckProof, usize) = - bincode::serde::decode_from_slice(&wire, bincode::config::standard()).unwrap(); - assert_eq!(read, wire.len()); - - let mut verifier_transcript = ExtensionTranscript::::new(b"extension-sumcheck"); - verifier_transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, &input_claim); - let verifier_coefficient = verifier_transcript.challenge_scalar(); - assert_eq!(verifier_coefficient, coefficient); - let reduced = SumcheckVerifier::verify_compressed( - &SumcheckClaim::new(2, 2, coefficient * input_claim), - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut verifier_transcript, - ) - .unwrap(); - for claim in &proved.member_claims { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, claim); - } - - let point: [F; 2] = reduced.point.as_slice().try_into().unwrap(); - let expected = affine(left_coefficients, point) * affine(right_coefficients, point); - assert_eq!(proved.member_claims, vec![expected]); - assert_eq!(reduced.value, coefficient * expected); - assert_eq!(proved.final_claim, reduced.value); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); -} - -fn run_uniskip_roundtrip(coefficients: [F; 3]) -where - F: ChallengeField, -{ - let polynomial = UnivariatePoly::new(coefficients.to_vec()); - let input_claim = [-1, 0, 1] - .into_iter() - .map(|point| polynomial.evaluate(F::from_i64(point))) - .sum(); - let mut prover_transcript = ExtensionTranscript::::new(b"extension-uniskip"); - let proved = prove_uniskip_clear::( - polynomial.clone(), - input_claim, - 2, - 3, - &mut prover_transcript, - ) - .unwrap(); - - let mut verifier_transcript = ExtensionTranscript::::new(b"extension-uniskip"); - let reduced = proved - .proof - .verify( - &SumcheckClaim::new(1, 2, input_claim), - CenteredIntegerDomain::new(3), - UNISKIP_ROUND_TRANSCRIPT_LABEL, - &mut verifier_transcript, - ) - .unwrap(); - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, &reduced.value); - - assert_eq!(reduced.point.as_slice(), &[proved.challenge]); - assert_eq!(reduced.value, polynomial.evaluate(proved.challenge)); - assert_eq!(reduced.value, proved.output_claim); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); -} - -#[test] -fn quadratic_extension_runs_prover_and_verifier() { - let value = |a, b| Quadratic::new(Prime64Offset59::from_u64(a), Prime64Offset59::from_u64(b)); - run_batched_roundtrip( - [value(1, 2), value(3, 4), value(5, 6)], - [value(7, 8), value(9, 10), value(11, 12)], - ); - run_uniskip_roundtrip([value(2, 3), value(5, 7), value(11, 13)]); -} - -#[test] -fn quadratic_transcript_absorption_reverses_the_complete_payload() { - let value = Quadratic::new(Prime64Offset59::from_u64(1), Prime64Offset59::from_u64(2)); - let mut transcript = CapturingTranscript::default(); - value.append_to_transcript(&mut transcript); - assert_eq!( - transcript.absorbed, - [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1] - ); -} - -#[test] -fn direct_128_bit_field_runs_prover_and_verifier() { - let value = Prime128Offset275::from_u64; - run_batched_roundtrip( - [value(1), value(3), value(5)], - [value(7), value(9), value(11)], - ); - run_uniskip_roundtrip([value(2), value(5), value(11)]); -} - -#[test] -fn quartic_extension_runs_prover_and_verifier() { - let value = |offset| { - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(offset + index as u64 + 1) - })) - }; - run_batched_roundtrip( - [value(0), value(4), value(8)], - [value(12), value(16), value(20)], - ); - run_uniskip_roundtrip([value(24), value(28), value(32)]); -} - -#[test] -fn tampered_quartic_proof_fails_final_evaluation_check() { - let left_coefficients = [ - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(index as u64 + 1) - })), - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(index as u64 + 5) - })), - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(index as u64 + 9) - })), - ]; - let right_coefficients = [ - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(index as u64 + 13) - })), - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(index as u64 + 17) - })), - Quartic::new(std::array::from_fn(|index| { - Prime32Offset99::from_u64(index as u64 + 21) - })), - ]; - let left = affine_table(left_coefficients); - let right = affine_table(right_coefficients); - let input_claim: Quartic = left.iter().zip(&right).map(|(&a, &b)| a * b).sum(); - - let mut prover_transcript = ExtensionTranscript::::new(b"tampered-extension"); - let mut recorder = ClearSumcheckRecorder::::new(); - recorder.absorb_input_claims(&[input_claim], &mut prover_transcript); - let coefficient = prover_transcript.challenge_scalar(); - let prelude = BatchPrelude::try_new( - vec![BatchMember { - input_claim, - coefficient, - rounds: 2, - offset: 0, - }], - 2, - 2, - ) - .unwrap(); - let mut product = ProductOfAffines::new(left, right); - let mut members: [&mut dyn ProveRounds; 1] = [&mut product]; - let proved = prove_batch( - &prelude, - &mut members, - &mut SequentialRounds, - &mut recorder, - &mut prover_transcript, - ) - .unwrap(); - let recorded = recorder - .finish(&proved.member_claims, &mut prover_transcript) - .unwrap(); - let SumcheckProof::Clear(ClearProof::Compressed(mut proof)) = recorded.proof else { - panic!("clear recorder returned a non-compressed proof") - }; - let mut coefficients = proof.round_polynomials[0] - .coeffs_except_linear_term() - .to_vec(); - coefficients[0] += Quartic::new([ - Prime32Offset99::zero(), - Prime32Offset99::one(), - Prime32Offset99::zero(), - Prime32Offset99::zero(), - ]); - proof.round_polynomials[0] = CompressedPoly::new(coefficients); - - let mut verifier_transcript = ExtensionTranscript::::new(b"tampered-extension"); - verifier_transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, &input_claim); - let verifier_coefficient = verifier_transcript.challenge_scalar(); - let reduced = SumcheckVerifier::verify_compressed( - &SumcheckClaim::new(2, 2, verifier_coefficient * input_claim), - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut verifier_transcript, - ) - .unwrap(); - let point: [Quartic; 2] = reduced.point.as_slice().try_into().unwrap(); - let expected = - verifier_coefficient * affine(left_coefficients, point) * affine(right_coefficients, point); - assert_ne!(reduced.value, expected); -} diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock index 8040d45eb7..dfe6fd3fab 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock @@ -8,6 +8,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cc" version = "1.4.5" @@ -18,19 +27,95 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + [[package]] name = "find-msvc-tools" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "jolt-field" version = "0.1.0" dependencies = [ "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", "thiserror", ] @@ -42,7 +127,7 @@ dependencies = [ "jolt-field", "jolt-utils", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", "thiserror", "tracing", @@ -69,7 +154,7 @@ dependencies = [ "jolt-sumcheck", "jolt-transcript", "num-traits", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -77,6 +162,10 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "jolt-field", + "rand 0.8.8", + "rand_core 0.6.4", + "spongefish", + "thiserror", ] [[package]] @@ -86,6 +175,50 @@ dependencies = [ "num-traits", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -95,12 +228,192 @@ dependencies = [ "autocfg", ] +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools", + "num-bigint", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cbc82137c157673e2d33d95f936a94ce237a790d46e0a2b3165dccfc385dd6" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools", + "num-bigint", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "spin", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -119,11 +432,56 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" @@ -155,12 +513,55 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spongefish" +version = "0.7.4" +source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" +dependencies = [ + "digest", + "keccak", + "p3-koala-bear", + "rand 0.8.8", + "sha2", + "sha3", +] + [[package]] name = "syn" version = "2.0.119" @@ -231,8 +632,40 @@ version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml index e8c6cf5ec8..1b73d98c36 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml @@ -16,6 +16,8 @@ jolt-field = { path = "../../../jolt-field", default-features = false, features ] } jolt-poly = { path = "../../../jolt-poly", default-features = false } jolt-sumcheck = { path = "../..", default-features = false } -jolt-transcript = { path = "../../../jolt-transcript", default-features = false } +jolt-transcript = { path = "../../../jolt-transcript", default-features = false, features = [ + "transcript-keccak", +] } num-traits = { version = "0.2.19", default-features = false } rand_core = { version = "0.6.4", default-features = false } diff --git a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs index a800e666a9..a66199d36a 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs +++ b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs @@ -4,15 +4,15 @@ use std::{ ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; -use jolt_field::{AdditiveGroup, Field, Prime64Offset59, Ring}; +use jolt_field::{ + AdditiveGroup, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, Prime64Offset59, Ring, +}; use jolt_poly::UnivariatePoly; use jolt_sumcheck::{ - prove_batch, BatchMember, BatchPrelude, BooleanHypercube, ClearProof, - ClearSumcheckRecorder, ProveRounds, SequentialRounds, SumcheckClaim, SumcheckError, - SumcheckProof, SumcheckRecorder, SumcheckVerifier, OPENING_CLAIM_TRANSCRIPT_LABEL, - SUMCHECK_CLAIM_TRANSCRIPT_LABEL, SUMCHECK_ROUND_TRANSCRIPT_LABEL, + prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, ProveRounds, SequentialRounds, + SumcheckClaim, SumcheckError, SumcheckRecorder, SumcheckVerifier, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, Keccak, ProtocolId, ProverTranscript, VerifierTranscript}; use num_traits::{One, Zero}; use rand_core::RngCore; @@ -173,48 +173,51 @@ impl Field for ExternalField { } } -impl AppendToTranscript for ExternalField { - fn append_to_transcript(&self, transcript: &mut T) { - self.0.append_to_transcript(transcript); +impl CanonicalBytes for ExternalField { + const NUM_BYTES: usize = Prime64Offset59::NUM_BYTES; + + fn to_bytes_le(&self, out: &mut [u8]) { + self.0.to_bytes_le(out); } } -#[derive(Default)] -struct ExternalTranscript { - state: [u8; 32], - cursor: usize, +impl CanonicalDecode for ExternalField { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Prime64Offset59::from_bytes_le_checked(bytes).map(Self) + } } -impl Transcript for ExternalTranscript { - type Challenge = ExternalField; +impl CanonicalEncoding for ExternalField { + const MODULUS_BITS: u32 = Prime64Offset59::MODULUS_BITS; + + fn from_bytes_le_reduced(bytes: &[u8]) -> Self { + Self(Prime64Offset59::from_bytes_le_reduced(bytes)) + } + + fn to_u128_checked(&self) -> Option { + self.0.to_u128_checked() + } - fn new(label: &'static [u8]) -> Self { - let mut transcript = Self::default(); - transcript.append_bytes(label); - transcript + fn from_u128_checked(value: u128) -> Option { + Prime64Offset59::from_u128_checked(value).map(Self) } - fn append_bytes(&mut self, bytes: &[u8]) { - for byte in bytes { - let index = self.cursor % self.state.len(); - self.state[index] = self.state[index] - .wrapping_mul(31) - .wrapping_add(*byte) - .wrapping_add(1); - self.cursor += 1; - } + fn from_u128_reduced(value: u128) -> Self { + Self(Prime64Offset59::from_u128_reduced(value)) } - fn challenge(&mut self) -> ExternalField { - self.append_bytes(b"challenge"); - ExternalField::from_u64(u64::from_le_bytes(self.state[..8].try_into().unwrap())) + fn num_bits(&self) -> u32 { + self.0.num_bits() } - fn state(&self) -> [u8; 32] { - self.state + fn from_scalar_challenge_bytes(bytes: &[u8]) -> Self { + Self(Prime64Offset59::from_scalar_challenge_bytes(bytes)) } } +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/external-field"); +const SESSION: &[u8] = b"external-field"; + struct LinearRound; impl ProveRounds for LinearRound { @@ -230,22 +233,16 @@ impl ProveRounds for LinearRound { ) -> Result, SumcheckError> { assert!(bind.is_none()); assert_eq!(round, 0); - let polynomial = field_only_polynomial([ - ExternalField::from_u64(3), - ExternalField::from_u64(2), - ]); + let polynomial = + field_only_polynomial([ExternalField::from_u64(3), ExternalField::from_u64(2)]); assert_eq!( - polynomial.evaluate(ExternalField::zero()) - + polynomial.evaluate(ExternalField::one()), + polynomial.evaluate(ExternalField::zero()) + polynomial.evaluate(ExternalField::one()), previous_claim ); Ok(polynomial) } - fn finish_rounds( - &mut self, - _bind: ExternalField, - ) -> Result<(), SumcheckError> { + fn finish_rounds(&mut self, _bind: ExternalField) -> Result<(), SumcheckError> { Ok(()) } } @@ -259,10 +256,10 @@ fn field_only_polynomial(coefficients: [F; 2]) -> UnivariatePoly { #[test] fn external_field_runs_stock_clear_prover_and_verifier() { let input_claim = ExternalField::from_u64(8); - let mut prover_transcript = ExternalTranscript::new(b"external-field"); + let mut prover_transcript = ProverTranscript::::new(&PROTOCOL, SESSION); let mut recorder = ClearSumcheckRecorder::::new(); recorder.absorb_input_claims(&[input_claim], &mut prover_transcript); - let coefficient = prover_transcript.challenge_scalar(); + let coefficient: ExternalField = prover_transcript.challenge_small(); let prelude = BatchPrelude::try_new( vec![BatchMember { input_claim, @@ -284,31 +281,27 @@ fn external_field_runs_stock_clear_prover_and_verifier() { &mut prover_transcript, ) .unwrap(); - let recorded = recorder + recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = recorded.proof else { - panic!("clear recorder returned a non-compressed proof") - }; + let prover_state: [u8; 32] = prover_transcript.preview().squeeze(); + let proof = prover_transcript.finish(); - let mut verifier_transcript = ExternalTranscript::new(b"external-field"); - verifier_transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, &input_claim); - let verifier_coefficient = verifier_transcript.challenge_scalar(); + let mut verifier_transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, &proof); + verifier_transcript.public(&input_claim); + let verifier_coefficient: ExternalField = verifier_transcript.challenge_small(); let reduced = SumcheckVerifier::verify_compressed( &SumcheckClaim::new(1, 1, verifier_coefficient * input_claim), - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, &mut verifier_transcript, ) .unwrap(); - for claim in &proved.member_claims { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, claim); - } + let opening_claims: Vec = verifier_transcript.receive_n(1).unwrap(); + assert_eq!(verifier_transcript.preview().squeeze::<32>(), prover_state); + verifier_transcript.finish().unwrap(); let point = reduced.point.as_slice()[0]; let expected = ExternalField::from_u64(3) + ExternalField::from_u64(2) * point; + assert_eq!(opening_claims, vec![expected]); assert_eq!(proved.member_claims, vec![expected]); assert_eq!(reduced.value, verifier_coefficient * expected); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); } diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index b041713177..eba3e91826 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -18,10 +18,13 @@ use jolt_field::{ AdditiveGroup, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator, }; +use jolt_poly::UnivariatePoly; use jolt_sumcheck::{ - BooleanHypercube, ClearRound, EvaluationClaim, RoundMessage, SumcheckClaim, SumcheckVerifier, + send_full_round, BooleanHypercube, EvaluationClaim, SumcheckClaim, SumcheckVerifier, +}; +use jolt_transcript::{ + Blake2b512, Channel, Keccak, ProtocolId, ProverTranscript, Sponge, VerifierTranscript, }; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, KeccakTranscript, Transcript}; use num_traits::{One, Zero}; const MODULUS: u64 = (1u64 << 61) - 1; @@ -295,81 +298,34 @@ impl WithAccumulator for Mersenne61 { type SignedProductAccumulator = NaiveAccumulator; } -#[derive(Clone, Debug)] -struct LinearRound { - coeffs: [Mersenne61; 2], -} - -impl RoundMessage for LinearRound { - fn degree(&self) -> usize { - 1 - } - - fn append_to_transcript(&self, transcript: &mut T) { - self.coeffs[0].append_to_transcript(transcript); - self.coeffs[1].append_to_transcript(transcript); - } -} - -impl ClearRound for LinearRound { - fn evaluate(&self, challenge: Mersenne61) -> Mersenne61 { - self.coeffs[0] + self.coeffs[1] * challenge - } - - fn coefficient_linear_combination(&self, coefficients: &[Mersenne61]) -> Mersenne61 { - self.coeffs - .iter() - .zip(coefficients) - .map(|(&coefficient, &scale)| coefficient * scale) - .sum() - } -} +/// Proves four degree-1 rounds of the claim 10 under sponge `H` (each round +/// splits its running sum as `3s + (s - 6s) X`), then verifies them. +fn roundtrip() { + let protocol = ProtocolId::new::("jolt-sumcheck/tests/mersenne61"); + let claim = SumcheckClaim::new(4, 1, Mersenne61::from_u64(10)); -fn build_rounds() -> ( - SumcheckClaim, - Vec, - EvaluationClaim, -) { - let mut transcript = Blake2bTranscript::::new(b"mersenne61"); - let mut running_sum = Mersenne61::from_u64(10); + let mut prover = ProverTranscript::::new(&protocol, b"mersenne61"); + let mut running_sum = claim.claimed_sum; let mut point = Vec::new(); - let mut rounds = Vec::new(); - - for _ in 0..4 { + for _ in 0..claim.num_vars { let c0 = running_sum * Mersenne61::from_u64(3); - let c1 = running_sum - c0 - c0; - let round = LinearRound { coeffs: [c0, c1] }; - round.append_to_transcript(&mut transcript); - let r = transcript.challenge(); + let round = UnivariatePoly::new(vec![c0, running_sum - c0 - c0]); + send_full_round(&round, claim.degree, &mut prover).unwrap(); + let r: Mersenne61 = prover.challenge_small(); running_sum = round.evaluate(r); point.push(r); - rounds.push(round); } + let narg = prover.finish(); + assert_eq!(narg.len(), claim.num_vars * 2 * Mersenne61::NUM_BYTES); - ( - SumcheckClaim::new(4, 1, Mersenne61::from_u64(10)), - rounds, - EvaluationClaim::new(point, running_sum), - ) -} - -#[test] -fn hash_transcripts_accept_mersenne61_without_bn254_field_surface() { - let mut blake = Blake2bTranscript::::new(b"compat"); - let mut keccak = KeccakTranscript::::new(b"compat"); - Mersenne61::from_u64(42).append_to_transcript(&mut blake); - Mersenne61::from_u64(42).append_to_transcript(&mut keccak); - - let _: Mersenne61 = blake.challenge(); - let _: Mersenne61 = keccak.challenge(); + let mut verifier = VerifierTranscript::::new(&protocol, b"mersenne61", &narg); + let actual = SumcheckVerifier::verify(&claim, BooleanHypercube, &mut verifier).unwrap(); + verifier.finish().unwrap(); + assert_eq!(actual, EvaluationClaim::new(point, running_sum)); } #[test] -fn sumcheck_verifier_accepts_mersenne61_round_proof() { - let (claim, rounds, expected) = build_rounds(); - let mut verifier_transcript = Blake2bTranscript::::new(b"mersenne61"); - let actual = - SumcheckVerifier::verify(&claim, &rounds, BooleanHypercube, &mut verifier_transcript) - .unwrap(); - assert_eq!(actual, expected); +fn sumcheck_roundtrips_mersenne61_under_blake2b_and_keccak() { + roundtrip::(); + roundtrip::(); } diff --git a/crates/jolt-sumcheck/tests/roundtrip.rs b/crates/jolt-sumcheck/tests/roundtrip.rs index 768f5fcd44..e892b20cd2 100644 --- a/crates/jolt-sumcheck/tests/roundtrip.rs +++ b/crates/jolt-sumcheck/tests/roundtrip.rs @@ -1,84 +1,74 @@ //! Integration tests: full prover-verifier roundtrips with product compositions. //! -//! These tests construct honest sumcheck proofs for polynomials of varying -//! degree and verify them, exercising the complete protocol flow including -//! transcript synchronization. +//! An honest prover writes each round into a `ProverTranscript`, in full or +//! compressed form, and the verifier reads the resulting proof back; the +//! reduced claim must match the product of the factors at the challenge point. -#![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] +#![expect( + clippy::unwrap_used, + clippy::indexing_slicing, + reason = "tests may panic on assertion failures and index fixture data" +)] use jolt_field::{Fr, Ring}; -use jolt_poly::{Polynomial, UnivariatePoly}; -use jolt_sumcheck::claim::{EvaluationClaim, SumcheckClaim}; -use jolt_sumcheck::proof::ClearSumcheckProof; -use jolt_sumcheck::round_proof::{CompressedLabeledRoundPoly, LabeledRoundPoly, RoundMessage}; -use jolt_sumcheck::{BooleanHypercube, SumcheckVerifier, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_poly::{EqPolynomial, Polynomial, UnivariatePoly}; +use jolt_sumcheck::{ + send_compressed_round, send_full_round, BooleanHypercube, EvaluationClaim, SumcheckClaim, + SumcheckVerifier, +}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; type F = Fr; -/// Prove a sumcheck for the product of `polys` multilinear polynomials. -/// -/// Given d multilinear polynomials over n variables, proves the claim -/// `sum_{x in {0,1}^n} prod_j f_j(x) = C`. The round polynomial in -/// round i is degree d, requiring d+1 evaluation points. -/// -/// Returns (proof, claimed_sum). +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/tests/roundtrip"); +const SESSION: &[u8] = b"sumcheck-roundtrip"; + +#[derive(Clone, Copy)] +enum Wire { + Full, + Compressed, +} + +/// Proves `sum_{x in {0,1}^n} prod_j f_j(x)` for multilinear `f_j` (HighToLow +/// binding) and returns the proof, the claim, and the prover's final sponge +/// fingerprint. fn prove_product( polys: &[Vec], num_vars: usize, - transcript: &mut Blake2bTranscript, -) -> (ClearSumcheckProof, F) { + wire: Wire, +) -> (Vec, SumcheckClaim, [u8; 32]) { let degree = polys.len(); let n = 1 << num_vars; assert!(polys.iter().all(|p| p.len() == n)); - - // Compute claimed sum let claimed_sum: F = (0..n) .map(|i| polys.iter().map(|p| p[i]).product::()) .sum(); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); let mut bufs: Vec> = polys.to_vec(); - let mut round_polys = Vec::with_capacity(num_vars); - for _round in 0..num_vars { let half = bufs[0].len() / 2; - - // Evaluate the round polynomial at points 0, 1, ..., degree. - // At point t, for each pair (lo, hi): - // f_j(t) = lo + t*(hi - lo) - // The round poly value is: sum_i prod_j f_j(t) - let evals: Vec = (0..=degree) + let points: Vec<(F, F)> = (0..=degree) .map(|t| { let ft = F::from_u64(t as u64); - let mut sum = F::from_u64(0); - for i in 0..half { - let mut prod = F::from_u64(1); - for buf in &bufs { - let lo = buf[i]; - let hi = buf[i + half]; - prod *= lo + ft * (hi - lo); - } - sum += prod; - } - sum + let value = (0..half) + .map(|i| { + bufs.iter() + .map(|buf| buf[i] + ft * (buf[i + half] - buf[i])) + .product::() + }) + .sum(); + (ft, value) }) .collect(); - - // Interpolate to get the degree-d round polynomial in coefficient form - let points: Vec<(F, F)> = evals - .iter() - .enumerate() - .map(|(i, &v)| (F::from_u64(i as u64), v)) - .collect(); let round_poly = UnivariatePoly::interpolate(&points); + match wire { + Wire::Full => send_full_round(&round_poly, degree, &mut transcript), + Wire::Compressed => send_compressed_round(&round_poly, degree, &mut transcript), + } + .unwrap(); + let r: F = transcript.challenge_small(); - // Absorb through the same path the unlabelled verifier uses. - as RoundMessage>::append_to_transcript(&round_poly, transcript); - - let r: F = transcript.challenge(); - round_polys.push(round_poly); - - // Bind all polynomials (HighToLow) for buf in &mut bufs { for i in 0..half { buf[i] = buf[i] + r * (buf[i + half] - buf[i]); @@ -87,331 +77,90 @@ fn prove_product( } } + let fingerprint = transcript.preview().squeeze(); ( - ClearSumcheckProof { - round_polynomials: round_polys, - }, - claimed_sum, + transcript.finish(), + SumcheckClaim::new(num_vars, degree, claimed_sum), + fingerprint, ) } -#[test] -fn degree2_product_roundtrip() { - // f(x) * g(x) where f, g are multilinear over 4 variables - let num_vars = 4; - let n = 1 << num_vars; +/// Proves and verifies the product of `polys` in `wire` form, checking the +/// reduced claim against the factors' evaluations at the challenge point. +fn assert_product_roundtrip(polys: &[Vec], num_vars: usize, wire: Wire) { + let (narg, claim, prover_state) = prove_product(polys, num_vars, wire); - let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let g: Vec = (0..n).map(|i| F::from_u64((i * 3 + 7) as u64)).collect(); - - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let (proof, claimed_sum) = prove_product(&[f, g], num_vars, &mut pt); + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); + let EvaluationClaim { point, value } = match wire { + Wire::Full => SumcheckVerifier::verify(&claim, BooleanHypercube, &mut transcript), + Wire::Compressed => SumcheckVerifier::verify_compressed(&claim, &mut transcript), + } + .unwrap(); + assert_eq!(transcript.preview().squeeze::<32>(), prover_state); + transcript.finish().unwrap(); - let claim = SumcheckClaim { - num_vars, - degree: 2, - claimed_sum, - }; + assert_eq!(point.len(), num_vars); + let expected: F = polys + .iter() + .map(|p| Polynomial::new(p.clone()).evaluate_and_consume(&point)) + .product(); + assert_eq!(value, expected); +} - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!(result.is_ok(), "degree-2 verify failed: {:?}", result.err()); +fn table(num_vars: usize, map: impl Fn(u64) -> u64) -> Vec { + (0..1u64 << num_vars).map(|i| F::from_u64(map(i))).collect() } #[test] -fn degree3_product_roundtrip() { - // f(x) * g(x) * h(x), degree 3, 3 variables - let num_vars = 3; - let n = 1 << num_vars; - - let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let g: Vec = (0..n).map(|i| F::from_u64((i * 2 + 3) as u64)).collect(); - let h: Vec = (0..n).map(|i| F::from_u64((i + 10) as u64)).collect(); - - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let (proof, claimed_sum) = prove_product(&[f, g, h], num_vars, &mut pt); - - let claim = SumcheckClaim { - num_vars, - degree: 3, - claimed_sum, - }; +fn degree1_roundtrip() { + for num_vars in [1, 3] { + for wire in [Wire::Full, Wire::Compressed] { + assert_product_roundtrip(&[table(num_vars, |i| i + 1)], num_vars, wire); + } + } +} - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!(result.is_ok(), "degree-3 verify failed: {:?}", result.err()); +#[test] +fn degree2_product_roundtrip() { + let num_vars = 4; + let polys = [table(num_vars, |i| i + 1), table(num_vars, |i| i * 3 + 7)]; + for wire in [Wire::Full, Wire::Compressed] { + assert_product_roundtrip(&polys, num_vars, wire); + } } #[test] -fn degree3_final_eval_correct() { - // Verify that the final eval matches the product of individual evals at the point +fn degree3_product_roundtrip() { let num_vars = 3; - let n = 1 << num_vars; - - let f_evals: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let g_evals: Vec = (0..n).map(|i| F::from_u64((i * 5 + 2) as u64)).collect(); - let h_evals: Vec = (0..n).map(|i| F::from_u64((i + 7) as u64)).collect(); - - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let (proof, claimed_sum) = prove_product( - &[f_evals.clone(), g_evals.clone(), h_evals.clone()], - num_vars, - &mut pt, - ); - - let claim = SumcheckClaim { - num_vars, - degree: 3, - claimed_sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let EvaluationClaim { - point: challenges, - value: final_eval, - } = SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt) - .unwrap(); - - // f(r) * g(r) * h(r) should equal the final_eval - let f_at_r = Polynomial::new(f_evals).evaluate_and_consume(&challenges); - let g_at_r = Polynomial::new(g_evals).evaluate_and_consume(&challenges); - let h_at_r = Polynomial::new(h_evals).evaluate_and_consume(&challenges); - assert_eq!(final_eval, f_at_r * g_at_r * h_at_r); + let polys = [ + table(num_vars, |i| i + 1), + table(num_vars, |i| i * 5 + 2), + table(num_vars, |i| i + 7), + ]; + for wire in [Wire::Full, Wire::Compressed] { + assert_product_roundtrip(&polys, num_vars, wire); + } } #[test] fn eq_weighted_sumcheck() { - // eq(r, x) * f(x), common pattern in Jolt (Spartan outer sumcheck). - // eq(r, x) = prod_i (r_i * x_i + (1 - r_i)(1 - x_i)) + // eq(r, x) * f(x), the Spartan outer-sumcheck shape. let num_vars = 4; - let n = 1 << num_vars; - - let f: Vec = (0..n).map(|i| F::from_u64(i as u64 * 3 + 1)).collect(); - - // Generate a random-ish point r for the eq polynomial let r: Vec = (0..num_vars) .map(|i| F::from_u64(i as u64 * 7 + 13)) .collect(); - - // Compute eq(r, x) for all x in {0,1}^n - let eq_evals = jolt_poly::EqPolynomial::evals::(&r, None); - - // Product: eq(r, x) * f(x) - let product: Vec = eq_evals.iter().zip(&f).map(|(&e, &fi)| e * fi).collect(); - - let claimed_sum: F = product.iter().copied().sum(); - - // Prove as degree-2 (eq * f, both multilinear) - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let (proof, sum) = prove_product(&[eq_evals, f], num_vars, &mut pt); - assert_eq!(sum, claimed_sum); - - let claim = SumcheckClaim { - num_vars, - degree: 2, - claimed_sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!( - result.is_ok(), - "eq-weighted verify failed: {:?}", - result.err() - ); + let polys = [ + EqPolynomial::evals::(&r, None), + table(num_vars, |i| i * 3 + 1), + ]; + for wire in [Wire::Full, Wire::Compressed] { + assert_product_roundtrip(&polys, num_vars, wire); + } } #[test] fn large_num_vars_roundtrip() { - // Stress test with 10 variables (1024 evaluations), degree 2 let num_vars = 10; - let n = 1 << num_vars; - - let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let g: Vec = (0..n).map(|i| F::from_u64((i * 7 + 3) as u64)).collect(); - - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let (proof, claimed_sum) = prove_product(&[f, g], num_vars, &mut pt); - - let claim = SumcheckClaim { - num_vars, - degree: 2, - claimed_sum, - }; - - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!(result.is_ok(), "large roundtrip failed: {:?}", result.err()); -} - -#[test] -fn compressed_round_verifier_roundtrip() { - // Full prover-verifier roundtrip where both the prover and the verifier - // absorb through `CompressedLabeledRoundPoly` — the wrapper is the - // single source of truth for the compressed wire format. - let num_vars = 3; - let n = 1 << num_vars; - let label = SUMCHECK_ROUND_TRANSCRIPT_LABEL; - let degree = 2; - - let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let g: Vec = (0..n).map(|i| F::from_u64(i as u64 * 2 + 3)).collect(); - - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let mut bufs = vec![f.clone(), g.clone()]; - let claimed_sum: F = (0..n).map(|i| bufs[0][i] * bufs[1][i]).sum(); - let mut round_polys = Vec::with_capacity(num_vars); - - for _round in 0..num_vars { - let half = bufs[0].len() / 2; - let evals: Vec = (0..=degree) - .map(|t| { - let ft = F::from_u64(t as u64); - let mut sum = F::from_u64(0); - for i in 0..half { - let mut prod = F::from_u64(1); - for buf in &bufs { - let lo = buf[i]; - let hi = buf[i + half]; - prod *= lo + ft * (hi - lo); - } - sum += prod; - } - sum - }) - .collect(); - - let points: Vec<(F, F)> = evals - .iter() - .enumerate() - .map(|(i, &v)| (F::from_u64(i as u64), v)) - .collect(); - let round_poly = UnivariatePoly::interpolate(&points); - - let compressed = CompressedLabeledRoundPoly::new(&round_poly, label); - as RoundMessage>::append_to_transcript( - &compressed, - &mut pt, - ); - - let r: F = pt.challenge(); - round_polys.push(round_poly); - - for buf in &mut bufs { - for i in 0..half { - buf[i] = buf[i] + r * (buf[i + half] - buf[i]); - } - buf.truncate(half); - } - } - - let proof = ClearSumcheckProof { - round_polynomials: round_polys, - }; - let claim = SumcheckClaim { - num_vars, - degree, - claimed_sum, - }; - - let wrapped: Vec> = proof - .round_polynomials - .iter() - .map(|p| CompressedLabeledRoundPoly::new(p, label)) - .collect(); - - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &wrapped, BooleanHypercube, &mut vt); - assert!( - result.is_ok(), - "compressed round verifier roundtrip failed: {:?}", - result.err() - ); -} - -#[test] -fn labeled_round_verifier_roundtrip() { - // Test the labeled round verifier path (used by jolt-verifier) - let num_vars = 3; - let n = 1 << num_vars; - - let f: Vec = (0..n).map(|i| F::from_u64(i as u64 + 1)).collect(); - let g: Vec = (0..n).map(|i| F::from_u64((i + 5) as u64)).collect(); - - let label = SUMCHECK_ROUND_TRANSCRIPT_LABEL; - - // Prove with labeled absorption - let mut pt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let degree = 2; - let mut bufs = vec![f.clone(), g.clone()]; - let claimed_sum: F = (0..n).map(|i| bufs[0][i] * bufs[1][i]).sum(); - let mut round_polys = Vec::new(); - - for _round in 0..num_vars { - let half = bufs[0].len() / 2; - let evals: Vec = (0..=degree) - .map(|t| { - let ft = F::from_u64(t as u64); - let mut sum = F::from_u64(0); - for i in 0..half { - let mut prod = F::from_u64(1); - for buf in &bufs { - let lo = buf[i]; - let hi = buf[i + half]; - prod *= lo + ft * (hi - lo); - } - sum += prod; - } - sum - }) - .collect(); - - let points: Vec<(F, F)> = evals - .iter() - .enumerate() - .map(|(i, &v)| (F::from_u64(i as u64), v)) - .collect(); - let round_poly = UnivariatePoly::interpolate(&points); - - let labeled = LabeledRoundPoly::new(&round_poly, label); - as RoundMessage>::append_to_transcript(&labeled, &mut pt); - - let r: F = pt.challenge(); - round_polys.push(round_poly); - - for buf in &mut bufs { - for i in 0..half { - buf[i] = buf[i] + r * (buf[i + half] - buf[i]); - } - buf.truncate(half); - } - } - - let proof = ClearSumcheckProof { - round_polynomials: round_polys, - }; - - let claim = SumcheckClaim { - num_vars, - degree, - claimed_sum, - }; - - let wrapped: Vec> = proof - .round_polynomials - .iter() - .map(|p| LabeledRoundPoly::new(p, label)) - .collect(); - - let mut vt = Blake2bTranscript::new(b"sumcheck-roundtrip"); - let result = SumcheckVerifier::verify(&claim, &wrapped, BooleanHypercube, &mut vt); - assert!( - result.is_ok(), - "labeled round verifier roundtrip failed: {:?}", - result.err() - ); + let polys = [table(num_vars, |i| i + 1), table(num_vars, |i| i * 7 + 3)]; + assert_product_roundtrip(&polys, num_vars, Wire::Compressed); } diff --git a/crates/jolt-sumcheck/tests/solinas_fields.rs b/crates/jolt-sumcheck/tests/solinas_fields.rs new file mode 100644 index 0000000000..bac7cee8ed --- /dev/null +++ b/crates/jolt-sumcheck/tests/solinas_fields.rs @@ -0,0 +1,224 @@ +//! The stock clear prover and verifier over non-BN254 prime fields, through +//! the NARG transcript. + +#![expect( + clippy::unwrap_used, + clippy::indexing_slicing, + reason = "tests may panic on assertion failures and index fixture data" +)] + +use jolt_field::{CanonicalEncoding, Field, Prime128Offset275, Prime32Offset99, Prime64Offset59}; +use jolt_poly::UnivariatePoly; +use jolt_sumcheck::{ + prove_batch, prove_uniskip_clear, BatchMember, BatchPrelude, CenteredIntegerDomain, + ClearSumcheckRecorder, ProveRounds, SequentialRounds, SumcheckClaim, SumcheckError, + SumcheckRecorder, SumcheckVerifier, +}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; + +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/tests/fields"); + +struct ProductOfAffines { + num_rounds: usize, + left: Vec, + right: Vec, +} + +impl ProductOfAffines { + fn new(left: Vec, right: Vec) -> Self { + assert_eq!(left.len(), right.len()); + assert!(left.len().is_power_of_two()); + Self { + num_rounds: left.len().ilog2() as usize, + left, + right, + } + } + + fn bind(table: &mut Vec, challenge: F) { + for index in 0..table.len() / 2 { + let low = table[2 * index]; + let high = table[2 * index + 1]; + table[index] = low + challenge * (high - low); + } + table.truncate(table.len() / 2); + } +} + +impl ProveRounds for ProductOfAffines { + fn num_rounds(&self) -> usize { + self.num_rounds + } + + fn prove_round( + &mut self, + bind: Option, + _round: usize, + previous_claim: F, + ) -> Result, SumcheckError> { + if let Some(challenge) = bind { + Self::bind(&mut self.left, challenge); + Self::bind(&mut self.right, challenge); + } + + let mut coefficients = [F::zero(); 3]; + for (left, right) in self.left.chunks_exact(2).zip(self.right.chunks_exact(2)) { + let left_delta = left[1] - left[0]; + let right_delta = right[1] - right[0]; + coefficients[0] += left[0] * right[0]; + coefficients[1] += left[0] * right_delta + left_delta * right[0]; + coefficients[2] += left_delta * right_delta; + } + let polynomial = UnivariatePoly::new(coefficients.to_vec()); + assert_eq!( + polynomial.evaluate(F::zero()) + polynomial.evaluate(F::one()), + previous_claim + ); + Ok(polynomial) + } + + fn finish_rounds(&mut self, bind: F) -> Result<(), SumcheckError> { + Self::bind(&mut self.left, bind); + Self::bind(&mut self.right, bind); + Ok(()) + } +} + +/// `c0 + c1 x0 + c2 x1`; the member binds `x0` (the low bit) first. +fn affine(coefficients: [F; 3], point: [F; 2]) -> F { + coefficients[0] + coefficients[1] * point[0] + coefficients[2] * point[1] +} + +fn affine_table(coefficients: [F; 3]) -> Vec { + [ + [F::zero(), F::zero()], + [F::one(), F::zero()], + [F::zero(), F::one()], + [F::one(), F::one()], + ] + .into_iter() + .map(|point| affine(coefficients, point)) + .collect() +} + +fn run_batched_roundtrip( + left_coefficients: [F; 3], + right_coefficients: [F; 3], +) { + let left = affine_table(left_coefficients); + let right = affine_table(right_coefficients); + let input_claim: F = left.iter().zip(&right).map(|(&a, &b)| a * b).sum(); + + let mut prover_transcript = ProverTranscript::::new(&PROTOCOL, b"batched"); + let mut recorder = ClearSumcheckRecorder::::new(); + recorder.absorb_input_claims(&[input_claim], &mut prover_transcript); + let coefficient: F = prover_transcript.challenge_small(); + let prelude = BatchPrelude::try_new( + vec![BatchMember { + input_claim, + coefficient, + rounds: 2, + offset: 0, + }], + 2, + 2, + ) + .unwrap(); + let mut product = ProductOfAffines::new(left, right); + let mut members: [&mut dyn ProveRounds; 1] = [&mut product]; + let proved = prove_batch( + &prelude, + &mut members, + &mut SequentialRounds, + &mut recorder, + &mut prover_transcript, + ) + .unwrap(); + recorder + .finish(&proved.member_claims, &mut prover_transcript) + .unwrap(); + let prover_state: [u8; 32] = prover_transcript.preview().squeeze(); + let narg = prover_transcript.finish(); + assert_eq!(narg.len(), (2 * 2 + 1) * F::NUM_BYTES); + + let mut verifier_transcript = + VerifierTranscript::::new(&PROTOCOL, b"batched", &narg); + verifier_transcript.public(&input_claim); + let verifier_coefficient: F = verifier_transcript.challenge_small(); + assert_eq!(verifier_coefficient, coefficient); + let reduced = SumcheckVerifier::verify_compressed( + &SumcheckClaim::new(2, 2, coefficient * input_claim), + &mut verifier_transcript, + ) + .unwrap(); + let member_claim: F = verifier_transcript.receive().unwrap(); + + let point: [F; 2] = reduced.point.as_slice().try_into().unwrap(); + let expected = affine(left_coefficients, point) * affine(right_coefficients, point); + assert_eq!(member_claim, expected); + assert_eq!(proved.member_claims, vec![expected]); + assert_eq!(reduced.value, coefficient * expected); + assert_eq!(proved.final_claim, reduced.value); + assert_eq!(verifier_transcript.preview().squeeze::<32>(), prover_state); + verifier_transcript.finish().unwrap(); +} + +fn run_uniskip_roundtrip(coefficients: [F; 3]) { + let polynomial = UnivariatePoly::new(coefficients.to_vec()); + let input_claim = [-1, 0, 1] + .into_iter() + .map(|point| polynomial.evaluate(F::from_i64(point))) + .sum(); + let mut prover_transcript = ProverTranscript::::new(&PROTOCOL, b"uniskip"); + let proved = prove_uniskip_clear( + polynomial.clone(), + input_claim, + 2, + 3, + &mut prover_transcript, + ) + .unwrap(); + let prover_state: [u8; 32] = prover_transcript.preview().squeeze(); + let narg = prover_transcript.finish(); + + let mut verifier_transcript = + VerifierTranscript::::new(&PROTOCOL, b"uniskip", &narg); + let reduced = SumcheckVerifier::verify( + &SumcheckClaim::new(1, 2, input_claim), + CenteredIntegerDomain::new(3), + &mut verifier_transcript, + ) + .unwrap(); + let output_claim: F = verifier_transcript.receive().unwrap(); + + assert_eq!(reduced.point.as_slice(), &[proved.challenge]); + assert_eq!(reduced.value, polynomial.evaluate(proved.challenge)); + assert_eq!(output_claim, proved.output_claim); + assert_eq!(reduced.value, proved.output_claim); + assert_eq!(verifier_transcript.preview().squeeze::<32>(), prover_state); + verifier_transcript.finish().unwrap(); +} + +fn run_roundtrips() { + let value = |v: u64| F::from_u64(v); + run_batched_roundtrip( + [value(1), value(3), value(5)], + [value(7), value(9), value(11)], + ); + run_uniskip_roundtrip([value(2), value(5), value(11)]); +} + +#[test] +fn prime32_runs_prover_and_verifier() { + run_roundtrips::(); +} + +#[test] +fn prime64_runs_prover_and_verifier() { + run_roundtrips::(); +} + +#[test] +fn prime128_runs_prover_and_verifier() { + run_roundtrips::(); +} diff --git a/crates/jolt-sumcheck/tests/soundness.rs b/crates/jolt-sumcheck/tests/soundness.rs index 4e641b23b0..8786aeaea7 100644 --- a/crates/jolt-sumcheck/tests/soundness.rs +++ b/crates/jolt-sumcheck/tests/soundness.rs @@ -1,191 +1,107 @@ -//! Soundness tests: adversarial scenarios for sumcheck verification. +//! Soundness tests: adversarial proofs against sumcheck verification. //! -//! These tests probe whether a malicious prover can trick the verifier into -//! accepting an invalid claim. Each test targets a specific attack vector -//! against the sumcheck protocol. - -#![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] - -use jolt_field::{Fr, Ring}; -use jolt_poly::{CompressedPoly, Polynomial, UnivariatePoly}; -use jolt_sumcheck::claim::{EvaluationClaim, SumcheckClaim}; -use jolt_sumcheck::error::SumcheckError; -use jolt_sumcheck::proof::{ClearSumcheckProof, CompressedSumcheckProof}; -use jolt_sumcheck::round_proof::RoundMessage; -use jolt_sumcheck::{BooleanHypercube, SumcheckVerifier, SUMCHECK_ROUND_TRANSCRIPT_LABEL}; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, LabelWithCount, Transcript}; +//! A proof is the NARG byte string, so every attack is a byte-level edit of +//! an honest proof or a mismatched public statement. + +#![expect( + clippy::unwrap_used, + clippy::indexing_slicing, + reason = "tests may panic on assertion failures and index fixture data" +)] + +use jolt_field::{CanonicalBytes, CanonicalDecode, Fr, Ring}; +use jolt_poly::{Polynomial, UnivariatePoly}; +use jolt_sumcheck::{ + send_compressed_round, send_full_round, BooleanHypercube, EvaluationClaim, SumcheckClaim, + SumcheckError, SumcheckVerifier, +}; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, TranscriptError, VerifierTranscript, +}; type F = Fr; -#[derive(Debug)] -enum OracleCheckError { - #[expect( - dead_code, - reason = "inner error shown via Debug in test panic messages" - )] - Sumcheck(SumcheckError), - FinalEvalMismatch, -} - -impl From> for OracleCheckError { - fn from(err: SumcheckError) -> Self { - OracleCheckError::Sumcheck(err) - } -} - -fn new_transcript() -> Blake2bTranscript { - Blake2bTranscript::new(b"soundness-test") -} +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/tests/soundness"); +const SESSION: &[u8] = b"soundness-test"; +/// Bytes per degree-1 full round message. +const ROUND_BYTES: usize = 2 * F::NUM_BYTES; -/// Honest degree-1 sumcheck prover. -fn honest_prove( - evals: &[F], - num_vars: usize, - transcript: &mut Blake2bTranscript, -) -> ClearSumcheckProof { +/// Honest degree-1 prover over HighToLow-bound evaluations, full rounds. +fn honest_prove(evals: &[F]) -> Vec { + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); let mut buf = evals.to_vec(); - let mut round_polys = Vec::with_capacity(num_vars); - - for _round in 0..num_vars { + while buf.len() > 1 { let half = buf.len() / 2; - let mut eval_0 = F::from_u64(0); - let mut eval_1 = F::from_u64(0); - for i in 0..half { - eval_0 += buf[i]; - eval_1 += buf[i + half]; - } + let eval_0: F = buf[..half].iter().copied().sum(); + let eval_1: F = buf[half..].iter().copied().sum(); let round_poly = UnivariatePoly::new(vec![eval_0, eval_1 - eval_0]); - as RoundMessage>::append_to_transcript(&round_poly, transcript); - let r: F = transcript.challenge(); - round_polys.push(round_poly); + send_full_round(&round_poly, 1, &mut transcript).unwrap(); + let r: F = transcript.challenge_small(); for i in 0..half { buf[i] = buf[i] + r * (buf[i + half] - buf[i]); } buf.truncate(half); } + transcript.finish() +} - ClearSumcheckProof { - round_polynomials: round_polys, - } +fn claim(num_vars: usize, claimed_sum: F) -> SumcheckClaim { + SumcheckClaim::new(num_vars, 1, claimed_sum) } -fn compute_sum(evals: &[F]) -> F { +fn sum(evals: &[F]) -> F { evals.iter().copied().sum() } -/// Full verification pipeline: sumcheck round checks + oracle evaluation check. -/// -/// Returns the challenge vector on success. Returns -/// `OracleCheckError::FinalEvalMismatch` if the proof passes all round checks -/// but the final evaluation doesn't match the intended polynomial. -fn verify_with_oracle_check( - claim: &SumcheckClaim, - proof: &ClearSumcheckProof, - intended_evals: &[F], -) -> Result, OracleCheckError> { - let mut transcript = new_transcript(); - let EvaluationClaim { - point: challenges, - value: final_eval, - } = SumcheckVerifier::verify( - claim, - &proof.round_polynomials, - BooleanHypercube, - &mut transcript, - )?; - - let expected = Polynomial::new(intended_evals.to_vec()).evaluate_and_consume(&challenges); - if final_eval != expected { - return Err(OracleCheckError::FinalEvalMismatch); - } +/// Runs the verifier over the whole proof, including the trailing-bytes check. +fn verify(claim: &SumcheckClaim, narg: &[u8]) -> Result, SumcheckError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let reduced = SumcheckVerifier::verify(claim, BooleanHypercube, &mut transcript)?; + transcript.finish()?; + Ok(reduced) +} - Ok(challenges.into_vec()) +fn oracle_accepts(reduced: &EvaluationClaim, intended_evals: &[F]) -> bool { + reduced.value == Polynomial::new(intended_evals.to_vec()).evaluate_and_consume(&reduced.point) } #[test] -fn honest_proof_passes_oracle_check() { - let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = new_transcript(); - let proof = honest_prove(&evals, 3, &mut pt); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - let result = verify_with_oracle_check(&claim, &proof, &evals); - assert!(result.is_ok()); +fn honest_proofs_pass_the_oracle_check() { + let tables: [Vec; 3] = [ + (1..=8).map(F::from_u64).collect(), + vec![F::from_u64(0); 8], + vec![F::from_u64(7); 8], + ]; + for evals in &tables { + let reduced = verify(&claim(3, sum(evals)), &honest_prove(evals)).unwrap(); + assert_eq!(reduced.point.len(), 3); + assert!(oracle_accepts(&reduced, evals)); + } + // A constant table reduces to the constant at every point. + let reduced = verify(&claim(3, sum(&tables[2])), &honest_prove(&tables[2])).unwrap(); + assert_eq!(reduced.value, F::from_u64(7)); } #[test] fn wrong_polynomial_same_sum_fails_oracle_check() { - // f and g have the same sum but are different polynomials. - // An honest proof for g will pass all round checks when verified against - // claimed_sum = sum(g), but the final evaluation will correspond to g(r), - // not f(r). The oracle check catches this. + // An honest proof for g passes every round check against sum(g) = sum(f); + // only the oracle check against f catches the substitution. let f_evals: Vec = (1..=8).map(F::from_u64).collect(); let g_evals: Vec = (1..=8).rev().map(F::from_u64).collect(); + assert_eq!(sum(&f_evals), sum(&g_evals)); - let sum_f = compute_sum(&f_evals); - let sum_g = compute_sum(&g_evals); - assert_eq!(sum_f, sum_g, "precondition: f and g must have equal sums"); - - // Construct honest proof for g - let mut pt = new_transcript(); - let proof = honest_prove(&g_evals, 3, &mut pt); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum_g, - }; - - // Round checks pass (proof is internally consistent for g) - let mut vt = new_transcript(); - let round_result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!( - round_result.is_ok(), - "round checks should pass for honest g proof" - ); - - // But oracle check against f fails — the final eval is g(r), not f(r) - let result = verify_with_oracle_check(&claim, &proof, &f_evals); - assert!( - matches!(result, Err(OracleCheckError::FinalEvalMismatch)), - "oracle check must catch polynomial substitution: {:?}", - result - ); + let reduced = verify(&claim(3, sum(&g_evals)), &honest_prove(&g_evals)).unwrap(); + assert!(oracle_accepts(&reduced, &g_evals)); + assert!(!oracle_accepts(&reduced, &f_evals)); } #[test] -fn proof_for_different_polynomial_different_sum_fails_round_check() { - // Construct an honest proof for g, but claim it proves f (different sum). - // The round check at round 0 must fail because the proof's s_0(0) + s_0(1) - // was computed for sum(g), not sum(f). - let f_evals: Vec = (1..=8).map(F::from_u64).collect(); - let g_evals: Vec = (10..=17).map(F::from_u64).collect(); - - let sum_f = compute_sum(&f_evals); - let sum_g = compute_sum(&g_evals); - assert_ne!(sum_f, sum_g); - - let mut pt = new_transcript(); - let proof = honest_prove(&g_evals, 3, &mut pt); - - // Claim sum(f) but provide proof for g - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum_f, - }; - - let mut vt = new_transcript(); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); +fn wrong_claimed_sum_fails_first_round_check() { + let evals: Vec = (10..=17).map(F::from_u64).collect(); + let result = verify( + &claim(3, sum(&evals) + F::from_u64(1)), + &honest_prove(&evals), + ); assert!(matches!( result, Err(SumcheckError::RoundCheckFailed { round: 0, .. }) @@ -193,397 +109,211 @@ fn proof_for_different_polynomial_different_sum_fails_round_check() { } #[test] -fn corrupted_middle_round_detected() { - let evals: Vec = (1..=16).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = new_transcript(); - let mut proof = honest_prove(&evals, 4, &mut pt); - - // Corrupt round 2 (middle round): replace with arbitrary polynomial - // that has the same degree but wrong s(0) + s(1). - proof.round_polynomials[2] = UnivariatePoly::new(vec![F::from_u64(999), F::from_u64(1)]); - - let claim = SumcheckClaim { - num_vars: 4, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = new_transcript(); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - - // Corruption at round 2 may be detected at round 2 (wrong sum) or later - // (transcript desync from corrupted absorption). Either way, it must fail. - assert!(result.is_err(), "corrupted middle round must be rejected"); -} - -#[test] -fn corrupted_last_round_detected() { +fn every_byte_flip_is_rejected_at_its_round() { + // A full degree-1 round is checked through 2*c0 + c1, so any change to + // either coefficient fails its own round, unless the edit already breaks + // the canonical encoding. let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = new_transcript(); - let mut proof = honest_prove(&evals, 3, &mut pt); - - // Corrupt only the last round polynomial - proof.round_polynomials[2] = UnivariatePoly::new(vec![F::from_u64(0), F::from_u64(0)]); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = new_transcript(); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - assert!(result.is_err(), "corrupted last round must be rejected"); + let claim = claim(3, sum(&evals)); + let narg = honest_prove(&evals); + assert_eq!(narg.len(), 3 * ROUND_BYTES); + + for index in 0..narg.len() { + for mask in [0x01, 0x80] { + let mut tampered = narg.clone(); + tampered[index] ^= mask; + let result = verify(&claim, &tampered); + let round = index / ROUND_BYTES; + assert!( + matches!( + result, + Err(SumcheckError::RoundCheckFailed { round: r, .. }) if r == round + ) || matches!( + result, + Err(SumcheckError::Transcript(TranscriptError::NonCanonical)) + ), + "flip {mask:#04x} at byte {index}: {result:?}" + ); + } + } } #[test] -fn swapped_round_order_rejected() { - // Swap rounds 0 and 1. The proof is no longer internally consistent because - // each round's polynomial depends on the previous challenge, and swapping - // changes the Fiat-Shamir transcript. +fn non_canonical_coefficient_is_rejected() { let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = new_transcript(); - let mut proof = honest_prove(&evals, 3, &mut pt); - - proof.round_polynomials.swap(0, 1); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = new_transcript(); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - - // Round 0 now has the wrong s(0)+s(1) (it was computed for a different running sum). - // Even if by accident s(0)+s(1) matched, the transcript would desync. - assert!(result.is_err(), "swapped round order must be rejected"); + let mut narg = honest_prove(&evals); + narg[ROUND_BYTES..ROUND_BYTES + F::NUM_BYTES].fill(0xff); + assert!(F::from_bytes_le_checked(&[0xff; F::NUM_BYTES]).is_none()); + assert!(matches!( + verify(&claim(3, sum(&evals)), &narg), + Err(SumcheckError::Transcript(TranscriptError::NonCanonical)) + )); } #[test] -fn replayed_round_polynomial_rejected() { - // Use the first round's polynomial for every round. The running sum check - // will fail because the replayed polynomial doesn't satisfy s(0)+s(1) == running_sum - // for rounds > 0. +fn rearranged_round_messages_are_rejected() { let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); + let claim = claim(3, sum(&evals)); + let narg = honest_prove(&evals); + let round = |k: usize| &narg[k * ROUND_BYTES..(k + 1) * ROUND_BYTES]; - let mut pt = new_transcript(); - let proof = honest_prove(&evals, 3, &mut pt); + let swapped = [round(1), round(0), round(2)].concat(); + assert!(verify(&claim, &swapped).is_err()); - let replayed = ClearSumcheckProof { - round_polynomials: vec![proof.round_polynomials[0].clone(); 3], - }; - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = new_transcript(); - let result = SumcheckVerifier::verify( - &claim, - &replayed.round_polynomials, - BooleanHypercube, - &mut vt, - ); - assert!(result.is_err(), "replayed rounds must be rejected"); + let replayed = [round(0), round(0), round(0)].concat(); + assert!(verify(&claim, &replayed).is_err()); } #[test] -fn all_zero_round_polynomials_rejected_for_nonzero_sum() { - // If the sum is nonzero, round 0 requires s(0) + s(1) == sum. - // All-zero polynomials have s(0) + s(1) = 0, so this must fail. +fn all_zero_rounds_rejected_for_nonzero_sum() { let evals: Vec = (1..=4).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - assert_ne!(sum, F::from_u64(0)); - - let zero_poly = UnivariatePoly::new(vec![F::from_u64(0)]); - let proof = ClearSumcheckProof { - round_polynomials: vec![zero_poly; 2], - }; - - let claim = SumcheckClaim { - num_vars: 2, - degree: 1, - claimed_sum: sum, - }; - - let mut vt = new_transcript(); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); assert!(matches!( - result, + verify(&claim(2, sum(&evals)), &[0; 2 * ROUND_BYTES]), Err(SumcheckError::RoundCheckFailed { round: 0, .. }) )); } #[test] -fn all_zero_polynomial_honest_proof_for_zero_sum() { - // The zero polynomial f(x) = 0 for all x has sum = 0. - // An honest proof should be all-zero round polynomials and must verify. - let num_vars = 3; - let evals = vec![F::from_u64(0); 1 << num_vars]; - - let mut pt = new_transcript(); - let proof = honest_prove(&evals, num_vars, &mut pt); - - let claim = SumcheckClaim { - num_vars, - degree: 1, - claimed_sum: F::from_u64(0), - }; - - let result = verify_with_oracle_check(&claim, &proof, &evals); - assert!( - result.is_ok(), - "zero polynomial must verify: {:?}", - result.err() - ); +fn every_truncation_is_rejected() { + let evals: Vec = (1..=8).map(F::from_u64).collect(); + let claim = claim(3, sum(&evals)); + let narg = honest_prove(&evals); + for len in 0..narg.len() { + assert!( + matches!( + verify(&claim, &narg[..len]), + Err(SumcheckError::Transcript(TranscriptError::Truncated)) + ), + "prefix of {len} bytes" + ); + } } #[test] -fn verifier_transcript_desync_rejected() { - // If the verifier's transcript has been poisoned with extra data before - // verification begins, the Fiat-Shamir challenges will differ from the - // prover's, causing the running sum check to fail. +fn trailing_bytes_are_rejected() { let evals: Vec = (1..=8).map(F::from_u64).collect(); - let sum = compute_sum(&evals); - - let mut pt = new_transcript(); - let proof = honest_prove(&evals, 3, &mut pt); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; - - // Poison the verifier transcript with extra data - let mut vt = new_transcript(); - F::from_u64(0xdead).append_to_transcript(&mut vt); - - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); - - // Round 0's s(0)+s(1) check passes (it doesn't depend on challenges), - // but the challenge r_0 will differ, so round 1's running sum will be wrong. - // For num_vars == 1 this wouldn't be caught by round checks (only by oracle check), - // but for num_vars >= 2, the transcript desync propagates to a round check failure. - assert!(result.is_err(), "transcript desync must be rejected"); + let claim = claim(3, sum(&evals)); + let narg = honest_prove(&evals); + + let extra_byte = [narg.as_slice(), &[0]].concat(); + let extra_round = [narg.as_slice(), &narg[..ROUND_BYTES]].concat(); + for proof in [extra_byte, extra_round] { + assert!(matches!( + verify(&claim, &proof), + Err(SumcheckError::Transcript(TranscriptError::TrailingBytes)) + )); + } } #[test] -fn num_vars_zero_accepts_any_claimed_sum() { - // With 0 variables, the "polynomial" is a constant. The sum over the empty - // hypercube {0,1}^0 = {()} is just the constant value itself. - // The verifier should accept with no rounds and return (claimed_sum, []). - let claim = SumcheckClaim { - num_vars: 0, - degree: 1, - claimed_sum: F::from_u64(42), - }; - - let round_proofs: &[UnivariatePoly] = &[]; - let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, round_proofs, BooleanHypercube, &mut vt); - assert!(result.is_ok()); - - let EvaluationClaim { - point: challenges, - value: final_eval, - } = result.unwrap(); - assert_eq!(final_eval, F::from_u64(42)); - assert!(challenges.is_empty()); +fn verifier_transcript_desync_rejected() { + // Round 0's check does not depend on any challenge; the desync surfaces + // when round 1 is checked against the running sum at the wrong challenge. + let evals: Vec = (1..=8).map(F::from_u64).collect(); + let claim = claim(3, sum(&evals)); + let narg = honest_prove(&evals); + + let mut extra_absorb = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); + extra_absorb.public(&F::from_u64(0xdead)); + let mut other_session = VerifierTranscript::::new(&PROTOCOL, b"other", &narg); + for transcript in [&mut extra_absorb, &mut other_session] { + assert!(matches!( + SumcheckVerifier::verify(&claim, BooleanHypercube, transcript), + Err(SumcheckError::RoundCheckFailed { round: 1, .. }) + )); + } } #[test] -fn num_vars_zero_no_oracle_check_possible() { - // With 0 rounds, the verifier returns the claimed_sum as-is with no - // Fiat-Shamir interaction. The protocol offers no soundness guarantee - // at this point — security relies entirely on the oracle check. - // A malicious prover can claim any sum and "verify" it. - let claim = SumcheckClaim { - num_vars: 0, - degree: 1, - claimed_sum: F::from_u64(999), // arbitrary lie - }; - - let round_proofs: &[UnivariatePoly] = &[]; - let mut vt = new_transcript(); - let result = SumcheckVerifier::verify(&claim, round_proofs, BooleanHypercube, &mut vt); - - // Passes — the verifier has nothing to check! - // Only the oracle check (comparing 999 against the actual constant) catches this. - assert!(result.is_ok()); - assert_eq!(result.unwrap().value, F::from_u64(999)); +fn num_vars_zero_reads_nothing_and_returns_the_claimed_sum() { + // No rounds means no soundness from the sumcheck itself: any claimed sum + // "verifies", and only the oracle check against the constant catches a lie. + let reduced = verify(&claim(0, F::from_u64(999)), &[]).unwrap(); + assert_eq!(reduced.value, F::from_u64(999)); + assert!(reduced.point.is_empty()); + + let evals: Vec = (1..=2).map(F::from_u64).collect(); + assert!(matches!( + verify(&claim(0, F::from_u64(999)), &honest_prove(&evals)), + Err(SumcheckError::Transcript(TranscriptError::TrailingBytes)) + )); } -/// Honest degree-2 compressed prover for f = g * h (both multilinear, -/// HighToLow binding), absorbing rounds exactly the way `verify_compressed` -/// replays them: `LabelWithCount(label, degree)` then the stored -/// coefficients `[c0, c2]`. -fn honest_prove_product_compressed( - g_evals: &[F], - h_evals: &[F], - num_vars: usize, - transcript: &mut Blake2bTranscript, -) -> CompressedSumcheckProof { +/// Honest degree-2 compressed prover for `g * h` (both multilinear, +/// HighToLow binding). +fn honest_prove_product_compressed(g_evals: &[F], h_evals: &[F]) -> Vec { + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); let mut g = g_evals.to_vec(); let mut h = h_evals.to_vec(); - let mut rounds = Vec::with_capacity(num_vars); - - for _round in 0..num_vars { + while g.len() > 1 { let half = g.len() / 2; - let mut c0 = F::from_u64(0); - let mut c1 = F::from_u64(0); - let mut c2 = F::from_u64(0); + let mut coefficients = [F::from_u64(0); 3]; for i in 0..half { let (g_lo, g_hi) = (g[i], g[i + half]); let (h_lo, h_hi) = (h[i], h[i + half]); - c0 += g_lo * h_lo; - c1 += g_lo * (h_hi - h_lo) + h_lo * (g_hi - g_lo); - c2 += (g_hi - g_lo) * (h_hi - h_lo); - } - let compressed = UnivariatePoly::new(vec![c0, c1, c2]).compress(); - - let coeffs = compressed.coeffs_except_linear_term(); - transcript.append(&LabelWithCount( - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - coeffs.len() as u64, - )); - for coeff in coeffs { - coeff.append_to_transcript(transcript); + coefficients[0] += g_lo * h_lo; + coefficients[1] += g_lo * (h_hi - h_lo) + h_lo * (g_hi - g_lo); + coefficients[2] += (g_hi - g_lo) * (h_hi - h_lo); } - let r: F = transcript.challenge(); - + send_compressed_round( + &UnivariatePoly::new(coefficients.to_vec()), + 2, + &mut transcript, + ) + .unwrap(); + let r: F = transcript.challenge_small(); for i in 0..half { g[i] = g[i] + r * (g[i + half] - g[i]); h[i] = h[i] + r * (h[i + half] - h[i]); } g.truncate(half); h.truncate(half); - rounds.push(compressed); - } - - CompressedSumcheckProof { - round_polynomials: rounds, } + transcript.finish() } -fn product_eval(g_evals: &[F], h_evals: &[F], point: &[F]) -> F { - Polynomial::new(g_evals.to_vec()).evaluate_and_consume(point) - * Polynomial::new(h_evals.to_vec()).evaluate_and_consume(point) +fn verify_compressed( + claim: &SumcheckClaim, + narg: &[u8], +) -> Result, SumcheckError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let reduced = SumcheckVerifier::verify_compressed(claim, &mut transcript)?; + transcript.finish()?; + Ok(reduced) } #[test] -fn tampered_compressed_nonlinear_coefficients_rejected_by_oracle_check() { +fn tampered_compressed_coefficients_rejected_only_by_oracle_check() { let num_vars = 3; let g_evals: Vec = (1..=8).map(F::from_u64).collect(); let h_evals: Vec = (3..=10).rev().map(F::from_u64).collect(); let claimed_sum: F = g_evals.iter().zip(&h_evals).map(|(&g, &h)| g * h).sum(); - let claim = SumcheckClaim { - num_vars, - degree: 2, - claimed_sum, + let claim = SumcheckClaim::new(num_vars, 2, claimed_sum); + let product_eval = |point: &[F]| { + Polynomial::new(g_evals.clone()).evaluate_and_consume(point) + * Polynomial::new(h_evals.clone()).evaluate_and_consume(point) }; - let mut pt = new_transcript(); - let proof = honest_prove_product_compressed(&g_evals, &h_evals, num_vars, &mut pt); - - // Harness sanity: the honest compressed proof verifies AND satisfies the - // oracle check, so any failure below is attributable to the tamper. - let mut vt = new_transcript(); - let honest = SumcheckVerifier::verify_compressed( - &claim, - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut vt, - ) - .unwrap(); - assert_eq!( - honest.value, - product_eval(&g_evals, &h_evals, &honest.point) - ); - - // Tamper every stored coefficient of every round in turn. The compressed - // wire form stores exactly the non-linear coefficients [c0, c2], so the - // proof's lengths and degrees stay valid. - for round in 0..num_vars { - for position in 0..2usize { - let mut tampered = proof.clone(); - let mut coeffs = tampered.round_polynomials[round] - .coeffs_except_linear_term() - .to_vec(); - coeffs[position] += F::from_u64(1); - tampered.round_polynomials[round] = CompressedPoly::new(coeffs); - - let mut vt = new_transcript(); - let result = SumcheckVerifier::verify_compressed( - &claim, - &tampered, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut vt, - ); - // The compressed encoding re-derives each linear coefficient from - // the running sum, so s(0)+s(1) == running_sum holds by - // construction and the round loop CANNOT reject this tamper. - // Soundness rests entirely on the final oracle check, exactly as - // for the num_vars == 0 case documented on `verify`. - assert!( - result.is_ok(), - "round checks are expected to pass for tampered round {round} \ - position {position}: {result:?}" - ); - let reduction = result.unwrap(); - assert_ne!( - reduction.value, - product_eval(&g_evals, &h_evals, &reduction.point), - "tampered non-linear coefficient (round {round}, position \ - {position}) must fail the final oracle check" - ); - } + let narg = honest_prove_product_compressed(&g_evals, &h_evals); + assert_eq!(narg.len(), num_vars * 2 * F::NUM_BYTES); + let honest = verify_compressed(&claim, &narg).unwrap(); + assert_eq!(honest.value, product_eval(&honest.point)); + + // Each round sends [c0, c2]; the verifier re-derives c1 from the running + // sum, so s(0) + s(1) == running_sum holds by construction and the round + // loop cannot reject. Soundness rests on the final oracle check. + for coefficient in 0..2 * num_vars { + let slot = coefficient * F::NUM_BYTES..(coefficient + 1) * F::NUM_BYTES; + let tampered_value = + F::from_bytes_le_checked(&narg[slot.clone()]).unwrap() + F::from_u64(1); + let mut tampered = narg.clone(); + tampered_value.to_bytes_le(&mut tampered[slot]); + + let reduced = verify_compressed(&claim, &tampered).unwrap(); + assert_ne!( + reduced.value, + product_eval(&reduced.point), + "tampered coefficient {coefficient} must fail the oracle check" + ); } } - -#[test] -fn constant_polynomial_all_same_evals() { - // f(x) = 7 for all x in {0,1}^3 → sum = 7 * 8 = 56 - let num_vars = 3; - let evals = vec![F::from_u64(7); 1 << num_vars]; - let sum = compute_sum(&evals); - - let mut pt = new_transcript(); - let proof = honest_prove(&evals, num_vars, &mut pt); - - let claim = SumcheckClaim { - num_vars, - degree: 1, - claimed_sum: sum, - }; - - let result = verify_with_oracle_check(&claim, &proof, &evals); - assert!(result.is_ok()); - - // The final eval should be 7 regardless of the challenge point, - // since f is constant. - let mut vt = new_transcript(); - let final_eval = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt) - .unwrap() - .value; - assert_eq!(final_eval, F::from_u64(7)); -} From 0136ab621b3d4d91f6aa4d59936e1bf30518a207 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:14:35 -0400 Subject: [PATCH 12/62] test(blindfold): port the BlindFold suite to the NARG transcript 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. --- crates/jolt-blindfold/src/protocol.rs | 354 ++---- crates/jolt-blindfold/src/verify.rs | 445 +++----- .../tests/assignment_pipeline.rs | 384 ++----- crates/jolt-blindfold/tests/proof_pipeline.rs | 247 +--- crates/jolt-blindfold/tests/r1cs_pipeline.rs | 2 +- crates/jolt-blindfold/tests/real_prover.rs | 191 ++-- .../jolt-blindfold/tests/sumcheck_pipeline.rs | 43 +- crates/jolt-blindfold/tests/support/mod.rs | 1009 +++++++++-------- 8 files changed, 1007 insertions(+), 1668 deletions(-) diff --git a/crates/jolt-blindfold/src/protocol.rs b/crates/jolt-blindfold/src/protocol.rs index aafc840da7..47ff5e4aa3 100644 --- a/crates/jolt-blindfold/src/protocol.rs +++ b/crates/jolt-blindfold/src/protocol.rs @@ -609,17 +609,14 @@ fn checked_sum( #[expect(clippy::indexing_slicing, reason = "tests index fixture data")] mod tests { use super::*; - use crate::{ - BlindFoldStage, BlindFoldStatement, CommittedClaimRows, FinalOpeningBinding, OpeningAlias, - }; + use crate::{BlindFoldStage, BlindFoldStatement, CommittedClaimRows, OpeningAlias}; use jolt_claims::{constant, opening, Expr}; use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment}; use jolt_field::{Fr, Ring}; use jolt_sumcheck::{ - CommittedOutputClaims, CommittedRound, CommittedSumcheckProof, SumcheckDomainSpec, - SumcheckError, SumcheckStatement, + CommittedOutputClaims, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckStatement, + VerifiedCommittedRound, }; - use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; #[derive(Clone, Debug)] struct TestStage { @@ -639,44 +636,64 @@ mod tests { } } - fn proof(rounds: &[(u64, usize)], output_claims: &[u64]) -> CommittedSumcheckProof { - CommittedSumcheckProof { - rounds: rounds - .iter() - .map(|&(commitment, degree)| CommittedRound { - commitment: Fr::from_u64(commitment), - degree, - }) - .collect(), - output_claims: CommittedOutputClaims { - commitments: output_claims - .iter() - .map(|&commitment| Fr::from_u64(commitment)) + /// A stage's committed messages as `SumcheckVerifier::verify_committed` + /// returns them: one commitment per round at the statement degree, then + /// the output-claim row commitments. + #[derive(Clone, Debug)] + struct Committed { + consistency: CommittedSumcheckConsistency, + output_claims: CommittedOutputClaims, + } + + fn committed( + stage: &TestStage, + rounds: Vec, + output_claims: Vec, + ) -> Committed { + assert_eq!(rounds.len(), stage.statement.num_vars); + Committed { + consistency: CommittedSumcheckConsistency { + rounds: rounds + .into_iter() + .enumerate() + .map(|(index, commitment)| VerifiedCommittedRound { + commitment, + degree: stage.statement.degree, + challenge: Fr::from_u64(index as u64 + 2), + }) .collect(), }, + output_claims: CommittedOutputClaims { + commitments: output_claims, + }, } } - fn commitment_proof( + fn scalar_committed(stage: &TestStage, rounds: &[u64], output_claims: &[u64]) -> Committed { + committed( + stage, + rounds.iter().copied().map(Fr::from_u64).collect(), + output_claims.iter().copied().map(Fr::from_u64).collect(), + ) + } + + fn pedersen_committed( setup: &PedersenSetup, - rounds: &[(u64, usize)], + stage: &TestStage, + rounds: &[u64], output_claims: &[u64], - ) -> CommittedSumcheckProof { - CommittedSumcheckProof { - rounds: rounds + ) -> Committed { + committed( + stage, + rounds .iter() - .map(|&(commitment, degree)| CommittedRound { - commitment: pedersen_commitment(setup, commitment), - degree, - }) + .map(|&value| pedersen_commitment(setup, value)) .collect(), - output_claims: CommittedOutputClaims { - commitments: output_claims - .iter() - .map(|&commitment| pedersen_commitment(setup, commitment)) - .collect(), - }, - } + output_claims + .iter() + .map(|&value| pedersen_commitment(setup, value)) + .collect(), + ) } fn pedersen_setup() -> PedersenSetup { @@ -691,65 +708,40 @@ mod tests { Pedersen::::commit(setup, &[Fr::from_u64(value)], &Fr::from_u64(value + 1000)) } - fn try_statement_from_proofs( + fn statement_from_committed( stages: &[TestStage], - proofs: &[CommittedSumcheckProof], - final_openings: Vec>, - ) -> Result, VerificationError> - where - Com: Clone + AppendToTranscript, - { - if stages.len() != proofs.len() { - return Err(VerificationError::StageCountMismatch { - claim_stages: stages.len(), - proof_stages: proofs.len(), - }); - } - - let mut transcript = Blake2bTranscript::::new(b"blindfold"); + committed: &[Committed], + ) -> BlindFoldStatement { + assert_eq!(stages.len(), committed.len()); let mut next_opening_id = 0usize; let stages = stages .iter() - .zip(proofs) - .enumerate() - .map(|(stage_index, (stage, proof))| { - let consistency = proof - .verify_committed_consistency(stage.statement, &mut transcript) - .map_err(|source| VerificationError::Sumcheck { - stage_index, - source, - })?; + .zip(committed) + .map(|(stage, committed)| { let row_len = stage.statement.degree + 1; - let output_opening_count = proof.output_claims.commitments.len() * row_len; + let output_opening_count = committed.output_claims.commitments.len() * row_len; let opening_ids = (next_opening_id..next_opening_id + output_opening_count).collect(); next_opening_id += output_opening_count; - Ok::, VerificationError>(BlindFoldStage::new( + BlindFoldStage::new( stage.name.clone(), stage.statement, SumcheckDomainSpec::BooleanHypercube, - consistency, - CommittedClaimRows::new(opening_ids, row_len, proof.output_claims.clone()), + committed.consistency.clone(), + CommittedClaimRows::new(opening_ids, row_len, committed.output_claims.clone()), stage.input_claim.clone(), stage.output_claim.clone(), - )) + ) }) - .collect::, _>>()?; - - Ok(BlindFoldStatement::new(stages, final_openings)) + .collect(); + BlindFoldStatement::new(stages, Vec::new()) } - fn protocol_from_proofs( + fn protocol_from_committed( stages: &[TestStage], - proofs: &[CommittedSumcheckProof], - final_openings: Vec>, - ) -> BlindFoldProtocol - where - Com: Clone + AppendToTranscript, - { - let statement = try_statement_from_proofs(stages, proofs, final_openings) - .expect("statement builds from committed proofs"); - protocol_from_statement(&statement) + committed: &[Committed], + ) -> BlindFoldProtocol { + protocol_from_statement(&statement_from_committed(stages, committed)) } fn protocol_from_statement( @@ -775,100 +767,14 @@ mod tests { .finish_stage() .expect("test stage statement is complete"); } - for binding in &statement.final_openings { - builder = builder.final_opening( - binding.opening_ids.clone(), - binding.coefficients.clone(), - binding.evaluation_commitment.clone(), - ); - } builder.build().expect("BlindFold protocol builds") } - #[test] - fn verifies_committed_stages_in_claim_order() { - let stages = vec![stage(2, 2), stage(1, 1)]; - let proofs = vec![ - proof(&[(11, 1), (12, 2)], &[21]), - proof(&[(13, 1)], &[34, 55]), - ]; - - let verified = - try_statement_from_proofs(&stages, &proofs, Vec::new()).expect("proofs verify"); - - assert_eq!(verified.stage_count(), 2); - assert_eq!(verified.stages[0].consistency.rounds.len(), 2); - assert_eq!(verified.stages[1].consistency.rounds.len(), 1); - let round_commitments = verified - .stages - .iter() - .flat_map(|stage| { - stage - .consistency - .rounds - .iter() - .map(|round| round.commitment) - }) - .collect::>(); - let output_claim_commitments = verified - .stages - .iter() - .flat_map(|stage| { - stage - .output_claim_rows - .commitments - .commitments - .iter() - .copied() - }) - .collect::>(); - assert_eq!( - round_commitments, - vec![Fr::from_u64(11), Fr::from_u64(12), Fr::from_u64(13)] - ); - assert_eq!( - output_claim_commitments, - vec![Fr::from_u64(21), Fr::from_u64(34), Fr::from_u64(55)] - ); - } - - #[test] - fn rejects_stage_count_mismatch() { - let stages = vec![stage(1, 1), stage(1, 1)]; - let proofs = vec![proof(&[(11, 1)], &[])]; - let error = - try_statement_from_proofs(&stages, &proofs, Vec::new()).expect_err("counts differ"); - - assert!(matches!( - error, - VerificationError::StageCountMismatch { - claim_stages: 2, - proof_stages: 1, - } - )); - } - - #[test] - fn reports_sumcheck_error_with_stage_index() { - let stages = vec![stage(1, 1), stage(1, 1)]; - let proofs = vec![proof(&[(11, 1)], &[]), proof(&[(12, 2)], &[])]; - let error = - try_statement_from_proofs(&stages, &proofs, Vec::new()).expect_err("degree fails"); - - assert!(matches!( - error, - VerificationError::Sumcheck { - stage_index: 1, - source: SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }, - } - )); - } - #[test] fn blindfold_protocol_builder_constructs_protocol() { let stages = vec![stage(1, 1)]; - let proofs = vec![proof(&[(11, 1)], &[21])]; - let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + let committed = vec![scalar_committed(&stages[0], &[11], &[21])]; + let protocol = protocol_from_committed(&stages, &committed); assert_eq!(protocol.sumcheck_consistency.len(), 1); assert_eq!(protocol.committed_output_claims.len(), 1); @@ -909,18 +815,18 @@ mod tests { Alias, } - let statement = SumcheckStatement::new(1, 1); - let proof = proof(&[(11, 1)], &[21]); - let mut transcript = Blake2bTranscript::::new(b"blindfold-alias"); - let consistency = proof - .verify_committed_consistency(statement, &mut transcript) - .expect("committed proof is consistent"); + let stage = stage(1, 1); + let statement = stage.statement; + let Committed { + consistency, + output_claims, + } = scalar_committed(&stage, &[11], &[21]); let protocol = BlindFoldProtocol::::builder::() .stage("alias") .sumcheck(statement) .domain(SumcheckDomainSpec::BooleanHypercube) .consistency(consistency) - .output_claim_rows(vec![Opening::Source], 1, proof.output_claims.clone()) + .output_claim_rows(vec![Opening::Source], 1, output_claims) .output_claim_aliases([OpeningAlias::new(Opening::Alias, Opening::Source)]) .input_claim(constant(Fr::from_u64(0))) .output_claim(opening(Opening::Alias)) @@ -941,18 +847,18 @@ mod tests { Other, } - let statement = SumcheckStatement::new(1, 1); - let proof = proof(&[(11, 1)], &[21]); - let mut transcript = Blake2bTranscript::::new(b"blindfold-alias"); - let consistency = proof - .verify_committed_consistency(statement, &mut transcript) - .expect("committed proof is consistent"); + let stage = stage(1, 1); + let statement = stage.statement; + let Committed { + consistency, + output_claims, + } = scalar_committed(&stage, &[11], &[21]); let error = BlindFoldProtocol::::builder::() .stage("alias") .sumcheck(statement) .domain(SumcheckDomainSpec::BooleanHypercube) .consistency(consistency) - .output_claim_rows(vec![Opening::Other], 1, proof.output_claims.clone()) + .output_claim_rows(vec![Opening::Other], 1, output_claims) .output_claim_aliases([OpeningAlias::new(Opening::Alias, Opening::Source)]) .input_claim(constant(Fr::from_u64(0))) .output_claim(opening(Opening::Alias)) @@ -969,31 +875,21 @@ mod tests { #[test] fn rejects_malformed_final_opening_binding() { - let stages = vec![stage(1, 1)]; - let proofs = vec![proof(&[(11, 1)], &[21])]; - let statement = try_statement_from_proofs( - &stages, - &proofs, - vec![FinalOpeningBinding::new( - vec![0], - Vec::new(), - Fr::from_u64(99), - )], - ) - .expect("statement construction verifies committed proof"); + let stage = stage(1, 1); + let committed = scalar_committed(&stage, &[11], &[21]); let error = BlindFoldProtocol::::builder::() .stage("stage") - .sumcheck(stages[0].statement) + .sumcheck(stage.statement) .domain(SumcheckDomainSpec::BooleanHypercube) - .consistency(statement.stages[0].consistency.clone()) + .consistency(committed.consistency.clone()) .output_claim_rows( vec![0, 1], - stages[0].statement.degree + 1, - proofs[0].output_claims.clone(), + stage.statement.degree + 1, + committed.output_claims.clone(), ) - .input_claim(stages[0].input_claim.clone()) - .output_claim(stages[0].output_claim.clone()) + .input_claim(stage.input_claim.clone()) + .output_claim(stage.output_claim.clone()) .finish_stage() .expect("stage is complete") .final_opening(vec![0], Vec::new(), Fr::from_u64(99)) @@ -1012,26 +908,20 @@ mod tests { #[test] fn rejects_empty_final_opening_binding() { - let stages = vec![stage(1, 1)]; - let proofs = vec![proof(&[(11, 1)], &[21])]; + let stage = stage(1, 1); + let committed = scalar_committed(&stage, &[11], &[21]); let error = BlindFoldProtocol::::builder::() .stage("stage") - .sumcheck(stages[0].statement) + .sumcheck(stage.statement) .domain(SumcheckDomainSpec::BooleanHypercube) - .consistency( - try_statement_from_proofs(&stages, &proofs, Vec::new()) - .expect("statement construction verifies committed proof") - .stages[0] - .consistency - .clone(), - ) + .consistency(committed.consistency.clone()) .output_claim_rows( vec![0, 1], - stages[0].statement.degree + 1, - proofs[0].output_claims.clone(), + stage.statement.degree + 1, + committed.output_claims.clone(), ) - .input_claim(stages[0].input_claim.clone()) - .output_claim(stages[0].output_claim.clone()) + .input_claim(stage.input_claim.clone()) + .output_claim(stage.output_claim.clone()) .finish_stage() .expect("stage is complete") .final_opening(Vec::new(), Vec::new(), Fr::from_u64(99)) @@ -1046,18 +936,18 @@ mod tests { #[test] fn rejects_extra_output_claim_rows_without_typed_openings() { - let statement = SumcheckStatement::new(1, 1); - let proof = proof(&[(11, 1)], &[21, 22]); - let mut transcript = Blake2bTranscript::::new(b"blindfold-output-row-count"); - let consistency = proof - .verify_committed_consistency(statement, &mut transcript) - .expect("committed proof is consistent"); + let stage = stage(1, 1); + let statement = stage.statement; + let Committed { + consistency, + output_claims, + } = scalar_committed(&stage, &[11], &[21, 22]); let error = BlindFoldProtocol::::builder::() .stage("row-count") .sumcheck(statement) .domain(SumcheckDomainSpec::BooleanHypercube) .consistency(consistency) - .output_claim_rows(vec![0, 1], 2, proof.output_claims.clone()) + .output_claim_rows(vec![0, 1], 2, output_claims) .input_claim(constant(Fr::from_u64(0))) .output_claim(constant(Fr::from_u64(0))) .finish_stage() @@ -1078,11 +968,11 @@ mod tests { #[test] fn dimensions_account_for_multiple_stages_and_padding() { let stages = vec![stage(2, 2), stage(1, 1)]; - let proofs = vec![ - proof(&[(11, 1), (12, 2)], &[21, 22, 23]), - proof(&[(13, 1)], &[34, 55]), + let committed = vec![ + scalar_committed(&stages[0], &[11, 12], &[21, 22, 23]), + scalar_committed(&stages[1], &[13], &[34, 55]), ]; - let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + let protocol = protocol_from_committed(&stages, &committed); assert_eq!( protocol.dimensions, @@ -1114,11 +1004,11 @@ mod tests { fn committed_relaxed_instance_assembles_witness_rows_in_layout_order() { let setup = pedersen_setup(); let stages = vec![stage(2, 2), stage(1, 1)]; - let proofs = vec![ - commitment_proof(&setup, &[(11, 1), (12, 2)], &[21, 22]), - commitment_proof(&setup, &[(13, 1)], &[34]), + let committed = vec![ + pedersen_committed(&setup, &stages[0], &[11, 12], &[21, 22]), + pedersen_committed(&setup, &stages[1], &[13], &[34]), ]; - let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + let protocol = protocol_from_committed(&stages, &committed); let auxiliary_rows = vec![ pedersen_commitment(&setup, 41), @@ -1163,8 +1053,8 @@ mod tests { fn committed_relaxed_instance_rejects_auxiliary_row_count_mismatch() { let setup = pedersen_setup(); let stages = vec![stage(1, 1)]; - let proofs = vec![commitment_proof(&setup, &[(11, 1)], &[21])]; - let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + let committed = vec![pedersen_committed(&setup, &stages[0], &[11], &[21])]; + let protocol = protocol_from_committed(&stages, &committed); let error = protocol .committed_relaxed_instance(&[]) @@ -1184,8 +1074,8 @@ mod tests { fn random_relaxed_instance_accepts_exact_dimensions() { let setup = pedersen_setup(); let stages = vec![stage(1, 1)]; - let proofs = vec![commitment_proof(&setup, &[(11, 1)], &[21])]; - let protocol = protocol_from_proofs(&stages, &proofs, Vec::new()); + let committed = vec![pedersen_committed(&setup, &stages[0], &[11], &[21])]; + let protocol = protocol_from_committed(&stages, &committed); let round_rows = vec![pedersen_commitment(&setup, 7); protocol.dimensions.coefficient_rows]; let output_claim_rows = @@ -1359,8 +1249,8 @@ mod tests { fn one_stage_protocol(setup: &PedersenSetup) -> BlindFoldProtocol { let stages = vec![stage(1, 1)]; - let proofs = vec![commitment_proof(setup, &[(11, 1)], &[21])]; + let committed = vec![pedersen_committed(setup, &stages[0], &[11], &[21])]; - protocol_from_proofs(&stages, &proofs, Vec::new()) + protocol_from_committed(&stages, &committed) } } diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs index 475305e2f9..347948f472 100644 --- a/crates/jolt-blindfold/src/verify.rs +++ b/crates/jolt-blindfold/src/verify.rs @@ -406,16 +406,20 @@ mod tests { use super::*; use crate::{ r1cs::{FinalOpeningLayout, Layout}, + wire::send_opening, BlindFoldDimensions, RowDimensions, WitnessRowLayout, }; use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorOpeningError, }; use jolt_field::{Fr, Ring}; - use jolt_poly::CompressedPoly; use jolt_r1cs::ConstraintMatrices; - use jolt_sumcheck::CompressedSumcheckProof; - use jolt_transcript::Blake2bTranscript; + use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript, TranscriptError}; + + type H = Blake2b512; + + const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-blindfold/verify-tests"); + const SESSION: &[u8] = b"blindfold-verify"; fn f(value: u64) -> Fr { Fr::from_u64(value) @@ -439,11 +443,6 @@ mod tests { ::identity() } - fn protocol(setup: &PedersenSetup) -> BlindFoldProtocol { - let _ = setup; - empty_protocol(Vec::new()) - } - fn protocol_with_eval(setup: &PedersenSetup) -> BlindFoldProtocol { empty_protocol(vec![commit_value(setup, f(7), f(70))]) } @@ -547,10 +546,6 @@ mod tests { protocol } - fn add_zero_inner_round(proof: &mut BlindFoldProof) { - proof.inner_sumcheck.round_polynomials = vec![CompressedPoly::new(vec![f(0)])]; - } - fn outer_round_protocol() -> BlindFoldProtocol { let mut protocol = empty_protocol(Vec::new()); protocol.dimensions.error = RowDimensions { @@ -560,19 +555,6 @@ mod tests { protocol } - fn coefficient_row_protocol() -> BlindFoldProtocol { - let mut protocol = empty_protocol(Vec::new()); - protocol.dimensions.witness_rows = WitnessRowLayout { - coefficients: 0..1, - output_claims: 1..1, - auxiliary: 1..1, - padding: 1..1, - }; - protocol.dimensions.coefficient_rows = 1; - protocol.dimensions.coefficient_values = 1; - protocol - } - fn opening(row_len: usize) -> VectorCommitmentOpening { VectorCommitmentOpening { combined_vector: vec![f(0); row_len], @@ -580,109 +562,102 @@ mod tests { } } - fn zero_outer_sumcheck( - protocol: &BlindFoldProtocol, - ) -> CompressedSumcheckProof { - let num_vars = protocol.dimensions.error.row_count.trailing_zeros() as usize - + protocol.dimensions.error.row_len.trailing_zeros() as usize; - CompressedSumcheckProof { - round_polynomials: vec![CompressedPoly::new(vec![f(0)]); num_vars], + fn num_vars(dimensions: RowDimensions) -> usize { + (dimensions.row_count.trailing_zeros() + dimensions.row_len.trailing_zeros()) as usize + } + + /// The prover messages of a BlindFold proof, written in the transcript + /// order of `wire.rs` and `prove.rs`. Sumcheck rounds hold their + /// compressed coefficients (every coefficient but the linear one). + struct Messages { + folding: FoldingCommitments, + folded_eval_outputs: Vec, + folded_eval_blindings: Vec, + outer_rounds: Vec>, + abc: [Fr; 3], + error_opening: VectorCommitmentOpening, + inner_rounds: Vec>, + witness_opening: VectorCommitmentOpening, + } + + impl Messages { + fn zero(setup: &PedersenSetup, protocol: &BlindFoldProtocol) -> Self { + let dimensions = &protocol.dimensions; + let eval_count = protocol.eval_commitments.len(); + Self { + folding: FoldingCommitments { + auxiliary_rows: vec![commitment(setup, 41); dimensions.auxiliary_rows], + random_u: f(3), + random_rounds: vec![identity(); dimensions.coefficient_rows], + random_output_claim_rows: vec![identity(); dimensions.output_claim_rows], + random_auxiliary_rows: vec![identity(); dimensions.auxiliary_rows], + random_error_rows: vec![identity(); dimensions.error.row_count], + random_evals: vec![commit_value(setup, f(11), f(110)); eval_count], + cross_term_error_rows: vec![identity(); dimensions.error.row_count], + }, + folded_eval_outputs: vec![f(0); eval_count], + folded_eval_blindings: vec![f(0); eval_count], + outer_rounds: vec![vec![f(0); OUTER_SUMCHECK_DEGREE]; num_vars(dimensions.error)], + abc: [f(0); 3], + error_opening: opening(dimensions.error.row_len), + inner_rounds: vec![vec![f(0); INNER_SUMCHECK_DEGREE]; num_vars(dimensions.witness)], + witness_opening: opening(dimensions.witness.row_len), + } } - } - fn proof( - setup: &PedersenSetup, - protocol: &BlindFoldProtocol, - ) -> BlindFoldProof { - BlindFoldProof { - auxiliary_row_commitments: vec![ - commitment(setup, 41); - protocol.dimensions.auxiliary_rows - ], - random_round_commitments: vec![identity(); protocol.dimensions.coefficient_rows], - random_output_claim_row_commitments: vec![ - identity(); - protocol.dimensions.output_claim_rows - ], - random_auxiliary_row_commitments: vec![identity(); protocol.dimensions.auxiliary_rows], - random_error_row_commitments: vec![identity(); protocol.dimensions.error.row_count], - random_eval_commitments: vec![ - commit_value(setup, f(11), f(110)); - protocol.eval_commitments.len() - ], - random_u: f(3), - cross_term_error_row_commitments: vec![identity(); protocol.dimensions.error.row_count], - outer_sumcheck: zero_outer_sumcheck(protocol), - az_rx: f(0), - bz_rx: f(0), - cz_rx: f(0), - inner_sumcheck: CompressedSumcheckProof::default(), - witness_opening: opening(protocol.dimensions.witness.row_len), - error_opening: opening(protocol.dimensions.error.row_len), - folded_eval_outputs: vec![f(0); protocol.eval_commitments.len()], - folded_eval_blindings: vec![f(0); protocol.eval_commitments.len()], - folded_eval_output_openings: Vec::new(), - folded_eval_blinding_openings: Vec::new(), + fn transcript() -> ProverTranscript { + ProverTranscript::new(&PROTOCOL, SESSION) } - } - fn folding_challenge( - protocol: &BlindFoldProtocol, - proof: &BlindFoldProof, - ) -> Fr { - let committed = protocol - .committed_relaxed_instance(&proof.auxiliary_row_commitments) - .expect("committed instance builds"); - let random = protocol - .random_relaxed_instance( - &proof.random_round_commitments, - &proof.random_output_claim_row_commitments, - &proof.random_auxiliary_row_commitments, - &proof.random_error_row_commitments, - &proof.random_eval_commitments, - proof.random_u, - ) - .expect("random instance builds"); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - committed.append_to_transcript( - &mut transcript, - b"bf_committed_u", - b"bf_committed_w", - b"bf_committed_e", - b"bf_committed_eval", - ); - random.append_to_transcript( - &mut transcript, - b"bf_random_u", - b"bf_random_w", - b"bf_random_e", - b"bf_random_eval", - ); - transcript.append_values(b"bf_cross_e", &proof.cross_term_error_row_commitments); - transcript.challenge() + fn folding_challenge(&self) -> Fr { + let mut transcript = Self::transcript(); + self.folding.send(&mut transcript); + transcript.challenge_small() + } + + fn with_valid_eval_opening(mut self) -> Self { + let folding_challenge = self.folding_challenge(); + self.folded_eval_outputs = vec![f(7) + folding_challenge * f(11)]; + self.folded_eval_blindings = vec![f(70) + folding_challenge * f(110)]; + self + } + + fn narg(&self) -> Vec { + let mut transcript = Self::transcript(); + self.folding.send(&mut transcript); + transcript.send_all(&self.folded_eval_outputs); + transcript.send_all(&self.folded_eval_blindings); + for round in &self.outer_rounds { + transcript.send_all(round); + } + transcript.send_all(&self.abc); + send_opening(&self.error_opening, &mut transcript); + for round in &self.inner_rounds { + transcript.send_all(round); + } + send_opening(&self.witness_opening, &mut transcript); + transcript.finish() + } } - fn proof_with_valid_eval_opening( - setup: &PedersenSetup, + fn verify( protocol: &BlindFoldProtocol, - ) -> BlindFoldProof { - let mut proof = proof(setup, protocol); - let folding_challenge = folding_challenge(protocol, &proof); - proof.folded_eval_outputs = vec![f(7) + folding_challenge * f(11)]; - proof.folded_eval_blindings = vec![f(70) + folding_challenge * f(110)]; - proof + setup: &PedersenSetup, + narg: &[u8], + ) -> Result<(), VerificationError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + protocol.verify::, H>(setup, &mut transcript)?; + Ok(transcript.finish()?) } #[test] fn verify_rejects_degenerate_outer_sumcheck() { let setup = setup(); - let protocol = protocol(&setup); - let proof = proof(&setup, &protocol); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let protocol = empty_protocol(Vec::new()); + let narg = Messages::zero(&setup, &protocol).narg(); - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("degenerate outer sumcheck is rejected"); + let error = + verify(&protocol, &setup, &narg).expect_err("degenerate outer sumcheck is rejected"); assert!(matches!( error, @@ -695,132 +670,78 @@ mod tests { #[test] fn folded_instance_uses_transcript_derived_challenge() { let setup = setup(); - let protocol = protocol(&setup); - let proof = proof(&setup, &protocol); + let protocol = inner_round_protocol(); + let messages = Messages::zero(&setup, &protocol); + let narg = messages.narg(); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); let folded = protocol - .folded_instance_from_proof(&proof, &mut transcript) + .folded_instance(&mut transcript) .expect("fold inputs are well-shaped"); + let sent = &messages.folding; let committed = protocol - .committed_relaxed_instance(&proof.auxiliary_row_commitments) + .committed_relaxed_instance(&sent.auxiliary_rows) .expect("committed instance builds"); let random = protocol .random_relaxed_instance( - &proof.random_round_commitments, - &proof.random_output_claim_row_commitments, - &proof.random_auxiliary_row_commitments, - &proof.random_error_row_commitments, - &proof.random_eval_commitments, - proof.random_u, + &sent.random_rounds, + &sent.random_output_claim_rows, + &sent.random_auxiliary_rows, + &sent.random_error_rows, + &sent.random_evals, + sent.random_u, ) .expect("random instance builds"); - let mut manual_transcript = Blake2bTranscript::::new(b"blindfold-verify"); - committed.append_to_transcript( - &mut manual_transcript, - b"bf_committed_u", - b"bf_committed_w", - b"bf_committed_e", - b"bf_committed_eval", - ); - random.append_to_transcript( - &mut manual_transcript, - b"bf_random_u", - b"bf_random_w", - b"bf_random_e", - b"bf_random_eval", - ); - manual_transcript.append_values(b"bf_cross_e", &proof.cross_term_error_row_commitments); - let folding_challenge = manual_transcript.challenge(); + let mut prover = Messages::transcript(); + sent.send(&mut prover); + let folding_challenge: Fr = prover.challenge_small(); let expected = committed - .fold( - &random, - &proof.cross_term_error_row_commitments, - folding_challenge, - ) + .fold(&random, &sent.cross_term_error_rows, folding_challenge) .expect("fold dimensions match"); assert_eq!(folded, expected); - assert_eq!(transcript.state(), manual_transcript.state()); + assert_eq!( + transcript.preview().squeeze::<32>(), + prover.preview().squeeze::<32>() + ); } - /// Regression: a proof with fewer `folded_eval_outputs` than the layout's - /// eval coordinates previously reached `folded_eval_outputs[index]` and - /// panicked; both the eager length gate and the per-coordinate lookup - /// must surface the same typed length error instead. #[test] - fn verify_rejects_truncated_folded_eval_outputs_without_panicking() { + fn verify_rejects_truncated_folded_eval_outputs() { let setup = setup(); let protocol = protocol_with_eval(&setup); - let mut proof = proof_with_valid_eval_opening(&setup, &protocol); - let _ = proof.folded_eval_outputs.pop(); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let messages = Messages::zero(&setup, &protocol).with_valid_eval_opening(); + let mut folding_only = Messages::transcript(); + messages.folding.send(&mut folding_only); + let narg = folding_only.finish(); - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("truncated folded eval outputs are rejected"); + let error = verify(&protocol, &setup, &narg).expect_err("truncated folded eval outputs"); assert!(matches!( error, - VerificationError::Relaxed(RelaxedError::LengthMismatch { - name: "folded eval outputs", - .. - }) + VerificationError::Transcript(TranscriptError::Truncated) )); } - #[test] - fn verify_rejects_random_round_count_mismatch() { - let setup = setup(); - let protocol = coefficient_row_protocol(); - let mut proof = proof(&setup, &protocol); - let _ = proof.random_round_commitments.pop(); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("random rows are missing"); - - assert!(matches!( - error, - VerificationError::Relaxed(RelaxedError::LengthMismatch { - name: "random round commitments", - .. - }) - )); - } - - #[test] - fn verify_rejects_folded_eval_output_count_mismatch() { - let setup = setup(); - let protocol = protocol(&setup); - let mut proof = proof(&setup, &protocol); - proof.folded_eval_outputs.push(f(7)); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("folded eval count differs"); - - assert_eq!( - error.to_string(), - "folded eval outputs length mismatch: expected 0, got 1" - ); - } - #[test] fn verify_accepts_folded_eval_commitment_opening() { let setup = setup(); let protocol = protocol_with_eval(&setup); - let proof = proof_with_valid_eval_opening(&setup, &protocol); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let narg = Messages::zero(&setup, &protocol) + .with_valid_eval_opening() + .narg(); + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); let folded = protocol - .folded_instance_from_proof(&proof, &mut transcript) + .folded_instance(&mut transcript) .expect("folded instance builds"); - proof - .verify_folded_eval_commitments::>(&setup, &folded) + protocol + .verify_folded_eval_witness_bindings::, H>( + &setup, + &folded, + &mut transcript, + ) .expect("folded eval commitment opens"); } @@ -828,12 +749,10 @@ mod tests { fn verify_rejects_bad_folded_eval_commitment_opening() { let setup = setup(); let protocol = protocol_with_eval(&setup); - let mut proof = proof_with_valid_eval_opening(&setup, &protocol); - proof.folded_eval_outputs[0] += f(1); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let mut messages = Messages::zero(&setup, &protocol).with_valid_eval_opening(); + messages.folded_eval_outputs[0] += f(1); - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) + let error = verify(&protocol, &setup, &messages.narg()) .expect_err("folded eval commitment opening is wrong"); assert!(matches!( @@ -842,57 +761,14 @@ mod tests { )); } - #[test] - fn verify_rejects_outer_sumcheck_round_count_mismatch() { - let setup = setup(); - let protocol = outer_round_protocol(); - let mut proof = proof(&setup, &protocol); - let _ = proof.outer_sumcheck.round_polynomials.pop(); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("outer sumcheck has wrong length"); - - assert!(matches!( - error, - VerificationError::OuterSumcheck { - source: jolt_sumcheck::SumcheckError::WrongNumberOfRounds { .. }, - } - )); - } - - #[test] - fn verify_rejects_outer_sumcheck_degree_bound() { - let setup = setup(); - let protocol = outer_round_protocol(); - let mut proof = proof(&setup, &protocol); - proof.outer_sumcheck.round_polynomials[0] = - CompressedPoly::new(vec![f(0), f(0), f(0), f(0)]); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("outer sumcheck degree is too high"); - - assert!(matches!( - error, - VerificationError::OuterSumcheck { - source: jolt_sumcheck::SumcheckError::DegreeBoundExceeded { got: 4, max: 3 }, - } - )); - } - #[test] fn verify_rejects_bad_error_opening() { let setup = setup(); let protocol = outer_round_protocol(); - let mut proof = proof(&setup, &protocol); - proof.error_opening.combined_blinding = f(1); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let mut messages = Messages::zero(&setup, &protocol); + messages.error_opening.combined_blinding = f(1); - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) + let error = verify(&protocol, &setup, &messages.narg()) .expect_err("error opening is not binding to folded rows"); assert!(matches!( @@ -905,13 +781,10 @@ mod tests { fn verify_rejects_outer_final_claim_mismatch() { let setup = setup(); let protocol = outer_round_protocol(); - let mut proof = proof(&setup, &protocol); - proof.az_rx = f(1); - proof.bz_rx = f(1); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let mut messages = Messages::zero(&setup, &protocol); + messages.abc = [f(1), f(1), f(0)]; - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) + let error = verify(&protocol, &setup, &messages.narg()) .expect_err("outer final claim does not match opened error row"); assert!(matches!( @@ -920,39 +793,14 @@ mod tests { )); } - #[test] - fn verify_rejects_inner_sumcheck_round_count_mismatch() { - let setup = setup(); - let protocol = inner_round_protocol(); - let proof = proof(&setup, &protocol); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) - .expect_err("inner sumcheck has wrong length"); - - assert!(matches!( - error, - VerificationError::InnerSumcheck { - source: jolt_sumcheck::SumcheckError::WrongNumberOfRounds { - expected: 1, - got: 0, - }, - } - )); - } - #[test] fn verify_rejects_bad_witness_opening() { let setup = setup(); let protocol = inner_round_protocol(); - let mut proof = proof(&setup, &protocol); - add_zero_inner_round(&mut proof); - proof.witness_opening.combined_blinding = f(1); - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); + let mut messages = Messages::zero(&setup, &protocol); + messages.witness_opening.combined_blinding = f(1); - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) + let error = verify(&protocol, &setup, &messages.narg()) .expect_err("witness opening is not binding to folded rows"); assert!(matches!( @@ -965,20 +813,17 @@ mod tests { fn verify_rejects_inner_final_claim_mismatch() { let setup = setup(); let protocol = inner_round_protocol(); - let mut proof = proof(&setup, &protocol); - add_zero_inner_round(&mut proof); - proof.auxiliary_row_commitments = vec![ + let mut messages = Messages::zero(&setup, &protocol); + messages.folding.auxiliary_rows = vec![ commit_value(&setup, f(5), f(50)), commit_value(&setup, f(5), f(50)), ]; - proof.witness_opening = VectorCommitmentOpening { + messages.witness_opening = VectorCommitmentOpening { combined_vector: vec![f(5)], combined_blinding: f(50), }; - let mut transcript = Blake2bTranscript::::new(b"blindfold-verify"); - let error = protocol - .verify::, _>(&proof, &setup, &mut transcript) + let error = verify(&protocol, &setup, &messages.narg()) .expect_err("inner final claim does not match opened witness row"); assert!(matches!( diff --git a/crates/jolt-blindfold/tests/assignment_pipeline.rs b/crates/jolt-blindfold/tests/assignment_pipeline.rs index 77bb086657..07610fa690 100644 --- a/crates/jolt-blindfold/tests/assignment_pipeline.rs +++ b/crates/jolt-blindfold/tests/assignment_pipeline.rs @@ -4,215 +4,46 @@ //! against the verifier on witnesses assembled from committed sumcheck data //! and the protocol's public parts alone. -#![expect(clippy::expect_used, reason = "integration tests should fail loudly")] +#![expect( + clippy::expect_used, + clippy::indexing_slicing, + reason = "integration tests should fail loudly" +)] mod support; -use jolt_blindfold::{BlindFoldProtocol, BlindFoldWitness, ProverError}; -use jolt_claims::{constant, opening}; -use jolt_crypto::{Bn254G1, JoltGroup, PedersenSetup, VectorCommitment}; -use jolt_sumcheck::{CommittedSumcheckWitness, SumcheckDomainSpec, SumcheckStatement}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_blindfold::{AssignedBlindFoldWitness, ProverError}; +use jolt_crypto::JoltGroup; +use jolt_sumcheck::{CommittedSumcheckWitness, SumcheckDomainSpec}; use rand_chacha::ChaCha20Rng; -use rand_core::{RngCore, SeedableRng}; +use rand_core::SeedableRng; use support::*; -const TRANSCRIPT_LABEL: &[u8] = b"assignment-backed-blindfold"; - -fn committed_witness(stage: &GeneratedStage) -> CommittedSumcheckWitness { - CommittedSumcheckWitness { - round_coefficients: stage.coefficients.clone(), - round_blindings: stage.blindings.clone(), - output_claim_rows: stage.output_claim_rows.clone(), - output_claim_blindings: stage.output_claim_blindings.clone(), - } -} - -struct AssignmentFixture { - setup: PedersenSetup, - protocol: BlindFoldProtocol, - stage_witnesses: Vec>, - eval_outputs: Vec, - eval_blindings: Vec, -} - const STAGE_DOMAINS: [SumcheckDomainSpec; 2] = [ SumcheckDomainSpec::BooleanHypercube, SumcheckDomainSpec::BooleanHypercube, ]; -/// The two-stage committed pipeline of the proof tests, rebuilt through the -/// public builder: constant claim expressions, one final opening bound to -/// the first output claim. -fn assignment_fixture(rng: &mut impl RngCore) -> AssignmentFixture { - let setup = pedersen_setup(4); - let statement1 = SumcheckStatement::new(3, 3); - let statement2 = SumcheckStatement::new(2, 3); - let input1 = f(37); - let input2 = f(89); - - let (stage1, stage2) = { - let mut prover = SumcheckTestProver::new(&mut *rng); - let mut transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - let stage1 = - prover.prove_stage_with_output_claims(&setup, &mut transcript, statement1, input1, 2); - let stage2 = - prover.prove_stage_with_output_claims(&setup, &mut transcript, statement2, input2, 1); - (stage1, stage2) - }; - let stage1_output = *stage1.claim_outs.last().expect("stage has rounds"); - let stage2_output = *stage2.claim_outs.last().expect("stage has rounds"); - let eval_outputs = vec![stage1.output_claim_rows[0][0]]; - let eval_blindings = vec![rng_field(rng)]; - let eval_commitment = VC::commit(&setup, &[eval_outputs[0]], &eval_blindings[0]); - - let mut transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - let stage1_consistency = stage1 - .proof - .verify_committed_consistency(statement1, &mut transcript) - .expect("stage 1 committed proof transcript verifies"); - let stage2_consistency = stage2 - .proof - .verify_committed_consistency(statement2, &mut transcript) - .expect("stage 2 committed proof transcript verifies"); - - let protocol = BlindFoldProtocol::::builder::() - .stage("assignment-stage-1") - .sumcheck(statement1) - .domain(SumcheckDomainSpec::BooleanHypercube) - .consistency(stage1_consistency) - .output_claim_rows( - (0..stage1.proof.output_claims.commitments.len() * (statement1.degree + 1)).collect(), - statement1.degree + 1, - stage1.proof.output_claims.clone(), - ) - .input_claim(constant(input1)) - .output_claim(constant(stage1_output)) - .finish_stage() - .expect("stage 1 is complete") - .stage("assignment-stage-2") - .sumcheck(statement2) - .domain(SumcheckDomainSpec::BooleanHypercube) - .consistency(stage2_consistency) - .output_claim_rows( - (100..100 + stage2.proof.output_claims.commitments.len() * (statement2.degree + 1)) - .collect(), - statement2.degree + 1, - stage2.proof.output_claims.clone(), - ) - .input_claim(constant(input2)) - .output_claim(constant(stage2_output)) - .finish_stage() - .expect("stage 2 is complete") - .final_opening(vec![0usize], vec![f(1)], eval_commitment) - .build() - .expect("protocol builds"); - - AssignmentFixture { - setup, - protocol, - stage_witnesses: vec![committed_witness(&stage1), committed_witness(&stage2)], - eval_outputs, - eval_blindings, - } -} - -/// A product-bearing variant of [`assignment_fixture`]: stage 2's input -/// claim is the product of two stage-1 output-claim openings, so the claim -/// lowering allocates a product auxiliary and `assign_witness` must solve it -/// (the constant-claim fixture above lowers to purely linear constraints and -/// never exercises the solver). -fn product_assignment_fixture(rng: &mut impl RngCore) -> AssignmentFixture { - let setup = pedersen_setup(4); - let statement1 = SumcheckStatement::new(3, 3); - let statement2 = SumcheckStatement::new(2, 3); - let input1 = f(37); - - let (stage1, stage2) = { - let mut prover = SumcheckTestProver::new(&mut *rng); - let mut transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - let stage1 = - prover.prove_stage_with_output_claims(&setup, &mut transcript, statement1, input1, 2); - // Stage 2 opens on the product of two stage-1 output-claim entries — - // the claim-binding shape that forces a product auxiliary. - let input2 = stage1.output_claim_rows[0][0] * stage1.output_claim_rows[0][1]; - let stage2 = - prover.prove_stage_with_output_claims(&setup, &mut transcript, statement2, input2, 1); - (stage1, stage2) - }; - let stage1_output = *stage1.claim_outs.last().expect("stage has rounds"); - let stage2_output = *stage2.claim_outs.last().expect("stage has rounds"); - let eval_outputs = vec![stage1.output_claim_rows[0][0]]; - let eval_blindings = vec![rng_field(rng)]; - let eval_commitment = VC::commit(&setup, &[eval_outputs[0]], &eval_blindings[0]); - - let mut transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - let stage1_consistency = stage1 - .proof - .verify_committed_consistency(statement1, &mut transcript) - .expect("stage 1 committed proof transcript verifies"); - let stage2_consistency = stage2 - .proof - .verify_committed_consistency(statement2, &mut transcript) - .expect("stage 2 committed proof transcript verifies"); - - let protocol = BlindFoldProtocol::::builder::() - .stage("product-stage-1") - .sumcheck(statement1) - .domain(SumcheckDomainSpec::BooleanHypercube) - .consistency(stage1_consistency) - .output_claim_rows( - (0..stage1.proof.output_claims.commitments.len() * (statement1.degree + 1)).collect(), - statement1.degree + 1, - stage1.proof.output_claims.clone(), - ) - .input_claim(constant(input1)) - .output_claim(constant(stage1_output)) - .finish_stage() - .expect("stage 1 is complete") - .stage("product-stage-2") - .sumcheck(statement2) - .domain(SumcheckDomainSpec::BooleanHypercube) - .consistency(stage2_consistency) - .output_claim_rows( - (100..100 + stage2.proof.output_claims.commitments.len() * (statement2.degree + 1)) - .collect(), - statement2.degree + 1, - stage2.proof.output_claims.clone(), - ) - .input_claim(opening(0usize) * opening(1usize)) - .output_claim(constant(stage2_output)) - .finish_stage() - .expect("stage 2 is complete") - .final_opening(vec![0usize], vec![f(1)], eval_commitment) - .build() - .expect("protocol builds"); - - AssignmentFixture { - setup, - protocol, - stage_witnesses: vec![committed_witness(&stage1), committed_witness(&stage2)], - eval_outputs, - eval_blindings, - } +fn assign( + fixture: &TwoStageFixture, + stage_witnesses: &[&CommittedSumcheckWitness], + rng: &mut ChaCha20Rng, +) -> Result, ProverError> { + fixture.protocol.assign_witness( + &STAGE_DOMAINS, + stage_witnesses, + &fixture.eval_outputs, + &fixture.eval_blindings, + rng, + ) } #[test] fn assigned_witness_proves_and_verifies_through_the_real_prover() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AB); - let fixture = assignment_fixture(&mut rng); - let stage_refs: Vec<&CommittedSumcheckWitness> = fixture.stage_witnesses.iter().collect(); + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); - let assigned = fixture - .protocol - .assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, - &mut rng, - ) - .expect("witness assigns"); + let assigned = assign(&fixture, &fixture.stage_witnesses(), &mut rng).expect("witness assigns"); let dimensions = &fixture.protocol.dimensions; assert_eq!(assigned.rows.len(), dimensions.witness.row_count); assert_eq!(assigned.blindings.len(), dimensions.witness.row_count); @@ -221,89 +52,39 @@ fn assigned_witness_proves_and_verifies_through_the_real_prover() { .iter() .all(|row| row.len() == dimensions.witness.row_len)); - let mut prover_transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&fixture.protocol, &mut prover_transcript); - let proof = jolt_blindfold::prove::( - &fixture.setup, - &fixture.protocol, - &mut prover_transcript, - BlindFoldWitness { - rows: &assigned.rows, - blindings: &assigned.blindings, - eval_outputs: &fixture.eval_outputs, - eval_blindings: &fixture.eval_blindings, - }, - &mut rng, - ) - .expect("assigned witness proves"); - - let mut verifier_transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&fixture.protocol, &mut verifier_transcript); + let narg = fixture + .prove(&assigned.rows, &assigned.blindings, &mut rng) + .expect("assigned witness proves"); fixture - .protocol - .verify::(&proof, &fixture.setup, &mut verifier_transcript) + .verify(&narg) .expect("assigned-witness proof verifies"); } #[test] fn product_auxiliaries_solve_and_prove_through_the_real_prover() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AE); - let fixture = product_assignment_fixture(&mut rng); - let stage_refs: Vec<&CommittedSumcheckWitness> = fixture.stage_witnesses.iter().collect(); + let fixture = two_stage_fixture(&mut rng, Stage2Input::ProductOfStage1Openings); // The point of the fixture: the claim lowering allocated at least one // product auxiliary, so `assign_witness` runs the solver. assert!(fixture.protocol.dimensions.auxiliary_values > 0); - let assigned = fixture - .protocol - .assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, - &mut rng, - ) - .expect("product witness assigns"); - - let mut prover_transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&fixture.protocol, &mut prover_transcript); - let proof = jolt_blindfold::prove::( - &fixture.setup, - &fixture.protocol, - &mut prover_transcript, - BlindFoldWitness { - rows: &assigned.rows, - blindings: &assigned.blindings, - eval_outputs: &fixture.eval_outputs, - eval_blindings: &fixture.eval_blindings, - }, - &mut rng, - ) - .expect("product witness proves"); - - let mut verifier_transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&fixture.protocol, &mut verifier_transcript); + let assigned = + assign(&fixture, &fixture.stage_witnesses(), &mut rng).expect("product witness assigns"); + let narg = fixture + .prove(&assigned.rows, &assigned.blindings, &mut rng) + .expect("product witness proves"); fixture - .protocol - .verify::(&proof, &fixture.setup, &mut verifier_transcript) + .verify(&narg) .expect("product-auxiliary proof verifies"); } #[test] fn assign_witness_rejects_stage_count_mismatch() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AC); - let fixture = assignment_fixture(&mut rng); - let stage_refs: Vec<&CommittedSumcheckWitness> = - fixture.stage_witnesses.iter().take(1).collect(); + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); - let result = fixture.protocol.assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, - &mut rng, - ); + let result = assign(&fixture, &fixture.stage_witnesses()[..1], &mut rng); assert!(matches!( result, Err(ProverError::LengthMismatch { @@ -316,17 +97,14 @@ fn assign_witness_rejects_stage_count_mismatch() { #[test] fn assign_witness_rejects_round_shape_mismatch() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AD); - let fixture = assignment_fixture(&mut rng); - let mut truncated = fixture.stage_witnesses.clone(); - let _ = truncated[0].round_coefficients.pop(); - let _ = truncated[0].round_blindings.pop(); - let stage_refs: Vec<&CommittedSumcheckWitness> = truncated.iter().collect(); - - let result = fixture.protocol.assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); + let mut truncated = fixture.stage_witnesses()[0].clone(); + let _ = truncated.round_coefficients.pop(); + let _ = truncated.round_blindings.pop(); + + let result = assign( + &fixture, + &[&truncated, fixture.stage_witnesses()[1]], &mut rng, ); assert!(matches!( @@ -342,16 +120,13 @@ fn assign_witness_rejects_round_shape_mismatch() { #[test] fn assign_witness_rejects_round_blinding_count_mismatch() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AE); - let fixture = assignment_fixture(&mut rng); - let mut truncated = fixture.stage_witnesses.clone(); - let _ = truncated[0].round_blindings.pop(); - let stage_refs: Vec<&CommittedSumcheckWitness> = truncated.iter().collect(); + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); + let mut truncated = fixture.stage_witnesses()[0].clone(); + let _ = truncated.round_blindings.pop(); - let result = fixture.protocol.assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, + let result = assign( + &fixture, + &[&truncated, fixture.stage_witnesses()[1]], &mut rng, ); assert!(matches!( @@ -367,22 +142,19 @@ fn assign_witness_rejects_round_blinding_count_mismatch() { #[test] fn assign_witness_rejects_output_claim_blinding_count_mismatch() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AF); - let fixture = assignment_fixture(&mut rng); - let mut extended = fixture.stage_witnesses.clone(); + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); + let mut extended = fixture.stage_witnesses()[0].clone(); // A surplus blind is the silent-truncation direction of the old bug. - let surplus = extended[0] + let surplus = extended .output_claim_blindings .first() .copied() .expect("fixture stages carry output-claim blinds"); - extended[0].output_claim_blindings.push(surplus); - let stage_refs: Vec<&CommittedSumcheckWitness> = extended.iter().collect(); + extended.output_claim_blindings.push(surplus); - let result = fixture.protocol.assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, + let result = assign( + &fixture, + &[&extended, fixture.stage_witnesses()[1]], &mut rng, ); assert!(matches!( @@ -403,34 +175,12 @@ fn assign_witness_rejects_output_claim_blinding_count_mismatch() { #[test] fn final_opening_rows_stay_hidden_from_public_proof_data() { let mut rng = ChaCha20Rng::seed_from_u64(0x00C0_57AC); - let fixture = assignment_fixture(&mut rng); - let stage_refs: Vec<&CommittedSumcheckWitness> = fixture.stage_witnesses.iter().collect(); - let assigned = fixture - .protocol - .assign_witness( - &STAGE_DOMAINS, - &stage_refs, - &fixture.eval_outputs, - &fixture.eval_blindings, - &mut rng, - ) - .expect("witness assigns"); - - let mut prover_transcript = Blake2bTranscript::::new(TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&fixture.protocol, &mut prover_transcript); - let proof = jolt_blindfold::prove::( - &fixture.setup, - &fixture.protocol, - &mut prover_transcript, - BlindFoldWitness { - rows: &assigned.rows, - blindings: &assigned.blindings, - eval_outputs: &fixture.eval_outputs, - eval_blindings: &fixture.eval_blindings, - }, - &mut rng, - ) - .expect("assigned witness proves"); + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); + let assigned = assign(&fixture, &fixture.stage_witnesses(), &mut rng).expect("witness assigns"); + let narg = fixture + .prove(&assigned.rows, &assigned.blindings, &mut rng) + .expect("assigned witness proves"); + let messages = fixture.messages(&narg); let g0 = fixture.setup.message_generators[0]; let h = fixture.setup.blinding_generator; @@ -441,8 +191,8 @@ fn final_opening_rows_stay_hidden_from_public_proof_data() { .expect("final opening coordinates")[0]; let eval_row = coordinates.evaluation.expect("evaluation row").row; - let real_eval_row = proof.auxiliary_row_commitments[eval_row - aux_start]; - let folded_eval_blinding = proof.folded_eval_output_openings[0].combined_blinding; + let real_eval_row = messages.auxiliary_rows[eval_row - aux_start]; + let folded_eval_blinding = messages.eval_output_openings[0].combined_blinding; assert_ne!( real_eval_row - h.scalar_mul(&folded_eval_blinding), g0.scalar_mul(&fixture.eval_outputs[0]), @@ -450,8 +200,8 @@ fn final_opening_rows_stay_hidden_from_public_proof_data() { ); let blinding_row = coordinates.blinding.expect("blinding row").row; - let real_blinding_row = proof.auxiliary_row_commitments[blinding_row - aux_start]; - let folded_blinding_blinding = proof.folded_eval_blinding_openings[0].combined_blinding; + let real_blinding_row = messages.auxiliary_rows[blinding_row - aux_start]; + let folded_blinding_blinding = messages.eval_blinding_openings[0].combined_blinding; assert_ne!( real_blinding_row - h.scalar_mul(&folded_blinding_blinding), g0.scalar_mul(&fixture.eval_blindings[0]), diff --git a/crates/jolt-blindfold/tests/proof_pipeline.rs b/crates/jolt-blindfold/tests/proof_pipeline.rs index af1e3e6058..d8402aeedd 100644 --- a/crates/jolt-blindfold/tests/proof_pipeline.rs +++ b/crates/jolt-blindfold/tests/proof_pipeline.rs @@ -1,37 +1,33 @@ -#![expect(clippy::expect_used, reason = "integration tests should fail loudly")] +#![expect( + clippy::expect_used, + clippy::indexing_slicing, + reason = "integration tests should fail loudly" +)] mod support; -use jolt_blindfold::VerificationError; -use jolt_poly::CompressedPoly; -use jolt_transcript::{Blake2bTranscript, Transcript}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; use support::*; -fn verify_blindfold_protocol_pipeline( - full: &BlindFoldTestProof, -) -> Result<(), VerificationError> { - let mut transcript = Blake2bTranscript::::new(b"protocol-backed-blindfold-proof"); - append_protocol_transcript_prefix(&full.protocol, &mut transcript); - full.protocol - .verify::(&full.proof, &full.setup, &mut transcript) -} - #[test] fn blindfold_protocol_pipeline_verifies_committed_sumcheck_outputs_and_eval_commitments() { let mut rng = ChaCha20Rng::from_seed([81; 32]); let full = prove_blindfold_protocol_pipeline(&mut rng); + let protocol = &full.instance.fixture.protocol; - assert!(full.protocol.dimensions.coefficient_rows > 0); - assert!(full.protocol.dimensions.output_claim_rows > 0); - assert!(full.protocol.dimensions.auxiliary_rows > 0); - assert!(!full.protocol.eval_commitments.is_empty()); - verify_blindfold_protocol_pipeline(&full).expect("protocol-backed BlindFold proof verifies"); + assert!(protocol.dimensions.coefficient_rows > 0); + assert!(protocol.dimensions.output_claim_rows > 0); + assert!(protocol.dimensions.auxiliary_rows > 0); + assert!(!protocol.eval_commitments.is_empty()); + full.instance + .fixture + .verify(&full.narg) + .expect("reference prover's BlindFold proof verifies"); } #[test] -fn blindfold_protocol_pipeline_randomness_is_empirically_independent() { +fn blindfold_proof_randomness_is_empirically_independent() { const SAMPLES: usize = 128; let mut rng = ChaCha20Rng::from_seed([61; 32]); let mut projections = [ @@ -47,32 +43,27 @@ fn blindfold_protocol_pipeline_randomness_is_empirically_independent() { ]; for _ in 0..SAMPLES { - let full = prove_blindfold_protocol_pipeline(&mut rng); - - verify_blindfold_protocol_pipeline(&full).expect("sample proof verifies"); + let instance = build_protocol_backed_instance(&mut rng); + let narg = instance.prove_real(&mut rng).expect("real prover succeeds"); + instance + .fixture + .verify(&narg) + .expect("sample proof verifies"); + let messages = instance.fixture.messages(&narg); let values = [ - field_low_u64(full.proof.random_u), - transcript_projection( - b"auxiliary_commitment", - &full.proof.auxiliary_row_commitments[0], - ), - transcript_projection( - b"random_round_commitment", - &full.proof.random_round_commitments[0], - ), - transcript_projection( - b"random_error_commitment", - &full.proof.random_error_row_commitments[0], - ), - transcript_projection( + field_low_u64(messages.random_u), + projection(b"auxiliary_commitment", &messages.auxiliary_rows[..1]), + projection(b"random_round_commitment", &messages.random_rounds[..1]), + projection(b"random_error_commitment", &messages.random_error_rows[..1]), + projection( b"cross_term_commitment", - &full.proof.cross_term_error_row_commitments[0], + &messages.cross_term_error_rows[..1], ), - compressed_sumcheck_projection(b"outer_sumcheck", &full.proof.outer_sumcheck), - compressed_sumcheck_projection(b"inner_sumcheck", &full.proof.inner_sumcheck), - opening_projection(b"witness_opening", &full.proof.witness_opening), - opening_projection(b"error_opening", &full.proof.error_opening), + projection(b"outer_sumcheck", &messages.outer_rounds), + projection(b"inner_sumcheck", &messages.inner_rounds), + opening_projection(b"witness_opening", &messages.witness_opening), + opening_projection(b"error_opening", &messages.error_opening), ]; for (projection, value) in projections.iter_mut().zip(values) { @@ -92,139 +83,10 @@ fn blindfold_protocol_pipeline_randomness_is_empirically_independent() { } #[test] -fn blindfold_protocol_pipeline_rejects_tampered_random_u() { - let mut rng = ChaCha20Rng::from_seed([71; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.random_u += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_outer_sumcheck() { - let mut rng = ChaCha20Rng::from_seed([72; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - let mut coefficients = full.proof.outer_sumcheck.round_polynomials[0] - .coeffs_except_linear_term() - .to_vec(); - coefficients[0] += f(1); - full.proof.outer_sumcheck.round_polynomials[0] = CompressedPoly::new(coefficients); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_folded_matrix_eval() { - let mut rng = ChaCha20Rng::from_seed([73; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.az_rx += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_witness_opening() { - let mut rng = ChaCha20Rng::from_seed([74; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.witness_opening.combined_vector[0] += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_error_opening_blinding() { - let mut rng = ChaCha20Rng::from_seed([75; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.error_opening.combined_blinding += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_wrong_transcript() { - let mut rng = ChaCha20Rng::from_seed([76; 32]); - let full = prove_blindfold_protocol_pipeline(&mut rng); - let mut transcript = Blake2bTranscript::::new(b"wrong-transcript"); - append_protocol_transcript_prefix(&full.protocol, &mut transcript); - - assert!(full - .protocol - .verify::(&full.proof, &full.setup, &mut transcript) - .is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_random_commitment_row() { - let mut rng = ChaCha20Rng::from_seed([77; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.random_round_commitments.swap(0, 1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_inner_sumcheck() { - let mut rng = ChaCha20Rng::from_seed([78; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - let mut coefficients = full.proof.inner_sumcheck.round_polynomials[1] - .coeffs_except_linear_term() - .to_vec(); - coefficients[0] += f(1); - full.proof.inner_sumcheck.round_polynomials[1] = CompressedPoly::new(coefficients); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_truncated_error_rows_before_opening_checks() { - let mut rng = ChaCha20Rng::from_seed([79; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - let _ = full.proof.random_error_row_commitments.pop(); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_opening() { - let mut rng = ChaCha20Rng::from_seed([82; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.folded_eval_outputs[0] += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_blinding() { - let mut rng = ChaCha20Rng::from_seed([87; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.folded_eval_blindings[0] += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_witness_opening() { - let mut rng = ChaCha20Rng::from_seed([85; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.folded_eval_output_openings[0].combined_vector[0] += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_folded_eval_blinding_witness_opening() { - let mut rng = ChaCha20Rng::from_seed([86; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.folded_eval_blinding_openings[0].combined_vector[0] += f(1); - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_final_eval_openings_use_dedicated_rows() { - let mut rng = ChaCha20Rng::from_seed([88; 32]); - let full = prove_blindfold_protocol_pipeline(&mut rng); - let coordinates = full +fn final_eval_openings_use_dedicated_rows() { + let mut rng = ChaCha20Rng::from_seed([110; 32]); + let fixture = two_stage_fixture(&mut rng, Stage2Input::Constant); + let coordinates = fixture .protocol .final_opening_witness_coordinates() .expect("final opening coordinates are valid"); @@ -238,41 +100,4 @@ fn blindfold_protocol_pipeline_final_eval_openings_use_dedicated_rows() { assert_eq!(eval.column, 0); assert_eq!(blinding.column, 0); assert_ne!(eval.row, blinding.row); - assert_eq!( - full.proof.folded_eval_output_openings[0].combined_vector[0], - full.proof.folded_eval_outputs[0] - ); - assert_eq!( - full.proof.folded_eval_blinding_openings[0].combined_vector[0], - full.proof.folded_eval_blindings[0] - ); - assert!( - full.proof.folded_eval_output_openings[0].combined_vector[1..] - .iter() - .all(|value| *value == f(0)) - ); - assert!( - full.proof.folded_eval_blinding_openings[0].combined_vector[1..] - .iter() - .all(|value| *value == f(0)) - ); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_auxiliary_commitment() { - let mut rng = ChaCha20Rng::from_seed([83; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.proof.auxiliary_row_commitments[0] = full.proof.random_round_commitments[0]; - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); -} - -#[test] -fn blindfold_protocol_pipeline_rejects_tampered_output_claim_row_commitment() { - let mut rng = ChaCha20Rng::from_seed([84; 32]); - let mut full = prove_blindfold_protocol_pipeline(&mut rng); - full.protocol.committed_output_claims[0].commitments[0] = - full.proof.random_round_commitments[0]; - - assert!(verify_blindfold_protocol_pipeline(&full).is_err()); } diff --git a/crates/jolt-blindfold/tests/r1cs_pipeline.rs b/crates/jolt-blindfold/tests/r1cs_pipeline.rs index 328da211c8..3ab03c2df7 100644 --- a/crates/jolt-blindfold/tests/r1cs_pipeline.rs +++ b/crates/jolt-blindfold/tests/r1cs_pipeline.rs @@ -17,7 +17,7 @@ fn r1cs_pipeline_rejects_late_stage_mismatch() { let mut prover = SumcheckTestProver::new(ChaCha20Rng::from_seed([6; 32])); let (stage1, stage2, stage3, values) = generated_deep_triple(&mut prover); let mut tampered_stage3 = stage3.clone(); - tampered_stage3.proof = stage2.proof.clone(); + tampered_stage3.consistency = stage2.consistency.clone(); assert!(build_deep_relation(&stage1, &stage2, &tampered_stage3, &values).is_err()); } diff --git a/crates/jolt-blindfold/tests/real_prover.rs b/crates/jolt-blindfold/tests/real_prover.rs index 41974aa80e..945ab33fcf 100644 --- a/crates/jolt-blindfold/tests/real_prover.rs +++ b/crates/jolt-blindfold/tests/real_prover.rs @@ -1,100 +1,53 @@ //! Drives the shipped prover (`jolt_blindfold::prove`) end-to-end against the -//! real verifier. The rest of the suite exercises the verifier via the test -//! harness's reference prover; these tests are what tie the production -//! folding/Spartan prover itself to the verifier's acceptance criteria. +//! real verifier, over the committed-stage messages that precede BlindFold in +//! the same proof. -#![expect(clippy::expect_used, reason = "integration tests should fail loudly")] +#![expect( + clippy::expect_used, + clippy::indexing_slicing, + reason = "integration tests should fail loudly" +)] mod support; -use jolt_blindfold::{prove, BlindFoldProof, BlindFoldWitness, ProverError, VerificationError}; -use jolt_crypto::Bn254G1; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_blindfold::{ProverError, VerificationError}; +use jolt_transcript::TranscriptError; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; use support::*; -fn prove_real( - instance: &ProtocolBackedInstance, - rng: &mut ChaCha20Rng, -) -> Result, ProverError> { - let mut transcript = Blake2bTranscript::::new(PROTOCOL_BACKED_TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&instance.protocol, &mut transcript); - prove::( - &instance.setup, - &instance.protocol, - &mut transcript, - BlindFoldWitness { - rows: &instance.rows, - blindings: &instance.blindings, - eval_outputs: &instance.eval_outputs, - eval_blindings: &instance.eval_blindings, - }, - rng, - ) -} - -fn verify_real( - instance: &ProtocolBackedInstance, - proof: &BlindFoldProof, -) -> Result<(), VerificationError> { - let mut transcript = Blake2bTranscript::::new(PROTOCOL_BACKED_TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&instance.protocol, &mut transcript); - instance - .protocol - .verify::(proof, &instance.setup, &mut transcript) +fn honest_proof(seed: u8) -> (ProtocolBackedInstance, Vec) { + let mut rng = ChaCha20Rng::from_seed([seed; 32]); + let instance = build_protocol_backed_instance(&mut rng); + let narg = instance + .prove_real(&mut rng) + .expect("real prover succeeds on a valid witness"); + (instance, narg) } #[test] fn real_prover_roundtrip_verifies() { - let mut rng = ChaCha20Rng::from_seed([101; 32]); - let instance = build_protocol_backed_instance(&mut rng); - let proof = prove_real(&instance, &mut rng).expect("real prover succeeds on a valid witness"); - verify_real(&instance, &proof).expect("real prover's proof verifies"); + let (instance, narg) = honest_proof(101); + instance + .fixture + .verify(&narg) + .expect("real prover's proof verifies"); } +/// Two final-opening bindings: the prover must send each binding's (output, +/// blinding) openings in the order the verifier reads them. #[test] fn real_prover_roundtrip_verifies_with_two_final_openings() { let mut rng = ChaCha20Rng::from_seed([107; 32]); let instance = build_protocol_backed_instance_with_bindings(&mut rng, 2); - assert_eq!(instance.protocol.eval_commitments.len(), 2); - let proof = prove_real(&instance, &mut rng).expect("real prover succeeds on a valid witness"); - verify_real(&instance, &proof).expect("real prover's proof verifies with two bindings"); -} - -#[test] -fn real_prover_proof_shape_matches_harness_prover() { - let mut rng = ChaCha20Rng::from_seed([102; 32]); - let instance = build_protocol_backed_instance(&mut rng); - let real = prove_real(&instance, &mut rng).expect("real prover succeeds"); - - let harness = prove_blindfold_protocol_pipeline(&mut ChaCha20Rng::from_seed([102; 32])); - - assert_eq!( - real.outer_sumcheck.round_polynomials.len(), - harness.proof.outer_sumcheck.round_polynomials.len(), - "outer sumcheck round count diverges from the reference prover" - ); - assert_eq!( - real.inner_sumcheck.round_polynomials.len(), - harness.proof.inner_sumcheck.round_polynomials.len(), - "inner sumcheck round count diverges from the reference prover" - ); - assert_eq!( - real.auxiliary_row_commitments.len(), - harness.proof.auxiliary_row_commitments.len(), - "auxiliary row commitment count diverges from the reference prover" - ); - assert_eq!( - real.cross_term_error_row_commitments.len(), - harness.proof.cross_term_error_row_commitments.len(), - "cross-term error row count diverges from the reference prover" - ); - assert_eq!( - real.random_round_commitments.len(), - harness.proof.random_round_commitments.len(), - "random round commitment count diverges from the reference prover" - ); + assert_eq!(instance.fixture.protocol.eval_commitments.len(), 2); + let narg = instance + .prove_real(&mut rng) + .expect("real prover succeeds on a valid witness"); + instance + .fixture + .verify(&narg) + .expect("real prover's proof verifies with two bindings"); } #[test] @@ -103,7 +56,9 @@ fn real_prover_rejects_missing_witness_row() { let mut instance = build_protocol_backed_instance(&mut rng); let _ = instance.rows.pop(); - let err = prove_real(&instance, &mut rng).expect_err("row count mismatch must be rejected"); + let err = instance + .prove_real(&mut rng) + .expect_err("row count mismatch must be rejected"); assert!( matches!( err, @@ -122,7 +77,9 @@ fn real_prover_rejects_truncated_witness_row() { let mut instance = build_protocol_backed_instance(&mut rng); let _ = instance.rows[0].pop(); - let err = prove_real(&instance, &mut rng).expect_err("row length mismatch must be rejected"); + let err = instance + .prove_real(&mut rng) + .expect_err("row length mismatch must be rejected"); assert!( matches!(err, ProverError::WitnessRowLengthMismatch { row: 0, .. }), "expected witness-row-0 length mismatch, got: {err}" @@ -133,9 +90,10 @@ fn real_prover_rejects_truncated_witness_row() { fn real_prover_rejects_eval_output_not_matching_commitment() { let mut rng = ChaCha20Rng::from_seed([105; 32]); let mut instance = build_protocol_backed_instance(&mut rng); - instance.eval_outputs[0] += f(1); + instance.fixture.eval_outputs[0] += f(1); - let err = prove_real(&instance, &mut rng) + let err = instance + .prove_real(&mut rng) .expect_err("evaluation output inconsistent with its commitment must be rejected"); assert!( matches!(err, ProverError::EvalCommitmentMismatch { index: 0 }), @@ -143,15 +101,66 @@ fn real_prover_rejects_eval_output_not_matching_commitment() { ); } +/// Flips one bit in bytes spread across the whole proof, committed-stage +/// prefix included. The odd stride walks the flip through every position +/// within the fixed-width field and commitment atoms. #[test] -fn real_prover_proof_rejects_tampered_random_u() { - let mut rng = ChaCha20Rng::from_seed([106; 32]); - let instance = build_protocol_backed_instance(&mut rng); - let mut proof = prove_real(&instance, &mut rng).expect("real prover succeeds"); - proof.random_u += f(1); +fn every_sampled_byte_flip_is_rejected() { + const FLIPS: usize = 512; + let (instance, narg) = honest_proof(106); + let stride = (narg.len() / FLIPS) | 1; + + let mut offsets = (0..narg.len()).step_by(stride).collect::>(); + offsets.push(narg.len() - 1); + for offset in offsets { + let mut tampered = narg.clone(); + tampered[offset] ^= 1; + assert!( + instance.fixture.verify(&tampered).is_err(), + "flipping byte {offset} of {} was accepted", + narg.len() + ); + } +} - assert!( - verify_real(&instance, &proof).is_err(), - "tampered random_u in a real prover proof must be rejected" - ); +#[test] +fn rejects_truncated_proof() { + let (instance, narg) = honest_proof(107); + + let error = instance + .fixture + .verify(&narg[..narg.len() - 1]) + .expect_err("truncated proof is rejected"); + + assert!(matches!( + error, + VerificationError::Transcript(TranscriptError::Truncated) + )); +} + +#[test] +fn rejects_trailing_bytes() { + let (instance, mut narg) = honest_proof(108); + narg.push(0); + + let error = instance + .fixture + .verify(&narg) + .expect_err("trailing bytes are rejected"); + + assert!(matches!( + error, + VerificationError::Transcript(TranscriptError::TrailingBytes) + )); +} + +#[test] +fn rejects_proof_under_another_session() { + let (instance, narg) = honest_proof(109); + let fixture = &instance.fixture; + + assert!(fixture + .template + .verify_with_session(&fixture.setup, b"another-session", &narg) + .is_err()); } diff --git a/crates/jolt-blindfold/tests/sumcheck_pipeline.rs b/crates/jolt-blindfold/tests/sumcheck_pipeline.rs index 5baa60f38f..04b4304239 100644 --- a/crates/jolt-blindfold/tests/sumcheck_pipeline.rs +++ b/crates/jolt-blindfold/tests/sumcheck_pipeline.rs @@ -3,8 +3,7 @@ mod support; use jolt_crypto::VectorCommitment; -use jolt_sumcheck::RoundMessage; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Channel, ProverTranscript}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; use support::*; @@ -31,21 +30,21 @@ fn committed_sumcheck_pipeline_satisfies_deep_r1cs_and_randomness_checks() { assert!(build_deep_relation(&stage1, &stage2, &stage3, &values).is_ok()); let sample = [ - transcript_projection( + projection( b"stage1_round_commitment", - &stage1.proof.rounds[0].commitment, + &[stage1.consistency.rounds[0].commitment], ), - transcript_projection( + projection( b"stage2_round_commitment", - &stage2.proof.rounds[0].commitment, + &[stage2.consistency.rounds[0].commitment], ), - transcript_projection( + projection( b"stage3_round_commitment", - &stage3.proof.rounds[0].commitment, + &[stage3.consistency.rounds[0].commitment], ), - field_low_u64(stage1.blindings[0]), - field_low_u64(stage2.blindings[0]), - field_low_u64(stage3.blindings[0]), + field_low_u64(stage1.witness.round_blindings[0]), + field_low_u64(stage2.witness.round_blindings[0]), + field_low_u64(stage3.witness.round_blindings[0]), field_low_u64(*stage1.claim_outs.last().expect("stage 1 output exists")), field_low_u64(*stage2.claim_outs.last().expect("stage 2 output exists")), field_low_u64(*stage3.claim_outs.last().expect("stage 3 output exists")), @@ -79,28 +78,20 @@ fn committed_round_blinding_is_empirically_independent_from_commitments_and_chal for _ in 0..SAMPLES { let blinding = rng_field(&mut rng); - let round = commit_round_with_blinding(&setup, coefficients.clone(), blinding); - let mut transcript = Blake2bTranscript::::new(b"blindfold-r1cs-independence"); - round.append_to_transcript(&mut transcript); - let challenge = transcript.challenge(); + let commitment = VC::commit(&setup, &coefficients, &blinding); + let mut transcript = ProverTranscript::::new(&PROTOCOL, b"blindfold-r1cs-independence"); + transcript.send(&commitment); + let challenge: F = transcript.challenge_small(); - assert!(VC::verify( - &setup, - &round.commitment, - &coefficients, - &blinding - )); + assert!(VC::verify(&setup, &commitment, &coefficients, &blinding)); assert!(!VC::verify( &setup, - &round.commitment, + &commitment, &coefficients, &(blinding + f(1)) )); blindings.push(field_low_u64(blinding)); - commitments.push(transcript_projection( - b"round_commitment", - &round.commitment, - )); + commitments.push(projection(b"round_commitment", &[commitment])); challenges.push(field_low_u64(challenge)); } diff --git a/crates/jolt-blindfold/tests/support/mod.rs b/crates/jolt-blindfold/tests/support/mod.rs index 571fb9ce08..60a6159cc2 100644 --- a/crates/jolt-blindfold/tests/support/mod.rs +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -5,28 +5,40 @@ )] use jolt_blindfold::{ - BlindFoldProof, BlindFoldProtocol, BlindFoldStage, BlindFoldStatement, CommittedClaimRows, - FinalOpeningBinding, WitnessCoordinate, + prove, BlindFoldProtocol, BlindFoldStage, BlindFoldStatement, BlindFoldWitness, + CommittedClaimRows, FinalOpeningBinding, ProverError, RowDimensions, VerificationError, + WitnessCoordinate, }; use jolt_claims::{challenge, constant, derived, opening, Expr}; use jolt_crypto::{ Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup, VectorCommitment, VectorCommitmentOpening, }; use jolt_field::{CanonicalBytes, Field, Fr, Ring}; -use jolt_poly::{CompressedPoly, EqPolynomial}; +use jolt_poly::{EqPolynomial, UnivariatePoly}; use jolt_r1cs::{ClaimSourceTable, ConstraintMatrices, R1csBuilder}; use jolt_sumcheck::{ - CommittedOutputClaims, CommittedRound, CommittedRoundWitness, CommittedSumcheckConsistency, - CommittedSumcheckProof, CompressedSumcheckProof, RoundMessage, SumcheckDomainSpec, - SumcheckR1csLayout, SumcheckStatement, SUMCHECK_ROUND_TRANSCRIPT_LABEL, + CommittedOutputClaims, CommittedSumcheckBuilder, CommittedSumcheckConsistency, + CommittedSumcheckWitness, SumcheckDomainSpec, SumcheckR1csLayout, SumcheckStatement, + SumcheckVerifier, VerifiedCommittedRound, }; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Label, Transcript}; -use rand_core::RngCore; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, TranscriptError, VerifierTranscript, +}; +use rand_chacha::ChaCha20Rng; +use rand_core::{RngCore, SeedableRng}; pub type F = Fr; pub type VC = Pedersen; +pub type H = Blake2b512; pub type TestExpr = Expr; +pub const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-blindfold/tests"); +pub const SESSION: &[u8] = b"blindfold-integration"; + +/// Compressed-round coefficient counts of the folded Spartan sumchecks. +const OUTER_ROUND_LEN: usize = 3; +const INNER_ROUND_LEN: usize = 2; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Opening { Start, @@ -49,14 +61,15 @@ pub enum Challenge { Mix, } +/// One committed sumcheck stage written into a prover transcript, with the +/// commitments and challenges the verifier reads back and the prover-retained +/// openings. #[derive(Clone, Debug)] pub struct GeneratedStage { pub statement: SumcheckStatement, - pub proof: CommittedSumcheckProof, - pub coefficients: Vec>, - pub blindings: Vec, - pub output_claim_rows: Vec>, - pub output_claim_blindings: Vec, + pub consistency: CommittedSumcheckConsistency, + pub output_claims: CommittedOutputClaims, + pub witness: CommittedSumcheckWitness, pub input_claim: F, pub claim_outs: Vec, } @@ -151,37 +164,19 @@ pub fn field_low_u64(value: F) -> u64 { ]) } -pub fn transcript_projection(label: &'static [u8], value: &A) -> u64 { - let mut transcript = Blake2bTranscript::::new(b"blindfold-statistical-projection"); - transcript.append(&Label(label)); - value.append_to_transcript(&mut transcript); - field_low_u64(transcript.challenge()) -} - -pub fn field_slice_projection(label: &'static [u8], values: &[F]) -> u64 { - let mut transcript = Blake2bTranscript::::new(b"blindfold-statistical-projection"); - transcript.append_values(label, values); - field_low_u64(transcript.challenge()) -} - -pub fn compressed_sumcheck_projection( - label: &'static [u8], - proof: &CompressedSumcheckProof, -) -> u64 { - let mut values = Vec::new(); - for round in &proof.round_polynomials { - values.extend_from_slice(round.coeffs_except_linear_term()); - } - field_slice_projection(label, &values) +pub fn projection(label: &'static [u8], values: &[A]) -> u64 { + let mut transcript = ProverTranscript::::new( + &ProtocolId::new::("blindfold-statistical-projection"), + label, + ); + transcript.public_all(values); + u64::from_le_bytes(transcript.challenge_bytes()) } -pub fn opening_projection( - label: &'static [u8], - opening: &jolt_crypto::VectorCommitmentOpening, -) -> u64 { +pub fn opening_projection(label: &'static [u8], opening: &VectorCommitmentOpening) -> u64 { let mut values = opening.combined_vector.clone(); values.push(opening.combined_blinding); - field_slice_projection(label, &values) + projection(label, &values) } pub fn assert_empirical_distribution(projection: &StatisticalProjection) { @@ -338,27 +333,6 @@ pub fn pedersen_setup(capacity: usize) -> PedersenSetup { PedersenSetup::new(message_generators, generator.scalar_mul(&f(99))) } -pub fn commit_round_with_blinding( - setup: &PedersenSetup, - coefficients: Vec, - blinding: F, -) -> CommittedRound { - CommittedRoundWitness { - coefficients, - blinding, - } - .commit::(setup) - .expect("round witness commits") -} - -pub fn commit_round( - setup: &PedersenSetup, - coefficients: Vec, - round: usize, -) -> CommittedRound { - commit_round_with_blinding(setup, coefficients, f(round as u64 + 17)) -} - pub fn coefficients_for_claim_with_rng(claim: F, degree: usize, rng: &mut impl RngCore) -> Vec { let mut coefficients = vec![f(0); degree + 1]; let mut nonconstant_sum = f(0); @@ -383,138 +357,86 @@ impl SumcheckTestProver { pub fn prove_stage( &mut self, setup: &PedersenSetup, - transcript: &mut Blake2bTranscript, + transcript: &mut ProverTranscript, statement: SumcheckStatement, input_claim: F, ) -> GeneratedStage { self.prove_stage_with_output_claims(setup, transcript, statement, input_claim, 0) } + /// Proves a committed stage whose rounds sum to `input_claim`, followed by + /// `output_claim_count` random output-claim rows of `degree + 1` values. pub fn prove_stage_with_output_claims( &mut self, setup: &PedersenSetup, - transcript: &mut Blake2bTranscript, + transcript: &mut ProverTranscript, statement: SumcheckStatement, input_claim: F, output_claim_count: usize, ) -> GeneratedStage { + let blinding_rng = ChaCha20Rng::seed_from_u64(self.rng.next_u64()); + let mut builder = CommittedSumcheckBuilder::::new(setup, blinding_rng) + .expect("setup has commitment capacity"); let mut claim = input_claim; - let mut rounds = Vec::with_capacity(statement.num_vars); - let mut coefficients = Vec::with_capacity(statement.num_vars); - let mut blindings = Vec::with_capacity(statement.num_vars); + let mut challenges = Vec::with_capacity(statement.num_vars); let mut claim_outs = Vec::with_capacity(statement.num_vars); - for _ in 0..statement.num_vars { - let round_coefficients = + let coefficients = coefficients_for_claim_with_rng(claim, statement.degree, &mut self.rng); - let blinding = rng_field(&mut self.rng); - let round = commit_round_with_blinding(setup, round_coefficients.clone(), blinding); - round.append_to_transcript(transcript); - let challenge = transcript.challenge(); - claim = eval_poly(&round_coefficients, challenge); - - rounds.push(round); - coefficients.push(round_coefficients); - blindings.push(blinding); + let challenge = builder + .commit_round( + &UnivariatePoly::new(coefficients.clone()), + statement.degree, + transcript, + ) + .expect("round commits"); + claim = eval_poly(&coefficients, challenge); + challenges.push(challenge); claim_outs.push(claim); } - let mut output_claim_rows = Vec::with_capacity(output_claim_count); - let mut output_claim_blindings = Vec::with_capacity(output_claim_count); - let mut output_commitments = Vec::with_capacity(output_claim_count); - for _ in 0..output_claim_count { - let row = (0..=statement.degree) - .map(|_| rng_field(&mut self.rng)) - .collect::>(); - let blinding = rng_field(&mut self.rng); - output_commitments.push(VC::commit(setup, &row, &blinding)); - output_claim_rows.push(row); - output_claim_blindings.push(blinding); - } + let output_claim_values = (0..output_claim_count * (statement.degree + 1)) + .map(|_| rng_field(&mut self.rng)) + .collect::>(); + let witness = builder + .finish(&output_claim_values, transcript) + .expect("output claims commit"); + assert_eq!(witness.output_claim_rows.len(), output_claim_count); + + let consistency = CommittedSumcheckConsistency { + rounds: witness + .round_coefficients + .iter() + .zip(&witness.round_blindings) + .zip(challenges) + .map( + |((coefficients, blinding), challenge)| VerifiedCommittedRound { + commitment: VC::commit(setup, coefficients, blinding), + degree: statement.degree, + challenge, + }, + ) + .collect(), + }; let output_claims = CommittedOutputClaims { - commitments: output_commitments, + commitments: witness + .output_claim_rows + .iter() + .zip(&witness.output_claim_blindings) + .map(|(row, blinding)| VC::commit(setup, row, blinding)) + .collect(), }; - output_claims.append_to_transcript(transcript); - GeneratedStage { statement, - proof: CommittedSumcheckProof { - rounds, - output_claims, - }, - coefficients, - blindings, - output_claim_rows, - output_claim_blindings, + consistency, + output_claims, + witness, input_claim, claim_outs, } } - - pub fn prove_stage_with_fresh_transcript( - &mut self, - setup: &PedersenSetup, - transcript_label: &'static [u8], - statement: SumcheckStatement, - input_claim: F, - ) -> GeneratedStage { - let mut transcript = Blake2bTranscript::::new(transcript_label); - self.prove_stage(setup, &mut transcript, statement, input_claim) - } -} - -pub fn generate_zero_stage(setup: &PedersenSetup, num_vars: usize) -> GeneratedStage { - let rounds = (0..num_vars) - .map(|round| commit_round(setup, vec![f(0)], round)) - .collect(); - GeneratedStage { - statement: SumcheckStatement::new(num_vars, 1), - proof: CommittedSumcheckProof { - rounds, - output_claims: CommittedOutputClaims::default(), - }, - coefficients: vec![vec![f(0)]; num_vars], - blindings: (0..num_vars).map(|round| f(round as u64 + 17)).collect(), - output_claim_rows: Vec::new(), - output_claim_blindings: Vec::new(), - input_claim: f(0), - claim_outs: vec![f(0); num_vars], - } -} - -pub fn stage_consistency( - statement: SumcheckStatement, - proof: &CommittedSumcheckProof, -) -> CommittedSumcheckConsistency { - let mut transcript = Blake2bTranscript::::new(b"blindfold-r1cs-e2e"); - proof - .verify_committed_consistency(statement, &mut transcript) - .expect("committed proof transcript verifies") -} - -pub fn stage_consistency_for_transcript( - stages: &[&GeneratedStage], -) -> Vec> { - stage_consistency_for_transcript_label(b"blindfold-r1cs-e2e", stages) -} - -pub fn stage_consistency_for_transcript_label( - transcript_label: &'static [u8], - stages: &[&GeneratedStage], -) -> Vec> { - let mut transcript = Blake2bTranscript::::new(transcript_label); - stages - .iter() - .map(|stage| { - stage - .proof - .verify_committed_consistency(stage.statement, &mut transcript) - .expect("committed proof transcript verifies") - }) - .collect() } -pub fn blindfold_statement_for_transcript_label( - transcript_label: &'static [u8], +pub fn blindfold_statement( relations: &[TestStageRelation], stages: &[&GeneratedStage], final_openings: Vec>, @@ -529,24 +451,19 @@ where stages.len(), "relations and generated stages must align" ); - let mut transcript = Blake2bTranscript::::new(transcript_label); let stages = relations .iter() .zip(stages) .map(|(relation, generated)| { - let consistency = generated - .proof - .verify_committed_consistency(generated.statement, &mut transcript) - .expect("committed proof transcript verifies"); BlindFoldStage::new( relation.name.clone(), relation.statement, relation.domain, - consistency, + generated.consistency.clone(), CommittedClaimRows::new( Vec::new(), relation.statement.degree + 1, - generated.proof.output_claims.clone(), + generated.output_claims.clone(), ), relation.input_claim.clone(), relation.output_claim.clone(), @@ -564,11 +481,13 @@ pub fn assign_generated_stage( builder .assign(layout.input_claim, generated.input_claim) .expect("input claim assigns"); - for (round_layout, (round_coefficients, &claim_out)) in layout - .rounds - .iter() - .zip(generated.coefficients.iter().zip(&generated.claim_outs)) - { + for (round_layout, (round_coefficients, &claim_out)) in layout.rounds.iter().zip( + generated + .witness + .round_coefficients + .iter() + .zip(&generated.claim_outs), + ) { for (&variable, &coefficient) in round_layout.coefficients.iter().zip(round_coefficients) { builder .assign(variable, coefficient) @@ -672,12 +591,7 @@ pub fn build_deep_relation( stage3_output, ), ]; - let statement = blindfold_statement_for_transcript_label( - b"blindfold-r1cs-e2e", - &relations, - &[stage1, stage2, stage3], - Vec::new(), - ); + let statement = blindfold_statement(&relations, &[stage1, stage2, stage3], Vec::new()); let mut builder = R1csBuilder::::new(); let mut sources = ClaimSourceTable::::new(); @@ -712,7 +626,7 @@ pub fn generated_deep_triple( let setup = pedersen_setup(4); let statement = SumcheckStatement::new(4, 3); let mut values = deep_values_without_links(); - let mut transcript = Blake2bTranscript::::new(b"blindfold-r1cs-e2e"); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); let stage1 = prover.prove_stage( &setup, &mut transcript, @@ -748,175 +662,425 @@ pub fn generated_deep_triple( (stage1, stage2, stage3, values) } +/// The public description of a committed-stage protocol: everything the +/// verifier needs besides the proof bytes. #[derive(Clone, Debug)] -pub struct BlindFoldTestProof { - pub protocol: BlindFoldProtocol, - pub proof: BlindFoldProof, - pub setup: PedersenSetup, +pub struct StageTemplate { + pub name: &'static str, + pub statement: SumcheckStatement, + pub output_claim_count: usize, + pub opening_ids: Vec, + pub input_claim: Expr, + pub output_claim: Expr, } #[derive(Clone, Debug)] -struct SumcheckTrace { - proof: CompressedSumcheckProof, - point: Vec, +pub struct ProtocolTemplate { + pub stages: Vec, + pub final_openings: Vec>, +} + +impl ProtocolTemplate { + pub fn statement( + &self, + committed: Vec<( + CommittedSumcheckConsistency, + CommittedOutputClaims, + )>, + ) -> BlindFoldStatement { + assert_eq!(self.stages.len(), committed.len()); + let stages = self + .stages + .iter() + .zip(committed) + .map(|(stage, (consistency, output_claims))| { + BlindFoldStage::new( + stage.name, + stage.statement, + SumcheckDomainSpec::BooleanHypercube, + consistency, + CommittedClaimRows::new( + stage.opening_ids.clone(), + stage.statement.degree + 1, + output_claims, + ), + stage.input_claim.clone(), + stage.output_claim.clone(), + ) + }) + .collect(); + BlindFoldStatement::new(stages, self.final_openings.clone()) + } + + /// Verifies a whole proof: reads every committed stage, rebuilds the + /// BlindFold protocol from what was read, verifies BlindFold, and requires + /// that nothing follows. + pub fn verify( + &self, + setup: &PedersenSetup, + narg: &[u8], + ) -> Result<(), VerificationError> { + self.verify_with_session(setup, SESSION, narg) + } + + pub fn verify_with_session( + &self, + setup: &PedersenSetup, + session: &[u8], + narg: &[u8], + ) -> Result<(), VerificationError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, session, narg); + let committed = self + .stages + .iter() + .enumerate() + .map(|(stage_index, stage)| { + SumcheckVerifier::verify_committed( + stage.statement, + stage.output_claim_count, + &mut transcript, + ) + .map_err(|source| VerificationError::Sumcheck { + stage_index, + source, + }) + }) + .collect::, _>>()?; + let protocol = blindfold_protocol_from_statement(&self.statement(committed))?; + protocol.verify::(setup, &mut transcript)?; + Ok(transcript.finish()?) + } } -/// Everything needed to drive a prover (harness or real) over the same -/// protocol-backed instance: the statement-derived protocol plus the real -/// witness rows, blindings, and final-opening evaluations. -pub struct ProtocolBackedInstance { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Stage2Input { + Constant, + /// The product of two stage-1 output-claim openings, which makes the + /// claim lowering allocate a product auxiliary. + ProductOfStage1Openings, +} + +/// Two committed stages already written into `transcript`, one final opening +/// bound to the first stage-1 output-claim value, and the protocol the prover +/// builds from them. +#[derive(Clone)] +pub struct TwoStageFixture { pub setup: PedersenSetup, + pub transcript: ProverTranscript, + pub stages: Vec, + pub template: ProtocolTemplate, + pub statement: BlindFoldStatement, pub protocol: BlindFoldProtocol, - pub rows: Vec>, - pub blindings: Vec, pub eval_outputs: Vec, pub eval_blindings: Vec, } -pub const PROTOCOL_BACKED_TRANSCRIPT_LABEL: &[u8] = b"protocol-backed-blindfold-proof"; +impl TwoStageFixture { + pub fn stage_witnesses(&self) -> Vec<&CommittedSumcheckWitness> { + self.stages.iter().map(|stage| &stage.witness).collect() + } -pub fn build_protocol_backed_instance(rng: &mut R) -> ProtocolBackedInstance { - build_protocol_backed_instance_with_bindings(rng, 1) + /// Runs the shipped BlindFold prover after the committed stages and + /// returns the whole proof. + pub fn prove( + &self, + rows: &[Vec], + blindings: &[F], + rng: &mut impl RngCore, + ) -> Result, ProverError> { + let mut transcript = self.transcript.clone(); + prove::( + &self.setup, + &self.protocol, + &mut transcript, + BlindFoldWitness { + rows, + blindings, + eval_outputs: &self.eval_outputs, + eval_blindings: &self.eval_blindings, + }, + rng, + )?; + Ok(transcript.finish()) + } + + pub fn verify(&self, narg: &[u8]) -> Result<(), VerificationError> { + self.template.verify(&self.setup, narg) + } + + pub fn messages(&self, narg: &[u8]) -> ProofMessages { + ProofMessages::parse(&self.protocol, self.transcript.narg().len(), narg) + .expect("proof parses in transcript order") + } } -/// Like [`build_protocol_backed_instance`], with `binding_count` (1 or 2) -/// final-opening bindings: the second opens stage 2's first output claim. -pub fn build_protocol_backed_instance_with_bindings( +pub fn two_stage_fixture(rng: &mut R, stage2_input: Stage2Input) -> TwoStageFixture { + two_stage_fixture_with_bindings(rng, stage2_input, 1) +} + +/// Like [`two_stage_fixture`], with `binding_count` (1 or 2) final-opening +/// bindings: the second opens stage 2's first output claim. +pub fn two_stage_fixture_with_bindings( rng: &mut R, + stage2_input: Stage2Input, binding_count: usize, -) -> ProtocolBackedInstance { +) -> TwoStageFixture { let setup = pedersen_setup(4); - let transcript_label = PROTOCOL_BACKED_TRANSCRIPT_LABEL; let statement1 = SumcheckStatement::new(3, 3); let statement2 = SumcheckStatement::new(2, 3); let input1 = f(37); - let input2 = f(89); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); - let (stage1, stage2) = { + let (stage1, stage2, input2_claim) = { let mut prover = SumcheckTestProver::new(&mut *rng); - let mut transcript = Blake2bTranscript::::new(transcript_label); let stage1 = prover.prove_stage_with_output_claims(&setup, &mut transcript, statement1, input1, 2); + let (input2, input2_claim) = match stage2_input { + Stage2Input::Constant => (f(89), constant(f(89))), + Stage2Input::ProductOfStage1Openings => { + let row = &stage1.witness.output_claim_rows[0]; + (row[0] * row[1], opening(0usize) * opening(1usize)) + } + }; let stage2 = prover.prove_stage_with_output_claims(&setup, &mut transcript, statement2, input2, 1); - (stage1, stage2) + (stage1, stage2, input2_claim) }; - let stage1_output = *stage1 - .claim_outs - .last() - .expect("stage has at least one round"); - let stage2_output = *stage2 - .claim_outs - .last() - .expect("stage has at least one round"); - let mut real_eval_outputs = vec![stage1.output_claim_rows[0][0]]; + let mut eval_outputs = vec![stage1.witness.output_claim_rows[0][0]]; if binding_count == 2 { - real_eval_outputs.push(stage2.output_claim_rows[0][0]); + eval_outputs.push(stage2.witness.output_claim_rows[0][0]); } - let real_eval_blindings = real_eval_outputs + let eval_blindings: Vec = eval_outputs.iter().map(|_| rng_field(rng)).collect(); + let eval_commitments: Vec<_> = eval_outputs .iter() - .map(|_| rng_field(rng)) - .collect::>(); - let eval_commitments = real_eval_outputs - .iter() - .zip(&real_eval_blindings) + .zip(&eval_blindings) .map(|(&output, blinding)| VC::commit(&setup, &[output], blinding)) - .collect::>(); + .collect(); - let mut transcript = Blake2bTranscript::::new(transcript_label); - let stage1_consistency = stage1 - .proof - .verify_committed_consistency(stage1.statement, &mut transcript) - .expect("stage 1 committed proof transcript verifies"); - let stage2_consistency = stage2 - .proof - .verify_committed_consistency(stage2.statement, &mut transcript) - .expect("stage 2 committed proof transcript verifies"); - let stages = vec![ - BlindFoldStage::new( - "protocol-backed-stage-1", - statement1, - SumcheckDomainSpec::BooleanHypercube, - stage1_consistency, - CommittedClaimRows::new( - (0..stage1.proof.output_claims.commitments.len() * (statement1.degree + 1)) - .collect(), - statement1.degree + 1, - stage1.proof.output_claims.clone(), - ), - constant(input1), - constant(stage1_output), - ), - BlindFoldStage::new( - "protocol-backed-stage-2", - statement2, - SumcheckDomainSpec::BooleanHypercube, - stage2_consistency, - CommittedClaimRows::new( - (100..100 + stage2.proof.output_claims.commitments.len() * (statement2.degree + 1)) - .collect(), - statement2.degree + 1, - stage2.proof.output_claims.clone(), - ), - constant(input2), - constant(stage2_output), - ), - ]; - let statement = BlindFoldStatement::new( - stages, - [0usize, 100] + let row_len = statement1.degree + 1; + let template = ProtocolTemplate { + stages: vec![ + StageTemplate { + name: "stage-1", + statement: statement1, + output_claim_count: 2, + opening_ids: (0..2 * row_len).collect(), + input_claim: constant(input1), + output_claim: constant(*stage1.claim_outs.last().expect("stage has rounds")), + }, + StageTemplate { + name: "stage-2", + statement: statement2, + output_claim_count: 1, + opening_ids: (100..100 + row_len).collect(), + input_claim: input2_claim, + output_claim: constant(*stage2.claim_outs.last().expect("stage has rounds")), + }, + ], + final_openings: [0usize, 100] .into_iter() .zip(&eval_commitments) .map(|(opening, &commitment)| { FinalOpeningBinding::new(vec![opening], vec![f(1)], commitment) }) .collect(), + }; + let statement = template.statement( + [&stage1, &stage2] + .iter() + .map(|stage| (stage.consistency.clone(), stage.output_claims.clone())) + .collect(), ); - let protocol = blindfold_protocol_from_statement(&statement) - .expect("protocol builds from committed statement"); - let (real_witness_rows, real_witness_blindings) = protocol_backed_witness( - &protocol, - &statement, - &[&stage1, &stage2], - &real_eval_outputs, - &real_eval_blindings, + let protocol = + blindfold_protocol_from_statement(&statement).expect("protocol builds from stages"); + TwoStageFixture { + setup, + transcript, + stages: vec![stage1, stage2], + template, + statement, + protocol, + eval_outputs, + eval_blindings, + } +} + +/// Everything needed to drive a prover (harness or real) over the same +/// protocol-backed instance: the committed stages and protocol plus the +/// witness rows assembled independently of `assign_witness`. +#[derive(Clone)] +pub struct ProtocolBackedInstance { + pub fixture: TwoStageFixture, + pub rows: Vec>, + pub blindings: Vec, +} + +impl ProtocolBackedInstance { + pub fn prove_real(&self, rng: &mut impl RngCore) -> Result, ProverError> { + self.fixture.prove(&self.rows, &self.blindings, rng) + } +} + +pub fn build_protocol_backed_instance(rng: &mut R) -> ProtocolBackedInstance { + build_protocol_backed_instance_with_bindings(rng, 1) +} + +/// [`build_protocol_backed_instance`] over +/// [`two_stage_fixture_with_bindings`]. +pub fn build_protocol_backed_instance_with_bindings( + rng: &mut R, + binding_count: usize, +) -> ProtocolBackedInstance { + let fixture = two_stage_fixture_with_bindings(rng, Stage2Input::Constant, binding_count); + let (rows, blindings) = protocol_backed_witness( + &fixture.protocol, + &fixture.statement, + &[&fixture.stages[0], &fixture.stages[1]], + &fixture.eval_outputs, + &fixture.eval_blindings, rng, ); ProtocolBackedInstance { - setup, - protocol, - rows: real_witness_rows, - blindings: real_witness_blindings, - eval_outputs: real_eval_outputs, - eval_blindings: real_eval_blindings, + fixture, + rows, + blindings, } } +/// A complete proof from the harness's reference BlindFold prover. +#[derive(Clone)] +pub struct BlindFoldTestProof { + pub instance: ProtocolBackedInstance, + pub narg: Vec, +} + pub fn prove_blindfold_protocol_pipeline(rng: &mut R) -> BlindFoldTestProof { let instance = build_protocol_backed_instance(rng); - let mut transcript = Blake2bTranscript::::new(PROTOCOL_BACKED_TRANSCRIPT_LABEL); - append_protocol_transcript_prefix(&instance.protocol, &mut transcript); + let fixture = &instance.fixture; + let mut transcript = fixture.transcript.clone(); let witness = ProtocolWitness { rows: &instance.rows, blindings: &instance.blindings, - eval_outputs: &instance.eval_outputs, - eval_blindings: &instance.eval_blindings, + eval_outputs: &fixture.eval_outputs, + eval_blindings: &fixture.eval_blindings, }; - let proof = prove_from_protocol_witness( - &instance.setup, - &instance.protocol, + prove_from_protocol_witness( + &fixture.setup, + &fixture.protocol, &mut transcript, witness, rng, ); - BlindFoldTestProof { - protocol: instance.protocol, - proof, - setup: instance.setup, + instance, + narg: transcript.finish(), + } +} + +/// The BlindFold messages of a proof, read back in transcript order. Field +/// and commitment widths are fixed, so the offsets also locate each message. +#[derive(Clone, Debug)] +pub struct ProofMessages { + pub auxiliary_rows: Vec, + pub random_u: F, + pub random_rounds: Vec, + pub random_output_claim_rows: Vec, + pub random_auxiliary_rows: Vec, + pub random_error_rows: Vec, + pub random_evals: Vec, + pub cross_term_error_rows: Vec, + pub folded_eval_outputs: Vec, + pub folded_eval_blindings: Vec, + pub eval_output_openings: Vec>, + pub eval_blinding_openings: Vec>, + pub outer_rounds: Vec, + pub abc: Vec, + pub error_opening: VectorCommitmentOpening, + pub inner_rounds: Vec, + pub witness_opening: VectorCommitmentOpening, +} + +impl ProofMessages { + pub fn parse( + protocol: &BlindFoldProtocol, + prefix_len: usize, + narg: &[u8], + ) -> Result { + let dimensions = &protocol.dimensions; + let eval_count = protocol.eval_commitments.len(); + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let _prefix = transcript.receive_bytes(prefix_len)?; + let auxiliary_rows = transcript.receive_n(dimensions.auxiliary_rows)?; + let random_u = transcript.receive()?; + let random_rounds = transcript.receive_n(dimensions.coefficient_rows)?; + let random_output_claim_rows = transcript.receive_n(dimensions.output_claim_rows)?; + let random_auxiliary_rows = transcript.receive_n(dimensions.auxiliary_rows)?; + let random_error_rows = transcript.receive_n(dimensions.error.row_count)?; + let random_evals = transcript.receive_n(eval_count)?; + let cross_term_error_rows = transcript.receive_n(dimensions.error.row_count)?; + let folded_eval_outputs = transcript.receive_n(eval_count)?; + let folded_eval_blindings = transcript.receive_n(eval_count)?; + let witness_row_len = dimensions.witness.row_len; + let mut eval_output_openings = Vec::new(); + let mut eval_blinding_openings = Vec::new(); + for coordinates in protocol + .final_opening_witness_coordinates() + .expect("final opening coordinates are in the witness layout") + { + if coordinates.evaluation.is_some() { + eval_output_openings.push(receive_opening(witness_row_len, &mut transcript)?); + } + if coordinates.blinding.is_some() { + eval_blinding_openings.push(receive_opening(witness_row_len, &mut transcript)?); + } + } + let outer_rounds = transcript.receive_n(OUTER_ROUND_LEN * num_vars(dimensions.error))?; + let abc = transcript.receive_n(3)?; + let error_opening = receive_opening(dimensions.error.row_len, &mut transcript)?; + let inner_rounds = transcript.receive_n(INNER_ROUND_LEN * num_vars(dimensions.witness))?; + let witness_opening = receive_opening(witness_row_len, &mut transcript)?; + transcript.finish()?; + Ok(Self { + auxiliary_rows, + random_u, + random_rounds, + random_output_claim_rows, + random_auxiliary_rows, + random_error_rows, + random_evals, + cross_term_error_rows, + folded_eval_outputs, + folded_eval_blindings, + eval_output_openings, + eval_blinding_openings, + outer_rounds, + abc, + error_opening, + inner_rounds, + witness_opening, + }) } } +fn receive_opening( + row_len: usize, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, TranscriptError> { + Ok(VectorCommitmentOpening { + combined_vector: transcript.receive_n(row_len)?, + combined_blinding: transcript.receive()?, + }) +} + +fn send_opening(opening: &VectorCommitmentOpening, transcript: &mut ProverTranscript) { + transcript.send_all(&opening.combined_vector); + transcript.send(&opening.combined_blinding); +} + +fn num_vars(dimensions: RowDimensions) -> usize { + log2(dimensions.row_count) + log2(dimensions.row_len) +} + pub fn blindfold_protocol_from_statement( statement: &BlindFoldStatement, ) -> Result, jolt_blindfold::VerificationError> @@ -952,27 +1116,6 @@ where builder.build() } -pub fn append_protocol_transcript_prefix( - protocol: &BlindFoldProtocol, - transcript: &mut Blake2bTranscript, -) { - for (stage, output_claims) in protocol - .sumcheck_consistency - .iter() - .zip(&protocol.committed_output_claims) - { - for round in &stage.rounds { - CommittedRound { - commitment: round.commitment, - degree: round.degree, - } - .append_to_transcript(transcript); - let _ = transcript.challenge(); - } - output_claims.append_to_transcript(transcript); - } -} - fn protocol_backed_witness( protocol: &BlindFoldProtocol, statement: &BlindFoldStatement, @@ -1009,6 +1152,7 @@ fn protocol_backed_witness( .iter() .flat_map(|row| row.variables.iter().take(stage.output_claim_rows.row_len)); let values = generated + .witness .output_claim_rows .iter() .flat_map(|row| row.iter().copied()); @@ -1045,7 +1189,7 @@ fn protocol_backed_witness( for row in stages .iter() - .flat_map(|stage| stage.output_claim_rows.iter()) + .flat_map(|stage| stage.witness.output_claim_rows.iter()) { let mut row = row.clone(); row.resize(row_len, f(0)); @@ -1079,12 +1223,12 @@ fn protocol_backed_witness( let mut blindings = stages .iter() - .flat_map(|stage| stage.blindings.iter().copied()) + .flat_map(|stage| stage.witness.round_blindings.iter().copied()) .collect::>(); blindings.extend( stages .iter() - .flat_map(|stage| stage.output_claim_blindings.iter().copied()), + .flat_map(|stage| stage.witness.output_claim_blindings.iter().copied()), ); blindings.extend((0..protocol.dimensions.auxiliary_rows).map(|_| rng_field(rng))); blindings.resize(protocol.dimensions.witness.row_count, f(0)); @@ -1102,13 +1246,18 @@ struct ProtocolWitness<'a> { eval_blindings: &'a [F], } +#[derive(Clone, Debug)] +struct SumcheckTrace { + point: Vec, +} + fn prove_from_protocol_witness( setup: &PedersenSetup, protocol: &BlindFoldProtocol, - transcript: &mut Blake2bTranscript, + transcript: &mut ProverTranscript, witness: ProtocolWitness<'_>, rng: &mut R, -) -> BlindFoldProof { +) { let auxiliary_range = protocol.dimensions.witness_rows.auxiliary.clone(); let auxiliary_row_commitments = commit_rows( setup, @@ -1239,34 +1388,15 @@ fn prove_from_protocol_witness( let cross_term_error_row_commitments = commit_rows(setup, &cross_term_error_rows, &cross_term_error_blindings); - append_relaxed_instance_from_parts( - transcript, - RelaxedInstanceLabels { - u: b"bf_committed_u", - witness: b"bf_committed_w", - error: b"bf_committed_e", - eval: b"bf_committed_eval", - }, - committed.u, - &committed.witness_row_commitments, - &committed.error_row_commitments, - &committed.eval_commitments, - ); - append_relaxed_instance_from_parts( - transcript, - RelaxedInstanceLabels { - u: b"bf_random_u", - witness: b"bf_random_w", - error: b"bf_random_e", - eval: b"bf_random_eval", - }, - random_u, - &random_instance.witness_row_commitments, - &random_instance.error_row_commitments, - &random_instance.eval_commitments, - ); - transcript.append_values(b"bf_cross_e", &cross_term_error_row_commitments); - let folding_challenge = transcript.challenge(); + transcript.send_all(&auxiliary_row_commitments); + transcript.send(&random_u); + transcript.send_all(&random_round_commitments); + transcript.send_all(&random_output_claim_row_commitments); + transcript.send_all(&random_auxiliary_row_commitments); + transcript.send_all(&random_error_row_commitments); + transcript.send_all(&random_eval_commitments); + transcript.send_all(&cross_term_error_row_commitments); + let folding_challenge: F = transcript.challenge_small(); let folded_u = f(1) + folding_challenge * random_u; let folded_witness_rows = fold_rows(witness.rows, &random_witness_rows, folding_challenge); @@ -1300,69 +1430,42 @@ fn prove_from_protocol_witness( &random_eval_blindings, folding_challenge, ); + transcript.send_all(&folded_eval_outputs); + transcript.send_all(&folded_eval_blindings); let final_coordinates = protocol .final_opening_witness_coordinates() .expect("final opening coordinates are in witness layout"); - let mut folded_eval_output_openings = Vec::new(); - let mut folded_eval_blinding_openings = Vec::new(); for (index, coordinates) in final_coordinates.iter().enumerate() { - if let Some(coordinate) = coordinates.evaluation { - let (opening, opened) = open_witness_coordinate( - &folded_witness_rows, - &folded_witness_blindings, - coordinate, - ); - assert_eq!(opened, folded_eval_outputs[index]); - folded_eval_output_openings.push(opening); - } - if let Some(coordinate) = coordinates.blinding { + for (coordinate, expected) in [ + (coordinates.evaluation, folded_eval_outputs[index]), + (coordinates.blinding, folded_eval_blindings[index]), + ] { + let Some(coordinate) = coordinate else { + continue; + }; let (opening, opened) = open_witness_coordinate( &folded_witness_rows, &folded_witness_blindings, coordinate, ); - assert_eq!(opened, folded_eval_blindings[index]); - folded_eval_blinding_openings.push(opening); + assert_eq!(opened, expected); + send_opening(&opening, transcript); } } - for opening in &folded_eval_output_openings { - append_vector_opening( - transcript, - b"bf_eval_out_open", - b"bf_eval_out_blind", - opening, - ); - } - for opening in &folded_eval_blinding_openings { - append_vector_opening( - transcript, - b"bf_eval_blind_open", - b"bf_eval_blind_bl", - opening, - ); - } - transcript.append(&Label(b"bf_spartan")); let outer_num_vars = log2(protocol.dimensions.error.row_count) + log2(protocol.dimensions.error.row_len); - let tau = transcript.challenge_vector(outer_num_vars); - let outer_trace = prove_slow_sumcheck( - outer_num_vars, - 3, - f(0), - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - transcript, - |point| { - outer_function( - &protocol.r1cs, - folded_u, - &flatten(&folded_witness_rows), - &folded_error_rows, - &tau, - point, - ) - }, - ); + let tau: Vec = transcript.challenges_small(outer_num_vars); + let outer_trace = prove_slow_sumcheck(outer_num_vars, 3, f(0), transcript, |point| { + outer_function( + &protocol.r1cs, + folded_u, + &flatten(&folded_witness_rows), + &folded_error_rows, + &tau, + point, + ) + }); let (az_rx, bz_rx, cz_rx) = abc_at_point( &protocol.r1cs, @@ -1382,17 +1485,12 @@ fn prove_from_protocol_witness( ) .expect("folded error rows open"); - transcript.append_values(b"bf_az_bz_cz", &[az_rx, bz_rx, cz_rx]); - append_vector_opening( - transcript, - b"bf_error_opening", - b"bf_error_blind", - &error_opening, - ); + transcript.send_all(&[az_rx, bz_rx, cz_rx]); + send_opening(&error_opening, transcript); - let ra = transcript.challenge(); - let rb = transcript.challenge(); - let rc = transcript.challenge(); + let ra: F = transcript.challenge_small(); + let rb: F = transcript.challenge_small(); + let rc: F = transcript.challenge_small(); let inner_num_vars = log2(protocol.dimensions.witness.row_count) + log2(protocol.dimensions.witness.row_len); let row_weights = EqPolynomial::::evals(&outer_trace.point, None); @@ -1401,24 +1499,17 @@ fn prove_from_protocol_witness( .public_column_contributions(&row_weights, 0, folded_u) .expect("public column contributions evaluate"); let inner_claim = ra * (az_rx - public.a) + rb * (bz_rx - public.b) + rc * (cz_rx - public.c); - let inner_trace = prove_slow_sumcheck( - inner_num_vars, - 2, - inner_claim, - b"inner_sumcheck_poly", - transcript, - |point| { - inner_function( - &protocol.r1cs, - &outer_trace.point, - &folded_witness_rows, - ra, - rb, - rc, - point, - ) - }, - ); + let inner_trace = prove_slow_sumcheck(inner_num_vars, 2, inner_claim, transcript, |point| { + inner_function( + &protocol.r1cs, + &outer_trace.point, + &folded_witness_rows, + ra, + rb, + rc, + point, + ) + }); let (witness_row_point, witness_entry_point) = inner_trace .point .split_at(log2(protocol.dimensions.witness.row_count)); @@ -1431,27 +1522,7 @@ fn prove_from_protocol_witness( ) .expect("folded witness rows open"); - BlindFoldProof { - auxiliary_row_commitments, - random_round_commitments, - random_output_claim_row_commitments, - random_auxiliary_row_commitments, - random_error_row_commitments, - random_eval_commitments, - random_u, - cross_term_error_row_commitments, - outer_sumcheck: outer_trace.proof, - az_rx, - bz_rx, - cz_rx, - inner_sumcheck: inner_trace.proof, - witness_opening, - error_opening, - folded_eval_outputs, - folded_eval_blindings, - folded_eval_output_openings, - folded_eval_blinding_openings, - } + send_opening(&witness_opening, transcript); } fn commit_rows(setup: &PedersenSetup, rows: &[Vec], blindings: &[F]) -> Vec { @@ -1487,40 +1558,6 @@ fn boolean_point(index: usize, num_vars: usize) -> Vec { .collect() } -#[derive(Clone, Copy, Debug)] -struct RelaxedInstanceLabels { - u: &'static [u8], - witness: &'static [u8], - error: &'static [u8], - eval: &'static [u8], -} - -fn append_relaxed_instance_from_parts( - transcript: &mut Blake2bTranscript, - labels: RelaxedInstanceLabels, - u: F, - witness_commitments: &[Bn254G1], - error_commitments: &[Bn254G1], - eval_commitments: &[Bn254G1], -) { - transcript.append(&Label(labels.u)); - u.append_to_transcript(transcript); - transcript.append_values(labels.witness, witness_commitments); - transcript.append_values(labels.error, error_commitments); - transcript.append_values(labels.eval, eval_commitments); -} - -fn append_vector_opening( - transcript: &mut Blake2bTranscript, - row_label: &'static [u8], - blinding_label: &'static [u8], - opening: &jolt_crypto::VectorCommitmentOpening, -) { - transcript.append_values(row_label, &opening.combined_vector); - transcript.append(&Label(blinding_label)); - opening.combined_blinding.append_to_transcript(transcript); -} - fn zero_rows(row_count: usize, row_len: usize) -> Vec> { vec![vec![f(0); row_len]; row_count] } @@ -1708,13 +1745,11 @@ fn prove_slow_sumcheck( num_vars: usize, degree: usize, claim: F, - label: &'static [u8], - transcript: &mut Blake2bTranscript, + transcript: &mut ProverTranscript, eval: impl Fn(&[F]) -> F, ) -> SumcheckTrace { let mut running_sum = claim; let mut prefix = Vec::with_capacity(num_vars); - let mut rounds = Vec::with_capacity(num_vars); for round in 0..num_vars { let remaining = num_vars - round - 1; @@ -1738,19 +1773,13 @@ fn prove_slow_sumcheck( let mut compressed = Vec::with_capacity(degree); compressed.push(coefficients[0]); compressed.extend_from_slice(&coefficients[2..]); - transcript.append_values(label, &compressed); - let challenge = transcript.challenge(); + transcript.send_all(&compressed); + let challenge: F = transcript.challenge_small(); running_sum = eval_poly(&coefficients, challenge); prefix.push(challenge); - rounds.push(CompressedPoly::new(compressed)); } - SumcheckTrace { - proof: CompressedSumcheckProof { - round_polynomials: rounds, - }, - point: prefix, - } + SumcheckTrace { point: prefix } } fn interpolate_zero_to_degree(values: &[F]) -> Vec { From bab5c108fa816280da255cab6ca874f5a81f9709 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:31:51 -0400 Subject: [PATCH 13/62] refactor(verifier)!: verify Jolt proofs as NARG argument strings 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. --- Cargo.lock | 4 + crates/jolt-claims/src/claim_data.rs | 14 +- .../src/protocols/jolt/geometry/dimensions.rs | 9 + crates/jolt-verifier-derive/src/lib.rs | 337 ++++-- crates/jolt-verifier/src/config.rs | 84 +- crates/jolt-verifier/src/error.rs | 10 + crates/jolt-verifier/src/lib.rs | 18 +- crates/jolt-verifier/src/proof.rs | 449 +++----- crates/jolt-verifier/src/stages/mod.rs | 6 +- crates/jolt-verifier/src/stages/relations.rs | 164 ++- .../jolt-verifier/src/stages/stage1/verify.rs | 76 +- .../src/stages/stage2/ram_output_check.rs | 8 +- .../stages/stage2/ram_read_write_checking.rs | 4 +- .../jolt-verifier/src/stages/stage2/verify.rs | 104 +- .../jolt-verifier/src/stages/stage3/verify.rs | 56 +- .../src/stages/stage4/outputs.rs | 92 +- .../src/stages/stage4/ram_val_check.rs | 107 +- .../jolt-verifier/src/stages/stage4/verify.rs | 57 +- .../jolt-verifier/src/stages/stage5/verify.rs | 56 +- .../src/stages/stage6a/booleanity.rs | 10 +- .../src/stages/stage6a/bytecode_read_raf.rs | 14 +- .../src/stages/stage6a/verify.rs | 63 +- .../jolt-verifier/src/stages/stage6b/batch.rs | 23 +- .../src/stages/stage6b/booleanity.rs | 4 +- .../committed_reduction_cycle_phase.rs | 68 +- .../stage6b/instruction_ra_virtualization.rs | 4 +- .../src/stages/stage6b/verify.rs | 475 +++----- .../jolt-verifier/src/stages/stage7/verify.rs | 63 +- .../src/stages/stage8/field_inline.rs | 26 +- .../jolt-verifier/src/stages/stage8/packed.rs | 24 +- .../jolt-verifier/src/stages/stage8/verify.rs | 118 +- crates/jolt-verifier/src/stages/uniskip.rs | 124 +- .../src/stages/zk/blindfold/mod.rs | 71 +- .../src/stages/zk/blindfold/stage1.rs | 4 +- .../src/stages/zk/blindfold/stage2.rs | 6 +- .../src/stages/zk/blindfold/stage3.rs | 4 +- .../src/stages/zk/blindfold/stage4.rs | 8 +- .../src/stages/zk/blindfold/stage5.rs | 4 +- .../src/stages/zk/blindfold/stage6a.rs | 6 +- .../src/stages/zk/blindfold/stage6b.rs | 6 +- .../src/stages/zk/blindfold/stage7.rs | 6 +- .../jolt-verifier/src/stages/zk/committed.rs | 47 +- crates/jolt-verifier/src/stages/zk/inputs.rs | 4 +- crates/jolt-verifier/src/stages/zk/outputs.rs | 7 + crates/jolt-verifier/src/verifier.rs | 1026 ++++------------- 45 files changed, 1635 insertions(+), 2235 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb57ab6693..6c5e313390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3232,11 +3232,15 @@ dependencies = [ name = "jolt-dory" version = "0.1.0" dependencies = [ + "akita-config", + "akita-pcs", + "akita-types", "ark-bn254", "ark-ec", "ark-serialize 0.5.0", "criterion", "dory-pcs", + "jolt-akita", "jolt-crypto", "jolt-field", "jolt-openings", diff --git a/crates/jolt-claims/src/claim_data.rs b/crates/jolt-claims/src/claim_data.rs index ca518581ec..520ac85cc9 100644 --- a/crates/jolt-claims/src/claim_data.rs +++ b/crates/jolt-claims/src/claim_data.rs @@ -10,10 +10,11 @@ //! (the verifier-derived opening point); the derives emit the value resolver on //! the `F` form and the point accessors on the `Vec` form. //! -//! Transcript I/O (`append_openings`, `draw_challenges`) deliberately lives in -//! `jolt-verifier`: `jolt-claims` stays free of any `Transcript` dependency. The -//! verifier-side `append_openings` is a thin consumer of [`OutputClaims::opening_values`], -//! so it cannot disagree with the canonical order defined here. +//! Transcript I/O (receiving openings, `draw_challenges`) deliberately lives in +//! `jolt-verifier`: `jolt-claims` stays free of any transcript dependency. The +//! verifier receives openings in [`OutputClaims::canonical_order`] and the prover +//! sends [`OutputClaims::opening_values`], so neither can disagree with the +//! canonical order defined here. use jolt_field::JoltField; use thiserror::Error; @@ -53,9 +54,8 @@ pub struct MissingOpeningValue { /// /// The implementor's field declaration order is the single definition of /// canonical opening order: [`opening_values`](Self::opening_values) and -/// [`canonical_order`](Self::canonical_order) derive from it, so the Fiat-Shamir -/// `append_openings` (in `jolt-verifier`) that iterates `opening_values()` cannot -/// disagree. +/// [`canonical_order`](Self::canonical_order) derive from it, so the prover's +/// sends and the verifier's receives (in `jolt-verifier`) cannot disagree. /// /// Generic over the opening-id type `O` (defaulting to [`JoltOpeningId`]) so the /// trait can live in the framework half and be reused by other protocol families. diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs index 8202cd7307..d79b5f94f1 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs @@ -32,6 +32,15 @@ impl TracePolynomialOrder { } } + /// Inverse of [`transcript_scalar`](Self::transcript_scalar). + pub const fn from_transcript_scalar(scalar: u64) -> Option { + match scalar { + 0 => Some(Self::CycleMajor), + 1 => Some(Self::AddressMajor), + _ => None, + } + } + pub const fn address_cycle_to_index( self, address: usize, diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index ff21a1feb8..756106a5d0 100644 --- a/crates/jolt-verifier-derive/src/lib.rs +++ b/crates/jolt-verifier-derive/src/lib.rs @@ -61,16 +61,18 @@ //! impls (`StageProver`/`KernelSource`), so no stage's member list, order, //! or presence is ever restated. See `specs/prover-stage-drivers.md`. //! - `verify_clear` — the composed clear-path driver: `begin_batch` with a clear -//! recorder, reduce the combined claim through the single-instance -//! `SumcheckProof::verify_compressed_boolean`, `derive_opening_points` at the -//! reduced point, then the `expected_final_claim` equality check. The batching -//! lives in the generated head; `jolt-sumcheck` provides only the single-instance -//! verifier. Returns the stage's produced `OutputPoints`. +//! recorder, read the combined claim's compressed rounds through +//! `jolt_sumcheck::SumcheckVerifier::verify_compressed`, receive the output +//! claims (`receive_output_claims`), `derive_opening_points` at the reduced +//! point, then the `expected_final_claim` equality check. The batching lives +//! in the generated head; `jolt-sumcheck` provides only the single-instance +//! verifier. Returns the stage's produced `OutputPoints` and `OutputClaims`. //! - `verify_zk` — the ZK-path driver: fold the members' dimensions, draw the -//! batching coefficients, and check committed consistency through -//! `SumcheckProof::verify_committed_consistency_dims`. Committed proofs reveal no -//! claim scalars, so the caller must have absorbed claim COMMITMENTS beforehand -//! (the coefficients are squeezed before the consistency rounds). +//! batching coefficients, and read the committed rounds and output-claim +//! commitments through `jolt_sumcheck::SumcheckVerifier::verify_committed`. +//! Committed proofs reveal no claim scalars, so the caller must have absorbed +//! claim COMMITMENTS beforehand (the coefficients are drawn before the +//! committed rounds). //! - `derive_opening_points` — slice each member's point from the batch challenge //! vector (`ConcreteSumcheck::instance_point`, at the member's overridable //! `instance_point_offset`) and map it through the member's @@ -86,9 +88,11 @@ //! - `output_claim_count` / `validate_output_claims` — the wire-shape helpers //! (via `relations::validate_member_{presence, output_shape}`), deriving each //! member's expected openings from `ConcreteSumcheck::wire_output_openings`. -//! - `draw_challenges`, `empty_input_points`, and the absorb plumbing -//! (`opening_values` / `append_output_claims`, via -//! `relations::absorbed_opening_values`). +//! - `draw_challenges`, `empty_input_points`, and the claim plumbing +//! (`opening_values` via `relations::absorbed_opening_values`, the order the +//! prover sends; `receive_output_claims` via +//! `relations::{receive_member_openings, assemble_member_claims}`, which reads +//! that order back). //! //! Every `#[sumcheck_batch(...)]` flag is an opt-OUT (`StageOptions` below is //! the canonical reference): `no_opening_values`, `no_output_shape`, and @@ -384,6 +388,38 @@ fn expand(input: DeriveInput) -> syn::Result { ) }); + let receive_members = plans.iter().map(|plan| { + let id = &plan.ident; + let instance = &plan.instance; + per_member( + plan.is_option, + &[(&member_ident, quote!(self.#id))], + quote! { + __received.extend(#relations::receive_member_openings::<#f, #instance, __H>( + __member, transcript, + )?); + }, + ) + }); + let assemble_fields = plans + .iter() + .map(|plan| { + let id = &plan.ident; + let instance = &plan.instance; + if plan.is_option { + quote! { + #id: self + .#id + .as_ref() + .map(|_| #relations::assemble_member_claims::<#f, #instance>(&__received)) + .transpose()? + } + } else { + quote!(#id: #relations::assemble_member_claims::<#f, #instance>(&__received)?) + } + }) + .collect::>(); + // Per-instance driver plumbing on the source `StageNSumchecks` struct itself: // draw each member's challenges into the stage's challenge aggregate, delegating // to each member's `ConcreteSumcheck::draw_challenges` in declaration order; @@ -502,10 +538,10 @@ fn expand(input: DeriveInput) -> syn::Result { .map(|(plan, (sum, coeff))| { if plan.is_option { quote! { - let #coeff = #sum.as_ref().map(|_| transcript.challenge_scalar()); + let #coeff = #sum.as_ref().map(|_| transcript.challenge()); } } else { - quote!(let #coeff = transcript.challenge_scalar();) + quote!(let #coeff = transcript.challenge();) } }); @@ -563,21 +599,22 @@ fn expand(input: DeriveInput) -> syn::Result { /// no-ops), never by a runtime flag. Returns the engine-form /// `jolt_sumcheck::BatchPrelude` paired with the stage's named /// batching coefficients. - pub fn begin_batch<__R, __T>( + pub fn begin_batch<__R, __C>( &self, inputs: &#input_claims_name<#f>, challenges: &#challenges_name<#f>, recorder: &mut __R, - transcript: &mut __T, + transcript: &mut __C, ) -> ::core::result::Result< (::jolt_sumcheck::BatchPrelude<#f>, #batching_coefficients_name<#f>), #krate::VerifierError, > where __R: ::jolt_sumcheck::SumcheckRecorder<#f>, - __T: ::jolt_transcript::Transcript, + __C: ::jolt_transcript::Channel, { use #relations::ConcreteSumcheck as _; + use ::jolt_transcript::Channel as _; #(#sum_bindings)* @@ -606,70 +643,118 @@ fn expand(input: DeriveInput) -> syn::Result { }; // The composed clear-path driver: begin the batch (clear recorder, so the - // claim absorbs are appended), verify the single-instance compressed-boolean - // sumcheck, derive the produced opening points at the reduced point, and check - // the reduced claim against the expected final-claim fold — the tail every - // stage repeats verbatim. Validation (`validate_output_claims`, - // member-presence guards) and the canonical opening absorb stay with the - // caller: their position and form are stage-specific. - let verify_clear_method = quote! { - /// Run the clear-path batched verification in one call: `begin_batch` - /// (with a clear recorder, so the claim absorbs are appended), the - /// single-instance `SumcheckProof::verify_compressed_boolean`, - /// [`Self::derive_opening_points`] at the reduced point, then the - /// [`Self::expected_final_claim`] equality check (attributed to `stage` - /// on mismatch). Returns the produced opening points. - #[expect( - clippy::too_many_arguments, - reason = "the composed tail threads every per-stage aggregate once" - )] - pub fn verify_clear<__C, __T>( - &self, - inputs: &#input_claims_name<#f>, - input_points: &#input_points_name<#f>, - challenges: &#challenges_name<#f>, - claims: &#output_claims_name<#f>, - proof: &::jolt_sumcheck::SumcheckProof<#f, __C>, - transcript: &mut __T, - stage: usize, - ) -> ::core::result::Result<#output_points_name<#f>, #krate::VerifierError> - where - __C: ::core::clone::Clone + ::jolt_transcript::AppendToTranscript, - __T: ::jolt_transcript::Transcript, - { - use #relations::ConcreteSumcheck as _; - - let mut __recorder = ::jolt_sumcheck::ClearSumcheckRecorder::<#f, __C>::new(); - let (__batch, __coefficients) = - self.begin_batch(inputs, challenges, &mut __recorder, transcript)?; - - let __reduction = proof - .verify_compressed_boolean( - __batch.max_num_vars, - __batch.max_degree, - __batch.claimed_sum, + // input claims are absorbed), verify the compressed Boolean-hypercube rounds, + // derive the produced opening points at the reduced point, receive the output + // claims, and check the reduced claim against the expected final-claim fold — + // the tail every stage repeats verbatim. It is parameterized by how the output + // claims are received; `verify_clear` (emitted unless `no_opening_values`) passes the + // generated `receive_output_claims`, and a stage with a curated claim order + // passes its own receive. + let verify_clear_method = { + let verify_clear = if options.no_opening_values { + quote!() + } else { + quote! { + /// [`Self::verify_clear_with`] receiving the output claims through + /// [`Self::receive_output_claims`]. + #[expect( + clippy::type_complexity, + reason = "the produced points and claims are the stage's two outputs" + )] + pub fn verify_clear<__H>( + &self, + inputs: &#input_claims_name<#f>, + input_points: &#input_points_name<#f>, + challenges: &#challenges_name<#f>, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + stage: usize, + ) -> ::core::result::Result< + (#output_points_name<#f>, #output_claims_name<#f>), + #krate::VerifierError, + > + where + __H: ::jolt_transcript::Sponge, + { + self.verify_clear_with( + inputs, + input_points, + challenges, + transcript, + stage, + |__sumchecks, _, __transcript| __sumchecks.receive_output_claims(__transcript), + ) + } + } + }; + quote! { + /// Run the clear-path batched verification in one call: `begin_batch` + /// (with a clear recorder, so the input claims are absorbed publicly), + /// the compressed Boolean-hypercube rounds, [`Self::derive_opening_points`] + /// at the reduced point, the output claims read by `receive_claims` + /// (given those points), then the [`Self::expected_final_claim`] check + /// (attributed to `stage` on mismatch). Returns the produced opening + /// points with the received output claims. + #[expect( + clippy::type_complexity, + reason = "the produced points and claims are the stage's two outputs" + )] + pub fn verify_clear_with<__H, __R>( + &self, + inputs: &#input_claims_name<#f>, + input_points: &#input_points_name<#f>, + challenges: &#challenges_name<#f>, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + stage: usize, + receive_claims: __R, + ) -> ::core::result::Result< + (#output_points_name<#f>, #output_claims_name<#f>), + #krate::VerifierError, + > + where + __H: ::jolt_transcript::Sponge, + __R: ::core::ops::FnOnce( + &Self, + &#output_points_name<#f>, + &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + ) -> ::core::result::Result<#output_claims_name<#f>, #krate::VerifierError>, + { + use #relations::ConcreteSumcheck as _; + + let mut __recorder = ::jolt_sumcheck::ClearSumcheckRecorder::<#f>::new(); + let (__batch, __coefficients) = + self.begin_batch(inputs, challenges, &mut __recorder, transcript)?; + + let __reduction = ::jolt_sumcheck::SumcheckVerifier::verify_compressed( + &::jolt_sumcheck::SumcheckClaim::new( + __batch.max_num_vars, + __batch.max_degree, + __batch.claimed_sum, + ), transcript, ) .map_err(|error| #krate::VerifierError::StageClaimSumcheckFailed { stage: #base_lit.to_string(), reason: error.to_string(), })?; - - let __output_points = - self.derive_opening_points(__reduction.point.as_slice(), input_points)?; - let __expected_final_claim = self.expected_final_claim( - &__coefficients, - input_points, - claims, - &__output_points, - challenges, - )?; - if __reduction.value != __expected_final_claim { - return ::core::result::Result::Err( - #krate::VerifierError::StageClaimOutputMismatch { stage }, - ); + let __output_points = + self.derive_opening_points(__reduction.point.as_slice(), input_points)?; + let __claims = receive_claims(self, &__output_points, transcript)?; + let __expected_final_claim = self.expected_final_claim( + &__coefficients, + input_points, + &__claims, + &__output_points, + challenges, + )?; + if __reduction.value != __expected_final_claim { + return ::core::result::Result::Err( + #krate::VerifierError::StageClaimOutputMismatch { stage }, + ); + } + ::core::result::Result::Ok((__output_points, __claims)) } - ::core::result::Result::Ok(__output_points) + + #verify_clear } }; @@ -694,28 +779,67 @@ fn expand(input: DeriveInput) -> syn::Result { if plan.is_option { quote! { if self.#id.is_some() { - __batching_coefficients.push(transcript.challenge_scalar()); + __batching_coefficients.push(transcript.challenge()); } } } else { - quote!(__batching_coefficients.push(transcript.challenge_scalar());) + quote!(__batching_coefficients.push(transcript.challenge());) } }); quote! { - pub fn verify_zk<__C, __T>( + /// Run the ZK-path batched verification: draw the batching + /// coefficients, then read the committed rounds and the + /// `num_output_commitments` output-claim row commitments. + #[expect( + clippy::type_complexity, + reason = "the batched consistency and the output-claim commitments are read together" + )] + pub fn verify_zk<__C, __H>( + &self, + num_output_commitments: usize, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + ) -> ::core::result::Result< + ( + ::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, + ::jolt_sumcheck::CommittedOutputClaims<__C>, + ), + #krate::VerifierError, + > + where + __C: ::jolt_field::CanonicalDecode, + __H: ::jolt_transcript::Sponge, + { + self.verify_zk_with(|_| ::core::result::Result::Ok(num_output_commitments), transcript) + } + + /// [`Self::verify_zk`] with the output-claim commitment count derived + /// from the verified rounds, for a stage whose committed-claim count + /// depends on its own opening points. + #[expect( + clippy::type_complexity, + reason = "the batched consistency and the output-claim commitments are read together" + )] + pub fn verify_zk_with<__C, __H, __N>( &self, - proof: &::jolt_sumcheck::SumcheckProof<#f, __C>, - transcript: &mut __T, + num_output_commitments: __N, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, ) -> ::core::result::Result< - ::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, + ( + ::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, + ::jolt_sumcheck::CommittedOutputClaims<__C>, + ), #krate::VerifierError, > where - __C: ::core::clone::Clone + ::jolt_transcript::AppendToTranscript, - __T: ::jolt_transcript::Transcript, + __C: ::jolt_field::CanonicalDecode, + __H: ::jolt_transcript::Sponge, + __N: ::core::ops::FnOnce( + &::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, + ) -> ::core::result::Result, { use #relations::ConcreteSumcheck as _; + use ::jolt_transcript::Channel as _; let mut __max_num_vars = 0usize; let mut __max_degree = 0usize; @@ -724,19 +848,27 @@ fn expand(input: DeriveInput) -> syn::Result { let mut __batching_coefficients = ::std::vec::Vec::new(); #(#coeff_draws_zk)* - let __consistency = proof - .verify_committed_consistency_dims(__max_num_vars, __max_degree, transcript) + let (__consistency, _) = + ::jolt_sumcheck::SumcheckVerifier::verify_committed::<#f, __H, __C>( + ::jolt_sumcheck::SumcheckStatement::new(__max_num_vars, __max_degree), + 0, + transcript, + ) .map_err(|error| #krate::VerifierError::StageClaimSumcheckFailed { stage: #base_lit.to_string(), reason: error.to_string(), })?; - - ::core::result::Result::Ok(::jolt_sumcheck::BatchedCommittedSumcheckConsistency { + let __consistency = ::jolt_sumcheck::BatchedCommittedSumcheckConsistency { consistency: __consistency, batching_coefficients: __batching_coefficients, max_num_vars: __max_num_vars, max_degree: __max_degree, - }) + }; + let __count = num_output_commitments(&__consistency)?; + let __output_claims = ::jolt_sumcheck::CommittedOutputClaims { + commitments: transcript.receive_n(__count)?, + }; + ::core::result::Result::Ok((__consistency, __output_claims)) } } }; @@ -978,9 +1110,8 @@ fn expand(input: DeriveInput) -> syn::Result { // The output-claim shape helpers: the total produced-opening count (for the ZK // commitment count) and a validator that the proof-supplied output claims match // the dims-derived expected shape. Both delegate per member to the generic - // `relations` helpers; an `Option` member's presence guards run first (a stage - // that curates its own shape checks calls `validate_member_presence` by hand — - // stage 6b). Suppressed by `#[sumcheck_batch(no_output_shape)]` for a stage + // `relations` helpers; an `Option` member's presence guards run first. + // Suppressed by `#[sumcheck_batch(no_output_shape)]` for a stage // whose wire shape is runtime-deduped (the count/validator would be wrong). let output_shape_methods = if options.no_output_shape { quote!() @@ -1060,9 +1191,9 @@ fn expand(input: DeriveInput) -> syn::Result { /// challenges draw nothing; `Option` members draw only when present. /// This single-sources the stage's inline per-instance draw, so its /// Fiat-Shamir order follows member declaration order. - pub fn draw_challenges<__T: ::jolt_transcript::Transcript>( + pub fn draw_challenges<__C: ::jolt_transcript::Channel>( &self, - transcript: &mut __T, + transcript: &mut __C, ) -> ::core::result::Result<#challenges_name<#f>, #krate::VerifierError> { use #relations::ConcreteSumcheck as _; ::core::result::Result::Ok(#challenges_name { @@ -1093,17 +1224,19 @@ fn expand(input: DeriveInput) -> syn::Result { __values } - /// Append every absorbed opening to the transcript in canonical order, - /// each under the `b"opening_claim"` label, matching the prover's - /// commitment order. - pub fn append_output_claims<__T: ::jolt_transcript::Transcript>( + /// Receive every member's produced openings in canonical order (the + /// order of [`Self::opening_values`], which the prover sends), then + /// assemble the stage's output claims, filling each aliased opening + /// from its canonical source. + pub fn receive_output_claims<__H: ::jolt_transcript::Sponge>( &self, - transcript: &mut __T, - claims: &#output_claims_name<#f>, - ) { - for value in self.opening_values(claims) { - transcript.append_labeled(b"opening_claim", &value); - } + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + ) -> ::core::result::Result<#output_claims_name<#f>, #krate::VerifierError> { + let mut __received = ::std::collections::BTreeMap::new(); + #(#receive_members)* + ::core::result::Result::Ok(#output_claims_name { + #(#assemble_fields,)* + }) } } }; diff --git a/crates/jolt-verifier/src/config.rs b/crates/jolt-verifier/src/config.rs index aa9d569bf0..f82bff12b4 100644 --- a/crates/jolt-verifier/src/config.rs +++ b/crates/jolt-verifier/src/config.rs @@ -1,13 +1,15 @@ //! Verifier-selected protocol configuration. //! //! Every protocol axis is fixed at compile time — the `zk` feature selects -//! BlindFold, the `akita` feature selects packed commitments and little-endian -//! scalar challenges, the `field-inline` feature enables the native -//! field-register extension — so one compiled verifier runs exactly one -//! protocol. A proof self-describes its axes and [`validate_proof_config`] -//! rejects a mismatch fail-closed. +//! BlindFold, the `akita` feature selects packed commitments, the +//! `field-inline` feature enables the native field-register extension — so one +//! compiled verifier runs exactly one protocol. The verifier absorbs its own +//! configuration into the transcript ([`JoltProtocolConfig::transcript_bytes`]); +//! a proof also self-describes its axes so [`validate_proof_config`] reports a +//! mismatch directly. pub use jolt_claims::protocols::field_inline::FieldInlineConfig; +use jolt_claims::protocols::field_inline::FieldInlineRepresentation; use jolt_riscv::JoltInstructionProfile; #[cfg(not(feature = "field-inline"))] use jolt_riscv::RV64IMAC_JOLT; @@ -41,18 +43,11 @@ pub enum CommitmentConfig { Packed, } -/// Byte order used to decode scalar Fiat-Shamir challenges. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ScalarChallengeEndianness { - Big, - Little, -} - +/// The protocol axes a proof is produced under. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct JoltProtocolConfig { pub zk: ZkConfig, pub commitment: CommitmentConfig, - pub scalar_challenge_endianness: ScalarChallengeEndianness, pub field_inline: FieldInlineConfig, } @@ -65,10 +60,42 @@ impl JoltProtocolConfig { ZkConfig::Transparent }, commitment: SELECTED_COMMITMENT_CONFIG, - scalar_challenge_endianness: SELECTED_SCALAR_CHALLENGE_ENDIANNESS, field_inline: SELECTED_FIELD_INLINE_CONFIG, } } + + /// The canonical encoding the transcript absorbs: one byte per axis, then + /// the field-inline parameters (`field_register_log_k` as `u64` little + /// endian, then the representation byte). + pub fn transcript_bytes(&self) -> [u8; 12] { + let zk = match self.zk { + ZkConfig::Transparent => 0, + ZkConfig::BlindFold => 1, + }; + let commitment = match self.commitment { + CommitmentConfig::Homomorphic => 0, + CommitmentConfig::Packed => 1, + }; + let representation = match self.field_inline.representation { + FieldInlineRepresentation::NativeFieldElement => 0, + }; + let [k0, k1, k2, k3, k4, k5, k6, k7] = + crate::num::u64_from_usize(self.field_inline.field_register_log_k).to_le_bytes(); + [ + zk, + commitment, + u8::from(self.field_inline.enabled), + k0, + k1, + k2, + k3, + k4, + k5, + k6, + k7, + representation, + ] + } } /// The instruction profile this build has constraints for. Input validation @@ -93,14 +120,6 @@ pub const SELECTED_COMMITMENT_CONFIG: CommitmentConfig = CommitmentConfig::Packe #[cfg(not(feature = "akita"))] pub const SELECTED_COMMITMENT_CONFIG: CommitmentConfig = CommitmentConfig::Homomorphic; -#[cfg(feature = "akita")] -pub const SELECTED_SCALAR_CHALLENGE_ENDIANNESS: ScalarChallengeEndianness = - ScalarChallengeEndianness::Little; - -#[cfg(not(feature = "akita"))] -pub const SELECTED_SCALAR_CHALLENGE_ENDIANNESS: ScalarChallengeEndianness = - ScalarChallengeEndianness::Big; - #[cfg(feature = "field-inline")] pub const SELECTED_FIELD_INLINE_CONFIG: FieldInlineConfig = FieldInlineConfig::enabled(); @@ -111,7 +130,6 @@ pub const SELECTED_FIELD_INLINE_CONFIG: FieldInlineConfig = FieldInlineConfig::d pub const JOLT_VERIFIER_CONFIG: JoltProtocolConfig = JoltProtocolConfig { zk: SELECTED_ZK_CONFIG, commitment: SELECTED_COMMITMENT_CONFIG, - scalar_challenge_endianness: SELECTED_SCALAR_CHALLENGE_ENDIANNESS, field_inline: SELECTED_FIELD_INLINE_CONFIG, }; @@ -141,15 +159,18 @@ mod tests { let protocol = JoltProtocolConfig { zk: ZkConfig::Transparent, commitment: CommitmentConfig::Homomorphic, - scalar_challenge_endianness: ScalarChallengeEndianness::Big, field_inline: FieldInlineConfig::disabled(), }; - assert_eq!(postcard::to_stdvec(&protocol).unwrap(), [0, 0, 0, 0, 4, 0]); + assert_eq!(postcard::to_stdvec(&protocol).unwrap(), [0, 0, 0, 4, 0]); assert_eq!( - postcard::from_bytes::(&[0, 0, 0, 0, 4, 0]).unwrap(), + postcard::from_bytes::(&[0, 0, 0, 4, 0]).unwrap(), protocol ); assert!(postcard::from_bytes::(&[0, 0, 0]).is_err()); + assert_eq!( + protocol.transcript_bytes(), + [0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0] + ); } #[test] @@ -187,15 +208,4 @@ mod tests { Err(VerifierError::ProtocolConfigMismatch { .. }) )); } - - #[test] - fn rejects_scalar_challenge_endianness_mismatch() { - let mut proof_config = JOLT_VERIFIER_CONFIG; - proof_config.scalar_challenge_endianness = match proof_config.scalar_challenge_endianness { - ScalarChallengeEndianness::Big => ScalarChallengeEndianness::Little, - ScalarChallengeEndianness::Little => ScalarChallengeEndianness::Big, - }; - - assert!(validate_proof_config(&JOLT_VERIFIER_CONFIG, proof_config).is_err()); - } } diff --git a/crates/jolt-verifier/src/error.rs b/crates/jolt-verifier/src/error.rs index ed29f51ff5..646868c17b 100644 --- a/crates/jolt-verifier/src/error.rs +++ b/crates/jolt-verifier/src/error.rs @@ -2,6 +2,7 @@ use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltRelationId}; use jolt_riscv::JoltInstructionKind; +use jolt_transcript::TranscriptError; use crate::config::JoltProtocolConfig; use crate::stages::ids::{VerifierChallengeId, VerifierDerivedId}; @@ -9,6 +10,15 @@ use jolt_claims::protocols::composed::ComposedOpeningId; #[derive(Debug, thiserror::Error)] pub enum VerifierError { + #[error("proof transcript: {0}")] + Transcript(#[from] TranscriptError), + + #[error("proof header field {field} is outside its encoding")] + MalformedProofHeader { field: &'static str }, + + #[error("proof commitment is malformed: {reason}")] + MalformedCommitment { reason: String }, + #[error("proof protocol config {got:?} does not match verifier config {expected:?}")] ProtocolConfigMismatch { expected: JoltProtocolConfig, diff --git a/crates/jolt-verifier/src/lib.rs b/crates/jolt-verifier/src/lib.rs index a6eb1dc631..7e9c232c64 100644 --- a/crates/jolt-verifier/src/lib.rs +++ b/crates/jolt-verifier/src/lib.rs @@ -55,21 +55,15 @@ pub mod proof; pub mod stages; pub mod verifier; -pub use config::{ - validate_proof_config, JoltProtocolConfig, ScalarChallengeEndianness, ZkConfig, - JOLT_VERIFIER_CONFIG, -}; +pub use config::{validate_proof_config, JoltProtocolConfig, ZkConfig, JOLT_VERIFIER_CONFIG}; pub use error::VerifierError; pub use preprocessing::{ CommittedProgramPreprocessing, JoltVerifierPreprocessing, ProgramPreprocessing, }; -pub use proof::{ClearProofClaims, JoltProof, JoltProofClaims}; -#[cfg(feature = "akita")] -pub use verifier::absorb_packed_commitments; -#[cfg(not(feature = "akita"))] -pub use verifier::absorb_transcript_commitments; +pub use proof::{JoltProof, ProofCommitments, ProofHeader}; pub use verifier::{ - absorb_committed_program_commitments, absorb_transcript_preamble, validate_and_seed_transcript, - validate_inputs_from_parts, verify, verify_until_stage1, CheckedInputs, PreStage1VerifierState, - ProofTranscriptConfig, + absorb_public_commitments, absorb_public_preamble, jolt_protocol_id, seed_transcript, + validate_inputs, verify, CheckedInputs, JoltSponge, JOLT_SESSION, }; +#[cfg(not(feature = "akita"))] +pub use verifier::{verify_stages, VerifiedStages}; diff --git a/crates/jolt-verifier/src/proof.rs b/crates/jolt-verifier/src/proof.rs index ec3aa043a6..8f4819fe1a 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -1,164 +1,103 @@ //! Verifier-owned proof model types. +//! +//! A proof is its argument string. Its leading prover messages are the +//! [`ProofHeader`] and the [`ProofCommitments`]; every later message's width is +//! derived from them and from the verifier's preprocessing. -use jolt_blindfold::BlindFoldProof; +#[cfg(not(feature = "akita"))] +use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; pub use jolt_claims::protocols::jolt::TracePolynomialOrder; use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltReadWriteConfig}; -use jolt_crypto::{Commitment, VectorCommitment}; -use jolt_field::JoltField; use jolt_openings::CommitmentScheme; -use jolt_sumcheck::SumcheckProof; +use jolt_transcript::{ProverTranscript, Sponge, VerifierTranscript}; use serde::{Deserialize, Serialize}; -use crate::{ - config::JoltProtocolConfig, - stages::{ - stage1::outputs::Stage1OutputClaims, stage2::outputs::Stage2OutputClaims, - stage3::outputs::Stage3OutputClaims, stage4::outputs::Stage4OutputClaims, - stage5::outputs::Stage5OutputClaims, stage6a::outputs::Stage6aOutputClaims, - stage6b::outputs::Stage6bOutputClaims, stage7::outputs::Stage7OutputClaims, - }, - VerifierError, -}; - -/// The proof-carried polynomial commitments on the homomorphic build: one -/// commitment per committed polynomial. -#[cfg(not(feature = "akita"))] -pub type ProofCommitments = JoltCommitments<::Output>; -/// The proof-carried polynomial commitments on the `akita` build: the single -/// packed `OneHotTrace` commitment carrying every per-proof column. -#[cfg(feature = "akita")] -pub type ProofCommitments = ::Output; +use crate::{config::JoltProtocolConfig, num, VerifierError}; -/// The final-opening discharge on the homomorphic build: one RLC-batched PCS -/// opening proof at the unified point. -#[cfg(not(feature = "akita"))] -pub type JointOpeningProof = ::Proof; -#[cfg(feature = "akita")] -pub type JointOpeningProof = ::Proof; - -#[expect(non_snake_case, reason = "Preserves the deployed proof field name.")] +/// A Jolt proof: the argument string, plus the protocol axes it was produced +/// under so a build mismatch is reported as such instead of as a transcript +/// failure. The verifier binds its own configuration into the transcript, so +/// `protocol` carries no soundness weight. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound( - serialize = "PCS::Field: Serialize, ZkProof: Serialize", - deserialize = "PCS::Field: for<'a> Deserialize<'a>, ZkProof: serde::de::DeserializeOwned" -))] -pub struct JoltProof< - PCS, - VC, - ZkProof = BlindFoldProof<::Field, ::Output>, -> where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ +pub struct JoltProof { pub protocol: JoltProtocolConfig, - pub commitments: ProofCommitments, - pub stages: JoltStageProofs, - pub joint_opening_proof: JointOpeningProof, - pub untrusted_advice_commitment: Option, - /// Direct commitment to the full field-register increment polynomial. - /// Required for every Akita field-inline proof, including an all-zero trace. - /// Producers without field-inline semantics leave this absent and are rejected - /// by the protocol-config gate when field-inline is required. - #[cfg(all(feature = "akita", feature = "field-inline"))] - pub field_inc_commitment: Option, - pub claims: JoltProofClaims, + pub narg: Vec, +} + +/// The prover-chosen shape parameters, sent first. +#[expect(non_snake_case, reason = "Preserves the protocol's parameter name.")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ProofHeader { pub trace_length: usize, pub ram_K: usize, pub rw_config: JoltReadWriteConfig, pub one_hot_config: JoltOneHotConfig, pub trace_polynomial_order: TracePolynomialOrder, + pub untrusted_advice: bool, } -impl JoltProof -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - /// Assemble a proof without a field-inline payload. Producers with no field-inline - /// semantics (the legacy converters) build through here so they never name the - /// feature-gated slots; the modular provers with field-inline enabled attach theirs with - /// [`Self::with_field_inc_commitment`]. - #[expect( - clippy::too_many_arguments, - reason = "one argument per proof component, mirroring the wire struct" - )] - pub fn new( - protocol: JoltProtocolConfig, - commitments: ProofCommitments, - stages: JoltStageProofs, - joint_opening_proof: JointOpeningProof, - untrusted_advice_commitment: Option, - claims: JoltProofClaims, - trace_length: usize, - ram_k: usize, - rw_config: JoltReadWriteConfig, - one_hot_config: JoltOneHotConfig, - trace_polynomial_order: TracePolynomialOrder, - ) -> Self { - Self { - protocol, - commitments, - stages, - joint_opening_proof, - untrusted_advice_commitment, - #[cfg(all(feature = "akita", feature = "field-inline"))] - field_inc_commitment: None, - claims, +impl ProofHeader { + pub fn send(&self, transcript: &mut ProverTranscript) { + transcript.send(&num::u64_from_usize(self.trace_length)); + transcript.send(&num::u64_from_usize(self.ram_K)); + transcript.send_all(&[ + self.rw_config.ram_rw_phase1_num_rounds, + self.rw_config.ram_rw_phase2_num_rounds, + self.rw_config.registers_rw_phase1_num_rounds, + self.rw_config.registers_rw_phase2_num_rounds, + self.one_hot_config.log_k_chunk, + self.one_hot_config.lookups_ra_virtual_log_k_chunk, + ]); + transcript.send(&self.trace_polynomial_order.transcript_scalar()); + transcript.send(&u8::from(self.untrusted_advice)); + } + + /// Reads the header, rejecting values outside each field's encoding. The + /// values themselves are validated against the preprocessing by + /// [`validate_inputs`](crate::verifier::validate_inputs). + pub fn receive( + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + let trace_length = usize_field(transcript.receive::()?, "trace_length")?; + let ram_k = usize_field(transcript.receive::()?, "ram_K")?; + let [ram_phase1, ram_phase2, registers_phase1, registers_phase2, log_k_chunk, lookups_chunk]: [u8; 6] = + transcript.receive()?; + let trace_polynomial_order = TracePolynomialOrder::from_transcript_scalar( + transcript.receive::()?, + ) + .ok_or(VerifierError::MalformedProofHeader { + field: "trace_polynomial_order", + })?; + let untrusted_advice = match transcript.receive::()? { + 0 => false, + 1 => true, + _ => { + return Err(VerifierError::MalformedProofHeader { + field: "untrusted_advice", + }) + } + }; + Ok(Self { trace_length, ram_K: ram_k, - rw_config, - one_hot_config, + rw_config: JoltReadWriteConfig { + ram_rw_phase1_num_rounds: ram_phase1, + ram_rw_phase2_num_rounds: ram_phase2, + registers_rw_phase1_num_rounds: registers_phase1, + registers_rw_phase2_num_rounds: registers_phase2, + }, + one_hot_config: JoltOneHotConfig { + log_k_chunk, + lookups_ra_virtual_log_k_chunk: lookups_chunk, + }, trace_polynomial_order, - } - } - - /// Attach the direct field-increment commitment required by Akita field-inline proofs. - #[cfg(all(feature = "akita", feature = "field-inline"))] - pub fn with_field_inc_commitment(mut self, commitment: PCS::Output) -> Self { - self.field_inc_commitment = Some(commitment); - self + untrusted_advice, + }) } } -impl JoltProof -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - pub(crate) fn clear_claims(&self) -> Result<&ClearProofClaims, VerifierError> { - match &self.claims { - JoltProofClaims::Clear(claims) => Ok(claims), - JoltProofClaims::Zk { .. } => Err(VerifierError::UnexpectedBlindFoldProof), - } - } - - #[cfg(not(feature = "akita"))] - pub(crate) fn blindfold_proof(&self) -> Result<&ZkProof, VerifierError> { - match &self.claims { - JoltProofClaims::Clear(_) => Err(VerifierError::MissingBlindFoldProof), - JoltProofClaims::Zk { blindfold_proof } => Ok(blindfold_proof), - } - } - - /// Replace the claims payload, retyping the `ZkProof` slot; every wire - /// field carries over unchanged. - pub fn with_claims(self, claims: JoltProofClaims) -> JoltProof { - JoltProof { - protocol: self.protocol, - commitments: self.commitments, - stages: self.stages, - joint_opening_proof: self.joint_opening_proof, - untrusted_advice_commitment: self.untrusted_advice_commitment, - #[cfg(all(feature = "akita", feature = "field-inline"))] - field_inc_commitment: self.field_inc_commitment, - claims, - trace_length: self.trace_length, - ram_K: self.ram_K, - rw_config: self.rw_config, - one_hot_config: self.one_hot_config, - trace_polynomial_order: self.trace_polynomial_order, - } - } +fn usize_field(value: u64, field: &'static str) -> Result { + usize::try_from(value).map_err(|_| VerifierError::MalformedProofHeader { field }) } /// The field-register commitments of the field-inline extension. `FieldRdInc` is the @@ -166,7 +105,7 @@ where /// (anchored through the bytecode read-RAF path), so this nest stays one deep until the /// protocol commits more. #[cfg(feature = "field-inline")] -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct FieldRegistersCommitments { pub rd_inc: C, } @@ -174,147 +113,129 @@ pub struct FieldRegistersCommitments { /// The field-inline extension's committed payload, grouped by component as the /// protocol spec lays it out (`FieldInlineCommitments::field_registers`). #[cfg(feature = "field-inline")] -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct FieldInlineCommitments { pub field_registers: FieldRegistersCommitments, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +/// One commitment per committed trace polynomial on the homomorphic build. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct JoltCommitments { pub rd_inc: C, pub ram_inc: C, pub instruction_ra: Vec, pub ram_ra: Vec, pub bytecode_ra: Vec, - /// Present on every field-inline proof (the build with field-inline enabled proves all - /// guests under the composed protocol). Carried as an `Option` because this type is shared - /// with producers that cannot supply field-inline commitments — the legacy prover and the - /// packed converter — whose proofs fail the protocol-config gate before this field is ever - /// read; [`validate_proof_consistency`] rejects a missing payload fail-closed for - /// everything else. - /// - /// [`validate_proof_consistency`]: crate::verifier::validate_proof_consistency #[cfg(feature = "field-inline")] - pub field_inline: Option>, -} - -impl JoltCommitments { - pub fn new( - rd_inc: C, - ram_inc: C, - instruction_ra: Vec, - ram_ra: Vec, - bytecode_ra: Vec, - ) -> Self { - Self { - rd_inc, - ram_inc, - instruction_ra, - ram_ra, - bytecode_ra, - #[cfg(feature = "field-inline")] - field_inline: None, - } - } - - /// Attach the field-inline committed payload (the modular prover's stage 0 sets this on - /// every field-inline proof). - #[cfg(feature = "field-inline")] - pub fn with_field_inline(mut self, field_inline: FieldInlineCommitments) -> Self { - self.field_inline = Some(field_inline); - self - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr( - not(feature = "akita"), - expect( - clippy::large_enum_variant, - reason = "clear claims stay inline on the standard verifier's common path" - ) -)] -#[serde(bound( - serialize = "F: Serialize, ZkProof: Serialize", - deserialize = "F: for<'a> Deserialize<'a>, ZkProof: serde::de::DeserializeOwned" -))] -pub enum JoltProofClaims -where - F: JoltField, -{ - Clear(ClearProofClaims), - Zk { blindfold_proof: ZkProof }, -} - -impl JoltProofClaims -where - F: JoltField, -{ - pub const fn is_zk(&self) -> bool { - matches!(self, Self::Zk { .. }) - } + pub field_inline: FieldInlineCommitments, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct ClearProofClaims { - pub stage1: Stage1OutputClaims, - pub stage2: Stage2OutputClaims, - pub stage3: Stage3OutputClaims, - pub stage4: Stage4OutputClaims, - pub stage5: Stage5OutputClaims, - pub stage6a: Stage6aOutputClaims, - pub stage6b: Stage6bOutputClaims, - pub stage7: Stage7OutputClaims, +/// The polynomial commitments a proof sends, in send order. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProofCommitments { + /// One commitment per committed trace polynomial. + #[cfg(not(feature = "akita"))] + pub trace: JoltCommitments, + /// The single packed `OneHotTrace` commitment carrying every per-proof column. + #[cfg(feature = "akita")] + pub one_hot_trace: C, + /// The direct commitment to the full field-register increment polynomial. + #[cfg(all(feature = "akita", feature = "field-inline"))] + pub field_inc: C, + /// Present exactly when the header declares untrusted advice. + pub untrusted_advice: Option, } -impl ClearProofClaims { - /// Assemble the clear claims from the stage outputs. - #[expect( - clippy::too_many_arguments, - reason = "one argument per stage, mirroring the wire struct" - )] - pub fn new( - stage1: Stage1OutputClaims, - stage2: Stage2OutputClaims, - stage3: Stage3OutputClaims, - stage4: Stage4OutputClaims, - stage5: Stage5OutputClaims, - stage6a: Stage6aOutputClaims, - stage6b: Stage6bOutputClaims, - stage7: Stage7OutputClaims, - ) -> Self { - Self { - stage1, - stage2, - stage3, - stage4, - stage5, - stage6a, - stage6b, - stage7, +impl ProofCommitments { + pub fn send(&self, transcript: &mut ProverTranscript) + where + PCS: CommitmentScheme, + H: Sponge, + { + #[cfg(not(feature = "akita"))] + { + let trace = &self.trace; + PCS::send_commitment(&trace.rd_inc, transcript); + PCS::send_commitment(&trace.ram_inc, transcript); + for commitment in trace + .instruction_ra + .iter() + .chain(&trace.ram_ra) + .chain(&trace.bytecode_ra) + { + PCS::send_commitment(commitment, transcript); + } + #[cfg(feature = "field-inline")] + PCS::send_commitment(&trace.field_inline.field_registers.rd_inc, transcript); + } + #[cfg(feature = "akita")] + { + PCS::send_commitment(&self.one_hot_trace, transcript); + #[cfg(feature = "field-inline")] + PCS::send_commitment(&self.field_inc, transcript); + } + if let Some(commitment) = &self.untrusted_advice { + PCS::send_commitment(commitment, transcript); } } -} -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound( - serialize = "F: Serialize, ::Output: Serialize", - deserialize = "F: for<'a> Deserialize<'a>, ::Output: serde::de::DeserializeOwned" -))] -pub struct JoltStageProofs -where - F: JoltField, - VC: VectorCommitment, -{ - pub stage1_uni_skip_first_round_proof: SumcheckProof, - pub stage1_sumcheck_proof: SumcheckProof, - pub stage2_uni_skip_first_round_proof: SumcheckProof, - pub stage2_sumcheck_proof: SumcheckProof, - pub stage3_sumcheck_proof: SumcheckProof, - pub stage4_sumcheck_proof: SumcheckProof, - pub stage5_sumcheck_proof: SumcheckProof, - pub stage6a_sumcheck_proof: SumcheckProof, - pub stage6b_sumcheck_proof: SumcheckProof, - pub stage7_sumcheck_proof: SumcheckProof, + /// Reads the commitments at the counts `layout` (homomorphic build) and + /// `header` fix. + pub fn receive( + setup: &PCS::VerifierSetup, + header: &ProofHeader, + #[cfg(not(feature = "akita"))] layout: JoltRaPolynomialLayout, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result + where + PCS: CommitmentScheme, + H: Sponge, + { + let mut receive = || { + PCS::receive_commitment(setup, transcript).map_err(|error| { + VerifierError::MalformedCommitment { + reason: error.to_string(), + } + }) + }; + #[cfg(not(feature = "akita"))] + let trace = { + let rd_inc = receive()?; + let ram_inc = receive()?; + let instruction_ra = (0..layout.instruction()) + .map(|_| receive()) + .collect::>()?; + let ram_ra = (0..layout.ram()) + .map(|_| receive()) + .collect::>()?; + let bytecode_ra = (0..layout.bytecode()) + .map(|_| receive()) + .collect::>()?; + JoltCommitments { + rd_inc, + ram_inc, + instruction_ra, + ram_ra, + bytecode_ra, + #[cfg(feature = "field-inline")] + field_inline: FieldInlineCommitments { + field_registers: FieldRegistersCommitments { rd_inc: receive()? }, + }, + } + }; + #[cfg(feature = "akita")] + let one_hot_trace = receive()?; + #[cfg(all(feature = "akita", feature = "field-inline"))] + let field_inc = receive()?; + let untrusted_advice = header.untrusted_advice.then(&mut receive).transpose()?; + Ok(Self { + #[cfg(not(feature = "akita"))] + trace, + #[cfg(feature = "akita")] + one_hot_trace, + #[cfg(all(feature = "akita", feature = "field-inline"))] + field_inc, + untrusted_advice, + }) + } } diff --git a/crates/jolt-verifier/src/stages/mod.rs b/crates/jolt-verifier/src/stages/mod.rs index 152af3fed0..37ce99a7b6 100644 --- a/crates/jolt-verifier/src/stages/mod.rs +++ b/crates/jolt-verifier/src/stages/mod.rs @@ -17,7 +17,6 @@ use jolt_lookup_tables::XLEN as RISCV_XLEN; use jolt_openings::CommitmentScheme; use crate::preprocessing::JoltVerifierPreprocessing; -use crate::proof::JoltProof; use crate::verifier::CheckedInputs; use crate::VerifierError; @@ -44,8 +43,7 @@ pub mod zk; /// length, RAM size), mapping the layout error to `stage`. Built once during /// verification and shared by the stages (5-8) that derive their RA layouts from /// it, and reused by the BlindFold input derivation. -pub fn build_formula_dimensions( - proof: &JoltProof, +pub fn build_formula_dimensions( preprocessing: &JoltVerifierPreprocessing, checked: &CheckedInputs, log_t: usize, @@ -56,7 +54,7 @@ where VC: VectorCommitment, { formula_dimensions_from_parts( - proof.one_hot_config, + checked.one_hot_config, log_t, preprocessing.program.bytecode_len(), checked.ram_K, diff --git a/crates/jolt-verifier/src/stages/relations.rs b/crates/jolt-verifier/src/stages/relations.rs index cbb5c860ad..e947be33ae 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -9,9 +9,10 @@ //! This makes the canonical opening **order** and **count** a single-sourced //! consequence of a struct's field declaration order. //! -//! Transcript I/O stays here: [`OutputAppend::append_openings`] is a thin -//! verifier-side consumer of [`OutputClaims::opening_values`], so `jolt-claims` -//! stays transcript-free while the Fiat-Shamir order remains single-sourced. +//! Transcript I/O stays here: [`receive_member_openings`] reads a relation's +//! produced openings in the canonical order [`OutputClaims::opening_values`] +//! defines, so `jolt-claims` stays transcript-free while the Fiat-Shamir order +//! remains single-sourced. pub use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; @@ -21,11 +22,11 @@ pub use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; pub use jolt_verifier_derive::SumcheckBatch; use core::fmt::Debug; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use jolt_claims::SymbolicSumcheck; use jolt_field::JoltField; -use jolt_transcript::Transcript; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; use crate::stages::ids::{VerifierChallengeId, VerifierDerivedId}; use crate::VerifierError; @@ -35,27 +36,6 @@ use crate::VerifierError; /// protocol family can chain into it. pub use jolt_claims::protocols::composed::ComposedOpeningId; -/// Transcript-side companion to [`OutputClaims`]: append a relation's produced -/// openings to the Fiat-Shamir transcript in canonical order. -/// -/// This lives in `jolt-verifier` (not `jolt-claims`) because it needs a -/// `Transcript`; `jolt-claims` stays transcript-free. It is a blanket extension -/// over every `OutputClaims` implementor, so the Fiat-Shamir order is -/// single-sourced by [`OutputClaims::opening_values`] and cannot disagree with it. -pub trait OutputAppend: OutputClaims { - /// Append every produced opening to the transcript in canonical - /// ([`OutputClaims::opening_values`]) order, each under the `b"opening_claim"` - /// label. This is the Fiat-Shamir order and MUST match the order in which the - /// prover commits the openings. - fn append_openings>(&self, transcript: &mut T) { - for value in self.opening_values() { - transcript.append_labeled(b"opening_claim", &value); - } - } -} - -impl> OutputAppend for C {} - /// The drawn Fiat-Shamir challenges of a [`ConcreteSumcheck`] instance: a readable /// alias for the relation's `Challenges` projection through its symbolic /// relation. This is the struct [`ConcreteSumcheck::draw_challenges`] returns and @@ -137,27 +117,27 @@ where /// transcript, in the exact order the stage's inline draw uses. Batch-level /// coefficients and the shared binding vector are NOT drawn here. /// - /// The default draws one `challenge_scalar` per `Challenges` field, in - /// declaration order, via [`SumcheckChallenges::from_transcript_values`]. This is - /// the correct draw for the common case — a relation whose challenges are each a - /// single `challenge_scalar` (and for [`NoChallenges`](::jolt_claims::NoChallenges), - /// which has no fields, it draws nothing). A `challenge_scalar_powers(n)` draw - /// reduces to this case: it performs exactly one squeeze and the relation keeps - /// the degree-1 power, which equals that squeezed scalar. Only relations whose - /// draw is genuinely different — an extra transcript append (a domain - /// separator), a value re-roll, or a powers draw whose kept value is not the - /// squeezed scalar — override this. + /// The default draws one exactly uniform `challenge` per `Challenges` field, + /// in declaration order, via [`SumcheckChallenges::from_transcript_values`]. + /// This is the correct draw for the common case — a relation whose challenges + /// are each a single `challenge` (and for + /// [`NoChallenges`](::jolt_claims::NoChallenges), which has no fields, it draws + /// nothing). A `challenge_powers(n)` draw reduces to this case: it performs + /// exactly one draw and the relation keeps the degree-1 power, which equals + /// that drawn scalar. Only relations whose draw is genuinely different — an + /// extra transcript absorb (a domain separator), a value re-roll, or a powers + /// draw whose kept value is not the drawn scalar — override this. /// /// The bound is `SumcheckChallenges` — which every `Challenges` already /// implements — so the default needs no separate `Default` derive. It errors /// only if the per-field draw cannot populate the struct, which cannot happen for - /// the infinite `challenge_scalar` stream the default supplies. - fn draw_challenges>( + /// the infinite `challenge` stream the default supplies. + fn draw_challenges( &self, - transcript: &mut T, + transcript: &mut C, ) -> Result, VerifierError> { SumcheckChallenges::from_transcript_values(::core::iter::repeat_with(|| { - transcript.challenge_scalar() + transcript.challenge() })) .map_err(VerifierError::from) } @@ -372,8 +352,7 @@ where /// Assert an optional member's output-claims presence agrees with the instance: /// reject a present instance missing its claims cell, and reject claims /// supplied for an absent instance. Called by the generated -/// `validate_output_claims` for each `Option` member (before its shape check), -/// and directly by a stage that curates its own shape checks (stage 6b). +/// `validate_output_claims` for each `Option` member (before its shape check). pub fn validate_member_presence( member: Option<&I>, claims: Option<&SumcheckOutputClaims>, @@ -422,6 +401,107 @@ where claims.resolve_output(&native) } +/// Receive one batch member's produced openings: its +/// [`wire_output_openings`](ConcreteSumcheck::wire_output_openings) in canonical +/// order, which is the order the prover's clear recorder sends them. Aliased +/// openings are not sent; [`assemble_member_claims`] fills them from their +/// sources. Called by the generated `receive_output_claims` per member. +pub fn receive_member_openings( + member: &I, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> +where + F: JoltField, + I: ConcreteSumcheck, + H: Sponge, + SumcheckOutputClaims: OutputClaims>, + OpeningIdOf: Copy + Ord + Debug + Into, +{ + let aliased: BTreeSet = I::aliased_output_openings() + .into_iter() + .map(|(aliased, _)| aliased.into()) + .collect(); + receive_member_openings_except(member, |id| aliased.contains(id), transcript) +} + +/// [`receive_member_openings`] with the skipped (aliased) openings chosen by +/// `skip` instead of the member's static alias declaration, for a stage whose +/// dedup is decided at runtime by opening-point equality. +pub fn receive_member_openings_except( + member: &I, + skip: impl Fn(&ComposedOpeningId) -> bool, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> +where + F: JoltField, + I: ConcreteSumcheck, + H: Sponge, + SumcheckOutputClaims: OutputClaims>, + OpeningIdOf: Copy + Ord + Debug + Into, +{ + let wire = member.wire_output_openings(); + let aliased: BTreeSet<_> = I::aliased_output_openings() + .into_iter() + .map(|(aliased, _)| aliased) + .collect(); + // A placeholder struct holding exactly the produced openings yields their + // canonical order without restating the relation's field layout. + let shape = SumcheckOutputClaims::::from_opening_values(|id| { + (wire.contains(id) || aliased.contains(id)).then(F::zero) + }) + .map_err(|missing| VerifierError::MissingOpeningClaim { + id: missing.id.into(), + })?; + shape + .canonical_order() + .into_iter() + .map(Into::into) + .filter(|id| !skip(id)) + .map(|id| Ok((id, transcript.receive()?))) + .collect() +} + +/// Assemble one batch member's claims from the batch's received openings +/// (keyed by composite id). Each aliased opening takes its canonical source's +/// value, so an alias can never disagree with its source. Called by the +/// generated `receive_output_claims` per member. +pub fn assemble_member_claims( + received: &BTreeMap, +) -> Result, VerifierError> +where + F: JoltField, + I: ConcreteSumcheck, + SumcheckOutputClaims: OutputClaims>, + OpeningIdOf: Copy + Ord + Debug + Into, +{ + let sources: BTreeMap = I::aliased_output_openings() + .into_iter() + .map(|(aliased, source)| (aliased.into(), source.into())) + .collect(); + assemble_member_claims_with::(received, |id| sources.get(id).copied()) +} + +/// [`assemble_member_claims`] with each aliased opening's source chosen by +/// `source` instead of the member's static alias declaration. +pub fn assemble_member_claims_with( + received: &BTreeMap, + source: impl Fn(&ComposedOpeningId) -> Option, +) -> Result, VerifierError> +where + F: JoltField, + I: ConcreteSumcheck, + SumcheckOutputClaims: OutputClaims>, + OpeningIdOf: Copy + Ord + Debug + Into, +{ + SumcheckOutputClaims::::from_opening_values(|id| { + let id: ComposedOpeningId = (*id).into(); + received.get(&source(&id).unwrap_or(id)).copied() + }) + .map_err(|missing| VerifierError::MissingOpeningClaim { + id: missing.id.into(), + }) +} + /// Enforce one member's declared cross-relation opening aliases: each aliased /// wire cell (resolved from the DECLARING member's claims) must equal its /// canonical source opening, resolved across the batch by `resolve_source` diff --git a/crates/jolt-verifier/src/stages/stage1/verify.rs b/crates/jolt-verifier/src/stages/stage1/verify.rs index 1f79982875..24a87e0c1e 100644 --- a/crates/jolt-verifier/src/stages/stage1/verify.rs +++ b/crates/jolt-verifier/src/stages/stage1/verify.rs @@ -1,8 +1,6 @@ -use jolt_claims::protocols::jolt::{geometry::spartan::SpartanOuterDimensions, JoltRelationId}; -use jolt_crypto::VectorCommitment; -use jolt_field::Ring; -use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_claims::protocols::jolt::geometry::spartan::SpartanOuterDimensions; +use jolt_field::{CanonicalDecode, JoltField}; +use jolt_transcript::{Sponge, VerifierTranscript}; use super::outer_remainder::{outer_remainder_input_values_from_uniskip_output, OuterRemainder}; use super::outputs::{ @@ -10,22 +8,20 @@ use super::outputs::{ Stage1Output, Stage1ZkOutput, }; use crate::{ - proof::JoltProof, - stages::{uniskip, zk::committed}, + stages::{uniskip, zk::committed, zk::outputs::CommittedOutputClaimOutput}, verifier::CheckedInputs, VerifierError, }; #[jolt_verifier_derive::fs_scope(Stage1)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, - proof: &JoltProof, - transcript: &mut T, -) -> Result, VerifierError> + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, + F: JoltField, + C: CanonicalDecode, + H: Sponge, { let uniskip_params = uniskip::UniskipParams::spartan_outer(); let log_t = crate::num::ilog2(checked.trace_length); @@ -33,23 +29,11 @@ where let tau = uniskip::draw_spartan_outer_tau(transcript, log_t); if !checked.zk { - let claims = &proof.clear_claims()?.stage1; - let uni_skip_first_round_proof = &proof.stages.stage1_uni_skip_first_round_proof; - let sumcheck_proof = &proof.stages.stage1_sumcheck_proof; - // The uni-skip first round consumes no openings: its symbolic `input_expression` // is `zero` (jolt-claims `spartan/outer_uniskip.rs`), so the input claim is the // constant zero. BlindFold still single-sources this claim from that same symbolic // Expr, and muldiv (host) catches any drift between the two. - let uniskip_input_claim = PCS::Field::from_u64(0); - let uniskip_output_claim = claims.uniskip_output_claim; - let uniskip_challenge = uniskip::verify_clear( - uni_skip_first_round_proof, - &uniskip_params, - uniskip_input_claim, - uniskip_output_claim, - transcript, - )?; + let uniskip = uniskip::verify_clear(&uniskip_params, F::from_u64(0), transcript)?; // Built after the uni-skip step so the relation carries `tau` and the // uni-skip reduction challenge; the coefficient table completes itself from @@ -57,46 +41,33 @@ where // (no-op) member draw are transcript-neutral, so their position relative to // the uni-skip is immaterial. let sumchecks = Stage1BatchSumchecks { - outer_remainder: OuterRemainder::new(dimensions, tau, uniskip_challenge), + outer_remainder: OuterRemainder::new(dimensions, tau, uniskip.challenge), }; let batch_challenges = sumchecks.draw_challenges(transcript)?; let input_points = sumchecks.empty_input_points(); - sumchecks.validate_output_claims(&claims.outer)?; - // The remainder consumes the uni-skip's reduced opening as its input claim // (the relation's `input_claim` is the bare consumed opening). let input_values = Stage1BatchInputClaims { - outer_remainder: outer_remainder_input_values_from_uniskip_output(uniskip_output_claim), + outer_remainder: outer_remainder_input_values_from_uniskip_output(uniskip.output_claim), }; - let output_points = sumchecks.verify_clear( + let (output_points, output_values) = sumchecks.verify_clear( &input_values, &input_points, &batch_challenges, - &claims.outer, - sumcheck_proof, transcript, 1, )?; - // Append the 35 produced openings in canonical (declaration) order, matching - // the prover's commitment order. - sumchecks.append_output_claims(transcript, &claims.outer); - return Ok(Stage1Output::Clear(Stage1ClearOutput { - output_values: claims.outer.clone(), + output_values, output_points, })); } { - let uniskip = uniskip::verify_zk( - checked, - &proof.stages.stage1_uni_skip_first_round_proof, - &uniskip_params, - transcript, - )?; + let uniskip = uniskip::verify_zk(checked, &uniskip_params, transcript)?; let uniskip_challenge = uniskip.challenge; // Built after the uni-skip step so the relation carries `tau` and the @@ -108,17 +79,10 @@ where }; let input_points = sumchecks.empty_input_points(); - let remainder_consistency = - sumchecks.verify_zk(&proof.stages.stage1_sumcheck_proof, transcript)?; // Clear and committed proofs share the composed member's canonical order. - let output_claim_count = sumchecks.output_claim_count(); - let remainder_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage1_sumcheck_proof, - "stage1_sumcheck_proof", - output_claim_count, - JoltRelationId::SpartanOuter, - )?; + let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; + let (remainder_consistency, commitments) = + sumchecks.verify_zk(shape.row_count(), transcript)?; let output_points = sumchecks.derive_opening_points(&remainder_consistency.challenges(), &input_points)?; @@ -130,7 +94,7 @@ where uniskip_consistency: uniskip.consistency, uniskip_output_claims: uniskip.output_claims, remainder_consistency, - remainder_output_claims, + remainder_output_claims: CommittedOutputClaimOutput { shape, commitments }, output_points, })) } diff --git a/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs index 155df892a4..b645d13d41 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs @@ -18,7 +18,7 @@ use jolt_claims::SymbolicSumcheck; use jolt_field::JoltField; use jolt_poly::{range_mask_mle_msb, sparse_segments_mle_msb, try_eq_mle}; use jolt_program::preprocess::PublicIoMemory; -use jolt_transcript::Transcript; +use jolt_transcript::Channel; use crate::stages::relations::ConcreteSumcheck; use crate::VerifierError; @@ -117,13 +117,13 @@ impl ConcreteSumcheck for RamOutputCheck { /// MUST stay `challenge()` (not `challenge_scalar()`): both decode the same /// 16-byte squeeze, but differently, so switching would silently change the /// address point values without changing the transcript bytes. - fn draw_challenges>( + fn draw_challenges( &self, - transcript: &mut T, + transcript: &mut C, ) -> Result, VerifierError> { Ok(RamOutputCheckChallenges { output_address: (0..self.read_write_dimensions.log_k()) - .map(|_| transcript.challenge()) + .map(|_| transcript.challenge_small()) .collect(), }) } diff --git a/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs b/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs index 64257b7581..eb81370097 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs @@ -128,7 +128,7 @@ mod tests { use super::*; use crate::stages::relations::draw_recording::{record, DrawEvent}; use jolt_field::Fr; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; // Representative of the 14 single-`challenge_scalar` relations that inherit the // default `draw_challenges`: the inline `ram_read_write_gamma = challenge_scalar()` @@ -139,7 +139,7 @@ mod tests { let relation = RamReadWriteChecking::::new(ReadWriteDimensions::new(4, 3, 2, 1), 3, Vec::new()); - let (inline_events, inline_gamma) = record(|t| t.challenge_scalar()); + let (inline_events, inline_gamma) = record(|t| t.challenge()); let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); assert_eq!(draw_events, inline_events); diff --git a/crates/jolt-verifier/src/stages/stage2/verify.rs b/crates/jolt-verifier/src/stages/stage2/verify.rs index 2bf043cc74..00fa4dd9d1 100644 --- a/crates/jolt-verifier/src/stages/stage2/verify.rs +++ b/crates/jolt-verifier/src/stages/stage2/verify.rs @@ -8,11 +8,9 @@ use jolt_claims::protocols::jolt::{ JoltRelationId, }; use jolt_claims::NoChallenges; -use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; -use jolt_openings::CommitmentScheme; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_program::preprocess::PublicIoMemory; -use jolt_transcript::Transcript; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(feature = "field-inline")] use super::field_registers_claim_reduction::{ @@ -33,12 +31,11 @@ use super::{ ram_read_write_checking::{ram_read_write_input_values_from_upstream, RamReadWriteChecking}, }; use crate::{ - proof::JoltProof, stages::{ relations::ConcreteSumcheck, stage1::{Stage1ClearOutput, Stage1Output}, uniskip, - zk::committed, + zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -56,7 +53,7 @@ struct ProductUniskipStep { } enum ProductUniskipVerified { - Clear, + Clear { output_claim: F }, Zk(uniskip::UniskipZk), } @@ -85,16 +82,15 @@ pub fn stage2_batch_input_values_from_upstream( } #[jolt_verifier_derive::fs_scope(Stage2)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, - proof: &JoltProof, - transcript: &mut T, - stage1: &Stage1Output, -) -> Result, VerifierError> + transcript: &mut VerifierTranscript<'_, H>, + stage1: &Stage1Output, +) -> Result, VerifierError> where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, + F: JoltField, + C: CanonicalDecode, + H: Sponge, { match (checked.zk, stage1) { (true, Stage1Output::Clear(_)) => { @@ -109,7 +105,7 @@ where let log_t = crate::num::ilog2(checked.trace_length); let log_k = crate::num::ilog2(checked.ram_K); let trace_dimensions = TraceDimensions::new(log_t); - let read_write_dimensions = proof.rw_config.ram_dimensions(log_t, log_k); + let read_write_dimensions = checked.rw_config.ram_dimensions(log_t, log_k); let product_dimensions = SpartanProductDimensions::new(log_t); let raf_dimensions = RamRafEvaluationDimensions::try_from(read_write_dimensions).map_err(|error| { @@ -119,8 +115,7 @@ where } })?; - let uniskip = - verify_product_uniskip::(checked, proof, transcript, stage1)?; + let uniskip = verify_product_uniskip(checked, transcript, stage1)?; // Build the batch relations once, pre-branch; each owns its input/output // claim algebra (single-sourced with its jolt-claims formula and the BlindFold @@ -181,16 +176,10 @@ where field: "stage2_uni_skip_first_round_proof", }); }; - let consistency = sumchecks.verify_zk(&proof.stages.stage2_sumcheck_proof, transcript)?; // Both modes omit aliases from the canonical member output rows. - let output_claim_count = sumchecks.output_claim_count(); - let batch_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage2_sumcheck_proof, - "stage2_sumcheck_proof", - output_claim_count, - JoltRelationId::RamReadWriteChecking, - )?; + let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; + let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; + let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; let output_points = sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; @@ -207,32 +196,24 @@ where })); } - let ProductUniskipVerified::Clear = uniskip.verified else { + let ProductUniskipVerified::Clear { + output_claim: product_uniskip_output_claim, + } = uniskip.verified + else { return Err(VerifierError::ExpectedClearProof { field: "stage2_uni_skip_first_round_proof", }); }; let stage1 = stage1.clear()?; - let claims = &proof.clear_claims()?.stage2; - sumchecks.validate_output_claims(&claims.batch_outputs)?; let input_values = - stage2_batch_input_values_from_upstream(stage1, claims.product_uniskip_output_claim); + stage2_batch_input_values_from_upstream(stage1, product_uniskip_output_claim); - let output_points = sumchecks.verify_clear( - &input_values, - &input_points, - &challenges, - &claims.batch_outputs, - &proof.stages.stage2_sumcheck_proof, - transcript, - 2, - )?; - - sumchecks.append_output_claims(transcript, &claims.batch_outputs); + let (output_points, output_values) = + sumchecks.verify_clear(&input_values, &input_points, &challenges, transcript, 2)?; Ok(Stage2Output::Clear(Stage2ClearOutput { - output_values: claims.batch_outputs.clone(), + output_values, output_points, product_tau_low: uniskip.tau_low, })) @@ -265,16 +246,15 @@ pub fn product_tau_low( Ok(tau_low) } -fn verify_product_uniskip( +fn verify_product_uniskip( checked: &CheckedInputs, - proof: &JoltProof, - transcript: &mut T, - stage1: &Stage1Output, -) -> Result, VerifierError> + transcript: &mut VerifierTranscript<'_, H>, + stage1: &Stage1Output, +) -> Result, VerifierError> where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, + F: JoltField, + C: CanonicalDecode, + H: Sponge, { let log_t = crate::num::ilog2(checked.trace_length); let product_dimensions = SpartanProductDimensions::new(log_t); @@ -284,33 +264,23 @@ where let uniskip_params = uniskip::UniskipParams::spartan_product(); match stage1 { Stage1Output::Clear(stage1) => { - let claims = &proof.clear_claims()?.stage2; let uniskip_relation = ProductUniskip::new(product_dimensions, tau_high); let uniskip_input_values = product_uniskip_input_values_from_stage1(stage1); let uniskip_input_claim = uniskip_relation.input_claim(&uniskip_input_values, &NoChallenges::default())?; - let challenge = uniskip::verify_clear( - &proof.stages.stage2_uni_skip_first_round_proof, - &uniskip_params, - uniskip_input_claim, - claims.product_uniskip_output_claim, - transcript, - )?; + let verified = uniskip::verify_clear(&uniskip_params, uniskip_input_claim, transcript)?; Ok(ProductUniskipStep { tau_low, tau_high, - challenge, - verified: ProductUniskipVerified::Clear, + challenge: verified.challenge, + verified: ProductUniskipVerified::Clear { + output_claim: verified.output_claim, + }, }) } Stage1Output::Zk(_) => { - let verified = uniskip::verify_zk( - checked, - &proof.stages.stage2_uni_skip_first_round_proof, - &uniskip_params, - transcript, - )?; + let verified = uniskip::verify_zk(checked, &uniskip_params, transcript)?; Ok(ProductUniskipStep { tau_low, tau_high, diff --git a/crates/jolt-verifier/src/stages/stage3/verify.rs b/crates/jolt-verifier/src/stages/stage3/verify.rs index 1acd29cb37..db9ec50f54 100644 --- a/crates/jolt-verifier/src/stages/stage3/verify.rs +++ b/crates/jolt-verifier/src/stages/stage3/verify.rs @@ -1,10 +1,8 @@ //! Stage 3 verifier: Spartan shift, instruction input, and register reduction. -use jolt_claims::protocols::jolt::{geometry::dimensions::TraceDimensions, JoltRelationId}; -use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; -use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; +use jolt_field::{CanonicalDecode, JoltField}; +use jolt_transcript::{Sponge, VerifierTranscript}; use super::{ instruction_input::{instruction_input_input_values_from_upstream, InstructionInput}, @@ -17,11 +15,10 @@ use super::{ spartan_shift::{spartan_shift_input_values_from_upstream, SpartanShift}, }; use crate::{ - proof::JoltProof, stages::{ stage1::{Stage1BatchOutputClaims, Stage1Output}, stage2::{Stage2BatchOutputClaims, Stage2Output}, - zk::committed, + zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -43,17 +40,16 @@ pub fn stage3_input_values_from_upstream( } #[jolt_verifier_derive::fs_scope(Stage3)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, - proof: &JoltProof, - transcript: &mut T, - stage1: &Stage1Output, - stage2: &Stage2Output, -) -> Result, VerifierError> + transcript: &mut VerifierTranscript<'_, H>, + stage1: &Stage1Output, + stage2: &Stage2Output, +) -> Result, VerifierError> where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, + F: JoltField, + C: CanonicalDecode, + H: Sponge, { let log_t = crate::num::ilog2(checked.trace_length); let dimensions = TraceDimensions::new(log_t); @@ -80,42 +76,26 @@ where let challenges = sumchecks.draw_challenges(transcript)?; if !checked.zk { - let claims = &proof.clear_claims()?.stage3; let stage1 = stage1.clear()?; let stage2 = stage2.clear()?; - sumchecks.validate_output_claims(claims)?; let input_values = stage3_input_values_from_upstream(&stage1.output_values, &stage2.output_values); let input_points = sumchecks.empty_input_points(); - let output_points = sumchecks.verify_clear( - &input_values, - &input_points, - &challenges, - claims, - &proof.stages.stage3_sumcheck_proof, - transcript, - 3, - )?; - - sumchecks.append_output_claims(transcript, claims); + let (output_points, output_values) = + sumchecks.verify_clear(&input_values, &input_points, &challenges, transcript, 3)?; return Ok(Stage3Output::Clear(Stage3ClearOutput { - output_values: claims.clone(), + output_values, output_points, })); } { - let consistency = sumchecks.verify_zk(&proof.stages.stage3_sumcheck_proof, transcript)?; - let batch_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage3_sumcheck_proof, - "stage3_sumcheck_proof", - sumchecks.output_claim_count(), - JoltRelationId::SpartanShift, - )?; + let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; + let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; + let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; let output_points = sumchecks .derive_opening_points(&consistency.challenges(), &sumchecks.empty_input_points())?; diff --git a/crates/jolt-verifier/src/stages/stage4/outputs.rs b/crates/jolt-verifier/src/stages/stage4/outputs.rs index a5b366e33d..e11428b0f0 100644 --- a/crates/jolt-verifier/src/stages/stage4/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage4/outputs.rs @@ -1,17 +1,24 @@ //! Typed inputs consumed and outputs produced by stage 4 verification. +use std::collections::BTreeMap; + use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; -use jolt_transcript::Transcript; +use jolt_transcript::{Sponge, VerifierTranscript}; -use crate::stages::relations::{OutputClaims, SumcheckBatch}; +use crate::stages::relations::{ + assemble_member_claims, receive_member_openings, OutputClaims, SumcheckBatch, +}; use crate::stages::zk::outputs::CommittedOutputClaimOutput; +use crate::VerifierError; #[cfg(feature = "field-inline")] pub use super::field_registers_read_write_checking::{ FieldRegistersReadWriteChecking, FieldRegistersReadWriteOutputClaims, }; -use super::ram_val_check::{RamValCheck, RamValCheckInitialEvaluation, RamValCheckOutputClaims}; +use super::ram_val_check::{ + RamValCheck, RamValCheckInitialEvaluation, RamValCheckOutputClaims, RamValCheckStagedOpenings, +}; use super::registers_read_write_checking::{ RegistersReadWriteChecking, RegistersReadWriteOutputClaims, }; @@ -71,25 +78,74 @@ impl Stage4OutputClaims { impl Stage4Sumchecks { /// The hand-written replacement for the absorb method the - /// `no_opening_values` opt-out suppresses: stage 4's canonical order - /// interleaves the RAM value-check's staged openings around the register - /// openings, so it delegates to the claims aggregate's curated order. + /// `no_opening_values` opt-out suppresses: the committed (ZK) output rows + /// follow the claims aggregate's curated canonical order. /// Same signature as the generated method, so the generated prove /// driver's default curation serves this stage unchanged. pub fn opening_values(&self, claims: &Stage4OutputClaims) -> Vec { claims.opening_values() } + + /// The openings a clear proof sends after the stage-4 rounds: the register + /// members' openings, then the RAM value-check's `ram_ra`/`ram_inc`. The + /// staged contributions travel before the batch + /// ([`RamValCheckStagedOpenings`]). + pub fn post_round_opening_values(&self, claims: &Stage4OutputClaims) -> Vec { + let mut values = claims.registers_read_write.opening_values(); + #[cfg(feature = "field-inline")] + values.extend(claims.field_registers_read_write.opening_values()); + values.extend([claims.ram_val_check.ram_ra, claims.ram_val_check.ram_inc]); + values + } + + /// Receives the post-round openings in + /// [`post_round_opening_values`](Self::post_round_opening_values) order and + /// completes the claims with the already-received `staged` openings. + pub fn receive_output_claims( + &self, + staged: RamValCheckStagedOpenings, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result, VerifierError> { + let registers: BTreeMap<_, _> = + receive_member_openings(&self.registers_read_write, transcript)? + .into_iter() + .collect(); + let registers_read_write = + assemble_member_claims::>(®isters)?; + #[cfg(feature = "field-inline")] + let field_registers_read_write = { + let received: BTreeMap<_, _> = + receive_member_openings(&self.field_registers_read_write, transcript)? + .into_iter() + .collect(); + assemble_member_claims::>(&received)? + }; + + Ok(Stage4OutputClaims { + registers_read_write, + #[cfg(feature = "field-inline")] + field_registers_read_write, + ram_val_check: RamValCheckOutputClaims { + untrusted_advice: staged.untrusted_advice, + trusted_advice: staged.trusted_advice, + program_image: staged.program_image, + ram_ra: transcript.receive()?, + ram_inc: transcript.receive()?, + }, + }) + } } impl Stage4OutputClaims { - /// The produced opening claims in canonical (Fiat-Shamir) order, matching the prover's - /// commitment (flush) order exactly: the `Val_init` advice openings, the committed - /// program-image contribution, the register read-write openings, under `field-inline` the - /// five field-register read-write openings (the spec's committed row order: after the - /// ordinary register openings, before the RAM value-check ones), then the RAM value-check - /// `ram_ra`/`ram_inc` openings. The advice and program-image openings are produced by the - /// RAM value-check instance but are *appended first* (before the registers), so this is - /// hand-written rather than a per-instance concatenation — see [`Stage4Sumchecks`]. + /// The produced opening claims in canonical order, the committed (ZK) output + /// row order: the `Val_init` advice openings, the committed program-image + /// contribution, the register read-write openings, under `field-inline` the + /// five field-register read-write openings (the spec's committed row order: + /// after the ordinary register openings, before the RAM value-check ones), + /// then the RAM value-check `ram_ra`/`ram_inc` openings. The advice and + /// program-image openings are produced by the RAM value-check instance but + /// come first (before the registers), so this is hand-written rather than a + /// per-instance concatenation — see [`Stage4Sumchecks`]. pub fn opening_values(&self) -> Vec { let ram = &self.ram_val_check; let mut values: Vec = ram @@ -104,14 +160,6 @@ impl Stage4OutputClaims { values.extend([ram.ram_ra, ram.ram_inc]); values } - - /// Append every produced opening to the transcript in canonical order, each - /// under the `b"opening_claim"` label, matching the prover's commitment order. - pub fn append_to_transcript>(&self, transcript: &mut T) { - for value in self.opening_values() { - transcript.append_labeled(b"opening_claim", &value); - } - } } /// The shared opening-point accessors over the point-only stage-4 aggregate. diff --git a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs index d292a5f692..0e9929dd96 100644 --- a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs +++ b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs @@ -29,15 +29,13 @@ use jolt_claims::protocols::jolt::{ use jolt_claims::SymbolicSumcheck; use jolt_field::JoltField; use jolt_poly::{block_selector_mle_msb, LtPolynomial}; -use jolt_transcript::{LabelWithCount, Transcript}; +use jolt_transcript::{Sponge, VerifierTranscript}; use crate::stages::relations::ConcreteSumcheck; use crate::stages::stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints}; use crate::verifier::CheckedInputs; use crate::VerifierError; -use super::outputs::Stage4OutputClaims; - /// Wire the consumed opening *values* from stage 2's RAM read-write `val` and /// output-check `val_final`, plus the reconstructed init contributions (the /// same advice / program-image openings the init evaluation is decomposed @@ -178,20 +176,6 @@ impl ConcreteSumcheck for RamValCheck { openings } - /// Reproduces the stage-4 inline RAM value-check gamma draw: the - /// `b"ram_val_check_gamma"` domain separator (an empty labeled append) followed - /// by `ram_val_check_gamma = challenge_scalar()`. The separator's empty append - /// is part of the soundness-critical byte stream, so it is replayed here too. - fn draw_challenges>( - &self, - transcript: &mut T, - ) -> Result, VerifierError> { - append_ram_val_check_gamma_domain_separator(transcript); - Ok(RamValCheckChallenges { - gamma: transcript.challenge_scalar(), - }) - } - fn derive_opening_points( &self, sumcheck_point: &[F], @@ -411,6 +395,24 @@ impl RamValCheckInitialEvaluation { .iter() .find(|contribution| contribution.kind == kind) } + + /// The staged opening values this evaluation decomposes `Val_init` into: + /// the inverse of attaching [`RamValCheckStagedOpenings`] to the init + /// structure. + pub fn staged_openings(&self) -> RamValCheckStagedOpenings { + let advice = |kind| { + self.advice_contribution(kind) + .map(|contribution| contribution.opening_value) + }; + RamValCheckStagedOpenings { + untrusted_advice: advice(JoltAdviceKind::Untrusted), + trusted_advice: advice(JoltAdviceKind::Trusted), + program_image: self + .program_image_contribution + .as_ref() + .map(|(_, value)| *value), + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -425,16 +427,63 @@ pub struct VerifiedRamValCheckAdviceContribution { pub opening_value: F, } -/// Attach the proof's staged advice / program-image opening *values* to the -/// pre-branch [`RamValCheckInitStructure`], validating that each claim is present -/// exactly when its contribution is. Clear-only (the values come from proof -/// claims); mirrors the prover's own init reconstruction so both decompose -/// `Val_init` identically. +/// The RAM value-check's staged `Val_init` contribution openings, each present +/// exactly when the init structure has that contribution. A clear proof sends +/// them before the stage-4 batch, because the RAM value-check input claim +/// consumes them; a committed proof carries them in its output-claim rows. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RamValCheckStagedOpenings { + pub untrusted_advice: Option, + pub trusted_advice: Option, + pub program_image: Option, +} + +impl RamValCheckStagedOpenings { + /// The staged openings of a produced claims struct. + pub fn from_claims(claims: &RamValCheckOutputClaims) -> Self { + Self { + untrusted_advice: claims.untrusted_advice, + trusted_advice: claims.trusted_advice, + program_image: claims.program_image, + } + } + + /// The present openings, in send order. + pub fn values(&self) -> Vec { + self.untrusted_advice + .into_iter() + .chain(self.trusted_advice) + .chain(self.program_image) + .collect() + } + + /// Receives the openings `structure` has contributions for. + pub fn receive( + structure: &RamValCheckInitStructure, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + let mut receive = |present: bool| -> Result, VerifierError> { + present + .then(|| transcript.receive()) + .transpose() + .map_err(Into::into) + }; + Ok(Self { + untrusted_advice: receive(structure.advice_block(JoltAdviceKind::Untrusted).is_some())?, + trusted_advice: receive(structure.advice_block(JoltAdviceKind::Trusted).is_some())?, + program_image: receive(structure.program_image_point.is_some())?, + }) + } +} + +/// Attach the staged advice / program-image opening *values* to the pre-branch +/// [`RamValCheckInitStructure`], validating that each is present exactly when +/// its contribution is. Clear-only; mirrors the prover's own init reconstruction +/// so both decompose `Val_init` identically. pub(crate) fn ram_val_check_initial_evaluation( structure: &RamValCheckInitStructure, - claims: &Stage4OutputClaims, + ram: &RamValCheckStagedOpenings, ) -> Result, VerifierError> { - let ram = &claims.ram_val_check; let program_image_opening = program_image::ram_val_check_contribution_opening(); let program_image_contribution = match (&structure.program_image_point, ram.program_image) { (None, Some(_)) => { @@ -553,16 +602,6 @@ fn ram_val_check_advice_block( }) } -/// Absorb the Fiat-Shamir domain separator for the RAM value-check gamma: an empty -/// message labeled `b"ram_val_check_gamma"`. The prover appends this empty labeled -/// chunk before sampling the gamma, so [`RamValCheck::draw_challenges`] must -/// reproduce it byte-for-byte (label chunk + empty payload) or every challenge from -/// here on diverges. -fn append_ram_val_check_gamma_domain_separator(transcript: &mut T) { - transcript.append(&LabelWithCount(b"ram_val_check_gamma", 0)); - transcript.append_bytes(&[]); -} - #[cfg(test)] #[expect( clippy::unwrap_used, diff --git a/crates/jolt-verifier/src/stages/stage4/verify.rs b/crates/jolt-verifier/src/stages/stage4/verify.rs index 4bd807cc88..4105823481 100644 --- a/crates/jolt-verifier/src/stages/stage4/verify.rs +++ b/crates/jolt-verifier/src/stages/stage4/verify.rs @@ -6,11 +6,11 @@ use jolt_claims::protocols::jolt::{ JoltRelationId, }; use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; use jolt_poly::sparse_segments_mle_msb; use jolt_program::preprocess::PublicInitialRam; -use jolt_transcript::Transcript; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(feature = "field-inline")] use super::field_registers_read_write_checking::{ @@ -26,6 +26,7 @@ use super::{ ram_val_check_init_structure, ram_val_check_initial_evaluation, ram_val_check_input_points_from_upstream, ram_val_check_input_values_from_upstream, RamValCheck, RamValCheckInitStructure, RamValCheckInitialEvaluation, + RamValCheckStagedOpenings, }, registers_read_write_checking::{ registers_read_write_input_points_from_upstream, @@ -36,11 +37,10 @@ use super::{ use crate::config::JOLT_VERIFIER_CONFIG; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{ stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage3::{Stage3Output, Stage3OutputClaims, Stage3OutputPoints}, - zk::committed, + zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -83,23 +83,23 @@ pub fn stage4_input_points_from_upstream( } #[jolt_verifier_derive::fs_scope(Stage4)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, stage2: &Stage2Output, stage3: &Stage3Output, ) -> Result, VerifierError> where PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + VC::Output: CanonicalDecode, + H: Sponge, { let log_t = crate::num::ilog2(checked.trace_length); let log_k = crate::num::ilog2(checked.ram_K); let trace_dimensions = TraceDimensions::new(log_t); - let register_dimensions = proof + let register_dimensions = checked .rw_config .register_dimensions(log_t, REGISTER_ADDRESS_BITS); // Eager: the proof-supplied phase split feeds round-count subtractions @@ -143,7 +143,7 @@ where // decomposition must stay in lockstep with the prover's and BlindFold's. let init_structure = ram_val_check_init_structure( checked, - proof.untrusted_advice_commitment.is_some(), + checked.untrusted_advice_commitment_present, r_address, ram_val_check_public_eval, )?; @@ -162,21 +162,18 @@ where }; // Draw the batching gammas in declaration order: the registers gamma, under `field-inline` - // the field-register read-write gamma (each a single `challenge_scalar`), then the RAM - // value-check gamma behind its `b"ram_val_check_gamma"` domain separator (the relation's - // `draw_challenges` override replays the separator at its exact transcript position). + // the field-register read-write gamma, then the RAM value-check gamma (each a single + // `challenge`). let challenges = sumchecks.draw_challenges(transcript)?; if !checked.zk { - let claims = &proof.clear_claims()?.stage4; let stage2 = stage2.clear()?; let stage3 = stage3.clear()?; - sumchecks.validate_output_claims(claims)?; - // Attaches the claimed advice / program-image opening values (consumed by the - // input wiring and carried downstream for the stage-6/7 address-phase - // reductions); presence against the init structure is validated by the - // generated `validate_output_claims` above and re-checked here. - let ram_val_check_init = ram_val_check_initial_evaluation(&init_structure, claims)?; + // The staged advice / program-image openings feed the RAM value-check input + // claim, so they arrive before the batch; attaching them also carries them + // downstream for the stage-6/7 address-phase reductions. + let staged = RamValCheckStagedOpenings::receive(&init_structure, transcript)?; + let ram_val_check_init = ram_val_check_initial_evaluation(&init_structure, &staged)?; let input_values = stage4_input_values_from_upstream( &stage2.output_values, @@ -189,34 +186,26 @@ where &init_structure, ); - let output_points = sumchecks.verify_clear( + let (output_points, output_values) = sumchecks.verify_clear_with( &input_values, &input_points, &challenges, - claims, - &proof.stages.stage4_sumcheck_proof, transcript, 4, + |sumchecks, _, transcript| sumchecks.receive_output_claims(staged, transcript), )?; - claims.append_to_transcript(transcript); - return Ok(Stage4Output::Clear(Stage4ClearOutput { - output_values: claims.clone(), + output_values, output_points, ram_val_check_init, })); } { - let consistency = sumchecks.verify_zk(&proof.stages.stage4_sumcheck_proof, transcript)?; - let batch_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage4_sumcheck_proof, - "stage4_sumcheck_proof", - sumchecks.output_claim_count(), - JoltRelationId::RegistersReadWriteChecking, - )?; + let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; + let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; + let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; // Built via the same wiring as the clear path, off the ZK-agnostic upstream // output points and init structure. Advice / program-image openings live in diff --git a/crates/jolt-verifier/src/stages/stage5/verify.rs b/crates/jolt-verifier/src/stages/stage5/verify.rs index 4317f1d7a6..3e0b9d80a8 100644 --- a/crates/jolt-verifier/src/stages/stage5/verify.rs +++ b/crates/jolt-verifier/src/stages/stage5/verify.rs @@ -1,10 +1,8 @@ #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; -use jolt_claims::protocols::jolt::{geometry::dimensions::JoltFormulaDimensions, JoltRelationId}; -use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; -use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_claims::protocols::jolt::geometry::dimensions::JoltFormulaDimensions; +use jolt_field::{CanonicalDecode, JoltField}; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(feature = "field-inline")] use super::field_registers_val_evaluation::{ @@ -30,11 +28,10 @@ use super::{ }, }; use crate::{ - proof::JoltProof, stages::{ stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage4::{Stage4Output, Stage4OutputClaims, Stage4OutputPoints}, - zk::committed, + zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -79,18 +76,17 @@ pub fn stage5_input_points_from_upstream( } #[jolt_verifier_derive::fs_scope(Stage5)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, - proof: &JoltProof, formula_dimensions: &JoltFormulaDimensions, - transcript: &mut T, - stage2: &Stage2Output, - stage4: &Stage4Output, -) -> Result, VerifierError> + transcript: &mut VerifierTranscript<'_, H>, + stage2: &Stage2Output, + stage4: &Stage4Output, +) -> Result, VerifierError> where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, + F: JoltField, + C: CanonicalDecode, + H: Sponge, { let log_k = crate::num::ilog2(checked.ram_K); let trace_dimensions = formula_dimensions.trace; @@ -112,10 +108,8 @@ where let challenges = sumchecks.draw_challenges(transcript)?; if !checked.zk { - let claims = &proof.clear_claims()?.stage5; let stage2 = stage2.clear()?; let stage4 = stage4.clear()?; - sumchecks.validate_output_claims(claims)?; // The reduced lookup output aliases the product remainder's lookup output // (same opening point and value); stage 2's generated `validate_aliases` @@ -126,22 +120,13 @@ where let input_points = stage5_input_points_from_upstream(&stage2.output_points, &stage4.output_points); - let output_points = sumchecks.verify_clear( - &input_values, - &input_points, - &challenges, - claims, - &proof.stages.stage5_sumcheck_proof, - transcript, - 5, - )?; - - sumchecks.append_output_claims(transcript, claims); + let (output_points, output_values) = + sumchecks.verify_clear(&input_values, &input_points, &challenges, transcript, 5)?; let instruction_r_address = output_points.instruction_r_address(); return Ok(Stage5Output::Clear(Stage5ClearOutput { challenges, - output_values: claims.clone(), + output_values, output_points, instruction_r_address, })); @@ -150,14 +135,9 @@ where { let stage2 = stage2.zk()?; let stage4 = stage4.zk()?; - let consistency = sumchecks.verify_zk(&proof.stages.stage5_sumcheck_proof, transcript)?; - let batch_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage5_sumcheck_proof, - "stage5_sumcheck_proof", - sumchecks.output_claim_count(), - JoltRelationId::InstructionReadRaf, - )?; + let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; + let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; + let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; // Built via the same wiring as the clear path, off the ZK-agnostic upstream // output points; `derive_opening_points` ignores the instruction relation's diff --git a/crates/jolt-verifier/src/stages/stage6a/booleanity.rs b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs index dd6ace5a6d..ae0416ccc6 100644 --- a/crates/jolt-verifier/src/stages/stage6a/booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs @@ -13,7 +13,7 @@ pub use jolt_claims::protocols::jolt::relations::booleanity::{ }; use jolt_claims::SymbolicSumcheck; use jolt_field::JoltField; -use jolt_transcript::Transcript; +use jolt_transcript::Channel; use crate::stages::relations::ConcreteSumcheck; use crate::VerifierError; @@ -80,9 +80,9 @@ impl ConcreteSumcheck for BooleanityAddressPhase { /// both decode the same 16-byte squeeze, but differently, so switching /// would silently change the reference/gamma values without changing the /// transcript bytes. - fn draw_challenges>( + fn draw_challenges( &self, - transcript: &mut T, + transcript: &mut C, ) -> Result, VerifierError> { let chunk_bits = self.dimensions.log_k_chunk; let mut reference_address: Vec = @@ -91,7 +91,7 @@ impl ConcreteSumcheck for BooleanityAddressPhase { // only settles the (unreachable) underflow for the arithmetic lint. if reference_address.len() < chunk_bits { let missing = chunk_bits.saturating_sub(reference_address.len()); - reference_address.extend(transcript.challenge_vector(missing)); + reference_address.extend(transcript.challenges_small::(missing)); } else { // Keep the trailing `chunk_bits` entries. let excess = reference_address.len().saturating_sub(chunk_bits); @@ -99,7 +99,7 @@ impl ConcreteSumcheck for BooleanityAddressPhase { } Ok(BooleanityAddressPhaseChallenges { reference_address, - gamma: transcript.challenge(), + gamma: transcript.challenge_small(), }) } diff --git a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs index 6c335994a2..46319a1643 100644 --- a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs @@ -410,7 +410,7 @@ mod tests { use jolt_field::Fr; use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; use jolt_riscv::NUM_CIRCUIT_FLAGS; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; // The address phase has the only multi-field `Challenges` (gamma + five stage // gammas), so it exercises that the default draws one `challenge_scalar` per @@ -437,12 +437,12 @@ mod tests { // degree-1 power. let (inline_events, inline_gammas) = record(|t| { [ - t.challenge_scalar_powers(8)[1], - t.challenge_scalar_powers(2 + NUM_CIRCUIT_FLAGS)[1], - t.challenge_scalar_powers(4)[1], - t.challenge_scalar_powers(9)[1], - t.challenge_scalar_powers(3)[1], - t.challenge_scalar_powers(2 + LookupTableKind::::COUNT)[1], + t.challenge_powers(8)[1], + t.challenge_powers(2 + NUM_CIRCUIT_FLAGS)[1], + t.challenge_powers(4)[1], + t.challenge_powers(9)[1], + t.challenge_powers(3)[1], + t.challenge_powers(2 + LookupTableKind::::COUNT)[1], ] }); let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); diff --git a/crates/jolt-verifier/src/stages/stage6a/verify.rs b/crates/jolt-verifier/src/stages/stage6a/verify.rs index 99026d90e4..e30c5b1345 100644 --- a/crates/jolt-verifier/src/stages/stage6a/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6a/verify.rs @@ -3,8 +3,9 @@ use jolt_claims::protocols::composed::ComposedClaims; use jolt_claims::protocols::jolt::{geometry::dimensions::JoltFormulaDimensions, JoltRelationId}; use jolt_crypto::VectorCommitment; +use jolt_field::CanonicalDecode; use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(feature = "field-inline")] use super::field_inline::field_inline_bytecode_read_raf_address_phase_input_values_from_upstream; @@ -19,10 +20,13 @@ use super::{ }; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{ - stage1::Stage1Output, stage2::Stage2Output, stage3::Stage3Output, stage4::Stage4Output, - stage5::Stage5Output, zk::committed, + stage1::Stage1Output, + stage2::Stage2Output, + stage3::Stage3Output, + stage4::Stage4Output, + stage5::Stage5Output, + zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -33,12 +37,11 @@ use crate::{ reason = "Stage 6a's address-phase input claim folds all five prior stage outputs directly; bundling them would reintroduce the removed `Deps` indirection." )] #[jolt_verifier_derive::fs_scope(Stage6a)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, formula_dimensions: &JoltFormulaDimensions, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, stage1: &Stage1Output, stage2: &Stage2Output, stage3: &Stage3Output, @@ -48,7 +51,8 @@ pub fn verify( where PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + VC::Output: CanonicalDecode, + H: Sponge, { // The upstream cycle/register points and entry index ride on the relation // (full geometry at construction) for the prover's address-phase kernel; @@ -59,7 +63,7 @@ where .entry_bytecode_index_checked(JoltRelationId::BytecodeReadRaf)?; let address_sumchecks = Stage6aSumchecks::build_from_parts(Stage6aBuildParts { formula_dimensions, - committed_chunk_bits: proof.one_hot_config.committed_chunk_bits(), + committed_chunk_bits: checked.one_hot_config.committed_chunk_bits(), committed_program: checked.precommitted.bytecode.is_some(), entry_bytecode_index, stage1_cycle_binding: &stage1_cycle_binding, @@ -71,8 +75,8 @@ where // The generated per-member draw: the bytecode member's six squeezes (the // fold gamma plus the five per-stage folding gammas, each formerly an - // inline `challenge_scalar_powers(..)` whose single squeeze's degree-1 - // power equals the squeezed scalar; byte- and value-equal, test-locked in + // inline `challenge_powers(..)` whose single draw's degree-1 + // power equals the drawn scalar; byte- and value-equal, test-locked in // `bytecode_read_raf.rs` — stage 6b's folds expand the power vectors via // `stage_gamma_powers`, test-locked below), then the booleanity member's // override (the reference-address pad draw and the gamma; schedule-locked @@ -88,15 +92,10 @@ where let address_input_points = address_sumchecks.empty_input_points(); if checked.zk { - let consistency = - address_sumchecks.verify_zk(&proof.stages.stage6a_sumcheck_proof, transcript)?; - let output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage6a_sumcheck_proof, - "stage6a_sumcheck_proof", - address_sumchecks.output_claim_count(), - JoltRelationId::BytecodeReadRaf, - )?; + let shape = committed::output_claim_shape(checked, address_sumchecks.output_claim_count())?; + let (consistency, commitments) = + address_sumchecks.verify_zk(shape.row_count(), transcript)?; + let output_claims = CommittedOutputClaimOutput { shape, commitments }; let output_points = address_sumchecks .derive_opening_points(&consistency.challenges(), &address_input_points)?; return Ok(Stage6aOutput::Zk(Stage6aZkOutput { @@ -107,12 +106,6 @@ where })); } - let claims = &proof.clear_claims()?.stage6a; - // Rejects val-stage claims whose presence or count disagrees with the - // committed-program mode (the bytecode member's wire set carries the staged - // `BytecodeValClaim` ids exactly when the program is committed). - address_sumchecks.validate_output_claims(claims)?; - // The bytecode address-phase input claim is the gamma-folded bind of every // prior clear stage opening (plus, under akita, the four reduced `Inc` // claims at the fused-inc consumer stage slots); the relation evaluates it @@ -148,24 +141,22 @@ where booleanity: BooleanityAddressPhaseInputClaims::default(), }; - let output_points = address_sumchecks.verify_clear( + // The address-phase opening order (bytecode `intermediate`, each `val_stages`, + // then booleanity `intermediate`) is single-sourced from the generated + // `receive_output_claims` (member declaration order = canonical Fiat-Shamir + // order; no alias dedup in the address phase). The bytecode member's wire set + // carries the staged `BytecodeValClaim` ids exactly when the program is + // committed. + let (output_points, output_values) = address_sumchecks.verify_clear( &address_input_values, &address_input_points, &address_challenges, - claims, - &proof.stages.stage6a_sumcheck_proof, transcript, 6, )?; - // The address-phase opening order (bytecode `intermediate`, each `val_stages`, - // then booleanity `intermediate`) is single-sourced from the generated - // `append_output_claims` (member declaration order = canonical Fiat-Shamir - // order; no alias dedup in the address phase). - address_sumchecks.append_output_claims(transcript, claims); - Ok(Stage6aOutput::Clear(Stage6aClearOutput { - output_values: claims.clone(), + output_values, output_points, challenges: carried, })) diff --git a/crates/jolt-verifier/src/stages/stage6b/batch.rs b/crates/jolt-verifier/src/stages/stage6b/batch.rs index bb822ef98c..b56355da11 100644 --- a/crates/jolt-verifier/src/stages/stage6b/batch.rs +++ b/crates/jolt-verifier/src/stages/stage6b/batch.rs @@ -27,7 +27,7 @@ use jolt_crypto::VectorCommitment; use jolt_field::JoltField; use jolt_openings::CommitmentScheme; use jolt_riscv::JoltInstructionRow; -use jolt_transcript::Transcript; +use jolt_transcript::Channel; use super::booleanity::{Booleanity, BooleanityCyclePhaseChallenges}; use super::bytecode_read_raf::{ @@ -56,7 +56,6 @@ use super::outputs::{Stage6bChallenges, Stage6bSumchecks}; use super::ram_hamming_booleanity::RamHammingBooleanity; use super::ram_ra_virtualization::RamRaVirtualization; use crate::preprocessing::JoltVerifierPreprocessing; -use crate::proof::JoltProof; use crate::stages::stage1::Stage1Output; use crate::stages::stage2::{Stage2BatchOutputPoints, Stage2Output}; use crate::stages::stage3::outputs::Stage3OutputPoints; @@ -122,19 +121,16 @@ pub struct Stage6bDraws { } impl Stage6bDraws { - pub fn draw>( - transcript: &mut T, - committed_bytecode: bool, - ) -> Self { + pub fn draw(transcript: &mut C, committed_bytecode: bool) -> Self { // Field order is draw order: the struct literal evaluates in // declaration order. Self { - instruction_ra_gamma: transcript.challenge_scalar(), + instruction_ra_gamma: transcript.challenge(), #[cfg(not(feature = "akita"))] - inc_gamma: transcript.challenge_scalar(), + inc_gamma: transcript.challenge(), #[cfg(feature = "field-inline")] - field_registers_inc_gamma: transcript.challenge_scalar(), - eta: committed_bytecode.then(|| transcript.challenge_scalar()), + field_registers_inc_gamma: transcript.challenge(), + eta: committed_bytecode.then(|| transcript.challenge()), } } } @@ -144,10 +140,9 @@ impl Stage6bSumchecks { clippy::too_many_arguments, reason = "Stage 6b's batch is built from the stage-6a output plus all five prior stage outputs directly; bundling them would reintroduce the removed `Stage6bParams` pack/unpack indirection." )] - pub(super) fn build( + pub(super) fn build( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, formula_dimensions: &JoltFormulaDimensions, stage1: &Stage1Output, stage2: &Stage2Output, @@ -217,7 +212,7 @@ impl Stage6bSumchecks { Self::build_from_parts(Stage6bBuildParts { formula_dimensions, ram_log_k: crate::num::ilog2(checked.ram_K), - committed_chunk_bits: proof.one_hot_config.committed_chunk_bits(), + committed_chunk_bits: checked.one_hot_config.committed_chunk_bits(), precommitted: &checked.precommitted, entry_bytecode_index, bytecode_table_rows, @@ -593,7 +588,7 @@ mod tests { let (inline_events, inline_values) = record(|t| { (0..expected_squeezes) - .map(|_| t.challenge_scalar()) + .map(|_| t.challenge()) .collect::>() }); let (draw_events, draws) = record(|t| Stage6bDraws::::draw(t, committed_bytecode)); diff --git a/crates/jolt-verifier/src/stages/stage6b/booleanity.rs b/crates/jolt-verifier/src/stages/stage6b/booleanity.rs index b6efa6de31..dab139f8aa 100644 --- a/crates/jolt-verifier/src/stages/stage6b/booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6b/booleanity.rs @@ -187,7 +187,7 @@ mod tests { use crate::stages::relations::draw_recording::{record, DrawEvent}; use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; use jolt_field::Fr; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; // Booleanity inherits the default `draw_challenges` (one `challenge_scalar`): the // inline draw is a single `challenge()`. The historical zero-gamma re-roll was @@ -201,7 +201,7 @@ mod tests { let dimensions = lattice_booleanity::LatticeBooleanityDimensions::new(dimensions).unwrap(); let relation = Booleanity::::new(dimensions, Vec::new(), Vec::new(), Vec::new()); - let (inline_events, inline_gamma) = record(|t| t.challenge()); + let (inline_events, inline_gamma) = record(|t| t.challenge_small()); let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); assert_eq!(draw_events, inline_events); diff --git a/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs b/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs index 5da43117c5..f9a7a7e2ce 100644 --- a/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs +++ b/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs @@ -16,6 +16,8 @@ //! produced opening id is dynamic in `has_address_phase`; the override computes //! exactly the formula value, so the clear path and BlindFold stay in sync. +use std::collections::BTreeSet; + use jolt_claims::protocols::jolt::geometry::claim_reductions::advice::ram_val_check_advice_opening; use jolt_claims::protocols::jolt::geometry::claim_reductions::bytecode::{ lane_weights, BytecodeLaneWeightInputs, @@ -33,10 +35,10 @@ pub use jolt_claims::protocols::jolt::relations::claim_reductions::program_image ProgramImageReductionCyclePhaseInputClaims, ProgramImageReductionCyclePhaseOutputClaims, }; use jolt_claims::protocols::jolt::{ - AdviceClaimReductionLayout, BytecodeClaimReductionLayout, JoltAdviceKind, JoltRelationId, - PrecommittedReductionLayout, ProgramImageClaimReductionLayout, + AdviceClaimReductionLayout, BytecodeClaimReductionLayout, JoltAdviceKind, JoltOpeningId, + JoltRelationId, PrecommittedReductionLayout, ProgramImageClaimReductionLayout, }; -use jolt_claims::{NoChallenges, SymbolicSumcheck}; +use jolt_claims::{NoChallenges, OutputClaims, SymbolicSumcheck}; use jolt_field::JoltField; use super::outputs::BytecodeReductionWeights; @@ -134,6 +136,18 @@ impl ConcreteSumcheck for TrustedAdviceCyclePhase { &self.symbolic } + /// The claims struct carries the completing opening under the cycle-phase + /// relation whether or not an address phase follows (`expected_output` + /// applies the final scale), but the symbolic output expression names the + /// final-reduction opening when none follows, so the default would omit the + /// cell the prover sends. + fn wire_output_openings(&self) -> BTreeSet { + TrustedAdviceCyclePhaseOutputClaims { trusted: F::zero() } + .canonical_order() + .into_iter() + .collect() + } + /// Precommitted cycle-phase reductions are bound on the offset-0 prefix of /// the batch challenge vector, not the front-loaded suffix. fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { @@ -223,6 +237,20 @@ impl ConcreteSumcheck for UntrustedAdviceCyclePhase { &self.symbolic } + /// The claims struct carries the completing opening under the cycle-phase + /// relation whether or not an address phase follows (`expected_output` + /// applies the final scale), but the symbolic output expression names the + /// final-reduction opening when none follows, so the default would omit the + /// cell the prover sends. + fn wire_output_openings(&self) -> BTreeSet { + UntrustedAdviceCyclePhaseOutputClaims { + untrusted: F::zero(), + } + .canonical_order() + .into_iter() + .collect() + } + fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { Ok(0) } @@ -330,6 +358,20 @@ impl ConcreteSumcheck for ProgramImageReductionCyclePhase { &self.symbolic } + /// The claims struct carries the completing opening under the cycle-phase + /// relation whether or not an address phase follows (`expected_output` + /// applies the final scale), but the symbolic output expression names the + /// final-reduction opening when none follows, so the default would omit the + /// cell the prover sends. + fn wire_output_openings(&self) -> BTreeSet { + ProgramImageReductionCyclePhaseOutputClaims { + program_image: F::zero(), + } + .canonical_order() + .into_iter() + .collect() + } + /// Precommitted cycle-phase reductions are bound on the offset-0 prefix of /// the batch challenge vector, not the front-loaded suffix. fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { @@ -443,6 +485,26 @@ impl ConcreteSumcheck for BytecodeReductionCyclePhase { &self.symbolic } + /// The claims struct carries the completing opening under the cycle-phase + /// relation whether or not an address phase follows (`expected_output` + /// applies the final scale), but the symbolic output expression names the + /// final-reduction opening when none follows, so the default would omit the + /// cell the prover sends. + fn wire_output_openings(&self) -> BTreeSet { + let claims = if self.layout.dimensions().has_address_phase() { + BytecodeReductionCyclePhaseOutputClaims { + intermediate: Some(F::zero()), + chunks: Vec::new(), + } + } else { + BytecodeReductionCyclePhaseOutputClaims { + intermediate: None, + chunks: vec![F::zero(); self.chunk_count], + } + }; + claims.canonical_order().into_iter().collect() + } + /// Precommitted cycle-phase reductions are bound on the offset-0 prefix of /// the batch challenge vector, not the front-loaded suffix. fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { diff --git a/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs b/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs index 0ff581ab6e..297f793e0f 100644 --- a/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs +++ b/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs @@ -160,7 +160,7 @@ mod tests { use crate::stages::relations::draw_recording::{record, DrawEvent}; use core::num::NonZeroUsize; use jolt_field::Fr; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; fn relation(num_virtual_ra_polys: usize) -> InstructionRaVirtualization { let dimensions = InstructionRaVirtualizationDimensions::new( @@ -179,7 +179,7 @@ mod tests { #[test] fn default_draw_challenges_matches_inline_instruction_ra_gamma() { let relation = relation(2); - let (inline_events, inline_gamma) = record(|t| t.challenge_scalar()); + let (inline_events, inline_gamma) = record(|t| t.challenge()); let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); assert_eq!(draw_events, inline_events); diff --git a/crates/jolt-verifier/src/stages/stage6b/verify.rs b/crates/jolt-verifier/src/stages/stage6b/verify.rs index 907e6e2226..c54dfe0f9a 100644 --- a/crates/jolt-verifier/src/stages/stage6b/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6b/verify.rs @@ -1,59 +1,66 @@ +use std::collections::BTreeMap; + use jolt_claims::protocols::jolt::{ - geometry::{bytecode, dimensions::JoltFormulaDimensions}, - BytecodeClaimReductionLayout, JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, - PrecommittedReductionLayout, + geometry::dimensions::JoltFormulaDimensions, JoltCommittedPolynomial, JoltOpeningId, + JoltRelationId, }; use jolt_claims::OutputClaims; use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(not(feature = "akita"))] use super::committed_reduction_cycle_phase::{ trusted_advice_cycle_phase_input_values_from_upstream, - untrusted_advice_cycle_phase_input_values_from_upstream, + untrusted_advice_cycle_phase_input_values_from_upstream, TrustedAdviceCyclePhase, + UntrustedAdviceCyclePhase, }; #[cfg(feature = "field-inline")] use super::field_registers_inc_claim_reduction::{ field_registers_inc_claim_reduction_input_points_from_upstream, field_registers_inc_claim_reduction_input_values_from_upstream, + FieldRegistersIncClaimReduction, }; #[cfg(not(feature = "akita"))] use super::inc_claim_reduction::{ inc_claim_reduction_input_points_from_upstream, inc_claim_reduction_input_values_from_upstream, + IncClaimReduction, }; #[cfg(not(feature = "akita"))] use super::outputs::{Stage6bCarriedChallenges, Stage6bZkOutput}; -use super::ram_hamming_booleanity::RamHammingBooleanityInputClaims; +use super::ram_hamming_booleanity::{RamHammingBooleanity, RamHammingBooleanityInputClaims}; use super::{ batch::Stage6bDraws, - booleanity::BooleanityInputClaims, - bytecode_read_raf::BytecodeReadRafInputClaims, + booleanity::{Booleanity, BooleanityInputClaims}, + bytecode_read_raf::{BytecodeReadRafCycle, BytecodeReadRafInputClaims}, committed_reduction_cycle_phase::{ program_image_reduction_cycle_phase_input_values_from_upstream, - BytecodeReductionCyclePhaseInputClaims, + BytecodeReductionCyclePhase, BytecodeReductionCyclePhaseInputClaims, + ProgramImageReductionCyclePhase, }, instruction_ra_virtualization::{ instruction_ra_virtualization_input_points_from_upstream, - instruction_ra_virtualization_input_values_from_upstream, + instruction_ra_virtualization_input_values_from_upstream, InstructionRaVirtualization, }, outputs::{ Stage6bClearOutput, Stage6bInputClaims, Stage6bInputPoints, Stage6bOutput, - Stage6bOutputClaims, Stage6bSumchecks, + Stage6bOutputClaims, Stage6bOutputPoints, Stage6bSumchecks, }, ram_ra_virtualization::{ ram_ra_virtualization_input_points_from_upstream, - ram_ra_virtualization_input_values_from_upstream, + ram_ra_virtualization_input_values_from_upstream, RamRaVirtualization, }, }; #[cfg(not(feature = "akita"))] -use crate::stages::zk::committed; +use crate::stages::zk::{committed, outputs::CommittedOutputClaimOutput}; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{ - relations::validate_member_presence, + relations::{ + assemble_member_claims, assemble_member_claims_with, receive_member_openings, + receive_member_openings_except, ComposedOpeningId, + }, stage1::Stage1Output, stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage3::Stage3Output, @@ -70,12 +77,11 @@ use crate::{ reason = "Stage 6b consumes the stage-6a output plus all five prior stage outputs directly; bundling them would reintroduce the removed `Deps` indirection." )] #[jolt_verifier_derive::fs_scope(Stage6b)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, formula_dimensions: &JoltFormulaDimensions, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, stage1: &Stage1Output, stage2: &Stage2Output, stage3: &Stage3Output, @@ -86,21 +92,20 @@ pub fn verify( where PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + VC::Output: CanonicalDecode, + H: Sponge, { // The bytecode fold gamma shares stage 6a's squeeze; it and the booleanity // gamma ride on the stage-6a output as typed upstream values. The post-6a // draws and the challenges aggregate are the promoted two-front helpers. let carried = stage6a.challenges(); - let bytecode_reduction_layout = checked.precommitted.bytecode.as_ref(); - let draws = Stage6bDraws::draw(transcript, bytecode_reduction_layout.is_some()); + let draws = Stage6bDraws::draw(transcript, checked.precommitted.bytecode.is_some()); // The batch is built after the post-6a draws, directly from the upstream stage // outputs; `build` derives every mode-agnostic constructor leg internally. let sumchecks = Stage6bSumchecks::build( checked, preprocessing, - proof, formula_dimensions, stage1, stage2, @@ -124,40 +129,33 @@ where // runtime point-alias dedup arithmetic) is base-only. #[cfg(not(feature = "akita"))] if checked.zk { - let consistency = sumchecks.verify_zk(&proof.stages.stage6b_sumcheck_proof, transcript)?; - let cycle_points = - sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; - - // The committed-claim count is the derived output-point-cell total minus the - // runtime aliases between the booleanity bytecode-RA openings and the - // bytecode read-RAF openings — a point-equality dedup that is not expressible - // from the output Exprs, so it stays hand-written. - let booleanity_opening_point = - cycle_points.booleanity_opening_point().ok_or_else(|| { - VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::Booleanity, - reason: "Stage 6 booleanity produced no opening point".to_string(), - } - })?; - let aliased_bytecode_ra_openings = cycle_points - .bytecode_read_raf - .bytecode_ra - .iter() - .filter(|point| point.as_slice() == booleanity_opening_point) - .count(); - // The aliased openings are a filtered subset of the bytecode RA points - // counted by `point_count`, so the subtraction is exact; `saturating_sub` - // only settles the (unreachable) underflow for the arithmetic lint. - let committed_output_claims = cycle_points - .point_count() - .saturating_sub(aliased_bytecode_ra_openings); - let batch_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage6b_sumcheck_proof, - "stage6b_sumcheck_proof", - committed_output_claims, - JoltRelationId::BytecodeReadRaf, + let mut committed_shape = None; + let mut cycle_points = None; + let (consistency, commitments) = sumchecks.verify_zk_with( + |consistency| { + let points = + sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; + // The committed-claim count is the output-point-cell total minus + // the booleanity bytecode-RA openings aliased to the bytecode + // read-RAF points, the same runtime dedup as the clear wire. + let aliases = BytecodeRaAliases::new(&points)?; + let shape = committed::output_claim_shape( + checked, + points.point_count().saturating_sub(aliases.len()), + )?; + let rows = shape.row_count(); + committed_shape = Some(shape); + cycle_points = Some(points); + Ok(rows) + }, + transcript, )?; + let (Some(shape), Some(cycle_points)) = (committed_shape, cycle_points) else { + return Err(VerifierError::StageClaimPublicInputFailed { + stage: JoltRelationId::BytecodeReadRaf, + reason: "Stage 6b committed shape was not derived".to_string(), + }); + }; return Ok(Stage6bOutput::Zk(Stage6bZkOutput { challenges: Stage6bCarriedChallenges { @@ -168,7 +166,7 @@ where bytecode_reduction_eta: draws.eta, }, batch_consistency: consistency, - batch_output_claims, + batch_output_claims: CommittedOutputClaimOutput { shape, commitments }, output_points: cycle_points, })); } @@ -177,43 +175,6 @@ where let stage4 = stage4.clear()?; let stage5 = stage5.clear()?; let claims_6a = &stage6a.clear()?.output_values; - let claims = &proof.clear_claims()?.stage6b; - - // Reject cycle-phase output claims whose presence disagrees with the member's - // layout: a present reduction missing its claims, or claims supplied for a - // reduction that did not run. Instance presence mirrors layout presence (see - // `Stage6bSumchecks::build`). Hand-listed because 6b curates its own shape - // checks (`no_output_shape`, so no generated validator runs these guards - // itself); one call per `Option` member. Transcript-free (runs before the - // batched verify); the tampering suite asserts generic rejection. - #[cfg(not(feature = "akita"))] - validate_member_presence( - sumchecks.trusted_advice.as_ref(), - claims.trusted_advice.as_ref(), - )?; - #[cfg(not(feature = "akita"))] - validate_member_presence( - sumchecks.untrusted_advice.as_ref(), - claims.untrusted_advice.as_ref(), - )?; - validate_member_presence( - sumchecks.bytecode_reduction.as_ref(), - claims.bytecode_reduction.as_ref(), - )?; - validate_member_presence( - sumchecks.program_image_reduction.as_ref(), - claims.program_image_reduction.as_ref(), - )?; - - #[cfg(not(feature = "akita"))] - validate_cycle_phase_claim_shape(formula_dimensions, claims, bytecode_reduction_layout)?; - #[cfg(feature = "akita")] - validate_cycle_phase_claim_shape( - formula_dimensions, - claims, - bytecode_reduction_layout, - proof.one_hot_config.committed_chunk_bits(), - )?; let input_values = stage6b_input_values_from_upstream( &sumchecks, @@ -222,37 +183,17 @@ where stage4, &stage5.output_values, )?; - let cycle_points = sumchecks.verify_clear( + let (cycle_points, claims) = sumchecks.verify_clear_with( &input_values, &input_points, &cycle_challenges, - claims, - &proof.stages.stage6b_sumcheck_proof, transcript, 6, + receive_output_claims, )?; - let booleanity_opening_point = cycle_points - .booleanity_opening_point() - .ok_or_else(|| VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::Booleanity, - reason: "Stage 6 booleanity produced no opening point".to_string(), - })? - .to_vec(); - validate_bytecode_ra_aliases( - claims, - &cycle_points.bytecode_read_raf.bytecode_ra, - &booleanity_opening_point, - )?; - append_opening_claims( - transcript, - claims, - &cycle_points.bytecode_read_raf.bytecode_ra, - &booleanity_opening_point, - ); - Ok(Stage6bOutput::Clear(Stage6bClearOutput { - output_values: claims.clone(), + output_values: claims, output_points: cycle_points, bytecode_reduction_weights: sumchecks .bytecode_reduction @@ -261,137 +202,142 @@ where })) } -/// The wire-shape checks over the cycle-phase output claims. Stage 6b opts out -/// of the generated shape validator (`no_output_shape`), so every wire claim -/// vector is pinned to its formula-dimension length here. Under-length already -/// fails closed in `expected_final_claim` (the dimension-generated output -/// expression resolves a missing opening to `MissingOpeningClaim`), but -/// over-length trailing entries are never algebraically consumed and would -/// otherwise reach the Fiat-Shamir absorb, letting a malicious prover pass off -/// padded, non-canonical proofs. Also checks the bytecode reduction's -/// intermediate-vs-chunks shape. Member presence is enforced separately by the -/// hand-listed `validate_member_presence` calls; a missing advice inner opening is caught by -/// `expected_final_claim` (the advice cycle phase's `expected_output`). -fn validate_cycle_phase_claim_shape( - formula_dimensions: &JoltFormulaDimensions, - claims: &Stage6bOutputClaims, - bytecode_reduction_layout: Option<&BytecodeClaimReductionLayout>, - #[cfg(feature = "akita")] committed_chunk_bits: usize, -) -> Result<(), VerifierError> { - let bytecode_output_openings = - bytecode::read_raf_output_openings(formula_dimensions.bytecode_read_raf); - require_claim_count( - JoltRelationId::BytecodeReadRaf, - "bytecode RA", - bytecode_output_openings.bytecode_ra.len(), - claims.bytecode_read_raf.bytecode_ra.len(), - )?; - - let ra_layout = formula_dimensions.ra_layout; - require_claim_count( - JoltRelationId::Booleanity, - "booleanity instruction RA", - ra_layout.instruction(), - claims.booleanity.instruction_ra.len(), - )?; - require_claim_count( - JoltRelationId::Booleanity, - "booleanity bytecode RA", - ra_layout.bytecode(), - claims.booleanity.bytecode_ra.len(), - )?; - require_claim_count( - JoltRelationId::Booleanity, - "booleanity RAM RA", - ra_layout.ram(), - claims.booleanity.ram_ra.len(), - )?; - - // The packed increment digit claims: one per chunk of the shared - // one-hot chunking. - #[cfg(feature = "akita")] - { - let expected_chunks = - jolt_claims::protocols::jolt::lattice::geometry::BalancedIncChunking::new( - committed_chunk_bits, - ) - .map_err(|error| VerifierError::StageClaimPublicInputFailed { +/// The booleanity bytecode-RA openings that share their bytecode read-RAF +/// source's opening point, keyed to that source. Such an opening is a copy of +/// its source: it is neither sent nor committed, and the verifier fills it from +/// the source. This is a runtime point equality the output `Expr`s cannot +/// express, which is why stage 6b curates its own claim order. +struct BytecodeRaAliases(BTreeMap); + +impl BytecodeRaAliases { + fn new(points: &Stage6bOutputPoints) -> Result { + let booleanity_point = points.booleanity_opening_point().ok_or_else(|| { + VerifierError::StageClaimPublicInputFailed { stage: JoltRelationId::Booleanity, - reason: error.to_string(), - })? - .chunk_count(); - require_claim_count( - JoltRelationId::Booleanity, - "balanced increment digit", - expected_chunks, - claims.booleanity.balanced_inc_digits.len(), - )?; + reason: "Stage 6 booleanity produced no opening point".to_string(), + } + })?; + Ok(Self( + points + .bytecode_read_raf + .bytecode_ra + .iter() + .enumerate() + .filter(|(_, point)| point.as_slice() == booleanity_point) + .map(|(index, _)| { + let polynomial = JoltCommittedPolynomial::BytecodeRa(index); + ( + JoltOpeningId::committed(polynomial, JoltRelationId::Booleanity).into(), + JoltOpeningId::committed(polynomial, JoltRelationId::BytecodeReadRaf) + .into(), + ) + }) + .collect(), + )) } - require_claim_count( - JoltRelationId::RamRaVirtualization, - "committed RAM RA", - formula_dimensions - .ram_ra_virtualization - .num_committed_ra_polys(), - claims.ram_ra_virtualization.ram_ra.len(), - )?; - require_claim_count( - JoltRelationId::InstructionRaVirtualization, - "committed instruction RA", - formula_dimensions - .instruction_ra_virtualization - .num_committed_ra_polys(), - claims - .instruction_ra_virtualization - .committed_instruction_ra - .len(), - )?; - - if let (Some(layout), Some(output_claims)) = ( - bytecode_reduction_layout, - claims.bytecode_reduction.as_ref(), - ) { - let has_address_phase = layout.dimensions().has_address_phase(); - // The wire shape must match the reduction mode: an `intermediate` (no - // chunks) when an address phase follows, else exactly `chunk_count` - // chunks (no intermediate). - let shape_ok = match ( - &output_claims.intermediate, - output_claims.chunks.is_empty(), - has_address_phase, - ) { - (Some(_), true, true) => true, - (None, false, false) => output_claims.chunks.len() == layout.chunk_count(), - _ => false, - }; - if !shape_ok { - return Err(VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::BytecodeClaimReductionCyclePhase, - reason: format!( - "bytecode reduction cycle output shape mismatch (address phase: {has_address_phase})" - ), - }); - } + #[cfg(not(feature = "akita"))] + fn len(&self) -> usize { + self.0.len() } - - Ok(()) } -/// Reject a wire claim vector whose length disagrees with its formula-dimension count. -fn require_claim_count( - stage: JoltRelationId, - label: &str, - expected: usize, - got: usize, -) -> Result<(), VerifierError> { - if got != expected { - return Err(VerifierError::StageClaimPublicInputFailed { - stage, - reason: format!("{label} claim count mismatch: expected {expected}, got {got}"), - }); +/// Receive the stage-6b output claims in the [`stage6b_opening_values`] order: +/// member declaration order, each member in its canonical order, with the +/// aliased booleanity bytecode-RA openings filled from their sources. +fn receive_output_claims( + sumchecks: &Stage6bSumchecks, + points: &Stage6bOutputPoints, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> { + let aliases = BytecodeRaAliases::new(points)?; + let mut received = BTreeMap::new(); + received.extend(receive_member_openings( + &sumchecks.bytecode_read_raf, + transcript, + )?); + received.extend(receive_member_openings_except( + &sumchecks.booleanity, + |id| aliases.0.contains_key(id), + transcript, + )?); + received.extend(receive_member_openings( + &sumchecks.ram_hamming_booleanity, + transcript, + )?); + received.extend(receive_member_openings( + &sumchecks.ram_ra_virtualization, + transcript, + )?); + received.extend(receive_member_openings( + &sumchecks.instruction_ra_virtualization, + transcript, + )?); + #[cfg(not(feature = "akita"))] + received.extend(receive_member_openings( + &sumchecks.inc_claim_reduction, + transcript, + )?); + #[cfg(feature = "field-inline")] + received.extend(receive_member_openings( + &sumchecks.field_registers_inc_claim_reduction, + transcript, + )?); + #[cfg(not(feature = "akita"))] + if let Some(member) = &sumchecks.trusted_advice { + received.extend(receive_member_openings(member, transcript)?); + } + #[cfg(not(feature = "akita"))] + if let Some(member) = &sumchecks.untrusted_advice { + received.extend(receive_member_openings(member, transcript)?); + } + if let Some(member) = &sumchecks.bytecode_reduction { + received.extend(receive_member_openings(member, transcript)?); } - Ok(()) + if let Some(member) = &sumchecks.program_image_reduction { + received.extend(receive_member_openings(member, transcript)?); + } + + Ok(Stage6bOutputClaims { + bytecode_read_raf: assemble_member_claims::>(&received)?, + booleanity: assemble_member_claims_with::>(&received, |id| { + aliases.0.get(id).copied() + })?, + ram_hamming_booleanity: assemble_member_claims::>(&received)?, + ram_ra_virtualization: assemble_member_claims::>(&received)?, + instruction_ra_virtualization: assemble_member_claims::>( + &received, + )?, + #[cfg(not(feature = "akita"))] + inc_claim_reduction: assemble_member_claims::>(&received)?, + #[cfg(feature = "field-inline")] + field_registers_inc_claim_reduction: assemble_member_claims::< + F, + FieldRegistersIncClaimReduction, + >(&received)?, + #[cfg(not(feature = "akita"))] + trusted_advice: sumchecks + .trusted_advice + .as_ref() + .map(|_| assemble_member_claims::>(&received)) + .transpose()?, + #[cfg(not(feature = "akita"))] + untrusted_advice: sumchecks + .untrusted_advice + .as_ref() + .map(|_| assemble_member_claims::>(&received)) + .transpose()?, + bytecode_reduction: sumchecks + .bytecode_reduction + .as_ref() + .map(|_| assemble_member_claims::>(&received)) + .transpose()?, + program_image_reduction: sumchecks + .program_image_reduction + .as_ref() + .map(|_| assemble_member_claims::>(&received)) + .transpose()?, + }) } /// Assemble the stage-6b consumed opening *values* from the address-phase claims @@ -548,53 +494,6 @@ pub fn stage6b_opening_values( values } -fn validate_bytecode_ra_aliases( - claims: &Stage6bOutputClaims, - bytecode_read_raf_points: &[Vec], - booleanity_point: &[F], -) -> Result<(), VerifierError> { - for (index, booleanity_claim) in claims.booleanity.bytecode_ra.iter().enumerate() { - if !bytecode_read_raf_points - .get(index) - .is_some_and(|point| point.as_slice() == booleanity_point) - { - continue; - } - - let polynomial = JoltCommittedPolynomial::BytecodeRa(index); - let source_id = JoltOpeningId::committed(polynomial, JoltRelationId::BytecodeReadRaf); - let source_claim = claims.bytecode_read_raf.bytecode_ra.get(index).ok_or( - VerifierError::MissingOpeningClaim { - id: source_id.into(), - }, - )?; - if booleanity_claim != source_claim { - return Err(VerifierError::StageClaimOpeningMismatch { - stage: format!("{:?}", JoltRelationId::Booleanity), - left: JoltOpeningId::committed(polynomial, JoltRelationId::Booleanity).into(), - right: source_id.into(), - }); - } - } - Ok(()) -} - -fn append_opening_claims( - transcript: &mut T, - claims: &Stage6bOutputClaims, - bytecode_read_raf_points: &[Vec], - booleanity_point: &[F], -) where - F: JoltField, - T: Transcript, -{ - // Single-sourced with the prover-curation order: both fronts absorb the - // `stage6b_opening_values` sequence, so the two transcripts cannot drift. - for value in stage6b_opening_values(claims, bytecode_read_raf_points, booleanity_point) { - transcript.append_labeled(b"opening_claim", &value); - } -} - #[cfg(test)] #[expect(clippy::unwrap_used, clippy::expect_used)] mod tests { diff --git a/crates/jolt-verifier/src/stages/stage7/verify.rs b/crates/jolt-verifier/src/stages/stage7/verify.rs index 15f361833b..a799bd2245 100644 --- a/crates/jolt-verifier/src/stages/stage7/verify.rs +++ b/crates/jolt-verifier/src/stages/stage7/verify.rs @@ -8,10 +8,8 @@ use jolt_claims::protocols::jolt::{ }, JoltOpeningId, JoltRelationId, PrecommittedReductionLayout, }; -use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; -use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_field::{CanonicalDecode, JoltField}; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(not(feature = "akita"))] use super::advice_address_phase::{ @@ -33,11 +31,10 @@ use super::outputs::{ #[cfg(not(feature = "akita"))] use crate::stages::stage6b::committed_reduction_cycle_phase::advice_reference_point_from_upstream; use crate::{ - proof::JoltProof, stages::{ stage4::{Stage4ClearOutput, Stage4Output}, stage6b::{outputs::Stage6bOutputPoints, Stage6bClearOutput, Stage6bOutput}, - zk::committed, + zk::{committed, outputs::CommittedOutputClaimOutput}, PrecommittedSchedule, }, verifier::CheckedInputs, @@ -49,22 +46,21 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::advice; use jolt_claims::protocols::jolt::JoltAdviceKind; #[jolt_verifier_derive::fs_scope(Stage7)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, - proof: &JoltProof, formula_dimensions: &JoltFormulaDimensions, - transcript: &mut T, - stage4: &Stage4Output, - stage6: &Stage6bOutput, -) -> Result, VerifierError> + transcript: &mut VerifierTranscript<'_, H>, + stage4: &Stage4Output, + stage6: &Stage6bOutput, +) -> Result, VerifierError> where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, + F: JoltField, + C: CanonicalDecode, + H: Sponge, { let hamming_dimensions = hamming_weight_claim_reduction_dimensions( formula_dimensions.ra_layout, - proof.one_hot_config.committed_chunk_bits(), + checked.one_hot_config.committed_chunk_bits(), )?; // The clear-only reference geometry each address phase's expected-output term @@ -90,7 +86,7 @@ where clear, )?; - // Draw the hamming-weight reduction's batching gamma (a single `challenge_scalar`, + // Draw the hamming-weight reduction's batching gamma (a single `challenge`, // matching the relation's default `draw_challenges`) path-agnostically before the // ZK/clear branch; the advice and committed-program address phases draw nothing // (`NoChallenges`). BlindFold sources the gamma from @@ -98,14 +94,9 @@ where let challenges = sumchecks.draw_challenges(transcript)?; if checked.zk { - let consistency = sumchecks.verify_zk(&proof.stages.stage7_sumcheck_proof, transcript)?; - let batch_output_claims = committed::verify_output_claim_commitments( - checked, - &proof.stages.stage7_sumcheck_proof, - "stage7_sumcheck_proof", - sumchecks.output_claim_count(), - JoltRelationId::HammingWeightClaimReduction, - )?; + let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; + let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; + let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; // The produced opening points, derived off the committed batch consistency; // stage 8 reads the hamming point and resolves the precommitted finals off @@ -124,30 +115,14 @@ where } let stage6 = stage6.clear()?; - let claims = &proof.clear_claims()?.stage7; - - // Also rejects claims supplied for phases that did not run, with the same - // `UnexpectedOpeningClaim` ids the former hand-written guards used (the id is - // derived from the supplied claims' canonical order). - sumchecks.validate_output_claims(claims)?; - let input_values = stage7_input_values_from_upstream(&sumchecks, stage6)?; let input_points = sumchecks.empty_input_points(); - let output_points = sumchecks.verify_clear( - &input_values, - &input_points, - &challenges, - claims, - &proof.stages.stage7_sumcheck_proof, - transcript, - 7, - )?; - - sumchecks.append_output_claims(transcript, claims); + let (output_points, output_values) = + sumchecks.verify_clear(&input_values, &input_points, &challenges, transcript, 7)?; Ok(Stage7Output::Clear(Stage7ClearOutput { - output_values: claims.clone(), + output_values, output_points, })) } diff --git a/crates/jolt-verifier/src/stages/stage8/field_inline.rs b/crates/jolt-verifier/src/stages/stage8/field_inline.rs index 48621e7681..d3b373786f 100644 --- a/crates/jolt-verifier/src/stages/stage8/field_inline.rs +++ b/crates/jolt-verifier/src/stages/stage8/field_inline.rs @@ -1,5 +1,4 @@ -//! Stage 8's field-inline seam: the composed final-opening splice and the field-inline -//! commitment-payload presence check. `verify.rs` interacts with the field-inline protocol +//! Stage 8's field-inline seam: the composed final-opening splice. `verify.rs` interacts with the field-inline protocol //! only through the functions here. use jolt_claims::protocols::field_inline::geometry::claim_reductions::increments::field_rd_inc_reduced; @@ -16,22 +15,10 @@ use crate::proof::JoltCommitments; use crate::VerifierError; use jolt_claims::protocols::composed::ComposedOpeningId; -/// The field-inline commitment payload is part of the expected layout: the composed final -/// opening cannot assemble without the `FieldRdInc` commitment. -pub(super) fn require_commitment(commitments: &JoltCommitments) -> Result<(), VerifierError> { - if commitments.field_inline.is_none() { - return Err(VerifierError::MissingProofPayload { - field: "commitments.field_inline", - }); - } - Ok(()) -} - /// Splice the reduced `FieldRdInc` final opening into the batch entries at the spec's position /// — immediately after `RdInc@IncClaimReduction`, before the RA families /// (`specs/field-inline-protocol.md`, the field-inline final-opening order). Mirrors `RdInc`'s -/// treatment exactly: the commitment comes from the proof's field-inline payload (present -/// fail-closed), the claim and point from the stage-6b field-register increment reduction, and +/// treatment exactly: the commitment comes from the proof's field-inline payload, the claim and point from the stage-6b field-register increment reduction, and /// the dense embedding scale through the same `commitment_embedding_scale` helper. Public /// because the prover's stage-8 recipe splices its PCS batch statement identically. pub fn splice_final_opening<'a, F, C>( @@ -45,13 +32,6 @@ pub fn splice_final_opening<'a, F, C>( where F: JoltField, { - let field_inline = - commitments - .field_inline - .as_ref() - .ok_or(VerifierError::MissingProofPayload { - field: "commitments.field_inline", - })?; let rd_inc_id: ComposedOpeningId = JoltOpeningId::committed( JoltCommittedPolynomial::RdInc, JoltRelationId::IncClaimReduction, @@ -69,7 +49,7 @@ where splice_position, Stage8BatchEntry { id: field_rd_inc_reduced().into(), - commitment: &field_inline.field_registers.rd_inc, + commitment: &commitments.field_inline.field_registers.rd_inc, opening_claim, scale: commitment_embedding_scale( opening_point, diff --git a/crates/jolt-verifier/src/stages/stage8/packed.rs b/crates/jolt-verifier/src/stages/stage8/packed.rs index 7b2eaa36ce..779e24a645 100644 --- a/crates/jolt-verifier/src/stages/stage8/packed.rs +++ b/crates/jolt-verifier/src/stages/stage8/packed.rs @@ -19,7 +19,7 @@ use jolt_openings::{ CommitmentScheme, EvaluationClaim, GroupOpeningClaim, TaggedGroupOpeningClaim, }; use jolt_poly::Point; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; use super::precommitted::precommitted_final_openings; #[cfg(feature = "akita")] @@ -143,14 +143,14 @@ struct ResolvedObject<'a, PCS: CommitmentScheme> { commitment: &'a PCS::Output, } -fn reduce_object( +fn reduce_object( object: &ResolvedObject<'_, PCS>, leaves: &BTreeMap>, - transcript: &mut T, + transcript: &mut C, ) -> Result, VerifierError> where PCS: CommitmentScheme, - T: Transcript, + C: Channel, { let claims = object_leaf_claims(&object.plan, leaves)?; let semantic = object.plan.packed_claims(&claims).map_err(batch_failed)?; @@ -217,16 +217,15 @@ pub fn field_inc_claim( clippy::too_many_arguments, reason = "the stage inputs are passed separately by the verifier driver" )] -pub fn verify( +pub fn verify( formula_dimensions: &JoltFormulaDimensions, one_hot_config: JoltOneHotConfig, preprocessing: &crate::preprocessing::JoltVerifierPreprocessing, one_hot_trace_commitment: &PCS::Output, untrusted_advice_commitment: Option<&PCS::Output>, trusted_advice_commitment: Option<&PCS::Output>, - #[cfg(feature = "field-inline")] field_inc_commitment: Option<&PCS::Output>, - proof: &PCS::Proof, - transcript: &mut T, + #[cfg(feature = "field-inline")] field_inc_commitment: &PCS::Output, + transcript: &mut VerifierTranscript<'_, H>, schedule: &PrecommittedSchedule, #[cfg(feature = "akita")] stage4: &Stage4ClearOutput, stage6b: &Stage6bClearOutput, @@ -234,10 +233,10 @@ pub fn verify( ) -> Result<(), VerifierError> where PCS: CommitmentScheme, - PCS::Output: Clone + AppendToTranscript + OneHotTraceCommitmentMetadata, + PCS::Output: Clone + OneHotTraceCommitmentMetadata, PCS::VerifierSetup: OneHotTraceSetupMetadata, VC: jolt_crypto::VectorCommitment, - T: Transcript, + H: Sponge, { // Auxiliary objects precede the OneHotTrace group in canonical role order: advice, // (field-inline) the always-present field-increment commitment, then the direct @@ -339,9 +338,7 @@ where } #[cfg(feature = "field-inline")] { - let commitment = field_inc_commitment.ok_or(VerifierError::MissingProofPayload { - field: "field_inc_commitment", - })?; + let commitment = field_inc_commitment; let layout = FieldIncLayout::new(formula_dimensions.trace.log_t()); validate_group_commitment_metadata(commitment, layout.layout_digest(), layout.num_vars())?; auxiliary_groups.push(field_inc_claim(commitment, stage6b)?); @@ -372,7 +369,6 @@ where &preprocessing.pcs_setup, &auxiliary_groups, &main_group, - proof, transcript, ) .map_err(opening_failed)?; diff --git a/crates/jolt-verifier/src/stages/stage8/verify.rs b/crates/jolt-verifier/src/stages/stage8/verify.rs index 52a8aa8c51..7fcc078a9f 100644 --- a/crates/jolt-verifier/src/stages/stage8/verify.rs +++ b/crates/jolt-verifier/src/stages/stage8/verify.rs @@ -5,13 +5,13 @@ use super::outputs::{Stage8ClearOutput, Stage8ZkOutput}; use super::precommitted::{precommitted_final_openings, PrecommittedFinalOpening}; #[cfg(not(feature = "akita"))] use crate::proof::JoltCommitments; +use crate::proof::ProofCommitments; #[cfg(feature = "akita")] use crate::stages::stage4::Stage4Output; #[cfg(not(feature = "akita"))] use crate::stages::{stage6b::outputs::Stage6bOutputClaims, stage7::outputs::Stage7OutputClaims}; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{stage6b::Stage6bOutput, stage7::Stage7Output}, verifier::CheckedInputs, VerifierError, @@ -45,8 +45,8 @@ use jolt_openings::{ #[cfg(not(feature = "akita"))] use jolt_poly::Point; #[cfg(not(feature = "akita"))] -use jolt_transcript::LabelWithCount; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::Channel; +use jolt_transcript::{Sponge, VerifierTranscript}; #[cfg(not(feature = "akita"))] /// One assembled final-opening batch entry. Public because the prover's @@ -70,13 +70,13 @@ pub struct Stage8BatchEntry<'a, F: JoltField, C> { )] #[cfg(not(feature = "akita"))] #[jolt_verifier_derive::fs_scope(Stage8)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, + commitments: &ProofCommitments, formula_dimensions: &JoltFormulaDimensions, trusted_advice_commitment: Option<&PCS::Output>, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, stage6: &Stage6bOutput, stage7: &Stage7Output, ) -> Result, VerifierError> @@ -87,7 +87,7 @@ where + ZkOpeningScheme, PCS::Output: Clone + HomomorphicCommitment, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_t = formula_dimensions.trace.log_t(); let layout = formula_dimensions.ra_layout; @@ -113,7 +113,6 @@ where let inc_opening_point = stage6_points.inc_opening_point(); // `batch_entries` reads the clear claims in (stage6, stage7) order. let clear_claims = clear.map(|(stage7_values, stage6_values)| (stage6_values, stage7_values)); - require_commitment_layout(&proof.commitments, layout)?; let hamming_opening_point = stage7_points .hamming_weight_opening_point() @@ -131,8 +130,8 @@ where .collect(); let opening_point = final_opening_point(FinalOpeningPointInputs { log_t, - log_k_chunk: proof.one_hot_config.committed_chunk_bits(), - trace_order: proof.trace_polynomial_order, + log_k_chunk: checked.one_hot_config.committed_chunk_bits(), + trace_order: checked.trace_polynomial_order, hamming_weight_opening_point: hamming_opening_point.as_slice(), inc_claim_reduction_opening_point: inc_opening_point, precommitted_anchor_points: &anchor_points, @@ -144,10 +143,10 @@ where let entries = batch_entries( preprocessing, - &proof.commitments, - proof.untrusted_advice_commitment.as_ref(), + &commitments.trace, + commitments.untrusted_advice.as_ref(), layout, - proof.trace_polynomial_order, + checked.trace_polynomial_order, trusted_advice_commitment, &opening_point, hamming_opening_point.as_slice(), @@ -160,8 +159,8 @@ where let mut entries = entries; super::field_inline::splice_final_opening( &mut entries, - &proof.commitments, - proof.trace_polynomial_order, + &commitments.trace, + checked.trace_polynomial_order, &opening_point, stage6_points.field_registers_inc_opening_point(), clear_claims.map(|(stage6, _)| stage6.field_registers_inc_claim_reduction.rd_inc), @@ -171,7 +170,7 @@ where let opening_ids: Vec = entries.iter().map(|entry| entry.id).collect(); if checked.zk { - let gamma_powers = transcript.challenge_scalar_powers(entries.len()); + let gamma_powers = transcript.challenge_powers(entries.len()); let commitments: Vec = entries .iter() .map(|entry| entry.commitment.clone()) @@ -186,7 +185,6 @@ where let hiding_evaluation_commitment = PCS::verify_zk( &joint_commitment, pcs_opening_point.as_slice(), - &proof.joint_opening_proof, &preprocessing.pcs_setup, transcript, ) @@ -194,7 +192,7 @@ where reason: error.to_string(), })?; ZkEvaluationClaim::new(pcs_opening_point.as_slice(), &hiding_evaluation_commitment) - .append_to_transcript(transcript); + .absorb(transcript); return Ok(Stage8Output::Zk(Stage8ZkOutput { opening_ids, @@ -224,14 +222,15 @@ where }) .collect::, VerifierError>>()?; - transcript.append(&LabelWithCount( - b"rlc_claims", - crate::num::u64_from_usize(opening_claims.len()), - )); - for claim in &opening_claims { - claim.evaluation.value.append_to_transcript(transcript); - } - let gamma_powers = transcript.challenge_scalar_powers(opening_claims.len()); + // The transcript sequence of `HomomorphicBatch::verify_batch`, which the + // prover runs: the scaled claim values, the batching challenge, the opening, + // then the joint evaluation claim. + let scaled_values: Vec = opening_claims + .iter() + .map(|claim| claim.evaluation.value) + .collect(); + transcript.public_all(&scaled_values); + let gamma_powers = transcript.challenge_powers(opening_claims.len()); let joint_claim = gamma_powers .iter() @@ -254,14 +253,14 @@ where &joint_commitment, pcs_opening_point.as_slice(), joint_claim, - &proof.joint_opening_proof, &preprocessing.pcs_setup, transcript, ) .map_err(|error| VerifierError::FinalOpeningVerificationFailed { reason: error.to_string(), })?; - EvaluationClaim::new(pcs_opening_point.clone(), joint_claim).append_to_transcript(transcript); + transcript.public_all(pcs_opening_point.as_slice()); + transcript.public(&joint_claim); Ok(Stage8Output::Clear(Stage8ClearOutput { opening_claims, @@ -480,50 +479,6 @@ where Ok(entries) } -#[cfg(not(feature = "akita"))] -fn require_commitment_layout( - commitments: &JoltCommitments, - layout: JoltRaPolynomialLayout, -) -> Result<(), VerifierError> { - // The field-inline commitment payload is part of the expected layout: the composed final - // opening cannot assemble without the `FieldRdInc` commitment. - #[cfg(feature = "field-inline")] - super::field_inline::require_commitment(commitments)?; - #[expect( - clippy::arithmetic_side_effects, - reason = "layout totals are small per-polynomial chunk counts; the sum cannot overflow usize" - )] - let expected = 2 + layout.total(); - #[expect( - clippy::arithmetic_side_effects, - reason = "a sum of in-memory commitment counts and a small constant cannot overflow usize" - )] - let got = 2 - + commitments.instruction_ra.len() - + commitments.bytecode_ra.len() - + commitments.ram_ra.len(); - if got != expected { - return Err(VerifierError::InvalidCommitmentCount { expected, got }); - } - if commitments.instruction_ra.len() != layout.instruction() - || commitments.bytecode_ra.len() != layout.bytecode() - || commitments.ram_ra.len() != layout.ram() - { - return Err(VerifierError::FinalOpeningBatchFailed { - reason: format!( - "commitment layout mismatch: expected instruction={}, bytecode={}, ram={}; got instruction={}, bytecode={}, ram={}", - layout.instruction(), - layout.bytecode(), - layout.ram(), - commitments.instruction_ra.len(), - commitments.bytecode_ra.len(), - commitments.ram_ra.len() - ), - }); - } - Ok(()) -} - #[cfg(all(test, not(feature = "akita")))] #[expect(clippy::unwrap_used)] mod tests { @@ -691,13 +646,13 @@ mod tests { reason = "same signature as the homomorphic build's verify" )] #[jolt_verifier_derive::fs_scope(Stage8)] -pub fn verify( +pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, + commitments: &ProofCommitments, formula_dimensions: &JoltFormulaDimensions, trusted_advice_commitment: Option<&PCS::Output>, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, stage4: &Stage4Output, stage6: &Stage6bOutput, stage7: &Stage7Output, @@ -705,21 +660,20 @@ pub fn verify( where F: JoltField, PCS: CommitmentScheme, - PCS::Output: Clone + AppendToTranscript + super::OneHotTraceCommitmentMetadata, + PCS::Output: Clone + super::OneHotTraceCommitmentMetadata, PCS::VerifierSetup: super::OneHotTraceSetupMetadata, VC: VectorCommitment, - T: Transcript, + H: Sponge, { super::packed::verify( formula_dimensions, - proof.one_hot_config, + checked.one_hot_config, preprocessing, - &proof.commitments, - proof.untrusted_advice_commitment.as_ref(), + &commitments.one_hot_trace, + commitments.untrusted_advice.as_ref(), trusted_advice_commitment, #[cfg(feature = "field-inline")] - proof.field_inc_commitment.as_ref(), - &proof.joint_opening_proof, + &commitments.field_inc, transcript, &checked.precommitted, stage4.clear()?, diff --git a/crates/jolt-verifier/src/stages/uniskip.rs b/crates/jolt-verifier/src/stages/uniskip.rs index e2983c944c..98eb94ab0f 100644 --- a/crates/jolt-verifier/src/stages/uniskip.rs +++ b/crates/jolt-verifier/src/stages/uniskip.rs @@ -13,15 +13,16 @@ use jolt_claims::protocols::composed::geometry::{ SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE, SPARTAN_PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, }; use jolt_claims::protocols::jolt::JoltRelationId; +use jolt_field::CanonicalDecode; use jolt_field::JoltField; use jolt_r1cs::constraints::jolt::{ SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE, SPARTAN_OUTER_UNISKIP_FIRST_ROUND_DEGREE, }; use jolt_sumcheck::{ - CenteredIntegerDomain, CommittedSumcheckConsistency, SumcheckClaim, SumcheckProof, - SumcheckStatement, UNISKIP_ROUND_TRANSCRIPT_LABEL, + CenteredIntegerDomain, CommittedSumcheckConsistency, SumcheckClaim, SumcheckStatement, + SumcheckVerifier, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; use crate::stages::zk::committed::{self, CommittedOutputClaimOutput}; use crate::verifier::CheckedInputs; @@ -41,8 +42,6 @@ pub struct UniskipParams { stage_number: usize, degree: usize, domain_size: usize, - /// The proof field name reported by the ZK commitment-count checks. - proof_field: &'static str, } impl UniskipParams { @@ -53,7 +52,6 @@ impl UniskipParams { stage_number: 1, degree: SPARTAN_OUTER_UNISKIP_FIRST_ROUND_DEGREE, domain_size: SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE, - proof_field: "stage1_uni_skip_first_round_proof", } } @@ -64,7 +62,6 @@ impl UniskipParams { stage_number: 2, degree: SPARTAN_PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, domain_size: SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE, - proof_field: "stage2_uni_skip_first_round_proof", } } @@ -92,29 +89,27 @@ impl UniskipParams { /// uni-skip round (the Spartan outer relation's cycle variables plus the two /// uni-skip-collapsed row variables). Both fronts call this, so the draw is /// single-sourced. -pub fn draw_spartan_outer_tau(transcript: &mut T, log_t: usize) -> Vec +pub fn draw_spartan_outer_tau(transcript: &mut C, log_t: usize) -> Vec where F: JoltField, - T: Transcript, + C: Channel, { #[expect( clippy::arithmetic_side_effects, reason = "log_t is an ilog2 result (< 64); log_t + 2 cannot overflow usize" )] - transcript.challenge_vector(log_t + 2) + transcript.challenges_small(log_t + 2) } -/// The stage-2 product tau_high, drawn before the product uni-skip round. -/// Both fronts call this, so the draw is single-sourced. MUST stay the raw -/// `challenge()` (not `challenge_scalar()`): both decode the same 16-byte -/// squeeze, but differently, so switching would silently change the value -/// without changing the transcript bytes. -pub fn draw_spartan_product_tau_high(transcript: &mut T) -> F +/// The stage-2 product tau_high, drawn before the product uni-skip round from +/// the small challenge set. Both fronts call this, so the draw is +/// single-sourced. +pub fn draw_spartan_product_tau_high(transcript: &mut C) -> F where F: JoltField, - T: Transcript, + C: Channel, { - transcript.challenge() + transcript.challenge_small() } /// The ZK uni-skip step's outputs: the committed round consistency and output @@ -126,79 +121,72 @@ pub struct UniskipZk { pub challenge: F, } -/// Verify a clear-mode uni-skip round against its input and output claims and -/// return the single reduction challenge. +/// The clear uni-skip step's outputs: the single reduction challenge and the +/// received output claim. +pub struct UniskipClear { + pub challenge: F, + pub output_claim: F, +} + +/// Verify a clear-mode uni-skip round against its input claim. /// -/// Protocol contract (byte-exact; the prover's uni-skip round must mirror it): -/// the wire proof is verified as a one-round sumcheck of `params`' degree over -/// `params`' centered integer domain, the reduced value is hard-checked -/// against `output_claim`, and `output_claim` is then absorbed under the -/// `b"opening_claim"` label — BEFORE any post-uni-skip draw (the remainder -/// batch's coefficient squeeze in particular). Errors are attributed to +/// Protocol contract (the prover's uni-skip round must mirror it): the round +/// polynomial is read as a one-round sumcheck of `params`' degree over +/// `params`' centered integer domain, then the output claim is received and +/// hard-checked against the reduced value — BEFORE any post-uni-skip draw (the +/// remainder batch's coefficient draw in particular). Errors are attributed to /// `params`' stage. -pub fn verify_clear( - proof: &SumcheckProof, +pub fn verify_clear( params: &UniskipParams, input_claim: F, - output_claim: F, - transcript: &mut T, -) -> Result + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> where F: JoltField, - T: Transcript, + H: Sponge, { - let reduction = proof - .verify( - &SumcheckClaim::new(UNISKIP_ROUNDS, params.degree, input_claim), - CenteredIntegerDomain::new(params.domain_size), - UNISKIP_ROUND_TRANSCRIPT_LABEL, - transcript, - ) - .map_err(|error| params.sumcheck_failed(error))?; + let reduction = SumcheckVerifier::verify( + &SumcheckClaim::new(UNISKIP_ROUNDS, params.degree, input_claim), + CenteredIntegerDomain::new(params.domain_size), + transcript, + ) + .map_err(|error| params.sumcheck_failed(error))?; + let output_claim: F = transcript.receive()?; if reduction.value != output_claim { return Err(VerifierError::StageClaimOutputMismatch { stage: params.stage_number, }); } - // Match the prover transcript: the uni-skip output is absorbed as an - // opening claim before any post-uni-skip draw (the remainder batch's RLC - // coefficient squeeze in particular). - transcript.append_labeled(b"opening_claim", &output_claim); - let [challenge] = reduction.point.as_slice() else { return Err(params.sumcheck_failed("uni-skip proof did not reduce to one challenge")); }; - Ok(*challenge) + Ok(UniskipClear { + challenge: *challenge, + output_claim, + }) } -/// Verify a ZK-mode uni-skip round: committed round consistency plus the -/// output-claim commitment count. The claims themselves stay committed -/// (BlindFold verifies them at stage 8). -pub fn verify_zk( +/// Verify a ZK-mode uni-skip round: the committed round and the single +/// output-claim commitment. The claims themselves stay committed (BlindFold +/// verifies them at stage 8). +pub fn verify_zk( checked: &CheckedInputs, - proof: &SumcheckProof, params: &UniskipParams, - transcript: &mut T, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result, VerifierError> where F: JoltField, - C: Clone + AppendToTranscript, - T: Transcript, + C: CanonicalDecode, + H: Sponge, { - let consistency = proof - .verify_committed_consistency( - SumcheckStatement::new(UNISKIP_ROUNDS, params.degree), - transcript, - ) - .map_err(|error| params.sumcheck_failed(error))?; - let output_claims = committed::verify_output_claim_commitments( - checked, - proof, - params.proof_field, - 1, - params.stage, - )?; + let shape = committed::output_claim_shape(checked, 1)?; + let (consistency, commitments) = SumcheckVerifier::verify_committed( + SumcheckStatement::new(UNISKIP_ROUNDS, params.degree), + shape.row_count(), + transcript, + ) + .map_err(|error| params.sumcheck_failed(error))?; let [round] = consistency.rounds.as_slice() else { return Err( params.sumcheck_failed("uni-skip committed consistency did not produce one challenge") @@ -207,7 +195,7 @@ where let challenge = round.challenge; Ok(UniskipZk { consistency, - output_claims, + output_claims: CommittedOutputClaimOutput { shape, commitments }, challenge, }) } diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs index 515acb909c..f071079c0f 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs @@ -205,8 +205,8 @@ struct SourceValues { publics: Vec<(VerifierPublicId, F)>, } -pub fn build( - input: BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub fn build( + input: BlindFoldInputs<'_, PCS, VC>, ) -> Result, VerifierError> where PCS: CommitmentScheme, @@ -522,15 +522,14 @@ fn domain_spec(domain: SumcheckDomain) -> SumcheckDomainSpec { } } -fn formula_dimensions( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn formula_dimensions( + input: &BlindFoldInputs<'_, PCS, VC>, ) -> Result where PCS: CommitmentScheme, VC: VectorCommitment, { crate::stages::build_formula_dimensions( - input.proof, input.preprocessing, input.checked, crate::num::ilog2(input.checked.trace_length), @@ -542,8 +541,8 @@ where clippy::type_complexity, reason = "the three RAM output-check publics (eq, mask, val_io)" )] -fn ram_output_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn ram_output_publics( + input: &BlindFoldInputs<'_, PCS, VC>, output_address_challenges: &[PCS::Field], ram_output_address: &[PCS::Field], ) -> Result<(PCS::Field, PCS::Field, PCS::Field), VerifierError> @@ -567,8 +566,8 @@ where // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field negations in the body. -fn ram_val_check_init( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn ram_val_check_init( + input: &BlindFoldInputs<'_, PCS, VC>, ) -> Result, VerifierError> where F: JoltField, @@ -582,7 +581,7 @@ where if input.checked.precommitted.program_image.is_some() { contributions.push(ram::RamValCheckInitContribution::program_image(-F::one())); } - if input.proof.untrusted_advice_commitment.is_some() { + if input.checked.untrusted_advice_commitment_present { let selector = advice_selector(input, JoltAdviceKind::Untrusted, &r_address)?; contributions.push(ram::RamValCheckInitContribution::untrusted(-selector.0)); } @@ -596,8 +595,8 @@ where )) } -fn ram_val_check_address( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn ram_val_check_address( + input: &BlindFoldInputs<'_, PCS, VC>, ) -> Result, VerifierError> where PCS: CommitmentScheme, @@ -616,8 +615,8 @@ where }) } -fn advice_selector( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn advice_selector( + input: &BlindFoldInputs<'_, PCS, VC>, kind: JoltAdviceKind, r_address: &[PCS::Field], ) -> Result<(PCS::Field, Vec), VerifierError> @@ -670,8 +669,8 @@ where Ok((selector, opening_point)) } -fn advice_source_point( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn advice_source_point( + input: &BlindFoldInputs<'_, PCS, VC>, kind: JoltAdviceKind, ) -> Result, VerifierError> where @@ -682,8 +681,8 @@ where advice_selector(input, kind, &r_address).map(|(_, point)| point) } -fn advice_layout( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn advice_layout( + input: &BlindFoldInputs<'_, PCS, VC>, kind: JoltAdviceKind, ) -> Option where @@ -697,8 +696,8 @@ where clippy::too_many_arguments, reason = "Stage 6 has several protocol components." )] -fn add_stage6_publics_and_challenges( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_stage6_publics_and_challenges( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, bytecode_address_rounds: usize, bytecode_rounds: usize, @@ -1086,8 +1085,8 @@ where Ok(()) } -fn bytecode_reduction_weights( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn bytecode_reduction_weights( + input: &BlindFoldInputs<'_, PCS, VC>, layout: &BytecodeClaimReductionLayout, ) -> Result, VerifierError> where @@ -1143,8 +1142,8 @@ fn add_bytecode_chunk_weight_publics( Ok(()) } -fn add_bytecode_reduction_cycle_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_bytecode_reduction_cycle_publics( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, layout: &BytecodeClaimReductionLayout, ) -> Result<(), VerifierError> @@ -1172,8 +1171,8 @@ where add_bytecode_chunk_weight_publics(values, chunk_weights) } -fn add_bytecode_reduction_address_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_bytecode_reduction_address_publics( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, layout: &BytecodeClaimReductionLayout, sumcheck_point: &[PCS::Field], @@ -1200,8 +1199,8 @@ where add_bytecode_chunk_weight_publics(values, chunk_weights) } -fn add_program_image_reduction_cycle_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_program_image_reduction_cycle_publics( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, layout: &ProgramImageClaimReductionLayout, ) -> Result<(), VerifierError> @@ -1231,8 +1230,8 @@ where ) } -fn add_program_image_reduction_address_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_program_image_reduction_address_publics( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, layout: &ProgramImageClaimReductionLayout, sumcheck_point: &[PCS::Field], @@ -1258,8 +1257,8 @@ where ) } -fn add_advice_cycle_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_advice_cycle_publics( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, layout: &AdviceClaimReductionLayout, kind: JoltAdviceKind, @@ -1291,8 +1290,8 @@ where ) } -fn add_advice_address_publics( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn add_advice_address_publics( + input: &BlindFoldInputs<'_, PCS, VC>, values: &mut SourceValues, layout: &AdviceClaimReductionLayout, kind: JoltAdviceKind, @@ -1319,8 +1318,8 @@ where ) } -fn stage6_virtualization_points( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +fn stage6_virtualization_points( + input: &BlindFoldInputs<'_, PCS, VC>, dimensions: hamming_weight::HammingWeightClaimReductionDimensions, ) -> Result>, VerifierError> where diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs index 19f30b6694..d74a98d0b5 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs @@ -3,8 +3,8 @@ use super::*; // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. -pub(super) fn add_stage1( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage1( + input: &BlindFoldInputs<'_, PCS, VC>, mut builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs index 4d63528a04..dec67b9dc1 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs @@ -22,8 +22,8 @@ use crate::stages::stage2::outputs::InstructionClaimReduction; // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. -pub(super) fn add_stage2( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage2( + input: &BlindFoldInputs<'_, PCS, VC>, mut builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> @@ -36,7 +36,7 @@ where let log_t = crate::num::ilog2(input.checked.trace_length); let log_k = crate::num::ilog2(input.checked.ram_K); let trace_dimensions = jolt_claims::protocols::jolt::TraceDimensions::new(log_t); - let read_write_dimensions = input.proof.rw_config.ram_dimensions(log_t, log_k); + let read_write_dimensions = input.checked.rw_config.ram_dimensions(log_t, log_k); let product_dimensions = SpartanProductDimensions::new(log_t); let raf_dimensions = ram::RamRafEvaluationDimensions::try_from(read_write_dimensions).map_err(|error| { diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs index ec67cbbf62..f48fd36c9d 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs @@ -2,8 +2,8 @@ use super::*; use jolt_claims::protocols::jolt::relations::spartan::SpartanShiftOutputClaims; -pub(super) fn add_stage3( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage3( + input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs index 1906feb55e..74d8af9fcf 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs @@ -6,8 +6,8 @@ use jolt_claims::protocols::jolt::relations::registers::RegistersReadWriteOutput // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. -pub(super) fn add_stage4( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage4( + input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> @@ -21,7 +21,7 @@ where let log_k = crate::num::ilog2(input.checked.ram_K); let trace_dimensions = jolt_claims::protocols::jolt::TraceDimensions::new(log_t); let register_dimensions = input - .proof + .checked .rw_config .register_dimensions(log_t, REGISTER_ADDRESS_BITS); // Eager: the proof-supplied phase split feeds round-count subtractions @@ -132,7 +132,7 @@ where )?; let output_ids = stage4_output_ids::( - input.proof.untrusted_advice_commitment.is_some(), + input.checked.untrusted_advice_commitment_present, input.checked.trusted_advice_commitment_present, input.checked.precommitted.program_image.is_some(), ); diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs index 5662ea4a49..d4b7dc75c9 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs @@ -7,8 +7,8 @@ use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationOu // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. -pub(super) fn add_stage5( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage5( + input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs index 2d18a868c2..7cc63aafd3 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs @@ -4,8 +4,8 @@ use jolt_claims::protocols::composed::ComposedClaims; use super::*; -pub(super) fn add_stage6a( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage6a( + input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> @@ -28,7 +28,7 @@ where let booleanity_dimensions = BooleanityDimensions::new( formula_dimensions.ra_layout, log_t, - input.proof.one_hot_config.committed_chunk_bits(), + input.checked.one_hot_config.committed_chunk_bits(), ); let booleanity_address_claims = relations::booleanity::BooleanityAddressPhase::new(booleanity_dimensions); diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs index 50042cfb2c..3dcb83510e 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs @@ -5,8 +5,8 @@ use jolt_claims::protocols::jolt::relations::ram::{ RamHammingBooleanityOutputClaims, RamRaVirtualizationOutputClaims, }; -pub(super) fn add_stage6b( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage6b( + input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> @@ -23,7 +23,7 @@ where let booleanity_dimensions = BooleanityDimensions::new( formula_dimensions.ra_layout, log_t, - input.proof.one_hot_config.committed_chunk_bits(), + input.checked.one_hot_config.committed_chunk_bits(), ); let booleanity_claims = relations::booleanity::BooleanityCyclePhase::new(booleanity_dimensions); let ram_hamming_claims = relations::ram::HammingBooleanity::new(trace_dimensions); diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs index 4ee4e85013..93d45bd86f 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs @@ -1,7 +1,7 @@ use super::*; -pub(super) fn add_stage7( - input: &BlindFoldInputs<'_, PCS, VC, ZkProof>, +pub(super) fn add_stage7( + input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, values: &mut SourceValues, ) -> Result, VerifierError> @@ -13,7 +13,7 @@ where let formula_dimensions = formula_dimensions(input)?; let hamming_dimensions = hamming_weight::HammingWeightClaimReductionDimensions::new( formula_dimensions.ra_layout, - input.proof.one_hot_config.committed_chunk_bits(), + input.checked.one_hot_config.committed_chunk_bits(), ); let hamming_claims = relations::claim_reductions::hamming_weight::ClaimReduction::new(hamming_dimensions); diff --git a/crates/jolt-verifier/src/stages/zk/committed.rs b/crates/jolt-verifier/src/stages/zk/committed.rs index 020243bc53..21afb2c8be 100644 --- a/crates/jolt-verifier/src/stages/zk/committed.rs +++ b/crates/jolt-verifier/src/stages/zk/committed.rs @@ -1,49 +1,26 @@ //! Shared checks for committed sumcheck stage boundaries. -use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_field::JoltField; -use jolt_sumcheck::SumcheckProof; - use crate::{verifier::CheckedInputs, VerifierError}; -pub(crate) use crate::stages::zk::outputs::CommittedOutputClaimOutput; +pub(crate) use crate::stages::zk::outputs::{ + CommittedOutputClaimOutput, CommittedOutputClaimShape, +}; -pub(crate) fn verify_output_claim_commitments( +/// The row layout of `output_claim_count` hidden claims at the build's vector +/// commitment capacity, which fixes how many output-claim commitments a +/// committed sumcheck sends. +pub(crate) fn output_claim_shape( checked: &CheckedInputs, - proof: &SumcheckProof, - proof_label: &'static str, output_claim_count: usize, - stage: JoltRelationId, -) -> Result, VerifierError> -where - F: JoltField, - C: Clone, -{ +) -> Result { // Invariant: Some(capacity) implies capacity >= MAX_BLINDFOLD_GENERATORS, // enforced by validate_zk_vector_commitment_setup before any stage runs (also - // guards the div_ceil below against zero). + // guards the div_ceil in `row_count` against zero). let capacity = checked .vc_capacity .ok_or(VerifierError::MissingVectorCommitmentSetup)?; - let expected = output_claim_count.div_ceil(capacity); - let committed = proof - .as_committed() - .ok_or(VerifierError::ExpectedCommittedProof { field: proof_label })?; - let got = committed.output_claims.commitments.len(); - if got != expected { - return Err(VerifierError::StageClaimSumcheckFailed { - stage: format!("{stage:?}"), - reason: format!( - "{proof_label} output-claim commitment count mismatch: expected {expected} for {output_claim_count} hidden claims at VC capacity {capacity}, got {got}", - ), - }); - } - - Ok(CommittedOutputClaimOutput { - shape: super::outputs::CommittedOutputClaimShape { - output_claim_count, - row_len: capacity, - }, - commitments: committed.output_claims.clone(), + Ok(CommittedOutputClaimShape { + output_claim_count, + row_len: capacity, }) } diff --git a/crates/jolt-verifier/src/stages/zk/inputs.rs b/crates/jolt-verifier/src/stages/zk/inputs.rs index 40df65e137..60b1479e73 100644 --- a/crates/jolt-verifier/src/stages/zk/inputs.rs +++ b/crates/jolt-verifier/src/stages/zk/inputs.rs @@ -2,7 +2,6 @@ use jolt_openings::CommitmentScheme; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{ stage1::Stage1ZkOutput, stage2::Stage2ZkOutput, stage3::Stage3ZkOutput, stage4::Stage4ZkOutput, stage5::Stage5ZkOutput, stage6a::Stage6aZkOutput, @@ -11,14 +10,13 @@ use crate::{ verifier::CheckedInputs, }; -pub struct BlindFoldInputs<'a, PCS, VC, ZkProof> +pub struct BlindFoldInputs<'a, PCS, VC> where PCS: CommitmentScheme, VC: jolt_crypto::VectorCommitment, { pub checked: &'a CheckedInputs, pub preprocessing: &'a JoltVerifierPreprocessing, - pub proof: &'a JoltProof, pub stage1: &'a Stage1ZkOutput, pub stage2: &'a Stage2ZkOutput, pub stage3: &'a Stage3ZkOutput, diff --git a/crates/jolt-verifier/src/stages/zk/outputs.rs b/crates/jolt-verifier/src/stages/zk/outputs.rs index adf09e8d28..666336d4dd 100644 --- a/crates/jolt-verifier/src/stages/zk/outputs.rs +++ b/crates/jolt-verifier/src/stages/zk/outputs.rs @@ -6,6 +6,13 @@ pub struct CommittedOutputClaimShape { pub row_len: usize, } +impl CommittedOutputClaimShape { + /// Number of output-claim row commitments the committed sumcheck sends. + pub fn row_count(&self) -> usize { + self.output_claim_count.div_ceil(self.row_len) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CommittedOutputClaimOutput { pub shape: CommittedOutputClaimShape, diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index 2aeb5f4c95..36e6716dc1 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -2,98 +2,144 @@ use common::constants::MAX_BLINDFOLD_GENERATORS; use common::jolt_device::JoltDevice; -use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltReadWriteConfig}; +#[cfg(not(feature = "akita"))] +use jolt_blindfold::BlindFoldProtocol; +use jolt_claims::protocols::jolt::geometry::dimensions::JoltFormulaDimensions; +use jolt_claims::protocols::jolt::{ + JoltOneHotConfig, JoltReadWriteConfig, JoltRelationId, TracePolynomialOrder, +}; #[cfg(not(feature = "akita"))] use jolt_crypto::HomomorphicCommitment; use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; #[cfg(not(feature = "akita"))] use jolt_openings::{AdditivelyHomomorphic, ZkOpeningScheme}; use jolt_program::preprocess::{compute_max_ram_k, compute_min_ram_k}; -use jolt_sumcheck::SumcheckProof; -#[cfg(feature = "akita")] -use jolt_transcript::append_length_prefixed; -use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript, U64Word}; +use jolt_transcript::{Channel, ProtocolId, Sponge, VerifierTranscript}; -#[cfg(not(feature = "akita"))] -use crate::proof::JoltCommitments; use crate::{ config::{ validate_proof_config, ZkConfig, JOLT_VERIFIER_CONFIG, JOLT_VERIFIER_INSTRUCTION_PROFILE, }, num, preprocessing::JoltVerifierPreprocessing, - proof::{JoltProof, TracePolynomialOrder}, + proof::{JoltProof, ProofCommitments, ProofHeader}, stages::{ - stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7, stage8, - CommittedProgramSchedule, PrecommittedSchedule, + build_formula_dimensions, stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7, + stage8, CommittedProgramSchedule, PrecommittedSchedule, }, VerifierError, }; +/// The sponge Jolt proofs run on. Every prover and verifier entry point is +/// generic over the sponge; this alias is the one place the default is chosen. +pub type JoltSponge = jolt_transcript::Blake2b512; + +/// The session every Jolt transcript is bound to. +pub const JOLT_SESSION: &[u8] = b""; + +/// The domain separator of a Jolt proof on sponge `H`. The protocol axes are +/// absorbed separately, by [`absorb_public_preamble`]. +pub fn jolt_protocol_id() -> ProtocolId { + ProtocolId::new::("jolt/v1") +} + #[cfg(not(feature = "akita"))] -pub fn verify( +pub fn verify( preprocessing: &JoltVerifierPreprocessing, public_io: &JoltDevice, - proof: &JoltProof, + proof: &JoltProof, trusted_advice_commitment: Option<&PCS::Output>, ) -> Result<(), VerifierError> where - F: JoltField + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, - PCS::Output: AppendToTranscript + HomomorphicCommitment, + PCS::Output: HomomorphicCommitment, VC: VectorCommitment, - VC::Output: Copy + HomomorphicCommitment + AppendToTranscript, - T: Transcript, + VC::Output: Copy + HomomorphicCommitment + CanonicalDecode, + H: Sponge, { - use crate::stages::zk::{blindfold, inputs::BlindFoldInputs}; - - let (checked, mut transcript) = validate_and_seed_transcript::( + validate_proof_config(&JOLT_VERIFIER_CONFIG, proof.protocol)?; + let mut transcript = + VerifierTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION, &proof.narg); + match verify_stages( preprocessing, public_io, - proof, trusted_advice_commitment, - )?; + &mut transcript, + )? { + VerifiedStages::Clear => {} + VerifiedStages::Zk(blindfold) => { + let vc_setup = preprocessing + .vc_setup + .as_ref() + .ok_or(VerifierError::MissingVectorCommitmentSetup)?; + blindfold + .verify::(vc_setup, &mut transcript) + .map_err(|error| VerifierError::BlindFoldVerificationFailed { + reason: error.to_string(), + })?; + } + } + transcript.finish()?; + Ok(()) +} - // Built once for the whole verification and shared by the stages that read - // the RA layout (5-8), instead of each rebuilding the same dimensions. - let formula_dimensions = crate::stages::build_formula_dimensions( - proof, - preprocessing, - &checked, - num::ilog2(checked.trace_length), - JoltRelationId::InstructionReadRaf, - )?; +/// What remains to check after the stage spine: nothing for a clear proof, +/// the BlindFold relation over the committed stage outputs for a ZK proof. +#[cfg(not(feature = "akita"))] +pub enum VerifiedStages { + Clear, + Zk(Box>), +} - let stage1 = stage1::verify(&checked, proof, &mut transcript)?; - let stage2 = stage2::verify(&checked, proof, &mut transcript, &stage1)?; - let stage3 = stage3::verify(&checked, proof, &mut transcript, &stage1, &stage2)?; - let stage4 = stage4::verify( - &checked, +/// Runs the stage spine on `transcript`: the seeding messages, stages 1–8, +/// and, for a ZK proof, the lowering of the committed stage outputs into the +/// BlindFold protocol. The ZK prover replays its own argument string through +/// this function to obtain the protocol it proves. +#[cfg(not(feature = "akita"))] +pub fn verify_stages( + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&PCS::Output>, + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> +where + F: JoltField, + PCS: CommitmentScheme + + AdditivelyHomomorphic + + ZkOpeningScheme, + PCS::Output: HomomorphicCommitment, + VC: VectorCommitment, + VC::Output: Copy + HomomorphicCommitment + CanonicalDecode, + H: Sponge, +{ + use crate::stages::zk::{blindfold, inputs::BlindFoldInputs}; + + let SeededTranscript { + checked, + commitments, + formula_dimensions, + } = seed_transcript( preprocessing, - proof, - &mut transcript, - &stage2, - &stage3, - )?; - let stage5 = stage5::verify( - &checked, - proof, - &formula_dimensions, - &mut transcript, - &stage2, - &stage4, + public_io, + trusted_advice_commitment, + transcript, )?; + + let stage1 = stage1::verify::(&checked, transcript)?; + let stage2 = stage2::verify(&checked, transcript, &stage1)?; + let stage3 = stage3::verify(&checked, transcript, &stage1, &stage2)?; + let stage4 = stage4::verify(&checked, preprocessing, transcript, &stage2, &stage3)?; + let stage5 = stage5::verify(&checked, &formula_dimensions, transcript, &stage2, &stage4)?; let stage6a = stage6a::verify( &checked, preprocessing, - proof, &formula_dimensions, - &mut transcript, + transcript, &stage1, &stage2, &stage3, @@ -103,9 +149,8 @@ where let stage6b = stage6b::verify( &checked, preprocessing, - proof, &formula_dimensions, - &mut transcript, + transcript, &stage1, &stage2, &stage3, @@ -113,30 +158,28 @@ where &stage5, &stage6a, )?; - let stage7 = stage7::verify( - &checked, - proof, - &formula_dimensions, - &mut transcript, - &stage4, - &stage6b, - )?; + let stage7 = stage7::verify(&checked, &formula_dimensions, transcript, &stage4, &stage6b)?; let stage8 = stage8::verify( &checked, preprocessing, - proof, + &commitments, &formula_dimensions, trusted_advice_commitment, - &mut transcript, + transcript, &stage6b, &stage7, )?; - if checked.zk { - let blindfold = blindfold::build(BlindFoldInputs { + if !checked.zk { + let stage8::Stage8Output::Clear(_) = stage8 else { + return Err(VerifierError::ExpectedClearProof { field: "stage8" }); + }; + return Ok(VerifiedStages::Clear); + } + Ok(VerifiedStages::Zk(Box::new(blindfold::build( + BlindFoldInputs { checked: &checked, preprocessing, - proof, stage1: stage1.zk()?, stage2: stage2.zk()?, stage3: stage3.zk()?, @@ -146,79 +189,49 @@ where stage6b: stage6b.zk()?, stage7: stage7.zk()?, stage8: stage8.zk()?, - })?; - let vc_setup = preprocessing - .vc_setup - .as_ref() - .ok_or(VerifierError::MissingVectorCommitmentSetup)?; - jolt_verifier_derive::fs_scope_guard!(BlindFold); - - transcript.append(&Label(b"BlindFold")); - blindfold - .verify::(proof.blindfold_proof()?, vc_setup, &mut transcript) - .map_err(|error| VerifierError::BlindFoldVerificationFailed { - reason: error.to_string(), - })?; - return Ok(()); - } - - let stage8::Stage8Output::Clear(_) = stage8 else { - return Err(VerifierError::ExpectedClearProof { field: "stage8" }); - }; - - Ok(()) + }, + )?))) } /// The Akita verification path: the same stage spine, with a random-selector /// reduction of the packed trace and one native opening for the trace, advice, /// and committed-program objects. No homomorphism bounds and no ZK tail. #[cfg(feature = "akita")] -pub fn verify( +pub fn verify( preprocessing: &JoltVerifierPreprocessing, public_io: &JoltDevice, - proof: &JoltProof, + proof: &JoltProof, trusted_advice_commitment: Option<&PCS::Output>, ) -> Result<(), VerifierError> where - F: JoltField + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme, - PCS::Output: Clone + AppendToTranscript + stage8::OneHotTraceCommitmentMetadata, + PCS::Output: Clone + stage8::OneHotTraceCommitmentMetadata, PCS::VerifierSetup: stage8::OneHotTraceSetupMetadata, VC: VectorCommitment, - VC::Output: Copy + AppendToTranscript, - T: Transcript, + VC::Output: Copy + CanonicalDecode, + H: Sponge, { - let (checked, mut transcript) = validate_and_seed_transcript::( + validate_proof_config(&JOLT_VERIFIER_CONFIG, proof.protocol)?; + let mut transcript = + VerifierTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION, &proof.narg); + let SeededTranscript { + checked, + commitments, + formula_dimensions, + } = seed_transcript( preprocessing, public_io, - proof, trusted_advice_commitment, - )?; - - // Built once for the whole verification and shared by the stages that read - // the RA layout (5-8), instead of each rebuilding the same dimensions. - let formula_dimensions = crate::stages::build_formula_dimensions( - proof, - preprocessing, - &checked, - num::ilog2(checked.trace_length), - JoltRelationId::InstructionReadRaf, - )?; - - let stage1 = stage1::verify(&checked, proof, &mut transcript)?; - let stage2 = stage2::verify(&checked, proof, &mut transcript, &stage1)?; - let stage3 = stage3::verify(&checked, proof, &mut transcript, &stage1, &stage2)?; - let stage4 = stage4::verify( - &checked, - preprocessing, - proof, &mut transcript, - &stage2, - &stage3, )?; + + let stage1 = stage1::verify::(&checked, &mut transcript)?; + let stage2 = stage2::verify(&checked, &mut transcript, &stage1)?; + let stage3 = stage3::verify(&checked, &mut transcript, &stage1, &stage2)?; + let stage4 = stage4::verify(&checked, preprocessing, &mut transcript, &stage2, &stage3)?; let stage5 = stage5::verify( &checked, - proof, &formula_dimensions, &mut transcript, &stage2, @@ -227,7 +240,6 @@ where let stage6a = stage6a::verify( &checked, preprocessing, - proof, &formula_dimensions, &mut transcript, &stage1, @@ -239,7 +251,6 @@ where let stage6b = stage6b::verify( &checked, preprocessing, - proof, &formula_dimensions, &mut transcript, &stage1, @@ -251,7 +262,6 @@ where )?; let stage7 = stage7::verify( &checked, - proof, &formula_dimensions, &mut transcript, &stage4, @@ -260,7 +270,7 @@ where let stage8 = stage8::verify( &checked, preprocessing, - proof, + &commitments, &formula_dimensions, trusted_advice_commitment, &mut transcript, @@ -273,45 +283,62 @@ where return Err(VerifierError::ExpectedClearProof { field: "stage8" }); }; + transcript.finish()?; Ok(()) } -/// Validates the inputs and proof shape, then seeds the Fiat-Shamir transcript -/// with the preamble and commitment absorptions. Every consumer of the staged -/// verification — [`verify`] on both builds, and the zk audit harness — starts -/// here, so there is exactly one absorption order. -pub fn validate_and_seed_transcript( +/// The verifier state after the transcript's leading messages: the validated +/// inputs, the received polynomial commitments, and the formula dimensions the +/// later stages share. +#[derive(Debug)] +pub struct SeededTranscript { + pub checked: CheckedInputs, + pub commitments: ProofCommitments, + pub formula_dimensions: JoltFormulaDimensions, +} + +/// Reads and validates the proof header, absorbs the public preamble, reads the +/// proof's commitments at the counts the header and preprocessing fix, and +/// absorbs the public commitments. The prover's stage 0 performs the mirrored +/// sequence (sending where this receives), so there is one order. +pub fn seed_transcript( preprocessing: &JoltVerifierPreprocessing, public_io: &JoltDevice, - proof: &JoltProof, trusted_advice_commitment: Option<&PCS::Output>, -) -> Result<(CheckedInputs, T), VerifierError> + transcript: &mut VerifierTranscript<'_, H>, +) -> Result, VerifierError> where PCS: CommitmentScheme, - PCS::Output: AppendToTranscript, VC: VectorCommitment, - VC::Output: AppendToTranscript, - T: Transcript, + H: Sponge, { - validate_proof_config(&JOLT_VERIFIER_CONFIG, proof.protocol)?; - validate_proof_consistency(proof, JOLT_VERIFIER_CONFIG.zk == ZkConfig::BlindFold)?; + let header = ProofHeader::receive(transcript)?; let checked = validate_inputs( preprocessing, public_io, - proof, + &header, trusted_advice_commitment.is_some(), )?; - - let mut transcript = T::new(b"Jolt"); - absorb_preamble(&checked, proof, &mut transcript); - absorb_commitments( + absorb_public_preamble(&checked, transcript); + let formula_dimensions = build_formula_dimensions( preprocessing, - proof, - trusted_advice_commitment, - &mut transcript, - ); - - Ok((checked, transcript)) + &checked, + num::ilog2(checked.trace_length), + JoltRelationId::InstructionReadRaf, + )?; + let commitments = ProofCommitments::receive::( + &preprocessing.pcs_setup, + &header, + #[cfg(not(feature = "akita"))] + formula_dimensions.ra_layout, + transcript, + )?; + absorb_public_commitments(preprocessing, trusted_advice_commitment, transcript); + Ok(SeededTranscript { + checked, + commitments, + formula_dimensions, + }) } #[expect(non_snake_case, reason = "Preserves the deployed proof field name.")] @@ -321,6 +348,10 @@ pub struct CheckedInputs { pub zk: bool, pub trace_length: usize, pub ram_K: usize, + pub rw_config: JoltReadWriteConfig, + pub one_hot_config: JoltOneHotConfig, + pub trace_polynomial_order: TracePolynomialOrder, + pub untrusted_advice_commitment_present: bool, pub entry_address: u64, pub preprocessing_digest: [u8; 32], pub trusted_advice_commitment_present: bool, @@ -328,22 +359,74 @@ pub struct CheckedInputs { pub precommitted: PrecommittedSchedule, } -pub fn validate_inputs( +/// Absorbs the public preamble: the verifier's protocol axes, the +/// preprocessing digest, the public I/O, and the entry address. Both sides run +/// it right after the proof header. +pub fn absorb_public_preamble(checked: &CheckedInputs, channel: &mut C) { + let public_io = &checked.public_io; + channel.public(&JOLT_VERIFIER_CONFIG.transcript_bytes()); + channel.public(&checked.preprocessing_digest); + channel.public_all(&[ + public_io.memory_layout.max_input_size, + public_io.memory_layout.max_output_size, + public_io.memory_layout.heap_size, + ]); + channel.public_bytes(&public_io.inputs); + channel.public_bytes(&public_io.outputs); + channel.public(&u8::from(public_io.panic)); + channel.public(&checked.entry_address); +} + +/// Absorbs the commitments both sides hold, after the proof's own: the trusted +/// advice commitment, then the committed program's preprocessing-held +/// commitments (bytecode chunks, then the program image). +pub fn absorb_public_commitments( + preprocessing: &JoltVerifierPreprocessing, + trusted_advice_commitment: Option<&PCS::Output>, + channel: &mut C, +) where + PCS: CommitmentScheme, + VC: VectorCommitment, + C: Channel, +{ + if let Some(commitment) = trusted_advice_commitment { + PCS::absorb_commitment(commitment, channel); + } + let Some(committed) = preprocessing.program.committed() else { + return; + }; + #[cfg(not(feature = "akita"))] + { + for commitment in &committed.bytecode_chunk_commitments { + PCS::absorb_commitment(commitment, channel); + } + PCS::absorb_commitment(&committed.program_image_commitment, channel); + } + #[cfg(feature = "akita")] + for commitment in &committed.direct_program_commitments { + PCS::absorb_commitment(commitment, channel); + } +} + +/// Validates the public inputs and the proof header against the preprocessing. +/// Both sides run it: the verifier on the header it received, the prover on the +/// header it is about to send. +pub fn validate_inputs( preprocessing: &JoltVerifierPreprocessing, public_io: &JoltDevice, - proof: &JoltProof, + header: &ProofHeader, trusted_advice_commitment_present: bool, ) -> Result where PCS: CommitmentScheme, VC: VectorCommitment, { - let trace_length = proof.trace_length; - let ram_k = proof.ram_K; - let trace_polynomial_order = proof.trace_polynomial_order; - let one_hot_config = proof.one_hot_config; + let trace_length = header.trace_length; + let ram_k = header.ram_K; + let trace_polynomial_order = header.trace_polynomial_order; + let one_hot_config = header.one_hot_config; #[cfg(not(feature = "akita"))] - let untrusted_advice_commitment_present = proof.untrusted_advice_commitment.is_some(); + let untrusted_advice_commitment_present = header.untrusted_advice; // The zk axis is fixed at compile time; every branch below const-folds. let zk = matches!(JOLT_VERIFIER_CONFIG.zk, ZkConfig::BlindFold); let vc_capacity = if zk { @@ -508,6 +591,10 @@ where zk, trace_length, ram_K: ram_k, + rw_config: header.rw_config, + one_hot_config, + trace_polynomial_order, + untrusted_advice_commitment_present: header.untrusted_advice, entry_address: program.entry_address(), preprocessing_digest: preprocessing.preprocessing_digest, trusted_advice_commitment_present, @@ -543,447 +630,6 @@ where Ok(got) } -pub fn validate_proof_consistency( - proof: &JoltProof, - zk: bool, -) -> Result<(), VerifierError> -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - // A build with field-inline enabled proves every guest under the composed protocol, so the - // field-inline committed payload is unconditionally required (absence means a producer - // without field-inline semantics — reject before any stage logic). On the packed axis the - // field-increment commitment slot is equally unconditional: presence is never claim-gated - // (an all-zero group still commits). - #[cfg(all(feature = "field-inline", not(feature = "akita")))] - if proof.commitments.field_inline.is_none() { - return Err(VerifierError::MissingProofPayload { - field: "commitments.field_inline", - }); - } - #[cfg(all(feature = "field-inline", feature = "akita"))] - if proof.field_inc_commitment.is_none() { - return Err(VerifierError::MissingProofPayload { - field: "field_inc_commitment", - }); - } - - let stage_proofs = [ - ( - &proof.stages.stage1_uni_skip_first_round_proof, - "stage1_uni_skip_first_round_proof", - ), - (&proof.stages.stage1_sumcheck_proof, "stage1_sumcheck_proof"), - ( - &proof.stages.stage2_uni_skip_first_round_proof, - "stage2_uni_skip_first_round_proof", - ), - (&proof.stages.stage2_sumcheck_proof, "stage2_sumcheck_proof"), - (&proof.stages.stage3_sumcheck_proof, "stage3_sumcheck_proof"), - (&proof.stages.stage4_sumcheck_proof, "stage4_sumcheck_proof"), - (&proof.stages.stage5_sumcheck_proof, "stage5_sumcheck_proof"), - ( - &proof.stages.stage6a_sumcheck_proof, - "stage6a_sumcheck_proof", - ), - ( - &proof.stages.stage6b_sumcheck_proof, - "stage6b_sumcheck_proof", - ), - (&proof.stages.stage7_sumcheck_proof, "stage7_sumcheck_proof"), - ]; - for (stage_proof, field) in stage_proofs { - validate_sumcheck_representation(stage_proof, field, zk)?; - } - match (&proof.claims, zk) { - (crate::proof::JoltProofClaims::Clear(_), false) - | (crate::proof::JoltProofClaims::Zk { .. }, true) => {} - (crate::proof::JoltProofClaims::Clear(_), true) => { - return Err(VerifierError::UnexpectedOpeningClaims); - } - (crate::proof::JoltProofClaims::Zk { .. }, false) => { - return Err(VerifierError::UnexpectedBlindFoldProof); - } - } - Ok(()) -} - -pub(crate) fn validate_sumcheck_representation( - proof: &SumcheckProof, - field: &'static str, - zk: bool, -) -> Result<(), VerifierError> -where - F: JoltField, -{ - if proof.is_committed() == zk { - return Ok(()); - } - - if zk { - Err(VerifierError::ExpectedCommittedProof { field }) - } else { - Err(VerifierError::ExpectedClearProof { field }) - } -} - -/// Absorbs the Jolt Fiat-Shamir preamble: the preprocessing digest, public I/O -/// metadata, and the proof-derived structural parameters. WARNING: the byte -/// order here is consensus-critical — the prover's preamble must absorb -/// identically or the transcripts diverge. -#[jolt_verifier_derive::fs_scope(Preamble)] -pub(crate) fn absorb_preamble( - checked: &CheckedInputs, - proof: &JoltProof, - transcript: &mut T, -) where - PCS: CommitmentScheme, - VC: VectorCommitment, - T: Transcript, -{ - let public_io = &checked.public_io; - absorb_labeled_bytes( - transcript, - b"preprocessing_digest", - &checked.preprocessing_digest, - ); - absorb_labeled_u64( - transcript, - b"max_input_size", - public_io.memory_layout.max_input_size, - ); - absorb_labeled_u64( - transcript, - b"max_output_size", - public_io.memory_layout.max_output_size, - ); - absorb_labeled_u64(transcript, b"heap_size", public_io.memory_layout.heap_size); - absorb_labeled_bytes(transcript, b"inputs", &public_io.inputs); - absorb_labeled_bytes(transcript, b"outputs", &public_io.outputs); - absorb_labeled_u64(transcript, b"panic", u64::from(public_io.panic)); - absorb_labeled_u64(transcript, b"ram_K", num::u64_from_usize(checked.ram_K)); - absorb_labeled_u64( - transcript, - b"trace_length", - num::u64_from_usize(checked.trace_length), - ); - absorb_labeled_u64(transcript, b"entry_address", checked.entry_address); - absorb_labeled_u64( - transcript, - b"ram_rw_phase1_num_rounds", - u64::from(proof.rw_config.ram_rw_phase1_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"ram_rw_phase2_num_rounds", - u64::from(proof.rw_config.ram_rw_phase2_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"registers_rw_phase1_num_rounds", - u64::from(proof.rw_config.registers_rw_phase1_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"registers_rw_phase2_num_rounds", - u64::from(proof.rw_config.registers_rw_phase2_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"log_k_chunk", - u64::from(proof.one_hot_config.log_k_chunk), - ); - absorb_labeled_u64( - transcript, - b"lookups_ra_virtual_log_k_chunk", - u64::from(proof.one_hot_config.lookups_ra_virtual_log_k_chunk), - ); - absorb_labeled_u64( - transcript, - b"dory_layout", - proof.trace_polynomial_order.transcript_scalar(), - ); -} - -/// Absorbs the commitments in the consensus-critical order — the proof-carried -/// polynomial commitments, the optional advice commitments, then the committed -/// program's preprocessing-held commitments. WARNING: the prover must absorb -/// identically or the transcripts diverge. On the `akita` build the order is -/// the canonical commitment-object order: `OneHotTrace`, untrusted advice, trusted -/// advice, and direct program objects. -#[jolt_verifier_derive::fs_scope(Commitments)] -pub(crate) fn absorb_commitments( - preprocessing: &JoltVerifierPreprocessing, - proof: &JoltProof, - trusted_advice_commitment: Option<&PCS::Output>, - transcript: &mut T, -) where - PCS: CommitmentScheme, - PCS::Output: AppendToTranscript, - VC: VectorCommitment, - T: Transcript, -{ - #[cfg(not(feature = "akita"))] - { - absorb_transcript_commitments( - &proof.commitments, - proof.untrusted_advice_commitment.as_ref(), - trusted_advice_commitment, - transcript, - ); - if let Some(committed) = preprocessing.program.committed() { - absorb_committed_program_commitments( - &committed.bytecode_chunk_commitments, - &committed.program_image_commitment, - transcript, - ); - } - } - #[cfg(feature = "akita")] - absorb_packed_commitments( - &proof.commitments, - proof.untrusted_advice_commitment.as_ref(), - trusted_advice_commitment, - #[cfg(feature = "field-inline")] - proof.field_inc_commitment.as_ref(), - preprocessing - .program - .committed() - .map_or(&[][..], |committed| &committed.direct_program_commitments), - transcript, - ); -} - -/// Absorbs the packed commitment objects in canonical object order: `OneHotTrace`, untrusted -/// advice, trusted advice, the field-increment commitment (field-inline builds), then direct -/// bytecode chunks and program image. Shared verbatim by the packed prover's stage 0. -#[cfg(feature = "akita")] -pub fn absorb_packed_commitments( - one_hot_trace: &C, - untrusted_advice_commitment: Option<&C>, - trusted_advice_commitment: Option<&C>, - #[cfg(feature = "field-inline")] field_inc_commitment: Option<&C>, - direct_program_commitments: &[C], - transcript: &mut T, -) where - C: AppendToTranscript, - T: Transcript, -{ - append_length_prefixed(transcript, b"commitment", one_hot_trace); - if let Some(commitment) = untrusted_advice_commitment { - append_length_prefixed(transcript, b"untrusted_advice", commitment); - } - if let Some(commitment) = trusted_advice_commitment { - append_length_prefixed(transcript, b"trusted_advice", commitment); - } - #[cfg(feature = "field-inline")] - if let Some(commitment) = field_inc_commitment { - append_length_prefixed(transcript, b"field_inc", commitment); - } - absorb_packed_program_commitments(direct_program_commitments, transcript); -} - -#[cfg(feature = "akita")] -pub fn absorb_packed_program_commitments(commitments: &[C], transcript: &mut T) -where - C: AppendToTranscript, - T: Transcript, -{ - let Some((image, chunks)) = commitments.split_last() else { - return; - }; - for (index, commitment) in chunks.iter().enumerate() { - transcript.append(&U64Word(num::u64_from_usize(index))); - append_length_prefixed(transcript, b"bytecode_chunk_commitment", commitment); - } - append_length_prefixed(transcript, b"program_image_init_commitment", image); -} - -/// Absorbs the preprocessing-held committed-program commitments (per-chunk -/// bytecode, then the program image), immediately after the proof-carried -/// commitments. Shared verbatim by the prover's stage 0. -pub fn absorb_committed_program_commitments( - bytecode_chunk_commitments: &[C], - program_image_commitment: &C, - transcript: &mut T, -) where - C: AppendToTranscript, - T: Transcript, -{ - for commitment in bytecode_chunk_commitments { - append_payload_label(transcript, b"bytecode_chunk_commit", commitment); - transcript.append(commitment); - } - append_payload_label( - transcript, - b"program_image_commitment", - program_image_commitment, - ); - transcript.append(program_image_commitment); -} - -/// Absorbs the proof-derived polynomial commitments (increment, one-hot `ra`, -/// and optional advice commitments) in the consensus-critical order. WARNING: -/// this covers only the commitments carried by the proof itself; committed -/// program-image commitments live in the preprocessing and are absorbed -/// separately by `absorb_commitments` immediately after this call. -#[cfg(not(feature = "akita"))] -pub fn absorb_transcript_commitments( - commitments: &JoltCommitments, - untrusted_advice_commitment: Option<&C>, - trusted_advice_commitment: Option<&C>, - transcript: &mut T, -) where - C: AppendToTranscript, - T: Transcript, -{ - let mut absorb_commitment = |commitment: &C| { - append_payload_label(transcript, b"commitment", commitment); - transcript.append(commitment); - }; - absorb_commitment(&commitments.rd_inc); - absorb_commitment(&commitments.ram_inc); - for commitment in &commitments.instruction_ra { - absorb_commitment(commitment); - } - for commitment in &commitments.ram_ra { - absorb_commitment(commitment); - } - for commitment in &commitments.bytecode_ra { - absorb_commitment(commitment); - } - // The field-inline payload absorbs after the base commitments and before - // advice, mirroring its appended-extension position everywhere else in the - // protocol. The prover's stage 0 must commit in this same order. - #[cfg(feature = "field-inline")] - if let Some(field_inline) = &commitments.field_inline { - append_payload_label( - transcript, - b"field_rd_inc_commitment", - &field_inline.field_registers.rd_inc, - ); - transcript.append(&field_inline.field_registers.rd_inc); - } - if let Some(untrusted_advice_commitment) = untrusted_advice_commitment { - append_payload_label(transcript, b"untrusted_advice", untrusted_advice_commitment); - transcript.append(untrusted_advice_commitment); - } - if let Some(trusted_advice_commitment) = trusted_advice_commitment { - append_payload_label(transcript, b"trusted_advice", trusted_advice_commitment); - transcript.append(trusted_advice_commitment); - } -} - -fn append_payload_label(transcript: &mut T, label: &'static [u8], payload: &A) -where - T: Transcript, - A: AppendToTranscript, -{ - if let Some(len) = payload.transcript_payload_len() { - transcript.append(&LabelWithCount(label, len)); - } else { - transcript.append(&Label(label)); - } -} - -/// Proof-derived configuration that participates in the Fiat-Shamir preamble. -/// Bundles the parameters [`absorb_transcript_preamble`] reads from the proof so -/// the modular prover can seed an identical transcript before it has a finished -/// [`JoltProof`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ProofTranscriptConfig { - pub rw_config: JoltReadWriteConfig, - pub one_hot_config: JoltOneHotConfig, - pub trace_polynomial_order: TracePolynomialOrder, -} - -/// Verifier state captured immediately before stage 1, after input validation -/// and the preamble/commitment absorption. The modular prover drives the staged -/// verification itself, so it reuses this entry point to obtain the checked -/// inputs and a transcript seeded identically to [`verify`]. -#[derive(Debug)] -pub struct PreStage1VerifierState { - pub checked: CheckedInputs, - pub transcript: T, -} - -/// Absorbs the Jolt Fiat-Shamir preamble: the preprocessing digest, public I/O -/// metadata, and the proof-derived structural parameters carried by -/// [`ProofTranscriptConfig`]. WARNING: the byte order here is consensus-critical. -/// It must stay identical to the order [`verify`] uses (via `absorb_preamble`) -/// or prover and verifier transcripts diverge. -pub fn absorb_transcript_preamble( - checked: &CheckedInputs, - config: ProofTranscriptConfig, - transcript: &mut T, -) where - T: Transcript, -{ - let public_io = &checked.public_io; - absorb_labeled_bytes( - transcript, - b"preprocessing_digest", - &checked.preprocessing_digest, - ); - absorb_labeled_u64( - transcript, - b"max_input_size", - public_io.memory_layout.max_input_size, - ); - absorb_labeled_u64( - transcript, - b"max_output_size", - public_io.memory_layout.max_output_size, - ); - absorb_labeled_u64(transcript, b"heap_size", public_io.memory_layout.heap_size); - absorb_labeled_bytes(transcript, b"inputs", &public_io.inputs); - absorb_labeled_bytes(transcript, b"outputs", &public_io.outputs); - absorb_labeled_u64(transcript, b"panic", u64::from(public_io.panic)); - absorb_labeled_u64(transcript, b"ram_K", num::u64_from_usize(checked.ram_K)); - absorb_labeled_u64( - transcript, - b"trace_length", - num::u64_from_usize(checked.trace_length), - ); - absorb_labeled_u64(transcript, b"entry_address", checked.entry_address); - absorb_labeled_u64( - transcript, - b"ram_rw_phase1_num_rounds", - u64::from(config.rw_config.ram_rw_phase1_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"ram_rw_phase2_num_rounds", - u64::from(config.rw_config.ram_rw_phase2_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"registers_rw_phase1_num_rounds", - u64::from(config.rw_config.registers_rw_phase1_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"registers_rw_phase2_num_rounds", - u64::from(config.rw_config.registers_rw_phase2_num_rounds), - ); - absorb_labeled_u64( - transcript, - b"log_k_chunk", - u64::from(config.one_hot_config.log_k_chunk), - ); - absorb_labeled_u64( - transcript, - b"lookups_ra_virtual_log_k_chunk", - u64::from(config.one_hot_config.lookups_ra_virtual_log_k_chunk), - ); - absorb_labeled_u64( - transcript, - b"dory_layout", - config.trace_polynomial_order.transcript_scalar(), - ); -} - /// Fail closed on a zero-based RAM remap. Stage 2's RAF-evaluation unmap is /// `8k + lowest_address`, and the lattice digit-zero reconstruction relies on /// `unmap(0) = lowest_address ≠ 0` to distinguish "no RAM access" from an @@ -1005,202 +651,6 @@ fn validate_ram_remap_base( Ok(()) } -#[expect( - clippy::too_many_arguments, - reason = "Mirrors the proof-derived inputs validate_inputs threads through; bundling them would obscure the FS-critical parameter set." -)] -pub fn validate_inputs_from_parts( - preprocessing: &JoltVerifierPreprocessing, - public_io: &JoltDevice, - trace_length: usize, - ram_k: usize, - trace_polynomial_order: TracePolynomialOrder, - one_hot_config: JoltOneHotConfig, - trusted_advice_commitment_present: bool, - #[cfg(not(feature = "akita"))] untrusted_advice_commitment_present: bool, - zk: bool, -) -> Result -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - let memory_layout = preprocessing.program.memory_layout(); - if &public_io.memory_layout != memory_layout { - return Err(VerifierError::MemoryLayoutMismatch); - } - validate_ram_remap_base(memory_layout)?; - - if num::u64_from_usize(public_io.inputs.len()) > memory_layout.max_input_size { - return Err(VerifierError::InputTooLarge { - got: public_io.inputs.len(), - // The failed comparison bounds the maximum below a usize length. - max: usize::try_from(memory_layout.max_input_size).unwrap_or(usize::MAX), - }); - } - - if num::u64_from_usize(public_io.outputs.len()) > memory_layout.max_output_size { - return Err(VerifierError::OutputTooLarge { - got: public_io.outputs.len(), - // The failed comparison bounds the maximum below a usize length. - max: usize::try_from(memory_layout.max_output_size).unwrap_or(usize::MAX), - }); - } - - if !trace_length.is_power_of_two() - || trace_length > preprocessing.program.max_padded_trace_length() - { - return Err(VerifierError::InvalidTraceLength { - got: trace_length, - max: preprocessing.program.max_padded_trace_length(), - }); - } - - let min_ram_k = compute_min_ram_k( - preprocessing.program.min_bytecode_address(), - preprocessing.program.program_image_len_words(), - memory_layout, - ) - .map_err(|error| VerifierError::InvalidMemoryLayout { - reason: error.to_string(), - })?; - let max_ram_k = - compute_max_ram_k(memory_layout).map_err(|error| VerifierError::InvalidMemoryLayout { - reason: error.to_string(), - })?; - if !ram_k.is_power_of_two() || ram_k < min_ram_k || ram_k > max_ram_k { - return Err(VerifierError::InvalidRamK { - got: ram_k, - min: min_ram_k, - max: max_ram_k, - }); - } - - let vc_capacity = if zk { - Some(validate_zk_vector_commitment_setup::( - preprocessing, - )?) - } else { - None - }; - - let mut normalized_public_io = public_io.clone(); - normalized_public_io.outputs.truncate( - normalized_public_io - .outputs - .iter() - .rposition(|&byte| byte != 0) - .map_or(0, |position| position.saturating_add(1)), - ); - - let committed_program = - preprocessing - .program - .committed() - .map(|committed| { - #[cfg(feature = "akita")] - if committed.trace_order != trace_polynomial_order { - return Err(VerifierError::InvalidCommittedProgram { - reason: "committed-program trace order disagrees with the proof".to_owned(), - }); - } - let program_image_start_index = memory_layout - .remapped_word_address(committed.meta.min_bytecode_address) - .map_err(|error| VerifierError::InvalidCommittedProgram { - reason: error.to_string(), - })?; - if committed.meta.entry_bytecode_index >= committed.meta.bytecode_len { - return Err(VerifierError::InvalidCommittedProgram { - reason: format!( - "entry bytecode index {} is out of range for bytecode length {}", - committed.meta.entry_bytecode_index, committed.meta.bytecode_len - ), - }); - } - let program_image_start_index = usize::try_from(program_image_start_index) - .map_err(|_| VerifierError::InvalidCommittedProgram { - reason: format!( - "program image start index {program_image_start_index} does not fit usize" - ), - })?; - Ok(CommittedProgramSchedule { - bytecode_len: committed.meta.bytecode_len, - bytecode_chunk_count: committed.bytecode_chunk_count(), - program_image_len_words: committed.meta.program_image_len_words, - program_image_start_index, - }) - }) - .transpose()?; - #[cfg(not(feature = "akita"))] - let trusted_advice_size = trusted_advice_commitment_present - .then(|| advice_size_to_usize(memory_layout.max_trusted_advice_size, "trusted")) - .transpose()?; - #[cfg(not(feature = "akita"))] - let untrusted_advice_size = untrusted_advice_commitment_present - .then(|| advice_size_to_usize(memory_layout.max_untrusted_advice_size, "untrusted")) - .transpose()?; - let precommitted = PrecommittedSchedule::new( - trace_polynomial_order, - num::ilog2(trace_length), - one_hot_config.committed_chunk_bits(), - #[cfg(not(feature = "akita"))] - trusted_advice_size, - #[cfg(not(feature = "akita"))] - untrusted_advice_size, - committed_program, - ) - .map_err(|error| VerifierError::InvalidPrecommittedSchedule { - reason: error.to_string(), - })?; - - Ok(CheckedInputs { - public_io: normalized_public_io, - zk, - trace_length, - ram_K: ram_k, - entry_address: preprocessing.program.entry_address(), - preprocessing_digest: preprocessing.preprocessing_digest, - trusted_advice_commitment_present, - vc_capacity, - precommitted, - }) -} - -/// Runs the [`verify`] preamble — input validation, proof-consistency and -/// config checks, then preamble + commitment absorption — and returns the -/// pre-stage-1 state. [`verify`] delegates to this function, so both paths share -/// one absorption order and cannot diverge. -pub fn verify_until_stage1( - preprocessing: &JoltVerifierPreprocessing, - public_io: &JoltDevice, - proof: &JoltProof, - trusted_advice_commitment: Option<&PCS::Output>, -) -> Result, VerifierError> -where - PCS: CommitmentScheme, - PCS::Output: AppendToTranscript, - VC: VectorCommitment, - VC::Output: AppendToTranscript, - T: Transcript, -{ - let (checked, transcript) = - validate_and_seed_transcript(preprocessing, public_io, proof, trusted_advice_commitment)?; - - Ok(PreStage1VerifierState { - checked, - transcript, - }) -} - -fn absorb_labeled_bytes(transcript: &mut T, label: &'static [u8], bytes: &[u8]) { - transcript.append(&LabelWithCount(label, num::u64_from_usize(bytes.len()))); - transcript.append_bytes(bytes); -} - -fn absorb_labeled_u64(transcript: &mut T, label: &'static [u8], value: u64) { - transcript.append(&Label(label)); - transcript.append(&U64Word(value)); -} - #[cfg(test)] #[cfg_attr( not(feature = "field-inline"), From b9a8aecdae1437d946d2a8108c4a8966fb9aafde Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:45:45 -0400 Subject: [PATCH 14/62] refactor(prover)!: write Jolt proofs as NARG argument strings 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. --- crates/jolt-prover/src/blindfold.rs | 197 ++++-------------- crates/jolt-prover/src/dory/prover.rs | 119 +++-------- crates/jolt-prover/src/dory/stages/stage0.rs | 144 ++++++------- crates/jolt-prover/src/dory/stages/stage8.rs | 43 ++-- crates/jolt-prover/src/driver.rs | 42 ++-- crates/jolt-prover/src/profile.rs | 6 +- crates/jolt-prover/src/recorder.rs | 71 ++----- crates/jolt-prover/src/stages/drivers.rs | 16 +- .../src/stages/field_inline_fixtures.rs | 48 ++--- crates/jolt-prover/src/stages/stage1.rs | 21 +- crates/jolt-prover/src/stages/stage2.rs | 21 +- crates/jolt-prover/src/stages/stage3.rs | 19 +- crates/jolt-prover/src/stages/stage4.rs | 41 ++-- crates/jolt-prover/src/stages/stage5.rs | 19 +- crates/jolt-prover/src/stages/stage6a.rs | 19 +- crates/jolt-prover/src/stages/stage6b.rs | 19 +- crates/jolt-prover/src/stages/stage7.rs | 19 +- crates/jolt-prover/tests/e2e_matrix.rs | 6 +- .../tests/support/field_inline/dory.rs | 8 +- 19 files changed, 315 insertions(+), 563 deletions(-) diff --git a/crates/jolt-prover/src/blindfold.rs b/crates/jolt-prover/src/blindfold.rs index 3c2f8e3477..ff73b93d3f 100644 --- a/crates/jolt-prover/src/blindfold.rs +++ b/crates/jolt-prover/src/blindfold.rs @@ -1,13 +1,11 @@ //! The ZK proof tail: BlindFold over the committed stage proofs. //! //! The prover does not mirror the verifier's protocol lowering — it *runs* -//! it, strictly through `jolt-verifier`'s existing public verification -//! surface. After stage 8 it assembles a shell proof (every wire field real, -//! the claims slot a unit placeholder) and replays it through the verifier's -//! own stage functions — `validate_and_seed_transcript`, `stage1::verify` … -//! `stage8::verify` — to obtain the per-stage ZK outputs and a transcript -//! positioned exactly where the verifier's will be, then lowers them with -//! the verifier's own `stages::zk::blindfold::build`. The `BlindFoldProtocol` +//! it, strictly through `jolt-verifier`'s public verification surface. After +//! stage 8 it replays its own argument string through the verifier's +//! `verify_stages` — the seeding messages, `stage1::verify` … `stage8::verify`, +//! and the lowering with `stages::zk::blindfold::build` — on a verifier +//! transcript that must land exactly on the prover's own. The `BlindFoldProtocol` //! the prover proves against is therefore the same code path the verifier //! executes — a claim-formula change that updates the verifier's lowering is //! picked up here automatically — and the replay doubles as a full @@ -19,21 +17,15 @@ //! stage recipes prove over. use common::jolt_device::JoltDevice; -use jolt_blindfold::{BlindFoldProof, BlindFoldProtocol, BlindFoldWitness}; +use jolt_blindfold::BlindFoldWitness; use jolt_claims::protocols::composed::geometry::SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE; -use jolt_claims::protocols::jolt::JoltRelationId; use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; -use jolt_field::{Accumulator, JoltField, WithAccumulator}; +use jolt_field::{Accumulator, CanonicalDecode, JoltField, WithAccumulator}; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; use jolt_r1cs::constraints::jolt::SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE; use jolt_sumcheck::{CommittedSumcheckWitness, SumcheckDomainSpec}; -use jolt_transcript::{AppendToTranscript, Label, Transcript}; -use jolt_verifier::proof::JoltProof; -use jolt_verifier::stages::zk::{blindfold, inputs::BlindFoldInputs}; -use jolt_verifier::stages::{ - stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7, stage8, -}; -use jolt_verifier::VerifierError; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; +use jolt_verifier::{jolt_protocol_id, verify_stages, VerifiedStages, VerifierError, JOLT_SESSION}; use crate::{JoltProverPreprocessing, ProverError}; @@ -97,40 +89,47 @@ pub(crate) struct ZkFinalOpening { pub evaluation_blind: F, } -/// Prove the BlindFold tail for `shell` (the assembled proof with a unit -/// claims placeholder). `forward_state` is the prover's own transcript state -/// at the stage-8 boundary — the replay must land on the same bytes. -pub(crate) fn prove_blindfold( +/// Prove the BlindFold tail onto `transcript`, which holds the argument string +/// through stage 8. The verifier's stage spine is replayed over that prefix to +/// obtain the BlindFold protocol, and the replay must land on the forward +/// transcript's exact state. +pub(crate) fn prove_blindfold( preprocessing: &JoltProverPreprocessing, public_io: &JoltDevice, trusted_advice_commitment: Option<&PCS::Output>, - shell: &JoltProof, witnesses: &ZkStageWitnesses, final_opening: &ZkFinalOpening, - forward_state: [u8; 32], -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result<(), ProverError> where - F: JoltField + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, - PCS::Output: AppendToTranscript + HomomorphicCommitment, + PCS::Output: HomomorphicCommitment, VC: VectorCommitment, - VC::Output: Copy + HomomorphicCommitment + AppendToTranscript, - T: Transcript, + VC::Output: Copy + HomomorphicCommitment + CanonicalDecode, + H: Sponge, ::Accumulator: Accumulator, { - let (protocol, mut transcript) = replay_stages::( + let mut replay = + VerifierTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION, transcript.narg()); + let VerifiedStages::Zk(protocol) = verify_stages::( &preprocessing.verifier, public_io, - shell, trusted_advice_commitment, - )?; - // A 32-byte compare guarding the exact seam the design rests on: the - // replay landing on the prover's forward transcript bytes. Hard error - // (not debug-only) so release provers diagnose drift here rather than - // as a downstream BlindFold verification failure. - if transcript.state() != forward_state { + &mut replay, + )? + else { + return Err(ProverError::InvariantViolation { + reason: "the verifier replay of a ZK proof produced no BlindFold protocol", + }); + }; + // Hard error (not debug-only) so release provers diagnose drift here + // rather than as a downstream BlindFold verification failure. + if replay.remaining() != 0 + || replay.preview().squeeze::<32>() != transcript.preview().squeeze::<32>() + { return Err(ProverError::InvariantViolation { reason: "the verifier replay diverged from the prover's forward transcript", }); @@ -151,11 +150,10 @@ where .ok_or(ProverError::Verifier( VerifierError::MissingVectorCommitmentSetup, ))?; - transcript.append(&Label(b"BlindFold")); - let proof = jolt_blindfold::prove::( + jolt_blindfold::prove::( vc_setup, &protocol, - &mut transcript, + transcript, BlindFoldWitness { rows: &assigned.rows, blindings: &assigned.blindings, @@ -164,126 +162,7 @@ where }, &mut rand_core::OsRng, )?; - Ok(proof) -} - -/// Replay the shell through the verifier's public stage spine and lower the -/// ZK outputs into the BlindFold protocol — the same call sequence -/// `jolt_verifier::verify` runs before its BlindFold tail, expressed against -/// the same public surface its ZK audit harness uses. -#[expect( - clippy::type_complexity, - reason = "the pair is the protocol plus the transcript it was lowered on" -)] -fn replay_stages( - preprocessing: &jolt_verifier::JoltVerifierPreprocessing, - public_io: &JoltDevice, - shell: &JoltProof, - trusted_advice_commitment: Option<&PCS::Output>, -) -> Result<(BlindFoldProtocol, T), ProverError> -where - F: JoltField + AppendToTranscript, - PCS: CommitmentScheme - + AdditivelyHomomorphic - + ZkOpeningScheme, - PCS::Output: AppendToTranscript + HomomorphicCommitment, - VC: VectorCommitment, - VC::Output: Copy + HomomorphicCommitment + AppendToTranscript, - T: Transcript, -{ - let (checked, mut transcript) = jolt_verifier::validate_and_seed_transcript::( - preprocessing, - public_io, - shell, - trusted_advice_commitment, - )?; - let formula_dimensions = jolt_verifier::stages::build_formula_dimensions( - shell, - preprocessing, - &checked, - checked.trace_length.ilog2() as usize, - JoltRelationId::InstructionReadRaf, - )?; - - let stage1 = stage1::verify(&checked, shell, &mut transcript)?; - let stage2 = stage2::verify(&checked, shell, &mut transcript, &stage1)?; - let stage3 = stage3::verify(&checked, shell, &mut transcript, &stage1, &stage2)?; - let stage4 = stage4::verify( - &checked, - preprocessing, - shell, - &mut transcript, - &stage2, - &stage3, - )?; - let stage5 = stage5::verify( - &checked, - shell, - &formula_dimensions, - &mut transcript, - &stage2, - &stage4, - )?; - let stage6a = stage6a::verify( - &checked, - preprocessing, - shell, - &formula_dimensions, - &mut transcript, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - )?; - let stage6b = stage6b::verify( - &checked, - preprocessing, - shell, - &formula_dimensions, - &mut transcript, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - &stage6a, - )?; - let stage7 = stage7::verify( - &checked, - shell, - &formula_dimensions, - &mut transcript, - &stage4, - &stage6b, - )?; - let stage8 = stage8::verify( - &checked, - preprocessing, - shell, - &formula_dimensions, - trusted_advice_commitment, - &mut transcript, - &stage6b, - &stage7, - )?; - - let protocol = blindfold::build(BlindFoldInputs { - checked: &checked, - preprocessing, - proof: shell, - stage1: stage1.zk()?, - stage2: stage2.zk()?, - stage3: stage3.zk()?, - stage4: stage4.zk()?, - stage5: stage5.zk()?, - stage6a: stage6a.zk()?, - stage6b: stage6b.zk()?, - stage7: stage7.zk()?, - stage8: stage8.zk()?, - })?; - - Ok((protocol, transcript)) + Ok(()) } #[cfg(test)] diff --git a/crates/jolt-prover/src/dory/prover.rs b/crates/jolt-prover/src/dory/prover.rs index 14b661ed4f..4cde87fcf5 100644 --- a/crates/jolt-prover/src/dory/prover.rs +++ b/crates/jolt-prover/src/dory/prover.rs @@ -1,17 +1,16 @@ //! The top-level prover: the stage recipes run in protocol order on one -//! transcript and one backend session, and their wire outputs assemble into -//! the complete [`JoltProof`]. +//! transcript and one backend session, and the transcript's argument string +//! is the [`JoltProof`]. use common::jolt_device::JoltDevice; use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; +use jolt_field::CanonicalDecode; use jolt_field::{Accumulator, JoltField, WithAccumulator}; use jolt_kernels::JoltBackend; use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::Sponge; use jolt_verifier::config::JoltProtocolConfig; -#[cfg(not(feature = "zk"))] -use jolt_verifier::proof::ClearProofClaims; -use jolt_verifier::proof::{JoltProof, JoltProofClaims, JoltStageProofs}; +use jolt_verifier::proof::JoltProof; use jolt_witness::JoltWitnessPlane; use crate::boundary::finish_stage; @@ -29,11 +28,10 @@ use crate::stages::stage7::prove_stage7; use crate::{JoltProverPreprocessing, ProverConfig, ProverError}; /// Prove one execution: run stages 0 through 8 on a fresh transcript and -/// backend session, and assemble the [`JoltProof`] in the compiled proof -/// mode — clear claims without the `zk` feature, the BlindFold tail with it. +/// backend session in the compiled proof mode — clear claims without the `zk` +/// feature, the BlindFold tail with it — and return the argument string. /// -/// `config` is the derived proof shape (its five wire fields are copied into -/// the proof verbatim), `witness` the trace-backed provider the kernels read, +/// `config` is the derived proof shape (sent as the proof header), `witness` the trace-backed provider the kernels read, /// and `public_io` the Fiat-Shamir preamble's program I/O. /// /// `trusted_advice` is the externally supplied (preprocessing-time) @@ -50,29 +48,29 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError}; /// chunk/image hints). Dominant advice returns /// [`ProverError::Unsupported`] at stage 0. #[tracing::instrument(skip_all, name = "jolt_prover::prove", fields(trace_length = config.trace_length))] -pub fn prove( +pub fn prove( backend: &JoltBackend, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&TrustedAdviceCommitment>, witness: &W, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result> where - F: JoltField + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, - PCS::Output: AppendToTranscript + HomomorphicCommitment, + PCS::Output: HomomorphicCommitment, VC: VectorCommitment, - VC::Output: Copy + HomomorphicCommitment + AppendToTranscript, - T: Transcript, + VC::Output: Copy + HomomorphicCommitment + CanonicalDecode, + H: Sponge, W: JoltWitnessPlane, ::Accumulator: Accumulator, { let mode = ProofMode::::new(preprocessing.verifier.vc_setup.as_ref())?; let mut session = backend.begin_proof(); - let stage0 = prove_stage0::( + let stage0 = prove_stage0::( backend, &mut session, preprocessing, @@ -86,7 +84,7 @@ where let checked = stage0.checked; let mut transcript = stage0.transcript; - let stage1 = prove_stage1::( + let stage1 = prove_stage1::( backend, &mut session, &mode, @@ -95,7 +93,7 @@ where &mut transcript, )?; finish_stage("stage1", log_t, &session, &stage1.clear_output); - let stage2 = prove_stage2::( + let stage2 = prove_stage2::( backend, &mut session, &mode, @@ -106,7 +104,7 @@ where &mut transcript, )?; finish_stage("stage2", log_t, &session, &stage2.clear_output); - let stage3 = prove_stage3::( + let stage3 = prove_stage3::( backend, &mut session, &mode, @@ -117,7 +115,7 @@ where &mut transcript, )?; finish_stage("stage3", log_t, &session, &stage3.clear_output); - let stage4 = prove_stage4::( + let stage4 = prove_stage4::( backend, &mut session, &mode, @@ -130,7 +128,7 @@ where &mut transcript, )?; finish_stage("stage4", log_t, &session, &stage4.clear_output); - let stage5 = prove_stage5::( + let stage5 = prove_stage5::( backend, &mut session, &mode, @@ -143,7 +141,7 @@ where &mut transcript, )?; finish_stage("stage5", log_t, &session, &stage5.clear_output); - let stage6a = prove_stage6a::( + let stage6a = prove_stage6a::( backend, &mut session, &mode, @@ -159,7 +157,7 @@ where &mut transcript, )?; finish_stage("stage6a", log_t, &session, &stage6a.clear_output); - let stage6b = prove_stage6b::( + let stage6b = prove_stage6b::( backend, &mut session, &mode, @@ -176,7 +174,7 @@ where &mut transcript, )?; finish_stage("stage6b", log_t, &session, &stage6b.clear_output); - let stage7 = prove_stage7::( + let stage7 = prove_stage7::( backend, &mut session, &mode, @@ -189,14 +187,13 @@ where &mut transcript, )?; finish_stage("stage7", log_t, &session, &stage7.clear_output); - let stage8 = prove_stage8::( + let stage8 = prove_stage8::( backend, &mut session, &checked, config, preprocessing, &stage0.commitments, - stage0.untrusted_advice_commitment.as_ref(), trusted_advice.map(|trusted| &trusted.commitment), stage0.hints, #[cfg(feature = "field-inline")] @@ -208,66 +205,12 @@ where )?; finish_stage("stage8", log_t, &session, &()); - let stages = JoltStageProofs { - stage1_uni_skip_first_round_proof: stage1.uniskip_proof, - stage1_sumcheck_proof: stage1.sumcheck_proof, - stage2_uni_skip_first_round_proof: stage2.uniskip_proof, - stage2_sumcheck_proof: stage2.sumcheck_proof, - stage3_sumcheck_proof: stage3.sumcheck_proof, - stage4_sumcheck_proof: stage4.sumcheck_proof, - stage5_sumcheck_proof: stage5.sumcheck_proof, - stage6a_sumcheck_proof: stage6a.sumcheck_proof, - stage6b_sumcheck_proof: stage6b.sumcheck_proof, - stage7_sumcheck_proof: stage7.sumcheck_proof, - }; - #[cfg(not(feature = "zk"))] - { - Ok(JoltProof { - protocol: JoltProtocolConfig::for_zk(false), - commitments: stage0.commitments, - stages, - joint_opening_proof: stage8.joint_opening_proof, - untrusted_advice_commitment: stage0.untrusted_advice_commitment, - claims: JoltProofClaims::Clear(ClearProofClaims { - stage1: stage1.claims, - stage2: stage2.claims, - stage3: stage3.claims, - stage4: stage4.claims, - stage5: stage5.claims, - stage6a: stage6a.claims, - stage6b: stage6b.claims, - stage7: stage7.claims, - }), - trace_length: config.trace_length, - ram_K: config.ram_K, - rw_config: config.rw_config, - one_hot_config: config.one_hot_config, - trace_polynomial_order: config.trace_polynomial_order, - }) - } + let _ = stage8; #[cfg(feature = "zk")] { use crate::blindfold::{self, ZkFinalOpening, ZkStageWitnesses}; - // The shell: every wire field real, the claims slot a unit - // placeholder the stage replay never reads (claims are not absorbed - // in ZK — the BlindFold proof replaces them after the tail). - let shell = JoltProof:: { - protocol: JoltProtocolConfig::for_zk(true), - commitments: stage0.commitments, - stages, - joint_opening_proof: stage8.joint_opening_proof, - untrusted_advice_commitment: stage0.untrusted_advice_commitment, - claims: JoltProofClaims::Zk { - blindfold_proof: (), - }, - trace_length: config.trace_length, - ram_K: config.ram_K, - rw_config: config.rw_config, - one_hot_config: config.one_hot_config, - trace_polynomial_order: config.trace_polynomial_order, - }; let witnesses = ZkStageWitnesses { stage1_uniskip: stage1.uniskip_witness, stage1: stage1.committed_witness, @@ -284,16 +227,18 @@ where joint_evaluation: stage8.joint_evaluation, evaluation_blind: stage8.evaluation_blind, }; - let blindfold_proof = blindfold::prove_blindfold::( + blindfold::prove_blindfold::( preprocessing, public_io, trusted_advice.map(|trusted| &trusted.commitment), - &shell, &witnesses, &final_opening, - transcript.state(), + &mut transcript, )?; - - Ok(shell.with_claims(JoltProofClaims::Zk { blindfold_proof })) } + + Ok(JoltProof { + protocol: JoltProtocolConfig::for_zk(cfg!(feature = "zk")), + narg: transcript.finish(), + }) } diff --git a/crates/jolt-prover/src/dory/stages/stage0.rs b/crates/jolt-prover/src/dory/stages/stage0.rs index 17637f145d..4b7e01fff9 100644 --- a/crates/jolt-prover/src/dory/stages/stage0.rs +++ b/crates/jolt-prover/src/dory/stages/stage0.rs @@ -2,11 +2,12 @@ //! commitment. //! //! The transcript work is the verifier's own exported code -//! ([`validate_inputs_from_parts`], [`absorb_transcript_preamble`], -//! [`absorb_transcript_commitments`]) — the two sides share the absorb -//! sequence structurally, so stage-0 Fiat-Shamir drift is impossible by -//! construction. The commitment compute is delegated to the `jolt-kernels` -//! witness-commitment kernel; only the absorbs happen here. +//! ([`validate_inputs`], [`ProofHeader::send`], [`absorb_public_preamble`], +//! [`ProofCommitments::send`], [`absorb_public_commitments`]), mirroring the +//! verifier's `seed_transcript` step for step, so the two sides share the +//! stage-0 order structurally. The commitment compute is delegated to the +//! `jolt-kernels` witness-commitment kernel; only the transcript work happens +//! here. use common::jolt_device::JoltDevice; #[cfg(feature = "field-inline")] @@ -19,13 +20,13 @@ use jolt_kernels::reference::bytecode_read_raf::BytecodeReadRafWitness; use jolt_kernels::reference::instruction_read_raf::InstructionReadRafWitness; use jolt_kernels::{CommitmentGrid, JoltBackend, ProofSession, WitnessCommitment}; use jolt_openings::CommitmentScheme; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::proof::JoltCommitments; #[cfg(feature = "field-inline")] use jolt_verifier::proof::{FieldInlineCommitments, FieldRegistersCommitments}; use jolt_verifier::{ - absorb_committed_program_commitments, absorb_transcript_commitments, - absorb_transcript_preamble, validate_inputs_from_parts, CheckedInputs, ProofTranscriptConfig, + absorb_public_commitments, absorb_public_preamble, jolt_protocol_id, validate_inputs, + CheckedInputs, ProofCommitments, ProofHeader, JOLT_SESSION, }; use jolt_witness::{ validate_servable, JoltWitnessOracle, JoltWitnessPlane, RowSource, WitnessBundle, @@ -44,18 +45,17 @@ pub struct TrustedAdviceCommitment { } /// Stage 0's outputs: the validated inputs, the seeded transcript (positioned -/// exactly where the verifier's `verify_until_stage1` leaves its own), the -/// witness commitments in wire form, the untrusted-advice commitment (proved -/// at prove time, carried on the proof), and the per-polynomial opening hints -/// the stage-8 joint opening will consume (advice hints included). -pub struct Stage0Output +/// exactly where the verifier's `seed_transcript` leaves its own), the sent +/// commitments, and the per-polynomial opening hints the stage-8 joint opening +/// will consume (advice hints included). +pub struct Stage0Output where PCS: CommitmentScheme, + H: Sponge, { pub checked: CheckedInputs, - pub transcript: T, - pub commitments: JoltCommitments, - pub untrusted_advice_commitment: Option, + pub transcript: ProverTranscript, + pub commitments: ProofCommitments, pub hints: Vec<(JoltCommittedPolynomial, PCS::OpeningHint)>, /// The field-inline opening hints, id-disjoint from the jolt hints; the /// stage-8 joint opening splices them after `RdInc@IncClaimReduction`. @@ -63,13 +63,12 @@ where pub field_inline_hints: Vec<(FieldInlineCommittedPolynomial, PCS::OpeningHint)>, } -/// Validate inputs, seed the transcript, commit the witness (the untrusted -/// advice polynomial in its own balanced grid), and absorb the commitments -/// (main, untrusted advice, trusted advice, then the preprocessing-held -/// committed-program chunk/image commitments — the verifier's own absorb -/// order). +/// Validate inputs, send the proof header and absorb the public preamble, +/// commit the witness (the untrusted advice polynomial in its own balanced +/// grid), send the commitments, and absorb the public ones (trusted advice, +/// then the preprocessing-held committed-program commitments). #[tracing::instrument(skip_all)] -pub fn prove_stage0( +pub fn prove_stage0( backend: &JoltBackend, session: &mut ProofSession, preprocessing: &JoltProverPreprocessing, @@ -77,13 +76,12 @@ pub fn prove_stage0( trusted_advice: Option<&TrustedAdviceCommitment>, witness: &W, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, - PCS::Output: AppendToTranscript, VC: VectorCommitment, - T: Transcript, + H: Sponge, W: JoltWitnessPlane, { // Committed-program mode needs the prover-retained full program + hints; @@ -118,21 +116,22 @@ where reason: "trusted-advice commitment presence disagrees with the trusted advice bytes", }); } - // The verifier's own input validation doubles as the prover's self-check - // and produces the normalized `CheckedInputs` the preamble absorbs. The - // zk axis is the compiled feature — the co-compiled verifier's - // `SELECTED_ZK_CONFIG` flips with the same feature, so both sides always - // agree. - let checked = validate_inputs_from_parts( + // The verifier's own input validation of the header about to be sent + // doubles as the prover's self-check and produces the normalized + // `CheckedInputs` the preamble absorbs. + let header = ProofHeader { + trace_length: config.trace_length, + ram_K: config.ram_K, + rw_config: config.rw_config, + one_hot_config: config.one_hot_config, + trace_polynomial_order: config.trace_polynomial_order, + untrusted_advice: untrusted_advice_present, + }; + let checked = validate_inputs( &preprocessing.verifier, public_io, - config.trace_length, - config.ram_K, - config.trace_polynomial_order, - config.one_hot_config, + &header, trusted_advice.is_some(), - untrusted_advice_present, - cfg!(feature = "zk"), )?; // The dominant-advice regime (an advice grid wider than every other @@ -160,16 +159,9 @@ where } } - let mut transcript = T::new(b"Jolt"); - absorb_transcript_preamble( - &checked, - ProofTranscriptConfig { - rw_config: config.rw_config, - one_hot_config: config.one_hot_config, - trace_polynomial_order: config.trace_polynomial_order, - }, - &mut transcript, - ); + let mut transcript = ProverTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION); + header.send(&mut transcript); + absorb_public_preamble(&checked, &mut transcript); let ids: Vec = witness .committed_order()? @@ -219,25 +211,22 @@ where &preprocessing.pcs_setup, ) })?; - let (commitments, mut hints) = assemble_commitments::(committed)?; - // The field-inline committed columns follow the base commitments and // precede the advice commitments — the same appended-extension position - // `absorb_transcript_commitments` absorbs them in. + // `ProofCommitments::send` sends them in. #[cfg(feature = "field-inline")] - let (commitments, field_inline_hints) = { - let (field_inline, field_inline_hints) = commit_field_inline::( - backend, - session, - witness as &dyn JoltWitnessPlane, - grid, - &preprocessing.pcs_setup, - )?; - ( - commitments.with_field_inline(field_inline), - field_inline_hints, - ) - }; + let (field_inline, field_inline_hints) = commit_field_inline::( + backend, + session, + witness as &dyn JoltWitnessPlane, + grid, + &preprocessing.pcs_setup, + )?; + let (trace, mut hints) = assemble_commitments::( + committed, + #[cfg(feature = "field-inline")] + field_inline, + )?; // The untrusted advice polynomial is committed at prove time in its OWN // balanced grid (its variable count comes from the memory layout's maximum @@ -295,25 +284,21 @@ where )); } - absorb_transcript_commitments( - &commitments, - untrusted_advice_commitment.as_ref(), + let commitments = ProofCommitments { + trace, + untrusted_advice: untrusted_advice_commitment, + }; + commitments.send::(&mut transcript); + absorb_public_commitments( + &preprocessing.verifier, trusted_advice.map(|trusted| &trusted.commitment), &mut transcript, ); - if let Some(committed) = preprocessing.verifier.program.committed() { - absorb_committed_program_commitments( - &committed.bytecode_chunk_commitments, - &committed.program_image_commitment, - &mut transcript, - ); - } Ok(Stage0Output { checked, transcript, commitments, - untrusted_advice_commitment, hints, #[cfg(feature = "field-inline")] field_inline_hints, @@ -389,6 +374,7 @@ where )] fn assemble_commitments( committed: Vec>, + #[cfg(feature = "field-inline")] field_inline: FieldInlineCommitments, ) -> Result< ( JoltCommitments, @@ -436,7 +422,15 @@ fn assemble_commitments( }); }; Ok(( - JoltCommitments::new(rd_inc, ram_inc, instruction, ram, bytecode), + JoltCommitments { + rd_inc, + ram_inc, + instruction_ra: instruction, + ram_ra: ram, + bytecode_ra: bytecode, + #[cfg(feature = "field-inline")] + field_inline, + }, hints, )) } diff --git a/crates/jolt-prover/src/dory/stages/stage8.rs b/crates/jolt-prover/src/dory/stages/stage8.rs index 64b34c4dab..95eba342ee 100644 --- a/crates/jolt-prover/src/dory/stages/stage8.rs +++ b/crates/jolt-prover/src/dory/stages/stage8.rs @@ -1,5 +1,5 @@ //! Stage 8: the joint batched opening — no sumcheck, one homomorphic PCS -//! batch over every committed polynomial. +//! batch over every committed polynomial, written to the transcript. //! //! Pure orchestration mirroring `stage8::verify`: the batch entries come from //! the verifier's promoted `batch_entries` assembly (the prover passes its @@ -48,8 +48,8 @@ use jolt_openings::{ #[cfg(feature = "field-inline")] use jolt_poly::MultilinearPoly; use jolt_poly::Point; -use jolt_transcript::Transcript; -use jolt_verifier::proof::JoltCommitments; +use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_verifier::proof::ProofCommitments; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::outputs::Stage7ClearOutput; use jolt_verifier::stages::stage8::{batch_entries, precommitted_final_openings}; @@ -58,29 +58,31 @@ use jolt_witness::JoltWitnessPlane; use crate::{CommittedProgramCandidates, JoltProverPreprocessing, ProverConfig, ProverError}; -/// Stage 8's output: the joint PCS opening proof (the last wire component of -/// a clear proof), plus — in ZK builds — the joint evaluation and its blind, -/// the secrets inside the hiding evaluation commitment that the BlindFold -/// final-opening binding opens. +/// Stage 8's ZK output: the joint evaluation and its blind, the secrets inside +/// the hiding evaluation commitment that the BlindFold final-opening binding +/// opens. The opening itself is written to the transcript. +#[cfg(feature = "zk")] pub struct Stage8ProverOutput { - pub joint_opening_proof: PCS::Proof, - #[cfg(feature = "zk")] pub joint_evaluation: PCS::Field, - #[cfg(feature = "zk")] pub evaluation_blind: PCS::Field, } +/// Stage 8's clear output: the opening is written to the transcript. +#[cfg(not(feature = "zk"))] +pub struct Stage8ProverOutput { + _pcs: core::marker::PhantomData, +} + /// Prove stage 8 on `transcript` (positioned at the stage-7 boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage8( +pub fn prove_stage8( backend: &JoltBackend, session: &mut ProofSession, checked: &CheckedInputs, config: &ProverConfig, preprocessing: &JoltProverPreprocessing, - commitments: &JoltCommitments, - untrusted_advice_commitment: Option<&PCS::Output>, + commitments: &ProofCommitments, trusted_advice_commitment: Option<&PCS::Output>, hints: impl Into>, #[cfg(feature = "field-inline")] field_inline_hints: Vec<( @@ -90,14 +92,14 @@ pub fn prove_stage8( stage6b: &Stage6bClearOutput, stage7: &Stage7ClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, PCS::Output: HomomorphicCommitment, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_t = checked.trace_length.ilog2() as usize; let precommitted = &checked.precommitted; @@ -148,8 +150,8 @@ where let entries = batch_entries::( &preprocessing.verifier, - commitments, - untrusted_advice_commitment, + &commitments.trace, + commitments.untrusted_advice.as_ref(), layout, config.trace_polynomial_order, trusted_advice_commitment, @@ -166,7 +168,7 @@ where let mut entries = entries; jolt_verifier::stages::stage8::field_inline::splice_final_opening( &mut entries, - commitments, + &commitments.trace, config.trace_polynomial_order, &opening_point, stage6b.output_points.field_registers_inc_opening_point(), @@ -323,7 +325,7 @@ where // joint evaluation and blind for BlindFold. #[cfg(not(feature = "zk"))] { - let joint_opening_proof = HomomorphicBatch::::prove_batch( + HomomorphicBatch::::prove_batch( &preprocessing.pcs_setup, statement, polynomials.iter().map(|poly| &**poly).collect(), @@ -333,7 +335,7 @@ where .map_err(KernelError::::from)?; Ok(Stage8ProverOutput { - joint_opening_proof, + _pcs: core::marker::PhantomData, }) } #[cfg(feature = "zk")] @@ -358,7 +360,6 @@ where .map_err(KernelError::::from)?; Ok(Stage8ProverOutput { - joint_opening_proof: opening.proof, joint_evaluation: opening.joint_evaluation, evaluation_blind: opening.blind, }) diff --git a/crates/jolt-prover/src/driver.rs b/crates/jolt-prover/src/driver.rs index b5d13b2012..a52c3bbe2b 100644 --- a/crates/jolt-prover/src/driver.rs +++ b/crates/jolt-prover/src/driver.rs @@ -29,10 +29,8 @@ use jolt_kernels::{ PrepareKernel, ProofSession, ProverInputs, SumcheckKernel, SumcheckKernelError, }; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::{ - ProveRounds, RecordedSumcheck, RoundScheduler, SumcheckError, SumcheckRecorder, -}; -use jolt_transcript::Transcript; +use jolt_sumcheck::{ProveRounds, RoundScheduler, SumcheckError, SumcheckRecorder}; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::relations::{ ConcreteSumcheck, ConcreteSumcheckChallenges, SumcheckInputClaims, SumcheckInputPoints, SumcheckOutputClaims, SumcheckOutputPoints, @@ -58,13 +56,13 @@ pub trait StageProver: Sized { /// Prove this stage's batch on `transcript`, recorder-generically: the /// recorder type decides clear vs. committed recording, never a runtime - /// flag. Returns the [`Proved`] carrier (recorded proof, typed output + /// flag. Returns the [`Proved`] carrier (recorder witness, typed output /// claims, derived points, hard-checked final claim). #[expect( clippy::too_many_arguments, reason = "the driver's fixed protocol signature: upstream carriers in, recorded proof out" )] - fn prove( + fn prove( &self, kernels: &B, session: &mut ProofSession, @@ -74,14 +72,14 @@ pub trait StageProver: Sized { input_points: &Self::InputPoints, challenges: &Self::Challenges, recorder: Rec, - transcript: &mut T, - ) -> Result, ProverError> + transcript: &mut ProverTranscript, + ) -> Result, ProverError> where B: KernelSource + ?Sized, Rec: SumcheckRecorder, - T: Transcript; + H: Sponge; - /// The stage's absorbed opening scalars, in the stage's curated order + /// The stage's sent (clear) or committed (ZK) opening scalars, in the stage's curated order /// (stage 6b's runtime point dedup reorders the returned values). The /// default emitted by `impl_stage_prover!` returns the derive-generated /// canonical order (`opening_values`); curated stages supply an override @@ -113,12 +111,12 @@ pub trait KernelSource> { } /// A proved stage batch, as assembled by the generated -/// [`prove`](StageProver::prove): the recorded wire proof (plus retained -/// witness for a committed recorder), the typed output claims and derived -/// opening points, and the batch's final running claim (already hard-checked -/// against the generated `expected_final_claim`). -pub struct Proved, C> { - pub recorded: RecordedSumcheck, +/// [`prove`](StageProver::prove): the recorder's retained witness (nothing for +/// a clear recorder), the typed output claims and derived opening points, and +/// the batch's final running claim (already hard-checked against the generated +/// `expected_final_claim`). +pub struct Proved, W> { + pub witness: W, pub output_claims: S::OutputClaims, pub output_points: S::OutputPoints, pub final_claim: F, @@ -479,7 +477,7 @@ macro_rules! impl_stage_prover { type OutputPoints = $output_points; type Kernels = ( $($crate::driver::__stage_member!(kernel_ty $presence $relation),)+ ); - fn prove( + fn prove( &self, kernels: &B, session: &mut ::jolt_kernels::ProofSession, @@ -489,15 +487,15 @@ macro_rules! impl_stage_prover { input_points: &Self::InputPoints, challenges: &Self::Challenges, mut recorder: Rec, - transcript: &mut T, + transcript: &mut ::jolt_transcript::ProverTranscript, ) -> ::core::result::Result< - $crate::driver::Proved, + $crate::driver::Proved, $crate::ProverError, > where B: $crate::driver::KernelSource + ?Sized, Rec: ::jolt_sumcheck::SumcheckRecorder, - T: ::jolt_transcript::Transcript, + H: ::jolt_transcript::Sponge, { let __stage_span = ::tracing::info_span!(concat!($label, "::prove")).entered(); @@ -576,9 +574,9 @@ macro_rules! impl_stage_prover { ); } - let __recorded = recorder.finish(&__opening_values, transcript)?; + let __witness = recorder.finish(&__opening_values, transcript)?; ::core::result::Result::Ok($crate::driver::Proved { - recorded: __recorded, + witness: __witness, output_claims: __output_claims, output_points: __output_points, final_claim: __proved.final_claim, diff --git a/crates/jolt-prover/src/profile.rs b/crates/jolt-prover/src/profile.rs index 9b4def87bb..38159ff915 100644 --- a/crates/jolt-prover/src/profile.rs +++ b/crates/jolt-prover/src/profile.rs @@ -74,7 +74,7 @@ type FieldInlineField = Fr; #[cfg(all(feature = "field-inline", feature = "akita"))] type FieldInlineField = AkitaField; #[cfg(not(feature = "akita"))] -use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; +use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; #[cfg(not(feature = "akita"))] use rayon::ThreadPoolBuilder; @@ -839,7 +839,7 @@ fn prove_workload( // `jolt_prover::prove` root span covers exactly this interval; the // Instant is the `--format none` no-subscriber baseline. let now = Instant::now(); - let proof = crate::dory::prove::, Blake2bTranscript, _>( + let proof = crate::dory::prove::, JoltSponge, _>( &backend, &prover_preprocessing, &config, @@ -866,7 +866,7 @@ fn prove_workload( .build() .expect("single-threaded verifier pool must build"); let verify = || { - jolt_verifier::verify::, Blake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( &prover_preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-prover/src/recorder.rs b/crates/jolt-prover/src/recorder.rs index 076fafbea9..b91c6c74c7 100644 --- a/crates/jolt-prover/src/recorder.rs +++ b/crates/jolt-prover/src/recorder.rs @@ -14,30 +14,25 @@ use core::marker::PhantomData; use jolt_crypto::VectorCommitment; use jolt_field::JoltField; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::SumcheckProof; #[cfg(not(feature = "zk"))] use jolt_sumcheck::{prove_uniskip_clear, ClearSumcheckRecorder}; #[cfg(feature = "zk")] -use jolt_sumcheck::{ - prove_uniskip_committed, CommittedSumcheckRecorder, CommittedSumcheckWitness, RecordedSumcheck, -}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_sumcheck::{prove_uniskip_committed, CommittedSumcheckRecorder, CommittedSumcheckWitness}; +use jolt_transcript::{ProverTranscript, Sponge}; use crate::ProverError; -/// The compiled mode's batch recorder type; `Commitment = VC::Output` in both -/// modes, so everything downstream of the recorder is mode-independent. +/// The compiled mode's batch recorder type. #[cfg(feature = "zk")] -pub type ModeRecorder<'a, F, VC> = CommittedSumcheckRecorder<'a, F, VC, rand_core::OsRng>; +pub type ModeRecorder<'a, VC> = + CommittedSumcheckRecorder<'a, ::Field, VC, rand_core::OsRng>; #[cfg(not(feature = "zk"))] -pub type ModeRecorder<'a, F, VC> = - ClearSumcheckRecorder::Output>; +pub type ModeRecorder<'a, VC> = ClearSumcheckRecorder<::Field>; -/// A proved uni-skip round in the compiled mode: the wire proof, the -/// reduction challenge, and the prover-internal output claim (absorbed by the -/// clear arm, committed and retained by the ZK arm). -pub struct ProvedUniskipMode { - pub proof: SumcheckProof, +/// A proved uni-skip round in the compiled mode: the reduction challenge and +/// the output claim (sent by the clear arm, committed and retained by the ZK +/// arm). +pub struct ProvedUniskipMode { pub challenge: F, pub output_claim: F, #[cfg(feature = "zk")] @@ -73,7 +68,7 @@ impl<'a, VC: VectorCommitment> ProofMode<'a, VC> { } /// A fresh batch recorder for one stage. - pub fn recorder(&self) -> Result, ProverError> { + pub fn recorder(&self) -> Result, ProverError> { #[cfg(feature = "zk")] { Ok(CommittedSumcheckRecorder::new( @@ -88,28 +83,20 @@ impl<'a, VC: VectorCommitment> ProofMode<'a, VC> { } } - /// Prove a uni-skip first round in the compiled mode. The clear arm - /// absorbs the full labeled polynomial and the output claim; the ZK arm - /// commits the coefficients and the output claim and retains the witness. - #[expect( - clippy::type_complexity, - reason = "the associated-type projections spell out one small struct" - )] - pub fn prove_uniskip( + /// Prove a uni-skip first round in the compiled mode. The clear arm sends + /// the full polynomial and the output claim; the ZK arm commits the + /// coefficients and the output claim and retains the witness. + pub fn prove_uniskip( &self, round_poly: UnivariatePoly, input_claim: VC::Field, degree: usize, domain_size: usize, - transcript: &mut T, - ) -> Result, ProverError> - where - VC::Output: Clone + AppendToTranscript, - T: Transcript, - { + transcript: &mut ProverTranscript, + ) -> Result, ProverError> { #[cfg(feature = "zk")] { - let proved = prove_uniskip_committed::( + let proved = prove_uniskip_committed::( round_poly, input_claim, degree, @@ -119,7 +106,6 @@ impl<'a, VC: VectorCommitment> ProofMode<'a, VC> { transcript, )?; Ok(ProvedUniskipMode { - proof: proved.proof, challenge: proved.challenge, output_claim: proved.output_claim, witness: proved.witness, @@ -128,7 +114,7 @@ impl<'a, VC: VectorCommitment> ProofMode<'a, VC> { #[cfg(not(feature = "zk"))] { let _ = self; - let proved = prove_uniskip_clear::( + let proved = prove_uniskip_clear::( round_poly, input_claim, degree, @@ -136,28 +122,9 @@ impl<'a, VC: VectorCommitment> ProofMode<'a, VC> { transcript, )?; Ok(ProvedUniskipMode { - proof: proved.proof, challenge: proved.challenge, output_claim: proved.output_claim, }) } } } - -/// Split a recorded sumcheck into its wire proof and the ZK-retained -/// witness; an absent witness is a recorder-contract violation in a ZK build. -#[cfg(feature = "zk")] -#[expect( - clippy::type_complexity, - reason = "the pair is the two halves of RecordedSumcheck, nothing more" -)] -pub(crate) fn split_recorded( - recorded: RecordedSumcheck, -) -> Result<(SumcheckProof, CommittedSumcheckWitness), ProverError> { - let witness = recorded - .committed_witness - .ok_or(ProverError::InvariantViolation { - reason: "the committed recorder retained no witness", - })?; - Ok((recorded.proof, witness)) -} diff --git a/crates/jolt-prover/src/stages/drivers.rs b/crates/jolt-prover/src/stages/drivers.rs index deb096fe34..cc0b3e2d47 100644 --- a/crates/jolt-prover/src/stages/drivers.rs +++ b/crates/jolt-prover/src/stages/drivers.rs @@ -60,10 +60,18 @@ mod stage4 { use crate::driver::impl_stage_prover; - // Stage 4's `no_opening_values` replacement keeps the generated - // signature (the claims aggregate's hand-ordered `opening_values`, which - // splices the field-inline openings under `field-inline`), so the driver's default - // curation serves both feature arms unchanged. + // A clear proof sent the staged openings before the batch, so only the + // post-round openings follow the rounds. + #[cfg(not(feature = "zk"))] + jolt_verifier::stage4_sumchecks_members!(impl_stage_prover + curate = |batch, claims, _points| { + Ok(batch.post_round_opening_values(claims)) + }, + ); + // A committed proof commits every opening in the claims aggregate's + // canonical order (the `no_opening_values` replacement keeps the generated + // signature, so the driver's default curation serves it). + #[cfg(feature = "zk")] jolt_verifier::stage4_sumchecks_members!(impl_stage_prover); } diff --git a/crates/jolt-prover/src/stages/field_inline_fixtures.rs b/crates/jolt-prover/src/stages/field_inline_fixtures.rs index 6471ddfdbf..8bc621ba44 100644 --- a/crates/jolt-prover/src/stages/field_inline_fixtures.rs +++ b/crates/jolt-prover/src/stages/field_inline_fixtures.rs @@ -528,7 +528,7 @@ pub(crate) mod twins { /// positions the transcript at the stage-2 boundary. pub(crate) fn replay_stage1( transcript: &mut Blake2bTranscript, - stage1: &Stage1ProverOutput, + stage1: &Stage1ProverOutput, ) { let tau = draw_spartan_outer_tau(transcript, LOG_T); let uniskip_challenge = uniskip::verify_clear( @@ -573,8 +573,8 @@ pub(crate) mod twins { transcript: &mut Blake2bTranscript, config: &ProverConfig, public_io: &JoltDevice, - stage1: &Stage1ProverOutput, - stage2: &Stage2ProverOutput, + stage1: &Stage1ProverOutput, + stage2: &Stage2ProverOutput, ) { let log_t = LOG_T; let log_k = config.ram_K.ilog2() as usize; @@ -657,9 +657,9 @@ pub(crate) mod twins { /// member): positions the transcript at the stage-4 boundary. pub(crate) fn replay_stage3( transcript: &mut Blake2bTranscript, - stage1: &Stage1ProverOutput, - stage2: &Stage2ProverOutput, - stage3: &Stage3ProverOutput, + stage1: &Stage1ProverOutput, + stage2: &Stage2ProverOutput, + stage3: &Stage3ProverOutput, ) { let dimensions = TraceDimensions::new(LOG_T); let tau_low = stage2.clear_output.product_tau_low.clone(); @@ -704,9 +704,9 @@ pub(crate) mod twins { config: &ProverConfig, checked: &CheckedInputs, preprocessing: &FixturePreprocessing, - stage2: &Stage2ProverOutput, - stage3: &Stage3ProverOutput, - stage4: &Stage4ProverOutput, + stage2: &Stage2ProverOutput, + stage3: &Stage3ProverOutput, + stage4: &Stage4ProverOutput, ) { let register_dimensions = config .rw_config @@ -768,9 +768,9 @@ pub(crate) mod twins { config: &ProverConfig, checked: &CheckedInputs, preprocessing: &FixturePreprocessing, - stage2: &Stage2ProverOutput, - stage4: &Stage4ProverOutput, - stage5: &Stage5ProverOutput, + stage2: &Stage2ProverOutput, + stage4: &Stage4ProverOutput, + stage5: &Stage5ProverOutput, ) { let formula_dimensions = crate::stages::formula_dimensions( checked, @@ -820,12 +820,12 @@ pub(crate) mod twins { config: &ProverConfig, checked: &CheckedInputs, preprocessing: &FixturePreprocessing, - stage1: &Stage1ProverOutput, - stage2: &Stage2ProverOutput, - stage3: &Stage3ProverOutput, - stage4: &Stage4ProverOutput, - stage5: &Stage5ProverOutput, - stage6a: &Stage6aProverOutput, + stage1: &Stage1ProverOutput, + stage2: &Stage2ProverOutput, + stage3: &Stage3ProverOutput, + stage4: &Stage4ProverOutput, + stage5: &Stage5ProverOutput, + stage6a: &Stage6aProverOutput, ) { let formula_dimensions = crate::stages::formula_dimensions( checked, @@ -919,13 +919,13 @@ pub(crate) mod proving { use jolt_witness::JoltWitnessPlane; type Commitment = Bn254G1; type Stages3 = ( - Stage1ProverOutput, - Stage2ProverOutput, - Stage3ProverOutput, + Stage1ProverOutput, + Stage2ProverOutput, + Stage3ProverOutput, ); - type Stages4 = (Stages3, Stage4ProverOutput); - type Stages5 = (Stages4, Stage5ProverOutput); - type Stages6a = (Stages5, Stage6aProverOutput); + type Stages4 = (Stages3, Stage4ProverOutput); + type Stages5 = (Stages4, Stage5ProverOutput); + type Stages6a = (Stages5, Stage6aProverOutput); pub(crate) struct FixtureProver<'a> { pub(crate) backend: &'a JoltBackend, diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index cb8293550e..239d2713a4 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -17,8 +17,7 @@ use jolt_r1cs::constraints::jolt::{ }; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::stage1::outer_remainder::{ outer_remainder_input_values_from_uniskip_output, OuterRemainder, }; @@ -33,9 +32,7 @@ use crate::{ProverError, StageProver as _}; /// Stage 1's outputs: the two wire proofs, the wire claims, and the /// verifier-typed cross-stage carrier downstream stages consume. -pub struct Stage1ProverOutput { - pub uniskip_proof: SumcheckProof, - pub sumcheck_proof: SumcheckProof, +pub struct Stage1ProverOutput { pub claims: Stage1OutputClaims, pub clear_output: Stage1ClearOutput, #[cfg(feature = "zk")] @@ -46,19 +43,19 @@ pub struct Stage1ProverOutput { /// Prove stage 1 on `transcript` (positioned at the stage-0 boundary). #[tracing::instrument(skip_all)] -pub fn prove_stage1( +pub fn prove_stage1( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, log_t: usize, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let tau = draw_spartan_outer_tau(transcript, log_t); // Backend-neutral kernel-seam spans at the call boundary, so every @@ -115,15 +112,11 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; let claims = Stage1OutputClaims::new(proved_uniskip.output_claim, proved.output_claims.clone()); let clear_output = Stage1ClearOutput::new(proved.output_claims, proved.output_points); Ok(Stage1ProverOutput { - uniskip_proof: proved_uniskip.proof, - sumcheck_proof, claims, clear_output, #[cfg(feature = "zk")] diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index 77fd8192b8..006e186d4f 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -26,8 +26,7 @@ use jolt_openings::CommitmentScheme; use jolt_program::preprocess::PublicIoMemory; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::relations::ConcreteSumcheck; use jolt_verifier::stages::stage1::Stage1ClearOutput; #[cfg(feature = "field-inline")] @@ -53,9 +52,7 @@ use crate::{ProverConfig, ProverError, StageProver as _}; /// Stage 2's outputs: the two wire proofs, the wire claims, and the /// verifier-typed cross-stage carrier downstream stages consume. -pub struct Stage2ProverOutput { - pub uniskip_proof: SumcheckProof, - pub sumcheck_proof: SumcheckProof, +pub struct Stage2ProverOutput { pub claims: Stage2OutputClaims, pub clear_output: Stage2ClearOutput, #[cfg(feature = "zk")] @@ -67,7 +64,7 @@ pub struct Stage2ProverOutput { /// Prove stage 2 on `transcript` (positioned at the stage-1 boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage2( +pub fn prove_stage2( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -75,13 +72,13 @@ pub fn prove_stage2( public_io: &JoltDevice, stage1: &Stage1ClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_t = config.trace_length.ilog2() as usize; let log_k = config.ram_K.ilog2() as usize; @@ -187,14 +184,10 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; let claims = Stage2OutputClaims::new(proved_uniskip.output_claim, proved.output_claims.clone()); Ok(Stage2ProverOutput { - uniskip_proof: proved_uniskip.proof, - sumcheck_proof, claims, clear_output: Stage2ClearOutput { output_values: proved.output_claims, diff --git a/crates/jolt-prover/src/stages/stage3.rs b/crates/jolt-prover/src/stages/stage3.rs index c57404b46c..ed37decd92 100644 --- a/crates/jolt-prover/src/stages/stage3.rs +++ b/crates/jolt-prover/src/stages/stage3.rs @@ -15,8 +15,7 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::{ @@ -32,8 +31,7 @@ use crate::{ProverConfig, ProverError, StageProver as _}; /// Stage 3's outputs: the wire proof, the wire claims (the raw batch /// aggregate — no uni-skip wrapper), and the verifier-typed cross-stage /// carrier downstream stages consume. -pub struct Stage3ProverOutput { - pub sumcheck_proof: SumcheckProof, +pub struct Stage3ProverOutput { pub claims: Stage3OutputClaims, pub clear_output: Stage3ClearOutput, #[cfg(feature = "zk")] @@ -43,7 +41,7 @@ pub struct Stage3ProverOutput { /// Prove stage 3 on `transcript` (positioned at the stage-2 boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage3( +pub fn prove_stage3( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -51,13 +49,13 @@ pub fn prove_stage3( stage1: &Stage1ClearOutput, stage2: &Stage2ClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_t = config.trace_length.ilog2() as usize; let trace_dimensions = TraceDimensions::new(log_t); @@ -91,12 +89,9 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; Ok(Stage3ProverOutput { - sumcheck_proof, claims: proved.output_claims.clone(), clear_output: Stage3ClearOutput { output_values: proved.output_claims, diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index e82fd6c66b..113ce1f1d5 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -6,10 +6,10 @@ //! with the verifier's own promoted helpers; the private opening VALUES //! are evaluated through the backend as one batch (program image and advice, //! staged transcript-silently before the RAM -//! value-check gamma draw). The stage's one curated behavior: the batch -//! carries `no_opening_values`, so the final absorbs use the claims struct's -//! hand-ordered `opening_values()` (staged advice/program-image openings -//! first, then registers, then RAM). +//! value-check gamma draw). The stage's one curated behavior: a clear proof +//! sends the staged advice/program-image openings after the gamma draws and +//! the register and RAM openings after the rounds, while a committed proof +//! commits all of them in the claims struct's canonical order. use jolt_claims::protocols::jolt::geometry::dimensions::REGISTER_ADDRESS_BITS; use jolt_claims::protocols::jolt::{JoltRelationId, TraceDimensions}; @@ -20,8 +20,7 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; #[cfg(feature = "field-inline")] use jolt_verifier::config::JOLT_VERIFIER_CONFIG; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; @@ -46,8 +45,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 4's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier downstream stages consume. -pub struct Stage4ProverOutput { - pub sumcheck_proof: SumcheckProof, +pub struct Stage4ProverOutput { pub claims: Stage4OutputClaims, pub clear_output: Stage4ClearOutput, #[cfg(feature = "zk")] @@ -57,7 +55,7 @@ pub struct Stage4ProverOutput { /// Prove stage 4 on `transcript` (positioned at the stage-3 boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage4( +pub fn prove_stage4( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -67,13 +65,13 @@ pub fn prove_stage4( stage2: &Stage2ClearOutput, stage3: &Stage3ClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_t = checked.trace_length.ilog2() as usize; let log_k = checked.ram_K.ilog2() as usize; @@ -188,6 +186,11 @@ where // then the RAM value-check gamma behind its `b"ram_val_check_gamma"` domain // separator (replayed by the relation's `draw_challenges` override). let challenges = sumchecks.draw_challenges(transcript)?; + // The RAM value-check input claim consumes the staged openings, so a clear + // proof sends them before the batch; a committed proof carries them in its + // output-claim rows instead. + #[cfg(not(feature = "zk"))] + transcript.send_all(&ram_val_check_init.staged_openings().values()); let inputs = stage4_input_values_from_upstream( &stage2.output_values, @@ -200,11 +203,10 @@ where &init_structure, ); - // No curation hook: the staged advice/program-image openings ride in from - // the RAM value-check kernel (captured off its own consumed input claims - // at prepare), and the stage's `no_opening_values` absorb order is the - // batch's hand-written `opening_values` replacement (staged openings - // first, then registers, then RAM) — the driver's default curation. + // The staged advice/program-image openings ride in from the RAM + // value-check kernel (captured off its own consumed input claims at + // prepare). The driver's curation sends the post-round openings in a clear + // build and commits the full canonical order in a ZK build. let mut scheduler = backend.round_scheduler.build(session); let proved = sumchecks.prove( backend, @@ -218,12 +220,9 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; Ok(Stage4ProverOutput { - sumcheck_proof, claims: proved.output_claims.clone(), clear_output: Stage4ClearOutput { output_values: proved.output_claims, diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index 03f49f3863..4b29f2076c 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -20,8 +20,7 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage4::outputs::Stage4ClearOutput; #[cfg(feature = "field-inline")] @@ -43,8 +42,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 5's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier downstream stages consume. -pub struct Stage5ProverOutput { - pub sumcheck_proof: SumcheckProof, +pub struct Stage5ProverOutput { pub claims: Stage5OutputClaims, pub clear_output: Stage5ClearOutput, #[cfg(feature = "zk")] @@ -54,7 +52,7 @@ pub struct Stage5ProverOutput { /// Prove stage 5 on `transcript` (positioned at the stage-4 boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage5( +pub fn prove_stage5( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -64,13 +62,13 @@ pub fn prove_stage5( stage2: &Stage2ClearOutput, stage4: &Stage4ClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_k = checked.ram_K.ilog2() as usize; let formula_dimensions = super::formula_dimensions( @@ -112,13 +110,10 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; let instruction_r_address = proved.output_points.instruction_r_address(); Ok(Stage5ProverOutput { - sumcheck_proof, claims: proved.output_claims.clone(), clear_output: Stage5ClearOutput { challenges, diff --git a/crates/jolt-prover/src/stages/stage6a.rs b/crates/jolt-prover/src/stages/stage6a.rs index 90c6aaec17..00a7527f41 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -24,8 +24,7 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::Stage3ClearOutput; @@ -48,8 +47,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 6a's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier stage 6b consumes. -pub struct Stage6aProverOutput { - pub sumcheck_proof: SumcheckProof, +pub struct Stage6aProverOutput { pub claims: Stage6aOutputClaims, pub clear_output: Stage6aClearOutput, #[cfg(feature = "zk")] @@ -59,7 +57,7 @@ pub struct Stage6aProverOutput { /// Prove stage 6a on `transcript` (positioned at the stage-5 boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage6a( +pub fn prove_stage6a( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -72,13 +70,13 @@ pub fn prove_stage6a( stage4: &Stage4ClearOutput, stage5: &Stage5ClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let formula_dimensions = super::formula_dimensions( checked, @@ -169,12 +167,9 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; Ok(Stage6aProverOutput { - sumcheck_proof, claims: proved.output_claims.clone(), clear_output: Stage6aClearOutput { output_values: proved.output_claims, diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index e69db4b0a0..d3d530e511 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -29,8 +29,7 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::Stage3ClearOutput; @@ -56,8 +55,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// cross-stage carrier stage 7 consumes. The precommitted reduction state /// that spans into stage 7's address phase travels as `ProofSession` carries, /// not output fields. -pub struct Stage6bProverOutput { - pub sumcheck_proof: SumcheckProof, +pub struct Stage6bProverOutput { pub claims: Stage6bOutputClaims, pub clear_output: Stage6bClearOutput, #[cfg(feature = "zk")] @@ -67,7 +65,7 @@ pub struct Stage6bProverOutput { /// Prove stage 6b on `transcript` (positioned at the stage-6a boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage6b( +pub fn prove_stage6b( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -81,13 +79,13 @@ pub fn prove_stage6b( stage5: &Stage5ClearOutput, stage6a: &Stage6aClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_k = checked.ram_K.ilog2() as usize; let precommitted = &checked.precommitted; @@ -188,12 +186,9 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; Ok(Stage6bProverOutput { - sumcheck_proof, claims: proved.output_claims.clone(), clear_output: Stage6bClearOutput { output_values: proved.output_claims, diff --git a/crates/jolt-prover/src/stages/stage7.rs b/crates/jolt-prover/src/stages/stage7.rs index af82e1015b..dde3395e82 100644 --- a/crates/jolt-prover/src/stages/stage7.rs +++ b/crates/jolt-prover/src/stages/stage7.rs @@ -18,8 +18,7 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::stages::stage4::Stage4ClearOutput; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::hamming_weight_claim_reduction::hamming_weight_claim_reduction_dimensions; @@ -33,8 +32,7 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _ /// Stage 7's outputs: the wire proof, the wire claims, and the verifier-typed /// cross-stage carrier stage 8 consumes. -pub struct Stage7ProverOutput { - pub sumcheck_proof: SumcheckProof, +pub struct Stage7ProverOutput { pub claims: Stage7OutputClaims, pub clear_output: Stage7ClearOutput, #[cfg(feature = "zk")] @@ -44,7 +42,7 @@ pub struct Stage7ProverOutput { /// Prove stage 7 on `transcript` (positioned at the stage-6b boundary). #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage7( +pub fn prove_stage7( backend: &JoltBackend, session: &mut ProofSession, mode: &ProofMode<'_, VC>, @@ -54,13 +52,13 @@ pub fn prove_stage7( stage4: &Stage4ClearOutput, stage6b: &Stage6bClearOutput, witness: &dyn JoltWitnessPlane, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let precommitted = &checked.precommitted; let formula_dimensions = super::formula_dimensions( @@ -98,12 +96,9 @@ where transcript, )?; #[cfg(feature = "zk")] - let (sumcheck_proof, committed_witness) = crate::recorder::split_recorded(proved.recorded)?; - #[cfg(not(feature = "zk"))] - let sumcheck_proof = proved.recorded.proof; + let committed_witness = proved.witness; Ok(Stage7ProverOutput { - sumcheck_proof, claims: proved.output_claims.clone(), clear_output: Stage7ClearOutput { output_values: proved.output_claims, diff --git a/crates/jolt-prover/tests/e2e_matrix.rs b/crates/jolt-prover/tests/e2e_matrix.rs index 62ac31f68f..193c17c7eb 100644 --- a/crates/jolt-prover/tests/e2e_matrix.rs +++ b/crates/jolt-prover/tests/e2e_matrix.rs @@ -229,7 +229,7 @@ mod matrix { use jolt_field::Fr; use jolt_program::execution::OwnedTrace; use jolt_prover::{dory, JoltBackend, JoltSharedPreprocessing, ProverConfig}; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use crate::support::{self, GuestCase}; @@ -278,7 +278,7 @@ mod matrix { .include_trusted_advice(trusted.is_some()), JoltVmWitnessInputs::new(&prepared.program, &program_preprocessing, prepared.trace), ); - let proof = dory::prove::, Blake2bTranscript, _>( + let proof = dory::prove::, JoltSponge, _>( &JoltBackend::optimized(), &preprocessing, &config, @@ -287,7 +287,7 @@ mod matrix { &public_io, ) .expect("Dory proof"); - jolt_verifier::verify::, Blake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( &preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-prover/tests/support/field_inline/dory.rs b/crates/jolt-prover/tests/support/field_inline/dory.rs index 0a79028272..b6e6e02086 100644 --- a/crates/jolt-prover/tests/support/field_inline/dory.rs +++ b/crates/jolt-prover/tests/support/field_inline/dory.rs @@ -5,14 +5,14 @@ use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; use jolt_prover::{dory, JoltBackend, JoltSharedPreprocessing, ProverConfig}; -use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; use jolt_verifier::proof::JoltProof; +use jolt_verifier::JoltSponge; use jolt_verifier::{JoltVerifierPreprocessing, VerifierError}; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use crate::support::{self, GuestCase, PreparedGuest}; -pub type Proof = JoltProof>; +pub type Proof = JoltProof; pub type VerifierPreprocessing = JoltVerifierPreprocessing>; pub fn prove( @@ -51,7 +51,7 @@ pub fn prove( ) .with_field_inline() .expect("field-inline witness view"); - let proof = dory::prove::, Blake2bTranscript, _>( + let proof = dory::prove::, JoltSponge, _>( &backend, &preprocessing, &config, @@ -68,7 +68,7 @@ pub fn verify_full( public_io: &JoltDevice, proof: &Proof, ) -> Result<(), VerifierError> { - jolt_verifier::verify::, Blake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( preprocessing, public_io, proof, From de6051ced316dc601e1737397aa7a3f987bd927f Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:58:16 -0400 Subject: [PATCH 15/62] test(prover): verify the driver, engine, and ZK suites from the argument 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. --- crates/jolt-prover/src/stages/drivers.rs | 81 +++++++++------ crates/jolt-prover/tests/engine_twins.rs | 121 +++++++++++++---------- crates/jolt-prover/tests/zk_e2e.rs | 73 ++++++-------- 3 files changed, 154 insertions(+), 121 deletions(-) diff --git a/crates/jolt-prover/src/stages/drivers.rs b/crates/jolt-prover/src/stages/drivers.rs index cc0b3e2d47..1d83327234 100644 --- a/crates/jolt-prover/src/stages/drivers.rs +++ b/crates/jolt-prover/src/stages/drivers.rs @@ -204,7 +204,9 @@ mod twin_tests { ClearSumcheckRecorder, CommittedSumcheckRecorder, ProveRounds, RoundScheduler, SequentialRounds, SumcheckError, }; - use jolt_transcript::{Blake2bTranscript, Transcript}; + use jolt_transcript::{ + Blake2b512, Channel as _, ProtocolId, ProverTranscript, VerifierTranscript, + }; use jolt_verifier::stages::relations::{ConcreteSumcheck, SumcheckBatch, SumcheckOutputClaims}; use jolt_verifier::VerifierError; use jolt_witness::{ @@ -757,9 +759,14 @@ mod twin_tests { } } - /// Drive the macro-expanded `prove` and its `verify_clear` twin, assert - /// byte-identical transcript states, and return the driver's output plus - /// the recorded prepare and residue call orders. + fn protocol(name: &str) -> ProtocolId { + ProtocolId::new::(name) + } + + /// Drive the macro-expanded `prove`, verify its argument string with the + /// `verify_clear` twin (which must consume every byte, receive the proved + /// claims, and land on the prover's sponge state), and return the driver's + /// output plus the recorded prepare and residue call orders. #[expect( clippy::type_complexity, reason = "the twin driver's aggregate return: the proved carrier plus the two recorded call orders" @@ -767,7 +774,7 @@ mod twin_tests { fn drive( beta: bool, ) -> ( - Proved, Fr>, + Proved, ()>, Vec<&'static str>, Vec, ) { @@ -781,7 +788,8 @@ mod twin_tests { 23, ))); - let mut prover_transcript = Blake2bTranscript::new(b"prove-driver-twin"); + let mut prover_transcript = + ProverTranscript::::new(&protocol("prove-driver-twin"), b""); let challenges = sumchecks.draw_challenges(&mut prover_transcript).unwrap(); let input_points = sumchecks.empty_input_points(); let proved = sumchecks @@ -793,30 +801,35 @@ mod twin_tests { &inputs, &input_points, &challenges, - ClearSumcheckRecorder::::new(), + ClearSumcheckRecorder::::new(), &mut prover_transcript, ) .unwrap(); // Verifier twin: generated draw + composed verify_clear (which runs the - // derive-opening-points and expected-final-claim checks internally) + - // output-claim absorbs. - let mut verifier_transcript = Blake2bTranscript::new(b"prove-driver-twin"); + // derive-opening-points and expected-final-claim checks internally and + // receives the output claims). + let mut verifier_transcript = VerifierTranscript::::new( + &protocol("prove-driver-twin"), + b"", + prover_transcript.narg(), + ); let verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); - let _ = sumchecks + let (_, received_claims) = sumchecks .verify_clear( &inputs, &input_points, &verifier_challenges, - &proved.output_claims, - &proved.recorded.proof, &mut verifier_transcript, 0, ) .unwrap(); - sumchecks.append_output_claims(&mut verifier_transcript, &proved.output_claims); - - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(received_claims, proved.output_claims); + assert_eq!( + verifier_transcript.preview().squeeze::<32>(), + prover_transcript.preview().squeeze::<32>() + ); + verifier_transcript.finish().unwrap(); let calls = session.take::().unwrap().0; let residues = session.take::().unwrap().0; @@ -886,7 +899,8 @@ mod twin_tests { }; let mut session = ProofSession::default(); - let mut prover_transcript = Blake2bTranscript::new(b"prove-driver-head-twin"); + let mut prover_transcript = + ProverTranscript::::new(&protocol("prove-driver-head-twin"), b""); let challenges = sumchecks.draw_challenges(&mut prover_transcript).unwrap(); let input_points = sumchecks.empty_input_points(); let proved = sumchecks @@ -898,27 +912,32 @@ mod twin_tests { &inputs, &input_points, &challenges, - ClearSumcheckRecorder::::new(), + ClearSumcheckRecorder::::new(), &mut prover_transcript, ) .unwrap(); - let mut verifier_transcript = Blake2bTranscript::new(b"prove-driver-head-twin"); + let mut verifier_transcript = VerifierTranscript::::new( + &protocol("prove-driver-head-twin"), + b"", + prover_transcript.narg(), + ); let verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); - let verified_points = sumchecks + let (verified_points, received_claims) = sumchecks .verify_clear( &inputs, &input_points, &verifier_challenges, - &proved.output_claims, - &proved.recorded.proof, &mut verifier_transcript, 0, ) .unwrap(); - sumchecks.append_output_claims(&mut verifier_transcript, &proved.output_claims); - - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(received_claims, proved.output_claims); + assert_eq!( + verifier_transcript.preview().squeeze::<32>(), + prover_transcript.preview().squeeze::<32>() + ); + verifier_transcript.finish().unwrap(); assert_eq!(verified_points, proved.output_points); // The head member is bound on the batch point's PREFIX: its opening // point is the leading entries of the full-window member's point. @@ -950,7 +969,8 @@ mod twin_tests { let kernels = toy_kernels(); let mut session = ProofSession::default(); - let mut transcript = Blake2bTranscript::new(b"prove-driver-twin"); + let mut transcript = + ProverTranscript::::new(&protocol("prove-driver-twin"), b""); let challenges = sumchecks.draw_challenges(&mut transcript).unwrap(); let input_points = sumchecks.empty_input_points(); let result = sumchecks.prove( @@ -961,7 +981,7 @@ mod twin_tests { &inputs, &input_points, &challenges, - ClearSumcheckRecorder::::new(), + ClearSumcheckRecorder::::new(), &mut transcript, ); assert!(matches!( @@ -1020,8 +1040,11 @@ mod twin_tests { jolt_crypto::Pedersen, rand_core::OsRng, >, - transcript: &mut Blake2bTranscript, - ) -> Result, jolt_crypto::Bn254G1>, ProverError> { + transcript: &mut ProverTranscript, + ) -> Result< + Proved, jolt_sumcheck::CommittedSumcheckWitness>, + ProverError, + > { sumchecks.prove( kernels, session, diff --git a/crates/jolt-prover/tests/engine_twins.rs b/crates/jolt-prover/tests/engine_twins.rs index bcf1c13b0c..2a64b1cd74 100644 --- a/crates/jolt-prover/tests/engine_twins.rs +++ b/crates/jolt-prover/tests/engine_twins.rs @@ -1,7 +1,8 @@ //! Twin-transcript engine locks: toy members driven through //! `jolt_sumcheck::prove_batch` (and `prove_uniskip_clear`) against the //! GENERATED `verify_clear` / `verify_zk` drivers and the shared uni-skip -//! `verify_clear` core, asserting byte-identical transcript states. This pins +//! `verify_clear` core. Each verifier reads the prover's argument string to +//! EOF and must land on the prover's sponge state. This pins //! the prove-side engine to the verifier independently of any real stage. //! Protocol-agnostic: the locks exercise the shared engine/driver seam and //! run under both the Dory and Akita builds. @@ -16,11 +17,13 @@ use jolt_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup}; use jolt_field::{Fr, JoltField, Ring}; use jolt_poly::{UnivariatePoly, UnivariatePolynomial}; use jolt_sumcheck::{ - prove_batch, prove_uniskip_clear, CenteredIntegerDomain, ClearRound, ClearSumcheckRecorder, - CommittedSumcheckRecorder, ProveRounds, SequentialRounds, SumcheckDomain, SumcheckError, - SumcheckRecorder, OPENING_CLAIM_TRANSCRIPT_LABEL, + prove_batch, prove_uniskip_clear, CenteredIntegerDomain, ClearSumcheckRecorder, + CommittedSumcheckRecorder, ProveRounds, SequentialRounds, SumcheckClaim, SumcheckDomain, + SumcheckError, SumcheckRecorder, SumcheckVerifier, +}; +use jolt_transcript::{ + Blake2b512, Channel as _, ProtocolId, ProverTranscript, Sponge, VerifierTranscript, }; -use jolt_transcript::{Blake2bTranscript, Transcript}; use jolt_verifier::stages::relations::{ConcreteSumcheck as _, SumcheckBatch}; use jolt_verifier::stages::stage5::{InstructionReadRaf, RegistersValEvaluation}; use jolt_verifier::stages::uniskip::{self, UniskipParams}; @@ -120,6 +123,24 @@ fn pedersen_setup(capacity: u64) -> PedersenSetup { PedersenSetup::new(generators, generator.scalar_mul(&Fr::from_u64(99))) } +fn prover_transcript(name: &str) -> ProverTranscript { + ProverTranscript::new(&ProtocolId::new::(name), b"") +} + +fn verifier_transcript<'a>(name: &str, narg: &'a [u8]) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new(&ProtocolId::new::(name), b"", narg) +} + +/// The verifier consumed the whole argument string and reached the prover's +/// sponge state. +fn assert_twins(prover: &ProverTranscript, verifier: VerifierTranscript<'_, H>) { + assert_eq!( + verifier.preview().squeeze::<32>(), + prover.preview().squeeze::<32>() + ); + verifier.finish().unwrap(); +} + /// Synthetic stand-ins for a stage's flattened output-claim values. fn synthetic_output_values() -> Vec { vec![Fr::from_u64(11), Fr::from_u64(22), Fr::from_u64(33)] @@ -130,7 +151,7 @@ fn clear_engine_twin_matches_generated_verify_clear() { // Prover: draw → sums → begin_batch(clear) → prove_batch → finish. let sumchecks = fixture(); let inputs = inputs(); - let mut prover_transcript = Blake2bTranscript::new(b"engine-twin"); + let mut prover_transcript = prover_transcript("engine-twin"); let prover_challenges = sumchecks.draw_challenges(&mut prover_transcript).unwrap(); let instruction_sum = sumchecks @@ -155,7 +176,7 @@ fn clear_engine_twin_matches_generated_verify_clear() { 91, ); - let mut recorder = ClearSumcheckRecorder::::new(); + let mut recorder = ClearSumcheckRecorder::::new(); let (batch, prover_coefficients) = sumchecks .begin_batch( &inputs, @@ -175,15 +196,15 @@ fn clear_engine_twin_matches_generated_verify_clear() { ) .unwrap(); let output_values = synthetic_output_values(); - let recorded = recorder + recorder .finish(&output_values, &mut prover_transcript) .unwrap(); - // Verifier: draw → begin_batch → verify_compressed_boolean → output-claim - // absorbs (the low-level clear path the composed `verify_clear` wraps). - let mut verifier_transcript = Blake2bTranscript::new(b"engine-twin"); + // Verifier: draw → begin_batch → compressed rounds → output claims (the + // low-level clear path the composed `verify_clear` wraps). + let mut verifier_transcript = verifier_transcript("engine-twin", prover_transcript.narg()); let verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); - let mut verifier_recorder = ClearSumcheckRecorder::::new(); + let mut verifier_recorder = ClearSumcheckRecorder::::new(); let (verifier_batch, verifier_coefficients) = sumchecks .begin_batch( &inputs, @@ -192,23 +213,22 @@ fn clear_engine_twin_matches_generated_verify_clear() { &mut verifier_transcript, ) .unwrap(); - let reduction = recorded - .proof - .verify_compressed_boolean( + let reduction = SumcheckVerifier::verify_compressed( + &SumcheckClaim::new( verifier_batch.max_num_vars, verifier_batch.max_degree, verifier_batch.claimed_sum, - &mut verifier_transcript, - ) - .unwrap(); - for value in &output_values { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, value); - } + ), + &mut verifier_transcript, + ) + .unwrap(); + let received_values: Vec = verifier_transcript.receive_n(output_values.len()).unwrap(); assert_eq!(reduction.value, proved.final_claim); assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); + assert_eq!(received_values, output_values); assert_eq!(verifier_coefficients, prover_coefficients); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_twins(&prover_transcript, verifier_transcript); } #[test] @@ -220,7 +240,7 @@ fn committed_engine_twin_matches_generated_verify_zk() { // prove_batch → finish (output-claim row commitments absorbed). let sumchecks = fixture(); let inputs = inputs(); - let mut prover_transcript = Blake2bTranscript::new(b"engine-zk-twin"); + let mut prover_transcript = prover_transcript("engine-zk-twin"); let prover_challenges = sumchecks.draw_challenges(&mut prover_transcript).unwrap(); let instruction_sum = sumchecks @@ -265,17 +285,16 @@ fn committed_engine_twin_matches_generated_verify_zk() { &mut prover_transcript, ) .unwrap(); - let recorded = recorder + let witness = recorder .finish(&synthetic_output_values(), &mut prover_transcript) .unwrap(); - assert!(recorded.committed_witness.is_some()); // Verifier: draw → generated verify_zk (coefficient draws, committed - // round consistency, output-claim commitment absorbs). - let mut verifier_transcript = Blake2bTranscript::new(b"engine-zk-twin"); + // rounds, output-claim row commitments). + let mut verifier_transcript = verifier_transcript("engine-zk-twin", prover_transcript.narg()); let _verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); - let consistency = sumchecks - .verify_zk(&recorded.proof, &mut verifier_transcript) + let (consistency, output_commitments) = sumchecks + .verify_zk::(witness.output_claim_rows.len(), &mut verifier_transcript) .unwrap(); assert_eq!(consistency.challenges(), proved.challenges); @@ -288,13 +307,17 @@ fn committed_engine_twin_matches_generated_verify_zk() { ); assert_eq!(consistency.max_num_vars, batch.max_num_vars); assert_eq!(consistency.max_degree, batch.max_degree); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!( + output_commitments.commitments.len(), + witness.output_claim_rows.len() + ); + assert_twins(&prover_transcript, verifier_transcript); } /// Twin-transcript lock for the shared uni-skip verification core: a clear /// uni-skip round proved through `jolt_sumcheck::prove_uniskip_clear` must be -/// accepted by `jolt-verifier`'s `uniskip::verify_clear` with byte-identical -/// transcript states (round proof, output-claim absorb, reduction challenge). +/// accepted by `jolt-verifier`'s `uniskip::verify_clear`, which reads the +/// round polynomial and output claim and lands on the prover's state. #[test] fn uniskip_prover_twin_matches_uniskip_verify_clear() { let params = UniskipParams::spartan_outer(); @@ -305,16 +328,19 @@ fn uniskip_prover_twin_matches_uniskip_verify_clear() { .map(|k| Fr::from_u64(3 * k + 2)) .collect(), ); - let coefficients = CenteredIntegerDomain::new(domain_size) + let coefficients: Vec = CenteredIntegerDomain::new(domain_size) .round_sum_coefficients(UnivariatePolynomial::degree(&poly)) .unwrap(); - let input_claim = as ClearRound>::coefficient_linear_combination( - &poly, - &coefficients, - ); + let input_claim = poly + .coefficients() + .iter() + .zip(&coefficients) + .fold(Fr::from_u64(0), |sum, (coefficient, weight)| { + sum + *coefficient * *weight + }); - let mut prover_transcript = Blake2bTranscript::new(b"uniskip-stage-twin"); - let proved = prove_uniskip_clear::( + let mut prover_transcript = prover_transcript("uniskip-stage-twin"); + let proved = prove_uniskip_clear::( poly, input_claim, degree, @@ -323,16 +349,11 @@ fn uniskip_prover_twin_matches_uniskip_verify_clear() { ) .unwrap(); - let mut verifier_transcript = Blake2bTranscript::new(b"uniskip-stage-twin"); - let challenge = uniskip::verify_clear( - &proved.proof, - ¶ms, - input_claim, - proved.output_claim, - &mut verifier_transcript, - ) - .unwrap(); + let mut verifier_transcript = + verifier_transcript("uniskip-stage-twin", prover_transcript.narg()); + let verified = uniskip::verify_clear(¶ms, input_claim, &mut verifier_transcript).unwrap(); - assert_eq!(challenge, proved.challenge); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(verified.challenge, proved.challenge); + assert_eq!(verified.output_claim, proved.output_claim); + assert_twins(&prover_transcript, verifier_transcript); } diff --git a/crates/jolt-prover/tests/zk_e2e.rs b/crates/jolt-prover/tests/zk_e2e.rs index a7c5706f18..0d69295393 100644 --- a/crates/jolt-prover/tests/zk_e2e.rs +++ b/crates/jolt-prover/tests/zk_e2e.rs @@ -15,24 +15,21 @@ mod support; feature = "zk", not(feature = "field-inline") ))] -#[expect( - clippy::expect_used, - clippy::panic, - reason = "integration tests should fail loudly" -)] +#[expect(clippy::expect_used, reason = "integration tests should fail loudly")] mod zk { extern crate jolt_inlines_keccak256; use common::jolt_device::JoltDevice; use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::{DoryCommitment, DoryScheme}; - use jolt_field::{Fr, Ring}; + use jolt_field::Fr; use jolt_program::execution::OwnedTrace; use jolt_prover::dory::DoryProverPreprocessing; use jolt_prover::{JoltBackend, JoltSharedPreprocessing, ProverConfig}; use jolt_riscv::{JoltInstructionKind, JoltTraceRow}; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; - use jolt_verifier::proof::{JoltProof, JoltProofClaims}; + use jolt_verifier::proof::JoltProof; + use jolt_verifier::JoltProtocolConfig; + use jolt_verifier::JoltSponge; use jolt_verifier::VerifierError; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; @@ -46,7 +43,7 @@ mod zk { const SHA3_INPUT_LEN: usize = 300; const SHA3_PERMUTATIONS: usize = 3; - type Proof = JoltProof>; + type Proof = JoltProof; struct ProvedGuest { preprocessing: DoryProverPreprocessing, @@ -105,16 +102,15 @@ mod zk { jolt_prover::dory::commit_trusted_advice(&preprocessing, &case.trusted_advice) .expect("trusted advice commitment") }); - let proof = - jolt_prover::dory::prove::, Blake2bTranscript, _>( - &backend, - &preprocessing, - &config, - trusted.as_ref(), - &witness, - &public_io, - ) - .expect("modular ZK prove"); + let proof = jolt_prover::dory::prove::, JoltSponge, _>( + &backend, + &preprocessing, + &config, + trusted.as_ref(), + &witness, + &public_io, + ) + .expect("modular ZK prove"); ProvedGuest { trusted_advice_commitment: trusted.map(|entry| entry.commitment), preprocessing, @@ -128,7 +124,7 @@ mod zk { } fn verify(proved: &ProvedGuest) -> Result<(), VerifierError> { - jolt_verifier::verify::, Blake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( &proved.preprocessing.verifier, &proved.public_io, &proved.proof, @@ -142,7 +138,7 @@ mod zk { fn zk_muldiv_reference_backend_proof_is_accepted() { with_zk_stack(|| { let proved = prove_muldiv(JoltBackend::reference()); - assert!(matches!(proved.proof.claims, JoltProofClaims::Zk { .. })); + assert_eq!(proved.proof.protocol, JoltProtocolConfig::for_zk(true)); verify(&proved).expect("modular ZK proof must verify"); }); } @@ -178,10 +174,8 @@ mod zk { fn zk_muldiv_tampered_blindfold_is_rejected() { with_zk_stack(|| { let mut proved = prove_muldiv(JoltBackend::reference()); - let JoltProofClaims::Zk { blindfold_proof } = &mut proved.proof.claims else { - panic!("ZK proof must carry BlindFold claims"); - }; - blindfold_proof.random_u += Fr::from_u64(1); + // The argument string ends with BlindFold's openings. + *proved.proof.narg.last_mut().expect("non-empty proof") ^= 1; assert!(verify(&proved).is_err()); }); } @@ -200,7 +194,7 @@ mod zk { JoltBackend::reference(), |_| {}, ); - assert!(proved.proof.untrusted_advice_commitment.is_some()); + assert!(!proved.public_io.untrusted_advice.is_empty()); verify(&proved).expect("modular ZK advice proof must verify"); }); } @@ -222,22 +216,17 @@ mod zk { ), JoltVmWitnessInputs::new(&run.program, &program_preprocessing, run.trace), ); - let proof = jolt_prover::dory::prove::< - Fr, - DoryScheme, - Pedersen, - Blake2bTranscript, - _, - >( - &JoltBackend::reference(), - &preprocessing, - &config, - None, - &witness, - &public_io, - ) - .expect("committed ZK prove"); - jolt_verifier::verify::, Blake2bTranscript>( + let proof = + jolt_prover::dory::prove::, JoltSponge, _>( + &JoltBackend::reference(), + &preprocessing, + &config, + None, + &witness, + &public_io, + ) + .expect("committed ZK prove"); + jolt_verifier::verify::, JoltSponge>( &preprocessing.verifier, &public_io, &proof, From 1e80b72d20197f5e3c7fef03ac8821e05a0c3abc Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 14:58:16 -0400 Subject: [PATCH 16/62] refactor(sdk)!: select the proof sponge instead of a transcript `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. --- examples/field-ops/src/main.rs | 8 +- examples/recursion/guest/src/lib.rs | 2 +- examples/recursion/src/main.rs | 4 +- jolt-eval/Cargo.toml | 1 - jolt-eval/src/guests/mod.rs | 7 +- .../src/invariant/transcript_symmetry.rs | 121 +++++++++--------- jolt-sdk/macros/src/lib.rs | 4 +- jolt-sdk/src/host_utils.rs | 55 ++------ jolt-sdk/tests/verifier_api.rs | 2 +- 9 files changed, 79 insertions(+), 125 deletions(-) diff --git a/examples/field-ops/src/main.rs b/examples/field-ops/src/main.rs index 07f6527010..b81781d8c7 100644 --- a/examples/field-ops/src/main.rs +++ b/examples/field-ops/src/main.rs @@ -12,8 +12,8 @@ mod pipeline { use jolt::host::JoltProgramSource; use jolt::{ JoltProgramPreprocessing, JoltProverPreprocessing, JoltSharedPreprocessing, MemoryConfig, - OwnedTrace, TraceInputs, TraceOutput, TracerBackend, VerifierField, VerifierPCS, - VerifierTranscript, VerifierVC, + OwnedTrace, ProtocolSponge, TraceInputs, TraceOutput, TracerBackend, VerifierField, + VerifierPCS, VerifierVC, }; use jolt_field::{CanonicalBytes, Ring}; use jolt_program::execution::{ExecutionBackend, JoltProgram, TraceRow}; @@ -173,7 +173,7 @@ mod pipeline { let prover_preprocessing = preprocessing; let backend = JoltBackend::::reference(); - let proof = jolt_prover::prove::( + let proof = jolt_prover::prove::( &backend, &prover_preprocessing, &config, @@ -183,7 +183,7 @@ mod pipeline { ) .expect("modular field-inline prove"); - jolt::jolt_verifier::verify::( + jolt::jolt_verifier::verify::( &prover_preprocessing.verifier, &public_io, &proof, diff --git a/examples/recursion/guest/src/lib.rs b/examples/recursion/guest/src/lib.rs index a021af3dd6..c8a8ba33c6 100644 --- a/examples/recursion/guest/src/lib.rs +++ b/examples/recursion/guest/src/lib.rs @@ -73,7 +73,7 @@ fn verify(bytes: &[u8]) -> u32 { jolt::VerifierField, jolt::VerifierPCS, jolt::VerifierVC, - jolt::VerifierTranscript, + jolt::ProtocolSponge, >(&verifier_preprocessing, &device, &proof, None) .is_ok(); end_cycle_tracking("verification"); diff --git a/examples/recursion/src/main.rs b/examples/recursion/src/main.rs index b9c375f306..03629875e7 100644 --- a/examples/recursion/src/main.rs +++ b/examples/recursion/src/main.rs @@ -395,7 +395,7 @@ fn collect_guest_proofs( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::VerifierTranscript, + jolt_sdk::ProtocolSponge, >(&guest_verifier_preprocessing, &io_device, &proof, None) .is_ok(); info!(" Verification result: {is_valid}"); @@ -540,7 +540,7 @@ fn run_recursion_proof( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::VerifierTranscript, + jolt_sdk::ProtocolSponge, >(&recursion_verifier_preprocessing, &io_device, &proof, None) .is_ok(); let rv = postcard::from_bytes::(&io_device.outputs).unwrap(); diff --git a/jolt-eval/Cargo.toml b/jolt-eval/Cargo.toml index ebd3760c59..de7beb4166 100644 --- a/jolt-eval/Cargo.toml +++ b/jolt-eval/Cargo.toml @@ -20,7 +20,6 @@ jolt-transcript = { workspace = true, features = [ "transcript-keccak", "transcript-poseidon", ] } -spongefish = { workspace = true } common = { workspace = true, features = ["std"] } tracer = { workspace = true } diff --git a/jolt-eval/src/guests/mod.rs b/jolt-eval/src/guests/mod.rs index dabce898aa..20809ebd62 100644 --- a/jolt-eval/src/guests/mod.rs +++ b/jolt-eval/src/guests/mod.rs @@ -25,9 +25,8 @@ pub use tracer::JoltDevice; pub type VerifierField = jolt_field::Fr; pub type VerifierPCS = jolt_dory::DoryScheme; pub type VerifierVC = jolt_crypto::Pedersen; -pub type VerifierTranscript = jolt_transcript::LegacyBlake2bTranscript; -pub type Proof = jolt_verifier::JoltProof; +pub type Proof = jolt_verifier::JoltProof; pub type ProverPreprocessing = DoryProverPreprocessing; pub type VerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; @@ -58,7 +57,7 @@ pub fn verify( proof: Proof, io_device: &JoltDevice, ) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( verifier_pp, io_device, &proof, @@ -89,7 +88,7 @@ pub fn verify_with_claims( io_device.outputs = claimed_outputs.to_vec(); io_device.panic = claimed_panic; - jolt_verifier::verify::( + jolt_verifier::verify::( verifier_pp, &io_device, &proof, diff --git a/jolt-eval/src/invariant/transcript_symmetry.rs b/jolt-eval/src/invariant/transcript_symmetry.rs index 461c0e9919..ee892e32ff 100644 --- a/jolt-eval/src/invariant/transcript_symmetry.rs +++ b/jolt-eval/src/invariant/transcript_symmetry.rs @@ -1,18 +1,21 @@ -//! `transcript_prover_verifier_consistency` — for each spongefish sponge, -//! a `ProverState` / `VerifierState` pair driven by the same operation -//! sequence must round-trip every prover message and produce the same -//! verifier challenges. +//! `transcript_prover_verifier_consistency` — for each sponge, a NARG +//! `ProverTranscript` / `VerifierTranscript` pair driven by the same operation +//! sequence must round-trip every prover message, agree on every challenge, +//! and consume the argument string exactly. use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; -use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr as JFr}; -use spongefish::instantiations::{Blake2b512, Keccak}; - -use jolt_transcript::{prover_transcript, verifier_transcript, BytesMsg, PoseidonSponge}; +use jolt_field::{CanonicalEncoding, Fr as JFr}; +use jolt_transcript::{ + Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, + TranscriptError, VerifierTranscript, +}; use crate::invariant::{CheckError, Invariant, InvariantViolation}; const SESSION: &[u8] = b"jolt-eval/transcript-symmetry/v1"; -const INSTANCE_DIGEST: [u8; 32] = [0u8; 32]; +const PROTOCOL: &str = "jolt-eval/transcript-symmetry"; +/// The longest byte message an [`Op::ProverBytes`] carries. +const MAX_PROVER_BYTES: usize = 64; /// One operation in the prover/verifier sequence. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] @@ -21,9 +24,9 @@ pub enum Op { PublicBytes(Vec), /// Both sides absorb the same public BN254 `Fr` scalar. PublicScalar(#[schemars(with = "[u8; 32]")] JFr), - /// Prover absorbs + emits bytes; verifier reads them back from the NARG. + /// Prover sends length-prefixed bytes; verifier reads them back. ProverBytes(Vec), - /// Prover absorbs + emits a BN254 `Fr` scalar; verifier reads it back. + /// Prover sends a BN254 `Fr` scalar; verifier reads it back. ProverScalar(#[schemars(with = "[u8; 32]")] JFr), /// Both sides squeeze a verifier challenge. Challenge, @@ -55,7 +58,7 @@ impl<'a> Arbitrary<'a> for Input { } fn arb_bytes(u: &mut Unstructured<'_>) -> ArbitraryResult> { - let len = u.int_in_range(0u8..=64)? as usize; + let len = u.int_in_range(0..=MAX_PROVER_BYTES)?; (0..len).map(|_| u.arbitrary()).collect() } @@ -64,52 +67,49 @@ fn arb_scalar(u: &mut Unstructured<'_>) -> ArbitraryResult { Ok(JFr::from_bytes_le_reduced(&bytes)) } -fn run_check(input: &Input, build_sponge: impl Fn() -> H) -> Result<(), CheckError> -where - H: spongefish::DuplexSpongeInterface, -{ - let mut prover = prover_transcript(SESSION, INSTANCE_DIGEST, build_sponge()); +fn run_check(input: &Input) -> Result<(), CheckError> { + let protocol = ProtocolId::new::(PROTOCOL); + let mut prover = ProverTranscript::::new(&protocol, SESSION); let mut prover_challenges: Vec<[u8; 32]> = Vec::new(); - for op in &input.ops { + for (op_idx, op) in input.ops.iter().enumerate() { match op { - Op::PublicBytes(b) => prover.public_message(&BytesMsg(b.clone())), - Op::PublicScalar(f) => prover.public_message(&scalar_bytes(*f)), - Op::ProverBytes(b) => prover.prover_message(&BytesMsg(b.clone())), - Op::ProverScalar(f) => prover.prover_message(&scalar_bytes(*f)), - Op::Challenge => { - let c: [u8; 32] = prover.verifier_message(); - prover_challenges.push(c); - } + Op::PublicBytes(b) => prover.public_bytes(b), + Op::PublicScalar(f) => prover.public(f), + Op::ProverBytes(b) => prover + .send_bounded_bytes(b, MAX_PROVER_BYTES) + .map_err(|e| violation("send_bounded_bytes", op_idx, e))?, + Op::ProverScalar(f) => prover.send(f), + Op::Challenge => prover_challenges.push(prover.challenge_bytes()), } } - let narg: Vec = prover.narg_string().to_vec(); - let mut verifier = verifier_transcript(SESSION, INSTANCE_DIGEST, build_sponge(), &narg); + let narg = prover.finish(); + let mut verifier = VerifierTranscript::::new(&protocol, SESSION, &narg); let mut challenge_idx = 0usize; for (op_idx, op) in input.ops.iter().enumerate() { match op { - Op::PublicBytes(b) => verifier.public_message(&BytesMsg(b.clone())), - Op::PublicScalar(f) => verifier.public_message(&scalar_bytes(*f)), + Op::PublicBytes(b) => verifier.public_bytes(b), + Op::PublicScalar(f) => verifier.public(f), Op::ProverBytes(expected) => { - let got: BytesMsg = verifier - .prover_message() - .map_err(|e| violation("prover_message", op_idx, e))?; - if got.as_slice() != expected.as_slice() { + let got = verifier + .receive_bounded_bytes(MAX_PROVER_BYTES) + .map_err(|e| violation("receive_bounded_bytes", op_idx, e))?; + if got != expected.as_slice() { return Err(mismatch("ProverBytes round-trip", op_idx)); } } Op::ProverScalar(expected) => { - let got: [u8; 32] = verifier - .prover_message() - .map_err(|e| violation("prover_message<[u8; 32]>", op_idx, e))?; - if got != scalar_bytes(*expected) { + let got: JFr = verifier + .receive() + .map_err(|e| violation("receive", op_idx, e))?; + if got != *expected { return Err(mismatch("ProverScalar round-trip", op_idx)); } } Op::Challenge => { - let verifier_c: [u8; 32] = verifier.verifier_message(); + let verifier_c: [u8; 32] = verifier.challenge_bytes(); if verifier_c != prover_challenges[challenge_idx] { return Err(mismatch("Challenge", op_idx)); } @@ -119,18 +119,12 @@ where } verifier - .check_eof() - .map_err(|e| violation("check_eof", input.ops.len(), e))?; + .finish() + .map_err(|e| violation("finish", input.ops.len(), e))?; Ok(()) } -fn scalar_bytes(value: JFr) -> [u8; 32] { - let mut out = [0u8; 32]; - value.to_bytes_le(&mut out); - out -} - -fn violation(what: &str, op_idx: usize, err: spongefish::VerificationError) -> CheckError { +fn violation(what: &str, op_idx: usize, err: TranscriptError) -> CheckError { CheckError::Violation(InvariantViolation::with_details( format!("{what} failed on verifier"), format!("op_idx={op_idx}, err={err:?}"), @@ -194,16 +188,15 @@ fn seed_corpus_shared() -> Vec { fn description_for(label: &str) -> String { format!( - "spongefish ProverState/VerifierState pair ({label} sponge) replaying \ - the same operation sequence must round-trip every prover message \ - and agree on every challenge." + "NARG prover/verifier transcript pair ({label} sponge) replaying \ + the same operation sequence must round-trip every prover message, \ + agree on every challenge, and consume the argument string exactly." ) } -/// Sponge selector for the merged fuzz target. Blake2b512 and Keccak are -/// upstream spongefish instantiations over the same generic layer, so per- -/// sponge fuzz targets duplicated coverage; one target fuzzes all three with -/// the fuzzer choosing the sponge. +/// Sponge selector for the merged fuzz target. The three sponges share one +/// transcript layer, so per-sponge fuzz targets duplicated coverage; one +/// target fuzzes all three with the fuzzer choosing the sponge. #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] pub enum SpongeKind { Blake2b, @@ -255,9 +248,9 @@ impl Invariant for TranscriptConsistencyInvariant { fn check(&self, _setup: &(), input: SpongeInput) -> Result<(), CheckError> { let ops = Input { ops: input.ops }; match input.sponge { - SpongeKind::Blake2b => run_check::(&ops, Blake2b512::default), - SpongeKind::Keccak => run_check::(&ops, Keccak::default), - SpongeKind::Poseidon => run_check::(&ops, PoseidonSponge::new), + SpongeKind::Blake2b => run_check::(&ops), + SpongeKind::Keccak => run_check::(&ops), + SpongeKind::Poseidon => run_check::(&ops), } } @@ -280,7 +273,7 @@ impl Invariant for TranscriptConsistencyInvariant { } } -/// Spongefish symmetry invariant for the Blake2b512 sponge. +/// Transcript symmetry invariant for the Blake2b512 sponge. #[jolt_eval_macros::invariant(Test, RedTeam)] #[derive(Default)] pub struct TranscriptConsistencyBlake2bInvariant; @@ -300,7 +293,7 @@ impl Invariant for TranscriptConsistencyBlake2bInvariant { fn setup(&self) {} fn check(&self, _setup: &(), input: Input) -> Result<(), CheckError> { - run_check::(&input, Blake2b512::default) + run_check::(&input) } fn seed_corpus(&self) -> Vec { @@ -308,7 +301,7 @@ impl Invariant for TranscriptConsistencyBlake2bInvariant { } } -/// Spongefish symmetry invariant for the Keccak sponge. +/// Transcript symmetry invariant for the Keccak sponge. #[jolt_eval_macros::invariant(Test, RedTeam)] #[derive(Default)] pub struct TranscriptConsistencyKeccakInvariant; @@ -328,7 +321,7 @@ impl Invariant for TranscriptConsistencyKeccakInvariant { fn setup(&self) {} fn check(&self, _setup: &(), input: Input) -> Result<(), CheckError> { - run_check::(&input, Keccak::default) + run_check::(&input) } fn seed_corpus(&self) -> Vec { @@ -336,7 +329,7 @@ impl Invariant for TranscriptConsistencyKeccakInvariant { } } -/// Spongefish symmetry invariant for the Poseidon sponge. +/// Transcript symmetry invariant for the Poseidon sponge. #[jolt_eval_macros::invariant(Test, RedTeam)] #[derive(Default)] pub struct TranscriptConsistencyPoseidonInvariant; @@ -356,7 +349,7 @@ impl Invariant for TranscriptConsistencyPoseidonInvariant { fn setup(&self) {} fn check(&self, _setup: &(), input: Input) -> Result<(), CheckError> { - run_check::(&input, PoseidonSponge::new) + run_check::(&input) } fn seed_corpus(&self) -> Vec { diff --git a/jolt-sdk/macros/src/lib.rs b/jolt-sdk/macros/src/lib.rs index b19306c41c..e9f7c962e3 100644 --- a/jolt-sdk/macros/src/lib.rs +++ b/jolt-sdk/macros/src/lib.rs @@ -280,7 +280,7 @@ impl MacroBuilder { jolt::VerifierField, jolt::VerifierPCS, jolt::VerifierVC, - jolt::VerifierTranscript, + jolt::ProtocolSponge, >( &__jolt_preprocessing, &__jolt_io_device, @@ -1179,7 +1179,7 @@ impl MacroBuilder { jolt::VerifierField, jolt::VerifierPCS, jolt::VerifierVC, - jolt::VerifierTranscript, + jolt::ProtocolSponge, >( &preprocessing, &io_device, diff --git a/jolt-sdk/src/host_utils.rs b/jolt-sdk/src/host_utils.rs index f1a9846855..4d272fec2a 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -15,34 +15,10 @@ use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryHint; #[cfg(any(feature = "host", feature = "guest-verifier"))] use jolt_dory::{DoryCommitment, DoryScheme}; -#[cfg(any(feature = "host", feature = "guest-verifier"))] -use jolt_field::Fr; #[cfg(feature = "host")] use jolt_prover::dory::stages::stage0::TrustedAdviceCommitment; #[cfg(feature = "host")] use jolt_prover::{JoltProverPreprocessing as GenericJoltProverPreprocessing, ProverConfig}; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - feature = "transcript-keccak" -))] -use jolt_transcript::KeccakTranscript; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - any( - feature = "transcript-blake2b", - not(any( - feature = "transcript-poseidon", - feature = "transcript-keccak", - feature = "transcript-blake2b" - )) - ) -))] -use jolt_transcript::LegacyBlake2bTranscript; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - feature = "transcript-poseidon" -))] -use jolt_transcript::PoseidonTranscript; #[cfg(feature = "host")] use jolt_verifier::ProgramPreprocessing as GenericProgramPreprocessing; #[cfg(feature = "host")] @@ -52,32 +28,23 @@ use tracer::TracerInlineExpansionProvider; #[cfg(feature = "host")] pub use jolt_host as host; +/// The sponge every SDK proof runs on, chosen by the `transcript-*` feature +/// (Blake2b by default). #[cfg(all( any(feature = "host", feature = "guest-verifier"), feature = "transcript-poseidon" ))] -pub type ProtocolTranscript = PoseidonTranscript; +pub type ProtocolSponge = jolt_transcript::PoseidonSponge; #[cfg(all( any(feature = "host", feature = "guest-verifier"), feature = "transcript-keccak" ))] -pub type ProtocolTranscript = KeccakTranscript; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - feature = "transcript-blake2b" -))] -pub type ProtocolTranscript = LegacyBlake2bTranscript; +pub type ProtocolSponge = jolt_transcript::Keccak; #[cfg(all( any(feature = "host", feature = "guest-verifier"), - not(any( - feature = "transcript-poseidon", - feature = "transcript-keccak", - feature = "transcript-blake2b" - )) + not(any(feature = "transcript-poseidon", feature = "transcript-keccak")) ))] -pub type ProtocolTranscript = LegacyBlake2bTranscript; -#[cfg(feature = "host")] -pub type ProofTranscript = ProtocolTranscript; +pub type ProtocolSponge = jolt_transcript::Blake2b512; #[cfg(feature = "host")] pub use jolt_program::execution::{ ExecutionBackend, OwnedTrace, TraceError, TraceInputs, TraceOutput, TraceSource, @@ -110,12 +77,10 @@ pub type VerifierVC = Pedersen; #[cfg(feature = "host")] pub type VerifierField = jolt_field::Fr; #[cfg(feature = "host")] -pub type VerifierTranscript = ProtocolTranscript; -#[cfg(feature = "host")] pub type JoltVerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; #[cfg(feature = "host")] -pub type RV64IMACProof = jolt_verifier::JoltProof; +pub type RV64IMACProof = jolt_verifier::JoltProof; #[cfg(feature = "host")] pub type JoltProof = RV64IMACProof; #[cfg(feature = "host")] @@ -186,12 +151,10 @@ pub type VerifierVC = Pedersen; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] pub type VerifierField = jolt_field::Fr; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] -pub type VerifierTranscript = ProtocolTranscript; -#[cfg(all(feature = "guest-verifier", not(feature = "host")))] pub type JoltVerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] -pub type RV64IMACProof = jolt_verifier::JoltProof; +pub type RV64IMACProof = jolt_verifier::JoltProof; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] pub type JoltProof = RV64IMACProof; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] @@ -359,7 +322,7 @@ pub fn prove_program( } }; let backend = jolt_prover::JoltBackend::::optimized(); - let proof = jolt_prover::dory::prove::( + let proof = jolt_prover::dory::prove::( &backend, preprocessing, &config, diff --git a/jolt-sdk/tests/verifier_api.rs b/jolt-sdk/tests/verifier_api.rs index 8111799c0b..c7a70e28e0 100644 --- a/jolt-sdk/tests/verifier_api.rs +++ b/jolt-sdk/tests/verifier_api.rs @@ -40,7 +40,7 @@ mod tests { jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::VerifierTranscript, + jolt_sdk::ProtocolSponge, >(&preprocessing, &device, &proof, None); let duration = start.elapsed(); println!("Verification took: {} ms", duration.as_millis()); From 2c959da0cef500b2738a04c191bd315ddf4cccf5 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:00:08 -0400 Subject: [PATCH 17/62] test(kernels): verify the naive kernel twin from the argument string --- crates/jolt-kernels/src/reference/naive.rs | 54 +++++++++++++--------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/crates/jolt-kernels/src/reference/naive.rs b/crates/jolt-kernels/src/reference/naive.rs index 0f8a7938db..2573755a9a 100644 --- a/crates/jolt-kernels/src/reference/naive.rs +++ b/crates/jolt-kernels/src/reference/naive.rs @@ -457,10 +457,10 @@ mod tests { use jolt_field::{Fr, JoltField, Ring}; use jolt_poly::{BindingOrder, EqPolynomial, Polynomial}; use jolt_sumcheck::{ - append_sumcheck_claim, prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, - ProveRounds, SequentialRounds, SumcheckRecorder, OPENING_CLAIM_TRANSCRIPT_LABEL, + prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, ProveRounds, + SequentialRounds, SumcheckClaim, SumcheckRecorder, SumcheckVerifier, }; - use jolt_transcript::{Blake2bTranscript, Transcript}; + use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; use jolt_verifier::stages::relations::ConcreteSumcheck; use jolt_verifier::VerifierError; @@ -651,7 +651,8 @@ mod tests { symbolic: ToySymbolic::new(ROUNDS), reference_point: reference_point(), }; - let mut prover_transcript = Blake2bTranscript::new(b"naive-toy"); + let protocol = ProtocolId::new::("naive-toy"); + let mut prover_transcript = ProverTranscript::::new(&protocol, b""); let challenges = relation.draw_challenges(&mut prover_transcript).unwrap(); let gamma = challenges.gamma; @@ -673,9 +674,9 @@ mod tests { }; let input_claim = relation.input_claim(&inputs, &challenges).unwrap(); assert_eq!(input_claim, claimed_sum); - let mut recorder = ClearSumcheckRecorder::::new(); + let mut recorder = ClearSumcheckRecorder::::new(); recorder.absorb_input_claims(&[input_claim], &mut prover_transcript); - let coefficient: Fr = prover_transcript.challenge_scalar(); + let coefficient: Fr = prover_transcript.challenge(); let prelude = BatchPrelude::new( vec![BatchMember { input_claim, @@ -740,39 +741,45 @@ mod tests { assert_eq!(coefficient * expected, proved.final_claim); assert_eq!(proved.member_claims, vec![expected]); - // Clear-verifier twin: same transcript schedule accepts the proof. - let recorded = recorder + // Clear-verifier twin: the verifier reads the argument string to EOF. + recorder .finish(&output_claims.opening_values(), &mut prover_transcript) .unwrap(); let verifier_relation = ToyRelation { symbolic: ToySymbolic::new(ROUNDS), reference_point: reference_point(), }; - let mut verifier_transcript = Blake2bTranscript::new(b"naive-toy"); + let mut verifier_transcript = + VerifierTranscript::::new(&protocol, b"", prover_transcript.narg()); let verifier_challenges = verifier_relation .draw_challenges(&mut verifier_transcript) .unwrap(); let verifier_input_claim = verifier_relation .input_claim(&inputs, &verifier_challenges) .unwrap(); - append_sumcheck_claim(&mut verifier_transcript, &verifier_input_claim); - let verifier_coefficient: Fr = verifier_transcript.challenge_scalar(); - let reduction = recorded - .proof - .verify_compressed_boolean( + verifier_transcript.public(&verifier_input_claim); + let verifier_coefficient: Fr = verifier_transcript.challenge(); + let reduction = SumcheckVerifier::verify_compressed( + &SumcheckClaim::new( ROUNDS, verifier_relation.degree(), verifier_coefficient * verifier_input_claim, - &mut verifier_transcript, - ) + ), + &mut verifier_transcript, + ) + .unwrap(); + let received: Vec = verifier_transcript + .receive_n(output_claims.opening_values().len()) .unwrap(); - for value in output_claims.opening_values() { - verifier_transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, &value); - } assert_eq!(reduction.value, proved.final_claim); assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_eq!(received, output_claims.opening_values()); + assert_eq!( + verifier_transcript.preview().squeeze::<32>(), + prover_transcript.preview().squeeze::<32>() + ); + verifier_transcript.finish().unwrap(); } /// A derived table materialized at the wrong point survives the sumcheck @@ -785,7 +792,8 @@ mod tests { symbolic: ToySymbolic::new(ROUNDS), reference_point: reference_point(), }; - let mut transcript = Blake2bTranscript::new(b"naive-drift"); + let mut transcript = + ProverTranscript::::new(&ProtocolId::new::("naive-drift"), b""); let challenges = relation.draw_challenges(&mut transcript).unwrap(); let gamma = challenges.gamma; @@ -795,7 +803,7 @@ mod tests { let derived_tables = derived_tables(&drifted_point); let claimed_sum = brute_force_sum(&opening_tables, &derived_tables, gamma); - let coefficient: Fr = transcript.challenge_scalar(); + let coefficient: Fr = transcript.challenge(); let prelude = BatchPrelude::new( vec![BatchMember { input_claim: claimed_sum, @@ -806,7 +814,7 @@ mod tests { ROUNDS, relation.degree(), ); - let mut recorder = ClearSumcheckRecorder::::new(); + let mut recorder = ClearSumcheckRecorder::::new(); let inputs = ToyInputs { total: claimed_sum, untrusted: None, From b23f0f3920ea396c5e374f55531f818671d4d4f7 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:09:08 -0400 Subject: [PATCH 18/62] docs(spec): record the as-built NARG transcript design 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. --- specs/jolt-transcript-narg.md | 241 ++++++++++++++++++++-------------- 1 file changed, 140 insertions(+), 101 deletions(-) diff --git a/specs/jolt-transcript-narg.md b/specs/jolt-transcript-narg.md index a24719637e..65ac5c504e 100644 --- a/specs/jolt-transcript-narg.md +++ b/specs/jolt-transcript-narg.md @@ -4,7 +4,7 @@ | --------- | ------------------ | | Author(s) | @markosg04 | | Created | 2026-10-01 | -| Status | proposed | +| Status | implemented (in review) | | PR | (Jolt) / (Akita) | ## Summary @@ -34,8 +34,9 @@ layer and no follow-up migration PR. ### Goal `jolt-transcript` provides a role-generic proof channel, prover and verifier -ends, typed atoms, exact and small-set challenges, a preview handle for proof -of work, and feature-gated site diagnostics. It is expressive enough that Akita +ends, exact and small-set challenges, a preview handle for proof of work, and +feature-gated site diagnostics. Messages are values with a canonical codec +owned by their type. It is expressive enough that Akita uses it unchanged, and Jolt's prover, verifier, BlindFold, Dory, and Akita integration all run on it. @@ -43,12 +44,12 @@ integration all run on it. | Component | Owner crate | Notes | | --- | --- | --- | -| `Sponge` trait, `Prover`, `Verifier<'a, H>`, `Channel`, `Atom`, `Preview`, grinding, `TranscriptError`, `SiteId` | `jolt-transcript` | Depends on `jolt-field` and `spongefish` only. | +| `Sponge` trait, `ProverTranscript`, `VerifierTranscript<'a, H>`, `Channel`, `Preview`, grinding, `TranscriptError`, `SiteId` | `jolt-transcript` | Depends on `jolt-field` and `spongefish` only. | | Sponge permutations / hash duplexes | `spongefish` (Blake2b512, Keccak, ...) and `jolt-transcript::PoseidonSponge` | One workspace pin for both repos. | | Exact uniform sampling contract | `jolt-field` (`Field::random`) | The channel feeds it a sponge-backed `RngCore`. | | Small-set challenge decoding | `jolt-field` (`from_challenge_bytes`) | Unchanged semantics. | -| Canonical encode/checked decode of fields | `jolt-field` (`CanonicalEncoding`) | Extension fields gain `CanonicalEncoding` (checked per coefficient). | -| Atom impls for group elements, commitments | `jolt-crypto`, `jolt-dory`, Akita crates | Each type's owner implements `Atom` locally. | +| Message codec: `CanonicalBytes` (encode) and `CanonicalDecode` (checked decode) | `jolt-field` | Implemented for every field, extension field, `u8`–`u128`, and `[u8; N]`. | +| Codec impls for group elements and commitments | `jolt-crypto`, `jolt-dory`, Akita crates | Each type's owner implements the codec locally. | | Round-message shapes, protocol order | `jolt-sumcheck`, `jolt-verifier`, Akita protocol crates | Shapes are positional, derived from public parameters. | | Akita site coordinates, descriptor bytes | `akita-types` | Plain data; converted to `SiteId` for diagnostics. | @@ -56,9 +57,10 @@ integration all run on it. 1. **Absorbed bytes are transmitted bytes.** Every prover message is absorbed with exactly the bytes written to (prover) or consumed from (verifier) the - NARG. `Atom::read` accepts only canonical encodings of valid values (field - elements below the modulus, on-curve and in-subgroup points), so a verifier - cannot accept two byte strings for one value. + NARG. `CanonicalDecode::from_bytes_le_checked` accepts only canonical + encodings of valid values (field elements below the modulus, on-curve and + in-subgroup points), so a verifier cannot accept two byte strings for one + value. 2. **No unabsorbed proof data.** The verifier obtains proof data only through `Verifier::receive*`. There is no hint channel and no side struct. The absence of a hint API is the enforcement mechanism. @@ -66,13 +68,15 @@ integration all run on it. fully consumed. Composite verifiers (Jolt calling Akita) call it once, at the outermost boundary. 4. **Domain separation.** Each channel starts from a 64-byte protocol id that - binds the protocol name, version, proof mode (clear or ZK), and - `Sponge::ID`, followed by a length-framed session value. A proof produced - under one sponge or mode cannot verify under another. + binds the protocol name and `Sponge::ID`, followed by a length-framed + session value. Protocol axes are public inputs: the Jolt verifier absorbs + its own build's `JoltProtocolConfig` first in the public preamble. A proof + produced under one sponge or protocol configuration cannot verify under + another. 5. **Role symmetry.** For any operation sequence, a verifier replaying it over the prover's NARG reproduces every challenge and every received value. `jolt-eval`'s `transcript_prover_verifier_consistency` invariant checks this - over `Prover`/`Verifier` for every sponge. + for every sponge. 6. **Previews cannot mutate the proof.** `Preview` holds only a clone of the public sponge state. It cannot write the NARG or advance the live sponge. 7. **Sites never affect bytes.** `SiteId` is diagnostic. It is recorded under @@ -108,88 +112,88 @@ pub trait Sponge: DuplexSpongeInterface + Default + Clone + Send + Sync const ID: &'static str; } -/// Fixed-width canonical value. Shapes are positional: callers send and receive -/// exactly the counts their public parameters imply. -pub trait Atom: Sized { - const SIZE: usize; - fn write(&self, out: &mut [u8]); // exactly SIZE bytes - fn read(bytes: &[u8]) -> Result; // canonical-only -} -impl Atom for F { /* LE canonical bytes */ } -// Local atoms: U32, U64, Bytes. Group elements and commitments implement -// Atom in their owner crates. +pub struct ProtocolId([u8; PROTOCOL_ID_LEN]); +impl ProtocolId { pub const fn new(name: &str) -> Self; } -pub struct ProtocolId([u8; 64]); -impl ProtocolId { - pub fn new(name: &str, version: u32, mode: ProofMode) -> Self; +/// Operations both roles perform identically; shared protocol code takes `&mut impl Channel`. +pub trait Channel { + type Sponge: Sponge; + fn site(&mut self, site: SiteId); + fn public(&mut self, value: &A); + fn public_all(&mut self, values: &[A]); + fn public_bytes(&mut self, bytes: &[u8]); // u64-length-framed + /// Prover: send `*value`. Verifier: overwrite `*value` with the received message. + fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError>; + fn exchange_all(&mut self, values: &mut [A]) -> Result<(), TranscriptError>; + fn challenge(&mut self) -> F; // exactly uniform + fn challenge_small(&mut self) -> F; // from_challenge_bytes(16 bytes) + fn challenge_bytes(&mut self) -> [u8; N]; + fn preview(&self) -> Preview; + fn challenges_small(&mut self, len: usize) -> Vec; // provided + fn challenge_powers(&mut self, len: usize) -> Vec; // provided: 1, γ, γ², ... } -pub struct Prover { /* sponge, narg, logging */ } -impl Prover { +impl ProverTranscript { pub fn new(protocol: &ProtocolId, session: &[u8]) -> Self; - pub fn send(&mut self, value: &A); - pub fn send_all(&mut self, values: &[A]); - pub fn send_bytes(&mut self, bytes: &[u8], max_len: usize) -> Result<(), TranscriptError>; - pub fn grind(&mut self, bits: u8) -> Result<(), TranscriptError>; + pub fn send(&mut self, value: &A); + pub fn send_all(&mut self, values: &[A]); + pub fn send_bytes(&mut self, bytes: &[u8]); // length fixed by the protocol + pub fn send_bounded_bytes(&mut self, bytes: &[u8], max_len: usize) -> Result<(), TranscriptError>; + pub fn send_nonce(&mut self, nonce: u32); + pub fn grind(&mut self, bits: u8) -> Result; // returns the nonce + pub fn narg(&self) -> &[u8]; pub fn finish(self) -> Vec; } -pub struct Verifier<'a, H: Sponge> { /* sponge, remaining narg, logging */ } -impl<'a, H: Sponge> Verifier<'a, H> { +impl<'a, H: Sponge> VerifierTranscript<'a, H> { pub fn new(protocol: &ProtocolId, session: &[u8], narg: &'a [u8]) -> Self; - pub fn receive(&mut self) -> Result; - pub fn receive_n(&mut self, count: usize) -> Result, TranscriptError>; - pub fn receive_bytes(&mut self, max_len: usize) -> Result, TranscriptError>; - pub fn check_grind(&mut self, bits: u8) -> Result<(), TranscriptError>; - pub fn finish(self) -> Result<(), TranscriptError>; -} - -/// Operations both roles perform identically; shared protocol code takes `&mut impl Channel`. -pub trait Channel { - type Sponge: Sponge; - fn public(&mut self, value: &A); - fn public_all(&mut self, values: &[A]); - fn public_bytes(&mut self, bytes: &[u8]); // length-framed - /// Prover: send `*value`. Verifier: overwrite `*value` with the received atom. - fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError>; - fn challenge(&mut self) -> F; - fn challenge_small(&mut self) -> F; - fn challenge_bytes(&mut self) -> [u8; N]; - fn preview(&self) -> Preview; - fn site(&mut self, site: SiteId); + pub fn receive(&mut self) -> Result; + pub fn receive_n(&mut self, count: usize) -> Result, TranscriptError>; + pub fn receive_bytes(&mut self, len: usize) -> Result<&'a [u8], TranscriptError>; + pub fn receive_bounded_bytes(&mut self, max_len: usize) -> Result<&'a [u8], TranscriptError>; + pub fn receive_nonce(&mut self, nonce_bits: u8) -> Result; + pub fn check_grind(&mut self, bits: u8) -> Result; + pub fn remaining(&self) -> usize; + pub fn finish(self) -> Result<(), TranscriptError>; // EOF and poison check } ``` -`Prover` and `Verifier` implement `Channel`. `public_*` and `challenge*` are -inherent on both as well, so role-specific code needs no trait import. +The verifier poisons itself on its first error: every later receive returns +`TranscriptError::Poisoned`, so a caller that drops an error cannot continue +on a desynchronized transcript. -Example: one sumcheck round of degree `d`. The verifier knows `d` from public -parameters, so the poly's length is never transmitted. +Example: one sumcheck round of degree bound `d`. The verifier knows `d` from +public parameters, so the polynomial's length is never transmitted; the prover +pads lower-degree rounds with zero coefficients. ```rust // prover // verifier -prover.send_all(round_poly.coeffs()); let c = verifier.receive_n::(d + 1)?; +prover.send_all(&padded_coeffs); let c = verifier.receive_n::(d + 1)?; let r = prover.challenge_small::(); let r = verifier.challenge_small::(); ``` ### Wire format -The NARG is the concatenation of atom encodings and length-framed byte payloads -(8-byte LE length, then bytes, with `len <= max_len` checked before any -allocation). It carries no labels or tags. `JoltProof` becomes a -`Vec` newtype with serde. Prover-chosen parameters the verifier needs before -reading shapes (`trace_length`, `ram_K`, configs, advice presence) are the first -prover messages. The verifier receives and validates each one before using it -to size a later receive. The proof mode is in the protocol id, and the verifier -also checks it against its build before reading, so a mismatch surfaces as a -typed error rather than a challenge mismatch. +The NARG is the concatenation of message encodings. Variable-length payloads +carry a 4-byte LE length checked against the caller's `max_len` before any +read. Public byte strings are absorbed with an 8-byte LE length frame. The NARG +carries no labels or tags. `JoltProof` is `{ protocol: JoltProtocolConfig, +narg: Vec }`. `protocol` exists only so a configuration mismatch surfaces as +a typed error before verification; it is never trusted, since the verifier +absorbs its own configuration. + +The first prover message is the proof header: `trace_length` and `ram_K` as +u64, the six rw/one-hot round counts as `[u8; 6]`, the trace order as u64, and +the untrusted-advice flag as u8. The verifier validates the header against its +preprocessing before using it to size any later receive. The commitments follow +in `ProofCommitments` order at counts fixed by the RA layout. Byte order is little-endian canonical (`CanonicalBytes`). The legacy big-endian reversal disappears. ### Composition: Jolt calls Akita -Akita's prove and verify entry points take `&mut Prover` / `&mut Verifier` +Akita's prove and verify entry points take `&mut ProverTranscript` / `&mut VerifierTranscript` (or `&mut impl Channel` for shared sites) and absorb their instance descriptor with `public_bytes` as their first operation. Akita's standalone API constructs its own channel with an Akita `ProtocolId` and calls the same inner function, so @@ -204,22 +208,29 @@ before any Akita challenge in both cases. ### Dory -`dory-pcs` exposes a symmetric `DoryTranscript` and its own proof struct. -`jolt-dory` implements `DoryTranscript` for `&mut impl Channel`: `append_*` maps -to `public_*` and `challenge_scalar` to `challenge`. The serialized Dory proof is -sent with `send_bytes` and decoded with `DorySerialize`, which rejects trailing -and non-canonical data. Dory therefore absorbs its group elements twice, once -as NARG bytes and once through its own transcript calls. That costs a few -hundred group-element absorptions per proof, and it keeps invariant 2: there is -no hint path. A Dory-native NARG integration belongs to a `dory-pcs` release -and is out of scope. +`dory-pcs` exposes a symmetric `DoryTranscript` and verifies from a proof struct +it receives up front. `jolt-dory` runs it inside the NARG with no double +transmission: + +- The prover adapter sends every value dory-pcs absorbs through `append_serde`, + so the NARG holds Dory's messages in Dory's Fiat-Shamir order. +- The verifier rebuilds the proof struct by reading those messages from a + scratch clone of the transcript (`read_proof`), then runs `dory::verify` with + an adapter whose every `append_serde` receives the next message from the live + transcript and checks it equals the value dory-pcs absorbs. +- The Σ₁ responses of a ZK proof, which dory-pcs never absorbs, are sent after + the Dory messages. + +Every NARG byte is therefore absorbed exactly once on each side, and invariant +2 holds without trusting `dory-pcs` to absorb what it reads. ### Grinding and previews -`Preview` clones the sponge and exposes `absorb` and -`squeeze(&mut [u8])`. `grind(bits)` searches `U32` nonces against a preview, -sends the winning nonce, then squeezes and checks the predicate on the live -channel. `check_grind` receives the nonce and checks the same predicate. The +`Preview` clones the sponge and exposes `absorb`, `absorb_bytes`, +`absorb_nonce`, and `squeeze::()`. `grind(bits)` searches LEB128 nonces +against a preview, sends the winning nonce, then squeezes and checks the +predicate on the live channel. `check_grind` receives the nonce and checks the +same predicate. The predicate (leading zero bits of 8 squeezed bytes, little-endian bit order) and the nonce bound move from `akita-transcript/src/grinding.rs`. Akita's fold-response nonce search builds on `Preview` inside Akita. @@ -232,32 +243,55 @@ with a `SiteId` and retires `fs_audit`'s thread-local scope. Tests use the recorded ranges for two purposes: - **Equivalence:** the prover's and verifier's site streams are identical. - This replaces Akita's `transcript_hardening` and Jolt's `fs_inventory`. + This replaces Akita's `transcript_hardening`. Jolt keeps its static source + census (`fs_obligations`), retargeted to the message, absorb, challenge, and + site calls, as a review gate. - **Tamper sweep:** flipping one byte in every recorded prover-message range makes verification fail. This is the NARG form of the existing per-field tamper sweeps. ### BlindFold -The ZK prover finishes stage 8 on its `Prover`, then runs the verifier's own -stage functions over a `Verifier` built on the NARG prefix to obtain the stage -outputs that BlindFold lowers. The replay has to end with the prefix fully -consumed and a 32-byte sponge fingerprint (squeezed from a clone) equal to the -prover's. Otherwise proving fails with the existing -`ProverError::InvariantViolation`. BlindFold then continues on the original `Prover`. ZK recorders send -round commitments instead of round polynomials. The choice is made by message -type, not by a second transcript. +The ZK prover finishes stage 8 on its `ProverTranscript`, then runs the +verifier's `verify_stages` over a `VerifierTranscript` built on its NARG prefix. +That is the same function `verify` runs: the seeding messages, stages 1–8, and +the lowering into the BlindFold protocol. The replay must consume the whole +prefix and reach the prover's sponge state (a 32-byte squeeze from a preview). +Otherwise proving fails with `ProverError::InvariantViolation`. BlindFold then +proves onto the original transcript. ZK recorders send round commitments +instead of round polynomials; the choice is made by recorder type, not by a +second transcript. + +### Stage-level wire decisions + +- **Output claims are received by shape.** Each batch member receives its wire + openings in canonical order. Aliased openings are not sent; they are filled + from their canonical source, so an alias cannot disagree with its source. + Stage 6b's booleanity bytecode-RA dedup is decided at runtime by opening-point + equality and goes through the same helpers. Its ZK commitment count depends + on the verified rounds (`verify_zk_with`). +- **Stage 4 staged openings.** The RAM value-check input claim consumes the + advice and program-image openings, so a clear proof sends them after the + stage's gamma draws and before the batch. A ZK proof commits them in its + output-claim rows in the claims aggregate's canonical order. +- **Uni-skip** output claims are sent right after the round's challenge, before + any later draw. +- **Stage 8** runs `HomomorphicBatch`'s sequence on both sides: absorb the + scaled claim values, draw the batching powers, open, and absorb the joint + evaluation claim. ### Sponge selection Protocol code is generic over `H: Sponge`. That replaces today's `T: Transcript` one for one, so generic arity is unchanged. Jolt fixes its -default in one place -(`pub type JoltSponge = spongefish::instantiations::Blake2b512` in -`jolt-verifier`). Akita drops the `transcript-blake2b`/`transcript-keccak` -features and the `compile_error!` that enforces exactly one of them. The sponge -reaches Akita through its caller's `H`. `PoseidonSponge` stays behind the -`transcript-poseidon` feature because it pulls in BN254. +default in one place (`pub type JoltSponge = jolt_transcript::Blake2b512` in +`jolt-verifier`). The SDK's `transcript-*` features choose `ProtocolSponge` +(Blake2b512, Keccak, or Poseidon). Akita drops the +`transcript-blake2b`/`transcript-keccak` features and the `compile_error!` that +enforces exactly one of them. The sponge reaches Akita through its caller's +`H`, and Akita's own default is `AkitaSponge = jolt_transcript::Blake2b512`. +`PoseidonSponge` stays behind the `transcript-poseidon` feature because it +pulls in BN254. ### Alternatives considered @@ -298,8 +332,8 @@ reaches Akita through its caller's `H`. `PoseidonSponge` stays behind the directly. Akita depends on `jolt-transcript` at the Jolt PR's revision. - [ ] `jolt-akita` contains no nested transcript. Akita's messages are in Jolt's NARG, and the only `finish()` call is Jolt's. -- [ ] `JoltProof` is a NARG newtype. No verifier stage reads proof data except - through `Verifier`. +- [ ] `JoltProof` is `{ protocol, narg }`. No verifier stage reads proof data + except through `VerifierTranscript`. - [ ] Changing `JoltSponge` to `Keccak` compiles, and the prover acceptance suites pass (exercised once in CI on one suite). - [ ] Tamper sweep: in the clear, ZK, and Akita fixture proofs, every recorded @@ -370,8 +404,13 @@ it onto Jolt `main`. PRs will conflict with the channel-taking signatures. - **Recursion cycle count.** `examples/recursion` verifies in-guest with software Blake2b512 duplexing. The cycle-count change is not yet measured. -- **Dory parsing.** That `DorySerialize` rejects every non-canonical proof - encoding is assumed from `jolt-dory`'s existing serde tests, not re-audited. +- **Dory parsing.** Every Dory message is decoded with checked canonical + decoding and compared against what dory-pcs absorbs; the byte-flip sweep in + `jolt-dory`'s tests covers one byte per message, not every byte. +- **API gaps found by the Akita port.** `Preview` cannot replay a + `public_bytes` call, so Akita absorbs its fold payload unframed. A semantic + failure after a successful receive does not poison the verifier; callers rely + on returning `Err`. - **Small-challenge wrapper constraints.** Whether any wrapper constraint depends on 125-bit challenges is out of scope here. It matters for the out-of-tree ports. From 585e5d6403e7d25018178128a5af2fdd33076a26 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:11:13 -0400 Subject: [PATCH 19/62] test(prover): verify field-inline stage tests with the production verifier 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. --- crates/jolt-prover/src/dory/stages/stage0.rs | 50 +- .../src/stages/field_inline_fixtures.rs | 602 ++++-------------- crates/jolt-prover/src/stages/stage1.rs | 125 ++-- crates/jolt-prover/src/stages/stage2.rs | 182 ++---- crates/jolt-prover/src/stages/stage4.rs | 80 +-- crates/jolt-prover/src/stages/stage5.rs | 51 +- crates/jolt-prover/src/stages/stage6a.rs | 61 +- crates/jolt-prover/src/stages/stage6b.rs | 199 +----- crates/jolt-prover/tests/field_inline_e2e.rs | 139 ++-- 9 files changed, 380 insertions(+), 1109 deletions(-) diff --git a/crates/jolt-prover/src/dory/stages/stage0.rs b/crates/jolt-prover/src/dory/stages/stage0.rs index 4b7e01fff9..c4c041d151 100644 --- a/crates/jolt-prover/src/dory/stages/stage0.rs +++ b/crates/jolt-prover/src/dory/stages/stage0.rs @@ -449,7 +449,6 @@ mod field_inline_tests { use jolt_field::{Fr, Ring}; use jolt_kernels::finish_streamed; use jolt_openings::{CommitmentScheme, StreamingCommitment}; - use jolt_transcript::LegacyBlake2bTranscript; fn grid() -> CommitmentGrid { CommitmentGrid { @@ -473,13 +472,11 @@ mod field_inline_tests { finish_streamed::(partial, setup).0 } - /// The prover attaches the field-inline payload and absorbs it through the - /// verifier's own `absorb_transcript_commitments` — pinned by asserting - /// the payload is `Some`, that both sides' absorbs agree byte-for-byte - /// (equal challenge streams), and that stripping the payload diverges - /// (the field-inline commitment is Fiat-Shamir-bound). + /// The prover assembles the field-inline payload into the trace commitments: + /// its hints name exactly `FieldRdInc`, and its commitment is the dense + /// trace-domain column committed with the jolt increment columns' placement. #[test] - fn stage0_attaches_and_absorbs_the_field_inline_payload() { + fn stage0_assembles_the_field_inline_payload() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); @@ -496,8 +493,6 @@ mod field_inline_tests { &setup, ) .unwrap(); - let (commitments, _hints) = assemble_commitments::(committed).unwrap(); - let (field_inline, field_inline_hints) = commit_field_inline::( &backend, &mut session, @@ -506,9 +501,9 @@ mod field_inline_tests { &setup, ) .unwrap(); - let commitments = commitments.with_field_inline(field_inline); + let (commitments, _hints) = + assemble_commitments::(committed, field_inline).unwrap(); - assert!(commitments.field_inline.is_some()); assert_eq!( field_inline_hints .iter() @@ -532,40 +527,9 @@ mod field_inline_tests { "fixture column" ); assert_eq!( - commitments - .field_inline - .as_ref() - .unwrap() - .field_registers - .rd_inc, + commitments.field_inline.field_registers.rd_inc, direct_dense_commitment(&column, &setup) ); - - // Byte-for-byte absorb parity between the prover-side call and the - // verifier's own absorb (the same shared fn on the same payload). - let mut prover_transcript = LegacyBlake2bTranscript::::new(b"Jolt"); - absorb_transcript_commitments(&commitments, None, None, &mut prover_transcript); - let mut verifier_transcript = LegacyBlake2bTranscript::::new(b"Jolt"); - jolt_verifier::absorb_transcript_commitments( - &commitments, - None, - None, - &mut verifier_transcript, - ); - assert_eq!( - prover_transcript.challenge(), - verifier_transcript.challenge() - ); - - let mut stripped = commitments.clone(); - stripped.field_inline = None; - let mut stripped_transcript = LegacyBlake2bTranscript::::new(b"Jolt"); - absorb_transcript_commitments(&stripped, None, None, &mut stripped_transcript); - assert_ne!( - prover_transcript.challenge(), - stripped_transcript.challenge(), - "the field-inline payload must be Fiat-Shamir-bound" - ); } /// Zero-short-circuit sanity: a field-inline guest with no field-inline instructions diff --git a/crates/jolt-prover/src/stages/field_inline_fixtures.rs b/crates/jolt-prover/src/stages/field_inline_fixtures.rs index 8bc621ba44..4ccf989159 100644 --- a/crates/jolt-prover/src/stages/field_inline_fixtures.rs +++ b/crates/jolt-prover/src/stages/field_inline_fixtures.rs @@ -20,9 +20,14 @@ use std::sync::Arc; use common::constants::{MAX_BLINDFOLD_GENERATORS, RAM_START_ADDRESS}; use common::jolt_device::{JoltDevice, MemoryConfig, MemoryLayout}; -use jolt_claims::protocols::jolt::JoltOneHotConfig; +use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltRelationId}; +#[cfg(feature = "zk")] +use jolt_crypto::{Bn254, JoltGroup, PedersenSetup}; use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; +use jolt_field::Fr; +#[cfg(feature = "zk")] +use jolt_field::Ring; use jolt_program::execution::{ JoltProgram, OwnedTrace, RamAccess, RamWrite, RegisterRead, RegisterState, RegisterWrite, TraceOutput, TraceRow, @@ -35,9 +40,13 @@ use jolt_riscv::{ FieldInlineOp, JoltInstructionKind, JoltInstructionProfile, JoltInstructionRow, NormalizedOperands, RV64IMAC_JOLT_FIELD_INLINE, }; +use jolt_transcript::{Channel, ProverTranscript, VerifierTranscript}; use jolt_verifier::preprocessing::{JoltVerifierPreprocessing, ProgramPreprocessing}; -use jolt_verifier::stages::PrecommittedSchedule; -use jolt_verifier::CheckedInputs; +use jolt_verifier::stages::{ + build_formula_dimensions, stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, + PrecommittedSchedule, +}; +use jolt_verifier::{jolt_protocol_id, CheckedInputs, JoltSponge, JOLT_SESSION}; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use crate::{JoltProverPreprocessing, ProverConfig}; @@ -50,6 +59,8 @@ pub(crate) const LOG_T: usize = 3; // Matches the witness backend's `JoltVmWitnessConfig` ram size (64). pub(crate) const RAM_LOG_K: usize = 6; +pub(crate) type FixturePreprocessing = JoltProverPreprocessing>; + fn instruction( instruction_kind: JoltInstructionKind, offset: usize, @@ -353,9 +364,7 @@ pub(crate) fn field_arithmetic_backend() -> TraceBackend { /// fixture program: a full-program verifier preprocessing (the same /// `JoltProgramPreprocessing` the witness backend holds) and a minimal Dory /// setup — the reference-tier stage recipes never commit through it. -fn prover_preprocessing( - bytecode: Vec, -) -> JoltProverPreprocessing> { +fn prover_preprocessing(bytecode: Vec) -> FixturePreprocessing { JoltProverPreprocessing { verifier: JoltVerifierPreprocessing::new( ProgramPreprocessing::Full(fixture_program_preprocessing(bytecode)), @@ -368,24 +377,29 @@ fn prover_preprocessing( } } -pub(crate) fn field_arithmetic_preprocessing( -) -> JoltProverPreprocessing> { +pub(crate) fn field_arithmetic_preprocessing() -> FixturePreprocessing { prover_preprocessing(field_arithmetic_program().0) } -pub(crate) fn addi_only_preprocessing() -> JoltProverPreprocessing> { +pub(crate) fn addi_only_preprocessing() -> FixturePreprocessing { prover_preprocessing(addi_only_program().0) } /// The stage-4+ recipes' checked-inputs carrier for the fixture traces, /// mirroring what shape validation derives for a field-inline proof at this scale -/// (no advice, no precommitted objects, full program). +/// (no advice, no precommitted objects, full program) under +/// [`test_prover_config`]'s shape. pub(crate) fn test_checked_inputs() -> CheckedInputs { + let config = test_prover_config(); CheckedInputs { public_io: test_public_io(), zk: cfg!(feature = "zk"), - trace_length: 1 << LOG_T, - ram_K: 1 << RAM_LOG_K, + trace_length: config.trace_length, + ram_K: config.ram_K, + rw_config: config.rw_config, + one_hot_config: config.one_hot_config, + trace_polynomial_order: config.trace_polynomial_order, + untrusted_advice_commitment_present: false, entry_address: ENTRY, preprocessing_digest: [0u8; 32], trusted_advice_commitment_present: false, @@ -437,470 +451,109 @@ pub(crate) fn test_public_io() -> JoltDevice { } } -/// Twin-transcript replays of the already-round-tripped upstream stages, for -/// the downstream stage twins: each helper advances `transcript` exactly as -/// `stageN::verify`'s clear body does over the prover's outputs (with -/// `verify_clear` hard-checking the wire rounds on the way). The full -/// `verify` entrypoints need an assembled `JoltProof`, so the twins drive the -/// same public constituents instead — the stage-1/2 bodies are the ones -/// stage 2's own round-trip test pins. -#[cfg(not(feature = "zk"))] -#[expect(clippy::unwrap_used, reason = "test twin helpers")] -pub(crate) mod twins { - use common::jolt_device::JoltDevice; - use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; - use jolt_claims::protocols::jolt::geometry::ram::RamRafEvaluationDimensions; - use jolt_claims::protocols::jolt::geometry::spartan::{ - SpartanOuterDimensions, SpartanProductDimensions, - }; - use jolt_claims::protocols::jolt::TraceDimensions; - use jolt_claims::NoChallenges; - use jolt_field::{Fr, Ring}; - use jolt_program::preprocess::PublicIoMemory; - use jolt_transcript::{AppendToTranscript, LegacyBlake2bTranscript as Blake2bTranscript}; - use jolt_verifier::config::JOLT_VERIFIER_CONFIG; - use jolt_verifier::stages::relations::ConcreteSumcheck; - use jolt_verifier::stages::stage1::outer_remainder::{ - outer_remainder_input_values_from_uniskip_output, OuterRemainder, - }; - use jolt_verifier::stages::stage1::outputs::{Stage1BatchInputClaims, Stage1BatchSumchecks}; - use jolt_verifier::stages::stage2::field_registers_claim_reduction::FieldRegistersClaimReduction; - use jolt_verifier::stages::stage2::instruction_claim_reduction::InstructionClaimReduction; - use jolt_verifier::stages::stage2::outputs::Stage2BatchSumchecks; - use jolt_verifier::stages::stage2::product_remainder::ProductRemainder; - use jolt_verifier::stages::stage2::product_uniskip::{ - product_uniskip_input_values_from_stage1, ProductUniskip, - }; - use jolt_verifier::stages::stage2::ram_output_check::RamOutputCheck; - use jolt_verifier::stages::stage2::ram_raf_evaluation::RamRafEvaluation; - use jolt_verifier::stages::stage2::ram_read_write_checking::RamReadWriteChecking; - use jolt_verifier::stages::stage2::{product_tau_low, stage2_batch_input_values_from_upstream}; - use jolt_verifier::stages::stage3::outputs::{ - InstructionInput, RegistersClaimReduction, SpartanShift, Stage3Sumchecks, - }; - use jolt_verifier::stages::stage3::stage3_input_values_from_upstream; - use jolt_verifier::stages::uniskip::{ - self, draw_spartan_outer_tau, draw_spartan_product_tau_high, UniskipParams, - }; - - use jolt_claims::protocols::jolt::geometry::dimensions::REGISTER_ADDRESS_BITS; - #[cfg(feature = "akita")] - use jolt_claims::protocols::jolt::lattice::relations::read_raf::LatticeReadRafAddressPhaseInputClaims; - use jolt_claims::protocols::jolt::JoltRelationId; - use jolt_crypto::{Bn254G1, Pedersen}; - use jolt_dory::DoryScheme; - use jolt_verifier::stages::stage4::field_registers_read_write_checking::FieldRegistersReadWriteChecking; - use jolt_verifier::stages::stage4::outputs::Stage4Sumchecks; - use jolt_verifier::stages::stage4::ram_val_check::RamValCheck; - use jolt_verifier::stages::stage4::registers_read_write_checking::RegistersReadWriteChecking; - use jolt_verifier::stages::stage4::{ - public_initial_ram_evaluation, ram_val_check_init_structure, - stage4_input_points_from_upstream, stage4_input_values_from_upstream, - RamValCheckInitialEvaluation, - }; - use jolt_verifier::stages::stage5::field_registers_val_evaluation::FieldRegistersValEvaluation; - use jolt_verifier::stages::stage5::instruction_read_raf::InstructionReadRaf; - use jolt_verifier::stages::stage5::outputs::Stage5Sumchecks; - use jolt_verifier::stages::stage5::ram_ra_claim_reduction::RamRaClaimReduction; - use jolt_verifier::stages::stage5::registers_val_evaluation::RegistersValEvaluation; - use jolt_verifier::stages::stage5::{ - stage5_input_points_from_upstream, stage5_input_values_from_upstream, - }; - use jolt_verifier::stages::stage6a::batch::Stage6aBuildParts; - use jolt_verifier::stages::stage6a::booleanity::BooleanityAddressPhaseInputClaims; - use jolt_verifier::stages::stage6a::bytecode_read_raf::bytecode_read_raf_address_phase_input_values_from_upstream; - use jolt_verifier::stages::stage6a::field_inline::field_inline_bytecode_read_raf_address_phase_input_values_from_upstream; - use jolt_verifier::stages::stage6a::outputs::{Stage6aInputClaims, Stage6aSumchecks}; - use jolt_verifier::CheckedInputs; - - use super::{LOG_T, RAM_LOG_K}; - use crate::stages::stage1::Stage1ProverOutput; - use crate::stages::stage2::Stage2ProverOutput; - use crate::stages::stage3::Stage3ProverOutput; - use crate::stages::stage4::Stage4ProverOutput; - use crate::stages::stage5::Stage5ProverOutput; - use crate::stages::stage6a::Stage6aProverOutput; - use crate::{JoltProverPreprocessing, ProverConfig}; - - pub(crate) type FixturePreprocessing = JoltProverPreprocessing>; - - /// Stage 1's twin (already round-tripped by stage 1's own tests): - /// positions the transcript at the stage-2 boundary. - pub(crate) fn replay_stage1( - transcript: &mut Blake2bTranscript, - stage1: &Stage1ProverOutput, - ) { - let tau = draw_spartan_outer_tau(transcript, LOG_T); - let uniskip_challenge = uniskip::verify_clear( - &stage1.uniskip_proof, - &UniskipParams::spartan_outer(), - Fr::from_u64(0), - stage1.claims.uniskip_output_claim, - transcript, - ) - .unwrap(); - let sumchecks = Stage1BatchSumchecks { - outer_remainder: OuterRemainder::new( - SpartanOuterDimensions::rv64(LOG_T), - tau, - uniskip_challenge, - ), - }; - let batch_challenges = sumchecks.draw_challenges(transcript).unwrap(); - let input_points = sumchecks.empty_input_points(); - let input_values = Stage1BatchInputClaims { - outer_remainder: outer_remainder_input_values_from_uniskip_output( - stage1.claims.uniskip_output_claim, - ), - }; - let _stage1_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &batch_challenges, - &stage1.claims.outer, - &stage1.sumcheck_proof, - transcript, - 1, - ) - .unwrap(); - sumchecks.append_output_claims(transcript, &stage1.claims.outer); - } - - /// Stage 2's twin (already round-tripped by stage 2's own tests): - /// positions the transcript at the stage-3 boundary. - pub(crate) fn replay_stage2( - transcript: &mut Blake2bTranscript, - config: &ProverConfig, - public_io: &JoltDevice, - stage1: &Stage1ProverOutput, - stage2: &Stage2ProverOutput, - ) { - let log_t = LOG_T; - let log_k = config.ram_K.ilog2() as usize; - let trace_dimensions = TraceDimensions::new(log_t); - let read_write_dimensions = config.rw_config.ram_dimensions(log_t, log_k); - let product_dimensions = SpartanProductDimensions::new(log_t); - let raf_dimensions = RamRafEvaluationDimensions::try_from(read_write_dimensions).unwrap(); - let tau_low = product_tau_low(&stage1.clear_output.remainder_point(), log_t).unwrap(); - - let tau_high: Fr = draw_spartan_product_tau_high(transcript); - let uniskip_relation = ProductUniskip::new(product_dimensions, tau_high); - let uniskip_inputs = product_uniskip_input_values_from_stage1(&stage1.clear_output); - let uniskip_input_claim = uniskip_relation - .input_claim(&uniskip_inputs, &NoChallenges::default()) - .unwrap(); - let uniskip_challenge = uniskip::verify_clear( - &stage2.uniskip_proof, - &UniskipParams::spartan_product(), - uniskip_input_claim, - stage2.claims.product_uniskip_output_claim, - transcript, - ) - .unwrap(); +/// The BlindFold row-commitment setup for the committed stage recipes: +/// `MAX_BLINDFOLD_GENERATORS` distinct generator multiples, matching +/// [`test_checked_inputs`]'s `vc_capacity`. +#[cfg(feature = "zk")] +pub(crate) fn test_vc_setup() -> PedersenSetup { + let generator = Bn254::g1_generator(); + let generators = (2..2 + MAX_BLINDFOLD_GENERATORS as u64) + .map(|k| generator.scalar_mul(&Fr::from_u64(k))) + .collect(); + PedersenSetup::new(generators, generator.scalar_mul(&Fr::from_u64(1))) +} - let lowest_address = public_io.memory_layout.get_lowest_address(); - let public_memory = PublicIoMemory::new(public_io).unwrap(); - let sumchecks = Stage2BatchSumchecks { - ram_read_write: RamReadWriteChecking::new( - read_write_dimensions, - log_k, - tau_low.clone(), - ), - product_remainder: ProductRemainder::new( - product_dimensions, - uniskip_challenge, - tau_high, - tau_low.clone(), - ), - instruction_claim_reduction: InstructionClaimReduction::new( - trace_dimensions, - tau_low.clone(), - ), - field_registers_claim_reduction: FieldRegistersClaimReduction::new( - FieldRegistersTraceDimensions::new(log_t), - tau_low.clone(), - ), - ram_raf_evaluation: RamRafEvaluation::new( - read_write_dimensions, - raf_dimensions, - log_k, - lowest_address, - tau_low.clone(), - ), - ram_output_check: RamOutputCheck::new(read_write_dimensions, public_memory), - }; - let challenges = sumchecks.draw_challenges(transcript).unwrap(); - let input_points = sumchecks.empty_input_points(); - sumchecks - .validate_output_claims(&stage2.claims.batch_outputs) - .unwrap(); - let input_values = stage2_batch_input_values_from_upstream( - &stage1.clear_output, - stage2.claims.product_uniskip_output_claim, - ); - let _stage2_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &challenges, - &stage2.claims.batch_outputs, - &stage2.sumcheck_proof, - transcript, - 2, - ) - .unwrap(); - sumchecks.append_output_claims(transcript, &stage2.claims.batch_outputs); - } +/// A fresh prover transcript under Jolt's protocol id and session. The +/// fixture prover starts at stage 1 (no stage 0), so [`verify_through`] +/// runs the verifier stages from stage 1 on the same fresh transcript. +pub(crate) fn fixture_transcript() -> ProverTranscript { + ProverTranscript::new(&jolt_protocol_id::(), JOLT_SESSION) +} - /// Stage 3's twin (`stage3::verify`'s clear body — the stage has no field-inline - /// member): positions the transcript at the stage-4 boundary. - pub(crate) fn replay_stage3( - transcript: &mut Blake2bTranscript, - stage1: &Stage1ProverOutput, - stage2: &Stage2ProverOutput, - stage3: &Stage3ProverOutput, - ) { - let dimensions = TraceDimensions::new(LOG_T); - let tau_low = stage2.clear_output.product_tau_low.clone(); - let product_remainder_point = stage2 - .clear_output - .output_points - .product_remainder_point() - .to_vec(); - let sumchecks = Stage3Sumchecks { - shift: SpartanShift::new(dimensions, tau_low.clone(), product_remainder_point.clone()), - instruction_input: InstructionInput::new(dimensions, product_remainder_point), - registers_claim_reduction: RegistersClaimReduction::new(dimensions, tau_low), - }; - let challenges = sumchecks.draw_challenges(transcript).unwrap(); - sumchecks.validate_output_claims(&stage3.claims).unwrap(); - let input_values = stage3_input_values_from_upstream( - &stage1.clear_output.output_values, - &stage2.clear_output.output_values, - ); - let input_points = sumchecks.empty_input_points(); - let _stage3_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &challenges, - &stage3.claims, - &stage3.sumcheck_proof, - transcript, - 3, - ) - .unwrap(); - sumchecks.append_output_claims(transcript, &stage3.claims); - } +/// The last production verifier stage [`verify_through`] runs. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Through { + Stage1, + Stage2, + Stage4, + Stage5, + Stage6a, + Stage6b, +} - /// Stage 4's twin (already round-tripped by stage 4's own test): - /// `stage4::verify`'s clear body, positioning the transcript at the - /// stage-5 boundary. The fixtures carry no advice and no committed - /// program image, so the attached-claims step degenerates to the public - /// initial-RAM evaluation alone. - pub(crate) fn replay_stage4( - transcript: &mut Blake2bTranscript, - config: &ProverConfig, - checked: &CheckedInputs, - preprocessing: &FixturePreprocessing, - stage2: &Stage2ProverOutput, - stage3: &Stage3ProverOutput, - stage4: &Stage4ProverOutput, - ) { - let register_dimensions = config - .rw_config - .register_dimensions(LOG_T, REGISTER_ADDRESS_BITS); - let ram_read_write_opening_point = stage2.clear_output.output_points.ram_read_write_point(); - let (r_address, _) = ram_read_write_opening_point.split_at(RAM_LOG_K); - let public_eval = - public_initial_ram_evaluation(checked, &preprocessing.verifier, r_address).unwrap(); - let init_structure = - ram_val_check_init_structure(checked, false, r_address, public_eval).unwrap(); - let sumchecks = Stage4Sumchecks { - registers_read_write: RegistersReadWriteChecking::new(register_dimensions), - field_registers_read_write: FieldRegistersReadWriteChecking::new( - JOLT_VERIFIER_CONFIG - .field_inline - .read_write_dimensions(LOG_T), - ), - ram_val_check: RamValCheck::new( - TraceDimensions::new(LOG_T), - RAM_LOG_K, - init_structure.decomposition(), - ), - }; - let challenges = sumchecks.draw_challenges(transcript).unwrap(); - sumchecks.validate_output_claims(&stage4.claims).unwrap(); - let ram_val_check_init = RamValCheckInitialEvaluation { - public_eval, - program_image_contribution: None, - advice_contributions: Vec::new(), - }; - let input_values = stage4_input_values_from_upstream( - &stage2.clear_output.output_values, - &stage3.clear_output.output_values, - &ram_val_check_init, - ); - let input_points = stage4_input_points_from_upstream( - &stage2.clear_output.output_points, - &stage3.clear_output.output_points, - &init_structure, - ); - let _stage4_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &challenges, - &stage4.claims, - &stage4.sumcheck_proof, - transcript, - 4, - ) +/// Verifies the fixture prover's argument string with the production stage +/// verifiers through `last`, requiring the verifier to consume every byte +/// and land on the prover's sponge state. +pub(crate) fn verify_through( + last: Through, + checked: &CheckedInputs, + preprocessing: &FixturePreprocessing, + prover: &ProverTranscript, +) { + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + prover.narg(), + ); + let verifier = &preprocessing.verifier; + let formula_dimensions = + build_formula_dimensions(verifier, checked, LOG_T, JoltRelationId::InstructionReadRaf) .unwrap(); - stage4.claims.append_to_transcript(transcript); - } - - /// Stage 5's twin (`stage5::verify`'s clear body): positions the - /// transcript at the stage-6a boundary. - pub(crate) fn replay_stage5( - transcript: &mut Blake2bTranscript, - config: &ProverConfig, - checked: &CheckedInputs, - preprocessing: &FixturePreprocessing, - stage2: &Stage2ProverOutput, - stage4: &Stage4ProverOutput, - stage5: &Stage5ProverOutput, - ) { - let formula_dimensions = crate::stages::formula_dimensions( + 'stages: { + let t = &mut transcript; + let stage1 = stage1::verify::(checked, t).unwrap(); + if last == Through::Stage1 { + break 'stages; + } + let stage2 = stage2::verify(checked, t, &stage1).unwrap(); + if last == Through::Stage2 { + break 'stages; + } + let stage3 = stage3::verify(checked, t, &stage1, &stage2).unwrap(); + let stage4 = stage4::verify(checked, verifier, t, &stage2, &stage3).unwrap(); + if last == Through::Stage4 { + break 'stages; + } + let stage5 = stage5::verify(checked, &formula_dimensions, t, &stage2, &stage4).unwrap(); + if last == Through::Stage5 { + break 'stages; + } + let stage6a = stage6a::verify( checked, - config, - preprocessing.verifier.program.bytecode_len(), - JoltRelationId::InstructionReadRaf, + verifier, + &formula_dimensions, + t, + &stage1, + &stage2, + &stage3, + &stage4, + &stage5, ) .unwrap(); - let trace_dimensions = formula_dimensions.trace; - let sumchecks = Stage5Sumchecks { - instruction_read_raf: InstructionReadRaf::new(formula_dimensions.instruction_read_raf), - ram_ra_claim_reduction: RamRaClaimReduction::new(trace_dimensions, RAM_LOG_K), - registers_val_evaluation: RegistersValEvaluation::new(trace_dimensions), - field_registers_val_evaluation: FieldRegistersValEvaluation::new( - FieldRegistersTraceDimensions::new(trace_dimensions.log_t()), - ), - }; - let challenges = sumchecks.draw_challenges(transcript).unwrap(); - sumchecks.validate_output_claims(&stage5.claims).unwrap(); - let input_values = stage5_input_values_from_upstream( - &stage2.clear_output.output_values, - &stage4.clear_output.output_values, - ); - let input_points = stage5_input_points_from_upstream( - &stage2.clear_output.output_points, - &stage4.clear_output.output_points, - ); - let _stage5_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &challenges, - &stage5.claims, - &stage5.sumcheck_proof, - transcript, - 5, - ) - .unwrap(); - sumchecks.append_output_claims(transcript, &stage5.claims); - } - - /// Stage 6a's twin (`stage6a::verify`'s clear body): positions the - /// transcript at the stage-6b boundary. - #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] - pub(crate) fn replay_stage6a( - transcript: &mut Blake2bTranscript, - config: &ProverConfig, - checked: &CheckedInputs, - preprocessing: &FixturePreprocessing, - stage1: &Stage1ProverOutput, - stage2: &Stage2ProverOutput, - stage3: &Stage3ProverOutput, - stage4: &Stage4ProverOutput, - stage5: &Stage5ProverOutput, - stage6a: &Stage6aProverOutput, - ) { - let formula_dimensions = crate::stages::formula_dimensions( + if last == Through::Stage6a { + break 'stages; + } + let _stage6b = stage6b::verify( checked, - config, - preprocessing.verifier.program.bytecode_len(), - JoltRelationId::BytecodeReadRaf, + verifier, + &formula_dimensions, + t, + &stage1, + &stage2, + &stage3, + &stage4, + &stage5, + &stage6a, ) .unwrap(); - let stage1_cycle_binding = stage1 - .clear_output - .cycle_binding_checked(JoltRelationId::BytecodeReadRaf) - .unwrap(); - let entry_bytecode_index = preprocessing - .verifier - .program - .entry_bytecode_index_checked(JoltRelationId::BytecodeReadRaf) - .unwrap(); - let sumchecks = Stage6aSumchecks::build_from_parts(Stage6aBuildParts { - formula_dimensions: &formula_dimensions, - committed_chunk_bits: config.one_hot_config.committed_chunk_bits(), - committed_program: false, - entry_bytecode_index, - stage1_cycle_binding: &stage1_cycle_binding, - stage2_points: &stage2.clear_output.output_points, - stage3_points: &stage3.clear_output.output_points, - stage4_points: &stage4.clear_output.output_points, - stage5_points: &stage5.clear_output.output_points, - }) - .unwrap(); - let challenges = sumchecks.draw_challenges(transcript).unwrap(); - sumchecks.validate_output_claims(&stage6a.claims).unwrap(); - let base_input_values = bytecode_read_raf_address_phase_input_values_from_upstream( - &stage1.clear_output.output_values, - &stage2.clear_output.output_values, - &stage3.clear_output.output_values, - &stage4.clear_output.output_values, - &stage5.clear_output.output_values, - ); - // The packed shape folds the four reduced Inc claims into the - // fused-inc consumer stage slots (stage6a::verify's own wrapper). - #[cfg(feature = "akita")] - let base_input_values = LatticeReadRafAddressPhaseInputClaims { - base: base_input_values, - inc: jolt_verifier::stages::stage6b::inc_claim_reduction::inc_claim_reduction_input_values_from_upstream( - &stage2.clear_output.output_values, - &stage4.clear_output.output_values, - &stage5.clear_output.output_values, - ), - }; - use jolt_claims::protocols::composed::ComposedClaims; - let base_input_values = ComposedClaims { - base: base_input_values, - field_inline: field_inline_bytecode_read_raf_address_phase_input_values_from_upstream( - &stage4.clear_output.output_values, - &stage5.clear_output.output_values, - ), - }; - let input_values = Stage6aInputClaims { - bytecode_read_raf: base_input_values, - booleanity: BooleanityAddressPhaseInputClaims::default(), - }; - let input_points = sumchecks.empty_input_points(); - let _stage6a_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &challenges, - &stage6a.claims, - &stage6a.sumcheck_proof, - transcript, - 6, - ) - .unwrap(); - sumchecks.append_output_claims(transcript, &stage6a.claims); } + assert_eq!(transcript.remaining(), 0, "unconsumed argument bytes"); + assert_eq!( + transcript.preview().squeeze::<32>(), + prover.preview().squeeze::<32>(), + "verifier and prover sponge states diverge" + ); + transcript.finish().unwrap(); } /// Shared upstream proving for clear and committed stage tests. @@ -913,11 +566,8 @@ pub(crate) mod proving { use crate::stages::stage5::{prove_stage5, Stage5ProverOutput}; use crate::stages::stage6a::{prove_stage6a, Stage6aProverOutput}; use crate::{JoltBackend, ProofMode}; - use jolt_field::Fr; use jolt_kernels::ProofSession; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; use jolt_witness::JoltWitnessPlane; - type Commitment = Bn254G1; type Stages3 = ( Stage1ProverOutput, Stage2ProverOutput, @@ -934,14 +584,14 @@ pub(crate) mod proving { pub(crate) config: &'a ProverConfig, pub(crate) public_io: &'a JoltDevice, pub(crate) checked: &'a CheckedInputs, - pub(crate) preprocessing: &'a JoltProverPreprocessing>, + pub(crate) preprocessing: &'a FixturePreprocessing, pub(crate) witness: &'a dyn JoltWitnessPlane, - pub(crate) transcript: &'a mut Blake2bTranscript, + pub(crate) transcript: &'a mut ProverTranscript, } impl FixtureProver<'_> { pub(crate) fn through_stage3(&mut self) -> Stages3 { - let stage1 = prove_stage1::, Blake2bTranscript>( + let stage1 = prove_stage1::, JoltSponge>( self.backend, self.session, self.mode, @@ -950,7 +600,7 @@ pub(crate) mod proving { self.transcript, ) .unwrap(); - let stage2 = prove_stage2::, Blake2bTranscript>( + let stage2 = prove_stage2::, JoltSponge>( self.backend, self.session, self.mode, @@ -961,7 +611,7 @@ pub(crate) mod proving { self.transcript, ) .unwrap(); - let stage3 = prove_stage3::, Blake2bTranscript>( + let stage3 = prove_stage3::, JoltSponge>( self.backend, self.session, self.mode, @@ -976,7 +626,7 @@ pub(crate) mod proving { } pub(crate) fn through_stage4(&mut self) -> Stages4 { let (stage1, stage2, stage3) = self.through_stage3(); - let stage4 = prove_stage4::, Blake2bTranscript>( + let stage4 = prove_stage4::, JoltSponge>( self.backend, self.session, self.mode, @@ -993,7 +643,7 @@ pub(crate) mod proving { } pub(crate) fn through_stage5(&mut self) -> Stages5 { let ((stage1, stage2, stage3), stage4) = self.through_stage4(); - let stage5 = prove_stage5::, Blake2bTranscript>( + let stage5 = prove_stage5::, JoltSponge>( self.backend, self.session, self.mode, @@ -1010,7 +660,7 @@ pub(crate) mod proving { } pub(crate) fn through_stage6a(&mut self) -> Stages6a { let (((stage1, stage2, stage3), stage4), stage5) = self.through_stage5(); - let stage6a = prove_stage6a::, Blake2bTranscript>( + let stage6a = prove_stage6a::, JoltSponge>( self.backend, self.session, self.mode, diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index 239d2713a4..ff8fc9a960 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -126,18 +126,11 @@ where }) } -/// Clear round-trips with field-inline enabled of the stage-1 recipe against the verifier's own -/// public constituents — `stage1::verify`'s clear body step for step (the -/// tau draw, `uniskip::verify_clear`, the batch relations, the field-inline seam's -/// attach, `verify_clear`, and the two-part opening absorb), on a twin -/// transcript. The full `stage1::verify` entrypoint needs an assembled -/// `JoltProof`, whose joint-opening slot has no test constructor, so this is -/// the closest public seam; the 32-byte transcript-state equality pins the -/// absorb order end to end. +/// Clear round-trips with field-inline enabled of the stage-1 recipe through +/// the production `stage1::verify` over the prover's argument string. #[cfg(all(test, feature = "field-inline", not(feature = "zk")))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_round_trip { - use crate::stages::field_inline_fixtures::twins; use jolt_claims::protocols::field_inline::geometry::spartan::FIELD_INLINE_SPARTAN_OUTER_R1CS_INPUTS; use jolt_claims::protocols::field_inline::FieldInlinePolynomialId; use jolt_claims::OutputClaims; @@ -146,22 +139,24 @@ mod field_inline_round_trip { use jolt_field::Fr; use jolt_poly::Polynomial; use jolt_program::execution::OwnedTrace; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; use jolt_verifier::stages::stage2::product_tau_low; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltWitnessOracle as _, TraceBackend}; use super::*; use crate::stages::field_inline_fixtures::{ - addi_only_backend, field_arithmetic_backend, LOG_T, + addi_only_backend, addi_only_preprocessing, field_arithmetic_backend, + field_arithmetic_preprocessing, fixture_transcript, test_checked_inputs, verify_through, + FixturePreprocessing, Through, LOG_T, }; - fn round_trip(trace_backend: TraceBackend) { + fn round_trip(trace_backend: TraceBackend, preprocessing: FixturePreprocessing) { let witness = trace_backend.with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); let mode = ProofMode::>::new(None).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(b"stage1-field-inline"); - let out = prove_stage1::, Blake2bTranscript>( + let mut prover_transcript = fixture_transcript(); + let out = prove_stage1::, JoltSponge>( &backend, &mut session, &mode, @@ -188,11 +183,12 @@ mod field_inline_round_trip { assert_eq!(Polynomial::::new(table).evaluate(&tau_low), value); } - // The verifier twin. - let mut transcript = Blake2bTranscript::new(b"stage1-field-inline"); - twins::replay_stage1(&mut transcript, &out); - - assert_eq!(transcript.state(), prover_transcript.state()); + verify_through( + Through::Stage1, + &test_checked_inputs(), + &preprocessing, + &prover_transcript, + ); } /// The ADDI-only field-inline trace: every field-inline column is zero, so this pins @@ -200,49 +196,46 @@ mod field_inline_round_trip { /// instruction. #[test] fn addi_only_stage1_round_trips_the_composed_verifier() { - round_trip(addi_only_backend()); + round_trip(addi_only_backend(), addi_only_preprocessing()); } /// Actual field-inline rows via decoded field-inline instruction words (two field loads and a /// multiply). #[test] fn field_arithmetic_stage1_round_trips_the_composed_verifier() { - round_trip(field_arithmetic_backend()); + round_trip(field_arithmetic_backend(), field_arithmetic_preprocessing()); } } -/// ZK with field-inline enabled: the committed stage-1 shell and the verifier replay. Mirrors -/// `blindfold.rs`'s hard transcript check at stage scope — the replay runs -/// `stage1::verify`'s zk body over its public constituents (the tau draw, -/// `uniskip::verify_zk`, the batch `verify_zk`) and must land on the -/// prover's forward transcript bytes. +/// ZK with field-inline enabled: the committed stage-1 witness carries the +/// composed rows, and the production `stage1::verify` zk branch consumes the +/// prover's argument string. #[cfg(all(test, feature = "field-inline", feature = "zk"))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_zk { use common::constants::MAX_BLINDFOLD_GENERATORS; - use common::jolt_device::JoltDevice; - use jolt_crypto::{Bn254G1, Pedersen, PedersenSetup}; + use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; - use jolt_verifier::stages::uniskip::{self, UniskipParams}; - use jolt_verifier::stages::PrecommittedSchedule; - use jolt_verifier::CheckedInputs; + use jolt_verifier::JoltSponge; use super::*; - use crate::stages::field_inline_fixtures::{field_arithmetic_backend, ENTRY, LOG_T}; + use crate::stages::field_inline_fixtures::{ + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_vc_setup, verify_through, Through, LOG_T, + }; const CAPACITY: usize = MAX_BLINDFOLD_GENERATORS; #[test] - fn committed_stage1_shell_carries_the_composed_rows_and_replays() { + fn committed_stage1_witness_carries_the_composed_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); - let setup = PedersenSetup::new(vec![Bn254G1::default(); CAPACITY], Bn254G1::default()); + let setup = test_vc_setup(); let mode = ProofMode::>::new(Some(&setup)).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(b"stage1-field-inline-zk"); - let out = prove_stage1::, Blake2bTranscript>( + let mut prover_transcript = fixture_transcript(); + let out = prove_stage1::, JoltSponge>( &backend, &mut session, &mode, @@ -252,17 +245,10 @@ mod field_inline_zk { ) .unwrap(); - // The committed shell carries the composed 50 output-claim values + // The committed witness carries the composed 50 output-claim values // (45 common openings + five field value/product openings), row-committed in // capacity-sized chunks — the shape the verifier's // `composed_output_claim_count` check derives. - let total: usize = out - .committed_witness - .output_claim_rows - .iter() - .map(Vec::len) - .sum(); - assert_eq!(total, 50); let row_lens: Vec = out .committed_witness .output_claim_rows @@ -280,50 +266,13 @@ mod field_inline_zk { lens }; assert_eq!(row_lens, expected_row_lens); - let committed = out.sumcheck_proof.as_committed().unwrap(); - assert_eq!( - committed.output_claims.commitments.len(), - 50usize.div_ceil(CAPACITY) - ); - - // The replay. - let checked = CheckedInputs { - public_io: JoltDevice::default(), - zk: true, - trace_length: 1 << LOG_T, - ram_K: 1 << 4, - entry_address: ENTRY, - preprocessing_digest: [0u8; 32], - trusted_advice_commitment_present: false, - vc_capacity: Some(CAPACITY), - precommitted: PrecommittedSchedule { - trusted_advice: None, - untrusted_advice: None, - bytecode: None, - program_image: None, - }, - }; - let mut transcript = Blake2bTranscript::new(b"stage1-field-inline-zk"); - let tau = draw_spartan_outer_tau(&mut transcript, LOG_T); - let uniskip_step = uniskip::verify_zk( - &checked, - &out.uniskip_proof, - &UniskipParams::spartan_outer(), - &mut transcript, - ) - .unwrap(); - let sumchecks = Stage1BatchSumchecks { - outer_remainder: OuterRemainder::new( - SpartanOuterDimensions::rv64(LOG_T), - tau, - uniskip_step.challenge, - ), - }; - let _consistency = sumchecks - .verify_zk(&out.sumcheck_proof, &mut transcript) - .unwrap(); - assert_eq!(transcript.state(), prover_transcript.state()); + verify_through( + Through::Stage1, + &test_checked_inputs(), + &field_arithmetic_preprocessing(), + &prover_transcript, + ); } } diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index 006e186d4f..1b70c11c11 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -201,27 +201,20 @@ where }) } -/// Clear round-trips with field-inline enabled of the stage-2 recipe against the verifier's own -/// public constituents — `stage2::verify`'s clear body step for step (the -/// `τ_high` draw, the composed uni-skip via the seam attach and -/// `uniskip::verify_clear`, the six-member batch with the field-inline claim-reduction -/// member, the field-inline product appendage composition with its alias equality, and the -/// curated absorb) on a twin transcript. The full `stage2::verify` entrypoint -/// needs an assembled `JoltProof` (no test constructor for the joint-opening -/// slot), so this is the closest public seam; the 32-byte transcript-state -/// equality pins the absorb order end to end. +/// Clear round-trips with field-inline enabled of the stage-2 recipe through +/// the production stage-1/2 verifiers over the prover's argument string. #[cfg(all(test, feature = "field-inline", not(feature = "zk")))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_round_trip { - use crate::stages::field_inline_fixtures::twins; use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; + use jolt_verifier::JoltSponge; use super::*; use crate::stages::field_inline_fixtures::{ - field_arithmetic_backend, test_prover_config, test_public_io, LOG_T, + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_prover_config, test_public_io, verify_through, Through, LOG_T, }; use crate::stages::stage1::prove_stage1; @@ -234,8 +227,8 @@ mod field_inline_round_trip { let config = test_prover_config(); let public_io = test_public_io(); - let mut prover_transcript = Blake2bTranscript::new(b"stage2-field-inline"); - let stage1 = prove_stage1::, Blake2bTranscript>( + let mut prover_transcript = fixture_transcript(); + let stage1 = prove_stage1::, JoltSponge>( &backend, &mut session, &mode, @@ -244,7 +237,7 @@ mod field_inline_round_trip { &mut prover_transcript, ) .unwrap(); - let out = prove_stage2::, Blake2bTranscript>( + let out = prove_stage2::, JoltSponge>( &backend, &mut session, &mode, @@ -259,63 +252,52 @@ mod field_inline_round_trip { // The field-inline product appendage is carried, and the spec's alias table // holds on honest data: the field-inline claim-reduction member outputs equal // the appendage values polynomial-for-polynomial. - let appendage = out - .claims - .batch_outputs - .product_remainder - .field_inline - .clone(); + let appendage = &out.claims.batch_outputs.product_remainder.field_inline; let reduction = &out.claims.batch_outputs.field_registers_claim_reduction; assert_eq!(reduction.rs1_value, appendage.rs1_value); assert_eq!(reduction.rs2_value, appendage.rs2_value); assert_eq!(reduction.rd_value, appendage.rd_value); - // The verifier twin (stage2::verify's clear body). - let mut transcript = Blake2bTranscript::new(b"stage2-field-inline"); - twins::replay_stage1(&mut transcript, &stage1); - twins::replay_stage2(&mut transcript, &config, &public_io, &stage1, &out); - - assert_eq!(transcript.state(), prover_transcript.state()); + verify_through( + Through::Stage2, + &test_checked_inputs(), + &field_arithmetic_preprocessing(), + &prover_transcript, + ); } } /// Committed stage-2 output rows use the same canonical alias layout as -/// clear claims, and replay to the prover's transcript state. +/// clear claims, and the production stage-1/2 zk verifiers consume the +/// prover's argument string. #[cfg(all(test, feature = "field-inline", feature = "zk"))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_zk { - use common::constants::MAX_BLINDFOLD_GENERATORS; - use jolt_claims::protocols::jolt::geometry::spartan::SpartanOuterDimensions; - use jolt_crypto::{Bn254G1, Pedersen, PedersenSetup}; + use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; - use jolt_verifier::stages::stage1::outer_remainder::OuterRemainder; - use jolt_verifier::stages::stage1::outputs::Stage1BatchSumchecks; - use jolt_verifier::stages::uniskip::{self, UniskipParams}; - use jolt_verifier::stages::PrecommittedSchedule; - use jolt_verifier::CheckedInputs; + use jolt_verifier::JoltSponge; use super::*; use crate::stages::field_inline_fixtures::{ - field_arithmetic_backend, test_prover_config, test_public_io, ENTRY, LOG_T, + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_prover_config, test_public_io, test_vc_setup, verify_through, + Through, LOG_T, }; use crate::stages::stage1::prove_stage1; - const CAPACITY: usize = MAX_BLINDFOLD_GENERATORS; - #[test] - fn committed_stage2_shell_carries_the_curated_rows_and_replays() { + fn committed_stage2_witness_carries_the_curated_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); - let setup = PedersenSetup::new(vec![Bn254G1::default(); CAPACITY], Bn254G1::default()); + let setup = test_vc_setup(); let mode = ProofMode::>::new(Some(&setup)).unwrap(); let config = test_prover_config(); let public_io = test_public_io(); - let mut prover_transcript = Blake2bTranscript::new(b"stage2-field-inline-zk"); - let stage1 = prove_stage1::, Blake2bTranscript>( + let mut prover_transcript = fixture_transcript(); + let stage1 = prove_stage1::, JoltSponge>( &backend, &mut session, &mode, @@ -324,7 +306,7 @@ mod field_inline_zk { &mut prover_transcript, ) .unwrap(); - let out = prove_stage2::, Blake2bTranscript>( + let out = prove_stage2::, JoltSponge>( &backend, &mut session, &mode, @@ -337,108 +319,20 @@ mod field_inline_zk { .unwrap(); // The three field-inline reduction openings alias the product member's rows. - let values: Vec = out + let value_count: usize = out .committed_witness .output_claim_rows .iter() - .flatten() - .copied() - .collect(); - assert_eq!(values.len(), 18); - - // The replay (stage2::verify's zk body over its public constituents), - // mirroring blindfold.rs's transcript hard check at stage scope. - let checked = CheckedInputs { - public_io: JoltDevice::default(), - zk: true, - trace_length: 1 << LOG_T, - ram_K: 1 << 4, - entry_address: ENTRY, - preprocessing_digest: [0u8; 32], - trusted_advice_commitment_present: false, - vc_capacity: Some(CAPACITY), - precommitted: PrecommittedSchedule { - trusted_advice: None, - untrusted_advice: None, - bytecode: None, - program_image: None, - }, - }; - let mut transcript = Blake2bTranscript::new(b"stage2-field-inline-zk"); - { - // Stage 1's zk twin, to position the transcript. - let tau = uniskip::draw_spartan_outer_tau(&mut transcript, LOG_T); - let uniskip_step = uniskip::verify_zk( - &checked, - &stage1.uniskip_proof, - &UniskipParams::spartan_outer(), - &mut transcript, - ) - .unwrap(); - let sumchecks = Stage1BatchSumchecks { - outer_remainder: OuterRemainder::new( - SpartanOuterDimensions::rv64(LOG_T), - tau, - uniskip_step.challenge, - ), - }; - let _stage1_consistency = sumchecks - .verify_zk(&stage1.sumcheck_proof, &mut transcript) - .unwrap(); - } - - let log_t = LOG_T; - let log_k = config.ram_K.ilog2() as usize; - let read_write_dimensions = config.rw_config.ram_dimensions(log_t, log_k); - let product_dimensions = SpartanProductDimensions::new(log_t); - let raf_dimensions = RamRafEvaluationDimensions::try_from(read_write_dimensions).unwrap(); - let tau_low = product_tau_low(&stage1.clear_output.remainder_point(), log_t).unwrap(); - let tau_high: Fr = draw_spartan_product_tau_high(&mut transcript); - let product_uniskip_step = uniskip::verify_zk( - &checked, - &out.uniskip_proof, - &UniskipParams::spartan_product(), - &mut transcript, - ) - .unwrap(); - - let lowest_address = public_io.memory_layout.get_lowest_address(); - let public_memory = PublicIoMemory::new(&public_io).unwrap(); - let sumchecks = Stage2BatchSumchecks { - ram_read_write: RamReadWriteChecking::new( - read_write_dimensions, - log_k, - tau_low.clone(), - ), - product_remainder: ProductRemainder::new( - product_dimensions, - product_uniskip_step.challenge, - tau_high, - tau_low.clone(), - ), - instruction_claim_reduction: InstructionClaimReduction::new( - TraceDimensions::new(log_t), - tau_low.clone(), - ), - field_registers_claim_reduction: FieldRegistersClaimReduction::new( - FieldRegistersTraceDimensions::new(log_t), - tau_low.clone(), - ), - ram_raf_evaluation: RamRafEvaluation::new( - read_write_dimensions, - raf_dimensions, - log_k, - lowest_address, - tau_low.clone(), - ), - ram_output_check: RamOutputCheck::new(read_write_dimensions, public_memory), - }; - let _challenges = sumchecks.draw_challenges(&mut transcript).unwrap(); - let _stage2_consistency = sumchecks - .verify_zk(&out.sumcheck_proof, &mut transcript) - .unwrap(); - - assert_eq!(transcript.state(), prover_transcript.state()); + .map(Vec::len) + .sum(); + assert_eq!(value_count, 18); + + verify_through( + Through::Stage2, + &test_checked_inputs(), + &field_arithmetic_preprocessing(), + &prover_transcript, + ); } } diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index 113ce1f1d5..1370142fe1 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -234,15 +234,12 @@ where }) } -/// Clear round-trips with field-inline enabled of the stage-4 recipe against the verifier's own -/// public constituents — `stage4::verify`'s clear body step for step (the -/// `Val_init` decomposition, the three-member batch with the field-register read-write -/// member, the generated absorb splicing the five field-inline openings) on a twin -/// transcript positioned by the stage-1..3 replays. The 32-byte -/// transcript-state equality pins the absorb order end to end. A second test -/// drives the field-register read-write kernel directly and ties every extracted opening -/// to a direct MLE evaluation of the witness oracle's tables at the bound -/// point. +/// Clear round-trips with field-inline enabled of the stage-4 recipe through +/// the production stage-1..4 verifiers over the prover's argument string +/// (including the RAM value-check staged openings sent after the gamma draws). +/// A second test drives the field-register read-write kernel directly and ties +/// every extracted opening to a direct MLE evaluation of the witness oracle's +/// tables at the bound point. #[cfg(all(test, feature = "field-inline", not(feature = "zk")))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_round_trip { @@ -258,14 +255,14 @@ mod field_inline_round_trip { use jolt_field::{Fr, Ring}; use jolt_kernels::ProverInputs; use jolt_poly::EqPolynomial; - use jolt_transcript::{LegacyBlake2bTranscript as Blake2bTranscript, Transcript}; use jolt_verifier::stages::relations::ConcreteSumcheck as _; + use jolt_verifier::JoltSponge; use jolt_witness::JoltWitnessOracle as _; use super::*; use crate::stages::field_inline_fixtures::{ - field_arithmetic_backend, field_arithmetic_preprocessing, test_checked_inputs, - test_prover_config, test_public_io, twins, LOG_T, + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_prover_config, test_public_io, verify_through, Through, LOG_T, }; #[test] @@ -279,8 +276,8 @@ mod field_inline_round_trip { let checked = test_checked_inputs(); let preprocessing = field_arithmetic_preprocessing(); - let mut prover_transcript = Blake2bTranscript::new(b"stage4-field-inline"); - let (stage1, stage2, stage3) = FixtureProver { + let mut prover_transcript = fixture_transcript(); + let (_stage1, stage2, stage3) = FixtureProver { backend: &backend, session: &mut session, mode: &mode, @@ -292,7 +289,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage3(); - let out = prove_stage4::, Blake2bTranscript>( + let _stage4 = prove_stage4::, JoltSponge>( &backend, &mut session, &mode, @@ -306,23 +303,12 @@ mod field_inline_round_trip { ) .unwrap(); - // The verifier twin (stage4::verify's clear body, shared as the - // stage-5+ twins' replay), positioned by the upstream replays. - let mut transcript = Blake2bTranscript::new(b"stage4-field-inline"); - twins::replay_stage1(&mut transcript, &stage1); - twins::replay_stage2(&mut transcript, &config, &public_io, &stage1, &stage2); - twins::replay_stage3(&mut transcript, &stage1, &stage2, &stage3); - twins::replay_stage4( - &mut transcript, - &config, + verify_through( + Through::Stage4, &checked, &preprocessing, - &stage2, - &stage3, - &out, + &prover_transcript, ); - - assert_eq!(transcript.state(), prover_transcript.state()); } fn fr(value: u64) -> Fr { @@ -466,41 +452,40 @@ mod field_inline_round_trip { } } -/// ZK with field-inline enabled: the stage-4 committed shell carries the curated row count — the +/// ZK with field-inline enabled: the stage-4 committed witness carries the curated row count — the /// 5 ordinary register openings, the 5 spliced field-register read-write openings, and /// the 2 RAM value-check openings (no advice / program-image rows at the -/// fixture's scale). +/// fixture's scale) — and the production stage-1..4 zk verifiers consume the +/// prover's argument string. #[cfg(all(test, feature = "field-inline", feature = "zk"))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_zk { use crate::stages::field_inline_fixtures::proving::FixtureProver; - use common::constants::MAX_BLINDFOLD_GENERATORS; - use jolt_crypto::{Bn254G1, Pedersen, PedersenSetup}; + use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; + use jolt_verifier::JoltSponge; use super::*; use crate::stages::field_inline_fixtures::{ - field_arithmetic_backend, field_arithmetic_preprocessing, test_checked_inputs, - test_prover_config, test_public_io, + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_prover_config, test_public_io, test_vc_setup, verify_through, + Through, }; - const CAPACITY: usize = MAX_BLINDFOLD_GENERATORS; - #[test] - fn committed_stage4_shell_carries_the_curated_rows() { + fn committed_stage4_witness_carries_the_curated_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); - let setup = PedersenSetup::new(vec![Bn254G1::default(); CAPACITY], Bn254G1::default()); + let setup = test_vc_setup(); let mode = ProofMode::>::new(Some(&setup)).unwrap(); let config = test_prover_config(); let public_io = test_public_io(); let checked = test_checked_inputs(); let preprocessing = field_arithmetic_preprocessing(); - let mut transcript = Blake2bTranscript::new(b"stage4-field-inline-zk"); + let mut transcript = fixture_transcript(); let (_stage1, stage2, stage3) = FixtureProver { backend: &backend, session: &mut session, @@ -513,7 +498,7 @@ mod field_inline_zk { transcript: &mut transcript, } .through_stage3(); - let out = prove_stage4::, Blake2bTranscript>( + let out = prove_stage4::, JoltSponge>( &backend, &mut session, &mode, @@ -527,13 +512,14 @@ mod field_inline_zk { ) .unwrap(); - let values: Vec = out + let value_count: usize = out .committed_witness .output_claim_rows .iter() - .flatten() - .copied() - .collect(); - assert_eq!(values.len(), 12); + .map(Vec::len) + .sum(); + assert_eq!(value_count, 12); + + verify_through(Through::Stage4, &checked, &preprocessing, &transcript); } } diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index 4b29f2076c..db1def1048 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -126,14 +126,11 @@ where }) } -/// Clear round-trips with field-inline enabled of the stage-5 recipe against the verifier's own -/// public constituents — `stage5::verify`'s clear body (the four-member batch -/// with the field-register value evaluation member, which draws no instance challenge) on a -/// twin transcript positioned by the stage-1..4 replays. The 32-byte -/// transcript-state equality pins the absorb order end to end. A second test -/// drives the field-register value evaluation kernel directly and ties both extracted -/// openings to direct MLE evaluations of the witness oracle's tables at the -/// bound point. +/// Clear round-trips with field-inline enabled of the stage-5 recipe through +/// the production stage-1..5 verifiers over the prover's argument string. A +/// second test drives the field-register value evaluation kernel directly and +/// ties both extracted openings to direct MLE evaluations of the witness +/// oracle's tables at the bound point. #[cfg(all(test, feature = "field-inline", not(feature = "zk")))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_round_trip { @@ -149,15 +146,15 @@ mod field_inline_round_trip { use jolt_field::{Fr, Ring}; use jolt_kernels::ProverInputs; use jolt_poly::EqPolynomial; - use jolt_transcript::{LegacyBlake2bTranscript as Blake2bTranscript, Transcript}; use jolt_verifier::stages::relations::ConcreteSumcheck as _; + use jolt_verifier::JoltSponge; use jolt_witness::JoltWitnessOracle as _; use super::*; use crate::recorder::ProofMode; use crate::stages::field_inline_fixtures::{ - field_arithmetic_backend, field_arithmetic_preprocessing, test_checked_inputs, - test_prover_config, test_public_io, twins, LOG_T, + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_prover_config, test_public_io, verify_through, Through, LOG_T, }; #[test] @@ -171,8 +168,8 @@ mod field_inline_round_trip { let checked = test_checked_inputs(); let preprocessing = field_arithmetic_preprocessing(); - let mut prover_transcript = Blake2bTranscript::new(b"stage5-field-inline"); - let ((stage1, stage2, stage3), stage4) = FixtureProver { + let mut prover_transcript = fixture_transcript(); + let ((_stage1, stage2, _stage3), stage4) = FixtureProver { backend: &backend, session: &mut session, mode: &mode, @@ -184,7 +181,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage4(); - let out = prove_stage5::, Blake2bTranscript>( + let _stage5 = prove_stage5::, JoltSponge>( &backend, &mut session, &mode, @@ -198,32 +195,12 @@ mod field_inline_round_trip { ) .unwrap(); - // The verifier twin (stage5::verify's clear body), positioned by the - // upstream replays. - let mut transcript = Blake2bTranscript::new(b"stage5-field-inline"); - twins::replay_stage1(&mut transcript, &stage1); - twins::replay_stage2(&mut transcript, &config, &public_io, &stage1, &stage2); - twins::replay_stage3(&mut transcript, &stage1, &stage2, &stage3); - twins::replay_stage4( - &mut transcript, - &config, - &checked, - &preprocessing, - &stage2, - &stage3, - &stage4, - ); - twins::replay_stage5( - &mut transcript, - &config, + verify_through( + Through::Stage5, &checked, &preprocessing, - &stage2, - &stage4, - &out, + &prover_transcript, ); - - assert_eq!(transcript.state(), prover_transcript.state()); } fn fr(value: u64) -> Fr { diff --git a/crates/jolt-prover/src/stages/stage6a.rs b/crates/jolt-prover/src/stages/stage6a.rs index 00a7527f41..45fe0eb02f 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -181,15 +181,10 @@ where }) } -/// Clear round-trips with field-inline enabled of the stage-6a recipe against the verifier's own -/// public constituents — `stage6a::verify`'s clear body (the batch built by -/// the promoted `build_from_parts` with the field-register access geometry on the bytecode -/// member, the field-inline appendage composition, the composed input claim with its -/// gamma-power extension) on a twin transcript positioned by the stage-1..5 -/// replays, on the field-active arithmetic trace: the appendage openings are -/// nonzero, so the address kernel's field-inline stage-value legs are exercised for -/// real (round 0's engine check pins the composed input claim to the -/// summand). +/// Clear round-trips with field-inline enabled of the stage-6a recipe through +/// the production stage-1..6a verifiers over the prover's argument string, on +/// the field-active arithmetic trace: the appendage openings are nonzero, so +/// the address kernel's field-inline stage-value legs are exercised for real. #[cfg(all(test, feature = "field-inline", not(feature = "zk")))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_round_trip { @@ -197,13 +192,13 @@ mod field_inline_round_trip { use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::{Fr, Ring}; - use jolt_transcript::{LegacyBlake2bTranscript as Blake2bTranscript, Transcript}; + use jolt_verifier::JoltSponge; use super::*; use crate::recorder::ProofMode; use crate::stages::field_inline_fixtures::{ - field_arithmetic_backend, field_arithmetic_preprocessing, test_checked_inputs, - test_prover_config, test_public_io, twins, + field_arithmetic_backend, field_arithmetic_preprocessing, fixture_transcript, + test_checked_inputs, test_prover_config, test_public_io, verify_through, Through, }; #[test] @@ -217,7 +212,7 @@ mod field_inline_round_trip { let checked = test_checked_inputs(); let preprocessing = field_arithmetic_preprocessing(); - let mut prover_transcript = Blake2bTranscript::new(b"stage6a-field-inline"); + let mut prover_transcript = fixture_transcript(); let (((stage1, stage2, stage3), stage4), stage5) = FixtureProver { backend: &backend, session: &mut session, @@ -230,7 +225,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage5(); - let out = prove_stage6a::, Blake2bTranscript>( + let _stage6a = prove_stage6a::, JoltSponge>( &backend, &mut session, &mode, @@ -258,43 +253,11 @@ mod field_inline_round_trip { let zero = Fr::from_u64(0); assert!(appendage.rd_wa_read_write != zero); - // The verifier twin (stage6a::verify's clear body), positioned by the - // upstream replays. - let mut transcript = Blake2bTranscript::new(b"stage6a-field-inline"); - twins::replay_stage1(&mut transcript, &stage1); - twins::replay_stage2(&mut transcript, &config, &public_io, &stage1, &stage2); - twins::replay_stage3(&mut transcript, &stage1, &stage2, &stage3); - twins::replay_stage4( - &mut transcript, - &config, - &checked, - &preprocessing, - &stage2, - &stage3, - &stage4, - ); - twins::replay_stage5( - &mut transcript, - &config, + verify_through( + Through::Stage6a, &checked, &preprocessing, - &stage2, - &stage4, - &stage5, + &prover_transcript, ); - twins::replay_stage6a( - &mut transcript, - &config, - &checked, - &preprocessing, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - &out, - ); - - assert_eq!(transcript.state(), prover_transcript.state()); } } diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index d3d530e511..ad6dd4f07a 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -200,17 +200,14 @@ where }) } -/// Clear round-trips with field-inline enabled of the stage-6b recipe against the verifier's own -/// public constituents — `stage6b::verify`'s clear body (the post-6a draws, -/// the batch with the field-inline increment-reduction member built by the promoted -/// `build_from_parts`, the curated `stage6b_opening_values` absorb with the -/// spliced reduced `FieldRdInc`) on a twin transcript positioned by the -/// stage-1..6a replays — on both fixture profiles: the field-inactive ADDI -/// trace (every field-inline fold zero) and the field-active arithmetic trace (the -/// composed bytecode read-RAF kernels' field-inline stage-value legs carry real -/// values). A further test drives the field-inline increment-reduction kernel directly -/// on the field-inline-arithmetic replay and ties the extracted opening to a direct -/// MLE evaluation. +/// Clear round-trips with field-inline enabled of the stage-6b recipe through +/// the production stage-1..6b verifiers over the prover's argument string — on +/// both fixture profiles: the field-inactive ADDI trace (every field-inline +/// fold zero) and the field-active arithmetic trace (the composed bytecode +/// read-RAF kernels' field-inline stage-value legs carry real values). A +/// further test drives the field-inline increment-reduction kernel directly on +/// the field-inline-arithmetic replay and ties the extracted opening to a +/// direct MLE evaluation. #[cfg(all(test, feature = "field-inline", not(feature = "zk")))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_round_trip { @@ -227,41 +224,31 @@ mod field_inline_round_trip { use jolt_kernels::ProverInputs; use jolt_poly::EqPolynomial; use jolt_program::execution::OwnedTrace; - use jolt_transcript::{LegacyBlake2bTranscript as Blake2bTranscript, Transcript}; use jolt_verifier::stages::relations::ConcreteSumcheck as _; use jolt_verifier::stages::stage6b::field_registers_inc_claim_reduction::FieldRegistersIncClaimReduction; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltWitnessOracle as _, TraceBackend}; use super::*; - use crate::stages::field_inline_fixtures::twins::FixturePreprocessing; use crate::stages::field_inline_fixtures::{ addi_only_backend, addi_only_preprocessing, field_arithmetic_backend, - field_arithmetic_preprocessing, test_checked_inputs, test_prover_config, test_public_io, - twins, LOG_T, RAM_LOG_K, + field_arithmetic_preprocessing, fixture_transcript, test_checked_inputs, + test_prover_config, test_public_io, verify_through, FixturePreprocessing, Through, LOG_T, }; #[test] fn addi_only_stage6b_round_trips_the_composed_verifier() { - stage6b_round_trips( - addi_only_backend(), - addi_only_preprocessing(), - b"stage6b-field-inline", - ); + stage6b_round_trips(addi_only_backend(), addi_only_preprocessing()); } #[test] fn field_arithmetic_stage6b_round_trips_the_composed_verifier() { - stage6b_round_trips( - field_arithmetic_backend(), - field_arithmetic_preprocessing(), - b"stage6b-field-inline-active", - ); + stage6b_round_trips(field_arithmetic_backend(), field_arithmetic_preprocessing()); } fn stage6b_round_trips( trace_backend: TraceBackend, preprocessing: FixturePreprocessing, - label: &'static [u8], ) { let witness = trace_backend.with_field_inline().unwrap(); let backend = JoltBackend::::reference(); @@ -271,7 +258,7 @@ mod field_inline_round_trip { let public_io = test_public_io(); let checked = test_checked_inputs(); - let mut prover_transcript = Blake2bTranscript::new(label); + let mut prover_transcript = fixture_transcript(); let ((((stage1, stage2, stage3), stage4), stage5), stage6a) = FixtureProver { backend: &backend, session: &mut session, @@ -284,7 +271,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage6a(); - let out = prove_stage6b::, Blake2bTranscript>( + let _stage6b = prove_stage6b::, JoltSponge>( &backend, &mut session, &mode, @@ -302,136 +289,12 @@ mod field_inline_round_trip { ) .unwrap(); - // The verifier twin (stage6b::verify's clear body), positioned by - // the upstream replays. The private wire-shape validator is - // transcript-free and elided; `verify_clear`'s hard checks and the - // final state equality pin the protocol content. - let mut transcript = Blake2bTranscript::new(label); - twins::replay_stage1(&mut transcript, &stage1); - twins::replay_stage2(&mut transcript, &config, &public_io, &stage1, &stage2); - twins::replay_stage3(&mut transcript, &stage1, &stage2, &stage3); - twins::replay_stage4( - &mut transcript, - &config, - &checked, - &preprocessing, - &stage2, - &stage3, - &stage4, - ); - twins::replay_stage5( - &mut transcript, - &config, + verify_through( + Through::Stage6b, &checked, &preprocessing, - &stage2, - &stage4, - &stage5, - ); - twins::replay_stage6a( - &mut transcript, - &config, - &checked, - &preprocessing, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - &stage6a, - ); - - let formula_dimensions = crate::stages::formula_dimensions( - &checked, - &config, - preprocessing.verifier.program.bytecode_len(), - JoltRelationId::BytecodeReadRaf, - ) - .unwrap(); - let carried = &stage6a.clear_output.challenges; - let draws = Stage6bDraws::draw(&mut transcript, false); - let program = preprocessing.program().unwrap(); - let entry_bytecode_index = preprocessing - .verifier - .program - .entry_bytecode_index_checked(JoltRelationId::BytecodeReadRaf) - .unwrap(); - let stage1_cycle_binding = stage1 - .clear_output - .cycle_binding_checked(JoltRelationId::BytecodeReadRaf) - .unwrap(); - let sumchecks = Stage6bSumchecks::build_from_parts(Stage6bBuildParts { - formula_dimensions: &formula_dimensions, - ram_log_k: RAM_LOG_K, - committed_chunk_bits: config.one_hot_config.committed_chunk_bits(), - precommitted: &checked.precommitted, - entry_bytecode_index, - bytecode_table_rows: Some(program.bytecode.bytecode.as_slice()), - carried, - eta: draws.eta, - stage1_cycle_binding, - stage2_points: &stage2.clear_output.output_points, - stage3_points: &stage3.clear_output.output_points, - stage4_points: &stage4.clear_output.output_points, - stage5_points: &stage5.clear_output.output_points, - stage6a_points: &stage6a.clear_output.output_points, - address_val_stages: stage6a - .clear_output - .output_values - .bytecode_read_raf - .val_stages - .clone(), - #[cfg(not(feature = "akita"))] - trusted_advice_reference_point: advice_reference_point_from_upstream( - &stage4.clear_output.ram_val_check_init, - JoltAdviceKind::Trusted, - ), - #[cfg(not(feature = "akita"))] - untrusted_advice_reference_point: advice_reference_point_from_upstream( - &stage4.clear_output.ram_val_check_init, - JoltAdviceKind::Untrusted, - ), - }) - .unwrap(); - let cycle_challenges = sumchecks.cycle_challenges(carried, &draws); - let input_values = stage6b_input_values_from_upstream( - &sumchecks, - &stage6a.clear_output.output_values, - &stage2.clear_output.output_values, - &stage4.clear_output, - &stage5.clear_output.output_values, - ) - .unwrap(); - let input_points = stage6b_input_points_from_upstream( - &sumchecks, - &stage2.clear_output.output_points, - &stage4.clear_output.output_points, - &stage5.clear_output.output_points, + &prover_transcript, ); - let cycle_points = sumchecks - .verify_clear( - &input_values, - &input_points, - &cycle_challenges, - &out.claims, - &out.sumcheck_proof, - &mut transcript, - 6, - ) - .unwrap(); - let booleanity_point = cycle_points.booleanity_opening_point().unwrap().to_vec(); - // The verifier's absorb: the curated order with the runtime - // booleanity-vs-bytecode dedup (single-sourced with the prover's - // curation through `stage6b_opening_values`). - for value in jolt_verifier::stages::stage6b::stage6b_opening_values( - &out.claims, - &cycle_points.bytecode_read_raf.bytecode_ra, - &booleanity_point, - ) { - transcript.append_labeled(b"opening_claim", &value); - } - - assert_eq!(transcript.state(), prover_transcript.state()); } fn fr(value: u64) -> Fr { @@ -524,41 +387,39 @@ mod field_inline_round_trip { } } -/// ZK with field-inline enabled: the stage-6b committed shell carries the curated row count — the +/// ZK with field-inline enabled: the stage-6b committed witness carries the curated row count — the /// alias-deduped cycle-point cell total, whose field-inline share is exactly the one -/// spliced reduced `FieldRdInc` row. +/// spliced reduced `FieldRdInc` row — and the production stage-1..6b zk +/// verifiers consume the prover's argument string. #[cfg(all(test, feature = "field-inline", feature = "zk"))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_zk { use crate::stages::field_inline_fixtures::proving::FixtureProver; - use common::constants::MAX_BLINDFOLD_GENERATORS; use jolt_claims::OutputClaims; - use jolt_crypto::{Bn254G1, Pedersen, PedersenSetup}; + use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; + use jolt_verifier::JoltSponge; use super::*; use crate::stages::field_inline_fixtures::{ - addi_only_backend, addi_only_preprocessing, test_checked_inputs, test_prover_config, - test_public_io, + addi_only_backend, addi_only_preprocessing, fixture_transcript, test_checked_inputs, + test_prover_config, test_public_io, test_vc_setup, verify_through, Through, }; - const CAPACITY: usize = MAX_BLINDFOLD_GENERATORS; - #[test] - fn committed_stage6b_shell_carries_the_curated_rows() { + fn committed_stage6b_witness_carries_the_curated_rows_and_verifies() { let witness = addi_only_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); - let setup = PedersenSetup::new(vec![Bn254G1::default(); CAPACITY], Bn254G1::default()); + let setup = test_vc_setup(); let mode = ProofMode::>::new(Some(&setup)).unwrap(); let config = test_prover_config(); let public_io = test_public_io(); let checked = test_checked_inputs(); let preprocessing = addi_only_preprocessing(); - let mut transcript = Blake2bTranscript::new(b"stage6b-field-inline-zk"); + let mut transcript = fixture_transcript(); let ((((stage1, stage2, stage3), stage4), stage5), stage6a) = FixtureProver { backend: &backend, session: &mut session, @@ -571,7 +432,7 @@ mod field_inline_zk { transcript: &mut transcript, } .through_stage6a(); - let out = prove_stage6b::, Blake2bTranscript>( + let out = prove_stage6b::, JoltSponge>( &backend, &mut session, &mode, @@ -616,5 +477,7 @@ mod field_inline_zk { OutputClaims::opening_values(&out.claims.field_registers_inc_claim_reduction).len(), 1 ); + + verify_through(Through::Stage6b, &checked, &preprocessing, &transcript); } } diff --git a/crates/jolt-prover/tests/field_inline_e2e.rs b/crates/jolt-prover/tests/field_inline_e2e.rs index 63dafbdbf5..e6688dde58 100644 --- a/crates/jolt-prover/tests/field_inline_e2e.rs +++ b/crates/jolt-prover/tests/field_inline_e2e.rs @@ -2,9 +2,10 @@ //! //! Acceptance across protocol modes lives in `e2e_matrix.rs`. These tests use //! the same guest cases and preparation, and cover distinct field-inline wire -//! properties: reference/optimized proof equality in clear mode, field commitment -//! presence and binding, and rejection of corrupted BlindFold payloads. Claim and -//! round-polynomial mutations live in the verifier fixture matrix. +//! properties: reference/optimized proof equality in clear mode and the binding +//! of the `FieldRdInc` commitment in both modes. Claim and round-polynomial +//! mutations live in the verifier fixture matrix; BlindFold tampering lives in +//! `zk_e2e.rs`. #[cfg(all( feature = "prover-fixtures", @@ -13,6 +14,65 @@ ))] mod support; +#[cfg(all( + feature = "prover-fixtures", + feature = "field-inline", + not(feature = "akita") +))] +#[expect(clippy::expect_used, reason = "integration tests should fail loudly")] +mod tamper { + use common::jolt_device::JoltDevice; + use jolt_crypto::{Bn254G1, Pedersen}; + use jolt_dory::DoryScheme; + use jolt_transcript::VerifierTranscript; + use jolt_verifier::proof::ProofHeader; + use jolt_verifier::{jolt_protocol_id, seed_transcript, JoltSponge, JOLT_SESSION}; + + use crate::support::field_inline::dory::{Proof, VerifierPreprocessing}; + + /// `proof` with its `FieldRdInc` commitment bytes overwritten by its `RamInc` + /// commitment bytes: a well-formed commitment to a different polynomial. The + /// byte ranges come from the verifier's own header and commitment reads, in + /// `ProofCommitments::send` order (`RdInc`, `RamInc`, the RA commitments, + /// then `FieldRdInc`; the fixtures carry no untrusted advice). + pub(crate) fn with_field_inline_commitment_replaced( + preprocessing: &VerifierPreprocessing, + public_io: &JoltDevice, + proof: &Proof, + ) -> Proof { + let narg = proof.narg.as_slice(); + let consumed = + |transcript: &VerifierTranscript<'_, JoltSponge>| narg.len() - transcript.remaining(); + let protocol = jolt_protocol_id::(); + + let mut transcript = VerifierTranscript::::new(&protocol, JOLT_SESSION, narg); + let _header = ProofHeader::receive(&mut transcript).expect("proof header"); + let header_end = consumed(&transcript); + + let mut transcript = VerifierTranscript::::new(&protocol, JOLT_SESSION, narg); + let seeded = seed_transcript::, JoltSponge>( + preprocessing, + public_io, + None, + &mut transcript, + ) + .expect("proof commitments"); + let commitments_end = consumed(&transcript); + assert!(seeded.commitments.untrusted_advice.is_none()); + let trace = &seeded.commitments.trace; + let count = 3 + trace.instruction_ra.len() + trace.ram_ra.len() + trace.bytecode_ra.len(); + let span = commitments_end - header_end; + assert_eq!(span % count, 0, "Dory commitments have one width"); + let width = span / count; + assert_ne!(trace.ram_inc, trace.field_inline.field_registers.rd_inc); + + let mut tampered = proof.clone(); + let ram_inc = header_end + width..header_end + 2 * width; + tampered.narg.copy_within(ram_inc, commitments_end - width); + tampered + } +} + #[cfg(all( feature = "prover-fixtures", feature = "field-inline", @@ -28,7 +88,6 @@ mod clear { use jolt_dory::DoryScheme; use jolt_field::Fr; use jolt_prover::JoltBackend; - use jolt_verifier::proof::JoltProofClaims; use crate::support::field_inline::dory; use crate::support::field_inline::{field_ops, muldiv}; @@ -42,7 +101,7 @@ mod clear { ] } - /// Both backends' proofs must verify AND be equal wire objects — clear + /// Both backends' proofs must verify AND be equal argument strings — clear /// mode draws nothing outside Fiat-Shamir, so reference/optimized /// divergence anywhere in the composed pipeline shows up here as a proof /// inequality even when both sides individually verify. @@ -51,11 +110,6 @@ mod clear { let mut proofs = Vec::new(); for (label, backend) in backends() { let (preprocessing, public_io, proof) = dory::prove(&field_ops(), backend()); - assert!( - proof.commitments.field_inline.is_some(), - "field-inline proofs must carry the field-inline commitment payload ({label})", - ); - assert!(matches!(proof.claims, JoltProofClaims::Clear(_))); dory::verify_full(&preprocessing, &public_io, &proof).unwrap_or_else(|error| { panic!("modular field-inline proof must verify ({label}): {error}") }); @@ -63,7 +117,7 @@ mod clear { } assert!( proofs[0] == proofs[1], - "reference and optimized field-inline proofs must be identical wire objects", + "reference and optimized field-inline proofs must be identical argument strings", ); } @@ -75,7 +129,6 @@ mod clear { let mut proofs = Vec::new(); for (label, backend) in backends() { let (preprocessing, public_io, proof) = dory::prove(&muldiv(), backend()); - assert!(proof.commitments.field_inline.is_some()); dory::verify_full(&preprocessing, &public_io, &proof).unwrap_or_else(|error| { panic!("field-inactive modular proof must verify ({label}): {error}") }); @@ -83,7 +136,7 @@ mod clear { } assert!( proofs[0] == proofs[1], - "reference and optimized field-inactive proofs must be identical wire objects", + "reference and optimized field-inactive proofs must be identical argument strings", ); } @@ -91,18 +144,14 @@ mod clear { /// this checks that the prover's emitted commitment is transcript-bound. #[test] fn field_inline_tampered_commitment_is_rejected() { - let (preprocessing, public_io, mut proof) = - dory::prove(&field_ops(), JoltBackend::optimized()); + let (preprocessing, public_io, proof) = dory::prove(&field_ops(), JoltBackend::optimized()); dory::verify_full(&preprocessing, &public_io, &proof).expect("honest proof"); - let replacement = proof.commitments.ram_inc.clone(); - let field_inline = proof - .commitments - .field_inline - .as_mut() - .expect("field-inline commitment"); - assert_ne!(field_inline.field_registers.rd_inc, replacement); - field_inline.field_registers.rd_inc = replacement; - assert!(dory::verify_full(&preprocessing, &public_io, &proof).is_err()); + let tampered = crate::tamper::with_field_inline_commitment_replaced( + &preprocessing, + &public_io, + &proof, + ); + assert!(dory::verify_full(&preprocessing, &public_io, &tampered).is_err()); } } @@ -112,53 +161,29 @@ mod clear { feature = "zk", not(feature = "akita") ))] -#[expect( - clippy::expect_used, - clippy::panic, - reason = "integration tests should fail loudly" -)] +#[expect(clippy::expect_used, reason = "integration tests should fail loudly")] mod zk { - use jolt_field::{Fr, Ring}; use jolt_prover::JoltBackend; - use jolt_verifier::proof::JoltProofClaims; use crate::support; use crate::support::field_inline::{dory, field_ops, muldiv}; - /// The field-inline tampers on the ZK wire: the FieldRdInc commitment and - /// the BlindFold payload. Mutate clones of one accepted base proof. + /// The FieldRdInc commitment is bound on the ZK wire too. #[test] - fn field_inline_tampered_proofs_are_rejected() { + fn field_inline_tampered_commitment_is_rejected() { support::with_zk_stack(|| { let (preprocessing, public_io, proof) = dory::prove(&field_ops(), JoltBackend::optimized()); - assert!(matches!(proof.claims, JoltProofClaims::Zk { .. })); - assert!(proof.commitments.field_inline.is_some()); dory::verify_full(&preprocessing, &public_io, &proof) .expect("modular field-inline ZK proof must verify"); - - let mut commitment_tampered = proof.clone(); - let replacement = commitment_tampered.commitments.ram_inc.clone(); - let field_inline = commitment_tampered - .commitments - .field_inline - .as_mut() - .expect("field-inline proof carries the field-inline payload"); - assert_ne!(field_inline.field_registers.rd_inc, replacement); - field_inline.field_registers.rd_inc = replacement; - assert!( - dory::verify_full(&preprocessing, &public_io, &commitment_tampered).is_err(), - "a tampered FieldRdInc commitment must be rejected in ZK mode", + let tampered = crate::tamper::with_field_inline_commitment_replaced( + &preprocessing, + &public_io, + &proof, ); - - let mut blindfold_tampered = proof; - let JoltProofClaims::Zk { blindfold_proof } = &mut blindfold_tampered.claims else { - panic!("ZK proof must carry the BlindFold claims variant"); - }; - blindfold_proof.random_u += Fr::from_u64(1); assert!( - dory::verify_full(&preprocessing, &public_io, &blindfold_tampered).is_err(), - "a tampered BlindFold proof must be rejected", + dory::verify_full(&preprocessing, &public_io, &tampered).is_err(), + "a tampered FieldRdInc commitment must be rejected in ZK mode", ); }); } From b1fb4b946e66a8ce95009eab451dd88c7bc9090e Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:15:10 -0400 Subject: [PATCH 20/62] test(transcript): pin per-sponge known answers and port the bench and 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. --- .../jolt-transcript/benches/transcript_ops.rs | 130 +++++++++-------- .../fuzz/fuzz_targets/encoding_injectivity.rs | 41 +++--- .../fuzz_targets/transcript_determinism.rs | 131 ++++++++++-------- crates/jolt-transcript/tests/known_answers.rs | 101 ++++++++++++++ 4 files changed, 258 insertions(+), 145 deletions(-) create mode 100644 crates/jolt-transcript/tests/known_answers.rs diff --git a/crates/jolt-transcript/benches/transcript_ops.rs b/crates/jolt-transcript/benches/transcript_ops.rs index 2cc82cbd65..0d5342fcb8 100644 --- a/crates/jolt-transcript/benches/transcript_ops.rs +++ b/crates/jolt-transcript/benches/transcript_ops.rs @@ -2,91 +2,89 @@ use std::hint::black_box; -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::measurement::WallTime; +use criterion::{ + criterion_group, criterion_main, BatchSize, BenchmarkGroup, BenchmarkId, Criterion, +}; use jolt_field::Fr; -use jolt_transcript::{Blake2bTranscript, KeccakTranscript, PoseidonTranscript, Transcript}; +use jolt_transcript::{ + Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, +}; -fn bench_append_bytes(c: &mut Criterion) { - let mut group = c.benchmark_group("append_bytes"); - let data_32 = [0xABu8; 32]; - let data_256 = [0xCDu8; 256]; +fn transcript() -> ProverTranscript { + ProverTranscript::new(&ProtocolId::new::("bench"), b"") +} - for (label, data) in [("32B", &data_32[..]), ("256B", &data_256[..])] { - group.bench_with_input(BenchmarkId::new("Blake2b", label), data, |bench, data| { - bench.iter_batched( - || Blake2bTranscript::::new(b"bench"), - |mut t| { - t.append_bytes(black_box(data)); - t - }, - criterion::BatchSize::SmallInput, - ); - }); - group.bench_with_input(BenchmarkId::new("Keccak", label), data, |bench, data| { - bench.iter_batched( - || KeccakTranscript::::new(b"bench"), - |mut t| { - t.append_bytes(black_box(data)); - t - }, - criterion::BatchSize::SmallInput, - ); - }); - group.bench_with_input(BenchmarkId::new("Poseidon", label), data, |bench, data| { +fn bench_send(group: &mut BenchmarkGroup<'_, WallTime>, name: &str) { + let values: Vec = (0..8u64).map(Fr::from).collect(); + group.bench_with_input(BenchmarkId::new(name, "8xFr"), &values, |bench, values| { + bench.iter_batched( + transcript::, + |mut t| { + t.send_all(black_box(values)); + t + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_public_bytes(group: &mut BenchmarkGroup<'_, WallTime>, name: &str) { + for (label, data) in [("32B", &[0xABu8; 32][..]), ("256B", &[0xCDu8; 256][..])] { + group.bench_with_input(BenchmarkId::new(name, label), data, |bench, data| { bench.iter_batched( - || PoseidonTranscript::::new(b"bench"), + transcript::, |mut t| { - t.append_bytes(black_box(data)); + t.public_bytes(black_box(data)); t }, - criterion::BatchSize::SmallInput, + BatchSize::SmallInput, ); }); } - group.finish(); } -fn bench_challenge(c: &mut Criterion) { - let mut group = c.benchmark_group("challenge"); - - group.bench_function("Blake2b", |bench| { - bench.iter_batched( - || { - let mut t = Blake2bTranscript::::new(b"bench"); - t.append_bytes(&[42u8; 32]); - t - }, - |mut t| t.challenge(), - criterion::BatchSize::SmallInput, - ); +fn bench_challenge(group: &mut BenchmarkGroup<'_, WallTime>, name: &str) { + let seeded = || { + let mut t = transcript::(); + t.public_bytes(&[42u8; 32]); + t + }; + group.bench_function(BenchmarkId::new(name, "uniform"), |bench| { + bench.iter_batched(seeded, |mut t| t.challenge::(), BatchSize::SmallInput); }); - - group.bench_function("Keccak", |bench| { + group.bench_function(BenchmarkId::new(name, "small"), |bench| { bench.iter_batched( - || { - let mut t = KeccakTranscript::::new(b"bench"); - t.append_bytes(&[42u8; 32]); - t - }, - |mut t| t.challenge(), - criterion::BatchSize::SmallInput, + seeded, + |mut t| t.challenge_small::(), + BatchSize::SmallInput, ); }); +} - group.bench_function("Poseidon", |bench| { - bench.iter_batched( - || { - let mut t = PoseidonTranscript::::new(b"bench"); - t.append_bytes(&[42u8; 32]); - t - }, - |mut t| t.challenge(), - criterion::BatchSize::SmallInput, - ); - }); +fn bench_send_all(c: &mut Criterion) { + let mut group = c.benchmark_group("send"); + bench_send::(&mut group, "Blake2b"); + bench_send::(&mut group, "Keccak"); + bench_send::(&mut group, "Poseidon"); + group.finish(); +} +fn bench_public(c: &mut Criterion) { + let mut group = c.benchmark_group("public_bytes"); + bench_public_bytes::(&mut group, "Blake2b"); + bench_public_bytes::(&mut group, "Keccak"); + bench_public_bytes::(&mut group, "Poseidon"); + group.finish(); +} + +fn bench_challenges(c: &mut Criterion) { + let mut group = c.benchmark_group("challenge"); + bench_challenge::(&mut group, "Blake2b"); + bench_challenge::(&mut group, "Keccak"); + bench_challenge::(&mut group, "Poseidon"); group.finish(); } -criterion_group!(benches, bench_append_bytes, bench_challenge); +criterion_group!(benches, bench_send_all, bench_public, bench_challenges); criterion_main!(benches); diff --git a/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs b/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs index 5f1abf3e49..7f1ec84e49 100644 --- a/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs +++ b/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs @@ -1,17 +1,16 @@ #![no_main] -//! Injectivity of the length-framed absorption convention: two op sequences -//! whose payloads concatenate to the same raw bytes but with different -//! chunk boundaries must reach different transcript states. +//! Injectivity of the public-bytes framing: two chunk sequences whose payloads +//! concatenate to the same raw bytes but with different chunk boundaries must +//! reach different transcript states. //! -//! Each chunk is absorbed the way production code absorbs variable-length -//! payloads — a `LabelWithCount` frame followed by the payload — so a -//! boundary move changes the framing and MUST change the state. A matching -//! state would mean the framing convention fails to separate `[ab]` from -//! `[a][b]`, the classic transcript-malleability footgun. +//! Each chunk is absorbed with `public_bytes`, which frames it with its length, +//! so a boundary move changes the framing and MUST change the state. A matching +//! state would mean the framing fails to separate `[ab]` from `[a][b]`, the +//! classic transcript-malleability footgun. use jolt_transcript::{ - Blake2bTranscript, KeccakTranscript, LabelWithCount, PoseidonTranscript, Transcript, + Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, }; use libfuzzer_sys::fuzz_target; @@ -32,13 +31,13 @@ fn parse_chunks(data: &[u8]) -> Vec<&[u8]> { chunks } -fn framed_state(chunks: &[Vec]) -> [u8; 32] { - let mut transcript = T::new(b"fuzz-injectivity"); +fn framed_state(chunks: &[Vec]) -> [u8; 32] { + let mut transcript = + ProverTranscript::::new(&ProtocolId::new::("fuzz-injectivity"), b""); for chunk in chunks { - transcript.append(&LabelWithCount(b"chunk", chunk.len() as u64)); - transcript.append_bytes(chunk); + transcript.public_bytes(chunk); } - transcript.state() + transcript.preview().squeeze() } fuzz_target!(|data: &[u8]| { @@ -63,7 +62,7 @@ fuzz_target!(|data: &[u8]| { } let position = 1 + split_at % original[index].len(); if position >= original[index].len() { - // Splitting at the end appends an extra empty-chunk frame rather than + // Splitting at the end appends an extra empty chunk rather than // moving a payload boundary; that is outside this harness's morph class. return; } @@ -75,18 +74,18 @@ fuzz_target!(|data: &[u8]| { debug_assert_eq!(original.concat(), morphed.concat()); assert_ne!( - framed_state::(&original), - framed_state::(&morphed), + framed_state::(&original), + framed_state::(&morphed), "Blake2b framed absorption is not boundary-injective" ); assert_ne!( - framed_state::(&original), - framed_state::(&morphed), + framed_state::(&original), + framed_state::(&morphed), "Keccak framed absorption is not boundary-injective" ); assert_ne!( - framed_state::(&original), - framed_state::(&morphed), + framed_state::(&original), + framed_state::(&morphed), "Poseidon framed absorption is not boundary-injective" ); }); diff --git a/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs b/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs index cff16cdff2..dfa0fb817d 100644 --- a/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs +++ b/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs @@ -1,86 +1,107 @@ #![no_main] -//! Determinism and absorption oracles over the digest transcripts. +//! Determinism and absorption oracles over the prover transcript, per sponge. //! -//! The old `transcript_no_panic` target asserted nothing on a surface with no -//! panic conditions. Instead, twin instances driven by the same fuzzer-chosen -//! op sequence must agree on every challenge and on the final `state()`, and -//! replaying the sequence with one absorbed byte flipped must reach a -//! different final state — a matching state would mean absorbed bytes are -//! ignored or collide. +//! Twin transcripts driven by the same fuzzer-chosen op sequence must agree on +//! every challenge, on the argument string, and on the final sponge state. +//! Replaying the sequence with one absorbed byte flipped (in a sent or a public +//! message) must reach a different final state; a matching state would mean +//! absorbed bytes are ignored or collide. -use jolt_transcript::{Blake2bTranscript, KeccakTranscript, PoseidonTranscript, Transcript}; +use jolt_transcript::{ + Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, +}; use libfuzzer_sys::fuzz_target; const MAX_OPS: usize = 32; +const MAX_MESSAGE: usize = 32; enum Op<'a> { - Absorb(&'a [u8]), + Send(&'a [u8]), + Public(&'a [u8]), Challenge, } +impl Op<'_> { + fn absorbed(&self) -> usize { + match self { + Op::Send(bytes) | Op::Public(bytes) => bytes.len(), + Op::Challenge => 0, + } + } +} + fn parse_ops(data: &[u8]) -> Vec> { let mut ops = Vec::new(); let mut cursor = 0; while cursor < data.len() && ops.len() < MAX_OPS { let tag = data[cursor]; cursor += 1; - if tag % 4 == 0 { + if tag % 3 == 0 { ops.push(Op::Challenge); - } else { - let len = tag as usize % 33; // 0..=32 bytes per absorb - if cursor + len > data.len() { - break; - } - ops.push(Op::Absorb(&data[cursor..cursor + len])); - cursor += len; + continue; } + let len = tag as usize % (MAX_MESSAGE + 1); + if cursor + len > data.len() { + break; + } + let bytes = &data[cursor..cursor + len]; + cursor += len; + ops.push(if tag % 3 == 1 { + Op::Send(bytes) + } else { + Op::Public(bytes) + }); } ops } -/// Replays `ops`, optionally XOR-ing `mask` into the absorbed byte at global -/// offset `position`. Returns the challenge stream and the final state. -fn run(ops: &[Op<'_>], flip: Option<(usize, u8)>) -> (Vec, [u8; 32]) { - let mut transcript = T::new(b"fuzz-determinism"); +/// The challenge stream, argument string, and final state of one replay of +/// `ops`, optionally XOR-ing `mask` into the absorbed byte at global offset +/// `position`. +fn run( + ops: &[Op<'_>], + flip: Option<(usize, u8)>, +) -> (Vec<[u8; 32]>, Vec, [u8; 32]) { + let mut transcript = + ProverTranscript::::new(&ProtocolId::new::("fuzz-determinism"), b""); let mut challenges = Vec::new(); let mut absorbed = 0usize; for op in ops { - match op { - Op::Absorb(chunk) => { - match flip { - Some((position, mask)) - if (absorbed..absorbed + chunk.len()).contains(&position) => - { - let mut flipped = chunk.to_vec(); - flipped[position - absorbed] ^= mask; - transcript.append_bytes(&flipped); - } - _ => transcript.append_bytes(chunk), - } - absorbed += chunk.len(); + let mut bytes = match op { + Op::Send(bytes) | Op::Public(bytes) => bytes.to_vec(), + Op::Challenge => { + challenges.push(transcript.challenge_bytes()); + continue; + } + }; + if let Some((position, mask)) = flip { + if (absorbed..absorbed + bytes.len()).contains(&position) { + bytes[position - absorbed] ^= mask; } - Op::Challenge => challenges.push(transcript.challenge()), + } + absorbed += bytes.len(); + match op { + Op::Send(_) => transcript + .send_bounded_bytes(&bytes, MAX_MESSAGE) + .expect("messages are within the bound"), + Op::Public(_) => transcript.public_bytes(&bytes), + Op::Challenge => {} } } - (challenges, transcript.state()) + let state = transcript.preview().squeeze(); + (challenges, transcript.finish(), state) } -fn check(ops: &[Op<'_>], total_absorbed: usize, position: usize, mask: u8) -where - T::Challenge: PartialEq, -{ - let (challenges_a, state_a) = run::(ops, None); - let (challenges_b, state_b) = run::(ops, None); - assert!( - challenges_a == challenges_b && state_a == state_b, - "transcript replay is nondeterministic" - ); +fn check(ops: &[Op<'_>], total_absorbed: usize, position: usize, mask: u8) { + let first = run::(ops, None); + let second = run::(ops, None); + assert!(first == second, "transcript replay is nondeterministic"); if total_absorbed > 0 && mask != 0 { - let (_, state_flipped) = run::(ops, Some((position % total_absorbed, mask))); + let (_, _, flipped_state) = run::(ops, Some((position % total_absorbed, mask))); assert_ne!( - state_a, state_flipped, + first.2, flipped_state, "flipping an absorbed byte did not change the transcript state" ); } @@ -93,15 +114,9 @@ fuzz_target!(|data: &[u8]| { let position = data[0] as usize; let mask = data[1]; let ops = parse_ops(&data[2..]); - let total_absorbed = ops - .iter() - .map(|op| match op { - Op::Absorb(chunk) => chunk.len(), - Op::Challenge => 0, - }) - .sum(); + let total_absorbed = ops.iter().map(Op::absorbed).sum(); - check::(&ops, total_absorbed, position, mask); - check::(&ops, total_absorbed, position, mask); - check::(&ops, total_absorbed, position, mask); + check::(&ops, total_absorbed, position, mask); + check::(&ops, total_absorbed, position, mask); + check::(&ops, total_absorbed, position, mask); }); diff --git a/crates/jolt-transcript/tests/known_answers.rs b/crates/jolt-transcript/tests/known_answers.rs new file mode 100644 index 0000000000..cee86da8a6 --- /dev/null +++ b/crates/jolt-transcript/tests/known_answers.rs @@ -0,0 +1,101 @@ +//! Frozen known-answer vectors, one per sponge: a fixed script exercising every +//! message kind, challenge kind, and grinding. Any change to the protocol-id +//! derivation, framing, encoding, or challenge mapping changes these bytes. + +#![cfg(all( + feature = "transcript-blake2b", + feature = "transcript-keccak", + feature = "transcript-poseidon" +))] +#![expect(clippy::unwrap_used, reason = "test crate")] + +use jolt_field::{CanonicalBytes, Fr}; +use jolt_transcript::{ + Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, + VerifierTranscript, +}; + +/// The script's argument string and the bytes of every challenge it draws. +fn run() -> (Vec, Vec) { + let protocol = ProtocolId::new::("jolt-transcript/known-answers"); + let mut prover = ProverTranscript::::new(&protocol, b"session"); + let mut challenges = Vec::new(); + prover.public_bytes(b"public input"); + prover.public(&Fr::from(5u64)); + prover.send(&Fr::from(7u64)); + prover.send_all(&[11u64, 13]); + prover.send_bounded_bytes(b"variable", 16).unwrap(); + challenges.extend(prover.challenge::().to_bytes_le_vec()); + challenges.extend(prover.challenge_small::().to_bytes_le_vec()); + let _nonce = prover.grind(6).unwrap(); + challenges.extend(prover.challenge_bytes::<32>()); + let narg = prover.finish(); + + let mut verifier = VerifierTranscript::::new(&protocol, b"session", &narg); + let mut replayed = Vec::new(); + verifier.public_bytes(b"public input"); + verifier.public(&Fr::from(5u64)); + assert_eq!(verifier.receive::().unwrap(), Fr::from(7u64)); + assert_eq!(verifier.receive_n::(2).unwrap(), vec![11, 13]); + assert_eq!(verifier.receive_bounded_bytes(16).unwrap(), b"variable"); + replayed.extend(verifier.challenge::().to_bytes_le_vec()); + replayed.extend(verifier.challenge_small::().to_bytes_le_vec()); + let _nonce = verifier.check_grind(6).unwrap(); + replayed.extend(verifier.challenge_bytes::<32>()); + verifier.finish().unwrap(); + assert_eq!(replayed, challenges); + + (narg, challenges) +} + +fn hex(bytes: &[u8]) -> String { + use std::fmt::Write; + bytes.iter().fold(String::new(), |mut out, byte| { + let _ = write!(out, "{byte:02x}"); + out + }) +} + +fn assert_vectors(name: &str, (narg, challenges): (Vec, Vec), expected: (&str, &str)) { + assert_eq!(hex(&narg), expected.0, "{name} argument string"); + assert_eq!(hex(&challenges), expected.1, "{name} challenges"); +} + +/// The argument string differs across sponges only in its grinding nonce. +const NARG_PREFIX: &str = "07000000000000000000000000000000000000000000000000000000000000000b000000000000000d00000000000000080000007661726961626c65"; + +#[test] +fn blake2b_known_answers() { + assert_vectors( + "blake2b", + run::(), + ( + &format!("{NARG_PREFIX}ad01"), + "3d4e089306afb69df886095eee15fbf14517c4ff924ec6bfd09265fc6a8a60054bb2626cc97cccb74e359d255207da9e39f4eee144c71404fe870f184ac4e31b422aaeb6c1a4720024b98c7f60fe220acf9f414231a6d2fd4dea709ae2c4eb14", + ), + ); +} + +#[test] +fn keccak_known_answers() { + assert_vectors( + "keccak", + run::(), + ( + &format!("{NARG_PREFIX}8101"), + "87b238e77e1881d46c35f627091e902ad1936d867b6c2fa7765547f44db00a230329d92cd97029872e99afbf8b9bdd57c59c13e503303e0c10e5a79fc642e11224bf861e6ff0b9bfa0c72d5ebcfe46562d97d675ea1b3814a87f1ebcaf118eb3", + ), + ); +} + +#[test] +fn poseidon_known_answers() { + assert_vectors( + "poseidon", + run::(), + ( + &format!("{NARG_PREFIX}49"), + "0be942b2a23ad3aa6fd8abf1580f85e78b2df6d4bf46d6564202c4f16cf2f022ce5db4a5e3f7288c0b37e0912b8214568272d5160be2592b4c87e9a74c374304fc9b0b01515cf511c4480ab9d67d3b1b88dba661536bf44d7b380cf04dd32604", + ), + ); +} From 09f76b1956bb5beee4ffe355a84bce08714d55a2 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:20:10 -0400 Subject: [PATCH 21/62] refactor(akita)!: open Akita inside the Jolt 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. --- Cargo.toml | 8 +- book/src/how/architecture/opening-proof.md | 2 +- crates/jolt-akita/Cargo.toml | 2 +- crates/jolt-akita/src/adapters.rs | 271 +++++++++------- crates/jolt-akita/src/lib.rs | 8 +- crates/jolt-akita/src/native_batching.rs | 293 ++++++------------ crates/jolt-akita/src/planning.rs | 4 +- crates/jolt-akita/src/schedule_registry.rs | 8 +- crates/jolt-akita/src/schedules/mod.rs | 4 +- crates/jolt-akita/src/scheme.rs | 242 +++++---------- crates/jolt-akita/src/shape_guard.rs | 4 +- crates/jolt-akita/src/test_transcripts.rs | 32 ++ crates/jolt-akita/tests/e2e.rs | 47 +-- crates/jolt-akita/tests/fold_paths.rs | 77 ++--- crates/jolt-akita/tests/grouped_capacity.rs | 21 +- crates/jolt-akita/tests/native_batching.rs | 70 ++--- crates/jolt-akita/tests/one_hot_groups.rs | 21 +- crates/jolt-akita/tests/pathologies.rs | 140 +++++---- crates/jolt-akita/tests/prefix_packing.rs | 29 +- crates/jolt-akita/tests/schedules.rs | 24 +- crates/jolt-akita/tests/scheme_surface.rs | 64 ++-- crates/jolt-akita/tests/support/mod.rs | 29 ++ crates/jolt-dory/Cargo.toml | 2 +- crates/jolt-dory/benches/akita_paths.rs | 66 ++-- crates/jolt-prover/src/akita/mod.rs | 21 +- crates/jolt-prover/src/akita/preprocessing.rs | 2 - crates/jolt-prover/src/akita/prover.rs | 76 ++--- crates/jolt-prover/src/akita/stage0.rs | 84 ++--- crates/jolt-prover/src/akita/stage8.rs | 23 +- crates/jolt-prover/src/profile.rs | 15 +- crates/jolt-prover/tests/akita_e2e.rs | 125 ++++---- .../tests/akita_field_inline_e2e.rs | 171 ++++++---- crates/jolt-prover/tests/e2e_matrix.rs | 7 +- .../tests/support/field_inline/akita.rs | 10 +- 34 files changed, 988 insertions(+), 1014 deletions(-) create mode 100644 crates/jolt-akita/src/test_transcripts.rs diff --git a/Cargo.toml b/Cargo.toml index 5be21be5ba..e23d73cce0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -198,12 +198,14 @@ ark-ff = { git = "https://github.com/a16z/arkworks-algebra", branch = "dev/twist ark-ec = { git = "https://github.com/a16z/arkworks-algebra", branch = "dev/twist-shout" } ark-serialize = { git = "https://github.com/a16z/arkworks-algebra", branch = "dev/twist-shout" } -# The refreshed Akita cutover consumes jolt-field from this exact Git source. -# Redirect that package to the workspace member so Cargo resolves one shared -# field identity rather than separate path and Git copies. +# Akita consumes jolt-field, jolt-poly, and jolt-transcript from this exact +# Git source. Redirect those packages to the workspace members so Cargo +# resolves one shared identity for each rather than separate path and Git +# copies. [patch."https://github.com/a16z/jolt"] jolt-field = { path = "crates/jolt-field" } jolt-poly = { path = "crates/jolt-poly" } +jolt-transcript = { path = "crates/jolt-transcript" } [workspace.metadata.cargo-machete] ignored = ["jolt-sdk"] diff --git a/book/src/how/architecture/opening-proof.md b/book/src/how/architecture/opening-proof.md index 109880e4b9..64d8ff791d 100644 --- a/book/src/how/architecture/opening-proof.md +++ b/book/src/how/architecture/opening-proof.md @@ -41,7 +41,7 @@ A claim reduction sumcheck takes multiple polynomial evaluation claims, potentia Akita commits the trace as one physical polynomial, `OneHotTrace`, with a fixed-capacity prefix selecting among its logical one-hot columns. The committed variable order is `(slot || cycle || address)`. After the preceding stages produce column evaluations at a common `(cycle || address)` point, Stage 8 binds those evaluations and their layout to the transcript, samples the slot selector, and reduces them to one evaluation of `OneHotTrace`. This [prefix-packing reduction](../optimizations/batched-openings.md#prefix-packing-and-native-grouped-openings-akita) does not require a homomorphic combination of commitments. -Advice and committed-program data are independent dense commitment objects: optional untrusted and trusted advice, one `BytecodeChunk(i)` per committed bytecode chunk, and a `ProgramImageInit` object. Stage 8 reduces the claims for each object separately, then opens all present objects together with `OneHotTrace` in one native Akita proof. Each group retains its own shape and opening point. The canonical order is untrusted advice, trusted advice, bytecode chunks, program image, and finally `OneHotTrace`, omitting absent objects. Group roles, commitments, points, and evaluations are bound to the transcript before the backend proof. +Advice and committed-program data are independent dense commitment objects: optional untrusted and trusted advice, one `BytecodeChunk(i)` per committed bytecode chunk, and a `ProgramImageInit` object. Stage 8 reduces the claims for each object separately, then opens all present objects together with `OneHotTrace` in one native Akita proof. Each group retains its own shape and opening point. The canonical order is untrusted advice, trusted advice, bytecode chunks, program image, and finally `OneHotTrace`, omitting absent objects. Akita runs on Jolt's transcript and binds every group's commitment, point, and evaluations before its own messages, so its opening proof is part of Jolt's argument string. The modular prover implements this in `crates/jolt-prover/src/akita/stage8.rs`, with the verifier counterpart in `crates/jolt-verifier/src/stages/stage8/packed.rs`. `AkitaNativeBatching` in `crates/jolt-akita/src/native_batching.rs` validates the groups and invokes the native backend opening. The Dory matrix embeddings described below apply only to the Dory backend. diff --git a/crates/jolt-akita/Cargo.toml b/crates/jolt-akita/Cargo.toml index d1f958688b..3399080479 100644 --- a/crates/jolt-akita/Cargo.toml +++ b/crates/jolt-akita/Cargo.toml @@ -19,7 +19,7 @@ profiling = [] akita-algebra.workspace = true akita-config.workspace = true akita-error.workspace = true -akita-pcs = { workspace = true, features = ["parallel", "transcript-blake2b"] } +akita-pcs = { workspace = true, features = ["parallel"] } akita-challenges.workspace = true akita-planner.workspace = true akita-schedules.workspace = true diff --git a/crates/jolt-akita/src/adapters.rs b/crates/jolt-akita/src/adapters.rs index 9c2d62ec2e..6bdc52fa7d 100644 --- a/crates/jolt-akita/src/adapters.rs +++ b/crates/jolt-akita/src/adapters.rs @@ -20,10 +20,10 @@ use akita_types::{ AkitaVerifierSetup as BackendVerifierSetup, Commitment as AkitaBackendRingCommitment, CommittedGroup as AkitaBackendCommittedGroup, OpeningScheduleSelection, ScheduleRowDigest, }; -use jolt_field::{CanonicalBytes, Zero}; -use jolt_openings::{OpeningsError, VerifierOpeningClaim}; +use jolt_field::{CanonicalBytes, CanonicalDecode, Zero}; +use jolt_openings::OpeningsError; use jolt_poly::{MultilinearPoly, OneHotIndexOrder, OneHotPolynomial, Polynomial}; -use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript, U64Word}; +use jolt_transcript::{Channel, ProverTranscript, Sponge, TranscriptError, VerifierTranscript}; use rayon::{ThreadPool, ThreadPoolBuilder}; use serde::{Deserialize, Serialize}; @@ -195,7 +195,6 @@ pub(crate) type AkitaBackend = CpuBackend; pub(crate) type AkitaBackendProverSetup = BackendProverSetup; pub(crate) type AkitaLayoutDigest = [u8; 32]; -const SCHEDULE_SELECTION_BYTES: usize = 32; /// Worker stack size for [`with_backend_pool`]. Stacks are lazily committed, /// so oversizing costs virtual address space only. @@ -777,23 +776,16 @@ impl PartialEq for BackendVerifierCache { impl Eq for BackendVerifierCache {} -/// Binds one backend flavor's setup identity into the transcript. The backend -/// key is determined by the absorbed dimensions and admitted catalog; binding -/// the validated catalog digest avoids hashing the large serialized key while -/// preventing cross-catalog replay. -pub(crate) fn append_verifier_setup( - transcript: &mut T, +/// Absorbs the identity of the schedule catalog `flavor` admits under `setup`. +/// +/// Akita's instance descriptor binds the setup seed and the selected row, but +/// not the catalog the verifier resolved that row in; this digest pins it, so +/// a proof never replays across verifier setups with different catalogs. +pub(crate) fn absorb_setup_catalog( + channel: &mut C, setup: &AkitaVerifierSetup, flavor: AkitaBackendFlavor, ) -> Result<(), OpeningsError> { - transcript.append(&Label(b"akita_setup_key")); - transcript.append_bytes(b"akita/fp128"); - transcript.append_bytes(flavor.transcript_label()); - transcript.append(&U64Word(setup.max_num_vars as u64)); - transcript.append(&U64Word(setup.max_num_polys_per_commitment_group as u64)); - transcript.append(&U64Word(setup.max_total_batch_polys as u64)); - transcript.append(&U64Word(setup.one_hot_k as u64)); - transcript.append_bytes(&setup.default_layout_digest); let catalog_digest = match flavor { AkitaBackendFlavor::Dense => setup.dense_scheme()?.schedules().catalog_digest(), AkitaBackendFlavor::OneHot => match setup.one_hot_k { @@ -806,26 +798,26 @@ pub(crate) fn append_verifier_setup( } }, }; - transcript.append_bytes(&catalog_digest); + channel.public(&catalog_digest); Ok(()) } -/// Binds the batch statement (commitment group, point, per-claim data) into -/// the transcript. -pub(crate) fn append_batch_statement( - transcript: &mut T, - statement: &[VerifierOpeningClaim], - commitment: &AkitaCommitment, - point: &[AkitaField], +/// Sends the schedule row the prover selected. The verifier needs it to +/// resolve the commitment profiles before Akita's own messages. +pub(crate) fn send_selection( + transcript: &mut ProverTranscript, + selection: OpeningScheduleSelection, ) { - transcript.append(&Label(b"akita_batch_statement")); - commitment.append_to_transcript(transcript); - transcript.append_values(b"akita_pcs_point", point); - transcript.append(&LabelWithCount(b"akita_claims", statement.len() as u64)); - for claim in statement { - claim.commitment.append_to_transcript(transcript); - claim.evaluation.value.append_to_transcript(transcript); - } + transcript.send(selection.row_digest.as_bytes()); +} + +/// Receives the schedule row written by [`send_selection`]. +pub(crate) fn receive_selection( + transcript: &mut VerifierTranscript<'_, H>, +) -> Result { + Ok(OpeningScheduleSelection { + row_digest: ScheduleRowDigest::from_bytes(transcript.receive()?), + }) } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -837,10 +829,18 @@ pub enum AkitaBackendFlavor { } impl AkitaBackendFlavor { - pub(crate) const fn transcript_label(self) -> &'static [u8] { + const fn wire_tag(self) -> u8 { match self { - Self::Dense => b"dense", - Self::OneHot => b"one_hot", + Self::Dense => 0, + Self::OneHot => 1, + } + } + + const fn from_wire_tag(tag: u8) -> Option { + match tag { + 0 => Some(Self::Dense), + 1 => Some(Self::OneHot), + _ => None, } } } @@ -947,79 +947,154 @@ impl AkitaCommitment { } } -impl AppendToTranscript for AkitaCommitment { - fn append_to_transcript(&self, transcript: &mut T) { - transcript.append(&Label(b"akita_commitment")); - transcript.append_bytes(self.backend_flavor.transcript_label()); - transcript.append_bytes(&self.layout_digest); - transcript.append(&U64Word(self.num_vars as u64)); - transcript.append(&U64Word(self.poly_count as u64)); - transcript.append(&U64Word(self.one_hot_k as u64)); - transcript.append(&U64Word(self.backend_coeff_len as u64)); - transcript.append(&LabelWithCount( - b"akita_commitment_bytes", - self.serialized_backend_bytes.len() as u64, - )); - transcript.append_bytes(&self.serialized_backend_bytes); +impl AkitaCommitment { + fn header(&self) -> CommitmentHeader { + CommitmentHeader { + backend_flavor: self.backend_flavor, + layout_digest: self.layout_digest, + num_vars: self.num_vars, + poly_count: self.poly_count, + one_hot_k: self.one_hot_k, + backend_coeff_len: self.backend_coeff_len, + } + } + + /// Writes the commitment as a prover message: the fixed-width header, + /// then exactly the backend coefficient bytes it sizes. + pub(crate) fn send(&self, transcript: &mut ProverTranscript) { + transcript.send(&self.header()); + transcript.send_bytes(&self.serialized_backend_bytes); + } + + /// Reads a commitment written by [`send`](Self::send). The header and + /// every backend coefficient decode canonically; the payload is read only + /// after its declared length fits the remaining proof. + pub(crate) fn receive( + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + let header: CommitmentHeader = transcript.receive()?; + let payload_len = header + .backend_coeff_len + .checked_mul(::NUM_BYTES) + .ok_or(TranscriptError::OutOfBounds)?; + let payload = transcript.receive_bytes(payload_len)?; + let _: AkitaBackendCommitmentPayload = + deserialize_akita(payload, &header.backend_coeff_len)?; + Ok(Self { + backend_flavor: header.backend_flavor, + layout_digest: header.layout_digest, + num_vars: header.num_vars, + poly_count: header.poly_count, + one_hot_k: header.one_hot_k, + backend_coeff_len: header.backend_coeff_len, + serialized_backend_bytes: payload.to_vec(), + }) + } + + /// Absorbs a commitment both sides hold. + pub(crate) fn absorb(&self, channel: &mut C) { + channel.public(&self.header()); + channel.public_bytes(&self.serialized_backend_bytes); } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct AkitaBatchProof { - /// Fixed-width public identity of the exact generated row selected by the - /// prover. The verifier resolves this digest under its configured catalog; - /// the backend proof body does not encode the selection itself. - pub(crate) schedule_selection: [u8; SCHEDULE_SELECTION_BYTES], - pub(crate) backend_proof: Vec, +/// Fixed-width prefix of a commitment's wire form: its metadata and the +/// backend coefficient count that sizes the payload after it. +struct CommitmentHeader { + backend_flavor: AkitaBackendFlavor, + layout_digest: AkitaLayoutDigest, + num_vars: usize, + poly_count: usize, + one_hot_k: usize, + backend_coeff_len: usize, } -impl AkitaBatchProof { - pub(crate) fn new(selection: OpeningScheduleSelection, backend_proof: Vec) -> Self { - Self { - schedule_selection: *selection.row_digest.as_bytes(), - backend_proof, - } - } +impl CommitmentHeader { + const DIGEST_BYTES: usize = 32; + const WORDS: usize = 4; - pub(crate) fn selection(&self) -> OpeningScheduleSelection { - OpeningScheduleSelection { - row_digest: ScheduleRowDigest::from_bytes(self.schedule_selection), - } + fn words(&self) -> [usize; Self::WORDS] { + [ + self.num_vars, + self.poly_count, + self.one_hot_k, + self.backend_coeff_len, + ] } +} + +impl CanonicalBytes for CommitmentHeader { + const NUM_BYTES: usize = 1 + Self::DIGEST_BYTES + Self::WORDS * u64::NUM_BYTES; - /// Headerless backend proof body: Akita's Spongefish argument bytes. - pub fn backend_proof_body_size(&self) -> usize { - self.backend_proof.len() + fn to_bytes_le(&self, out: &mut [u8]) { + let (tag, rest) = out.split_at_mut(1); + tag.copy_from_slice(&[self.backend_flavor.wire_tag()]); + let (digest, words) = rest.split_at_mut(Self::DIGEST_BYTES); + digest.copy_from_slice(&self.layout_digest); + for (out, word) in words.chunks_exact_mut(u64::NUM_BYTES).zip(self.words()) { + (word as u64).to_bytes_le(out); + } } +} - /// Sum of the raw component bytes before the enclosing Jolt serializer - /// adds container tags or length prefixes. - pub fn unframed_payload_size(&self) -> Option { - SCHEDULE_SELECTION_BYTES.checked_add(self.backend_proof.len()) +impl CanonicalDecode for CommitmentHeader { + /// Accepts only headers [`AkitaScheme`](crate::AkitaScheme) can produce: a + /// known flavor, word values that fit `usize`, and the flavor's one-hot + /// chunk size (zero for dense, 16 or 256 for one-hot). + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + if bytes.len() != Self::NUM_BYTES { + return None; + } + let (&tag, rest) = bytes.split_first()?; + let (digest, words) = rest.split_first_chunk::<{ Self::DIGEST_BYTES }>()?; + let mut words = words.chunks_exact(u64::NUM_BYTES).map(|word| { + u64::from_bytes_le_checked(word).and_then(|word| usize::try_from(word).ok()) + }); + let mut next = || words.next().flatten(); + let header = Self { + backend_flavor: AkitaBackendFlavor::from_wire_tag(tag)?, + layout_digest: *digest, + num_vars: next()?, + poly_count: next()?, + one_hot_k: next()?, + backend_coeff_len: next()?, + }; + let one_hot_k_matches = match header.backend_flavor { + AkitaBackendFlavor::Dense => header.one_hot_k == 0, + AkitaBackendFlavor::OneHot => validate_one_hot_k(header.one_hot_k).is_ok(), + }; + one_hot_k_matches.then_some(header) } } +/// The transparent stand-in for a hiding evaluation commitment: the +/// evaluation's canonical encoding. Akita has no ZK opening, so nothing +/// verifies against it. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AkitaHidingCommitment { - pub(crate) eval: Vec, + pub(crate) eval: [u8; ::NUM_BYTES], } impl AkitaHidingCommitment { - pub(crate) fn new(eval: Vec) -> Self { - Self { eval } + pub(crate) fn new(eval: AkitaField) -> Self { + let mut bytes = [0; ::NUM_BYTES]; + eval.to_bytes_le(&mut bytes); + Self { eval: bytes } } } -impl AppendToTranscript for AkitaHidingCommitment { - fn append_to_transcript(&self, transcript: &mut T) { - transcript.append(&Label(b"akita_hiding_commitment")); - transcript.append(&LabelWithCount( - b"akita_hiding_eval", - self.eval.len() as u64, - )); - transcript.append_bytes(&self.eval); +impl CanonicalBytes for AkitaHidingCommitment { + const NUM_BYTES: usize = ::NUM_BYTES; + + fn to_bytes_le(&self, out: &mut [u8]) { + out.copy_from_slice(&self.eval); + } +} + +impl CanonicalDecode for AkitaHidingCommitment { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + AkitaField::from_bytes_le_checked(bytes).map(Self::new) } } @@ -1315,26 +1390,6 @@ pub(crate) fn transparent_zk_error() -> OpeningsError { ) } -/// Ends outer Jolt challenge derivation at one statement-bound challenge and -/// uses it to domain-separate the nested Akita argument's session. No -/// subsequent Jolt challenge consumes the terminal opening proof, so -/// reabsorbing that proof into the outer transcript could not affect -/// acceptance. -pub(crate) fn bridged_akita_session(jolt_transcript: &mut T, session_label: &[u8]) -> Vec -where - T: Transcript, -{ - let bridge = jolt_transcript.challenge_scalar(); - let bridge_bytes = bridge.to_bytes_le_vec(); - // Akita binds the concrete instance into its own Fiat-Shamir state but - // keeps the session bytes as domain separator, so the cross-protocol - // bridge belongs here. - let mut bridged_session = Vec::with_capacity(session_label.len() + bridge_bytes.len()); - bridged_session.extend_from_slice(session_label); - bridged_session.extend_from_slice(&bridge_bytes); - bridged_session -} - #[cfg(test)] mod tests { #![expect( diff --git a/crates/jolt-akita/src/lib.rs b/crates/jolt-akita/src/lib.rs index ab5fabe300..4f5a607c43 100644 --- a/crates/jolt-akita/src/lib.rs +++ b/crates/jolt-akita/src/lib.rs @@ -28,6 +28,8 @@ pub mod schedule_registry; pub mod schedules; mod scheme; mod shape_guard; +#[cfg(test)] +mod test_transcripts; mod trace_onehot; pub use akita_pcs::AkitaError; @@ -39,9 +41,9 @@ pub use adapters::{ with_single_threaded_verifier_backend, }; pub use adapters::{ - AkitaBackendFlavor, AkitaBatchProof, AkitaCommitment, AkitaField, AkitaHidingCommitment, - AkitaProverHint, AkitaProverSetup, AkitaScheduleArtifacts, AkitaSetupParams, - AkitaVerifierSetup, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, + AkitaBackendFlavor, AkitaCommitment, AkitaField, AkitaHidingCommitment, AkitaProverHint, + AkitaProverSetup, AkitaScheduleArtifacts, AkitaSetupParams, AkitaVerifierSetup, + AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, }; pub use native_batching::{ AkitaNativeBatchPolynomials, AkitaNativeBatchStatement, AkitaNativeBatching, diff --git a/crates/jolt-akita/src/native_batching.rs b/crates/jolt-akita/src/native_batching.rs index ce5d88124d..681072f22b 100644 --- a/crates/jolt-akita/src/native_batching.rs +++ b/crates/jolt-akita/src/native_batching.rs @@ -12,8 +12,15 @@ //! backend proof. //! //! This adapter performs no claim combination of its own — it validates the -//! statement shape, bridges Jolt's Fiat-Shamir transcript into Akita's -//! session, and embeds the backend argument bytes wholesale. +//! statement shape and runs Akita's opening on the caller's transcript, so +//! Akita's messages land in the Jolt argument string. +//! +//! Before Akita's own messages, each opening absorbs the digest of the +//! verifier's schedule catalog and sends the schedule row the prover selected. +//! Akita then absorbs its instance descriptor (setup seed, selected row, +//! opening layout, basis), every group's backend commitment and point, and the +//! claimed evaluations. The Jolt-side commitment metadata is bound by the +//! caller, which sent or absorbed every commitment before stage 8. use akita_config::{CommitmentConfig, TrustedScheduleCatalog}; use akita_pcs::{AkitaError, SelectedProverOpeningData}; @@ -25,16 +32,15 @@ use jolt_openings::{ TaggedGroupOpeningClaim, VerifierOpeningClaim, }; use jolt_poly::MultilinearPoly; -use jolt_transcript::{AppendToTranscript, Label, LabelWithCount, Transcript, U64Word}; +use jolt_transcript::{ProverTranscript, Sponge, VerifierTranscript}; use tracing::info_span; use crate::adapters::{ - akita_error, append_batch_statement, append_verifier_setup, bridged_akita_session, - invalid_batch, prove_failed, reverse_point, serialize_akita, validate_one_hot_k, - with_backend_pool, AkitaBackendCommitment, AkitaBackendExtField, AkitaBackendFlavor, - AkitaBackendHint, AkitaBatchProof, AkitaCommitment, AkitaConfig, AkitaField, AkitaHintSource, - AkitaOneHotK16Config, AkitaOneHotK256Config, AkitaProverHint, AkitaProverSetup, - AkitaVerifierSetup, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, + absorb_setup_catalog, akita_error, invalid_batch, prove_failed, receive_selection, + reverse_point, send_selection, validate_one_hot_k, with_backend_pool, AkitaBackendCommitment, + AkitaBackendExtField, AkitaBackendFlavor, AkitaBackendHint, AkitaCommitment, AkitaConfig, + AkitaField, AkitaHintSource, AkitaOneHotK16Config, AkitaOneHotK256Config, AkitaProverHint, + AkitaProverSetup, AkitaVerifierSetup, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, }; use crate::scheme::validate_group_order; @@ -150,68 +156,40 @@ fn validate_trace_batch_statement( Ok(()) } -/// Binds the grouped statement into Jolt's transcript and bridges a Jolt -/// challenge into the Akita session bytes. -fn bind_grouped_statement_transcripts( - transcript: &mut T, +/// Prover side of the binding that precedes Akita's messages: the verifier's +/// catalog identity, then the selected schedule row. +fn bind_prover_opening( + transcript: &mut ProverTranscript, setup: &AkitaVerifierSetup, + flavor: AkitaBackendFlavor, selection: OpeningScheduleSelection, - auxiliary_groups: &[TaggedGroupOpeningClaim], - main: &GroupOpeningClaim, -) -> Result, OpeningsError> -where - T: Transcript, -{ - append_verifier_setup(transcript, setup, AkitaBackendFlavor::OneHot)?; - transcript.append(&Label(b"akita_precommit_batch_v3")); - transcript.append_bytes(&serialize_akita(&selection)?); - let group_count = auxiliary_groups - .len() - .checked_add(1) - .ok_or_else(|| invalid_batch("Akita grouped statement group count overflows"))?; - transcript.append(&LabelWithCount(b"akita_groups", group_count as u64)); - let groups = auxiliary_groups - .iter() - .map(|entry| (Some(entry.role), &entry.claim)) - .chain(std::iter::once((None, main))); - for (index, (role, claim)) in groups.enumerate() { - transcript.append(&U64Word(index as u64)); - if let Some(role) = role { - transcript.append_bytes(role.transcript_label()); - if let Some(role_index) = role.transcript_index() { - transcript.append(&U64Word(role_index)); - } - } else { - transcript.append_bytes(b"main_trace"); - } - transcript.append(&U64Word(u64::from(role.is_some()))); - claim.commitment.append_to_transcript(transcript); - transcript.append_values(b"akita_group_point", &claim.point); - transcript.append(&LabelWithCount( - b"akita_group_evaluations", - claim.evaluations.len() as u64, - )); - for evaluation in &claim.evaluations { - evaluation.append_to_transcript(transcript); - } - } - Ok(bridged_akita_session( - transcript, - b"jolt-akita/precommitted-group-batch/v3", - )) +) -> Result<(), OpeningsError> { + absorb_setup_catalog(transcript, setup, flavor)?; + send_selection(transcript, selection); + Ok(()) +} + +/// Verifier side of [`bind_prover_opening`]; returns the received row. +fn bind_verifier_opening( + transcript: &mut VerifierTranscript<'_, H>, + setup: &AkitaVerifierSetup, + flavor: AkitaBackendFlavor, +) -> Result { + absorb_setup_catalog(transcript, setup, flavor)?; + receive_selection(transcript) } /// Runs the one-hot backend prover for an opening whose final group was /// committed under the setup's K-specific scheme. -fn prove_one_hot_opening( +fn prove_one_hot_opening( setup: &AkitaProverSetup, opening: AkitaOpening<'_>, - session: &[u8], -) -> Result, OpeningsError> { + transcript: &mut ProverTranscript, +) -> Result<(), OpeningsError> { let (backend_prover_setup, backend) = setup.one_hot_backend()?; let _span = info_span!("AkitaNativeBatching::backend_batched_prove").entered(); with_backend_pool(|| { - let proof = match setup.one_hot_k() { + match setup.one_hot_k() { AKITA_ONE_HOT_K16 => setup .verifier .one_hot_k16_scheme() @@ -220,7 +198,7 @@ fn prove_one_hot_opening( backend_prover_setup, opening, backend, - session, + transcript, BasisMode::Lagrange, ), AKITA_ONE_HOT_K256 => setup @@ -231,7 +209,7 @@ fn prove_one_hot_opening( backend_prover_setup, opening, backend, - session, + transcript, BasisMode::Lagrange, ), _ => unreachable!("the one-hot setup geometry was validated during setup"), @@ -239,40 +217,29 @@ fn prove_one_hot_opening( // Akita never trims its NTT caches itself; drop the root and suffix // transforms built for this proof instead of keeping them resident. let _ = backend.trim_caches()?; - Ok::<_, AkitaError>(proof) + Ok::<_, AkitaError>(()) }) .map_err(prove_failed) } /// Replays the one-hot backend verifier for `statement` under the setup's /// K-specific catalog. -fn verify_one_hot_statement( +fn verify_one_hot_statement( setup: &AkitaVerifierSetup, - proof: &AkitaBatchProof, - session: &[u8], + transcript: &mut VerifierTranscript<'_, H>, statement: GroupBatchStatement<'_, AkitaBackendExtField, AkitaField>, ) -> Result<(), OpeningsError> { let verified = match setup.one_hot_k { AKITA_ONE_HOT_K16 => { let verifier = setup.one_hot_k16_verifier()?; with_backend_pool(|| { - verifier.batched_verify( - &proof.backend_proof, - session, - statement, - BasisMode::Lagrange, - ) + verifier.batched_verify(transcript, statement, BasisMode::Lagrange) }) } AKITA_ONE_HOT_K256 => { let verifier = setup.one_hot_k256_verifier()?; with_backend_pool(|| { - verifier.batched_verify( - &proof.backend_proof, - session, - statement, - BasisMode::Lagrange, - ) + verifier.batched_verify(transcript, statement, BasisMode::Lagrange) }) } other => { @@ -285,16 +252,13 @@ fn verify_one_hot_statement( } impl AkitaNativeBatching { - pub(crate) fn prove_trace_batch( + pub(crate) fn prove_trace_batch( setup: &AkitaProverSetup, auxiliary_groups: Vec>, main: GroupOpeningClaim, main_hint: AkitaProverHint, - transcript: &mut T, - ) -> Result - where - T: Transcript, - { + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { let auxiliary_claims = auxiliary_groups .iter() .map(|(entry, _)| entry.clone()) @@ -386,29 +350,23 @@ impl AkitaNativeBatching { _ => unreachable!("one-hot K was validated by setup"), } .map_err(akita_error)?; - let selection = opening.selection(); - let session = bind_grouped_statement_transcripts( + bind_prover_opening( transcript, &setup.verifier, - selection, - &auxiliary_claims, - &main, + AkitaBackendFlavor::OneHot, + opening.selection(), )?; - let backend_proof = prove_one_hot_opening(setup, opening, &session)?; - Ok(AkitaBatchProof::new(selection, backend_proof)) + prove_one_hot_opening(setup, opening, transcript) } - pub(crate) fn verify_trace_batch( + pub(crate) fn verify_trace_batch( setup: &AkitaVerifierSetup, auxiliary_groups: &[TaggedGroupOpeningClaim], main: &GroupOpeningClaim, - proof: &AkitaBatchProof, - transcript: &mut T, - ) -> Result<(), OpeningsError> - where - T: Transcript, - { + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result<(), OpeningsError> { validate_trace_batch_statement(setup, auxiliary_groups, main)?; + let selection = bind_verifier_opening(transcript, setup, AkitaBackendFlavor::OneHot)?; let backend_main_point = reverse_point(&main.point); let auxiliary_commitments = auxiliary_groups .iter() @@ -419,13 +377,13 @@ impl AkitaNativeBatching { setup.one_hot_k16_scheme()?.schedules(), &auxiliary_commitments, &main.commitment, - proof.selection(), + selection, ), AKITA_ONE_HOT_K256 => crate::shape_guard::deserialize_checked_grouped_backend_payload( setup.one_hot_k256_scheme()?.schedules(), &auxiliary_commitments, &main.commitment, - proof.selection(), + selection, ), other => { return Err(invalid_batch(format!( @@ -433,14 +391,6 @@ impl AkitaNativeBatching { ))) } }?; - let selection = proof.selection(); - let session = bind_grouped_statement_transcripts( - transcript, - setup, - selection, - auxiliary_groups, - main, - )?; let mut group_claims = Vec::with_capacity(auxiliary_groups.len() + 1); for (entry, backend) in auxiliary_groups.iter().zip(&auxiliary_backend) { group_claims.push( @@ -458,7 +408,7 @@ impl AkitaNativeBatching { ); let claims = OpeningClaims::from_groups(group_claims).map_err(akita_error)?; let batch_statement = GroupBatchStatement::new(selection, claims).map_err(akita_error)?; - verify_one_hot_statement(setup, proof, &session, batch_statement) + verify_one_hot_statement(setup, transcript, batch_statement) } } @@ -586,28 +536,6 @@ fn validate_witness( Ok(()) } -/// Binds the verifier setup and statement into Jolt's transcript, then bridges -/// a Jolt challenge into the Akita session bytes so the backend argument is -/// bound to everything Jolt observed. -fn bind_statement_transcripts( - transcript: &mut T, - verifier_setup: &AkitaVerifierSetup, - statement: &[VerifierOpeningClaim], - commitment: &AkitaCommitment, - point: &[AkitaField], -) -> Result, OpeningsError> -where - T: Transcript, -{ - { - let _span = info_span!("AkitaNativeBatching::append_setup_and_statement").entered(); - append_verifier_setup(transcript, verifier_setup, commitment.backend_flavor)?; - append_batch_statement(transcript, statement, commitment, point); - } - let _span = info_span!("AkitaNativeBatching::bridge_transcripts").entered(); - Ok(bridged_akita_session(transcript, b"jolt-akita/batch")) -} - /// Assembles the single-group opening data handed to Akita's native batched /// prover: the shared point, per-polynomial claimed values, the group /// commitment, and the commit-time handle retaining the source. @@ -629,14 +557,13 @@ where /// The one-hot backend consumes the point in reversed variable order and uses /// the dedicated one-hot setup pair. -fn prove_one_hot( +fn one_hot_opening<'a>( setup: &AkitaProverSetup, point: &[AkitaField], evaluations: &[AkitaField], backend_commitment: AkitaBackendCommitment, backend_hint: AkitaBackendHint, - session: &[u8], -) -> Result<(OpeningScheduleSelection, Vec), OpeningsError> { +) -> Result, OpeningsError> { let backend_point = reverse_point(point); let opening = match setup.one_hot_k() { AKITA_ONE_HOT_K16 => single_group_batch::( @@ -657,9 +584,7 @@ fn prove_one_hot( .map_err(akita_error)?, _ => unreachable!("the one-hot setup geometry was validated during setup"), }; - let selection = opening.selection(); - let proof = prove_one_hot_opening(setup, opening, session)?; - Ok((selection, proof)) + Ok(opening) } impl BatchOpeningScheme for AkitaNativeBatching { @@ -672,18 +597,16 @@ impl BatchOpeningScheme for AkitaNativeBatching { where Self: 'a; type Hints = AkitaProverHint; - type Proof = AkitaBatchProof; - fn prove_batch<'a, T>( + fn prove_batch<'a, H: Sponge>( setup: &Self::ProverSetup, statement: Self::Statement, polynomials: Self::Polynomials<'a>, hint: Self::Hints, - transcript: &mut T, - ) -> Result + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> where Self: 'a, - T: Transcript, { let ValidatedStatement { commitment, point } = validate_statement( &statement, @@ -704,14 +627,11 @@ impl BatchOpeningScheme for AkitaNativeBatching { .backend .ok_or_else(|| invalid_batch("Akita prover hint is missing backend opening data"))?; - let session = - bind_statement_transcripts(transcript, &setup.verifier, &statement, commitment, point)?; - let evaluations: Vec = statement .iter() .map(|claim| claim.evaluation.value) .collect(); - let (selection, backend_proof) = match hint.source { + match hint.source { AkitaHintSource::Dense { .. } => { let scheme = setup.verifier.dense_scheme()?; let opening = single_group_batch::( @@ -722,44 +642,46 @@ impl BatchOpeningScheme for AkitaNativeBatching { backend_hint, ) .map_err(akita_error)?; - let selection = opening.selection(); + bind_prover_opening( + transcript, + &setup.verifier, + AkitaBackendFlavor::Dense, + opening.selection(), + )?; let (backend_prover_setup, backend) = setup.dense_backend()?; let _span = info_span!("AkitaNativeBatching::backend_batched_prove").entered(); - let proof = with_backend_pool(|| { - let proof = scheme.batched_prove( + with_backend_pool(|| { + scheme.batched_prove( backend_prover_setup, opening, backend, - &session, + transcript, BasisMode::Lagrange, )?; let _ = backend.trim_caches()?; - Ok::<_, AkitaError>(proof) + Ok::<_, AkitaError>(()) }) - .map_err(prove_failed)?; - (selection, proof) + .map_err(prove_failed) } - AkitaHintSource::OneHot { .. } | AkitaHintSource::TraceOneHot { .. } => prove_one_hot( - setup, - point, - &evaluations, - backend_commitment, - backend_hint, - &session, - )?, - }; - Ok(AkitaBatchProof::new(selection, backend_proof)) + AkitaHintSource::OneHot { .. } | AkitaHintSource::TraceOneHot { .. } => { + let opening = + one_hot_opening(setup, point, &evaluations, backend_commitment, backend_hint)?; + bind_prover_opening( + transcript, + &setup.verifier, + AkitaBackendFlavor::OneHot, + opening.selection(), + )?; + prove_one_hot_opening(setup, opening, transcript) + } + } } - fn verify_batch( + fn verify_batch( setup: &Self::VerifierSetup, statement: &Self::Statement, - proof: &Self::Proof, - transcript: &mut T, - ) -> Result<(), OpeningsError> - where - T: Transcript, - { + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result<(), OpeningsError> { let ValidatedStatement { commitment, point } = validate_statement( statement, setup.max_num_vars, @@ -770,6 +692,7 @@ impl BatchOpeningScheme for AkitaNativeBatching { AkitaBackendFlavor::Dense => point.to_vec(), AkitaBackendFlavor::OneHot => reverse_point(point), }; + let selection = bind_verifier_opening(transcript, setup, commitment.backend_flavor)?; // Deserializes the proof-controlled backend commitment only after its // shape is validated against the trusted schedule, so a malformed // proof cannot drive shape-backed allocations (see `shape_guard`). @@ -777,7 +700,7 @@ impl BatchOpeningScheme for AkitaNativeBatching { AkitaBackendFlavor::Dense => crate::shape_guard::deserialize_checked_backend_payload( setup.dense_scheme()?.schedules(), commitment, - proof.selection(), + selection, statement.len(), &backend_point, ), @@ -785,14 +708,14 @@ impl BatchOpeningScheme for AkitaNativeBatching { AKITA_ONE_HOT_K16 => crate::shape_guard::deserialize_checked_backend_payload( setup.one_hot_k16_scheme()?.schedules(), commitment, - proof.selection(), + selection, statement.len(), &backend_point, ), AKITA_ONE_HOT_K256 => crate::shape_guard::deserialize_checked_backend_payload( setup.one_hot_k256_scheme()?.schedules(), commitment, - proof.selection(), + selection, statement.len(), &backend_point, ), @@ -804,8 +727,6 @@ impl BatchOpeningScheme for AkitaNativeBatching { }, }?; - let session = bind_statement_transcripts(transcript, setup, statement, commitment, point)?; - let openings: Vec = statement .iter() .map(|claim| claim.evaluation.value) @@ -813,23 +734,17 @@ impl BatchOpeningScheme for AkitaNativeBatching { let group = PolynomialGroupClaims::new(backend_point, openings, &backend_commitment) .map_err(akita_error)?; let claims = OpeningClaims::from_groups(vec![group]).map_err(akita_error)?; - let batch_statement = - GroupBatchStatement::new(proof.selection(), claims).map_err(akita_error)?; + let batch_statement = GroupBatchStatement::new(selection, claims).map_err(akita_error)?; match commitment.backend_flavor { AkitaBackendFlavor::Dense => { let verifier = setup.dense_verifier()?; with_backend_pool(|| { - verifier.batched_verify( - &proof.backend_proof, - &session, - batch_statement, - BasisMode::Lagrange, - ) + verifier.batched_verify(transcript, batch_statement, BasisMode::Lagrange) }) .map_err(|_| OpeningsError::VerificationFailed) } AkitaBackendFlavor::OneHot => { - verify_one_hot_statement(setup, proof, &session, batch_statement) + verify_one_hot_statement(setup, transcript, batch_statement) } } } @@ -846,9 +761,9 @@ mod tests { use super::*; use jolt_field::Zero; use jolt_openings::{CommitmentGroupRole, EvaluationClaim}; - use jolt_transcript::Blake2bTranscript; use crate::adapters::AkitaVerifierScheduleArtifacts; + use crate::test_transcripts::new_verifier_transcript; fn commitment( backend_flavor: AkitaBackendFlavor, @@ -895,15 +810,11 @@ mod tests { commitment: commitment(AkitaBackendFlavor::OneHot, 4, [9; 32], 8), evaluation: EvaluationClaim::new(vec![AkitaField::zero(); 4], AkitaField::zero()), }]; - let proof = AkitaBatchProof { - schedule_selection: [0; 32], - backend_proof: Vec::new(), - }; - let mut transcript = Blake2bTranscript::new(b"invalid-transported-setup"); + let proof = [0; 32]; + let mut transcript = new_verifier_transcript(b"invalid-transported-setup", &proof); let error = ::verify_batch( &transported, &statement, - &proof, &mut transcript, ) .expect_err("unsupported setup K must reject before backend dispatch"); diff --git a/crates/jolt-akita/src/planning.rs b/crates/jolt-akita/src/planning.rs index 15d0321e79..bd935bb812 100644 --- a/crates/jolt-akita/src/planning.rs +++ b/crates/jolt-akita/src/planning.rs @@ -4,10 +4,10 @@ use akita_config::{policy_of, CommitmentConfig}; use akita_pcs::AkitaError; use akita_planner::find_schedule; use akita_types::sis::CommittedSourceContract; -use akita_types::{AkitaScheduleLookupKey, FoldSchedule}; +use akita_types::{FoldSchedule, ScheduleLookupKey}; pub(crate) fn plan_schedule( - key: &AkitaScheduleLookupKey, + key: &ScheduleLookupKey, auxiliary_source_contracts: &[CommittedSourceContract], ) -> Result { let planned = find_schedule( diff --git a/crates/jolt-akita/src/schedule_registry.rs b/crates/jolt-akita/src/schedule_registry.rs index 6b8b9fe9f2..01409f0218 100644 --- a/crates/jolt-akita/src/schedule_registry.rs +++ b/crates/jolt-akita/src/schedule_registry.rs @@ -16,8 +16,8 @@ use akita_planner::emit::{GroupedGenerationRequest, PrecommittedProducer}; use akita_planner::find_adapted_schedule; use akita_schedules::{ResolvedScheduleRow, ValidatedScheduleCatalog}; use akita_types::{ - AkitaScheduleLookupKey, CommittedGroupBatchProfile, GroupCommitPhaseParams, - PolynomialGroupLayout, ScheduleRowDigest, + CommittedGroupBatchProfile, GroupCommitPhaseParams, PolynomialGroupLayout, ScheduleLookupKey, + ScheduleRowDigest, }; use serde::{Deserialize, Serialize}; @@ -184,7 +184,7 @@ fn plan_row( if base.resolve_key(&key).is_ok() { return Ok(None); } - let main_row = base.resolve_key(&AkitaScheduleLookupKey::single(key.final_group))?; + let main_row = base.resolve_key(&ScheduleLookupKey::single(key.final_group))?; let adapted = find_adapted_schedule( main_row, &request, @@ -271,7 +271,7 @@ pub fn dense_group_profile( layout: PolynomialGroupLayout, ) -> Result { Ok(dense_catalog - .resolve_key(&AkitaScheduleLookupKey::single(layout))? + .resolve_key(&ScheduleLookupKey::single(layout))? .profiles() .final_group) } diff --git a/crates/jolt-akita/src/schedules/mod.rs b/crates/jolt-akita/src/schedules/mod.rs index ae71f16e93..22c7a4489c 100644 --- a/crates/jolt-akita/src/schedules/mod.rs +++ b/crates/jolt-akita/src/schedules/mod.rs @@ -18,7 +18,7 @@ pub mod emit { use akita_planner::emit::GroupedGenerationRequest; use akita_planner::EmitSpec; use akita_types::{ - AkitaScheduleLookupKey, FoldSchedule, OpeningClaimsLayout, PolynomialGroupLayout, + FoldSchedule, OpeningClaimsLayout, PolynomialGroupLayout, ScheduleLookupKey, }; use crate::configs::{ @@ -56,7 +56,7 @@ pub mod emit { fn regen( key: PolynomialGroupLayout, ) -> Result { - plan_schedule::(&AkitaScheduleLookupKey::single(key), &[]) + plan_schedule::(&ScheduleLookupKey::single(key), &[]) } fn regen_one_hot_k16(key: PolynomialGroupLayout) -> Result { diff --git a/crates/jolt-akita/src/scheme.rs b/crates/jolt-akita/src/scheme.rs index 9ceb8ff254..ecceb17363 100644 --- a/crates/jolt-akita/src/scheme.rs +++ b/crates/jolt-akita/src/scheme.rs @@ -2,14 +2,13 @@ use akita_pcs::custom_source::RootPolyMeta; use akita_pcs::{AkitaError, CommitOutput, CpuBackend, GroupContext, SourceHandle}; use akita_types::PrecommittedGroupProfiles; use jolt_crypto::Commitment; -use jolt_field::CanonicalBytes; use jolt_openings::{ BatchOpeningScheme, CommitmentGroupRole, CommitmentScheme, EvaluationClaim, GroupOpeningClaim, GroupOpeningWithHint, OpeningsError, TaggedGroupOpeningClaim, TransparentObjectSetup, - VerifierOpeningClaim, ZkBatchOpeningScheme, ZkOpeningScheme, + VerifierOpeningClaim, ZkBatchOpening, ZkBatchOpeningScheme, ZkOpeningScheme, }; -use jolt_poly::{MultilinearPoly, OneHotPolynomial, Polynomial}; -use jolt_transcript::Transcript; +use jolt_poly::{MultilinearPoly, OneHotPolynomial, Point, Polynomial, HIGH_TO_LOW}; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use serde::{Deserialize, Serialize}; use std::{ collections::{btree_map::Entry, BTreeMap}, @@ -21,11 +20,11 @@ use crate::adapters::{ invalid_setup, one_hot_polynomial, owned_one_hot_polynomial, serialize_akita, transparent_zk_error, validate_one_hot_k, with_backend_pool, AkitaBackend, AkitaBackendCommitment, AkitaBackendDensePoly, AkitaBackendExtField, AkitaBackendFlavor, - AkitaBackendHint, AkitaBackendOneHotPoly, AkitaBatchProof, AkitaCommitment, AkitaField, - AkitaHidingCommitment, AkitaHintSource, AkitaLayoutDigest, AkitaProverHint, AkitaProverSetup, - AkitaScheduleArtifacts, AkitaSetupFlavor, AkitaSetupParams, AkitaVerifierScheduleArtifacts, - AkitaVerifierSetup, BackendVerifierCache, FullWidthBackendSetup, AKITA_ONE_HOT_K16, - AKITA_ONE_HOT_K256, AKITA_SOURCE_RING_DIMENSION, + AkitaBackendHint, AkitaBackendOneHotPoly, AkitaCommitment, AkitaField, AkitaHidingCommitment, + AkitaHintSource, AkitaLayoutDigest, AkitaProverHint, AkitaProverSetup, AkitaScheduleArtifacts, + AkitaSetupFlavor, AkitaSetupParams, AkitaVerifierScheduleArtifacts, AkitaVerifierSetup, + BackendVerifierCache, FullWidthBackendSetup, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, + AKITA_SOURCE_RING_DIMENSION, }; use crate::native_batching::{AkitaNativeBatchPolynomials, AkitaNativeBatching}; use crate::trace_onehot::{TraceOneHotRows, TracePackedOneHot}; @@ -430,7 +429,6 @@ impl Commitment for AkitaScheme { impl CommitmentScheme for AkitaScheme { type Field = AkitaField; - type Proof = AkitaBatchProof; type ProverSetup = AkitaProverSetup; type VerifierSetup = AkitaVerifierSetup; type OpeningHint = AkitaProverHint; @@ -600,14 +598,29 @@ impl CommitmentScheme for AkitaScheme { Self::commit_dense_backend(setup, setup.default_layout_digest(), num_vars, dense) } - fn open + ?Sized>( + fn send_commitment(commitment: &Self::Output, transcript: &mut ProverTranscript) { + commitment.send(transcript); + } + + fn receive_commitment( + _setup: &Self::VerifierSetup, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + AkitaCommitment::receive(transcript) + } + + fn absorb_commitment(commitment: &Self::Output, channel: &mut C) { + commitment.absorb(channel); + } + + fn open + ?Sized, H: Sponge>( poly: &P, point: &[Self::Field], eval: Self::Field, setup: &Self::ProverSetup, hint: Option, - transcript: &mut impl Transcript, - ) -> Result { + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { let hint = match hint { Some(hint) => hint, None => Self::commit(poly, setup)?.1, @@ -627,21 +640,18 @@ impl CommitmentScheme for AkitaScheme { ) } - fn verify( + fn verify( commitment: &Self::Output, point: &[Self::Field], eval: Self::Field, - proof: &Self::Proof, setup: &Self::VerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { let statement = vec![VerifierOpeningClaim { commitment: commitment.clone(), evaluation: EvaluationClaim::new(point.to_vec(), eval), }]; - ::verify_batch( - setup, &statement, proof, transcript, - ) + ::verify_batch(setup, &statement, transcript) } /// Commits a group of row-major one-hot polynomials through the backend's @@ -692,13 +702,13 @@ impl CommitmentScheme for AkitaScheme { ) } - fn prove_batch( + fn prove_batch( setup: &Self::ProverSetup, auxiliary_groups: Vec>, final_group: GroupOpeningClaim, final_hint: Self::OpeningHint, - transcript: &mut impl Transcript, - ) -> Result { + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { AkitaNativeBatching::prove_trace_batch( setup, auxiliary_groups, @@ -708,20 +718,13 @@ impl CommitmentScheme for AkitaScheme { ) } - fn verify_batch( + fn verify_batch( setup: &Self::VerifierSetup, auxiliary_groups: &[TaggedGroupOpeningClaim], final_group: &GroupOpeningClaim, - proof: &Self::Proof, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { - AkitaNativeBatching::verify_trace_batch( - setup, - auxiliary_groups, - final_group, - proof, - transcript, - ) + AkitaNativeBatching::verify_trace_batch(setup, auxiliary_groups, final_group, transcript) } } @@ -810,28 +813,23 @@ impl ZkOpeningScheme for AkitaScheme { Self::commit(poly, setup) } - fn open_zk + ?Sized>( + fn open_zk + ?Sized, H: Sponge>( poly: &P, point: &[Self::Field], eval: Self::Field, setup: &Self::ProverSetup, hint: Self::OpeningHint, - transcript: &mut impl Transcript, - ) -> Result<(Self::Proof, Self::HidingCommitment, Self::Blind), OpeningsError> { - let proof = Self::open(poly, point, eval, setup, Some(hint), transcript)?; - Ok(( - proof, - AkitaHidingCommitment::new(eval.to_bytes_le_vec()), - (), - )) + transcript: &mut ProverTranscript, + ) -> Result<(Self::HidingCommitment, Self::Blind), OpeningsError> { + Self::open(poly, point, eval, setup, Some(hint), transcript)?; + Ok((AkitaHidingCommitment::new(eval), ())) } - fn verify_zk( + fn verify_zk( _commitment: &Self::Output, _point: &[Self::Field], - _proof: &Self::Proof, _setup: &Self::VerifierSetup, - _transcript: &mut impl Transcript, + _transcript: &mut VerifierTranscript<'_, H>, ) -> Result { Err(transparent_zk_error()) } @@ -842,32 +840,27 @@ impl ZkBatchOpeningScheme for AkitaNativeBatching { type HidingCommitment = AkitaHidingCommitment; type Blind = (); - fn prove_batch_zk<'a, T>( + fn prove_batch_zk<'a, H: Sponge>( _setup: &Self::ProverSetup, - _point: jolt_poly::Point<{ jolt_poly::HIGH_TO_LOW }, Self::Field>, + _point: Point, _commitments: Vec, _polynomials: Self::Polynomials<'a>, _hints: Self::Hints, _evaluations: Vec, - _transcript: &mut T, - ) -> Result, OpeningsError> + _transcript: &mut ProverTranscript, + ) -> Result, OpeningsError> where Self: 'a, - T: Transcript, { Err(transparent_zk_error()) } - fn verify_batch_zk( + fn verify_batch_zk( _setup: &Self::VerifierSetup, - _point: jolt_poly::Point<{ jolt_poly::HIGH_TO_LOW }, Self::Field>, + _point: Point, _commitments: Vec, - _proof: &Self::Proof, - _transcript: &mut T, - ) -> Result - where - T: Transcript, - { + _transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { Err(transparent_zk_error()) } } @@ -879,12 +872,14 @@ mod tests { #![expect(clippy::indexing_slicing, reason = "tests index fixture data")] use super::*; - use crate::adapters::{append_verifier_setup, AkitaBackendFlavor}; use crate::configs::JoltDenseBounded; + use crate::test_transcripts::{ + assert_transcripts_agree, new_prover_transcript, new_verifier_transcript, + }; use akita_config::{policy_of, CommitmentConfig}; use akita_schedules::ValidatedScheduleCatalog; + use akita_types::{OpeningScheduleSelection, ScheduleRowDigest}; use jolt_field::Ring; - use jolt_transcript::Blake2bTranscript; #[test] fn full_width_objects_do_not_widen_bounded_dense_commitments() { @@ -906,72 +901,6 @@ mod tests { assert_eq!(commitment.num_vars, polynomial.num_vars()); } - #[test] - fn setup_key_transcript_binds_backend_shape() { - let artifacts = AkitaScheduleArtifacts::shared_from_default_directory(); - let setup = AkitaVerifierSetup { - max_num_vars: 4, - max_num_polys_per_commitment_group: 1, - max_total_batch_polys: 1, - default_layout_digest: [7; 32], - one_hot_k: AKITA_ONE_HOT_K256, - schedule_artifacts: AkitaVerifierScheduleArtifacts::Both { - dense: artifacts - .dense_catalog() - .unwrap() - .to_artifact_bytes() - .unwrap(), - one_hot: artifacts - .one_hot_catalog(AKITA_ONE_HOT_K256) - .unwrap() - .to_artifact_bytes() - .unwrap(), - }, - backend_cache: Default::default(), - }; - let mut baseline = Blake2bTranscript::::new(b"akita-setup-key-test"); - let initial_state = baseline.state(); - - append_verifier_setup(&mut baseline, &setup, AkitaBackendFlavor::Dense).unwrap(); - assert_ne!(baseline.state(), initial_state); - - let mut same = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup(&mut same, &setup, AkitaBackendFlavor::Dense).unwrap(); - assert_eq!(baseline.state(), same.state()); - - let mut flavor_transcript = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup(&mut flavor_transcript, &setup, AkitaBackendFlavor::OneHot).unwrap(); - assert_ne!(baseline.state(), flavor_transcript.state()); - - let mut changed_shape = setup.clone(); - changed_shape.max_num_vars = 5; - let mut shape_transcript = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup( - &mut shape_transcript, - &changed_shape, - AkitaBackendFlavor::Dense, - ) - .unwrap(); - assert_ne!(baseline.state(), shape_transcript.state()); - - let mut changed_digest = setup; - changed_digest.default_layout_digest = [8; 32]; - let mut digest_transcript = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup( - &mut digest_transcript, - &changed_digest, - AkitaBackendFlavor::Dense, - ) - .unwrap(); - assert_ne!(baseline.state(), digest_transcript.state()); - - let mut changed_k = changed_digest; - changed_k.one_hot_k = AKITA_ONE_HOT_K16; - let mut k_transcript = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup(&mut k_transcript, &changed_k, AkitaBackendFlavor::Dense).unwrap(); - assert_ne!(digest_transcript.state(), k_transcript.state()); - } - #[test] fn transparent_object_setup_reuses_backend_for_a_new_layout() { let context = AkitaScheduleArtifacts::shared_from_default_directory(); @@ -1025,8 +954,8 @@ mod tests { commitment: commitment.clone(), evaluation: EvaluationClaim::new(point, value), }]; - let mut prover_transcript = Blake2bTranscript::::new(b"akita-one-hot-k"); - let proof = ::prove_batch( + let mut prover_transcript = new_prover_transcript(b"akita-one-hot-k"); + ::prove_batch( &prover_setup, statement.clone(), vec![&polynomial], @@ -1034,15 +963,15 @@ mod tests { &mut prover_transcript, ) .unwrap(); - let mut verifier_transcript = Blake2bTranscript::::new(b"akita-one-hot-k"); + let proof = prover_transcript.narg().to_vec(); + let mut verifier_transcript = new_verifier_transcript(b"akita-one-hot-k", &proof); ::verify_batch( &verifier_setup, &statement, - &proof, &mut verifier_transcript, ) .unwrap(); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); let mut wrong_k_statement = statement; wrong_k_statement[0].commitment.one_hot_k = if one_hot_k == AKITA_ONE_HOT_K16 { @@ -1050,11 +979,10 @@ mod tests { } else { AKITA_ONE_HOT_K16 }; - let mut verifier_transcript = Blake2bTranscript::::new(b"akita-one-hot-k"); + let mut verifier_transcript = new_verifier_transcript(b"akita-one-hot-k", &proof); let _ = ::verify_batch( &verifier_setup, &wrong_k_statement, - &proof, &mut verifier_transcript, ) .expect_err("commitment K must match verifier setup K"); @@ -1189,8 +1117,8 @@ mod tests { commitment, evaluation: EvaluationClaim::new(point, value), }]; - let mut prover_transcript = Blake2bTranscript::::new(b"catalog-replay"); - let proof = ::prove_batch( + let mut prover_transcript = new_prover_transcript(b"catalog-replay"); + ::prove_batch( &prover_setup, statement.clone(), vec![&polynomial], @@ -1198,8 +1126,12 @@ mod tests { &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.narg().to_vec(); - let selected = proof.selection(); + // The opening's first prover message is the selected schedule row. + let selected = OpeningScheduleSelection { + row_digest: ScheduleRowDigest::from_bytes(proof[..32].try_into().unwrap()), + }; let full_catalog = artifacts.dense_catalog().unwrap(); let omitted = full_catalog .rows() @@ -1246,20 +1178,18 @@ mod tests { alternate_artifacts, )) .unwrap(); - let mut verifier_transcript = Blake2bTranscript::::new(b"catalog-replay"); + let mut verifier_transcript = new_verifier_transcript(b"catalog-replay", &proof); let _ = ::verify_batch( &alternate_verifier_setup, &statement, - &proof, &mut verifier_transcript, ) .expect_err("a proof must not replay across catalogs with different digests"); - let mut original_transcript = Blake2bTranscript::::new(b"catalog-replay"); + let mut original_transcript = new_verifier_transcript(b"catalog-replay", &proof); ::verify_batch( &verifier_setup, &statement, - &proof, &mut original_transcript, ) .unwrap(); @@ -1291,8 +1221,11 @@ mod tests { evaluation: EvaluationClaim::new(point.clone(), claim), }]; - let mut prover_transcript = Blake2bTranscript::::new(b"akita-direct-layout"); - let proof = ::prove_batch( + // A caller binds the commitments before the opening (see + // `BatchOpeningScheme`); the opening itself binds only what Akita reads. + let mut prover_transcript = new_prover_transcript(b"akita-direct-layout"); + AkitaScheme::absorb_commitment(&commitment, &mut prover_transcript); + ::prove_batch( &prover_setup, statement.clone(), vec![&polynomial], @@ -1300,37 +1233,30 @@ mod tests { &mut prover_transcript, ) .expect("direct proof should prove"); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::::new(b"akita-direct-layout"); + let mut verifier_transcript = new_verifier_transcript(b"akita-direct-layout", &proof); + AkitaScheme::absorb_commitment(&commitment, &mut verifier_transcript); ::verify_batch( &verifier_setup, &statement, - &proof, &mut verifier_transcript, ) .expect("direct proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); - let mut changed_commitment_statement = statement.clone(); + let mut changed_commitment_statement = statement; changed_commitment_statement[0].commitment.layout_digest = [15; 32]; - let mut verifier_transcript = Blake2bTranscript::::new(b"akita-direct-layout"); + let mut verifier_transcript = new_verifier_transcript(b"akita-direct-layout", &proof); + AkitaScheme::absorb_commitment( + &changed_commitment_statement[0].commitment, + &mut verifier_transcript, + ); let _error = ::verify_batch( &verifier_setup, &changed_commitment_statement, - &proof, &mut verifier_transcript, ) .expect_err("changed direct commitment digest should reject"); - - let mut changed_setup = verifier_setup; - changed_setup.default_layout_digest = commitment_digest; - let mut verifier_transcript = Blake2bTranscript::::new(b"akita-direct-layout"); - let _error = ::verify_batch( - &changed_setup, - &statement, - &proof, - &mut verifier_transcript, - ) - .expect_err("direct commitment layout must not be accepted through setup default"); } } diff --git a/crates/jolt-akita/src/shape_guard.rs b/crates/jolt-akita/src/shape_guard.rs index 4898f3c5c4..e710695e65 100644 --- a/crates/jolt-akita/src/shape_guard.rs +++ b/crates/jolt-akita/src/shape_guard.rs @@ -157,7 +157,7 @@ mod tests { use super::*; use akita_config::TrustedScheduleCatalog; - use akita_types::AkitaScheduleLookupKey; + use akita_types::ScheduleLookupKey; use jolt_field::Ring; use crate::{ @@ -195,7 +195,7 @@ mod tests { poly_count: usize, ) -> OpeningScheduleSelection { schedules - .resolve_key(&AkitaScheduleLookupKey::single(PolynomialGroupLayout::new( + .resolve_key(&ScheduleLookupKey::single(PolynomialGroupLayout::new( num_vars, poly_count, ))) .expect("test schedule") diff --git a/crates/jolt-akita/src/test_transcripts.rs b/crates/jolt-akita/src/test_transcripts.rs new file mode 100644 index 0000000000..d38babdda9 --- /dev/null +++ b/crates/jolt-akita/src/test_transcripts.rs @@ -0,0 +1,32 @@ +//! Standalone transcripts for exercising openings outside a Jolt proof. + +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; + +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-akita/test"); + +pub(crate) fn new_prover_transcript(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&PROTOCOL, session) +} + +pub(crate) fn new_verifier_transcript<'a>( + session: &[u8], + proof: &'a [u8], +) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new(&PROTOCOL, session, proof) +} + +/// The verifier consumed exactly the prover's argument string and ended in the +/// same sponge state. +#[expect(clippy::expect_used, reason = "test assertion")] +pub(crate) fn assert_transcripts_agree( + mut prover: ProverTranscript, + mut verifier: VerifierTranscript<'_, Blake2b512>, +) { + assert_eq!( + prover.challenge_bytes::<32>(), + verifier.challenge_bytes::<32>() + ); + verifier + .finish() + .expect("the verifier consumes the whole proof"); +} diff --git a/crates/jolt-akita/tests/e2e.rs b/crates/jolt-akita/tests/e2e.rs index 2219f1107d..feccfedd67 100644 --- a/crates/jolt-akita/tests/e2e.rs +++ b/crates/jolt-akita/tests/e2e.rs @@ -9,8 +9,10 @@ pub mod support; use jolt_akita::{AkitaScheduleArtifacts, AkitaScheme, AkitaSetupParams}; use jolt_openings::{CommitmentScheme, OpeningsError}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; -use support::{f, layout, polynomial, setup_for}; +use support::{ + assert_transcripts_agree, f, layout, new_prover_transcript, new_verifier_transcript, + polynomial, setup_for, +}; #[test] fn akita_single_opening_roundtrips_across_dimensions() { @@ -20,15 +22,15 @@ fn akita_single_opening_roundtrips_across_dimensions() { } #[test] -fn akita_single_opening_rejects_wrong_eval_point_setup_and_transcript() { +fn akita_single_opening_rejects_wrong_eval_point_and_transcript() { let (prover_setup, verifier_setup) = setup_for(14, 1, layout(7)); let poly = polynomial(14, 100); let point: Vec<_> = (0..14).map(|i| f(2 + 3 * i)).collect(); let eval = poly.evaluate(&point); let (commitment, hint) = AkitaScheme::commit(&poly, &prover_setup).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(b"akita-e2e-rejects"); - let proof = AkitaScheme::open( + let mut prover_transcript = new_prover_transcript(b"akita-e2e-rejects"); + AkitaScheme::open( &poly, &point, eval, @@ -37,14 +39,14 @@ fn akita_single_opening_rejects_wrong_eval_point_setup_and_transcript() { &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.narg().to_vec(); - let mut transcript = Blake2bTranscript::new(b"akita-e2e-rejects"); + let mut transcript = new_verifier_transcript(b"akita-e2e-rejects", &proof); assert!( AkitaScheme::verify( &commitment, &point, eval + f(1), - &proof, &verifier_setup, &mut transcript, ) @@ -54,13 +56,12 @@ fn akita_single_opening_rejects_wrong_eval_point_setup_and_transcript() { let mut wrong_point = point.clone(); wrong_point[2] += f(1); - let mut transcript = Blake2bTranscript::new(b"akita-e2e-rejects"); + let mut transcript = new_verifier_transcript(b"akita-e2e-rejects", &proof); assert!( AkitaScheme::verify( &commitment, &wrong_point, eval, - &proof, &verifier_setup, &mut transcript, ) @@ -68,34 +69,18 @@ fn akita_single_opening_rejects_wrong_eval_point_setup_and_transcript() { "wrong opening point should reject" ); - let mut wrong_transcript = Blake2bTranscript::new(b"akita-e2e-wrong-domain"); + let mut wrong_transcript = new_verifier_transcript(b"akita-e2e-wrong-domain", &proof); assert!( AkitaScheme::verify( &commitment, &point, eval, - &proof, &verifier_setup, &mut wrong_transcript, ) .is_err(), "different transcript domain should reject" ); - - let (_, wrong_layout_setup) = setup_for(14, 1, layout(8)); - let mut transcript = Blake2bTranscript::new(b"akita-e2e-rejects"); - assert!( - AkitaScheme::verify( - &commitment, - &point, - eval, - &proof, - &wrong_layout_setup, - &mut transcript, - ) - .is_err(), - "wrong verifier setup key should reject" - ); } #[test] @@ -160,8 +145,8 @@ fn single_opening_roundtrip( let eval = poly.evaluate(&point); let (commitment, hint) = AkitaScheme::commit(&poly, &prover_setup).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(label); - let proof = AkitaScheme::open( + let mut prover_transcript = new_prover_transcript(label); + AkitaScheme::open( &poly, &point, eval, @@ -170,16 +155,16 @@ fn single_opening_roundtrip( &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::new(label); + let mut verifier_transcript = new_verifier_transcript(label, &proof); AkitaScheme::verify( &commitment, &point, eval, - &proof, &verifier_setup, &mut verifier_transcript, ) .expect("single proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); } diff --git a/crates/jolt-akita/tests/fold_paths.rs b/crates/jolt-akita/tests/fold_paths.rs index 02b871a5ea..a992c6edf3 100644 --- a/crates/jolt-akita/tests/fold_paths.rs +++ b/crates/jolt-akita/tests/fold_paths.rs @@ -12,34 +12,36 @@ )] mod support; -use akita_types::{AkitaScheduleLookupKey, PolynomialGroupLayout}; -use jolt_akita::{ - AkitaBatchProof, AkitaCommitment, AkitaField, AkitaScheduleArtifacts, AkitaScheme, -}; +use akita_types::{PolynomialGroupLayout, ScheduleLookupKey}; +use jolt_akita::{AkitaCommitment, AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_openings::{CommitmentScheme, OpeningsError}; -use jolt_transcript::{Blake2bTranscript, Transcript}; -use support::{f, layout, polynomial, setup_for}; +use jolt_transcript::TranscriptError; +use support::{ + assert_transcripts_agree, f, layout, new_prover_transcript, new_verifier_transcript, + polynomial, setup_for, +}; struct ProofFixture { verifier_setup: ::VerifierSetup, commitment: AkitaCommitment, point: Vec, eval: AkitaField, - proof: AkitaBatchProof, + proof: Vec, label: &'static [u8], } impl ProofFixture { - fn verify(&self, proof: &AkitaBatchProof) -> Result<(), OpeningsError> { - let mut transcript = Blake2bTranscript::new(self.label); + /// Verifies `proof` as the whole argument string of a standalone opening. + fn verify(&self, proof: &[u8]) -> Result<(), OpeningsError> { + let mut transcript = new_verifier_transcript(self.label, proof); AkitaScheme::verify( &self.commitment, &self.point, self.eval, - proof, &self.verifier_setup, &mut transcript, - ) + )?; + Ok(transcript.finish()?) } } @@ -51,8 +53,8 @@ fn fold_roundtrip(num_vars: usize, label: &'static [u8]) -> ProofFixture { let (commitment, hint) = AkitaScheme::commit(&poly, &prover_setup).expect("dense commit should succeed"); - let mut prover_transcript = Blake2bTranscript::new(label); - let proof = AkitaScheme::open( + let mut prover_transcript = new_prover_transcript(label); + AkitaScheme::open( &poly, &point, eval, @@ -61,18 +63,18 @@ fn fold_roundtrip(num_vars: usize, label: &'static [u8]) -> ProofFixture { &mut prover_transcript, ) .expect("fold-schedule proof should be produced"); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::new(label); + let mut verifier_transcript = new_verifier_transcript(label, &proof); AkitaScheme::verify( &commitment, &point, eval, - &proof, &verifier_setup, &mut verifier_transcript, ) .expect("fold-schedule proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); ProofFixture { verifier_setup, @@ -94,7 +96,7 @@ fn deep_recursive_fold_schedule_roundtrips() { let depth = AkitaScheduleArtifacts::shared_from_default_directory() .dense_catalog() .expect("dense catalog") - .resolve_key(&AkitaScheduleLookupKey::single(PolynomialGroupLayout::new( + .resolve_key(&ScheduleLookupKey::single(PolynomialGroupLayout::new( NUM_VARS, 1, ))) .expect("deep fixture row must resolve") @@ -106,13 +108,12 @@ fn deep_recursive_fold_schedule_roundtrips() { let mut tampered_eval = fixture.eval; tampered_eval += f(1); - let mut transcript = Blake2bTranscript::new(fixture.label); + let mut transcript = new_verifier_transcript(fixture.label, &fixture.proof); assert!( AkitaScheme::verify( &fixture.commitment, &fixture.point, tampered_eval, - &fixture.proof, &fixture.verifier_setup, &mut transcript, ) @@ -121,43 +122,23 @@ fn deep_recursive_fold_schedule_roundtrips() { ); } -/// The argument parser must consume exactly one complete proof. +/// The opening must consume exactly one complete argument string. #[test] fn proof_payloads_with_trailing_or_missing_bytes_reject() { let fixture = fold_roundtrip(14, b"akita-fold-trailing"); - let mut value = serde_json::to_value(&fixture.proof).expect("proof should serialize to JSON"); - value - .get_mut("backend_proof") - .expect("proof should expose the payload") - .as_array_mut() - .expect("payload should serialize as a byte array") - .push(serde_json::json!(0)); - let extended: AkitaBatchProof = - serde_json::from_value(value.clone()).expect("extended proof should deserialize"); - - let err = fixture - .verify(&extended) - .expect_err("trailing payload bytes must be rejected"); - assert!( - matches!(&err, OpeningsError::VerificationFailed), - "expected a verification failure, got: {err}" + let mut extended = fixture.proof.clone(); + extended.push(0); + assert_eq!( + fixture.verify(&extended), + Err(OpeningsError::Transcript(TranscriptError::TrailingBytes)), + "trailing proof bytes must be rejected" ); - let proof_len = fixture.proof.backend_proof_body_size(); + let proof_len = fixture.proof.len(); for length in [0, 1, proof_len / 2, proof_len - 1] { - let mut truncated = value.clone(); - truncated["backend_proof"] - .as_array_mut() - .expect("payload should serialize as a byte array") - .truncate(length); - let truncated: AkitaBatchProof = - serde_json::from_value(truncated).expect("truncated proof should deserialize"); assert!( - matches!( - fixture.verify(&truncated), - Err(OpeningsError::VerificationFailed) - ), + fixture.verify(&fixture.proof[..length]).is_err(), "proof truncated to {length} bytes must reject" ); } diff --git a/crates/jolt-akita/tests/grouped_capacity.rs b/crates/jolt-akita/tests/grouped_capacity.rs index e3b59781db..c369f7aa9d 100644 --- a/crates/jolt-akita/tests/grouped_capacity.rs +++ b/crates/jolt-akita/tests/grouped_capacity.rs @@ -21,8 +21,10 @@ use jolt_openings::{ TransparentObjectSetup, }; use jolt_poly::{MultilinearPoly, OneHotPolynomial}; -use jolt_transcript::{Blake2bTranscript, Transcript}; -use support::{f, layout, polynomial}; +use jolt_transcript::Channel; +use support::{ + assert_transcripts_agree, f, layout, new_prover_transcript, new_verifier_transcript, polynomial, +}; const FINAL_NUM_VARS: usize = 16; /// Six variables above this one-column trace group. The canonical Jolt trace @@ -106,8 +108,8 @@ fn grouped_opening_proves_advice_larger_than_the_trace_group() { .expect("trace group should commit against the larger advice object"); let main = GroupOpeningClaim::new(trace_commitment, trace_point, vec![trace_evaluation]); - let mut prover_transcript = Blake2bTranscript::new(b"akita-grouped-capacity"); - let proof = AkitaScheme::prove_batch( + let mut prover_transcript = new_prover_transcript(b"akita-grouped-capacity"); + AkitaScheme::prove_batch( &prover_setup, vec![(advice_claim.clone(), advice_hint)], main.clone(), @@ -115,31 +117,30 @@ fn grouped_opening_proves_advice_larger_than_the_trace_group() { &mut prover_transcript, ) .expect("grouped opening should prove"); + let proof = prover_transcript.narg().to_vec(); let transported: AkitaVerifierSetup = serde_json::from_str( &serde_json::to_string(&verifier_setup).expect("verifier setup should serialize"), ) .expect("verifier setup should deserialize"); for setup in [&verifier_setup, &transported] { - let mut verifier_transcript = Blake2bTranscript::new(b"akita-grouped-capacity"); + let mut verifier_transcript = new_verifier_transcript(b"akita-grouped-capacity", &proof); AkitaScheme::verify_batch( setup, std::slice::from_ref(&advice_claim), &main, - &proof, &mut verifier_transcript, ) .expect("grouped opening should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript.clone(), verifier_transcript); } - let mut other_context = Blake2bTranscript::new(b"akita-grouped-capacity"); - other_context.append_bytes(b"different preceding Jolt messages"); + let mut other_context = new_verifier_transcript(b"akita-grouped-capacity", &proof); + other_context.public_bytes(b"different preceding Jolt messages"); assert!(AkitaScheme::verify_batch( &transported, std::slice::from_ref(&advice_claim), &main, - &proof, &mut other_context, ) .is_err()); diff --git a/crates/jolt-akita/tests/native_batching.rs b/crates/jolt-akita/tests/native_batching.rs index a06a36855c..9d36b4d501 100644 --- a/crates/jolt-akita/tests/native_batching.rs +++ b/crates/jolt-akita/tests/native_batching.rs @@ -6,10 +6,9 @@ use jolt_akita::{AkitaCommitment, AkitaNativeBatching, AkitaProverHint, AkitaSch use jolt_openings::{ BatchOpeningScheme, CommitmentScheme, EvaluationClaim, OpeningsError, VerifierOpeningClaim, }; -use jolt_transcript::{Blake2bTranscript, Transcript}; use support::{ - batch_polynomials, f, layout, native_setup, native_statement, polynomial, setup_for, - single_statement, + assert_transcripts_agree, batch_polynomials, f, layout, native_setup, native_statement, + new_prover_transcript, new_verifier_transcript, polynomial, setup_for, single_statement, }; #[test] @@ -25,8 +24,8 @@ fn akita_native_batching_roundtrips_grouped_commitment() { .expect("grouped commit should succeed"); let statement = native_statement(commitment, &point, [eval_a, eval_b]); - let mut prover_transcript = Blake2bTranscript::new(b"akita-bb-roundtrip"); - let proof = ::prove_batch( + let mut prover_transcript = new_prover_transcript(b"akita-bb-roundtrip"); + ::prove_batch( &prover_setup, statement.clone(), batch_polynomials([&poly_a, &poly_b]), @@ -34,16 +33,16 @@ fn akita_native_batching_roundtrips_grouped_commitment() { &mut prover_transcript, ) .expect("black-box proof should be produced"); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-bb-roundtrip"); + let mut verifier_transcript = new_verifier_transcript(b"akita-bb-roundtrip", &proof); ::verify_batch( &verifier_setup, &statement, - &proof, &mut verifier_transcript, ) .expect("black-box proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); } #[test] @@ -61,7 +60,7 @@ fn akita_native_batching_rejects_malformed_statements() { AkitaScheme::commit_group(&prover_setup, layout(7), &[polynomial(16, 80)]) .expect("other commit should succeed"); - let mut transcript = Blake2bTranscript::new(b"akita-bb-empty"); + let mut transcript = new_prover_transcript(b"akita-bb-empty"); assert!(matches!( ::prove_batch( &prover_setup, @@ -83,7 +82,7 @@ fn akita_native_batching_rejects_malformed_statements() { evaluation: EvaluationClaim::new(point.clone(), eval_b), }, ]; - let mut transcript = Blake2bTranscript::new(b"akita-bb-mixed-commit"); + let mut transcript = new_prover_transcript(b"akita-bb-mixed-commit"); assert!(matches!( ::prove_batch( &prover_setup, @@ -99,7 +98,7 @@ fn akita_native_batching_rejects_malformed_statements() { let mut shifted_point = point.clone(); shifted_point[0] += f(1); mixed_points[1].evaluation = EvaluationClaim::new(shifted_point, eval_b); - let mut transcript = Blake2bTranscript::new(b"akita-bb-mixed-points"); + let mut transcript = new_prover_transcript(b"akita-bb-mixed-points"); assert!(matches!( ::prove_batch( &prover_setup, @@ -112,7 +111,7 @@ fn akita_native_batching_rejects_malformed_statements() { )); let one_claim_for_two_slots = single_statement(group_commitment, &point, eval_a); - let mut transcript = Blake2bTranscript::new(b"akita-bb-claim-count"); + let mut transcript = new_prover_transcript(b"akita-bb-claim-count"); assert!(matches!( ::prove_batch( &prover_setup, @@ -144,7 +143,7 @@ fn akita_native_batching_rejects_bad_prover_witnesses() { .expect("other grouped commit should succeed"); let statement = native_statement(commitment, &point, [eval_a, eval_b]); - let mut transcript = Blake2bTranscript::new(b"akita-bb-wrong-hint"); + let mut transcript = new_prover_transcript(b"akita-bb-wrong-hint"); assert!( matches!( ::prove_batch( @@ -159,7 +158,7 @@ fn akita_native_batching_rejects_bad_prover_witnesses() { "mismatched prover hint should reject" ); - let mut transcript = Blake2bTranscript::new(b"akita-bb-wrong-count"); + let mut transcript = new_prover_transcript(b"akita-bb-wrong-count"); assert!(matches!( ::prove_batch( &prover_setup, @@ -172,7 +171,7 @@ fn akita_native_batching_rejects_bad_prover_witnesses() { )); let wrong_dimension = polynomial(12, 200); - let mut transcript = Blake2bTranscript::new(b"akita-bb-wrong-dim"); + let mut transcript = new_prover_transcript(b"akita-bb-wrong-dim"); assert!(matches!( ::prove_batch( &prover_setup, @@ -198,8 +197,8 @@ fn akita_native_batching_rejects_tampered_verifier_inputs() { .expect("grouped commit should succeed"); let statement = native_statement(commitment.clone(), &point, [eval_a, eval_b]); - let mut prover_transcript = Blake2bTranscript::new(b"akita-bb-tamper"); - let proof = ::prove_batch( + let mut prover_transcript = new_prover_transcript(b"akita-bb-tamper"); + ::prove_batch( &prover_setup, statement.clone(), batch_polynomials([&poly_a, &poly_b]), @@ -207,6 +206,7 @@ fn akita_native_batching_rejects_tampered_verifier_inputs() { &mut prover_transcript, ) .expect("black-box proof should be produced"); + let proof = prover_transcript.narg().to_vec(); let mut tampered_value = statement.clone(); tampered_value[0].evaluation.value += f(1); @@ -226,21 +226,17 @@ fn akita_native_batching_rejects_tampered_verifier_inputs() { .expect("other grouped commit should succeed"); let tampered_commitment = native_statement(other_commitment, &point, [eval_a, eval_b]); assert_native_verify_rejects(&verifier_setup, tampered_commitment, &proof); - - let (_, wrong_layout_setup) = setup_for(16, 2, layout(8)); - assert_native_verify_rejects(&wrong_layout_setup, statement, &proof); } fn assert_native_verify_rejects( setup: &::VerifierSetup, statement: jolt_akita::AkitaNativeBatchStatement, - proof: &jolt_akita::AkitaBatchProof, + proof: &[u8], ) { - let mut transcript = Blake2bTranscript::new(b"akita-bb-tamper"); + let mut transcript = new_verifier_transcript(b"akita-bb-tamper", proof); assert!(::verify_batch( setup, &statement, - proof, &mut transcript, ) .is_err()); @@ -267,7 +263,7 @@ fn akita_native_batching_rejects_point_commitment_dimension_mismatch() { .expect("commit should succeed"); let statement = single_statement(commitment, &short_point, f(9)); - let mut transcript = Blake2bTranscript::new(b"akita-bb-short-point"); + let mut transcript = new_prover_transcript(b"akita-bb-short-point"); expect_invalid_batch( ::prove_batch( &prover_setup, @@ -292,8 +288,8 @@ fn akita_native_batching_rejects_statements_outside_the_verifier_setup() { AkitaScheme::commit_group(&small_setup, layout(7), std::slice::from_ref(&small_poly)) .expect("commit should succeed"); let small_statement = single_statement(small_commitment, &small_point, small_eval); - let mut transcript = Blake2bTranscript::new(b"akita-bb-cross-setup"); - let small_proof = ::prove_batch( + let mut transcript = new_prover_transcript(b"akita-bb-cross-setup"); + ::prove_batch( &small_setup, small_statement.clone(), batch_polynomials([&small_poly]), @@ -301,15 +297,15 @@ fn akita_native_batching_rejects_statements_outside_the_verifier_setup() { &mut transcript, ) .expect("proof should be produced"); + let small_proof = transcript.narg().to_vec(); // A 14-variable commitment against a 15-variable verifier setup. let (_, wider_verifier) = setup_for(15, 2, layout(7)); - let mut transcript = Blake2bTranscript::new(b"akita-bb-cross-setup"); + let mut transcript = new_verifier_transcript(b"akita-bb-cross-setup", &small_proof); expect_invalid_batch( ::verify_batch( &wider_verifier, &small_statement, - &small_proof, &mut transcript, ), "does not match exact setup dimension", @@ -331,8 +327,8 @@ fn akita_native_batching_rejects_statements_outside_the_verifier_setup() { &point, [poly_a.evaluate(&point), poly_b.evaluate(&point)], ); - let mut transcript = Blake2bTranscript::new(b"akita-bb-cross-setup"); - let group_proof = ::prove_batch( + let mut transcript = new_prover_transcript(b"akita-bb-cross-setup"); + ::prove_batch( &two_slot_setup, group_statement.clone(), batch_polynomials([&poly_a, &poly_b]), @@ -340,13 +336,13 @@ fn akita_native_batching_rejects_statements_outside_the_verifier_setup() { &mut transcript, ) .expect("group proof should be produced"); + let group_proof = transcript.narg().to_vec(); let (_, one_slot_verifier) = setup_for(16, 1, layout(7)); - let mut transcript = Blake2bTranscript::new(b"akita-bb-cross-setup"); + let mut transcript = new_verifier_transcript(b"akita-bb-cross-setup", &group_proof); expect_invalid_batch( ::verify_batch( &one_slot_verifier, &group_statement, - &group_proof, &mut transcript, ), "but setup supports 1", @@ -365,8 +361,8 @@ fn akita_native_batching_rejects_dense_commitment_with_chunk_size() { AkitaScheme::commit_group(&prover_setup, layout(7), std::slice::from_ref(&poly)) .expect("commit should succeed"); let statement = single_statement(commitment.clone(), &point, eval); - let mut transcript = Blake2bTranscript::new(b"akita-bb-full-chunk"); - let proof = ::prove_batch( + let mut transcript = new_prover_transcript(b"akita-bb-full-chunk"); + ::prove_batch( &prover_setup, statement.clone(), batch_polynomials([&poly]), @@ -374,6 +370,7 @@ fn akita_native_batching_rejects_dense_commitment_with_chunk_size() { &mut transcript, ) .expect("proof should be produced"); + let proof = transcript.narg().to_vec(); let mut forged = serde_json::to_value(&commitment).expect("commitment serializes"); *forged @@ -383,12 +380,11 @@ fn akita_native_batching_rejects_dense_commitment_with_chunk_size() { serde_json::from_value(forged).expect("forged commitment deserializes"); let forged_statement = single_statement(forged, &point, eval); - let mut transcript = Blake2bTranscript::new(b"akita-bb-full-chunk"); + let mut transcript = new_verifier_transcript(b"akita-bb-full-chunk", &proof); expect_invalid_batch( ::verify_batch( &verifier_setup, &forged_statement, - &proof, &mut transcript, ), "invalid one-hot metadata", @@ -429,7 +425,7 @@ fn akita_native_batching_rejects_dense_witnesses_for_one_hot_hints() { let dense_12 = polynomial(12, 1); let point_12: Vec<_> = (0..12).map(|index| f(index as u64 + 2)).collect(); let statement = single_statement(commitment, &point_12, dense_12.evaluate(&point_12)); - let mut transcript = Blake2bTranscript::new(b"akita-bb-dense-for-onehot"); + let mut transcript = new_prover_transcript(b"akita-bb-dense-for-onehot"); expect_invalid_batch( ::prove_batch( &one_hot_setup, diff --git a/crates/jolt-akita/tests/one_hot_groups.rs b/crates/jolt-akita/tests/one_hot_groups.rs index bfece33e62..eec6405aa0 100644 --- a/crates/jolt-akita/tests/one_hot_groups.rs +++ b/crates/jolt-akita/tests/one_hot_groups.rs @@ -16,8 +16,10 @@ use jolt_akita::{ }; use jolt_openings::{BatchOpeningScheme, CommitmentScheme, OpeningsError}; use jolt_poly::{MultilinearPoly, OneHotIndexOrder, OneHotPolynomial}; -use jolt_transcript::{Blake2bTranscript, Transcript}; -use support::{f, layout, native_statement}; +use support::{ + assert_transcripts_agree, f, layout, native_statement, new_prover_transcript, + new_verifier_transcript, +}; /// `log2(K) + 8`: the smallest K=16 one-hot dimension the folded-only /// planner schedules (mirrors the scheme unit tests' roundtrip size). @@ -95,8 +97,8 @@ fn owned_group_opens_through_native_batching_and_verifies() { .collect(); let statement = native_statement(commitment, &point, evaluations.iter().copied()); - let mut prover_transcript = Blake2bTranscript::new(b"akita-owned-one-hot"); - let proof = ::prove_batch( + let mut prover_transcript = new_prover_transcript(b"akita-owned-one-hot"); + ::prove_batch( &prover_setup, statement.clone(), witnesses.clone(), @@ -104,32 +106,31 @@ fn owned_group_opens_through_native_batching_and_verifies() { &mut prover_transcript, ) .expect("owned one-hot group should prove"); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-owned-one-hot"); + let mut verifier_transcript = new_verifier_transcript(b"akita-owned-one-hot", &proof); ::verify_batch( &verifier_setup, &statement, - &proof, &mut verifier_transcript, ) .expect("owned one-hot group proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); let mut tampered = statement.clone(); tampered[0].evaluation.value += f(1); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-owned-one-hot"); + let mut verifier_transcript = new_verifier_transcript(b"akita-owned-one-hot", &proof); assert!( ::verify_batch( &verifier_setup, &tampered, - &proof, &mut verifier_transcript, ) .is_err(), "tampered evaluation must reject" ); - let mut transcript = Blake2bTranscript::new(b"akita-owned-one-hot"); + let mut transcript = new_prover_transcript(b"akita-owned-one-hot"); let err = ::prove_batch( &prover_setup, statement[..1].to_vec(), diff --git a/crates/jolt-akita/tests/pathologies.rs b/crates/jolt-akita/tests/pathologies.rs index 64394d5c2e..df5b886460 100644 --- a/crates/jolt-akita/tests/pathologies.rs +++ b/crates/jolt-akita/tests/pathologies.rs @@ -10,7 +10,7 @@ pub mod support; use jolt_akita::{ - AkitaBackendFlavor, AkitaBatchProof, AkitaCommitment, AkitaField, AkitaNativeBatchStatement, + AkitaBackendFlavor, AkitaCommitment, AkitaField, AkitaNativeBatchStatement, AkitaNativeBatching, AkitaScheme, }; use jolt_field::JoltField; @@ -18,9 +18,12 @@ use jolt_openings::{ BatchOpeningScheme, CommitmentScheme, OpeningsError, ZkBatchOpeningScheme, ZkOpeningScheme, }; use jolt_poly::{MultilinearPoly, OneHotPolynomial, Point, Polynomial, HIGH_TO_LOW}; -use jolt_transcript::{Blake2bTranscript, Transcript}; -use serde_json::{json, Value}; -use support::{batch_polynomials, f, layout, native_setup, polynomial, setup_for}; +use jolt_transcript::TranscriptError; +use serde_json::json; +use support::{ + assert_transcripts_agree, batch_polynomials, f, layout, native_setup, new_prover_transcript, + new_verifier_transcript, polynomial, setup_for, +}; type VerifierSetup = ::VerifierSetup; @@ -93,8 +96,8 @@ fn akita_public_commit_open_uses_upstream_one_hot_path_for_k256() { let eval = one_hot.evaluate(&point); assert_eq!(eval, dense.evaluate(&point)); - let mut prover_transcript = Blake2bTranscript::new(b"akita-native-one-hot"); - let proof = AkitaScheme::open( + let mut prover_transcript = new_prover_transcript(b"akita-native-one-hot"); + AkitaScheme::open( &one_hot, &point, eval, @@ -103,30 +106,23 @@ fn akita_public_commit_open_uses_upstream_one_hot_path_for_k256() { &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-native-one-hot"); + let mut verifier_transcript = new_verifier_transcript(b"akita-native-one-hot", &proof); AkitaScheme::verify( &one_hot_commitment, &point, eval, - &proof, &verifier_setup, &mut verifier_transcript, ) .expect("native Akita one-hot proof should verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); } #[test] -fn akita_proof_payloads_reject_unknown_serialized_fields() { - let (_, statement, proof) = native_proof_fixture(b"akita-payload-unknown-fields"); - - let mut top_level = serde_json::to_value(&proof).expect("proof should serialize"); - let _ = top_level - .as_object_mut() - .expect("proof should serialize as object") - .insert("unexpected".to_owned(), json!(true)); - assert!(serde_json::from_value::(top_level).is_err()); +fn akita_commitments_reject_unknown_serialized_fields() { + let (_, statement, _) = native_proof_fixture(b"akita-payload-unknown-fields"); let commitment = &statement[0].commitment; let mut tampered = serde_json::to_value(commitment).expect("commitment should serialize"); @@ -159,11 +155,10 @@ fn akita_forged_commitment_metadata_rejects_before_shape_backed_allocation() { evaluation: claim.evaluation.clone(), }) .collect(); - let mut transcript = Blake2bTranscript::new(b"akita-forged-metadata"); + let mut transcript = new_verifier_transcript(b"akita-forged-metadata", &proof); let err = ::verify_batch( &verifier_setup, &forged_statement, - &proof, &mut transcript, ) .expect_err("forged backend_coeff_len should reject"); @@ -177,22 +172,74 @@ fn akita_forged_commitment_metadata_rejects_before_shape_backed_allocation() { fn akita_native_batching_rejects_corrupted_proof_payloads() { let (verifier_setup, statement, proof) = native_proof_fixture(b"akita-corrupt-proof"); - for field in ["schedule_selection", "backend_proof"] { - let tampered = mutate_byte_array_field(&proof, field); - let mut transcript = Blake2bTranscript::new(b"akita-corrupt-proof"); + // The selected schedule row leads the opening; Akita's messages follow. + for position in [0, 32, proof.len() / 2, proof.len() - 1] { + let mut tampered = proof.clone(); + tampered[position] ^= 1; + let mut transcript = new_verifier_transcript(b"akita-corrupt-proof", &tampered); assert!( ::verify_batch( &verifier_setup, - &statement.clone(), - &tampered, + &statement, &mut transcript, ) .is_err(), - "tampered {field} should reject" + "a flipped bit at proof byte {position} should reject" ); } } +/// A commitment's wire form decodes only from the exact bytes +/// `send_commitment` writes: a known flavor, the flavor's chunk size, +/// canonical coefficients, and nothing after them. +#[test] +fn akita_commitment_wire_form_rejects_noncanonical_encodings() { + let (verifier_setup, statement, _) = native_proof_fixture(b"akita-commitment-wire"); + let commitment = &statement[0].commitment; + let mut prover_transcript = new_prover_transcript(b"akita-commitment-wire"); + AkitaScheme::send_commitment(commitment, &mut prover_transcript); + let wire = prover_transcript.finish(); + let receive = |bytes: &[u8]| { + let mut transcript = new_verifier_transcript(b"akita-commitment-wire", bytes); + let received = AkitaScheme::receive_commitment(&verifier_setup, &mut transcript)?; + transcript.finish()?; + Ok::<_, OpeningsError>(received) + }; + assert_eq!(receive(&wire).as_ref(), Ok(commitment)); + + // Header: flavor tag, 32-byte layout digest, then u64 num_vars, + // poly_count, one_hot_k, and coefficient count. + let mut unknown_flavor = wire.clone(); + unknown_flavor[0] = 2; + assert!(receive(&unknown_flavor).is_err(), "unknown flavor tag"); + + let mut dense_chunk_size = wire.clone(); + dense_chunk_size[1 + 32 + 16] = 16; + assert!( + receive(&dense_chunk_size).is_err(), + "a dense commitment must carry no one-hot chunk size" + ); + + let mut noncanonical = wire.clone(); + let coefficient_start = noncanonical.len() - 16; + noncanonical[coefficient_start..].fill(0xff); + assert!( + receive(&noncanonical).is_err(), + "a coefficient at or above the modulus" + ); + + let mut trailing = wire.clone(); + trailing.push(0); + assert_eq!( + receive(&trailing), + Err(OpeningsError::Transcript(TranscriptError::TrailingBytes)) + ); + assert!( + receive(&wire[..wire.len() - 1]).is_err(), + "truncated payload" + ); +} + #[test] fn akita_zk_interfaces_are_explicitly_unsupported() { let (prover_setup, verifier_setup) = native_setup(); @@ -201,8 +248,8 @@ fn akita_zk_interfaces_are_explicitly_unsupported() { let eval = poly.evaluate(&point); let (commitment, hint) = AkitaScheme::commit_zk(&poly, &prover_setup).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(b"akita-zk-unsupported"); - let (proof, _, ()) = AkitaScheme::open_zk( + let mut prover_transcript = new_prover_transcript(b"akita-zk-unsupported"); + let _ = AkitaScheme::open_zk( &poly, &point, eval, @@ -211,18 +258,18 @@ fn akita_zk_interfaces_are_explicitly_unsupported() { &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.finish(); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-zk-unsupported"); + let mut verifier_transcript = new_verifier_transcript(b"akita-zk-unsupported", &proof); assert_transparent_zk_error(AkitaScheme::verify_zk( &commitment, &point, - &proof, &verifier_setup, &mut verifier_transcript, )); let zk_point = Point::::high_to_low(point); - let mut transcript = Blake2bTranscript::new(b"akita-zk-batch-prove-unsup"); + let mut transcript = new_prover_transcript(b"akita-zk-batch-prove-unsup"); assert_transparent_zk_error( ::prove_batch_zk( &prover_setup, @@ -235,13 +282,12 @@ fn akita_zk_interfaces_are_explicitly_unsupported() { ), ); - let mut transcript = Blake2bTranscript::new(b"akita-zk-batch-verify-unsup"); + let mut transcript = new_verifier_transcript(b"akita-zk-batch-verify-unsup", &proof); assert_transparent_zk_error( ::verify_batch_zk( &verifier_setup, zk_point, vec![commitment], - &proof, &mut transcript, ), ); @@ -249,7 +295,7 @@ fn akita_zk_interfaces_are_explicitly_unsupported() { fn native_proof_fixture( label: &'static [u8], -) -> (VerifierSetup, AkitaNativeBatchStatement, AkitaBatchProof) { +) -> (VerifierSetup, AkitaNativeBatchStatement, Vec) { let (prover_setup, verifier_setup) = native_setup(); let poly_a = polynomial(16, 1); let poly_b = polynomial(16, 20); @@ -261,8 +307,8 @@ fn native_proof_fixture( .expect("grouped commit should succeed"); let statement = support::native_statement(commitment, &point, [eval_a, eval_b]); - let mut transcript = Blake2bTranscript::new(label); - let proof = ::prove_batch( + let mut transcript = new_prover_transcript(label); + ::prove_batch( &prover_setup, statement.clone(), batch_polynomials([&poly_a, &poly_b]), @@ -270,27 +316,7 @@ fn native_proof_fixture( &mut transcript, ) .expect("black-box proof should be produced"); - (verifier_setup, statement, proof) -} - -fn mutate_byte_array_field(proof: &AkitaBatchProof, field: &str) -> AkitaBatchProof { - let mut value = serde_json::to_value(proof).expect("proof should serialize"); - let bytes = value - .get_mut(field) - .expect("proof should contain field") - .as_array_mut() - .expect("proof field should serialize as byte array"); - let first = bytes - .first_mut() - .expect("proof byte array should not be empty"); - let byte = u8::try_from( - first - .as_u64() - .expect("proof byte array element should be a number"), - ) - .expect("proof byte array element should fit in u8"); - *first = Value::from(byte ^ 1); - serde_json::from_value(value).expect("mutated proof should still deserialize") + (verifier_setup, statement, transcript.finish()) } fn assert_transparent_zk_error(result: Result) { diff --git a/crates/jolt-akita/tests/prefix_packing.rs b/crates/jolt-akita/tests/prefix_packing.rs index 104ffa8279..34d402d779 100644 --- a/crates/jolt-akita/tests/prefix_packing.rs +++ b/crates/jolt-akita/tests/prefix_packing.rs @@ -1,5 +1,6 @@ #![expect( clippy::unwrap_used, + clippy::expect_used, reason = "tests exercise successful PCS operations" )] @@ -7,7 +8,13 @@ use jolt_akita::{AkitaField, AkitaScheduleArtifacts, AkitaScheme, AkitaSetupPara use jolt_field::Ring; use jolt_openings::{CommitmentScheme, PrefixPackedClaims, PrefixPackedLayout}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use support::{new_prover_transcript, new_verifier_transcript}; + +#[expect( + dead_code, + reason = "shared integration-test support is compiled independently per test file" +)] +mod support; fn f(value: u64) -> AkitaField { AkitaField::from_u64(value) @@ -55,11 +62,11 @@ fn fixed_prefix_claim_opens_the_materialized_akita_polynomial() { )) .unwrap(); let (commitment, hint) = AkitaScheme::commit(&physical, &prover_setup).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(b"akita/fixed-prefix"); + let mut prover_transcript = new_prover_transcript(b"akita/fixed-prefix"); let physical_claim = layout .reduce_claims(&claims, &mut prover_transcript) .unwrap(); - let proof = AkitaScheme::open( + AkitaScheme::open( &physical, physical_claim.point.as_slice(), physical_claim.value, @@ -68,8 +75,9 @@ fn fixed_prefix_claim_opens_the_materialized_akita_polynomial() { &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.finish(); - let mut verifier_transcript = Blake2bTranscript::new(b"akita/fixed-prefix"); + let mut verifier_transcript = new_verifier_transcript(b"akita/fixed-prefix", &proof); let verifier_claim = layout .reduce_claims(&claims, &mut verifier_transcript) .unwrap(); @@ -77,7 +85,6 @@ fn fixed_prefix_claim_opens_the_materialized_akita_polynomial() { &commitment, verifier_claim.point.as_slice(), verifier_claim.value, - &proof, &verifier_setup, &mut verifier_transcript, ) @@ -115,11 +122,11 @@ fn changed_fixed_prefix_statement_rejects_the_original_proof() { )) .unwrap(); let (commitment, hint) = AkitaScheme::commit(&physical, &prover_setup).unwrap(); - let mut prover_transcript = Blake2bTranscript::new(b"akita/fixed-prefix-tamper"); + let mut prover_transcript = new_prover_transcript(b"akita/fixed-prefix-tamper"); let physical_claim = layout .reduce_claims(&claims, &mut prover_transcript) .unwrap(); - let proof = AkitaScheme::open( + AkitaScheme::open( &physical, physical_claim.point.as_slice(), physical_claim.value, @@ -128,8 +135,10 @@ fn changed_fixed_prefix_statement_rejects_the_original_proof() { &mut prover_transcript, ) .unwrap(); + let proof = prover_transcript.finish(); - let mut honest_verifier_transcript = Blake2bTranscript::new(b"akita/fixed-prefix-tamper"); + let mut honest_verifier_transcript = + new_verifier_transcript(b"akita/fixed-prefix-tamper", &proof); let honest_claim = layout .reduce_claims(&claims, &mut honest_verifier_transcript) .unwrap(); @@ -137,7 +146,6 @@ fn changed_fixed_prefix_statement_rejects_the_original_proof() { &commitment, honest_claim.point.as_slice(), honest_claim.value, - &proof, &verifier_setup, &mut honest_verifier_transcript, ) @@ -145,7 +153,7 @@ fn changed_fixed_prefix_statement_rejects_the_original_proof() { evaluations[1] += f(1); let changed = PrefixPackedClaims::new(digest, logical_point, evaluations); - let mut verifier_transcript = Blake2bTranscript::new(b"akita/fixed-prefix-tamper"); + let mut verifier_transcript = new_verifier_transcript(b"akita/fixed-prefix-tamper", &proof); let changed_claim = layout .reduce_claims(&changed, &mut verifier_transcript) .unwrap(); @@ -153,7 +161,6 @@ fn changed_fixed_prefix_statement_rejects_the_original_proof() { &commitment, changed_claim.point.as_slice(), changed_claim.value, - &proof, &verifier_setup, &mut verifier_transcript, ) diff --git a/crates/jolt-akita/tests/schedules.rs b/crates/jolt-akita/tests/schedules.rs index f3f47df8bb..10507b3cdc 100644 --- a/crates/jolt-akita/tests/schedules.rs +++ b/crates/jolt-akita/tests/schedules.rs @@ -11,8 +11,8 @@ use std::path::PathBuf; use akita_config::{SetupRequirements, TrustedScheduleCatalog}; use akita_schedules::{ResolvedScheduleRow, ValidatedScheduleCatalog}; use akita_types::{ - commit_only_setup_field_elements, setup_matrix_capacity_for_schedule, AkitaScheduleLookupKey, - FoldSchedule, PolynomialGroupLayout, + commit_only_setup_field_elements, setup_matrix_capacity_for_schedule, FoldSchedule, + PolynomialGroupLayout, ScheduleLookupKey, }; use jolt_akita::configs::{JoltOneHotK16, JoltOneHotK256}; use jolt_akita::schedule_registry::{ @@ -55,7 +55,7 @@ fn catalogs_cover_every_reachable_one_hot_trace_shape() { assert!(!grid.is_empty()); for key in &grid { let resolved = catalog - .resolve_key(&AkitaScheduleLookupKey::single(*key)) + .resolve_key(&ScheduleLookupKey::single(*key)) .expect("reachable scalar shape must resolve"); assert!(resolved.profiles().precommitteds.is_empty()); } @@ -65,7 +65,7 @@ fn catalogs_cover_every_reachable_one_hot_trace_shape() { fn scalar_schedule(catalog: &ValidatedScheduleCatalog, num_vars: usize) -> FoldSchedule { catalog - .resolve_key(&AkitaScheduleLookupKey::single(PolynomialGroupLayout::new( + .resolve_key(&ScheduleLookupKey::single(PolynomialGroupLayout::new( num_vars, 1, ))) .expect("cutover row must resolve") @@ -101,10 +101,10 @@ fn one_hot_catalogs_switch_to_setup_offloading_at_the_trace_cutover() { const TRUSTED_ADVICE_GROUP: PolynomialGroupLayout = PolynomialGroupLayout::new(20, 1); const TRUSTED_ADVICE_K256_FINAL_GROUP: PolynomialGroupLayout = PolynomialGroupLayout::new(39, 1); -fn trusted_advice_grouped_key(dense: &ValidatedScheduleCatalog) -> AkitaScheduleLookupKey { +fn trusted_advice_grouped_key(dense: &ValidatedScheduleCatalog) -> ScheduleLookupKey { let trusted_profile = dense_group_profile(dense, TRUSTED_ADVICE_GROUP) .expect("trusted advice standalone row must resolve"); - AkitaScheduleLookupKey { + ScheduleLookupKey { final_group: TRUSTED_ADVICE_K256_FINAL_GROUP, precommitteds: vec![trusted_profile], } @@ -116,7 +116,7 @@ fn assert_adaptation_preserves_main_skeleton( final_group: PolynomialGroupLayout, ) { let main = base - .resolve_key(&AkitaScheduleLookupKey::single(final_group)) + .resolve_key(&ScheduleLookupKey::single(final_group)) .expect("main scalar row"); assert_eq!( resolved.schedule().root.params.own_group(), @@ -206,7 +206,7 @@ fn grouped_adaptation_preserves_direct_and_recursive_k16_trace_skeletons() { .expect("freeze adapted K=16 catalog"); let final_group = PolynomialGroupLayout::new(final_num_vars, 1); let resolved = setup_catalog - .resolve_key(&AkitaScheduleLookupKey { + .resolve_key(&ScheduleLookupKey { final_group, precommitteds: vec![precommit], }) @@ -270,7 +270,7 @@ fn base_catalogs_contain_no_grouped_advice_rows() { vec![trusted_profile, trusted_profile], ] { for num_vars in FIXTURE_K16_FINAL_NUM_VARS.0..=FIXTURE_K16_FINAL_NUM_VARS.1 { - let key = AkitaScheduleLookupKey { + let key = ScheduleLookupKey { final_group: PolynomialGroupLayout::new(num_vars, 1), precommitteds: precommitteds.clone(), }; @@ -366,7 +366,7 @@ mod field_inc { )] use akita_config::CommitmentConfig; - use akita_types::{AkitaScheduleLookupKey, PolynomialGroupLayout}; + use akita_types::{PolynomialGroupLayout, ScheduleLookupKey}; use jolt_akita::configs::{JoltOneHotK16, JoltOneHotK256}; use jolt_akita::schedule_registry::GroupedScheduleParams; use jolt_akita::schedule_registry::{ @@ -413,7 +413,7 @@ mod field_inc { 1, "K={one_hot_k} final arity {final_num_vars} must plan its field-inline row" ); - let key = AkitaScheduleLookupKey { + let key = ScheduleLookupKey { final_group: PolynomialGroupLayout::new(final_num_vars, 1), precommitteds: vec![dense_group_profile( &full_dense, @@ -516,7 +516,7 @@ mod field_inc { dense_group_profile(&full_dense, PolynomialGroupLayout::new(num_vars, 1)) .expect("full-width advice-shaped profile"); assert!(catalog - .resolve_key(&AkitaScheduleLookupKey { + .resolve_key(&ScheduleLookupKey { final_group: PolynomialGroupLayout::new(final_num_vars, 1), precommitteds: vec![widened_advice, inc], }) diff --git a/crates/jolt-akita/tests/scheme_surface.rs b/crates/jolt-akita/tests/scheme_surface.rs index 19f8dc267f..96f0548c15 100644 --- a/crates/jolt-akita/tests/scheme_surface.rs +++ b/crates/jolt-akita/tests/scheme_surface.rs @@ -10,13 +10,16 @@ mod support; use jolt_akita::{ - AkitaBackendFlavor, AkitaField, AkitaScheduleArtifacts, AkitaScheme, AkitaSetupParams, - AKITA_ONE_HOT_K16, + AkitaBackendFlavor, AkitaHidingCommitment, AkitaScheduleArtifacts, AkitaScheme, + AkitaSetupParams, AKITA_ONE_HOT_K16, }; +use jolt_field::{CanonicalBytes, CanonicalDecode}; use jolt_openings::{CommitmentScheme, OpeningsError, ZkOpeningScheme}; use jolt_poly::{MultilinearPoly, OneHotPolynomial}; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; -use support::{f, layout, polynomial, setup_for}; +use support::{ + assert_transcripts_agree, f, layout, new_prover_transcript, new_verifier_transcript, + polynomial, setup_for, +}; /// The smallest dense dimension the checked-in catalog schedules. const DENSE_VARS: usize = 14; @@ -95,8 +98,8 @@ fn single_k16_one_hot_commit_roundtrips_with_transported_verifier_setup() { let point: Vec<_> = (0..ONE_HOT_VARS).map(|index| f(index as u64 + 5)).collect(); let eval = MultilinearPoly::evaluate(&one_hot, &point); - let mut prover_transcript = Blake2bTranscript::new(b"akita-k16-transported"); - let proof = AkitaScheme::open( + let mut prover_transcript = new_prover_transcript(b"akita-k16-transported"); + AkitaScheme::open( &one_hot, &point, eval, @@ -105,23 +108,23 @@ fn single_k16_one_hot_commit_roundtrips_with_transported_verifier_setup() { &mut prover_transcript, ) .expect("one-hot opening should prove"); + let proof = prover_transcript.narg().to_vec(); let json = serde_json::to_string(&verifier_setup).expect("verifier setup serializes"); let transported: ::VerifierSetup = serde_json::from_str(&json).expect("verifier setup deserializes"); assert_eq!(transported, verifier_setup); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-k16-transported"); + let mut verifier_transcript = new_verifier_transcript(b"akita-k16-transported", &proof); AkitaScheme::verify( &commitment, &point, eval, - &proof, &transported, &mut verifier_transcript, ) .expect("transported setup must re-derive the one-hot backend key"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); } /// Without a commit-time hint, `open` re-commits internally; the resulting @@ -134,8 +137,8 @@ fn open_without_hint_recommits_deterministically() { let eval = poly.evaluate(&point); let (commitment, _) = AkitaScheme::commit(&poly, &prover_setup).expect("commit succeeds"); - let mut prover_transcript = Blake2bTranscript::new(b"akita-no-hint"); - let proof = AkitaScheme::open( + let mut prover_transcript = new_prover_transcript(b"akita-no-hint"); + AkitaScheme::open( &poly, &point, eval, @@ -144,32 +147,32 @@ fn open_without_hint_recommits_deterministically() { &mut prover_transcript, ) .expect("hint-free opening should prove"); + let proof = prover_transcript.narg().to_vec(); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-no-hint"); + let mut verifier_transcript = new_verifier_transcript(b"akita-no-hint", &proof); AkitaScheme::verify( &commitment, &point, eval, - &proof, &verifier_setup, &mut verifier_transcript, ) .expect("hint-free proof should verify against the original commitment"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + assert_transcripts_agree(prover_transcript, verifier_transcript); } -/// The hiding commitment binds the committed evaluation bytes into the -/// transcript: equal evaluations bind identically, distinct ones diverge. +/// The transparent hiding commitment is the evaluation's canonical encoding: +/// it round-trips through the checked decoder and tracks the evaluation. #[test] -fn hiding_commitment_transcript_binding_tracks_the_evaluation() { +fn hiding_commitment_encodes_the_evaluation() { let (prover_setup, _) = setup_for(DENSE_VARS, 1, layout(7)); let poly = polynomial(DENSE_VARS, 50); let point: Vec<_> = (0..DENSE_VARS).map(|index| f(index as u64 + 2)).collect(); let eval = poly.evaluate(&point); let (_, hint) = AkitaScheme::commit_zk(&poly, &prover_setup).expect("commit_zk succeeds"); - let mut transcript = Blake2bTranscript::new(b"akita-hiding"); - let (_, hiding, ()) = AkitaScheme::open_zk( + let mut transcript = new_prover_transcript(b"akita-hiding"); + let (hiding, ()) = AkitaScheme::open_zk( &poly, &point, eval, @@ -178,19 +181,19 @@ fn hiding_commitment_transcript_binding_tracks_the_evaluation() { &mut transcript, ) .expect("open_zk should produce a hiding commitment"); - - let mut first = Blake2bTranscript::::new(b"akita-hiding-bind"); - hiding.append_to_transcript(&mut first); - let mut second = Blake2bTranscript::::new(b"akita-hiding-bind"); - hiding.append_to_transcript(&mut second); - assert_eq!(first.state(), second.state(), "binding is deterministic"); + let encoded = hiding.to_bytes_le_vec(); + assert_eq!(encoded, eval.to_bytes_le_vec()); + assert_eq!( + AkitaHidingCommitment::from_bytes_le_checked(&encoded), + Some(hiding.clone()) + ); let mut other_point = point.clone(); other_point[DENSE_VARS - 1] += f(1); let other_eval = poly.evaluate(&other_point); assert_ne!(eval, other_eval, "fixture needs distinct evaluations"); - let mut transcript = Blake2bTranscript::new(b"akita-hiding"); - let (_, other_hiding, ()) = AkitaScheme::open_zk( + let mut transcript = new_prover_transcript(b"akita-hiding"); + let (other_hiding, ()) = AkitaScheme::open_zk( &poly, &other_point, other_eval, @@ -199,11 +202,8 @@ fn hiding_commitment_transcript_binding_tracks_the_evaluation() { &mut transcript, ) .expect("open_zk should produce a hiding commitment"); - let mut third = Blake2bTranscript::::new(b"akita-hiding-bind"); - other_hiding.append_to_transcript(&mut third); assert_ne!( - first.state(), - third.state(), - "distinct evaluations must bind distinct transcript states" + hiding, other_hiding, + "distinct evaluations must encode distinctly" ); } diff --git a/crates/jolt-akita/tests/support/mod.rs b/crates/jolt-akita/tests/support/mod.rs index d4d4d84753..b01bd4611d 100644 --- a/crates/jolt-akita/tests/support/mod.rs +++ b/crates/jolt-akita/tests/support/mod.rs @@ -5,6 +5,35 @@ use jolt_akita::{ use jolt_field::Ring; use jolt_openings::{CommitmentScheme, EvaluationClaim, VerifierOpeningClaim}; use jolt_poly::{MultilinearPoly, Polynomial}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; + +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-akita/test"); + +pub fn new_prover_transcript(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&PROTOCOL, session) +} + +pub fn new_verifier_transcript<'a>( + session: &[u8], + proof: &'a [u8], +) -> VerifierTranscript<'a, Blake2b512> { + VerifierTranscript::new(&PROTOCOL, session, proof) +} + +/// The verifier consumed exactly the prover's argument string and ended in the +/// same sponge state. +pub fn assert_transcripts_agree( + mut prover: ProverTranscript, + mut verifier: VerifierTranscript<'_, Blake2b512>, +) { + assert_eq!( + prover.challenge_bytes::<32>(), + verifier.challenge_bytes::<32>() + ); + verifier + .finish() + .expect("the verifier consumes the whole proof"); +} pub fn f(value: u64) -> AkitaField { AkitaField::from_u64(value) diff --git a/crates/jolt-dory/Cargo.toml b/crates/jolt-dory/Cargo.toml index 16f13dc998..e4fa86ff7b 100644 --- a/crates/jolt-dory/Cargo.toml +++ b/crates/jolt-dory/Cargo.toml @@ -31,7 +31,7 @@ profiling = ["jolt-akita/profiling"] [dev-dependencies] jolt-akita.workspace = true akita-config.workspace = true -akita-pcs = { workspace = true, features = ["parallel", "transcript-blake2b"] } +akita-pcs = { workspace = true, features = ["parallel"] } akita-types.workspace = true jolt-profiling = { path = "../jolt-profiling", default-features = false } criterion = { workspace = true } diff --git a/crates/jolt-dory/benches/akita_paths.rs b/crates/jolt-dory/benches/akita_paths.rs index 052e4bc2a6..5c01b4074d 100644 --- a/crates/jolt-dory/benches/akita_paths.rs +++ b/crates/jolt-dory/benches/akita_paths.rs @@ -12,8 +12,8 @@ //! uses the same one-hot data expanded into a dense evaluation table, forcing //! the slower dense path and making the expected sparse speedup visible. The //! `akita_prover` groups use the same Akita-order data without Jolt commitment -//! wrappers or transcript bridging; those numbers bound the overhead introduced -//! by the adapter. The Dory groups run the same logical dense, +//! wrappers, proving on Akita's standalone transcript; those numbers bound the +//! overhead introduced by the adapter. The Dory groups run the same logical dense, //! sparse one-hot, and sparse materialized inputs over BN254 Fr as a familiar //! PCS baseline, not as a field-for-field security comparison. //! @@ -54,9 +54,10 @@ use jolt_openings::{ VerifierOpeningClaim, }; use jolt_poly::{MultilinearPoly, OneHotPolynomial, Polynomial}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript}; const LAYOUT_DIGEST: [u8; 32] = [0xA5; 32]; +const BENCH_PROTOCOL: ProtocolId = ProtocolId::new::("jolt-akita/bench"); const NUM_POLYS: usize = 1; const BATCH_POLYS: usize = 4; const BATCH_PREFIX_BITS: usize = 2; @@ -482,12 +483,16 @@ fn native_batch_commit(case: &AkitaBatchCase) -> (jolt_akita::AkitaCommitment, A .expect("black-box batch commit should succeed") } +fn bench_transcript(session: &[u8]) -> ProverTranscript { + ProverTranscript::new(&BENCH_PROTOCOL, session) +} + fn native_batch_open( case: &AkitaBatchCase, commitment: jolt_akita::AkitaCommitment, hint: AkitaProverHint, -) -> jolt_akita::AkitaBatchProof { - let mut transcript = Blake2bTranscript::new(b"jolt-akita/black-box-batch-bench"); +) -> Vec { + let mut transcript = bench_transcript(b"jolt-akita/black-box-batch-bench"); ::prove_batch( &case.native_setup, native_batch_statement(case, commitment), @@ -495,18 +500,16 @@ fn native_batch_open( hint, &mut transcript, ) - .expect("black-box batch proof should succeed") + .expect("black-box batch proof should succeed"); + transcript.finish() } fn packed_batch_commit(case: &AkitaBatchCase) -> (jolt_akita::AkitaCommitment, AkitaProverHint) { AkitaScheme::commit(black_box(&case.packed_polynomial), &case.packed_pcs_setup).unwrap() } -fn packed_batch_open( - case: &AkitaBatchCase, - hint: AkitaProverHint, -) -> ::Proof { - let mut transcript = Blake2bTranscript::new(b"jolt-akita/packed-batch-bench"); +fn packed_batch_open(case: &AkitaBatchCase, hint: AkitaProverHint) -> Vec { + let mut transcript = bench_transcript(b"jolt-akita/packed-batch-bench"); let physical = case .packing .reduce_claims(&case.packed_claims, &mut transcript) @@ -519,7 +522,8 @@ fn packed_batch_open( Some(hint), &mut transcript, ) - .expect("packed batch proof should succeed") + .expect("packed batch proof should succeed"); + transcript.finish() } fn wrapper_commit( @@ -542,12 +546,8 @@ fn wrapper_commit( } } -fn wrapper_open( - case: &AkitaCase, - path: DataPath, - hint: AkitaProverHint, -) -> jolt_akita::AkitaBatchProof { - let mut transcript = Blake2bTranscript::new(b"jolt-akita/bench"); +fn wrapper_open(case: &AkitaCase, path: DataPath, hint: AkitaProverHint) -> Vec { + let mut transcript = bench_transcript(b"jolt-akita/bench"); match path { DataPath::DenseData => AkitaScheme::open( &case.dense_poly, @@ -556,8 +556,7 @@ fn wrapper_open( &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), DataPath::SparseDataSparsePath => AkitaScheme::open( &case.sparse_one_hot, &case.point, @@ -565,8 +564,7 @@ fn wrapper_open( &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), DataPath::SparseDataDensePath => AkitaScheme::open( &case.sparse_dense_poly, &case.point, @@ -574,9 +572,10 @@ fn wrapper_open( &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), } + .unwrap(); + transcript.finish() } const NATIVE_BENCH_SESSION: &[u8] = b"jolt-akita/native-bench"; @@ -636,7 +635,7 @@ fn akita_prover_open_dense( ) -> Vec { case.akita_prover_setup .dense_scheme - .batched_prove( + .prove_standalone( &case.akita_prover_setup.dense_prover, akita_prover_claims::( case.akita_prover_setup.dense_scheme.schedules(), @@ -661,7 +660,7 @@ fn akita_prover_open_one_hot( let backend_point = reverse_point(&case.point); case.akita_prover_setup .one_hot_scheme - .batched_prove( + .prove_standalone( &case.akita_prover_setup.one_hot_prover, akita_prover_claims::( case.akita_prover_setup.one_hot_scheme.schedules(), @@ -691,8 +690,8 @@ fn dory_commit(case: &DoryCase, path: DataPath) -> (DoryCommitment, DoryHint) { } } -fn dory_open(case: &DoryCase, path: DataPath, hint: DoryHint) -> jolt_dory::DoryProof { - let mut transcript = Blake2bTranscript::new(b"jolt-akita/dory-baseline-bench"); +fn dory_open(case: &DoryCase, path: DataPath, hint: DoryHint) -> Vec { + let mut transcript = bench_transcript(b"jolt-akita/dory-baseline-bench"); match path { DataPath::DenseData => DoryScheme::open( &case.dense_poly, @@ -701,8 +700,7 @@ fn dory_open(case: &DoryCase, path: DataPath, hint: DoryHint) -> jolt_dory::Dory &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), DataPath::SparseDataSparsePath => DoryScheme::open( &case.sparse_one_hot, &case.point, @@ -710,8 +708,7 @@ fn dory_open(case: &DoryCase, path: DataPath, hint: DoryHint) -> jolt_dory::Dory &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), DataPath::SparseDataDensePath => DoryScheme::open( &case.sparse_dense_poly, &case.point, @@ -719,9 +716,10 @@ fn dory_open(case: &DoryCase, path: DataPath, hint: DoryHint) -> jolt_dory::Dory &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), } + .unwrap(); + transcript.finish() } fn bench_jolt_akita_commit(c: &mut Criterion) { diff --git a/crates/jolt-prover/src/akita/mod.rs b/crates/jolt-prover/src/akita/mod.rs index cd372a686d..bcd2404466 100644 --- a/crates/jolt-prover/src/akita/mod.rs +++ b/crates/jolt-prover/src/akita/mod.rs @@ -6,7 +6,7 @@ use jolt_akita::TraceOneHotCommitment; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldInlineCommittedPolynomial; use jolt_crypto::VectorCommitment; -use jolt_field::{CanonicalBytes, JoltField}; +use jolt_field::JoltField; #[cfg(feature = "field-inline")] use jolt_kernels::FieldInlineWitnessCommitment; use jolt_kernels::{ @@ -16,7 +16,7 @@ use jolt_kernels::{ use jolt_openings::{ CommitmentScheme, GroupCommitmentMetadata, GroupSetupMetadata, TransparentObjectSetup, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::Sponge; use jolt_verifier::proof::JoltProof; use jolt_witness::{JoltWitnessPlane, RowSource}; @@ -203,28 +203,29 @@ where } } -/// Prove one execution using the packed Akita commitment path. +/// Prove one execution using the packed Akita commitment path, returning the +/// argument string. Akita's opening runs on the Jolt transcript, so its +/// messages are the proof's last ones. /// /// Trusted advice is precommitted; untrusted advice is committed from public input. -pub fn prove( +pub fn prove( backend: &JoltAkitaBackend, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&AdviceObject>, witness: &W, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result> where - F: JoltField + CanonicalBytes + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme + TransparentObjectSetup + TraceOneHotCommitment, PCS::ProverSetup: GroupSetupMetadata, - PCS::Output: Clone + PartialEq + AppendToTranscript + GroupCommitmentMetadata, + PCS::Output: Clone + PartialEq + GroupCommitmentMetadata, VC: VectorCommitment, - VC::Output: Clone + AppendToTranscript, - T: Transcript, + H: Sponge, W: JoltWitnessPlane, { - prover::prove::( + prover::prove::( backend, preprocessing, config, diff --git a/crates/jolt-prover/src/akita/preprocessing.rs b/crates/jolt-prover/src/akita/preprocessing.rs index f73ffdb0fd..7954c0f03f 100644 --- a/crates/jolt-prover/src/akita/preprocessing.rs +++ b/crates/jolt-prover/src/akita/preprocessing.rs @@ -11,7 +11,6 @@ use jolt_claims::protocols::jolt::{JoltAdviceKind, TracePolynomialOrder}; use jolt_crypto::NoVectorCommitment; use jolt_openings::{CommitmentScheme, TransparentObjectSetup}; use jolt_program::preprocess::JoltProgramPreprocessing; -use jolt_transcript::LegacyBlake2bTranscript; use jolt_verifier::{ CommittedProgramPreprocessing, JoltVerifierPreprocessing, ProgramPreprocessing, }; @@ -24,7 +23,6 @@ use super::one_hot_trace_setup_shape; use super::witness::{commit_advice, commit_direct_program, AdviceObject}; pub type AkitaVc = NoVectorCommitment; -pub type AkitaTranscript = LegacyBlake2bTranscript; pub type AkitaProverPreprocessing = JoltProverPreprocessing; pub type AkitaVerifierPreprocessing = JoltVerifierPreprocessing; diff --git a/crates/jolt-prover/src/akita/prover.rs b/crates/jolt-prover/src/akita/prover.rs index 0862b2d027..029033cee6 100644 --- a/crates/jolt-prover/src/akita/prover.rs +++ b/crates/jolt-prover/src/akita/prover.rs @@ -1,17 +1,17 @@ //! The packed top-level prover: the stage recipes run in protocol order on -//! one transcript and one backend session, and their wire outputs assemble -//! into the packed-envelope [`JoltProof`]. +//! one transcript and one backend session, and the transcript's argument +//! string is the [`JoltProof`]; Akita's opening messages are its tail. use common::jolt_device::JoltDevice; use jolt_akita::TraceOneHotCommitment; use jolt_crypto::VectorCommitment; -use jolt_field::{CanonicalBytes, JoltField}; +use jolt_field::JoltField; use jolt_openings::{ CommitmentScheme, GroupCommitmentMetadata, GroupSetupMetadata, TransparentObjectSetup, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::Sponge; use jolt_verifier::config::JoltProtocolConfig; -use jolt_verifier::proof::{ClearProofClaims, JoltProof, JoltProofClaims, JoltStageProofs}; +use jolt_verifier::proof::JoltProof; use jolt_witness::JoltWitnessPlane; use super::stage0::prove_stage0; @@ -31,22 +31,21 @@ use crate::{JoltProverPreprocessing, ProofMode, ProverConfig, ProverError}; /// See [`super::prove`]. #[tracing::instrument(skip_all, name = "jolt_prover::prove", fields(trace_length = config.trace_length))] -pub fn prove( +pub fn prove( backend: &JoltAkitaBackend, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&AdviceObject>, witness: &W, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result> where - F: JoltField + CanonicalBytes + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme + TransparentObjectSetup + TraceOneHotCommitment, PCS::ProverSetup: GroupSetupMetadata, - PCS::Output: Clone + PartialEq + AppendToTranscript + GroupCommitmentMetadata, + PCS::Output: Clone + PartialEq + GroupCommitmentMetadata, VC: VectorCommitment, - VC::Output: Clone + AppendToTranscript, - T: Transcript, + H: Sponge, W: JoltWitnessPlane, { // The packed path is transparent-only (`akita` and `zk` are mutually @@ -54,7 +53,7 @@ where // recipes still thread it to mint their clear recorders. let mode = ProofMode::::new(None)?; let mut session = backend.begin_proof(); - let stage0 = prove_stage0::( + let stage0 = prove_stage0::( preprocessing, config, trusted_advice, @@ -68,7 +67,7 @@ where let checked = stage0.checked; let mut transcript = stage0.transcript; - let stage1 = prove_stage1::( + let stage1 = prove_stage1::( &backend.base, &mut session, &mode, @@ -77,7 +76,7 @@ where &mut transcript, )?; finish_stage("stage1", log_t, &session, &stage1.clear_output); - let stage2 = prove_stage2::( + let stage2 = prove_stage2::( &backend.base, &mut session, &mode, @@ -88,7 +87,7 @@ where &mut transcript, )?; finish_stage("stage2", log_t, &session, &stage2.clear_output); - let stage3 = prove_stage3::( + let stage3 = prove_stage3::( &backend.base, &mut session, &mode, @@ -99,7 +98,7 @@ where &mut transcript, )?; finish_stage("stage3", log_t, &session, &stage3.clear_output); - let stage4 = prove_stage4::( + let stage4 = prove_stage4::( &backend.base, &mut session, &mode, @@ -112,7 +111,7 @@ where &mut transcript, )?; finish_stage("stage4", log_t, &session, &stage4.clear_output); - let stage5 = prove_stage5::( + let stage5 = prove_stage5::( &backend.base, &mut session, &mode, @@ -125,7 +124,7 @@ where &mut transcript, )?; finish_stage("stage5", log_t, &session, &stage5.clear_output); - let stage6a = prove_stage6a::( + let stage6a = prove_stage6a::( &backend.base, &mut session, &mode, @@ -141,7 +140,7 @@ where &mut transcript, )?; finish_stage("stage6a", log_t, &session, &stage6a.clear_output); - let stage6b = prove_stage6b::( + let stage6b = prove_stage6b::( &backend.base, &mut session, &mode, @@ -158,7 +157,7 @@ where &mut transcript, )?; finish_stage("stage6b", log_t, &session, &stage6b.clear_output); - let stage7 = prove_stage7::( + let stage7 = prove_stage7::( &backend.base, &mut session, &mode, @@ -171,7 +170,7 @@ where &mut transcript, )?; finish_stage("stage7", log_t, &session, &stage7.clear_output); - let joint_opening_proof = prove_stage8::( + prove_stage8::( &checked, config, preprocessing, @@ -194,39 +193,6 @@ where Ok(JoltProof { protocol: JoltProtocolConfig::for_zk(false), - commitments: stage0.commitment, - stages: JoltStageProofs { - stage1_uni_skip_first_round_proof: stage1.uniskip_proof, - stage1_sumcheck_proof: stage1.sumcheck_proof, - stage2_uni_skip_first_round_proof: stage2.uniskip_proof, - stage2_sumcheck_proof: stage2.sumcheck_proof, - stage3_sumcheck_proof: stage3.sumcheck_proof, - stage4_sumcheck_proof: stage4.sumcheck_proof, - stage5_sumcheck_proof: stage5.sumcheck_proof, - stage6a_sumcheck_proof: stage6a.sumcheck_proof, - stage6b_sumcheck_proof: stage6b.sumcheck_proof, - stage7_sumcheck_proof: stage7.sumcheck_proof, - }, - joint_opening_proof, - untrusted_advice_commitment: stage0 - .untrusted_advice - .map(|object| object.commitment.clone()), - #[cfg(feature = "field-inline")] - field_inc_commitment: Some(stage0.field_inc.commitment), - claims: JoltProofClaims::Clear(ClearProofClaims { - stage1: stage1.claims, - stage2: stage2.claims, - stage3: stage3.claims, - stage4: stage4.claims, - stage5: stage5.claims, - stage6a: stage6a.claims, - stage6b: stage6b.claims, - stage7: stage7.claims, - }), - trace_length: config.trace_length, - ram_K: config.ram_K, - rw_config: config.rw_config, - one_hot_config: config.one_hot_config, - trace_polynomial_order: config.trace_polynomial_order, + narg: transcript.finish(), }) } diff --git a/crates/jolt-prover/src/akita/stage0.rs b/crates/jolt-prover/src/akita/stage0.rs index 0163f3aa5f..2ace6b66c7 100644 --- a/crates/jolt-prover/src/akita/stage0.rs +++ b/crates/jolt-prover/src/akita/stage0.rs @@ -1,4 +1,10 @@ -//! Packed stage 0: input validation, commitments, and transcript setup. +//! Packed stage 0: input validation, the Fiat-Shamir preamble, and the +//! packed commitments. +//! +//! The transcript work is the verifier's own exported code +//! ([`validate_inputs`], [`ProofHeader::send`], [`absorb_public_preamble`], +//! [`ProofCommitments::send`], [`absorb_public_commitments`]), mirroring the +//! verifier's `seed_transcript` step for step. use common::jolt_device::JoltDevice; use std::sync::Arc; @@ -11,10 +17,10 @@ use jolt_field::JoltField; use jolt_openings::{ CommitmentGroupRole, CommitmentScheme, GroupSetupMetadata, TransparentObjectSetup, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{ProverTranscript, Sponge}; use jolt_verifier::{ - absorb_packed_commitments, absorb_transcript_preamble, validate_inputs_from_parts, - CheckedInputs, ProofTranscriptConfig, VerifierError, + absorb_public_commitments, absorb_public_preamble, jolt_protocol_id, validate_inputs, + CheckedInputs, ProofCommitments, ProofHeader, VerifierError, JOLT_SESSION, }; use jolt_witness::JoltWitnessPlane; @@ -23,13 +29,15 @@ use super::field_inline::FieldIncObject; use super::witness::{assemble_one_hot_trace_rows, commit_advice, AdviceObject}; use crate::{JoltProverPreprocessing, ProverConfig, ProverError}; -/// Outputs retained for later prover stages. -pub struct Stage0Output +/// Outputs retained for later prover stages. The transcript is positioned +/// exactly where the verifier's `seed_transcript` leaves its own. +pub struct Stage0Output where PCS: CommitmentScheme, + H: Sponge, { pub checked: CheckedInputs, - pub transcript: T, + pub transcript: ProverTranscript, pub commitment: PCS::Output, pub hint: PCS::OpeningHint, pub untrusted_advice: Option>, @@ -38,22 +46,24 @@ where pub field_inc: FieldIncObject, } -/// Validate inputs, commit the packed objects, and seed the transcript. +/// Validate inputs, send the proof header and absorb the public preamble, +/// commit the packed objects, send the per-proof commitments, and absorb the +/// public ones (trusted advice, then the direct committed-program objects). #[tracing::instrument(skip_all)] -pub fn prove_stage0( +pub fn prove_stage0( preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&AdviceObject>, witness: &W, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme + TransparentObjectSetup + TraceOneHotCommitment, PCS::ProverSetup: GroupSetupMetadata, - PCS::Output: Clone + AppendToTranscript, + PCS::Output: Clone, VC: VectorCommitment, - T: Transcript, + H: Sponge, W: JoltWitnessPlane, { if config.trace_polynomial_order != TracePolynomialOrder::CycleMajor { @@ -97,27 +107,24 @@ where } } let untrusted_advice_present = !public_io.untrusted_advice.is_empty(); - let checked = validate_inputs_from_parts( + let header = ProofHeader { + trace_length: config.trace_length, + ram_K: config.ram_K, + rw_config: config.rw_config, + one_hot_config: config.one_hot_config, + trace_polynomial_order: config.trace_polynomial_order, + untrusted_advice: untrusted_advice_present, + }; + let checked = validate_inputs( &preprocessing.verifier, public_io, - config.trace_length, - config.ram_K, - config.trace_polynomial_order, - config.one_hot_config, + &header, trusted_advice.is_some(), - false, )?; - let mut transcript = T::new(b"Jolt"); - absorb_transcript_preamble( - &checked, - ProofTranscriptConfig { - rw_config: config.rw_config, - one_hot_config: config.one_hot_config, - trace_polynomial_order: config.trace_polynomial_order, - }, - &mut transcript, - ); + let mut transcript = ProverTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION); + header.send(&mut transcript); + absorb_public_preamble(&checked, &mut transcript); let log_t = config.trace_length.ilog2() as usize; let log_k_chunk = config.one_hot_config.committed_chunk_bits(); @@ -237,17 +244,18 @@ where Ok::<_, ProverError>((commitment, hint)) })?; - absorb_packed_commitments( - &commitment, - untrusted_advice.as_ref().map(|object| &object.commitment), - trusted_advice.map(|object| &object.commitment), + ProofCommitments { + one_hot_trace: commitment.clone(), #[cfg(feature = "field-inline")] - Some(&field_inc.commitment), - preprocessing - .verifier - .program - .committed() - .map_or(&[][..], |committed| &committed.direct_program_commitments), + field_inc: field_inc.commitment.clone(), + untrusted_advice: untrusted_advice + .as_ref() + .map(|object| object.commitment.clone()), + } + .send::(&mut transcript); + absorb_public_commitments( + &preprocessing.verifier, + trusted_advice.map(|object| &object.commitment), &mut transcript, ); diff --git a/crates/jolt-prover/src/akita/stage8.rs b/crates/jolt-prover/src/akita/stage8.rs index 0e86437fd2..817b571bd1 100644 --- a/crates/jolt-prover/src/akita/stage8.rs +++ b/crates/jolt-prover/src/akita/stage8.rs @@ -13,7 +13,7 @@ use jolt_field::JoltField; use jolt_openings::{ CommitmentScheme, EvaluationClaim, GroupOpeningClaim, TaggedGroupOpeningClaim, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; use jolt_verifier::stages::stage4::outputs::Stage4ClearOutput; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::outputs::Stage7ClearOutput; @@ -35,14 +35,14 @@ fn batch_failed(reason: impl ToString) -> ProverError { }) } -fn reduce_precommitted( +fn reduce_precommitted( plan: &PrefixPackedObjectPlan, leaves: &BTreeMap>, - transcript: &mut T, + transcript: &mut C, ) -> Result, ProverError> where F: JoltField, - T: Transcript, + C: Channel, { let claims = object_leaf_claims(plan, leaves).map_err(ProverError::Verifier)?; let semantic = plan.packed_claims(&claims).map_err(batch_failed::)?; @@ -53,7 +53,7 @@ where #[expect(clippy::too_many_arguments, reason = "the stage's upstream carriers")] #[tracing::instrument(skip_all)] -pub fn prove_stage8( +pub fn prove_stage8( checked: &CheckedInputs, config: &ProverConfig, preprocessing: &JoltProverPreprocessing, @@ -66,14 +66,14 @@ pub fn prove_stage8( stage4: &Stage4ClearOutput, stage6b: &Stage6bClearOutput, stage7: &Stage7ClearOutput, - transcript: &mut T, -) -> Result> + transcript: &mut ProverTranscript, +) -> Result<(), ProverError> where F: JoltField, PCS: CommitmentScheme, - PCS::Output: Clone + AppendToTranscript, + PCS::Output: Clone, VC: VectorCommitment, - T: Transcript, + H: Sponge, { let log_t = checked.trace_length.ilog2() as usize; let chunk_width = config.one_hot_config.committed_chunk_bits(); @@ -159,7 +159,7 @@ where packed_claim.point.as_slice().to_vec(), vec![packed_claim.value], ); - let joint_opening_proof = tracing::info_span!("akita_main_batched_prove").in_scope(|| { + tracing::info_span!("akita_main_batched_prove").in_scope(|| { PCS::prove_batch( &preprocessing.pcs_setup, auxiliary_groups, @@ -168,6 +168,5 @@ where transcript, ) .map_err(batch_failed::) - })?; - Ok(joint_opening_proof) + }) } diff --git a/crates/jolt-prover/src/profile.rs b/crates/jolt-prover/src/profile.rs index 38159ff915..e157adb2ea 100644 --- a/crates/jolt-prover/src/profile.rs +++ b/crates/jolt-prover/src/profile.rs @@ -73,7 +73,6 @@ type ProfileTrace = OwnedTrace; type FieldInlineField = Fr; #[cfg(all(feature = "field-inline", feature = "akita"))] type FieldInlineField = AkitaField; -#[cfg(not(feature = "akita"))] use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; #[cfg(not(feature = "akita"))] @@ -896,7 +895,7 @@ fn prove_workload( trace_output: TraceOutput, backend: BackendKind, ) -> ProvenRun { - use crate::akita::preprocessing::{AkitaTranscript, AkitaVc}; + use crate::akita::preprocessing::AkitaVc; use crate::JoltProverPreprocessing; use jolt_akita::{AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_openings::CommitmentScheme; @@ -973,7 +972,7 @@ fn prove_workload( // The `jolt_prover::prove` root span covers exactly // this interval; the Instant is the `--format none` baseline. let now = Instant::now(); - let proof = crate::akita::prove::( + let proof = crate::akita::prove::( &backend, &prover_preprocessing, &config, @@ -984,23 +983,17 @@ fn prove_workload( .expect("modular packed prove"); let duration = now.elapsed(); - let akita_proof_body_size = proof.joint_opening_proof.backend_proof_body_size(); - let akita_opening_unframed_size = proof - .joint_opening_proof - .unframed_payload_size() - .expect("packed opening component lengths must fit usize"); let proof_size = bincode::serde::encode_to_vec(&proof, bincode::config::standard()) .expect("serialize packed proof") .len(); tracing::info!( - akita_proof_body_size, - akita_opening_unframed_size, + argument_string_size = proof.narg.len(), jolt_proof_wire_size = proof_size, "packed proof sizes" ); let verify = || { - jolt_verifier::verify::( + jolt_verifier::verify::( &prover_preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-prover/tests/akita_e2e.rs b/crates/jolt-prover/tests/akita_e2e.rs index 7b72494c48..ccf268ad1d 100644 --- a/crates/jolt-prover/tests/akita_e2e.rs +++ b/crates/jolt-prover/tests/akita_e2e.rs @@ -14,30 +14,62 @@ mod support; feature = "akita", not(feature = "field-inline") ))] -#[expect( - clippy::expect_used, - clippy::panic, - reason = "integration tests should fail loudly" -)] +#[expect(clippy::expect_used, reason = "integration tests should fail loudly")] mod akita_tests { use common::constants::DEFAULT_MAX_UNTRUSTED_ADVICE_SIZE; use common::jolt_device::JoltDevice; use jolt_akita::{AkitaCommitment, AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_claims::protocols::jolt::{JoltOneHotConfig, TracePolynomialOrder}; - use jolt_field::Ring; use jolt_program::execution::OwnedTrace; - use jolt_prover::akita::preprocessing::{ - self, AkitaProverPreprocessing, AkitaTranscript, AkitaVc, - }; + use jolt_prover::akita::preprocessing::{self, AkitaProverPreprocessing, AkitaVc}; use jolt_prover::akita::{self, JoltAkitaBackend}; use jolt_prover::{PreprocessingError, ProverConfig, ProverError}; - use jolt_verifier::proof::{ClearProofClaims, JoltProof, JoltProofClaims}; - use jolt_verifier::VerifierError; + use jolt_transcript::VerifierTranscript; + use jolt_verifier::proof::JoltProof; + use jolt_verifier::{ + jolt_protocol_id, seed_transcript, JoltSponge, VerifierError, JOLT_SESSION, + }; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use crate::support::{self, GuestCase, PreparedGuest}; - type Proof = JoltProof; + type Proof = JoltProof; + + /// `proof` with one bit flipped in each sampled region of its argument + /// string: the header, the stage spine, and the trailing Akita opening. + fn bit_flips(proof: &Proof) -> impl Iterator + '_ { + let len = proof.narg.len(); + [0, len / 2, len - 1].into_iter().map(move |position| { + let mut tampered = proof.clone(); + tampered.narg[position] ^= 1; + tampered + }) + } + + /// Whether the proof sends an untrusted-advice commitment, as the + /// verifier's own stage-0 read sees it. + fn sends_untrusted_advice_commitment( + preprocessing: &AkitaProverPreprocessing, + public_io: &JoltDevice, + proof: &Proof, + trusted_advice_commitment: Option<&AkitaCommitment>, + ) -> bool { + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + &proof.narg, + ); + seed_transcript::( + &preprocessing.verifier, + public_io, + trusted_advice_commitment, + &mut transcript, + ) + .expect("proof commitments") + .commitments + .untrusted_advice + .is_some() + } struct ProvedGuest { preprocessing: AkitaProverPreprocessing, @@ -113,7 +145,7 @@ mod akita_tests { witness_config(&config, untrusted_advice, has_trusted_advice), JoltVmWitnessInputs::new(&run.program, &program_preprocessing, run.trace), ); - let proof = akita::prove::( + let proof = akita::prove::( &JoltAkitaBackend::optimized(), &preprocessing, &config, @@ -131,7 +163,7 @@ mod akita_tests { } fn verify(proved: &ProvedGuest) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( &proved.preprocessing.verifier, &proved.public_io, &proved.proof, @@ -166,30 +198,7 @@ mod akita_tests { let proved = prove_guest(run, config, false, &[]); verify(&proved).expect("Akita proof must verify"); - let tamper = |mutate: &dyn Fn(&mut ClearProofClaims)| { - let mut proof = proved.proof.clone(); - let JoltProofClaims::Clear(claims) = &mut proof.claims else { - panic!("Akita proofs carry clear claims"); - }; - mutate(claims); - proof - }; - let one = AkitaField::from_u64(1); - for proof in [ - tamper(&|claims| claims.stage6b.bytecode_read_raf.fused_inc += one), - tamper(&|claims| { - claims - .stage7 - .hamming_weight_claim_reduction - .balanced_inc_digits[0] += one; - }), - tamper(&|claims| { - claims - .stage7 - .hamming_weight_claim_reduction - .balanced_inc_carry += one; - }), - ] { + for proof in bit_flips(&proved.proof) { let tampered = ProvedGuest { preprocessing: proved.preprocessing.clone(), public_io: proved.public_io.clone(), @@ -240,7 +249,7 @@ mod akita_tests { witness_config(&config, false, false), JoltVmWitnessInputs::new(&run.program, &program_preprocessing, run.trace), ); - let result = akita::prove::( + let result = akita::prove::( &JoltAkitaBackend::optimized(), &preprocessing, &config, @@ -270,7 +279,12 @@ mod akita_tests { let run = guest_run("advice-consumer-guest", &inputs, &untrusted, &trusted); let config = derive_config(&run); let proved = prove_guest(run, config, true, &trusted); - assert!(proved.proof.untrusted_advice_commitment.is_some()); + assert!(sends_untrusted_advice_commitment( + &proved.preprocessing, + &proved.public_io, + &proved.proof, + proved.trusted_advice_commitment.as_ref(), + )); verify(&proved).expect("advice proof must verify"); } } @@ -303,7 +317,7 @@ mod akita_tests { witness_config(&config, false, false), JoltVmWitnessInputs::new(&run.program, &program_preprocessing, run.trace), ); - let proof = akita::prove::( + let proof = akita::prove::( &JoltAkitaBackend::optimized(), &preprocessing, &config, @@ -313,7 +327,7 @@ mod akita_tests { ) .expect("committed Akita proof"); let verify = |proof: &Proof| { - jolt_verifier::verify::( + jolt_verifier::verify::( &preprocessing.verifier, &public_io, proof, @@ -321,19 +335,9 @@ mod akita_tests { ) }; verify(&proof).expect("committed Akita proof must verify"); - - // A mutated direct bytecode claim breaks the grouped opening. - let mut tampered = proof; - let JoltProofClaims::Clear(claims) = &mut tampered.claims else { - panic!("Akita proofs carry clear claims"); - }; - claims - .stage7 - .bytecode_address_phase - .as_mut() - .expect("committed proofs carry the bytecode address phase") - .chunks[0] += AkitaField::from_u64(1); - assert!(verify(&tampered).is_err()); + for tampered in bit_flips(&proof) { + assert!(verify(&tampered).is_err()); + } } #[test] @@ -366,7 +370,7 @@ mod akita_tests { witness_config(&config, true, true), JoltVmWitnessInputs::new(&run.program, &program_preprocessing, run.trace), ); - let proof = akita::prove::( + let proof = akita::prove::( &JoltAkitaBackend::optimized(), &preprocessing, &config, @@ -376,8 +380,13 @@ mod akita_tests { ) .expect("committed advice Akita proof"); - assert!(proof.untrusted_advice_commitment.is_some()); - jolt_verifier::verify::( + assert!(sends_untrusted_advice_commitment( + &preprocessing, + &public_io, + &proof, + Some(&trusted_object.commitment), + )); + jolt_verifier::verify::( &preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-prover/tests/akita_field_inline_e2e.rs b/crates/jolt-prover/tests/akita_field_inline_e2e.rs index fd5c79860e..5672f73e6a 100644 --- a/crates/jolt-prover/tests/akita_field_inline_e2e.rs +++ b/crates/jolt-prover/tests/akita_field_inline_e2e.rs @@ -4,8 +4,9 @@ //! values and muldiv with an identically zero `FieldRdInc`, producing identical //! wire objects. The field-increment commitment is present in both cases. //! Guest acceptance across modes lives in `e2e_matrix.rs`. -//! Tamper cases cover the reduced increment claim, commitment layout digest, -//! batched opening proof, missing commitment, and duplicate batch role. +//! Tamper cases cover the stage spine, the field-increment commitment's layout +//! digest, the batched opening, a missing commitment, and a duplicate batch +//! role. #[cfg(all( feature = "prover-fixtures", @@ -34,13 +35,81 @@ mod clear { use jolt_openings::{CommitmentScheme, GroupCommitmentMetadata}; use jolt_prover::akita::JoltAkitaBackend; use jolt_prover::ProverConfig; - use jolt_verifier::proof::JoltProofClaims; + use std::ops::Range; + + use jolt_prover::akita::preprocessing::AkitaVc; + use jolt_transcript::{ProverTranscript, VerifierTranscript}; + use jolt_verifier::proof::ProofHeader; + use jolt_verifier::{jolt_protocol_id, seed_transcript, JoltSponge, JOLT_SESSION}; use jolt_witness::field_inline::FieldInlineWitnessOracle; - use serde_json::Value; - use crate::support::field_inline::akita::Proof; + use crate::support::field_inline::akita::{Proof, ProveOutput}; use crate::support::field_inline::{akita, field_ops, muldiv}; + /// One sent commitment and the argument-string bytes it occupies. + struct SentCommitment { + commitment: AkitaCommitment, + bytes: Range, + } + + /// The commitments a packed field-inline proof sends, in + /// `ProofCommitments::send` order, as the verifier's own stage-0 read sees + /// them. + struct SentCommitments { + one_hot_trace: SentCommitment, + field_inc: SentCommitment, + untrusted_advice: Option, + } + + /// The byte width of `commitment`'s wire form. + fn commitment_width(commitment: &AkitaCommitment) -> usize { + let mut transcript = + ProverTranscript::::new(&jolt_protocol_id::(), b""); + AkitaScheme::send_commitment(commitment, &mut transcript); + transcript.narg().len() + } + + fn sent_commitments(output: &ProveOutput) -> SentCommitments { + let narg = output.proof.narg.as_slice(); + let consumed = + |transcript: &VerifierTranscript<'_, JoltSponge>| narg.len() - transcript.remaining(); + let protocol = jolt_protocol_id::(); + + let mut transcript = VerifierTranscript::::new(&protocol, JOLT_SESSION, narg); + let _header = ProofHeader::receive(&mut transcript).expect("proof header"); + let header_end = consumed(&transcript); + + let mut transcript = VerifierTranscript::::new(&protocol, JOLT_SESSION, narg); + let commitments = seed_transcript::( + &output.verifier_preprocessing, + &output.public_io, + None, + &mut transcript, + ) + .expect("proof commitments") + .commitments; + let mut next = header_end; + let mut place = |commitment: AkitaCommitment| { + let start = next; + next += commitment_width(&commitment); + SentCommitment { + commitment, + bytes: start..next, + } + }; + let sent = SentCommitments { + one_hot_trace: place(commitments.one_hot_trace), + field_inc: place(commitments.field_inc), + untrusted_advice: commitments.untrusted_advice.map(&mut place), + }; + assert_eq!( + next, + consumed(&transcript), + "commitments end the seeding reads" + ); + sent + } + /// A labeled packed kernel-backend constructor. type BackendCase = ( &'static str, @@ -105,13 +174,9 @@ mod clear { for (label, backend) in backends() { let (output, inc) = akita::prove(&case, backend(), collect_inc); assert!( - output.proof.untrusted_advice_commitment.is_some(), + sent_commitments(&output).untrusted_advice.is_some(), "the mixed batch must include bounded advice ({label})", ); - assert!( - output.proof.field_inc_commitment.is_some(), - "packed field-inline proofs must carry the field-increment commitment ({label})", - ); assert!( inc.rd_inc.iter().any(|value| { value.to_canonical_u128() > u128::from(u64::MAX) @@ -146,17 +211,10 @@ mod clear { .rd_inc .iter() .all(|value| *value == AkitaField::from_u64(0))); - assert!( - output.proof.field_inc_commitment.is_some(), - "a zero FieldRdInc still carries its commitment ({label})", - ); - let JoltProofClaims::Clear(claims) = &output.proof.claims else { - panic!("packed proofs carry clear claims"); - }; assert_eq!( - claims.stage6b.field_registers_inc_claim_reduction.rd_inc, - AkitaField::from_u64(0), - "an all-zero polynomial opens to zero ({label})", + sent_commitments(&output).field_inc.commitment, + commit_inc_with_digest(&inc, FieldIncLayout::new(inc.log_t).layout_digest()), + "a zero FieldRdInc still carries its commitment ({label})", ); akita::verify_full( &output.verifier_preprocessing, @@ -185,15 +243,11 @@ mod clear { &output.proof, ) .expect("base proof must verify before tampering"); - let one = AkitaField::from_u64(1); + let sent = sent_commitments(&output); // Bind the layout identity independently of the committed values. let wrong_digest_commitment = { - let honest = output - .proof - .field_inc_commitment - .as_ref() - .expect("packed field-inline proofs carry the field-increment commitment"); + let honest = &sent.field_inc.commitment; let digest = GroupCommitmentMetadata::layout_digest(honest); // The forgery path reproduces the prover's commit exactly under // the honest digest, so the flipped-digest commitment below @@ -207,45 +261,42 @@ mod clear { digest[0] ^= 0x01; commit_inc_with_digest(&inc, digest) }; + let mut wrong_digest_bytes = + ProverTranscript::::new(&jolt_protocol_id::(), b""); + AkitaScheme::send_commitment(&wrong_digest_commitment, &mut wrong_digest_bytes); + let wrong_digest_bytes = wrong_digest_bytes.finish(); + let field_inc_bytes = sent.field_inc.bytes.clone(); type Tamper = (&'static str, Box); let tampers: Vec = vec![ ( - "reduced field-increment claim offset", - Box::new(move |proof| { - let JoltProofClaims::Clear(claims) = &mut proof.claims else { - panic!("clear proof expected"); - }; - claims.stage6b.field_registers_inc_claim_reduction.rd_inc += one; + "stage-spine bit flip", + Box::new(|proof| { + let mid = proof.narg.len() / 2; + proof.narg[mid] ^= 0x01; }), ), ( "field-increment commitment layout-digest byte flip", - Box::new(move |proof| { - proof.field_inc_commitment = Some(wrong_digest_commitment.clone()); + Box::new({ + let range = field_inc_bytes.clone(); + move |proof| { + let _ = proof + .narg + .splice(range.clone(), wrong_digest_bytes.iter().copied()); + } }), ), ( "batched opening proof mutation", Box::new(|proof| { - let mut value = serde_json::to_value(&proof.joint_opening_proof) - .expect("serialize batch proof"); - let bytes = value - .get_mut("backend_proof") - .and_then(Value::as_array_mut) - .expect("batch proof carries the headerless backend proof body"); - let mid = bytes.len() / 2; - let byte = bytes.get_mut(mid).expect("nonempty backend proof"); - let flipped = byte.as_u64().expect("byte value") ^ 0x01; - *byte = Value::from(flipped); - proof.joint_opening_proof = - serde_json::from_value(value).expect("deserialize mutated batch proof"); + *proof.narg.last_mut().expect("nonempty proof") ^= 0x01; }), ), ( "field-increment commitment stripped from the proof", - Box::new(|proof| { - proof.field_inc_commitment = None; + Box::new(move |proof| { + let _ = proof.narg.drain(field_inc_bytes.clone()); }), ), ]; @@ -267,34 +318,30 @@ mod clear { fn akita_field_inline_duplicate_inc_group_is_rejected() { use jolt_claims::protocols::field_inline::lattice::field_inc_group_role; use jolt_openings::{GroupOpeningClaim, TaggedGroupOpeningClaim}; - use jolt_prover::akita::preprocessing::AkitaTranscript; - use jolt_transcript::Transcript; let (output, ()) = akita::prove(&field_ops(), JoltAkitaBackend::optimized(), |_, _, _| ()); - let commitment = output - .proof - .field_inc_commitment - .clone() - .expect("packed field-inline proofs carry the field-increment commitment"); + let sent = sent_commitments(&output); + let commitment = sent.field_inc.commitment; let point = vec![AkitaField::from_u64(3); GroupCommitmentMetadata::num_vars(&commitment)]; let field_claim = TaggedGroupOpeningClaim::new( field_inc_group_role(), GroupOpeningClaim::new(commitment, point, vec![AkitaField::from_u64(0)]), ); + let trace = sent.one_hot_trace.commitment; let main = GroupOpeningClaim::new( - output.proof.commitments.clone(), - vec![ - AkitaField::from_u64(3); - GroupCommitmentMetadata::num_vars(&output.proof.commitments) - ], + trace.clone(), + vec![AkitaField::from_u64(3); GroupCommitmentMetadata::num_vars(&trace)], vec![AkitaField::from_u64(0)], ); - let mut transcript = AkitaTranscript::new(b"spurious-field_inline-group"); + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + b"spurious-field_inline-group", + &output.proof.narg, + ); let error = ::verify_batch( &output.verifier_preprocessing.pcs_setup, &[field_claim.clone(), field_claim], &main, - &output.proof.joint_opening_proof, &mut transcript, ) .expect_err("a duplicated field-inline-role group must be rejected"); diff --git a/crates/jolt-prover/tests/e2e_matrix.rs b/crates/jolt-prover/tests/e2e_matrix.rs index 193c17c7eb..90faed1de3 100644 --- a/crates/jolt-prover/tests/e2e_matrix.rs +++ b/crates/jolt-prover/tests/e2e_matrix.rs @@ -301,9 +301,10 @@ mod matrix { mod mode { use jolt_akita::{AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_program::execution::OwnedTrace; - use jolt_prover::akita::preprocessing::{self, AkitaTranscript, AkitaVc}; + use jolt_prover::akita::preprocessing::{self, AkitaVc}; use jolt_prover::akita::{self, JoltAkitaBackend}; use jolt_prover::ProverConfig; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use crate::support::{self, GuestCase}; @@ -348,7 +349,7 @@ mod matrix { .include_trusted_advice(trusted_advice), JoltVmWitnessInputs::new(&prepared.program, &program_preprocessing, prepared.trace), ); - let proof = akita::prove::( + let proof = akita::prove::( &JoltAkitaBackend::optimized(), &preprocessing, &config, @@ -357,7 +358,7 @@ mod matrix { &public_io, ) .expect("Akita proof"); - jolt_verifier::verify::( + jolt_verifier::verify::( &preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-prover/tests/support/field_inline/akita.rs b/crates/jolt-prover/tests/support/field_inline/akita.rs index 25de6cb951..1dfe4ee4c0 100644 --- a/crates/jolt-prover/tests/support/field_inline/akita.rs +++ b/crates/jolt-prover/tests/support/field_inline/akita.rs @@ -4,17 +4,17 @@ use common::jolt_device::JoltDevice; use jolt_akita::AkitaProverSetup; use jolt_akita::{AkitaField, AkitaScheduleArtifacts, AkitaScheme}; -use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; +use jolt_prover::akita::preprocessing::AkitaVc; use jolt_prover::akita::JoltAkitaBackend; use jolt_prover::{akita, ProverConfig}; use jolt_verifier::proof::JoltProof; -use jolt_verifier::{JoltVerifierPreprocessing, VerifierError}; +use jolt_verifier::{JoltSponge, JoltVerifierPreprocessing, VerifierError}; use jolt_witness::field_inline::FieldInlineWitnessOracle; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, JoltWitnessOracle, TraceBackend}; use crate::support::{GuestCase, PreparedGuest}; -pub type Proof = JoltProof; +pub type Proof = JoltProof; pub struct ProveOutput { pub verifier_preprocessing: JoltVerifierPreprocessing, @@ -78,7 +78,7 @@ pub fn prove( &prover_preprocessing.pcs_setup, ); - let proof = akita::prove::( + let proof = akita::prove::( &backend, &prover_preprocessing, &config, @@ -102,7 +102,7 @@ pub fn verify_full( public_io: &JoltDevice, proof: &Proof, ) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( preprocessing, public_io, proof, From 7ddfb6697db2b809dff4e8a683fa47f197f828bb Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:24:58 -0400 Subject: [PATCH 22/62] refactor(verifier)!: tag protocol regions with transcript sites `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. --- crates/jolt-prover/src/akita/stage0.rs | 5 +- crates/jolt-prover/src/akita/stage8.rs | 2 + crates/jolt-prover/src/blindfold.rs | 2 + crates/jolt-prover/src/dory/stages/stage0.rs | 5 +- crates/jolt-prover/src/dory/stages/stage8.rs | 4 +- crates/jolt-prover/src/stages/stage1.rs | 4 +- crates/jolt-prover/src/stages/stage2.rs | 4 +- crates/jolt-prover/src/stages/stage3.rs | 4 +- crates/jolt-prover/src/stages/stage4.rs | 4 +- crates/jolt-prover/src/stages/stage5.rs | 4 +- crates/jolt-prover/src/stages/stage6a.rs | 4 +- crates/jolt-prover/src/stages/stage6b.rs | 4 +- crates/jolt-prover/src/stages/stage7.rs | 4 +- crates/jolt-verifier-derive/src/lib.rs | 77 +----- crates/jolt-verifier/Cargo.toml | 4 +- crates/jolt-verifier/src/fs_audit.rs | 60 ---- crates/jolt-verifier/src/lib.rs | 4 +- crates/jolt-verifier/src/sites.rs | 20 ++ .../jolt-verifier/src/stages/stage1/verify.rs | 6 +- .../jolt-verifier/src/stages/stage2/verify.rs | 6 +- .../jolt-verifier/src/stages/stage3/verify.rs | 6 +- .../src/stages/stage4/ram_val_check.rs | 55 ---- .../jolt-verifier/src/stages/stage4/verify.rs | 6 +- .../jolt-verifier/src/stages/stage5/verify.rs | 6 +- .../src/stages/stage6a/verify.rs | 6 +- .../src/stages/stage6b/verify.rs | 257 +----------------- .../jolt-verifier/src/stages/stage7/verify.rs | 6 +- .../jolt-verifier/src/stages/stage8/verify.rs | 10 +- crates/jolt-verifier/src/verifier.rs | 6 + 29 files changed, 119 insertions(+), 466 deletions(-) delete mode 100644 crates/jolt-verifier/src/fs_audit.rs create mode 100644 crates/jolt-verifier/src/sites.rs diff --git a/crates/jolt-prover/src/akita/stage0.rs b/crates/jolt-prover/src/akita/stage0.rs index 2ace6b66c7..e01f82e123 100644 --- a/crates/jolt-prover/src/akita/stage0.rs +++ b/crates/jolt-prover/src/akita/stage0.rs @@ -17,7 +17,8 @@ use jolt_field::JoltField; use jolt_openings::{ CommitmentGroupRole, CommitmentScheme, GroupSetupMetadata, TransparentObjectSetup, }; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::{COMMITMENTS, PREAMBLE}; use jolt_verifier::{ absorb_public_commitments, absorb_public_preamble, jolt_protocol_id, validate_inputs, CheckedInputs, ProofCommitments, ProofHeader, VerifierError, JOLT_SESSION, @@ -123,6 +124,7 @@ where )?; let mut transcript = ProverTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION); + transcript.site(PREAMBLE); header.send(&mut transcript); absorb_public_preamble(&checked, &mut transcript); @@ -244,6 +246,7 @@ where Ok::<_, ProverError>((commitment, hint)) })?; + transcript.site(COMMITMENTS); ProofCommitments { one_hot_trace: commitment.clone(), #[cfg(feature = "field-inline")] diff --git a/crates/jolt-prover/src/akita/stage8.rs b/crates/jolt-prover/src/akita/stage8.rs index 817b571bd1..330a8bb2ae 100644 --- a/crates/jolt-prover/src/akita/stage8.rs +++ b/crates/jolt-prover/src/akita/stage8.rs @@ -14,6 +14,7 @@ use jolt_openings::{ CommitmentScheme, EvaluationClaim, GroupOpeningClaim, TaggedGroupOpeningClaim, }; use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE8; use jolt_verifier::stages::stage4::outputs::Stage4ClearOutput; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::outputs::Stage7ClearOutput; @@ -75,6 +76,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE8); let log_t = checked.trace_length.ilog2() as usize; let chunk_width = config.one_hot_config.committed_chunk_bits(); let formula_dimensions = crate::stages::formula_dimensions( diff --git a/crates/jolt-prover/src/blindfold.rs b/crates/jolt-prover/src/blindfold.rs index ff73b93d3f..5df17d5259 100644 --- a/crates/jolt-prover/src/blindfold.rs +++ b/crates/jolt-prover/src/blindfold.rs @@ -25,6 +25,7 @@ use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; use jolt_r1cs::constraints::jolt::SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE; use jolt_sumcheck::{CommittedSumcheckWitness, SumcheckDomainSpec}; use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; +use jolt_verifier::sites::BLINDFOLD; use jolt_verifier::{jolt_protocol_id, verify_stages, VerifiedStages, VerifierError, JOLT_SESSION}; use crate::{JoltProverPreprocessing, ProverError}; @@ -150,6 +151,7 @@ where .ok_or(ProverError::Verifier( VerifierError::MissingVectorCommitmentSetup, ))?; + transcript.site(BLINDFOLD); jolt_blindfold::prove::( vc_setup, &protocol, diff --git a/crates/jolt-prover/src/dory/stages/stage0.rs b/crates/jolt-prover/src/dory/stages/stage0.rs index c4c041d151..c6240698b9 100644 --- a/crates/jolt-prover/src/dory/stages/stage0.rs +++ b/crates/jolt-prover/src/dory/stages/stage0.rs @@ -20,10 +20,11 @@ use jolt_kernels::reference::bytecode_read_raf::BytecodeReadRafWitness; use jolt_kernels::reference::instruction_read_raf::InstructionReadRafWitness; use jolt_kernels::{CommitmentGrid, JoltBackend, ProofSession, WitnessCommitment}; use jolt_openings::CommitmentScheme; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; use jolt_verifier::proof::JoltCommitments; #[cfg(feature = "field-inline")] use jolt_verifier::proof::{FieldInlineCommitments, FieldRegistersCommitments}; +use jolt_verifier::sites::{COMMITMENTS, PREAMBLE}; use jolt_verifier::{ absorb_public_commitments, absorb_public_preamble, jolt_protocol_id, validate_inputs, CheckedInputs, ProofCommitments, ProofHeader, JOLT_SESSION, @@ -160,6 +161,7 @@ where } let mut transcript = ProverTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION); + transcript.site(PREAMBLE); header.send(&mut transcript); absorb_public_preamble(&checked, &mut transcript); @@ -288,6 +290,7 @@ where trace, untrusted_advice: untrusted_advice_commitment, }; + transcript.site(COMMITMENTS); commitments.send::(&mut transcript); absorb_public_commitments( &preprocessing.verifier, diff --git a/crates/jolt-prover/src/dory/stages/stage8.rs b/crates/jolt-prover/src/dory/stages/stage8.rs index 95eba342ee..3a5626adf8 100644 --- a/crates/jolt-prover/src/dory/stages/stage8.rs +++ b/crates/jolt-prover/src/dory/stages/stage8.rs @@ -48,8 +48,9 @@ use jolt_openings::{ #[cfg(feature = "field-inline")] use jolt_poly::MultilinearPoly; use jolt_poly::Point; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; use jolt_verifier::proof::ProofCommitments; +use jolt_verifier::sites::STAGE8; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::outputs::Stage7ClearOutput; use jolt_verifier::stages::stage8::{batch_entries, precommitted_final_openings}; @@ -101,6 +102,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE8); let log_t = checked.trace_length.ilog2() as usize; let precommitted = &checked.precommitted; let formula_dimensions = JoltFormulaDimensions::try_from(config.one_hot_config.dimensions( diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index ff8fc9a960..9391269378 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -17,7 +17,8 @@ use jolt_r1cs::constraints::jolt::{ }; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE1; use jolt_verifier::stages::stage1::outer_remainder::{ outer_remainder_input_values_from_uniskip_output, OuterRemainder, }; @@ -57,6 +58,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE1); let tau = draw_spartan_outer_tau(transcript, log_t); // Backend-neutral kernel-seam spans at the call boundary, so every // `UniskipKernel` implementation inherits them — see the taxonomy's diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index 1b70c11c11..5ec4fbfe75 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -26,7 +26,8 @@ use jolt_openings::CommitmentScheme; use jolt_program::preprocess::PublicIoMemory; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE2; use jolt_verifier::stages::relations::ConcreteSumcheck; use jolt_verifier::stages::stage1::Stage1ClearOutput; #[cfg(feature = "field-inline")] @@ -80,6 +81,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE2); let log_t = config.trace_length.ilog2() as usize; let log_k = config.ram_K.ilog2() as usize; let trace_dimensions = TraceDimensions::new(log_t); diff --git a/crates/jolt-prover/src/stages/stage3.rs b/crates/jolt-prover/src/stages/stage3.rs index ed37decd92..611e9fc4ba 100644 --- a/crates/jolt-prover/src/stages/stage3.rs +++ b/crates/jolt-prover/src/stages/stage3.rs @@ -15,7 +15,8 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE3; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::{ @@ -57,6 +58,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE3); let log_t = config.trace_length.ilog2() as usize; let trace_dimensions = TraceDimensions::new(log_t); let product_tau_low = stage2.product_tau_low.clone(); diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index 1370142fe1..62a686caa6 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -20,9 +20,10 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; #[cfg(feature = "field-inline")] use jolt_verifier::config::JOLT_VERIFIER_CONFIG; +use jolt_verifier::sites::STAGE4; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::Stage3ClearOutput; #[cfg(feature = "field-inline")] @@ -73,6 +74,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE4); let log_t = checked.trace_length.ilog2() as usize; let log_k = checked.ram_K.ilog2() as usize; let trace_dimensions = TraceDimensions::new(log_t); diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index db1def1048..cd3f3e5916 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -20,7 +20,8 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE5; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage4::outputs::Stage4ClearOutput; #[cfg(feature = "field-inline")] @@ -70,6 +71,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE5); let log_k = checked.ram_K.ilog2() as usize; let formula_dimensions = super::formula_dimensions( checked, diff --git a/crates/jolt-prover/src/stages/stage6a.rs b/crates/jolt-prover/src/stages/stage6a.rs index 45fe0eb02f..a028455b66 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -24,7 +24,8 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE6A; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::Stage3ClearOutput; @@ -78,6 +79,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE6A); let formula_dimensions = super::formula_dimensions( checked, config, diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index ad6dd4f07a..4ec0a6ab02 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -29,7 +29,8 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE6B; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::Stage3ClearOutput; @@ -87,6 +88,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE6B); let log_k = checked.ram_K.ilog2() as usize; let precommitted = &checked.precommitted; let formula_dimensions = super::formula_dimensions( diff --git a/crates/jolt-prover/src/stages/stage7.rs b/crates/jolt-prover/src/stages/stage7.rs index dde3395e82..a53b86ce3a 100644 --- a/crates/jolt-prover/src/stages/stage7.rs +++ b/crates/jolt-prover/src/stages/stage7.rs @@ -18,7 +18,8 @@ use jolt_kernels::{JoltBackend, ProofSession}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_transcript::{ProverTranscript, Sponge}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE7; use jolt_verifier::stages::stage4::Stage4ClearOutput; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::hamming_weight_claim_reduction::hamming_weight_claim_reduction_dimensions; @@ -60,6 +61,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE7); let precommitted = &checked.precommitted; let formula_dimensions = super::formula_dimensions( checked, diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index 756106a5d0..6c40a3c026 100644 --- a/crates/jolt-verifier-derive/src/lib.rs +++ b/crates/jolt-verifier-derive/src/lib.rs @@ -136,83 +136,10 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ - parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Fields, GenericArgument, - GenericParam, Ident, ItemFn, Meta, PathArguments, Token, Type, + parse_macro_input, Attribute, Data, DeriveInput, Fields, GenericArgument, GenericParam, Ident, + Meta, PathArguments, Token, Type, }; -/// Assign a verifier function's transcript operations to a Fiat-Shamir audit scope. -/// -/// The annotation has no effect unless `jolt-verifier` is built with `fs-audit`. -/// -/// ```ignore -/// #[fs_scope(Stage3)] -/// pub fn verify(...) -> Result<..., VerifierError> { -/// // ... -/// } -/// ``` -#[proc_macro_attribute] -pub fn fs_scope(attr: TokenStream, item: TokenStream) -> TokenStream { - let scope = parse_macro_input!(attr as Ident); - let mut function = parse_macro_input!(item as ItemFn); - - if let Err(error) = validate_fs_scope(&scope) { - return error.into_compile_error().into(); - } - - let guard = fs_scope_guard_statement(&scope); - function.block.stmts.insert(0, parse_quote!(#guard)); - quote!(#function).into() -} - -/// Assign the remainder of the current block to a Fiat-Shamir audit scope. -/// -/// Use this when one verifier function contains multiple protocol regions: -/// -/// ```ignore -/// jolt_verifier_derive::fs_scope_guard!(BlindFold); -/// ``` -#[proc_macro] -pub fn fs_scope_guard(input: TokenStream) -> TokenStream { - let scope = parse_macro_input!(input as Ident); - if let Err(error) = validate_fs_scope(&scope) { - return error.into_compile_error().into(); - } - fs_scope_guard_statement(&scope).into() -} - -fn validate_fs_scope(scope: &Ident) -> syn::Result<()> { - let valid = matches!( - scope.to_string().as_str(), - "Preamble" - | "Commitments" - | "Stage1" - | "Stage2" - | "Stage3" - | "Stage4" - | "Stage5" - | "Stage6a" - | "Stage6b" - | "Stage7" - | "Reconstruction" - | "Stage8" - | "BlindFold" - ); - if !valid { - return Err(syn::Error::new_spanned( - scope, - "unknown Fiat-Shamir verifier scope", - )); - } - Ok(()) -} - -fn fs_scope_guard_statement(scope: &Ident) -> TokenStream2 { - quote! { - #[cfg(feature = "fs-audit")] - let _fs_scope = crate::fs_audit::enter(crate::fs_audit::FsScope::#scope); - } -} - /// Generate a stage's aggregate claim types (`StageN{Input,Output}{Claims,Points}` /// / `StageNChallenges`) from a struct of `ConcreteSumcheck` instances. See the /// crate-level docs. diff --git a/crates/jolt-verifier/Cargo.toml b/crates/jolt-verifier/Cargo.toml index 0ceff52684..4d835b0632 100644 --- a/crates/jolt-verifier/Cargo.toml +++ b/crates/jolt-verifier/Cargo.toml @@ -17,7 +17,9 @@ default = ["transcript-blake2b"] # harness's per-stage SVGs). allocative = ["dep:allocative", "jolt-claims/allocative"] prover-fixtures = [] -fs-audit = [] +# Records every transcript operation with its protocol site (crate::sites) +# and argument-string range, for the transcript test suites. +logging = ["jolt-transcript/logging"] fuzzing = [] field-inline = [ "common/field-inline", diff --git a/crates/jolt-verifier/src/fs_audit.rs b/crates/jolt-verifier/src/fs_audit.rs deleted file mode 100644 index a978e98f71..0000000000 --- a/crates/jolt-verifier/src/fs_audit.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Test-only Fiat-Shamir verifier scopes. - -use std::cell::Cell; - -/// Verifier region active when a transcript challenge is drawn. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -pub enum FsScope { - /// Input validation and transcript preamble. - #[default] - Preamble, - /// Proof and preprocessing commitments. - Commitments, - /// Stage 1. - Stage1, - /// Stage 2. - Stage2, - /// Stage 3. - Stage3, - /// Stage 4. - Stage4, - /// Stage 5. - Stage5, - /// Stage 6 address phase. - Stage6a, - /// Stage 6 cycle phase. - Stage6b, - /// Stage 7. - Stage7, - /// Final opening checks. - Stage8, - /// BlindFold verification. - BlindFold, -} - -thread_local! { - static CURRENT_SCOPE: Cell = const { Cell::new(FsScope::Preamble) }; -} - -/// Restores the previous verifier scope on drop. -pub struct FsScopeGuard { - previous: FsScope, -} - -impl Drop for FsScopeGuard { - fn drop(&mut self) { - CURRENT_SCOPE.set(self.previous); - } -} - -/// Marks subsequent transcript operations as belonging to `scope`. -#[must_use] -pub fn enter(scope: FsScope) -> FsScopeGuard { - let previous = CURRENT_SCOPE.replace(scope); - FsScopeGuard { previous } -} - -/// Returns the verifier scope active on this thread. -pub fn current() -> FsScope { - CURRENT_SCOPE.get() -} diff --git a/crates/jolt-verifier/src/lib.rs b/crates/jolt-verifier/src/lib.rs index 7e9c232c64..f21cddb468 100644 --- a/crates/jolt-verifier/src/lib.rs +++ b/crates/jolt-verifier/src/lib.rs @@ -47,11 +47,9 @@ pub(crate) mod num { pub mod config; pub mod error; -#[cfg(feature = "fs-audit")] -#[doc(hidden)] -pub mod fs_audit; pub mod preprocessing; pub mod proof; +pub mod sites; pub mod stages; pub mod verifier; diff --git a/crates/jolt-verifier/src/sites.rs b/crates/jolt-verifier/src/sites.rs new file mode 100644 index 0000000000..38d9a52012 --- /dev/null +++ b/crates/jolt-verifier/src/sites.rs @@ -0,0 +1,20 @@ +//! The Jolt protocol's diagnostic transcript sites. +//! +//! Each protocol region sets its site on entry, on both the prover and the +//! verifier, so a `logging` build attributes every transcript event to a +//! region. Sites never reach the sponge. + +use jolt_transcript::SiteId; + +pub const PREAMBLE: SiteId = SiteId::label("jolt/preamble"); +pub const COMMITMENTS: SiteId = SiteId::label("jolt/commitments"); +pub const STAGE1: SiteId = SiteId::label("jolt/stage1"); +pub const STAGE2: SiteId = SiteId::label("jolt/stage2"); +pub const STAGE3: SiteId = SiteId::label("jolt/stage3"); +pub const STAGE4: SiteId = SiteId::label("jolt/stage4"); +pub const STAGE5: SiteId = SiteId::label("jolt/stage5"); +pub const STAGE6A: SiteId = SiteId::label("jolt/stage6a"); +pub const STAGE6B: SiteId = SiteId::label("jolt/stage6b"); +pub const STAGE7: SiteId = SiteId::label("jolt/stage7"); +pub const STAGE8: SiteId = SiteId::label("jolt/stage8"); +pub const BLINDFOLD: SiteId = SiteId::label("jolt/blindfold"); diff --git a/crates/jolt-verifier/src/stages/stage1/verify.rs b/crates/jolt-verifier/src/stages/stage1/verify.rs index 24a87e0c1e..b91df4842e 100644 --- a/crates/jolt-verifier/src/stages/stage1/verify.rs +++ b/crates/jolt-verifier/src/stages/stage1/verify.rs @@ -1,6 +1,8 @@ use jolt_claims::protocols::jolt::geometry::spartan::SpartanOuterDimensions; use jolt_field::{CanonicalDecode, JoltField}; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE1; use super::outer_remainder::{outer_remainder_input_values_from_uniskip_output, OuterRemainder}; use super::outputs::{ @@ -13,7 +15,6 @@ use crate::{ VerifierError, }; -#[jolt_verifier_derive::fs_scope(Stage1)] pub fn verify( checked: &CheckedInputs, transcript: &mut VerifierTranscript<'_, H>, @@ -23,6 +24,7 @@ where C: CanonicalDecode, H: Sponge, { + transcript.site(STAGE1); let uniskip_params = uniskip::UniskipParams::spartan_outer(); let log_t = crate::num::ilog2(checked.trace_length); let dimensions = SpartanOuterDimensions::rv64(log_t); diff --git a/crates/jolt-verifier/src/stages/stage2/verify.rs b/crates/jolt-verifier/src/stages/stage2/verify.rs index 00fa4dd9d1..0ca28d7c6c 100644 --- a/crates/jolt-verifier/src/stages/stage2/verify.rs +++ b/crates/jolt-verifier/src/stages/stage2/verify.rs @@ -10,7 +10,9 @@ use jolt_claims::protocols::jolt::{ use jolt_claims::NoChallenges; use jolt_field::{CanonicalDecode, JoltField}; use jolt_program::preprocess::PublicIoMemory; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE2; #[cfg(feature = "field-inline")] use super::field_registers_claim_reduction::{ @@ -81,7 +83,6 @@ pub fn stage2_batch_input_values_from_upstream( } } -#[jolt_verifier_derive::fs_scope(Stage2)] pub fn verify( checked: &CheckedInputs, transcript: &mut VerifierTranscript<'_, H>, @@ -92,6 +93,7 @@ where C: CanonicalDecode, H: Sponge, { + transcript.site(STAGE2); match (checked.zk, stage1) { (true, Stage1Output::Clear(_)) => { return Err(VerifierError::ExpectedCommittedProof { field: "stage1" }); diff --git a/crates/jolt-verifier/src/stages/stage3/verify.rs b/crates/jolt-verifier/src/stages/stage3/verify.rs index db9ec50f54..e817e90706 100644 --- a/crates/jolt-verifier/src/stages/stage3/verify.rs +++ b/crates/jolt-verifier/src/stages/stage3/verify.rs @@ -2,7 +2,9 @@ use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; use jolt_field::{CanonicalDecode, JoltField}; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE3; use super::{ instruction_input::{instruction_input_input_values_from_upstream, InstructionInput}, @@ -39,7 +41,6 @@ pub fn stage3_input_values_from_upstream( } } -#[jolt_verifier_derive::fs_scope(Stage3)] pub fn verify( checked: &CheckedInputs, transcript: &mut VerifierTranscript<'_, H>, @@ -51,6 +52,7 @@ where C: CanonicalDecode, H: Sponge, { + transcript.site(STAGE3); let log_t = crate::num::ilog2(checked.trace_length); let dimensions = TraceDimensions::new(log_t); diff --git a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs index 0e9929dd96..75246626af 100644 --- a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs +++ b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs @@ -601,58 +601,3 @@ fn ram_val_check_advice_block( opening_point, }) } - -#[cfg(test)] -#[expect( - clippy::unwrap_used, - clippy::indexing_slicing, - reason = "test code indexes its own fixed-size fixtures" -)] -mod tests { - use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; - use jolt_field::Fr; - - // Overrides the default to prepend the `b"ram_val_check_gamma"` domain separator - // (an empty labeled append) before the gamma squeeze; the append is part of the - // soundness-critical byte stream, so it must appear in `draw_challenges` too. - #[test] - fn draw_challenges_appends_domain_separator_then_draws_gamma() { - let relation = RamValCheck::::new( - TraceDimensions::new(4), - 3, - RamValCheckInit::from(Fr::from(0u64)), - ); - - // Inline (stage4/verify.rs L125-126): domain-separator append, then - // `ram_val_check_gamma = challenge_scalar()`. - let (inline_events, inline_gamma) = record(|t| { - append_ram_val_check_gamma_domain_separator(t); - t.challenge_scalar() - }); - let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - // The draw is the domain-separator append(s) followed by exactly one squeeze; - // no challenge is squeezed before the gamma. - assert!(draw_events.len() >= 2); - let (separator, last) = draw_events.split_at(draw_events.len() - 1); - assert_eq!(last, [DrawEvent::Squeeze(1)]); - assert!(separator - .iter() - .all(|event| matches!(event, DrawEvent::Append(_)))); - assert_eq!(challenges.gamma, inline_gamma); - } - - #[test] - fn ram_val_check_gamma_domain_separator_matches_core_empty_bytes_append() { - let (events, ()) = record(append_ram_val_check_gamma_domain_separator); - - let mut packed = vec![0; 32]; - packed[..b"ram_val_check_gamma".len()].copy_from_slice(b"ram_val_check_gamma"); - assert_eq!( - events, - [DrawEvent::Append(packed), DrawEvent::Append(Vec::new())] - ); - } -} diff --git a/crates/jolt-verifier/src/stages/stage4/verify.rs b/crates/jolt-verifier/src/stages/stage4/verify.rs index 4105823481..13661bb3c4 100644 --- a/crates/jolt-verifier/src/stages/stage4/verify.rs +++ b/crates/jolt-verifier/src/stages/stage4/verify.rs @@ -10,7 +10,9 @@ use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; use jolt_poly::sparse_segments_mle_msb; use jolt_program::preprocess::PublicInitialRam; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE4; #[cfg(feature = "field-inline")] use super::field_registers_read_write_checking::{ @@ -82,7 +84,6 @@ pub fn stage4_input_points_from_upstream( } } -#[jolt_verifier_derive::fs_scope(Stage4)] pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, @@ -96,6 +97,7 @@ where VC::Output: CanonicalDecode, H: Sponge, { + transcript.site(STAGE4); let log_t = crate::num::ilog2(checked.trace_length); let log_k = crate::num::ilog2(checked.ram_K); let trace_dimensions = TraceDimensions::new(log_t); diff --git a/crates/jolt-verifier/src/stages/stage5/verify.rs b/crates/jolt-verifier/src/stages/stage5/verify.rs index 3e0b9d80a8..67941c1614 100644 --- a/crates/jolt-verifier/src/stages/stage5/verify.rs +++ b/crates/jolt-verifier/src/stages/stage5/verify.rs @@ -2,7 +2,9 @@ use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; use jolt_claims::protocols::jolt::geometry::dimensions::JoltFormulaDimensions; use jolt_field::{CanonicalDecode, JoltField}; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE5; #[cfg(feature = "field-inline")] use super::field_registers_val_evaluation::{ @@ -75,7 +77,6 @@ pub fn stage5_input_points_from_upstream( } } -#[jolt_verifier_derive::fs_scope(Stage5)] pub fn verify( checked: &CheckedInputs, formula_dimensions: &JoltFormulaDimensions, @@ -88,6 +89,7 @@ where C: CanonicalDecode, H: Sponge, { + transcript.site(STAGE5); let log_k = crate::num::ilog2(checked.ram_K); let trace_dimensions = formula_dimensions.trace; diff --git a/crates/jolt-verifier/src/stages/stage6a/verify.rs b/crates/jolt-verifier/src/stages/stage6a/verify.rs index e30c5b1345..afd1fa40a4 100644 --- a/crates/jolt-verifier/src/stages/stage6a/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6a/verify.rs @@ -5,7 +5,9 @@ use jolt_claims::protocols::jolt::{geometry::dimensions::JoltFormulaDimensions, use jolt_crypto::VectorCommitment; use jolt_field::CanonicalDecode; use jolt_openings::CommitmentScheme; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE6A; #[cfg(feature = "field-inline")] use super::field_inline::field_inline_bytecode_read_raf_address_phase_input_values_from_upstream; @@ -36,7 +38,6 @@ use crate::{ clippy::too_many_arguments, reason = "Stage 6a's address-phase input claim folds all five prior stage outputs directly; bundling them would reintroduce the removed `Deps` indirection." )] -#[jolt_verifier_derive::fs_scope(Stage6a)] pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, @@ -54,6 +55,7 @@ where VC::Output: CanonicalDecode, H: Sponge, { + transcript.site(STAGE6A); // The upstream cycle/register points and entry index ride on the relation // (full geometry at construction) for the prover's address-phase kernel; // the verifier itself never evaluates them here. diff --git a/crates/jolt-verifier/src/stages/stage6b/verify.rs b/crates/jolt-verifier/src/stages/stage6b/verify.rs index c54dfe0f9a..cd69a98e18 100644 --- a/crates/jolt-verifier/src/stages/stage6b/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6b/verify.rs @@ -8,7 +8,9 @@ use jolt_claims::OutputClaims; use jolt_crypto::VectorCommitment; use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE6B; #[cfg(not(feature = "akita"))] use super::committed_reduction_cycle_phase::{ @@ -76,7 +78,6 @@ use crate::{ clippy::too_many_arguments, reason = "Stage 6b consumes the stage-6a output plus all five prior stage outputs directly; bundling them would reintroduce the removed `Deps` indirection." )] -#[jolt_verifier_derive::fs_scope(Stage6b)] pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, @@ -95,6 +96,7 @@ where VC::Output: CanonicalDecode, H: Sponge, { + transcript.site(STAGE6B); // The bytecode fold gamma shares stage 6a's squeeze; it and the booleanity // gamma ride on the stage-6a output as typed upstream values. The post-6a // draws and the challenges aggregate are the promoted two-front helpers. @@ -511,7 +513,6 @@ mod tests { use super::super::ram_hamming_booleanity::RamHammingBooleanityOutputClaims; use super::super::ram_ra_virtualization::RamRaVirtualizationOutputClaims; use super::*; - use crate::stages::relations::append_recording::RecordingTranscript; use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { @@ -595,246 +596,18 @@ mod tests { ) } - const TEST_COMMITTED_CHUNK_BITS: usize = 8; - - fn shape_formula_dimensions() -> JoltFormulaDimensions { - JoltFormulaDimensions::try_from( - jolt_claims::protocols::jolt::geometry::dimensions::JoltOneHotDimensions { - log_t: 8, - instruction_address_bits: 128, - bytecode_k: 1024, - ram_k: 4096, - committed_chunk_bits: TEST_COMMITTED_CHUNK_BITS, - lookup_virtual_chunk_bits: 32, - }, - ) - .unwrap() - } - - /// Claims whose every wire vector length matches `formula_dimensions` exactly. - fn shape_matched_claims(formula_dimensions: &JoltFormulaDimensions) -> Stage6bOutputClaims { - let bytecode_ra_len = - bytecode::read_raf_output_openings(formula_dimensions.bytecode_read_raf) - .bytecode_ra - .len(); - let ra_layout = formula_dimensions.ra_layout; - #[cfg(not(feature = "akita"))] - let bytecode_read_raf = BytecodeReadRafOutputClaims { - bytecode_ra: vec![fr(1); bytecode_ra_len], - }; - #[cfg(feature = "akita")] - let bytecode_read_raf = LatticeBytecodeReadRafOutputClaims { - bytecode_ra: vec![fr(1); bytecode_ra_len], - fused_inc: fr(2), - }; - #[cfg(not(feature = "akita"))] - let booleanity = BooleanityOutputClaims { - instruction_ra: vec![fr(3); ra_layout.instruction()], - bytecode_ra: vec![fr(4); ra_layout.bytecode()], - ram_ra: vec![fr(5); ra_layout.ram()], - }; - #[cfg(feature = "akita")] - let booleanity = - jolt_claims::protocols::jolt::lattice::relations::booleanity::LatticeBooleanityOutputClaims { - instruction_ra: vec![fr(3); ra_layout.instruction()], - bytecode_ra: vec![fr(4); ra_layout.bytecode()], - ram_ra: vec![fr(5); ra_layout.ram()], - balanced_inc_digits: vec![ - fr(6); - jolt_claims::protocols::jolt::lattice::geometry::BalancedIncChunking::new( - TEST_COMMITTED_CHUNK_BITS - ) - .unwrap() - .chunk_count() - ], - balanced_inc_carry: fr(7), - }; - Stage6bOutputClaims { - bytecode_read_raf, - booleanity, - ram_hamming_booleanity: RamHammingBooleanityOutputClaims { - ram_hamming_weight: fr(8), - }, - ram_ra_virtualization: RamRaVirtualizationOutputClaims { - ram_ra: vec![ - fr(9); - formula_dimensions - .ram_ra_virtualization - .num_committed_ra_polys() - ], - }, - instruction_ra_virtualization: InstructionRaVirtualizationOutputClaims { - committed_instruction_ra: vec![ - fr(10); - formula_dimensions - .instruction_ra_virtualization - .num_committed_ra_polys() - ], - }, - #[cfg(not(feature = "akita"))] - inc_claim_reduction: IncClaimReductionOutputClaims { - ram_inc: fr(11), - rd_inc: fr(12), - }, - #[cfg(feature = "field-inline")] - field_registers_inc_claim_reduction: FieldRegistersIncClaimReductionOutputClaims { - rd_inc: fr(13), - }, - #[cfg(not(feature = "akita"))] - trusted_advice: None, - #[cfg(not(feature = "akita"))] - untrusted_advice: None, - bytecode_reduction: None, - program_image_reduction: None, - } - } - - fn validate_shape( - formula_dimensions: &JoltFormulaDimensions, - claims: &Stage6bOutputClaims, - ) -> Result<(), VerifierError> { - #[cfg(not(feature = "akita"))] - let result = validate_cycle_phase_claim_shape(formula_dimensions, claims, None); - #[cfg(feature = "akita")] - let result = validate_cycle_phase_claim_shape( - formula_dimensions, - claims, - None, - TEST_COMMITTED_CHUNK_BITS, - ); - result - } - - fn tamper_vec(vec: &mut Vec, pad: bool) { - if pad { - vec.push(fr(99)); - } else { - let _ = vec.pop(); - } - } - - /// Every stage-6b wire claim vector is exact-length-pinned: padding or - /// truncating any of them must be rejected before the claims reach the - /// Fiat-Shamir absorb (v12 #116402 — trailing entries were absorbed, - /// admitting non-canonical padded proofs). + /// Locks the stage-6b opening order (the prover's send order and the + /// verifier's receive order) against silent drift. The full relations are + /// single-sourced via their `OutputClaims` derive; `booleanity` + /// (conditional `bytecode_ra` dedup) and the optional reductions stay + /// explicit. Points are empty so no `bytecode_ra` element is deduped; the + /// `None` reductions contribute nothing. #[test] - fn cycle_phase_claim_shape_pins_every_wire_vector_length() { - let formula_dimensions = shape_formula_dimensions(); - let claims = shape_matched_claims(&formula_dimensions); - validate_shape(&formula_dimensions, &claims).expect("shape-matched claims validate"); - - type Tamper = fn(&mut Stage6bOutputClaims, bool); - #[cfg_attr(not(feature = "akita"), expect(unused_mut))] - let mut tampers: Vec<(&str, Tamper)> = vec![ - ("bytecode_read_raf.bytecode_ra", |c, pad| { - tamper_vec(&mut c.bytecode_read_raf.bytecode_ra, pad); - }), - ("booleanity.instruction_ra", |c, pad| { - tamper_vec(&mut c.booleanity.instruction_ra, pad); - }), - ("booleanity.bytecode_ra", |c, pad| { - tamper_vec(&mut c.booleanity.bytecode_ra, pad); - }), - ("booleanity.ram_ra", |c, pad| { - tamper_vec(&mut c.booleanity.ram_ra, pad); - }), - ("ram_ra_virtualization.ram_ra", |c, pad| { - tamper_vec(&mut c.ram_ra_virtualization.ram_ra, pad); - }), - ( - "instruction_ra_virtualization.committed_instruction_ra", - |c, pad| { - tamper_vec( - &mut c.instruction_ra_virtualization.committed_instruction_ra, - pad, - ); - }, - ), - ]; - #[cfg(feature = "akita")] - tampers.push(("booleanity.balanced_inc_digits", |c, pad| { - tamper_vec(&mut c.booleanity.balanced_inc_digits, pad); - })); - - for (label, tamper) in tampers { - for pad in [true, false] { - let mut tampered = claims.clone(); - tamper(&mut tampered, pad); - let verb = if pad { "padded" } else { "truncated" }; - assert!( - validate_shape(&formula_dimensions, &tampered).is_err(), - "{verb} {label} must be rejected" - ); - } - } - } - - /// Locks the stage-6b cycle-phase Fiat-Shamir append order against silent drift. - /// The full relations are single-sourced via their `OutputClaims` derive; - /// `booleanity` (conditional `bytecode_ra` dedup) and the optional reductions - /// stay explicit. Points are empty so no `bytecode_ra` element is deduped; - /// the `None` reductions carry absent sentinels to prove they are not appended. - #[test] - fn append_opening_claims_follows_canonical_order() { + fn opening_values_follow_canonical_order() { let (claims, last) = sample_claims(); - - let mut got = RecordingTranscript::default(); - append_opening_claims(&mut got, &claims, &[], &[]); - - let mut want = RecordingTranscript::default(); - for value in (1..=last).map(fr) { - want.append_labeled(b"opening_claim", &value); - } - - assert_eq!(got.chunks, want.chunks); - } - - #[test] - fn bytecode_runtime_alias_requires_equal_claims() { - let (mut claims, _) = sample_claims(); - let alias_point = vec![fr(41), fr(42)]; - let other_point = vec![fr(43), fr(44)]; - let bytecode_points = vec![alias_point.clone(), other_point]; - let source_claim = claims - .bytecode_read_raf - .bytecode_ra - .first() - .copied() - .expect("sample has a bytecode read-RAF claim"); - *claims - .booleanity - .bytecode_ra - .first_mut() - .expect("sample has a bytecode booleanity claim") = fr(1) - source_claim; - - let error = validate_bytecode_ra_aliases(&claims, &bytecode_points, &alias_point) - .expect_err("mismatched evaluations at an aliased point must be rejected"); - let polynomial = JoltCommittedPolynomial::BytecodeRa(0); - assert!(matches!( - error, - VerifierError::StageClaimOpeningMismatch { stage, left, right } - if stage == "Booleanity" - && left - == JoltOpeningId::committed(polynomial, JoltRelationId::Booleanity).into() - && right - == JoltOpeningId::committed(polynomial, JoltRelationId::BytecodeReadRaf) - .into() - )); - - *claims - .booleanity - .bytecode_ra - .first_mut() - .expect("sample has a bytecode booleanity claim") = source_claim; - validate_bytecode_ra_aliases(&claims, &bytecode_points, &alias_point) - .expect("equal evaluations at an aliased point must validate"); - - *claims - .booleanity - .bytecode_ra - .first_mut() - .expect("sample has a bytecode booleanity claim") = fr(99); - validate_bytecode_ra_aliases(&claims, &bytecode_points, &[fr(45), fr(46)]) - .expect("different evaluations at different points are not aliases"); + assert_eq!( + stage6b_opening_values(&claims, &[], &[]), + (1..=last).map(fr).collect::>() + ); } } diff --git a/crates/jolt-verifier/src/stages/stage7/verify.rs b/crates/jolt-verifier/src/stages/stage7/verify.rs index a799bd2245..171465115e 100644 --- a/crates/jolt-verifier/src/stages/stage7/verify.rs +++ b/crates/jolt-verifier/src/stages/stage7/verify.rs @@ -9,7 +9,9 @@ use jolt_claims::protocols::jolt::{ JoltOpeningId, JoltRelationId, PrecommittedReductionLayout, }; use jolt_field::{CanonicalDecode, JoltField}; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE7; #[cfg(not(feature = "akita"))] use super::advice_address_phase::{ @@ -45,7 +47,6 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::advice; #[cfg(not(feature = "akita"))] use jolt_claims::protocols::jolt::JoltAdviceKind; -#[jolt_verifier_derive::fs_scope(Stage7)] pub fn verify( checked: &CheckedInputs, formula_dimensions: &JoltFormulaDimensions, @@ -58,6 +59,7 @@ where C: CanonicalDecode, H: Sponge, { + transcript.site(STAGE7); let hamming_dimensions = hamming_weight_claim_reduction_dimensions( formula_dimensions.ra_layout, checked.one_hot_config.committed_chunk_bits(), diff --git a/crates/jolt-verifier/src/stages/stage8/verify.rs b/crates/jolt-verifier/src/stages/stage8/verify.rs index 7fcc078a9f..50dfafffdd 100644 --- a/crates/jolt-verifier/src/stages/stage8/verify.rs +++ b/crates/jolt-verifier/src/stages/stage8/verify.rs @@ -44,9 +44,9 @@ use jolt_openings::{ }; #[cfg(not(feature = "akita"))] use jolt_poly::Point; -#[cfg(not(feature = "akita"))] -use jolt_transcript::Channel; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE8; #[cfg(not(feature = "akita"))] /// One assembled final-opening batch entry. Public because the prover's @@ -69,7 +69,6 @@ pub struct Stage8BatchEntry<'a, F: JoltField, C> { reason = "Stage 8 takes the shared formula dimensions, trusted-advice commitment, and the two upstream stage outputs it batches; bundling them would add indirection." )] #[cfg(not(feature = "akita"))] -#[jolt_verifier_derive::fs_scope(Stage8)] pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, @@ -89,6 +88,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE8); let log_t = formula_dimensions.trace.log_t(); let layout = formula_dimensions.ra_layout; @@ -645,7 +645,6 @@ mod tests { clippy::too_many_arguments, reason = "same signature as the homomorphic build's verify" )] -#[jolt_verifier_derive::fs_scope(Stage8)] pub fn verify( checked: &CheckedInputs, preprocessing: &JoltVerifierPreprocessing, @@ -665,6 +664,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(STAGE8); super::packed::verify( formula_dimensions, checked.one_hot_config, diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index 36e6716dc1..ec7451b518 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -18,7 +18,10 @@ use jolt_openings::{AdditivelyHomomorphic, ZkOpeningScheme}; use jolt_program::preprocess::{compute_max_ram_k, compute_min_ram_k}; use jolt_transcript::{Channel, ProtocolId, Sponge, VerifierTranscript}; +#[cfg(not(feature = "akita"))] +use crate::sites::BLINDFOLD; use crate::{ + sites::{COMMITMENTS, PREAMBLE}, config::{ validate_proof_config, ZkConfig, JOLT_VERIFIER_CONFIG, JOLT_VERIFIER_INSTRUCTION_PROFILE, }, @@ -73,6 +76,7 @@ where )? { VerifiedStages::Clear => {} VerifiedStages::Zk(blindfold) => { + transcript.site(BLINDFOLD); let vc_setup = preprocessing .vc_setup .as_ref() @@ -312,6 +316,7 @@ where VC: VectorCommitment, H: Sponge, { + transcript.site(PREAMBLE); let header = ProofHeader::receive(transcript)?; let checked = validate_inputs( preprocessing, @@ -326,6 +331,7 @@ where num::ilog2(checked.trace_length), JoltRelationId::InstructionReadRaf, )?; + transcript.site(COMMITMENTS); let commitments = ProofCommitments::receive::( &preprocessing.pcs_setup, &header, From c9c1d1973a256328d3c87c82e70e1c75fafb1020 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:24:58 -0400 Subject: [PATCH 23/62] test(fuzz): fuzz sumcheck and Dory verifiers over argument strings 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. --- crates/jolt-dory/fuzz/Cargo.lock | 4 +- .../fuzz/fuzz_targets/deser_commitment.rs | 47 +++- .../fuzz/fuzz_targets/verify_tampered.rs | 261 +++++++++++------- .../fuzz/fuzz_targets/zk_mode_confusion.rs | 93 ++++--- .../deser_commitment/valid-commitment-atom | Bin 0 -> 384 bytes .../deser_commitment/valid-commitment-bincode | Bin 387 -> 387 bytes .../seeds/deser_commitment/valid-opening-narg | Bin 0 -> 3872 bytes .../deser_commitment/valid-proof-bincode | Bin 3509 -> 0 bytes .../deser_commitment/valid-zk-opening-narg | Bin 0 -> 6240 bytes .../fuzz/seeds/verify_tampered/negate-vmv | Bin 8 -> 6 bytes .../seeds/verify_tampered/strip-final-message | Bin 3 -> 6 bytes .../fuzz/seeds/verify_tampered/swap-rounds | Bin 3 -> 6 bytes .../fuzz/seeds/verify_tampered/zk-flip | Bin 0 -> 6 bytes crates/jolt-dory/fuzz/tests/generate_seeds.rs | 63 +++-- crates/jolt-sumcheck/fuzz/Cargo.lock | 2 + .../fuzz_targets/clear_vs_compressed_diff.rs | 88 +++--- .../fuzz/fuzz_targets/honest_then_corrupt.rs | 161 ++++++----- .../fuzz/fuzz_targets/sumcheck_verifier.rs | 75 ++--- .../fuzz/fuzz_targets/valid_prefix_proof.rs | 142 ++++------ .../{dropped-round => strict-prefix} | Bin .../seeds/sumcheck_verifier/one-round-poly | Bin 99 -> 66 bytes 21 files changed, 491 insertions(+), 445 deletions(-) create mode 100644 crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-atom create mode 100644 crates/jolt-dory/fuzz/seeds/deser_commitment/valid-opening-narg delete mode 100644 crates/jolt-dory/fuzz/seeds/deser_commitment/valid-proof-bincode create mode 100644 crates/jolt-dory/fuzz/seeds/deser_commitment/valid-zk-opening-narg create mode 100644 crates/jolt-dory/fuzz/seeds/verify_tampered/zk-flip rename crates/jolt-sumcheck/fuzz/seeds/honest_then_corrupt/{dropped-round => strict-prefix} (100%) diff --git a/crates/jolt-dory/fuzz/Cargo.lock b/crates/jolt-dory/fuzz/Cargo.lock index 8c0f1456d2..03cc3a87ef 100644 --- a/crates/jolt-dory/fuzz/Cargo.lock +++ b/crates/jolt-dory/fuzz/Cargo.lock @@ -622,7 +622,6 @@ dependencies = [ "ark-std 0.5.0", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint 0.4.8", "num-integer", "num-traits", @@ -717,7 +716,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -738,7 +736,9 @@ dependencies = [ "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] diff --git a/crates/jolt-dory/fuzz/fuzz_targets/deser_commitment.rs b/crates/jolt-dory/fuzz/fuzz_targets/deser_commitment.rs index fcb36d7c1b..775234aad8 100644 --- a/crates/jolt-dory/fuzz/fuzz_targets/deser_commitment.rs +++ b/crates/jolt-dory/fuzz/fuzz_targets/deser_commitment.rs @@ -1,12 +1,51 @@ #![no_main] -use jolt_dory::{DoryCommitment, DoryProof}; + +//! Decoding Dory commitments and openings from attacker bytes must never +//! panic. +//! +//! The input is tried as a serde (JSON, bincode) commitment, as a commitment +//! transcript atom, and as an argument string holding a commitment followed +//! by a transparent or ZK opening at a fixed statement. The last path drives +//! `receive_commitment`, the verifier's proof rebuild from the transcript, +//! and, for well-formed elements, `dory::verify`; every outcome must be `Ok` +//! or a typed error, and `finish` must then terminate without panicking. + +use std::sync::OnceLock; + +use jolt_dory::{DoryCommitment, DoryScheme, DoryVerifierSetup}; +use jolt_field::{CanonicalDecode, Fr, Ring}; +use jolt_openings::{CommitmentScheme, ZkOpeningScheme}; +use jolt_transcript::{Blake2b512, ProtocolId, VerifierTranscript}; use libfuzzer_sys::fuzz_target; +/// Matches `tests/generate_seeds.rs`, so its seed openings parse in full. +const NUM_VARS: usize = 2; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-dory-fuzz/deser"); +const SESSION: &[u8] = b"fuzz-deser"; + +fn verifier_setup() -> &'static DoryVerifierSetup { + static SETUP: OnceLock = OnceLock::new(); + SETUP.get_or_init(|| DoryScheme::setup_verifier(NUM_VARS)) +} + fuzz_target!(|data: &[u8]| { let config = bincode::config::standard(); - let _ = serde_json::from_slice::(data); let _ = bincode::serde::decode_from_slice::(data, config); - let _ = serde_json::from_slice::(data); - let _ = bincode::serde::decode_from_slice::(data, config); + let _ = DoryCommitment::from_bytes_le_checked(data); + + let setup = verifier_setup(); + let point: Vec = (1..=NUM_VARS as u64).map(Fr::from_u64).collect(); + + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, data); + if let Ok(commitment) = DoryScheme::receive_commitment(setup, &mut transcript) { + let _ = DoryScheme::verify(&commitment, &point, Fr::from_u64(0), setup, &mut transcript); + } + let _ = transcript.finish(); + + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, data); + if let Ok(commitment) = DoryScheme::receive_commitment(setup, &mut transcript) { + let _ = DoryScheme::verify_zk(&commitment, &point, setup, &mut transcript); + } + let _ = transcript.finish(); }); diff --git a/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs b/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs index f38851c1ac..25454b583a 100644 --- a/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs +++ b/crates/jolt-dory/fuzz/fuzz_targets/verify_tampered.rs @@ -1,35 +1,79 @@ #![no_main] -//! Structured tampering of an honest Dory opening must be rejected. +//! Tampering with an honest Dory opening's argument string must be rejected. //! -//! The previous harness byte-decoded a full `DoryProof` from raw fuzzer -//! input, so essentially no input survived deserialization and `verify` was -//! never reached — it degenerated into a slower `deser_commitment`. Build one -//! honest proof in a process-wide fixture instead; every iteration reaches -//! the verifier with one structured mutation: a negated protocol element, a -//! dropped or swapped reduce round, or a stripped final message. +//! A process-wide fixture writes one honest transparent and one honest ZK +//! opening; every iteration picks a mode, applies one mutation to that +//! argument string, and runs the verifier plus the transcript's `finish`. +//! Raw byte edits mostly die at element decoding, so two mutations keep the +//! bytes decodable and reach dory-pcs's algebraic and Fiat-Shamir checks: +//! negating a G1/G2/GT element in place (the first offset at or after the +//! chosen one where that element type decodes), and swapping two +//! element-width chunks (reordering messages). The others flip a byte, +//! truncate, append trailing bytes, or delete an element-width range. The +//! verifier absorbs every Dory message and rereads the Σ₁ responses, so any +//! change to the bytes must fail. use std::sync::OnceLock; +use dory::backends::arkworks::{ArkG1, ArkG2, ArkGT}; use dory::primitives::arithmetic::Group; -use jolt_dory::{DoryCommitment, DoryProof, DoryScheme, DoryVerifierSetup}; +use dory::primitives::{DoryDeserialize, DorySerialize}; +use jolt_dory::{DoryCommitment, DoryScheme, DoryVerifierSetup}; use jolt_field::{Field, Fr}; -use jolt_openings::CommitmentScheme; +use jolt_openings::{CommitmentScheme, OpeningsError, ZkOpeningScheme}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript, VerifierTranscript}; use libfuzzer_sys::fuzz_target; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; const NUM_VARS: usize = 4; -const TRANSCRIPT_LABEL: &[u8] = b"fuzz-tampered"; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-dory-fuzz/tampered"); +const SESSION: &[u8] = b"fuzz-tampered"; + +struct Opening { + commitment: DoryCommitment, + narg: Vec, +} struct Fixture { verifier_setup: DoryVerifierSetup, - commitment: DoryCommitment, point: Vec, eval: Fr, - proof: DoryProof, + transparent: Opening, + zk: Opening, +} + +impl Fixture { + fn opening(&self, zk: bool) -> &Opening { + if zk { + &self.zk + } else { + &self.transparent + } + } + + fn verify(&self, zk: bool, narg: &[u8]) -> Result<(), OpeningsError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + if zk { + DoryScheme::verify_zk( + &self.zk.commitment, + &self.point, + &self.verifier_setup, + &mut transcript, + )?; + } else { + DoryScheme::verify( + &self.transparent.commitment, + &self.point, + self.eval, + &self.verifier_setup, + &mut transcript, + )?; + } + Ok(transcript.finish()?) + } } fn fixture() -> &'static Fixture { @@ -37,127 +81,140 @@ fn fixture() -> &'static Fixture { FIX.get_or_init(|| { let mut rng = ChaCha20Rng::seed_from_u64(0xF0_22); let prover_setup = DoryScheme::setup_prover(NUM_VARS); - let verifier_setup = DoryScheme::setup_verifier(NUM_VARS); + let verifier_setup = DoryScheme::verifier_setup(&prover_setup); let poly = Polynomial::::random(NUM_VARS, &mut rng); let point: Vec = (0..NUM_VARS).map(|_| Fr::random(&mut rng)).collect(); let eval = poly.evaluate(&point); + let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).expect("fixture commit"); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + DoryScheme::open( + &poly, + &point, + eval, + &prover_setup, + Some(hint), + &mut transcript, + ) + .expect("fixture open"); + let transparent = Opening { + commitment, + narg: transcript.finish(), + }; - let mut pt = Blake2bTranscript::new(TRANSCRIPT_LABEL); - let proof = DoryScheme::open(&poly, &point, eval, &prover_setup, Some(hint), &mut pt) - .expect("fixture open"); - - let mut vt = Blake2bTranscript::new(TRANSCRIPT_LABEL); - DoryScheme::verify(&commitment, &point, eval, &proof, &verifier_setup, &mut vt) - .expect("fixture proof must verify before tampering"); + let (commitment, hint) = + DoryScheme::commit_zk(poly.evaluations(), &prover_setup).expect("fixture commit_zk"); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + let (_y_com, _blind) = + DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut transcript) + .expect("fixture open_zk"); + let zk = Opening { + commitment, + narg: transcript.finish(), + }; - Fixture { + let fixture = Fixture { verifier_setup, - commitment, point, eval, - proof, + transparent, + zk, + }; + for zk in [false, true] { + fixture + .verify(zk, &fixture.opening(zk).narg) + .expect("fixture opening must verify before tampering"); } + fixture }) } -/// Negates a group element in place. Returns false (skip the iteration) when -/// negation is a no-op because the element is the identity. -fn negate(element: &mut G) -> bool { - let negated = element.neg(); - if negated == *element { - return false; +/// Compressed width of the element kind `kind % 3` selects. +fn element_width(kind: u8) -> usize { + match kind % 3 { + 0 => 32, + 1 => 64, + _ => 384, + } +} + +/// Negates the first element of type `G` that decodes at or after `from`. +/// Returns false when none decodes or the negation is a no-op. +fn negate_first( + narg: &mut [u8], + from: usize, + width: usize, +) -> bool { + for start in from..=narg.len().saturating_sub(width) { + let window = &mut narg[start..start + width]; + let Ok(element) = G::deserialize_compressed(&*window) else { + continue; + }; + let negated = element.neg(); + if negated == element { + return false; + } + let mut bytes = Vec::with_capacity(width); + negated + .serialize_compressed(&mut bytes) + .expect("serializing into a Vec cannot fail"); + window.copy_from_slice(&bytes); + return true; } - *element = negated; - true + false } fuzz_target!(|data: &[u8]| { - if data.len() < 3 { + if data.len() < 6 { return; } let fix = fixture(); - let mut proof = fix.proof.clone(); - let inner = &mut proof.0; - - let changed = match data[0] % 8 { - 0 => match data[1] % 3 { - 0 => negate(&mut inner.vmv_message.c), - 1 => negate(&mut inner.vmv_message.d2), - _ => negate(&mut inner.vmv_message.e1), - }, - 1 => { - if inner.first_messages.is_empty() { + let zk = data[0] & 0x80 != 0; + let class = data[0] % 6; + let honest = &fix.opening(zk).narg; + let offset = usize::from(u16::from_le_bytes([data[1], data[2]])) % honest.len(); + let param = data[3]; + let other = usize::from(u16::from_le_bytes([data[4], data[5]])) % honest.len(); + let payload = &data[6..]; + let width = element_width(param); + + let mut narg = honest.clone(); + match class { + 0 => narg[offset] ^= param, + 1 => narg.truncate(offset), + 2 => narg.extend_from_slice(payload), + 3 => { + let negated = match param % 3 { + 0 => negate_first::(&mut narg, offset, width), + 1 => negate_first::(&mut narg, offset, width), + _ => negate_first::(&mut narg, offset, width), + }; + if !negated { return; } - let round = (data[1] as usize) % inner.first_messages.len(); - let message = &mut inner.first_messages[round]; - match data[2] % 6 { - 0 => negate(&mut message.d1_left), - 1 => negate(&mut message.d1_right), - 2 => negate(&mut message.d2_left), - 3 => negate(&mut message.d2_right), - 4 => negate(&mut message.e1_beta), - _ => negate(&mut message.e2_beta), - } } - 2 => { - if inner.second_messages.is_empty() { + 4 => { + if offset + width > narg.len() || other + width > narg.len() { return; } - let round = (data[1] as usize) % inner.second_messages.len(); - let message = &mut inner.second_messages[round]; - match data[2] % 6 { - 0 => negate(&mut message.c_plus), - 1 => negate(&mut message.c_minus), - 2 => negate(&mut message.e1_plus), - 3 => negate(&mut message.e1_minus), - 4 => negate(&mut message.e2_plus), - _ => negate(&mut message.e2_minus), - } + let first = honest[offset..offset + width].to_vec(); + let second = honest[other..other + width].to_vec(); + narg[other..other + width].copy_from_slice(&first); + narg[offset..offset + width].copy_from_slice(&second); } - 3 => match &mut inner.final_message { - Some(message) => { - if data[1] % 2 == 0 { - negate(&mut message.e1) - } else { - negate(&mut message.e2) - } - } - None => return, - }, - 4 => inner.final_message.take().is_some(), - 5 => inner.first_messages.pop().is_some(), - 6 => inner.second_messages.pop().is_some(), _ => { - // Swap two reduce rounds (Fiat-Shamir round binding). - if inner.first_messages.len() < 2 { - return; - } - if inner.first_messages[0] == inner.first_messages[1] { - return; - } - inner.first_messages.swap(0, 1); - true + let end = (offset + width).min(narg.len()); + narg.drain(offset..end); } - }; - if !changed { + } + if narg == *honest { return; } - let mut transcript = Blake2bTranscript::new(TRANSCRIPT_LABEL); - let result = DoryScheme::verify( - &fix.commitment, - &fix.point, - fix.eval, - &proof, - &fix.verifier_setup, - &mut transcript, - ); assert!( - result.is_err(), - "verifier accepted a tampered proof (mutation class {})", - data[0] % 8, + fix.verify(zk, &narg).is_err(), + "verifier accepted a tampered {} opening (mutation class {class})", + if zk { "ZK" } else { "transparent" }, ); }); diff --git a/crates/jolt-dory/fuzz/fuzz_targets/zk_mode_confusion.rs b/crates/jolt-dory/fuzz/fuzz_targets/zk_mode_confusion.rs index efc5b88c48..992dbe0612 100644 --- a/crates/jolt-dory/fuzz/fuzz_targets/zk_mode_confusion.rs +++ b/crates/jolt-dory/fuzz/fuzz_targets/zk_mode_confusion.rs @@ -3,34 +3,53 @@ //! ZK/transparent mode separation for Dory openings. //! //! For a fuzzer-controlled polynomial and point: both modes must be complete -//! (an honest opening verifies in its own mode), and each mode's verifier -//! must reject the other mode's proof — a ZK proof carries `y_com` and no -//! final scalar-product message, so accepting it transparently (or vice -//! versa) would confuse two different soundness contracts. +//! (an honest opening's argument string verifies in its own mode and is +//! consumed exactly), and each mode's verifier must reject the other mode's +//! argument string under the same protocol id and session. A ZK opening +//! carries `y_com`, Σ-protocol messages, and no final scalar-product message, +//! so accepting it transparently (or vice versa) would confuse two different +//! soundness contracts. use std::sync::OnceLock; -use jolt_dory::{DoryProverSetup, DoryScheme, DoryVerifierSetup}; +use jolt_dory::{DoryCommitment, DoryProverSetup, DoryScheme, DoryVerifierSetup}; use jolt_field::{CanonicalEncoding, Fr}; -use jolt_openings::{CommitmentScheme, ZkOpeningScheme}; +use jolt_openings::{CommitmentScheme, OpeningsError, ZkOpeningScheme}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript, VerifierTranscript}; use libfuzzer_sys::fuzz_target; const SCALAR_BYTES: usize = 32; const NUM_VARS: usize = 4; -const TRANSCRIPT_LABEL: &[u8] = b"fuzz-zk-mode"; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-dory-fuzz/zk-mode"); +const SESSION: &[u8] = b"fuzz-zk-mode"; fn setups() -> &'static (DoryProverSetup, DoryVerifierSetup) { static SETUPS: OnceLock<(DoryProverSetup, DoryVerifierSetup)> = OnceLock::new(); SETUPS.get_or_init(|| { - ( - DoryScheme::setup_prover(NUM_VARS), - DoryScheme::setup_verifier(NUM_VARS), - ) + let prover_setup = DoryScheme::setup_prover(NUM_VARS); + let verifier_setup = DoryScheme::verifier_setup(&prover_setup); + (prover_setup, verifier_setup) }) } +fn verify( + commitment: &DoryCommitment, + point: &[Fr], + eval: Fr, + narg: &[u8], +) -> Result<(), OpeningsError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + DoryScheme::verify(commitment, point, eval, &setups().1, &mut transcript)?; + Ok(transcript.finish()?) +} + +fn verify_zk(commitment: &DoryCommitment, point: &[Fr], narg: &[u8]) -> Result<(), OpeningsError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let _y_com = DoryScheme::verify_zk(commitment, point, &setups().1, &mut transcript)?; + Ok(transcript.finish()?) +} + fuzz_target!(|data: &[u8]| { let n = 1usize << NUM_VARS; if data.len() < (n + NUM_VARS) * SCALAR_BYTES { @@ -45,44 +64,40 @@ fuzz_target!(|data: &[u8]| { let poly = Polynomial::new(evals); let eval = poly.evaluate(&point); - let (prover_setup, verifier_setup) = setups(); + let prover_setup = &setups().0; // ZK completeness. let (zk_commitment, zk_hint) = DoryScheme::commit_zk(poly.evaluations(), prover_setup).expect("commit_zk"); - let mut pt = Blake2bTranscript::new(TRANSCRIPT_LABEL); - let (zk_proof, _y_com, _blind) = - DoryScheme::open_zk(&poly, &point, eval, prover_setup, zk_hint, &mut pt).expect("open_zk"); - let mut vt = Blake2bTranscript::new(TRANSCRIPT_LABEL); - DoryScheme::verify_zk(&zk_commitment, &point, &zk_proof, verifier_setup, &mut vt) - .expect("honest ZK opening must verify in ZK mode"); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + let (_y_com, _blind) = + DoryScheme::open_zk(&poly, &point, eval, prover_setup, zk_hint, &mut transcript) + .expect("open_zk"); + let zk_narg = transcript.finish(); + verify_zk(&zk_commitment, &point, &zk_narg).expect("honest ZK opening must verify in ZK mode"); // Transparent completeness. let (commitment, hint) = DoryScheme::commit(poly.evaluations(), prover_setup).expect("commit"); - let mut pt = Blake2bTranscript::new(TRANSCRIPT_LABEL); - let proof = - DoryScheme::open(&poly, &point, eval, prover_setup, Some(hint), &mut pt).expect("open"); - let mut vt = Blake2bTranscript::new(TRANSCRIPT_LABEL); - DoryScheme::verify(&commitment, &point, eval, &proof, verifier_setup, &mut vt) - .expect("honest transparent opening must verify"); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + DoryScheme::open( + &poly, + &point, + eval, + prover_setup, + Some(hint), + &mut transcript, + ) + .expect("open"); + let narg = transcript.finish(); + verify(&commitment, &point, eval, &narg).expect("honest transparent opening must verify"); // Mode confusion must be rejected in both directions. - let mut vt = Blake2bTranscript::new(TRANSCRIPT_LABEL); assert!( - DoryScheme::verify( - &zk_commitment, - &point, - eval, - &zk_proof, - verifier_setup, - &mut vt, - ) - .is_err(), - "transparent verifier accepted a ZK proof" + verify(&zk_commitment, &point, eval, &zk_narg).is_err(), + "transparent verifier accepted a ZK opening" ); - let mut vt = Blake2bTranscript::new(TRANSCRIPT_LABEL); assert!( - DoryScheme::verify_zk(&commitment, &point, &proof, verifier_setup, &mut vt).is_err(), - "ZK verifier accepted a transparent proof" + verify_zk(&commitment, &point, &narg).is_err(), + "ZK verifier accepted a transparent opening" ); }); diff --git a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-atom b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-atom new file mode 100644 index 0000000000000000000000000000000000000000..b29ec9f23f8daed3c06247402d019af0b47c9695 GIT binary patch literal 384 zcmV-`0e}9&-OVt@8}4k8v#Tjj4j8;$6ENH$fRmlNUWn;1E433BaMb6(y1$S2^E}@6 z^vKF+bK&ujkQNQetgg2;_mm-_;{y#N?a8b|{0Wb)M1YdgkeQH*pwJ3lD~HFZ!g&B% ztS2)9cu3b( z7?7ju@`Iwhmz4rHZg$~`;w^4J{5_rAk6~eMX*BQ3=K=ksa}BSL8*&SKD}Cn8Kra#Q zI$1@Qj4Zx}Aq`i~gZ}h!m315_tpf&JsdayN?4eYjz>^oLT#iL5_yNzfV%6o+|}Pw)di-ucr{HB45|}gK77Kbzc5t!;mT(gM0{at9^kP>O8w$RTUe7(4On* e(1#Y5sRjky?B!xY3jyjYw;zvbfZKi%l359eguj6R literal 0 HcmV?d00001 diff --git a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-bincode b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-bincode index ddf7b592c91c6153136ee40d114e10e46b53f6fa..7172562d5479b00bda414efdbcc53d37df03d07f 100644 GIT binary patch literal 387 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 literal 0 HcmV?d00001 diff --git a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-proof-bincode b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-proof-bincode deleted file mode 100644 index 8413680655c353f5bbfde044c6f344e11ff766bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3509 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 diff --git a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-zk-opening-narg b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-zk-opening-narg new file mode 100644 index 0000000000000000000000000000000000000000..7f19bc149ff0ec47f27bbce97bf7a7f7b86cf0f0 GIT binary patch literal 6240 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"jolt-dory-fuzz/deser"); +const SESSION: &[u8] = b"fuzz-deser"; + #[test] #[ignore = "writes the checked-in seed corpus; run explicitly and commit the outputs"] fn generate_deser_commitment_seeds() { @@ -23,15 +32,28 @@ fn generate_deser_commitment_seeds() { fs::create_dir_all(&seed_dir).expect("seed directory"); let config = bincode::config::standard(); - let num_vars = 2; let mut rng = ChaCha20Rng::seed_from_u64(0x5EED); - let prover_setup = DoryScheme::setup_prover(num_vars); - let poly = Polynomial::::random(num_vars, &mut rng); - let point: Vec = (0..num_vars).map(|_| Fr::random(&mut rng)).collect(); + let prover_setup = DoryScheme::setup_prover(NUM_VARS); + let point: Vec = (1..=NUM_VARS as u64).map(Fr::from_u64).collect(); + // Shift a random polynomial so it evaluates to zero at `point`. + let random = Polynomial::::random(NUM_VARS, &mut rng); + let shift = random.evaluate(&point); + let poly = Polynomial::new(random.evaluations().iter().map(|&e| e - shift).collect()); let eval = poly.evaluate(&point); + assert_eq!(eval, Fr::from_u64(0)); + let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).expect("commit"); - let mut transcript = Blake2bTranscript::new(b"seed-gen"); - let proof = DoryScheme::open( + let commitment_bytes = + bincode::serde::encode_to_vec(&commitment, config).expect("encode commitment"); + fs::write(seed_dir.join("valid-commitment-bincode"), commitment_bytes) + .expect("write commitment seed"); + let mut atom = vec![0u8; ::NUM_BYTES]; + commitment.to_bytes_le(&mut atom); + fs::write(seed_dir.join("valid-commitment-atom"), atom).expect("write atom seed"); + + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + DoryScheme::send_commitment(&commitment, &mut transcript); + DoryScheme::open( &poly, &point, eval, @@ -40,11 +62,16 @@ fn generate_deser_commitment_seeds() { &mut transcript, ) .expect("open"); + fs::write(seed_dir.join("valid-opening-narg"), transcript.finish()) + .expect("write opening seed"); - let commitment_bytes = - bincode::serde::encode_to_vec(&commitment, config).expect("encode commitment"); - fs::write(seed_dir.join("valid-commitment-bincode"), commitment_bytes) - .expect("write commitment seed"); - let proof_bytes = bincode::serde::encode_to_vec(&proof, config).expect("encode proof"); - fs::write(seed_dir.join("valid-proof-bincode"), proof_bytes).expect("write proof seed"); + let (commitment, hint) = + DoryScheme::commit_zk(poly.evaluations(), &prover_setup).expect("commit_zk"); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + DoryScheme::send_commitment(&commitment, &mut transcript); + let (_y_com, _blind) = + DoryScheme::open_zk(&poly, &point, eval, &prover_setup, hint, &mut transcript) + .expect("open_zk"); + fs::write(seed_dir.join("valid-zk-opening-narg"), transcript.finish()) + .expect("write ZK opening seed"); } diff --git a/crates/jolt-sumcheck/fuzz/Cargo.lock b/crates/jolt-sumcheck/fuzz/Cargo.lock index f4e94a30da..46f835da15 100644 --- a/crates/jolt-sumcheck/fuzz/Cargo.lock +++ b/crates/jolt-sumcheck/fuzz/Cargo.lock @@ -582,7 +582,9 @@ dependencies = [ "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/clear_vs_compressed_diff.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/clear_vs_compressed_diff.rs index 84fc432a90..3c676f721d 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/clear_vs_compressed_diff.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/clear_vs_compressed_diff.rs @@ -1,24 +1,27 @@ #![no_main] //! Differential check of `verify_compressed` against an in-harness reference -//! verifier that decompresses each wire round and evaluates it in full. +//! that decompresses each wire round and evaluates it in full. //! //! The compressed wire format omits every round's linear coefficient; the -//! verifier recovers it from the running sum. This harness replays the same -//! wire rounds through (a) `SumcheckVerifier::verify_compressed` and (b) an -//! explicit decompress-then-evaluate loop with identical transcript framing, -//! and requires identical challenges and final claims. A divergence means -//! the c₁-recovery arithmetic disagrees with the polynomial it defines. +//! verifier recovers it from the running sum. The harness writes fuzzer-chosen +//! compressed rounds `[c0, c2, ..., cd]` into a prover transcript, computing +//! the reference along the way (decompress with the running-sum hint, draw the +//! prover-side challenge, evaluate the full polynomial). It then runs +//! `SumcheckVerifier::verify_compressed` over the resulting argument string +//! and requires it to accept, consume every byte, and return the reference's +//! challenges and final claim. A divergence means either the c₁-recovery +//! arithmetic disagrees with the polynomial it defines or the verifier's +//! challenges disagree with the prover's for the same bytes. use jolt_field::{CanonicalEncoding, Fr}; use jolt_poly::CompressedPoly; -use jolt_sumcheck::{ - BooleanHypercube, CompressedSumcheckProof, EvaluationClaim, SumcheckClaim, SumcheckVerifier, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, -}; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, LabelWithCount, Transcript}; +use jolt_sumcheck::{EvaluationClaim, SumcheckClaim, SumcheckVerifier}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; use libfuzzer_sys::fuzz_target; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck-fuzz/diff"); +const SESSION: &[u8] = b"jolt-sumcheck-diff-fuzz"; const SCALAR_BYTES: usize = 32; const MAX_NUM_VARS: usize = 8; const MAX_DEGREE: usize = 4; @@ -32,58 +35,35 @@ fuzz_target!(|data: &[u8]| { let claimed_sum = read_scalar(&data[2..2 + SCALAR_BYTES]); let claim = SumcheckClaim::new(num_vars, degree, claimed_sum); - // Wire rounds: `degree` stored coefficients each (c₁ omitted). - let mut cursor = 2 + SCALAR_BYTES; - let mut wire_rounds: Vec> = Vec::with_capacity(num_vars); - for _ in 0..num_vars { - let needed = degree * SCALAR_BYTES; - if cursor + needed > data.len() { - return; - } - let coeffs: Vec = (0..degree) - .map(|i| read_scalar(&data[cursor + i * SCALAR_BYTES..cursor + (i + 1) * SCALAR_BYTES])) - .collect(); - cursor += needed; - wire_rounds.push(CompressedPoly::new(coeffs)); + let round_bytes = degree * SCALAR_BYTES; + let rounds = data[2 + SCALAR_BYTES..].chunks_exact(round_bytes); + if rounds.len() < num_vars { + return; } - // Production path. - let proof = CompressedSumcheckProof { - round_polynomials: wire_rounds.clone(), - }; - let mut transcript = Blake2bTranscript::new(b"jolt-sumcheck-diff-fuzz"); - let production = SumcheckVerifier::verify_compressed( - &claim, - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); - - // Reference: decompress each round with the running-sum hint, evaluate - // the full polynomial, and mirror the wire transcript framing exactly. - let mut transcript = Blake2bTranscript::new(b"jolt-sumcheck-diff-fuzz"); + let mut prover = ProverTranscript::::new(&PROTOCOL, SESSION); let mut running_sum = claimed_sum; let mut challenges: Vec = Vec::with_capacity(num_vars); - for round in &wire_rounds { - let stored = round.coeffs_except_linear_term(); - transcript.append(&LabelWithCount( - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - stored.len() as u64, - )); - for coefficient in stored { - coefficient.append_to_transcript(&mut transcript); - } - let r: Fr = transcript.challenge(); - let full = round.decompress(running_sum); - running_sum = full.evaluate(r); + for round in rounds.take(num_vars) { + let stored: Vec = round.chunks_exact(SCALAR_BYTES).map(read_scalar).collect(); + prover.send_all(&stored); + let r: Fr = prover.challenge_small(); + running_sum = CompressedPoly::new(stored) + .decompress(running_sum) + .evaluate(r); challenges.push(r); } let reference = EvaluationClaim::new(challenges, running_sum); + let narg = prover.finish(); - let production_claim = production.expect("verify_compressed rejected well-formed wire rounds"); + let mut verifier = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); + let production = SumcheckVerifier::verify_compressed(&claim, &mut verifier) + .expect("verify_compressed rejected well-formed wire rounds"); + verifier + .finish() + .expect("verify_compressed left wire rounds unread"); assert_eq!( - production_claim, reference, + production, reference, "verify_compressed disagrees with decompress-then-evaluate" ); }); diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/honest_then_corrupt.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/honest_then_corrupt.rs index d8b3581286..950f7711dc 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/honest_then_corrupt.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/honest_then_corrupt.rs @@ -3,38 +3,44 @@ //! Honest sumcheck proof over a real MLE product, then one fuzzer-chosen //! corruption; the verifier must reject. //! -//! The harness proves `Σ_x A(x)·B(x)` honestly with an in-harness -//! degree-2 prover (LSB-first binding), then corrupts exactly one thing: the -//! claimed sum, one round coefficient, the round count, or the degree bound. -//! Every corruption breaks a check the verifier performs deterministically — -//! a wrong claimed sum or coefficient breaks that round's `s(0) + s(1)` -//! comparison, and shape corruptions break the count/degree checks. As a -//! belt-and-braces discharge, an accept of a false statement only counts as -//! sound if the returned claim matches the true product evaluation. +//! The harness proves `Σ_x A(x)·B(x)` with an in-harness degree-2 prover +//! (LSB-first binding) into a NARG argument string, checks that the honest +//! string verifies, then corrupts exactly one thing: the claimed sum, one +//! round coefficient (rewritten as another canonical scalar), one byte, the +//! string's length (a strict prefix or a duplicated trailing round), or the +//! statement's degree bound or round count. Every corruption breaks a check +//! the verifier performs deterministically: a changed coefficient moves that +//! round's `s(0) + s(1) = 2·c0 + c1 + c2` (each coefficient enters with a +//! nonzero weight), a flipped byte does the same or breaks the canonical +//! encoding, and every length or shape mismatch misframes the fixed-width +//! rounds, ending in `Truncated` or in `finish`'s `TrailingBytes`. So the +//! harness asserts a plain reject: corrupted proofs never verify. -use jolt_field::{CanonicalEncoding, Field, Fr, Ring}; +use jolt_field::{CanonicalBytes, CanonicalEncoding, Field, Fr, Ring}; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; +use jolt_sumcheck::{ + send_full_round, BooleanHypercube, EvaluationClaim, SumcheckClaim, SumcheckError, + SumcheckVerifier, +}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; use libfuzzer_sys::fuzz_target; use num_traits::Zero; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck-fuzz/corrupt"); +const SESSION: &[u8] = b"jolt-sumcheck-corrupt-fuzz"; const SCALAR_BYTES: usize = 32; const MAX_NUM_VARS: usize = 5; +const DEGREE: usize = 2; +const ROUND_BYTES: usize = (DEGREE + 1) * SCALAR_BYTES; -/// Evaluates the multilinear extension of `evals` at `point`, with -/// `point[k]` bound to index bit `k` (LSB-first, matching the prover below). -fn mle_eval(evals: &[Fr], point: &[Fr]) -> Fr { - let one = Fr::from_u64(1); - let mut sum = Fr::zero(); - for (index, &coeff) in evals.iter().enumerate() { - let mut weight = one; - for (k, &p) in point.iter().enumerate() { - weight *= if (index >> k) & 1 == 1 { p } else { one - p }; - } - sum += coeff * weight; - } - sum +fn verify( + claim: &SumcheckClaim, + narg: &[u8], +) -> Result, SumcheckError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let reduced = SumcheckVerifier::verify(claim, BooleanHypercube, &mut transcript)?; + transcript.finish()?; + Ok(reduced) } fuzz_target!(|data: &[u8]| { @@ -45,7 +51,7 @@ fuzz_target!(|data: &[u8]| { let n = 1usize << num_vars; let corruption = data[1]; let corruption_round = data[2] as usize % num_vars; - let corruption_coeff = data[3] as usize % 3; + let corruption_coeff = data[3] as usize % (DEGREE + 1); // Corruption scalar + the two evaluation tables. if data.len() < 4 + (1 + 2 * n) * SCALAR_BYTES { return; @@ -55,18 +61,18 @@ fuzz_target!(|data: &[u8]| { ::from_bytes_le_reduced(&data[start..start + SCALAR_BYTES]) }; let corruption_scalar = scalar_at(0); - let evals_a: Vec = (0..n).map(|i| scalar_at(1 + i)).collect(); - let evals_b: Vec = (0..n).map(|i| scalar_at(1 + n + i)).collect(); + // The corruption scalar's first two raw bytes also pick a byte within + // the chosen coefficient and an XOR mask. + let corruption_byte = data[4] as usize % SCALAR_BYTES; + let corruption_mask = data[5]; + let mut a: Vec = (0..n).map(|i| scalar_at(1 + i)).collect(); + let mut b: Vec = (0..n).map(|i| scalar_at(1 + n + i)).collect(); - let true_sum: Fr = evals_a.iter().zip(&evals_b).map(|(&a, &b)| a * b).sum(); + let true_sum: Fr = a.iter().zip(&b).map(|(&a, &b)| a * b).sum(); - // Honest degree-2 prover, binding the low variable each round and - // mirroring the verifier's transcript exactly. + // Honest degree-2 prover, binding the low variable each round. let two_inverse = Fr::from_u64(2).inverse().expect("2 is invertible"); - let mut a = evals_a.clone(); - let mut b = evals_b.clone(); - let mut transcript = Blake2bTranscript::new(b"jolt-sumcheck-corrupt-fuzz"); - let mut rounds: Vec> = Vec::with_capacity(num_vars); + let mut prover = ProverTranscript::::new(&PROTOCOL, SESSION); for _ in 0..num_vars { let half = a.len() / 2; let mut s0 = Fr::zero(); @@ -80,83 +86,74 @@ fuzz_target!(|data: &[u8]| { // s(2) with a(2) = 2·a1 − a0 by multilinearity. s2 += (a1 + a1 - a0) * (b1 + b1 - b0); } - let c0 = s0; let c2 = (s2 - s1 - s1 + s0) * two_inverse; let c1 = s1 - s0 - c2; - let poly = UnivariatePoly::new(vec![c0, c1, c2]); + let poly = UnivariatePoly::new(vec![s0, c1, c2]); + send_full_round(&poly, DEGREE, &mut prover).expect("degree-2 round"); - for coefficient in poly.coefficients() { - coefficient.append_to_transcript(&mut transcript); - } - let r: Fr = transcript.challenge(); + let r: Fr = prover.challenge_small(); for j in 0..half { a[j] = a[2 * j] + r * (a[2 * j + 1] - a[2 * j]); b[j] = b[2 * j] + r * (b[2 * j + 1] - b[2 * j]); } a.truncate(half); b.truncate(half); - rounds.push(poly); } + let honest = prover.finish(); + assert_eq!(honest.len(), num_vars * ROUND_BYTES); + let honest_claim = SumcheckClaim::new(num_vars, DEGREE, true_sum); + verify(&honest_claim, &honest).expect("honest proof must verify"); - // Apply exactly one corruption. - let mut claimed_sum = true_sum; - match corruption % 5 { + let coeff_offset = corruption_round * ROUND_BYTES + corruption_coeff * SCALAR_BYTES; + let mut claim = honest_claim.clone(); + let mut narg = honest.clone(); + match corruption % 7 { 0 => { // False statement: honest proof, wrong claimed sum. if corruption_scalar.is_zero() { return; } - claimed_sum += corruption_scalar; + claim.claimed_sum += corruption_scalar; } 1 => { - // One round coefficient replaced; breaks that round's s(0)+s(1). - let coefficients = rounds[corruption_round].coefficients(); - if coefficients[corruption_coeff] == corruption_scalar { + // One round coefficient replaced by another canonical scalar. + corruption_scalar.to_bytes_le(&mut narg[coeff_offset..coeff_offset + SCALAR_BYTES]); + if narg == honest { return; } - let mut replaced = coefficients.to_vec(); - replaced[corruption_coeff] = corruption_scalar; - rounds[corruption_round] = UnivariatePoly::new(replaced); } 2 => { - rounds.pop(); + // Any strict prefix. + narg.truncate(coeff_offset + corruption_byte); } 3 => { - let last = rounds.last().cloned().expect("num_vars >= 1"); - rounds.push(last); + // Duplicated last round. + narg.extend_from_within(narg.len() - ROUND_BYTES..); } - _ => { - // Degree inflation past the claimed bound. - let coefficients = rounds[corruption_round].coefficients(); - let mut inflated = coefficients.to_vec(); - inflated.push(corruption_scalar); - if inflated.len() <= 3 { + 4 => { + if corruption_mask == 0 { return; } - rounds[corruption_round] = UnivariatePoly::new(inflated); + narg[coeff_offset + corruption_byte] ^= corruption_mask; + } + 5 => { + // Statement degree bound disagrees with the proof's round width. + claim.degree = if corruption_mask % 2 == 0 { 1 } else { 3 }; + } + _ => { + // Statement round count disagrees with the proof. + claim.num_vars = if corruption_mask % 2 == 0 { + num_vars - 1 + } else { + num_vars + 1 + }; } } - let claim = SumcheckClaim::new(num_vars, 2, claimed_sum); - let mut verifier_transcript = Blake2bTranscript::new(b"jolt-sumcheck-corrupt-fuzz"); - let result = SumcheckVerifier::verify::, _>( - &claim, - &rounds, - BooleanHypercube, - &mut verifier_transcript, + let result = verify(&claim, &narg); + assert!( + result.is_err(), + "verifier accepted a corrupted proof (class {}): {result:?}", + corruption % 7, ); - - match result { - Err(_) => {} - Ok(final_claim) => { - // Discharge: an accept is only sound if the reduced claim is - // actually true of the underlying product. - let product = - mle_eval(&evals_a, &final_claim.point) * mle_eval(&evals_b, &final_claim.point); - assert_eq!( - final_claim.value, product, - "verifier accepted a corrupted proof reducing to a false claim" - ); - } - } }); diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs index 3943aea87f..ef9e314644 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/sumcheck_verifier.rs @@ -1,24 +1,27 @@ //! Fuzz `SumcheckVerifier::verify_compressed` — the production wire path — -//! with an attacker-controlled claim and compressed round polynomials. The -//! verifier MUST never panic on any input: it must either return -//! `Ok(EvaluationClaim)` or a typed [`SumcheckError`]. +//! with an attacker-controlled claim and an attacker-controlled argument +//! string. The verifier MUST never panic on any input: it must either return +//! `Ok(EvaluationClaim)` or a typed [`SumcheckError`], and the transcript's +//! `finish` must then report trailing bytes or poison without panicking. //! -//! This exercises the c₁-recovery arithmetic (`evaluate_with_hint`) and the -//! `CompressedPolynomialTooShort` and degree-bound rejects that the clear -//! path never reaches. Full-depth accept-path coverage lives in +//! Round width is fixed by the claim's degree bound, so the argument string +//! is read as `num_vars` rounds of `degree` coefficients. Short inputs hit +//! `TranscriptError::Truncated`, out-of-range 32-byte words hit +//! `TranscriptError::NonCanonical`, and canonical words exercise the +//! c₁-recovery arithmetic (`evaluate_with_hint`) that the full-round path +//! never reaches. Full-depth accept-path coverage lives in //! `valid_prefix_proof`. #![no_main] use jolt_field::{CanonicalEncoding, Fr}; -use jolt_poly::CompressedPoly; -use jolt_sumcheck::{ - BooleanHypercube, CompressedSumcheckProof, SumcheckClaim, SumcheckVerifier, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, -}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_sumcheck::{SumcheckClaim, SumcheckVerifier}; +use jolt_transcript::{Blake2b512, ProtocolId, VerifierTranscript}; use libfuzzer_sys::fuzz_target; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck-fuzz/verifier"); +const SESSION: &[u8] = b"jolt-sumcheck-fuzz"; + /// Bytes per BN254 scalar. const SCALAR_BYTES: usize = 32; @@ -32,53 +35,19 @@ const MAX_NUM_VARS: usize = 8; const MAX_DEGREE: usize = 6; fuzz_target!(|data: &[u8]| { - // Header: 1 byte num_vars + 1 byte degree + 32 bytes claimed_sum. + // Header: 1 byte num_vars + 1 byte degree + 32 bytes claimed_sum; the + // rest is the argument string. if data.len() < 2 + SCALAR_BYTES { return; } let num_vars = (data[0] as usize) % (MAX_NUM_VARS + 1); let degree = ((data[1] as usize) % MAX_DEGREE) + 1; // SumcheckClaim::new requires >= 1 - let claimed_sum = read_scalar(&data[2..2 + SCALAR_BYTES]); + let claimed_sum = ::from_bytes_le_reduced(&data[2..2 + SCALAR_BYTES]); let claim = SumcheckClaim::new(num_vars, degree, claimed_sum); + let narg = &data[2 + SCALAR_BYTES..]; - // A compressed round stores `degree` coefficients (the linear term is - // omitted and recovered from the running sum). The fuzzer picks each - // round's stored length (0..=degree+1) so we exercise the too-short and - // degree-bound rejects around the recovery path. - let mut cursor = 2 + SCALAR_BYTES; - let mut round_polynomials: Vec> = Vec::with_capacity(num_vars); - for _ in 0..num_vars { - if cursor >= data.len() { - return; - } - let coeff_count = (data[cursor] as usize) % (degree + 2); // 0..=degree+1 - cursor += 1; - let needed = coeff_count * SCALAR_BYTES; - if cursor + needed > data.len() { - return; - } - let coeffs: Vec = (0..coeff_count) - .map(|i| read_scalar(&data[cursor + i * SCALAR_BYTES..cursor + (i + 1) * SCALAR_BYTES])) - .collect(); - cursor += needed; - round_polynomials.push(CompressedPoly::new(coeffs)); - } - let proof = CompressedSumcheckProof { round_polynomials }; - - // The verifier must terminate without panicking on any input. - let mut transcript = Blake2bTranscript::new(b"jolt-sumcheck-fuzz"); - let _ = SumcheckVerifier::verify_compressed( - &claim, - &proof, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, - &mut transcript, - ); + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + let _ = SumcheckVerifier::verify_compressed(&claim, &mut transcript); + let _ = transcript.finish(); }); - -#[inline] -fn read_scalar(bytes: &[u8]) -> Fr { - debug_assert_eq!(bytes.len(), SCALAR_BYTES); - ::from_bytes_le_reduced(bytes) -} diff --git a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs index c1be71bd5d..37d359d9f8 100644 --- a/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs +++ b/crates/jolt-sumcheck/fuzz/fuzz_targets/valid_prefix_proof.rs @@ -1,33 +1,33 @@ -//! Fuzz `SumcheckVerifier::verify` with proofs whose first `K` round -//! polynomials are constructed to satisfy the sum-check invariant -//! (`s_i(0) + s_i(1) = running_sum_i`). The remaining rounds use raw -//! random bytes from the fuzzer. +//! Fuzz `SumcheckVerifier::verify` with argument strings whose first `K` +//! rounds are written by an honest-shaped prover to satisfy the sum-check +//! invariant (`s_i(0) + s_i(1) = running_sum_i`), followed by raw fuzzer +//! bytes. //! //! Why this exists alongside `sumcheck_verifier`: -//! the panic-guard target tends to fail the verifier's first sum-check -//! comparison (round 0) and return `Err` early, leaving every Fiat-Shamir -//! transcript step after round 0 unexercised. By feeding valid leading -//! rounds, the verifier proceeds round-by-round, exercising -//! `append_to_transcript`, `challenge`, and `evaluate` over the full -//! depth of the protocol. +//! raw bytes almost always fail the first round (non-canonical word or round +//! check) and return early, leaving every Fiat-Shamir step after round 0 +//! unexercised. Valid leading rounds drive the verifier round by round +//! through `receive_full_round`, `challenge_small`, and `evaluate` over the +//! full depth of the protocol, with the raw tail landing at any round. //! //! Per-round bytes pick `c_0` and `c_2 .. c_d` for valid rounds; the -//! linear coefficient `c_1` is derived from the sum-check invariant -//! exactly as the standard compressed-unipoly format does. When -//! `K == num_vars` the entire proof is valid by construction and the -//! verifier MUST accept and return the running sum we computed -//! prover-side. +//! linear coefficient `c_1` is derived from the sum-check invariant. When +//! `K == num_vars` no tail is appended, the argument string is valid by +//! construction, and the verifier MUST accept, consume every byte, and +//! return the running sum computed prover-side. //! //! Addresses review on PR #1493. #![no_main] -use jolt_field::{Fr, CanonicalEncoding}; +use jolt_field::{CanonicalEncoding, Fr}; use jolt_poly::UnivariatePoly; -use jolt_sumcheck::{BooleanHypercube, SumcheckClaim, SumcheckVerifier}; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; +use jolt_sumcheck::{send_full_round, BooleanHypercube, SumcheckClaim, SumcheckVerifier}; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript, VerifierTranscript}; use libfuzzer_sys::fuzz_target; +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck-fuzz/valid-prefix"); +const SESSION: &[u8] = b"jolt-sumcheck-valid-fuzz"; const SCALAR_BYTES: usize = 32; const MAX_NUM_VARS: usize = 8; const MAX_DEGREE: usize = 6; @@ -45,94 +45,54 @@ fuzz_target!(|data: &[u8]| { let claim = SumcheckClaim::new(num_vars, degree, claimed_sum); let mut cursor = 3 + SCALAR_BYTES; - - // Mirror the verifier's transcript steps prover-side over the first - // `valid_rounds` rounds, so the proof is valid by construction up to - // round `valid_rounds - 1` and the verifier's running sum after - // ingesting those rounds matches what we compute below. - let mut prover_transcript = Blake2bTranscript::new(b"jolt-sumcheck-valid-fuzz"); + let mut prover = ProverTranscript::::new(&PROTOCOL, SESSION); let mut running_sum = claimed_sum; - let mut round_proofs: Vec> = Vec::with_capacity(num_vars); - - for round in 0..num_vars { - if round < valid_rounds { - // Need `degree` scalars: c_0 + (degree - 1) high-order coefficients. - let needed = SCALAR_BYTES * degree; - if cursor + needed > data.len() { - return; - } - let c0 = read_scalar(&data[cursor..cursor + SCALAR_BYTES]); - cursor += SCALAR_BYTES; - let mut c_high: Vec = Vec::with_capacity(degree - 1); - for _ in 0..(degree - 1) { - c_high.push(read_scalar(&data[cursor..cursor + SCALAR_BYTES])); - cursor += SCALAR_BYTES; - } + for _ in 0..valid_rounds { + // `degree` scalars: c_0 + (degree - 1) high-order coefficients. + let needed = SCALAR_BYTES * degree; + if cursor + needed > data.len() { + return; + } + let mut scalars = data[cursor..cursor + needed] + .chunks_exact(SCALAR_BYTES) + .map(read_scalar); + cursor += needed; + let c0 = scalars.next().expect("degree >= 1"); + let c_high: Vec = scalars.collect(); - // Derive c_1 from the sum-check invariant: - // s(0) + s(1) = c_0 + (c_0 + c_1 + c_2 + … + c_d) = running_sum - // ⇒ c_1 = running_sum − 2·c_0 − c_2 − … − c_d - let mut c1 = running_sum - c0 - c0; - for c in &c_high { - c1 -= *c; - } + // s(0) + s(1) = 2·c_0 + c_1 + c_2 + … + c_d = running_sum + let c1 = c_high.iter().fold(running_sum - c0 - c0, |acc, &c| acc - c); + let mut coeffs = vec![c0, c1]; + coeffs.extend_from_slice(&c_high); + let poly = UnivariatePoly::new(coeffs); - let mut coeffs = vec![c0, c1]; - coeffs.extend_from_slice(&c_high); - let poly = UnivariatePoly::new(coeffs); + send_full_round(&poly, degree, &mut prover).expect("round fits its degree bound"); + let r: Fr = prover.challenge_small(); + running_sum = poly.evaluate(r); + } - // Mirror the verifier's transcript / challenge / evaluate sequence. - for c in poly.coefficients() { - c.append_to_transcript(&mut prover_transcript); - } - let r: Fr = prover_transcript.challenge(); - running_sum = poly.evaluate(r); - round_proofs.push(poly); - } else { - // Tail rounds: variable-length raw polynomial from fuzzer bytes. - // Most of these will fail the verifier's sum check; the contract - // is that `verify` returns an `Err` (or `Ok` if the random bytes - // happen to land on a valid value) without panicking. - if cursor >= data.len() { - return; - } - let coeff_count = (data[cursor] as usize) % (degree + 2); - cursor += 1; - let needed = coeff_count * SCALAR_BYTES; - if cursor + needed > data.len() { - return; - } - let coeffs: Vec = (0..coeff_count) - .map(|i| { - read_scalar(&data[cursor + i * SCALAR_BYTES..cursor + (i + 1) * SCALAR_BYTES]) - }) - .collect(); - cursor += needed; - round_proofs.push(UnivariatePoly::new(coeffs)); - } + let mut narg = prover.finish(); + let fully_valid = valid_rounds == num_vars; + if !fully_valid { + narg.extend_from_slice(&data[cursor..]); } - let mut verifier_transcript = Blake2bTranscript::new(b"jolt-sumcheck-valid-fuzz"); - let result = SumcheckVerifier::verify::, _>( - &claim, - &round_proofs, - BooleanHypercube, - &mut verifier_transcript, - ); + let mut verifier = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); + let result = SumcheckVerifier::verify(&claim, BooleanHypercube, &mut verifier); + let finished = verifier.finish(); - if valid_rounds == num_vars { - // Proof is valid by construction across every round. The verifier - // MUST accept and return the same running sum we computed. + if fully_valid { let eval_claim = result.expect("fully valid proof must verify"); + finished.expect("fully valid proof must be consumed exactly"); assert_eq!( eval_claim.value, running_sum, "verifier final eval disagrees with prover-side running sum", ); assert_eq!(eval_claim.point.len(), num_vars); } - // Otherwise (`valid_rounds < num_vars`) `verify` may return Ok or Err - // depending on the random tail; the contract is just no panic. + // Otherwise the raw tail may or may not happen to verify; the contract + // is just no panic. }); #[inline] diff --git a/crates/jolt-sumcheck/fuzz/seeds/honest_then_corrupt/dropped-round b/crates/jolt-sumcheck/fuzz/seeds/honest_then_corrupt/strict-prefix similarity index 100% rename from crates/jolt-sumcheck/fuzz/seeds/honest_then_corrupt/dropped-round rename to crates/jolt-sumcheck/fuzz/seeds/honest_then_corrupt/strict-prefix diff --git a/crates/jolt-sumcheck/fuzz/seeds/sumcheck_verifier/one-round-poly b/crates/jolt-sumcheck/fuzz/seeds/sumcheck_verifier/one-round-poly index 0f2821d709ece7db34182824fc86900127d33831..5a1ab926497d301a097043b8fb8d156733cb9d26 100644 GIT binary patch literal 66 RcmZQ%U}eA$nD9$80006600{s9 literal 99 zcmZQ%RCLRpu=N&;rhm!IeUEvK!fO{De Date: Thu, 1 Oct 2026 15:28:40 -0400 Subject: [PATCH 24/62] test(prover): prove and verify end to end on a swapped sponge 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. --- crates/jolt-prover/Cargo.toml | 1 + crates/jolt-prover/tests/sponge_swap.rs | 88 +++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 crates/jolt-prover/tests/sponge_swap.rs diff --git a/crates/jolt-prover/Cargo.toml b/crates/jolt-prover/Cargo.toml index c3089977f2..8517612a39 100644 --- a/crates/jolt-prover/Cargo.toml +++ b/crates/jolt-prover/Cargo.toml @@ -140,6 +140,7 @@ clap.workspace = true jolt-dory.workspace = true jolt-inlines-keccak256 = { workspace = true, features = ["host"] } jolt-inlines-sha2 = { workspace = true, features = ["host"] } +jolt-transcript = { workspace = true, features = ["transcript-keccak"] } sha2.workspace = true sha3.workspace = true # Subscriber stack for tests; the profiling smoke test's summary usage diff --git a/crates/jolt-prover/tests/sponge_swap.rs b/crates/jolt-prover/tests/sponge_swap.rs new file mode 100644 index 0000000000..0154acaba9 --- /dev/null +++ b/crates/jolt-prover/tests/sponge_swap.rs @@ -0,0 +1,88 @@ +//! The proof sponge is a type parameter: the stock prover and verifier run end +//! to end on Keccak, and the sponge's identity separates proofs, so a Keccak +//! proof does not verify under Blake2b. + +#[cfg(all( + feature = "prover-fixtures", + not(feature = "akita"), + not(feature = "field-inline") +))] +mod support; + +#[cfg(all( + feature = "prover-fixtures", + not(feature = "akita"), + not(feature = "field-inline") +))] +#[expect(clippy::expect_used, reason = "end-to-end fixtures fail loudly")] +mod keccak { + use jolt_crypto::{Bn254G1, Pedersen}; + use jolt_dory::DoryScheme; + use jolt_field::Fr; + use jolt_program::execution::OwnedTrace; + use jolt_prover::{dory, JoltBackend, JoltSharedPreprocessing, ProverConfig}; + use jolt_transcript::{Blake2b512, Keccak}; + use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; + + use crate::support::{self, GuestCase}; + + #[test] + fn muldiv_proves_and_verifies_on_keccak_only() { + let case = GuestCase { + inputs: postcard::to_stdvec(&[9u32, 5u32, 3u32]).expect("serialize inputs"), + ..GuestCase::new("muldiv-guest") + }; + let prepared = support::prepare(&case); + let config = ProverConfig::derive_compact::( + prepared.trace.trace.as_slice(), + &prepared.preprocessing.memory_layout, + prepared.preprocessing.ram.min_bytecode_address, + prepared.preprocessing.ram.bytecode_words.len(), + prepared.preprocessing.max_padded_trace_length, + ) + .expect("derive config"); + let preprocessing = dory::from_shared( + JoltSharedPreprocessing::new(prepared.preprocessing).expect("shared preprocessing"), + ) + .expect("Dory preprocessing"); + let program_preprocessing = preprocessing + .program_arc() + .expect("full program preprocessing"); + let public_io = prepared.trace.device.clone(); + let witness = TraceBackend::::from_compact( + JoltVmWitnessConfig::new( + config.trace_length.ilog2() as usize, + config.ram_K, + config.one_hot_config, + ), + JoltVmWitnessInputs::new(&prepared.program, &program_preprocessing, prepared.trace), + ); + + let proof = dory::prove::, Keccak, _>( + &JoltBackend::optimized(), + &preprocessing, + &config, + None, + &witness, + &public_io, + ) + .expect("Keccak proof"); + jolt_verifier::verify::, Keccak>( + &preprocessing.verifier, + &public_io, + &proof, + None, + ) + .expect("a Keccak proof verifies under Keccak"); + assert!( + jolt_verifier::verify::, Blake2b512>( + &preprocessing.verifier, + &public_io, + &proof, + None, + ) + .is_err(), + "a Keccak proof must not verify under Blake2b" + ); + } +} From a20a3a9cff5124b0cf0dc82ea873c4ddf9ce245c Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:38:39 -0400 Subject: [PATCH 25/62] test(verifier): census transcript messages, absorbs, challenges, and 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. --- .github/workflows/fs-soundness.yml | 3 +- .../tests/fs_inventory/absorb-sites.inventory | 230 +----- .../fs_inventory/challenge-sites.inventory | 71 +- .../fs_inventory/delegation-sites.inventory | 232 ++++++ .../fs_inventory/message-sites.inventory | 80 ++ .../tests/fs_inventory/scope-sites.inventory | 15 - .../tests/fs_inventory/site-tags.inventory | 42 + .../fs_inventory/source-schema.inventory | 67 +- crates/jolt-verifier/tests/fs_obligations.rs | 756 ++++++++++++------ scripts/ci/fs-soundness.sh | 3 +- 10 files changed, 954 insertions(+), 545 deletions(-) create mode 100644 crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory create mode 100644 crates/jolt-verifier/tests/fs_inventory/message-sites.inventory delete mode 100644 crates/jolt-verifier/tests/fs_inventory/scope-sites.inventory create mode 100644 crates/jolt-verifier/tests/fs_inventory/site-tags.inventory diff --git a/.github/workflows/fs-soundness.yml b/.github/workflows/fs-soundness.yml index a666963587..b778a51004 100644 --- a/.github/workflows/fs-soundness.yml +++ b/.github/workflows/fs-soundness.yml @@ -56,13 +56,12 @@ jobs: cargo nextest run -p jolt-verifier --test fs_obligations - --features fs-audit --cargo-quiet - name: Probe Akita ZK support shell: bash run: | set -euo pipefail - if cargo check -p jolt-verifier --lib --features akita,fs-audit,zk 2>akita-zk.log; then + if cargo check -p jolt-verifier --lib --features akita,zk 2>akita-zk.log; then echo "Akita+ZK now builds; add it to the Fiat-Shamir fixture matrix." exit 1 fi diff --git a/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory index 08cc3f082f..9ed901e1e7 100644 --- a/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory @@ -1,202 +1,32 @@ # Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ -# --test fs_obligations --features fs-audit`. +# --test fs_obligations`. # Review changes; this inventories identities, not transcript values. -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append#0::transcript.append(&Label(b"akita_commitment")) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append#3::transcript.append(&U64Word(self.num_varsasu64)) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append#4::transcript.append(&U64Word(self.poly_countasu64)) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append#5::transcript.append(&U64Word(self.one_hot_kasu64)) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append#6::transcript.append(&U64Word(self.backend_coeff_lenasu64)) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append#7::transcript.append(&LabelWithCount(b"akita_commitment_bytes",self.serialized_backend_bytes.len()asu64,)) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append_bytes#1::transcript.append_bytes(self.backend_flavor.transcript_label()) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append_bytes#2::transcript.append_bytes(&self.layout_digest) -jolt-akita::adapters.rs::AkitaCommitment::append_to_transcript::append_bytes#8::transcript.append_bytes(&self.serialized_backend_bytes) -jolt-akita::adapters.rs::AkitaHidingCommitment::append_to_transcript::append#0::transcript.append(&Label(b"akita_hiding_commitment")) -jolt-akita::adapters.rs::AkitaHidingCommitment::append_to_transcript::append#1::transcript.append(&LabelWithCount(b"akita_hiding_eval",self.eval.len()asu64,)) -jolt-akita::adapters.rs::AkitaHidingCommitment::append_to_transcript::append_bytes#2::transcript.append_bytes(&self.eval) -jolt-akita::adapters.rs::append_batch_statement::append#0::transcript.append(&Label(b"akita_batch_statement")) -jolt-akita::adapters.rs::append_batch_statement::append#2::transcript.append(&LabelWithCount(b"akita_claims",statement.len()asu64)) -jolt-akita::adapters.rs::append_batch_statement::append_to_transcript#1::commitment.append_to_transcript(transcript) -jolt-akita::adapters.rs::append_batch_statement::append_to_transcript#3::claim.commitment.append_to_transcript(transcript) -jolt-akita::adapters.rs::append_batch_statement::append_to_transcript#4::claim.evaluation.value.append_to_transcript(transcript) -jolt-akita::adapters.rs::append_verifier_setup::append#0::transcript.append(&Label(b"akita_setup_key")) -jolt-akita::adapters.rs::append_verifier_setup::append#3::transcript.append(&U64Word(setup.max_num_varsasu64)) -jolt-akita::adapters.rs::append_verifier_setup::append#4::transcript.append(&U64Word(setup.max_num_polys_per_commitment_groupasu64)) -jolt-akita::adapters.rs::append_verifier_setup::append#5::transcript.append(&U64Word(setup.max_total_batch_polysasu64)) -jolt-akita::adapters.rs::append_verifier_setup::append#6::transcript.append(&U64Word(setup.one_hot_kasu64)) -jolt-akita::adapters.rs::append_verifier_setup::append_bytes#1::transcript.append_bytes(b"akita/fp128") -jolt-akita::adapters.rs::append_verifier_setup::append_bytes#2::transcript.append_bytes(flavor.transcript_label()) -jolt-akita::adapters.rs::append_verifier_setup::append_bytes#7::transcript.append_bytes(&setup.default_layout_digest) -jolt-akita::adapters.rs::append_verifier_setup::append_bytes#8::transcript.append_bytes(&catalog_digest) -jolt-akita::native_batching.rs::AkitaNativeBatching::verify_batch::bind_statement_transcripts#0::bind_statement_transcripts(transcript,setup,statement,commitment,point) -jolt-akita::native_batching.rs::AkitaNativeBatching::verify_trace_batch::bind_grouped_statement_transcripts#0::bind_grouped_statement_transcripts(transcript,setup,selection,auxiliary_groups,main,) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append#10::transcript.append(&LabelWithCount(b"akita_group_evaluations",claim.evaluations.len()asu64,)) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append#1::transcript.append(&Label(b"akita_precommit_batch_v3")) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append#3::transcript.append(&LabelWithCount(b"akita_groups",group_countasu64)) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append#4::transcript.append(&U64Word(indexasu64)) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append#6::transcript.append(&U64Word(role_index)) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append#8::transcript.append(&U64Word(u64::from(role.is_some()))) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append_bytes#2::transcript.append_bytes(&serialize_akita(&selection)?) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append_bytes#5::transcript.append_bytes(role.transcript_label()) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append_bytes#7::transcript.append_bytes(b"main_trace") -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append_to_transcript#11::evaluation.append_to_transcript(transcript) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append_to_transcript#9::claim.commitment.append_to_transcript(transcript) -jolt-akita::native_batching.rs::bind_grouped_statement_transcripts::append_verifier_setup#0::append_verifier_setup(transcript,setup,AkitaBackendFlavor::OneHot) -jolt-akita::native_batching.rs::bind_statement_transcripts::append_batch_statement#1::append_batch_statement(transcript,statement,commitment,point) -jolt-akita::native_batching.rs::bind_statement_transcripts::append_verifier_setup#0::append_verifier_setup(transcript,verifier_setup,commitment.backend_flavor) -jolt-blindfold::verify.rs::BlindFoldProtocol::folded_instance_from_proof::append_to_transcript#0::committed.append_to_transcript(transcript,b"bf_committed_u",b"bf_committed_w",b"bf_committed_e",b"bf_committed_eval",) -jolt-blindfold::verify.rs::BlindFoldProtocol::folded_instance_from_proof::append_to_transcript#1::random.append_to_transcript(transcript,b"bf_random_u",b"bf_random_w",b"bf_random_e",b"bf_random_eval",) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::append_vector_opening#0::append_vector_opening(transcript,b"bf_eval_out_open",b"bf_eval_out_blind",opening,) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::append_vector_opening#1::append_vector_opening(transcript,b"bf_eval_blind_open",b"bf_eval_blind_bl",opening,) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::append_vector_opening#3::append_vector_opening(transcript,b"bf_witness_opening",b"bf_witness_blind",&proof.witness_opening,) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::append#0::transcript.append(&Label(b"bf_spartan")) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::append_vector_opening#2::append_vector_opening(transcript,b"bf_error_opening",b"bf_error_blind",&proof.error_opening,) -jolt-blindfold::verify.rs::RelaxedInstance::append_to_transcript::append#0::transcript.append(&Label(u_label)) -jolt-blindfold::verify.rs::RelaxedInstance::append_to_transcript::append_to_transcript#1::self.u.append_to_transcript(transcript) -jolt-blindfold::verify.rs::append_vector_opening::append#0::transcript.append(&Label(blinding_label)) -jolt-blindfold::verify.rs::append_vector_opening::append_to_transcript#1::opening.combined_blinding.append_to_transcript(transcript) -jolt-crypto::ec/bn254/gt.rs::Bn254GT::append_to_transcript::append_bytes#0::transcript.append_bytes(&buf) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_bytes::append#0::self.transcript.append(&LabelWithCount(b"dory_bytes",bytes.len()asu64)) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_bytes::append_bytes#1::self.transcript.append_bytes(bytes) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_field::append#0::self.transcript.append(&Label(b"dory_field")) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_field::append_to_transcript#1::jolt_scalar.append_to_transcript(self.transcript) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_group::append#0::self.transcript.append(&LabelWithCount(b"dory_group",buffer.len()asu64)) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_group::append_bytes#1::self.transcript.append_bytes(&buffer) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_serde::append#0::self.transcript.append(&LabelWithCount(b"dory_serde",buffer.len()asu64)) -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::append_serde::append_bytes#1::self.transcript.append_bytes(&buffer) -jolt-dory::types.rs::DoryCommitment::append_to_transcript::append_to_transcript#0::self.0.append_to_transcript(transcript) -jolt-openings::claims.rs::VerifierRlcClaims<'_,F,C>::append_to_transcript::append#0::transcript.append(&LabelWithCount(b"rlc_claims",self.0.len()asu64)) -jolt-openings::claims.rs::VerifierRlcClaims<'_,F,C>::append_to_transcript::append_to_transcript#1::claim.evaluation.value.append_to_transcript(transcript) -jolt-openings::claims.rs::ZkEvaluationClaim<'_,F,C>::append_to_transcript::append#0::transcript.append(&LabelWithCount(b"zk_opening_point",self.point.len()asu64,)) -jolt-openings::claims.rs::ZkEvaluationClaim<'_,F,C>::append_to_transcript::append#2::transcript.append(&Label(b"zk_eval_commitment")) -jolt-openings::claims.rs::ZkEvaluationClaim<'_,F,C>::append_to_transcript::append_to_transcript#1::coordinate.append_to_transcript(transcript) -jolt-openings::claims.rs::ZkEvaluationClaim<'_,F,C>::append_to_transcript::append_to_transcript#3::self.hiding_commitment.append_to_transcript(transcript) -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::append#0::transcript.append(&Label(b"prefix_packed_claim")) -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::append#1::transcript.append(&U64Word(self.logical_num_varsasu64)) -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::append#2::transcript.append(&U64Word(self.slot_capacityasu64)) -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::append#3::transcript.append(&Label(b"prefix_pack_layout")) -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::append_bytes#4::transcript.append_bytes(&claims.layout_digest) -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::append_to_transcript#0::statement.append_to_transcript(transcript) -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::append_to_transcript#2::EvaluationClaim::new(statement.point.clone(),joint_eval).append_to_transcript(transcript) -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::append_to_transcript#1::ZkEvaluationClaim::new(point.as_slice(),&hiding_commitment).append_to_transcript(transcript) -jolt-openings::schemes.rs::HomomorphicBatchStatement<'_,F,C>::append_to_transcript::append_to_transcript#0::VerifierRlcClaims(self.claims).append_to_transcript(transcript) -jolt-poly::evaluation_claim.rs::EvaluationClaim::append_to_transcript::append#0::transcript.append(&LabelWithCount(b"opening_point",self.point.len()asu64)) -jolt-poly::evaluation_claim.rs::EvaluationClaim::append_to_transcript::append#2::transcript.append(&Label(b"opening_eval")) -jolt-poly::evaluation_claim.rs::EvaluationClaim::append_to_transcript::append_to_transcript#1::coordinate.append_to_transcript(transcript) -jolt-poly::evaluation_claim.rs::EvaluationClaim::append_to_transcript::append_to_transcript#3::self.value.append_to_transcript(transcript) -jolt-sumcheck::committed.rs::CommittedOutputClaims::append_to_transcript::append#0::transcript.append(&LabelWithCount(OUTPUT_CLAIMS_LABEL,self.commitments.len()asu64,)) -jolt-sumcheck::committed.rs::CommittedOutputClaims::append_to_transcript::append_to_transcript#1::commitment.append_to_transcript(transcript) -jolt-sumcheck::committed.rs::CommittedRound::append_to_transcript::append#0::transcript.append(&LabelWithCount(SUMCHECK_COMMITMENT_LABEL,self.degreeasu64,)) -jolt-sumcheck::committed.rs::CommittedRound::append_to_transcript::append_to_transcript#1::self.commitment.append_to_transcript(transcript) -jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::finish::append_to_transcript#0::output_claims.append_to_transcript(transcript) -jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_input_claims::append_sumcheck_claim#0::append_sumcheck_claim(transcript,input_claim) -jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_round::append_to_transcript#0::CompressedLabeledRoundPoly::sumcheck(round_poly).append_to_transcript(transcript) -jolt-sumcheck::round_proof.rs::CompressedLabeledRoundPoly<'_,F>::append_to_transcript::append#0::transcript.append(&LabelWithCount(self.label,(coeffs.len()-1)asu64)) -jolt-sumcheck::round_proof.rs::CompressedLabeledRoundPoly<'_,F>::append_to_transcript::append_to_transcript#1::constant.append_to_transcript(transcript) -jolt-sumcheck::round_proof.rs::CompressedLabeledRoundPoly<'_,F>::append_to_transcript::append_to_transcript#2::c.append_to_transcript(transcript) -jolt-sumcheck::round_proof.rs::LabeledRoundPoly<'_,F>::append_to_transcript::append#0::transcript.append(&LabelWithCount(self.label,coeffs.len()asu64)) -jolt-sumcheck::round_proof.rs::LabeledRoundPoly<'_,F>::append_to_transcript::append_to_transcript#1::coeff.append_to_transcript(transcript) -jolt-sumcheck::round_proof.rs::UnivariatePoly::append_to_transcript::append_to_transcript#0::coeff.append_to_transcript(transcript) -jolt-sumcheck::verifier.rs::CommittedSumcheckProof::verify_committed_consistency::append_to_transcript#0::self.output_claims.append_to_transcript(transcript) -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::append_to_transcript#0::round_proof.append_to_transcript(transcript) -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_committed_round_consistency::append_to_transcript#0::round_proof.append_to_transcript(transcript) -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::append#0::transcript.append(&LabelWithCount(round_label,coeffs.len()asu64)) -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::append_to_transcript#1::coeff.append_to_transcript(transcript) -jolt-transcript::legacy.rs::F::append_to_transcript::append_bytes#0::transcript.append_bytes(&buf) -jolt-transcript::legacy.rs::Label::append_to_transcript::append_bytes#0::transcript.append_bytes(&padded) -jolt-transcript::legacy.rs::LabelWithCount::append_to_transcript::append_bytes#0::transcript.append_bytes(&packed) -jolt-transcript::legacy.rs::SpongeTranscript::append_bytes::absorb#0::self.sponge.absorb(&buf) -jolt-transcript::legacy.rs::SpongeTranscript::new::absorb_encoded#0::absorb_encoded(&mutsponge,&PROTOCOL_ID) -jolt-transcript::legacy.rs::SpongeTranscript::new::absorb_encoded#1::absorb_encoded(&mutsponge,&BytesMsg(label.to_vec())) -jolt-transcript::legacy.rs::SpongeTranscript::new::absorb_encoded#2::absorb_encoded(&mutsponge,&EmptyInstance) -jolt-transcript::legacy.rs::Transcript::append::append_to_transcript#0::value.append_to_transcript(self) -jolt-transcript::legacy.rs::Transcript::append_labeled::append#0::self.append(&Label(label)) -jolt-transcript::legacy.rs::Transcript::append_labeled::append#1::self.append(value) -jolt-transcript::legacy.rs::Transcript::append_values::append#0::self.append(&LabelWithCount(label,values.len()asu64)) -jolt-transcript::legacy.rs::Transcript::append_values::append#1::self.append(value) -jolt-transcript::legacy.rs::U64Word::append_to_transcript::append_bytes#0::transcript.append_bytes(&packed) -jolt-transcript::legacy.rs::absorb_encoded::absorb#0::sponge.absorb(value.encode().as_ref()) -jolt-transcript::legacy.rs::append_length_prefixed::append#0::transcript.append(&LabelWithCount(label,len)) -jolt-transcript::legacy.rs::append_length_prefixed::append#1::transcript.append(&Label(label)) -jolt-transcript::legacy.rs::append_length_prefixed::append#2::transcript.append(payload) -jolt-transcript::poseidon.rs::PoseidonSponge::absorb::absorb_fr_pair#0::self.absorb_fr_pair(len_fr,Fr::zero()) -jolt-transcript::poseidon.rs::PoseidonSponge::absorb::absorb_fr_pair#1::self.absorb_fr_pair(a,b) -jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheck::draw_challenges::append_ram_val_check_gamma_domain_separator#0::append_ram_val_check_gamma_domain_separator(transcript) -jolt-verifier::stages/stage4/ram_val_check.rs::append_ram_val_check_gamma_domain_separator::append#0::transcript.append(&LabelWithCount(b"ram_val_check_gamma",0)) -jolt-verifier::stages/stage4/ram_val_check.rs::append_ram_val_check_gamma_domain_separator::append_bytes#1::transcript.append_bytes(&[]) -jolt-verifier::stages/stage4/verify.rs::verify::append_to_transcript#0::claims.append_to_transcript(transcript) -jolt-verifier::stages/stage6b/verify.rs::verify::append_opening_claims#0::append_opening_claims(transcript,claims,&cycle_points.bytecode_read_raf.bytecode_ra,&booleanity_opening_point,) -jolt-verifier::stages/stage8/verify.rs::verify::append#2::transcript.append(&LabelWithCount(b"rlc_claims",crate::num::u64_from_usize(opening_claims.len()),)) -jolt-verifier::stages/stage8/verify.rs::verify::append_to_transcript#1::ZkEvaluationClaim::new(pcs_opening_point.as_slice(),&hiding_evaluation_commitment).append_to_transcript(transcript) -jolt-verifier::stages/stage8/verify.rs::verify::append_to_transcript#3::claim.evaluation.value.append_to_transcript(transcript) -jolt-verifier::stages/stage8/verify.rs::verify::append_to_transcript#5::EvaluationClaim::new(pcs_opening_point.clone(),joint_claim).append_to_transcript(transcript) -jolt-verifier::verifier.rs::absorb_commitments::absorb_committed_program_commitments#1::absorb_committed_program_commitments(&committed.bytecode_chunk_commitments,&committed.program_image_commitment,transcript,) -jolt-verifier::verifier.rs::absorb_commitments::absorb_packed_commitments#2::#[cfg(feature="akita")]absorb_packed_commitments(&proof.commitments,proof.untrusted_advice_commitment.as_ref(),trusted_advice_commitment,#[cfg(feature="field-inline")]proof.field_inc_commitment.as_ref(),preprocessing.program.committed().map_or(&[][..],|committed|&committed.direct_program_commitments),transcript,) -jolt-verifier::verifier.rs::absorb_commitments::absorb_transcript_commitments#0::absorb_transcript_commitments(&proof.commitments,proof.untrusted_advice_commitment.as_ref(),trusted_advice_commitment,transcript,) -jolt-verifier::verifier.rs::absorb_committed_program_commitments::append#1::transcript.append(commitment) -jolt-verifier::verifier.rs::absorb_committed_program_commitments::append#3::transcript.append(program_image_commitment) -jolt-verifier::verifier.rs::absorb_committed_program_commitments::append_payload_label#0::append_payload_label(transcript,b"bytecode_chunk_commit",commitment) -jolt-verifier::verifier.rs::absorb_committed_program_commitments::append_payload_label#2::append_payload_label(transcript,b"program_image_commitment",program_image_commitment,) -jolt-verifier::verifier.rs::absorb_labeled_bytes::append#0::transcript.append(&LabelWithCount(label,num::u64_from_usize(bytes.len()))) -jolt-verifier::verifier.rs::absorb_labeled_bytes::append_bytes#1::transcript.append_bytes(bytes) -jolt-verifier::verifier.rs::absorb_labeled_u64::append#0::transcript.append(&Label(label)) -jolt-verifier::verifier.rs::absorb_labeled_u64::append#1::transcript.append(&U64Word(value)) -jolt-verifier::verifier.rs::absorb_packed_commitments::absorb_packed_program_commitments#4::absorb_packed_program_commitments(direct_program_commitments,transcript) -jolt-verifier::verifier.rs::absorb_packed_commitments::append_length_prefixed#0::append_length_prefixed(transcript,b"commitment",one_hot_trace) -jolt-verifier::verifier.rs::absorb_packed_commitments::append_length_prefixed#1::append_length_prefixed(transcript,b"untrusted_advice",commitment) -jolt-verifier::verifier.rs::absorb_packed_commitments::append_length_prefixed#2::append_length_prefixed(transcript,b"trusted_advice",commitment) -jolt-verifier::verifier.rs::absorb_packed_commitments::append_length_prefixed#3::append_length_prefixed(transcript,b"field_inc",commitment) -jolt-verifier::verifier.rs::absorb_packed_program_commitments::append#0::transcript.append(&U64Word(num::u64_from_usize(index))) -jolt-verifier::verifier.rs::absorb_packed_program_commitments::append_length_prefixed#1::append_length_prefixed(transcript,b"bytecode_chunk_commitment",commitment) -jolt-verifier::verifier.rs::absorb_packed_program_commitments::append_length_prefixed#2::append_length_prefixed(transcript,b"program_image_init_commitment",image) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_bytes#0::absorb_labeled_bytes(transcript,b"preprocessing_digest",&checked.preprocessing_digest,) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_bytes#4::absorb_labeled_bytes(transcript,b"inputs",&public_io.inputs) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_bytes#5::absorb_labeled_bytes(transcript,b"outputs",&public_io.outputs) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#10::absorb_labeled_u64(transcript,b"ram_rw_phase1_num_rounds",u64::from(proof.rw_config.ram_rw_phase1_num_rounds),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#11::absorb_labeled_u64(transcript,b"ram_rw_phase2_num_rounds",u64::from(proof.rw_config.ram_rw_phase2_num_rounds),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#12::absorb_labeled_u64(transcript,b"registers_rw_phase1_num_rounds",u64::from(proof.rw_config.registers_rw_phase1_num_rounds),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#13::absorb_labeled_u64(transcript,b"registers_rw_phase2_num_rounds",u64::from(proof.rw_config.registers_rw_phase2_num_rounds),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#14::absorb_labeled_u64(transcript,b"log_k_chunk",u64::from(proof.one_hot_config.log_k_chunk),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#15::absorb_labeled_u64(transcript,b"lookups_ra_virtual_log_k_chunk",u64::from(proof.one_hot_config.lookups_ra_virtual_log_k_chunk),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#16::absorb_labeled_u64(transcript,b"dory_layout",proof.trace_polynomial_order.transcript_scalar(),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#1::absorb_labeled_u64(transcript,b"max_input_size",public_io.memory_layout.max_input_size,) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#2::absorb_labeled_u64(transcript,b"max_output_size",public_io.memory_layout.max_output_size,) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#3::absorb_labeled_u64(transcript,b"heap_size",public_io.memory_layout.heap_size) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#6::absorb_labeled_u64(transcript,b"panic",u64::from(public_io.panic)) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#7::absorb_labeled_u64(transcript,b"ram_K",num::u64_from_usize(checked.ram_K)) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#8::absorb_labeled_u64(transcript,b"trace_length",num::u64_from_usize(checked.trace_length),) -jolt-verifier::verifier.rs::absorb_preamble::absorb_labeled_u64#9::absorb_labeled_u64(transcript,b"entry_address",checked.entry_address) -jolt-verifier::verifier.rs::absorb_transcript_commitments::absorb_commitment#2::absorb_commitment(&commitments.rd_inc) -jolt-verifier::verifier.rs::absorb_transcript_commitments::absorb_commitment#3::absorb_commitment(&commitments.ram_inc) -jolt-verifier::verifier.rs::absorb_transcript_commitments::absorb_commitment#4::absorb_commitment(commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::absorb_commitment#5::absorb_commitment(commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::absorb_commitment#6::absorb_commitment(commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append#10::transcript.append(untrusted_advice_commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append#12::transcript.append(trusted_advice_commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append#1::transcript.append(commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append#8::transcript.append(&field_inline.field_registers.rd_inc) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append_payload_label#0::append_payload_label(transcript,b"commitment",commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append_payload_label#11::append_payload_label(transcript,b"trusted_advice",trusted_advice_commitment) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append_payload_label#7::append_payload_label(transcript,b"field_rd_inc_commitment",&field_inline.field_registers.rd_inc,) -jolt-verifier::verifier.rs::absorb_transcript_commitments::append_payload_label#9::append_payload_label(transcript,b"untrusted_advice",untrusted_advice_commitment) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_bytes#0::absorb_labeled_bytes(transcript,b"preprocessing_digest",&checked.preprocessing_digest,) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_bytes#4::absorb_labeled_bytes(transcript,b"inputs",&public_io.inputs) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_bytes#5::absorb_labeled_bytes(transcript,b"outputs",&public_io.outputs) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#10::absorb_labeled_u64(transcript,b"ram_rw_phase1_num_rounds",u64::from(config.rw_config.ram_rw_phase1_num_rounds),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#11::absorb_labeled_u64(transcript,b"ram_rw_phase2_num_rounds",u64::from(config.rw_config.ram_rw_phase2_num_rounds),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#12::absorb_labeled_u64(transcript,b"registers_rw_phase1_num_rounds",u64::from(config.rw_config.registers_rw_phase1_num_rounds),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#13::absorb_labeled_u64(transcript,b"registers_rw_phase2_num_rounds",u64::from(config.rw_config.registers_rw_phase2_num_rounds),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#14::absorb_labeled_u64(transcript,b"log_k_chunk",u64::from(config.one_hot_config.log_k_chunk),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#15::absorb_labeled_u64(transcript,b"lookups_ra_virtual_log_k_chunk",u64::from(config.one_hot_config.lookups_ra_virtual_log_k_chunk),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#16::absorb_labeled_u64(transcript,b"dory_layout",config.trace_polynomial_order.transcript_scalar(),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#1::absorb_labeled_u64(transcript,b"max_input_size",public_io.memory_layout.max_input_size,) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#2::absorb_labeled_u64(transcript,b"max_output_size",public_io.memory_layout.max_output_size,) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#3::absorb_labeled_u64(transcript,b"heap_size",public_io.memory_layout.heap_size) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#6::absorb_labeled_u64(transcript,b"panic",u64::from(public_io.panic)) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#7::absorb_labeled_u64(transcript,b"ram_K",num::u64_from_usize(checked.ram_K)) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#8::absorb_labeled_u64(transcript,b"trace_length",num::u64_from_usize(checked.trace_length),) -jolt-verifier::verifier.rs::absorb_transcript_preamble::absorb_labeled_u64#9::absorb_labeled_u64(transcript,b"entry_address",checked.entry_address) -jolt-verifier::verifier.rs::append_payload_label::append#0::transcript.append(&LabelWithCount(label,len)) -jolt-verifier::verifier.rs::append_payload_label::append#1::transcript.append(&Label(label)) -jolt-verifier::verifier.rs::validate_and_seed_transcript::absorb_commitments#1::absorb_commitments(preprocessing,proof,trusted_advice_commitment,&muttranscript,) -jolt-verifier::verifier.rs::validate_and_seed_transcript::absorb_preamble#0::absorb_preamble(&checked,proof,&muttranscript) -jolt-verifier::verifier.rs::verify::append#0::transcript.append(&Label(b"BlindFold")) +jolt-akita::adapters.rs::AkitaCommitment::absorb::public#0::channel.public(&self.header()) +jolt-akita::adapters.rs::AkitaCommitment::absorb::public_bytes#1::channel.public_bytes(&self.serialized_backend_bytes) +jolt-akita::adapters.rs::absorb_setup_catalog::public#0::channel.public(&catalog_digest) +jolt-dory::scheme.rs::DoryScheme::absorb_commitment::public#0::channel.public(commitment) +jolt-openings::claims.rs::ZkEvaluationClaim<'_,F,C>::absorb::public#1::channel.public(self.hiding_commitment) +jolt-openings::claims.rs::ZkEvaluationClaim<'_,F,C>::absorb::public_all#0::channel.public_all(self.point) +jolt-openings::claims.rs::absorb_evaluation::public#1::channel.public(value) +jolt-openings::claims.rs::absorb_evaluation::public_all#0::channel.public_all(point) +jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::public#0::transcript.public(&(self.logical_num_varsasu64)) +jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::public#1::transcript.public(&(self.slot_capacityasu64)) +jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::public#2::transcript.public(&claims.layout_digest) +jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::public_all#3::transcript.public_all(claims.point.as_slice()) +jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::public_all#4::transcript.public_all(&claims.evaluations) +jolt-openings::schemes.rs::HomomorphicBatchStatement<'_,F,C>::absorb_values::public_all#0::channel.public_all(&values) +jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_input_claims::public_all#0::channel.public_all(input_claims) +jolt-verifier::stages/stage8/verify.rs::verify::public#7::transcript.public(&joint_claim) +jolt-verifier::stages/stage8/verify.rs::verify::public_all#3::transcript.public_all(&scaled_values) +jolt-verifier::stages/stage8/verify.rs::verify::public_all#6::transcript.public_all(pcs_opening_point.as_slice()) +jolt-verifier::verifier.rs::absorb_public_commitments::absorb_commitment#0::PCS::absorb_commitment(commitment,channel) +jolt-verifier::verifier.rs::absorb_public_commitments::absorb_commitment#1::PCS::absorb_commitment(commitment,channel) +jolt-verifier::verifier.rs::absorb_public_commitments::absorb_commitment#2::PCS::absorb_commitment(&committed.program_image_commitment,channel) +jolt-verifier::verifier.rs::absorb_public_commitments::absorb_commitment#3::PCS::absorb_commitment(commitment,channel) +jolt-verifier::verifier.rs::absorb_public_preamble::public#0::channel.public(&JOLT_VERIFIER_CONFIG.transcript_bytes()) +jolt-verifier::verifier.rs::absorb_public_preamble::public#1::channel.public(&checked.preprocessing_digest) +jolt-verifier::verifier.rs::absorb_public_preamble::public#5::channel.public(&u8::from(public_io.panic)) +jolt-verifier::verifier.rs::absorb_public_preamble::public#6::channel.public(&checked.entry_address) +jolt-verifier::verifier.rs::absorb_public_preamble::public_all#2::channel.public_all(&[public_io.memory_layout.max_input_size,public_io.memory_layout.max_output_size,public_io.memory_layout.heap_size,]) +jolt-verifier::verifier.rs::absorb_public_preamble::public_bytes#3::channel.public_bytes(&public_io.inputs) +jolt-verifier::verifier.rs::absorb_public_preamble::public_bytes#4::channel.public_bytes(&public_io.outputs) diff --git a/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory index 131c5f25cb..89e06530bf 100644 --- a/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory @@ -1,43 +1,48 @@ # Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ -# --test fs_obligations --features fs-audit`. +# --test fs_obligations`. # Review changes; this inventories identities, not transcript values. -jolt-akita::adapters.rs::bridged_akita_session::challenge_scalar#0 -jolt-blindfold::verify.rs::BlindFoldProtocol::folded_instance_from_proof::challenge#2 -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge#0 -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge#1 -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge#2 -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::challenge_vector#1 -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::challenge_scalar::challenge_scalar#0 -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::challenge_vector#5 -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::challenge_scalar_powers#1 -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::challenge_scalar_powers#0 -jolt-r1cs::lowering.rs::lower_claim_expr::challenge#0 -jolt-sumcheck::r1cs.rs::append_sumcheck_r1cs_constraints_for_domain::challenge#0 -jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_round::challenge#1 -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::challenge#1 -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_committed_round_consistency::challenge#1 -jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::challenge#2 -jolt-transcript::legacy.rs::Transcript::challenge_scalar::challenge#0 -jolt-transcript::legacy.rs::Transcript::challenge_scalar_powers::challenge_scalar#0 -jolt-transcript::legacy.rs::Transcript::challenge_vector::challenge#0 -jolt-verifier::stages/relations.rs::ConcreteSumcheck::draw_challenges::challenge_scalar#0 +jolt-blindfold::prove.rs::prove_inner_sumcheck::challenge_small#1 +jolt-blindfold::prove.rs::prove_outer_sumcheck::challenge_small#1 +jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#1 +jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#10 +jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#11 +jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#12 +jolt-blindfold::prove.rs::prove_with_row_committer::challenges_small#6 +jolt-blindfold::verify.rs::BlindFoldProtocol::folded_instance::challenge_small#1 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge_small#0 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge_small#1 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge_small#2 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::challenges_small#0 +jolt-dory::transcript.rs::DoryProverChannel<'_,H>::challenge_scalar::challenge#0 +jolt-dory::transcript.rs::DoryVerifierChannel<'_,'_,H>::challenge_scalar::challenge#0 +jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::challenges_small#5 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch::challenge_powers#1 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch_zk::challenge_powers#0 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::challenge_powers#1 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::challenge_powers#0 +jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::commit_round::challenge_small#1 +jolt-sumcheck::prover.rs::prove_uniskip_clear::challenge_small#1 +jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_round::challenge_small#1 +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::challenge_small#1 +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_committed::challenge_small#1 +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::challenge_small#1 +jolt-verifier::stages/relations.rs::ConcreteSumcheck::draw_challenges::challenge#0 jolt-verifier::stages/stage1/outputs.rs::Stage1BatchSumchecks::::batching_coefficient[*] jolt-verifier::stages/stage2/outputs.rs::Stage2BatchSumchecks::::batching_coefficient[*] -jolt-verifier::stages/stage2/ram_output_check.rs::RamOutputCheck::draw_challenges::challenge#0 +jolt-verifier::stages/stage2/ram_output_check.rs::RamOutputCheck::draw_challenges::challenge_small#0 jolt-verifier::stages/stage3/outputs.rs::Stage3Sumchecks::::batching_coefficient[*] jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::::batching_coefficient[*] -jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheck::draw_challenges::challenge_scalar#1 jolt-verifier::stages/stage5/outputs.rs::Stage5Sumchecks::::batching_coefficient[*] -jolt-verifier::stages/stage6a/booleanity.rs::BooleanityAddressPhase::draw_challenges::challenge#1 -jolt-verifier::stages/stage6a/booleanity.rs::BooleanityAddressPhase::draw_challenges::challenge_vector#0 +jolt-verifier::stages/stage6a/booleanity.rs::BooleanityAddressPhase::draw_challenges::challenge_small#1 +jolt-verifier::stages/stage6a/booleanity.rs::BooleanityAddressPhase::draw_challenges::challenges_small#0 jolt-verifier::stages/stage6a/outputs.rs::Stage6aSumchecks::::batching_coefficient[*] -jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge_scalar#0 -jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge_scalar#1 -jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge_scalar#2 -jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge_scalar#3 +jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge#0 +jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge#1 +jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge#2 +jolt-verifier::stages/stage6b/batch.rs::Stage6bDraws::draw::challenge#3 jolt-verifier::stages/stage6b/outputs.rs::Stage6bSumchecks::::batching_coefficient[*] jolt-verifier::stages/stage7/outputs.rs::Stage7Sumchecks::::batching_coefficient[*] -jolt-verifier::stages/stage8/verify.rs::verify::challenge_scalar_powers#0 -jolt-verifier::stages/stage8/verify.rs::verify::challenge_scalar_powers#4 -jolt-verifier::stages/uniskip.rs::draw_spartan_outer_tau::challenge_vector#0 -jolt-verifier::stages/uniskip.rs::draw_spartan_product_tau_high::challenge#0 +jolt-verifier::stages/stage8/verify.rs::verify::challenge_powers#0 +jolt-verifier::stages/stage8/verify.rs::verify::challenge_powers#4 +jolt-verifier::stages/uniskip.rs::draw_spartan_outer_tau::challenges_small#0 +jolt-verifier::stages/uniskip.rs::draw_spartan_product_tau_high::challenge_small#0 diff --git a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory new file mode 100644 index 0000000000..327c5d8f27 --- /dev/null +++ b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory @@ -0,0 +1,232 @@ +# Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ +# --test fs_obligations`. +# Review changes; this inventories identities, not transcript values. +jolt-akita::native_batching.rs::AkitaNativeBatching::prove_batch::.batched_prove#1 +jolt-akita::native_batching.rs::AkitaNativeBatching::prove_batch::bind_prover_opening#0 +jolt-akita::native_batching.rs::AkitaNativeBatching::prove_batch::bind_prover_opening#2 +jolt-akita::native_batching.rs::AkitaNativeBatching::prove_batch::prove_one_hot_opening#3 +jolt-akita::native_batching.rs::AkitaNativeBatching::prove_trace_batch::bind_prover_opening#0 +jolt-akita::native_batching.rs::AkitaNativeBatching::prove_trace_batch::prove_one_hot_opening#1 +jolt-akita::native_batching.rs::AkitaNativeBatching::verify_batch::.batched_verify#1 +jolt-akita::native_batching.rs::AkitaNativeBatching::verify_batch::bind_verifier_opening#0 +jolt-akita::native_batching.rs::AkitaNativeBatching::verify_batch::verify_one_hot_statement#2 +jolt-akita::native_batching.rs::AkitaNativeBatching::verify_trace_batch::bind_verifier_opening#0 +jolt-akita::native_batching.rs::AkitaNativeBatching::verify_trace_batch::verify_one_hot_statement#1 +jolt-akita::native_batching.rs::bind_prover_opening::absorb_setup_catalog#0 +jolt-akita::native_batching.rs::bind_prover_opening::send_selection#1 +jolt-akita::native_batching.rs::bind_verifier_opening::absorb_setup_catalog#0 +jolt-akita::native_batching.rs::bind_verifier_opening::receive_selection#1 +jolt-akita::native_batching.rs::prove_one_hot_opening::.batched_prove#0 +jolt-akita::native_batching.rs::prove_one_hot_opening::.batched_prove#1 +jolt-akita::native_batching.rs::verify_one_hot_statement::.batched_verify#0 +jolt-akita::native_batching.rs::verify_one_hot_statement::.batched_verify#1 +jolt-akita::scheme.rs::AkitaScheme::absorb_commitment::.absorb#0 +jolt-akita::scheme.rs::AkitaScheme::open::::prove_batch#0 +jolt-akita::scheme.rs::AkitaScheme::open_zk::Self::open#0 +jolt-akita::scheme.rs::AkitaScheme::prove_batch::AkitaNativeBatching::prove_trace_batch#0 +jolt-akita::scheme.rs::AkitaScheme::receive_commitment::AkitaCommitment::receive#0 +jolt-akita::scheme.rs::AkitaScheme::send_commitment::.send#0 +jolt-akita::scheme.rs::AkitaScheme::verify::::verify_batch#0 +jolt-akita::scheme.rs::AkitaScheme::verify_batch::AkitaNativeBatching::verify_trace_batch#0 +jolt-blindfold::prove.rs::prove::prove_with_row_committer::#0 +jolt-blindfold::prove.rs::prove_inner_sumcheck::send_compressed_round#0 +jolt-blindfold::prove.rs::prove_outer_sumcheck::send_compressed_round#0 +jolt-blindfold::prove.rs::prove_with_row_committer::.send#0 +jolt-blindfold::prove.rs::prove_with_row_committer::prove_inner_sumcheck#13 +jolt-blindfold::prove.rs::prove_with_row_committer::prove_outer_sumcheck#7 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#14 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#4 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#5 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#9 +jolt-blindfold::verify.rs::BlindFoldProtocol::folded_instance::FoldingCommitments::receive#0 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.folded_instance#0 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.verify_folded_eval_witness_bindings#1 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.verify_inner_folded_r1cs#3 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.verify_outer_folded_r1cs#2 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_opening#2 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::SumcheckVerifier::verify_compressed#3 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::receive_opening#4 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::SumcheckVerifier::verify_compressed#1 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive_opening#5 +jolt-dory::scheme.rs::DoryScheme::open::DoryProverChannel::new#0 +jolt-dory::scheme.rs::DoryScheme::open::dory::prove::#1 +jolt-dory::scheme.rs::DoryScheme::open_zk::.send_sigma1_responses#2 +jolt-dory::scheme.rs::DoryScheme::open_zk::DoryProverChannel::new#0 +jolt-dory::scheme.rs::DoryScheme::open_zk::dory::prove::#1 +jolt-dory::scheme.rs::DoryScheme::verify::.clone#0 +jolt-dory::scheme.rs::DoryScheme::verify::.finish#3 +jolt-dory::scheme.rs::DoryScheme::verify::DoryVerifierChannel::new#1 +jolt-dory::scheme.rs::DoryScheme::verify::dory::verify::#2 +jolt-dory::scheme.rs::DoryScheme::verify_zk::.clone#0 +jolt-dory::scheme.rs::DoryScheme::verify_zk::.finish#3 +jolt-dory::scheme.rs::DoryScheme::verify_zk::DoryVerifierChannel::new#1 +jolt-dory::scheme.rs::DoryScheme::verify_zk::dory::verify::#2 +jolt-openings::schemes.rs::CommitmentGroupRole::new_indexed::Some#0 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch::.absorb_values#0 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch::PCS::open#2 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch::absorb_evaluation#3 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch_zk::.absorb#2 +jolt-openings::schemes.rs::HomomorphicBatch::prove_batch_zk::PCS::open_zk#1 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::.absorb_values#0 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::PCS::verify#2 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::absorb_evaluation#3 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::.absorb#2 +jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::PCS::verify_zk#1 +jolt-prover::akita/prover.rs::prove::.finish#9 +jolt-prover::akita/prover.rs::prove::prove_stage1::#0 +jolt-prover::akita/prover.rs::prove::prove_stage2::#1 +jolt-prover::akita/prover.rs::prove::prove_stage3::#2 +jolt-prover::akita/prover.rs::prove::prove_stage4::#3 +jolt-prover::akita/prover.rs::prove::prove_stage5::#4 +jolt-prover::akita/prover.rs::prove::prove_stage6a::#5 +jolt-prover::akita/prover.rs::prove::prove_stage6b::#6 +jolt-prover::akita/prover.rs::prove::prove_stage7::#7 +jolt-prover::akita/prover.rs::prove::prove_stage8::#8 +jolt-prover::akita/stage0.rs::prove_stage0::.send#0 +jolt-prover::akita/stage0.rs::prove_stage0::.send#2 +jolt-prover::akita/stage0.rs::prove_stage0::absorb_public_commitments#3 +jolt-prover::akita/stage0.rs::prove_stage0::absorb_public_preamble#1 +jolt-prover::akita/stage8.rs::prove_stage8::.reduce_claims#0 +jolt-prover::akita/stage8.rs::prove_stage8::PCS::prove_batch#4 +jolt-prover::akita/stage8.rs::prove_stage8::reduce_precommitted#1 +jolt-prover::akita/stage8.rs::prove_stage8::reduce_precommitted#2 +jolt-prover::akita/stage8.rs::prove_stage8::reduce_precommitted#3 +jolt-prover::akita/stage8.rs::reduce_precommitted::.reduce_claims#0 +jolt-prover::blindfold.rs::prove_blindfold::.narg#0 +jolt-prover::blindfold.rs::prove_blindfold::.preview#1 +jolt-prover::blindfold.rs::prove_blindfold::jolt_blindfold::prove::#2 +jolt-prover::dory/prover.rs::prove::.finish#10 +jolt-prover::dory/prover.rs::prove::blindfold::prove_blindfold::#9 +jolt-prover::dory/prover.rs::prove::prove_stage1::#0 +jolt-prover::dory/prover.rs::prove::prove_stage2::#1 +jolt-prover::dory/prover.rs::prove::prove_stage3::#2 +jolt-prover::dory/prover.rs::prove::prove_stage4::#3 +jolt-prover::dory/prover.rs::prove::prove_stage5::#4 +jolt-prover::dory/prover.rs::prove::prove_stage6a::#5 +jolt-prover::dory/prover.rs::prove::prove_stage6b::#6 +jolt-prover::dory/prover.rs::prove::prove_stage7::#7 +jolt-prover::dory/prover.rs::prove::prove_stage8::#8 +jolt-prover::dory/stages/stage0.rs::prove_stage0::.send#0 +jolt-prover::dory/stages/stage0.rs::prove_stage0::.send#2 +jolt-prover::dory/stages/stage0.rs::prove_stage0::absorb_public_commitments#3 +jolt-prover::dory/stages/stage0.rs::prove_stage0::absorb_public_preamble#1 +jolt-prover::dory/stages/stage8.rs::prove_stage8::HomomorphicBatch::::prove_batch#0 +jolt-prover::dory/stages/stage8.rs::prove_stage8::HomomorphicBatch::::prove_batch_zk#1 +jolt-prover::recorder.rs::ProofMode<'a,VC>::prove_uniskip::prove_uniskip_clear::#1 +jolt-prover::recorder.rs::ProofMode<'a,VC>::prove_uniskip::prove_uniskip_committed::#0 +jolt-prover::stages/stage1.rs::prove_stage1::.draw_challenges#2 +jolt-prover::stages/stage1.rs::prove_stage1::.prove#3 +jolt-prover::stages/stage1.rs::prove_stage1::.prove_uniskip#1 +jolt-prover::stages/stage1.rs::prove_stage1::draw_spartan_outer_tau#0 +jolt-prover::stages/stage2.rs::prove_stage2::.draw_challenges#2 +jolt-prover::stages/stage2.rs::prove_stage2::.prove#3 +jolt-prover::stages/stage2.rs::prove_stage2::.prove_uniskip#1 +jolt-prover::stages/stage2.rs::prove_stage2::draw_spartan_product_tau_high#0 +jolt-prover::stages/stage3.rs::prove_stage3::.draw_challenges#0 +jolt-prover::stages/stage3.rs::prove_stage3::.prove#1 +jolt-prover::stages/stage4.rs::prove_stage4::.draw_challenges#0 +jolt-prover::stages/stage4.rs::prove_stage4::.prove#2 +jolt-prover::stages/stage5.rs::prove_stage5::.draw_challenges#0 +jolt-prover::stages/stage5.rs::prove_stage5::.prove#1 +jolt-prover::stages/stage6a.rs::prove_stage6a::.draw_challenges#0 +jolt-prover::stages/stage6a.rs::prove_stage6a::.prove#1 +jolt-prover::stages/stage6b.rs::prove_stage6b::.prove#1 +jolt-prover::stages/stage6b.rs::prove_stage6b::Stage6bDraws::draw#0 +jolt-prover::stages/stage7.rs::prove_stage7::.draw_challenges#0 +jolt-prover::stages/stage7.rs::prove_stage7::.prove#1 +jolt-sumcheck::prover.rs::prove_batch::.absorb_round#0 +jolt-sumcheck::prover.rs::prove_uniskip_clear::send_full_round#0 +jolt-sumcheck::prover.rs::prove_uniskip_committed::.commit_round#0 +jolt-sumcheck::prover.rs::prove_uniskip_committed::.finish#1 +jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_round::send_compressed_round#0 +jolt-sumcheck::recorder.rs::CommittedSumcheckRecorder<'_,F,VC,R>::absorb_round::.commit_round#0 +jolt-sumcheck::recorder.rs::CommittedSumcheckRecorder<'_,F,VC,R>::finish::.finish#0 +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::receive_full_round#0 +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::receive_compressed_round#0 +jolt-verifier::stages/relations.rs::receive_member_openings::receive_member_openings_except#0 +jolt-verifier::stages/stage1/verify.rs::verify::.draw_challenges#2 +jolt-verifier::stages/stage1/verify.rs::verify::.verify_clear#3 +jolt-verifier::stages/stage1/verify.rs::verify::.verify_zk#5 +jolt-verifier::stages/stage1/verify.rs::verify::uniskip::draw_spartan_outer_tau#0 +jolt-verifier::stages/stage1/verify.rs::verify::uniskip::verify_clear#1 +jolt-verifier::stages/stage1/verify.rs::verify::uniskip::verify_zk#4 +jolt-verifier::stages/stage2/verify.rs::verify::.draw_challenges#1 +jolt-verifier::stages/stage2/verify.rs::verify::.verify_clear#3 +jolt-verifier::stages/stage2/verify.rs::verify::.verify_zk#2 +jolt-verifier::stages/stage2/verify.rs::verify::verify_product_uniskip#0 +jolt-verifier::stages/stage2/verify.rs::verify_product_uniskip::uniskip::draw_spartan_product_tau_high#0 +jolt-verifier::stages/stage2/verify.rs::verify_product_uniskip::uniskip::verify_clear#1 +jolt-verifier::stages/stage2/verify.rs::verify_product_uniskip::uniskip::verify_zk#2 +jolt-verifier::stages/stage3/verify.rs::verify::.draw_challenges#0 +jolt-verifier::stages/stage3/verify.rs::verify::.verify_clear#1 +jolt-verifier::stages/stage3/verify.rs::verify::.verify_zk#2 +jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive_member_openings#0 +jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive_member_openings#1 +jolt-verifier::stages/stage4/verify.rs::verify::.draw_challenges#0 +jolt-verifier::stages/stage4/verify.rs::verify::.receive_output_claims#3 +jolt-verifier::stages/stage4/verify.rs::verify::.verify_clear_with#2 +jolt-verifier::stages/stage4/verify.rs::verify::.verify_zk#4 +jolt-verifier::stages/stage4/verify.rs::verify::RamValCheckStagedOpenings::receive#1 +jolt-verifier::stages/stage5/verify.rs::verify::.draw_challenges#0 +jolt-verifier::stages/stage5/verify.rs::verify::.verify_clear#1 +jolt-verifier::stages/stage5/verify.rs::verify::.verify_zk#2 +jolt-verifier::stages/stage6a/verify.rs::verify::.draw_challenges#0 +jolt-verifier::stages/stage6a/verify.rs::verify::.verify_clear#2 +jolt-verifier::stages/stage6a/verify.rs::verify::.verify_zk#1 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#0 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#10 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#2 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#3 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#4 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#5 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#6 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#7 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#8 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#9 +jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings_except#1 +jolt-verifier::stages/stage6b/verify.rs::verify::.verify_clear_with#2 +jolt-verifier::stages/stage6b/verify.rs::verify::.verify_zk_with#1 +jolt-verifier::stages/stage6b/verify.rs::verify::Stage6bDraws::draw#0 +jolt-verifier::stages/stage7/verify.rs::verify::.draw_challenges#0 +jolt-verifier::stages/stage7/verify.rs::verify::.verify_clear#2 +jolt-verifier::stages/stage7/verify.rs::verify::.verify_zk#1 +jolt-verifier::stages/stage8/packed.rs::reduce_object::.reduce_claims#0 +jolt-verifier::stages/stage8/packed.rs::verify::.reduce_claims#0 +jolt-verifier::stages/stage8/packed.rs::verify::PCS::verify_batch#4 +jolt-verifier::stages/stage8/packed.rs::verify::reduce_object#1 +jolt-verifier::stages/stage8/packed.rs::verify::reduce_object#2 +jolt-verifier::stages/stage8/packed.rs::verify::reduce_object#3 +jolt-verifier::stages/stage8/verify.rs::verify::.absorb#2 +jolt-verifier::stages/stage8/verify.rs::verify::PCS::verify#5 +jolt-verifier::stages/stage8/verify.rs::verify::PCS::verify_zk#1 +jolt-verifier::stages/stage8/verify.rs::verify::super::packed::verify#8 +jolt-verifier::stages/uniskip.rs::verify_clear::SumcheckVerifier::verify#0 +jolt-verifier::stages/uniskip.rs::verify_zk::SumcheckVerifier::verify_committed#0 +jolt-verifier::verifier.rs::seed_transcript::ProofCommitments::receive::#2 +jolt-verifier::verifier.rs::seed_transcript::ProofHeader::receive#0 +jolt-verifier::verifier.rs::seed_transcript::absorb_public_commitments#3 +jolt-verifier::verifier.rs::seed_transcript::absorb_public_preamble#1 +jolt-verifier::verifier.rs::verify::.finish#13 +jolt-verifier::verifier.rs::verify::.finish#2 +jolt-verifier::verifier.rs::verify::.verify#1 +jolt-verifier::verifier.rs::verify::seed_transcript#3 +jolt-verifier::verifier.rs::verify::stage1::verify::#4 +jolt-verifier::verifier.rs::verify::stage2::verify#5 +jolt-verifier::verifier.rs::verify::stage3::verify#6 +jolt-verifier::verifier.rs::verify::stage4::verify#7 +jolt-verifier::verifier.rs::verify::stage5::verify#8 +jolt-verifier::verifier.rs::verify::stage6a::verify#9 +jolt-verifier::verifier.rs::verify::stage6b::verify#10 +jolt-verifier::verifier.rs::verify::stage7::verify#11 +jolt-verifier::verifier.rs::verify::stage8::verify#12 +jolt-verifier::verifier.rs::verify::verify_stages#0 +jolt-verifier::verifier.rs::verify_stages::seed_transcript#0 +jolt-verifier::verifier.rs::verify_stages::stage1::verify::#1 +jolt-verifier::verifier.rs::verify_stages::stage2::verify#2 +jolt-verifier::verifier.rs::verify_stages::stage3::verify#3 +jolt-verifier::verifier.rs::verify_stages::stage4::verify#4 +jolt-verifier::verifier.rs::verify_stages::stage5::verify#5 +jolt-verifier::verifier.rs::verify_stages::stage6a::verify#6 +jolt-verifier::verifier.rs::verify_stages::stage6b::verify#7 +jolt-verifier::verifier.rs::verify_stages::stage7::verify#8 +jolt-verifier::verifier.rs::verify_stages::stage8::verify#9 diff --git a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory new file mode 100644 index 0000000000..baa57ba920 --- /dev/null +++ b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory @@ -0,0 +1,80 @@ +# Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ +# --test fs_obligations`. +# Review changes; this inventories identities, not transcript values. +jolt-akita::adapters.rs::AkitaCommitment::receive::receive#0::transcript.receive() +jolt-akita::adapters.rs::AkitaCommitment::receive::receive_bytes#1::transcript.receive_bytes(payload_len) +jolt-akita::adapters.rs::AkitaCommitment::send::send#0::transcript.send(&self.header()) +jolt-akita::adapters.rs::AkitaCommitment::send::send_bytes#1::transcript.send_bytes(&self.serialized_backend_bytes) +jolt-akita::adapters.rs::receive_selection::receive#0::transcript.receive() +jolt-akita::adapters.rs::send_selection::send#0::transcript.send(selection.row_digest.as_bytes()) +jolt-blindfold::prove.rs::prove_with_row_committer::send_all#2::transcript.send_all(&folded_eval_outputs) +jolt-blindfold::prove.rs::prove_with_row_committer::send_all#3::transcript.send_all(&folded_eval_blindings) +jolt-blindfold::prove.rs::prove_with_row_committer::send_all#8::transcript.send_all(&[az_rx,bz_rx,cz_rx]) +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_n#0::transcript.receive_n(eval_count) +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_n#1::transcript.receive_n(eval_count) +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive#2::transcript.receive() +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive#3::transcript.receive() +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive#4::transcript.receive() +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive#1::transcript.receive() +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#0::transcript.receive_n(dimensions.auxiliary_rows) +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#2::transcript.receive_n(dimensions.coefficient_rows) +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#3::transcript.receive_n(dimensions.output_claim_rows) +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#4::transcript.receive_n(dimensions.auxiliary_rows) +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#5::transcript.receive_n(dimensions.error.row_count) +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#6::transcript.receive_n(eval_count) +jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#7::transcript.receive_n(dimensions.error.row_count) +jolt-blindfold::wire.rs::FoldingCommitments::send::send#1::transcript.send(&self.random_u) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#0::transcript.send_all(&self.auxiliary_rows) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#2::transcript.send_all(&self.random_rounds) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#3::transcript.send_all(&self.random_output_claim_rows) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#4::transcript.send_all(&self.random_auxiliary_rows) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#5::transcript.send_all(&self.random_error_rows) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#6::transcript.send_all(&self.random_evals) +jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#7::transcript.send_all(&self.cross_term_error_rows) +jolt-blindfold::wire.rs::receive_opening::receive#1::transcript.receive() +jolt-blindfold::wire.rs::receive_opening::receive_n#0::transcript.receive_n(row_len) +jolt-blindfold::wire.rs::send_opening::send#1::transcript.send(&opening.combined_blinding) +jolt-blindfold::wire.rs::send_opening::send_all#0::transcript.send_all(&opening.combined_vector) +jolt-dory::scheme.rs::DoryScheme::receive_commitment::receive#0::transcript.receive() +jolt-dory::scheme.rs::DoryScheme::send_commitment::send#0::transcript.send(commitment) +jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_bytes::send_bytes#0::self.transcript.send_bytes(bytes) +jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_field::send_bytes#0::self.transcript.send_bytes(&compressed(x)) +jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_group::send_bytes#0::self.transcript.send_bytes(&compressed(g)) +jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_serde::send_bytes#0::self.transcript.send_bytes(&compressed(s)) +jolt-dory::transcript.rs::DoryProverChannel<'a,H>::send_sigma1_responses::send_bytes#0::self.transcript.send_bytes(&compressed(response)) +jolt-dory::transcript.rs::DoryVerifierChannel<'a,'p,H>::expect::receive_bytes#0::self.transcript.receive_bytes(bytes.len()) +jolt-dory::transcript.rs::read::receive_bytes#0::scout.receive_bytes(T::BYTES) +jolt-prover::stages/stage4.rs::prove_stage4::send_all#1::#[cfg(not(feature="zk"))]transcript.send_all(&ram_val_check_init.staged_openings().values()) +jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::commit_round::send#0::transcript.send(&commitment) +jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::finish::send#0::transcript.send(&VC::commit(self.setup,row,&blinding)) +jolt-sumcheck::prover.rs::prove_uniskip_clear::send#2::transcript.send(&output_claim) +jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::finish::send_all#0::transcript.send_all(output_claim_values) +jolt-sumcheck::round_proof.rs::receive_compressed_round::receive_n#0::transcript.receive_n(degree.max(1)) +jolt-sumcheck::round_proof.rs::receive_full_round::receive_n#0::transcript.receive_n(width) +jolt-sumcheck::round_proof.rs::send_compressed_round::send_all#0::transcript.send_all(compressed.coeffs_except_linear_term()) +jolt-sumcheck::round_proof.rs::send_full_round::send_all#0::transcript.send_all(&padded_coefficients(poly,degree)?) +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_committed::receive#0::transcript.receive() +jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_committed::receive_n#2::transcript.receive_n(num_output_commitments) +jolt-verifier::proof.rs::ProofCommitments::receive::receive_commitment#0::PCS::receive_commitment(setup,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#0::PCS::send_commitment(&trace.rd_inc,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#1::PCS::send_commitment(&trace.ram_inc,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#2::PCS::send_commitment(commitment,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#3::#[cfg(feature="field-inline")]PCS::send_commitment(&trace.field_inline.field_registers.rd_inc,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#4::PCS::send_commitment(&self.one_hot_trace,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#5::#[cfg(feature="field-inline")]PCS::send_commitment(&self.field_inc,transcript) +jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#6::PCS::send_commitment(commitment,transcript) +jolt-verifier::proof.rs::ProofHeader::receive::receive#0::transcript.receive::() +jolt-verifier::proof.rs::ProofHeader::receive::receive#1::transcript.receive::() +jolt-verifier::proof.rs::ProofHeader::receive::receive#2::transcript.receive() +jolt-verifier::proof.rs::ProofHeader::receive::receive#3::transcript.receive::() +jolt-verifier::proof.rs::ProofHeader::receive::receive#4::transcript.receive::() +jolt-verifier::proof.rs::ProofHeader::send::send#0::transcript.send(&num::u64_from_usize(self.trace_length)) +jolt-verifier::proof.rs::ProofHeader::send::send#1::transcript.send(&num::u64_from_usize(self.ram_K)) +jolt-verifier::proof.rs::ProofHeader::send::send#3::transcript.send(&self.trace_polynomial_order.transcript_scalar()) +jolt-verifier::proof.rs::ProofHeader::send::send#4::transcript.send(&u8::from(self.untrusted_advice)) +jolt-verifier::proof.rs::ProofHeader::send::send_all#2::transcript.send_all(&[self.rw_config.ram_rw_phase1_num_rounds,self.rw_config.ram_rw_phase2_num_rounds,self.rw_config.registers_rw_phase1_num_rounds,self.rw_config.registers_rw_phase2_num_rounds,self.one_hot_config.log_k_chunk,self.one_hot_config.lookups_ra_virtual_log_k_chunk,]) +jolt-verifier::stages/relations.rs::receive_member_openings_except::receive#0::transcript.receive() +jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive#2::transcript.receive() +jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive#3::transcript.receive() +jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheckStagedOpenings::receive::receive#0::transcript.receive() +jolt-verifier::stages/uniskip.rs::verify_clear::receive#1::transcript.receive() diff --git a/crates/jolt-verifier/tests/fs_inventory/scope-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/scope-sites.inventory deleted file mode 100644 index 3200cf7b3b..0000000000 --- a/crates/jolt-verifier/tests/fs_inventory/scope-sites.inventory +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ -# --test fs_obligations --features fs-audit`. -# Review changes; this inventories identities, not transcript values. -jolt-verifier::stages/stage1/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage1) -jolt-verifier::stages/stage2/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage2) -jolt-verifier::stages/stage3/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage3) -jolt-verifier::stages/stage4/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage4) -jolt-verifier::stages/stage5/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage5) -jolt-verifier::stages/stage6a/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage6a) -jolt-verifier::stages/stage6b/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage6b) -jolt-verifier::stages/stage7/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage7) -jolt-verifier::stages/stage8/verify.rs::verify::jolt_verifier_derive::fs_scope(Stage8) -jolt-verifier::verifier.rs::absorb_commitments::jolt_verifier_derive::fs_scope(Commitments) -jolt-verifier::verifier.rs::absorb_preamble::jolt_verifier_derive::fs_scope(Preamble) -jolt-verifier::verifier.rs::verify::fs_scope_guard(BlindFold) diff --git a/crates/jolt-verifier/tests/fs_inventory/site-tags.inventory b/crates/jolt-verifier/tests/fs_inventory/site-tags.inventory new file mode 100644 index 0000000000..bbb490d7aa --- /dev/null +++ b/crates/jolt-verifier/tests/fs_inventory/site-tags.inventory @@ -0,0 +1,42 @@ +# Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ +# --test fs_obligations`. +# Review changes; this inventories identities, not transcript values. +jolt-prover::akita/stage0.rs::prove_stage0::site(COMMITMENTS) +jolt-prover::akita/stage0.rs::prove_stage0::site(PREAMBLE) +jolt-prover::akita/stage8.rs::prove_stage8::site(STAGE8) +jolt-prover::blindfold.rs::prove_blindfold::site(BLINDFOLD) +jolt-prover::dory/stages/stage0.rs::prove_stage0::site(COMMITMENTS) +jolt-prover::dory/stages/stage0.rs::prove_stage0::site(PREAMBLE) +jolt-prover::dory/stages/stage8.rs::prove_stage8::site(STAGE8) +jolt-prover::stages/stage1.rs::prove_stage1::site(STAGE1) +jolt-prover::stages/stage2.rs::prove_stage2::site(STAGE2) +jolt-prover::stages/stage3.rs::prove_stage3::site(STAGE3) +jolt-prover::stages/stage4.rs::prove_stage4::site(STAGE4) +jolt-prover::stages/stage5.rs::prove_stage5::site(STAGE5) +jolt-prover::stages/stage6a.rs::prove_stage6a::site(STAGE6A) +jolt-prover::stages/stage6b.rs::prove_stage6b::site(STAGE6B) +jolt-prover::stages/stage7.rs::prove_stage7::site(STAGE7) +jolt-verifier::sites.rs::const BLINDFOLD=SiteId::label("jolt/blindfold") +jolt-verifier::sites.rs::const COMMITMENTS=SiteId::label("jolt/commitments") +jolt-verifier::sites.rs::const PREAMBLE=SiteId::label("jolt/preamble") +jolt-verifier::sites.rs::const STAGE1=SiteId::label("jolt/stage1") +jolt-verifier::sites.rs::const STAGE2=SiteId::label("jolt/stage2") +jolt-verifier::sites.rs::const STAGE3=SiteId::label("jolt/stage3") +jolt-verifier::sites.rs::const STAGE4=SiteId::label("jolt/stage4") +jolt-verifier::sites.rs::const STAGE5=SiteId::label("jolt/stage5") +jolt-verifier::sites.rs::const STAGE6A=SiteId::label("jolt/stage6a") +jolt-verifier::sites.rs::const STAGE6B=SiteId::label("jolt/stage6b") +jolt-verifier::sites.rs::const STAGE7=SiteId::label("jolt/stage7") +jolt-verifier::sites.rs::const STAGE8=SiteId::label("jolt/stage8") +jolt-verifier::stages/stage1/verify.rs::verify::site(STAGE1) +jolt-verifier::stages/stage2/verify.rs::verify::site(STAGE2) +jolt-verifier::stages/stage3/verify.rs::verify::site(STAGE3) +jolt-verifier::stages/stage4/verify.rs::verify::site(STAGE4) +jolt-verifier::stages/stage5/verify.rs::verify::site(STAGE5) +jolt-verifier::stages/stage6a/verify.rs::verify::site(STAGE6A) +jolt-verifier::stages/stage6b/verify.rs::verify::site(STAGE6B) +jolt-verifier::stages/stage7/verify.rs::verify::site(STAGE7) +jolt-verifier::stages/stage8/verify.rs::verify::site(STAGE8) +jolt-verifier::verifier.rs::seed_transcript::site(COMMITMENTS) +jolt-verifier::verifier.rs::seed_transcript::site(PREAMBLE) +jolt-verifier::verifier.rs::verify::site(BLINDFOLD) diff --git a/crates/jolt-verifier/tests/fs_inventory/source-schema.inventory b/crates/jolt-verifier/tests/fs_inventory/source-schema.inventory index 5efe818a78..409a5e5029 100644 --- a/crates/jolt-verifier/tests/fs_inventory/source-schema.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/source-schema.inventory @@ -1,5 +1,5 @@ # Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ -# --test fs_obligations --features fs-audit`. +# --test fs_obligations`. # Review changes; this inventories identities, not transcript values. common::jolt_device.rs::JoltDevice.inputs common::jolt_device.rs::JoltDevice.memory_layout @@ -27,6 +27,14 @@ common::jolt_device.rs::MemoryLayout.trusted_advice_end common::jolt_device.rs::MemoryLayout.trusted_advice_start common::jolt_device.rs::MemoryLayout.untrusted_advice_end common::jolt_device.rs::MemoryLayout.untrusted_advice_start +jolt-claims::protocols/jolt/geometry/dimensions.rs::JoltOneHotConfig.log_k_chunk +jolt-claims::protocols/jolt/geometry/dimensions.rs::JoltOneHotConfig.lookups_ra_virtual_log_k_chunk +jolt-claims::protocols/jolt/geometry/dimensions.rs::JoltReadWriteConfig.ram_rw_phase1_num_rounds +jolt-claims::protocols/jolt/geometry/dimensions.rs::JoltReadWriteConfig.ram_rw_phase2_num_rounds +jolt-claims::protocols/jolt/geometry/dimensions.rs::JoltReadWriteConfig.registers_rw_phase1_num_rounds +jolt-claims::protocols/jolt/geometry/dimensions.rs::JoltReadWriteConfig.registers_rw_phase2_num_rounds +jolt-claims::protocols/jolt/geometry/dimensions.rs::TracePolynomialOrder::AddressMajor +jolt-claims::protocols/jolt/geometry/dimensions.rs::TracePolynomialOrder::CycleMajor jolt-program::preprocess/program.rs::JoltProgramPreprocessing.bytecode jolt-program::preprocess/program.rs::JoltProgramPreprocessing.max_padded_trace_length jolt-program::preprocess/program.rs::JoltProgramPreprocessing.memory_layout @@ -38,59 +46,40 @@ jolt-program::preprocess/program.rs::ProgramMetadata.min_bytecode_address jolt-program::preprocess/program.rs::ProgramMetadata.program_image_len_words jolt-verifier::config.rs::JoltProtocolConfig.commitment jolt-verifier::config.rs::JoltProtocolConfig.field_inline -jolt-verifier::config.rs::JoltProtocolConfig.scalar_challenge_endianness jolt-verifier::config.rs::JoltProtocolConfig.zk jolt-verifier::config.rs::ZkConfig::BlindFold jolt-verifier::config.rs::ZkConfig::Transparent -jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.bytecode_chunk_commitments -jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.bytecode_chunk_count -jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.direct_program_commitments +jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.bytecode_chunk_commitments @cfg(not(feature="akita")) +jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.bytecode_chunk_count @cfg(feature="akita") +jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.direct_program_commitments @cfg(feature="akita") jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.max_padded_trace_length jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.memory_layout jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.meta -jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.program_image_commitment -jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.trace_order +jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.program_image_commitment @cfg(not(feature="akita")) +jolt-verifier::preprocessing.rs::CommittedProgramPreprocessing.trace_order @cfg(feature="akita") jolt-verifier::preprocessing.rs::JoltVerifierPreprocessing.pcs_setup jolt-verifier::preprocessing.rs::JoltVerifierPreprocessing.preprocessing_digest jolt-verifier::preprocessing.rs::JoltVerifierPreprocessing.program jolt-verifier::preprocessing.rs::JoltVerifierPreprocessing.vc_setup jolt-verifier::preprocessing.rs::ProgramPreprocessing::Committed.0 jolt-verifier::preprocessing.rs::ProgramPreprocessing::Full.0 -jolt-verifier::proof.rs::ClearProofClaims.stage1 -jolt-verifier::proof.rs::ClearProofClaims.stage2 -jolt-verifier::proof.rs::ClearProofClaims.stage3 -jolt-verifier::proof.rs::ClearProofClaims.stage4 -jolt-verifier::proof.rs::ClearProofClaims.stage5 -jolt-verifier::proof.rs::ClearProofClaims.stage6a -jolt-verifier::proof.rs::ClearProofClaims.stage6b -jolt-verifier::proof.rs::ClearProofClaims.stage7 +jolt-verifier::proof.rs::FieldInlineCommitments.field_registers +jolt-verifier::proof.rs::FieldRegistersCommitments.rd_inc jolt-verifier::proof.rs::JoltCommitments.bytecode_ra -jolt-verifier::proof.rs::JoltCommitments.field_inline +jolt-verifier::proof.rs::JoltCommitments.field_inline @cfg(feature="field-inline") jolt-verifier::proof.rs::JoltCommitments.instruction_ra jolt-verifier::proof.rs::JoltCommitments.ram_inc jolt-verifier::proof.rs::JoltCommitments.ram_ra jolt-verifier::proof.rs::JoltCommitments.rd_inc -jolt-verifier::proof.rs::JoltProof.claims -jolt-verifier::proof.rs::JoltProof.commitments -jolt-verifier::proof.rs::JoltProof.field_inc_commitment -jolt-verifier::proof.rs::JoltProof.joint_opening_proof -jolt-verifier::proof.rs::JoltProof.one_hot_config +jolt-verifier::proof.rs::JoltProof.narg jolt-verifier::proof.rs::JoltProof.protocol -jolt-verifier::proof.rs::JoltProof.ram_K -jolt-verifier::proof.rs::JoltProof.rw_config -jolt-verifier::proof.rs::JoltProof.stages -jolt-verifier::proof.rs::JoltProof.trace_length -jolt-verifier::proof.rs::JoltProof.trace_polynomial_order -jolt-verifier::proof.rs::JoltProof.untrusted_advice_commitment -jolt-verifier::proof.rs::JoltProofClaims::Clear.0 -jolt-verifier::proof.rs::JoltProofClaims::Zk.blindfold_proof -jolt-verifier::proof.rs::JoltStageProofs.stage1_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage1_uni_skip_first_round_proof -jolt-verifier::proof.rs::JoltStageProofs.stage2_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage2_uni_skip_first_round_proof -jolt-verifier::proof.rs::JoltStageProofs.stage3_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage4_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage5_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage6a_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage6b_sumcheck_proof -jolt-verifier::proof.rs::JoltStageProofs.stage7_sumcheck_proof +jolt-verifier::proof.rs::ProofCommitments.field_inc @cfg(all(feature="akita",feature="field-inline")) +jolt-verifier::proof.rs::ProofCommitments.one_hot_trace @cfg(feature="akita") +jolt-verifier::proof.rs::ProofCommitments.trace @cfg(not(feature="akita")) +jolt-verifier::proof.rs::ProofCommitments.untrusted_advice +jolt-verifier::proof.rs::ProofHeader.one_hot_config +jolt-verifier::proof.rs::ProofHeader.ram_K +jolt-verifier::proof.rs::ProofHeader.rw_config +jolt-verifier::proof.rs::ProofHeader.trace_length +jolt-verifier::proof.rs::ProofHeader.trace_polynomial_order +jolt-verifier::proof.rs::ProofHeader.untrusted_advice diff --git a/crates/jolt-verifier/tests/fs_obligations.rs b/crates/jolt-verifier/tests/fs_obligations.rs index 72bfcc2bb1..d1b46b5731 100644 --- a/crates/jolt-verifier/tests/fs_obligations.rs +++ b/crates/jolt-verifier/tests/fs_obligations.rs @@ -1,4 +1,27 @@ -#![cfg(feature = "fs-audit")] +//! Source census of every place the Jolt protocol touches its Fiat-Shamir +//! transcript, frozen into `tests/fs_inventory/*.inventory`. +//! +//! The census walks the production module tree of the verifier's runtime +//! closure (seeded at `jolt-verifier`, `jolt-dory`, `jolt-akita`) plus +//! `jolt-prover`, so both halves of every prover message are inventoried. It is +//! a review gate, not a soundness check: adding, removing, or reordering a +//! transcript operation changes an inventory line, and the diff must be +//! reviewed and blessed. Run-time agreement between the prover's and +//! verifier's transcripts is checked separately from the `logging` event +//! streams. +//! +//! `jolt-transcript` itself is out of scope: its sponge, framing, and +//! grinding behaviour is pinned by its per-sponge known-answer tests. +//! +//! Only receivers the census can see are transcripts are recorded: names or +//! fields containing `transcript`/`channel`, parameters and `let` bindings +//! typed (or constructed) as a transcript or a `Channel`/`Transcript`-bounded +//! generic, and `self` inside a `Channel`/`*Transcript*` impl. An untyped +//! closure parameter (`|t| t.challenge()`) is invisible to it. +//! +//! Regenerate with +//! `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier --test fs_obligations`. + #![expect( clippy::expect_used, clippy::panic, @@ -15,37 +38,105 @@ use std::{ use quote::ToTokens; use serde_json::Value; use syn::{ + punctuated::Punctuated, visit::{self, Visit}, - Attribute, ExprCall, ExprMethodCall, ImplItemFn, ItemEnum, ItemFn, ItemImpl, ItemMod, - ItemStruct, ItemTrait, Macro, TraitItemFn, + Attribute, Expr, ExprCall, ExprClosure, ExprMethodCall, FnArg, Generics, ImplItemFn, Item, + ItemConst, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, ItemTrait, Local, Macro, Pat, + Signature, Token, TraitItemFn, WherePredicate, }; const ABSORB_INVENTORY: &str = "tests/fs_inventory/absorb-sites.inventory"; +const MESSAGE_INVENTORY: &str = "tests/fs_inventory/message-sites.inventory"; const CHALLENGE_INVENTORY: &str = "tests/fs_inventory/challenge-sites.inventory"; -const SCOPE_INVENTORY: &str = "tests/fs_inventory/scope-sites.inventory"; +const DELEGATION_INVENTORY: &str = "tests/fs_inventory/delegation-sites.inventory"; +const SITE_INVENTORY: &str = "tests/fs_inventory/site-tags.inventory"; const SOURCE_INVENTORY: &str = "tests/fs_inventory/source-schema.inventory"; +/// Public-input absorbs on a [`jolt_transcript::Channel`]. +const ABSORB_METHODS: &[&str] = &["public", "public_all", "public_bytes"]; +/// Prover messages: written by `ProverTranscript`, read by `VerifierTranscript`, +/// or exchanged role-generically through `Channel`. +const MESSAGE_METHODS: &[&str] = &[ + "send", + "send_all", + "send_bytes", + "send_bounded_bytes", + "send_nonce", + "grind", + "receive", + "receive_n", + "receive_bytes", + "receive_bounded_bytes", + "receive_nonce", + "check_grind", + "exchange", + "exchange_all", +]; +const CHALLENGE_METHODS: &[&str] = &[ + "challenge", + "challenge_small", + "challenges_small", + "challenge_powers", + "challenge_bytes", +]; +/// `CommitmentScheme`'s transcript hooks, recognised by name at any call shape +/// (`PCS::send_commitment(..)`, `::..`). +const PCS_ABSORBS: &[&str] = &["absorb_commitment"]; +const PCS_MESSAGES: &[&str] = &["send_commitment", "receive_commitment"]; + +/// Types whose shape is part of the proof or of the public statement. The +/// header and commitments are the proof's leading prover messages; the +/// preprocessing, device, and configuration types are what the preamble must +/// bind. A field added to any of them needs a matching send/receive or public +/// absorb, so the schema diff is reviewed next to the site inventories. +const SOURCE_TYPES: &[&str] = &[ + "CommittedProgramPreprocessing", + "FieldInlineCommitments", + "FieldRegistersCommitments", + "JoltCommitments", + "JoltDevice", + "JoltOneHotConfig", + "JoltProgramPreprocessing", + "JoltProof", + "JoltProtocolConfig", + "JoltReadWriteConfig", + "JoltVerifierPreprocessing", + "MemoryLayout", + "ProgramMetadata", + "ProgramPreprocessing", + "ProofCommitments", + "ProofHeader", + "TracePolynomialOrder", + "ZkConfig", +]; + #[derive(Clone)] struct PackageSource { name: String, src: PathBuf, } +#[derive(Default)] +struct Census { + absorb: BTreeSet, + message: BTreeSet, + challenge: BTreeSet, + delegation: BTreeSet, + site: BTreeSet, + source: BTreeSet, +} + #[test] -fn verifier_fs_inventory_is_complete() { +fn fs_inventory_is_complete() { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace = manifest_dir .parent() .and_then(Path::parent) .expect("jolt-verifier must be in /crates"); - let packages = production_sources(workspace); - let mut absorb_sites = BTreeSet::new(); - let mut challenge_sites = BTreeSet::new(); - let mut scope_sites = BTreeSet::new(); - let mut source_fields = BTreeSet::new(); - - for package in packages { - for path in rust_sources(&package.src) { + let mut census = Census::default(); + + for package in production_sources(workspace) { + for path in module_tree(&package.src) { let source = fs::read_to_string(&path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); let syntax = syn::parse_file(&source) @@ -58,36 +149,28 @@ fn verifier_fs_inventory_is_complete() { package.name, relative.to_string_lossy().replace('\\', "/") ); - - let mut visitor = InventoryVisitor::new(file_id); - visitor.visit_file(&syntax); - absorb_sites.extend(visitor.absorb_sites); - challenge_sites.extend(visitor.challenge_sites); - scope_sites.extend(visitor.scope_sites); - source_fields.extend(visitor.source_fields); + InventoryVisitor::new(file_id, &mut census).visit_file(&syntax); } } - check_inventory( - &manifest_dir.join(ABSORB_INVENTORY), - "Fiat-Shamir absorption sites", - &absorb_sites, - ); - check_inventory( - &manifest_dir.join(CHALLENGE_INVENTORY), - "Fiat-Shamir challenge sites", - &challenge_sites, - ); - check_inventory( - &manifest_dir.join(SCOPE_INVENTORY), - "Fiat-Shamir scope annotations", - &scope_sites, - ); - check_inventory( - &manifest_dir.join(SOURCE_INVENTORY), - "verifier source schema", - &source_fields, - ); + for (file, label, actual) in [ + (ABSORB_INVENTORY, "public absorb sites", &census.absorb), + (MESSAGE_INVENTORY, "prover message sites", &census.message), + (CHALLENGE_INVENTORY, "challenge sites", &census.challenge), + ( + DELEGATION_INVENTORY, + "transcript delegation sites", + &census.delegation, + ), + (SITE_INVENTORY, "transcript site tags", &census.site), + ( + SOURCE_INVENTORY, + "proof and statement schema", + &census.source, + ), + ] { + check_inventory(&manifest_dir.join(file), label, actual); + } } fn production_sources(workspace: &Path) -> Vec { @@ -110,6 +193,14 @@ fn production_sources(workspace: &Path) -> Vec { .as_array() .expect("cargo metadata resolve nodes are missing"); + // Path dependencies outside the workspace (a local Akita checkout) are + // upstream code with their own transcript tests. + let workspace_members = metadata["workspace_members"] + .as_array() + .expect("cargo metadata workspace members are missing") + .iter() + .map(|id| id.as_str().expect("workspace member id is missing")) + .collect::>(); let package_by_id = packages .iter() .map(|package| { @@ -132,7 +223,7 @@ fn production_sources(workspace: &Path) -> Vec { Some("jolt-verifier" | "jolt-dory" | "jolt-akita") ) }) - .map(|package| package["id"].as_str().expect("seed package id is missing")) + .map(package_id) .collect::>(); let mut closure = BTreeSet::new(); while let Some(id) = pending.pop() { @@ -160,11 +251,21 @@ fn production_sources(workspace: &Path) -> Vec { } } } + // The prover writes the messages the verifier closure reads; its own + // dependency closure (tracer, witness, kernels) never touches a transcript. + closure.extend( + packages + .iter() + .filter(|package| package["name"] == "jolt-prover") + .map(package_id), + ); let mut sources = closure .into_iter() .filter_map(|id| package_by_id.get(id)) - .filter(|package| package["source"].is_null()) + .filter(|package| { + workspace_members.contains(package_id(package)) && package["name"] != "jolt-transcript" + }) .filter_map(|package| { let manifest = PathBuf::from( package["manifest_path"] @@ -172,7 +273,7 @@ fn production_sources(workspace: &Path) -> Vec { .expect("package manifest path is missing"), ); let src = manifest.parent()?.join("src"); - src.is_dir().then(|| PackageSource { + src.join("lib.rs").is_file().then(|| PackageSource { name: package["name"] .as_str() .expect("package name is missing") @@ -185,35 +286,60 @@ fn production_sources(workspace: &Path) -> Vec { sources } -fn rust_sources(root: &Path) -> Vec { - fn collect(path: &Path, output: &mut Vec) { - let mut entries = fs::read_dir(path) - .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) - .map(|entry| entry.expect("failed to read source directory entry").path()) - .collect::>(); - entries.sort(); - for entry in entries { - if entry.is_dir() { - collect(&entry, output); - } else if entry.extension().is_some_and(|extension| extension == "rs") - && entry - .file_stem() - .is_none_or(|stem| !matches!(stem.to_str(), Some("prove" | "prover" | "tests"))) - { - output.push(entry); +/// The library's source files reachable from `lib.rs` through `mod` items that +/// are not test-gated. Feature-gated modules are included: every build's +/// transcript sites are inventoried. +fn module_tree(src: &Path) -> Vec { + fn declared_modules(items: &[Item], dir: &Path, output: &mut Vec) { + for item in items { + let Item::Mod(module) = item else { + continue; + }; + if cfg_test(&module.attrs) { + continue; } + let name = module.ident.to_string(); + if let Some((_, items)) = &module.content { + declared_modules(items, &dir.join(&name), output); + continue; + } + let file = dir.join(format!("{name}.rs")); + let nested = dir.join(&name).join("mod.rs"); + let path = if file.is_file() { file } else { nested }; + assert!( + path.is_file(), + "module `{name}` under {} has no source file", + dir.display() + ); + visit_file(&path, output); } } + fn visit_file(path: &Path, output: &mut Vec) { + let source = fs::read_to_string(path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + let syntax = syn::parse_file(&source) + .unwrap_or_else(|error| panic!("failed to parse {}: {error}", path.display())); + let parent = path.parent().expect("source file has no parent"); + let dir = match path.file_stem().and_then(|stem| stem.to_str()) { + Some("lib" | "mod") => parent.to_path_buf(), + Some(stem) => parent.join(stem), + None => panic!("source file {} has no stem", path.display()), + }; + output.push(path.to_path_buf()); + declared_modules(&syntax.items, &dir, output); + } + let mut output = Vec::new(); - collect(root, &mut output); + visit_file(&src.join("lib.rs"), &mut output); + output.sort(); output } fn check_inventory(path: &Path, label: &str, actual: &BTreeSet) { if env::var_os("JOLT_FS_BLESS").is_some() { let body = format!( - "# Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \\\n# --test fs_obligations --features fs-audit`.\n# Review changes; this inventories identities, not transcript values.\n{}\n", + "# Generated by `JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \\\n# --test fs_obligations`.\n# Review changes; this inventories identities, not transcript values.\n{}\n", actual.iter().cloned().collect::>().join("\n") ); fs::create_dir_all(path.parent().expect("inventory path has no parent")) @@ -234,64 +360,155 @@ fn check_inventory(path: &Path, label: &str, actual: &BTreeSet) { let removed = expected.difference(actual).collect::>(); assert!( added.is_empty() && removed.is_empty(), - "{label} changed.\nUnclassified: {added:#?}\nNo longer present: {removed:#?}\n\ - Run the blessed command and review the inventory diff." + "Fiat-Shamir {label} changed.\nUnclassified: {added:#?}\nNo longer present: {removed:#?}\n\ + Rerun with JOLT_FS_BLESS=1 and review the inventory diff." ); } -struct InventoryVisitor { +/// Names visible in one item scope that denote a transcript. +#[derive(Clone, Default)] +struct TranscriptScope { + /// Generic parameters bounded by `Channel` or a `*Transcript*` trait. + generics: BTreeSet, + /// Bindings typed or constructed as a transcript. + bindings: BTreeSet, + /// `self` is itself a transcript. + self_is_transcript: bool, +} + +struct InventoryVisitor<'a> { file_id: String, context: Vec, - /// One counter per context, shared by absorb and challenge records, so - /// an identity encodes its position in the combined absorb/challenge - /// sequence. Reordering an absorb against a squeeze — the canonical - /// weak-FS bug — renumbers both sites and trips the inventory even when - /// each per-kind subsequence is unchanged. + scopes: Vec, + /// One counter per context, shared by absorb, message, challenge, and + /// delegation records, so an identity encodes its position in the + /// function's combined transcript sequence. Reordering a message against + /// a squeeze — the canonical weak-FS bug — renumbers both sites and trips + /// the inventory even when each per-kind subsequence is unchanged. call_ordinals: BTreeMap, - absorb_sites: BTreeSet, - challenge_sites: BTreeSet, - scope_sites: BTreeSet, - source_fields: BTreeSet, + census: &'a mut Census, } -impl InventoryVisitor { - fn new(file_id: String) -> Self { +impl<'a> InventoryVisitor<'a> { + fn new(file_id: String, census: &'a mut Census) -> Self { Self { file_id, context: Vec::new(), + scopes: vec![TranscriptScope::default()], call_ordinals: BTreeMap::new(), - absorb_sites: BTreeSet::new(), - challenge_sites: BTreeSet::new(), - scope_sites: BTreeSet::new(), - source_fields: BTreeSet::new(), + census, } } - fn with_context(&mut self, name: String, visit: impl FnOnce(&mut Self)) { + fn scope(&mut self) -> &mut TranscriptScope { + self.scopes.last_mut().expect("scope stack is never empty") + } + + fn with_context( + &mut self, + name: String, + scope: TranscriptScope, + visit: impl FnOnce(&mut Self), + ) { self.context.push(name); + self.scopes.push(scope); visit(self); + let _ = self.scopes.pop(); let _ = self.context.pop(); } - fn record_absorb(&mut self, kind: &str, expression: &impl ToTokens) { - let context = self.context(); - let ordinal = self.call_ordinals.entry(context.clone()).or_default(); - let expression = expression.to_token_stream().to_string().replace(' ', ""); - let _ = self.absorb_sites.insert(format!( - "{}::{}::{}#{}::{expression}", - self.file_id, context, kind, *ordinal - )); - *ordinal += 1; + /// The scope for a new item: inherits enclosing generics and `self`, not + /// bindings, and adds `generics`' transcript-bounded parameters. + fn child_scope(&self, generics: &Generics) -> TranscriptScope { + let parent = self.scopes.last().expect("scope stack is never empty"); + let mut scope = TranscriptScope { + generics: parent.generics.clone(), + bindings: BTreeSet::new(), + self_is_transcript: parent.self_is_transcript, + }; + for parameter in generics.type_params() { + if names_transcript_type(&tokens(¶meter.bounds)) { + let _ = scope.generics.insert(parameter.ident.to_string()); + } + } + for predicate in generics.where_clause.iter().flat_map(|w| &w.predicates) { + if let WherePredicate::Type(predicate) = predicate { + if names_transcript_type(&tokens(&predicate.bounds)) { + let _ = scope.generics.insert(tokens(&predicate.bounded_ty)); + } + } + } + scope + } + + fn fn_scope(&self, signature: &Signature) -> TranscriptScope { + let mut scope = self.child_scope(&signature.generics); + for input in &signature.inputs { + if let FnArg::Typed(argument) = input { + if is_transcript_type(&scope, &tokens(&argument.ty)) { + if let Some(name) = binding_name(&argument.pat) { + let _ = scope.bindings.insert(name); + } + } + } + } + scope } - fn record_challenge(&mut self, kind: &str) { + fn is_transcript_expr(&self, expression: &str) -> bool { + let expression = expression + .trim_start_matches(['&', '*']) + .trim_start_matches("mut"); + let is_place = expression + .chars() + .all(|character| character.is_alphanumeric() || matches!(character, '_' | '.')); + if !is_place { + return false; + } + let lowercase = expression.to_ascii_lowercase(); + if lowercase.contains("transcript") || lowercase.contains("channel") { + return true; + } + let root = expression.split('.').next().unwrap_or_default(); + let scope = self.scopes.last().expect("scope stack is never empty"); + scope.bindings.contains(root) || (expression == "self" && scope.self_is_transcript) + } + + fn next_ordinal(&mut self) -> (String, usize) { let context = self.context(); let ordinal = self.call_ordinals.entry(context.clone()).or_default(); - let _ = self.challenge_sites.insert(format!( - "{}::{}::{}#{}", - self.file_id, context, kind, *ordinal - )); + let current = *ordinal; *ordinal += 1; + (context, current) + } + + fn record_with_expression(&mut self, kind: Kind, name: &str, expression: &impl ToTokens) { + let (context, ordinal) = self.next_ordinal(); + let entry = format!( + "{}::{context}::{name}#{ordinal}::{}", + self.file_id, + tokens(expression) + ); + let _ = match kind { + Kind::Absorb => self.census.absorb.insert(entry), + Kind::Message => self.census.message.insert(entry), + }; + } + + fn record_challenge(&mut self, name: &str) { + let (context, ordinal) = self.next_ordinal(); + let _ = self + .census + .challenge + .insert(format!("{}::{context}::{name}#{ordinal}", self.file_id)); + } + + fn record_delegation(&mut self, callee: &str) { + let (context, ordinal) = self.next_ordinal(); + let _ = self + .census + .delegation + .insert(format!("{}::{context}::{callee}#{ordinal}", self.file_id)); } fn context(&self) -> String { @@ -304,75 +521,67 @@ impl InventoryVisitor { fn record_struct(&mut self, item: &ItemStruct) { if has_derive(&item.attrs, "SumcheckBatch") { - let _ = self.challenge_sites.insert(format!( + let _ = self.census.challenge.insert(format!( "{}::{}::::batching_coefficient[*]", self.file_id, item.ident )); } - if !is_source_type(&item.ident.to_string()) { + if !SOURCE_TYPES.contains(&item.ident.to_string().as_str()) { return; } for (index, field) in item.fields.iter().enumerate() { - let field = field + let name = field .ident .as_ref() .map_or_else(|| index.to_string(), ToString::to_string); - let _ = self - .source_fields - .insert(format!("{}::{}.{}", self.file_id, item.ident, field)); + let _ = self.census.source.insert(format!( + "{}::{}.{name}{}", + self.file_id, + item.ident, + cfg_suffix(&field.attrs) + )); } } fn record_enum(&mut self, item: &ItemEnum) { - if !is_source_type(&item.ident.to_string()) { + if !SOURCE_TYPES.contains(&item.ident.to_string().as_str()) { return; } for variant in &item.variants { + let variant_id = format!("{}::{}::{}", self.file_id, item.ident, variant.ident); if variant.fields.is_empty() { - let _ = self.source_fields.insert(format!( - "{}::{}::{}", - self.file_id, item.ident, variant.ident - )); + let _ = self + .census + .source + .insert(format!("{variant_id}{}", cfg_suffix(&variant.attrs))); } for (index, field) in variant.fields.iter().enumerate() { - let field = field + let name = field .ident .as_ref() .map_or_else(|| index.to_string(), ToString::to_string); - let _ = self.source_fields.insert(format!( - "{}::{}::{}.{}", - self.file_id, item.ident, variant.ident, field - )); + let _ = self + .census + .source + .insert(format!("{variant_id}.{name}{}", cfg_suffix(&field.attrs))); } } } } -impl<'ast> Visit<'ast> for InventoryVisitor { +#[derive(Clone, Copy)] +enum Kind { + Absorb, + Message, +} + +impl<'ast> Visit<'ast> for InventoryVisitor<'_> { fn visit_item_fn(&mut self, item: &'ast ItemFn) { - if cfg_test(&item.attrs) || prover_only_name(&item.sig.ident.to_string()) { + if cfg_test(&item.attrs) { return; } - for attribute in &item.attrs { - if attribute - .path() - .segments - .last() - .is_some_and(|segment| segment.ident == "fs_scope") - { - let _ = self.scope_sites.insert(format!( - "{}::{}::{}", - self.file_id, - item.sig.ident, - attribute - .meta - .to_token_stream() - .to_string() - .replace(' ', "") - )); - } - } - self.with_context(item.sig.ident.to_string(), |visitor| { + let scope = self.fn_scope(&item.sig); + self.with_context(item.sig.ident.to_string(), scope, |visitor| { visit::visit_item_fn(visitor, item); }); } @@ -381,24 +590,43 @@ impl<'ast> Visit<'ast> for InventoryVisitor { if cfg_test(&item.attrs) { return; } - let name = item.self_ty.to_token_stream().to_string().replace(' ', ""); - self.with_context(name, |visitor| visit::visit_item_impl(visitor, item)); + let name = tokens(&item.self_ty); + let mut scope = self.child_scope(&item.generics); + let implements_channel = item + .trait_ + .as_ref() + .and_then(|(path, ..)| path.segments.last()) + .is_some_and(|segment| segment.ident == "Channel"); + scope.self_is_transcript = implements_channel || name.contains("Transcript"); + self.with_context(name, scope, |visitor| visit::visit_item_impl(visitor, item)); } fn visit_impl_item_fn(&mut self, item: &'ast ImplItemFn) { - if cfg_test(&item.attrs) || prover_only_name(&item.sig.ident.to_string()) { + if cfg_test(&item.attrs) { return; } - self.with_context(item.sig.ident.to_string(), |visitor| { + let scope = self.fn_scope(&item.sig); + self.with_context(item.sig.ident.to_string(), scope, |visitor| { visit::visit_impl_item_fn(visitor, item); }); } + fn visit_trait_item_fn(&mut self, item: &'ast TraitItemFn) { + if cfg_test(&item.attrs) { + return; + } + let scope = self.fn_scope(&item.sig); + self.with_context(item.sig.ident.to_string(), scope, |visitor| { + visit::visit_trait_item_fn(visitor, item); + }); + } + fn visit_item_mod(&mut self, item: &'ast ItemMod) { if cfg_test(&item.attrs) { return; } - self.with_context(item.ident.to_string(), |visitor| { + let scope = TranscriptScope::default(); + self.with_context(item.ident.to_string(), scope, |visitor| { visit::visit_item_mod(visitor, item); }); } @@ -407,102 +635,126 @@ impl<'ast> Visit<'ast> for InventoryVisitor { if cfg_test(&item.attrs) { return; } - self.with_context(item.ident.to_string(), |visitor| { + let mut scope = self.child_scope(&item.generics); + scope.self_is_transcript = + item.ident == "Channel" || item.ident.to_string().contains("Transcript"); + self.with_context(item.ident.to_string(), scope, |visitor| { visit::visit_item_trait(visitor, item); }); } + fn visit_item_const(&mut self, item: &'ast ItemConst) { + if !cfg_test(&item.attrs) && tokens(&item.ty) == "SiteId" { + let _ = self.census.site.insert(format!( + "{}::const {}={}", + self.file_id, + item.ident, + tokens(&item.expr) + )); + } + visit::visit_item_const(self, item); + } + + fn visit_local(&mut self, local: &'ast Local) { + let (pattern, ty) = match &local.pat { + Pat::Type(typed) => (&*typed.pat, Some(tokens(&typed.ty))), + pattern => (pattern, None), + }; + let constructs_transcript = local + .init + .as_ref() + .is_some_and(|init| tokens(&init.expr).contains("Transcript::new")); + let typed_transcript = ty.is_some_and(|ty| { + let scope = self.scopes.last().expect("scope stack is never empty"); + is_transcript_type(scope, &ty) + }); + if constructs_transcript || typed_transcript { + if let Some(name) = binding_name(pattern) { + let _ = self.scope().bindings.insert(name); + } + } + visit::visit_local(self, local); + } + + fn visit_expr_closure(&mut self, closure: &'ast ExprClosure) { + for input in &closure.inputs { + if let Pat::Type(typed) = input { + let scope = self.scopes.last().expect("scope stack is never empty"); + if is_transcript_type(scope, &tokens(&typed.ty)) { + if let Some(name) = binding_name(&typed.pat) { + let _ = self.scope().bindings.insert(name); + } + } + } + } + visit::visit_expr_closure(self, closure); + } + fn visit_expr_method_call(&mut self, expression: &'ast ExprMethodCall) { let method = expression.method.to_string(); - if is_challenge_call(&method) { + let on_transcript = self.is_transcript_expr(&tokens(&expression.receiver)); + if PCS_ABSORBS.contains(&method.as_str()) { + self.record_with_expression(Kind::Absorb, &method, expression); + } else if PCS_MESSAGES.contains(&method.as_str()) { + self.record_with_expression(Kind::Message, &method, expression); + } else if on_transcript && ABSORB_METHODS.contains(&method.as_str()) { + self.record_with_expression(Kind::Absorb, &method, expression); + } else if on_transcript && MESSAGE_METHODS.contains(&method.as_str()) { + self.record_with_expression(Kind::Message, &method, expression); + } else if on_transcript && CHALLENGE_METHODS.contains(&method.as_str()) { self.record_challenge(&method); - } - let receiver = expression - .receiver - .to_token_stream() - .to_string() - .replace(' ', ""); - let arguments = expression - .args - .to_token_stream() - .to_string() - .replace(' ', ""); - let receives_transcript = arguments.to_ascii_lowercase().contains("transcript"); - let is_named_absorb = - method.starts_with("absorb") || (method.starts_with("bind_") && receives_transcript); - // A bare `self.append(...)` is a transcript absorb only when `self` - // is a transcript: inside the `jolt-transcript` package or inside a - // `*Transcript*` impl/trait scope. Without the scope requirement, - // unrelated builder methods (e.g. `BlindFoldStatement::build`'s - // `self.append(builder, ...)`) enroll as false positives. - let self_is_transcript = self.file_id.starts_with("jolt-transcript::") - || self - .context + } else if on_transcript && method == "site" { + let _ = self.census.site.insert(format!( + "{}::{}::site({})", + self.file_id, + self.context(), + tokens(&expression.args) + )); + } else if on_transcript + || expression + .args .iter() - .any(|scope| scope.contains("Transcript")); - let is_transcript_method = is_absorb_method(&method) - && (method == "append_to_transcript" - || receiver.to_ascii_lowercase().contains("transcript") - || (receiver == "self" && self_is_transcript)); - if is_named_absorb || is_transcript_method { - self.record_absorb(&method, expression); + .any(|argument| self.is_transcript_expr(&tokens(argument))) + { + self.record_delegation(&format!(".{method}")); } visit::visit_expr_method_call(self, expression); } fn visit_expr_call(&mut self, expression: &'ast ExprCall) { - let function = expression - .func - .to_token_stream() - .to_string() - .replace(' ', ""); - let name = function.rsplit("::").next().unwrap_or(&function); - let arguments = expression + let function = tokens(&expression.func); + let name = function + .rsplit("::") + .find(|segment| !segment.starts_with('<')) + .unwrap_or(&function) + .to_owned(); + if PCS_ABSORBS.contains(&name.as_str()) { + self.record_with_expression(Kind::Absorb, &name, expression); + } else if PCS_MESSAGES.contains(&name.as_str()) { + self.record_with_expression(Kind::Message, &name, expression); + } else if expression .args - .to_token_stream() - .to_string() - .replace(' ', ""); - if name == "append_to_transcript" - || name.starts_with("absorb") - || ((name.starts_with("append_") || name.starts_with("bind_")) - && arguments.to_ascii_lowercase().contains("transcript")) + .iter() + .any(|argument| self.is_transcript_expr(&tokens(argument))) { - self.record_absorb(name, expression); + self.record_delegation(&function); } visit::visit_expr_call(self, expression); } + /// Expression-list macro bodies (`vec!`, `assert_eq!`, `format!`) are + /// visited as expressions; other bodies are opaque to the census. fn visit_macro(&mut self, invocation: &'ast Macro) { - if invocation - .path - .segments - .last() - .is_some_and(|segment| segment.ident == "fs_scope_guard") + if let Ok(arguments) = + invocation.parse_body_with(Punctuated::::parse_terminated) { - let context = if self.context.is_empty() { - "".to_owned() - } else { - self.context.join("::") - }; - let _ = self.scope_sites.insert(format!( - "{}::{}::fs_scope_guard({})", - self.file_id, - context, - invocation.tokens.to_string().replace(' ', "") - )); + for argument in &arguments { + self.visit_expr(argument); + } } visit::visit_macro(self, invocation); } - fn visit_trait_item_fn(&mut self, item: &'ast TraitItemFn) { - if cfg_test(&item.attrs) || prover_only_name(&item.sig.ident.to_string()) { - return; - } - self.with_context(item.sig.ident.to_string(), |visitor| { - visit::visit_trait_item_fn(visitor, item); - }); - } - fn visit_item_struct(&mut self, item: &'ast ItemStruct) { if !cfg_test(&item.attrs) { self.record_struct(item); @@ -518,61 +770,57 @@ impl<'ast> Visit<'ast> for InventoryVisitor { } } +fn package_id(package: &Value) -> &str { + package["id"].as_str().expect("package id is missing") +} + +fn tokens(node: &impl ToTokens) -> String { + node.to_token_stream().to_string().replace(' ', "") +} + +fn names_transcript_type(bounds: &str) -> bool { + bounds.contains("Channel") || bounds.contains("Transcript") +} + +fn is_transcript_type(scope: &TranscriptScope, ty: &str) -> bool { + names_transcript_type(ty) + || ty + .split(|character: char| !character.is_alphanumeric() && character != '_') + .any(|token| scope.generics.contains(token)) +} + +fn binding_name(pattern: &Pat) -> Option { + match pattern { + Pat::Ident(binding) => Some(binding.ident.to_string()), + Pat::Type(typed) => binding_name(&typed.pat), + _ => None, + } +} + fn cfg_test(attributes: &[Attribute]) -> bool { attributes.iter().any(|attribute| { - attribute.path().is_ident("cfg") - && attribute - .meta - .to_token_stream() - .to_string() - .contains("test") + attribute.path().is_ident("cfg") && tokens(&attribute.meta).contains("test") }) } +/// ` @cfg(...)` for a cfg-gated field, so per-build proof shapes stay distinct. +fn cfg_suffix(attributes: &[Attribute]) -> String { + let mut suffix = String::new(); + for attribute in attributes + .iter() + .filter(|attribute| attribute.path().is_ident("cfg")) + { + suffix.push_str(" @"); + suffix.push_str(&tokens(&attribute.meta)); + } + suffix +} + fn has_derive(attributes: &[Attribute], derive: &str) -> bool { attributes.iter().any(|attribute| { attribute.path().is_ident("derive") - && attribute - .meta - .to_token_stream() - .to_string() + && tokens(&attribute.meta) .split(|character: char| !character.is_alphanumeric() && character != '_') .any(|name| name == derive) }) } - -fn prover_only_name(name: &str) -> bool { - name.starts_with("prove") || name.starts_with("commit_round") -} - -fn is_challenge_call(name: &str) -> bool { - matches!( - name, - "challenge" | "challenge_scalar" | "challenge_vector" | "challenge_scalar_powers" - ) -} - -fn is_absorb_method(name: &str) -> bool { - matches!(name, "append" | "append_bytes" | "append_to_transcript") -} - -fn is_source_type(name: &str) -> bool { - matches!( - name, - "AkitaJointOpeningProof" - | "ClearProofClaims" - | "CommittedProgramPreprocessing" - | "JoltCommitments" - | "JoltDevice" - | "JoltProgramPreprocessing" - | "JoltProof" - | "JoltProofClaims" - | "JoltProtocolConfig" - | "JoltStageProofs" - | "JoltVerifierPreprocessing" - | "MemoryLayout" - | "ProgramMetadata" - | "ProgramPreprocessing" - | "ZkConfig" - ) -} diff --git a/scripts/ci/fs-soundness.sh b/scripts/ci/fs-soundness.sh index 3b9fe5f04e..e5e43570e8 100755 --- a/scripts/ci/fs-soundness.sh +++ b/scripts/ci/fs-soundness.sh @@ -4,7 +4,6 @@ set -euo pipefail cargo nextest run \ -p jolt-verifier \ --test fs_obligations \ - --features fs-audit \ --cargo-quiet cargo nextest run \ @@ -30,7 +29,7 @@ trap 'rm -f "$unsupported_log"' EXIT if cargo check \ -p jolt-verifier \ --lib \ - --features akita,fs-audit,zk \ + --features akita,zk \ --quiet \ 2>"$unsupported_log"; then echo "Akita+ZK now builds; add it to the Fiat-Shamir fixture matrix." >&2 From f4c7833da6dfcfd6435e64f84a7fe670642d5f0c Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 15:45:29 -0400 Subject: [PATCH 26/62] test(verifier): port the verifier unit tests to the NARG transcript 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. --- crates/jolt-verifier/src/stages/relations.rs | 240 +++----- .../src/stages/stage1/outer_remainder.rs | 8 +- .../src/stages/stage2/outputs.rs | 56 +- .../stages/stage2/ram_read_write_checking.rs | 26 - .../src/stages/stage4/outputs.rs | 68 ++- .../src/stages/stage5/outputs.rs | 30 +- .../src/stages/stage6a/bytecode_read_raf.rs | 45 +- .../src/stages/stage6a/verify.rs | 115 ++-- .../jolt-verifier/src/stages/stage6b/batch.rs | 47 +- .../src/stages/stage6b/booleanity.rs | 30 - .../stage6b/instruction_ra_virtualization.rs | 35 -- .../src/stages/stage6b/verify.rs | 1 - .../jolt-verifier/src/stages/stage8/verify.rs | 44 +- crates/jolt-verifier/src/verifier.rs | 572 ++++-------------- 14 files changed, 357 insertions(+), 960 deletions(-) diff --git a/crates/jolt-verifier/src/stages/relations.rs b/crates/jolt-verifier/src/stages/relations.rs index e947be33ae..75d884dc36 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -608,115 +608,41 @@ pub fn stage_claim_failed(stage: impl Debug, reason: impl ToString) -> VerifierE } } -/// Test-only transcript double for asserting [`ConcreteSumcheck::draw_challenges`] -/// reproduces a stage's inline Fiat-Shamir draw exactly. -/// -/// Unlike the `append_openings` recorder, challenge *squeezes* -/// (`challenge`/`challenge_scalar`/`challenge_scalar_powers`) append no bytes, so a -/// byte-chunk recorder cannot observe them. This double instead records an ordered -/// event log that distinguishes a squeeze from a byte-append (e.g. the -/// `ram_val_check` gamma domain separator), and returns a *distinct sequential* -/// scalar from each squeeze so a relation's stored challenge can be checked against -/// the squeeze that produced it. +/// Test-only transcripts for pinning a production Fiat-Shamir draw against +/// its documented sequence. Every transcript starts from the same protocol and +/// session, so two of them yield equal challenges and equal sponge fingerprints +/// exactly when they performed the same operations. #[cfg(test)] -#[expect( - clippy::arithmetic_side_effects, - reason = "tests use plain arithmetic on fixture data" -)] -pub(crate) mod draw_recording { - use jolt_field::{Fr, Ring}; - use jolt_transcript::Transcript; - - /// One observable transcript operation a `draw_challenges` performs. - #[derive(Clone, Debug, PartialEq, Eq)] - pub(crate) enum DrawEvent { - /// A challenge squeeze (`challenge`/`challenge_scalar`/the single squeeze - /// inside `challenge_scalar_powers`). Carries the 1-based squeeze index so - /// the value a relation kept can be matched to its squeeze. - Squeeze(u64), - /// A raw byte append (a domain separator preceding a squeeze). - Append(Vec), - } - - /// A `Transcript` that logs every squeeze and byte-append in order. Each - /// squeeze returns `Fr(index)` for the 1-based squeeze counter, so the powers - /// `challenge_scalar_powers` derives are distinct and a stored `gamma` can be - /// asserted to equal the squeezed value. - #[derive(Clone, Default)] - pub(crate) struct DrawRecordingTranscript { - pub(crate) events: Vec, - squeezes: u64, - } +pub(crate) mod test_transcript { + use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript}; - impl Transcript for DrawRecordingTranscript { - type Challenge = Fr; + pub(crate) type TestTranscript = ProverTranscript; - fn new(_label: &'static [u8]) -> Self { - Self::default() - } - - fn append_bytes(&mut self, bytes: &[u8]) { - self.events.push(DrawEvent::Append(bytes.to_vec())); - } - - fn challenge(&mut self) -> Self::Challenge { - self.squeezes += 1; - self.events.push(DrawEvent::Squeeze(self.squeezes)); - Fr::from_u64(self.squeezes) - } - - fn state(&self) -> [u8; 32] { - [0u8; 32] - } - } - - /// Run `draw` against a fresh recorder, returning its ordered event log and the - /// draw's result. A `draw_challenges` and a hand-written replica of the inline - /// draw, each passed through this, are directly comparable: equal event logs - /// prove the same squeeze/append sequence, and the recorder's distinct - /// sequential squeeze values let the returned challenge be checked against the - /// replica's captured value. - pub(crate) fn record( - draw: impl FnOnce(&mut DrawRecordingTranscript) -> R, - ) -> (Vec, R) { - let mut transcript = DrawRecordingTranscript::default(); - let result = draw(&mut transcript); - (transcript.events, result) - } -} - -/// The append-order recorder shared by the stage `append_output_claims` ordering -/// locks: unlike the challenge recorder, it observes only byte appends. -#[cfg(test)] -pub(crate) mod append_recording { - use jolt_field::{Fr, Ring}; - use jolt_transcript::Transcript; - - /// A minimal `Transcript` double that records each appended byte chunk, so - /// that append order can be compared without depending on the digest. - #[derive(Clone, Default)] - pub(crate) struct RecordingTranscript { - pub(crate) chunks: Vec>, + pub(crate) fn fresh() -> TestTranscript { + ProverTranscript::new( + &ProtocolId::new::("jolt-verifier/unit-tests"), + b"unit-tests", + ) } - impl Transcript for RecordingTranscript { - type Challenge = Fr; - - fn new(_label: &'static [u8]) -> Self { - Self::default() - } - - fn append_bytes(&mut self, bytes: &[u8]) { - self.chunks.push(bytes.to_vec()); - } - - fn challenge(&mut self) -> Self::Challenge { - Fr::from_u64(0) - } - - fn state(&self) -> [u8; 32] { - [0u8; 32] - } + /// Runs `production` and `documented` on identical fresh transcripts and + /// asserts they leave the sponge in the same state, which pins the number, + /// kind, and order of their operations. Returns both results so the caller + /// can compare the values `production` kept against the documented draws. + pub(crate) fn assert_same_draws( + production: impl FnOnce(&mut TestTranscript) -> A, + documented: impl FnOnce(&mut TestTranscript) -> B, + ) -> (A, B) { + let mut left = fresh(); + let produced = production(&mut left); + let mut right = fresh(); + let expected = documented(&mut right); + assert_eq!( + left.preview().squeeze::<32>(), + right.preview().squeeze::<32>(), + "production and documented draws diverge" + ); + (produced, expected) } } @@ -748,22 +674,6 @@ mod tests { JoltOpeningId::committed(polynomial, relation) } - use super::append_recording::RecordingTranscript; - - /// The chunk stream produced by appending `opening_values()` one-by-one is - /// the reference Fiat-Shamir order; `append_openings` must reproduce it. - fn assert_append_matches_values>(claims: &C) { - let mut via_append = RecordingTranscript::default(); - claims.append_openings(&mut via_append); - - let mut via_values = RecordingTranscript::default(); - for value in claims.opening_values() { - via_values.append_labeled(b"opening_claim", &value); - } - - assert_eq!(via_append.chunks, via_values.chunks); - } - #[derive(OutputClaims)] #[relation(InstructionReadRaf)] struct InstructionLeaf { @@ -792,7 +702,6 @@ mod tests { claims.canonical_order().len(), claims.opening_values().len() ); - assert_append_matches_values(&claims); } #[test] @@ -860,7 +769,6 @@ mod tests { )), Some(fr(9)), ); - assert_append_matches_values(&claims); } #[derive(OutputClaims)] @@ -901,7 +809,6 @@ mod tests { )), None, ); - assert_append_matches_values(&claims); } #[derive(OutputClaims)] @@ -930,10 +837,9 @@ mod tests { present.resolve_output(&committed(JoltCommittedPolynomial::RamInc, relation)), Some(fr(8)), ); - assert_append_matches_values(&present); // An absent optional opening drops out of the count, the value stream, - // the transcript appends, and id resolution. + // and id resolution. let absent = OptionalOutput { untrusted: None, ram_inc: fr(8), @@ -944,7 +850,6 @@ mod tests { absent.resolve_output(&JoltOpeningId::untrusted_advice(relation)), None, ); - assert_append_matches_values(&absent); } #[test] @@ -1361,7 +1266,7 @@ mod sumcheck_batch_derive_tests { // The opt-out fixture: `#[sumcheck_batch(no_opening_values)]` must still // generate the five aggregate structs but emit NO `opening_values` / - // `append_output_claims` on the source struct. The inherent `opening_values` + // `receive_output_claims` / `verify_clear` on the source struct. The inherent `opening_values` // below would collide with a generated one (the compiler rejects two inherent // methods of the same name), so this module compiling at all proves the // opt-out suppressed it. @@ -1444,7 +1349,7 @@ mod sumcheck_batch_derive_tests { #[cfg(test)] #[expect(clippy::unwrap_used)] mod begin_batch_tests { - use super::draw_recording::{record, DrawEvent}; + use super::test_transcript::{assert_same_draws, fresh}; use super::ConcreteSumcheck as _; use crate::stages::stage5::{InstructionReadRaf, RegistersValEvaluation}; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; @@ -1452,8 +1357,8 @@ mod begin_batch_tests { use jolt_claims::protocols::jolt::relations::instruction::InstructionReadRafInputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationInputClaims; use jolt_field::{Fr, JoltField, Ring}; - use jolt_sumcheck::{append_sumcheck_claim, BatchMember, ClearSumcheckRecorder}; - use jolt_transcript::Transcript; + use jolt_sumcheck::{BatchMember, ClearSumcheckRecorder}; + use jolt_transcript::Channel; #[derive(super::SumcheckBatch)] #[sumcheck_batch(crate = "crate")] @@ -1481,13 +1386,12 @@ mod begin_batch_tests { } } - /// `begin_batch` with a clear recorder must reproduce the exact head - /// Fiat-Shamir sequence `verify_clear` performed before the factoring — - /// per-member `input_claim` absorbed under `b"sumcheck_claim"` in - /// declaration order, then one coefficient squeeze per member — and pack - /// the prelude's engine and named views consistently. + /// `begin_batch` with a clear recorder absorbs every member's + /// `input_claim` as public values in declaration order, then draws one + /// uniform batching coefficient per member, and packs the prelude from + /// exactly those values. #[test] - fn begin_batch_matches_head_replica_and_packs_prelude() { + fn begin_batch_absorbs_input_claims_then_draws_coefficients() { let sumchecks = fixture(true); let inputs = HeadFixtureInputClaims:: { instruction_read_raf: instruction_inputs(), @@ -1495,17 +1399,10 @@ mod begin_batch_tests { registers_val: Fr::from_u64(7), }), }; - let (_, challenges) = record(|t| sumchecks.draw_challenges(t)); - let challenges = challenges.unwrap(); - - let (events, head) = record(|t| { - let mut recorder = ClearSumcheckRecorder::::new(); - sumchecks.begin_batch(&inputs, &challenges, &mut recorder, t) - }); - let (batch, coefficients) = head.unwrap(); + let challenges = sumchecks.draw_challenges(&mut fresh()).unwrap(); - // The replica head: input_claim is transcript-pure, so only the absorbs - // and coefficient squeezes are observable events. + // `input_claim` is transcript-pure, so the expected sums come from the + // members directly. let instruction_sum = sumchecks .instruction_read_raf .input_claim( @@ -1522,12 +1419,17 @@ mod begin_batch_tests { challenges.registers_val_evaluation.as_ref().unwrap(), ) .unwrap(); - let (replica_events, (instruction_coeff, registers_coeff)) = record(|t| { - append_sumcheck_claim(t, &instruction_sum); - append_sumcheck_claim(t, ®isters_sum); - (t.challenge_scalar(), t.challenge_scalar()) - }); - assert_eq!(events, replica_events); + let (head, (instruction_coeff, registers_coeff)) = assert_same_draws( + |t| { + let mut recorder = ClearSumcheckRecorder::::new(); + sumchecks.begin_batch(&inputs, &challenges, &mut recorder, t) + }, + |t| { + t.public_all(&[instruction_sum, registers_sum]); + (t.challenge::(), t.challenge::()) + }, + ); + let (batch, coefficients) = head.unwrap(); let instruction_rounds = sumchecks.instruction_read_raf.rounds(); let registers_rounds = sumchecks @@ -1563,7 +1465,7 @@ mod begin_batch_tests { assert_eq!(coefficients.registers_val_evaluation, Some(registers_coeff)); } - /// An absent `Option` member contributes no absorb, no coefficient squeeze, + /// An absent `Option` member contributes no absorb, no coefficient draw, /// and no batch entry. #[test] fn begin_batch_skips_absent_option_member() { @@ -1572,24 +1474,30 @@ mod begin_batch_tests { instruction_read_raf: instruction_inputs(), registers_val_evaluation: None, }; - let (_, challenges) = record(|t| sumchecks.draw_challenges(t)); - let challenges = challenges.unwrap(); + let challenges = sumchecks.draw_challenges(&mut fresh()).unwrap(); + let instruction_sum = sumchecks + .instruction_read_raf + .input_claim( + &inputs.instruction_read_raf, + &challenges.instruction_read_raf, + ) + .unwrap(); - let (events, head) = record(|t| { - let mut recorder = ClearSumcheckRecorder::::new(); - sumchecks.begin_batch(&inputs, &challenges, &mut recorder, t) - }); + let (head, instruction_coeff) = assert_same_draws( + |t| { + let mut recorder = ClearSumcheckRecorder::::new(); + sumchecks.begin_batch(&inputs, &challenges, &mut recorder, t) + }, + |t| { + t.public_all(&[instruction_sum]); + t.challenge::() + }, + ); let (batch, coefficients) = head.unwrap(); - assert_eq!( - events - .iter() - .filter(|event| matches!(event, DrawEvent::Squeeze(_))) - .count(), - 1, - ); assert_eq!(batch.members.len(), 1); assert_eq!(batch.max_num_vars, sumchecks.instruction_read_raf.rounds()); + assert_eq!(coefficients.instruction_read_raf, instruction_coeff); assert_eq!(coefficients.registers_val_evaluation, None); } } diff --git a/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs b/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs index 285ff99775..dbde673432 100644 --- a/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs +++ b/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs @@ -345,12 +345,12 @@ mod tests { use jolt_field::{Fr, Ring}; /// The produced `OuterRemainderOutputClaims` field (declaration) order is the - /// canonical `SPARTAN_OUTER_R1CS_INPUTS` order, so the generated absorb - /// (`append_output_claims`) reproduces the input order, - /// byte-identically. `canonical_order()` surfaces that field order as opening ids, + /// canonical `SPARTAN_OUTER_R1CS_INPUTS` order, so the generated opening + /// order (`opening_values`, the order the prover sends) reproduces the input + /// order. `canonical_order()` surfaces that field order as opening ids, /// which must line up one-for-one with the R1CS inputs. #[test] - fn append_order_matches_r1cs_input_order() { + fn opening_order_matches_r1cs_input_order() { let expected = SPARTAN_OUTER_R1CS_INPUTS .iter() .copied() diff --git a/crates/jolt-verifier/src/stages/stage2/outputs.rs b/crates/jolt-verifier/src/stages/stage2/outputs.rs index 4928e7fa4d..11d6ae2f02 100644 --- a/crates/jolt-verifier/src/stages/stage2/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage2/outputs.rs @@ -231,7 +231,7 @@ impl Stage2Output { )] mod tests { use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; + use crate::stages::relations::test_transcript::assert_same_draws; use crate::stages::relations::ConcreteSumcheck; use common::jolt_device::{JoltDevice, MemoryConfig}; #[cfg(feature = "field-inline")] @@ -243,7 +243,7 @@ mod tests { }; use jolt_field::{Fr, Ring}; use jolt_program::preprocess::PublicIoMemory; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; fn fr(value: u64) -> Fr { Fr::from_u64(value) @@ -287,38 +287,31 @@ mod tests { } } - /// Pins the batch's `draw_challenges` to the pre-port inline draw: the RAM read-write - /// gamma, the instruction claim-reduction gamma (each a single `challenge_scalar`), under - /// `field-inline` the field-inline claim-reduction gamma (the spec's draw slot: after the - /// instruction claim-reduction gamma, before the RAM output address challenges), then the - /// RAM output-check address reference point — one raw `challenge()` per RAM address - /// variable, via the last member's `draw_challenges` override (the other members draw - /// nothing). + /// The batch draws the RAM read-write gamma, the instruction claim-reduction + /// gamma, under `field-inline` the field-inline claim-reduction gamma (the + /// spec's draw slot: after the instruction claim-reduction gamma, before the + /// RAM output address challenges), each one uniform challenge, then the RAM + /// output-check address reference point — one small challenge per RAM + /// address variable, via the last member's `draw_challenges` override (the + /// other members draw nothing). #[test] - fn draw_challenges_matches_inline_draw_sequence() { + fn draw_challenges_follow_member_order() { let sumchecks = sumchecks(); let log_k = sumchecks.ram_output_check.read_write_dimensions().log_k(); #[cfg(not(feature = "field-inline"))] let gamma_draws = 2usize; #[cfg(feature = "field-inline")] let gamma_draws = 3usize; - let (inline_events, (inline_gammas, inline_output_address)) = record(|t| { - ( - (0..gamma_draws) - .map(|_| t.challenge_scalar()) - .collect::>(), - (0..log_k).map(|_| t.challenge()).collect::>(), - ) - }); - let (draw_events, challenges) = record(|t| sumchecks.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - assert_eq!( - draw_events, - (1..=(gamma_draws + log_k) as u64) - .map(DrawEvent::Squeeze) - .collect::>() + let (challenges, (gammas, output_address)) = assert_same_draws( + |t| sumchecks.draw_challenges(t).unwrap(), + |t| { + ( + (0..gamma_draws).map(|_| t.challenge()).collect::>(), + t.challenges_small::(log_k), + ) + }, ); + #[cfg(not(feature = "field-inline"))] let drawn_gammas = vec![ challenges.ram_read_write.gamma, @@ -330,19 +323,16 @@ mod tests { challenges.instruction_claim_reduction.gamma, challenges.field_registers_claim_reduction.gamma, ]; - assert_eq!(drawn_gammas, inline_gammas); - assert_eq!( - challenges.ram_output_check.output_address, - inline_output_address - ); + assert_eq!(drawn_gammas, gammas); + assert_eq!(challenges.ram_output_check.output_address, output_address); } /// A stage-2 batch output whose reduced instruction openings equal the /// product-remainder ones they alias (`lookup_output`, /// `left`/`right_instruction_input`). `validate_aliases` accepts it; the tests /// below perturb one alias each to assert rejection. The aliased cells carry - /// the product values; the absorb test overrides them with sentinels to prove - /// they are skipped. + /// the product values; the opening-order tests override them with sentinels + /// to prove they are skipped. #[cfg_attr( not(feature = "field-inline"), expect( diff --git a/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs b/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs index eb81370097..0e7752adaa 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_read_write_checking.rs @@ -121,29 +121,3 @@ impl ConcreteSumcheck for RamReadWriteChecking { } } } - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; - use jolt_field::Fr; - use jolt_transcript::Channel; - - // Representative of the 14 single-`challenge_scalar` relations that inherit the - // default `draw_challenges`: the inline `ram_read_write_gamma = challenge_scalar()` - // is one squeeze, and the default's one-`challenge_scalar`-per-field draw stores - // exactly that scalar. - #[test] - fn default_draw_challenges_matches_inline_ram_read_write_gamma() { - let relation = - RamReadWriteChecking::::new(ReadWriteDimensions::new(4, 3, 2, 1), 3, Vec::new()); - - let (inline_events, inline_gamma) = record(|t| t.challenge()); - let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - assert_eq!(draw_events, vec![DrawEvent::Squeeze(1)]); - assert_eq!(challenges.gamma, inline_gamma); - } -} diff --git a/crates/jolt-verifier/src/stages/stage4/outputs.rs b/crates/jolt-verifier/src/stages/stage4/outputs.rs index e11428b0f0..b76af63e8f 100644 --- a/crates/jolt-verifier/src/stages/stage4/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage4/outputs.rs @@ -244,7 +244,7 @@ impl Stage4Output { #[expect(clippy::unwrap_used)] mod tests { use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; + use crate::stages::relations::test_transcript::assert_same_draws; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldInlineConfig; use jolt_claims::protocols::jolt::geometry::dimensions::{ @@ -254,7 +254,7 @@ mod tests { use jolt_claims::protocols::jolt::relations::ram::RamValCheckOutputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersReadWriteOutputClaims; use jolt_field::{Fr, Ring}; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; fn fr(value: u64) -> Fr { Fr::from_u64(value) @@ -333,6 +333,23 @@ mod tests { assert_eq!(claims_with_advice(true).opening_values(), expected); } + /// The openings sent after the rounds exclude the staged advice and + /// program-image openings, which travel before the batch: the five register + /// openings, under `field-inline` the five field-inline openings, then + /// `ram_ra`/`ram_inc`. + #[test] + fn post_round_opening_values_omit_staged_openings() { + let expected: Vec = (3..=7) + .map(fr) + .chain(field_inline_splice()) + .chain([fr(8), fr(9)]) + .collect(); + assert_eq!( + sumchecks().post_round_opening_values(&claims_with_advice(true)), + expected + ); + } + fn sumchecks() -> Stage4Sumchecks { let log_t = 4usize; let ram_log_k = 3usize; @@ -355,45 +372,34 @@ mod tests { } } - /// Pins the batch's `draw_challenges` to the inline draw order: one `challenge_scalar` per - /// leading member — the registers gamma, under `field-inline` the field-register - /// read-write gamma (the spec's draw slot: after the registers gamma, before the RAM - /// value-check draw) — then the RAM value-check draw (its domain separator + gamma; that - /// draw's byte exactness is pinned by its own member test). The replica reuses the RAM - /// member's `draw_challenges` so this test pins the member ORDER. + /// The batch draws one uniform gamma per member in declaration order: the + /// registers gamma, under `field-inline` the field-register read-write gamma + /// (the spec's draw slot: after the registers gamma, before the RAM + /// value-check gamma), then the RAM value-check gamma. #[test] - fn draw_challenges_matches_inline_draw_sequence() { - use crate::stages::relations::ConcreteSumcheck as _; - + fn draw_challenges_follow_member_order() { let sumchecks = sumchecks(); #[cfg(not(feature = "field-inline"))] - let leading_gamma_draws = 1usize; + let gamma_draws = 2usize; #[cfg(feature = "field-inline")] - let leading_gamma_draws = 2usize; - let (inline_events, (inline_gammas, inline_ram_gamma)) = record(|t| { - let gammas = (0..leading_gamma_draws) - .map(|_| t.challenge_scalar()) - .collect::>(); - let ram = sumchecks.ram_val_check.draw_challenges(t).unwrap(); - (gammas, ram.gamma) - }); - let (draw_events, challenges) = record(|t| sumchecks.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - // The RAM value-check domain separator lands after the leading gammas. - assert!(matches!(draw_events.first(), Some(DrawEvent::Squeeze(1)))); - assert!(draw_events - .iter() - .any(|event| matches!(event, DrawEvent::Append(_)))); + let gamma_draws = 3usize; + let (challenges, gammas) = assert_same_draws( + |t| sumchecks.draw_challenges(t).unwrap(), + |t| (0..gamma_draws).map(|_| t.challenge()).collect::>(), + ); + #[cfg(not(feature = "field-inline"))] - let drawn_gammas = vec![challenges.registers_read_write.gamma]; + let drawn_gammas = vec![ + challenges.registers_read_write.gamma, + challenges.ram_val_check.gamma, + ]; #[cfg(feature = "field-inline")] let drawn_gammas = vec![ challenges.registers_read_write.gamma, challenges.field_registers_read_write.gamma, + challenges.ram_val_check.gamma, ]; - assert_eq!(drawn_gammas, inline_gammas); - assert_eq!(challenges.ram_val_check.gamma, inline_ram_gamma); + assert_eq!(drawn_gammas, gammas); } /// The generated `output_claim_count` sums the members' wire sets: the five register diff --git a/crates/jolt-verifier/src/stages/stage5/outputs.rs b/crates/jolt-verifier/src/stages/stage5/outputs.rs index d638a23d75..c06c64b4e5 100644 --- a/crates/jolt-verifier/src/stages/stage5/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage5/outputs.rs @@ -169,7 +169,7 @@ impl Stage5Output { #[expect(clippy::unwrap_used)] mod tests { use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; + use crate::stages::relations::test_transcript::assert_same_draws; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; @@ -178,7 +178,7 @@ mod tests { use jolt_claims::protocols::jolt::relations::ram::RamRaClaimReductionOutputClaims; use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationOutputClaims; use jolt_field::{Fr, Ring}; - use jolt_transcript::Transcript; + use jolt_transcript::Channel; fn fr(value: u64) -> Fr { Fr::from_u64(value) @@ -219,7 +219,7 @@ mod tests { } } - /// Locks the stage-5 Fiat-Shamir append order against silent drift: the instruction + /// Locks the stage-5 opening order against silent drift: the instruction /// read-RAF openings, then the RAM-RA reduced opening, then the register value-evaluation /// openings, under `field-inline` the field-inline value-evaluation openings last (the /// spec's committed row order: `FieldRdInc`, `FieldRdWa`), each member single-sourcing its @@ -234,28 +234,24 @@ mod tests { assert_eq!(sumchecks().opening_values(&claims()), expected); } - /// Pins the batch's `draw_challenges` to the inline draw: the instruction gamma, then the - /// RAM-RA gamma. The register value-evaluation member draws nothing, and so does the - /// `field-inline` field-register value-evaluation member (`NoChallenges`) — composing it - /// changes no stage-5 draw. + /// The batch draws the instruction gamma, then the RAM-RA gamma, each one + /// uniform challenge. The register value-evaluation member draws nothing, + /// and so does the `field-inline` field-register value-evaluation member + /// (`NoChallenges`) — composing it changes no stage-5 draw. #[test] - fn draw_challenges_matches_inline_draw_sequence() { + fn draw_challenges_follow_member_order() { let sumchecks = sumchecks(); - let (inline_events, inline_gammas) = - record(|t| (0..2).map(|_| t.challenge_scalar()).collect::>()); - let (draw_events, challenges) = record(|t| sumchecks.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - assert_eq!( - draw_events, - vec![DrawEvent::Squeeze(1), DrawEvent::Squeeze(2)] + let (challenges, gammas) = assert_same_draws( + |t| sumchecks.draw_challenges(t).unwrap(), + |t| (0..2).map(|_| t.challenge()).collect::>(), ); + assert_eq!( vec![ challenges.instruction_read_raf.gamma, challenges.ram_ra_claim_reduction.gamma, ], - inline_gammas + gammas ); } diff --git a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs index 46319a1643..3d5837a4cb 100644 --- a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs @@ -399,28 +399,19 @@ impl ConcreteSumcheck for BytecodeReadRafAddressPhase { } #[cfg(test)] -#[expect( - clippy::unwrap_used, - clippy::indexing_slicing, - reason = "test code indexes its own fixed-size fixtures" -)] +#[expect(clippy::unwrap_used)] mod tests { use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; + use crate::stages::relations::test_transcript::assert_same_draws; use jolt_field::Fr; - use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; - use jolt_riscv::NUM_CIRCUIT_FLAGS; use jolt_transcript::Channel; // The address phase has the only multi-field `Challenges` (gamma + five stage - // gammas), so it exercises that the default draws one `challenge_scalar` per - // field in declaration order. Each inline draw is a `challenge_scalar_powers(..)` - // whose single squeeze's degree-1 power equals that squeezed scalar, so the - // default's six `challenge_scalar` squeezes reproduce the inline byte stream - // (six squeezes) and the six stored values. The cycle and committed variants are + // gammas), so it exercises that the default draws one uniform challenge per + // field in declaration order. The cycle and committed variants are // single-field and use the same default path. #[test] - fn default_draw_challenges_matches_inline_bytecode_address_gammas() { + fn default_draw_challenges_draws_one_uniform_gamma_per_field() { let relation = BytecodeReadRafAddressPhase::::new( BytecodeReadRafDimensions::new(3, 4, 2), false, @@ -433,27 +424,11 @@ mod tests { 0, ); - // Inline: six `challenge_scalar_powers(..)`, each contributing its - // degree-1 power. - let (inline_events, inline_gammas) = record(|t| { - [ - t.challenge_powers(8)[1], - t.challenge_powers(2 + NUM_CIRCUIT_FLAGS)[1], - t.challenge_powers(4)[1], - t.challenge_powers(9)[1], - t.challenge_powers(3)[1], - t.challenge_powers(2 + LookupTableKind::::COUNT)[1], - ] - }); - let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); - - // Six squeezes in the same order, byte-for-byte. - assert_eq!(draw_events, inline_events); - assert_eq!( - draw_events, - (1..=6).map(DrawEvent::Squeeze).collect::>() + let (challenges, gammas) = assert_same_draws( + |t| relation.draw_challenges(t).unwrap(), + |t| std::array::from_fn::(|_| t.challenge()), ); - // Each field stores the corresponding inline degree-1 power. + assert_eq!( [ challenges.gamma, @@ -463,7 +438,7 @@ mod tests { challenges.stage4_gamma, challenges.stage5_gamma, ], - inline_gammas, + gammas, ); } } diff --git a/crates/jolt-verifier/src/stages/stage6a/verify.rs b/crates/jolt-verifier/src/stages/stage6a/verify.rs index afd1fa40a4..252fe39258 100644 --- a/crates/jolt-verifier/src/stages/stage6a/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6a/verify.rs @@ -179,8 +179,7 @@ mod tests { }; use super::super::outputs::Stage6aOutputClaims; use super::*; - use crate::stages::relations::append_recording::RecordingTranscript; - use crate::stages::relations::draw_recording::{record, DrawEvent}; + use crate::stages::relations::test_transcript::{assert_same_draws, fresh}; use jolt_claims::protocols::jolt::geometry::booleanity::BooleanityDimensions; use jolt_claims::protocols::jolt::geometry::bytecode::BytecodeReadRafDimensions; use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; @@ -231,33 +230,26 @@ mod tests { } } - /// Pins the batch's `draw_challenges` to the retired hand pre-batch draw: - /// the bytecode member's six gammas, then the booleanity member's override — - /// the reference-address pad draw (the reversed stage-5 instruction address - /// is narrower than the committed chunk width here) and the booleanity - /// gamma. The reference vectors are pure computation (reversal, pad slot) - /// off the stage-5 point the relation carries. + /// The batch draws the bytecode member's six uniform gammas, then the + /// booleanity member's reference-address pad (the reversed stage-5 + /// instruction address is narrower than the committed chunk width here, so + /// one small challenge fills the missing slot) and its small gamma. #[test] #[expect(clippy::unwrap_used)] - fn draw_challenges_matches_inline_draw_sequence() { + fn draw_challenges_pads_narrow_reference_address() { let address = vec![fr(11)]; let cycle = vec![fr(21), fr(22), fr(23), fr(24)]; let sumchecks = sumchecks(address.clone(), cycle.clone()); - let (inline_events, (inline_gammas, inline_reference_address, inline_gamma)) = - record(|t| { - let gammas: Vec = (0..6).map(|_| t.challenge_scalar()).collect(); - let mut reference_address: Vec = address.iter().rev().copied().collect(); - reference_address.extend(t.challenge_vector(1)); - (gammas, reference_address, t.challenge()) - }); - let (draw_events, challenges) = record(|t| sumchecks.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - assert_eq!( - draw_events, - (1..=8u64).map(DrawEvent::Squeeze).collect::>() + let (challenges, (gammas, pad, gamma)) = assert_same_draws( + |t| sumchecks.draw_challenges(t).unwrap(), + |t| { + let gammas: Vec = (0..6).map(|_| t.challenge()).collect(); + let pad: Vec = t.challenges_small(1); + (gammas, pad, t.challenge_small::()) + }, ); + assert_eq!( vec![ challenges.bytecode_read_raf.gamma, @@ -267,22 +259,22 @@ mod tests { challenges.bytecode_read_raf.stage4_gamma, challenges.bytecode_read_raf.stage5_gamma, ], - inline_gammas, + gammas, ); assert_eq!( challenges.booleanity.reference_address, - inline_reference_address + [fr(11)].into_iter().chain(pad).collect::>() ); assert_eq!( sumchecks.booleanity.reference_cycle(), cycle.iter().rev().copied().collect::>() ); - assert_eq!(challenges.booleanity.gamma, inline_gamma); + assert_eq!(challenges.booleanity.gamma, gamma); } /// The truncate branch: a stage-5 instruction address wider than the /// committed chunk width keeps its reversed tail and draws no pad — only - /// the booleanity gamma squeeze follows the bytecode member's six. + /// the booleanity gamma follows the bytecode member's six. #[test] #[expect( clippy::unwrap_used, @@ -293,69 +285,45 @@ mod tests { let address = vec![fr(11), fr(12), fr(13)]; let sumchecks = sumchecks(address.clone(), vec![fr(21)]); - let (draw_events, challenges) = record(|t| sumchecks.draw_challenges(t).unwrap()); - - assert_eq!( - draw_events, - (1..=7u64).map(DrawEvent::Squeeze).collect::>() + let (challenges, gamma) = assert_same_draws( + |t| sumchecks.draw_challenges(t).unwrap(), + |t| { + for _ in 0..6 { + let _: Fr = t.challenge(); + } + t.challenge_small::() + }, ); + let reversed: Vec = address.iter().rev().copied().collect(); assert_eq!(challenges.booleanity.reference_address, reversed[1..]); assert_eq!(sumchecks.booleanity.reference_cycle(), vec![fr(21)]); - assert_eq!(challenges.booleanity.gamma, fr(7)); + assert_eq!(challenges.booleanity.gamma, gamma); } - /// Locks the stage-6a address-phase Fiat-Shamir append order against silent - /// drift: bytecode read-RAF `intermediate`, each `val_stages` entry, then - /// booleanity `intermediate`. Single-sourced from the generated - /// `append_output_claims`. + /// Locks the stage-6a address-phase opening order: bytecode read-RAF + /// `intermediate`, each `val_stages` entry, then booleanity `intermediate`. #[test] - fn stage6a_output_claims_append_follows_canonical_order() { + fn stage6a_opening_values_follow_canonical_order() { let sumchecks = sumchecks(Vec::new(), Vec::new()); let mut claims = sample_claims(); claims.bytecode_read_raf.val_stages = (903..908).map(fr).collect(); - let mut got = RecordingTranscript::default(); - sumchecks.append_output_claims(&mut got, &claims); - - let mut want = RecordingTranscript::default(); - for value in [901, 903, 904, 905, 906, 907, 902].map(fr) { - want.append_labeled(b"opening_claim", &value); - } - - assert_eq!(got.chunks, want.chunks); - } - - /// A transcript double whose every squeeze returns the same nontrivial - /// scalar, so `challenge_scalar_powers`' output is a genuine power vector. - #[derive(Clone, Default)] - struct ConstantChallengeTranscript; - - impl Transcript for ConstantChallengeTranscript { - type Challenge = Fr; - fn new(_label: &'static [u8]) -> Self { - Self - } - fn append_bytes(&mut self, _bytes: &[u8]) {} - fn challenge(&mut self) -> Self::Challenge { - Fr::from_u64(7) - } - fn state(&self) -> [u8; 32] { - [0u8; 32] - } + assert_eq!( + sumchecks.opening_values(&claims), + [901, 903, 904, 905, 906, 907, 902].map(fr).to_vec() + ); } - /// The `stage_gamma_powers` expansion of a drawn scalar must equal - /// `challenge_scalar_powers`' output for the same squeezed scalar — the - /// value-identity the stage-6a generated draw substitution relies on - /// (the prover draws each stage's power vector inline; the verifier stores - /// the scalar and expands at the stage-6b folds). + /// `stage_gamma_powers` expands each stored stage gamma into the power + /// vector `[1, gamma, ...]` of the stage's fold width, the vector + /// `challenge_powers` yields from the same uniform draw. #[test] - fn stage_gamma_powers_matches_challenge_scalar_powers() { + fn stage_gamma_powers_expand_each_stage_gamma() { use jolt_claims::protocols::jolt::geometry::bytecode::BYTECODE_STAGE_GAMMA_COUNTS; use jolt_claims::protocols::jolt::relations::bytecode::BytecodeReadRafAddressPhaseChallenges; - let gamma = Fr::from_u64(7); + let gamma: Fr = fresh().challenge(); let challenges = BytecodeReadRafAddressPhaseChallenges { gamma, stage1_gamma: gamma, @@ -364,13 +332,12 @@ mod tests { stage4_gamma: gamma, stage5_gamma: gamma, }; - let mut transcript = ConstantChallengeTranscript; for (powers, len) in challenges .stage_gamma_powers() .into_iter() .zip(BYTECODE_STAGE_GAMMA_COUNTS) { - assert_eq!(powers, transcript.challenge_scalar_powers(len)); + assert_eq!(powers, fresh().challenge_powers::(len)); } } } diff --git a/crates/jolt-verifier/src/stages/stage6b/batch.rs b/crates/jolt-verifier/src/stages/stage6b/batch.rs index b56355da11..73be55da4d 100644 --- a/crates/jolt-verifier/src/stages/stage6b/batch.rs +++ b/crates/jolt-verifier/src/stages/stage6b/batch.rs @@ -559,54 +559,47 @@ impl Stage6bSumchecks { } #[cfg(test)] -#[expect( - clippy::as_conversions, - reason = "tests use plain arithmetic on fixture data" -)] mod tests { use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; + use crate::stages::relations::test_transcript::assert_same_draws; use jolt_field::Fr; - /// Pins the post-6a draw schedule to member declaration order: the instruction-RA gamma, - /// (base) the inc gamma, under `field-inline` the field-inline inc gamma (the spec's `eta` - /// draw slot: after the ordinary inc gamma, before the optional committed-bytecode eta), - /// then the committed bytecode eta exactly when the bytecode layout is committed. + /// Pins the post-6a draw schedule to member declaration order, one uniform + /// challenge each: the instruction-RA gamma, (base) the inc gamma, under + /// `field-inline` the field-inline inc gamma (the spec's `eta` draw slot: + /// after the ordinary inc gamma, before the optional committed-bytecode + /// eta), then the committed bytecode eta exactly when the bytecode layout is + /// committed. #[test] fn stage6b_draws_follow_member_declaration_order() { for committed_bytecode in [false, true] { - let mut expected_squeezes = 1usize; + let mut expected_draws = 1usize; #[cfg(not(feature = "akita"))] { - expected_squeezes += 1; + expected_draws += 1; } #[cfg(feature = "field-inline")] { - expected_squeezes += 1; + expected_draws += 1; } - expected_squeezes += usize::from(committed_bytecode); + expected_draws += usize::from(committed_bytecode); - let (inline_events, inline_values) = record(|t| { - (0..expected_squeezes) - .map(|_| t.challenge()) - .collect::>() - }); - let (draw_events, draws) = record(|t| Stage6bDraws::::draw(t, committed_bytecode)); - - assert_eq!(draw_events, inline_events); - assert_eq!( - draw_events, - (1..=expected_squeezes as u64) - .map(DrawEvent::Squeeze) - .collect::>() + let (draws, values) = assert_same_draws( + |t| Stage6bDraws::::draw(t, committed_bytecode), + |t| { + (0..expected_draws) + .map(|_| t.challenge()) + .collect::>() + }, ); + let mut ordered = vec![draws.instruction_ra_gamma]; #[cfg(not(feature = "akita"))] ordered.push(draws.inc_gamma); #[cfg(feature = "field-inline")] ordered.push(draws.field_registers_inc_gamma); ordered.extend(draws.eta); - assert_eq!(ordered, inline_values); + assert_eq!(ordered, values); assert_eq!(draws.eta.is_some(), committed_bytecode); } } diff --git a/crates/jolt-verifier/src/stages/stage6b/booleanity.rs b/crates/jolt-verifier/src/stages/stage6b/booleanity.rs index dab139f8aa..648152a46f 100644 --- a/crates/jolt-verifier/src/stages/stage6b/booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6b/booleanity.rs @@ -179,33 +179,3 @@ impl ConcreteSumcheck for Booleanity { try_eq_mle(&full_sumcheck_point, &reference_eq_point).map_err(public_input_failed) } } - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; - use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; - use jolt_field::Fr; - use jolt_transcript::Channel; - - // Booleanity inherits the default `draw_challenges` (one `challenge_scalar`): the - // inline draw is a single `challenge()`. The historical zero-gamma re-roll was - // dropped — a real Fiat-Shamir transcript never yields zero, and nothing else - // checks for it. - #[test] - fn default_draw_challenges_matches_inline_booleanity_gamma() { - let layout = JoltRaPolynomialLayout::new(1, 1, 1).unwrap(); - let dimensions = BooleanityDimensions::new(layout, 3, 2); - #[cfg(feature = "akita")] - let dimensions = lattice_booleanity::LatticeBooleanityDimensions::new(dimensions).unwrap(); - let relation = Booleanity::::new(dimensions, Vec::new(), Vec::new(), Vec::new()); - - let (inline_events, inline_gamma) = record(|t| t.challenge_small()); - let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - assert_eq!(draw_events, vec![DrawEvent::Squeeze(1)]); - assert_eq!(challenges.gamma, inline_gamma); - } -} diff --git a/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs b/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs index 297f793e0f..4e24bd1d4e 100644 --- a/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs +++ b/crates/jolt-verifier/src/stages/stage6b/instruction_ra_virtualization.rs @@ -152,38 +152,3 @@ impl ConcreteSumcheck for InstructionRaVirtualization { try_eq_mle(&self.instruction_read_raf_cycle, r_cycle).map_err(public_input_failed) } } - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; - use core::num::NonZeroUsize; - use jolt_field::Fr; - use jolt_transcript::Channel; - - fn relation(num_virtual_ra_polys: usize) -> InstructionRaVirtualization { - let dimensions = InstructionRaVirtualizationDimensions::new( - 3, - NonZeroUsize::new(num_virtual_ra_polys).unwrap(), - NonZeroUsize::new(1).unwrap(), - ) - .unwrap(); - InstructionRaVirtualization::new(dimensions, Vec::new(), Vec::new(), 1) - } - - // Inherits the default `draw_challenges`: one `challenge_scalar` squeeze - // storing the squeezed scalar — the same draw the prover's - // `InstructionRaSumcheckParams::new` performs and stage 6b's hand-assembled - // `Stage6bChallenges` mirrors. - #[test] - fn default_draw_challenges_matches_inline_instruction_ra_gamma() { - let relation = relation(2); - let (inline_events, inline_gamma) = record(|t| t.challenge()); - let (draw_events, challenges) = record(|t| relation.draw_challenges(t).unwrap()); - - assert_eq!(draw_events, inline_events); - assert_eq!(draw_events, vec![DrawEvent::Squeeze(1)]); - assert_eq!(challenges.gamma, inline_gamma); - } -} diff --git a/crates/jolt-verifier/src/stages/stage6b/verify.rs b/crates/jolt-verifier/src/stages/stage6b/verify.rs index cd69a98e18..465c30af46 100644 --- a/crates/jolt-verifier/src/stages/stage6b/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6b/verify.rs @@ -497,7 +497,6 @@ pub fn stage6b_opening_values( } #[cfg(test)] -#[expect(clippy::unwrap_used, clippy::expect_used)] mod tests { #[cfg(not(feature = "akita"))] use super::super::booleanity::BooleanityOutputClaims; diff --git a/crates/jolt-verifier/src/stages/stage8/verify.rs b/crates/jolt-verifier/src/stages/stage8/verify.rs index 50dfafffdd..6ff6482821 100644 --- a/crates/jolt-verifier/src/stages/stage8/verify.rs +++ b/crates/jolt-verifier/src/stages/stage8/verify.rs @@ -539,10 +539,16 @@ mod tests { use crate::proof::{FieldInlineCommitments, FieldRegistersCommitments}; use jolt_claims::protocols::field_inline::geometry::claim_reductions::increments::field_rd_inc_reduced; - let commitments = JoltCommitments::new((), (), vec![(), ()], vec![(), ()], vec![()]) - .with_field_inline(FieldInlineCommitments { + let commitments = JoltCommitments { + rd_inc: (), + ram_inc: (), + instruction_ra: vec![(), ()], + ram_ra: vec![(), ()], + bytecode_ra: vec![()], + field_inline: FieldInlineCommitments { field_registers: FieldRegistersCommitments { rd_inc: () }, - }); + }, + }; let opening_point = [2u64, 3, 5].map(Fr::from_u64); let field_point = [3u64, 5].map(Fr::from_u64); @@ -598,33 +604,23 @@ mod tests { ); } - /// The splice fails closed on a missing field-inline commitment payload and on a plan - /// without its RdInc anchor. + /// The splice fails closed on a plan without its RdInc anchor. #[cfg(feature = "field-inline")] #[test] - fn field_inline_splice_fails_closed() { + fn field_inline_splice_fails_closed_without_anchor() { use crate::proof::{FieldInlineCommitments, FieldRegistersCommitments}; let opening_point = [2u64, 3].map(Fr::from_u64); - let without_payload = JoltCommitments::new((), (), Vec::new(), Vec::new(), Vec::new()); - assert!(matches!( - crate::stages::stage8::field_inline::splice_final_opening( - &mut base_entries(false), - &without_payload, - TracePolynomialOrder::CycleMajor, - &opening_point, - &opening_point, - None, - ), - Err(VerifierError::MissingProofPayload { - field: "commitments.field_inline" - }) - )); - - let commitments = JoltCommitments::new((), (), Vec::new(), Vec::new(), Vec::new()) - .with_field_inline(FieldInlineCommitments { + let commitments = JoltCommitments { + rd_inc: (), + ram_inc: (), + instruction_ra: Vec::new(), + ram_ra: Vec::new(), + bytecode_ra: Vec::new(), + field_inline: FieldInlineCommitments { field_registers: FieldRegistersCommitments { rd_inc: () }, - }); + }, + }; let mut anchorless: Vec> = Vec::new(); assert!(matches!( crate::stages::stage8::field_inline::splice_final_opening( diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index ec7451b518..d85a883f95 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -21,13 +21,13 @@ use jolt_transcript::{Channel, ProtocolId, Sponge, VerifierTranscript}; #[cfg(not(feature = "akita"))] use crate::sites::BLINDFOLD; use crate::{ - sites::{COMMITMENTS, PREAMBLE}, config::{ validate_proof_config, ZkConfig, JOLT_VERIFIER_CONFIG, JOLT_VERIFIER_INSTRUCTION_PROFILE, }, num, preprocessing::JoltVerifierPreprocessing, proof::{JoltProof, ProofCommitments, ProofHeader}, + sites::{COMMITMENTS, PREAMBLE}, stages::{ build_formula_dimensions, stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7, stage8, CommittedProgramSchedule, PrecommittedSchedule, @@ -658,52 +658,22 @@ fn validate_ram_remap_base( } #[cfg(test)] -#[cfg_attr( - not(feature = "field-inline"), - expect( - clippy::useless_conversion, - reason = "field-inline selects composed claim and opening types" - ) -)] mod tests { use std::sync::Arc; use common::constants::RAM_START_ADDRESS; use super::*; - #[cfg(not(feature = "akita"))] - use crate::proof::JoltCommitments; - use crate::proof::{ClearProofClaims, JoltProofClaims, JoltStageProofs}; - #[cfg(all(not(feature = "akita"), feature = "field-inline"))] - use crate::proof::{FieldInlineCommitments, FieldRegistersCommitments}; - use crate::stages::stage1::outputs::Stage1OutputClaims; - use crate::stages::stage1::OuterRemainderOutputClaims; - use crate::stages::stage2::outputs::{Stage2BatchOutputClaims, Stage2OutputClaims}; - #[cfg(feature = "field-inline")] - use crate::stages::{ - stage2::outputs::FieldRegistersClaimReductionOutputClaims, - stage4::FieldRegistersReadWriteOutputClaims, - stage5::FieldRegistersValEvaluationOutputClaims, - stage6b::outputs::FieldRegistersIncClaimReductionOutputClaims, - }; - #[cfg(any(feature = "field-inline", feature = "zk"))] - use common::constants::MAX_BLINDFOLD_GENERATORS; + use crate::preprocessing::ProgramPreprocessing; use common::jolt_device::{JoltDevice, MemoryConfig}; - use jolt_claims::protocols::jolt::{JoltOneHotConfig, JoltReadWriteConfig}; #[cfg(feature = "zk")] use jolt_crypto::PedersenSetup; - use jolt_crypto::{Bn254G1, Commitment, Pedersen, VectorCommitmentOpening}; + use jolt_crypto::{Bn254G1, Commitment, Pedersen}; use jolt_field::Fr; use jolt_openings::{CommitmentScheme, OpeningsError}; use jolt_poly::MultilinearPoly; use jolt_program::preprocess::JoltProgramPreprocessing; - use jolt_sumcheck::{ - ClearProof, ClearSumcheckProof, CommittedSumcheckProof, CompressedSumcheckProof, - }; - use jolt_transcript::Transcript; - use num_traits::Zero; - - use crate::preprocessing::ProgramPreprocessing; + use jolt_transcript::{ProverTranscript, VerifierTranscript}; #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] struct TestPcs; @@ -717,7 +687,6 @@ mod tests { impl CommitmentScheme for TestPcs { type Field = Fr; - type Proof = (); type ProverSetup = (); type VerifierSetup = (); type OpeningHint = (); @@ -738,35 +707,95 @@ mod tests { Ok((TestCommitment, ())) } - fn open + ?Sized>( + fn send_commitment( + _commitment: &Self::Output, + _transcript: &mut ProverTranscript, + ) { + } + + fn receive_commitment( + _setup: &Self::VerifierSetup, + _transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(TestCommitment) + } + + fn absorb_commitment(_commitment: &Self::Output, _channel: &mut C) {} + + fn open + ?Sized, H: Sponge>( _poly: &P, _point: &[Self::Field], _eval: Self::Field, _setup: &Self::ProverSetup, _hint: Option, - _transcript: &mut impl Transcript, - ) -> Result { + _transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { Ok(()) } - fn verify( + fn verify( _commitment: &Self::Output, _point: &[Self::Field], _eval: Self::Field, - _proof: &Self::Proof, _setup: &Self::VerifierSetup, - _transcript: &mut impl Transcript, + _transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { Ok(()) } } - impl jolt_transcript::AppendToTranscript for TestCommitment { - fn append_to_transcript(&self, _transcript: &mut T) {} + // The homomorphic-build entry point's bounds; never reached by the tests, + // which reject before any commitment arithmetic. + #[cfg(not(feature = "akita"))] + impl HomomorphicCommitment for TestCommitment { + fn add(_c1: &Self, _c2: &Self) -> Self { + Self + } + + fn linear_combine(_c1: &Self, _c2: &Self, _scalar: &Fr) -> Self { + Self + } } - type TestProof = JoltProof>; - type TestClaims = JoltProofClaims>; + #[cfg(not(feature = "akita"))] + impl AdditivelyHomomorphic for TestPcs { + fn combine(_commitments: &[Self::Output], _scalars: &[Self::Field]) -> Self::Output { + TestCommitment + } + } + + #[cfg(not(feature = "akita"))] + impl ZkOpeningScheme for TestPcs { + type HidingCommitment = Bn254G1; + type Blind = (); + + fn commit_zk + ?Sized>( + _poly: &P, + _setup: &Self::ProverSetup, + ) -> Result<(Self::Output, Self::OpeningHint), OpeningsError> { + Ok((TestCommitment, ())) + } + + fn open_zk + ?Sized, H: Sponge>( + _poly: &P, + _point: &[Self::Field], + _eval: Self::Field, + _setup: &Self::ProverSetup, + _hint: Self::OpeningHint, + _transcript: &mut ProverTranscript, + ) -> Result<(Self::HidingCommitment, Self::Blind), OpeningsError> { + Ok((Bn254G1::default(), ())) + } + + fn verify_zk( + _commitment: &Self::Output, + _point: &[Self::Field], + _setup: &Self::VerifierSetup, + _transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(Bn254G1::default()) + } + } #[test] fn proof_wrapper_uses_modular_trait_bounds() { @@ -784,73 +813,20 @@ mod tests { { } - assert_proof_traits::(); + assert_proof_traits::(); } - #[test] - fn accepts_standard_proof_consistency() { - let proof = proof_with_zk(false, clear_claims()); - - assert!(validate_proof_consistency(&proof, false).is_ok()); - } - - /// A zk proof cannot exist on the akita build (`zk` and `akita` are - /// mutually exclusive), so the accept case is base-only; the reject cases - /// below run on both builds. + /// `verify` rejects a proof produced under other protocol axes as a + /// configuration mismatch before reading the argument string or + /// validating the preprocessing: here the argument string is empty and + /// the bytecode is invalid, each of which would fail later. #[cfg(not(feature = "akita"))] #[test] - fn accepts_zk_proof_consistency() { - let proof = proof_with_zk(true, zk_claims()); - - assert!(validate_proof_consistency(&proof, true).is_ok()); - } - - #[test] - fn rejects_wrong_stage_representation() { - let mut proof = proof_with_zk(false, clear_claims()); - proof.stages.stage5_sumcheck_proof = - SumcheckProof::Committed(CommittedSumcheckProof::default()); - - assert!(matches!( - validate_proof_consistency(&proof, false), - Err(VerifierError::ExpectedClearProof { - field: "stage5_sumcheck_proof", - }) - )); - } - - #[test] - fn rejects_wrong_verifier_zk_flag() { - let proof = proof_with_zk(false, clear_claims()); - - assert!(matches!( - validate_proof_consistency(&proof, true), - Err(VerifierError::ExpectedCommittedProof { - field: "stage1_uni_skip_first_round_proof", - }) - )); - } - - #[test] - fn checks_payload_for_selected_zk_flag() { - assert!(matches!( - validate_proof_consistency(&proof_with_zk(false, zk_claims()), false), - Err(VerifierError::UnexpectedBlindFoldProof) - )); - assert!(matches!( - validate_proof_consistency(&proof_with_zk(true, clear_claims()), true), - Err(VerifierError::UnexpectedOpeningClaims) - )); - } - - #[test] - fn protocol_and_payload_reject_before_preprocessing_validation() { + fn protocol_mismatch_rejects_before_transcript_and_preprocessing() { #[cfg(feature = "field-inline")] use jolt_riscv::JoltInstructionKind; - use jolt_transcript::LegacyBlake2bTranscript; #[cfg_attr(not(feature = "field-inline"), expect(unused_mut))] let mut preprocessing = test_preprocessing(); - // Invalid metadata and layout give independent later-stage failures. #[cfg(feature = "field-inline")] if let ProgramPreprocessing::Full(full) = &mut preprocessing.program { let bytecode = &mut Arc::make_mut(full).bytecode.bytecode; @@ -859,38 +835,25 @@ mod tests { instruction.operands.rs1 = Some(u8::MAX); } } - let is_zk = JOLT_VERIFIER_CONFIG.zk == ZkConfig::BlindFold; - let mut proof = proof_with_zk(is_zk, if is_zk { zk_claims() } else { clear_claims() }); - proof.protocol.zk = if is_zk { - ZkConfig::Transparent - } else { - ZkConfig::BlindFold + let mut protocol = JOLT_VERIFIER_CONFIG; + protocol.zk = match protocol.zk { + ZkConfig::BlindFold => ZkConfig::Transparent, + ZkConfig::Transparent => ZkConfig::BlindFold, }; - let public_io = JoltDevice::default(); + let proof = JoltProof { + protocol, + narg: Vec::new(), + }; + assert!(matches!( - validate_and_seed_transcript::<_, _, LegacyBlake2bTranscript, _>( + verify::, JoltSponge>( &preprocessing, - &public_io, + &JoltDevice::default(), &proof, - None + None, ), Err(VerifierError::ProtocolConfigMismatch { .. }) )); - proof.protocol = JOLT_VERIFIER_CONFIG; - proof.stages.stage1_sumcheck_proof = sumcheck_proof(!is_zk); - assert!(matches!( - validate_and_seed_transcript::<_, _, LegacyBlake2bTranscript, _>( - &preprocessing, - &public_io, - &proof, - None - ), - Err(VerifierError::ExpectedClearProof { - field: "stage1_sumcheck_proof" - } | VerifierError::ExpectedCommittedProof { - field: "stage1_sumcheck_proof" - }) - )); } #[test] @@ -903,17 +866,17 @@ mod tests { outputs: vec![3, 0, 0], ..JoltDevice::default() }; - let proof = proof_with_zk(false, clear_claims()); + let header = test_header(); - let checked = validate_inputs(&preprocessing, &public_io, &proof, false).unwrap(); + let checked = validate_inputs(&preprocessing, &public_io, &header, false).unwrap(); assert_eq!(checked.public_io.inputs, vec![1, 2]); assert_eq!(checked.public_io.outputs, vec![3]); - assert_eq!(checked.trace_length, proof.trace_length); - assert_eq!(checked.ram_K, proof.ram_K); + assert_eq!(checked.trace_length, header.trace_length); + assert_eq!(checked.ram_K, header.ram_K); public_io.outputs = vec![0, 0]; - let checked = validate_inputs(&preprocessing, &public_io, &proof, false).unwrap(); + let checked = validate_inputs(&preprocessing, &public_io, &header, false).unwrap(); assert!(checked.public_io.outputs.is_empty()); } @@ -921,10 +884,9 @@ mod tests { fn validate_inputs_rejects_public_io_layout_mismatch() { let preprocessing = test_preprocessing(); let public_io = JoltDevice::default(); - let proof = proof_with_zk(false, clear_claims()); assert!(matches!( - validate_inputs(&preprocessing, &public_io, &proof, false), + validate_inputs(&preprocessing, &public_io, &test_header(), false), Err(VerifierError::MemoryLayoutMismatch) )); } @@ -941,10 +903,9 @@ mod tests { memory_layout: preprocessing.program.memory_layout().clone(), ..JoltDevice::default() }; - let proof = proof_with_zk(false, clear_claims()); assert!(matches!( - validate_inputs(&preprocessing, &public_io, &proof, false), + validate_inputs(&preprocessing, &public_io, &test_header(), false), Err(VerifierError::InvalidMemoryLayout { reason }) if reason.contains("lowest remapped RAM address") )); @@ -957,11 +918,13 @@ mod tests { memory_layout: preprocessing.program.memory_layout().clone(), ..JoltDevice::default() }; - let mut proof = proof_with_zk(false, clear_claims()); - proof.ram_K = 2; + let header = ProofHeader { + ram_K: 2, + ..test_header() + }; assert!(matches!( - validate_inputs(&preprocessing, &public_io, &proof, false), + validate_inputs(&preprocessing, &public_io, &header, false), Err(VerifierError::InvalidRamK { got: 2, min: 4, .. }) )); } @@ -973,11 +936,13 @@ mod tests { memory_layout: preprocessing.program.memory_layout().clone(), ..JoltDevice::default() }; - let mut proof = proof_with_zk(false, clear_claims()); - proof.ram_K = 1 << 20; + let header = ProofHeader { + ram_K: 1 << 20, + ..test_header() + }; assert!(matches!( - validate_inputs(&preprocessing, &public_io, &proof, false), + validate_inputs(&preprocessing, &public_io, &header, false), Err(VerifierError::InvalidRamK { got, min: 4, @@ -1013,10 +978,9 @@ mod tests { memory_layout: preprocessing.program.memory_layout().clone(), ..JoltDevice::default() }; - let proof = proof_with_zk(true, zk_claims()); assert!(matches!( - validate_inputs(&preprocessing, &public_io, &proof, false), + validate_inputs(&preprocessing, &public_io, &test_header(), false), Err(VerifierError::MissingVectorCommitmentSetup) )); } @@ -1033,30 +997,15 @@ mod tests { memory_layout: preprocessing.program.memory_layout().clone(), ..JoltDevice::default() }; - let proof = proof_with_zk(true, zk_claims()); assert!(matches!( - validate_inputs(&preprocessing, &public_io, &proof, false), + validate_inputs(&preprocessing, &public_io, &test_header(), false), Err(VerifierError::InvalidVectorCommitmentCapacity { got: 1, .. }) )); } - fn proof_with_zk(is_zk: bool, claims: TestClaims) -> TestProof { - JoltProof { - protocol: crate::config::JoltProtocolConfig::for_zk(claims.is_zk()), - #[cfg(not(feature = "akita"))] - commitments: test_commitments(), - #[cfg(feature = "akita")] - commitments: TestCommitment, - stages: stage_proofs(is_zk), - #[cfg(not(feature = "akita"))] - joint_opening_proof: (), - #[cfg(feature = "akita")] - joint_opening_proof: (), - untrusted_advice_commitment: None, - #[cfg(all(feature = "akita", feature = "field-inline"))] - field_inc_commitment: Some(TestCommitment), - claims, + fn test_header() -> ProofHeader { + ProofHeader { trace_length: 1, ram_K: 4, rw_config: JoltReadWriteConfig { @@ -1069,299 +1018,8 @@ mod tests { log_k_chunk: 0, lookups_ra_virtual_log_k_chunk: 0, }, - trace_polynomial_order: crate::proof::TracePolynomialOrder::CycleMajor, - } - } - - #[cfg(not(feature = "akita"))] - fn test_commitments() -> JoltCommitments { - #[cfg(feature = "field-inline")] - { - JoltCommitments::new( - TestCommitment, - TestCommitment, - Vec::::new(), - Vec::::new(), - Vec::::new(), - ) - .with_field_inline(FieldInlineCommitments { - field_registers: FieldRegistersCommitments { - rd_inc: TestCommitment, - }, - }) - } - #[cfg(not(feature = "field-inline"))] - JoltCommitments::new( - TestCommitment, - TestCommitment, - Vec::::new(), - Vec::::new(), - Vec::::new(), - ) - } - - fn clear_claims() -> TestClaims { - let zero = Fr::zero(); - - JoltProofClaims::Clear(ClearProofClaims { - stage1: Stage1OutputClaims::new(zero, empty_spartan_outer_claims()), - stage2: Stage2OutputClaims::new( - zero, - Stage2BatchOutputClaims { - ram_read_write: stage2::outputs::RamReadWriteOutputClaims { - val: zero, - ra: zero, - inc: zero, - }, - product_remainder: stage2::outputs::ProductRemainderOutputClaims { - left_instruction_input: zero, - right_instruction_input: zero, - jump_flag: zero, - write_lookup_output_to_rd: zero, - lookup_output: zero, - branch_flag: zero, - next_is_noop: zero, - virtual_instruction: zero, - }.into(), - instruction_claim_reduction: - stage2::outputs::InstructionClaimReductionOutputClaims { - lookup_output: zero, - left_lookup_operand: zero, - right_lookup_operand: zero, - left_instruction_input: zero, - right_instruction_input: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_claim_reduction: - FieldRegistersClaimReductionOutputClaims { - rd_value: zero, - rs1_value: zero, - rs2_value: zero, - }, - ram_raf_evaluation: stage2::outputs::RamRafEvaluationOutputClaims { - ram_ra: zero, - }, - ram_output_check: stage2::outputs::RamOutputCheckOutputClaims { - val_final: zero, - }, - }, - ), - stage3: stage3::outputs::Stage3OutputClaims { - shift: stage3::outputs::SpartanShiftOutputClaims { - unexpanded_pc: zero, - pc: zero, - is_virtual: zero, - is_first_in_sequence: zero, - is_noop: zero, - }, - instruction_input: stage3::outputs::InstructionInputOutputClaims { - left_operand_is_rs1: zero, - rs1_value: zero, - left_operand_is_pc: zero, - unexpanded_pc: zero, - right_operand_is_rs2: zero, - rs2_value: zero, - right_operand_is_imm: zero, - imm: zero, - }, - registers_claim_reduction: stage3::outputs::RegistersClaimReductionOutputClaims { - rd_write_value: zero, - rs1_value: zero, - rs2_value: zero, - }, - }, - stage4: stage4::outputs::Stage4OutputClaims { - registers_read_write: stage4::RegistersReadWriteOutputClaims { - registers_val: zero, - rs1_ra: zero, - rs2_ra: zero, - rd_wa: zero, - rd_inc: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_read_write: FieldRegistersReadWriteOutputClaims { - registers_val: zero, - rs1_ra: zero, - rs2_ra: zero, - rd_wa: zero, - rd_inc: zero, - }, - ram_val_check: stage4::RamValCheckOutputClaims { - untrusted_advice: None, - trusted_advice: None, - program_image: None, - ram_ra: zero, - ram_inc: zero, - }, - }, - stage5: stage5::outputs::Stage5OutputClaims { - instruction_read_raf: stage5::InstructionReadRafOutputClaims { - lookup_table_flags: Vec::new(), - instruction_ra: Vec::new(), - instruction_raf_flag: zero, - }, - ram_ra_claim_reduction: stage5::RamRaClaimReductionOutputClaims { ram_ra: zero }, - registers_val_evaluation: stage5::RegistersValEvaluationOutputClaims { - rd_inc: zero, - rd_wa: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_val_evaluation: FieldRegistersValEvaluationOutputClaims { - rd_inc: zero, - rd_wa: zero, - }, - }, - stage6a: stage6a::outputs::Stage6aOutputClaims { - bytecode_read_raf: stage6a::outputs::BytecodeReadRafAddressPhaseOutputClaims { - intermediate: zero, - val_stages: Vec::new(), - }.into(), - booleanity: stage6a::outputs::BooleanityAddressPhaseOutputClaims { - intermediate: zero, - }, - }, - stage6b: stage6b::outputs::Stage6bOutputClaims { - #[cfg(not(feature = "akita"))] - bytecode_read_raf: stage6b::outputs::BytecodeReadRafOutputClaims { - bytecode_ra: Vec::new(), - }, - #[cfg(feature = "akita")] - bytecode_read_raf: - stage6b::bytecode_read_raf::LatticeBytecodeReadRafOutputClaims { - bytecode_ra: Vec::new(), - fused_inc: zero, - }, - #[cfg(not(feature = "akita"))] - booleanity: stage6b::outputs::BooleanityOutputClaims { - instruction_ra: Vec::new(), - bytecode_ra: Vec::new(), - ram_ra: Vec::new(), - }, - #[cfg(feature = "akita")] - booleanity: - jolt_claims::protocols::jolt::lattice::relations::booleanity::LatticeBooleanityOutputClaims { - instruction_ra: Vec::new(), - bytecode_ra: Vec::new(), - ram_ra: Vec::new(), - balanced_inc_digits: Vec::new(), - balanced_inc_carry: zero, - }, - ram_hamming_booleanity: stage6b::outputs::RamHammingBooleanityOutputClaims { - ram_hamming_weight: zero, - }, - ram_ra_virtualization: stage6b::outputs::RamRaVirtualizationOutputClaims { - ram_ra: Vec::new(), - }, - instruction_ra_virtualization: - stage6b::outputs::InstructionRaVirtualizationOutputClaims { - committed_instruction_ra: Vec::new(), - }, - #[cfg(not(feature = "akita"))] - inc_claim_reduction: stage6b::outputs::IncClaimReductionOutputClaims { - ram_inc: zero, - rd_inc: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_inc_claim_reduction: - FieldRegistersIncClaimReductionOutputClaims { rd_inc: zero }, - #[cfg(not(feature = "akita"))] - trusted_advice: None, - #[cfg(not(feature = "akita"))] - untrusted_advice: None, - bytecode_reduction: None, - program_image_reduction: None, - }, - stage7: stage7::outputs::Stage7OutputClaims { - hamming_weight_claim_reduction: - stage7::hamming_weight_claim_reduction::HammingWeightClaimReductionOutputClaims { - instruction_ra: Vec::new(), - bytecode_ra: Vec::new(), - ram_ra: Vec::new(), - #[cfg(feature = "akita")] - balanced_inc_digits: Vec::new(), - #[cfg(feature = "akita")] - balanced_inc_carry: zero, - }, - #[cfg(not(feature = "akita"))] - trusted_advice: None, - #[cfg(not(feature = "akita"))] - untrusted_advice: None, - bytecode_address_phase: None, - program_image_address_phase: None, - }, - }) - } - - fn empty_spartan_outer_claims() -> stage1::outputs::Stage1BatchOutputClaims { - stage1::outputs::Stage1BatchOutputClaims { - outer_remainder: OuterRemainderOutputClaims::::default().into(), - } - } - - fn zk_claims() -> TestClaims { - JoltProofClaims::Zk { - blindfold_proof: empty_blindfold_proof(), - } - } - - fn empty_blindfold_proof() -> jolt_blindfold::BlindFoldProof { - jolt_blindfold::BlindFoldProof { - auxiliary_row_commitments: Vec::new(), - random_round_commitments: Vec::new(), - random_output_claim_row_commitments: Vec::new(), - random_auxiliary_row_commitments: Vec::new(), - random_error_row_commitments: Vec::new(), - random_eval_commitments: Vec::new(), - random_u: Fr::zero(), - cross_term_error_row_commitments: Vec::new(), - outer_sumcheck: CompressedSumcheckProof::default(), - az_rx: Fr::zero(), - bz_rx: Fr::zero(), - cz_rx: Fr::zero(), - inner_sumcheck: CompressedSumcheckProof::default(), - witness_opening: VectorCommitmentOpening { - combined_vector: Vec::new(), - combined_blinding: Fr::zero(), - }, - error_opening: VectorCommitmentOpening { - combined_vector: Vec::new(), - combined_blinding: Fr::zero(), - }, - folded_eval_outputs: Vec::new(), - folded_eval_blindings: Vec::new(), - folded_eval_output_openings: Vec::new(), - folded_eval_blinding_openings: Vec::new(), - } - } - - fn stage_proofs(is_zk: bool) -> JoltStageProofs> { - JoltStageProofs { - stage1_uni_skip_first_round_proof: uniskip_proof(is_zk), - stage1_sumcheck_proof: sumcheck_proof(is_zk), - stage2_uni_skip_first_round_proof: uniskip_proof(is_zk), - stage2_sumcheck_proof: sumcheck_proof(is_zk), - stage3_sumcheck_proof: sumcheck_proof(is_zk), - stage4_sumcheck_proof: sumcheck_proof(is_zk), - stage5_sumcheck_proof: sumcheck_proof(is_zk), - stage6a_sumcheck_proof: sumcheck_proof(is_zk), - stage6b_sumcheck_proof: sumcheck_proof(is_zk), - stage7_sumcheck_proof: sumcheck_proof(is_zk), - } - } - - fn uniskip_proof(is_zk: bool) -> SumcheckProof { - if is_zk { - SumcheckProof::Committed(CommittedSumcheckProof::default()) - } else { - SumcheckProof::Clear(ClearProof::Full(ClearSumcheckProof::default())) - } - } - - fn sumcheck_proof(is_zk: bool) -> SumcheckProof { - if is_zk { - SumcheckProof::Committed(CommittedSumcheckProof::default()) - } else { - SumcheckProof::Clear(ClearProof::Compressed(CompressedSumcheckProof::default())) + trace_polynomial_order: TracePolynomialOrder::CycleMajor, + untrusted_advice: false, } } From 2f24ffabccecf53591d0298d58fa514ddca2afb3 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 16:00:09 -0400 Subject: [PATCH 27/62] test(verifier): port fixtures, completeness, and verifier fuzzers to 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. --- crates/jolt-verifier/fuzz/Cargo.lock | 6 +- crates/jolt-verifier/fuzz/Cargo.toml | 7 +- .../fuzz/fixtures/advice-consumer-bundle.bin | Bin 174384 -> 207258 bytes .../fuzz/fixtures/committed-muldiv-bundle.bin | Bin 96447 -> 129887 bytes .../fuzz/fixtures/muldiv-bundle.bin | Bin 169403 -> 202284 bytes .../fixtures/zk-advice-consumer-bundle.bin | Bin 177206 -> 176092 bytes .../fixtures/zk-committed-muldiv-bundle.bin | Bin 99118 -> 97985 bytes .../fuzz/fixtures/zk-muldiv-bundle.bin | Bin 172417 -> 171310 bytes .../proof_deserialize_no_panic.rs | 50 +++- .../fuzz_targets/proof_tamper_must_reject.rs | 256 +---------------- .../fuzz_targets/validate_inputs_no_panic.rs | 89 +++--- .../zk_proof_tamper_must_reject.rs | 260 +----------------- .../proof_deserialize_no_panic/honest_header | Bin 0 -> 31 bytes .../seeds/proof_tamper_must_reject/append | Bin 0 -> 7 bytes .../proof_tamper_must_reject/clear_claim | 1 - .../seeds/proof_tamper_must_reject/delete | Bin 0 -> 7 bytes .../fixture_program_swap | 1 - .../proof_tamper_must_reject/header_flip | Bin 0 -> 7 bytes .../seeds/proof_tamper_must_reject/heap_size | Bin 0 -> 6 bytes .../seeds/proof_tamper_must_reject/insert | Bin 0 -> 7 bytes .../seeds/proof_tamper_must_reject/narg_flip | Bin 0 -> 7 bytes .../opening_commitment | 1 - .../outer-round-eight | Bin 6 -> 0 bytes .../seeds/proof_tamper_must_reject/overwrite | Bin 0 -> 8 bytes .../proof_tamper_must_reject/program_swap | Bin 0 -> 6 bytes .../proof_tamper_must_reject/protocol_mode | Bin 0 -> 6 bytes .../proof_tamper_must_reject/public_input | Bin 0 -> 7 bytes .../proof_tamper_must_reject/sumcheck_round | 1 - .../seeds/proof_tamper_must_reject/tail_flip | Bin 0 -> 7 bytes .../proof_tamper_must_reject/trace_length | 1 - .../seeds/proof_tamper_must_reject/truncate | Bin 0 -> 6 bytes .../proof_tamper_must_reject/trusted_advice | Bin 0 -> 6 bytes .../seeds/validate_inputs_no_panic/valid-ish | Bin 20 -> 37 bytes .../advice_commitment | 1 - .../seeds/zk_proof_tamper_must_reject/append | Bin 0 -> 7 bytes .../blindfold_random_u | 1 - .../committed_sumcheck | 1 - .../seeds/zk_proof_tamper_must_reject/delete | Bin 0 -> 7 bytes .../zk_proof_tamper_must_reject/header_flip | Bin 0 -> 7 bytes .../zk_proof_tamper_must_reject/heap_size | Bin 0 -> 6 bytes .../seeds/zk_proof_tamper_must_reject/insert | Bin 0 -> 7 bytes .../zk_proof_tamper_must_reject/narg_flip | Bin 0 -> 7 bytes .../outer-round-eight | Bin 5 -> 0 bytes .../zk_proof_tamper_must_reject/overwrite | Bin 0 -> 8 bytes .../zk_proof_tamper_must_reject/program_swap | Bin 5 -> 6 bytes .../zk_proof_tamper_must_reject/protocol_mode | Bin 5 -> 6 bytes .../zk_proof_tamper_must_reject/public_input | Bin 5 -> 7 bytes .../zk_proof_tamper_must_reject/tail_flip | Bin 0 -> 7 bytes .../zk_proof_tamper_must_reject/truncate | Bin 0 -> 6 bytes .../trusted_advice | Bin 0 -> 6 bytes crates/jolt-verifier/fuzz/src/lib.rs | 177 ++++++++++++ crates/jolt-verifier/tests/completeness/zk.rs | 207 +++----------- .../tests/generate_fuzz_fixture.rs | 14 +- .../tests/support/akita_fixtures.rs | 19 +- crates/jolt-verifier/tests/support/mod.rs | 26 +- .../tests/support/verifier_fixtures.rs | 63 +++-- 56 files changed, 396 insertions(+), 786 deletions(-) create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_deserialize_no_panic/honest_header create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/append delete mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/clear_claim create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/delete delete mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/fixture_program_swap create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/header_flip create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/heap_size create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/insert create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/narg_flip delete mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/opening_commitment delete mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/outer-round-eight create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/overwrite create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/program_swap create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/protocol_mode create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/public_input delete mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/sumcheck_round create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/tail_flip delete mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trace_length create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/truncate create mode 100644 crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trusted_advice delete mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/advice_commitment create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/append delete mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/blindfold_random_u delete mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/committed_sumcheck create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/delete create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/header_flip create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/heap_size create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/insert create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/narg_flip delete mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/outer-round-eight create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/overwrite create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/tail_flip create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/truncate create mode 100644 crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/trusted_advice create mode 100644 crates/jolt-verifier/fuzz/src/lib.rs diff --git a/crates/jolt-verifier/fuzz/Cargo.lock b/crates/jolt-verifier/fuzz/Cargo.lock index da9245fc14..04a35bb2b1 100644 --- a/crates/jolt-verifier/fuzz/Cargo.lock +++ b/crates/jolt-verifier/fuzz/Cargo.lock @@ -697,7 +697,6 @@ dependencies = [ "ark-std 0.5.0", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint 0.4.8", "num-integer", "num-traits", @@ -785,7 +784,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -857,7 +855,9 @@ dependencies = [ "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] @@ -913,8 +913,6 @@ dependencies = [ "jolt-crypto", "jolt-dory", "jolt-field", - "jolt-poly", - "jolt-sumcheck", "jolt-transcript", "jolt-verifier", "libfuzzer-sys", diff --git a/crates/jolt-verifier/fuzz/Cargo.toml b/crates/jolt-verifier/fuzz/Cargo.toml index d866c35201..f6525cc63a 100644 --- a/crates/jolt-verifier/fuzz/Cargo.toml +++ b/crates/jolt-verifier/fuzz/Cargo.toml @@ -51,6 +51,11 @@ weekly-seconds = 900 [features] zk = ["jolt-verifier/zk"] +[lib] +path = "src/lib.rs" +test = false +doc = false + [dependencies] libfuzzer-sys = "0.4" jolt-verifier = { path = "..", features = ["fuzzing"] } @@ -58,8 +63,6 @@ jolt-claims = { path = "../../jolt-claims" } jolt-crypto = { path = "../../jolt-crypto" } jolt-dory = { path = "../../jolt-dory" } jolt-field = { path = "../../jolt-field", features = ["bn254"] } -jolt-poly = { path = "../../jolt-poly" } -jolt-sumcheck = { path = "../../jolt-sumcheck" } jolt-transcript = { path = "../../jolt-transcript" } common = { path = "../../../common" } bincode = { version = "2", features = ["serde"] } diff --git a/crates/jolt-verifier/fuzz/fixtures/advice-consumer-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/advice-consumer-bundle.bin index f13bc7e991acba52eaff93c1119587313dbda670..bc12e6902080eed1846d221495d64b5740d5a532 100644 GIT binary patch delta 86015 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 F{|^Sbm~Q|8 diff --git a/crates/jolt-verifier/fuzz/fixtures/committed-muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/committed-muldiv-bundle.bin index 7f71d497dbb0b27dc15050e3d13e8ceafcdbda9a..f8053763c53b5e82caf1e16b46326dfb4e8e6c21 100644 GIT binary patch literal 129887 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

9`yF>G{}@H^Kg7efRWfhG&oAd_ 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⪚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 Kg&9`Y79;>>I!ZDC literal 96447 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 p|C5c>lN>;s4p1|9|y>|X}={{g*aF6jUO diff --git a/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin index f91ab83e652d0c08e176929e3a635aa4e445cfd5..eea30cfc07952704dd16617ebdb1eb5161dc55e5 100644 GIT binary patch delta 85763 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 X``x0=zd+7*W2{KS&;DfulV=$Kj1Hp3 delta 85751 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̥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

E^G2 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 z%0D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 I#K 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 M01QajE*!%D0FFxntpET3 delta 88122 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%0A-Od5_CzQ%u0 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�D_R3-bf}fB}t^5y%_t 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

!)p)NV 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 literal 99118 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%0DAXPy>wM}U!wN@$ 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

{WdjN`u;WNL|iU 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 diff --git a/crates/jolt-verifier/fuzz/fuzz_targets/proof_deserialize_no_panic.rs b/crates/jolt-verifier/fuzz/fuzz_targets/proof_deserialize_no_panic.rs index 3f8a0e58da..f4cc9b0bce 100644 --- a/crates/jolt-verifier/fuzz/fuzz_targets/proof_deserialize_no_panic.rs +++ b/crates/jolt-verifier/fuzz/fuzz_targets/proof_deserialize_no_panic.rs @@ -1,21 +1,47 @@ #![no_main] -//! Attacker-controlled proof bytes fed to the public bincode deserializer for -//! `JoltProof` must never panic or over-allocate — only -//! decode successfully or return an error. This is the genuine untrusted-input -//! boundary: a verifier receives serialized proofs from untrusted provers. +//! Attacker-controlled proof bytes must never panic or over-allocate the +//! verifier's untrusted-input boundary, only decode or verify to a typed +//! result: +//! +//! - `data` as a serialized `JoltProof` through the public bincode decoder, +//! verifying whatever decodes; +//! - `data` as the argument string of a proof with the honest protocol +//! configuration, verified against the muldiv fixture. Unless it equals the +//! honest argument string it must be rejected. + +use std::sync::OnceLock; -use jolt_crypto::{Bn254G1, Pedersen}; -use jolt_dory::DoryScheme; use jolt_verifier::JoltProof; +use jolt_verifier_fuzz::Bundle; use libfuzzer_sys::fuzz_target; -type FuzzProof = JoltProof>; +static FIXTURE: &[u8] = include_bytes!("../fixtures/muldiv-bundle.bin"); + +fn bundle() -> &'static Bundle { + static BUNDLE: OnceLock = OnceLock::new(); + BUNDLE.get_or_init(|| Bundle::decode_verified(FIXTURE)) +} fuzz_target!(|data: &[u8]| { - let config = bincode::config::standard(); - // The decoder must handle arbitrary bytes without panicking. A successful - // decode is fine; libFuzzer's RSS limit guards against unbounded growth - // from an attacker-chosen length prefix. - let _ = bincode::serde::decode_from_slice::(data, config); + let bundle = bundle(); + // libFuzzer's RSS limit guards against unbounded growth from an + // attacker-chosen length prefix. + if let Ok((proof, _)) = + bincode::serde::decode_from_slice::(data, bincode::config::standard()) + { + let _ = bundle.verify(&proof); + } + + if data == bundle.proof.narg.as_slice() { + return; + } + let proof = JoltProof { + protocol: bundle.proof.protocol, + narg: data.to_vec(), + }; + assert!( + bundle.verify(&proof).is_err(), + "verifier accepted an arbitrary argument string" + ); }); diff --git a/crates/jolt-verifier/fuzz/fuzz_targets/proof_tamper_must_reject.rs b/crates/jolt-verifier/fuzz/fuzz_targets/proof_tamper_must_reject.rs index b409e2da5e..d5feb2c935 100644 --- a/crates/jolt-verifier/fuzz/fuzz_targets/proof_tamper_must_reject.rs +++ b/crates/jolt-verifier/fuzz/fuzz_targets/proof_tamper_must_reject.rs @@ -1,28 +1,20 @@ #![no_main] -//! Structured mutation of accepted transparent proofs. +//! Argument-string mutation of accepted transparent proofs. //! -//! Each input selects an accepted fixture and exactly one semantic mutation. -//! The mutation families cover preamble inputs, commitments, every sumcheck -//! stage, clear claims, advice, and the final Dory opening. +//! Each input selects an accepted fixture (muldiv, advice consumer with both +//! advice kinds, committed-program muldiv) and applies exactly one mutation to +//! its NARG argument string or to the public statement the verifier binds; +//! see `jolt_verifier_fuzz::tamper_must_reject` for the input layout and the +//! mutation families. The verifier absorbs every argument-string byte and +//! decodes only canonical encodings, so every mutation that changes the +//! verifier's input must be rejected. use std::sync::OnceLock; -use common::jolt_device::JoltDevice; -use jolt_crypto::{Bn254G1, Pedersen}; -use jolt_dory::{DoryCommitment, DoryScheme}; -use jolt_field::{CanonicalEncoding, Fr, Ring}; -use jolt_poly::{CompressedPoly, UnivariatePoly}; -use jolt_sumcheck::{ClearProof, SumcheckProof}; -use jolt_transcript::LegacyBlake2bTranscript; -use jolt_verifier::{verify, JoltProof, JoltProofClaims, JoltVerifierPreprocessing, ZkConfig}; +use jolt_verifier_fuzz::{tamper_must_reject, Bundle}; use libfuzzer_sys::fuzz_target; -type Preprocessing = JoltVerifierPreprocessing>; -type Proof = JoltProof>; -type Bundle = (Preprocessing, JoltDevice, Proof, Option); -type StageProof = SumcheckProof; - static FIXTURES: [&[u8]; 3] = [ include_bytes!("../fixtures/muldiv-bundle.bin"), include_bytes!("../fixtures/advice-consumer-bundle.bin"), @@ -34,237 +26,11 @@ fn bundles() -> &'static [Bundle] { BUNDLES.get_or_init(|| { FIXTURES .iter() - .map(|bytes| { - let (bundle, consumed): (Bundle, usize) = - bincode::serde::decode_from_slice(bytes, bincode::config::standard()) - .expect("fixture decodes"); - assert_eq!(consumed, bytes.len(), "fixture has trailing bytes"); - let (preprocessing, public_io, proof, advice) = &bundle; - verify::, LegacyBlake2bTranscript>( - preprocessing, - public_io, - proof, - advice.as_ref(), - ) - .expect("honest fixture proof must verify before tampering"); - bundle - }) + .map(|bytes| Bundle::decode_verified(bytes)) .collect() }) } -fn stage_proof(proof: &mut Proof, index: usize) -> &mut StageProof { - match index % 10 { - 0 => &mut proof.stages.stage1_uni_skip_first_round_proof, - 1 => &mut proof.stages.stage1_sumcheck_proof, - 2 => &mut proof.stages.stage2_uni_skip_first_round_proof, - 3 => &mut proof.stages.stage2_sumcheck_proof, - 4 => &mut proof.stages.stage3_sumcheck_proof, - 5 => &mut proof.stages.stage4_sumcheck_proof, - 6 => &mut proof.stages.stage5_sumcheck_proof, - 7 => &mut proof.stages.stage6a_sumcheck_proof, - 8 => &mut proof.stages.stage6b_sumcheck_proof, - _ => &mut proof.stages.stage7_sumcheck_proof, - } -} - -fn mutate_clear_sumcheck(proof: &mut StageProof, operation: u8, index: usize, delta: Fr) -> bool { - let before = proof.clone(); - match proof { - SumcheckProof::Clear(ClearProof::Full(clear)) => match operation % 3 { - 0 => { - let _ = clear.round_polynomials.pop(); - } - 1 => clear - .round_polynomials - .push(UnivariatePoly::new(vec![Fr::from_u64(1)])), - _ => { - if clear.round_polynomials.is_empty() { - return false; - } - let round = index % clear.round_polynomials.len(); - let mut coefficients = clear.round_polynomials[round].coefficients().to_vec(); - let coefficient = operation as usize / 3 % coefficients.len(); - coefficients[coefficient] += delta; - clear.round_polynomials[round] = UnivariatePoly::new(coefficients); - } - }, - SumcheckProof::Clear(ClearProof::Compressed(clear)) => match operation % 3 { - 0 => { - let _ = clear.round_polynomials.pop(); - } - 1 => clear - .round_polynomials - .push(CompressedPoly::new(vec![Fr::from_u64(1)])), - _ => { - if clear.round_polynomials.is_empty() { - return false; - } - let round = index % clear.round_polynomials.len(); - let mut coefficients = clear.round_polynomials[round] - .coeffs_except_linear_term() - .to_vec(); - let coefficient = operation as usize / 3 % coefficients.len(); - coefficients[coefficient] += delta; - clear.round_polynomials[round] = CompressedPoly::new(coefficients); - } - }, - SumcheckProof::Committed(_) => return false, - } - *proof != before -} - -fn mutate_clear_claim(proof: &mut Proof, stage: usize, delta: Fr) -> bool { - let JoltProofClaims::Clear(claims) = &mut proof.claims else { - return false; - }; - match stage % 8 { - 0 => claims.stage1.uniskip_output_claim += delta, - 1 => claims.stage2.product_uniskip_output_claim += delta, - 2 => claims.stage3.shift.pc += delta, - 3 => claims.stage4.ram_val_check.ram_ra += delta, - 4 => claims.stage5.registers_val_evaluation.rd_inc += delta, - 5 => claims.stage6a.booleanity.intermediate += delta, - 6 => claims.stage6b.ram_hamming_booleanity.ram_hamming_weight += delta, - _ => { - let Some(value) = claims - .stage7 - .hamming_weight_claim_reduction - .instruction_ra - .first_mut() - else { - return false; - }; - *value += delta; - } - } - true -} - fuzz_target!(|data: &[u8]| { - if data.len() < 5 { - return; - } - let fixture = &bundles()[data[0] as usize % bundles().len()]; - let mut preprocessing = fixture.0.clone(); - let mut public_io = fixture.1.clone(); - let mut proof = fixture.2.clone(); - let mut advice = fixture.3.clone(); - let mutation = data[1] % 16; - let stage = data[2] as usize; - let operation = data[3]; - let index = data[4] as usize; - let mut delta = Fr::from_bytes_le_reduced(&data[5..data.len().min(37)]); - if delta == Fr::from_u64(0) { - delta = Fr::from_u64(1); - } - - let changed = match mutation { - // Legal-but-wrong dimensions: still powers of two, so they pass - // `validate_inputs` and must be rejected by the stage round - // structure rather than a typed shape guard. - 0 => { - proof.trace_length *= 2; - true - } - 1 => { - proof.ram_K *= 2; - true - } - // Shape guard, not a soundness probe: clear claims under a BlindFold - // header are rejected by `validate_proof_consistency` before any - // crypto runs. - 2 => { - proof.protocol.zk = ZkConfig::BlindFold; - true - } - 3 => { - if public_io.inputs.is_empty() { - return; - } - let position = index % public_io.inputs.len(); - public_io.inputs[position] ^= operation.max(1); - true - } - 4 => { - public_io.memory_layout.heap_size = public_io.memory_layout.heap_size.saturating_add(1); - true - } - 5 => { - if proof.commitments.rd_inc == proof.commitments.ram_inc { - return; - } - std::mem::swap( - &mut proof.commitments.rd_inc, - &mut proof.commitments.ram_inc, - ); - true - } - 6 => proof.commitments.bytecode_ra.pop().is_some(), - 7 => { - if proof.commitments.instruction_ra.is_empty() { - return; - } - let position = index % proof.commitments.instruction_ra.len(); - let before = proof.commitments.instruction_ra[position].clone(); - proof.commitments.instruction_ra[position] = DoryCommitment::default(); - proof.commitments.instruction_ra[position] != before - } - 8 => { - proof.untrusted_advice_commitment = match proof.untrusted_advice_commitment.take() { - Some(_) => None, - None => Some(DoryCommitment::default()), - }; - true - } - 9 => { - advice = match advice.take() { - Some(_) => None, - None => Some(DoryCommitment::default()), - }; - true - } - 10 => mutate_clear_sumcheck(stage_proof(&mut proof, stage), operation, index, delta), - 11 => mutate_clear_claim(&mut proof, stage, delta), - 12 => proof.joint_opening_proof.0.final_message.take().is_some(), - 13 => { - preprocessing.program = bundles()[(data[0] as usize + 1) % bundles().len()] - .0 - .program - .clone(); - preprocessing.program != fixture.0.program - } - 14 => { - if proof.commitments.ram_ra.is_empty() { - return; - } - let position = index % proof.commitments.ram_ra.len(); - let before = proof.commitments.ram_ra[position].clone(); - proof.commitments.ram_ra[position] = DoryCommitment::default(); - proof.commitments.ram_ra[position] != before - } - _ => { - if proof.commitments.bytecode_ra.len() < 2 - || proof.commitments.bytecode_ra[0] == proof.commitments.bytecode_ra[1] - { - return; - } - proof.commitments.bytecode_ra.swap(0, 1); - true - } - }; - if !changed { - return; - } - - let result = verify::, LegacyBlake2bTranscript>( - &preprocessing, - &public_io, - &proof, - advice.as_ref(), - ); - assert!( - result.is_err(), - "verifier accepted a tampered transparent proof (mutation class {mutation})" - ); + tamper_must_reject(bundles(), data, "transparent"); }); diff --git a/crates/jolt-verifier/fuzz/fuzz_targets/validate_inputs_no_panic.rs b/crates/jolt-verifier/fuzz/fuzz_targets/validate_inputs_no_panic.rs index e144917f0d..98c3912d39 100644 --- a/crates/jolt-verifier/fuzz/fuzz_targets/validate_inputs_no_panic.rs +++ b/crates/jolt-verifier/fuzz/fuzz_targets/validate_inputs_no_panic.rs @@ -1,71 +1,58 @@ #![no_main] -//! `validate_inputs_from_parts` performs the verifier's pre-crypto input -//! checks — memory-layout match, input/output size bounds, trace-length and -//! RAM-size validity — over attacker-influenced dimensions. It must return a -//! typed `Ok`/`Err` for any inputs, never panic or over-allocate. +//! The verifier's pre-crypto input checks — proof-header decoding, then +//! `validate_inputs`' memory-layout match, input/output size bounds, and +//! trace-length, RAM-size, one-hot, read-write, and trace-order validity — +//! over attacker-chosen header bytes and public I/O sizes. They must return +//! a typed `Ok`/`Err` for any input, never panic or over-allocate. //! -//! The honest preprocessing and proof metadata come from the checked-in -//! fixture; the fuzzer drives the scalar dimensions (trace length, RAM size, -//! advice presence, ZK flag) and the public I/O buffer sizes. -//! -//! `one_hot_config` is held at its fixture value; `rw_config` is not a -//! parameter of `validate_inputs_from_parts`. Configuration mutations and -//! the full verifier are outside this target's scope. +//! The honest preprocessing and public I/O come from the checked-in muldiv +//! fixture. Input layout: `data[0]` bit 0 declares a trusted-advice +//! commitment, `data[1]` sets the public input length in 64-byte units, and +//! `data[2..]` is read as an argument string: the proof header first, with +//! the bytes after it becoming the public outputs. use std::sync::OnceLock; -use common::jolt_device::JoltDevice; use jolt_crypto::{Bn254G1, Pedersen}; -use jolt_dory::{DoryCommitment, DoryScheme}; -use jolt_verifier::{validate_inputs_from_parts, JoltProof, JoltVerifierPreprocessing}; +use jolt_dory::DoryScheme; +use jolt_transcript::VerifierTranscript; +use jolt_verifier::{jolt_protocol_id, validate_inputs, JoltSponge, ProofHeader, JOLT_SESSION}; +use jolt_verifier_fuzz::Bundle; use libfuzzer_sys::fuzz_target; -type Preprocessing = JoltVerifierPreprocessing>; -type Proof = JoltProof>; -type Bundle = (Preprocessing, JoltDevice, Proof, Option); - static FIXTURE: &[u8] = include_bytes!("../fixtures/muldiv-bundle.bin"); fn bundle() -> &'static Bundle { static BUNDLE: OnceLock = OnceLock::new(); - BUNDLE.get_or_init(|| { - let (bundle, _): (Bundle, usize) = - bincode::serde::decode_from_slice(FIXTURE, bincode::config::standard()) - .expect("fixture decodes"); - bundle - }) + BUNDLE.get_or_init(|| Bundle::decode(FIXTURE)) } fuzz_target!(|data: &[u8]| { - if data.len() < 18 { + if data.len() < 2 { return; } - let (preprocessing, public_io, proof, _) = bundle(); - - // Fuzzer-chosen dimensions, drawn from the header bytes. - let trace_length = u64::from_le_bytes(data[0..8].try_into().unwrap()) as usize; - let ram_k = u64::from_le_bytes(data[8..16].try_into().unwrap()) as usize; - let trusted_present = data[16] & 1 == 1; - let untrusted_present = data[16] & 2 == 2; - let zk = data[16] & 4 == 4; - - // Fuzzer-chosen public I/O sizes, bounded so the harness itself does not - // allocate unreasonably; the validator enforces the real limits. - let input_len = (data[17] as usize) * 64; - let mut io = public_io.clone(); - io.inputs = vec![0u8; input_len.min(1 << 16)]; - io.outputs = data[18..].to_vec(); - - let _ = validate_inputs_from_parts::>( - preprocessing, - &io, - trace_length, - ram_k, - proof.trace_polynomial_order, - proof.one_hot_config, - trusted_present, - untrusted_present, - zk, + let bundle = bundle(); + let trusted_advice_present = data[0] & 1 == 1; + let narg = &data[2..]; + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + narg, + ); + let Ok(header) = ProofHeader::receive(&mut transcript) else { + return; + }; + let header_len = narg.len() - transcript.remaining(); + + let mut public_io = bundle.public_io.clone(); + public_io.inputs = vec![0u8; data[1] as usize * 64]; + public_io.outputs = narg[header_len..].to_vec(); + + let _ = validate_inputs::>( + &bundle.preprocessing, + &public_io, + &header, + trusted_advice_present, ); }); diff --git a/crates/jolt-verifier/fuzz/fuzz_targets/zk_proof_tamper_must_reject.rs b/crates/jolt-verifier/fuzz/fuzz_targets/zk_proof_tamper_must_reject.rs index 77cfe63428..783a0f35a0 100644 --- a/crates/jolt-verifier/fuzz/fuzz_targets/zk_proof_tamper_must_reject.rs +++ b/crates/jolt-verifier/fuzz/fuzz_targets/zk_proof_tamper_must_reject.rs @@ -1,29 +1,20 @@ #![no_main] -//! Structured mutation of accepted BlindFold proofs. +//! Argument-string mutation of accepted BlindFold proofs. //! -//! The fixtures are generated by the real ZK prover and include trusted and -//! untrusted advice plus a committed program. Every non-skipped input -//! changes one committed-sumcheck, final-opening, BlindFold, program-binding, -//! or advice component and requires the compiled ZK verifier to reject it. +//! The fixtures are generated by the real ZK prover and cover a plain program, +//! a committed program, and trusted plus untrusted advice. Each input applies +//! exactly one mutation to the argument string (committed sumcheck rounds, +//! the Dory opening, and the BlindFold proof all live there) or to the public +//! statement; see `jolt_verifier_fuzz::tamper_must_reject` for the input +//! layout. Every mutation that changes the verifier's input must be rejected +//! by the compiled ZK verifier. use std::sync::OnceLock; -use common::jolt_device::JoltDevice; -use jolt_crypto::{Bn254G1, Pedersen}; -use jolt_dory::{DoryCommitment, DoryScheme}; -use jolt_field::{CanonicalEncoding, Fr, Ring}; -use jolt_poly::CompressedPoly; -use jolt_sumcheck::{CompressedSumcheckProof, SumcheckProof}; -use jolt_transcript::LegacyBlake2bTranscript; -use jolt_verifier::{verify, JoltProof, JoltProofClaims, JoltVerifierPreprocessing, ZkConfig}; +use jolt_verifier_fuzz::{tamper_must_reject, Bundle}; use libfuzzer_sys::fuzz_target; -type Preprocessing = JoltVerifierPreprocessing>; -type Proof = JoltProof>; -type Bundle = (Preprocessing, JoltDevice, Proof, Option); -type StageProof = SumcheckProof; - static FIXTURES: [&[u8]; 3] = [ include_bytes!("../fixtures/zk-muldiv-bundle.bin"), include_bytes!("../fixtures/zk-committed-muldiv-bundle.bin"), @@ -35,240 +26,11 @@ fn bundles() -> &'static [Bundle] { BUNDLES.get_or_init(|| { FIXTURES .iter() - .map(|bytes| { - let (bundle, consumed): (Bundle, usize) = - bincode::serde::decode_from_slice(bytes, bincode::config::standard()) - .expect("fixture decodes"); - assert_eq!(consumed, bytes.len(), "fixture has trailing bytes"); - let (preprocessing, public_io, proof, advice) = &bundle; - verify::, LegacyBlake2bTranscript>( - preprocessing, - public_io, - proof, - advice.as_ref(), - ) - .expect("honest ZK fixture proof must verify before tampering"); - bundle - }) + .map(|bytes| Bundle::decode_verified(bytes)) .collect() }) } -fn stage_proof(proof: &mut Proof, index: usize) -> &mut StageProof { - match index % 10 { - 0 => &mut proof.stages.stage1_uni_skip_first_round_proof, - 1 => &mut proof.stages.stage1_sumcheck_proof, - 2 => &mut proof.stages.stage2_uni_skip_first_round_proof, - 3 => &mut proof.stages.stage2_sumcheck_proof, - 4 => &mut proof.stages.stage3_sumcheck_proof, - 5 => &mut proof.stages.stage4_sumcheck_proof, - 6 => &mut proof.stages.stage5_sumcheck_proof, - 7 => &mut proof.stages.stage6a_sumcheck_proof, - 8 => &mut proof.stages.stage6b_sumcheck_proof, - _ => &mut proof.stages.stage7_sumcheck_proof, - } -} - -fn mutate_committed_sumcheck(proof: &mut StageProof, operation: u8, index: usize) -> bool { - let SumcheckProof::Committed(committed) = proof else { - return false; - }; - match operation % 5 { - 0 => committed.rounds.pop().is_some(), - 1 => { - let len = committed.rounds.len(); - if len == 0 { - return false; - } - let round = &mut committed.rounds[index % len]; - round.degree = round.degree.saturating_add(1024); - true - } - 2 => { - let len = committed.rounds.len(); - if len == 0 { - return false; - } - let round = &mut committed.rounds[index % len]; - let before = round.commitment; - round.commitment = Bn254G1::default(); - round.commitment != before - } - 3 => committed.output_claims.commitments.pop().is_some(), - _ => { - let len = committed.output_claims.commitments.len(); - if len == 0 { - return false; - } - let commitment = &mut committed.output_claims.commitments[index % len]; - let before = *commitment; - *commitment = Bn254G1::default(); - *commitment != before - } - } -} - -fn mutate_compressed( - proof: &mut CompressedSumcheckProof, - operation: u8, - index: usize, - delta: Fr, -) -> bool { - match operation % 2 { - 0 => proof.round_polynomials.pop().is_some(), - _ => { - if proof.round_polynomials.is_empty() { - return false; - } - let round = index % proof.round_polynomials.len(); - let mut coefficients = proof.round_polynomials[round] - .coeffs_except_linear_term() - .to_vec(); - let coefficient = operation as usize / 2 % coefficients.len(); - coefficients[coefficient] += delta; - proof.round_polynomials[round] = CompressedPoly::new(coefficients); - true - } - } -} - fuzz_target!(|data: &[u8]| { - if data.len() < 5 { - return; - } - let fixture = &bundles()[data[0] as usize % bundles().len()]; - let mut preprocessing = fixture.0.clone(); - let mut public_io = fixture.1.clone(); - let mut proof = fixture.2.clone(); - let mut advice = fixture.3.clone(); - let mutation = data[1] % 16; - let stage = data[2] as usize; - let operation = data[3]; - let index = data[4] as usize; - let mut delta = Fr::from_bytes_le_reduced(&data[5..data.len().min(37)]); - if delta == Fr::from_u64(0) { - delta = Fr::from_u64(1); - } - - let changed = match mutation { - // Shape guard, not a soundness probe: ZK claims under a Transparent - // header are rejected by `validate_proof_consistency` before any - // crypto runs. - 0 => { - proof.protocol.zk = ZkConfig::Transparent; - true - } - // Legal-but-wrong dimensions: still powers of two, so they pass - // `validate_inputs` and must be rejected by the stage round - // structure rather than a typed shape guard. - 1 => { - proof.trace_length *= 2; - true - } - 2 => { - proof.ram_K *= 2; - true - } - 3 => { - preprocessing.vc_setup = None; - true - } - 4 => mutate_committed_sumcheck(stage_proof(&mut proof, stage), operation, index), - 5 => { - proof.joint_opening_proof.0.y_com = None; - true - } - 6 => { - let JoltProofClaims::Zk { blindfold_proof } = &mut proof.claims else { - return; - }; - blindfold_proof.random_u += delta; - true - } - 7 => { - let JoltProofClaims::Zk { blindfold_proof } = &mut proof.claims else { - return; - }; - mutate_compressed(&mut blindfold_proof.outer_sumcheck, operation, index, delta) - } - 8 => { - let JoltProofClaims::Zk { blindfold_proof } = &mut proof.claims else { - return; - }; - mutate_compressed(&mut blindfold_proof.inner_sumcheck, operation, index, delta) - } - 9 => { - let JoltProofClaims::Zk { blindfold_proof } = &mut proof.claims else { - return; - }; - blindfold_proof.random_round_commitments.pop().is_some() - } - 10 => { - let JoltProofClaims::Zk { blindfold_proof } = &mut proof.claims else { - return; - }; - blindfold_proof - .random_output_claim_row_commitments - .pop() - .is_some() - } - 11 => { - let JoltProofClaims::Zk { blindfold_proof } = &mut proof.claims else { - return; - }; - let Some(value) = blindfold_proof.folded_eval_outputs.first_mut() else { - return; - }; - *value += delta; - true - } - 12 => { - if public_io.inputs.is_empty() { - return; - } - let position = index % public_io.inputs.len(); - public_io.inputs[position] ^= operation.max(1); - true - } - 13 => { - let before = proof.commitments.rd_inc.clone(); - proof.commitments.rd_inc = DoryCommitment::default(); - proof.commitments.rd_inc != before - } - 14 => { - preprocessing.program = bundles()[(data[0] as usize + 1) % bundles().len()] - .0 - .program - .clone(); - preprocessing.program != fixture.0.program - } - _ => { - if operation % 2 == 0 { - advice = match advice.take() { - Some(_) => None, - None => Some(DoryCommitment::default()), - }; - } else { - proof.untrusted_advice_commitment = match proof.untrusted_advice_commitment.take() { - Some(_) => None, - None => Some(DoryCommitment::default()), - }; - } - true - } - }; - if !changed { - return; - } - - let result = verify::, LegacyBlake2bTranscript>( - &preprocessing, - &public_io, - &proof, - advice.as_ref(), - ); - assert!( - result.is_err(), - "verifier accepted a tampered ZK proof (mutation class {mutation})" - ); + tamper_must_reject(bundles(), data, "ZK"); }); diff --git a/crates/jolt-verifier/fuzz/seeds/proof_deserialize_no_panic/honest_header b/crates/jolt-verifier/fuzz/seeds/proof_deserialize_no_panic/honest_header new file mode 100644 index 0000000000000000000000000000000000000000..7b96db4ed0e481e9636b94169aed0a22c45848d6 GIT binary patch literal 31 YcmZQzVSoSy2*t(A#m*uC6=Prk00u<>VE_OC literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/append b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/append new file mode 100644 index 0000000000000000000000000000000000000000..9d5642c1794409accbb3085612149573dcd04411 GIT binary patch literal 7 LcmZQzX8-{J05Jdv literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/clear_claim b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/clear_claim deleted file mode 100644 index c6dcbad7ca..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/clear_claim +++ /dev/null @@ -1 +0,0 @@ -0:000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/delete b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/delete new file mode 100644 index 0000000000000000000000000000000000000000..a2ee4022a066c1c98c9ddaea2269390d60b4c989 GIT binary patch literal 7 OcmZQzWnf}p-~a#sH2@F* literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/fixture_program_swap b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/fixture_program_swap deleted file mode 100644 index c5cfed3efb..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/fixture_program_swap +++ /dev/null @@ -1 +0,0 @@ -0?000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/header_flip b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/header_flip new file mode 100644 index 0000000000000000000000000000000000000000..3f6751bd91e26d69185fa5e8db21efce2aa723bd GIT binary patch literal 7 OcmZQzWM*JsXaE2JoB)LY literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/heap_size b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/heap_size new file mode 100644 index 0000000000000000000000000000000000000000..4f9e30c313b470b4f012b95d0af58e0647f32e1e GIT binary patch literal 6 LcmZSJWB>vH05bpz literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/insert b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/insert new file mode 100644 index 0000000000000000000000000000000000000000..4b23fa04302d5f28a3107ac8ef9869e08e26a6e7 GIT binary patch literal 7 McmZQ#k!Jt`0077UCIA2c literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/narg_flip b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/narg_flip new file mode 100644 index 0000000000000000000000000000000000000000..230da9907345349d83d37849639917640fdc22c9 GIT binary patch literal 7 OcmZQz5Kw1eU<3dF^Z+>k literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/opening_commitment b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/opening_commitment deleted file mode 100644 index 3f86bbcbab..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/opening_commitment +++ /dev/null @@ -1 +0,0 @@ -0=000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/outer-round-eight b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/outer-round-eight deleted file mode 100644 index b02fdc795269ece3837f64fb0191c58711b05aaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6 NcmZSJVr1fA1^@t602u%P diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/overwrite b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/overwrite new file mode 100644 index 0000000000000000000000000000000000000000..7e57c7d2367292f242fdd45ccefffbdeb75a15c8 GIT binary patch literal 8 PcmZQ%W)NUtxVIJn0^R|j literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/program_swap b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/program_swap new file mode 100644 index 0000000000000000000000000000000000000000..ff6dab747d2fb8a1571be999fce03afa2b500e50 GIT binary patch literal 6 LcmZSJW&i>J06hQ< literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/protocol_mode b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/protocol_mode new file mode 100644 index 0000000000000000000000000000000000000000..392d0c58236552301588a2f0dfa358d48d08f8c7 GIT binary patch literal 6 NcmZSJW@KPs0000%01W^D literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/public_input b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/public_input new file mode 100644 index 0000000000000000000000000000000000000000..875cbc528e21638fdfbdf9edd39b59cb13a152e7 GIT binary patch literal 7 McmZSJU;qL}001}u3IG5A literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/sumcheck_round b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/sumcheck_round deleted file mode 100644 index f230b88285..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/sumcheck_round +++ /dev/null @@ -1 +0,0 @@ -05000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/tail_flip b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/tail_flip new file mode 100644 index 0000000000000000000000000000000000000000..a99b0dadd7345a33a2b44fc9a945e667867c3f0d GIT binary patch literal 7 McmZQzVgLe0000yK1ONa4 literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trace_length b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trace_length deleted file mode 100644 index ecec880228..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trace_length +++ /dev/null @@ -1 +0,0 @@ -00000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/truncate b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/truncate new file mode 100644 index 0000000000000000000000000000000000000000..bcf0eb9cc6914059fffb3305f26722caeb814b91 GIT binary patch literal 6 NcmZQz`~RPT0RRUb0tEm7 literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trusted_advice b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trusted_advice new file mode 100644 index 0000000000000000000000000000000000000000..cef4896c8b1067cf1af677cb8ebb985860246b6f GIT binary patch literal 6 LcmZSNVgLdF06qW= literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/validate_inputs_no_panic/valid-ish b/crates/jolt-verifier/fuzz/seeds/validate_inputs_no_panic/valid-ish index 846fe1917da949ee9a96149eed0d782c3c3b8c49..5bb4222c58a6219935a34b59f60a3398dd356f58 100644 GIT binary patch literal 37 ZcmZQzU|?Z@00jue#mmLcA^;VGPyh$Z0ATvH05bpz literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/insert b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/insert new file mode 100644 index 0000000000000000000000000000000000000000..4b23fa04302d5f28a3107ac8ef9869e08e26a6e7 GIT binary patch literal 7 McmZQ#k!Jt`0077UCIA2c literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/narg_flip b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/narg_flip new file mode 100644 index 0000000000000000000000000000000000000000..230da9907345349d83d37849639917640fdc22c9 GIT binary patch literal 7 OcmZQz5Kw1eU<3dF^Z+>k literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/outer-round-eight b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/outer-round-eight deleted file mode 100644 index 6cb31c1e81901e4809350c4ce7d86bbae8522866..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5 McmZQzVPxU}001Na5C8xG diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/overwrite b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/overwrite new file mode 100644 index 0000000000000000000000000000000000000000..7e57c7d2367292f242fdd45ccefffbdeb75a15c8 GIT binary patch literal 8 PcmZQ%W)NUtxVIJn0^R|j literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/program_swap b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/program_swap index 33c1eadbc3fadc78f03a267494c0dff75e492660..ff6dab747d2fb8a1571be999fce03afa2b500e50 100644 GIT binary patch literal 6 LcmZSJW&i>J06hQ< literal 5 McmZSLV_;wa002h-5dZ)H diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/protocol_mode b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/protocol_mode index 191dc569f98a70504e937d7687ccd9ffca2ae364..392d0c58236552301588a2f0dfa358d48d08f8c7 100644 GIT binary patch literal 6 NcmZSJW@KPs0000%01W^D literal 5 McmXreGB7Xz00O-L>i_@% diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/public_input b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/public_input index 230b53fd48d9b0e1d12ec27d58df1326359091bb..875cbc528e21638fdfbdf9edd39b59cb13a152e7 100644 GIT binary patch literal 7 McmZSJU;qL}001}u3IG5A literal 5 McmXpoF)%Oy00QLz^#A|> diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/tail_flip b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/tail_flip new file mode 100644 index 0000000000000000000000000000000000000000..a99b0dadd7345a33a2b44fc9a945e667867c3f0d GIT binary patch literal 7 McmZQzVgLe0000yK1ONa4 literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/truncate b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/truncate new file mode 100644 index 0000000000000000000000000000000000000000..bcf0eb9cc6914059fffb3305f26722caeb814b91 GIT binary patch literal 6 NcmZQz`~RPT0RRUb0tEm7 literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/trusted_advice b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/trusted_advice new file mode 100644 index 0000000000000000000000000000000000000000..c682901b3867e655a708d2c1696eaec9f0070152 GIT binary patch literal 6 LcmZSLVgLdF07L){ literal 0 HcmV?d00001 diff --git a/crates/jolt-verifier/fuzz/src/lib.rs b/crates/jolt-verifier/fuzz/src/lib.rs new file mode 100644 index 0000000000..d1cd6028eb --- /dev/null +++ b/crates/jolt-verifier/fuzz/src/lib.rs @@ -0,0 +1,177 @@ +//! Shared fixture loading and argument-string mutation for the +//! `jolt-verifier` fuzz targets. +//! +//! Fixtures are the bundles `tests/generate_fuzz_fixture.rs` writes: +//! `(preprocessing, public_io, proof, trusted_advice_commitment)` in +//! bincode-serde encoding, with `proof` the NARG `JoltProof`. + +use common::jolt_device::JoltDevice; +use jolt_crypto::{Bn254G1, Pedersen}; +use jolt_dory::{DoryCommitment, DoryScheme}; +use jolt_field::Fr; +use jolt_transcript::VerifierTranscript; +use jolt_verifier::{ + jolt_protocol_id, verify, JoltProof, JoltSponge, JoltVerifierPreprocessing, ProofHeader, + VerifierError, ZkConfig, JOLT_SESSION, +}; + +pub type Preprocessing = JoltVerifierPreprocessing>; + +#[derive(Clone)] +pub struct Bundle { + pub preprocessing: Preprocessing, + pub public_io: JoltDevice, + pub proof: JoltProof, + pub trusted_advice_commitment: Option, +} + +impl Bundle { + /// Decodes a checked-in fixture, rejecting trailing bytes. + pub fn decode(bytes: &[u8]) -> Self { + let ((preprocessing, public_io, proof, trusted_advice_commitment), consumed): ( + (Preprocessing, JoltDevice, JoltProof, Option), + usize, + ) = bincode::serde::decode_from_slice(bytes, bincode::config::standard()) + .expect("fixture decodes"); + assert_eq!(consumed, bytes.len(), "fixture has trailing bytes"); + Self { + preprocessing, + public_io, + proof, + trusted_advice_commitment, + } + } + + /// Decodes a fixture and checks that the compiled verifier accepts it. + pub fn decode_verified(bytes: &[u8]) -> Self { + let bundle = Self::decode(bytes); + bundle + .verify(&bundle.proof) + .expect("honest fixture proof must verify before tampering"); + bundle + } + + pub fn verify(&self, proof: &JoltProof) -> Result<(), VerifierError> { + verify::, JoltSponge>( + &self.preprocessing, + &self.public_io, + proof, + self.trusted_advice_commitment.as_ref(), + ) + } + + /// The byte length of the proof header at the front of the argument + /// string, as the verifier reads it. + pub fn header_len(&self) -> usize { + let narg = &self.proof.narg; + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + narg, + ); + ProofHeader::receive(&mut transcript).expect("honest fixture header decodes"); + narg.len() - transcript.remaining() + } +} + +const TAIL_WINDOW: usize = 4096; +const MAX_DELETE: usize = 64; + +/// Applies one mutation chosen by `data` to an honest bundle and requires the +/// verifier to reject the result whenever it differs from the honest input. +/// +/// Input layout: `data[0]` picks the bundle, `data[1]` the mutation, +/// `data[2..6]` a little-endian position, and `data[6..]` the payload. Most +/// mutations edit the argument string (flip, overwrite, insert, delete, +/// truncate, extend, with header- and tail-anchored flips so the short header +/// and the final opening are reached as often as the commitment block). The +/// rest edit the public statement the verifier binds outside the argument +/// string: public input, memory layout, trusted-advice commitment, program +/// preprocessing, and the declared protocol configuration. +pub fn tamper_must_reject(bundles: &[Bundle], data: &[u8], mode: &str) { + if data.len() < 6 { + return; + } + let bundle_index = data[0] as usize % bundles.len(); + let honest = &bundles[bundle_index]; + let mutation = data[1] % 12; + let position = u32::from_le_bytes([data[2], data[3], data[4], data[5]]) as usize; + let payload = &data[6..]; + let mask = payload.first().copied().unwrap_or(1).max(1); + + let mut statement = honest.clone(); + let narg = &mut statement.proof.narg; + let len = narg.len(); + if len == 0 { + return; + } + let at = position % len; + match mutation { + 0 => narg[at] ^= mask, + 1 => narg[position % honest.header_len()] ^= mask, + 2 => narg[len - 1 - position % len.min(TAIL_WINDOW)] ^= mask, + 3 => { + let end = len.min(at + payload.len()); + narg[at..end].copy_from_slice(&payload[..end - at]); + } + 4 => { + let inserted = if payload.is_empty() { + &[0][..] + } else { + payload + }; + narg.splice(at..at, inserted.iter().copied()); + } + 5 => { + let count = 1 + mask as usize % MAX_DELETE; + narg.drain(at..len.min(at + count)); + } + 6 => narg.truncate(at), + 7 => narg.extend_from_slice(if payload.is_empty() { &[0] } else { payload }), + 8 => { + let inputs = &mut statement.public_io.inputs; + if inputs.is_empty() { + return; + } + let index = position % inputs.len(); + inputs[index] ^= mask; + } + 9 => { + let layout = &mut statement.public_io.memory_layout; + layout.heap_size ^= 1; + } + 10 => { + statement.trusted_advice_commitment = match statement.trusted_advice_commitment { + Some(_) => None, + None => Some(DoryCommitment::default()), + }; + } + _ => { + if position.is_multiple_of(2) { + statement.preprocessing.program = bundles[(bundle_index + 1) % bundles.len()] + .preprocessing + .program + .clone(); + } else { + let protocol = &mut statement.proof.protocol; + protocol.zk = match protocol.zk { + ZkConfig::Transparent => ZkConfig::BlindFold, + ZkConfig::BlindFold => ZkConfig::Transparent, + }; + } + } + } + + let unchanged = statement.proof == honest.proof + && statement.public_io == honest.public_io + && statement.trusted_advice_commitment == honest.trusted_advice_commitment + && statement.preprocessing.program == honest.preprocessing.program; + if unchanged { + return; + } + let result = statement.verify(&statement.proof); + assert!( + result.is_err(), + "verifier accepted a tampered {mode} proof (bundle {bundle_index}, mutation {mutation})" + ); +} diff --git a/crates/jolt-verifier/tests/completeness/zk.rs b/crates/jolt-verifier/tests/completeness/zk.rs index 0087fe32b5..37b094f7c9 100644 --- a/crates/jolt-verifier/tests/completeness/zk.rs +++ b/crates/jolt-verifier/tests/completeness/zk.rs @@ -8,7 +8,9 @@ )] #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use crate::support; +use crate::support::{self, verifier_fixtures::ZkVerifierFixtureCase}; +#[cfg(all(feature = "prover-fixtures", feature = "zk"))] +use jolt_blindfold::BlindFoldProtocol; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_crypto::{Bn254G1, Pedersen}; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] @@ -16,11 +18,11 @@ use jolt_dory::DoryScheme; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] use jolt_field::Fr; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_sumcheck::SumcheckProof; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; +use jolt_transcript::VerifierTranscript; #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_verifier::JoltProofClaims; +use jolt_verifier::{ + jolt_protocol_id, verify_stages, JoltSponge, ProofHeader, VerifiedStages, JOLT_SESSION, +}; #[test] #[cfg(all(feature = "prover-fixtures", feature = "zk"))] @@ -36,173 +38,50 @@ fn zk_committed_muldiv_verifier_proof_is_accepted() { ); } +/// Golden BlindFold shape of the muldiv fixture at trace_length 1024 / +/// ram_K 8192. The header is pinned alongside so a drift here reads as a +/// protocol change rather than a fixture change. #[test] #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn zk_committed_muldiv_blindfold_shape_audit_matches_modular_protocol() { - let case = crate::support::verifier_fixtures::zk_committed_muldiv_case(); - let modular = support::zk_audit::audit_zk_blindfold_protocol_shape::< - Fr, - DoryScheme, - Pedersen, - Blake2bTranscript, - _, - >(&case.preprocessing, &case.public_io, &case.proof, None) - .expect("build modular BlindFold protocol shape"); - let JoltProofClaims::Zk { blindfold_proof } = &case.proof.claims else { - panic!("ZK verifier fixture must carry a BlindFold proof"); - }; - let proof_shape = blindfold_proof_shape(blindfold_proof); - - assert_eq!( - proof_shape.random_round_commitment_rows, - modular.coefficient_rows - ); - assert_eq!( - proof_shape.random_output_claim_rows, - modular.output_claim_rows - ); - assert_eq!( - proof_shape.random_eval_commitments, - modular.eval_commitments - ); - assert_eq!(proof_shape.auxiliary_rows, modular.auxiliary_rows); - assert_eq!(proof_shape.random_auxiliary_rows, modular.auxiliary_rows); - assert_eq!(proof_shape.random_error_rows, modular.error_row_count); - assert_eq!(proof_shape.cross_term_error_rows, modular.error_row_count); -} - -#[test] -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn zk_muldiv_blindfold_shape_audit_matches_modular_protocol() { +fn zk_muldiv_blindfold_shape_matches_golden() { let case = crate::support::verifier_fixtures::zk_muldiv_case(); - let modular = support::zk_audit::audit_zk_blindfold_protocol_shape::< - Fr, - DoryScheme, - Pedersen, - Blake2bTranscript, - _, - >(&case.preprocessing, &case.public_io, &case.proof, None) - .expect("build modular BlindFold protocol shape"); - let JoltProofClaims::Zk { blindfold_proof } = &case.proof.claims else { - panic!("ZK verifier fixture must carry a BlindFold proof"); - }; - let proof_shape = blindfold_proof_shape(blindfold_proof); - let committed_round_rows = - committed_round_rows(&case.proof.stages.stage1_uni_skip_first_round_proof) - + committed_round_rows(&case.proof.stages.stage1_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage2_uni_skip_first_round_proof) - + committed_round_rows(&case.proof.stages.stage2_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage3_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage4_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage5_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage6a_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage6b_sumcheck_proof) - + committed_round_rows(&case.proof.stages.stage7_sumcheck_proof); - let committed_output_claim_rows = - committed_output_claim_rows(&case.proof.stages.stage1_uni_skip_first_round_proof) - + committed_output_claim_rows(&case.proof.stages.stage1_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage2_uni_skip_first_round_proof) - + committed_output_claim_rows(&case.proof.stages.stage2_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage3_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage4_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage5_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage6a_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage6b_sumcheck_proof) - + committed_output_claim_rows(&case.proof.stages.stage7_sumcheck_proof); - - assert_eq!(committed_round_rows, modular.coefficient_rows); - assert_eq!( - proof_shape.random_round_commitment_rows, - modular.coefficient_rows - ); - assert_eq!(committed_output_claim_rows, modular.output_claim_rows); - assert_eq!( - proof_shape.random_output_claim_rows, - modular.output_claim_rows - ); - assert_eq!( - proof_shape.random_eval_commitments, - modular.eval_commitments + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + &case.proof.narg, ); - assert_eq!(proof_shape.auxiliary_rows, modular.auxiliary_rows); - assert_eq!(proof_shape.random_auxiliary_rows, modular.auxiliary_rows); - assert_eq!(proof_shape.random_error_rows, modular.error_row_count); - assert_eq!(proof_shape.cross_term_error_rows, modular.error_row_count); - assert_eq!( - proof_shape.folded_eval_output_openings, - modular.eval_commitments - ); - assert_eq!( - proof_shape.folded_eval_blinding_openings, - modular.eval_commitments - ); - - // Golden shape of the muldiv fixture at trace_length 1024 / ram_K 8192. - // The config is pinned alongside so a drift here reads as a protocol - // change rather than a fixture change. - assert_eq!(case.proof.trace_length, 1024); - assert_eq!(case.proof.ram_K, 8192); - assert_eq!(modular.coefficient_rows, 227); - assert_eq!(modular.output_claim_rows, 16); - assert_eq!(modular.eval_commitments, 1); - assert_eq!(modular.auxiliary_rows, 37); - assert_eq!(modular.error_row_count, 64); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct BlindFoldProofShape { - auxiliary_rows: usize, - random_round_commitment_rows: usize, - random_output_claim_rows: usize, - random_auxiliary_rows: usize, - random_error_rows: usize, - random_eval_commitments: usize, - cross_term_error_rows: usize, - folded_eval_output_openings: usize, - folded_eval_blinding_openings: usize, -} + let header = ProofHeader::receive(&mut transcript).expect("decode proof header"); + assert_eq!(header.trace_length, 1024); + assert_eq!(header.ram_K, 8192); -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn blindfold_proof_shape( - proof: &jolt_blindfold::BlindFoldProof, -) -> BlindFoldProofShape { - BlindFoldProofShape { - auxiliary_rows: proof.auxiliary_row_commitments.len(), - random_round_commitment_rows: proof.random_round_commitments.len(), - random_output_claim_rows: proof.random_output_claim_row_commitments.len(), - random_auxiliary_rows: proof.random_auxiliary_row_commitments.len(), - random_error_rows: proof.random_error_row_commitments.len(), - random_eval_commitments: proof.random_eval_commitments.len(), - cross_term_error_rows: proof.cross_term_error_row_commitments.len(), - folded_eval_output_openings: proof.folded_eval_output_openings.len(), - folded_eval_blinding_openings: proof.folded_eval_blinding_openings.len(), - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn committed_round_rows(proof: &SumcheckProof) -> usize -where - F: jolt_field::JoltField, -{ - proof - .as_committed() - .expect("ZK verifier fixture must use committed sumcheck proofs") - .rounds - .len() + let protocol = blindfold_protocol(&case); + assert_eq!(protocol.dimensions.coefficient_rows, 227); + assert_eq!(protocol.dimensions.output_claim_rows, 16); + assert_eq!(protocol.dimensions.auxiliary_rows, 37); + assert_eq!(protocol.dimensions.error.row_count, 64); + assert_eq!(protocol.eval_commitments.len(), 1); } +/// Replays the stage spine over the fixture's argument string and returns +/// the BlindFold protocol its committed outputs lower to. #[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn committed_output_claim_rows(proof: &SumcheckProof) -> usize -where - F: jolt_field::JoltField, -{ - proof - .as_committed() - .expect("ZK verifier fixture must use committed sumcheck proofs") - .output_claims - .commitments - .len() +fn blindfold_protocol(case: &ZkVerifierFixtureCase) -> BlindFoldProtocol { + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + &case.proof.narg, + ); + let stages = verify_stages::, JoltSponge>( + &case.preprocessing, + &case.public_io, + case.trusted_advice_commitment.as_ref(), + &mut transcript, + ) + .expect("stage spine accepts the honest ZK fixture"); + let VerifiedStages::Zk(protocol) = stages else { + panic!("ZK verifier fixture must lower to a BlindFold protocol"); + }; + *protocol } #[test] diff --git a/crates/jolt-verifier/tests/generate_fuzz_fixture.rs b/crates/jolt-verifier/tests/generate_fuzz_fixture.rs index 13872d774d..c96cc01ad7 100644 --- a/crates/jolt-verifier/tests/generate_fuzz_fixture.rs +++ b/crates/jolt-verifier/tests/generate_fuzz_fixture.rs @@ -2,9 +2,13 @@ //! fuzz workspace. Transparent and ZK builds generate their own protocol- //! compatible fixtures. //! -//! Run explicitly and commit the output: +//! Each bundle is `(preprocessing, public_io, proof, trusted_advice_commitment)` +//! in bincode-serde encoding, with `proof` the NARG `JoltProof`. Run both +//! builds explicitly and commit the output: //! `cargo nextest run -p jolt-verifier --features prover-fixtures \ //! --test generate_fuzz_fixture --run-ignored ignored-only` +//! `cargo nextest run -p jolt-verifier --features prover-fixtures,zk \ +//! --test generate_fuzz_fixture --run-ignored ignored-only` #![cfg(all( feature = "prover-fixtures", @@ -24,17 +28,15 @@ use common::jolt_device::JoltDevice; use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::{DoryCommitment, DoryScheme}; use jolt_field::Fr; -use jolt_transcript::LegacyBlake2bTranscript; -use jolt_verifier::{JoltProof, JoltVerifierPreprocessing}; +use jolt_verifier::{JoltProof, JoltSponge, JoltVerifierPreprocessing}; mod support; type FuzzPreprocessing = JoltVerifierPreprocessing>; -type FuzzProof = JoltProof>; type FuzzBundle = ( FuzzPreprocessing, JoltDevice, - FuzzProof, + JoltProof, Option, ); @@ -134,7 +136,7 @@ fn write_bundle(filename: &str, bundle: FuzzBundle, zk: bool) { assert_eq!(consumed, bytes.len(), "fuzz bundle has trailing bytes"); support::assert_accepts_mode( zk, - jolt_verifier::verify::, LegacyBlake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( &decoded.0, &decoded.1, &decoded.2, diff --git a/crates/jolt-verifier/tests/support/akita_fixtures.rs b/crates/jolt-verifier/tests/support/akita_fixtures.rs index 87668184d9..d5b8d175b9 100644 --- a/crates/jolt-verifier/tests/support/akita_fixtures.rs +++ b/crates/jolt-verifier/tests/support/akita_fixtures.rs @@ -10,22 +10,20 @@ use std::sync::OnceLock; use common::jolt_device::JoltDevice; use jolt_akita::{AkitaCommitment, AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_host::Program; -use jolt_prover::akita::preprocessing::{self, AkitaProverPreprocessing, AkitaTranscript, AkitaVc}; +use jolt_prover::akita::preprocessing::{self, AkitaProverPreprocessing, AkitaVc}; use jolt_prover::akita::{self, JoltAkitaBackend}; use jolt_prover::ProverConfig; use jolt_verifier::proof::JoltProof; -use jolt_verifier::{verify, JoltVerifierPreprocessing, VerifierError}; +use jolt_verifier::{verify, JoltSponge, JoltVerifierPreprocessing, VerifierError}; use super::guest_fixtures::{fixture_witness, prepare_guest, PreparedGuest}; const MAX_PADDED_TRACE_LENGTH: usize = 1 << 16; -pub type AkitaJoltProof = JoltProof; - pub struct AkitaFixtureCase { pub preprocessing: JoltVerifierPreprocessing, pub public_io: JoltDevice, - pub proof: AkitaJoltProof, + pub proof: JoltProof, pub trusted_advice_commitment: Option, } @@ -34,8 +32,8 @@ impl AkitaFixtureCase { self.verify_proof(&self.proof) } - pub fn verify_proof(&self, proof: &AkitaJoltProof) -> Result<(), VerifierError> { - verify::( + pub fn verify_proof(&self, proof: &JoltProof) -> Result<(), VerifierError> { + verify::( &self.preprocessing, &self.public_io, proof, @@ -160,7 +158,7 @@ fn prove_prepared( preprocessing::commit_trusted_advice(&preprocessing, trusted_advice) .expect("trusted advice commitment") }); - let proof = akita::prove::( + let proof = akita::prove::( &JoltAkitaBackend::optimized(), &preprocessing, &config, @@ -200,9 +198,10 @@ mod field_inline { ExecutionBackend, JoltProgram, OwnedTrace, TraceInputs, TraceOutput, TraceRow, }; use jolt_program::preprocess::JoltProgramPreprocessing; - use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; + use jolt_prover::akita::preprocessing::AkitaVc; use jolt_prover::akita::JoltAkitaBackend; use jolt_prover::{akita, ProverConfig}; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use tracer::execution_backend::TracerBackend; @@ -309,7 +308,7 @@ mod field_inline { ) .with_field_inline() .expect("field-inline witness view"); - let proof = akita::prove::( + let proof = akita::prove::( &JoltAkitaBackend::optimized(), &prover_preprocessing, &config, diff --git a/crates/jolt-verifier/tests/support/mod.rs b/crates/jolt-verifier/tests/support/mod.rs index 1bfe09787a..8ceea89d48 100644 --- a/crates/jolt-verifier/tests/support/mod.rs +++ b/crates/jolt-verifier/tests/support/mod.rs @@ -54,18 +54,28 @@ pub fn assert_zk_rejects(result: Result<(), VerifierError>) { } #[cfg(all(feature = "prover-fixtures", feature = "akita"))] pub mod akita_fixtures; -#[cfg(feature = "fs-audit")] -pub mod fs_mutations; -#[cfg(feature = "fs-audit")] -pub mod fs_transcript; #[cfg(feature = "prover-fixtures")] pub mod guest_fixtures; -#[cfg(feature = "prover-fixtures")] -pub mod proof_claims; -pub mod tamper_manifest; #[cfg(all(feature = "prover-fixtures", not(feature = "akita")))] pub mod verifier_fixtures; -#[cfg(feature = "zk")] + +// TEMPORARY: these modules still target the pre-NARG proof struct and the +// removed `fs-audit` feature. `cfg(any())` keeps them out of every build until +// the NARG tamper/FS port restores their real gates (`fs-audit` becomes +// `logging`): +// fs_mutations, fs_transcript: feature = "fs-audit" +// proof_claims: feature = "prover-fixtures" +// tamper_manifest: always +// zk_audit: feature = "zk" +#[cfg(any())] +pub mod fs_mutations; +#[cfg(any())] +pub mod fs_transcript; +#[cfg(any())] +pub mod proof_claims; +#[cfg(any())] +pub mod tamper_manifest; +#[cfg(any())] pub mod zk_audit; use jolt_verifier::VerifierError; diff --git a/crates/jolt-verifier/tests/support/verifier_fixtures.rs b/crates/jolt-verifier/tests/support/verifier_fixtures.rs index df4a257360..7b972a8dac 100644 --- a/crates/jolt-verifier/tests/support/verifier_fixtures.rs +++ b/crates/jolt-verifier/tests/support/verifier_fixtures.rs @@ -24,15 +24,14 @@ use jolt_host::Program; use jolt_program::execution::{JoltProgram, TraceOutput}; use jolt_prover::dory::DoryProverPreprocessing; use jolt_prover::{JoltBackend, JoltSharedPreprocessing, ProverConfig}; -use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; -use jolt_verifier::{verify, JoltProof, JoltVerifierPreprocessing, VerifierError}; +use jolt_verifier::{verify, JoltProof, JoltSponge, JoltVerifierPreprocessing, VerifierError}; use super::guest_fixtures::{fixture_witness, prepare_guest, PreparedGuest}; static VERIFIER_FIXTURE_LOCK: Mutex<()> = Mutex::new(()); -// Program digests derive from the serde encoding (`ProgramPreprocessing::digest`); -// fixtures carrying the legacy digest layout must regenerate. -const FIXTURE_MAGIC: &[u8; 8] = b"JVCF0005"; +// Bumped whenever a cached section's encoding changes: proofs are NARG +// argument strings (`JoltProof { protocol, narg }`) since JVCF0006. +const FIXTURE_MAGIC: &[u8; 8] = b"JVCF0006"; const REGENERATE_ARTIFACTS_ENV: &str = "JOLT_VERIFIER_REGENERATE_VERIFIER_FIXTURES"; const VERIFIER_FIXTURE_LOCK_FILE: &str = "jolt-verifier-fixtures.lock"; @@ -115,7 +114,6 @@ fn lock_exclusive(file: &fs::File) { } } -pub type VerifierFixtureProof = JoltProof>; type VerifierFixturePreprocessing = JoltVerifierPreprocessing>; #[cfg(not(feature = "zk"))] @@ -123,17 +121,23 @@ type VerifierFixturePreprocessing = JoltVerifierPreprocessing, } #[cfg(not(feature = "zk"))] impl VerifierFixtureCase { pub fn verify(&self) -> Result<(), VerifierError> { - verify::, Blake2bTranscript>( + self.verify_proof(&self.proof) + } + + /// Verifies `proof` against this case's preprocessing, public I/O, and + /// trusted-advice commitment. + pub fn verify_proof(&self, proof: &JoltProof) -> Result<(), VerifierError> { + verify::, JoltSponge>( &self.preprocessing, &self.public_io, - &self.proof, + proof, self.trusted_advice_commitment.as_ref(), ) } @@ -144,17 +148,23 @@ impl VerifierFixtureCase { pub struct ZkVerifierFixtureCase { pub preprocessing: VerifierFixturePreprocessing, pub public_io: JoltDevice, - pub proof: VerifierFixtureProof, + pub proof: JoltProof, pub trusted_advice_commitment: Option, } #[cfg(feature = "zk")] impl ZkVerifierFixtureCase { pub fn verify(&self) -> Result<(), VerifierError> { - verify::, Blake2bTranscript>( + self.verify_proof(&self.proof) + } + + /// Verifies `proof` against this case's preprocessing, public I/O, and + /// trusted-advice commitment. + pub fn verify_proof(&self, proof: &JoltProof) -> Result<(), VerifierError> { + verify::, JoltSponge>( &self.preprocessing, &self.public_io, - &self.proof, + proof, self.trusted_advice_commitment.as_ref(), ) } @@ -334,7 +344,7 @@ fn case_from_parts( struct GeneratedVerifierFixture { preprocessing: VerifierFixturePreprocessing, public_io: JoltDevice, - proof: VerifierFixtureProof, + proof: JoltProof, trusted_advice_commitment: Option, } @@ -533,10 +543,10 @@ fn deserialize_verifier_object(bytes: &[u8]) -> fn assert_verifier_accepts( fixture: &GeneratedVerifierFixture, - proof: VerifierFixtureProof, + proof: JoltProof, public_io: JoltDevice, ) { - let result = verify::, Blake2bTranscript>( + let result = verify::, JoltSponge>( &fixture.preprocessing, &public_io, &proof, @@ -751,16 +761,15 @@ fn prove_prepared( jolt_prover::dory::commit_trusted_advice(&preprocessing, trusted_advice) .expect("trusted advice commitment") }); - let proof = - jolt_prover::dory::prove::, Blake2bTranscript, _>( - &JoltBackend::optimized(), - &preprocessing, - &config, - trusted.as_ref(), - &witness, - &public_io, - ) - .expect("prove verifier fixture"); + let proof = jolt_prover::dory::prove::, JoltSponge, _>( + &JoltBackend::optimized(), + &preprocessing, + &config, + trusted.as_ref(), + &witness, + &public_io, + ) + .expect("prove verifier fixture"); GeneratedVerifierFixture { preprocessing: preprocessing.verifier, public_io, @@ -781,7 +790,7 @@ mod field_inline { ExecutionBackend, JoltProgram, OwnedTrace, TraceInputs, TraceOutput, TraceRow, }; use jolt_prover::{JoltBackend, ProverConfig}; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use tracer::execution_backend::TracerBackend; @@ -866,7 +875,7 @@ mod field_inline { let witness = Arc::new(witness); let backend = JoltBackend::::reference(); - let proof = jolt_prover::prove::, Blake2bTranscript, _>( + let proof = jolt_prover::prove::, JoltSponge, _>( &backend, &prover_preprocessing, &config, From a35dad23c55fb34a09e7d5cba149fdda1a423a39 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 16:34:29 -0400 Subject: [PATCH 28/62] test(verifier): replay frozen challenges through an audit sponge 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`. --- .github/workflows/fs-soundness.yml | 6 +- book/src/dev/testing-gates.md | 17 +- crates/jolt-verifier/tests/fs_attacks.rs | 719 ++++++++---------- .../tests/support/fs_mutations.rs | 363 ++++++--- .../tests/support/fs_transcript.rs | 454 +++++------ scripts/ci/fs-soundness.sh | 6 +- 6 files changed, 771 insertions(+), 794 deletions(-) diff --git a/.github/workflows/fs-soundness.yml b/.github/workflows/fs-soundness.yml index b778a51004..69e9eaa9b5 100644 --- a/.github/workflows/fs-soundness.yml +++ b/.github/workflows/fs-soundness.yml @@ -75,11 +75,11 @@ jobs: matrix: include: - instance: dory-clear - features: fs-audit,prover-fixtures + features: logging,prover-fixtures - instance: dory-zk - features: fs-audit,prover-fixtures,zk + features: logging,prover-fixtures,zk - instance: akita-clear - features: akita,fs-audit,prover-fixtures + features: akita,logging,prover-fixtures steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2 diff --git a/book/src/dev/testing-gates.md b/book/src/dev/testing-gates.md index 611d45a2e7..b4c54be689 100644 --- a/book/src/dev/testing-gates.md +++ b/book/src/dev/testing-gates.md @@ -55,12 +55,15 @@ soon as that verifier combination becomes supported. An attack test is valid only when all four steps hold: -1. The original fixture verifies and records typed challenge calls. -2. A coordinated mutation makes an individual protocol claim false. -3. Verification with the recorded challenges replayed succeeds. This proves the - mutation preserves the verifier's algebraic checks when Fiat-Shamir binding - is removed. -4. Verification with the production transcript changes a relevant challenge and +1. The original fixture verifies on a recording sponge, which saves every + squeezed challenge byte. +2. A coordinated mutation of the argument string or the statement makes an + individual protocol claim false. Messages are located through the + `logging` transcript event log. +3. Verification with the recorded challenge bytes replayed succeeds and + consumes the whole recording. This proves the mutation preserves the + verifier's algebraic checks when Fiat-Shamir binding is removed. +4. Verification with the production transcript changes a challenge and rejects. Production acceptance is a soundness finding. Frozen-challenge rejection is a @@ -83,7 +86,7 @@ regenerate the inventories with: ```bash JOLT_FS_BLESS=1 cargo nextest run -p jolt-verifier \ - --test fs_obligations --features fs-audit --cargo-quiet + --test fs_obligations --cargo-quiet ``` Review the resulting diff as a list of new or removed security obligations. diff --git a/crates/jolt-verifier/tests/fs_attacks.rs b/crates/jolt-verifier/tests/fs_attacks.rs index 0847c7aafd..2731776fed 100644 --- a/crates/jolt-verifier/tests/fs_attacks.rs +++ b/crates/jolt-verifier/tests/fs_attacks.rs @@ -1,4 +1,15 @@ -#![cfg(all(feature = "fs-audit", feature = "prover-fixtures"))] +//! Live Fiat-Shamir attacks: each test shows one transcript binding is +//! load-bearing. +//! +//! An attack is valid only when all four steps hold: +//! 1. the honest fixture verifies and its squeeze stream is recorded; +//! 2. a coordinated mutation of the argument string (or statement) makes an +//! individual protocol claim false; +//! 3. verification with the recorded squeezes replayed accepts, so the +//! mutation survives every algebraic check once the binding is removed; +//! 4. live verification changes the squeeze stream and rejects. + +#![cfg(all(feature = "logging", feature = "prover-fixtures"))] #![expect( dead_code, reason = "the shared support module is compiled into every integration-test target but only partially used per feature configuration" @@ -6,441 +17,337 @@ mod support; -#[cfg(not(feature = "akita"))] -use support::fs_mutations::cancel_dory_final_opening_commitments; -#[cfg(not(feature = "zk"))] -use support::fs_mutations::equivocate_stage1_clear; -use support::fs_transcript::{record_challenges, replay_challenges, AuditTranscript}; +#[path = "support/fs_mutations.rs"] +mod fs_mutations; +#[path = "support/fs_transcript.rs"] +mod fs_transcript; -#[cfg(not(feature = "akita"))] -use jolt_crypto::{Bn254G1, Pedersen}; -#[cfg(not(feature = "akita"))] -use jolt_dory::DoryScheme; -#[cfg(not(feature = "akita"))] -use jolt_field::Fr; -#[cfg(all(not(feature = "akita"), not(feature = "zk")))] -use jolt_field::Ring; -#[cfg(not(feature = "akita"))] -use jolt_transcript::LegacyBlake2bTranscript; - -#[cfg(all(not(feature = "akita"), not(feature = "zk")))] -#[test] -fn dory_clear_preprocessing_digest_requires_fiat_shamir_binding() { - use support::verifier_fixtures::standard_muldiv_case; - - let case = standard_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - honest.is_ok(), - "honest Dory clear fixture rejected: {honest:?}" - ); +use fs_transcript::{record, replay, AuditSponge, ChallengeTape}; +use jolt_verifier::VerifierError; - let mut preprocessing = case.preprocessing.clone(); - preprocessing.preprocessing_digest[0] ^= 1; - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - frozen.output.is_ok(), - "frozen Dory clear verifier rejected the replay attack: {:?}", - frozen.output - ); - assert_eq!(frozen.consumed, frozen.expected); - - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - production.is_err(), - "production Dory clear verifier accepted a proof under different preprocessing" - ); - assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "preprocessing mutation did not alter a production challenge" - ); +/// Step 1: the honest proof verifies on the audit sponge. +fn record_honest(verify: impl FnOnce() -> Result<(), VerifierError>) -> ChallengeTape { + let (honest, tape) = record(verify); + assert!(honest.is_ok(), "honest fixture rejected: {honest:?}"); + tape } -#[cfg(all(not(feature = "akita"), not(feature = "zk")))] -#[test] -fn dory_clear_stage1_sumcheck_requires_fiat_shamir_challenges() { - use support::verifier_fixtures::standard_muldiv_case; - - let case = standard_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - honest.is_ok(), - "honest Dory clear fixture rejected: {honest:?}" - ); - - let mut proof = case.proof.clone(); - equivocate_stage1_clear(&mut proof, &tape, Fr::from_u64(1)); - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &proof, - case.trusted_advice_commitment.as_ref(), - ) - }); +/// Steps 3 and 4 for an attacked verification. +fn assert_binding_load_bearing( + tape: &ChallengeTape, + attacked: impl Fn() -> Result<(), VerifierError>, + attack: &str, +) { + let frozen = replay(tape, &attacked); assert!( frozen.output.is_ok(), - "frozen Dory clear verifier rejected stage-1 equivocation: {:?}", + "frozen-challenge verifier rejected {attack}: {:?}", frozen.output ); - assert_eq!(frozen.consumed, frozen.expected); - - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - production.is_err(), - "production Dory clear verifier accepted stage-1 equivocation" + assert_eq!( + frozen.consumed, frozen.recorded, + "{attack} changed the squeeze schedule under frozen challenges" ); + + let (live, live_tape) = record(&attacked); + assert!(live.is_err(), "production verifier accepted {attack}"); assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "stage-1 equivocation did not alter a production challenge" + tape.first_divergence(&live_tape).is_some(), + "{attack} did not alter a production challenge" ); } -#[cfg(all(not(feature = "akita"), not(feature = "zk")))] -#[test] -fn dory_clear_final_opening_batch_requires_commitment_binding() { - use support::verifier_fixtures::standard_muldiv_case; - - let case = standard_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - honest.is_ok(), - "honest Dory clear fixture rejected: {honest:?}" - ); +mod audit_sponge { + use jolt_transcript::Blake2b512; + use jolt_transcript::DuplexSpongeInterface; - let mut proof = case.proof.clone(); - cancel_dory_final_opening_commitments(&mut proof, &tape); - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - frozen.output.is_ok(), - "frozen Dory clear verifier rejected opening-batch cancellation: {:?}", - frozen.output - ); - assert_eq!(frozen.consumed, frozen.expected); - - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >( - &case.preprocessing, - &case.public_io, - &proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - production.is_err(), - "production Dory clear verifier accepted false individual openings" - ); - assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "commitment cancellation did not alter a production challenge" - ); -} + use super::fs_transcript::{record, replay, AuditSponge}; -#[cfg(all(not(feature = "akita"), feature = "zk"))] -#[test] -fn dory_zk_preprocessing_digest_requires_fiat_shamir_binding() { - use support::verifier_fixtures::zk_muldiv_case; - - let case = zk_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >(&case.preprocessing, &case.public_io, &case.proof, None) - }); - assert!( - honest.is_ok(), - "honest Dory ZK fixture rejected: {honest:?}" - ); + fn squeeze(sponge: &mut impl DuplexSpongeInterface) -> [u8; N] { + let mut out = [0u8; N]; + let _ = sponge.squeeze(&mut out); + out + } - let mut preprocessing = case.preprocessing.clone(); - preprocessing.preprocessing_digest[0] ^= 1; - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >(&preprocessing, &case.public_io, &case.proof, None) - }); - assert!( - frozen.output.is_ok(), - "frozen Dory ZK verifier rejected the replay attack: {:?}", - frozen.output - ); - assert_eq!(frozen.consumed, frozen.expected); - - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >(&preprocessing, &case.public_io, &case.proof, None) - }); - assert!( - production.is_err(), - "production Dory ZK verifier accepted a proof under different preprocessing" - ); - assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "preprocessing mutation did not alter a production challenge" - ); -} + #[test] + fn records_the_blake2b_stream() { + let mut reference = Blake2b512::default(); + let _ = reference.absorb(b"statement"); + let expected: [u8; 48] = squeeze(&mut reference); -#[cfg(all(not(feature = "akita"), feature = "zk"))] -#[test] -fn dory_zk_final_opening_batch_requires_commitment_binding() { - use support::verifier_fixtures::zk_muldiv_case; - - let case = zk_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >(&case.preprocessing, &case.public_io, &case.proof, None) - }); - assert!( - honest.is_ok(), - "honest Dory ZK fixture rejected: {honest:?}" - ); + let (squeezed, tape) = record(|| { + let mut sponge = AuditSponge::default(); + let _ = sponge.absorb(b"statement"); + let head: [u8; 16] = squeeze(&mut sponge); + let tail: [u8; 32] = squeeze(&mut sponge); + [head.as_slice(), tail.as_slice()].concat() + }); + assert_eq!(squeezed, expected); + assert_eq!(tape.bytes, expected); + } - let mut proof = case.proof.clone(); - cancel_dory_final_opening_commitments(&mut proof, &tape); - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >(&case.preprocessing, &case.public_io, &proof, None) - }); - assert!( - frozen.output.is_ok(), - "frozen Dory ZK verifier rejected opening-batch cancellation: {:?}", - frozen.output - ); - assert_eq!(frozen.consumed, frozen.expected); - - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::< - Fr, - DoryScheme, - Pedersen, - AuditTranscript>, - >(&case.preprocessing, &case.public_io, &proof, None) - }); - assert!( - production.is_err(), - "production Dory ZK verifier accepted false individual openings" - ); - assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "commitment cancellation did not alter a production challenge" - ); + /// A clone continues the stream from its fork point in both modes, so a + /// scout that squeezes ahead on a transcript clone neither duplicates nor + /// shifts the live transcript's tape, and replay answers both from the + /// same offsets regardless of chunking or absorbed data. + #[test] + fn clones_share_one_stream() { + let (scouted, tape) = record(|| { + let mut live = AuditSponge::default(); + let _ = live.absorb(b"statement"); + let mut scout = live.clone(); + let scouted: [u8; 32] = squeeze(&mut scout); + let first: [u8; 16] = squeeze(&mut live); + let second: [u8; 16] = squeeze(&mut live); + assert_eq!([first, second].concat(), scouted); + scouted + }); + assert_eq!(tape.bytes, scouted); + + let replayed = replay(&tape, || { + let mut live = AuditSponge::default(); + let _ = live.absorb(b"different statement"); + let mut scout = live.clone(); + let head: [u8; 8] = squeeze(&mut live); + let scouted: [u8; 32] = squeeze(&mut scout); + let tail: [u8; 24] = squeeze(&mut live); + assert_eq!([head.as_slice(), tail.as_slice()].concat(), scouted); + scouted + }); + assert_eq!(replayed.output, scouted); + assert_eq!(replayed.consumed, replayed.recorded); + } + + #[test] + #[should_panic(expected = "forked audit sponges squeezed different bytes")] + fn diverging_fork_panics_while_recording() { + let _ = record(|| { + let mut live = AuditSponge::default(); + let mut preview = live.clone(); + let _ = preview.absorb(b"candidate nonce"); + let _: [u8; 16] = squeeze(&mut preview); + let _: [u8; 16] = squeeze(&mut live); + }); + } + + #[test] + #[should_panic(expected = "challenge replay exhausted")] + fn replay_past_the_tape_panics() { + let (_, tape) = record(|| squeeze::<16>(&mut AuditSponge::default())); + let _ = replay(&tape, || squeeze::<17>(&mut AuditSponge::default())); + } } -#[cfg(feature = "akita")] -#[test] -fn akita_clear_preprocessing_digest_requires_fiat_shamir_binding() { - use jolt_akita::{AkitaField, AkitaScheme}; - use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; - use support::akita_fixtures::akita_muldiv_case; +#[cfg(not(feature = "akita"))] +mod dory { + use common::jolt_device::JoltDevice; + use jolt_crypto::{Bn254G1, Pedersen}; + use jolt_dory::{DoryCommitment, DoryScheme}; + use jolt_field::Fr; + use jolt_verifier::{ + verify, verify_stages, JoltProof, JoltSponge, JoltVerifierPreprocessing, VerifierError, + }; - let case = akita_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::>( - &case.preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!(honest.is_ok(), "honest Akita fixture rejected: {honest:?}"); - - let mut preprocessing = case.preprocessing.clone(); - preprocessing.preprocessing_digest[0] ^= 1; - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::>( - &preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - frozen.output.is_ok(), - "frozen Akita verifier rejected the replay attack: {:?}", - frozen.output - ); - assert_eq!(frozen.consumed, frozen.expected); + use super::fs_mutations::{cancel_dory_final_opening_commitments, locate_events, LocatedEvent}; + use super::{assert_binding_load_bearing, record_honest, AuditSponge}; - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::>( - &preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - production.is_err(), - "production Akita verifier accepted a proof under different preprocessing" - ); - assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "preprocessing mutation did not alter a production challenge" - ); + type Preprocessing = JoltVerifierPreprocessing>; + + pub(super) struct Statement<'a> { + pub preprocessing: &'a Preprocessing, + pub public_io: &'a JoltDevice, + pub trusted_advice_commitment: Option<&'a DoryCommitment>, + } + + impl Statement<'_> { + pub(super) fn verify(&self, proof: &JoltProof) -> Result<(), VerifierError> { + verify::, AuditSponge>( + self.preprocessing, + self.public_io, + proof, + self.trusted_advice_commitment, + ) + } + + /// The honest proof's events through stage 8 (BlindFold excluded). + pub(super) fn events(&self, proof: &JoltProof) -> Vec { + locate_events(&proof.narg, |transcript| { + verify_stages::, JoltSponge>( + self.preprocessing, + self.public_io, + self.trusted_advice_commitment, + transcript, + ) + .map(drop) + }) + } + } + + pub(super) fn preprocessing_digest_attack(statement: &Statement<'_>, proof: &JoltProof) { + let tape = record_honest(|| statement.verify(proof)); + let mut preprocessing = statement.preprocessing.clone(); + preprocessing.preprocessing_digest[0] ^= 1; + let attacked = Statement { + preprocessing: &preprocessing, + ..*statement + }; + assert_binding_load_bearing( + &tape, + || attacked.verify(proof), + "a proof under different preprocessing", + ); + } + + pub(super) fn final_opening_commitment_attack(statement: &Statement<'_>, proof: &JoltProof) { + let tape = record_honest(|| statement.verify(proof)); + let events = statement.events(proof); + let mut attacked = proof.clone(); + cancel_dory_final_opening_commitments::(&mut attacked.narg, &events, &tape); + assert_binding_load_bearing( + &tape, + || statement.verify(&attacked), + "commitments cancelled in the final-opening batch", + ); + } + + #[cfg(not(feature = "zk"))] + mod clear { + use jolt_field::{Fr, Ring}; + + use super::super::fs_mutations::equivocate_stage1_clear; + use super::super::support::verifier_fixtures::{standard_muldiv_case, VerifierFixtureCase}; + use super::super::{assert_binding_load_bearing, record_honest}; + use super::{final_opening_commitment_attack, preprocessing_digest_attack, Statement}; + + fn statement(case: &VerifierFixtureCase) -> Statement<'_> { + Statement { + preprocessing: &case.preprocessing, + public_io: &case.public_io, + trusted_advice_commitment: case.trusted_advice_commitment.as_ref(), + } + } + + #[test] + fn dory_clear_preprocessing_digest_requires_fiat_shamir_binding() { + let case = standard_muldiv_case(); + preprocessing_digest_attack(&statement(&case), &case.proof); + } + + #[test] + fn dory_clear_stage1_sumcheck_requires_fiat_shamir_challenges() { + let case = standard_muldiv_case(); + let statement = statement(&case); + let tape = record_honest(|| statement.verify(&case.proof)); + let events = statement.events(&case.proof); + let mut attacked = case.proof.clone(); + equivocate_stage1_clear(&mut attacked.narg, &events, &tape, Fr::from_u64(1)); + assert_binding_load_bearing( + &tape, + || statement.verify(&attacked), + "a stage-1 sumcheck equivocation", + ); + } + + #[test] + fn dory_clear_final_opening_batch_requires_commitment_binding() { + let case = standard_muldiv_case(); + final_opening_commitment_attack(&statement(&case), &case.proof); + } + } + + #[cfg(feature = "zk")] + mod zk { + use super::super::support::verifier_fixtures::{zk_muldiv_case, ZkVerifierFixtureCase}; + use super::{final_opening_commitment_attack, preprocessing_digest_attack, Statement}; + + fn statement(case: &ZkVerifierFixtureCase) -> Statement<'_> { + Statement { + preprocessing: &case.preprocessing, + public_io: &case.public_io, + trusted_advice_commitment: case.trusted_advice_commitment.as_ref(), + } + } + + #[test] + fn dory_zk_preprocessing_digest_requires_fiat_shamir_binding() { + let case = zk_muldiv_case(); + preprocessing_digest_attack(&statement(&case), &case.proof); + } + + #[test] + fn dory_zk_final_opening_batch_requires_commitment_binding() { + let case = zk_muldiv_case(); + final_opening_commitment_attack(&statement(&case), &case.proof); + } + } } #[cfg(feature = "akita")] -#[test] -fn akita_clear_stage1_sumcheck_requires_fiat_shamir_challenges() { +mod akita { use jolt_akita::{AkitaField, AkitaScheme}; + use jolt_crypto::Commitment; use jolt_field::Ring; - use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; - use support::akita_fixtures::akita_muldiv_case; + use jolt_prover::akita::preprocessing::AkitaVc; + use jolt_verifier::{ + seed_transcript, stages::stage1, verify, JoltProof, JoltSponge, JoltVerifierPreprocessing, + VerifierError, + }; - let case = akita_muldiv_case(); - let (honest, tape) = record_challenges::(|| { - jolt_verifier::verify::>( - &case.preprocessing, - &case.public_io, - &case.proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!(honest.is_ok(), "honest Akita fixture rejected: {honest:?}"); - - let mut proof = case.proof.clone(); - equivocate_stage1_clear(&mut proof, &tape, AkitaField::from_u64(1)); - let frozen = replay_challenges(&tape, || { - jolt_verifier::verify::>( - &case.preprocessing, - &case.public_io, - &proof, - case.trusted_advice_commitment.as_ref(), - ) - }); - assert!( - frozen.output.is_ok(), - "frozen Akita verifier rejected stage-1 equivocation: {:?}", - frozen.output - ); - assert_eq!(frozen.consumed, frozen.expected); + use super::fs_mutations::{equivocate_stage1_clear, locate_events}; + use super::support::akita_fixtures::{akita_muldiv_case, AkitaFixtureCase}; + use super::{assert_binding_load_bearing, record_honest, AuditSponge}; - let (production, mutated_tape) = record_challenges::(|| { - jolt_verifier::verify::>( - &case.preprocessing, + fn verify_with( + case: &AkitaFixtureCase, + preprocessing: &JoltVerifierPreprocessing, + proof: &JoltProof, + ) -> Result<(), VerifierError> { + verify::( + preprocessing, &case.public_io, - &proof, + proof, case.trusted_advice_commitment.as_ref(), ) - }); - assert!( - production.is_err(), - "production Akita verifier accepted stage-1 equivocation" - ); - assert!( - tape.first_value_divergence(&mutated_tape).is_some(), - "stage-1 equivocation did not alter a production challenge" - ); + } + + #[test] + fn akita_clear_preprocessing_digest_requires_fiat_shamir_binding() { + let case = akita_muldiv_case(); + let tape = record_honest(|| verify_with(case, &case.preprocessing, &case.proof)); + let mut preprocessing = case.preprocessing.clone(); + preprocessing.preprocessing_digest[0] ^= 1; + assert_binding_load_bearing( + &tape, + || verify_with(case, &preprocessing, &case.proof), + "a proof under different preprocessing", + ); + } + + #[test] + fn akita_clear_stage1_sumcheck_requires_fiat_shamir_challenges() { + let case = akita_muldiv_case(); + let tape = record_honest(|| verify_with(case, &case.preprocessing, &case.proof)); + // Stage 1 is the attack's last transcript dependency, so the event log + // stops there. + let events = locate_events(&case.proof.narg, |transcript| { + let seeded = seed_transcript::( + &case.preprocessing, + &case.public_io, + case.trusted_advice_commitment.as_ref(), + transcript, + )?; + stage1::verify::::Output, JoltSponge>( + &seeded.checked, + transcript, + ) + .map(drop) + }); + let mut attacked = case.proof.clone(); + equivocate_stage1_clear(&mut attacked.narg, &events, &tape, AkitaField::from_u64(1)); + assert_binding_load_bearing( + &tape, + || verify_with(case, &case.preprocessing, &attacked), + "a stage-1 sumcheck equivocation", + ); + } } diff --git a/crates/jolt-verifier/tests/support/fs_mutations.rs b/crates/jolt-verifier/tests/support/fs_mutations.rs index aafd92f5da..17b7908a11 100644 --- a/crates/jolt-verifier/tests/support/fs_mutations.rs +++ b/crates/jolt-verifier/tests/support/fs_mutations.rs @@ -1,149 +1,276 @@ +//! Fiat-Shamir attack constructors over a proof's argument string. +//! +//! Each constructor locates its target messages through a `logging` verifier +//! transcript's event log (site, operation, and NARG byte range per message) +//! and reads challenge values from the matching bytes of a recorded +//! [`ChallengeTape`]. The rewritten messages keep every byte length, so the +//! frozen verifier runs the same schedule over the mutated proof. + #![expect( clippy::expect_used, clippy::panic, reason = "attack constructors require a fixture with the exact audited proof shape" )] +use std::{fmt::Debug, ops::Range}; + #[cfg(not(feature = "akita"))] use jolt_crypto::HomomorphicCommitment; -use jolt_crypto::VectorCommitment; -use jolt_field::{Field, Ring}; -use jolt_openings::CommitmentScheme; -use jolt_poly::{CompressedPoly, UnivariatePoly}; -use jolt_r1cs::constraints::jolt::SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE; -use jolt_sumcheck::{CenteredIntegerDomain, ClearProof, SumcheckProof}; -use jolt_verifier::{fs_audit::FsScope, JoltProof, JoltProofClaims}; -use num_traits::One; - -use super::fs_transcript::{ChallengeKind, ChallengeTape}; - -/// Changes two Dory commitments in the kernel of the frozen final-opening RLC. + +use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField}; #[cfg(not(feature = "akita"))] -pub fn cancel_dory_final_opening_commitments( - proof: &mut JoltProof, - tape: &ChallengeTape, -) where - PCS: CommitmentScheme, - PCS::Output: HomomorphicCommitment, - VC: VectorCommitment, -{ - let gamma_base = tape - .records +use jolt_field::{Fr, Ring}; +use jolt_sumcheck::CenteredIntegerDomain; +use jolt_transcript::{SiteId, TranscriptEvent, TranscriptOp, VerifierTranscript}; +use jolt_verifier::{ + jolt_protocol_id, sites, stages::uniskip::UniskipParams, JoltSponge, JOLT_SESSION, +}; + +use crate::fs_transcript::{decode_challenge, decode_small_challenge, ChallengeTape}; + +/// One verifier transcript operation, with the bytes it touched. +#[derive(Clone, Debug)] +pub struct LocatedEvent { + pub site: SiteId, + pub op: TranscriptOp, + /// Argument-string range of a received prover message. + pub narg: Option>, + /// Challenge-tape range of a squeeze. + pub tape: Option>, +} + +/// Runs `verify` over a `logging` transcript on `narg` and returns its events +/// with each squeeze placed in the challenge stream. `verify` must succeed: an +/// attack is located on the honest proof. +pub fn locate_events( + narg: &[u8], + verify: impl FnOnce(&mut VerifierTranscript<'_, JoltSponge>) -> Result<(), E>, +) -> Vec { + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + narg, + ); + verify(&mut transcript).expect("honest proof rejected while locating its transcript events"); + let mut squeezed = 0; + transcript + .events() .iter() - .find(|record| { - record.id.scope == FsScope::Stage8 - && matches!(record.id.kind, ChallengeKind::PowersBase { len } if len >= 2) + .map(|event: &TranscriptEvent| { + let tape = (event.op == TranscriptOp::Challenge).then(|| { + let range = squeezed..squeezed + event.len; + squeezed = range.end; + range + }); + LocatedEvent { + site: event.site, + op: event.op, + narg: event.narg.clone(), + tape, + } }) - .expect("stage-8 final-opening batching challenge is missing"); - let gamma_0 = PCS::Field::one(); - let gamma_1 = gamma_base.values[0]; - let direction = proof - .commitments - .instruction_ra - .first() - .expect("proof has no instruction Ra commitment") - .clone(); - - let original_ram_inc = proof.commitments.ram_inc.clone(); - let original_rd_inc = proof.commitments.rd_inc.clone(); - proof.commitments.ram_inc = - PCS::Output::linear_combine(&original_ram_inc, &direction, &gamma_1); - proof.commitments.rd_inc = PCS::Output::linear_combine(&original_rd_inc, &direction, &-gamma_0); - assert_ne!(proof.commitments.ram_inc, original_ram_inc); - assert_ne!(proof.commitments.rd_inc, original_rd_inc); + .collect() +} + +/// Cursor over one site's events. +struct SiteEvents<'a> { + events: Box + 'a>, + tape: &'a ChallengeTape, +} + +impl<'a> SiteEvents<'a> { + fn new(events: &'a [LocatedEvent], site: SiteId, tape: &'a ChallengeTape) -> Self { + Self { + events: Box::new(events.iter().filter(move |event| event.site == site)), + tape, + } + } + + /// The next event of kind `op`, skipping public absorbs and any event + /// kinds in `skip`. + fn next_skipping( + &mut self, + op: TranscriptOp, + skip: &[TranscriptOp], + what: &str, + ) -> &'a LocatedEvent { + loop { + let event = self + .events + .next() + .unwrap_or_else(|| panic!("{what} is missing")); + if event.op == op { + return event; + } + assert!( + event.op == TranscriptOp::Public || skip.contains(&event.op), + "expected {what}, found {:?}", + event.op + ); + } + } + + fn next(&mut self, op: TranscriptOp, what: &str) -> &'a LocatedEvent { + self.next_skipping(op, &[], what) + } + + fn message(&mut self, what: &str) -> Range { + self.next(TranscriptOp::Message, what) + .narg + .clone() + .expect("received message without an argument-string range") + } + + fn challenge_bytes(&mut self, what: &str) -> &'a [u8] { + let range = self + .next(TranscriptOp::Challenge, what) + .tape + .clone() + .expect("squeeze without a tape range"); + self.tape + .bytes + .get(range) + .expect("recorded tape is shorter than the located squeezes") + } + + fn challenge(&mut self, what: &str) -> F { + decode_challenge(self.challenge_bytes(what)) + } + + fn small_challenge(&mut self, what: &str) -> F { + decode_small_challenge(self.challenge_bytes(what)) + } +} + +fn read(narg: &[u8], at: usize) -> A { + A::from_bytes_le_checked(&narg[at..at + A::NUM_BYTES]).expect("non-canonical atom in fixture") +} + +fn write(narg: &mut [u8], at: usize, value: &A) { + value.to_bytes_le(&mut narg[at..at + A::NUM_BYTES]); } -/// Rewrites the clear stage-1 proof while preserving every algebraic check at -/// the recorded challenges. -pub fn equivocate_stage1_clear( - proof: &mut JoltProof, - tape: &ChallengeTape, - delta: PCS::Field, +/// Changes two homomorphic trace commitments in the kernel of the frozen +/// stage-8 batching combination: `RamInc += gamma * D` and `RdInc -= D` for +/// `D` the first instruction-RA commitment. The final-opening batch orders +/// `RamInc, RdInc` first (`final_opening_polynomial_order`), so their +/// coefficients are `1, gamma` and the joint commitment is unchanged, while +/// both individual openings become false. +#[cfg(not(feature = "akita"))] +pub fn cancel_dory_final_opening_commitments( + narg: &mut [u8], + events: &[LocatedEvent], + tape: &ChallengeTape, ) where - PCS: CommitmentScheme, - VC: VectorCommitment, + C: CanonicalDecode + HomomorphicCommitment + PartialEq, { - let mut stage1 = tape - .records - .iter() - .filter(|record| record.id.scope == FsScope::Stage1); - - let tau = stage1.next().expect("stage 1 tau challenge is missing"); - assert!(matches!( - tau.id.kind, - ChallengeKind::VectorElement { index: 0, .. } - )); - - let uniskip_challenge = stage1 - .find(|record| record.id.kind == ChallengeKind::Challenge) - .expect("stage 1 uni-skip challenge is missing") - .values[0]; - let batching_coefficient = stage1 - .find(|record| record.id.kind == ChallengeKind::Scalar) - .expect("stage 1 batching coefficient is missing") - .values[0]; - let remainder_challenge = stage1 - .find(|record| record.id.kind == ChallengeKind::Challenge) - .expect("stage 1 remainder challenge is missing") - .values[0]; - - let JoltProofClaims::Clear(claims) = &mut proof.claims else { - panic!("stage-1 clear equivocation requires a clear proof"); - }; - claims.stage1.uniskip_output_claim += delta; - - let SumcheckProof::Clear(ClearProof::Full(uniskip)) = - &mut proof.stages.stage1_uni_skip_first_round_proof - else { - panic!("stage-1 uni-skip proof is not full cleartext"); - }; - let round = uniskip - .round_polynomials - .first_mut() - .expect("stage-1 uni-skip round is missing"); - let mut coefficients = round.coefficients().to_vec(); - assert!(coefficients.len() >= 2); - - let domain = CenteredIntegerDomain::new(SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE); - let power_sums = domain + let mut commitments = SiteEvents::new(events, sites::COMMITMENTS, tape); + // `ProofCommitments` send order: RdInc, RamInc, then the instruction RA family. + let rd_inc = commitments.message("RdInc commitment"); + let ram_inc = commitments.message("RamInc commitment"); + let instruction_ra = commitments.message("first InstructionRa commitment"); + for range in [&rd_inc, &ram_inc, &instruction_ra] { + assert_eq!(range.len(), C::NUM_BYTES, "commitment message width"); + } + let gamma: Fr = SiteEvents::new(events, sites::STAGE8, tape) + .challenge("stage-8 final-opening batching challenge"); + + let direction: C = read(narg, instruction_ra.start); + let original_ram_inc: C = read(narg, ram_inc.start); + let original_rd_inc: C = read(narg, rd_inc.start); + let ram_inc_value = C::linear_combine(&original_ram_inc, &direction, &gamma); + let rd_inc_value = C::linear_combine(&original_rd_inc, &direction, &-Fr::from_u64(1)); + assert!(ram_inc_value != original_ram_inc && rd_inc_value != original_rd_inc); + write(narg, ram_inc.start, &ram_inc_value); + write(narg, rd_inc.start, &rd_inc_value); +} + +/// Adds `delta` to the clear stage-1 uni-skip output claim and rewrites the +/// uni-skip round and the first remainder round so every algebraic check +/// still holds at the recorded challenges: +/// +/// - the uni-skip round gains `slope * (X - mean)`, which sums to zero over the +/// centered domain and adds `delta` at the uni-skip challenge; +/// - the first compressed remainder round absorbs the batched input-claim +/// change `coefficient * delta` into its hint while keeping its value at +/// the first remainder challenge. +/// +/// Every later round, and therefore every output claim, is unchanged. +pub fn equivocate_stage1_clear( + narg: &mut [u8], + events: &[LocatedEvent], + tape: &ChallengeTape, + delta: F, +) { + let params = UniskipParams::spartan_outer(); + let mut stage1 = SiteEvents::new(events, sites::STAGE1, tape); + // The uni-skip round is the stage's first message, after the tau draws. + let uniskip_round = stage1 + .next_skipping( + TranscriptOp::Message, + &[TranscriptOp::Challenge], + "stage-1 uni-skip round", + ) + .narg + .clone() + .expect("received message without an argument-string range"); + assert_eq!( + uniskip_round.len(), + (params.degree() + 1) * F::NUM_BYTES, + "uni-skip round width" + ); + let uniskip_challenge: F = stage1.small_challenge("stage-1 uni-skip challenge"); + let output_claim = stage1.message("stage-1 uni-skip output claim"); + assert_eq!( + output_claim.len(), + F::NUM_BYTES, + "uni-skip output claim width" + ); + let batching_coefficient: F = stage1.challenge("stage-1 batching coefficient"); + let remainder_round = stage1.message("first stage-1 remainder round"); + assert_eq!(remainder_round.len() % F::NUM_BYTES, 0); + let remainder_challenge: F = stage1.small_challenge("first stage-1 remainder challenge"); + + let claim: F = read(narg, output_claim.start); + write(narg, output_claim.start, &(claim + delta)); + + let power_sums = CenteredIntegerDomain::new(params.domain_size()) .power_sums(2) .expect("stage-1 uni-skip domain is invalid"); - let domain_size = PCS::Field::from_i128(power_sums[0]); - let mean = PCS::Field::from_i128(power_sums[1]) - * domain_size.inverse().expect("domain size is zero in field"); + let mean = F::from_i128(power_sums[1]) + * F::from_i128(power_sums[0]) + .inverse() + .expect("domain size is zero in field"); let slope = delta * (uniskip_challenge - mean) .inverse() .expect("uni-skip challenge equals the domain mean"); - coefficients[0] -= slope * mean; - coefficients[1] += slope; - *round = UnivariatePoly::new(coefficients); - - let SumcheckProof::Clear(ClearProof::Compressed(remainder)) = - &mut proof.stages.stage1_sumcheck_proof - else { - panic!("stage-1 remainder proof is not compressed cleartext"); - }; - let round = remainder - .round_polynomials - .first_mut() - .expect("stage-1 remainder round is missing"); - let mut coefficients = round.coeffs_except_linear_term().to_vec(); + let c0 = uniskip_round.start; + let c1 = c0 + F::NUM_BYTES; + write(narg, c0, &(read::(narg, c0) - slope * mean)); + write(narg, c1, &(read::(narg, c1) + slope)); + + // A compressed round omits the linear coefficient, which the verifier + // recovers from the running-sum hint. Shifting the hint by `h` moves the + // value at `r` by `h * r`; the next transmitted coefficient cancels it. let hint_delta = batching_coefficient * delta; - if coefficients.len() >= 2 { - let denominator = remainder_challenge * remainder_challenge - remainder_challenge; - coefficients[1] -= remainder_challenge + let r = remainder_challenge; + let at = remainder_round.start; + if remainder_round.len() >= 2 * F::NUM_BYTES { + // coeffs_except_linear_term = [c0, c2, ...]: adjust c2. + let c2 = at + F::NUM_BYTES; + let correction = r * hint_delta - * denominator + * (r * r - r) .inverse() .expect("remainder challenge is Boolean"); + write(narg, c2, &(read::(narg, c2) - correction)); } else { - let denominator = PCS::Field::one() - PCS::Field::from_u64(2) * remainder_challenge; - coefficients[0] -= remainder_challenge + let correction = r * hint_delta - * denominator + * (F::from_u64(1) - F::from_u64(2) * r) .inverse() .expect("remainder challenge is one half"); + write(narg, at, &(read::(narg, at) - correction)); } - *round = CompressedPoly::new(coefficients); } diff --git a/crates/jolt-verifier/tests/support/fs_transcript.rs b/crates/jolt-verifier/tests/support/fs_transcript.rs index 2ed5d2c218..eba1a60308 100644 --- a/crates/jolt-verifier/tests/support/fs_transcript.rs +++ b/crates/jolt-verifier/tests/support/fs_transcript.rs @@ -1,309 +1,249 @@ +//! A challenge-freezing sponge for Fiat-Shamir attack tests. +//! +//! [`AuditSponge`] runs the production [`Blake2b512`] duplex and shares its +//! [`Sponge::ID`], so it verifies honest Blake2b proofs unchanged. Inside a +//! [`record`] session it also writes every squeezed byte to a tape; inside a +//! [`replay`] session every squeeze returns the recorded bytes instead, so +//! the verifier's challenges no longer depend on anything it absorbs. A +//! mutation the frozen verifier accepts is therefore stopped only by +//! Fiat-Shamir binding. +//! +//! The tape is one byte stream indexed by squeeze position, not a list of +//! per-call records. Exact challenges rejection-sample a data-dependent number +//! of bytes, so only the byte stream is invariant under replay. +//! +//! Each sponge value carries its own stream position, so a clone continues the +//! stream from where it was forked, exactly as a clone of a real sponge would. +//! While recording, a fork that squeezes over an already recorded position +//! must reproduce the recorded bytes, so verifier scouts that run ahead on a +//! transcript clone stay consistent with the live transcript. A fork that +//! squeezes different bytes (one that absorbed different data, such as a +//! proof-of-work preview) panics: its challenges have no single frozen value. + #![expect( clippy::expect_used, clippy::panic, - reason = "the audit test double must fail loudly on a malformed replay tape" + reason = "the audit test double must fail loudly outside a session or on a malformed tape" )] -use std::{any::Any, cell::RefCell, collections::BTreeMap, sync::Arc}; - -use jolt_field::{Field, Ring}; -use jolt_transcript::Transcript; -use jolt_verifier::fs_audit::{self, FsScope}; +use std::{ + cell::RefCell, + sync::{Arc, Mutex}, +}; -/// The transcript API used to derive a challenge. -/// -/// Multi-value APIs are recorded at draw granularity, mirroring the -/// production trait defaults: `challenge_vector(len)` is `len` independent -/// squeezes (one [`ChallengeKind::VectorElement`] record each), and -/// `challenge_scalar_powers(len)` squeezes only its base scalar (one -/// [`ChallengeKind::PowersBase`] record) with the powers derived locally. -/// A verifier refactor that drops or short-consumes an element therefore -/// registers as a per-element tape divergence instead of hiding inside one -/// atomic `Vec` record. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ChallengeKind { - Challenge, - Scalar, - VectorElement { len: usize, index: usize }, - PowersBase { len: usize }, -} +use jolt_field::{CanonicalEncoding, Field}; +use jolt_transcript::DuplexSpongeInterface; +use jolt_transcript::{Blake2b512, Channel, ProtocolId, Sponge, VerifierTranscript}; -/// A challenge's verifier scope and ordinal within that scope. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ChallengeId { - pub scope: FsScope, - pub index: usize, - pub kind: ChallengeKind, -} - -/// One typed challenge call and all values it returned. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ChallengeRecord { - pub id: ChallengeId, - pub values: Vec, -} - -/// Challenges produced while verifying one fixture. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ChallengeTape { - pub records: Vec>, -} - -impl ChallengeTape { - /// First record where the tapes diverge in schedule (id sequence or - /// length) or value. A dropped or inserted draw shifts every later - /// squeeze without necessarily changing any shared-prefix value, so a - /// pure `zip` would truncate to the shorter tape and miss it; the - /// trailing check surfaces the first unmatched record instead. - pub fn first_value_divergence(&self, other: &Self) -> Option { - self.records - .iter() - .zip(&other.records) - .find_map(|(left, right)| { - (left.id != right.id || left.values != right.values).then_some(left.id) - }) - .or_else(|| { - self.records - .get(other.records.len()) - .map(|record| record.id) - }) - .or_else(|| { - other - .records - .get(self.records.len()) - .map(|record| record.id) - }) - } -} - -#[derive(Clone)] -struct ErasedRecord { - id: ChallengeId, - values: Arc, +enum Mode { + Record, + Replay, } -enum Session { - Record { - records: Vec, - counters: BTreeMap, - }, - Replay { - records: Vec, - counters: BTreeMap, - cursor: usize, - }, +#[derive(Debug)] +struct Tape { + mode: Mode, + bytes: Vec, + /// Furthest stream position any sponge of the session squeezed to. + high_water: usize, } thread_local! { - static SESSION: RefCell> = const { RefCell::new(None) }; -} - -fn next_id(counters: &mut BTreeMap, kind: ChallengeKind) -> ChallengeId { - let scope = fs_audit::current(); - let index = counters.entry(scope).or_default(); - let id = ChallengeId { - scope, - index: *index, - kind, - }; - *index += 1; - id -} - -fn draw(kind: ChallengeKind, produce: impl FnOnce() -> Vec) -> Vec -where - F: Field + 'static, -{ - let actual = produce(); - SESSION.with(|slot| { - let mut slot = slot.borrow_mut(); - let Some(session) = slot.as_mut() else { - return actual; - }; - match session { - Session::Record { records, counters } => { - let id = next_id(counters, kind); - records.push(ErasedRecord { - id, - values: Arc::new(actual.clone()), - }); - actual - } - Session::Replay { - records, - counters, - cursor, - } => { - let id = next_id(counters, kind); - let expected = records - .get(*cursor) - .unwrap_or_else(|| panic!("challenge replay exhausted at {id:?}")); - assert_eq!( - expected.id, id, - "challenge replay kind or scope diverged at tape index {cursor}" - ); - *cursor += 1; - expected - .values - .downcast_ref::>() - .expect("challenge replay field type changed") - .clone() - } - } - }) + static ACTIVE: RefCell>>> = const { RefCell::new(None) }; } -/// Transcript wrapper that records or replays challenge calls for the active session. -pub struct AuditTranscript { - inner: T, +/// [`Blake2b512`] with a recorded or replayed squeeze stream. +/// +/// [`Default`] binds the new sponge to the session active on the calling +/// thread and panics outside one; clones share that session. +#[derive(Clone)] +pub struct AuditSponge { + inner: Blake2b512, + position: usize, + tape: Arc>, } -impl Default for AuditTranscript { +impl Default for AuditSponge { fn default() -> Self { + let tape = ACTIVE + .with(|active| active.borrow().clone()) + .expect("AuditSponge constructed outside a record or replay session"); Self { - inner: T::default(), + inner: Blake2b512::default(), + position: 0, + tape, } } } -impl Transcript for AuditTranscript -where - T: Transcript, - T::Challenge: Field + 'static, -{ - type Challenge = T::Challenge; +impl DuplexSpongeInterface for AuditSponge { + type U = u8; - fn new(label: &'static [u8]) -> Self { - Self { - inner: T::new(label), - } + fn absorb(&mut self, input: &[u8]) -> &mut Self { + let _ = self.inner.absorb(input); + self } - fn append_bytes(&mut self, bytes: &[u8]) { - self.inner.append_bytes(bytes); + fn squeeze(&mut self, output: &mut [u8]) -> &mut Self { + let start = self.position; + let end = start + output.len(); + let mut tape = self.tape.lock().expect("audit tape lock poisoned"); + match tape.mode { + Mode::Record => { + let _ = self.inner.squeeze(output); + assert!( + start <= tape.bytes.len(), + "audit sponge skipped tape bytes {}..{start}", + tape.bytes.len() + ); + let recorded_end = end.min(tape.bytes.len()); + let (overlap, fresh) = output.split_at(recorded_end - start); + assert_eq!( + &tape.bytes[start..recorded_end], + overlap, + "forked audit sponges squeezed different bytes at tape offset {start}" + ); + tape.bytes.extend_from_slice(fresh); + } + Mode::Replay => { + let recorded = tape.bytes.get(start..end).unwrap_or_else(|| { + panic!( + "challenge replay exhausted: squeeze {start}..{end} past {} recorded bytes", + tape.bytes.len() + ) + }); + output.copy_from_slice(recorded); + } + } + tape.high_water = tape.high_water.max(end); + drop(tape); + self.position = end; + self } - fn challenge(&mut self) -> Self::Challenge { - draw(ChallengeKind::Challenge, || vec![self.inner.challenge()])[0] + fn ratchet(&mut self) -> &mut Self { + panic!("Jolt transcripts never ratchet; the audit tape cannot model one"); } +} - fn challenge_scalar(&mut self) -> Self::Challenge { - draw(ChallengeKind::Scalar, || { - vec![self.inner.challenge_scalar()] - })[0] - } +impl Sponge for AuditSponge { + const ID: &'static str = ::ID; +} - // WARNING: both bodies must mirror the `Transcript` trait defaults - // (`legacy.rs`), which no production transcript overrides — the audit - // records at the same granularity the sponge is actually squeezed. - fn challenge_vector(&mut self, len: usize) -> Vec { - (0..len) - .map(|index| { - draw(ChallengeKind::VectorElement { len, index }, || { - vec![self.inner.challenge()] - })[0] +/// Every byte squeezed during one recorded verification, in stream order. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ChallengeTape { + pub bytes: Vec, +} + +impl ChallengeTape { + /// First stream offset at which the tapes differ, including one tape + /// ending before the other. + pub fn first_divergence(&self, other: &Self) -> Option { + self.bytes + .iter() + .zip(&other.bytes) + .position(|(left, right)| left != right) + .or_else(|| { + (self.bytes.len() != other.bytes.len()) + .then_some(self.bytes.len().min(other.bytes.len())) }) - .collect() } +} - fn challenge_scalar_powers(&mut self, len: usize) -> Vec { - let base = draw(ChallengeKind::PowersBase { len }, || { - vec![self.inner.challenge_scalar()] - })[0]; - let mut powers = vec![Self::Challenge::from_u64(1); len]; - for index in 1..len { - powers[index] = powers[index - 1] * base; - } - powers +/// Clears the thread's session even when the closure under audit panics. +struct SessionGuard; + +impl SessionGuard { + fn start(tape: Tape) -> (Self, Arc>) { + let tape = Arc::new(Mutex::new(tape)); + ACTIVE.with(|active| { + let mut active = active.borrow_mut(); + assert!( + active.is_none(), + "nested Fiat-Shamir audit sessions are unsupported" + ); + *active = Some(Arc::clone(&tape)); + }); + (Self, tape) } +} - fn state(&self) -> [u8; 32] { - self.inner.state() +impl Drop for SessionGuard { + fn drop(&mut self) { + ACTIVE.with(|active| *active.borrow_mut() = None); } } -/// Runs `verify` with challenge recording enabled. -pub fn record_challenges(verify: impl FnOnce() -> R) -> (R, ChallengeTape) -where - F: Field + 'static, -{ - SESSION.with(|slot| { - assert!( - slot.borrow().is_none(), - "nested Fiat-Shamir audit sessions are unsupported" - ); - *slot.borrow_mut() = Some(Session::Record { - records: Vec::new(), - counters: BTreeMap::new(), - }); - }); +fn finish(guard: SessionGuard, tape: &Mutex) -> (Vec, usize) { + drop(guard); + let tape = tape.lock().expect("audit tape lock poisoned"); + (tape.bytes.clone(), tape.high_water) +} - let result = verify(); - let records = SESSION.with(|slot| match slot.borrow_mut().take() { - Some(Session::Record { records, .. }) => records, - Some(Session::Replay { .. }) => panic!("recording session changed to replay"), - None => panic!("recording session disappeared"), +/// Runs `run` with every [`AuditSponge`] squeeze recorded. +pub fn record(run: impl FnOnce() -> R) -> (R, ChallengeTape) { + let (guard, tape) = SessionGuard::start(Tape { + mode: Mode::Record, + bytes: Vec::new(), + high_water: 0, }); - let records = records - .into_iter() - .map(|record| ChallengeRecord { - id: record.id, - values: record - .values - .downcast_ref::>() - .expect("recorded challenge field type changed") - .clone(), - }) - .collect(); - (result, ChallengeTape { records }) + let output = run(); + let (bytes, _) = finish(guard, &tape); + (output, ChallengeTape { bytes }) } -/// Result of verifying with a frozen challenge tape. -pub struct ReplayResult { +/// Result of running against a frozen tape. +pub struct Replayed { pub output: R, + /// Bytes of the tape the run squeezed. pub consumed: usize, - pub expected: usize, + pub recorded: usize, } -/// Runs `verify` while returning the recorded values for every challenge call. -pub fn replay_challenges( - tape: &ChallengeTape, - verify: impl FnOnce() -> R, -) -> ReplayResult -where - F: Field + 'static, -{ - let records = tape - .records - .iter() - .map(|record| ErasedRecord { - id: record.id, - values: Arc::new(record.values.clone()), - }) - .collect(); - SESSION.with(|slot| { - assert!( - slot.borrow().is_none(), - "nested Fiat-Shamir audit sessions are unsupported" - ); - *slot.borrow_mut() = Some(Session::Replay { - records, - counters: BTreeMap::new(), - cursor: 0, - }); - }); - - let output = verify(); - let (consumed, expected) = SESSION.with(|slot| match slot.borrow_mut().take() { - Some(Session::Replay { - records, cursor, .. - }) => (cursor, records.len()), - Some(Session::Record { .. }) => panic!("replay session changed to recording"), - None => panic!("replay session disappeared"), +/// Runs `run` with every [`AuditSponge`] squeeze answered from `tape`. +pub fn replay(tape: &ChallengeTape, run: impl FnOnce() -> R) -> Replayed { + let (guard, session) = SessionGuard::start(Tape { + mode: Mode::Replay, + bytes: tape.bytes.clone(), + high_water: 0, }); - ReplayResult { + let output = run(); + let (_, consumed) = finish(guard, &session); + Replayed { output, consumed, - expected, + recorded: tape.bytes.len(), } } + +/// The value a production transcript draws when its squeezes return `bytes`. +fn draw_from( + bytes: &[u8], + draw: impl FnOnce(&mut VerifierTranscript<'_, AuditSponge>) -> T, +) -> T { + let tape = ChallengeTape { + bytes: bytes.to_vec(), + }; + let replayed = replay(&tape, || { + let protocol = ProtocolId::new::("jolt/fs-attacks/decode"); + draw(&mut VerifierTranscript::new(&protocol, b"", &[])) + }); + assert_eq!( + replayed.consumed, + bytes.len(), + "challenge decode consumed a different byte count than the recorded draw" + ); + replayed.output +} + +/// Decodes an exactly uniform [`Channel::challenge`] from the bytes it squeezed. +pub fn decode_challenge(bytes: &[u8]) -> F { + draw_from(bytes, |transcript| transcript.challenge()) +} + +/// Decodes a [`Channel::challenge_small`] from the bytes it squeezed. +pub fn decode_small_challenge(bytes: &[u8]) -> F { + draw_from(bytes, |transcript| transcript.challenge_small()) +} diff --git a/scripts/ci/fs-soundness.sh b/scripts/ci/fs-soundness.sh index e5e43570e8..e898809296 100755 --- a/scripts/ci/fs-soundness.sh +++ b/scripts/ci/fs-soundness.sh @@ -9,19 +9,19 @@ cargo nextest run \ cargo nextest run \ -p jolt-verifier \ --test fs_attacks \ - --features fs-audit,prover-fixtures \ + --features logging,prover-fixtures \ --cargo-quiet cargo nextest run \ -p jolt-verifier \ --test fs_attacks \ - --features fs-audit,prover-fixtures,zk \ + --features logging,prover-fixtures,zk \ --cargo-quiet cargo nextest run \ -p jolt-verifier \ --test fs_attacks \ - --features akita,fs-audit,prover-fixtures \ + --features akita,logging,prover-fixtures \ --cargo-quiet unsupported_log="$(mktemp)" From 91c57abdb6a08c68e32c56787d2344771a5d3141 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 17:08:43 -0400 Subject: [PATCH 29/62] test(verifier): sweep every argument-string message against its rejection 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`. --- .github/workflows/rust.yml | 46 +- crates/jolt-verifier/tests/soundness/akita.rs | 75 + .../jolt-verifier/tests/soundness/checks.rs | 467 ++++ crates/jolt-verifier/tests/soundness/dory.rs | 100 + crates/jolt-verifier/tests/soundness/mod.rs | 31 +- .../prover_transitivity/commitments.rs | 3 - .../soundness/prover_transitivity/configs.rs | 3 - .../soundness/prover_transitivity/mod.rs | 6 - .../soundness/prover_transitivity/openings.rs | 3 - .../soundness/prover_transitivity/preamble.rs | 3 - .../prover_transitivity/proof_shape.rs | 3 - .../tests/soundness/prover_transitivity/zk.rs | 3 - .../tests/soundness/tampering/akita.rs | 753 ------- .../tests/soundness/tampering/commitments.rs | 117 - .../tests/soundness/tampering/configs.rs | 3 - .../tests/soundness/tampering/field_inline.rs | 160 -- .../tests/soundness/tampering/manifest.rs | 156 -- .../tests/soundness/tampering/mod.rs | 44 - .../tests/soundness/tampering/openings.rs | 79 - .../tests/soundness/tampering/preamble.rs | 203 -- .../tests/soundness/tampering/proof_shape.rs | 118 - .../tests/soundness/tampering/sumcheck.rs | 1300 ----------- .../tests/soundness/tampering/zk.rs | 344 --- crates/jolt-verifier/tests/soundness/zk.rs | 105 + .../tests/statistical_independence/mod.rs | 11 +- .../tests/statistical_independence/zk.rs | 769 +------ crates/jolt-verifier/tests/support/mod.rs | 35 +- crates/jolt-verifier/tests/support/narg.rs | 825 +++++++ .../tests/support/proof_claims.rs | 619 ------ .../tests/support/tamper_manifest.rs | 1906 ----------------- .../jolt-verifier/tests/support/zk_audit.rs | 162 -- 31 files changed, 1735 insertions(+), 6717 deletions(-) create mode 100644 crates/jolt-verifier/tests/soundness/akita.rs create mode 100644 crates/jolt-verifier/tests/soundness/checks.rs create mode 100644 crates/jolt-verifier/tests/soundness/dory.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/commitments.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/configs.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/mod.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/openings.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/preamble.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/proof_shape.rs delete mode 100644 crates/jolt-verifier/tests/soundness/prover_transitivity/zk.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/akita.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/commitments.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/configs.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/field_inline.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/manifest.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/mod.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/openings.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/preamble.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs delete mode 100644 crates/jolt-verifier/tests/soundness/tampering/zk.rs create mode 100644 crates/jolt-verifier/tests/soundness/zk.rs create mode 100644 crates/jolt-verifier/tests/support/narg.rs delete mode 100644 crates/jolt-verifier/tests/support/proof_claims.rs delete mode 100644 crates/jolt-verifier/tests/support/tamper_manifest.rs delete mode 100644 crates/jolt-verifier/tests/support/zk_audit.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ed4f0b081e..fd08a62739 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -100,18 +100,19 @@ jobs: # jolt-verifier's prover-fixtures feature gates live prover artifact # generation for verifier tests. It is not enabled by the workspace-wide # invocations above, so build it explicitly in both modes. - - name: cargo clippy -p jolt-verifier --features prover-fixtures - run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures - - name: cargo clippy -p jolt-verifier --features prover-fixtures,zk - run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,zk + - name: cargo clippy -p jolt-verifier --features prover-fixtures,logging + run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,logging + - name: cargo clippy -p jolt-verifier --features prover-fixtures,zk,logging + run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,zk,logging # The akita feature selects the lattice (Akita) PIOP; it is not enabled # by the workspace-wide invocations above. - name: cargo clippy -p jolt-verifier --features akita run: cargo clippy -p jolt-verifier --all-targets --features akita # The akita fixture-driven tamper suite compiles under akita,prover-fixtures - # (the real packed prover generates its fixtures). Lint it explicitly. - - name: cargo clippy -p jolt-verifier --features akita,prover-fixtures - run: cargo clippy -p jolt-verifier --all-targets --features akita,prover-fixtures + # (the real packed prover generates its fixtures) with `logging`, whose + # transcript event log locates each rejection. Lint it explicitly. + - name: cargo clippy -p jolt-verifier --features akita,prover-fixtures,logging + run: cargo clippy -p jolt-verifier --all-targets --features akita,prover-fixtures,logging # The modular prover's packed (Akita) path: lint alone and with # prover-fixtures so the acceptance suites keep compiling. - name: cargo clippy -p jolt-prover --features akita @@ -177,14 +178,14 @@ jobs: run: cargo clippy -p jolt-prover --all-targets --features prover-fixtures,field-inline - name: cargo clippy -p jolt-prover --features prover-fixtures,field-inline,zk run: cargo clippy -p jolt-prover --all-targets --features prover-fixtures,field-inline,zk - - name: cargo clippy -p jolt-verifier --features prover-fixtures,field-inline - run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,field-inline + - name: cargo clippy -p jolt-verifier --features prover-fixtures,field-inline,logging + run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,field-inline,logging # The packed field-inline combination: the full-width increment group rides the # dense-group heterogeneous batch beside advice. - name: cargo clippy -p jolt-verifier --features field-inline,akita run: cargo clippy -p jolt-verifier --all-targets --features field-inline,akita - - name: cargo clippy -p jolt-verifier --features prover-fixtures,field-inline,akita - run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,field-inline,akita + - name: cargo clippy -p jolt-verifier --features prover-fixtures,field-inline,akita,logging + run: cargo clippy -p jolt-verifier --all-targets --features prover-fixtures,field-inline,akita,logging - name: cargo clippy -p jolt-prover --features prover-fixtures,field-inline,akita run: cargo clippy -p jolt-prover --all-targets --features prover-fixtures,field-inline,akita - name: cargo clippy -p jolt-sdk --features host,field-inline @@ -374,7 +375,7 @@ jobs: # step existed they were clippy-only, so the golden BlindFold shape pins # in completeness/zk.rs could go stale without any lane noticing. - name: Run jolt-verifier fixtures with zk feature - run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,zk --test-threads 1 --cargo-quiet + run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,zk,logging --test-threads 1 --cargo-quiet # The jolt-kernels unit suite in its zk flavor. - name: Run jolt-kernels tests with zk feature run: cargo nextest run --cargo-profile ci -p jolt-kernels --features zk @@ -513,7 +514,7 @@ jobs: - name: Run jolt-akita with field-inline run: cargo nextest run --cargo-profile ci -p jolt-akita --features field-inline - name: Run jolt-verifier with prover-fixtures,field-inline,akita - run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,field-inline,akita --test-threads 1 + run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,field-inline,akita,logging --test-threads 1 test-verifier-field-inline-fixtures: runs-on: ubuntu-latest @@ -546,11 +547,11 @@ jobs: - name: Install nextest uses: taiki-e/install-action@nextest # The fixture-driven field-inline suites: valid-fixture acceptance - # (completeness/field_inline.rs) and the typed field-inline clear-claim tamper - # sweep (tampering/field_inline.rs), backed by fixtures the modular - # prover generates at test time. + # (completeness/field_inline.rs) and the argument-string tamper sweep + # (soundness/dory.rs, `logging` for its event log), backed by fixtures + # the modular prover generates at test time. - name: Run jolt-verifier field-inline fixture and tamper suites - run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,field-inline --test-threads 1 + run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,field-inline,logging --test-threads 1 test-verifier-akita-fixtures: runs-on: ubuntu-latest @@ -583,11 +584,12 @@ jobs: - name: Install nextest uses: taiki-e/install-action@45fceba0fc176f41d2aee1ca9d6598ba628eea62 # nextest # The fixture-driven akita suites: valid-fixture acceptance - # (completeness/akita.rs) and the typed clear-claim, commitment-byte, - # proof-shape, and presence tamper sweeps (tampering/akita.rs), backed - # by fixtures the real packed prover generates at test time. + # (completeness/akita.rs) and the argument-string message, structural, + # and statement tamper sweeps (soundness/akita.rs, `logging` for their + # event log), backed by fixtures the real packed prover generates at + # test time. - name: Run jolt-verifier akita fixture and tamper suites - run: cargo nextest run --cargo-profile ci -p jolt-verifier --features akita,prover-fixtures --test-threads 1 + run: cargo nextest run --cargo-profile ci -p jolt-verifier --features akita,prover-fixtures,logging --test-threads 1 typos: runs-on: ubuntu-latest @@ -827,7 +829,7 @@ jobs: - name: Install nextest uses: taiki-e/install-action@45fceba0fc176f41d2aee1ca9d6598ba628eea62 # nextest - name: Run verifier fixtures - run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures --test-threads 1 --cargo-quiet + run: cargo nextest run --cargo-profile ci -p jolt-verifier --features prover-fixtures,logging --test-threads 1 --cargo-quiet test-inlines: name: Test inlines diff --git a/crates/jolt-verifier/tests/soundness/akita.rs b/crates/jolt-verifier/tests/soundness/akita.rs new file mode 100644 index 0000000000..9c30a359a2 --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/akita.rs @@ -0,0 +1,75 @@ +//! Akita (packed lattice) fixtures. Legacy-shape fixtures run with +//! field-inline disabled; the packed field-inline eq-MLE fixture runs with +//! it enabled. + +use super::checks; + +#[cfg(not(feature = "field-inline"))] +mod base { + use jolt_claims::protocols::jolt::TracePolynomialOrder; + use jolt_verifier::VerifierError; + + use super::checks; + use crate::support::akita_fixtures::{ + akita_advice_case, akita_committed_muldiv_case, akita_muldiv_case, + }; + use crate::support::narg::TracedCase; + + #[test] + fn akita_muldiv_message_sweep() { + let case = akita_muldiv_case(); + checks::message_sweep(case, None); + checks::structural_tampers(case, None); + } + + #[test] + fn akita_muldiv_statement_tampers() { + let case = akita_muldiv_case(); + checks::statement_tampers(case); + checks::header_equivocations(case); + checks::preprocessing_swap(case, &akita_committed_muldiv_case().preprocessing); + } + + #[test] + fn akita_advice_message_sweep() { + let case = akita_advice_case(); + checks::message_sweep(case, None); + checks::trusted_advice_tampers(case, &checks::sent_commitment(case, 1)); + } + + #[test] + fn akita_committed_message_sweep() { + let case = akita_committed_muldiv_case(); + checks::message_sweep(case, None); + checks::committed_program_tampers( + case, + &[("swapped direct program commitments", |preprocessing| { + let committed = checks::committed_mut(preprocessing); + assert!(committed.direct_program_commitments.len() >= 2); + committed.direct_program_commitments.swap(0, 1); + })], + ); + + let mut preprocessing = case.preprocessing.clone(); + let committed = checks::committed_mut(&mut preprocessing); + committed.trace_order = match committed.trace_order { + TracePolynomialOrder::CycleMajor => TracePolynomialOrder::AddressMajor, + TracePolynomialOrder::AddressMajor => TracePolynomialOrder::CycleMajor, + }; + assert!(matches!( + case.verify_statement_with(&preprocessing), + Err(VerifierError::InvalidCommittedProgram { .. }) + )); + } +} + +#[cfg(feature = "field-inline")] +#[test] +fn akita_field_inline_eqpoly_message_sweep() { + use crate::support::akita_fixtures::akita_field_inline_eqpoly_case; + + let case = akita_field_inline_eqpoly_case(); + checks::message_sweep(case, None); + checks::structural_tampers(case, Some(64)); + checks::statement_tampers(case); +} diff --git a/crates/jolt-verifier/tests/soundness/checks.rs b/crates/jolt-verifier/tests/soundness/checks.rs new file mode 100644 index 0000000000..9f4ea14a47 --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/checks.rs @@ -0,0 +1,467 @@ +//! Backend-generic soundness checks over a fixture's argument string and +//! statement. Each backend module runs them over its own fixtures. + +#![expect( + clippy::expect_used, + clippy::panic, + reason = "tamper checks fail loudly when a fixture breaks an assumption" +)] + +use std::sync::Arc; + +use common::jolt_device::JoltDevice; +use jolt_claims::protocols::jolt::TracePolynomialOrder; +use jolt_openings::CommitmentScheme; +use jolt_transcript::TranscriptError; +use jolt_verifier::{ + JoltProof, JoltVerifierPreprocessing, ProgramPreprocessing, ProofHeader, VerifierError, +}; + +use crate::support::narg::{ + assert_coverage_closure, assert_truncations_reject, decode_header, message_in, sweep, + with_header, CommitmentOf, Region, TracedCase, +}; + +/// Coverage closure, then the single-byte message sweep with deadlines. +/// `budget` caps the number of tampers (`None`: every tamper). +pub fn message_sweep(case: &C, budget: Option) { + let honest = case.honest_trace(); + assert_coverage_closure(&honest, case.proof().narg.len()); + let report = sweep(case, budget); + assert!(report.tampers > 0, "the sweep exercised no tamper"); +} + +/// Truncation at message boundaries, trailing bytes, an empty argument +/// string, and a mismatched protocol field. +pub fn structural_tampers(case: &C, truncation_budget: Option) { + assert_truncations_reject(case, truncation_budget); + + let mut trailing = case.proof().narg.clone(); + trailing.push(0); + assert!( + matches!( + case.verify_proof(&case.with_narg(trailing)), + Err(VerifierError::Transcript(TranscriptError::TrailingBytes)) + ), + "a trailing byte must fail the exact-consumption check" + ); + + assert!( + matches!( + case.verify_proof(&case.with_narg(Vec::new())), + Err(VerifierError::Transcript(TranscriptError::Truncated)) + ), + "an empty argument string must fail at the proof header" + ); + + for protocol in wrong_protocols(case.proof()) { + let proof = JoltProof { + protocol, + narg: case.proof().narg.clone(), + }; + assert!( + matches!( + case.verify_proof(&proof), + Err(VerifierError::ProtocolConfigMismatch { .. }) + ), + "a proof declaring {protocol:?} must fail the protocol-config check" + ); + } +} + +fn wrong_protocols(proof: &JoltProof) -> Vec { + use jolt_verifier::config::CommitmentConfig; + use jolt_verifier::ZkConfig; + + let mut zk = proof.protocol; + zk.zk = match zk.zk { + ZkConfig::Transparent => ZkConfig::BlindFold, + ZkConfig::BlindFold => ZkConfig::Transparent, + }; + let mut commitment = proof.protocol; + commitment.commitment = match commitment.commitment { + CommitmentConfig::Homomorphic => CommitmentConfig::Packed, + CommitmentConfig::Packed => CommitmentConfig::Homomorphic, + }; + vec![zk, commitment] +} + +/// Runs `narg` against a mutated statement and requires rejection no later +/// than the region whose public absorptions the mutation changes. Returns +/// the error for callers that pin a typed rejection. +#[must_use = "the typed rejection is the caller's to pin or drop"] +fn statement_rejection( + label: &str, + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&CommitmentOf>, + proof: &JoltProof, + deadline: Region, +) -> VerifierError { + let error = C::verify_statement(preprocessing, public_io, proof, trusted_advice_commitment) + .expect_err(label); + let trace = C::trace_statement( + preprocessing, + public_io, + trusted_advice_commitment, + &proof.narg, + ); + assert!(trace.result.is_err(), "{label}: traced replay accepted"); + assert!( + trace.stop_region() <= deadline, + "{label}: rejected in {:?}, after its {deadline:?} deadline: {:?}", + trace.stop_region(), + trace.result + ); + error +} + +fn assert_statement_rejects( + label: &str, + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&CommitmentOf>, + proof: &JoltProof, + deadline: Region, +) { + let _rejection = statement_rejection::( + label, + preprocessing, + public_io, + trusted_advice_commitment, + proof, + deadline, + ); +} + +/// Public-statement tampers: the verifier absorbs the statement (the +/// preprocessing digest, memory layout bounds, public I/O, and entry +/// address) in the preamble, so each mutation must reject by the preamble's +/// deadline, and a statement outside the preprocessing's bounds must reject +/// with its typed error. +pub fn statement_tampers(case: &C) { + let preprocessing = case.preprocessing(); + let public_io = case.public_io(); + let trusted = case.trusted_advice_commitment(); + let proof = case.proof(); + let deadline = Region::statement_deadline(); + let check = |label: &str, + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice| { + assert_statement_rejects::(label, preprocessing, public_io, trusted, proof, deadline); + }; + let typed = |label: &str, + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice| { + statement_rejection::(label, preprocessing, public_io, trusted, proof, deadline) + }; + + let mut io = public_io.clone(); + io.inputs + .first_mut() + .map(|byte| *byte ^= 1) + .expect("fixture has public inputs"); + check("flipped public input byte", preprocessing, &io); + + let mut io = public_io.clone(); + io.inputs.push(1); + check("appended public input byte", preprocessing, &io); + + let mut io = public_io.clone(); + io.outputs + .first_mut() + .map(|byte| *byte ^= 1) + .expect("fixture has public outputs"); + check("flipped public output byte", preprocessing, &io); + + let mut io = public_io.clone(); + io.panic = !io.panic; + check("flipped panic bit", preprocessing, &io); + + let layout = preprocessing.program.memory_layout(); + let mut io = public_io.clone(); + io.inputs = vec![0; usize::try_from(layout.max_input_size).expect("fits") + 1]; + assert!(matches!( + typed("oversized public input", preprocessing, &io), + VerifierError::InputTooLarge { .. } + )); + + let mut io = public_io.clone(); + io.outputs = vec![1; usize::try_from(layout.max_output_size).expect("fits") + 1]; + assert!(matches!( + typed("oversized public output", preprocessing, &io), + VerifierError::OutputTooLarge { .. } + )); + + let mut io = public_io.clone(); + io.memory_layout.heap_size += 1; + assert!(matches!( + typed("public memory layout mismatch", preprocessing, &io), + VerifierError::MemoryLayoutMismatch + )); + + let mut tampered = preprocessing.clone(); + tampered.preprocessing_digest[0] ^= 1; + check("flipped preprocessing digest", &tampered, public_io); + + if let ProgramPreprocessing::Full(full) = &preprocessing.program { + let mut tampered = preprocessing.clone(); + let mut program = Arc::clone(full); + Arc::make_mut(&mut program).bytecode.entry_address += 4; + tampered.program = ProgramPreprocessing::Full(program); + check("shifted entry address", &tampered, public_io); + } +} + +/// Trusted-advice tampers: dropping or adding the commitment changes the +/// opening schedule, and replacing it with another valid commitment changes +/// the absorbed public commitments. +pub fn trusted_advice_tampers(case: &C, replacement: &CommitmentOf) { + let deadline = Region::statement_deadline(); + let run = |label: &str, trusted| { + assert_statement_rejects::( + label, + case.preprocessing(), + case.public_io(), + trusted, + case.proof(), + deadline, + ); + }; + match case.trusted_advice_commitment() { + Some(_) => { + run("dropped trusted advice commitment", None); + run("replaced trusted advice commitment", Some(replacement)); + } + None => run("added trusted advice commitment", Some(replacement)), + } +} + +/// Decodes the `index`-th polynomial commitment the proof sends, reading the +/// commitment region from its start (one commitment may span several +/// messages). +pub fn sent_commitment(case: &C, index: usize) -> CommitmentOf { + let trace = case.honest_trace(); + let start = message_in(&trace, Region::Commitments, 0).start; + let mut transcript = jolt_transcript::VerifierTranscript::::new( + &jolt_verifier::jolt_protocol_id::(), + jolt_verifier::JOLT_SESSION, + &case.proof().narg[start..], + ); + let mut receive = || { + C::Pcs::receive_commitment(&case.preprocessing().pcs_setup, &mut transcript) + .expect("a sent commitment decodes") + }; + for _ in 0..index { + let _skipped = receive(); + } + receive() +} + +/// Well-formed but wrong header values, re-encoded with the production +/// encoder: each decodes, so rejection must come from validation or from the +/// re-randomized transcript, by the preamble's deadline. +pub fn header_equivocations(case: &C) { + let narg = &case.proof().narg; + let honest = decode_header(narg); + let max_trace = case.preprocessing().program.max_padded_trace_length(); + let mut variants: Vec<(&str, ProofHeader)> = vec![ + ( + "trace length 3", + ProofHeader { + trace_length: 3, + ..honest + }, + ), + ( + "trace length above the preprocessing bound", + ProofHeader { + trace_length: max_trace * 2, + ..honest + }, + ), + ("ram_K 3", ProofHeader { ram_K: 3, ..honest }), + ("ram_K 0", ProofHeader { ram_K: 0, ..honest }), + ( + "doubled ram_K", + ProofHeader { + ram_K: honest.ram_K * 2, + ..honest + }, + ), + ( + "flipped trace polynomial order", + ProofHeader { + trace_polynomial_order: match honest.trace_polynomial_order { + TracePolynomialOrder::CycleMajor => TracePolynomialOrder::AddressMajor, + TracePolynomialOrder::AddressMajor => TracePolynomialOrder::CycleMajor, + }, + ..honest + }, + ), + ( + "toggled untrusted advice", + ProofHeader { + untrusted_advice: !honest.untrusted_advice, + ..honest + }, + ), + ]; + let mut one_hot = honest.one_hot_config; + one_hot.log_k_chunk += 1; + variants.push(( + "log_k_chunk + 1", + ProofHeader { + one_hot_config: one_hot, + ..honest + }, + )); + let mut rw = honest.rw_config; + rw.ram_rw_phase1_num_rounds ^= 1; + variants.push(( + "ram phase-1 rounds toggled", + ProofHeader { + rw_config: rw, + ..honest + }, + )); + if honest.trace_length > 1 { + variants.push(( + "halved trace length", + ProofHeader { + trace_length: honest.trace_length / 2, + ..honest + }, + )); + } + + let deadline = Region::statement_deadline(); + for (label, header) in variants { + let narg = with_header(narg, &header); + let proof = case.with_narg(narg); + let error = statement_rejection::( + label, + case.preprocessing(), + case.public_io(), + case.trusted_advice_commitment(), + &proof, + deadline, + ); + match label { + "trace length 3" | "trace length above the preprocessing bound" => assert!( + matches!(error, VerifierError::InvalidTraceLength { .. }), + "{label}: {error:?}" + ), + "ram_K 3" | "ram_K 0" => assert!( + matches!(error, VerifierError::InvalidRamK { .. }), + "{label}: {error:?}" + ), + _ => {} + } + } +} + +/// Substitutes one sent commitment's bytes with another same-width sent +/// commitment's, and swaps the two: valid encodings, wrong commitments. +/// Requires at least two same-width commitments in the proof. +pub fn commitment_substitutions(case: &C) { + let trace = case.honest_trace(); + let commitments: Vec<_> = trace + .messages() + .into_iter() + .filter(|message| message.region == Region::Commitments) + .map(|message| message.range) + .collect(); + let (first, second) = commitments + .iter() + .enumerate() + .find_map(|(index, first)| { + commitments + .get(index + 1..)? + .iter() + .find(|second| second.len() == first.len()) + .map(|second| (first.clone(), second.clone())) + }) + .expect("the proof sends two same-width commitments"); + let narg = &case.proof().narg; + assert_ne!(narg[first.clone()], narg[second.clone()]); + + let deadline = Region::statement_deadline(); + let mut substituted = narg.clone(); + substituted.copy_within(second.clone(), first.start); + let mut swapped = substituted.clone(); + swapped[second.clone()].copy_from_slice(&narg[first.clone()]); + for (label, narg) in [("substituted", substituted), ("swapped", swapped)] { + assert_statement_rejects::( + &format!("{label} commitments {first:?}/{second:?}"), + case.preprocessing(), + case.public_io(), + case.trusted_advice_commitment(), + &case.with_narg(narg), + deadline, + ); + } +} + +pub type PreprocessingMutation = + fn(&mut JoltVerifierPreprocessing<::Pcs, ::Vc>); + +/// Program-commitment tampers on a committed-program fixture: each listed +/// preprocessing mutation changes the absorbed public commitments. +pub fn committed_program_tampers( + case: &C, + mutations: &[(&str, PreprocessingMutation)], +) { + assert!( + case.preprocessing().program.committed().is_some(), + "fixture must carry committed-program preprocessing" + ); + for (label, mutate) in mutations { + let mut preprocessing = case.preprocessing().clone(); + mutate(&mut preprocessing); + assert_statement_rejects::( + label, + &preprocessing, + case.public_io(), + case.trusted_advice_commitment(), + case.proof(), + Region::statement_deadline(), + ); + } +} + +/// Mutable committed-program preprocessing. +pub fn committed_mut( + preprocessing: &mut JoltVerifierPreprocessing, +) -> &mut jolt_verifier::CommittedProgramPreprocessing +where + VC: jolt_crypto::VectorCommitment, +{ + let ProgramPreprocessing::Committed(committed) = &mut preprocessing.program else { + panic!("fixture must carry committed-program preprocessing"); + }; + committed +} + +/// The proof against another fixture's preprocessing of the same guest (the +/// full program against its committed form, or the reverse): the +/// preprocessing digest and the public commitments differ, so the proof is +/// bound to a different statement. +pub fn preprocessing_swap( + case: &C, + other: &JoltVerifierPreprocessing, +) { + assert_ne!( + case.preprocessing().preprocessing_digest, + other.preprocessing_digest, + "the swap must change the preprocessing" + ); + assert_statement_rejects::( + "swapped preprocessing", + other, + case.public_io(), + case.trusted_advice_commitment(), + case.proof(), + Region::statement_deadline(), + ); +} diff --git a/crates/jolt-verifier/tests/soundness/dory.rs b/crates/jolt-verifier/tests/soundness/dory.rs new file mode 100644 index 0000000000..2f6b04b226 --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/dory.rs @@ -0,0 +1,100 @@ +//! Clear Dory fixtures. Legacy-shape fixtures run with field-inline +//! disabled; the field-inline eq-MLE fixture runs with it enabled. + +use super::checks; + +#[cfg(not(feature = "field-inline"))] +mod base { + use super::checks; + use crate::support::verifier_fixtures::{ + standard_advice_consumer_case, standard_committed_muldiv_case, standard_muldiv_case, + }; + + /// Tamper cap for the larger fixtures; muldiv runs every tamper. + const SAMPLE: Option = Some(400); + + #[test] + fn standard_muldiv_message_sweep() { + checks::message_sweep(&standard_muldiv_case(), None); + } + + #[test] + fn standard_muldiv_structural_tampers() { + checks::structural_tampers(&standard_muldiv_case(), None); + } + + #[test] + fn standard_muldiv_statement_tampers() { + let case = standard_muldiv_case(); + checks::statement_tampers(&case); + checks::header_equivocations(&case); + checks::commitment_substitutions(&case); + checks::trusted_advice_tampers(&case, &checks::sent_commitment(&case, 0)); + checks::preprocessing_swap(&case, &standard_committed_muldiv_case().preprocessing); + } + + #[test] + fn standard_advice_message_sweep() { + let case = standard_advice_consumer_case(); + checks::message_sweep(&case, SAMPLE); + checks::structural_tampers(&case, Some(32)); + } + + #[test] + fn standard_advice_statement_tampers() { + let case = standard_advice_consumer_case(); + assert!(case.trusted_advice_commitment.is_some()); + checks::statement_tampers(&case); + checks::trusted_advice_tampers(&case, &checks::sent_commitment(&case, 0)); + } + + #[test] + fn standard_committed_message_sweep() { + let case = standard_committed_muldiv_case(); + checks::message_sweep(&case, SAMPLE); + checks::structural_tampers(&case, Some(32)); + } + + #[test] + fn standard_committed_program_tampers() { + checks::committed_program_tampers( + &standard_committed_muldiv_case(), + &[ + ("swapped bytecode chunk commitments", |preprocessing| { + let committed = checks::committed_mut(preprocessing); + assert!(committed.bytecode_chunk_commitments.len() >= 2); + committed.bytecode_chunk_commitments.swap(0, 1); + }), + ( + "program image replaced by a chunk commitment", + |preprocessing| { + let committed = checks::committed_mut(preprocessing); + committed.program_image_commitment = + committed.bytecode_chunk_commitments[0].clone(); + }, + ), + ], + ); + } +} + +#[cfg(feature = "field-inline")] +#[test] +fn field_inline_eqpoly_message_sweep() { + use crate::support::verifier_fixtures::standard_field_inline_eqpoly_case; + + let case = standard_field_inline_eqpoly_case(); + checks::message_sweep(&case, Some(600)); + checks::structural_tampers(&case, Some(64)); +} + +#[cfg(feature = "field-inline")] +#[test] +fn field_inline_eqpoly_statement_tampers() { + use crate::support::verifier_fixtures::standard_field_inline_eqpoly_case; + + let case = standard_field_inline_eqpoly_case(); + checks::statement_tampers(&case); + checks::header_equivocations(&case); + checks::commitment_substitutions(&case); +} diff --git a/crates/jolt-verifier/tests/soundness/mod.rs b/crates/jolt-verifier/tests/soundness/mod.rs index 40178086ce..a8b6a69323 100644 --- a/crates/jolt-verifier/tests/soundness/mod.rs +++ b/crates/jolt-verifier/tests/soundness/mod.rs @@ -1,2 +1,29 @@ -pub mod prover_transitivity; -pub mod tampering; +//! Soundness over the argument string: every prover message is a byte range +//! of the proof's NARG, so the suites alter bytes and the statement rather +//! than typed proof fields, and pin where each alteration must be caught +//! (`support::narg::Region::deadline`). +//! +//! Each backend runs the same checks (`checks`) over its own fixtures. The +//! event log that attributes a rejection to a protocol region needs the +//! `logging` feature. + +#[cfg(all(feature = "prover-fixtures", feature = "logging"))] +mod checks; + +#[cfg(all(feature = "prover-fixtures", feature = "logging", feature = "akita"))] +mod akita; +#[cfg(all( + feature = "prover-fixtures", + feature = "logging", + not(feature = "akita"), + not(feature = "zk") +))] +mod dory; +#[cfg(all( + feature = "prover-fixtures", + feature = "logging", + not(feature = "akita"), + not(feature = "field-inline"), + feature = "zk" +))] +mod zk; diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/commitments.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/commitments.rs deleted file mode 100644 index 5e8b789f73..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/commitments.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "prover invalid-proof fixture generation is not wired yet"] -fn verifier_rejects_trusted_advice_commitment_mismatch() {} diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/configs.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/configs.rs deleted file mode 100644 index 4191879320..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/configs.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to run this verifier-transitivity fixture"] -fn verifier_rejects_config_mismatch() {} diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/mod.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/mod.rs deleted file mode 100644 index 75d279ac55..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod commitments; -pub mod configs; -pub mod openings; -pub mod preamble; -pub mod proof_shape; -pub mod zk; diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/openings.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/openings.rs deleted file mode 100644 index 36fb3365e7..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/openings.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "opening-claim rejection fixtures are not wired yet"] -fn verifier_rejects_opening_claim_mismatch() {} diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/preamble.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/preamble.rs deleted file mode 100644 index a5ab67d8cf..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/preamble.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to run this verifier-transitivity fixture"] -fn verifier_rejects_public_io_mismatch() {} diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/proof_shape.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/proof_shape.rs deleted file mode 100644 index 9303488dbe..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/proof_shape.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "prover invalid-proof fixture generation is not wired yet"] -fn verifier_rejects_mixed_proof_mode() {} diff --git a/crates/jolt-verifier/tests/soundness/prover_transitivity/zk.rs b/crates/jolt-verifier/tests/soundness/prover_transitivity/zk.rs deleted file mode 100644 index 5f91058e4b..0000000000 --- a/crates/jolt-verifier/tests/soundness/prover_transitivity/zk.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "ZK invalid-proof fixture generation is not wired yet"] -fn verifier_rejects_blindfold_mismatch() {} diff --git a/crates/jolt-verifier/tests/soundness/tampering/akita.rs b/crates/jolt-verifier/tests/soundness/tampering/akita.rs deleted file mode 100644 index 383f6adf18..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/akita.rs +++ /dev/null @@ -1,753 +0,0 @@ -//! Fixture-driven tamper suite for the akita path. -//! -//! Three layers, all over real packed-prover fixtures: -//! -//! - An exhaustive typed sweep ([`every_clear_claim_wire_rejects_offset`]): -//! every field-element leaf of the clear claims is offset by one, one at a -//! time, and the verifier must reject each. The visitor -//! ([`for_each_scalar_mut`]) fully destructures every aggregate, so a future -//! claim wire cannot be added without failing to compile until it is covered. -//! - A byte-level commitment sweep ([`every_commitment_wire_rejects_perturbation`]): -//! every serde leaf of each trace, advice, and direct-program commitment is -//! perturbed; a deserialization failure or a verifier rejection both count. -//! - Proof-shape tampers ([`akita_proof_shape_tampers_reject`], -//! [`akita_advice_commitment_presence_rejects`]): a swapped phase proof, -//! reordered direct-program commitments, and an absent trusted-advice -//! commitment. -//! -//! Together these are the active coverage behind the akita -//! `TamperCoverage::Active` manifest entries. - -#![expect( - clippy::expect_used, - clippy::panic, - reason = "fixture tamper tests should fail loudly when the stored proof shape changes" -)] - -use jolt_akita::{AkitaField, AkitaScheme}; -use jolt_claims::protocols::jolt::lattice::relations::{ - booleanity::LatticeBooleanityOutputClaims, read_raf::LatticeBytecodeReadRafOutputClaims, -}; -use jolt_claims::protocols::jolt::TracePolynomialOrder; -use jolt_field::{JoltField, Ring}; -use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; -use jolt_verifier::preprocessing::ProgramPreprocessing; -use jolt_verifier::proof::{ClearProofClaims, JoltProofClaims}; -use jolt_verifier::stages::{ - stage1::{ - outputs::{Stage1BatchOutputClaims, Stage1OutputClaims}, - OuterRemainderOutputClaims, - }, - stage2::outputs::{ - InstructionClaimReductionOutputClaims, ProductRemainderOutputClaims, - RamOutputCheckOutputClaims, RamRafEvaluationOutputClaims, RamReadWriteOutputClaims, - Stage2BatchOutputClaims, Stage2OutputClaims, - }, - stage3::outputs::{ - InstructionInputOutputClaims, RegistersClaimReductionOutputClaims, - SpartanShiftOutputClaims, Stage3OutputClaims, - }, - stage4::{ - outputs::Stage4OutputClaims, RamValCheckOutputClaims, RegistersReadWriteOutputClaims, - }, - stage5::{ - outputs::Stage5OutputClaims, InstructionReadRafOutputClaims, - RamRaClaimReductionOutputClaims, RegistersValEvaluationOutputClaims, - }, - stage6a::outputs::{ - BooleanityAddressPhaseOutputClaims, BytecodeReadRafAddressPhaseOutputClaims, - Stage6aOutputClaims, - }, - stage6b::outputs::{ - BytecodeReductionCyclePhaseOutputClaims, InstructionRaVirtualizationOutputClaims, - ProgramImageReductionCyclePhaseOutputClaims, RamHammingBooleanityOutputClaims, - RamRaVirtualizationOutputClaims, Stage6bOutputClaims, - }, - stage7::{ - committed_reduction_address_phase::{ - BytecodeReductionAddressPhaseOutputClaims, - ProgramImageReductionAddressPhaseOutputClaims, - }, - hamming_weight_claim_reduction::HammingWeightClaimReductionOutputClaims, - outputs::Stage7OutputClaims, - }, -}; -use jolt_verifier::VerifierError; - -use crate::support::akita_fixtures::{ - akita_advice_case, akita_committed_muldiv_case, akita_muldiv_case, AkitaFixtureCase, - AkitaJoltProof, -}; -use crate::support::assert_rejects; - -/// The single packed `OneHotTrace` commitment object type (also the advice object -/// type): the concrete `Commitment::Output` of the akita scheme. -type AkitaCommitment = ::Output; - -fn one() -> AkitaField { - AkitaField::from_u64(1) -} - -fn clear_claims_mut(proof: &mut AkitaJoltProof) -> &mut ClearProofClaims { - match &mut proof.claims { - JoltProofClaims::Clear(claims) => claims, - JoltProofClaims::Zk { .. } => panic!("packed akita fixtures always carry clear claims"), - } -} - -/// Visit every field-element leaf of the clear claims by mutable reference, in -/// a fixed order. Each aggregate is fully destructured (no `..`), so a new -/// claim field is a compile error here until it is threaded through — the wire -/// coverage the sweep depends on cannot silently regress. -fn for_each_scalar_mut(claims: &mut ClearProofClaims, f: &mut impl FnMut(&mut F)) { - let f: &mut dyn FnMut(&mut F) = f; - let ClearProofClaims { - stage1, - stage2, - stage3, - stage4, - stage5, - stage6a, - stage6b, - stage7, - } = claims; - visit_stage1(stage1, f); - visit_stage2(stage2, f); - visit_stage3(stage3, f); - visit_stage4(stage4, f); - visit_stage5(stage5, f); - visit_stage6a(stage6a, f); - visit_stage6b(stage6b, f); - visit_stage7(stage7, f); -} - -fn visit_stage1(claims: &mut Stage1OutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage1OutputClaims { - uniskip_output_claim, - outer, - } = claims; - f(uniskip_output_claim); - let Stage1BatchOutputClaims { outer_remainder } = outer; - let OuterRemainderOutputClaims { - left_instruction_input, - right_instruction_input, - product, - should_branch, - pc, - unexpanded_pc, - imm, - ram_address, - rs1_value, - rs2_value, - rd_write_value, - ram_read_value, - ram_write_value, - left_lookup_operand, - right_lookup_operand, - next_unexpanded_pc, - next_pc, - next_is_virtual, - next_is_first_in_sequence, - lookup_output, - should_jump, - add_operands, - subtract_operands, - multiply_operands, - load, - store, - jump, - write_lookup_output_to_rd, - virtual_instruction, - assert, - do_not_update_unexpanded_pc, - advice, - is_compressed, - is_first_in_sequence, - is_last_in_sequence, - } = outer_remainder; - for scalar in [ - left_instruction_input, - right_instruction_input, - product, - should_branch, - pc, - unexpanded_pc, - imm, - ram_address, - rs1_value, - rs2_value, - rd_write_value, - ram_read_value, - ram_write_value, - left_lookup_operand, - right_lookup_operand, - next_unexpanded_pc, - next_pc, - next_is_virtual, - next_is_first_in_sequence, - lookup_output, - should_jump, - add_operands, - subtract_operands, - multiply_operands, - load, - store, - jump, - write_lookup_output_to_rd, - virtual_instruction, - assert, - do_not_update_unexpanded_pc, - advice, - is_compressed, - is_first_in_sequence, - is_last_in_sequence, - ] { - f(scalar); - } -} - -fn visit_stage2(claims: &mut Stage2OutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage2OutputClaims { - product_uniskip_output_claim, - batch_outputs, - } = claims; - f(product_uniskip_output_claim); - let Stage2BatchOutputClaims { - ram_read_write, - product_remainder, - instruction_claim_reduction, - ram_raf_evaluation, - ram_output_check, - } = batch_outputs; - let RamReadWriteOutputClaims { val, ra, inc } = ram_read_write; - let ProductRemainderOutputClaims { - left_instruction_input, - right_instruction_input, - jump_flag, - write_lookup_output_to_rd, - lookup_output, - branch_flag, - next_is_noop, - virtual_instruction, - } = product_remainder; - let InstructionClaimReductionOutputClaims { - lookup_output: icr_lookup_output, - left_lookup_operand, - right_lookup_operand, - left_instruction_input: icr_left_instruction_input, - right_instruction_input: icr_right_instruction_input, - } = instruction_claim_reduction; - let RamRafEvaluationOutputClaims { ram_ra } = ram_raf_evaluation; - let RamOutputCheckOutputClaims { val_final } = ram_output_check; - for scalar in [ - val, - ra, - inc, - left_instruction_input, - right_instruction_input, - jump_flag, - write_lookup_output_to_rd, - lookup_output, - branch_flag, - next_is_noop, - virtual_instruction, - icr_lookup_output, - left_lookup_operand, - right_lookup_operand, - icr_left_instruction_input, - icr_right_instruction_input, - ram_ra, - val_final, - ] { - f(scalar); - } -} - -fn visit_stage3(claims: &mut Stage3OutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage3OutputClaims { - shift, - instruction_input, - registers_claim_reduction, - } = claims; - let SpartanShiftOutputClaims { - unexpanded_pc, - pc, - is_virtual, - is_first_in_sequence, - is_noop, - } = shift; - let InstructionInputOutputClaims { - left_operand_is_rs1, - rs1_value, - left_operand_is_pc, - unexpanded_pc: ii_unexpanded_pc, - right_operand_is_rs2, - rs2_value, - right_operand_is_imm, - imm, - } = instruction_input; - let RegistersClaimReductionOutputClaims { - rd_write_value, - rs1_value: rcr_rs1_value, - rs2_value: rcr_rs2_value, - } = registers_claim_reduction; - for scalar in [ - unexpanded_pc, - pc, - is_virtual, - is_first_in_sequence, - is_noop, - left_operand_is_rs1, - rs1_value, - left_operand_is_pc, - ii_unexpanded_pc, - right_operand_is_rs2, - rs2_value, - right_operand_is_imm, - imm, - rd_write_value, - rcr_rs1_value, - rcr_rs2_value, - ] { - f(scalar); - } -} - -fn visit_stage4(claims: &mut Stage4OutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage4OutputClaims { - registers_read_write, - ram_val_check, - } = claims; - let RegistersReadWriteOutputClaims { - registers_val, - rs1_ra, - rs2_ra, - rd_wa, - rd_inc, - } = registers_read_write; - for scalar in [registers_val, rs1_ra, rs2_ra, rd_wa, rd_inc] { - f(scalar); - } - let RamValCheckOutputClaims { - untrusted_advice, - trusted_advice, - program_image, - ram_ra, - ram_inc, - } = ram_val_check; - for scalar in [untrusted_advice, trusted_advice, program_image] - .into_iter() - .flatten() - { - f(scalar); - } - for scalar in [ram_ra, ram_inc] { - f(scalar); - } -} - -fn visit_stage5(claims: &mut Stage5OutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage5OutputClaims { - instruction_read_raf, - ram_ra_claim_reduction, - registers_val_evaluation, - } = claims; - let InstructionReadRafOutputClaims { - lookup_table_flags, - instruction_ra, - instruction_raf_flag, - } = instruction_read_raf; - for scalar in lookup_table_flags.iter_mut() { - f(scalar); - } - for scalar in instruction_ra.iter_mut() { - f(scalar); - } - f(instruction_raf_flag); - let RamRaClaimReductionOutputClaims { ram_ra } = ram_ra_claim_reduction; - f(ram_ra); - let RegistersValEvaluationOutputClaims { rd_inc, rd_wa } = registers_val_evaluation; - for scalar in [rd_inc, rd_wa] { - f(scalar); - } -} - -fn visit_stage6a(claims: &mut Stage6aOutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage6aOutputClaims { - bytecode_read_raf, - booleanity, - } = claims; - let BytecodeReadRafAddressPhaseOutputClaims { - intermediate, - val_stages, - } = bytecode_read_raf; - f(intermediate); - for scalar in val_stages.iter_mut() { - f(scalar); - } - let BooleanityAddressPhaseOutputClaims { - intermediate: booleanity_intermediate, - } = booleanity; - f(booleanity_intermediate); -} - -fn visit_stage6b(claims: &mut Stage6bOutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage6bOutputClaims { - bytecode_read_raf, - booleanity, - ram_hamming_booleanity, - ram_ra_virtualization, - instruction_ra_virtualization, - bytecode_reduction, - program_image_reduction, - } = claims; - let LatticeBytecodeReadRafOutputClaims { - bytecode_ra, - fused_inc, - } = bytecode_read_raf; - for scalar in bytecode_ra.iter_mut() { - f(scalar); - } - f(fused_inc); - let LatticeBooleanityOutputClaims { - instruction_ra, - bytecode_ra: booleanity_bytecode_ra, - ram_ra, - balanced_inc_digits, - balanced_inc_carry, - } = booleanity; - for scalar in instruction_ra.iter_mut() { - f(scalar); - } - for scalar in booleanity_bytecode_ra.iter_mut() { - f(scalar); - } - for scalar in ram_ra.iter_mut() { - f(scalar); - } - for scalar in balanced_inc_digits.iter_mut() { - f(scalar); - } - f(balanced_inc_carry); - let RamHammingBooleanityOutputClaims { ram_hamming_weight } = ram_hamming_booleanity; - f(ram_hamming_weight); - let RamRaVirtualizationOutputClaims { - ram_ra: virt_ram_ra, - } = ram_ra_virtualization; - for scalar in virt_ram_ra.iter_mut() { - f(scalar); - } - let InstructionRaVirtualizationOutputClaims { - committed_instruction_ra, - } = instruction_ra_virtualization; - for scalar in committed_instruction_ra.iter_mut() { - f(scalar); - } - if let Some(BytecodeReductionCyclePhaseOutputClaims { - intermediate, - chunks, - }) = bytecode_reduction - { - if let Some(scalar) = intermediate { - f(scalar); - } - for scalar in chunks.iter_mut() { - f(scalar); - } - } - if let Some(ProgramImageReductionCyclePhaseOutputClaims { program_image }) = - program_image_reduction - { - f(program_image); - } -} - -fn visit_stage7(claims: &mut Stage7OutputClaims, f: &mut dyn FnMut(&mut F)) { - let Stage7OutputClaims { - hamming_weight_claim_reduction, - bytecode_address_phase, - program_image_address_phase, - } = claims; - let HammingWeightClaimReductionOutputClaims { - instruction_ra, - bytecode_ra, - ram_ra, - balanced_inc_digits, - balanced_inc_carry, - } = hamming_weight_claim_reduction; - for scalar in instruction_ra.iter_mut() { - f(scalar); - } - for scalar in bytecode_ra.iter_mut() { - f(scalar); - } - for scalar in ram_ra.iter_mut() { - f(scalar); - } - for scalar in balanced_inc_digits.iter_mut() { - f(scalar); - } - f(balanced_inc_carry); - if let Some(BytecodeReductionAddressPhaseOutputClaims { chunks }) = bytecode_address_phase { - for scalar in chunks.iter_mut() { - f(scalar); - } - } - if let Some(ProgramImageReductionAddressPhaseOutputClaims { program_image }) = - program_image_address_phase - { - f(program_image); - } -} - -fn clear_claim_scalar_count(case: &AkitaFixtureCase) -> usize { - let mut proof = case.proof.clone(); - let mut count = 0usize; - for_each_scalar_mut(clear_claims_mut(&mut proof), &mut |_| count += 1); - count -} - -/// Offset each clear-claim scalar in turn by one and assert the verifier -/// rejects the mutated proof. -fn sweep_clear_claim_offsets(case: &AkitaFixtureCase, scalar_count: usize) { - for target in 0..scalar_count { - let mut proof = case.proof.clone(); - let mut index = 0usize; - for_each_scalar_mut(clear_claims_mut(&mut proof), &mut |scalar| { - if index == target { - *scalar += one(); - } - index += 1; - }); - assert_rejects(case.verify_proof(&proof)); - } -} - -/// Every clear-claim scalar of every fixture case rejects a one-off offset. -#[test] -fn every_clear_claim_wire_rejects_offset() { - let muldiv = akita_muldiv_case(); - let muldiv_scalars = clear_claim_scalar_count(muldiv); - assert!( - muldiv_scalars >= 150, - "muldiv exposes only {muldiv_scalars} clear-claim scalars, expected at least 150" - ); - sweep_clear_claim_offsets(muldiv, muldiv_scalars); - - let advice = akita_advice_case(); - sweep_clear_claim_offsets(advice, clear_claim_scalar_count(advice)); - - let committed = akita_committed_muldiv_case(); - sweep_clear_claim_offsets(committed, clear_claim_scalar_count(committed)); -} - -/// Collect every perturbable serde leaf under `value`, keyed by a dot/index -/// path rooted at `prefix`. A non-empty all-number array (a limb/byte column) -/// is one leaf; an empty array has none. -fn leaf_paths(prefix: &str, value: &serde_json::Value, out: &mut Vec) { - match value { - serde_json::Value::Object(map) => { - for (key, child) in map { - leaf_paths(&format!("{prefix}.{key}"), child, out); - } - } - serde_json::Value::Array(items) => { - if items.is_empty() { - // No leaf to perturb. - } else if items.iter().all(serde_json::Value::is_number) { - out.push(prefix.to_string()); - } else { - for (index, child) in items.iter().enumerate() { - leaf_paths(&format!("{prefix}[{index}]"), child, out); - } - } - } - serde_json::Value::Number(_) | serde_json::Value::String(_) => { - out.push(prefix.to_string()); - } - serde_json::Value::Bool(_) | serde_json::Value::Null => {} - } -} - -/// Perturb the leaf at `path` (as produced by [`leaf_paths`], whose first -/// segment names the root and is skipped). -fn perturb_leaf(value: &mut serde_json::Value, path: &str) { - let mut cursor = value; - for segment in path.split('.').skip(1) { - let (key, indices) = match segment.find('[') { - Some(split) => (&segment[..split], &segment[split..]), - None => (segment, ""), - }; - if !key.is_empty() { - cursor = cursor.get_mut(key).expect("path segment must exist"); - } - for index in indices.split(['[', ']']).filter(|s| !s.is_empty()) { - let index: usize = index.parse().expect("index must parse"); - cursor = cursor.get_mut(index).expect("indexed segment must exist"); - } - } - match cursor { - serde_json::Value::Number(number) => { - let flipped = number.as_u64().map_or(1, |n| n ^ 1); - *cursor = serde_json::Value::from(flipped); - } - serde_json::Value::String(text) => { - text.push('0'); - } - serde_json::Value::Array(items) => { - let first = items.first_mut().expect("swept arrays are non-empty"); - let flipped = first.as_u64().map_or(1, |n| n ^ 1); - *first = serde_json::Value::from(flipped); - } - other => panic!("unsupported leaf {other:?}"), - } -} - -/// Perturb every serde leaf of `commitment` one at a time. A mutation that no -/// longer deserializes is rejected at the boundary; otherwise verification -/// must fail. -fn sweep_commitment( - commitment: &AkitaCommitment, - minimum_leaves: usize, - mut verify: impl FnMut(AkitaCommitment) -> Result<(), VerifierError>, -) { - let value = serde_json::to_value(commitment).expect("commitment serializes"); - let mut paths = Vec::new(); - leaf_paths("commitment", &value, &mut paths); - assert!( - paths.len() >= minimum_leaves, - "expected at least {minimum_leaves} commitment leaves, found {}", - paths.len() - ); - for path in paths { - let mut mutated = value.clone(); - perturb_leaf(&mut mutated, &path); - match serde_json::from_value::(mutated) { - Err(_) => {} - Ok(commitment) => assert_rejects(verify(commitment)), - } - } -} - -/// Every trace, advice, and direct-program commitment rejects a leaf-level -/// perturbation. -#[test] -fn every_commitment_wire_rejects_perturbation() { - for case in [ - akita_muldiv_case(), - akita_advice_case(), - akita_committed_muldiv_case(), - ] { - sweep_commitment(&case.proof.commitments, 6, |commitment| { - let mut proof = case.proof.clone(); - proof.commitments = commitment; - case.verify_proof(&proof) - }); - } - - let advice = akita_advice_case(); - if let Some(untrusted) = &advice.proof.untrusted_advice_commitment { - sweep_commitment(untrusted, 6, |commitment| { - let mut proof = advice.proof.clone(); - proof.untrusted_advice_commitment = Some(commitment); - advice.verify_proof(&proof) - }); - } - if let Some(trusted) = &advice.trusted_advice_commitment { - sweep_commitment(trusted, 6, |commitment| { - jolt_verifier::verify::( - &advice.preprocessing, - &advice.public_io, - &advice.proof, - Some(&commitment), - ) - }); - } - - let committed = akita_committed_muldiv_case(); - let ProgramPreprocessing::Committed(program) = &committed.preprocessing.program else { - panic!("committed fixture must carry committed preprocessing"); - }; - for (index, original) in program - .direct_program_commitments - .iter() - .cloned() - .enumerate() - { - sweep_commitment(&original, 6, |commitment| { - let mut preprocessing = committed.preprocessing.clone(); - let ProgramPreprocessing::Committed(program) = &mut preprocessing.program else { - panic!("committed fixture must carry committed preprocessing"); - }; - program.direct_program_commitments[index] = commitment; - jolt_verifier::verify::( - &preprocessing, - &committed.public_io, - &committed.proof, - None, - ) - }); - } -} - -/// A swapped phase proof and reordered direct-program commitments both fail -/// closed. -#[test] -fn akita_proof_shape_tampers_reject() { - let muldiv = akita_muldiv_case(); - let mut proof = muldiv.proof.clone(); - proof.stages.stage6b_sumcheck_proof = proof.stages.stage3_sumcheck_proof.clone(); - assert_rejects(muldiv.verify_proof(&proof)); - - let committed = akita_committed_muldiv_case(); - let mut preprocessing = committed.preprocessing.clone(); - let ProgramPreprocessing::Committed(program) = &mut preprocessing.program else { - panic!("committed fixture must carry committed preprocessing"); - }; - program.direct_program_commitments.swap(0, 1); - assert_rejects(jolt_verifier::verify::< - AkitaField, - AkitaScheme, - AkitaVc, - AkitaTranscript, - >( - &preprocessing, - &committed.public_io, - &committed.proof, - None, - )); - - let mut preprocessing = committed.preprocessing.clone(); - let ProgramPreprocessing::Committed(program) = &mut preprocessing.program else { - panic!("committed fixture must carry committed preprocessing"); - }; - program.trace_order = match program.trace_order { - TracePolynomialOrder::CycleMajor => TracePolynomialOrder::AddressMajor, - TracePolynomialOrder::AddressMajor => TracePolynomialOrder::CycleMajor, - }; - assert_rejects(jolt_verifier::verify::< - AkitaField, - AkitaScheme, - AkitaVc, - AkitaTranscript, - >( - &preprocessing, - &committed.public_io, - &committed.proof, - None, - )); -} - -/// The advice case fails closed when its trusted-advice commitment is absent: -/// the direct dense opening has no commitment to bind against. -#[test] -fn akita_advice_commitment_presence_rejects() { - let advice = akita_advice_case(); - let result = jolt_verifier::verify::( - &advice.preprocessing, - &advice.public_io, - &advice.proof, - None, - ); - assert_rejects(result); -} diff --git a/crates/jolt-verifier/tests/soundness/tampering/commitments.rs b/crates/jolt-verifier/tests/soundness/tampering/commitments.rs deleted file mode 100644 index a977069fe9..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/commitments.rs +++ /dev/null @@ -1,117 +0,0 @@ -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use crate::support::{tamper_manifest, verifier_fixtures}; - -#[test] -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tampered_commitment_order_reject() { - for base in verifier_fixtures::ordinary_tamper_bases() { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.commitments.order"), - &base, - |case| { - std::mem::swap( - &mut case.proof.commitments.rd_inc, - &mut case.proof.commitments.ram_inc, - ); - }, - ); - } -} - -#[test] -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tampered_commitment_value_reject() { - for base in verifier_fixtures::ordinary_tamper_bases() { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.commitments.value"), - &base, - |case| { - case.proof.commitments.rd_inc = case.proof.commitments.ram_inc.clone(); - }, - ); - } -} - -#[test] -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tampered_commitment_count_reject() { - for base in verifier_fixtures::ordinary_tamper_bases() { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.commitments.missing"), - &base, - |case| { - let _ = case.proof.commitments.bytecode_ra.pop(); - }, - ); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.commitments.extra"), - &base, - |case| { - case.proof - .commitments - .bytecode_ra - .push(case.proof.commitments.ram_inc.clone()); - }, - ); - } -} - -#[test] -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -fn tampered_joint_opening_proof_reject() { - let base = verifier_fixtures::standard_muldiv_case(); - let replacement = verifier_fixtures::standard_fibonacci_small_case() - .proof - .joint_opening_proof; - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.joint_opening_proof"), - &base, - |case| { - case.proof.joint_opening_proof = replacement; - }, - ); -} - -#[test] -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn replaced_untrusted_advice_commitment_rejects() { - let base = verifier_fixtures::standard_advice_consumer_case(); - assert!( - base.proof.untrusted_advice_commitment.is_some(), - "advice-consumer fixture must carry an untrusted advice commitment" - ); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.untrusted_advice_commitment"), - &base, - |case| { - // valid-but-wrong payload: another commitment from the same proof - case.proof.untrusted_advice_commitment = Some(case.proof.commitments.rd_inc.clone()); - }, - ); -} - -#[test] -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn replaced_trusted_advice_commitment_rejects() { - let base = verifier_fixtures::standard_advice_consumer_case(); - assert!( - base.trusted_advice_commitment.is_some(), - "advice-consumer fixture must carry a trusted advice commitment" - ); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("trusted_advice_commitment"), - &base, - |case| { - case.trusted_advice_commitment = Some(case.proof.commitments.rd_inc.clone()); - }, - ); -} - -#[test] -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[ignore = "direct commitment tampering fixtures are not wired yet"] -fn tampered_commitment_order_reject() {} diff --git a/crates/jolt-verifier/tests/soundness/tampering/configs.rs b/crates/jolt-verifier/tests/soundness/tampering/configs.rs deleted file mode 100644 index 8c6b269a78..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/configs.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[test] -#[ignore = "direct config tampering fixtures are not wired yet"] -fn tampered_trace_length_reject() {} diff --git a/crates/jolt-verifier/tests/soundness/tampering/field_inline.rs b/crates/jolt-verifier/tests/soundness/tampering/field_inline.rs deleted file mode 100644 index f3623d6247..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/field_inline.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Fixture-driven tamper suite for the field-inline path: every field-inline wire cell of the -//! clear claims is offset by one over a real modular-prover fixture (the eq-MLE field-inline -//! guest — the field-inline-capable prover is the modular one), and the verifier must reject -//! each mutation. These are the active coverage behind the field-inline -//! `TamperCoverage::Active` manifest entries. - -#![cfg_attr( - all( - feature = "prover-fixtures", - feature = "field-inline", - not(feature = "zk") - ), - expect( - clippy::panic, - reason = "fixture tamper tests should fail loudly when the stored proof shape changes" - ) -)] - -#[cfg(all( - feature = "prover-fixtures", - feature = "field-inline", - not(feature = "zk") -))] -mod active { - use jolt_claims::protocols::field_inline::relations::product::FieldRegistersProductOutputClaims; - use jolt_claims::protocols::field_inline::relations::spartan::FieldRegistersSpartanOuterOutputClaims; - use jolt_field::{Fr, Ring}; - use jolt_verifier::proof::{ClearProofClaims, JoltProofClaims}; - - use crate::support::tamper_manifest::{ - assert_verifier_fixture_tamper_rejects, required_target, - }; - use crate::support::verifier_fixtures::{ - standard_field_inline_eqpoly_case, VerifierFixtureCase, - }; - - type CellSelector = fn(&mut ClearProofClaims) -> &mut Fr; - - fn clear_claims(case: &mut VerifierFixtureCase) -> &mut ClearProofClaims { - let JoltProofClaims::Clear(claims) = &mut case.proof.claims else { - panic!("field-inline fixture must carry clear claims"); - }; - claims - } - - /// Offset each listed field-inline claim cell by one on a fresh clone of `base`; each - /// mutation must reject, under the named (Active) manifest target. - fn offset_each_cell(base: &VerifierFixtureCase, target: &str, cells: &[CellSelector]) { - for select in cells { - assert_verifier_fixture_tamper_rejects(required_target(target), base, |case| { - *select(clear_claims(case)) += Fr::from_u64(1); - }); - } - } - - #[test] - fn tampered_field_inline_stage1_outer_claims_reject() { - fn outer( - claims: &mut ClearProofClaims, - ) -> &mut FieldRegistersSpartanOuterOutputClaims { - &mut claims.stage1.outer.outer_remainder.field_inline - } - offset_each_cell( - &standard_field_inline_eqpoly_case(), - "stage1.claims.field_inline_outer", - &[ - |claims| &mut outer(claims).rs1_value, - |claims| &mut outer(claims).rs2_value, - |claims| &mut outer(claims).rd_value, - |claims| &mut outer(claims).product, - |claims| &mut outer(claims).inv_product, - ], - ); - } - - #[test] - fn tampered_field_inline_stage2_claim_reduction_claims_reject() { - offset_each_cell( - &standard_field_inline_eqpoly_case(), - "stage2.claims.batch_outputs.field_registers_claim_reduction", - &[ - |claims| { - &mut claims - .stage2 - .batch_outputs - .field_registers_claim_reduction - .rd_value - }, - |claims| { - &mut claims - .stage2 - .batch_outputs - .field_registers_claim_reduction - .rs1_value - }, - |claims| { - &mut claims - .stage2 - .batch_outputs - .field_registers_claim_reduction - .rs2_value - }, - ], - ); - } - - #[test] - fn tampered_field_inline_stage2_product_appendage_claims_reject() { - fn product( - claims: &mut ClearProofClaims, - ) -> &mut FieldRegistersProductOutputClaims { - &mut claims.stage2.batch_outputs.product_remainder.field_inline - } - offset_each_cell( - &standard_field_inline_eqpoly_case(), - "stage2.claims.field_inline_product", - &[ - |claims| &mut product(claims).rs1_value, - |claims| &mut product(claims).rs2_value, - |claims| &mut product(claims).rd_value, - ], - ); - } - - #[test] - fn tampered_field_inline_stage4_read_write_claims_reject() { - offset_each_cell( - &standard_field_inline_eqpoly_case(), - "stage4.claims.field_registers_read_write", - &[ - |claims| &mut claims.stage4.field_registers_read_write.registers_val, - |claims| &mut claims.stage4.field_registers_read_write.rs1_ra, - |claims| &mut claims.stage4.field_registers_read_write.rs2_ra, - |claims| &mut claims.stage4.field_registers_read_write.rd_wa, - |claims| &mut claims.stage4.field_registers_read_write.rd_inc, - ], - ); - } - - #[test] - fn tampered_field_inline_stage5_val_evaluation_claims_reject() { - offset_each_cell( - &standard_field_inline_eqpoly_case(), - "stage5.claims.field_registers_val_evaluation", - &[ - |claims| &mut claims.stage5.field_registers_val_evaluation.rd_inc, - |claims| &mut claims.stage5.field_registers_val_evaluation.rd_wa, - ], - ); - } - - #[test] - fn tampered_field_inline_stage6_inc_claim_reduction_claim_rejects() { - offset_each_cell( - &standard_field_inline_eqpoly_case(), - "stage6.claims.field_registers_inc_claim_reduction.rd_inc", - &[|claims| &mut claims.stage6b.field_registers_inc_claim_reduction.rd_inc], - ); - } -} diff --git a/crates/jolt-verifier/tests/soundness/tampering/manifest.rs b/crates/jolt-verifier/tests/soundness/tampering/manifest.rs deleted file mode 100644 index 8fab51fb2b..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/manifest.rs +++ /dev/null @@ -1,156 +0,0 @@ -#[cfg(not(feature = "akita"))] -use std::path::Path; - -use jolt_verifier::VerifierError; - -use crate::support::tamper_manifest::{ - all_targets, clear_claim_leaf_paths, manifest_paths, observed_rejection_phase, - proof_field_paths, target_names_are_unique, verifier_owned_targets_without_active_coverage, - TamperCoverage, -}; -use crate::support::VerifierPhase; - -#[test] -fn batch_sumcheck_errors_have_rejection_phases() { - for (stage, phase) in [ - ("Stage1Batch", VerifierPhase::Stage1), - ("Stage2Batch", VerifierPhase::Stage2), - ("Stage3", VerifierPhase::Stage3), - ("Stage4", VerifierPhase::Stage4), - ("Stage5", VerifierPhase::Stage5), - ("Stage6a", VerifierPhase::Stage6), - ("Stage6b", VerifierPhase::Stage6), - ("Stage7", VerifierPhase::Stage7), - #[cfg(feature = "field-inline")] - ("FieldRegistersSpartanOuter", VerifierPhase::Stage1), - #[cfg(feature = "field-inline")] - ("FieldRegistersClaimReduction", VerifierPhase::Stage2), - #[cfg(feature = "field-inline")] - ("FieldRegistersProduct", VerifierPhase::Stage2), - #[cfg(feature = "field-inline")] - ("FieldRegistersReadWriteChecking", VerifierPhase::Stage4), - #[cfg(feature = "field-inline")] - ("FieldRegistersValEvaluation", VerifierPhase::Stage5), - #[cfg(feature = "field-inline")] - ("FieldRegistersIncClaimReduction", VerifierPhase::Stage6), - ] { - let error = VerifierError::StageClaimSumcheckFailed { - stage: stage.to_string(), - reason: "invalid round polynomial".to_string(), - }; - assert_eq!(observed_rejection_phase(&error), Some(phase), "{stage}"); - } -} - -#[test] -#[should_panic(expected = "unrecognized verifier stage")] -fn unknown_stage_errors_cannot_skip_rejection_deadlines() { - let error = VerifierError::StageClaimSumcheckFailed { - stage: "UnknownStage".to_string(), - reason: "invalid round polynomial".to_string(), - }; - let _ = observed_rejection_phase(&error); -} - -#[test] -fn tamper_manifest_target_names_are_unique() { - assert!(target_names_are_unique()); -} - -/// Closes the Active ⇒ test direction: `assert_verifier_fixture_tamper_rejects` -/// proves an exercised target is Active, but nothing else stops a target from -/// being flipped Active (inflating the enforced tamper ratio) without a test. -/// Every tamper test names its target by string literal, so requiring each -/// Active name to appear in the tampering test sources makes the ratio honest; -/// deleting a test or its target reference fails here. -/// -/// Standard-mode only: the akita suite tampers its targets structurally -/// (destructured field mutation in tampering/akita.rs) without naming them, -/// so the name-literal convention this audit enforces does not apply to the -/// akita-gated target arrays. -#[cfg(not(feature = "akita"))] -#[test] -#[expect( - clippy::expect_used, - reason = "manifest audits should fail loudly if test sources are unreadable" -)] -fn active_tamper_targets_are_referenced_by_tampering_tests() { - let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/soundness/tampering"); - let mut sources = String::new(); - for entry in std::fs::read_dir(&dir).expect("tampering test directory is readable") { - let path = entry.expect("tampering test directory entry").path(); - if path.extension().is_some_and(|ext| ext == "rs") - && path.file_name().is_some_and(|name| name != "manifest.rs") - { - sources.push_str(&std::fs::read_to_string(&path).expect("tampering test source")); - } - } - - let unreferenced = all_targets() - .into_iter() - .filter(|target| target.coverage == TamperCoverage::Active) - .filter(|target| !sources.contains(&format!("\"{}\"", target.name))) - .map(|target| target.name) - .collect::>(); - - assert!( - unreferenced.is_empty(), - "Active tamper targets with no referencing tamper test: {unreferenced:?}" - ); -} - -#[test] -fn tamper_manifest_covers_clear_claim_fields() { - let manifest_paths = manifest_paths(); - let missing = clear_claim_leaf_paths() - .into_iter() - .filter(|path| !manifest_paths.contains(path.as_str())) - .collect::>(); - - assert!( - missing.is_empty(), - "clear claim fields missing from tamper manifest: {missing:?}" - ); -} - -#[test] -fn tamper_manifest_covers_top_level_proof_fields() { - let manifest_paths = manifest_paths(); - let missing = proof_field_paths() - .iter() - .copied() - .filter(|path| !manifest_paths.contains(*path)) - .collect::>(); - - assert!( - missing.is_empty(), - "top-level proof fields missing from tamper manifest: {missing:?}" - ); -} - -#[test] -fn verifier_owned_inactive_tamper_targets_are_documented() { - let undocumented = verifier_owned_targets_without_active_coverage() - .into_iter() - .filter(|target| target.reason.is_empty()) - .collect::>(); - - assert!( - undocumented.is_empty(), - "verifier-owned tamper targets without active coverage need a reason: {undocumented:?}" - ); -} - -#[test] -fn deferred_tamper_targets_are_documented() { - let undocumented = all_targets() - .into_iter() - .filter(|target| target.coverage != TamperCoverage::Active) - .filter(|target| target.reason.is_empty()) - .collect::>(); - - assert!( - undocumented.is_empty(), - "deferred or ignored tamper targets need a reason: {undocumented:?}" - ); -} diff --git a/crates/jolt-verifier/tests/soundness/tampering/mod.rs b/crates/jolt-verifier/tests/soundness/tampering/mod.rs deleted file mode 100644 index 7a903bf98b..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -// The `not(akita)` modules tamper the base (dory) proof shape, which does not exist under the -// akita feature — one compiled verifier runs exactly one protocol. The packed pipeline gets -// its own typed tamper suite (`akita`: clear-claim wire sweep, commitment-byte sweeps, -// proof-shape and presence tampers); only the shape-agnostic `manifest` checks run under both. -// Same per-family split for field-inline: legacy-fixture-driven suites run only when -// field-inline is disabled (the verifier with field-inline enabled rejects legacy proofs at -// the protocol-config gate); the field-inline wire cells get their own typed suite over -// modular-prover fixtures (`field_inline`). The ordinary `sumcheck`, `openings`, and -// `commitments` sweeps run under both field-inline configurations over -// `ordinary_tamper_bases()` (legacy muldiv with field-inline disabled, the modular eq-MLE -// fixture with field-inline enabled), so the ordinary stage payloads are also rejected by the -// composed verifier; their legacy-advice-only tests keep field-inline disabled. The -// shape-agnostic `manifest` checks run under every family. The akita sweep is -// legacy-fixture-driven too, so it also runs with field-inline disabled (its exhaustive claim -// destructuring would otherwise need the field-inline wire cells no legacy fixture can -// populate). -#[cfg(all( - feature = "prover-fixtures", - feature = "akita", - not(feature = "field-inline") -))] -pub mod akita; -#[cfg(not(feature = "akita"))] -pub mod commitments; -#[cfg(not(feature = "akita"))] -pub mod configs; -#[cfg(all( - feature = "prover-fixtures", - feature = "field-inline", - not(feature = "akita"), - not(feature = "zk") -))] -pub mod field_inline; -pub mod manifest; -#[cfg(not(feature = "akita"))] -pub mod openings; -#[cfg(all(not(feature = "akita"), not(feature = "field-inline")))] -pub mod preamble; -#[cfg(all(not(feature = "akita"), not(feature = "field-inline")))] -pub mod proof_shape; -#[cfg(not(feature = "akita"))] -pub mod sumcheck; -#[cfg(all(not(feature = "akita"), not(feature = "field-inline")))] -pub mod zk; diff --git a/crates/jolt-verifier/tests/soundness/tampering/openings.rs b/crates/jolt-verifier/tests/soundness/tampering/openings.rs deleted file mode 100644 index 48bb49b9ed..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/openings.rs +++ /dev/null @@ -1,79 +0,0 @@ -#![cfg_attr( - all(feature = "prover-fixtures", not(feature = "zk")), - expect( - clippy::expect_used, - reason = "fixture dimension helpers should fail loudly when stored verifier objects are malformed" - ) -)] - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use crate::support::proof_claims::offset_opening_claim; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use crate::support::{tamper_manifest, verifier_fixtures}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_claims::protocols::jolt::{ - geometry::{ - committed_openings::{final_opening_id, final_opening_polynomial_order}, - dimensions::JoltFormulaDimensions, - }, - JoltOpeningId, -}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_field::{Fr, Ring}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_lookup_tables::XLEN as RISCV_XLEN; - -#[test] -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tampered_opening_value_reject() { - for base in verifier_fixtures::ordinary_tamper_bases() { - tamper_final_opening_claims(&base); - } - #[cfg(not(feature = "field-inline"))] - tamper_final_opening_claims(&verifier_fixtures::standard_advice_consumer_case()); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_final_opening_claims(base: &verifier_fixtures::VerifierFixtureCase) { - for id in stage8_final_opening_ids(base) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("stage8.opening_claim_values"), - base, - |case| { - assert!( - offset_opening_claim(&mut case.proof, id, Fr::from_u64(1)), - "converted verifier fixture is missing final opening claim {id:?}" - ); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage8_final_opening_ids(base: &verifier_fixtures::VerifierFixtureCase) -> Vec { - let log_t = base.proof.trace_length.ilog2() as usize; - let dimensions = JoltFormulaDimensions::try_from(base.proof.one_hot_config.dimensions( - log_t, - 2 * RISCV_XLEN, - base.preprocessing.program.bytecode_len(), - base.proof.ram_K, - )) - .expect("verifier fixture has invalid final opening dimensions"); - final_opening_polynomial_order( - dimensions.ra_layout, - base.trusted_advice_commitment.is_some(), - base.proof.untrusted_advice_commitment.is_some(), - base.preprocessing - .program - .committed() - .map(|committed| committed.bytecode_chunk_count()), - ) - .into_iter() - .map(final_opening_id) - .collect() -} - -#[test] -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[ignore = "opening verification is not wired yet"] -fn tampered_opening_value_reject() {} diff --git a/crates/jolt-verifier/tests/soundness/tampering/preamble.rs b/crates/jolt-verifier/tests/soundness/tampering/preamble.rs deleted file mode 100644 index 9387b34da9..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/preamble.rs +++ /dev/null @@ -1,203 +0,0 @@ -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use std::sync::Arc; - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_verifier::ProgramPreprocessing; - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use crate::support::{self, tamper_manifest, verifier_fixtures::standard_muldiv_case}; - -/// A preprocessing whose entry address differs from the proven program's must -/// reject the honest proof: the entry address is a transcript input and the -/// initial PC constraint. -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn entry_address_mismatch_rejects_now() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("preprocessing.program.bytecode.entry_address"), - &base, - |case| { - if let ProgramPreprocessing::Full(full) = &mut case.preprocessing.program { - Arc::make_mut(full).bytecode.entry_address += 4; - } - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn public_io_memory_layout_mismatch_rejects_now() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("public_io.memory_layout"), - &base, - |case| { - case.public_io.memory_layout.heap_size += 1; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn invalid_trace_length_rejects_now() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.trace_length"), - &base, - |case| { - case.proof.trace_length = 3; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn excessive_trace_length_rejects_now() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.trace_length"), - &base, - |case| { - case.proof.trace_length = case.preprocessing.program.max_padded_trace_length() * 2; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn invalid_ram_domain_rejects_now() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.ram_K"), - &base, - |case| { - case.proof.ram_K = 3; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn zero_ram_domain_rejects_now() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.ram_K"), - &base, - |case| { - case.proof.ram_K = 0; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn oversized_public_input_rejects_now() { - let mut case = standard_muldiv_case(); - case.public_io.inputs = - vec![0; case.preprocessing.program.memory_layout().max_input_size as usize + 1]; - - support::assert_rejects(case.verify()); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn oversized_public_output_rejects_now() { - let mut case = standard_muldiv_case(); - case.public_io.outputs = - vec![0; case.preprocessing.program.memory_layout().max_output_size as usize + 1]; - - support::assert_rejects(case.verify()); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_public_input_bytes_reject() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("public_io.inputs"), - &base, - |case| { - case.public_io.inputs[0] ^= 1; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_public_output_bytes_reject() { - let base = standard_muldiv_case(); - assert!( - !base.public_io.outputs.is_empty(), - "muldiv fixture must produce public outputs for this tamper to be meaningful" - ); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("public_io.outputs"), - &base, - |case| { - case.public_io.outputs[0] ^= 1; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn flipped_panic_bit_rejects() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("public_io.panic"), - &base, - |case| { - case.public_io.panic = !case.public_io.panic; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn flipped_trace_polynomial_order_rejects() { - use jolt_claims::protocols::jolt::geometry::dimensions::TracePolynomialOrder; - - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.trace_polynomial_order"), - &base, - |case| { - case.proof.trace_polynomial_order = match case.proof.trace_polynomial_order { - TracePolynomialOrder::CycleMajor => TracePolynomialOrder::AddressMajor, - TracePolynomialOrder::AddressMajor => TracePolynomialOrder::CycleMajor, - }; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_preprocessing_digest_rejects() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("preprocessing.preprocessing_digest"), - &base, - |case| { - case.preprocessing.preprocessing_digest[0] ^= 1; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_one_hot_config_rejects() { - let base = standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.one_hot_config"), - &base, - |case| { - case.proof.one_hot_config.log_k_chunk += 1; - }, - ); -} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn preamble_tampering_requires_verifier_fixtures() {} diff --git a/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs b/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs deleted file mode 100644 index c1fc78867e..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs +++ /dev/null @@ -1,118 +0,0 @@ -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use crate::support; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use crate::support::proof_claims::attach_empty_opening_claims; -#[cfg(feature = "prover-fixtures")] -use crate::support::tamper_manifest; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_verifier::proof::JoltProofClaims; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use { - jolt_blindfold::BlindFoldProof, - jolt_crypto::VectorCommitmentOpening, - jolt_field::{Fr, Ring}, - jolt_sumcheck::CompressedSumcheckProof, -}; - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn mixed_clear_and_committed_stage_proofs_reject_now() { - let base = crate::support::verifier_fixtures::standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.stages.clear_vs_committed"), - &base, - |case| { - case.proof.stages.stage5_sumcheck_proof = jolt_sumcheck::SumcheckProof::Committed( - jolt_sumcheck::CommittedSumcheckProof::default(), - ); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn mixed_uniskip_stage_proof_rejects_now() { - let base = crate::support::verifier_fixtures::standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.stages.clear_vs_committed"), - &base, - |case| { - case.proof.stages.stage1_uni_skip_first_round_proof = - jolt_sumcheck::SumcheckProof::Committed( - jolt_sumcheck::CommittedSumcheckProof::default(), - ); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn zk_claim_payload_in_clear_mode_rejects_now() { - let base = crate::support::verifier_fixtures::standard_muldiv_case(); - tamper_manifest::assert_verifier_fixture_tamper_rejects( - tamper_manifest::required_target("proof.claims.mode_payload"), - &base, - |case| { - case.proof.claims = JoltProofClaims::Zk { - blindfold_proof: empty_blindfold_proof(), - }; - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn unexpected_zk_opening_claims_reject_now() { - tamper_manifest::assert_zk_target_active("proof.claims.mode_payload"); - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - attach_empty_opening_claims(&mut case.proof); - - support::assert_zk_rejects(case.verify()); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn clear_stage_in_zk_proof_rejects_now() { - tamper_manifest::assert_zk_target_active("proof.stages.clear_vs_committed"); - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - case.proof.stages.stage3_sumcheck_proof = jolt_sumcheck::SumcheckProof::Clear( - jolt_sumcheck::ClearProof::Compressed(jolt_sumcheck::CompressedSumcheckProof::default()), - ); - - support::assert_zk_rejects(case.verify()); -} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures to live-generate and tamper verifier-native proofs"] -fn tampered_mixed_proof_shape_reject() {} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn empty_blindfold_proof() -> BlindFoldProof { - let zero = Fr::from_u64(0); - let opening = VectorCommitmentOpening { - combined_vector: Vec::new(), - combined_blinding: zero, - }; - BlindFoldProof { - auxiliary_row_commitments: Vec::new(), - random_round_commitments: Vec::new(), - random_output_claim_row_commitments: Vec::new(), - random_auxiliary_row_commitments: Vec::new(), - random_error_row_commitments: Vec::new(), - random_eval_commitments: Vec::new(), - random_u: zero, - cross_term_error_row_commitments: Vec::new(), - outer_sumcheck: CompressedSumcheckProof::default(), - az_rx: zero, - bz_rx: zero, - cz_rx: zero, - inner_sumcheck: CompressedSumcheckProof::default(), - witness_opening: opening.clone(), - error_opening: opening.clone(), - folded_eval_outputs: Vec::new(), - folded_eval_blindings: Vec::new(), - folded_eval_output_openings: Vec::new(), - folded_eval_blinding_openings: Vec::new(), - } -} diff --git a/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs b/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs deleted file mode 100644 index 7940ed98b2..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs +++ /dev/null @@ -1,1300 +0,0 @@ -#![cfg_attr( - all(feature = "prover-fixtures", not(feature = "zk")), - expect( - clippy::panic, - reason = "test fixtures should fail loudly when their assumed proof shape changes" - ) -)] -#![cfg(not(feature = "akita"))] - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use crate::support::{ - tamper_manifest, - verifier_fixtures::{ordinary_tamper_bases, VerifierFixtureCase}, -}; - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use crate::support::proof_claims::{offset_opening_claim, opening_claim}; -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -use jolt_claims::protocols::jolt::geometry::claim_reductions::{ - bytecode as bytecode_reduction, program_image as program_image_reduction, -}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_claims::protocols::jolt::{ - geometry::{ - booleanity, bytecode, - claim_reductions::{ - advice, hamming_weight, increments, instruction as instruction_claim_reduction, - registers as registers_claim_reduction, - }, - dimensions::JoltFormulaDimensions, - instruction, ram, registers, - spartan::{ - outer_opening, outer_uniskip_opening, product_uniskip_opening, SpartanOuterDimensions, - }, - }, - JoltAdviceKind, JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, - PrecommittedReductionLayout, -}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_claims::{protocols::jolt::geometry::spartan, protocols::jolt::relations, OutputClaims}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_field::{Fr, Ring}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_poly::{CompressedPoly, UnivariatePoly}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_sumcheck::{ClearProof, SumcheckProof}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use jolt_verifier::stages::{CommittedProgramSchedule, PrecommittedSchedule}; -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -use num_traits::Zero; - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage1_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage1_uniskip_round(&base); - tamper_each_stage1_remainder_round(&base); - tamper_stage1_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage1_opening_claims_reject() { - for base in ordinary_tamper_bases() { - for (target_name, id) in stage1_required_openings(&base) { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage2_uniskip_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage2_uniskip_round(&base); - tamper_stage2_uniskip_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage2_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage2_batch_round(&base); - tamper_stage2_batch_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage2_input_claims_reject() { - for base in ordinary_tamper_bases() { - // The Spartan-outer input openings this stage consumes are already swept by - // tampered_stage1_opening_claims_reject (every SPARTAN_OUTER_R1CS_INPUTS - // variable). Only the product uni-skip output claim, which lives under the - // SpartanProductVirtualization relation, is unique to this stage. - offset_claim_rejects( - &base, - "stage2.claims.product_uniskip_output_claim", - product_uniskip_opening(), - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage2_output_claims_reject() { - for base in ordinary_tamper_bases() { - // Every wire cell is present (the aliased reduction cells carry - // validated-equal copies of the product-remainder values), so a plain offset - // suffices; the aliased offsets are rejected by the generated - // `validate_aliases`, the rest by the batch fold. - for (target_name, id) in stage2_formula_output_openings() { - offset_claim_rejects(&base, target_name, id); - } - - for (target_name, id) in [ - ( - "stage2.claims.batch_outputs.product_remainder.write_lookup_output_to_rd", - spartan::write_lookup_output_to_rd_product(), - ), - ( - "stage2.claims.batch_outputs.product_remainder.virtual_instruction", - spartan::virtual_instruction_product(), - ), - ] { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage2_ram_phase_config_reject() { - for base in ordinary_tamper_bases() { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("proof.rw_config"), - &base, - |case| { - case.proof.rw_config.ram_rw_phase1_num_rounds = - case.proof.trace_length.ilog2() as u8 + 1; - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("proof.rw_config"), - &base, - |case| { - case.proof.rw_config.ram_rw_phase2_num_rounds = case.proof.ram_K.ilog2() as u8 + 1; - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage3_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage3_batch_round(&base); - tamper_stage3_batch_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage3_output_claims_reject() { - for base in ordinary_tamper_bases() { - for (target_name, id) in stage3_formula_output_openings() { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage4_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage4_batch_round(&base); - tamper_stage4_batch_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage4_output_claims_reject() { - for base in ordinary_tamper_bases() { - for (target_name, id) in stage4_formula_output_openings() { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage4_advice_claims_reject() { - let base = real_advice_case(); - - for (target_name, id) in stage4_advice_openings() { - offset_claim_rejects(&base, target_name, id); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage5_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage5_batch_round(&base); - tamper_stage5_batch_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage5_output_claims_reject() { - for base in ordinary_tamper_bases() { - for (target_name, id) in stage5_formula_output_openings(&base) { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage6_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage6_address_phase_round(&base); - tamper_stage6_address_phase_round_counts(&base); - tamper_each_stage6_cycle_phase_round(&base); - tamper_stage6_cycle_phase_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage6_output_claims_reject() { - for base in ordinary_tamper_bases() { - for (target_name, id) in stage6_formula_output_openings(&base) { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage6_advice_claims_reject() { - let base = real_advice_case(); - - for (target_name, id) in stage6_advice_output_openings(&base) { - offset_claim_rejects(&base, target_name, id); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage7_sumcheck_payload_reject() { - for base in ordinary_tamper_bases() { - tamper_each_stage7_batch_round(&base); - tamper_stage7_batch_round_counts(&base); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -#[test] -fn tampered_stage7_output_claims_reject() { - for base in ordinary_tamper_bases() { - for (target_name, id) in stage7_formula_output_openings(&base) { - offset_claim_rejects(&base, target_name, id); - } - } -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage7_advice_claims_reject() { - let base = real_advice_case(); - - for (target_name, id) in stage7_advice_output_openings(&base) { - offset_claim_rejects(&base, target_name, id); - } -} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage1_sumcheck_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage2_uniskip_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage2_sumcheck_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage3_sumcheck_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage4_sumcheck_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage5_sumcheck_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage6_sumcheck_payload_reject() {} - -#[cfg(any(not(feature = "prover-fixtures"), feature = "zk"))] -#[test] -#[ignore = "enable --features prover-fixtures in a non-ZK build to live-generate and tamper verifier-native proofs"] -fn tampered_stage7_sumcheck_payload_reject() {} - -/// Legacy advice fixture: only when field-inline is disabled (see `tampering/mod.rs`). -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -fn real_advice_case() -> VerifierFixtureCase { - crate::support::verifier_fixtures::standard_advice_consumer_case() -} - -// Field-inline verification currently requires full public bytecode. -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -fn real_committed_case() -> VerifierFixtureCase { - crate::support::verifier_fixtures::standard_committed_muldiv_case() -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage4_program_image_contribution_rejects() { - let base = real_committed_case(); - offset_claim_rejects( - &base, - "stage4.claims.program_image_contribution", - program_image_reduction::ram_val_check_contribution_opening(), - ); -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage6_committed_bytecode_claims_reject() { - let base = real_committed_case(); - let schedule = case_advice_layouts(&base); - let layout = schedule - .bytecode - .unwrap_or_else(|| panic!("committed fixture must schedule a bytecode reduction")); - let chunk_count = base.preprocessing.program.committed().map_or_else( - || panic!("committed fixture must carry chunk commitments"), - |committed| committed.bytecode_chunk_count(), - ); - - for stage in 0..bytecode_reduction::NUM_BYTECODE_VAL_STAGES { - offset_claim_rejects( - &base, - "stage6.claims.address_phase.bytecode_val_stages", - bytecode_reduction::bytecode_val_stage_opening(stage), - ); - } - - let dimensions = layout.dimensions(); - assert!( - dimensions.has_address_phase(), - "committed muldiv fixture lost its bytecode address phase; retire the \ - stage6 intermediate / stage7 chunk tamper targets" - ); - for id in bytecode_reduction::cycle_phase_output_openings(dimensions, chunk_count) { - offset_claim_rejects(&base, "stage6.claims.bytecode_reduction.intermediate", id); - } -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage6_committed_program_image_claim_rejects() { - let base = real_committed_case(); - let schedule = case_advice_layouts(&base); - let layout = schedule - .program_image - .unwrap_or_else(|| panic!("committed fixture must schedule a program-image reduction")); - - for id in program_image_reduction::cycle_phase_output_openings(layout.dimensions()) { - offset_claim_rejects( - &base, - "stage6.claims.program_image_reduction.program_image", - id, - ); - } -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "field-inline") -))] -#[test] -fn tampered_stage7_committed_claims_reject() { - let base = real_committed_case(); - let schedule = case_advice_layouts(&base); - let bytecode_layout = schedule - .bytecode - .unwrap_or_else(|| panic!("committed fixture must schedule a bytecode reduction")); - let image_layout = schedule - .program_image - .unwrap_or_else(|| panic!("committed fixture must schedule a program-image reduction")); - let chunk_count = base.preprocessing.program.committed().map_or_else( - || panic!("committed fixture must carry chunk commitments"), - |committed| committed.bytecode_chunk_count(), - ); - - assert!( - bytecode_layout.dimensions().has_address_phase() - && image_layout.dimensions().has_address_phase(), - "committed muldiv fixture lost its address phase; retire the stage7 \ - committed tamper targets" - ); - for chunk in 0..chunk_count { - offset_claim_rejects( - &base, - "stage7.claims.bytecode_address_phase.chunks", - bytecode_reduction::final_bytecode_chunk_opening(chunk), - ); - } - offset_claim_rejects( - &base, - "stage7.claims.program_image_address_phase", - program_image_reduction::final_program_image_opening(), - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage1_uniskip_round(base: &VerifierFixtureCase) { - let round_count = clear_full_round_count(&base.proof.stages.stage1_uni_skip_first_round_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage1.uni_skip.round_polynomial"), - base, - |case| { - mutate_full_round( - &mut case.proof.stages.stage1_uni_skip_first_round_proof, - round_index, - ); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage1_remainder_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage1_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage1.remainder.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage1_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage1_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage1.uni_skip.round_count.missing"), - base, - |case| { - pop_full_round(&mut case.proof.stages.stage1_uni_skip_first_round_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage1.uni_skip.round_count.extra"), - base, - |case| { - push_full_round(&mut case.proof.stages.stage1_uni_skip_first_round_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage1.remainder.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage1_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage1.remainder.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage1_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage2_uniskip_round(base: &VerifierFixtureCase) { - let round_count = clear_full_round_count(&base.proof.stages.stage2_uni_skip_first_round_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage2.product_uniskip.round_polynomial"), - base, - |case| { - mutate_full_round( - &mut case.proof.stages.stage2_uni_skip_first_round_proof, - round_index, - ); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage2_uniskip_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage2.product_uniskip.round_count.missing"), - base, - |case| { - pop_full_round(&mut case.proof.stages.stage2_uni_skip_first_round_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage2.product_uniskip.round_count.extra"), - base, - |case| { - push_full_round(&mut case.proof.stages.stage2_uni_skip_first_round_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage2_batch_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage2_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage2.batch.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage2_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage2_batch_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage2.batch.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage2_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage2.batch.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage2_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage3_batch_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage3_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage3.batch.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage3_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage3_batch_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage3.batch.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage3_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage3.batch.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage3_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage4_batch_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage4_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage4.batch.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage4_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage4_batch_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage4.batch.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage4_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage4.batch.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage4_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage5_batch_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage5_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage5.batch.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage5_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage5_batch_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage5.batch.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage5_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage5.batch.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage5_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage6_address_phase_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage6a_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage6.address_phase.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage6a_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage6_address_phase_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage6.address_phase.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage6a_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage6.address_phase.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage6a_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage6_cycle_phase_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage6b_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage6.cycle_phase.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage6b_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage6_cycle_phase_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage6.cycle_phase.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage6b_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage6.cycle_phase.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage6b_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_each_stage7_batch_round(base: &VerifierFixtureCase) { - let round_count = compressed_round_count(&base.proof.stages.stage7_sumcheck_proof); - for round_index in 0..round_count { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage7.batch.round_polynomial"), - base, - |case| { - mutate_compressed_round(&mut case.proof.stages.stage7_sumcheck_proof, round_index); - }, - ); - } -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn tamper_stage7_batch_round_counts(base: &VerifierFixtureCase) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage7.batch.round_count.missing"), - base, - |case| { - pop_compressed_round(&mut case.proof.stages.stage7_sumcheck_proof); - }, - ); - - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target("stage7.batch.round_count.extra"), - base, - |case| { - push_compressed_round(&mut case.proof.stages.stage7_sumcheck_proof); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn clear_full_round_count(proof: &SumcheckProof) -> usize { - let SumcheckProof::Clear(ClearProof::Full(proof)) = proof else { - panic!("converted verifier fixture must use a clear full uni-skip proof"); - }; - proof.round_polynomials.len() -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn compressed_round_count(proof: &SumcheckProof) -> usize { - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = proof else { - panic!("converted verifier fixture must use a clear compressed sumcheck proof"); - }; - proof.round_polynomials.len() -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn mutate_full_round(proof: &mut SumcheckProof, round_index: usize) { - let SumcheckProof::Clear(ClearProof::Full(proof)) = proof else { - panic!("converted verifier fixture must use a clear full uni-skip proof"); - }; - let Some(round) = proof.round_polynomials.get_mut(round_index) else { - panic!("converted verifier fixture is missing expected uni-skip round {round_index}"); - }; - *round = UnivariatePoly::new(vec![Fr::from_u64(round_index as u64 + 1)]); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn mutate_compressed_round( - proof: &mut SumcheckProof, - round_index: usize, -) { - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = proof else { - panic!("converted verifier fixture must use a clear compressed sumcheck proof"); - }; - let Some(round) = proof.round_polynomials.get_mut(round_index) else { - panic!("converted verifier fixture is missing expected compressed round {round_index}"); - }; - *round = CompressedPoly::new(vec![Fr::from_u64(round_index as u64 + 1)]); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn pop_full_round(proof: &mut SumcheckProof) { - let SumcheckProof::Clear(ClearProof::Full(proof)) = proof else { - panic!("converted verifier fixture must use a clear full uni-skip proof"); - }; - let removed = proof.round_polynomials.pop(); - assert!( - removed.is_some(), - "converted verifier fixture has no full round to remove" - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn push_full_round(proof: &mut SumcheckProof) { - let SumcheckProof::Clear(ClearProof::Full(proof)) = proof else { - panic!("converted verifier fixture must use a clear full uni-skip proof"); - }; - proof - .round_polynomials - .push(UnivariatePoly::new(vec![Fr::from_u64(1)])); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn pop_compressed_round(proof: &mut SumcheckProof) { - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = proof else { - panic!("converted verifier fixture must use a clear compressed sumcheck proof"); - }; - let removed = proof.round_polynomials.pop(); - assert!( - removed.is_some(), - "converted verifier fixture has no compressed round to remove" - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn push_compressed_round(proof: &mut SumcheckProof) { - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = proof else { - panic!("converted verifier fixture must use a clear compressed sumcheck proof"); - }; - proof - .round_polynomials - .push(CompressedPoly::new(vec![Fr::from_u64(1)])); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn offset_claim_rejects(base: &VerifierFixtureCase, target_name: &str, id: JoltOpeningId) { - tamper_manifest::assert_verifier_fixture_tamper_rejects( - manifest_target(target_name), - base, - |case| { - assert!( - offset_opening_claim(&mut case.proof, id, Fr::from_u64(1)), - "converted verifier fixture is missing opening claim {id:?}" - ); - }, - ); -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage1_required_openings(base: &VerifierFixtureCase) -> Vec<(&'static str, JoltOpeningId)> { - let log_t = base.proof.trace_length.ilog2() as usize; - let dimensions = SpartanOuterDimensions::rv64(log_t); - let mut openings = Vec::with_capacity(dimensions.variables().len() + 1); - openings.push(( - "stage1.claims.uniskip_output_claim", - outer_uniskip_opening(), - )); - openings.extend( - dimensions - .variables() - .iter() - .copied() - .map(outer_opening) - .map(|id| ("stage1.claims.outer", id)), - ); - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage2_formula_output_openings() -> Vec<(&'static str, JoltOpeningId)> { - let mut openings = Vec::new(); - openings.extend( - [ram::ram_val(), ram::ram_ra(), ram::ram_inc()] - .into_iter() - .map(|id| ("stage2.claims.batch_outputs.ram_read_write", id)), - ); - openings.extend( - [ - spartan::left_instruction_input_product(), - spartan::right_instruction_input_product(), - spartan::jump_flag_product(), - spartan::lookup_output_product(), - spartan::branch_flag_product(), - spartan::next_is_noop_product(), - ] - .into_iter() - .map(|id| ("stage2.claims.batch_outputs.product_remainder.checked", id)), - ); - openings.extend( - [ - instruction_claim_reduction::lookup_output_reduced(), - instruction_claim_reduction::left_lookup_operand_reduced(), - instruction_claim_reduction::right_lookup_operand_reduced(), - instruction_claim_reduction::left_instruction_input_reduced(), - instruction_claim_reduction::right_instruction_input_reduced(), - ] - .into_iter() - .map(|id| { - ( - "stage2.claims.batch_outputs.instruction_claim_reduction", - id, - ) - }), - ); - openings.extend( - [ram::ram_ra_raf_evaluation()] - .into_iter() - .map(|id| ("stage2.claims.batch_outputs.ram_raf_evaluation", id)), - ); - openings.extend( - [ram::ram_val_final()] - .into_iter() - .map(|id| ("stage2.claims.batch_outputs.ram_output_check", id)), - ); - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage3_formula_output_openings() -> Vec<(&'static str, JoltOpeningId)> { - let mut openings = Vec::new(); - openings.extend( - [ - spartan::unexpanded_pc_shift(), - spartan::pc_shift(), - spartan::is_virtual_shift(), - spartan::is_first_in_sequence_shift(), - spartan::is_noop_shift(), - ] - .into_iter() - .map(|id| ("stage3.claims.shift", id)), - ); - openings.extend( - [ - instruction::right_operand_is_rs2(), - instruction::rs2_value(), - instruction::right_operand_is_imm(), - instruction::imm(), - instruction::left_operand_is_rs1(), - instruction::rs1_value(), - instruction::left_operand_is_pc(), - instruction::unexpanded_pc(), - ] - .into_iter() - .map(|id| ("stage3.claims.instruction_input", id)), - ); - openings.extend( - [ - registers_claim_reduction::rd_write_value_reduced(), - registers_claim_reduction::rs1_value_reduced(), - registers_claim_reduction::rs2_value_reduced(), - ] - .into_iter() - .map(|id| ("stage3.claims.registers_claim_reduction", id)), - ); - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage4_formula_output_openings() -> Vec<(&'static str, JoltOpeningId)> { - let mut openings = Vec::new(); - openings.extend( - [ - registers::registers_val_read_write(), - registers::rs1_ra_read_write(), - registers::rs2_ra_read_write(), - registers::rd_wa_read_write(), - registers::rd_inc_read_write(), - ] - .into_iter() - .map(|id| ("stage4.claims.registers_read_write", id)), - ); - openings.extend( - [ram::ram_ra_val_check(), ram::ram_inc_val_check()] - .into_iter() - .map(|id| ("stage4.claims.ram_val_check", id)), - ); - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage4_advice_openings() -> Vec<(&'static str, JoltOpeningId)> { - vec![ - ( - "stage4.claims.advice.untrusted", - ram::val_check_advice_opening(JoltAdviceKind::Untrusted), - ), - ( - "stage4.claims.advice.trusted", - ram::val_check_advice_opening(JoltAdviceKind::Trusted), - ), - ] -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage5_formula_output_openings( - base: &VerifierFixtureCase, -) -> Vec<(&'static str, JoltOpeningId)> { - let mut openings = Vec::new(); - openings.extend(LookupTableKind::::iter().map(|table| { - ( - "stage5.claims.instruction_read_raf.lookup_table_flags", - instruction::lookup_table_flag(table), - ) - })); - for index in 0.. { - let id = instruction::instruction_ra(index); - if opening_claim(&base.proof, id).is_none() { - break; - } - openings.push(("stage5.claims.instruction_read_raf.instruction_ra", id)); - } - openings.push(( - "stage5.claims.instruction_read_raf.instruction_raf_flag", - instruction::instruction_raf_flag(), - )); - openings.extend( - [ram::ram_ra_claim_reduction()] - .into_iter() - .map(|id| ("stage5.claims.ram_ra_claim_reduction", id)), - ); - openings.extend( - [ - registers::rd_inc_val_evaluation(), - registers::rd_wa_val_evaluation(), - ] - .into_iter() - .map(|id| ("stage5.claims.registers_val_evaluation", id)), - ); - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage6_formula_output_openings( - base: &VerifierFixtureCase, -) -> Vec<(&'static str, JoltOpeningId)> { - let dimensions = stage6_dimensions(base); - let mut openings = Vec::new(); - - openings.extend([ - ( - "stage6.claims.address_phase.bytecode_read_raf", - bytecode::bytecode_read_raf_address_phase_opening(), - ), - ( - "stage6.claims.address_phase.booleanity", - booleanity::booleanity_address_phase_opening(), - ), - ]); - openings.extend( - bytecode::read_raf_output_openings(dimensions.bytecode_read_raf) - .bytecode_ra - .into_iter() - .map(|id| ("stage6.claims.bytecode_read_raf.bytecode_ra", id)), - ); - for index in 0..dimensions.ra_layout.instruction() { - openings.push(( - "stage6.claims.booleanity.instruction_ra", - JoltOpeningId::committed( - JoltCommittedPolynomial::InstructionRa(index), - JoltRelationId::Booleanity, - ), - )); - } - for index in 0..dimensions.ra_layout.bytecode() { - openings.push(( - "stage6.claims.booleanity.bytecode_ra", - JoltOpeningId::committed( - JoltCommittedPolynomial::BytecodeRa(index), - JoltRelationId::Booleanity, - ), - )); - } - for index in 0..dimensions.ra_layout.ram() { - openings.push(( - "stage6.claims.booleanity.ram_ra", - JoltOpeningId::committed( - JoltCommittedPolynomial::RamRa(index), - JoltRelationId::Booleanity, - ), - )); - } - openings.extend([ram::ram_hamming_weight()].into_iter().map(|id| { - ( - "stage6.claims.ram_hamming_booleanity.ram_hamming_weight", - id, - ) - })); - openings.extend( - relations::ram::RamRaVirtualizationOutputClaims:: { - ram_ra: vec![Fr::zero(); dimensions.ram_ra_virtualization.num_committed_ra_polys()], - } - .canonical_order() - .into_iter() - .map(|id| ("stage6.claims.ram_ra_virtualization.ram_ra", id)), - ); - openings.extend( - instruction::ra_virtualization_output_openings(dimensions.instruction_ra_virtualization) - .all() - .into_iter() - .map(|id| { - ( - "stage6.claims.instruction_ra_virtualization.committed_instruction_ra", - id, - ) - }), - ); - let [ram_inc, rd_inc] = [increments::ram_inc_reduced(), increments::rd_inc_reduced()]; - openings.extend([ - ("stage6.claims.inc_claim_reduction.ram_inc", ram_inc), - ("stage6.claims.inc_claim_reduction.rd_inc", rd_inc), - ]); - - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn case_advice_layouts(base: &VerifierFixtureCase) -> PrecommittedSchedule { - let committed_program = base.preprocessing.program.committed().map(|committed| { - let start_index = base - .public_io - .memory_layout - .remapped_word_address(committed.meta.min_bytecode_address) - .unwrap_or_else(|error| { - panic!("committed fixture bytecode address must remap: {error}") - }); - CommittedProgramSchedule { - bytecode_len: committed.meta.bytecode_len, - bytecode_chunk_count: committed.bytecode_chunk_count(), - program_image_len_words: committed.meta.program_image_len_words, - program_image_start_index: start_index as usize, - } - }); - PrecommittedSchedule::new( - base.proof.trace_polynomial_order, - base.proof.trace_length.ilog2() as usize, - base.proof.one_hot_config.committed_chunk_bits(), - base.trusted_advice_commitment - .is_some() - .then_some(base.public_io.memory_layout.max_trusted_advice_size as usize), - base.proof - .untrusted_advice_commitment - .is_some() - .then_some(base.public_io.memory_layout.max_untrusted_advice_size as usize), - committed_program, - ) - .unwrap_or_else(|error| panic!("precommitted schedule should build: {error}")) -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage6_advice_output_openings(base: &VerifierFixtureCase) -> Vec<(&'static str, JoltOpeningId)> { - let schedule = case_advice_layouts(base); - let (trusted_layout, untrusted_layout) = (schedule.trusted_advice, schedule.untrusted_advice); - let mut openings = Vec::new(); - - if let Some(layout) = trusted_layout { - openings.extend( - advice::cycle_phase_output_openings(JoltAdviceKind::Trusted, layout.dimensions()) - .into_iter() - .map(|id| ("stage6.claims.trusted_advice.trusted", id)), - ); - } - if let Some(layout) = untrusted_layout { - openings.extend( - advice::cycle_phase_output_openings(JoltAdviceKind::Untrusted, layout.dimensions()) - .into_iter() - .map(|id| ("stage6.claims.untrusted_advice.untrusted", id)), - ); - } - - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage7_formula_output_openings( - base: &VerifierFixtureCase, -) -> Vec<(&'static str, JoltOpeningId)> { - let dimensions = stage6_dimensions(base); - let output_openings = hamming_weight::claim_reduction_output_openings( - hamming_weight::HammingWeightClaimReductionDimensions::new( - dimensions.ra_layout, - base.proof.one_hot_config.committed_chunk_bits(), - ), - ); - let mut openings = Vec::new(); - openings.extend(output_openings.instruction_ra.into_iter().map(|id| { - ( - "stage7.claims.hamming_weight_claim_reduction.instruction_ra", - id, - ) - })); - openings.extend(output_openings.bytecode_ra.into_iter().map(|id| { - ( - "stage7.claims.hamming_weight_claim_reduction.bytecode_ra", - id, - ) - })); - openings.extend( - output_openings - .ram_ra - .into_iter() - .map(|id| ("stage7.claims.hamming_weight_claim_reduction.ram_ra", id)), - ); - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage7_advice_output_openings(base: &VerifierFixtureCase) -> Vec<(&'static str, JoltOpeningId)> { - let schedule = case_advice_layouts(base); - let (trusted_layout, untrusted_layout) = (schedule.trusted_advice, schedule.untrusted_advice); - let mut openings = Vec::new(); - - if trusted_layout.is_some_and(|layout| layout.dimensions().has_address_phase()) { - openings.push(( - "stage7.claims.trusted_advice.trusted", - advice::final_advice_opening(JoltAdviceKind::Trusted), - )); - } - if untrusted_layout.is_some_and(|layout| layout.dimensions().has_address_phase()) { - openings.push(( - "stage7.claims.untrusted_advice.untrusted", - advice::final_advice_opening(JoltAdviceKind::Untrusted), - )); - } - - openings -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn stage6_dimensions(base: &VerifierFixtureCase) -> JoltFormulaDimensions { - let log_t = base.proof.trace_length.ilog2() as usize; - JoltFormulaDimensions::try_from(base.proof.one_hot_config.dimensions( - log_t, - 2 * RISCV_XLEN, - base.preprocessing.program.bytecode_len(), - base.proof.ram_K, - )) - .unwrap_or_else(|error| panic!("verifier fixture has invalid Stage 6 dimensions: {error}")) -} - -#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] -fn manifest_target(name: &str) -> tamper_manifest::TamperTarget { - tamper_manifest::required_target(name) -} diff --git a/crates/jolt-verifier/tests/soundness/tampering/zk.rs b/crates/jolt-verifier/tests/soundness/tampering/zk.rs deleted file mode 100644 index dbf3200d7d..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/zk.rs +++ /dev/null @@ -1,344 +0,0 @@ -#![cfg_attr( - all(feature = "prover-fixtures", feature = "zk"), - expect( - clippy::expect_used, - clippy::panic, - reason = "ZK tampering helpers assert fixture shape before mutating real verifier objects" - ) -)] - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use crate::support; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use crate::support::tamper_manifest; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::Ring as _; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_verifier::JoltProofClaims; - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn missing_zk_vector_commitment_setup_rejects_now() { - with_zk_verifier_stack(|| { - tamper_manifest::assert_zk_target_active("zk.vector_commitment_setup"); - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - case.preprocessing.vc_setup = None; - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage1_remainder_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage1_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage2_uniskip_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage2_uni_skip_first_round_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage2_batch_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage2_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage3_batch_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage3_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage3_batch_round_degree_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - exceed_first_committed_round_degree_bound(&mut case.proof.stages.stage3_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage3_batch_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage3_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage4_batch_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage4_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage4_batch_round_degree_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - exceed_first_committed_round_degree_bound(&mut case.proof.stages.stage4_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage4_batch_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage4_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage5_batch_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage5_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage5_batch_round_degree_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - exceed_first_committed_round_degree_bound(&mut case.proof.stages.stage5_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage5_batch_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage5_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage6_address_phase_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage6a_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage6_address_phase_round_degree_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - exceed_first_committed_round_degree_bound(&mut case.proof.stages.stage6a_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage6_address_phase_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage6a_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage6_cycle_phase_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage6b_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage6_cycle_phase_round_degree_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - exceed_first_committed_round_degree_bound(&mut case.proof.stages.stage6b_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage6_cycle_phase_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage6b_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage7_batch_round_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_round(&mut case.proof.stages.stage7_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage7_batch_round_degree_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - exceed_first_committed_round_degree_bound(&mut case.proof.stages.stage7_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_stage7_batch_output_commitment_count_rejects_now() { - with_zk_verifier_stack(|| { - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - pop_committed_output_claim_row(&mut case.proof.stages.stage7_sumcheck_proof); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_zk_joint_opening_eval_commitment_rejects_now() { - with_zk_verifier_stack(|| { - tamper_manifest::assert_zk_target_active("zk.joint_opening_proof.eval_commitment"); - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - case.proof.joint_opening_proof.0.y_com = None; - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[test] -fn tampered_blindfold_proof_rejects_now() { - with_zk_verifier_stack(|| { - tamper_manifest::assert_zk_target_active("zk.blindfold_proof"); - let mut case = crate::support::verifier_fixtures::zk_muldiv_case(); - let JoltProofClaims::Zk { blindfold_proof } = &mut case.proof.claims else { - panic!("ZK fixture must carry a BlindFold proof"); - }; - blindfold_proof.random_u += jolt_field::Fr::from_u64(1); - - support::assert_zk_rejects(case.verify()); - }); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn with_zk_verifier_stack(test: impl FnOnce() + Send + 'static) { - std::thread::Builder::new() - .name("zk-verifier-tamper".to_string()) - .stack_size(128 * 1024 * 1024) - .spawn(test) - .expect("spawn ZK verifier tamper test") - .join() - .expect("ZK verifier tamper test panicked"); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn pop_committed_round(proof: &mut jolt_sumcheck::SumcheckProof) -where - F: jolt_field::JoltField, -{ - let jolt_sumcheck::SumcheckProof::Committed(proof) = proof else { - panic!("ZK fixture must use committed sumcheck proofs"); - }; - let _ = proof.rounds.pop(); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn exceed_first_committed_round_degree_bound(proof: &mut jolt_sumcheck::SumcheckProof) -where - F: jolt_field::JoltField, -{ - let jolt_sumcheck::SumcheckProof::Committed(proof) = proof else { - panic!("ZK fixture must use committed sumcheck proofs"); - }; - let Some(round) = proof.rounds.first_mut() else { - panic!("ZK committed sumcheck proof must have at least one round"); - }; - round.degree = round.degree.saturating_add(1024); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn pop_committed_output_claim_row(proof: &mut jolt_sumcheck::SumcheckProof) -where - F: jolt_field::JoltField, -{ - let jolt_sumcheck::SumcheckProof::Committed(proof) = proof else { - panic!("ZK fixture must use committed sumcheck proofs"); - }; - let _ = proof.output_claims.commitments.pop(); -} - -#[cfg(any(not(feature = "prover-fixtures"), not(feature = "zk")))] -#[test] -#[ignore = "enable --features prover-fixtures,zk to live-generate and tamper verifier-native ZK proofs"] -fn missing_zk_vector_commitment_setup_rejects_now() {} - -#[cfg(any(not(feature = "prover-fixtures"), not(feature = "zk")))] -#[test] -#[ignore = "enable --features prover-fixtures,zk to live-generate and tamper verifier-native ZK proofs"] -fn tampered_blindfold_proof_reject() {} diff --git a/crates/jolt-verifier/tests/soundness/zk.rs b/crates/jolt-verifier/tests/soundness/zk.rs new file mode 100644 index 0000000000..1dfc67d3eb --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/zk.rs @@ -0,0 +1,105 @@ +//! ZK Dory fixtures: every stage sends hiding commitments, so the clear +//! stage checks move to the Stage 8 opening and the BlindFold tail. + +#![expect( + clippy::expect_used, + reason = "tamper tests fail loudly when a fixture breaks an assumption" +)] + +use super::checks; +use crate::support::narg::{Region, TracedCase}; +use crate::support::verifier_fixtures::{ + fresh_zk_muldiv_case, zk_advice_consumer_case, zk_committed_muldiv_case, zk_muldiv_case, +}; +use jolt_verifier::VerifierError; + +const SAMPLE: Option = Some(300); + +#[test] +fn zk_muldiv_message_sweep() { + checks::message_sweep(&zk_muldiv_case(), None); +} + +#[test] +fn zk_muldiv_structural_tampers() { + checks::structural_tampers(&zk_muldiv_case(), Some(128)); +} + +#[test] +fn zk_muldiv_statement_tampers() { + let case = zk_muldiv_case(); + checks::statement_tampers(&case); + checks::header_equivocations(&case); + checks::commitment_substitutions(&case); + checks::preprocessing_swap(&case, &zk_committed_muldiv_case().preprocessing); + + let mut preprocessing = case.preprocessing.clone(); + preprocessing.vc_setup = None; + assert!(matches!( + case.verify_statement_with(&preprocessing), + Err(VerifierError::MissingVectorCommitmentSetup) + )); +} + +#[test] +fn zk_advice_message_sweep() { + let case = zk_advice_consumer_case(); + checks::message_sweep(&case, SAMPLE); + checks::trusted_advice_tampers(&case, &checks::sent_commitment(&case, 0)); +} + +#[test] +fn zk_committed_message_sweep() { + let case = zk_committed_muldiv_case(); + checks::message_sweep(&case, SAMPLE); + checks::committed_program_tampers( + &case, + &[("swapped bytecode chunk commitments", |preprocessing| { + let committed = checks::committed_mut(preprocessing); + assert!(committed.bytecode_chunk_commitments.len() >= 2); + committed.bytecode_chunk_commitments.swap(0, 1); + })], + ); +} + +/// Two honest ZK proofs of one statement share every message width, so a +/// region transplanted from one into the other is a well-formed argument +/// string whose messages are each valid but bound to another transcript. +#[test] +fn zk_region_transplants_reject() { + let base = zk_muldiv_case(); + let donor = fresh_zk_muldiv_case(); + let base_messages = base.honest_trace().messages(); + assert_eq!( + base_messages, + donor.honest_trace().messages(), + "fresh proofs of one statement have one message layout" + ); + for region in Region::expected() { + if region == Region::Preamble { + continue; + } + let deadline = base_messages + .iter() + .filter(|m| m.region == region) + .map(|m| m.deadline) + .max() + .expect("every region sends a message"); + let mut narg = base.proof.narg.clone(); + for message in base_messages.iter().filter(|m| m.region == region) { + narg[message.range.clone()].copy_from_slice(&donor.proof.narg[message.range.clone()]); + } + assert_ne!( + narg, base.proof.narg, + "{region:?} transplant changed nothing" + ); + let trace = base.trace(&narg); + assert!(trace.result.is_err(), "{region:?} transplant accepted"); + assert!( + trace.stop_region() <= deadline, + "{region:?} transplant rejected in {:?}: {:?}", + trace.stop_region(), + trace.result + ); + } +} diff --git a/crates/jolt-verifier/tests/statistical_independence/mod.rs b/crates/jolt-verifier/tests/statistical_independence/mod.rs index f2d3174cd3..d84620b794 100644 --- a/crates/jolt-verifier/tests/statistical_independence/mod.rs +++ b/crates/jolt-verifier/tests/statistical_independence/mod.rs @@ -1,4 +1,9 @@ -// Legacy-generated ZK fixtures; only when field-inline is disabled (see -// soundness/tampering/mod.rs). -#[cfg(not(feature = "field-inline"))] +// ZK fixtures exist only with field-inline disabled; the message layout comes +// from the `logging` event log. +#[cfg(all( + feature = "prover-fixtures", + feature = "logging", + feature = "zk", + not(feature = "field-inline") +))] pub mod zk; diff --git a/crates/jolt-verifier/tests/statistical_independence/zk.rs b/crates/jolt-verifier/tests/statistical_independence/zk.rs index b6b79782de..263486227a 100644 --- a/crates/jolt-verifier/tests/statistical_independence/zk.rs +++ b/crates/jolt-verifier/tests/statistical_independence/zk.rs @@ -1,702 +1,133 @@ -#![cfg_attr( - all(feature = "prover-fixtures", feature = "zk"), - expect( - clippy::cast_precision_loss, - clippy::expect_used, - clippy::panic, - reason = "statistical tests compute empirical floating-point test statistics and fail loudly" - ) +//! Statistical independence of ZK proofs, over the argument string. +//! +//! Every message after the proof header is a hiding commitment, a masked +//! Dory message, or a BlindFold message over a randomized folded instance. +//! Two honest proofs of one statement must therefore differ in every such +//! message, the messages' low bytes must look uniform, and no commitment or +//! masked message may repeat within a proof (a repeat means reused +//! randomness). + +#![expect( + clippy::cast_precision_loss, + reason = "chi-squared statistics are computed in floating point" )] -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] +use crate::support::narg::{Message, Region, TracedCase}; +use crate::support::verifier_fixtures::{fresh_zk_muldiv_case, zk_muldiv_case}; use std::collections::BTreeMap; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use ark_serialize::CanonicalSerialize; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_field::{CanonicalBytes, Fr}; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_sumcheck::SumcheckProof; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_transcript::{ - AppendToTranscript, LegacyBlake2bTranscript as Blake2bTranscript, Transcript, -}; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -use jolt_verifier::JoltProofClaims; - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -const DEFAULT_SAMPLES: usize = 64; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] const NUM_BUCKETS: usize = 16; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -const MIN_SAMPLES: usize = NUM_BUCKETS * 4; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -const CHI2_CRITICAL: f64 = 43.84; +/// Chi-squared critical value for 15 degrees of freedom at p ~ 1.2e-5. +const CHI2_CRITICAL: f64 = 50.0; -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] #[test] -#[ignore = "run with --release: cargo nextest run -p jolt-verifier --release --features prover-fixtures,zk zk_muldiv_jolt_proof_components_are_statistically_independent --run-ignored ignored-only --cargo-quiet"] -fn zk_muldiv_jolt_proof_components_are_statistically_independent() { - with_zk_statistical_stack(run_zk_muldiv_jolt_proof_components_are_statistically_independent); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn run_zk_muldiv_jolt_proof_components_are_statistically_independent() { - require_release_build(); - - let samples = statistical_sample_count(); - assert!( - samples >= MIN_SAMPLES, - "JOLT_VERIFIER_ZK_STAT_SAMPLES must be at least {MIN_SAMPLES}" - ); - - let mut all = BucketTracker::new(); - let mut even = BucketTracker::new(); - let mut odd = BucketTracker::new(); - let mut baseline = None; - - for sample_index in 0..samples { - let case = crate::support::verifier_fixtures::fresh_zk_muldiv_case(); - crate::support::assert_zk_accepts(case.verify()); - - let shape = StableZkProofShape::from_case(&case); - if let Some(baseline) = &baseline { - assert_eq!( - baseline, &shape, - "fresh ZK samples must prove the same public statement and proof shape" - ); - } else { - baseline = Some(shape); - } - - collect_jolt_proof_statistics(&case, &mut all); - if sample_index.is_multiple_of(2) { - collect_jolt_proof_statistics(&case, &mut even); - } else { - collect_jolt_proof_statistics(&case, &mut odd); - } +fn zk_proofs_are_independent_over_hiding_messages() { + let cached = zk_muldiv_case(); + let fresh = fresh_zk_muldiv_case(); + let messages = cached.honest_trace().messages(); + assert_eq!( + messages, + fresh.honest_trace().messages(), + "proofs of one statement share one message layout" + ); + let (header, hiding): (Vec<_>, Vec<_>) = messages + .into_iter() + .partition(|message| message.region == Region::Preamble); + let hiding: Vec = hiding + .into_iter() + .filter(|message| !message.range.is_empty()) + .collect(); + + for message in &header { + assert_eq!( + cached.proof.narg[message.range.clone()], + fresh.proof.narg[message.range.clone()], + "the header is fixed by the statement" + ); } - - let expected = samples as f64 / NUM_BUCKETS as f64; - assert_uniformity(&all, expected); - assert_same_distribution(&even, &odd); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn with_zk_statistical_stack(test: impl FnOnce() + Send + 'static) { - std::thread::Builder::new() - .name("zk-statistical-independence".to_string()) - .stack_size(128 * 1024 * 1024) - .spawn(test) - .expect("spawn ZK statistical-independence test") - .join() - .expect("ZK statistical-independence test panicked"); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn require_release_build() { + let repeated: Vec<_> = hiding + .iter() + .filter(|m| cached.proof.narg[m.range.clone()] == fresh.proof.narg[m.range.clone()]) + .collect(); assert!( - !(cfg!(debug_assertions) - && std::env::var_os("JOLT_VERIFIER_ALLOW_DEBUG_STAT_TESTS").is_none()), - "run this statistical test with --release, or set JOLT_VERIFIER_ALLOW_DEBUG_STAT_TESTS=1" - ); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn statistical_sample_count() -> usize { - std::env::var("JOLT_VERIFIER_ZK_STAT_SAMPLES") - .ok() - .map_or(DEFAULT_SAMPLES, |value| { - value - .parse::() - .expect("JOLT_VERIFIER_ZK_STAT_SAMPLES must be a positive integer") - }) -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Debug, PartialEq)] -struct StableZkProofShape { - public_io: common::jolt_device::JoltDevice, - preprocessing_digest: [u8; 32], - trace_length: usize, - ram_k: usize, - rw_config: jolt_claims::protocols::jolt::JoltReadWriteConfig, - one_hot_config: jolt_claims::protocols::jolt::JoltOneHotConfig, - trace_polynomial_order: jolt_claims::protocols::jolt::TracePolynomialOrder, - commitment_shape: CommitmentShape, - stage_shapes: Vec, - dory_shape: DoryOpeningProofShape, - blindfold_shape: BlindFoldProofShape, -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -impl StableZkProofShape { - fn from_case(case: &crate::support::verifier_fixtures::ZkVerifierFixtureCase) -> Self { - let JoltProofClaims::Zk { blindfold_proof } = &case.proof.claims else { - panic!("ZK statistical fixture must carry a BlindFold proof"); - }; - - Self { - public_io: case.public_io.clone(), - preprocessing_digest: case.preprocessing.preprocessing_digest, - trace_length: case.proof.trace_length, - ram_k: case.proof.ram_K, - rw_config: case.proof.rw_config, - one_hot_config: case.proof.one_hot_config, - trace_polynomial_order: case.proof.trace_polynomial_order, - commitment_shape: CommitmentShape { - instruction_ra: case.proof.commitments.instruction_ra.len(), - ram_ra: case.proof.commitments.ram_ra.len(), - bytecode_ra: case.proof.commitments.bytecode_ra.len(), - has_untrusted_advice: case.proof.untrusted_advice_commitment.is_some(), - }, - stage_shapes: committed_stage_shapes(&case.proof.stages), - dory_shape: DoryOpeningProofShape::from_proof(&case.proof.joint_opening_proof), - blindfold_shape: BlindFoldProofShape::from_proof(blindfold_proof), - } - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Debug, PartialEq, Eq)] -struct CommitmentShape { - instruction_ra: usize, - ram_ra: usize, - bytecode_ra: usize, - has_untrusted_advice: bool, -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Debug, PartialEq, Eq)] -struct CommittedStageShape { - rounds: usize, - degrees: Vec, - output_claim_rows: usize, -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Debug, PartialEq, Eq)] -struct DoryOpeningProofShape { - first_messages: usize, - second_messages: usize, - nu: usize, - sigma: usize, - has_e2: bool, - has_y_com: bool, - has_sigma1: bool, - has_sigma2: bool, - has_scalar_product: bool, -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct BlindFoldProofShape { - auxiliary_rows: usize, - random_round_commitment_rows: usize, - random_output_claim_rows: usize, - random_auxiliary_rows: usize, - random_error_rows: usize, - random_eval_commitments: usize, - cross_term_error_rows: usize, - folded_eval_output_openings: usize, - folded_eval_blinding_openings: usize, -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -impl BlindFoldProofShape { - fn from_proof(proof: &jolt_blindfold::BlindFoldProof) -> Self { - Self { - auxiliary_rows: proof.auxiliary_row_commitments.len(), - random_round_commitment_rows: proof.random_round_commitments.len(), - random_output_claim_rows: proof.random_output_claim_row_commitments.len(), - random_auxiliary_rows: proof.random_auxiliary_row_commitments.len(), - random_error_rows: proof.random_error_row_commitments.len(), - random_eval_commitments: proof.random_eval_commitments.len(), - cross_term_error_rows: proof.cross_term_error_row_commitments.len(), - folded_eval_output_openings: proof.folded_eval_output_openings.len(), - folded_eval_blinding_openings: proof.folded_eval_blinding_openings.len(), - } - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -impl DoryOpeningProofShape { - fn from_proof(proof: &jolt_dory::DoryProof) -> Self { - let proof = &proof.0; - Self { - first_messages: proof.first_messages.len(), - second_messages: proof.second_messages.len(), - nu: proof.nu, - sigma: proof.sigma, - has_e2: proof.e2.is_some(), - has_y_com: proof.y_com.is_some(), - has_sigma1: proof.sigma1_proof.is_some(), - has_sigma2: proof.sigma2_proof.is_some(), - has_scalar_product: proof.scalar_product_proof.is_some(), - } - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn committed_stage_shapes( - stages: &jolt_verifier::proof::JoltStageProofs>, -) -> Vec { - [ - &stages.stage1_uni_skip_first_round_proof, - &stages.stage1_sumcheck_proof, - &stages.stage2_uni_skip_first_round_proof, - &stages.stage2_sumcheck_proof, - &stages.stage3_sumcheck_proof, - &stages.stage4_sumcheck_proof, - &stages.stage5_sumcheck_proof, - &stages.stage6a_sumcheck_proof, - &stages.stage6b_sumcheck_proof, - &stages.stage7_sumcheck_proof, - ] - .into_iter() - .map(committed_stage_shape) - .collect() -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn committed_stage_shape(proof: &SumcheckProof) -> CommittedStageShape { - let proof = proof - .as_committed() - .expect("ZK statistical fixture must use committed sumcheck proofs"); - CommittedStageShape { - rounds: proof.rounds.len(), - degrees: proof.rounds.iter().map(|round| round.degree).collect(), - output_claim_rows: proof.output_claims.commitments.len(), - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn collect_jolt_proof_statistics( - case: &crate::support::verifier_fixtures::ZkVerifierFixtureCase, - tracker: &mut BucketTracker, -) { - let proof = &case.proof; - let JoltProofClaims::Zk { blindfold_proof } = &proof.claims else { - panic!("ZK statistical fixture must carry a BlindFold proof"); - }; - - tracker.record_append("pcs.commitment.rd_inc", &proof.commitments.rd_inc); - tracker.record_append("pcs.commitment.ram_inc", &proof.commitments.ram_inc); - tracker.record_append_positions( - "pcs.commitment.instruction_ra", - &proof.commitments.instruction_ra, + repeated.is_empty(), + "hiding messages identical across fresh proofs: {repeated:?}" ); - tracker.record_append_positions("pcs.commitment.ram_ra", &proof.commitments.ram_ra); - tracker.record_append_positions("pcs.commitment.bytecode_ra", &proof.commitments.bytecode_ra); - if let Some(commitment) = &proof.untrusted_advice_commitment { - tracker.record_append("pcs.commitment.untrusted_advice", commitment); - } - collect_sumcheck_statistics( - "sumcheck.stage1_uniskip", - &proof.stages.stage1_uni_skip_first_round_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage1_batch", - &proof.stages.stage1_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage2_uniskip", - &proof.stages.stage2_uni_skip_first_round_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage2_batch", - &proof.stages.stage2_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage3_batch", - &proof.stages.stage3_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage4_batch", - &proof.stages.stage4_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage5_batch", - &proof.stages.stage5_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage6_address_phase", - &proof.stages.stage6a_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage6_cycle_phase", - &proof.stages.stage6b_sumcheck_proof, - tracker, - ); - collect_sumcheck_statistics( - "sumcheck.stage7_batch", - &proof.stages.stage7_sumcheck_proof, - tracker, - ); - - collect_dory_opening_statistics(&proof.joint_opening_proof, tracker); - collect_blindfold_statistics(blindfold_proof, tracker); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn collect_sumcheck_statistics( - prefix: &'static str, - proof: &SumcheckProof, - tracker: &mut BucketTracker, -) where - C: AppendToTranscript, -{ - let proof = proof - .as_committed() - .expect("ZK statistical fixture must use committed sumcheck proofs"); - for index in selected_positions(proof.rounds.len()) { - tracker.record_append( - format!("{prefix}.round.{index}"), - &proof.rounds[index].commitment, + let cached_buckets = low_byte_buckets(&cached.proof.narg, &hiding); + let fresh_buckets = low_byte_buckets(&fresh.proof.narg, &hiding); + assert!( + hiding.len() >= NUM_BUCKETS * 4, + "too few hiding messages ({}) for a bucketed test", + hiding.len() + ); + for (label, buckets) in [("cached", &cached_buckets), ("fresh", &fresh_buckets)] { + let chi2 = uniform_chi_squared(buckets); + assert!( + chi2 < CHI2_CRITICAL, + "{label} proof: low-byte chi2 {chi2:.2} >= {CHI2_CRITICAL} over {buckets:?}" ); } - tracker.record_append_positions( - &format!("{prefix}.output_claim"), - &proof.output_claims.commitments, - ); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn collect_dory_opening_statistics(proof: &jolt_dory::DoryProof, tracker: &mut BucketTracker) { - let proof = &proof.0; - tracker.record_canonical("dory.vmv.c", &proof.vmv_message.c); - tracker.record_canonical("dory.vmv.d2", &proof.vmv_message.d2); - tracker.record_canonical("dory.vmv.e1", &proof.vmv_message.e1); - if let Some(e2) = &proof.e2 { - tracker.record_canonical("dory.vmv.e2", e2); - } - if let Some(y_com) = &proof.y_com { - tracker.record_canonical("dory.vmv.y_com", y_com); - } - for index in selected_positions(proof.first_messages.len()) { - let message = &proof.first_messages[index]; - let prefix = format!("dory.first.{index}"); - tracker.record_canonical(format!("{prefix}.d1_left"), &message.d1_left); - tracker.record_canonical(format!("{prefix}.d1_right"), &message.d1_right); - tracker.record_canonical(format!("{prefix}.d2_left"), &message.d2_left); - tracker.record_canonical(format!("{prefix}.d2_right"), &message.d2_right); - } - for index in selected_positions(proof.second_messages.len()) { - let message = &proof.second_messages[index]; - let prefix = format!("dory.second.{index}"); - tracker.record_canonical(format!("{prefix}.c_plus"), &message.c_plus); - tracker.record_canonical(format!("{prefix}.c_minus"), &message.c_minus); - tracker.record_canonical(format!("{prefix}.e1_plus"), &message.e1_plus); - tracker.record_canonical(format!("{prefix}.e1_minus"), &message.e1_minus); - tracker.record_canonical(format!("{prefix}.e2_plus"), &message.e2_plus); - tracker.record_canonical(format!("{prefix}.e2_minus"), &message.e2_minus); - } - // A ZK proof carries no clear final message (it would reveal the folded - // witness); the hiding scalar-product Σ-proof recorded below replaces it. + let chi2 = two_sample_chi_squared(&cached_buckets, &fresh_buckets); assert!( - proof.final_message.is_none(), - "ZK proof must not carry a clear final message" - ); - - if let Some(sigma1) = &proof.sigma1_proof { - tracker.record_canonical("dory.sigma1.a1", &sigma1.a1); - tracker.record_canonical("dory.sigma1.a2", &sigma1.a2); - tracker.record_canonical("dory.sigma1.z1", &sigma1.z1); - tracker.record_canonical("dory.sigma1.z2", &sigma1.z2); - tracker.record_canonical("dory.sigma1.z3", &sigma1.z3); - } - if let Some(sigma2) = &proof.sigma2_proof { - tracker.record_canonical("dory.sigma2.a", &sigma2.a); - tracker.record_canonical("dory.sigma2.z1", &sigma2.z1); - tracker.record_canonical("dory.sigma2.z2", &sigma2.z2); - } - if let Some(scalar_product) = &proof.scalar_product_proof { - tracker.record_canonical("dory.scalar_product.p1", &scalar_product.p1); - tracker.record_canonical("dory.scalar_product.p2", &scalar_product.p2); - tracker.record_canonical("dory.scalar_product.q", &scalar_product.q); - tracker.record_canonical("dory.scalar_product.r", &scalar_product.r); - tracker.record_canonical("dory.scalar_product.e1", &scalar_product.e1); - tracker.record_canonical("dory.scalar_product.e2", &scalar_product.e2); - tracker.record_canonical("dory.scalar_product.r1", &scalar_product.r1); - tracker.record_canonical("dory.scalar_product.r2", &scalar_product.r2); - tracker.record_canonical("dory.scalar_product.r3", &scalar_product.r3); - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn collect_blindfold_statistics( - proof: &jolt_blindfold::BlindFoldProof, - tracker: &mut BucketTracker, -) { - tracker.record_canonical("blindfold.random.u", &proof.random_u); - tracker.record_append_positions( - "blindfold.random.round_commitment", - &proof.random_round_commitments, - ); - tracker.record_append_positions( - "blindfold.random.output_claim_commitment", - &proof.random_output_claim_row_commitments, - ); - tracker.record_append_positions( - "blindfold.random.auxiliary_commitment", - &proof.random_auxiliary_row_commitments, - ); - tracker.record_append_positions( - "blindfold.random.error_commitment", - &proof.random_error_row_commitments, - ); - tracker.record_append_positions( - "blindfold.random.eval_commitment", - &proof.random_eval_commitments, - ); - tracker.record_append_positions( - "blindfold.real.auxiliary_commitment", - &proof.auxiliary_row_commitments, - ); - tracker.record_append_positions( - "blindfold.cross_term_commitment", - &proof.cross_term_error_row_commitments, + chi2 < CHI2_CRITICAL, + "cached and fresh low-byte distributions differ: chi2 {chi2:.2}" ); - tracker.record_canonical("blindfold.az_rx", &proof.az_rx); - tracker.record_canonical("blindfold.bz_rx", &proof.bz_rx); - tracker.record_canonical("blindfold.cz_rx", &proof.cz_rx); - tracker.record_vector_opening("blindfold.witness_opening", &proof.witness_opening); - tracker.record_vector_opening("blindfold.error_opening", &proof.error_opening); - tracker.record_canonical_positions("blindfold.folded_eval.output", &proof.folded_eval_outputs); - tracker.record_canonical_positions( - "blindfold.folded_eval.blinding", - &proof.folded_eval_blindings, - ); - // These openings are fixed-coordinate checks; the rows intentionally contain - // structural zero slots, so the hiding component to sample is the opening - // blinding. - for index in selected_positions(proof.folded_eval_output_openings.len()) { - tracker.record_vector_opening_blinding( - &format!("blindfold.folded_eval.output_opening.{index}"), - &proof.folded_eval_output_openings[index], - ); - } - for index in selected_positions(proof.folded_eval_blinding_openings.len()) { - tracker.record_vector_opening_blinding( - &format!("blindfold.folded_eval.blinding_opening.{index}"), - &proof.folded_eval_blinding_openings[index], - ); - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -#[derive(Clone, Debug, Default)] -struct BucketTracker { - buckets: BTreeMap>, - samples: BTreeMap>, -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -impl BucketTracker { - fn new() -> Self { - Self::default() - } - - fn record_append(&mut self, name: impl Into, value: &A) { - let name = name.into(); - let mut transcript = Blake2bTranscript::::new(b"jolt-zk-stat"); - transcript.append_bytes(name.as_bytes()); - value.append_to_transcript(&mut transcript); - self.record_projected(name, field_low_u64(transcript.challenge())); - } - - fn record_append_positions(&mut self, prefix: &str, values: &[A]) { - for index in selected_positions(values.len()) { - self.record_append(format!("{prefix}.{index}"), &values[index]); - } - } - - fn record_canonical(&mut self, name: impl Into, value: &A) { - let name = name.into(); - let mut bytes = Vec::new(); - value - .serialize_compressed(&mut bytes) - .expect("canonical serialization succeeds"); - self.record_bytes(name, &bytes); - } - fn record_canonical_positions(&mut self, prefix: &str, values: &[A]) { - for index in selected_positions(values.len()) { - self.record_canonical(format!("{prefix}.{index}"), &values[index]); - } - } - - fn record_vector_opening( - &mut self, - prefix: &str, - opening: &jolt_crypto::VectorCommitmentOpening, - ) where - F: CanonicalSerialize, - { - self.record_canonical_positions(&format!("{prefix}.row"), &opening.combined_vector); - self.record_canonical(format!("{prefix}.blinding"), &opening.combined_blinding); - } - - fn record_vector_opening_blinding( - &mut self, - prefix: &str, - opening: &jolt_crypto::VectorCommitmentOpening, - ) where - F: CanonicalSerialize, - { - self.record_canonical(format!("{prefix}.blinding"), &opening.combined_blinding); - } - - fn record_bytes(&mut self, name: String, bytes: &[u8]) { - let mut transcript = Blake2bTranscript::::new(b"jolt-zk-stat"); - transcript.append_bytes(name.as_bytes()); - transcript.append_bytes(bytes); - self.record_projected(name, field_low_u64(transcript.challenge())); - } - - fn record_projected(&mut self, name: String, value: u64) { - self.buckets - .entry(name.clone()) - .or_insert_with(|| vec![0; NUM_BUCKETS])[(value as usize) % NUM_BUCKETS] += 1; - self.samples.entry(name).or_default().push(value); - } - - fn names(&self) -> Vec { - self.buckets.keys().cloned().collect() - } - - fn chi_squared(&self, name: &str, expected: f64) -> f64 { - self.buckets[name] + // BlindFold is exempt: it reveals the folded witness, and each folded + // evaluation output and blinding is sent once as a scalar and again as + // the dedicated row's opened coordinate, which the verifier checks equal. + for (label, narg) in [("cached", &cached.proof.narg), ("fresh", &fresh.proof.narg)] { + let mut first_seen = BTreeMap::new(); + let repeats: Vec<_> = hiding .iter() - .map(|&observed| { - let delta = observed as f64 - expected; - delta * delta / expected + .filter(|message| message.region != Region::BlindFold) + .filter_map(|message| { + let range = &message.range; + let prefix = &narg[range.start..range.start + 8.min(range.len())]; + first_seen + .insert(prefix, message) + .map(|earlier| (earlier, message)) }) - .sum() - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn selected_positions(len: usize) -> Vec { - match len { - 0 => Vec::new(), - 1 => vec![0], - 2 => vec![0, 1], - _ => { - let mut positions = vec![0, len / 2, len - 1]; - positions.dedup(); - positions - } - } -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn field_low_u64(value: Fr) -> u64 { - let bytes = value.to_bytes_le_vec(); - u64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]) -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn assert_uniformity(tracker: &BucketTracker, expected: f64) { - let mut failures = Vec::new(); - for name in tracker.names() { - let unique = unique_sample_count(&tracker.samples[&name]); - let minimum_unique = tracker.samples[&name].len() * 95 / 100; - if unique < minimum_unique { - failures.push(format!( - "{name}: only {unique} unique projected samples out of {}", - tracker.samples[&name].len() - )); - continue; - } - - let chi2 = tracker.chi_squared(&name, expected); - if chi2 >= CHI2_CRITICAL { - failures.push(format!("{name}: chi2={chi2:.2} >= {CHI2_CRITICAL:.2}")); - } + .collect(); + assert!( + repeats.is_empty(), + "{label} proof repeats a hiding message's leading bytes: {repeats:?}" + ); } - - assert!( - failures.is_empty(), - "ZK statistical uniformity check failed for {} projected components:\n{}", - failures.len(), - failures.join("\n") - ); } -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn assert_same_distribution(lhs: &BucketTracker, rhs: &BucketTracker) { - let mut failures = Vec::new(); - for name in lhs.names() { - let Some(rhs_buckets) = rhs.buckets.get(&name) else { - continue; - }; - let chi2 = two_sample_chi_squared(&lhs.buckets[&name], rhs_buckets); - if chi2 >= CHI2_CRITICAL { - failures.push(format!( - "{name}: split-half chi2={chi2:.2} >= {CHI2_CRITICAL:.2}" - )); - } +/// Buckets each message's first byte, the low byte of its first canonical +/// coordinate or scalar, by its low nibble. +fn low_byte_buckets(narg: &[u8], messages: &[Message]) -> [usize; NUM_BUCKETS] { + let mut buckets = [0; NUM_BUCKETS]; + for message in messages { + buckets[usize::from(narg[message.range.start]) % NUM_BUCKETS] += 1; } - - assert!( - failures.is_empty(), - "ZK statistical split-half check failed for {} projected components:\n{}", - failures.len(), - failures.join("\n") - ); + buckets } -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn unique_sample_count(values: &[u64]) -> usize { - let mut values = values.to_vec(); - values.sort_unstable(); - values.dedup(); - values.len() +fn uniform_chi_squared(buckets: &[usize; NUM_BUCKETS]) -> f64 { + let expected = buckets.iter().sum::() as f64 / NUM_BUCKETS as f64; + buckets + .iter() + .map(|&observed| (observed as f64 - expected).powi(2) / expected) + .sum() } -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -fn two_sample_chi_squared(a: &[usize], b: &[usize]) -> f64 { +fn two_sample_chi_squared(a: &[usize; NUM_BUCKETS], b: &[usize; NUM_BUCKETS]) -> f64 { let n_a = a.iter().sum::() as f64; let n_b = b.iter().sum::() as f64; - let n_total = n_a + n_b; - + let n = n_a + n_b; a.iter() .zip(b) - .map(|(&observed_a, &observed_b)| { - let pooled = observed_a as f64 + observed_b as f64; - if pooled < 1.0 { - return 0.0; - } - let expected_a = pooled * n_a / n_total; - let expected_b = pooled * n_b / n_total; - let term_a = if expected_a > 0.0 { - (observed_a as f64 - expected_a).powi(2) / expected_a - } else { - 0.0 - }; - let term_b = if expected_b > 0.0 { - (observed_b as f64 - expected_b).powi(2) / expected_b - } else { - 0.0 - }; - term_a + term_b + .filter(|(&x, &y)| x + y > 0) + .map(|(&x, &y)| { + let pooled = (x + y) as f64; + let expected_a = pooled * n_a / n; + let expected_b = pooled * n_b / n; + (x as f64 - expected_a).powi(2) / expected_a + + (y as f64 - expected_b).powi(2) / expected_b }) .sum() } - -#[cfg(any(not(feature = "prover-fixtures"), not(feature = "zk")))] -#[test] -#[ignore = "enable --features prover-fixtures,zk and run with --release to generate fresh ZK proof samples"] -fn zk_muldiv_jolt_proof_components_are_statistically_independent() {} diff --git a/crates/jolt-verifier/tests/support/mod.rs b/crates/jolt-verifier/tests/support/mod.rs index 8ceea89d48..8a9d7439ee 100644 --- a/crates/jolt-verifier/tests/support/mod.rs +++ b/crates/jolt-verifier/tests/support/mod.rs @@ -1,18 +1,3 @@ -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum VerifierPhase { - Preamble, - Commitments, - Stage1, - Stage2, - Stage3, - Stage4, - Stage5, - Stage6, - Stage7, - Stage8Openings, - Zk, -} - pub fn assert_accepts_mode(zk: bool, result: Result<(), VerifierError>) { let result_debug = format!("{result:?}"); @@ -59,23 +44,7 @@ pub mod guest_fixtures; #[cfg(all(feature = "prover-fixtures", not(feature = "akita")))] pub mod verifier_fixtures; -// TEMPORARY: these modules still target the pre-NARG proof struct and the -// removed `fs-audit` feature. `cfg(any())` keeps them out of every build until -// the NARG tamper/FS port restores their real gates (`fs-audit` becomes -// `logging`): -// fs_mutations, fs_transcript: feature = "fs-audit" -// proof_claims: feature = "prover-fixtures" -// tamper_manifest: always -// zk_audit: feature = "zk" -#[cfg(any())] -pub mod fs_mutations; -#[cfg(any())] -pub mod fs_transcript; -#[cfg(any())] -pub mod proof_claims; -#[cfg(any())] -pub mod tamper_manifest; -#[cfg(any())] -pub mod zk_audit; +#[cfg(all(feature = "prover-fixtures", feature = "logging"))] +pub mod narg; use jolt_verifier::VerifierError; diff --git a/crates/jolt-verifier/tests/support/narg.rs b/crates/jolt-verifier/tests/support/narg.rs new file mode 100644 index 0000000000..707b65cb2a --- /dev/null +++ b/crates/jolt-verifier/tests/support/narg.rs @@ -0,0 +1,825 @@ +//! Argument-string tamper harness: traced verification, protocol regions, and +//! the per-message rejection deadlines the soundness sweeps assert. +//! +//! A proof is its NARG, and every prover message is a byte range of it. Under +//! the `logging` feature the verifier transcript records each operation with +//! its protocol site, so a run over a tampered NARG reports how far +//! verification got before it rejected. + +#![expect( + clippy::expect_used, + clippy::panic, + reason = "tamper harness helpers fail loudly when a fixture or trace breaks an assumption" +)] + +use std::collections::BTreeMap; +use std::fmt::Write as _; +use std::ops::Range; + +use common::jolt_device::JoltDevice; +use jolt_crypto::{Commitment, VectorCommitment}; +use jolt_openings::{CommitmentScheme, OpeningsError}; +use jolt_transcript::{ + Channel, ProverTranscript, SiteId, TranscriptError, TranscriptEvent, TranscriptOp, + VerifierTranscript, +}; +use jolt_verifier::sites::{ + BLINDFOLD, COMMITMENTS, PREAMBLE, STAGE1, STAGE2, STAGE3, STAGE4, STAGE5, STAGE6A, STAGE6B, + STAGE7, STAGE8, +}; +use jolt_verifier::{ + jolt_protocol_id, JoltProof, JoltSponge, JoltVerifierPreprocessing, ProofHeader, VerifierError, + JOLT_SESSION, +}; + +/// A Jolt protocol region, in transcript order. Each is entered by the +/// verifier setting the matching [`jolt_verifier::sites`] site. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Region { + Preamble, + Commitments, + Stage1, + Stage2, + Stage3, + Stage4, + Stage5, + Stage6a, + Stage6b, + Stage7, + Stage8, + BlindFold, +} + +impl Region { + pub const ALL: [Self; 12] = [ + Self::Preamble, + Self::Commitments, + Self::Stage1, + Self::Stage2, + Self::Stage3, + Self::Stage4, + Self::Stage5, + Self::Stage6a, + Self::Stage6b, + Self::Stage7, + Self::Stage8, + Self::BlindFold, + ]; + + /// The regions a proof of this build passes through. + pub fn expected() -> Vec { + Self::ALL + .into_iter() + .filter(|region| cfg!(feature = "zk") || *region != Self::BlindFold) + .collect() + } + + fn from_site(site: SiteId) -> Option { + [ + (PREAMBLE, Self::Preamble), + (COMMITMENTS, Self::Commitments), + (STAGE1, Self::Stage1), + (STAGE2, Self::Stage2), + (STAGE3, Self::Stage3), + (STAGE4, Self::Stage4), + (STAGE5, Self::Stage5), + (STAGE6A, Self::Stage6a), + (STAGE6B, Self::Stage6b), + (STAGE7, Self::Stage7), + (STAGE8, Self::Stage8), + (BLINDFOLD, Self::BlindFold), + ] + .into_iter() + .find_map(|(candidate, region)| (candidate == site).then_some(region)) + } + + /// Whether the region's prover messages are all clear field elements + /// (sumcheck coefficients and opening claims). + pub fn is_clear_sumcheck_stage(self) -> bool { + !cfg!(feature = "zk") && self >= Self::Stage1 && self <= Self::Stage7 + } + + /// The latest region by which the verifier must reject an altered public + /// statement (preamble values or public commitments), which only moves + /// the challenges: Stage 1 in the clear, Stage 8 under ZK, as for the + /// header and commitments in [`Trace::messages`]. + pub fn statement_deadline() -> Self { + if cfg!(feature = "zk") { + Self::Stage8 + } else { + Self::Stage1 + } + } +} + +/// One prover message of a traced run. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Message { + pub region: Region, + pub range: Range, + /// The latest region by which an alteration of this message must be + /// rejected. + pub deadline: Region, +} + +/// The outcome of one verification over an argument string, with every +/// transcript operation it completed. +#[derive(Debug)] +pub struct Trace { + pub events: Vec, + pub result: Result<(), VerifierError>, +} + +impl Trace { + /// The region of each event. A nested protocol (the PCS) may tag its own + /// sites; those operations belong to the Jolt region that called it. + pub fn regions(&self) -> Vec { + let mut current = Region::Preamble; + self.events + .iter() + .map(|event| { + if let Some(region) = Region::from_site(event.site) { + current = region; + } + current + }) + .collect() + } + + /// The run's prover messages with their rejection deadlines. + /// + /// Every message is absorbed, so altering one changes its value and + /// re-randomizes every later challenge. The deadline is the first region + /// that checks either: + /// + /// - Clear builds: the region of the first challenge drawn after the + /// message (its own region if none follows). Every region that draws a + /// challenge ends in a check that depends on it: each stage's batched + /// sumcheck ends in its output check (compressed rounds recover each + /// round's omitted coefficient from the running claim, so the rounds are + /// checked through that final evaluation), uni-skip checks its first + /// round's sum explicitly, and Stage 8's opening checks the joint claim. + /// So round polynomials are caught in their own stage, the header and + /// commitments by Stage 1 (or in place, by `validate_inputs` or the + /// commitment decoder), and a stage's final output claims, sent after + /// its last challenge, by the next region. That last case is real: + /// some Stage 1 outer openings are consumed only as Stage 2's + /// product-virtualization inputs, some Stage 2 claims only by Stage 3's + /// output check, and Stage 6a's address-phase claims only by Stage 6b's. + /// - ZK builds: Stage 8 for the header, commitments, and Stages 1–7, whose + /// messages are hiding commitments with no clear check; the Stage 8 + /// Dory evaluation proof is bound to the transcript and to the opening + /// point every stage challenge feeds. Stage 8 and BlindFold messages by + /// their own region, which verifies them before returning. + pub fn messages(&self) -> Vec { + let regions = self.regions(); + let mut next_challenge = None; + let mut deadlines = vec![None; self.events.len()]; + for (index, event) in self.events.iter().enumerate().rev() { + deadlines[index] = next_challenge; + if event.op == TranscriptOp::Challenge { + next_challenge = Some(regions[index]); + } + } + self.events + .iter() + .zip(regions) + .zip(deadlines) + .filter(|((event, _), _)| event.op == TranscriptOp::Message) + .map(|((event, region), next_challenge)| Message { + region, + range: event + .narg + .clone() + .expect("every message event records its argument-string range"), + deadline: if cfg!(feature = "zk") { + region.max(Region::Stage8) + } else { + next_challenge.unwrap_or(region).max(region) + }, + }) + .collect() + } + + /// Where verification stopped: the region of the last completed + /// operation (the preamble if none completed). A rejection between two + /// regions, before the later one performs any operation, is attributed + /// to the earlier one. + pub fn stop_region(&self) -> Region { + self.regions().last().copied().unwrap_or(Region::Preamble) + } +} + +/// The polynomial commitment type of a fixture's scheme. +pub type CommitmentOf = <::Pcs as Commitment>::Output; + +/// A fixture the harness can verify over an arbitrary statement and argument +/// string. +pub trait TracedCase { + type Pcs: CommitmentScheme; + type Vc: VectorCommitment::Field>; + + /// Width of one field element message. + const FIELD_BYTES: usize; + + fn preprocessing(&self) -> &JoltVerifierPreprocessing; + + fn public_io(&self) -> &JoltDevice; + + fn proof(&self) -> &JoltProof; + + fn trusted_advice_commitment(&self) -> Option<&::Output>; + + /// The public `jolt_verifier::verify` entry point. + fn verify_statement( + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice, + proof: &JoltProof, + trusted_advice_commitment: Option<&::Output>, + ) -> Result<(), VerifierError>; + + /// Verifies `narg` exactly as `jolt_verifier::verify` does after its + /// protocol-config check, keeping the transcript's event log. + fn trace_statement( + preprocessing: &JoltVerifierPreprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&::Output>, + narg: &[u8], + ) -> Trace; + + fn trace(&self, narg: &[u8]) -> Trace { + Self::trace_statement( + self.preprocessing(), + self.public_io(), + self.trusted_advice_commitment(), + narg, + ) + } + + fn verify_proof(&self, proof: &JoltProof) -> Result<(), VerifierError> { + Self::verify_statement( + self.preprocessing(), + self.public_io(), + proof, + self.trusted_advice_commitment(), + ) + } + + /// The honest proof against `preprocessing` in place of the fixture's. + fn verify_statement_with( + &self, + preprocessing: &JoltVerifierPreprocessing, + ) -> Result<(), VerifierError> { + Self::verify_statement( + preprocessing, + self.public_io(), + self.proof(), + self.trusted_advice_commitment(), + ) + } + + /// The honest proof's trace, checked to accept through both the traced + /// replay and the public entry point. + fn honest_trace(&self) -> Trace { + self.verify_proof(self.proof()) + .expect("honest fixture verifies through jolt_verifier::verify"); + let trace = self.trace(&self.proof().narg); + if let Err(error) = &trace.result { + panic!("honest fixture rejected by the traced replay: {error:?}"); + } + trace + } + + fn with_narg(&self, narg: Vec) -> JoltProof { + JoltProof { + protocol: self.proof().protocol, + narg, + } + } +} + +/// Ends a traced run: records the events, then applies the exact-consumption +/// check `verify` applies. +fn finish_trace( + transcript: VerifierTranscript<'_, JoltSponge>, + result: Result<(), VerifierError>, +) -> Trace { + let events = transcript.events().to_vec(); + let result = result.and_then(|()| transcript.finish().map_err(VerifierError::from)); + Trace { events, result } +} + +fn new_transcript(narg: &[u8]) -> VerifierTranscript<'_, JoltSponge> { + VerifierTranscript::new(&jolt_protocol_id::(), JOLT_SESSION, narg) +} + +#[cfg(not(feature = "akita"))] +mod dory { + use common::jolt_device::JoltDevice; + use jolt_crypto::{Bn254G1, Pedersen}; + use jolt_dory::{DoryCommitment, DoryScheme}; + use jolt_field::{CanonicalBytes, Fr}; + use jolt_transcript::Channel; + use jolt_verifier::sites::BLINDFOLD; + use jolt_verifier::{ + verify, verify_stages, JoltProof, JoltSponge, JoltVerifierPreprocessing, VerifiedStages, + VerifierError, + }; + + use super::{finish_trace, new_transcript, Trace, TracedCase}; + + type Preprocessing = JoltVerifierPreprocessing>; + + fn trace( + preprocessing: &Preprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&DoryCommitment>, + narg: &[u8], + ) -> Trace { + let mut transcript = new_transcript(narg); + let result = verify_stages::, _>( + preprocessing, + public_io, + trusted_advice_commitment, + &mut transcript, + ) + .and_then(|stages| match stages { + VerifiedStages::Clear => Ok(()), + VerifiedStages::Zk(protocol) => { + transcript.site(BLINDFOLD); + let vc_setup = preprocessing + .vc_setup + .as_ref() + .ok_or(VerifierError::MissingVectorCommitmentSetup)?; + protocol + .verify::, _>(vc_setup, &mut transcript) + .map_err(|error| VerifierError::BlindFoldVerificationFailed { + reason: error.to_string(), + }) + } + }); + finish_trace(transcript, result) + } + + macro_rules! impl_traced_case { + ($case:ty) => { + impl TracedCase for $case { + type Pcs = DoryScheme; + type Vc = Pedersen; + const FIELD_BYTES: usize = ::NUM_BYTES; + + fn preprocessing(&self) -> &Preprocessing { + &self.preprocessing + } + + fn public_io(&self) -> &JoltDevice { + &self.public_io + } + + fn proof(&self) -> &JoltProof { + &self.proof + } + + fn trusted_advice_commitment(&self) -> Option<&DoryCommitment> { + self.trusted_advice_commitment.as_ref() + } + + fn verify_statement( + preprocessing: &Preprocessing, + public_io: &JoltDevice, + proof: &JoltProof, + trusted_advice_commitment: Option<&DoryCommitment>, + ) -> Result<(), VerifierError> { + verify::, JoltSponge>( + preprocessing, + public_io, + proof, + trusted_advice_commitment, + ) + } + + fn trace_statement( + preprocessing: &Preprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&DoryCommitment>, + narg: &[u8], + ) -> Trace { + trace(preprocessing, public_io, trusted_advice_commitment, narg) + } + } + }; + } + + #[cfg(not(feature = "zk"))] + impl_traced_case!(crate::support::verifier_fixtures::VerifierFixtureCase); + #[cfg(feature = "zk")] + impl_traced_case!(crate::support::verifier_fixtures::ZkVerifierFixtureCase); +} + +#[cfg(feature = "akita")] +mod akita { + use common::jolt_device::JoltDevice; + use jolt_akita::{AkitaCommitment, AkitaField, AkitaScheme}; + use jolt_crypto::Commitment; + use jolt_field::CanonicalBytes; + use jolt_prover::akita::preprocessing::AkitaVc; + use jolt_verifier::stages::{ + stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7, stage8, + }; + use jolt_verifier::verifier::SeededTranscript; + use jolt_verifier::{ + seed_transcript, verify, JoltProof, JoltSponge, JoltVerifierPreprocessing, VerifierError, + }; + + use super::{finish_trace, new_transcript, Trace, TracedCase}; + use crate::support::akita_fixtures::AkitaFixtureCase; + + type Preprocessing = JoltVerifierPreprocessing; + + /// `jolt_verifier::verify`'s Akita body after its protocol-config check. + /// The Akita build exports no stage-spine entry point, so the sequence is + /// restated here; `TracedCase::honest_trace` checks it against `verify`. + fn stages( + preprocessing: &Preprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&AkitaCommitment>, + transcript: &mut jolt_transcript::VerifierTranscript<'_, JoltSponge>, + ) -> Result<(), VerifierError> { + let SeededTranscript { + checked, + commitments, + formula_dimensions, + } = seed_transcript( + preprocessing, + public_io, + trusted_advice_commitment, + transcript, + )?; + let dims = &formula_dimensions; + let s1 = + stage1::verify::::Output, _>(&checked, transcript)?; + let s2 = stage2::verify(&checked, transcript, &s1)?; + let s3 = stage3::verify(&checked, transcript, &s1, &s2)?; + let s4 = stage4::verify(&checked, preprocessing, transcript, &s2, &s3)?; + let s5 = stage5::verify(&checked, dims, transcript, &s2, &s4)?; + let s6a = stage6a::verify( + &checked, + preprocessing, + dims, + transcript, + &s1, + &s2, + &s3, + &s4, + &s5, + )?; + let s6b = stage6b::verify( + &checked, + preprocessing, + dims, + transcript, + &s1, + &s2, + &s3, + &s4, + &s5, + &s6a, + )?; + let s7 = stage7::verify(&checked, dims, transcript, &s4, &s6b)?; + let s8 = stage8::verify::( + &checked, + preprocessing, + &commitments, + dims, + trusted_advice_commitment, + transcript, + &s4, + &s6b, + &s7, + )?; + match s8 { + stage8::Stage8Output::Clear => Ok(()), + stage8::Stage8Output::Zk(_) => { + Err(VerifierError::ExpectedClearProof { field: "stage8" }) + } + } + } + + impl TracedCase for AkitaFixtureCase { + type Pcs = AkitaScheme; + type Vc = AkitaVc; + const FIELD_BYTES: usize = ::NUM_BYTES; + + fn preprocessing(&self) -> &Preprocessing { + &self.preprocessing + } + + fn public_io(&self) -> &JoltDevice { + &self.public_io + } + + fn proof(&self) -> &JoltProof { + &self.proof + } + + fn trusted_advice_commitment(&self) -> Option<&AkitaCommitment> { + self.trusted_advice_commitment.as_ref() + } + + fn verify_statement( + preprocessing: &Preprocessing, + public_io: &JoltDevice, + proof: &JoltProof, + trusted_advice_commitment: Option<&AkitaCommitment>, + ) -> Result<(), VerifierError> { + verify::( + preprocessing, + public_io, + proof, + trusted_advice_commitment, + ) + } + + fn trace_statement( + preprocessing: &Preprocessing, + public_io: &JoltDevice, + trusted_advice_commitment: Option<&AkitaCommitment>, + narg: &[u8], + ) -> Trace { + let mut transcript = new_transcript(narg); + let result = stages( + preprocessing, + public_io, + trusted_advice_commitment, + &mut transcript, + ); + finish_trace(transcript, result) + } + } +} + +/// Asserts the honest message events tile the argument string: every byte +/// belongs to exactly one message, in order, and only message events carry a +/// range. Also asserts every region of this build carries at least one +/// message and no foreign Jolt region appears. +pub fn assert_coverage_closure(trace: &Trace, narg_len: usize) { + let mut cursor = 0; + for event in &trace.events { + match (event.op, &event.narg) { + (TranscriptOp::Message, Some(range)) => { + assert_eq!( + range.start, cursor, + "message at {range:?} leaves bytes {cursor}..{} unaccounted", + range.start + ); + assert_eq!( + range.len(), + event.len, + "message length disagrees with its range" + ); + cursor = range.end; + } + (TranscriptOp::Message, None) => panic!("message event without a range: {event:?}"), + (TranscriptOp::Public | TranscriptOp::Challenge, None) => {} + (TranscriptOp::Public | TranscriptOp::Challenge, Some(_)) => { + panic!("non-message event carries an argument-string range: {event:?}") + } + } + } + assert_eq!( + cursor, narg_len, + "argument-string bytes after the last message" + ); + + let present: Vec = { + let mut regions: Vec<_> = trace.messages().into_iter().map(|m| m.region).collect(); + regions.dedup(); + regions + }; + assert_eq!( + present, + Region::expected(), + "message regions, in transcript order, differ from this build's protocol regions" + ); +} + +/// One single-byte alteration of the argument string. +#[derive(Clone, Debug)] +pub struct Tamper { + pub region: Region, + pub deadline: Region, + pub message: Range, + pub offset: usize, +} + +/// The tampers of `message`: the low byte of every field element in a clear +/// sumcheck stage (each flip is a well-formed claim or coefficient off by +/// one), else the first, middle, and last byte. An empty message (a +/// zero-count vector) has none. +pub fn tampers_of(message: &Message) -> Vec { + let len = message.range.len(); + let offsets: Vec = if len == 0 { + Vec::new() + } else if message.region.is_clear_sumcheck_stage() { + assert_eq!( + len % C::FIELD_BYTES, + 0, + "clear stage message {message:?} is not a whole number of field elements" + ); + (0..len).step_by(C::FIELD_BYTES).collect() + } else { + let mut offsets = vec![0, len / 2, len - 1]; + offsets.dedup(); + offsets + }; + offsets + .into_iter() + .map(|offset| Tamper { + region: message.region, + deadline: message.deadline, + message: message.range.clone(), + offset: message.range.start + offset, + }) + .collect() +} + +/// Every tamper of every message, or a deterministic stride sample of at +/// most `budget` of them that always keeps each region's first and last +/// tamper. +pub fn select_tampers(messages: &[Message], budget: Option) -> Vec { + let all: Vec = messages.iter().flat_map(tampers_of::).collect(); + let Some(budget) = budget.filter(|&budget| budget < all.len()) else { + return all; + }; + let stride = all.len().div_ceil(budget); + all.iter() + .enumerate() + .filter(|(index, tamper)| { + index % stride == 0 + || index + .checked_sub(1) + .and_then(|previous| all.get(previous)) + .map(|t| t.region) + != Some(tamper.region) + || all.get(index + 1).map(|t| t.region) != Some(tamper.region) + }) + .map(|(_, tamper)| tamper.clone()) + .collect() +} + +/// Where each tamper of a sweep was rejected, keyed by (message region, +/// rejection region). +#[derive(Debug, Default)] +pub struct SweepReport { + pub tampers: usize, + pub messages: usize, + pub rejections: BTreeMap<(Region, Region), usize>, +} + +impl SweepReport { + pub fn summary(&self) -> String { + let mut out = format!("{} tampers over {} messages\n", self.tampers, self.messages); + for ((message, rejected), count) in &self.rejections { + let _ = writeln!(out, " {message:?} -> {rejected:?}: {count}"); + } + out + } +} + +/// Flips the low bit of each selected byte, one tamper at a time, and +/// requires every tampered proof to reject no later than its message's +/// deadline ([`Trace::messages`]). +pub fn sweep(case: &C, budget: Option) -> SweepReport { + let honest = case.honest_trace(); + let messages = honest.messages(); + let tampers = select_tampers::(&messages, budget); + let mut report = SweepReport { + tampers: tampers.len(), + messages: messages.len(), + ..SweepReport::default() + }; + let mut failures = Vec::new(); + let mut narg = case.proof().narg.clone(); + for tamper in &tampers { + narg[tamper.offset] ^= 1; + let trace = case.trace(&narg); + narg[tamper.offset] ^= 1; + let stopped = trace.stop_region(); + *report + .rejections + .entry((tamper.region, stopped)) + .or_default() += 1; + if trace.result.is_ok() { + failures.push(format!("accepted: {tamper:?}")); + } else if stopped > tamper.deadline { + failures.push(format!( + "rejected in {stopped:?} after its deadline: {tamper:?}: {:?}", + trace.result + )); + } + } + assert!( + failures.is_empty(), + "{} of {} tampers broke their deadline:\n{}\n{}", + failures.len(), + tampers.len(), + report.summary(), + failures + .iter() + .take(40) + .cloned() + .collect::>() + .join("\n") + ); + report +} + +/// The argument string cut at each selected message boundary must fail +/// with a truncation error: typed, or carried in the reason of the wrapping +/// error (the sumcheck verifier, the commitment decoder, the opening +/// verifier, and BlindFold report transcript failures as strings). +/// +/// WARNING: `jolt-akita`'s native opening maps every Akita verifier error, +/// truncation included, to the bare `OpeningsError::VerificationFailed`, so +/// on the Akita build a cut inside Stage 8 can only be required to fail that +/// opening. +pub fn assert_truncations_reject(case: &C, budget: Option) { + let honest = case.honest_trace(); + let narg = &case.proof().narg; + let boundaries: Vec<(usize, Region)> = honest + .messages() + .iter() + .map(|message| (message.range.start, message.region)) + .collect(); + let stride = budget.map_or(1, |budget| boundaries.len().div_ceil(budget).max(1)); + let mut failures = Vec::new(); + for (index, &(boundary, region)) in boundaries.iter().enumerate() { + if index % stride != 0 && index + 1 != boundaries.len() { + continue; + } + let result = case.trace(&narg[..boundary]).result; + let opaque_akita_opening = cfg!(feature = "akita") + && region == Region::Stage8 + && matches!( + &result, + Err(VerifierError::FinalOpeningVerificationFailed { reason }) + if *reason == OpeningsError::VerificationFailed.to_string() + ); + if !is_truncation(&result) && !opaque_akita_opening { + failures.push(format!("cut at {boundary}: {result:?}")); + } + } + assert!( + failures.is_empty(), + "truncations not rejected as truncated:\n{}", + failures.join("\n") + ); +} + +fn is_truncation(result: &Result<(), VerifierError>) -> bool { + let truncated = TranscriptError::Truncated.to_string(); + match result { + Err(VerifierError::Transcript(TranscriptError::Truncated)) => true, + Err( + VerifierError::MalformedCommitment { reason } + | VerifierError::FinalOpeningVerificationFailed { reason } + | VerifierError::FinalOpeningBatchFailed { reason } + | VerifierError::BlindFoldVerificationFailed { reason } + | VerifierError::StageClaimSumcheckFailed { reason, .. }, + ) => reason.contains(&truncated), + _ => false, + } +} + +/// The proof header's encoding, produced by the production encoder. +pub fn encode_header(header: &ProofHeader) -> Vec { + let mut transcript = + ProverTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION); + transcript.site(PREAMBLE); + header.send(&mut transcript); + transcript.finish() +} + +/// The proof's header, decoded by the production decoder. +pub fn decode_header(narg: &[u8]) -> ProofHeader { + ProofHeader::receive(&mut new_transcript(narg)).expect("honest proof header decodes") +} + +/// `narg` with its header replaced by `header`'s encoding. +pub fn with_header(narg: &[u8], header: &ProofHeader) -> Vec { + let honest_len = encode_header(&decode_header(narg)).len(); + let mut out = encode_header(header); + out.extend_from_slice(&narg[honest_len..]); + out +} + +/// The `index`-th message of `region` in the honest trace. +pub fn message_in(trace: &Trace, region: Region, index: usize) -> Range { + trace + .messages() + .into_iter() + .filter(|message| message.region == region) + .nth(index) + .unwrap_or_else(|| panic!("no message {index} in {region:?}")) + .range +} diff --git a/crates/jolt-verifier/tests/support/proof_claims.rs b/crates/jolt-verifier/tests/support/proof_claims.rs deleted file mode 100644 index 03ce58f954..0000000000 --- a/crates/jolt-verifier/tests/support/proof_claims.rs +++ /dev/null @@ -1,619 +0,0 @@ -//! Opening-claim projection for verifier-native prover proofs. -#[cfg(not(feature = "akita"))] -use jolt_claims::protocols::jolt::geometry::claim_reductions::advice; -#[cfg(not(feature = "akita"))] -use jolt_claims::protocols::jolt::geometry::claim_reductions::increments; -use jolt_claims::protocols::jolt::geometry::spartan::SpartanOuterDimensions; -use jolt_claims::protocols::jolt::{ - self as native, - geometry::{ - booleanity, bytecode, - claim_reductions::instruction as instruction_claim_reduction, - claim_reductions::registers as registers_claim_reduction, - claim_reductions::{ - bytecode as bytecode_reduction, program_image as program_image_reduction, - }, - instruction, ram, registers, spartan, - spartan::{outer_opening, outer_uniskip_opening, product_uniskip_opening}, - }, - JoltAdviceKind, JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, JoltVirtualPolynomial, -}; -use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; -use jolt_lookup_tables::{LookupTableKind, XLEN as RISCV_XLEN}; -use jolt_openings::CommitmentScheme; -use jolt_riscv::CircuitFlags; -use jolt_verifier::{ - proof::{ClearProofClaims, JoltProof, JoltProofClaims}, - stages::{ - stage1::outputs::Stage1BatchOutputClaims, stage2::outputs::Stage2BatchOutputClaims, - stage3::outputs::Stage3OutputClaims, stage4::Stage4OutputClaims, - stage5::Stage5OutputClaims, stage6a::outputs::Stage6aOutputClaims, - stage6b::outputs::Stage6bOutputClaims, stage7::outputs::Stage7OutputClaims, - }, -}; - -#[doc(hidden)] -pub fn attach_empty_opening_claims(proof: &mut JoltProof) -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - proof.claims = JoltProofClaims::Clear(crate::support::tamper_manifest::clear_claims(false)); -} - -#[doc(hidden)] -pub fn offset_opening_claim( - proof: &mut JoltProof, - id: native::JoltOpeningId, - delta: PCS::Field, -) -> bool -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - let Some(opening_claim) = claim_mut(proof, id) else { - return false; - }; - *opening_claim += delta; - true -} - -#[doc(hidden)] -pub fn opening_claim( - proof: &JoltProof, - id: native::JoltOpeningId, -) -> Option -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - claim(proof, id) -} - -fn claim( - proof: &JoltProof, - id: native::JoltOpeningId, -) -> Option -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - let JoltProofClaims::Clear(claims) = &proof.claims else { - return None; - }; - - claim_from_clear(claims, proof.trace_length, id) -} - -fn claim_mut( - proof: &mut JoltProof, - id: native::JoltOpeningId, -) -> Option<&mut PCS::Field> -where - PCS: CommitmentScheme, - VC: VectorCommitment, -{ - let JoltProofClaims::Clear(claims) = &mut proof.claims else { - return None; - }; - - claim_mut_from_clear(claims, proof.trace_length, id) -} - -fn claim_from_clear( - claims: &ClearProofClaims, - trace_length: usize, - id: native::JoltOpeningId, -) -> Option { - // The &mut chain is the single source of truth for the ID -> field mapping; - // the read path clones and projects through it to avoid duplicating it. - let mut copy = claims.clone(); - claim_mut_from_clear(&mut copy, trace_length, id).map(|value| *value) -} - -fn claim_mut_from_clear( - claims: &mut ClearProofClaims, - trace_length: usize, - id: native::JoltOpeningId, -) -> Option<&mut F> { - if id == outer_uniskip_opening() { - return Some(&mut claims.stage1.uniskip_output_claim); - } - if let Some(variable) = stage1_outer_variable(trace_length, id) { - return claim_mut_from_spartan_outer(&mut claims.stage1.outer, variable); - } - if id == product_uniskip_opening() { - return Some(&mut claims.stage2.product_uniskip_output_claim); - } - - claim_mut_from_stage2_batch_outputs(&mut claims.stage2.batch_outputs, id) - .or_else(|| claim_mut_from_stage3_outputs(&mut claims.stage3, id)) - .or_else(|| claim_mut_from_stage4_outputs(&mut claims.stage4, id)) - .or_else(|| claim_mut_from_stage5_outputs(&mut claims.stage5, id)) - .or_else(|| claim_mut_from_stage7_outputs(&mut claims.stage7, id)) - .or_else(|| claim_mut_from_stage6_outputs(&mut claims.stage6a, &mut claims.stage6b, id)) -} - -fn claim_mut_from_spartan_outer( - claims: &mut Stage1BatchOutputClaims, - variable: JoltVirtualPolynomial, -) -> Option<&mut F> { - let claims = &mut claims.outer_remainder; - match variable { - JoltVirtualPolynomial::LeftInstructionInput => Some(&mut claims.left_instruction_input), - JoltVirtualPolynomial::RightInstructionInput => Some(&mut claims.right_instruction_input), - JoltVirtualPolynomial::Product => Some(&mut claims.product), - JoltVirtualPolynomial::ShouldBranch => Some(&mut claims.should_branch), - JoltVirtualPolynomial::PC => Some(&mut claims.pc), - JoltVirtualPolynomial::UnexpandedPC => Some(&mut claims.unexpanded_pc), - JoltVirtualPolynomial::Imm => Some(&mut claims.imm), - JoltVirtualPolynomial::RamAddress => Some(&mut claims.ram_address), - JoltVirtualPolynomial::Rs1Value => Some(&mut claims.rs1_value), - JoltVirtualPolynomial::Rs2Value => Some(&mut claims.rs2_value), - JoltVirtualPolynomial::RdWriteValue => Some(&mut claims.rd_write_value), - JoltVirtualPolynomial::RamReadValue => Some(&mut claims.ram_read_value), - JoltVirtualPolynomial::RamWriteValue => Some(&mut claims.ram_write_value), - JoltVirtualPolynomial::LeftLookupOperand => Some(&mut claims.left_lookup_operand), - JoltVirtualPolynomial::RightLookupOperand => Some(&mut claims.right_lookup_operand), - JoltVirtualPolynomial::NextUnexpandedPC => Some(&mut claims.next_unexpanded_pc), - JoltVirtualPolynomial::NextPC => Some(&mut claims.next_pc), - JoltVirtualPolynomial::NextIsVirtual => Some(&mut claims.next_is_virtual), - JoltVirtualPolynomial::NextIsFirstInSequence => Some(&mut claims.next_is_first_in_sequence), - JoltVirtualPolynomial::LookupOutput => Some(&mut claims.lookup_output), - JoltVirtualPolynomial::ShouldJump => Some(&mut claims.should_jump), - JoltVirtualPolynomial::OpFlags(flag) => match flag { - CircuitFlags::AddOperands => Some(&mut claims.add_operands), - CircuitFlags::SubtractOperands => Some(&mut claims.subtract_operands), - CircuitFlags::MultiplyOperands => Some(&mut claims.multiply_operands), - CircuitFlags::Load => Some(&mut claims.load), - CircuitFlags::Store => Some(&mut claims.store), - CircuitFlags::Jump => Some(&mut claims.jump), - CircuitFlags::WriteLookupOutputToRD => Some(&mut claims.write_lookup_output_to_rd), - CircuitFlags::VirtualInstruction => Some(&mut claims.virtual_instruction), - CircuitFlags::Assert => Some(&mut claims.assert), - CircuitFlags::DoNotUpdateUnexpandedPC => Some(&mut claims.do_not_update_unexpanded_pc), - CircuitFlags::Advice => Some(&mut claims.advice), - CircuitFlags::IsCompressed => Some(&mut claims.is_compressed), - CircuitFlags::IsFirstInSequence => Some(&mut claims.is_first_in_sequence), - CircuitFlags::IsLastInSequence => Some(&mut claims.is_last_in_sequence), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldAdd => Some(&mut claims.field_add), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldSub => Some(&mut claims.field_sub), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldMul => Some(&mut claims.field_mul), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldInv => Some(&mut claims.field_inv), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldAssertEq => Some(&mut claims.field_assert_eq), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldLoadAccumulateFromRegister => { - Some(&mut claims.field_load_accumulate_from_register) - } - #[cfg(feature = "field-inline")] - CircuitFlags::FieldAssertZero => Some(&mut claims.field_assert_zero), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldLoadImm => Some(&mut claims.field_load_imm), - #[cfg(feature = "field-inline")] - CircuitFlags::FieldLoadAccumulateFromMemory => { - Some(&mut claims.field_load_accumulate_from_memory) - } - #[cfg(feature = "field-inline")] - CircuitFlags::FieldAdviceLimb => Some(&mut claims.field_advice_limb), - }, - _ => None, - } -} - -fn stage1_outer_variable( - trace_length: usize, - id: native::JoltOpeningId, -) -> Option { - let log_t = trace_length.ilog2() as usize; - SpartanOuterDimensions::rv64(log_t) - .variables() - .iter() - .copied() - .find(|variable| id == outer_opening(*variable)) -} - -fn claim_mut_from_stage2_batch_outputs( - claims: &mut Stage2BatchOutputClaims, - id: native::JoltOpeningId, -) -> Option<&mut F> { - let [ram_val, ram_ra, ram_inc] = [ram::ram_val(), ram::ram_ra(), ram::ram_inc()]; - let [product_left_instruction_input, product_right_instruction_input, product_jump_flag, product_write_lookup_output_to_rd, product_lookup_output, product_branch_flag, product_next_is_noop, product_virtual_instruction] = [ - spartan::left_instruction_input_product(), - spartan::right_instruction_input_product(), - spartan::jump_flag_product(), - spartan::write_lookup_output_to_rd_product(), - spartan::lookup_output_product(), - spartan::branch_flag_product(), - spartan::next_is_noop_product(), - spartan::virtual_instruction_product(), - ]; - let [instruction_lookup_output, instruction_left_lookup_operand, instruction_right_lookup_operand, instruction_left_instruction_input, instruction_right_instruction_input] = [ - instruction_claim_reduction::lookup_output_reduced(), - instruction_claim_reduction::left_lookup_operand_reduced(), - instruction_claim_reduction::right_lookup_operand_reduced(), - instruction_claim_reduction::left_instruction_input_reduced(), - instruction_claim_reduction::right_instruction_input_reduced(), - ]; - let ram_ra_raf_evaluation = ram::ram_ra_raf_evaluation(); - let ram_val_final = ram::ram_val_final(); - - match id { - id if id == ram_val => Some(&mut claims.ram_read_write.val), - id if id == ram_ra => Some(&mut claims.ram_read_write.ra), - id if id == ram_inc => Some(&mut claims.ram_read_write.inc), - id if id == product_left_instruction_input => { - Some(&mut claims.product_remainder.left_instruction_input) - } - id if id == product_right_instruction_input => { - Some(&mut claims.product_remainder.right_instruction_input) - } - id if id == product_jump_flag => Some(&mut claims.product_remainder.jump_flag), - id if id == product_write_lookup_output_to_rd => { - Some(&mut claims.product_remainder.write_lookup_output_to_rd) - } - id if id == product_lookup_output => Some(&mut claims.product_remainder.lookup_output), - id if id == product_branch_flag => Some(&mut claims.product_remainder.branch_flag), - id if id == product_next_is_noop => Some(&mut claims.product_remainder.next_is_noop), - id if id == product_virtual_instruction => { - Some(&mut claims.product_remainder.virtual_instruction) - } - id if id == instruction_lookup_output => { - Some(&mut claims.instruction_claim_reduction.lookup_output) - } - id if id == instruction_left_lookup_operand => { - Some(&mut claims.instruction_claim_reduction.left_lookup_operand) - } - id if id == instruction_right_lookup_operand => { - Some(&mut claims.instruction_claim_reduction.right_lookup_operand) - } - id if id == instruction_left_instruction_input => { - Some(&mut claims.instruction_claim_reduction.left_instruction_input) - } - id if id == instruction_right_instruction_input => { - Some(&mut claims.instruction_claim_reduction.right_instruction_input) - } - id if id == ram_ra_raf_evaluation => Some(&mut claims.ram_raf_evaluation.ram_ra), - id if id == ram_val_final => Some(&mut claims.ram_output_check.val_final), - _ => None, - } -} - -fn claim_mut_from_stage3_outputs( - claims: &mut Stage3OutputClaims, - id: native::JoltOpeningId, -) -> Option<&mut F> { - let [unexpanded_pc_shift, pc_shift, is_virtual_shift, is_first_in_sequence_shift, is_noop_shift] = [ - spartan::unexpanded_pc_shift(), - spartan::pc_shift(), - spartan::is_virtual_shift(), - spartan::is_first_in_sequence_shift(), - spartan::is_noop_shift(), - ]; - let [right_operand_is_rs2, rs2_value_input, right_operand_is_imm, imm_input, left_operand_is_rs1, rs1_value_input, left_operand_is_pc, unexpanded_pc_input] = [ - instruction::right_operand_is_rs2(), - instruction::rs2_value(), - instruction::right_operand_is_imm(), - instruction::imm(), - instruction::left_operand_is_rs1(), - instruction::rs1_value(), - instruction::left_operand_is_pc(), - instruction::unexpanded_pc(), - ]; - let [rd_write_value_reduced, rs1_value_reduced, rs2_value_reduced] = [ - registers_claim_reduction::rd_write_value_reduced(), - registers_claim_reduction::rs1_value_reduced(), - registers_claim_reduction::rs2_value_reduced(), - ]; - - match id { - id if id == unexpanded_pc_shift => Some(&mut claims.shift.unexpanded_pc), - id if id == pc_shift => Some(&mut claims.shift.pc), - id if id == is_virtual_shift => Some(&mut claims.shift.is_virtual), - id if id == is_first_in_sequence_shift => Some(&mut claims.shift.is_first_in_sequence), - id if id == is_noop_shift => Some(&mut claims.shift.is_noop), - id if id == left_operand_is_rs1 => Some(&mut claims.instruction_input.left_operand_is_rs1), - id if id == rs1_value_input => Some(&mut claims.instruction_input.rs1_value), - id if id == left_operand_is_pc => Some(&mut claims.instruction_input.left_operand_is_pc), - id if id == unexpanded_pc_input => Some(&mut claims.instruction_input.unexpanded_pc), - id if id == right_operand_is_rs2 => { - Some(&mut claims.instruction_input.right_operand_is_rs2) - } - id if id == rs2_value_input => Some(&mut claims.instruction_input.rs2_value), - id if id == right_operand_is_imm => { - Some(&mut claims.instruction_input.right_operand_is_imm) - } - id if id == imm_input => Some(&mut claims.instruction_input.imm), - id if id == rd_write_value_reduced => { - Some(&mut claims.registers_claim_reduction.rd_write_value) - } - id if id == rs1_value_reduced => Some(&mut claims.registers_claim_reduction.rs1_value), - id if id == rs2_value_reduced => Some(&mut claims.registers_claim_reduction.rs2_value), - _ => None, - } -} - -fn claim_mut_from_stage4_outputs( - claims: &mut Stage4OutputClaims, - id: native::JoltOpeningId, -) -> Option<&mut F> { - let [registers_val, rs1_ra, rs2_ra, rd_wa, rd_inc] = [ - registers::registers_val_read_write(), - registers::rs1_ra_read_write(), - registers::rs2_ra_read_write(), - registers::rd_wa_read_write(), - registers::rd_inc_read_write(), - ]; - let [ram_ra, ram_inc] = [ram::ram_ra_val_check(), ram::ram_inc_val_check()]; - - match id { - id if id == ram::val_check_advice_opening(JoltAdviceKind::Untrusted) => { - claims.ram_val_check.untrusted_advice.as_mut() - } - id if id == ram::val_check_advice_opening(JoltAdviceKind::Trusted) => { - claims.ram_val_check.trusted_advice.as_mut() - } - id if id == program_image_reduction::ram_val_check_contribution_opening() => { - claims.ram_val_check.program_image.as_mut() - } - id if id == registers_val => Some(&mut claims.registers_read_write.registers_val), - id if id == rs1_ra => Some(&mut claims.registers_read_write.rs1_ra), - id if id == rs2_ra => Some(&mut claims.registers_read_write.rs2_ra), - id if id == rd_wa => Some(&mut claims.registers_read_write.rd_wa), - id if id == rd_inc => Some(&mut claims.registers_read_write.rd_inc), - id if id == ram_ra => Some(&mut claims.ram_val_check.ram_ra), - id if id == ram_inc => Some(&mut claims.ram_val_check.ram_inc), - _ => None, - } -} - -fn claim_mut_from_stage5_outputs( - claims: &mut Stage5OutputClaims, - id: native::JoltOpeningId, -) -> Option<&mut F> { - for table in LookupTableKind::::iter() { - if id == instruction::lookup_table_flag(table) { - return claims - .instruction_read_raf - .lookup_table_flags - .get_mut(table.index()); - } - } - for (index, opening_claim) in claims - .instruction_read_raf - .instruction_ra - .iter_mut() - .enumerate() - { - if id == instruction::instruction_ra(index) { - return Some(opening_claim); - } - } - - let ram_ra = ram::ram_ra_claim_reduction(); - let [rd_inc, rd_wa] = [ - registers::rd_inc_val_evaluation(), - registers::rd_wa_val_evaluation(), - ]; - match id { - id if id == instruction::instruction_raf_flag() => { - Some(&mut claims.instruction_read_raf.instruction_raf_flag) - } - id if id == ram_ra => Some(&mut claims.ram_ra_claim_reduction.ram_ra), - id if id == rd_inc => Some(&mut claims.registers_val_evaluation.rd_inc), - id if id == rd_wa => Some(&mut claims.registers_val_evaluation.rd_wa), - _ => None, - } -} - -fn claim_mut_from_stage6_outputs<'a, F: JoltField>( - stage6a: &'a mut Stage6aOutputClaims, - stage6b: &'a mut Stage6bOutputClaims, - id: native::JoltOpeningId, -) -> Option<&'a mut F> { - if let Some(stage) = (0..stage6a.bytecode_read_raf.val_stages.len()) - .find(|&stage| id == bytecode_reduction::bytecode_val_stage_opening(stage)) - { - return stage6a.bytecode_read_raf.val_stages.get_mut(stage); - } - if let Some(reduction) = stage6b.bytecode_reduction.as_mut() { - if id == bytecode_reduction::cycle_phase_intermediate_opening() { - if let Some(intermediate) = reduction.intermediate.as_mut() { - return Some(intermediate); - } - } - // cycle-phase-only shapes emit the final chunk claims here - for (chunk, opening_claim) in reduction.chunks.iter_mut().enumerate() { - if id == bytecode_reduction::final_bytecode_chunk_opening(chunk) { - return Some(opening_claim); - } - } - } - if let Some(reduction) = stage6b.program_image_reduction.as_mut() { - if id == program_image_reduction::cycle_phase_program_image_opening() - || id == program_image_reduction::final_program_image_opening() - { - return Some(&mut reduction.program_image); - } - } - for (index, opening_claim) in stage6b.bytecode_read_raf.bytecode_ra.iter_mut().enumerate() { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::BytecodeRa(index), - JoltRelationId::BytecodeReadRaf, - ) - { - return Some(opening_claim); - } - } - for (index, opening_claim) in stage6b.booleanity.instruction_ra.iter_mut().enumerate() { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::InstructionRa(index), - JoltRelationId::Booleanity, - ) - { - return Some(opening_claim); - } - } - for (index, opening_claim) in stage6b.booleanity.bytecode_ra.iter_mut().enumerate() { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::BytecodeRa(index), - JoltRelationId::Booleanity, - ) - { - return Some(opening_claim); - } - } - for (index, opening_claim) in stage6b.booleanity.ram_ra.iter_mut().enumerate() { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::RamRa(index), - JoltRelationId::Booleanity, - ) - { - return Some(opening_claim); - } - } - if id == ram::ram_hamming_weight() { - return Some(&mut stage6b.ram_hamming_booleanity.ram_hamming_weight); - } - for (index, opening_claim) in stage6b.ram_ra_virtualization.ram_ra.iter_mut().enumerate() { - if id == ram::committed_ram_ra(index) { - return Some(opening_claim); - } - } - for (index, opening_claim) in stage6b - .instruction_ra_virtualization - .committed_instruction_ra - .iter_mut() - .enumerate() - { - if id == instruction::committed_instruction_ra(index) { - return Some(opening_claim); - } - } - #[cfg(not(feature = "akita"))] - let [ram_inc, rd_inc] = [increments::ram_inc_reduced(), increments::rd_inc_reduced()]; - match id { - id if id == bytecode::bytecode_read_raf_address_phase_opening() => { - Some(&mut stage6a.bytecode_read_raf.intermediate) - } - id if id == booleanity::booleanity_address_phase_opening() => { - Some(&mut stage6a.booleanity.intermediate) - } - #[cfg(not(feature = "akita"))] - id if id == ram_inc => Some(&mut stage6b.inc_claim_reduction.ram_inc), - #[cfg(not(feature = "akita"))] - id if id == rd_inc => Some(&mut stage6b.inc_claim_reduction.rd_inc), - #[cfg(not(feature = "akita"))] - id if id == advice::cycle_phase_advice_opening(JoltAdviceKind::Trusted) - || id == advice::final_advice_opening(JoltAdviceKind::Trusted) => - { - stage6b - .trusted_advice - .as_mut() - .map(|claim| &mut claim.trusted) - } - #[cfg(not(feature = "akita"))] - id if id == advice::cycle_phase_advice_opening(JoltAdviceKind::Untrusted) - || id == advice::final_advice_opening(JoltAdviceKind::Untrusted) => - { - stage6b - .untrusted_advice - .as_mut() - .map(|claim| &mut claim.untrusted) - } - _ => None, - } -} - -fn claim_mut_from_stage7_outputs( - claims: &mut Stage7OutputClaims, - id: native::JoltOpeningId, -) -> Option<&mut F> { - for (index, opening) in claims - .hamming_weight_claim_reduction - .instruction_ra - .iter_mut() - .enumerate() - { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::InstructionRa(index), - JoltRelationId::HammingWeightClaimReduction, - ) - { - return Some(opening); - } - } - for (index, opening) in claims - .hamming_weight_claim_reduction - .bytecode_ra - .iter_mut() - .enumerate() - { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::BytecodeRa(index), - JoltRelationId::HammingWeightClaimReduction, - ) - { - return Some(opening); - } - } - for (index, opening) in claims - .hamming_weight_claim_reduction - .ram_ra - .iter_mut() - .enumerate() - { - if id - == JoltOpeningId::committed( - JoltCommittedPolynomial::RamRa(index), - JoltRelationId::HammingWeightClaimReduction, - ) - { - return Some(opening); - } - } - - if let Some(address_phase) = claims.bytecode_address_phase.as_mut() { - for (chunk, opening) in address_phase.chunks.iter_mut().enumerate() { - if id == bytecode_reduction::final_bytecode_chunk_opening(chunk) { - return Some(opening); - } - } - } - if let Some(address_phase) = claims.program_image_address_phase.as_mut() { - if id == program_image_reduction::final_program_image_opening() { - return Some(&mut address_phase.program_image); - } - } - - #[cfg(not(feature = "akita"))] - { - match id { - id if id == advice::final_advice_opening(JoltAdviceKind::Trusted) => claims - .trusted_advice - .as_mut() - .map(|claims| &mut claims.trusted), - id if id == advice::final_advice_opening(JoltAdviceKind::Untrusted) => claims - .untrusted_advice - .as_mut() - .map(|claims| &mut claims.untrusted), - _ => None, - } - } - #[cfg(feature = "akita")] - { - let _ = id; - None - } -} diff --git a/crates/jolt-verifier/tests/support/tamper_manifest.rs b/crates/jolt-verifier/tests/support/tamper_manifest.rs deleted file mode 100644 index 39f56e56e1..0000000000 --- a/crates/jolt-verifier/tests/support/tamper_manifest.rs +++ /dev/null @@ -1,1906 +0,0 @@ -use std::collections::BTreeSet; - -#[cfg(feature = "field-inline")] -use jolt_claims::protocols::field_inline::FieldInlineRelationId; -use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_field::{Fr, JoltField}; -#[cfg(feature = "field-inline")] -use jolt_verifier::stages::stage2::outputs::FieldRegistersClaimReductionOutputClaims; -#[cfg(feature = "field-inline")] -use jolt_verifier::stages::stage4::FieldRegistersReadWriteOutputClaims; -#[cfg(feature = "field-inline")] -use jolt_verifier::stages::stage5::FieldRegistersValEvaluationOutputClaims; -#[cfg(feature = "field-inline")] -use jolt_verifier::stages::stage6b::outputs::FieldRegistersIncClaimReductionOutputClaims; -use jolt_verifier::{ - proof::ClearProofClaims, - stages::stage1::outputs::{Stage1BatchOutputClaims, Stage1OutputClaims}, - stages::stage1::OuterRemainderOutputClaims, - stages::stage2::outputs::{Stage2BatchOutputClaims, Stage2OutputClaims}, - stages::{stage2, stage3, stage4, stage5, stage6a, stage6b, stage7}, - VerifierError, -}; -use serde_json::Value; - -use crate::support::VerifierPhase; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TamperMode { - Standard, - Zk, - Both, -} - -impl TamperMode { - pub fn includes(self, zk: bool) -> bool { - matches!( - (self, zk), - (Self::Both, _) | (Self::Standard, false) | (Self::Zk, true) - ) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[expect( - clippy::enum_variant_names, - reason = "the shared `Checked` prefix records where a tampered field is caught; dropping it would obscure the disposition semantics" -)] -pub enum TamperDisposition { - CheckedAtStage, - CheckedByLaterStage(VerifierPhase), - CheckedByFinalOpenings, - CheckedByZk, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MutationStrategy { - OffsetScalar, - FlipBool, - ChangeEnumVariant, - RemoveItem, - DuplicateItem, - SwapOrder, - TruncateVector, - ExtendVector, - ReplaceProofPayload, - ReplaceModePayload, - ReplaceSetup, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TamperCoverage { - Active, - IgnoredUntilFixture, - Deferred, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct TamperTarget { - pub name: &'static str, - pub path: &'static str, - pub mode: TamperMode, - pub checked_at: VerifierPhase, - pub disposition: TamperDisposition, - pub strategy: MutationStrategy, - pub coverage: TamperCoverage, - pub reason: &'static str, -} - -const fn checked_standard( - name: &'static str, - path: &'static str, - checked_at: VerifierPhase, - strategy: MutationStrategy, - coverage: TamperCoverage, - reason: &'static str, -) -> TamperTarget { - TamperTarget { - name, - path, - mode: TamperMode::Standard, - checked_at, - disposition: TamperDisposition::CheckedAtStage, - strategy, - coverage, - reason, - } -} - -const fn checked_both( - name: &'static str, - path: &'static str, - checked_at: VerifierPhase, - strategy: MutationStrategy, - coverage: TamperCoverage, - reason: &'static str, -) -> TamperTarget { - TamperTarget { - name, - path, - mode: TamperMode::Both, - checked_at, - disposition: TamperDisposition::CheckedAtStage, - strategy, - coverage, - reason, - } -} - -const fn later_standard( - name: &'static str, - path: &'static str, - later_phase: VerifierPhase, - strategy: MutationStrategy, - coverage: TamperCoverage, - reason: &'static str, -) -> TamperTarget { - TamperTarget { - name, - path, - mode: TamperMode::Standard, - checked_at: later_phase, - disposition: TamperDisposition::CheckedByLaterStage(later_phase), - strategy, - coverage, - reason, - } -} - -const fn final_opening_standard( - name: &'static str, - path: &'static str, - strategy: MutationStrategy, - coverage: TamperCoverage, - reason: &'static str, -) -> TamperTarget { - TamperTarget { - name, - path, - mode: TamperMode::Standard, - checked_at: VerifierPhase::Stage8Openings, - disposition: TamperDisposition::CheckedByFinalOpenings, - strategy, - coverage, - reason, - } -} - -const fn final_opening_zk( - name: &'static str, - path: &'static str, - strategy: MutationStrategy, - coverage: TamperCoverage, - reason: &'static str, -) -> TamperTarget { - TamperTarget { - name, - path, - mode: TamperMode::Zk, - checked_at: VerifierPhase::Stage8Openings, - disposition: TamperDisposition::CheckedByFinalOpenings, - strategy, - coverage, - reason, - } -} - -const fn zk_target( - name: &'static str, - path: &'static str, - strategy: MutationStrategy, - coverage: TamperCoverage, - reason: &'static str, -) -> TamperTarget { - TamperTarget { - name, - path, - mode: TamperMode::Zk, - checked_at: VerifierPhase::Zk, - disposition: TamperDisposition::CheckedByZk, - strategy, - coverage, - reason, - } -} - -pub const PREAMBLE_TARGETS: &[TamperTarget] = &[ - checked_standard( - "public_io.inputs", - "public_io.inputs", - VerifierPhase::Stage1, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "absorbed into the transcript; the first concrete rejection is a stage sumcheck mismatch", - ), - checked_standard( - "public_io.outputs", - "public_io.outputs", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "real fixture output mutation; rejected no later than the RAM output check", - ), - checked_standard( - "public_io.panic", - "public_io.panic", - VerifierPhase::Stage2, - MutationStrategy::FlipBool, - TamperCoverage::Active, - "panic materializes as public memory for the RAM output check; real fixture bool flip", - ), - checked_both( - "public_io.memory_layout", - "public_io.memory_layout", - VerifierPhase::Preamble, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "validate_inputs rejects layout mismatch before transcript use", - ), - checked_standard( - "proof.trace_length", - "proof.trace_length", - VerifierPhase::Preamble, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "invalid and excessive trace lengths are checked during input validation", - ), - checked_standard( - "proof.ram_K", - "proof.ram_K", - VerifierPhase::Preamble, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "invalid RAM domains are checked during input validation", - ), - checked_standard( - "proof.rw_config", - "proof.rw_config", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "stage 2 consumes RAM read-write phase lengths when slicing batched points", - ), - later_standard( - "proof.one_hot_config", - "proof.one_hot_config", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "transcript-bound and substantively checked by later RA virtualization stages; real fixture config offset", - ), - checked_standard( - "proof.trace_polynomial_order", - "proof.trace_polynomial_order", - VerifierPhase::Stage1, - MutationStrategy::ChangeEnumVariant, - TamperCoverage::Active, - "transcript-bound; real fixture enum flip diverges challenges at the first stage sumcheck", - ), - checked_standard( - "preprocessing.preprocessing_digest", - "preprocessing.preprocessing_digest", - VerifierPhase::Stage1, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "transcript-bound; real fixture digest offset diverges challenges at the first stage sumcheck", - ), - checked_standard( - "preprocessing.program.bytecode.entry_address", - "preprocessing.program.bytecode.entry_address", - VerifierPhase::Stage1, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "entry address is transcript-bound; preamble.rs offsets the full program's entry address", - ), -]; - -pub const COMMITMENT_TARGETS: &[TamperTarget] = &[ - checked_standard( - "proof.commitments.value", - "proof.commitments[*]", - VerifierPhase::Stage1, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "real verifier fixture replaces one commitment with another and rejects before acceptance", - ), - checked_standard( - "proof.commitments.missing", - "proof.commitments", - VerifierPhase::Stage1, - MutationStrategy::RemoveItem, - TamperCoverage::Active, - "real verifier fixture removes one commitment and rejects before acceptance", - ), - checked_standard( - "proof.commitments.extra", - "proof.commitments", - VerifierPhase::Stage1, - MutationStrategy::DuplicateItem, - TamperCoverage::Active, - "real verifier fixture duplicates one commitment and rejects before acceptance", - ), - checked_standard( - "proof.commitments.order", - "proof.commitments", - VerifierPhase::Stage1, - MutationStrategy::SwapOrder, - TamperCoverage::Active, - "real verifier fixture swaps commitments and rejects before acceptance", - ), - checked_standard( - "proof.untrusted_advice_commitment", - "proof.untrusted_advice_commitment", - VerifierPhase::Stage1, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "advice fixture replaces the untrusted advice commitment with a valid but wrong commitment", - ), - checked_standard( - "trusted_advice_commitment", - "trusted_advice_commitment", - VerifierPhase::Stage1, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "advice fixture replaces the trusted advice commitment with a valid but wrong commitment", - ), - final_opening_standard( - "proof.joint_opening_proof", - "proof.joint_opening_proof", - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "real verifier fixture replaces the joint opening proof with another valid proof payload", - ), -]; - -pub const PROOF_SHAPE_TARGETS: &[TamperTarget] = &[ - checked_both( - "proof.stages.clear_vs_committed", - "proof.stages.*", - VerifierPhase::Preamble, - MutationStrategy::ReplaceModePayload, - TamperCoverage::Active, - "validate_proof_consistency rejects mixed clear/committed mode", - ), - checked_both( - "proof.claims.mode_payload", - "proof.claims", - VerifierPhase::Preamble, - MutationStrategy::ReplaceModePayload, - TamperCoverage::Active, - "validate_proof_consistency rejects clear/ZK payload mismatches", - ), -]; - -pub const STAGE1_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage1.uni_skip.round_polynomial", - "proof.stages.stage1_uni_skip_first_round_proof.round_polynomials[*]", - VerifierPhase::Stage1, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every full uni-skip round polynomial", - ), - checked_standard( - "stage1.uni_skip.round_count.missing", - "proof.stages.stage1_uni_skip_first_round_proof.round_polynomials", - VerifierPhase::Stage1, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a uni-skip round", - ), - checked_standard( - "stage1.uni_skip.round_count.extra", - "proof.stages.stage1_uni_skip_first_round_proof.round_polynomials", - VerifierPhase::Stage1, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a uni-skip round", - ), - checked_standard( - "stage1.remainder.round_polynomial", - "proof.stages.stage1_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage1, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed remainder round polynomial", - ), - checked_standard( - "stage1.remainder.round_count.missing", - "proof.stages.stage1_sumcheck_proof.round_polynomials", - VerifierPhase::Stage1, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a remainder round", - ), - checked_standard( - "stage1.remainder.round_count.extra", - "proof.stages.stage1_sumcheck_proof.round_polynomials", - VerifierPhase::Stage1, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a remainder round", - ), - checked_standard( - "stage1.claims.uniskip_output_claim", - "claims.stage1.uniskip_output_claim", - VerifierPhase::Stage1, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the Spartan outer uni-skip opening claim", - ), - later_standard( - "stage1.claims.outer", - "claims.stage1.outer.*", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "outer output claims are consumed as stage-2 input claims (product \ - virtualization); the PCS binds them again at final openings", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "stage1.claims.field_inline_outer", - "claims.stage1.outer.outer_remainder.field_inline.*", - VerifierPhase::Stage1, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "field-inline fixture test offsets every field-inline Spartan outer opening claim", - ), -]; - -pub const STAGE2_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage2.product_uniskip.round_polynomial", - "proof.stages.stage2_uni_skip_first_round_proof.round_polynomials[*]", - VerifierPhase::Stage2, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every product uni-skip round polynomial", - ), - checked_standard( - "stage2.product_uniskip.round_count.missing", - "proof.stages.stage2_uni_skip_first_round_proof.round_polynomials", - VerifierPhase::Stage2, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a product uni-skip round", - ), - checked_standard( - "stage2.product_uniskip.round_count.extra", - "proof.stages.stage2_uni_skip_first_round_proof.round_polynomials", - VerifierPhase::Stage2, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a product uni-skip round", - ), - checked_standard( - "stage2.batch.round_polynomial", - "proof.stages.stage2_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage2, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 2 batch round polynomial", - ), - checked_standard( - "stage2.batch.round_count.missing", - "proof.stages.stage2_sumcheck_proof.round_polynomials", - VerifierPhase::Stage2, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 2 batch round", - ), - checked_standard( - "stage2.batch.round_count.extra", - "proof.stages.stage2_sumcheck_proof.round_polynomials", - VerifierPhase::Stage2, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 2 batch round", - ), - checked_standard( - "stage2.claims.product_uniskip_output_claim", - "claims.stage2.product_uniskip_output_claim", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the product uni-skip opening claim", - ), - checked_standard( - "stage2.claims.batch_outputs.ram_read_write", - "claims.stage2.batch_outputs.ram_read_write.*", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each RAM read-write output claim", - ), - checked_standard( - "stage2.claims.batch_outputs.product_remainder.checked", - "claims.stage2.batch_outputs.product_remainder.{left_instruction_input,right_instruction_input,jump_flag,lookup_output,branch_flag,next_is_noop}", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every product remainder output used by the Stage 2 formula", - ), - checked_standard( - "stage2.claims.batch_outputs.product_remainder.write_lookup_output_to_rd", - "claims.stage2.batch_outputs.product_remainder.write_lookup_output_to_rd", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "Stage 6 bytecode read-RAF consumes this Stage 2 pass-through claim", - ), - checked_standard( - "stage2.claims.batch_outputs.product_remainder.virtual_instruction", - "claims.stage2.batch_outputs.product_remainder.virtual_instruction", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "Stage 6 bytecode read-RAF consumes this Stage 2 pass-through claim", - ), - checked_standard( - "stage2.claims.batch_outputs.instruction_claim_reduction", - "claims.stage2.batch_outputs.instruction_claim_reduction.*", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each instruction claim-reduction output (aliased cells are rejected by the generated validate_aliases)", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "stage2.claims.batch_outputs.field_registers_claim_reduction", - "claims.stage2.batch_outputs.field_registers_claim_reduction.*", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "field-inline fixture test offsets each field-inline claim-reduction output (the stage-2 fold \ - and the generated alias check consume them)", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "stage2.claims.field_inline_product", - "claims.stage2.batch_outputs.product_remainder.field_inline.*", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "field-inline fixture test offsets each composed field-inline product opening (the remainder and generated alias check consume them)", - ), - checked_standard( - "stage2.claims.batch_outputs.ram_raf_evaluation", - "claims.stage2.batch_outputs.ram_raf_evaluation.ram_ra", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the RAM RAF evaluation output claim", - ), - checked_standard( - "stage2.claims.batch_outputs.ram_output_check", - "claims.stage2.batch_outputs.ram_output_check.val_final", - VerifierPhase::Stage2, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the RAM output-check claim", - ), -]; - -pub const STAGE3_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage3.batch.round_polynomial", - "proof.stages.stage3_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage3, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 3 batch round polynomial", - ), - checked_standard( - "stage3.batch.round_count.missing", - "proof.stages.stage3_sumcheck_proof.round_polynomials", - VerifierPhase::Stage3, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 3 batch round", - ), - checked_standard( - "stage3.batch.round_count.extra", - "proof.stages.stage3_sumcheck_proof.round_polynomials", - VerifierPhase::Stage3, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 3 batch round", - ), - checked_standard( - "stage3.claims.shift", - "claims.stage3.shift.*", - VerifierPhase::Stage3, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each Spartan shift output claim", - ), - checked_standard( - "stage3.claims.instruction_input", - "claims.stage3.instruction_input.*", - VerifierPhase::Stage3, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each instruction-input output claim", - ), - checked_standard( - "stage3.claims.registers_claim_reduction", - "claims.stage3.registers_claim_reduction.*", - VerifierPhase::Stage3, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each register claim-reduction output claim", - ), -]; - -pub const STAGE4_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage4.batch.round_polynomial", - "proof.stages.stage4_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage4, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 4 batch round polynomial", - ), - checked_standard( - "stage4.batch.round_count.missing", - "proof.stages.stage4_sumcheck_proof.round_polynomials", - VerifierPhase::Stage4, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 4 batch round", - ), - checked_standard( - "stage4.batch.round_count.extra", - "proof.stages.stage4_sumcheck_proof.round_polynomials", - VerifierPhase::Stage4, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 4 batch round", - ), - checked_standard( - "stage4.claims.registers_read_write", - "claims.stage4.registers_read_write.*", - VerifierPhase::Stage4, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each register read-write output claim", - ), - checked_standard( - "stage4.claims.ram_val_check", - "claims.stage4.ram_val_check.*", - VerifierPhase::Stage4, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each RAM value-check output claim", - ), - checked_standard( - "stage4.claims.advice.untrusted", - "claims.stage4.ram_val_check.untrusted_advice", - VerifierPhase::Stage4, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "advice fixture test offsets the untrusted advice opening consumed by RAM value check", - ), - checked_standard( - "stage4.claims.advice.trusted", - "claims.stage4.ram_val_check.trusted_advice", - VerifierPhase::Stage4, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "advice fixture test offsets the trusted advice opening consumed by RAM value check", - ), - checked_standard( - "stage4.claims.program_image_contribution", - "claims.stage4.ram_val_check.program_image", - VerifierPhase::Stage4, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets the staged program-image init contribution", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "stage4.claims.field_registers_read_write", - "claims.stage4.field_registers_read_write.*", - VerifierPhase::Stage4, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "field-inline fixture test offsets each field-register read-write output (the stage-4 fold and \ - the stage-5 field-register value-evaluation input consume them)", - ), -]; - -pub const STAGE5_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage5.batch.round_polynomial", - "proof.stages.stage5_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage5, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 5 batch round polynomial", - ), - checked_standard( - "stage5.batch.round_count.missing", - "proof.stages.stage5_sumcheck_proof.round_polynomials", - VerifierPhase::Stage5, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 5 batch round", - ), - checked_standard( - "stage5.batch.round_count.extra", - "proof.stages.stage5_sumcheck_proof.round_polynomials", - VerifierPhase::Stage5, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 5 batch round", - ), - checked_standard( - "stage5.claims.instruction_read_raf.lookup_table_flags", - "claims.stage5.instruction_read_raf.lookup_table_flags", - VerifierPhase::Stage5, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each instruction read-RAF lookup-table flag claim", - ), - checked_standard( - "stage5.claims.instruction_read_raf.instruction_ra", - "claims.stage5.instruction_read_raf.instruction_ra", - VerifierPhase::Stage5, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each instruction read-RAF virtual RA claim", - ), - checked_standard( - "stage5.claims.instruction_read_raf.instruction_raf_flag", - "claims.stage5.instruction_read_raf.instruction_raf_flag", - VerifierPhase::Stage5, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the instruction read-RAF flag claim", - ), - checked_standard( - "stage5.claims.ram_ra_claim_reduction", - "claims.stage5.ram_ra_claim_reduction.ram_ra", - VerifierPhase::Stage5, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the RAM RA claim-reduction output claim", - ), - checked_standard( - "stage5.claims.registers_val_evaluation", - "claims.stage5.registers_val_evaluation.*", - VerifierPhase::Stage5, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets each register value-evaluation output claim", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "stage5.claims.field_registers_val_evaluation", - "claims.stage5.field_registers_val_evaluation.*", - VerifierPhase::Stage5, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "field-inline fixture test offsets each field-register value-evaluation output (the stage-5 fold \ - and the stage-6 FieldRdInc reduction consume them)", - ), -]; - -pub const STAGE6_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage6.address_phase.round_polynomial", - "proof.stages.stage6a_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage6, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 6 address-phase round polynomial", - ), - checked_standard( - "stage6.address_phase.round_count.missing", - "proof.stages.stage6a_sumcheck_proof.round_polynomials", - VerifierPhase::Stage6, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 6 address-phase round", - ), - checked_standard( - "stage6.address_phase.round_count.extra", - "proof.stages.stage6a_sumcheck_proof.round_polynomials", - VerifierPhase::Stage6, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 6 address-phase round", - ), - checked_standard( - "stage6.cycle_phase.round_polynomial", - "proof.stages.stage6b_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage6, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 6 cycle-phase round polynomial", - ), - checked_standard( - "stage6.cycle_phase.round_count.missing", - "proof.stages.stage6b_sumcheck_proof.round_polynomials", - VerifierPhase::Stage6, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 6 cycle-phase round", - ), - checked_standard( - "stage6.cycle_phase.round_count.extra", - "proof.stages.stage6b_sumcheck_proof.round_polynomials", - VerifierPhase::Stage6, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 6 cycle-phase round", - ), - checked_standard( - "stage6.claims.address_phase.bytecode_read_raf", - "claims.stage6a.bytecode_read_raf.intermediate", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the Stage 6 bytecode read-RAF address-phase output claim", - ), - checked_standard( - "stage6.claims.address_phase.booleanity", - "claims.stage6a.booleanity.intermediate", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the Stage 6 Booleanity address-phase output claim", - ), - checked_standard( - "stage6.claims.bytecode_read_raf.bytecode_ra", - "claims.stage6b.bytecode_read_raf.bytecode_ra", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every bytecode read-RAF output claim", - ), - checked_standard( - "stage6.claims.booleanity.instruction_ra", - "claims.stage6b.booleanity.instruction_ra", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every Booleanity instruction RA output claim", - ), - checked_standard( - "stage6.claims.booleanity.bytecode_ra", - "claims.stage6b.booleanity.bytecode_ra", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every Booleanity bytecode RA output claim", - ), - checked_standard( - "stage6.claims.booleanity.ram_ra", - "claims.stage6b.booleanity.ram_ra", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every Booleanity RAM RA output claim", - ), - checked_standard( - "stage6.claims.ram_hamming_booleanity.ram_hamming_weight", - "claims.stage6b.ram_hamming_booleanity.ram_hamming_weight", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the RAM hamming Booleanity output claim", - ), - checked_standard( - "stage6.claims.ram_ra_virtualization.ram_ra", - "claims.stage6b.ram_ra_virtualization.ram_ra", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every RAM RA virtualization output claim", - ), - checked_standard( - "stage6.claims.instruction_ra_virtualization.committed_instruction_ra", - "claims.stage6b.instruction_ra_virtualization.committed_instruction_ra", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every instruction RA virtualization output claim", - ), - checked_standard( - "stage6.claims.inc_claim_reduction.ram_inc", - "claims.stage6b.inc_claim_reduction.ram_inc", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the RAM increment reduction output claim", - ), - checked_standard( - "stage6.claims.inc_claim_reduction.rd_inc", - "claims.stage6b.inc_claim_reduction.rd_inc", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets the register increment reduction output claim", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "stage6.claims.field_registers_inc_claim_reduction.rd_inc", - "claims.stage6b.field_registers_inc_claim_reduction.rd_inc", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "field-inline fixture test offsets the reduced FieldRdInc claim (the anchor of the \ - stage-8 joint opening's FieldRdInc entry)", - ), - #[cfg(not(feature = "akita"))] - checked_standard( - "stage6.claims.trusted_advice.trusted", - "claims.stage6b.trusted_advice.trusted", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "advice fixture test offsets the trusted advice cycle-phase output claim", - ), - #[cfg(not(feature = "akita"))] - checked_standard( - "stage6.claims.untrusted_advice.untrusted", - "claims.stage6b.untrusted_advice.untrusted", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "advice fixture test offsets the untrusted advice cycle-phase output claim", - ), - checked_standard( - "stage6.claims.address_phase.bytecode_val_stages", - "claims.stage6a.bytecode_read_raf.val_stages", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets each staged bytecode Val-stage claim", - ), - checked_standard( - "stage6.claims.bytecode_reduction.intermediate", - "claims.stage6b.bytecode_reduction.intermediate", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets the bytecode reduction cycle-phase intermediate output claim", - ), - checked_standard( - "stage6.claims.bytecode_reduction.chunks", - "claims.stage6b.bytecode_reduction.chunks", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::IgnoredUntilFixture, - "cycle-phase-only committed shapes emit final chunks at stage 6, but every current committed fixture has an address phase; needs a cycle-phase-only fixture", - ), - checked_standard( - "stage6.claims.program_image_reduction.program_image", - "claims.stage6b.program_image_reduction.program_image", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets the program-image reduction cycle-phase output claim", - ), -]; - -pub const STAGE7_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage7.batch.round_polynomial", - "proof.stages.stage7_sumcheck_proof.round_polynomials[*]", - VerifierPhase::Stage7, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "prover-fixture test mutates every compressed Stage 7 batch round polynomial", - ), - checked_standard( - "stage7.batch.round_count.missing", - "proof.stages.stage7_sumcheck_proof.round_polynomials", - VerifierPhase::Stage7, - MutationStrategy::TruncateVector, - TamperCoverage::Active, - "prover-fixture test removes a Stage 7 batch round", - ), - checked_standard( - "stage7.batch.round_count.extra", - "proof.stages.stage7_sumcheck_proof.round_polynomials", - VerifierPhase::Stage7, - MutationStrategy::ExtendVector, - TamperCoverage::Active, - "prover-fixture test appends a Stage 7 batch round", - ), - checked_standard( - "stage7.claims.hamming_weight_claim_reduction.instruction_ra", - "claims.stage7.hamming_weight_claim_reduction.instruction_ra", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every HammingWeight instruction RA output claim", - ), - checked_standard( - "stage7.claims.hamming_weight_claim_reduction.bytecode_ra", - "claims.stage7.hamming_weight_claim_reduction.bytecode_ra", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every HammingWeight bytecode RA output claim", - ), - checked_standard( - "stage7.claims.hamming_weight_claim_reduction.ram_ra", - "claims.stage7.hamming_weight_claim_reduction.ram_ra", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "prover-fixture test offsets every HammingWeight RAM RA output claim", - ), - #[cfg(not(feature = "akita"))] - checked_standard( - "stage7.claims.trusted_advice.trusted", - "claims.stage7.trusted_advice.trusted", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "advice fixture test offsets the trusted advice address-phase output claim", - ), - #[cfg(not(feature = "akita"))] - checked_standard( - "stage7.claims.untrusted_advice.untrusted", - "claims.stage7.untrusted_advice.untrusted", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "advice fixture test offsets the untrusted advice address-phase output claim", - ), - checked_standard( - "stage7.claims.bytecode_address_phase.chunks", - "claims.stage7.bytecode_address_phase.chunks", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets each final bytecode chunk claim", - ), - checked_standard( - "stage7.claims.program_image_address_phase", - "claims.stage7.program_image_address_phase.program_image", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets the final program-image claim", - ), -]; - -pub const FUTURE_STAGE_TARGETS: &[TamperTarget] = &[ - final_opening_standard( - "stage8.opening_claim_values", - "stage8.opening_claim_values", - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "real verifier fixture offsets final opening claim fields ", - ), - final_opening_standard( - "stage8.opening_claim_points", - "stage8.opening_claim_points", - MutationStrategy::OffsetScalar, - TamperCoverage::Deferred, - "final opening points are transcript-derived; stage sumcheck payload tampering covers point changes", - ), - final_opening_zk( - "zk.joint_opening_proof.eval_commitment", - "proof.joint_opening_proof.y_com", - MutationStrategy::RemoveItem, - TamperCoverage::Active, - "Stage 8 ZK opening verification rejects a missing Dory evaluation commitment", - ), - zk_target( - "zk.blindfold_proof", - "proof.claims.Zk.blindfold_proof", - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "verifier-native ZK fixture mutates the imported BlindFold proof payload ", - ), - zk_target( - "zk.vector_commitment_setup", - "preprocessing.vc_setup", - MutationStrategy::ReplaceSetup, - TamperCoverage::Active, - "validate_inputs rejects missing VC setup in ZK mode", - ), -]; - -/// The Akita-path claim cells: the read-raf fused-inc opening, lattice -/// Booleanity, and the fused Stage-7 Hamming reduction. All active: the fixture-driven sweep in -/// `soundness/tampering/akita.rs` (`every_clear_claim_wire_rejects_offset`) -/// offsets every clear-claim scalar of the real packed-prover fixtures and -/// asserts each offset rejects. -#[cfg(feature = "akita")] -pub const AKITA_TARGETS: &[TamperTarget] = &[ - checked_standard( - "stage6.claims.bytecode_read_raf.fused_inc", - "claims.stage6b.bytecode_read_raf.fused_inc", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "the lattice read-raf cycle output fold rejects an offset fused-inc opening", - ), - checked_standard( - "stage6.claims.booleanity.balanced_inc_digits", - "claims.stage6b.booleanity.balanced_inc_digits", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "the lattice booleanity output fold covers every chunk cell", - ), - checked_standard( - "stage6.claims.booleanity.balanced_inc_carry", - "claims.stage6b.booleanity.balanced_inc_carry", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "the lattice booleanity output fold covers the carry cell", - ), - checked_standard( - "stage7.claims.hamming_weight_claim_reduction.balanced_inc_digits", - "claims.stage7.hamming_weight_claim_reduction.balanced_inc_digits", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "the hamming-weight reduction final-claim fold covers every increment chunk", - ), - checked_standard( - "stage7.claims.hamming_weight_claim_reduction.balanced_inc_carry", - "claims.stage7.hamming_weight_claim_reduction.balanced_inc_carry", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "the hamming-weight reduction final-claim fold covers the increment carry", - ), - #[cfg(feature = "field-inline")] - checked_standard( - "proof.field_inc_commitment", - "proof.field_inc_commitment", - VerifierPhase::Stage8Openings, - MutationStrategy::ReplaceProofPayload, - TamperCoverage::Active, - "the packed field-inline e2e flips the field increment commitment's layout-digest byte, mutates the batch \ - proof, and strips the group; each rejects", - ), -]; - -pub fn all_targets() -> Vec { - let targets = PREAMBLE_TARGETS - .iter() - .chain(COMMITMENT_TARGETS) - .chain(PROOF_SHAPE_TARGETS) - .chain(STAGE1_TARGETS) - .chain(STAGE2_TARGETS) - .chain(STAGE3_TARGETS) - .chain(STAGE4_TARGETS) - .chain(STAGE5_TARGETS) - .chain(STAGE6_TARGETS) - .chain(STAGE7_TARGETS) - .chain(FUTURE_STAGE_TARGETS); - #[cfg(feature = "akita")] - let targets = targets.chain(AKITA_TARGETS); - targets.copied().collect() -} - -#[expect( - clippy::panic, - reason = "tamper tests should fail loudly when they reference a missing manifest entry" -)] -pub fn required_target(name: &str) -> TamperTarget { - all_targets() - .into_iter() - .find(|target| target.name == name) - .unwrap_or_else(|| panic!("missing tamper manifest target {name}")) -} - -pub fn target_names_are_unique() -> bool { - let mut names = BTreeSet::new(); - all_targets() - .into_iter() - .all(|target| names.insert(target.name)) -} - -pub fn manifest_paths() -> BTreeSet { - all_targets() - .into_iter() - .flat_map(expand_manifest_path) - .map(|path| { - let path = path.to_owned(); - #[cfg(feature = "field-inline")] - for prefix in [ - "claims.stage1.outer.outer_remainder.", - "claims.stage2.batch_outputs.product_remainder.", - "claims.stage6a.bytecode_read_raf.", - ] { - if let Some(field) = path.strip_prefix(prefix) { - if !field.starts_with("field_inline.") { - return format!("{prefix}base.{field}"); - } - } - } - path - }) - .collect() -} - -#[expect( - clippy::expect_used, - reason = "manifest structural tests should fail loudly if claim serialization breaks" -)] -pub fn clear_claim_leaf_paths() -> BTreeSet { - let claims = clear_claims::(true); - let value = serde_json::to_value(claims).expect("clear claims should serialize"); - let mut paths = BTreeSet::new(); - collect_leaf_paths("claims", &value, &mut paths); - paths -} - -pub fn proof_field_paths() -> &'static [&'static str] { - &[ - "proof.commitments[*]", - "proof.joint_opening_proof", - "proof.untrusted_advice_commitment", - #[cfg(all(feature = "akita", feature = "field-inline"))] - "proof.field_inc_commitment", - "proof.claims", - "proof.trace_length", - "proof.ram_K", - "proof.rw_config", - "proof.one_hot_config", - "proof.trace_polynomial_order", - "proof.stages.stage1_uni_skip_first_round_proof.round_polynomials[*]", - "proof.stages.stage1_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage2_uni_skip_first_round_proof.round_polynomials[*]", - "proof.stages.stage2_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage3_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage4_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage5_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage6a_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage6b_sumcheck_proof.round_polynomials[*]", - "proof.stages.stage7_sumcheck_proof.round_polynomials[*]", - ] -} - -pub fn verifier_owned_targets_without_active_coverage() -> Vec { - all_targets() - .into_iter() - .filter(|target| { - target.disposition == TamperDisposition::CheckedAtStage - && target.coverage != TamperCoverage::Active - }) - .collect() -} - -pub fn assert_manifest_target_is_active(target: TamperTarget) { - assert_eq!( - target.coverage, - TamperCoverage::Active, - "tamper target is not active: {target:?}" - ); -} - -#[cfg(all(feature = "prover-fixtures", feature = "zk"))] -pub fn assert_zk_target_active(name: &str) { - let target = required_target(name); - assert_manifest_target_is_active(target); - assert!( - target.mode.includes(true), - "tamper target mode does not include ZK: {target:?}" - ); -} - -/// The phase where a target's rejection is documented to fire, derived from -/// its disposition. -pub fn expected_rejection_phase(target: TamperTarget) -> VerifierPhase { - match target.disposition { - TamperDisposition::CheckedAtStage => target.checked_at, - TamperDisposition::CheckedByLaterStage(phase) => phase, - TamperDisposition::CheckedByFinalOpenings => VerifierPhase::Stage8Openings, - TamperDisposition::CheckedByZk => VerifierPhase::Zk, - } -} - -/// Which stage's batched sumcheck verifies each relation. Folds 6a/6b into -/// `Stage6`. -fn relation_phase(id: JoltRelationId) -> VerifierPhase { - match id { - JoltRelationId::SpartanOuter => VerifierPhase::Stage1, - JoltRelationId::SpartanProductVirtualization - | JoltRelationId::RamReadWriteChecking - | JoltRelationId::RamRafEvaluation - | JoltRelationId::RamOutputCheck - | JoltRelationId::InstructionClaimReduction => VerifierPhase::Stage2, - JoltRelationId::SpartanShift - | JoltRelationId::InstructionInputVirtualization - | JoltRelationId::RegistersClaimReduction => VerifierPhase::Stage3, - JoltRelationId::RamValCheck | JoltRelationId::RegistersReadWriteChecking => { - VerifierPhase::Stage4 - } - JoltRelationId::InstructionReadRaf - | JoltRelationId::RamRaClaimReduction - | JoltRelationId::RegistersValEvaluation => VerifierPhase::Stage5, - JoltRelationId::BytecodeReadRaf - | JoltRelationId::Booleanity - | JoltRelationId::RamHammingBooleanity - | JoltRelationId::InstructionRaVirtualization - | JoltRelationId::RamRaVirtualization - | JoltRelationId::IncClaimReduction - | JoltRelationId::AdviceClaimReductionCyclePhase - | JoltRelationId::BytecodeClaimReductionCyclePhase - | JoltRelationId::ProgramImageClaimReductionCyclePhase => VerifierPhase::Stage6, - JoltRelationId::AdviceClaimReduction - | JoltRelationId::BytecodeClaimReduction - | JoltRelationId::HammingWeightClaimReduction - | JoltRelationId::ProgramImageClaimReduction => VerifierPhase::Stage7, - } -} - -/// Relation-level errors use `format!("{:?}", relation_id)`. -fn relation_phase_from_stage_string(stage: &str) -> Option { - let phase = [ - JoltRelationId::SpartanOuter, - JoltRelationId::SpartanProductVirtualization, - JoltRelationId::SpartanShift, - JoltRelationId::InstructionClaimReduction, - JoltRelationId::InstructionInputVirtualization, - JoltRelationId::InstructionReadRaf, - JoltRelationId::InstructionRaVirtualization, - JoltRelationId::RamReadWriteChecking, - JoltRelationId::RamRafEvaluation, - JoltRelationId::RamOutputCheck, - JoltRelationId::RamValCheck, - JoltRelationId::RamRaClaimReduction, - JoltRelationId::RamHammingBooleanity, - JoltRelationId::RamRaVirtualization, - JoltRelationId::RegistersClaimReduction, - JoltRelationId::RegistersReadWriteChecking, - JoltRelationId::RegistersValEvaluation, - JoltRelationId::BytecodeReadRaf, - JoltRelationId::Booleanity, - JoltRelationId::AdviceClaimReductionCyclePhase, - JoltRelationId::AdviceClaimReduction, - JoltRelationId::BytecodeClaimReductionCyclePhase, - JoltRelationId::BytecodeClaimReduction, - JoltRelationId::ProgramImageClaimReductionCyclePhase, - JoltRelationId::ProgramImageClaimReduction, - JoltRelationId::IncClaimReduction, - JoltRelationId::HammingWeightClaimReduction, - ] - .into_iter() - .find(|id| format!("{id:?}") == stage) - .map(relation_phase); - #[cfg(feature = "field-inline")] - let phase = phase.or_else(|| { - [ - FieldInlineRelationId::FieldRegistersSpartanOuter, - FieldInlineRelationId::FieldRegistersClaimReduction, - FieldInlineRelationId::FieldRegistersProduct, - FieldInlineRelationId::FieldRegistersReadWriteChecking, - FieldInlineRelationId::FieldRegistersValEvaluation, - FieldInlineRelationId::FieldRegistersIncClaimReduction, - ] - .into_iter() - .find(|id| format!("{id:?}") == stage) - .map(|id| match id { - FieldInlineRelationId::FieldRegistersSpartanOuter => VerifierPhase::Stage1, - FieldInlineRelationId::FieldRegistersClaimReduction - | FieldInlineRelationId::FieldRegistersProduct => VerifierPhase::Stage2, - FieldInlineRelationId::FieldRegistersReadWriteChecking => VerifierPhase::Stage4, - FieldInlineRelationId::FieldRegistersValEvaluation => VerifierPhase::Stage5, - FieldInlineRelationId::FieldRegistersIncClaimReduction => VerifierPhase::Stage6, - }) - }); - phase -} - -/// Maps a rejection to the verifier phase that raised it, where the error -/// variant carries enough information. `None` means the variant is -/// phase-agnostic (e.g. a missing claim id observed wherever it is first -/// consumed) and the caller should skip phase attribution. -pub fn observed_rejection_phase(error: &VerifierError) -> Option { - match error { - VerifierError::ProtocolConfigMismatch { .. } - | VerifierError::ExpectedClearProof { .. } - | VerifierError::ExpectedCommittedProof { .. } - | VerifierError::UnexpectedBlindFoldProof - | VerifierError::MissingBlindFoldProof - | VerifierError::UnexpectedOpeningClaims - | VerifierError::MissingVectorCommitmentSetup - | VerifierError::InvalidVectorCommitmentCapacity { .. } - | VerifierError::MemoryLayoutMismatch - | VerifierError::InputTooLarge { .. } - | VerifierError::OutputTooLarge { .. } - | VerifierError::InvalidTraceLength { .. } - | VerifierError::InvalidRamK { .. } - | VerifierError::InvalidMemoryLayout { .. } - | VerifierError::InvalidPrecommittedSchedule { .. } - | VerifierError::InvalidCommittedProgram { .. } - | VerifierError::MissingPreprocessingPayload { .. } - | VerifierError::UnsupportedInstruction { .. } - | VerifierError::InvalidFieldInlineBytecode { .. } - | VerifierError::PreprocessingDigestFailed { .. } => Some(VerifierPhase::Preamble), - VerifierError::MissingProofPayload { field } => match *field { - "commitments.field_inline" | "field_inc_commitment" => Some(VerifierPhase::Preamble), - _ => None, - }, - VerifierError::StageClaimSumcheckFailed { stage, .. } - | VerifierError::StageClaimOpeningMismatch { stage, .. } => { - let phase = match stage.as_str() { - "Stage1Batch" => Some(VerifierPhase::Stage1), - "Stage2Batch" => Some(VerifierPhase::Stage2), - "Stage3" => Some(VerifierPhase::Stage3), - "Stage4" => Some(VerifierPhase::Stage4), - "Stage5" => Some(VerifierPhase::Stage5), - "Stage6a" | "Stage6b" => Some(VerifierPhase::Stage6), - "Stage7" => Some(VerifierPhase::Stage7), - _ => relation_phase_from_stage_string(stage), - }; - assert!(phase.is_some(), "unrecognized verifier stage: {stage}"); - phase - } - VerifierError::StageClaimPublicInputFailed { stage, .. } => Some(relation_phase(*stage)), - VerifierError::StageClaimOutputMismatch { stage } => match stage { - 1 => Some(VerifierPhase::Stage1), - 2 => Some(VerifierPhase::Stage2), - 3 => Some(VerifierPhase::Stage3), - 4 => Some(VerifierPhase::Stage4), - 5 => Some(VerifierPhase::Stage5), - 6 => Some(VerifierPhase::Stage6), - 7 => Some(VerifierPhase::Stage7), - _ => None, - }, - VerifierError::InvalidCommitmentCount { .. } - | VerifierError::MissingFinalOpeningCommitment { .. } - | VerifierError::FinalOpeningBatchFailed { .. } - | VerifierError::FinalOpeningVerificationFailed { .. } => { - Some(VerifierPhase::Stage8Openings) - } - VerifierError::BlindFoldConstructionFailed { .. } - | VerifierError::BlindFoldVerificationFailed { .. } => Some(VerifierPhase::Zk), - VerifierError::ProtocolAxisUnimplemented { .. } - | VerifierError::MissingOpeningClaim { .. } - | VerifierError::UnexpectedOpeningClaim { .. } - | VerifierError::MissingStageClaimChallenge { .. } - | VerifierError::MissingStageClaimDerived { .. } - | VerifierError::ChallengeDraw(_) => None, - } -} - -#[cfg(all( - feature = "prover-fixtures", - not(feature = "zk"), - not(feature = "akita") -))] -pub fn assert_verifier_fixture_tamper_rejects( - target: TamperTarget, - base: &crate::support::verifier_fixtures::VerifierFixtureCase, - mutate: impl FnOnce(&mut crate::support::verifier_fixtures::VerifierFixtureCase), -) { - assert_manifest_target_is_active(target); - // A tamper test is vacuous unless the untampered fixture verifies. - crate::support::assert_accepts(base.verify()); - let mut case = base.clone(); - mutate(&mut case); - let result = case.verify(); - assert!( - result.is_err(), - "tampered standard proof was accepted for target {}", - target.name - ); - // The manifest documents the LAST line of defense: rejection may fire - // earlier (transcript-bound values diverge challenges at the first - // post-absorption check) but never later than the documented phase. - if let Err(error) = result { - if let Some(observed) = observed_rejection_phase(&error) { - let expected = expected_rejection_phase(target); - assert!( - observed <= expected, - "target {} was rejected in {observed:?}, later than the manifest's \ - documented {expected:?} (disposition {:?}); error: {error}", - target.name, - target.disposition - ); - } - } -} - -fn expand_manifest_path(target: TamperTarget) -> Vec<&'static str> { - match target.path { - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_inline.*" => vec![ - "claims.stage1.outer.outer_remainder.field_inline.rs1_value", - "claims.stage1.outer.outer_remainder.field_inline.rs2_value", - "claims.stage1.outer.outer_remainder.field_inline.rd_value", - "claims.stage1.outer.outer_remainder.field_inline.product", - "claims.stage1.outer.outer_remainder.field_inline.inv_product", - ], - #[cfg(feature = "field-inline")] - "claims.stage2.batch_outputs.product_remainder.field_inline.*" => vec![ - "claims.stage2.batch_outputs.product_remainder.field_inline.rs1_value", - "claims.stage2.batch_outputs.product_remainder.field_inline.rs2_value", - "claims.stage2.batch_outputs.product_remainder.field_inline.rd_value", - ], - "claims.stage1.outer.*" => vec![ - "claims.stage1.outer.outer_remainder.left_instruction_input", - "claims.stage1.outer.outer_remainder.right_instruction_input", - "claims.stage1.outer.outer_remainder.product", - "claims.stage1.outer.outer_remainder.should_branch", - "claims.stage1.outer.outer_remainder.pc", - "claims.stage1.outer.outer_remainder.unexpanded_pc", - "claims.stage1.outer.outer_remainder.imm", - "claims.stage1.outer.outer_remainder.ram_address", - "claims.stage1.outer.outer_remainder.rs1_value", - "claims.stage1.outer.outer_remainder.rs2_value", - "claims.stage1.outer.outer_remainder.rd_write_value", - "claims.stage1.outer.outer_remainder.ram_read_value", - "claims.stage1.outer.outer_remainder.ram_write_value", - "claims.stage1.outer.outer_remainder.left_lookup_operand", - "claims.stage1.outer.outer_remainder.right_lookup_operand", - "claims.stage1.outer.outer_remainder.next_unexpanded_pc", - "claims.stage1.outer.outer_remainder.next_pc", - "claims.stage1.outer.outer_remainder.next_is_virtual", - "claims.stage1.outer.outer_remainder.next_is_first_in_sequence", - "claims.stage1.outer.outer_remainder.lookup_output", - "claims.stage1.outer.outer_remainder.should_jump", - "claims.stage1.outer.outer_remainder.add_operands", - "claims.stage1.outer.outer_remainder.subtract_operands", - "claims.stage1.outer.outer_remainder.multiply_operands", - "claims.stage1.outer.outer_remainder.load", - "claims.stage1.outer.outer_remainder.store", - "claims.stage1.outer.outer_remainder.jump", - "claims.stage1.outer.outer_remainder.write_lookup_output_to_rd", - "claims.stage1.outer.outer_remainder.virtual_instruction", - "claims.stage1.outer.outer_remainder.assert", - "claims.stage1.outer.outer_remainder.do_not_update_unexpanded_pc", - "claims.stage1.outer.outer_remainder.advice", - "claims.stage1.outer.outer_remainder.is_compressed", - "claims.stage1.outer.outer_remainder.is_first_in_sequence", - "claims.stage1.outer.outer_remainder.is_last_in_sequence", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_add", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_sub", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_mul", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_inv", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_assert_eq", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_load_accumulate_from_register", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_assert_zero", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_load_imm", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_load_accumulate_from_memory", - #[cfg(feature = "field-inline")] - "claims.stage1.outer.outer_remainder.field_advice_limb", - ], - "claims.stage2.batch_outputs.ram_read_write.*" => vec![ - "claims.stage2.batch_outputs.ram_read_write.val", - "claims.stage2.batch_outputs.ram_read_write.ra", - "claims.stage2.batch_outputs.ram_read_write.inc", - ], - "claims.stage2.batch_outputs.product_remainder.{left_instruction_input,right_instruction_input,jump_flag,lookup_output,branch_flag,next_is_noop}" => vec![ - "claims.stage2.batch_outputs.product_remainder.left_instruction_input", - "claims.stage2.batch_outputs.product_remainder.right_instruction_input", - "claims.stage2.batch_outputs.product_remainder.jump_flag", - "claims.stage2.batch_outputs.product_remainder.lookup_output", - "claims.stage2.batch_outputs.product_remainder.branch_flag", - "claims.stage2.batch_outputs.product_remainder.next_is_noop", - ], - "claims.stage2.batch_outputs.instruction_claim_reduction.*" => vec![ - "claims.stage2.batch_outputs.instruction_claim_reduction.lookup_output", - "claims.stage2.batch_outputs.instruction_claim_reduction.left_lookup_operand", - "claims.stage2.batch_outputs.instruction_claim_reduction.right_lookup_operand", - "claims.stage2.batch_outputs.instruction_claim_reduction.left_instruction_input", - "claims.stage2.batch_outputs.instruction_claim_reduction.right_instruction_input", - ], - #[cfg(feature = "field-inline")] - "claims.stage2.batch_outputs.field_registers_claim_reduction.*" => vec![ - "claims.stage2.batch_outputs.field_registers_claim_reduction.rd_value", - "claims.stage2.batch_outputs.field_registers_claim_reduction.rs1_value", - "claims.stage2.batch_outputs.field_registers_claim_reduction.rs2_value", - ], - "claims.stage3.shift.*" => vec![ - "claims.stage3.shift.unexpanded_pc", - "claims.stage3.shift.pc", - "claims.stage3.shift.is_virtual", - "claims.stage3.shift.is_first_in_sequence", - "claims.stage3.shift.is_noop", - ], - "claims.stage3.instruction_input.*" => vec![ - "claims.stage3.instruction_input.left_operand_is_rs1", - "claims.stage3.instruction_input.rs1_value", - "claims.stage3.instruction_input.left_operand_is_pc", - "claims.stage3.instruction_input.unexpanded_pc", - "claims.stage3.instruction_input.right_operand_is_rs2", - "claims.stage3.instruction_input.rs2_value", - "claims.stage3.instruction_input.right_operand_is_imm", - "claims.stage3.instruction_input.imm", - ], - "claims.stage3.registers_claim_reduction.*" => vec![ - "claims.stage3.registers_claim_reduction.rd_write_value", - "claims.stage3.registers_claim_reduction.rs1_value", - "claims.stage3.registers_claim_reduction.rs2_value", - ], - "claims.stage4.registers_read_write.*" => vec![ - "claims.stage4.registers_read_write.registers_val", - "claims.stage4.registers_read_write.rs1_ra", - "claims.stage4.registers_read_write.rs2_ra", - "claims.stage4.registers_read_write.rd_wa", - "claims.stage4.registers_read_write.rd_inc", - ], - #[cfg(feature = "field-inline")] - "claims.stage4.field_registers_read_write.*" => vec![ - "claims.stage4.field_registers_read_write.registers_val", - "claims.stage4.field_registers_read_write.rs1_ra", - "claims.stage4.field_registers_read_write.rs2_ra", - "claims.stage4.field_registers_read_write.rd_wa", - "claims.stage4.field_registers_read_write.rd_inc", - ], - "claims.stage4.ram_val_check.*" => vec![ - "claims.stage4.ram_val_check.ram_ra", - "claims.stage4.ram_val_check.ram_inc", - ], - "claims.stage5.registers_val_evaluation.*" => vec![ - "claims.stage5.registers_val_evaluation.rd_inc", - "claims.stage5.registers_val_evaluation.rd_wa", - ], - #[cfg(feature = "field-inline")] - "claims.stage5.field_registers_val_evaluation.*" => vec![ - "claims.stage5.field_registers_val_evaluation.rd_inc", - "claims.stage5.field_registers_val_evaluation.rd_wa", - ], - path => vec![path], - } -} - -fn collect_leaf_paths(prefix: &str, value: &Value, paths: &mut BTreeSet) { - match value { - Value::Null if prefix.ends_with(".field_inline") => {} - Value::Object(map) => { - for (key, value) in map { - collect_leaf_paths(&format!("{prefix}.{key}"), value, paths); - } - } - _ => { - let _ = paths.insert(prefix.to_string()); - } - } -} - -#[cfg_attr( - not(feature = "field-inline"), - expect( - clippy::useless_conversion, - reason = "field-inline selects composed claim and opening types" - ) -)] -pub fn clear_claims(fill_optionals: bool) -> ClearProofClaims { - let zero = F::zero(); - let optional = fill_optionals.then_some(zero); - - ClearProofClaims { - stage1: Stage1OutputClaims::new( - zero, - Stage1BatchOutputClaims { - outer_remainder: OuterRemainderOutputClaims::::default().into(), - }, - ), - stage2: Stage2OutputClaims::new( - zero, - Stage2BatchOutputClaims { - ram_read_write: stage2::outputs::RamReadWriteOutputClaims { - val: zero, - ra: zero, - inc: zero, - }, - product_remainder: stage2::outputs::ProductRemainderOutputClaims { - left_instruction_input: zero, - right_instruction_input: zero, - jump_flag: zero, - write_lookup_output_to_rd: zero, - lookup_output: zero, - branch_flag: zero, - next_is_noop: zero, - virtual_instruction: zero, - }.into(), - instruction_claim_reduction: - stage2::outputs::InstructionClaimReductionOutputClaims { - lookup_output: zero, - left_lookup_operand: zero, - right_lookup_operand: zero, - left_instruction_input: zero, - right_instruction_input: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_claim_reduction: - FieldRegistersClaimReductionOutputClaims { - rd_value: zero, - rs1_value: zero, - rs2_value: zero, - }, - ram_raf_evaluation: stage2::outputs::RamRafEvaluationOutputClaims { ram_ra: zero }, - ram_output_check: stage2::outputs::RamOutputCheckOutputClaims { val_final: zero }, - }, - ), - stage3: stage3::outputs::Stage3OutputClaims { - shift: stage3::outputs::SpartanShiftOutputClaims { - unexpanded_pc: zero, - pc: zero, - is_virtual: zero, - is_first_in_sequence: zero, - is_noop: zero, - }, - instruction_input: stage3::outputs::InstructionInputOutputClaims { - left_operand_is_rs1: zero, - rs1_value: zero, - left_operand_is_pc: zero, - unexpanded_pc: zero, - right_operand_is_rs2: zero, - rs2_value: zero, - right_operand_is_imm: zero, - imm: zero, - }, - registers_claim_reduction: stage3::outputs::RegistersClaimReductionOutputClaims { - rd_write_value: zero, - rs1_value: zero, - rs2_value: zero, - }, - }, - stage4: stage4::outputs::Stage4OutputClaims { - registers_read_write: stage4::RegistersReadWriteOutputClaims { - registers_val: zero, - rs1_ra: zero, - rs2_ra: zero, - rd_wa: zero, - rd_inc: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_read_write: FieldRegistersReadWriteOutputClaims { - registers_val: zero, - rs1_ra: zero, - rs2_ra: zero, - rd_wa: zero, - rd_inc: zero, - }, - ram_val_check: stage4::RamValCheckOutputClaims { - untrusted_advice: optional, - trusted_advice: optional, - program_image: None, - ram_ra: zero, - ram_inc: zero, - }, - }, - stage5: stage5::outputs::Stage5OutputClaims { - instruction_read_raf: stage5::InstructionReadRafOutputClaims { - lookup_table_flags: vec![zero], - instruction_ra: vec![zero], - instruction_raf_flag: zero, - }, - ram_ra_claim_reduction: stage5::RamRaClaimReductionOutputClaims { ram_ra: zero }, - registers_val_evaluation: stage5::RegistersValEvaluationOutputClaims { - rd_inc: zero, - rd_wa: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_val_evaluation: FieldRegistersValEvaluationOutputClaims { - rd_inc: zero, - rd_wa: zero, - }, - }, - stage6a: stage6a::outputs::Stage6aOutputClaims { - bytecode_read_raf: stage6a::outputs::BytecodeReadRafAddressPhaseOutputClaims { - intermediate: zero, - val_stages: Vec::new(), - }.into(), - booleanity: stage6a::outputs::BooleanityAddressPhaseOutputClaims { - intermediate: zero, - }, - }, - stage6b: stage6b::outputs::Stage6bOutputClaims { - #[cfg(not(feature = "akita"))] - bytecode_read_raf: stage6b::outputs::BytecodeReadRafOutputClaims { - bytecode_ra: vec![zero], - }, - #[cfg(feature = "akita")] - bytecode_read_raf: - stage6b::bytecode_read_raf::LatticeBytecodeReadRafOutputClaims { - bytecode_ra: vec![zero], - fused_inc: zero, - }, - #[cfg(not(feature = "akita"))] - booleanity: stage6b::outputs::BooleanityOutputClaims { - instruction_ra: vec![zero], - bytecode_ra: vec![zero], - ram_ra: vec![zero], - }, - #[cfg(feature = "akita")] - booleanity: - jolt_claims::protocols::jolt::lattice::relations::booleanity::LatticeBooleanityOutputClaims { - instruction_ra: vec![zero], - bytecode_ra: vec![zero], - ram_ra: vec![zero], - balanced_inc_digits: vec![zero], - balanced_inc_carry: zero, - }, - ram_hamming_booleanity: stage6b::outputs::RamHammingBooleanityOutputClaims { - ram_hamming_weight: zero, - }, - ram_ra_virtualization: stage6b::outputs::RamRaVirtualizationOutputClaims { - ram_ra: vec![zero], - }, - instruction_ra_virtualization: - stage6b::outputs::InstructionRaVirtualizationOutputClaims { - committed_instruction_ra: vec![zero], - }, - #[cfg(not(feature = "akita"))] - inc_claim_reduction: stage6b::outputs::IncClaimReductionOutputClaims { - ram_inc: zero, - rd_inc: zero, - }, - #[cfg(feature = "field-inline")] - field_registers_inc_claim_reduction: - FieldRegistersIncClaimReductionOutputClaims { rd_inc: zero }, - #[cfg(not(feature = "akita"))] - trusted_advice: fill_optionals.then_some( - stage6b::outputs::TrustedAdviceCyclePhaseOutputClaims { trusted: zero }, - ), - #[cfg(not(feature = "akita"))] - untrusted_advice: fill_optionals.then_some( - stage6b::outputs::UntrustedAdviceCyclePhaseOutputClaims { untrusted: zero }, - ), - bytecode_reduction: fill_optionals.then_some( - stage6b::outputs::BytecodeReductionCyclePhaseOutputClaims { - intermediate: Some(zero), - chunks: Vec::new(), - }, - ), - program_image_reduction: fill_optionals.then_some( - stage6b::outputs::ProgramImageReductionCyclePhaseOutputClaims { - program_image: zero, - }, - ), - }, - stage7: stage7::outputs::Stage7OutputClaims { - hamming_weight_claim_reduction: - stage7::hamming_weight_claim_reduction::HammingWeightClaimReductionOutputClaims { - instruction_ra: vec![zero], - bytecode_ra: vec![zero], - ram_ra: vec![zero], - #[cfg(feature = "akita")] - balanced_inc_digits: vec![zero], - #[cfg(feature = "akita")] - balanced_inc_carry: zero, - }, - #[cfg(not(feature = "akita"))] - trusted_advice: fill_optionals.then_some( - stage7::advice_address_phase::TrustedAdviceAddressPhaseOutputClaims { - trusted: zero, - }, - ), - #[cfg(not(feature = "akita"))] - untrusted_advice: fill_optionals.then_some( - stage7::advice_address_phase::UntrustedAdviceAddressPhaseOutputClaims { - untrusted: zero, - }, - ), - bytecode_address_phase: fill_optionals.then_some( - stage7::committed_reduction_address_phase::BytecodeReductionAddressPhaseOutputClaims { - chunks: vec![zero], - }, - ), - program_image_address_phase: fill_optionals.then_some( - stage7::committed_reduction_address_phase::ProgramImageReductionAddressPhaseOutputClaims { - program_image: zero, - }, - ), - }, - } -} diff --git a/crates/jolt-verifier/tests/support/zk_audit.rs b/crates/jolt-verifier/tests/support/zk_audit.rs deleted file mode 100644 index f259a34338..0000000000 --- a/crates/jolt-verifier/tests/support/zk_audit.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! Shape audits for verifier-native ZK proofs. - -use common::jolt_device::JoltDevice; -use jolt_blindfold::BlindFoldProtocol; -use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_crypto::{HomomorphicCommitment, VectorCommitment}; -use jolt_field::JoltField; -use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, ZkOpeningScheme}; -use jolt_transcript::{AppendToTranscript, Transcript}; - -use jolt_verifier::{ - preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, - stages::{ - stage1, stage2, stage3, stage4, stage5, stage6a, stage6b, stage7, stage8, - zk::{blindfold, inputs::BlindFoldInputs}, - }, - VerifierError, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ZkBlindFoldProtocolShape { - pub coefficient_rows: usize, - pub output_claim_rows: usize, - pub auxiliary_rows: usize, - pub witness_row_count: usize, - pub witness_row_len: usize, - pub error_row_count: usize, - pub error_row_len: usize, - pub eval_commitments: usize, -} - -impl ZkBlindFoldProtocolShape { - fn from_protocol(protocol: &BlindFoldProtocol) -> Self - where - F: JoltField, - { - Self { - coefficient_rows: protocol.dimensions.coefficient_rows, - output_claim_rows: protocol.dimensions.output_claim_rows, - auxiliary_rows: protocol.dimensions.auxiliary_rows, - witness_row_count: protocol.dimensions.witness.row_count, - witness_row_len: protocol.dimensions.witness.row_len, - error_row_count: protocol.dimensions.error.row_count, - error_row_len: protocol.dimensions.error.row_len, - eval_commitments: protocol.eval_commitments.len(), - } - } -} - -pub fn audit_zk_blindfold_protocol_shape( - preprocessing: &JoltVerifierPreprocessing, - public_io: &JoltDevice, - proof: &JoltProof, - trusted_advice_commitment: Option<&PCS::Output>, -) -> Result -where - F: JoltField + AppendToTranscript, - PCS: CommitmentScheme - + AdditivelyHomomorphic - + ZkOpeningScheme, - PCS::Output: AppendToTranscript + HomomorphicCommitment, - VC: VectorCommitment, - VC::Output: Copy + HomomorphicCommitment + AppendToTranscript, - T: Transcript, -{ - let (checked, mut transcript) = - jolt_verifier::validate_and_seed_transcript::( - preprocessing, - public_io, - proof, - trusted_advice_commitment, - )?; - - let formula_dimensions = jolt_verifier::stages::build_formula_dimensions( - proof, - preprocessing, - &checked, - checked.trace_length.ilog2() as usize, - JoltRelationId::InstructionReadRaf, - )?; - - let stage1 = stage1::verify(&checked, proof, &mut transcript)?; - let stage2 = stage2::verify(&checked, proof, &mut transcript, &stage1)?; - let stage3 = stage3::verify(&checked, proof, &mut transcript, &stage1, &stage2)?; - let stage4 = stage4::verify( - &checked, - preprocessing, - proof, - &mut transcript, - &stage2, - &stage3, - )?; - let stage5 = stage5::verify( - &checked, - proof, - &formula_dimensions, - &mut transcript, - &stage2, - &stage4, - )?; - let stage6a = stage6a::verify( - &checked, - preprocessing, - proof, - &formula_dimensions, - &mut transcript, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - )?; - let stage6b = stage6b::verify( - &checked, - preprocessing, - proof, - &formula_dimensions, - &mut transcript, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - &stage6a, - )?; - let stage7 = stage7::verify( - &checked, - proof, - &formula_dimensions, - &mut transcript, - &stage4, - &stage6b, - )?; - let stage8 = stage8::verify( - &checked, - preprocessing, - proof, - &formula_dimensions, - trusted_advice_commitment, - &mut transcript, - &stage6b, - &stage7, - )?; - - let blindfold = blindfold::build(BlindFoldInputs { - checked: &checked, - preprocessing, - proof, - stage1: stage1.zk()?, - stage2: stage2.zk()?, - stage3: stage3.zk()?, - stage4: stage4.zk()?, - stage5: stage5.zk()?, - stage6a: stage6a.zk()?, - stage6b: stage6b.zk()?, - stage7: stage7.zk()?, - stage8: stage8.zk()?, - })?; - - Ok(ZkBlindFoldProtocolShape::from_protocol(&blindfold)) -} From a87a2d36a0d3ce12a14ad44cf14cb16207e38c6e Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 17:12:07 -0400 Subject: [PATCH 30/62] refactor(transcript)!: delete the legacy symmetric transcript facade `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. --- crates/jolt-transcript/Cargo.toml | 39 +-- crates/jolt-transcript/src/codec.rs | 132 -------- crates/jolt-transcript/src/digest.rs | 196 ----------- crates/jolt-transcript/src/legacy.rs | 316 ------------------ crates/jolt-transcript/src/lib.rs | 68 ---- crates/jolt-transcript/src/setup.rs | 179 ---------- crates/jolt-transcript/tests/blake2b_tests.rs | 78 ----- crates/jolt-transcript/tests/common/mod.rs | 177 ---------- .../tests/digest_transcript_tests.rs | 156 --------- crates/jolt-transcript/tests/keccak_tests.rs | 29 -- crates/jolt-transcript/tests/known_answers.rs | 5 - .../tests/legacy_digest_tests.rs | 183 ---------- crates/jolt-transcript/tests/narg.rs | 11 +- .../jolt-transcript/tests/narg_eof_tests.rs | 105 ------ .../jolt-transcript/tests/poseidon_tests.rs | 29 -- 15 files changed, 18 insertions(+), 1685 deletions(-) delete mode 100644 crates/jolt-transcript/src/codec.rs delete mode 100644 crates/jolt-transcript/src/digest.rs delete mode 100644 crates/jolt-transcript/src/legacy.rs delete mode 100644 crates/jolt-transcript/src/setup.rs delete mode 100644 crates/jolt-transcript/tests/blake2b_tests.rs delete mode 100644 crates/jolt-transcript/tests/common/mod.rs delete mode 100644 crates/jolt-transcript/tests/digest_transcript_tests.rs delete mode 100644 crates/jolt-transcript/tests/keccak_tests.rs delete mode 100644 crates/jolt-transcript/tests/legacy_digest_tests.rs delete mode 100644 crates/jolt-transcript/tests/narg_eof_tests.rs delete mode 100644 crates/jolt-transcript/tests/poseidon_tests.rs diff --git a/crates/jolt-transcript/Cargo.toml b/crates/jolt-transcript/Cargo.toml index cfd995eb4f..453aa283d5 100644 --- a/crates/jolt-transcript/Cargo.toml +++ b/crates/jolt-transcript/Cargo.toml @@ -15,17 +15,10 @@ workspace = true default = ["transcript-blake2b", "transcript-keccak", "transcript-poseidon"] # Records every transcript operation with its site and argument-string range. logging = [] -spongefish = ["dep:rand"] -digest = ["dep:digest"] -bn254 = [ - "dep:ark-bn254", - "dep:ark-ff", - "jolt-field/bn254", - "spongefish/ark-ff", -] -transcript-blake2b = ["spongefish", "digest", "spongefish/blake2", "dep:blake2"] -transcript-keccak = ["spongefish", "spongefish/keccak"] -transcript-poseidon = ["spongefish", "bn254", "dep:light-poseidon"] +bn254 = ["dep:ark-bn254", "dep:ark-ff", "jolt-field/bn254"] +transcript-blake2b = ["spongefish/blake2"] +transcript-keccak = ["spongefish/keccak"] +transcript-poseidon = ["bn254", "dep:light-poseidon"] [dependencies] ark-bn254 = { workspace = true, optional = true } @@ -36,13 +29,9 @@ light-poseidon = { workspace = true, optional = true } spongefish = { workspace = true, features = ["sha3"] } rand_core = { workspace = true } thiserror = { workspace = true } -blake2 = { workspace = true, optional = true } -digest = { workspace = true, optional = true } jolt-field = { path = "../jolt-field", default-features = false } -rand = { workspace = true, features = ["std_rng"], optional = true } [dev-dependencies] -num-traits = { workspace = true } criterion = { workspace = true } [[bench]] @@ -55,13 +44,17 @@ required-features = [ ] [[test]] -name = "blake2b_tests" -required-features = ["bn254", "transcript-blake2b"] - -[[test]] -name = "keccak_tests" -required-features = ["bn254", "transcript-keccak"] +name = "known_answers" +required-features = [ + "transcript-blake2b", + "transcript-keccak", + "transcript-poseidon", +] [[test]] -name = "poseidon_tests" -required-features = ["transcript-poseidon"] +name = "narg" +required-features = [ + "transcript-blake2b", + "transcript-keccak", + "transcript-poseidon", +] diff --git a/crates/jolt-transcript/src/codec.rs b/crates/jolt-transcript/src/codec.rs deleted file mode 100644 index 0ce945b90c..0000000000 --- a/crates/jolt-transcript/src/codec.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Local codecs for absorbing / decoding Jolt-native messages over a -//! byte-oriented spongefish sponge. -//! -//! Field-element codecs come from spongefish's `ark-ff` feature -//! (`spongefish::Encoding<[u8]>` is implemented for every `ark_ff::Fp` -//! using big-endian canonical encoding per RFC8017). 128-bit-truncated -//! challenges decode through spongefish's built-in `u128` codec directly; -//! see [`crate::prover::OptimizedChallenge`]. -//! -//! This module keeps only `BytesMsg`, the length-prefixed byte string -//! framing that spongefish 0.6 does not provide. - -use spongefish::{Encoding, NargDeserialize, VerificationError, VerificationResult}; - -/// Length-prefixed byte string. 8-byte LE length keeps `BytesMsg(a) ; BytesMsg(b)` -/// distinguishable from `BytesMsg(a||b)`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BytesMsg(pub Vec); - -impl BytesMsg { - /// Returns the inner bytes. - pub fn as_slice(&self) -> &[u8] { - &self.0 - } -} - -impl From> for BytesMsg { - fn from(v: Vec) -> Self { - Self(v) - } -} - -impl Encoding<[u8]> for BytesMsg { - fn encode(&self) -> impl AsRef<[u8]> { - let mut out = Vec::with_capacity(8 + self.0.len()); - out.extend_from_slice(&(self.0.len() as u64).to_le_bytes()); - out.extend_from_slice(&self.0); - out - } -} - -impl NargDeserialize for BytesMsg { - fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { - let data = *buf; - let Some((len_bytes, rest)) = data.split_first_chunk::<8>() else { - return Err(VerificationError); - }; - let len = usize::try_from(u64::from_le_bytes(*len_bytes)).map_err(|_| VerificationError)?; - if rest.len() < len { - return Err(VerificationError); - } - let (body, remainder) = rest.split_at(len); - *buf = remainder; - Ok(BytesMsg(body.to_vec())) - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing, reason = "tests index fixture data")] -mod tests { - use super::*; - - #[test] - fn bytes_msg_is_length_prefixed() { - let m = BytesMsg(vec![1, 2, 3, 4]); - let enc = m.encode(); - let bytes = enc.as_ref(); - assert_eq!(bytes.len(), 8 + 4); - assert_eq!(&bytes[..8], &4u64.to_le_bytes()); - assert_eq!(&bytes[8..], &[1, 2, 3, 4]); - } - - #[test] - fn bytes_msg_narg_rejects_truncation() { - let m = BytesMsg(vec![9, 8, 7]); - let mut narg: Vec = Vec::new(); - narg.extend_from_slice(m.encode().as_ref()); - let _ = narg.pop(); - let mut cursor: &[u8] = &narg; - let before = cursor.len(); - let result = BytesMsg::deserialize_from_narg(&mut cursor); - assert!(result.is_err()); - assert_eq!(cursor.len(), before, "cursor must not advance on error"); - } - - #[test] - fn bytes_msg_narg_rejects_oversized_length() { - let mut narg = Vec::new(); - narg.extend_from_slice(&u64::MAX.to_le_bytes()); - let mut cursor: &[u8] = &narg; - let before = cursor.len(); - let result = BytesMsg::deserialize_from_narg(&mut cursor); - assert!(result.is_err()); - assert_eq!(cursor.len(), before, "cursor must not advance on error"); - } - - #[test] - // A representable length that survives `checked_add` but exceeds the - // remaining buffer must be rejected by the bounds check BEFORE the payload - // copy — the allocation is bounded by the caller-supplied NARG size. If the - // copy ever moves ahead of the bounds check, this test attempts a 1 TiB - // allocation and aborts. - fn rejects_huge_representable_length_without_allocating() { - let mut narg = Vec::new(); - narg.extend_from_slice(&(1u64 << 40).to_le_bytes()); - narg.extend_from_slice(&[0u8; 16]); - let mut cursor: &[u8] = &narg; - let before = cursor.len(); - assert!(BytesMsg::deserialize_from_narg(&mut cursor).is_err()); - assert_eq!(cursor.len(), before, "cursor must not advance on error"); - } - - #[test] - // On 32-bit this exercises the usize::try_from rejection path. - // On 64-bit it exercises the length-vs-remaining rejection with a - // length near usize::MAX. - fn rejects_non_representable_lengths() { - #[cfg(target_pointer_width = "32")] - let len: u64 = (u32::MAX as u64) + 1; - - #[cfg(target_pointer_width = "64")] - let len: u64 = u64::MAX; - - let mut narg = Vec::new(); - narg.extend_from_slice(&len.to_le_bytes()); - - let mut cursor: &[u8] = &narg; - let before = cursor.len(); - assert!(BytesMsg::deserialize_from_narg(&mut cursor).is_err()); - assert_eq!(cursor.len(), before, "cursor must not advance on error"); - } -} diff --git a/crates/jolt-transcript/src/digest.rs b/crates/jolt-transcript/src/digest.rs deleted file mode 100644 index 544c4f49ce..0000000000 --- a/crates/jolt-transcript/src/digest.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Generic digest-based Fiat-Shamir transcript for the deployed proof format. -//! -//! Appends hash `state || round || payload`, squeezes hash `state || round`, -//! and preserves the established challenge decoding. The spongefish-backed -//! [`crate::SpongeTranscript`] is the native transcript; this one exists for -//! the proof-format compatibility boundary. - -use digest::{consts::U32, Digest}; - -use crate::legacy::{Transcript, MAX_LABEL_LEN}; - -#[cfg(test)] -#[derive(Clone, Default)] -struct TestState { - state_history: Vec<[u8; 32]>, - expected_state_history: Option>, -} - -/// Fiat-Shamir transcript backed by a 256-bit digest. -/// -/// Generic over the hash function `D` and field type `F`. Challenges are -/// produced as field elements through `F::from_challenge_bytes`. -pub struct DigestTranscript + 'static, F> { - state: [u8; 32], - n_rounds: u32, - #[cfg(test)] - test_state: TestState, - _marker: std::marker::PhantomData<(fn() -> D, F)>, -} - -impl Clone for DigestTranscript -where - D: Digest, - F: jolt_field::CanonicalEncoding, -{ - fn clone(&self) -> Self { - Self { - state: self.state, - n_rounds: self.n_rounds, - #[cfg(test)] - test_state: self.test_state.clone(), - _marker: std::marker::PhantomData, - } - } -} - -/// Exists only because [`Transcript`] requires `Default`. -/// -/// WARNING: `Default` does not produce the zero-state transcript used by the -/// deployed proof format; use [`Transcript::new`] for compatible transcripts. -impl Default for DigestTranscript -where - D: Digest, - F: jolt_field::CanonicalEncoding, -{ - fn default() -> Self { - Self::new(b"") - } -} - -impl std::fmt::Debug for DigestTranscript -where - D: Digest, - F: jolt_field::CanonicalEncoding, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DigestTranscript") - .field("state", &format_args!("{:02x?}", self.state)) - .field("n_rounds", &self.n_rounds) - .finish_non_exhaustive() - } -} - -impl DigestTranscript -where - D: Digest, - F: jolt_field::CanonicalEncoding, -{ - /// Raw multi-byte squeeze backing the deployed challenge decoding, so the - /// compatibility `Transcript` vocabulary can drive this engine - /// directly (no state handoffs). Hidden: protocol code squeezes through - /// the [`Transcript`] challenge methods. - #[doc(hidden)] - pub fn raw_challenge_bytes(&mut self, out: &mut [u8]) { - self.challenge_bytes(out); - } - - #[inline] - fn hasher(&self) -> D { - let mut round_bytes = [0u8; 32]; - round_bytes[28..].copy_from_slice(&self.n_rounds.to_be_bytes()); - D::new().chain_update(self.state).chain_update(round_bytes) - } - - fn challenge_bytes(&mut self, out: &mut [u8]) { - let mut out = &mut *out; - - while out.len() > 32 { - let mut chunk = [0u8; 32]; - self.challenge_bytes32(&mut chunk); - let (head, tail) = std::mem::take(&mut out).split_at_mut(32); - head.copy_from_slice(&chunk); - out = tail; - } - - let mut final_chunk = [0u8; 32]; - self.challenge_bytes32(&mut final_chunk); - let (src, _) = final_chunk.split_at(out.len()); - out.copy_from_slice(src); - } - - #[inline] - fn challenge_bytes32(&mut self, out: &mut [u8; 32]) { - let hash: [u8; 32] = self.hasher().finalize().into(); - out.copy_from_slice(&hash); - self.update_state(hash); - } - - fn update_state(&mut self, new_state: [u8; 32]) { - self.state = new_state; - self.n_rounds += 1; - - #[cfg(test)] - { - if let Some(ref expected) = self.test_state.expected_state_history { - #[expect(clippy::indexing_slicing, reason = "tests index fixture data")] - let expected_state = expected[self.n_rounds as usize]; - assert_eq!( - new_state, expected_state, - "Fiat-Shamir transcript mismatch at round {}", - self.n_rounds - ); - } - self.test_state.state_history.push(new_state); - } - } -} - -impl Transcript for DigestTranscript -where - D: Digest, - F: jolt_field::CanonicalEncoding, -{ - type Challenge = F; - - fn new(label: &'static [u8]) -> Self { - assert!( - label.len() <= MAX_LABEL_LEN, - "label must be at most {MAX_LABEL_LEN} bytes", - ); - - let mut padded = [0u8; MAX_LABEL_LEN]; - let (head, _) = padded.split_at_mut(label.len()); - head.copy_from_slice(label); - - let hash: [u8; 32] = D::new().chain_update(padded).finalize().into(); - - Self { - state: hash, - n_rounds: 0, - #[cfg(test)] - test_state: TestState { - state_history: vec![hash], - expected_state_history: None, - }, - _marker: std::marker::PhantomData, - } - } - - fn append_bytes(&mut self, bytes: &[u8]) { - let hash: [u8; 32] = self.hasher().chain_update(bytes).finalize().into(); - self.update_state(hash); - } - - fn challenge(&mut self) -> F { - let mut buf = [0u8; 16]; - self.challenge_bytes(&mut buf); - F::from_challenge_bytes(&buf) - } - - fn challenge_scalar(&mut self) -> F { - let mut buf = [0u8; 16]; - self.challenge_bytes(&mut buf); - F::from_scalar_challenge_bytes(&buf) - } - - #[inline] - fn state(&self) -> [u8; 32] { - self.state - } - - #[cfg(test)] - fn compare_to(&mut self, other: &Self) { - self.test_state.expected_state_history = Some(other.test_state.state_history.clone()); - } -} diff --git a/crates/jolt-transcript/src/legacy.rs b/crates/jolt-transcript/src/legacy.rs deleted file mode 100644 index cb19565703..0000000000 --- a/crates/jolt-transcript/src/legacy.rs +++ /dev/null @@ -1,316 +0,0 @@ -//! Source-compatible facade for `jolt-sumcheck`, `jolt-openings`, and -//! `jolt-crypto`. -//! -//! Wraps a duplex sponge over each of the three backends and re-exposes -//! the compatibility `Transcript` / `AppendToTranscript` API. It can be -//! removed once all consumers migrate to the split-trait surface. - -#[cfg(feature = "spongefish")] -use std::marker::PhantomData; - -#[cfg(feature = "spongefish")] -use jolt_field::CanonicalEncoding; -use jolt_field::{CanonicalBytes, Ring}; -#[cfg(feature = "spongefish")] -use spongefish::{DuplexSpongeInterface, Encoding}; - -#[cfg(feature = "spongefish")] -use crate::codec::BytesMsg; -#[cfg(feature = "spongefish")] -use crate::setup::{EmptyInstance, PROTOCOL_ID}; - -/// Maximum label length in bytes accepted by [`Transcript::new`] and the -/// label helpers below. -pub const MAX_LABEL_LEN: usize = 32; - -/// Fiat-Shamir transcript for non-interactive proofs. -/// -/// A transcript absorbs data and produces deterministic challenges. Both -/// prover and verifier maintain identical transcripts to derive the same -/// challenges. -/// -/// # Security -/// -/// The label passed to [`new`](Transcript::new) is mapped to the -/// spongefish session value, so distinct labels carry distinct domain -/// barriers. -pub trait Transcript: Default + Sync + Send + 'static { - /// The challenge type produced by this transcript. Concrete transcript - /// implementations state any decoding or canonical-encoding capability - /// they need; custom transcripts may return an algebra-only field type. - type Challenge; - - /// Creates a new transcript with the given domain separation label. - /// - /// # Panics - /// - /// Panics if `label.len() > MAX_LABEL_LEN`. - fn new(label: &'static [u8]) -> Self; - - /// Absorbs raw bytes. - fn append_bytes(&mut self, bytes: &[u8]); - - /// Absorbs a value via [`AppendToTranscript`]. - fn append(&mut self, value: &A) { - value.append_to_transcript(self); - } - - /// Absorbs a domain label followed by a value. - fn append_labeled(&mut self, label: &'static [u8], value: &A) { - self.append(&Label(label)); - self.append(value); - } - - /// Absorbs a domain label with a count followed by each value in order. - fn append_values(&mut self, label: &'static [u8], values: &[A]) { - self.append(&LabelWithCount(label, values.len() as u64)); - for value in values { - self.append(value); - } - } - - /// Squeezes a challenge. - #[must_use] - fn challenge(&mut self) -> Self::Challenge; - - /// Squeezes a non-optimized scalar challenge from the transcript. - #[must_use] - fn challenge_scalar(&mut self) -> Self::Challenge { - self.challenge() - } - - /// Squeezes `len` challenges. - #[must_use] - fn challenge_vector(&mut self, len: usize) -> Vec { - (0..len).map(|_| self.challenge()).collect() - } - - /// Squeezes one scalar challenge and returns its powers `[1, gamma, gamma^2, ...]`. - #[must_use] - fn challenge_scalar_powers(&mut self, len: usize) -> Vec - where - Self::Challenge: Ring, - { - let gamma = self.challenge_scalar(); - let one = Self::Challenge::from_u64(1); - std::iter::successors(Some(one), |power| Some(*power * gamma)) - .take(len) - .collect() - } - - /// Current 256-bit transcript state. Peeked non-destructively by - /// squeezing 32 bytes from a sponge clone, so callers can read it - /// without advancing the real state. Useful for debug-only - /// cross-verifier comparison. - #[must_use] - fn state(&self) -> [u8; 32]; - - /// Enables transcript comparison for tests; mirrors upstream's signature. - /// Spongefish sponges have no replayable state history, so this is a - /// no-op on the legacy facade — call sites already only use it under - /// `#[cfg(test)]` for debugging digest-based transcripts. - #[cfg(test)] - fn compare_to(&mut self, _other: &Self) {} -} - -/// Implement on types that absorb themselves into a [`Transcript`]. -pub trait AppendToTranscript { - /// Absorbs this value into the transcript. - fn append_to_transcript(&self, transcript: &mut T); - - /// Byte length of the payload absorbed by [`Self::append_to_transcript`], when - /// the type participates in variable-length labeled appends. - fn transcript_payload_len(&self) -> Option { - None - } -} - -/// Big-endian field element absorption used by the deployed proof format. -impl AppendToTranscript for F { - fn append_to_transcript(&self, transcript: &mut T) { - let mut buf = vec![0u8; F::NUM_BYTES]; - self.to_bytes_le(&mut buf); - buf.reverse(); - transcript.append_bytes(&buf); - } -} - -/// Absorbs `label` (with the payload's transcript length when it exposes -/// one) followed by the payload itself. The single home of the labeled-append -/// convention shared by the prover's and verifier's commitment absorption — -/// order and labels are consensus-critical. -pub fn append_length_prefixed(transcript: &mut T, label: &'static [u8], payload: &A) -where - T: Transcript, - A: AppendToTranscript, -{ - if let Some(len) = payload.transcript_payload_len() { - transcript.append(&LabelWithCount(label, len)); - } else { - transcript.append(&Label(label)); - } - transcript.append(payload); -} - -/// 32-byte zero-padded label word used by the deployed proof format. -pub struct Label(pub &'static [u8]); - -impl AppendToTranscript for Label { - fn append_to_transcript(&self, transcript: &mut T) { - assert!( - self.0.len() <= 32, - "label {:?} exceeds 32 bytes", - core::str::from_utf8(self.0) - ); - let mut padded = [0u8; 32]; - let (head, _) = padded.split_at_mut(self.0.len()); - head.copy_from_slice(self.0); - transcript.append_bytes(&padded); - } -} - -/// Packed label (24 bytes) + count (8-byte big-endian) in one 32-byte word -/// used by the deployed proof format. -pub struct LabelWithCount(pub &'static [u8], pub u64); - -impl AppendToTranscript for LabelWithCount { - fn append_to_transcript(&self, transcript: &mut T) { - assert!( - self.0.len() <= 24, - "label {:?} exceeds 24 bytes", - core::str::from_utf8(self.0) - ); - let mut packed = [0u8; 32]; - let (head, _) = packed.split_at_mut(self.0.len()); - head.copy_from_slice(self.0); - packed[24..32].copy_from_slice(&self.1.to_be_bytes()); - transcript.append_bytes(&packed); - } -} - -/// EVM-compatible left-padded u64: 24 zero bytes + 8-byte BE value. -pub struct U64Word(pub u64); - -impl AppendToTranscript for U64Word { - fn append_to_transcript(&self, transcript: &mut T) { - let mut packed = [0u8; 32]; - packed[24..].copy_from_slice(&self.0.to_be_bytes()); - transcript.append_bytes(&packed); - } -} - -/// Sponge-backed transcript driving a duplex sponge directly. -/// -/// The legacy facade does not produce or consume a NARG byte string — -/// existing modular consumers only call `append_bytes` / `challenge` / -/// `state`. New code should use [`crate::ProverTranscript`] / -/// [`crate::VerifierTranscript`] instead. -/// -/// Construction mirrors spongefish's `DomainSeparator` builder: -/// `protocol_id || session(label) || instance(())` are absorbed in order. -#[cfg(feature = "spongefish")] -pub struct SpongeTranscript -where - H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: CanonicalEncoding, -{ - sponge: H, - _field: PhantomData, -} - -#[cfg(feature = "spongefish")] -impl Default for SpongeTranscript -where - H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: CanonicalEncoding, -{ - fn default() -> Self { - Self::new(b"") - } -} - -#[cfg(feature = "spongefish")] -fn absorb_encoded(sponge: &mut H, value: &T) -where - H: DuplexSpongeInterface, - T: Encoding<[u8]> + ?Sized, -{ - let _ = sponge.absorb(value.encode().as_ref()); -} - -/// Peeks 32 bytes from a clone of the sponge so the real state stays put. -#[cfg(feature = "spongefish")] -fn peek_state + Clone>(sponge: &H) -> [u8; 32] { - let mut clone = sponge.clone(); - let mut buf = [0u8; 32]; - let _ = clone.squeeze(&mut buf); - buf -} - -#[cfg(feature = "spongefish")] -impl Transcript for SpongeTranscript -where - H: DuplexSpongeInterface + Clone + Default + Send + Sync + 'static, - F: CanonicalEncoding, -{ - type Challenge = F; - - fn new(label: &'static [u8]) -> Self { - assert!( - label.len() <= MAX_LABEL_LEN, - "label must be at most {MAX_LABEL_LEN} bytes", - ); - let mut sponge = H::default(); - absorb_encoded(&mut sponge, &PROTOCOL_ID); - absorb_encoded(&mut sponge, &BytesMsg(label.to_vec())); - absorb_encoded(&mut sponge, &EmptyInstance); - Self { - sponge, - _field: PhantomData, - } - } - - fn append_bytes(&mut self, bytes: &[u8]) { - // 1-byte non-zero domain marker + 8-byte LE length + body. - // - // The marker sub-domain-separates legacy-facade `append_bytes` calls - // from spongefish-native `public_message` / `prover_message` calls on - // the same sponge type, so a future protocol that mixes both paths - // can't have a legacy append collide with a spongefish-native - // BytesMsg of the same body. The length prefix keeps - // `append_bytes(a) ; append_bytes(b)` distinct from - // `append_bytes(a || b)`. - const APPEND_MARKER: u8 = 0x9B; - let mut buf = Vec::with_capacity(9 + bytes.len()); - buf.push(APPEND_MARKER); - buf.extend_from_slice(&(bytes.len() as u64).to_le_bytes()); - buf.extend_from_slice(bytes); - let _ = self.sponge.absorb(&buf); - } - - fn challenge(&mut self) -> F { - // WARNING: this squeezes 16 bytes for every sponge — including - // Poseidon — even though the split-trait surface (`OptimizedChallenge`, - // see `prover.rs:53-55`) deliberately makes 128-bit challenges a - // compile error on Poseidon-backed states. The two surfaces - // disagree on purpose: the legacy facade preserves the legacy - // deployed challenge width for in-flight consumers (jolt-sumcheck, - // jolt-openings, jolt-crypto). Once those migrate to the split-trait - // surface this facade goes away and the inconsistency with it. - let mut buf = [0u8; 16]; - let _ = self.sponge.squeeze(&mut buf); - F::from_challenge_bytes(&buf) - } - - fn challenge_scalar(&mut self) -> F { - // Mirrors the digest transcript's scalar challenge: same 16-byte - // squeeze width as `challenge`, but the non-optimized decoding path. - let mut buf = [0u8; 16]; - let _ = self.sponge.squeeze(&mut buf); - F::from_scalar_challenge_bytes(&buf) - } - - fn state(&self) -> [u8; 32] { - peek_state(&self.sponge) - } -} diff --git a/crates/jolt-transcript/src/lib.rs b/crates/jolt-transcript/src/lib.rs index 781f1db14d..b9e6b2e1f4 100644 --- a/crates/jolt-transcript/src/lib.rs +++ b/crates/jolt-transcript/src/lib.rs @@ -12,9 +12,6 @@ //! type's crate. Challenges are exactly uniform ([`Channel::challenge`]) or //! drawn from a field's small challenge set ([`Channel::challenge_small`]). //! The sponge is a type parameter ([`Sponge`]) bound into the [`ProtocolId`]. -//! -//! The legacy symmetric facade ([`Transcript`], [`AppendToTranscript`], -//! [`DigestTranscript`]) remains until every consumer moves to the channel. #![deny(missing_docs)] // In the jolt-verifier runtime closure: stricter panic and unsafe discipline @@ -34,21 +31,14 @@ )] mod channel; -#[cfg(feature = "spongefish")] -mod codec; -#[cfg(feature = "digest")] -mod digest; mod duplex; mod error; mod grinding; -mod legacy; #[cfg(feature = "transcript-poseidon")] mod poseidon; mod preview; mod protocol; mod prover; -#[cfg(feature = "spongefish")] -mod setup; mod site; mod sponge; mod verifier; @@ -76,63 +66,5 @@ pub use spongefish::instantiations::Keccak; pub use spongefish::DuplexSpongeInterface; pub use verifier::VerifierTranscript; -#[cfg(feature = "spongefish")] -pub use codec::BytesMsg; -#[cfg(feature = "digest")] -pub use digest::DigestTranscript; -#[cfg(feature = "spongefish")] -pub use legacy::SpongeTranscript; -pub use legacy::{ - append_length_prefixed, AppendToTranscript, Label, LabelWithCount, Transcript, U64Word, - MAX_LABEL_LEN, -}; -#[cfg(feature = "spongefish")] -pub use setup::{prover_transcript, transcript_builder, verifier_transcript, PROTOCOL_ID}; - -/// Source-compatible re-exports of legacy label / count / word helpers -/// under their `jolt_transcript::domain::*` path (matches the path used -/// by jolt-dory and earlier modular consumers). -pub mod domain { - pub use crate::legacy::{Label, LabelWithCount, U64Word}; -} - #[cfg(feature = "transcript-poseidon")] pub use poseidon::PoseidonSponge; - -#[cfg(feature = "transcript-blake2b")] -use blake2::{digest::consts::U32, Blake2b}; -#[cfg(all( - feature = "bn254", - any( - feature = "transcript-blake2b", - feature = "transcript-keccak", - feature = "transcript-poseidon" - ) -))] -use jolt_field::Fr; - -/// Fiat-Shamir transcript backed by Blake2b-512 (spongefish duplex sponge). -#[cfg(all(feature = "transcript-blake2b", feature = "bn254"))] -pub type Blake2bTranscript = SpongeTranscript; -/// Fiat-Shamir transcript backed by Blake2b-512 for an explicitly selected field. -#[cfg(all(feature = "transcript-blake2b", not(feature = "bn254")))] -pub type Blake2bTranscript = SpongeTranscript; - -/// Blake2b-256 chained-digest transcript used by the deployed proof format. -/// New protocols should use [`Blake2bTranscript`] instead. -#[cfg(all(feature = "transcript-blake2b", feature = "bn254"))] -pub type LegacyBlake2bTranscript = DigestTranscript, F>; -/// Legacy Blake2b-256 transcript for an explicitly selected field. -#[cfg(all(feature = "transcript-blake2b", not(feature = "bn254")))] -pub type LegacyBlake2bTranscript = DigestTranscript, F>; - -/// Fiat-Shamir transcript backed by Keccak-f1600 (spongefish duplex sponge). -#[cfg(all(feature = "transcript-keccak", feature = "bn254"))] -pub type KeccakTranscript = SpongeTranscript; -/// Fiat-Shamir transcript backed by Keccak-f1600 for an explicitly selected field. -#[cfg(all(feature = "transcript-keccak", not(feature = "bn254")))] -pub type KeccakTranscript = SpongeTranscript; - -/// Fiat-Shamir transcript backed by Circom-compatible BN254 Poseidon. -#[cfg(feature = "transcript-poseidon")] -pub type PoseidonTranscript = SpongeTranscript; diff --git a/crates/jolt-transcript/src/setup.rs b/crates/jolt-transcript/src/setup.rs deleted file mode 100644 index 4fef3c445c..0000000000 --- a/crates/jolt-transcript/src/setup.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! Spongefish domain-separator entry points for the native split-trait -//! surface. -//! -//! Two factory functions cover every Jolt caller: -//! -//! - [`prover_transcript`] — `(session, instance, sponge) → ProverState` -//! - [`verifier_transcript`] — `(session, instance, sponge, narg) → VerifierState` -//! -//! Both pre-bind [`PROTOCOL_ID`] and absorb the spongefish -//! `session`/`instance` steps internally. Wire format mirrors spongefish's -//! `DomainSeparator` exactly: -//! -//! ```text -//! PROTOCOL_ID (fixed, crate-wide) -//! ↓ -//! session(bytes) -//! ↓ -//! instance(32-byte digest) -//! ↓ -//! to_prover(sponge) | to_verifier(sponge, narg) -//! ``` -//! -//! Power users (e.g. `BlindFold` sub-verifiers, custom Encoding types) can -//! drop down to [`transcript_builder`], which returns spongefish's -//! `DomainSeparator` with PROTOCOL_ID pre-bound — the full type-state -//! builder is then available. - -use rand::rngs::StdRng; -use spongefish::{ - DomainSeparator, DuplexSpongeInterface, Encoding, ProverState, VerifierState, WithoutInstance, - WithoutSession, -}; - -use crate::codec::BytesMsg; - -/// Crate-wide spongefish protocol identifier (ASCII left, zero-padded to -/// 64 bytes). Pre-bound by every factory in this module; exposed so callers -/// who reach for [`transcript_builder`] can align on the same identifier. -pub const PROTOCOL_ID: [u8; 64] = pad_id(b"a16z/jolt-transcript/v1"); - -const fn pad_id(src: &[u8]) -> [u8; 64] { - assert!(src.len() <= 64, "protocol id exceeds 64 bytes"); - let mut buf = [0u8; 64]; - let mut i = 0; - while i < src.len() { - #[expect( - clippy::indexing_slicing, - reason = "i < src.len() <= 64 == buf.len(); const-evaluated, so an out-of-bounds index fails compilation" - )] - { - buf[i] = src[i]; - } - i += 1; - } - buf -} - -/// Empty `instance` value used by the legacy facade. Encodes to zero -/// bytes. The native factory functions use [`InstanceDigest`] instead. -#[derive(Clone, Copy, Debug, Default)] -pub struct EmptyInstance; - -impl Encoding<[u8]> for EmptyInstance { - fn encode(&self) -> impl AsRef<[u8]> { - [0u8; 0] - } -} - -/// 32-byte instance digest. Internal adapter — exposes `Encoding<[u8]>` -/// over a fixed-size digest so it slots into spongefish's `.instance(...)` -/// step. Public callers pass a plain `[u8; 32]` to [`prover_transcript`] / -/// [`verifier_transcript`]; the factory wraps internally. -#[derive(Clone, Copy, Debug, Default)] -struct InstanceDigest([u8; 32]); - -impl Encoding<[u8]> for InstanceDigest { - fn encode(&self) -> impl AsRef<[u8]> { - self.0 - } -} - -/// Build a prover transcript bound to `session` and `instance`. -/// -/// `session` is protocol-version bytes (e.g. `b"jolt-rv64imac/v1"`). -/// `instance` is the 32-byte digest of the public statement (typically -/// `Blake2b(CanonicalSerialize(public_state))`). Both are absorbed under -/// spongefish's domain-separator steps after [`PROTOCOL_ID`]. -/// -/// # Example -/// -/// ``` -/// use jolt_transcript::{prover_transcript, verifier_transcript, BytesMsg, ProverTranscript, VerifierTranscript}; -/// use spongefish::instantiations::Blake2b512; -/// -/// const SESSION: &[u8] = b"jolt-rv64imac/v1"; -/// let instance: [u8; 32] = [0xAB; 32]; -/// -/// let mut prover = prover_transcript(SESSION, instance, Blake2b512::default()); -/// ProverTranscript::::prover_message( -/// &mut prover, -/// &BytesMsg(b"commitment-a".to_vec()), -/// ); -/// let narg = ProverTranscript::::narg_string(&prover).to_vec(); -/// -/// let mut verifier = verifier_transcript(SESSION, instance, Blake2b512::default(), &narg); -/// let _: BytesMsg = -/// VerifierTranscript::::prover_message(&mut verifier).unwrap(); -/// VerifierTranscript::::check_eof(verifier).unwrap(); -/// ``` -#[must_use] -pub fn prover_transcript(session: &[u8], instance: [u8; 32], sponge: H) -> ProverState -where - H: DuplexSpongeInterface, -{ - DomainSeparator::new(PROTOCOL_ID) - .session(BytesMsg(session.to_vec())) - .instance(InstanceDigest(instance)) - .to_prover(sponge) -} - -/// Build a verifier transcript bound to `session` and `instance` over -/// `narg`. The verifier MUST pass the same `session`/`instance` as the -/// prover or the transcript states diverge. -#[must_use] -pub fn verifier_transcript<'a, H>( - session: &[u8], - instance: [u8; 32], - sponge: H, - narg: &'a [u8], -) -> VerifierState<'a, H> -where - H: DuplexSpongeInterface, -{ - DomainSeparator::new(PROTOCOL_ID) - .session(BytesMsg(session.to_vec())) - .instance(InstanceDigest(instance)) - .to_verifier(sponge, narg) -} - -/// Escape hatch returning spongefish's `DomainSeparator` with -/// [`PROTOCOL_ID`] pre-bound. Use only when the standard -/// `(session, instance)` shape doesn't fit — e.g. `BlindFold` sub-verifiers -/// that compose sub-domain-separators, or callers absorbing a non-32-byte -/// instance. -/// -/// Almost every Jolt caller should use [`prover_transcript`] / -/// [`verifier_transcript`] instead. -#[must_use] -pub fn transcript_builder() -> DomainSeparator { - DomainSeparator::new(PROTOCOL_ID) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pad_id_left_aligns_and_zero_pads_to_64_bytes() { - assert_eq!( - pad_id(b"a16z/jolt-transcript/v1"), - PROTOCOL_ID, - "PROTOCOL_ID must be the padded ASCII identifier" - ); - let short = pad_id(b"xy"); - assert_eq!(&short[..2], b"xy"); - assert!(short[2..].iter().all(|&byte| byte == 0)); - let full = [0x41u8; 64]; - assert_eq!(pad_id(&full), full, "a 64-byte id is passed through"); - } - - /// The instance digest is absorbed as its raw 32 bytes (no framing); - /// the legacy empty instance encodes to nothing. - #[test] - fn instance_encodings_are_raw_bytes() { - let digest = InstanceDigest([0xA5; 32]); - assert_eq!(digest.encode().as_ref(), &[0xA5; 32]); - assert!(EmptyInstance.encode().as_ref().is_empty()); - } -} diff --git a/crates/jolt-transcript/tests/blake2b_tests.rs b/crates/jolt-transcript/tests/blake2b_tests.rs deleted file mode 100644 index 7834f8693d..0000000000 --- a/crates/jolt-transcript/tests/blake2b_tests.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Tests for Blake2bTranscript implementation. - -mod common; - -use jolt_field::{Fr, Ring}; -use jolt_transcript::Blake2bTranscript; - -type B2b = Blake2bTranscript; - -transcript_tests!(B2b); - -#[test] -fn test_blake2b_known_vector() { - use ark_ff::PrimeField; - use jolt_transcript::Transcript; - - let mut transcript = Blake2bTranscript::::new(b"Jolt"); - transcript.append_bytes(&12345u64.to_be_bytes()); - let challenge: Fr = transcript.challenge(); - - // Pinned wire-format check: any change to PROTOCOL_ID, the session - // encoding, the append_bytes layout, or the challenge decoder will - // flip these bytes. Update only with an audit trail. - // - // Audit trail: `from_challenge_bytes` now builds the field element from the - // squeezed bytes via the 125-bit Montgomery-friendly decode rather than a - // plain little-endian reduction, so the canonical challenge value changed. - let expected: ark_bn254::Fr = ark_bn254::Fr::from_le_bytes_mod_order(&[ - 0xAE, 0x28, 0x1C, 0xAE, 0x3E, 0x93, 0x36, 0xA9, 0xE2, 0x57, 0xE0, 0x30, 0x2F, 0xC0, 0x48, - 0x2E, 0xCF, 0xC9, 0x89, 0x1B, 0x7D, 0x65, 0x4E, 0x6A, 0xB5, 0xEF, 0x55, 0x46, 0x36, 0x71, - 0x8A, 0x29, - ]); - let got: ark_bn254::Fr = challenge.into(); - assert_eq!(got, expected, "Blake2b known-vector regression"); -} - -#[test] -fn test_field_zero_one_distinct_states() { - use jolt_field::Fr; - use jolt_transcript::{AppendToTranscript, Transcript}; - use num_traits::{One, Zero}; - - let mut t_zero = Blake2bTranscript::::new(b"field_test"); - Fr::zero().append_to_transcript(&mut t_zero); - let c_zero: Fr = t_zero.challenge(); - - let mut t_one = Blake2bTranscript::::new(b"field_test"); - Fr::one().append_to_transcript(&mut t_one); - let c_one: Fr = t_one.challenge(); - - assert_ne!( - c_zero, c_one, - "Fr::zero() and Fr::one() must produce distinct transcript states" - ); -} - -#[test] -fn test_field_element_ordering_sensitivity() { - use jolt_transcript::{AppendToTranscript, Transcript}; - - let a = Fr::from_u64(42); - let b = Fr::from_u64(99); - - let mut t1 = Blake2bTranscript::::new(b"order_test"); - a.append_to_transcript(&mut t1); - b.append_to_transcript(&mut t1); - let c1: Fr = t1.challenge(); - - let mut t2 = Blake2bTranscript::::new(b"order_test"); - b.append_to_transcript(&mut t2); - a.append_to_transcript(&mut t2); - let c2: Fr = t2.challenge(); - - assert_ne!( - c1, c2, - "append(a, b) and append(b, a) must produce different challenges" - ); -} diff --git a/crates/jolt-transcript/tests/common/mod.rs b/crates/jolt-transcript/tests/common/mod.rs deleted file mode 100644 index 3faaf9e181..0000000000 --- a/crates/jolt-transcript/tests/common/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Common test utilities and standardized test suite for transcript implementations. - -/// Standardized test suite macro for any `Transcript` implementation. -/// -/// All comparisons are done through `challenge()` outputs since the -/// underlying spongefish duplex sponges do not expose internal state. -#[macro_export] -macro_rules! transcript_tests { - ($transcript_type:ty) => { - use jolt_transcript::Transcript; - use std::collections::HashSet; - - // Helper: drive a transcript through a closure and squeeze a challenge. - fn challenge_after( - label: &'static [u8], - f: F, - ) -> <$transcript_type as Transcript>::Challenge { - let mut t = <$transcript_type>::new(label); - f(&mut t); - t.challenge() - } - - #[test] - fn test_determinism() { - let c1 = challenge_after(b"determinism_test", |t| { - t.append_bytes(&42u64.to_be_bytes()); - t.append_bytes(b"hello world"); - }); - let c2 = challenge_after(b"determinism_test", |t| { - t.append_bytes(&42u64.to_be_bytes()); - t.append_bytes(b"hello world"); - }); - assert_eq!( - c1, c2, - "Identical operations must yield identical challenges" - ); - } - - #[test] - fn test_domain_separation() { - let c1 = challenge_after(b"protocol_a", |_| {}); - let c2 = challenge_after(b"protocol_b", |_| {}); - assert_ne!(c1, c2, "Different labels must produce different challenges"); - } - - #[test] - fn test_challenge_uniqueness() { - let mut transcript = <$transcript_type>::new(b"uniqueness_test"); - let mut challenges = HashSet::new(); - - for i in 0..10_000 { - let c = transcript.challenge(); - assert!( - challenges.insert(c), - "Duplicate challenge found at iteration {i}" - ); - } - } - - #[test] - fn test_append_changes_state() { - let baseline = challenge_after(b"mutation_test", |_| {}); - let after_append = challenge_after(b"mutation_test", |t| { - t.append_bytes(&1u64.to_be_bytes()); - }); - assert_ne!( - baseline, after_append, - "append must change observable challenge" - ); - } - - #[test] - fn test_order_matters() { - let c1 = challenge_after(b"order_test", |t| { - t.append_bytes(&1u64.to_be_bytes()); - t.append_bytes(&2u64.to_be_bytes()); - }); - let c2 = challenge_after(b"order_test", |t| { - t.append_bytes(&2u64.to_be_bytes()); - t.append_bytes(&1u64.to_be_bytes()); - }); - assert_ne!(c1, c2, "Order of appends must affect challenge"); - } - - #[test] - fn test_data_sensitivity() { - let c1 = challenge_after(b"data_test", |t| { - t.append_bytes(&0u64.to_be_bytes()); - }); - let c2 = challenge_after(b"data_test", |t| { - t.append_bytes(&1u64.to_be_bytes()); - }); - assert_ne!(c1, c2, "Different data must produce different challenges"); - } - - #[test] - fn test_empty_bytes() { - let baseline = challenge_after(b"empty_test", |_| {}); - let with_empty = challenge_after(b"empty_test", |t| { - t.append_bytes(&[]); - }); - assert_ne!( - baseline, with_empty, - "append_bytes(&[]) must observably change challenge" - ); - // Determinism for empty appends. - let with_empty_again = challenge_after(b"empty_test", |t| { - t.append_bytes(&[]); - }); - assert_eq!(with_empty, with_empty_again); - } - - #[test] - fn test_large_data() { - let mut transcript = <$transcript_type>::new(b"large_data_test"); - let large_data = vec![0xABu8; 10_000]; - - transcript.append_bytes(&large_data); - let _ = transcript.challenge(); - } - - #[test] - fn test_prover_verifier_consistency() { - let mut prover = <$transcript_type>::new(b"protocol"); - prover.append_bytes(&42u64.to_be_bytes()); - prover.append_bytes(b"commitment"); - let prover_challenge = prover.challenge(); - - let mut verifier = <$transcript_type>::new(b"protocol"); - verifier.append_bytes(&42u64.to_be_bytes()); - verifier.append_bytes(b"commitment"); - let verifier_challenge = verifier.challenge(); - - assert_eq!( - prover_challenge, verifier_challenge, - "Prover and verifier must derive identical challenges" - ); - } - - #[test] - fn test_default_delegates_to_new() { - let mut default_transcript = <$transcript_type>::default(); - let mut new_transcript = <$transcript_type>::new(b""); - assert_eq!( - default_transcript.challenge(), - new_transcript.challenge(), - "Default must delegate to new(b\"\")" - ); - } - - #[test] - #[should_panic(expected = "label must be at most 32 bytes")] - fn test_label_too_long() { - let long_label: &[u8; 33] = &[b'x'; 33]; - let _ = <$transcript_type>::new(long_label); - } - - #[test] - fn test_max_valid_label() { - let max_label: &[u8; 32] = &[b'L'; 32]; - let mut t1 = <$transcript_type>::new(max_label); - let mut t2 = <$transcript_type>::new(max_label); - assert_eq!(t1.challenge(), t2.challenge()); - } - - #[test] - fn test_challenge_vector() { - let mut transcript = <$transcript_type>::new(b"vector_test"); - let challenges = transcript.challenge_vector(5); - - assert_eq!(challenges.len(), 5); - - let unique: HashSet<_> = challenges.iter().collect(); - assert_eq!(unique.len(), 5, "All challenges in vector should be unique"); - } - }; -} diff --git a/crates/jolt-transcript/tests/digest_transcript_tests.rs b/crates/jolt-transcript/tests/digest_transcript_tests.rs deleted file mode 100644 index be6c3a3c9e..0000000000 --- a/crates/jolt-transcript/tests/digest_transcript_tests.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Tests for the legacy digest-chained transcript (`DigestTranscript`). -//! -//! `LegacyBlake2bTranscript` exists solely for byte compatibility with -//! `jolt-prover-legacy`'s chained-digest transcript, so beyond the shared -//! `transcript_tests!` behavioral suite, every state transition is checked -//! against an independently recomputed Blake2b-256 hash chain: -//! `state0 = H(label || 0^pad)`, append is `H(state || round || payload)`, -//! squeeze is `H(state || round)` with the 4-byte big-endian round counter -//! right-aligned in a 32-byte word. - -#![cfg(feature = "transcript-blake2b")] - -mod common; - -use blake2::{digest::consts::U32, Blake2b, Digest}; -use jolt_field::{CanonicalEncoding, Fr}; -use jolt_transcript::LegacyBlake2bTranscript; - -type LegacyB2b = LegacyBlake2bTranscript; -type Blake2b256 = Blake2b; - -transcript_tests!(LegacyB2b); - -fn manual_step(state: &[u8; 32], round: u32, payload: &[u8]) -> [u8; 32] { - let mut round_word = [0u8; 32]; - round_word[28..].copy_from_slice(&round.to_be_bytes()); - let mut hasher = Blake2b256::new(); - hasher.update(state); - hasher.update(round_word); - hasher.update(payload); - hasher.finalize().into() -} - -fn manual_label_state(label: &[u8]) -> [u8; 32] { - let mut padded = [0u8; 32]; - padded[..label.len()].copy_from_slice(label); - Blake2b256::digest(padded).into() -} - -#[test] -fn new_state_is_the_hash_of_the_zero_padded_label() { - let transcript = LegacyB2b::new(b"legacy-compat"); - assert_eq!(transcript.state(), manual_label_state(b"legacy-compat")); -} - -#[test] -fn appends_and_challenges_follow_the_legacy_hash_chain() { - let mut transcript = LegacyB2b::new(b"legacy-chain"); - let mut expected = manual_label_state(b"legacy-chain"); - - transcript.append_bytes(b"first message"); - expected = manual_step(&expected, 0, b"first message"); - assert_eq!( - transcript.state(), - expected, - "append must chain the payload" - ); - - transcript.append_bytes(&[]); - expected = manual_step(&expected, 1, &[]); - assert_eq!( - transcript.state(), - expected, - "an empty append must still advance the round counter" - ); - - let challenge = transcript.challenge(); - expected = manual_step(&expected, 2, &[]); - assert_eq!(transcript.state(), expected, "challenge must chain state"); - assert_eq!( - challenge, - Fr::from_challenge_bytes(&expected[..16]), - "challenge must decode the first 16 squeezed bytes" - ); - - let scalar = transcript.challenge_scalar(); - expected = manual_step(&expected, 3, &[]); - assert_eq!( - scalar, - Fr::from_scalar_challenge_bytes(&expected[..16]), - "scalar challenge must use the non-optimized decoding path" - ); -} - -#[test] -fn multi_block_squeeze_chains_one_hash_per_32_byte_block() { - let mut transcript = LegacyB2b::new(b"legacy-blocks"); - let mut out = [0u8; 80]; - transcript.raw_challenge_bytes(&mut out); - - let state0 = manual_label_state(b"legacy-blocks"); - let block1 = manual_step(&state0, 0, &[]); - let block2 = manual_step(&block1, 1, &[]); - let block3 = manual_step(&block2, 2, &[]); - assert_eq!(&out[..32], &block1); - assert_eq!(&out[32..64], &block2); - assert_eq!(&out[64..], &block3[..16], "the final block is truncated"); - assert_eq!( - transcript.state(), - block3, - "three blocks must advance the chain three rounds" - ); -} - -#[test] -fn clone_replays_identically_until_inputs_diverge() { - let mut original = LegacyB2b::new(b"legacy-clone"); - original.append_bytes(b"shared prefix"); - let mut cloned = original.clone(); - assert_eq!(original.state(), cloned.state()); - assert_eq!( - original.challenge(), - cloned.challenge(), - "clones must squeeze identical challenges" - ); - - original.append_bytes(b"left"); - cloned.append_bytes(b"right"); - assert_ne!( - original.state(), - cloned.state(), - "divergent appends must yield divergent states" - ); -} - -#[test] -fn debug_output_names_the_engine_and_round_counter() { - let transcript = LegacyB2b::new(b"legacy-debug"); - let output = format!("{transcript:?}"); - assert!( - output.contains("DigestTranscript") && output.contains("n_rounds"), - "unexpected Debug output: {output}" - ); -} - -/// The `challenge_scalar_powers` default must equal independently computed -/// powers of the scalar squeezed by an identically driven transcript. -#[test] -fn challenge_scalar_powers_are_the_geometric_sequence_of_one_squeeze() { - let mut transcript = LegacyB2b::new(b"legacy-powers"); - transcript.append_bytes(b"bind"); - let mut reference = transcript.clone(); - - let powers = transcript.challenge_scalar_powers(4); - let gamma = reference.challenge_scalar(); - assert_eq!( - powers, - vec![Fr::from(1u128), gamma, gamma * gamma, gamma * gamma * gamma], - "powers must be [1, gamma, gamma^2, gamma^3] of a single squeeze" - ); - assert_eq!( - transcript.state(), - reference.state(), - "challenge_scalar_powers must consume exactly one squeeze" - ); -} diff --git a/crates/jolt-transcript/tests/keccak_tests.rs b/crates/jolt-transcript/tests/keccak_tests.rs deleted file mode 100644 index ce68cc6dca..0000000000 --- a/crates/jolt-transcript/tests/keccak_tests.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Tests for KeccakTranscript implementation. - -mod common; - -use jolt_field::Fr; -use jolt_transcript::KeccakTranscript; - -type Kec = KeccakTranscript; - -transcript_tests!(Kec); - -#[test] -fn test_keccak_known_vector() { - use ark_ff::PrimeField; - use jolt_transcript::Transcript; - - let mut transcript = KeccakTranscript::::new(b"Jolt"); - transcript.append_bytes(&12345u64.to_be_bytes()); - let challenge: Fr = transcript.challenge(); - - // Pinned wire-format check; see Blake2b counterpart for rationale. - let expected: ark_bn254::Fr = ark_bn254::Fr::from_le_bytes_mod_order(&[ - 0x2E, 0xDF, 0x34, 0x68, 0x85, 0xEE, 0x1C, 0x8B, 0xEC, 0xBD, 0x68, 0xA6, 0x3E, 0x23, 0x00, - 0x9F, 0x10, 0x00, 0xBC, 0xA3, 0xC4, 0xBA, 0x1C, 0xF4, 0x63, 0xDC, 0x84, 0x8D, 0x45, 0xD9, - 0xDD, 0x1E, - ]); - let got: ark_bn254::Fr = challenge.into(); - assert_eq!(got, expected, "Keccak known-vector regression"); -} diff --git a/crates/jolt-transcript/tests/known_answers.rs b/crates/jolt-transcript/tests/known_answers.rs index cee86da8a6..4e322ad4cc 100644 --- a/crates/jolt-transcript/tests/known_answers.rs +++ b/crates/jolt-transcript/tests/known_answers.rs @@ -2,11 +2,6 @@ //! message kind, challenge kind, and grinding. Any change to the protocol-id //! derivation, framing, encoding, or challenge mapping changes these bytes. -#![cfg(all( - feature = "transcript-blake2b", - feature = "transcript-keccak", - feature = "transcript-poseidon" -))] #![expect(clippy::unwrap_used, reason = "test crate")] use jolt_field::{CanonicalBytes, Fr}; diff --git a/crates/jolt-transcript/tests/legacy_digest_tests.rs b/crates/jolt-transcript/tests/legacy_digest_tests.rs deleted file mode 100644 index ba220b9d54..0000000000 --- a/crates/jolt-transcript/tests/legacy_digest_tests.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Known-answer tests for `LegacyBlake2bTranscript` (`DigestTranscript`). -//! -//! This transcript exists solely for byte-compatibility with the -//! `jolt-prover-legacy` Fiat-Shamir transcript, so prover/verifier round-trips -//! cannot catch a chaining regression — both sides would drift together. The -//! hex vectors below were generated with an independent BLAKE2b-256 -//! implementation (Python `hashlib.blake2b(digest_size=32)`) from the -//! documented formula: -//! -//! ```text -//! state_0 = H(label zero-padded to 32 bytes) -//! round_word(n) = 28 zero bytes || n as u32 big-endian -//! append(bytes) : state' = H(state || round_word(n) || bytes), n += 1 -//! squeeze : out = H(state || round_word(n)), state' = out, n += 1 -//! ``` - -use jolt_field::{CanonicalEncoding, Fr}; -use jolt_transcript::{Label, LabelWithCount, LegacyBlake2bTranscript, Transcript, U64Word}; - -type T = LegacyBlake2bTranscript; - -const KAT_LABEL: &[u8] = b"jolt transcript kat"; - -fn hex(bytes: &[u8]) -> String { - use std::fmt::Write; - bytes.iter().fold(String::new(), |mut out, byte| { - // writing to a String is infallible - let _ = write!(out, "{byte:02x}"); - out - }) -} - -#[test] -fn initial_state_is_hash_of_zero_padded_label() { - let transcript = T::new(KAT_LABEL); - assert_eq!( - hex(&transcript.state()), - "b57f1b2110a7b1720489cf99300d3a5641498671160090254ef89dc6f926b19e", - ); -} - -#[test] -fn empty_label_hashes_thirty_two_zero_bytes() { - let transcript = T::new(b""); - assert_eq!( - hex(&transcript.state()), - "89eb0d6a8a691dae2cd15ed0369931ce0a949ecafa5c3f93f8121833646e15c3", - ); -} - -#[test] -fn append_bytes_chains_state_with_round_counter() { - let mut transcript = T::new(KAT_LABEL); - transcript.append_bytes(b"jolt says hello"); - assert_eq!( - hex(&transcript.state()), - "e0581c34f7f60080a2b1cac4282e5420a88034e11a876c47f94374122fa1f905", - ); -} - -#[test] -fn challenge_draw_rotates_state_and_truncates_to_sixteen_bytes() { - const EXPECTED_CHUNK: &str = "25a496912b5b75d6504cbc204a25b13d6121e6266ac056589689c43dc06f9142"; - - let mut transcript = T::new(KAT_LABEL); - transcript.append_bytes(b"jolt says hello"); - let mut buf = [0u8; 16]; - transcript.raw_challenge_bytes(&mut buf); - - assert_eq!( - hex(&buf), - &EXPECTED_CHUNK[..32], - "low 16 bytes of the chunk" - ); - assert_eq!( - hex(&transcript.state()), - EXPECTED_CHUNK, - "state becomes the squeezed chunk" - ); -} - -#[test] -fn multi_chunk_draw_concatenates_successive_squeezes() { - let mut transcript = T::new(KAT_LABEL); - let mut out = [0u8; 48]; - transcript.raw_challenge_bytes(&mut out); - assert_eq!( - hex(&out), - "0db99faadc348c2ecdd3faf44d5f94cf27b2a8b93e757d60eae8a2bc1c057d95\ - b6d6e19661c63658f73f61b9f25d4e70", - ); -} - -#[test] -fn challenge_decodes_the_pinned_bytes_via_from_challenge_bytes() { - let mut transcript = T::new(KAT_LABEL); - transcript.append_bytes(b"jolt says hello"); - let challenge: Fr = transcript.challenge(); - - let pinned: [u8; 16] = [ - 0x25, 0xa4, 0x96, 0x91, 0x2b, 0x5b, 0x75, 0xd6, 0x50, 0x4c, 0xbc, 0x20, 0x4a, 0x25, 0xb1, - 0x3d, - ]; - assert_eq!(challenge, Fr::from_challenge_bytes(&pinned)); -} - -#[test] -fn challenge_scalar_uses_the_scalar_decode_path() { - let mut transcript = T::new(KAT_LABEL); - transcript.append_bytes(b"jolt says hello"); - let challenge: Fr = transcript.challenge_scalar(); - - let pinned: [u8; 16] = [ - 0x25, 0xa4, 0x96, 0x91, 0x2b, 0x5b, 0x75, 0xd6, 0x50, 0x4c, 0xbc, 0x20, 0x4a, 0x25, 0xb1, - 0x3d, - ]; - assert_eq!(challenge, Fr::from_scalar_challenge_bytes(&pinned)); -} - -#[test] -fn challenge_scalar_powers_are_consecutive_powers_of_one_draw() { - let mut probe = T::new(KAT_LABEL); - let gamma: Fr = probe.challenge_scalar(); - - let mut transcript = T::new(KAT_LABEL); - let powers = transcript.challenge_scalar_powers(4); - - assert_eq!(powers[0], Fr::from(1u64)); - assert_eq!(powers[1], gamma); - assert_eq!(powers[2], gamma * gamma); - assert_eq!(powers[3], gamma * gamma * gamma); - assert_eq!( - transcript.state(), - probe.state(), - "powers consume exactly one draw" - ); -} - -#[test] -fn label_helper_packs_zero_padded_thirty_two_byte_word() { - let mut via_helper = T::new(KAT_LABEL); - via_helper.append(&Label(b"opening claim")); - - let mut explicit = T::new(KAT_LABEL); - let mut word = [0u8; 32]; - word[..13].copy_from_slice(b"opening claim"); - explicit.append_bytes(&word); - - assert_eq!(via_helper.state(), explicit.state()); -} - -#[test] -fn label_with_count_packs_label_and_big_endian_count() { - let mut via_helper = T::new(KAT_LABEL); - via_helper.append(&LabelWithCount(b"round polys", 7)); - - let mut explicit = T::new(KAT_LABEL); - let mut word = [0u8; 32]; - word[..11].copy_from_slice(b"round polys"); - word[24..].copy_from_slice(&7u64.to_be_bytes()); - explicit.append_bytes(&word); - - assert_eq!(via_helper.state(), explicit.state()); -} - -#[test] -fn u64_word_packs_left_padded_big_endian_value() { - let mut via_helper = T::new(KAT_LABEL); - via_helper.append(&U64Word(0xDEAD_BEEF)); - - let mut explicit = T::new(KAT_LABEL); - let mut word = [0u8; 32]; - word[24..].copy_from_slice(&0xDEAD_BEEFu64.to_be_bytes()); - explicit.append_bytes(&word); - - assert_eq!(via_helper.state(), explicit.state()); -} - -#[test] -#[should_panic(expected = "label must be at most")] -fn overlong_transcript_label_is_rejected() { - let _ = T::new(b"this label is thirty-three bytes!"); -} diff --git a/crates/jolt-transcript/tests/narg.rs b/crates/jolt-transcript/tests/narg.rs index 3676b32f08..1253a4a8b6 100644 --- a/crates/jolt-transcript/tests/narg.rs +++ b/crates/jolt-transcript/tests/narg.rs @@ -1,20 +1,13 @@ //! Prover/verifier symmetry, rejection, and wire-format pins for the NARG //! transcript. -#![cfg(all( - feature = "bn254", - feature = "transcript-blake2b", - feature = "transcript-keccak", - feature = "transcript-poseidon" -))] #![expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "tests")] use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr, Ring}; use jolt_transcript::{ - Channel, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, TranscriptError, - VerifierTranscript, SMALL_CHALLENGE_BYTES, + Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, + TranscriptError, VerifierTranscript, SMALL_CHALLENGE_BYTES, }; -use spongefish::instantiations::{Blake2b512, Keccak}; const SESSION: &[u8] = b"narg-tests"; diff --git a/crates/jolt-transcript/tests/narg_eof_tests.rs b/crates/jolt-transcript/tests/narg_eof_tests.rs deleted file mode 100644 index 33f4e4021f..0000000000 --- a/crates/jolt-transcript/tests/narg_eof_tests.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Soundness regression: NARG strings appended with trailing garbage must -//! be rejected by `check_eof`. Without this check, `valid_proof || garbage` -//! would round-trip through verification, making top-level proof bytes -//! malleable. -//! -//! See PR #1455 review for the original report. - -#![cfg(feature = "transcript-blake2b")] -#![expect(clippy::expect_used, reason = "tests")] - -use jolt_transcript::{prover_transcript, verifier_transcript, BytesMsg, PROTOCOL_ID}; -use spongefish::instantiations::Blake2b512; - -const SESSION: &[u8] = b"narg-eof-test"; -const INSTANCE: [u8; 32] = [0x42; 32]; - -fn build_valid_narg(messages: &[&[u8]]) -> Vec { - let mut prover = prover_transcript(SESSION, INSTANCE, Blake2b512::default()); - for m in messages { - prover.prover_message(&BytesMsg(m.to_vec())); - } - prover.narg_string().to_vec() -} - -fn build_verifier(narg: &[u8]) -> spongefish::VerifierState<'_, Blake2b512> { - verifier_transcript(SESSION, INSTANCE, Blake2b512::default(), narg) -} - -#[test] -fn check_eof_accepts_exact_narg() { - let msgs: &[&[u8]] = &[b"alpha", b"beta", b"gamma"]; - let narg = build_valid_narg(msgs); - - let mut verifier = build_verifier(&narg); - for expected in msgs { - let got: BytesMsg = verifier - .prover_message() - .expect("valid prover message must deserialize"); - assert_eq!(got.as_slice(), *expected); - } - verifier - .check_eof() - .expect("exact narg must pass check_eof"); -} - -#[test] -fn check_eof_rejects_trailing_garbage() { - let msgs: &[&[u8]] = &[b"alpha", b"beta", b"gamma"]; - let mut narg = build_valid_narg(msgs); - let original_len = narg.len(); - narg.extend_from_slice(&[0xFFu8; 7]); - - let mut verifier = build_verifier(&narg); - for expected in msgs { - let got: BytesMsg = verifier - .prover_message() - .expect("valid prefix must deserialize"); - assert_eq!(got.as_slice(), *expected); - } - let result = verifier.check_eof(); - assert!( - result.is_err(), - "narg with {} trailing bytes (original_len={}) must fail check_eof", - 7, - original_len, - ); -} - -#[test] -fn check_eof_rejects_single_trailing_byte() { - let narg = { - let mut n = build_valid_narg(&[b"only"]); - n.push(0x00); - n - }; - let mut verifier = build_verifier(&narg); - let _: BytesMsg = verifier - .prover_message() - .expect("valid prefix must deserialize"); - assert!( - verifier.check_eof().is_err(), - "even a single trailing byte must fail check_eof", - ); -} - -#[test] -fn check_eof_rejects_unread_messages() { - let msgs: &[&[u8]] = &[b"alpha", b"beta"]; - let narg = build_valid_narg(msgs); - - let mut verifier = build_verifier(&narg); - let _: BytesMsg = verifier - .prover_message() - .expect("first message must deserialize"); - assert!( - verifier.check_eof().is_err(), - "leaving prover messages unread must fail check_eof", - ); -} - -#[test] -fn protocol_id_byte_check() { - assert_eq!(&PROTOCOL_ID[..23], b"a16z/jolt-transcript/v1"); - assert!(PROTOCOL_ID[23..].iter().all(|&b| b == 0)); -} diff --git a/crates/jolt-transcript/tests/poseidon_tests.rs b/crates/jolt-transcript/tests/poseidon_tests.rs deleted file mode 100644 index d0008ce101..0000000000 --- a/crates/jolt-transcript/tests/poseidon_tests.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Tests for PoseidonTranscript implementation. - -mod common; - -use jolt_field::Fr; -use jolt_transcript::PoseidonTranscript; - -type Pos = PoseidonTranscript; - -transcript_tests!(Pos); - -#[test] -fn test_poseidon_known_vector() { - use ark_ff::PrimeField; - use jolt_transcript::Transcript; - - let mut transcript = PoseidonTranscript::::new(b"Jolt"); - transcript.append_bytes(&12345u64.to_be_bytes()); - let challenge: Fr = transcript.challenge(); - - // Pinned wire-format check; see Blake2b counterpart for rationale. - let expected: ark_bn254::Fr = ark_bn254::Fr::from_le_bytes_mod_order(&[ - 0xF7, 0x54, 0x3B, 0x32, 0x71, 0x47, 0x68, 0xEE, 0x04, 0x09, 0xEC, 0xAB, 0x9B, 0x91, 0x2E, - 0x8A, 0xD0, 0x51, 0x7E, 0x7C, 0x6E, 0xB2, 0xB8, 0x77, 0x52, 0x59, 0x2B, 0x10, 0x38, 0x67, - 0x78, 0x06, - ]); - let got: ark_bn254::Fr = challenge.into(); - assert_eq!(got, expected, "Poseidon known-vector regression"); -} From 7af887eeadf6d341d59ffc58b8baa4a5b5bfd4b9 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 17:12:07 -0400 Subject: [PATCH 31/62] feat(verifier): decode a proof's header without verifying it `JoltProof::header::()` reads the argument string's first message, so callers can report the trace length and configs of a proof. The modinv and advice-demo examples use it to print the trace length. --- crates/jolt-verifier/src/proof.rs | 12 +++++++++++- .../tests/fs_inventory/delegation-sites.inventory | 1 + examples/advice-demo/src/main.rs | 5 ++++- examples/modinv/src/main.rs | 5 ++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/jolt-verifier/src/proof.rs b/crates/jolt-verifier/src/proof.rs index 8f4819fe1a..cd26e867b1 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -12,7 +12,7 @@ use jolt_openings::CommitmentScheme; use jolt_transcript::{ProverTranscript, Sponge, VerifierTranscript}; use serde::{Deserialize, Serialize}; -use crate::{config::JoltProtocolConfig, num, VerifierError}; +use crate::{config::JoltProtocolConfig, jolt_protocol_id, num, VerifierError, JOLT_SESSION}; /// A Jolt proof: the argument string, plus the protocol axes it was produced /// under so a build mismatch is reported as such instead of as a transcript @@ -24,6 +24,16 @@ pub struct JoltProof { pub narg: Vec, } +impl JoltProof { + /// Decodes the proof header (the argument string's first message) on + /// sponge `H`, without verifying the proof. + pub fn header(&self) -> Result { + let mut transcript = + VerifierTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION, &self.narg); + ProofHeader::receive(&mut transcript) + } +} + /// The prover-chosen shape parameters, sent first. #[expect(non_snake_case, reason = "Preserves the protocol's parameter name.")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory index 327c5d8f27..746f782baf 100644 --- a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory @@ -143,6 +143,7 @@ jolt-sumcheck::recorder.rs::CommittedSumcheckRecorder<'_,F,VC,R>::absorb_round:: jolt-sumcheck::recorder.rs::CommittedSumcheckRecorder<'_,F,VC,R>::finish::.finish#0 jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::receive_full_round#0 jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::receive_compressed_round#0 +jolt-verifier::proof.rs::JoltProof::header::ProofHeader::receive#0 jolt-verifier::stages/relations.rs::receive_member_openings::receive_member_openings_except#0 jolt-verifier::stages/stage1/verify.rs::verify::.draw_challenges#2 jolt-verifier::stages/stage1/verify.rs::verify::.verify_clear#3 diff --git a/examples/advice-demo/src/main.rs b/examples/advice-demo/src/main.rs index 0c9d0c899d..d8453ff907 100644 --- a/examples/advice-demo/src/main.rs +++ b/examples/advice-demo/src/main.rs @@ -25,7 +25,10 @@ pub fn main() { let prove_time = now.elapsed(); info!("Prover runtime: {} s", prove_time.as_secs_f64()); - let trace_length_advice = proof.trace_length; + let trace_length_advice = proof + .header::() + .expect("proof header") + .trace_length; let is_valid_advice = verify_advice_demo(n, a, b, output, program_io.panic, proof); diff --git a/examples/modinv/src/main.rs b/examples/modinv/src/main.rs index 9d0ddcb3f4..11f345244f 100644 --- a/examples/modinv/src/main.rs +++ b/examples/modinv/src/main.rs @@ -27,7 +27,10 @@ pub fn main() { let prove_time_advice = now.elapsed(); info!("Prover runtime: {} s", prove_time_advice.as_secs_f64()); - let trace_length_advice = proof_advice.trace_length; + let trace_length_advice = proof_advice + .header::() + .expect("proof header") + .trace_length; let is_valid_advice = verify_modinv(a, m, output_advice, io_device_advice.panic, proof_advice); From bff39da9c5298cdb9060a95d6c95f68ebf1bff47 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 17:16:53 -0400 Subject: [PATCH 32/62] refactor(verifier)!: drop the per-stage wire claim aggregates Proof claims travel only as transcript messages now, so `Stage1OutputClaims`, `Stage2OutputClaims`, and the field-inline serde codec for the stage-2 batch had no producer. The prover's stage outputs also stop cloning their output claims into a separate field; downstream code and tests read `clear_output`. --- crates/jolt-prover/src/stages/stage1.rs | 10 +- crates/jolt-prover/src/stages/stage2.rs | 21 ++-- crates/jolt-prover/src/stages/stage3.rs | 9 +- crates/jolt-prover/src/stages/stage4.rs | 9 +- crates/jolt-prover/src/stages/stage5.rs | 9 +- crates/jolt-prover/src/stages/stage6a.rs | 8 +- crates/jolt-prover/src/stages/stage6b.rs | 20 ++-- crates/jolt-prover/src/stages/stage7.rs | 7 +- .../src/stages/stage1/outputs.rs | 18 ---- .../src/stages/stage2/outputs.rs | 99 ------------------- 10 files changed, 34 insertions(+), 176 deletions(-) diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index 9391269378..eee0bea169 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -23,7 +23,7 @@ use jolt_verifier::stages::stage1::outer_remainder::{ outer_remainder_input_values_from_uniskip_output, OuterRemainder, }; use jolt_verifier::stages::stage1::outputs::{ - Stage1BatchInputClaims, Stage1BatchSumchecks, Stage1ClearOutput, Stage1OutputClaims, + Stage1BatchInputClaims, Stage1BatchSumchecks, Stage1ClearOutput, }; use jolt_verifier::stages::uniskip::draw_spartan_outer_tau; use jolt_witness::JoltWitnessPlane; @@ -31,10 +31,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{ProverError, StageProver as _}; -/// Stage 1's outputs: the two wire proofs, the wire claims, and the -/// verifier-typed cross-stage carrier downstream stages consume. +/// Stage 1's outputs: the verifier-typed cross-stage carrier downstream stages consume. pub struct Stage1ProverOutput { - pub claims: Stage1OutputClaims, pub clear_output: Stage1ClearOutput, #[cfg(feature = "zk")] pub uniskip_witness: CommittedSumcheckWitness, @@ -116,10 +114,8 @@ where #[cfg(feature = "zk")] let committed_witness = proved.witness; - let claims = Stage1OutputClaims::new(proved_uniskip.output_claim, proved.output_claims.clone()); let clear_output = Stage1ClearOutput::new(proved.output_claims, proved.output_points); Ok(Stage1ProverOutput { - claims, clear_output, #[cfg(feature = "zk")] uniskip_witness: proved_uniskip.witness, @@ -168,7 +164,7 @@ mod field_inline_round_trip { ) .unwrap(); - let field_inline_outer = &out.claims.outer.outer_remainder.field_inline; + let field_inline_outer = &out.clear_output.output_values.outer_remainder.field_inline; // The appendage values are honest evaluations: each field-inline cycle-domain // column's MLE at the stage-1 cycle binding (`tau_low`, the point diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index 5ec4fbfe75..70d725edbe 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -33,9 +33,7 @@ use jolt_verifier::stages::stage1::Stage1ClearOutput; #[cfg(feature = "field-inline")] use jolt_verifier::stages::stage2::field_registers_claim_reduction::FieldRegistersClaimReduction; use jolt_verifier::stages::stage2::instruction_claim_reduction::InstructionClaimReduction; -use jolt_verifier::stages::stage2::outputs::{ - Stage2BatchSumchecks, Stage2ClearOutput, Stage2OutputClaims, -}; +use jolt_verifier::stages::stage2::outputs::{Stage2BatchSumchecks, Stage2ClearOutput}; use jolt_verifier::stages::stage2::product_remainder::ProductRemainder; use jolt_verifier::stages::stage2::product_uniskip::{ product_uniskip_input_values_from_stage1, ProductUniskip, @@ -51,10 +49,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{ProverConfig, ProverError, StageProver as _}; -/// Stage 2's outputs: the two wire proofs, the wire claims, and the -/// verifier-typed cross-stage carrier downstream stages consume. +/// Stage 2's outputs: the verifier-typed cross-stage carrier downstream stages consume. pub struct Stage2ProverOutput { - pub claims: Stage2OutputClaims, pub clear_output: Stage2ClearOutput, #[cfg(feature = "zk")] pub uniskip_witness: CommittedSumcheckWitness, @@ -188,9 +184,7 @@ where #[cfg(feature = "zk")] let committed_witness = proved.witness; - let claims = Stage2OutputClaims::new(proved_uniskip.output_claim, proved.output_claims.clone()); Ok(Stage2ProverOutput { - claims, clear_output: Stage2ClearOutput { output_values: proved.output_claims, output_points: proved.output_points, @@ -254,8 +248,15 @@ mod field_inline_round_trip { // The field-inline product appendage is carried, and the spec's alias table // holds on honest data: the field-inline claim-reduction member outputs equal // the appendage values polynomial-for-polynomial. - let appendage = &out.claims.batch_outputs.product_remainder.field_inline; - let reduction = &out.claims.batch_outputs.field_registers_claim_reduction; + let appendage = &out + .clear_output + .output_values + .product_remainder + .field_inline; + let reduction = &out + .clear_output + .output_values + .field_registers_claim_reduction; assert_eq!(reduction.rs1_value, appendage.rs1_value); assert_eq!(reduction.rs2_value, appendage.rs2_value); assert_eq!(reduction.rd_value, appendage.rd_value); diff --git a/crates/jolt-prover/src/stages/stage3.rs b/crates/jolt-prover/src/stages/stage3.rs index 611e9fc4ba..bcc83993ba 100644 --- a/crates/jolt-prover/src/stages/stage3.rs +++ b/crates/jolt-prover/src/stages/stage3.rs @@ -20,8 +20,7 @@ use jolt_verifier::sites::STAGE3; use jolt_verifier::stages::stage1::Stage1ClearOutput; use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::{ - InstructionInput, RegistersClaimReduction, SpartanShift, Stage3ClearOutput, Stage3OutputClaims, - Stage3Sumchecks, + InstructionInput, RegistersClaimReduction, SpartanShift, Stage3ClearOutput, Stage3Sumchecks, }; use jolt_verifier::stages::stage3::stage3_input_values_from_upstream; use jolt_witness::JoltWitnessPlane; @@ -29,11 +28,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{ProverConfig, ProverError, StageProver as _}; -/// Stage 3's outputs: the wire proof, the wire claims (the raw batch -/// aggregate — no uni-skip wrapper), and the verifier-typed cross-stage -/// carrier downstream stages consume. +/// Stage 3's outputs: the verifier-typed cross-stage carrier downstream stages consume. pub struct Stage3ProverOutput { - pub claims: Stage3OutputClaims, pub clear_output: Stage3ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -94,7 +90,6 @@ where let committed_witness = proved.witness; Ok(Stage3ProverOutput { - claims: proved.output_claims.clone(), clear_output: Stage3ClearOutput { output_values: proved.output_claims, output_points: proved.output_points, diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index 62a686caa6..33e82821ef 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -28,9 +28,7 @@ use jolt_verifier::stages::stage2::outputs::Stage2ClearOutput; use jolt_verifier::stages::stage3::outputs::Stage3ClearOutput; #[cfg(feature = "field-inline")] use jolt_verifier::stages::stage4::field_registers_read_write_checking::FieldRegistersReadWriteChecking; -use jolt_verifier::stages::stage4::outputs::{ - Stage4ClearOutput, Stage4OutputClaims, Stage4Sumchecks, -}; +use jolt_verifier::stages::stage4::outputs::{Stage4ClearOutput, Stage4Sumchecks}; use jolt_verifier::stages::stage4::ram_val_check::RamValCheck; use jolt_verifier::stages::stage4::registers_read_write_checking::RegistersReadWriteChecking; use jolt_verifier::stages::stage4::{ @@ -44,10 +42,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _}; -/// Stage 4's outputs: the wire proof, the wire claims, and the verifier-typed -/// cross-stage carrier downstream stages consume. +/// Stage 4's outputs: the verifier-typed cross-stage carrier downstream stages consume. pub struct Stage4ProverOutput { - pub claims: Stage4OutputClaims, pub clear_output: Stage4ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -225,7 +221,6 @@ where let committed_witness = proved.witness; Ok(Stage4ProverOutput { - claims: proved.output_claims.clone(), clear_output: Stage4ClearOutput { output_values: proved.output_claims, output_points: proved.output_points, diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index cd3f3e5916..1808b6b45d 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -27,9 +27,7 @@ use jolt_verifier::stages::stage4::outputs::Stage4ClearOutput; #[cfg(feature = "field-inline")] use jolt_verifier::stages::stage5::field_registers_val_evaluation::FieldRegistersValEvaluation; use jolt_verifier::stages::stage5::instruction_read_raf::InstructionReadRaf; -use jolt_verifier::stages::stage5::outputs::{ - Stage5ClearOutput, Stage5OutputClaims, Stage5Sumchecks, -}; +use jolt_verifier::stages::stage5::outputs::{Stage5ClearOutput, Stage5Sumchecks}; use jolt_verifier::stages::stage5::ram_ra_claim_reduction::RamRaClaimReduction; use jolt_verifier::stages::stage5::registers_val_evaluation::RegistersValEvaluation; use jolt_verifier::stages::stage5::{ @@ -41,10 +39,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _}; -/// Stage 5's outputs: the wire proof, the wire claims, and the verifier-typed -/// cross-stage carrier downstream stages consume. +/// Stage 5's outputs: the verifier-typed cross-stage carrier downstream stages consume. pub struct Stage5ProverOutput { - pub claims: Stage5OutputClaims, pub clear_output: Stage5ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -116,7 +112,6 @@ where let instruction_r_address = proved.output_points.instruction_r_address(); Ok(Stage5ProverOutput { - claims: proved.output_claims.clone(), clear_output: Stage5ClearOutput { challenges, output_values: proved.output_claims, diff --git a/crates/jolt-prover/src/stages/stage6a.rs b/crates/jolt-prover/src/stages/stage6a.rs index a028455b66..7992fcc6a8 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -37,8 +37,7 @@ use jolt_verifier::stages::stage6a::bytecode_read_raf::bytecode_read_raf_address #[cfg(feature = "field-inline")] use jolt_verifier::stages::stage6a::field_inline::field_inline_bytecode_read_raf_address_phase_input_values_from_upstream; use jolt_verifier::stages::stage6a::outputs::{ - Stage6aCarriedChallenges, Stage6aClearOutput, Stage6aInputClaims, Stage6aOutputClaims, - Stage6aSumchecks, + Stage6aCarriedChallenges, Stage6aClearOutput, Stage6aInputClaims, Stage6aSumchecks, }; use jolt_verifier::CheckedInputs; use jolt_witness::JoltWitnessPlane; @@ -46,10 +45,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _}; -/// Stage 6a's outputs: the wire proof, the wire claims, and the verifier-typed -/// cross-stage carrier stage 6b consumes. +/// Stage 6a's outputs: the verifier-typed cross-stage carrier stage 6b consumes. pub struct Stage6aProverOutput { - pub claims: Stage6aOutputClaims, pub clear_output: Stage6aClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -172,7 +169,6 @@ where let committed_witness = proved.witness; Ok(Stage6aProverOutput { - claims: proved.output_claims.clone(), clear_output: Stage6aClearOutput { output_values: proved.output_claims, output_points: proved.output_points, diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index 4ec0a6ab02..3750c1ecfc 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -40,9 +40,7 @@ use jolt_verifier::stages::stage6a::outputs::Stage6aClearOutput; use jolt_verifier::stages::stage6b::batch::{Stage6bBuildParts, Stage6bDraws}; #[cfg(not(feature = "akita"))] use jolt_verifier::stages::stage6b::committed_reduction_cycle_phase::advice_reference_point_from_upstream; -use jolt_verifier::stages::stage6b::outputs::{ - Stage6bClearOutput, Stage6bOutputClaims, Stage6bSumchecks, -}; +use jolt_verifier::stages::stage6b::outputs::{Stage6bClearOutput, Stage6bSumchecks}; use jolt_verifier::stages::stage6b::{ stage6b_input_points_from_upstream, stage6b_input_values_from_upstream, }; @@ -52,12 +50,10 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _}; -/// Stage 6b's outputs: the wire proof, the wire claims, and the verifier-typed -/// cross-stage carrier stage 7 consumes. The precommitted reduction state -/// that spans into stage 7's address phase travels as `ProofSession` carries, -/// not output fields. +/// Stage 6b's outputs: the verifier-typed cross-stage carrier stage 7 +/// consumes. The precommitted reduction state that spans into stage 7's +/// address phase travels as `ProofSession` carries, not output fields. pub struct Stage6bProverOutput { - pub claims: Stage6bOutputClaims, pub clear_output: Stage6bClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -191,7 +187,6 @@ where let committed_witness = proved.witness; Ok(Stage6bProverOutput { - claims: proved.output_claims.clone(), clear_output: Stage6bClearOutput { output_values: proved.output_claims, output_points: proved.output_points, @@ -476,7 +471,12 @@ mod field_inline_zk { cycle_points.point_count().saturating_sub(aliased) ); assert_eq!( - OutputClaims::opening_values(&out.claims.field_registers_inc_claim_reduction).len(), + OutputClaims::opening_values( + &out.clear_output + .output_values + .field_registers_inc_claim_reduction + ) + .len(), 1 ); diff --git a/crates/jolt-prover/src/stages/stage7.rs b/crates/jolt-prover/src/stages/stage7.rs index a53b86ce3a..9ef806d2af 100644 --- a/crates/jolt-prover/src/stages/stage7.rs +++ b/crates/jolt-prover/src/stages/stage7.rs @@ -23,7 +23,7 @@ use jolt_verifier::sites::STAGE7; use jolt_verifier::stages::stage4::Stage4ClearOutput; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::hamming_weight_claim_reduction::hamming_weight_claim_reduction_dimensions; -use jolt_verifier::stages::stage7::outputs::{Stage7ClearOutput, Stage7OutputClaims}; +use jolt_verifier::stages::stage7::outputs::Stage7ClearOutput; use jolt_verifier::stages::stage7::{build_stage7_sumchecks, stage7_input_values_from_upstream}; use jolt_verifier::CheckedInputs; use jolt_witness::JoltWitnessPlane; @@ -31,10 +31,8 @@ use jolt_witness::JoltWitnessPlane; use crate::recorder::ProofMode; use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _}; -/// Stage 7's outputs: the wire proof, the wire claims, and the verifier-typed -/// cross-stage carrier stage 8 consumes. +/// Stage 7's outputs: the verifier-typed cross-stage carrier stage 8 consumes. pub struct Stage7ProverOutput { - pub claims: Stage7OutputClaims, pub clear_output: Stage7ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -101,7 +99,6 @@ where let committed_witness = proved.witness; Ok(Stage7ProverOutput { - claims: proved.output_claims.clone(), clear_output: Stage7ClearOutput { output_values: proved.output_claims, output_points: proved.output_points, diff --git a/crates/jolt-verifier/src/stages/stage1/outputs.rs b/crates/jolt-verifier/src/stages/stage1/outputs.rs index 29a05e812c..78c35fc8b2 100644 --- a/crates/jolt-verifier/src/stages/stage1/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage1/outputs.rs @@ -3,30 +3,12 @@ use jolt_claims::protocols::jolt::JoltRelationId; use jolt_field::JoltField; use jolt_sumcheck::{BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency}; -use serde::{Deserialize, Serialize}; use super::outer_remainder::{OuterRemainder, OuterRemainderOutputClaims}; use crate::stages::relations::SumcheckBatch; use crate::stages::zk::outputs::CommittedOutputClaimOutput; use crate::VerifierError; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct Stage1OutputClaims { - pub uniskip_output_claim: F, - pub outer: Stage1BatchOutputClaims, -} - -impl Stage1OutputClaims { - /// Construct the stage-1 claims from the uni-skip and remainder outputs. - pub fn new(uniskip_output_claim: F, outer: Stage1BatchOutputClaims) -> Self { - Self { - uniskip_output_claim, - outer, - } - } -} - /// Source-of-truth for stage 1's singleton sumcheck batch: the Spartan outer /// *remainder* sumcheck (the companion uni-skip first round is a separate /// univariate-skip sub-sumcheck, not a batch member — verified inline in diff --git a/crates/jolt-verifier/src/stages/stage2/outputs.rs b/crates/jolt-verifier/src/stages/stage2/outputs.rs index 11d6ae2f02..55193f50d8 100644 --- a/crates/jolt-verifier/src/stages/stage2/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage2/outputs.rs @@ -2,7 +2,6 @@ use jolt_field::JoltField; use jolt_sumcheck::{BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency}; -use serde::{Deserialize, Serialize}; use crate::stages::relations::SumcheckBatch; use crate::stages::zk::outputs::CommittedOutputClaimOutput; @@ -22,25 +21,6 @@ pub use super::ram_read_write_checking::{RamReadWriteChecking, RamReadWriteOutpu #[cfg(feature = "field-inline")] pub use jolt_claims::protocols::field_inline::relations::product::FieldRegistersProductOutputClaims; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound(serialize = "F: Serialize", deserialize = "F: for<'a> Deserialize<'a>"))] -pub struct Stage2OutputClaims { - pub product_uniskip_output_claim: F, - #[cfg_attr(feature = "field-inline", serde(with = "canonical_batch"))] - pub batch_outputs: Stage2BatchOutputClaims, -} - -impl Stage2OutputClaims { - /// Combine the product uni-skip claim with the selected batch's output claims. - /// Field-inline builds carry mandatory composed product and register-reduction fields. - pub fn new(product_uniskip_output_claim: F, batch_outputs: Stage2BatchOutputClaims) -> Self { - Self { - product_uniskip_output_claim, - batch_outputs, - } - } -} - impl Stage2BatchOutputClaims { /// Construct the ordinary stage-2 batch claims. Producers without field-inline semantics /// use this regardless of the build's feature set — the field-inline claim-reduction slot @@ -500,24 +480,6 @@ mod tests { } } - #[cfg(feature = "field-inline")] - #[test] - fn wire_claims_reconstruct_reduction_aliases_from_the_product() { - let claims = Stage2OutputClaims::new(fr(19), consistent_values()); - let bytes = postcard::to_stdvec(&claims).unwrap(); - let decoded: Stage2OutputClaims = postcard::from_bytes(&bytes).unwrap(); - assert_eq!(decoded, claims); - let mut inconsistent = claims; - inconsistent - .batch_outputs - .field_registers_claim_reduction - .rs1_value += fr(1); - assert!(sumchecks() - .validate_aliases(&inconsistent.batch_outputs) - .is_err()); - assert_eq!(postcard::to_stdvec(&inconsistent).unwrap(), bytes); - } - #[test] fn validate_aliases_accepts_consistent_reduction() { assert!(sumchecks().validate_aliases(&consistent_values()).is_ok()); @@ -617,64 +579,3 @@ mod tests { assert_eq!(reduction_points.rd_value, reduction_points.rs2_value); } } - -#[cfg(feature = "field-inline")] -mod canonical_batch { - use super::*; - use jolt_claims::protocols::composed::ProductOutputs; - use serde::ser::SerializeStruct; - use serde::{Deserializer, Serializer}; - - // Alias values remain available to the generic batch evaluator, but are - // reconstructed from their canonical source when decoding the wire proof. - pub fn serialize( - claims: &Stage2BatchOutputClaims, - serializer: S, - ) -> Result { - let mut wire = serializer.serialize_struct("Stage2BatchOutputClaims", 5)?; - wire.serialize_field("ram_read_write", &claims.ram_read_write)?; - wire.serialize_field("product_remainder", &claims.product_remainder)?; - wire.serialize_field( - "instruction_claim_reduction", - &claims.instruction_claim_reduction, - )?; - wire.serialize_field("ram_raf_evaluation", &claims.ram_raf_evaluation)?; - wire.serialize_field("ram_output_check", &claims.ram_output_check)?; - wire.end() - } - - #[derive(Deserialize)] - #[serde(bound = "F: JoltField")] - struct CanonicalBatch { - ram_read_write: RamReadWriteOutputClaims, - product_remainder: ProductOutputs, - instruction_claim_reduction: InstructionClaimReductionOutputClaims, - ram_raf_evaluation: RamRafEvaluationOutputClaims, - ram_output_check: RamOutputCheckOutputClaims, - } - - pub fn deserialize<'de, F: JoltField, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - let CanonicalBatch { - ram_read_write, - product_remainder, - instruction_claim_reduction, - ram_raf_evaluation, - ram_output_check, - } = CanonicalBatch::::deserialize(deserializer)?; - let field_registers_claim_reduction = FieldRegistersClaimReductionOutputClaims { - rs1_value: product_remainder.field_inline.rs1_value, - rs2_value: product_remainder.field_inline.rs2_value, - rd_value: product_remainder.field_inline.rd_value, - }; - Ok(Stage2BatchOutputClaims { - ram_read_write, - product_remainder, - instruction_claim_reduction, - field_registers_claim_reduction, - ram_raf_evaluation, - ram_output_check, - }) - } -} From 663291ff0fd191253941b066ed112e387e2949b6 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Thu, 1 Oct 2026 17:17:33 -0400 Subject: [PATCH 33/62] build(deps): refresh the standalone fuzz and consumer lockfiles jolt-transcript now depends on rand_core and thiserror and no longer on blake2, digest, or rand. --- crates/jolt-crypto/fuzz/Cargo.lock | 22 +- crates/jolt-dory/fuzz/Cargo.lock | 150 +++----- crates/jolt-field/fuzz/Cargo.lock | 14 + crates/jolt-lookup-tables/fuzz/Cargo.lock | 14 + crates/jolt-openings/fuzz/Cargo.lock | 154 +++----- crates/jolt-poly/fuzz/Cargo.lock | 14 + crates/jolt-r1cs/fuzz/Cargo.lock | 350 +++++++++++++++++- crates/jolt-sumcheck/fuzz/Cargo.lock | 130 ++----- .../tests/external-consumer/Cargo.lock | 15 +- crates/jolt-transcript/fuzz/Cargo.lock | 132 ++----- crates/jolt-verifier/fuzz/Cargo.lock | 158 +++----- 11 files changed, 607 insertions(+), 546 deletions(-) diff --git a/crates/jolt-crypto/fuzz/Cargo.lock b/crates/jolt-crypto/fuzz/Cargo.lock index 1541a2197a..e9a1054df1 100644 --- a/crates/jolt-crypto/fuzz/Cargo.lock +++ b/crates/jolt-crypto/fuzz/Cargo.lock @@ -353,7 +353,6 @@ dependencies = [ "ark-std", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint", "num-integer", "num-traits", @@ -403,13 +402,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "jolt-transcript" -version = "0.1.0" -dependencies = [ - "jolt-field", -] - [[package]] name = "jolt-utils" version = "0.1.0" @@ -758,3 +750,17 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + + + + + + + + + + + + + + diff --git a/crates/jolt-dory/fuzz/Cargo.lock b/crates/jolt-dory/fuzz/Cargo.lock index 03cc3a87ef..69d841e870 100644 --- a/crates/jolt-dory/fuzz/Cargo.lock +++ b/crates/jolt-dory/fuzz/Cargo.lock @@ -53,9 +53,9 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -64,10 +64,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -85,10 +85,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "allocative", - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -100,23 +100,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint 0.4.8", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -126,16 +109,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -148,28 +121,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint 0.4.8", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-poly" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -180,26 +140,14 @@ name = "ark-serialize" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", + "ark-serialize-derive", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint 0.4.8", "rayon", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint 0.4.8", -] - [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -210,17 +158,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -232,16 +169,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "arrayvec" version = "0.7.8" @@ -463,9 +390,9 @@ checksum = "065945e9fff3c7dac5d6f72f8260eac82ad577e7c7009b6290847d1252d14d78" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "bincode 1.3.3", "blake2 0.10.6", "digest 0.10.7", @@ -617,9 +544,9 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "jolt-field", "jolt-poly", "num-bigint 0.4.8", @@ -636,7 +563,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -671,8 +598,8 @@ name = "jolt-field" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "cc", "num-traits", "rand_core 0.6.4", @@ -700,9 +627,9 @@ source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout# dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "arrayvec", "num-bigint 0.4.8", "num-integer", @@ -730,12 +657,9 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "blake2 0.11.0-rc.6", - "digest 0.11.3", + "ark-ff", "jolt-field", "light-poseidon", - "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -781,7 +705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "num-bigint 0.4.8", "thiserror 1.0.69", ] @@ -1232,8 +1156,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2 0.11.0-rc.6", "digest 0.11.3", "keccak", @@ -1423,3 +1345,17 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + + + + + + + + + + + + + + diff --git a/crates/jolt-field/fuzz/Cargo.lock b/crates/jolt-field/fuzz/Cargo.lock index 507bca117a..25007c085e 100644 --- a/crates/jolt-field/fuzz/Cargo.lock +++ b/crates/jolt-field/fuzz/Cargo.lock @@ -565,3 +565,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-lookup-tables/fuzz/Cargo.lock b/crates/jolt-lookup-tables/fuzz/Cargo.lock index 3c68416447..2e77bedcb0 100644 --- a/crates/jolt-lookup-tables/fuzz/Cargo.lock +++ b/crates/jolt-lookup-tables/fuzz/Cargo.lock @@ -611,3 +611,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-openings/fuzz/Cargo.lock b/crates/jolt-openings/fuzz/Cargo.lock index e17320ff0d..cd82d8d5f8 100644 --- a/crates/jolt-openings/fuzz/Cargo.lock +++ b/crates/jolt-openings/fuzz/Cargo.lock @@ -53,9 +53,9 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -64,10 +64,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -85,10 +85,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "allocative", - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -100,23 +100,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint 0.4.8", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -126,16 +109,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -148,28 +121,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint 0.4.8", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-poly" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -180,26 +140,14 @@ name = "ark-serialize" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", + "ark-serialize-derive", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint 0.4.8", "rayon", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint 0.4.8", -] - [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -210,17 +158,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -232,16 +169,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "arrayvec" version = "0.7.8" @@ -443,9 +370,9 @@ checksum = "065945e9fff3c7dac5d6f72f8260eac82ad577e7c7009b6290847d1252d14d78" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "bincode", "blake2 0.10.6", "digest 0.10.7", @@ -591,12 +518,11 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint 0.4.8", "num-integer", "num-traits", @@ -611,7 +537,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -629,8 +555,8 @@ name = "jolt-field" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "cc", "num-traits", "rand_core 0.6.4", @@ -670,9 +596,9 @@ source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout# dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "arrayvec", "num-bigint 0.4.8", "num-integer", @@ -686,7 +612,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -701,13 +626,12 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "blake2 0.11.0-rc.6", - "digest 0.11.3", + "ark-ff", "jolt-field", "light-poseidon", - "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] @@ -750,7 +674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "num-bigint 0.4.8", "thiserror 1.0.69", ] @@ -1182,8 +1106,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2 0.11.0-rc.6", "digest 0.11.3", "keccak", @@ -1355,3 +1277,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-poly/fuzz/Cargo.lock b/crates/jolt-poly/fuzz/Cargo.lock index 84a86d8217..886cec8a92 100644 --- a/crates/jolt-poly/fuzz/Cargo.lock +++ b/crates/jolt-poly/fuzz/Cargo.lock @@ -664,3 +664,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-r1cs/fuzz/Cargo.lock b/crates/jolt-r1cs/fuzz/Cargo.lock index 03cd9b83b7..cfce7591b5 100644 --- a/crates/jolt-r1cs/fuzz/Cargo.lock +++ b/crates/jolt-r1cs/fuzz/Cargo.lock @@ -50,8 +50,8 @@ dependencies = [ "educe", "fnv", "hashbrown", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-integer", "num-traits", "zeroize", @@ -69,8 +69,8 @@ dependencies = [ "arrayvec", "digest 0.10.7", "educe", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-traits", "paste", "zeroize", @@ -90,7 +90,7 @@ name = "ark-ff-macros" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-traits", "proc-macro2", "quote", @@ -120,7 +120,7 @@ dependencies = [ "ark-std", "arrayvec", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.8", ] [[package]] @@ -140,7 +140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand", + "rand 0.8.7", ] [[package]] @@ -197,6 +197,21 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -287,6 +302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common 0.2.2", "ctutils", ] @@ -351,6 +367,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -395,13 +422,22 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "jobserver" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom", + "getrandom 0.4.3", "libc", ] @@ -442,11 +478,10 @@ dependencies = [ "ark-std", "jolt-field", "jolt-poly", - "jolt-transcript", - "num-bigint", + "num-bigint 0.4.8", "num-integer", "num-traits", - "rand_core", + "rand_core 0.6.4", "rayon", "serde", ] @@ -460,7 +495,7 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", "thiserror", ] @@ -493,10 +528,9 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", - "rand_core", + "rand_core 0.6.4", "rayon", "serde", "thiserror", @@ -542,6 +576,9 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "jolt-field", + "rand_core 0.6.4", + "spongefish", + "thiserror", ] [[package]] @@ -551,6 +588,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.189" @@ -567,6 +614,15 @@ dependencies = [ "cc", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -577,6 +633,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -601,6 +667,171 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cbc82137c157673e2d33d95f936a94ce237a790d46e0a2b3165dccfc385dd6" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.2", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.2", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + [[package]] name = "paste" version = "1.0.15" @@ -652,8 +883,18 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ + "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -663,7 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -671,6 +912,15 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" @@ -701,6 +951,12 @@ dependencies = [ "semver", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -737,12 +993,54 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spongefish" +version = "0.7.4" +source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" +dependencies = [ + "digest 0.11.3", + "p3-koala-bear", + "rand 0.8.7", + "sha2", + "sha3", +] + [[package]] name = "strum" version = "0.28.0" @@ -852,6 +1150,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "zerocopy" version = "0.8.55" @@ -891,3 +1195,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-sumcheck/fuzz/Cargo.lock b/crates/jolt-sumcheck/fuzz/Cargo.lock index 46f835da15..336c23219f 100644 --- a/crates/jolt-sumcheck/fuzz/Cargo.lock +++ b/crates/jolt-sumcheck/fuzz/Cargo.lock @@ -53,9 +53,9 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -64,10 +64,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -84,10 +84,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "allocative", - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -98,23 +98,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint 0.4.8", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -124,16 +107,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -146,28 +119,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint 0.4.8", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-poly" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -178,25 +138,13 @@ name = "ark-serialize" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", + "ark-serialize-derive", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint 0.4.8", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint 0.4.8", -] - [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -207,17 +155,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -228,16 +165,6 @@ dependencies = [ "rand 0.8.7", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "arrayvec" version = "0.7.8" @@ -524,8 +451,8 @@ name = "jolt-field" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "cc", "num-traits", "rand_core 0.6.4", @@ -576,12 +503,9 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "blake2", - "digest 0.11.3", + "ark-ff", "jolt-field", "light-poseidon", - "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -627,7 +551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "num-bigint 0.4.8", "thiserror 1.0.69", ] @@ -1049,8 +973,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2", "digest 0.11.3", "keccak", @@ -1213,3 +1135,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock index dfe6fd3fab..10d2fea739 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock @@ -162,7 +162,6 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "jolt-field", - "rand 0.8.8", "rand_core 0.6.4", "spongefish", "thiserror", @@ -669,3 +668,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-transcript/fuzz/Cargo.lock b/crates/jolt-transcript/fuzz/Cargo.lock index 02338ad2ed..15fedafbbe 100644 --- a/crates/jolt-transcript/fuzz/Cargo.lock +++ b/crates/jolt-transcript/fuzz/Cargo.lock @@ -53,9 +53,9 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -64,10 +64,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -84,10 +84,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "allocative", - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -98,23 +98,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint 0.4.8", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -124,16 +107,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -146,28 +119,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint 0.4.8", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-poly" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -178,25 +138,13 @@ name = "ark-serialize" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", + "ark-serialize-derive", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint 0.4.8", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint 0.4.8", -] - [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -207,17 +155,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -228,16 +165,6 @@ dependencies = [ "rand 0.8.7", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "arrayvec" version = "0.7.8" @@ -499,8 +426,8 @@ name = "jolt-field" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "cc", "num-traits", "rand_core 0.6.4", @@ -513,13 +440,12 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "blake2", - "digest 0.11.3", + "ark-ff", "jolt-field", "light-poseidon", - "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.20", ] [[package]] @@ -563,7 +489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "num-bigint 0.4.8", "thiserror 1.0.69", ] @@ -965,8 +891,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2", "digest 0.11.3", "keccak", @@ -1129,3 +1053,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + diff --git a/crates/jolt-verifier/fuzz/Cargo.lock b/crates/jolt-verifier/fuzz/Cargo.lock index 04a35bb2b1..39296d77c3 100644 --- a/crates/jolt-verifier/fuzz/Cargo.lock +++ b/crates/jolt-verifier/fuzz/Cargo.lock @@ -53,9 +53,9 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -64,10 +64,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -85,10 +85,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "allocative", - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -100,23 +100,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint 0.4.8", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -126,16 +109,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -148,28 +121,15 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint 0.4.8", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-poly" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown", @@ -180,26 +140,14 @@ name = "ark-serialize" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", + "ark-serialize-derive", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint 0.4.8", "rayon", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint 0.4.8", -] - [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -210,17 +158,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -232,16 +169,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "arrayvec" version = "0.7.8" @@ -348,7 +275,7 @@ name = "common" version = "0.2.0" dependencies = [ "allocative", - "ark-serialize 0.5.0", + "ark-serialize", "serde", "syn 3.0.3", ] @@ -494,9 +421,9 @@ checksum = "065945e9fff3c7dac5d6f72f8260eac82ad577e7c7009b6290847d1252d14d78" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "bincode 1.3.3", "blake2 0.10.6", "digest 0.10.7", @@ -692,9 +619,9 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "jolt-field", "jolt-poly", "num-bigint 0.4.8", @@ -711,7 +638,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -729,8 +656,8 @@ name = "jolt-field" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "cc", "num-traits", "rand_core 0.6.4", @@ -768,9 +695,9 @@ source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout# dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "arrayvec", "num-bigint 0.4.8", "num-integer", @@ -797,7 +724,7 @@ dependencies = [ name = "jolt-program" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "common", "jolt-riscv", "serde", @@ -823,7 +750,7 @@ dependencies = [ name = "jolt-riscv" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "serde", "strum", "thiserror 2.0.19", @@ -849,12 +776,9 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "blake2 0.11.0-rc.6", - "digest 0.11.3", + "ark-ff", "jolt-field", "light-poseidon", - "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -871,7 +795,7 @@ dependencies = [ name = "jolt-verifier" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "bincode 2.0.1", "blake2 0.11.0-rc.6", "common", @@ -951,7 +875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "num-bigint 0.4.8", "thiserror 1.0.69", ] @@ -1398,8 +1322,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2 0.11.0-rc.6", "digest 0.11.3", "keccak", @@ -1604,3 +1526,17 @@ dependencies = [ "quote", "syn 2.0.119", ] + + + + + + + + + + + + + + From 99c3d11511de57c7f9936b782999d3608eaf6abe Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 12:00:33 -0400 Subject: [PATCH 34/62] docs(spec): specify typed, declared proof messages Claims are received into and sent from their typed claims structs in the shape of the verifier-derived output points, on typed per-cell routes; the BlindFold layout and the prover's recorded values come from the same generated functions. --- specs/jolt-transcript-narg.md | 14 ++-- specs/jolt-verifier-typed-messages.md | 111 ++++++++++++++++++++++++++ specs/prover-stage-drivers.md | 17 ++-- 3 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 specs/jolt-verifier-typed-messages.md diff --git a/specs/jolt-transcript-narg.md b/specs/jolt-transcript-narg.md index 65ac5c504e..cc520fcc02 100644 --- a/specs/jolt-transcript-narg.md +++ b/specs/jolt-transcript-narg.md @@ -264,16 +264,16 @@ second transcript. ### Stage-level wire decisions -- **Output claims are received by shape.** Each batch member receives its wire - openings in canonical order. Aliased openings are not sent; they are filled - from their canonical source, so an alias cannot disagree with its source. - Stage 6b's booleanity bytecode-RA dedup is decided at runtime by opening-point - equality and goes through the same helpers. Its ZK commitment count depends - on the verified rounds (`verify_zk_with`). +- **Output claims are received by shape.** Each stage receives its claims in + the shape of its derived output points, on its typed claim routes + (`specs/jolt-verifier-typed-messages.md`). Aliased openings are not sent; + they are filled from their canonical source. Stage 6b's booleanity + bytecode-RA alias is decided by opening-point equality over the derived + points, so its ZK commitment count follows the verified rounds. - **Stage 4 staged openings.** The RAM value-check input claim consumes the advice and program-image openings, so a clear proof sends them after the stage's gamma draws and before the batch. A ZK proof commits them in its - output-claim rows in the claims aggregate's canonical order. + output-claim rows in the claims struct's declaration order. - **Uni-skip** output claims are sent right after the round's challenge, before any later draw. - **Stage 8** runs `HomomorphicBatch`'s sequence on both sides: absorb the diff --git a/specs/jolt-verifier-typed-messages.md b/specs/jolt-verifier-typed-messages.md new file mode 100644 index 0000000000..a9af8b4b78 --- /dev/null +++ b/specs/jolt-verifier-typed-messages.md @@ -0,0 +1,111 @@ +# Design note: typed, declared proof messages + +| Field | Value | +| --------- | ----------- | +| Author(s) | @markosg04 | +| Created | 2026-10-05 | +| Status | implemented | + +## Decision + +Every claim a proof carries is received into, and sent from, a typed claims +struct by code generated from that struct's declaration. The shape of a +stage's output claims is the shape of its typed output-points struct, which +`derive_opening_points` builds from typed dimensions. No wire shape is +computed from symbolic expressions. Each claim cell has a typed `ClaimRoute`: +sent, aliased to a source cell, or staged before the batch. One generated +function per stage turns points plus routes into the clear receive, the clear +send, the committed (ZK) rows, and the BlindFold row layout. Non-claim message +groups are typed structs that own their `send` and `receive` in one `impl`. + +## Problem + +The NARG port moved proof data from a typed `JoltProof` struct into the +argument string. Stage data flow stayed typed, but the proof boundary became +less declarative: + +- The verifier received claims by building a placeholder from + `wire_output_openings`, a set derived at runtime from each relation's + symbolic output expression. That set disagreed with the claims struct for + the address-major committed-program cycle phases. The mismatch surfaced only + as a rejected honest proof. +- The same claim order existed in several hand-written copies: stage 6b's + `stage6b_opening_values` and its receive, stage 4's + `opening_values` / `post_round_opening_values` and its receive, and one + BlindFold id list per stage (plus the field-inline splices). +- Stage 4's staged openings were sent and received by unrelated functions. + +## Invariants + +1. A claims struct's field declaration order is the only definition of claim + order on the wire, in committed rows, and in the BlindFold lowering. +2. The verifier receives claims in the shape of its own output points, derived + from public dimensions and the batch point. The prover checks its values + against the same points (`validate_output_shape`) before recording them. +3. Every non-claim message group is a type with `send` and `receive` in one + `impl`. +4. Routes are a function of the derived points alone, declared once per stage. + An alias reads a cell already received; a missing source is a typed error. + +## Mechanism + +**`MapCells` (jolt-claims).** `#[derive(OutputClaims)]` also implements +`MapCells`: `try_map_cells(&S, f) -> S` visits every cell in canonical +order with its opening id. The composed aggregates delegate to their parts. + +**`ClaimRoute` / `ClaimRoutes` (jolt-verifier `relations`).** `Sent`, +`Alias(source)`, or `Staged`. A cell's route is its override if one is set, +else its member's static alias (`aliased_output_openings`), else `Sent`. + +**Generated per stage (`#[derive(SumcheckBatch)]`).** + +- `claim_routes(points)`: the default routes, or, under + `#[sumcheck_batch(routes)]`, the output-points struct's inherent + `claim_routes`. Stage 4 routes its staged contribution cells `Staged`. + Stage 6b aliases a booleanity `bytecode_ra` cell whose point equals its + bytecode read-RAF source's point. +- `receive_output_claims(points, routes, staged, t)`: `Sent` cells are + received, `Alias` cells copy their source, `Staged` cells come from + `staged`. +- `wire_claim_values` (the clear send) and `committed_claim_values` (the ZK + rows) over claims and routes. +- `committed_claim_layout(points, routes)`: the committed ids in row order plus + the `OpeningAlias` rows. `verify_zk` derives the points, builds this layout, + and returns it in the `CommittedOutputClaimShape`. BlindFold's `add_stage` + reads ids and aliases from the shape, so no lowering keeps its own list. +- `validate_output_shape(claims, points)`: the prover's self-check. + +`ConcreteSumcheck::wire_output_openings`, `output_claim_count`, the +`no_opening_values` / `no_output_shape` opt-outs, and the prover driver's +`curate` hook are deleted. + +**Recording.** The prover driver records `committed_claim_values` when its +recorder's `ClaimRecorder::RECORDS_STAGED` is true (committed) and +`wire_claim_values` otherwise (clear). Stage 4 sends its staged openings +before the batch exactly when the mode recorder does not record them. + +**Typed message groups.** `RamValCheckStagedOpenings` is a cell-generic +claims struct over the RAM value check's staged fields, with `send`, +`receive(points, t)`, and `by_id` (the `staged` pre-fill). +`RamValCheckInitStructure::staged_points` builds its points, which +`RamValCheck::derive_opening_points` now fills instead of leaving absent. + +## Compatibility + +Clear wire bytes are unchanged. ZK proofs change: stage 4's committed rows are +now in declaration order (registers, field registers, then the RAM value +check's fields with its staged cells in place) instead of staged cells first. +The BlindFold lowering follows automatically. Fixtures regenerate. + +## Verification + +- Prover e2e in clear, ZK, and Akita builds; the verifier fixture suite. +- Order pins with distinct sentinels against the generated functions: stages + 2–7 `wire_claim_values`, stage 4 `committed_claim_values`, and the 6b alias + route. +- `MapCells` visit order equals `canonical_order`, keeps shape, and stops at + the first error. +- The BlindFold stage-1 row order equals the composed R1CS column order the + factored output formula consumes, from the production layout. +- The engine ZK twin checks `verify_zk`'s derived points and row count against + the prover's. diff --git a/specs/prover-stage-drivers.md b/specs/prover-stage-drivers.md index c937182c04..cf263d9f2b 100644 --- a/specs/prover-stage-drivers.md +++ b/specs/prover-stage-drivers.md @@ -144,12 +144,9 @@ Key abstractions introduced or modified: `Proved` is one generic carrier in jolt-prover `{ recorded, output_claims: S::OutputClaims, output_points: S::OutputPoints, final_claim }` (replaces v1's per-stage generated `ProvedStageN`). - Output curation (stage-6b's dedup'd absorb order) is a per-impl - `curate_opening_values(&self, claims: &mut Self::OutputClaims, points: - &Self::OutputPoints) -> Result, _>` hook: the macro emits the default body - (`self.opening_values(claims)`, the derive-generated canonical order) and accepts an - override block at the invocation site for the curated stages (6b passes the promoted - `stage6b_opening_values`). + The recorded claim values are the derive-generated `committed_claim_values` (committed + recorders) or `wire_claim_values` (clear recorders) on the batch's generated + `claim_routes`; no stage curates its own order (`specs/jolt-verifier-typed-messages.md`). - **Member-list callback macros** (emitted by `#[derive(SumcheckBatch)]`): for each batch struct, an inert `#[macro_export] macro_rules! _members` that forwards a structured token list to a caller-chosen macro: @@ -357,7 +354,7 @@ macro_rules! stage3_sumchecks_members { ($cb:path) => { $cb! { ] } } } -// jolt-prover (hand-written, one line per stage; override block only for curated stages): +// jolt-prover (hand-written, one line per stage): stage3_sumchecks_members!(impl_stage_prover); ``` @@ -369,8 +366,8 @@ stage3_sumchecks_members!(impl_stage_prover); id) → `prove_batch` → `derive_opening_points` → per-member `validate_derived_tables` → typed `output_claims()` into the aggregate → per-member `park_residue` (cross-batch residues into the session; the call consumes the kernel, so it follows the borrowing extraction) → -`curate_opening_values` (default: generated canonical order) → `validate_output_claims` → -`expected_final_claim` hard check → `recorder.finish`. +`validate_output_shape` against the derived points → `expected_final_claim` hard check → +`recorder.finish` over the generated claim values on the batch's routes. **Edge classes and their mechanisms** (the exhaustive list; v1's `external` row replaced): @@ -379,7 +376,7 @@ the session; the call consumes the kernel, so it follows the borrowing extractio | Mid-head hand draws (stage 2/6b fronts; stage 6a's booleanity reference draws since moved into its member's `draw_challenges` override) | Stay in `prove_stageX`, between `draw_challenges` and `prove` — same seam `verify` uses today. The derive's existing draw-suppression opt-outs are unchanged. | | Uni-skip pre-phases (stages 1–2) | The front runs the uni-skip round (`SpartanOuterInstance` etc.) and parks the bound instance in `ProofSession`; the remainder member is a regular batch member whose `PrepareKernel::prepare` reclaims the instance and calls `into_remainder(&relation)`. | | Cross-batch 6b→7 precommitted carry | The 6b cycle kernel's `park_residue` override — the default-no-op `SumcheckKernel` hook the driver invokes uniformly on every member after extraction — moves its post-cycle bound state (tables, running scale, schedule) into `ProofSession` as a plain owned carry keyed by the address-phase relation; stage 7's `PrepareKernel` reclaims it by move and mounts a fresh address-phase kernel. No live object spans the batches. Intermediate-vs-final wire claims (`has_address_phase()`) resolve inside the kernel's `output_claims()` — the layout lives on the relation. | -| Output curation (6b opening-value dedup) | `curate_opening_values` override at the macro invocation site, calling the promoted `stage6b_opening_values`. The derive's `no_opening_values` opt-out continues to suppress the verify-side generated absorb for the same stages. | +| Runtime claim routing (6b bytecode-RA point alias, stage-4 staged openings) | `#[sumcheck_batch(routes)]`: the output-points struct's `claim_routes` declares the overrides; the generated receive, send, committed layout, and the prover driver all read them. | | Non-oracle witness channels (read-RAF rows, stage-6 bytecode indices, prover-retained program data) | Typed rows: fetched inside `prepare` via the witness plane's typed accessors. Program data: `ProofSession` residency established at proof start. Never a driver concern. | ### Alternatives Considered From 841e7a3d30fdc19abb2b13004005bd17982e2fd9 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 12:00:33 -0400 Subject: [PATCH 35/62] feat(claims): map claims structs cell by cell MapCells visits every cell of a claims struct in canonical order with its opening id and rebuilds the struct over the mapped cells, so one declaration fixes both the order and the shape a cell-typed message travels in. --- crates/jolt-claims-derive/src/lib.rs | 32 +++++++ crates/jolt-claims/src/claim_data.rs | 89 +++++++++++++++++++ crates/jolt-claims/src/lib.rs | 4 +- .../src/protocols/composed/claims.rs | 35 +++++++- 4 files changed, 157 insertions(+), 3 deletions(-) diff --git a/crates/jolt-claims-derive/src/lib.rs b/crates/jolt-claims-derive/src/lib.rs index 8b8a76d89b..e22f8e2e21 100644 --- a/crates/jolt-claims-derive/src/lib.rs +++ b/crates/jolt-claims-derive/src/lib.rs @@ -519,6 +519,9 @@ fn expand_output(input: DeriveInput) -> Result { let mut order_chains = Vec::new(); let mut resolve_arms = Vec::new(); let mut construct_fields = Vec::new(); + // Field initializers for `MapCells`, in declaration order (struct literal + // fields evaluate in written order, so cells are visited canonically). + let mut map_fields = Vec::new(); for plan in &plans { let FieldPlan { @@ -531,6 +534,14 @@ fn expand_output(input: DeriveInput) -> Result { if *is_many { let id = id_expr(&namespace, kind, relation, Some(quote!(index))); order_chains.push(quote!(.chain(self.#ident.iter().enumerate().map(|(index, _)| #id)))); + map_fields.push(quote! { + #ident: self + .#ident + .iter() + .enumerate() + .map(|(index, __cell)| f(&#id, __cell)) + .collect::<::core::result::Result<::std::vec::Vec<_>, __E>>()?, + }); resolve_arms.push(quote! { for (index, __value) in self.#ident.iter().enumerate() { if *id == #id { @@ -555,6 +566,9 @@ fn expand_output(input: DeriveInput) -> Result { } else if *is_option { let id = id_expr(&namespace, kind, relation, None); order_chains.push(quote!(.chain(self.#ident.as_ref().map(|_| #id)))); + map_fields.push(quote! { + #ident: self.#ident.as_ref().map(|__cell| f(&#id, __cell)).transpose()?, + }); resolve_arms.push(quote! { if let ::core::option::Option::Some(__value) = &self.#ident { if *id == #id { @@ -567,6 +581,9 @@ fn expand_output(input: DeriveInput) -> Result { } else { let id = id_expr(&namespace, kind, relation, None); order_chains.push(quote!(.chain(::core::iter::once(#id)))); + map_fields.push(quote! { + #ident: f(&#id, &self.#ident)?, + }); resolve_arms.push(quote! { if *id == #id { return ::core::option::Option::Some(self.#ident); @@ -643,6 +660,21 @@ fn expand_output(input: DeriveInput) -> Result { } } + // The structural cell map, over any pair of cell types: the struct's + // own declaration fixes the visit order and the mapped shape. + impl<__A, __B> ::jolt_claims::MapCells<__A, __B, #id_ty> for #name<__A> { + type Mapped = #name<__B>; + + fn try_map_cells<__E>( + &self, + f: &mut impl ::core::ops::FnMut(&#id_ty, &__A) -> ::core::result::Result<__B, __E>, + ) -> ::core::result::Result<#name<__B>, __E> { + ::core::result::Result::Ok(#name { + #(#map_fields)* + }) + } + } + // The per-field opening-point accessors live on the point cell // (`C = Vec`): each field is a `Vec` point (or `Vec>` / // `Option>`). A field and its accessor share a name; `x` reads the diff --git a/crates/jolt-claims/src/claim_data.rs b/crates/jolt-claims/src/claim_data.rs index 520ac85cc9..0a06ea30d0 100644 --- a/crates/jolt-claims/src/claim_data.rs +++ b/crates/jolt-claims/src/claim_data.rs @@ -107,6 +107,26 @@ pub trait OutputClaims { O: core::fmt::Debug; } +/// A produced-claim struct over cell type `A`, mapped cell by cell into the +/// same struct over cell type `B`. +/// +/// Cells are visited in canonical (field-declaration) order, each with its +/// opening id, and the result has exactly the source's shape: every `Vec` +/// family keeps its length and every `Option` cell its presence. This is how +/// a claims struct's own declaration drives code that walks it: receiving +/// values into the shape of verifier-derived opening points, or listing the +/// ids of the cells a proof commits. `#[derive(OutputClaims)]` implements it. +pub trait MapCells { + /// The same struct over cell type `B`. + type Mapped; + + /// Map every cell through `f`, stopping at the first error. + fn try_map_cells( + &self, + f: &mut impl FnMut(&O, &A) -> Result, + ) -> Result; +} + /// The input-formula resolver for a relation's *consumed* opening-claim struct /// (populated by explicit cross-stage wiring). /// @@ -266,3 +286,72 @@ mod sumcheck_challenges_tests { ); } } + +#[cfg(test)] +#[expect(clippy::unwrap_used)] +mod map_cells_tests { + use crate::{MapCells, OutputClaims}; + use jolt_field::{Fr, Ring}; + + #[derive(Debug, PartialEq, crate::OutputClaims)] + #[relation(SpartanOuter)] + struct Shaped { + #[opening(PC)] + pc: C, + #[opening(LookupTableFlag)] + flags: Vec, + #[opening(committed = RamInc)] + inc: Option, + #[opening(committed = RdInc)] + absent: Option, + } + + fn points() -> Shaped> { + Shaped { + pc: vec![Fr::from_u64(1)], + flags: vec![vec![Fr::from_u64(2)], vec![Fr::from_u64(3)]], + inc: Some(vec![Fr::from_u64(4)]), + absent: None, + } + } + + /// The map visits exactly the cells `canonical_order` lists, in that order, + /// and the mapped struct keeps every `Vec` length and `Option` presence. + #[test] + fn map_follows_canonical_order_and_keeps_shape() { + let mut next = 0u64; + let mut visited = Vec::new(); + let values = points() + .try_map_cells(&mut |id, _point| { + visited.push(*id); + next += 1; + Ok::<_, ()>(Fr::from_u64(next)) + }) + .unwrap(); + assert_eq!( + values, + Shaped { + pc: Fr::from_u64(1), + flags: vec![Fr::from_u64(2), Fr::from_u64(3)], + inc: Some(Fr::from_u64(4)), + absent: None, + } + ); + assert_eq!(visited, values.canonical_order()); + } + + #[test] + fn map_stops_at_the_first_error() { + let mut calls = 0; + let result = points().try_map_cells(&mut |_, _| { + calls += 1; + if calls == 2 { + Err(calls) + } else { + Ok(Fr::from_u64(0)) + } + }); + assert_eq!(result, Err(2)); + assert_eq!(calls, 2); + } +} diff --git a/crates/jolt-claims/src/lib.rs b/crates/jolt-claims/src/lib.rs index 7a230e9652..abaa7f3704 100644 --- a/crates/jolt-claims/src/lib.rs +++ b/crates/jolt-claims/src/lib.rs @@ -34,8 +34,8 @@ mod symbolic; pub mod twist; pub use claim_data::{ - ChallengeDrawError, InputClaims, MissingOpeningValue, NoChallenges, NoInputs, NoOutputs, - OutputClaims, SumcheckChallenges, + ChallengeDrawError, InputClaims, MapCells, MissingOpeningValue, NoChallenges, NoInputs, + NoOutputs, OutputClaims, SumcheckChallenges, }; pub use claims::{challenge, constant, derived, opening, Expr, Source, Term}; pub use jolt_claims_derive::{InputClaims, OutputClaims, SumcheckChallenges}; diff --git a/crates/jolt-claims/src/protocols/composed/claims.rs b/crates/jolt-claims/src/protocols/composed/claims.rs index 636b1b11fd..2bc9c8cb9e 100644 --- a/crates/jolt-claims/src/protocols/composed/claims.rs +++ b/crates/jolt-claims/src/protocols/composed/claims.rs @@ -7,7 +7,7 @@ use crate::protocols::field_inline::relations::{ use crate::protocols::field_inline::FieldInlineOpeningId; use crate::protocols::jolt::relations::spartan as base; use crate::protocols::jolt::{JoltChallengeId, JoltDerivedId}; -use crate::{Expr, InputClaims, MissingOpeningValue, OutputClaims}; +use crate::{Expr, InputClaims, MapCells, MissingOpeningValue, OutputClaims}; use jolt_field::JoltField; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; @@ -126,6 +126,39 @@ impl, E: OutputClaims> } } +impl MapCells for EmptyClaims { + type Mapped = Self; + + fn try_map_cells( + &self, + _: &mut impl FnMut(&FieldInlineOpeningId, &A) -> Result, + ) -> Result { + Ok(Self) + } +} + +impl MapCells for ComposedClaims +where + Base: MapCells, + Ext: MapCells, +{ + type Mapped = ComposedClaims; + + fn try_map_cells( + &self, + f: &mut impl FnMut(&ComposedOpeningId, &A) -> Result, + ) -> Result { + Ok(ComposedClaims { + base: self + .base + .try_map_cells(&mut |id, cell| f(&(*id).into(), cell))?, + field_inline: self + .field_inline + .try_map_cells(&mut |id, cell| f(&(*id).into(), cell))?, + }) + } +} + pub type ComposedExpr = Expr; #[derive(Clone, Debug, Default, PartialEq, Eq, InputClaims)] From 6886b823e2680c3ff95bde2620d3ef3f786c4029 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 12:05:59 -0400 Subject: [PATCH 36/62] refactor(verifier)!: derive claim wire shapes from the output points Each stage receives its output claims in the shape of its derived output points, on typed per-cell routes (Sent, Alias, Staged) that the batch declares once. The generated receive, clear send, committed values, and committed layout all walk the claims struct's declaration order, and BlindFold reads its row ids and aliases from the verified layout. This deletes wire_output_openings, output_claim_count, the no_opening_values/no_output_shape opt-outs, stage 6b's hand receive and order, stage 4's hand orders, every per-stage BlindFold id list, and the prover driver's curate hook. Stage 4's staged advice and program-image openings become a cell-generic claims struct with send and receive in one impl, and the RAM value check now derives their points. The ZK stage-4 committed row order changes to declaration order. --- .../src/optimized/ram_val_check.rs | 2 + crates/jolt-prover/src/driver.rs | 78 +-- crates/jolt-prover/src/recorder.rs | 26 +- crates/jolt-prover/src/stages/drivers.rs | 35 +- crates/jolt-prover/src/stages/stage1.rs | 4 +- crates/jolt-prover/src/stages/stage2.rs | 2 +- crates/jolt-prover/src/stages/stage4.rs | 29 +- crates/jolt-prover/src/stages/stage6b.rs | 39 +- crates/jolt-prover/tests/engine_twins.rs | 30 +- crates/jolt-verifier-derive/src/lib.rs | 642 +++++++++--------- crates/jolt-verifier/src/proof.rs | 14 + crates/jolt-verifier/src/stages/relations.rs | 498 ++++++-------- .../src/stages/stage1/outputs.rs | 10 +- .../jolt-verifier/src/stages/stage1/verify.rs | 19 +- .../src/stages/stage2/outputs.rs | 42 +- .../src/stages/stage2/product_remainder.rs | 23 +- .../jolt-verifier/src/stages/stage2/verify.rs | 13 +- .../src/stages/stage3/outputs.rs | 29 +- .../jolt-verifier/src/stages/stage3/verify.rs | 17 +- .../src/stages/stage4/outputs.rs | 202 ++---- .../src/stages/stage4/ram_val_check.rs | 157 +++-- .../jolt-verifier/src/stages/stage4/verify.rs | 38 +- .../src/stages/stage5/outputs.rs | 35 +- .../jolt-verifier/src/stages/stage5/verify.rs | 16 +- .../src/stages/stage6a/bytecode_read_raf.rs | 16 - .../src/stages/stage6a/outputs.rs | 7 +- .../src/stages/stage6a/verify.rs | 29 +- .../src/stages/stage6b/bytecode_read_raf.rs | 6 +- .../committed_reduction_cycle_phase.rs | 68 +- .../src/stages/stage6b/field_inline.rs | 13 - .../jolt-verifier/src/stages/stage6b/mod.rs | 5 +- .../src/stages/stage6b/outputs.rs | 82 +-- .../src/stages/stage6b/verify.rs | 309 ++------- .../src/stages/stage7/outputs.rs | 84 +-- .../jolt-verifier/src/stages/stage7/verify.rs | 14 +- crates/jolt-verifier/src/stages/uniskip.rs | 20 +- .../src/stages/zk/blindfold/field_inline.rs | 45 +- .../src/stages/zk/blindfold/mod.rs | 65 +- .../src/stages/zk/blindfold/stage1.rs | 58 +- .../src/stages/zk/blindfold/stage2.rs | 213 +----- .../src/stages/zk/blindfold/stage3.rs | 58 -- .../src/stages/zk/blindfold/stage4.rs | 138 ---- .../src/stages/zk/blindfold/stage5.rs | 135 ---- .../src/stages/zk/blindfold/stage6a.rs | 13 - .../src/stages/zk/blindfold/stage6b.rs | 298 -------- .../src/stages/zk/blindfold/stage7.rs | 14 +- .../jolt-verifier/src/stages/zk/committed.rs | 26 - crates/jolt-verifier/src/stages/zk/mod.rs | 1 - crates/jolt-verifier/src/stages/zk/outputs.rs | 32 +- crates/jolt-verifier/src/verifier.rs | 10 + .../fs_inventory/delegation-sites.inventory | 21 +- .../fs_inventory/message-sites.inventory | 6 +- 52 files changed, 1078 insertions(+), 2708 deletions(-) delete mode 100644 crates/jolt-verifier/src/stages/zk/committed.rs diff --git a/crates/jolt-kernels/src/optimized/ram_val_check.rs b/crates/jolt-kernels/src/optimized/ram_val_check.rs index 9f84356d0e..95b79d4ef6 100644 --- a/crates/jolt-kernels/src/optimized/ram_val_check.rs +++ b/crates/jolt-kernels/src/optimized/ram_val_check.rs @@ -262,6 +262,7 @@ mod tests { use super::*; use crate::reference::views::address_fold; use crate::ReferenceBackend; + use jolt_verifier::stages::stage4::ram_val_check::RamValCheckStagedOpenings; fn run_parity(shape: FixtureShape, ops: Vec, seed: u64) { with_ram_fixture(shape, ops, |witness| { @@ -272,6 +273,7 @@ mod tests { TraceDimensions::new(shape.log_t), shape.log_k(), RamValCheckInit::full(Fr::from_u64(0)), + RamValCheckStagedOpenings::default(), ); let claims = RamValCheckInputClaims { ram_val: Fr::from_u64(0), diff --git a/crates/jolt-prover/src/driver.rs b/crates/jolt-prover/src/driver.rs index a52c3bbe2b..b238c488cd 100644 --- a/crates/jolt-prover/src/driver.rs +++ b/crates/jolt-prover/src/driver.rs @@ -12,10 +12,10 @@ //! `validate_derived_tables` → typed extraction into the stage's //! `OutputClaims` aggregate → per-member `park_residue` (cross-batch residues //! into the session; consumes the kernels, so it follows the borrowing -//! extraction) → [`curate_opening_values`](StageProver::curate_opening_values) -//! (default: the derive-generated canonical absorb order; curated stages -//! override at the invocation site) → shape validation → the -//! `expected_final_claim` hard self-check → `recorder.finish`. The recorder +//! extraction) → the generated `validate_output_shape` against the derived +//! points → the `expected_final_claim` hard self-check → `recorder.finish` +//! over the generated claim values on the stage's `claim_routes` (the cells a +//! [`ClaimRecorder`](crate::recorder::ClaimRecorder) records). The recorder //! is the clear/ZK seam, exactly as in `begin_batch`: no `prove_clear`/ //! `prove_zk` split exists to drift. //! @@ -38,6 +38,7 @@ use jolt_verifier::stages::relations::{ use jolt_verifier::VerifierError; use jolt_witness::JoltWitnessPlane; +use crate::recorder::ClaimRecorder; use crate::ProverError; /// The generated per-stage driver: implemented for each batch struct by @@ -76,19 +77,8 @@ pub trait StageProver: Sized { ) -> Result, ProverError> where B: KernelSource + ?Sized, - Rec: SumcheckRecorder, + Rec: SumcheckRecorder + ClaimRecorder, H: Sponge; - - /// The stage's sent (clear) or committed (ZK) opening scalars, in the stage's curated order - /// (stage 6b's runtime point dedup reorders the returned values). The - /// default emitted by `impl_stage_prover!` returns the derive-generated - /// canonical order (`opening_values`); curated stages supply an override - /// block at the macro invocation site. - fn curate_opening_values( - &self, - claims: &Self::OutputClaims, - points: &Self::OutputPoints, - ) -> Result, ProverError>; } /// The per-stage kernel-source bound collector: `impl_stage_prover!` emits @@ -418,17 +408,6 @@ macro_rules! __stage_member { }; } -/// The output-shape leg of the generated driver, keyed by the derive-emitted -/// flag: `checked` runs the generated `validate_output_claims`; `unchecked` -/// (a `no_output_shape` stage, whose wire shape is runtime-curated) has no -/// validator to run. -macro_rules! __stage_shape_check { - (checked, $self:expr, $claims:expr) => { - $self.validate_output_claims(&$claims)?; - }; - (unchecked, $self:expr, $claims:expr) => {}; -} - /// Expand one stage's [`StageProver`] and [`KernelSource`] impls from its /// derive-emitted member-list macro. Invoke as the member-list macro's /// callback, at a site where the batch's relation and aggregate names @@ -436,25 +415,9 @@ macro_rules! __stage_shape_check { /// /// ```ignore /// jolt_verifier::stage3_sumchecks_members!(impl_stage_prover); -/// // a curated stage passes its absorb-order override ahead of the list -/// // (the first closure parameter binds the batch, i.e. `&self`): -/// jolt_verifier::stage6b_sumchecks_members!(impl_stage_prover curate = |batch, claims, points| { .. },); /// ``` macro_rules! impl_stage_prover { - // Uncurated stage: default to the derive-generated canonical absorb order. - // The batch receiver is an explicit closure parameter (not `self`) because - // macro hygiene separates a `self` written in this arm from the `&self` - // the main arm's method signature introduces. - (batch = $($rest:tt)*) => { - $crate::driver::impl_stage_prover! { - curate = |__batch, __claims, __points| { - ::core::result::Result::Ok(__batch.opening_values(__claims)) - }, - batch = $($rest)* - } - }; ( - curate = |$curate_batch:ident, $curate_claims:ident, $curate_points:ident| $curate_body:block, batch = $batch:ident, label = $label:literal, aggregates = { @@ -464,7 +427,6 @@ macro_rules! impl_stage_prover { output_points = $output_points:ident, challenges = $challenges_ty:ident, }, - shape = $shape:ident, members = [ $({ name: $member:ident, relation: $relation:ident, presence: $presence:ident },)+ ] @@ -494,7 +456,7 @@ macro_rules! impl_stage_prover { > where B: $crate::driver::KernelSource + ?Sized, - Rec: ::jolt_sumcheck::SumcheckRecorder, + Rec: ::jolt_sumcheck::SumcheckRecorder + $crate::recorder::ClaimRecorder, H: ::jolt_transcript::Sponge, { let __stage_span = @@ -549,9 +511,13 @@ macro_rules! impl_stage_prover { }; $($crate::driver::__stage_member!(park $presence $member, session);)+ - let __opening_values = - self.curate_opening_values(&__output_claims, &__output_points)?; - $crate::driver::__stage_shape_check!($shape, self, __output_claims); + self.validate_output_shape(&__output_claims, &__output_points)?; + let __routes = Self::claim_routes(&__output_points)?; + let __claim_values = if Rec::RECORDS_STAGED { + Self::committed_claim_values(&__output_claims, &__routes) + } else { + Self::wire_claim_values(&__output_claims, &__routes) + }; let __expected = self.expected_final_claim( &__coefficients, @@ -574,7 +540,7 @@ macro_rules! impl_stage_prover { ); } - let __witness = recorder.finish(&__opening_values, transcript)?; + let __witness = recorder.finish(&__claim_values, transcript)?; ::core::result::Result::Ok($crate::driver::Proved { witness: __witness, output_claims: __output_claims, @@ -582,18 +548,6 @@ macro_rules! impl_stage_prover { final_claim: __proved.final_claim, }) } - - fn curate_opening_values( - &self, - $curate_claims: &Self::OutputClaims, - $curate_points: &Self::OutputPoints, - ) -> ::core::result::Result<::std::vec::Vec, $crate::ProverError> { - let $curate_batch = self; - let _ = ($curate_batch, $curate_points); - $curate_body - } - - } impl $crate::driver::KernelSource> for B @@ -623,4 +577,4 @@ macro_rules! impl_stage_prover { }; } -pub(crate) use {__stage_member, __stage_shape_check, impl_stage_prover}; +pub(crate) use {__stage_member, impl_stage_prover}; diff --git a/crates/jolt-prover/src/recorder.rs b/crates/jolt-prover/src/recorder.rs index b91c6c74c7..560683c3b5 100644 --- a/crates/jolt-prover/src/recorder.rs +++ b/crates/jolt-prover/src/recorder.rs @@ -15,7 +15,8 @@ use jolt_crypto::VectorCommitment; use jolt_field::JoltField; use jolt_poly::UnivariatePoly; #[cfg(not(feature = "zk"))] -use jolt_sumcheck::{prove_uniskip_clear, ClearSumcheckRecorder}; +use jolt_sumcheck::prove_uniskip_clear; +use jolt_sumcheck::ClearSumcheckRecorder; #[cfg(feature = "zk")] use jolt_sumcheck::{prove_uniskip_committed, CommittedSumcheckRecorder, CommittedSumcheckWitness}; use jolt_transcript::{ProverTranscript, Sponge}; @@ -29,6 +30,29 @@ pub type ModeRecorder<'a, VC> = #[cfg(not(feature = "zk"))] pub type ModeRecorder<'a, VC> = ClearSumcheckRecorder<::Field>; +/// Which output-claim cells a recorder records after a batch's rounds. +/// A clear recorder sends the `Sent` cells; the `Staged` cells went on the +/// wire before the batch. A committed recorder commits every non-alias cell, +/// staged ones included, because nothing was revealed earlier. +pub trait ClaimRecorder { + const RECORDS_STAGED: bool; +} + +impl ClaimRecorder for ClearSumcheckRecorder { + const RECORDS_STAGED: bool = false; +} + +// The driver's twin tests run a committed recorder in clear builds too. +#[cfg(any(feature = "zk", test))] +impl ClaimRecorder for jolt_sumcheck::CommittedSumcheckRecorder<'_, F, VC, R> +where + F: JoltField, + VC: VectorCommitment, + R: rand_core::RngCore, +{ + const RECORDS_STAGED: bool = true; +} + /// A proved uni-skip round in the compiled mode: the reduction challenge and /// the output claim (sent by the clear arm, committed and retained by the ZK /// arm). diff --git a/crates/jolt-prover/src/stages/drivers.rs b/crates/jolt-prover/src/stages/drivers.rs index 1d83327234..0a59d1989c 100644 --- a/crates/jolt-prover/src/stages/drivers.rs +++ b/crates/jolt-prover/src/stages/drivers.rs @@ -60,18 +60,6 @@ mod stage4 { use crate::driver::impl_stage_prover; - // A clear proof sent the staged openings before the batch, so only the - // post-round openings follow the rounds. - #[cfg(not(feature = "zk"))] - jolt_verifier::stage4_sumchecks_members!(impl_stage_prover - curate = |batch, claims, _points| { - Ok(batch.post_round_opening_values(claims)) - }, - ); - // A committed proof commits every opening in the claims aggregate's - // canonical order (the `no_opening_values` replacement keeps the generated - // signature, so the driver's default curation serves it). - #[cfg(feature = "zk")] jolt_verifier::stage4_sumchecks_members!(impl_stage_prover); } @@ -105,7 +93,6 @@ mod stage6a { } mod stage6b { - use jolt_claims::protocols::jolt::JoltRelationId; use jolt_verifier::stages::stage6b::booleanity::Booleanity; use jolt_verifier::stages::stage6b::bytecode_read_raf::BytecodeReadRafCycle; use jolt_verifier::stages::stage6b::committed_reduction_cycle_phase::{ @@ -128,30 +115,10 @@ mod stage6b { }; use jolt_verifier::stages::stage6b::ram_hamming_booleanity::RamHammingBooleanity; use jolt_verifier::stages::stage6b::ram_ra_virtualization::RamRaVirtualization; - use jolt_verifier::stages::stage6b::stage6b_opening_values; - use jolt_verifier::VerifierError; use crate::driver::impl_stage_prover; - // The stage's `no_opening_values` curation: the promoted verifier - // helper's canonical order, including the runtime dedup of booleanity's - // `BytecodeRa` claims against the bytecode read-RAF points. - jolt_verifier::stage6b_sumchecks_members!(impl_stage_prover - curate = |_batch, claims, points| { - let booleanity_opening_point = - points.booleanity_opening_point().ok_or_else(|| { - VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::Booleanity, - reason: "stage-6b booleanity produced no opening point".to_string(), - } - })?; - Ok(stage6b_opening_values( - claims, - &points.bytecode_read_raf.bytecode_ra, - booleanity_opening_point, - )) - }, - ); + jolt_verifier::stage6b_sumchecks_members!(impl_stage_prover); } mod stage7 { diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index eee0bea169..c26348df03 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -245,8 +245,8 @@ mod field_inline_zk { // The committed witness carries the composed 50 output-claim values // (45 common openings + five field value/product openings), row-committed in - // capacity-sized chunks — the shape the verifier's - // `composed_output_claim_count` check derives. + // capacity-sized chunks — the layout the verifier's generated + // `committed_claim_layout` derives. let row_lens: Vec = out .committed_witness .output_claim_rows diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index 70d725edbe..56a623eb09 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -290,7 +290,7 @@ mod field_inline_zk { use crate::stages::stage1::prove_stage1; #[test] - fn committed_stage2_witness_carries_the_curated_rows_and_verifies() { + fn committed_stage2_witness_carries_the_declared_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index 33e82821ef..4ecdc2bddd 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -6,10 +6,10 @@ //! with the verifier's own promoted helpers; the private opening VALUES //! are evaluated through the backend as one batch (program image and advice, //! staged transcript-silently before the RAM -//! value-check gamma draw). The stage's one curated behavior: a clear proof -//! sends the staged advice/program-image openings after the gamma draws and -//! the register and RAM openings after the rounds, while a committed proof -//! commits all of them in the claims struct's canonical order. +//! value-check gamma draw). The advice/program-image openings are routed +//! `Staged`: a clear proof sends them after the gamma draws and the register +//! and RAM openings after the rounds, while a committed proof commits all of +//! them in the claims struct's declaration order. use jolt_claims::protocols::jolt::geometry::dimensions::REGISTER_ADDRESS_BITS; use jolt_claims::protocols::jolt::{JoltRelationId, TraceDimensions}; @@ -39,7 +39,7 @@ use jolt_verifier::stages::stage4::{ use jolt_verifier::{CheckedInputs, VerifierError}; use jolt_witness::JoltWitnessPlane; -use crate::recorder::ProofMode; +use crate::recorder::{ClaimRecorder, ModeRecorder, ProofMode}; use crate::{JoltProverPreprocessing, ProverConfig, ProverError, StageProver as _}; /// Stage 4's outputs: the verifier-typed cross-stage carrier downstream stages consume. @@ -178,7 +178,12 @@ where .field_inline .read_write_dimensions(log_t), ), - ram_val_check: RamValCheck::new(trace_dimensions, log_k, init_structure.decomposition()), + ram_val_check: RamValCheck::new( + trace_dimensions, + log_k, + init_structure.decomposition(), + init_structure.staged_points(), + ), }; // Draws the registers gamma, under `field-inline` the field-register read-write gamma, // then the RAM value-check gamma behind its `b"ram_val_check_gamma"` domain @@ -187,8 +192,9 @@ where // The RAM value-check input claim consumes the staged openings, so a clear // proof sends them before the batch; a committed proof carries them in its // output-claim rows instead. - #[cfg(not(feature = "zk"))] - transcript.send_all(&ram_val_check_init.staged_openings().values()); + if ! as ClaimRecorder>::RECORDS_STAGED { + ram_val_check_init.staged_openings().send(transcript); + } let inputs = stage4_input_values_from_upstream( &stage2.output_values, @@ -203,8 +209,7 @@ where // The staged advice/program-image openings ride in from the RAM // value-check kernel (captured off its own consumed input claims at - // prepare). The driver's curation sends the post-round openings in a clear - // build and commits the full canonical order in a ZK build. + // prepare); the driver records them on the stage's `Staged` routes. let mut scheduler = backend.round_scheduler.build(session); let proved = sumchecks.prove( backend, @@ -449,7 +454,7 @@ mod field_inline_round_trip { } } -/// ZK with field-inline enabled: the stage-4 committed witness carries the curated row count — the +/// ZK with field-inline enabled: the stage-4 committed witness carries the declared row count — the /// 5 ordinary register openings, the 5 spliced field-register read-write openings, and /// the 2 RAM value-check openings (no advice / program-image rows at the /// fixture's scale) — and the production stage-1..4 zk verifiers consume the @@ -471,7 +476,7 @@ mod field_inline_zk { }; #[test] - fn committed_stage4_witness_carries_the_curated_rows_and_verifies() { + fn committed_stage4_witness_carries_the_declared_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index 3750c1ecfc..436af91ec9 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -14,11 +14,10 @@ //! and `cycle_challenges` helpers (the batch suppresses the generated draw), //! the batch is built by the verifier's own promoted //! `Stage6bSumchecks::build_from_parts` over the clear -//! carriers, and the driver's curation hook supplies -//! the verifier's promoted `stage6b_opening_values` — the curated order with -//! the runtime dedup of booleanity's `BytecodeRa` claims against the -//! bytecode read-RAF points (which fires when the bytecode address width is -//! a multiple of the committed chunk width). +//! carriers, and the driver records the claims on the batch's generated +//! routes — including the runtime alias of booleanity's `BytecodeRa` claims +//! to the bytecode read-RAF points (which fires when the bytecode address +//! width is a multiple of the committed chunk width). #[cfg(not(feature = "akita"))] use jolt_claims::protocols::jolt::JoltAdviceKind; @@ -384,10 +383,9 @@ mod field_inline_round_trip { } } -/// ZK with field-inline enabled: the stage-6b committed witness carries the curated row count — the -/// alias-deduped cycle-point cell total, whose field-inline share is exactly the one -/// spliced reduced `FieldRdInc` row — and the production stage-1..6b zk -/// verifiers consume the prover's argument string. +/// ZK with field-inline enabled: the stage-6b committed witness carries the generated layout's +/// row count, whose field-inline share is exactly the one reduced `FieldRdInc` row, and the +/// production stage-1..6b zk verifiers consume the prover's argument string. #[cfg(all(test, feature = "field-inline", feature = "zk"))] #[expect(clippy::unwrap_used, reason = "test module")] mod field_inline_zk { @@ -405,7 +403,7 @@ mod field_inline_zk { }; #[test] - fn committed_stage6b_witness_carries_the_curated_rows_and_verifies() { + fn committed_stage6b_witness_carries_the_layout_rows_and_verifies() { let witness = addi_only_backend().with_field_inline().unwrap(); let backend = JoltBackend::::reference(); let mut session = backend.begin_proof(); @@ -447,10 +445,9 @@ mod field_inline_zk { ) .unwrap(); - // The committed row total is the verifier's expectation: the derived - // output-point cell count minus the runtime booleanity-vs-bytecode - // aliases. The field-inline reduction contributes exactly one of those cells — - // its single reduced `FieldRdInc` opening. + // The committed row total is the verifier's generated layout over the + // derived points and the batch's routes (the booleanity-vs-bytecode + // aliases dropped). let values: Vec = out .committed_witness .output_claim_rows @@ -459,17 +456,11 @@ mod field_inline_zk { .copied() .collect(); let cycle_points = &out.clear_output.output_points; - let booleanity_point = cycle_points.booleanity_opening_point().unwrap().to_vec(); - let aliased = cycle_points - .bytecode_read_raf - .bytecode_ra - .iter() - .filter(|point| point.as_slice() == booleanity_point) - .count(); - assert_eq!( - values.len(), - cycle_points.point_count().saturating_sub(aliased) + let layout = Stage6bSumchecks::committed_claim_layout( + cycle_points, + &Stage6bSumchecks::claim_routes(cycle_points).unwrap(), ); + assert_eq!(values.len(), layout.ids.len()); assert_eq!( OutputClaims::opening_values( &out.clear_output diff --git a/crates/jolt-prover/tests/engine_twins.rs b/crates/jolt-prover/tests/engine_twins.rs index 2a64b1cd74..aca6c7ed9d 100644 --- a/crates/jolt-prover/tests/engine_twins.rs +++ b/crates/jolt-prover/tests/engine_twins.rs @@ -115,6 +115,9 @@ impl ProveRounds for DenseMember { } } +/// The ZK twin's vector-commitment capacity: one output-claim row. +const ROW_LEN: usize = 8; + fn pedersen_setup(capacity: u64) -> PedersenSetup { let generator = Bn254::g1_generator(); let generators = (2..2 + capacity) @@ -234,7 +237,7 @@ fn clear_engine_twin_matches_generated_verify_clear() { #[test] fn committed_engine_twin_matches_generated_verify_zk() { type VC = Pedersen; - let setup = pedersen_setup(8); + let setup = pedersen_setup(ROW_LEN as u64); // Prover: draw → sums → begin_batch(committed; claim absorbs no-op) → // prove_batch → finish (output-claim row commitments absorbed). @@ -285,17 +288,34 @@ fn committed_engine_twin_matches_generated_verify_zk() { &mut prover_transcript, ) .unwrap(); + // Synthetic values in the shape the verifier commits: one per committed + // cell of the points derived at the batch point. + // The registers member reads its read-write point: the 7 register + // address bits then the 3 cycle bits. + let mut input_points = sumchecks.empty_input_points(); + input_points.registers_val_evaluation.registers_val = (0..10).map(Fr::from_u64).collect(); + let points = sumchecks + .derive_opening_points(&proved.challenges, &input_points) + .unwrap(); + let layout = TwinFixtureSumchecks::committed_claim_layout( + &points, + &TwinFixtureSumchecks::claim_routes(&points).unwrap(), + ); + let output_values: Vec = (0..layout.ids.len() as u64).map(Fr::from_u64).collect(); let witness = recorder - .finish(&synthetic_output_values(), &mut prover_transcript) + .finish(&output_values, &mut prover_transcript) .unwrap(); // Verifier: draw → generated verify_zk (coefficient draws, committed - // rounds, output-claim row commitments). + // rounds, derived points, output-claim row commitments). let mut verifier_transcript = verifier_transcript("engine-zk-twin", prover_transcript.narg()); let _verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); - let (consistency, output_commitments) = sumchecks - .verify_zk::(witness.output_claim_rows.len(), &mut verifier_transcript) + let verified = sumchecks + .verify_zk::(ROW_LEN, &input_points, &mut verifier_transcript) .unwrap(); + assert_eq!(verified.output_points, points); + let (consistency, output_commitments) = + (verified.consistency, verified.output_claims.commitments); assert_eq!(consistency.challenges(), proved.challenges); assert_eq!( diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index 6c40a3c026..c1294a3299 100644 --- a/crates/jolt-verifier-derive/src/lib.rs +++ b/crates/jolt-verifier-derive/src/lib.rs @@ -85,22 +85,21 @@ //! via `relations::validate_member_aliases` against the batch-wide resolver. Run //! unskippably by `expected_final_claim`, so declaring a pair on a relation //! enforces it everywhere. -//! - `output_claim_count` / `validate_output_claims` — the wire-shape helpers -//! (via `relations::validate_member_{presence, output_shape}`), deriving each -//! member's expected openings from `ConcreteSumcheck::wire_output_openings`. -//! - `draw_challenges`, `empty_input_points`, and the claim plumbing -//! (`opening_values` via `relations::absorbed_opening_values`, the order the -//! prover sends; `receive_output_claims` via -//! `relations::{receive_member_openings, assemble_member_claims}`, which reads -//! that order back). +//! - The claim wire plumbing, the stage's one definition of where each claim +//! cell travels: `receive_output_claims` (into the shape of the derived +//! output points), `wire_claim_values` (the clear sends), +//! `committed_claim_values` / `committed_claim_layout` (the ZK rows), all +//! walking each member's claims struct in its canonical order on the stage's +//! `relations::ClaimRoutes`; and `validate_output_shape`, the prover's +//! self-check that its values have the points' shape. +//! - `draw_challenges` and `empty_input_points`. //! -//! Every `#[sumcheck_batch(...)]` flag is an opt-OUT (`StageOptions` below is -//! the canonical reference): `no_opening_values`, `no_output_shape`, and -//! `no_draw_challenges` each suppress generated methods that would be WRONG to -//! call on their stage (a member-interleaved or runtime-deduped absorb order; a -//! runtime-deduped wire shape; stage-level challenge provenance) — suppressed -//! rather than overridden so they cannot be miscalled. A flagless stage gets -//! the full method suite. +//! The one opt-out flag is `#[sumcheck_batch(no_draw_challenges)]`, which +//! suppresses the generated `draw_challenges` for a stage whose member +//! challenges have stage-level provenance, so it cannot be miscalled. +//! `#[sumcheck_batch(routes)]` declares that the stage's claim routes come from +//! its output-points struct's inherent `claim_routes`, so every generated +//! receive, send, and committed layout uses the same routes. //! //! The one non-flag entry is the serde-style crate-path override //! `#[sumcheck_batch(crate = "...")]`: the path the generated code names @@ -144,11 +143,9 @@ use syn::{ /// / `StageNChallenges`) from a struct of `ConcreteSumcheck` instances. See the /// crate-level docs. /// -/// The struct-level `#[sumcheck_batch(...)]` flags are opt-outs -/// (`no_opening_values`, `no_output_shape`, `no_draw_challenges`), each -/// suppressing generated methods that would be wrong to call on the flagged -/// stage, which supplies its own replacement where one is needed. The -/// aggregate structs and their derives are emitted unchanged. +/// The struct-level `#[sumcheck_batch(no_draw_challenges)]` flag suppresses +/// the generated `draw_challenges` for a stage that assembles its challenges +/// itself. The aggregate structs and their derives are emitted unchanged. /// `#[sumcheck_batch(crate = "...")]` overrides the `::jolt_verifier` path /// the generated code names this crate by (the defining crate passes /// `"crate"`). @@ -296,53 +293,66 @@ fn expand(input: DeriveInput) -> syn::Result { }) .collect::>(); - // The instance-based absorb: each member contributes its - // `absorbed_opening_values` (its claims' `canonical_order`-aligned values - // minus its aliased opening ids) in declaration order. `Option` members - // follow the claims cell (validators police presence mismatches; the absorb - // itself is infallible). + // Per-member claim plumbing, each delegating to the `relations` helper that + // walks the member's claims struct in its declared canonical order on the + // stage's `ClaimRoutes`. `Option` members follow their claims or points cell. let claims_ident = format_ident!("__claims"); let member_ident = format_ident!("__member"); - let opening_extends = plans.iter().map(|plan| { - let id = &plan.ident; - let instance = &plan.instance; - per_member( - plan.is_option, - &[(&claims_ident, quote!(claims.#id))], - quote! { - __values.extend(#relations::absorbed_opening_values::<#f, #instance>(__claims)); - }, - ) - }); - - let receive_members = plans.iter().map(|plan| { + let points_ident = format_ident!("__points"); + let claim_value_extends = |with_staged: bool| { + plans + .iter() + .map(|plan| { + let id = &plan.ident; + let instance = &plan.instance; + per_member( + plan.is_option, + &[(&claims_ident, quote!(claims.#id))], + quote! { + __values.extend(#relations::member_claim_values::<#f, #instance>( + __claims, routes, #with_staged, + )); + }, + ) + }) + .collect::>() + }; + let wire_value_extends = claim_value_extends(false); + let committed_value_extends = claim_value_extends(true); + let committed_id_extends = plans.iter().map(|plan| { let id = &plan.ident; let instance = &plan.instance; per_member( plan.is_option, - &[(&member_ident, quote!(self.#id))], + &[(&points_ident, quote!(points.#id))], quote! { - __received.extend(#relations::receive_member_openings::<#f, #instance, __H>( - __member, transcript, - )?); + #relations::extend_committed_layout::<#f, #instance>(&mut __layout, __points, routes); }, ) }); - let assemble_fields = plans + let receive_fields = plans .iter() .map(|plan| { let id = &plan.ident; let instance = &plan.instance; if plan.is_option { quote! { - #id: self + #id: points .#id .as_ref() - .map(|_| #relations::assemble_member_claims::<#f, #instance>(&__received)) + .map(|__points| { + #relations::receive_member_claims::<#f, #instance, __H>( + __points, routes, &mut received, transcript, + ) + }) .transpose()? } } else { - quote!(#id: #relations::assemble_member_claims::<#f, #instance>(&__received)?) + quote! { + #id: #relations::receive_member_claims::<#f, #instance, __H>( + &points.#id, routes, &mut received, transcript, + )? + } } }) .collect::>(); @@ -572,122 +582,119 @@ fn expand(input: DeriveInput) -> syn::Result { // The composed clear-path driver: begin the batch (clear recorder, so the // input claims are absorbed), verify the compressed Boolean-hypercube rounds, // derive the produced opening points at the reduced point, receive the output - // claims, and check the reduced claim against the expected final-claim fold — - // the tail every stage repeats verbatim. It is parameterized by how the output - // claims are received; `verify_clear` (emitted unless `no_opening_values`) passes the - // generated `receive_output_claims`, and a stage with a curated claim order - // passes its own receive. - let verify_clear_method = { - let verify_clear = if options.no_opening_values { - quote!() - } else { - quote! { - /// [`Self::verify_clear_with`] receiving the output claims through - /// [`Self::receive_output_claims`]. - #[expect( - clippy::type_complexity, - reason = "the produced points and claims are the stage's two outputs" - )] - pub fn verify_clear<__H>( - &self, - inputs: &#input_claims_name<#f>, - input_points: &#input_points_name<#f>, - challenges: &#challenges_name<#f>, - transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, - stage: usize, - ) -> ::core::result::Result< - (#output_points_name<#f>, #output_claims_name<#f>), - #krate::VerifierError, - > - where - __H: ::jolt_transcript::Sponge, - { - self.verify_clear_with( - inputs, - input_points, - challenges, - transcript, - stage, - |__sumchecks, _, __transcript| __sumchecks.receive_output_claims(__transcript), + // claims in the shape of those points, and check the reduced claim against + // the expected final-claim fold — the tail every stage repeats verbatim. + // `verify_clear` receives on the stage's routes; `verify_clear_with` lets a + // stage with staged cells pass its own receive. + let verify_clear_method = quote! { + /// [`Self::verify_clear_with`] receiving the output claims through + /// [`Self::receive_output_claims`] on [`Self::claim_routes`]. + #[expect( + clippy::type_complexity, + reason = "the produced points and claims are the stage's two outputs" + )] + pub fn verify_clear<__H>( + &self, + inputs: &#input_claims_name<#f>, + input_points: &#input_points_name<#f>, + challenges: &#challenges_name<#f>, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + stage: usize, + ) -> ::core::result::Result< + (#output_points_name<#f>, #output_claims_name<#f>), + #krate::VerifierError, + > + where + __H: ::jolt_transcript::Sponge, + { + self.verify_clear_with( + inputs, + input_points, + challenges, + transcript, + stage, + |__points, __transcript| { + Self::receive_output_claims( + __points, + &Self::claim_routes(__points)?, + ::std::collections::BTreeMap::new(), + __transcript, ) - } - } - }; - quote! { - /// Run the clear-path batched verification in one call: `begin_batch` - /// (with a clear recorder, so the input claims are absorbed publicly), - /// the compressed Boolean-hypercube rounds, [`Self::derive_opening_points`] - /// at the reduced point, the output claims read by `receive_claims` - /// (given those points), then the [`Self::expected_final_claim`] check - /// (attributed to `stage` on mismatch). Returns the produced opening - /// points with the received output claims. - #[expect( - clippy::type_complexity, - reason = "the produced points and claims are the stage's two outputs" - )] - pub fn verify_clear_with<__H, __R>( - &self, - inputs: &#input_claims_name<#f>, - input_points: &#input_points_name<#f>, - challenges: &#challenges_name<#f>, - transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, - stage: usize, - receive_claims: __R, - ) -> ::core::result::Result< - (#output_points_name<#f>, #output_claims_name<#f>), - #krate::VerifierError, - > - where - __H: ::jolt_transcript::Sponge, - __R: ::core::ops::FnOnce( - &Self, - &#output_points_name<#f>, - &mut ::jolt_transcript::VerifierTranscript<'_, __H>, - ) -> ::core::result::Result<#output_claims_name<#f>, #krate::VerifierError>, - { - use #relations::ConcreteSumcheck as _; - - let mut __recorder = ::jolt_sumcheck::ClearSumcheckRecorder::<#f>::new(); - let (__batch, __coefficients) = - self.begin_batch(inputs, challenges, &mut __recorder, transcript)?; + }, + ) + } - let __reduction = ::jolt_sumcheck::SumcheckVerifier::verify_compressed( - &::jolt_sumcheck::SumcheckClaim::new( - __batch.max_num_vars, - __batch.max_degree, - __batch.claimed_sum, - ), - transcript, - ) - .map_err(|error| #krate::VerifierError::StageClaimSumcheckFailed { - stage: #base_lit.to_string(), - reason: error.to_string(), - })?; - let __output_points = - self.derive_opening_points(__reduction.point.as_slice(), input_points)?; - let __claims = receive_claims(self, &__output_points, transcript)?; - let __expected_final_claim = self.expected_final_claim( - &__coefficients, - input_points, - &__claims, - &__output_points, - challenges, - )?; - if __reduction.value != __expected_final_claim { - return ::core::result::Result::Err( - #krate::VerifierError::StageClaimOutputMismatch { stage }, - ); - } - ::core::result::Result::Ok((__output_points, __claims)) + /// Run the clear-path batched verification in one call: `begin_batch` + /// (with a clear recorder, so the input claims are absorbed publicly), + /// the compressed Boolean-hypercube rounds, [`Self::derive_opening_points`] + /// at the reduced point, the output claims read by `receive_claims` + /// (given those points), then the [`Self::expected_final_claim`] check + /// (attributed to `stage` on mismatch). Returns the produced opening + /// points with the received output claims. + #[expect( + clippy::type_complexity, + reason = "the produced points and claims are the stage's two outputs" + )] + pub fn verify_clear_with<__H, __R>( + &self, + inputs: &#input_claims_name<#f>, + input_points: &#input_points_name<#f>, + challenges: &#challenges_name<#f>, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + stage: usize, + receive_claims: __R, + ) -> ::core::result::Result< + (#output_points_name<#f>, #output_claims_name<#f>), + #krate::VerifierError, + > + where + __H: ::jolt_transcript::Sponge, + __R: ::core::ops::FnOnce( + &#output_points_name<#f>, + &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + ) -> ::core::result::Result<#output_claims_name<#f>, #krate::VerifierError>, + { + use #relations::ConcreteSumcheck as _; + + let mut __recorder = ::jolt_sumcheck::ClearSumcheckRecorder::<#f>::new(); + let (__batch, __coefficients) = + self.begin_batch(inputs, challenges, &mut __recorder, transcript)?; + + let __reduction = ::jolt_sumcheck::SumcheckVerifier::verify_compressed( + &::jolt_sumcheck::SumcheckClaim::new( + __batch.max_num_vars, + __batch.max_degree, + __batch.claimed_sum, + ), + transcript, + ) + .map_err(|error| #krate::VerifierError::StageClaimSumcheckFailed { + stage: #base_lit.to_string(), + reason: error.to_string(), + })?; + let __output_points = + self.derive_opening_points(__reduction.point.as_slice(), input_points)?; + let __claims = receive_claims(&__output_points, transcript)?; + let __expected_final_claim = self.expected_final_claim( + &__coefficients, + input_points, + &__claims, + &__output_points, + challenges, + )?; + if __reduction.value != __expected_final_claim { + return ::core::result::Result::Err( + #krate::VerifierError::StageClaimOutputMismatch { stage }, + ); } - - #verify_clear + ::core::result::Result::Ok((__output_points, __claims)) } }; - // The ZK-path batched-verify driver: compute the combined `(max_num_vars, - // max_degree)`, draw the batching coefficients, then check committed consistency - // through `SumcheckProof::verify_committed_consistency_dims`. Committed proofs + // The ZK-path batched-verify driver: draw the batching coefficients, read the + // committed rounds, derive the produced opening points at the batch point, + // then read one commitment per output-claim row; the rows hold the committed + // claim cells (`committed_claim_layout`) on the stage's routes. Committed proofs // never reveal claim scalars, so no claimed sums are absorbed. // // Soundness ordering: because the batching coefficients are squeezed here @@ -716,54 +723,25 @@ fn expand(input: DeriveInput) -> syn::Result { quote! { /// Run the ZK-path batched verification: draw the batching - /// coefficients, then read the committed rounds and the - /// `num_output_commitments` output-claim row commitments. - #[expect( - clippy::type_complexity, - reason = "the batched consistency and the output-claim commitments are read together" - )] + /// coefficients, read the committed rounds, derive the output points + /// at the batch point, then read the output-claim row commitments + /// over those points' committed cells on [`Self::claim_routes`]. pub fn verify_zk<__C, __H>( &self, - num_output_commitments: usize, - transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, - ) -> ::core::result::Result< - ( - ::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, - ::jolt_sumcheck::CommittedOutputClaims<__C>, - ), - #krate::VerifierError, - > - where - __C: ::jolt_field::CanonicalDecode, - __H: ::jolt_transcript::Sponge, - { - self.verify_zk_with(|_| ::core::result::Result::Ok(num_output_commitments), transcript) - } - - /// [`Self::verify_zk`] with the output-claim commitment count derived - /// from the verified rounds, for a stage whose committed-claim count - /// depends on its own opening points. - #[expect( - clippy::type_complexity, - reason = "the batched consistency and the output-claim commitments are read together" - )] - pub fn verify_zk_with<__C, __H, __N>( - &self, - num_output_commitments: __N, + row_len: usize, + input_points: &#input_points_name<#f>, transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, ) -> ::core::result::Result< - ( - ::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, - ::jolt_sumcheck::CommittedOutputClaims<__C>, - ), + #krate::stages::zk::outputs::VerifiedCommittedBatch< + #f, + __C, + #output_points_name<#f>, + >, #krate::VerifierError, > where __C: ::jolt_field::CanonicalDecode, __H: ::jolt_transcript::Sponge, - __N: ::core::ops::FnOnce( - &::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, - ) -> ::core::result::Result, { use #relations::ConcreteSumcheck as _; use ::jolt_transcript::Channel as _; @@ -775,7 +753,7 @@ fn expand(input: DeriveInput) -> syn::Result { let mut __batching_coefficients = ::std::vec::Vec::new(); #(#coeff_draws_zk)* - let (__consistency, _) = + let (__rounds, _) = ::jolt_sumcheck::SumcheckVerifier::verify_committed::<#f, __H, __C>( ::jolt_sumcheck::SumcheckStatement::new(__max_num_vars, __max_degree), 0, @@ -786,16 +764,31 @@ fn expand(input: DeriveInput) -> syn::Result { reason: error.to_string(), })?; let __consistency = ::jolt_sumcheck::BatchedCommittedSumcheckConsistency { - consistency: __consistency, + consistency: __rounds, batching_coefficients: __batching_coefficients, max_num_vars: __max_num_vars, max_degree: __max_degree, }; - let __count = num_output_commitments(&__consistency)?; - let __output_claims = ::jolt_sumcheck::CommittedOutputClaims { - commitments: transcript.receive_n(__count)?, + let __output_points = + self.derive_opening_points(&__consistency.challenges(), input_points)?; + let __shape = #krate::stages::zk::outputs::CommittedOutputClaimShape::new( + row_len, + Self::committed_claim_layout( + &__output_points, + &Self::claim_routes(&__output_points)?, + ), + ); + let __commitments = ::jolt_sumcheck::CommittedOutputClaims { + commitments: transcript.receive_n(__shape.row_count())?, }; - ::core::result::Result::Ok((__consistency, __output_claims)) + ::core::result::Result::Ok(#krate::stages::zk::outputs::VerifiedCommittedBatch { + consistency: __consistency, + output_points: __output_points, + output_claims: #krate::stages::zk::outputs::CommittedOutputClaimOutput { + shape: __shape, + commitments: __commitments, + }, + }) } } }; @@ -1034,74 +1027,55 @@ fn expand(input: DeriveInput) -> syn::Result { } }; - // The output-claim shape helpers: the total produced-opening count (for the ZK - // commitment count) and a validator that the proof-supplied output claims match - // the dims-derived expected shape. Both delegate per member to the generic - // `relations` helpers; an `Option` member's presence guards run first. - // Suppressed by `#[sumcheck_batch(no_output_shape)]` for a stage - // whose wire shape is runtime-deduped (the count/validator would be wrong). - let output_shape_methods = if options.no_output_shape { - quote!() - } else { - let count_terms = plans.iter().map(|plan| { - let id = &plan.ident; - per_member( - plan.is_option, - &[(&member_ident, quote!(self.#id))], - quote!(__count += __member.wire_output_openings().len();), - ) - }); - let validate_checks = plans.iter().map(|plan| { - let id = &plan.ident; - let instance = &plan.instance; - let shape = per_member( - plan.is_option, - &[ - (&member_ident, quote!(self.#id)), - (&claims_ident, quote!(claims.#id)), - ], - quote! { - #relations::validate_member_output_shape::<#f, #instance>( - __member, __claims, - )?; - }, - ); - if plan.is_option { - quote! { - #relations::validate_member_presence::<#f, #instance>( - self.#id.as_ref(), - claims.#id.as_ref(), - )?; - #shape + // The prover's shape self-check: every member's claim values must have the + // shape of the verifier-derived output points, and an `Option` member's + // claims, points, and instance must be present together. + let shape_checks = plans.iter().map(|plan| { + let id = &plan.ident; + let instance = &plan.instance; + if plan.is_option { + quote! { + match (self.#id.as_ref(), claims.#id.as_ref(), points.#id.as_ref()) { + ( + ::core::option::Option::Some(__member), + ::core::option::Option::Some(__claims), + ::core::option::Option::Some(__points), + ) => #relations::validate_member_output_shape::<#f, #instance>( + __member, __claims, __points, + )?, + (::core::option::Option::None, ::core::option::Option::None, ::core::option::Option::None) => {} + _ => { + return ::core::result::Result::Err( + #krate::VerifierError::StageClaimSumcheckFailed { + stage: #base_lit.to_string(), + reason: ::std::format!( + "member `{}` presence disagrees across its instance, claims, and points", + ::core::stringify!(#id), + ), + }, + ); + } } - } else { - shape - } - }); - quote! { - /// The total number of absorbed/committed opening claims across the - /// batch (each member's `ConcreteSumcheck::wire_output_openings`), - /// e.g. the committed-output-claim count. - pub fn output_claim_count(&self) -> usize { - use #relations::ConcreteSumcheck as _; - let mut __count = 0usize; - #(#count_terms)* - __count } - - /// Assert the proof-supplied output claims match the expected shape: per - /// member, the provided `canonical_order` id-set (minus the member's - /// aliased openings) equals the relation's - /// `ConcreteSumcheck::wire_output_openings`; an `Option` member's claim - /// presence must agree with the instance's. - pub fn validate_output_claims( - &self, - claims: &#output_claims_name<#f>, - ) -> ::core::result::Result<(), #krate::VerifierError> { - #(#validate_checks)* - ::core::result::Result::Ok(()) + } else { + quote! { + #relations::validate_member_output_shape::<#f, #instance>( + &self.#id, &claims.#id, &points.#id, + )?; } } + }); + let output_shape_methods = quote! { + /// Assert `claims` has the shape of the derived output `points`, member + /// by member: the prover's self-check before it sends or commits them. + pub fn validate_output_shape( + &self, + claims: &#output_claims_name<#f>, + points: &#output_points_name<#f>, + ) -> ::core::result::Result<(), #krate::VerifierError> { + #(#shape_checks)* + ::core::result::Result::Ok(()) + } }; // Suppressed by `#[sumcheck_batch(no_draw_challenges)]`: a stage whose member @@ -1130,41 +1104,71 @@ fn expand(input: DeriveInput) -> syn::Result { } }; - // The generated absorb plumbing, on the source `StageNSumchecks` struct (it - // consults each member's `aliased_output_openings` skip-set, an instance - // method). Gated out when the stage opts in to - // `#[sumcheck_batch(no_opening_values)]`, in which case the stage supplies - // its own absorb (e.g. one whose order interleaves members, or whose dedup is - // runtime point-driven). - let absorb_methods = if options.no_opening_values { - quote!() + // The claim wire plumbing: the stage's single definition of which claim + // cells travel where, in what order — member declaration order, each + // member's claims struct in its canonical order, on the stage's routes. + let routes_body = if options.routes { + quote! { points.claim_routes() } } else { - quote! { - /// Produced opening scalars in canonical order — member declaration - /// order, each member's claims in its `canonical_order` — skipping - /// each member's aliased openings (absorbed once via their canonical - /// source relation). This is the Fiat-Shamir order and MUST match the - /// prover's commitment order. - pub fn opening_values(&self, claims: &#output_claims_name<#f>) -> ::std::vec::Vec<#f> { - let mut __values = ::std::vec::Vec::new(); - #(#opening_extends)* - __values - } + quote! { ::core::result::Result::Ok(#relations::ClaimRoutes::default()) } + }; + let claim_methods = quote! { + /// The stage's claim routes over its derived output `points`: the + /// members' static aliases, plus (under `#[sumcheck_batch(routes)]`) the + /// overrides the points struct's own `claim_routes` declares. + pub fn claim_routes( + points: &#output_points_name<#f>, + ) -> ::core::result::Result<#relations::ClaimRoutes, #krate::VerifierError> { + #routes_body + } - /// Receive every member's produced openings in canonical order (the - /// order of [`Self::opening_values`], which the prover sends), then - /// assemble the stage's output claims, filling each aliased opening - /// from its canonical source. - pub fn receive_output_claims<__H: ::jolt_transcript::Sponge>( - &self, - transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, - ) -> ::core::result::Result<#output_claims_name<#f>, #krate::VerifierError> { - let mut __received = ::std::collections::BTreeMap::new(); - #(#receive_members)* - ::core::result::Result::Ok(#output_claims_name { - #(#assemble_fields,)* - }) - } + /// The output-claim values a clear proof sends after the rounds: the + /// `Sent` cells, in wire order. + pub fn wire_claim_values( + claims: &#output_claims_name<#f>, + routes: &#relations::ClaimRoutes, + ) -> ::std::vec::Vec<#f> { + let mut __values = ::std::vec::Vec::new(); + #(#wire_value_extends)* + __values + } + + /// The output-claim values a committed proof commits: the `Sent` and + /// `Staged` cells, in row order (the ids of + /// [`Self::committed_claim_layout`]). + pub fn committed_claim_values( + claims: &#output_claims_name<#f>, + routes: &#relations::ClaimRoutes, + ) -> ::std::vec::Vec<#f> { + let mut __values = ::std::vec::Vec::new(); + #(#committed_value_extends)* + __values + } + + /// The ids of the committed output-claim cells, in row order, over the + /// derived output `points`. + pub fn committed_claim_layout( + points: &#output_points_name<#f>, + routes: &#relations::ClaimRoutes, + ) -> #relations::CommittedClaimLayout { + let mut __layout = #relations::CommittedClaimLayout::default(); + #(#committed_id_extends)* + __layout + } + + /// Receive the output claims in the shape of the derived output + /// `points`, in wire order: `Sent` cells from the transcript, `Alias` + /// cells from their sources, `Staged` cells from `received`, which the + /// caller pre-fills with the stage's staged values. + pub fn receive_output_claims<__H: ::jolt_transcript::Sponge>( + points: &#output_points_name<#f>, + routes: &#relations::ClaimRoutes, + mut received: ::std::collections::BTreeMap<#relations::ComposedOpeningId, #f>, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, + ) -> ::core::result::Result<#output_claims_name<#f>, #krate::VerifierError> { + ::core::result::Result::Ok(#output_claims_name { + #(#receive_fields,)* + }) } }; @@ -1186,14 +1190,8 @@ fn expand(input: DeriveInput) -> syn::Result { let macro_doc = format!( "The member-list callback macro for [`{name}`], emitted by \ `#[derive(SumcheckBatch)]`: forwards the batch's declaration (member names, \ - relation paths, presence, stage label, aggregate names, output-shape flag) to a \ - caller-chosen macro. See `specs/prover-stage-drivers.md`." + relation paths, presence, stage label, aggregate names) to a caller-chosen macro. See `specs/prover-stage-drivers.md`." ); - let shape = if options.no_output_shape { - format_ident!("unchecked") - } else { - format_ident!("checked") - }; let member_entries = plans .iter() .map(|plan| { @@ -1225,7 +1223,6 @@ fn expand(input: DeriveInput) -> syn::Result { output_points = #output_points_name, challenges = #challenges_name, }, - shape = #shape, members = [ #(#member_entries)* ] @@ -1247,7 +1244,7 @@ fn expand(input: DeriveInput) -> syn::Result { #expected_final_claim_method #output_shape_methods #empty_input_points_method - #absorb_methods + #claim_methods } }; @@ -1312,22 +1309,17 @@ fn expand(input: DeriveInput) -> syn::Result { /// anything else. #[derive(Default)] struct StageOptions { - /// `#[sumcheck_batch(no_opening_values)]`: skip emitting the generated - /// `opening_values` / `append_output_claims` absorb methods so the stage can - /// supply its own (a member-interleaved order or a runtime point-driven dedup). - no_opening_values: bool, - /// `#[sumcheck_batch(no_output_shape)]`: skip emitting `output_claim_count` and - /// `validate_output_claims` (which derive the expected output-claim shape from - /// each member's `wire_output_openings`) for a stage whose wire shape is not - /// statically derivable (a runtime point-driven dedup) — the count/validator - /// would be wrong there, so they must not exist to be miscalled. - no_output_shape: bool, /// `#[sumcheck_batch(no_draw_challenges)]`: skip emitting the generated /// `draw_challenges` for a stage whose member challenges have stage-level /// provenance (shared squeezes, pre-batch draws, value re-rolls) — calling a /// per-member draw there would squeeze at the wrong transcript position, so /// the method must not exist to be miscalled. no_draw_challenges: bool, + /// `#[sumcheck_batch(routes)]`: the stage's claim routes come from the + /// inherent `claim_routes(&self) -> Result` of + /// its generated output-points struct, for cells routed by runtime data + /// (staged before the batch, or aliased by point equality). + routes: bool, /// `#[sumcheck_batch(crate = "...")]`: the path the generated code names /// `jolt-verifier` by (serde's `crate` attribute shape). `None` means the /// absolute `::jolt_verifier` default; the defining crate passes @@ -1360,21 +1352,19 @@ impl StageOptions { let Meta::Path(path) = &flag else { return Err(syn::Error::new_spanned( &flag, - "expected a bare `sumcheck_batch` flag (e.g. `no_opening_values`) or \ - `crate = \"...\"`", + "expected a bare `sumcheck_batch` flag (`no_draw_challenges`, \ + `routes`) or `crate = \"...\"`", )); }; - if path.is_ident("no_opening_values") { - options.no_opening_values = true; - } else if path.is_ident("no_output_shape") { - options.no_output_shape = true; - } else if path.is_ident("no_draw_challenges") { + if path.is_ident("no_draw_challenges") { options.no_draw_challenges = true; + } else if path.is_ident("routes") { + options.routes = true; } else { return Err(syn::Error::new_spanned( path, - "unknown `sumcheck_batch` flag (supported: `no_opening_values`, \ - `no_output_shape`, `no_draw_challenges`, `crate = \"...\"`)", + "unknown `sumcheck_batch` flag (supported: `no_draw_challenges`, \ + `routes`, `crate = \"...\"`)", )); } } diff --git a/crates/jolt-verifier/src/proof.rs b/crates/jolt-verifier/src/proof.rs index cd26e867b1..c043a0dc20 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -18,6 +18,20 @@ use crate::{config::JoltProtocolConfig, jolt_protocol_id, num, VerifierError, JO /// under so a build mismatch is reported as such instead of as a transcript /// failure. The verifier binds its own configuration into the transcript, so /// `protocol` carries no soundness weight. +/// +/// The argument string's regions, in order, each opened by its +/// [`sites`](crate::sites) label: +/// +/// 1. [`ProofHeader`], then [`ProofCommitments`]. +/// 2. Stages 1–7: per batch, any uni-skip round, the rounds, then the output +/// claims in the shape of the verifier-derived output points. A clear proof +/// sends the generated `wire_claim_values`; a committed proof sends row +/// commitments over the generated `committed_claim_layout`. A clear stage 4 +/// sends its +/// [`RamValCheckStagedOpenings`](crate::stages::stage4::ram_val_check::RamValCheckStagedOpenings) +/// before its batch. +/// 3. Stage 8: the joint opening. +/// 4. BlindFold, in committed proofs only. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct JoltProof { pub protocol: JoltProtocolConfig, diff --git a/crates/jolt-verifier/src/stages/relations.rs b/crates/jolt-verifier/src/stages/relations.rs index 75d884dc36..1098701a13 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -9,21 +9,24 @@ //! This makes the canonical opening **order** and **count** a single-sourced //! consequence of a struct's field declaration order. //! -//! Transcript I/O stays here: [`receive_member_openings`] reads a relation's -//! produced openings in the canonical order [`OutputClaims::opening_values`] -//! defines, so `jolt-claims` stays transcript-free while the Fiat-Shamir order -//! remains single-sourced. +//! Transcript I/O stays here: [`receive_member_claims`] reads a relation's +//! produced claims into the shape of its derived output points, in the +//! canonical order the claims struct's declaration defines, on the routes of +//! [`ClaimRoutes`]. `jolt-claims` stays transcript-free while the wire order +//! remains single-sourced in each claims struct. -pub use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; +pub use jolt_claims::{InputClaims, MapCells, OutputClaims, SumcheckChallenges}; /// `#[derive(SumcheckBatch)]` generates a stage's aggregate claim types from a /// struct of [`ConcreteSumcheck`] instances; re-exported here alongside the /// per-relation claim plumbing it composes. See `specs/sumcheck-batch-derive.md`. pub use jolt_verifier_derive::SumcheckBatch; +use core::convert::Infallible; use core::fmt::Debug; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; +use jolt_blindfold::OpeningAlias; use jolt_claims::SymbolicSumcheck; use jolt_field::JoltField; use jolt_transcript::{Channel, Sponge, VerifierTranscript}; @@ -148,9 +151,9 @@ where /// identical) point, as the `source` opening produced by another member of the /// same stage batch. Aliased openings appear on the wire claims struct as /// plain (present) cells but are absorbed/committed once via their source, so - /// the generated drivers use this set three ways: the absorb skips the aliased - /// ids, the shape/count arithmetic subtracts them (see - /// [`wire_output_openings`](Self::wire_output_openings)), and the generated + /// the generated drivers use this set three ways: these cells default to + /// [`ClaimRoute::Alias`], so they are neither sent nor committed; BlindFold + /// binds them through the generated layout's `OpeningAlias` rows; and the generated /// `validate_aliases` — run by every `expected_final_claim` — enforces the /// wire copies equal their sources. That equality check is load-bearing: the /// aliased cells are never Fiat-Shamir-absorbed and the batch fold pins only @@ -174,24 +177,6 @@ where Vec::new() } - /// The opening ids this instance absorbs into the transcript (and commits in - /// ZK): the output-`Expr`-referenced set minus the aliased openings (absorbed - /// once via their canonical source). The generated `output_claim_count` sums - /// these; the generated `validate_output_claims` compares the wire claims - /// against them. A relation that absorbs openings its own output `Expr` does - /// not reference (values whose constraining fold happens downstream, e.g. the - /// product remainder's stage-6a-consumed flags) overrides this to add them. - fn wire_output_openings(&self) -> BTreeSet> - where - Self: Sized, - { - let mut openings = self.symbolic().expected_output_openings::(); - for (aliased, _) in Self::aliased_output_openings() { - let _ = openings.remove(&aliased); - } - openings - } - /// The offset of this instance's point within the batch challenge vector: the /// instance is bound on `batch_point[offset .. offset + rounds]`. Defaults to /// the front-loaded suffix (`batch_num_vars - rounds`); the two-phase address @@ -324,64 +309,6 @@ where } } -/// One member's absorbed opening scalars: its claims' `canonical_order`-aligned -/// values minus the member's [aliased -/// openings](ConcreteSumcheck::aliased_output_openings) (absorbed once via -/// their canonical source relation). Called by the generated `opening_values` -/// per member, in member declaration order. -pub fn absorbed_opening_values(claims: &SumcheckOutputClaims) -> Vec -where - F: JoltField, - I: ConcreteSumcheck, - SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Ord, -{ - let skip: BTreeSet<_> = I::aliased_output_openings() - .into_iter() - .map(|(aliased, _)| aliased) - .collect(); - claims - .canonical_order() - .into_iter() - .zip(claims.opening_values()) - .filter(|(id, _)| !skip.contains(id)) - .map(|(_, value)| value) - .collect() -} - -/// Assert an optional member's output-claims presence agrees with the instance: -/// reject a present instance missing its claims cell, and reject claims -/// supplied for an absent instance. Called by the generated -/// `validate_output_claims` for each `Option` member (before its shape check). -pub fn validate_member_presence( - member: Option<&I>, - claims: Option<&SumcheckOutputClaims>, -) -> Result<(), VerifierError> -where - F: JoltField, - I: ConcreteSumcheck, - SumcheckOutputClaims: OutputClaims>, - // The diagnostic formats the relation id, so the helper spans every - // protocol family a batch mixes (the derive's own presence errors use the - // same string-typed variant). - as SymbolicSumcheck>::RelationId: Debug, -{ - match (member, claims) { - (Some(_), Some(_)) | (None, None) => Ok(()), - (Some(member), None) => Err(VerifierError::StageClaimSumcheckFailed { - stage: format!("{:?}", member.id()), - reason: "present instance is missing its output claims".to_string(), - }), - (None, Some(claims)) => Err(match claims.canonical_order().into_iter().next() { - Some(opening) => VerifierError::UnexpectedOpeningClaim { id: opening.into() }, - None => VerifierError::StageClaimSumcheckFailed { - stage: format!("{:?}", ::id()), - reason: "output claims supplied for an absent instance".to_string(), - }, - }), - } -} - /// Resolve one batch member's produced opening by composite id: downcast the /// composite to the member's own opening-id family, then resolve within the /// member's claims. A foreign-family or unknown id is a miss (`None`), so the @@ -401,105 +328,205 @@ where claims.resolve_output(&native) } -/// Receive one batch member's produced openings: its -/// [`wire_output_openings`](ConcreteSumcheck::wire_output_openings) in canonical -/// order, which is the order the prover's clear recorder sends them. Aliased -/// openings are not sent; [`assemble_member_claims`] fills them from their -/// sources. Called by the generated `receive_output_claims` per member. -pub fn receive_member_openings( - member: &I, - transcript: &mut VerifierTranscript<'_, H>, -) -> Result, VerifierError> +/// How one produced claim cell reaches the verifier. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ClaimRoute { + /// Sent after the stage's rounds (clear) or committed in the stage's + /// output-claim rows (ZK). + Sent, + /// A copy of an earlier cell of the same batch: never sent or committed, + /// filled from that source. + Alias(ComposedOpeningId), + /// Sent earlier in the stage as part of a typed staged message (clear), + /// committed in the output-claim rows (ZK). + Staged, +} + +/// One stage batch's claim routes. A cell takes the route set here, else its +/// member's static alias ([`ConcreteSumcheck::aliased_output_openings`]), +/// else [`ClaimRoute::Sent`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ClaimRoutes { + routes: BTreeMap, +} + +impl ClaimRoutes { + /// Routes cell `id` by `route`, overriding its member's static route. + pub fn set(&mut self, id: impl Into, route: ClaimRoute) { + let _ = self.routes.insert(id.into(), route); + } + + fn of( + &self, + id: &ComposedOpeningId, + static_aliases: &BTreeMap, + ) -> ClaimRoute { + self.routes.get(id).copied().unwrap_or_else(|| { + static_aliases + .get(id) + .map_or(ClaimRoute::Sent, |source| ClaimRoute::Alias(*source)) + }) + } +} + +fn static_aliases() -> BTreeMap where F: JoltField, I: ConcreteSumcheck, - H: Sponge, - SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Copy + Ord + Debug + Into, + OpeningIdOf: Into, { - let aliased: BTreeSet = I::aliased_output_openings() + I::aliased_output_openings() .into_iter() - .map(|(aliased, _)| aliased.into()) - .collect(); - receive_member_openings_except(member, |id| aliased.contains(id), transcript) + .map(|(aliased, source)| (aliased.into(), source.into())) + .collect() } -/// [`receive_member_openings`] with the skipped (aliased) openings chosen by -/// `skip` instead of the member's static alias declaration, for a stage whose -/// dedup is decided at runtime by opening-point equality. -pub fn receive_member_openings_except( - member: &I, - skip: impl Fn(&ComposedOpeningId) -> bool, +/// Receive one batch member's output claims in the shape of its derived +/// output `points`, in the claims struct's canonical order: each `Sent` cell +/// is read from the transcript, each `Alias` cell copies its already received +/// source, and each `Staged` cell takes the value the stage received earlier. +/// `received` holds the batch's values by id (pre-filled with the staged +/// ones) and gains every cell read here. Called by the generated +/// `receive_output_claims` per member, in declaration order. +pub fn receive_member_claims( + points: &SumcheckOutputPoints, + routes: &ClaimRoutes, + received: &mut BTreeMap, transcript: &mut VerifierTranscript<'_, H>, -) -> Result, VerifierError> +) -> Result, VerifierError> where F: JoltField, I: ConcreteSumcheck, H: Sponge, - SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Copy + Ord + Debug + Into, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy + Into, { - let wire = member.wire_output_openings(); - let aliased: BTreeSet<_> = I::aliased_output_openings() - .into_iter() - .map(|(aliased, _)| aliased) - .collect(); - // A placeholder struct holding exactly the produced openings yields their - // canonical order without restating the relation's field layout. - let shape = SumcheckOutputClaims::::from_opening_values(|id| { - (wire.contains(id) || aliased.contains(id)).then(F::zero) + let static_aliases = static_aliases::(); + points.try_map_cells(&mut |id, _point| { + let id: ComposedOpeningId = (*id).into(); + let source = match routes.of(&id, &static_aliases) { + ClaimRoute::Sent => { + let value: F = transcript.receive()?; + let _ = received.insert(id, value); + return Ok(value); + } + ClaimRoute::Alias(source) => source, + ClaimRoute::Staged => id, + }; + received + .get(&source) + .copied() + .ok_or(VerifierError::MissingOpeningClaim { id: source }) }) - .map_err(|missing| VerifierError::MissingOpeningClaim { - id: missing.id.into(), - })?; - shape +} + +/// One member's claim values on the given routes, in canonical order: +/// `Sent` cells, plus `Staged` cells when `with_staged`. Clear proofs send +/// `with_staged = false` after the rounds; committed proofs commit +/// `with_staged = true`. +pub fn member_claim_values( + claims: &SumcheckOutputClaims, + routes: &ClaimRoutes, + with_staged: bool, +) -> Vec +where + F: JoltField, + I: ConcreteSumcheck, + SumcheckOutputClaims: OutputClaims>, + OpeningIdOf: Copy + Into, +{ + let static_aliases = static_aliases::(); + claims .canonical_order() .into_iter() - .map(Into::into) - .filter(|id| !skip(id)) - .map(|id| Ok((id, transcript.receive()?))) + .zip(claims.opening_values()) + .filter(|(id, _)| match routes.of(&(*id).into(), &static_aliases) { + ClaimRoute::Sent => true, + ClaimRoute::Staged => with_staged, + ClaimRoute::Alias(_) => false, + }) + .map(|(_, value)| value) .collect() } -/// Assemble one batch member's claims from the batch's received openings -/// (keyed by composite id). Each aliased opening takes its canonical source's -/// value, so an alias can never disagree with its source. Called by the -/// generated `receive_output_claims` per member. -pub fn assemble_member_claims( - received: &BTreeMap, -) -> Result, VerifierError> +/// A stage's committed claim layout over its derived output points: the +/// committed (`Sent` and `Staged`) cells' ids in row order, and each `Alias` +/// cell bound to its source row. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct CommittedClaimLayout { + pub ids: Vec, + pub aliases: Vec>, +} + +/// Appends one member's committed cells to `layout`, in canonical order over +/// the member's derived output `points`. +pub fn extend_committed_layout( + layout: &mut CommittedClaimLayout, + points: &SumcheckOutputPoints, + routes: &ClaimRoutes, +) where + F: JoltField, + I: ConcreteSumcheck, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy + Into, +{ + let static_aliases = static_aliases::(); + for id in point_cell_ids::(points).into_iter().map(Into::into) { + match routes.of(&id, &static_aliases) { + ClaimRoute::Alias(source) => layout.aliases.push(OpeningAlias::new(id, source)), + ClaimRoute::Sent | ClaimRoute::Staged => layout.ids.push(id), + } + } +} + +/// The ids of a member's output-point cells, in canonical order. +fn point_cell_ids(points: &SumcheckOutputPoints) -> Vec> where F: JoltField, I: ConcreteSumcheck, - SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Copy + Ord + Debug + Into, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy, { - let sources: BTreeMap = I::aliased_output_openings() - .into_iter() - .map(|(aliased, source)| (aliased.into(), source.into())) - .collect(); - assemble_member_claims_with::(received, |id| sources.get(id).copied()) + let mut ids = Vec::new(); + let Ok(_) = points.try_map_cells(&mut |id, _point| { + ids.push(*id); + Ok::(F::zero()) + }); + ids } -/// [`assemble_member_claims`] with each aliased opening's source chosen by -/// `source` instead of the member's static alias declaration. -pub fn assemble_member_claims_with( - received: &BTreeMap, - source: impl Fn(&ComposedOpeningId) -> Option, -) -> Result, VerifierError> +/// Assert a member's claim values have the shape of its derived output +/// points: the same canonical ids, so every `Vec` family has its length and +/// every `Option` cell its presence. The prover's self-check before sending. +pub fn validate_member_output_shape( + member: &I, + claims: &SumcheckOutputClaims, + points: &SumcheckOutputPoints, +) -> Result<(), VerifierError> where F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Copy + Ord + Debug + Into, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy + PartialEq, + RelationIdOf: Debug, { - SumcheckOutputClaims::::from_opening_values(|id| { - let id: ComposedOpeningId = (*id).into(); - received.get(&source(&id).unwrap_or(id)).copied() - }) - .map_err(|missing| VerifierError::MissingOpeningClaim { - id: missing.id.into(), - }) + let point_ids = point_cell_ids::(points); + if claims.canonical_order() != point_ids { + return Err(VerifierError::StageClaimSumcheckFailed { + stage: format!("{:?}", member.id()), + reason: format!( + "output claim shape mismatch: the points have {} cells, the claims {}", + point_ids.len(), + claims.canonical_order().len(), + ), + }); + } + Ok(()) } /// Enforce one member's declared cross-relation opening aliases: each aliased @@ -543,47 +570,6 @@ where Ok(()) } -/// Assert one member's wire claims match its expected output shape: the -/// provided `canonical_order` id-set, minus the member's aliased openings -/// (absorbed via their canonical source; their value equality is enforced -/// separately by `validate_aliases`), must equal the member's -/// [`wire_output_openings`](ConcreteSumcheck::wire_output_openings). Called by -/// the generated `validate_output_claims` per member. Family-agnostic (the -/// mismatch is attributed by the member's Debug-formatted relation id), so -/// mixed-family batches validate every member through it. -pub fn validate_member_output_shape( - member: &I, - claims: &SumcheckOutputClaims, -) -> Result<(), VerifierError> -where - F: JoltField, - I: ConcreteSumcheck, - SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Ord, -{ - let expected = member.wire_output_openings(); - let aliased: BTreeSet<_> = I::aliased_output_openings() - .into_iter() - .map(|(id, _)| id) - .collect(); - let provided: BTreeSet<_> = claims - .canonical_order() - .into_iter() - .filter(|id| !aliased.contains(id)) - .collect(); - if provided != expected { - return Err(VerifierError::StageClaimSumcheckFailed { - stage: format!("{:?}", member.id()), - reason: format!( - "output claim shape mismatch: expected {} openings, got {}", - expected.len(), - provided.len(), - ), - }); - } - Ok(()) -} - /// Project a composite-family derived id onto one relation's own public enum — /// the typed destructure every `derive_output_term` starts with, driven by the /// id family's generated `TryFrom` inverses of its `From` embeddings. A foreign @@ -1124,27 +1110,18 @@ mod tests { // `Fixture*Sumchecks` exist only to exercise `#[derive(SumcheckBatch)]`. #[expect(clippy::unwrap_used)] mod sumcheck_batch_derive_tests { - use super::SumcheckBatch; + use super::{ClaimRoutes, SumcheckBatch}; use crate::stages::stage5::{ InstructionReadRaf, InstructionReadRafOutputClaims, RegistersValEvaluation, RegistersValEvaluationOutputClaims, }; - use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafDimensions; use jolt_field::{Fr, JoltField, Ring}; - fn instruction_read_raf() -> InstructionReadRaf { - InstructionReadRaf::new(InstructionReadRafDimensions::try_from((5, 128, 3)).unwrap()) - } - - fn registers_val_evaluation() -> RegistersValEvaluation { - RegistersValEvaluation::new(TraceDimensions::new(4)) - } - #[derive(SumcheckBatch)] #[sumcheck_batch(crate = "crate")] - // The generated absorb resolves the alias skip-sets statically (no instance - // state), so this alias-free fixture's members are never read. + // The generated claim plumbing reads only the claims and routes, so this + // fixture's members are never read. #[expect(dead_code)] struct FixtureSumchecks { instruction_read_raf: InstructionReadRaf, @@ -1152,12 +1129,8 @@ mod sumcheck_batch_derive_tests { } #[test] - fn output_aggregate_opening_values_follow_declaration_order() { + fn wire_claims_follow_declaration_order() { let fr = Fr::from_u64; - let sumchecks = FixtureSumchecks { - instruction_read_raf: instruction_read_raf(), - registers_val_evaluation: registers_val_evaluation(), - }; let claims = FixtureOutputClaims:: { instruction_read_raf: InstructionReadRafOutputClaims { lookup_table_flags: vec![fr(1), fr(2)], @@ -1171,7 +1144,7 @@ mod sumcheck_batch_derive_tests { }; assert_eq!( - sumchecks.opening_values(&claims), + FixtureSumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), vec![fr(1), fr(2), fr(3), fr(4), fr(5), fr(6)], ); } @@ -1184,7 +1157,7 @@ mod sumcheck_batch_derive_tests { } #[test] - fn output_aggregate_chains_present_and_skips_absent_option_members() { + fn wire_claims_chain_present_and_skip_absent_option_members() { let fr = Fr::from_u64; let instruction = || InstructionReadRafOutputClaims { lookup_table_flags: vec![fr(1)], @@ -1192,10 +1165,6 @@ mod sumcheck_batch_derive_tests { instruction_raf_flag: fr(3), }; - let with_registers = FixtureOptionSumchecks { - instruction_read_raf: instruction_read_raf(), - registers_val_evaluation: Some(registers_val_evaluation()), - }; let present = FixtureOptionOutputClaims:: { instruction_read_raf: instruction(), registers_val_evaluation: Some(RegistersValEvaluationOutputClaims { @@ -1204,33 +1173,31 @@ mod sumcheck_batch_derive_tests { }), }; assert_eq!( - with_registers.opening_values(&present), + FixtureOptionSumchecks::wire_claim_values(&present, &ClaimRoutes::default()), vec![fr(1), fr(2), fr(3), fr(4), fr(5)] ); - let without_registers = FixtureOptionSumchecks { - instruction_read_raf: instruction_read_raf(), - registers_val_evaluation: None, - }; let absent = FixtureOptionOutputClaims:: { instruction_read_raf: instruction(), registers_val_evaluation: None, }; assert_eq!( - without_registers.opening_values(&absent), + FixtureOptionSumchecks::wire_claim_values(&absent, &ClaimRoutes::default()), vec![fr(1), fr(2), fr(3)] ); } - /// Wire claims supplied for an `Option` member whose instance did not run are - /// rejected by the generated `validate_output_claims` (attributed to the first - /// supplied opening id), and the well-formed absent case still validates. + /// Claims supplied for an `Option` member whose instance did not run are + /// rejected by the generated `validate_output_shape`, and the well-formed + /// absent case still validates. #[test] - fn validate_output_claims_rejects_claims_for_absent_member() { + fn validate_output_shape_rejects_claims_for_absent_member() { + use super::ConcreteSumcheck as _; + use jolt_claims::protocols::jolt::geometry::instruction::read_raf_output_openings; let fr = Fr::from_u64; - let dimensions = InstructionReadRafDimensions::try_from((5, 128, 3)).unwrap(); + let dimensions = InstructionReadRafDimensions::try_from((2, 6, 2)).unwrap(); let sumchecks = FixtureOptionSumchecks:: { instruction_read_raf: InstructionReadRaf::new(dimensions), registers_val_evaluation: None, @@ -1252,77 +1219,22 @@ mod sumcheck_batch_derive_tests { rd_wa: fr(2), }), }; + let challenges = vec![fr(7); sumchecks.instruction_read_raf.rounds()]; + let points = sumchecks + .derive_opening_points(&challenges, &sumchecks.empty_input_points()) + .unwrap(); assert!(matches!( - sumchecks.validate_output_claims(&unexpected), - Err(crate::VerifierError::UnexpectedOpeningClaim { .. }) + sumchecks.validate_output_shape(&unexpected, &points), + Err(crate::VerifierError::StageClaimSumcheckFailed { .. }) )); let well_formed = FixtureOptionOutputClaims:: { instruction_read_raf: instruction(), registers_val_evaluation: None, }; - assert!(sumchecks.validate_output_claims(&well_formed).is_ok()); - } - - // The opt-out fixture: `#[sumcheck_batch(no_opening_values)]` must still - // generate the five aggregate structs but emit NO `opening_values` / - // `receive_output_claims` / `verify_clear` on the source struct. The inherent `opening_values` - // below would collide with a generated one (the compiler rejects two inherent - // methods of the same name), so this module compiling at all proves the - // opt-out suppressed it. - #[derive(SumcheckBatch)] - #[sumcheck_batch(no_opening_values, crate = "crate")] - // The custom absorb below never reads the members (no aliased sets to consult). - #[expect(dead_code)] - struct FixtureCustomSumchecks { - instruction_read_raf: InstructionReadRaf, - registers_val_evaluation: RegistersValEvaluation, - } - - impl FixtureCustomSumchecks { - /// A curated order distinct from the generated declaration order, to prove - /// this is the one in effect (the generated method would chain instruction - /// then registers; this reverses them). - #[expect( - clippy::unused_self, - reason = "the signature mirrors the generated method it collides with" - )] - fn opening_values(&self, claims: &FixtureCustomOutputClaims) -> Vec { - use crate::stages::relations::OutputClaims as _; - claims - .registers_val_evaluation - .opening_values() - .into_iter() - .chain(claims.instruction_read_raf.opening_values()) - .collect() - } - } - - #[test] - fn no_opening_values_suppresses_generated_impl() { - let fr = Fr::from_u64; - let sumchecks = FixtureCustomSumchecks { - instruction_read_raf: instruction_read_raf(), - registers_val_evaluation: registers_val_evaluation(), - }; - let claims = FixtureCustomOutputClaims:: { - instruction_read_raf: InstructionReadRafOutputClaims { - lookup_table_flags: vec![fr(1)], - instruction_ra: vec![fr(2)], - instruction_raf_flag: fr(3), - }, - registers_val_evaluation: RegistersValEvaluationOutputClaims { - rd_inc: fr(4), - rd_wa: fr(5), - }, - }; - - // The hand-written curated order (registers first), proving no generated - // `opening_values` (which would be instruction-first) shadows or collides. - assert_eq!( - sumchecks.opening_values(&claims), - vec![fr(4), fr(5), fr(1), fr(2), fr(3)] - ); + assert!(sumchecks + .validate_output_shape(&well_formed, &points) + .is_ok()); } // The draw opt-out fixture: `#[sumcheck_batch(no_draw_challenges)]` must emit diff --git a/crates/jolt-verifier/src/stages/stage1/outputs.rs b/crates/jolt-verifier/src/stages/stage1/outputs.rs index 78c35fc8b2..4264e20508 100644 --- a/crates/jolt-verifier/src/stages/stage1/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage1/outputs.rs @@ -16,10 +16,8 @@ use crate::VerifierError; /// `#[derive(SumcheckBatch)]` generates the `Stage1BatchInputClaims` / /// `Stage1BatchInputPoints`, `Stage1BatchOutputClaims` / /// `Stage1BatchOutputPoints`, and `Stage1BatchChallenges` aggregates — one -/// field per instance, in this declaration order. With a single instance and no -/// cross-relation aliasing there is no `no_opening_values` opt-out: the -/// generated absorb (`opening_values` / `append_output_claims` on this struct) -/// delegates to the member's typed output claims: base columns followed by +/// field per instance, in this declaration order. The generated claim +/// plumbing walks the member's typed output claims: base columns followed by /// field-inline columns when enabled. /// /// The member's `SpartanOuterPublic` coefficient table depends on the batch's own @@ -143,6 +141,10 @@ pub struct Stage1ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] +#[expect( + clippy::large_enum_variant, + reason = "one stage output per proof, built once and never stored in bulk" +)] pub enum Stage1Output { Clear(Stage1ClearOutput), Zk(Stage1ZkOutput), diff --git a/crates/jolt-verifier/src/stages/stage1/verify.rs b/crates/jolt-verifier/src/stages/stage1/verify.rs index b91df4842e..e3d2619743 100644 --- a/crates/jolt-verifier/src/stages/stage1/verify.rs +++ b/crates/jolt-verifier/src/stages/stage1/verify.rs @@ -9,11 +9,7 @@ use super::outputs::{ Stage1BatchInputClaims, Stage1BatchSumchecks, Stage1Challenges, Stage1ClearOutput, Stage1Output, Stage1ZkOutput, }; -use crate::{ - stages::{uniskip, zk::committed, zk::outputs::CommittedOutputClaimOutput}, - verifier::CheckedInputs, - VerifierError, -}; +use crate::{stages::uniskip, verifier::CheckedInputs, VerifierError}; pub fn verify( checked: &CheckedInputs, @@ -81,12 +77,7 @@ where }; let input_points = sumchecks.empty_input_points(); - // Clear and committed proofs share the composed member's canonical order. - let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; - let (remainder_consistency, commitments) = - sumchecks.verify_zk(shape.row_count(), transcript)?; - let output_points = - sumchecks.derive_opening_points(&remainder_consistency.challenges(), &input_points)?; + let batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; Ok(Stage1Output::Zk(Stage1ZkOutput { challenges: Stage1Challenges { @@ -95,9 +86,9 @@ where }, uniskip_consistency: uniskip.consistency, uniskip_output_claims: uniskip.output_claims, - remainder_consistency, - remainder_output_claims: CommittedOutputClaimOutput { shape, commitments }, - output_points, + remainder_consistency: batch.consistency, + remainder_output_claims: batch.output_claims, + output_points: batch.output_points, })) } } diff --git a/crates/jolt-verifier/src/stages/stage2/outputs.rs b/crates/jolt-verifier/src/stages/stage2/outputs.rs index 55193f50d8..52386bef69 100644 --- a/crates/jolt-verifier/src/stages/stage2/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage2/outputs.rs @@ -164,6 +164,10 @@ pub struct Stage2ZkOutput { } #[derive(Clone, Debug, PartialEq, Eq)] +#[expect( + clippy::large_enum_variant, + reason = "one stage output per proof, built once and never stored in bulk" +)] pub enum Stage2Output { Clear(Stage2ClearOutput), Zk(Stage2ZkOutput), @@ -212,7 +216,7 @@ impl Stage2Output { mod tests { use super::*; use crate::stages::relations::test_transcript::assert_same_draws; - use crate::stages::relations::ConcreteSumcheck; + use crate::stages::relations::{ClaimRoutes, ConcreteSumcheck}; use common::jolt_device::{JoltDevice, MemoryConfig}; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; @@ -373,14 +377,14 @@ mod tests { /// distinct sentinels here to prove the skip is id-driven, not value-driven. #[cfg(not(feature = "field-inline"))] #[test] - fn opening_values_follow_canonical_order() { + fn wire_claims_follow_canonical_order() { let mut claims = consistent_values(); claims.instruction_claim_reduction.lookup_output = fr(101); claims.instruction_claim_reduction.left_instruction_input = fr(102); claims.instruction_claim_reduction.right_instruction_input = fr(103); assert_eq!( - sumchecks().opening_values(&claims), + Stage2BatchSumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), (1..=15).map(fr).collect::>() ); } @@ -392,7 +396,7 @@ mod tests { /// distinct sentinels to prove the id-driven skip still applies. #[cfg(feature = "field-inline")] #[test] - fn opening_values_follow_canonical_field_inline_order() { + fn wire_claims_follow_canonical_field_inline_order() { let mut claims = consistent_values(); claims.instruction_claim_reduction.lookup_output = fr(101); claims.instruction_claim_reduction.left_instruction_input = fr(102); @@ -412,27 +416,9 @@ mod tests { // RAM RAF evaluation, RAM output check. .chain([fr(14), fr(15)]) .collect::>(); - assert_eq!(sumchecks().opening_values(&claims), expected); - } - - /// The generated `output_claim_count` sums the members' wire sets: 16 - /// expression-referenced openings, minus the reduction's 3 aliases, plus the product - /// remainder's 2 staged openings — plus, under `field-inline`, the product member's 3 - /// field-inline openings (the field-inline reduction aliases them). - #[test] - fn output_claim_count_matches_absorbed_openings() { - let sumchecks = sumchecks(); - assert_eq!( - sumchecks.opening_values(&consistent_values()).len(), - sumchecks.output_claim_count() - ); assert_eq!( - sumchecks.output_claim_count(), - if cfg!(feature = "field-inline") { - 18 - } else { - 15 - } + Stage2BatchSumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), + expected ); } @@ -451,7 +437,7 @@ mod tests { ) )] fn alias_declarations_are_valid() { - use jolt_claims::SymbolicSumcheck as _; + use jolt_claims::{OutputClaims as _, SymbolicSumcheck as _}; use std::collections::BTreeSet; let sumchecks = sumchecks(); @@ -459,7 +445,11 @@ mod tests { .instruction_claim_reduction .symbolic() .expected_output_openings::(); - let source_wire_openings = sumchecks.product_remainder.wire_output_openings(); + let source_wire_openings: BTreeSet<_> = consistent_values() + .product_remainder + .canonical_order() + .into_iter() + .collect(); let pairs = InstructionClaimReduction::::aliased_output_openings(); assert_eq!(pairs.len(), 3); diff --git a/crates/jolt-verifier/src/stages/stage2/product_remainder.rs b/crates/jolt-verifier/src/stages/stage2/product_remainder.rs index 9df76a783a..5ec48045d9 100644 --- a/crates/jolt-verifier/src/stages/stage2/product_remainder.rs +++ b/crates/jolt-verifier/src/stages/stage2/product_remainder.rs @@ -10,7 +10,6 @@ #[cfg(feature = "field-inline")] use jolt_claims::protocols::composed::ComposedClaims; -use std::collections::BTreeSet; use jolt_claims::protocols::composed::geometry::SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE; #[cfg(feature = "field-inline")] @@ -29,7 +28,7 @@ pub use jolt_claims::protocols::jolt::relations::spartan::{ ProductRemainderInputClaims, ProductRemainderOutputClaims, }; use jolt_claims::protocols::jolt::{ - geometry::spartan, geometry::spartan::SpartanProductDimensions, JoltDerivedId, JoltRelationId, + geometry::spartan::SpartanProductDimensions, JoltDerivedId, JoltRelationId, SpartanProductVirtualizationPublic, }; use jolt_claims::{NoChallenges, SymbolicSumcheck}; @@ -108,26 +107,6 @@ impl ConcreteSumcheck for ProductRemainder { &self.symbolic } - #[cfg_attr( - not(feature = "field-inline"), - expect( - clippy::useless_conversion, - reason = "field-inline selects a composed claim or opening id" - ) - )] - fn wire_output_openings(&self) -> BTreeSet<::OpeningId> { - // Two wire openings beyond the output-`Expr`-referenced set: - // `write_lookup_output_to_rd` and `virtual_instruction` are absorbed here - // but their constraining fold happens downstream, in stage 6a's bytecode - // read-RAF input claim. - let mut openings = self.symbolic().expected_output_openings::(); - openings.extend::<[::OpeningId; 2]>([ - spartan::write_lookup_output_to_rd_product().into(), - spartan::virtual_instruction_product().into(), - ]); - openings - } - fn derive_opening_points( &self, sumcheck_point: &[F], diff --git a/crates/jolt-verifier/src/stages/stage2/verify.rs b/crates/jolt-verifier/src/stages/stage2/verify.rs index 0ca28d7c6c..c799323aee 100644 --- a/crates/jolt-verifier/src/stages/stage2/verify.rs +++ b/crates/jolt-verifier/src/stages/stage2/verify.rs @@ -37,7 +37,6 @@ use crate::{ relations::ConcreteSumcheck, stage1::{Stage1ClearOutput, Stage1Output}, uniskip, - zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -179,11 +178,7 @@ where }); }; // Both modes omit aliases from the canonical member output rows. - let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; - let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; - let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; - let output_points = - sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; + let batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; return Ok(Stage2Output::Zk(Stage2ZkOutput { challenges, @@ -192,9 +187,9 @@ where product_tau_high: uniskip.tau_high, product_uniskip_consistency: product_uniskip.consistency, product_uniskip_output_claims: product_uniskip.output_claims, - batch_consistency: consistency, - batch_output_claims, - output_points, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, + output_points: batch.output_points, })); } diff --git a/crates/jolt-verifier/src/stages/stage3/outputs.rs b/crates/jolt-verifier/src/stages/stage3/outputs.rs index 966f4b11a2..23d095006a 100644 --- a/crates/jolt-verifier/src/stages/stage3/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage3/outputs.rs @@ -102,7 +102,7 @@ impl Stage3Output { )] mod tests { use super::*; - use crate::stages::relations::ConcreteSumcheck; + use crate::stages::relations::{ClaimRoutes, ConcreteSumcheck}; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; use jolt_field::{Fr, Ring}; @@ -157,30 +157,18 @@ mod tests { /// their canonical sources). The aliased cells carry distinct sentinels here /// to prove the skip is id-driven, not value-driven. #[test] - fn opening_values_follow_canonical_order() { + fn wire_claims_follow_canonical_order() { let mut claims = consistent(); claims.instruction_input.unexpanded_pc = fr(101); claims.registers_claim_reduction.rs1_value = fr(102); claims.registers_claim_reduction.rs2_value = fr(103); assert_eq!( - sumchecks().opening_values(&claims), + Stage3Sumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), (1..=13).map(fr).collect::>() ); } - /// The generated `output_claim_count` sums the members' wire sets: the 16 - /// expression-referenced openings minus the 3 aliases. - #[test] - fn output_claim_count_matches_absorbed_openings() { - let sumchecks = sumchecks(); - assert_eq!(sumchecks.output_claim_count(), 13); - assert_eq!( - sumchecks.opening_values(&consistent()).len(), - sumchecks.output_claim_count(), - ); - } - /// Pins the stage's alias declarations: each aliased id is distinct and /// referenced by its declaring member's own output `Expr` (so the batch fold /// constrains the wire cell), and each canonical source is absorbed by its @@ -189,12 +177,17 @@ mod tests { /// on is pinned by `aliased_members_derive_identical_opening_points`. #[test] fn alias_declarations_are_valid() { - use jolt_claims::SymbolicSumcheck as _; + use jolt_claims::{OutputClaims as _, SymbolicSumcheck as _}; use std::collections::BTreeSet; let sumchecks = sumchecks(); - let shift_wire = sumchecks.shift.wire_output_openings(); - let instruction_wire = sumchecks.instruction_input.wire_output_openings(); + let claims = consistent(); + let shift_wire: BTreeSet<_> = claims.shift.canonical_order().into_iter().collect(); + let instruction_wire: BTreeSet<_> = claims + .instruction_input + .canonical_order() + .into_iter() + .collect(); let instruction_pairs = InstructionInput::::aliased_output_openings(); assert_eq!(instruction_pairs.len(), 1); diff --git a/crates/jolt-verifier/src/stages/stage3/verify.rs b/crates/jolt-verifier/src/stages/stage3/verify.rs index e817e90706..da32b83b6d 100644 --- a/crates/jolt-verifier/src/stages/stage3/verify.rs +++ b/crates/jolt-verifier/src/stages/stage3/verify.rs @@ -20,7 +20,6 @@ use crate::{ stages::{ stage1::{Stage1BatchOutputClaims, Stage1Output}, stage2::{Stage2BatchOutputClaims, Stage2Output}, - zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -95,17 +94,17 @@ where } { - let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; - let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; - let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; - let output_points = sumchecks - .derive_opening_points(&consistency.challenges(), &sumchecks.empty_input_points())?; + let batch = sumchecks.verify_zk( + checked.committed_row_len()?, + &sumchecks.empty_input_points(), + transcript, + )?; Ok(Stage3Output::Zk(Stage3ZkOutput { challenges, - batch_consistency: consistency, - batch_output_claims, - output_points, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, + output_points: batch.output_points, })) } } diff --git a/crates/jolt-verifier/src/stages/stage4/outputs.rs b/crates/jolt-verifier/src/stages/stage4/outputs.rs index b76af63e8f..d09bc52836 100644 --- a/crates/jolt-verifier/src/stages/stage4/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage4/outputs.rs @@ -1,14 +1,9 @@ //! Typed inputs consumed and outputs produced by stage 4 verification. -use std::collections::BTreeMap; - use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; -use jolt_transcript::{Sponge, VerifierTranscript}; -use crate::stages::relations::{ - assemble_member_claims, receive_member_openings, OutputClaims, SumcheckBatch, -}; +use crate::stages::relations::{ClaimRoutes, SumcheckBatch}; use crate::stages::zk::outputs::CommittedOutputClaimOutput; use crate::VerifierError; @@ -29,24 +24,14 @@ use super::registers_read_write_checking::{ /// `Stage4InputPoints`, `Stage4OutputClaims`, `Stage4OutputPoints`, and /// `Stage4Challenges` aggregates — one field per instance, in this declaration order. /// -/// The RAM value-check instance produces *more* openings than the register one: -/// besides its main `ram_ra`/`ram_inc`, it also stages the `Val_init` advice -/// contributions and (in committed program mode) the program-image contribution. -/// Those staged openings are folded into `RamValCheckOutputClaims`, so the -/// aggregate is genuinely one-field-per-instance. But the stage-4 Fiat-Shamir -/// append order interleaves them around the register openings — advice + -/// program-image come *before* the register openings, then `ram_ra`/`ram_inc` -/// come *after* — which a plain per-instance concatenation cannot express. The -/// stage therefore opts out of the generated absorb methods via -/// `#[sumcheck_batch(no_opening_values)]` and supplies the exact interleaved -/// order below. -/// -/// The RAM value-check member's wire set extends its output `Expr` -/// (`ram_ra`/`ram_inc`) with the present staged advice / program-image openings -/// (see its `wire_output_openings` override), so the generated output-shape -/// count/validator cover their presence and count. +/// Besides its `ram_ra`/`ram_inc`, the RAM value-check member produces the +/// staged `Val_init` advice and program-image contribution openings +/// ([`RamValCheckStagedOpenings`]), +/// routed [`ClaimRoute::Staged`](crate::stages::relations::ClaimRoute::Staged): +/// a clear proof sends them before the batch, and a committed proof commits +/// them in declaration order with the rest of the stage's claims. #[derive(SumcheckBatch)] -#[sumcheck_batch(no_opening_values, crate = "crate")] +#[sumcheck_batch(routes, crate = "crate")] pub struct Stage4Sumchecks { pub registers_read_write: RegistersReadWriteChecking, /// The field-inline Twist read/write instance over `T * 2^log_k`. Declaration position @@ -76,94 +61,14 @@ impl Stage4OutputClaims { } } -impl Stage4Sumchecks { - /// The hand-written replacement for the absorb method the - /// `no_opening_values` opt-out suppresses: the committed (ZK) output rows - /// follow the claims aggregate's curated canonical order. - /// Same signature as the generated method, so the generated prove - /// driver's default curation serves this stage unchanged. - pub fn opening_values(&self, claims: &Stage4OutputClaims) -> Vec { - claims.opening_values() - } - - /// The openings a clear proof sends after the stage-4 rounds: the register - /// members' openings, then the RAM value-check's `ram_ra`/`ram_inc`. The - /// staged contributions travel before the batch - /// ([`RamValCheckStagedOpenings`]). - pub fn post_round_opening_values(&self, claims: &Stage4OutputClaims) -> Vec { - let mut values = claims.registers_read_write.opening_values(); - #[cfg(feature = "field-inline")] - values.extend(claims.field_registers_read_write.opening_values()); - values.extend([claims.ram_val_check.ram_ra, claims.ram_val_check.ram_inc]); - values - } - - /// Receives the post-round openings in - /// [`post_round_opening_values`](Self::post_round_opening_values) order and - /// completes the claims with the already-received `staged` openings. - pub fn receive_output_claims( - &self, - staged: RamValCheckStagedOpenings, - transcript: &mut VerifierTranscript<'_, H>, - ) -> Result, VerifierError> { - let registers: BTreeMap<_, _> = - receive_member_openings(&self.registers_read_write, transcript)? - .into_iter() - .collect(); - let registers_read_write = - assemble_member_claims::>(®isters)?; - #[cfg(feature = "field-inline")] - let field_registers_read_write = { - let received: BTreeMap<_, _> = - receive_member_openings(&self.field_registers_read_write, transcript)? - .into_iter() - .collect(); - assemble_member_claims::>(&received)? - }; - - Ok(Stage4OutputClaims { - registers_read_write, - #[cfg(feature = "field-inline")] - field_registers_read_write, - ram_val_check: RamValCheckOutputClaims { - untrusted_advice: staged.untrusted_advice, - trusted_advice: staged.trusted_advice, - program_image: staged.program_image, - ram_ra: transcript.receive()?, - ram_inc: transcript.receive()?, - }, - }) - } -} - -impl Stage4OutputClaims { - /// The produced opening claims in canonical order, the committed (ZK) output - /// row order: the `Val_init` advice openings, the committed program-image - /// contribution, the register read-write openings, under `field-inline` the - /// five field-register read-write openings (the spec's committed row order: - /// after the ordinary register openings, before the RAM value-check ones), - /// then the RAM value-check `ram_ra`/`ram_inc` openings. The advice and - /// program-image openings are produced by the RAM value-check instance but - /// come first (before the registers), so this is hand-written rather than a - /// per-instance concatenation — see [`Stage4Sumchecks`]. - pub fn opening_values(&self) -> Vec { - let ram = &self.ram_val_check; - let mut values: Vec = ram - .untrusted_advice - .into_iter() - .chain(ram.trusted_advice) - .chain(ram.program_image) - .chain(self.registers_read_write.opening_values()) - .collect(); - #[cfg(feature = "field-inline")] - values.extend(self.field_registers_read_write.opening_values()); - values.extend([ram.ram_ra, ram.ram_inc]); - values - } -} - /// The shared opening-point accessors over the point-only stage-4 aggregate. impl Stage4OutputPoints { + /// The stage's claim routes: the RAM value check's staged contribution + /// cells are [`ClaimRoute::Staged`](crate::stages::relations::ClaimRoute::Staged). + pub fn claim_routes(&self) -> Result { + Ok(RamValCheckStagedOpenings::from_claims(&self.ram_val_check).claim_routes()) + } + /// The register read-write opening point (shared by all five register /// openings). pub fn registers_read_write_point(&self) -> &[F] { @@ -189,10 +94,7 @@ pub struct Stage4ClearOutput { /// the Fiat-Shamir opening-claim encoder. pub output_values: Stage4OutputClaims, /// The produced stage-4 opening *points*, paired field-for-field with - /// `output_values` for the register and RAM value-check leaves. The advice / - /// program-image opening points are carried on `ram_val_check_init` (they sit at - /// the staged RAM address sub-point, not the batch sumcheck point), so they are - /// left absent here. + /// `output_values`. pub output_points: Stage4OutputPoints, pub ram_val_check_init: RamValCheckInitialEvaluation, } @@ -204,9 +106,7 @@ pub struct Stage4ZkOutput { pub batch_output_claims: CommittedOutputClaimOutput, pub ram_val_check_public_eval: F, /// The produced opening points, the ZK counterpart of the clear path's - /// `output_points`. Read through the same `*_point()` accessors. The advice / - /// program-image leaves are absent in ZK (BlindFold carries those openings), so - /// only the register and RAM value-check points are populated. + /// `output_points`. Read through the same `*_point()` accessors. pub output_points: Stage4OutputPoints, } @@ -304,48 +204,44 @@ mod tests { Vec::new() } - /// Locks the stage-4 Fiat-Shamir append order against silent drift: with no staged advice - /// / program-image openings, the order is the five register openings, under `field-inline` - /// the five field-inline openings, then the two RAM value-check openings. A wrong order - /// here silently breaks soundness, so it is pinned with distinct sentinels. - #[test] - fn opening_values_follow_canonical_order_without_advice() { - let expected: Vec = (3..=7) - .map(fr) - .chain(field_inline_splice()) - .chain([fr(8), fr(9)]) - .collect(); - assert_eq!(claims_with_advice(false).opening_values(), expected); + /// The staged routes when every staged contribution is present. + fn staged_routes() -> ClaimRoutes { + RamValCheckStagedOpenings::> { + untrusted_advice: Some(Vec::new()), + trusted_advice: Some(Vec::new()), + program_image: Some(Vec::new()), + } + .claim_routes() } - /// The full interleaved order: advice (untrusted, trusted) and the program-image - /// contribution come *first*, then the five register openings, under `field-inline` the - /// five field-inline openings, then `ram_ra`/`ram_inc` last — exactly matching the - /// prover's stage-4 `pending_claims` flush order. + /// The clear post-round claims, pinned with distinct sentinels: the five + /// register openings, under `field-inline` the five field-register openings, + /// then `ram_ra`/`ram_inc`; the staged contributions travel before the batch. #[test] - fn opening_values_interleave_advice_then_registers_then_ram() { - let expected: Vec = [fr(1), fr(2), fr(10)] - .into_iter() - .chain((3..=7).map(fr)) + fn wire_claims_omit_staged_openings() { + let expected: Vec = (3..=7) + .map(fr) .chain(field_inline_splice()) .chain([fr(8), fr(9)]) .collect(); - assert_eq!(claims_with_advice(true).opening_values(), expected); + assert_eq!( + Stage4Sumchecks::wire_claim_values(&claims_with_advice(true), &staged_routes()), + expected + ); } - /// The openings sent after the rounds exclude the staged advice and - /// program-image openings, which travel before the batch: the five register - /// openings, under `field-inline` the five field-inline openings, then - /// `ram_ra`/`ram_inc`. + /// The committed rows are declaration order: the registers, under + /// `field-inline` the field registers, then the RAM value check's fields + /// (untrusted, trusted, program image, `ram_ra`, `ram_inc`). #[test] - fn post_round_opening_values_omit_staged_openings() { + fn committed_claims_follow_declaration_order() { let expected: Vec = (3..=7) .map(fr) .chain(field_inline_splice()) - .chain([fr(8), fr(9)]) + .chain([fr(1), fr(2), fr(10), fr(8), fr(9)]) .collect(); assert_eq!( - sumchecks().post_round_opening_values(&claims_with_advice(true)), + Stage4Sumchecks::committed_claim_values(&claims_with_advice(true), &staged_routes()), expected ); } @@ -368,6 +264,7 @@ mod tests { TraceDimensions::new(log_t), ram_log_k, RamValCheckInit::from(fr(0)), + RamValCheckStagedOpenings::default(), ), } } @@ -401,21 +298,4 @@ mod tests { ]; assert_eq!(drawn_gammas, gammas); } - - /// The generated `output_claim_count` sums the members' wire sets: the five register - /// openings and the two RAM value-check ones (no staged advice / program-image - /// contributions in this fixture) — plus, under `field-inline`, the field-register - /// read-write member's five. - #[test] - fn output_claim_count_matches_absorbed_openings() { - let sumchecks = sumchecks(); - #[cfg(not(feature = "field-inline"))] - assert_eq!(sumchecks.output_claim_count(), 7); - #[cfg(feature = "field-inline")] - assert_eq!(sumchecks.output_claim_count(), 12); - assert_eq!( - claims_with_advice(false).opening_values().len(), - sumchecks.output_claim_count(), - ); - } } diff --git a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs index 75246626af..e62d487323 100644 --- a/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs +++ b/crates/jolt-verifier/src/stages/stage4/ram_val_check.rs @@ -14,6 +14,7 @@ //! fields and in the serialized `Stage4OutputClaims` aggregate. Only their values feed //! the input claim; their staged points are carried for completeness. +use core::convert::Infallible; pub use jolt_claims::protocols::jolt::relations::ram::{ RamValCheckChallenges, RamValCheckInputClaims, RamValCheckOutputClaims, }; @@ -24,14 +25,16 @@ use jolt_claims::protocols::jolt::{ ram::{self, RamValCheckInit, RamValCheckInitContribution}, }, relations::ram::{RamValCheck as RamValCheckSymbolic, RamValCheckShape, RamValContribution}, - JoltAdviceKind, JoltDerivedId, JoltOpeningId, JoltRelationId, RamValCheckPublic, + JoltAdviceKind, JoltDerivedId, JoltRelationId, RamValCheckPublic, }; -use jolt_claims::SymbolicSumcheck; +use std::collections::BTreeMap; + +use jolt_claims::{MapCells, OutputClaims, SymbolicSumcheck}; use jolt_field::JoltField; use jolt_poly::{block_selector_mle_msb, LtPolynomial}; -use jolt_transcript::{Sponge, VerifierTranscript}; +use jolt_transcript::{ProverTranscript, Sponge, VerifierTranscript}; -use crate::stages::relations::ConcreteSumcheck; +use crate::stages::relations::{ClaimRoute, ClaimRoutes, ComposedOpeningId, ConcreteSumcheck}; use crate::stages::stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints}; use crate::verifier::CheckedInputs; use crate::VerifierError; @@ -91,12 +94,10 @@ pub struct RamValCheck { /// The negated block selector for each present `Val_init` contribution — /// resolves the `InitSelector`/`InitSelectorProgramImage` input publics. init_selectors: Vec<(RamValCheckPublic, F)>, - /// The present `Val_init` contribution openings (advice / program image): - /// staged on the stage-4 wire but consumed by this relation's *input* `Expr` - /// (the init-eval decomposition) and the stage-6/7 reductions, so they extend - /// [`wire_output_openings`](ConcreteSumcheck::wire_output_openings) beyond the - /// output-`Expr` set. - contribution_openings: Vec, + /// The points of the present staged `Val_init` contribution openings + /// (advice / program image), which sit at the staged RAM address sub-point + /// rather than the batch point. + staged_points: RamValCheckStagedOpenings>, } impl RamValCheck { @@ -110,6 +111,7 @@ impl RamValCheck { trace_dimensions: TraceDimensions, ram_log_k: usize, init: RamValCheckInit, + staged_points: RamValCheckStagedOpenings>, ) -> Self { let public_eval = init.public_eval; let init_selectors = init @@ -117,11 +119,6 @@ impl RamValCheck { .iter() .map(|contribution| (contribution.selector, contribution.neg_selector)) .collect(); - let contribution_openings = init - .contributions - .iter() - .map(|contribution| contribution.opening) - .collect(); let symbolic = RamValCheckSymbolic::new(RamValCheckShape { dimensions: trace_dimensions, contributions: init @@ -139,7 +136,7 @@ impl RamValCheck { ram_log_k, public_eval, init_selectors, - contribution_openings, + staged_points, } } @@ -166,16 +163,6 @@ impl ConcreteSumcheck for RamValCheck { &self.symbolic } - fn wire_output_openings(&self) -> std::collections::BTreeSet { - // Wire openings beyond the output-`Expr` set (`ram_ra`/`ram_inc`): the - // present staged `Val_init` contribution openings (advice / - // program-image), consumed by this relation's input `Expr` and the - // stage-6/7 reductions rather than its own output fold. - let mut openings = self.symbolic().expected_output_openings::(); - openings.extend(self.contribution_openings.iter().copied()); - openings - } - fn derive_opening_points( &self, sumcheck_point: &[F], @@ -202,14 +189,15 @@ impl ConcreteSumcheck for RamValCheck { .cycle_opening_point(sumcheck_point) .map_err(public_input_failed)?; let opening_point = [r_address, cycle.as_slice()].concat(); - // The advice / program-image points sit at the staged RAM address sub-point, - // not the batch sumcheck point; downstream reads them from - // `RamValCheckInitialEvaluation` (clear) or BlindFold's own init decomposition - // (ZK), so they are left absent here. + let RamValCheckStagedOpenings { + untrusted_advice, + trusted_advice, + program_image, + } = self.staged_points.clone(); Ok(RamValCheckOutputClaims { - untrusted_advice: None, - trusted_advice: None, - program_image: None, + untrusted_advice, + trusted_advice, + program_image, ram_ra: opening_point.clone(), ram_inc: opening_point, }) @@ -307,6 +295,20 @@ pub struct RamValCheckInitStructure { } impl RamValCheckInitStructure { + /// The staged contribution openings' points: each advice block's opening + /// point and the program image's full RAM address point. + pub fn staged_points(&self) -> RamValCheckStagedOpenings> { + let advice = |kind| { + self.advice_block(kind) + .map(|block| block.opening_point.clone()) + }; + RamValCheckStagedOpenings { + untrusted_advice: advice(JoltAdviceKind::Untrusted), + trusted_advice: advice(JoltAdviceKind::Trusted), + program_image: self.program_image_point.clone(), + } + } + pub fn advice_block(&self, kind: JoltAdviceKind) -> Option<&RamValCheckAdviceBlock> { self.advice_blocks .iter() @@ -428,51 +430,68 @@ pub struct VerifiedRamValCheckAdviceContribution { } /// The RAM value-check's staged `Val_init` contribution openings, each present -/// exactly when the init structure has that contribution. A clear proof sends -/// them before the stage-4 batch, because the RAM value-check input claim -/// consumes them; a committed proof carries them in its output-claim rows. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct RamValCheckStagedOpenings { - pub untrusted_advice: Option, - pub trusted_advice: Option, - pub program_image: Option, +/// exactly when the init structure has that contribution: the stage-4 claim +/// cells a clear proof sends before the batch (the RAM value-check input claim +/// consumes them), and that a committed proof commits with the rest of the +/// stage's claims. Generic over the cell: the points come from +/// [`RamValCheckInitStructure::staged_points`]; the cells and their order are +/// the matching fields of [`RamValCheckOutputClaims`]. +#[derive(Clone, Debug, Default, PartialEq, Eq, OutputClaims)] +#[relation(RamValCheck)] +pub struct RamValCheckStagedOpenings { + #[opening(untrusted_advice)] + pub untrusted_advice: Option, + #[opening(trusted_advice)] + pub trusted_advice: Option, + #[opening(ProgramImageInitContributionRw)] + pub program_image: Option, } impl RamValCheckStagedOpenings { - /// The staged openings of a produced claims struct. - pub fn from_claims(claims: &RamValCheckOutputClaims) -> Self { - Self { - untrusted_advice: claims.untrusted_advice, - trusted_advice: claims.trusted_advice, - program_image: claims.program_image, - } + /// Sends the present openings, in canonical order. + pub fn send(&self, transcript: &mut ProverTranscript) { + transcript.send_all(&self.opening_values()); } - /// The present openings, in send order. - pub fn values(&self) -> Vec { - self.untrusted_advice + /// Receives the openings in the shape of their staged `points`. + pub fn receive( + points: &RamValCheckStagedOpenings>, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + points.try_map_cells(&mut |_, _point| transcript.receive().map_err(Into::into)) + } + + /// The received openings by id, the pre-filled `Staged` cells of the + /// stage-4 claims receive. + pub fn by_id(&self) -> BTreeMap { + self.canonical_order() .into_iter() - .chain(self.trusted_advice) - .chain(self.program_image) + .map(Into::into) + .zip(self.opening_values()) .collect() } +} - /// Receives the openings `structure` has contributions for. - pub fn receive( - structure: &RamValCheckInitStructure, - transcript: &mut VerifierTranscript<'_, H>, - ) -> Result { - let mut receive = |present: bool| -> Result, VerifierError> { - present - .then(|| transcript.receive()) - .transpose() - .map_err(Into::into) - }; - Ok(Self { - untrusted_advice: receive(structure.advice_block(JoltAdviceKind::Untrusted).is_some())?, - trusted_advice: receive(structure.advice_block(JoltAdviceKind::Trusted).is_some())?, - program_image: receive(structure.program_image_point.is_some())?, - }) +impl RamValCheckStagedOpenings { + /// The staged cells of the RAM value check's claims (or points). + pub fn from_claims(claims: &RamValCheckOutputClaims) -> Self { + Self { + untrusted_advice: claims.untrusted_advice.clone(), + trusted_advice: claims.trusted_advice.clone(), + program_image: claims.program_image.clone(), + } + } +} + +impl RamValCheckStagedOpenings> { + /// The stage-4 claim routes: every staged cell is [`ClaimRoute::Staged`]. + pub fn claim_routes(&self) -> ClaimRoutes { + let mut routes = ClaimRoutes::default(); + let Ok(_) = self.try_map_cells(&mut |id, _point| { + routes.set(*id, ClaimRoute::Staged); + Ok::(F::zero()) + }); + routes } } diff --git a/crates/jolt-verifier/src/stages/stage4/verify.rs b/crates/jolt-verifier/src/stages/stage4/verify.rs index 13661bb3c4..1058a7ac62 100644 --- a/crates/jolt-verifier/src/stages/stage4/verify.rs +++ b/crates/jolt-verifier/src/stages/stage4/verify.rs @@ -42,7 +42,6 @@ use crate::{ stages::{ stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage3::{Stage3Output, Stage3OutputClaims, Stage3OutputPoints}, - zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -149,6 +148,7 @@ where r_address, ram_val_check_public_eval, )?; + let staged_points = init_structure.staged_points(); // Field-register dimensions use the compile-time config's phase split, so they need // no validation of a proof-supplied split like the ordinary register dimensions above. @@ -160,7 +160,12 @@ where .field_inline .read_write_dimensions(log_t), ), - ram_val_check: RamValCheck::new(trace_dimensions, log_k, init_structure.decomposition()), + ram_val_check: RamValCheck::new( + trace_dimensions, + log_k, + init_structure.decomposition(), + staged_points.clone(), + ), }; // Draw the batching gammas in declaration order: the registers gamma, under `field-inline` @@ -174,7 +179,7 @@ where // The staged advice / program-image openings feed the RAM value-check input // claim, so they arrive before the batch; attaching them also carries them // downstream for the stage-6/7 address-phase reductions. - let staged = RamValCheckStagedOpenings::receive(&init_structure, transcript)?; + let staged = RamValCheckStagedOpenings::receive(&staged_points, transcript)?; let ram_val_check_init = ram_val_check_initial_evaluation(&init_structure, &staged)?; let input_values = stage4_input_values_from_upstream( @@ -194,7 +199,14 @@ where &challenges, transcript, 4, - |sumchecks, _, transcript| sumchecks.receive_output_claims(staged, transcript), + |points, transcript| { + Stage4Sumchecks::receive_output_claims( + points, + &Stage4Sumchecks::claim_routes(points)?, + staged.by_id(), + transcript, + ) + }, )?; return Ok(Stage4Output::Clear(Stage4ClearOutput { @@ -205,28 +217,22 @@ where } { - let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; - let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; - let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; - // Built via the same wiring as the clear path, off the ZK-agnostic upstream - // output points and init structure. Advice / program-image openings live in - // BlindFold for ZK proofs, so `derive_opening_points` leaves those leaves - // absent in the produced points. + // output points and init structure. The staged cells are committed rows + // like the rest of the stage's claims. let input_points = stage4_input_points_from_upstream( stage2.batch_output_points(), stage3.output_points(), &init_structure, ); - let output_points = - sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; + let batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; Ok(Stage4Output::Zk(Stage4ZkOutput { challenges, - batch_consistency: consistency, - batch_output_claims, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, ram_val_check_public_eval, - output_points, + output_points: batch.output_points, })) } } diff --git a/crates/jolt-verifier/src/stages/stage5/outputs.rs b/crates/jolt-verifier/src/stages/stage5/outputs.rs index c06c64b4e5..4bf75bb864 100644 --- a/crates/jolt-verifier/src/stages/stage5/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage5/outputs.rs @@ -170,6 +170,7 @@ impl Stage5Output { mod tests { use super::*; use crate::stages::relations::test_transcript::assert_same_draws; + use crate::stages::relations::ClaimRoutes; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; @@ -226,12 +227,15 @@ mod tests { /// own per-field order from its `OutputClaims` derive. A wrong batch order here silently /// breaks soundness, so it is pinned with distinct sentinels. #[test] - fn opening_values_follow_canonical_order() { + fn wire_claims_follow_canonical_order() { #[cfg(not(feature = "field-inline"))] let expected = (1..=8).map(fr).collect::>(); #[cfg(feature = "field-inline")] let expected = (1..=10).map(fr).collect::>(); - assert_eq!(sumchecks().opening_values(&claims()), expected); + assert_eq!( + Stage5Sumchecks::wire_claim_values(&claims(), &ClaimRoutes::default()), + expected + ); } /// The batch draws the instruction gamma, then the RAM-RA gamma, each one @@ -254,31 +258,4 @@ mod tests { gammas ); } - - /// The field-register value-evaluation member's wire set is exactly the two spec outputs - /// (`FieldRdInc`, `FieldRdWa` at `FieldRegistersValEvaluation`), so composing it grows the - /// stage-5 absorbed/committed opening count by two. - #[cfg(feature = "field-inline")] - #[test] - fn field_registers_val_evaluation_wire_set_is_the_two_spec_outputs() { - use crate::stages::relations::ConcreteSumcheck as _; - use jolt_claims::protocols::field_inline::geometry::registers::val_evaluation_output_openings; - - let sumchecks = sumchecks(); - let wire = sumchecks - .field_registers_val_evaluation - .wire_output_openings(); - assert_eq!(wire, val_evaluation_output_openings().into_iter().collect()); - - let others = sumchecks.instruction_read_raf.wire_output_openings().len() - + sumchecks - .ram_ra_claim_reduction - .wire_output_openings() - .len() - + sumchecks - .registers_val_evaluation - .wire_output_openings() - .len(); - assert_eq!(sumchecks.output_claim_count(), others + 2); - } } diff --git a/crates/jolt-verifier/src/stages/stage5/verify.rs b/crates/jolt-verifier/src/stages/stage5/verify.rs index 67941c1614..c39c0cc3b9 100644 --- a/crates/jolt-verifier/src/stages/stage5/verify.rs +++ b/crates/jolt-verifier/src/stages/stage5/verify.rs @@ -33,7 +33,6 @@ use crate::{ stages::{ stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage4::{Stage4Output, Stage4OutputClaims, Stage4OutputPoints}, - zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -137,24 +136,19 @@ where { let stage2 = stage2.zk()?; let stage4 = stage4.zk()?; - let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; - let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; - let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; - // Built via the same wiring as the clear path, off the ZK-agnostic upstream // output points; `derive_opening_points` ignores the instruction relation's // input points, so carrying the real claim-reduction point here is harmless. let input_points = stage5_input_points_from_upstream(&stage2.output_points, &stage4.output_points); - let output_points = - sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; - let instruction_r_address = output_points.instruction_r_address(); + let batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; + let instruction_r_address = batch.output_points.instruction_r_address(); Ok(Stage5Output::Zk(Stage5ZkOutput { challenges, - batch_consistency: consistency, - batch_output_claims, - output_points, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, + output_points: batch.output_points, instruction_r_address, })) } diff --git a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs index 3d5837a4cb..475ed73065 100644 --- a/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6a/bytecode_read_raf.rs @@ -17,7 +17,6 @@ use jolt_claims::protocols::composed::ComposedClaims; use jolt_claims::protocols::composed::ReadRafAddressPhase as ComposedReadRafAddressPhase; #[cfg(not(feature = "akita"))] use relations::bytecode::ReadRafAddressPhase as BaseAddressPhaseSymbolic; -use std::collections::BTreeSet; #[cfg(not(feature = "akita"))] use jolt_claims::protocols::jolt::relations; @@ -360,21 +359,6 @@ impl ConcreteSumcheck for BytecodeReadRafAddressPhase { &self.symbolic } - fn wire_output_openings( - &self, - ) -> BTreeSet<::OpeningId> { - // Committed-program mode absorbs the staged `BytecodeValClaim` columns - // beyond the output-`Expr` set (the address-phase intermediate); their - // constraining fold happens in stage 6b's bytecode claim reduction. - let mut openings = self.symbolic().expected_output_openings::(); - openings.extend((0..self.num_val_stages()).map(|i| { - ::OpeningId::from( - bytecode_reduction::bytecode_val_stage_opening(i), - ) - })); - openings - } - #[cfg_attr( not(feature = "field-inline"), expect( diff --git a/crates/jolt-verifier/src/stages/stage6a/outputs.rs b/crates/jolt-verifier/src/stages/stage6a/outputs.rs index 746ecbb9f8..a9c0e6ab4a 100644 --- a/crates/jolt-verifier/src/stages/stage6a/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage6a/outputs.rs @@ -27,10 +27,9 @@ use super::bytecode_read_raf::BytecodeReadRafAddressPhase; /// `intermediate` then `val_stages`, then booleanity's `intermediate`) is the /// canonical Fiat-Shamir order. /// -/// The bytecode read-RAF member's wire set extends its output `Expr` with the -/// committed-program-only staged `BytecodeValClaim` openings (see its -/// `wire_output_openings` override), so the generated output-shape -/// count/validator cover the val-stage presence and count. +/// The bytecode read-RAF member's claims carry the committed-program-only +/// `BytecodeValClaim` openings beyond its output `Expr`; their points (and so +/// their presence and count) come from its `derive_opening_points`. /// /// The generated `draw_challenges` draws each member in declaration order — /// the bytecode member's six gammas (its default per-field draw), then the diff --git a/crates/jolt-verifier/src/stages/stage6a/verify.rs b/crates/jolt-verifier/src/stages/stage6a/verify.rs index 252fe39258..d02d2c7e4a 100644 --- a/crates/jolt-verifier/src/stages/stage6a/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6a/verify.rs @@ -23,12 +23,8 @@ use super::{ use crate::{ preprocessing::JoltVerifierPreprocessing, stages::{ - stage1::Stage1Output, - stage2::Stage2Output, - stage3::Stage3Output, - stage4::Stage4Output, + stage1::Stage1Output, stage2::Stage2Output, stage3::Stage3Output, stage4::Stage4Output, stage5::Stage5Output, - zk::{committed, outputs::CommittedOutputClaimOutput}, }, verifier::CheckedInputs, VerifierError, @@ -94,17 +90,16 @@ where let address_input_points = address_sumchecks.empty_input_points(); if checked.zk { - let shape = committed::output_claim_shape(checked, address_sumchecks.output_claim_count())?; - let (consistency, commitments) = - address_sumchecks.verify_zk(shape.row_count(), transcript)?; - let output_claims = CommittedOutputClaimOutput { shape, commitments }; - let output_points = address_sumchecks - .derive_opening_points(&consistency.challenges(), &address_input_points)?; + let batch = address_sumchecks.verify_zk( + checked.committed_row_len()?, + &address_input_points, + transcript, + )?; return Ok(Stage6aOutput::Zk(Stage6aZkOutput { challenges: carried, - consistency, - output_claims, - output_points, + consistency: batch.consistency, + output_claims: batch.output_claims, + output_points: batch.output_points, })); } @@ -180,6 +175,7 @@ mod tests { use super::super::outputs::Stage6aOutputClaims; use super::*; use crate::stages::relations::test_transcript::{assert_same_draws, fresh}; + use crate::stages::relations::ClaimRoutes; use jolt_claims::protocols::jolt::geometry::booleanity::BooleanityDimensions; use jolt_claims::protocols::jolt::geometry::bytecode::BytecodeReadRafDimensions; use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; @@ -304,13 +300,12 @@ mod tests { /// Locks the stage-6a address-phase opening order: bytecode read-RAF /// `intermediate`, each `val_stages` entry, then booleanity `intermediate`. #[test] - fn stage6a_opening_values_follow_canonical_order() { - let sumchecks = sumchecks(Vec::new(), Vec::new()); + fn stage6a_wire_claims_follow_canonical_order() { let mut claims = sample_claims(); claims.bytecode_read_raf.val_stages = (903..908).map(fr).collect(); assert_eq!( - sumchecks.opening_values(&claims), + Stage6aSumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), [901, 903, 904, 905, 906, 907, 902].map(fr).to_vec() ); } diff --git a/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs index 1b1bfad705..22884a1789 100644 --- a/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs @@ -955,9 +955,9 @@ impl BytecodeReadRafCycle { /// the differing halves — `expected_output` (output `Expr`) and /// `derive_opening_points` — is overridden to dispatch per variant, converting /// the anchor's `Challenges` into the full variant's. It stays sound only while -/// those overrides stand and the batch keeps `no_output_shape` (the -/// committed output `Expr` references the staged `BytecodeValClaim` openings, -/// which the full mode never produces). +/// those overrides stand: the committed output `Expr` references the staged +/// `BytecodeValClaim` openings, which the full mode never produces, and the +/// claim shape comes from the per-variant derived points, never the `Expr`. impl ConcreteSumcheck for BytecodeReadRafCycle { type Symbolic = CycleSymbolicCommitted; diff --git a/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs b/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs index f9a7a7e2ce..5da43117c5 100644 --- a/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs +++ b/crates/jolt-verifier/src/stages/stage6b/committed_reduction_cycle_phase.rs @@ -16,8 +16,6 @@ //! produced opening id is dynamic in `has_address_phase`; the override computes //! exactly the formula value, so the clear path and BlindFold stay in sync. -use std::collections::BTreeSet; - use jolt_claims::protocols::jolt::geometry::claim_reductions::advice::ram_val_check_advice_opening; use jolt_claims::protocols::jolt::geometry::claim_reductions::bytecode::{ lane_weights, BytecodeLaneWeightInputs, @@ -35,10 +33,10 @@ pub use jolt_claims::protocols::jolt::relations::claim_reductions::program_image ProgramImageReductionCyclePhaseInputClaims, ProgramImageReductionCyclePhaseOutputClaims, }; use jolt_claims::protocols::jolt::{ - AdviceClaimReductionLayout, BytecodeClaimReductionLayout, JoltAdviceKind, JoltOpeningId, - JoltRelationId, PrecommittedReductionLayout, ProgramImageClaimReductionLayout, + AdviceClaimReductionLayout, BytecodeClaimReductionLayout, JoltAdviceKind, JoltRelationId, + PrecommittedReductionLayout, ProgramImageClaimReductionLayout, }; -use jolt_claims::{NoChallenges, OutputClaims, SymbolicSumcheck}; +use jolt_claims::{NoChallenges, SymbolicSumcheck}; use jolt_field::JoltField; use super::outputs::BytecodeReductionWeights; @@ -136,18 +134,6 @@ impl ConcreteSumcheck for TrustedAdviceCyclePhase { &self.symbolic } - /// The claims struct carries the completing opening under the cycle-phase - /// relation whether or not an address phase follows (`expected_output` - /// applies the final scale), but the symbolic output expression names the - /// final-reduction opening when none follows, so the default would omit the - /// cell the prover sends. - fn wire_output_openings(&self) -> BTreeSet { - TrustedAdviceCyclePhaseOutputClaims { trusted: F::zero() } - .canonical_order() - .into_iter() - .collect() - } - /// Precommitted cycle-phase reductions are bound on the offset-0 prefix of /// the batch challenge vector, not the front-loaded suffix. fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { @@ -237,20 +223,6 @@ impl ConcreteSumcheck for UntrustedAdviceCyclePhase { &self.symbolic } - /// The claims struct carries the completing opening under the cycle-phase - /// relation whether or not an address phase follows (`expected_output` - /// applies the final scale), but the symbolic output expression names the - /// final-reduction opening when none follows, so the default would omit the - /// cell the prover sends. - fn wire_output_openings(&self) -> BTreeSet { - UntrustedAdviceCyclePhaseOutputClaims { - untrusted: F::zero(), - } - .canonical_order() - .into_iter() - .collect() - } - fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { Ok(0) } @@ -358,20 +330,6 @@ impl ConcreteSumcheck for ProgramImageReductionCyclePhase { &self.symbolic } - /// The claims struct carries the completing opening under the cycle-phase - /// relation whether or not an address phase follows (`expected_output` - /// applies the final scale), but the symbolic output expression names the - /// final-reduction opening when none follows, so the default would omit the - /// cell the prover sends. - fn wire_output_openings(&self) -> BTreeSet { - ProgramImageReductionCyclePhaseOutputClaims { - program_image: F::zero(), - } - .canonical_order() - .into_iter() - .collect() - } - /// Precommitted cycle-phase reductions are bound on the offset-0 prefix of /// the batch challenge vector, not the front-loaded suffix. fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { @@ -485,26 +443,6 @@ impl ConcreteSumcheck for BytecodeReductionCyclePhase { &self.symbolic } - /// The claims struct carries the completing opening under the cycle-phase - /// relation whether or not an address phase follows (`expected_output` - /// applies the final scale), but the symbolic output expression names the - /// final-reduction opening when none follows, so the default would omit the - /// cell the prover sends. - fn wire_output_openings(&self) -> BTreeSet { - let claims = if self.layout.dimensions().has_address_phase() { - BytecodeReductionCyclePhaseOutputClaims { - intermediate: Some(F::zero()), - chunks: Vec::new(), - } - } else { - BytecodeReductionCyclePhaseOutputClaims { - intermediate: None, - chunks: vec![F::zero(); self.chunk_count], - } - }; - claims.canonical_order().into_iter().collect() - } - /// Precommitted cycle-phase reductions are bound on the offset-0 prefix of /// the batch challenge vector, not the front-loaded suffix. fn instance_point_offset(&self, _batch_num_vars: usize) -> Result { diff --git a/crates/jolt-verifier/src/stages/stage6b/field_inline.rs b/crates/jolt-verifier/src/stages/stage6b/field_inline.rs index 26096e409b..5a191e8fce 100644 --- a/crates/jolt-verifier/src/stages/stage6b/field_inline.rs +++ b/crates/jolt-verifier/src/stages/stage6b/field_inline.rs @@ -3,10 +3,8 @@ use jolt_claims::protocols::field_inline::{FieldInlineRelationId, FIELD_REGISTERS_LOG_K}; use jolt_claims::protocols::jolt::JoltRelationId; -use jolt_claims::OutputClaims as _; use jolt_field::JoltField; -use super::outputs::Stage6bOutputClaims; use crate::stages::field_inline_bytecode::{ field_inline_checked_split, field_inline_stage_gamma_powers, FieldInlineBytecodeFold, }; @@ -77,14 +75,3 @@ pub(super) fn bytecode_fold_and_cycles( val_evaluation_cycle: val_evaluation_cycle.to_vec(), }) } - -/// Splice the reduced `FieldRdInc` opening into the stage-6b Fiat-Shamir value -/// order: at its member position, after the ordinary increment reduction and -/// before the optional advice cycle phases (the spec's committed output row -/// order). -pub(super) fn splice_inc_values( - values: &mut Vec, - claims: &Stage6bOutputClaims, -) { - values.extend(claims.field_registers_inc_claim_reduction.opening_values()); -} diff --git a/crates/jolt-verifier/src/stages/stage6b/mod.rs b/crates/jolt-verifier/src/stages/stage6b/mod.rs index 9afaa0be09..c3b69827b8 100644 --- a/crates/jolt-verifier/src/stages/stage6b/mod.rs +++ b/crates/jolt-verifier/src/stages/stage6b/mod.rs @@ -16,7 +16,4 @@ pub mod ram_ra_virtualization; pub mod verify; pub use outputs::{Stage6bClearOutput, Stage6bOutput, Stage6bZkOutput}; -pub use verify::{ - stage6b_input_points_from_upstream, stage6b_input_values_from_upstream, stage6b_opening_values, - verify, -}; +pub use verify::{stage6b_input_points_from_upstream, stage6b_input_values_from_upstream, verify}; diff --git a/crates/jolt-verifier/src/stages/stage6b/outputs.rs b/crates/jolt-verifier/src/stages/stage6b/outputs.rs index b0ac30f9d4..6b1c29212f 100644 --- a/crates/jolt-verifier/src/stages/stage6b/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage6b/outputs.rs @@ -5,8 +5,11 @@ use jolt_claims::protocols::jolt::geometry::claim_reductions::bytecode::Bytecode use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; -use crate::stages::relations::SumcheckBatch; +use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltOpeningId, JoltRelationId}; + +use crate::stages::relations::{ClaimRoute, ClaimRoutes, SumcheckBatch}; use crate::stages::zk::outputs::CommittedOutputClaimOutput; +use crate::VerifierError; // The per-relation produced-claim structs live in their relation modules // (cell-generic, `#[derive(OutputClaims)]`); re-export them so consumers and the @@ -68,21 +71,12 @@ use jolt_claims::protocols::jolt::lattice::relations::booleanity::LatticeBoolean /// per-member draw would squeeze at the wrong transcript position if it /// existed to be called. /// -/// The opt-out `#[sumcheck_batch(no_opening_values)]` suppresses the generated -/// absorb methods: booleanity's `bytecode_ra` openings -/// alias the bytecode-read-RAF points and must NOT be re-absorbed, so the canonical -/// order is curated by [`stage6b_opening_values`](super::verify::stage6b_opening_values) -/// which threads the dedup points (the verifier absorbs its output; the -/// prover's recorder absorbs the same sequence). `no_output_shape`: shape methods are inapplicable — the committed -/// bytecode output `Expr` consumes the 6a-produced `BytecodeValClaim` openings -/// (not 6b outputs), and the ZK commitment count dedups runtime point aliases. +/// A booleanity `bytecode_ra` opening whose point equals its bytecode +/// read-RAF source's point is that source's copy, routed +/// [`ClaimRoute::Alias`] by [`Stage6bOutputPoints::claim_routes`]: it is +/// neither sent nor committed. #[derive(SumcheckBatch)] -#[sumcheck_batch( - no_opening_values, - no_draw_challenges, - no_output_shape, - crate = "crate" -)] +#[sumcheck_batch(no_draw_challenges, routes, crate = "crate")] pub struct Stage6bSumchecks { pub bytecode_read_raf: BytecodeReadRafCycle, pub booleanity: Booleanity, @@ -116,6 +110,32 @@ pub struct Stage6bSumchecks { /// `reverse(opening_point)` (see `cycle_phase_opening_point` in `jolt-claims` /// `claim_reductions::precommitted`). impl Stage6bOutputPoints { + /// The stage's claim routes: each booleanity `bytecode_ra` opening at its + /// bytecode read-RAF source's point aliases that source. A runtime point + /// equality, so it is a route rather than a static alias pair. + pub fn claim_routes(&self) -> Result { + let booleanity_point = self.booleanity_opening_point().ok_or_else(|| { + VerifierError::StageClaimPublicInputFailed { + stage: JoltRelationId::Booleanity, + reason: "Stage 6 booleanity produced no opening point".to_string(), + } + })?; + let mut routes = ClaimRoutes::default(); + for (index, point) in self.bytecode_read_raf.bytecode_ra.iter().enumerate() { + if point.as_slice() == booleanity_point { + let polynomial = JoltCommittedPolynomial::BytecodeRa(index); + routes.set( + JoltOpeningId::committed(polynomial, JoltRelationId::Booleanity), + ClaimRoute::Alias( + JoltOpeningId::committed(polynomial, JoltRelationId::BytecodeReadRaf) + .into(), + ), + ); + } + } + Ok(routes) + } + /// The shared booleanity opening point (`r_address ++ r_cycle`); every /// produced booleanity RA opening uses it. `None` only if booleanity produced /// no openings (never in practice — at least one RA family is always present). @@ -211,38 +231,6 @@ impl Stage6bOutputPoints { pub fn bytecode_cycle_phase_variables(&self) -> Option> { Some(reversed(self.bytecode_reduction_opening_point()?)) } - - /// The total number of produced opening-point cells across every member. This - /// is the derived, layout-independent claim count; the ZK path subtracts its - /// runtime bytecode/booleanity point-alias dedup from it to size the committed - /// output claims. ZK-only, hence base-only (no zk protocol exists over the - /// packed axis). - #[cfg(not(feature = "akita"))] - #[expect( - clippy::arithmetic_side_effects, - reason = "a sum of in-memory vector lengths and small constants cannot overflow usize" - )] - pub fn point_count(&self) -> usize { - self.bytecode_read_raf.bytecode_ra.len() - + self.booleanity.instruction_ra.len() - + self.booleanity.bytecode_ra.len() - + self.booleanity.ram_ra.len() - + 1 - + self.ram_ra_virtualization.ram_ra.len() - + self - .instruction_ra_virtualization - .committed_instruction_ra - .len() - + 2 - // The field-register increment reduction's single reduced `FieldRdInc` cell. - + usize::from(cfg!(feature = "field-inline")) - + usize::from(self.trusted_advice.is_some()) - + usize::from(self.untrusted_advice.is_some()) - + self.bytecode_reduction.as_ref().map_or(0, |reduction| { - usize::from(reduction.intermediate.is_some()) + reduction.chunks.len() - }) - + usize::from(self.program_image_reduction.is_some()) - } } impl Stage6bOutputClaims { diff --git a/crates/jolt-verifier/src/stages/stage6b/verify.rs b/crates/jolt-verifier/src/stages/stage6b/verify.rs index 465c30af46..54f46a0ed5 100644 --- a/crates/jolt-verifier/src/stages/stage6b/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6b/verify.rs @@ -1,10 +1,4 @@ -use std::collections::BTreeMap; - -use jolt_claims::protocols::jolt::{ - geometry::dimensions::JoltFormulaDimensions, JoltCommittedPolynomial, JoltOpeningId, - JoltRelationId, -}; -use jolt_claims::OutputClaims; +use jolt_claims::protocols::jolt::geometry::dimensions::JoltFormulaDimensions; use jolt_crypto::VectorCommitment; use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; @@ -15,54 +9,43 @@ use crate::sites::STAGE6B; #[cfg(not(feature = "akita"))] use super::committed_reduction_cycle_phase::{ trusted_advice_cycle_phase_input_values_from_upstream, - untrusted_advice_cycle_phase_input_values_from_upstream, TrustedAdviceCyclePhase, - UntrustedAdviceCyclePhase, + untrusted_advice_cycle_phase_input_values_from_upstream, }; #[cfg(feature = "field-inline")] use super::field_registers_inc_claim_reduction::{ field_registers_inc_claim_reduction_input_points_from_upstream, field_registers_inc_claim_reduction_input_values_from_upstream, - FieldRegistersIncClaimReduction, }; #[cfg(not(feature = "akita"))] use super::inc_claim_reduction::{ inc_claim_reduction_input_points_from_upstream, inc_claim_reduction_input_values_from_upstream, - IncClaimReduction, }; #[cfg(not(feature = "akita"))] use super::outputs::{Stage6bCarriedChallenges, Stage6bZkOutput}; -use super::ram_hamming_booleanity::{RamHammingBooleanity, RamHammingBooleanityInputClaims}; +use super::ram_hamming_booleanity::RamHammingBooleanityInputClaims; use super::{ batch::Stage6bDraws, - booleanity::{Booleanity, BooleanityInputClaims}, - bytecode_read_raf::{BytecodeReadRafCycle, BytecodeReadRafInputClaims}, + booleanity::BooleanityInputClaims, + bytecode_read_raf::BytecodeReadRafInputClaims, committed_reduction_cycle_phase::{ program_image_reduction_cycle_phase_input_values_from_upstream, - BytecodeReductionCyclePhase, BytecodeReductionCyclePhaseInputClaims, - ProgramImageReductionCyclePhase, + BytecodeReductionCyclePhaseInputClaims, }, instruction_ra_virtualization::{ instruction_ra_virtualization_input_points_from_upstream, - instruction_ra_virtualization_input_values_from_upstream, InstructionRaVirtualization, + instruction_ra_virtualization_input_values_from_upstream, }, outputs::{ - Stage6bClearOutput, Stage6bInputClaims, Stage6bInputPoints, Stage6bOutput, - Stage6bOutputClaims, Stage6bOutputPoints, Stage6bSumchecks, + Stage6bClearOutput, Stage6bInputClaims, Stage6bInputPoints, Stage6bOutput, Stage6bSumchecks, }, ram_ra_virtualization::{ ram_ra_virtualization_input_points_from_upstream, - ram_ra_virtualization_input_values_from_upstream, RamRaVirtualization, + ram_ra_virtualization_input_values_from_upstream, }, }; -#[cfg(not(feature = "akita"))] -use crate::stages::zk::{committed, outputs::CommittedOutputClaimOutput}; use crate::{ preprocessing::JoltVerifierPreprocessing, stages::{ - relations::{ - assemble_member_claims, assemble_member_claims_with, receive_member_openings, - receive_member_openings_except, ComposedOpeningId, - }, stage1::Stage1Output, stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage3::Stage3Output, @@ -131,33 +114,7 @@ where // runtime point-alias dedup arithmetic) is base-only. #[cfg(not(feature = "akita"))] if checked.zk { - let mut committed_shape = None; - let mut cycle_points = None; - let (consistency, commitments) = sumchecks.verify_zk_with( - |consistency| { - let points = - sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; - // The committed-claim count is the output-point-cell total minus - // the booleanity bytecode-RA openings aliased to the bytecode - // read-RAF points, the same runtime dedup as the clear wire. - let aliases = BytecodeRaAliases::new(&points)?; - let shape = committed::output_claim_shape( - checked, - points.point_count().saturating_sub(aliases.len()), - )?; - let rows = shape.row_count(); - committed_shape = Some(shape); - cycle_points = Some(points); - Ok(rows) - }, - transcript, - )?; - let (Some(shape), Some(cycle_points)) = (committed_shape, cycle_points) else { - return Err(VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::BytecodeReadRaf, - reason: "Stage 6b committed shape was not derived".to_string(), - }); - }; + let batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; return Ok(Stage6bOutput::Zk(Stage6bZkOutput { challenges: Stage6bCarriedChallenges { @@ -167,9 +124,9 @@ where field_registers_inc_gamma: draws.field_registers_inc_gamma, bytecode_reduction_eta: draws.eta, }, - batch_consistency: consistency, - batch_output_claims: CommittedOutputClaimOutput { shape, commitments }, - output_points: cycle_points, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, + output_points: batch.output_points, })); } @@ -185,13 +142,12 @@ where stage4, &stage5.output_values, )?; - let (cycle_points, claims) = sumchecks.verify_clear_with( + let (cycle_points, claims) = sumchecks.verify_clear( &input_values, &input_points, &cycle_challenges, transcript, 6, - receive_output_claims, )?; Ok(Stage6bOutput::Clear(Stage6bClearOutput { @@ -204,144 +160,6 @@ where })) } -/// The booleanity bytecode-RA openings that share their bytecode read-RAF -/// source's opening point, keyed to that source. Such an opening is a copy of -/// its source: it is neither sent nor committed, and the verifier fills it from -/// the source. This is a runtime point equality the output `Expr`s cannot -/// express, which is why stage 6b curates its own claim order. -struct BytecodeRaAliases(BTreeMap); - -impl BytecodeRaAliases { - fn new(points: &Stage6bOutputPoints) -> Result { - let booleanity_point = points.booleanity_opening_point().ok_or_else(|| { - VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::Booleanity, - reason: "Stage 6 booleanity produced no opening point".to_string(), - } - })?; - Ok(Self( - points - .bytecode_read_raf - .bytecode_ra - .iter() - .enumerate() - .filter(|(_, point)| point.as_slice() == booleanity_point) - .map(|(index, _)| { - let polynomial = JoltCommittedPolynomial::BytecodeRa(index); - ( - JoltOpeningId::committed(polynomial, JoltRelationId::Booleanity).into(), - JoltOpeningId::committed(polynomial, JoltRelationId::BytecodeReadRaf) - .into(), - ) - }) - .collect(), - )) - } - - #[cfg(not(feature = "akita"))] - fn len(&self) -> usize { - self.0.len() - } -} - -/// Receive the stage-6b output claims in the [`stage6b_opening_values`] order: -/// member declaration order, each member in its canonical order, with the -/// aliased booleanity bytecode-RA openings filled from their sources. -fn receive_output_claims( - sumchecks: &Stage6bSumchecks, - points: &Stage6bOutputPoints, - transcript: &mut VerifierTranscript<'_, H>, -) -> Result, VerifierError> { - let aliases = BytecodeRaAliases::new(points)?; - let mut received = BTreeMap::new(); - received.extend(receive_member_openings( - &sumchecks.bytecode_read_raf, - transcript, - )?); - received.extend(receive_member_openings_except( - &sumchecks.booleanity, - |id| aliases.0.contains_key(id), - transcript, - )?); - received.extend(receive_member_openings( - &sumchecks.ram_hamming_booleanity, - transcript, - )?); - received.extend(receive_member_openings( - &sumchecks.ram_ra_virtualization, - transcript, - )?); - received.extend(receive_member_openings( - &sumchecks.instruction_ra_virtualization, - transcript, - )?); - #[cfg(not(feature = "akita"))] - received.extend(receive_member_openings( - &sumchecks.inc_claim_reduction, - transcript, - )?); - #[cfg(feature = "field-inline")] - received.extend(receive_member_openings( - &sumchecks.field_registers_inc_claim_reduction, - transcript, - )?); - #[cfg(not(feature = "akita"))] - if let Some(member) = &sumchecks.trusted_advice { - received.extend(receive_member_openings(member, transcript)?); - } - #[cfg(not(feature = "akita"))] - if let Some(member) = &sumchecks.untrusted_advice { - received.extend(receive_member_openings(member, transcript)?); - } - if let Some(member) = &sumchecks.bytecode_reduction { - received.extend(receive_member_openings(member, transcript)?); - } - if let Some(member) = &sumchecks.program_image_reduction { - received.extend(receive_member_openings(member, transcript)?); - } - - Ok(Stage6bOutputClaims { - bytecode_read_raf: assemble_member_claims::>(&received)?, - booleanity: assemble_member_claims_with::>(&received, |id| { - aliases.0.get(id).copied() - })?, - ram_hamming_booleanity: assemble_member_claims::>(&received)?, - ram_ra_virtualization: assemble_member_claims::>(&received)?, - instruction_ra_virtualization: assemble_member_claims::>( - &received, - )?, - #[cfg(not(feature = "akita"))] - inc_claim_reduction: assemble_member_claims::>(&received)?, - #[cfg(feature = "field-inline")] - field_registers_inc_claim_reduction: assemble_member_claims::< - F, - FieldRegistersIncClaimReduction, - >(&received)?, - #[cfg(not(feature = "akita"))] - trusted_advice: sumchecks - .trusted_advice - .as_ref() - .map(|_| assemble_member_claims::>(&received)) - .transpose()?, - #[cfg(not(feature = "akita"))] - untrusted_advice: sumchecks - .untrusted_advice - .as_ref() - .map(|_| assemble_member_claims::>(&received)) - .transpose()?, - bytecode_reduction: sumchecks - .bytecode_reduction - .as_ref() - .map(|_| assemble_member_claims::>(&received)) - .transpose()?, - program_image_reduction: sumchecks - .program_image_reduction - .as_ref() - .map(|_| assemble_member_claims::>(&received)) - .transpose()?, - }) -} - /// Assemble the stage-6b consumed opening *values* from the address-phase claims /// and the upstream clear outputs into the generated `Stage6bInputClaims` /// aggregate. The `Option` cells track member presence, so a present member always @@ -441,61 +259,6 @@ pub fn stage6b_input_points_from_upstream( } } -/// The stage-6b Fiat-Shamir opening-claim values in canonical absorb order. -/// Full relations and the optional members single-source their per-field order -/// from the `OutputClaims` derive's `opening_values`; `booleanity` stays -/// explicit because its `bytecode_ra` openings are conditionally deduped -/// against the bytecode-read-RAF points (a runtime point-equality the output -/// `Expr`s cannot express). Public because the prover's recorder absorbs the -/// same curated sequence. -pub fn stage6b_opening_values( - claims: &Stage6bOutputClaims, - bytecode_read_raf_points: &[Vec], - booleanity_point: &[F], -) -> Vec { - let mut values = claims.bytecode_read_raf.opening_values(); - values.extend(&claims.booleanity.instruction_ra); - for (index, opening_claim) in claims.booleanity.bytecode_ra.iter().enumerate() { - if bytecode_read_raf_points - .get(index) - .is_some_and(|point| point.as_slice() == booleanity_point) - { - continue; - } - values.push(*opening_claim); - } - values.extend(&claims.booleanity.ram_ra); - #[cfg(feature = "akita")] - { - values.extend(&claims.booleanity.balanced_inc_digits); - values.push(claims.booleanity.balanced_inc_carry); - } - values.extend(claims.ram_hamming_booleanity.opening_values()); - values.extend(claims.ram_ra_virtualization.opening_values()); - values.extend(claims.instruction_ra_virtualization.opening_values()); - #[cfg(not(feature = "akita"))] - values.extend(claims.inc_claim_reduction.opening_values()); - #[cfg(feature = "field-inline")] - super::field_inline::splice_inc_values(&mut values, claims); - // Each advice member is a single-slot per-kind claims struct, so it - // contributes exactly its own kind's opening. - #[cfg(not(feature = "akita"))] - if let Some(advice) = &claims.trusted_advice { - values.extend(advice.opening_values()); - } - #[cfg(not(feature = "akita"))] - if let Some(advice) = &claims.untrusted_advice { - values.extend(advice.opening_values()); - } - if let Some(reduction) = &claims.bytecode_reduction { - values.extend(reduction.opening_values()); - } - if let Some(reduction) = &claims.program_image_reduction { - values.extend(reduction.opening_values()); - } - values -} - #[cfg(test)] mod tests { #[cfg(not(feature = "akita"))] @@ -509,9 +272,12 @@ mod tests { #[cfg(not(feature = "akita"))] use super::super::inc_claim_reduction::IncClaimReductionOutputClaims; use super::super::instruction_ra_virtualization::InstructionRaVirtualizationOutputClaims; + use super::super::outputs::Stage6bOutputClaims; use super::super::ram_hamming_booleanity::RamHammingBooleanityOutputClaims; use super::super::ram_ra_virtualization::RamRaVirtualizationOutputClaims; use super::*; + use crate::stages::relations::{ClaimRoute, ClaimRoutes}; + use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltOpeningId, JoltRelationId}; use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { @@ -595,18 +361,43 @@ mod tests { ) } - /// Locks the stage-6b opening order (the prover's send order and the - /// verifier's receive order) against silent drift. The full relations are - /// single-sourced via their `OutputClaims` derive; `booleanity` - /// (conditional `bytecode_ra` dedup) and the optional reductions stay - /// explicit. Points are empty so no `bytecode_ra` element is deduped; the - /// `None` reductions contribute nothing. + /// Locks the stage-6b clear claim order (member declaration order, each + /// member in its canonical order) against silent drift with distinct + /// sentinels; the `None` reductions contribute nothing. #[test] - fn opening_values_follow_canonical_order() { + fn wire_claims_follow_declaration_order() { let (claims, last) = sample_claims(); assert_eq!( - stage6b_opening_values(&claims, &[], &[]), + Stage6bSumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), (1..=last).map(fr).collect::>() ); } + + /// A booleanity `bytecode_ra` cell routed as an alias of its bytecode + /// read-RAF source is neither sent nor committed. + #[test] + fn aliased_bytecode_ra_is_not_sent() { + let (claims, last) = sample_claims(); + let polynomial = JoltCommittedPolynomial::BytecodeRa(0); + let mut routes = ClaimRoutes::default(); + routes.set( + JoltOpeningId::committed(polynomial, JoltRelationId::Booleanity), + ClaimRoute::Alias( + JoltOpeningId::committed(polynomial, JoltRelationId::BytecodeReadRaf).into(), + ), + ); + let aliased = claims.booleanity.bytecode_ra.clone(); + let expected: Vec = (1..=last) + .map(fr) + .filter(|value| !aliased.contains(value)) + .collect(); + assert_eq!( + Stage6bSumchecks::wire_claim_values(&claims, &routes), + expected + ); + assert_eq!( + Stage6bSumchecks::committed_claim_values(&claims, &routes), + expected + ); + } } diff --git a/crates/jolt-verifier/src/stages/stage7/outputs.rs b/crates/jolt-verifier/src/stages/stage7/outputs.rs index d4c6b49030..39bc6a5d50 100644 --- a/crates/jolt-verifier/src/stages/stage7/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage7/outputs.rs @@ -161,9 +161,9 @@ mod tests { #[cfg(not(feature = "akita"))] use super::*; #[cfg(not(feature = "akita"))] - use crate::stages::stage7::hamming_weight_claim_reduction::{ - hamming_weight_claim_reduction_dimensions, HammingWeightClaimReductionOutputClaims, - }; + use crate::stages::relations::ClaimRoutes; + #[cfg(not(feature = "akita"))] + use crate::stages::stage7::hamming_weight_claim_reduction::HammingWeightClaimReductionOutputClaims; #[cfg(not(feature = "akita"))] use jolt_claims::protocols::jolt::relations::claim_reductions::advice::{ TrustedAdviceAddressPhaseOutputClaims, UntrustedAdviceAddressPhaseOutputClaims, @@ -191,49 +191,7 @@ mod tests { /// member carries only its own kind's slot. #[cfg(not(feature = "akita"))] #[test] - #[expect(clippy::unwrap_used)] - fn opening_values_follow_canonical_order() { - use crate::stages::{CommittedProgramSchedule, PrecommittedSchedule}; - use jolt_claims::protocols::jolt::geometry::ra::JoltRaPolynomialLayout; - use jolt_claims::protocols::jolt::TracePolynomialOrder; - - let schedule = PrecommittedSchedule::new( - TracePolynomialOrder::CycleMajor, - 4, - 2, - Some(64), - Some(64), - Some(CommittedProgramSchedule { - bytecode_len: 8, - bytecode_chunk_count: 2, - program_image_len_words: 8, - program_image_start_index: 0, - }), - ) - .unwrap(); - let hamming_instance = || { - let dimensions = hamming_weight_claim_reduction_dimensions( - JoltRaPolynomialLayout::new(2, 1, 1).unwrap(), - 4, - ) - .unwrap(); - HammingWeightClaimReduction::new(dimensions, Vec::new(), Vec::new(), Vec::new()) - }; - let trusted_instance = || { - TrustedAdviceAddressPhase::new( - schedule.trusted_advice.as_ref().unwrap(), - None, - Vec::new(), - ) - }; - let untrusted_instance = || { - UntrustedAdviceAddressPhase::new( - schedule.untrusted_advice.as_ref().unwrap(), - None, - Vec::new(), - ) - }; - + fn wire_claims_follow_canonical_order() { // Sentinels are sequential in canonical append order. let (trusted, untrusted, chunk1, chunk2, image, plain_last, committed_last) = (5, 6, 7, 8, 9, 6, 9); @@ -249,13 +207,6 @@ mod tests { untrusted: fr(untrusted), }; - let without_committed_sumchecks = Stage7Sumchecks:: { - hamming_weight_claim_reduction: hamming_instance(), - trusted_advice: Some(trusted_instance()), - untrusted_advice: Some(untrusted_instance()), - bytecode_address_phase: None, - program_image_address_phase: None, - }; let without_committed = Stage7OutputClaims:: { hamming_weight_claim_reduction: hamming.clone(), trusted_advice: Some(trusted_advice.clone()), @@ -264,25 +215,10 @@ mod tests { program_image_address_phase: None, }; assert_eq!( - without_committed_sumchecks.opening_values(&without_committed), + Stage7Sumchecks::wire_claim_values(&without_committed, &ClaimRoutes::default()), (1..=plain_last).map(fr).collect::>() ); - let with_committed_sumchecks = Stage7Sumchecks:: { - hamming_weight_claim_reduction: hamming_instance(), - trusted_advice: Some(trusted_instance()), - untrusted_advice: Some(untrusted_instance()), - bytecode_address_phase: Some(BytecodeReductionAddressPhase::new( - schedule.bytecode.as_ref().unwrap(), - None, - Vec::new(), - )), - program_image_address_phase: Some(ProgramImageReductionAddressPhase::new( - schedule.program_image.as_ref().unwrap(), - None, - Vec::new(), - )), - }; let with_committed = Stage7OutputClaims:: { hamming_weight_claim_reduction: hamming, trusted_advice: Some(trusted_advice), @@ -295,16 +231,14 @@ mod tests { }), }; assert_eq!( - with_committed_sumchecks.opening_values(&with_committed), + Stage7Sumchecks::wire_claim_values(&with_committed, &ClaimRoutes::default()), (1..=committed_last).map(fr).collect::>() ); } - /// Locks the `output_shape` commitment count against Expr/geometry drift: the - /// per-member `expected_output_openings` counts (which the generated - /// `output_claim_count` sums) must match the hand-derived opening counts each - /// configuration commits. Guards the ZK commitment count switching from the - /// old hand count to the Expr-derived sums. + /// Locks each member's output `Expr` against geometry drift: the per-member + /// `expected_output_openings` counts must match the hand-derived opening + /// counts each configuration commits. #[test] #[expect(clippy::unwrap_used)] fn output_shape_column_counts_match_hand_derived_openings() { diff --git a/crates/jolt-verifier/src/stages/stage7/verify.rs b/crates/jolt-verifier/src/stages/stage7/verify.rs index 171465115e..2a1abcca63 100644 --- a/crates/jolt-verifier/src/stages/stage7/verify.rs +++ b/crates/jolt-verifier/src/stages/stage7/verify.rs @@ -36,7 +36,6 @@ use crate::{ stages::{ stage4::{Stage4ClearOutput, Stage4Output}, stage6b::{outputs::Stage6bOutputPoints, Stage6bClearOutput, Stage6bOutput}, - zk::{committed, outputs::CommittedOutputClaimOutput}, PrecommittedSchedule, }, verifier::CheckedInputs, @@ -96,23 +95,18 @@ where let challenges = sumchecks.draw_challenges(transcript)?; if checked.zk { - let shape = committed::output_claim_shape(checked, sumchecks.output_claim_count())?; - let (consistency, commitments) = sumchecks.verify_zk(shape.row_count(), transcript)?; - let batch_output_claims = CommittedOutputClaimOutput { shape, commitments }; - // The produced opening points, derived off the committed batch consistency; // stage 8 reads the hamming point and resolves the precommitted finals off // them. BlindFold recomputes each relation's sumcheck point and publics // independently from `batch_consistency`. let input_points = sumchecks.empty_input_points(); - let output_points = - sumchecks.derive_opening_points(&consistency.challenges(), &input_points)?; + let batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; return Ok(Stage7Output::Zk(Stage7ZkOutput { challenges, - batch_consistency: consistency, - batch_output_claims, - output_points, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, + output_points: batch.output_points, })); } diff --git a/crates/jolt-verifier/src/stages/uniskip.rs b/crates/jolt-verifier/src/stages/uniskip.rs index 98eb94ab0f..fad5b2de00 100644 --- a/crates/jolt-verifier/src/stages/uniskip.rs +++ b/crates/jolt-verifier/src/stages/uniskip.rs @@ -12,7 +12,10 @@ use jolt_claims::protocols::composed::geometry::{ SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE, SPARTAN_PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, }; -use jolt_claims::protocols::jolt::JoltRelationId; +use jolt_claims::protocols::jolt::geometry::spartan::{ + outer_uniskip_opening, product_uniskip_opening, +}; +use jolt_claims::protocols::jolt::{JoltOpeningId, JoltRelationId}; use jolt_field::CanonicalDecode; use jolt_field::JoltField; use jolt_r1cs::constraints::jolt::{ @@ -24,7 +27,8 @@ use jolt_sumcheck::{ }; use jolt_transcript::{Channel, Sponge, VerifierTranscript}; -use crate::stages::zk::committed::{self, CommittedOutputClaimOutput}; +use crate::stages::relations::CommittedClaimLayout; +use crate::stages::zk::outputs::{CommittedOutputClaimOutput, CommittedOutputClaimShape}; use crate::verifier::CheckedInputs; use crate::VerifierError; @@ -38,6 +42,8 @@ const UNISKIP_ROUNDS: usize = 1; /// two constructors are the only instances. pub struct UniskipParams { stage: JoltRelationId, + /// The round's single output opening. + output_opening: JoltOpeningId, /// The stage number reported by `StageClaimOutputMismatch`. stage_number: usize, degree: usize, @@ -49,6 +55,7 @@ impl UniskipParams { pub fn spartan_outer() -> Self { Self { stage: JoltRelationId::SpartanOuter, + output_opening: outer_uniskip_opening(), stage_number: 1, degree: SPARTAN_OUTER_UNISKIP_FIRST_ROUND_DEGREE, domain_size: SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE, @@ -59,6 +66,7 @@ impl UniskipParams { pub fn spartan_product() -> Self { Self { stage: JoltRelationId::SpartanProductVirtualization, + output_opening: product_uniskip_opening(), stage_number: 2, degree: SPARTAN_PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, domain_size: SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE, @@ -180,7 +188,13 @@ where C: CanonicalDecode, H: Sponge, { - let shape = committed::output_claim_shape(checked, 1)?; + let shape = CommittedOutputClaimShape::new( + checked.committed_row_len()?, + CommittedClaimLayout { + ids: vec![params.output_opening.into()], + aliases: Vec::new(), + }, + ); let (consistency, commitments) = SumcheckVerifier::verify_committed( SumcheckStatement::new(UNISKIP_ROUNDS, params.degree), shape.row_count(), diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs b/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs index aaf8907828..bf5db21014 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs @@ -5,9 +5,6 @@ //! exactly one contiguous, flagged region per interaction point, calling into here. use crate::stages::derivations; -use jolt_claims::protocols::field_inline::geometry::claim_reductions as field_claim_reductions; -use jolt_claims::protocols::field_inline::geometry::registers as field_registers_geometry; -use jolt_claims::protocols::field_inline::geometry::spartan as field_spartan_geometry; use jolt_claims::protocols::field_inline::relations::claim_reductions::increments as field_increments; use jolt_claims::protocols::field_inline::relations::claim_reductions::registers as field_registers_reduction; use jolt_claims::protocols::field_inline::relations::registers as field_registers; @@ -25,7 +22,7 @@ use jolt_openings::CommitmentScheme; use jolt_riscv::JoltInstructionRow; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; -use super::{ComposedOpeningId, SourceValues}; +use super::SourceValues; use crate::config::JOLT_VERIFIER_CONFIG; use crate::preprocessing::ProgramPreprocessing; use crate::stages::field_inline_bytecode::field_inline_stage_gamma_powers; @@ -56,13 +53,6 @@ pub(super) fn point_suffix( }) } -/// The five field value/product openings following the common stage-1 columns. -pub(super) fn stage1_appended_opening_ids() -> impl Iterator { - field_spartan_geometry::outer_output_openings() - .into_iter() - .map(ComposedOpeningId::from) -} - /// The stage-2 field-inline claim-reduction member and its baked publics: its `EqSpartan` is /// the same `Eq(reduced point, tau_low)` derivation as the instruction reduction (same rounds, /// same batch suffix, same reversed opening point — pinned in stage2's clear tests); its gamma @@ -95,13 +85,6 @@ pub(super) fn stage2_claim_reduction( Ok(reduction) } -/// The field-inline portion of the product member's canonical output rows. -pub(super) fn stage2_product_opening_ids() -> impl Iterator { - jolt_claims::protocols::field_inline::geometry::product::selected_product_remainder_output_openings() - .into_iter() - .map(ComposedOpeningId::from) -} - /// The stage-4 field-register read/write member and its baked publics: shape from the /// compile-time protocol config, gamma from the drawn batch, `EqCycle` mirroring the ordinary /// registers derivation — `Eq(upstream field-inline reduced cycle point, own cycle sub-point @@ -139,14 +122,6 @@ pub(super) fn stage4_read_write( Ok(claims) } -/// The five field-register read/write rows, spliced after the register openings and before -/// `ram_ra`/`ram_inc` — the clear absorb order. -pub(super) fn stage4_output_ids() -> impl Iterator { - field_registers_geometry::read_write_checking_output_openings() - .into_iter() - .map(ComposedOpeningId::from) -} - /// The stage-5 field-register value-evaluation member (declared last, no instance challenge) /// and its baked `LtCycle` public: `Lt(own cycle sub-point, upstream field-register read/write /// cycle sub-point)` over the field-register address prefix. @@ -170,15 +145,6 @@ pub(super) fn stage5_val_evaluation( Ok(claims) } -/// The two field-register value-evaluation rows, after the ordinary register value-evaluation -/// outputs — the clear absorb order (the field-inline member is declared last, so the -/// generated absorb appends them at the tail). -pub(super) fn stage5_output_ids() -> impl Iterator { - field_registers_geometry::val_evaluation_output_openings() - .into_iter() - .map(ComposedOpeningId::from) -} - /// Derive field-register accesses from the bytecode and add their stage-value /// contributions onto the ordinary staged bytecode publics BEFORE they bake, so the same /// `StageValue(i)` publics the symbolic output expression references carry both families — @@ -305,12 +271,3 @@ pub(super) fn stage6b_inc_publics( )?; Ok(()) } - -/// The reduced field-inline `FieldRdInc` row, after the ordinary increment-reduction outputs -/// and before the optional advice cycle phases — the clear absorb order -/// (`stage6b_opening_values`). -pub(super) fn stage6b_inc_output_ids() -> impl Iterator { - field_claim_reductions::increments::claim_reduction_output_openings() - .into_iter() - .map(ComposedOpeningId::from) -} diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs index f071079c0f..98404d98ac 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs @@ -54,7 +54,7 @@ //! final PCS opening proof: no clear output claim scalars are accepted by the //! verifier, and every hidden scalar that crosses a stage boundary is either in //! a committed output-claim row or in the final hiding evaluation commitment. -use jolt_blindfold::{BlindFoldProtocol, BlindFoldProtocolBuilder, OpeningAlias}; +use jolt_blindfold::{BlindFoldProtocol, BlindFoldProtocolBuilder}; use jolt_claims::protocols::composed::geometry::{ SPARTAN_PRODUCT_UNISKIP_DOMAIN_SIZE, SPARTAN_PRODUCT_UNISKIP_FIRST_ROUND_DEGREE, }; @@ -70,7 +70,7 @@ use jolt_claims::{ opening, protocols::jolt::{ geometry::{ - booleanity::{self, BooleanityDimensions}, + booleanity::BooleanityDimensions, bytecode::{self, BytecodeReadRafEvaluationInputs}, claim_reductions::{ advice, @@ -78,10 +78,10 @@ use jolt_claims::{ hamming_weight, program_image, }, dimensions::{JoltFormulaDimensions, REGISTER_ADDRESS_BITS}, - instruction, ram, + ram, spartan::{ - outer_opening, outer_uniskip_opening, product_uniskip_opening, - SpartanOuterDimensions, SpartanProductDimensions, + outer_uniskip_opening, product_uniskip_opening, SpartanOuterDimensions, + SpartanProductDimensions, }, }, AdviceClaimReductionLayout, AdviceClaimReductionPublic, BooleanityChallenge, @@ -92,16 +92,16 @@ use jolt_claims::{ InstructionClaimReductionPublic, InstructionInputChallenge, InstructionInputPublic, InstructionRaVirtualizationChallenge, InstructionRaVirtualizationPublic, InstructionReadRafChallenge, InstructionReadRafPublic, JoltAdviceKind, JoltChallengeId, - JoltCommittedPolynomial, JoltDerivedId, JoltOpeningId, JoltPolynomialId, JoltRelationId, - PrecommittedReductionLayout, ProgramImageClaimReductionLayout, - ProgramImageClaimReductionPublic, RamHammingBooleanityPublic, RamOutputCheckPublic, - RamRaClaimReductionChallenge, RamRaClaimReductionPublic, RamRaVirtualizationPublic, - RamRafEvaluationPublic, RamReadWriteChallenge, RamReadWritePublic, RamValCheckChallenge, - RamValCheckPublic, RegistersClaimReductionChallenge, RegistersClaimReductionPublic, + JoltDerivedId, JoltRelationId, PrecommittedReductionLayout, + ProgramImageClaimReductionLayout, ProgramImageClaimReductionPublic, + RamHammingBooleanityPublic, RamOutputCheckPublic, RamRaClaimReductionChallenge, + RamRaClaimReductionPublic, RamRaVirtualizationPublic, RamRafEvaluationPublic, + RamReadWriteChallenge, RamReadWritePublic, RamValCheckChallenge, RamValCheckPublic, + RegistersClaimReductionChallenge, RegistersClaimReductionPublic, RegistersReadWriteChallenge, RegistersReadWritePublic, RegistersValEvaluationPublic, SpartanOuterPublic, SpartanShiftChallenge, SpartanShiftPublic, }, - Expr, OutputClaims, Source, SymbolicSumcheck, Term, + Expr, Source, SymbolicSumcheck, Term, }; use jolt_crypto::VectorCommitment; use jolt_field::JoltField; @@ -122,7 +122,6 @@ use jolt_sumcheck::{ BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency, SumcheckDomainSpec, SumcheckStatement, }; -use num_traits::Zero; use super::{inputs::BlindFoldInputs, outputs::CommittedOutputClaimOutput}; use crate::stages::{ @@ -250,10 +249,6 @@ where Ok(protocol) } -#[expect( - clippy::too_many_arguments, - reason = "BlindFold stages are deliberately explicit." -)] fn add_batched_stage( builder: Builder, name: &'static str, @@ -262,8 +257,6 @@ fn add_batched_stage( consistency: &BatchedCommittedSumcheckConsistency, output_claims: &CommittedOutputClaimOutput, values: &SourceValues, - opening_ids: Vec, - aliases: Vec>, ) -> Result, VerifierError> where F: JoltField, @@ -317,8 +310,6 @@ where consistency.consistency.clone(), output_claims, values, - opening_ids, - aliases, input_claim, output_claim, ) @@ -336,8 +327,6 @@ fn add_stage( consistency: CommittedSumcheckConsistency, output_claims: &CommittedOutputClaimOutput, values: &SourceValues, - opening_ids: Vec, - aliases: Vec>, input_claim: VerifierExpr, output_claim: VerifierExpr, ) -> Result, VerifierError> @@ -347,26 +336,17 @@ where { require_expr_sources(name, "input claim", &input_claim, values)?; require_expr_sources(name, "output claim", &output_claim, values)?; - if opening_ids.len() != output_claims.shape.output_claim_count { - return Err(VerifierError::BlindFoldConstructionFailed { - reason: format!( - "{name}: output opening id count mismatch: expected {}, got {}", - output_claims.shape.output_claim_count, - opening_ids.len() - ), - }); - } builder .stage(name) .sumcheck(statement) .domain(domain) .consistency(consistency) .output_claim_rows( - opening_ids, + output_claims.shape.layout.ids.clone(), output_claims.shape.row_len, output_claims.commitments.clone(), ) - .output_claim_aliases(aliases) + .output_claim_aliases(output_claims.shape.layout.aliases.clone()) .input_claim(input_claim) .output_claim(output_claim) .finish_stage() @@ -433,21 +413,6 @@ where } } -/// Lift a jolt-typed opening-id list into the composite id space. -fn composite_ids(ids: impl IntoIterator) -> Vec { - ids.into_iter().map(Into::into).collect() -} - -/// Lift jolt-typed `(aliased, source)` pairs into composite [`OpeningAlias`] rows. -fn composite_aliases>( - pairs: impl IntoIterator, -) -> Vec> { - pairs - .into_iter() - .map(|(aliased, source)| OpeningAlias::new(aliased.into(), source.into())) - .collect() -} - /// Evaluates the BlindFold form of a Jolt claim expression with the same /// source values used by the clear verifier. /// @@ -1457,7 +1422,7 @@ mod field_inline_relation_parity { FieldInlineChallengeId, FieldInlineDerivedId, FieldInlineOpeningId, FieldRegistersTraceDimensions, }; - use jolt_claims::{InputClaims, SumcheckChallenges}; + use jolt_claims::{InputClaims, OutputClaims, SumcheckChallenges}; use jolt_field::{Fr, Ring}; use jolt_sumcheck::VerifiedCommittedRound; diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs index d74a98d0b5..8bcd720208 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage1.rs @@ -27,8 +27,6 @@ where input.stage1.uniskip_consistency.clone(), &input.stage1.uniskip_output_claims, values, - vec![outer_uniskip_opening().into()], - Vec::new(), VerifierExpr::zero(), opening(outer_uniskip_opening()), )?; @@ -53,11 +51,6 @@ where values.public(VerifierPublicId::SpartanOuter(id), value)?; } - // The composed opening row order: the common value/flag openings in canonical order, then (under - // `field-inline`) the five field value/product columns in appended-column order — the clear path's - // absorb order exactly. - let opening_ids = stage1_spartan_outer_opening_ids(&dimensions); - #[expect( clippy::arithmetic_side_effects, reason = "log_t is an ilog2 result (< 64); 1 + log_t cannot overflow usize" @@ -104,32 +97,11 @@ where input.stage1.remainder_consistency.consistency.clone(), &input.stage1.remainder_output_claims, values, - opening_ids, - Vec::new(), input_claim, output_claim, ) } -/// The composed stage-1 committed opening row order: the common Spartan-outer openings in -/// canonical (`dimensions.variables()`) order, then — under `field-inline` — the five -/// field value/product openings in appended-column order. This is exactly `stage1::verify`'s -/// absorb/commit order. -pub(super) fn stage1_spartan_outer_opening_ids( - dimensions: &SpartanOuterDimensions, -) -> Vec { - #[cfg_attr(not(feature = "field-inline"), expect(unused_mut))] - let mut opening_ids: Vec = dimensions - .variables() - .iter() - .copied() - .map(|variable| outer_opening(variable).into()) - .collect(); - #[cfg(feature = "field-inline")] - opening_ids.extend(super::field_inline::stage1_appended_opening_ids()); - opening_ids -} - fn stage1_spartan_outer_output_expr( dimensions: &SpartanOuterDimensions, ) -> VerifierExpr { @@ -146,9 +118,28 @@ fn stage1_spartan_outer_output_expr( )] mod tests { use super::*; + use crate::stages::relations::ClaimRoutes; + use crate::stages::stage1::outer_remainder::OuterRemainder; + use crate::stages::stage1::outputs::Stage1BatchSumchecks; + use jolt_claims::protocols::jolt::geometry::spartan::outer_opening; use jolt_field::{Fr, Ring}; + use num_traits::Zero; use std::collections::BTreeMap; + /// The stage-1 committed row ids, from the stage's generated layout. + fn stage1_committed_ids(log_t: usize) -> Vec { + let dimensions = SpartanOuterDimensions::rv64(log_t); + let tau = vec![Fr::from_u64(2); log_t.saturating_add(2)]; + let sumchecks = Stage1BatchSumchecks { + outer_remainder: OuterRemainder::new(dimensions, tau, Fr::from_u64(17)), + }; + let challenges: Vec = (0..=(log_t as u64)).map(Fr::from_u64).collect(); + let points = sumchecks + .derive_opening_points(&challenges, &sumchecks.empty_input_points()) + .unwrap(); + Stage1BatchSumchecks::committed_claim_layout(&points, &ClaimRoutes::default()).ids + } + /// Resolve one of the coefficient-table publics from its `(id, value)` /// list; ids outside the SpartanOuter family resolve to zero (the stage-1 /// output expression never names them). @@ -166,13 +157,14 @@ mod tests { } } - /// The composed opening row order is the clear absorb order: the common value/flag openings in - /// canonical order, then (with field-inline enabled) the five field value/product openings in - /// appended-column order, matching the composed jolt-r1cs column count. + /// The committed row order is the composed R1CS column order the factored + /// output formula consumes: the common value/flag openings in canonical + /// order, then (with field-inline enabled) the five field value/product + /// openings in appended-column order. #[test] fn stage1_opening_ids_follow_the_composed_column_order() { let dimensions = SpartanOuterDimensions::rv64(3); - let ids = stage1_spartan_outer_opening_ids(&dimensions); + let ids = stage1_committed_ids(3); let expected_len = jolt_r1cs::constraints::jolt::spartan_outer_opening_columns().len(); assert_eq!(ids.len(), expected_len); @@ -219,7 +211,7 @@ mod tests { remainder: &remainder, }) .unwrap(); - let opening_ids = stage1_spartan_outer_opening_ids(&dimensions); + let opening_ids = stage1_committed_ids(log_t); let openings: Vec = (0..opening_ids.len() as u64) .map(|i| Fr::from_u64(1_000 + i)) .collect(); diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs index dec67b9dc1..5b4484a4e0 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs @@ -1,24 +1,12 @@ -#[cfg(feature = "field-inline")] -use crate::stages::stage2::field_registers_claim_reduction::FieldRegistersClaimReduction; #[cfg(all(test, feature = "field-inline"))] use jolt_claims::protocols::composed::ComposedClaims; #[cfg(all(test, feature = "field-inline"))] use jolt_claims::protocols::composed::FieldProductUniskipInputs; #[cfg(all(test, feature = "field-inline"))] use jolt_claims::protocols::composed::ProductInputs; -use std::collections::BTreeSet; use super::*; -use jolt_claims::protocols::jolt::relations::claim_reductions::instruction::InstructionClaimReductionOutputClaims; -use jolt_claims::protocols::jolt::relations::ram::{ - RamOutputCheckOutputClaims, RamRafEvaluationOutputClaims, RamReadWriteOutputClaims, -}; -use jolt_claims::protocols::jolt::relations::spartan::ProductRemainderOutputClaims; - -use crate::stages::relations::ConcreteSumcheck; -use crate::stages::stage2::outputs::InstructionClaimReduction; - // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. @@ -72,8 +60,6 @@ where input.stage2.product_uniskip_consistency.clone(), &input.stage2.product_uniskip_output_claims, values, - vec![product_uniskip_opening().into()], - Vec::new(), product_uniskip_input, opening(product_uniskip_opening()), )?; @@ -218,8 +204,6 @@ where output_publics.2, )?; - let (output_ids, aliases) = stage2_output_ids_and_aliases::(); - // Member declaration order (= batching-coefficient draw order): the field-inline claim // reduction sits between the instruction reduction and RAM RAF evaluation, exactly as in // `Stage2BatchSumchecks`. @@ -245,85 +229,9 @@ where &input.stage2.batch_consistency, &input.stage2.batch_output_claims, values, - output_ids, - aliases, ) } -/// The stage-2 committed output row order and alias rows. -/// -/// With field-inline disabled: the jolt members' canonical orders with the instruction -/// reduction's aliased ids elided (absorbed once via their product-remainder sources). -/// Canonical output rows in member order, excluding aliased reduction claims. -fn stage2_output_ids_and_aliases( -) -> (Vec, Vec>) { - let product_order = ProductRemainderOutputClaims:: { - left_instruction_input: F::zero(), - right_instruction_input: F::zero(), - jump_flag: F::zero(), - write_lookup_output_to_rd: F::zero(), - lookup_output: F::zero(), - branch_flag: F::zero(), - next_is_noop: F::zero(), - virtual_instruction: F::zero(), - } - .canonical_order(); - let instruction_outputs = InstructionClaimReductionOutputClaims:: { - lookup_output: F::zero(), - left_lookup_operand: F::zero(), - right_lookup_operand: F::zero(), - left_instruction_input: F::zero(), - right_instruction_input: F::zero(), - } - .canonical_order(); - - // Single-sourced from the reduction's declared alias pairs - // (`ConcreteSumcheck::aliased_output_openings`): the committed output rows - // absorb the reduction's canonical openings minus its aliased ids, and the - // `OpeningAlias` rows mirror the same `(aliased, source)` pairs — so - // BlindFold's row layout cannot drift from the clear path's generated absorb - // and `validate_aliases`. - let alias_pairs = - as ConcreteSumcheck>::aliased_output_openings(); - let aliased_targets: BTreeSet<_> = alias_pairs.iter().map(|(aliased, _)| *aliased).collect(); - - let mut output_ids: Vec = composite_ids( - RamReadWriteOutputClaims:: { - val: F::zero(), - ra: F::zero(), - inc: F::zero(), - } - .canonical_order(), - ); - output_ids.extend(composite_ids(product_order)); - #[cfg(feature = "field-inline")] - output_ids.extend(super::field_inline::stage2_product_opening_ids()); - output_ids.extend( - instruction_outputs - .into_iter() - .filter(|id| !aliased_targets.contains(id)) - .map(ComposedOpeningId::from), - ); - output_ids.extend(composite_ids( - RamRafEvaluationOutputClaims:: { ram_ra: F::zero() }.canonical_order(), - )); - output_ids.extend(composite_ids( - RamOutputCheckOutputClaims:: { - val_final: F::zero(), - } - .canonical_order(), - )); - let aliases = composite_aliases(alias_pairs); - #[cfg(feature = "field-inline")] - let aliases = aliases - .into_iter() - .chain(composite_aliases( - FieldRegistersClaimReduction::::aliased_output_openings(), - )) - .collect(); - (output_ids, aliases) -} - fn selected_product_uniskip_input_expr( weights: &[F], ) -> Result, VerifierError> { @@ -385,22 +293,11 @@ fn bake_product_weights( Ok(expr) } -#[cfg(test)] -#[cfg_attr(feature = "field-inline", expect(clippy::unwrap_used))] -#[cfg_attr( - not(feature = "field-inline"), - expect( - clippy::useless_conversion, - reason = "field-inline selects composed claim and opening types" - ) -)] +#[cfg(all(test, feature = "field-inline"))] +#[expect(clippy::unwrap_used)] mod tests { use super::*; - #[cfg(feature = "field-inline")] - use crate::stages::stage2::outputs::{ - FieldRegistersClaimReductionOutputClaims, FieldRegistersProductOutputClaims, - }; - #[cfg(feature = "field-inline")] + use crate::stages::stage2::outputs::FieldRegistersProductOutputClaims; use jolt_claims::protocols::jolt::geometry::spartan::{ product_outer_opening, product_should_branch_outer_opening, product_should_jump_outer_opening, @@ -411,110 +308,6 @@ mod tests { Fr::from_u64(value) } - /// The stage-2 committed row order is the clear curated absorb order, - /// locked entry-for-entry: build sentinel-valued claim structs, run the - /// batch's `opening_values` curation, and check every lowered id resolves - /// to the value at its row position. - #[test] - fn stage2_output_ids_match_the_clear_absorb_order() { - use crate::stages::stage2::outputs::{ - InstructionClaimReductionOutputClaims, ProductRemainderOutputClaims, - RamOutputCheckOutputClaims, RamRafEvaluationOutputClaims, RamReadWriteOutputClaims, - Stage2BatchOutputClaims, - }; - use jolt_claims::OutputClaims as _; - - let claims = Stage2BatchOutputClaims:: { - ram_read_write: RamReadWriteOutputClaims { - val: fr(1), - ra: fr(2), - inc: fr(3), - }, - product_remainder: ProductRemainderOutputClaims { - left_instruction_input: fr(4), - right_instruction_input: fr(5), - jump_flag: fr(6), - write_lookup_output_to_rd: fr(7), - lookup_output: fr(8), - branch_flag: fr(9), - next_is_noop: fr(10), - virtual_instruction: fr(11), - } - .into(), - instruction_claim_reduction: InstructionClaimReductionOutputClaims { - lookup_output: fr(8), - left_lookup_operand: fr(12), - right_lookup_operand: fr(13), - left_instruction_input: fr(4), - right_instruction_input: fr(5), - }, - #[cfg(feature = "field-inline")] - field_registers_claim_reduction: FieldRegistersClaimReductionOutputClaims { - rd_value: fr(16), - rs1_value: fr(17), - rs2_value: fr(18), - }, - ram_raf_evaluation: RamRafEvaluationOutputClaims { ram_ra: fr(14) }, - ram_output_check: RamOutputCheckOutputClaims { val_final: fr(15) }, - }; - - // Resolve a lowered composite id against the claim structs (the field-inline appendage - // rides beside the batch, so it has its own resolver). - #[cfg(feature = "field-inline")] - let appendage = FieldRegistersProductOutputClaims:: { - rs1_value: fr(201), - rs2_value: fr(202), - rd_value: fr(203), - }; - let resolve = |id: &ComposedOpeningId| -> Option { - match id { - ComposedOpeningId::Jolt(id) => claims - .ram_read_write - .resolve_output(id) - .or_else(|| claims.product_remainder.resolve_output(&(*id).into())) - .or_else(|| claims.instruction_claim_reduction.resolve_output(id)) - .or_else(|| claims.ram_raf_evaluation.resolve_output(id)) - .or_else(|| claims.ram_output_check.resolve_output(id)), - #[cfg(feature = "field-inline")] - ComposedOpeningId::FieldInline(id) => claims - .field_registers_claim_reduction - .resolve_output(id) - .or_else(|| appendage.resolve_output(id)), - #[cfg(not(feature = "field-inline"))] - ComposedOpeningId::FieldInline(_) => None, - } - }; - - // The clear curated order over the same sentinels. - #[cfg(not(feature = "field-inline"))] - let expected_values: Vec = (1..=15).map(fr).collect(); - #[cfg(feature = "field-inline")] - let expected_values: Vec = (1..=11) - .map(fr) - .chain([fr(201), fr(202), fr(203)]) - .chain([fr(12), fr(13)]) - .chain([fr(14), fr(15)]) - .collect(); - - let (output_ids, aliases) = stage2_output_ids_and_aliases::(); - assert_eq!(output_ids.len(), expected_values.len()); - #[cfg(not(feature = "field-inline"))] - assert_eq!(output_ids.len(), 15); - #[cfg(feature = "field-inline")] - assert_eq!(output_ids.len(), 18); - for (id, expected) in output_ids.iter().zip(expected_values) { - assert_eq!( - resolve(id), - Some(expected), - "row {id:?} must sit at the clear absorb position of value {expected:?}", - ); - } - assert_eq!( - aliases.len(), - if cfg!(feature = "field-inline") { 6 } else { 3 } - ); - } - /// The lowered composed uni-skip input expression evaluates identically to the clear /// `ProductUniskip::input_claim` composition on synthetic values (with field-inline /// enabled: all five lanes; the field-inline inputs read from the stage-1 field-inline diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs index f48fd36c9d..0455eed178 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs @@ -1,7 +1,5 @@ use super::*; -use jolt_claims::protocols::jolt::relations::spartan::SpartanShiftOutputClaims; - pub(super) fn add_stage3( input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, @@ -90,60 +88,6 @@ where .map_err(|error| public_error(JoltRelationId::RegistersClaimReduction, error))?, )?; - // Single-sourced from the relations' declared alias pairs - // (`ConcreteSumcheck::aliased_output_openings`): the committed output rows - // absorb each member's canonical openings minus its aliased ids, and the - // `OpeningAlias` rows mirror the same `(aliased, source)` pairs — so - // BlindFold's row layout cannot drift from the clear path's generated absorb - // and `validate_aliases`. - let alias_pairs: Vec<_> = as - crate::stages::relations::ConcreteSumcheck>::aliased_output_openings() - .into_iter() - .chain( as - crate::stages::relations::ConcreteSumcheck>::aliased_output_openings()) - .collect(); - let aliased_targets: std::collections::BTreeSet<_> = - alias_pairs.iter().map(|(aliased, _)| *aliased).collect(); - - let zero = PCS::Field::zero(); - let mut output_ids = composite_ids( - SpartanShiftOutputClaims:: { - unexpanded_pc: zero, - pc: zero, - is_virtual: zero, - is_first_in_sequence: zero, - is_noop: zero, - } - .canonical_order(), - ); - output_ids.extend( - relations::instruction::InstructionInputOutputClaims:: { - left_operand_is_rs1: zero, - rs1_value: zero, - left_operand_is_pc: zero, - unexpanded_pc: zero, - right_operand_is_rs2: zero, - rs2_value: zero, - right_operand_is_imm: zero, - imm: zero, - } - .canonical_order() - .into_iter() - .filter(|id| !aliased_targets.contains(id)) - .map(ComposedOpeningId::from), - ); - output_ids.extend( - relations::claim_reductions::registers::RegistersClaimReductionOutputClaims:: { - rd_write_value: zero, - rs1_value: zero, - rs2_value: zero, - } - .canonical_order() - .into_iter() - .filter(|id| !aliased_targets.contains(id)) - .map(ComposedOpeningId::from), - ); - let aliases = composite_aliases(alias_pairs); add_batched_stage( builder, "stage3.batch", @@ -156,7 +100,5 @@ where &input.stage3.batch_consistency, &input.stage3.batch_output_claims, values, - output_ids, - aliases, ) } diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs index 74d8af9fcf..ae5b662cc5 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs @@ -1,8 +1,5 @@ use super::*; -use jolt_claims::protocols::jolt::relations::ram::RamValCheckOutputClaims; -use jolt_claims::protocols::jolt::relations::registers::RegistersReadWriteOutputClaims; - // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. @@ -131,12 +128,6 @@ where + input.stage4.challenges.ram_val_check.gamma, )?; - let output_ids = stage4_output_ids::( - input.checked.untrusted_advice_commitment_present, - input.checked.trusted_advice_commitment_present, - input.checked.precommitted.program_image.is_some(), - ); - // Member declaration order (= batching-coefficient draw order): the field-inline // read/write member sits between the registers and the RAM value-check, exactly as in // `Stage4Sumchecks`. @@ -153,134 +144,5 @@ where &input.stage4.batch_consistency, &input.stage4.batch_output_claims, values, - output_ids, - Vec::new(), ) } - -/// The stage-4 committed output row order: the staged `Val_init` advice / program-image -/// openings first, the register read/write openings, then (under `field-inline`) the five -/// field-register read/write rows, then `ram_ra`/`ram_inc` — the clear absorb order -/// (`Stage4OutputClaims::opening_values`) exactly. -fn stage4_output_ids( - untrusted_advice: bool, - trusted_advice: bool, - program_image: bool, -) -> Vec { - let mut output_ids: Vec = Vec::new(); - if untrusted_advice { - output_ids.push(ram::val_check_advice_opening(JoltAdviceKind::Untrusted).into()); - } - if trusted_advice { - output_ids.push(ram::val_check_advice_opening(JoltAdviceKind::Trusted).into()); - } - if program_image { - output_ids.push(program_image::ram_val_check_contribution_opening().into()); - } - output_ids.extend(composite_ids( - RegistersReadWriteOutputClaims:: { - registers_val: F::zero(), - rs1_ra: F::zero(), - rs2_ra: F::zero(), - rd_wa: F::zero(), - rd_inc: F::zero(), - } - .canonical_order(), - )); - // The five field-register read/write rows, spliced after the register openings and before - // `ram_ra`/`ram_inc` — the clear absorb order. - #[cfg(feature = "field-inline")] - output_ids.extend(super::field_inline::stage4_output_ids()); - // The advice / program-image openings are produced by the RAM value-check - // instance, but the stage-4 commit (flush) order appends them *first* (above), - // before the registers; so here, at the tail, only the main `ram_ra`/`ram_inc` - // canonical order is emitted (advice / program-image leaves left `None`), - // preserving the prover's per-stage opening-id block order. - output_ids.extend(composite_ids( - RamValCheckOutputClaims:: { - untrusted_advice: None, - trusted_advice: None, - program_image: None, - ram_ra: F::zero(), - ram_inc: F::zero(), - } - .canonical_order(), - )); - output_ids -} - -#[cfg(test)] -mod tests { - use super::*; - #[cfg(feature = "field-inline")] - use crate::stages::stage4::outputs::FieldRegistersReadWriteOutputClaims; - use jolt_field::{Fr, Ring}; - - fn fr(value: u64) -> Fr { - Fr::from_u64(value) - } - - /// The stage-4 committed row order is the clear curated absorb order - /// (`Stage4OutputClaims::opening_values`), locked entry-for-entry over sentinel-valued - /// claims: every lowered id resolves to the value at its row position (with field-inline - /// enabled: the five field-inline rows spliced after the registers, before - /// `ram_ra`/`ram_inc`). - #[test] - fn stage4_output_ids_match_the_clear_absorb_order() { - use crate::stages::stage4::outputs::Stage4OutputClaims; - use jolt_claims::protocols::jolt::relations::ram::RamValCheckOutputClaims; - use jolt_claims::protocols::jolt::relations::registers::RegistersReadWriteOutputClaims; - - let claims = Stage4OutputClaims:: { - registers_read_write: RegistersReadWriteOutputClaims { - registers_val: fr(1), - rs1_ra: fr(2), - rs2_ra: fr(3), - rd_wa: fr(4), - rd_inc: fr(5), - }, - #[cfg(feature = "field-inline")] - field_registers_read_write: FieldRegistersReadWriteOutputClaims { - registers_val: fr(11), - rs1_ra: fr(12), - rs2_ra: fr(13), - rd_wa: fr(14), - rd_inc: fr(15), - }, - ram_val_check: RamValCheckOutputClaims { - untrusted_advice: None, - trusted_advice: None, - program_image: None, - ram_ra: fr(6), - ram_inc: fr(7), - }, - }; - let clear_values = claims.opening_values(); - let output_ids = stage4_output_ids::(false, false, false); - assert_eq!(output_ids.len(), clear_values.len()); - #[cfg(not(feature = "field-inline"))] - assert_eq!(output_ids.len(), 7); - #[cfg(feature = "field-inline")] - assert_eq!(output_ids.len(), 12); - - for (id, expected) in output_ids.iter().zip(clear_values) { - let resolved = match id { - ComposedOpeningId::Jolt(id) => claims - .registers_read_write - .resolve_output(id) - .or_else(|| claims.ram_val_check.resolve_output(id)), - #[cfg(feature = "field-inline")] - ComposedOpeningId::FieldInline(id) => { - claims.field_registers_read_write.resolve_output(id) - } - #[cfg(not(feature = "field-inline"))] - ComposedOpeningId::FieldInline(_) => None, - }; - assert_eq!( - resolved, - Some(expected), - "row {id:?} must sit at the clear absorb position of value {expected:?}", - ); - } - } -} diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs index d4b7dc75c9..124afaa5e0 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs @@ -1,9 +1,5 @@ use super::*; -use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafOutputOpenings; -use jolt_claims::protocols::jolt::relations::ram::RamRaClaimReductionOutputClaims; -use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationOutputClaims; - // Binding the scalar field to a bare `F` parameter (rather than spelling // `PCS::Field`) lets clippy.toml's `arithmetic-side-effects-allowed = ["F"]` // recognize the side-effect-free field arithmetic in the body. @@ -31,8 +27,6 @@ where VerifierPublicId::Challenge(JoltChallengeId::from(InstructionReadRafChallenge::Gamma)), input.stage5.challenges.instruction_read_raf.gamma, )?; - let instruction_output_openings = - instruction::read_raf_output_openings(formula_dimensions.instruction_read_raf); let instruction_point = input .stage5 .batch_consistency @@ -160,8 +154,6 @@ where .field_registers_read_write_point(), )?; - let output_ids = stage5_output_ids::(instruction_output_openings); - // Member declaration order (= batching-coefficient draw order): the field-inline // val-evaluation member is declared last, exactly as in `Stage5Sumchecks`. #[cfg_attr(not(feature = "field-inline"), expect(unused_mut))] @@ -181,132 +173,5 @@ where &input.stage5.batch_consistency, &input.stage5.batch_output_claims, values, - output_ids, - Vec::new(), ) } - -/// The stage-5 committed output row order: the instruction read-RAF openings, the reduced RAM -/// RA, the register value-evaluation openings, then (under `field-inline`) the two -/// field-register value-evaluation rows at the tail — the clear absorb order (the generated -/// `Stage5Sumchecks` member-declaration absorb). -fn stage5_output_ids( - instruction_output_openings: InstructionReadRafOutputOpenings, -) -> Vec { - let mut output_ids: Vec = - composite_ids(instruction_output_openings.lookup_table_flags); - output_ids.extend(composite_ids(instruction_output_openings.instruction_ra)); - output_ids.push(instruction_output_openings.instruction_raf_flag.into()); - output_ids.extend(composite_ids( - RamRaClaimReductionOutputClaims:: { ram_ra: F::zero() }.canonical_order(), - )); - output_ids.extend(composite_ids( - RegistersValEvaluationOutputClaims:: { - rd_inc: F::zero(), - rd_wa: F::zero(), - } - .canonical_order(), - )); - // The two field-register value-evaluation rows, after the ordinary register - // value-evaluation outputs — the clear absorb order (the field-inline member is declared - // last, so the generated absorb appends them at the tail). - #[cfg(feature = "field-inline")] - output_ids.extend(super::field_inline::stage5_output_ids()); - output_ids -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -#[expect( - clippy::as_conversions, - reason = "tests use plain arithmetic on fixture data" -)] -mod tests { - use super::*; - #[cfg(feature = "field-inline")] - use crate::stages::stage5::outputs::{ - FieldRegistersValEvaluation, FieldRegistersValEvaluationOutputClaims, - }; - use crate::stages::stage5::outputs::{Stage5OutputClaims, Stage5Sumchecks}; - use crate::stages::stage5::ram_ra_claim_reduction::RamRaClaimReduction; - use crate::stages::stage5::registers_val_evaluation::RegistersValEvaluation; - use crate::stages::stage5::InstructionReadRaf; - #[cfg(feature = "field-inline")] - use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; - use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafDimensions; - use jolt_claims::protocols::jolt::relations::instruction::InstructionReadRafOutputClaims; - use jolt_claims::protocols::jolt::relations::ram::RamRaClaimReductionOutputClaims; - use jolt_claims::protocols::jolt::relations::registers::RegistersValEvaluationOutputClaims; - use jolt_claims::protocols::jolt::TraceDimensions; - use jolt_field::{Fr, Ring}; - - fn fr(value: u64) -> Fr { - Fr::from_u64(value) - } - - /// The stage-5 committed row order is the clear absorb order (the generated - /// member-declaration `opening_values`), locked entry-for-entry over sentinel-valued - /// claims (with field-inline enabled: the two field-register value-evaluation rows at the - /// tail). - #[test] - fn stage5_output_ids_match_the_clear_absorb_order() { - let log_t = 3usize; - let dimensions = InstructionReadRafDimensions::try_from((log_t, 128, 3)).unwrap(); - let trace_dimensions = TraceDimensions::new(log_t); - let sumchecks = Stage5Sumchecks:: { - instruction_read_raf: InstructionReadRaf::new(dimensions), - ram_ra_claim_reduction: RamRaClaimReduction::new(trace_dimensions, 3), - registers_val_evaluation: RegistersValEvaluation::new(trace_dimensions), - #[cfg(feature = "field-inline")] - field_registers_val_evaluation: FieldRegistersValEvaluation::new( - FieldRegistersTraceDimensions::new(log_t), - ), - }; - let openings = instruction::read_raf_output_openings(dimensions); - let claims = Stage5OutputClaims:: { - instruction_read_raf: InstructionReadRafOutputClaims { - lookup_table_flags: (0..openings.lookup_table_flags.len() as u64) - .map(|index| fr(100 + index)) - .collect(), - instruction_ra: (0..openings.instruction_ra.len() as u64) - .map(|index| fr(200 + index)) - .collect(), - instruction_raf_flag: fr(300), - }, - ram_ra_claim_reduction: RamRaClaimReductionOutputClaims { ram_ra: fr(301) }, - registers_val_evaluation: RegistersValEvaluationOutputClaims { - rd_inc: fr(302), - rd_wa: fr(303), - }, - #[cfg(feature = "field-inline")] - field_registers_val_evaluation: FieldRegistersValEvaluationOutputClaims { - rd_inc: fr(401), - rd_wa: fr(402), - }, - }; - let clear_values = sumchecks.opening_values(&claims); - - let output_ids = stage5_output_ids::(instruction::read_raf_output_openings(dimensions)); - assert_eq!(output_ids.len(), clear_values.len()); - for (id, expected) in output_ids.iter().zip(clear_values) { - let resolved = match id { - ComposedOpeningId::Jolt(id) => claims - .instruction_read_raf - .resolve_output(id) - .or_else(|| claims.ram_ra_claim_reduction.resolve_output(id)) - .or_else(|| claims.registers_val_evaluation.resolve_output(id)), - #[cfg(feature = "field-inline")] - ComposedOpeningId::FieldInline(id) => { - claims.field_registers_val_evaluation.resolve_output(id) - } - #[cfg(not(feature = "field-inline"))] - ComposedOpeningId::FieldInline(_) => None, - }; - assert_eq!( - resolved, - Some(expected), - "row {id:?} must sit at the clear absorb position of value {expected:?}", - ); - } - } -} diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs index 7cc63aafd3..c9e9af8138 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage6a.rs @@ -98,17 +98,6 @@ where add_program_image_reduction_cycle_publics(input, values, layout)?; } - let mut address_phase_output_ids: Vec = - vec![bytecode::bytecode_read_raf_address_phase_opening().into()]; - if bytecode_reduction_layout.is_some() { - address_phase_output_ids.extend( - (0..bytecode_reduction::NUM_BYTECODE_VAL_STAGES) - .map(bytecode_reduction::bytecode_val_stage_opening) - .map(ComposedOpeningId::from), - ); - } - address_phase_output_ids.push(booleanity::booleanity_address_phase_opening().into()); - // Field op flags use the ordinary stage-1 circuit-flag fold. The composed // claim adds field-register access terms at the extended stage-4/5 powers, // referencing the same committed opening rows as the clear relation. @@ -122,8 +111,6 @@ where &input.stage6a.consistency, &input.stage6a.output_claims, values, - address_phase_output_ids, - Vec::new(), ) } diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs index 3dcb83510e..85dda9cf11 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs @@ -1,10 +1,5 @@ use super::*; -use jolt_claims::protocols::jolt::relations::claim_reductions::increments::IncClaimReductionOutputClaims; -use jolt_claims::protocols::jolt::relations::ram::{ - RamHammingBooleanityOutputClaims, RamRaVirtualizationOutputClaims, -}; - pub(super) fn add_stage6b( input: &BlindFoldInputs<'_, PCS, VC>, builder: Builder, @@ -95,24 +90,6 @@ where batch_claims.push(relation_claim(claim)); } - let booleanity_opening_point = input - .stage6b - .output_points - .booleanity_opening_point() - .ok_or_else(|| VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::Booleanity, - reason: "Stage 6 booleanity produced no opening point".to_string(), - })?; - let (output_ids, aliases) = stage6b_output_ids_and_aliases::( - formula_dimensions, - &input.stage6b.output_points.bytecode_read_raf.bytecode_ra, - booleanity_opening_point, - trusted_layout.as_ref(), - untrusted_layout.as_ref(), - bytecode_reduction_layout.as_ref(), - program_image_reduction_layout.as_ref(), - ); - add_batched_stage( builder, "stage6.cycle_phase", @@ -121,284 +98,9 @@ where &input.stage6b.batch_consistency, &input.stage6b.batch_output_claims, values, - output_ids, - aliases, ) } -/// The stage-6b committed output row order and alias rows: the bytecode-RA rows with the -/// booleanity dedup, the remaining members' canonical orders in member declaration order, and -/// — under `field-inline` — the reduced field-inline `FieldRdInc` row after the ordinary -/// increment-reduction outputs and before the optional advice cycle phases: the clear absorb -/// order (`stage6b_opening_values`) exactly. -fn stage6b_output_ids_and_aliases( - formula_dimensions: JoltFormulaDimensions, - bytecode_ra_opening_points: &[Vec], - booleanity_opening_point: &[F], - trusted_layout: Option<&AdviceClaimReductionLayout>, - untrusted_layout: Option<&AdviceClaimReductionLayout>, - bytecode_reduction_layout: Option<&BytecodeClaimReductionLayout>, - program_image_reduction_layout: Option<&ProgramImageClaimReductionLayout>, -) -> (Vec, Vec>) { - let (mut output_ids, aliases) = stage6_cycle_output_openings_and_aliases( - formula_dimensions, - bytecode_ra_opening_points, - booleanity_opening_point, - ); - output_ids.extend(composite_ids( - RamHammingBooleanityOutputClaims:: { - ram_hamming_weight: F::zero(), - } - .canonical_order(), - )); - output_ids.extend(composite_ids( - RamRaVirtualizationOutputClaims:: { - ram_ra: vec![ - F::zero(); - formula_dimensions - .ram_ra_virtualization - .num_committed_ra_polys() - ], - } - .canonical_order(), - )); - output_ids.extend(composite_ids( - instruction::ra_virtualization_output_openings( - formula_dimensions.instruction_ra_virtualization, - ) - .all(), - )); - output_ids.extend( - IncClaimReductionOutputClaims:: { - ram_inc: F::zero(), - rd_inc: F::zero(), - } - .canonical_order() - .into_iter() - .map(ComposedOpeningId::from), - ); - // The reduced field-inline `FieldRdInc` row, after the ordinary increment-reduction - // outputs and before the optional advice cycle phases — the clear absorb order - // (`stage6b_opening_values`). - #[cfg(feature = "field-inline")] - output_ids.extend(super::field_inline::stage6b_inc_output_ids()); - if let Some(layout) = trusted_layout { - output_ids.extend( - advice::cycle_phase_output_openings(JoltAdviceKind::Trusted, layout.dimensions()) - .into_iter() - .map(ComposedOpeningId::from), - ); - } - if let Some(layout) = untrusted_layout { - output_ids.extend( - advice::cycle_phase_output_openings(JoltAdviceKind::Untrusted, layout.dimensions()) - .into_iter() - .map(ComposedOpeningId::from), - ); - } - if let Some(layout) = bytecode_reduction_layout { - output_ids.extend( - bytecode_reduction::cycle_phase_output_openings( - layout.dimensions(), - layout.chunk_count(), - ) - .into_iter() - .map(ComposedOpeningId::from), - ); - } - if let Some(layout) = program_image_reduction_layout { - output_ids.extend( - program_image::cycle_phase_output_openings(layout.dimensions()) - .into_iter() - .map(ComposedOpeningId::from), - ); - } - (output_ids, aliases) -} - -#[expect( - clippy::wildcard_enum_match_arm, - reason = "fail-closed: unmatched opening ids yield no alias and are reported missing below" -)] -fn stage6_cycle_output_openings_and_aliases( - formula_dimensions: JoltFormulaDimensions, - bytecode_ra_opening_points: &[Vec], - booleanity_opening_point: &[F], -) -> (Vec, Vec>) { - let bytecode_output_openings = - bytecode::read_raf_output_openings(formula_dimensions.bytecode_read_raf); - let booleanity_output_openings = - booleanity::booleanity_output_openings(formula_dimensions.ra_layout); - - let mut output_ids = composite_ids(bytecode_output_openings.bytecode_ra.clone()); - let mut aliases = Vec::new(); - for id in booleanity_output_openings { - let source = match id { - JoltOpeningId::Polynomial { - polynomial: JoltPolynomialId::Committed(JoltCommittedPolynomial::BytecodeRa(index)), - relation: JoltRelationId::Booleanity, - } if bytecode_ra_opening_points - .get(index) - .is_some_and(|point| point.as_slice() == booleanity_opening_point) => - { - bytecode_output_openings.bytecode_ra.get(index).copied() - } - _ => None, - }; - if let Some(source) = source { - aliases.push(OpeningAlias::new(id.into(), source.into())); - } else { - output_ids.push(id.into()); - } - } - - (output_ids, aliases) -} - -#[cfg(test)] -#[expect(clippy::unwrap_used)] -#[expect( - clippy::arithmetic_side_effects, - reason = "tests use plain arithmetic on fixture data" -)] -mod tests { - use super::*; - use crate::stages::stage6b::booleanity::BooleanityOutputClaims; - use crate::stages::stage6b::bytecode_read_raf::BytecodeReadRafOutputClaims; - use crate::stages::stage6b::inc_claim_reduction::IncClaimReductionOutputClaims; - use crate::stages::stage6b::instruction_ra_virtualization::InstructionRaVirtualizationOutputClaims; - #[cfg(feature = "field-inline")] - use crate::stages::stage6b::outputs::FieldRegistersIncClaimReductionOutputClaims; - use crate::stages::stage6b::outputs::Stage6bOutputClaims; - use crate::stages::stage6b::ram_hamming_booleanity::RamHammingBooleanityOutputClaims; - use crate::stages::stage6b::ram_ra_virtualization::RamRaVirtualizationOutputClaims; - use crate::stages::stage6b::stage6b_opening_values; - use jolt_claims::protocols::jolt::geometry::dimensions::JoltOneHotDimensions; - use jolt_field::{Fr, Ring}; - - fn fr(value: u64) -> Fr { - Fr::from_u64(value) - } - - fn formula_dimensions() -> JoltFormulaDimensions { - JoltFormulaDimensions::try_from(JoltOneHotDimensions { - log_t: 8, - instruction_address_bits: 128, - bytecode_k: 1024, - ram_k: 4096, - committed_chunk_bits: 8, - lookup_virtual_chunk_bits: 32, - }) - .unwrap() - } - - /// Claims with distinct sentinel values, every wire vector sized from the - /// formula dimensions. - fn sentinel_claims(formula_dimensions: &JoltFormulaDimensions) -> Stage6bOutputClaims { - let bytecode_ra_len = - bytecode::read_raf_output_openings(formula_dimensions.bytecode_read_raf) - .bytecode_ra - .len(); - let ra_layout = formula_dimensions.ra_layout; - let mut next = { - let mut counter = 0u64; - move || { - counter += 1; - fr(counter) - } - }; - Stage6bOutputClaims { - bytecode_read_raf: BytecodeReadRafOutputClaims { - bytecode_ra: (0..bytecode_ra_len).map(|_| next()).collect(), - }, - booleanity: BooleanityOutputClaims { - instruction_ra: (0..ra_layout.instruction()).map(|_| next()).collect(), - bytecode_ra: (0..ra_layout.bytecode()).map(|_| next()).collect(), - ram_ra: (0..ra_layout.ram()).map(|_| next()).collect(), - }, - ram_hamming_booleanity: RamHammingBooleanityOutputClaims { - ram_hamming_weight: next(), - }, - ram_ra_virtualization: RamRaVirtualizationOutputClaims { - ram_ra: (0..formula_dimensions - .ram_ra_virtualization - .num_committed_ra_polys()) - .map(|_| next()) - .collect(), - }, - instruction_ra_virtualization: InstructionRaVirtualizationOutputClaims { - committed_instruction_ra: (0..formula_dimensions - .instruction_ra_virtualization - .num_committed_ra_polys()) - .map(|_| next()) - .collect(), - }, - inc_claim_reduction: IncClaimReductionOutputClaims { - ram_inc: next(), - rd_inc: next(), - }, - #[cfg(feature = "field-inline")] - field_registers_inc_claim_reduction: FieldRegistersIncClaimReductionOutputClaims { - rd_inc: next(), - }, - trusted_advice: None, - untrusted_advice: None, - bytecode_reduction: None, - program_image_reduction: None, - } - } - - /// The stage-6b committed row order is the clear curated absorb order - /// (`stage6b_opening_values`), locked entry-for-entry over sentinel-valued claims — with - /// field-inline enabled: the reduced `FieldRdInc` row after the ordinary - /// increment-reduction outputs, before the (absent here) advice cycle phases. Empty points - /// mean no booleanity dedup fires on either side. - #[test] - fn stage6b_output_ids_match_the_clear_absorb_order() { - use jolt_claims::OutputClaims as _; - - let formula_dimensions = formula_dimensions(); - let claims = sentinel_claims(&formula_dimensions); - let clear_values = stage6b_opening_values(&claims, &[], &[]); - - let (output_ids, aliases) = stage6b_output_ids_and_aliases::( - formula_dimensions, - &[], - &[], - None, - None, - None, - None, - ); - assert!(aliases.is_empty()); - assert_eq!(output_ids.len(), clear_values.len()); - - for (id, expected) in output_ids.iter().zip(clear_values) { - let resolved = match id { - ComposedOpeningId::Jolt(id) => claims - .bytecode_read_raf - .resolve_output(id) - .or_else(|| claims.booleanity.resolve_output(id)) - .or_else(|| claims.ram_hamming_booleanity.resolve_output(id)) - .or_else(|| claims.ram_ra_virtualization.resolve_output(id)) - .or_else(|| claims.instruction_ra_virtualization.resolve_output(id)) - .or_else(|| claims.inc_claim_reduction.resolve_output(id)), - #[cfg(feature = "field-inline")] - ComposedOpeningId::FieldInline(id) => claims - .field_registers_inc_claim_reduction - .resolve_output(id), - #[cfg(not(feature = "field-inline"))] - ComposedOpeningId::FieldInline(_) => None, - }; - assert_eq!( - resolved, - Some(expected), - "row {id:?} must sit at the clear absorb position of value {expected:?}", - ); - } - } -} - #[cfg(all(test, feature = "field-inline"))] #[expect(clippy::unwrap_used, clippy::get_unwrap)] #[expect( diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs b/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs index 93d45bd86f..ad48faaeec 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs @@ -121,31 +121,21 @@ where add_program_image_reduction_address_publics(input, values, layout, &point)?; } - let output_openings = hamming_weight::claim_reduction_output_openings(hamming_dimensions); - let mut output_ids = composite_ids(output_openings.all()); let mut claims = vec![relation_claim(&hamming_claims)]; if let Some(claim) = trusted_claims { claims.push(relation_claim(&claim)); - output_ids.push(advice::final_advice_opening(JoltAdviceKind::Trusted).into()); } if let Some(claim) = untrusted_claims { claims.push(relation_claim(&claim)); - output_ids.push(advice::final_advice_opening(JoltAdviceKind::Untrusted).into()); } - if let (Some(layout), Some(claim)) = ( + if let (Some(_), Some(claim)) = ( bytecode_reduction_layout.as_ref(), bytecode_reduction_claims, ) { claims.push(relation_claim(&claim)); - output_ids.extend( - (0..layout.chunk_count()) - .map(bytecode_reduction::final_bytecode_chunk_opening) - .map(ComposedOpeningId::from), - ); } if let Some(claim) = program_image_reduction_claims { claims.push(relation_claim(&claim)); - output_ids.push(program_image::final_program_image_opening().into()); } add_batched_stage( builder, @@ -155,7 +145,5 @@ where &input.stage7.batch_consistency, &input.stage7.batch_output_claims, values, - output_ids, - Vec::new(), ) } diff --git a/crates/jolt-verifier/src/stages/zk/committed.rs b/crates/jolt-verifier/src/stages/zk/committed.rs deleted file mode 100644 index 21afb2c8be..0000000000 --- a/crates/jolt-verifier/src/stages/zk/committed.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Shared checks for committed sumcheck stage boundaries. - -use crate::{verifier::CheckedInputs, VerifierError}; - -pub(crate) use crate::stages::zk::outputs::{ - CommittedOutputClaimOutput, CommittedOutputClaimShape, -}; - -/// The row layout of `output_claim_count` hidden claims at the build's vector -/// commitment capacity, which fixes how many output-claim commitments a -/// committed sumcheck sends. -pub(crate) fn output_claim_shape( - checked: &CheckedInputs, - output_claim_count: usize, -) -> Result { - // Invariant: Some(capacity) implies capacity >= MAX_BLINDFOLD_GENERATORS, - // enforced by validate_zk_vector_commitment_setup before any stage runs (also - // guards the div_ceil in `row_count` against zero). - let capacity = checked - .vc_capacity - .ok_or(VerifierError::MissingVectorCommitmentSetup)?; - Ok(CommittedOutputClaimShape { - output_claim_count, - row_len: capacity, - }) -} diff --git a/crates/jolt-verifier/src/stages/zk/mod.rs b/crates/jolt-verifier/src/stages/zk/mod.rs index 435ebcc50a..bc1ae8eff5 100644 --- a/crates/jolt-verifier/src/stages/zk/mod.rs +++ b/crates/jolt-verifier/src/stages/zk/mod.rs @@ -5,7 +5,6 @@ #[cfg(not(feature = "akita"))] #[doc(hidden)] pub mod blindfold; -pub(crate) mod committed; #[cfg(not(feature = "akita"))] #[doc(hidden)] pub mod inputs; diff --git a/crates/jolt-verifier/src/stages/zk/outputs.rs b/crates/jolt-verifier/src/stages/zk/outputs.rs index 666336d4dd..965440eeb8 100644 --- a/crates/jolt-verifier/src/stages/zk/outputs.rs +++ b/crates/jolt-verifier/src/stages/zk/outputs.rs @@ -1,15 +1,29 @@ -use jolt_sumcheck::CommittedOutputClaims; +use jolt_field::JoltField; +use jolt_sumcheck::{BatchedCommittedSumcheckConsistency, CommittedOutputClaims}; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +use crate::stages::relations::CommittedClaimLayout; +#[cfg(doc)] +use crate::verifier::CheckedInputs; + +/// The row layout of a committed sumcheck's hidden output claims: the +/// committed claim cells in row order, packed `row_len` to a row, and the +/// aliased cells that read their source rows. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct CommittedOutputClaimShape { - pub output_claim_count: usize, + pub layout: CommittedClaimLayout, pub row_len: usize, } impl CommittedOutputClaimShape { + /// `layout` packed `row_len` cells to a row + /// ([`CheckedInputs::committed_row_len`]). + pub fn new(row_len: usize, layout: CommittedClaimLayout) -> Self { + Self { layout, row_len } + } + /// Number of output-claim row commitments the committed sumcheck sends. pub fn row_count(&self) -> usize { - self.output_claim_count.div_ceil(self.row_len) + self.layout.ids.len().div_ceil(self.row_len) } } @@ -18,3 +32,13 @@ pub struct CommittedOutputClaimOutput { pub shape: CommittedOutputClaimShape, pub commitments: CommittedOutputClaims, } + +/// A committed stage batch as the ZK verifier reads it: the round +/// consistency, the output points derived at the batch point, and the +/// output-claim row commitments over those points' committed cells. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifiedCommittedBatch { + pub consistency: BatchedCommittedSumcheckConsistency, + pub output_points: P, + pub output_claims: CommittedOutputClaimOutput, +} diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index d85a883f95..dd9c85921f 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -365,6 +365,16 @@ pub struct CheckedInputs { pub precommitted: PrecommittedSchedule, } +impl CheckedInputs { + /// The committed output-claim row length: the vector-commitment capacity. + /// `validate_zk_vector_commitment_setup` has already checked it is at + /// least `MAX_BLINDFOLD_GENERATORS`, so it is nonzero. + pub fn committed_row_len(&self) -> Result { + self.vc_capacity + .ok_or(VerifierError::MissingVectorCommitmentSetup) + } +} + /// Absorbs the public preamble: the verifier's protocol axes, the /// preprocessing digest, the public I/O, and the entry address. Both sides run /// it right after the proof header. diff --git a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory index 746f782baf..621fd0dbf2 100644 --- a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory @@ -126,6 +126,7 @@ jolt-prover::stages/stage3.rs::prove_stage3::.draw_challenges#0 jolt-prover::stages/stage3.rs::prove_stage3::.prove#1 jolt-prover::stages/stage4.rs::prove_stage4::.draw_challenges#0 jolt-prover::stages/stage4.rs::prove_stage4::.prove#2 +jolt-prover::stages/stage4.rs::prove_stage4::.send#1 jolt-prover::stages/stage5.rs::prove_stage5::.draw_challenges#0 jolt-prover::stages/stage5.rs::prove_stage5::.prove#1 jolt-prover::stages/stage6a.rs::prove_stage6a::.draw_challenges#0 @@ -144,7 +145,6 @@ jolt-sumcheck::recorder.rs::CommittedSumcheckRecorder<'_,F,VC,R>::finish::.finis jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::receive_full_round#0 jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::receive_compressed_round#0 jolt-verifier::proof.rs::JoltProof::header::ProofHeader::receive#0 -jolt-verifier::stages/relations.rs::receive_member_openings::receive_member_openings_except#0 jolt-verifier::stages/stage1/verify.rs::verify::.draw_challenges#2 jolt-verifier::stages/stage1/verify.rs::verify::.verify_clear#3 jolt-verifier::stages/stage1/verify.rs::verify::.verify_zk#5 @@ -161,32 +161,19 @@ jolt-verifier::stages/stage2/verify.rs::verify_product_uniskip::uniskip::verify_ jolt-verifier::stages/stage3/verify.rs::verify::.draw_challenges#0 jolt-verifier::stages/stage3/verify.rs::verify::.verify_clear#1 jolt-verifier::stages/stage3/verify.rs::verify::.verify_zk#2 -jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive_member_openings#0 -jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive_member_openings#1 jolt-verifier::stages/stage4/verify.rs::verify::.draw_challenges#0 -jolt-verifier::stages/stage4/verify.rs::verify::.receive_output_claims#3 jolt-verifier::stages/stage4/verify.rs::verify::.verify_clear_with#2 jolt-verifier::stages/stage4/verify.rs::verify::.verify_zk#4 jolt-verifier::stages/stage4/verify.rs::verify::RamValCheckStagedOpenings::receive#1 +jolt-verifier::stages/stage4/verify.rs::verify::Stage4Sumchecks::receive_output_claims#3 jolt-verifier::stages/stage5/verify.rs::verify::.draw_challenges#0 jolt-verifier::stages/stage5/verify.rs::verify::.verify_clear#1 jolt-verifier::stages/stage5/verify.rs::verify::.verify_zk#2 jolt-verifier::stages/stage6a/verify.rs::verify::.draw_challenges#0 jolt-verifier::stages/stage6a/verify.rs::verify::.verify_clear#2 jolt-verifier::stages/stage6a/verify.rs::verify::.verify_zk#1 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#0 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#10 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#2 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#3 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#4 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#5 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#6 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#7 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#8 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings#9 -jolt-verifier::stages/stage6b/verify.rs::receive_output_claims::receive_member_openings_except#1 -jolt-verifier::stages/stage6b/verify.rs::verify::.verify_clear_with#2 -jolt-verifier::stages/stage6b/verify.rs::verify::.verify_zk_with#1 +jolt-verifier::stages/stage6b/verify.rs::verify::.verify_clear#2 +jolt-verifier::stages/stage6b/verify.rs::verify::.verify_zk#1 jolt-verifier::stages/stage6b/verify.rs::verify::Stage6bDraws::draw#0 jolt-verifier::stages/stage7/verify.rs::verify::.draw_challenges#0 jolt-verifier::stages/stage7/verify.rs::verify::.verify_clear#2 diff --git a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory index baa57ba920..b498eef468 100644 --- a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory @@ -44,7 +44,6 @@ jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_serde::send_bytes#0::s jolt-dory::transcript.rs::DoryProverChannel<'a,H>::send_sigma1_responses::send_bytes#0::self.transcript.send_bytes(&compressed(response)) jolt-dory::transcript.rs::DoryVerifierChannel<'a,'p,H>::expect::receive_bytes#0::self.transcript.receive_bytes(bytes.len()) jolt-dory::transcript.rs::read::receive_bytes#0::scout.receive_bytes(T::BYTES) -jolt-prover::stages/stage4.rs::prove_stage4::send_all#1::#[cfg(not(feature="zk"))]transcript.send_all(&ram_val_check_init.staged_openings().values()) jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::commit_round::send#0::transcript.send(&commitment) jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::finish::send#0::transcript.send(&VC::commit(self.setup,row,&blinding)) jolt-sumcheck::prover.rs::prove_uniskip_clear::send#2::transcript.send(&output_claim) @@ -73,8 +72,7 @@ jolt-verifier::proof.rs::ProofHeader::send::send#1::transcript.send(&num::u64_fr jolt-verifier::proof.rs::ProofHeader::send::send#3::transcript.send(&self.trace_polynomial_order.transcript_scalar()) jolt-verifier::proof.rs::ProofHeader::send::send#4::transcript.send(&u8::from(self.untrusted_advice)) jolt-verifier::proof.rs::ProofHeader::send::send_all#2::transcript.send_all(&[self.rw_config.ram_rw_phase1_num_rounds,self.rw_config.ram_rw_phase2_num_rounds,self.rw_config.registers_rw_phase1_num_rounds,self.rw_config.registers_rw_phase2_num_rounds,self.one_hot_config.log_k_chunk,self.one_hot_config.lookups_ra_virtual_log_k_chunk,]) -jolt-verifier::stages/relations.rs::receive_member_openings_except::receive#0::transcript.receive() -jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive#2::transcript.receive() -jolt-verifier::stages/stage4/outputs.rs::Stage4Sumchecks::receive_output_claims::receive#3::transcript.receive() +jolt-verifier::stages/relations.rs::receive_member_claims::receive#0::transcript.receive() jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheckStagedOpenings::receive::receive#0::transcript.receive() +jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheckStagedOpenings::send::send_all#0::transcript.send_all(&self.opening_values()) jolt-verifier::stages/uniskip.rs::verify_clear::receive#1::transcript.receive() From 34218525221b367c1192064bc462580a19da7434 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 14:37:26 -0400 Subject: [PATCH 37/62] docs(spec): run jolt-transcript on spongefish's NARG state jolt-transcript becomes a typed layer over spongefish's ProverState and VerifierState, with spongefish codecs on the owning crates, exact challenges, squeeze-then-grind with no yolocrypto, and Aerie opening Akita inline. --- specs/jolt-transcript-spongefish-state.md | 185 ++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 specs/jolt-transcript-spongefish-state.md diff --git a/specs/jolt-transcript-spongefish-state.md b/specs/jolt-transcript-spongefish-state.md new file mode 100644 index 0000000000..91f6185e3c --- /dev/null +++ b/specs/jolt-transcript-spongefish-state.md @@ -0,0 +1,185 @@ +# Spec: run jolt-transcript on spongefish's NARG state + +| Field | Value | +| --------- | --------------------------------------------------------------- | +| Author(s) | @markosg04 | +| Created | 2026-10-05 | +| Status | proposed | +| Amends | `specs/jolt-transcript-narg.md` (Grinding and previews; Alternatives considered) | + +## Decision + +`jolt-transcript` stops reimplementing the NARG state machine and becomes a +typed layer over spongefish's `ProverState` and +`VerifierState<'a, H>`. Atoms implement spongefish's `Encoding`, +`NargSerialize`, and `NargDeserialize` in the crates that own them. Domain +separation uses spongefish's `DomainSeparator`. The explicit typing stays +where it is now: claims structs, routes, and message groups, generated per +stage. Challenges stay exactly uniform. Grinding becomes squeeze-then-grind, +so no crate needs spongefish's `yolocrypto` feature. Jolt, Akita, and Aerie +all run on the result. Aerie opens Akita inside its own PIOP transcript. + +This reverses the narg spec's rejection of spongefish's prover and verifier +state. The four reasons given there now resolve as follows: + +| Reason given | Resolution | +| --- | --- | +| `VerificationError` is a unit type | The wrapper maps it to `TranscriptError` with the message index and site, which it tracks itself. | +| Previews need `yolocrypto` | Previews are removed. Twin and replay checks compare argument strings plus one final 32-byte squeeze on each side. Grinding no longer previews. | +| Encode and serialize are separate | For byte sponges (`H::U = u8`), spongefish's blanket `NargSerialize for T: Encoding<[u8]>` makes them identical. Jolt pins byte sponges only (see Non-goals). | +| No logging hook | The wrapper logs each call with its site before delegating. | + +## Invariants + +1. Every prover message reaches the sponge only through `prover_message` and + `prover_messages*`, and every public value only through `public_message*`. + No crate reads, clones, or writes spongefish's duplex state. +2. No crate in the Jolt, Akita, or Aerie dependency graphs enables + spongefish's `yolocrypto`. CI checks this with `cargo tree -e features`. +3. Field challenges are exactly uniform. The wrapper samples them by + rejection over fixed-width `verifier_message::<[u8; N]>()` squeezes, using + `Field::random`'s contract. The small challenge set is unchanged: 16 + squeezed bytes through `CanonicalEncoding::from_challenge_bytes`. +4. The prover's private randomness comes from spongefish's transcript-bound + `ProverState::rng()`. Protocol code never seeds its own RNG for blindings. +5. Message shapes stay positional. No message carries a length the public + parameters already fix. + +## Mechanism + +**Crates.** `jolt-transcript` depends on `spongefish` with +`default-features = false` plus each sponge's feature. It keeps +`ProverTranscript`, `VerifierTranscript<'a, H>`, `Channel`, `Sponge`, +`SiteId`, `TranscriptError`, and `ProtocolId`, and adds `Fork`. `Duplex` and +`Preview` are deleted. `CanonicalBytes` and `CanonicalDecode` stay as each +type's fixed-width codec, because the wrapper needs `NUM_BYTES` to bound a read +before it allocates and to log byte ranges. They gain spongefish's `Encoding` +and `NargDeserialize` as supertraits. Each owning crate implements those two +traits through the shared `jolt_field::narg::{encode, deserialize}`: canonical +little endian, with non-canonical encodings rejected. spongefish implements +`NargDeserialize` for `u32` and byte arrays only, so `u8`/`u64` header fields +travel as byte arrays. + +**Construction.** `ProtocolId` stays the 64-byte protocol tag (name plus +sponge id). A transcript starts from +`DomainSeparator::new(protocol_id).session(B(session)).instance(B(instance))` +followed by `to_prover(H::default())` or `to_verifier(H::default(), narg)`. +`B` is a length-framed byte string. Jolt passes an empty instance and keeps +binding its preamble through public messages. Akita and Aerie pass their +instance digests. + +**Operations.** + +| `jolt-transcript` | spongefish | +| --- | --- | +| `send` / `send_all`, `receive` / `receive_n` | `prover_message`, `prover_messages`, `prover_messages_vec` | +| `send_bytes(len known)` / `receive_bytes(len)` | `prover_messages::` / `prover_messages_vec::(len)` | +| `send_bounded_bytes` / `receive_bounded_bytes` | `u32` LE length as a prover message, then the bytes; the verifier enforces the maximum before reading | +| `public*` | `public_message*` | +| `challenge::()` | rejection loop over `verifier_message::<[u8; N]>()` | +| `challenge_small`, `challenge_bytes::()` | `verifier_message::<[u8; N]>()` | +| `finish` | `narg_string()` (prover); `check_eof()` (verifier) | + +**Seeded forks.** `Fork::new(seed, counter)` is a fresh `H` that absorbed +the tag `jolt-transcript/fork/v1`, a 32-byte seed squeezed from the live +transcript, and `LE32(counter)`. Every prover search runs on forks: + +1. Squeeze a 32-byte seed `s` from the transcript. +2. The prover tries counters `c` upward from zero, each on `Fork(s, c)`, off + the transcript. +3. The prover sends the accepted `c` as a `u32` prover message. The verifier + receives it, range-checks it, and rebuilds `Fork(s, c)`. + +Proof of work with difficulty `g > 0` searches `c` in `[0, 2^(g+7))` and +accepts when the fork's first 32 squeezed bytes have `g` leading zero bits, +least significant bit first. The protected challenge is drawn from the +transcript after the nonce. Akita's fold-response search (Fiat-Shamir with +aborts) draws each candidate's fold challenges from the fork and accepts the +first counter whose response meets its bounds. Both roles draw the accepted +challenges from the same fork. The transcript sees one squeeze and one 4-byte +message per search, whatever the search cost. The seed binds the full +prechallenge state, and the counter is in the argument string. + +**Replay checkpoint.** The ZK prover replays its argument string through the +verifier's stage spine before BlindFold. With no previews, every ZK transcript +squeezes a 32-byte checkpoint after the stage spine. The prover compares its +own checkpoint with the replay's. **Parse-ahead.** dory-pcs needs its whole +proof struct before verifying, so `VerifierTranscript::unread()` exposes the +unread bytes for parsing. The live transcript still receives and absorbs every +one of them. + +**Sites and logging.** `Channel::site` and the event log stay in the wrapper. +Byte ranges come from the known atom widths. Logging stays feature-gated. + +**Cloning.** `ProverTranscript` is not `Clone`, matching `ProverState`. +Fixtures that cloned a mid-proof transcript rebuild it from a closure. + +## Non-goals + +- Algebraic sponges (`H::U` a field element). The encoding traits are + implemented over `[u8]` only. Supporting a field-native Poseidon later means + adding `Encoding<[Fr]>` impls. It does not change the typed layer. +- Changing message order or adding labels. Aerie's event headers stay an + Aerie-level framing, absorbed as public messages. + +## Alternatives considered + +- **Statistical challenges via spongefish's field `Decoding`** (modulus bytes + plus 32, then reduce). Rejected. Exact sampling works on spongefish state + through looped fixed-width squeezes. It keeps Aerie's frozen sampler, and it + keeps the ledgers' rejection-sampling query accounting. +- **Remove `CanonicalBytes` and bound on spongefish's traits directly.** + Rejected. The wrapper needs a fixed width per atom to check a read against + the remaining proof before allocating. +- **Keep the preview-based grinding transition with `yolocrypto` confined to + `jolt-transcript`.** Rejected. Cargo unifies features, so the duplex state + would become public to every crate in the graph. +- **`spongefish-pow` (Blake3 or Keccak) for the predicate.** Rejected. It adds + a second hash function to the security assumptions. For a Poseidon + transcript it also makes in-circuit grind verification expensive. The + predicate hashes with the transcript's own sponge type. + +## Compatibility + +Every proof changes. The domain-separator layout, the grinding transition, +and the nonce encoding (`u32` LE instead of LEB128) all differ. Fixtures, +known-answer tests, the FS census, and Akita's and Aerie's grinding tables are +regenerated. Akita's `specs/transcript-grinding.md` and Aerie's +`specs/falcon.md` §10.1–10.2 are amended in their own PRs. + +## Execution + +Three PRs, landed together. Akita and Aerie pin the Jolt PR's revision. + +1. **Jolt:** this spec. The spongefish codecs in `jolt-field` and + `jolt-crypto`, the transcript core, the grinding transition, preview + removal, then fixture and census regeneration. +2. **Akita:** port its proof-of-work and fold-response searches to forks + (`PreviewFoldDraw` becomes a fork draw that both roles use), and amend + `transcript-grinding.md`. +3. **Aerie:** delete its `akita-transcript` use. Run its PIOP stream on + `jolt-transcript`, keeping its event headers as public messages. Open Akita + inline instead of exporting `pcs_session`. Amend `falcon.md` §10.1–10.2, + §12.3's export description, and the grinding description in the ledgers. + +## Verification + +- Clear, ZK, and Akita e2e; the verifier fixtures; tamper and FS sweeps; the + census re-blessed and reviewed. +- Per-sponge known answers regenerated, plus a test that the exact sampler + rejects and resamples at the boundary. +- Grinding: tests for accept, reject, an out-of-range nonce, and a wrong seed. +- `cargo tree -e features` contains no `yolocrypto` in any of the three + workspaces. +- Aerie: the end-to-end, adversarial, and specification-script suites its + CONTRIBUTING.md names. + +## Unverified + +- Aerie's PCS-extraction argument (`falcon-pcs-extraction.md` §2) leaves the + random-oracle composition as an open upstream obligation. It does not use + the separation of the PIOP and PCS streams. Inlining replaces two oracles + chained by a 32-byte export with one oracle, so it adds no assumption. Only + `falcon.md` §10 and §12.3 describe the export and need amending. +- Prover cost of squeeze-then-grind versus the current transition. Both hash + one block per candidate, so parity is expected. From d3264f8f12fc5bb13d6b22115c583a00ddcfbc10 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 15:31:08 -0400 Subject: [PATCH 38/62] feat(field)!: make transcript atoms spongefish codecs CanonicalBytes and CanonicalDecode gain spongefish's Encoding and NargDeserialize as supertraits, implemented by every atom's owning crate through jolt_field::narg, so the fixed-width canonical codec is exactly what spongefish absorbs and reads. spongefish deserializes only u32 and byte arrays among primitives, so the u8/u64 proof-header fields and the mock opening length travel as byte arrays (same wire bytes). --- crates/jolt-akita/Cargo.toml | 1 + crates/jolt-akita/src/adapters.rs | 27 ++++++++- crates/jolt-crypto/Cargo.toml | 1 + crates/jolt-crypto/src/commitment.rs | 12 ++++ crates/jolt-crypto/src/ec/bn254/gt.rs | 12 ++++ crates/jolt-crypto/src/ec/bn254/mod.rs | 12 ++++ crates/jolt-dory/Cargo.toml | 1 + crates/jolt-dory/src/types.rs | 12 ++++ crates/jolt-field/Cargo.toml | 4 ++ crates/jolt-field/src/algebra.rs | 58 ++++++++++++++++--- crates/jolt-field/src/bn254/mod.rs | 12 ++++ crates/jolt-field/src/lib.rs | 1 + crates/jolt-field/src/solinas/ext.rs | 36 ++++++++++++ crates/jolt-field/src/solinas/fp128.rs | 12 ++++ crates/jolt-field/src/solinas/word.rs | 12 ++++ crates/jolt-field/tests/spine.rs | 12 ++++ crates/jolt-openings/Cargo.toml | 1 + crates/jolt-openings/tests/support/mock.rs | 16 ++++- crates/jolt-sumcheck/Cargo.toml | 1 + .../tests/external-consumer/Cargo.toml | 2 + .../external-consumer/tests/external_field.rs | 16 ++++- .../jolt-sumcheck/tests/mersenne61_compat.rs | 12 ++++ crates/jolt-verifier/src/proof.rs | 36 ++++++++---- zklean-extractor/Cargo.toml | 1 + zklean-extractor/src/mle_ast.rs | 12 ++++ 25 files changed, 296 insertions(+), 26 deletions(-) diff --git a/crates/jolt-akita/Cargo.toml b/crates/jolt-akita/Cargo.toml index 3399080479..4b88e5f2d6 100644 --- a/crates/jolt-akita/Cargo.toml +++ b/crates/jolt-akita/Cargo.toml @@ -14,6 +14,7 @@ field-inline = [] profiling = [] [dependencies] +spongefish = { workspace = true, features = ["sha3"] } # Jolt keeps its own catalogs for Jolt-specific trace shapes; Akita's shipped # catalogs do not cover this adapter's complete production and test grids. akita-algebra.workspace = true diff --git a/crates/jolt-akita/src/adapters.rs b/crates/jolt-akita/src/adapters.rs index 6bdc52fa7d..cd2977aca5 100644 --- a/crates/jolt-akita/src/adapters.rs +++ b/crates/jolt-akita/src/adapters.rs @@ -1037,6 +1037,12 @@ impl CanonicalBytes for CommitmentHeader { } } +impl ::spongefish::Encoding<[u8]> for CommitmentHeader { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for CommitmentHeader { /// Accepts only headers [`AkitaScheme`](crate::AkitaScheme) can produce: a /// known flavor, word values that fit `usize`, and the flavor's one-hot @@ -1048,7 +1054,8 @@ impl CanonicalDecode for CommitmentHeader { let (&tag, rest) = bytes.split_first()?; let (digest, words) = rest.split_first_chunk::<{ Self::DIGEST_BYTES }>()?; let mut words = words.chunks_exact(u64::NUM_BYTES).map(|word| { - u64::from_bytes_le_checked(word).and_then(|word| usize::try_from(word).ok()) + let word = u64::from_le_bytes(word.try_into().ok()?); + usize::try_from(word).ok() }); let mut next = || words.next().flatten(); let header = Self { @@ -1067,6 +1074,12 @@ impl CanonicalDecode for CommitmentHeader { } } +impl ::spongefish::NargDeserialize for CommitmentHeader { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + ::jolt_field::narg::deserialize(buf) + } +} + /// The transparent stand-in for a hiding evaluation commitment: the /// evaluation's canonical encoding. Akita has no ZK opening, so nothing /// verifies against it. @@ -1092,12 +1105,24 @@ impl CanonicalBytes for AkitaHidingCommitment { } } +impl ::spongefish::Encoding<[u8]> for AkitaHidingCommitment { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for AkitaHidingCommitment { fn from_bytes_le_checked(bytes: &[u8]) -> Option { AkitaField::from_bytes_le_checked(bytes).map(Self::new) } } +impl ::spongefish::NargDeserialize for AkitaHidingCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + ::jolt_field::narg::deserialize(buf) + } +} + #[derive(Clone, Debug, Default)] pub struct AkitaProverHint { pub(crate) commitment: AkitaCommitment, diff --git a/crates/jolt-crypto/Cargo.toml b/crates/jolt-crypto/Cargo.toml index a1e92313d4..cc2e1ad2a9 100644 --- a/crates/jolt-crypto/Cargo.toml +++ b/crates/jolt-crypto/Cargo.toml @@ -34,6 +34,7 @@ bn254 = [ parallel = ["dep:rayon", "jolt-poly/parallel"] [dependencies] +spongefish = { workspace = true, features = ["sha3"] } jolt-field = { path = "../jolt-field", default-features = false } jolt-poly = { path = "../jolt-poly", default-features = false } serde = { workspace = true, features = ["derive", "alloc"] } diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index f1b5d8558b..169832fe56 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -55,12 +55,24 @@ impl CanonicalBytes for NoCommitment { fn to_bytes_le(&self, _out: &mut [u8]) {} } +impl ::spongefish::Encoding<[u8]> for NoCommitment { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for NoCommitment { fn from_bytes_le_checked(bytes: &[u8]) -> Option { bytes.is_empty().then_some(Self) } } +impl ::spongefish::NargDeserialize for NoCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + ::jolt_field::narg::deserialize(buf) + } +} + impl HomomorphicCommitment for NoCommitment { fn add(_c1: &Self, _c2: &Self) -> Self { Self diff --git a/crates/jolt-crypto/src/ec/bn254/gt.rs b/crates/jolt-crypto/src/ec/bn254/gt.rs index c895db06ab..79aa0aef8e 100644 --- a/crates/jolt-crypto/src/ec/bn254/gt.rs +++ b/crates/jolt-crypto/src/ec/bn254/gt.rs @@ -256,12 +256,24 @@ impl CanonicalBytes for Bn254GT { } } +impl ::spongefish::Encoding<[u8]> for Bn254GT { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for Bn254GT { fn from_bytes_le_checked(bytes: &[u8]) -> Option { Self::decode_compressed(bytes).ok() } } +impl ::spongefish::NargDeserialize for Bn254GT { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + ::jolt_field::narg::deserialize(buf) + } +} + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "tests may fail loudly")] mod tests { diff --git a/crates/jolt-crypto/src/ec/bn254/mod.rs b/crates/jolt-crypto/src/ec/bn254/mod.rs index 2d236f29d9..ce3a79ad0e 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -186,12 +186,24 @@ macro_rules! impl_jolt_group_wrapper { } } + impl ::spongefish::Encoding<[u8]> for $wrapper { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } + } + impl ::jolt_field::CanonicalDecode for $wrapper { fn from_bytes_le_checked(bytes: &[u8]) -> Option { Self::decode_compressed(bytes).ok() } } + impl ::spongefish::NargDeserialize for $wrapper { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + ::jolt_field::narg::deserialize(buf) + } + } + impl $crate::JoltGroup for $wrapper { #[inline(always)] fn identity() -> Self { diff --git a/crates/jolt-dory/Cargo.toml b/crates/jolt-dory/Cargo.toml index e4fa86ff7b..93543ef875 100644 --- a/crates/jolt-dory/Cargo.toml +++ b/crates/jolt-dory/Cargo.toml @@ -9,6 +9,7 @@ description = "Dory commitment scheme implementation for the Jolt zkVM" workspace = true [dependencies] +spongefish = { workspace = true, features = ["sha3"] } jolt-crypto = { workspace = true } jolt-field = { workspace = true } jolt-openings = { workspace = true } diff --git a/crates/jolt-dory/src/types.rs b/crates/jolt-dory/src/types.rs index 0822867960..468454f903 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -47,12 +47,24 @@ impl CanonicalBytes for DoryCommitment { } } +impl ::spongefish::Encoding<[u8]> for DoryCommitment { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for DoryCommitment { fn from_bytes_le_checked(bytes: &[u8]) -> Option { Bn254GT::from_bytes_le_checked(bytes).map(Self) } } +impl ::spongefish::NargDeserialize for DoryCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + ::jolt_field::narg::deserialize(buf) + } +} + impl HomomorphicCommitment for DoryCommitment { #[inline] fn add(c1: &Self, c2: &Self) -> Self { diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index 4a69964844..47c27c8652 100644 --- a/crates/jolt-field/Cargo.toml +++ b/crates/jolt-field/Cargo.toml @@ -25,6 +25,9 @@ bytemuck = { workspace = true, optional = true } rand_core = { workspace = true } rayon = { workspace = true, optional = true } thiserror = { workspace = true } +# The transcript codec traits atoms implement. spongefish 0.7 references its +# sha3 `StdHash` even when another sponge is selected. +spongefish = { workspace = true, features = ["sha3"] } [features] default = ["bn254"] @@ -40,6 +43,7 @@ fp128-proof-linkage = ["asm"] fp64-proof-linkage = [] [dev-dependencies] +spongefish = { workspace = true, features = ["sha3"] } # The spine test's oracle field must satisfy `JoltField`, which requires # `Allocative` under the `allocative` feature; integration tests do not see # the optional dependency above. diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index b49fa27e73..04dbee635e 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -264,7 +264,9 @@ pub trait PseudoMersenne: Field + CanonicalEncoding { /// produce equal bytes, distinct values produce distinct bytes. /// - [`to_bytes_le`](Self::to_bytes_le) always writes exactly /// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. -pub trait CanonicalBytes { +/// - The spongefish [`Encoding`](spongefish::Encoding) is this encoding; +/// implement it with [`narg::encode`]. +pub trait CanonicalBytes: spongefish::Encoding<[u8]> { /// Byte length of the fixed-size canonical encoding. const NUM_BYTES: usize; @@ -290,14 +292,52 @@ pub trait CanonicalBytes { /// - `from_bytes_le_checked(&x.to_bytes_le_vec()) == Some(x)` for every `x`. /// - Every other input, including any input whose length is not /// [`CanonicalBytes::NUM_BYTES`], decodes to `None`. -pub trait CanonicalDecode: CanonicalBytes + Sized { +/// - The spongefish [`NargDeserialize`](spongefish::NargDeserialize) reads +/// exactly this decoding; implement it with [`narg::deserialize`]. +pub trait CanonicalDecode: CanonicalBytes + spongefish::NargDeserialize + Sized { /// Decodes exactly [`CanonicalBytes::NUM_BYTES`] canonical bytes; /// `None` on wrong length or a non-canonical value. fn from_bytes_le_checked(bytes: &[u8]) -> Option; } +/// The spongefish codec of a fixed-width canonical type, shared by every +/// implementing crate so each type's `Encoding` and `NargDeserialize` are its +/// [`CanonicalBytes`]/[`CanonicalDecode`] codec and nothing else. +pub mod narg { + use spongefish::{VerificationError, VerificationResult}; + + use super::{CanonicalBytes, CanonicalDecode}; + + /// The `Encoding::encode` body: the canonical little-endian bytes. + #[inline] + pub fn encode(value: &T) -> Vec { + let mut out = vec![0u8; T::NUM_BYTES]; + value.to_bytes_le(&mut out); + out + } + + /// The `NargDeserialize::deserialize_from_narg` body: decodes exactly + /// `NUM_BYTES` canonical bytes and advances `buf` only on success. + /// + /// # Errors + /// + /// When `buf` is shorter than `NUM_BYTES` or the bytes are not canonical. + #[inline] + pub fn deserialize(buf: &mut &[u8]) -> VerificationResult { + let (head, tail) = buf + .split_at_checked(T::NUM_BYTES) + .ok_or(VerificationError)?; + let value = T::from_bytes_le_checked(head).ok_or(VerificationError)?; + *buf = tail; + Ok(value) + } +} + /// Fixed-width little-endian codecs for unsigned integers, so protocol /// counters and nonces travel as transcript atoms like field elements do. +/// spongefish owns their `Encoding`; only `u32` also has its +/// `NargDeserialize`, so only `u32` is a [`CanonicalDecode`] atom. Wider +/// header fields travel inside their message type's own codec. macro_rules! impl_uint_codec { ($($t:ty),*) => {$( impl CanonicalBytes for $t { @@ -308,18 +348,18 @@ macro_rules! impl_uint_codec { out.copy_from_slice(&self.to_le_bytes()); } } - - impl CanonicalDecode for $t { - #[inline] - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - bytes.try_into().ok().map(<$t>::from_le_bytes) - } - } )*}; } impl_uint_codec!(u8, u16, u32, u64, u128); +impl CanonicalDecode for u32 { + #[inline] + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + bytes.try_into().ok().map(Self::from_le_bytes) + } +} + /// Raw bytes are their own canonical encoding. impl CanonicalBytes for [u8; N] { const NUM_BYTES: usize = N; diff --git a/crates/jolt-field/src/bn254/mod.rs b/crates/jolt-field/src/bn254/mod.rs index ec64e20177..c50b8bf0d9 100644 --- a/crates/jolt-field/src/bn254/mod.rs +++ b/crates/jolt-field/src/bn254/mod.rs @@ -126,6 +126,12 @@ macro_rules! wrap_bn254 { } } + impl ::spongefish::Encoding<[u8]> for $ty { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } + } + impl CanonicalDecode for $ty { #[inline] fn from_bytes_le_checked(bytes: &[u8]) -> Option { @@ -137,6 +143,12 @@ macro_rules! wrap_bn254 { } } + impl ::spongefish::NargDeserialize for $ty { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + crate::narg::deserialize(buf) + } + } + impl CanonicalEncoding for $ty { const MODULUS_BITS: u32 = 254; diff --git a/crates/jolt-field/src/lib.rs b/crates/jolt-field/src/lib.rs index cbe8cfd771..b23f742b2c 100644 --- a/crates/jolt-field/src/lib.rs +++ b/crates/jolt-field/src/lib.rs @@ -105,6 +105,7 @@ pub mod signed; pub mod solinas; mod unreduced; +pub use algebra::narg; pub use algebra::{ Accumulator, AdditiveGroup, CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, JoltField, MaybeAllocative, NaiveAccumulator, PseudoMersenne, Ring, WithAccumulator, diff --git a/crates/jolt-field/src/solinas/ext.rs b/crates/jolt-field/src/solinas/ext.rs index 18c430d077..ea32e9b3c5 100644 --- a/crates/jolt-field/src/solinas/ext.rs +++ b/crates/jolt-field/src/solinas/ext.rs @@ -142,6 +142,12 @@ impl> CanonicalBytes for FpExt2> ::spongefish::Encoding<[u8]> for FpExt2 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + impl> CanonicalDecode for FpExt2 { fn from_bytes_le_checked(bytes: &[u8]) -> Option { let [c0, c1] = decode_coeffs(bytes)?; @@ -149,6 +155,12 @@ impl> CanonicalDecode for FpExt2> ::spongefish::NargDeserialize for FpExt2 { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + crate::narg::deserialize(buf) + } +} + crate::impl_ring_ops!(impl[F: Field, C: Ext2Config] FpExt2 { add(a, b): FpExt2::new(a.coeffs[0] + b.coeffs[0], a.coeffs[1] + b.coeffs[1]), sub(a, b): FpExt2::new(a.coeffs[0] - b.coeffs[0], a.coeffs[1] - b.coeffs[1]), @@ -311,12 +323,24 @@ impl CanonicalBytes for FpExt4 { } } +impl ::spongefish::Encoding<[u8]> for FpExt4 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + impl CanonicalDecode for FpExt4 { fn from_bytes_le_checked(bytes: &[u8]) -> Option { decode_coeffs(bytes).map(Self::new) } } +impl ::spongefish::NargDeserialize for FpExt4 { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + crate::narg::deserialize(buf) + } +} + crate::impl_ring_ops!(impl[F: PseudoMersenne] FpExt4 { add(a, b): FpExt4::new(std::array::from_fn(|i| a.coeffs[i] + b.coeffs[i])), sub(a, b): FpExt4::new(std::array::from_fn(|i| a.coeffs[i] - b.coeffs[i])), @@ -455,12 +479,24 @@ impl CanonicalBytes for FpExt8 { } } +impl ::spongefish::Encoding<[u8]> for FpExt8 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + impl CanonicalDecode for FpExt8 { fn from_bytes_le_checked(bytes: &[u8]) -> Option { decode_coeffs(bytes).map(Self::new) } } +impl ::spongefish::NargDeserialize for FpExt8 { + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + crate::narg::deserialize(buf) + } +} + /// Decodes `N` canonical base coefficients laid out in basis order, rejecting /// any wrong length or non-canonical coefficient. fn decode_coeffs(bytes: &[u8]) -> Option<[F; N]> { diff --git a/crates/jolt-field/src/solinas/fp128.rs b/crates/jolt-field/src/solinas/fp128.rs index 9d7277220b..d820af6265 100644 --- a/crates/jolt-field/src/solinas/fp128.rs +++ b/crates/jolt-field/src/solinas/fp128.rs @@ -1132,6 +1132,12 @@ impl CanonicalBytes for Fp128

{ } } +impl ::spongefish::Encoding<[u8]> for Fp128

{ + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + impl CanonicalDecode for Fp128

{ #[inline] fn from_bytes_le_checked(bytes: &[u8]) -> Option { @@ -1140,6 +1146,12 @@ impl CanonicalDecode for Fp128

{ } } +impl ::spongefish::NargDeserialize for Fp128

{ + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + crate::narg::deserialize(buf) + } +} + impl CanonicalEncoding for Fp128

{ // C < 2^32 implies p > 2^127, so the modulus is exactly 128 bits. const MODULUS_BITS: u32 = 128; diff --git a/crates/jolt-field/src/solinas/word.rs b/crates/jolt-field/src/solinas/word.rs index 085fd3ca7c..9107df1566 100644 --- a/crates/jolt-field/src/solinas/word.rs +++ b/crates/jolt-field/src/solinas/word.rs @@ -311,6 +311,12 @@ macro_rules! define_solinas_prime { } } + impl ::spongefish::Encoding<[u8]> for $name

{ + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } + } + impl CanonicalDecode for $name

{ #[inline] fn from_bytes_le_checked(bytes: &[u8]) -> Option { @@ -319,6 +325,12 @@ macro_rules! define_solinas_prime { } } + impl ::spongefish::NargDeserialize for $name

{ + fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { + crate::narg::deserialize(buf) + } + } + impl CanonicalEncoding for $name

{ const MODULUS_BITS: u32 = Self::BITS; diff --git a/crates/jolt-field/tests/spine.rs b/crates/jolt-field/tests/spine.rs index 106aec726e..c32727770d 100644 --- a/crates/jolt-field/tests/spine.rs +++ b/crates/jolt-field/tests/spine.rs @@ -88,6 +88,12 @@ impl CanonicalBytes for M61 { } } +impl spongefish::Encoding<[u8]> for M61 { + fn encode(&self) -> impl AsRef<[u8]> { + jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for M61 { fn from_bytes_le_checked(bytes: &[u8]) -> Option { let arr: [u8; 8] = bytes.try_into().ok()?; @@ -95,6 +101,12 @@ impl CanonicalDecode for M61 { } } +impl spongefish::NargDeserialize for M61 { + fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl CanonicalEncoding for M61 { const MODULUS_BITS: u32 = 61; fn from_bytes_le_reduced(bytes: &[u8]) -> Self { diff --git a/crates/jolt-openings/Cargo.toml b/crates/jolt-openings/Cargo.toml index cedcf1206e..485067b90c 100644 --- a/crates/jolt-openings/Cargo.toml +++ b/crates/jolt-openings/Cargo.toml @@ -23,6 +23,7 @@ bn254 = ["jolt-crypto/bn254", "jolt-field/bn254"] parallel = ["jolt-crypto/parallel", "jolt-poly/parallel"] [dev-dependencies] +spongefish = { workspace = true, features = ["sha3"] } jolt-dory.workspace = true jolt-crypto = { workspace = true, features = ["bn254"] } jolt-transcript = { workspace = true, features = [ diff --git a/crates/jolt-openings/tests/support/mock.rs b/crates/jolt-openings/tests/support/mock.rs index 1263554618..947892df71 100644 --- a/crates/jolt-openings/tests/support/mock.rs +++ b/crates/jolt-openings/tests/support/mock.rs @@ -30,14 +30,14 @@ fn send_evaluations( evaluations: &[F], transcript: &mut ProverTranscript, ) { - transcript.send(&(evaluations.len() as u64)); + transcript.send(&(evaluations.len() as u64).to_le_bytes()); transcript.send_all(evaluations); } fn receive_evaluations( transcript: &mut VerifierTranscript<'_, H>, ) -> Result, OpeningsError> { - let len = usize::try_from(transcript.receive::()?) + let len = usize::try_from(u64::from_le_bytes(transcript.receive()?)) .map_err(|_| OpeningsError::VerificationFailed)?; Ok(transcript.receive_n(len)?) } @@ -172,12 +172,24 @@ impl CanonicalBytes for MockHidingCommitment { } } +impl spongefish::Encoding<[u8]> for MockHidingCommitment { + fn encode(&self) -> impl AsRef<[u8]> { + jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for MockHidingCommitment { fn from_bytes_le_checked(bytes: &[u8]) -> Option { F::from_bytes_le_checked(bytes).map(|eval| Self { eval }) } } +impl spongefish::NargDeserialize for MockHidingCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl ZkOpeningScheme for MockCommitmentScheme where F: JoltField + Serialize + DeserializeOwned, diff --git a/crates/jolt-sumcheck/Cargo.toml b/crates/jolt-sumcheck/Cargo.toml index f087fccb33..0a16959010 100644 --- a/crates/jolt-sumcheck/Cargo.toml +++ b/crates/jolt-sumcheck/Cargo.toml @@ -34,6 +34,7 @@ tracing.workspace = true thiserror.workspace = true [dev-dependencies] +spongefish = { workspace = true, features = ["sha3"] } bincode = { workspace = true } jolt-crypto = { workspace = true, features = ["bn254"] } jolt-field = { workspace = true, features = ["bn254", "solinas"] } diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml index 1b73d98c36..be3a285ce6 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml @@ -11,6 +11,8 @@ ignored = ["jolt-field", "jolt-poly", "jolt-sumcheck", "jolt-transcript"] [workspace] [dependencies] +# An external field implements the spongefish codec traits the transcript reads atoms through. +spongefish = { git = "https://github.com/arkworks-rs/spongefish", rev = "ef9741346a150039427d9e0c02d6b2d73e93ec81", default-features = false, features = ["sha3"] } jolt-field = { path = "../../../jolt-field", default-features = false, features = [ "solinas", ] } diff --git a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs index a66199d36a..69a688c749 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs +++ b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs @@ -181,12 +181,24 @@ impl CanonicalBytes for ExternalField { } } +impl spongefish::Encoding<[u8]> for ExternalField { + fn encode(&self) -> impl AsRef<[u8]> { + jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for ExternalField { fn from_bytes_le_checked(bytes: &[u8]) -> Option { Prime64Offset59::from_bytes_le_checked(bytes).map(Self) } } +impl spongefish::NargDeserialize for ExternalField { + fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl CanonicalEncoding for ExternalField { const MODULUS_BITS: u32 = Prime64Offset59::MODULUS_BITS; @@ -284,7 +296,7 @@ fn external_field_runs_stock_clear_prover_and_verifier() { recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - let prover_state: [u8; 32] = prover_transcript.preview().squeeze(); + let prover_state: [u8; 32] = prover_transcript.challenge_bytes::<32>(); let proof = prover_transcript.finish(); let mut verifier_transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, &proof); @@ -296,7 +308,7 @@ fn external_field_runs_stock_clear_prover_and_verifier() { ) .unwrap(); let opening_claims: Vec = verifier_transcript.receive_n(1).unwrap(); - assert_eq!(verifier_transcript.preview().squeeze::<32>(), prover_state); + assert_eq!(verifier_transcript.challenge_bytes::<32>(), prover_state); verifier_transcript.finish().unwrap(); let point = reduced.point.as_slice()[0]; diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index eba3e91826..1cde0ad35b 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -250,6 +250,12 @@ impl CanonicalBytes for Mersenne61 { } } +impl spongefish::Encoding<[u8]> for Mersenne61 { + fn encode(&self) -> impl AsRef<[u8]> { + jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for Mersenne61 { fn from_bytes_le_checked(bytes: &[u8]) -> Option { let arr: [u8; 8] = bytes.try_into().ok()?; @@ -257,6 +263,12 @@ impl CanonicalDecode for Mersenne61 { } } +impl spongefish::NargDeserialize for Mersenne61 { + fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl CanonicalEncoding for Mersenne61 { const MODULUS_BITS: u32 = 61; diff --git a/crates/jolt-verifier/src/proof.rs b/crates/jolt-verifier/src/proof.rs index c043a0dc20..87b8fcd395 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -62,8 +62,8 @@ pub struct ProofHeader { impl ProofHeader { pub fn send(&self, transcript: &mut ProverTranscript) { - transcript.send(&num::u64_from_usize(self.trace_length)); - transcript.send(&num::u64_from_usize(self.ram_K)); + transcript.send(&num::u64_from_usize(self.trace_length).to_le_bytes()); + transcript.send(&num::u64_from_usize(self.ram_K).to_le_bytes()); transcript.send_all(&[ self.rw_config.ram_rw_phase1_num_rounds, self.rw_config.ram_rw_phase2_num_rounds, @@ -72,8 +72,13 @@ impl ProofHeader { self.one_hot_config.log_k_chunk, self.one_hot_config.lookups_ra_virtual_log_k_chunk, ]); - transcript.send(&self.trace_polynomial_order.transcript_scalar()); - transcript.send(&u8::from(self.untrusted_advice)); + transcript.send( + &self + .trace_polynomial_order + .transcript_scalar() + .to_le_bytes(), + ); + transcript.send(&[u8::from(self.untrusted_advice)]); } /// Reads the header, rejecting values outside each field's encoding. The @@ -82,19 +87,19 @@ impl ProofHeader { pub fn receive( transcript: &mut VerifierTranscript<'_, H>, ) -> Result { - let trace_length = usize_field(transcript.receive::()?, "trace_length")?; - let ram_k = usize_field(transcript.receive::()?, "ram_K")?; + let trace_length = usize_field(receive_u64(transcript)?, "trace_length")?; + let ram_k = usize_field(receive_u64(transcript)?, "ram_K")?; let [ram_phase1, ram_phase2, registers_phase1, registers_phase2, log_k_chunk, lookups_chunk]: [u8; 6] = transcript.receive()?; - let trace_polynomial_order = TracePolynomialOrder::from_transcript_scalar( - transcript.receive::()?, - ) + let trace_polynomial_order = TracePolynomialOrder::from_transcript_scalar(receive_u64( + transcript, + )?) .ok_or(VerifierError::MalformedProofHeader { field: "trace_polynomial_order", })?; - let untrusted_advice = match transcript.receive::()? { - 0 => false, - 1 => true, + let untrusted_advice = match transcript.receive::<[u8; 1]>()? { + [0] => false, + [1] => true, _ => { return Err(VerifierError::MalformedProofHeader { field: "untrusted_advice", @@ -120,6 +125,13 @@ impl ProofHeader { } } +/// A `u64` header field, carried as its eight little-endian bytes. +fn receive_u64( + transcript: &mut VerifierTranscript<'_, H>, +) -> Result { + Ok(u64::from_le_bytes(transcript.receive()?)) +} + fn usize_field(value: u64, field: &'static str) -> Result { usize::try_from(value).map_err(|_| VerifierError::MalformedProofHeader { field }) } diff --git a/zklean-extractor/Cargo.toml b/zklean-extractor/Cargo.toml index 80cb84ba0b..fd86a9f4f1 100644 --- a/zklean-extractor/Cargo.toml +++ b/zklean-extractor/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" build-fs-tree = "0.8.1" [dependencies] +spongefish = { workspace = true, features = ["sha3"] } ark-std.workspace = true ark-serialize.workspace = true ark-ff.workspace = true diff --git a/zklean-extractor/src/mle_ast.rs b/zklean-extractor/src/mle_ast.rs index 4478fae8c8..068e67f6aa 100644 --- a/zklean-extractor/src/mle_ast.rs +++ b/zklean-extractor/src/mle_ast.rs @@ -1411,6 +1411,12 @@ impl CanonicalBytes for MleAst { } } +impl spongefish::Encoding<[u8]> for MleAst { + fn encode(&self) -> impl AsRef<[u8]> { + jolt_field::narg::encode(self) + } +} + impl CanonicalDecode for MleAst { fn from_bytes_le_checked(bytes: &[u8]) -> Option { if let Some(challenge) = take_pending_challenge() { @@ -1434,6 +1440,12 @@ impl CanonicalDecode for MleAst { } } +impl spongefish::NargDeserialize for MleAst { + fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl CanonicalEncoding for MleAst { const MODULUS_BITS: u32 = 254; From c93d6f0899661b9f1ff907bf3dea30386aaf59b6 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 15:31:35 -0400 Subject: [PATCH 39/62] refactor(transcript)!: run on spongefish's prover and verifier state ProverTranscript and VerifierTranscript are typed layers over spongefish's ProverState and VerifierState: messages go through prover_message, public values through public_message, and challenges stay exactly uniform by rejection over fixed-width verifier_message squeezes. Duplex and Preview are deleted; no code reads or copies spongefish's sponge state, and CI rejects the yolocrypto feature. Searches run on seeded forks: proof of work squeezes a seed, tries nonces on forks, and sends the winner as a canonical LEB128 Nonce message. Twin and replay checks compare one final squeeze; the ZK prover's replay check uses an explicit 32-byte checkpoint every ZK transcript draws after the stage spine. dory-pcs parses ahead over VerifierTranscript::unread before the live transcript receives the same bytes. Transcripts are no longer Clone; fixtures rebuild them. --- .github/workflows/rust.yml | 2 + crates/jolt-akita/tests/grouped_capacity.rs | 14 +- crates/jolt-blindfold/src/verify.rs | 4 +- crates/jolt-blindfold/tests/support/mod.rs | 87 ++++++--- crates/jolt-dory/src/scheme.rs | 4 +- crates/jolt-dory/src/transcript.rs | 65 ++++--- crates/jolt-kernels/src/reference/naive.rs | 8 +- .../dory_homomorphic_batch_must_reject.rs | 8 +- .../jolt-openings/tests/homomorphic_dory.rs | 8 +- crates/jolt-openings/tests/schemes.rs | 8 +- crates/jolt-openings/tests/support/common.rs | 4 +- crates/jolt-prover/src/blindfold.rs | 16 +- crates/jolt-prover/src/stages/drivers.rs | 24 +-- .../src/stages/field_inline_fixtures.rs | 9 +- crates/jolt-prover/src/stages/stage1.rs | 4 +- crates/jolt-prover/src/stages/stage2.rs | 4 +- crates/jolt-prover/src/stages/stage4.rs | 4 +- crates/jolt-prover/src/stages/stage5.rs | 2 +- crates/jolt-prover/src/stages/stage6a.rs | 2 +- crates/jolt-prover/src/stages/stage6b.rs | 4 +- crates/jolt-prover/tests/engine_twins.rs | 25 ++- .../src/round_scheduler_tests.rs | 2 +- crates/jolt-sumcheck/src/tests.rs | 32 ++-- crates/jolt-sumcheck/tests/committed.rs | 4 +- crates/jolt-sumcheck/tests/roundtrip.rs | 4 +- crates/jolt-sumcheck/tests/solinas_fields.rs | 8 +- crates/jolt-transcript/Cargo.toml | 1 + .../fuzz/fuzz_targets/encoding_injectivity.rs | 2 +- .../fuzz_targets/transcript_determinism.rs | 2 +- crates/jolt-transcript/src/channel.rs | 5 +- crates/jolt-transcript/src/duplex.rs | 144 -------------- crates/jolt-transcript/src/fork.rs | 48 +++++ crates/jolt-transcript/src/grinding.rs | 121 +++--------- crates/jolt-transcript/src/lib.rs | 24 ++- crates/jolt-transcript/src/nonce.rs | 86 +++++++++ crates/jolt-transcript/src/preview.rs | 44 ----- crates/jolt-transcript/src/prover.rs | 141 ++++++++------ crates/jolt-transcript/src/state.rs | 132 +++++++++++++ crates/jolt-transcript/src/verifier.rs | 178 +++++++++++------- crates/jolt-transcript/tests/known_answers.rs | 20 +- crates/jolt-transcript/tests/narg.rs | 70 ++++--- crates/jolt-verifier/src/stages/relations.rs | 4 +- crates/jolt-verifier/src/verifier.rs | 47 +++-- crates/jolt-verifier/tests/completeness/zk.rs | 2 +- .../fs_inventory/challenge-sites.inventory | 2 + .../fs_inventory/delegation-sites.inventory | 8 +- .../fs_inventory/message-sites.inventory | 15 +- crates/jolt-verifier/tests/support/narg.rs | 2 +- scripts/check-no-yolocrypto.sh | 29 +++ 49 files changed, 843 insertions(+), 640 deletions(-) delete mode 100644 crates/jolt-transcript/src/duplex.rs create mode 100644 crates/jolt-transcript/src/fork.rs create mode 100644 crates/jolt-transcript/src/nonce.rs delete mode 100644 crates/jolt-transcript/src/preview.rs create mode 100644 crates/jolt-transcript/src/state.rs create mode 100755 scripts/check-no-yolocrypto.sh diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fd08a62739..54e18ec4c6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -69,6 +69,8 @@ jobs: run: scripts/check-shared-field-identity.sh - name: Keep Metal out of the verifier run: scripts/check-metal-isolation.sh + - name: Keep spongefish's sponge state private (no yolocrypto) + run: scripts/check-no-yolocrypto.sh - name: Check no backend run: cargo check -p jolt-field --no-default-features - name: Check BN254 backend diff --git a/crates/jolt-akita/tests/grouped_capacity.rs b/crates/jolt-akita/tests/grouped_capacity.rs index c369f7aa9d..dbce3f0381 100644 --- a/crates/jolt-akita/tests/grouped_capacity.rs +++ b/crates/jolt-akita/tests/grouped_capacity.rs @@ -22,9 +22,7 @@ use jolt_openings::{ }; use jolt_poly::{MultilinearPoly, OneHotPolynomial}; use jolt_transcript::Channel; -use support::{ - assert_transcripts_agree, f, layout, new_prover_transcript, new_verifier_transcript, polynomial, -}; +use support::{f, layout, new_prover_transcript, new_verifier_transcript, polynomial}; const FINAL_NUM_VARS: usize = 16; /// Six variables above this one-column trace group. The canonical Jolt trace @@ -118,6 +116,8 @@ fn grouped_opening_proves_advice_larger_than_the_trace_group() { ) .expect("grouped opening should prove"); let proof = prover_transcript.narg().to_vec(); + // The prover's sponge state, as one final squeeze both roles draw. + let prover_checkpoint = prover_transcript.challenge_bytes::<32>(); let transported: AkitaVerifierSetup = serde_json::from_str( &serde_json::to_string(&verifier_setup).expect("verifier setup should serialize"), @@ -132,7 +132,13 @@ fn grouped_opening_proves_advice_larger_than_the_trace_group() { &mut verifier_transcript, ) .expect("grouped opening should verify"); - assert_transcripts_agree(prover_transcript.clone(), verifier_transcript); + assert_eq!( + verifier_transcript.challenge_bytes::<32>(), + prover_checkpoint + ); + verifier_transcript + .finish() + .expect("the verifier consumes the whole proof"); } let mut other_context = new_verifier_transcript(b"akita-grouped-capacity", &proof); diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs index 347948f472..e9ff8f3157 100644 --- a/crates/jolt-blindfold/src/verify.rs +++ b/crates/jolt-blindfold/src/verify.rs @@ -702,8 +702,8 @@ mod tests { assert_eq!(folded, expected); assert_eq!( - transcript.preview().squeeze::<32>(), - prover.preview().squeeze::<32>() + transcript.challenge_bytes::<32>(), + prover.challenge_bytes::<32>() ); } diff --git a/crates/jolt-blindfold/tests/support/mod.rs b/crates/jolt-blindfold/tests/support/mod.rs index 60a6159cc2..ee43bb7731 100644 --- a/crates/jolt-blindfold/tests/support/mod.rs +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -760,13 +760,16 @@ pub enum Stage2Input { ProductOfStage1Openings, } -/// Two committed stages already written into `transcript`, one final opening -/// bound to the first stage-1 output-claim value, and the protocol the prover -/// builds from them. +/// Two committed stages, one final opening bound to the first stage-1 +/// output-claim value, and the protocol the prover builds from them. The +/// stages are proved from `stage_seed`, so [`transcript`](Self::transcript) +/// rebuilds the prover transcript at the BlindFold boundary on demand: a +/// prover transcript cannot be copied. #[derive(Clone)] pub struct TwoStageFixture { pub setup: PedersenSetup, - pub transcript: ProverTranscript, + stage_seed: [u8; 32], + stage2_input: Stage2Input, pub stages: Vec, pub template: ProtocolTemplate, pub statement: BlindFoldStatement, @@ -776,6 +779,11 @@ pub struct TwoStageFixture { } impl TwoStageFixture { + /// The prover transcript after both committed stages. + pub fn transcript(&self) -> ProverTranscript { + prove_stages(&self.setup, self.stage_seed, self.stage2_input).0 + } + pub fn stage_witnesses(&self) -> Vec<&CommittedSumcheckWitness> { self.stages.iter().map(|stage| &stage.witness).collect() } @@ -788,7 +796,7 @@ impl TwoStageFixture { blindings: &[F], rng: &mut impl RngCore, ) -> Result, ProverError> { - let mut transcript = self.transcript.clone(); + let mut transcript = self.transcript(); prove::( &self.setup, &self.protocol, @@ -809,11 +817,49 @@ impl TwoStageFixture { } pub fn messages(&self, narg: &[u8]) -> ProofMessages { - ProofMessages::parse(&self.protocol, self.transcript.narg().len(), narg) + ProofMessages::parse(&self.protocol, self.transcript().narg().len(), narg) .expect("proof parses in transcript order") } } +/// Proves the two committed stages from `seed` on a fresh transcript. +fn prove_stages( + setup: &PedersenSetup, + seed: [u8; 32], + stage2_input: Stage2Input, +) -> ( + ProverTranscript, + GeneratedStage, + GeneratedStage, + Expr, +) { + let mut rng = ChaCha20Rng::from_seed(seed); + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + let mut prover = SumcheckTestProver::new(&mut rng); + let stage1 = prover.prove_stage_with_output_claims( + setup, + &mut transcript, + SumcheckStatement::new(3, 3), + f(37), + 2, + ); + let (input2, input2_claim) = match stage2_input { + Stage2Input::Constant => (f(89), constant(f(89))), + Stage2Input::ProductOfStage1Openings => { + let row = &stage1.witness.output_claim_rows[0]; + (row[0] * row[1], opening(0usize) * opening(1usize)) + } + }; + let stage2 = prover.prove_stage_with_output_claims( + setup, + &mut transcript, + SumcheckStatement::new(2, 3), + input2, + 1, + ); + (transcript, stage1, stage2, input2_claim) +} + pub fn two_stage_fixture(rng: &mut R, stage2_input: Stage2Input) -> TwoStageFixture { two_stage_fixture_with_bindings(rng, stage2_input, 1) } @@ -826,26 +872,12 @@ pub fn two_stage_fixture_with_bindings( binding_count: usize, ) -> TwoStageFixture { let setup = pedersen_setup(4); - let statement1 = SumcheckStatement::new(3, 3); - let statement2 = SumcheckStatement::new(2, 3); + let mut stage_seed = [0u8; 32]; + rng.fill_bytes(&mut stage_seed); + let (_, stage1, stage2, input2_claim) = prove_stages(&setup, stage_seed, stage2_input); + let statement1 = stage1.statement; + let statement2 = stage2.statement; let input1 = f(37); - let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); - - let (stage1, stage2, input2_claim) = { - let mut prover = SumcheckTestProver::new(&mut *rng); - let stage1 = - prover.prove_stage_with_output_claims(&setup, &mut transcript, statement1, input1, 2); - let (input2, input2_claim) = match stage2_input { - Stage2Input::Constant => (f(89), constant(f(89))), - Stage2Input::ProductOfStage1Openings => { - let row = &stage1.witness.output_claim_rows[0]; - (row[0] * row[1], opening(0usize) * opening(1usize)) - } - }; - let stage2 = - prover.prove_stage_with_output_claims(&setup, &mut transcript, statement2, input2, 1); - (stage1, stage2, input2_claim) - }; let mut eval_outputs = vec![stage1.witness.output_claim_rows[0][0]]; if binding_count == 2 { eval_outputs.push(stage2.witness.output_claim_rows[0][0]); @@ -895,7 +927,8 @@ pub fn two_stage_fixture_with_bindings( blindfold_protocol_from_statement(&statement).expect("protocol builds from stages"); TwoStageFixture { setup, - transcript, + stage_seed, + stage2_input, stages: vec![stage1, stage2], template, statement, @@ -957,7 +990,7 @@ pub struct BlindFoldTestProof { pub fn prove_blindfold_protocol_pipeline(rng: &mut R) -> BlindFoldTestProof { let instance = build_protocol_backed_instance(rng); let fixture = &instance.fixture; - let mut transcript = fixture.transcript.clone(); + let mut transcript = fixture.transcript(); let witness = ProtocolWitness { rows: &instance.rows, blindings: &instance.blindings, diff --git a/crates/jolt-dory/src/scheme.rs b/crates/jolt-dory/src/scheme.rs index 7d3b892099..1c4fc42aeb 100644 --- a/crates/jolt-dory/src/scheme.rs +++ b/crates/jolt-dory/src/scheme.rs @@ -295,7 +295,7 @@ impl CommitmentScheme for DoryScheme { setup: &Self::VerifierSetup, transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { - let proof = read_proof(transcript.clone(), point.len(), false)?; + let proof = read_proof(transcript.unread(), point.len(), false)?; let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); let ark_eval = jolt_fr_to_ark(&eval); let ark_commitment = jolt_gt_to_ark(&commitment.0); @@ -427,7 +427,7 @@ impl ZkOpeningScheme for DoryScheme { setup: &Self::VerifierSetup, transcript: &mut VerifierTranscript<'_, H>, ) -> Result { - let proof = read_proof(transcript.clone(), point.len(), true)?; + let proof = read_proof(transcript.unread(), point.len(), true)?; let ark_point: Vec = point.iter().rev().map(jolt_fr_to_ark).collect(); // In ZK mode dory::verify reads the evaluation commitment from `proof.y_com`, // so the caller-side eval is unused here. diff --git a/crates/jolt-dory/src/transcript.rs b/crates/jolt-dory/src/transcript.rs index 885e5aca7f..2591c716a5 100644 --- a/crates/jolt-dory/src/transcript.rs +++ b/crates/jolt-dory/src/transcript.rs @@ -6,7 +6,7 @@ //! value as a prover message, so the argument string holds exactly Dory's //! messages in Fiat-Shamir order. dory-pcs verifies from a proof struct it //! receives up front, so the verifier first rebuilds that struct by reading the -//! messages on a scratch copy of the transcript, then runs `dory::verify` with +//! messages from the transcript's unread bytes without absorbing them, then runs `dory::verify` with //! an adapter whose every `append_serde` receives the next message from the //! live transcript and checks it equals the value dory-pcs absorbs. //! @@ -29,7 +29,7 @@ use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use crate::scheme::{jolt_fr_to_ark, ArkFr, ArkG1, ArkGT}; /// Prover side: every absorbed value becomes a prover message. -pub(crate) struct DoryProverChannel<'a, H> { +pub(crate) struct DoryProverChannel<'a, H: Sponge> { transcript: &'a mut ProverTranscript, } @@ -82,7 +82,7 @@ impl DoryTranscript for DoryProverChannel<'_, H> { } /// Verifier side: every absorbed value must be the next prover message. -pub(crate) struct DoryVerifierChannel<'a, 'p, H> { +pub(crate) struct DoryVerifierChannel<'a, 'p, H: Sponge> { transcript: &'a mut VerifierTranscript<'p, H>, mismatch: bool, } @@ -160,27 +160,27 @@ fn compressed(value: &S) -> Vec { /// Rebuilds the Dory proof for a `num_vars`-variable opening from the next /// prover messages, reading them from `scout`, a scratch copy of the live /// verifier transcript. Message order mirrors `dory::verify`. -pub(crate) fn read_proof( - mut scout: VerifierTranscript<'_, H>, +pub(crate) fn read_proof( + unread: &[u8], num_vars: usize, zk: bool, ) -> Result { let sigma = num_vars.div_ceil(2); let nu = num_vars - sigma; - let reader = &mut scout; + let reader = &mut { unread }; let vmv_message = VMVMessage { - c: read::(reader)?, + c: read::(reader)?, d2: read(reader)?, - e1: read::(reader)?, + e1: read::(reader)?, }; let zk_prefix = if zk { - let e2 = read::(reader)?; - let y_com = read::(reader)?; - let sigma1_commitments = (read::(reader)?, read::(reader)?); + let e2 = read::(reader)?; + let y_com = read::(reader)?; + let sigma1_commitments = (read::(reader)?, read::(reader)?); let sigma2 = Sigma2Proof { - a: read::(reader)?, - z1: read::(reader)?, + a: read::(reader)?, + z1: read::(reader)?, z2: read(reader)?, }; Some((e2, y_com, sigma1_commitments, sigma2)) @@ -192,27 +192,27 @@ pub(crate) fn read_proof( let mut second_messages = Vec::with_capacity(sigma); for _ in 0..sigma { first_messages.push(FirstReduceMessage { - d1_left: read::(reader)?, + d1_left: read::(reader)?, d1_right: read(reader)?, d2_left: read(reader)?, d2_right: read(reader)?, - e1_beta: read::(reader)?, - e2_beta: read::(reader)?, + e1_beta: read::(reader)?, + e2_beta: read::(reader)?, }); second_messages.push(SecondReduceMessage { - c_plus: read::(reader)?, + c_plus: read::(reader)?, c_minus: read(reader)?, - e1_plus: read::(reader)?, + e1_plus: read::(reader)?, e1_minus: read(reader)?, - e2_plus: read::(reader)?, + e2_plus: read::(reader)?, e2_minus: read(reader)?, }); } let Some((e2, y_com, (a1, a2), sigma2)) = zk_prefix else { let final_message = ScalarProductMessage { - e1: read::(reader)?, - e2: read::(reader)?, + e1: read::(reader)?, + e2: read::(reader)?, }; return Ok(ArkDoryProof { vmv_message, @@ -230,20 +230,20 @@ pub(crate) fn read_proof( }; let scalar_product = ScalarProductProof { - p1: read::(reader)?, + p1: read::(reader)?, p2: read(reader)?, q: read(reader)?, r: read(reader)?, - e1: read::(reader)?, - e2: read::(reader)?, - r1: read::(reader)?, + e1: read::(reader)?, + e2: read::(reader)?, + r1: read::(reader)?, r2: read(reader)?, r3: read(reader)?, }; let sigma1 = Sigma1Proof { a1, a2, - z1: read::(reader)?, + z1: read::(reader)?, z2: read(reader)?, z3: read(reader)?, }; @@ -284,13 +284,18 @@ impl CompressedWidth for ArkGT { } /// Reads one compressed element. -fn read(scout: &mut VerifierTranscript<'_, H>) -> Result +/// Parses the next compressed `T` from the unread proof bytes, advancing +/// `unread` past it. +fn read(unread: &mut &[u8]) -> Result where T: DoryDeserialize + CompressedWidth, - H: Sponge, { - let bytes = scout.receive_bytes(T::BYTES)?; - T::deserialize_compressed(bytes).map_err(|_| OpeningsError::VerificationFailed) + let (bytes, rest) = unread + .split_at_checked(T::BYTES) + .ok_or(OpeningsError::VerificationFailed)?; + let value = T::deserialize_compressed(bytes).map_err(|_| OpeningsError::VerificationFailed)?; + *unread = rest; + Ok(value) } #[cfg(test)] diff --git a/crates/jolt-kernels/src/reference/naive.rs b/crates/jolt-kernels/src/reference/naive.rs index 2573755a9a..204fc4cdee 100644 --- a/crates/jolt-kernels/src/reference/naive.rs +++ b/crates/jolt-kernels/src/reference/naive.rs @@ -749,8 +749,8 @@ mod tests { symbolic: ToySymbolic::new(ROUNDS), reference_point: reference_point(), }; - let mut verifier_transcript = - VerifierTranscript::::new(&protocol, b"", prover_transcript.narg()); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = VerifierTranscript::::new(&protocol, b"", &narg); let verifier_challenges = verifier_relation .draw_challenges(&mut verifier_transcript) .unwrap(); @@ -776,8 +776,8 @@ mod tests { assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); assert_eq!(received, output_claims.opening_values()); assert_eq!( - verifier_transcript.preview().squeeze::<32>(), - prover_transcript.preview().squeeze::<32>() + verifier_transcript.challenge_bytes::<32>(), + prover_transcript.challenge_bytes::<32>() ); verifier_transcript.finish().unwrap(); } diff --git a/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs b/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs index bbce2e6482..1ce27d8c1f 100644 --- a/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs +++ b/crates/jolt-openings/fuzz/fuzz_targets/dory_homomorphic_batch_must_reject.rs @@ -176,8 +176,8 @@ fuzz_target!(|data: &[u8]| { ) .unwrap_or_else(|error| panic!("honest clear batch verification failed: {error}")); assert_eq!( - prover_transcript.preview().squeeze::<32>(), - verifier_transcript.preview().squeeze::<32>() + prover_transcript.challenge_bytes::<32>(), + verifier_transcript.challenge_bytes::<32>() ); verifier_transcript .finish() @@ -313,8 +313,8 @@ fuzz_target!(|data: &[u8]| { .unwrap_or_else(|error| panic!("honest ZK batch verification failed: {error}")); assert_eq!(opening.hiding_commitment, verifier_hiding); assert_eq!( - prover_transcript.preview().squeeze::<32>(), - verifier_transcript.preview().squeeze::<32>() + prover_transcript.challenge_bytes::<32>(), + verifier_transcript.challenge_bytes::<32>() ); verifier_transcript .finish() diff --git a/crates/jolt-openings/tests/homomorphic_dory.rs b/crates/jolt-openings/tests/homomorphic_dory.rs index 59dc1ddf26..905d9620af 100644 --- a/crates/jolt-openings/tests/homomorphic_dory.rs +++ b/crates/jolt-openings/tests/homomorphic_dory.rs @@ -45,8 +45,8 @@ fn dory_homomorphic_batch_roundtrip_clear_many_polynomials() { .expect("Dory homomorphic batch proof should verify"); assert_eq!( - fingerprint(&prover_transcript), - fingerprint(&verifier_transcript) + fingerprint(&mut prover_transcript), + fingerprint(&mut verifier_transcript) ); verifier_transcript.finish().expect("proof fully consumed"); } @@ -179,8 +179,8 @@ fn dory_homomorphic_zk_batch_roundtrip() { assert_eq!(opening.hiding_commitment, verifier_hiding); assert_eq!( - fingerprint(&prover_transcript), - fingerprint(&verifier_transcript) + fingerprint(&mut prover_transcript), + fingerprint(&mut verifier_transcript) ); verifier_transcript.finish().expect("proof fully consumed"); } diff --git a/crates/jolt-openings/tests/schemes.rs b/crates/jolt-openings/tests/schemes.rs index 78ec20e8e7..ca44580979 100644 --- a/crates/jolt-openings/tests/schemes.rs +++ b/crates/jolt-openings/tests/schemes.rs @@ -59,8 +59,8 @@ fn homomorphic_batch_opening_roundtrip_clear() { .expect("batch proof should verify"); assert_eq!( - fingerprint(&prover_transcript), - fingerprint(&verifier_transcript) + fingerprint(&mut prover_transcript), + fingerprint(&mut verifier_transcript) ); verifier_transcript.finish().expect("proof fully consumed"); } @@ -186,8 +186,8 @@ fn homomorphic_batch_opening_roundtrip_zk() { assert_eq!(verifier_hiding, opening.hiding_commitment); assert_eq!( - fingerprint(&prover_transcript), - fingerprint(&verifier_transcript) + fingerprint(&mut prover_transcript), + fingerprint(&mut verifier_transcript) ); verifier_transcript.finish().expect("proof fully consumed"); } diff --git a/crates/jolt-openings/tests/support/common.rs b/crates/jolt-openings/tests/support/common.rs index 0439b110b3..25f05079d5 100644 --- a/crates/jolt-openings/tests/support/common.rs +++ b/crates/jolt-openings/tests/support/common.rs @@ -18,8 +18,8 @@ pub fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, Bl } /// Sponge state after both sides finish, which must agree for an honest run. -pub fn fingerprint(channel: &C) -> [u8; 32] { - channel.preview().squeeze() +pub fn fingerprint(channel: &mut C) -> [u8; 32] { + channel.challenge_bytes::<32>() } pub fn fr(value: u64) -> Fr { diff --git a/crates/jolt-prover/src/blindfold.rs b/crates/jolt-prover/src/blindfold.rs index 5df17d5259..86eb0b7c60 100644 --- a/crates/jolt-prover/src/blindfold.rs +++ b/crates/jolt-prover/src/blindfold.rs @@ -115,7 +115,10 @@ where { let mut replay = VerifierTranscript::::new(&jolt_protocol_id::(), JOLT_SESSION, transcript.narg()); - let VerifiedStages::Zk(protocol) = verify_stages::( + let VerifiedStages::Zk { + protocol, + checkpoint, + } = verify_stages::( &preprocessing.verifier, public_io, trusted_advice_commitment, @@ -126,11 +129,12 @@ where reason: "the verifier replay of a ZK proof produced no BlindFold protocol", }); }; - // Hard error (not debug-only) so release provers diagnose drift here - // rather than as a downstream BlindFold verification failure. - if replay.remaining() != 0 - || replay.preview().squeeze::<32>() != transcript.preview().squeeze::<32>() - { + // The prover draws the same checkpoint the verifier draws after the stage + // spine; equal checkpoints over the whole argument string mean the replay + // reached the prover's sponge state. Hard error (not debug-only) so release + // provers diagnose drift here rather than as a downstream BlindFold + // verification failure. + if replay.remaining() != 0 || transcript.challenge_bytes::<32>() != checkpoint { return Err(ProverError::InvariantViolation { reason: "the verifier replay diverged from the prover's forward transcript", }); diff --git a/crates/jolt-prover/src/stages/drivers.rs b/crates/jolt-prover/src/stages/drivers.rs index 0a59d1989c..9840580e7c 100644 --- a/crates/jolt-prover/src/stages/drivers.rs +++ b/crates/jolt-prover/src/stages/drivers.rs @@ -776,11 +776,9 @@ mod twin_tests { // Verifier twin: generated draw + composed verify_clear (which runs the // derive-opening-points and expected-final-claim checks internally and // receives the output claims). - let mut verifier_transcript = VerifierTranscript::::new( - &protocol("prove-driver-twin"), - b"", - prover_transcript.narg(), - ); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = + VerifierTranscript::::new(&protocol("prove-driver-twin"), b"", &narg); let verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); let (_, received_claims) = sumchecks .verify_clear( @@ -793,8 +791,8 @@ mod twin_tests { .unwrap(); assert_eq!(received_claims, proved.output_claims); assert_eq!( - verifier_transcript.preview().squeeze::<32>(), - prover_transcript.preview().squeeze::<32>() + verifier_transcript.challenge_bytes::<32>(), + prover_transcript.challenge_bytes::<32>() ); verifier_transcript.finish().unwrap(); @@ -884,11 +882,9 @@ mod twin_tests { ) .unwrap(); - let mut verifier_transcript = VerifierTranscript::::new( - &protocol("prove-driver-head-twin"), - b"", - prover_transcript.narg(), - ); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = + VerifierTranscript::::new(&protocol("prove-driver-head-twin"), b"", &narg); let verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); let (verified_points, received_claims) = sumchecks .verify_clear( @@ -901,8 +897,8 @@ mod twin_tests { .unwrap(); assert_eq!(received_claims, proved.output_claims); assert_eq!( - verifier_transcript.preview().squeeze::<32>(), - prover_transcript.preview().squeeze::<32>() + verifier_transcript.challenge_bytes::<32>(), + prover_transcript.challenge_bytes::<32>() ); verifier_transcript.finish().unwrap(); assert_eq!(verified_points, proved.output_points); diff --git a/crates/jolt-prover/src/stages/field_inline_fixtures.rs b/crates/jolt-prover/src/stages/field_inline_fixtures.rs index 4ccf989159..b2853b5839 100644 --- a/crates/jolt-prover/src/stages/field_inline_fixtures.rs +++ b/crates/jolt-prover/src/stages/field_inline_fixtures.rs @@ -488,12 +488,13 @@ pub(crate) fn verify_through( last: Through, checked: &CheckedInputs, preprocessing: &FixturePreprocessing, - prover: &ProverTranscript, + prover: &mut ProverTranscript, ) { + let narg = prover.narg().to_vec(); let mut transcript = VerifierTranscript::::new( &jolt_protocol_id::(), JOLT_SESSION, - prover.narg(), + &narg, ); let verifier = &preprocessing.verifier; let formula_dimensions = @@ -549,8 +550,8 @@ pub(crate) fn verify_through( } assert_eq!(transcript.remaining(), 0, "unconsumed argument bytes"); assert_eq!( - transcript.preview().squeeze::<32>(), - prover.preview().squeeze::<32>(), + transcript.challenge_bytes::<32>(), + prover.challenge_bytes::<32>(), "verifier and prover sponge states diverge" ); transcript.finish().unwrap(); diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index c26348df03..d488c2da63 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -185,7 +185,7 @@ mod field_inline_round_trip { Through::Stage1, &test_checked_inputs(), &preprocessing, - &prover_transcript, + &mut prover_transcript, ); } @@ -269,7 +269,7 @@ mod field_inline_zk { Through::Stage1, &test_checked_inputs(), &field_arithmetic_preprocessing(), - &prover_transcript, + &mut prover_transcript, ); } } diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index 56a623eb09..055405a5c0 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -265,7 +265,7 @@ mod field_inline_round_trip { Through::Stage2, &test_checked_inputs(), &field_arithmetic_preprocessing(), - &prover_transcript, + &mut prover_transcript, ); } } @@ -334,7 +334,7 @@ mod field_inline_zk { Through::Stage2, &test_checked_inputs(), &field_arithmetic_preprocessing(), - &prover_transcript, + &mut prover_transcript, ); } } diff --git a/crates/jolt-prover/src/stages/stage4.rs b/crates/jolt-prover/src/stages/stage4.rs index 4ecdc2bddd..48044a0d3e 100644 --- a/crates/jolt-prover/src/stages/stage4.rs +++ b/crates/jolt-prover/src/stages/stage4.rs @@ -309,7 +309,7 @@ mod field_inline_round_trip { Through::Stage4, &checked, &preprocessing, - &prover_transcript, + &mut prover_transcript, ); } @@ -522,6 +522,6 @@ mod field_inline_zk { .sum(); assert_eq!(value_count, 12); - verify_through(Through::Stage4, &checked, &preprocessing, &transcript); + verify_through(Through::Stage4, &checked, &preprocessing, &mut transcript); } } diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index 1808b6b45d..02fb1b8140 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -196,7 +196,7 @@ mod field_inline_round_trip { Through::Stage5, &checked, &preprocessing, - &prover_transcript, + &mut prover_transcript, ); } diff --git a/crates/jolt-prover/src/stages/stage6a.rs b/crates/jolt-prover/src/stages/stage6a.rs index 7992fcc6a8..f918afca46 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -255,7 +255,7 @@ mod field_inline_round_trip { Through::Stage6a, &checked, &preprocessing, - &prover_transcript, + &mut prover_transcript, ); } } diff --git a/crates/jolt-prover/src/stages/stage6b.rs b/crates/jolt-prover/src/stages/stage6b.rs index 436af91ec9..a0d2432e10 100644 --- a/crates/jolt-prover/src/stages/stage6b.rs +++ b/crates/jolt-prover/src/stages/stage6b.rs @@ -289,7 +289,7 @@ mod field_inline_round_trip { Through::Stage6b, &checked, &preprocessing, - &prover_transcript, + &mut prover_transcript, ); } @@ -471,6 +471,6 @@ mod field_inline_zk { 1 ); - verify_through(Through::Stage6b, &checked, &preprocessing, &transcript); + verify_through(Through::Stage6b, &checked, &preprocessing, &mut transcript); } } diff --git a/crates/jolt-prover/tests/engine_twins.rs b/crates/jolt-prover/tests/engine_twins.rs index aca6c7ed9d..1500025e14 100644 --- a/crates/jolt-prover/tests/engine_twins.rs +++ b/crates/jolt-prover/tests/engine_twins.rs @@ -136,10 +136,13 @@ fn verifier_transcript<'a>(name: &str, narg: &'a [u8]) -> VerifierTranscript<'a, /// The verifier consumed the whole argument string and reached the prover's /// sponge state. -fn assert_twins(prover: &ProverTranscript, verifier: VerifierTranscript<'_, H>) { +fn assert_twins( + prover: &mut ProverTranscript, + mut verifier: VerifierTranscript<'_, H>, +) { assert_eq!( - verifier.preview().squeeze::<32>(), - prover.preview().squeeze::<32>() + verifier.challenge_bytes::<32>(), + prover.challenge_bytes::<32>() ); verifier.finish().unwrap(); } @@ -205,7 +208,8 @@ fn clear_engine_twin_matches_generated_verify_clear() { // Verifier: draw → begin_batch → compressed rounds → output claims (the // low-level clear path the composed `verify_clear` wraps). - let mut verifier_transcript = verifier_transcript("engine-twin", prover_transcript.narg()); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier_transcript("engine-twin", &narg); let verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); let mut verifier_recorder = ClearSumcheckRecorder::::new(); let (verifier_batch, verifier_coefficients) = sumchecks @@ -231,7 +235,7 @@ fn clear_engine_twin_matches_generated_verify_clear() { assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); assert_eq!(received_values, output_values); assert_eq!(verifier_coefficients, prover_coefficients); - assert_twins(&prover_transcript, verifier_transcript); + assert_twins(&mut prover_transcript, verifier_transcript); } #[test] @@ -308,7 +312,8 @@ fn committed_engine_twin_matches_generated_verify_zk() { // Verifier: draw → generated verify_zk (coefficient draws, committed // rounds, derived points, output-claim row commitments). - let mut verifier_transcript = verifier_transcript("engine-zk-twin", prover_transcript.narg()); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier_transcript("engine-zk-twin", &narg); let _verifier_challenges = sumchecks.draw_challenges(&mut verifier_transcript).unwrap(); let verified = sumchecks .verify_zk::(ROW_LEN, &input_points, &mut verifier_transcript) @@ -331,7 +336,7 @@ fn committed_engine_twin_matches_generated_verify_zk() { output_commitments.commitments.len(), witness.output_claim_rows.len() ); - assert_twins(&prover_transcript, verifier_transcript); + assert_twins(&mut prover_transcript, verifier_transcript); } /// Twin-transcript lock for the shared uni-skip verification core: a clear @@ -369,11 +374,11 @@ fn uniskip_prover_twin_matches_uniskip_verify_clear() { ) .unwrap(); - let mut verifier_transcript = - verifier_transcript("uniskip-stage-twin", prover_transcript.narg()); + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier_transcript("uniskip-stage-twin", &narg); let verified = uniskip::verify_clear(¶ms, input_claim, &mut verifier_transcript).unwrap(); assert_eq!(verified.challenge, proved.challenge); assert_eq!(verified.output_claim, proved.output_claim); - assert_twins(&prover_transcript, verifier_transcript); + assert_twins(&mut prover_transcript, verifier_transcript); } diff --git a/crates/jolt-sumcheck/src/round_scheduler_tests.rs b/crates/jolt-sumcheck/src/round_scheduler_tests.rs index 68e274dd33..9901e26c14 100644 --- a/crates/jolt-sumcheck/src/round_scheduler_tests.rs +++ b/crates/jolt-sumcheck/src/round_scheduler_tests.rs @@ -106,7 +106,7 @@ fn proof_is_invariant_under_a_reordering_traversal() { recorder .finish(&proved.member_claims, &mut transcript) .unwrap(); - let state = fingerprint(&transcript); + let state = fingerprint(&mut transcript); (proved, transcript.finish(), state) }; diff --git a/crates/jolt-sumcheck/src/tests.rs b/crates/jolt-sumcheck/src/tests.rs index 4a8091beaa..fc72b168ca 100644 --- a/crates/jolt-sumcheck/src/tests.rs +++ b/crates/jolt-sumcheck/src/tests.rs @@ -45,8 +45,8 @@ fn verifier<'a>(session: &[u8], narg: &'a [u8]) -> VerifierTranscript<'a, H> { } /// The sponge state both roles must agree on after the same message sequence. -pub(crate) fn fingerprint(channel: &C) -> [u8; 32] { - channel.preview().squeeze() +pub(crate) fn fingerprint(channel: &mut C) -> [u8; 32] { + channel.challenge_bytes::<32>() } fn poly(coefficients: &[u64]) -> UnivariatePoly { @@ -92,7 +92,7 @@ fn round_messages_travel_at_the_degree_bound_width() { fn senders_reject_rounds_above_the_degree_bound_before_writing() { let cubic = poly(&[1, 2, 3, 4]); let mut transcript = prover(b"over-degree"); - let before = fingerprint(&transcript); + let mut untouched = prover(b"over-degree"); assert!(matches!( send_full_round(&cubic, 2, &mut transcript), @@ -102,8 +102,8 @@ fn senders_reject_rounds_above_the_degree_bound_before_writing() { send_compressed_round(&cubic, 2, &mut transcript), Err(SumcheckError::DegreeBoundExceeded { got: 3, max: 2 }) )); - assert_eq!(fingerprint(&transcript), before); - assert!(transcript.finish().is_empty()); + assert!(transcript.narg().is_empty()); + assert_eq!(fingerprint(&mut transcript), fingerprint(&mut untouched)); } fn verify_single_round( @@ -423,7 +423,7 @@ fn clear_recorder_roundtrip_matches_compressed_verifier() { recorder .finish(&[final_eval], &mut prover_transcript) .unwrap(); - let prover_state = fingerprint(&prover_transcript); + let prover_state = fingerprint(&mut prover_transcript); let narg = prover_transcript.finish(); let mut verifier_transcript = verifier(b"recorder", &narg); @@ -437,7 +437,7 @@ fn clear_recorder_roundtrip_matches_compressed_verifier() { assert_eq!(reduction.value, final_eval); assert_eq!(output_claim, final_eval); - assert_eq!(fingerprint(&verifier_transcript), prover_state); + assert_eq!(fingerprint(&mut verifier_transcript), prover_state); verifier_transcript.finish().unwrap(); } @@ -606,7 +606,7 @@ fn fused_bind_eval_member_byte_matches_separate_passes() { recorder .finish(&proved.member_claims, &mut transcript) .unwrap(); - let state = fingerprint(&transcript); + let state = fingerprint(&mut transcript); (proved, transcript.finish(), state) }; @@ -698,7 +698,7 @@ fn clear_batch_twin( recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - let prover_state = fingerprint(&prover_transcript); + let prover_state = fingerprint(&mut prover_transcript); let narg = prover_transcript.finish(); let mut verifier_transcript = verifier(session, &narg); @@ -723,7 +723,7 @@ fn clear_batch_twin( assert_eq!(member_claims, proved.member_claims); assert_eq!(reduction.value, proved.final_claim); assert_eq!(reduction.point.as_slice(), proved.challenges.as_slice()); - assert_eq!(fingerprint(&verifier_transcript), prover_state); + assert_eq!(fingerprint(&mut verifier_transcript), prover_state); verifier_transcript.finish().unwrap(); proved } @@ -800,7 +800,7 @@ fn prove_batch_committed_twin_matches_committed_consistency() { let witness = recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - let prover_state = fingerprint(&prover_transcript); + let prover_state = fingerprint(&mut prover_transcript); let narg = prover_transcript.finish(); let mut verifier_transcript = verifier(b"prove-batch-zk-twin", &narg); @@ -814,7 +814,7 @@ fn prove_batch_committed_twin_matches_committed_consistency() { .unwrap(); assert_eq!(consistency.challenges(), proved.challenges); - assert_eq!(fingerprint(&verifier_transcript), prover_state); + assert_eq!(fingerprint(&mut verifier_transcript), prover_state); verifier_transcript.finish().unwrap(); assert_eq!(witness.round_coefficients.len(), 3); @@ -864,7 +864,7 @@ fn prove_uniskip_clear_twin_matches_uniskip_verify() { &mut prover_transcript, ) .unwrap(); - let prover_state = fingerprint(&prover_transcript); + let prover_state = fingerprint(&mut prover_transcript); let narg = prover_transcript.finish(); let mut verifier_transcript = verifier(b"uniskip-twin", &narg); @@ -880,7 +880,7 @@ fn prove_uniskip_clear_twin_matches_uniskip_verify() { assert_eq!(reduction.value, round_poly.evaluate(proved.challenge)); assert_eq!(output_claim, reduction.value); assert_eq!(proved.output_claim, reduction.value); - assert_eq!(fingerprint(&verifier_transcript), prover_state); + assert_eq!(fingerprint(&mut verifier_transcript), prover_state); verifier_transcript.finish().unwrap(); } @@ -920,7 +920,7 @@ fn prove_uniskip_committed_twin_matches_committed_consistency() { &mut prover_transcript, ) .unwrap(); - let prover_state = fingerprint(&prover_transcript); + let prover_state = fingerprint(&mut prover_transcript); let narg = prover_transcript.finish(); let mut verifier_transcript = verifier(b"uniskip-zk-twin", &narg); @@ -932,7 +932,7 @@ fn prove_uniskip_committed_twin_matches_committed_consistency() { .unwrap(); assert_eq!(consistency.challenges(), vec![proved.challenge]); - assert_eq!(fingerprint(&verifier_transcript), prover_state); + assert_eq!(fingerprint(&mut verifier_transcript), prover_state); verifier_transcript.finish().unwrap(); let witness = &proved.witness; diff --git a/crates/jolt-sumcheck/tests/committed.rs b/crates/jolt-sumcheck/tests/committed.rs index 67a28a7904..ce1388f6d0 100644 --- a/crates/jolt-sumcheck/tests/committed.rs +++ b/crates/jolt-sumcheck/tests/committed.rs @@ -55,7 +55,7 @@ fn prove(setup: &PedersenSetup) -> Proved { .collect(); let outputs: Vec = [23, 29, 31, 37].map(F::from_u64).to_vec(); let witness = builder.finish(&outputs, &mut transcript).unwrap(); - let fingerprint = transcript.preview().squeeze(); + let fingerprint = transcript.challenge_bytes::<32>(); Proved { narg: transcript.finish(), challenges, @@ -77,7 +77,7 @@ fn verify( let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); let verified = SumcheckVerifier::verify_committed(statement, num_output_commitments, &mut transcript)?; - let fingerprint = transcript.preview().squeeze(); + let fingerprint = transcript.challenge_bytes::<32>(); transcript.finish()?; Ok((verified, fingerprint)) } diff --git a/crates/jolt-sumcheck/tests/roundtrip.rs b/crates/jolt-sumcheck/tests/roundtrip.rs index e892b20cd2..86957bf520 100644 --- a/crates/jolt-sumcheck/tests/roundtrip.rs +++ b/crates/jolt-sumcheck/tests/roundtrip.rs @@ -77,7 +77,7 @@ fn prove_product( } } - let fingerprint = transcript.preview().squeeze(); + let fingerprint = transcript.challenge_bytes::<32>(); ( transcript.finish(), SumcheckClaim::new(num_vars, degree, claimed_sum), @@ -96,7 +96,7 @@ fn assert_product_roundtrip(polys: &[Vec], num_vars: usize, wire: Wire) { Wire::Compressed => SumcheckVerifier::verify_compressed(&claim, &mut transcript), } .unwrap(); - assert_eq!(transcript.preview().squeeze::<32>(), prover_state); + assert_eq!(transcript.challenge_bytes::<32>(), prover_state); transcript.finish().unwrap(); assert_eq!(point.len(), num_vars); diff --git a/crates/jolt-sumcheck/tests/solinas_fields.rs b/crates/jolt-sumcheck/tests/solinas_fields.rs index bac7cee8ed..ad2490969d 100644 --- a/crates/jolt-sumcheck/tests/solinas_fields.rs +++ b/crates/jolt-sumcheck/tests/solinas_fields.rs @@ -137,7 +137,7 @@ fn run_batched_roundtrip( recorder .finish(&proved.member_claims, &mut prover_transcript) .unwrap(); - let prover_state: [u8; 32] = prover_transcript.preview().squeeze(); + let prover_state: [u8; 32] = prover_transcript.challenge_bytes::<32>(); let narg = prover_transcript.finish(); assert_eq!(narg.len(), (2 * 2 + 1) * F::NUM_BYTES); @@ -159,7 +159,7 @@ fn run_batched_roundtrip( assert_eq!(proved.member_claims, vec![expected]); assert_eq!(reduced.value, coefficient * expected); assert_eq!(proved.final_claim, reduced.value); - assert_eq!(verifier_transcript.preview().squeeze::<32>(), prover_state); + assert_eq!(verifier_transcript.challenge_bytes::<32>(), prover_state); verifier_transcript.finish().unwrap(); } @@ -178,7 +178,7 @@ fn run_uniskip_roundtrip(coefficients: [F; 3]) { &mut prover_transcript, ) .unwrap(); - let prover_state: [u8; 32] = prover_transcript.preview().squeeze(); + let prover_state: [u8; 32] = prover_transcript.challenge_bytes::<32>(); let narg = prover_transcript.finish(); let mut verifier_transcript = @@ -195,7 +195,7 @@ fn run_uniskip_roundtrip(coefficients: [F; 3]) { assert_eq!(reduced.value, polynomial.evaluate(proved.challenge)); assert_eq!(output_claim, proved.output_claim); assert_eq!(reduced.value, proved.output_claim); - assert_eq!(verifier_transcript.preview().squeeze::<32>(), prover_state); + assert_eq!(verifier_transcript.challenge_bytes::<32>(), prover_state); verifier_transcript.finish().unwrap(); } diff --git a/crates/jolt-transcript/Cargo.toml b/crates/jolt-transcript/Cargo.toml index 453aa283d5..5ebd221d21 100644 --- a/crates/jolt-transcript/Cargo.toml +++ b/crates/jolt-transcript/Cargo.toml @@ -27,6 +27,7 @@ light-poseidon = { workspace = true, optional = true } # spongefish 0.7 references its sha3 `StdHash` even when another sponge is # selected, so the feature is required to build at all. spongefish = { workspace = true, features = ["sha3"] } +rand = { workspace = true, features = ["std_rng"] } rand_core = { workspace = true } thiserror = { workspace = true } jolt-field = { path = "../jolt-field", default-features = false } diff --git a/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs b/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs index 7f1ec84e49..58d94be0b4 100644 --- a/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs +++ b/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs @@ -37,7 +37,7 @@ fn framed_state(chunks: &[Vec]) -> [u8; 32] { for chunk in chunks { transcript.public_bytes(chunk); } - transcript.preview().squeeze() + transcript.challenge_bytes::<32>() } fuzz_target!(|data: &[u8]| { diff --git a/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs b/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs index dfa0fb817d..83d9c6400d 100644 --- a/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs +++ b/crates/jolt-transcript/fuzz/fuzz_targets/transcript_determinism.rs @@ -89,7 +89,7 @@ fn run( Op::Challenge => {} } } - let state = transcript.preview().squeeze(); + let state = transcript.challenge_bytes::<32>(); (challenges, transcript.finish(), state) } diff --git a/crates/jolt-transcript/src/channel.rs b/crates/jolt-transcript/src/channel.rs index 70e17e0574..6f3444edfc 100644 --- a/crates/jolt-transcript/src/channel.rs +++ b/crates/jolt-transcript/src/channel.rs @@ -2,7 +2,7 @@ use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; -use crate::{Preview, SiteId, Sponge, TranscriptError}; +use crate::{SiteId, Sponge, TranscriptError}; /// One party's end of a Fiat-Shamir proof channel. /// @@ -71,7 +71,4 @@ pub trait Channel { /// Squeezes `N` raw challenge bytes. fn challenge_bytes(&mut self) -> [u8; N]; - - /// A detached copy of the current sponge state. - fn preview(&self) -> Preview; } diff --git a/crates/jolt-transcript/src/duplex.rs b/crates/jolt-transcript/src/duplex.rs deleted file mode 100644 index fd5137c8e4..0000000000 --- a/crates/jolt-transcript/src/duplex.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Sponge state shared by both transcript roles. - -use jolt_field::{CanonicalBytes, CanonicalEncoding, Field}; -use rand_core::RngCore; - -#[cfg(feature = "logging")] -use crate::site::TranscriptEvent; -use crate::site::{Log, TranscriptOp}; -use crate::{Preview, ProtocolId, SiteId, Sponge}; - -/// Bytes squeezed for a [`Channel::challenge_small`](crate::Channel::challenge_small). -pub const SMALL_CHALLENGE_BYTES: usize = 16; - -/// Width of the little-endian length prefix on framed public bytes. -const FRAME_LEN_BYTES: usize = 8; - -/// The sponge plus its event log. Both roles run every absorb and squeeze -/// through here, so their sponge transitions cannot drift apart. -#[derive(Clone, Debug)] -pub(crate) struct Duplex { - sponge: H, - log: Log, -} - -impl Duplex { - /// Absorbs the protocol id, then the length-framed session. - pub(crate) fn new(protocol: &ProtocolId, session: &[u8]) -> Self { - let mut sponge = H::default(); - let _ = sponge.absorb(protocol.as_bytes()); - absorb_framed(&mut sponge, session); - Self { - sponge, - log: Log::default(), - } - } - - pub(crate) fn set_site(&mut self, site: SiteId) { - self.log.set_site(site); - } - - pub(crate) fn absorb_public(&mut self, bytes: &[u8]) { - let _ = self.sponge.absorb(bytes); - self.log.record(TranscriptOp::Public, bytes.len(), None); - } - - pub(crate) fn absorb_public_atoms(&mut self, values: &[A]) { - let mut bytes = vec![0u8; A::NUM_BYTES * values.len()]; - for (value, out) in values.iter().zip(bytes.chunks_exact_mut(A::NUM_BYTES)) { - value.to_bytes_le(out); - } - self.absorb_public(&bytes); - } - - pub(crate) fn absorb_public_framed(&mut self, bytes: &[u8]) { - absorb_framed(&mut self.sponge, bytes); - self.log - .record(TranscriptOp::Public, FRAME_LEN_BYTES + bytes.len(), None); - } - - /// Absorbs prover-message bytes found at `narg_start` in the argument string. - pub(crate) fn absorb_message(&mut self, bytes: &[u8], narg_start: usize) { - let _ = self.sponge.absorb(bytes); - self.log.record( - TranscriptOp::Message, - bytes.len(), - Some(narg_start..narg_start + bytes.len()), - ); - } - - pub(crate) fn squeeze(&mut self, out: &mut [u8]) { - let _ = self.sponge.squeeze(out); - self.log.record(TranscriptOp::Challenge, out.len(), None); - } - - pub(crate) fn squeeze_array(&mut self) -> [u8; N] { - let mut out = [0u8; N]; - self.squeeze(&mut out); - out - } - - pub(crate) fn challenge(&mut self) -> F { - let mut rng = SqueezeRng { - sponge: &mut self.sponge, - squeezed: 0, - }; - let value = F::random(&mut rng); - let squeezed = rng.squeezed; - self.log.record(TranscriptOp::Challenge, squeezed, None); - value - } - - pub(crate) fn challenge_small(&mut self) -> F { - let mut bytes = [0u8; SMALL_CHALLENGE_BYTES]; - self.squeeze(&mut bytes); - F::from_challenge_bytes(&bytes) - } - - pub(crate) fn preview(&self) -> Preview { - Preview::new(self.sponge.clone()) - } - - #[cfg(feature = "logging")] - pub(crate) fn events(&self) -> &[TranscriptEvent] { - self.log.events() - } -} - -/// Absorbs `len(bytes)` as a `u64` little-endian prefix, then `bytes`, so -/// consecutive variable-length values cannot run into each other. -fn absorb_framed(sponge: &mut H, bytes: &[u8]) { - let _ = sponge.absorb(&(bytes.len() as u64).to_le_bytes()); - let _ = sponge.absorb(bytes); -} - -/// Feeds squeezed sponge output to [`Field::random`], whose rejection -/// sampling contract defines exactly uniform challenges. -struct SqueezeRng<'a, H> { - sponge: &'a mut H, - squeezed: usize, -} - -impl RngCore for SqueezeRng<'_, H> { - fn next_u32(&mut self) -> u32 { - let mut bytes = [0u8; 4]; - self.fill_bytes(&mut bytes); - u32::from_le_bytes(bytes) - } - - fn next_u64(&mut self) -> u64 { - let mut bytes = [0u8; 8]; - self.fill_bytes(&mut bytes); - u64::from_le_bytes(bytes) - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - let _ = self.sponge.squeeze(dest); - self.squeezed += dest.len(); - } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { - self.fill_bytes(dest); - Ok(()) - } -} diff --git a/crates/jolt-transcript/src/fork.rs b/crates/jolt-transcript/src/fork.rs new file mode 100644 index 0000000000..3b9d247cf6 --- /dev/null +++ b/crates/jolt-transcript/src/fork.rs @@ -0,0 +1,48 @@ +//! Seeded forks: off-transcript search keyed by the live transcript. + +use crate::Sponge; + +/// Byte length of the seed a fork is keyed by. +pub const FORK_SEED_LEN: usize = 32; + +/// Domain tag separating every fork from every transcript sponge, which +/// starts by absorbing a 64-byte protocol id. +const FORK_TAG: &[u8] = b"jolt-transcript/fork/v1"; + +/// A fresh sponge keyed by a seed squeezed from the live transcript and a +/// prover-chosen counter. +/// +/// A prover that must search (proof of work, or Fiat-Shamir with aborts) squeezes +/// one [`FORK_SEED_LEN`]-byte seed, tries counters on forks, then sends the +/// accepted counter as a `u32` prover message. The verifier squeezes the same +/// seed, receives the counter, and rebuilds the same fork. The seed binds the +/// whole prechallenge transcript, and the counter is in the argument string, +/// so nothing a fork derives can be chosen independently of the transcript. +/// No transcript state is ever copied. +#[derive(Clone, Debug)] +pub struct Fork(H); + +impl Fork { + /// The fork for `counter` under `seed`. + #[must_use] + pub fn new(seed: &[u8; FORK_SEED_LEN], counter: u32) -> Self { + let mut sponge = H::default(); + let _ = sponge.absorb(FORK_TAG); + let _ = sponge.absorb(seed); + let _ = sponge.absorb(&counter.to_le_bytes()); + Self(sponge) + } + + /// Absorbs `bytes`. + pub fn absorb(&mut self, bytes: &[u8]) { + let _ = self.0.absorb(bytes); + } + + /// Squeezes `N` bytes. + #[must_use] + pub fn squeeze(&mut self) -> [u8; N] { + let mut out = [0u8; N]; + let _ = self.0.squeeze(&mut out); + out + } +} diff --git a/crates/jolt-transcript/src/grinding.rs b/crates/jolt-transcript/src/grinding.rs index 5b15107477..a8a079e709 100644 --- a/crates/jolt-transcript/src/grinding.rs +++ b/crates/jolt-transcript/src/grinding.rs @@ -1,22 +1,24 @@ -//! Proof of work and its nonce codec. +//! Proof of work: squeeze-then-grind. //! -//! A prover grinds `bits` bits by finding a nonce whose absorption makes the -//! next 32 squeezed bytes start with `bits` zero bits, low bit first. Nonces -//! are searched over `bits + GRINDING_NONCE_SLACK_BITS` bits, so an honest -//! search exhausts its range with probability about `exp(-2^7)`. +//! A grind squeezes a seed from the live transcript, then searches nonces off +//! the transcript: a candidate passes when the first 32 bytes its +//! [`Fork`] squeezes start with `bits` zero bits, low bit first. The nonce is then sent as a `u32` prover +//! message, so the protected challenge drawn after it binds the solution. +//! Nonces are searched over `bits + GRINDING_NONCE_SLACK_BITS` bits, so an +//! honest search exhausts its range with probability about `exp(-2^7)`. use std::num::NonZeroU8; +use crate::{Fork, Sponge}; + /// Extra nonce bits searched beyond the difficulty. pub const GRINDING_NONCE_SLACK_BITS: u8 = 7; /// Largest supported difficulty; the nonce range then fills a `u32`. pub const MAX_GRINDING_BITS: u8 = u32::BITS as u8 - GRINDING_NONCE_SLACK_BITS; /// Byte length of the squeezed proof-of-work predicate. pub const GRINDING_PREDICATE_LEN: usize = 32; - -/// Largest encoded nonce: a `u32` in 7-bit groups. -const NONCE_MAX_BYTES: usize = 5; - +/// Byte length of the seed a grind squeezes from the transcript. +pub const GRINDING_SEED_LEN: usize = crate::FORK_SEED_LEN; /// Returns whether the low `bits` bits of `predicate`, low byte first and low /// bit first within each byte, are all zero. #[must_use] @@ -34,72 +36,29 @@ pub fn grinding_predicate_accepts( .is_some_and(|&byte| byte & ((1u8 << remaining) - 1) == 0)) } -/// Nonce search width for difficulty `bits`. -pub(crate) fn nonce_bits(bits: NonZeroU8) -> Option { - (bits.get() <= MAX_GRINDING_BITS).then(|| bits.get() + GRINDING_NONCE_SLACK_BITS) -} - -/// The canonical encoding of a nonce: little-endian base-128 groups with a -/// continuation bit and no redundant trailing zero group. -pub(crate) struct NonceBytes { - bytes: [u8; NONCE_MAX_BYTES], - len: usize, -} - -impl NonceBytes { - pub(crate) fn as_slice(&self) -> &[u8] { - self.bytes.split_at(self.len).0 - } -} - -pub(crate) fn encode_nonce(mut value: u32) -> NonceBytes { - let mut encoded = NonceBytes { - bytes: [0; NONCE_MAX_BYTES], - len: 0, - }; - for slot in &mut encoded.bytes { - let mut byte = (value & 0x7f) as u8; - value >>= 7; - if value != 0 { - byte |= 0x80; - } - *slot = byte; - encoded.len += 1; - if value == 0 { - break; - } - } - encoded -} - -/// Decodes a canonical nonce below `2^nonce_bits` from the front of `bytes`, -/// returning it with its encoded length. Rejects non-minimal encodings. -pub(crate) fn decode_nonce(bytes: &[u8], nonce_bits: u8) -> Option<(u32, usize)> { - let mut value = 0u64; - for (index, &byte) in bytes.iter().take(NONCE_MAX_BYTES).enumerate() { - let payload = byte & 0x7f; - value |= u64::from(payload) << (7 * index); - if byte & 0x80 == 0 { - if index != 0 && payload == 0 { - return None; - } - let value = u32::try_from(value).ok()?; - return (u64::from(value) >> nonce_bits == 0).then_some((value, index + 1)); - } - } - None +/// Whether `nonce` passes `bits` bits of work under `seed`. +pub(crate) fn grinding_accepts( + seed: &[u8; GRINDING_SEED_LEN], + nonce: u32, + bits: NonZeroU8, +) -> bool { + grinding_predicate_accepts(&Fork::::new(seed, nonce).squeeze(), bits) } -/// Searches `0..2^nonce_bits` for the first nonce whose predicate accepts. -pub(crate) fn search_nonce( +/// The first nonce in `0..2^nonce_bits` that passes `bits` bits of work. +pub(crate) fn grind_nonce( + seed: &[u8; GRINDING_SEED_LEN], bits: NonZeroU8, nonce_bits: u8, - mut predicate_for: impl FnMut(u32) -> [u8; GRINDING_PREDICATE_LEN], ) -> Option { - let attempts = 1u64 << nonce_bits; - (0..attempts) + (0..1u64 << nonce_bits) .filter_map(|candidate| u32::try_from(candidate).ok()) - .find(|&nonce| grinding_predicate_accepts(&predicate_for(nonce), bits)) + .find(|&nonce| grinding_accepts::(seed, nonce, bits)) +} + +/// Nonce search width for difficulty `bits`. +pub(crate) fn nonce_bits(bits: NonZeroU8) -> Option { + (bits.get() <= MAX_GRINDING_BITS).then(|| bits.get() + GRINDING_NONCE_SLACK_BITS) } #[cfg(test)] @@ -122,28 +81,4 @@ mod tests { assert!(grinding_predicate_accepts(&predicate, nonzero)); } } - - #[test] - fn nonce_codec_is_canonical() { - for value in [0, 1, 0x7f, 0x80, 0x3fff, 0x4000, u32::MAX] { - let encoded = encode_nonce(value); - assert_eq!( - decode_nonce(encoded.as_slice(), 32), - Some((value, encoded.as_slice().len())) - ); - } - assert_eq!( - decode_nonce(&[0x80, 0x00], 32), - None, - "redundant zero group" - ); - assert_eq!(decode_nonce(&[0x80], 32), None, "truncated"); - assert_eq!( - decode_nonce(&[0xff, 0xff, 0xff, 0xff, 0x1f], 32), - None, - "exceeds u32" - ); - assert_eq!(decode_nonce(&[0x80, 0x01], 7), None, "exceeds nonce_bits"); - assert_eq!(decode_nonce(&[0x7f], 7), Some((0x7f, 1))); - } } diff --git a/crates/jolt-transcript/src/lib.rs b/crates/jolt-transcript/src/lib.rs index b9e6b2e1f4..13fb78890b 100644 --- a/crates/jolt-transcript/src/lib.rs +++ b/crates/jolt-transcript/src/lib.rs @@ -1,15 +1,17 @@ -//! Fiat-Shamir transcripts for Jolt in the NARG model. +//! Fiat-Shamir transcripts for Jolt in the NARG model, on spongefish. //! -//! A proof is its argument string. [`ProverTranscript`] appends every prover -//! message and absorbs exactly the appended bytes; [`VerifierTranscript`] -//! reads messages back and absorbs exactly the bytes it read. Code that both +//! A proof is its argument string. [`ProverTranscript`] and +//! [`VerifierTranscript`] are typed layers over spongefish's `ProverState` and +//! `VerifierState`: the prover appends every message and absorbs its +//! encoding, the verifier reads it back and absorbs the same encoding. No +//! code reads or copies spongefish's sponge state. Code that both //! roles run identically is written against [`Channel`]. Protocols compose by //! sharing one transcript: a sub-protocol takes the caller's transcript rather //! than starting its own. //! //! Message atoms are the [`CanonicalBytes`](jolt_field::CanonicalBytes) / //! [`CanonicalDecode`](jolt_field::CanonicalDecode) codecs owned by each -//! type's crate. Challenges are exactly uniform ([`Channel::challenge`]) or +//! type's crate, whose spongefish `Encoding` / `NargDeserialize` they are. Challenges are exactly uniform ([`Channel::challenge`]) or //! drawn from a field's small challenge set ([`Channel::challenge_small`]). //! The sponge is a type parameter ([`Sponge`]) bound into the [`ProtocolId`]. @@ -31,26 +33,27 @@ )] mod channel; -mod duplex; mod error; +mod fork; mod grinding; +mod nonce; #[cfg(feature = "transcript-poseidon")] mod poseidon; -mod preview; mod protocol; mod prover; mod site; mod sponge; +mod state; mod verifier; pub use channel::Channel; -pub use duplex::SMALL_CHALLENGE_BYTES; pub use error::TranscriptError; +pub use fork::{Fork, FORK_SEED_LEN}; pub use grinding::{ grinding_predicate_accepts, GRINDING_NONCE_SLACK_BITS, GRINDING_PREDICATE_LEN, - MAX_GRINDING_BITS, + GRINDING_SEED_LEN, MAX_GRINDING_BITS, }; -pub use preview::Preview; +pub use nonce::Nonce; pub use protocol::{ProtocolId, PROTOCOL_ID_LEN}; pub use prover::ProverTranscript; #[cfg(feature = "logging")] @@ -64,6 +67,7 @@ pub use spongefish::instantiations::Keccak; /// The duplex interface every [`Sponge`] implements; re-exported so a custom /// sponge needs no direct spongefish dependency. pub use spongefish::DuplexSpongeInterface; +pub use state::SMALL_CHALLENGE_BYTES; pub use verifier::VerifierTranscript; #[cfg(feature = "transcript-poseidon")] diff --git a/crates/jolt-transcript/src/nonce.rs b/crates/jolt-transcript/src/nonce.rs new file mode 100644 index 0000000000..ba57d5cc2e --- /dev/null +++ b/crates/jolt-transcript/src/nonce.rs @@ -0,0 +1,86 @@ +//! The nonce message: a `u32` in canonical unsigned LEB128. + +use spongefish::{Encoding, NargDeserialize, VerificationError, VerificationResult}; + +/// Largest encoded nonce: a `u32` in 7-bit groups. +const NONCE_MAX_BYTES: usize = 5; + +/// A prover-chosen search counter (proof of work, fold response) as a prover +/// message: canonical unsigned LEB128, so small counters cost few bytes. The +/// encoding is little-endian base-128 groups with a continuation bit and no +/// redundant trailing zero group; any other byte string is rejected. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Nonce(pub u32); + +impl Nonce { + /// The encoded byte length. + #[must_use] + pub const fn encoded_len(self) -> usize { + let bits = u32::BITS - self.0.leading_zeros(); + if bits == 0 { + 1 + } else { + bits.div_ceil(7) as usize + } + } +} + +impl Encoding<[u8]> for Nonce { + fn encode(&self) -> impl AsRef<[u8]> { + let mut value = self.0; + let mut out = Vec::with_capacity(NONCE_MAX_BYTES); + loop { + let byte = (value & 0x7f) as u8; + value >>= 7; + if value == 0 { + out.push(byte); + return out; + } + out.push(byte | 0x80); + } + } +} + +impl NargDeserialize for Nonce { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + let mut value = 0u64; + for (index, &byte) in buf.iter().take(NONCE_MAX_BYTES).enumerate() { + let payload = byte & 0x7f; + value |= u64::from(payload) << (7 * index); + if byte & 0x80 == 0 { + if index != 0 && payload == 0 { + return Err(VerificationError); + } + let value = u32::try_from(value).map_err(|_| VerificationError)?; + *buf = buf.split_at(index + 1).1; + return Ok(Self(value)); + } + } + Err(VerificationError) + } +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + + fn decode(bytes: &[u8]) -> Option<(u32, usize)> { + let mut rest = bytes; + let nonce = Nonce::deserialize_from_narg(&mut rest).ok()?; + Some((nonce.0, bytes.len() - rest.len())) + } + + #[test] + fn nonce_codec_is_canonical() { + for value in [0, 1, 0x7f, 0x80, 0x3fff, 0x4000, u32::MAX] { + let encoded = Nonce(value).encode().as_ref().to_vec(); + assert_eq!(encoded.len(), Nonce(value).encoded_len()); + assert_eq!(decode(&encoded), Some((value, encoded.len()))); + } + assert_eq!(decode(&[0x80, 0x00]), None, "redundant zero group"); + assert_eq!(decode(&[0x80]), None, "truncated"); + assert_eq!(decode(&[0xff, 0xff, 0xff, 0xff, 0x1f]), None, "exceeds u32"); + assert_eq!(decode(&[0x7f, 0x01]).unwrap(), (0x7f, 1)); + } +} diff --git a/crates/jolt-transcript/src/preview.rs b/crates/jolt-transcript/src/preview.rs deleted file mode 100644 index 6462403154..0000000000 --- a/crates/jolt-transcript/src/preview.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Look-ahead over a copy of the public sponge state. - -use jolt_field::CanonicalBytes; - -use crate::grinding::encode_nonce; -use crate::Sponge; - -/// A detached copy of a transcript's sponge. -/// -/// A preview absorbs and squeezes exactly as the live transcript would, but it -/// holds no argument string and no reference to the transcript, so it cannot -/// change the proof or advance the live sponge. Proof-of-work searches use it -/// to test candidate nonces before committing the winner. -#[derive(Clone, Debug)] -pub struct Preview(H); - -impl Preview { - pub(crate) fn new(sponge: H) -> Self { - Self(sponge) - } - - /// Absorbs `value` as a prover message or public atom would be absorbed. - pub fn absorb(&mut self, value: &A) { - let _ = self.0.absorb(&value.to_bytes_le_vec()); - } - - /// Absorbs raw bytes, as an exact-length byte message would be absorbed. - pub fn absorb_bytes(&mut self, bytes: &[u8]) { - let _ = self.0.absorb(bytes); - } - - /// Absorbs `nonce` in the encoding [`send_nonce`](crate::ProverTranscript::send_nonce) uses. - pub fn absorb_nonce(&mut self, nonce: u32) { - let _ = self.0.absorb(encode_nonce(nonce).as_slice()); - } - - /// Squeezes `N` bytes, as a challenge would. - #[must_use] - pub fn squeeze(&mut self) -> [u8; N] { - let mut out = [0u8; N]; - let _ = self.0.squeeze(&mut out); - out - } -} diff --git a/crates/jolt-transcript/src/prover.rs b/crates/jolt-transcript/src/prover.rs index ac3a7e621a..528db6aec8 100644 --- a/crates/jolt-transcript/src/prover.rs +++ b/crates/jolt-transcript/src/prover.rs @@ -1,24 +1,34 @@ -//! The prover's end: absorb every message exactly as it is written. +//! The prover's end: spongefish's prover state, written through typed atoms. use std::num::NonZeroU8; use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; +use rand::rngs::StdRng; +use spongefish::ProverState; -use crate::duplex::Duplex; -use crate::grinding::{encode_nonce, nonce_bits, search_nonce, GRINDING_PREDICATE_LEN}; +use crate::grinding::{grind_nonce, nonce_bits, GRINDING_SEED_LEN}; +use crate::site::{Log, TranscriptOp}; +use crate::state::{domain, Framed, Squeeze, BYTE_BLOCK}; #[cfg(feature = "logging")] use crate::TranscriptEvent; -use crate::{Channel, Preview, ProtocolId, SiteId, Sponge, TranscriptError}; +use crate::{Channel, Nonce, ProtocolId, SiteId, Sponge, TranscriptError}; -/// Prover transcript: a sponge plus the argument string it writes. +/// Prover transcript: spongefish's [`ProverState`], which appends every prover +/// message to the argument string and absorbs exactly its encoding. /// -/// Every prover message is appended to the argument string and the appended -/// bytes are absorbed, so the bytes the verifier reads are exactly the bytes -/// that bound the following challenges. -#[derive(Clone, Debug)] -pub struct ProverTranscript { - duplex: Duplex, - narg: Vec, +/// Not `Clone`: copying a prover state lets a caller rewind the sponge and +/// re-draw challenges. +pub struct ProverTranscript { + state: ProverState, + log: Log, +} + +impl core::fmt::Debug for ProverTranscript { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ProverTranscript") + .field("narg_len", &self.state.narg_string().len()) + .finish_non_exhaustive() + } } impl ProverTranscript { @@ -26,39 +36,36 @@ impl ProverTranscript { #[must_use] pub fn new(protocol: &ProtocolId, session: &[u8]) -> Self { Self { - duplex: Duplex::new(protocol, session), - narg: Vec::new(), + state: domain(protocol, session).to_prover(H::default()), + log: Log::default(), } } /// Sends one atom. pub fn send(&mut self, value: &A) { - let start = self.narg.len(); - self.narg.resize(start + A::NUM_BYTES, 0); - value.to_bytes_le(self.narg.split_at_mut(start).1); - self.absorb_written(start); + let start = self.narg_len(); + self.state.prover_message(value); + self.record_message(start); } /// Sends atoms in order. pub fn send_all(&mut self, values: &[A]) { - let start = self.narg.len(); - self.narg.resize(start + A::NUM_BYTES * values.len(), 0); - for (value, out) in values.iter().zip( - self.narg - .split_at_mut(start) - .1 - .chunks_exact_mut(A::NUM_BYTES), - ) { - value.to_bytes_le(out); - } - self.absorb_written(start); + let start = self.narg_len(); + self.state.prover_messages(values); + self.record_message(start); } /// Sends bytes whose length the verifier already knows. pub fn send_bytes(&mut self, bytes: &[u8]) { - let start = self.narg.len(); - self.narg.extend_from_slice(bytes); - self.absorb_written(start); + let start = self.narg_len(); + let (blocks, rest) = bytes.as_chunks::(); + for block in blocks { + self.state.prover_message(block); + } + for byte in rest { + self.state.prover_message(&[*byte]); + } + self.record_message(start); } /// Sends a byte string of at most `max_len` bytes, prefixed by its `u32` @@ -81,13 +88,19 @@ impl ProverTranscript { Ok(()) } - /// Sends a nonce in its canonical variable-length encoding. + /// Sends a search counter as a [`Nonce`] message. pub fn send_nonce(&mut self, nonce: u32) { - self.send_bytes(encode_nonce(nonce).as_slice()); + let start = self.narg_len(); + self.state.prover_message(&Nonce(nonce)); + self.record_message(start); } - /// Grinds `bits` bits of proof of work and returns the committed nonce. - /// A zero difficulty sends nothing. + /// Grinds `bits` bits of proof of work and returns the sent nonce. A zero + /// difficulty draws and sends nothing. + /// + /// Squeezes a seed, searches nonces on [`Fork`](crate::Fork)s, then sends + /// the first accepted nonce as a [`Nonce`]; the protected challenge is + /// drawn after. /// /// # Errors /// @@ -99,39 +112,40 @@ impl ProverTranscript { return Ok(0); }; let nonce_bits = nonce_bits(bits).ok_or(TranscriptError::UnsupportedGrinding)?; - let nonce = search_nonce(bits, nonce_bits, |nonce| { - let mut preview = self.preview(); - preview.absorb_nonce(nonce); - preview.squeeze::() - }) - .ok_or(TranscriptError::GrindingExhausted)?; + let seed: [u8; GRINDING_SEED_LEN] = self.challenge_bytes(); + let nonce = + grind_nonce::(&seed, bits, nonce_bits).ok_or(TranscriptError::GrindingExhausted)?; self.send_nonce(nonce); - let _predicate: [u8; GRINDING_PREDICATE_LEN] = self.challenge_bytes(); Ok(nonce) } /// The argument string written so far. #[must_use] pub fn narg(&self) -> &[u8] { - &self.narg + self.state.narg_string() } /// Recorded operations, in order. #[cfg(feature = "logging")] #[must_use] pub fn events(&self) -> &[TranscriptEvent] { - self.duplex.events() + self.log.events() } /// Ends the transcript, returning the proof. #[must_use] pub fn finish(self) -> Vec { - self.narg + self.state.narg_string().to_vec() + } + + fn narg_len(&self) -> usize { + self.state.narg_string().len() } - fn absorb_written(&mut self, start: usize) { - let written = self.narg.split_at(start).1; - self.duplex.absorb_message(written, start); + fn record_message(&mut self, start: usize) { + let end = self.narg_len(); + self.log + .record(TranscriptOp::Message, end - start, Some(start..end)); } } @@ -139,19 +153,23 @@ impl Channel for ProverTranscript { type Sponge = H; fn site(&mut self, site: SiteId) { - self.duplex.set_site(site); + self.log.set_site(site); } fn public(&mut self, value: &A) { - self.duplex.absorb_public_atoms(std::slice::from_ref(value)); + self.state.public_message(value); + self.log.record(TranscriptOp::Public, A::NUM_BYTES, None); } fn public_all(&mut self, values: &[A]) { - self.duplex.absorb_public_atoms(values); + self.state.public_messages(values); + self.log + .record(TranscriptOp::Public, A::NUM_BYTES * values.len(), None); } fn public_bytes(&mut self, bytes: &[u8]) { - self.duplex.absorb_public_framed(bytes); + self.state.public_message(&Framed(bytes)); + self.log.record(TranscriptOp::Public, 8 + bytes.len(), None); } fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError> { @@ -168,18 +186,21 @@ impl Channel for ProverTranscript { } fn challenge(&mut self) -> F { - self.duplex.challenge() + let (value, squeezed) = self.state.exact_challenge(); + self.log.record(TranscriptOp::Challenge, squeezed, None); + value } fn challenge_small(&mut self) -> F { - self.duplex.challenge_small() + let value = self.state.small_challenge(); + self.log + .record(TranscriptOp::Challenge, crate::SMALL_CHALLENGE_BYTES, None); + value } fn challenge_bytes(&mut self) -> [u8; N] { - self.duplex.squeeze_array() - } - - fn preview(&self) -> Preview { - self.duplex.preview() + let value = self.state.squeeze_array(); + self.log.record(TranscriptOp::Challenge, N, None); + value } } diff --git a/crates/jolt-transcript/src/state.rs b/crates/jolt-transcript/src/state.rs new file mode 100644 index 0000000000..8b5e5bfdf7 --- /dev/null +++ b/crates/jolt-transcript/src/state.rs @@ -0,0 +1,132 @@ +//! Operations shared by both roles' spongefish states. + +use jolt_field::{CanonicalEncoding, Field}; +use rand_core::RngCore; +use spongefish::{Encoding, ProverState, VerifierState}; + +use crate::{ProtocolId, Sponge}; + +/// Bytes squeezed for a [`Channel::challenge_small`](crate::Channel::challenge_small). +pub const SMALL_CHALLENGE_BYTES: usize = 16; + +/// Width of the squeeze blocks a challenge is drawn in. +const SQUEEZE_BLOCK: usize = 32; + +/// Width of the blocks a known-length byte message is sent and read in. Both +/// roles split identically, then the remainder byte by byte, so every sponge +/// sees the same absorb calls on both sides. +pub(crate) const BYTE_BLOCK: usize = 32; + +/// A byte string absorbed as `LE64(len) || bytes`, so consecutive +/// variable-length values cannot run into each other. +pub(crate) struct Framed<'a>(pub(crate) &'a [u8]); + +impl Encoding<[u8]> for Framed<'_> { + fn encode(&self) -> impl AsRef<[u8]> { + let mut out = Vec::with_capacity(8 + self.0.len()); + out.extend_from_slice(&(self.0.len() as u64).to_le_bytes()); + out.extend_from_slice(self.0); + out + } +} + +/// The spongefish domain separator every Jolt-family transcript starts from: +/// the protocol id, then the framed session, then an empty framed instance. +pub(crate) fn domain<'s>( + protocol: &ProtocolId, + session: &'s [u8], +) -> spongefish::DomainSeparator< + spongefish::WithInstance>, + spongefish::WithSession>, +> { + spongefish::DomainSeparator::new(*protocol.as_bytes()) + .session(Framed(session)) + .instance(Framed(&[])) +} + +/// The verifier-message (squeeze) half of a spongefish state. +pub(crate) trait Squeeze { + /// Squeezes exactly `out.len()` bytes of the byte stream. + fn squeeze_into(&mut self, out: &mut [u8]); + + fn squeeze_array(&mut self) -> [u8; N] { + let mut out = [0u8; N]; + self.squeeze_into(&mut out); + out + } + + /// An exactly uniform element of `F`, per [`Field::random`]'s + /// rejection-sampling contract over the squeezed byte stream. Returns the + /// element and the number of bytes squeezed. + fn exact_challenge(&mut self) -> (F, usize) + where + Self: Sized, + { + let mut rng = SqueezeRng { + state: self, + squeezed: 0, + }; + let value = F::random(&mut rng); + (value, rng.squeezed) + } + + fn small_challenge(&mut self) -> F { + F::from_challenge_bytes(&self.squeeze_array::()) + } +} + +/// Squeezes `out` in whole blocks, then single bytes; both roles squeeze +/// through here, so they make the same squeeze calls. +macro_rules! squeeze_blocks { + ($state:expr, $out:expr) => {{ + let mut blocks = $out.chunks_exact_mut(SQUEEZE_BLOCK); + for block in &mut blocks { + block.copy_from_slice(&$state.verifier_message::<[u8; SQUEEZE_BLOCK]>()); + } + for byte in blocks.into_remainder() { + [*byte] = $state.verifier_message::<[u8; 1]>(); + } + }}; +} + +impl Squeeze for ProverState { + fn squeeze_into(&mut self, out: &mut [u8]) { + squeeze_blocks!(self, out); + } +} + +impl Squeeze for VerifierState<'_, H> { + fn squeeze_into(&mut self, out: &mut [u8]) { + squeeze_blocks!(self, out); + } +} + +/// Feeds squeezed bytes to [`Field::random`]. +struct SqueezeRng<'a, S> { + state: &'a mut S, + squeezed: usize, +} + +impl RngCore for SqueezeRng<'_, S> { + fn next_u32(&mut self) -> u32 { + let mut bytes = [0u8; 4]; + self.fill_bytes(&mut bytes); + u32::from_le_bytes(bytes) + } + + fn next_u64(&mut self) -> u64 { + let mut bytes = [0u8; 8]; + self.fill_bytes(&mut bytes); + u64::from_le_bytes(bytes) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.state.squeeze_into(dest); + self.squeezed += dest.len(); + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } +} diff --git a/crates/jolt-transcript/src/verifier.rs b/crates/jolt-transcript/src/verifier.rs index edd2eda115..3727a55df8 100644 --- a/crates/jolt-transcript/src/verifier.rs +++ b/crates/jolt-transcript/src/verifier.rs @@ -1,29 +1,40 @@ -//! The verifier's end: read every message from the argument string, absorb -//! exactly the bytes read. +//! The verifier's end: spongefish's verifier state, read through typed atoms. use std::num::NonZeroU8; use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; +use spongefish::VerifierState; -use crate::duplex::Duplex; -use crate::grinding::{ - decode_nonce, grinding_predicate_accepts, nonce_bits, GRINDING_PREDICATE_LEN, -}; +use crate::grinding::{grinding_accepts, nonce_bits, GRINDING_SEED_LEN}; +use crate::site::{Log, TranscriptOp}; +use crate::state::{domain, Framed, Squeeze, BYTE_BLOCK}; #[cfg(feature = "logging")] use crate::TranscriptEvent; -use crate::{Channel, Preview, ProtocolId, SiteId, Sponge, TranscriptError}; +use crate::{Channel, Nonce, ProtocolId, SiteId, Sponge, TranscriptError}; -/// Verifier transcript: a sponge plus a cursor over the proof bytes. +/// Verifier transcript: spongefish's [`VerifierState`] over the proof bytes. /// -/// The only way to obtain proof data is a `receive*` call, which absorbs the -/// bytes it consumed. After any failed receive the transcript is poisoned: -/// later receives fail and [`finish`](Self::finish) fails. -#[derive(Clone, Debug)] -pub struct VerifierTranscript<'a, H> { - duplex: Duplex, +/// The only way to obtain proof data is a `receive*` call, which reads and +/// absorbs through spongefish. This wrapper tracks the read offset itself so +/// it can bound a read before allocating and report typed errors. After any +/// failed receive the transcript is poisoned: later receives fail and +/// [`finish`](Self::finish) fails. +pub struct VerifierTranscript<'a, H: Sponge> { + state: VerifierState<'a, H>, narg: &'a [u8], consumed: usize, poisoned: bool, + log: Log, +} + +impl core::fmt::Debug for VerifierTranscript<'_, H> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("VerifierTranscript") + .field("consumed", &self.consumed) + .field("remaining", &self.remaining()) + .field("poisoned", &self.poisoned) + .finish_non_exhaustive() + } } impl<'a, H: Sponge> VerifierTranscript<'a, H> { @@ -31,10 +42,11 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { #[must_use] pub fn new(protocol: &ProtocolId, session: &[u8], narg: &'a [u8]) -> Self { Self { - duplex: Duplex::new(protocol, session), + state: domain(protocol, session).to_verifier(H::default(), narg), narg, consumed: 0, poisoned: false, + log: Log::default(), } } @@ -44,9 +56,9 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { /// /// [`TranscriptError::Truncated`] or [`TranscriptError::NonCanonical`]. pub fn receive(&mut self) -> Result { - let bytes = self.peek(A::NUM_BYTES)?; - let value = A::from_bytes_le_checked(bytes).ok_or(TranscriptError::NonCanonical); - let value = self.check(value)?; + self.bound(A::NUM_BYTES)?; + let value = self.state.prover_message::(); + let value = self.check(value.map_err(|_| TranscriptError::NonCanonical))?; self.advance(A::NUM_BYTES); Ok(value) } @@ -64,12 +76,9 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { .checked_mul(count) .ok_or(TranscriptError::Truncated); let len = self.check(len)?; - let bytes = self.peek(len)?; - let values = bytes - .chunks_exact(A::NUM_BYTES) - .map(|chunk| A::from_bytes_le_checked(chunk).ok_or(TranscriptError::NonCanonical)) - .collect::, _>>(); - let values = self.check(values)?; + self.bound(len)?; + let values = self.state.prover_messages_vec::(count); + let values = self.check(values.map_err(|_| TranscriptError::NonCanonical))?; self.advance(len); Ok(values) } @@ -80,7 +89,22 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { /// /// [`TranscriptError::Truncated`]. pub fn receive_bytes(&mut self, len: usize) -> Result<&'a [u8], TranscriptError> { - let bytes = self.peek(len)?; + self.bound(len)?; + let narg = self.narg; + let bytes = narg + .get(self.consumed..) + .and_then(|rest| rest.get(..len)) + .ok_or(TranscriptError::Truncated); + let bytes = self.check(bytes)?; + // The same block split the prover sends in, so both sides make the + // same absorb calls whatever the sponge's block handling. + let read = (0..len / BYTE_BLOCK) + .try_for_each(|_| self.state.prover_message::<[u8; BYTE_BLOCK]>().map(|_| ())) + .and_then(|()| { + (0..len % BYTE_BLOCK) + .try_for_each(|_| self.state.prover_message::<[u8; 1]>().map(|_| ())) + }); + self.check(read.map_err(|_| TranscriptError::Truncated))?; self.advance(len); Ok(bytes) } @@ -102,39 +126,45 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { self.receive_bytes(len) } - /// Receives a canonically encoded nonce below `2^nonce_bits`. + /// Receives a [`Nonce`] below `2^nonce_bits`. /// /// # Errors /// - /// [`TranscriptError::NonCanonical`] for a malformed, non-minimal, or - /// out-of-range encoding. + /// [`TranscriptError::NonCanonical`] for a malformed or non-minimal + /// encoding, [`TranscriptError::OutOfBounds`] for a value at or above + /// `2^nonce_bits`. pub fn receive_nonce(&mut self, nonce_bits: u8) -> Result { self.check(Ok(()))?; - let rest = self.narg.split_at(self.consumed).1; - let decoded = decode_nonce(rest, nonce_bits).ok_or(TranscriptError::NonCanonical); - let (nonce, len) = self.check(decoded)?; - self.advance(len); - Ok(nonce) + let nonce = self.state.prover_message::(); + let Nonce(nonce) = self.check(nonce.map_err(|_| TranscriptError::NonCanonical))?; + self.advance(Nonce(nonce).encoded_len()); + let in_range = if u64::from(nonce) >> nonce_bits == 0 { + Ok(nonce) + } else { + Err(TranscriptError::OutOfBounds) + }; + self.check(in_range) } /// Checks `bits` bits of proof of work and returns the received nonce. A - /// zero difficulty reads nothing. + /// zero difficulty draws and reads nothing. /// /// # Errors /// /// [`TranscriptError::UnsupportedGrinding`] above /// [`MAX_GRINDING_BITS`](crate::MAX_GRINDING_BITS), + /// [`TranscriptError::OutOfBounds`] for a nonce outside the search range, /// [`TranscriptError::GrindingRejected`] if the predicate fails, else as - /// [`receive_nonce`](Self::receive_nonce). + /// [`receive`](Self::receive). pub fn check_grind(&mut self, bits: u8) -> Result { let Some(bits) = NonZeroU8::new(bits) else { return Ok(0); }; let nonce_bits = nonce_bits(bits).ok_or(TranscriptError::UnsupportedGrinding); let nonce_bits = self.check(nonce_bits)?; + let seed: [u8; GRINDING_SEED_LEN] = self.challenge_bytes(); let nonce = self.receive_nonce(nonce_bits)?; - let predicate: [u8; GRINDING_PREDICATE_LEN] = self.challenge_bytes(); - let accepted = if grinding_predicate_accepts(&predicate, bits) { + let accepted = if grinding_accepts::(&seed, nonce, bits) { Ok(nonce) } else { Err(TranscriptError::GrindingRejected) @@ -142,6 +172,17 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { self.check(accepted) } + /// The proof bytes not yet received, without receiving or absorbing them. + /// + /// Only for parse-ahead by a component that needs a whole proof struct + /// before it runs (dory-pcs). Every byte it parses must still be received + /// through `receive*`, which is what binds it; parsed-ahead values carry + /// no weight until then. + #[must_use] + pub fn unread(&self) -> &'a [u8] { + self.narg.split_at(self.consumed).1 + } + /// Bytes of the proof not yet received. #[must_use] pub fn remaining(&self) -> usize { @@ -152,7 +193,7 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { #[cfg(feature = "logging")] #[must_use] pub fn events(&self) -> &[TranscriptEvent] { - self.duplex.events() + self.log.events() } /// Ends the transcript. Call once, at the outermost verifier boundary, @@ -166,31 +207,29 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { pub fn finish(self) -> Result<(), TranscriptError> { if self.poisoned { Err(TranscriptError::Poisoned) - } else if self.remaining() != 0 { - Err(TranscriptError::TrailingBytes) } else { - Ok(()) + self.state + .check_eof() + .map_err(|_| TranscriptError::TrailingBytes) } } - /// The next `len` unread bytes, without consuming them. - fn peek(&mut self, len: usize) -> Result<&'a [u8], TranscriptError> { - self.check(Ok(()))?; - let narg = self.narg; - let bytes = narg - .get(self.consumed..) - .and_then(|rest| rest.get(..len)) - .ok_or(TranscriptError::Truncated); - self.check(bytes) + /// Fails unless `len` more bytes remain. + fn bound(&mut self, len: usize) -> Result<(), TranscriptError> { + let fits = if len <= self.remaining() { + Ok(()) + } else { + Err(TranscriptError::Truncated) + }; + self.check(fits) } - /// Consumes and absorbs the next `len` bytes, which `peek` already bounded. + /// Records a read of `len` bytes that spongefish already absorbed. fn advance(&mut self, len: usize) { - let narg = self.narg; - let (_, rest) = narg.split_at(self.consumed); - let (bytes, _) = rest.split_at(len); - self.duplex.absorb_message(bytes, self.consumed); + let start = self.consumed; self.consumed += len; + self.log + .record(TranscriptOp::Message, len, Some(start..self.consumed)); } /// Poisons the transcript on failure; fails immediately once poisoned. @@ -206,19 +245,23 @@ impl Channel for VerifierTranscript<'_, H> { type Sponge = H; fn site(&mut self, site: SiteId) { - self.duplex.set_site(site); + self.log.set_site(site); } fn public(&mut self, value: &A) { - self.duplex.absorb_public_atoms(std::slice::from_ref(value)); + self.state.public_message(value); + self.log.record(TranscriptOp::Public, A::NUM_BYTES, None); } fn public_all(&mut self, values: &[A]) { - self.duplex.absorb_public_atoms(values); + self.state.public_messages(values); + self.log + .record(TranscriptOp::Public, A::NUM_BYTES * values.len(), None); } fn public_bytes(&mut self, bytes: &[u8]) { - self.duplex.absorb_public_framed(bytes); + self.state.public_message(&Framed(bytes)); + self.log.record(TranscriptOp::Public, 8 + bytes.len(), None); } fn exchange(&mut self, value: &mut A) -> Result<(), TranscriptError> { @@ -238,18 +281,21 @@ impl Channel for VerifierTranscript<'_, H> { } fn challenge(&mut self) -> F { - self.duplex.challenge() + let (value, squeezed) = self.state.exact_challenge(); + self.log.record(TranscriptOp::Challenge, squeezed, None); + value } fn challenge_small(&mut self) -> F { - self.duplex.challenge_small() + let value = self.state.small_challenge(); + self.log + .record(TranscriptOp::Challenge, crate::SMALL_CHALLENGE_BYTES, None); + value } fn challenge_bytes(&mut self) -> [u8; N] { - self.duplex.squeeze_array() - } - - fn preview(&self) -> Preview { - self.duplex.preview() + let value = self.state.squeeze_array(); + self.log.record(TranscriptOp::Challenge, N, None); + value } } diff --git a/crates/jolt-transcript/tests/known_answers.rs b/crates/jolt-transcript/tests/known_answers.rs index 4e322ad4cc..e2610ae164 100644 --- a/crates/jolt-transcript/tests/known_answers.rs +++ b/crates/jolt-transcript/tests/known_answers.rs @@ -18,7 +18,7 @@ fn run() -> (Vec, Vec) { prover.public_bytes(b"public input"); prover.public(&Fr::from(5u64)); prover.send(&Fr::from(7u64)); - prover.send_all(&[11u64, 13]); + prover.send_all(&[11u32, 13]); prover.send_bounded_bytes(b"variable", 16).unwrap(); challenges.extend(prover.challenge::().to_bytes_le_vec()); challenges.extend(prover.challenge_small::().to_bytes_le_vec()); @@ -31,7 +31,7 @@ fn run() -> (Vec, Vec) { verifier.public_bytes(b"public input"); verifier.public(&Fr::from(5u64)); assert_eq!(verifier.receive::().unwrap(), Fr::from(7u64)); - assert_eq!(verifier.receive_n::(2).unwrap(), vec![11, 13]); + assert_eq!(verifier.receive_n::(2).unwrap(), vec![11, 13]); assert_eq!(verifier.receive_bounded_bytes(16).unwrap(), b"variable"); replayed.extend(verifier.challenge::().to_bytes_le_vec()); replayed.extend(verifier.challenge_small::().to_bytes_le_vec()); @@ -56,8 +56,8 @@ fn assert_vectors(name: &str, (narg, challenges): (Vec, Vec), expected: assert_eq!(hex(&challenges), expected.1, "{name} challenges"); } -/// The argument string differs across sponges only in its grinding nonce. -const NARG_PREFIX: &str = "07000000000000000000000000000000000000000000000000000000000000000b000000000000000d00000000000000080000007661726961626c65"; +/// The argument string differs across sponges only in its `u32` grinding nonce. +const NARG_PREFIX: &str = "07000000000000000000000000000000000000000000000000000000000000000b0000000d000000080000007661726961626c65"; #[test] fn blake2b_known_answers() { @@ -65,8 +65,8 @@ fn blake2b_known_answers() { "blake2b", run::(), ( - &format!("{NARG_PREFIX}ad01"), - "3d4e089306afb69df886095eee15fbf14517c4ff924ec6bfd09265fc6a8a60054bb2626cc97cccb74e359d255207da9e39f4eee144c71404fe870f184ac4e31b422aaeb6c1a4720024b98c7f60fe220acf9f414231a6d2fd4dea709ae2c4eb14", + &format!("{NARG_PREFIX}78"), + "f14acf6a0e8500083aff06c75c0e690988c707180bdd447ee79c61ae1a7af622b4b8f1266fc13197f631ede2d2c8407a823d4388e79fba572325bdbd113cdf140edab4fa7e6bf134a286a792d365eea0671007657c372b32315757aca6c9990f", ), ); } @@ -77,8 +77,8 @@ fn keccak_known_answers() { "keccak", run::(), ( - &format!("{NARG_PREFIX}8101"), - "87b238e77e1881d46c35f627091e902ad1936d867b6c2fa7765547f44db00a230329d92cd97029872e99afbf8b9bdd57c59c13e503303e0c10e5a79fc642e11224bf861e6ff0b9bfa0c72d5ebcfe46562d97d675ea1b3814a87f1ebcaf118eb3", + &format!("{NARG_PREFIX}1f"), + "d8255bbe1b4ab7de6af9a08d7f52525044f0faa2d1f77159e6f5dbca1a897210b54afa2edf054d0a9e9d21f565d947dba3c96d3a662cbc5dc8f371998554c30845ffeaf9f80a897b327911715039879241ea7de1a6b98b5546c5f7fe87110275", ), ); } @@ -89,8 +89,8 @@ fn poseidon_known_answers() { "poseidon", run::(), ( - &format!("{NARG_PREFIX}49"), - "0be942b2a23ad3aa6fd8abf1580f85e78b2df6d4bf46d6564202c4f16cf2f022ce5db4a5e3f7288c0b37e0912b8214568272d5160be2592b4c87e9a74c374304fc9b0b01515cf511c4480ab9d67d3b1b88dba661536bf44d7b380cf04dd32604", + &format!("{NARG_PREFIX}0d"), + "6186b569421edf0a7490f9020cf5124637e90bcd2ff7c905ec433482548d541557d1d4a63d6ea9a8847ad61506d5ebba81550033c00c4991a4a586a3db288810bfbea94f9af0dd473dff8a154a7cc1f5e6f1f6dad5bfdbcf1025b3510cd8ab2a", ), ); } diff --git a/crates/jolt-transcript/tests/narg.rs b/crates/jolt-transcript/tests/narg.rs index 1253a4a8b6..38774cea2b 100644 --- a/crates/jolt-transcript/tests/narg.rs +++ b/crates/jolt-transcript/tests/narg.rs @@ -5,9 +5,10 @@ use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr, Ring}; use jolt_transcript::{ - Blake2b512, Channel, Keccak, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, - TranscriptError, VerifierTranscript, SMALL_CHALLENGE_BYTES, + Blake2b512, Channel, Keccak, Nonce, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, + TranscriptError, VerifierTranscript, GRINDING_NONCE_SLACK_BITS, SMALL_CHALLENGE_BYTES, }; +use spongefish::Encoding as _; const SESSION: &[u8] = b"narg-tests"; @@ -132,13 +133,13 @@ fn public_bytes_are_framed() { #[test] fn failed_receive_poisons_without_consuming() { let mut prover = ProverTranscript::::new(&protocol::(), SESSION); - prover.send(&7u64); + prover.send(&7u32); let narg = prover.finish(); let mut verifier = VerifierTranscript::::new(&protocol::(), SESSION, &narg); assert_eq!(verifier.receive::(), Err(TranscriptError::Truncated)); assert_eq!(verifier.remaining(), narg.len()); - assert_eq!(verifier.receive::(), Err(TranscriptError::Poisoned)); + assert_eq!(verifier.receive::(), Err(TranscriptError::Poisoned)); assert_eq!(verifier.finish(), Err(TranscriptError::Poisoned)); } @@ -179,20 +180,6 @@ fn bounds_are_checked_before_reading() { ); } -#[test] -fn preview_does_not_advance_the_transcript() { - let mut prover = ProverTranscript::::new(&protocol::(), SESSION); - let mut untouched = prover.clone(); - let mut preview = prover.preview(); - preview.absorb(&Fr::from_u64(1)); - let _ = preview.squeeze::<32>(); - assert_eq!( - prover.challenge_bytes::<32>(), - untouched.challenge_bytes::<32>() - ); - assert!(prover.narg().is_empty()); -} - #[test] fn grinding_rejects_a_stronger_claim_and_unsupported_difficulty() { let protocol = protocol::(); @@ -212,14 +199,53 @@ fn grinding_rejects_a_stronger_claim_and_unsupported_difficulty() { #[test] fn small_challenge_decodes_the_next_squeezed_bytes() { - let mut prover = ProverTranscript::::new(&protocol::(), SESSION); + let mut small = ProverTranscript::::new(&protocol::(), SESSION); + let mut raw = ProverTranscript::::new(&protocol::(), SESSION); for _ in 0..8 { - let expected = - Fr::from_challenge_bytes(&prover.preview().squeeze::()); - assert_eq!(prover.challenge_small::(), expected); + let expected = Fr::from_challenge_bytes(&raw.challenge_bytes::()); + assert_eq!(small.challenge_small::(), expected); } } +/// A grind squeezes a seed and sends a `u32` nonce; the verifier rejects a +/// nonce outside the search range and a nonce that fails the predicate. +#[test] +fn grinding_rejects_out_of_range_and_failing_nonces() { + let protocol = protocol::(); + let bits = 6; + let mut prover = ProverTranscript::::new(&protocol, SESSION); + let nonce = prover.grind(bits).unwrap(); + let honest = prover.finish(); + assert_eq!(honest, Nonce(nonce).encode().as_ref()); + + let out_of_range = Nonce(1u32 << (bits + GRINDING_NONCE_SLACK_BITS)) + .encode() + .as_ref() + .to_vec(); + let mut verifier = VerifierTranscript::::new(&protocol, SESSION, &out_of_range); + assert_eq!( + verifier.check_grind(bits), + Err(TranscriptError::OutOfBounds) + ); + + let failing = Nonce((0..nonce).next().unwrap_or(nonce + 1)) + .encode() + .as_ref() + .to_vec(); + let mut verifier = VerifierTranscript::::new(&protocol, SESSION, &failing); + assert_eq!( + verifier.check_grind(bits), + Err(TranscriptError::GrindingRejected) + ); + + // The same nonce under a different seed (another session) is rejected. + let mut verifier = VerifierTranscript::::new(&protocol, b"other", &honest); + assert_eq!( + verifier.check_grind(bits), + Err(TranscriptError::GrindingRejected) + ); +} + #[test] fn exchange_is_send_for_the_prover_and_receive_for_the_verifier() { fn shared(channel: &mut C, value: &mut Fr) -> Fr { diff --git a/crates/jolt-verifier/src/stages/relations.rs b/crates/jolt-verifier/src/stages/relations.rs index 1098701a13..0ad9d06d49 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -624,8 +624,8 @@ pub(crate) mod test_transcript { let mut right = fresh(); let expected = documented(&mut right); assert_eq!( - left.preview().squeeze::<32>(), - right.preview().squeeze::<32>(), + left.challenge_bytes::<32>(), + right.challenge_bytes::<32>(), "production and documented draws diverge" ); (produced, expected) diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index dd9c85921f..fc62e78607 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -75,7 +75,10 @@ where &mut transcript, )? { VerifiedStages::Clear => {} - VerifiedStages::Zk(blindfold) => { + VerifiedStages::Zk { + protocol: blindfold, + checkpoint: _, + } => { transcript.site(BLINDFOLD); let vc_setup = preprocessing .vc_setup @@ -97,7 +100,14 @@ where #[cfg(not(feature = "akita"))] pub enum VerifiedStages { Clear, - Zk(Box>), + Zk { + protocol: Box>, + /// The 32 bytes squeezed after the stage spine, before BlindFold. + /// Every ZK transcript draws it; the prover compares it against its + /// replay of this function, which checks the replay reached the + /// prover's sponge state without reading that state. + checkpoint: [u8; 32], + }, } /// Runs the stage spine on `transcript`: the seeding messages, stages 1–8, @@ -180,21 +190,24 @@ where }; return Ok(VerifiedStages::Clear); } - Ok(VerifiedStages::Zk(Box::new(blindfold::build( - BlindFoldInputs { - checked: &checked, - preprocessing, - stage1: stage1.zk()?, - stage2: stage2.zk()?, - stage3: stage3.zk()?, - stage4: stage4.zk()?, - stage5: stage5.zk()?, - stage6a: stage6a.zk()?, - stage6b: stage6b.zk()?, - stage7: stage7.zk()?, - stage8: stage8.zk()?, - }, - )?))) + let protocol = Box::new(blindfold::build(BlindFoldInputs { + checked: &checked, + preprocessing, + stage1: stage1.zk()?, + stage2: stage2.zk()?, + stage3: stage3.zk()?, + stage4: stage4.zk()?, + stage5: stage5.zk()?, + stage6a: stage6a.zk()?, + stage6b: stage6b.zk()?, + stage7: stage7.zk()?, + stage8: stage8.zk()?, + })?); + let checkpoint = transcript.challenge_bytes(); + Ok(VerifiedStages::Zk { + protocol, + checkpoint, + }) } /// The Akita verification path: the same stage spine, with a random-selector diff --git a/crates/jolt-verifier/tests/completeness/zk.rs b/crates/jolt-verifier/tests/completeness/zk.rs index 37b094f7c9..bbc47368a5 100644 --- a/crates/jolt-verifier/tests/completeness/zk.rs +++ b/crates/jolt-verifier/tests/completeness/zk.rs @@ -78,7 +78,7 @@ fn blindfold_protocol(case: &ZkVerifierFixtureCase) -> BlindFoldProtocol::prove_batch::challenge_powers# jolt-openings::schemes.rs::HomomorphicBatch::prove_batch_zk::challenge_powers#0 jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::challenge_powers#1 jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::challenge_powers#0 +jolt-prover::blindfold.rs::prove_blindfold::challenge_bytes#1 jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::commit_round::challenge_small#1 jolt-sumcheck::prover.rs::prove_uniskip_clear::challenge_small#1 jolt-sumcheck::recorder.rs::ClearSumcheckRecorder::absorb_round::challenge_small#1 @@ -46,3 +47,4 @@ jolt-verifier::stages/stage8/verify.rs::verify::challenge_powers#0 jolt-verifier::stages/stage8/verify.rs::verify::challenge_powers#4 jolt-verifier::stages/uniskip.rs::draw_spartan_outer_tau::challenges_small#0 jolt-verifier::stages/uniskip.rs::draw_spartan_product_tau_high::challenge_small#0 +jolt-verifier::verifier.rs::verify_stages::challenge_bytes#10 diff --git a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory index 621fd0dbf2..4dd1bb01ee 100644 --- a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory @@ -53,12 +53,12 @@ jolt-dory::scheme.rs::DoryScheme::open::dory::prove::#1 -jolt-dory::scheme.rs::DoryScheme::verify::.clone#0 jolt-dory::scheme.rs::DoryScheme::verify::.finish#3 +jolt-dory::scheme.rs::DoryScheme::verify::.unread#0 jolt-dory::scheme.rs::DoryScheme::verify::DoryVerifierChannel::new#1 jolt-dory::scheme.rs::DoryScheme::verify::dory::verify::#2 -jolt-dory::scheme.rs::DoryScheme::verify_zk::.clone#0 jolt-dory::scheme.rs::DoryScheme::verify_zk::.finish#3 +jolt-dory::scheme.rs::DoryScheme::verify_zk::.unread#0 jolt-dory::scheme.rs::DoryScheme::verify_zk::DoryVerifierChannel::new#1 jolt-dory::scheme.rs::DoryScheme::verify_zk::dory::verify::#2 jolt-openings::schemes.rs::CommitmentGroupRole::new_indexed::Some#0 @@ -93,7 +93,6 @@ jolt-prover::akita/stage8.rs::prove_stage8::reduce_precommitted#2 jolt-prover::akita/stage8.rs::prove_stage8::reduce_precommitted#3 jolt-prover::akita/stage8.rs::reduce_precommitted::.reduce_claims#0 jolt-prover::blindfold.rs::prove_blindfold::.narg#0 -jolt-prover::blindfold.rs::prove_blindfold::.preview#1 jolt-prover::blindfold.rs::prove_blindfold::jolt_blindfold::prove::#2 jolt-prover::dory/prover.rs::prove::.finish#10 jolt-prover::dory/prover.rs::prove::blindfold::prove_blindfold::#9 @@ -145,6 +144,9 @@ jolt-sumcheck::recorder.rs::CommittedSumcheckRecorder<'_,F,VC,R>::finish::.finis jolt-sumcheck::verifier.rs::SumcheckVerifier::verify::receive_full_round#0 jolt-sumcheck::verifier.rs::SumcheckVerifier::verify_compressed::receive_compressed_round#0 jolt-verifier::proof.rs::JoltProof::header::ProofHeader::receive#0 +jolt-verifier::proof.rs::ProofHeader::receive::receive_u64#0 +jolt-verifier::proof.rs::ProofHeader::receive::receive_u64#1 +jolt-verifier::proof.rs::ProofHeader::receive::receive_u64#3 jolt-verifier::stages/stage1/verify.rs::verify::.draw_challenges#2 jolt-verifier::stages/stage1/verify.rs::verify::.verify_clear#3 jolt-verifier::stages/stage1/verify.rs::verify::.verify_zk#5 diff --git a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory index b498eef468..40a7159c97 100644 --- a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory @@ -43,7 +43,6 @@ jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_group::send_bytes#0::s jolt-dory::transcript.rs::DoryProverChannel<'_,H>::append_serde::send_bytes#0::self.transcript.send_bytes(&compressed(s)) jolt-dory::transcript.rs::DoryProverChannel<'a,H>::send_sigma1_responses::send_bytes#0::self.transcript.send_bytes(&compressed(response)) jolt-dory::transcript.rs::DoryVerifierChannel<'a,'p,H>::expect::receive_bytes#0::self.transcript.receive_bytes(bytes.len()) -jolt-dory::transcript.rs::read::receive_bytes#0::scout.receive_bytes(T::BYTES) jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::commit_round::send#0::transcript.send(&commitment) jolt-sumcheck::committed.rs::CommittedSumcheckBuilder<'a,F,VC,R>::finish::send#0::transcript.send(&VC::commit(self.setup,row,&blinding)) jolt-sumcheck::prover.rs::prove_uniskip_clear::send#2::transcript.send(&output_claim) @@ -62,16 +61,14 @@ jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#3::#[cfg(fea jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#4::PCS::send_commitment(&self.one_hot_trace,transcript) jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#5::#[cfg(feature="field-inline")]PCS::send_commitment(&self.field_inc,transcript) jolt-verifier::proof.rs::ProofCommitments::send::send_commitment#6::PCS::send_commitment(commitment,transcript) -jolt-verifier::proof.rs::ProofHeader::receive::receive#0::transcript.receive::() -jolt-verifier::proof.rs::ProofHeader::receive::receive#1::transcript.receive::() jolt-verifier::proof.rs::ProofHeader::receive::receive#2::transcript.receive() -jolt-verifier::proof.rs::ProofHeader::receive::receive#3::transcript.receive::() -jolt-verifier::proof.rs::ProofHeader::receive::receive#4::transcript.receive::() -jolt-verifier::proof.rs::ProofHeader::send::send#0::transcript.send(&num::u64_from_usize(self.trace_length)) -jolt-verifier::proof.rs::ProofHeader::send::send#1::transcript.send(&num::u64_from_usize(self.ram_K)) -jolt-verifier::proof.rs::ProofHeader::send::send#3::transcript.send(&self.trace_polynomial_order.transcript_scalar()) -jolt-verifier::proof.rs::ProofHeader::send::send#4::transcript.send(&u8::from(self.untrusted_advice)) +jolt-verifier::proof.rs::ProofHeader::receive::receive#4::transcript.receive::<[u8;1]>() +jolt-verifier::proof.rs::ProofHeader::send::send#0::transcript.send(&num::u64_from_usize(self.trace_length).to_le_bytes()) +jolt-verifier::proof.rs::ProofHeader::send::send#1::transcript.send(&num::u64_from_usize(self.ram_K).to_le_bytes()) +jolt-verifier::proof.rs::ProofHeader::send::send#3::transcript.send(&self.trace_polynomial_order.transcript_scalar().to_le_bytes(),) +jolt-verifier::proof.rs::ProofHeader::send::send#4::transcript.send(&[u8::from(self.untrusted_advice)]) jolt-verifier::proof.rs::ProofHeader::send::send_all#2::transcript.send_all(&[self.rw_config.ram_rw_phase1_num_rounds,self.rw_config.ram_rw_phase2_num_rounds,self.rw_config.registers_rw_phase1_num_rounds,self.rw_config.registers_rw_phase2_num_rounds,self.one_hot_config.log_k_chunk,self.one_hot_config.lookups_ra_virtual_log_k_chunk,]) +jolt-verifier::proof.rs::receive_u64::receive#0::transcript.receive() jolt-verifier::stages/relations.rs::receive_member_claims::receive#0::transcript.receive() jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheckStagedOpenings::receive::receive#0::transcript.receive() jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheckStagedOpenings::send::send_all#0::transcript.send_all(&self.opening_values()) diff --git a/crates/jolt-verifier/tests/support/narg.rs b/crates/jolt-verifier/tests/support/narg.rs index 707b65cb2a..bcbd96617e 100644 --- a/crates/jolt-verifier/tests/support/narg.rs +++ b/crates/jolt-verifier/tests/support/narg.rs @@ -345,7 +345,7 @@ mod dory { ) .and_then(|stages| match stages { VerifiedStages::Clear => Ok(()), - VerifiedStages::Zk(protocol) => { + VerifiedStages::Zk { protocol, .. } => { transcript.site(BLINDFOLD); let vc_setup = preprocessing .vc_setup diff --git a/scripts/check-no-yolocrypto.sh b/scripts/check-no-yolocrypto.sh new file mode 100755 index 0000000000..7991fc4530 --- /dev/null +++ b/scripts/check-no-yolocrypto.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Keeps spongefish's sponge state private (specs/jolt-transcript-spongefish-state.md, +# invariant 2). +# +# spongefish's `yolocrypto` feature makes `ProverState`/`VerifierState`'s +# duplex state public. Cargo unifies features across the graph, so one crate +# enabling it would expose the state to every crate. No crate in the +# workspace may enable it, under any feature combination, on any target. +export CARGO_TERM_COLOR=never + +features() { + cargo tree --locked --workspace --all-features --target all -e features \ + -i spongefish --prefix none --format '{p} {f}' 2>/dev/null +} + +graph=$(features) +# Self-test: the inverted tree must reach spongefish at all, or an empty +# result would make the check below vacuous. +if ! grep -Eq '^spongefish ' <<<"$graph"; then + echo "error: self-test failed: spongefish not found in the workspace graph" >&2 + exit 1 +fi +if grep -q 'yolocrypto' <<<"$graph"; then + echo "error: spongefish's yolocrypto feature is enabled:" >&2 + grep 'yolocrypto' <<<"$graph" >&2 + exit 1 +fi From 27267cdaedcced20f964fcd64c4fad27517bc7ad Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Mon, 5 Oct 2026 19:35:15 -0400 Subject: [PATCH 40/62] build(deps): pin akita to the transcript-unification branch Akita moves to LayerZero-Labs/akita c521f8f1, which runs on this branch's jolt-transcript; the root [patch] keeps one jolt-field, jolt-poly, and jolt-transcript. The root and standalone fuzz/consumer lockfiles are regenerated against the published revisions. --- Cargo.lock | 65 ++- Cargo.toml | 16 +- crates/jolt-crypto/fuzz/Cargo.lock | 392 ++++++++++++++-- crates/jolt-dory/fuzz/Cargo.lock | 18 +- crates/jolt-field/fuzz/Cargo.lock | 419 ++++++++++++++++-- crates/jolt-lookup-tables/fuzz/Cargo.lock | 417 +++++++++++++++-- crates/jolt-openings/fuzz/Cargo.lock | 18 +- crates/jolt-poly/fuzz/Cargo.lock | 385 ++++++++++++++-- crates/jolt-r1cs/fuzz/Cargo.lock | 17 +- crates/jolt-sumcheck/fuzz/Cargo.lock | 16 +- .../tests/external-consumer/Cargo.lock | 17 +- crates/jolt-transcript/fuzz/Cargo.lock | 16 +- crates/jolt-verifier/fuzz/Cargo.lock | 18 +- jolt-eval/fuzz/Cargo.lock | 159 ++----- 14 files changed, 1595 insertions(+), 378 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c5e313390..89b08300db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ [[package]] name = "akita-algebra" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-error", "akita-serialization", @@ -107,11 +107,11 @@ dependencies = [ [[package]] name = "akita-challenges" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-error", - "akita-transcript", "jolt-field", + "jolt-transcript", "keccak 0.1.6", "serde", "smallvec", @@ -120,13 +120,12 @@ dependencies = [ [[package]] name = "akita-config" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-challenges", "akita-error", "akita-schedules", "akita-serialization", - "akita-transcript", "akita-types", "jolt-field", ] @@ -134,7 +133,7 @@ dependencies = [ [[package]] name = "akita-cpu-backend" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-algebra", "akita-challenges", @@ -143,7 +142,6 @@ dependencies = [ "akita-prover", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", @@ -154,15 +152,16 @@ dependencies = [ [[package]] name = "akita-error" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ + "jolt-transcript", "thiserror 2.0.20", ] [[package]] name = "akita-pcs" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-algebra", "akita-challenges", @@ -172,10 +171,10 @@ dependencies = [ "akita-prover", "akita-serialization", "akita-setup", - "akita-transcript", "akita-types", "akita-verifier", "jolt-field", + "jolt-transcript", "rayon", "tracing", ] @@ -183,7 +182,7 @@ dependencies = [ [[package]] name = "akita-planner" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-challenges", "akita-error", @@ -195,7 +194,7 @@ dependencies = [ [[package]] name = "akita-prover" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-algebra", "akita-challenges", @@ -203,17 +202,17 @@ dependencies = [ "akita-error", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", + "jolt-transcript", "tracing", ] [[package]] name = "akita-schedules" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-challenges", "akita-error", @@ -225,7 +224,7 @@ dependencies = [ [[package]] name = "akita-serialization" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "jolt-field", "jolt-poly", @@ -235,7 +234,7 @@ dependencies = [ [[package]] name = "akita-setup" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-config", "akita-cpu-backend", @@ -249,39 +248,30 @@ dependencies = [ [[package]] name = "akita-sumcheck" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-algebra", "akita-error", "akita-serialization", - "akita-transcript", "jolt-field", "jolt-poly", -] - -[[package]] -name = "akita-transcript" -version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" -dependencies = [ - "jolt-field", - "spongefish", + "jolt-transcript", ] [[package]] name = "akita-types" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-algebra", "akita-challenges", "akita-error", "akita-serialization", "akita-sumcheck", - "akita-transcript", "blake2 0.11.0", "jolt-field", "jolt-poly", + "jolt-transcript", "rand_core 0.6.4", "serde", "sha3 0.12.0", @@ -292,7 +282,7 @@ dependencies = [ [[package]] name = "akita-verifier" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=1cc7a2a90774d4d402f201701d08158edc106d67#1cc7a2a90774d4d402f201701d08158edc106d67" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a#c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a" dependencies = [ "akita-algebra", "akita-challenges", @@ -300,10 +290,10 @@ dependencies = [ "akita-error", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", + "jolt-transcript", "rayon", "tracing", ] @@ -3152,6 +3142,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "spongefish", "tracing", ] @@ -3226,6 +3217,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "spongefish", ] [[package]] @@ -3253,6 +3245,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "spongefish", "tracing", ] @@ -3293,7 +3286,6 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "spongefish", "tempfile", "tracer", "tracing", @@ -3328,6 +3320,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "spongefish", "thiserror 2.0.20", ] @@ -3562,6 +3555,7 @@ dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.20", "tracing", ] @@ -3765,6 +3759,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.20", "tracing", ] @@ -3793,12 +3788,9 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ff 0.5.0", - "blake2 0.11.0", "criterion", - "digest 0.11.3", "jolt-field", "light-poseidon", - "num-traits", "rand 0.8.7", "rand_core 0.6.4", "spongefish", @@ -6449,8 +6441,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2 0.11.0", "digest 0.11.3", "keccak 0.2.0", @@ -7872,6 +7862,7 @@ dependencies = [ "regex", "serde", "serde_json", + "spongefish", "strum 0.28.0", "tracer", ] diff --git a/Cargo.toml b/Cargo.toml index e23d73cce0..21bf1b052a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -301,14 +301,14 @@ dory = { package = "dory-pcs", version = "0.4.2", features = [ "zk", ] } # Immutable upstream revision containing the quotient-free relation and external schedule catalogs. -akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } -akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "1cc7a2a90774d4d402f201701d08158edc106d67", default-features = false } +akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } +akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "c521f8f1b505beb9b5f5aacc0ecb86a7c70fce0a", default-features = false } # Core Utilities clap = { version = "4.6.1", features = ["derive"] } diff --git a/crates/jolt-crypto/fuzz/Cargo.lock b/crates/jolt-crypto/fuzz/Cargo.lock index e9a1054df1..dacf489d1f 100644 --- a/crates/jolt-crypto/fuzz/Cargo.lock +++ b/crates/jolt-crypto/fuzz/Cargo.lock @@ -50,8 +50,8 @@ dependencies = [ "educe", "fnv", "hashbrown", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-integer", "num-traits", "zeroize", @@ -67,10 +67,10 @@ dependencies = [ "ark-serialize", "ark-std", "arrayvec", - "digest", + "digest 0.10.7", "educe", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-traits", "paste", "zeroize", @@ -90,7 +90,7 @@ name = "ark-ff-macros" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-traits", "proc-macro2", "quote", @@ -119,8 +119,8 @@ dependencies = [ "ark-serialize-derive", "ark-std", "arrayvec", - "digest", - "num-bigint", + "digest 0.10.7", + "num-bigint 0.4.8", ] [[package]] @@ -140,7 +140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand", + "rand 0.8.7", ] [[package]] @@ -175,6 +175,15 @@ dependencies = [ "virtue", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cc" version = "1.3.0" @@ -193,6 +202,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -228,13 +252,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -297,6 +341,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -317,6 +372,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "itertools" version = "0.13.0" @@ -326,6 +390,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -338,7 +411,7 @@ version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom", + "getrandom 0.4.3", "libc", ] @@ -353,12 +426,13 @@ dependencies = [ "ark-std", "jolt-field", "jolt-poly", - "num-bigint", + "num-bigint 0.4.8", "num-integer", "num-traits", - "rand_core", + "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -370,7 +444,7 @@ dependencies = [ "jolt-field", "libfuzzer-sys", "rand_chacha", - "rand_core", + "rand_core 0.6.4", "serde_json", ] @@ -383,8 +457,9 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -395,7 +470,7 @@ dependencies = [ "jolt-field", "jolt-utils", "num-traits", - "rand_core", + "rand_core 0.6.4", "rayon", "serde", "thiserror", @@ -409,6 +484,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.189" @@ -425,6 +510,15 @@ dependencies = [ "cc", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "memchr" version = "2.8.3" @@ -441,6 +535,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -465,6 +569,171 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cbc82137c157673e2d33d95f936a94ce237a790d46e0a2b3165dccfc385dd6" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "spin", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + [[package]] name = "paste" version = "1.0.15" @@ -516,8 +785,18 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ + "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -527,7 +806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -535,6 +814,15 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" @@ -556,6 +844,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -599,12 +893,54 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spongefish" +version = "0.7.4" +source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" +dependencies = [ + "digest 0.11.3", + "p3-koala-bear", + "rand 0.8.7", + "sha2", + "sha3", +] + [[package]] name = "syn" version = "2.0.119" @@ -705,6 +1041,12 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "zerocopy" version = "0.8.55" @@ -750,17 +1092,3 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - - - - - - - - - - - - - - diff --git a/crates/jolt-dory/fuzz/Cargo.lock b/crates/jolt-dory/fuzz/Cargo.lock index 69d841e870..f83f9e9bf8 100644 --- a/crates/jolt-dory/fuzz/Cargo.lock +++ b/crates/jolt-dory/fuzz/Cargo.lock @@ -555,6 +555,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -573,6 +574,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -604,6 +606,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -660,6 +663,7 @@ dependencies = [ "ark-ff", "jolt-field", "light-poseidon", + "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -1345,17 +1349,3 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - - - - - - - - - - - - - - diff --git a/crates/jolt-field/fuzz/Cargo.lock b/crates/jolt-field/fuzz/Cargo.lock index 25007c085e..7c017ddc27 100644 --- a/crates/jolt-field/fuzz/Cargo.lock +++ b/crates/jolt-field/fuzz/Cargo.lock @@ -50,8 +50,8 @@ dependencies = [ "educe", "fnv", "hashbrown", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-integer", "num-traits", "zeroize", @@ -67,10 +67,10 @@ dependencies = [ "ark-serialize", "ark-std", "arrayvec", - "digest", + "digest 0.10.7", "educe", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-traits", "paste", "zeroize", @@ -90,7 +90,7 @@ name = "ark-ff-macros" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-traits", "proc-macro2", "quote", @@ -119,8 +119,8 @@ dependencies = [ "ark-serialize-derive", "ark-std", "arrayvec", - "digest", - "num-bigint", + "digest 0.10.7", + "num-bigint 0.4.8", ] [[package]] @@ -140,7 +140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand", + "rand 0.8.7", ] [[package]] @@ -155,6 +155,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cc" version = "1.4.0" @@ -173,6 +182,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -183,13 +207,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -252,6 +296,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -272,6 +327,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "itertools" version = "0.13.0" @@ -281,13 +345,22 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "jobserver" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom", + "getrandom 0.4.3", "libc", ] @@ -300,8 +373,9 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -311,10 +385,20 @@ version = "0.0.0" dependencies = [ "jolt-field", "libfuzzer-sys", - "num-bigint", + "num-bigint 0.4.8", "num-traits", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.189" @@ -331,6 +415,15 @@ dependencies = [ "cc", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -341,6 +434,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -365,12 +468,183 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cbc82137c157673e2d33d95f936a94ce237a790d46e0a2b3165dccfc385dd6" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "spin", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -410,8 +684,18 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ + "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -421,7 +705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -429,6 +713,21 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" @@ -460,12 +759,54 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spongefish" +version = "0.7.4" +source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" +dependencies = [ + "digest 0.11.3", + "p3-koala-bear", + "rand 0.8.7", + "sha2", + "sha3", +] + [[package]] name = "syn" version = "2.0.119" @@ -508,6 +849,34 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + [[package]] name = "typenum" version = "1.20.1" @@ -526,6 +895,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "zerocopy" version = "0.8.55" @@ -565,17 +940,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-lookup-tables/fuzz/Cargo.lock b/crates/jolt-lookup-tables/fuzz/Cargo.lock index 2e77bedcb0..eae535c45c 100644 --- a/crates/jolt-lookup-tables/fuzz/Cargo.lock +++ b/crates/jolt-lookup-tables/fuzz/Cargo.lock @@ -50,8 +50,8 @@ dependencies = [ "educe", "fnv", "hashbrown", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-integer", "num-traits", "zeroize", @@ -67,10 +67,10 @@ dependencies = [ "ark-serialize", "ark-std", "arrayvec", - "digest", + "digest 0.10.7", "educe", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-traits", "paste", "zeroize", @@ -90,7 +90,7 @@ name = "ark-ff-macros" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-traits", "proc-macro2", "quote", @@ -119,8 +119,8 @@ dependencies = [ "ark-serialize-derive", "ark-std", "arrayvec", - "digest", - "num-bigint", + "digest 0.10.7", + "num-bigint 0.4.8", ] [[package]] @@ -140,7 +140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand", + "rand 0.8.7", ] [[package]] @@ -155,6 +155,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cc" version = "1.3.0" @@ -173,6 +182,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -183,13 +207,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -252,6 +296,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -278,6 +333,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "itertools" version = "0.13.0" @@ -287,13 +351,22 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "jobserver" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom", + "getrandom 0.4.3", "libc", ] @@ -306,8 +379,9 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -340,6 +414,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.189" @@ -356,6 +440,15 @@ dependencies = [ "cc", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -366,6 +459,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -390,12 +493,183 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cbc82137c157673e2d33d95f936a94ce237a790d46e0a2b3165dccfc385dd6" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "spin", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -435,8 +709,18 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ + "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -446,7 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -454,6 +738,21 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" @@ -485,12 +784,54 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spongefish" +version = "0.7.4" +source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" +dependencies = [ + "digest 0.11.3", + "p3-koala-bear", + "rand 0.8.7", + "sha2", + "sha3", +] + [[package]] name = "strum" version = "0.28.0" @@ -554,6 +895,34 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + [[package]] name = "typenum" version = "1.20.1" @@ -572,6 +941,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "zerocopy" version = "0.8.55" @@ -611,17 +986,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-openings/fuzz/Cargo.lock b/crates/jolt-openings/fuzz/Cargo.lock index cd82d8d5f8..7e177180e9 100644 --- a/crates/jolt-openings/fuzz/Cargo.lock +++ b/crates/jolt-openings/fuzz/Cargo.lock @@ -529,6 +529,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -547,6 +548,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -561,6 +563,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -629,6 +632,7 @@ dependencies = [ "ark-ff", "jolt-field", "light-poseidon", + "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -1277,17 +1281,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-poly/fuzz/Cargo.lock b/crates/jolt-poly/fuzz/Cargo.lock index 886cec8a92..dcbf585951 100644 --- a/crates/jolt-poly/fuzz/Cargo.lock +++ b/crates/jolt-poly/fuzz/Cargo.lock @@ -50,8 +50,8 @@ dependencies = [ "educe", "fnv", "hashbrown", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-integer", "num-traits", "zeroize", @@ -67,10 +67,10 @@ dependencies = [ "ark-serialize", "ark-std", "arrayvec", - "digest", + "digest 0.10.7", "educe", - "itertools", - "num-bigint", + "itertools 0.13.0", + "num-bigint 0.4.8", "num-traits", "paste", "zeroize", @@ -90,7 +90,7 @@ name = "ark-ff-macros" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-traits", "proc-macro2", "quote", @@ -119,8 +119,8 @@ dependencies = [ "ark-serialize-derive", "ark-std", "arrayvec", - "digest", - "num-bigint", + "digest 0.10.7", + "num-bigint 0.4.8", ] [[package]] @@ -140,7 +140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "num-traits", - "rand", + "rand 0.8.7", ] [[package]] @@ -155,6 +155,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cc" version = "1.3.0" @@ -173,6 +182,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -208,13 +232,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -277,6 +321,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -297,6 +352,15 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "itertools" version = "0.13.0" @@ -306,13 +370,22 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "jobserver" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom", + "getrandom 0.4.3", "libc", ] @@ -325,8 +398,9 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -337,7 +411,7 @@ dependencies = [ "jolt-field", "jolt-utils", "num-traits", - "rand_core", + "rand_core 0.6.4", "rayon", "serde", "thiserror", @@ -360,6 +434,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.189" @@ -376,6 +460,15 @@ dependencies = [ "cc", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -386,6 +479,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -410,6 +513,171 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cbc82137c157673e2d33d95f936a94ce237a790d46e0a2b3165dccfc385dd6" +dependencies = [ + "p3-challenger", + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.3", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.3", + "serde", + "spin", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.3", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.3", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + [[package]] name = "paste" version = "1.0.15" @@ -461,8 +729,18 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ + "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -472,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -480,6 +758,15 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" @@ -501,6 +788,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -531,12 +824,54 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spongefish" +version = "0.7.4" +source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" +dependencies = [ + "digest 0.11.3", + "p3-koala-bear", + "rand 0.8.7", + "sha2", + "sha3", +] + [[package]] name = "syn" version = "2.0.119" @@ -625,6 +960,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "zerocopy" version = "0.8.55" @@ -664,17 +1005,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-r1cs/fuzz/Cargo.lock b/crates/jolt-r1cs/fuzz/Cargo.lock index cfce7591b5..a6f2a4194b 100644 --- a/crates/jolt-r1cs/fuzz/Cargo.lock +++ b/crates/jolt-r1cs/fuzz/Cargo.lock @@ -484,6 +484,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -497,6 +498,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -576,6 +578,7 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "jolt-field", + "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror", @@ -1195,17 +1198,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-sumcheck/fuzz/Cargo.lock b/crates/jolt-sumcheck/fuzz/Cargo.lock index 336c23219f..13c32a15cb 100644 --- a/crates/jolt-sumcheck/fuzz/Cargo.lock +++ b/crates/jolt-sumcheck/fuzz/Cargo.lock @@ -457,6 +457,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -506,6 +507,7 @@ dependencies = [ "ark-ff", "jolt-field", "light-poseidon", + "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -1135,17 +1137,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock index 10d2fea739..730d85afbe 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock @@ -117,6 +117,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -155,6 +156,7 @@ dependencies = [ "jolt-transcript", "num-traits", "rand_core 0.6.4", + "spongefish", ] [[package]] @@ -162,6 +164,7 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "jolt-field", + "rand 0.8.8", "rand_core 0.6.4", "spongefish", "thiserror", @@ -668,17 +671,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-transcript/fuzz/Cargo.lock b/crates/jolt-transcript/fuzz/Cargo.lock index 15fedafbbe..47bc7dda52 100644 --- a/crates/jolt-transcript/fuzz/Cargo.lock +++ b/crates/jolt-transcript/fuzz/Cargo.lock @@ -432,6 +432,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.20", ] @@ -443,6 +444,7 @@ dependencies = [ "ark-ff", "jolt-field", "light-poseidon", + "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.20", @@ -1053,17 +1055,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/crates/jolt-verifier/fuzz/Cargo.lock b/crates/jolt-verifier/fuzz/Cargo.lock index 39296d77c3..cabafc2fe3 100644 --- a/crates/jolt-verifier/fuzz/Cargo.lock +++ b/crates/jolt-verifier/fuzz/Cargo.lock @@ -630,6 +630,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -648,6 +649,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -662,6 +664,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -779,6 +782,7 @@ dependencies = [ "ark-ff", "jolt-field", "light-poseidon", + "rand 0.8.7", "rand_core 0.6.4", "spongefish", "thiserror 2.0.19", @@ -1526,17 +1530,3 @@ dependencies = [ "quote", "syn 2.0.119", ] - - - - - - - - - - - - - - diff --git a/jolt-eval/fuzz/Cargo.lock b/jolt-eval/fuzz/Cargo.lock index f6ae1726db..78477d2599 100644 --- a/jolt-eval/fuzz/Cargo.lock +++ b/jolt-eval/fuzz/Cargo.lock @@ -152,9 +152,9 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -163,10 +163,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", + "ark-ff", "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown 0.15.5", @@ -184,10 +184,10 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "allocative", - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "arrayvec", "digest 0.10.7", "educe", @@ -199,23 +199,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint 0.4.6", - "num-traits", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -225,16 +208,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.117", -] - [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -247,28 +220,15 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "ark-poly" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "educe", "fnv", "hashbrown 0.15.5", @@ -280,8 +240,8 @@ version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ "ark-ec", - "ark-ff 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-std", ] [[package]] @@ -289,26 +249,14 @@ name = "ark-serialize" version = "0.5.0" source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout#76bb3a4518928f1ff7f15875f940d614bb9845e6" dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", + "ark-serialize-derive", + "ark-std", "arrayvec", "digest 0.10.7", "num-bigint 0.4.6", "rayon", ] -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint 0.4.6", -] - [[package]] name = "ark-serialize-derive" version = "0.5.0" @@ -319,17 +267,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "ark-std" version = "0.5.0" @@ -341,16 +278,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -546,7 +473,7 @@ name = "common" version = "0.2.0" dependencies = [ "allocative", - "ark-serialize 0.5.0", + "ark-serialize", "serde", "syn 3.0.3", ] @@ -792,9 +719,9 @@ checksum = "065945e9fff3c7dac5d6f72f8260eac82ad577e7c7009b6290847d1252d14d78" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "bincode 1.3.3", "blake2 0.10.6", "digest 0.10.7", @@ -1244,18 +1171,18 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint 0.4.6", "num-integer", "num-traits", "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -1264,7 +1191,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -1274,6 +1201,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -1311,7 +1239,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "spongefish", "tempfile", "tracer", "tracing", @@ -1340,13 +1267,14 @@ name = "jolt-field" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", + "ark-ff", + "ark-serialize", "cc", "num-traits", "rand_core 0.6.4", "rayon", "serde", + "spongefish", "thiserror 2.0.18", ] @@ -1386,7 +1314,7 @@ dependencies = [ name = "jolt-inlines-secp256k1" version = "0.1.0" dependencies = [ - "ark-ff 0.5.0", + "ark-ff", "ark-secp256k1", "jolt-inlines-sdk", "num-bigint 0.4.6", @@ -1463,9 +1391,9 @@ source = "git+https://github.com/a16z/arkworks-algebra?branch=dev%2Ftwist-shout# dependencies = [ "ark-bn254", "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ff", + "ark-serialize", + "ark-std", "arrayvec", "num-bigint 0.4.6", "num-integer", @@ -1488,7 +1416,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -1502,7 +1429,7 @@ dependencies = [ name = "jolt-program" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "common", "jolt-riscv", "object", @@ -1555,7 +1482,7 @@ dependencies = [ name = "jolt-riscv" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "serde", "strum", "thiserror 2.0.18", @@ -1631,13 +1558,13 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", - "blake2 0.11.0-rc.6", - "digest 0.11.2", + "ark-ff", "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.18", ] [[package]] @@ -1652,7 +1579,7 @@ dependencies = [ name = "jolt-verifier" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "bincode 2.0.1", "blake2 0.11.0-rc.6", "common", @@ -1765,7 +1692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" dependencies = [ "ark-bn254", - "ark-ff 0.5.0", + "ark-ff", "num-bigint 0.4.6", "thiserror 1.0.69", ] @@ -2659,8 +2586,6 @@ name = "spongefish" version = "0.7.4" source = "git+https://github.com/arkworks-rs/spongefish?rev=ef9741346a150039427d9e0c02d6b2d73e93ec81#ef9741346a150039427d9e0c02d6b2d73e93ec81" dependencies = [ - "ark-ff 0.6.0", - "ark-serialize 0.6.0", "blake2 0.11.0-rc.6", "digest 0.11.2", "keccak", @@ -2818,7 +2743,7 @@ name = "tracer" version = "0.2.0" dependencies = [ "addr2line", - "ark-serialize 0.5.0", + "ark-serialize", "clap", "common", "derive_more", From a7bd787b41d4ebe5ca5265731b737a870a7b0d95 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:13 -0400 Subject: [PATCH 41/62] fix(transcript)!: squeeze only the uniform low half of each Poseidon 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. --- crates/jolt-transcript/src/poseidon.rs | 9 ++++++--- crates/jolt-transcript/tests/known_answers.rs | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/jolt-transcript/src/poseidon.rs b/crates/jolt-transcript/src/poseidon.rs index db452d8e27..db5aeb5ec3 100644 --- a/crates/jolt-transcript/src/poseidon.rs +++ b/crates/jolt-transcript/src/poseidon.rs @@ -8,8 +8,11 @@ //! //! Byte traffic is mapped to `Fr` via 31-byte little-endian chunks //! (`Fr::from_le_bytes_mod_order` is injective on chunks ≤ 31 bytes since -//! 248 bits < BN254 modulus). Squeezed bytes come from -//! `into_bigint().to_bytes_le()` of the running state. +//! 248 bits < BN254 modulus). Each permutation yields the low 16 bytes of +//! `into_bigint().to_bytes_le()` of the running state: a uniform element of +//! `[0, p)` reduced mod `2^128` is within `2^-125` of uniform, while its full +//! 32 bytes are not (the top byte is at most `0x30`), so challenges drawn at +//! any squeeze offset stay uniform. //! //! Round constants are built once per `PoseidonSponge` construction; the //! same `Poseidon` is reused for every `permute` call in this sponge's @@ -21,7 +24,7 @@ use ark_ff::{BigInteger, PrimeField, Zero}; use light_poseidon::{Poseidon, PoseidonHasher}; use spongefish::DuplexSpongeInterface; -const SQUEEZE_BYTES: usize = 32; +const SQUEEZE_BYTES: usize = 16; const ABSORB_CHUNK_BYTES: usize = 31; #[expect( diff --git a/crates/jolt-transcript/tests/known_answers.rs b/crates/jolt-transcript/tests/known_answers.rs index e2610ae164..b2058df38d 100644 --- a/crates/jolt-transcript/tests/known_answers.rs +++ b/crates/jolt-transcript/tests/known_answers.rs @@ -56,7 +56,7 @@ fn assert_vectors(name: &str, (narg, challenges): (Vec, Vec), expected: assert_eq!(hex(&challenges), expected.1, "{name} challenges"); } -/// The argument string differs across sponges only in its `u32` grinding nonce. +/// The argument string differs across sponges only in its grinding nonce. const NARG_PREFIX: &str = "07000000000000000000000000000000000000000000000000000000000000000b0000000d000000080000007661726961626c65"; #[test] @@ -89,8 +89,8 @@ fn poseidon_known_answers() { "poseidon", run::(), ( - &format!("{NARG_PREFIX}0d"), - "6186b569421edf0a7490f9020cf5124637e90bcd2ff7c905ec433482548d541557d1d4a63d6ea9a8847ad61506d5ebba81550033c00c4991a4a586a3db288810bfbea94f9af0dd473dff8a154a7cc1f5e6f1f6dad5bfdbcf1025b3510cd8ab2a", + &format!("{NARG_PREFIX}0f"), + "06667373737856e7bdd1796e22de9aa04c6ed6f0f25b3aeffc440008ac6acc2ea7cd1cc8aa62afa214666b1fc1f7547a8c0b471bfb8744ddcd96b061beeb872121dca3360f599c2057204f0f61ec2c603f42a92a9ec56dabf15c2a63bd7f7567", ), ); } From bc1da025584a1fa0f166f532ccb0c0b3f0ec20aa Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:13 -0400 Subject: [PATCH 42/62] fix(transcript): bind returned bytes to the sponge and harden nonce reads - 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. --- crates/jolt-transcript/src/error.rs | 4 ++ crates/jolt-transcript/src/fork.rs | 2 +- crates/jolt-transcript/src/grinding.rs | 7 ++-- crates/jolt-transcript/src/lib.rs | 5 +-- crates/jolt-transcript/src/nonce.rs | 2 +- crates/jolt-transcript/src/protocol.rs | 2 +- crates/jolt-transcript/src/prover.rs | 2 +- crates/jolt-transcript/src/state.rs | 47 +++++++++++++++++++++++- crates/jolt-transcript/src/verifier.rs | 51 ++++++++++++++++++++------ crates/jolt-transcript/tests/narg.rs | 4 +- 10 files changed, 102 insertions(+), 24 deletions(-) diff --git a/crates/jolt-transcript/src/error.rs b/crates/jolt-transcript/src/error.rs index da3e05ab92..b9a0c8f7ca 100644 --- a/crates/jolt-transcript/src/error.rs +++ b/crates/jolt-transcript/src/error.rs @@ -23,6 +23,10 @@ pub enum TranscriptError { /// Bytes remain after the protocol's last prover message. #[error("proof has trailing bytes")] TrailingBytes, + /// The bytes a read returned differ from the bytes the sponge absorbed: + /// an atom's decoder consumed other than its `NUM_BYTES`. + #[error("the transcript's byte cursor disagrees with the sponge's")] + CursorMismatch, /// The proof-of-work predicate rejected the received nonce. #[error("proof-of-work predicate rejected the nonce")] GrindingRejected, diff --git a/crates/jolt-transcript/src/fork.rs b/crates/jolt-transcript/src/fork.rs index 3b9d247cf6..232fd52415 100644 --- a/crates/jolt-transcript/src/fork.rs +++ b/crates/jolt-transcript/src/fork.rs @@ -14,7 +14,7 @@ const FORK_TAG: &[u8] = b"jolt-transcript/fork/v1"; /// /// A prover that must search (proof of work, or Fiat-Shamir with aborts) squeezes /// one [`FORK_SEED_LEN`]-byte seed, tries counters on forks, then sends the -/// accepted counter as a `u32` prover message. The verifier squeezes the same +/// accepted counter as a canonical LEB128 [`Nonce`](crate::Nonce) message. The verifier squeezes the same /// seed, receives the counter, and rebuilds the same fork. The seed binds the /// whole prechallenge transcript, and the counter is in the argument string, /// so nothing a fork derives can be chosen independently of the transcript. diff --git a/crates/jolt-transcript/src/grinding.rs b/crates/jolt-transcript/src/grinding.rs index a8a079e709..5d8e13977d 100644 --- a/crates/jolt-transcript/src/grinding.rs +++ b/crates/jolt-transcript/src/grinding.rs @@ -2,8 +2,9 @@ //! //! A grind squeezes a seed from the live transcript, then searches nonces off //! the transcript: a candidate passes when the first 32 bytes its -//! [`Fork`] squeezes start with `bits` zero bits, low bit first. The nonce is then sent as a `u32` prover -//! message, so the protected challenge drawn after it binds the solution. +//! [`Fork`] squeezes start with `bits` zero bits, low bit first. The nonce is then sent as a +//! canonical LEB128 [`Nonce`](crate::Nonce) message, so the protected challenge drawn after it +//! binds the solution. //! Nonces are searched over `bits + GRINDING_NONCE_SLACK_BITS` bits, so an //! honest search exhausts its range with probability about `exp(-2^7)`. @@ -18,7 +19,7 @@ pub const MAX_GRINDING_BITS: u8 = u32::BITS as u8 - GRINDING_NONCE_SLACK_BITS; /// Byte length of the squeezed proof-of-work predicate. pub const GRINDING_PREDICATE_LEN: usize = 32; /// Byte length of the seed a grind squeezes from the transcript. -pub const GRINDING_SEED_LEN: usize = crate::FORK_SEED_LEN; +pub(crate) const GRINDING_SEED_LEN: usize = crate::FORK_SEED_LEN; /// Returns whether the low `bits` bits of `predicate`, low byte first and low /// bit first within each byte, are all zero. #[must_use] diff --git a/crates/jolt-transcript/src/lib.rs b/crates/jolt-transcript/src/lib.rs index 13fb78890b..199e569032 100644 --- a/crates/jolt-transcript/src/lib.rs +++ b/crates/jolt-transcript/src/lib.rs @@ -51,10 +51,10 @@ pub use error::TranscriptError; pub use fork::{Fork, FORK_SEED_LEN}; pub use grinding::{ grinding_predicate_accepts, GRINDING_NONCE_SLACK_BITS, GRINDING_PREDICATE_LEN, - GRINDING_SEED_LEN, MAX_GRINDING_BITS, + MAX_GRINDING_BITS, }; pub use nonce::Nonce; -pub use protocol::{ProtocolId, PROTOCOL_ID_LEN}; +pub use protocol::ProtocolId; pub use prover::ProverTranscript; #[cfg(feature = "logging")] pub use site::TranscriptEvent; @@ -67,7 +67,6 @@ pub use spongefish::instantiations::Keccak; /// The duplex interface every [`Sponge`] implements; re-exported so a custom /// sponge needs no direct spongefish dependency. pub use spongefish::DuplexSpongeInterface; -pub use state::SMALL_CHALLENGE_BYTES; pub use verifier::VerifierTranscript; #[cfg(feature = "transcript-poseidon")] diff --git a/crates/jolt-transcript/src/nonce.rs b/crates/jolt-transcript/src/nonce.rs index ba57d5cc2e..072237a3d1 100644 --- a/crates/jolt-transcript/src/nonce.rs +++ b/crates/jolt-transcript/src/nonce.rs @@ -3,7 +3,7 @@ use spongefish::{Encoding, NargDeserialize, VerificationError, VerificationResult}; /// Largest encoded nonce: a `u32` in 7-bit groups. -const NONCE_MAX_BYTES: usize = 5; +pub(crate) const NONCE_MAX_BYTES: usize = 5; /// A prover-chosen search counter (proof of work, fold response) as a prover /// message: canonical unsigned LEB128, so small counters cost few bytes. The diff --git a/crates/jolt-transcript/src/protocol.rs b/crates/jolt-transcript/src/protocol.rs index ec68c556d5..113544dd8e 100644 --- a/crates/jolt-transcript/src/protocol.rs +++ b/crates/jolt-transcript/src/protocol.rs @@ -3,7 +3,7 @@ use crate::Sponge; /// Byte width of a [`ProtocolId`]. -pub const PROTOCOL_ID_LEN: usize = 64; +pub(crate) const PROTOCOL_ID_LEN: usize = 64; /// The 64-byte domain separator every transcript absorbs first. /// diff --git a/crates/jolt-transcript/src/prover.rs b/crates/jolt-transcript/src/prover.rs index 528db6aec8..81f289b54d 100644 --- a/crates/jolt-transcript/src/prover.rs +++ b/crates/jolt-transcript/src/prover.rs @@ -194,7 +194,7 @@ impl Channel for ProverTranscript { fn challenge_small(&mut self) -> F { let value = self.state.small_challenge(); self.log - .record(TranscriptOp::Challenge, crate::SMALL_CHALLENGE_BYTES, None); + .record(TranscriptOp::Challenge, crate::state::SMALL_CHALLENGE_BYTES, None); value } diff --git a/crates/jolt-transcript/src/state.rs b/crates/jolt-transcript/src/state.rs index 8b5e5bfdf7..c1243099ac 100644 --- a/crates/jolt-transcript/src/state.rs +++ b/crates/jolt-transcript/src/state.rs @@ -7,7 +7,7 @@ use spongefish::{Encoding, ProverState, VerifierState}; use crate::{ProtocolId, Sponge}; /// Bytes squeezed for a [`Channel::challenge_small`](crate::Channel::challenge_small). -pub const SMALL_CHALLENGE_BYTES: usize = 16; +pub(crate) const SMALL_CHALLENGE_BYTES: usize = 16; /// Width of the squeeze blocks a challenge is drawn in. const SQUEEZE_BLOCK: usize = 32; @@ -130,3 +130,48 @@ impl RngCore for SqueezeRng<'_, S> { Ok(()) } } + +#[cfg(test)] +#[expect( + clippy::indexing_slicing, + reason = "test squeezes stay inside their fixed buffer" +)] +mod tests { + use super::Squeeze; + use jolt_field::Fr; + + /// A squeeze stream that replays fixed bytes. + struct Replayed { + bytes: Vec, + position: usize, + } + + impl Squeeze for Replayed { + fn squeeze_into(&mut self, out: &mut [u8]) { + out.copy_from_slice(&self.bytes[self.position..self.position + out.len()]); + self.position += out.len(); + } + } + + /// A candidate at or above the modulus is rejected and the draw resamples + /// from the next 32 squeezed bytes, so it equals a draw from those bytes + /// alone. + #[test] + fn exact_challenge_rejects_out_of_range_candidates_and_resamples() { + let accepted: Vec = (1..=32).collect(); + let mut direct = Replayed { + bytes: accepted.clone(), + position: 0, + }; + let (expected, squeezed): (Fr, usize) = direct.exact_challenge(); + assert_eq!(squeezed, 32); + + let mut resampled = Replayed { + bytes: [vec![0xff; 32], accepted].concat(), + position: 0, + }; + let (value, squeezed): (Fr, usize) = resampled.exact_challenge(); + assert_eq!(squeezed, 64); + assert_eq!(value, expected); + } +} diff --git a/crates/jolt-transcript/src/verifier.rs b/crates/jolt-transcript/src/verifier.rs index 3727a55df8..f47a7d925b 100644 --- a/crates/jolt-transcript/src/verifier.rs +++ b/crates/jolt-transcript/src/verifier.rs @@ -6,6 +6,7 @@ use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; use spongefish::VerifierState; use crate::grinding::{grinding_accepts, nonce_bits, GRINDING_SEED_LEN}; +use crate::nonce::NONCE_MAX_BYTES; use crate::site::{Log, TranscriptOp}; use crate::state::{domain, Framed, Squeeze, BYTE_BLOCK}; #[cfg(feature = "logging")] @@ -97,14 +98,20 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { .ok_or(TranscriptError::Truncated); let bytes = self.check(bytes)?; // The same block split the prover sends in, so both sides make the - // same absorb calls whatever the sponge's block handling. - let read = (0..len / BYTE_BLOCK) - .try_for_each(|_| self.state.prover_message::<[u8; BYTE_BLOCK]>().map(|_| ())) - .and_then(|()| { - (0..len % BYTE_BLOCK) - .try_for_each(|_| self.state.prover_message::<[u8; 1]>().map(|_| ())) - }); - self.check(read.map_err(|_| TranscriptError::Truncated))?; + // same absorb calls whatever the sponge's block handling. The returned + // slice comes from this wrapper's cursor, so each block is compared + // with what spongefish read and absorbed: a cursor that drifted from + // spongefish's (an atom whose decode consumed other than `NUM_BYTES`) + // fails here rather than handing out unbound bytes. + let (blocks, rest) = bytes.as_chunks::(); + let mut read = Ok(()); + for block in blocks { + read = read.and_then(|()| self.read_matching(block)); + } + for byte in rest { + read = read.and_then(|()| self.read_matching(&[*byte])); + } + self.check(read)?; self.advance(len); Ok(bytes) } @@ -134,11 +141,24 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { /// encoding, [`TranscriptError::OutOfBounds`] for a value at or above /// `2^nonce_bits`. pub fn receive_nonce(&mut self, nonce_bits: u8) -> Result { - self.check(Ok(()))?; + // A proof that ends inside the nonce (no byte, or only continuation + // bytes) is truncated, not malformed. + let unread = self.unread(); + let truncated = + unread.len() < NONCE_MAX_BYTES && unread.iter().all(|byte| byte & 0x80 != 0); + self.check(if truncated { + Err(TranscriptError::Truncated) + } else { + Ok(()) + })?; let nonce = self.state.prover_message::(); let Nonce(nonce) = self.check(nonce.map_err(|_| TranscriptError::NonCanonical))?; self.advance(Nonce(nonce).encoded_len()); - let in_range = if u64::from(nonce) >> nonce_bits == 0 { + let in_range = if u64::from(nonce) + .checked_shr(u32::from(nonce_bits)) + .unwrap_or(0) + == 0 + { Ok(nonce) } else { Err(TranscriptError::OutOfBounds) @@ -214,6 +234,15 @@ impl<'a, H: Sponge> VerifierTranscript<'a, H> { } } + /// Reads one fixed-width byte message and requires it to equal `expected`. + fn read_matching(&mut self, expected: &[u8; N]) -> Result<(), TranscriptError> { + match self.state.prover_message::<[u8; N]>() { + Ok(read) if &read == expected => Ok(()), + Ok(_) => Err(TranscriptError::CursorMismatch), + Err(_) => Err(TranscriptError::Truncated), + } + } + /// Fails unless `len` more bytes remain. fn bound(&mut self, len: usize) -> Result<(), TranscriptError> { let fits = if len <= self.remaining() { @@ -289,7 +318,7 @@ impl Channel for VerifierTranscript<'_, H> { fn challenge_small(&mut self) -> F { let value = self.state.small_challenge(); self.log - .record(TranscriptOp::Challenge, crate::SMALL_CHALLENGE_BYTES, None); + .record(TranscriptOp::Challenge, crate::state::SMALL_CHALLENGE_BYTES, None); value } diff --git a/crates/jolt-transcript/tests/narg.rs b/crates/jolt-transcript/tests/narg.rs index 38774cea2b..3388621bae 100644 --- a/crates/jolt-transcript/tests/narg.rs +++ b/crates/jolt-transcript/tests/narg.rs @@ -6,7 +6,7 @@ use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr, Ring}; use jolt_transcript::{ Blake2b512, Channel, Keccak, Nonce, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, - TranscriptError, VerifierTranscript, GRINDING_NONCE_SLACK_BITS, SMALL_CHALLENGE_BYTES, + TranscriptError, VerifierTranscript, GRINDING_NONCE_SLACK_BITS, }; use spongefish::Encoding as _; @@ -202,7 +202,7 @@ fn small_challenge_decodes_the_next_squeezed_bytes() { let mut small = ProverTranscript::::new(&protocol::(), SESSION); let mut raw = ProverTranscript::::new(&protocol::(), SESSION); for _ in 0..8 { - let expected = Fr::from_challenge_bytes(&raw.challenge_bytes::()); + let expected = Fr::from_challenge_bytes(&raw.challenge_bytes::<16>()); assert_eq!(small.challenge_small::(), expected); } } From d9d3030dbfb34404a947f31b99578d6180570253 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:13 -0400 Subject: [PATCH 43/62] fix(verifier)!: send the proof header's widths as one message 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. --- crates/jolt-verifier/src/proof.rs | 4 +++- crates/jolt-verifier/src/stages/zk/outputs.rs | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/jolt-verifier/src/proof.rs b/crates/jolt-verifier/src/proof.rs index 03a7e776cc..df7b476728 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -63,7 +63,9 @@ impl ProofHeader { pub fn send(&self, transcript: &mut ProverTranscript) { transcript.send(&num::u64_from_usize(self.trace_length).to_le_bytes()); transcript.send(&num::u64_from_usize(self.ram_K).to_le_bytes()); - transcript.send_all(&[ + // One `[u8; 6]` message, matching `receive`: a length-binding sponge + // such as Poseidon absorbs six 1-byte messages differently. + transcript.send(&[ self.rw_config.ram_rw_phase1_num_rounds, self.rw_config.ram_rw_phase2_num_rounds, self.rw_config.registers_rw_phase1_num_rounds, diff --git a/crates/jolt-verifier/src/stages/zk/outputs.rs b/crates/jolt-verifier/src/stages/zk/outputs.rs index 965440eeb8..5daff2b049 100644 --- a/crates/jolt-verifier/src/stages/zk/outputs.rs +++ b/crates/jolt-verifier/src/stages/zk/outputs.rs @@ -17,7 +17,11 @@ pub struct CommittedOutputClaimShape { impl CommittedOutputClaimShape { /// `layout` packed `row_len` cells to a row /// ([`CheckedInputs::committed_row_len`]). + /// `row_len` must be nonzero; every caller passes the validated + /// [`CheckedInputs::committed_row_len`], which is at least the BlindFold + /// generator count. pub fn new(row_len: usize, layout: CommittedClaimLayout) -> Self { + debug_assert_ne!(row_len, 0, "committed output rows need a nonzero length"); Self { layout, row_len } } From 5aed5f604ba7d49db33eb409d33bf45a2cf4d461 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:13 -0400 Subject: [PATCH 44/62] fix(crypto): accept only the canonical encoding of the identity 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. --- crates/jolt-crypto/src/ec/bn254/mod.rs | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/jolt-crypto/src/ec/bn254/mod.rs b/crates/jolt-crypto/src/ec/bn254/mod.rs index cc5a656ee2..5e45871994 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -158,9 +158,18 @@ macro_rules! impl_jolt_group_wrapper { bytes.len() )); } - <$projective>::deserialize_compressed(bytes) + let point = <$projective>::deserialize_compressed(bytes) .map(Self) - .map_err(|error| error.to_string()) + .map_err(|error| error.to_string())?; + // arkworks returns the identity for any x under the infinity + // flag; only the canonical encoding may decode. + if ::to_bytes_le_vec(&point) != bytes { + return Err(format!( + "{} encoding is not canonical", + stringify!($wrapper) + )); + } + Ok(point) } } @@ -349,6 +358,7 @@ mod tests { use jolt_field::{CanonicalBytes, CanonicalDecode}; use super::{Bn254, Bn254G1, Bn254G2}; + use crate::JoltGroup; #[test] fn canonical_codec_is_the_compressed_encoding() { @@ -370,6 +380,24 @@ mod tests { check(g2, &g2.0); } + /// The identity decodes from its canonical encoding only: arkworks + /// ignores x under the infinity flag, so nonzero x bits there must not + /// alias it. + #[test] + fn identity_has_one_accepted_encoding() { + fn check(identity: P) { + let canonical = identity.to_bytes_le_vec(); + assert_eq!(P::from_bytes_le_checked(&canonical), Some(identity)); + let mut alias = canonical; + if let Some(low) = alias.first_mut() { + *low ^= 1; + } + assert_eq!(P::from_bytes_le_checked(&alias), None); + } + check(Bn254G1::identity()); + check(Bn254G2::identity()); + } + fn encode_with_trailing_byte(point: &P) -> Vec { let mut bytes = Vec::new(); point From 2fc2b2d7fa6f06820d126d39044c905cf5b9c700 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:13 -0400 Subject: [PATCH 45/62] refactor(blindfold): own every BlindFold message group in wire.rs 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. --- crates/jolt-blindfold/src/prove.rs | 23 ++++++-- crates/jolt-blindfold/src/verify.rs | 52 ++++++++--------- crates/jolt-blindfold/src/wire.rs | 56 ++++++++++++++++++- .../fs_inventory/challenge-sites.inventory | 6 +- .../fs_inventory/delegation-sites.inventory | 18 +++--- .../fs_inventory/message-sites.inventory | 18 +++--- 6 files changed, 121 insertions(+), 52 deletions(-) diff --git a/crates/jolt-blindfold/src/prove.rs b/crates/jolt-blindfold/src/prove.rs index d085a7cbbc..d50bb4d31a 100644 --- a/crates/jolt-blindfold/src/prove.rs +++ b/crates/jolt-blindfold/src/prove.rs @@ -7,7 +7,7 @@ use jolt_transcript::{Channel, ProverTranscript, Sponge}; use rand_core::RngCore; use rayon::prelude::*; -use crate::wire::{send_opening, FoldingCommitments}; +use crate::wire::{send_opening, FoldedEvaluations, FoldingCommitments, OuterClaims}; use crate::{BlindFoldProtocol, ProverError, WitnessCoordinate}; pub(crate) const OUTER_SUMCHECK_DEGREE: usize = 3; @@ -456,11 +456,19 @@ where "folded eval blindings", )?; - transcript.send_all(&folded_eval_outputs); - transcript.send_all(&folded_eval_blindings); + let folded_evaluations = FoldedEvaluations { + outputs: folded_eval_outputs, + blindings: folded_eval_blindings, + }; + folded_evaluations.send(transcript); for (index, (coordinates, (&folded_output, &folded_blinding))) in final_coordinates .iter() - .zip(folded_eval_outputs.iter().zip(&folded_eval_blindings)) + .zip( + folded_evaluations + .outputs + .iter() + .zip(&folded_evaluations.blindings), + ) .enumerate() { if let Some(coordinate) = coordinates.evaluation { @@ -539,8 +547,11 @@ where "folded error row opening", )?; - transcript.send_all(&[az_rx, bz_rx, cz_rx]); - send_opening(&error_opening, transcript); + OuterClaims { + abc: [az_rx, bz_rx, cz_rx], + error_opening, + } + .send(transcript); let ra: F = transcript.challenge_small(); let rb: F = transcript.challenge_small(); diff --git a/crates/jolt-blindfold/src/verify.rs b/crates/jolt-blindfold/src/verify.rs index 7247db7ca8..3036efb2b1 100644 --- a/crates/jolt-blindfold/src/verify.rs +++ b/crates/jolt-blindfold/src/verify.rs @@ -6,7 +6,7 @@ use jolt_sumcheck::{SumcheckClaim, SumcheckVerifier}; use jolt_transcript::{Channel, Sponge, VerifierTranscript}; use crate::prove::{INNER_SUMCHECK_DEGREE, OUTER_SUMCHECK_DEGREE}; -use crate::wire::{receive_opening, FoldingCommitments}; +use crate::wire::{receive_opening, FoldedEvaluations, FoldingCommitments, OuterClaims}; use crate::{ BlindFoldProtocol, RelaxedError, RelaxedInstance, VerificationError, WitnessCoordinate, }; @@ -99,12 +99,10 @@ where let outer = SumcheckVerifier::verify_compressed(&claim, transcript) .map_err(|source| VerificationError::OuterSumcheck { source })?; - let [az_rx, bz_rx, cz_rx]: [F; 3] = [ - transcript.receive()?, - transcript.receive()?, - transcript.receive()?, - ]; - let error_opening = receive_opening(error_row_len, transcript)?; + let OuterClaims { + abc: [az_rx, bz_rx, cz_rx], + error_opening, + } = OuterClaims::receive(error_row_len, transcript)?; let (row_point, entry_point) = outer.point.split_at(row_vars); let e_rx = VC::verify_committed_rows( @@ -143,8 +141,10 @@ where H: Sponge, { let eval_count = folded.eval_commitments.len(); - let folded_eval_outputs: Vec = transcript.receive_n(eval_count)?; - let folded_eval_blindings: Vec = transcript.receive_n(eval_count)?; + let FoldedEvaluations { + outputs: folded_eval_outputs, + blindings: folded_eval_blindings, + } = FoldedEvaluations::receive(eval_count, transcript)?; for (index, ((commitment, &output), &blinding)) in folded .eval_commitments .iter() @@ -571,11 +571,9 @@ mod tests { /// compressed coefficients (every coefficient but the linear one). struct Messages { folding: FoldingCommitments, - folded_eval_outputs: Vec, - folded_eval_blindings: Vec, + folded: FoldedEvaluations, outer_rounds: Vec>, - abc: [Fr; 3], - error_opening: VectorCommitmentOpening, + outer: OuterClaims, inner_rounds: Vec>, witness_opening: VectorCommitmentOpening, } @@ -595,11 +593,15 @@ mod tests { random_evals: vec![commit_value(setup, f(11), f(110)); eval_count], cross_term_error_rows: vec![identity(); dimensions.error.row_count], }, - folded_eval_outputs: vec![f(0); eval_count], - folded_eval_blindings: vec![f(0); eval_count], + folded: FoldedEvaluations { + outputs: vec![f(0); eval_count], + blindings: vec![f(0); eval_count], + }, outer_rounds: vec![vec![f(0); OUTER_SUMCHECK_DEGREE]; num_vars(dimensions.error)], - abc: [f(0); 3], - error_opening: opening(dimensions.error.row_len), + outer: OuterClaims { + abc: [f(0); 3], + error_opening: opening(dimensions.error.row_len), + }, inner_rounds: vec![vec![f(0); INNER_SUMCHECK_DEGREE]; num_vars(dimensions.witness)], witness_opening: opening(dimensions.witness.row_len), } @@ -617,21 +619,19 @@ mod tests { fn with_valid_eval_opening(mut self) -> Self { let folding_challenge = self.folding_challenge(); - self.folded_eval_outputs = vec![f(7) + folding_challenge * f(11)]; - self.folded_eval_blindings = vec![f(70) + folding_challenge * f(110)]; + self.folded.outputs = vec![f(7) + folding_challenge * f(11)]; + self.folded.blindings = vec![f(70) + folding_challenge * f(110)]; self } fn narg(&self) -> Vec { let mut transcript = Self::transcript(); self.folding.send(&mut transcript); - transcript.send_all(&self.folded_eval_outputs); - transcript.send_all(&self.folded_eval_blindings); + self.folded.send(&mut transcript); for round in &self.outer_rounds { transcript.send_all(round); } - transcript.send_all(&self.abc); - send_opening(&self.error_opening, &mut transcript); + self.outer.send(&mut transcript); for round in &self.inner_rounds { transcript.send_all(round); } @@ -714,7 +714,7 @@ mod tests { let setup = setup(); let protocol = protocol_with_eval(&setup); let mut messages = Messages::zero(&setup, &protocol).with_valid_eval_opening(); - messages.folded_eval_outputs[0] += f(1); + messages.folded.outputs[0] += f(1); let error = verify(&protocol, &setup, &messages.narg()) .expect_err("folded eval commitment opening is wrong"); @@ -730,7 +730,7 @@ mod tests { let setup = setup(); let protocol = outer_round_protocol(); let mut messages = Messages::zero(&setup, &protocol); - messages.error_opening.combined_blinding = f(1); + messages.outer.error_opening.combined_blinding = f(1); let error = verify(&protocol, &setup, &messages.narg()) .expect_err("error opening is not binding to folded rows"); @@ -746,7 +746,7 @@ mod tests { let setup = setup(); let protocol = outer_round_protocol(); let mut messages = Messages::zero(&setup, &protocol); - messages.abc = [f(1), f(1), f(0)]; + messages.outer.abc = [f(1), f(1), f(0)]; let error = verify(&protocol, &setup, &messages.narg()) .expect_err("outer final claim does not match opened error row"); diff --git a/crates/jolt-blindfold/src/wire.rs b/crates/jolt-blindfold/src/wire.rs index 0dad504e4e..e1bb6581f5 100644 --- a/crates/jolt-blindfold/src/wire.rs +++ b/crates/jolt-blindfold/src/wire.rs @@ -1,4 +1,6 @@ -//! BlindFold's prover messages and their order in the transcript. +//! BlindFold's prover messages and their order in the transcript. The prover, +//! the verifier, and the tests all send and read them through these types; +//! only the sumcheck rounds between them belong to `jolt-sumcheck`. //! //! Every count below is fixed by [`BlindFoldDimensions`] and the protocol's //! final-opening layout, so no length travels with a message. The committed @@ -74,3 +76,55 @@ pub(crate) fn receive_opening( combined_blinding: transcript.receive()?, }) } + +/// The folded evaluation outputs and their blindings, sent after the folding +/// challenge: one of each per final-opening evaluation commitment. +pub(crate) struct FoldedEvaluations { + pub(crate) outputs: Vec, + pub(crate) blindings: Vec, +} + +impl FoldedEvaluations { + pub(crate) fn send(&self, transcript: &mut ProverTranscript) { + transcript.send_all(&self.outputs); + transcript.send_all(&self.blindings); + } + + pub(crate) fn receive( + eval_count: usize, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(Self { + outputs: transcript.receive_n(eval_count)?, + blindings: transcript.receive_n(eval_count)?, + }) + } +} + +/// The outer folded-R1CS sumcheck's terminal claims: `Az`, `Bz`, and `Cz` at +/// the outer point, then the opening of the folded error rows there. +pub(crate) struct OuterClaims { + pub(crate) abc: [F; 3], + pub(crate) error_opening: VectorCommitmentOpening, +} + +impl OuterClaims { + pub(crate) fn send(&self, transcript: &mut ProverTranscript) { + transcript.send_all(&self.abc); + send_opening(&self.error_opening, transcript); + } + + pub(crate) fn receive( + error_row_len: usize, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(Self { + abc: [ + transcript.receive()?, + transcript.receive()?, + transcript.receive()?, + ], + error_opening: receive_opening(error_row_len, transcript)?, + }) + } +} diff --git a/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory index 6610a5d3e3..0ea0df2453 100644 --- a/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory @@ -5,9 +5,9 @@ jolt-blindfold::prove.rs::prove_inner_sumcheck::challenge_small#1 jolt-blindfold::prove.rs::prove_outer_sumcheck::challenge_small#1 jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#1 jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#10 -jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#11 -jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#12 -jolt-blindfold::prove.rs::prove_with_row_committer::challenges_small#6 +jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#8 +jolt-blindfold::prove.rs::prove_with_row_committer::challenge_small#9 +jolt-blindfold::prove.rs::prove_with_row_committer::challenges_small#5 jolt-blindfold::verify.rs::BlindFoldProtocol::folded_instance::challenge_small#1 jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge_small#0 jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::challenge_small#1 diff --git a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory index c609ddb7b3..a431f353e1 100644 --- a/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory @@ -28,22 +28,26 @@ jolt-blindfold::prove.rs::prove::prove_with_row_committer::::folded_instance::FoldingCommitments::receive#0 jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.folded_instance#0 jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.verify_folded_eval_witness_bindings#1 jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.verify_inner_folded_r1cs#3 jolt-blindfold::verify.rs::BlindFoldProtocol::verify::.verify_outer_folded_r1cs#2 -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_opening#2 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::FoldedEvaluations::receive#0 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_opening#1 jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::SumcheckVerifier::verify_compressed#3 jolt-blindfold::verify.rs::BlindFoldProtocol::verify_inner_folded_r1cs::receive_opening#4 +jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::OuterClaims::receive#2 jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::SumcheckVerifier::verify_compressed#1 -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive_opening#5 +jolt-blindfold::wire.rs::OuterClaims::receive::receive_opening#3 +jolt-blindfold::wire.rs::OuterClaims::send::send_opening#1 jolt-dory::scheme.rs::DoryScheme::open::DoryProverChannel::new#0 jolt-dory::scheme.rs::DoryScheme::open::dory::prove::#1 jolt-dory::scheme.rs::DoryScheme::open_zk::.send_sigma1_responses#2 diff --git a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory index d467fbd0b8..886f1e2582 100644 --- a/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory @@ -7,14 +7,10 @@ jolt-akita::adapters.rs::AkitaCommitment::send::send#0::transcript.send(&self.he jolt-akita::adapters.rs::AkitaCommitment::send::send_bytes#1::transcript.send_bytes(&self.serialized_backend_bytes) jolt-akita::adapters.rs::receive_selection::receive#0::transcript.receive() jolt-akita::adapters.rs::send_selection::send#0::transcript.send(selection.row_digest.as_bytes()) -jolt-blindfold::prove.rs::prove_with_row_committer::send_all#2::transcript.send_all(&folded_eval_outputs) -jolt-blindfold::prove.rs::prove_with_row_committer::send_all#3::transcript.send_all(&folded_eval_blindings) -jolt-blindfold::prove.rs::prove_with_row_committer::send_all#8::transcript.send_all(&[az_rx,bz_rx,cz_rx]) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_n#0::transcript.receive_n(eval_count) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_folded_eval_witness_bindings::receive_n#1::transcript.receive_n(eval_count) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive#2::transcript.receive() -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive#3::transcript.receive() -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::receive#4::transcript.receive() +jolt-blindfold::wire.rs::FoldedEvaluations::receive::receive_n#0::transcript.receive_n(eval_count) +jolt-blindfold::wire.rs::FoldedEvaluations::receive::receive_n#1::transcript.receive_n(eval_count) +jolt-blindfold::wire.rs::FoldedEvaluations::send::send_all#0::transcript.send_all(&self.outputs) +jolt-blindfold::wire.rs::FoldedEvaluations::send::send_all#1::transcript.send_all(&self.blindings) jolt-blindfold::wire.rs::FoldingCommitments::receive::receive#1::transcript.receive() jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#0::transcript.receive_n(dimensions.auxiliary_rows) jolt-blindfold::wire.rs::FoldingCommitments::receive::receive_n#2::transcript.receive_n(dimensions.coefficient_rows) @@ -31,6 +27,10 @@ jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#4::transcript jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#5::transcript.send_all(&self.random_error_rows) jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#6::transcript.send_all(&self.random_evals) jolt-blindfold::wire.rs::FoldingCommitments::send::send_all#7::transcript.send_all(&self.cross_term_error_rows) +jolt-blindfold::wire.rs::OuterClaims::receive::receive#0::transcript.receive() +jolt-blindfold::wire.rs::OuterClaims::receive::receive#1::transcript.receive() +jolt-blindfold::wire.rs::OuterClaims::receive::receive#2::transcript.receive() +jolt-blindfold::wire.rs::OuterClaims::send::send_all#0::transcript.send_all(&self.abc) jolt-blindfold::wire.rs::receive_opening::receive#1::transcript.receive() jolt-blindfold::wire.rs::receive_opening::receive_n#0::transcript.receive_n(row_len) jolt-blindfold::wire.rs::send_opening::send#1::transcript.send(&opening.combined_blinding) @@ -73,9 +73,9 @@ jolt-verifier::proof.rs::ProofHeader::receive::receive#2::transcript.receive() jolt-verifier::proof.rs::ProofHeader::receive::receive#4::transcript.receive::<[u8;1]>() jolt-verifier::proof.rs::ProofHeader::send::send#0::transcript.send(&num::u64_from_usize(self.trace_length).to_le_bytes()) jolt-verifier::proof.rs::ProofHeader::send::send#1::transcript.send(&num::u64_from_usize(self.ram_K).to_le_bytes()) +jolt-verifier::proof.rs::ProofHeader::send::send#2::transcript.send(&[self.rw_config.ram_rw_phase1_num_rounds,self.rw_config.ram_rw_phase2_num_rounds,self.rw_config.registers_rw_phase1_num_rounds,self.rw_config.registers_rw_phase2_num_rounds,self.one_hot_config.log_k_chunk,self.one_hot_config.lookups_ra_virtual_log_k_chunk,]) jolt-verifier::proof.rs::ProofHeader::send::send#3::transcript.send(&self.trace_polynomial_order.transcript_scalar().to_le_bytes(),) jolt-verifier::proof.rs::ProofHeader::send::send#4::transcript.send(&[u8::from(self.untrusted_advice)]) -jolt-verifier::proof.rs::ProofHeader::send::send_all#2::transcript.send_all(&[self.rw_config.ram_rw_phase1_num_rounds,self.rw_config.ram_rw_phase2_num_rounds,self.rw_config.registers_rw_phase1_num_rounds,self.rw_config.registers_rw_phase2_num_rounds,self.one_hot_config.log_k_chunk,self.one_hot_config.lookups_ra_virtual_log_k_chunk,]) jolt-verifier::proof.rs::receive_u64::receive#0::transcript.receive() jolt-verifier::stages/relations.rs::receive_member_claims::receive#0::transcript.receive() jolt-verifier::stages/stage4/ram_val_check.rs::RamValCheckStagedOpenings::receive::receive#0::transcript.receive() From 537cd152ce248796bd6112188916f788e5f55251 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:14 -0400 Subject: [PATCH 46/62] test(verifier): leave seeded forks untaped in the Fiat-Shamir audit sponge 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. --- crates/jolt-verifier/tests/fs_attacks.rs | 58 +++++++----------- .../tests/support/fs_transcript.rs | 60 +++++++++++-------- 2 files changed, 55 insertions(+), 63 deletions(-) diff --git a/crates/jolt-verifier/tests/fs_attacks.rs b/crates/jolt-verifier/tests/fs_attacks.rs index 2731776fed..b59f760a6c 100644 --- a/crates/jolt-verifier/tests/fs_attacks.rs +++ b/crates/jolt-verifier/tests/fs_attacks.rs @@ -58,8 +58,8 @@ fn assert_binding_load_bearing( } mod audit_sponge { - use jolt_transcript::Blake2b512; use jolt_transcript::DuplexSpongeInterface; + use jolt_transcript::{Blake2b512, Fork, FORK_SEED_LEN}; use super::fs_transcript::{record, replay, AuditSponge}; @@ -86,48 +86,30 @@ mod audit_sponge { assert_eq!(tape.bytes, expected); } - /// A clone continues the stream from its fork point in both modes, so a - /// scout that squeezes ahead on a transcript clone neither duplicates nor - /// shifts the live transcript's tape, and replay answers both from the - /// same offsets regardless of chunking or absorbed data. + /// Forks are untaped: their squeezes match plain Blake2b forks in and out + /// of a session, on any thread, and leave the session's tape untouched. #[test] - fn clones_share_one_stream() { - let (scouted, tape) = record(|| { - let mut live = AuditSponge::default(); - let _ = live.absorb(b"statement"); - let mut scout = live.clone(); - let scouted: [u8; 32] = squeeze(&mut scout); - let first: [u8; 16] = squeeze(&mut live); - let second: [u8; 16] = squeeze(&mut live); - assert_eq!([first, second].concat(), scouted); - scouted - }); - assert_eq!(tape.bytes, scouted); - - let replayed = replay(&tape, || { - let mut live = AuditSponge::default(); - let _ = live.absorb(b"different statement"); - let mut scout = live.clone(); - let head: [u8; 8] = squeeze(&mut live); - let scouted: [u8; 32] = squeeze(&mut scout); - let tail: [u8; 24] = squeeze(&mut live); - assert_eq!([head.as_slice(), tail.as_slice()].concat(), scouted); - scouted + #[expect(clippy::expect_used, reason = "a panicking fork thread fails the test")] + fn forks_pass_through_untaped() { + let seed = [7u8; FORK_SEED_LEN]; + let expected: [u8; 32] = Fork::::new(&seed, 3).squeeze(); + let (squeezed, tape) = record(|| { + let on_session_thread: [u8; 32] = Fork::::new(&seed, 3).squeeze(); + let on_worker_thread: [u8; 32] = + std::thread::spawn(move || Fork::::new(&seed, 3).squeeze()) + .join() + .expect("fork thread"); + assert_eq!(on_worker_thread, on_session_thread); + on_session_thread }); - assert_eq!(replayed.output, scouted); - assert_eq!(replayed.consumed, replayed.recorded); + assert_eq!(squeezed, expected); + assert!(tape.bytes.is_empty()); } #[test] - #[should_panic(expected = "forked audit sponges squeezed different bytes")] - fn diverging_fork_panics_while_recording() { - let _ = record(|| { - let mut live = AuditSponge::default(); - let mut preview = live.clone(); - let _ = preview.absorb(b"candidate nonce"); - let _: [u8; 16] = squeeze(&mut preview); - let _: [u8; 16] = squeeze(&mut live); - }); + #[should_panic(expected = "outside a record or replay session")] + fn transcript_sponge_needs_a_session() { + let _ = AuditSponge::default().absorb(b"statement"); } #[test] diff --git a/crates/jolt-verifier/tests/support/fs_transcript.rs b/crates/jolt-verifier/tests/support/fs_transcript.rs index eba1a60308..1206381e79 100644 --- a/crates/jolt-verifier/tests/support/fs_transcript.rs +++ b/crates/jolt-verifier/tests/support/fs_transcript.rs @@ -12,13 +12,12 @@ //! per-call records. Exact challenges rejection-sample a data-dependent number //! of bytes, so only the byte stream is invariant under replay. //! -//! Each sponge value carries its own stream position, so a clone continues the -//! stream from where it was forked, exactly as a clone of a real sponge would. -//! While recording, a fork that squeezes over an already recorded position -//! must reproduce the recorded bytes, so verifier scouts that run ahead on a -//! transcript clone stay consistent with the live transcript. A fork that -//! squeezes different bytes (one that absorbed different data, such as a -//! proof-of-work preview) panics: its challenges have no single frozen value. +//! Seeded forks ([`jolt_transcript::Fork`]) are not taped. A fork's output is +//! a function of its seed, squeezed from the taped transcript, and its +//! counter, a proof message, so freezing the transcript already freezes every +//! fork. A sponge is a fork when its first absorb is the fork domain tag; it +//! then runs plain Blake2b on any thread, inside a session or not (Akita +//! searches fold responses on worker threads). #![expect( clippy::expect_used, @@ -53,26 +52,29 @@ thread_local! { static ACTIVE: RefCell>>> = const { RefCell::new(None) }; } +/// The first absorb of every [`jolt_transcript::Fork`]. +const FORK_TAG: &[u8] = b"jolt-transcript/fork/v1"; + /// [`Blake2b512`] with a recorded or replayed squeeze stream. /// /// [`Default`] binds the new sponge to the session active on the calling -/// thread and panics outside one; clones share that session. +/// thread, if any. A transcript sponge must have one; a fork drops it on its +/// first absorb. #[derive(Clone)] pub struct AuditSponge { inner: Blake2b512, position: usize, - tape: Arc>, + tape: Option>>, + absorbed: bool, } impl Default for AuditSponge { fn default() -> Self { - let tape = ACTIVE - .with(|active| active.borrow().clone()) - .expect("AuditSponge constructed outside a record or replay session"); Self { inner: Blake2b512::default(), position: 0, - tape, + tape: ACTIVE.with(|active| active.borrow().clone()), + absorbed: false, } } } @@ -81,30 +83,38 @@ impl DuplexSpongeInterface for AuditSponge { type U = u8; fn absorb(&mut self, input: &[u8]) -> &mut Self { + if !self.absorbed { + self.absorbed = true; + if input == FORK_TAG { + self.tape = None; + } else { + assert!( + self.tape.is_some(), + "AuditSponge transcript constructed outside a record or replay session" + ); + } + } let _ = self.inner.absorb(input); self } fn squeeze(&mut self, output: &mut [u8]) -> &mut Self { + let Some(tape) = &self.tape else { + let _ = self.inner.squeeze(output); + return self; + }; let start = self.position; let end = start + output.len(); - let mut tape = self.tape.lock().expect("audit tape lock poisoned"); + let mut tape = tape.lock().expect("audit tape lock poisoned"); match tape.mode { Mode::Record => { let _ = self.inner.squeeze(output); - assert!( - start <= tape.bytes.len(), - "audit sponge skipped tape bytes {}..{start}", - tape.bytes.len() - ); - let recorded_end = end.min(tape.bytes.len()); - let (overlap, fresh) = output.split_at(recorded_end - start); assert_eq!( - &tape.bytes[start..recorded_end], - overlap, - "forked audit sponges squeezed different bytes at tape offset {start}" + start, + tape.bytes.len(), + "a second transcript sponge squeezed into one session's tape" ); - tape.bytes.extend_from_slice(fresh); + tape.bytes.extend_from_slice(output); } Mode::Replay => { let recorded = tape.bytes.get(start..end).unwrap_or_else(|| { From af1534e4424bf991cded9bbcac4e55180574f6b3 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:14 -0400 Subject: [PATCH 47/62] refactor(sdk)!: choose the proof sponge once, in jolt-verifier 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. --- crates/jolt-verifier/Cargo.toml | 3 ++- crates/jolt-verifier/src/verifier.rs | 13 +++++++++++-- jolt-sdk/Cargo.toml | 6 +++--- jolt-sdk/src/host_utils.rs | 22 +++++----------------- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/crates/jolt-verifier/Cargo.toml b/crates/jolt-verifier/Cargo.toml index 69342054f7..3a665d084a 100644 --- a/crates/jolt-verifier/Cargo.toml +++ b/crates/jolt-verifier/Cargo.toml @@ -63,7 +63,8 @@ jolt-poly.workspace = true jolt-program = { workspace = true, features = ["serialization"] } jolt-riscv = { workspace = true, features = ["serialization"] } jolt-sumcheck.workspace = true -jolt-transcript.workspace = true +# Blake2b is always built: it is `JoltSponge` unless another sponge is selected. +jolt-transcript = { workspace = true, features = ["transcript-blake2b"] } jolt-verifier-derive.workspace = true num-traits.workspace = true serde.workspace = true diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index 3a0a1a60ef..7d43f24138 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -35,8 +35,17 @@ use crate::{ VerifierError, }; -/// The sponge Jolt proofs run on. Every prover and verifier entry point is -/// generic over the sponge; this alias is the one place the default is chosen. +/// The sponge Jolt proofs run on: Poseidon under `transcript-poseidon`, else +/// Keccak under `transcript-keccak`, else Blake2b. Every prover and verifier +/// entry point is generic over the sponge; this alias is the one place the +/// default is chosen, and the SDK's `ProtocolSponge` is this alias. +#[cfg(feature = "transcript-poseidon")] +pub type JoltSponge = jolt_transcript::PoseidonSponge; +/// See the Poseidon arm. +#[cfg(all(feature = "transcript-keccak", not(feature = "transcript-poseidon")))] +pub type JoltSponge = jolt_transcript::Keccak; +/// See the Poseidon arm. +#[cfg(not(any(feature = "transcript-keccak", feature = "transcript-poseidon")))] pub type JoltSponge = jolt_transcript::Blake2b512; /// The session every Jolt transcript is bound to. diff --git a/jolt-sdk/Cargo.toml b/jolt-sdk/Cargo.toml index 2c30f933b5..ae7ac3ba6d 100644 --- a/jolt-sdk/Cargo.toml +++ b/jolt-sdk/Cargo.toml @@ -69,9 +69,9 @@ guest-verifier = [ ] malloc-shim = ["jolt-platform/malloc-shim"] -transcript-poseidon = ["jolt-transcript/transcript-poseidon"] -transcript-keccak = ["jolt-transcript/transcript-keccak"] -transcript-blake2b = ["jolt-transcript/transcript-blake2b"] +transcript-poseidon = ["jolt-transcript/transcript-poseidon", "jolt-verifier?/transcript-poseidon"] +transcript-keccak = ["jolt-transcript/transcript-keccak", "jolt-verifier?/transcript-keccak"] +transcript-blake2b = ["jolt-transcript/transcript-blake2b", "jolt-verifier?/transcript-blake2b"] # Jolt-SDK native features (optional capabilities) # diff --git a/jolt-sdk/src/host_utils.rs b/jolt-sdk/src/host_utils.rs index bfbabd78ca..a94589c9b0 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -28,23 +28,11 @@ use tracer::TracerInlineExpansionProvider; #[cfg(feature = "host")] pub use jolt_host as host; -/// The sponge every SDK proof runs on, chosen by the `transcript-*` feature -/// (Blake2b by default). -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - feature = "transcript-poseidon" -))] -pub type ProtocolSponge = jolt_transcript::PoseidonSponge; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - feature = "transcript-keccak" -))] -pub type ProtocolSponge = jolt_transcript::Keccak; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - not(any(feature = "transcript-poseidon", feature = "transcript-keccak")) -))] -pub type ProtocolSponge = jolt_transcript::Blake2b512; +/// The sponge every SDK proof runs on: the verifier's +/// [`JoltSponge`](jolt_verifier::JoltSponge), chosen by the `transcript-*` +/// feature (Blake2b by default). +#[cfg(any(feature = "host", feature = "guest-verifier"))] +pub type ProtocolSponge = jolt_verifier::JoltSponge; #[cfg(feature = "host")] pub use jolt_program::execution::{ ExecutionBackend, OwnedTrace, TraceError, TraceInputs, TraceOutput, TraceSource, From 25482b6bdd14255fa2f79d482173bebf08b2b34d Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:14 -0400 Subject: [PATCH 48/62] ci: check every standalone workspace for spongefish's yolocrypto 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. --- scripts/check-no-yolocrypto.sh | 50 ++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/scripts/check-no-yolocrypto.sh b/scripts/check-no-yolocrypto.sh index 7991fc4530..79a2e38fa1 100755 --- a/scripts/check-no-yolocrypto.sh +++ b/scripts/check-no-yolocrypto.sh @@ -6,24 +6,40 @@ set -euo pipefail # # spongefish's `yolocrypto` feature makes `ProverState`/`VerifierState`'s # duplex state public. Cargo unifies features across the graph, so one crate -# enabling it would expose the state to every crate. No crate in the -# workspace may enable it, under any feature combination, on any target. +# enabling it would expose the state to every crate. No crate may enable it, +# under any feature combination, on any target, in the root workspace or in +# any standalone workspace (fuzz harnesses, external consumers) whose lockfile +# contains spongefish. export CARGO_TERM_COLOR=never -features() { - cargo tree --locked --workspace --all-features --target all -e features \ - -i spongefish --prefix none --format '{p} {f}' 2>/dev/null +root=$(git rev-parse --show-toplevel) +cd "$root" + +check() { + local manifest=$1 graph + if ! graph=$(cargo tree --locked --manifest-path "$manifest" --workspace --all-features \ + --target all -e features -i spongefish --prefix none --format '{p} {f}'); then + echo "error: cargo tree failed for $manifest" >&2 + exit 1 + fi + # Self-test: the inverted tree must reach spongefish, or an empty result + # would make the check below vacuous. + if ! grep -Eq '^spongefish ' <<<"$graph"; then + echo "error: self-test failed: spongefish not found in $manifest's graph" >&2 + exit 1 + fi + if grep -q 'yolocrypto' <<<"$graph"; then + echo "error: spongefish's yolocrypto feature is enabled in $manifest:" >&2 + grep 'yolocrypto' <<<"$graph" >&2 + exit 1 + fi } -graph=$(features) -# Self-test: the inverted tree must reach spongefish at all, or an empty -# result would make the check below vacuous. -if ! grep -Eq '^spongefish ' <<<"$graph"; then - echo "error: self-test failed: spongefish not found in the workspace graph" >&2 - exit 1 -fi -if grep -q 'yolocrypto' <<<"$graph"; then - echo "error: spongefish's yolocrypto feature is enabled:" >&2 - grep 'yolocrypto' <<<"$graph" >&2 - exit 1 -fi +check Cargo.toml +while IFS= read -r lock; do + manifest="$(dirname "$lock")/Cargo.toml" + [[ "$manifest" == "./Cargo.toml" ]] && continue + if grep -q '^name = "spongefish"$' "$lock"; then + check "$manifest" + fi +done < <(git ls-files -- '*Cargo.lock' | sed 's|^|./|') From 9d2eb0f211ae7c35f2c765002e7ece72c1dae424 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:40:20 -0400 Subject: [PATCH 49/62] docs: make specs and comments state what the transcript code does - 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. --- CLAUDE.md | 6 ++- crates/jolt-claims-derive/src/lib.rs | 4 +- crates/jolt-claims-derive/src/tests.rs | 2 +- .../src/protocols/jolt/geometry/bytecode.rs | 2 +- .../bytecode/read_raf_address_phase.rs | 4 +- crates/jolt-dory/src/transcript.rs | 11 +++-- crates/jolt-sumcheck/README.md | 15 ++++--- crates/jolt-verifier-derive/src/lib.rs | 6 ++- .../src/stages/stage2/ram_output_check.rs | 7 ++-- .../jolt-verifier/src/stages/stage2/verify.rs | 6 +-- .../jolt-verifier/src/stages/stage3/verify.rs | 2 +- .../src/stages/stage6a/booleanity.rs | 10 ++--- specs/jolt-transcript-narg.md | 6 ++- specs/jolt-transcript-spongefish-state.md | 41 +++++++++++-------- 14 files changed, 72 insertions(+), 50 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 63f4ec57f7..1198face6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,12 +122,14 @@ The SDK's `host` feature enables native build/prove APIs. On `jolt-prover`, `aki ### Key Type Parameters The staged prover and verifier are generic over the PCS, vector commitment, -and transcript. Dory uses `Fr`; the `akita` build uses `AkitaField`. +and the transcript's sponge. Dory uses `Fr`; the `akita` build uses +`AkitaField`. Proofs are argument strings written through +`jolt_transcript::ProverTranscript` and read through `VerifierTranscript<'_, H>`. ``` PCS: CommitmentScheme VC: VectorCommitment -T: Transcript +H: Sponge ``` ### Prover Pipeline diff --git a/crates/jolt-claims-derive/src/lib.rs b/crates/jolt-claims-derive/src/lib.rs index 9ce1c8330d..1f1ea5ae7c 100644 --- a/crates/jolt-claims-derive/src/lib.rs +++ b/crates/jolt-claims-derive/src/lib.rs @@ -70,7 +70,7 @@ //! (one drawn Fiat-Shamir scalar). A `Vec` field is rejected (challenge sub-enum //! variants are unit, so there is no indexed id), and an `Option` field is //! rejected (no relation draws a conditional challenge, and the `draw_challenges` -//! default treats every field as one unconditional `challenge_scalar`). +//! default treats every field as one unconditional exact `challenge()`). //! //! Generates both halves of [`SumcheckChallenges`]: `resolve_challenge` (id → //! value) and `from_transcript_values` (consume one drawn scalar per field in @@ -798,7 +798,7 @@ fn plan_challenge_field(field: &Field) -> Result { &field.ty, "challenge fields are an unconditional scalar `F`; a conditional \ `Option` challenge is not supported (no relation draws one, and the \ - `draw_challenges` default treats every field as one `challenge_scalar`)", + `draw_challenges` default treats every field as one exact `challenge()`)", )); } Ok(ChallengeFieldPlan { ident, path }) diff --git a/crates/jolt-claims-derive/src/tests.rs b/crates/jolt-claims-derive/src/tests.rs index 7e227fd457..49374cedf9 100644 --- a/crates/jolt-claims-derive/src/tests.rs +++ b/crates/jolt-claims-derive/src/tests.rs @@ -249,7 +249,7 @@ fn challenges_reject_option_fields() { tau: Option, } }, - "challenge fields are an unconditional scalar `F`; a conditional `Option` challenge is not supported (no relation draws one, and the `draw_challenges` default treats every field as one `challenge_scalar`)", + "challenge fields are an unconditional scalar `F`; a conditional `Option` challenge is not supported (no relation draws one, and the `draw_challenges` default treats every field as one exact `challenge()`)", ); } diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs b/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs index 147bdbfc93..aa66628c52 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs @@ -22,7 +22,7 @@ use super::registers::{ use super::spartan::{pc_shift, unexpanded_pc_shift}; /// Per-stage (1..=5) gamma-power vector lengths for the bytecode read-RAF stage -/// folds — the arities of the prover's `challenge_scalar_powers` draws. The +/// folds — the arities of the powers of each stage's drawn gamma. The /// verifier stores each stage's single drawn scalar and expands it with /// [`stage_gamma_powers`], so these lengths are single-sourced with the /// fold-side `require_len` guards. diff --git a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs index 701067c5d0..2681ebe51a 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/bytecode/read_raf_address_phase.rs @@ -166,8 +166,8 @@ pub struct BytecodeReadRafAddressPhaseChallenges { impl BytecodeReadRafAddressPhaseChallenges { /// Expand the five drawn per-stage scalars into the gamma-power vectors the - /// bytecode folds consume (`[1, γ, γ², …]` — the recurrence the prover's - /// `challenge_scalar_powers` applies to its single squeezed scalar), sized + /// bytecode folds consume (`[1, γ, γ², …]`, the powers of each stage's + /// single drawn scalar), sized /// by [`BYTECODE_STAGE_GAMMA_COUNTS`]. pub fn stage_gamma_powers(&self) -> [Vec; 5] { let stage_gammas = [ diff --git a/crates/jolt-dory/src/transcript.rs b/crates/jolt-dory/src/transcript.rs index 2591c716a5..c697a50e05 100644 --- a/crates/jolt-dory/src/transcript.rs +++ b/crates/jolt-dory/src/transcript.rs @@ -158,8 +158,14 @@ fn compressed(value: &S) -> Vec { } /// Rebuilds the Dory proof for a `num_vars`-variable opening from the next -/// prover messages, reading them from `scout`, a scratch copy of the live -/// verifier transcript. Message order mirrors `dory::verify`. +/// prover messages, parsing ahead in `unread`, the bytes the live verifier +/// transcript has not yet received. +/// +/// WARNING: this restates `dory::verify`'s message order and compressed +/// widths, which dory-pcs owns. A drift from an upstream reorder fails +/// completeness, not soundness: [`DoryVerifierChannel`] still receives every +/// byte and checks it against the parsed value. Update this parser with any +/// dory-pcs bump. pub(crate) fn read_proof( unread: &[u8], num_vars: usize, @@ -283,7 +289,6 @@ impl CompressedWidth for ArkGT { const BYTES: usize = 384; } -/// Reads one compressed element. /// Parses the next compressed `T` from the unread proof bytes, advancing /// `unread` past it. fn read(unread: &mut &[u8]) -> Result diff --git a/crates/jolt-sumcheck/README.md b/crates/jolt-sumcheck/README.md index 859f3f7fe8..477bd49f82 100644 --- a/crates/jolt-sumcheck/README.md +++ b/crates/jolt-sumcheck/README.md @@ -17,10 +17,10 @@ pool. Its default feature set is empty. Polynomial storage, compression, round generation, and the shared prover and verifier engines require `jolt_field::Field`. The stock clear recorder and -proof-verification conveniences additionally require -`jolt_transcript::AppendToTranscript`, because they absorb field elements. -Implement `jolt_transcript::Transcript` to choose challenge generation; stock -hash transcripts retain their `CanonicalEncoding` requirement. Jolt's own +the verifiers additionally require `jolt_field::CanonicalEncoding`, because +they send, receive, and absorb field elements through `jolt_transcript`'s +prover and verifier transcripts. Choose the sponge with the transcripts' `H` +parameter. Jolt's own optimized kernels and commitment-backed modes may require the broader `JoltField` bundle at their integration boundary. @@ -33,10 +33,9 @@ Run the minimal example with: cargo run -p jolt-sumcheck --example clear_custom_transcript --no-default-features ``` -The example transcript tracks every absorb but returns fixed challenges, which -makes the Fiat–Shamir schedule and algebraic API easy to inspect. It is not a -production transcript; applications must use a cryptographic transcript with -adequate challenge entropy. +The example proves one round into a Keccak prover transcript and verifies the +resulting argument string, which keeps the Fiat–Shamir schedule and the +algebraic API small enough to read in one file. The isolated fixture runs outside Cargo's Jolt workspace feature unification: diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index 0d6ad00976..496562fd82 100644 --- a/crates/jolt-verifier-derive/src/lib.rs +++ b/crates/jolt-verifier-derive/src/lib.rs @@ -697,7 +697,11 @@ fn expand(input: DeriveInput) -> syn::Result { // caller MUST have already absorbed commitments to those claim scalars into // the transcript before invoking this driver. Without that prior binding a // malicious prover could choose its claim openings adaptively after seeing - // the batching challenge. + // the batching challenge. One exception: stage 4's staged advice and + // program-image openings are committed only in stage 4's own output rows. + // They stay sound because each is the evaluation of a committed polynomial + // at a point fixed before stage 4, pinned later by the stage-6/7 reductions + // and the PCS opening rather than by transcript order. let verify_zk_method = { let max_fold = max_fold(); diff --git a/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs index b645d13d41..17429295db 100644 --- a/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs +++ b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs @@ -114,9 +114,10 @@ impl ConcreteSumcheck for RamOutputCheck { /// so the generated aggregate draw lands the vector exactly where the legacy /// stage front drew it — after both batch gammas. /// - /// MUST stay `challenge()` (not `challenge_scalar()`): both decode the same - /// 16-byte squeeze, but differently, so switching would silently change the - /// address point values without changing the transcript bytes. + /// WARNING: these MUST stay `challenge_small()` draws, unlike the + /// relations' default exact `challenge()`: the two consume different + /// squeezes and decode them differently, so switching would change the + /// address point and every challenge after it. fn draw_challenges( &self, transcript: &mut C, diff --git a/crates/jolt-verifier/src/stages/stage2/verify.rs b/crates/jolt-verifier/src/stages/stage2/verify.rs index d962d99d99..7a2f8cfe7b 100644 --- a/crates/jolt-verifier/src/stages/stage2/verify.rs +++ b/crates/jolt-verifier/src/stages/stage2/verify.rs @@ -161,9 +161,9 @@ where // Draw each relation's challenges in declaration order: the RAM read-write gamma, the // instruction claim-reduction gamma, under `field-inline` the field-inline claim-reduction - // gamma (each a single `challenge_scalar`), then the RAM output-check address reference - // point (the last member's `draw_challenges` override — one raw `challenge()` per RAM - // address variable, landing after the gammas as the inline draw did). The drawn challenges + // gamma (each a single exact `challenge()`), then the RAM output-check address reference + // point (the last member's `draw_challenges` override — one `challenge_small()` per RAM + // address variable, landing after the gammas). The drawn challenges // feed the input/output claims and populate the stage aggregate carried downstream. let challenges = sumchecks.draw_challenges(transcript)?; diff --git a/crates/jolt-verifier/src/stages/stage3/verify.rs b/crates/jolt-verifier/src/stages/stage3/verify.rs index 2887936567..73d6060408 100644 --- a/crates/jolt-verifier/src/stages/stage3/verify.rs +++ b/crates/jolt-verifier/src/stages/stage3/verify.rs @@ -69,7 +69,7 @@ where }; // Draw each relation's batching gamma in declaration order (shift, instruction - // input, register reduction); each is a single `challenge_scalar`. The drawn + // input, register reduction); each is a single exact `challenge()`. The drawn // challenges feed the input/output claims and populate the stage aggregate // carried downstream. let challenges = sumchecks.draw_challenges(transcript)?; diff --git a/crates/jolt-verifier/src/stages/stage6a/booleanity.rs b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs index 612a78b2eb..108d941610 100644 --- a/crates/jolt-verifier/src/stages/stage6a/booleanity.rs +++ b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs @@ -69,17 +69,17 @@ impl ConcreteSumcheck for BooleanityAddressPhase { /// Draws the booleanity pre-batch challenges at the frozen wire positions: /// the reference address is the reversed stage-5 instruction address, - /// padded with a fresh `challenge_vector` draw or truncated to the + /// padded with fresh `challenges_small` draws or truncated to the /// committed chunk width (`log_k_chunk`); then the batching gamma. This /// member is declared after the bytecode read-RAF one in /// `Stage6aSumchecks`, so the generated aggregate draw lands these /// squeezes exactly where the hand pre-batch block drew them — after the /// bytecode member's six gammas. /// - /// MUST stay `challenge_vector` + `challenge()` (not `challenge_scalar`): - /// both decode the same 16-byte squeeze, but differently, so switching - /// would silently change the reference/gamma values without changing the - /// transcript bytes. + /// WARNING: these MUST stay `challenges_small` + `challenge_small()`, + /// unlike the relations' default exact `challenge()`: the two consume + /// different squeezes and decode them differently, so switching would + /// change the reference, the gamma, and every challenge after them. fn draw_challenges( &self, transcript: &mut C, diff --git a/specs/jolt-transcript-narg.md b/specs/jolt-transcript-narg.md index cc520fcc02..9fa4f134b1 100644 --- a/specs/jolt-transcript-narg.md +++ b/specs/jolt-transcript-narg.md @@ -4,8 +4,9 @@ | --------- | ------------------ | | Author(s) | @markosg04 | | Created | 2026-10-01 | -| Status | implemented (in review) | +| Status | implemented (in review); amended | | PR | (Jolt) / (Akita) | +| Amended by | `specs/jolt-transcript-spongefish-state.md`: the transcript runs on spongefish's prover and verifier state, `Preview` is removed, and grinding and fold-response search run on seeded forks. Sections below that describe previews are superseded. | ## Summary @@ -226,6 +227,9 @@ Every NARG byte is therefore absorbed exactly once on each side, and invariant ### Grinding and previews +> Superseded: previews are gone. Grinding squeezes a seed and searches nonces +> on seeded forks; see `specs/jolt-transcript-spongefish-state.md`. + `Preview` clones the sponge and exposes `absorb`, `absorb_bytes`, `absorb_nonce`, and `squeeze::()`. `grind(bits)` searches LEB128 nonces against a preview, sends the winning nonce, then squeezes and checks the diff --git a/specs/jolt-transcript-spongefish-state.md b/specs/jolt-transcript-spongefish-state.md index 91f6185e3c..8b7b67f1f2 100644 --- a/specs/jolt-transcript-spongefish-state.md +++ b/specs/jolt-transcript-spongefish-state.md @@ -4,7 +4,7 @@ | --------- | --------------------------------------------------------------- | | Author(s) | @markosg04 | | Created | 2026-10-05 | -| Status | proposed | +| Status | implemented (in review) | | Amends | `specs/jolt-transcript-narg.md` (Grinding and previews; Alternatives considered) | ## Decision @@ -24,7 +24,7 @@ state. The four reasons given there now resolve as follows: | Reason given | Resolution | | --- | --- | -| `VerificationError` is a unit type | The wrapper maps it to `TranscriptError` with the message index and site, which it tracks itself. | +| `VerificationError` is a unit type | The wrapper checks each read against the remaining proof itself and reports a typed `TranscriptError` (`Truncated`, `NonCanonical`, `OutOfBounds`, ...). Sites stay in the diagnostic log. | | Previews need `yolocrypto` | Previews are removed. Twin and replay checks compare argument strings plus one final 32-byte squeeze on each side. Grinding no longer previews. | | Encode and serialize are separate | For byte sponges (`H::U = u8`), spongefish's blanket `NargSerialize for T: Encoding<[u8]>` makes them identical. Jolt pins byte sponges only (see Non-goals). | | No logging hook | The wrapper logs each call with its site before delegating. | @@ -40,9 +40,7 @@ state. The four reasons given there now resolve as follows: rejection over fixed-width `verifier_message::<[u8; N]>()` squeezes, using `Field::random`'s contract. The small challenge set is unchanged: 16 squeezed bytes through `CanonicalEncoding::from_challenge_bytes`. -4. The prover's private randomness comes from spongefish's transcript-bound - `ProverState::rng()`. Protocol code never seeds its own RNG for blindings. -5. Message shapes stay positional. No message carries a length the public +4. Message shapes stay positional. No message carries a length the public parameters already fix. ## Mechanism @@ -64,16 +62,16 @@ travel as byte arrays. sponge id). A transcript starts from `DomainSeparator::new(protocol_id).session(B(session)).instance(B(instance))` followed by `to_prover(H::default())` or `to_verifier(H::default(), narg)`. -`B` is a length-framed byte string. Jolt passes an empty instance and keeps -binding its preamble through public messages. Akita and Aerie pass their -instance digests. +`B` is a length-framed byte string. Every transcript passes an empty instance: +Jolt, Akita, and Aerie bind their statement data through public messages, so +the instance slot carries nothing. **Operations.** | `jolt-transcript` | spongefish | | --- | --- | | `send` / `send_all`, `receive` / `receive_n` | `prover_message`, `prover_messages`, `prover_messages_vec` | -| `send_bytes(len known)` / `receive_bytes(len)` | `prover_messages::` / `prover_messages_vec::(len)` | +| `send_bytes(len known)` / `receive_bytes(len)` | one `[u8; 32]` message per whole block, then one `[u8; 1]` message per remaining byte, on both sides; the verifier also checks the bytes it returns equal the bytes the sponge absorbed | | `send_bounded_bytes` / `receive_bounded_bytes` | `u32` LE length as a prover message, then the bytes; the verifier enforces the maximum before reading | | `public*` | `public_message*` | | `challenge::()` | rejection loop over `verifier_message::<[u8; N]>()` | @@ -87,8 +85,9 @@ transcript, and `LE32(counter)`. Every prover search runs on forks: 1. Squeeze a 32-byte seed `s` from the transcript. 2. The prover tries counters `c` upward from zero, each on `Fork(s, c)`, off the transcript. -3. The prover sends the accepted `c` as a `u32` prover message. The verifier - receives it, range-checks it, and rebuilds `Fork(s, c)`. +3. The prover sends the accepted `c` as a canonical unsigned LEB128 `Nonce` + message. The verifier receives it, rejects a non-minimal encoding or a + value outside the search range, and rebuilds `Fork(s, c)`. Proof of work with difficulty `g > 0` searches `c` in `[0, 2^(g+7))` and accepts when the fork's first 32 squeezed bytes have `g` leading zero bits, @@ -96,8 +95,8 @@ least significant bit first. The protected challenge is drawn from the transcript after the nonce. Akita's fold-response search (Fiat-Shamir with aborts) draws each candidate's fold challenges from the fork and accepts the first counter whose response meets its bounds. Both roles draw the accepted -challenges from the same fork. The transcript sees one squeeze and one 4-byte -message per search, whatever the search cost. The seed binds the full +challenges from the same fork. The transcript sees one squeeze and one nonce +message of at most five bytes per search, whatever the search cost. The seed binds the full prechallenge state, and the counter is in the argument string. **Replay checkpoint.** The ZK prover replays its argument string through the @@ -116,8 +115,16 @@ Fixtures that cloned a mid-proof transcript rebuild it from a closure. ## Non-goals +- Transcript-bound prover randomness. BlindFold and the committed recorders + draw blindings from the operating system's RNG, not from spongefish's + `ProverState::rng()`. Binding them to the transcript is a separate change; + the `StdRng` inside `ProverState` is unused. + - Algebraic sponges (`H::U` a field element). The encoding traits are - implemented over `[u8]` only. Supporting a field-native Poseidon later means + implemented over `[u8]` only. The byte-level `PoseidonSponge` emits only the + low 16 bytes of each permutation's output, whose distribution is within + $2^{-125}$ of uniform, so challenges drawn from it stay exact up to that + distance. Supporting a field-native Poseidon later means adding `Encoding<[Fr]>` impls. It does not change the typed layer. - Changing message order or adding labels. Aerie's event headers stay an Aerie-level framing, absorbed as public messages. @@ -141,8 +148,8 @@ Fixtures that cloned a mid-proof transcript rebuild it from a closure. ## Compatibility -Every proof changes. The domain-separator layout, the grinding transition, -and the nonce encoding (`u32` LE instead of LEB128) all differ. Fixtures, +Every proof changes. The domain-separator layout and the grinding transition +differ; the nonce keeps its canonical unsigned LEB128 encoding. Fixtures, known-answer tests, the FS census, and Akita's and Aerie's grinding tables are regenerated. Akita's `specs/transcript-grinding.md` and Aerie's `specs/falcon.md` §10.1–10.2 are amended in their own PRs. @@ -167,7 +174,7 @@ Three PRs, landed together. Akita and Aerie pin the Jolt PR's revision. - Clear, ZK, and Akita e2e; the verifier fixtures; tamper and FS sweeps; the census re-blessed and reviewed. - Per-sponge known answers regenerated, plus a test that the exact sampler - rejects and resamples at the boundary. + rejects an out-of-range candidate and resamples from the next squeeze. - Grinding: tests for accept, reject, an out-of-range nonce, and a wrong seed. - `cargo tree -e features` contains no `yolocrypto` in any of the three workspaces. From e8c2f20be20a679ea836831b3c04644e65f0830c Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 01:48:50 -0400 Subject: [PATCH 50/62] build(deps): pin akita to the transcript-unification branch Pins every Akita git dependency to LayerZero-Labs/akita 7109b3ea, the Akita head that runs on this branch's transcript and pins Jolt 9d2eb0f2, and regenerates the lockfile. The root patch keeps Akita's jolt-* crates on this workspace. --- Cargo.lock | 58 +++++++++++++++++++++++------------------------------- Cargo.toml | 18 ++++++++--------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30287ca723..8d20b5b859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ [[package]] name = "akita-algebra" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-error", "akita-serialization", @@ -107,11 +107,11 @@ dependencies = [ [[package]] name = "akita-challenges" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-error", - "akita-transcript", "jolt-field", + "jolt-transcript", "keccak 0.1.6", "serde", "smallvec", @@ -120,14 +120,13 @@ dependencies = [ [[package]] name = "akita-config" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-challenges", "akita-error", "akita-params", "akita-schedules", "akita-serialization", - "akita-transcript", "akita-types", "jolt-field", ] @@ -135,7 +134,7 @@ dependencies = [ [[package]] name = "akita-cpu-backend" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-challenges", @@ -145,7 +144,6 @@ dependencies = [ "akita-prover", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", @@ -156,24 +154,25 @@ dependencies = [ [[package]] name = "akita-error" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ + "jolt-transcript", "thiserror 2.0.20", ] [[package]] name = "akita-params" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-challenges", "akita-error", "akita-serialization", "akita-sumcheck", - "akita-transcript", "blake2 0.11.0", "jolt-field", + "jolt-transcript", "serde", "sha3 0.12.0", ] @@ -181,7 +180,7 @@ dependencies = [ [[package]] name = "akita-pcs" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-challenges", @@ -192,10 +191,10 @@ dependencies = [ "akita-prover", "akita-serialization", "akita-setup", - "akita-transcript", "akita-types", "akita-verifier", "jolt-field", + "jolt-transcript", "rayon", "tracing", ] @@ -203,7 +202,7 @@ dependencies = [ [[package]] name = "akita-planner" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-challenges", "akita-error", @@ -215,7 +214,7 @@ dependencies = [ [[package]] name = "akita-prover" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-challenges", @@ -224,17 +223,17 @@ dependencies = [ "akita-params", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", + "jolt-transcript", "tracing", ] [[package]] name = "akita-schedules" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-challenges", "akita-error", @@ -246,7 +245,7 @@ dependencies = [ [[package]] name = "akita-serialization" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "jolt-field", "jolt-poly", @@ -256,7 +255,7 @@ dependencies = [ [[package]] name = "akita-setup" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-config", "akita-cpu-backend", @@ -271,29 +270,20 @@ dependencies = [ [[package]] name = "akita-sumcheck" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-error", "akita-serialization", - "akita-transcript", "jolt-field", "jolt-poly", -] - -[[package]] -name = "akita-transcript" -version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" -dependencies = [ - "akita-error", - "jolt-field", + "jolt-transcript", ] [[package]] name = "akita-types" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-challenges", @@ -301,10 +291,12 @@ dependencies = [ "akita-params", "akita-serialization", "akita-sumcheck", - "akita-transcript", + "blake2 0.11.0", "jolt-field", "jolt-poly", + "jolt-transcript", "rand_core 0.6.4", + "serde", "shake", "tracing", ] @@ -312,7 +304,7 @@ dependencies = [ [[package]] name = "akita-verifier" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=e2c49ed450f1999a743e7fd4a648f230ce45472f#e2c49ed450f1999a743e7fd4a648f230ce45472f" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" dependencies = [ "akita-algebra", "akita-challenges", @@ -321,10 +313,10 @@ dependencies = [ "akita-params", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", + "jolt-transcript", "rayon", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 6d2bad32ed..d2b87d78aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -304,15 +304,15 @@ dory = { package = "dory-pcs", version = "0.4.2", features = [ "zk", ] } # Immutable upstream revision containing the quotient-free relation and external schedule catalogs. -akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-params = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } -akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "e2c49ed450f1999a743e7fd4a648f230ce45472f", default-features = false } +akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-params = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } # Core Utilities clap = { version = "4.6.1", features = ["derive"] } From 1ade6a6fd87c5cf46627c98c91dfe2c4fd58be0c Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 02:09:38 -0400 Subject: [PATCH 51/62] style(transcript): format the small-challenge log calls --- crates/jolt-transcript/src/prover.rs | 7 +++++-- crates/jolt-transcript/src/verifier.rs | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/jolt-transcript/src/prover.rs b/crates/jolt-transcript/src/prover.rs index 81f289b54d..8f99aaa8ba 100644 --- a/crates/jolt-transcript/src/prover.rs +++ b/crates/jolt-transcript/src/prover.rs @@ -193,8 +193,11 @@ impl Channel for ProverTranscript { fn challenge_small(&mut self) -> F { let value = self.state.small_challenge(); - self.log - .record(TranscriptOp::Challenge, crate::state::SMALL_CHALLENGE_BYTES, None); + self.log.record( + TranscriptOp::Challenge, + crate::state::SMALL_CHALLENGE_BYTES, + None, + ); value } diff --git a/crates/jolt-transcript/src/verifier.rs b/crates/jolt-transcript/src/verifier.rs index f47a7d925b..e2edcf3f3b 100644 --- a/crates/jolt-transcript/src/verifier.rs +++ b/crates/jolt-transcript/src/verifier.rs @@ -317,8 +317,11 @@ impl Channel for VerifierTranscript<'_, H> { fn challenge_small(&mut self) -> F { let value = self.state.small_challenge(); - self.log - .record(TranscriptOp::Challenge, crate::state::SMALL_CHALLENGE_BYTES, None); + self.log.record( + TranscriptOp::Challenge, + crate::state::SMALL_CHALLENGE_BYTES, + None, + ); value } From 4ff055f73254a5b5022ab2d5c1e214fd83fffcf2 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 02:14:24 -0400 Subject: [PATCH 52/62] build(deps): pin akita to its updated head Akita 10a13def drops akita-types' unused dependencies and gives wasm a getrandom backend; the code Jolt builds is unchanged. --- Cargo.lock | 33 ++++++++++++++++----------------- Cargo.toml | 18 +++++++++--------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d20b5b859..118a2f21ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ [[package]] name = "akita-algebra" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-error", "akita-serialization", @@ -107,7 +107,7 @@ dependencies = [ [[package]] name = "akita-challenges" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-error", "jolt-field", @@ -120,7 +120,7 @@ dependencies = [ [[package]] name = "akita-config" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-challenges", "akita-error", @@ -134,7 +134,7 @@ dependencies = [ [[package]] name = "akita-cpu-backend" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-challenges", @@ -154,7 +154,7 @@ dependencies = [ [[package]] name = "akita-error" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "jolt-transcript", "thiserror 2.0.20", @@ -163,7 +163,7 @@ dependencies = [ [[package]] name = "akita-params" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-challenges", @@ -180,7 +180,7 @@ dependencies = [ [[package]] name = "akita-pcs" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-challenges", @@ -202,7 +202,7 @@ dependencies = [ [[package]] name = "akita-planner" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-challenges", "akita-error", @@ -214,7 +214,7 @@ dependencies = [ [[package]] name = "akita-prover" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-challenges", @@ -233,7 +233,7 @@ dependencies = [ [[package]] name = "akita-schedules" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-challenges", "akita-error", @@ -245,8 +245,9 @@ dependencies = [ [[package]] name = "akita-serialization" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ + "getrandom 0.2.17", "jolt-field", "jolt-poly", "thiserror 2.0.20", @@ -255,7 +256,7 @@ dependencies = [ [[package]] name = "akita-setup" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-config", "akita-cpu-backend", @@ -270,7 +271,7 @@ dependencies = [ [[package]] name = "akita-sumcheck" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-error", @@ -283,7 +284,7 @@ dependencies = [ [[package]] name = "akita-types" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-challenges", @@ -291,12 +292,10 @@ dependencies = [ "akita-params", "akita-serialization", "akita-sumcheck", - "blake2 0.11.0", "jolt-field", "jolt-poly", "jolt-transcript", "rand_core 0.6.4", - "serde", "shake", "tracing", ] @@ -304,7 +303,7 @@ dependencies = [ [[package]] name = "akita-verifier" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=7109b3eac39b1b1c62a24fdab11358c84f54d1c3#7109b3eac39b1b1c62a24fdab11358c84f54d1c3" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=10a13def9d254d8becc4162b52e2f06975976ea5#10a13def9d254d8becc4162b52e2f06975976ea5" dependencies = [ "akita-algebra", "akita-challenges", diff --git a/Cargo.toml b/Cargo.toml index d2b87d78aa..d5f7c0d171 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -304,15 +304,15 @@ dory = { package = "dory-pcs", version = "0.4.2", features = [ "zk", ] } # Immutable upstream revision containing the quotient-free relation and external schedule catalogs. -akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-params = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } -akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "7109b3eac39b1b1c62a24fdab11358c84f54d1c3", default-features = false } +akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-params = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } +akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "10a13def9d254d8becc4162b52e2f06975976ea5", default-features = false } # Core Utilities clap = { version = "4.6.1", features = ["derive"] } From 8e63f669e08eb1a9ce07cbd6b1428ab168db55a5 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 04:00:31 -0400 Subject: [PATCH 53/62] fix(dory): report a proof that ends inside the parse-ahead as truncated 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. --- crates/jolt-dory/src/transcript.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/jolt-dory/src/transcript.rs b/crates/jolt-dory/src/transcript.rs index c697a50e05..fb5bbd6887 100644 --- a/crates/jolt-dory/src/transcript.rs +++ b/crates/jolt-dory/src/transcript.rs @@ -24,7 +24,7 @@ use dory::primitives::transcript::Transcript as DoryTranscript; use dory::primitives::{DoryDeserialize, DorySerialize}; use jolt_field::Fr; use jolt_openings::OpeningsError; -use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; +use jolt_transcript::{Channel, ProverTranscript, Sponge, TranscriptError, VerifierTranscript}; use crate::scheme::{jolt_fr_to_ark, ArkFr, ArkG1, ArkGT}; @@ -295,9 +295,11 @@ fn read(unread: &mut &[u8]) -> Result where T: DoryDeserialize + CompressedWidth, { + // A proof that ends early is truncated, as the live transcript would + // report on the same bytes; only a malformed element fails verification. let (bytes, rest) = unread .split_at_checked(T::BYTES) - .ok_or(OpeningsError::VerificationFailed)?; + .ok_or(OpeningsError::Transcript(TranscriptError::Truncated))?; let value = T::deserialize_compressed(bytes).map_err(|_| OpeningsError::VerificationFailed)?; *unread = rest; Ok(value) From 70c1d37d2563efd2052c556c6a7b6242400f774d Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 04:00:31 -0400 Subject: [PATCH 54/62] fix: build the verifier fuzzer and WASM verifier, and drop unused dependencies - 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. --- Cargo.lock | 4 ---- crates/jolt-blindfold/Cargo.toml | 1 - crates/jolt-sumcheck/Cargo.toml | 1 - crates/jolt-verifier/Cargo.toml | 1 - crates/jolt-verifier/src/stages/zk/blindfold/mod.rs | 2 +- jolt-sdk/Cargo.toml | 9 +++------ src/build_wasm.rs | 2 +- 7 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 118a2f21ce..de06a79a13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3184,7 +3184,6 @@ dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", "rayon", - "serde", "thiserror 2.0.20", ] @@ -3756,7 +3755,6 @@ dependencies = [ "jolt-program", "jolt-prover", "jolt-sdk-macros", - "jolt-transcript", "jolt-verifier", "jolt-witness", "postcard", @@ -3810,7 +3808,6 @@ dependencies = [ name = "jolt-sumcheck" version = "0.1.0" dependencies = [ - "bincode 2.0.1", "jolt-crypto", "jolt-field", "jolt-poly", @@ -3871,7 +3868,6 @@ name = "jolt-verifier" version = "0.1.0" dependencies = [ "allocative", - "ark-serialize 0.5.0", "bincode 2.0.1", "blake2 0.11.0", "common", diff --git a/crates/jolt-blindfold/Cargo.toml b/crates/jolt-blindfold/Cargo.toml index 78b1192c85..0b84248f6d 100644 --- a/crates/jolt-blindfold/Cargo.toml +++ b/crates/jolt-blindfold/Cargo.toml @@ -19,7 +19,6 @@ jolt-transcript.workspace = true jolt-utils.workspace = true rand_core.workspace = true rayon.workspace = true -serde = { workspace = true, features = ["derive"] } thiserror.workspace = true [dev-dependencies] diff --git a/crates/jolt-sumcheck/Cargo.toml b/crates/jolt-sumcheck/Cargo.toml index 54ee863062..849d83f4f9 100644 --- a/crates/jolt-sumcheck/Cargo.toml +++ b/crates/jolt-sumcheck/Cargo.toml @@ -34,7 +34,6 @@ thiserror.workspace = true [dev-dependencies] spongefish = { workspace = true, features = ["sha3"] } -bincode = { workspace = true } jolt-crypto = { workspace = true, features = ["bn254"] } jolt-field = { workspace = true, features = ["bn254", "solinas"] } jolt-transcript = { workspace = true, features = [ diff --git a/crates/jolt-verifier/Cargo.toml b/crates/jolt-verifier/Cargo.toml index 3a665d084a..599deb34c9 100644 --- a/crates/jolt-verifier/Cargo.toml +++ b/crates/jolt-verifier/Cargo.toml @@ -48,7 +48,6 @@ transcript-poseidon = ["jolt-transcript/transcript-poseidon"] [dependencies] allocative = { workspace = true, optional = true } -ark-serialize.workspace = true bincode.workspace = true blake2.workspace = true common.workspace = true diff --git a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs index d784206e84..4749b9ec60 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/mod.rs @@ -68,7 +68,7 @@ use jolt_claims::protocols::field_inline::{FieldInlineChallengeId, FieldInlineDe use jolt_claims::protocols::jolt::geometry::bytecode::BytecodeReadRafCommittedEvaluationInputs; use jolt_claims::protocols::jolt::relations; #[cfg(feature = "fuzzing")] -use jolt_claims::protocols::jolt::JoltExpr; +use jolt_claims::protocols::jolt::{JoltExpr, JoltOpeningId}; use jolt_claims::SumcheckDomain; use jolt_claims::{ opening, diff --git a/jolt-sdk/Cargo.toml b/jolt-sdk/Cargo.toml index ae7ac3ba6d..5cc2a8c791 100644 --- a/jolt-sdk/Cargo.toml +++ b/jolt-sdk/Cargo.toml @@ -38,7 +38,6 @@ host = [ "dep:jolt-openings", "dep:jolt-program", "dep:jolt-prover", - "dep:jolt-transcript", "dep:jolt-verifier", "dep:jolt-witness", "dep:tracer", @@ -64,14 +63,13 @@ guest-verifier = [ "dep:jolt-crypto", "dep:jolt-dory", "dep:jolt-field", - "dep:jolt-transcript", "dep:jolt-verifier", ] malloc-shim = ["jolt-platform/malloc-shim"] -transcript-poseidon = ["jolt-transcript/transcript-poseidon", "jolt-verifier?/transcript-poseidon"] -transcript-keccak = ["jolt-transcript/transcript-keccak", "jolt-verifier?/transcript-keccak"] -transcript-blake2b = ["jolt-transcript/transcript-blake2b", "jolt-verifier?/transcript-blake2b"] +transcript-poseidon = ["jolt-verifier?/transcript-poseidon"] +transcript-keccak = ["jolt-verifier?/transcript-keccak"] +transcript-blake2b = ["jolt-verifier?/transcript-blake2b"] # Jolt-SDK native features (optional capabilities) # @@ -115,7 +113,6 @@ jolt-host = { workspace = true, optional = true } jolt-openings = { workspace = true, optional = true } jolt-program = { workspace = true, optional = true } jolt-prover = { workspace = true, optional = true } -jolt-transcript = { workspace = true, optional = true } jolt-verifier = { workspace = true, optional = true } jolt-witness = { workspace = true, optional = true } tracer = { workspace = true, default-features = false, features = [ diff --git a/src/build_wasm.rs b/src/build_wasm.rs index bb7951f472..4169f72453 100644 --- a/src/build_wasm.rs +++ b/src/build_wasm.rs @@ -183,7 +183,7 @@ pub fn verify_{func_name}( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::VerifierTranscript, + jolt_sdk::ProtocolSponge, >(&preprocessing, &program_io, &proof, trusted_advice_commitment.as_ref()).is_ok() }} "# From 71dba6672bcb42196144afe7e0bac3de0246b194 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 04:00:31 -0400 Subject: [PATCH 55/62] style: import the types this branch names by path 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. --- crates/jolt-akita/src/adapters.rs | 21 ++++++++------- crates/jolt-crypto/src/commitment.rs | 9 ++++--- crates/jolt-crypto/src/ec/bn254/gt.rs | 12 ++++++--- crates/jolt-crypto/src/ec/bn254/mod.rs | 5 ++-- crates/jolt-dory/src/types.rs | 12 ++++++--- crates/jolt-field/src/algebra.rs | 6 +++-- crates/jolt-field/src/solinas/ext.rs | 21 ++++++++------- crates/jolt-field/src/solinas/fp128.rs | 9 ++++--- crates/jolt-field/tests/spine.rs | 9 ++++--- crates/jolt-openings/tests/support/mock.rs | 9 ++++--- crates/jolt-prover/src/dory/stages/stage8.rs | 8 +++--- crates/jolt-prover/src/recorder.rs | 12 ++++++--- crates/jolt-sumcheck/src/tests.rs | 3 ++- .../tests/external-consumer/Cargo.toml | 4 ++- .../external-consumer/tests/external_field.rs | 9 ++++--- .../jolt-sumcheck/tests/mersenne61_compat.rs | 9 ++++--- crates/jolt-transcript/src/grinding.rs | 3 ++- crates/jolt-transcript/src/prover.rs | 15 ++++++----- crates/jolt-transcript/src/site.rs | 18 +++---------- crates/jolt-transcript/src/sponge.rs | 12 ++++++--- crates/jolt-transcript/src/state.rs | 16 +++++++----- crates/jolt-transcript/src/verifier.rs | 15 ++++++----- crates/jolt-verifier/fuzz/Cargo.lock | 2 -- crates/jolt-verifier/src/stages/relations.rs | 3 ++- .../jolt-verifier/src/stages/stage8/verify.rs | 4 ++- crates/jolt-verifier/src/verifier.rs | 26 +++++++++++++------ .../jolt-verifier/tests/soundness/checks.rs | 18 ++++++++----- .../tests/support/fs_mutations.rs | 13 +++++++--- crates/jolt-verifier/tests/support/narg.rs | 18 ++++++++----- examples/advice-demo/src/main.rs | 3 ++- examples/modinv/src/main.rs | 3 ++- examples/recursion/src/main.rs | 5 ++-- jolt-eval/fuzz/Cargo.lock | 3 --- jolt-eval/src/guests/mod.rs | 8 +++--- jolt-sdk/src/host_utils.rs | 6 +++-- jolt-sdk/tests/verifier_api.rs | 3 ++- zklean-extractor/src/mle_ast.rs | 9 ++++--- 37 files changed, 220 insertions(+), 141 deletions(-) diff --git a/crates/jolt-akita/src/adapters.rs b/crates/jolt-akita/src/adapters.rs index abb5c6d5ea..30840d23bb 100644 --- a/crates/jolt-akita/src/adapters.rs +++ b/crates/jolt-akita/src/adapters.rs @@ -34,6 +34,9 @@ use crate::one_hot_family::{ }; pub use crate::one_hot_family::{AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256}; use crate::schedule_registry::GroupedScheduleParams; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; pub type AkitaField = akita_config::proof_optimized::fp128::Field; pub(crate) type AkitaConfig = JoltDenseBounded; @@ -1067,20 +1070,20 @@ impl CommitmentHeader { } impl CanonicalBytes for CommitmentHeader { - const NUM_BYTES: usize = 1 + Self::DIGEST_BYTES + Self::WORDS * u64::NUM_BYTES; + const NUM_BYTES: usize = 1 + Self::DIGEST_BYTES + Self::WORDS * size_of::(); fn to_bytes_le(&self, out: &mut [u8]) { let (tag, rest) = out.split_at_mut(1); tag.copy_from_slice(&[self.backend_flavor.wire_tag()]); let (digest, words) = rest.split_at_mut(Self::DIGEST_BYTES); digest.copy_from_slice(&self.layout_digest); - for (out, word) in words.chunks_exact_mut(u64::NUM_BYTES).zip(self.words()) { + for (out, word) in words.chunks_exact_mut(size_of::()).zip(self.words()) { (word as u64).to_bytes_le(out); } } } -impl ::spongefish::Encoding<[u8]> for CommitmentHeader { +impl Encoding<[u8]> for CommitmentHeader { fn encode(&self) -> impl AsRef<[u8]> { ::jolt_field::narg::encode(self) } @@ -1096,7 +1099,7 @@ impl CanonicalDecode for CommitmentHeader { } let (&tag, rest) = bytes.split_first()?; let (digest, words) = rest.split_first_chunk::<{ Self::DIGEST_BYTES }>()?; - let mut words = words.chunks_exact(u64::NUM_BYTES).map(|word| { + let mut words = words.chunks_exact(size_of::()).map(|word| { let word = u64::from_le_bytes(word.try_into().ok()?); usize::try_from(word).ok() }); @@ -1117,8 +1120,8 @@ impl CanonicalDecode for CommitmentHeader { } } -impl ::spongefish::NargDeserialize for CommitmentHeader { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for CommitmentHeader { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { ::jolt_field::narg::deserialize(buf) } } @@ -1148,7 +1151,7 @@ impl CanonicalBytes for AkitaHidingCommitment { } } -impl ::spongefish::Encoding<[u8]> for AkitaHidingCommitment { +impl Encoding<[u8]> for AkitaHidingCommitment { fn encode(&self) -> impl AsRef<[u8]> { ::jolt_field::narg::encode(self) } @@ -1160,8 +1163,8 @@ impl CanonicalDecode for AkitaHidingCommitment { } } -impl ::spongefish::NargDeserialize for AkitaHidingCommitment { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for AkitaHidingCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { ::jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-crypto/src/commitment.rs b/crates/jolt-crypto/src/commitment.rs index 9758929bf3..f9c23fca0a 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -7,6 +7,9 @@ use std::{ use jolt_field::{Accumulator, CanonicalBytes, CanonicalDecode, JoltField, WithAccumulator}; use jolt_poly::EqPolynomial; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[cfg(feature = "parallel")] const PAR_THRESHOLD: usize = 1024; @@ -54,7 +57,7 @@ impl CanonicalBytes for NoCommitment { fn to_bytes_le(&self, _out: &mut [u8]) {} } -impl ::spongefish::Encoding<[u8]> for NoCommitment { +impl Encoding<[u8]> for NoCommitment { fn encode(&self) -> impl AsRef<[u8]> { ::jolt_field::narg::encode(self) } @@ -66,8 +69,8 @@ impl CanonicalDecode for NoCommitment { } } -impl ::spongefish::NargDeserialize for NoCommitment { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for NoCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { ::jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-crypto/src/ec/bn254/gt.rs b/crates/jolt-crypto/src/ec/bn254/gt.rs index c6100a43d6..f2d8a0343a 100644 --- a/crates/jolt-crypto/src/ec/bn254/gt.rs +++ b/crates/jolt-crypto/src/ec/bn254/gt.rs @@ -9,6 +9,10 @@ use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField}; use crate::JoltGroup; use super::field_to_fr; +use serde::de::Error; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; /// BN254 target group element (pairing output). /// @@ -198,7 +202,7 @@ impl serde::Serialize for Bn254GT { impl<'de> serde::Deserialize<'de> for Bn254GT { fn deserialize>(deserializer: D) -> Result { let buf = >::deserialize(deserializer)?; - Self::decode_compressed(&buf).map_err(serde::de::Error::custom) + Self::decode_compressed(&buf).map_err(Error::custom) } } @@ -256,7 +260,7 @@ impl CanonicalBytes for Bn254GT { } } -impl ::spongefish::Encoding<[u8]> for Bn254GT { +impl Encoding<[u8]> for Bn254GT { fn encode(&self) -> impl AsRef<[u8]> { ::jolt_field::narg::encode(self) } @@ -268,8 +272,8 @@ impl CanonicalDecode for Bn254GT { } } -impl ::spongefish::NargDeserialize for Bn254GT { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for Bn254GT { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { ::jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-crypto/src/ec/bn254/mod.rs b/crates/jolt-crypto/src/ec/bn254/mod.rs index 5e45871994..06fa310374 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -359,10 +359,11 @@ mod tests { use super::{Bn254, Bn254G1, Bn254G2}; use crate::JoltGroup; + use std::fmt::Debug; #[test] fn canonical_codec_is_the_compressed_encoding() { - fn check( + fn check( point: P, compressed: &impl CanonicalSerialize, ) { @@ -385,7 +386,7 @@ mod tests { /// alias it. #[test] fn identity_has_one_accepted_encoding() { - fn check(identity: P) { + fn check(identity: P) { let canonical = identity.to_bytes_le_vec(); assert_eq!(P::from_bytes_le_checked(&canonical), Some(identity)); let mut alias = canonical; diff --git a/crates/jolt-dory/src/types.rs b/crates/jolt-dory/src/types.rs index b42958ac74..0c9ff12bb2 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -5,6 +5,9 @@ use dory::backends::arkworks::{ArkG1, ArkG2, ArkGT, ArkworksProverSetup, Arkwork use jolt_crypto::{Bn254G1, Bn254GT, HomomorphicCommitment}; use jolt_field::{CanonicalBytes, CanonicalDecode, Fr}; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; /// Bounds the rounds any supported proof can use, and so the verifier setup's /// per-round tables. Dory runs `ceil(num_vars / 2)` rounds, so 64 covers @@ -45,7 +48,7 @@ impl CanonicalBytes for DoryCommitment { } } -impl ::spongefish::Encoding<[u8]> for DoryCommitment { +impl Encoding<[u8]> for DoryCommitment { fn encode(&self) -> impl AsRef<[u8]> { ::jolt_field::narg::encode(self) } @@ -57,8 +60,8 @@ impl CanonicalDecode for DoryCommitment { } } -impl ::spongefish::NargDeserialize for DoryCommitment { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for DoryCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { ::jolt_field::narg::deserialize(buf) } } @@ -214,6 +217,7 @@ mod tests { use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; + use crate::DoryScheme; use jolt_field::Fr; #[test] @@ -253,7 +257,7 @@ mod tests { crate::DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); let mut prove_transcript = crate::test_support::prover(b"serde-vs"); - crate::DoryScheme::open( + DoryScheme::open( &poly, &point, eval, diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index ceb2f8ead9..69c4797269 100644 --- a/crates/jolt-field/src/algebra.rs +++ b/crates/jolt-field/src/algebra.rs @@ -20,6 +20,8 @@ use std::iter::{Product, Sum}; use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::signed::S256; +use spongefish::Encoding; +use spongefish::NargDeserialize; pub trait AdditiveGroup: Sized @@ -256,7 +258,7 @@ pub trait PseudoMersenne: Field + CanonicalEncoding { /// [`NUM_BYTES`](Self::NUM_BYTES) bytes of the unique representative. /// - The spongefish [`Encoding`](spongefish::Encoding) is this encoding; /// implement it with [`narg::encode`]. -pub trait CanonicalBytes: spongefish::Encoding<[u8]> { +pub trait CanonicalBytes: Encoding<[u8]> { /// Byte length of the fixed-size canonical encoding. const NUM_BYTES: usize; @@ -284,7 +286,7 @@ pub trait CanonicalBytes: spongefish::Encoding<[u8]> { /// [`CanonicalBytes::NUM_BYTES`], decodes to `None`. /// - The spongefish [`NargDeserialize`](spongefish::NargDeserialize) reads /// exactly this decoding; implement it with [`narg::deserialize`]. -pub trait CanonicalDecode: CanonicalBytes + spongefish::NargDeserialize + Sized { +pub trait CanonicalDecode: CanonicalBytes + NargDeserialize + Sized { /// Decodes exactly [`CanonicalBytes::NUM_BYTES`] canonical bytes; /// `None` on wrong length or a non-canonical value. fn from_bytes_le_checked(bytes: &[u8]) -> Option; diff --git a/crates/jolt-field/src/solinas/ext.rs b/crates/jolt-field/src/solinas/ext.rs index 8f8cb77d74..7f8734e9c2 100644 --- a/crates/jolt-field/src/solinas/ext.rs +++ b/crates/jolt-field/src/solinas/ext.rs @@ -33,6 +33,9 @@ use crate::{ use bytemuck::{CheckedBitPattern, NoUninit, Pod, Zeroable}; use num_traits::Zero; use rand_core::RngCore; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; use std::marker::PhantomData; /// Quadratic extension element `c0 + c1·u` with `u^2 = NR` given by the @@ -142,7 +145,7 @@ impl> CanonicalBytes for FpExt2> ::spongefish::Encoding<[u8]> for FpExt2 { +impl> Encoding<[u8]> for FpExt2 { fn encode(&self) -> impl AsRef<[u8]> { crate::narg::encode(self) } @@ -155,8 +158,8 @@ impl> CanonicalDecode for FpExt2> ::spongefish::NargDeserialize for FpExt2 { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl> NargDeserialize for FpExt2 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { crate::narg::deserialize(buf) } } @@ -364,7 +367,7 @@ impl CanonicalBytes for FpExt4 { } } -impl ::spongefish::Encoding<[u8]> for FpExt4 { +impl Encoding<[u8]> for FpExt4 { fn encode(&self) -> impl AsRef<[u8]> { crate::narg::encode(self) } @@ -376,8 +379,8 @@ impl CanonicalDecode for FpExt4 { } } -impl ::spongefish::NargDeserialize for FpExt4 { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for FpExt4 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { crate::narg::deserialize(buf) } } @@ -520,7 +523,7 @@ impl CanonicalBytes for FpExt8 { } } -impl ::spongefish::Encoding<[u8]> for FpExt8 { +impl Encoding<[u8]> for FpExt8 { fn encode(&self) -> impl AsRef<[u8]> { crate::narg::encode(self) } @@ -532,8 +535,8 @@ impl CanonicalDecode for FpExt8 { } } -impl ::spongefish::NargDeserialize for FpExt8 { - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for FpExt8 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { crate::narg::deserialize(buf) } } diff --git a/crates/jolt-field/src/solinas/fp128.rs b/crates/jolt-field/src/solinas/fp128.rs index 9f9a0327d0..7673a2f1b2 100644 --- a/crates/jolt-field/src/solinas/fp128.rs +++ b/crates/jolt-field/src/solinas/fp128.rs @@ -26,6 +26,9 @@ use crate::{ #[cfg(feature = "bytemuck")] use bytemuck::{CheckedBitPattern, NoUninit, Zeroable}; use rand_core::RngCore; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[cfg(all(feature = "asm", any(target_arch = "aarch64", target_arch = "x86_64")))] use std::arch::asm; #[cfg(all(feature = "fuzzing", target_arch = "x86_64"))] @@ -1123,7 +1126,7 @@ impl CanonicalBytes for Fp128

{ } } -impl ::spongefish::Encoding<[u8]> for Fp128

{ +impl Encoding<[u8]> for Fp128

{ fn encode(&self) -> impl AsRef<[u8]> { crate::narg::encode(self) } @@ -1137,8 +1140,8 @@ impl CanonicalDecode for Fp128

{ } } -impl ::spongefish::NargDeserialize for Fp128

{ - fn deserialize_from_narg(buf: &mut &[u8]) -> ::spongefish::VerificationResult { +impl NargDeserialize for Fp128

{ + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { crate::narg::deserialize(buf) } } diff --git a/crates/jolt-field/tests/spine.rs b/crates/jolt-field/tests/spine.rs index 8f267f190a..2b174a61b5 100644 --- a/crates/jolt-field/tests/spine.rs +++ b/crates/jolt-field/tests/spine.rs @@ -12,6 +12,9 @@ use jolt_field::{ }; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; const P: u64 = (1 << 61) - 1; @@ -88,7 +91,7 @@ impl CanonicalBytes for M61 { } } -impl spongefish::Encoding<[u8]> for M61 { +impl Encoding<[u8]> for M61 { fn encode(&self) -> impl AsRef<[u8]> { jolt_field::narg::encode(self) } @@ -101,8 +104,8 @@ impl CanonicalDecode for M61 { } } -impl spongefish::NargDeserialize for M61 { - fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { +impl NargDeserialize for M61 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-openings/tests/support/mock.rs b/crates/jolt-openings/tests/support/mock.rs index 947892df71..3fe5d3ea9f 100644 --- a/crates/jolt-openings/tests/support/mock.rs +++ b/crates/jolt-openings/tests/support/mock.rs @@ -6,6 +6,9 @@ use jolt_openings::{AdditivelyHomomorphic, CommitmentScheme, OpeningsError, ZkOp use jolt_poly::{MultilinearPoly, Polynomial}; use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] @@ -172,7 +175,7 @@ impl CanonicalBytes for MockHidingCommitment { } } -impl spongefish::Encoding<[u8]> for MockHidingCommitment { +impl Encoding<[u8]> for MockHidingCommitment { fn encode(&self) -> impl AsRef<[u8]> { jolt_field::narg::encode(self) } @@ -184,8 +187,8 @@ impl CanonicalDecode for MockHidingCommitment { } } -impl spongefish::NargDeserialize for MockHidingCommitment { - fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { +impl NargDeserialize for MockHidingCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-prover/src/dory/stages/stage8.rs b/crates/jolt-prover/src/dory/stages/stage8.rs index 3a5626adf8..6b8406f6d7 100644 --- a/crates/jolt-prover/src/dory/stages/stage8.rs +++ b/crates/jolt-prover/src/dory/stages/stage8.rs @@ -58,6 +58,8 @@ use jolt_verifier::{CheckedInputs, VerifierError}; use jolt_witness::JoltWitnessPlane; use crate::{CommittedProgramCandidates, JoltProverPreprocessing, ProverConfig, ProverError}; +#[cfg(not(feature = "zk"))] +use core::marker::PhantomData; /// Stage 8's ZK output: the joint evaluation and its blind, the secrets inside /// the hiding evaluation commitment that the BlindFold final-opening binding @@ -71,7 +73,7 @@ pub struct Stage8ProverOutput { /// Stage 8's clear output: the opening is written to the transcript. #[cfg(not(feature = "zk"))] pub struct Stage8ProverOutput { - _pcs: core::marker::PhantomData, + _pcs: PhantomData, } /// Prove stage 8 on `transcript` (positioned at the stage-7 boundary). @@ -336,9 +338,7 @@ where ) .map_err(KernelError::::from)?; - Ok(Stage8ProverOutput { - _pcs: core::marker::PhantomData, - }) + Ok(Stage8ProverOutput { _pcs: PhantomData }) } #[cfg(feature = "zk")] { diff --git a/crates/jolt-prover/src/recorder.rs b/crates/jolt-prover/src/recorder.rs index 560683c3b5..2f3505ea87 100644 --- a/crates/jolt-prover/src/recorder.rs +++ b/crates/jolt-prover/src/recorder.rs @@ -22,11 +22,17 @@ use jolt_sumcheck::{prove_uniskip_committed, CommittedSumcheckRecorder, Committe use jolt_transcript::{ProverTranscript, Sponge}; use crate::ProverError; +#[cfg(all(test, not(feature = "zk")))] +use jolt_sumcheck::CommittedSumcheckRecorder; +#[cfg(feature = "zk")] +use rand_core::OsRng; +#[cfg(any(feature = "zk", test))] +use rand_core::RngCore; /// The compiled mode's batch recorder type. #[cfg(feature = "zk")] pub type ModeRecorder<'a, VC> = - CommittedSumcheckRecorder<'a, ::Field, VC, rand_core::OsRng>; + CommittedSumcheckRecorder<'a, ::Field, VC, OsRng>; #[cfg(not(feature = "zk"))] pub type ModeRecorder<'a, VC> = ClearSumcheckRecorder<::Field>; @@ -44,11 +50,11 @@ impl ClaimRecorder for ClearSumcheckRecorder { // The driver's twin tests run a committed recorder in clear builds too. #[cfg(any(feature = "zk", test))] -impl ClaimRecorder for jolt_sumcheck::CommittedSumcheckRecorder<'_, F, VC, R> +impl ClaimRecorder for CommittedSumcheckRecorder<'_, F, VC, R> where F: JoltField, VC: VectorCommitment, - R: rand_core::RngCore, + R: RngCore, { const RECORDS_STAGED: bool = true; } diff --git a/crates/jolt-sumcheck/src/tests.rs b/crates/jolt-sumcheck/src/tests.rs index 4dfacaae71..cf7d310f51 100644 --- a/crates/jolt-sumcheck/src/tests.rs +++ b/crates/jolt-sumcheck/src/tests.rs @@ -29,6 +29,7 @@ use crate::round_proof::{ receive_compressed_round, receive_full_round, send_compressed_round, send_full_round, }; use crate::verifier::SumcheckVerifier; +use jolt_poly::EvaluationClaim; type F = Fr; type H = Blake2b512; @@ -111,7 +112,7 @@ fn verify_single_round( degree: usize, claimed_sum: F, domain: CenteredIntegerDomain, -) -> Result, SumcheckError> { +) -> Result, SumcheckError> { let mut transcript = prover(b"integer-domain"); send_full_round(round, degree, &mut transcript).unwrap(); let narg = transcript.finish(); diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml index be3a285ce6..f12e06e827 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml @@ -12,7 +12,9 @@ ignored = ["jolt-field", "jolt-poly", "jolt-sumcheck", "jolt-transcript"] [dependencies] # An external field implements the spongefish codec traits the transcript reads atoms through. -spongefish = { git = "https://github.com/arkworks-rs/spongefish", rev = "ef9741346a150039427d9e0c02d6b2d73e93ec81", default-features = false, features = ["sha3"] } +spongefish = { git = "https://github.com/arkworks-rs/spongefish", rev = "ef9741346a150039427d9e0c02d6b2d73e93ec81", default-features = false, features = [ + "sha3", +] } jolt-field = { path = "../../../jolt-field", default-features = false, features = [ "solinas", ] } diff --git a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs index 69a688c749..98f98edff8 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs +++ b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs @@ -15,6 +15,9 @@ use jolt_sumcheck::{ use jolt_transcript::{Channel, Keccak, ProtocolId, ProverTranscript, VerifierTranscript}; use num_traits::{One, Zero}; use rand_core::RngCore; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] struct ExternalField(Prime64Offset59); @@ -181,7 +184,7 @@ impl CanonicalBytes for ExternalField { } } -impl spongefish::Encoding<[u8]> for ExternalField { +impl Encoding<[u8]> for ExternalField { fn encode(&self) -> impl AsRef<[u8]> { jolt_field::narg::encode(self) } @@ -193,8 +196,8 @@ impl CanonicalDecode for ExternalField { } } -impl spongefish::NargDeserialize for ExternalField { - fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { +impl NargDeserialize for ExternalField { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-sumcheck/tests/mersenne61_compat.rs b/crates/jolt-sumcheck/tests/mersenne61_compat.rs index 1cde0ad35b..3710c45b2d 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -26,6 +26,9 @@ use jolt_transcript::{ Blake2b512, Channel, Keccak, ProtocolId, ProverTranscript, Sponge, VerifierTranscript, }; use num_traits::{One, Zero}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; const MODULUS: u64 = (1u64 << 61) - 1; @@ -250,7 +253,7 @@ impl CanonicalBytes for Mersenne61 { } } -impl spongefish::Encoding<[u8]> for Mersenne61 { +impl Encoding<[u8]> for Mersenne61 { fn encode(&self) -> impl AsRef<[u8]> { jolt_field::narg::encode(self) } @@ -263,8 +266,8 @@ impl CanonicalDecode for Mersenne61 { } } -impl spongefish::NargDeserialize for Mersenne61 { - fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { +impl NargDeserialize for Mersenne61 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { jolt_field::narg::deserialize(buf) } } diff --git a/crates/jolt-transcript/src/grinding.rs b/crates/jolt-transcript/src/grinding.rs index 5d8e13977d..2ffae66e11 100644 --- a/crates/jolt-transcript/src/grinding.rs +++ b/crates/jolt-transcript/src/grinding.rs @@ -10,6 +10,7 @@ use std::num::NonZeroU8; +use crate::FORK_SEED_LEN; use crate::{Fork, Sponge}; /// Extra nonce bits searched beyond the difficulty. @@ -19,7 +20,7 @@ pub const MAX_GRINDING_BITS: u8 = u32::BITS as u8 - GRINDING_NONCE_SLACK_BITS; /// Byte length of the squeezed proof-of-work predicate. pub const GRINDING_PREDICATE_LEN: usize = 32; /// Byte length of the seed a grind squeezes from the transcript. -pub(crate) const GRINDING_SEED_LEN: usize = crate::FORK_SEED_LEN; +pub(crate) const GRINDING_SEED_LEN: usize = FORK_SEED_LEN; /// Returns whether the low `bits` bits of `predicate`, low byte first and low /// bit first within each byte, are all zero. #[must_use] diff --git a/crates/jolt-transcript/src/prover.rs b/crates/jolt-transcript/src/prover.rs index 8f99aaa8ba..802497727c 100644 --- a/crates/jolt-transcript/src/prover.rs +++ b/crates/jolt-transcript/src/prover.rs @@ -1,5 +1,6 @@ //! The prover's end: spongefish's prover state, written through typed atoms. +use core::fmt::Result as FmtResult; use std::num::NonZeroU8; use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; @@ -8,10 +9,13 @@ use spongefish::ProverState; use crate::grinding::{grind_nonce, nonce_bits, GRINDING_SEED_LEN}; use crate::site::{Log, TranscriptOp}; +use crate::state::SMALL_CHALLENGE_BYTES; use crate::state::{domain, Framed, Squeeze, BYTE_BLOCK}; #[cfg(feature = "logging")] use crate::TranscriptEvent; use crate::{Channel, Nonce, ProtocolId, SiteId, Sponge, TranscriptError}; +use core::fmt::Debug; +use core::fmt::Formatter; /// Prover transcript: spongefish's [`ProverState`], which appends every prover /// message to the argument string and absorbs exactly its encoding. @@ -23,8 +27,8 @@ pub struct ProverTranscript { log: Log, } -impl core::fmt::Debug for ProverTranscript { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Debug for ProverTranscript { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.debug_struct("ProverTranscript") .field("narg_len", &self.state.narg_string().len()) .finish_non_exhaustive() @@ -193,11 +197,8 @@ impl Channel for ProverTranscript { fn challenge_small(&mut self) -> F { let value = self.state.small_challenge(); - self.log.record( - TranscriptOp::Challenge, - crate::state::SMALL_CHALLENGE_BYTES, - None, - ); + self.log + .record(TranscriptOp::Challenge, SMALL_CHALLENGE_BYTES, None); value } diff --git a/crates/jolt-transcript/src/site.rs b/crates/jolt-transcript/src/site.rs index 5dcade41ed..3e1a605cef 100644 --- a/crates/jolt-transcript/src/site.rs +++ b/crates/jolt-transcript/src/site.rs @@ -4,6 +4,7 @@ //! and without the `logging` feature. Under `logging`, each transcript records //! one [`TranscriptEvent`] per operation, tagged with the site set by the most //! recent [`Channel::site`](crate::Channel::site) call. +use core::ops::Range; /// Opaque 32-byte name of a protocol site, chosen by the protocol. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -56,7 +57,7 @@ pub struct TranscriptEvent { /// Bytes absorbed or squeezed. pub len: usize, /// Argument-string byte range of a prover message. - pub narg: Option>, + pub narg: Option>, } /// Event log of one transcript; empty and free without the `logging` feature. @@ -74,12 +75,7 @@ impl Log { self.site = site; } - pub(crate) fn record( - &mut self, - op: TranscriptOp, - len: usize, - narg: Option>, - ) { + pub(crate) fn record(&mut self, op: TranscriptOp, len: usize, narg: Option>) { self.events.push(TranscriptEvent { site: self.site, op, @@ -103,11 +99,5 @@ impl Log { pub(crate) fn set_site(&mut self, _site: SiteId) {} #[inline(always)] - pub(crate) fn record( - &mut self, - _op: TranscriptOp, - _len: usize, - _narg: Option>, - ) { - } + pub(crate) fn record(&mut self, _op: TranscriptOp, _len: usize, _narg: Option>) {} } diff --git a/crates/jolt-transcript/src/sponge.rs b/crates/jolt-transcript/src/sponge.rs index 6fec2ac528..bdada88801 100644 --- a/crates/jolt-transcript/src/sponge.rs +++ b/crates/jolt-transcript/src/sponge.rs @@ -1,5 +1,11 @@ //! Duplex sponges a transcript can run on. +#[cfg(feature = "transcript-poseidon")] +use crate::PoseidonSponge; +#[cfg(feature = "transcript-blake2b")] +use spongefish::instantiations::Blake2b512; +#[cfg(feature = "transcript-keccak")] +use spongefish::instantiations::Keccak; use spongefish::DuplexSpongeInterface; /// A byte-oriented duplex sponge with a stable identity. @@ -14,16 +20,16 @@ pub trait Sponge: DuplexSpongeInterface + Default + Clone + Send + Sync } #[cfg(feature = "transcript-blake2b")] -impl Sponge for spongefish::instantiations::Blake2b512 { +impl Sponge for Blake2b512 { const ID: &'static str = "blake2b512"; } #[cfg(feature = "transcript-keccak")] -impl Sponge for spongefish::instantiations::Keccak { +impl Sponge for Keccak { const ID: &'static str = "keccak-f1600"; } #[cfg(feature = "transcript-poseidon")] -impl Sponge for crate::PoseidonSponge { +impl Sponge for PoseidonSponge { const ID: &'static str = "poseidon-bn254-circom-t4"; } diff --git a/crates/jolt-transcript/src/state.rs b/crates/jolt-transcript/src/state.rs index c1243099ac..9a645d1f5c 100644 --- a/crates/jolt-transcript/src/state.rs +++ b/crates/jolt-transcript/src/state.rs @@ -5,6 +5,11 @@ use rand_core::RngCore; use spongefish::{Encoding, ProverState, VerifierState}; use crate::{ProtocolId, Sponge}; +use rand::rngs::StdRng; +use rand_core::Error; +use spongefish::DomainSeparator; +use spongefish::WithInstance; +use spongefish::WithSession; /// Bytes squeezed for a [`Channel::challenge_small`](crate::Channel::challenge_small). pub(crate) const SMALL_CHALLENGE_BYTES: usize = 16; @@ -35,11 +40,8 @@ impl Encoding<[u8]> for Framed<'_> { pub(crate) fn domain<'s>( protocol: &ProtocolId, session: &'s [u8], -) -> spongefish::DomainSeparator< - spongefish::WithInstance>, - spongefish::WithSession>, -> { - spongefish::DomainSeparator::new(*protocol.as_bytes()) +) -> DomainSeparator>, WithSession>> { + DomainSeparator::new(*protocol.as_bytes()) .session(Framed(session)) .instance(Framed(&[])) } @@ -89,7 +91,7 @@ macro_rules! squeeze_blocks { }}; } -impl Squeeze for ProverState { +impl Squeeze for ProverState { fn squeeze_into(&mut self, out: &mut [u8]) { squeeze_blocks!(self, out); } @@ -125,7 +127,7 @@ impl RngCore for SqueezeRng<'_, S> { self.squeezed += dest.len(); } - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { self.fill_bytes(dest); Ok(()) } diff --git a/crates/jolt-transcript/src/verifier.rs b/crates/jolt-transcript/src/verifier.rs index e2edcf3f3b..5e7a7614ef 100644 --- a/crates/jolt-transcript/src/verifier.rs +++ b/crates/jolt-transcript/src/verifier.rs @@ -1,5 +1,6 @@ //! The verifier's end: spongefish's verifier state, read through typed atoms. +use core::fmt::Result as FmtResult; use std::num::NonZeroU8; use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; @@ -8,10 +9,13 @@ use spongefish::VerifierState; use crate::grinding::{grinding_accepts, nonce_bits, GRINDING_SEED_LEN}; use crate::nonce::NONCE_MAX_BYTES; use crate::site::{Log, TranscriptOp}; +use crate::state::SMALL_CHALLENGE_BYTES; use crate::state::{domain, Framed, Squeeze, BYTE_BLOCK}; #[cfg(feature = "logging")] use crate::TranscriptEvent; use crate::{Channel, Nonce, ProtocolId, SiteId, Sponge, TranscriptError}; +use core::fmt::Debug; +use core::fmt::Formatter; /// Verifier transcript: spongefish's [`VerifierState`] over the proof bytes. /// @@ -28,8 +32,8 @@ pub struct VerifierTranscript<'a, H: Sponge> { log: Log, } -impl core::fmt::Debug for VerifierTranscript<'_, H> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +impl Debug for VerifierTranscript<'_, H> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { f.debug_struct("VerifierTranscript") .field("consumed", &self.consumed) .field("remaining", &self.remaining()) @@ -317,11 +321,8 @@ impl Channel for VerifierTranscript<'_, H> { fn challenge_small(&mut self) -> F { let value = self.state.small_challenge(); - self.log.record( - TranscriptOp::Challenge, - crate::state::SMALL_CHALLENGE_BYTES, - None, - ); + self.log + .record(TranscriptOp::Challenge, SMALL_CHALLENGE_BYTES, None); value } diff --git a/crates/jolt-verifier/fuzz/Cargo.lock b/crates/jolt-verifier/fuzz/Cargo.lock index 3dac360331..8d0523b44e 100644 --- a/crates/jolt-verifier/fuzz/Cargo.lock +++ b/crates/jolt-verifier/fuzz/Cargo.lock @@ -583,7 +583,6 @@ dependencies = [ "jolt-utils", "rand_core 0.6.4", "rayon", - "serde", "thiserror 2.0.19", ] @@ -794,7 +793,6 @@ dependencies = [ name = "jolt-verifier" version = "0.1.0" dependencies = [ - "ark-serialize", "bincode 2.0.1", "blake2 0.11.0-rc.6", "common", diff --git a/crates/jolt-verifier/src/stages/relations.rs b/crates/jolt-verifier/src/stages/relations.rs index 365b1f99cd..0cfd75e997 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -1044,6 +1044,7 @@ mod sumcheck_batch_derive_tests { InstructionReadRaf, InstructionReadRafOutputClaims, RegistersValEvaluation, RegistersValEvaluationOutputClaims, }; + use crate::VerifierError; use jolt_claims::protocols::jolt::geometry::instruction::InstructionReadRafDimensions; use jolt_field::{Fr, JoltField, Ring}; @@ -1154,7 +1155,7 @@ mod sumcheck_batch_derive_tests { .unwrap(); assert!(matches!( sumchecks.validate_output_shape(&unexpected, &points), - Err(crate::VerifierError::StageClaimSumcheckFailed { .. }) + Err(VerifierError::StageClaimSumcheckFailed { .. }) )); let well_formed = FixtureOptionOutputClaims:: { diff --git a/crates/jolt-verifier/src/stages/stage8/verify.rs b/crates/jolt-verifier/src/stages/stage8/verify.rs index b793b86723..03d14e2d3f 100644 --- a/crates/jolt-verifier/src/stages/stage8/verify.rs +++ b/crates/jolt-verifier/src/stages/stage8/verify.rs @@ -3,6 +3,8 @@ use super::outputs::Stage8Output; use super::outputs::{Stage8ClearOutput, Stage8ZkOutput}; #[cfg(not(feature = "akita"))] use super::precommitted::{precommitted_final_openings, PrecommittedFinalOpening}; +#[cfg(feature = "akita")] +use super::OneHotTraceCommitmentMetadata; #[cfg(not(feature = "akita"))] use crate::proof::JoltCommitments; use crate::proof::ProofCommitments; @@ -630,7 +632,7 @@ pub fn verify( where F: JoltField, PCS: CommitmentScheme, - PCS::Output: Clone + super::OneHotTraceCommitmentMetadata, + PCS::Output: Clone + OneHotTraceCommitmentMetadata, PCS::VerifierSetup: super::OneHotTraceSetupMetadata, VC: VectorCommitment, H: Sponge, diff --git a/crates/jolt-verifier/src/verifier.rs b/crates/jolt-verifier/src/verifier.rs index 7d43f24138..d70d916ec2 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -34,19 +34,31 @@ use crate::{ }, VerifierError, }; +use common::jolt_device::MemoryLayout; +#[cfg(not(any(feature = "transcript-keccak", feature = "transcript-poseidon")))] +use jolt_transcript::Blake2b512; +#[cfg(all(feature = "transcript-keccak", not(feature = "transcript-poseidon")))] +use jolt_transcript::Keccak; +#[cfg(feature = "transcript-poseidon")] +use jolt_transcript::PoseidonSponge; + +#[cfg(feature = "akita")] +use crate::stages::stage8::OneHotTraceCommitmentMetadata; +#[cfg(not(feature = "akita"))] +use crate::stages::stage8::Stage8Output; /// The sponge Jolt proofs run on: Poseidon under `transcript-poseidon`, else /// Keccak under `transcript-keccak`, else Blake2b. Every prover and verifier /// entry point is generic over the sponge; this alias is the one place the /// default is chosen, and the SDK's `ProtocolSponge` is this alias. #[cfg(feature = "transcript-poseidon")] -pub type JoltSponge = jolt_transcript::PoseidonSponge; +pub type JoltSponge = PoseidonSponge; /// See the Poseidon arm. #[cfg(all(feature = "transcript-keccak", not(feature = "transcript-poseidon")))] -pub type JoltSponge = jolt_transcript::Keccak; +pub type JoltSponge = Keccak; /// See the Poseidon arm. #[cfg(not(any(feature = "transcript-keccak", feature = "transcript-poseidon")))] -pub type JoltSponge = jolt_transcript::Blake2b512; +pub type JoltSponge = Blake2b512; /// The session every Jolt transcript is bound to. pub const JOLT_SESSION: &[u8] = b""; @@ -194,7 +206,7 @@ where )?; if !checked.zk { - let stage8::Stage8Output::Clear(_) = stage8 else { + let Stage8Output::Clear(_) = stage8 else { return Err(VerifierError::ExpectedClearProof { field: "stage8" }); }; return Ok(VerifiedStages::Clear); @@ -232,7 +244,7 @@ pub fn verify( where F: JoltField, PCS: CommitmentScheme, - PCS::Output: Clone + stage8::OneHotTraceCommitmentMetadata, + PCS::Output: Clone + OneHotTraceCommitmentMetadata, PCS::VerifierSetup: stage8::OneHotTraceSetupMetadata, VC: VectorCommitment, VC::Output: Copy + CanonicalDecode, @@ -673,9 +685,7 @@ where /// access at remapped word zero (see "Where the RAM activation is pinned" in /// `specs/lattice-claims.md`). Mirrors the prover-side /// `UnmapRamAddressPolynomial::new` assertion (`start_address > 8`). -fn validate_ram_remap_base( - memory_layout: &common::jolt_device::MemoryLayout, -) -> Result<(), VerifierError> { +fn validate_ram_remap_base(memory_layout: &MemoryLayout) -> Result<(), VerifierError> { let lowest_address = memory_layout.get_lowest_address(); if lowest_address <= 8 { return Err(VerifierError::InvalidMemoryLayout { diff --git a/crates/jolt-verifier/tests/soundness/checks.rs b/crates/jolt-verifier/tests/soundness/checks.rs index 9f4ea14a47..0584c9dfe9 100644 --- a/crates/jolt-verifier/tests/soundness/checks.rs +++ b/crates/jolt-verifier/tests/soundness/checks.rs @@ -21,6 +21,12 @@ use crate::support::narg::{ assert_coverage_closure, assert_truncations_reject, decode_header, message_in, sweep, with_header, CommitmentOf, Region, TracedCase, }; +use jolt_crypto::VectorCommitment; +use jolt_transcript::VerifierTranscript; +use jolt_verifier::CommittedProgramPreprocessing; +use jolt_verifier::JoltProtocolConfig; +use jolt_verifier::JoltSponge; +use jolt_verifier::JOLT_SESSION; /// Coverage closure, then the single-byte message sweep with deadlines. /// `budget` caps the number of tampers (`None`: every tamper). @@ -69,7 +75,7 @@ pub fn structural_tampers(case: &C, truncation_budget: Option Vec { +fn wrong_protocols(proof: &JoltProof) -> Vec { use jolt_verifier::config::CommitmentConfig; use jolt_verifier::ZkConfig; @@ -243,9 +249,9 @@ pub fn trusted_advice_tampers(case: &C, replacement: &CommitmentO pub fn sent_commitment(case: &C, index: usize) -> CommitmentOf { let trace = case.honest_trace(); let start = message_in(&trace, Region::Commitments, 0).start; - let mut transcript = jolt_transcript::VerifierTranscript::::new( - &jolt_verifier::jolt_protocol_id::(), - jolt_verifier::JOLT_SESSION, + let mut transcript = VerifierTranscript::::new( + &jolt_verifier::jolt_protocol_id::(), + JOLT_SESSION, &case.proof().narg[start..], ); let mut receive = || { @@ -433,9 +439,9 @@ pub fn committed_program_tampers( /// Mutable committed-program preprocessing. pub fn committed_mut( preprocessing: &mut JoltVerifierPreprocessing, -) -> &mut jolt_verifier::CommittedProgramPreprocessing +) -> &mut CommittedProgramPreprocessing where - VC: jolt_crypto::VectorCommitment, + VC: VectorCommitment, { let ProgramPreprocessing::Committed(committed) = &mut preprocessing.program else { panic!("fixture must carry committed-program preprocessing"); diff --git a/crates/jolt-verifier/tests/support/fs_mutations.rs b/crates/jolt-verifier/tests/support/fs_mutations.rs index 17b7908a11..8c2fe477f5 100644 --- a/crates/jolt-verifier/tests/support/fs_mutations.rs +++ b/crates/jolt-verifier/tests/support/fs_mutations.rs @@ -27,6 +27,11 @@ use jolt_verifier::{ }; use crate::fs_transcript::{decode_challenge, decode_small_challenge, ChallengeTape}; +#[cfg(not(feature = "akita"))] +use sites::COMMITMENTS; +use sites::STAGE1; +#[cfg(not(feature = "akita"))] +use sites::STAGE8; /// One verifier transcript operation, with the bytes it touched. #[derive(Clone, Debug)] @@ -164,7 +169,7 @@ pub fn cancel_dory_final_opening_commitments( ) where C: CanonicalDecode + HomomorphicCommitment + PartialEq, { - let mut commitments = SiteEvents::new(events, sites::COMMITMENTS, tape); + let mut commitments = SiteEvents::new(events, COMMITMENTS, tape); // `ProofCommitments` send order: RdInc, RamInc, then the instruction RA family. let rd_inc = commitments.message("RdInc commitment"); let ram_inc = commitments.message("RamInc commitment"); @@ -172,8 +177,8 @@ pub fn cancel_dory_final_opening_commitments( for range in [&rd_inc, &ram_inc, &instruction_ra] { assert_eq!(range.len(), C::NUM_BYTES, "commitment message width"); } - let gamma: Fr = SiteEvents::new(events, sites::STAGE8, tape) - .challenge("stage-8 final-opening batching challenge"); + let gamma: Fr = + SiteEvents::new(events, STAGE8, tape).challenge("stage-8 final-opening batching challenge"); let direction: C = read(narg, instruction_ra.start); let original_ram_inc: C = read(narg, ram_inc.start); @@ -203,7 +208,7 @@ pub fn equivocate_stage1_clear( delta: F, ) { let params = UniskipParams::spartan_outer(); - let mut stage1 = SiteEvents::new(events, sites::STAGE1, tape); + let mut stage1 = SiteEvents::new(events, STAGE1, tape); // The uni-skip round is the stage's first message, after the tau draws. let uniskip_round = stage1 .next_skipping( diff --git a/crates/jolt-verifier/tests/support/narg.rs b/crates/jolt-verifier/tests/support/narg.rs index bcbd96617e..75ad70f13e 100644 --- a/crates/jolt-verifier/tests/support/narg.rs +++ b/crates/jolt-verifier/tests/support/narg.rs @@ -327,6 +327,10 @@ mod dory { }; use super::{finish_trace, new_transcript, Trace, TracedCase}; + #[cfg(not(feature = "zk"))] + use crate::support::verifier_fixtures::VerifierFixtureCase; + #[cfg(feature = "zk")] + use crate::support::verifier_fixtures::ZkVerifierFixtureCase; type Preprocessing = JoltVerifierPreprocessing>; @@ -411,9 +415,9 @@ mod dory { } #[cfg(not(feature = "zk"))] - impl_traced_case!(crate::support::verifier_fixtures::VerifierFixtureCase); + impl_traced_case!(VerifierFixtureCase); #[cfg(feature = "zk")] - impl_traced_case!(crate::support::verifier_fixtures::ZkVerifierFixtureCase); + impl_traced_case!(ZkVerifierFixtureCase); } #[cfg(feature = "akita")] @@ -433,6 +437,8 @@ mod akita { use super::{finish_trace, new_transcript, Trace, TracedCase}; use crate::support::akita_fixtures::AkitaFixtureCase; + use jolt_transcript::VerifierTranscript; + use stage8::Stage8Output; type Preprocessing = JoltVerifierPreprocessing; @@ -443,7 +449,7 @@ mod akita { preprocessing: &Preprocessing, public_io: &JoltDevice, trusted_advice_commitment: Option<&AkitaCommitment>, - transcript: &mut jolt_transcript::VerifierTranscript<'_, JoltSponge>, + transcript: &mut VerifierTranscript<'_, JoltSponge>, ) -> Result<(), VerifierError> { let SeededTranscript { checked, @@ -498,10 +504,8 @@ mod akita { &s7, )?; match s8 { - stage8::Stage8Output::Clear => Ok(()), - stage8::Stage8Output::Zk(_) => { - Err(VerifierError::ExpectedClearProof { field: "stage8" }) - } + Stage8Output::Clear => Ok(()), + Stage8Output::Zk(_) => Err(VerifierError::ExpectedClearProof { field: "stage8" }), } } diff --git a/examples/advice-demo/src/main.rs b/examples/advice-demo/src/main.rs index dafcde2b7a..707149f68c 100644 --- a/examples/advice-demo/src/main.rs +++ b/examples/advice-demo/src/main.rs @@ -1,3 +1,4 @@ +use jolt_sdk::ProtocolSponge; use std::time::Instant; use tracing::info; @@ -24,7 +25,7 @@ pub fn main() { info!("Prover runtime: {} s", prove_time.as_secs_f64()); let trace_length_advice = proof - .header::() + .header::() .expect("proof header") .trace_length; diff --git a/examples/modinv/src/main.rs b/examples/modinv/src/main.rs index ce71ed8a1d..3aa846e045 100644 --- a/examples/modinv/src/main.rs +++ b/examples/modinv/src/main.rs @@ -1,3 +1,4 @@ +use jolt_sdk::ProtocolSponge; use std::time::Instant; use tracing::info; @@ -25,7 +26,7 @@ pub fn main() { info!("Prover runtime: {} s", prove_time_advice.as_secs_f64()); let trace_length_advice = proof_advice - .header::() + .header::() .expect("proof header") .trace_length; diff --git a/examples/recursion/src/main.rs b/examples/recursion/src/main.rs index 77e564cbeb..f6a402d7b1 100644 --- a/examples/recursion/src/main.rs +++ b/examples/recursion/src/main.rs @@ -1,5 +1,6 @@ use clap::{Parser, Subcommand}; use jolt_sdk::host::Program; +use jolt_sdk::ProtocolSponge; use jolt_sdk::{ JoltDevice, JoltProverPreprocessing, JoltVerifierPreprocessing, MemoryConfig, RV64IMACProof, }; @@ -395,7 +396,7 @@ fn collect_guest_proofs( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::ProtocolSponge, + ProtocolSponge, >(&guest_verifier_preprocessing, &io_device, &proof, None) .is_ok(); info!(" Verification result: {is_valid}"); @@ -538,7 +539,7 @@ fn run_recursion_proof( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::ProtocolSponge, + ProtocolSponge, >(&recursion_verifier_preprocessing, &io_device, &proof, None) .is_ok(); let rv = postcard::from_bytes::(&io_device.outputs).unwrap(); diff --git a/jolt-eval/fuzz/Cargo.lock b/jolt-eval/fuzz/Cargo.lock index 4d71e897f6..00834f2c52 100644 --- a/jolt-eval/fuzz/Cargo.lock +++ b/jolt-eval/fuzz/Cargo.lock @@ -1135,7 +1135,6 @@ dependencies = [ "jolt-utils", "rand_core 0.6.4", "rayon", - "serde", "thiserror 2.0.18", ] @@ -1499,7 +1498,6 @@ dependencies = [ "jolt-program", "jolt-prover", "jolt-sdk-macros", - "jolt-transcript", "jolt-verifier", "jolt-witness", "postcard", @@ -1573,7 +1571,6 @@ dependencies = [ name = "jolt-verifier" version = "0.1.0" dependencies = [ - "ark-serialize", "bincode 2.0.1", "blake2 0.11.0-rc.6", "common", diff --git a/jolt-eval/src/guests/mod.rs b/jolt-eval/src/guests/mod.rs index 08f33f812e..faf87dd108 100644 --- a/jolt-eval/src/guests/mod.rs +++ b/jolt-eval/src/guests/mod.rs @@ -11,6 +11,8 @@ pub use jolt_verifier::VerifierError; use common::constants::{DEFAULT_MAX_TRUSTED_ADVICE_SIZE, DEFAULT_MAX_UNTRUSTED_ADVICE_SIZE}; use common::jolt_device::MemoryConfig; use jolt_prover::dory::DoryProverPreprocessing; +use jolt_sdk::ProtocolSponge; +use jolt_verifier::JoltProof; pub use btreemap::BTreeMapOps; pub use fibonacci::Fibonacci; @@ -26,7 +28,7 @@ pub type VerifierField = jolt_field::Fr; pub type VerifierPCS = jolt_dory::DoryScheme; pub type VerifierVC = jolt_crypto::Pedersen; -pub type Proof = jolt_verifier::JoltProof; +pub type Proof = JoltProof; pub type ProverPreprocessing = DoryProverPreprocessing; pub type VerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; @@ -57,7 +59,7 @@ pub fn verify( proof: Proof, io_device: &JoltDevice, ) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( verifier_pp, io_device, &proof, @@ -88,7 +90,7 @@ pub fn verify_with_claims( io_device.outputs = claimed_outputs.to_vec(); io_device.panic = claimed_panic; - jolt_verifier::verify::( + jolt_verifier::verify::( verifier_pp, &io_device, &proof, diff --git a/jolt-sdk/src/host_utils.rs b/jolt-sdk/src/host_utils.rs index a94589c9b0..1e1d029a16 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -19,6 +19,8 @@ use jolt_dory::{DoryCommitment, DoryScheme}; use jolt_prover::dory::stages::stage0::TrustedAdviceCommitment; #[cfg(feature = "host")] use jolt_prover::{JoltProverPreprocessing as GenericJoltProverPreprocessing, ProverConfig}; +#[cfg(any(feature = "host", feature = "guest-verifier"))] +use jolt_verifier::JoltProof as VerifierJoltProof; #[cfg(feature = "host")] use jolt_verifier::ProgramPreprocessing as GenericProgramPreprocessing; #[cfg(feature = "host")] @@ -68,7 +70,7 @@ pub type VerifierField = jolt_field::Fr; pub type JoltVerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; #[cfg(feature = "host")] -pub type RV64IMACProof = jolt_verifier::JoltProof; +pub type RV64IMACProof = VerifierJoltProof; #[cfg(feature = "host")] pub type JoltProof = RV64IMACProof; #[cfg(feature = "host")] @@ -142,7 +144,7 @@ pub type VerifierField = jolt_field::Fr; pub type JoltVerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] -pub type RV64IMACProof = jolt_verifier::JoltProof; +pub type RV64IMACProof = VerifierJoltProof; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] pub type JoltProof = RV64IMACProof; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] diff --git a/jolt-sdk/tests/verifier_api.rs b/jolt-sdk/tests/verifier_api.rs index c7a70e28e0..51c0ea69c0 100644 --- a/jolt-sdk/tests/verifier_api.rs +++ b/jolt-sdk/tests/verifier_api.rs @@ -1,6 +1,7 @@ #[cfg(test)] #[cfg(feature = "host")] mod tests { + use jolt_sdk::ProtocolSponge; use jolt_sdk::{ deserialize_verifier_object, JoltDevice, JoltVerifierPreprocessing, RV64IMACProof, }; @@ -40,7 +41,7 @@ mod tests { jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::ProtocolSponge, + ProtocolSponge, >(&preprocessing, &device, &proof, None); let duration = start.elapsed(); println!("Verification took: {} ms", duration.as_millis()); diff --git a/zklean-extractor/src/mle_ast.rs b/zklean-extractor/src/mle_ast.rs index e56cbb3537..1f2840d9c1 100644 --- a/zklean-extractor/src/mle_ast.rs +++ b/zklean-extractor/src/mle_ast.rs @@ -24,6 +24,9 @@ use crate::util::LetBinderIndex; // Import scalar constants for internal use (pub use handles function re-exports) use crate::scalar_ops::{BN254_MODULUS, SCALAR_ONE, SCALAR_ZERO}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; // Re-export scalar_ops for external use pub use crate::scalar_ops::{ @@ -1326,7 +1329,7 @@ impl CanonicalBytes for MleAst { } } -impl spongefish::Encoding<[u8]> for MleAst { +impl Encoding<[u8]> for MleAst { fn encode(&self) -> impl AsRef<[u8]> { jolt_field::narg::encode(self) } @@ -1355,8 +1358,8 @@ impl CanonicalDecode for MleAst { } } -impl spongefish::NargDeserialize for MleAst { - fn deserialize_from_narg(buf: &mut &[u8]) -> spongefish::VerificationResult { +impl NargDeserialize for MleAst { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { jolt_field::narg::deserialize(buf) } } From 31103b01c5a70261dd92b192dbb9d138f9dda083 Mon Sep 17 00:00:00 2001 From: Markos Georghiades Date: Wed, 7 Oct 2026 05:01:11 -0400 Subject: [PATCH 56/62] test(fuzz): regenerate the verifier fuzz fixtures 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. --- .../fuzz/fixtures/advice-consumer-bundle.bin | Bin 207258 -> 206723 bytes .../fuzz/fixtures/committed-muldiv-bundle.bin | Bin 129887 -> 129887 bytes .../fuzz/fixtures/muldiv-bundle.bin | Bin 202284 -> 201723 bytes .../fixtures/zk-advice-consumer-bundle.bin | Bin 176092 -> 175557 bytes .../fixtures/zk-committed-muldiv-bundle.bin | Bin 97985 -> 97985 bytes .../fuzz/fixtures/zk-muldiv-bundle.bin | Bin 171310 -> 170749 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/jolt-verifier/fuzz/fixtures/advice-consumer-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/advice-consumer-bundle.bin index bc12e6902080eed1846d221495d64b5740d5a532..220206afa90b70212b65c25dfa51e63f48ffe660 100644 GIT binary patch delta 62922 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%0DlKf|0`^%srD|#`c57 z5e{}xj9<)zvw`MH#Z+EU%uIYdXmjV}7iygI`f+VASh+!Jvuh zHi|%Mfdtr2#WTum1MhqHEu6_$Jfc8f3!%5=(UG-He0)XD{Nj_p{wMTJXGJKN_e0T5 zXZM4l0Ts}yPu}pEznyx00;TA-9p6?*WH?KK4!z>pb|J$V66(xpPC|N5(&Mo>>@vUj zWd2-I$ojW7#=X-y>N&RSIH8X6wUzY(+4HO}NlRB8=9+n04NWoaA1UEO<(xwH#FD~f ztFpyWJTJaH#1#4Zuc#m{v!}&mZY}~w`;A$K34E?TZXUol^l05o+xE3~VipFc2{D7P9C|hC;LRANgru96$lLv+ zFMg1&u#ZQPhLY+Ggn29UY6R zUdzqy@j>nIcop&u(XUyVA^8Mpkz`Oo2+*O7HT24B$k}pR@XxYe?kZr^06E1nj=8|B zljqAGq!Vx19gi`lYA4|_vcnVkYlQ8Uj%e?BPOtby)M7ORV_Y7nV7q0^g>Iqrz&1ra z`Ex3snkxM4wY|Z{P?KV?0Sp7Th@n#zWmniQO%BVbJahcLyTR^Z9+pcfU(EK3LM}3z z=BSyOthJ&_Bk&W3J}?bRzJsWV1(_Ht8VxVA^%Ymvvk;7P8nI68=#h+sC*BWE&hw_g zR_veZ&JjO{)AZ8{h$m48IOOP{P46}>Aq~y^=e(?8XIaUd$a!Biq*cTo^1yJ!9CjJzLlQ|lW|Op*gp$AvJL9`Fmt5wNoRDnt=S z69{DrvNN%LEo+%VF~AscOKs2(m>I^HFoMgo%4xX=e)%h+hD`$gefkS5qYEOE-`A~o zxQCSb`Le_%rp~!+5#pAShK(FM_un283lMe-TMrbQGZ#?ICT=!_e~kmHEw_Qu*V^&M zy4aic_`A?RxfD(WmoXmjV8xvKNKQs7tp!s}2IP#4s?>81M|4>6)nKaYU^A{=gfp?Wt0<$)d)djy! zjG_OkzqP_+*?fF1v)vU1M%6&}t|Q|Dk;=#3!$DZ(7_Mpp_pt!RLpjhvaS5Ihy{NtSfNie`Y`C8jsqb{Z^HH@yaQID|^+lK=> zStwcvlmlUjVFeR9-z3R@dcosFrP0>|BT`A7wY z_plCb=7WDnDgPbI`|w)&iv_7?>EH^l82!TrQ2>rBG;(nIdc@<8FZw44iHciYzkR{J zdaCa3O{_@seA{4;YS<*rFJm&MXLVotM}tgH81$4skw6}oR3Fb!*zE26PDwJ9GRY-Q zc|Hs@pCH{~ktLC$cV$%N+ZYBxVT%cZyLx!*ALm%Rr~%ei4kRKDkVX7dDp2FYGvhz& zjOnEY57S6Xw3ycj&6cUv#=fG*DEaRz5gMXXR{>?^RgQH90DF>`%8X9a#$OTZ9zWR0 z|6&CKUlM@#I2l$X_a31P>voqCg4z$o=331Ci_p-%lBP=Zq0p-;!Mj*@1h8iQyypzM zyOg!x0#b`6)W)F^KE(DBQUjc1@|vDBp^O>@)l*8YUjA)E3yx0R01~6$PhZSdj`I@j zn6|TeaDIF6W&C(u>=bsW@o`qrs6GhFrTzkWS4a3zLAs-CX!L6-Y1~cU4)qd3GSawosJ(@mw*!&3_=Vz=p{8jou zHi{zf4XB3M`FFPNVYsXDWincBDq|KHkt%H^)7*$4Wt!Fi zr4!tT?S|2QsH6020Y-6M5Z$EOY1toSU_$Re;~i?M%sbJrA_q3Y+xb4Y;B=AHxS$E z5%f$nm^}FCkQme(Q)^;a$tjm$pyj-`ePJR~97SLi_O-U!>c@*LgklRLhggNo6KQ78 zUv88WrhHPeeNQe68nS-$SAr=1-Dap1w=x{xO`^5vnI;e?har$aYSz6k={M3f;)mx8 zwjBxlPNGRj?(jm=HNsDqAs8|G9#oQ^gY#||qlWY_`A)t%(C4<4g3%zPq-#SR2Yk31 zP)7~V+Kuo0wSpe~>$vodvhWOtsTF^ThdLEfurG10mfii59d4j)is8t|HQ13#AYejc5mV5eK1H zio^L=t#bh1P2pFCj1&mA33KC|+A_>wuD*c#hrlj!Am~S#delsBJiTc&ku_?8J#A+n zkW)tlLR<~;xA#SH)B&i#D3`ga%x_s);`uf@4bi$YhQNvmXm?f=+6~vLEuB+@-++*3qD<@+4yWB`7DpO(p1UYC zgMjMn3A!f|UtC`j_)2#|9(Zc92R*JK;rE(TE*Gy)2Iw?Q9BLdpthV%#zrivNTtn55 za6KGQ5ws;6FGgNK&f@iL-ZW7WY6D&68Lxf0o%l@yh@LJNb1_E5`T7MZ{gK0zWZP6$ zVKC;=kg#z*93ZN;ZshsPlj0rcOIRq`Ey_3FViu9iHEbG)OCD?~`cJdRG;8$v5%nBG z4Qe)$Pi5FQ2m9C+xuN12WGnxKPy7IE24~Sp;WyunZ-MhTK?uLq_{iYW?`Jr$oQYR{ zZWILj38GGqPNnGAsP5g*j}cZx#iUI%pgslB)6SibEB&s@kKZDY-(g|%f(ztGFRNn# zNex2n5wTTQi~)S*o?_3r`X$NjdEZ&G0KDy9Zc6l}4b`t$@`(c%TuLK&zDpR@sX62q zC&$@`#eWyHf$2lHRxAFT>c~tHT5f*dQQb~4ZvXSej?>je3KbC9f71zB`(6GParnW! zHuXd^*otkx?ktvG<1Ow3&%b!0>@CUrh#t(>7OyqA<2|KPrS_65AM@I9e?_2-!PYD#!SK3 z3nb_jpJ={vYgq)Wc99K2Q^gKqu$hgVWPZ}=ykB-eel7PqK!`|C2rC|!3D)>irx7)z z@V{L8(@;fO`Nou76SNG0mDwZ5zKRyC@H48CS$4t3kZ~kaJmh*#q-Q5df{3~5?A#P0 zddA?X2ZVpF9>!D{p^^07)c~TkLYQA5DKH=x_^5EcTAg`ibu%v2PCb zbl5X-A8=%{TC#c?dWr^WK>Lllk#42ax?AVRNEw8FNbmY+^aPU{xmFZdUn!ur z4{kUbzp1Ml6;k2}O~CrJkUE2cpI!*ok&nhHJs)f=YPOZKSyr9R-(GjG_x!UB3EUyw znHe~@P4t)=)xfWGC)EGx{!tapaFaAR+H6t-vb~5Z%vIQB`*=ntTk_X;P_nBqqyd;P ziA&t~Hq=SIWHc}32$E8g%vW*bwUjozFFP@+p1_(aVRhGcOI)aIzO~HXd(ajFV?CDW zGwox-jwFRuERA8-qqx?juf8vEY6(KNC^?H8cDFtzp<>6DcoZ7*>z6eQ@`D3>`;O- zb>eET@5Al%I|ce3S$&pch33Up@$@RK{iDdLT7|?o`kn?ILZNW7z^0u+*iJgc7;Sc^ zkn>lxjT#{9uchr2Zs+yH;c=Kxhv~mBNluor}H0?3Kl#EALS0mcwMT)$C z@C6VM-6s{)kr3rgK=HwrmnB`kFQBX5k{2t8F+pwiE4cr1M5fli5DL$W}TbSzIvTXDIE*uTQ5#IAz`?A;*!Kc-m)0ro~)Y{_`tICqB zSQxfs4Gem7m`eLFjZR|D4ConlJVbzP3_k(=4+8ELK?8j^_$hJPb5AOo@AY_}!x`CE zvzM!YW{eHDWu5FUr<-I>Ipeb+;AmjF3b+Oc!1rYAbbL42ebB0sFIH%fdciScV|$qa zv0}($zk@nP{-Q_rtV8yhI!QW@Sy{15pRAk)=-aPcd0!c|99WLg1 zY}AKO??_bZDr8qQ92yJCAw;t(>}rsolFUmE!r3J8$Q+JS7S1prORN(7)xBuUc~_)P zcD(Cj1tzs&&)FT!*}0%QU|5gbe1BePY<>9ScdqHf_5SeM{}tL(+XXVH-U~b@<%d=# zmr5&{BNr8fo1@@a=+L#0eXP**eB{X%JDco3ox(}EZ@;=hwJ47U8dbc;onr`09o~OM zN-F*k?4YLV+>n%I-9?4h@)1Alaz=rqg!Ur`Q)|hOfMwV;M^O{Xp_T?s871w+I=sEz zHHI3)N5{cg2`=UHK0ySF`hQCEH7zK!MB`8zkp}Pefrqh-)>E5R9$~!jqU+$M3OyZF zn*C{oUY(D7?&z99pPb?CO@%ZqF8moWpLO6~e9T|caP_N!lx11lcj-`JPl!alS z_8pH7DiW9BgPlNdbFbJ9CW~GpbHqYev9n|$6C6JM0R18gzFeJN#X-KZR-wcM_r(n6{3_X;qTn9!P zB%16A96l{%P(F3y=f=y9 zXEypHu^U$iVw_VQmATW+gn$5nJ7n)MErK1g;3{X(uZDv}8K<#H+JyBDn{_W&DGDlA zO~%fb6D0JjXaS>?S*nmVm1Ug;jq0gh%#oOtZ{nW@;U)*_lgks15{1o! zJ^$UHcBz7Q0Cuo+o!OEW_r|mr4{03I)op+&_rb=M=-AF?{Nz zIeOBe6e+`CX21!(HVc^-w)!~vayTI*gdz8j-Z~1Eqr{wB8Jm`P7BLlF2uwzxAdT|Be)_7qsH3 z<`X}opE^|m5;!wbnY;g)!XK!0LmhvrkDP%$Lztd^A)k}*b4pgHonATu7dmgN!*4VB z=qo;Xx06ik)j{wlY_t8S$(b9NMs-X>b{p-bWL76@K2<}h724qO_%?*aa+N2EILAg_ z`noiR7YLV^noX^+ay|GJQA2;%FOyRVlk40@m{r^HK>==bVeBRhUzD-|i2neFBG2hT zC^z|+g2BO_l&@{+wTJk~6xKKWlDY-WJS{Nj_zH%JwQ?MI|1_j9q?j_NR&%wK`LFQQ zY}r)9SEKGt24Mix+8@exUMHM?*C(z&F9L8m4EpKKnrU&&d7ehlm)m|>Z1xOSQSd}O zyWG^nbFl;#m>3;71a57&6M2=VMU)bR|D+1PcxYJ&U|7ctW2R^jX9c?jeDP&%zU zgO?YMf#4-{N|#!F9BovhZRy5iE4Mh>8F z^!)kUL5XpWZYmMK28p96dPtVWTM!KhjxR!!qm zK^JM-X$+p?yCyR^${AaLWJGT4^%NoMX`z(hB}-&}1v#Mb4h$@t?j^LX8BSA8GbBG0 z<(Wx0YSZv^PfJcgopHA#7`Oa3i^E0~{$xW9R1EyMhE7>27p+uSYbS`NbpLW0TeUmW zH*OfkQMb?49`706UW;kE>%}Y}Qy&N`S>A|bA2&8eWoLdb8oGM4uJBMVb0-&r368*aLB3CSn1O2ELl=&FLW1rNrjxO`9G zyQimGKji}`0GAd2C|jNputKu&d_48Io4f=u z5!4|k3ubI331pTtSgnqvD|dec2T>TgRwOGN9b8Y`BsQC&eRX+5`1g*_q9~avn*mf3 zIFg5U6EV3NoMyOn)cC%ZRbDs<>?GvnSeaj~ArUCxb&eX5iOSw*q`-1)#>BBYx|_W= zJoY>9VoJMCZLnf&We8f#)3i#2sZgFHskgCIcwA8y^Z|C)gad#w@(MznIM-ouesy;5 zz~`cq^PWL#wkF^tibZo^!^{qzqGQTUH0(W-FaMq+aMGenS-0YsI*W|@Bq}wK?JAGd zW`FgO_Nw1S*7kx~B|_|vmbh(V0s*w!>G~bOhK#*H#y{^^7e2{MU-&T#dh!#svg=Ls z$P8svDkAg+jVTt7wAp;m$7UY;qJowdqk~Fjn^)s2%nI zRbg-?Jm63|9w5F)8J#e?e>;p;7AE1ONUMjtzfpdwyj-0xK0;7f3fQhG_iq)5F?_(P z{}9$?ZYa&VQg7z-ztH7eYXzmjU^(1uU(T%YnK&O^_dki9|mqEwU^GYvB4{yTLAkhz{l*)7Smb~&28ema2%cl41&CeFSt^B&!R zJaQ-OSvXTVcz}eOGKRfpT;8=s&?QLsSfl|4QZLF6I>3&A9P^~K zNcwu^!?JojGIcIA{Sy@gp@dbZLF6w#q~H^MWENu)J15l7fS;%)o*f46==`&+Ocl)q zJCd~B+|_VtJTU`rYPHp@g5v0k04~pVrddM48wlE7Mi~)PGf3))-Whuq_br2i_T*{F znRGBf`=4}+LbCZLJRhQE+coZ5dzm2;lnz2~Ib5GSe7qo{Wy@Ge*v?zwHoy>YO5$T0ioNT=50QG;@ek##) zkxj|nfCj>w`EwX`oQrj$GURWad~V3xntC;-VBgwu+WQ%*sBTcdQF4HDBKuiD=MV2c z1L>6M@Ujvw%8KWJvMhx@r_QtQ8ht9QHOsE{q2(lo?#!7>8Fe>Ar2L{{lV*joE7)aOFDloYF z-s=7AB$s|Vvd)NpFat=6vV`1AyUZXcr?bN8IfX|ZmQH#q_U9g+96uLG=Rw1?P-b&4 zcEyZRF%*DiD3@Iau1$lCt5b!#La^ctJ{1;AUT$6Cfy}g&&)UJ()<<3)!PJZ=cc?UX zuWIlTZBhrFx+%zVoq3dZy~!hFBL!zBRs~;jhnV1tSyI^f;Q)~5_76WT%l|EeK@w~S z?vW4IDbD;8e#K84hdomD@Em&PUFM1q^8~VXxp>zZ)s3k|hMcVncxV>$_XxOur*#cy z0!QSXWe)+4$ZF#%we7e46AbAf9%p-K!{5nRWAX15F>xeF1++I{KJTlD&|zMR&KPkf zh>g{%i%!i7=%@hp#N)#7h!xFN0=z9LduV(tv8=l0DA?_z`QP1}Yvqr{{ZQjsEQr)E z`v%sWm?Y{!LxDGIt%|N!5Z+NRA8xJYW$FRZBIII(4u*6uKD9$4!1_5t-6FN@X5c{i>596BQgKh*83M zOL`%N8%zNw?I0tR4YE%sbPI*;<+mu?JMvz2OhIh`*5Gnopd8xMsA2s!LZBm%I2|Sf z!vURL!I*VTD!SbtJM!*7y8=i(mCZX0hWEAZV;HFndLB{rwJW+72~qxD$+)O&bV9Qn zPKQhW@&}BrZa6X&@uAw|?v-bl2xXu1lsTb*%uZcVV`{L{~wETHI^o?I9@z zxx*A5!R86+2~gcfNzzBeH&+y;SVu7bmPZc{Fb3+W#PcT6G%Lpq!^g~wu2LX{${QFC zTi}P`$yms_LhjUH+Fp$#_lZ1+6%$ zuyR3>=hwIz{q~xRZfs=|`>%}el`_XF!j?{ZcW>0(h|}?FP2!xzgx4QG68szEb-I#-|}6rw-!kYLO_nP(XObXLA8Z-jPB|u9t1eyS{FhdQ$#AI&Xdv+ zV(A(N2Wz$r!Q^XYEWzsoE06~Q$36bwxwqIBvsbc~RuvsmE< zCr@;ee+W=IhV8o$Ly=#LU;wHNd37%d0PFj16Q~YW=+VZqan-LMpvclWp(BAYnA0&B2sEakyUg?kQwzkYovuVVx^KK0!0E{*io zcyk17zg@q$2u%y)ds}IyVq_?F0!IA%2{1hr&=0K8tP_f)R|dnJcmGJSkc{~&X9Tp~ zI&&|20z2S^rpYutAL-Y3GI6p zwYEBh^SAAM~2X2qM7e6qAL5Ern2*39|(Zzvx&A~+?t|!IAgOP81(g2i0w^u0xAKU+U ze;;VYV=JSHY!z8O`|A*1Nkagr|LgnuVDliiqOk#kPf+}k+)~=T#OXG42)>gkib~L-SX0Rzx=3c7n;Rs0L;9@&X6DtE6z$D=-1c6t{N@QWy95Pe z(Ze{nz98db8YZGd4H-}W^dVz_z(S6ZDr`yZvH<)q@Z&(``+@*2nJVPxBN-v)bM<6@ zpmd{5J-bW7%PwYVj8$43xFPG>W>wkzFlmCHCO~dHH*da`#zzz#s%uPwy)QWzh14&w zGO72$Y2mI=tILU0Huf^qV9mV}kv0mdV%Lu?4DJPz;WqiWCS$rz3rJF56uh|Zwanvh zLKDJPq5O&Rm#O{${VwBmstAnxpA<%XLXP`N%f*}J&6@`h5G%^k%_Tm>;xvIw0}tEL zxlowosi?EJAc17NyOAu#7V1Z-`Z0QT*Tnh*qK!4H5ai$z}$9{nS zual9Gl4~A$s$od57}E;u8-YsrH?#Hq^c#Y2ri8x`4GC&d>VAZEns*+r);YeQj>!=c znbQ2Whb4qOwm*$QG7*^k%>M-++EEc;P*L|g>fAkpLSn~Ro6JX$JOq1ZPK4k=8&k-{ z4{1|q`RJdZd7BDg;qx}!|GiclLf4@J|f_EJb4j2T{QEG%3^vKuAot3=a zxZg(|RbF+KK*Z)ZJ0@m+i$otZNC7$2)(|-`_Pi=v-%we3f#JQurowgzrIv)tbCn$K zK5**^cWD`M{FfE@7V=Wdw)C>R0nkJ0Ee`RQ$URT7JGEM~y*gBTzDZ2pam?#_O22B^ zVBmIa(V@5)w$~WgV1$FjsufPDA^0P6CkIoi$Xc9GC*wWfs!Re+gvMz6%A)m{n=QIS z)JeS#;YhJu#+NEwz(;rFW1<(^|1o>D81q*z>R3YpNgdo_KLLE;8cf?s2EarRU?$_R z8ut27`{o@^W67|GuKWHITU|nMgJ|&QR%tvV?UsQIJ0^rvCG3#J4mkW)^8+KP_1gS< zw#V{X%7iQdp=hSZpO^@spfNdUC-z(!Q+b^AY6V=bLxonDu3u#Ew?h?U@vv^#goPf0 z0Xhi>zvK?(aGVFN;vq7+vP7A5DA;zd^t}*e+q)O9)m-S~@L$So9F|{e3?OX+my>hp zu)KEz4#OSK>&in0Bd2-JQBlv|mhQ%amOAaEwG;; zJz{2qLo&n|r21ZMl#uz#zx?TASn|DnuS920jN7!Z1^V)35Vy|~^$8?b^wkw-xou}a z!a|Zs`qV*25gi>+K0!#yx~Jd$@8=3jJX3B=p{up}O~^>nJIEO07l+1as!1F%*jZdk ziyd=@U`=tm1SfQX2@|idG92HAf3~UO>VkcOluvX~5jzn3Me=s&=vT}8GRT;yjyUpZ zc#iRSrWEEfO0C>n9iJVmo}l986xkEFu+-Q}6KQhfG3ocuu6qv=eu8v^Xu>Oicw$_l zG5Wy;fJ-_~fvKtmXc|l7GAE>E`XNkVJBaPy4zh#`hxO}Y7K9$QeiJ(_z35m~*7YbP z1pU&9&``AJv#x%9Nc@8~zmzjYw76>hB64Apk7rr+%NYk2mNF#P%v(-CKWO0)fyC|{ z#2**GS^eHu0kh2rBwrJVPf6r2C4EiL%`RMqb5;mGB$s7U!tqgTu@F3Wju$!WqA(pB zrC_3fyj>>^yHfSCoSyrzid4P%K`VQLCPftZb^fD#AtRp*N334?sMv2gv-4{{zC7>Q z8y3n2R*i(ai5}|N^F&xUVxw6N<@Xc&71j6H?o(9D@5lRWrL7w}ZtnAk>&BDeM7 zj4x_r8+4*wl&#h)6Mok7Z{8{mM@8-3b#VVaLS!@+bLlfjKz3A3O|)1BPV=8#|oAPP_@fy)F^29PbBtHjFW;$4vbB~2)4&0ZZ zYy{UTdF7^J_4sozB^Mnl`Zbab7^yKw0nc&m-+O_N5U~B3E-FEw!*@S>-RC%vbQPgC zL=SNS5(R0#cPF`-#Mwlm>z=XGPv#ql4?XNRXvUEYvgyHg>K;Xtz#Iwq(45p2s#h3ByWjYuAgySplV(eec%$qc(A#th z(eE2K-QY>iTe?drEm4a%BkMSBB3kpw9+cW+`-Ts4boG)yp>k z-9l2HzV0v5;McF}za}D^Aoh;4yCGS`Tp=noZc^5W(Eq8)NQgRm z#TNWK@LdD(6h_J)x(xch6`VHCTAOY-2eB?nUT=ePmaQnb zl7nb-|M8@e(3(sVq6h?*44`9He}-`CxM|W1vOkds&`-Dk;bKUO3%4jLiA{&&fa;wT zM@K1-#^`44eQ$gK%5#Yx+53y+KOb)dUglu_sCUvgYIU}VeDB;Hr4O>(#(~O8Y$aCWWM(T<$z07s(1MdsY^;V)2Ko}}Jx9jdu#3_8b%&#n^ZKv#gd zKjt8XI_bldT!=s|`=MdA?@c}&m2oGO6C6ZtLa?94sTDqhoT{#+_TwFG zslZw0&=PUMXIdI0Cl|KVOfk0rf5RyO4uWGboF1RN zCol`e1RRoUsgVT`$@Ft<0wl0sp&kl#_s@RdX}MYJtoa6*fYMlFC7GXD;l~d8{O*kP zBF_(=$~xYp9a%2Fl8;|a5Lr#GT=T#r!vS(xdyzTVKq#>Q1xGg(EgPJ!@;G59U%B1@ zT4bAR%<4L$=(S4o$L-fn(a=fKim1q23GsLytx#P&f*IxzRlbW50EFa#vs1P&h&B0d zgI6WRE2Y2Xa1IX1WxzQf6-W|FrQc0QelVS{G4^86o&ArzVa5+i$a4dYRw~ z9Dh6`&_&;mE6=%cD{(0Fkq*wmO{Tisq7n!b5jUB%WuR?j@E`%oqZ0s5dQGP(#UdgZpoh1gcF7yOj=SGS7(L=w5r}rm}h9H%Dm`F z>fpvdnrdDM`8^u!+p~xtL7VyCKFJ^Ed-M;r0aVWSU^XI|fq<8&Tl&gr_`AxsaN>VI z%Su2~Ku++o*l$%+7MXtumKU2s^MXcXU>bHE<)cNAX4o6T8OU=rLL^%jg+7C$DO7`c zuWo#u$Jb+^rG6mV*lIbT2}-c?dz?!12u*AV(di-lZh>Z0gs3{vPC4O00R?Dc9kX*g zu+kWg9yey=(Ez+Sn-j0>Z9qG!`fEM5MFN+gD>0bW2F#4QE64nJJDhs@-LsbYuuJxo zzC%Aaoovj~x0ml8VBrvKrEHwrA+D6QC=xj1qDd{BwPEFHXFm17JhsZkBLU0GeA>K4 zcBD3t#Sa4)^= zPC`$VlwHOO?lu>Oqg>#R&1;9Y*V7xc@&v9Zz(Y#`xI5OKfGue^VcV#+@-0I-oWEsK zB-ZL~q=ab_By?-X{yDFWv6FxvTaTAFH9bzpZiq+c;!M{uDIe2pLnY}+8 zyRQNJCtP@RydC>S_rdFPV?sv{vOMwO`$UAfTZ1_1G?~IX>fX?q}8z#RjA= zN`)oG@tYvUwt_FaR6({kI#HVp>2_eZ?{O@)OcH4Q|Dhf5KXv$j+z$93CH#LY0N{UC z(f=xdF7w$55pn6?xZi|&LY)Qxj#3eLSsmZL;lusC+!x15 zs5A^DGTgil-l`LLzc1&RzY z4I?r`*g=KV=Eftl7uygn{{Z|w%lMq7aLYg)|D=F}4v`|F!Nr%|wzD~T?7L8FAK4lk zpdCbd{ONmTF`2=mjv^c{>AGKN!=BNRtqZ^>UFXke;H8(FO@HW~3v^mZ_Q z!A-NgSEO8`*I9L0d*6y48{?(3=3PG!n#SXs6WrdLB$;tyUm^H+DnF_8C?m~Otr&`= zSnL=8G0&nwxrgs@`>p4>uaWBiqwO7o18<^t!PvHKPV8i2+r~_6JHI#+O>En?ZQHgp zv2)+8+TCxv|EgPcKR>6sPjy#!^*N`{^Xw~N5FD5hfB~?v%3!*~v|%AXyT$3|czD%Y z&YSPIGH_Gx#eO|xYAkhVagAX}QdXH0)`IRfvzhAayyCN&*}+)rlhxgL3`4@+JA(hA z$P)d*$m#qFv;`3O!m+5H2z5jH=I5YFTpK0 zO)6U3CIjJEA2Gu)#&vq+L-$dFHJN<`=TiUl2fCnX+E|#%bnsb0vhs>z?CvMG$5cu6 zSN8UPDI!$F!cbQ{e#-Y)=IinBC__sj1 z)t0asg@f+vhx%*+9YQgvm`1iOXTO+<1;y z-2&g>YH&os;*sY4XVn?@i?FFsSPS~{?$Joa;h8z+oN?Q|4Nlk?ef z*OBwCMqqU}Bo3lb4$om9yZqB037hsu+Ab)6y-4AEfReT|^JUoO&n|MJl?rNtnYJB$ z(RT0s`7Hg~C>G1MqCZAgE``ZC3}onO@|gW{wj4|}S~ImWQA@ImY&7#ADjb{RsF$PI zA}9$Dazcg~Ed%vtJJNgW0l{$foe?kV zV-iwx62CXVj_BUjlaY{zQ&F30w$1`TU1E<7yAqmKiBLQDqFo)}l{tF`l@~?lo3nbHUx% z)Q?#xLc%r;M@?68dXhhJ0d&qGk72yuc7$`JCa{!}?-s&cB~$`^v^{m^&Rn&Pv?t{) ztJ&SzNCe<~< zjCI6QV0X)_6r{|?@TvHNqg08M%8DinMhC1`mpBa*4NQ_{?W zW2=5>z*}O@Lh{fA@Ssy|R;NxQK*M zsNN`%+sfT~Bn_(2XBmEQqz{yv{eSTp1SmG!X9!k6)Uz#w+~1~>zPNk8jWJ&~Io6g; zrP3_KF<1<(juf>Ym7=xM{BTznixqUk_>4|UPFB}_AKrx106Gt<93%cEN!=PJqsr&z zzBvru4AIyOPRI*~4`t=0O1nNm-mhi)!qcAG5`089i#+CITAcvHqT|~zr+NdFnNUg? zvHpb?2%)HJ)rKI1N?o4Aty!d;>(63|qYY07P)JUq|34h~pA7yFc<*q~iZ{xuKJwO#&-xESEhWiv6 z{T!KkWhb^DuaxXGg12XAmXSZ4GAYe3G1@PB8EPZ0L@XJx%m^YH( zd=`ZU$4Q5b1YUbBR@&u5xox*BO3J$Y3{FiwxF`Dz4 zWC|Q5{@i;^>tSfFo$$WnbBa-&0md<}xf0M%QTZaC`r}=>yT|ojXRE-L+7O{QHYU3Zxs0(({ z#6dLJ@LZwz2i!rPMw7A{b&t7$#B7QGWe!nsl|-_(ztlXVLZH4M{lFTMLX5uaos*rQ z8TyhdQCE6sgb+`7S-AfKFqh0eN@APce??|=(f7BqCZg|wq@Ew62`_6N7Z%i_IXBV`uwqX!B z)0Tjwr{1cLlS7{ewOk8x*cDQ6Zya)s4jJBx2rZIdE!zn5@K(o~bQ~ z_dN0%L7vAnasRKz0qP*LXcquP?;#Riob zGtNDR?w-l0S1@cGbK3szCXJ7tX>|sRv8iFo(wL=)Duj7F2654Fnfg8KK-``3!WNfh zgW?tT#|)g} zQNf~H6KeKDj$IvvoHx%4wH0BNZ}9&XMaAy)##X zh)K}`aJno5ieju+or_vgdOi_1E$)<%FR#|+3cqZ!8G2q%>7s-KO`gh0Jt?r`#N~t| z{cDjtL-9L)_|}fgihyHU#XBjjiSN%F*-HH!;KzFb3&yi2ge{kE$q0({o8ZSIh~5T< zdqK%=4RuiY@9_hZvbAlv`x30C)pJt*xJafu3cDkroz2ngE_ zuFpU&aaK}q@~CgaWtuL17Tmp8)~!r0y|W98H`wP1nQASt3O{NSMt+O@ly^8Z4?MpS z04<-#NZr(qrI_{swG|YSnbHp86EwH;(JTtq=`nV_@|1Di7Er5L1xM~Wh zq!Pl$6=aqle|9Y_wL;(!moIqG?pT}AnBH@y4Fwlw9GW(az)k_XUYUj0iN9*nYo%3? zSD}ZPwURNfS<17N$g1>=X8S!X_xNo`q56*tv=F_BwR(|2YMpD6iCmX{bTxVb-+avm z+1)P-R&ieNJv1Xwsnnv9aB(JEtO)$EUff8cVk?B!`QHPnpe2$tHP^8+y_>PSlv3}b zE7FWftl7D~>jUiciBOt0q@A{?T~=>$Mt-F1BwRjTJzyVk9I~$?iMGcM4%u*H&r9o+ zO5l5+`yHv($b1L%(fPqcN_5tM^co|E)3%t)!uYw1Yud}8wWO+9UlGlXKl zKTJ$_Q+l`XS^Ka?Y(6yvgX{uB+T`yU>Ln(A_KQ64>)#1tX_A(CDB*!9pX6|L zrsl5>+&hWn&i{0!Qw+vr%VpZe!*Qc`gpI2Q_&Br*; z7FoloPle=#$@GJM(BhFHt}CHCM-IH%aU$2InWEL*{Dlq&ARxq9sS zfchn6=_K%9Y zlUV+XbbAxN*hA&GKOG(p(iJHKCPDPSijVmPaCm$Wpo$Q(#g6)N)QM3wG!>DDFDDJUncq?t*ymYLED&(c4KEXJzMtom&({@M)=3?d?(6q-H^f@@C3<+ftQQ=9 zeHBm6Ur{Q%F0e4HPy)r-mE*BChRw`Rab+>Zl2?F(QtM4+6lACwi2=IKx4%;Hm; zhIo#-qv|Qspg`hkUfBx>3)1t4ucScWyH95E*)TFY2MV88Y4ZD;s-8pC~gU zCL(lHWsAR#_Tm2|1Q@E#MPb)Fv-5!9RQGVhuR(B}qrKG@LE|Hds48ABQ`2li^Y`{p zEssX57|-5>n{9yuVqEvInRbefrqTODl}#;IRH8f!m!lDUEaEvN{i2j1$L#Yu7mfZ< zed&iA!!zR0&;_N`J-uMM?xoCEk9gN9-*4xOy9%f_jYJ(MmNJk(%ydfD#0+!bLs|@78B-Pqj`6;x|3E7$$mpCT z@0xxaxBl!BH+EMxZSpH?AscY?B3oVUO_r4FJ&Wv5{P0viDJPju^*Tw;IKtjZr_3Yr zg-M&aL7o@WvWT=(v9EvS4H5<|V7Cj8`qjjA!0e=tLD0Q$%<(b{W}A>VGjK2uUQVdq zJ@F)uOp)zUBG`1h1y3j&Cra4FGxTeJoy-$n2z}+O7hQZFF@4tO+Z|{N*h0*_8WLJ5 zTZL>BYFm*SIaU;s3ZiJ`o5w<9&J7Q9)|Fzc zwfr-pHbh+c@Ss976kpL0sK^Ry!0e!@LfNCu&tSw2=EW8^CxTT3!dJQ65_SzaJUVG-@!{KYYr zXe8PE6{tlxF&<`1t^1X}( z=zux>Sg`r4;Y9ri*5;)&2vklK9*#zQ*T;L{Ft9+fWG4eU8>gjxU~g48-R6=XyuZ8X z7V&ZT6bLx)(p=-l6;UvMMBYR`qiljKOnm*oQJXF0lUvbW4M&|4DRF4U0-U_Sd^+*Q z5q+X?&QsnY9daczd(LWvZpS?$!1Wjjz+Em%0oRfa`C4=Y{7OQF%w<0F38Ei9lAe|+ z>5-E>HlzT!?Y_X^Gw2b)lnWVURyR{@z_M=9SX(T92ERjaLCt?$eq~Nqty-Iir5=>) zj$20J|7OCBIYILpN?G&`60#%M zHnn@N7w-xqH0(1OW{5q~KH+Qd*>oBF5eN9gOCQAzDZt$cH7IoI!$6Pxep6bi! z%AX+e`74whSa0k5_*I|G^*b$Em|+b6h|+_@<^wDt{A`%$dGj%|1Gt%01g|=!KqTYZ{0UOo|nXg zk75b_R{gT)7Pb^IjIBlCQ$^VcxibGg^8C?RN5X$^P`av^xV2&&GSd#CtTfj`>P>n$ zu5drqiRzjOH_K7rr2N&CkD+u8L$&dr5dDd%xi1B*pr5)CZ*<#LT4?MM#uc^M<1kQa z%__0sLXkj4-GhX^2XD541W1Z{rQNWk1iz_VSEaechBjFHsLRgk1AGuk`49HX3{?#L zGO`k7euQ3FaoYjBj~uC;c0jGhbthqgIrDG$KT-Zkcm)cx70@Q+M8%#jT7PI0TMisc zVslYBoXdpW0_R9 zB-ZrM9dMGyAGlobQR+8n&VCFK4&eW#U+pka=lf_lgZwpW{;RcUXk4A1-}tscch2Cc zn-qYE!8E1Qd;I0fK7!t&Ot`QkOFT?;_giBCIp9WIAn3jpV@9j64TM_~S^=1j3ib|v z5a;PaPzuP6Fd_bJ8-#z@pc5W4UNik6X~qwl#0(-GBM5Yt2mC7MMn4Wa{Pa`p}Pz z@CU#Uo(t#4(xULxNe6ysSZ|1Q6%(SJrX<>F`O`o$VHVh2QE50u!dyyIr!xFwe@wdR z*+SY!1W)7$uQuMHn$<0IE}AnzC`I13$ML^lYRaY(`iD}_R`mOn-zQT8e%|yt%9kLC zWn4IX>A4;bugH;pWSUaj>TmC1O*=O+VUU#AELZ#)b~YJ&1p$E6rn9KQ+REQGfgD98 zuoGDAMWIt$EUSyFj(f`Hb%UfQym~4*YNv$jjb~NGFMu78yC0y5;xU-AE1)i)pKe^4e-H^-f zW@|hQ{+o)znWnlMUmC}-9oQ(%x7xOdP+Jb19@I`7B)DL9u5~|0%|kST*?wgH7MDH! zNBP&l2TJE(BKPBX(~zBcJZlC*PYW!W*kqcm*oEpESv)Wbg_Sb$+lRKu4-9rP8!l@0 zvbha1x!ngVR~g(N*bUkoXRq1yAeD3@a+JO*79Di50+bk6IzH{vYg&!)c{X| zdgrYC9h7YB<690S-jv~5=WY_>Y8*l&0^E5d@u*(8tj2l7eHapo4H?X}_Nvh#Q+HC$ zaoyT^JeHidKh*wYBw%URY9Lmq{U()8SHeF8oB*W4O{?7NPfKzvw-KFw{bEd;-K$ku zCOUe7!rH#f0Vrd4Ccn+?UD5>84I22QpOVjSi0yAVPCi21-9MC#S3G4X5bR~+vQ~yT9u@s~u+FZ}^I(TuG5Vw5 z9pM3`1{7yQH-$EtM&st#95L#}`fDb)34F?&?e~5%cc{47;!GzP8VX;I9WX`kC%Xlj z)%lZgB+AY3&EqQ0yGftj9$;0hS>}YptAHynDuwvg;!LBaqM-0a1r!t!Ep;auaX!(v z%Nk-w)8dg>-k*s z5Aa7FLfjo2@OuP_$S`fFJR+Xi13nBMsO;R0db2p1%E@Gq3gI1;Z8@5^k6`%2A)tJf z=|gV+=e>x~oBRMLndopxix3|}(6ps(-z)_NLh}j!GEogx$qar3$aq=DGHU?# z_ZBYvnFKULi_UiB+GA@^rDJg1vw#};1;Z93JvF|T+kgMsF+^jf7rbPdIrqrVGF?58 z6fPTZP#jkig*s{R6B!AJVjD-TRRE(B3UYK}#Ven>tT9Rd56?Bh!@zy~_udhxqADw%Qq$s5_1-sU*BVOrw?tt-WTjFhu zH(!aK6dpP(#ou#>_ogZpvJ7oTGw^_oe2yFvEpZBUgI~*o4i%;aLpXTZ0&Wt#UR_;# z1Ri#A{T)V2;0!a|5`V*c$(v0b`>&#XZAV$w&}?B!TN;p`KX%JQ1b?6k6Uv%yqhF12 z6kR;8&<*UVL+my%{|mZq76k+y85z@~`MNfxQE#627=PHs3njIF&fbchOQ~YEausZW z(#vhxw6)6!laV;tQ-*CM*hQ{JNlcgc#fs37)JDzG+Y?AghQRU^#yTVBhD77hhKQ-v1S>{ zOBlh681#7Hy(y7m9av5Cmma!`A2@S@bmcbo6_yBemDj~fC6dk%bJ|u;`uE1Ao&wVC_2NdEAw;`cKJ*1P;&*+F9 z$G};PwT11%wd3IB{6ZV7=5%DM;--fz43&>nlxETE#lz z!GnbJv#jeE0D#%)^vQ-g_FSJ_{`qpXNb35lK*xVBW#u5}pk*q&=MJhB;V2ca(A;890<{@4%=HK6h9;Nk>_={0p-OE!7l;g)q>9DyeUyi?~2j@>M@6Qiq{VW*> zFnV;3-?yWP&y5!ZA%?Ddb35-|#bVwPAld2RwT*rBC%`e5W$rMyX^9qWlJ!vjzIg2d zl)EElwn)CT85!1F`gZ~4-2}GV>-5xUZcuOEJ??;N(kvW1d@)0_7m-sSyR8UDNX(&p zP~eZvHCxBrpM1ssPUL?C0Tuf-5ZNS^P8(Z-5z=!L<>aT!D=x{vOp%Ny{Gv&$nMDS1 zf&kdfYTyJNH9KOaoduhd6>({iU+6b}AbxAT>UIfFwLuk4R5Vxv^SHT=<-zyn@P(Z} z26@&$g7N!QFC5%^4|s2qg|zJs;se6($G$iCeHRIs0kQkBeQe}GWKW&EeMLcnJIO46 z+N|}mE*YA%KOXDQjox_Gio`Tvt{m>@i$E;`8K_D&{`E43d9v}@d+6TVD^`bj zWcF!+eB7Ms0a8cSGf6wzT`z+evpHG_=1G#tdz^S6%MM%pf5qh#*&>veYGT@whMU&# zC-vs?!;kw~&CCWQ3RnMVF%ONBPfgR$Kp0pS7xp^6Zc`~JlV`^XKpPhA4mY8(!k5K~ z5U{H7EdamHy#S3CjpM#MRmkJ*8lsw5I?STr>Ol2TMMk2^aw1o9YBE;#@;b)SyC>zc zf6(514U(TirB z$}(vWMO#+tpLl1_rOB(9OQp(psy{~JP>c=pPiEat8AO4wxBKCVxqDnUfMy6F0G zr2J>urUkvOp)qN}E`L8$#SyDuzrZ`=);=L>GFW|rsRNiyG!6kF+@1a#L3gf#^% zXMZ3(zR|Ue;`1N$uI%d0YydXZ0K_|{JOOwEeT)eO*!3KQbin(5ei3}Ip*_UYM*}2^ z+S0Y`k>p7-&W&DB{G&Vn=(a}Oj+V~gpRsq|6arLmpdyZ z3@~3RxpV~sw$IT~exZh5Y=laWC_9zAf9wD5N=`T+O=lNgp>?1RlCgHK0YeIPjS|XG z_*ep5>)osu-Q>DBZ%WBg>&v5*g=FS-^X)GSJ~4vm=8>xIqQnN z+=dH0sD|*zLx)@^XHW^4s!>N&sk1+tXa7kY?FVyHCpqJx{lK?e1CtIdM1q1=Bs%bi zs_U{sL#!TaP_Yy^)S!N40Q5<)_U}ztw59QQBj;Cnd|WmO#K`vP;M)IkVGMD!+Yc~X zUPfiubMis))nqi{zq2#%%r$%PF0F*>c@p!8;) zIsee9>_;y;b|wlCyw|GE2{q?T>2l5#H+P1l4L?QE*sUr`_C+{u=?zy>tBrxofJ0oF z)!=GpLHlBqo`)(S0sMC5fEb)v8c4t5 zh|krn=a`&!0~{_py_8&PA-pf%1celbYm-tN+7$yCv!L%Y;d>dOl3n699%f1(W7>wh zL6`X0wG*l-;{UO!#c*}JxnSE>#n<kH01|_sPlM!w-Ry!rlAn0)CvZZL(=2+$T%PgT>>+Lje}nQV>6o!!L%aIO zG>kc^S`Hd8UYp~`p9nDM*sdQqF6bdnAo!G+Eqe{hd_zh3hu}2io7wl()!i+k{7xa@ za9(GxKo|k>R%3KnEn<=dc;bK-5^o)?%b@Ma%+p@_ReOi$1rc1y1!TDvIexMeLqgqi zvIyRdOnP!(uF_#lIo=U>jcgOvjWsz3h$$KJA9qaKksZ`s)i-Ux%TG=kJ^ z>(%SsNP*%gsvwytIFpI0<-=Y_Dy!A*Ppmmrp#FOzw0O)IA<9p`>fwtsm15zErYCklO9;mcwr(rpVmG%8f`+uvW zz-$*sTY3Rr<=9}SA%aVnia$w?^n)8l*n#DUP}SN-RhtX!dgO1BlT=bDkZGRwoC|m| zJ`dELcy2;OB;!5_WG8U~@FAGVS}F+7nd;p;GZ_l#H;yLh=frob_=kLpkTiTYrRxB> z0$q_k=CN3h8EJTna(^f?2tmX}SQwHxz>ZByg=d1Qu||ETqyI4HWEBBFjF!(ihU{6{ z^u?0mXLZAt@JR^2I9q{K0hB1MmuDA~*_U-ijcs6~`fGDrA^Po^G;d63R*GdCR?1sq zJNFCTVXg_(Z95{|?=Y3V6XX~$Y8;TyW@}c4TG@|Y^b}Yo^kh^umyW-D3YPHSKu%uE zrWQWV(lipRCA$ByPsJ%N&IAzzqsg-{dxIPcgo{hnxphc)l#d$DpK}X>r5iG4MzxFf z?7V+A3Q@GWMA>X}8w{lAk{P=K+Ld{bXMBWt&{t|9R1JK=2RFh@{EvIN+{X-@taspX zWGQ>#<8kr@)NB`K5K&tjjvfsbKp(`a<#^WVN+YD)njUgZk1{X$3oMQ+mb?8!wYM`f z;|Vck4rS1^Es_`xdpq<*gbAB8G?_(Pm4-+Mx8CTw^QYWw^vsK`4w)T+(-T$Ht#JnH zICh4pCK+|ztYlG++Xcg>he9mDZ}>%ul3c}*F-RURdtC822`>X^4iaV(AdJcWF@dT2 zJ2hWZ-cPSrIRiVlSry?%P;bQD&!6%LklU0e+ND8v=wlF4 z^n zFqwWM<&pMB~;j3AgBpP zufNPV%*wy$YkF?s3lwa}p=n!?&npq#{KeM*J1o;$Uu{cQ^^tibemQiovQ# zkF;TsE~a%|#tp_8H*m@S-0I$&IgQp~H;xOZm3`KG;K+$(v4-<3v)4PNGM)HL{e~-y zw3I?FotSy9<>rn6Iuy;E4~h7s)1O;eq}zM7hms~yYuv2Ur%`5mODtW_j^8WjBB~wl>{&TXM(P zeUcO@S5;4gEEtC$TIhM-DPA|5+RETvn@bU{K4NYX3H@8C6R0V=OF#+%hW_} zn)Wa337bRAJaAz+lL1a4=lJR>62F-OQ*2BxmImnjMN=|@?sEFR1(8-+aF^Z+W5lT zY*HLW|JQ$`Ef%eaJY=6)?l;C`DeZj?>Z@RYw$u)a&dF=``8otdQcrM;ftY9oNtG6+757r91eHD? zBOjU9$NCPJk2!?z$2pSRY%t8h5+{JDh^4}J`2ga6a^(+@;{?Zt#EG_tA-<2?`)Dwj z6Z=?MzGVXl6-k7*Xp1ehS!1n#Q5=~TNg!R*@4G4g6hPG4U)d00R~TK|(aRw%mpn%a zg?*v3fh=?TAt_&4lB_l{yP5M6_vI-0iyC^|rKO~u*!!5Ryy&-`+qdzVfD=DZbuFJ` z7XtN^0=4sJ=LsibV^BJQO8yIdDTXa-7@oxBzXU@}iK7oryN@8O$$wBvQwWfp6j^+I zwN6A=H2d;1g&O>Rfc3M`pmYD|qW=2t$LD$Ws<%vGTK0{Q?GnuLQwQfZ1(=nmiQ{SE zh|JQIt&^iCTaF}8SEbNSCR&mBGNmpx8sr> z*M`JPgAT_NXsNTLLBqumI_STfL-6#X0!@NE0XWPh*c-)p?Cq`n;l+ZlYf|8=L`x>BMsP7`ECPdnr zjRNyblJWa~#_cxG2%JsZJ44v}L&-r>M%wt-(V84aZN#GtMpL5J>1rmOuVyBEgaPpF zt6%&8oRZ~GguQd1;8+AUWpi3t&wu&jX~EjA$mF&qFOSfQeOW~$?l7EKbs*&$^U;eD(Cf~k9R>R^d$-UF?Ji19L+jW5*~(<>;)+l%3`Bdk^IAK^c{G`4}riasoE zzf&Fqw!aurl$IPdQc%&cML{u~G?ZT=vjGpU;fVX5ts5ZBEC0Gtu@;GL+1on=NHeZT zBiNg)%veWK@cb_!`_m zZ2ZRz)6hIp`9^wV zZMV*GW{G}u*f6t;(a__TMlQQHt$s9OEL)hO$S|FU2J4GHO$9=wWf{5cqs(F^GDl#W^-Tfa~pJ-_WPdn7#SQEvi@kpyawVEi&31D|^xAn8= zyDlc`9i9N|ND4g6QINps7Ut!?UjzJn#kNL^k=o%oB{7!V(Y`RFg4fBJX>z0X5JGTs zAeOIEDGm>+z7+2j59oC|_7H`#GL0e-;{#QZQRb|>7B>807qi-{oYH8?P4b?8n5qw- zz%4pq;mWVfz7Wqsy=ka!eKTHI?1#GKZ!h8{?(jjzU}TX`#(-%H&Zcjt<8PIEFuZ?J zPGDZ3U3A6)%1lZS6+OM5dXnC`1Ocfg8};<-1g+~8|NO6S1@OI}jqW%Hz6(Vx5u1*_ z_J@f1&pWT5V4=jYwbQQv!nRM_uWfsH1{s`k;rc3!ODAwex^l=cP7W(+6k@}v?~4dk zDG+NAhTR;mi-O<6I;lmCS4&<6@34hlb73lo$XH8p#=7>J%!XJG6`-+-CEhN-Hz-gB z1^qkltN_bc1K_$qD8q=b9<Rq^6~yEjvg{F0F&$|K5C zO#irDEsRm=Lt!1#`5oUyCX5M)pnCl_&ZxE*(st)O1h^67BA^bc_9%NJM;V9XeT-d1 zW2{i|v~YhIiNH5|O~G@{&Vugfh2t~Y246Zt^zg};tG5<@Dwf3 z=7yiaXEThk$WFjrQ}(UAYqF=6vti$Uvg6Xm@a)ELh{21AuT9N8ESIkQO>;f=P-HKm7Cb3RT!pH%@&=n z$A&oyWqKHmE@9WH+4jKv?0Mt-y;BAr{W-Jp6G;71R_3$08+7c&YSsLx9Wx1yGN86UsmzSD-Hy84_EHVyAntlzHz#C7L6q#{vX1l2?!>xoU zGXQIf9L)8)+=Y$W5iLd-fV5+5g}C_|R}{Fud+tQ1{RyA4@)Di7E=RUvL7p4%W_O`@ zP_w0~5Rg(;?8(yh9T`J@@y*8DkAahp$s`rrRTEktcj%_z&dZXb>dTVn2D>Y>mN^r` zO{DxG!m#;6X|mQlZ%S3IZDfAzu2pFW1}JUlyj)qgi}X&M{Rhf9R`j9eMo&(YUl9u? zoIg7ux2S*E9BF5Xp@)|~s|eDg>p!3JZ0$QrrHRLrc0GqdPn;W2Dy|4BC4GLwTS)gF zk&iU|53awKf1`jqP@77~|Gxip(;N3TgqH|BEDelhg@N*Tt4$5kDX;KzyTOj87zjin z2j4n)|9wv7D^i4^r}s->-A{%x)LZ|GG`HyiD9da)Bx|ZVTz9ROgbssDFNlT)t@WAl zfEMMyzgsQzgD!A(OluS~D@Yz)z$pf$+LG*~Htxc)uM<~&>TcCh6`HU*A!3E$c~(i8 z>z&21@$fpq{1QL@h&jOEj)Oo{2gYDa`y9{Y98a)6-V!>h$Yl5afaFthrb(#yOo&~Wb$0nDy?XI9b#Kg9Z{>w1pizv0pOUZ2Y(Gj74-G`bB4fEF(&s;ZkK6V$q2@hsUBzJ3_p{#-k8y6RyBIo>U}f&ifa{P zc?-E?NlBuk9_2P^I)GSX)@~yAW5irpo+a?6B^p7jp}-;d0_J<1+MHri<=JOncPUVW zHu0c%bKx<~5MMh~fVjP^{tx+wddxhZse(Fwyh92#>85p`V8{pg*PTUAtN*GmFFhQ&HKKzGJ!& z;3#QSK(t|+B{YP?RRkRiV?tj`%}VsT$oSWwR)4jNtRq?E(|G?JW^S92Zq7QsLG}AWhgXV|UKU-0 z+H6vg^V~;3!kP%?v^rPUTL|;lC4+y6M2X?&$(S{1hbOnsFGfT#f#q}cF~W~cZF!$q z@0&nz8rU#KR&>)@ipo}kDtlgo1kL`hO{K-4vvK$e@H@4rN+|t^8Rb+LI}ti08={3zfURM21 zByv_cppw`a#dJ>VPyRFay5~q~!p+lIkUSRl+br-gp2?2$@_35xZJjLgV1wYt3pCf$ z;|;&esx3YgzIScO-XtkJQUoQ|9ttA{I58|RP<7Kd5%RaCEwy6D~Hfc%Fdginax!WhuGhr_|k}W z?K&u&4SIwGi?w2pJjlg{eg4G*heB~o7-}{*ZAnF&qj>r4leKR^&c~^Qh8kCKB;raK zP^}Px{>P_-c(I&WVa?p9Tq6)dIVlIm9?U0?CY`Ubu~y>#^Ika4{^Gdjhbf~o@ahC( zW}jSj47LDv)rrE#x9*a7Z)HEtMo7nEK1oqA=s=6~WI|h2j~0uN8plALGR!NRREbED zgE&POQFXR2iLIJX;qFf9hjbuEPNeu7NWej?4}_fU7}yZ5sQ4=VaK-j=l*5IRP7a6# z(41;YZ_3b_H^b3Sq@?m#Ld+K~ZA=71*2N3*m+;?1oTQfH%*>+AX`G?;97gcWJwBTi z)@geL@3z}A{jl7lU~Eu%YWhP-+yA^f@%+hQ*H{b_He=hpxJY74zlI{}=m3WUoL~4D zOx`oHXL9Gm2~24;&;MK$3`f_#dU&@Xf4wvbIUwMUqsnx zcZnb;Db;Pze^?m_8)HB*ThV9Ycz?DipvCt2;8#U>4PL@6hM%M9Kp^-B&CKZSmziWn zh;?n?RHVFb#Qv2m73HFffsJSca*-dr%fBbs+(Jsw=KrfWO+CR16JX)LND`()B`eJ` z@?@!K`Ud{509-Po&8a5BCM9kmXU9?vnseyDBoNfI{Q3u!0{S`ffdlCWIyn$d9T96$ zI+k1v(Rwm=3W^)gW~2fkGoMO$;L4&+4-0rYwzN#z@SVDJ{qlju)QSlV7C0#lCI^4f zkPc-&ht3e}YO*T!ZRMJviY345CCxk_sAZv-|`yTo+_*fc^=)4J>%} z;)9DcGjB{NBRI=+;nt8WScLwzgZi(4PaZP7$I(@YC6pc+r(+c)In3ar-k@mwAGl7% z9{7#!r(o=SC`6+$Hp8_~pJ4$VESZ1um3E#k`T68X`h<8J2eh{Z3Cf^%vY4#GEM~pa z2W^MhaA6FSq|3RyJipuKKX3V37p7%YIQG{LCQa`H7>k~U3b(@^xlWx(J6C9mJf!{| z7NN4x?Okk4FdNX73|#^JQ&r6%q#u~WAR};~UbGn}`S@#x^!!KO(_<>J2hzUhAC(NlPr3n0fkre#;X&A<)@S0uB5YYwx zZ0C`OqakCI)C10AIY4R$(xtUSlKE-VXBj$@Va@zp=U9p>g_9ZfhYG z_1fkf0}eI!Gu|b4u4_vV^xer3gN({z|2EF9R|yKPgTtQPM)Bn8m8~1g;g9&pL*3he zG{dL%hpIsrQJEou#Q$OYJoQA?m}x3?X#Kwa-58=C8OQBA>Jk1Y{=R=JdC&nb_4ztU zg(Xn_5**yw5}_Bp!3K7*D8-`(AoJ`Gslc-V>sA~_u+7^S4@zEEAqKsdn*DP|B?l>@ zp~?^kpBBwu)kc|19%}0Uc;a1`ZgG+vg1g08WxH? zHJ_?gcl==B3BVx+$IPz2zxEPu0TE7i{qpC9DT2%XdJy>t`mHuMT`wYeJnEkSpSO{+ z2N0+mZ5vy7DhUYcq8vCFuYD;VuyYyAhm9 z9u7PFRhpXk5I1(?M{nu>Z1orFNKF+s<{G+sGvmN38VnU@_QIq-7S98-`I!o^;%Rim zx$F+LHpRyNZ&-gRuiz*KA#hXaxe*mw<(9nG)JSy<{w?oULF3X9;2d0IdSMSW%B~#s z#6)(xnGMrQ8Bz2V^kxCk=`+?iFrZ+57TghyqWUlZU2SII-z9dQ*Q0aeCLw|b)U^2qNq?#Hhu9e{( zZHV84llwz^IP;Qcp`cohx(L`LE-rZ#oO5~XrJU~aE+{~)Pk8?<4-aQ9JZjaS;}YL3 zlc0|8WLgt|NqcEG4X8q^u1-zmZ)N$T9mz<=&|)qbx=3(I!0+%Jgv{ctD5*Q}Xvr>x zSfIPl;$nZphkdHZ1SQZ3zTy6+N7eyzKPJFUtuC$FF9~AxudXHxYo(^+BV)3>szopd zsm%qUk0n%G%cl35GY^*I0Oo`HphqGNf=OQAd!59@Kg0^8nuD)=!F#g3Rv)|Dn&w)I z2Z>Ch5YsGJ@J#YbeUe7`h1I7Z3tE`MVDMFHBnp28=SYY1TY=qDhk@B2Um)ykJS+fU z#f5f$gH{U7r07#HK*VeC#V^UJj;SVfATF@aIqG~E0_o9p*R_?0TnUydQ;Q@-?k0{A z<*|!~a_Tvejj8?QgET|GSl5mK)OtHhi0MUEpB|p1p!3twt3stGj8zT*F#h$pTnv}s zoj`vFOA6%yfae=t(^HRbj2;U7hT)3Ta{U2H3JJc8v)qQ_8L3O1L0bmvmU!BB@R$7K zcnHm7pk)CIQ;cnkRUmvNyZWM>+2IPo(A9D@ZDCUkTfO?p4#2w$i_t{PRIW!-Pj`Xh zg|Zb}GUy5uhMV$?1cBIGeIq@jYv#P@-QIuhfnA=PxWxMvKqeV-6&7c1x%r%s#QPeI zfQ!A}kz%H$D4i58C^^R>ukP*nn;q!CW!&)V&5ArNAP(X$Z{1Kn(XqIy^|Kx#dYnG- zT=>fk!&ng?T#v7tau&q&6yd4I`Hi;A;f4?{^=N0rxsrlIzJYpkq*=}?0aDOvGu<2St~DI zAts=d{D2;ZkZjFD=(n9sw`7PDJDHEyoZjT3@-Z+ISDHN-g^}RB^ z%ydGtJ zj`B>Y)+Mm*S$&-ic!qVOh*~G#9y{=Tbh&SfjEfHo6c`+aG`t&BmQ#Z%8-spmw(1>a zoO^!~a?LT|+dga!SLCZGVlAg3Z{ zm&~1C6DN#hu$nn)6h>Ryd%9vA>_wyvy-=egkKB?|qlDIr$Si*Cc0YgXKZq(M@GpnH z@tk^l1PCU^NQ$ovdHEDxL+NgFVxqIm0$Q6;Ym;Wgs#!=j8C{>4>LhP?U*I$5`zLA_ z2?g6n9gVi48HJ?C&FPV3h)|4in)Lg624brN9hyRBA*<8J8nF;RS#%aO*V^VdAN2!; z2BB1rJs`)MlADzDYvz9*oMum910ZqR(7Hhp?H(V^^8&ngpD~hyX+)sR@?rVq_`Shk zS9#A1KDM_SsvJK*3!57&U3yj3RuA{FQ83WoSu}1n`hge~?_3TZ1ssJ~)ep{f@n2>r z@=LrwCVhb8Hbhf3BoURK1Yy0NJ|&>5sU+$Dr7B@-TDJx`GxvXDRK<(_oA--oi$5LW zp?)PRC_41;jWvz2e3~$uZ&_<8IZO4FAO>hM)jSUnzuhGe3t~N^b9G& zfMDa=Xopx^M`?fmZ72pzpBP!1Wq|JyHD`-0_Na0*iumaJXQBj=RKpv=s+IXsjF5ay zmC&D7fE0Epp;rZzZlFDf0<96BHYeJ$*dGwS2~XWsGKA)Gnid}#)Y21E3SRN0@jyk} zhCyQ?x(SdU`CFMP|J07ymaKmVM?D=ei7D<0=w#2>*WzY< z3+X$T=T8|xU=Me*BNX|Pd4pIeo*r0U?NzHHA9hrJDX-91i(GJYPoUl;&X|TTfzpZPqNe zpqd_UwzOXJJve%BQE$=Rx51WTEKG3#zxzqn`8o_tpZoH`F9&A6nu_DbwG$*rWHQ69 z)tskD#UCN*{}3v~M2@w=-aZrWFCIP*06wv5?7DESskFBU`N#_qsGTTnwY7&DWV$4` zX6Ju?7~5!#5=n8M+8vUbs?UY?y#sm&s;j(%JPV}RKZz?N&fd^<#Pmlh45drv{lSmvcfD-WmB^_HV6A^lBcKhvr=8z=bY4$Z`9ym>U<%TPFZn_S zoVzE!^VjibS!#fIE%W@t(N`WD9QuH4`TcuIB-EO|PthRiFP$2D2!oi;=$u$Meq59S zhRUvNGFEU#5!z4bGGwy=VK6fINwHo;VBdL`=7_wpf=!%?pqfNI6+v;QxQz zGKf#_=xoVCznjitG2OzL;9w;DvX<`r;iUo0;kp z?jqtL|Hmvf0Y2-fnRrZ4jbB@4nmCOu#WP_0Qe_S@)e$3(uRabXIiA{B&u#Er;4FZ3 z_@RIxe(gBU(Ad00=pgCVwlwBF2ibqm>fbfI(h%RrQz>UbBYsqb5ZIO)`D+LfJx}CC zUY4QO0*BK1wNNIxFmpVThGCsqm$^_cM*0aJ^a( zy&D(DrfwI1Cd$!c>H0Q#>H+r5A>+aPhGs#eUe7^*Yk*sk5I!s3!1Oc$u$q4`<5Oa zU2~l5-i#UT{1@sLXED-QPcMHN0#LJsl5UYZX9`%U;M#w3LT*CMy;AsB0Z6%FJHc(N2kOA6g|GzueT>UO^fs0Zap3J2Xiy&K38H#U=&je-xRTVqGwTI}_!ccFc}A zlGP|4Gq8N#ttIM;%A;?~$2{V1&Tu?VQjs8pyTbid3ECNri#LB0E8+!?vEyLL2AN%+ zdS$KUq&8;8k5**{L9YM?3rkhRAZMV57B?JdN))XNBAjPQ;cdieY!1>k!IEVg3ZW>3 zhk#3!RKsbKg~e6zA#{J}RKeJ&yF0Y0SD&*1QTDqp!TfjuI+pmkoC|41w|M{rlV91L z@tZ?)lPC8iqh5bPqw6amll$DEVAEgckOfBPlUHzU{aX1EO;kiJ92Iod8bay-p8U$? zi#xYc=S3Z!yl7nhSHL;EW5BS96DjJmZ%%7EG7X@+5w?_&ja%(jyW;vw?BlCYFdSOaJoJ-Nj&Kk4egpcFy zBv$WH<=kn_0wTGRA_E^Nbm5oRNm9=vTji3By$UtIa|eRN&=1*gKuckUmja1P#eQiy zU>s_4@=}CB4beX}W7hWWV-s6`F?K5(uqR9E%Fj+@YvS=;l-w(M!1_p}S z!XDuYV~r@(p}><)T#P5vV0lV;oJJ=n zzt%4$IYhQcj{0i+V5>A?!Wix{@3rMB>S24C)QEp3!}SB0Y-vN^j;Tol0-}6Wa9d$C zA{LagNGuU)08zv$Krj{tdH56WZv3Y01I4LA<^f3nY48x*o{O1{+OOg#D71>rsR$qC zVz=yRjDtXBH&j#qBI!IyPQJ&l&OBunWN7Lc71&c)tH2V2?|wudO$#8+xNF(1Dk&NI z(v*KWhgt@dNt{1yHBw|qt9H1?D04tMu-UTeg5g59GG?gM2mwhFz1pv$MAX@Nk$x~| z?2wmCXSTC@nrDyBi0Aelknu=wTxqa8G60g zz;Y*H3=d>ONqsqLp#GDR?M{WG7uyaFy+VINEUP*4uK@`aUc75>IBv~6R+{}gX}nSs zorceEa!L=6I0d%JEXrsq&Mo01g^~aLdc3b!5AoOCw+)n{Ggn|S@hXc2&iNxZ%IN_e zKrdGdn#=^}%btm{Z0Ag|j+J0T3nYAN&$hV?d7G6erDd6tOVctQUriDP#=0iinvs7N zGnpO8caY889!?}`6ym8IF}vUH>XDiZFvh_=4AB%>tHTkmx8d$@8l#Jy?cOG8?ULhW zvINUb2)`3NMA0?j=uQcQzn3ELtCQptR2{G48M!Sa`}?9w*UZS^!hexQ&|5!$Z60XiI-`9;cN! z5xmKjkt0_4!T>Wvd=7^VPM@Q{WmYagPvW5aj?^2-4zfx|8FPO@?@&!K3}Lcgj)1m5 zq?Is~GsZ)Qg;YMfDySZb36c28zeQjJoHm7lsIL5sJ`DkbU5c6F$D%ss1!py*0j)&n zsYfjjPiG=~RU0H$dec-By84dUY}QEv2c!3@x_~ zIVD1G@j2O9UlEmUmU5m2uv@iCzHzVnwFDYv+@jTGdFYdvwfKLygtsl|A+P^lD(!^4 zUXQlbUw@>Sd8Fm|Y57g3|K^nGn{XL()V1Q(3(?kj#l7Ufw^M2p>SD*d1m24iAea{( z*?$>K{H72(0iY+$>3q z6|s~udQ-gU%restq9x8SE>uqURtPscSWg`8Io?7ug?yfpwnyP+5AhOTxnka;^5a2e|(HZZFT>a62lEAI8JOL|0rSV#-M18%Ez=QXY5dxTPM z^m#$>p*DYN+y-ka6w;txiq0nuVnDz(Z;Lv8e-db@I&UwqJ19P9mP`l{_ADYp4c`(O z$7S^8a4l=_AJyBmEl#FP2!krhFfGrq6Y{o_#5l)6LLJfO3_RLD5{0}p>Q3kCk1-jw5Y!geRX@qQ4B!_(xF61i5W%EG&QZ z6xeF`3>heXvgwI#u17);(T@xl?OyUuPSNAoLlk7R?Il{?bjy-Pg^4bj{*sb<{~_ZD<%hah{rrTt8zy{4!SG3$hpm;_M(=YZ3VTi_hiE&eM0z4E#9@C3zYG8U z?-vx}$}2ehS0{scJrEWj*V`pA`v*u_8ocrTDl~pDrH=8%rWh}?XkLs`x z3Rz34eq`qMF#3W$9arNfm99UUqrUsldgkoS$2a3rD8fo`CCQFf)r(P)2a1x9!G_XEcA)jpIPRZ6=<{FTtrQ_h))>S48~<049%J?-Xb@pDb4jnE zYCqp+NctO+Lk_?IZ`J7K#vc3+HP*RdjLx{58!2t-35ZgGVK4cKmDCmWJS;El!Skqvmn-m$-XnIrma>A<6<9l3@`b^a&6rOBBsFvj{+Z^AM4pi*U7Dk^I4b{ zKm*ZJ$5S&-2i1C48X2ZOxr<|Pb}ns1VWem{H$Q9TO-SlNr-C4sJxR*u3Ov4Fz8k`* zIS@AR6W*bp(n3r(FzChsffE@&!4Ac;2Rj=V9jt$h(u%joDjOhuV`8W5Of0yf6A4sC zk${C?zYSAmCo`tY>s>c?gH!6k0@*iZ5~9PYzRY0qCVpBs>>Q{O?=>{uHD=E*li7Bd z!45R3|KOhFHjzF?#P*Zf@ePj$(X}d{U)5B3S>8p+&fw#>R~YLzo-@NFZE@%N-2))G zc?N$0JB}7mN-x`4M&(UwjD8!ufFw74iO89nMPemN@xNlBW~fhGS;5;vkkV!bMZI`r zm(G9q?*qj6T=EF&Y9fSf?T4hhduIClIFe zj|}cTzZW@+v{D2i{HU*VF@%l(el7z{lL&tl0V=f#_k1y=z=rYHy}kZt-XyD@Xm6N6 z6(`6UWA!Wg6H)xDu?selEPmoq#U-uvD0loUi0t6pbEXCYN5SBz8CH}^W&4k4S#dua z0w1U2-R)ZjC50X+7HYr&HZLT*KzVyW1Ce`mkJAQ|B|V17Hw@G|WpS;FRv$1If4hI3 z4MW=%snRHSJBIuok6H(&^Kz_Y2A<^gU+8i~ytU>NOppC%!I(!It=thOmU0 z&%zH^W?vl<+&{U0tTH;6Ak~{i0X=_ksNcmXlp>R2A2nN*>q;%V$c|#c0hycjsZ04& zP8hi8XpKfFl>ph^gyL%=+bTd3wpvUsZVJ2T$BoStyIiO`F}hIw#^_*<>MV5rJ&YCAeDqHgC8w(GU09hSZHF`XhI-a?cUDXPY|=ZfL4b1ErL~gQ+w^aY$(VgD2za9ncj_2KLx{7vUK|S-e&4uA*v-0^2#Oa94e;bT{VQ= zd^J0R`nEfgW8()pqZOC(zSn;X%&b+A%OO7$pRh;gnEv)89Pg_=%Jjd^(km|}{?HHw z!ox8h5oHBTMJC#I-bn6fOZ>vQqlV(Npf~g==zzN}ke#dxslf98RVSW%P@lsVQYznt z1%%!9Y$71VHk-^0O4Sw2*NUj_)YSG5#mvU9by@)ZZv9KEj1(JP4(ESjEZm8hpyG^Z z3crY$1Bo~lwY&9&w5K5R=38EZN^lFQ48Q%A{o={%y=RvKx(F~}!j7&$8~Yx0*e)%` zOgV|Q9t&luHuM##UvmDixCai7%&ury}aXk&f8ZfNkPoUL~Dbm%lA zC^0R}$0t0v6kCp7B-07jA#cX=g%x|>-rm3sbgBS+ZiNBA1t^#WU05^P_nVK^7v&gr z_m#+=ejXnjVWWTNzoUN`^PQiZO5B~}+rp7Q+6uzPU4|Z9jayECUE!D6G@WZLkwEL_ zWDQu3(1!fNQbmr4>3iRx!sUHzFXHVwbDt*$xzUG>VLEWwRWS8ItEe`OBAM2OieLn$ zLz1K)q3eSRSe{LW~(VoK&s2Z7U4xMlf)~ z6!fPlO#wZYa^g_U#BS+lBdULRCc=!oCQehihnkZYBx3t*fpd~84Uhf@5U71Z>Zo_v z?|;dn;<10lwF~MTlq>CMyE*GHg18W>I4Tq&9<2V}D2t8kAW)=?mcmrq)u$(3o8a@&6-5bVZ|7N-@sTEUePW-9(PR+BEc zaCc79`taJ}_)3X@*>H?;{zC^*`OLtDdnM0e{6)0GKLQPNAI0qpSSIL9`*5=+<5(s7 ztlEMzMzIWHaLR`aNu}nc#&p=@WG8r@FuvntytWGkxi?uRXXgIH(dRrE%v=8YGi5rs zBNKn=3=S;-Wje_#RZR=MlZn{epaKYYF*~)=Cb5RB?JON*`@68&eYUy?V^Qoab*GTz zAge;}!7BRtFIrm?1cTR*>%)-i@kE*jw!F{?2SOw7upEK%+S}Hymwvr5gmg$Lb|1RT z7#}Sf@vt>}NyBTW|B)e1RGg9e48g~sao2w&tEt7}EIhyhE5cdqwekPw-xDx**SbqK zcp`Obj|)p#7v8!5UYM-x345&SSN^(L(3cg{pe{?yB{$frwo;>M622aevKMdN6&V55 zwmZo|p4Kv_VeFYkvmRjFZHq8?z|OSCTbN`Ppux?82>sU+zbz!CT-_n10ejFy&CP$+ zC$lnCj}5~;gd1^@6d@%fHdyHfZP*Sx!S>_@vao|VNHg~hz)lkA0<=)QKvQ)!QEpmB2Djtj=-Ot0nDrGwLEo|X^UOD z)*Ba{cviV^oA0<%P;OF7xDMF*{w054fLY;7aV-%(47vOL zYqzFwr!vIUZauv)8}Y0nbj#K%)ri(18xano$n`t&!@Hq1hh)4N`<@I#;+TK-h;~me ztC~)oyrI?*x-b45A%<|ZydOW2lhtd0a4|TS{v!k5QS3gX%5|k7Wm`bECXx0O=^ZUWzie9zp$>vsJ;G6Q&%Al&mm}e@d!|XUs|msix4@~cJ$$Y$ zKVvA641%`=A)-p3;I}+|J#c?$8p{vt;HSoBG(g+D1HjAx$bSRoEN>C!K?rgm*;wFV zo8SxG)=I_6nzN=l7%P}a&|(YN42wglC5-LifE$1he@FnFmCcT;y={X~9uh(JajhS$ zHLtr4B7^%3jAbv*I;cB9lTZ?7>`5c!2)ZT;lcMyZiH=5BfUl$gs_lPBib*-X(}L!j1~ekBj~Sm1tvJ zD^yMS>SFQST3-bG_x8~8{6rZk@hBv2tp>& ze$SIg;)j){Jy9E0_CJ`e*<4_>gzOZMtAC@zWt(~uvd5ZK@)&`cf`mVej6((86=Q9ibPt;P?9zYwvsuS zE1P8YS%*C1f@ptdBP{5Of7gn6^F?v#_VXsS3iYrg8NMN3ocN&>?#=com1n#n;7lqM zz_KR?$S?!-8tlBbGI6?DM?IPQXjU7YfRUKy`OY%tF0a!AP~g5hD0ygi3uerC{B45Wo5LfK#MwP3bL zg8o!aq%$CVggMcn3@+d>_2Naz5Wh`NDB*X>9-%(+6aJ{QT;m1a^8*X>6$H$FQ-T)? z^yZa&&qaSH@8BQ)23BSXf>M)oJZDhx+hHlJ+ULzX|9Lm0$WVL+@34jpmtgNWAz zo`~0b5gSw4fBsN z+s&fCaourq>QZY7H*zcu?lB`9)~aOy+@HVN(kCavhgFCAvh>gFCn?4%O;CbIpAMOn zTZ4Zak}l`2%gSu0>NgtC`L$AREZEKrALY{r!Jq|;Rz;Qz8oo%f^P;&05{_Kc|Gs3T zZ;VX58Ya~xy$A%U1wEd>1X9?YTv~>Lj<}UAF~`-1Jy&$E3*7T1ZfO{6X(dQZls0_) zOY3w!9YhM7-n=N|fY>CNAs&-9hGdxGYI}dxzqA9FEf)=WiA#xIw~DP<=Bx*cb`M<3 zSwKXj{Llq;%@@v;l7R)M1^nb0FHxkudmmxg&nvgY@IwVpJVap!ww;h!%6-TF(kR_A zE`Gd2z>mC@uNHPF5Oyui54)<>@1((p@J6d=;vNF zW!b`{!zXZ1%ew8n4pkT>dCy(3{EB~q(5j*kZcS4&T>;yxf-2>N@RLo)y20KfMyxk* zmqf${>N>T((BQ##uQd8%KuoHHl`o!01(4PWe~(;aiy@&JA8kC=*NIH zI$oGLiJp5~$cEF5MTFT?ZPs^l1m-0P7&F`eeLH5B9g)pVtfG72lwzAc)FQIYruHZI zA_1#*3h!#VU+XwLO>X>HmX+XmkM3fTXWbo9^7tz80*r+QEypfz{Jch7M^b!H_+tpW zRp<}$(Qv*6fu&ZFA^O|RFXDesCbLr)<$N+8KvdB%Tw&Eu1U%Pz{J6tKFd5U3w%?cQ zmkIS(al|XKT+_*o?sUQ?RDQf`q;^dF94G`?^3VP7qwQb?1H7FC*wNjSLJ3vM)fzIP0`x?K6S-24VXO$0i#D*{yUloLhFWEfHDOte3U*cgFJ6BsFN|Xw<2H~A zWZ#Si-U(`9ikaz9?Fv17kN5;L{ zff+%z%}l>xW|NO^%D7r|0y@!3Ym_2+H@0Ly%nwiK(KC6!;^GiB$YAA56{O0 z@zZ3Rw_%?h71;e!($tVL$0xSo1E*rwE*8{31J3?SR&N)Zev*4|UKliVyE2@fNg_s$ z_tNj@SMMCBm4AQqLTU79`j;wWw_%5#TZj_`s!a_r9K>*IDcl9VZH~f#qF_*P<~SS; z_dEplxchM9m81uRR~YD8h^>w57a5G0S9pn#(dcuE!JvA8Ca9j<>@F~vN0|LfBG#gu z%7h?^x@UyI4Q7(bdo*|1H9KkpT=_fEF8C~znr^g7*93n}ELc-qAR)_*MCzD+tBw>E zG*}nXU$Xt7(A(5$(F34)|9ckvMuWAo84J0$iK#l(Xt8Ek10Nr`-*$6L6!fFp&1^f{ zdqXYALlCp)%n+fK)}?R0E+(DDC#@0py*J*dH&R9$sdtv@eN ze_o(40G}WrAc6l2zy}QY2MF>9HfgdhHxQfj|0{!jX0n0=e}WAB06(GsFOdH)KtKQ> zpdWz%r(ry`xJ*=bA!evVgEa*{HM(jvBU75sGW1y6$GE*{wEUgqvNorn1Q9Kfg82ufvJ$Un<+ zLZb1e+c?eQ4$U|EON)GQMvewyu_uGW(2uRF(rA7NhN(YzfhCNfkarYh`->oZB z%ASqP=7!B;k(;8UYnyPuW2Q-Ui05tivf9{;Dx7FeC z>@t^@3j&yVtfd561%YKz8ZbOq4=4b1-kl`M_J+b#seae(Y-5)NB#kTuBzH z^-FmV%@BN}U=y};)?j~ZA8=_Wd9%zPeLViDm&wl2t?KzF+5o5WZzYZb93=oX`a>hj zxmCI=QpW_<%(WGV`=tP@TUHNUZY|tvaNRoLc)Us z$5dxYFYW%hV7s`QV~<5kOt)?hG1T;N{2eJWyqII02iF`1L3kCQGtX7XI zy}73@7agMOtp5Oh;}T9zAu%!8dbJa=%)`huPBRx@Ej;r}<0WYy9aa1HYH#X3Rn~QV zdaGYWo3o^DhKsF7owLTW-+XS#4GuBF$2W}lgDPXM>BTILc{or{_-U+v!;kq>MH7qD zCnFS&fb{g>1{_14Mvmu;6=PUX0SiPg&uvjK;V;o8Bvc>gDf(d!CqHi@schiv;y5XU z|8>E#(3B2S&~3h&Xc8`}Gu}5x?WA1^N=^vTmB4aFO%|;6t2nL%V~wEyaE;G9-xKwL z3eMDR@@bNmMqLWh(U=N%sCGOQnx4@ z2P&U422l-wf#t~82hD2~&=+s96aArB|2&$vyd9}Kpf}Wc9z{<>P0kUGZf!6@5=}e~ z+~SGPw#`MtdpipBkb)*ZmpR77oK3zXXWY(o8@-j9hql`fA`aq>NDjh^5xVOYpC#SK zDs@Qc&&pPhkA}%;*4cBqcT)ibv*G}w+MTz!N@?+zgpUxdsXVVylkO%( zA!NEI&LiD^N4By@OJ&@koTrQW(U9%sh{IJ^(_y;$AtaLWbIvl=O?Q?*fi+1?waZsb zLg@NIyYArOB^KOJ=PC+cgom!9K*y9zC2DyChtL51-vWdC|1Gc-FjKKdN31s5L3GN$vG%^T8HIFyN;_M427BXdr zjwK{ACYK}(h^Cg|$B*=;!Mbj&$u1@js)rkl4O9JbYvErc&d6Ha0*nxb5zcN($M#mI z6Mtgc!Htp_Kw#Gufvp`73Aa_A4|ZzzG|o7Y^`C~nyZY%6wONUhzA6d?nuhu;7Ob_? zf(H9(nVRNuxJDP2XY!p4niSYD(}pPJ06{Trk?*BcMDlF3cn25e6Vwu!(jFbGWP+?X>rwNk{XJJ zoi3By^d>R?lN8SA_5d97+VSSs!-j2Z@)B2U!TgZ3*rPr$&z=X{!v1%h!X^mfLkyR) zl>0+*LH)Si3-d-ari|#mw73nn>q+haMObfn*|)0qyd4AfU6Bbv1#w^?7ln@h6bu%1 z``!C!mat9qQG>VJG{sbAv4{Y4T-vaKfqC$!n!z+VHW#DGKU3&yo{xoc5FB%;O}A4+ zEz{@X{tM*&qD8p$_Cr@INnni)3P|cWY z97kkRH|7O|!H3+~Wm{Nrws=O=!qH$4xHHEI%n!Sb1&#GJ|cn0NWB^5QqtECOU+JAHvgu zs@VV_?n6#(TQR@B!~%f`55^d@ZprlS15mteTV77U@I~|Tb^L8y7NBDZrRL<)4&mhN zQ#Z<`2ZQxWT6vmkr+CU)Ge)U#EEE5bOv6|gp>s06Lm--}rDc>y4=a_-U^@S~_)!qq zesCt*8U*1sfkSTwDH;&L-?;!Deis~|6~%v$HRn7IiwG6BUV$WYZgPWD?w(DOX?e8v z!ndR@z$djEw|mGCT?=g?iIyJW+boanRqLS8<9ixB zDif(C{I{6O@VpB~2jc2QlPRZFWa(q5VQwyzXR=uYiIxTQPP)nWMgKfcG_oxIhReG+ z;hc^Fb_+JXP9E42AxKMrdmu9K=gFEmYFYDMpgx!)_zV#F-RfRi7^mtbWIn4%Ot+8U z(*eB(ETz%3S8hze(}*7OXa;h8H)1SSfc6P>`>itAM-K-7iD6_hu*8_)*Gs$q-lFn( zL{=k<+zO8>8=r>v9cBL#@8@n7%jf@kcESd!E-llYWRR-BWNP{vvTns-{=0<9bKGh31lM zJ-qf(Ot#PDu#n;<2)cjZ(tB3cb#G|Isp(PU!AP?;zn&i;(C=%asYl_N(PwRfed1l1 zmWt@e+ks)cr+BigBaTK!2F_HIOfSU&gIf^X@_%QFv-o?vI%oiUX~;gZj!1gZm4yK` z9h*g$oZs~A2lYZOdo);;_%q7Tm?(9(7j^+42zIXq<1UW}{(*~zEP25%X_VpjI}Keo z9gM%qcEYNRt7xVj`g?2!>=|<&3?;j4gp!N^;}n_WVWia%-uyakX!`7vJI(IGndlKw zUn7_g*Lrz#r82;{O?1L(_Ej5W8|}#L5>#tOlo#D;PcMHmaaMe~u@ovZI{_k5{(BWB znvB2?CN2}q7*bOhJ7x9$oUVuqCJJAu{Ch^2c4*bi2ktm8yR&5Txik!{&GG)_KWa_% z#&mqe%Y~4cwYAjQ`fv5@;>SHB1KZA$oWwqbqo&PxMv1_#{KR!6(A2YPD!VV35ZW6P zr3cdC4lw?|F0hphTLh|MQQOR5f4yYauo(>^-vU@f9l+yE{La?5~f(NzMVFV!-%bkdncUsZYSBq@zd?IVQ11c2(t)n~ z5TAe8xDRkr*`&r3C0X_tKqJ^+d~(;a|H^A!D?^+o*u4e!kin&

0na29seVL5qc! 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πQg_LX$4Qsr_3D5f17J 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 G0we&B{1#gP diff --git a/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin index eea30cfc07952704dd16617ebdb1eb5161dc55e5..29112f19e8158323aa87649538f232383e24ff5e 100644 GIT binary patch delta 63278 zcmZr&2|Sct)SqX@*!QvT`@ZiXOO{HO5RoO3HAR+?Wl;7d8bxFc6)8)Ww4f-`A|#a} zTOv!Ah#9{7Jk;y^eqX=$p6>sgbI(2ZoO92e;eFpbcW5_m(q`~5>!DC643P|liiR*K z2(!`3kg3g&E+VUOTGoX;>a6tM(V<%@2%_eukUt^7{3ZI|3dR?wgz+Kp%m zf|ErTvk$awiRLe#qfX=SJ|;Px3;$6pn#~M=!oVp9|RjE z0yrYX!!QJSquBsl_{c|K1{!PyA_X7$$o7#D_O2r%c-N5;y!*%qWA~AFYamBP7`u*a z_jhNm?VS+}wvgOh*dk~dvEUKLE+76X{0%#Tcl&@asCR{Zw+7+^!r0}5BN81@#vqR% z?9Kp!i9`k-kK4VG(E~bR^Ws?r{4f1|fQs3=-}D z7}CMvZru*|Ihe64+~<&R6S(gCQtgD{{5GfRz8XYJ7{4#Yf+0rzUBt5;si^H7A?#i8 zLGZ2|A$WIu5XSEK?AAcygD`f*Cl)ylhyXbfpNjy(@iE+vPdqFIr=x@86HQDeWH$0c zsf3zFJ}86G)5yo1_1E#8;>y}~9AWQr9KpLBNAPaP5ytM~+O2^&jxcsPp7VF-ov0#s zcX1(%T|WF(_*+~E-t7azpxaekyEPCW5XLSauK)F6Cv6Dc?E}Kt<-=cvzdj&%w+{$| zc9##kH4q;V#x5U<{`$bTT{;Nf?E}Kt<-=cvzdj&%w+{$ocM2xeE=0h zO9==Mu^d1!*?uDMpmH$8iXAU$KpO^K{tgUxKN;0K7IvPD+8wy_WHjtRN8CHN7=_aJ7#T@V0^X^Fisgg5B{t5k>sh!mnmb_HGb6O6&l zVWbCQH1R78sUb$tH^3ii;^;P}hnqGKPj0Vfgy6RqLKCO9@wPP(uW5D^17Z#&7HQ(s z$USoh{Ag%bZV?Q0{^l0JyK{>$cHgtz8c1#t#;)AXu@e;9coi1lo#0LYKVNjj#cc-} zP{3hGg}4kuT5vo##NhO6?1bURym%tA2%<-V0_tHC#xODkTERp#2Y?{-j3js!_=ARD zG#(;3-~+P^M3jJl0AL3bsbNh9z!Cr&ARe@T@dgU`gAyV#!VC~Th!H@rfCa{QMz8|0 z4J*O6g^3)~+ElpylJn0^}p!+Z!tLG-{5NV34TlnzA!l(9q+7{bp5$Rfy+7!2va zxp$H(wT|IGi@UEmGcz4ne#_pu;)ho~fFZ!h2QM^Hb6dJoa%iG9jH&+~j+Usmjp40B1OC8k0;|AR22KKoXwE@+Q^Sit40D0f&b_gMA$)H-fszW*7LopU zZ;rwEzkA~hL-^jnAqDrwbsO`bQ~*R1J+@c>yEk6j_}?LXI8YYwnQ4#|LAgXMy~&pgLD9&^LFH*A2WgKfe+j2@rc&HUX=_(9&nf7eh5Bbro)&Kc#{esHSzMcm<|OO z4w`rshW{eWNz8#Uh%npuMW%C;P&5}XFu0}l&)2brJ4W_DG|^X*DO z*t;qP!MiF2!MiI3VbJfYl-(Lgr67!5mGW|D=bcJcU95BRwL>UEc0iryx1Cgvr5C@U9zzI01D#TAPqz4s)v;_?DDF>YN z2q5JouE7u-3f>tCCa%K}ya`Ni?-_b)^)3JSDTMEoxh`TykGfymgAgFsM#9Kg3+1h$9$YhT!%DDiIt zSJ5ca<~agUz`fJ6)1LHq1_rtoVHE={m!0Dlz|-qou3KR z9*C114BQGVE8Xi9z+co{}-nn_%BWf{uif9f7Rh*0%tO|vE)v{ zzcTf@3f2LDWVVFhJt81OLTIdpk}D`(K9l1A;=jVMhJx`;t>XX9A2c}FvOEP3n<{O zaE|B!Ly*<&1^n9>`#0{OLHWQ6^bqg1KN#W}7=zRB3IKm3hye)P7J{>$+mRwK94#>v z4`W_%I4z)!Ax6Lu>2%JZJ!4RW&q;Q3S$1@g&;6pLeSgDB@VF-VP$O4P8 z#6%c^7iy<7rXp*Bv?kjxH>_ek%Q&J20#rUZ-OxKIt=N7eNLonfTtc|{MYS@wyP6i|EtdH+iJV2 z6OsQ{odw$pj9_DFa16K}c55Kjxoz^l>MY&yj2zB)II$dte+vW;DgsL5?=uZTpyd1o8TvbJ3kBr67UD|Aijas;TLSXJ@3GOZ3n^s z5^H$rul~Qjv#s#&I3C1a*dW-Mal6gzK=3Ia-USQ9$g_p8|0VL$_NrZpMCAV^vS?dj zcbnO*fh2O<oa58Cl3L=3{54a8{T5Dei8-+8u1VEkV@ z-huz(g5ZB~$tV7mZx?*VHr`$EaM<8t!qE|zVGJOu1%!>2 z_?xqi{;hy`qrU-k0?G!I0<%G)tyBEXB?^9&voT5q_ShJsf;PsH*#wwPg7F=UDTMQb zifUsKnJrO)L~U#U2@wnu!rMgX7D7?rR}ZKS@F9Eyjm$9Aus#h!ae&JL1}h?ALnxjd z`HKUgoIuEh%(#&S!U&Z{s4|eLfT4=W)xb<0nQ0(&52B-w(1QrIM5sN`JBDaDfSDsQ za{@DGWOkey!f&_$cmfPpFxVZ&Cgn9v)H!|}<=qZHyBJ?yu&j1ZSF#M6N zL%}QzjPqcGBk~A@Mgkm#%a58ug&J?gLw9VgUlh8<`KF8^nM{WR}gz7fHx7z7MP)EkfNpm-)1(*kvRnoRdx*@ z!~$O*78>jZCk?1v8Y+N42Fo7&XIIHU!dMzye1|~edl`s`hD5V_<)Iab zJ-br@GK6s1bc#?d+9f@^O%YP%ASV&x;<|BIGBgQC9!--CK1YzgM++op(kehVna(@)KQl4>>*W12J&sBQiC2t(8b0V>X0Y|1!d1_K$&Pgf$Zo#kSI2hKu(d7 zU9kspX9cy%4nBaAaM-it!1*ZPotRp-h$f_uhhCD@Z4RSw*efEWIAaF^@Fp0MVx#q$ za99*23H{vdcNt2UoK%Ct8dnwN4Upn8I?4&sIi7jPva(z6Ba4vqU`KntW z=>r;<6NhPJCowW!rXVCxFp`ut*3blYN;IkNI|aluCP+aVJM&?aH(5D>^c>~3I*AgF zCN-cU+2Y9EH$luQ;*}}DS;sslHs80fW55rhfjcODCfFg@-44;i4n=lAz#)7^yj;SC z0~9C{iCs9F4T>WtVJT)eo?cOAAd%}6LN0CL)=0sqI`U1F8;(Rt=63!oN-lxK=q9Z2 z1|>*O5~LVA^>y>f6%cbgD7@z=lH1fIO7T`jJPCHj?fe9agn5AlP6A&iQE}vP*wgDI zICwD!1oxBB_;FMmgQ-8@Ji$kJ}XR!PFqqo}w8xDb*s>cp^e0xLwifTI9@Fhe9^#t>AJ1P~dZ!uk*q zhr!hx;i4(jZi9HlVh9MnG6kbPE|8RfVN1ABdjUnNb7qH%G0NE_SCq2n2Z%sVqKAYP z=HNq*efzoz85CnkO#9K}ZoyMzB;2YfN^l0ZN@7J(b>|W=og@rO1FZ7|E}|$PAvfhg z5;KHs5dyYQW*-AW5(Ok|2rTZ7-q_E{Ii^O;O|V|Goyh=L6h+y5R!(pOVdS7dY=ZkM z%t!%I34>i(QASIVjg2#VAptaWIGa`v(gJ@S(1ViE?OcuhdQb+$+s=iG79a(|AJD-7 zl*k5tEkcxn$)oI;eGto00bVedg^3PKEMejUlUSHsg~=_LG{fW-Oh#a`2op*^KsaF{ z4ignXkZm>w1+&d^APt&9LBIwqG|{e6-T>mULH|FJ?2H^p3*Qn~sOeoV%9GKu-XZzK zM_1$Pct`h9<&pTw5Ke}NK2klI*5eHajl!DlM?86@8~-(xo9q45*3gl-WJ?;#$%Ck; z@fWW}D>3U?=gGIL+L2GSlpJd9R`Joll`|ACw3nO1@c4~fr*%;oYqoPA-|_PPzDIdX z0s3GvI_?vvt)`cXNBvpaIIl;uIe5mZX85~Q2SMuYkCq9F8jg++{0~RPNMf%KW~{mo zFO1K1UHASpb&*9bul(Y8bd_LiwbbC-$LBe96lW z%Y3|2O=~M8W`EMChzB!oj@MtFoCvVCnWgtR^&ZzOf44ZW<~&}E?D93e=XP?zO8gWT zw3Q>}bf5S18hv#A<*-TvU??v$C($)Tvtc$^YLkN3lKK5XvPq7(bcM&HWN9y#B7|5>Z9 zvi{b2@_mn%quSlS{1SzuNK3-F<`IlWS+>>8@NoD{MNIBfMIi zY}+;2E9CUnuti`J&F!$o>vmzCsI#U|0l5{)mX08@4t;dG;_Hx1z8`kdUvXcHS;EkC#Yh@ zWl?G-enfbA^kK*?mX2l4?J!h!yXn(#T%Fz}Lloo!MpJXK<~PZ;*PB zPJhiP4$~$W*v6yxv2Fmb9{SvacR;~}52ME>eI2`*_258rOPQ8}Srh+td-Ifbo_SRZ zdIdacp(4ZTawWz>uw|c(9i7mHyH5E7ksMx&%Wo!+Gcl)yoRdiUaN3{$TU=bJ=rf|k zQ@NIv+4}aA2}4=uK5)scl-Jr&x*IF_$i7>zywZ0=W$AT}Lf^-(%jEA4^s@aCZS2m2 zqS^4wQbD-w_ScLlRC)Df=Q1X{%Yfp_qoWp=b}&B za<3oLqPxg3(v`C@jF%dP#BbNxzkT(FSKh^P(pD`|m-yYzcj&qOr#Ty}?Lgfg-UCnM zK6U7F;)tM9_q{oKOd%|yBThjX)nN68-ktItUVvb(zwmRp?ZQ0$W%?){*4Y%luE+=N zE8*-KpWGE>gD5F_T8FC58fSW|oXPxt2TXS0c)vIko{zk5X0h71E5{RaFO{?7ckg&m z{Z#8a-e2v4Euu$o#riyY4-(e@+%FYL@ezNewe%WAo$_QonuYAAHg8OPj9r@E*GE!W z0TcK*w4mz&o00wQeSa_-)hd(s-W0u&HTOnh{T#%=YuBZl>i?25EM2 zvAH|!ABdHCBA6rfq4dKX_7aTBUYfTKmn+Ssz5XS~Si9%_t)vrJLGqF^=0J)EMky&w ztzRt`j-n2(CA@vSz`N?}KGMFAJH+Bszf$K>X=W7?nQCoV{>l2=6vh%E)#j~!<663J z1a6JZY{)#K~Ft+^?ZD?wf&3YJN&qhRig#vCf9Wtie%`m=%H-;AbJnS#*{UEOqg|bWkQ*S5g=pt(QD}-`|r+RQE!lX4I zWa@K$P=JTnUOmb*A$;coJIC;```lO0ez1MBS|^%vB|LHLzPjsJs$txvOJ}crDx|5{ zcpi}Ri0N)4OYkJnP)r>rwz!vUeFNrn9fdh^D^a{B-SV%Gz}eo zX#eV}a7No-AZnbH(0Ndkj`w3~q)2-wtz2s% zlc2Y%S*fhHLDpP)_-8t*DW%*mL|y4hAN9x`n_~uF_GD5C&+~eOQdF^}uo|`dE~ws% z(b)gA0!=fM^4VFJd$=mtDRTO2bn%z!19rR>E_`AmS6iM{RS2=(t7s>QCI1|Lw4JM%oi_Bx?TH^Q)cVXjVc+#_(#=gn{guFrdGkZP>+jGJE5xwm!8<=7`zQAe|dM9t0L=sEUwsPshm zrR=G)?a3a_Oe?R--=Z(kd`Y;h9(?kncECuqL7&5?h$?&efS3bGL-SE|ea}~jA^n@(9VM1b!o=Am5g~~> zh1L(I`(-M87_=2ytn^|e@yEp9mWwh(#x!bLizb>EJT$tBzZ9TGsv?EeA$-fG58(lcx(ij_ePcD}l_-ZoQ0zrOR#a zPY;M#C})$epTi5pZy8_GqKJ$U41aCIe>;&p=0t#8(5h89YmWw5!^B~xRF;v9OAiZD zLJf_w>YwJG-TUqK$+8R2&nf?LkTA65IZLV7b!uhxaJA$}KDF3kji(0dcz=9xMgUFc z`R8&1p&WxP6cvYRZjAdn2)hMU65i`R#JucCS6Mn?t8)W)Ym+jg^~3z_&}9d=(#`8( zk({?4wHD-7tdd0b&>03%={`$o=X2GKpe!>LD7CI=+wkwXD$z?2T>7rpOJ=bq*CJAk z>+BPi%3>1m3iugN@MQd9ITr7UnF{dzRN&!VNjZIXpz?k6 z>~|N8g3}W?1@e#WNxRCaT(Bmb#>HKeZL7L|hf+h5*1P3hs*@BFY2a~V{ldG2_t?BfZn9BqBoIJEaZ zd4AM4nOB{~GIVu9)ved3R?yvm>2u+oiLW zILuu(^FBl4d5~TDK?T#wOg#ZlntQf%|7a&J^5j@J&1Kt@%cYOHur1pBTl}FPVj4?Y zS~uf*R7BbS#MrFu744hTWu#~OW3}NU`s(CnzQQ-!VzwF!LY2ne`+g_-x`hV5YqB|P z3URNu{ZKNty0<3{oA6_9sm5Fd`OKRc$>kX4+G$85@8JfK=sBvWC*)6ve2zMLp((~F&7UcP^xSFzhl z(EiDkc+#{(i{YC*aZf%u+d&3wnP^9;#p^OxtNEcaIhMxmZV3Ea9Me}?##s7%dfrBS zipp#5EL#a!^64|#N;_St$)(}K&!8aMS#L05LH}a)Xn+or8#?wkyGcK1%c-*1&l#ha zm>^6mbKpoV6XTTtR{i;O=9Q9tOs*cjQM|lt){*83oT4=kY3LT-vu?>pNVv>+X1_co zxtcfo;#4)8f@zI@_5>d7{O5wUCiAOy*QD97J!UqnZ!8T@+!gqZN1HJ2YZNaa-2DEd!KGSDWFSOV z{Cta6ZOgum&%9we*u7T8It1nS7brHTo|!9qsfN0-O-960x!DcN;JM}Zv!Xv7R}^BD za~-;5^2lpL@Mmq$UGrRFnsC3w+xhvTgI6=$m?$O-F@9(6z^yE=K$fh z`db{CE*}ik3hlPvV0zd-f;#83`~hk zj~K96qdIh#rTtRali}#j;=3p6{S4QHAA1yjDtXG9bi`dr#b-n1hD5gSyc@Sao&2NC za`Tz}*S{=i7Fpu;B1V0PFYy~@yu8Bu&QyNCh&`aiSTpFq*7*AX&%S-~4MO!~;Q|?~ z-MvC&Nj0zIkKcBdFsJjqV(yL3Kk?Yt%`ivmd-!kfx7=6}PFWuEhUI02yRLP;Ra?&O z6C0t$oW}!Ch0XWr;5wS`iB4QMA&5<~)6KbL1n#l;!CV-anANgEeiwR+x6PzEn?=d% z=&co2!IqH079l3P*KlILYa@*`4FkPi>6oKHcmt-twX|YhSR?wvtE~%GO5dJURCU!| zZe_Wv>`qhlrTHiSwBXy+JE7cNGwc^X3_d_BIEK~T>#SnJEgv6KUJX82wS#y#{5O9{2sC?k%1Dc@`VNy4iVxefb%GqTPJi zT(~rvkbKXfua>o-#!aGAj-8I+U`C~o*F^cJ(IZdiKt<@NB3GCMg}xBp;p_Jhqvx4C zrK*2s^wtQuAKsqpv8|B(vm$@}Ll?pHWK%s$_W6nl{vP&W84W?Z%Xyl4zgbSHyDF

o;F6-or$Db3Kb}cp~xb@t%e{ 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 aD7HwHgd|6C6tYrA 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

|KqJ5m3 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 diff --git a/crates/jolt-verifier/fuzz/fixtures/zk-advice-consumer-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/zk-advice-consumer-bundle.bin index 6fb0ec507a26a3e408aaaa3762cae4c9dd3539c7..7133435b128adf8e0b54da5ab0bcf3c6d090423e 100644 GIT binary patch delta 74211 zcmY&e2Rzl?8^6Q7_OqBhFU3ZFotVpa&nB2K8L z6<8uEIwue9K~Ah8Lj)lZNa_uQ7Qkp?J-|pR1d?han+|3)K@4gHl4jF@Fj~u8en~7t z6{$|Z*u|nZq>$x_cE_-&D0KP6rPoNLFyY~qXw)RCT-NzD5|v9y=+doL) z6ZAtkkrRLnBy{9xqq4VyC762!saSp|*8qbWT8B@aE9}LhI#$c;OX`qF_VS>z7!XEG zIj%whiBvDwsVoJtC#*i_W!mW74wVx7t~bJj?kZ-2VbvuN5-_7a`A_0f-d%-7BCnT! zt|rGIDGXu_RIoQa~dRS=$V1q&TDjY(xE@ z2C!Rvn;`)fz;2+4mv;Q3fr7(AyaFJYi1z|QkRZCiR)Wxx5KCKIkmDdUz&!UIKGjXw z`s;+3Z+H!TcbGJ!AW%oU%Cy8lh!6Ha2!rLq#G4QViKN~DT*&tjh=Br!g3v*mhrqtW z+&#X-c#rQe-s?Ne*z5aV4cK>>vB&p_-Ge!|4~8)~LSl1ii*UA)84ok|*s!ax8-Ez@ zwE<>O?TPlvB!pJI5?oJK|(?NV*msb@pwQm62w?Y3KcyRX96(+f}qXdNC?E{ zzX+p=$%KGrUc>YqDM%K$$^XSa3*v)C2x&O}*$||K;$OcV|6GW%C;qu`>6$dTAUO#& zO=pob1de7&CN%NMt{(SxxSqgyg1LKQ1mitn-hyoUl zQ5k?xjC7$GF%T;tDe#9Dict`;n(+1=AMzz(`W-LLYe)p*MZGx+_d13~h_T1jM%V?I zyT=6>?{NXfdtHDTdrNJv2J8aN*yF<6-Gg^Z4aR#*4QA}IVOL?d)L^{V2ADy+r_}ap zz&5~)JvOxM+OU&981Jk1I*ZC!~0zuc(-c^#(Qmm z8GCHlRoJxw#(Qmm8GFlVuLf)b%-CbYN5}?HJ2Vu4a1(n01e5I-1P^KmP5iuLB{gWy z;1T!&!M!ia*PR`9UX;-txbvb+?m+lOK@+DT2x<<+4>n^Z&O&hafk6@HU}Yo?s9+#Q z?I+G}GpNC}2MluJ0>EHG4G^&G7bJx$dZP&k805rN!k0Q8n!gYm3+}|G_$u_M<2T`5 zz@^{^YiWoGP9TTQ=mLuriDVE2F2jd{j!OYyaE0ihJyAp&2vQ;GLCXL?R7Co1jDs3D z5HoMDrbj{#Iub=>-NxIyfpD>NB5?3_Ag^#kXD2YU^IZn)AmpCR!WgLRW){YKGYd2J z-m<+KaAsk~p3HJXN6-<*!r7G2L|zC$eF1veXo>vWR?;DW)ksC6AOvZEk5HIF2t;8_ zpfhF_0xn9zYmqoOK%g|FAq*j7pzKB5M=*WQhF1hkuooQ>!OzbR^OYerI>6!w(FKuE z-NhRSU<5*tcnD&Ez#-`Y1RLqV7%Tt{9fTGPun-|c)Q1;Bnh;-{Xar&0HUj$^L+}g^ z^mMQfNbm7&F)ado^Z>q`gdp^4fc$~{m_d*hIJ=Y1Gus%4;0D@g;#nAi+S$q0StwgB zFn3S3V7w<=Fy5Ojn6Woodo|!}!HhlGy0B{|;b#%k1>lz5jwL%8y9hzx1)MQfq9X(W zhCi91h?lmdJ5`4wUV$*xu9q}Kw{46=2mvoyh}Q@jT|B(k0TaXoyd%(817Ao5x;0QX z(8NFjZ3j0VO}q)Qf!5C534$PWcUpj!A~6h>{&#nxApGCmiGd(=cc9>cyA!{Sxe;=R!y9 zUPCnT(T+bj=vJ!I5(^*990 z#0psdU+1cZAUC*QP&Y-;5^Eq#5A2EokcwEpEv7|4HH0EIK@jxP?O1aV-$EF~8fpnh zabnB1HiV!+cEAw47|@+V6W?Vdg?st3k#-8d~-#Hj=325Tq9nYwt>cP_zi4cYo22~H5xCz1ko?5{DuVbKz zC

l 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 BZnXda delta 73562 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 z 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 delta 74979 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 8QPViUD!0aE>#vF@YhIc4>bef~WV2TuBQh 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 diff --git a/crates/jolt-verifier/fuzz/fixtures/zk-committed-muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/zk-committed-muldiv-bundle.bin index 44f4529066af6e646eaa8a85c7161d347804bf56..72a05ca690482c7aac21de0492761ea649a7e396 100644 GIT binary patch delta 73562 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