Skip to content

feat(jolt-hyperkzg): add HyperKZG multilinear commitment scheme#1488

Merged
moodlezoup merged 4 commits into
mainfrom
jolt-v2/jolt-hyperkzg
May 12, 2026
Merged

feat(jolt-hyperkzg): add HyperKZG multilinear commitment scheme#1488
moodlezoup merged 4 commits into
mainfrom
jolt-v2/jolt-hyperkzg

Conversation

@0xAndoroid
Copy link
Copy Markdown
Collaborator

Summary

  • Introduces jolt-hyperkzg, a pairing-based multilinear polynomial commitment scheme implementing the jolt-openings trait surface
  • Generic over P: PairingGroup; BN254 is the production instantiation
  • Builds on univariate KZG primitives (commit, evaluate, witness polynomial division) reused internally by the multilinear protocol

Changes

  • src/scheme.rsHyperKZGScheme implementing CommitmentScheme and AdditivelyHomomorphic; prove/verify built on KZG univariate openings
  • src/kzg.rs — univariate KZG primitives used internally
  • src/types.rsHyperKZGCommitment, HyperKZGProof, HyperKZGProverSetup, HyperKZGVerifierSetup
  • src/error.rsHyperKzgError enum
  • tests/commit_open_verify.rs — end-to-end commit/open/verify coverage
  • benches/hyperkzg.rs — perf benchmarks
  • fuzz/ — three fuzz targets: commit_open_verify, tampered_proof, wrong_eval

Testing

  • cargo nextest run -p jolt-hyperkzg
  • Fuzz targets exercise tampering and wrong-eval paths

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Warning

This PR has more than 500 changed lines and does not include a spec.

Large features and architectural changes benefit from a spec-driven workflow.
See CONTRIBUTING.md for details on how to create a spec.

If this PR is a bug fix, refactor, or doesn't warrant a spec, feel free to ignore this message.

