feat: 4×64-bit limb representation with Math.multiplyHigh (WIP — breaks build)

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
This commit is contained in:
Claude
2026-04-06 01:10:10 +00:00
parent 9a8ed53279
commit 040b4ce02a
5 changed files with 298 additions and 172 deletions
@@ -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)
}
@@ -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
}
@@ -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)
}
@@ -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)
@@ -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)