Skip to content

feat(jolt-witness): add witness crate and modular helpers#1512

Open
markosg04 wants to merge 1 commit into
mainfrom
jolt-v2/jolt-witness
Open

feat(jolt-witness): add witness crate and modular helpers#1512
markosg04 wants to merge 1 commit into
mainfrom
jolt-v2/jolt-witness

Conversation

@markosg04
Copy link
Copy Markdown
Collaborator

@markosg04 markosg04 commented May 8, 2026

Adds the jolt-witness crate and the supporting modular crate APIs needed before landing the Bolt and generated-code stack.

The new witness crate centralizes Bolt-facing witness/oracle helpers for commitment trace inputs, one-hot chunk materialization, sparse stage 4/5 trace data, stage 6 bytecode/read-RAF/booleanity inputs, and small deterministic oracle helpers used by generated tests.

This also moves the primitive modular crates onto the semantic surface expected by Bolt-generated code: a simplified jolt-field trait bundle, transcript byte-length accounting and MockTranscript, reusable polynomial/source helpers, R1CS row-dot helpers, trace extraction over CycleRow, canonical lookup-table ordering/names, and sumcheck verifier helper modules.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 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 8, 2026
@moodlezoup moodlezoup force-pushed the jolt-v2/jolt-hyperkzg branch from e5ea5bb to 76c5b22 Compare May 12, 2026 15:42
@markosg04 markosg04 force-pushed the jolt-v2/jolt-witness branch from 01d21cc to 8818148 Compare May 13, 2026 02:40
@github-actions github-actions Bot added spec Tracking issue for a feature spec implementation PR contains implementation of a spec and removed no-spec PR has no spec file labels May 13, 2026
@markosg04 markosg04 changed the base branch from jolt-v2/jolt-hyperkzg to main May 13, 2026 02:41
@markosg04 markosg04 force-pushed the jolt-v2/jolt-witness branch from 8818148 to c6aa875 Compare May 13, 2026 14:32
@github-actions github-actions Bot added no-spec PR has no spec file and removed spec Tracking issue for a feature spec labels May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Benchmark comparison (crates)

group                           main_run                               pr_run
-----                           --------                               ------
EqPolynomial::evaluations/20    1.06     29.0±0.33ms        ? ?/sec    1.00     27.5±0.17ms        ? ?/sec

@markosg04 markosg04 marked this pull request as ready for review May 13, 2026 14:59
@moodlezoup moodlezoup added the claude-review-request Request a review from Claude Code label May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

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

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.

Reviewed the new jolt-witness crate and the supporting helpers in jolt-poly/jolt-r1cs/jolt-transcript. The migrated GruenSplitEqPolynomial and ExpandingTable look algorithmically faithful to the jolt-core originals. A few items below — the biggest one is that MockTranscript is exposed unconditionally as a public re-export, which is a soundness footgun if anything ever instantiates it outside tests.


Generated by Claude Code

pub use digest::DigestTranscript;
pub use domain::{Label, LabelWithCount, U64Word};
pub use keccak::KeccakTranscript;
pub use mock::MockTranscript;
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.

MockTranscript is exposed as an unconditional pub re-export. Its append_* are no-ops, state() is a fixed seed, and the default new() ignores the label — anything that constructs it instead of Blake2bTranscript/KeccakTranscript silently collapses Fiat-Shamir and produces unsound proofs. Worth gating both mod mock (line 54) and this re-export behind #[cfg(any(test, feature = "test-utils"))] so misuse is impossible at link time.


Generated by Claude Code

Comment on lines +400 to +406
/// Evaluates a `u64`-valued multilinear extension at `point`.
pub fn mle_eval_u64<F: Field>(values: &[u64], point: &[F]) -> F {
EqPolynomial::<F>::evals(point, None)
.iter()
.zip(values)
.map(|(&weight, &value)| weight * F::from_u64(value))
.sum()
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.

.iter().zip(values) silently stops at the shorter side. If a caller passes values whose length isn't exactly 1 << point.len(), the result is a partial MLE with no warning — and stage4_ram_val_init_opening just below relies on the caller matching them. An explicit length check would surface miscalibrations instead of returning a wrong field element.

Suggested change
/// Evaluates a `u64`-valued multilinear extension at `point`.
pub fn mle_eval_u64<F: Field>(values: &[u64], point: &[F]) -> F {
EqPolynomial::<F>::evals(point, None)
.iter()
.zip(values)
.map(|(&weight, &value)| weight * F::from_u64(value))
.sum()
/// Evaluates a `u64`-valued multilinear extension at `point`.
pub fn mle_eval_u64<F: Field>(values: &[u64], point: &[F]) -> F {
assert_eq!(
values.len(),
1 << point.len(),
"mle_eval_u64 expected {} values, got {}",
1 << point.len(),
values.len()
);
EqPolynomial::<F>::evals(point, None)
.iter()
.zip(values)
.map(|(&weight, &value)| weight * F::from_u64(value))
.sum()
}

Generated by Claude Code

Comment on lines +711 to +714
instruction_ra_booleanity: Vec::new(),
bytecode_ra_booleanity: Vec::new(),
ram_ra_booleanity: Vec::new(),
bytecode_ra_read_raf: Vec::new(),
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.

stage6_witness_polynomials leaves instruction_ra_booleanity, bytecode_ra_booleanity, ram_ra_booleanity, and bytecode_ra_read_raf as Vec::new(), but Stage6WitnessPolynomials::slices() happily folds those empties into booleanity_chunks and bytecode_ra_read_raf_chunks. A caller that takes the returned struct and immediately calls slices() gets misleadingly-aligned empty slices with no type-level signal that the witness is half-built. Worth either documenting the two-phase contract on the struct/function or splitting into Stage6WitnessPartial + Stage6WitnessComplete so the type system enforces ordering.


Generated by Claude Code

Comment on lines +317 to +320
} else {
assert!(!e_out.is_empty(), "split eq e_out invariant");
std::process::abort();
}
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.

std::process::abort() here is unreachable in practice (the assert! on the line above panics first) and, if it ever did run, would kill the whole process without unwinding or a backtrace — worse than panic!/unreachable!, and inconsistent with the .expect("E_in_vec is never empty") pattern in jolt-core/src/poly/split_eq_poly.rs.

Suggested change
} else {
assert!(!e_out.is_empty(), "split eq e_out invariant");
std::process::abort();
}
} else {
unreachable!("split eq e_out invariant violated: e_out is empty");
}

Generated by Claude Code

}

fn state(&self) -> &[u8; 32] {
&self.seed
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.

state() returns the original seed regardless of how many challenges have been squeezed, so two mock transcripts that have advanced to different counters still compare equal — which defeats the trait's documented "synchronization-detection" purpose. Either fold counter into the returned digest (e.g. cache H(seed || counter) on each squeeze) or document the limitation explicitly so callers don't trust state() for divergence checks.


Generated by Claude Code

Comment on lines +38 to +39
pub ram_addresses: Vec<Option<u128>>,
pub bytecode_indices: Vec<Option<u128>>,
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.

can't these be u64?

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.

this file is a bit bloated, let's split it up

impl CycleInput {
pub const PADDING: Self = Self {
dense: [0; NUM_DENSE_TRACE_COLUMNS],
one_hot: [Some(0), Some(0), None],
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.

add a comment here

Comment on lines +15 to +18
pub struct CycleInput {
pub dense: [i128; NUM_DENSE_TRACE_COLUMNS],
pub one_hot: [Option<u128>; NUM_ONE_HOT_TRACE_SOURCES],
}
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.

why this shape, as opposed to:

Suggested change
pub struct CycleInput {
pub dense: [i128; NUM_DENSE_TRACE_COLUMNS],
pub one_hot: [Option<u128>; NUM_ONE_HOT_TRACE_SOURCES],
}
pub struct CycleInput {
pub rd_inc: i128,
pub ram_inc: i128,
pub instruction_keys: Option<u128>,
pub ram_addresses: Option<u128>,
pub bytecode_indices: Option<u128>,
}

}

/// Returns a dense trace source by its generated oracle source name.
pub fn dense_cycle_source(cycle_inputs: &[CycleInput], source: &str) -> Vec<i128> {
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.

I'm lacking context here, but why &str as opposed to enum variants for source labels?

Comment on lines +123 to +124
/// Deterministic placeholder data for optional advice oracles in synthetic tests.
pub fn deterministic_oracle_data<F: Field>(oracle: &str, num_vars: usize) -> Vec<F> {
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.

put this behind #[cfg(test)] if it's only used for tests

EqPolynomial::<F>::evals(point, None)
.iter()
.zip(values)
.map(|(&weight, &value)| weight * F::from_u64(value))
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.

Suggested change
.map(|(&weight, &value)| weight * F::from_u64(value))
.map(|(&weight, &value)| weight.mul_u64(value))

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 implementation PR contains implementation of a spec no-spec PR has no spec file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants