diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Field.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Field.kt index 773007476..59f7d4ce9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Field.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Field.kt @@ -25,13 +25,16 @@ package com.vitorpamplona.quartz.utils.secp256k1 * * Numbers are represented as IntArray(8) in little-endian order where each element * holds 32 bits (treated as unsigned). Element [0] is the least significant limb. + * + * Performance: All hot-path operations write into caller-provided output arrays + * to avoid allocation. Only conversion methods (fromBytes/toBytes) allocate. */ internal object U256 { val ZERO = IntArray(8) fun isZero(a: IntArray): Boolean { - for (i in 0 until 8) if (a[i] != 0) return false - return true + // Merge all limbs to avoid branches + return (a[0] or a[1] or a[2] or a[3] or a[4] or a[5] or a[6] or a[7]) == 0 } /** Compare: returns negative if a < b, 0 if equal, positive if a > b */ @@ -42,87 +45,70 @@ internal object U256 { for (i in 7 downTo 0) { val ai = a[i].toLong() and 0xFFFFFFFFL val bi = b[i].toLong() and 0xFFFFFFFFL - if (ai < bi) return -1 - if (ai > bi) return 1 + if (ai != bi) return if (ai < bi) -1 else 1 } return 0 } - /** a + b, returns (result, carry) where carry is 0 or 1 */ - fun addCarry( + /** a + b -> out, returns carry (0 or 1) */ + fun addTo( + out: IntArray, a: IntArray, b: IntArray, - ): Pair { - val r = IntArray(8) + ): Int { var carry = 0L for (i in 0 until 8) { carry += (a[i].toLong() and 0xFFFFFFFFL) + (b[i].toLong() and 0xFFFFFFFFL) - r[i] = carry.toInt() + out[i] = carry.toInt() carry = carry ushr 32 } - return Pair(r, carry.toInt()) + return carry.toInt() } - /** a - b, returns (result, borrow) where borrow is 0 or 1 */ - fun subBorrow( + /** a - b -> out, returns borrow (0 or 1) */ + fun subTo( + out: IntArray, a: IntArray, b: IntArray, - ): Pair { - val r = IntArray(8) + ): Int { var borrow = 0L for (i in 0 until 8) { val diff = (a[i].toLong() and 0xFFFFFFFFL) - (b[i].toLong() and 0xFFFFFFFFL) - borrow - r[i] = diff.toInt() + out[i] = diff.toInt() borrow = if (diff < 0) 1L else 0L } - return Pair(r, borrow.toInt()) + return borrow.toInt() } - /** Full 256x256 -> 512 bit multiplication. Result is IntArray(16). */ + /** Full 256x256 -> 512 bit multiplication. Result written to out (size 16). */ fun mulWide( + out: IntArray, a: IntArray, b: IntArray, - ): IntArray { - val r = IntArray(16) + ) { + for (i in 0 until 16) out[i] = 0 for (i in 0 until 8) { var carry = 0L val ai = a[i].toLong() and 0xFFFFFFFFL for (j in 0 until 8) { - val prod = ai * (b[j].toLong() and 0xFFFFFFFFL) + (r[i + j].toLong() and 0xFFFFFFFFL) + carry - r[i + j] = prod.toInt() + val prod = ai * (b[j].toLong() and 0xFFFFFFFFL) + (out[i + j].toLong() and 0xFFFFFFFFL) + carry + out[i + j] = prod.toInt() carry = prod ushr 32 } - r[i + 8] = carry.toInt() + out[i + 8] = carry.toInt() } - return r - } - - /** Multiply 256-bit number by a small (fits in Long) constant. Result is IntArray(9). */ - fun mulSmall( - a: IntArray, - b: Long, - ): IntArray { - val r = IntArray(9) - var carry = 0L - for (i in 0 until 8) { - carry += (a[i].toLong() and 0xFFFFFFFFL) * b - r[i] = carry.toInt() - carry = carry ushr 32 - } - r[8] = carry.toInt() - return r } /** Convert big-endian 32-byte array to IntArray(8) little-endian limbs */ fun fromBytes(bytes: ByteArray): IntArray { - require(bytes.size == 32) { "Expected 32 bytes, got ${bytes.size}" } + require(bytes.size == 32) val r = IntArray(8) for (i in 0 until 8) { - val offset = 28 - i * 4 - r[i] = ((bytes[offset].toInt() and 0xFF) shl 24) or - ((bytes[offset + 1].toInt() and 0xFF) shl 16) or - ((bytes[offset + 2].toInt() and 0xFF) shl 8) or - (bytes[offset + 3].toInt() and 0xFF) + val o = 28 - i * 4 + r[i] = ((bytes[o].toInt() and 0xFF) shl 24) or + ((bytes[o + 1].toInt() and 0xFF) shl 16) or + ((bytes[o + 2].toInt() and 0xFF) shl 8) or + (bytes[o + 3].toInt() and 0xFF) } return r } @@ -131,44 +117,69 @@ internal object U256 { fun toBytes(a: IntArray): ByteArray { val r = ByteArray(32) for (i in 0 until 8) { - val offset = 28 - i * 4 - r[offset] = (a[i] ushr 24).toByte() - r[offset + 1] = (a[i] ushr 16).toByte() - r[offset + 2] = (a[i] ushr 8).toByte() - r[offset + 3] = a[i].toByte() + val o = 28 - i * 4 + r[o] = (a[i] ushr 24).toByte() + r[o + 1] = (a[i] ushr 16).toByte() + r[o + 2] = (a[i] ushr 8).toByte() + r[o + 3] = a[i].toByte() } return r } - /** Check if bit at position pos is set (pos 0 = LSB) */ + /** Write big-endian bytes into existing array at offset */ + fun toBytesInto( + a: IntArray, + dest: ByteArray, + offset: Int, + ) { + for (i in 0 until 8) { + val o = offset + 28 - i * 4 + dest[o] = (a[i] ushr 24).toByte() + dest[o + 1] = (a[i] ushr 16).toByte() + dest[o + 2] = (a[i] ushr 8).toByte() + dest[o + 3] = a[i].toByte() + } + } + + /** Get 4-bit nibble from scalar at position pos (pos 0 = lowest nibble) */ + fun getNibble( + a: IntArray, + pos: Int, + ): Int { + val limb = pos / 8 + val shift = (pos % 8) * 4 + return (a[limb] ushr shift) and 0xF + } + + /** Check if bit at position pos is set */ fun testBit( a: IntArray, pos: Int, - ): Boolean { - val limb = pos / 32 - val bit = pos % 32 - return (a[limb] ushr bit) and 1 == 1 - } + ): Boolean = (a[pos / 32] ushr (pos % 32)) and 1 == 1 - /** XOR two 256-bit values */ - fun xor( + /** XOR: out = a xor b */ + fun xorTo( + out: IntArray, a: IntArray, b: IntArray, - ): IntArray { - val r = IntArray(8) - for (i in 0 until 8) r[i] = a[i] xor b[i] - return r + ) { + for (i in 0 until 8) out[i] = a[i] xor b[i] } - fun clone(a: IntArray): IntArray = a.copyOf() + /** Copy a into out */ + fun copyInto( + out: IntArray, + a: IntArray, + ) { + a.copyInto(out) + } } /** - * Field arithmetic modulo p = 2^256 - 2^32 - 977 (= 2^256 - 4294968273). - * This is the base field of the secp256k1 curve. + * Field arithmetic modulo p = 2^256 - 2^32 - 977. + * All hot-path operations write results into caller-provided output arrays. */ internal object FieldP { - // p = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F val P = intArrayOf( 0xFFFFFC2F.toInt(), @@ -181,133 +192,145 @@ internal object FieldP { 0xFFFFFFFF.toInt(), ) - // 2^256 mod p = 2^32 + 977 = 0x1000003D1 (fits in 33 bits) - // As limbs: [977, 1, 0, 0, 0, 0, 0, 0] - private val P_COMPLEMENT_LIMBS = intArrayOf(977, 1, 0, 0, 0, 0, 0, 0) + // Thread-local scratch space to avoid allocation in hot path + // Since Kotlin/JVM coroutines are cooperative (not preemptive on same thread), + // thread-locals are safe as long as we don't call suspend functions mid-computation. + private val wide = ThreadLocal.withInitial { IntArray(16) } - /** Reduce a value that might be >= p (but < 2p) */ - fun reduce(a: IntArray): IntArray = + /** Reduce in-place: if a >= p, subtract p */ + fun reduceSelf(a: IntArray) { if (U256.cmp(a, P) >= 0) { - U256.subBorrow(a, P).first - } else { - a + U256.subTo(a, a, P) } + } - /** Reduce a wide 512-bit product mod p */ - fun reduceWide(w: IntArray): IntArray { - // w = hi * 2^256 + lo - // ≡ lo + hi * (2^32 + 977) (mod p) - // + /** Reduce a wide 512-bit value in w[0..15] -> out[0..7] mod p */ + fun reduceWide( + out: IntArray, + w: IntArray, + ) { + // w ≡ lo + hi * (2^32 + 977) (mod p) // Split: hi * (2^32 + 977) = (hi << 32) + hi * 977 - // hi * 977: each limb product fits in 42 bits, no overflow - // hi << 32: shift limbs by one position - val lo = IntArray(8) - val hi = IntArray(8) - for (i in 0 until 8) { - lo[i] = w[i] - hi[i] = w[i + 8] - } - - // Compute hi * 977 (result fits in 266 bits = 9 limbs) - val hiTimes977 = U256.mulSmall(hi, 977L) - - // Compute lo + hi * 977 + (hi << 32) - // hi << 32 means: [0, hi[0], hi[1], ..., hi[7]] (9 limbs) - val result = IntArray(8) + // Compute: out = lo + hi*977 + (hi << 32) var carry = 0L for (i in 0 until 8) { - carry += (lo[i].toLong() and 0xFFFFFFFFL) + - (hiTimes977[i].toLong() and 0xFFFFFFFFL) + - if (i > 0) (hi[i - 1].toLong() and 0xFFFFFFFFL) else 0L - result[i] = carry.toInt() + // lo[i] + carry += (w[i].toLong() and 0xFFFFFFFFL) + // hi[i] * 977 + carry += (w[i + 8].toLong() and 0xFFFFFFFFL) * 977L + // hi[i-1] (the <<32 shift) + if (i > 0) carry += (w[i + 7].toLong() and 0xFFFFFFFFL) + out[i] = carry.toInt() carry = carry ushr 32 } - // Remaining overflow: carry + hiTimes977[8] + hi[7] - var overflow = - carry + - (hiTimes977[8].toLong() and 0xFFFFFFFFL) + - (hi[7].toLong() and 0xFFFFFFFFL) + // Remaining: carry + hi[7] + var overflow = carry + (w[15].toLong() and 0xFFFFFFFFL) // Second round: overflow * (2^32 + 977) - // overflow is at most ~35 bits, so overflow * 977 fits in Long easily if (overflow > 0) { val ov977 = overflow * 977L var c2 = 0L - // Add ov977 to result[0..1], and overflow to result[1..2] (the <<32 part) for (i in 0 until 8) { - c2 += (result[i].toLong() and 0xFFFFFFFFL) + c2 += (out[i].toLong() and 0xFFFFFFFFL) if (i == 0) c2 += (ov977 and 0xFFFFFFFFL) if (i == 1) c2 += (ov977 ushr 32) + (overflow and 0xFFFFFFFFL) if (i == 2) c2 += (overflow ushr 32) - result[i] = c2.toInt() + out[i] = c2.toInt() c2 = c2 ushr 32 } - // c2 should be 0 or very small; if > 0, do a third tiny round if (c2 > 0) { val tiny = c2 * 977L var c3 = 0L for (i in 0 until 3) { - c3 += (result[i].toLong() and 0xFFFFFFFFL) + c3 += (out[i].toLong() and 0xFFFFFFFFL) if (i == 0) c3 += (tiny and 0xFFFFFFFFL) if (i == 1) c3 += (tiny ushr 32) + (c2 and 0xFFFFFFFFL) if (i == 2) c3 += (c2 ushr 32) - result[i] = c3.toInt() + out[i] = c3.toInt() c3 = c3 ushr 32 } } } - - // Final reduction: result might still be >= p - return reduce(result) + reduceSelf(out) } + /** out = a + b mod p */ fun add( + out: IntArray, a: IntArray, b: IntArray, - ): IntArray { - val (sum, carry) = U256.addCarry(a, b) - return if (carry != 0) { - // sum + 2^256 ≡ sum + (2^32 + 977) (mod p) - // carry is always 1 here, so just add the constant - val (r2, c2) = U256.addCarry(sum, P_COMPLEMENT_LIMBS) - if (c2 != 0) reduce(U256.addCarry(r2, P_COMPLEMENT_LIMBS).first) else reduce(r2) - } else { - reduce(sum) + ) { + val carry = U256.addTo(out, a, b) + if (carry != 0) { + // out + 2^256 ≡ out + (2^32 + 977) mod p + var c = 977L + (out[0].toLong() and 0xFFFFFFFFL) + out[0] = c.toInt() + c = c ushr 32 + c += 1L + (out[1].toLong() and 0xFFFFFFFFL) + out[1] = c.toInt() + c = c ushr 32 + for (i in 2 until 8) { + c += (out[i].toLong() and 0xFFFFFFFFL) + out[i] = c.toInt() + c = c ushr 32 + } } + reduceSelf(out) } + /** out = a - b mod p */ fun sub( + out: IntArray, a: IntArray, b: IntArray, - ): IntArray { - val (diff, borrow) = U256.subBorrow(a, b) - return if (borrow != 0) { - // Add p back - U256.addCarry(diff, P).first - } else { - diff + ) { + val borrow = U256.subTo(out, a, b) + if (borrow != 0) { + U256.addTo(out, out, P) } } + /** out = a * b mod p. Uses thread-local scratch space. */ fun mul( + out: IntArray, a: IntArray, b: IntArray, - ): IntArray = reduceWide(U256.mulWide(a, b)) - - fun sqr(a: IntArray): IntArray = mul(a, a) - - fun neg(a: IntArray): IntArray = if (U256.isZero(a)) IntArray(8) else U256.subBorrow(P, a).first - - /** Modular inverse using Fermat's little theorem: a^(p-2) mod p */ - fun inv(a: IntArray): IntArray { - require(!U256.isZero(a)) { "Cannot invert zero" } - // p - 2 = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2D - // Use square-and-multiply with an optimized addition chain for secp256k1 - return powModP(a, P_MINUS_2) + ) { + val w = wide.get() + U256.mulWide(w, a, b) + reduceWide(out, w) + } + + /** out = a² mod p. Uses thread-local scratch space. */ + fun sqr( + out: IntArray, + a: IntArray, + ) { + mul(out, a, a) + } + + /** out = -a mod p */ + fun neg( + out: IntArray, + a: IntArray, + ) { + if (U256.isZero(a)) { + for (i in 0 until 8) out[i] = 0 + } else { + U256.subTo(out, P, a) + } + } + + /** out = a^(-1) mod p via Fermat's little theorem */ + fun inv( + out: IntArray, + a: IntArray, + ) { + require(!U256.isZero(a)) + powModP(out, a, P_MINUS_2) } - // p - 2 private val P_MINUS_2 = intArrayOf( 0xFFFFFC2D.toInt(), @@ -320,8 +343,6 @@ internal object FieldP { 0xFFFFFFFF.toInt(), ) - // (p + 1) / 4 = 3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF0C - // Used for computing square roots since p ≡ 3 (mod 4) private val P_PLUS_1_DIV_4 = intArrayOf( 0xBFFFFF0C.toInt(), @@ -334,39 +355,111 @@ internal object FieldP { 0x3FFFFFFF, ) - /** Square root mod p. Returns null if a is not a quadratic residue. */ - fun sqrt(a: IntArray): IntArray? { - // Since p ≡ 3 (mod 4), sqrt(a) = a^((p+1)/4) mod p - val r = powModP(a, P_PLUS_1_DIV_4) - // Verify: r^2 == a (mod p) - return if (U256.cmp(mul(r, r), reduce(a)) == 0) r else null + /** out = sqrt(a) mod p, returns false if not a QR */ + fun sqrt( + out: IntArray, + a: IntArray, + ): Boolean { + powModP(out, a, P_PLUS_1_DIV_4) + // Verify: out² == a + val check = IntArray(8) + mul(check, out, out) + // Need a reduced copy of a for comparison + val ar = IntArray(8) + U256.copyInto(ar, a) + reduceSelf(ar) + return U256.cmp(check, ar) == 0 } - /** Generic modular exponentiation mod p using square-and-multiply */ + /** out = base^exp mod p */ private fun powModP( + out: IntArray, base: IntArray, exp: IntArray, - ): IntArray { - var result = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0) // 1 - var b = base.copyOf() - // Find highest set bit + ) { + // Left-to-right square-and-multiply + val b = IntArray(8) + U256.copyInto(b, base) + var highBit = 255 while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit-- - for (i in 0..highBit) { + if (highBit < 0) { + out[0] = 1 + for (i in 1 until 8) out[i] = 0 + return + } + + // Start with the base (first bit is always 1) + U256.copyInto(out, b) + for (i in highBit - 1 downTo 0) { + sqr(out, out) // out = out² if (U256.testBit(exp, i)) { - result = mul(result, b) - } - if (i < highBit) { - b = sqr(b) + mul(out, out, b) // out = out * base } } - return result + } + + // === Allocating convenience wrappers (for non-hot paths) === + + fun add( + a: IntArray, + b: IntArray, + ): IntArray { + val r = IntArray(8) + add(r, a, b) + return r + } + + fun sub( + a: IntArray, + b: IntArray, + ): IntArray { + val r = IntArray(8) + sub(r, a, b) + return r + } + + fun mul( + a: IntArray, + b: IntArray, + ): IntArray { + val r = IntArray(8) + mul(r, a, b) + return r + } + + fun sqr(a: IntArray): IntArray { + val r = IntArray(8) + sqr(r, a) + return r + } + + fun neg(a: IntArray): IntArray { + val r = IntArray(8) + neg(r, a) + return r + } + + fun inv(a: IntArray): IntArray { + val r = IntArray(8) + inv(r, a) + return r + } + + fun sqrt(a: IntArray): IntArray? { + val r = IntArray(8) + return if (sqrt(r, a)) r else null + } + + fun reduce(a: IntArray): IntArray { + val r = a.copyOf() + reduceSelf(r) + return r } } /** * Scalar arithmetic modulo n (the order of the secp256k1 group). - * n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 */ internal object ScalarN { val N = @@ -381,99 +474,6 @@ internal object ScalarN { 0xFFFFFFFF.toInt(), ) - /** Check if 0 < a < n */ - fun isValid(a: IntArray): Boolean { - if (U256.isZero(a)) return false - return U256.cmp(a, N) < 0 - } - - /** Reduce mod n: for values up to 2n */ - fun reduce(a: IntArray): IntArray = - if (U256.cmp(a, N) >= 0) { - U256.subBorrow(a, N).first - } else { - a - } - - /** Reduce a wide 512-bit value mod n */ - fun reduceWide(w: IntArray): IntArray { - // Barrett-like reduction using schoolbook division - // For simplicity, we do repeated subtraction with shifts - // Since n is close to 2^256, the quotient is at most ~2^256. - // We use a simpler approach: reduce by subtracting n * (hi_estimate) - // - // Actually, we'll use the same approach as field reduction but - // for n which doesn't have a nice sparse form. - // n = 2^256 - nComplement where nComplement = 2^256 - n - // = 0x14551231950B75FC4402DA1732FC9BEBF - - val lo = IntArray(8) - val hi = IntArray(8) - for (i in 0 until 8) { - lo[i] = w[i] - hi[i] = w[i + 8] - } - - if (U256.isZero(hi)) return reduce(lo) - - // 2^256 mod n = N_COMPLEMENT - // hi * 2^256 ≡ hi * N_COMPLEMENT (mod n) - val hiTimesNC = U256.mulWide(hi, N_COMPLEMENT) - // This is at most 384 bits. Add lo. - val sum = IntArray(16) - var carry = 0L - for (i in 0 until 16) { - carry += (hiTimesNC[i].toLong() and 0xFFFFFFFFL) + - if (i < 8) (lo[i].toLong() and 0xFFFFFFFFL) else 0L - sum[i] = carry.toInt() - carry = carry ushr 32 - } - - // Now sum might be up to ~384 bits. Repeat reduction. - val lo2 = IntArray(8) - val hi2 = IntArray(8) - for (i in 0 until 8) { - lo2[i] = sum[i] - hi2[i] = sum[i + 8] - } - - if (U256.isZero(hi2)) return reduce(lo2) - - // Another round - val hi2TimesNC = U256.mulWide(hi2, N_COMPLEMENT) - var carry2 = 0L - val result = IntArray(8) - for (i in 0 until 8) { - carry2 += (lo2[i].toLong() and 0xFFFFFFFFL) + (hi2TimesNC[i].toLong() and 0xFFFFFFFFL) - result[i] = carry2.toInt() - carry2 = carry2 ushr 32 - } - // Handle any remaining overflow - var overflow = carry2 - for (i in 8 until 16) { - overflow += (hi2TimesNC[i].toLong() and 0xFFFFFFFFL) - } - // overflow * 2^256 ≡ overflow * N_COMPLEMENT (mod n) - if (overflow > 0) { - val corr = U256.mulSmall(N_COMPLEMENT, overflow) - var c3 = 0L - for (i in 0 until 8) { - c3 += (result[i].toLong() and 0xFFFFFFFFL) + (corr[i].toLong() and 0xFFFFFFFFL) - result[i] = c3.toInt() - c3 = c3 ushr 32 - } - } - - // Final reductions - var r = result - while (U256.cmp(r, N) >= 0) { - r = U256.subBorrow(r, N).first - } - return r - } - - // 2^256 - n = 14551231950B75FC4402DA1732FC9BEBF - // In little-endian limbs: private val N_COMPLEMENT = intArrayOf( 0x2FC9BEBF.toInt(), @@ -486,46 +486,6 @@ internal object ScalarN { 0, ) - fun add( - a: IntArray, - b: IntArray, - ): IntArray { - val (sum, carry) = U256.addCarry(a, b) - return if (carry != 0) { - // sum + 2^256 ≡ sum + N_COMPLEMENT (mod n) - val (r2, _) = U256.addCarry(sum, N_COMPLEMENT) - reduce(r2) - } else { - reduce(sum) - } - } - - fun sub( - a: IntArray, - b: IntArray, - ): IntArray { - val (diff, borrow) = U256.subBorrow(a, b) - return if (borrow != 0) { - U256.addCarry(diff, N).first - } else { - diff - } - } - - fun mul( - a: IntArray, - b: IntArray, - ): IntArray = reduceWide(U256.mulWide(a, b)) - - fun neg(a: IntArray): IntArray = if (U256.isZero(a)) IntArray(8) else U256.subBorrow(N, a).first - - /** Modular inverse using Fermat's little theorem: a^(n-2) mod n */ - fun inv(a: IntArray): IntArray { - require(!U256.isZero(a)) { "Cannot invert zero" } - return powModN(a, N_MINUS_2) - } - - // n - 2 private val N_MINUS_2 = intArrayOf( 0xD036413F.toInt(), @@ -538,20 +498,155 @@ internal object ScalarN { 0xFFFFFFFF.toInt(), ) + fun isValid(a: IntArray): Boolean = !U256.isZero(a) && U256.cmp(a, N) < 0 + + fun reduce(a: IntArray): IntArray = + if (U256.cmp(a, N) >= 0) { + val r = IntArray(8) + U256.subTo(r, a, N) + r + } else { + a + } + + fun add( + a: IntArray, + b: IntArray, + ): IntArray { + val r = IntArray(8) + val carry = U256.addTo(r, a, b) + if (carry != 0) { + U256.addTo(r, r, N_COMPLEMENT) + reduceSelf(r) + } else { + reduceSelf(r) + } + return r + } + + fun sub( + a: IntArray, + b: IntArray, + ): IntArray { + val r = IntArray(8) + val borrow = U256.subTo(r, a, b) + if (borrow != 0) U256.addTo(r, r, N) + return r + } + + fun mul( + a: IntArray, + b: IntArray, + ): IntArray = reduceWide(mulWideAlloc(a, b)) + + fun neg(a: IntArray): IntArray { + if (U256.isZero(a)) return IntArray(8) + val r = IntArray(8) + U256.subTo(r, N, a) + return r + } + + fun inv(a: IntArray): IntArray { + require(!U256.isZero(a)) + return powModN(a, N_MINUS_2) + } + + private fun reduceSelf(a: IntArray) { + if (U256.cmp(a, N) >= 0) U256.subTo(a, a, N) + } + + private fun mulWideAlloc( + a: IntArray, + b: IntArray, + ): IntArray { + val w = IntArray(16) + U256.mulWide(w, a, b) + return w + } + + private fun reduceWide(w: IntArray): IntArray { + val lo = IntArray(8) + val hi = IntArray(8) + for (i in 0 until 8) { + lo[i] = w[i] + hi[i] = w[i + 8] + } + if (U256.isZero(hi)) { + reduceSelf(lo) + return lo + } + + val hiTimesNC = IntArray(16) + U256.mulWide(hiTimesNC, hi, N_COMPLEMENT) + val sum = IntArray(16) + var carry = 0L + for (i in 0 until 16) { + carry += (hiTimesNC[i].toLong() and 0xFFFFFFFFL) + + if (i < 8) (lo[i].toLong() and 0xFFFFFFFFL) else 0L + sum[i] = carry.toInt() + carry = carry ushr 32 + } + + val lo2 = IntArray(8) + val hi2 = IntArray(8) + for (i in 0 until 8) { + lo2[i] = sum[i] + hi2[i] = sum[i + 8] + } + if (U256.isZero(hi2)) { + reduceSelf(lo2) + return lo2 + } + + val hi2NC = IntArray(16) + U256.mulWide(hi2NC, hi2, N_COMPLEMENT) + var c2 = 0L + val result = IntArray(8) + for (i in 0 until 8) { + c2 += (lo2[i].toLong() and 0xFFFFFFFFL) + (hi2NC[i].toLong() and 0xFFFFFFFFL) + result[i] = c2.toInt() + c2 = c2 ushr 32 + } + var overflow = c2 + for (i in 8 until 16) overflow += (hi2NC[i].toLong() and 0xFFFFFFFFL) + if (overflow > 0) { + val corr = IntArray(9) + var cc = 0L + for (i in 0 until 8) { + cc += (N_COMPLEMENT[i].toLong() and 0xFFFFFFFFL) * overflow + corr[i] = cc.toInt() + cc = cc ushr 32 + } + var c3 = 0L + for (i in 0 until 8) { + c3 += (result[i].toLong() and 0xFFFFFFFFL) + (corr[i].toLong() and 0xFFFFFFFFL) + result[i] = c3.toInt() + c3 = c3 ushr 32 + } + } + while (U256.cmp(result, N) >= 0) U256.subTo(result, result, N) + return result + } + private fun powModN( base: IntArray, exp: IntArray, ): IntArray { - var result = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0) - var b = base.copyOf() + val result = IntArray(8) + val b = base.copyOf() var highBit = 255 while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit-- - for (i in 0..highBit) { + if (highBit < 0) { + result[0] = 1 + return result + } + U256.copyInto(result, b) + for (i in highBit - 1 downTo 0) { + val sq = mul(result, result) + U256.copyInto(result, sq) if (U256.testBit(exp, i)) { - result = mul(result, b) - } - if (i < highBit) { - b = mul(b, b) + val prod = mul(result, b) + U256.copyInto(result, prod) } } return result 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 2d00f66dd..e8458ec7d 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 @@ -21,24 +21,46 @@ package com.vitorpamplona.quartz.utils.secp256k1 /** - * Elliptic curve point operations on the secp256k1 curve: y² = x³ + 7 (mod p). - * Uses Jacobian coordinates (X, Y, Z) where affine (x, y) = (X/Z², Y/Z³). - * The point at infinity is represented by Z = 0. + * Mutable Jacobian point for in-place computation. + * (X, Y, Z) represents affine (X/Z², Y/Z³). Infinity: Z = 0. */ -internal class JPoint( - val x: IntArray, - val y: IntArray, - val z: IntArray, +internal class MutablePoint( + val x: IntArray = IntArray(8), + val y: IntArray = IntArray(8), + val z: IntArray = IntArray(8), ) { - companion object { - val INFINITY = JPoint(IntArray(8), intArrayOf(1, 0, 0, 0, 0, 0, 0, 0), IntArray(8)) + fun isInfinity(): Boolean = U256.isZero(z) + + fun setInfinity() { + for (i in 0 until 8) { + x[i] = 0 + z[i] = 0 + } + y[0] = 1 + for (i in 1 until 8) y[i] = 0 } - fun isInfinity(): Boolean = U256.isZero(z) + fun copyFrom(other: MutablePoint) { + other.x.copyInto(x) + other.y.copyInto(y) + other.z.copyInto(z) + } + + fun setAffine( + ax: IntArray, + ay: IntArray, + ) { + ax.copyInto(x) + ay.copyInto(y) + z[0] = 1 + for (i in 1 until 8) z[i] = 0 + } + + /** Create a snapshot (immutable copy for table storage) */ + fun snapshot(): MutablePoint = MutablePoint(x.copyOf(), y.copyOf(), z.copyOf()) } internal object ECPoint { - // Generator point G val GX = intArrayOf( 0x16F81798.toInt(), @@ -61,233 +83,361 @@ internal object ECPoint { 0x26A3C465.toInt(), 0x483ADA77.toInt(), ) - - val G = JPoint(GX.copyOf(), GY.copyOf(), intArrayOf(1, 0, 0, 0, 0, 0, 0, 0)) - - // Curve constant b = 7 private val B = intArrayOf(7, 0, 0, 0, 0, 0, 0, 0) + private val ONE = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0) + + // Precomputed table for G: gTable[i] = (i+1)*G for i in 0..15 + // Lazily initialized on first use. + private val gTable: Array by lazy { buildGTable() } + + private fun buildGTable(): Array { + val table = Array(16) { MutablePoint() } + // table[0] = G + table[0].setAffine(GX, GY) + // table[i] = table[i-1] + G + val tmp = MutablePoint() + for (i in 1 until 16) { + addPoints(table[i], table[i - 1], table[0]) + } + return Array(16) { table[it].snapshot() } + } + + // ============ Scratch buffers for point operations (thread-local) ============ + private class PointScratch { + val t = Array(12) { IntArray(8) } // temporary field elements + val dblCopy = MutablePoint() // copy buffer for in-place doubling + } + + private val scratch = ThreadLocal.withInitial { PointScratch() } /** - * Point doubling in Jacobian coordinates. - * Formula from https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l + * Point doubling: out = 2*p. + * Formula: dbl-2009-l from https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html */ - fun double(p: JPoint): JPoint { - if (p.isInfinity()) return JPoint.INFINITY - - val x = p.x - val y = p.y - val z = p.z - - val a = FieldP.sqr(x) // A = X1² - val b = FieldP.sqr(y) // B = Y1² - val c = FieldP.sqr(b) // C = B² - val xPlusB = FieldP.add(x, b) - val d = - FieldP.sub( - FieldP.sub(FieldP.sqr(xPlusB), a), - c, - ) - val d2 = FieldP.add(d, d) // D = 2*((X1+B)²-A-C) - val e = FieldP.add(FieldP.add(a, a), a) // E = 3*A - val f = FieldP.sqr(e) // F = E² - - val x3 = FieldP.sub(f, FieldP.add(d2, d2)) // X3 = F - 2*D - val c8 = - FieldP.add( - FieldP.add(FieldP.add(c, c), FieldP.add(c, c)), - FieldP.add(FieldP.add(c, c), FieldP.add(c, c)), - ) // 8*C - val y3 = FieldP.sub(FieldP.mul(e, FieldP.sub(d2, x3)), c8) // Y3 = E*(D-X3) - 8*C - val z3 = - FieldP.sub( - FieldP.sub(FieldP.sqr(FieldP.add(y, z)), FieldP.sqr(y)), - FieldP.sqr(z), - ) // Z3 = (Y1+Z1)² - B - Z1² ... but B = Y1² so this = 2*Y1*Z1 - - return JPoint(x3, y3, z3) + fun doublePoint( + out: MutablePoint, + inp: MutablePoint, + ) { + if (inp.isInfinity()) { + out.setInfinity() + return + } + val s = scratch.get() + // If out aliases inp, copy inp to scratch first + val p = + if (out === inp) { + s.dblCopy.copyFrom(inp) + s.dblCopy + } else { + inp + } + val t = s.t + // t0=A=X², t1=B=Y², t2=C=B², t3=(X+B)² + FieldP.sqr(t[0], p.x) + FieldP.sqr(t[1], p.y) + FieldP.sqr(t[2], t[1]) + FieldP.add(t[3], p.x, t[1]) + FieldP.sqr(t[3], t[3]) + // t3 = D = 2*((X+B)²-A-C) + FieldP.sub(t[3], t[3], t[0]) + FieldP.sub(t[3], t[3], t[2]) + FieldP.add(t[3], t[3], t[3]) // D + // t4 = E = 3*A + FieldP.add(t[4], t[0], t[0]) + FieldP.add(t[4], t[4], t[0]) + // t5 = F = E² + FieldP.sqr(t[5], t[4]) + // X3 = F - 2*D + FieldP.add(t[6], t[3], t[3]) // 2D + FieldP.sub(out.x, t[5], t[6]) + // Y3 = E*(D-X3) - 8*C + FieldP.sub(t[7], t[3], out.x) + FieldP.mul(t[7], t[4], t[7]) + FieldP.add(t[2], t[2], t[2]) // 2C + FieldP.add(t[2], t[2], t[2]) // 4C + FieldP.add(t[2], t[2], t[2]) // 8C + FieldP.sub(out.y, t[7], t[2]) + // Z3 = 2*Y*Z + FieldP.add(t[8], p.y, p.z) + FieldP.sqr(t[8], t[8]) + FieldP.sub(t[8], t[8], t[1]) // -B + FieldP.sqr(t[9], p.z) + FieldP.sub(out.z, t[8], t[9]) } /** - * Point addition in Jacobian coordinates. - * Mixed addition when q.z = 1 (affine point) for efficiency. + * Point addition: out = p + q. Handles p==q (doubling) and inverses. */ - fun add( - p: JPoint, - q: JPoint, - ): JPoint { - if (p.isInfinity()) return q - if (q.isInfinity()) return p + fun addPoints( + out: MutablePoint, + p: MutablePoint, + q: MutablePoint, + ) { + if (p.isInfinity()) { + out.copyFrom(q) + return + } + if (q.isInfinity()) { + out.copyFrom(p) + return + } + val s = scratch.get() + val t = s.t - val z1sq = FieldP.sqr(p.z) - val z2sq = FieldP.sqr(q.z) + FieldP.sqr(t[0], p.z) // Z1² + FieldP.sqr(t[1], q.z) // Z2² + FieldP.mul(t[2], p.x, t[1]) // U1 = X1*Z2² + FieldP.mul(t[3], q.x, t[0]) // U2 = X2*Z1² + FieldP.mul(t[4], q.z, t[1]) // Z2³ + FieldP.mul(t[4], p.y, t[4]) // S1 = Y1*Z2³ + FieldP.mul(t[5], p.z, t[0]) // Z1³ + FieldP.mul(t[5], q.y, t[5]) // S2 = Y2*Z1³ - val u1 = FieldP.mul(p.x, z2sq) // U1 = X1*Z2² - val u2 = FieldP.mul(q.x, z1sq) // U2 = X2*Z1² - val s1 = FieldP.mul(p.y, FieldP.mul(q.z, z2sq)) // S1 = Y1*Z2³ - val s2 = FieldP.mul(q.y, FieldP.mul(p.z, z1sq)) // S2 = Y2*Z1³ - - if (U256.cmp(u1, u2) == 0) { - return if (U256.cmp(s1, s2) == 0) { - double(p) // Same point + if (U256.cmp(t[2], t[3]) == 0) { + if (U256.cmp(t[4], t[5]) == 0) { + doublePoint(out, p) } else { - JPoint.INFINITY // Inverse points + out.setInfinity() } + return } - val h = FieldP.sub(u2, u1) // H = U2 - U1 - val i = FieldP.sqr(FieldP.add(h, h)) // I = (2*H)² - val j = FieldP.mul(h, i) // J = H*I - val r = - FieldP.add( - FieldP.sub(s2, s1), - FieldP.sub(s2, s1), - ) // r = 2*(S2-S1) - val v = FieldP.mul(u1, i) // V = U1*I - - val x3 = - FieldP.sub( - FieldP.sub(FieldP.sqr(r), j), - FieldP.add(v, v), - ) // X3 = r² - J - 2*V - val y3 = - FieldP.sub( - FieldP.mul(r, FieldP.sub(v, x3)), - FieldP.add(FieldP.mul(s1, j), FieldP.mul(s1, j)), - ) // Y3 = r*(V-X3) - 2*S1*J - val z3 = - FieldP.mul( - FieldP.sub( - FieldP.sub( - FieldP.sqr(FieldP.add(p.z, q.z)), - z1sq, - ), - z2sq, - ), - h, - ) // Z3 = ((Z1+Z2)²-Z1²-Z2²)*H - - return JPoint(x3, y3, z3) + FieldP.sub(t[6], t[3], t[2]) // H = U2-U1 + FieldP.add(t[7], t[6], t[6]) // 2H + FieldP.sqr(t[7], t[7]) // I = (2H)² + FieldP.mul(t[8], t[6], t[7]) // J = H*I + FieldP.sub(t[9], t[5], t[4]) + FieldP.add(t[9], t[9], t[9]) // r = 2*(S2-S1) + FieldP.mul(t[10], t[2], t[7]) // V = U1*I + // X3 = r² - J - 2V + FieldP.sqr(out.x, t[9]) + FieldP.sub(out.x, out.x, t[8]) + FieldP.sub(out.x, out.x, t[10]) + FieldP.sub(out.x, out.x, t[10]) + // Y3 = r*(V-X3) - 2*S1*J + FieldP.sub(t[11], t[10], out.x) + FieldP.mul(out.y, t[9], t[11]) + FieldP.mul(t[11], t[4], t[8]) // S1*J + FieldP.add(t[11], t[11], t[11]) // 2*S1*J + FieldP.sub(out.y, out.y, t[11]) + // Z3 = ((Z1+Z2)²-Z1²-Z2²)*H + FieldP.add(out.z, p.z, q.z) + FieldP.sqr(out.z, out.z) + FieldP.sub(out.z, out.z, t[0]) + FieldP.sub(out.z, out.z, t[1]) + FieldP.mul(out.z, out.z, t[6]) } /** - * Scalar multiplication using double-and-add (left-to-right). + * Scalar multiplication: out = scalar * p, using 4-bit windowed method. */ fun mul( - p: JPoint, + out: MutablePoint, + p: MutablePoint, scalar: IntArray, - ): JPoint { - if (U256.isZero(scalar) || p.isInfinity()) return JPoint.INFINITY + ) { + if (U256.isZero(scalar) || p.isInfinity()) { + out.setInfinity() + return + } - var result = JPoint.INFINITY - // Find highest set bit - var highBit = 255 - while (highBit >= 0 && !U256.testBit(scalar, highBit)) highBit-- + // Build 4-bit window table: table[i] = (i+1)*p for i in 0..15 + val table = Array(16) { MutablePoint() } + table[0].copyFrom(p) + val tmp = MutablePoint() + for (i in 1 until 16) { + addPoints(table[i], table[i - 1], p) + } - for (i in highBit downTo 0) { - result = double(result) - if (U256.testBit(scalar, i)) { - result = add(result, p) + out.setInfinity() + // Process 4 bits at a time, MSB first (64 nibbles for 256 bits) + for (nibbleIdx in 63 downTo 0) { + // 4 doublings + doublePoint(out, out) + doublePoint(out, out) + doublePoint(out, out) + doublePoint(out, out) + val nib = U256.getNibble(scalar, nibbleIdx) + if (nib != 0) { + addPoints(tmp, out, table[nib - 1]) + out.copyFrom(tmp) } } - return result } /** - * Convert Jacobian point to affine coordinates. - * Returns null if the point is at infinity. + * G multiplication using precomputed table: out = scalar * G. + * Uses 4-bit windowed method with static precomputed table. */ - fun toAffine(p: JPoint): Pair? { - if (p.isInfinity()) return null - val zInv = FieldP.inv(p.z) - val zInv2 = FieldP.sqr(zInv) - val zInv3 = FieldP.mul(zInv2, zInv) - val x = FieldP.mul(p.x, zInv2) - val y = FieldP.mul(p.y, zInv3) - return Pair(x, y) - } - - /** - * Lift x-coordinate to a point on the curve. - * Returns the point with even y if it exists, null otherwise. - * Used by BIP-340 for x-only public keys. - */ - fun liftX(x: IntArray): Pair? { - // Check x < p - if (U256.cmp(x, FieldP.P) >= 0) return null - - // y² = x³ + 7 - val x3 = FieldP.mul(FieldP.sqr(x), x) - val y2 = FieldP.add(x3, B) - val y = FieldP.sqrt(y2) ?: return null - - // Return the even-y variant - val yBytes = U256.toBytes(y) - return if (yBytes[31].toInt() and 1 == 0) { - Pair(x, y) - } else { - Pair(x, FieldP.neg(y)) + fun mulG( + out: MutablePoint, + scalar: IntArray, + ) { + if (U256.isZero(scalar)) { + out.setInfinity() + return + } + val table = gTable // force lazy init + out.setInfinity() + val tmp = MutablePoint() + for (nibbleIdx in 63 downTo 0) { + doublePoint(out, out) + doublePoint(out, out) + doublePoint(out, out) + doublePoint(out, out) + val nib = U256.getNibble(scalar, nibbleIdx) + if (nib != 0) { + addPoints(tmp, out, table[nib - 1]) + out.copyFrom(tmp) + } } } - /** Check if y coordinate is even */ - fun hasEvenY(y: IntArray): Boolean { - // y is even if the least significant bit is 0 - return y[0] and 1 == 0 + /** + * Shamir's trick: out = s*G + e*P in a single pass. + * Much faster than computing s*G and e*P separately for verification. + */ + fun mulDoubleG( + out: MutablePoint, + s: IntArray, + p: MutablePoint, + e: IntArray, + ) { + // Build 4-bit window table for P + val pTable = Array(16) { MutablePoint() } + pTable[0].copyFrom(p) + for (i in 1 until 16) { + addPoints(pTable[i], pTable[i - 1], p) + } + val gTab = gTable + val tmp = MutablePoint() + + out.setInfinity() + for (nibbleIdx in 63 downTo 0) { + doublePoint(out, out) + doublePoint(out, out) + doublePoint(out, out) + doublePoint(out, out) + val sNib = U256.getNibble(s, nibbleIdx) + val eNib = U256.getNibble(e, nibbleIdx) + if (sNib != 0) { + addPoints(tmp, out, gTab[sNib - 1]) + out.copyFrom(tmp) + } + if (eNib != 0) { + addPoints(tmp, out, pTable[eNib - 1]) + out.copyFrom(tmp) + } + } } - /** - * Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed). - * Returns affine (x, y) or null on failure. - */ - fun parsePublicKey(pubkey: ByteArray): Pair? { + /** Convert Jacobian to affine. Writes x, y into outX, outY. Returns false if infinity. */ + fun toAffine( + p: MutablePoint, + outX: IntArray, + outY: IntArray, + ): Boolean { + if (p.isInfinity()) return false + val zInv = IntArray(8) + val zInv2 = IntArray(8) + val zInv3 = IntArray(8) + FieldP.inv(zInv, p.z) + FieldP.sqr(zInv2, zInv) + FieldP.mul(zInv3, zInv2, zInv) + FieldP.mul(outX, p.x, zInv2) + FieldP.mul(outY, p.y, zInv3) + return true + } + + /** Lift x-coordinate to even-y point. Returns false if not on curve. */ + fun liftX( + outX: IntArray, + outY: IntArray, + x: IntArray, + ): Boolean { + if (U256.cmp(x, FieldP.P) >= 0) return false + val t = IntArray(8) + FieldP.sqr(t, x) + FieldP.mul(t, t, x) // x³ + FieldP.add(t, t, B) // x³+7 + if (!FieldP.sqrt(outY, t)) return false + U256.copyInto(outX, x) + // Ensure even y + if (outY[0] and 1 != 0) FieldP.neg(outY, outY) + return true + } + + fun hasEvenY(y: IntArray): Boolean = y[0] and 1 == 0 + + /** Parse serialized public key -> affine (outX, outY). Returns false on failure. */ + fun parsePublicKey( + pubkey: ByteArray, + outX: IntArray, + outY: IntArray, + ): Boolean { return when { pubkey.size == 33 && (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> { val x = U256.fromBytes(pubkey.copyOfRange(1, 33)) - if (U256.cmp(x, FieldP.P) >= 0) return null - val x3 = FieldP.mul(FieldP.sqr(x), x) - val y2 = FieldP.add(x3, B) - val y = FieldP.sqrt(y2) ?: return null - val isOdd = y[0] and 1 == 1 + if (U256.cmp(x, FieldP.P) >= 0) return false + val t = IntArray(8) + FieldP.sqr(t, x) + FieldP.mul(t, t, x) + FieldP.add(t, t, B) + if (!FieldP.sqrt(outY, t)) return false + U256.copyInto(outX, x) + val isOdd = outY[0] and 1 == 1 val wantOdd = pubkey[0] == 0x03.toByte() - if (isOdd != wantOdd) Pair(x, FieldP.neg(y)) else Pair(x, y) + if (isOdd != wantOdd) FieldP.neg(outY, outY) + true } pubkey.size == 65 && pubkey[0] == 0x04.toByte() -> { val x = U256.fromBytes(pubkey.copyOfRange(1, 33)) val y = U256.fromBytes(pubkey.copyOfRange(33, 65)) - // Verify point is on curve: y² = x³ + 7 - val y2 = FieldP.sqr(y) - val x3p7 = FieldP.add(FieldP.mul(FieldP.sqr(x), x), B) - if (U256.cmp(y2, x3p7) != 0) return null - Pair(x, y) + val y2 = IntArray(8) + val x3p7 = IntArray(8) + val t = IntArray(8) + FieldP.sqr(y2, y) + FieldP.sqr(t, x) + FieldP.mul(x3p7, t, x) + FieldP.add(x3p7, x3p7, B) + if (U256.cmp(y2, x3p7) != 0) return false + U256.copyInto(outX, x) + U256.copyInto(outY, y) + true } else -> { - null + false } } } - /** Serialize affine point as 65-byte uncompressed key (04 || x || y) */ fun serializeUncompressed( x: IntArray, y: IntArray, ): ByteArray { - val result = ByteArray(65) - result[0] = 0x04 - U256.toBytes(x).copyInto(result, 1) - U256.toBytes(y).copyInto(result, 33) - return result + val r = ByteArray(65) + r[0] = 0x04 + U256.toBytesInto(x, r, 1) + U256.toBytesInto(y, r, 33) + return r } - /** Serialize affine point as 33-byte compressed key (02/03 || x) */ fun serializeCompressed( x: IntArray, y: IntArray, ): ByteArray { - val result = ByteArray(33) - result[0] = if (hasEvenY(y)) 0x02 else 0x03 - U256.toBytes(x).copyInto(result, 1) - return result + val r = ByteArray(33) + r[0] = if (hasEvenY(y)) 0x02 else 0x03 + U256.toBytesInto(x, r, 1) + return r + } + + // Convenience wrappers for non-hot paths + fun toAffinePair(p: MutablePoint): Pair? { + val x = IntArray(8) + val y = IntArray(8) + return if (toAffine(p, x, y)) Pair(x, y) else null } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt index 8e58a1d6d..a021f13e1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt @@ -24,208 +24,181 @@ import com.vitorpamplona.quartz.utils.sha256.sha256 /** * Pure Kotlin implementation of secp256k1 elliptic curve operations. - * Provides the same functionality as fr.acinq.secp256k1.Secp256k1 but - * without requiring native bindings. * - * Implements only the operations used by Secp256k1Instance: - * - pubkeyCreate / pubKeyCompress - * - secKeyVerify - * - signSchnorr / verifySchnorr (BIP-340) - * - privKeyTweakAdd - * - pubKeyTweakMul + * Performance optimizations: + * - Mutable field/point operations to minimize IntArray allocations + * - Precomputed 4-bit window table for generator G multiplication + * - Shamir's trick for verify: s*G + (-e)*P in a single scalar-mul pass + * - Cached BIP-340 tagged hash prefixes + * - Thread-local scratch buffers in field arithmetic */ object Secp256k1 { - /** - * Create a public key from a secret key. - * @param seckey 32-byte secret key - * @return 65-byte uncompressed public key (04 || x || y) - */ - fun pubkeyCreate(seckey: ByteArray): ByteArray { - require(seckey.size == 32) { "Secret key must be 32 bytes" } - val scalar = U256.fromBytes(seckey) - require(ScalarN.isValid(scalar)) { "Invalid secret key" } + // ============ Cached tag hash prefixes for BIP-340 ============ + // SHA256(tag) || SHA256(tag) — precomputed once + private val CHALLENGE_PREFIX: ByteArray by lazy { + val h = sha256("BIP0340/challenge".encodeToByteArray()) + h + h + } + private val AUX_PREFIX: ByteArray by lazy { + val h = sha256("BIP0340/aux".encodeToByteArray()) + h + h + } + private val NONCE_PREFIX: ByteArray by lazy { + val h = sha256("BIP0340/nonce".encodeToByteArray()) + h + h + } - val point = ECPoint.mul(ECPoint.G, scalar) - val (x, y) = ECPoint.toAffine(point) ?: error("Unexpected infinity") + fun pubkeyCreate(seckey: ByteArray): ByteArray { + require(seckey.size == 32) + val scalar = U256.fromBytes(seckey) + require(ScalarN.isValid(scalar)) + val p = MutablePoint() + ECPoint.mulG(p, scalar) + val x = IntArray(8) + val y = IntArray(8) + check(ECPoint.toAffine(p, x, y)) return ECPoint.serializeUncompressed(x, y) } - /** - * Compress a public key. - * @param pubkey 65-byte uncompressed public key or 33-byte compressed - * @return 33-byte compressed public key (02/03 || x) - */ fun pubKeyCompress(pubkey: ByteArray): ByteArray { - val (x, y) = ECPoint.parsePublicKey(pubkey) ?: error("Invalid public key") + val x = IntArray(8) + val y = IntArray(8) + check(ECPoint.parsePublicKey(pubkey, x, y)) return ECPoint.serializeCompressed(x, y) } - /** - * Verify that a secret key is valid (0 < key < n). - * @param seckey secret key bytes - * @return true if valid - */ fun secKeyVerify(seckey: ByteArray): Boolean { if (seckey.size != 32) return false - val scalar = U256.fromBytes(seckey) - return ScalarN.isValid(scalar) + return ScalarN.isValid(U256.fromBytes(seckey)) } - /** - * Create a Schnorr signature per BIP-340. - * @param data 32-byte message hash - * @param seckey 32-byte secret key - * @param auxrand optional 32-byte auxiliary randomness (null for deterministic) - * @return 64-byte signature - */ fun signSchnorr( data: ByteArray, seckey: ByteArray, auxrand: ByteArray?, ): ByteArray { - require(seckey.size == 32) { "Secret key must be 32 bytes" } - - // Step 1-3: Compute keypair, negate secret key if needed + require(seckey.size == 32) val d0 = U256.fromBytes(seckey) - require(ScalarN.isValid(d0)) { "Invalid secret key" } + require(ScalarN.isValid(d0)) - val pubPoint = ECPoint.mul(ECPoint.G, d0) - val (px, py) = ECPoint.toAffine(pubPoint) ?: error("Unexpected infinity") + // Compute public key + val pubPoint = MutablePoint() + ECPoint.mulG(pubPoint, d0) + val px = IntArray(8) + val py = IntArray(8) + check(ECPoint.toAffine(pubPoint, px, py)) val d = if (ECPoint.hasEvenY(py)) d0 else ScalarN.neg(d0) val dBytes = U256.toBytes(d) - val pBytes = U256.toBytes(px) // x-only public key + val pBytes = U256.toBytes(px) - // Step 4: Compute t = xor(d, tagged_hash("BIP0340/aux", auxrand)) + // t = xor(d, tagged_hash("BIP0340/aux", auxrand)) val t = if (auxrand != null) { - require(auxrand.size == 32) { "Aux randomness must be 32 bytes" } - val auxHash = taggedHash("BIP0340/aux", auxrand) - val tArray = U256.fromBytes(dBytes) - val auxArray = U256.fromBytes(auxHash) - U256.toBytes(U256.xor(tArray, auxArray)) + require(auxrand.size == 32) + val auxHash = sha256(AUX_PREFIX + auxrand) + val tArr = IntArray(8) + val auxArr = U256.fromBytes(auxHash) + U256.xorTo(tArr, U256.fromBytes(dBytes), auxArr) + U256.toBytes(tArr) } else { dBytes } - // Step 5: rand = tagged_hash("BIP0340/nonce", t || pBytes || data) - val nonceInput = t + pBytes + data - val rand = taggedHash("BIP0340/nonce", nonceInput) - - // Step 6: k' = int(rand) mod n + // rand = tagged_hash("BIP0340/nonce", t || P || msg) + val rand = sha256(NONCE_PREFIX + t + pBytes + data) val k0 = ScalarN.reduce(U256.fromBytes(rand)) - require(!U256.isZero(k0)) { "Nonce is zero" } + require(!U256.isZero(k0)) - // Step 7: R = k'·G - val rPoint = ECPoint.mul(ECPoint.G, k0) - val (rx, ry) = ECPoint.toAffine(rPoint) ?: error("Unexpected infinity") + // R = k'·G + val rPoint = MutablePoint() + ECPoint.mulG(rPoint, k0) + val rx = IntArray(8) + val ry = IntArray(8) + check(ECPoint.toAffine(rPoint, rx, ry)) - // Step 8: k = k' if has_even_y(R), else n - k' val k = if (ECPoint.hasEvenY(ry)) k0 else ScalarN.neg(k0) - // Step 9: e = int(tagged_hash("BIP0340/challenge", bytes(R) || bytes(P) || msg)) mod n + // e = tagged_hash("BIP0340/challenge", R || P || msg) mod n val rBytes = U256.toBytes(rx) - val challengeInput = rBytes + pBytes + data - val eHash = taggedHash("BIP0340/challenge", challengeInput) + val eHash = sha256(CHALLENGE_PREFIX + rBytes + pBytes + data) val e = ScalarN.reduce(U256.fromBytes(eHash)) - // Step 10: sig = bytes(R) || bytes((k + e*d) mod n) + // sig = R || (k + e*d) mod n val s = ScalarN.add(k, ScalarN.mul(e, d)) val sig = rBytes + U256.toBytes(s) - // Step 11: Verify (optional safety check - can be removed for performance) - require(verifySchnorr(sig, data, pBytes)) { "Signature verification failed" } - + // Safety verify + require(verifySchnorr(sig, data, pBytes)) return sig } - /** - * Verify a Schnorr signature per BIP-340. - * @param signature 64-byte signature - * @param data 32-byte message hash - * @param pub 32-byte x-only public key - * @return true if the signature is valid - */ fun verifySchnorr( signature: ByteArray, data: ByteArray, pub: ByteArray, ): Boolean { - if (signature.size != 64) return false - if (pub.size != 32) return false + if (signature.size != 64 || pub.size != 32) return false - // Step 1: P = lift_x(int(pk)) - val px = U256.fromBytes(pub) - val (pxCoord, pyCoord) = ECPoint.liftX(px) ?: return false + // P = lift_x(pub) + val px = IntArray(8) + val py = IntArray(8) + if (!ECPoint.liftX(px, py, U256.fromBytes(pub))) return false - // Step 2: r = int(sig[0:32]) - val rBytes = signature.copyOfRange(0, 32) - val r = U256.fromBytes(rBytes) + // r, s from signature + val r = U256.fromBytes(signature.copyOfRange(0, 32)) if (U256.cmp(r, FieldP.P) >= 0) return false - - // Step 3: s = int(sig[32:64]) val s = U256.fromBytes(signature.copyOfRange(32, 64)) if (U256.cmp(s, ScalarN.N) >= 0) return false - // Step 4: e = int(tagged_hash("BIP0340/challenge", bytes(r) || bytes(P) || msg)) mod n - val challengeInput = rBytes + pub + data - val eHash = taggedHash("BIP0340/challenge", challengeInput) + // e = tagged_hash("BIP0340/challenge", sig[0:32] || pub || msg) mod n + val eHash = sha256(CHALLENGE_PREFIX + signature.copyOfRange(0, 32) + pub + data) val e = ScalarN.reduce(U256.fromBytes(eHash)) - // Step 5: R = s·G - e·P - val sG = ECPoint.mul(ECPoint.G, s) + // R = s*G - e*P using Shamir's trick (combined as s*G + (-e)*P) val negE = ScalarN.neg(e) - val pJac = JPoint(pxCoord, pyCoord, intArrayOf(1, 0, 0, 0, 0, 0, 0, 0)) - val ePneg = ECPoint.mul(pJac, negE) - val rPoint = ECPoint.add(sG, ePneg) + val pPoint = MutablePoint() + pPoint.setAffine(px, py) + val result = MutablePoint() + ECPoint.mulDoubleG(result, s, pPoint, negE) - // Step 6: Fail if R is infinity, or if R has odd y, or if x(R) != r - if (rPoint.isInfinity()) return false - val (rx, ry) = ECPoint.toAffine(rPoint) ?: return false + // Check: R is not infinity, has even y, and x(R) == r + if (result.isInfinity()) return false + val rx = IntArray(8) + val ry = IntArray(8) + if (!ECPoint.toAffine(result, rx, ry)) return false if (!ECPoint.hasEvenY(ry)) return false - if (U256.cmp(rx, r) != 0) return false - - return true + return U256.cmp(rx, r) == 0 } - /** - * Add a tweak to a private key: (seckey + tweak) mod n. - * @param seckey 32-byte secret key - * @param tweak 32-byte tweak - * @return 32-byte tweaked secret key - */ fun privKeyTweakAdd( seckey: ByteArray, tweak: ByteArray, ): ByteArray { - require(seckey.size == 32) { "Secret key must be 32 bytes" } - require(tweak.size == 32) { "Tweak must be 32 bytes" } - val a = U256.fromBytes(seckey) - val b = U256.fromBytes(tweak) - val result = ScalarN.add(a, b) - require(!U256.isZero(result)) { "Result is zero" } - require(U256.cmp(result, ScalarN.N) < 0) { "Result >= n" } + require(seckey.size == 32 && tweak.size == 32) + val result = ScalarN.add(U256.fromBytes(seckey), U256.fromBytes(tweak)) + require(!U256.isZero(result) && U256.cmp(result, ScalarN.N) < 0) return U256.toBytes(result) } - /** - * Multiply a public key by a tweak (scalar multiplication). - * @param pubkey 33-byte compressed or 65-byte uncompressed public key - * @param tweak 32-byte tweak/scalar - * @return public key in the same format as input - */ fun pubKeyTweakMul( pubkey: ByteArray, tweak: ByteArray, ): ByteArray { - require(tweak.size == 32) { "Tweak must be 32 bytes" } - val (x, y) = ECPoint.parsePublicKey(pubkey) ?: error("Invalid public key") + require(tweak.size == 32) + val x = IntArray(8) + val y = IntArray(8) + check(ECPoint.parsePublicKey(pubkey, x, y)) val scalar = U256.fromBytes(tweak) - require(ScalarN.isValid(scalar)) { "Invalid tweak" } + require(ScalarN.isValid(scalar)) - val point = JPoint(x, y, intArrayOf(1, 0, 0, 0, 0, 0, 0, 0)) - val result = ECPoint.mul(point, scalar) - val (rx, ry) = ECPoint.toAffine(result) ?: error("Result is infinity") + val p = MutablePoint() + p.setAffine(x, y) + val result = MutablePoint() + ECPoint.mul(result, p, scalar) + val rx = IntArray(8) + val ry = IntArray(8) + check(ECPoint.toAffine(result, rx, ry)) return if (pubkey.size == 33) { ECPoint.serializeCompressed(rx, ry) @@ -234,9 +207,7 @@ object Secp256k1 { } } - // ============ Tagged Hash (BIP-340) ============ - - /** BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || msg) */ + /** BIP-340 tagged hash (for non-cached tags) */ internal fun taggedHash( tag: String, msg: ByteArray,