docs: document why lazy fe_sub is NOT safe with 4x64 limbs

Analyzed lazy fe_sub: on underflow, the unsigned-wrapped value
(a - b + 2^256) differs from (a - b + P) by C = 2^32 + 977.
When multiplied: (a-b+2^256)*x ≠ (a-b)*x mod p (off by x*C mod p).
The P-add-back on underflow is mandatory for correctness.

This is a fundamental difference from 5x52 lazy reduction where
magnitude tracking keeps values representable. With 4x64 fully-packed
limbs, sub MUST add P back, but add CAN skip reduceSelf since values
in [P, 2^256) differ from [0, C) which is handled by mulWide+reduceWide.

Also evaluated:
- WINDOW_G 12→14: only 1.2µs savings for 4x memory (128→512KB).
  Not worth it on phones where L1 cache is 128-256KB.
- fe_sqrt optimization: only 831ns overhead over theoretical minimum
  of 5.85µs. The addition chain is already near-optimal.

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
This commit is contained in:
Claude
2026-04-12 01:32:21 +00:00
parent ae77935625
commit 8291987145
@@ -95,6 +95,12 @@ internal object FieldP {
* out = a - b mod p. Specialized add-back for P = [P0, -1, -1, -1]:
* adding -1 to limbs 1-3 with carry=1 is identity, so only the carry=0
* case needs work (subtract 1 with borrow propagation). ~500 calls/verify.
*
* NOTE: unlike fe_add, fe_sub CANNOT be lazy because the unsigned-wrapped
* underflow value (a - b + 2^256) differs from (a - b + P) by C = 2^32+977.
* When multiplied, this C factor produces wrong results:
* (a-b+2^256) * x mod p ≠ (a-b) * x mod p (off by x*C mod p)
* The P-add-back is required to keep values in [0, P).
*/
fun sub(
out: Fe4,
@@ -107,15 +113,11 @@ internal object FieldP {
val s0 = out.l0 + P0
val c0 = if (uLtInline(s0, out.l0)) 1L else 0L
out.l0 = s0
// For limbs 1-3: adding P[i]=-1 with carry c:
// c=1 → result unchanged, carry out=1 (identity propagation)
// c=0 → result = out[i]-1, carry out = (out[i] != 0) ? 1 : 0
// So if c0=1, limbs 1-3 are untouched. If c0=0, subtract 1 with borrow:
if (c0 == 0L) {
if (out.l1 != 0L) {
out.l1--
} else {
out.l1 = -1L // 0-1 wraps
out.l1 = -1L
if (out.l2 != 0L) {
out.l2--
} else {