From 829198714514cac51d861e18f1fd5df8cc75e562 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 01:32:21 +0000 Subject: [PATCH] docs: document why lazy fe_sub is NOT safe with 4x64 limbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../vitorpamplona/quartz/utils/secp256k1/FieldP.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt index 64212dd5c..0bdbb3ec2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt @@ -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 {