You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use this FIRST to understand the shape of the diff (gate count mismatch? type mismatch? coefficient-only?).
157
+
158
+
### circuit-r1cs-diff.mjs
159
+
Unpacks Generic gates into individual R1CS constraints in generation order (right half = queued/first, left half = new/second) and finds the first divergence. Essential for diagnosing R1CS double-packing alignment issues where gate types match but coefficients differ.
The output shows the R1CS sequence side-by-side with decoded constraints, highlighting the first divergence. The divergent R1CS's constant coefficient often identifies which operation (seal, constant materialization, boolean check) is misplaced.
166
+
145
167
## Step 5: Iterate on Differences
146
168
147
169
### Extra boolean check constraints
@@ -165,6 +187,18 @@ OCaml may seal (reduce to single variable) inputs before passing to a gate. Chec
165
187
166
188
**Seal at loop boundaries**: OCaml's `scale_fast_unpack` calls `seal base` once at the top, before the VarBaseMul loop. Without this, a complex CVar base point (e.g. from `groupMapCircuit`) gets re-reduced in every round, generating 2 extra GenericPlonk gates per round. PureScript's `varBaseMul` must `sealPoint base` before the loop.
167
189
190
+
**Seal in sponge add_assign**: OCaml's sponge `add_assign` calls `Utils.seal` after every `state[i] += x`. PureScript's circuit sponge (`Snarky.Circuit.RandomOracle.Sponge.absorb`) must do the same. Without seal, complex CVars like `Add(Const 0, Var v)` accumulate in the sponge state and are only reduced during the Poseidon gate's `reduceToVariable` call. This changes the TIMING of R1CS generation: seal produces R1CS during absorption (as standalone `KimchiBasic` constraints), while deferred reduction produces R1CS inside the Poseidon `reduce`. The different timing shifts the R1CS double-packing alignment, causing every subsequent Generic gate's coefficient layout to differ from OCaml even though the same total R1CS exist. This is invisible in standalone sub-circuit tests (which start with an empty packing queue) and only manifests when composing sub-circuits (like the IVP).
191
+
192
+
### R1CS double-packing alignment
193
+
194
+
The Kimchi constraint system pairs consecutive R1CS constraints into a single Generic gate (2 R1CS per gate). The pending queue persists across ALL constraint types — non-Generic gates (Poseidon, CompleteAdd, VarBaseMul, etc.) do NOT flush the queue.
195
+
196
+
This means: if any operation before a composed circuit boundary generates an ODD number of R1CS, all subsequent Generic gate coefficient layouts shift by one position. The circuits have the same gate kinds, same gate counts, same cached constants — but different coefficient pairings within Generic gates.
197
+
198
+
**Diagnosis**: Extract R1CS in generation order by unpacking Generic gates (right half = first/queued, left half = second/new). Compare the two R1CS sequences to find the first divergence index. The diverging R1CS's constant coefficient often identifies which operation (seal, constant materialization, boolean check) is at the wrong position.
199
+
200
+
**Prevention**: When translating OCaml code that modifies state with side effects (like sponge `add_assign`), check whether OCaml calls `seal` or `reduce_to_v` at that point. Missing a seal changes the R1CS generation timing.
201
+
168
202
### Different witness variable ordering
169
203
170
204
If the gate structure matches but wires differ, the issue may be in the order variables are introduced via `exists`.
0 commit comments