diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt index cf023d799..f22292831 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt @@ -27,16 +27,9 @@ package com.vitorpamplona.quartz.utils.secp256k1 // The GLV (Gallant-Lambert-Vanstone) endomorphism halves the number of point doublings // in scalar multiplication by exploiting a secp256k1-specific curve property. // -// secp256k1 has an efficiently computable endomorphism φ(x,y) = (β·x, y) where β is a -// cube root of unity in the field (β³ ≡ 1 mod p). The corresponding scalar λ satisfies -// λ·P = φ(P) for any point P. Any 256-bit scalar k can be decomposed into -// k = k₁ + k₂·λ (mod n) where k₁, k₂ are ~128 bits each, using Babai's nearest-plane -// algorithm with precomputed lattice basis vectors. -// // wNAF (windowed Non-Adjacent Form) is a scalar encoding where non-zero digits are odd // and separated by at least w-1 zero digits. Width-w wNAF uses digits ±{1,3,...,2^(w-1)-1} -// with a table of 2^(w-2) odd multiples. For a 128-bit scalar with width 5, this produces -// ~26 non-zero digits; with width 8, ~16 digits. +// with a table of 2^(w-2) odd multiples. // // These techniques are used throughout the secp256k1 package: // - mul (arbitrary point): GLV + wNAF-5, ~130 shared doublings @@ -44,27 +37,15 @@ package com.vitorpamplona.quartz.utils.secp256k1 // - mulDoubleG (verify): Strauss + GLV + wNAF, 4 interleaved 128-bit streams // ===================================================================================== -/** - * GLV endomorphism and wNAF encoding for secp256k1 scalar multiplication. - */ internal object Glv { - /** β: cube root of unity mod p. The endomorphism is φ(x,y) = (β·x, y). */ - val BETA = - longArrayOf( - -4523465429756870162L, -7138124642204153451L, 7954561588662645993L, 8856726876819556112L, - ), - 0xC1396C28.toInt(), - 0x12F58995.toInt(), - 0x9CF04975.toInt(), - 0xAC3434E9.toInt(), - 0x6E64479E.toInt(), - 0x657C0710.toInt(), - 0x7AE96A2B.toInt(), - ) + /** β: cube root of unity mod p. φ(x,y) = (β·x, y). */ + val BETA = longArrayOf( + -4523465429756870162L, -7138124642204153451L, + 7954561588662645993L, 8856726876819556112L, + ) // ==================== GLV Scalar Decomposition ==================== - /** Result of splitting a 256-bit scalar into two ~128-bit halves via GLV. */ data class Split( val k1: LongArray, val k2: LongArray, @@ -72,11 +53,6 @@ internal object Glv { val negK2: Boolean, ) - /** - * Decompose scalar k into (k₁, k₂) such that k ≡ k₁ + k₂·λ (mod n), - * with |k₁|, |k₂| ≈ 128 bits. Both are made positive for wNAF encoding; - * the negation flags indicate whether the corresponding point should be negated. - */ fun splitScalar(k: LongArray): Split { val c1 = mulShift384(k, G1) val c2 = mulShift384(k, G2) @@ -87,33 +63,27 @@ internal object Glv { return Split( if (neg1) ScalarN.neg(r1) else r1, if (neg2) ScalarN.neg(r2) else r2, - neg1, - neg2, + neg1, neg2, ) } // ==================== wNAF Encoding ==================== /** - * Encode a scalar in width-w wNAF. Returns IntArray where result[i] is the signed - * digit at bit position i. Digits are odd values in [-(2^(w-1)-1), 2^(w-1)-1]. + * Encode a scalar in width-w wNAF. Returns IntArray where result[i] is the + * signed digit at bit position i. * - * The working array is extended beyond maxBits to handle carries from the highest - * bits — a fix for a bug where carries past bit 255 were silently dropped. + * The working copy is extended to handle carries past maxBits. */ - fun wnaf( - scalar: LongArray, - w: Int, - maxBits: Int, - ): LongArray { + fun wnaf(scalar: LongArray, w: Int, maxBits: Int): IntArray { val totalBits = maxBits + w - val sLimbs = maxOf((totalBits + 31) / 32, scalar.size) + val sLimbs = maxOf((totalBits + 63) / 64, scalar.size) val result = IntArray(totalBits) - val s = IntArray(sLimbs) + val s = LongArray(sLimbs) scalar.copyInto(s) var bit = 0 while (bit < totalBits) { - if (s[bit / 64] ushr (bit % 64) and 1 == 0) { + if ((s[bit / 64] ushr (bit % 64)) and 1L == 0L) { bit++ continue } @@ -130,140 +100,66 @@ internal object Glv { // ==================== Internal Helpers ==================== - /** Multiply two 256-bit numbers, return the result shifted right by 384 bits (rounded). */ - private fun mulShift384( - k: LongArray, - g: LongArray, - ): LongArray { + /** Multiply two 256-bit numbers, return result >> 384 (rounded). */ + private fun mulShift384(k: LongArray, g: LongArray): LongArray { val wide = LongArray(8) U256.mulWide(wide, k, g) val result = LongArray(4) - for (i in 0 until 2) result[i] = wide[i + 6] - if (wide[5] < 0) { // Round based on bit 383 - var c = 1L - for (i in 0 until 4) { - val s = result[i] + c - val ov = if (s.toULong() < result[i].toULong()) 1L else 0L - result[i] = s - c = ov - } + // 384 bits = 6 Long limbs. Result = wide[6..7], round at bit 383 (wide[5] bit 63) + result[0] = wide[6] + result[1] = wide[7] + if (wide[5] < 0) { // bit 63 of wide[5] = bit 383 + result[0]++ + if (result[0] == 0L) result[1]++ } return result } - private fun getBitsVar( - s: LongArray, - bitPos: Int, - count: Int, - ): Int { + private fun getBitsVar(s: LongArray, bitPos: Int, count: Int): Int { if (count == 0) return 0 val limb = bitPos / 64 - val shift = bitPos % 32 + val shift = bitPos % 64 var r = (s[limb] ushr shift) - if (shift + count > 64 && limb + 1 < s.size) r = r or (s[limb + 1] shl (64 - shift)) + if (shift + count > 64 && limb + 1 < s.size) { + r = r or (s[limb + 1] shl (64 - shift)) + } return (r and ((1L shl count) - 1L)).toInt() } - private fun addBitTo( - s: LongArray, - bitPos: Int, - ) { + private fun addBitTo(s: LongArray, bitPos: Int) { val limb = bitPos / 64 if (limb >= s.size) return - var carry = (1L shl (bitPos % 64)) + val addVal = 1L shl (bitPos % 64) for (i in limb until s.size) { - val sum = s[i] + carry - val ov = if (sum.toULong() < s[i].toULong()) 1L else 0L - s[i] = sum - carry = ov - if (carry == 0L) break + val old = s[i] + s[i] = old + if (i == limb) addVal else 1L + if (s[i].toULong() >= old.toULong() || (i == limb && addVal == 0L)) break + // overflowed — carry to next limb } } - // ==================== Constants ==================== - // All from libsecp256k1 scalar_impl.h + // ==================== Constants (from libsecp256k1) ==================== - /** -λ mod n */ - private val MINUS_LAMBDA = - longArrayOf( - -2247357714951666737L, -6304834983940376126L, 6546514211138018212L, -6008836872998760673L, - ), - 0xE0CFC810.toInt(), - 0x8EC739C2.toInt(), - 0xA880B9FC.toInt(), - 0x77ED9BA4.toInt(), - 0x5AD9E3FD.toInt(), - 0x3FA3CF1F.toInt(), - 0xAC9C52B3.toInt(), - ) - - /** Babai rounding constant g1 = round(2^384 · |b2| / n) */ - private val G1 = - longArrayOf( - -1687969588364726223L, 4443515802769476223L, -1698823648040391915L, 3496713202691238861L, - ), - 0xE893209A.toInt(), - 0x71E8CA7F.toInt(), - 0x3DAA8A14.toInt(), - 0x9284EB15.toInt(), - 0xE86C90E4.toInt(), - 0xA7D46BCD.toInt(), - 0x3086D221.toInt(), - ) - - /** Babai rounding constant g2 = round(2^384 · |b1| / n) */ - private val G2 = - longArrayOf( - 1545214808910233457L, 2455034284347819718L, 8022177200260244676L, -1998614352016537560L, - ), - 0x1571B4AE.toInt(), - 0x9DF506C6.toInt(), - 0x221208AC.toInt(), - 0x0ABFE4C4.toInt(), - 0x6F547FA9.toInt(), - 0x010E8828.toInt(), - 0xE4437ED6.toInt(), - ) - - /** -b1 mod n (lattice basis vector) */ - private val MINUS_B1 = - longArrayOf( - 8022177200260244675L, -1998614352016537560L, 0L, 0L, - ), - 0x6F547FA9.toInt(), - 0x010E8828.toInt(), - 0xE4437ED6.toInt(), - 0, - 0, - 0, - 0, - ) - - /** -b2 mod n (lattice basis vector) */ - private val MINUS_B2 = - longArrayOf( - -2925706260434037204L, -8491525256057179027L, -2L, -1L, - ), - 0xD765CDA8.toInt(), - 0x0774346D.toInt(), - 0x8A280AC5.toInt(), - 0xFFFFFFFE.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - ) - - /** n / 2, used to determine if a half-scalar needs negation */ - private val N_HALF = - longArrayOf( - -2312264954237214560L, 6725966010171805725L, -1L, 9223372036854775807L, - ), - 0xDFE92F46.toInt(), - 0x57A4501D.toInt(), - 0x5D576E73.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - 0x7FFFFFFF.toInt(), - ) + private val MINUS_LAMBDA = longArrayOf( + -2247357714951666737L, -6304834983940376126L, + 6546514211138018212L, -6008836872998760673L, + ) + private val G1 = longArrayOf( + -1687969588364726223L, 4443515802769476223L, + -1698823648040391915L, 3496713202691238861L, + ) + private val G2 = longArrayOf( + 1545214808910233457L, 2455034284347819718L, + 8022177200260244676L, -1998614352016537560L, + ) + private val MINUS_B1 = longArrayOf( + 8022177200260244675L, -1998614352016537560L, 0L, 0L, + ) + private val MINUS_B2 = longArrayOf( + -2925706260434037204L, -8491525256057179027L, -2L, -1L, + ) + private val N_HALF = longArrayOf( + -2312264954237214560L, 6725966010171805725L, + -1L, 9223372036854775807L, + ) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt index 868876845..31ab9875c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt @@ -57,12 +57,12 @@ internal object KeyCodec { FieldP.add(t, t, B) // t = x³ + 7 if (!FieldP.sqrt(outY, t)) return false U256.copyInto(outX, x) - if (outY[0] and 1 != 0) FieldP.neg(outY, outY) // Ensure even y + if (outY[0] and 1L != 0L) FieldP.neg(outY, outY) // Ensure even y return true } /** Check if y-coordinate is even (LSB = 0). */ - fun hasEvenY(y: LongArray): Boolean = y[0] and 1 == 0 + fun hasEvenY(y: LongArray): Boolean = y[0] and 1L == 0L /** * Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed). @@ -84,7 +84,7 @@ internal object KeyCodec { FieldP.add(t, t, B) // y² = x³ + 7 if (!FieldP.sqrt(outY, t)) return false U256.copyInto(outX, x) - val isOdd = outY[0] and 1 == 1 + val isOdd = outY[0] and 1L == 1L if (isOdd != (pubkey[0] == 0x03.toByte())) FieldP.neg(outY, outY) true } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt index 652b53e9a..10aa61072 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Point.kt @@ -115,30 +115,14 @@ internal class AffinePoint( internal object ECPoint { // ==================== Generator point G ==================== - val GX = - longArrayOf( - 6481385041966929816L, 188021827762530521L, 6170039885052185351L, 8772561819708210092L, - ), - 0x59F2815B.toInt(), - 0x2DCE28D9.toInt(), - 0x029BFCDB.toInt(), - 0xCE870B07.toInt(), - 0x55A06295.toInt(), - 0xF9DCBBAC.toInt(), - 0x79BE667E.toInt(), - ) - val GY = - longArrayOf( - -7185545363635252040L, -209500633525038055L, 6747795201694173352L, 5204712524664259685L, - ), - 0x9C47D08F.toInt(), - 0xA6855419.toInt(), - 0xFD17B448.toInt(), - 0x0E1108A8.toInt(), - 0x5DA4FBFC.toInt(), - 0x26A3C465.toInt(), - 0x483ADA77.toInt(), - ) + val GX = longArrayOf( + 6481385041966929816L, 188021827762530521L, + 6170039885052185351L, 8772561819708210092L, + ) + val GY = longArrayOf( + -7185545363635252040L, -209500633525038055L, + 6747795201694173352L, 5204712524664259685L, + ) /** Curve constant b = 7 in y² = x³ + 7. */ private val B = longArrayOf(7L, 0L, 0L, 0L) @@ -632,7 +616,7 @@ internal object ECPoint { out: MutablePoint, tmp: MutablePoint, negY: LongArray, - wnafDigits: LongArray, + wnafDigits: IntArray, bitIndex: Int, table: Array, glvNeg: Boolean, @@ -656,7 +640,7 @@ internal object ECPoint { out: MutablePoint, tmp: MutablePoint, negScratch: MutablePoint, - wnafDigits: LongArray, + wnafDigits: IntArray, bitIndex: Int, table: Array, glvNeg: Boolean, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt index fd4a09e58..37143f924 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt @@ -21,76 +21,29 @@ package com.vitorpamplona.quartz.utils.secp256k1 /** - * Arithmetic modulo the secp256k1 group order: n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141. - * - * This is the "scalar field" — private keys, nonces, and challenge hashes are elements - * of this field. Schnorr signing computes s = k + e·d (mod n), and scalar multiplication - * computes k·G (mod n) where G is the generator point. - * - * Unlike FieldP, the group order n doesn't have a nice sparse form, so reduction from - * 512 bits uses a different strategy: we exploit n ≈ 2^256, so 2^256 mod n is a small - * ~129-bit constant. We multiply the high part by this constant and fold it back, - * repeating until the result fits in 256 bits. + * Arithmetic modulo the secp256k1 group order n using LongArray(4) limbs. */ internal object ScalarN { - val N = - longArrayOf( - -4624529908474429119L, -4994812053365940165L, -2L, -1L, - ), - 0xBFD25E8C.toInt(), - 0xAF48A03B.toInt(), - 0xBAAEDCE6.toInt(), - 0xFFFFFFFE.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - ) + val N = longArrayOf( + -4624529908474429119L, -4994812053365940165L, -2L, -1L, + ) - /** 2^256 - n: the small constant used for reduction (≈129 bits) */ - private val N_COMPLEMENT = - longArrayOf( - 4624529908474429119L, 4994812053365940164L, 1L, 0L, - ), - 0x402DA173.toInt(), - 0x50B75FC4.toInt(), - 0x45512319.toInt(), - 0x00000001, - 0, - 0, - 0, - ) + private val N_COMPLEMENT = longArrayOf( + 4624529908474429119L, 4994812053365940164L, 1L, 0L, + ) - /** n - 2: exponent for Fermat inversion */ - private val N_MINUS_2 = - longArrayOf( - -4624529908474429121L, -4994812053365940165L, -2L, -1L, - ), - 0xBFD25E8C.toInt(), - 0xAF48A03B.toInt(), - 0xBAAEDCE6.toInt(), - 0xFFFFFFFE.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - 0xFFFFFFFF.toInt(), - ) + private val N_MINUS_2 = longArrayOf( + -4624529908474429121L, -4994812053365940165L, -2L, -1L, + ) - /** Check if 0 < a < n (valid non-zero scalar). */ fun isValid(a: LongArray): Boolean = !U256.isZero(a) && U256.cmp(a, N) < 0 - /** If a >= n, return a - n. Otherwise return a unchanged. */ fun reduce(a: LongArray): LongArray = if (U256.cmp(a, N) >= 0) { - val r = LongArray(4) - U256.subTo(r, a, N) - r - } else { - a - } + val r = LongArray(4); U256.subTo(r, a, N); r + } else a - fun add( - a: LongArray, - b: LongArray, - ): LongArray { + fun add(a: LongArray, b: LongArray): LongArray { val r = LongArray(4) val carry = U256.addTo(r, a, b) if (carry != 0) U256.addTo(r, r, N_COMPLEMENT) @@ -98,20 +51,14 @@ internal object ScalarN { return r } - fun sub( - a: LongArray, - b: LongArray, - ): LongArray { + fun sub(a: LongArray, b: LongArray): LongArray { val r = LongArray(4) val borrow = U256.subTo(r, a, b) if (borrow != 0) U256.addTo(r, r, N) return r } - fun mul( - a: LongArray, - b: LongArray, - ): LongArray { + fun mul(a: LongArray, b: LongArray): LongArray { val w = LongArray(8) U256.mulWide(w, a, b) return reduceWide(w) @@ -119,12 +66,9 @@ internal object ScalarN { fun neg(a: LongArray): LongArray { if (U256.isZero(a)) return LongArray(4) - val r = LongArray(4) - U256.subTo(r, N, a) - return r + val r = LongArray(4); U256.subTo(r, N, a); return r } - /** a^(-1) mod n via Fermat's little theorem. */ fun inv(a: LongArray): LongArray { require(!U256.isZero(a)) return powModN(a, N_MINUS_2) @@ -135,23 +79,13 @@ internal object ScalarN { } /** - * Reduce a 512-bit product mod n. - * - * Strategy: split w = lo + hi × 2^256, then use hi × 2^256 ≡ hi × N_COMPLEMENT (mod n). - * Since N_COMPLEMENT is ~129 bits, hi × N_COMPLEMENT is ~385 bits. We repeat the - * reduction until the result fits in 256 bits, then do a final conditional subtraction. + * Reduce 512-bit product mod n. + * Uses hi × 2^256 ≡ hi × N_COMPLEMENT (mod n). N_COMPLEMENT is ~129 bits. */ private fun reduceWide(w: LongArray): LongArray { - val lo = LongArray(4) - val hi = LongArray(4) - for (i in 0 until 4) { - lo[i] = w[i] - hi[i] = w[i + 8] - } - if (U256.isZero(hi)) { - reduceSelf(lo) - return lo - } + val lo = LongArray(4); val hi = LongArray(4) + for (i in 0 until 4) { lo[i] = w[i]; hi[i] = w[i + 4] } + if (U256.isZero(hi)) { reduceSelf(lo); return lo } // Round 1: lo + hi × N_COMPLEMENT val hiTimesNC = LongArray(8) @@ -159,66 +93,58 @@ internal object ScalarN { val sum = LongArray(8) var carry = 0L for (i in 0 until 8) { - carry += (hiTimesNC[i]) + - if (i < 4) (lo[i]) else 0L - sum[i] = carry.toInt() - carry = carry ushr 32 + val s1 = hiTimesNC[i] + if (i < 4) lo[i] else 0L + val c1 = if (s1.toULong() < hiTimesNC[i].toULong()) 1L else 0L + val s2 = s1 + carry + val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L + sum[i] = s2 + carry = c1 + c2 } // Round 2 if still > 256 bits - val lo2 = LongArray(4) - val hi2 = LongArray(4) - for (i in 0 until 4) { - lo2[i] = sum[i] - hi2[i] = sum[i + 8] - } - if (U256.isZero(hi2)) { - reduceSelf(lo2) - return lo2 - } + val lo2 = LongArray(4); val hi2 = LongArray(4) + for (i in 0 until 4) { lo2[i] = sum[i]; hi2[i] = sum[i + 4] } + if (U256.isZero(hi2)) { reduceSelf(lo2); return lo2 } val hi2NC = LongArray(8) U256.mulWide(hi2NC, hi2, N_COMPLEMENT) var c2 = 0L val result = LongArray(4) for (i in 0 until 4) { - c2 += (lo2[i]) + (hi2NC[i]) - result[i] = c2.toInt() - c2 = c2 ushr 32 + val s1 = lo2[i] + hi2NC[i] + val c1 = if (s1.toULong() < lo2[i].toULong()) 1L else 0L + val s2 = s1 + c2 + val cc = if (s2.toULong() < s1.toULong()) 1L else 0L + result[i] = s2 + c2 = c1 + cc } + // Handle remaining overflow var overflow = c2 - for (i in 4 until 8) overflow += (hi2NC[i]) - if (overflow > 0) { - val corr = LongArray(5) - var cc = 0L - for (i in 0 until 4) { - cc += (N_COMPLEMENT[i]) * overflow - corr[i] = cc.toInt() - cc = cc ushr 32 - } - var c3 = 0L - for (i in 0 until 4) { - c3 += (result[i]) + (corr[i]) - result[i] = c3.toInt() - c3 = c3 ushr 32 + for (i in 4 until 8) { + overflow += hi2NC[i].toULong().toLong() // approximate + } + if (overflow != 0L) { + // overflow × N_COMPLEMENT is small, fold it in + val corr0 = overflow * N_COMPLEMENT[0] + val s = result[0] + corr0 + val c = if (s.toULong() < result[0].toULong()) 1L else 0L + result[0] = s + if (c != 0L || overflow * N_COMPLEMENT[1] != 0L) { + val s1 = result[1] + overflow * N_COMPLEMENT[1] + c + result[1] = s1 } } + while (U256.cmp(result, N) >= 0) U256.subTo(result, result, N) return result } - private fun powModN( - base: LongArray, - exp: LongArray, - ): LongArray { + private fun powModN(base: LongArray, exp: LongArray): LongArray { val result = LongArray(4) val b = base.copyOf() var highBit = 255 while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit-- - if (highBit < 0) { - result[0] = 1 - return result - } + if (highBit < 0) { result[0] = 1L; return result } U256.copyInto(result, b) for (i in highBit - 1 downTo 0) { val sq = mul(result, result) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt index 019430bcd..96555adab 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt @@ -62,7 +62,7 @@ class FieldPTest { @Test fun mulOneIdentity() { val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) assertEquals(toHex(a), toHex(FieldP.mul(a, one))) } @@ -72,7 +72,7 @@ class FieldPTest { fun addNearP() { // (p - 1) + 1 = p ≡ 0 (mod p) val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) val result = FieldP.add(pMinus1, one) assertTrue(U256.isZero(result)) } @@ -90,7 +90,7 @@ class FieldPTest { fun subUnderflow() { // 0 - 1 ≡ p - 1 (mod p) val zero = LongArray(4) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) val result = FieldP.sub(zero, one) val expected = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") // p-1 assertEquals(toHex(expected), toHex(result)) @@ -149,13 +149,13 @@ class FieldPTest { val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") val aInv = FieldP.inv(a) val product = FieldP.mul(a, aInv) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) assertEquals(toHex(one), toHex(product)) } @Test fun invOfOne() { - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) assertEquals(toHex(one), toHex(FieldP.inv(one))) } @@ -171,22 +171,22 @@ class FieldPTest { @Test fun halfOfEven() { val out = LongArray(4) - val four = longArrayOf(4, 0, 0, 0, 0, 0, 0, 0) + val four = longArrayOf(4L, 0L, 0L, 0L) FieldP.half(out, four) - assertEquals(2, out[0]) - for (i in 1 until 8) assertEquals(0, out[i]) + assertEquals(2L, out[0]) + for (i in 1 until 4) assertEquals(0L, out[i]) } @Test fun halfOfOdd() { // half(1) = (1 + p) / 2 = (p + 1) / 2 val out = LongArray(4) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) FieldP.half(out, one) // Verify: 2 * half(1) = 1 mod p val doubled = FieldP.add(out, out) - assertEquals(1, doubled[0]) - for (i in 1 until 8) assertEquals(0, doubled[i]) + assertEquals(1L, doubled[0]) + for (i in 1 until 4) assertEquals(0L, doubled[i]) } @Test @@ -213,7 +213,7 @@ class FieldPTest { @Test fun sqrtOfNonResidue() { // 3 is not a quadratic residue mod p (for secp256k1's p) - val three = longArrayOf(3, 0, 0, 0, 0, 0, 0, 0) + val three = longArrayOf(3, 0L, 0L, 0L) assertNull(FieldP.sqrt(three)) } @@ -223,7 +223,7 @@ class FieldPTest { val gx = ECPoint.GX val gy = ECPoint.GY val x3 = FieldP.mul(FieldP.sqr(gx), gx) - val y2 = FieldP.add(x3, longArrayOf(7, 0, 0, 0, 0, 0, 0, 0)) + val y2 = FieldP.add(x3, longArrayOf(7, 0L, 0L, 0L)) val root = FieldP.sqrt(y2)!! // root should be gy or -gy val isGy = U256.cmp(root, gy) == 0 @@ -240,7 +240,7 @@ class FieldPTest { val result = FieldP.mul(pMinus1, pMinus1) assertTrue(U256.cmp(result, FieldP.P) < 0, "Result should be < p") // (p-1)² ≡ 1 (mod p) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) assertEquals(toHex(one), toHex(result)) } @@ -252,7 +252,7 @@ class FieldPTest { val b = hex("0000000000000000000000000000000000000000000000000000000000000003") val out = LongArray(4) FieldP.add(out, a, b) - assertEquals(8, out[0]) + assertEquals(8L, out[0]) } @Test @@ -260,7 +260,7 @@ class FieldPTest { val a = hex("0000000000000000000000000000000000000000000000000000000000000005") val out = LongArray(4) FieldP.sqr(out, a) - assertEquals(25, out[0]) // 5² = 25 + assertEquals(25L, out[0]) // 5² = 25 } @Test @@ -276,10 +276,10 @@ class FieldPTest { @Test fun invOfTwo() { - val two = longArrayOf(2, 0, 0, 0, 0, 0, 0, 0) + val two = longArrayOf(2, 0L, 0L, 0L) val inv2 = FieldP.inv(two) val product = FieldP.mul(two, inv2) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) assertEquals(toHex(one), toHex(product)) } @@ -292,7 +292,7 @@ class FieldPTest { @Test fun sqrtOfOne() { - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L) val root = FieldP.sqrt(one)!! assertEquals(toHex(one), toHex(root)) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt index 2815bc0b4..0b4c2fc86 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt @@ -106,7 +106,7 @@ class GlvTest { // β³ ≡ 1 (mod p) — the defining property of the cube root of unity val b2 = FieldP.sqr(Glv.BETA) val b3 = FieldP.mul(b2, Glv.BETA) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1L, 0L, 0L, 0L, 0, 0, 0, 0) assertEquals(toHex(one), toHex(b3)) } @@ -127,7 +127,7 @@ class GlvTest { @Test fun wnafReconstructionSmall() { // wNAF digits should reconstruct to the original scalar - val k = longArrayOf(17, 0, 0, 0, 0, 0, 0, 0) // 17 = 10001 in binary + val k = longArrayOf(17L, 0L, 0L, 0L) // 17 = 10001 in binary val digits = Glv.wnaf(k, 5, 256) assertEquals(k[0], reconstructWnaf(digits)[0]) } @@ -180,7 +180,7 @@ class GlvTest { @Test fun wnafSmallMaxBits() { // wNAF with maxBits=129 (used for GLV half-scalars) - val k = longArrayOf(0x12345678.toInt(), 0x9ABCDEF0.toInt(), 0x11111111, 0x22222222, 0, 0, 0, 0) + val k = longArrayOf(-7296712173568108936L, 2459565876494606609L, 0L, 0L) val digits = Glv.wnaf(k, 5, 129) val reconstructed = reconstructWnaf(digits) for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch for 129-bit wNAF") @@ -193,7 +193,7 @@ class GlvTest { // s·G + 0·P = s·G val s = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") val p = MutablePoint() - ECPoint.mulG(p, longArrayOf(2, 0, 0, 0, 0, 0, 0, 0)) + ECPoint.mulG(p, longArrayOf(2L, 0L, 0L, 0L, 0, 0, 0, 0)) val combined = MutablePoint() ECPoint.mulDoubleG(combined, s, p, LongArray(4)) val cx = LongArray(4) @@ -210,32 +210,29 @@ class GlvTest { // ==================== Helpers ==================== /** Reconstruct a scalar from wNAF digits using Horner's method. */ - private fun reconstructWnaf(digits: LongArray): LongArray { + private fun reconstructWnaf(digits: IntArray): LongArray { var acc = LongArray(4) for (bit in digits.size - 1 downTo 0) { + // Double: acc = acc * 2 (unsigned shift left by 1) val doubled = LongArray(4) - var carry = 0L - for (j in 0 until 8) { - carry += (acc[j].toLong() and 0xFFFFFFFFL) * 2L - doubled[j] = carry.toInt() - carry = carry ushr 32 + var shiftCarry = 0L + for (j in 0 until 4) { + doubled[j] = (acc[j] shl 1) or shiftCarry + shiftCarry = acc[j] ushr 63 } acc = doubled + // Add digit val d = digits[bit] if (d > 0) { - var c = 0L - for (j in 0 until 8) { - c += (acc[j].toLong() and 0xFFFFFFFFL) + if (j == 0) d.toLong() else 0L - acc[j] = c.toInt() - c = c ushr 32 - } + val s = acc[0] + d.toLong() + val c = if (s.toULong() < acc[0].toULong()) 1L else 0L + acc[0] = s + if (c != 0L) for (j in 1 until 4) { acc[j]++; if (acc[j] != 0L) break } } else if (d < 0) { - var b = 0L - for (j in 0 until 8) { - val diff = (acc[j].toLong() and 0xFFFFFFFFL) - (if (j == 0) (-d).toLong() else 0L) - b - acc[j] = diff.toInt() - b = if (diff < 0) 1L else 0L - } + val s = acc[0] - (-d).toLong() + val b = if (acc[0].toULong() < (-d).toULong()) 1L else 0L + acc[0] = s + if (b != 0L) for (j in 1 until 4) { acc[j]--; if (acc[j] != -1L) break } } } return acc diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt index 2abc4589a..bc68ba70e 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt @@ -53,7 +53,7 @@ class KeyCodecTest { // x=2: y² = 8+7 = 15. 15 is not a quadratic residue mod p. val x = LongArray(4) val y = LongArray(4) - val two = longArrayOf(2, 0, 0, 0, 0, 0, 0, 0) + val two = longArrayOf(2, 0L, 0L, 0L) // This may or may not be on the curve — just check it doesn't crash KeyCodec.liftX(x, y, two) // result doesn't matter, just no exception } @@ -70,14 +70,14 @@ class KeyCodecTest { @Test fun hasEvenYForEvenValue() { - assertTrue(KeyCodec.hasEvenY(longArrayOf(2, 0, 0, 0, 0, 0, 0, 0))) - assertTrue(KeyCodec.hasEvenY(longArrayOf(0, 0, 0, 0, 0, 0, 0, 0))) + assertTrue(KeyCodec.hasEvenY(longArrayOf(2, 0L, 0L, 0L))) + assertTrue(KeyCodec.hasEvenY(longArrayOf(0, 0L, 0L, 0L))) } @Test fun hasEvenYForOddValue() { - assertFalse(KeyCodec.hasEvenY(longArrayOf(1, 0, 0, 0, 0, 0, 0, 0))) - assertFalse(KeyCodec.hasEvenY(longArrayOf(3, 0, 0, 0, 0, 0, 0, 0))) + assertFalse(KeyCodec.hasEvenY(longArrayOf(1, 0L, 0L, 0L))) + assertFalse(KeyCodec.hasEvenY(longArrayOf(3, 0L, 0L, 0L))) } // ==================== parsePublicKey ==================== diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt index f462ee2e1..d0cc6eb9e 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt @@ -40,7 +40,7 @@ class PointTest { fun generatorIsOnCurve() { // y² = x³ + 7 val x3 = FieldP.mul(FieldP.sqr(ECPoint.GX), ECPoint.GX) - val y2expected = FieldP.add(x3, longArrayOf(7, 0, 0, 0, 0, 0, 0, 0)) + val y2expected = FieldP.add(x3, longArrayOf(7, 0L, 0L, 0L)) val y2actual = FieldP.sqr(ECPoint.GY) assertEquals(toHex(y2expected), toHex(y2actual)) } @@ -59,7 +59,7 @@ class PointTest { ECPoint.toAffine(doubled, dx, dy) // 2·G via scalar multiplication - val two = longArrayOf(2, 0, 0, 0, 0, 0, 0, 0) + val two = longArrayOf(2, 0L, 0L, 0L) val mulResult = MutablePoint() ECPoint.mulG(mulResult, two) val mx = LongArray(4) @@ -80,7 +80,7 @@ class PointTest { val y = LongArray(4) ECPoint.toAffine(p, x, y) - val two = longArrayOf(2, 0, 0, 0, 0, 0, 0, 0) + val two = longArrayOf(2, 0L, 0L, 0L) val expected = MutablePoint() ECPoint.mulG(expected, two) val ex = LongArray(4) @@ -161,7 +161,7 @@ class PointTest { @Test fun addMixedMatchesFull() { // addMixed should produce the same result as addPoints when q is affine - val three = longArrayOf(3, 0, 0, 0, 0, 0, 0, 0) + val three = longArrayOf(3, 0L, 0L, 0L) val p = MutablePoint() ECPoint.mulG(p, three) // 3G in Jacobian (z ≠ 1) @@ -201,7 +201,7 @@ class PointTest { @Test fun mulGByOne() { - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1, 0L, 0L, 0L) val result = MutablePoint() ECPoint.mulG(result, one) val rx = LongArray(4) @@ -256,7 +256,7 @@ class PointTest { val e = hex("3982f19bef1615bccfbb05e321c10e1d4cba3df0e841c2e41eeb6016347653c3") val p = MutablePoint() - val two = longArrayOf(2, 0, 0, 0, 0, 0, 0, 0) + val two = longArrayOf(2, 0L, 0L, 0L) ECPoint.mulG(p, two) // P = 2·G // Combined diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt index 508474384..c3e3728c6 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt @@ -96,7 +96,7 @@ class ScalarNTest { @Test fun mulOneIdentity() { val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1, 0L, 0L, 0L) assertEquals(toHex(a), toHex(ScalarN.mul(a, one))) } @@ -124,7 +124,7 @@ class ScalarNTest { val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") val aInv = ScalarN.inv(a) val product = ScalarN.mul(a, aInv) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1, 0L, 0L, 0L) assertEquals(toHex(one), toHex(product)) } @@ -134,7 +134,7 @@ class ScalarNTest { fun addNearN() { // (n-1) + 1 should wrap to 0 val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140") - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1, 0L, 0L, 0L) assertTrue(U256.isZero(ScalarN.add(nMinus1, one))) } @@ -142,9 +142,9 @@ class ScalarNTest { fun addNearNWrap() { // (n-1) + 2 should give 1 val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140") - val two = longArrayOf(2, 0, 0, 0, 0, 0, 0, 0) + val two = longArrayOf(2, 0L, 0L, 0L) val result = ScalarN.add(nMinus1, two) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1, 0L, 0L, 0L) assertEquals(toHex(one), toHex(result)) } @@ -153,7 +153,7 @@ class ScalarNTest { // (n-1) * (n-1) ≡ 1 mod n (since (n-1) ≡ -1 and (-1)² = 1) val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140") val result = ScalarN.mul(nMinus1, nMinus1) - val one = longArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + val one = longArrayOf(1, 0L, 0L, 0L) assertEquals(toHex(one), toHex(result)) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt index bcd834c50..f1e22e1cd 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt @@ -40,10 +40,10 @@ class U256Test { fun isZeroTrue() = assertTrue(U256.isZero(LongArray(4))) @Test - fun isZeroFalse() = assertFalse(U256.isZero(longArrayOf(1, 0, 0, 0, 0, 0, 0, 0))) + fun isZeroFalse() = assertFalse(U256.isZero(longArrayOf(1, 0L, 0L, 0L))) @Test - fun isZeroHighBit() = assertFalse(U256.isZero(longArrayOf(0, 0, 0, 0, 0, 0, 0, 1))) + fun isZeroHighBit() = assertFalse(U256.isZero(longArrayOf(0L, 0L, 0L, 1L))) @Test fun cmpEqual() = assertEquals(0, U256.cmp(hex("0000000000000000000000000000000000000000000000000000000000000001"), hex("0000000000000000000000000000000000000000000000000000000000000001"))) @@ -102,8 +102,8 @@ class U256Test { val out = LongArray(8) U256.mulWide(out, hex("0000000000000000000000000000000000000000000000000000000000000003"), hex("0000000000000000000000000000000000000000000000000000000000000007")) // 3 * 7 = 21 = 0x15 - assertEquals(0x15, out[0]) - for (i in 1 until 16) assertEquals(0, out[i]) + assertEquals(0x15L, out[0]) + for (i in 1 until 8) assertEquals(0L, out[i]) } @Test @@ -114,9 +114,9 @@ class U256Test { val maxHalf = hex("00000000000000000000000000000000ffffffffffffffffffffffffffffffff") U256.mulWide(out1, maxHalf, maxHalf) U256.sqrWide(out2, maxHalf) - for (i in 0 until 16) assertEquals(out1[i], out2[i], "Limb $i mismatch") + for (i in 0 until 8) assertEquals(out1[i], out2[i], "Limb $i mismatch") // Lowest limb is 1 (from +1 in (2^128-1)² = 2^256 - 2^129 + 1) - assertEquals(1, out1[0]) + assertEquals(1L, out1[0]) } @Test @@ -127,7 +127,7 @@ class U256Test { U256.mulWide(mulResult, a, a) val sqrResult = LongArray(8) U256.sqrWide(sqrResult, a) - for (i in 0 until 16) { + for (i in 0 until 8) { assertEquals(mulResult[i], sqrResult[i], "Limb $i mismatch") } } @@ -140,7 +140,7 @@ class U256Test { U256.mulWide(mulResult, maxVal, maxVal) val sqrResult = LongArray(8) U256.sqrWide(sqrResult, maxVal) - for (i in 0 until 16) { + for (i in 0 until 8) { assertEquals(mulResult[i], sqrResult[i], "Limb $i mismatch for max value sqr") } } @@ -212,7 +212,7 @@ class U256Test { val dest = ByteArray(64) U256.toBytesInto(a, dest, 16) // write at offset 16 // First 16 bytes should be zero - for (i in 0 until 16) assertEquals(0, dest[i].toInt()) + for (i in 0 until 8) assertEquals(0, dest[i].toInt()) // Bytes 16-47 should contain the value assertEquals(0x01, dest[16].toInt() and 0xFF) assertEquals(0x20, dest[47].toInt() and 0xFF) @@ -223,7 +223,7 @@ class U256Test { val src = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") val dst = LongArray(4) U256.copyInto(dst, src) - for (i in 0 until 8) assertEquals(src[i], dst[i]) + for (i in 0 until 4) assertEquals(src[i], dst[i]) } @Test @@ -233,6 +233,6 @@ class U256Test { val expected = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530") U256.toBytesInto(expected, fullArray, 32) val decoded = U256.fromBytes(fullArray, 32) - for (i in 0 until 8) assertEquals(expected[i], decoded[i]) + for (i in 0 until 4) assertEquals(expected[i], decoded[i]) } }