@@ -94,7 +94,6 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
9494 const auto & q_double = View (in.msm_double ); // is 1 iff we are at an DOUBLE row in Straus algorithm
9595 const auto & q_double_shift = View (in.msm_double_shift );
9696 const auto & msm_size = View (in.msm_size_of_msm );
97- // const auto& msm_size_shift = View(in.msm_size_of_msm_shift);
9897 const auto & pc = View (in.msm_pc );
9998 const auto & pc_shift = View (in.msm_pc_shift );
10099 const auto & count = View (in.msm_count );
@@ -153,7 +152,8 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
153152 * 4. The set of reads into (pc_{count + 3}, round, wnaf_slice_{count + 3}) is constructed when q_add = 1 AND
154153 * q_add4 = 1
155154 *
156- * To ensure that all q_addi values are correctly set we apply consistency checks to q_add1/q_add2/q_add3/q_add4:
155+ * To ensure that all q_addi values are correctly set we apply consistency/continuity checks to
156+ * q_add1/q_add2/q_add3/q_add4:
157157 * 1. If q_add2 = 1, require q_add1 = 1
158158 * 2. If q_add3 = 1, require q_add2 = 1
159159 * 3. If q_add4 = 1, require q_add3 = 1
@@ -171,9 +171,11 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
171171 * other times simply want to propagate values. (consider, e.g., when `q_add2 == 0`.) This method returns two
172172 * Accumulators that represent x/y coord of output. Output is either an addition of inputs (if `selector == 1`), or
173173 * xa/ya (if `selector == 0`). Additionally, we require `lambda = 0` if `selector = 0`. The `collision_relation`
174- * accumulator tracks a subrelation that validates xb != xa. Repeated calls to this method will increase the max
175- * degree of the Accumulator output: degree of x_out, y_out = max degree of x_a/x_b + 1. 4 Iterations will produce
176- * an output degree of 5.
174+ * accumulator tracks a subrelation that validates xb != xa.
175+ * Repeated calls to this method will increase the max degree of the Accumulator output:
176+ * deg(x_out) = 1 + max(deg(xa, xb)), deg(y_out) = max(1 + deg(x_out), 1 + deg(ya))
177+ * in our application, we chain together 4 of these with the pattern in such a way that the final x_out will have
178+ * degree 5 and the final y_out will have degree 6.
177179 */
178180 auto add = [&](auto & xb,
179181 auto & yb,
@@ -183,15 +185,18 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
183185 auto & selector,
184186 auto & relation,
185187 auto & collision_relation) {
186- // computation of lambda is valid: if s == 1, then L == (yb - ya) / (xb - xa)
187- // if s == 0, then L == 0. combining these into a single constraint yields:
188- // s * (L * (xb - xa - 1) - (yb - ya)) + L = 0
188+ // computation of lambda is valid: if q == 1, then L == (yb - ya) / (xb - xa)
189+ // if q == 0, then L == 0. combining these into a single constraint yields:
190+ // q * (L * (xb - xa - 1) - (yb - ya)) + L = 0
189191 relation += selector * (lambda * (xb - xa - 1 ) - (yb - ya)) + lambda;
190192 collision_relation += selector * (xb - xa);
191- // x3 = L.L + (-xb - xa) * q + (1 - q) xa
193+ // x_out = L.L + (-xb - xa) * q + (1 - q) xa
194+ // deg L = 1, deg q = 1, min(deg(xa), deg(xb))≥ 1.
195+ // hence deg(x_out) = 1 + max(deg(xa, xb))
192196 auto x_out = lambda.sqr () + (-xb - xa - xa) * selector + xa;
193197
194- // y3 = L . (xa - x3) - ya * q + (1 - q) ya
198+ // y_out = L . (xa - x_out) - ya * q + (1 - q) ya
199+ // hence deg(y_out) = max(1 + deg(x_out), 1 + deg(ya))
195200 auto y_out = lambda * (xa - x_out) + (-ya - ya) * selector + ya;
196201 return std::array<Accumulator, 2 >{ x_out, y_out };
197202 };
@@ -242,13 +247,13 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
242247 Accumulator x4_collision_relation (0 );
243248 // If `msm_transition == 1`, we have started a new MSM. We need to treat the current value of [Acc] as the point at
244249 // infinity!
245- auto [x_t1, y_t1] = first_add (acc_x, acc_y, x1, y1, lambda1, msm_transition, add_relation, x1_collision_relation);
246- auto [x_t2, y_t2] = add (x2, y2, x_t1, y_t1, lambda2, add2, add_relation, x2_collision_relation);
247- auto [x_t3, y_t3] = add (x3, y3, x_t2, y_t2, lambda3, add3, add_relation, x3_collision_relation);
248- auto [x_t4, y_t4] = add (x4, y4, x_t3, y_t3, lambda4, add4, add_relation, x4_collision_relation);
250+ auto [x_t1, y_t1] =
251+ first_add (acc_x, acc_y, x1, y1, lambda1, msm_transition, add_relation, x1_collision_relation); // [deg 2, deg 3]
252+ auto [x_t2, y_t2] = add (x2, y2, x_t1, y_t1, lambda2, add2, add_relation, x2_collision_relation); // [deg 3, deg 4]
253+ auto [x_t3, y_t3] = add (x3, y3, x_t2, y_t2, lambda3, add3, add_relation, x3_collision_relation); // [deg 4, deg 5]
254+ auto [x_t4, y_t4] = add (x4, y4, x_t3, y_t3, lambda4, add4, add_relation, x4_collision_relation); // [deg 5, deg 6]
249255
250256 // Validate accumulator output matches ADD output if q_add = 1
251- // (this is a degree-6 relation)
252257 std::get<0 >(accumulator) += q_add * (acc_x_shift - x_t4) * scaling_factor;
253258 std::get<1 >(accumulator) += q_add * (acc_y_shift - y_t4) * scaling_factor;
254259 std::get<2 >(accumulator) += q_add * add_relation * scaling_factor;
@@ -280,7 +285,8 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
280285 *
281286 * As with additions, the column q_double describes whether row is a double round. It is Prover-defined.
282287 * The value of `msm_round` can only update when `q_double = 1` and we use this to ensure Prover correctly sets
283- * `q_double`. (see round transition relations further down)
288+ * `q_double`. The reason for this is that `msm_round` witnesses the wNAF digit we are processing, and we only
289+ * perform the four doublings when we are done processing a wNAF digit. See round transition relations further down.
284290 */
285291 Accumulator double_relation (0 );
286292 auto [x_d1, y_d1] = dbl (acc_x, acc_y, lambda1, double_relation);
@@ -299,6 +305,10 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
299305 * If scalar slice == 7, we add into accumulator (point_table[7] maps to -[P])
300306 * If scalar slice == 0, we do not add into accumulator
301307 * i.e. for the skew round we can use the slice values as our "selector" when doing conditional point adds
308+ *
309+ * As with addition and doubling, the column q_skew is prover-defined. It is precisely turned on when the round
310+ * is 32. We implement this constraint slightly differently. For more details, see the round transition relations
311+ * below.
302312 */
303313 Accumulator skew_relation (0 );
304314 static FF inverse_seven = FF (7 ).invert ();
@@ -321,7 +331,6 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
321331 auto [x_s4, y_s4] = add (x4, y4, x_s3, y_s3, lambda4, skew4_select, skew_relation, x4_skew_collision_relation);
322332
323333 // Validate accumulator output matches SKEW output if q_skew = 1
324- // (this is a degree-6 relation)
325334 std::get<3 >(accumulator) += q_skew * (acc_x_shift - x_s4) * scaling_factor;
326335 std::get<4 >(accumulator) += q_skew * (acc_y_shift - y_s4) * scaling_factor;
327336 std::get<5 >(accumulator) += q_skew * skew_relation * scaling_factor;
@@ -333,7 +342,8 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
333342 const auto add_second_point = add2 * q_add + q_skew * skew2_select;
334343 const auto add_third_point = add3 * q_add + q_skew * skew3_select;
335344 const auto add_fourth_point = add4 * q_add + q_skew * skew4_select;
336- // Step 2: construct the delta between x-coordinates for each point add (depending on if row is ADD or SKEW)
345+ // Step 2: construct the difference a.k.a. delta between x-coordinates for each point add (depending on if row is
346+ // ADD or SKEW)
337347 const auto x1_delta = x1_skew_collision_relation * q_skew + x1_collision_relation * q_add;
338348 const auto x2_delta = x2_skew_collision_relation * q_skew + x2_collision_relation * q_add;
339349 const auto x3_delta = x3_skew_collision_relation * q_skew + x3_collision_relation * q_add;
@@ -345,65 +355,83 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
345355 std::get<9 >(accumulator) += (x4_delta * collision_inverse4 - add_fourth_point) * scaling_factor;
346356
347357 // Validate that if q_add = 1 or q_skew = 1, add1 also is 1
348- // TODO(@zac-williamson) Once we have a stable base to work off of, remove q_add1 and replace with q_msm_add +
349- // q_msm_skew (issue #2222)
358+ // Optimize(@zac-williamson): could just get rid of add1 as a column, as it is a linear combination, see issue #2222
350359 std::get<32 >(accumulator) += (add1 - q_add - q_skew) * scaling_factor;
351360
352- // If add_i = 0, slice_i = 0
353361 // When add_i = 0, force slice_i to ALSO be 0
354362 std::get<13 >(accumulator) += (-add1 + 1 ) * slice1 * scaling_factor;
355363 std::get<14 >(accumulator) += (-add2 + 1 ) * slice2 * scaling_factor;
356364 std::get<15 >(accumulator) += (-add3 + 1 ) * slice3 * scaling_factor;
357365 std::get<16 >(accumulator) += (-add4 + 1 ) * slice4 * scaling_factor;
358366
359- // only one of q_skew, q_double, q_add can be nonzero
367+ // SELECTORS ARE MUTUALLY EXCLUSIVE
368+ // at most one of q_skew, q_double, q_add can be nonzero
360369 std::get<17 >(accumulator) += (q_add * q_double + q_add * q_skew + q_double * q_skew) * scaling_factor;
361370
362- // We look up wnaf slices by mapping round + pc -> slice
363- // We use an exact set membership check to validate that
364- // wnafs written in wnaf_relation == wnafs read in msm relation
365- // We use `add1/add2/add3/add4` to flag whether we are performing a wnaf read op
366- // We can set these to be Prover-defined as the set membership check implicitly ensures that the correct reads
367- // have occurred.
368- // if msm_transition = 0, round_shift - round = 0 or 1
369- const auto round_delta = round_shift - round;
371+ // ROUND TRANSITION LOGIC
372+ // `round_transition` describes whether we are transitioning between "rounds" of the MSM according to the Straus
373+ // algorithm. In particular, the `round` corresponds to the wNAF digit we are currently processing.
370374
371- // ROUND TRANSITION LOGIC (when round does not change)
372- // If msm_transition = 0 (next row) then round_delta = 0 or 1
375+ const auto round_delta = round_shift - round;
376+ // If `msm_transition == 0` (next row) then `round_delta` is boolean; the round is internal to a given MSM and
377+ // represents the wNAF digit currently being processed. `round_delta == 0` means that the current and next steps of
378+ // the Straus algorithm are processing the same wNAF digit place.
379+
380+ // `round_transition == 0` if `round_delta == 0` or the next row is an MSM transition.
381+ // if `round_transition != 1`, then `round_transition == round_delta == 1` by the following constraint.
382+ // in particular, `round_transition` is boolean. (`round_delta` is not boolean precisely one step before an MSM
383+ // transition, but that does not concern us.)
373384 const auto round_transition = round_delta * (-msm_transition_shift + 1 );
374385 std::get<18 >(accumulator) += round_transition * (round_delta - 1 ) * scaling_factor;
375386
376- // ROUND TRANSITION LOGIC (when round DOES change)
377- // round_transition describes whether we are transitioning between rounds of an MSM
378- // If round_transition = 1, the next row is either a double (if round != 31) or we are adding skew (if round ==
379- // 31) round_transition * skew * (round - 31) = 0 (if round tx and skew, round == 31) round_transition * (skew +
380- // double - 1) = 0 (if round tx, skew XOR double = 1) i.e. if round tx and round != 31, double = 1
387+ // If `round_transition == 1`, then `round_delta == 1` and `msm_transition_shift == 0`. Therefore, the
388+ // next row in the VM is either a double (if `round != 31`) or skew (if `round == 31`). In either case, the point is
389+ // that we have finished processing a wNAF digit place and need to either perform the doublings to move on to the
390+ // next place _or_ we are at the last place and need to perform the skew computation to finish. These are
391+ // equationally represented as:
392+ // round_transition * skew_shift * (round - 31) = 0 (if round tx and skew, then round == 31);
393+ // round_transition * (skew_shift + double_shift - 1) = 0 (if round tx, then skew XOR double = 1).
394+ // together, these have the following implications: if round tx and round != 31, then double_shift = 1.
395+ // conversely, if round tx and double_shift == 0, then `skew_shift == 1`, which then forces `round == 31`.
396+ //
397+ // NOTE(@notnotraju): we must also constrain q_skew == 1 when round == 32. As far as I can tell, this is not done
398+ // anywhere. Here is the potential bad thing that could happen: we could set q_skew to be 0 after the only place
399+ // where it is forced (at round transition). Or worse, we could alternative q_skew and double in this round 32. As
400+ // far as I can tell, this requires adding an extra column, which bears witness to the fact that the round is 32.
381401 std::get<19 >(accumulator) += round_transition * q_skew_shift * (round - 31 ) * scaling_factor;
382402 std::get<20 >(accumulator) += round_transition * (q_skew_shift + q_double_shift - 1 ) * scaling_factor;
383403
384- // if no double or no skew, round_delta = 0
404+ // if the next is neither double nor skew, and we are not at an msm_transition, then round_delta = 0 and the next
405+ // "row" of our VM is processing the same wNAF digit place.
385406 std::get<21 >(accumulator) += round_transition * (-q_double_shift + 1 ) * (-q_skew_shift + 1 ) * scaling_factor;
386407
387- // if double, next double != 1
388- std::get<22 >(accumulator) += q_double * q_double_shift * scaling_factor;
408+ // if double, next add = 1. As q_double, q_add, and q_skew are mutually exclusive, this suffices to force
409+ // q_double_shift == q_skew_shift == 0.
410+ std::get<22 >(accumulator) += q_double * (-q_add_shift + 1 ) * scaling_factor;
389411
390- // if double, next add = 1
391- std::get<23 >(accumulator) += q_double * (-q_add_shift + 1 ) * scaling_factor;
412+ // UPDATING THE COUNT
392413
393- // updating count
394- // if msm_transition = 0 and round_transition = 0, count_shift = count + add1 + add2 + add3 + add4
395- // todo: we need this?
414+ // if we are changing the "round" (i.e. starting to process a new wNAF digit), the count_shift must be 0.
415+ std::get<23 >(accumulator) += round_delta * count_shift * scaling_factor;
416+ // if msm_transition = 0 and round_transition = 0, then the next "row" of the VM is processing the same wNAF digit.
417+ // this means that the count must increase: count_shift = count + add1 + add2 + add3 + add4
396418 std::get<24 >(accumulator) += (-msm_transition_shift + 1 ) * (-round_delta + 1 ) *
397419 (count_shift - count - add1 - add2 - add3 - add4) * scaling_factor;
398420
421+ // at least one of the following must be true:
422+ // the next step is an MSM transition;
423+ // the next count is zero (meaning we are starting the processing of a new wNAF digit)
424+ // the next step is processing the same wNAF digit (i.e., round_delta == 0)
425+ // (note that at the start of a new MSM, the count is also zero, so the above are not mutually exclusive.)
399426 std::get<25 >(accumulator) +=
400427 is_not_first_row * (-msm_transition_shift + 1 ) * round_delta * count_shift * scaling_factor;
401428
402- // if msm_transition = 1, count_shift = 0
403- std::get<26 >(accumulator) += is_not_first_row * msm_transition_shift * count_shift * scaling_factor;
429+ // if msm_transition = 1, then count = 0 (as we are starting a new MSM and hence a new wNAF
430+ // digit)
431+ std::get<26 >(accumulator) += msm_transition * count * scaling_factor;
404432
405- // if msm_transition = 1, pc = pc_shift + msm_size
406- // `ecc_set_relation` ensures `msm_size` maps to `transcript.msm_count` for the current value of `pc`
433+ // if msm_transition_shift = 1, pc = pc_shift + msm_size
434+ // NB: `ecc_set_relation` ensures `msm_size` maps to `transcript.msm_count` for the current value of `pc`
407435 std::get<27 >(accumulator) += is_not_first_row * msm_transition_shift * (msm_size + pc_shift - pc) * scaling_factor;
408436
409437 // Addition continuity checks
@@ -413,8 +441,7 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
413441 // Case 3: add4 = 1, add3 = 0
414442 // These checks ensure that the current row does not skip points (for both ADD and SKEW ops)
415443 // This is part of a wider set of checks we use to ensure that all point data is used in the assigned
416- // multiscalar multiplication operation.
417- // (and not in a different MSM operation)
444+ // multiscalar multiplication operation (and not in a different MSM operation).
418445 std::get<28 >(accumulator) += add2 * (-add1 + 1 ) * scaling_factor;
419446 std::get<29 >(accumulator) += add3 * (-add2 + 1 ) * scaling_factor;
420447 std::get<30 >(accumulator) += add4 * (-add3 + 1 ) * scaling_factor;
@@ -433,6 +460,13 @@ void ECCVMMSMRelationImpl<FF>::accumulate(ContainerOverSubrelations& accumulator
433460 // when transition occurs, perform set membership lookup on (accumulator / pc / msm_size)
434461 // perform set membership lookups on add_i * (pc / round / slice_i)
435462 // perform lookups on (pc / slice_i / x / y)
463+
464+ // We look up wnaf slices by mapping round + pc -> slice
465+ // We use an exact set membership check to validate that
466+ // wnafs written in wnaf_relation == wnafs read in msm relation
467+ // We use `add1/add2/add3/add4` to flag whether we are performing a wnaf read op
468+ // We can set these to be Prover-defined as the set membership check implicitly ensures that the correct reads
469+ // have occurred.
436470}
437471
438472} // namespace bb
0 commit comments