@github-actions github-actions Bot added the no-spec PR has no spec file label May 1, 2026
Comment thread crates/jolt-hyperkzg/src/kzg.rs Outdated
Comment on lines +162 to +166
assert_eq!(
wit.len(),
3,
"HyperKZG requires exactly 3 evaluation points"
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical DoS vulnerability: This assertion in the verifier code path will panic if a malicious prover sends a proof with incorrect number of witness commitments. A panic in verification creates a denial-of-service vector.

// Replace assertion with error return:
if wit.len() != 3 {
    return false;
}

The verifier should gracefully reject invalid proofs rather than panic.

Suggested change
assert_eq!(
wit.len(),
3,
"HyperKZG requires exactly 3 evaluation points"
);
if wit.len() != 3 {
return false;
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@0xAndoroid 0xAndoroid force-pushed the jolt-v2/jolt-hyperkzg branch from cae9bc9 to 9cdc512 Compare May 1, 2026 20:59
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Benchmark comparison (crates)

group                           main_run                               pr_run
-----                           --------                               ------
EqPolynomial::evals/17          1.08      2.1±0.02ms        ? ?/sec    1.00  1926.5±35.90µs        ? ?/sec
EqPolynomial::evals/19          1.09      7.9±0.06ms        ? ?/sec    1.00      7.2±0.11ms        ? ?/sec
EqPolynomial::evals/20          1.07     15.9±0.17ms        ? ?/sec    1.00     14.9±0.18ms        ? ?/sec
EqPolynomial::evals/22          1.08     62.4±0.27ms        ? ?/sec    1.00     57.6±0.26ms        ? ?/sec
EqPolynomial::evaluations/14    1.00   612.2±16.04µs        ? ?/sec    1.24   756.8±21.77µs        ? ?/sec
EqPolynomial::evaluations/18    1.10      7.1±0.03ms        ? ?/sec    1.00      6.4±0.05ms        ? ?/sec
EqPolynomial::evaluations/20    1.06     29.3±0.14ms        ? ?/sec    1.00     27.7±0.24ms        ? ?/sec
Fr::from_bytes                  1.06    168.1±1.66ns        ? ?/sec    1.00    158.3±1.50ns        ? ?/sec
Fr::mul_u128                    1.11     24.3±0.20ns        ? ?/sec    1.00     21.9±0.26ns        ? ?/sec
Fr::to_bytes                    1.14     20.1±0.33ns        ? ?/sec    1.00     17.6±0.23ns        ? ?/sec
Polynomial::bind/14             1.24   162.3±17.26µs        ? ?/sec    1.00    131.2±5.94µs        ? ?/sec
Polynomial::bind/18             1.11      2.1±0.02ms        ? ?/sec    1.00  1918.9±11.28µs        ? ?/sec
Polynomial::bind/20             1.10      8.4±0.07ms        ? ?/sec    1.00      7.7±0.17ms        ? ?/sec
Polynomial::evaluate/20         1.13     46.0±0.38ms        ? ?/sec    1.00     40.6±0.33ms        ? ?/sec
append_bytes/Blake2b/256B       1.10    463.1±2.08ns        ? ?/sec    1.00    420.9±3.09ns        ? ?/sec
append_bytes/Blake2b/32B        1.09    169.8±1.55ns        ? ?/sec    1.00    156.2±2.49ns        ? ?/sec
append_bytes/Keccak/256B        1.09      4.1±0.06µs        ? ?/sec    1.00      3.7±0.04µs        ? ?/sec
append_bytes/Keccak/32B         1.08  1404.5±31.72ns        ? ?/sec    1.00  1301.1±32.13ns        ? ?/sec
challenge/Keccak                1.11  1546.7±23.19ns        ? ?/sec    1.00  1398.7±11.17ns        ? ?/sec
g1_add                          1.13    464.7±8.81ns        ? ?/sec    1.00    410.4±4.66ns        ? ?/sec
g1_deserialize_bincode          1.06      9.8±0.16µs        ? ?/sec    1.00      9.3±0.04µs        ? ?/sec
g1_double                       1.11    239.7±1.75ns        ? ?/sec    1.00    216.1±2.19ns        ? ?/sec
g1_msm/1024                     1.10     14.2±0.29ms        ? ?/sec    1.00     12.9±0.11ms        ? ?/sec
g1_msm/16                       1.12    605.3±3.93µs        ? ?/sec    1.00    542.6±7.43µs        ? ?/sec
g1_msm/256                      1.11      4.8±0.03ms        ? ?/sec    1.00      4.3±0.01ms        ? ?/sec
g1_msm/4                        1.12    285.9±2.90µs        ? ?/sec    1.00    256.0±1.40µs        ? ?/sec
g1_scalar_mul                   1.11     72.9±3.60µs        ? ?/sec    1.00     65.4±1.21µs        ? ?/sec
g2_msm/16                       1.05      2.0±0.02ms        ? ?/sec    1.00   1915.0±7.48µs        ? ?/sec
g2_msm/256                      1.13     17.5±0.04ms        ? ?/sec    1.00     15.4±0.04ms        ? ?/sec
g2_msm/4                        1.14    875.7±9.54µs        ? ?/sec    1.00    768.9±6.46µs        ? ?/sec
g2_msm/64                       1.14      6.4±0.11ms        ? ?/sec    1.00      5.6±0.02ms        ? ?/sec
g2_scalar_mul                   1.12    354.8±2.97µs        ? ?/sec    1.00    315.6±3.41µs        ? ?/sec
gt_scalar_mul                   1.00    756.3±5.67µs        ? ?/sec    1.16    877.3±6.52µs        ? ?/sec
multi_pairing/16                1.10      6.3±0.01ms        ? ?/sec    1.00      5.7±0.01ms        ? ?/sec
multi_pairing/2                 1.00  1049.9±27.38µs        ? ?/sec    1.18  1235.7±18.75µs        ? ?/sec
multi_pairing/4                 1.07  1941.6±18.98µs        ? ?/sec    1.00  1816.6±21.09µs        ? ?/sec
multi_pairing/8                 1.10      3.4±0.01ms        ? ?/sec    1.00      3.1±0.01ms        ? ?/sec
pairing                         1.00    788.6±8.52µs        ? ?/sec    1.16    914.4±9.67µs        ? ?/sec
pedersen_commit/1024            1.10     14.3±0.03ms        ? ?/sec    1.00     13.0±0.05ms        ? ?/sec
pedersen_commit/16              1.11    681.3±4.71µs        ? ?/sec    1.00    613.5±8.53µs        ? ?/sec
pedersen_commit/256             1.11      4.8±0.01ms        ? ?/sec    1.00      4.4±0.01ms        ? ?/sec
pedersen_commit/4               1.12    358.1±3.13µs        ? ?/sec    1.00    320.9±3.56µs        ? ?/sec

@0xAndoroid 0xAndoroid force-pushed the jolt-v2/jolt-hyperkzg branch from 9cdc512 to 093cf0e Compare May 5, 2026 19:52
wstran pushed a commit to wstran/jolt that referenced this pull request May 6, 2026
Adds a `fuzz/` sub-workspace to `jolt-sumcheck` with two libFuzzer targets,
matching the pattern already established for `jolt-crypto`, `jolt-field`,
`jolt-poly`, and `jolt-transcript`.

## Targets

* `sumcheck_verifier` — drives `SumcheckVerifier::verify` with attacker-
  controlled `SumcheckClaim` (num_vars 0..=8, degree 1..=6, arbitrary
  claimed_sum) and a fuzzer-chosen sequence of `UnivariatePoly<Fr>`
  round polynomials whose lengths are independently controlled per
  round (0..=degree+1). Asserts the verifier returns `Ok(_)` or a typed
  `SumcheckError` and never panics.

* `batched_sumcheck_verifier` — same panic-guard for
  `BatchedSumcheckVerifier::verify`, exercising the front-loaded
  batching path: variable `num_claims` (including 0, which must
  surface as `EmptyClaims`), per-claim `num_vars` mismatches, and
  `mul_pow_2` scaling.

## CI

* Adds `jolt-sumcheck` to the matrix in `.github/workflows/fuzz-crates.yml`.

## Why

`jolt-sumcheck` is verifier-only and protocol-critical: a panic on a
malformed proof would let a malicious prover crash the verifier rather
than receive a clean rejection. This is the same threat model that
PR a16z#1408 hardened in `jolt-core` (`catch_unwind` around the verifier)
and that `verify_tampered` in `jolt-dory` and the `tampered_proof`
target in `jolt-hyperkzg` (a16z#1488) cover for the PCS layer. The existing
unit tests in `crates/jolt-sumcheck/src/tests.rs` cover honest-prover
soundness; this crate covers the dual property — adversarial-input
robustness — with coverage-guided exploration.

## Local validation

* `cargo +nightly fuzz build` (apple-darwin) — clean
* `cargo +nightly fuzz run sumcheck_verifier -- -max_total_time=15` —
  2.66M runs, no crashes
* `cargo +nightly fuzz run batched_sumcheck_verifier -- -max_total_time=15` —
  3.06M runs, no crashes
* `cargo nextest run -p jolt-sumcheck` — 40/40 pass

The fuzz sub-workspace re-applies the root workspace's
`[patch.crates-io]` for the arkworks fork, mirroring the existing
`jolt-transcript/fuzz` and `jolt-crypto/fuzz` setup, since fuzz
sub-workspaces do not inherit the root workspace patches.
Comment on lines +36 to +43
pub(crate) fn compute_witness_polynomial<F: Field>(f: &[F], u: F) -> Vec<F> {
let d = f.len();
let mut h = vec![F::zero(); d];
for i in (1..d).rev() {
h[i - 1] = f[i] + h[i] * u;
}
h
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The witness polynomial returns a vector of length d but the quotient polynomial h(x) = f(x)/(x-u) should have degree d-2 (i.e., d-1 coefficients). The returned vector includes a trailing zero at h[d-1] that is never set in the loop. While mathematically correct, this causes the MSM at line 111 to include an unnecessary zero coefficient, wasting computation. More critically, if the SRS has exactly d-1 powers, the MSM at line 111 will attempt to access setup.g1_powers[d-1] which would be out of bounds.

pub(crate) fn compute_witness_polynomial<F: Field>(f: &[F], u: F) -> Vec<F> {
    let d = f.len();
    let mut h = vec![F::zero(); d - 1]; // Should be d-1, not d
    for i in (1..d).rev() {
        h[i - 1] = f[i] + h[i] * u;
    }
    h
}
Suggested change
pub(crate) fn compute_witness_polynomial<F: Field>(f: &[F], u: F) -> Vec<F> {
let d = f.len();
let mut h = vec![F::zero(); d];
for i in (1..d).rev() {
h[i - 1] = f[i] + h[i] * u;
}
h
}
pub(crate) fn compute_witness_polynomial<F: Field>(f: &[F], u: F) -> Vec<F> {
let d = f.len();
let mut h = vec![F::zero(); d - 1];
for i in (1..d).rev() {
let h_i = if i < d - 1 { h[i] } else { F::zero() };
h[i - 1] = f[i] + h_i * u;
}
h
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

0xAndoroid pushed a commit that referenced this pull request May 7, 2026
* feat(jolt-sumcheck): add fuzz crate for verifier panic coverage

Adds a `fuzz/` sub-workspace to `jolt-sumcheck` with two libFuzzer targets,
matching the pattern already established for `jolt-crypto`, `jolt-field`,
`jolt-poly`, and `jolt-transcript`.

## Targets

* `sumcheck_verifier` — drives `SumcheckVerifier::verify` with attacker-
  controlled `SumcheckClaim` (num_vars 0..=8, degree 1..=6, arbitrary
  claimed_sum) and a fuzzer-chosen sequence of `UnivariatePoly<Fr>`
  round polynomials whose lengths are independently controlled per
  round (0..=degree+1). Asserts the verifier returns `Ok(_)` or a typed
  `SumcheckError` and never panics.

* `batched_sumcheck_verifier` — same panic-guard for
  `BatchedSumcheckVerifier::verify`, exercising the front-loaded
  batching path: variable `num_claims` (including 0, which must
  surface as `EmptyClaims`), per-claim `num_vars` mismatches, and
  `mul_pow_2` scaling.

## CI

* Adds `jolt-sumcheck` to the matrix in `.github/workflows/fuzz-crates.yml`.

## Why

`jolt-sumcheck` is verifier-only and protocol-critical: a panic on a
malformed proof would let a malicious prover crash the verifier rather
than receive a clean rejection. This is the same threat model that
PR #1408 hardened in `jolt-core` (`catch_unwind` around the verifier)
and that `verify_tampered` in `jolt-dory` and the `tampered_proof`
target in `jolt-hyperkzg` (#1488) cover for the PCS layer. The existing
unit tests in `crates/jolt-sumcheck/src/tests.rs` cover honest-prover
soundness; this crate covers the dual property — adversarial-input
robustness — with coverage-guided exploration.

## Local validation

* `cargo +nightly fuzz build` (apple-darwin) — clean
* `cargo +nightly fuzz run sumcheck_verifier -- -max_total_time=15` —
  2.66M runs, no crashes
* `cargo +nightly fuzz run batched_sumcheck_verifier -- -max_total_time=15` —
  3.06M runs, no crashes
* `cargo nextest run -p jolt-sumcheck` — 40/40 pass

The fuzz sub-workspace re-applies the root workspace's
`[patch.crates-io]` for the arkworks fork, mirroring the existing
`jolt-transcript/fuzz` and `jolt-crypto/fuzz` setup, since fuzz
sub-workspaces do not inherit the root workspace patches.

* fuzz(jolt-sumcheck): add valid_prefix_proof target

Address review feedback on PR #1493 from @0xAndoroid: extend the
verifier panic-guard fuzzing to cover proofs whose first `K` round
polynomials are constructed to satisfy the sum-check invariant, so the
verifier proceeds past round 0 and exercises the Fiat-Shamir transcript
chain end-to-end.

The existing `sumcheck_verifier` and `batched_sumcheck_verifier`
targets feed raw fuzz bytes; most iterations fail the verifier's
round-0 sum check and return `Err` early, leaving every later
`append_to_transcript` / `challenge` / `evaluate` call uncovered.

`valid_prefix_proof` lets the fuzzer pick:
  - `num_vars` (0..=8), `degree` (1..=6), `claimed_sum` (any field elt)
  - `valid_rounds` ∈ [0, num_vars]: how many leading rounds are valid
    by construction

For valid rounds, the target reads `c_0` and `c_2 .. c_d` from the
fuzz stream and derives `c_1` from the sum-check invariant
`s(0) + s(1) = running_sum`, mirroring the standard compressed-unipoly
format. It then mirrors the verifier's `append_to_transcript /
challenge / evaluate` pipeline prover-side so the running sum the
verifier will check against is known. Tail rounds (after `valid_rounds`)
use raw random bytes as before.

Invariants:
  - `verify` must never panic on any fuzzer input.
  - When `valid_rounds == num_vars` the proof is fully valid by
    construction, so `verify` MUST return `Ok(EvaluationClaim)` whose
    `value` equals the prover-side running sum and whose `point`
    length equals `num_vars`.

Local validation:
  - `cargo +nightly fuzz build` clean
  - `cargo +nightly fuzz run valid_prefix_proof -- -max_total_time=15`
    — 742k runs, no crash
  - `cargo +nightly fuzz run sumcheck_verifier -- -max_total_time=8`
    — 1.46M runs, no crash (regression-checked)
  - `cargo +nightly fuzz run batched_sumcheck_verifier -- -max_total_time=8`
    — 1.22M runs, no crash (regression-checked)
  - `cargo nextest run -p jolt-sumcheck` — 42/42 pass
  - `cargo clippy -p jolt-poly --all-targets -- -D warnings` clean

---------

Co-authored-by: wstran <wstran@Wilson-Tran.local>
@0xAndoroid 0xAndoroid force-pushed the jolt-v2/jolt-hyperkzg branch 2 times, most recently from 43a2e35 to bcb566e Compare May 7, 2026 21:19
markosg04 and others added 2 commits May 8, 2026 00:28
Introduces jolt-hyperkzg, a pairing-based multilinear polynomial
commitment scheme implementing the jolt-openings trait surface.
Generic over `P: PairingGroup`; BN254 is the production instantiation.

Contents:
- scheme.rs: HyperKZGScheme implementing CommitmentScheme and
  AdditivelyHomomorphic; prove/verify built on KZG univariate openings
- kzg.rs: univariate KZG primitives (commit, evaluate, witness
  polynomial division) used internally by the multilinear protocol
- types.rs: HyperKZGCommitment, HyperKZGProof, HyperKZGProverSetup,
  HyperKZGVerifierSetup
- error.rs: HyperKzgError enum
@markosg04 markosg04 force-pushed the jolt-v2/jolt-hyperkzg branch from bcb566e to 65f7570 Compare May 8, 2026 04:41
let mut polys = Vec::with_capacity(ell);
polys.push(evals.to_vec());

for i in 0..ell - 1 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer underflow when ell = 0. If a 0-variable polynomial is passed (where point.len() = 0), the expression 0..ell - 1 becomes 0..usize::MAX due to wraparound, causing either a hang or memory exhaustion.

for i in 0..ell.saturating_sub(1) {
    // ...
}

Alternatively, add an assertion at the start of the function:

assert!(ell > 0, "polynomial must have at least 1 variable");
Suggested change
for i in 0..ell - 1 {
for i in 0..ell.saturating_sub(1) {

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@moodlezoup moodlezoup added the claude-review-request Request a review from Claude Code label May 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Claude code review session started: https://claude.ai/code/session_01MF57kXRHWCn4a7oeMifTUc

Copy link
Copy Markdown
Collaborator

@moodlezoup moodlezoup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protocol math (folding consistency + 3-point batched KZG pairing) checks out, and the dimension validation before transcript mutation is good. A few items below — mostly perf in hot paths plus one trapdoor-leakage hardening for setup_from_secret.


Generated by Claude Code

h[i - 1] = f[i] + h[i] * u;
}
h
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h[d-1] is always 0 (the loop writes h[0..=d-2]), but the returned vec has length d and the caller at line 109 MSMs &setup.g1_powers[..h.len()] over the trailing zero. The MSM backend (arkworks msm_bigint) still pays the into_affine/into_bigint conversion for that term. Return a length-d-1 vec and slice the SRS accordingly — three witness commits per open in a hot path.


Generated by Claude Code

Comment on lines +334 to +342
fn combine(commitments: &[Self::Output], scalars: &[Self::Field]) -> Self::Output {
assert_eq!(commitments.len(), scalars.len());
let combined = commitments
.iter()
.zip(scalars.iter())
.map(|(c, s)| c.point.scalar_mul(s))
.fold(P::G1::identity(), |acc, x| acc + x);
HyperKZGCommitment { point: combined }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive scalar_mul + fold here loses to P::G1::msm(points, scalars) for any non-trivial commitments.len(). The rest of the crate (kzg.rs:26, :109, :196) uses msm directly.

Suggested change
fn combine(commitments: &[Self::Output], scalars: &[Self::Field]) -> Self::Output {
assert_eq!(commitments.len(), scalars.len());
let combined = commitments
.iter()
.zip(scalars.iter())
.map(|(c, s)| c.point.scalar_mul(s))
.fold(P::G1::identity(), |acc, x| acc + x);
HyperKZGCommitment { point: combined }
}
fn combine(commitments: &[Self::Output], scalars: &[Self::Field]) -> Self::Output {
assert_eq!(commitments.len(), scalars.len());
let bases: Vec<P::G1> = commitments.iter().map(|c| c.point).collect();
HyperKZGCommitment {
point: P::G1::msm(&bases, scalars),
}
}

Generated by Claude Code

let mut polys = Vec::with_capacity(ell);
polys.push(evals.to_vec());

for i in 0..ell - 1 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for i in 0..ell - 1 underflows usize when ell == 0. The trait-level commit/open API makes this hard to reach in normal use (a 0-var poly has 1 eval), but the public HyperKZGScheme::open(setup, evals, point, ...) is also pub and would crash on an empty point. Either return HyperKZGError::InvalidProof up front (this function's caller already returns Result) or guard with an early if ell == 0 return so a malformed caller doesn't panic.


Generated by Claude Code

Comment on lines +229 to +241
impl<P: PairingGroup> DeriveSetup<HyperKZGProverSetup<P>> for PedersenSetup<P::G1> {
fn derive(source: &HyperKZGProverSetup<P>, capacity: usize) -> Self {
assert!(
source.g1_powers.len() > capacity,
"SRS has {} G1 powers, need at least {} (capacity + 1 for blinding)",
source.g1_powers.len(),
capacity + 1,
);
let message_generators = source.g1_powers[..capacity].to_vec();
let blinding_generator = source.g1_powers[capacity];
PedersenSetup::new(message_generators, blinding_generator)
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using KZG SRS powers as Pedersen generators couples Pedersen binding to the same trapdoor beta that breaks KZG binding — once beta is destroyed both are sound, but you lose defense-in-depth (a typical Pedersen setup uses independent hash-to-curve generators). Worth a sentence in the doc here so an integrator doesn't assume the two schemes have independent security.


Generated by Claude Code

Comment thread crates/jolt-hyperkzg/src/error.rs Outdated
VerificationFailed,

#[error("invalid proof structure: {0}")]
InvalidProof(&'static str),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InvalidProof(&'static str) and the context-free VerificationFailed lose information that would help debugging. The three call sites in scheme.rs:159-178 are semantically distinct (wrong com length, wrong v shape, wrong w length), and VerificationFailed covers both "folding consistency failed at level i" and "pairing check failed" — those have very different implications when a downstream regression triggers them. Consider splitting into typed variants.


Generated by Claude Code

Comment thread crates/jolt-hyperkzg/src/kzg.rs Outdated
Comment on lines +113 to +118
// Absorb witness commitments and derive one more challenge to keep
// prover/verifier transcripts in sync
for wi in &w {
transcript.append(wi);
}
let _: P::ScalarField = transcript.challenge();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming this _ (and the "keep transcripts in sync" framing) obscures that this is the verifier's d_0. The prover doesn't use it, but it must advance the transcript identically to the verifier (kzg.rs:161).

Suggested change
// Absorb witness commitments and derive one more challenge to keep
// prover/verifier transcripts in sync
for wi in &w {
transcript.append(wi);
}
let _: P::ScalarField = transcript.challenge();
// Absorb witness commitments and mirror the verifier's `d_0` challenge
// to keep prover/verifier transcripts in sync.
for wi in &w {
transcript.append(wi);
}
let _d_0: P::ScalarField = transcript.challenge();

Generated by Claude Code

}

let mut tampered = proof.clone();
tampered.v[tamper_row][tamper_col] = tamper_val;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fuzz only perturbs proof.v. A target that tampers proof.com[i] or proof.w[i] (e.g., scalar-multiplying by a fuzzer-supplied value) would exercise the pairing-check side of the verifier, which is currently uncovered.


Generated by Claude Code

@moodlezoup moodlezoup force-pushed the jolt-v2/jolt-hyperkzg branch from cc3a810 to e5ea5bb Compare May 12, 2026 15:33
- Return d-1 elements from compute_witness_polynomial to avoid wasted
  zero-coefficient MSM work in the hot path
- Use MSM in AdditivelyHomomorphic::combine instead of naive scalar_mul+fold
- Guard against ell==0 underflow in HyperKZGScheme::open
- Split HyperKZGError into typed variants (FoldingConsistencyFailed,
  PairingCheckFailed, DegenerateChallenge, dimension-specific errors)
  for better debuggability
- Rename prover transcript challenge to _d_0 to clarify it mirrors the
  verifier's d_0
- Document shared KZG/Pedersen trapdoor in DeriveSetup impl
- Expand fuzz target to tamper proof.com and proof.w fields, exercising
  the pairing-check side of the verifier

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@moodlezoup moodlezoup force-pushed the jolt-v2/jolt-hyperkzg branch from e5ea5bb to 76c5b22 Compare May 12, 2026 15:42
…fields

Replace Vec with [T; 3] for the three evaluation points {r, -r, r²}
throughout the HyperKZG crate. The proof struct now uses [P::G1; 3]
for witness commitments and [Vec<P::ScalarField>; 3] for evaluation
rows, enforcing the length invariant at the type level.

- Serde rejects wrong-length proofs at deserialization
- Removed WrongEvaluationRowCount and WrongWitnessCount error variants
- Removed malformed_witness_commitments test (type system enforces it)
- Switched kzg_open_batch from par_iter to [T;3]::map (3 elements)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@moodlezoup moodlezoup force-pushed the jolt-v2/jolt-hyperkzg branch from fba5e43 to 73a1f47 Compare May 12, 2026 16:02
@moodlezoup moodlezoup merged commit 3c3b5e4 into main May 12, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-review-request Request a review from Claude Code no-spec PR has no spec file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants