Refactor: Consolidate WitnessBuilder metadata to eliminate silent bugs and reduce per-variant boilerplate
Nobody has claimed this yet.
- Dominant language
- Noir
- Stars
- 138
- Forks
- 47
- Avg merge
- 1d 34m
- Merged PRs (30d)
- 6
Description
Problem
The WitnessBuilder enum (~30 variants) requires synchronized changes across 4 files when adding a new variant. This is error-prone and has an active silent-bug vector.
Current state
Adding one WitnessBuilder variant requires edits to:
| File | Method | Fails if forgotten? |
|---|---|---|
common/src/witness/witness_builder.rs |
num_witnesses() |
No — _ => 1 catch-all silently returns wrong count |
common/src/witness/scheduling/dependency.rs |
inputs() |
Yes (compile error) |
common/src/witness/scheduling/dependency.rs |
outputs() |
Yes (compile error) |
common/src/witness/scheduling/remapper.rs |
remap() |
Yes (compile error) |
prover/src/witness/witness_builder.rs |
solve() |
Yes (compile error) |
The _ => 1 catch-all in num_witnesses() (line 481) is the most dangerous: a forgotten arm silently returns 1 for a multi-output builder, corrupting all subsequent witness index allocations. The other 4 sites already force exhaustive matching.
Additionally, outputs() in dependency.rs re-derives num_witnesses() from scratch for every multi-output variant, duplicating the logic.
Proposed Fix (3 phases)
Phase 1: Kill the catch-all (15 min, highest impact)
Replace _ => 1 in num_witnesses() with an explicit enumeration of all single-output variants:
WitnessBuilder::Constant(..)
| WitnessBuilder::Acir(..)
| WitnessBuilder::Sum(..)
| WitnessBuilder::Product(..)
| WitnessBuilder::Inverse(..)
| WitnessBuilder::SafeInverse(..)
| WitnessBuilder::Challenge(..)
| WitnessBuilder::IntegerQuotient(..)
| WitnessBuilder::ModularInverse(..)
| WitnessBuilder::LogUpInverse(..)
| WitnessBuilder::LogUpDenominator(..)
| WitnessBuilder::SpiceMultisetFactor(..)
| WitnessBuilder::ProductLinearOperation(..)
| WitnessBuilder::BinOpLookupDenominator(..)
| WitnessBuilder::CombinedBinOpLookupDenominator(..)
| WitnessBuilder::And(..)
| WitnessBuilder::Xor(..)
| WitnessBuilder::CombinedTableEntryInverse(..)
| WitnessBuilder::SpreadWitness(..)
| WitnessBuilder::SpreadLookupDenominator(..)
| WitnessBuilder::SpreadTableQuotient { .. }
| WitnessBuilder::SelectWitness { .. }
| WitnessBuilder::BooleanOr { .. }
| WitnessBuilder::SumQuotient { .. }
| WitnessBuilder::MultiLimbAddQuotient { .. }
| WitnessBuilder::MultiLimbSubBorrow { .. } => 1,
Now a missing arm is a compile error instead of a silent bug.
Phase 2: Unify outputs() with num_witnesses() (~1 hour)
Add an output_start() helper, then express outputs() as start..start + num_witnesses():
impl WitnessBuilder {
/// Returns the first output index for multi-output builders.
fn output_start(&self) -> Option<usize> {
match self {
WitnessBuilder::EcScalarMulHint { output_start, .. }
| WitnessBuilder::NonNativeEcHint { output_start, .. }
| WitnessBuilder::FakeGLVHint { output_start, .. }
| WitnessBuilder::SignedBitHint { output_start, .. }
| WitnessBuilder::EcDoubleHint { output_start, .. }
| WitnessBuilder::EcAddHint { output_start, .. }
| WitnessBuilder::MultiLimbMulModHint { output_start, .. }
| WitnessBuilder::MultiLimbModularInverse { output_start, .. }
| WitnessBuilder::ChunkDecompose { output_start, .. }
| WitnessBuilder::SpreadBitExtract { output_start, .. } => Some(*output_start),
_ => None,
}
}
}
This eliminates the duplicated num_witnesses logic inside outputs().
Phase 3: Add witness_indices_mut() to simplify remap() (~2 hours)
The remap() match reconstructs every variant field-by-field (~500 lines). A witness_indices_mut() method returning Vec<&mut usize> for all witness-index fields would reduce each variant's remap to a single match arm:
impl WitnessBuilder {
fn witness_indices_mut(&mut self) -> Vec<&mut usize> {
match self {
WitnessBuilder::EcScalarMulHint {
output_start, px_limbs, py_limbs, s_lo, s_hi, ..
} => {
let mut v: Vec<&mut usize> = vec![output_start, s_lo, s_hi];
v.extend(px_limbs.iter_mut());
v.extend(py_limbs.iter_mut());
v
}
// ... other variants: ~3 lines each instead of ~15
}
}
}
// remap() becomes:
fn remap_builder(&self, builder: &mut WitnessBuilder) {
for idx in builder.witness_indices_mut() {
*idx = self.remap(*idx);
}
}
Why not a trait?
WitnessBuilder derives Serialize/Deserialize and is part of the proof scheme wire format (.pkp files). Replacing it with trait objects would require typetag or manual enum-dispatch for serde and break the serialization format. The enum is the right choice given this constraint — it just needs the metadata consolidated.
Impact
- Phase 1: Prevents a class of silent soundness bugs. Zero risk — pure refactor.
- Phase 2: Reduces
dependency.rsoutputs()by ~60 lines and eliminates logic duplication. - Phase 3: Reduces
remapper.rsby ~300 lines and makes adding new variants a 1-file-per-concern change instead of reconstructing the full variant.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with common/src/witness/witness_builder.rs and read num_witnesses(), then compare outputs() in common/src/witness/scheduling/dependency.rs and remap() in common/src/witness/scheduling/remapper.rs. Review prover/src/witness/witness_builder.rs for solve(); done means the metadata logic is consolidated across the three phases, exhaustive matching prevents silent omissions, and existing witness behavior remains correct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cryptography
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100