Problem
During the prove phase, Spartan2's SatisfyingAssignment::enforce() is a no-op — it ignores all constraint-enforcement calls (src/bellpepper/solver.rs:70-78):
fn enforce<A, AR, LA, LB, LC>(&mut self, _: A, _a: LA, _b: LB, _c: LC) {
// Do nothing: we don't care about linear-combination evaluations in this context.
}
Yet in both PrepareCircuit and ShowCircuit, the prove-phase code loads the full R1CS and iterates all constraints through circom_scotia::synthesize just to call that no-op:
// prepare_circuit.rs & show_circuit.rs — current prove-phase path
let witness = self.get_or_generate_witness()?;
match load_r1cs::<Scalar>(&self.r1cs_path()) {
Ok(r1cs) => {
synthesize(cs, r1cs, Some(witness))?; // ← wasteful on native
}
Err(_) => {
synthesize_witness_only(cs, &witness, num_public)?; // ← efficient fallback
}
}
On the native (non-WASM) path, load_r1cs succeeds, so it always takes the Ok(r1cs) branch. This wastes:
- Memory: full R1CS struct held in RAM alongside the proving key
- CPU: millions of
LinearCombination allocations that go nowhere
Affected files
wallet-unit-poc/ecdsa-spartan2/src/circuits/prepare_circuit.rs
wallet-unit-poc/ecdsa-spartan2/src/circuits/show_circuit.rs
Proposed fix
The efficient synthesize_witness_only helper already exists in circuits/mod.rs — it does direct cs.alloc_input / cs.alloc calls. It's currently only used as a WASM/browser fallback. The fix is to always use it during prove:
// Replace the match block with:
let witness = self.get_or_generate_witness()?;
synthesize_witness_only(cs, &witness, num_public)?;
- For
PrepareCircuit: num_public comes from layout.num_public()
- For
ShowCircuit: num_public = 3
R1CS loading should only happen during setup (ShapeCS phase), where Spartan2 actually needs the constraint shape.
Expected impact
- ~100–300 MB peak RSS reduction during prove (no R1CS struct in memory)
- Significant prove-time reduction (no constraint iteration + LinearCombination allocations)
- Setup phase unchanged — no impact on key generation
Reference
This optimization was implemented for the zkmopro/zkID fork in zkmopro/zkID#30, which applies the same pattern to Sha256RsaCircuit (RSA-SHA256 certificate verification).
Problem
During the prove phase, Spartan2's
SatisfyingAssignment::enforce()is a no-op — it ignores all constraint-enforcement calls (src/bellpepper/solver.rs:70-78):Yet in both
PrepareCircuitandShowCircuit, the prove-phase code loads the full R1CS and iterates all constraints throughcircom_scotia::synthesizejust to call that no-op:On the native (non-WASM) path,
load_r1cssucceeds, so it always takes theOk(r1cs)branch. This wastes:LinearCombinationallocations that go nowhereAffected files
wallet-unit-poc/ecdsa-spartan2/src/circuits/prepare_circuit.rswallet-unit-poc/ecdsa-spartan2/src/circuits/show_circuit.rsProposed fix
The efficient
synthesize_witness_onlyhelper already exists incircuits/mod.rs— it does directcs.alloc_input/cs.alloccalls. It's currently only used as a WASM/browser fallback. The fix is to always use it during prove:PrepareCircuit:num_publiccomes fromlayout.num_public()ShowCircuit:num_public = 3R1CS loading should only happen during setup (ShapeCS phase), where Spartan2 actually needs the constraint shape.
Expected impact
Reference
This optimization was implemented for the
zkmopro/zkIDfork in zkmopro/zkID#30, which applies the same pattern toSha256RsaCircuit(RSA-SHA256 certificate verification).