From 040b4ce02a232a7193d5a5db1cd931b9ce18ed47 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 01:10:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=204=C3=9764-bit=20limb=20representation?= =?UTF-8?q?=20with=20Math.multiplyHigh=20(WIP=20=E2=80=94=20breaks=20build?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation layer for the 4×64-bit limb optimization. This commit rewrites U256 from IntArray(8) to LongArray(4) and adds platform-specific Math.multiplyHigh for 64×64→128-bit products: - JVM: Math.multiplyHigh intrinsic (single IMULH/SMULH instruction) - Android API 31+: Math.multiplyHigh, fallback to pure Kotlin on older - Native: pure Kotlin fallback (4 sub-products per multiplyHigh call) The 4×64 representation reduces inner products from 64 to 16 per field multiply on JVM, a potential ~1.5-2× speedup on the critical path. NOTE: This commit intentionally breaks the build — FieldP, ScalarN, Glv, Point, KeyCodec, Secp256k1, and all tests still expect IntArray(8). They will be updated in subsequent commits. https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg --- .../utils/secp256k1/MultiplyHigh.android.kt | 35 ++ .../quartz/utils/secp256k1/MultiplyHigh.kt | 63 ++++ .../quartz/utils/secp256k1/U256.kt | 314 ++++++++---------- .../utils/secp256k1/MultiplyHigh.jvm.kt | 31 ++ .../utils/secp256k1/MultiplyHigh.native.kt | 27 ++ 5 files changed, 298 insertions(+), 172 deletions(-) create mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.android.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.kt create mode 100644 quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.jvm.kt create mode 100644 quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.native.kt diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.android.kt new file mode 100644 index 000000000..96c711979 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.android.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils.secp256k1 + +/** + * Android implementation. Uses Math.multiplyHigh on API 31+ (Android 12), + * falls back to pure Kotlin on older devices. + */ +internal actual fun multiplyHigh( + a: Long, + b: Long, +): Long = + if (android.os.Build.VERSION.SDK_INT >= 31) { + Math.multiplyHigh(a, b) + } else { + multiplyHighFallback(a, b) + } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.kt new file mode 100644 index 000000000..a7af7dea1 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils.secp256k1 + +/** + * Returns the upper 64 bits of the signed 128-bit product of two Long values. + * + * On JVM (Java 9+), this maps to Math.multiplyHigh which the JIT compiles to + * a single SMULH (ARM64) or IMUL (x86-64) instruction. On other platforms, + * a pure-Kotlin fallback computes it via four 32-bit sub-products. + */ +internal expect fun multiplyHigh( + a: Long, + b: Long, +): Long + +/** + * Returns the upper 64 bits of the UNSIGNED 128-bit product of two Long values. + * Built on top of the signed multiplyHigh with a correction for sign bits. + */ +internal fun unsignedMultiplyHigh( + a: Long, + b: Long, +): Long = multiplyHigh(a, b) + (a and (b shr 63)) + (b and (a shr 63)) + +/** + * Pure-Kotlin fallback for multiplyHigh, using four 32-bit sub-products. + * Used on platforms where no hardware intrinsic is available. + */ +internal fun multiplyHighFallback( + a: Long, + b: Long, +): Long { + val aLo = a and 0xFFFFFFFFL + val aHi = a ushr 32 + val bLo = b and 0xFFFFFFFFL + val bHi = b ushr 32 + + val mid1 = aHi * bLo + val mid2 = aLo * bHi + val low = aLo * bLo + + val carry = ((low ushr 32) + (mid1 and 0xFFFFFFFFL) + (mid2 and 0xFFFFFFFFL)) ushr 32 + return aHi * bHi + (mid1 ushr 32) + (mid2 ushr 32) + carry +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt index 88f6baafa..9099e1b03 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt @@ -21,275 +21,245 @@ package com.vitorpamplona.quartz.utils.secp256k1 // ===================================================================================== -// 256-BIT UNSIGNED INTEGER ARITHMETIC FOR secp256k1 +// 256-BIT UNSIGNED INTEGER ARITHMETIC FOR secp256k1 (4×64-bit limbs) // ===================================================================================== // -// This file implements the raw 256-bit unsigned integer arithmetic that underlies all -// secp256k1 operations. It is the foundation of the secp256k1 package, which consists of: +// Numbers are stored as LongArray(4) in little-endian order. Each Long holds 64 bits +// treated as unsigned. Element [0] is the least significant limb. // -// - U256.kt (this file): Raw 256-bit arithmetic (add, subtract, multiply, compare) -// - FieldP.kt: Modular arithmetic mod p (field prime), for point coordinates -// - ScalarN.kt: Modular arithmetic mod n (group order), for keys and signatures -// - Glv.kt: GLV endomorphism and wNAF scalar encoding -// - Point.kt: EC point operations, scalar multiplication (comb, Strauss, GLV) -// - Secp256k1.kt: Public API (sign, verify, key operations) +// This representation uses Math.multiplyHigh (JVM 9+) or a pure-Kotlin fallback to +// compute the upper 64 bits of 64×64→128-bit products. On JVM, this maps to a single +// hardware instruction (IMULH on x86-64, SMULH on ARM64), reducing the inner product +// count from 64 (with 8×32-bit limbs) to 16 per field multiplication. // -// REPRESENTATION -// ============== -// A 256-bit number is stored as IntArray(8) in little-endian order. Each Int holds 32 bits, -// treated as unsigned. Element [0] is the least significant. For example, the number 1 is -// stored as [1, 0, 0, 0, 0, 0, 0, 0]. -// -// We chose 8×32-bit limbs over alternatives like 5×52-bit because Kotlin's Long (64-bit) -// can hold the product of two 32-bit values without overflow (32+32=64 ≤ 63 signed bits -// for most cases). The C reference implementation uses 5×52-bit with compiler-specific -// __int128 which is unavailable on JVM. A future optimization could use 5×52-bit with -// split-product techniques to reduce the inner product count from 64 to ~40. -// -// FIELD REDUCTION -// =============== -// secp256k1's field prime p = 2^256 - 2^32 - 977 has a special sparse form that makes -// modular reduction efficient. After a 512-bit multiplication result, we split into -// lo (256-bit) + hi (256-bit) and use the identity: -// -// hi × 2^256 ≡ hi × (2^32 + 977) (mod p) -// -// This replaces a generic 512-bit mod with a 256×33-bit multiply and add. A second -// round handles any remaining overflow. This is much cheaper than generic Barrett or -// Montgomery reduction because secp256k1's prime was specifically chosen for this property. -// -// MODULAR INVERSION -// ================= -// We use Fermat's little theorem: a^(-1) = a^(p-2) mod p, computed via repeated -// squaring (~255 squarings + ~255 multiplications). This is simple but expensive. -// -// The C reference library uses a faster algorithm called "safegcd" (Bernstein-Yang 2019) -// that computes the modular inverse using ~590 cheap division steps (shifts and additions) -// instead of ~510 field multiplications. Implementing safegcd would make inversion ~10× -// faster, but since inversion only happens once per signature verification (in the final -// Jacobian-to-affine conversion), the total impact on verify throughput is modest (~10%). -// -// PERFORMANCE APPROACH -// ==================== -// Hot-path functions (mul, sqr, add, sub) take an output IntArray parameter to avoid -// allocating a new array on every call. During a single signature verification, the field -// multiplication is called thousands of times — allocating a new IntArray(8) each time -// would create significant GC pressure on Android. Convenience wrappers that allocate -// are provided for non-hot-path code. -// -// A thread-local IntArray(16) scratch buffer is reused across field multiplications to -// avoid allocating a 512-bit intermediate on every mul/sqr call. +// Package structure: U256 → FieldP/ScalarN → Glv/KeyCodec → Point → Secp256k1 // ===================================================================================== /** - * Raw 256-bit unsigned integer arithmetic. - * - * All operations treat IntArray(8) as a 256-bit unsigned integer in little-endian - * limb order. No modular reduction is performed — callers (FieldP, ScalarN) handle that. + * Raw 256-bit unsigned integer arithmetic using 4×64-bit limbs. */ internal object U256 { - val ZERO = IntArray(8) + val ZERO = LongArray(4) - /** Branchless zero check — OR all limbs, avoiding per-limb branching. */ - fun isZero(a: IntArray): Boolean = (a[0] or a[1] or a[2] or a[3] or a[4] or a[5] or a[6] or a[7]) == 0 + fun isZero(a: LongArray): Boolean = (a[0] or a[1] or a[2] or a[3]) == 0L /** Unsigned comparison. Returns -1 if a < b, 0 if equal, 1 if a > b. */ fun cmp( - a: IntArray, - b: IntArray, + a: LongArray, + b: LongArray, ): Int { - for (i in 7 downTo 0) { - val ai = a[i].toLong() and 0xFFFFFFFFL - val bi = b[i].toLong() and 0xFFFFFFFFL - if (ai != bi) return if (ai < bi) -1 else 1 + for (i in 3 downTo 0) { + if (a[i] != b[i]) { + return if (a[i].toULong() < b[i].toULong()) -1 else 1 + } } return 0 } - /** out = a + b. Returns the carry bit (0 or 1). Safe for out aliasing a or b. */ + /** out = a + b. Returns carry (0 or 1). Safe for aliasing. */ fun addTo( - out: IntArray, - a: IntArray, - b: IntArray, + out: LongArray, + a: LongArray, + b: LongArray, ): Int { var carry = 0L - for (i in 0 until 8) { - carry += (a[i].toLong() and 0xFFFFFFFFL) + (b[i].toLong() and 0xFFFFFFFFL) - out[i] = carry.toInt() - carry = carry ushr 32 + for (i in 0 until 4) { + val s1 = a[i] + b[i] + val c1 = if (s1.toULong() < a[i].toULong()) 1L else 0L + val s2 = s1 + carry + val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L + out[i] = s2 + carry = c1 + c2 } return carry.toInt() } - /** out = a - b. Returns the borrow bit (0 or 1). Safe for out aliasing a or b. */ + /** out = a - b. Returns borrow (0 or 1). Safe for aliasing. */ fun subTo( - out: IntArray, - a: IntArray, - b: IntArray, + out: LongArray, + a: LongArray, + b: LongArray, ): Int { var borrow = 0L - for (i in 0 until 8) { - val diff = (a[i].toLong() and 0xFFFFFFFFL) - (b[i].toLong() and 0xFFFFFFFFL) - borrow - out[i] = diff.toInt() - borrow = if (diff < 0) 1L else 0L + for (i in 0 until 4) { + val d1 = a[i] - b[i] + val c1 = if (a[i].toULong() < b[i].toULong()) 1L else 0L + val d2 = d1 - borrow + val c2 = if (d1.toULong() < borrow.toULong()) 1L else 0L + out[i] = d2 + borrow = c1 + c2 } return borrow.toInt() } /** - * Schoolbook multiplication: out = a × b (512-bit result in IntArray(16)). + * 4×4 schoolbook multiplication: out = a × b (512-bit result in LongArray(8)). * - * Uses the standard O(n²) algorithm with 8×8 = 64 inner Long multiplications. - * Each partial product is at most 32×32 = 64 bits, which fits in a signed Long - * with room for carry accumulation. - * - * Note: Karatsuba (splitting into 4-limb halves for 48 products) was attempted - * but the overhead of extra additions, carry propagation, and 5 temporary array - * allocations per call negates the product-count savings at only 8 limbs. + * Uses unsignedMultiplyHigh for the upper 64 bits of each 64×64→128-bit product. + * On JVM, this is a hardware intrinsic (single instruction). Total: 16 products + * vs 64 for the previous 8×32-bit representation. */ fun mulWide( - out: IntArray, - a: IntArray, - b: IntArray, + out: LongArray, + a: LongArray, + b: LongArray, ) { - for (i in 0 until 16) out[i] = 0 - for (i in 0 until 8) { + for (i in 0 until 8) out[i] = 0L + + for (i in 0 until 4) { var carry = 0L - val ai = a[i].toLong() and 0xFFFFFFFFL - for (j in 0 until 8) { - val prod = ai * (b[j].toLong() and 0xFFFFFFFFL) + (out[i + j].toLong() and 0xFFFFFFFFL) + carry - out[i + j] = prod.toInt() - carry = prod ushr 32 + val ai = a[i] + for (j in 0 until 4) { + val lo = ai * b[j] + val hi = unsignedMultiplyHigh(ai, b[j]) + + val prev = out[i + j] + val s1 = prev + lo + val c1 = if (s1.toULong() < prev.toULong()) 1L else 0L + val s2 = s1 + carry + val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L + out[i + j] = s2 + carry = hi + c1 + c2 } - out[i + 8] = carry.toInt() + out[i + 4] = carry } } /** - * Dedicated squaring: out = a² (512-bit result in IntArray(16)). - * - * Exploits the identity a²[i,j] = a²[j,i] to compute each cross-product once - * and double it, reducing from 64 to 36 multiplications: - * - 28 cross-products (i < j), doubled - * - 8 diagonal products (i == i) - * - * This gives ~40% fewer multiplications than generic mulWide for squaring. + * Dedicated squaring: out = a² (512-bit result in LongArray(8)). + * Exploits symmetry: 6 cross-products doubled + 4 diagonal = 10 multiplyHigh calls. */ fun sqrWide( - out: IntArray, - a: IntArray, + out: LongArray, + a: LongArray, ) { - for (i in 0 until 16) out[i] = 0 + for (i in 0 until 8) out[i] = 0L - // Pass 1: accumulate cross-products a[i]*a[j] for i < j (single, not doubled yet) - for (i in 0 until 8) { + // Pass 1: cross-products a[i]*a[j] for i < j (single) + for (i in 0 until 4) { var carry = 0L - val ai = a[i].toLong() and 0xFFFFFFFFL - for (j in i + 1 until 8) { - val prod = ai * (a[j].toLong() and 0xFFFFFFFFL) + (out[i + j].toLong() and 0xFFFFFFFFL) + carry - out[i + j] = prod.toInt() - carry = prod ushr 32 + val ai = a[i] + for (j in i + 1 until 4) { + val lo = ai * a[j] + val hi = unsignedMultiplyHigh(ai, a[j]) + val prev = out[i + j] + val s1 = prev + lo + val c1 = if (s1.toULong() < prev.toULong()) 1L else 0L + val s2 = s1 + carry + val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L + out[i + j] = s2 + carry = hi + c1 + c2 } - out[i + 8] = carry.toInt() + out[i + 4] = carry } - // Pass 2: double all cross-products (shift entire 512-bit result left by 1 bit) - var shiftCarry = 0 - for (i in 1 until 16) { + // Pass 2: double all cross-products (shift left by 1 bit) + var shiftCarry = 0L + for (i in 1 until 8) { val v = out[i] out[i] = (v shl 1) or shiftCarry - shiftCarry = v ushr 31 + shiftCarry = v ushr 63 } - // Pass 3: add diagonal products a[i]² at positions 2i and 2i+1 + // Pass 3: add diagonal products a[i]² var dCarry = 0L - for (i in 0 until 8) { - val ai = a[i].toLong() and 0xFFFFFFFFL - val diag = ai * ai + for (i in 0 until 4) { + val lo = a[i] * a[i] + val hi = unsignedMultiplyHigh(a[i], a[i]) val pos = 2 * i - dCarry += (out[pos].toLong() and 0xFFFFFFFFL) + (diag and 0xFFFFFFFFL) - out[pos] = dCarry.toInt() - dCarry = dCarry ushr 32 - dCarry += (out[pos + 1].toLong() and 0xFFFFFFFFL) + (diag ushr 32) - out[pos + 1] = dCarry.toInt() - dCarry = dCarry ushr 32 + + val s1 = out[pos] + lo + val c1 = if (s1.toULong() < out[pos].toULong()) 1L else 0L + val s2 = s1 + dCarry + val c2 = if (s2.toULong() < s1.toULong()) 1L else 0L + out[pos] = s2 + + val s3 = out[pos + 1] + hi + val c3 = if (s3.toULong() < out[pos + 1].toULong()) 1L else 0L + val s4 = s3 + c1 + c2 + val c4 = if (s4.toULong() < s3.toULong()) 1L else 0L + out[pos + 1] = s4 + dCarry = c3 + c4 } } // ==================== Serialization ==================== - /** Decode a big-endian 32-byte array into little-endian IntArray(8). */ - fun fromBytes(bytes: ByteArray): IntArray = fromBytes(bytes, 0) + /** Decode big-endian 32 bytes into LongArray(4) little-endian limbs. */ + fun fromBytes(bytes: ByteArray): LongArray = fromBytes(bytes, 0) - /** Decode 32 big-endian bytes starting at [offset] into little-endian IntArray(8). */ fun fromBytes( bytes: ByteArray, offset: Int, - ): IntArray { - val r = IntArray(8) - for (i in 0 until 8) { - val o = offset + 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) + ): LongArray { + val r = LongArray(4) + for (i in 0 until 4) { + val o = offset + 24 - i * 8 + r[i] = ((bytes[o].toLong() and 0xFF) shl 56) or + ((bytes[o + 1].toLong() and 0xFF) shl 48) or + ((bytes[o + 2].toLong() and 0xFF) shl 40) or + ((bytes[o + 3].toLong() and 0xFF) shl 32) or + ((bytes[o + 4].toLong() and 0xFF) shl 24) or + ((bytes[o + 5].toLong() and 0xFF) shl 16) or + ((bytes[o + 6].toLong() and 0xFF) shl 8) or + (bytes[o + 7].toLong() and 0xFF) } return r } - /** Encode little-endian IntArray(8) to a big-endian 32-byte array. */ - fun toBytes(a: IntArray): ByteArray { + fun toBytes(a: LongArray): ByteArray { val r = ByteArray(32) toBytesInto(a, r, 0) return r } - /** Encode into an existing byte array at the given offset. Avoids allocation. */ fun toBytesInto( - a: IntArray, + a: LongArray, 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() + for (i in 0 until 4) { + val o = offset + 24 - i * 8 + dest[o] = (a[i] ushr 56).toByte() + dest[o + 1] = (a[i] ushr 48).toByte() + dest[o + 2] = (a[i] ushr 40).toByte() + dest[o + 3] = (a[i] ushr 32).toByte() + dest[o + 4] = (a[i] ushr 24).toByte() + dest[o + 5] = (a[i] ushr 16).toByte() + dest[o + 6] = (a[i] ushr 8).toByte() + dest[o + 7] = a[i].toByte() } } // ==================== Bit manipulation ==================== - /** Extract 4-bit nibble at position pos (0 = lowest nibble). Used by windowed scalar mul. */ + /** Extract 4-bit nibble at position pos (0 = lowest nibble). */ fun getNibble( - a: IntArray, + a: LongArray, pos: Int, ): Int { - val limb = pos / 8 - val shift = (pos % 8) * 4 - return (a[limb] ushr shift) and 0xF + val limb = pos / 16 + val shift = (pos % 16) * 4 + return ((a[limb] ushr shift) and 0xF).toInt() } - /** Test if bit at position pos is set (0 = LSB). */ + /** Test if bit at position pos is set. */ fun testBit( - a: IntArray, + a: LongArray, pos: Int, - ): Boolean = (a[pos / 32] ushr (pos % 32)) and 1 == 1 + ): Boolean = (a[pos / 64] ushr (pos % 64)) and 1L == 1L - /** out = a XOR b. Used by BIP-340 signing for nonce derivation. */ fun xorTo( - out: IntArray, - a: IntArray, - b: IntArray, + out: LongArray, + a: LongArray, + b: LongArray, ) { - for (i in 0 until 8) out[i] = a[i] xor b[i] + for (i in 0 until 4) out[i] = a[i] xor b[i] } - /** Copy the contents of a into out. */ fun copyInto( - out: IntArray, - a: IntArray, + out: LongArray, + a: LongArray, ) { a.copyInto(out) } diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.jvm.kt new file mode 100644 index 000000000..8541f81ac --- /dev/null +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.jvm.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils.secp256k1 + +/** + * JVM implementation using Math.multiplyHigh (Java 9+). + * The HotSpot JIT compiles this to a single hardware instruction: + * IMULH on x86-64, SMULH on ARM64. + */ +internal actual fun multiplyHigh( + a: Long, + b: Long, +): Long = Math.multiplyHigh(a, b) diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.native.kt new file mode 100644 index 000000000..24109c9ef --- /dev/null +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/MultiplyHigh.native.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils.secp256k1 + +/** Native implementation using the pure-Kotlin fallback (no hardware intrinsic). */ +internal actual fun multiplyHigh( + a: Long, + b: Long, +): Long = multiplyHighFallback(a, b)