diff --git a/.github/workflows/fs-soundness.yml b/.github/workflows/fs-soundness.yml index 25ec3dc794..e12348a4fc 100644 --- a/.github/workflows/fs-soundness.yml +++ b/.github/workflows/fs-soundness.yml @@ -58,13 +58,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 @@ -78,11 +77,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/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8f42d04efd..7020d93624 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -73,6 +73,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 @@ -104,18 +106,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 Akita 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 Akita 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 Akita path: lint alone and with # prover-fixtures so the acceptance suites keep compiling. - name: cargo clippy -p jolt-prover --features akita @@ -180,14 +183,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 Akita 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 @@ -383,7 +386,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 @@ -536,7 +539,7 @@ jobs: 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 @@ -571,11 +574,11 @@ jobs: with: tool: 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 @@ -610,11 +613,12 @@ jobs: with: tool: 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 Akita 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 Akita 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 @@ -850,7 +854,7 @@ jobs: with: tool: 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/CLAUDE.md b/CLAUDE.md index b164f01798..04b2fa6125 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/Cargo.lock b/Cargo.lock index 8c6354faa9..c4d1d56270 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" dependencies = [ "akita-challenges", "akita-error", @@ -246,8 +245,9 @@ dependencies = [ [[package]] name = "akita-serialization" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" dependencies = [ + "getrandom 0.2.17", "jolt-field", "jolt-poly", "thiserror 2.0.20", @@ -256,7 +256,7 @@ dependencies = [ [[package]] name = "akita-setup" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" dependencies = [ "akita-config", "akita-cpu-backend", @@ -271,30 +271,20 @@ dependencies = [ [[package]] name = "akita-sumcheck" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" 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=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" -dependencies = [ - "akita-error", - "jolt-field", - "spongefish", + "jolt-transcript", ] [[package]] name = "akita-types" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" dependencies = [ "akita-algebra", "akita-challenges", @@ -302,9 +292,9 @@ dependencies = [ "akita-params", "akita-serialization", "akita-sumcheck", - "akita-transcript", "jolt-field", "jolt-poly", + "jolt-transcript", "rand_core 0.6.4", "shake", "tracing", @@ -313,7 +303,7 @@ dependencies = [ [[package]] name = "akita-verifier" version = "0.1.0" -source = "git+https://github.com/LayerZero-Labs/akita.git?rev=cf95fd05b1d407083096ddf00752aea0048cd0b2#cf95fd05b1d407083096ddf00752aea0048cd0b2" +source = "git+https://github.com/LayerZero-Labs/akita.git?rev=a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e#a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e" dependencies = [ "akita-algebra", "akita-challenges", @@ -322,10 +312,10 @@ dependencies = [ "akita-params", "akita-serialization", "akita-sumcheck", - "akita-transcript", "akita-types", "jolt-field", "jolt-poly", + "jolt-transcript", "rayon", "tracing", ] @@ -3193,6 +3183,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "spongefish", "tracing", ] @@ -3211,7 +3202,6 @@ dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", "rayon", - "serde", "thiserror 2.0.20", ] @@ -3262,7 +3252,6 @@ dependencies = [ "criterion", "jolt-field", "jolt-poly", - "jolt-transcript", "num-bigint 0.4.6", "num-integer", "num-traits", @@ -3271,6 +3260,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "spongefish", ] [[package]] @@ -3299,6 +3289,7 @@ dependencies = [ "rayon", "serde", "serde_json", + "spongefish", "tracing", ] @@ -3339,7 +3330,6 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "spongefish", "tempfile", "tracer", "tracing", @@ -3374,6 +3364,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "serde", + "spongefish", "thiserror 2.0.20", ] @@ -3622,6 +3613,7 @@ dependencies = [ "rand_chacha 0.3.1", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.20", "tracing", ] @@ -3661,7 +3653,6 @@ dependencies = [ "bincode 2.0.1", "criterion", "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand 0.8.7", @@ -3787,7 +3778,6 @@ dependencies = [ "jolt-program", "jolt-prover", "jolt-sdk-macros", - "jolt-transcript", "jolt-verifier", "jolt-witness", "postcard", @@ -3811,7 +3801,6 @@ dependencies = [ name = "jolt-spartan-prover" version = "0.1.0" dependencies = [ - "bincode 2.0.1", "jolt-crypto", "jolt-dory", "jolt-field", @@ -3835,7 +3824,6 @@ dependencies = [ "jolt-r1cs", "jolt-sumcheck", "jolt-transcript", - "serde", "thiserror 2.0.20", ] @@ -3843,7 +3831,6 @@ dependencies = [ name = "jolt-sumcheck" version = "0.1.0" dependencies = [ - "bincode 2.0.1", "jolt-crypto", "jolt-field", "jolt-poly", @@ -3852,6 +3839,7 @@ dependencies = [ "num-traits", "rand_core 0.6.4", "serde", + "spongefish", "thiserror 2.0.20", "tracing", ] @@ -3880,13 +3868,13 @@ 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", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.20", ] [[package]] @@ -3903,7 +3891,6 @@ name = "jolt-verifier" version = "0.1.0" dependencies = [ "allocative", - "ark-serialize 0.5.0", "bincode 2.0.1", "blake2 0.11.0", "common", @@ -6602,8 +6589,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", @@ -8044,6 +8029,7 @@ dependencies = [ "regex", "serde", "serde_json", + "spongefish", "strum 0.28.0", "tracer", ] diff --git a/Cargo.toml b/Cargo.toml index 96f5cda32a..5d34c2a459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -202,12 +202,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"] @@ -303,15 +305,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 = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } -akita-params = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "cf95fd05b1d407083096ddf00752aea0048cd0b2", default-features = false } +akita-algebra = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-config = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-error = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-params = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-pcs = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-planner = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-schedules = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-challenges = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } +akita-types = { git = "https://github.com/LayerZero-Labs/akita.git", rev = "a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e", default-features = false } # Core Utilities clap = { version = "4.6.1", features = ["derive"] } diff --git a/book/src/dev/testing-gates.md b/book/src/dev/testing-gates.md index c4ec13ff37..f419303ece 100644 --- a/book/src/dev/testing-gates.md +++ b/book/src/dev/testing-gates.md @@ -57,12 +57,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 @@ -85,7 +88,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/book/src/how/akita.md b/book/src/how/akita.md index b2e828c69c..607b8915ae 100644 --- a/book/src/how/akita.md +++ b/book/src/how/akita.md @@ -61,7 +61,7 @@ The verifier derives the expected layouts and roles from preprocessing and proto Akita uses transparent setup. Jolt supplies versioned schedule catalogs as `.aks` artifacts under `crates/jolt-akita/schedules/`. Preprocessing provisions the grouped schedules needed by the program and advice configuration; the resulting verifier setup carries the catalog used during verification. The proof selects a schedule by digest from that catalog rather than supplying new schedule parameters. -The workspace pins Akita to `cf95fd05b1d407083096ddf00752aea0048cd0b2`. +The workspace pins Akita to `a4978d59f4b170ff3ad0b8f5c5ebe0172dc4be7e`. Jolt delegates to Akita's adaptive ring-dimension schedule configuration and uses the CPU backend for every fold. The checked-in schedule catalogs match this revision. diff --git a/book/src/how/architecture/opening-proof.md b/book/src/how/architecture/opening-proof.md index 8e97d98f74..e2be1f5cb5 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 native `OneHotTrace` group containing the actual one-hot columns. Each column has `log_T + log_K` variables in `(cycle || address)` order. Stage 8 assembles exactly one evaluation per canonical column at the common point; it adds no slot variables and performs no selector reduction. The [native grouped opening](../optimizations/batched-openings.md#native-grouped-openings-akita) proves the ordered evaluations without combining commitments homomorphically. -Advice and committed-program data are independent dense commitment objects: optional untrusted and trusted advice, one whole `ProgramBytecode` object, 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, whole bytecode, 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 whole `ProgramBytecode` object, 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, whole bytecode, 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/akita.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 5812200f87..4e21e60a1c 100644 --- a/crates/jolt-akita/Cargo.toml +++ b/crates/jolt-akita/Cargo.toml @@ -14,17 +14,18 @@ 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 akita-config.workspace = true akita-error.workspace = true -akita-pcs = { workspace = true, features = ["parallel", "transcript-blake2b"] } +akita-params.workspace = true +akita-pcs = { workspace = true, features = ["parallel"] } akita-challenges.workspace = true akita-planner.workspace = true akita-schedules.workspace = true akita-types.workspace = true -akita-params.workspace = true # Canonical trace geometry without selecting the Akita PIOP feature. jolt-claims.workspace = true jolt-crypto = { path = "../jolt-crypto", default-features = false, features = [ diff --git a/crates/jolt-akita/src/adapters.rs b/crates/jolt-akita/src/adapters.rs index 45243a9c75..ba385127f7 100644 --- a/crates/jolt-akita/src/adapters.rs +++ b/crates/jolt-akita/src/adapters.rs @@ -22,19 +22,22 @@ use akita_types::{ AkitaVerifierSetup as BackendVerifierSetup, Commitment as AkitaBackendRingCommitment, CommittedGroup as AkitaBackendCommittedGroup, }; -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}; -use crate::configs::{AkitaChunkProfile, JoltDenseBounded, JoltDenseFull}; +use crate::configs::{AkitaChunkProfile, CatalogChunking, JoltDenseBounded, JoltDenseFull}; use crate::one_hot_family::{ with_one_hot_family, AkitaOneHotBackendScheme, AkitaOneHotBackendVerifier, OneHotFamily, }; 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; @@ -186,19 +189,28 @@ impl AkitaScheduleArtifacts { } pub fn dense_catalog(&self) -> Result { - TrustedScheduleCatalog::::from_artifact_bytes(&self.dense) - .map(|catalog| catalog.catalog().clone()) + TrustedScheduleCatalog::::from_artifact_bytes( + &self.dense, + JoltDenseBounded::CHUNKING, + ) + .map(|catalog| catalog.catalog().clone()) } pub fn full_dense_catalog(&self) -> Result { - TrustedScheduleCatalog::::from_artifact_bytes(&self.full_dense) - .map(|catalog| catalog.catalog().clone()) + TrustedScheduleCatalog::::from_artifact_bytes( + &self.full_dense, + JoltDenseFull::CHUNKING, + ) + .map(|catalog| catalog.catalog().clone()) } pub(crate) fn full_dense_scheme( &self, ) -> Result, AkitaError> { - AkitaCommitmentScheme::::from_schedule_artifact(&self.full_dense) + AkitaCommitmentScheme::::from_schedule_artifact( + &self.full_dense, + JoltDenseFull::CHUNKING, + ) } pub fn one_hot_catalog( @@ -215,7 +227,7 @@ impl AkitaScheduleArtifacts { ) -> Result { let family = OneHotFamily::from_parts(one_hot_k, profile)?; with_one_hot_family!(family, |Cfg| { - TrustedScheduleCatalog::::from_artifact_bytes(family.artifact(self)) + TrustedScheduleCatalog::::from_artifact_bytes(family.artifact(self), Cfg::CHUNKING) .map(|catalog| catalog.catalog().clone()) }) } @@ -236,7 +248,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. @@ -733,7 +744,7 @@ impl AkitaVerifierSetup { .dense() .ok_or_else(|| "Akita verifier setup has no dense schedule artifact".to_string()) .and_then(|bytes| { - AkitaBackendScheme::from_schedule_artifact(bytes) + AkitaBackendScheme::from_schedule_artifact(bytes, AkitaConfig::CHUNKING) .map_err(|error| error.to_string()) }) }); @@ -841,28 +852,30 @@ 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 shape of `setup` under `flavor` and the identity of the +/// schedule catalog it admits. +/// +/// Akita's instance descriptor binds the setup seed and the selected row, but +/// not the setup bounds, the one-hot chunk profile, or the catalog the +/// verifier resolved that row in; binding them here keeps a proof from +/// replaying across verifier setups that differ in any of them. +pub(crate) fn absorb_setup_catalog( + channel: &mut C, setup: &AkitaVerifierSetup, flavor: AkitaBackendFlavor, ) -> Result<(), OpeningsError> { - let akita_chunk_profile = setup.akita_chunk_profile(); - 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)); - if flavor == AkitaBackendFlavor::OneHot && akita_chunk_profile != AkitaChunkProfile::Single { - transcript.append(&Label(b"akita_chunk_profile")); - transcript.append(&U64Word(akita_chunk_profile.num_chunks() as u64)); - } - transcript.append_bytes(&setup.default_layout_digest); + let word = |value: usize| u64::try_from(value).unwrap_or(u64::MAX).to_le_bytes(); + channel.public(&[flavor.wire_tag()]); + channel.public_all(&[ + word(setup.max_num_vars), + word(setup.max_num_polys_per_commitment_group), + word(setup.max_total_batch_polys), + word(setup.one_hot_k), + ]); + if flavor == AkitaBackendFlavor::OneHot { + channel.public(&word(setup.akita_chunk_profile().num_chunks())); + } + channel.public(&setup.default_layout_digest); let catalog_digest = match flavor { AkitaBackendFlavor::Dense => setup.dense_scheme()?.schedules().catalog_digest(), AkitaBackendFlavor::OneHot => { @@ -871,26 +884,26 @@ pub(crate) fn append_verifier_setup( .catalog_digest()) } }; - 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)] @@ -902,10 +915,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, } } } @@ -1020,79 +1041,179 @@ 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; + + fn words(&self) -> [usize; Self::WORDS] { + [ + self.num_vars, + self.poly_count, + self.one_hot_k, + self.backend_coeff_len, + ] } +} - pub(crate) fn selection(&self) -> OpeningScheduleSelection { - OpeningScheduleSelection { - row_digest: ScheduleRowDigest::from_bytes(self.schedule_selection), +impl CanonicalBytes for CommitmentHeader { + 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(size_of::()).zip(self.words()) { + (word as u64).to_bytes_le(out); } } +} + +impl Encoding<[u8]> for CommitmentHeader { + fn encode(&self) -> impl AsRef<[u8]> { + ::jolt_field::narg::encode(self) + } +} - /// Headerless backend proof body: Akita's Spongefish argument bytes. - pub fn backend_proof_body_size(&self) -> usize { - 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(size_of::()).map(|word| { + let word = u64::from_le_bytes(word.try_into().ok()?); + 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) } +} - /// 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 NargDeserialize for CommitmentHeader { + fn deserialize_from_narg(buf: &mut &[u8]) -> 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. #[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 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 NargDeserialize for AkitaHidingCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + ::jolt_field::narg::deserialize(buf) } } @@ -1368,26 +1489,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/bin/gen_jolt_schedules.rs b/crates/jolt-akita/src/bin/gen_jolt_schedules.rs index fb10dcce53..835ac698d7 100644 --- a/crates/jolt-akita/src/bin/gen_jolt_schedules.rs +++ b/crates/jolt-akita/src/bin/gen_jolt_schedules.rs @@ -15,7 +15,7 @@ use akita_planner::emit::{ publish_artifact_outputs, render_schedule_artifact_outputs_with_validation, MaterializationDiagnostics, }; -use jolt_akita::schedules::emit::family_specs; +use jolt_akita::schedules::emit::{family_specs, preplan_direct_rows}; #[expect( clippy::expect_used, @@ -57,7 +57,7 @@ fn main() { // The documented selectors are family-name infixes, except the explicit // `*-single` selectors that distinguish base one-hot catalogs from their // multi-chunk companions. - let specs = family_specs(generation_dir.clone()) + let mut specs = family_specs(&generation_dir) .expect("every family must declare a valid contract") .into_iter() .filter(|family| match only.as_deref() { @@ -68,6 +68,8 @@ fn main() { }) .collect::>(); assert!(!specs.is_empty(), "no schedule family matches {only:?}"); + preplan_direct_rows(&mut specs) + .expect("direct rows below the setup-offloading cutover must plan"); for family in &specs { println!( "generating {} ({} keys)…", diff --git a/crates/jolt-akita/src/configs.rs b/crates/jolt-akita/src/configs.rs index f866bf7e29..19e6df6418 100644 --- a/crates/jolt-akita/src/configs.rs +++ b/crates/jolt-akita/src/configs.rs @@ -66,17 +66,31 @@ macro_rules! delegate_preset { $committed_source_class } - fn chunked_witness_cfg() -> akita_params::ChunkedWitnessCfg { - $chunked_witness_cfg - } - fn recursive_setup_planning() -> bool { <$base>::recursive_setup_planning() } } + + impl CatalogChunking for $name { + const CHUNKING: ChunkedWitnessCfg = $chunked_witness_cfg; + } }; } +/// The witness chunking one Jolt catalog family is planned and admitted under. +/// +/// Akita takes chunking from its caller at catalog admission rather than from +/// the config; every Jolt family fixes its own, so this trait is its one owner. +pub trait CatalogChunking: CommitmentConfig { + const CHUNKING: ChunkedWitnessCfg; +} + +impl CatalogChunking + for RecursiveCommitmentConfig +{ + const CHUNKING: ChunkedWitnessCfg = Direct::CHUNKING; +} + delegate_preset!( /// Direct-planning policy used to generate the below-cutover K=16 rows. JoltOneHotK16Direct, diff --git a/crates/jolt-akita/src/lib.rs b/crates/jolt-akita/src/lib.rs index 0c92c38fb1..0bf6dafb8f 100644 --- a/crates/jolt-akita/src/lib.rs +++ b/crates/jolt-akita/src/lib.rs @@ -30,6 +30,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; @@ -41,9 +43,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 configs::AkitaChunkProfile; pub use native_batching::{ diff --git a/crates/jolt-akita/src/native_batching.rs b/crates/jolt-akita/src/native_batching.rs index 316aca4b8f..93f97d6232 100644 --- a/crates/jolt-akita/src/native_batching.rs +++ b/crates/jolt-akita/src/native_batching.rs @@ -12,26 +12,36 @@ //! 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_params::{BasisMode, OpeningScheduleSelection}; -use akita_pcs::{AkitaError, SelectedProverOpeningData}; +use akita_pcs::{ + batched_prove, AkitaCommitmentScheme, AkitaError, BackendRegistry, FixedFoldRoute, + SelectedProverOpeningData, +}; use akita_types::{GroupBatchStatement, OpeningClaims, PolynomialGroupClaims}; use jolt_openings::{ BatchOpeningScheme, GroupOpeningClaim, GroupOpeningWithHint, OpeningsError, 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, + absorb_setup_catalog, akita_error, invalid_batch, prove_failed, receive_selection, + reverse_point, send_selection, validate_one_hot_k, with_backend_pool, AkitaBackend, + AkitaBackendCommitment, AkitaBackendExtField, AkitaBackendFlavor, AkitaBackendHint, + AkitaBackendProverSetup, AkitaCommitment, AkitaConfig, AkitaField, AkitaHintSource, AkitaProverHint, AkitaProverSetup, AkitaVerifierSetup, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, }; use crate::one_hot_family::with_one_hot_family; @@ -149,88 +159,97 @@ fn validate_trace_batch_statement( Ok(()) } -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> +) -> 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 Akita's core prover for `opening` on the caller's `transcript`, with +/// every fold on `backend`: the embedding counterpart of +/// `AkitaCommitmentScheme::batched_prove`, which proves into a fresh +/// transcript instead. +fn prove_on_transcript( + scheme: &AkitaCommitmentScheme, + setup: &AkitaBackendProverSetup, + opening: AkitaOpening<'_>, + backend: &AkitaBackend, + transcript: &mut ProverTranscript, +) -> Result<(), AkitaError> where - T: Transcript, + Cfg: CommitmentConfig, + H: Sponge, { - append_verifier_setup(transcript, setup, AkitaBackendFlavor::OneHot)?; - transcript.append(&Label(b"akita_precommit_batch_v4")); - 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( + let resolved = scheme.schedules().resolve_selection(opening.selection())?; + let required_prefix_ids = akita_config::required_setup_prefix_slot_ids_for_schedule( + resolved.schedule(), + opening.opening_layout(), + )?; + let prefix_slots = backend.import_setup_prefixes(&setup.prefix_slots, &required_prefix_ids)?; + let mut registry = BackendRegistry::::new()?; + let root = registry.register(backend, &prefix_slots)?; + let mut route = FixedFoldRoute::new(vec![root; resolved.schedule().num_fold_levels()]); + batched_prove::( + setup.expanded.descriptor(), + scheme.schedules(), + ®istry, + opening, transcript, - b"jolt-akita/precommitted-group-batch/v4", - )) + BasisMode::Lagrange, + &mut route, + ) } -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(); let scheme = setup.verifier.one_hot_scheme()?; with_backend_pool(|| { - let proof = with_one_hot_family!(scheme scheme, |scheme| scheme.batched_prove( + with_one_hot_family!(scheme scheme, |scheme| prove_on_transcript( + scheme, backend_prover_setup, opening, backend, - session, - BasisMode::Lagrange, + transcript, ))?; + // 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) } -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 verifier = setup.one_hot_verifier()?; let verified = with_backend_pool(|| { with_one_hot_family!(verifier verifier, |verifier| verifier.batched_verify( - &proof.backend_proof, - session, + transcript, statement, BasisMode::Lagrange, )) @@ -239,16 +258,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()) @@ -330,29 +346,23 @@ impl AkitaNativeBatching { ) }) .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() @@ -363,17 +373,9 @@ impl AkitaNativeBatching { scheme.schedules(), &auxiliary_commitments, &main.commitment, - proof.selection(), + selection, ) })?; - 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( @@ -391,7 +393,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) } } @@ -517,28 +519,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")) -} - fn single_group_batch<'a, Cfg>( schedules: &TrustedScheduleCatalog, point: &[AkitaField], @@ -557,14 +537,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 = with_one_hot_family!(scheme setup.verifier.one_hot_scheme()?, |scheme, Cfg| { single_group_batch::( @@ -576,9 +555,7 @@ fn prove_one_hot( ) }) .map_err(akita_error)?; - let selection = opening.selection(); - let proof = prove_one_hot_opening(setup, opening, session)?; - Ok((selection, proof)) + Ok(opening) } impl BatchOpeningScheme for AkitaNativeBatching { @@ -591,18 +568,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, @@ -623,14 +598,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::( @@ -641,44 +613,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(|| { + prove_on_transcript( + scheme, backend_prover_setup, opening, backend, - &session, - BasisMode::Lagrange, + transcript, )?; 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, @@ -689,6 +663,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`). @@ -696,7 +671,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, ), @@ -705,7 +680,7 @@ impl BatchOpeningScheme for AkitaNativeBatching { crate::shape_guard::deserialize_checked_backend_payload( scheme.schedules(), commitment, - proof.selection(), + selection, statement.len(), &backend_point, ) @@ -713,8 +688,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) @@ -722,23 +695,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) } } } @@ -755,9 +722,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, @@ -804,15 +771,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/one_hot_family.rs b/crates/jolt-akita/src/one_hot_family.rs index 7f1fa18e11..4b3bc31585 100644 --- a/crates/jolt-akita/src/one_hot_family.rs +++ b/crates/jolt-akita/src/one_hot_family.rs @@ -3,7 +3,7 @@ use akita_pcs::{AkitaCommitmentScheme, AkitaError, AkitaVerifier}; use akita_types::AkitaVerifierSetup; use crate::adapters::AkitaScheduleArtifacts; -use crate::configs::{AkitaChunkProfile, JoltOneHotK16}; +use crate::configs::{AkitaChunkProfile, CatalogChunking, JoltOneHotK16}; use crate::family_contract::OneHotFamilyContract; pub use crate::family_contract::{AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256}; @@ -110,7 +110,7 @@ macro_rules! define_family_types { ) -> Result { match family { $(OneHotFamily::$variant => - AkitaCommitmentScheme::<$cfg>::from_schedule_artifact(bytes) + AkitaCommitmentScheme::<$cfg>::from_schedule_artifact(bytes, <$cfg>::CHUNKING) .map(Self::$variant)),+ } } diff --git a/crates/jolt-akita/src/planning.rs b/crates/jolt-akita/src/planning.rs index 2dea07462f..41d7503c17 100644 --- a/crates/jolt-akita/src/planning.rs +++ b/crates/jolt-akita/src/planning.rs @@ -1,12 +1,14 @@ //! Canonical schedule planning for Jolt's Akita configurations. -use akita_config::{policy_of, CommitmentConfig}; +use akita_config::policy_of; use akita_params::sis::CommittedSourceContract; use akita_params::{FoldSchedule, ScheduleLookupKey}; use akita_pcs::AkitaError; use akita_planner::find_schedule; -pub(crate) fn plan_schedule( +use crate::configs::CatalogChunking; + +pub(crate) fn plan_schedule( key: &ScheduleLookupKey, auxiliary_source_contracts: &[CommittedSourceContract], ) -> Result { @@ -14,7 +16,7 @@ pub(crate) fn plan_schedule( key, Cfg::committed_source_contract()?, auxiliary_source_contracts, - &policy_of::(), + &policy_of::(Cfg::CHUNKING), Cfg::ring_challenge_config, )?; planned.schedule.validate_structure()?; diff --git a/crates/jolt-akita/src/schedule_registry.rs b/crates/jolt-akita/src/schedule_registry.rs index 67da2056c1..6601f27fc5 100644 --- a/crates/jolt-akita/src/schedule_registry.rs +++ b/crates/jolt-akita/src/schedule_registry.rs @@ -22,7 +22,7 @@ use akita_planner::find_adapted_schedule; use akita_schedules::{ResolvedScheduleRow, ValidatedScheduleCatalog}; use serde::{Deserialize, Serialize}; -use crate::configs::{AkitaChunkProfile, JoltDenseBounded, JoltDenseFull}; +use crate::configs::{AkitaChunkProfile, CatalogChunking, JoltDenseBounded, JoltDenseFull}; use crate::one_hot_family::{with_one_hot_family, OneHotFamily, AKITA_ONE_HOT_K256}; const MAX_PROVISIONED_ROWS: usize = 128; @@ -147,14 +147,14 @@ impl RegisteredRows { } /// Freeze base and setup-specific rows into one validated immutable catalog. -pub fn extend_catalog( +pub fn extend_catalog( base: &ValidatedScheduleCatalog, extra: &RegisteredRows, ) -> Result { - akita_config::validate_config_policy::()?; + akita_config::validate_config_policy::(Cfg::CHUNKING)?; base.validate_binding( Cfg::schedule_family_name(), - &policy_of::(), + &policy_of::(Cfg::CHUNKING), Cfg::ring_challenge_config, )?; let rows = base @@ -165,12 +165,12 @@ pub fn extend_catalog( ValidatedScheduleCatalog::try_new( Cfg::schedule_family_name(), rows, - &policy_of::(), + &policy_of::(Cfg::CHUNKING), Cfg::ring_challenge_config, ) } -fn plan_row( +fn plan_row( base: &ValidatedScheduleCatalog, final_group: PolynomialGroupLayout, producers: &[PrecommittedProducer], @@ -200,7 +200,7 @@ fn plan_row( main_row, &request, Cfg::committed_source_contract()?, - &policy_of::(), + &policy_of::(Cfg::CHUNKING), Cfg::ring_challenge_config, )? .schedule; @@ -211,18 +211,18 @@ fn plan_row( )?, precommitteds: key.precommitteds, }; - ResolvedScheduleRow::try_new(profiles, schedule, &policy_of::()).map(Some) + ResolvedScheduleRow::try_new(profiles, schedule, &policy_of::(Cfg::CHUNKING)).map(Some) } -fn provision_producers( +fn provision_producers( base: &ValidatedScheduleCatalog, group_combinations: &[Vec], final_group: PolynomialGroupLayout, ) -> Result { - akita_config::validate_config_policy::()?; + akita_config::validate_config_policy::(Cfg::CHUNKING)?; base.validate_binding( Cfg::schedule_family_name(), - &policy_of::(), + &policy_of::(Cfg::CHUNKING), Cfg::ring_challenge_config, )?; if group_combinations.iter().any(Vec::is_empty) { @@ -306,7 +306,7 @@ impl AdvicePrecommitLayouts { pub const FIXTURE_TRUSTED_ADVICE_GROUP: PolynomialGroupLayout = PolynomialGroupLayout::new(14, 1); pub const FIXTURE_K16_FINAL_NUM_VARS: (usize, usize) = (22, 26); -fn provision_groups_for_config( +fn provision_groups_for_config( dense_catalog: &ValidatedScheduleCatalog, full_dense_catalog: &ValidatedScheduleCatalog, one_hot_catalog: &ValidatedScheduleCatalog, @@ -315,15 +315,15 @@ fn provision_groups_for_config( ) -> Result { let final_group = params.final_group; family.validate_num_vars(final_group.num_vars(), final_group.num_polynomials())?; - akita_config::validate_config_policy::()?; + akita_config::validate_config_policy::(JoltDenseBounded::CHUNKING)?; dense_catalog.validate_binding( JoltDenseBounded::schedule_family_name(), - &policy_of::(), + &policy_of::(JoltDenseBounded::CHUNKING), JoltDenseBounded::ring_challenge_config, )?; full_dense_catalog.validate_binding( JoltDenseFull::schedule_family_name(), - &policy_of::(), + &policy_of::(JoltDenseFull::CHUNKING), JoltDenseFull::ring_challenge_config, )?; let layouts = AdvicePrecommitLayouts { diff --git a/crates/jolt-akita/src/schedules/mod.rs b/crates/jolt-akita/src/schedules/mod.rs index e3dfa7d925..8b039d4d5f 100644 --- a/crates/jolt-akita/src/schedules/mod.rs +++ b/crates/jolt-akita/src/schedules/mod.rs @@ -12,20 +12,20 @@ /// Emit-spec construction shared by the generator and drift tests. pub mod emit { use std::collections::BTreeSet; - use std::path::PathBuf; + use std::path::Path; use akita_config::{policy_of, CommitmentConfig}; - use akita_params::{ - FoldSchedule, OpeningClaimsLayout, PolynomialGroupLayout, ScheduleLookupKey, - }; + use akita_params::{OpeningClaimsLayout, PolynomialGroupLayout, ScheduleLookupKey}; use akita_pcs::AkitaError; - use akita_planner::emit::GroupedGenerationRequest; + use akita_planner::emit::offline_planning_worker_count; use akita_planner::EmitSpec; use jolt_claims::protocols::jolt::lattice::strategy::MAX_ONE_HOT_TRACE_COLUMNS; use jolt_claims::protocols::jolt::lattice::{one_hot_trace_columns, OneHotTraceShape}; use jolt_claims::protocols::jolt::{JoltFormulaDimensions, JoltOneHotDimensions}; + use rayon::prelude::*; + use rayon::ThreadPoolBuilder; - use crate::configs::{AkitaChunkProfile, JoltDenseBounded, JoltDenseFull}; + use crate::configs::{AkitaChunkProfile, CatalogChunking, JoltDenseBounded, JoltDenseFull}; use crate::one_hot_family::{with_one_hot_family, OneHotFamily}; use crate::planning::plan_schedule; use crate::{AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256}; @@ -46,39 +46,6 @@ pub mod emit { pub const K16_COLUMN_VARIABLES: usize = 4; pub const K256_COLUMN_VARIABLES: usize = 8; - fn regen( - key: PolynomialGroupLayout, - ) -> Result { - plan_schedule::(&ScheduleLookupKey::single(key), &[]) - } - - fn regen_one_hot_k16( - key: PolynomialGroupLayout, - ) -> Result { - if key.num_vars() >= RECURSIVE_TRACE_LOG_T_CUTOVER + K16_COLUMN_VARIABLES { - regen::(key) - } else { - regen::(key) - } - } - - fn regen_one_hot_k256( - key: PolynomialGroupLayout, - ) -> Result { - if key.num_vars() >= RECURSIVE_TRACE_LOG_T_CUTOVER + K256_COLUMN_VARIABLES { - regen::(key) - } else { - regen::(key) - } - } - - fn reject_grouped(request: GroupedGenerationRequest) -> Result { - Err(AkitaError::InvalidSetup(format!( - "jolt base families emit no grouped rows; refusing to plan {:?}", - request.key() - ))) - } - /// Reachable scalar keys for one family grid. pub fn keys( num_polys: &[usize], @@ -199,22 +166,19 @@ pub mod emit { Ok(admitted) } - fn spec( + fn spec( family_name: &'static str, keys: Vec, - regen: fn(PolynomialGroupLayout) -> Result, - output_dir: PathBuf, + output_dir: &Path, ) -> Result { Ok(EmitSpec { family_name, - policy: policy_of::(), + policy: policy_of::(Cfg::CHUNKING), source_contract: Cfg::committed_source_contract()?, keys, grouped_requests: Vec::new(), - preplanned_scalar: Vec::new(), - output_dir, - regen, - regen_group_batch: reject_grouped, + preplanned: Vec::new(), + output_path: output_dir.join(format!("{family_name}.aks")), ring_challenge_config: Cfg::ring_challenge_config, }) } @@ -223,21 +187,17 @@ pub mod emit { /// /// Instance-specific grouped advice/program rows are planned during setup /// and folded into the exact catalog serialized with that verifier setup. - pub fn family_specs(output_dir: PathBuf) -> Result, AkitaError> { + /// The emitter plans every key under the family's own policy; call + /// [`preplan_direct_rows`] first to plan the rows below a one-hot family's + /// setup-offloading cutover with its direct config. + pub fn family_specs(output_dir: &Path) -> Result, AkitaError> { let mut specs = Vec::with_capacity(OneHotFamily::ALL.len() + 2); for family in OneHotFamily::ALL.iter().copied() { - specs.push(with_one_hot_family!(family, |Cfg, DirectCfg| { - let regen: fn(PolynomialGroupLayout) -> Result = - if family.k() == AKITA_ONE_HOT_K16 { - regen_one_hot_k16:: - } else { - regen_one_hot_k256:: - }; + specs.push(with_one_hot_family!(family, |Cfg| { spec::( family.family_name(), one_hot_keys(family.k(), family.profile())?, - regen, - output_dir.clone(), + output_dir, ) })?); } @@ -245,16 +205,55 @@ pub mod emit { spec::( JoltDenseBounded::schedule_family_name(), keys(&[1, 2], DENSE_NUM_VARS), - regen::, - output_dir.clone(), + output_dir, )?, spec::( JoltDenseFull::schedule_family_name(), keys(&[1], DENSE_NUM_VARS), - regen::, output_dir, )?, ]); Ok(specs) } + + /// Plans, with each one-hot family's direct config, the keys below its + /// first setup-offloading trace exponent, as preplanned rows of `specs`. + pub fn preplan_direct_rows(specs: &mut [EmitSpec]) -> Result<(), AkitaError> { + for family in OneHotFamily::ALL.iter().copied() { + let Some(spec) = specs + .iter_mut() + .find(|spec| spec.family_name == family.family_name()) + else { + continue; + }; + let column_variables = if family.k() == AKITA_ONE_HOT_K16 { + K16_COLUMN_VARIABLES + } else { + K256_COLUMN_VARIABLES + }; + let direct_keys = spec + .keys + .iter() + .copied() + .filter(|key| key.num_vars() < RECURSIVE_TRACE_LOG_T_CUTOVER + column_variables) + .map(ScheduleLookupKey::single) + .collect::>(); + let pool = ThreadPoolBuilder::new() + .num_threads(offline_planning_worker_count(direct_keys.len())) + .build() + .map_err(|error| AkitaError::InvalidSetup(error.to_string()))?; + let planned = pool.install(|| { + with_one_hot_family!(family, |_Cfg, DirectCfg| { + direct_keys + .into_par_iter() + .map(|key| { + plan_schedule::(&key, &[]).map(|schedule| (key, schedule)) + }) + .collect::, AkitaError>>() + }) + })?; + spec.preplanned.extend(planned); + } + Ok(()) + } } diff --git a/crates/jolt-akita/src/scheme.rs b/crates/jolt-akita/src/scheme.rs index 0a30d6f5a9..f0f79af3c5 100644 --- a/crates/jolt-akita/src/scheme.rs +++ b/crates/jolt-akita/src/scheme.rs @@ -2,14 +2,13 @@ use akita_params::{PolynomialGroupLayout, PrecommittedGroupProfiles}; use akita_pcs::custom_source::RootPolyMeta; use akita_pcs::{AkitaError, CommitOutput, CpuBackend, GroupContext, SourceHandle}; 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,10 +20,10 @@ 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_SOURCE_RING_DIMENSION, + AkitaBackendHint, AkitaBackendOneHotPoly, AkitaCommitment, AkitaField, AkitaHidingCommitment, + AkitaHintSource, AkitaLayoutDigest, AkitaProverHint, AkitaProverSetup, AkitaScheduleArtifacts, + AkitaSetupFlavor, AkitaSetupParams, AkitaVerifierScheduleArtifacts, AkitaVerifierSetup, + BackendVerifierCache, FullWidthBackendSetup, AKITA_SOURCE_RING_DIMENSION, }; use crate::configs::AkitaChunkProfile; use crate::native_batching::{AkitaNativeBatchPolynomials, AkitaNativeBatching}; @@ -419,7 +418,6 @@ impl Commitment for AkitaScheme { impl CommitmentScheme for AkitaScheme { type Field = AkitaField; - type Proof = AkitaBatchProof; type ProverSetup = AkitaProverSetup; type VerifierSetup = AkitaVerifierSetup; type OpeningHint = AkitaProverHint; @@ -621,14 +619,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, @@ -648,21 +661,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 @@ -713,13 +723,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, @@ -729,20 +739,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) } } @@ -831,28 +834,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()) } @@ -863,32 +861,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()) } } @@ -902,13 +895,16 @@ mod tests { use super::*; use crate::adapters::{ - append_verifier_setup, AkitaBackendFlavor, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, + absorb_setup_catalog, AkitaBackendFlavor, AKITA_ONE_HOT_K16, AKITA_ONE_HOT_K256, + }; + use crate::configs::{CatalogChunking, JoltDenseBounded}; + use crate::test_transcripts::{ + assert_transcripts_agree, new_prover_transcript, new_verifier_transcript, }; - use crate::configs::JoltDenseBounded; use akita_config::{policy_of, CommitmentConfig}; + use akita_params::{OpeningScheduleSelection, ScheduleRowDigest}; use akita_schedules::ValidatedScheduleCatalog; use jolt_field::Ring; - use jolt_transcript::Blake2bTranscript; #[test] fn full_width_objects_do_not_widen_bounded_dense_commitments() { @@ -930,72 +926,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(); @@ -1057,8 +987,8 @@ mod tests { evaluation: EvaluationClaim::new(point.clone(), polynomial.evaluate(&point)), }) .collect::>(); - 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(), polynomials @@ -1069,15 +999,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); if num_vars == 16 && num_polys == 1 { let one_hot = match &verifier_setup.schedule_artifacts { @@ -1109,11 +1039,10 @@ mod tests { // Transport drops the honest setup's primed backend caches. let bytes = serde_json::to_vec(&tampered).unwrap(); let transported: AkitaVerifierSetup = serde_json::from_slice(&bytes).unwrap(); - let mut transcript = Blake2bTranscript::::new(b"akita-one-hot-k"); + let mut transcript = new_verifier_transcript(b"akita-one-hot-k", &proof); let error = ::verify_batch( &transported, &statement, - &proof, &mut transcript, ) .expect_err("a verifier profile must match its embedded catalog"); @@ -1127,11 +1056,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"); @@ -1275,20 +1203,16 @@ mod tests { ); assert_eq!(legacy.akita_chunk_profile(), AkitaChunkProfile::Single); - let mut current_transcript = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup(&mut current_transcript, &setup, AkitaBackendFlavor::OneHot).unwrap(); - let mut legacy_transcript = Blake2bTranscript::::new(b"akita-setup-key-test"); - append_verifier_setup(&mut legacy_transcript, &legacy, AkitaBackendFlavor::OneHot).unwrap(); - // This fixture binds the entire K16 catalog, so catalog changes alter it - // even when the selected row and single-chunk transcript encoding are unchanged. + // The legacy encoding binds the same setup shape and catalog as the + // current one. + let mut current_transcript = new_prover_transcript(b"akita-setup-key-test"); + absorb_setup_catalog(&mut current_transcript, &setup, AkitaBackendFlavor::OneHot).unwrap(); + let mut legacy_transcript = new_prover_transcript(b"akita-setup-key-test"); + absorb_setup_catalog(&mut legacy_transcript, &legacy, AkitaBackendFlavor::OneHot).unwrap(); assert_eq!( - legacy_transcript.state(), - [ - 205, 2, 243, 247, 235, 52, 107, 127, 77, 82, 189, 184, 174, 107, 52, 230, 100, 169, - 34, 174, 75, 3, 75, 147, 196, 211, 62, 205, 25, 116, 31, 213, - ] + current_transcript.challenge_bytes::<32>(), + legacy_transcript.challenge_bytes::<32>() ); - assert_eq!(current_transcript.state(), legacy_transcript.state()); } #[test] @@ -1385,8 +1309,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], @@ -1394,8 +1318,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() @@ -1408,7 +1336,7 @@ mod tests { .rows() .filter(|row| row.selection() != omitted) .map(|row| (row.profiles().clone(), row.schedule().clone())), - &policy_of::(), + &policy_of::(JoltDenseBounded::CHUNKING), JoltDenseBounded::ring_challenge_config, ) .unwrap(); @@ -1442,20 +1370,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(); @@ -1487,8 +1413,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], @@ -1496,37 +1425,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 ab8af7f185..98f4fe4b13 100644 --- a/crates/jolt-akita/src/shape_guard.rs +++ b/crates/jolt-akita/src/shape_guard.rs @@ -156,6 +156,7 @@ mod tests { )] use super::*; + use crate::configs::CatalogChunking; use akita_config::TrustedScheduleCatalog; use akita_params::ScheduleLookupKey; use jolt_field::Ring; @@ -185,8 +186,11 @@ mod tests { let artifacts = AkitaScheduleArtifacts::from_directory(AkitaScheduleArtifacts::packaged_directory()) .expect("workspace schedule artifacts"); - TrustedScheduleCatalog::new(artifacts.dense_catalog().expect("dense schedule catalog")) - .expect("typed dense schedule catalog") + TrustedScheduleCatalog::new( + artifacts.dense_catalog().expect("dense schedule catalog"), + AkitaConfig::CHUNKING, + ) + .expect("typed dense schedule catalog") } fn test_selection( 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/external_catalog.rs b/crates/jolt-akita/tests/external_catalog.rs index dcf58624d1..ef87de38ec 100644 --- a/crates/jolt-akita/tests/external_catalog.rs +++ b/crates/jolt-akita/tests/external_catalog.rs @@ -11,14 +11,14 @@ use akita_params::{ }; use akita_pcs::AkitaError; use akita_schedules::ValidatedScheduleCatalog; +use jolt_akita::configs::CatalogChunking; use jolt_akita::configs::{JoltOneHotK16W2R2, JoltOneHotK16W2R2Direct}; use jolt_akita::{ - AkitaBatchProof, AkitaChunkProfile, AkitaCommitment, AkitaField, AkitaNativeBatching, - AkitaScheduleArtifacts, AkitaScheme, AkitaSetupParams, AkitaVerifierSetup, + AkitaChunkProfile, AkitaCommitment, AkitaField, AkitaNativeBatching, AkitaScheduleArtifacts, + AkitaScheme, AkitaSetupParams, AkitaVerifierSetup, }; use jolt_openings::{BatchOpeningScheme, CommitmentScheme}; use jolt_poly::{MultilinearPoly, OneHotPolynomial}; -use jolt_transcript::{Blake2bTranscript, Transcript}; #[expect( dead_code, @@ -40,19 +40,17 @@ fn external_multichunk_catalog_survives_transport() { AkitaCommitment, Vec, AkitaField, - AkitaBatchProof, + Vec, ) = serde_json::from_slice(&fs::read(path).expect("read public verification inputs")) .expect("restore verifier without source catalogs"); let mut statement = support::single_statement(commitment, &point, evaluation); assert_eq!(setup.akita_chunk_profile(), AkitaChunkProfile::Two); - let mut transcript = Blake2bTranscript::new(DOMAIN); - AkitaNativeBatching::verify_batch(&setup, &statement, &proof, &mut transcript) + let mut transcript = support::new_verifier_transcript(DOMAIN, &proof); + AkitaNativeBatching::verify_batch(&setup, &statement, &mut transcript) .expect("upstream verifier accepts the transported multi-chunk proof"); statement[0].evaluation.value += support::f(1); - let mut transcript = Blake2bTranscript::new(DOMAIN); - assert!( - AkitaNativeBatching::verify_batch(&setup, &statement, &proof, &mut transcript).is_err() - ); + let mut transcript = support::new_verifier_transcript(DOMAIN, &proof); + assert!(AkitaNativeBatching::verify_batch(&setup, &statement, &mut transcript).is_err()); return; } @@ -80,7 +78,7 @@ fn external_multichunk_catalog_survives_transport() { &key, JoltOneHotK16W2R2Direct::committed_source_contract().expect("direct source contract"), &[], - &policy_of::(), + &policy_of::(JoltOneHotK16W2R2Direct::CHUNKING), JoltOneHotK16W2R2Direct::ring_challenge_config, ) .expect("plan external-only shape") @@ -96,7 +94,7 @@ fn external_multichunk_catalog_survives_transport() { let external = ValidatedScheduleCatalog::try_new( JoltOneHotK16W2R2::schedule_family_name(), vec![(profile, schedule)], - &policy_of::(), + &policy_of::(JoltOneHotK16W2R2::CHUNKING), JoltOneHotK16W2R2::ring_challenge_config, ) .expect("admit external-only catalog"); @@ -129,8 +127,8 @@ fn external_multichunk_catalog_survives_transport() { AkitaScheme::commit_one_hot_group(&prover, support::layout(3), &polynomials) .expect("commit one-hot source"); let statement = support::single_statement(commitment.clone(), &point, evaluation); - let mut transcript = Blake2bTranscript::new(DOMAIN); - let proof = AkitaNativeBatching::prove_batch( + let mut transcript = support::new_prover_transcript(DOMAIN); + AkitaNativeBatching::prove_batch( &prover, statement.clone(), polynomials @@ -141,6 +139,7 @@ fn external_multichunk_catalog_survives_transport() { &mut transcript, ) .expect("produce multi-chunk proof"); + let proof = transcript.narg().to_vec(); let inputs = serde_json::to_vec(&(&verifier, &commitment, &point, evaluation, &proof)) .expect("export public verification inputs"); drop(prover); diff --git a/crates/jolt-akita/tests/fold_paths.rs b/crates/jolt-akita/tests/fold_paths.rs index cbae77a9f7..a36e1dca90 100644 --- a/crates/jolt-akita/tests/fold_paths.rs +++ b/crates/jolt-akita/tests/fold_paths.rs @@ -11,33 +11,35 @@ mod support; use akita_params::{PolynomialGroupLayout, ScheduleLookupKey}; -use jolt_akita::{ - AkitaBatchProof, AkitaCommitment, AkitaField, AkitaScheduleArtifacts, AkitaScheme, -}; +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()?) } } @@ -49,8 +51,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, @@ -59,18 +61,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, @@ -103,13 +105,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, ) @@ -118,42 +119,23 @@ fn deep_recursive_fold_schedule_roundtrips() { ); } +/// 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 de846106de..c9754f203e 100644 --- a/crates/jolt-akita/tests/grouped_capacity.rs +++ b/crates/jolt-akita/tests/grouped_capacity.rs @@ -22,8 +22,8 @@ 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::{f, layout, new_prover_transcript, new_verifier_transcript, polynomial}; const FINAL_NUM_VARS: usize = 16; /// Six variables above this one-column trace group. @@ -105,8 +105,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(), @@ -114,31 +114,38 @@ 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(); + // 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"), ) .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_eq!( + verifier_transcript.challenge_bytes::<32>(), + prover_checkpoint + ); + verifier_transcript + .finish() + .expect("the verifier consumes the whole proof"); } - 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 d1db489ba8..6d116ce711 100644 --- a/crates/jolt-akita/tests/native_batching.rs +++ b/crates/jolt-akita/tests/native_batching.rs @@ -14,10 +14,9 @@ use jolt_openings::{ VerifierOpeningClaim, }; use jolt_poly::{MultilinearPoly, OneHotPolynomial}; -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, }; struct TraceRows { @@ -86,8 +85,8 @@ fn akita_streamed_trace_batch_rejects_swapped_evaluations() { ) .expect("streamed trace group should commit"); let mut statement = GroupOpeningClaim::new(commitment, point, evaluations); - let mut prover_transcript = Blake2bTranscript::new(b"akita-streamed-tamper"); - let proof = AkitaScheme::prove_batch( + let mut prover_transcript = new_prover_transcript(b"akita-streamed-tamper"); + AkitaScheme::prove_batch( &prover_setup, Vec::new(), statement.clone(), @@ -95,27 +94,18 @@ fn akita_streamed_trace_batch_rejects_swapped_evaluations() { &mut prover_transcript, ) .expect("streamed trace batch should prove"); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-streamed-tamper"); - AkitaScheme::verify_batch( - &verifier_setup, - &[], - &statement, - &proof, - &mut verifier_transcript, - ) - .expect("original streamed trace evaluations must verify"); - assert_eq!(prover_transcript.state(), verifier_transcript.state()); + let proof = prover_transcript.narg().to_vec(); + let mut verifier_transcript = new_verifier_transcript(b"akita-streamed-tamper", &proof); + AkitaScheme::verify_batch(&verifier_setup, &[], &statement, &mut verifier_transcript) + .expect("original streamed trace evaluations must verify"); + assert_transcripts_agree(prover_transcript, verifier_transcript); statement.evaluations.swap(0, 1); - let mut verifier_transcript = Blake2bTranscript::new(b"akita-streamed-tamper"); - assert!(AkitaScheme::verify_batch( - &verifier_setup, - &[], - &statement, - &proof, - &mut verifier_transcript, - ) - .is_err()); + let mut verifier_transcript = new_verifier_transcript(b"akita-streamed-tamper", &proof); + assert!( + AkitaScheme::verify_batch(&verifier_setup, &[], &statement, &mut verifier_transcript) + .is_err() + ); } #[test] @@ -131,8 +121,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]), @@ -140,16 +130,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] @@ -167,7 +157,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, @@ -189,7 +179,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, @@ -205,7 +195,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, @@ -218,7 +208,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, @@ -250,7 +240,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( @@ -265,7 +255,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, @@ -278,7 +268,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, @@ -304,8 +294,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]), @@ -313,12 +303,12 @@ 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 verifier_transcript = Blake2bTranscript::new(b"akita-bb-tamper"); + let mut verifier_transcript = new_verifier_transcript(b"akita-bb-tamper", &proof); ::verify_batch( &verifier_setup, &statement, - &proof, &mut verifier_transcript, ) .expect("original ordered evaluations must verify"); @@ -344,21 +334,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()); @@ -385,7 +371,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, @@ -410,8 +396,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]), @@ -419,14 +405,14 @@ fn akita_native_batching_rejects_statements_outside_the_verifier_setup() { &mut transcript, ) .expect("proof should be produced"); + let small_proof = transcript.narg().to_vec(); 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", @@ -447,8 +433,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]), @@ -456,13 +442,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", @@ -479,8 +465,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]), @@ -488,6 +474,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 @@ -497,12 +484,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", @@ -540,7 +526,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 3adef89437..c86bd7f409 100644 --- a/crates/jolt-akita/tests/one_hot_groups.rs +++ b/crates/jolt-akita/tests/one_hot_groups.rs @@ -12,8 +12,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). @@ -91,8 +93,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(), @@ -100,32 +102,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 dbda70ab95..c1d0036e0f 100644 --- a/crates/jolt-akita/tests/pathologies.rs +++ b/crates/jolt-akita/tests/pathologies.rs @@ -10,16 +10,19 @@ pub mod support; use jolt_akita::{ - AkitaBackendFlavor, AkitaBatchProof, AkitaCommitment, AkitaNativeBatchStatement, - AkitaNativeBatching, AkitaScheme, + AkitaBackendFlavor, AkitaCommitment, AkitaNativeBatchStatement, AkitaNativeBatching, + AkitaScheme, }; 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; @@ -83,8 +86,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, @@ -93,30 +96,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"); @@ -149,11 +145,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"); @@ -167,22 +162,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(); @@ -191,8 +238,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, @@ -201,18 +248,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, @@ -225,13 +272,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, ), ); @@ -239,7 +285,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); @@ -251,8 +297,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]), @@ -260,27 +306,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 9e55712b01..d94c3b6125 100644 --- a/crates/jolt-akita/tests/schedules.rs +++ b/crates/jolt-akita/tests/schedules.rs @@ -5,7 +5,7 @@ use jolt_akita::schedule_registry::GroupedScheduleParams; use std::collections::BTreeSet; -use std::path::PathBuf; +use std::path::Path; use akita_config::{policy_of, CommitmentConfig, SetupRequirements, TrustedScheduleCatalog}; use akita_params::{ @@ -15,8 +15,9 @@ use akita_params::{ }; use akita_schedules::{ResolvedScheduleRow, ValidatedScheduleCatalog}; use jolt_akita::configs::{ - JoltDenseBounded, JoltDenseFull, JoltOneHotK16, JoltOneHotK16W2R2, JoltOneHotK16W4R2, - JoltOneHotK16W8R2, JoltOneHotK256, JoltOneHotK256W2R2, JoltOneHotK256W4R2, JoltOneHotK256W8R2, + CatalogChunking, JoltDenseBounded, JoltDenseFull, JoltOneHotK16, JoltOneHotK16W2R2, + JoltOneHotK16W4R2, JoltOneHotK16W8R2, JoltOneHotK256, JoltOneHotK256W2R2, JoltOneHotK256W4R2, + JoltOneHotK256W8R2, }; use jolt_akita::schedule_registry::{ dense_group_profile, FIXTURE_K16_FINAL_NUM_VARS, FIXTURE_TRUSTED_ADVICE_GROUP, @@ -56,8 +57,14 @@ fn full_dense_catalog() -> ValidatedScheduleCatalog { #[test] fn dense_producer_certificates_cover_every_supported_chunk_count() { for (catalog, policy) in [ - (dense_catalog(), policy_of::()), - (full_dense_catalog(), policy_of::()), + ( + dense_catalog(), + policy_of::(JoltDenseBounded::CHUNKING), + ), + ( + full_dense_catalog(), + policy_of::(JoltDenseFull::CHUNKING), + ), ] { for row in catalog.rows() { let root = &row.schedule().root.params; @@ -497,7 +504,8 @@ fn grouped_setup_capacity_covers_precommit_and_complete_schedule() { ) .expect("precommit capacity"); let trusted_catalog = - TrustedScheduleCatalog::::new(setup_catalog).expect("config-bound catalog"); + TrustedScheduleCatalog::::new(setup_catalog, JoltOneHotK256::CHUNKING) + .expect("config-bound catalog"); let setup_capacity = SetupRequirements::from_catalog(&trusted_catalog, 38, 28) .expect("catalog-backed setup capacity") .matrix_capacity(); @@ -595,7 +603,7 @@ fn grouped_provisioning_rejects_out_of_family_final_arity() { /// compares complete artifact contents against the pinned backend. #[test] fn catalogs_have_exact_emitted_keys() { - let specs = family_specs(PathBuf::new()).expect("emit specs"); + let specs = family_specs(Path::new("")).expect("emit specs"); let cases = [ ( "jolt-fp128-onehot-k16", diff --git a/crates/jolt-akita/tests/scheme_surface.rs b/crates/jolt-akita/tests/scheme_surface.rs index 0b52d0fb65..60f519460e 100644 --- a/crates/jolt-akita/tests/scheme_surface.rs +++ b/crates/jolt-akita/tests/scheme_surface.rs @@ -7,13 +7,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; @@ -90,8 +93,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, @@ -100,23 +103,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); } #[test] @@ -127,8 +130,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, @@ -137,30 +140,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 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, @@ -169,19 +174,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, @@ -190,11 +195,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-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-blindfold/src/error.rs b/crates/jolt-blindfold/src/error.rs index dd83d02718..2dfb11fd14 100644 --- a/crates/jolt-blindfold/src/error.rs +++ b/crates/jolt-blindfold/src/error.rs @@ -3,6 +3,7 @@ use jolt_crypto::VectorOpeningError; use jolt_field::JoltField; use jolt_r1cs::ConstraintMatrixEvalError; use jolt_sumcheck::{SumcheckError, SumcheckR1csError}; +use jolt_transcript::TranscriptError; use thiserror::Error as ThisError; #[derive(Clone, Debug, ThisError, PartialEq, Eq)] @@ -155,6 +156,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/protocol.rs b/crates/jolt-blindfold/src/protocol.rs index 4681a1dcb8..191a48c4d8 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, - 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,13 +767,6 @@ 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") } @@ -793,18 +778,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)) @@ -825,18 +810,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)) @@ -853,31 +838,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)) @@ -896,26 +871,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)) @@ -930,18 +899,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() @@ -962,11 +931,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, @@ -998,11 +967,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), @@ -1047,8 +1016,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(&[]) @@ -1068,8 +1037,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 = @@ -1196,8 +1165,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/prove.rs b/crates/jolt-blindfold/src/prove.rs index 97616ae9ae..d50bb4d31a 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, FoldedEvaluations, FoldingCommitments, OuterClaims}; +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,11 +456,19 @@ where "folded eval blindings", )?; - let mut folded_eval_output_openings = Vec::new(); - let mut folded_eval_blinding_openings = Vec::new(); + 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 { @@ -504,15 +488,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 +507,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 +518,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 +547,15 @@ 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, - ); + OuterClaims { + abc: [az_rx, bz_rx, cz_rx], + error_opening, + } + .send(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 +591,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 +753,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 +788,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 +834,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 +843,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 +860,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 +875,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 +904,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 +1026,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 157902fb98..3036efb2b1 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, FoldedEvaluations, FoldingCommitments, OuterClaims}; 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,27 @@ 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 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( 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 +122,34 @@ 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 FoldedEvaluations { + outputs: folded_eval_outputs, + blindings: folded_eval_blindings, + } = FoldedEvaluations::receive(eval_count, transcript)?; 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( @@ -607,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) @@ -640,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))]) } @@ -748,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 { @@ -761,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], @@ -781,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: FoldedEvaluations, + outer_rounds: Vec>, + outer: OuterClaims, + 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: 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)], + 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), + } } - } - 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.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); + self.folded.send(&mut transcript); + for round in &self.outer_rounds { + transcript.send_all(round); + } + self.outer.send(&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, @@ -898,77 +672,40 @@ mod tests { /// 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"); } @@ -976,12 +713,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.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!( @@ -990,57 +725,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.outer.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!( @@ -1053,13 +745,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.outer.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!( @@ -1068,39 +757,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!( @@ -1113,20 +777,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/src/wire.rs b/crates/jolt-blindfold/src/wire.rs new file mode 100644 index 0000000000..e1bb6581f5 --- /dev/null +++ b/crates/jolt-blindfold/src/wire.rs @@ -0,0 +1,130 @@ +//! 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 +//! 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()?, + }) +} + +/// 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-blindfold/tests/assignment_pipeline.rs b/crates/jolt-blindfold/tests/assignment_pipeline.rs index 88b07741f7..182e58253d 100644 --- a/crates/jolt-blindfold/tests/assignment_pipeline.rs +++ b/crates/jolt-blindfold/tests/assignment_pipeline.rs @@ -1,209 +1,43 @@ -#![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, ]; -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); @@ -212,89 +46,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 { @@ -307,17 +91,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!( @@ -333,16 +114,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!( @@ -358,22 +136,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!( @@ -394,34 +169,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; @@ -432,8 +185,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]), @@ -441,8 +194,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 0767ee5348..90f26935d6 100644 --- a/crates/jolt-blindfold/tests/r1cs_pipeline.rs +++ b/crates/jolt-blindfold/tests/r1cs_pipeline.rs @@ -9,7 +9,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 56199668f3..945ab33fcf 100644 --- a/crates/jolt-blindfold/tests/real_prover.rs +++ b/crates/jolt-blindfold/tests/real_prover.rs @@ -1,65 +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"); + 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] @@ -68,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, @@ -87,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}" @@ -98,12 +90,77 @@ 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 }), "expected eval-commitment mismatch at index 0, got: {err}" ); } + +/// 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 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() + ); + } +} + +#[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 d609aee43d..2da6bdad90 100644 --- a/crates/jolt-blindfold/tests/support/mod.rs +++ b/crates/jolt-blindfold/tests/support/mod.rs @@ -5,8 +5,9 @@ )] use jolt_blindfold::{ - BlindFoldProof, BlindFoldProtocol, BlindFoldStage, BlindFoldStatement, CommittedClaimRows, - FinalOpeningBinding, WitnessCoordinate, + prove, BlindFoldProtocol, BlindFoldStage, BlindFoldStatement, BlindFoldWitness, + CommittedClaimRows, FinalOpeningBinding, ProverError, RowDimensions, VerificationError, + WitnessCoordinate, }; use jolt_claims::r1cs::ClaimSourceTable; use jolt_claims::{challenge, constant, derived, opening, Expr}; @@ -14,20 +15,31 @@ 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::{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, @@ -50,14 +62,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, } @@ -152,37 +165,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) { @@ -339,27 +334,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); @@ -384,138 +358,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>, @@ -530,24 +452,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(), @@ -565,11 +482,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) @@ -673,12 +592,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(); @@ -713,7 +627,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, @@ -749,172 +663,458 @@ 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()?) + } } -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, 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, + stage_seed: [u8; 32], + stage2_input: Stage2Input, + 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 { + /// 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 build_protocol_backed_instance(rng: &mut R) -> ProtocolBackedInstance { - build_protocol_backed_instance_with_bindings(rng, 1) + pub fn stage_witnesses(&self) -> Vec<&CommittedSumcheckWitness> { + self.stages.iter().map(|stage| &stage.witness).collect() + } + + /// 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(); + 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( +/// 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) +} + +/// 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 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 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 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 - .iter() - .map(|_| rng_field(rng)) - .collect::>(); - let eval_commitments = real_eval_outputs + let eval_blindings: Vec = eval_outputs.iter().map(|_| rng_field(rng)).collect(); + let eval_commitments: Vec<_> = 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, + stage_seed, + stage2_input, + 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(); 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> @@ -950,27 +1150,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, @@ -1007,6 +1186,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()); @@ -1043,7 +1223,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)); @@ -1077,12 +1257,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)); @@ -1100,13 +1280,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, @@ -1237,34 +1422,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); @@ -1298,69 +1464,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, @@ -1380,17 +1519,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); @@ -1399,24 +1533,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)); @@ -1429,27 +1556,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 { @@ -1485,40 +1592,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] } @@ -1706,13 +1779,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; @@ -1736,19 +1807,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 { diff --git a/crates/jolt-claims-derive/src/lib.rs b/crates/jolt-claims-derive/src/lib.rs index a900a516ba..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 @@ -501,6 +501,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 { @@ -513,6 +516,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 { @@ -534,6 +545,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 { @@ -545,6 +559,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); @@ -617,6 +634,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)* + }) + } + } + impl #name<::std::vec::Vec> { #from_shared_point #(#point_accessors)* @@ -766,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/fuzz/Cargo.lock b/crates/jolt-claims/fuzz/Cargo.lock index 4626bd5eb2..c685395cb4 100644 --- a/crates/jolt-claims/fuzz/Cargo.lock +++ b/crates/jolt-claims/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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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", ] @@ -454,13 +490,13 @@ 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", + "spongefish", ] [[package]] @@ -472,8 +508,9 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -505,10 +542,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", @@ -539,6 +575,10 @@ name = "jolt-transcript" version = "0.1.0" dependencies = [ "jolt-field", + "rand 0.8.7", + "rand_core 0.6.4", + "spongefish", + "thiserror", ] [[package]] @@ -548,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" @@ -564,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" @@ -574,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" @@ -598,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" @@ -649,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]] @@ -660,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]] @@ -668,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" @@ -698,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" @@ -734,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" @@ -849,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" diff --git a/crates/jolt-claims/src/claim_data.rs b/crates/jolt-claims/src/claim_data.rs index 8be28d757c..dbb5bf9fb0 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. @@ -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). /// @@ -259,3 +279,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 02f8596a9d..05cf26064c 100644 --- a/crates/jolt-claims/src/lib.rs +++ b/crates/jolt-claims/src/lib.rs @@ -35,8 +35,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)] diff --git a/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs b/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs index 0af4c37864..103c01a88e 100644 --- a/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs +++ b/crates/jolt-claims/src/protocols/jolt/geometry/bytecode.rs @@ -24,7 +24,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/geometry/dimensions.rs b/crates/jolt-claims/src/protocols/jolt/geometry/dimensions.rs index e9f9c14617..8415b382f3 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-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-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs index c12ec4701c..14e1b3ac05 100644 --- a/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs +++ b/crates/jolt-claims/src/protocols/jolt/relations/claim_reductions/bytecode/cycle_phase.rs @@ -12,8 +12,8 @@ use crate::protocols::jolt::{ JoltRelationId, PrecommittedReductionDimensions, }; use crate::{ - challenge, opening, InputClaims, MissingOpeningValue, OutputClaims, SumcheckChallenges, - SymbolicSumcheck, + challenge, opening, InputClaims, MapCells, MissingOpeningValue, OutputClaims, + SumcheckChallenges, SymbolicSumcheck, }; #[cfg_attr(feature = "allocative", derive(::allocative::Allocative))] @@ -82,6 +82,20 @@ impl OutputClaims for BytecodeReductionCyclePhaseOutputClaims MapCells for BytecodeReductionCyclePhaseOutputClaims { + type Mapped = BytecodeReductionCyclePhaseOutputClaims; + + fn try_map_cells( + &self, + f: &mut impl FnMut(&JoltOpeningId, &A) -> Result, + ) -> Result { + Ok(match self { + Self::Intermediate(claims) => Self::Mapped::Intermediate(claims.try_map_cells(f)?), + Self::Final(claims) => Self::Mapped::Final(claims.try_map_cells(f)?), + }) + } +} + /// The consumed staged `BytecodeValClaim` openings from the bytecode read-RAF /// address phase. #[derive(Clone, Debug, Default, PartialEq, Eq, InputClaims)] diff --git a/crates/jolt-crypto/Cargo.toml b/crates/jolt-crypto/Cargo.toml index d773fb9a1b..cc2e1ad2a9 100644 --- a/crates/jolt-crypto/Cargo.toml +++ b/crates/jolt-crypto/Cargo.toml @@ -34,9 +34,9 @@ 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 } -jolt-transcript = { path = "../jolt-transcript", default-features = false } serde = { workspace = true, features = ["derive", "alloc"] } rand_core = { workspace = true } @@ -57,10 +57,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.lock b/crates/jolt-crypto/fuzz/Cargo.lock index 23bc4c85ae..4ef247eaeb 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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,13 +426,13 @@ 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", + "spongefish", ] [[package]] @@ -371,7 +444,7 @@ dependencies = [ "jolt-field", "libfuzzer-sys", "rand_chacha", - "rand_core", + "rand_core 0.6.4", "serde_json", ] @@ -384,8 +457,9 @@ dependencies = [ "ark-serialize", "cc", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", + "spongefish", "thiserror", ] @@ -396,7 +470,7 @@ dependencies = [ "jolt-field", "jolt-utils", "num-traits", - "rand_core", + "rand_core 0.6.4", "rayon", "serde", "thiserror", @@ -404,17 +478,20 @@ dependencies = [ ] [[package]] -name = "jolt-transcript" +name = "jolt-utils" version = "0.1.0" dependencies = [ - "jolt-field", + "num-traits", ] [[package]] -name = "jolt-utils" -version = "0.1.0" +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ - "num-traits", + "cfg-if", + "cpufeatures", ] [[package]] @@ -433,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" @@ -449,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" @@ -473,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" @@ -524,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]] @@ -535,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]] @@ -543,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" @@ -564,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" @@ -607,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" @@ -713,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" 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 fe0064cf91..f9c23fca0a 100644 --- a/crates/jolt-crypto/src/commitment.rs +++ b/crates/jolt-crypto/src/commitment.rs @@ -4,10 +4,12 @@ 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}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[cfg(feature = "parallel")] const PAR_THRESHOLD: usize = 1024; @@ -55,6 +57,24 @@ impl CanonicalBytes for NoCommitment { fn to_bytes_le(&self, _out: &mut [u8]) {} } +impl 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 NargDeserialize for NoCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + ::jolt_field::narg::deserialize(buf) + } +} + impl HomomorphicCommitment for NoCommitment { fn add(_c1: &Self, _c2: &Self) -> Self { Self @@ -105,7 +125,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 41a7792536..f2d8a0343a 100644 --- a/crates/jolt-crypto/src/ec/bn254/gt.rs +++ b/crates/jolt-crypto/src/ec/bn254/gt.rs @@ -4,13 +4,15 @@ 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; use super::field_to_fr; +use serde::de::Error; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; /// BN254 target group element (pairing output). /// @@ -150,24 +152,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 +195,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(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 +233,51 @@ 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 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 NargDeserialize for Bn254GT { + fn deserialize_from_narg(buf: &mut &[u8]) -> 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 9ac7ec37ce..06fa310374 100644 --- a/crates/jolt-crypto/src/ec/bn254/mod.rs +++ b/crates/jolt-crypto/src/ec/bn254/mod.rs @@ -1,12 +1,12 @@ /// 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)] @@ -129,12 +129,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)) } } @@ -142,33 +137,70 @@ 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 point = <$projective>::deserialize_compressed(bytes) + .map(Self) + .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) + )); } - let inner = <$projective>::deserialize_compressed(&buf[..]) - .map_err(::serde::de::Error::custom)?; - Ok(Self(inner)) + Ok(point) } } - 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 ::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) } } @@ -323,8 +355,49 @@ 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::{CanonicalBytes, CanonicalDecode}; use super::{Bn254, Bn254G1, Bn254G2}; + use crate::JoltGroup; + use std::fmt::Debug; + + #[test] + 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); + } + + /// 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(); diff --git a/crates/jolt-crypto/src/ec/group.rs b/crates/jolt-crypto/src/ec/group.rs index 4d155629c4..f20f8e9338 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 { #[must_use] fn identity() -> Self; diff --git a/crates/jolt-dory/Cargo.toml b/crates/jolt-dory/Cargo.toml index 7a946e7a57..afbb6292a3 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 } @@ -32,7 +33,7 @@ profiling = ["jolt-akita/profiling"] jolt-akita.workspace = true akita-config.workspace = true akita-params.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 57268980b6..b5802a3235 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. //! @@ -44,7 +44,9 @@ use akita_pcs::{ use akita_types::{CommittedGroup, OpeningClaims, PolynomialGroupClaims}; use criterion::{criterion_group, BatchSize, BenchmarkGroup, BenchmarkId, Criterion}; use jolt_akita::{ - configs::{JoltDenseBounded as AkitaConfig, JoltOneHotK256 as AkitaOneHotConfig}, + configs::{ + CatalogChunking, JoltDenseBounded as AkitaConfig, JoltOneHotK256 as AkitaOneHotConfig, + }, jolt_to_akita_evals, reverse_point, AkitaField, AkitaNativeBatching, AkitaProverHint, AkitaScheduleArtifacts, AkitaScheme, AkitaSetupParams, AKITA_ONE_HOT_K256, }; @@ -55,9 +57,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; @@ -328,14 +331,18 @@ fn akita_case(num_vars: usize) -> AkitaCase { )) .unwrap(); let dense_scheme = BackendScheme::new( - TrustedScheduleCatalog::new(artifacts.dense_catalog().expect("dense catalog")) - .expect("dense scheme"), + TrustedScheduleCatalog::new( + artifacts.dense_catalog().expect("dense catalog"), + AkitaConfig::CHUNKING, + ) + .expect("dense scheme"), ); let one_hot_scheme = OneHotBackendScheme::new( TrustedScheduleCatalog::new( artifacts .one_hot_catalog(AKITA_ONE_HOT_K256) .expect("one-hot catalog"), + AkitaOneHotConfig::CHUNKING, ) .expect("one-hot scheme"), ); @@ -483,12 +490,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), @@ -496,18 +507,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) @@ -520,7 +529,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( @@ -543,12 +553,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, @@ -557,8 +563,7 @@ fn wrapper_open( &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), DataPath::SparseDataSparsePath => AkitaScheme::open( &case.sparse_one_hot, &case.point, @@ -566,8 +571,7 @@ fn wrapper_open( &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), DataPath::SparseDataDensePath => AkitaScheme::open( &case.sparse_dense_poly, &case.point, @@ -575,9 +579,10 @@ fn wrapper_open( &case.setup, Some(hint), &mut transcript, - ) - .unwrap(), + ), } + .unwrap(); + transcript.finish() } const NATIVE_BENCH_SESSION: &[u8] = b"jolt-akita/native-bench"; @@ -692,8 +697,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, @@ -702,8 +707,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, @@ -711,8 +715,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, @@ -720,9 +723,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-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/fuzz/Cargo.lock b/crates/jolt-dory/fuzz/Cargo.lock index 3b292a8d8a..af4dc3d674 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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,18 +544,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.8", "num-integer", "num-traits", "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -637,7 +564,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -647,6 +574,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -672,12 +600,13 @@ 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", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -701,9 +630,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", @@ -717,7 +646,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -732,13 +660,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.3", + "ark-ff", "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] @@ -781,7 +709,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 +1160,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", 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 0000000000..b29ec9f23f Binary files /dev/null and b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-atom differ 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 ddf7b592c9..7172562d54 100644 Binary files a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-bincode and b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-commitment-bincode differ diff --git a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-opening-narg b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-opening-narg new file mode 100644 index 0000000000..98bfd0ba44 Binary files /dev/null and b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-opening-narg differ 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 8413680655..0000000000 Binary files a/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-proof-bincode and /dev/null differ 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 0000000000..7f19bc149f Binary files /dev/null and b/crates/jolt-dory/fuzz/seeds/deser_commitment/valid-zk-opening-narg differ diff --git a/crates/jolt-dory/fuzz/seeds/verify_tampered/negate-vmv b/crates/jolt-dory/fuzz/seeds/verify_tampered/negate-vmv index e900ba804c..75f989db0b 100644 Binary files a/crates/jolt-dory/fuzz/seeds/verify_tampered/negate-vmv and b/crates/jolt-dory/fuzz/seeds/verify_tampered/negate-vmv differ diff --git a/crates/jolt-dory/fuzz/seeds/verify_tampered/strip-final-message b/crates/jolt-dory/fuzz/seeds/verify_tampered/strip-final-message index 7c4860275e..db80394cdf 100644 Binary files a/crates/jolt-dory/fuzz/seeds/verify_tampered/strip-final-message and b/crates/jolt-dory/fuzz/seeds/verify_tampered/strip-final-message differ diff --git a/crates/jolt-dory/fuzz/seeds/verify_tampered/swap-rounds b/crates/jolt-dory/fuzz/seeds/verify_tampered/swap-rounds index 00b574a5cc..d1a78db6c6 100644 Binary files a/crates/jolt-dory/fuzz/seeds/verify_tampered/swap-rounds and b/crates/jolt-dory/fuzz/seeds/verify_tampered/swap-rounds differ diff --git a/crates/jolt-dory/fuzz/seeds/verify_tampered/zk-flip b/crates/jolt-dory/fuzz/seeds/verify_tampered/zk-flip new file mode 100644 index 0000000000..1da5ce804f Binary files /dev/null and b/crates/jolt-dory/fuzz/seeds/verify_tampered/zk-flip differ diff --git a/crates/jolt-dory/fuzz/tests/generate_seeds.rs b/crates/jolt-dory/fuzz/tests/generate_seeds.rs index 625f08d588..0c8d40060d 100644 --- a/crates/jolt-dory/fuzz/tests/generate_seeds.rs +++ b/crates/jolt-dory/fuzz/tests/generate_seeds.rs @@ -1,21 +1,30 @@ //! Regenerates the checked-in `seeds/deser_commitment/` corpus from a genuine -//! serialized Dory commitment and proof, so mutation explores the +//! Dory commitment and genuine argument strings, so mutation explores the //! neighborhood of valid encodings instead of pure garbage. //! +//! The opening seeds use the target's protocol id, session, and statement +//! (point `(1, ..., NUM_VARS)`, evaluation zero), so they parse in full and +//! reach `dory::verify`. Whether they also accept depends on the SRS: dory-pcs +//! caches a randomly generated URS per machine. +//! //! Run explicitly, then commit the outputs: -//! `cargo test --manifest-path crates/jolt-dory/fuzz/Cargo.toml -- --ignored` +//! `cargo nextest run --manifest-path crates/jolt-dory/fuzz/Cargo.toml --run-ignored only` use std::fs; use std::path::Path; -use jolt_dory::DoryScheme; -use jolt_field::{Field, Fr}; -use jolt_openings::CommitmentScheme; +use jolt_dory::{DoryCommitment, DoryScheme}; +use jolt_field::{CanonicalBytes, Fr, Ring}; +use jolt_openings::{CommitmentScheme, ZkOpeningScheme}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{Blake2b512, ProtocolId, ProverTranscript}; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; +const NUM_VARS: usize = 2; +const PROTOCOL: ProtocolId = ProtocolId::new::("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-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 a4915a1a3a..db947416d1 100644 --- a/crates/jolt-dory/src/scheme.rs +++ b/crates/jolt-dory/src/scheme.rs @@ -14,12 +14,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. @@ -200,7 +200,6 @@ impl Commitment for DoryScheme { impl CommitmentScheme for DoryScheme { type Field = Fr; - type Proof = DoryProof; type ProverSetup = DoryProverSetup; type VerifierSetup = DoryVerifierSetup; type OpeningHint = DoryHint; @@ -226,15 +225,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); @@ -253,9 +267,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, @@ -267,42 +282,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.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); - 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) } } @@ -369,19 +375,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); @@ -389,7 +391,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::( @@ -403,6 +405,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 @@ -412,38 +415,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.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. 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) } diff --git a/crates/jolt-dory/src/streaming.rs b/crates/jolt-dory/src/streaming.rs index 39fea02bdd..13938c6dd6 100644 --- a/crates/jolt-dory/src/streaming.rs +++ b/crates/jolt-dory/src/streaming.rs @@ -463,7 +463,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; @@ -502,8 +501,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, @@ -513,12 +512,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, ); @@ -559,8 +558,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, @@ -570,12 +569,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, ); @@ -691,8 +690,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, @@ -701,14 +700,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"); } @@ -754,8 +749,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, @@ -765,12 +760,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..fb5bbd6887 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 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. +//! +//! 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, TranscriptError, 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: Sponge> { + 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,244 @@ 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: Sponge> { + 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, 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, + zk: bool, +) -> Result { + let sigma = num_vars.div_ceil(2); + let nu = num_vars - sigma; + let reader = &mut { unread }; + + 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; +} + +/// Parses the next compressed `T` from the unread proof bytes, advancing +/// `unread` past it. +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::Transcript(TranscriptError::Truncated))?; + let value = T::deserialize_compressed(bytes).map_err(|_| OpeningsError::VerificationFailed)?; + *unread = rest; + Ok(value) +} + +#[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 9c6e8f0d06..0c9ff12bb2 100644 --- a/crates/jolt-dory/src/types.rs +++ b/crates/jolt-dory/src/types.rs @@ -1,25 +1,18 @@ 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}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; -/// 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); @@ -46,13 +39,30 @@ 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); + } +} + +impl 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) } +} - fn transcript_payload_len(&self) -> Option { - self.0.transcript_payload_len() +impl NargDeserialize for DoryCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + ::jolt_field::narg::deserialize(buf) } } @@ -68,37 +78,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); @@ -178,9 +157,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 @@ -225,51 +203,21 @@ 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; + use crate::DoryScheme; use jolt_field::Fr; #[test] @@ -308,8 +256,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"); + DoryScheme::open( &poly, &point, eval, @@ -319,12 +267,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, ); @@ -365,128 +313,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 58957d95d4..b813eb71a4 100644 --- a/crates/jolt-dory/tests/commit_open_verify.rs +++ b/crates/jolt-dory/tests/commit_open_verify.rs @@ -5,17 +5,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); @@ -26,40 +37,35 @@ fn round_trip>(num_vars: usize, seed: u64, label: let eval = poly.evaluate(&point); let (commitment, hint) = DoryScheme::commit(poly.evaluations(), &prover_setup).unwrap(); - 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"); - 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] @@ -143,14 +149,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); } @@ -168,19 +174,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"); } @@ -198,20 +198,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"); } @@ -292,23 +286,17 @@ 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(); let wrong_poly = Polynomial::::random(num_vars, &mut rng); let (wrong_commitment, _) = 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"); } @@ -326,15 +314,24 @@ 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"); } -fn zk_round_trip>(num_vars: usize, seed: u64, label: &'static [u8]) { +#[test] +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"); + } +} + +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); @@ -346,29 +343,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] @@ -386,12 +383,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" @@ -414,8 +412,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); @@ -424,8 +422,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"); } @@ -447,18 +446,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" @@ -497,8 +491,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, @@ -508,15 +502,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); } @@ -535,17 +525,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"); } @@ -564,12 +560,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" @@ -622,8 +619,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, @@ -633,14 +630,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"); + } } diff --git a/crates/jolt-field/Cargo.toml b/crates/jolt-field/Cargo.toml index 39c1e77367..73d007ebf6 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"] @@ -41,6 +44,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/fuzz/Cargo.lock b/crates/jolt-field/fuzz/Cargo.lock index 35cc723b70..2e415c8e99 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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" diff --git a/crates/jolt-field/src/algebra.rs b/crates/jolt-field/src/algebra.rs index 475879f796..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 @@ -254,7 +256,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: Encoding<[u8]> { /// Byte length of the fixed-size canonical encoding. const NUM_BYTES: usize; @@ -270,18 +274,113 @@ 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`. +/// - The spongefish [`NargDeserialize`](spongefish::NargDeserialize) reads +/// exactly this decoding; implement it with [`narg::deserialize`]. +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; +} + +/// 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 { + 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_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; + + #[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; @@ -289,10 +388,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/binary/f128.rs b/crates/jolt-field/src/binary/f128.rs index f8d388d74c..907e9e3edd 100644 --- a/crates/jolt-field/src/binary/f128.rs +++ b/crates/jolt-field/src/binary/f128.rs @@ -1,8 +1,12 @@ use super::{arithmetic, inverse}; -use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{ + CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, +}; #[cfg(feature = "allocative")] use allocative::Allocative; use rand_core::RngCore; +use spongefish::{Encoding, NargDeserialize, VerificationResult}; use std::fmt::{Display, Formatter, Result as FmtResult}; /// `F_2[x]/(x^128 + x^7 + x^2 + x + 1)`, with bit i the coefficient of x^i. @@ -84,6 +88,24 @@ impl CanonicalBytes for F128 { } } +impl Encoding<[u8]> for F128 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + +impl CanonicalDecode for F128 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Some(Self(u128::from_le_bytes(bytes.try_into().ok()?))) + } +} + +impl NargDeserialize for F128 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + crate::narg::deserialize(buf) + } +} + impl CanonicalEncoding for F128 { const MODULUS_BITS: u32 = 129; @@ -95,10 +117,6 @@ impl CanonicalEncoding for F128 { Self(u128::from_le_bytes(word)) } - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - Some(Self(u128::from_le_bytes(bytes.try_into().ok()?))) - } - fn to_u128_checked(&self) -> Option { Some(self.0) } diff --git a/crates/jolt-field/src/binary/f192.rs b/crates/jolt-field/src/binary/f192.rs index 00d83d749f..3aaaa2da64 100644 --- a/crates/jolt-field/src/binary/f192.rs +++ b/crates/jolt-field/src/binary/f192.rs @@ -1,11 +1,12 @@ use super::{arithmetic, inverse, F64}; use crate::{ - CanonicalBytes, CanonicalEncoding, ExtField, Field, NaiveAccumulator, One, Ring, - WithAccumulator, Zero, + CanonicalBytes, CanonicalDecode, CanonicalEncoding, ExtField, Field, NaiveAccumulator, One, + Ring, WithAccumulator, Zero, }; #[cfg(feature = "allocative")] use allocative::Allocative; use rand_core::RngCore; +use spongefish::{Encoding, NargDeserialize, VerificationResult}; use std::fmt::{Display, Formatter, Result as FmtResult}; /// `F64[y]/(y^3 + y + 1)`, with coefficients in ascending degree order. @@ -116,6 +117,24 @@ impl CanonicalBytes for F192 { } } +impl Encoding<[u8]> for F192 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + +impl CanonicalDecode for F192 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + (bytes.len() == Self::NUM_BYTES).then(|| Self::from_bytes_le_reduced(bytes)) + } +} + +impl NargDeserialize for F192 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + crate::narg::deserialize(buf) + } +} + impl CanonicalEncoding for F192 { const MODULUS_BITS: u32 = 193; @@ -127,10 +146,6 @@ impl CanonicalEncoding for F192 { Self(coefficients) } - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - (bytes.len() == Self::NUM_BYTES).then(|| Self::from_bytes_le_reduced(bytes)) - } - fn to_u128_checked(&self) -> Option { let [c0, c1, c2] = self.0; if c1.is_zero() && c2.is_zero() { diff --git a/crates/jolt-field/src/binary/f64.rs b/crates/jolt-field/src/binary/f64.rs index 2b78f465c2..70a20367e1 100644 --- a/crates/jolt-field/src/binary/f64.rs +++ b/crates/jolt-field/src/binary/f64.rs @@ -1,8 +1,12 @@ use super::{arithmetic, inverse}; -use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{ + CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, +}; #[cfg(feature = "allocative")] use allocative::Allocative; use rand_core::RngCore; +use spongefish::{Encoding, NargDeserialize, VerificationResult}; use std::fmt::{Display, Formatter, Result as FmtResult}; /// `F_2[x]/(x^64 + x^4 + x^3 + x + 1)`, with bit i the coefficient of x^i. @@ -84,6 +88,24 @@ impl CanonicalBytes for F64 { } } +impl Encoding<[u8]> for F64 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + +impl CanonicalDecode for F64 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Some(Self(u64::from_le_bytes(bytes.try_into().ok()?))) + } +} + +impl NargDeserialize for F64 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + crate::narg::deserialize(buf) + } +} + impl CanonicalEncoding for F64 { const MODULUS_BITS: u32 = 65; @@ -95,10 +117,6 @@ impl CanonicalEncoding for F64 { Self(u64::from_le_bytes(word)) } - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - Some(Self(u64::from_le_bytes(bytes.try_into().ok()?))) - } - fn to_u128_checked(&self) -> Option { Some(self.0 as u128) } diff --git a/crates/jolt-field/src/binary/f8.rs b/crates/jolt-field/src/binary/f8.rs index c69c086834..dc2dae96dc 100644 --- a/crates/jolt-field/src/binary/f8.rs +++ b/crates/jolt-field/src/binary/f8.rs @@ -1,8 +1,12 @@ use super::inverse; -use crate::{CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, WithAccumulator}; +use crate::{ + CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field, NaiveAccumulator, Ring, + WithAccumulator, +}; #[cfg(feature = "allocative")] use allocative::Allocative; use rand_core::RngCore; +use spongefish::{Encoding, NargDeserialize, VerificationResult}; use std::fmt::{Display, Formatter, Result as FmtResult}; /// `F_2[x]/(x^8 + x^4 + x^3 + x + 1)`, with bit i the coefficient of x^i. @@ -99,6 +103,24 @@ impl CanonicalBytes for F8 { } } +impl Encoding<[u8]> for F8 { + fn encode(&self) -> impl AsRef<[u8]> { + crate::narg::encode(self) + } +} + +impl CanonicalDecode for F8 { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Some(Self(u8::from_le_bytes(bytes.try_into().ok()?))) + } +} + +impl NargDeserialize for F8 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + crate::narg::deserialize(buf) + } +} + impl CanonicalEncoding for F8 { const MODULUS_BITS: u32 = 9; @@ -110,10 +132,6 @@ impl CanonicalEncoding for F8 { Self(u8::from_le_bytes(word)) } - fn from_bytes_le_checked(bytes: &[u8]) -> Option { - Some(Self(u8::from_le_bytes(bytes.try_into().ok()?))) - } - fn to_u128_checked(&self) -> Option { Some(self.0 as u128) } diff --git a/crates/jolt-field/src/bn254/mod.rs b/crates/jolt-field/src/bn254/mod.rs index 9f10910a1f..30097a8110 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; @@ -119,14 +122,13 @@ 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 ::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 { use ark_serialize::CanonicalDeserialize; @@ -135,6 +137,22 @@ macro_rules! wrap_bn254 { } <$inner>::deserialize_compressed(bytes).ok().map($ty) } + } + + 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; + + #[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 a53b085163..bb89e10f82 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 + @@ -73,7 +74,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. @@ -115,9 +116,10 @@ pub mod signed; pub mod solinas; mod unreduced; +pub use algebra::narg; 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 = "binary")] pub use binary::{F128, F192, F64, F8}; 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 37a714c985..7f8734e9c2 100644 --- a/crates/jolt-field/src/solinas/ext.rs +++ b/crates/jolt-field/src/solinas/ext.rs @@ -26,11 +26,16 @@ )] 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, +}; #[cfg(feature = "bytemuck")] 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 @@ -140,6 +145,25 @@ impl> CanonicalBytes for FpExt2> 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)?; + Some(Self::new(c0, c1)) + } +} + +impl> NargDeserialize for FpExt2 { + fn deserialize_from_narg(buf: &mut &[u8]) -> 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]), @@ -343,6 +367,24 @@ impl CanonicalBytes for FpExt4 { } } +impl 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 NargDeserialize for FpExt4 { + fn deserialize_from_narg(buf: &mut &[u8]) -> 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])), @@ -469,6 +511,49 @@ 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 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 NargDeserialize for FpExt8 { + fn deserialize_from_narg(buf: &mut &[u8]) -> 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]> { + 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 543b61e519..8a52300f42 100644 --- a/crates/jolt-field/src/solinas/fp128.rs +++ b/crates/jolt-field/src/solinas/fp128.rs @@ -20,12 +20,15 @@ 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}; 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"))] @@ -1126,6 +1129,26 @@ impl CanonicalBytes for Fp128

{ } } +impl 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 { + let arr: [u8; 16] = bytes.try_into().ok()?; + Self::from_u128_checked(u128::from_le_bytes(arr)) + } +} + +impl NargDeserialize for Fp128

{ + fn deserialize_from_narg(buf: &mut &[u8]) -> 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; @@ -1140,12 +1163,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 0d55b18abf..e1a24e215f 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, +}; #[cfg(feature = "bytemuck")] use bytemuck::{CheckedBitPattern, NoUninit, Zeroable}; use rand_core::RngCore; @@ -305,6 +308,26 @@ 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 { + let arr: [u8; (<$word>::BITS / 8) as usize] = bytes.try_into().ok()?; + Self::from_u128_checked(<$word>::from_le_bytes(arr) as u128) + } + } + + 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; @@ -318,11 +341,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/binary_f8.rs b/crates/jolt-field/tests/binary_f8.rs index 39f5ae4993..62977126b8 100644 --- a/crates/jolt-field/tests/binary_f8.rs +++ b/crates/jolt-field/tests/binary_f8.rs @@ -2,8 +2,8 @@ use jolt_field::signed::S256; use jolt_field::{ - Accumulator, CanonicalBytes, CanonicalEncoding, ExtField, Field, JoltField, NaiveAccumulator, - One, Ring, WithAccumulator, Zero, F128, F192, F64, F8, + Accumulator, CanonicalBytes, CanonicalDecode, CanonicalEncoding, ExtField, Field, JoltField, + NaiveAccumulator, One, Ring, WithAccumulator, Zero, F128, F192, F64, F8, }; use rand_core::{Error, RngCore}; use std::collections::HashSet; diff --git a/crates/jolt-field/tests/solinas_ext_differential.rs b/crates/jolt-field/tests/solinas_ext_differential.rs index 4c7295e51f..46cfe243f1 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) @@ -377,6 +379,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 + ); let (mut r1, mut r2) = ( ChaCha20Rng::seed_from_u64(0x5EED_0001), diff --git a/crates/jolt-field/tests/solinas_fp128_differential.rs b/crates/jolt-field/tests/solinas_fp128_differential.rs index 5c58a71db3..363d5c5cb8 100644 --- a/crates/jolt-field/tests/solinas_fp128_differential.rs +++ b/crates/jolt-field/tests/solinas_fp128_differential.rs @@ -11,7 +11,7 @@ use jolt_field as two; use rand::{Rng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{CanonicalBytes, CanonicalEncoding, Field as _, PseudoMersenne, Ring}; +use two::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field as _, PseudoMersenne, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xf128_a5a5) @@ -283,16 +283,16 @@ macro_rules! check_prime128 { 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 e024375e73..3e00631564 100644 --- a/crates/jolt-field/tests/solinas_words_differential.rs +++ b/crates/jolt-field/tests/solinas_words_differential.rs @@ -11,7 +11,7 @@ use jolt_field as two; use num_bigint::BigUint; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use two::{CanonicalBytes, CanonicalEncoding, Field as _, PseudoMersenne, Ring}; +use two::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field as _, PseudoMersenne, Ring}; fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0x5011_a5a5) @@ -181,11 +181,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 a77a40b2e1..2b174a61b5 100644 --- a/crates/jolt-field/tests/spine.rs +++ b/crates/jolt-field/tests/spine.rs @@ -7,11 +7,14 @@ #![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; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; const P: u64 = (1 << 61) - 1; @@ -88,6 +91,25 @@ impl CanonicalBytes for M61 { } } +impl 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()?; + Self::from_u128_checked(u64::from_le_bytes(arr) as u128) + } +} + +impl NargDeserialize for M61 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl CanonicalEncoding for M61 { const MODULUS_BITS: u32 = 61; fn from_bytes_le_reduced(bytes: &[u8]) -> Self { @@ -97,10 +119,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-hyperkzg/src/kzg.rs b/crates/jolt-hyperkzg/src/kzg.rs index 94757b6925..2e9fae086a 100644 --- a/crates/jolt-hyperkzg/src/kzg.rs +++ b/crates/jolt-hyperkzg/src/kzg.rs @@ -1,6 +1,6 @@ use jolt_crypto::{Bn254, Bn254G1, JoltGroup, PairingGroup}; use jolt_field::{Fr, One, Zero}; -use jolt_transcript::Transcript; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; use crate::{HyperKZGError, HyperKZGProverSetup, HyperKZGVerifierSetup}; @@ -16,19 +16,20 @@ impl HyperKZGProverSetup { Ok(Bn254G1::msm(bases, coefficients)) } - pub(crate) fn open_batch( + /// Sends the evaluations at `points` and the batched KZG witnesses. + pub(crate) fn open_batch( &self, polynomials: &[Vec], points: [Fr; 3], - transcript: &mut impl Transcript, - ) -> Result<([Bn254G1; 3], [Vec; 3]), HyperKZGError> { - let evaluations = points.map(|point| { + transcript: &mut ProverTranscript, + ) -> Result<(), HyperKZGError> { + let mut evaluations = points.map(|point| { polynomials .iter() .map(|coefficients| evaluate(coefficients, point)) .collect::>() }); - let powers = absorb_evaluations(&evaluations, transcript); + let powers = exchange_evaluations(&mut evaluations, transcript)?; let first = polynomials.first().ok_or(HyperKZGError::PolynomialShape)?; let mut combined = vec![Fr::zero(); first.len()]; for (polynomial, weight) in polynomials.iter().zip(powers) { @@ -37,35 +38,37 @@ impl HyperKZGProverSetup { } } let [a, b, c] = points.map(|point| self.commit_coefficients("ient(&combined, point))); - let witnesses = [a?, b?, c?]; - let _ = absorb_witnesses(&witnesses, transcript); - Ok((witnesses, evaluations)) + let mut witnesses = [a?, b?, c?]; + let _ = exchange_witnesses(&mut witnesses, transcript)?; + Ok(()) } } impl HyperKZGVerifierSetup { - pub(crate) fn verify_batch( + /// Receives the KZG witnesses for `evaluations` (already received, with + /// their batching `powers`) and checks the batched pairing equation. + pub(crate) fn verify_batch( &self, commitments: &[Bn254G1], points: [Fr; 3], evaluations: &[Vec; 3], - witnesses: &[Bn254G1; 3], - transcript: &mut impl Transcript, + powers: &[Fr], + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), HyperKZGError> { - let powers = absorb_evaluations(evaluations, transcript); - let d = absorb_witnesses(witnesses, transcript); + let mut witnesses = [Bn254G1::identity(); 3]; + let d = exchange_witnesses(&mut witnesses, transcript)?; let weights = [Fr::one(), d, d * d]; let scale = weights.iter().copied().sum::(); - let mut lhs = Bn254G1::msm(commitments, &powers).scalar_mul(&scale); + let mut lhs = Bn254G1::msm(commitments, powers).scalar_mul(&scale); let mut rhs = Bn254G1::identity(); let mut evaluation = Fr::zero(); for (((row, point), witness), weight) in - evaluations.iter().zip(points).zip(witnesses).zip(weights) + evaluations.iter().zip(points).zip(&witnesses).zip(weights) { evaluation += weight * row .iter() - .zip(&powers) + .zip(powers) .map(|(value, power)| *value * power) .sum::(); lhs += witness.scalar_mul(&(point * weight)); @@ -79,30 +82,30 @@ impl HyperKZGVerifierSetup { } } -fn absorb_evaluations( - evaluations: &[Vec; 3], - transcript: &mut impl Transcript, -) -> Vec { - for row in evaluations { - for value in row { - transcript.append(value); - } +/// Exchanges the three evaluation rows, then draws the batching powers. +pub(crate) fn exchange_evaluations( + evaluations: &mut [Vec; 3], + channel: &mut C, +) -> Result, HyperKZGError> { + for row in evaluations.iter_mut() { + channel.exchange_all(row)?; } - let q = transcript.challenge(); + let q: Fr = channel.challenge(); let [first, _, _] = evaluations; - std::iter::successors(Some(Fr::one()), |power| Some(*power * q)) - .take(first.len()) - .collect() + Ok( + std::iter::successors(Some(Fr::one()), |power| Some(*power * q)) + .take(first.len()) + .collect(), + ) } -fn absorb_witnesses( - witnesses: &[Bn254G1; 3], - transcript: &mut impl Transcript, -) -> Fr { - for witness in witnesses { - transcript.append(witness); - } - transcript.challenge() +/// Exchanges the three KZG witnesses, then draws their combination weight. +fn exchange_witnesses( + witnesses: &mut [Bn254G1; 3], + channel: &mut C, +) -> Result { + channel.exchange_all(witnesses)?; + Ok(channel.challenge()) } fn evaluate(coefficients: &[Fr], point: Fr) -> Fr { diff --git a/crates/jolt-hyperkzg/src/lib.rs b/crates/jolt-hyperkzg/src/lib.rs index f6a4e15b67..c966c3a20c 100644 --- a/crates/jolt-hyperkzg/src/lib.rs +++ b/crates/jolt-hyperkzg/src/lib.rs @@ -25,6 +25,4 @@ mod scheme; mod types; pub use scheme::HyperKZGScheme; -pub use types::{ - HyperKZGError, HyperKZGProof, HyperKZGProverSetup, HyperKZGSetupParams, HyperKZGVerifierSetup, -}; +pub use types::{HyperKZGError, HyperKZGProverSetup, HyperKZGSetupParams, HyperKZGVerifierSetup}; diff --git a/crates/jolt-hyperkzg/src/scheme.rs b/crates/jolt-hyperkzg/src/scheme.rs index 907beeedee..3c2d31d7be 100644 --- a/crates/jolt-hyperkzg/src/scheme.rs +++ b/crates/jolt-hyperkzg/src/scheme.rs @@ -2,11 +2,10 @@ use jolt_crypto::{Bn254G1, Commitment, JoltGroup}; use jolt_field::{Fr, One, Zero}; use jolt_openings::{CommitmentScheme, OpeningsError}; use jolt_poly::MultilinearPoly; -use jolt_transcript::Transcript; +use jolt_transcript::{Channel, ProverTranscript, Sponge, VerifierTranscript}; -use crate::{ - HyperKZGError, HyperKZGProof, HyperKZGProverSetup, HyperKZGSetupParams, HyperKZGVerifierSetup, -}; +use crate::kzg::exchange_evaluations; +use crate::{HyperKZGError, HyperKZGProverSetup, HyperKZGSetupParams, HyperKZGVerifierSetup}; /// Clear binary HyperKZG with high-to-low multilinear coordinates. /// @@ -45,14 +44,16 @@ impl HyperKZGScheme { }) } - fn open_table( + /// Writes the opening: the binary fold commitments, the fold evaluations at + /// `[r, -r, r^2]`, then the KZG witnesses. + fn open_table( evaluations: &[Fr], point: &[Fr], evaluation: Fr, setup: &HyperKZGProverSetup, hint: Option, - transcript: &mut impl Transcript, - ) -> Result { + transcript: &mut ProverTranscript, + ) -> Result<(), HyperKZGError> { let len = setup.verifier.check_arity(point.len())?; if evaluations.len() != len { return Err(HyperKZGError::PolynomialShape); @@ -78,46 +79,46 @@ impl HyperKZGScheme { }; setup .verifier - .append_statement(&commitment, point, evaluation, transcript); - let com = polynomials + .bind_statement(&commitment, point, evaluation, transcript); + let mut com = polynomials .iter() .skip(1) .map(|polynomial| setup.commit_coefficients(polynomial)) .collect::, _>>()?; - for commitment in &com { - transcript.append(commitment); - } - let r = transcript.challenge(); + let r = Self::exchange_fold_commitments(&mut com, transcript)?; + setup.open_batch(&polynomials, [r, -r, r * r], transcript) + } + + /// Exchanges the binary fold commitments and draws the nonzero fold point. + fn exchange_fold_commitments( + com: &mut [Bn254G1], + channel: &mut C, + ) -> Result { + channel.exchange_all(com)?; + let r: Fr = channel.challenge(); if r.is_zero() { return Err(HyperKZGError::DegenerateChallenge); } - let (w, v) = setup.open_batch(&polynomials, [r, -r, r * r], transcript)?; - Ok(HyperKZGProof { com, v, w }) + Ok(r) } - /// Verifies shape, binary fold identities, and the batched KZG equation. - pub fn verify_opening( + /// Reads an opening of `commitment` at `point` and checks the binary fold + /// identities and the batched KZG equation. The arity is checked against + /// the setup before anything is bound or read. + pub fn verify_opening( commitment: &Bn254G1, point: &[Fr], evaluation: Fr, - proof: &HyperKZGProof, setup: &HyperKZGVerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), HyperKZGError> { let _ = setup.check_arity(point.len())?; - if proof.com.len() != point.len() - 1 || proof.v.iter().any(|row| row.len() != point.len()) - { - return Err(HyperKZGError::ProofShape); - } - setup.append_statement(commitment, point, evaluation, transcript); - for commitment in &proof.com { - transcript.append(commitment); - } - let r = transcript.challenge(); - if r.is_zero() { - return Err(HyperKZGError::DegenerateChallenge); - } - let [positive, negative, squared] = &proof.v; + setup.bind_statement(commitment, point, evaluation, transcript); + let mut com = vec![Bn254G1::identity(); point.len() - 1]; + let r = Self::exchange_fold_commitments(&mut com, transcript)?; + let mut evaluations: [Vec; 3] = std::array::from_fn(|_| vec![Fr::zero(); point.len()]); + let powers = exchange_evaluations(&mut evaluations, transcript)?; + let [positive, negative, squared] = &evaluations; let next = squared .iter() .skip(1) @@ -137,8 +138,14 @@ impl HyperKZGScheme { } let mut commitments = Vec::with_capacity(point.len()); commitments.push(*commitment); - commitments.extend_from_slice(&proof.com); - setup.verify_batch(&commitments, [r, -r, r * r], &proof.v, &proof.w, transcript) + commitments.extend_from_slice(&com); + setup.verify_batch( + &commitments, + [r, -r, r * r], + &evaluations, + &powers, + transcript, + ) } } @@ -148,7 +155,6 @@ impl Commitment for HyperKZGScheme { impl CommitmentScheme for HyperKZGScheme { type Field = Fr; - type Proof = HyperKZGProof; type ProverSetup = HyperKZGProverSetup; type VerifierSetup = HyperKZGVerifierSetup; /// The commitment returned by `commit`. `open` binds it into the statement @@ -189,14 +195,29 @@ impl CommitmentScheme for HyperKZGScheme { .map_err(|error| OpeningsError::CommitFailed(error.to_string())) } - fn open + ?Sized>( + fn send_commitment(commitment: &Bn254G1, transcript: &mut ProverTranscript) { + transcript.send(commitment); + } + + fn receive_commitment( + _setup: &Self::VerifierSetup, + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result { + Ok(transcript.receive()?) + } + + fn absorb_commitment(commitment: &Bn254G1, channel: &mut C) { + channel.public(commitment); + } + + fn open + ?Sized, H: Sponge>( poly: &P, point: &[Fr], evaluation: Fr, setup: &Self::ProverSetup, hint: Option, - transcript: &mut impl Transcript, - ) -> Result { + transcript: &mut ProverTranscript, + ) -> Result<(), OpeningsError> { if poly.num_vars() != point.len() { return Err(OpeningsError::ProveFailed( HyperKZGError::PolynomialShape.to_string(), @@ -210,15 +231,14 @@ impl CommitmentScheme for HyperKZGScheme { .map_err(|error| OpeningsError::ProveFailed(error.to_string())) } - fn verify( + fn verify( commitment: &Self::Output, point: &[Fr], evaluation: Fr, - proof: &Self::Proof, setup: &Self::VerifierSetup, - transcript: &mut impl Transcript, + transcript: &mut VerifierTranscript<'_, H>, ) -> Result<(), OpeningsError> { - Self::verify_opening(commitment, point, evaluation, proof, setup, transcript) + Self::verify_opening(commitment, point, evaluation, setup, transcript) .map_err(|_| OpeningsError::VerificationFailed) } } diff --git a/crates/jolt-hyperkzg/src/types.rs b/crates/jolt-hyperkzg/src/types.rs index f8c89f825f..a4020d875b 100644 --- a/crates/jolt-hyperkzg/src/types.rs +++ b/crates/jolt-hyperkzg/src/types.rs @@ -1,6 +1,6 @@ use jolt_crypto::{Bn254G1, Bn254G2, JoltGroup}; use jolt_field::Fr; -use jolt_transcript::{Transcript, U64Word}; +use jolt_transcript::{Channel, TranscriptError}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -61,32 +61,26 @@ impl HyperKZGVerifierSetup { Ok(len) } - pub(crate) fn append_statement( + /// Binds the setup and the whole opening statement before any opening + /// message, on either side. + pub(crate) fn bind_statement( &self, commitment: &Bn254G1, point: &[Fr], evaluation: Fr, - transcript: &mut impl Transcript, + channel: &mut C, ) { - transcript.append_labeled(b"hyperkzg-binary-bn254-v1", &U64Word(self.num_powers)); - transcript.append_bytes(&self.setup_id); - transcript.append(&self.g1); - transcript.append(&self.g2); - transcript.append(&self.beta_g2); - transcript.append(commitment); - transcript.append_values(b"opening-point", point); - transcript.append(&evaluation); + channel.public(&self.num_powers.to_le_bytes()); + channel.public(&self.setup_id); + channel.public(&self.g1); + channel.public(&self.g2); + channel.public(&self.beta_g2); + channel.public(commitment); + channel.public_all(point); + channel.public(&evaluation); } } -/// Binary fold commitments, evaluations at `[r, -r, r^2]`, and KZG witnesses. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct HyperKZGProof { - pub com: Vec, - pub v: [Vec; 3], - pub w: [Bn254G1; 3], -} - #[derive(Clone, Debug, PartialEq, Eq, Error)] pub enum HyperKZGError { #[error("invalid imported HyperKZG setup")] @@ -105,6 +99,8 @@ pub enum HyperKZGError { Folding, #[error("KZG pairing equation failed")] Pairing, + #[error(transparent)] + Transcript(#[from] TranscriptError), } #[cfg(test)] @@ -115,11 +111,13 @@ pub enum HyperKZGError { mod tests { use jolt_crypto::Bn254; use jolt_field::{One, Ring, Zero}; - use jolt_transcript::Blake2bTranscript; + use jolt_transcript::{Blake2b512, ProtocolId, VerifierTranscript}; use super::*; use crate::HyperKZGScheme; + const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-hyperkzg/test"); + #[test] fn decoded_verifier_metadata_is_rechecked() { let valid = HyperKZGVerifierSetup { @@ -129,22 +127,22 @@ mod tests { g2: Bn254::g2_generator(), beta_g2: Bn254::g2_generator().scalar_mul(&Fr::from_u64(7)), }; - let proof = HyperKZGProof { - com: vec![], - v: std::array::from_fn(|_| vec![Fr::zero()]), - w: [Bn254G1::identity(); 3], - }; let verify = |key: &HyperKZGVerifierSetup| { + let mut transcript = + VerifierTranscript::::new(&PROTOCOL, b"decoded-key", &[]); HyperKZGScheme::verify_opening( &Bn254G1::identity(), &[Fr::one()], Fr::zero(), - &proof, key, - &mut Blake2bTranscript::new(b"decoded-key"), + &mut transcript, ) }; - verify(&valid).unwrap(); + // A valid key reaches the (here empty) opening messages. + assert_eq!( + verify(&valid), + Err(HyperKZGError::Transcript(TranscriptError::Truncated)) + ); let mut invalid = std::array::from_fn::<_, 4, _>(|_| valid.clone()); let [capacity, g1, g2, beta_g2] = &mut invalid; capacity.num_powers = 0; diff --git a/crates/jolt-hyperkzg/tests/openings.rs b/crates/jolt-hyperkzg/tests/openings.rs index 0dda652a9c..3cc41bbce4 100644 --- a/crates/jolt-hyperkzg/tests/openings.rs +++ b/crates/jolt-hyperkzg/tests/openings.rs @@ -5,14 +5,18 @@ )] use jolt_crypto::{Bn254, Bn254G1, JoltGroup}; -use jolt_field::{Fr, One, Ring, Zero}; +use jolt_field::{CanonicalBytes, CanonicalDecode, Fr, One, Ring, Zero}; use jolt_hyperkzg::{ - HyperKZGError, HyperKZGProof, HyperKZGProverSetup, HyperKZGScheme, HyperKZGSetupParams, - HyperKZGVerifierSetup, + HyperKZGError, HyperKZGProverSetup, HyperKZGScheme, HyperKZGSetupParams, HyperKZGVerifierSetup, }; -use jolt_openings::CommitmentScheme; +use jolt_openings::{CommitmentScheme, OpeningsError}; use jolt_poly::Polynomial; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, TranscriptError, VerifierTranscript, +}; + +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-hyperkzg/test"); +const SESSION: &[u8] = b"hyperkzg-test"; fn setup_params(beta: u64, capacity: usize) -> HyperKZGSetupParams { let beta = Fr::from_u64(beta); @@ -31,8 +35,24 @@ fn setup(beta: u64, capacity: usize) -> (HyperKZGProverSetup, HyperKZGVerifierSe HyperKZGScheme::setup(setup_params(beta, capacity)).unwrap() } -fn transcript() -> Blake2bTranscript { - Blake2bTranscript::new(b"hyperkzg-test") +fn prover() -> ProverTranscript { + ProverTranscript::new(&PROTOCOL, SESSION) +} + +fn verifier(narg: &[u8]) -> VerifierTranscript<'_, Blake2b512> { + VerifierTranscript::new(&PROTOCOL, SESSION, narg) +} + +fn open( + poly: &Polynomial, + point: &[Fr], + evaluation: Fr, + pk: &HyperKZGProverSetup, + hint: Option, +) -> Result, OpeningsError> { + let mut transcript = prover(); + HyperKZGScheme::open(poly, point, evaluation, pk, hint, &mut transcript)?; + Ok(transcript.finish()) } fn multilinear_oracle(table: &[Fr], point: &[Fr]) -> Fr { @@ -56,6 +76,49 @@ fn multilinear_oracle(table: &[Fr], point: &[Fr]) -> Fr { .sum() } +/// The opening's prover messages, decoded from its argument string: binary +/// fold commitments, evaluations at `[r, -r, r^2]`, and KZG witnesses. +#[derive(Clone)] +struct Messages { + com: Vec, + v: [Vec; 3], + w: [Bn254G1; 3], +} + +impl Messages { + fn decode(narg: &[u8], num_vars: usize) -> Self { + let mut rest = narg; + let mut take = |len: usize| { + let (head, tail) = rest.split_at(len); + rest = tail; + head + }; + let mut point = || Bn254G1::from_bytes_le_checked(take(Bn254G1::NUM_BYTES)).unwrap(); + let com = (1..num_vars).map(|_| point()).collect(); + let mut scalar = || Fr::from_bytes_le_checked(take(Fr::NUM_BYTES)).unwrap(); + let v = std::array::from_fn(|_| (0..num_vars).map(|_| scalar()).collect()); + let w = std::array::from_fn(|_| { + Bn254G1::from_bytes_le_checked(take(Bn254G1::NUM_BYTES)).unwrap() + }); + assert!(rest.is_empty()); + Self { com, v, w } + } + + fn encode(&self) -> Vec { + let mut narg = Vec::new(); + for point in &self.com { + narg.extend(point.to_bytes_le_vec()); + } + for value in self.v.iter().flatten() { + narg.extend(value.to_bytes_le_vec()); + } + for point in &self.w { + narg.extend(point.to_bytes_le_vec()); + } + narg + } +} + #[test] fn independent_evaluations_and_transcripts_match() { let (pk, vk) = setup(7, 64); @@ -69,16 +132,17 @@ fn independent_evaluations_and_transcripts_match() { let evaluation = multilinear_oracle(&table, &point); let poly = Polynomial::new(table); let (commitment, hint) = HyperKZGScheme::commit(&poly, &pk).unwrap(); - let mut pt = transcript(); - let proof = - HyperKZGScheme::open(&poly, &point, evaluation, &pk, Some(hint), &mut pt).unwrap(); - let bytes = bincode::serde::encode_to_vec(&proof, bincode::config::standard()).unwrap(); - let (proof, consumed): (HyperKZGProof, _) = - bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); - assert_eq!(consumed, bytes.len()); - let mut vt = transcript(); - HyperKZGScheme::verify(&commitment, &point, evaluation, &proof, &vk, &mut vt).unwrap(); - assert_eq!(pt.challenge(), vt.challenge()); + let mut pt = prover(); + HyperKZGScheme::open(&poly, &point, evaluation, &pk, Some(hint), &mut pt).unwrap(); + let narg = pt.narg().to_vec(); + assert_eq!( + narg.len(), + (num_vars - 1 + 3) * Bn254G1::NUM_BYTES + 3 * num_vars * Fr::NUM_BYTES + ); + let mut vt = verifier(&narg); + HyperKZGScheme::verify(&commitment, &point, evaluation, &vk, &mut vt).unwrap(); + assert_eq!(pt.challenge_bytes::<32>(), vt.challenge_bytes::<32>()); + vt.finish().unwrap(); } } @@ -97,7 +161,7 @@ struct Fixture { commitment: Bn254G1, point: Vec, evaluation: Fr, - proof: HyperKZGProof, + messages: Messages, vk: HyperKZGVerifierSetup, } @@ -109,46 +173,52 @@ impl Fixture { let evaluation = multilinear_oracle(&table, &point); let poly = Polynomial::new(table); let (commitment, _) = HyperKZGScheme::commit(&poly, &pk).unwrap(); - let proof = - HyperKZGScheme::open(&poly, &point, evaluation, &pk, None, &mut transcript()).unwrap(); + let narg = open(&poly, &point, evaluation, &pk, None).unwrap(); Self { commitment, + messages: Messages::decode(&narg, point.len()), point, evaluation, - proof, vk, } } - fn verify(&self, proof: &HyperKZGProof) -> Result<(), HyperKZGError> { + /// Verifies `narg` as the whole argument string of one opening. + fn verify(&self, narg: &[u8]) -> Result<(), HyperKZGError> { + let mut transcript = verifier(narg); HyperKZGScheme::verify_opening( &self.commitment, &self.point, self.evaluation, - proof, &self.vk, - &mut transcript(), - ) + &mut transcript, + )?; + Ok(transcript.finish()?) + } + + fn verify_messages(&self, messages: &Messages) -> Result<(), HyperKZGError> { + self.verify(&messages.encode()) } } #[test] fn every_transmitted_proof_component_is_bound() { let f = Fixture::new(); + f.verify_messages(&f.messages).unwrap(); for row in 0..3 { for column in 0..f.point.len() { - let mut proof = f.proof.clone(); - proof.v[row][column] += Fr::one(); - assert!(f.verify(&proof).is_err()); + let mut messages = f.messages.clone(); + messages.v[row][column] += Fr::one(); + assert!(f.verify_messages(&messages).is_err()); } - let mut proof = f.proof.clone(); - proof.w[row] += Bn254::g1_generator(); - assert!(f.verify(&proof).is_err()); + let mut messages = f.messages.clone(); + messages.w[row] += Bn254::g1_generator(); + assert!(f.verify_messages(&messages).is_err()); } - for index in 0..f.proof.com.len() { - let mut proof = f.proof.clone(); - proof.com[index] += Bn254::g1_generator(); - assert!(f.verify(&proof).is_err()); + for index in 0..f.messages.com.len() { + let mut messages = f.messages.clone(); + messages.com[index] += Bn254::g1_generator(); + assert!(f.verify_messages(&messages).is_err()); } } @@ -156,76 +226,65 @@ fn every_transmitted_proof_component_is_bound() { fn statement_changes_reject() { let mut f = Fixture::new(); f.evaluation += Fr::one(); - assert!(f.verify(&f.proof).is_err()); + assert!(f.verify_messages(&f.messages).is_err()); f.evaluation -= Fr::one(); for index in 0..f.point.len() { f.point[index] += Fr::one(); - assert!(f.verify(&f.proof).is_err()); + assert!(f.verify_messages(&f.messages).is_err()); f.point[index] -= Fr::one(); } f.commitment += Bn254::g1_generator(); - assert!(f.verify(&f.proof).is_err()); + assert!(f.verify_messages(&f.messages).is_err()); f.commitment -= Bn254::g1_generator(); f.vk = setup(11, 16).1; - assert!(f.verify(&f.proof).is_err()); + assert!(f.verify_messages(&f.messages).is_err()); f.vk = setup(7, 32).1; - assert!(f.verify(&f.proof).is_err()); + assert!(f.verify_messages(&f.messages).is_err()); let mut params = setup_params(7, 16); params.setup_id = [43; 32]; f.vk = HyperKZGScheme::setup(params).unwrap().1; - assert!(f.verify(&f.proof).is_err()); + assert!(f.verify_messages(&f.messages).is_err()); } #[test] fn absent_hint_is_recomputed_and_present_hint_is_bound() { let f = Fixture::new(); - f.verify(&f.proof).unwrap(); + f.verify_messages(&f.messages).unwrap(); let (pk, _) = setup(7, 16); let poly = Polynomial::new((1..=16).map(Fr::from_u64).collect::>()); let hint = f.commitment + Bn254::g1_generator(); - let proof = HyperKZGScheme::open( - &poly, - &f.point, - f.evaluation, - &pk, - Some(hint), - &mut transcript(), - ) - .unwrap(); - assert!(f.verify(&proof).is_err()); + let narg = open(&poly, &f.point, f.evaluation, &pk, Some(hint)).unwrap(); + assert!(f.verify(&narg).is_err()); } #[test] fn malformed_shape_and_arity_return_errors_before_transcript_mutation() { let f = Fixture::new(); - for row in 0..3 { - let mut proof = f.proof.clone(); - let _ = proof.v[row].pop(); - assert_eq!(f.verify(&proof), Err(HyperKZGError::ProofShape)); - proof.v[row].extend([Fr::zero(); 2]); - assert_eq!(f.verify(&proof), Err(HyperKZGError::ProofShape)); - } - let mut proof = f.proof.clone(); - let _ = proof.com.pop(); - assert_eq!(f.verify(&proof), Err(HyperKZGError::ProofShape)); + let narg = f.messages.encode(); + assert_eq!( + f.verify(&narg[..narg.len() - 1]), + Err(HyperKZGError::Transcript(TranscriptError::Truncated)) + ); + let trailing = [narg.as_slice(), &[0]].concat(); + assert_eq!( + f.verify(&trailing), + Err(HyperKZGError::Transcript(TranscriptError::TrailingBytes)) + ); for point in [ vec![], vec![Fr::one(); 5], vec![Fr::one(); usize::BITS as usize], ] { - let mut actual = transcript(); + let mut actual = verifier(&narg); assert_eq!( - HyperKZGScheme::verify_opening( - &f.commitment, - &point, - f.evaluation, - &f.proof, - &f.vk, - &mut actual - ), + HyperKZGScheme::verify_opening(&f.commitment, &point, f.evaluation, &f.vk, &mut actual), Err(HyperKZGError::InvalidArity) ); - assert_eq!(actual.challenge(), transcript().challenge()); + assert_eq!(actual.remaining(), narg.len()); + assert_eq!( + actual.challenge_bytes::<32>(), + verifier(&narg).challenge_bytes::<32>() + ); } } @@ -242,90 +301,19 @@ fn bad_imports_and_false_prover_claims_reject() { assert!(HyperKZGScheme::setup(params).is_err()); let (pk, _) = setup(7, 4); let poly = Polynomial::new(vec![Fr::one(); 4]); - assert!(HyperKZGScheme::open( - &poly, - &[Fr::zero(); 2], - Fr::zero(), - &pk, - None, - &mut transcript() - ) - .is_err()); - assert!(HyperKZGScheme::open( - &poly, - &[Fr::zero()], - Fr::one(), - &pk, - None, - &mut transcript() - ) - .is_err()); + assert!(open(&poly, &[Fr::zero(); 2], Fr::zero(), &pk, None).is_err()); + assert!(open(&poly, &[Fr::zero()], Fr::one(), &pk, None).is_err()); let zero = Polynomial::new(vec![Fr::zero(); 4]); let (commitment, _) = HyperKZGScheme::commit(&zero, &pk).unwrap(); - let proof = HyperKZGScheme::open( - &zero, - &[Fr::one(); 2], - Fr::zero(), - &pk, - None, - &mut transcript(), - ) - .unwrap(); + let narg = open(&zero, &[Fr::one(); 2], Fr::zero(), &pk, None).unwrap(); + let mut transcript = verifier(&narg); HyperKZGScheme::verify( &commitment, &[Fr::one(); 2], Fr::zero(), - &proof, &HyperKZGScheme::verifier_setup(&pk), - &mut transcript(), + &mut transcript, ) .unwrap(); -} - -#[derive(Default)] -struct ZeroTranscript; - -impl Transcript for ZeroTranscript { - type Challenge = Fr; - - fn new(_: &'static [u8]) -> Self { - Self - } - - fn append_bytes(&mut self, _: &[u8]) {} - - fn challenge(&mut self) -> Fr { - Fr::zero() - } - - fn state(&self) -> [u8; 32] { - [0; 32] - } -} - -#[test] -fn zero_fold_challenge_rejects_on_both_paths() { - let f = Fixture::new(); - assert_eq!( - HyperKZGScheme::verify_opening( - &f.commitment, - &f.point, - f.evaluation, - &f.proof, - &f.vk, - &mut ZeroTranscript, - ), - Err(HyperKZGError::DegenerateChallenge), - ); - let (pk, _) = setup(7, 4); - let poly = Polynomial::new(vec![Fr::one(); 4]); - assert!(HyperKZGScheme::open( - &poly, - &[Fr::one(); 2], - Fr::one(), - &pk, - None, - &mut ZeroTranscript, - ) - .is_err()); + transcript.finish().unwrap(); } diff --git a/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs b/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs index e96b9a84a3..73da27764d 100644 --- a/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs +++ b/crates/jolt-kernels/src/optimized/inc_claim_reduction.rs @@ -322,7 +322,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/ram_val_check.rs b/crates/jolt-kernels/src/optimized/ram_val_check.rs index 3c116d11b4..69bfbf10d0 100644 --- a/crates/jolt-kernels/src/optimized/ram_val_check.rs +++ b/crates/jolt-kernels/src/optimized/ram_val_check.rs @@ -256,6 +256,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| { @@ -266,6 +267,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-kernels/src/optimized/registers_val_evaluation.rs b/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs index d455d7cb2e..5422c3e3dd 100644 --- a/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs +++ b/crates/jolt-kernels/src/optimized/registers_val_evaluation.rs @@ -265,7 +265,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-kernels/src/reference/naive.rs b/crates/jolt-kernels/src/reference/naive.rs index 5c00a91448..ca44bd1e4a 100644 --- a/crates/jolt-kernels/src/reference/naive.rs +++ b/crates/jolt-kernels/src/reference/naive.rs @@ -449,11 +449,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, SumcheckProof, 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; @@ -642,7 +641,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; @@ -663,7 +663,7 @@ mod tests { assert_eq!(input_claim, claimed_sum); 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, @@ -724,37 +724,45 @@ mod tests { assert_eq!(coefficient * expected, proved.final_claim); assert_eq!(proved.member_claims, vec![expected]); - 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 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(); 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 = SumcheckProof::::from(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.challenge_bytes::<32>(), + prover_transcript.challenge_bytes::<32>() + ); + verifier_transcript.finish().unwrap(); } /// A derived table materialized at the wrong point survives the sumcheck @@ -767,7 +775,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; @@ -776,7 +785,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, diff --git a/crates/jolt-lookup-tables/fuzz/Cargo.lock b/crates/jolt-lookup-tables/fuzz/Cargo.lock index 8a052d697e..6e6dfbd34a 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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" diff --git a/crates/jolt-openings/Cargo.toml b/crates/jolt-openings/Cargo.toml index ee5e6bad5d..095d0bddcc 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 @@ -25,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/fuzz/Cargo.lock b/crates/jolt-openings/fuzz/Cargo.lock index bb473eb238..43c5f79411 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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,18 +518,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.8", "num-integer", "num-traits", "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -611,7 +538,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -621,6 +548,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -629,12 +557,13 @@ 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", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -670,9 +599,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 +615,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -701,13 +629,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.3", + "ark-ff", "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] @@ -750,7 +678,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 +1110,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", 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..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 @@ -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.challenge_bytes::<32>(), + verifier_transcript.challenge_bytes::<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.challenge_bytes::<32>(), + verifier_transcript.challenge_bytes::<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 3e02d7389f..d5951ea2d0 100644 --- a/crates/jolt-openings/src/claims.rs +++ b/crates/jolt-openings/src/claims.rs @@ -1,6 +1,6 @@ -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> { @@ -17,21 +17,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); } } @@ -41,16 +31,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 7d51a24b1d..c8489a05a3 100644 --- a/crates/jolt-openings/src/error.rs +++ b/crates/jolt-openings/src/error.rs @@ -1,3 +1,5 @@ +use jolt_transcript::TranscriptError; + #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum OpeningsError { #[error("opening proof verification failed")] @@ -17,4 +19,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 93bfc5860e..52f8735d3d 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}; @@ -181,14 +181,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!( @@ -205,15 +205,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 fd1e4b1cb1..42efd5cce9 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, @@ -49,9 +48,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; @@ -71,22 +74,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 @@ -110,13 +126,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(), )) @@ -124,12 +140,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(), @@ -332,7 +347,8 @@ pub trait ZkOpeningScheme: CommitmentScheme { + 'static + Serialize + DeserializeOwned - + AppendToTranscript; + + CanonicalBytes + + CanonicalDecode; type Blind: Clone + Send + Sync; @@ -344,27 +360,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; } @@ -417,36 +428,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, @@ -463,28 +468,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 @@ -509,23 +510,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() { @@ -537,13 +536,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, @@ -551,22 +550,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); @@ -574,11 +569,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(()) } } @@ -648,12 +642,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); } } @@ -671,18 +669,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( @@ -701,7 +698,7 @@ where commitments.len() ))); } - let scalars = transcript.challenge_scalar_powers(commitments.len()); + let scalars = transcript.challenge_powers(commitments.len()); let joint_eval = evaluations .iter() @@ -711,7 +708,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, @@ -719,42 +716,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 658d7906b8..98b705dd4b 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(&mut prover_transcript), + fingerprint(&mut 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,16 +71,54 @@ 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"); } +#[test] +fn dory_homomorphic_batch_rejects_mismatched_points() { + let (polynomials, point) = homomorphic_polynomials(4, 3, 0x71_00_01); + let prover_setup = DoryScheme::setup_prover(point.len()); + 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 = prover(b"dory-batch-point-mismatch"); + let result = ::prove_batch( + &prover_setup, + claims, + sources(&polynomials), + hints, + &mut transcript, + ); + assert!(matches!(result, Err(OpeningsError::InvalidBatch(_)))); +} + +#[test] +fn dory_homomorphic_batch_rejects_witness_count_mismatch() { + let (polynomials, point) = homomorphic_polynomials(3, 3, 0x71_00_05); + let prover_setup = DoryScheme::setup_prover(point.len()); + let (claims, mut hints) = clear_claims::(&polynomials, &point, &prover_setup); + let mut polynomial_sources = sources(&polynomials); + let _dropped = polynomial_sources.pop(); + let _dropped_hint = hints.pop(); + + let mut transcript = prover(b"dory-batch-witness-count"); + let result = ::prove_batch( + &prover_setup, + claims, + polynomial_sources, + hints, + &mut transcript, + ); + assert!(matches!(result, Err(OpeningsError::InvalidBatch(_)))); +} + #[test] fn dory_homomorphic_batch_rejects_wrong_witness_dimension() { let (polynomials, point) = homomorphic_polynomials(3, 3, 0x71_00_02); @@ -87,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, @@ -114,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(), @@ -126,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(&mut prover_transcript), + fingerprint(&mut 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; + fn zk_batch_fixture(seed: u64, label: &'static [u8]) -> ZkBatchFixture { let (polynomials, point) = homomorphic_polynomials(2, 2, seed); let prover_setup = DoryScheme::setup_prover(point.len()); @@ -162,8 +213,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(), @@ -178,7 +229,7 @@ fn zk_batch_fixture(seed: u64, label: &'static [u8]) -> ZkBatchFixture { point, commitments, verifier_setup, - proof: opening.proof, + narg: transcript.finish(), } } @@ -187,16 +238,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] @@ -219,7 +271,7 @@ fn dory_homomorphic_zk_batch_rejects_tampered_commitment() { label, fixture.point.clone(), tampered, - &fixture.proof, + &fixture.narg, ); assert!( matches!(result, Err(OpeningsError::VerificationFailed)), @@ -231,7 +283,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"); } @@ -250,7 +302,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)), @@ -262,7 +314,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"); } @@ -276,12 +328,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, @@ -297,8 +351,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, @@ -316,7 +370,39 @@ 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"); } + +#[test] +fn dory_homomorphic_zk_batch_rejects_witness_count_mismatch() { + let (polynomials, point) = homomorphic_polynomials(2, 2, 0x71_00_04); + let prover_setup = DoryScheme::setup_prover(point.len()); + let mut commitments = Vec::with_capacity(polynomials.len()); + let mut hints = Vec::with_capacity(polynomials.len()); + let mut evaluations = Vec::with_capacity(polynomials.len()); + for polynomial in &polynomials { + let (commitment, hint) = + ::commit_zk(polynomial, &prover_setup).unwrap(); + commitments.push(commitment); + hints.push(hint); + evaluations.push(polynomial.evaluate(&point)); + } + let mut polynomial_sources = sources(&polynomials); + let _dropped = polynomial_sources.pop(); + let _dropped_hint = hints.pop(); + let _dropped_eval = evaluations.pop(); + + let mut transcript = prover(b"dory-batch-zk-witness-count"); + let result = ::prove_batch_zk( + &prover_setup, + point, + commitments, + polynomial_sources, + hints, + evaluations, + &mut transcript, + ); + assert!(matches!(result, Err(OpeningsError::InvalidBatch(_)))); +} 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 c17727a815..ca44580979 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; @@ -35,9 +34,68 @@ fn unit_hints(count: usize) -> Vec<()> { vec![(); count] } +#[test] +fn homomorphic_batch_opening_roundtrip_clear() { + let (polynomials, point) = batch_polynomials(); + let (claims, _) = clear_claims::(&polynomials, &point, &()); + + let mut prover_transcript = prover(b"batch-clear"); + ::prove_batch( + &(), + claims.clone(), + sources(&polynomials), + unit_hints(polynomials.len()), + &mut prover_transcript, + ) + .expect("batch proof should be produced"); + + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"batch-clear", &narg); + ::verify_batch( + &(), + &claims, + &mut verifier_transcript, + ) + .expect("batch proof should verify"); + + assert_eq!( + fingerprint(&mut prover_transcript), + fingerprint(&mut verifier_transcript) + ); + verifier_transcript.finish().expect("proof fully consumed"); +} + +#[test] +fn homomorphic_batch_opening_rejects_tampered_clear_claim() { + let (polynomials, point) = batch_polynomials(); + let (claims, _) = clear_claims::(&polynomials, &point, &()); + + let mut prover_transcript = prover(b"batch-clear-tampered"); + ::prove_batch( + &(), + claims.clone(), + sources(&polynomials), + unit_hints(polynomials.len()), + &mut prover_transcript, + ) + .expect("batch proof should be produced"); + + let mut tampered = claims; + tampered[1].evaluation.value += fr(1); + + let narg = prover_transcript.finish(); + let mut verifier_transcript = verifier(b"batch-clear-tampered", &narg); + let result = ::verify_batch( + &(), + &tampered, + &mut verifier_transcript, + ); + assert!(result.is_err(), "tampered claim should fail"); +} + #[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(), @@ -54,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, @@ -70,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, @@ -99,12 +157,47 @@ fn evaluations(polynomials: &[Polynomial], point: &Point) - .collect() } +#[test] +fn homomorphic_batch_opening_roundtrip_zk() { + let (polynomials, point) = batch_polynomials(); + let commitments = zk_commitments(&polynomials); + + let mut prover_transcript = prover(b"batch-zk"); + let opening = ::prove_batch_zk( + &(), + point.clone(), + commitments.clone(), + sources(&polynomials), + unit_hints(polynomials.len()), + evaluations(&polynomials, &point), + &mut prover_transcript, + ) + .expect("ZK batch proof should be produced"); + + let narg = prover_transcript.narg().to_vec(); + let mut verifier_transcript = verifier(b"batch-zk", &narg); + let verifier_hiding = ::verify_batch_zk( + &(), + point, + commitments, + &mut verifier_transcript, + ) + .expect("ZK batch proof should verify"); + + assert_eq!(verifier_hiding, opening.hiding_commitment); + assert_eq!( + fingerprint(&mut prover_transcript), + fingerprint(&mut verifier_transcript) + ); + verifier_transcript.finish().expect("proof fully consumed"); +} + #[test] 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..25f05079d5 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: &mut C) -> [u8; 32] { + channel.challenge_bytes::<32>() +} + 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..3fe5d3ea9f 100644 --- a/crates/jolt-openings/tests/support/mock.rs +++ b/crates/jolt-openings/tests/support/mock.rs @@ -1,11 +1,14 @@ 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}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] @@ -25,18 +28,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).to_le_bytes()); + 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(u64::from_le_bytes(transcript.receive()?)) + .map_err(|_| OpeningsError::VerificationFailed)?; + Ok(transcript.receive_n(len)?) } impl Commitment for MockCommitmentScheme @@ -51,7 +57,6 @@ where F: JoltField + Serialize + DeserializeOwned, { type Field = F; - type Proof = MockProof; type ProverSetup = (); type VerifierSetup = (); type OpeningHint = (); @@ -71,31 +76,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 +167,29 @@ 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 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 NargDeserialize for MockHidingCommitment { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + jolt_field::narg::deserialize(buf) } } @@ -166,31 +207,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/fuzz/Cargo.lock b/crates/jolt-poly/fuzz/Cargo.lock index 510cc03a82..44f1851882 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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" diff --git a/crates/jolt-poly/src/evaluation_claim.rs b/crates/jolt-poly/src/evaluation_claim.rs index 76032b5098..b3cdb97330 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}; - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct EvaluationClaim { pub point: Point, @@ -19,18 +16,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); - } -} diff --git a/crates/jolt-prover/Cargo.toml b/crates/jolt-prover/Cargo.toml index 2226777fa3..3f0a867c3a 100644 --- a/crates/jolt-prover/Cargo.toml +++ b/crates/jolt-prover/Cargo.toml @@ -139,6 +139,7 @@ tracer = { workspace = true, optional = true } bincode.workspace = true jolt-inlines-keccak256 = { workspace = true, features = ["host"] } jolt-inlines-sha2 = { workspace = true, features = ["host"] } +jolt-transcript = { workspace = true, features = ["transcript-keccak"] } jolt-platform.workspace = true sha2.workspace = true sha3.workspace = true diff --git a/crates/jolt-prover/src/akita/preprocessing.rs b/crates/jolt-prover/src/akita/preprocessing.rs index e29273c159..5d2585eedb 100644 --- a/crates/jolt-prover/src/akita/preprocessing.rs +++ b/crates/jolt-prover/src/akita/preprocessing.rs @@ -12,7 +12,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, }; @@ -25,7 +24,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 8281511ab1..7adaa9d625 100644 --- a/crates/jolt-prover/src/akita/prover.rs +++ b/crates/jolt-prover/src/akita/prover.rs @@ -1,16 +1,16 @@ //! The Akita top-level prover: the stage recipes run in protocol order on -//! one transcript and one backend session, and their wire outputs assemble -//! into the Akita-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_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 super::stage0::prove_stage0; use super::stage8::prove_stage8; @@ -29,28 +29,27 @@ use jolt_kernels::{JoltBackend, KernelContext}; /// See [`super::prove`]. #[tracing::instrument(skip_all, name = "jolt_prover::prove", fields(trace_length = config.trace_length))] -pub fn prove( +pub fn prove( mut backend: KernelContext<'_, F, JoltBackend>, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&AdviceObject>, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result> where - F: JoltField + CanonicalBytes + AppendToTranscript, + F: JoltField, PCS: CommitmentScheme + TransparentObjectSetup, PCS::ProverSetup: GroupSetupMetadata, PCS::VerifierSetup: GroupSetupMetadata, - PCS::Output: Clone + PartialEq + AppendToTranscript + GroupCommitmentMetadata, + PCS::Output: Clone + PartialEq + GroupCommitmentMetadata, VC: VectorCommitment, - VC::Output: Clone + AppendToTranscript, - T: Transcript, + H: Sponge, { // The Akita path is transparent-only (`akita` and `zk` are mutually // exclusive), so the mode context carries nothing; the shared stage // recipes still thread it to mint their clear recorders. let mode = ProofMode::::new(None)?; - let stage0 = prove_stage0::( + let stage0 = prove_stage0::( &mut backend, preprocessing, config, @@ -62,9 +61,9 @@ where let checked = stage0.checked; let mut transcript = stage0.transcript; - let stage1 = prove_stage1::(&mut backend, &mode, log_t, &mut transcript)?; + let stage1 = prove_stage1::(&mut backend, &mode, log_t, &mut transcript)?; finish_stage("stage1", log_t, backend.session(), &stage1.clear_output); - let stage2 = prove_stage2::( + let stage2 = prove_stage2::( &mut backend, &mode, config, @@ -73,7 +72,7 @@ where &mut transcript, )?; finish_stage("stage2", log_t, backend.session(), &stage2.clear_output); - let stage3 = prove_stage3::( + let stage3 = prove_stage3::( &mut backend, &mode, config, @@ -82,7 +81,7 @@ where &mut transcript, )?; finish_stage("stage3", log_t, backend.session(), &stage3.clear_output); - let stage4 = prove_stage4::( + let stage4 = prove_stage4::( &mut backend, &mode, &checked, @@ -93,7 +92,7 @@ where &mut transcript, )?; finish_stage("stage4", log_t, backend.session(), &stage4.clear_output); - let stage5 = prove_stage5::( + let stage5 = prove_stage5::( &mut backend, &mode, &checked, @@ -104,7 +103,7 @@ where &mut transcript, )?; finish_stage("stage5", log_t, backend.session(), &stage5.clear_output); - let stage6a = prove_stage6a::( + let stage6a = prove_stage6a::( &mut backend, &mode, &checked, @@ -118,7 +117,7 @@ where &mut transcript, )?; finish_stage("stage6a", log_t, backend.session(), &stage6a.clear_output); - let stage6b = prove_stage6b::( + let stage6b = prove_stage6b::( &mut backend, &mode, &checked, @@ -133,7 +132,7 @@ where &mut transcript, )?; finish_stage("stage6b", log_t, backend.session(), &stage6b.clear_output); - let stage7 = prove_stage7::( + let stage7 = prove_stage7::( &mut backend, &mode, &checked, @@ -144,7 +143,7 @@ where &mut transcript, )?; finish_stage("stage7", log_t, backend.session(), &stage7.clear_output); - let joint_opening_proof = prove_stage8::( + prove_stage8::( &checked, config, preprocessing, @@ -168,39 +167,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 f10c68facc..8e35c607fd 100644 --- a/crates/jolt-prover/src/akita/stage0.rs +++ b/crates/jolt-prover/src/akita/stage0.rs @@ -1,3 +1,11 @@ +//! Akita stage 0: input validation, the Fiat-Shamir preamble, and the +//! Akita 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 jolt_kernels::akita::commitment::{WitnessCommitRequest, WitnessCommitment}; use jolt_kernels::{JoltBackend, KernelContext}; @@ -7,10 +15,11 @@ use jolt_claims::protocols::jolt::{JoltAdviceKind, JoltRelationId, TracePolynomi use jolt_crypto::VectorCommitment; use jolt_field::JoltField; use jolt_openings::{CommitmentScheme, GroupSetupMetadata, TransparentObjectSetup}; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::{COMMITMENTS, PREAMBLE}; use jolt_verifier::{ - absorb_akita_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 super::witness::{commit_advice, AdviceObject}; @@ -18,12 +27,15 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError}; #[cfg(feature = "field-inline")] use jolt_kernels::akita::commitment::FieldIncObject; -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>, @@ -32,23 +44,26 @@ where pub field_inc: FieldIncObject, } -/// Validate inputs, commit the native trace group and auxiliary objects, and seed the transcript. +/// Validate inputs, send the proof header and absorb the public preamble, +/// commit the native trace group and auxiliary 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( backend: &mut KernelContext<'_, F, JoltBackend>, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&AdviceObject>, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme + TransparentObjectSetup, PCS::ProverSetup: GroupSetupMetadata, PCS::VerifierSetup: GroupSetupMetadata, - PCS::Output: Clone + AppendToTranscript, + PCS::Output: Clone, VC: VectorCommitment, - T: Transcript, + H: Sponge, { if config.akita_chunk_profile.num_chunks() != preprocessing.verifier.pcs_setup.one_hot_num_chunks() @@ -94,27 +109,25 @@ 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); + transcript.site(PREAMBLE); + 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(); @@ -170,18 +183,19 @@ where )?) })?; - absorb_akita_commitments( - &commitment, - untrusted_advice.as_ref().map(|object| &object.commitment), - trusted_advice.map(|object| &object.commitment), + transcript.site(COMMITMENTS); + ProofCommitments { + one_hot_trace: commitment.clone(), #[cfg(feature = "field-inline")] - Some(&field_inc.commitment), - preprocessing.verifier.program.committed().map(|committed| { - ( - &committed.bytecode_commitment, - &committed.program_image_commitment, - ) - }), + 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 6738b4954f..f9a42950b3 100644 --- a/crates/jolt-prover/src/akita/stage8.rs +++ b/crates/jolt-prover/src/akita/stage8.rs @@ -13,7 +13,8 @@ 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::sites::STAGE8; use jolt_verifier::stages::stage4::outputs::Stage4ClearOutput; use jolt_verifier::stages::stage6b::outputs::Stage6bClearOutput; use jolt_verifier::stages::stage7::outputs::Stage7ClearOutput; @@ -33,14 +34,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::)?; @@ -51,7 +52,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, @@ -64,15 +65,16 @@ 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, { + 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( @@ -149,7 +151,7 @@ where } } - 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, @@ -158,6 +160,5 @@ where transcript, ) .map_err(batch_failed::) - })?; - Ok(joint_opening_proof) + }) } diff --git a/crates/jolt-prover/src/blindfold.rs b/crates/jolt-prover/src/blindfold.rs index ef2bc86ed2..67f41c5ea8 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,16 @@ //! 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::composed::r1cs::SPARTAN_OUTER_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_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::sites::BLINDFOLD; +use jolt_verifier::{jolt_protocol_id, verify_stages, VerifiedStages, VerifierError, JOLT_SESSION}; use crate::{JoltProverPreprocessing, ProverError}; @@ -97,40 +90,51 @@ 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, + checkpoint, + } = 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", + }); + }; + // 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", }); @@ -151,11 +155,11 @@ where .ok_or(ProverError::Verifier( VerifierError::MissingVectorCommitmentSetup, ))?; - transcript.append(&Label(b"BlindFold")); - let proof = jolt_blindfold::prove::( + transcript.site(BLINDFOLD); + jolt_blindfold::prove::( vc_setup, &protocol, - &mut transcript, + transcript, BlindFoldWitness { rows: &assigned.rows, blindings: &assigned.blindings, @@ -164,124 +168,5 @@ 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(()) } diff --git a/crates/jolt-prover/src/dory/prover.rs b/crates/jolt-prover/src/dory/prover.rs index 636b34b495..d716da95b3 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, KernelContext}; 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 crate::boundary::finish_stage; use crate::dory::stages::stage0::{prove_stage0, TrustedAdviceCommitment}; @@ -28,11 +27,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), `backend` binds the witness used by the kernels, +/// `config` is the derived proof shape (sent as the proof header), `backend` binds the witness used by the kernels, /// and `public_io` the Fiat-Shamir preamble's program I/O. /// /// `trusted_advice` is the externally supplied (preprocessing-time) @@ -49,26 +47,26 @@ use crate::{JoltProverPreprocessing, ProverConfig, ProverError}; /// bytecode/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( mut backend: KernelContext<'_, F, JoltBackend>, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&TrustedAdviceCommitment>, 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, ::Accumulator: Accumulator, { let mode = ProofMode::::new(preprocessing.verifier.vc_setup.as_ref())?; - let stage0 = prove_stage0::( + let stage0 = prove_stage0::( &mut backend, preprocessing, config, @@ -80,9 +78,9 @@ where let checked = stage0.checked; let mut transcript = stage0.transcript; - let stage1 = prove_stage1::(&mut backend, &mode, log_t, &mut transcript)?; + let stage1 = prove_stage1::(&mut backend, &mode, log_t, &mut transcript)?; finish_stage("stage1", log_t, backend.session(), &stage1.clear_output); - let stage2 = prove_stage2::( + let stage2 = prove_stage2::( &mut backend, &mode, config, @@ -91,7 +89,7 @@ where &mut transcript, )?; finish_stage("stage2", log_t, backend.session(), &stage2.clear_output); - let stage3 = prove_stage3::( + let stage3 = prove_stage3::( &mut backend, &mode, config, @@ -100,7 +98,7 @@ where &mut transcript, )?; finish_stage("stage3", log_t, backend.session(), &stage3.clear_output); - let stage4 = prove_stage4::( + let stage4 = prove_stage4::( &mut backend, &mode, &checked, @@ -111,7 +109,7 @@ where &mut transcript, )?; finish_stage("stage4", log_t, backend.session(), &stage4.clear_output); - let stage5 = prove_stage5::( + let stage5 = prove_stage5::( &mut backend, &mode, &checked, @@ -122,7 +120,7 @@ where &mut transcript, )?; finish_stage("stage5", log_t, backend.session(), &stage5.clear_output); - let stage6a = prove_stage6a::( + let stage6a = prove_stage6a::( &mut backend, &mode, &checked, @@ -136,7 +134,7 @@ where &mut transcript, )?; finish_stage("stage6a", log_t, backend.session(), &stage6a.clear_output); - let stage6b = prove_stage6b::( + let stage6b = prove_stage6b::( &mut backend, &mode, &checked, @@ -151,7 +149,7 @@ where &mut transcript, )?; finish_stage("stage6b", log_t, backend.session(), &stage6b.clear_output); - let stage7 = prove_stage7::( + let stage7 = prove_stage7::( &mut backend, &mode, &checked, @@ -162,13 +160,12 @@ where &mut transcript, )?; finish_stage("stage7", log_t, backend.session(), &stage7.clear_output); - let stage8 = prove_stage8::( + let stage8 = prove_stage8::( &mut backend, &checked, config, preprocessing, &stage0.commitments, - stage0.untrusted_advice_commitment.as_ref(), trusted_advice.map(|trusted| &trusted.commitment), stage0.hints, #[cfg(feature = "field-inline")] @@ -180,66 +177,12 @@ where finish_stage("stage8", log_t, backend.session(), &()); backend.finish(); - 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, @@ -256,16 +199,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 00d7021892..6fcc5b75c8 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")] @@ -20,13 +21,14 @@ use jolt_kernels::reference::bytecode_read_raf::BytecodeReadRafWitness; use jolt_kernels::reference::instruction_read_raf::InstructionReadRafWitness; use jolt_kernels::{CommitmentGrid, JoltBackend, KernelContext, WitnessCommitment}; use jolt_openings::CommitmentScheme; -use jolt_transcript::{AppendToTranscript, Transcript}; +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_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::WitnessBundle; @@ -43,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`. @@ -62,25 +63,23 @@ 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 bytecode/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: &mut KernelContext<'_, F, JoltBackend>, preprocessing: &JoltProverPreprocessing, config: &ProverConfig, trusted_advice: Option<&TrustedAdviceCommitment>, public_io: &JoltDevice, -) -> Result, ProverError> +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, - PCS::Output: AppendToTranscript, VC: VectorCommitment, - T: Transcript, + H: Sponge, { // Committed-program mode needs the prover-retained full program + hints; // require presence to agree with the verifier preprocessing's mode. @@ -114,21 +113,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 @@ -156,16 +156,10 @@ 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); + transcript.site(PREAMBLE); + header.send(&mut transcript); + absorb_public_preamble(&checked, &mut transcript); let dimensions = crate::stages::formula_dimensions( &checked, @@ -201,20 +195,17 @@ where total_vars = grid.total_vars ) .in_scope(|| backend.commit_witness(&ids, grid, &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, grid, &preprocessing.pcs_setup)?; - ( - commitments.with_field_inline(field_inline), - field_inline_hints, - ) - }; + let (field_inline, field_inline_hints) = + commit_field_inline::(backend, 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 @@ -261,25 +252,22 @@ where )); } - absorb_transcript_commitments( - &commitments, - untrusted_advice_commitment.as_ref(), + let commitments = ProofCommitments { + trace, + untrusted_advice: untrusted_advice_commitment, + }; + transcript.site(COMMITMENTS); + 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_commitment, - &committed.program_image_commitment, - &mut transcript, - ); - } Ok(Stage0Output { checked, transcript, commitments, - untrusted_advice_commitment, hints, #[cfg(feature = "field-inline")] field_inline_hints, @@ -342,6 +330,7 @@ where )] fn assemble_commitments( committed: Vec>, + #[cfg(feature = "field-inline")] field_inline: FieldInlineCommitments, ) -> Result< ( JoltCommitments, @@ -389,7 +378,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, )) } @@ -407,7 +404,6 @@ mod field_inline_tests { use jolt_kernels::finish_streamed; use jolt_kernels::KernelError; use jolt_openings::{CommitmentScheme, StreamingCommitment}; - use jolt_transcript::LegacyBlake2bTranscript; fn grid() -> CommitmentGrid { CommitmentGrid { @@ -429,13 +425,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 context = backend.with_witness(&witness); @@ -444,13 +438,11 @@ mod field_inline_tests { let ids: Vec = witness.metadata().committed_polynomial_order().unwrap(); let committed = context.commit_witness(&ids, grid(), &setup).unwrap(); - let (commitments, _hints) = assemble_commitments::(committed).unwrap(); - let (field_inline, field_inline_hints) = commit_field_inline::(&mut context, grid(), &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() @@ -474,38 +466,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) ); - - 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" - ); } #[test] diff --git a/crates/jolt-prover/src/dory/stages/stage8.rs b/crates/jolt-prover/src/dory/stages/stage8.rs index a6ca7dff7c..36ea65cd26 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 @@ -44,37 +44,42 @@ use jolt_openings::{ VerifierOpeningClaim, ZkOpeningScheme, }; use jolt_poly::Point; -use jolt_transcript::Transcript; -use jolt_verifier::proof::JoltCommitments; +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}; use jolt_verifier::{CheckedInputs, VerifierError}; use crate::{CommittedProgramCandidates, JoltProverPreprocessing, ProverConfig, ProverError}; +#[cfg(not(feature = "zk"))] +use core::marker::PhantomData; -/// 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: 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: &mut KernelContext<'_, F, JoltBackend>, 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<( @@ -83,15 +88,16 @@ pub fn prove_stage8( )>, stage6b: &Stage6bClearOutput, stage7: &Stage7ClearOutput, - transcript: &mut T, + transcript: &mut ProverTranscript, ) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme + AdditivelyHomomorphic + ZkOpeningScheme, PCS::Output: HomomorphicCommitment, VC: VectorCommitment, - T: Transcript, + 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( @@ -141,8 +147,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, @@ -159,7 +165,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(), @@ -293,7 +299,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(), @@ -302,9 +308,7 @@ where ) .map_err(KernelError::::from)?; - Ok(Stage8ProverOutput { - joint_opening_proof, - }) + Ok(Stage8ProverOutput { _pcs: PhantomData }) } #[cfg(feature = "zk")] { @@ -328,7 +332,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 c3280ac8ca..6d9c714fc5 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. //! @@ -31,16 +31,15 @@ use jolt_kernels::{ KernelContext, PrepareKernel, 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, }; use jolt_verifier::VerifierError; +use crate::recorder::ClaimRecorder; use crate::ProverError; /// The generated per-stage driver: implemented for each batch struct by @@ -59,13 +58,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: &mut KernelContext<'_, F, B>, scheduler: &mut dyn RoundScheduler, @@ -73,24 +72,13 @@ 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: ?Sized, for<'a> KernelContext<'a, F, B>: KernelSource, - Rec: SumcheckRecorder, - T: Transcript; - - /// The stage's absorbed 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>; + Rec: SumcheckRecorder + ClaimRecorder, + H: Sponge; } /// The per-stage kernel-source bound collector: `impl_stage_prover!` emits @@ -111,12 +99,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, P> { - 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, @@ -404,17 +392,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 @@ -422,25 +399,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 = { @@ -450,7 +411,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 },)+ ] @@ -463,7 +423,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: &mut ::jolt_kernels::KernelContext<'_, F, B>, scheduler: &mut dyn ::jolt_sumcheck::RoundScheduler, @@ -471,16 +431,16 @@ 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: ?Sized, for<'a> ::jolt_kernels::KernelContext<'a, F, B>: $crate::driver::KernelSource, - Rec: ::jolt_sumcheck::SumcheckRecorder, - T: ::jolt_transcript::Transcript, + Rec: ::jolt_sumcheck::SumcheckRecorder + $crate::recorder::ClaimRecorder, + H: ::jolt_transcript::Sponge, { let __stage_span = ::tracing::info_span!(concat!($label, "::prove")).entered(); @@ -532,9 +492,13 @@ macro_rules! impl_stage_prover { }; $($crate::driver::__stage_member!(park $presence $member, kernels);)+ - 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, @@ -557,26 +521,14 @@ macro_rules! impl_stage_prover { ); } - let __recorded = recorder.finish(&__opening_values, transcript)?; + let __witness = recorder.finish(&__claim_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, }) } - - 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 ::jolt_kernels::KernelContext<'_, F, B> @@ -604,4 +556,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/profile.rs b/crates/jolt-prover/src/profile.rs index fa048b1792..6d9b6e8257 100644 --- a/crates/jolt-prover/src/profile.rs +++ b/crates/jolt-prover/src/profile.rs @@ -69,8 +69,7 @@ use jolt_riscv::RV64IMAC_JOLT_FIELD_INLINE; 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; @@ -864,7 +863,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.with_witness(&witness), &prover_preprocessing, &config, @@ -890,7 +889,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, @@ -920,7 +919,7 @@ fn prove_workload( backend: BackendKind, chunk_profile: AkitaChunkProfile, ) -> 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; @@ -988,7 +987,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.with_witness(&witness), &prover_preprocessing, &config, @@ -998,23 +997,17 @@ fn prove_workload( .expect("modular Akita 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("Akita opening component lengths must fit usize"); let proof_size = bincode::serde::encode_to_vec(&proof, bincode::config::standard()) .expect("serialize Akita proof") .len(); tracing::info!( - akita_proof_body_size, - akita_opening_unframed_size, + argument_string_size = proof.narg.len(), jolt_proof_wire_size = proof_size, "Akita proof sizes" ); let verify = || { - jolt_verifier::verify::( + jolt_verifier::verify::( &prover_preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-prover/src/recorder.rs b/crates/jolt-prover/src/recorder.rs index 9743423cff..86abf5cf06 100644 --- a/crates/jolt-prover/src/recorder.rs +++ b/crates/jolt-prover/src/recorder.rs @@ -14,32 +14,55 @@ 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}; +use jolt_sumcheck::prove_uniskip_clear; +use jolt_sumcheck::ClearSumcheckRecorder; #[cfg(feature = "zk")] -use jolt_sumcheck::{ - prove_uniskip_committed, CommittedSumcheckRecorder, CommittedSumcheckWitness, RecordedSumcheck, -}; -use jolt_transcript::{AppendToTranscript, Transcript}; -#[cfg(feature = "zk")] -use rand_core::OsRng; +use jolt_sumcheck::{prove_uniskip_committed, CommittedSumcheckRecorder, CommittedSumcheckWitness}; +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: compressed clear proofs or committed -/// proofs with retained witnesses for BlindFold. +/// The compiled mode's batch recorder type. #[cfg(feature = "zk")] pub type ModeRecorder<'a, VC> = CommittedSumcheckRecorder<'a, ::Field, VC, OsRng>; #[cfg(not(feature = "zk"))] 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, +/// 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 CommittedSumcheckRecorder<'_, F, VC, R> +where + F: JoltField, + VC: VectorCommitment, + R: 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). +pub struct ProvedUniskipMode { pub challenge: F, pub output_claim: F, #[cfg(feature = "zk")] @@ -87,28 +110,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, @@ -118,7 +133,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, @@ -127,7 +141,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, @@ -135,28 +149,9 @@ impl<'a, VC: VectorCommitment> ProofMode<'a, VC> { transcript, )?; Ok(ProvedUniskipMode { - proof: proved.proof.into(), 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 6dd7504f07..6293865520 100644 --- a/crates/jolt-prover/src/stages/drivers.rs +++ b/crates/jolt-prover/src/stages/drivers.rs @@ -60,10 +60,6 @@ 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. jolt_verifier::stage4_sumchecks_members!(impl_stage_prover); } @@ -97,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::{ @@ -120,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 { @@ -192,11 +167,10 @@ mod twin_tests { SumcheckKernel, SumcheckKernelError, }; use jolt_poly::UnivariatePoly; - use jolt_sumcheck::{ - ClearSumcheckRecorder, CompressedSumcheckProof, ProveRounds, SequentialRounds, - SumcheckError, SumcheckProof, + use jolt_sumcheck::{ClearSumcheckRecorder, ProveRounds, SequentialRounds, SumcheckError}; + use jolt_transcript::{ + Blake2b512, Channel as _, ProtocolId, ProverTranscript, VerifierTranscript, }; - use jolt_transcript::{Blake2bTranscript, Transcript}; use jolt_verifier::stages::relations::{ConcreteSumcheck, SumcheckBatch, SumcheckOutputClaims}; use jolt_verifier::VerifierError; use jolt_witness::{ @@ -735,6 +709,14 @@ mod twin_tests { } } + 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" @@ -742,7 +724,7 @@ mod twin_tests { fn drive( beta: bool, ) -> ( - Proved, CompressedSumcheckProof>, + Proved, ()>, Vec<&'static str>, Vec, ) { @@ -757,7 +739,8 @@ mod twin_tests { ))); let mut context = KernelContext::new(&kernels, NoWitness, session); - 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 @@ -773,24 +756,27 @@ mod twin_tests { .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 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 _ = sumchecks + let (_, received_claims) = sumchecks .verify_clear( &inputs, &input_points, &verifier_challenges, - &proved.output_claims, - &SumcheckProof::::from(proved.recorded.proof.clone()), &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.challenge_bytes::<32>(), + prover_transcript.challenge_bytes::<32>() + ); + verifier_transcript.finish().unwrap(); let calls = context .session() @@ -868,7 +854,8 @@ mod twin_tests { let session = ProofSession::default(); let mut context = KernelContext::new(&kernels, NoWitness, session); - 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 @@ -883,22 +870,25 @@ mod twin_tests { ) .unwrap(); - let mut verifier_transcript = Blake2bTranscript::new(b"prove-driver-head-twin"); + 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 = sumchecks + let (verified_points, received_claims) = sumchecks .verify_clear( &inputs, &input_points, &verifier_challenges, - &proved.output_claims, - &SumcheckProof::::from(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.challenge_bytes::<32>(), + prover_transcript.challenge_bytes::<32>() + ); + verifier_transcript.finish().unwrap(); assert_eq!(verified_points, proved.output_points); assert_eq!( proved.output_points.delta.value.as_slice(), @@ -933,7 +923,8 @@ mod twin_tests { let sumchecks = fixture(false); let inputs = inputs(false); let kernels = toy_kernels(); - 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( diff --git a/crates/jolt-prover/src/stages/field_inline_fixtures.rs b/crates/jolt-prover/src/stages/field_inline_fixtures.rs index 5218762e45..0fb237814b 100644 --- a/crates/jolt-prover/src/stages/field_inline_fixtures.rs +++ b/crates/jolt-prover/src/stages/field_inline_fixtures.rs @@ -21,10 +21,14 @@ use std::sync::Arc; use crate::JoltBackend; 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; #[cfg(feature = "akita")] use jolt_kernels::{ CommitWitness, KernelError, ProofSession, WitnessCommitRequest, WitnessCommitment, @@ -39,9 +43,13 @@ use jolt_riscv::{ NormalizedOperands, RamAccess, RamWrite, RegisterRead, RegisterState, RegisterWrite, TraceRowError, 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}; #[cfg(feature = "akita")] use jolt_witness::JoltWitnessPlane; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; @@ -56,6 +64,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, @@ -379,9 +389,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)), @@ -394,24 +402,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, @@ -464,471 +477,110 @@ 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::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_field::Fr; - 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: &mut ProverTranscript, +) { + let narg = prover.narg().to_vec(); + let mut transcript = VerifierTranscript::::new( + &jolt_protocol_id::(), + JOLT_SESSION, + &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 Akita 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.challenge_bytes::<32>(), + prover.challenge_bytes::<32>(), + "verifier and prover sponge states diverge" + ); + transcript.finish().unwrap(); } pub(crate) mod proving { @@ -940,18 +592,15 @@ 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::KernelContext; - use jolt_transcript::LegacyBlake2bTranscript as Blake2bTranscript; - 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, 'w> { pub(crate) backend: &'a mut KernelContext<'w, Fr, JoltBackend>, @@ -959,20 +608,20 @@ 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) transcript: &'a mut Blake2bTranscript, + pub(crate) preprocessing: &'a FixturePreprocessing, + 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.mode, LOG_T, self.transcript, ) .unwrap(); - let stage2 = prove_stage2::, Blake2bTranscript>( + let stage2 = prove_stage2::, JoltSponge>( self.backend, self.mode, self.config, @@ -981,7 +630,7 @@ pub(crate) mod proving { self.transcript, ) .unwrap(); - let stage3 = prove_stage3::, Blake2bTranscript>( + let stage3 = prove_stage3::, JoltSponge>( self.backend, self.mode, self.config, @@ -994,7 +643,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.mode, self.checked, @@ -1009,7 +658,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.mode, self.checked, @@ -1024,7 +673,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.mode, self.checked, diff --git a/crates/jolt-prover/src/stages/stage1.rs b/crates/jolt-prover/src/stages/stage1.rs index da18c228ae..968813b0db 100644 --- a/crates/jolt-prover/src/stages/stage1.rs +++ b/crates/jolt-prover/src/stages/stage1.rs @@ -17,25 +17,21 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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, }; use jolt_verifier::stages::stage1::outputs::{ - Stage1BatchInputClaims, Stage1BatchSumchecks, Stage1ClearOutput, Stage1OutputClaims, + Stage1BatchInputClaims, Stage1BatchSumchecks, Stage1ClearOutput, }; use jolt_verifier::stages::uniskip::draw_spartan_outer_tau; 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. -pub struct Stage1ProverOutput { - pub uniskip_proof: SumcheckProof, - pub sumcheck_proof: SumcheckProof, - pub claims: Stage1OutputClaims, +/// Stage 1's outputs: the verifier-typed cross-stage carrier downstream stages consume. +pub struct Stage1ProverOutput { pub clear_output: Stage1ClearOutput, #[cfg(feature = "zk")] pub uniskip_witness: CommittedSumcheckWitness, @@ -45,18 +41,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: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, log_t: usize, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + 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 @@ -101,16 +98,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.into(); + 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")] uniskip_witness: proved_uniskip.witness, @@ -119,18 +110,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; @@ -138,20 +122,23 @@ mod field_inline_round_trip { use jolt_dory::DoryScheme; use jolt_field::Fr; use jolt_poly::Polynomial; - 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::{field_arithmetic_backend, LOG_T}; + use crate::stages::field_inline_fixtures::{ + 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 = crate::stages::field_inline_fixtures::reference_backend(); let mut context = backend.with_witness(&witness); 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>( &mut context, &mode, LOG_T, @@ -159,7 +146,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 @@ -176,50 +163,49 @@ mod field_inline_round_trip { assert_eq!(Polynomial::::new(table).evaluate(&tau_low), value); } - 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, + &mut prover_transcript, + ); } #[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 = crate::stages::field_inline_fixtures::reference_backend(); let mut context = backend.with_witness(&witness); - 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>( &mut context, &mode, LOG_T, @@ -227,17 +213,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); + // capacity-sized chunks — the layout the verifier's generated + // `committed_claim_layout` derives. let row_lens: Vec = out .committed_witness .output_claim_rows @@ -255,48 +234,12 @@ 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) - ); - 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(), + &mut prover_transcript, + ); } } diff --git a/crates/jolt-prover/src/stages/stage2.rs b/crates/jolt-prover/src/stages/stage2.rs index c94ced69e2..4559bad613 100644 --- a/crates/jolt-prover/src/stages/stage2.rs +++ b/crates/jolt-prover/src/stages/stage2.rs @@ -26,16 +26,14 @@ 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::{Channel, ProverTranscript, Sponge}; +use jolt_verifier::sites::STAGE2; use jolt_verifier::stages::relations::ConcreteSumcheck; 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, @@ -50,12 +48,8 @@ use jolt_verifier::VerifierError; 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. -pub struct Stage2ProverOutput { - pub uniskip_proof: SumcheckProof, - pub sumcheck_proof: SumcheckProof, - pub claims: Stage2OutputClaims, +/// Stage 2's outputs: the verifier-typed cross-stage carrier downstream stages consume. +pub struct Stage2ProverOutput { pub clear_output: Stage2ClearOutput, #[cfg(feature = "zk")] pub uniskip_witness: CommittedSumcheckWitness, @@ -65,20 +59,21 @@ pub struct Stage2ProverOutput { /// Prove stage 2 on `transcript` (positioned at the stage-1 boundary). #[tracing::instrument(skip_all)] -pub fn prove_stage2( +pub fn prove_stage2( backend: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, config: &ProverConfig, public_io: &JoltDevice, stage1: &Stage1ClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + 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); @@ -173,15 +168,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.into(); + 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, output_points: proved.output_points, @@ -194,27 +183,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; @@ -227,15 +209,15 @@ 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>( &mut context, &mode, LOG_T, &mut prover_transcript, ) .unwrap(); - let out = prove_stage2::, Blake2bTranscript>( + let out = prove_stage2::, JoltSponge>( &mut context, &mode, &config, @@ -245,67 +227,65 @@ mod field_inline_round_trip { ) .unwrap(); - let appendage = out - .claims - .batch_outputs + let appendage = &out + .clear_output + .output_values .product_remainder - .field_inline - .clone(); - let reduction = &out.claims.batch_outputs.field_registers_claim_reduction; + .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); - 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(), + &mut prover_transcript, + ); } } +/// 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_declared_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = crate::stages::field_inline_fixtures::reference_backend(); let mut context = backend.with_witness(&witness); - 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>( &mut context, &mode, LOG_T, &mut prover_transcript, ) .unwrap(); - let out = prove_stage2::, Blake2bTranscript>( + let out = prove_stage2::, JoltSponge>( &mut context, &mode, &config, @@ -315,106 +295,19 @@ 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(), 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"); - { - 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(); + .map(Vec::len) + .sum(); + assert_eq!(value_count, 18); - assert_eq!(transcript.state(), prover_transcript.state()); + verify_through( + Through::Stage2, + &test_checked_inputs(), + &field_arithmetic_preprocessing(), + &mut prover_transcript, + ); } } diff --git a/crates/jolt-prover/src/stages/stage3.rs b/crates/jolt-prover/src/stages/stage3.rs index c8fca8f3eb..464cc5ba64 100644 --- a/crates/jolt-prover/src/stages/stage3.rs +++ b/crates/jolt-prover/src/stages/stage3.rs @@ -15,25 +15,20 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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::{ - InstructionInput, RegistersClaimReduction, SpartanShift, Stage3ClearOutput, Stage3OutputClaims, - Stage3Sumchecks, + InstructionInput, RegistersClaimReduction, SpartanShift, Stage3ClearOutput, Stage3Sumchecks, }; use jolt_verifier::stages::stage3::stage3_input_values_from_upstream; 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. -pub struct Stage3ProverOutput { - pub sumcheck_proof: SumcheckProof, - pub claims: Stage3OutputClaims, +/// Stage 3's outputs: the verifier-typed cross-stage carrier downstream stages consume. +pub struct Stage3ProverOutput { pub clear_output: Stage3ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -41,20 +36,21 @@ pub struct Stage3ProverOutput { /// Prove stage 3 on `transcript` (positioned at the stage-2 boundary). #[tracing::instrument(skip_all)] -pub fn prove_stage3( +pub fn prove_stage3( backend: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, config: &ProverConfig, stage1: &Stage1ClearOutput, stage2: &Stage2ClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + 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(); @@ -84,13 +80,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.into(); + let committed_witness = proved.witness; Ok(Stage3ProverOutput { - sumcheck_proof, - 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 648d2fd17b..a9cd0d230d 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 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}; @@ -20,17 +20,15 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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")] 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::{ @@ -40,14 +38,11 @@ use jolt_verifier::stages::stage4::{ }; use jolt_verifier::{CheckedInputs, VerifierError}; -use crate::recorder::ProofMode; +use crate::recorder::{ClaimRecorder, ModeRecorder, 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. -pub struct Stage4ProverOutput { - pub sumcheck_proof: SumcheckProof, - pub claims: Stage4OutputClaims, +/// Stage 4's outputs: the verifier-typed cross-stage carrier downstream stages consume. +pub struct Stage4ProverOutput { pub clear_output: Stage4ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -56,7 +51,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: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, checked: &CheckedInputs, @@ -64,14 +59,15 @@ pub fn prove_stage4( preprocessing: &JoltProverPreprocessing, stage2: &Stage2ClearOutput, stage3: &Stage3ClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + 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); @@ -176,12 +172,23 @@ 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 // 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. + if ! as ClaimRecorder>::RECORDS_STAGED { + ram_val_check_init.staged_openings().send(transcript); + } let inputs = stage4_input_values_from_upstream( &stage2.output_values, @@ -194,11 +201,9 @@ 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 records them on the stage's `Staged` routes. let mut scheduler = backend.build_round_scheduler(); let proved = sumchecks.prove( backend, @@ -210,13 +215,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.into(); + let committed_witness = proved.witness; Ok(Stage4ProverOutput { - sumcheck_proof, - claims: proved.output_claims.clone(), clear_output: Stage4ClearOutput { output_values: proved.output_claims, output_points: proved.output_points, @@ -227,15 +228,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 { @@ -251,14 +249,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] @@ -272,8 +270,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: &mut context, mode: &mode, config: &config, @@ -283,7 +281,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage3(); - let out = prove_stage4::, Blake2bTranscript>( + let _stage4 = prove_stage4::, JoltSponge>( &mut context, &mode, &checked, @@ -295,23 +293,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, + &mut prover_transcript, ); - - assert_eq!(transcript.state(), prover_transcript.state()); } fn fr(value: u64) -> Fr { @@ -439,41 +426,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 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). +/// 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_declared_rows_and_verifies() { let witness = field_arithmetic_backend().with_field_inline().unwrap(); let backend = crate::stages::field_inline_fixtures::reference_backend(); let mut context = backend.with_witness(&witness); - 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: &mut context, mode: &mode, @@ -484,7 +470,7 @@ mod field_inline_zk { transcript: &mut transcript, } .through_stage3(); - let out = prove_stage4::, Blake2bTranscript>( + let out = prove_stage4::, JoltSponge>( &mut context, &mode, &checked, @@ -496,13 +482,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, &mut transcript); } } diff --git a/crates/jolt-prover/src/stages/stage5.rs b/crates/jolt-prover/src/stages/stage5.rs index 6981c45fd8..2a2563a5ea 100644 --- a/crates/jolt-prover/src/stages/stage5.rs +++ b/crates/jolt-prover/src/stages/stage5.rs @@ -20,16 +20,14 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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")] 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::{ @@ -40,11 +38,8 @@ use jolt_verifier::CheckedInputs; 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. -pub struct Stage5ProverOutput { - pub sumcheck_proof: SumcheckProof, - pub claims: Stage5OutputClaims, +/// Stage 5's outputs: the verifier-typed cross-stage carrier downstream stages consume. +pub struct Stage5ProverOutput { pub clear_output: Stage5ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -53,7 +48,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: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, checked: &CheckedInputs, @@ -61,14 +56,15 @@ pub fn prove_stage5( preprocessing: &JoltProverPreprocessing, stage2: &Stage2ClearOutput, stage4: &Stage4ClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { + transcript.site(STAGE5); let log_k = checked.ram_K.ilog2() as usize; let formula_dimensions = super::formula_dimensions( checked, @@ -107,14 +103,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.into(); + 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, output_values: proved.output_claims, @@ -126,14 +118,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 +138,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 +160,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: &mut context, mode: &mode, config: &config, @@ -182,7 +171,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage4(); - let out = prove_stage5::, Blake2bTranscript>( + let _stage5 = prove_stage5::, JoltSponge>( &mut context, &mode, &checked, @@ -194,32 +183,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, + &mut 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 f0205aa864..c35f069925 100644 --- a/crates/jolt-prover/src/stages/stage6a.rs +++ b/crates/jolt-prover/src/stages/stage6a.rs @@ -24,8 +24,8 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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; @@ -37,19 +37,15 @@ 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 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. -pub struct Stage6aProverOutput { - pub sumcheck_proof: SumcheckProof, - pub claims: Stage6aOutputClaims, +/// Stage 6a's outputs: the verifier-typed cross-stage carrier stage 6b consumes. +pub struct Stage6aProverOutput { pub clear_output: Stage6aClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -58,7 +54,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: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, checked: &CheckedInputs, @@ -69,14 +65,15 @@ pub fn prove_stage6a( stage3: &Stage3ClearOutput, stage4: &Stage4ClearOutput, stage5: &Stage5ClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { + transcript.site(STAGE6A); let formula_dimensions = super::formula_dimensions( checked, config, @@ -158,13 +155,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.into(); + let committed_witness = proved.witness; Ok(Stage6aProverOutput { - sumcheck_proof, - claims: proved.output_claims.clone(), clear_output: Stage6aClearOutput { output_values: proved.output_claims, output_points: proved.output_points, @@ -175,15 +168,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 { @@ -191,13 +179,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] @@ -211,7 +199,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: &mut context, mode: &mode, @@ -222,7 +210,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage5(); - let out = prove_stage6a::, Blake2bTranscript>( + let _stage6a = prove_stage6a::, JoltSponge>( &mut context, &mode, &checked, @@ -248,41 +236,11 @@ mod field_inline_round_trip { let zero = Fr::from_u64(0); assert!(appendage.rd_wa_read_write != zero); - 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, - ); - twins::replay_stage6a( - &mut transcript, - &config, - &checked, - &preprocessing, - &stage1, - &stage2, - &stage3, - &stage4, - &stage5, - &out, + &mut prover_transcript, ); - - 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 5037a0c2ab..8dc4aef74f 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; @@ -29,8 +28,8 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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; @@ -40,9 +39,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, }; @@ -51,13 +48,10 @@ use jolt_verifier::CheckedInputs; 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. -pub struct Stage6bProverOutput { - pub sumcheck_proof: SumcheckProof, - pub claims: Stage6bOutputClaims, +/// 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 clear_output: Stage6bClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -66,7 +60,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: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, checked: &CheckedInputs, @@ -78,14 +72,15 @@ pub fn prove_stage6b( stage4: &Stage4ClearOutput, stage5: &Stage5ClearOutput, stage6a: &Stage6aClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { + transcript.site(STAGE6B); let log_k = checked.ram_K.ilog2() as usize; let precommitted = &checked.precommitted; let formula_dimensions = super::formula_dimensions( @@ -190,13 +185,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.into(); + let committed_witness = proved.witness; Ok(Stage6bProverOutput { - sumcheck_proof, - claims: proved.output_claims.clone(), clear_output: Stage6bClearOutput { output_values: proved.output_claims, output_points: proved.output_points, @@ -207,17 +198,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 { @@ -233,42 +221,29 @@ 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::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], - ) { + fn stage6b_round_trips(trace_backend: TraceBackend, preprocessing: FixturePreprocessing) { let witness = trace_backend.with_field_inline().unwrap(); let backend = crate::stages::field_inline_fixtures::reference_backend(); let mut context = backend.with_witness(&witness); @@ -277,7 +252,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: &mut context, mode: &mode, @@ -288,7 +263,7 @@ mod field_inline_round_trip { transcript: &mut prover_transcript, } .through_stage6a(); - let out = prove_stage6b::, Blake2bTranscript>( + let _stage6b = prove_stage6b::, JoltSponge>( &mut context, &mode, &checked, @@ -304,136 +279,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, + &mut 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 { @@ -521,41 +372,38 @@ mod field_inline_round_trip { } } -/// ZK with field-inline enabled: the stage-6b committed shell carries the curated row count — the -/// alias-deduped cycle-point cell total, whose field-inline share is exactly the one -/// spliced reduced `FieldRdInc` row. +/// 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 { 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_layout_rows_and_verifies() { let witness = addi_only_backend().with_field_inline().unwrap(); let backend = crate::stages::field_inline_fixtures::reference_backend(); let mut context = backend.with_witness(&witness); - 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: &mut context, mode: &mode, @@ -566,7 +414,7 @@ mod field_inline_zk { transcript: &mut transcript, } .through_stage6a(); - let out = prove_stage6b::, Blake2bTranscript>( + let out = prove_stage6b::, JoltSponge>( &mut context, &mode, &checked, @@ -582,10 +430,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 @@ -594,20 +441,21 @@ 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.claims.field_registers_inc_claim_reduction).len(), + OutputClaims::opening_values( + &out.clear_output + .output_values + .field_registers_inc_claim_reduction + ) + .len(), 1 ); + + verify_through(Through::Stage6b, &checked, &preprocessing, &mut transcript); } } diff --git a/crates/jolt-prover/src/stages/stage7.rs b/crates/jolt-prover/src/stages/stage7.rs index 8c12c55d42..9246d57d8a 100644 --- a/crates/jolt-prover/src/stages/stage7.rs +++ b/crates/jolt-prover/src/stages/stage7.rs @@ -18,23 +18,20 @@ use jolt_kernels::{JoltBackend, KernelContext}; use jolt_openings::CommitmentScheme; #[cfg(feature = "zk")] use jolt_sumcheck::CommittedSumcheckWitness; -use jolt_sumcheck::SumcheckProof; -use jolt_transcript::Transcript; +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; -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 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. -pub struct Stage7ProverOutput { - pub sumcheck_proof: SumcheckProof, - pub claims: Stage7OutputClaims, +/// Stage 7's outputs: the verifier-typed cross-stage carrier stage 8 consumes. +pub struct Stage7ProverOutput { pub clear_output: Stage7ClearOutput, #[cfg(feature = "zk")] pub committed_witness: CommittedSumcheckWitness, @@ -43,7 +40,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: &mut KernelContext<'_, F, JoltBackend>, mode: &ProofMode<'_, VC>, checked: &CheckedInputs, @@ -51,14 +48,15 @@ pub fn prove_stage7( preprocessing: &JoltProverPreprocessing, stage4: &Stage4ClearOutput, stage6b: &Stage6bClearOutput, - transcript: &mut T, -) -> Result, ProverError> + transcript: &mut ProverTranscript, +) -> Result, ProverError> where F: JoltField, PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + H: Sponge, { + transcript.site(STAGE7); let precommitted = &checked.precommitted; let formula_dimensions = super::formula_dimensions( checked, @@ -93,13 +91,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.into(); + let committed_witness = proved.witness; Ok(Stage7ProverOutput { - sumcheck_proof, - claims: proved.output_claims.clone(), clear_output: Stage7ClearOutput { output_values: proved.output_claims, output_points: proved.output_points, diff --git a/crates/jolt-prover/tests/akita_e2e.rs b/crates/jolt-prover/tests/akita_e2e.rs index 6972fa733f..91d7c64ca3 100644 --- a/crates/jolt-prover/tests/akita_e2e.rs +++ b/crates/jolt-prover/tests/akita_e2e.rs @@ -22,11 +22,7 @@ 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_TRUSTED_ADVICE_SIZE, DEFAULT_MAX_UNTRUSTED_ADVICE_SIZE}; use common::jolt_device::JoltDevice; @@ -36,21 +32,58 @@ mod akita_tests { use jolt_claims::protocols::jolt::{ JoltAdviceKind, JoltCommittedPolynomial, JoltOneHotConfig, TracePolynomialOrder, }; - use jolt_field::Ring; use jolt_prover::akita; - use jolt_prover::akita::preprocessing::{ - self, AkitaProverPreprocessing, AkitaTranscript, AkitaVc, - }; + use jolt_prover::akita::preprocessing::{self, AkitaProverPreprocessing, AkitaVc}; use jolt_prover::akita::witness::commit_advice; use jolt_prover::JoltBackend; use jolt_prover::{PreprocessingError, ProverConfig, ProverError}; - use jolt_verifier::proof::{ClearProofClaims, JoltProof, JoltProofClaims}; - use jolt_verifier::{JoltVerifierPreprocessing, VerifierError}; + use jolt_transcript::VerifierTranscript; + use jolt_verifier::proof::JoltProof; + use jolt_verifier::{ + jolt_protocol_id, seed_transcript, JoltSponge, JoltVerifierPreprocessing, 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, @@ -126,7 +159,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::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -143,7 +176,7 @@ mod akita_tests { } fn verify(proved: &ProvedGuest) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( &proved.preprocessing.verifier, &proved.public_io, &proved.proof, @@ -236,30 +269,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(), @@ -310,7 +320,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::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -352,7 +362,7 @@ mod akita_tests { continue; } config.akita_chunk_profile = requested; - let result = akita::prove::( + let result = akita::prove::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -383,7 +393,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"); } } @@ -461,7 +476,7 @@ mod akita_tests { witness_config(&config, true, true), JoltVmWitnessInputs::new(&run.program, &program, run.trace), ); - let proof = akita::prove::( + let proof = akita::prove::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -469,7 +484,7 @@ mod akita_tests { &public_io, ) .expect("reuse the original advice commitment and opening hint"); - jolt_verifier::verify::( + jolt_verifier::verify::( &preprocessing.verifier, &public_io, &proof, @@ -541,7 +556,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::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -563,23 +578,14 @@ mod akita_tests { .expect("transport proof"); assert_eq!(consumed, encoded.len()); let verify = |proof: &Proof| { - jolt_verifier::verify::( + jolt_verifier::verify::( &verifier, &public_io, proof, None, ) }; verify(&proof).expect("committed Akita proof must verify"); - - 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") - .bytecode += AkitaField::from_u64(1); - assert!(verify(&tampered).is_err()); + for tampered in bit_flips(&proof) { + assert!(verify(&tampered).is_err()); + } } #[test] @@ -617,7 +623,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::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -626,8 +632,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 a99cf295b1..55c600a9ea 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::JoltBackend; 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 an Akita 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 + } + type BackendCase = (&'static str, fn() -> JoltBackend); fn backends() -> [BackendCase; 2] { @@ -99,13 +168,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(), - "Akita 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) @@ -140,17 +205,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!("Akita 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, @@ -177,15 +235,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("Akita 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 @@ -199,45 +253,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()); }), ), ]; @@ -259,34 +310,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(), JoltBackend::optimized(), |_, _, _| ()); - let commitment = output - .proof - .field_inc_commitment - .clone() - .expect("Akita 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/backend_contract.rs b/crates/jolt-prover/tests/backend_contract.rs index d8a24598e2..4d770f864b 100644 --- a/crates/jolt-prover/tests/backend_contract.rs +++ b/crates/jolt-prover/tests/backend_contract.rs @@ -34,7 +34,7 @@ mod tests { dory, JoltBackend, JoltProverPreprocessing, JoltSharedPreprocessing, ProverConfig, ProverError, }; - use jolt_transcript::LegacyBlake2bTranscript; + use jolt_verifier::JoltSponge; use jolt_witness::{ ChunkVisitor, JoltVmWitnessConfig, JoltVmWitnessInputs, JoltWitnessOracle, JoltWitnessPlane, ProgramSource, RowSource, Shape, TraceBackend, WitnessError, @@ -188,7 +188,7 @@ mod tests { Fr, DoryScheme, Pedersen, - LegacyBlake2bTranscript, + JoltSponge, >(&mut context, &preprocessing, &config, None, &public_io); assert!( matches!(result, Err(ProverError::Witness(WitnessError::InvalidWitnessData { label: "stage-0 validation", reason })) @@ -220,7 +220,7 @@ mod tests { trace_length: config.trace_length, }); let context = KernelContext::new(®istry, &witness, session); - let result = dory::prove::, LegacyBlake2bTranscript>( + let result = dory::prove::, JoltSponge>( context, &preprocessing, &proof_config, diff --git a/crates/jolt-prover/tests/e2e_matrix.rs b/crates/jolt-prover/tests/e2e_matrix.rs index 1b4dd95b33..8fbf870cb4 100644 --- a/crates/jolt-prover/tests/e2e_matrix.rs +++ b/crates/jolt-prover/tests/e2e_matrix.rs @@ -251,7 +251,7 @@ mod matrix { use jolt_dory::DoryScheme; use jolt_field::Fr; 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}; @@ -300,7 +300,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().with_witness(&witness), &preprocessing, &config, @@ -308,7 +308,7 @@ mod matrix { &public_io, ) .expect("Dory proof"); - jolt_verifier::verify::, Blake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( &preprocessing.verifier, &public_io, &proof, @@ -322,9 +322,10 @@ mod matrix { mod mode { use jolt_akita::{AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_prover::akita; - use jolt_prover::akita::preprocessing::{self, AkitaTranscript, AkitaVc}; + use jolt_prover::akita::preprocessing::{self, AkitaVc}; use jolt_prover::JoltBackend; use jolt_prover::ProverConfig; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use crate::support::{self, GuestCase}; @@ -369,7 +370,7 @@ mod matrix { .include_trusted_advice(trusted_advice), JoltVmWitnessInputs::new(&prepared.program, &program_preprocessing, prepared.trace), ); - let proof = akita::prove::( + let proof = akita::prove::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -377,7 +378,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/engine_twins.rs b/crates/jolt-prover/tests/engine_twins.rs index f10c82b52d..5d24fc6270 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, - SumcheckProof, 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}; @@ -112,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) @@ -120,6 +126,27 @@ 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: &mut ProverTranscript, + mut verifier: VerifierTranscript<'_, H>, +) { + assert_eq!( + verifier.challenge_bytes::<32>(), + prover.challenge_bytes::<32>() + ); + verifier.finish().unwrap(); +} + fn synthetic_output_values() -> Vec { vec![Fr::from_u64(11), Fr::from_u64(22), Fr::from_u64(33)] } @@ -128,7 +155,7 @@ fn synthetic_output_values() -> Vec { fn clear_engine_twin_matches_generated_verify_clear() { 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 @@ -173,13 +200,14 @@ 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 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 @@ -190,34 +218,34 @@ fn clear_engine_twin_matches_generated_verify_clear() { &mut verifier_transcript, ) .unwrap(); - let reduction = SumcheckProof::::from(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(&mut prover_transcript, verifier_transcript); } #[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). 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 @@ -262,16 +290,34 @@ fn committed_engine_twin_matches_generated_verify_zk() { &mut prover_transcript, ) .unwrap(); - let recorded = recorder - .finish(&synthetic_output_values(), &mut prover_transcript) + // 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(&output_values, &mut prover_transcript) .unwrap(); - assert!(recorded.committed_witness.is_some()); - let mut verifier_transcript = Blake2bTranscript::new(b"engine-zk-twin"); + // rounds, derived points, output-claim row commitments). + 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 consistency = sumchecks - .verify_zk(&recorded.proof, &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!( @@ -283,13 +329,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(&mut 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(); @@ -300,15 +350,18 @@ 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 mut prover_transcript = prover_transcript("uniskip-stage-twin"); let proved = prove_uniskip_clear::( poly, input_claim, @@ -318,16 +371,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( - &SumcheckProof::::from(proved.proof), - ¶ms, - input_claim, - proved.output_claim, - &mut verifier_transcript, - ) - .unwrap(); + 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!(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(&mut prover_transcript, verifier_transcript); } diff --git a/crates/jolt-prover/tests/field_inline_e2e.rs b/crates/jolt-prover/tests/field_inline_e2e.rs index 8a119e197a..6c801c7e6c 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, ProverError}; - use jolt_verifier::proof::JoltProofClaims; use jolt_verifier::VerifierError; use crate::support::field_inline::dory; @@ -88,7 +147,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. @@ -97,11 +156,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}") }); @@ -109,7 +163,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", ); } @@ -121,7 +175,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}") }); @@ -129,7 +182,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", ); } @@ -137,18 +190,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()); } } @@ -158,51 +207,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 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", ); }); } diff --git a/crates/jolt-prover/tests/sponge_swap.rs b/crates/jolt-prover/tests/sponge_swap.rs new file mode 100644 index 0000000000..fd897b11d0 --- /dev/null +++ b/crates/jolt-prover/tests/sponge_swap.rs @@ -0,0 +1,93 @@ +//! 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_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() { + #[cfg(feature = "zk")] + support::with_zk_stack(prove_and_verify_on_keccak_only); + #[cfg(not(feature = "zk"))] + prove_and_verify_on_keccak_only(); + } + + fn prove_and_verify_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_from_dimensions::( + prepared.trace.dimensions, + &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::new( + 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().with_witness(&witness), + &preprocessing, + &config, + None, + &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" + ); + } +} diff --git a/crates/jolt-prover/tests/support/akita_backend.rs b/crates/jolt-prover/tests/support/akita_backend.rs index 9955ebd2a0..1e99d6a5c4 100644 --- a/crates/jolt-prover/tests/support/akita_backend.rs +++ b/crates/jolt-prover/tests/support/akita_backend.rs @@ -23,8 +23,9 @@ use jolt_prover::ProverError; use common::jolt_device::JoltDevice; use jolt_akita::{AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_prover::akita; -use jolt_prover::akita::preprocessing::{AkitaProverPreprocessing, AkitaTranscript, AkitaVc}; +use jolt_prover::akita::preprocessing::{AkitaProverPreprocessing, AkitaVc}; use jolt_prover::ProverConfig; +use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; struct UnreadableWitness; @@ -130,7 +131,7 @@ fn registry_reuses_short_lived_witnesses_and_releases_each_session() { trace_length: fixture.config.trace_length, }); let backend = KernelContext::new(®istry, &witness, session); - let result = akita::prove::( + let result = akita::prove::( backend, &fixture.preprocessing, &config, @@ -176,7 +177,7 @@ fn successful_proof_releases_the_bound_witness_and_session() { trace_length: fixture.config.trace_length, }); let context = KernelContext::new(®istry, witness, session); - let _proof = akita::prove::( + let _proof = akita::prove::( context, &fixture.preprocessing, &fixture.config, diff --git a/crates/jolt-prover/tests/support/field_inline/akita.rs b/crates/jolt-prover/tests/support/field_inline/akita.rs index be4e5b31f0..1f344ef889 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::JoltBackend; 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.with_witness(&witness), &prover_preprocessing, &config, @@ -101,7 +101,7 @@ pub fn verify_full( public_io: &JoltDevice, proof: &Proof, ) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( preprocessing, 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 a674c8d9a3..430adb83fa 100644 --- a/crates/jolt-prover/tests/support/field_inline/dory.rs +++ b/crates/jolt-prover/tests/support/field_inline/dory.rs @@ -3,14 +3,14 @@ use jolt_crypto::{Bn254G1, Pedersen}; use jolt_dory::DoryScheme; use jolt_field::Fr; use jolt_prover::{dory, JoltBackend, JoltSharedPreprocessing, ProverConfig, ProverError}; -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( @@ -56,7 +56,7 @@ pub fn try_prove( ) .with_field_inline() .expect("field-inline witness view"); - let proof = dory::prove::, Blake2bTranscript>( + let proof = dory::prove::, JoltSponge>( backend.with_witness(&witness), &preprocessing, &config, @@ -71,7 +71,7 @@ pub fn verify_full( public_io: &JoltDevice, proof: &Proof, ) -> Result<(), VerifierError> { - jolt_verifier::verify::, Blake2bTranscript>( + jolt_verifier::verify::, JoltSponge>( preprocessing, public_io, proof, diff --git a/crates/jolt-prover/tests/zk_e2e.rs b/crates/jolt-prover/tests/zk_e2e.rs index d8a0f55ae0..15cd092e6a 100644 --- a/crates/jolt-prover/tests/zk_e2e.rs +++ b/crates/jolt-prover/tests/zk_e2e.rs @@ -15,29 +15,26 @@ 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_prover::dory::DoryProverPreprocessing; use jolt_prover::{JoltBackend, JoltSharedPreprocessing, ProverConfig}; use jolt_riscv::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}; use crate::support::{self, with_zk_stack, GuestCase, PreparedGuest}; - type Proof = JoltProof>; + type Proof = JoltProof; struct ProvedGuest { preprocessing: DoryProverPreprocessing, @@ -96,15 +93,14 @@ mod zk { jolt_prover::dory::commit_trusted_advice(&preprocessing, &case.trusted_advice) .expect("trusted advice commitment") }); - let proof = - jolt_prover::dory::prove::, Blake2bTranscript>( - backend.with_witness(&witness), - &preprocessing, - &config, - trusted.as_ref(), - &public_io, - ) - .expect("modular ZK prove"); + let proof = jolt_prover::dory::prove::, JoltSponge>( + backend.with_witness(&witness), + &preprocessing, + &config, + trusted.as_ref(), + &public_io, + ) + .expect("modular ZK prove"); ProvedGuest { trusted_advice_commitment: trusted.map(|entry| entry.commitment), preprocessing, @@ -118,7 +114,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, @@ -132,7 +128,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"); }); } @@ -141,14 +137,31 @@ 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()); }); } + #[test] + fn zk_advice_consumer_modular_proof_is_accepted() { + with_zk_stack(|| { + let proved = prove_guest( + GuestCase { + inputs: postcard::to_stdvec(&12u64).expect("serialize input"), + untrusted_advice: postcard::to_stdvec(&5u64) + .expect("serialize untrusted advice"), + trusted_advice: postcard::to_stdvec(&7u64).expect("serialize trusted advice"), + ..GuestCase::new("advice-consumer-guest") + }, + JoltBackend::reference(), + |_| {}, + ); + assert!(!proved.public_io.untrusted_advice.is_empty()); + verify(&proved).expect("modular ZK advice proof must verify"); + }); + } + #[test] fn zk_committed_muldiv_modular_proof_is_accepted() { with_zk_stack(|| { @@ -166,16 +179,15 @@ mod zk { ), JoltVmWitnessInputs::new(&run.program, &program_preprocessing, run.trace), ); - let proof = - jolt_prover::dory::prove::, Blake2bTranscript>( - JoltBackend::reference().with_witness(&witness), - &preprocessing, - &config, - None, - &public_io, - ) - .expect("committed ZK prove"); - jolt_verifier::verify::, Blake2bTranscript>( + let proof = jolt_prover::dory::prove::, JoltSponge>( + JoltBackend::reference().with_witness(&witness), + &preprocessing, + &config, + None, + &public_io, + ) + .expect("committed ZK prove"); + jolt_verifier::verify::, JoltSponge>( &preprocessing.verifier, &public_io, &proof, diff --git a/crates/jolt-spartan-prover/Cargo.toml b/crates/jolt-spartan-prover/Cargo.toml index d70f44503e..c8ba557339 100644 --- a/crates/jolt-spartan-prover/Cargo.toml +++ b/crates/jolt-spartan-prover/Cargo.toml @@ -25,4 +25,3 @@ jolt-transcript = { workspace = true, features = [ "bn254", "transcript-blake2b", ] } -bincode.workspace = true diff --git a/crates/jolt-spartan-prover/src/lib.rs b/crates/jolt-spartan-prover/src/lib.rs index 764f31238f..4b5db16671 100644 --- a/crates/jolt-spartan-prover/src/lib.rs +++ b/crates/jolt-spartan-prover/src/lib.rs @@ -5,38 +5,32 @@ mod rounds; -use jolt_field::{JoltField, One, Zero}; +use jolt_field::{CanonicalDecode, JoltField, One, Zero}; use jolt_openings::CommitmentScheme; use jolt_poly::{EqPolynomial, Polynomial}; -use jolt_spartan_verifier::{ - inner_relation, SpartanError, SpartanKey, SpartanProof, INNER_DEGREE, OUTER_DEGREE, -}; +use jolt_spartan_verifier::{inner_relation, SpartanError, SpartanKey, INNER_DEGREE, OUTER_DEGREE}; use jolt_sumcheck::{ - prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, CompressedSumcheckProof, - ProveRounds, SequentialRounds, SumcheckRecorder, + prove_batch, BatchMember, BatchPrelude, ClearSumcheckRecorder, ProveRounds, SequentialRounds, + SumcheckRecorder, }; -use jolt_transcript::{AppendToTranscript, Transcript}; +use jolt_transcript::{ProverTranscript, Sponge}; use rounds::{InnerRounds, OuterRounds}; -/// Proves `A z * B z = C z` for `z = [1, public_inputs, witness]`. +/// Proves `A z * B z = C z` for `z = [1, public_inputs, witness]` into +/// `transcript`, in the message order [`SpartanKey::verify`] reads. /// /// This is a clear argument; it reveals sumcheck coefficients and evaluations. /// The caller must not interpret it as a zero-knowledge wrapper. -#[expect( - clippy::type_complexity, - reason = "the PCS determines field, commitment, and opening proof types" -)] -pub fn prove( +pub fn prove( key: &SpartanKey, public_inputs: &[PCS::Field], witness: &[PCS::Field], pcs_setup: &PCS::ProverSetup, - transcript: &mut impl Transcript, -) -> Result, SpartanError> + transcript: &mut ProverTranscript, +) -> Result<(), SpartanError> where - PCS::Field: AppendToTranscript, - PCS::Output: AppendToTranscript, + PCS::Field: CanonicalDecode, { key.validate_public_inputs(public_inputs)?; if witness.len() != key.witness_len() { @@ -50,19 +44,25 @@ where witness.resize(key.padded_witness_len(), PCS::Field::zero()); let witness_poly = Polynomial::new(witness.clone()); let (witness_commitment, hint) = PCS::commit(&witness_poly, pcs_setup)?; - let tau = key.begin(public_inputs, &witness_commitment, transcript)?; + key.bind_statement(public_inputs, transcript)?; + PCS::send_commitment(&witness_commitment, transcript); + let tau = key.draw_tau(transcript); let mut outer_rounds = OuterRounds::new(key, &assignment, &tau)?; - let (outer, rx, outer_claim) = prove_rounds( + let (rx, outer_claim) = prove_rounds( &mut outer_rounds, OUTER_DEGREE, PCS::Field::zero(), transcript, )?; - let outer_evaluations = outer_rounds.evaluations()?; + let mut outer_evaluations = outer_rounds.evaluations()?; key.check_outer(&tau, &rx, outer_claim, outer_evaluations)?; let row_weights = EqPolynomial::new(rx).evaluations(); - let (weights, inner_claim) = - key.begin_inner(&row_weights, public_inputs, outer_evaluations, transcript)?; + let (weights, inner_claim) = key.begin_inner( + &row_weights, + public_inputs, + &mut outer_evaluations, + transcript, + )?; let mut linear = key.matrices().project_column_range( &row_weights, key.public_columns(), @@ -71,14 +71,13 @@ where )?; linear.resize(key.padded_witness_len(), PCS::Field::zero()); let mut inner_rounds = InnerRounds::new(linear, witness, key.witness_vars()); - let (inner, ry, final_claim) = - prove_rounds(&mut inner_rounds, INNER_DEGREE, inner_claim, transcript)?; - let [linear_evaluation, witness_evaluation] = inner_rounds.evaluations()?; + let (ry, final_claim) = prove_rounds(&mut inner_rounds, INNER_DEGREE, inner_claim, transcript)?; + let [linear_evaluation, mut witness_evaluation] = inner_rounds.evaluations()?; if final_claim != inner_relation(linear_evaluation, witness_evaluation) { return Err(SpartanError::InnerClaim); } - SpartanKey::append_witness_evaluation(witness_evaluation, transcript); - let opening = PCS::open( + SpartanKey::exchange_witness_evaluation(&mut witness_evaluation, transcript)?; + PCS::open( &witness_poly, &ry, witness_evaluation, @@ -86,22 +85,16 @@ where Some(hint), transcript, )?; - Ok(SpartanProof { - witness_commitment, - outer, - outer_evaluations, - inner, - witness_evaluation, - opening, - }) + Ok(()) } -fn prove_rounds( +/// Sends one compressed sumcheck and returns its point and final claim. +fn prove_rounds( member: &mut dyn ProveRounds, degree: usize, claim: F, - transcript: &mut impl Transcript, -) -> Result<(CompressedSumcheckProof, Vec, F), SpartanError> { + transcript: &mut ProverTranscript, +) -> Result<(Vec, F), SpartanError> { let prelude = BatchPrelude::try_new( vec![BatchMember { input_claim: claim, @@ -120,6 +113,6 @@ fn prove_rounds( &mut recorder, transcript, )?; - let recorded = recorder.finish(&[], transcript)?; - Ok((recorded.proof, result.challenges, result.final_claim)) + recorder.finish(&[], transcript)?; + Ok((result.challenges, result.final_claim)) } diff --git a/crates/jolt-spartan-prover/tests/spartan.rs b/crates/jolt-spartan-prover/tests/spartan.rs index ef7a64dabf..8e5d703007 100644 --- a/crates/jolt-spartan-prover/tests/spartan.rs +++ b/crates/jolt-spartan-prover/tests/spartan.rs @@ -9,12 +9,15 @@ use jolt_dory::DoryScheme; use jolt_field::{Fr, One, Ring, Zero}; use jolt_hyperkzg::{HyperKZGScheme, HyperKZGSetupParams}; use jolt_openings::CommitmentScheme; -use jolt_poly::CompressedPoly; use jolt_r1cs::ConstraintMatrices; use jolt_spartan_prover::prove; use jolt_spartan_verifier::{SpartanError, SpartanKey}; -use jolt_sumcheck::SumcheckError; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, TranscriptError, VerifierTranscript, +}; + +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-spartan/test"); +const SESSION: &[u8] = b"spartan-test"; fn matrices() -> ConstraintMatrices { let one = Fr::one(); @@ -40,9 +43,6 @@ fn public() -> [Fr; 1] { fn witness() -> [Fr; 3] { [3, 9, 27].map(Fr::from_u64) } -fn transcript() -> Blake2bTranscript { - Blake2bTranscript::new(b"spartan-test") -} fn hyperkzg_setup() -> ( ::ProverSetup, @@ -61,25 +61,42 @@ fn hyperkzg_setup() -> ( .unwrap() } -fn check_backend>(pk: &PCS::ProverSetup, vk: &PCS::VerifierSetup) -where - PCS::Output: AppendToTranscript, -{ +fn prove_narg>( + key: &SpartanKey, + public_inputs: &[Fr], + witness: &[Fr], + pk: &PCS::ProverSetup, +) -> Result, SpartanError> { + let mut transcript = ProverTranscript::::new(&PROTOCOL, SESSION); + prove::(key, public_inputs, witness, pk, &mut transcript)?; + Ok(transcript.finish()) +} + +/// Verifies `narg` as the whole argument string of one Spartan proof. +fn verify_narg>( + key: &SpartanKey, + public_inputs: &[Fr], + vk: &PCS::VerifierSetup, + narg: &[u8], +) -> Result<(), SpartanError> { + let mut transcript = VerifierTranscript::::new(&PROTOCOL, SESSION, narg); + key.verify::(public_inputs, vk, &mut transcript)?; + Ok(transcript.finish()?) +} + +fn check_backend>( + pk: &PCS::ProverSetup, + vk: &PCS::VerifierSetup, +) { let key = key(); - let mut pt = transcript(); - let proof = prove::(&key, &public(), &witness(), pk, &mut pt).unwrap(); - let mut vt = transcript(); - key.verify::(&public(), &proof, vk, &mut vt).unwrap(); - assert_eq!(pt.challenge(), vt.challenge()); - let bytes = bincode::serde::encode_to_vec(&proof, bincode::config::standard()).unwrap(); - let (decoded, used) = - bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); - assert_eq!(used, bytes.len()); - key.verify::(&public(), &decoded, vk, &mut transcript()) - .unwrap(); - assert!(key - .verify::(&[Fr::from_u64(33)], &proof, vk, &mut transcript()) - .is_err()); + let mut pt = ProverTranscript::::new(&PROTOCOL, SESSION); + prove::(&key, &public(), &witness(), pk, &mut pt).unwrap(); + let narg = pt.narg().to_vec(); + let mut vt = VerifierTranscript::::new(&PROTOCOL, SESSION, &narg); + key.verify::(&public(), vk, &mut vt).unwrap(); + assert_eq!(pt.challenge_bytes::<32>(), vt.challenge_bytes::<32>()); + vt.finish().unwrap(); + assert!(verify_narg::(&key, &[Fr::from_u64(33)], vk, &narg).is_err()); } #[test] @@ -115,25 +132,23 @@ fn one_row_one_witness_and_no_public_input_are_supported() { ); let key = SpartanKey::new(m, 0, [19; 32]).unwrap(); let (pk, vk) = hyperkzg_setup(); - let proof = - prove::(&key, &[], &[Fr::from_u64(3)], &pk, &mut transcript()).unwrap(); - key.verify::(&[], &proof, &vk, &mut transcript()) - .unwrap(); + let narg = prove_narg::(&key, &[], &[Fr::from_u64(3)], &pk).unwrap(); + verify_narg::(&key, &[], &vk, &narg).unwrap(); } #[test] fn bad_inputs_and_malformed_matrices_reject() { let (pk, _) = hyperkzg_setup(); assert!(matches!( - prove::(&key(), &[], &witness(), &pk, &mut transcript()), + prove_narg::(&key(), &[], &witness(), &pk), Err(SpartanError::PublicInputs) )); assert!(matches!( - prove::(&key(), &public(), &[], &pk, &mut transcript()), + prove_narg::(&key(), &public(), &[], &pk), Err(SpartanError::WitnessLength) )); assert!(matches!( - prove::(&key(), &public(), &[Fr::zero(); 3], &pk, &mut transcript()), + prove_narg::(&key(), &public(), &[Fr::zero(); 3], &pk), Err(SpartanError::Unsatisfied(_)) )); let mut m = matrices(); @@ -159,100 +174,51 @@ fn bad_inputs_and_malformed_matrices_reject() { #[test] fn relation_digest_binds_encoding_of_evaluation_equivalent_matrices() { let (pk, vk) = hyperkzg_setup(); - let proof = - prove::(&key(), &public(), &witness(), &pk, &mut transcript()).unwrap(); + let narg = prove_narg::(&key(), &public(), &witness(), &pk).unwrap(); let mut m = matrices(); m.c[0] = vec![(3, Fr::from_u64(2)), (3, -Fr::one())]; m.b[0].push((1, Fr::zero())); let equivalent = SpartanKey::new(m, 1, [19; 32]).unwrap(); - let equivalent_proof = - prove::(&equivalent, &public(), &witness(), &pk, &mut transcript()) - .unwrap(); - equivalent - .verify::(&public(), &equivalent_proof, &vk, &mut transcript()) - .unwrap(); - assert!(equivalent - .verify::(&public(), &proof, &vk, &mut transcript()) - .is_err()); + let equivalent_narg = + prove_narg::(&equivalent, &public(), &witness(), &pk).unwrap(); + verify_narg::(&equivalent, &public(), &vk, &equivalent_narg).unwrap(); + assert!(verify_narg::(&equivalent, &public(), &vk, &narg).is_err()); let another_policy = SpartanKey::new(matrices(), 1, [20; 32]).unwrap(); - assert!(another_policy - .verify::(&public(), &proof, &vk, &mut transcript()) - .is_err()); + assert!(verify_narg::(&another_policy, &public(), &vk, &narg).is_err()); } #[test] -fn proof_claims_commitment_and_opening_are_bound() { +fn every_proof_message_is_bound() { + // Every message in the argument string (the witness commitment, each + // sumcheck round, the outer and witness evaluations, and the HyperKZG + // opening) is a 32-byte canonical element, so flipping a bit of any one + // either breaks its encoding or changes a bound value. let key = key(); let (pk, vk) = hyperkzg_setup(); - let proof = - prove::(&key, &public(), &witness(), &pk, &mut transcript()).unwrap(); - let reject = |proof: &_| { - assert!(key - .verify::(&public(), proof, &vk, &mut transcript()) - .is_err()); - }; - for index in 0..3 { - let mut changed = proof.clone(); - changed.outer_evaluations[index] += Fr::one(); - reject(&changed); - } - let mut changed = proof.clone(); - changed.witness_evaluation += Fr::one(); - reject(&changed); - let mut changed = proof.clone(); - changed.witness_commitment += Bn254::g1_generator(); - reject(&changed); - let mut changed = proof.clone(); - changed.opening.w[0] += Bn254::g1_generator(); - reject(&changed); - for outer in [true, false] { - let mut changed = proof.clone(); - let round = if outer { - &mut changed.outer.round_polynomials[0] - } else { - &mut changed.inner.round_polynomials[0] - }; - let mut coefficients = round.coeffs_except_linear_term().to_vec(); - coefficients[0] += Fr::one(); - *round = CompressedPoly::new(coefficients); - reject(&changed); + let narg = prove_narg::(&key, &public(), &witness(), &pk).unwrap(); + verify_narg::(&key, &public(), &vk, &narg).unwrap(); + assert_eq!(narg.len() % 32, 0); + for element in 0..narg.len() / 32 { + let mut changed = narg.clone(); + changed[element * 32] ^= 1; + assert!( + verify_narg::(&key, &public(), &vk, &changed).is_err(), + "flipping element {element} must reject" + ); } } #[test] -fn exact_round_counts_and_degrees_are_enforced() { +fn truncated_and_extended_arguments_reject() { let key = key(); let (pk, vk) = hyperkzg_setup(); - let proof = - prove::(&key, &public(), &witness(), &pk, &mut transcript()).unwrap(); - for outer in [true, false] { - let mut changed = proof.clone(); - let rounds = if outer { - &mut changed.outer.round_polynomials - } else { - &mut changed.inner.round_polynomials - }; - rounds[0] = CompressedPoly::new(vec![Fr::zero(); if outer { 4 } else { 3 }]); - assert!(matches!( - key.verify::(&public(), &changed, &vk, &mut transcript()), - Err(SpartanError::Sumcheck( - SumcheckError::DegreeBoundExceeded { .. } - )) - )); - let mut changed = proof.clone(); - let rounds = if outer { - &mut changed.outer.round_polynomials - } else { - &mut changed.inner.round_polynomials - }; - let _ = rounds.pop(); - assert!(matches!( - key.verify::(&public(), &changed, &vk, &mut transcript()), - Err(SpartanError::Sumcheck( - SumcheckError::WrongNumberOfRounds { .. } - )) - )); - } + let narg = prove_narg::(&key, &public(), &witness(), &pk).unwrap(); + assert!(verify_narg::(&key, &public(), &vk, &narg[..narg.len() - 32]).is_err()); + let extended = [narg.as_slice(), &[0; 32]].concat(); + assert!(matches!( + verify_narg::(&key, &public(), &vk, &extended), + Err(SpartanError::Transcript(TranscriptError::TrailingBytes)) + )); } #[test] @@ -270,11 +236,8 @@ fn unequal_row_and_witness_padding_accepts() { ], ); let key = SpartanKey::new(m, 0, [19; 32]).unwrap(); + assert_eq!((key.row_vars(), key.witness_vars()), (2, 1)); let (pk, vk) = hyperkzg_setup(); - let proof = - prove::(&key, &[], &[Fr::from_u64(3)], &pk, &mut transcript()).unwrap(); - assert_eq!(proof.outer.round_polynomials.len(), 2); - assert_eq!(proof.inner.round_polynomials.len(), 1); - key.verify::(&[], &proof, &vk, &mut transcript()) - .unwrap(); + let narg = prove_narg::(&key, &[], &[Fr::from_u64(3)], &pk).unwrap(); + verify_narg::(&key, &[], &vk, &narg).unwrap(); } diff --git a/crates/jolt-spartan-verifier/Cargo.toml b/crates/jolt-spartan-verifier/Cargo.toml index 12e15a4f30..0a4059fe35 100644 --- a/crates/jolt-spartan-verifier/Cargo.toml +++ b/crates/jolt-spartan-verifier/Cargo.toml @@ -16,5 +16,4 @@ jolt-poly.workspace = true jolt-r1cs = { path = "../jolt-r1cs", default-features = false } jolt-sumcheck.workspace = true jolt-transcript.workspace = true -serde = { workspace = true, features = ["derive"] } thiserror.workspace = true diff --git a/crates/jolt-spartan-verifier/src/key.rs b/crates/jolt-spartan-verifier/src/key.rs index 147f6e0682..b9a3af98dc 100644 --- a/crates/jolt-spartan-verifier/src/key.rs +++ b/crates/jolt-spartan-verifier/src/key.rs @@ -1,8 +1,8 @@ use blake2::{digest::consts::U32, Blake2b, Digest}; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_poly::EqPolynomial; use jolt_r1cs::ConstraintMatrices; -use jolt_transcript::{AppendToTranscript, Label, Transcript}; +use jolt_transcript::Channel; use crate::{outer_relation, SpartanError}; @@ -147,40 +147,43 @@ impl SpartanKey { } } -impl SpartanKey { - /// Bind the authenticated relation, public input, and commitment before tau. - pub fn begin( +/// The Spartan message schedule, shared by both roles: every public value and +/// prover message outside the two sumchecks and the PCS calls is bound here, +/// in this order. +impl SpartanKey { + /// Binds the authenticated relation and the public input. The caller then + /// exchanges the witness commitment and calls [`Self::draw_tau`]. + pub fn bind_statement( &self, public_inputs: &[F], - commitment: &C, - transcript: &mut impl Transcript, - ) -> Result, SpartanError> { + channel: &mut C, + ) -> Result<(), SpartanError> { self.validate_public_inputs(public_inputs)?; - transcript.append(&Label(b"spartan-clear-v1")); - transcript.append_bytes(&self.relation_digest); - transcript.append_bytes(&self.policy_id); - transcript.append_values(b"public-inputs", public_inputs); - transcript.append_labeled(b"witness-commitment", commitment); - let tau = transcript.challenge_vector(self.row_vars()); - transcript.append_labeled(b"spartan-outer", &F::zero()); - Ok(tau) + channel.public(&self.relation_digest); + channel.public(&self.policy_id); + channel.public_all(public_inputs); + Ok(()) + } + + /// Draws the outer sumcheck point `tau` and binds its zero input claim. + pub fn draw_tau(&self, channel: &mut C) -> Vec { + let tau = (0..self.row_vars()).map(|_| channel.challenge()).collect(); + channel.public(&F::zero()); + tau } - /// Absorb outer claims, draw matrix weights, and bind the inner input claim. - pub fn begin_inner( + /// Exchanges the outer evaluations, draws the matrix weights, and binds + /// the inner input claim it returns with the weights. + pub fn begin_inner( &self, row_weights: &[F], public_inputs: &[F], - evaluations: [F; 3], - transcript: &mut impl Transcript, + evaluations: &mut [F; 3], + channel: &mut C, ) -> Result<([F; 3], F), SpartanError> { self.validate_public_inputs(public_inputs)?; - transcript.append_values(b"outer-evaluations", &evaluations); - let weights = [ - transcript.challenge(), - transcript.challenge(), - transcript.challenge(), - ]; + channel.exchange_all(evaluations)?; + let weights: [F; 3] = std::array::from_fn(|_| channel.challenge()); let public = std::iter::once(F::one()) .chain(public_inputs.iter().copied()) .collect::>(); @@ -193,19 +196,19 @@ impl SpartanKey { )?; let claim = weights .iter() - .zip(evaluations) + .zip(evaluations.iter()) .map(|(weight, evaluation)| *weight * evaluation) .sum::() - contribution; - transcript.append_labeled(b"spartan-inner", &claim); + channel.public(&claim); Ok((weights, claim)) } - /// Binds the terminal witness claim before the PCS opening transcript. - pub fn append_witness_evaluation( - evaluation: F, - transcript: &mut impl Transcript, - ) { - transcript.append_labeled(b"witness-evaluation", &evaluation); + /// Exchanges the terminal witness claim, which the PCS opening then proves. + pub fn exchange_witness_evaluation( + evaluation: &mut F, + channel: &mut C, + ) -> Result<(), SpartanError> { + Ok(channel.exchange(evaluation)?) } } diff --git a/crates/jolt-spartan-verifier/src/lib.rs b/crates/jolt-spartan-verifier/src/lib.rs index b3c0a31a52..3ce574f8fc 100644 --- a/crates/jolt-spartan-verifier/src/lib.rs +++ b/crates/jolt-spartan-verifier/src/lib.rs @@ -21,16 +21,12 @@ mod key; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::{CommitmentScheme, OpeningsError}; use jolt_poly::EqPolynomial; use jolt_r1cs::ConstraintMatrixEvalError; -use jolt_sumcheck::{ - BooleanHypercube, CompressedSumcheckProof, SumcheckClaim, SumcheckError, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, -}; -use jolt_transcript::{AppendToTranscript, Transcript}; -use serde::{Deserialize, Serialize}; +use jolt_sumcheck::{SumcheckClaim, SumcheckError, SumcheckVerifier}; +use jolt_transcript::{Sponge, TranscriptError, VerifierTranscript}; use thiserror::Error; pub use key::SpartanKey; @@ -50,20 +46,6 @@ pub fn inner_relation(linear: F, witness: F) -> F { linear * witness } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound( - serialize = "F: Serialize, C: Serialize, O: Serialize", - deserialize = "C: Deserialize<'de>, O: Deserialize<'de>" -))] -pub struct SpartanProof { - pub witness_commitment: C, - pub outer: CompressedSumcheckProof, - pub outer_evaluations: [F; 3], - pub inner: CompressedSumcheckProof, - pub witness_evaluation: F, - pub opening: O, -} - #[derive(Debug, Error)] pub enum SpartanError { #[error("invalid R1CS shape or public/private partition")] @@ -86,55 +68,48 @@ pub enum SpartanError { Matrix(#[from] ConstraintMatrixEvalError), #[error(transparent)] Opening(#[from] OpeningsError), + #[error(transparent)] + Transcript(#[from] TranscriptError), } -impl SpartanKey { - /// Verify against this authenticated relation and the application's PCS key. +impl SpartanKey { + /// Verify against this authenticated relation and the application's PCS key, + /// reading the proof from `transcript`. /// - /// The application's key policy must fix PCS setup and transcript choices. - /// Container decoding limits and authentication remain application-owned. - pub fn verify>( + /// The application's key policy must fix PCS setup and transcript choices, + /// and the caller finishes the transcript. + pub fn verify, H: Sponge>( &self, public_inputs: &[F], - proof: &SpartanProof, pcs_setup: &PCS::VerifierSetup, - transcript: &mut impl Transcript, - ) -> Result<(), SpartanError> - where - PCS::Output: AppendToTranscript, - { - let tau = self.begin(public_inputs, &proof.witness_commitment, transcript)?; - let outer = proof.outer.verify( + transcript: &mut VerifierTranscript<'_, H>, + ) -> Result<(), SpartanError> { + self.bind_statement(public_inputs, transcript)?; + let witness_commitment = PCS::receive_commitment(pcs_setup, transcript)?; + let tau = self.draw_tau(transcript); + let outer = SumcheckVerifier::verify_compressed( &SumcheckClaim { num_vars: self.row_vars(), degree: OUTER_DEGREE, claimed_sum: F::zero(), }, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, transcript, )?; - self.check_outer( - &tau, - outer.point.as_slice(), - outer.value, - proof.outer_evaluations, - )?; + let mut outer_evaluations = [F::zero(); 3]; let row_weights = EqPolynomial::new(outer.point.as_slice().to_vec()).evaluations(); let (weights, claim) = self.begin_inner( &row_weights, public_inputs, - proof.outer_evaluations, + &mut outer_evaluations, transcript, )?; - let inner = proof.inner.verify( + self.check_outer(&tau, outer.point.as_slice(), outer.value, outer_evaluations)?; + let inner = SumcheckVerifier::verify_compressed( &SumcheckClaim { num_vars: self.witness_vars(), degree: INNER_DEGREE, claimed_sum: claim, }, - BooleanHypercube, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, transcript, )?; let column_weights = EqPolynomial::new(inner.point.as_slice().to_vec()).evaluations(); @@ -148,15 +123,15 @@ impl SpartanKey { self.witness_len(), weights, )?; - if inner.value != inner_relation(linear, proof.witness_evaluation) { + let mut witness_evaluation = F::zero(); + Self::exchange_witness_evaluation(&mut witness_evaluation, transcript)?; + if inner.value != inner_relation(linear, witness_evaluation) { return Err(SpartanError::InnerClaim); } - Self::append_witness_evaluation(proof.witness_evaluation, transcript); PCS::verify( - &proof.witness_commitment, + &witness_commitment, inner.point.as_slice(), - proof.witness_evaluation, - &proof.opening, + witness_evaluation, pcs_setup, transcript, )?; diff --git a/crates/jolt-sumcheck/Cargo.toml b/crates/jolt-sumcheck/Cargo.toml index 23f1e3ea69..de84c0fab8 100644 --- a/crates/jolt-sumcheck/Cargo.toml +++ b/crates/jolt-sumcheck/Cargo.toml @@ -33,7 +33,7 @@ tracing.workspace = true thiserror.workspace = true [dev-dependencies] -bincode = { workspace = true } +spongefish = { workspace = true, features = ["sha3"] } jolt-crypto = { workspace = true, features = ["bn254"] } jolt-field = { workspace = true, features = ["bn254", "solinas", "binary"] } jolt-transcript = { workspace = true, features = [ 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-sumcheck/examples/clear_custom_transcript.rs b/crates/jolt-sumcheck/examples/clear_custom_transcript.rs index 67985d7b13..5cd7067008 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, ClearSumcheckRecorder, ProveRounds, - SequentialRounds, SumcheckClaim, SumcheckError, 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,18 +30,18 @@ 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(()) } } fn main() -> Result<(), SumcheckError> { 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, @@ -96,32 +61,26 @@ fn main() -> Result<(), SumcheckError> { &mut recorder, &mut prover_transcript, )?; - let recorded = recorder.finish(&proved.member_claims, &mut prover_transcript)?; - let proof = recorded.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/fuzz/Cargo.lock b/crates/jolt-sumcheck/fuzz/Cargo.lock index aa9d485644..ca919d6b90 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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,12 +451,13 @@ 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", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -576,13 +504,13 @@ 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", ] [[package]] @@ -625,7 +553,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", ] @@ -1047,8 +975,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", 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 942e450a8c..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,57 +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; } - 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 a7ea6b411b..2dc47e4e8c 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); if data.len() < 4 + (1 + 2 * n) * SCALAR_BYTES { return; } @@ -54,18 +60,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(); @@ -78,79 +84,73 @@ fuzz_target!(|data: &[u8]| { s1 += a1 * b1; 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"); - 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 => { if corruption_scalar.is_zero() { return; } - claimed_sum += corruption_scalar; + claim.claimed_sum += corruption_scalar; } 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..); } - _ => { - 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 791839de8e..3105558a29 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"; + const SCALAR_BYTES: usize = 32; /// Cap on `num_vars` to keep the fuzz iteration cheap. Real sumchecks bind up @@ -29,52 +32,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; 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); - 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 a80c4333d7..71f0c13a0f 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; @@ -44,91 +44,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 { - 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 { + 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 0f2821d709..5a1ab92649 100644 Binary files a/crates/jolt-sumcheck/fuzz/seeds/sumcheck_verifier/one-round-poly and b/crates/jolt-sumcheck/fuzz/seeds/sumcheck_verifier/one-round-poly differ diff --git a/crates/jolt-sumcheck/src/batch.rs b/crates/jolt-sumcheck/src/batch.rs index 5b6628217c..12c4441bbd 100644 --- a/crates/jolt-sumcheck/src/batch.rs +++ b/crates/jolt-sumcheck/src/batch.rs @@ -126,7 +126,7 @@ impl BatchPrelude { /// Validates dimensions as [`crate::prove_batch`] does, including the /// padding-exponent limit. Then returns /// [`SumcheckError::RoundMemberIndexOutOfRange`] for an absent member or - /// [`SumcheckError::WrongNumberOfRounds`] for an incorrect challenge count. + /// [`SumcheckError::WrongNumberOfChallenges`] for an incorrect challenge count. pub fn member_output_scale( &self, member: usize, @@ -141,7 +141,7 @@ impl BatchPrelude { members: self.members.len(), })?; if challenges.len() != self.max_num_vars { - return Err(SumcheckError::WrongNumberOfRounds { + return Err(SumcheckError::WrongNumberOfChallenges { expected: self.max_num_vars, got: challenges.len(), }); diff --git a/crates/jolt-sumcheck/src/committed.rs b/crates/jolt-sumcheck/src/committed.rs index c1e45893db..29928a3cac 100644 --- a/crates/jolt-sumcheck/src/committed.rs +++ b/crates/jolt-sumcheck/src/committed.rs @@ -1,67 +1,35 @@ +//! +//! 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, @@ -200,12 +168,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 @@ -215,14 +183,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, { @@ -233,22 +200,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(), @@ -257,79 +221,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 64a44100ad..4264f6e4f9 100644 --- a/crates/jolt-sumcheck/src/domain.rs +++ b/crates/jolt-sumcheck/src/domain.rs @@ -1,24 +1,24 @@ use crate::error::SumcheckError; -use crate::round_proof::ClearRound; use core::ops::Range; 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, @@ -27,7 +27,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 5967b267a6..59f9b2bb2f 100644 --- a/crates/jolt-sumcheck/src/error.rs +++ b/crates/jolt-sumcheck/src/error.rs @@ -1,8 +1,13 @@ use jolt_field::Field; +use jolt_transcript::TranscriptError; #[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}")] @@ -70,17 +75,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}")] @@ -100,16 +94,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, - }, - #[error("round polynomial must contain at least one coefficient")] EmptyRoundCoefficients, @@ -189,16 +173,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 { @@ -224,4 +198,13 @@ pub enum SumcheckError { /// Consecutive integer domain points have colliding images in the field. #[error("integer sumcheck domain of size {domain_size} is not distinct in the field")] IntegerDomainNotDistinct { domain_size: usize }, + + /// A batch's round challenges were supplied with the wrong count. + #[error("expected {expected} round challenges, got {got}")] + WrongNumberOfChallenges { + /// The batch's round count. + expected: usize, + /// Number of challenges supplied. + got: usize, + }, } diff --git a/crates/jolt-sumcheck/src/lib.rs b/crates/jolt-sumcheck/src/lib.rs index 2ceb08fed5..8c271e960b 100644 --- a/crates/jolt-sumcheck/src/lib.rs +++ b/crates/jolt-sumcheck/src/lib.rs @@ -29,14 +29,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 | //! @@ -47,27 +46,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 //! @@ -80,10 +71,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 @@ -107,7 +98,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; @@ -120,42 +110,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, @@ -164,6 +134,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 e3573e20dd..0000000000 --- a/crates/jolt-sumcheck/src/proof.rs +++ /dev/null @@ -1,210 +0,0 @@ -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 From> for SumcheckProof { - fn from(proof: ClearSumcheckProof) -> Self { - Self::Clear(ClearProof::Full(proof)) - } -} - -impl From> for SumcheckProof { - fn from(proof: CompressedSumcheckProof) -> Self { - Self::Clear(ClearProof::Compressed(proof)) - } -} - -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 d30e72e58c..12163b48a8 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, PaddingRule}; #[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::{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 @@ -163,17 +160,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 and the padding rule of [`BatchPrelude`], self-check @@ -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 { @@ -316,7 +302,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 { @@ -326,7 +312,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); @@ -374,23 +360,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: ClearSumcheckProof, +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, @@ -399,91 +383,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, + 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: 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 724af06a0c..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); @@ -15,148 +15,110 @@ //! also runs `begin_batch` (with [`ClearSumcheckRecorder`]) so the two sides //! share the head's Fiat-Shamir sequence structurally. +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::CompressedSumcheckProof; -#[cfg(feature = "committed")] -use crate::proof::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 encoding produced by this recorder. - type Proof; + /// 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: P, - 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>, +/// 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(), + _field: PhantomData, } } } -impl SumcheckRecorder for ClearSumcheckRecorder { - type Proof = CompressedSumcheckProof; +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: 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> @@ -189,37 +151,24 @@ where VC: VectorCommitment, R: RngCore, { - type Proof = SumcheckProof; + 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 028d344781..a43915e38d 100644 --- a/crates/jolt-sumcheck/src/round_proof.rs +++ b/crates/jolt-sumcheck/src/round_proof.rs @@ -1,164 +1,115 @@ -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}; -pub trait RoundMessage { - fn degree(&self) -> usize; - - fn append_to_transcript(&self, transcript: &mut T); -} - -/// 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(()) - } -} - -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() - } -} - -/// 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); - } - } +/// `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) } -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, - ) - } +/// 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(()) } -/// 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) - } +/// 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(()) } -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); - } - } +/// 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 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/round_scheduler_tests.rs b/crates/jolt-sumcheck/src/round_scheduler_tests.rs index 44abce3a9c..9901e26c14 100644 --- a/crates/jolt-sumcheck/src/round_scheduler_tests.rs +++ b/crates/jolt-sumcheck/src/round_scheduler_tests.rs @@ -7,7 +7,7 @@ )] 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; @@ -15,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; @@ -52,7 +52,7 @@ fn traversal_fixture() -> ( DenseMember, DenseMember, BatchPrelude, - Blake2bTranscript, + ProverTranscript, ClearSumcheckRecorder, ) { let sum_long = F::from_u64(4242); @@ -60,11 +60,11 @@ fn traversal_fixture() -> ( 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 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 { @@ -103,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(&mut 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 b01af9c9b5..e94fa1cdff 100644 --- a/crates/jolt-sumcheck/src/tests.rs +++ b/crates/jolt-sumcheck/src/tests.rs @@ -1,246 +1,179 @@ +//! 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; +use jolt_poly::EvaluationClaim; type F = Fr; +type H = Blake2b512; +type VC = Pedersen; -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; - - 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 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); +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/unit-tests"); - let r: F = transcript.challenge(); - round_polys.push(round_poly); - - for i in 0..half { - buf[i] = buf[i] + r * (buf[i + half] - buf[i]); - } - buf.truncate(half); - } - - 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: &mut C) -> [u8; 32] { + channel.challenge_bytes::<32>() +} - ( - 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()) } -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() { - 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 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), - }; +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 mut untouched = prover(b"over-degree"); - let mut transcript = Blake2bTranscript::new(b"sumcheck-integer-domain-test"); - let result = SumcheckVerifier::verify( - &claim, - std::slice::from_ref(&round_poly), - CenteredIntegerDomain::new(3), + 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!(transcript.narg().is_empty()); + assert_eq!(fingerprint(&mut transcript), fingerprint(&mut untouched)); +} + +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, - ) - .unwrap(); + )?; + transcript.finish()?; + Ok(reduced) +} - assert_eq!(result.point.len(), 1); - assert_eq!(result.value, round_poly.evaluate(result.point[0])); +#[test] +fn centered_integer_domain_verifies_round_sum() { + // 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 }) @@ -276,373 +209,58 @@ 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); - - 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); - - let _ = proof.round_polynomials.pop(); - - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, +fn batched_committed_consistency_accessors() { + // `BatchedCommittedSumcheckConsistency` is produced by the generated ZK + // verify driver (jolt-verifier-derive) and read back by BlindFold through + // these accessors. Exercise the front-loaded suffix arithmetic and its + // range errors directly on a hand-built instance. + let consistency = CommittedSumcheckConsistency { + rounds: vec![ + VerifiedCommittedRound { + commitment: F::from_u64(11), + degree: 2, + challenge: F::from_u64(101), + }, + VerifiedCommittedRound { + commitment: F::from_u64(12), + degree: 1, + challenge: F::from_u64(102), + }, + VerifiedCommittedRound { + commitment: F::from_u64(13), + degree: 0, + challenge: F::from_u64(103), + }, + ], }; - - 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); - - 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, - claimed_sum: sum, + let batched = BatchedCommittedSumcheckConsistency { + consistency, + batching_coefficients: vec![F::from_u64(7), F::from_u64(9)], + max_num_vars: 3, + max_degree: 2, }; - 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 clear_round_verifier_with_label_absorbs_label() { - 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(); - - 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(); - - 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() { - 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(); - + let challenges = vec![F::from_u64(101), F::from_u64(102), F::from_u64(103)]; + assert_eq!(batched.challenges(), challenges); + assert_eq!(batched.try_round_offset(1).unwrap(), 2); 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, + batched.try_instance_point(1).unwrap(), + challenges[2..].to_vec() ); - + assert_eq!(batched.try_instance_point_at(0, 3).unwrap(), challenges); assert!(matches!( - result, - Err(SumcheckError::WrongProofEncoding { - expected: "full clear", - got: "compressed clear", + batched.try_instance_point(4), + Err(SumcheckError::BatchedPointOutOfRange { + offset: 0, + num_vars: 4, + total: 3 }) )); -} - -#[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", + batched.try_instance_point_at(usize::MAX, 1), + Err(SumcheckError::BatchedPointRangeOverflow { + offset: usize::MAX, + num_vars: 1 }) )); } @@ -767,261 +385,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_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))); -} - -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); - assert_eq!(final_eval, fixed); -} - #[test] #[should_panic(expected = "degree >= 1")] fn sumcheck_claim_new_rejects_degree_zero() { @@ -1034,23 +397,18 @@ 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(); - let mut prover_transcript = Blake2bTranscript::new(b"recorder-test"); + let mut prover_transcript = prover(b"recorder"); let mut recorder = ClearSumcheckRecorder::::new(); recorder.absorb_input_claims(&[claimed_sum], &mut prover_transcript); @@ -1062,7 +420,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 { @@ -1071,24 +429,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(&mut prover_transcript); + let narg = prover_transcript.finish(); - let mut verifier_transcript = Blake2bTranscript::new(b"recorder-test"); - append_sumcheck_claim(&mut verifier_transcript, &claimed_sum); - let reduction = SumcheckProof::::from(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(&mut 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, @@ -1106,9 +470,7 @@ impl DenseMember { fn final_eval(&self) -> F { self.evals[0] } -} -impl DenseMember { fn bind(&mut self, challenge: F) { let half = self.evals.len() / 2; for i in 0..half { @@ -1118,7 +480,7 @@ impl DenseMember { } } -impl crate::prover::ProveRounds for DenseMember { +impl ProveRounds for DenseMember { fn num_rounds(&self) -> usize { self.num_rounds } @@ -1145,6 +507,129 @@ impl crate::prover::ProveRounds for DenseMember { } } +/// A dense member exercising the fused contract for real: on each +/// `prove_round` the pending bind and the round evaluation happen in ONE pass +/// over the table (each pair is bound and immediately accumulated), never +/// leaving a fully bound intermediate table behind. +struct FusedDenseMember { + evals: Vec, + num_rounds: usize, +} + +impl ProveRounds for FusedDenseMember { + 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 { + let half = self.evals.len() / 2; + let quarter = half / 2; + let mut eval_0 = F::from_u64(0); + let mut eval_1 = F::from_u64(0); + for i in 0..half { + let bound = self.evals[i] + challenge * (self.evals[i + half] - self.evals[i]); + self.evals[i] = bound; + if i < quarter { + eval_0 += bound; + } else { + eval_1 += bound; + } + } + self.evals.truncate(half); + assert_eq!(eval_0 + eval_1, previous_claim); + return Ok(UnivariatePoly::new(vec![eval_0, eval_1 - eval_0])); + } + let half = self.evals.len() / 2; + let eval_0: F = self.evals[..half].iter().copied().sum(); + let eval_1: F = self.evals[half..].iter().copied().sum(); + assert_eq!(eval_0 + eval_1, previous_claim); + Ok(UnivariatePoly::new(vec![eval_0, eval_1 - eval_0])) + } + + fn finish_rounds(&mut self, bind: F) -> Result<(), SumcheckError> { + let half = self.evals.len() / 2; + for i in 0..half { + self.evals[i] = self.evals[i] + bind * (self.evals[i + half] - self.evals[i]); + } + self.evals.truncate(half); + Ok(()) + } +} + +/// 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. +#[test] +fn fused_bind_eval_member_byte_matches_separate_passes() { + let num_rounds = 4; + let sum = F::from_u64(90210); + let prove = |member: &mut dyn ProveRounds| { + 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, + &mut members, + &mut SequentialRounds, + &mut recorder, + &mut transcript, + ) + .unwrap(); + recorder + .finish(&proved.member_claims, &mut transcript) + .unwrap(); + let state = fingerprint(&mut transcript); + (proved, transcript.finish(), state) + }; + + let mut separate = DenseMember::with_sum(num_rounds, sum, 41); + let mut fused = FusedDenseMember { + evals: separate.evals.clone(), + num_rounds, + }; + 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_narg, fused_narg); + assert_eq!(separate.final_eval(), fused.evals[0]); + assert_eq!(separate_state, fused_state); +} + fn pedersen_setup(capacity: u64) -> PedersenSetup { let generator = Bn254::g1_generator(); let generators = (2..2 + capacity) @@ -1158,10 +643,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 { @@ -1176,7 +657,7 @@ 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 transcript = prover(b"zero-degree-batch"); let mut recorder = ClearSumcheckRecorder::::new(); let result = prove_batch( &prelude, @@ -1191,48 +672,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); - - let mut prover_transcript = Blake2bTranscript::new(b"prove-batch-twin"); +/// 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(); - 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, 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, @@ -1241,164 +701,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()); + let prover_state = fingerprint(&mut prover_transcript); + let narg = prover_transcript.finish(); - 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 mut verifier_transcript = verifier(session, &narg); + let verifier_coefficients = batch_head( + &mut ClearSumcheckRecorder::::new(), + &input_claims, + &mut verifier_transcript, ); - - let claimed_sum = - verifier_coeff_long * sum_long + verifier_coeff_short * sum_short.mul_pow_2(2); + 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 = SumcheckProof::::from(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(&mut verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); + proved +} + +#[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()] + ); } -/// 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. +/// 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(); 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 = SumcheckProof::::from(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( @@ -1409,41 +803,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(&mut 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()); + assert_eq!(fingerprint(&mut verifier_transcript), prover_state); + verifier_transcript.finish().unwrap(); - 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!(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) @@ -1451,85 +840,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); } -/// 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. +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) +} + +/// 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(&mut prover_transcript); + let narg = prover_transcript.finish(); - let mut verifier_transcript = Blake2bTranscript::new(b"uniskip-twin"); - let reduction = SumcheckProof::::from(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(&mut 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, @@ -1538,17 +926,38 @@ fn prove_uniskip_committed_twin_matches_committed_consistency() { &mut prover_transcript, ) .unwrap(); + let prover_state = fingerprint(&mut 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(&mut 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/src/verifier.rs b/crates/jolt-sumcheck/src/verifier.rs index a191411293..5222c18961 100644 --- a/crates/jolt-sumcheck/src/verifier.rs +++ b/crates/jolt-sumcheck/src/verifier.rs @@ -1,214 +1,115 @@ -use jolt_field::Field; -use jolt_poly::UnivariatePolynomial; -use jolt_transcript::{AppendToTranscript, LabelWithCount, Transcript}; +//! Sumcheck verifier: reads round polynomials from the proof and checks them +//! against the running sum. + +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}; 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)) } } diff --git a/crates/jolt-sumcheck/tests/binary_fields.rs b/crates/jolt-sumcheck/tests/binary_fields.rs index 574d86bd9f..2c46173a90 100644 --- a/crates/jolt-sumcheck/tests/binary_fields.rs +++ b/crates/jolt-sumcheck/tests/binary_fields.rs @@ -1,24 +1,22 @@ -#![expect( - clippy::panic, - clippy::unwrap_used, - reason = "tests may panic on assertion failures" -)] +#![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] + +use std::vec::IntoIter; use jolt_field::{Field, Fr, JoltField, One, Ring, F128, F64}; -use jolt_poly::{CompressedPoly, Polynomial, UnivariatePoly}; +use jolt_poly::{Polynomial, UnivariatePoly}; use jolt_sumcheck::{ - prove_batch, prove_uniskip_clear, BatchMember, BatchPrelude, BooleanHypercube, - CenteredIntegerDomain, ClearProof, ClearSumcheckRecorder, EvaluationClaim, LabeledRoundPoly, - ProveRounds, ProvedBatch, RoundMessage, SequentialRounds, SumcheckClaim, SumcheckDomain, - SumcheckDomainSpec, SumcheckError, SumcheckProof, SumcheckRecorder, SumcheckVerifier, - OPENING_CLAIM_TRANSCRIPT_LABEL, SUMCHECK_CLAIM_TRANSCRIPT_LABEL, - SUMCHECK_ROUND_TRANSCRIPT_LABEL, + prove_batch, prove_uniskip_clear, send_compressed_round, send_full_round, BatchMember, + BatchPrelude, BooleanHypercube, CenteredIntegerDomain, ClearSumcheckRecorder, EvaluationClaim, + ProveRounds, ProvedBatch, SequentialRounds, SumcheckClaim, SumcheckDomain, SumcheckDomainSpec, + SumcheckError, SumcheckRecorder, SumcheckVerifier, +}; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, Sponge, VerifierTranscript, }; -use jolt_transcript::{AppendToTranscript, Blake2bTranscript, Transcript}; const LABEL: &[u8] = b"binary-sumcheck"; -trait BinaryField: JoltField + AppendToTranscript { +trait BinaryField: JoltField { fn sample(seed: u64) -> Self; } @@ -145,31 +143,98 @@ impl ProveRounds for DenseProduct { } } +const PROTOCOL: ProtocolId = ProtocolId::new::("jolt-sumcheck/binary-fields"); + +fn prover() -> ProverTranscript { + ProverTranscript::new(&PROTOCOL, LABEL) +} + +fn verifier(narg: &[u8]) -> VerifierTranscript<'_, Blake2b512> { + VerifierTranscript::new(&PROTOCOL, LABEL, narg) +} + +/// The sponge state both roles must agree on after the same message sequence. +fn fingerprint(channel: &mut C) -> [u8; 32] { + channel.challenge_bytes::<32>() +} + +/// A clear recorder whose round challenges come from a fixed script instead of +/// the transcript, to reach challenge values a sponge would draw only by +/// chance. Its argument string has no verifier counterpart. +struct ScriptedRecorder { + script: IntoIter, +} + +impl SumcheckRecorder for ScriptedRecorder { + type Witness = (); + + fn absorb_input_claims(&mut self, input_claims: &[F], channel: &mut C) { + channel.public_all(input_claims); + } + + fn absorb_round( + &mut self, + round_poly: &UnivariatePoly, + degree: usize, + transcript: &mut ProverTranscript, + ) -> Result> { + send_compressed_round(round_poly, degree, transcript)?; + Ok(self.script.next().unwrap()) + } + + fn finish( + self, + output_claim_values: &[F], + transcript: &mut ProverTranscript, + ) -> Result<(), SumcheckError> { + transcript.send_all(output_claim_values); + Ok(()) + } +} + +/// How a fixture draws its batching coefficients and round challenges. +enum Challenges { + Transcript, + Scripted { + coefficients: Vec, + rounds: Vec, + }, +} + struct BatchFixture { prelude: BatchPrelude, proved: ProvedBatch, - proof: SumcheckProof, - transcript_state: [u8; 32], + narg: Vec, + fingerprint: [u8; 32], } -impl BatchFixture { - fn prove>( +impl BatchFixture { + fn prove( original: &[DenseProduct], offsets: &[usize], max_num_vars: usize, max_degree: usize, - mut transcript: T, + challenges: Challenges, ) -> Self { - let mut recorder = ClearSumcheckRecorder::::new(); + let mut transcript = prover(); let claims: Vec<_> = original.iter().map(DenseProduct::claimed_sum).collect(); - recorder.absorb_input_claims(&claims, &mut transcript); + transcript.public_all(&claims); + let (mut coefficients, scripted_rounds) = match challenges { + Challenges::Transcript => (None, None), + Challenges::Scripted { + coefficients, + rounds, + } => (Some(coefficients.into_iter()), Some(rounds)), + }; let members = original .iter() .zip(offsets) .zip(claims) .map(|((member, &offset), input_claim)| BatchMember { input_claim, - coefficient: transcript.challenge_scalar(), + coefficient: coefficients + .as_mut() + .map_or_else(|| transcript.challenge(), |script| script.next().unwrap()), rounds: member.rounds, offset, }) @@ -186,51 +251,92 @@ impl BatchFixture { .iter_mut() .map(|member| member as &mut dyn ProveRounds) .collect(); - let proved = prove_batch( - &prelude, - &mut members, - &mut SequentialRounds, - &mut recorder, - &mut transcript, - ) - .unwrap(); - let recorded = recorder - .finish(&proved.member_claims, &mut transcript) + let proved = if let Some(rounds) = scripted_rounds { + let mut recorder = ScriptedRecorder { + script: rounds.into_iter(), + }; + let proved = prove_batch( + &prelude, + &mut members, + &mut SequentialRounds, + &mut recorder, + &mut transcript, + ) .unwrap(); + recorder + .finish(&proved.member_claims, &mut transcript) + .unwrap(); + proved + } else { + let mut recorder = ClearSumcheckRecorder::::new(); + let proved = prove_batch( + &prelude, + &mut members, + &mut SequentialRounds, + &mut recorder, + &mut transcript, + ) + .unwrap(); + recorder + .finish(&proved.member_claims, &mut transcript) + .unwrap(); + proved + }; + let narg = transcript.narg().to_vec(); Self { prelude, proved, - proof: recorded.proof.into(), - transcript_state: transcript.state(), + narg, + fingerprint: fingerprint(&mut transcript), } } - fn replay_head>(&self, transcript: &mut T) { - for member in &self.prelude.members { - transcript.append_labeled(SUMCHECK_CLAIM_TRANSCRIPT_LABEL, &member.input_claim); - } + fn replay_head(&self, transcript: &mut VerifierTranscript<'_, Blake2b512>) { + let claims: Vec = self + .prelude + .members + .iter() + .map(|member| member.input_claim) + .collect(); + transcript.public_all(&claims); for member in &self.prelude.members { - assert_eq!(transcript.challenge_scalar(), member.coefficient); + assert_eq!(transcript.challenge::(), member.coefficient); } } - fn verify>(&self, mut transcript: T) -> EvaluationClaim { + fn reduce(&self, narg: &[u8]) -> Result, SumcheckError> { + let mut transcript = verifier(narg); self.replay_head(&mut transcript); - let reduced = self - .proof - .verify_compressed_boolean( + SumcheckVerifier::verify_compressed( + &SumcheckClaim::new( self.prelude.max_num_vars, self.prelude.max_degree, self.prelude.claimed_sum, - &mut transcript, - ) + ), + &mut transcript, + ) + } + + fn verify(&self) -> EvaluationClaim { + let mut transcript = verifier(&self.narg); + self.replay_head(&mut transcript); + let reduced = SumcheckVerifier::verify_compressed( + &SumcheckClaim::new( + self.prelude.max_num_vars, + self.prelude.max_degree, + self.prelude.claimed_sum, + ), + &mut transcript, + ) + .unwrap(); + let claims: Vec = transcript + .receive_n(self.proved.member_claims.len()) .unwrap(); - for claim in &self.proved.member_claims { - transcript.append_labeled(OPENING_CLAIM_TRANSCRIPT_LABEL, claim); - } + assert_eq!(claims, self.proved.member_claims); assert_eq!(reduced.point.as_slice(), self.proved.challenges); assert_eq!(reduced.value, self.proved.final_claim); - assert_eq!(transcript.state(), self.transcript_state); + assert_eq!(fingerprint(&mut transcript), self.fingerprint); + transcript.finish().unwrap(); reduced } @@ -258,29 +364,17 @@ impl BatchFixture { fn single_members() { for degree in 1..=3 { let products = [DenseProduct::::sampled(4, degree, 11)]; - let script = vec![ - F::one(), - F::sample(1001), - F::sample(1002), - F::sample(1003), - F::sample(1004), - ]; - let fixture = BatchFixture::prove( - &products, - &[0], - 4, - degree, - ScriptedTranscript::with_script(script.clone()), - ); - let reduced = fixture.verify(ScriptedTranscript::with_script(script)); + let fixture = BatchFixture::prove(&products, &[0], 4, degree, Challenges::Transcript); + let reduced = fixture.verify(); assert_eq!( fixture.prelude.members[0].input_claim, products[0].claimed_sum() ); let expected = products[0].evaluate(reduced.point.as_slice()); assert_eq!(fixture.proved.member_claims[0], expected); - assert_eq!(fixture.proved.final_claim, expected); - assert_eq!(reduced.value, expected); + let scaled = fixture.prelude.members[0].coefficient * expected; + assert_eq!(fixture.proved.final_claim, scaled); + assert_eq!(reduced.value, scaled); fixture.check_output_scales(&products); } } @@ -292,14 +386,8 @@ fn mixed_linear_members() { DenseProduct::::sampled(2, 1, 300), DenseProduct::::sampled(2, 1, 400), ]; - let fixture = BatchFixture::prove( - &products, - &[0, 2, 0, 1], - 4, - 1, - Blake2bTranscript::::new(LABEL), - ); - let _ = fixture.verify(Blake2bTranscript::::new(LABEL)); + let fixture = BatchFixture::prove(&products, &[0, 2, 0, 1], 4, 1, Challenges::Transcript); + let _ = fixture.verify(); for (member, indices) in [(1, [0, 1, 2, 3]), (2, [0, 4, 8, 12]), (3, [0, 2, 4, 6])] { let mut extended = vec![F::zero(); 16]; for (&value, index) in products[member].factors[0].iter().zip(indices) { @@ -319,14 +407,8 @@ fn mixed_quadratic_members() { DenseProduct::::sampled(2, 2, 200), DenseProduct::::sampled(2, 2, 300), ]; - let fixture = BatchFixture::prove( - &products, - &[0, 2, 1], - 4, - 2, - Blake2bTranscript::::new(LABEL), - ); - let _ = fixture.verify(Blake2bTranscript::::new(LABEL)); + let fixture = BatchFixture::prove(&products, &[0, 2, 1], 4, 2, Challenges::Transcript); + let _ = fixture.verify(); let r = &fixture.proved.challenges; assert_eq!( fixture.proved.member_claims[1], @@ -349,79 +431,29 @@ fn zero_native_claim() { rounds: 1, }, ]; - let fixture = BatchFixture::prove(&products, &[0, 1], 2, 1, Blake2bTranscript::::new(LABEL)); + let fixture = BatchFixture::prove(&products, &[0, 1], 2, 1, Challenges::Transcript); assert_eq!(fixture.prelude.members[1].input_claim, F::zero()); assert_eq!( fixture.proved.member_claims[1], u * (F::one() - fixture.proved.challenges[0]) ); assert!(!fixture.proved.member_claims[1].is_zero()); - let _ = fixture.verify(Blake2bTranscript::::new(LABEL)); + let _ = fixture.verify(); fixture.check_output_scales(&products); } -#[derive(Default)] -struct ScriptedTranscript { - script: Vec, - next: usize, - state: [u8; 32], - cursor: usize, -} - -impl ScriptedTranscript { - fn with_script(script: Vec) -> Self { - let mut transcript = Self { - script, - ..Self::default() - }; - transcript.append_bytes(LABEL); - transcript - } -} - -impl Transcript for ScriptedTranscript { - 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 { - let challenge = self.script[self.next]; - self.next += 1; - self.append_bytes(b"scripted_challenge"); - challenge.append_to_transcript(self); - challenge - } - - fn state(&self) -> [u8; 32] { - self.state - } -} - fn zero_padding_factor() { for (offset, challenges) in [(1, [F::one(), F::sample(6)]), (0, [F::sample(6), F::one()])] { let products = [DenseProduct::::sampled(1, 2, 200)]; - let script = vec![F::sample(19), challenges[0], challenges[1]]; let fixture = BatchFixture::prove( &products, &[offset], 2, 2, - ScriptedTranscript::with_script(script.clone()), + Challenges::Scripted { + coefficients: vec![F::sample(19)], + rounds: challenges.to_vec(), + }, ); assert_eq!(fixture.proved.challenges, challenges); assert_eq!( @@ -430,7 +462,6 @@ fn zero_padding_factor() { ); assert_eq!(fixture.proved.member_claims, vec![F::zero()]); assert_eq!(fixture.proved.final_claim, F::zero()); - let _ = fixture.verify(ScriptedTranscript::with_script(script)); fixture.check_output_scales(&products); } } @@ -440,7 +471,7 @@ fn rejection() { DenseProduct::::sampled(4, 2, 31), DenseProduct::::sampled(2, 2, 97), ]; - let fixture = BatchFixture::prove(&products, &[0, 1], 4, 2, Blake2bTranscript::::new(LABEL)); + let fixture = BatchFixture::prove(&products, &[0, 1], 4, 2, Challenges::Transcript); let mut changed_prelude = fixture.prelude.clone(); changed_prelude.claimed_sum += F::one(); let mut members = products.clone(); @@ -448,32 +479,26 @@ fn rejection() { .iter_mut() .map(|member| member as &mut dyn ProveRounds) .collect(); - let mut transcript = Blake2bTranscript::::new(LABEL); - fixture.replay_head(&mut transcript); assert!(matches!( prove_batch( &changed_prelude, &mut references, &mut SequentialRounds, &mut ClearSumcheckRecorder::::new(), - &mut transcript + &mut prover() ), Err(SumcheckError::RoundCheckFailed { round: 0, .. }), )); - let mut altered = fixture.proof.clone(); - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = &mut altered else { - panic!("expected a compressed proof") - }; - let round = proof.round_polynomials.last_mut().unwrap(); - let mut coefficients = round.coeffs_except_linear_term().to_vec(); - coefficients[0] += F::one(); - *round = CompressedPoly::new(coefficients); - let mut transcript = Blake2bTranscript::::new(LABEL); - fixture.replay_head(&mut transcript); - let reduced = altered - .verify_compressed_boolean(4, 2, fixture.prelude.claimed_sum, &mut transcript) - .unwrap(); + // Each compressed degree-2 round carries two coefficients; shift the last + // round's constant term. + let mut altered = fixture.narg.clone(); + let last_round = 3 * 2 * F::NUM_BYTES; + let constant = F::from_bytes_le_checked(&altered[last_round..last_round + F::NUM_BYTES]) + .unwrap() + + F::one(); + altered[last_round..last_round + F::NUM_BYTES].copy_from_slice(&constant.to_bytes_le_vec()); + let reduced = fixture.reduce(&altered).unwrap(); let expected: F = fixture .prelude .members @@ -493,60 +518,42 @@ fn rejection() { .sum(); assert_ne!(reduced.value, expected); - let mut missing_round = fixture.proof.clone(); - let SumcheckProof::Clear(ClearProof::Compressed(proof)) = &mut missing_round else { - panic!("expected a compressed proof") - }; - let _ = proof.round_polynomials.pop().unwrap(); - let mut transcript = Blake2bTranscript::::new(LABEL); - fixture.replay_head(&mut transcript); - let state = transcript.state(); + let missing_round = &fixture.narg[..last_round]; assert!(matches!( - missing_round.verify_compressed_boolean(4, 2, fixture.prelude.claimed_sum, &mut transcript), - Err(SumcheckError::WrongNumberOfRounds { - expected: 4, - got: 3 - }) + fixture.reduce(missing_round), + Err(SumcheckError::Transcript(_)) )); - assert_eq!(transcript.state(), state); } fn uncompressed() { let original = DenseProduct::::sampled(4, 3, 27); let input_claim = original.claimed_sum(); let mut member = original.clone(); - let mut prover = Blake2bTranscript::::new(LABEL); + let mut transcript = prover(); let mut previous_claim = input_claim; let mut bind = None; - let mut rounds = Vec::new(); let mut challenges = Vec::new(); for round in 0..4 { let polynomial = member.prove_round(bind, round, previous_claim).unwrap(); - LabeledRoundPoly::new(&polynomial, SUMCHECK_ROUND_TRANSCRIPT_LABEL) - .append_to_transcript(&mut prover); - let challenge = prover.challenge(); + send_full_round(&polynomial, 3, &mut transcript).unwrap(); + let challenge = transcript.challenge_small(); previous_claim = polynomial.evaluate(challenge); bind = Some(challenge); challenges.push(challenge); - rounds.push(polynomial); } member.finish_rounds(bind.unwrap()).unwrap(); - let labeled: Vec<_> = rounds - .iter() - .map(|polynomial| LabeledRoundPoly::new(polynomial, SUMCHECK_ROUND_TRANSCRIPT_LABEL)) - .collect(); - let mut verifier = Blake2bTranscript::::new(LABEL); + let narg = transcript.narg().to_vec(); + let mut reader = verifier(&narg); let reduced = SumcheckVerifier::verify( &SumcheckClaim::new(4, 3, input_claim), - &labeled, BooleanHypercube, - &mut verifier, + &mut reader, ) .unwrap(); assert_eq!(reduced.point.as_slice(), challenges); assert_eq!(reduced.value, original.evaluate(&challenges)); assert_eq!(reduced.value, previous_claim); - assert_eq!(prover.state(), verifier.state()); + assert_eq!(fingerprint(&mut transcript), fingerprint(&mut reader)); } fn output_scale_errors() { @@ -566,7 +573,7 @@ fn output_scale_errors() { )); assert!(matches!( prelude.member_output_scale(0, &[F::sample(7); 3]), - Err(SumcheckError::WrongNumberOfRounds { + Err(SumcheckError::WrongNumberOfChallenges { expected: 4, got: 3 }) @@ -634,18 +641,18 @@ fn integer_domains() { CenteredIntegerDomain::new(3).check_round_sum(0, F::sample(4), &polynomial), Err(SumcheckError::IntegerDomainNotDistinct { domain_size: 3 }) )); - let mut transcript = Blake2bTranscript::::new(LABEL); - let state = transcript.state(); + let mut transcript = prover(); + let mut untouched = prover(); assert!(matches!( prove_uniskip_clear::(polynomial.clone(), F::sample(4), 2, 3, &mut transcript), Err(SumcheckError::IntegerDomainNotDistinct { domain_size: 3 }) )); - assert_eq!(transcript.state(), state); assert!(matches!( prove_uniskip_clear::(polynomial, F::sample(4), 1, 3, &mut transcript), Err(SumcheckError::DegreeBoundExceeded { got: 2, max: 1 }) )); - assert_eq!(transcript.state(), state); + assert!(transcript.narg().is_empty()); + assert_eq!(fingerprint(&mut transcript), fingerprint(&mut untouched)); } macro_rules! binary_tests { diff --git a/crates/jolt-sumcheck/tests/committed.rs b/crates/jolt-sumcheck/tests/committed.rs index db9977c227..ce1388f6d0 100644 --- a/crates/jolt-sumcheck/tests/committed.rs +++ b/crates/jolt-sumcheck/tests/committed.rs @@ -1,14 +1,30 @@ -#![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_crypto::{Bn254, Bn254G1, JoltGroup, Pedersen, PedersenSetup}; -use jolt_field::{Fr, Ring}; -use jolt_sumcheck::round_proof::RoundMessage; -use jolt_sumcheck::{CommittedRound, CommittedRoundWitness, SumcheckStatement, SumcheckVerifier}; -use jolt_transcript::{Blake2bTranscript, Transcript}; +use jolt_crypto::{ + Bn254, Bn254G1, JoltGroup, NoVectorCommitment, Pedersen, PedersenSetup, VectorCommitment, +}; +use jolt_field::{CanonicalBytes, Fr, Ring}; +use jolt_poly::UnivariatePoly; +use jolt_sumcheck::{ + CommittedOutputClaims, CommittedSumcheckBuilder, CommittedSumcheckConsistency, + CommittedSumcheckWitness, SumcheckError, SumcheckStatement, SumcheckVerifier, +}; +use jolt_transcript::{ + Blake2b512, Channel, ProtocolId, ProverTranscript, TranscriptError, VerifierTranscript, +}; +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) @@ -17,89 +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 +fn poly(coefficients: &[u64]) -> UnivariatePoly { + UnivariatePoly::new(coefficients.iter().copied().map(F::from_u64).collect()) +} + +struct Proved { + narg: Vec, + challenges: Vec, + witness: CommittedSumcheckWitness, + fingerprint: [u8; 32], +} + +/// 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() - .enumerate() - .map(|(round, coefficients)| { - CommittedRoundWitness { - coefficients: coefficients.clone(), - blinding: F::from_u64(round as u64 + 17), - } - .commit::(setup) - .unwrap() - }) - .collect() + .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.challenge_bytes::<32>(); + Proved { + narg: transcript.finish(), + challenges, + witness, + fingerprint, + } +} + +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.challenge_bytes::<32>(); + transcript.finish()?; + Ok((verified, fingerprint)) } #[test] -fn committed_rounds_complete_with_pedersen_commitments() { +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)], - vec![F::from_u64(13), F::from_u64(17), F::from_u64(19)], - ], + 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 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()); + assert_eq!( + witness.output_claim_rows, + vec![ + [23, 29, 31].map(F::from_u64).to_vec(), + vec![F::from_u64(37)] + ] + ); + 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 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()); +#[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 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!(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 3b42f08d21..0000000000 --- a/crates/jolt-sumcheck/tests/extension_fields.rs +++ /dev/null @@ -1,334 +0,0 @@ -#![expect(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::UnivariatePoly; -use jolt_sumcheck::{ - prove_batch, prove_uniskip_clear, BatchMember, BatchPrelude, BooleanHypercube, - CenteredIntegerDomain, 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::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 proof = recorded.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 = SumcheckProof::::from(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_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 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)]); -} diff --git a/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock b/crates/jolt-sumcheck/tests/external-consumer/Cargo.lock index 8040d45eb7..730d85afbe 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,20 +27,97 @@ 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", + "spongefish", "thiserror", ] @@ -42,7 +128,7 @@ dependencies = [ "jolt-field", "jolt-utils", "num-traits", - "rand_core", + "rand_core 0.6.4", "serde", "thiserror", "tracing", @@ -69,7 +155,8 @@ dependencies = [ "jolt-sumcheck", "jolt-transcript", "num-traits", - "rand_core", + "rand_core 0.6.4", + "spongefish", ] [[package]] @@ -77,6 +164,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 +177,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 +230,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 +434,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 +515,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 +634,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..f12e06e827 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml +++ b/crates/jolt-sumcheck/tests/external-consumer/Cargo.toml @@ -11,11 +11,17 @@ 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", ] } 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 a3326863fa..98f98edff8 100644 --- a/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs +++ b/crates/jolt-sumcheck/tests/external-consumer/tests/external_field.rs @@ -4,17 +4,20 @@ 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, ClearSumcheckRecorder, ProveRounds, - SequentialRounds, SumcheckClaim, SumcheckError, 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; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] struct ExternalField(Prime64Offset59); @@ -173,48 +176,63 @@ 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 Encoding<[u8]> for ExternalField { + fn encode(&self) -> impl AsRef<[u8]> { + jolt_field::narg::encode(self) + } } -impl Transcript for ExternalTranscript { - type Challenge = ExternalField; +impl CanonicalDecode for ExternalField { + fn from_bytes_le_checked(bytes: &[u8]) -> Option { + Prime64Offset59::from_bytes_le_checked(bytes).map(Self) + } +} - fn new(label: &'static [u8]) -> Self { - let mut transcript = Self::default(); - transcript.append_bytes(label); - transcript +impl NargDeserialize for ExternalField { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + jolt_field::narg::deserialize(buf) } +} - 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; - } +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 challenge(&mut self) -> ExternalField { - self.append_bytes(b"challenge"); - ExternalField::from_u64(u64::from_le_bytes(self.state[..8].try_into().unwrap())) + fn to_u128_checked(&self) -> Option { + self.0.to_u128_checked() } - fn state(&self) -> [u8; 32] { - self.state + fn from_u128_checked(value: u128) -> Option { + Prime64Offset59::from_u128_checked(value).map(Self) + } + + fn from_u128_reduced(value: u128) -> Self { + Self(Prime64Offset59::from_u128_reduced(value)) + } + + fn num_bits(&self) -> u32 { + self.0.num_bits() + } + + 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 { @@ -253,10 +271,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, @@ -278,29 +296,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 proof = recorded.proof; + let prover_state: [u8; 32] = prover_transcript.challenge_bytes::<32>(); + 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.challenge_bytes::<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 c4aad11897..3710c45b2d 100644 --- a/crates/jolt-sumcheck/tests/mersenne61_compat.rs +++ b/crates/jolt-sumcheck/tests/mersenne61_compat.rs @@ -15,14 +15,20 @@ use std::{ }; use jolt_field::{ - AdditiveGroup, CanonicalBytes, CanonicalEncoding, Field, NaiveAccumulator, Ring, - WithAccumulator, + 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}; +use spongefish::Encoding; +use spongefish::NargDeserialize; +use spongefish::VerificationResult; const MODULUS: u64 = (1u64 << 61) - 1; @@ -247,6 +253,25 @@ impl CanonicalBytes for Mersenne61 { } } +impl 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()?; + Self::from_u128_checked(u64::from_le_bytes(arr) as u128) + } +} + +impl NargDeserialize for Mersenne61 { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + impl CanonicalEncoding for Mersenne61 { const MODULUS_BITS: u32 = 61; @@ -257,11 +282,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) } @@ -293,81 +313,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 31fa90e315..f2822b0c06 100644 --- a/crates/jolt-sumcheck/tests/roundtrip.rs +++ b/crates/jolt-sumcheck/tests/roundtrip.rs @@ -1,72 +1,72 @@ -#![expect(clippy::unwrap_used, reason = "tests may panic on assertion failures")] +//! 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, + 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)); - 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; - - 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(); - - 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]); @@ -75,212 +75,90 @@ fn prove_product( } } + let fingerprint = transcript.challenge_bytes::<32>(); ( - ClearSumcheckProof { - round_polynomials: round_polys, - }, - claimed_sum, + transcript.finish(), + SumcheckClaim::new(num_vars, degree, claimed_sum), + fingerprint, ) } -#[test] -fn degree3_final_eval_correct() { - 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, - ); +/// 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 claim = SumcheckClaim { - num_vars, - degree: 3, - claimed_sum, - }; + 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.challenge_bytes::<32>(), prover_state); + transcript.finish().unwrap(); - 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(); + 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 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); +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 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); +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 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 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 labeled_round_verifier_roundtrip() { - // Test the labeled round verifier path (used by jolt-verifier) +fn degree3_product_roundtrip() { 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; - - 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 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); } +} - 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)) +#[test] +fn eq_weighted_sumcheck() { + // eq(r, x) * f(x), the Spartan outer-sumcheck shape. + let num_vars = 4; + let r: Vec = (0..num_vars) + .map(|i| F::from_u64(i as u64 * 7 + 13)) .collect(); + 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); + } +} - 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() - ); +#[test] +fn large_num_vars_roundtrip() { + let num_vars = 10; + 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..ad2490969d --- /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.challenge_bytes::<32>(); + 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.challenge_bytes::<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.challenge_bytes::<32>(); + 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.challenge_bytes::<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 69ec2b6d7f..454aebb4ce 100644 --- a/crates/jolt-sumcheck/tests/soundness.rs +++ b/crates/jolt-sumcheck/tests/soundness.rs @@ -1,388 +1,318 @@ -#![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}; +//! Soundness tests: adversarial proofs against sumcheck verification. +//! 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; -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_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"); - - 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" - ); + 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)); +} - // 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 +#[test] +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, .. }) + )); } #[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); - - 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 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)) + )); +} - let claim = SumcheckClaim { - num_vars: 3, - degree: 1, - claimed_sum: sum, - }; +#[test] +fn rearranged_round_messages_are_rejected() { + let evals: Vec = (1..=8).map(F::from_u64).collect(); + 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 vt = new_transcript(); - let result = - SumcheckVerifier::verify(&claim, &proof.round_polynomials, BooleanHypercube, &mut vt); + let swapped = [round(1), round(0), round(2)].concat(); + assert!(verify(&claim, &swapped).is_err()); - // 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 replayed = [round(0), round(0), round(0)].concat(); + assert!(verify(&claim, &replayed).is_err()); } #[test] -fn all_zero_polynomial_honest_proof_for_zero_sum() { - 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); +fn all_zero_rounds_rejected_for_nonzero_sum() { + let evals: Vec = (1..=4).map(F::from_u64).collect(); + assert!(matches!( + verify(&claim(2, sum(&evals)), &[0; 2 * ROUND_BYTES]), + Err(SumcheckError::RoundCheckFailed { round: 0, .. }) + )); +} - let claim = SumcheckClaim { - num_vars, - degree: 1, - claimed_sum: F::from_u64(0), - }; +#[test] +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" + ); + } +} - let result = verify_with_oracle_check(&claim, &proof, &evals); - assert!( - result.is_ok(), - "zero polynomial must verify: {:?}", - result.err() - ); +#[test] +fn trailing_bytes_are_rejected() { + let evals: Vec = (1..=8).map(F::from_u64).collect(); + 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 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. + // 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 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 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 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_accepts_any_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 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); + 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 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); - } - 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" + ); } } diff --git a/crates/jolt-transcript/Cargo.toml b/crates/jolt-transcript/Cargo.toml index c8b8266ca5..5ebd221d21 100644 --- a/crates/jolt-transcript/Cargo.toml +++ b/crates/jolt-transcript/Cargo.toml @@ -13,27 +13,24 @@ workspace = true [features] default = ["transcript-blake2b", "transcript-keccak", "transcript-poseidon"] -spongefish = ["dep:rand", "dep:spongefish", "spongefish/sha3"] -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"] +# Records every transcript operation with its site and argument-string range. +logging = [] +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 } ark-ff = { workspace = true, optional = true } light-poseidon = { workspace = true, optional = true } -spongefish = { workspace = true, optional = true } -blake2 = { workspace = true, optional = true } -digest = { 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 } -rand = { workspace = true, features = ["std_rng"], optional = true } [dev-dependencies] criterion = { workspace = true } @@ -48,13 +45,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/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/Cargo.lock b/crates/jolt-transcript/fuzz/Cargo.lock index c41d7b7ff5..2ac6524778 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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,12 +426,13 @@ 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", "serde", + "spongefish", "thiserror 2.0.20", ] @@ -513,13 +441,13 @@ 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 +491,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 +893,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", diff --git a/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs b/crates/jolt-transcript/fuzz/fuzz_targets/encoding_injectivity.rs index 31194d819e..e657cb9aed 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.challenge_bytes::<32>() } 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 80f17ae436..83d9c6400d 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; - 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.challenge_bytes::<32>(); + (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/src/channel.rs b/crates/jolt-transcript/src/channel.rs new file mode 100644 index 0000000000..86a87ce7df --- /dev/null +++ b/crates/jolt-transcript/src/channel.rs @@ -0,0 +1,81 @@ +//! Operations both transcript roles perform identically. + +use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding, Field}; + +use crate::{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); + + /// Records, in the event log only, one draw from an accepted [`Fork`](crate::Fork) + /// of this transcript: `absorbed` context bytes, then `squeezed` seed bytes, + /// at the current site. The sponge is untouched. A prover records only the + /// accepted counter's draws, after the search, in the order the verifier + /// redraws them, so both roles' logs compare. + fn record_fork_draw(&mut self, absorbed: usize, squeezed: usize); + + /// 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; + + /// 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]; +} 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/error.rs b/crates/jolt-transcript/src/error.rs new file mode 100644 index 0000000000..b9a0c8f7ca --- /dev/null +++ b/crates/jolt-transcript/src/error.rs @@ -0,0 +1,42 @@ +//! 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 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, + /// 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/fork.rs b/crates/jolt-transcript/src/fork.rs new file mode 100644 index 0000000000..232fd52415 --- /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 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. +/// 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 new file mode 100644 index 0000000000..2ffae66e11 --- /dev/null +++ b/crates/jolt-transcript/src/grinding.rs @@ -0,0 +1,86 @@ +//! Proof of work: squeeze-then-grind. +//! +//! 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 +//! 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)`. + +use std::num::NonZeroU8; + +use crate::FORK_SEED_LEN; +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; +/// Byte length of the seed a grind squeezes from the transcript. +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] +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)) +} + +/// 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) +} + +/// 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, +) -> Option { + (0..1u64 << nonce_bits) + .filter_map(|candidate| u32::try_from(candidate).ok()) + .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)] +#[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)); + } + } +} 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 d80980fcac..199e569032 100644 --- a/crates/jolt-transcript/src/lib.rs +++ b/crates/jolt-transcript/src/lib.rs @@ -1,19 +1,19 @@ -//! Fiat-Shamir transcripts for Jolt, backed by spongefish. +//! Fiat-Shamir transcripts for Jolt in the NARG model, on spongefish. //! -//! Two surfaces: +//! 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. //! -//! - **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. -//! -//! Three sponges feature-gated: `transcript-blake2b` (spongefish -//! `Blake2b512`), `transcript-keccak` (spongefish `Keccak`), -//! `transcript-poseidon` (local Circom-compatible BN254 [`PoseidonSponge`]). +//! Message atoms are the [`CanonicalBytes`](jolt_field::CanonicalBytes) / +//! [`CanonicalDecode`](jolt_field::CanonicalDecode) codecs owned by each +//! 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`]. #![deny(missing_docs)] // In the jolt-verifier runtime closure: stricter panic and unsafe discipline @@ -32,87 +32,42 @@ clippy::wildcard_enum_match_arm )] -#[cfg(feature = "spongefish")] -mod codec; -#[cfg(feature = "digest")] -mod digest; -mod legacy; +mod channel; +mod error; +mod fork; +mod grinding; +mod nonce; #[cfg(feature = "transcript-poseidon")] mod poseidon; -#[cfg(feature = "spongefish")] +mod protocol; mod prover; -#[cfg(feature = "spongefish")] -mod setup; -#[cfg(feature = "spongefish")] +mod site; +mod sponge; +mod state; mod verifier; -#[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, +pub use channel::Channel; +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, }; -#[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(all(feature = "bn254", feature = "spongefish"))] -pub use prover::OptimizedChallenge; -#[cfg(feature = "spongefish")] +pub use nonce::Nonce; +pub use protocol::ProtocolId; pub use prover::ProverTranscript; -#[cfg(feature = "spongefish")] -pub use verifier::VerifierTranscript; - +#[cfg(feature = "logging")] +pub use site::TranscriptEvent; +pub use site::{SiteId, TranscriptOp}; +pub use sponge::Sponge; #[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; -#[cfg(feature = "transcript-blake2b")] -use spongefish::instantiations::Blake2b512; +pub 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"))] -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; +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 verifier::VerifierTranscript; -/// Fiat-Shamir transcript backed by Circom-compatible BN254 Poseidon. #[cfg(feature = "transcript-poseidon")] -pub type PoseidonTranscript = SpongeTranscript; +pub use poseidon::PoseidonSponge; diff --git a/crates/jolt-transcript/src/nonce.rs b/crates/jolt-transcript/src/nonce.rs new file mode 100644 index 0000000000..072237a3d1 --- /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. +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 +/// 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/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/src/protocol.rs b/crates/jolt-transcript/src/protocol.rs new file mode 100644 index 0000000000..113544dd8e --- /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(crate) 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..baaa4226dc 100644 --- a/crates/jolt-transcript/src/prover.rs +++ b/crates/jolt-transcript/src/prover.rs @@ -1,79 +1,218 @@ -//! 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: spongefish's prover state, written through typed atoms. + +use core::fmt::Result as FmtResult; +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}; +use rand::rngs::StdRng; +use spongefish::ProverState; - /// Squeezes a verifier challenge. - fn verifier_message>(&mut self) -> T; +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; - /// Bytes accumulated in the NARG so far. - fn narg_string(&self) -> &[u8]; +/// Prover transcript: spongefish's [`ProverState`], which appends every prover +/// message to the argument string and absorbs exactly its encoding. +/// +/// Not `Clone`: copying a prover state lets a caller rewind the sponge and +/// re-draw challenges. +pub struct ProverTranscript { + state: ProverState, + log: Log, } -impl ProverTranscript for ProverState -where - H: DuplexSpongeInterface, - R: RngCore + CryptoRng, -{ - fn public_message + ?Sized>(&mut self, msg: &T) { - ProverState::public_message(self, msg); +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() } +} - fn prover_message + NargSerialize + ?Sized>(&mut self, msg: &T) { - ProverState::prover_message(self, msg); +impl ProverTranscript { + /// Starts a transcript for `protocol`, bound to `session`. + #[must_use] + pub fn new(protocol: &ProtocolId, session: &[u8]) -> Self { + Self { + state: domain(protocol, session).to_prover(H::default()), + log: Log::default(), + } } - fn verifier_message>(&mut self) -> T { - ProverState::verifier_message::(self) + /// Sends one atom. + pub fn send(&mut self, value: &A) { + let start = self.narg_len(); + self.state.prover_message(value); + self.record_message(start); } - fn narg_string(&self) -> &[u8] { - ProverState::narg_string(self) + /// Sends atoms in order. + pub fn send_all(&mut self, values: &[A]) { + let start = self.narg_len(); + self.state.prover_messages(values); + self.record_message(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 bytes whose length the verifier already knows. + pub fn send_bytes(&mut self, bytes: &[u8]) { + 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` + /// 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 search counter as a [`Nonce`] message. + pub fn send_nonce(&mut self, nonce: u32) { + 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 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 + /// + /// [`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 seed: [u8; GRINDING_SEED_LEN] = self.challenge_bytes(); + let nonce = + grind_nonce::(&seed, bits, nonce_bits).ok_or(TranscriptError::GrindingExhausted)?; + self.send_nonce(nonce); + Ok(nonce) + } + + /// The argument string written so far. + #[must_use] + pub fn narg(&self) -> &[u8] { + self.state.narg_string() + } + + /// Recorded operations, in order. + #[cfg(feature = "logging")] + #[must_use] + pub fn events(&self) -> &[TranscriptEvent] { + self.log.events() + } + + /// Ends the transcript, returning the proof. + #[must_use] + pub fn finish(self) -> Vec { + self.state.narg_string().to_vec() + } + + fn narg_len(&self) -> usize { + self.state.narg_string().len() + } -#[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)) + fn record_message(&mut self, start: usize) { + let end = self.narg_len(); + self.log + .record(TranscriptOp::Message, end - start, Some(start..end)); } } -#[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.log.set_site(site); + } + + fn record_fork_draw(&mut self, absorbed: usize, squeezed: usize) { + self.log.record(TranscriptOp::ForkAbsorb, absorbed, None); + self.log.record(TranscriptOp::ForkSqueeze, squeezed, None); + } + + fn public(&mut self, value: &A) { + self.state.public_message(value); + self.log.record(TranscriptOp::Public, A::NUM_BYTES, None); + } + + fn public_all(&mut self, values: &[A]) { + self.state.public_messages(values); + self.log + .record(TranscriptOp::Public, A::NUM_BYTES * values.len(), None); + } + + fn public_bytes(&mut self, bytes: &[u8]) { + self.state.public_message(&Framed(bytes)); + self.log.record(TranscriptOp::Public, 8 + bytes.len(), None); + } + + 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 { + let (value, squeezed) = self.state.exact_challenge(); + self.log.record(TranscriptOp::Challenge, squeezed, None); + if H::ABSORB_FORGETS_SQUEEZE_WIDTH { + self.public(&(squeezed as u64)); + } + value + } + + fn challenge_small(&mut self) -> F { + let value = self.state.small_challenge(); + self.log + .record(TranscriptOp::Challenge, SMALL_CHALLENGE_BYTES, None); + value + } + + fn challenge_bytes(&mut self) -> [u8; N] { + let value = self.state.squeeze_array(); + self.log.record(TranscriptOp::Challenge, N, None); + value } } 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/src/site.rs b/crates/jolt-transcript/src/site.rs new file mode 100644 index 0000000000..0be3b67ad4 --- /dev/null +++ b/crates/jolt-transcript/src/site.rs @@ -0,0 +1,108 @@ +//! 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. +use core::ops::Range; + +/// 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, + /// Absorbed draw context into an accepted [`Fork`](crate::Fork). Event log + /// only; recorded through [`Channel::record_fork_draw`](crate::Channel::record_fork_draw). + ForkAbsorb, + /// Squeezed a draw seed from an accepted [`Fork`](crate::Fork). Event log only. + ForkSqueeze, +} + +/// 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..56372d3429 --- /dev/null +++ b/crates/jolt-transcript/src/sponge.rs @@ -0,0 +1,46 @@ +//! 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. +/// +/// 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; + + /// Whether an absorb erases how many bytes were squeezed within the + /// current output block. An overwrite-mode duplex squeezes by reading its + /// rate without permuting, so squeezing one byte or two before the same + /// absorb leaves the same state. On such a sponge the transcript absorbs + /// the width of every variable-length squeeze (the exact-challenge + /// rejection loop), so distinct retry histories cannot reconverge. + const ABSORB_FORGETS_SQUEEZE_WIDTH: bool; +} + +#[cfg(feature = "transcript-blake2b")] +impl Sponge for Blake2b512 { + const ID: &'static str = "blake2b512"; + const ABSORB_FORGETS_SQUEEZE_WIDTH: bool = false; +} + +#[cfg(feature = "transcript-keccak")] +impl Sponge for Keccak { + const ID: &'static str = "keccak-f1600"; + const ABSORB_FORGETS_SQUEEZE_WIDTH: bool = true; +} + +#[cfg(feature = "transcript-poseidon")] +impl Sponge for PoseidonSponge { + const ID: &'static str = "poseidon-bn254-circom-t4"; + const ABSORB_FORGETS_SQUEEZE_WIDTH: bool = true; +} diff --git a/crates/jolt-transcript/src/state.rs b/crates/jolt-transcript/src/state.rs new file mode 100644 index 0000000000..4e2aadf01c --- /dev/null +++ b/crates/jolt-transcript/src/state.rs @@ -0,0 +1,188 @@ +//! 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}; +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; + +/// Width of the squeeze blocks a challenge is drawn in. +const SQUEEZE_BLOCK: usize = 32; + +/// Width of the squeezes that follow the whole blocks. +const SQUEEZE_WORD: usize = 8; + +/// 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], +) -> DomainSeparator>, WithSession>> { + 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 words, then single bytes; both roles +/// squeeze through here, so they make the same squeeze calls. Every sponge's +/// output stream is independent of how its squeezes are split, so the split +/// only sets the per-call overhead: exact challenges draw `u64` words. +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]>()); + } + let mut words = blocks.into_remainder().chunks_exact_mut(SQUEEZE_WORD); + for word in &mut words { + word.copy_from_slice(&$state.verifier_message::<[u8; SQUEEZE_WORD]>()); + } + for byte in words.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<(), Error> { + self.fill_bytes(dest); + 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 37d7656798..a01d5ff5b8 100644 --- a/crates/jolt-transcript/src/verifier.rs +++ b/crates/jolt-transcript/src/verifier.rs @@ -1,79 +1,342 @@ -//! 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, -}; - -#[cfg(all( - feature = "bn254", - any(feature = "transcript-blake2b", feature = "transcript-keccak") -))] -use crate::prover::OptimizedChallenge; - -/// Verifier-side spongefish transcript. -pub trait VerifierTranscript { - /// Absorbs `msg` symmetrically with the prover. - fn public_message + ?Sized>(&mut self, msg: &T); - - /// Reads a prover message from the NARG, absorbing it into the sponge. - fn prover_message + NargDeserialize>(&mut self) -> VerificationResult; - - /// Squeezes a verifier challenge. - fn verifier_message>(&mut self) -> T; - - /// Asserts the NARG was fully consumed. +//! 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}; +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. +/// +/// 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 Debug for VerifierTranscript<'_, H> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + 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> { + /// 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 { + state: domain(protocol, session).to_verifier(H::default(), narg), + narg, + consumed: 0, + poisoned: false, + log: Log::default(), + } + } + + /// Receives one atom. /// - /// 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`] or [`TranscriptError::NonCanonical`]. + pub fn receive(&mut self) -> Result { + 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) + } + + /// Receives `count` atoms. Checks that the proof holds them before allocating. /// /// # Errors /// - /// Returns `Err` if the NARG has unread bytes. - fn check_eof(self) -> VerificationResult<()>; -} + /// [`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)?; + 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) + } -impl VerifierTranscript for VerifierState<'_, H> -where - H: DuplexSpongeInterface, -{ - fn public_message + ?Sized>(&mut self, msg: &T) { - VerifierState::public_message(self, msg); + /// Receives `len` bytes whose length the verifier already knows. + /// + /// # Errors + /// + /// [`TranscriptError::Truncated`]. + pub fn receive_bytes(&mut self, len: usize) -> Result<&'a [u8], TranscriptError> { + 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. 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) + } + + /// Receives a length-prefixed byte string of at most `max_len` bytes, + /// checking the length before reading the body. + /// + /// # Errors + /// + /// [`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) } - fn prover_message + NargDeserialize>(&mut self) -> VerificationResult { - VerifierState::prover_message::(self) + /// Receives a [`Nonce`] below `2^nonce_bits`. + /// + /// # Errors + /// + /// [`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 { + // 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) + .checked_shr(u32::from(nonce_bits)) + .unwrap_or(0) + == 0 + { + Ok(nonce) + } else { + Err(TranscriptError::OutOfBounds) + }; + self.check(in_range) } - fn verifier_message>(&mut self) -> T { - VerifierState::verifier_message::(self) + /// Checks `bits` bits of proof of work and returns the received nonce. A + /// 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`](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 accepted = if grinding_accepts::(&seed, nonce, bits) { + Ok(nonce) + } else { + Err(TranscriptError::GrindingRejected) + }; + self.check(accepted) } - fn check_eof(self) -> VerificationResult<()> { - VerifierState::check_eof(self) + /// 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 { + self.narg.len() - self.consumed } -} -#[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)) + /// Recorded operations, in order. + #[cfg(feature = "logging")] + #[must_use] + pub fn events(&self) -> &[TranscriptEvent] { + self.log.events() + } + + /// 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 { + self.state + .check_eof() + .map_err(|_| TranscriptError::TrailingBytes) + } + } + + /// 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() { + Ok(()) + } else { + Err(TranscriptError::Truncated) + }; + self.check(fits) + } + + /// Records a read of `len` bytes that spongefish already absorbed. + fn advance(&mut self, len: usize) { + 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. + 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.log.set_site(site); + } + + fn record_fork_draw(&mut self, absorbed: usize, squeezed: usize) { + self.log.record(TranscriptOp::ForkAbsorb, absorbed, None); + self.log.record(TranscriptOp::ForkSqueeze, squeezed, None); + } + + fn public(&mut self, value: &A) { + self.state.public_message(value); + self.log.record(TranscriptOp::Public, A::NUM_BYTES, None); + } + + fn public_all(&mut self, values: &[A]) { + self.state.public_messages(values); + self.log + .record(TranscriptOp::Public, A::NUM_BYTES * values.len(), None); + } + + fn public_bytes(&mut self, bytes: &[u8]) { + self.state.public_message(&Framed(bytes)); + self.log.record(TranscriptOp::Public, 8 + bytes.len(), None); + } + + 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 { + let (value, squeezed) = self.state.exact_challenge(); + self.log.record(TranscriptOp::Challenge, squeezed, None); + if H::ABSORB_FORGETS_SQUEEZE_WIDTH { + self.public(&(squeezed as u64)); + } + value + } + + fn challenge_small(&mut self) -> F { + let value = self.state.small_challenge(); + self.log + .record(TranscriptOp::Challenge, SMALL_CHALLENGE_BYTES, None); + value + } + + fn challenge_bytes(&mut self) -> [u8; N] { + let value = self.state.squeeze_array(); + self.log.record(TranscriptOp::Challenge, N, None); + value } } diff --git a/crates/jolt-transcript/tests/blake2b_tests.rs b/crates/jolt-transcript/tests/blake2b_tests.rs deleted file mode 100644 index a32922cfa4..0000000000 --- a/crates/jolt-transcript/tests/blake2b_tests.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Tests for Blake2bTranscript implementation. - -mod common; - -use jolt_field::Fr; -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"); -} diff --git a/crates/jolt-transcript/tests/common/mod.rs b/crates/jolt-transcript/tests/common/mod.rs deleted file mode 100644 index e6572d9711..0000000000 --- a/crates/jolt-transcript/tests/common/mod.rs +++ /dev/null @@ -1,136 +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; - - 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_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" - ); - let with_empty_again = challenge_after(b"empty_test", |t| { - t.append_bytes(&[]); - }); - assert_eq!(with_empty, with_empty_again); - } - - #[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 5a5151a1c3..0000000000 --- a/crates/jolt-transcript/tests/digest_transcript_tests.rs +++ /dev/null @@ -1,98 +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 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 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" - ); -} 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 new file mode 100644 index 0000000000..5c298378ce --- /dev/null +++ b/crates/jolt-transcript/tests/known_answers.rs @@ -0,0 +1,96 @@ +//! 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. + +#![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(&[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()); + 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 = "07000000000000000000000000000000000000000000000000000000000000000b0000000d000000080000007661726961626c65"; + +#[test] +fn blake2b_known_answers() { + assert_vectors( + "blake2b", + run::(), + ( + &format!("{NARG_PREFIX}78"), + "f14acf6a0e8500083aff06c75c0e690988c707180bdd447ee79c61ae1a7af622b4b8f1266fc13197f631ede2d2c8407a823d4388e79fba572325bdbd113cdf140edab4fa7e6bf134a286a792d365eea0671007657c372b32315757aca6c9990f", + ), + ); +} + +#[test] +fn keccak_known_answers() { + assert_vectors( + "keccak", + run::(), + ( + &format!("{NARG_PREFIX}41"), + "d8255bbe1b4ab7de6af9a08d7f52525044f0faa2d1f77159e6f5dbca1a89721044f898b914a69b88e14f4f1f8473ccac3701424fa1d67cc1bc6364be494dad28be074a6aac84e97e16fba3adb815c292453791a65937282f7b6aac73cb5e126e", + ), + ); +} + +#[test] +fn poseidon_known_answers() { + assert_vectors( + "poseidon", + run::(), + ( + &format!("{NARG_PREFIX}77"), + "06667373737856e7bdd1796e22de9aa04c6ed6f0f25b3aeffc440008ac6acc2ec9cc9dc21411bf2fe3200c7096263a2cac392db7ce5388f348ddc0d01aa11e0a370267031f8822ea3b9e157c139bbe731112ac283127d79610b197f9e4b0c649", + ), + ); +} 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 1613ae197f..0000000000 --- a/crates/jolt-transcript/tests/legacy_digest_tests.rs +++ /dev/null @@ -1,168 +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 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()); -} diff --git a/crates/jolt-transcript/tests/narg.rs b/crates/jolt-transcript/tests/narg.rs new file mode 100644 index 0000000000..217939038d --- /dev/null +++ b/crates/jolt-transcript/tests/narg.rs @@ -0,0 +1,340 @@ +//! Prover/verifier symmetry, rejection, and wire-format pins for the NARG +//! transcript. + +#![expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "tests")] + +use jolt_field::{CanonicalBytes, CanonicalEncoding, Fr, Ring}; +use jolt_transcript::{ + Blake2b512, Channel, Keccak, Nonce, PoseidonSponge, ProtocolId, ProverTranscript, Sponge, + TranscriptError, VerifierTranscript, GRINDING_NONCE_SLACK_BITS, +}; +use spongefish::Encoding as _; + +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(&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.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 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 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::<16>()); + 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 { + 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)); +} + +#[cfg(feature = "logging")] +#[test] +fn fork_draws_are_logged_without_touching_the_transcript() { + use jolt_transcript::{SiteId, TranscriptOp}; + let protocol = protocol::(); + let run = |record: bool| { + let mut prover = ProverTranscript::::new(&protocol, SESSION); + prover.site(SiteId::label("fold")); + if record { + prover.record_fork_draw(40, 32); + } + let challenge: Fr = prover.challenge(); + let events: Vec<_> = prover.events().iter().map(|e| (e.op, e.len)).collect(); + (prover.finish(), challenge, events) + }; + let (narg, challenge, events) = run(true); + let (plain_narg, plain_challenge, _) = run(false); + assert_eq!((narg, challenge), (plain_narg, plain_challenge)); + assert_eq!( + events[..2], + [ + (TranscriptOp::ForkAbsorb, 40), + (TranscriptOp::ForkSqueeze, 32) + ] + ); +} + +/// Pins [`Sponge::ABSORB_FORGETS_SQUEEZE_WIDTH`] to each sponge's real +/// behavior: squeezing one byte or two and then absorbing the same bytes +/// leaves the same state exactly when the flag is set. +#[test] +fn absorb_forgets_squeeze_width_matches_each_sponge() { + fn forgets() -> bool { + let next_block = |width: usize| { + let mut sponge = H::default(); + let _ = sponge.absorb(b"prefix"); + let _ = sponge.squeeze(&mut vec![0u8; width]); + let _ = sponge.absorb(&[17]); + let mut out = [0u8; 32]; + let _ = sponge.squeeze(&mut out); + out + }; + next_block(1) == next_block(2) + } + assert_eq!( + forgets::(), + Blake2b512::ABSORB_FORGETS_SQUEEZE_WIDTH + ); + assert_eq!(forgets::(), Keccak::ABSORB_FORGETS_SQUEEZE_WIDTH); + assert_eq!( + forgets::(), + PoseidonSponge::ABSORB_FORGETS_SQUEEZE_WIDTH + ); +} 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 35a10578c3..0000000000 --- a/crates/jolt-transcript/tests/narg_eof_tests.rs +++ /dev/null @@ -1,99 +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}; -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", - ); -} 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()); -} 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"); -} diff --git a/crates/jolt-verifier-derive/src/lib.rs b/crates/jolt-verifier-derive/src/lib.rs index 4c1982d1ca..496562fd82 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 @@ -83,20 +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 absorb plumbing -//! (`opening_values` / `append_output_claims`, via -//! `relations::absorbed_opening_values`). +//! - 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 @@ -132,91 +135,17 @@ 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" - | "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. /// -/// 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"`). @@ -360,24 +289,69 @@ 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 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, - &[(&claims_ident, quote!(claims.#id))], + &[(&points_ident, quote!(points.#id))], quote! { - __values.extend(#relations::absorbed_opening_values::<#f, #instance>(__claims)); + #relations::extend_committed_layout::<#f, #instance>(&mut __layout, __points, routes); }, ) }); + let receive_fields = plans + .iter() + .map(|plan| { + let id = &plan.ident; + let instance = &plan.instance; + if plan.is_option { + quote! { + #id: points + .#id + .as_ref() + .map(|__points| { + #relations::receive_member_claims::<#f, #instance, __H>( + __points, routes, &mut received, transcript, + ) + }) + .transpose()? + } + } else { + quote! { + #id: #relations::receive_member_claims::<#f, #instance, __H>( + &points.#id, routes, &mut received, transcript, + )? + } + } + }) + .collect::>(); // Per-instance driver plumbing on the source `StageNSumchecks` struct itself: // draw each member's challenges into the stage's challenge aggregate, delegating @@ -497,10 +471,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();) } }); @@ -557,21 +531,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)* @@ -600,36 +575,79 @@ 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. + // input claims are absorbed), verify the compressed Boolean-hypercube rounds, + // derive the produced opening points at the reduced point, receive the output + // 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, + ) + }, + ) + } + /// 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. + /// (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::too_many_arguments, - reason = "the composed tail threads every per-stage aggregate once" + clippy::type_complexity, + reason = "the produced points and claims are the stage's two outputs" )] - pub fn verify_clear<__C, __T>( + pub fn verify_clear_with<__H, __R>( &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, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, stage: usize, - ) -> ::core::result::Result<#output_points_name<#f>, #krate::VerifierError> + receive_claims: __R, + ) -> ::core::result::Result< + (#output_points_name<#f>, #output_claims_name<#f>), + #krate::VerifierError, + > where - __C: ::core::clone::Clone + ::jolt_transcript::AppendToTranscript, - __T: ::jolt_transcript::Transcript, + __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 _; @@ -637,24 +655,25 @@ fn expand(input: DeriveInput) -> syn::Result { let (__batch, __coefficients) = self.begin_batch(inputs, challenges, &mut __recorder, transcript)?; - let __reduction = proof - .verify_compressed_boolean( + 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(), - })?; - + ), + 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, + &__claims, &__output_points, challenges, )?; @@ -663,13 +682,14 @@ fn expand(input: DeriveInput) -> syn::Result { #krate::VerifierError::StageClaimOutputMismatch { stage }, ); } - ::core::result::Result::Ok(__output_points) + ::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 @@ -677,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(); @@ -688,28 +712,38 @@ 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, 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, - proof: &::jolt_sumcheck::SumcheckProof<#f, __C>, - transcript: &mut __T, + row_len: usize, + input_points: &#input_points_name<#f>, + transcript: &mut ::jolt_transcript::VerifierTranscript<'_, __H>, ) -> ::core::result::Result< - ::jolt_sumcheck::BatchedCommittedSumcheckConsistency<#f, __C>, + #krate::stages::zk::outputs::VerifiedCommittedBatch< + #f, + __C, + #output_points_name<#f>, + >, #krate::VerifierError, > where - __C: ::core::clone::Clone + ::jolt_transcript::AppendToTranscript, - __T: ::jolt_transcript::Transcript, + __C: ::jolt_field::CanonicalDecode, + __H: ::jolt_transcript::Sponge, { use #relations::ConcreteSumcheck as _; + use ::jolt_transcript::Channel as _; let mut __max_num_vars = 0usize; let mut __max_degree = 0usize; @@ -718,18 +752,41 @@ 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 (__rounds, _) = + ::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 { - consistency: __consistency, + let __consistency = ::jolt_sumcheck::BatchedCommittedSumcheckConsistency { + consistency: __rounds, batching_coefficients: __batching_coefficients, max_num_vars: __max_num_vars, max_degree: __max_degree, + }; + 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(#krate::stages::zk::outputs::VerifiedCommittedBatch { + consistency: __consistency, + output_points: __output_points, + output_claims: #krate::stages::zk::outputs::CommittedOutputClaimOutput { + shape: __shape, + commitments: __commitments, + }, }) } } @@ -969,75 +1026,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 (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 - // 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 @@ -1054,9 +1091,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 { @@ -1066,39 +1103,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 + } - /// 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>( - &self, - transcript: &mut __T, - claims: &#output_claims_name<#f>, - ) { - for value in self.opening_values(claims) { - transcript.append_labeled(b"opening_claim", &value); - } - } + /// 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,)* + }) } }; @@ -1120,14 +1189,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| { @@ -1159,7 +1222,6 @@ fn expand(input: DeriveInput) -> syn::Result { output_points = #output_points_name, challenges = #challenges_name, }, - shape = #shape, members = [ #(#member_entries)* ] @@ -1181,7 +1243,7 @@ fn expand(input: DeriveInput) -> syn::Result { #expected_final_claim_method #output_shape_methods #empty_input_points_method - #absorb_methods + #claim_methods } }; @@ -1243,22 +1305,17 @@ fn expand(input: DeriveInput) -> syn::Result { #[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 @@ -1287,21 +1344,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/Cargo.toml b/crates/jolt-verifier/Cargo.toml index 59a33b3a15..e1cfb1a68e 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", @@ -46,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 @@ -61,7 +62,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/fuzz/Cargo.lock b/crates/jolt-verifier/fuzz/Cargo.lock index c604a12a70..469f358d8a 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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", @@ -656,7 +583,6 @@ dependencies = [ "jolt-utils", "rand_core 0.6.4", "rayon", - "serde", "thiserror 2.0.19", ] @@ -694,18 +620,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.8", "num-integer", "num-traits", "rand_core 0.6.4", "rayon", "serde", + "spongefish", ] [[package]] @@ -714,7 +640,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -724,6 +650,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -732,12 +659,13 @@ 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", "serde", + "spongefish", "thiserror 2.0.19", ] @@ -771,9 +699,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", @@ -787,7 +715,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -801,7 +728,7 @@ dependencies = [ name = "jolt-program" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "common", "jolt-riscv", "serde", @@ -821,7 +748,7 @@ dependencies = [ name = "jolt-riscv" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "serde", "strum", "thiserror 2.0.19", @@ -847,13 +774,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.3", + "ark-ff", "jolt-field", "light-poseidon", "rand 0.8.7", + "rand_core 0.6.4", "spongefish", + "thiserror 2.0.19", ] [[package]] @@ -867,7 +794,6 @@ dependencies = [ name = "jolt-verifier" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", "bincode 2.0.1", "blake2 0.11.0-rc.6", "common", @@ -908,8 +834,6 @@ dependencies = [ "jolt-crypto", "jolt-dory", "jolt-field", - "jolt-poly", - "jolt-sumcheck", "jolt-transcript", "jolt-verifier", "libfuzzer-sys", @@ -948,7 +872,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", ] @@ -1395,8 +1319,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", 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 ad711395c1..94cd4ddadc 100644 Binary files a/crates/jolt-verifier/fuzz/fixtures/advice-consumer-bundle.bin and b/crates/jolt-verifier/fuzz/fixtures/advice-consumer-bundle.bin differ diff --git a/crates/jolt-verifier/fuzz/fixtures/committed-muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/committed-muldiv-bundle.bin index cffc3b9e65..e3149c7eec 100644 Binary files a/crates/jolt-verifier/fuzz/fixtures/committed-muldiv-bundle.bin and b/crates/jolt-verifier/fuzz/fixtures/committed-muldiv-bundle.bin differ diff --git a/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin index aa0dbd932b..6368dcd0f2 100644 Binary files a/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin and b/crates/jolt-verifier/fuzz/fixtures/muldiv-bundle.bin differ 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 078348adac..074d1e3208 100644 Binary files a/crates/jolt-verifier/fuzz/fixtures/zk-advice-consumer-bundle.bin and b/crates/jolt-verifier/fuzz/fixtures/zk-advice-consumer-bundle.bin differ 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 f14f78fe8b..ebda75dfb3 100644 Binary files a/crates/jolt-verifier/fuzz/fixtures/zk-committed-muldiv-bundle.bin and b/crates/jolt-verifier/fuzz/fixtures/zk-committed-muldiv-bundle.bin differ diff --git a/crates/jolt-verifier/fuzz/fixtures/zk-muldiv-bundle.bin b/crates/jolt-verifier/fuzz/fixtures/zk-muldiv-bundle.bin index 5b77019d94..04f8be067b 100644 Binary files a/crates/jolt-verifier/fuzz/fixtures/zk-muldiv-bundle.bin and b/crates/jolt-verifier/fuzz/fixtures/zk-muldiv-bundle.bin differ 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 8f7043c087..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,70 +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(); - - 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 0000000000..7b96db4ed0 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_deserialize_no_panic/honest_header differ 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 0000000000..9d5642c179 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/append differ 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 0000000000..a2ee4022a0 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/delete differ 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 0000000000..3f6751bd91 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/header_flip differ 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 0000000000..4f9e30c313 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/heap_size differ 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 0000000000..4b23fa0430 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/insert differ 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 0000000000..230da99073 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/narg_flip differ 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 b02fdc7952..0000000000 Binary files a/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/outer-round-eight and /dev/null differ 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 0000000000..7e57c7d236 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/overwrite differ 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 0000000000..ff6dab747d Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/program_swap differ 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 0000000000..392d0c5823 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/protocol_mode differ 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 0000000000..875cbc528e Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/public_input differ 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 0000000000..a99b0dadd7 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/tail_flip differ 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 0000000000..bcf0eb9cc6 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/truncate differ 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 0000000000..cef4896c8b Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/proof_tamper_must_reject/trusted_advice differ 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 846fe1917d..5bb4222c58 100644 Binary files a/crates/jolt-verifier/fuzz/seeds/validate_inputs_no_panic/valid-ish and b/crates/jolt-verifier/fuzz/seeds/validate_inputs_no_panic/valid-ish differ diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/advice_commitment b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/advice_commitment deleted file mode 100644 index ecec880228..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/advice_commitment +++ /dev/null @@ -1 +0,0 @@ -00000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/append b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/append new file mode 100644 index 0000000000..9d5642c179 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/append differ diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/blindfold_random_u b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/blindfold_random_u deleted file mode 100644 index ecec880228..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/blindfold_random_u +++ /dev/null @@ -1 +0,0 @@ -00000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/committed_sumcheck b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/committed_sumcheck deleted file mode 100644 index d0e6ec60a6..0000000000 --- a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/committed_sumcheck +++ /dev/null @@ -1 +0,0 @@ -0.000 \ No newline at end of file diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/delete b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/delete new file mode 100644 index 0000000000..a2ee4022a0 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/delete differ diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/header_flip b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/header_flip new file mode 100644 index 0000000000..3f6751bd91 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/header_flip differ diff --git a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/heap_size b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/heap_size new file mode 100644 index 0000000000..4f9e30c313 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/heap_size differ 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 0000000000..4b23fa0430 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/insert differ 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 0000000000..230da99073 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/narg_flip differ 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 6cb31c1e81..0000000000 Binary files a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/outer-round-eight and /dev/null differ 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 0000000000..7e57c7d236 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/overwrite differ 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 33c1eadbc3..ff6dab747d 100644 Binary files a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/program_swap and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/program_swap differ 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 191dc569f9..392d0c5823 100644 Binary files a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/protocol_mode and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/protocol_mode differ 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 230b53fd48..875cbc528e 100644 Binary files a/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/public_input and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/public_input differ 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 0000000000..a99b0dadd7 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/tail_flip differ 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 0000000000..bcf0eb9cc6 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/truncate differ 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 0000000000..c682901b38 Binary files /dev/null and b/crates/jolt-verifier/fuzz/seeds/zk_proof_tamper_must_reject/trusted_advice differ 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/src/config.rs b/crates/jolt-verifier/src/config.rs index 7b1d3d4e77..d0c5c27782 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 Akita 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 Akita 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 { Akita, } -/// 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::Akita => 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::Akita #[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, }; @@ -139,28 +157,34 @@ 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] + ); let akita_protocol = JoltProtocolConfig { commitment: CommitmentConfig::Akita, - scalar_challenge_endianness: ScalarChallengeEndianness::Little, ..protocol }; assert_eq!( postcard::to_stdvec(&akita_protocol).unwrap(), - [0, 1, 1, 0, 4, 0] + [0, 1, 0, 4, 0] ); assert_eq!( - postcard::from_bytes::(&[0, 1, 1, 0, 4, 0]).unwrap(), + postcard::from_bytes::(&[0, 1, 0, 4, 0]).unwrap(), akita_protocol ); + assert_eq!( + akita_protocol.transcript_bytes(), + [0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0] + ); } /// A proof declaring a different field-register file size rejects even when the enabled diff --git a/crates/jolt-verifier/src/error.rs b/crates/jolt-verifier/src/error.rs index 4a046332c3..9eb1aea714 100644 --- a/crates/jolt-verifier/src/error.rs +++ b/crates/jolt-verifier/src/error.rs @@ -1,5 +1,6 @@ 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}; @@ -7,6 +8,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/fs_audit.rs b/crates/jolt-verifier/src/fs_audit.rs deleted file mode 100644 index 4ee7ac3dc3..0000000000 --- a/crates/jolt-verifier/src/fs_audit.rs +++ /dev/null @@ -1,51 +0,0 @@ -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, - Stage1, - Stage2, - Stage3, - Stage4, - Stage5, - /// Stage 6 address phase. - Stage6a, - /// Stage 6 cycle phase. - Stage6b, - Stage7, - /// Final opening checks. - Stage8, - 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 717eb5a3a7..77cc56a5de 100644 --- a/crates/jolt-verifier/src/lib.rs +++ b/crates/jolt-verifier/src/lib.rs @@ -46,29 +46,21 @@ 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; -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_akita_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 153a310a5e..88b48c0dd9 100644 --- a/crates/jolt-verifier/src/proof.rs +++ b/crates/jolt-verifier/src/proof.rs @@ -1,162 +1,140 @@ -use jolt_blindfold::BlindFoldProof; +//! +//! 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. + +#[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 -/// native `OneHotTrace` group commitment carrying every per-proof column. -#[cfg(feature = "akita")] -pub type ProofCommitments = ::Output; - -/// 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; +use crate::{config::JoltProtocolConfig, jolt_protocol_id, num, VerifierError, JOLT_SESSION}; -#[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. +/// +/// 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)] -#[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, +} + +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)] +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).to_le_bytes()); + transcript.send(&num::u64_from_usize(self.ram_K).to_le_bytes()); + // 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, + 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() + .to_le_bytes(), + ); + 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(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(receive_u64( + transcript, + )?) + .ok_or(VerifierError::MalformedProofHeader { + field: "trace_polynomial_order", + })?; + let untrusted_advice = match transcript.receive::<[u8; 1]>()? { + [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), - } - } +/// 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()?)) +} - /// 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 @@ -164,7 +142,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, } @@ -172,146 +150,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 - /// Akita 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 }, + pub field_inline: FieldInlineCommitments, } -impl JoltProofClaims -where - F: JoltField, -{ - pub const fn is_zk(&self) -> bool { - matches!(self, Self::Zk { .. }) - } -} - -#[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 native `OneHotTrace` group 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 { - #[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/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/mod.rs b/crates/jolt-verifier/src/stages/mod.rs index 2553bb9991..3c66b165e1 100644 --- a/crates/jolt-verifier/src/stages/mod.rs +++ b/crates/jolt-verifier/src/stages/mod.rs @@ -15,7 +15,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; @@ -42,8 +41,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, @@ -54,7 +52,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 e3b122b2b3..70ac646e5a 100644 --- a/crates/jolt-verifier/src/stages/relations.rs +++ b/crates/jolt-verifier/src/stages/relations.rs @@ -9,23 +9,27 @@ //! 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_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::BTreeSet; +use std::collections::BTreeMap; +use jolt_blindfold::OpeningAlias; 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 +39,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 +120,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) } @@ -168,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 @@ -194,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 @@ -344,82 +309,224 @@ 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 +/// 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 +/// generated batch-wide alias resolver can chain members across protocol +/// families. Called by the generated `validate_aliases` per member. +pub fn resolve_member_opening( + claims: &SumcheckOutputClaims, + id: &ComposedOpeningId, +) -> Option where F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: Ord, + OpeningIdOf: TryFrom, +{ + let native = OpeningIdOf::::try_from(*id).ok()?; + claims.resolve_output(&native) +} + +/// 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, + OpeningIdOf: Into, { - let skip: BTreeSet<_> = I::aliased_output_openings() + I::aliased_output_openings() .into_iter() - .map(|(aliased, _)| aliased) - .collect(); + .map(|(aliased, source)| (aliased.into(), source.into())) + .collect() +} + +/// 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> +where + F: JoltField, + I: ConcreteSumcheck, + H: Sponge, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy + Into, +{ + 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 }) + }) +} + +/// 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() .zip(claims.opening_values()) - .filter(|(id, _)| !skip.contains(id)) + .filter(|(id, _)| match routes.of(&(*id).into(), &static_aliases) { + ClaimRoute::Sent => true, + ClaimRoute::Staged => with_staged, + ClaimRoute::Alias(_) => false, + }) .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), -/// and directly by a stage that curates its own shape checks (stage 6b). -pub fn validate_member_presence( - member: Option<&I>, - claims: Option<&SumcheckOutputClaims>, -) -> Result<(), VerifierError> -where +/// 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, - 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, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy + Into, { - 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(), - }, - }), + 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), + } } } -/// 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 -/// generated batch-wide alias resolver can chain members across protocol -/// families. Called by the generated `validate_aliases` per member. -pub fn resolve_member_opening( +/// The ids of a member's output-point cells, in canonical order. +fn point_cell_ids(points: &SumcheckOutputPoints) -> Vec> +where + F: JoltField, + I: ConcreteSumcheck, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy, +{ + let mut ids = Vec::new(); + let Ok(_) = points.try_map_cells(&mut |id, _point| { + ids.push(*id); + Ok::(F::zero()) + }); + ids +} + +/// 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, - id: &ComposedOpeningId, -) -> Option + points: &SumcheckOutputPoints, +) -> Result<(), VerifierError> where F: JoltField, I: ConcreteSumcheck, SumcheckOutputClaims: OutputClaims>, - OpeningIdOf: TryFrom, + SumcheckOutputPoints: + MapCells, F, OpeningIdOf, Mapped = SumcheckOutputClaims>, + OpeningIdOf: Copy + PartialEq, + RelationIdOf: Debug, { - let native = OpeningIdOf::::try_from(*id).ok()?; - claims.resolve_output(&native) + 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 @@ -463,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 @@ -528,114 +594,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; - - #[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, - } - - impl Transcript for DrawRecordingTranscript { - type Challenge = Fr; - - 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] - } - } +pub(crate) mod test_transcript { + use jolt_transcript::{Blake2b512, Channel, ProtocolId, ProverTranscript}; - /// 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) - } -} + pub(crate) type TestTranscript = ProverTranscript; -/// 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.challenge_bytes::<32>(), + right.challenge_bytes::<32>(), + "production and documented draws diverge" + ); + (produced, expected) } } @@ -663,22 +656,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 { @@ -707,7 +684,6 @@ mod tests { claims.canonical_order().len(), claims.opening_values().len() ); - assert_append_matches_values(&claims); } #[test] @@ -774,7 +750,6 @@ mod tests { )), Some(fr(8)), ); - assert_append_matches_values(&claims); } #[derive(OutputClaims)] @@ -814,7 +789,6 @@ mod tests { )), None, ); - assert_append_matches_values(&claims); } #[derive(OutputClaims)] @@ -843,8 +817,8 @@ mod tests { present.resolve_output(&committed(JoltCommittedPolynomial::RamInc, relation)), Some(fr(8)), ); - assert_append_matches_values(&present); + // and id resolution. let absent = OptionalOutput { untrusted: None, ram_inc: fr(8), @@ -855,7 +829,6 @@ mod tests { absent.resolve_output(&JoltOpeningId::untrusted_advice(relation)), None, ); - assert_append_matches_values(&absent); } #[test] @@ -1066,25 +1039,19 @@ mod tests { #[cfg(test)] #[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 crate::VerifierError; 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 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, @@ -1092,12 +1059,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)], @@ -1111,7 +1074,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)], ); } @@ -1124,7 +1087,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)], @@ -1132,10 +1095,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 { @@ -1144,33 +1103,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, @@ -1192,74 +1149,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(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` / - // `append_output_claims` 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")] - #[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), - }, - }; - - 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 @@ -1286,7 +1191,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; @@ -1294,8 +1199,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")] @@ -1323,13 +1228,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(), @@ -1337,17 +1241,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( @@ -1364,12 +1261,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 @@ -1405,6 +1307,7 @@ mod begin_batch_tests { assert_eq!(coefficients.registers_val_evaluation, Some(registers_coeff)); } + /// An absent `Option` member contributes no absorb, no coefficient draw, #[test] fn begin_batch_skips_absent_option_member() { let sumchecks = fixture(false); @@ -1412,24 +1315,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 8af2f87936..0de30c10f4 100644 --- a/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs +++ b/crates/jolt-verifier/src/stages/stage1/outer_remainder.rs @@ -346,12 +346,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/stage1/outputs.rs b/crates/jolt-verifier/src/stages/stage1/outputs.rs index 14785c94df..09442303ad 100644 --- a/crates/jolt-verifier/src/stages/stage1/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage1/outputs.rs @@ -1,29 +1,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 { - 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 @@ -31,10 +14,8 @@ impl Stage1OutputClaims { /// `#[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 @@ -158,6 +139,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 1f79982875..e3d2619743 100644 --- a/crates/jolt-verifier/src/stages/stage1/verify.rs +++ b/crates/jolt-verifier/src/stages/stage1/verify.rs @@ -1,55 +1,37 @@ -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::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE1; use super::outer_remainder::{outer_remainder_input_values_from_uniskip_output, OuterRemainder}; use super::outputs::{ Stage1BatchInputClaims, Stage1BatchSumchecks, Stage1Challenges, Stage1ClearOutput, Stage1Output, Stage1ZkOutput, }; -use crate::{ - proof::JoltProof, - stages::{uniskip, zk::committed}, - verifier::CheckedInputs, - VerifierError, -}; +use crate::{stages::uniskip, 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, { + 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); 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 +39,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,19 +77,7 @@ 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 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 { @@ -129,9 +86,9 @@ where }, uniskip_consistency: uniskip.consistency, uniskip_output_claims: uniskip.output_claims, - remainder_consistency, - remainder_output_claims, - 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 c9c87906f7..f6cf36c72e 100644 --- a/crates/jolt-verifier/src/stages/stage2/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage2/outputs.rs @@ -1,6 +1,5 @@ use jolt_field::JoltField; use jolt_sumcheck::{BatchedCommittedSumcheckConsistency, CommittedSumcheckConsistency}; -use serde::{Deserialize, Serialize}; use crate::stages::relations::SumcheckBatch; use crate::stages::zk::outputs::CommittedOutputClaimOutput; @@ -20,25 +19,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 @@ -180,6 +160,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), @@ -227,8 +211,8 @@ impl Stage2Output { )] mod tests { use super::*; - use crate::stages::relations::draw_recording::{record, DrawEvent}; - use crate::stages::relations::ConcreteSumcheck; + use crate::stages::relations::test_transcript::assert_same_draws; + use crate::stages::relations::{ClaimRoutes, ConcreteSumcheck}; use common::jolt_device::{JoltDevice, MemoryConfig}; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; @@ -239,7 +223,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) @@ -283,38 +267,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, @@ -326,19 +303,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( @@ -399,14 +373,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::>() ); } @@ -418,7 +392,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); @@ -435,27 +409,9 @@ mod tests { .chain([fr(12), fr(13)]) .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 ); } @@ -474,7 +430,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(); @@ -482,7 +438,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); @@ -503,24 +463,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()); @@ -606,64 +548,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, - }) - } -} diff --git a/crates/jolt-verifier/src/stages/stage2/product_remainder.rs b/crates/jolt-verifier/src/stages/stage2/product_remainder.rs index 415fafdeaf..d9b8d79eb4 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/ram_output_check.rs b/crates/jolt-verifier/src/stages/stage2/ram_output_check.rs index 155df892a4..17429295db 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; @@ -114,16 +114,17 @@ 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. - fn draw_challenges>( + /// 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 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/verify.rs b/crates/jolt-verifier/src/stages/stage2/verify.rs index 18b42a5b78..7a2f8cfe7b 100644 --- a/crates/jolt-verifier/src/stages/stage2/verify.rs +++ b/crates/jolt-verifier/src/stages/stage2/verify.rs @@ -8,11 +8,11 @@ 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::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE2; #[cfg(feature = "field-inline")] use super::field_registers_claim_reduction::{ @@ -33,12 +33,10 @@ 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, }, verifier::CheckedInputs, VerifierError, @@ -56,7 +54,7 @@ struct ProductUniskipStep { } enum ProductUniskipVerified { - Clear, + Clear { output_claim: F }, Zk(uniskip::UniskipZk), } @@ -84,18 +82,17 @@ 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, { + transcript.site(STAGE2); match (checked.zk, stage1) { (true, Stage1Output::Clear(_)) => { return Err(VerifierError::ExpectedCommittedProof { field: "stage1" }); @@ -109,7 +106,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 +116,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 @@ -165,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)?; @@ -179,18 +175,8 @@ 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 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, @@ -199,38 +185,30 @@ 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, })); } - 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); - - let output_points = sumchecks.verify_clear( - &input_values, - &input_points, - &challenges, - &claims.batch_outputs, - &proof.stages.stage2_sumcheck_proof, - transcript, - 2, - )?; + stage2_batch_input_values_from_upstream(stage1, product_uniskip_output_claim); - 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, })) @@ -263,16 +241,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); @@ -282,33 +259,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/outputs.rs b/crates/jolt-verifier/src/stages/stage3/outputs.rs index c1787f7a48..bd03c522c4 100644 --- a/crates/jolt-verifier/src/stages/stage3/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage3/outputs.rs @@ -100,7 +100,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}; @@ -155,30 +155,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 @@ -187,12 +175,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 5face84f29..73d6060408 100644 --- a/crates/jolt-verifier/src/stages/stage3/verify.rs +++ b/crates/jolt-verifier/src/stages/stage3/verify.rs @@ -1,8 +1,8 @@ -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::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE3; use super::{ instruction_input::{instruction_input_input_values_from_upstream, InstructionInput}, @@ -15,11 +15,9 @@ use super::{ spartan_shift::{spartan_shift_input_values_from_upstream, SpartanShift}, }; use crate::{ - proof::JoltProof, stages::{ stage1::{Stage1BatchOutputClaims, Stage1Output}, stage2::{Stage2BatchOutputClaims, Stage2Output}, - zk::committed, }, verifier::CheckedInputs, VerifierError, @@ -40,19 +38,18 @@ 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, { + transcript.site(STAGE3); let log_t = crate::num::ilog2(checked.trace_length); let dimensions = TraceDimensions::new(log_t); @@ -72,56 +69,40 @@ 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)?; 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 batch = sumchecks.verify_zk( + checked.committed_row_len()?, + &sumchecks.empty_input_points(), + transcript, )?; - let output_points = sumchecks - .derive_opening_points(&consistency.challenges(), &sumchecks.empty_input_points())?; 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 4c26834861..4b6176c1bb 100644 --- a/crates/jolt-verifier/src/stages/stage4/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage4/outputs.rs @@ -1,15 +1,17 @@ use jolt_field::JoltField; use jolt_sumcheck::BatchedCommittedSumcheckConsistency; -use jolt_transcript::Transcript; -use crate::stages::relations::{OutputClaims, SumcheckBatch}; +use crate::stages::relations::{ClaimRoutes, 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, }; @@ -20,24 +22,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 @@ -67,52 +59,13 @@ 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. - /// 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() - } -} - -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`]. - 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 - } - - /// 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); - } +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()) } -} -impl Stage4OutputPoints { /// The register read-write opening point (shared by all five register /// openings). pub fn registers_read_write_point(&self) -> &[F] { @@ -138,10 +91,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, } @@ -153,9 +103,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, } @@ -193,7 +141,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::{ @@ -203,7 +151,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) @@ -253,19 +201,46 @@ mod tests { Vec::new() } - /// 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 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 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 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 committed_claims_follow_declaration_order() { + let expected: Vec = (3..=7) + .map(fr) + .chain(field_inline_splice()) + .chain([fr(1), fr(2), fr(10), fr(8), fr(9)]) + .collect(); + assert_eq!( + Stage4Sumchecks::committed_claim_values(&claims_with_advice(true), &staged_routes()), + expected + ); } fn sumchecks() -> Stage4Sumchecks { @@ -286,60 +261,38 @@ mod tests { TraceDimensions::new(log_t), ram_log_k, RamValCheckInit::from(fr(0)), + RamValCheckStagedOpenings::default(), ), } } - /// 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()); + let gamma_draws = 3usize; + let (challenges, gammas) = assert_same_draws( + |t| sumchecks.draw_challenges(t).unwrap(), + |t| (0..gamma_draws).map(|_| t.challenge()).collect::>(), + ); - assert_eq!(draw_events, inline_events); - assert!(matches!(draw_events.first(), Some(DrawEvent::Squeeze(1)))); - assert!(draw_events - .iter() - .any(|event| matches!(event, DrawEvent::Append(_)))); #[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); - } - - #[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(), - ); + assert_eq!(drawn_gammas, gammas); } } 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 82057138f7..d9de2a07ff 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,20 +25,20 @@ 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::{LabelWithCount, Transcript}; +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; -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 @@ -93,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 { @@ -112,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 @@ -119,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 @@ -141,7 +136,7 @@ impl RamValCheck { ram_log_k, public_eval, init_selectors, - contribution_openings, + staged_points, } } @@ -168,30 +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 - } - - /// 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], @@ -218,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, }) @@ -318,6 +290,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() @@ -406,6 +392,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)] @@ -420,16 +424,80 @@ 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: 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 { + /// Sends the present openings, in canonical order. + pub fn send(&self, transcript: &mut ProverTranscript) { + transcript.send_all(&self.opening_values()); + } + + /// 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() + .map(Into::into) + .zip(self.opening_values()) + .collect() + } +} + +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 + } +} + +/// 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(_)) => { @@ -547,64 +615,3 @@ fn ram_val_check_advice_block( opening_point, }) } - -/// 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, - 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)), - ); - - 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); - 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 4bd807cc88..1058a7ac62 100644 --- a/crates/jolt-verifier/src/stages/stage4/verify.rs +++ b/crates/jolt-verifier/src/stages/stage4/verify.rs @@ -6,11 +6,13 @@ 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::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE4; #[cfg(feature = "field-inline")] use super::field_registers_read_write_checking::{ @@ -26,6 +28,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 +39,9 @@ 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, }, verifier::CheckedInputs, VerifierError, @@ -82,24 +83,24 @@ 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, { + 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); - 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,10 +144,11 @@ 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, )?; + 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. @@ -158,25 +160,27 @@ 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` - // 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(&staged_points, 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,53 +193,46 @@ 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, + |points, transcript| { + Stage4Sumchecks::receive_output_claims( + points, + &Stage4Sumchecks::claim_routes(points)?, + staged.by_id(), + 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, - )?; - // 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 54aace3241..154560f0cd 100644 --- a/crates/jolt-verifier/src/stages/stage5/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage5/outputs.rs @@ -166,7 +166,8 @@ 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; + use crate::stages::relations::ClaimRoutes; #[cfg(feature = "field-inline")] use jolt_claims::protocols::field_inline::FieldRegistersTraceDimensions; use jolt_claims::protocols::jolt::geometry::dimensions::TraceDimensions; @@ -175,7 +176,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) @@ -216,70 +217,42 @@ 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 /// 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 + ); } - /// 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 ); } - - /// 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 4317f1d7a6..c39c0cc3b9 100644 --- a/crates/jolt-verifier/src/stages/stage5/verify.rs +++ b/crates/jolt-verifier/src/stages/stage5/verify.rs @@ -1,10 +1,10 @@ #[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::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE5; #[cfg(feature = "field-inline")] use super::field_registers_val_evaluation::{ @@ -30,11 +30,9 @@ use super::{ }, }; use crate::{ - proof::JoltProof, stages::{ stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage4::{Stage4Output, Stage4OutputClaims, Stage4OutputPoints}, - zk::committed, }, verifier::CheckedInputs, VerifierError, @@ -78,20 +76,19 @@ 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, { + transcript.site(STAGE5); let log_k = crate::num::ilog2(checked.ram_K); let trace_dimensions = formula_dimensions.trace; @@ -112,10 +109,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 +121,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,29 +136,19 @@ 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, - )?; - // 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/booleanity.rs b/crates/jolt-verifier/src/stages/stage6a/booleanity.rs index 72d1f27205..108d941610 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; @@ -69,20 +69,20 @@ 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. - fn draw_challenges>( + /// 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 T, + transcript: &mut C, ) -> Result, VerifierError> { let chunk_bits = self.dimensions.log_k_chunk; let mut reference_address: Vec = @@ -91,14 +91,14 @@ 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 { let excess = reference_address.len().saturating_sub(chunk_bits); reference_address = reference_address.split_off(excess); } 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 de9af4d008..31cc32f4c3 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; @@ -355,21 +354,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 f5751c9e6c..a46dba18bd 100644 --- a/crates/jolt-verifier/src/stages/stage6a/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage6a/outputs.rs @@ -24,10 +24,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 58aaaaade7..85279118f4 100644 --- a/crates/jolt-verifier/src/stages/stage6a/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6a/verify.rs @@ -3,8 +3,11 @@ 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::{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; @@ -19,10 +22,9 @@ use super::{ }; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{ stage1::Stage1Output, stage2::Stage2Output, stage3::Stage3Output, stage4::Stage4Output, - stage5::Stage5Output, zk::committed, + stage5::Stage5Output, }, verifier::CheckedInputs, VerifierError, @@ -32,13 +34,11 @@ 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( +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,8 +48,10 @@ pub fn verify( where PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + 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. @@ -59,7 +61,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 +73,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,31 +90,19 @@ 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 batch = address_sumchecks.verify_zk( + checked.committed_row_len()?, + &address_input_points, + transcript, )?; - let output_points = address_sumchecks - .derive_opening_points(&consistency.challenges(), &address_input_points)?; 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, })); } - 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 +138,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, })) @@ -186,8 +174,8 @@ 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 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; @@ -235,33 +223,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, @@ -271,22 +252,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, @@ -297,69 +278,44 @@ 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() { - 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(); - 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!( + Stage6aSumchecks::wire_claim_values(&claims, &ClaimRoutes::default()), + [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, @@ -368,13 +324,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 6057d2d735..32340c7850 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, @@ -562,54 +557,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_scalar()) - .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/bytecode_read_raf.rs b/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs index 78da6bf30f..9ba1ac0114 100644 --- a/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs +++ b/crates/jolt-verifier/src/stages/stage6b/bytecode_read_raf.rs @@ -951,9 +951,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/field_inline.rs b/crates/jolt-verifier/src/stages/stage6b/field_inline.rs index bf48755ccc..0d48c25775 100644 --- a/crates/jolt-verifier/src/stages/stage6b/field_inline.rs +++ b/crates/jolt-verifier/src/stages/stage6b/field_inline.rs @@ -1,9 +1,7 @@ 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, }; @@ -74,14 +72,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 655608eb74..dcc1d53277 100644 --- a/crates/jolt-verifier/src/stages/stage6b/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage6b/outputs.rs @@ -2,8 +2,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 @@ -65,21 +68,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, @@ -113,6 +107,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). @@ -207,35 +227,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 - /// Akita 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 - + usize::from(cfg!(feature = "field-inline")) - + usize::from(self.trusted_advice.is_some()) - + usize::from(self.untrusted_advice.is_some()) - + usize::from(self.bytecode_reduction.is_some()) - + 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 0ff0daf5b8..ba1df6ce97 100644 --- a/crates/jolt-verifier/src/stages/stage6b/verify.rs +++ b/crates/jolt-verifier/src/stages/stage6b/verify.rs @@ -1,13 +1,10 @@ -use jolt_claims::protocols::jolt::{ - geometry::{bytecode, dimensions::JoltFormulaDimensions}, - BytecodeClaimReductionLayout, JoltCommittedPolynomial, JoltOpeningId, JoltRelationId, - PrecommittedReductionLayout, -}; -use jolt_claims::OutputClaims; +use jolt_claims::protocols::jolt::geometry::dimensions::JoltFormulaDimensions; use jolt_crypto::VectorCommitment; -use jolt_field::JoltField; +use jolt_field::{CanonicalDecode, JoltField}; use jolt_openings::CommitmentScheme; -use jolt_transcript::Transcript; +use jolt_transcript::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE6B; #[cfg(not(feature = "akita"))] use super::committed_reduction_cycle_phase::{ @@ -39,21 +36,16 @@ use super::{ instruction_ra_virtualization_input_values_from_upstream, }, outputs::{ - Stage6bClearOutput, Stage6bInputClaims, Stage6bInputPoints, Stage6bOutput, - Stage6bOutputClaims, Stage6bSumchecks, + Stage6bClearOutput, Stage6bInputClaims, Stage6bInputPoints, Stage6bOutput, Stage6bSumchecks, }, ram_ra_virtualization::{ ram_ra_virtualization_input_points_from_upstream, ram_ra_virtualization_input_values_from_upstream, }, }; -#[cfg(not(feature = "akita"))] -use crate::stages::zk::committed; use crate::{ preprocessing::JoltVerifierPreprocessing, - proof::JoltProof, stages::{ - relations::validate_member_presence, stage1::Stage1Output, stage2::{Stage2BatchOutputClaims, Stage2BatchOutputPoints, Stage2Output}, stage3::Stage3Output, @@ -69,13 +61,11 @@ 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( +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,19 +76,19 @@ pub fn verify( where PCS: CommitmentScheme, VC: VectorCommitment, - T: Transcript, + 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. 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()); let sumchecks = Stage6bSumchecks::build( checked, preprocessing, - proof, formula_dimensions, stage1, stage2, @@ -122,40 +112,7 @@ 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 batch = sumchecks.verify_zk(checked.committed_row_len()?, &input_points, transcript)?; return Ok(Stage6bOutput::Zk(Stage6bZkOutput { challenges: Stage6bCarriedChallenges { @@ -165,9 +122,9 @@ where field_registers_inc_gamma: draws.field_registers_inc_gamma, bytecode_reduction_eta: draws.eta, }, - batch_consistency: consistency, - batch_output_claims, - output_points: cycle_points, + batch_consistency: batch.consistency, + batch_output_claims: batch.output_claims, + output_points: batch.output_points, })); } @@ -175,43 +132,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, @@ -220,37 +140,16 @@ where stage4, &stage5.output_values, )?; - let cycle_points = sumchecks.verify_clear( + let (cycle_points, claims) = sumchecks.verify_clear( &input_values, &input_points, &cycle_challenges, - claims, - &proof.stages.stage6b_sumcheck_proof, transcript, 6, )?; - 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 @@ -259,125 +158,6 @@ 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-final 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(), - )?; - - #[cfg(feature = "akita")] - { - let expected_chunks = - jolt_claims::protocols::jolt::lattice::geometry::BalancedIncChunking::new( - committed_chunk_bits, - ) - .map_err(|error| 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(), - )?; - } - - 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(); - let shape_ok = output_claims.intermediate().is_some() == has_address_phase; - if !shape_ok { - return Err(VerifierError::StageClaimPublicInputFailed { - stage: JoltRelationId::BytecodeClaimReductionCyclePhase, - reason: format!( - "bytecode reduction cycle output shape mismatch (address phase: {has_address_phase})" - ), - }); - } - } - - Ok(()) -} - -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}"), - }); - } - Ok(()) -} - /// 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 @@ -477,108 +257,7 @@ 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); - #[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 -} - -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 { #[cfg(not(feature = "akita"))] use super::super::booleanity::BooleanityOutputClaims; @@ -591,10 +270,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::append_recording::RecordingTranscript; + use crate::stages::relations::{ClaimRoute, ClaimRoutes}; + use jolt_claims::protocols::jolt::{JoltCommittedPolynomial, JoltOpeningId, JoltRelationId}; use jolt_field::{Fr, Ring}; fn fr(value: u64) -> Fr { @@ -674,245 +355,43 @@ 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() - } - - 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 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 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 wire_claims_follow_declaration_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); + assert_eq!( + 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 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"); + fn aliased_bytecode_ra_is_not_sent() { + let (claims, last) = sample_claims(); 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"); + 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 a1bdc68f0a..aa8af8ab47 100644 --- a/crates/jolt-verifier/src/stages/stage7/outputs.rs +++ b/crates/jolt-verifier/src/stages/stage7/outputs.rs @@ -159,9 +159,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, @@ -189,48 +189,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, - 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() { let (trusted, untrusted, bytecode, image, plain_last, committed_last) = (5, 6, 7, 8, 6, 8); let hamming = HammingWeightClaimReductionOutputClaims { instruction_ra: vec![fr(1), fr(2)], @@ -244,13 +203,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()), @@ -259,25 +211,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), @@ -290,16 +227,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 bc7ceefe73..f1897d5c15 100644 --- a/crates/jolt-verifier/src/stages/stage7/verify.rs +++ b/crates/jolt-verifier/src/stages/stage7/verify.rs @@ -8,10 +8,10 @@ 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::{Channel, Sponge, VerifierTranscript}; + +use crate::sites::STAGE7; #[cfg(not(feature = "akita"))] use super::advice_address_phase::{ @@ -33,11 +33,9 @@ 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, PrecommittedSchedule, }, verifier::CheckedInputs, @@ -48,23 +46,22 @@ 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( +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, { + transcript.site(STAGE7); 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 +87,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,56 +95,30 @@ 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, - )?; - // 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, })); } 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/akita.rs b/crates/jolt-verifier/src/stages/stage8/akita.rs index 6aa841ad30..c431f4868f 100644 --- a/crates/jolt-verifier/src/stages/stage8/akita.rs +++ b/crates/jolt-verifier/src/stages/stage8/akita.rs @@ -20,7 +20,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")] @@ -142,14 +142,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)?; @@ -215,16 +215,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, @@ -232,10 +231,10 @@ pub fn verify( ) -> Result<(), VerifierError> where PCS: CommitmentScheme, - PCS::Output: Clone + AppendToTranscript + OneHotTraceCommitmentMetadata, + PCS::Output: Clone + OneHotTraceCommitmentMetadata, PCS::VerifierSetup: OneHotTraceSetupMetadata, VC: 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 @@ -321,9 +320,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)?); @@ -352,7 +349,6 @@ where &preprocessing.pcs_setup, &auxiliary_groups, &main_group, - proof, transcript, ) .map_err(opening_failed)?; 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/verify.rs b/crates/jolt-verifier/src/stages/stage8/verify.rs index 453aa1c9d0..53934eaee1 100644 --- a/crates/jolt-verifier/src/stages/stage8/verify.rs +++ b/crates/jolt-verifier/src/stages/stage8/verify.rs @@ -3,15 +3,17 @@ 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; #[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, @@ -44,9 +46,9 @@ 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, Sponge, VerifierTranscript}; + +use crate::sites::STAGE8; #[cfg(not(feature = "akita"))] /// One assembled final-opening batch entry. Public because the prover's @@ -69,14 +71,13 @@ 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( +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,8 +88,9 @@ where + ZkOpeningScheme, PCS::Output: Clone + HomomorphicCommitment, VC: VectorCommitment, - T: Transcript, + H: Sponge, { + transcript.site(STAGE8); let log_t = formula_dimensions.trace.log_t(); let layout = formula_dimensions.ra_layout; @@ -113,7 +115,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 +132,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 +145,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 +161,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 +172,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 +187,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 +194,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 +224,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 +255,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, @@ -478,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"), feature = "field-inline"))] #[expect(clippy::unwrap_used)] mod tests { @@ -563,10 +520,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); @@ -622,29 +585,20 @@ mod tests { } #[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( @@ -665,14 +619,13 @@ mod tests { clippy::too_many_arguments, 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, @@ -680,21 +633,21 @@ pub fn verify( where F: JoltField, PCS: CommitmentScheme, - PCS::Output: Clone + AppendToTranscript + super::OneHotTraceCommitmentMetadata, + PCS::Output: Clone + OneHotTraceCommitmentMetadata, PCS::VerifierSetup: super::OneHotTraceSetupMetadata, VC: VectorCommitment, - T: Transcript, + H: Sponge, { + transcript.site(STAGE8); super::akita::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 35673572f4..515a0fd3d7 100644 --- a/crates/jolt-verifier/src/stages/uniskip.rs +++ b/crates/jolt-verifier/src/stages/uniskip.rs @@ -15,15 +15,21 @@ use jolt_claims::protocols::composed::geometry::{ use jolt_claims::protocols::composed::r1cs::{ SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE, SPARTAN_OUTER_UNISKIP_FIRST_ROUND_DEGREE, }; +use jolt_claims::protocols::jolt::geometry::spartan::{ + outer_uniskip_opening, product_uniskip_opening, +}; +use jolt_claims::protocols::jolt::JoltOpeningId; use jolt_claims::protocols::jolt::JoltRelationId; +use jolt_field::CanonicalDecode; use jolt_field::JoltField; 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::stages::relations::CommittedClaimLayout; +use crate::stages::zk::outputs::{CommittedOutputClaimOutput, CommittedOutputClaimShape}; use crate::verifier::CheckedInputs; use crate::VerifierError; @@ -36,10 +42,11 @@ 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, stage_number: usize, degree: usize, domain_size: usize, - proof_field: &'static str, } impl UniskipParams { @@ -47,10 +54,10 @@ 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, - proof_field: "stage1_uni_skip_first_round_proof", } } @@ -58,10 +65,10 @@ 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, - proof_field: "stage2_uni_skip_first_round_proof", } } @@ -89,29 +96,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 @@ -123,79 +128,78 @@ 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 = 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(), + 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") @@ -204,7 +208,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/field_inline.rs b/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs index 850e66c141..32c0bb169b 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/field_inline.rs @@ -1,7 +1,4 @@ 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; @@ -19,7 +16,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; @@ -50,13 +47,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 @@ -89,13 +79,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 @@ -133,14 +116,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. @@ -164,15 +139,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 — @@ -298,12 +264,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 75dbfbd9ab..68b798cd4e 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, }; @@ -68,13 +68,13 @@ 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, protocols::jolt::{ geometry::{ - booleanity::{self, BooleanityDimensions}, + booleanity::BooleanityDimensions, bytecode::{self, BytecodeReadRafEvaluationInputs}, claim_reductions::{ advice, @@ -82,10 +82,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, @@ -96,16 +96,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::{ @@ -205,8 +204,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, @@ -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,20 +413,6 @@ where } } -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. /// @@ -521,15 +487,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), @@ -541,8 +506,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> @@ -566,8 +531,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, @@ -581,7 +546,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)); } @@ -595,8 +560,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, @@ -615,8 +580,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> @@ -669,8 +634,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 @@ -681,8 +646,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 @@ -696,8 +661,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, @@ -1085,8 +1050,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 @@ -1129,8 +1094,8 @@ where ) } -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> @@ -1161,8 +1126,8 @@ where ) } -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], @@ -1192,8 +1157,8 @@ where ) } -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> @@ -1223,8 +1188,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], @@ -1250,8 +1215,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, @@ -1283,8 +1248,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, @@ -1311,8 +1276,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 @@ -1447,7 +1412,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 2c5593406f..916c258a4e 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> @@ -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 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_claims::protocols::composed::r1cs::spartan_outer_opening_columns().len(); @@ -220,7 +212,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 70cc333ef4..ebe3d7c536 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage2.rs @@ -1,29 +1,17 @@ -#[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. -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 +24,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| { @@ -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()), )?; @@ -216,8 +202,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`. @@ -243,85 +227,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> { @@ -383,22 +291,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, @@ -409,109 +306,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, - } - }; - - #[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 ec67cbbf62..0455eed178 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage3.rs @@ -1,9 +1,7 @@ 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> @@ -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 1906feb55e..ae5b662cc5 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage4.rs @@ -1,13 +1,10 @@ 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. -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 +18,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 @@ -131,12 +128,6 @@ where + input.stage4.challenges.ram_val_check.gamma, )?; - let output_ids = stage4_output_ids::( - input.proof.untrusted_advice_commitment.is_some(), - 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 153d4259fb..647c451788 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage5.rs @@ -1,14 +1,10 @@ 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. -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> @@ -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 @@ -158,8 +152,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))] @@ -179,132 +171,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 2d18a868c2..c9e9af8138 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); @@ -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 f0d7e1670a..cbecfde09e 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage6b.rs @@ -1,14 +1,9 @@ -use super::*; - use jolt_claims::protocols::jolt::relations::claim_reductions::bytecode::CyclePhase as BytecodeCyclePhase; -use jolt_claims::protocols::jolt::relations::claim_reductions::increments::IncClaimReductionOutputClaims; -use jolt_claims::protocols::jolt::relations::ram::{ - RamHammingBooleanityOutputClaims, RamRaVirtualizationOutputClaims, -}; +use super::*; -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> @@ -25,7 +20,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); @@ -92,24 +87,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", @@ -118,279 +95,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()) - .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() - } - - 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 a6552c6592..21cdc7924e 100644 --- a/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs +++ b/crates/jolt-verifier/src/stages/zk/blindfold/stage7.rs @@ -2,8 +2,8 @@ use super::*; use jolt_claims::protocols::jolt::relations::claim_reductions::bytecode::AddressPhase as BytecodeAddressPhase; -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> @@ -15,7 +15,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); @@ -121,24 +121,18 @@ 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(claim) = bytecode_reduction_claims { claims.push(relation_claim(&claim)); - output_ids.push(bytecode_reduction::final_program_bytecode_opening().into()); } 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, @@ -148,7 +142,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 5c6aed7450..0000000000 --- a/crates/jolt-verifier/src/stages/zk/committed.rs +++ /dev/null @@ -1,47 +0,0 @@ -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) fn verify_output_claim_commitments( - checked: &CheckedInputs, - proof: &SumcheckProof, - proof_label: &'static str, - output_claim_count: usize, - stage: JoltRelationId, -) -> Result, VerifierError> -where - F: JoltField, - C: Clone, -{ - // 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). - 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(), - }) -} 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/mod.rs b/crates/jolt-verifier/src/stages/zk/mod.rs index 91b664187c..2f6f1560b9 100644 --- a/crates/jolt-verifier/src/stages/zk/mod.rs +++ b/crates/jolt-verifier/src/stages/zk/mod.rs @@ -3,7 +3,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 adf09e8d28..5daff2b049 100644 --- a/crates/jolt-verifier/src/stages/zk/outputs.rs +++ b/crates/jolt-verifier/src/stages/zk/outputs.rs @@ -1,13 +1,48 @@ -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`]). + /// `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 } + } + + /// Number of output-claim row commitments the committed sumcheck sends. + pub fn row_count(&self) -> usize { + self.layout.ids.len().div_ceil(self.row_len) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] 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 60eb97434a..7d0fd03eab 100644 --- a/crates/jolt-verifier/src/verifier.rs +++ b/crates/jolt-verifier/src/verifier.rs @@ -2,98 +2,179 @@ 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::sites::BLINDFOLD; use crate::{ config::{ validate_proof_config, ZkConfig, JOLT_VERIFIER_CONFIG, JOLT_VERIFIER_INSTRUCTION_PROFILE, }, num, preprocessing::JoltVerifierPreprocessing, - proof::{JoltProof, TracePolynomialOrder}, + proof::{JoltProof, ProofCommitments, ProofHeader}, + sites::{COMMITMENTS, PREAMBLE}, 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, }; +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 = PoseidonSponge; +/// See the Poseidon arm. +#[cfg(all(feature = "transcript-keccak", not(feature = "transcript-poseidon")))] +pub type JoltSponge = Keccak; +/// See the Poseidon arm. +#[cfg(not(any(feature = "transcript-keccak", feature = "transcript-poseidon")))] +pub type JoltSponge = 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 { + protocol: blindfold, + checkpoint: _, + } => { + transcript.site(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 { + 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], + }, +} - 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 +184,8 @@ where let stage6b = stage6b::verify( &checked, preprocessing, - proof, &formula_dimensions, - &mut transcript, + transcript, &stage1, &stage2, &stage3, @@ -113,112 +193,83 @@ 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 { - 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()?, - })?; - 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(()); + if !checked.zk { + let Stage8Output::Clear(_) = stage8 else { + return Err(VerifierError::ExpectedClearProof { field: "stage8" }); + }; + return Ok(VerifiedStages::Clear); } - - let stage8::Stage8Output::Clear(_) = stage8 else { - return Err(VerifierError::ExpectedClearProof { field: "stage8" }); - }; - - Ok(()) + 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 ordered native /// trace-column claims and one batch 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 + 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 +278,6 @@ where let stage6a = stage6a::verify( &checked, preprocessing, - proof, &formula_dimensions, &mut transcript, &stage1, @@ -239,7 +289,6 @@ where let stage6b = stage6b::verify( &checked, preprocessing, - proof, &formula_dimensions, &mut transcript, &stage1, @@ -251,7 +300,6 @@ where )?; let stage7 = stage7::verify( &checked, - proof, &formula_dimensions, &mut transcript, &stage4, @@ -260,7 +308,7 @@ where let stage8 = stage8::verify( &checked, preprocessing, - proof, + &commitments, &formula_dimensions, trusted_advice_commitment, &mut transcript, @@ -273,45 +321,64 @@ 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)?; + transcript.site(PREAMBLE); + 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, + )?; + transcript.site(COMMITMENTS); + 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 +388,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 +399,75 @@ pub struct CheckedInputs { pub precommitted: PrecommittedSchedule, } -pub fn validate_inputs( +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. +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 (the whole bytecode, 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; + }; + PCS::absorb_commitment(&committed.bytecode_commitment, channel); + PCS::absorb_commitment(&committed.program_image_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; let zk = matches!(JOLT_VERIFIER_CONFIG.zk, ZkConfig::BlindFold); let vc_capacity = if zk { Some(validate_zk_vector_commitment_setup::( @@ -505,6 +629,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, @@ -540,723 +668,59 @@ 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 Akita 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", +/// 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 +/// 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: &MemoryLayout) -> Result<(), VerifierError> { + let lowest_address = memory_layout.get_lowest_address(); + if lowest_address <= 8 { + return Err(VerifierError::InvalidMemoryLayout { + reason: format!( + "lowest remapped RAM address {lowest_address:#x} must exceed 8 so the RAF \ + unmap constant stays clear of the null-address range" + ), }); } - - 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(()); - } +#[cfg(test)] +mod tests { + use std::sync::Arc; - if zk { - Err(VerifierError::ExpectedCommittedProof { field }) - } else { - Err(VerifierError::ExpectedClearProof { field }) - } -} + use common::constants::RAM_START_ADDRESS; -/// 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(), - ); -} + use super::*; + use crate::preprocessing::ProgramPreprocessing; + use common::jolt_device::{JoltDevice, MemoryConfig}; + #[cfg(feature = "zk")] + use jolt_crypto::PedersenSetup; + 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_transcript::{ProverTranscript, VerifierTranscript}; -/// 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_commitment, - &committed.program_image_commitment, - transcript, - ); - } - } - #[cfg(feature = "akita")] - absorb_akita_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(|committed| { - ( - &committed.bytecode_commitment, - &committed.program_image_commitment, - ) - }), - transcript, - ); -} + #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + struct TestPcs; -/// Absorbs the Akita commitment objects in canonical object order: `OneHotTrace`, untrusted -/// advice, trusted advice, the field-increment commitment (field-inline builds), then the whole -/// bytecode and program image. Shared verbatim by the Akita prover's stage 0. -#[cfg(feature = "akita")] -pub fn absorb_akita_commitments( - one_hot_trace: &C, - untrusted_advice_commitment: Option<&C>, - trusted_advice_commitment: Option<&C>, - #[cfg(feature = "field-inline")] field_inc_commitment: Option<&C>, - program_commitments: Option<(&C, &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); - } - if let Some((bytecode, image)) = program_commitments { - absorb_akita_program_commitments(bytecode, image, transcript); + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + struct TestCommitment; + + impl Commitment for TestPcs { + type Output = TestCommitment; } -} -#[cfg(feature = "akita")] -pub fn absorb_akita_program_commitments(bytecode: &C, image: &C, transcript: &mut T) -where - C: AppendToTranscript, - T: Transcript, -{ - append_length_prefixed(transcript, b"program_bytecode_commitment", bytecode); - append_length_prefixed(transcript, b"program_image_init_commitment", image); -} - -/// Absorbs the preprocessing-held committed-program commitments (whole -/// 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_commitment: &C, - program_image_commitment: &C, - transcript: &mut T, -) where - C: AppendToTranscript, - T: Transcript, -{ - append_payload_label(transcript, b"program_bytecode_commit", bytecode_commitment); - transcript.append(bytecode_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 -/// 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> { - let lowest_address = memory_layout.get_lowest_address(); - if lowest_address <= 8 { - return Err(VerifierError::InvalidMemoryLayout { - reason: format!( - "lowest remapped RAM address {lowest_address:#x} must exceed 8 so the RAF \ - unmap constant stays clear of the null-address range" - ), - }); - } - 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| { - 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, - 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"), - 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 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_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; - - #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - struct TestPcs; - - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] - struct TestCommitment; - - impl Commitment for TestPcs { - type Output = TestCommitment; - } - - impl CommitmentScheme for TestPcs { - type Field = Fr; - type Proof = (); - type ProverSetup = (); - type VerifierSetup = (); - type OpeningHint = (); - type SetupParams = (); + impl CommitmentScheme for TestPcs { + type Field = Fr; + type ProverSetup = (); + type VerifierSetup = (); + type OpeningHint = (); + type SetupParams = (); fn setup( _params: Self::SetupParams, @@ -1273,35 +737,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() { @@ -1319,26 +843,18 @@ mod tests { { } - assert_proof_traits::(); - } - - #[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) - )); + assert_proof_traits::(); } + /// `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 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(); #[cfg(feature = "field-inline")] @@ -1349,38 +865,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] @@ -1393,17 +896,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()); } @@ -1411,10 +914,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) )); } @@ -1431,10 +933,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") )); @@ -1447,11 +948,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, .. }) )); } @@ -1463,11 +966,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, @@ -1503,10 +1008,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) )); } @@ -1523,30 +1027,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 { @@ -1559,299 +1048,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, } } diff --git a/crates/jolt-verifier/tests/completeness/standard.rs b/crates/jolt-verifier/tests/completeness/standard.rs index 03cd424b34..1f5145aec2 100644 --- a/crates/jolt-verifier/tests/completeness/standard.rs +++ b/crates/jolt-verifier/tests/completeness/standard.rs @@ -8,6 +8,8 @@ #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] use crate::support; +#[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] +use jolt_verifier::JoltSponge; #[test] #[cfg(all(feature = "prover-fixtures", not(feature = "zk")))] @@ -61,10 +63,10 @@ fn standard_committed_muldiv_verifier_proof_is_accepted() { case.preprocessing.program.bytecode_len(), ) .unwrap(); + let header = case.proof.header::().unwrap(); assert!( bytecode_vars - > case.proof.trace_length.ilog2() as usize - + case.proof.one_hot_config.committed_chunk_bits() + > header.trace_length.ilog2() as usize + header.one_hot_config.committed_chunk_bits() ); let config = bincode::config::standard(); let encoded = bincode::serde::encode_to_vec(&case.preprocessing, config).unwrap(); diff --git a/crates/jolt-verifier/tests/completeness/zk.rs b/crates/jolt-verifier/tests/completeness/zk.rs index e1066f1d1c..d34663ff2e 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,171 +38,48 @@ 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 } diff --git a/crates/jolt-verifier/tests/fs_attacks.rs b/crates/jolt-verifier/tests/fs_attacks.rs index 0847c7aafd..b59f760a6c 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,319 @@ 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::DuplexSpongeInterface; + use jolt_transcript::{Blake2b512, Fork, FORK_SEED_LEN}; - 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" - ); + /// 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] + #[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!(squeezed, expected); + assert!(tape.bytes.is_empty()); + } + + #[test] + #[should_panic(expected = "outside a record or replay session")] + fn transcript_sponge_needs_a_session() { + let _ = AuditSponge::default().absorb(b"statement"); + } + + #[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/fs_inventory/absorb-sites.inventory b/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory index ec223a1486..b64639a7e3 100644 --- a/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/absorb-sites.inventory @@ -1,234 +1,49 @@ # 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#3::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#4::claim.commitment.append_to_transcript(transcript) -jolt-akita::adapters.rs::append_batch_statement::append_to_transcript#5::claim.evaluation.value.append_to_transcript(transcript) -jolt-akita::adapters.rs::append_batch_statement::append_values#2::transcript.append_values(b"akita_pcs_point",point) -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#7::transcript.append(&Label(b"akita_chunk_profile")) -jolt-akita::adapters.rs::append_verifier_setup::append#8::transcript.append(&U64Word(akita_chunk_profile.num_chunks()asu64)) -jolt-akita::adapters.rs::append_verifier_setup::append_bytes#10::transcript.append_bytes(&catalog_digest) -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#9::transcript.append_bytes(&setup.default_layout_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#11::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_v4")) -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#12::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_values#10::transcript.append_values(b"akita_group_point",&claim.point) -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::folded_instance_from_proof::append_values#2::transcript.append_values(b"bf_cross_e",&proof.cross_term_error_row_commitments) -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_values#2::transcript.append_values(b"bf_az_bz_cz",&[proof.az_rx,proof.bz_rx,proof.cz_rx]) -jolt-blindfold::verify.rs::BlindFoldProtocol::verify_outer_folded_r1cs::append_vector_opening#3::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::RelaxedInstance::append_to_transcript::append_values#2::transcript.append_values(witness_label,&self.witness_row_commitments) -jolt-blindfold::verify.rs::RelaxedInstance::append_to_transcript::append_values#3::transcript.append_values(error_label,&self.error_row_commitments) -jolt-blindfold::verify.rs::RelaxedInstance::append_to_transcript::append_values#4::transcript.append_values(eval_label,&self.eval_commitments) -jolt-blindfold::verify.rs::append_vector_opening::append#1::transcript.append(&Label(blinding_label)) -jolt-blindfold::verify.rs::append_vector_opening::append_to_transcript#2::opening.combined_blinding.append_to_transcript(transcript) -jolt-blindfold::verify.rs::append_vector_opening::append_values#0::transcript.append_values(row_label,&opening.combined_vector) -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-hyperkzg::kzg.rs::HyperKZGProverSetup::open_batch::absorb_evaluations#0::absorb_evaluations(&evaluations,transcript) -jolt-hyperkzg::kzg.rs::HyperKZGProverSetup::open_batch::absorb_witnesses#1::absorb_witnesses(&witnesses,transcript) -jolt-hyperkzg::kzg.rs::HyperKZGVerifierSetup::verify_batch::absorb_evaluations#0::absorb_evaluations(evaluations,transcript) -jolt-hyperkzg::kzg.rs::HyperKZGVerifierSetup::verify_batch::absorb_witnesses#1::absorb_witnesses(witnesses,transcript) -jolt-hyperkzg::kzg.rs::absorb_evaluations::append#0::transcript.append(value) -jolt-hyperkzg::kzg.rs::absorb_witnesses::append#0::transcript.append(witness) -jolt-hyperkzg::scheme.rs::HyperKZGScheme::open_table::append#0::transcript.append(commitment) -jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify_opening::append#0::transcript.append(commitment) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append#1::transcript.append(&self.g1) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append#2::transcript.append(&self.g2) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append#3::transcript.append(&self.beta_g2) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append#4::transcript.append(commitment) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append#6::transcript.append(&evaluation) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append_bytes#0::transcript.append_bytes(&self.setup_id) -jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::append_statement::append_values#5::transcript.append_values(b"opening-point",point) -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::prefix.rs::PrefixPackedLayout::reduce_claims::append_values#5::transcript.append_values(b"prefix_pack_point",claims.point.as_slice()) -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::append_values#6::transcript.append_values(b"prefix_pack_evals",&claims.evaluations) -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-spartan-verifier::key.rs::SpartanKey::begin::append#0::transcript.append(&Label(b"spartan-clear-v1")) -jolt-spartan-verifier::key.rs::SpartanKey::begin::append_bytes#1::transcript.append_bytes(&self.relation_digest) -jolt-spartan-verifier::key.rs::SpartanKey::begin::append_bytes#2::transcript.append_bytes(&self.policy_id) -jolt-spartan-verifier::key.rs::SpartanKey::begin::append_values#3::transcript.append_values(b"public-inputs",public_inputs) -jolt-spartan-verifier::key.rs::SpartanKey::begin_inner::append_values#0::transcript.append_values(b"outer-evaluations",&evaluations) -jolt-spartan-verifier::lib.rs::SpartanKey::verify::append_witness_evaluation#0::Self::append_witness_evaluation(proof.witness_evaluation,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_akita_commitments::absorb_akita_program_commitments#4::absorb_akita_program_commitments(bytecode,image,transcript) -jolt-verifier::verifier.rs::absorb_akita_commitments::append_length_prefixed#0::append_length_prefixed(transcript,b"commitment",one_hot_trace) -jolt-verifier::verifier.rs::absorb_akita_commitments::append_length_prefixed#1::append_length_prefixed(transcript,b"untrusted_advice",commitment) -jolt-verifier::verifier.rs::absorb_akita_commitments::append_length_prefixed#2::append_length_prefixed(transcript,b"trusted_advice",commitment) -jolt-verifier::verifier.rs::absorb_akita_commitments::append_length_prefixed#3::append_length_prefixed(transcript,b"field_inc",commitment) -jolt-verifier::verifier.rs::absorb_akita_program_commitments::append_length_prefixed#0::append_length_prefixed(transcript,b"program_bytecode_commitment",bytecode) -jolt-verifier::verifier.rs::absorb_akita_program_commitments::append_length_prefixed#1::append_length_prefixed(transcript,b"program_image_init_commitment",image) -jolt-verifier::verifier.rs::absorb_commitments::absorb_akita_commitments#2::#[cfg(feature="akita")]absorb_akita_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(|committed|{(&committed.bytecode_commitment,&committed.program_image_commitment,)}),transcript,) -jolt-verifier::verifier.rs::absorb_commitments::absorb_committed_program_commitments#1::absorb_committed_program_commitments(&committed.bytecode_commitment,&committed.program_image_commitment,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(bytecode_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"program_bytecode_commit",bytecode_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_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(&[flavor.wire_tag()]) +jolt-akita::adapters.rs::absorb_setup_catalog::public#2::channel.public(&word(setup.akita_chunk_profile().num_chunks())) +jolt-akita::adapters.rs::absorb_setup_catalog::public#3::channel.public(&setup.default_layout_digest) +jolt-akita::adapters.rs::absorb_setup_catalog::public#4::channel.public(&catalog_digest) +jolt-akita::adapters.rs::absorb_setup_catalog::public_all#1::channel.public_all(&[word(setup.max_num_vars),word(setup.max_num_polys_per_commitment_group),word(setup.max_total_batch_polys),word(setup.one_hot_k),]) +jolt-dory::scheme.rs::DoryScheme::absorb_commitment::public#0::channel.public(commitment) +jolt-hyperkzg::scheme.rs::HyperKZGScheme::absorb_commitment::public#0::channel.public(commitment) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#0::channel.public(&self.num_powers.to_le_bytes()) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#1::channel.public(&self.setup_id) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#2::channel.public(&self.g1) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#3::channel.public(&self.g2) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#4::channel.public(&self.beta_g2) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#5::channel.public(commitment) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public#7::channel.public(&evaluation) +jolt-hyperkzg::types.rs::HyperKZGVerifierSetup::bind_statement::public_all#6::channel.public_all(point) +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-spartan-verifier::key.rs::SpartanKey::begin_inner::public#2::channel.public(&claim) +jolt-spartan-verifier::key.rs::SpartanKey::bind_statement::public#0::channel.public(&self.relation_digest) +jolt-spartan-verifier::key.rs::SpartanKey::bind_statement::public#1::channel.public(&self.policy_id) +jolt-spartan-verifier::key.rs::SpartanKey::bind_statement::public_all#2::channel.public_all(public_inputs) +jolt-spartan-verifier::key.rs::SpartanKey::draw_tau::public#1::channel.public(&F::zero()) +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(&committed.bytecode_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_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 ca195218b0..0ea0df2453 100644 --- a/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory +++ b/crates/jolt-verifier/tests/fs_inventory/challenge-sites.inventory @@ -1,51 +1,55 @@ # 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#3 -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-claims::r1cs.rs::lower_claim_expr::challenge#0 -jolt-dory::transcript.rs::JoltToDoryTranscript<'_,T>::challenge_scalar::challenge_scalar#0 -jolt-hyperkzg::kzg.rs::absorb_evaluations::challenge#1 -jolt-hyperkzg::kzg.rs::absorb_witnesses::challenge#1 -jolt-hyperkzg::scheme.rs::HyperKZGScheme::open_table::challenge#1 -jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify_opening::challenge#1 -jolt-openings::prefix.rs::PrefixPackedLayout::reduce_claims::challenge_vector#7 -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch::challenge_scalar_powers#1 -jolt-openings::schemes.rs::HomomorphicBatch::verify_batch_zk::challenge_scalar_powers#0 -jolt-spartan-verifier::key.rs::SpartanKey::begin::challenge_vector#4 +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#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 +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-hyperkzg::kzg.rs::exchange_evaluations::challenge#1 +jolt-hyperkzg::kzg.rs::exchange_witnesses::challenge#1 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::exchange_fold_commitments::challenge#1 +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-prover::blindfold.rs::prove_blindfold::challenge_bytes#1 jolt-spartan-verifier::key.rs::SpartanKey::begin_inner::challenge#1 -jolt-spartan-verifier::key.rs::SpartanKey::begin_inner::challenge#2 -jolt-spartan-verifier::key.rs::SpartanKey::begin_inner::challenge#3 -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-spartan-verifier::key.rs::SpartanKey::draw_tau::challenge#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 +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 new file mode 100644 index 0000000000..c73a5ddd58 --- /dev/null +++ b/crates/jolt-verifier/tests/fs_inventory/delegation-sites.inventory @@ -0,0 +1,240 @@ +# 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::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_on_transcript#1 +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_on_transcript::batched_prove::#0 +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::.send#2 +jolt-blindfold::prove.rs::prove_with_row_committer::.send#7 +jolt-blindfold::prove.rs::prove_with_row_committer::prove_inner_sumcheck#11 +jolt-blindfold::prove.rs::prove_with_row_committer::prove_outer_sumcheck#6 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#12 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#3 +jolt-blindfold::prove.rs::prove_with_row_committer::send_opening#4 +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::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::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 +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::.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::.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-hyperkzg::kzg.rs::HyperKZGProverSetup::open_batch::exchange_evaluations#0 +jolt-hyperkzg::kzg.rs::HyperKZGProverSetup::open_batch::exchange_witnesses#1 +jolt-hyperkzg::kzg.rs::HyperKZGVerifierSetup::verify_batch::exchange_witnesses#0 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::open::Self::open_table#0 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::open_table::.bind_statement#0 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::open_table::.open_batch#2 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::open_table::Self::exchange_fold_commitments#1 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify::Self::verify_opening#0 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify_opening::.bind_statement#0 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify_opening::.verify_batch#3 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify_opening::Self::exchange_fold_commitments#1 +jolt-hyperkzg::scheme.rs::HyperKZGScheme::verify_opening::exchange_evaluations#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::PCS::prove_batch#3 +jolt-prover::akita/stage8.rs::prove_stage8::reduce_precommitted#0 +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::reduce_precommitted::.reduce_claims#0 +jolt-prover::blindfold.rs::prove_blindfold::.narg#0 +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/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 +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-spartan-verifier::lib.rs::SpartanKey::verify::.begin_inner#4 +jolt-spartan-verifier::lib.rs::SpartanKey::verify::.bind_statement#0 +jolt-spartan-verifier::lib.rs::SpartanKey::verify::.draw_tau#2 +jolt-spartan-verifier::lib.rs::SpartanKey::verify::PCS::verify#7 +jolt-spartan-verifier::lib.rs::SpartanKey::verify::Self::exchange_witness_evaluation#6 +jolt-spartan-verifier::lib.rs::SpartanKey::verify::SumcheckVerifier::verify_compressed#3 +jolt-spartan-verifier::lib.rs::SpartanKey::verify::SumcheckVerifier::verify_compressed#5 +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::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 +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/verify.rs::verify::.draw_challenges#0 +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::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 +jolt-verifier::stages/stage7/verify.rs::verify::.verify_zk#1 +jolt-verifier::stages/stage8/akita.rs::reduce_object::.reduce_claims#0 +jolt-verifier::stages/stage8/akita.rs::verify::PCS::verify_batch#3 +jolt-verifier::stages/stage8/akita.rs::verify::reduce_object#0 +jolt-verifier::stages/stage8/akita.rs::verify::reduce_object#1 +jolt-verifier::stages/stage8/akita.rs::verify::reduce_object#2 +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::akita::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..886f1e2582 --- /dev/null +++ b/crates/jolt-verifier/tests/fs_inventory/message-sites.inventory @@ -0,0 +1,83 @@ +# 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::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) +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::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) +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-hyperkzg::kzg.rs::exchange_evaluations::exchange_all#0::channel.exchange_all(row) +jolt-hyperkzg::kzg.rs::exchange_witnesses::exchange_all#0::channel.exchange_all(witnesses) +jolt-hyperkzg::scheme.rs::HyperKZGScheme::exchange_fold_commitments::exchange_all#0::channel.exchange_all(com) +jolt-hyperkzg::scheme.rs::HyperKZGScheme::receive_commitment::receive#0::transcript.receive() +jolt-hyperkzg::scheme.rs::HyperKZGScheme::send_commitment::send#0::transcript.send(commitment) +jolt-spartan-verifier::key.rs::SpartanKey::begin_inner::exchange_all#0::channel.exchange_all(evaluations) +jolt-spartan-verifier::key.rs::SpartanKey::exchange_witness_evaluation::exchange#0::channel.exchange(evaluation) +jolt-spartan-verifier::lib.rs::SpartanKey::verify::receive_commitment#1::PCS::receive_commitment(pcs_setup,transcript) +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#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::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()) +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 385db32132..b6607fc9ed 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,7 +46,6 @@ 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 @@ -54,41 +61,23 @@ 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 5a20f88498..2787c47772 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, ExprLit, ExprMethodCall, FnArg, Generics, ImplItemFn, + Item, ItemConst, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, ItemTrait, Lit, Local, Macro, + Meta, 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| { @@ -138,7 +229,7 @@ fn production_sources(workspace: &Path) -> Vec { ) ) }) - .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() { @@ -166,11 +257,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"] @@ -178,7 +279,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") @@ -191,35 +292,80 @@ 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 { + /// A `#[path = "..."]` override, relative to the declaring file's directory. + fn path_attribute(attrs: &[Attribute]) -> Option { + attrs.iter().find_map(|attr| match &attr.meta { + Meta::NameValue(meta) if meta.path.is_ident("path") => match &meta.value { + Expr::Lit(ExprLit { + lit: Lit::Str(path), + .. + }) => Some(path.value()), + _ => None, + }, + _ => None, + }) + } + + fn declared_modules(items: &[Item], dir: &Path, file_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), file_dir, output); + continue; } + let file = dir.join(format!("{name}.rs")); + let nested = dir.join(&name).join("mod.rs"); + let path = if let Some(path) = path_attribute(&module.attrs) { + file_dir.join(path) + } else 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, parent, 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")) @@ -240,64 +386,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 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 record_challenge(&mut self, kind: &str) { + 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 { @@ -310,75 +547,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); }); } @@ -387,24 +616,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); }); } @@ -413,102 +661,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); @@ -524,64 +796,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" | "append_values" - ) -} - -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/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/soundness/akita.rs b/crates/jolt-verifier/tests/soundness/akita.rs new file mode 100644 index 0000000000..1327476d49 --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/akita.rs @@ -0,0 +1,65 @@ +//! Akita (native lattice) fixtures. Legacy-shape fixtures run with +//! field-inline disabled; the Akita 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::akita_fixtures::{ + akita_advice_case, akita_committed_muldiv_case, akita_muldiv_case, + }; + + #[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 bytecode and program image commitments", + |preprocessing| { + let committed = checks::committed_mut(preprocessing); + std::mem::swap( + &mut committed.bytecode_commitment, + &mut committed.program_image_commitment, + ); + }, + )], + ); + } +} + +#[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..9ebabee9ac --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/checks.rs @@ -0,0 +1,487 @@ +//! 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, +}; +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). +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::Akita, + CommitmentConfig::Akita => 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 = VerifierTranscript::::new( + &jolt_verifier::jolt_protocol_id::(), + 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(), + ); + } + + let mut preprocessing = case.preprocessing().clone(); + let committed = 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 { .. }) + ), + "a committed program with another trace order must be rejected" + ); +} + +/// Mutable committed-program preprocessing. +pub fn committed_mut( + preprocessing: &mut JoltVerifierPreprocessing, +) -> &mut CommittedProgramPreprocessing +where + VC: 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..08f539239a --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/dory.rs @@ -0,0 +1,104 @@ +//! 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 and program image commitments", + |preprocessing| { + let committed = checks::committed_mut(preprocessing); + std::mem::swap( + &mut committed.bytecode_commitment, + &mut committed.program_image_commitment, + ); + }, + ), + ( + "program image replaced by the bytecode commitment", + |preprocessing| { + let committed = checks::committed_mut(preprocessing); + committed.program_image_commitment = committed.bytecode_commitment.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 e83958c75d..a8b6a69323 100644 --- a/crates/jolt-verifier/tests/soundness/mod.rs +++ b/crates/jolt-verifier/tests/soundness/mod.rs @@ -1 +1,29 @@ -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/tampering/akita.rs b/crates/jolt-verifier/tests/soundness/tampering/akita.rs deleted file mode 100644 index c24664d661..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/akita.rs +++ /dev/null @@ -1,754 +0,0 @@ -//! Fixture-driven tamper suite for the akita path. -//! -//! Three layers, all over real Akita-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::relations::claim_reductions::bytecode::BytecodeReductionIntermediateClaims; -use jolt_claims::protocols::jolt::TracePolynomialOrder; -use jolt_field::{JoltField, Ring}; -use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; -use jolt_verifier::preprocessing::{CommittedProgramPreprocessing, 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; - -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!("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(reduction) = bytecode_reduction { - match reduction { - BytecodeReductionCyclePhaseOutputClaims::Intermediate( - BytecodeReductionIntermediateClaims { intermediate }, - ) => f(intermediate), - BytecodeReductionCyclePhaseOutputClaims::Final( - BytecodeReductionAddressPhaseOutputClaims { bytecode }, - ) => f(bytecode), - } - } - 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 { bytecode }) = bytecode_address_phase { - f(bytecode); - } - 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 -} - -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)); - } -} - -#[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)); -} - -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() { - } 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)), - } - } -} - -#[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"); - }; - let sweep_program = - |original, replace: fn(&mut CommittedProgramPreprocessing, _)| { - 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"); - }; - replace(program, commitment); - jolt_verifier::verify::( - &preprocessing, - &committed.public_io, - &committed.proof, - None, - ) - }); - }; - sweep_program(&program.bytecode_commitment, |program, commitment| { - program.bytecode_commitment = commitment; - }); - sweep_program(&program.program_image_commitment, |program, commitment| { - program.program_image_commitment = commitment; - }); -} - -#[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 proof = committed.proof.clone(); - let reduction = &mut clear_claims_mut(&mut proof).stage6b.bytecode_reduction; - let Some(BytecodeReductionCyclePhaseOutputClaims::Intermediate(claim)) = reduction else { - panic!("the committed fixture must require an address phase"); - }; - *reduction = Some(BytecodeReductionCyclePhaseOutputClaims::Final( - BytecodeReductionAddressPhaseOutputClaims { - bytecode: claim.intermediate, - }, - )); - assert_rejects(committed.verify_proof(&proof)); - - let mut preprocessing = committed.preprocessing.clone(); - let ProgramPreprocessing::Committed(program) = &mut preprocessing.program else { - panic!("committed fixture must carry committed preprocessing"); - }; - std::mem::swap( - &mut program.bytecode_commitment, - &mut program.program_image_commitment, - ); - 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 575c9eb820..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/commitments.rs +++ /dev/null @@ -1,111 +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| { - 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()); - }, - ); -} 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 19ae016493..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/field_inline.rs +++ /dev/null @@ -1,158 +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 - } - - 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 53fc980606..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/manifest.rs +++ /dev/null @@ -1,77 +0,0 @@ -#[cfg(not(feature = "akita"))] -use std::path::Path; - -#[cfg(not(feature = "akita"))] -use crate::support::tamper_manifest::{all_targets, TamperCoverage}; -use crate::support::tamper_manifest::{clear_claim_leaf_paths, manifest_paths, proof_field_paths}; - -/// 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:?}" - ); -} 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 60a5c9d168..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/mod.rs +++ /dev/null @@ -1,42 +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 Akita 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(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 2d0ba85a6c..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/openings.rs +++ /dev/null @@ -1,71 +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().is_some(), - ) - .into_iter() - .map(final_opening_id) - .collect() -} 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 4bfc0d72fb..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/preamble.rs +++ /dev/null @@ -1,185 +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 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; - }, - ); -} 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 fad6a3a914..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/proof_shape.rs +++ /dev/null @@ -1,114 +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 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(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(), - } -} - -#[cfg(feature = "prover-fixtures")] -#[test] -fn committed_program_rejects_mismatched_trace_order() { - use jolt_claims::protocols::jolt::TracePolynomialOrder; - use jolt_verifier::VerifierError; - - #[cfg(not(feature = "zk"))] - let mut case = crate::support::verifier_fixtures::standard_committed_muldiv_case(); - #[cfg(feature = "zk")] - let mut case = crate::support::verifier_fixtures::zk_committed_muldiv_case(); - case.proof.trace_polynomial_order = TracePolynomialOrder::AddressMajor; - assert!(matches!( - case.verify(), - Err(VerifierError::InvalidCommittedProgram { .. }) - )); -} 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 369a62a09f..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/sumcheck.rs +++ /dev/null @@ -1,1249 +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); - } -} - -/// 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")); - - 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 bytecode tamper targets" - ); - for id in bytecode_reduction::cycle_phase_output_openings(dimensions) { - 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")); - - 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" - ); - offset_claim_rejects( - &base, - "stage7.claims.bytecode_address_phase.bytecode", - bytecode_reduction::final_program_bytecode_opening(), - ); - 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, - 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 1ce35fdb70..0000000000 --- a/crates/jolt-verifier/tests/soundness/tampering/zk.rs +++ /dev/null @@ -1,334 +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(); -} diff --git a/crates/jolt-verifier/tests/soundness/zk.rs b/crates/jolt-verifier/tests/soundness/zk.rs new file mode 100644 index 0000000000..fabda96a1d --- /dev/null +++ b/crates/jolt-verifier/tests/soundness/zk.rs @@ -0,0 +1,110 @@ +//! 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 and program image commitments", + |preprocessing| { + let committed = checks::committed_mut(preprocessing); + std::mem::swap( + &mut committed.bytecode_commitment, + &mut committed.program_image_commitment, + ); + }, + )], + ); +} + +/// 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 24651614a0..263486227a 100644 --- a/crates/jolt-verifier/tests/statistical_independence/zk.rs +++ b/crates/jolt-verifier/tests/statistical_independence/zk.rs @@ -1,697 +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" + repeated.is_empty(), + "hiding messages identical across fresh proofs: {repeated:?}" ); -} -#[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, - ); - 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, + chi2 < CHI2_CRITICAL, + "cached and fresh low-byte distributions differ: chi2 {chi2:.2}" ); - 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, - ); - 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() } diff --git a/crates/jolt-verifier/tests/support/akita_fixtures.rs b/crates/jolt-verifier/tests/support/akita_fixtures.rs index da49a99b5d..bcde1a28c2 100644 --- a/crates/jolt-verifier/tests/support/akita_fixtures.rs +++ b/crates/jolt-verifier/tests/support/akita_fixtures.rs @@ -9,22 +9,20 @@ use common::jolt_device::JoltDevice; use jolt_akita::{AkitaCommitment, AkitaField, AkitaScheduleArtifacts, AkitaScheme}; use jolt_host::Program; use jolt_prover::akita; -use jolt_prover::akita::preprocessing::{self, AkitaProverPreprocessing, AkitaTranscript, AkitaVc}; +use jolt_prover::akita::preprocessing::{self, AkitaProverPreprocessing, AkitaVc}; use jolt_prover::JoltBackend; 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, } @@ -33,8 +31,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, @@ -144,7 +142,7 @@ fn prove_prepared( preprocessing::commit_trusted_advice(&preprocessing, trusted_advice) .expect("trusted advice commitment") }); - let proof = akita::prove::( + let proof = akita::prove::( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -181,9 +179,10 @@ mod field_inline { use jolt_host::{JoltProgramSource, Program}; use jolt_program::execution::{ExecutionBackend, JoltProgram, TraceInputs, TraceOutput}; use jolt_program::preprocess::JoltProgramPreprocessing; - use jolt_prover::akita::preprocessing::{AkitaTranscript, AkitaVc}; + use jolt_prover::akita::preprocessing::AkitaVc; use jolt_prover::JoltBackend; use jolt_prover::{akita, ProverConfig}; + use jolt_verifier::JoltSponge; use jolt_witness::{JoltVmWitnessConfig, JoltVmWitnessInputs, TraceBackend}; use tracer::execution_backend::TracerBackend; @@ -283,7 +282,7 @@ mod field_inline { .expect("trace witness") .with_field_inline() .expect("field-inline witness view"); - let proof = akita::prove::( + let proof = akita::prove::( JoltBackend::optimized().with_witness(&witness), &prover_preprocessing, &config, diff --git a/crates/jolt-verifier/tests/support/fs_mutations.rs b/crates/jolt-verifier/tests/support/fs_mutations.rs index 4b5e5adae4..8c2fe477f5 100644 --- a/crates/jolt-verifier/tests/support/fs_mutations.rs +++ b/crates/jolt-verifier/tests/support/fs_mutations.rs @@ -1,149 +1,281 @@ +//! 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 jolt_claims::protocols::composed::r1cs::SPARTAN_OUTER_UNISKIP_DOMAIN_SIZE; +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_sumcheck::{CenteredIntegerDomain, ClearProof, SumcheckProof}; -use jolt_verifier::{fs_audit::FsScope, JoltProof, JoltProofClaims}; -use num_traits::One; -use super::fs_transcript::{ChallengeKind, ChallengeTape}; +use jolt_field::{CanonicalBytes, CanonicalDecode, JoltField}; +#[cfg(not(feature = "akita"))] +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, +}; -/// Changes two Dory commitments in the kernel of the frozen final-opening RLC. +use crate::fs_transcript::{decode_challenge, decode_small_challenge, ChallengeTape}; #[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 sites::COMMITMENTS; +use sites::STAGE1; +#[cfg(not(feature = "akita"))] +use sites::STAGE8; + +/// 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, 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, 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, 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..5219c25bcb 100644 --- a/crates/jolt-verifier/tests/support/fs_transcript.rs +++ b/crates/jolt-verifier/tests/support/fs_transcript.rs @@ -1,309 +1,260 @@ +//! 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. +//! +//! 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, 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, +enum Mode { + Record, + Replay, } -/// 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 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) }; + static ACTIVE: 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() - } - } - }) -} +/// The first absorb of every [`jolt_transcript::Fork`]. +const FORK_TAG: &[u8] = b"jolt-transcript/fork/v1"; -/// 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, 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: Option>>, + absorbed: bool, } -impl Default for AuditTranscript { +impl Default for AuditSponge { fn default() -> Self { Self { - inner: T::default(), + inner: Blake2b512::default(), + position: 0, + tape: ACTIVE.with(|active| active.borrow().clone()), + absorbed: false, } } } -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 { + 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 append_bytes(&mut self, bytes: &[u8]) { - self.inner.append_bytes(bytes); + 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 = tape.lock().expect("audit tape lock poisoned"); + match tape.mode { + Mode::Record => { + let _ = self.inner.squeeze(output); + assert_eq!( + start, + tape.bytes.len(), + "a second transcript sponge squeezed into one session's tape" + ); + tape.bytes.extend_from_slice(output); + } + 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; + const ABSORB_FORGETS_SQUEEZE_WIDTH: bool = ::ABSORB_FORGETS_SQUEEZE_WIDTH; +} - // 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/crates/jolt-verifier/tests/support/mod.rs b/crates/jolt-verifier/tests/support/mod.rs index 1bfe09787a..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:?}"); @@ -54,18 +39,12 @@ 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")] -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..4bb2d5e462 --- /dev/null +++ b/crates/jolt-verifier/tests/support/narg.rs @@ -0,0 +1,841 @@ +//! 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}; + #[cfg(not(feature = "zk"))] + use crate::support::verifier_fixtures::VerifierFixtureCase; + #[cfg(feature = "zk")] + use crate::support::verifier_fixtures::ZkVerifierFixtureCase; + + 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!(VerifierFixtureCase); + #[cfg(feature = "zk")] + impl_traced_case!(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; + use jolt_transcript::VerifierTranscript; + use stage8::Stage8Output; + + 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 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 { + Stage8Output::Clear => Ok(()), + 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 + | TranscriptOp::ForkAbsorb + | TranscriptOp::ForkSqueeze, + None, + ) => {} + ( + TranscriptOp::Public + | TranscriptOp::Challenge + | TranscriptOp::ForkAbsorb + | TranscriptOp::ForkSqueeze, + 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 b8c5a2c85c..0000000000 --- a/crates/jolt-verifier/tests/support/proof_claims.rs +++ /dev/null @@ -1,619 +0,0 @@ -#[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::relations::claim_reductions::bytecode::BytecodeReductionCyclePhaseOutputClaims; -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() { - match reduction { - BytecodeReductionCyclePhaseOutputClaims::Intermediate(claim) - if id == bytecode_reduction::cycle_phase_intermediate_opening() => - { - return Some(&mut claim.intermediate); - } - BytecodeReductionCyclePhaseOutputClaims::Final(claim) - if id == bytecode_reduction::final_program_bytecode_opening() => - { - return Some(&mut claim.bytecode); - } - _ => {} - } - } - 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() { - if id == bytecode_reduction::final_program_bytecode_opening() { - return Some(&mut address_phase.bytecode); - } - } - 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 8092347792..0000000000 --- a/crates/jolt-verifier/tests/support/tamper_manifest.rs +++ /dev/null @@ -1,1888 +0,0 @@ -use std::collections::BTreeSet; - -#[cfg(feature = "field-inline")] -use jolt_claims::protocols::field_inline::FieldInlineRelationId; -use jolt_claims::protocols::jolt::relations::claim_reductions::bytecode::BytecodeReductionIntermediateClaims; -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; -use jolt_verifier::stages::stage6b::outputs::BytecodeReductionCyclePhaseOutputClaims; -#[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.intermediate", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets the cycle-phase intermediate", - ), - checked_standard( - "stage6.claims.bytecode_reduction.bytecode", - "claims.stage6b.bytecode_reduction.Final.bytecode", - VerifierPhase::Stage6, - MutationStrategy::OffsetScalar, - TamperCoverage::IgnoredUntilFixture, - "cycle-only bytecode claims require a fixture without an address phase", - ), - 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.bytecode", - "claims.stage7.bytecode_address_phase.bytecode", - VerifierPhase::Stage7, - MutationStrategy::OffsetScalar, - TamperCoverage::Active, - "committed fixture offsets the whole-bytecode 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 Akita-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 Akita 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 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 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, - } -} - -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( - BytecodeReductionCyclePhaseOutputClaims::Intermediate( - BytecodeReductionIntermediateClaims { intermediate: zero }, - ), - ), - 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 { - bytecode: 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/verifier_fixtures.rs b/crates/jolt-verifier/tests/support/verifier_fixtures.rs index a94ff73b9b..add9c07447 100644 --- a/crates/jolt-verifier/tests/support/verifier_fixtures.rs +++ b/crates/jolt-verifier/tests/support/verifier_fixtures.rs @@ -23,14 +23,13 @@ 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. +// 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"; @@ -114,7 +113,6 @@ fn lock_exclusive(file: &fs::File) { } } -pub type VerifierFixtureProof = JoltProof>; type VerifierFixturePreprocessing = JoltVerifierPreprocessing>; #[cfg(not(feature = "zk"))] @@ -122,17 +120,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(), ) } @@ -143,17 +147,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(), ) } @@ -328,7 +338,7 @@ fn case_from_parts( struct GeneratedVerifierFixture { preprocessing: VerifierFixturePreprocessing, public_io: JoltDevice, - proof: VerifierFixtureProof, + proof: JoltProof, trusted_advice_commitment: Option, } @@ -527,10 +537,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, @@ -725,7 +735,7 @@ fn prove_prepared( jolt_prover::dory::commit_trusted_advice(&preprocessing, trusted_advice) .expect("trusted advice commitment") }); - let proof = jolt_prover::dory::prove::, Blake2bTranscript>( + let proof = jolt_prover::dory::prove::, JoltSponge>( JoltBackend::optimized().with_witness(&witness), &preprocessing, &config, @@ -751,7 +761,7 @@ mod field_inline { use jolt_field::Fr; use jolt_program::execution::{ExecutionBackend, JoltProgram, TraceInputs, TraceOutput}; 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; @@ -829,7 +839,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.with_witness(&witness), &prover_preprocessing, &config, 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 fbc2028fc8..0000000000 --- a/crates/jolt-verifier/tests/support/zk_audit.rs +++ /dev/null @@ -1,160 +0,0 @@ -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)) -} diff --git a/examples/advice-demo/src/main.rs b/examples/advice-demo/src/main.rs index beef31d88f..dcf2b36e6f 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; @@ -23,7 +24,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; verify_advice_demo(n, a, b, output, program_io.panic, proof) .expect("Proof verification failed"); diff --git a/examples/field-ops/src/main.rs b/examples/field-ops/src/main.rs index 909fb193bd..dacf0ae9ce 100644 --- a/examples/field-ops/src/main.rs +++ b/examples/field-ops/src/main.rs @@ -12,7 +12,7 @@ mod pipeline { use jolt::host::JoltProgramSource; use jolt::{ JoltProgramPreprocessing, JoltProverPreprocessing, JoltSharedPreprocessing, MemoryConfig, - TraceInputs, TraceOutput, TracerBackend, VerifierField, VerifierPCS, VerifierTranscript, + ProtocolSponge, TraceInputs, TraceOutput, TracerBackend, VerifierField, VerifierPCS, VerifierVC, }; use jolt_field::{CanonicalBytes, Ring}; @@ -164,7 +164,7 @@ mod pipeline { let prover_preprocessing = preprocessing; let backend = JoltBackend::::reference(); - let proof = jolt_prover::prove::( + let proof = jolt_prover::prove::( backend.with_witness(&witness), &prover_preprocessing, &config, @@ -173,7 +173,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/modinv/src/main.rs b/examples/modinv/src/main.rs index 69de4a19ed..c5f64c5fb9 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; @@ -24,7 +25,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; verify_modinv(a, m, output_advice, io_device_advice.panic, proof_advice) .expect("Proof verification failed"); diff --git a/examples/recursion/guest/src/lib.rs b/examples/recursion/guest/src/lib.rs index 64a79c1e0c..96d5cf94e1 100644 --- a/examples/recursion/guest/src/lib.rs +++ b/examples/recursion/guest/src/lib.rs @@ -70,7 +70,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 bef163fbbd..065b2b0fcc 100644 --- a/examples/recursion/src/main.rs +++ b/examples/recursion/src/main.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; use jolt_sdk::host::Program; use jolt_sdk::ProgramPreprocessingMode; +use jolt_sdk::ProtocolSponge; use jolt_sdk::{ JoltDevice, JoltProverPreprocessing, JoltVerifierPreprocessing, MemoryConfig, RV64IMACProof, }; @@ -386,7 +387,7 @@ fn collect_guest_proofs( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::VerifierTranscript, + ProtocolSponge, >(&guest_verifier_preprocessing, &io_device, &proof, None) .is_ok(); info!(" Verification result: {is_valid}"); @@ -533,7 +534,7 @@ fn run_recursion_proof( jolt_sdk::VerifierField, jolt_sdk::VerifierPCS, jolt_sdk::VerifierVC, - jolt_sdk::VerifierTranscript, + 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/fuzz/Cargo.lock b/jolt-eval/fuzz/Cargo.lock index eacc476e13..d680a7dd9e 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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#b8f0f5129501c42c8a934f4832746d335c8f8ca1" 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", @@ -1208,7 +1135,6 @@ dependencies = [ "jolt-utils", "rand_core 0.6.4", "rayon", - "serde", "thiserror 2.0.18", ] @@ -1246,18 +1172,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]] @@ -1266,7 +1192,7 @@ version = "0.1.0" dependencies = [ "ark-bn254", "ark-ec", - "ark-serialize 0.5.0", + "ark-serialize", "dory-pcs", "jolt-crypto", "jolt-field", @@ -1276,6 +1202,7 @@ dependencies = [ "jolt-transcript", "rayon", "serde", + "spongefish", "tracing", ] @@ -1313,7 +1240,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "spongefish", "tempfile", "tracer", "tracing", @@ -1342,13 +1268,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", ] @@ -1389,7 +1316,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", @@ -1466,9 +1393,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", @@ -1491,7 +1418,6 @@ name = "jolt-poly" version = "0.1.0" dependencies = [ "jolt-field", - "jolt-transcript", "jolt-utils", "num-traits", "rand_core 0.6.4", @@ -1505,7 +1431,7 @@ dependencies = [ name = "jolt-program" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "common", "jolt-riscv", "object", @@ -1552,7 +1478,7 @@ dependencies = [ name = "jolt-riscv" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", + "ark-serialize", "serde", "strum", "thiserror 2.0.18", @@ -1575,7 +1501,6 @@ dependencies = [ "jolt-program", "jolt-prover", "jolt-sdk-macros", - "jolt-transcript", "jolt-verifier", "jolt-witness", "postcard", @@ -1628,13 +1553,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]] @@ -1649,7 +1574,6 @@ dependencies = [ name = "jolt-verifier" version = "0.1.0" dependencies = [ - "ark-serialize 0.5.0", "bincode 2.0.1", "blake2 0.11.0-rc.6", "common", @@ -1761,7 +1685,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", ] @@ -2655,8 +2579,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", @@ -2814,7 +2736,7 @@ name = "tracer" version = "0.2.0" dependencies = [ "addr2line", - "ark-serialize 0.5.0", + "ark-serialize", "clap", "common", "derive_more", diff --git a/jolt-eval/src/guests/mod.rs b/jolt-eval/src/guests/mod.rs index 5844d578fd..5805bb4ff2 100644 --- a/jolt-eval/src/guests/mod.rs +++ b/jolt-eval/src/guests/mod.rs @@ -12,6 +12,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,9 +28,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 = JoltProof; pub type ProverPreprocessing = DoryProverPreprocessing; pub type VerifierPreprocessing = jolt_verifier::JoltVerifierPreprocessing; @@ -64,7 +65,7 @@ pub fn verify( proof: Proof, io_device: &JoltDevice, ) -> Result<(), VerifierError> { - jolt_verifier::verify::( + jolt_verifier::verify::( verifier_pp, io_device, &proof, @@ -95,7 +96,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 dda9070a36..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,6 +273,7 @@ impl Invariant for TranscriptConsistencyInvariant { } } +/// Transcript symmetry invariant for the Blake2b512 sponge. #[jolt_eval_macros::invariant(Test, RedTeam)] #[derive(Default)] pub struct TranscriptConsistencyBlake2bInvariant; @@ -299,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 { @@ -307,6 +301,7 @@ impl Invariant for TranscriptConsistencyBlake2bInvariant { } } +/// Transcript symmetry invariant for the Keccak sponge. #[jolt_eval_macros::invariant(Test, RedTeam)] #[derive(Default)] pub struct TranscriptConsistencyKeccakInvariant; @@ -326,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 { @@ -334,6 +329,7 @@ impl Invariant for TranscriptConsistencyKeccakInvariant { } } +/// Transcript symmetry invariant for the Poseidon sponge. #[jolt_eval_macros::invariant(Test, RedTeam)] #[derive(Default)] pub struct TranscriptConsistencyPoseidonInvariant; @@ -353,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/Cargo.toml b/jolt-sdk/Cargo.toml index 2c30f933b5..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"] -transcript-keccak = ["jolt-transcript/transcript-keccak"] -transcript-blake2b = ["jolt-transcript/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/jolt-sdk/macros/src/lib.rs b/jolt-sdk/macros/src/lib.rs index 0a0f973549..eb42b4922e 100644 --- a/jolt-sdk/macros/src/lib.rs +++ b/jolt-sdk/macros/src/lib.rs @@ -278,7 +278,7 @@ impl MacroBuilder { jolt::VerifierField, jolt::VerifierPCS, jolt::VerifierVC, - jolt::VerifierTranscript, + jolt::ProtocolSponge, >( &__jolt_preprocessing, &__jolt_io_device, @@ -1155,7 +1155,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 f00393ae29..86931534a3 100644 --- a/jolt-sdk/src/host_utils.rs +++ b/jolt-sdk/src/host_utils.rs @@ -15,34 +15,12 @@ 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(any(feature = "host", feature = "guest-verifier"))] +use jolt_verifier::JoltProof as VerifierJoltProof; #[cfg(feature = "host")] use jolt_verifier::ProgramPreprocessing as GenericProgramPreprocessing; #[cfg(feature = "host")] @@ -52,32 +30,11 @@ use tracer::TracerInlineExpansionProvider; #[cfg(feature = "host")] pub use jolt_host as host; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - feature = "transcript-poseidon" -))] -pub type ProtocolTranscript = PoseidonTranscript; -#[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; -#[cfg(all( - any(feature = "host", feature = "guest-verifier"), - not(any( - feature = "transcript-poseidon", - feature = "transcript-keccak", - feature = "transcript-blake2b" - )) -))] -pub type ProtocolTranscript = LegacyBlake2bTranscript; -#[cfg(feature = "host")] -pub type ProofTranscript = ProtocolTranscript; +/// 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, TraceError, TraceInputs, TraceOutput}; #[cfg(feature = "host")] @@ -108,12 +65,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 = VerifierJoltProof; #[cfg(feature = "host")] pub type JoltProof = RV64IMACProof; #[cfg(feature = "host")] @@ -193,12 +148,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 = VerifierJoltProof; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] pub type JoltProof = RV64IMACProof; #[cfg(all(feature = "guest-verifier", not(feature = "host")))] @@ -348,7 +301,7 @@ pub fn prove_program( } }; let backend = jolt_prover::JoltBackend::::optimized(); - let proof = jolt_prover::dory::prove::( + let proof = jolt_prover::dory::prove::( backend.with_witness(&witness), preprocessing, &config, diff --git a/jolt-sdk/tests/verifier_api.rs b/jolt-sdk/tests/verifier_api.rs index 8111799c0b..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::VerifierTranscript, + ProtocolSponge, >(&preprocessing, &device, &proof, None); let duration = start.elapsed(); println!("Verification took: {} ms", duration.as_millis()); diff --git a/scripts/check-no-yolocrypto.sh b/scripts/check-no-yolocrypto.sh new file mode 100755 index 0000000000..79a2e38fa1 --- /dev/null +++ b/scripts/check-no-yolocrypto.sh @@ -0,0 +1,45 @@ +#!/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 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 + +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 +} + +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|^|./|') diff --git a/scripts/ci/fs-soundness.sh b/scripts/ci/fs-soundness.sh index 3b9fe5f04e..e898809296 100755 --- a/scripts/ci/fs-soundness.sh +++ b/scripts/ci/fs-soundness.sh @@ -4,25 +4,24 @@ set -euo pipefail cargo nextest run \ -p jolt-verifier \ --test fs_obligations \ - --features fs-audit \ --cargo-quiet 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)" @@ -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 diff --git a/specs/jolt-transcript-narg.md b/specs/jolt-transcript-narg.md new file mode 100644 index 0000000000..9fa4f134b1 --- /dev/null +++ b/specs/jolt-transcript-narg.md @@ -0,0 +1,426 @@ +# Spec: One NARG transcript for Jolt and Akita + +| Field | Value | +| --------- | ------------------ | +| Author(s) | @markosg04 | +| Created | 2026-10-01 | +| 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 + +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, 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. + +### Ownership + +| Component | Owner crate | Notes | +| --- | --- | --- | +| `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. | +| 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. | + +### 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. `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. +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 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 + 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; +} + +pub struct ProtocolId([u8; PROTOCOL_ID_LEN]); +impl ProtocolId { pub const fn new(name: &str) -> 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, γ, γ², ... +} + +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]); // 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; +} + +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, 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 +} +``` + +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 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(&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 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 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 +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 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 + +> 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 +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`. 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 `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 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 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 + 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 = 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 + +- **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 `{ 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 + 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.** 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. + +## 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`). diff --git a/specs/jolt-transcript-spongefish-state.md b/specs/jolt-transcript-spongefish-state.md new file mode 100644 index 0000000000..08377ceb21 --- /dev/null +++ b/specs/jolt-transcript-spongefish-state.md @@ -0,0 +1,201 @@ +# Spec: run jolt-transcript on spongefish's NARG state + +| Field | Value | +| --------- | --------------------------------------------------------------- | +| Author(s) | @markosg04 | +| Created | 2026-10-05 | +| Status | implemented (in review) | +| 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 and Akita both +run on the result, and Akita opens inside its caller's 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 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. | + +## 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 or Akita 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`. + On a sponge whose absorb erases the squeeze width within an output block + (`Sponge::ABSORB_FORGETS_SQUEEZE_WIDTH`: spongefish's overwrite-mode + Keccak duplex and the byte-level Poseidon sponge), each exact challenge + then absorbs the number of bytes it squeezed as a public `u64`, so distinct + rejection-retry histories cannot reconverge at the next absorb. Blake2b + retains the width and absorbs nothing extra. +4. 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. Every transcript passes an empty instance: +Jolt and Akita 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)` | 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]>()` | +| `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 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, +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 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 +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 + +- 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. 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. A caller's own framing stays its + own, 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, and it keeps the 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 and the grinding transition +differ; the nonce keeps its canonical unsigned LEB128 encoding. Fixtures, +known-answer tests, the FS census, and Akita's grinding tables are +regenerated. Akita's `specs/transcript-grinding.md` is amended in its own PR. + +## Execution + +Two PRs, landed together. Akita pins 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`. + +## 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 an out-of-range candidate and resamples from the next squeeze, and + one that pins each sponge's `ABSORB_FORGETS_SQUEEZE_WIDTH` to its actual + absorb-after-squeeze behavior. +- Grinding: tests for accept, reject, an out-of-range nonce, and a wrong seed. +- `cargo tree -e features` contains no `yolocrypto` in either workspace. + +## Cost + +Squeeze-then-grind costs the verifier more hashing than a preview did. Each +grinding site with nonzero difficulty squeezes a seed from the live state, +absorbs the nonce, builds and squeezes one fork, and then squeezes the +protected challenge; a preview squeezed the predicate and the challenge from +one live squeeze phase. On the spongefish Blake2b sponge that is about five +more compressions per site. Akita's proofs carry several hundred such sites, +which measures at 0.4 ms (about 20%) of single-threaded Akita verification +for an fp64 dense opening at 2^16 to 2^20 coefficients (Akita +`specs/transcript-grinding.md`, Cost). It is the price of keeping +spongefish's duplex state private. Squeezes split into 32-byte blocks, then +8-byte words, then bytes; the split never changes the output stream. + +## Unverified + +- Prover cost of squeeze-then-grind versus the preview transition. Both hash + one block per candidate, so parity is expected; not measured. 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 8e90c7318a..ff276a8f6d 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 diff --git a/src/build_wasm.rs b/src/build_wasm.rs index 386cb33fdc..58f6e75297 100644 --- a/src/build_wasm.rs +++ b/src/build_wasm.rs @@ -175,7 +175,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()) .map_err(JsError::from) }} diff --git a/tracer/src/instruction/field_inline/advice_limb.rs b/tracer/src/instruction/field_inline/advice_limb.rs index b2dbcafe9f..7cb0a22346 100644 --- a/tracer/src/instruction/field_inline/advice_limb.rs +++ b/tracer/src/instruction/field_inline/advice_limb.rs @@ -197,7 +197,7 @@ mod injection { #[cfg(test)] mod tests { use common::constants::RISCV_REGISTER_COUNT; - use jolt_field::{CanonicalBytes, CanonicalEncoding}; + use jolt_field::{CanonicalBytes, CanonicalDecode, CanonicalEncoding}; use jolt_riscv::FIELD_REGISTER_COUNT; use rand::SeedableRng; diff --git a/tracer/src/instruction/field_inline/mod.rs b/tracer/src/instruction/field_inline/mod.rs index 4a810b03df..ad1d61f000 100644 --- a/tracer/src/instruction/field_inline/mod.rs +++ b/tracer/src/instruction/field_inline/mod.rs @@ -92,7 +92,7 @@ fn encode_field(value: F) -> FieldEncodedValue { #[cfg(test)] mod tests { - use jolt_field::{CanonicalBytes, Ring}; + use jolt_field::{CanonicalBytes, CanonicalDecode, Ring}; use super::*; 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 9ac02cf7d0..1f2840d9c1 100644 --- a/zklean-extractor/src/mle_ast.rs +++ b/zklean-extractor/src/mle_ast.rs @@ -14,8 +14,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)] @@ -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,24 +1329,13 @@ 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 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() { return Some(challenge); @@ -1364,6 +1356,31 @@ impl CanonicalEncoding for MleAst { } Some(Self::new_scalar(limbs)) } +} + +impl NargDeserialize for MleAst { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + jolt_field::narg::deserialize(buf) + } +} + +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) {