perf: optimize secp256k1 for minimum allocations and maximum verify throughput

Key optimizations:

1. Mutable field operations: All FieldP hot-path methods (add, sub, mul, sqr)
   now write into caller-provided output arrays instead of allocating new ones.
   Thread-local IntArray(16) scratch for mulWide avoids per-mul allocation.

2. Mutable point operations: MutablePoint replaces immutable JPoint. Point
   doubling/addition write into output points. Aliasing protection via
   thread-local copy buffer for in-place doublePoint(out, out).

3. 4-bit windowed scalar multiplication: Processes 4 bits per iteration
   (16 table entries) instead of 1 bit. Reduces point additions by ~4x.

4. Precomputed G table: Static lazy table of 16*G multiples. Generator
   multiplication (signing, key creation) uses precomputed table directly.

5. Shamir's trick (mulDoubleG): Computes s*G + e*P in a single pass for
   verification, eliminating the need for two separate scalar multiplications.
   This roughly halves the cost of verifySchnorr.

6. Cached BIP-340 tag hashes: SHA256("BIP0340/challenge") etc. computed
   once and reused, eliminating 2 SHA256 calls per verify.

7. toBytesInto: Writes directly into existing ByteArray at offset,
   avoiding intermediate allocations in serialization.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-05 13:58:21 +00:00
parent 4c3b31fe8e
commit 4d55dfcff5
3 changed files with 826 additions and 610 deletions
@@ -25,13 +25,16 @@ package com.vitorpamplona.quartz.utils.secp256k1
* *
* Numbers are represented as IntArray(8) in little-endian order where each element * 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. * 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 { internal object U256 {
val ZERO = IntArray(8) val ZERO = IntArray(8)
fun isZero(a: IntArray): Boolean { fun isZero(a: IntArray): Boolean {
for (i in 0 until 8) if (a[i] != 0) return false // Merge all limbs to avoid branches
return true 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 */ /** 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) { for (i in 7 downTo 0) {
val ai = a[i].toLong() and 0xFFFFFFFFL val ai = a[i].toLong() and 0xFFFFFFFFL
val bi = b[i].toLong() and 0xFFFFFFFFL val bi = b[i].toLong() and 0xFFFFFFFFL
if (ai < bi) return -1 if (ai != bi) return if (ai < bi) -1 else 1
if (ai > bi) return 1
} }
return 0 return 0
} }
/** a + b, returns (result, carry) where carry is 0 or 1 */ /** a + b -> out, returns carry (0 or 1) */
fun addCarry( fun addTo(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): Pair<IntArray, Int> { ): Int {
val r = IntArray(8)
var carry = 0L var carry = 0L
for (i in 0 until 8) { for (i in 0 until 8) {
carry += (a[i].toLong() and 0xFFFFFFFFL) + (b[i].toLong() and 0xFFFFFFFFL) carry += (a[i].toLong() and 0xFFFFFFFFL) + (b[i].toLong() and 0xFFFFFFFFL)
r[i] = carry.toInt() out[i] = carry.toInt()
carry = carry ushr 32 carry = carry ushr 32
} }
return Pair(r, carry.toInt()) return carry.toInt()
} }
/** a - b, returns (result, borrow) where borrow is 0 or 1 */ /** a - b -> out, returns borrow (0 or 1) */
fun subBorrow( fun subTo(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): Pair<IntArray, Int> { ): Int {
val r = IntArray(8)
var borrow = 0L var borrow = 0L
for (i in 0 until 8) { for (i in 0 until 8) {
val diff = (a[i].toLong() and 0xFFFFFFFFL) - (b[i].toLong() and 0xFFFFFFFFL) - borrow 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 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( fun mulWide(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): IntArray { ) {
val r = IntArray(16) for (i in 0 until 16) out[i] = 0
for (i in 0 until 8) { for (i in 0 until 8) {
var carry = 0L var carry = 0L
val ai = a[i].toLong() and 0xFFFFFFFFL val ai = a[i].toLong() and 0xFFFFFFFFL
for (j in 0 until 8) { for (j in 0 until 8) {
val prod = ai * (b[j].toLong() and 0xFFFFFFFFL) + (r[i + j].toLong() and 0xFFFFFFFFL) + carry val prod = ai * (b[j].toLong() and 0xFFFFFFFFL) + (out[i + j].toLong() and 0xFFFFFFFFL) + carry
r[i + j] = prod.toInt() out[i + j] = prod.toInt()
carry = prod ushr 32 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 */ /** Convert big-endian 32-byte array to IntArray(8) little-endian limbs */
fun fromBytes(bytes: ByteArray): IntArray { fun fromBytes(bytes: ByteArray): IntArray {
require(bytes.size == 32) { "Expected 32 bytes, got ${bytes.size}" } require(bytes.size == 32)
val r = IntArray(8) val r = IntArray(8)
for (i in 0 until 8) { for (i in 0 until 8) {
val offset = 28 - i * 4 val o = 28 - i * 4
r[i] = ((bytes[offset].toInt() and 0xFF) shl 24) or r[i] = ((bytes[o].toInt() and 0xFF) shl 24) or
((bytes[offset + 1].toInt() and 0xFF) shl 16) or ((bytes[o + 1].toInt() and 0xFF) shl 16) or
((bytes[offset + 2].toInt() and 0xFF) shl 8) or ((bytes[o + 2].toInt() and 0xFF) shl 8) or
(bytes[offset + 3].toInt() and 0xFF) (bytes[o + 3].toInt() and 0xFF)
} }
return r return r
} }
@@ -131,44 +117,69 @@ internal object U256 {
fun toBytes(a: IntArray): ByteArray { fun toBytes(a: IntArray): ByteArray {
val r = ByteArray(32) val r = ByteArray(32)
for (i in 0 until 8) { for (i in 0 until 8) {
val offset = 28 - i * 4 val o = 28 - i * 4
r[offset] = (a[i] ushr 24).toByte() r[o] = (a[i] ushr 24).toByte()
r[offset + 1] = (a[i] ushr 16).toByte() r[o + 1] = (a[i] ushr 16).toByte()
r[offset + 2] = (a[i] ushr 8).toByte() r[o + 2] = (a[i] ushr 8).toByte()
r[offset + 3] = a[i].toByte() r[o + 3] = a[i].toByte()
} }
return r 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( fun testBit(
a: IntArray, a: IntArray,
pos: Int, pos: Int,
): Boolean { ): Boolean = (a[pos / 32] ushr (pos % 32)) and 1 == 1
val limb = pos / 32
val bit = pos % 32
return (a[limb] ushr bit) and 1 == 1
}
/** XOR two 256-bit values */ /** XOR: out = a xor b */
fun xor( fun xorTo(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): IntArray { ) {
val r = IntArray(8) for (i in 0 until 8) out[i] = a[i] xor b[i]
for (i in 0 until 8) r[i] = a[i] xor b[i]
return r
} }
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). * Field arithmetic modulo p = 2^256 - 2^32 - 977.
* This is the base field of the secp256k1 curve. * All hot-path operations write results into caller-provided output arrays.
*/ */
internal object FieldP { internal object FieldP {
// p = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
val P = val P =
intArrayOf( intArrayOf(
0xFFFFFC2F.toInt(), 0xFFFFFC2F.toInt(),
@@ -181,133 +192,145 @@ internal object FieldP {
0xFFFFFFFF.toInt(), 0xFFFFFFFF.toInt(),
) )
// 2^256 mod p = 2^32 + 977 = 0x1000003D1 (fits in 33 bits) // Thread-local scratch space to avoid allocation in hot path
// As limbs: [977, 1, 0, 0, 0, 0, 0, 0] // Since Kotlin/JVM coroutines are cooperative (not preemptive on same thread),
private val P_COMPLEMENT_LIMBS = intArrayOf(977, 1, 0, 0, 0, 0, 0, 0) // 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) */ /** Reduce in-place: if a >= p, subtract p */
fun reduce(a: IntArray): IntArray = fun reduceSelf(a: IntArray) {
if (U256.cmp(a, P) >= 0) { if (U256.cmp(a, P) >= 0) {
U256.subBorrow(a, P).first U256.subTo(a, a, P)
} else { }
a
} }
/** Reduce a wide 512-bit product mod p */ /** Reduce a wide 512-bit value in w[0..15] -> out[0..7] mod p */
fun reduceWide(w: IntArray): IntArray { fun reduceWide(
// w = hi * 2^256 + lo out: IntArray,
// ≡ lo + hi * (2^32 + 977) (mod p) w: IntArray,
// ) {
// w ≡ lo + hi * (2^32 + 977) (mod p)
// Split: hi * (2^32 + 977) = (hi << 32) + hi * 977 // 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) // Compute: out = lo + hi*977 + (hi << 32)
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)
var carry = 0L var carry = 0L
for (i in 0 until 8) { for (i in 0 until 8) {
carry += (lo[i].toLong() and 0xFFFFFFFFL) + // lo[i]
(hiTimes977[i].toLong() and 0xFFFFFFFFL) + carry += (w[i].toLong() and 0xFFFFFFFFL)
if (i > 0) (hi[i - 1].toLong() and 0xFFFFFFFFL) else 0L // hi[i] * 977
result[i] = carry.toInt() 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 carry = carry ushr 32
} }
// Remaining overflow: carry + hiTimes977[8] + hi[7] // Remaining: carry + hi[7]
var overflow = var overflow = carry + (w[15].toLong() and 0xFFFFFFFFL)
carry +
(hiTimes977[8].toLong() and 0xFFFFFFFFL) +
(hi[7].toLong() and 0xFFFFFFFFL)
// Second round: overflow * (2^32 + 977) // Second round: overflow * (2^32 + 977)
// overflow is at most ~35 bits, so overflow * 977 fits in Long easily
if (overflow > 0) { if (overflow > 0) {
val ov977 = overflow * 977L val ov977 = overflow * 977L
var c2 = 0L var c2 = 0L
// Add ov977 to result[0..1], and overflow to result[1..2] (the <<32 part)
for (i in 0 until 8) { 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 == 0) c2 += (ov977 and 0xFFFFFFFFL)
if (i == 1) c2 += (ov977 ushr 32) + (overflow and 0xFFFFFFFFL) if (i == 1) c2 += (ov977 ushr 32) + (overflow and 0xFFFFFFFFL)
if (i == 2) c2 += (overflow ushr 32) if (i == 2) c2 += (overflow ushr 32)
result[i] = c2.toInt() out[i] = c2.toInt()
c2 = c2 ushr 32 c2 = c2 ushr 32
} }
// c2 should be 0 or very small; if > 0, do a third tiny round
if (c2 > 0) { if (c2 > 0) {
val tiny = c2 * 977L val tiny = c2 * 977L
var c3 = 0L var c3 = 0L
for (i in 0 until 3) { 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 == 0) c3 += (tiny and 0xFFFFFFFFL)
if (i == 1) c3 += (tiny ushr 32) + (c2 and 0xFFFFFFFFL) if (i == 1) c3 += (tiny ushr 32) + (c2 and 0xFFFFFFFFL)
if (i == 2) c3 += (c2 ushr 32) if (i == 2) c3 += (c2 ushr 32)
result[i] = c3.toInt() out[i] = c3.toInt()
c3 = c3 ushr 32 c3 = c3 ushr 32
} }
} }
} }
reduceSelf(out)
// Final reduction: result might still be >= p
return reduce(result)
} }
/** out = a + b mod p */
fun add( fun add(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): IntArray { ) {
val (sum, carry) = U256.addCarry(a, b) val carry = U256.addTo(out, a, b)
return if (carry != 0) { if (carry != 0) {
// sum + 2^256 ≡ sum + (2^32 + 977) (mod p) // out + 2^256 ≡ out + (2^32 + 977) mod p
// carry is always 1 here, so just add the constant var c = 977L + (out[0].toLong() and 0xFFFFFFFFL)
val (r2, c2) = U256.addCarry(sum, P_COMPLEMENT_LIMBS) out[0] = c.toInt()
if (c2 != 0) reduce(U256.addCarry(r2, P_COMPLEMENT_LIMBS).first) else reduce(r2) c = c ushr 32
} else { c += 1L + (out[1].toLong() and 0xFFFFFFFFL)
reduce(sum) 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( fun sub(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): IntArray { ) {
val (diff, borrow) = U256.subBorrow(a, b) val borrow = U256.subTo(out, a, b)
return if (borrow != 0) { if (borrow != 0) {
// Add p back U256.addTo(out, out, P)
U256.addCarry(diff, P).first
} else {
diff
} }
} }
/** out = a * b mod p. Uses thread-local scratch space. */
fun mul( fun mul(
out: IntArray,
a: IntArray, a: IntArray,
b: IntArray, b: IntArray,
): IntArray = reduceWide(U256.mulWide(a, b)) ) {
val w = wide.get()
fun sqr(a: IntArray): IntArray = mul(a, a) U256.mulWide(w, a, b)
reduceWide(out, w)
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 */ /** out = a² mod p. Uses thread-local scratch space. */
fun inv(a: IntArray): IntArray { fun sqr(
require(!U256.isZero(a)) { "Cannot invert zero" } out: IntArray,
// p - 2 = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2D a: IntArray,
// Use square-and-multiply with an optimized addition chain for secp256k1 ) {
return powModP(a, P_MINUS_2) 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 = private val P_MINUS_2 =
intArrayOf( intArrayOf(
0xFFFFFC2D.toInt(), 0xFFFFFC2D.toInt(),
@@ -320,8 +343,6 @@ internal object FieldP {
0xFFFFFFFF.toInt(), 0xFFFFFFFF.toInt(),
) )
// (p + 1) / 4 = 3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF0C
// Used for computing square roots since p ≡ 3 (mod 4)
private val P_PLUS_1_DIV_4 = private val P_PLUS_1_DIV_4 =
intArrayOf( intArrayOf(
0xBFFFFF0C.toInt(), 0xBFFFFF0C.toInt(),
@@ -334,39 +355,111 @@ internal object FieldP {
0x3FFFFFFF, 0x3FFFFFFF,
) )
/** Square root mod p. Returns null if a is not a quadratic residue. */ /** out = sqrt(a) mod p, returns false if not a QR */
fun sqrt(a: IntArray): IntArray? { fun sqrt(
// Since p ≡ 3 (mod 4), sqrt(a) = a^((p+1)/4) mod p out: IntArray,
val r = powModP(a, P_PLUS_1_DIV_4) a: IntArray,
// Verify: r^2 == a (mod p) ): Boolean {
return if (U256.cmp(mul(r, r), reduce(a)) == 0) r else null 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( private fun powModP(
out: IntArray,
base: IntArray, base: IntArray,
exp: IntArray, exp: IntArray,
): IntArray { ) {
var result = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0) // 1 // Left-to-right square-and-multiply
var b = base.copyOf() val b = IntArray(8)
// Find highest set bit U256.copyInto(b, base)
var highBit = 255 var highBit = 255
while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit-- 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)) { if (U256.testBit(exp, i)) {
result = mul(result, b) mul(out, out, b) // out = out * base
}
if (i < highBit) {
b = sqr(b)
} }
} }
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). * Scalar arithmetic modulo n (the order of the secp256k1 group).
* n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
*/ */
internal object ScalarN { internal object ScalarN {
val N = val N =
@@ -381,99 +474,6 @@ internal object ScalarN {
0xFFFFFFFF.toInt(), 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 = private val N_COMPLEMENT =
intArrayOf( intArrayOf(
0x2FC9BEBF.toInt(), 0x2FC9BEBF.toInt(),
@@ -486,46 +486,6 @@ internal object ScalarN {
0, 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 = private val N_MINUS_2 =
intArrayOf( intArrayOf(
0xD036413F.toInt(), 0xD036413F.toInt(),
@@ -538,20 +498,155 @@ internal object ScalarN {
0xFFFFFFFF.toInt(), 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( private fun powModN(
base: IntArray, base: IntArray,
exp: IntArray, exp: IntArray,
): IntArray { ): IntArray {
var result = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0) val result = IntArray(8)
var b = base.copyOf() val b = base.copyOf()
var highBit = 255 var highBit = 255
while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit-- while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit--
for (i in 0..highBit) { if (highBit < 0) {
if (U256.testBit(exp, i)) { result[0] = 1
result = mul(result, b) return result
} }
if (i < highBit) { U256.copyInto(result, b)
b = mul(b, b) for (i in highBit - 1 downTo 0) {
val sq = mul(result, result)
U256.copyInto(result, sq)
if (U256.testBit(exp, i)) {
val prod = mul(result, b)
U256.copyInto(result, prod)
} }
} }
return result return result
@@ -21,24 +21,46 @@
package com.vitorpamplona.quartz.utils.secp256k1 package com.vitorpamplona.quartz.utils.secp256k1
/** /**
* Elliptic curve point operations on the secp256k1 curve: y² = x³ + 7 (mod p). * Mutable Jacobian point for in-place computation.
* Uses Jacobian coordinates (X, Y, Z) where affine (x, y) = (X/Z², Y/Z³). * (X, Y, Z) represents affine (X/Z², Y/Z³). Infinity: Z = 0.
* The point at infinity is represented by Z = 0.
*/ */
internal class JPoint( internal class MutablePoint(
val x: IntArray, val x: IntArray = IntArray(8),
val y: IntArray, val y: IntArray = IntArray(8),
val z: IntArray, val z: IntArray = IntArray(8),
) { ) {
companion object { fun isInfinity(): Boolean = U256.isZero(z)
val INFINITY = JPoint(IntArray(8), intArrayOf(1, 0, 0, 0, 0, 0, 0, 0), IntArray(8))
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 { internal object ECPoint {
// Generator point G
val GX = val GX =
intArrayOf( intArrayOf(
0x16F81798.toInt(), 0x16F81798.toInt(),
@@ -61,233 +83,361 @@ internal object ECPoint {
0x26A3C465.toInt(), 0x26A3C465.toInt(),
0x483ADA77.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 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
* Point doubling in Jacobian coordinates. // Lazily initialized on first use.
* Formula from https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l private val gTable: Array<MutablePoint> by lazy { buildGTable() }
*/
fun double(p: JPoint): JPoint {
if (p.isInfinity()) return JPoint.INFINITY
val x = p.x private fun buildGTable(): Array<MutablePoint> {
val y = p.y val table = Array(16) { MutablePoint() }
val z = p.z // table[0] = G
table[0].setAffine(GX, GY)
val a = FieldP.sqr(x) // A = X1² // table[i] = table[i-1] + G
val b = FieldP.sqr(y) // B = Y1² val tmp = MutablePoint()
val c = FieldP.sqr(b) // C = B² for (i in 1 until 16) {
val xPlusB = FieldP.add(x, b) addPoints(table[i], table[i - 1], table[0])
val d = }
FieldP.sub( return Array(16) { table[it].snapshot() }
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)
} }
// ============ 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 addition in Jacobian coordinates. * Point doubling: out = 2*p.
* Mixed addition when q.z = 1 (affine point) for efficiency. * Formula: dbl-2009-l from https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html
*/ */
fun add( fun doublePoint(
p: JPoint, out: MutablePoint,
q: JPoint, inp: MutablePoint,
): JPoint { ) {
if (p.isInfinity()) return q if (inp.isInfinity()) {
if (q.isInfinity()) return p out.setInfinity()
return
val z1sq = FieldP.sqr(p.z) }
val z2sq = FieldP.sqr(q.z) val s = scratch.get()
// If out aliases inp, copy inp to scratch first
val u1 = FieldP.mul(p.x, z2sq) // U1 = X1*Z2² val p =
val u2 = FieldP.mul(q.x, z1sq) // U2 = X2*Z1² if (out === inp) {
val s1 = FieldP.mul(p.y, FieldP.mul(q.z, z2sq)) // S1 = Y1*Z2³ s.dblCopy.copyFrom(inp)
val s2 = FieldP.mul(q.y, FieldP.mul(p.z, z1sq)) // S2 = Y2*Z1³ s.dblCopy
if (U256.cmp(u1, u2) == 0) {
return if (U256.cmp(s1, s2) == 0) {
double(p) // Same point
} else { } else {
JPoint.INFINITY // Inverse points inp
} }
} val t = s.t
// t0=A=X², t1=B=Y², t2=C=B², t3=(X+B)²
val h = FieldP.sub(u2, u1) // H = U2 - U1 FieldP.sqr(t[0], p.x)
val i = FieldP.sqr(FieldP.add(h, h)) // I = (2*H)² FieldP.sqr(t[1], p.y)
val j = FieldP.mul(h, i) // J = H*I FieldP.sqr(t[2], t[1])
val r = FieldP.add(t[3], p.x, t[1])
FieldP.add( FieldP.sqr(t[3], t[3])
FieldP.sub(s2, s1), // t3 = D = 2*((X+B)²-A-C)
FieldP.sub(s2, s1), FieldP.sub(t[3], t[3], t[0])
) // r = 2*(S2-S1) FieldP.sub(t[3], t[3], t[2])
val v = FieldP.mul(u1, i) // V = U1*I FieldP.add(t[3], t[3], t[3]) // D
// t4 = E = 3*A
val x3 = FieldP.add(t[4], t[0], t[0])
FieldP.sub( FieldP.add(t[4], t[4], t[0])
FieldP.sub(FieldP.sqr(r), j), // t5 = F = E²
FieldP.add(v, v), FieldP.sqr(t[5], t[4])
) // X3 = r² - J - 2*V // X3 = F - 2*D
val y3 = FieldP.add(t[6], t[3], t[3]) // 2D
FieldP.sub( FieldP.sub(out.x, t[5], t[6])
FieldP.mul(r, FieldP.sub(v, x3)), // Y3 = E*(D-X3) - 8*C
FieldP.add(FieldP.mul(s1, j), FieldP.mul(s1, j)), FieldP.sub(t[7], t[3], out.x)
) // Y3 = r*(V-X3) - 2*S1*J FieldP.mul(t[7], t[4], t[7])
val z3 = FieldP.add(t[2], t[2], t[2]) // 2C
FieldP.mul( FieldP.add(t[2], t[2], t[2]) // 4C
FieldP.sub( FieldP.add(t[2], t[2], t[2]) // 8C
FieldP.sub( FieldP.sub(out.y, t[7], t[2])
FieldP.sqr(FieldP.add(p.z, q.z)), // Z3 = 2*Y*Z
z1sq, FieldP.add(t[8], p.y, p.z)
), FieldP.sqr(t[8], t[8])
z2sq, FieldP.sub(t[8], t[8], t[1]) // -B
), FieldP.sqr(t[9], p.z)
h, FieldP.sub(out.z, t[8], t[9])
) // Z3 = ((Z1+Z2)²-Z1²-Z2²)*H
return JPoint(x3, y3, z3)
} }
/** /**
* Scalar multiplication using double-and-add (left-to-right). * Point addition: out = p + q. Handles p==q (doubling) and inverses.
*/
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
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³
if (U256.cmp(t[2], t[3]) == 0) {
if (U256.cmp(t[4], t[5]) == 0) {
doublePoint(out, p)
} else {
out.setInfinity()
}
return
}
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: out = scalar * p, using 4-bit windowed method.
*/ */
fun mul( fun mul(
p: JPoint, out: MutablePoint,
p: MutablePoint,
scalar: IntArray, 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 // Build 4-bit window table: table[i] = (i+1)*p for i in 0..15
// Find highest set bit val table = Array(16) { MutablePoint() }
var highBit = 255 table[0].copyFrom(p)
while (highBit >= 0 && !U256.testBit(scalar, highBit)) highBit-- val tmp = MutablePoint()
for (i in 1 until 16) {
addPoints(table[i], table[i - 1], p)
}
for (i in highBit downTo 0) { out.setInfinity()
result = double(result) // Process 4 bits at a time, MSB first (64 nibbles for 256 bits)
if (U256.testBit(scalar, i)) { for (nibbleIdx in 63 downTo 0) {
result = add(result, p) // 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. * G multiplication using precomputed table: out = scalar * G.
* Returns null if the point is at infinity. * Uses 4-bit windowed method with static precomputed table.
*/ */
fun toAffine(p: JPoint): Pair<IntArray, IntArray>? { fun mulG(
if (p.isInfinity()) return null out: MutablePoint,
val zInv = FieldP.inv(p.z) scalar: IntArray,
val zInv2 = FieldP.sqr(zInv) ) {
val zInv3 = FieldP.mul(zInv2, zInv) if (U256.isZero(scalar)) {
val x = FieldP.mul(p.x, zInv2) out.setInfinity()
val y = FieldP.mul(p.y, zInv3) return
return Pair(x, y) }
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)
}
}
} }
/** /**
* Lift x-coordinate to a point on the curve. * Shamir's trick: out = s*G + e*P in a single pass.
* Returns the point with even y if it exists, null otherwise. * Much faster than computing s*G and e*P separately for verification.
* Used by BIP-340 for x-only public keys.
*/ */
fun liftX(x: IntArray): Pair<IntArray, IntArray>? { fun mulDoubleG(
// Check x < p out: MutablePoint,
if (U256.cmp(x, FieldP.P) >= 0) return null 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()
// y² = x³ + 7 out.setInfinity()
val x3 = FieldP.mul(FieldP.sqr(x), x) for (nibbleIdx in 63 downTo 0) {
val y2 = FieldP.add(x3, B) doublePoint(out, out)
val y = FieldP.sqrt(y2) ?: return null doublePoint(out, out)
doublePoint(out, out)
// Return the even-y variant doublePoint(out, out)
val yBytes = U256.toBytes(y) val sNib = U256.getNibble(s, nibbleIdx)
return if (yBytes[31].toInt() and 1 == 0) { val eNib = U256.getNibble(e, nibbleIdx)
Pair(x, y) if (sNib != 0) {
} else { addPoints(tmp, out, gTab[sNib - 1])
Pair(x, FieldP.neg(y)) out.copyFrom(tmp)
}
if (eNib != 0) {
addPoints(tmp, out, pTable[eNib - 1])
out.copyFrom(tmp)
}
} }
} }
/** Check if y coordinate is even */ /** Convert Jacobian to affine. Writes x, y into outX, outY. Returns false if infinity. */
fun hasEvenY(y: IntArray): Boolean { fun toAffine(
// y is even if the least significant bit is 0 p: MutablePoint,
return y[0] and 1 == 0 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. */
* Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed). fun liftX(
* Returns affine (x, y) or null on failure. outX: IntArray,
*/ outY: IntArray,
fun parsePublicKey(pubkey: ByteArray): Pair<IntArray, 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 { return when {
pubkey.size == 33 && (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> { pubkey.size == 33 && (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> {
val x = U256.fromBytes(pubkey.copyOfRange(1, 33)) val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
if (U256.cmp(x, FieldP.P) >= 0) return null if (U256.cmp(x, FieldP.P) >= 0) return false
val x3 = FieldP.mul(FieldP.sqr(x), x) val t = IntArray(8)
val y2 = FieldP.add(x3, B) FieldP.sqr(t, x)
val y = FieldP.sqrt(y2) ?: return null FieldP.mul(t, t, x)
val isOdd = y[0] and 1 == 1 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() 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() -> { pubkey.size == 65 && pubkey[0] == 0x04.toByte() -> {
val x = U256.fromBytes(pubkey.copyOfRange(1, 33)) val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
val y = U256.fromBytes(pubkey.copyOfRange(33, 65)) val y = U256.fromBytes(pubkey.copyOfRange(33, 65))
// Verify point is on curve: y² = x³ + 7 val y2 = IntArray(8)
val y2 = FieldP.sqr(y) val x3p7 = IntArray(8)
val x3p7 = FieldP.add(FieldP.mul(FieldP.sqr(x), x), B) val t = IntArray(8)
if (U256.cmp(y2, x3p7) != 0) return null FieldP.sqr(y2, y)
Pair(x, 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 -> { else -> {
null false
} }
} }
} }
/** Serialize affine point as 65-byte uncompressed key (04 || x || y) */
fun serializeUncompressed( fun serializeUncompressed(
x: IntArray, x: IntArray,
y: IntArray, y: IntArray,
): ByteArray { ): ByteArray {
val result = ByteArray(65) val r = ByteArray(65)
result[0] = 0x04 r[0] = 0x04
U256.toBytes(x).copyInto(result, 1) U256.toBytesInto(x, r, 1)
U256.toBytes(y).copyInto(result, 33) U256.toBytesInto(y, r, 33)
return result return r
} }
/** Serialize affine point as 33-byte compressed key (02/03 || x) */
fun serializeCompressed( fun serializeCompressed(
x: IntArray, x: IntArray,
y: IntArray, y: IntArray,
): ByteArray { ): ByteArray {
val result = ByteArray(33) val r = ByteArray(33)
result[0] = if (hasEvenY(y)) 0x02 else 0x03 r[0] = if (hasEvenY(y)) 0x02 else 0x03
U256.toBytes(x).copyInto(result, 1) U256.toBytesInto(x, r, 1)
return result return r
}
// Convenience wrappers for non-hot paths
fun toAffinePair(p: MutablePoint): Pair<IntArray, IntArray>? {
val x = IntArray(8)
val y = IntArray(8)
return if (toAffine(p, x, y)) Pair(x, y) else null
} }
} }
@@ -24,208 +24,181 @@ import com.vitorpamplona.quartz.utils.sha256.sha256
/** /**
* Pure Kotlin implementation of secp256k1 elliptic curve operations. * 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: * Performance optimizations:
* - pubkeyCreate / pubKeyCompress * - Mutable field/point operations to minimize IntArray allocations
* - secKeyVerify * - Precomputed 4-bit window table for generator G multiplication
* - signSchnorr / verifySchnorr (BIP-340) * - Shamir's trick for verify: s*G + (-e)*P in a single scalar-mul pass
* - privKeyTweakAdd * - Cached BIP-340 tagged hash prefixes
* - pubKeyTweakMul * - Thread-local scratch buffers in field arithmetic
*/ */
object Secp256k1 { object Secp256k1 {
/** // ============ Cached tag hash prefixes for BIP-340 ============
* Create a public key from a secret key. // SHA256(tag) || SHA256(tag) — precomputed once
* @param seckey 32-byte secret key private val CHALLENGE_PREFIX: ByteArray by lazy {
* @return 65-byte uncompressed public key (04 || x || y) val h = sha256("BIP0340/challenge".encodeToByteArray())
*/ h + h
fun pubkeyCreate(seckey: ByteArray): ByteArray { }
require(seckey.size == 32) { "Secret key must be 32 bytes" } private val AUX_PREFIX: ByteArray by lazy {
val scalar = U256.fromBytes(seckey) val h = sha256("BIP0340/aux".encodeToByteArray())
require(ScalarN.isValid(scalar)) { "Invalid secret key" } h + h
}
private val NONCE_PREFIX: ByteArray by lazy {
val h = sha256("BIP0340/nonce".encodeToByteArray())
h + h
}
val point = ECPoint.mul(ECPoint.G, scalar) fun pubkeyCreate(seckey: ByteArray): ByteArray {
val (x, y) = ECPoint.toAffine(point) ?: error("Unexpected infinity") 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) 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 { 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) 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 { fun secKeyVerify(seckey: ByteArray): Boolean {
if (seckey.size != 32) return false if (seckey.size != 32) return false
val scalar = U256.fromBytes(seckey) return ScalarN.isValid(U256.fromBytes(seckey))
return ScalarN.isValid(scalar)
} }
/**
* 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( fun signSchnorr(
data: ByteArray, data: ByteArray,
seckey: ByteArray, seckey: ByteArray,
auxrand: ByteArray?, auxrand: ByteArray?,
): ByteArray { ): ByteArray {
require(seckey.size == 32) { "Secret key must be 32 bytes" } require(seckey.size == 32)
// Step 1-3: Compute keypair, negate secret key if needed
val d0 = U256.fromBytes(seckey) val d0 = U256.fromBytes(seckey)
require(ScalarN.isValid(d0)) { "Invalid secret key" } require(ScalarN.isValid(d0))
val pubPoint = ECPoint.mul(ECPoint.G, d0) // Compute public key
val (px, py) = ECPoint.toAffine(pubPoint) ?: error("Unexpected infinity") 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 d = if (ECPoint.hasEvenY(py)) d0 else ScalarN.neg(d0)
val dBytes = U256.toBytes(d) 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 = val t =
if (auxrand != null) { if (auxrand != null) {
require(auxrand.size == 32) { "Aux randomness must be 32 bytes" } require(auxrand.size == 32)
val auxHash = taggedHash("BIP0340/aux", auxrand) val auxHash = sha256(AUX_PREFIX + auxrand)
val tArray = U256.fromBytes(dBytes) val tArr = IntArray(8)
val auxArray = U256.fromBytes(auxHash) val auxArr = U256.fromBytes(auxHash)
U256.toBytes(U256.xor(tArray, auxArray)) U256.xorTo(tArr, U256.fromBytes(dBytes), auxArr)
U256.toBytes(tArr)
} else { } else {
dBytes dBytes
} }
// Step 5: rand = tagged_hash("BIP0340/nonce", t || pBytes || data) // rand = tagged_hash("BIP0340/nonce", t || P || msg)
val nonceInput = t + pBytes + data val rand = sha256(NONCE_PREFIX + t + pBytes + data)
val rand = taggedHash("BIP0340/nonce", nonceInput)
// Step 6: k' = int(rand) mod n
val k0 = ScalarN.reduce(U256.fromBytes(rand)) val k0 = ScalarN.reduce(U256.fromBytes(rand))
require(!U256.isZero(k0)) { "Nonce is zero" } require(!U256.isZero(k0))
// Step 7: R = k'·G // R = k'·G
val rPoint = ECPoint.mul(ECPoint.G, k0) val rPoint = MutablePoint()
val (rx, ry) = ECPoint.toAffine(rPoint) ?: error("Unexpected infinity") 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) 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 rBytes = U256.toBytes(rx)
val challengeInput = rBytes + pBytes + data val eHash = sha256(CHALLENGE_PREFIX + rBytes + pBytes + data)
val eHash = taggedHash("BIP0340/challenge", challengeInput)
val e = ScalarN.reduce(U256.fromBytes(eHash)) 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 s = ScalarN.add(k, ScalarN.mul(e, d))
val sig = rBytes + U256.toBytes(s) val sig = rBytes + U256.toBytes(s)
// Step 11: Verify (optional safety check - can be removed for performance) // Safety verify
require(verifySchnorr(sig, data, pBytes)) { "Signature verification failed" } require(verifySchnorr(sig, data, pBytes))
return sig 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( fun verifySchnorr(
signature: ByteArray, signature: ByteArray,
data: ByteArray, data: ByteArray,
pub: ByteArray, pub: ByteArray,
): Boolean { ): Boolean {
if (signature.size != 64) return false if (signature.size != 64 || pub.size != 32) return false
if (pub.size != 32) return false
// Step 1: P = lift_x(int(pk)) // P = lift_x(pub)
val px = U256.fromBytes(pub) val px = IntArray(8)
val (pxCoord, pyCoord) = ECPoint.liftX(px) ?: return false val py = IntArray(8)
if (!ECPoint.liftX(px, py, U256.fromBytes(pub))) return false
// Step 2: r = int(sig[0:32]) // r, s from signature
val rBytes = signature.copyOfRange(0, 32) val r = U256.fromBytes(signature.copyOfRange(0, 32))
val r = U256.fromBytes(rBytes)
if (U256.cmp(r, FieldP.P) >= 0) return false if (U256.cmp(r, FieldP.P) >= 0) return false
// Step 3: s = int(sig[32:64])
val s = U256.fromBytes(signature.copyOfRange(32, 64)) val s = U256.fromBytes(signature.copyOfRange(32, 64))
if (U256.cmp(s, ScalarN.N) >= 0) return false if (U256.cmp(s, ScalarN.N) >= 0) return false
// Step 4: e = int(tagged_hash("BIP0340/challenge", bytes(r) || bytes(P) || msg)) mod n // e = tagged_hash("BIP0340/challenge", sig[0:32] || pub || msg) mod n
val challengeInput = rBytes + pub + data val eHash = sha256(CHALLENGE_PREFIX + signature.copyOfRange(0, 32) + pub + data)
val eHash = taggedHash("BIP0340/challenge", challengeInput)
val e = ScalarN.reduce(U256.fromBytes(eHash)) val e = ScalarN.reduce(U256.fromBytes(eHash))
// Step 5: R = s·G - e·P // R = s*G - e*P using Shamir's trick (combined as s*G + (-e)*P)
val sG = ECPoint.mul(ECPoint.G, s)
val negE = ScalarN.neg(e) val negE = ScalarN.neg(e)
val pJac = JPoint(pxCoord, pyCoord, intArrayOf(1, 0, 0, 0, 0, 0, 0, 0)) val pPoint = MutablePoint()
val ePneg = ECPoint.mul(pJac, negE) pPoint.setAffine(px, py)
val rPoint = ECPoint.add(sG, ePneg) 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 // Check: R is not infinity, has even y, and x(R) == r
if (rPoint.isInfinity()) return false if (result.isInfinity()) return false
val (rx, ry) = ECPoint.toAffine(rPoint) ?: 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 (!ECPoint.hasEvenY(ry)) return false
if (U256.cmp(rx, r) != 0) return false return U256.cmp(rx, r) == 0
return true
} }
/**
* 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( fun privKeyTweakAdd(
seckey: ByteArray, seckey: ByteArray,
tweak: ByteArray, tweak: ByteArray,
): ByteArray { ): ByteArray {
require(seckey.size == 32) { "Secret key must be 32 bytes" } require(seckey.size == 32 && tweak.size == 32)
require(tweak.size == 32) { "Tweak must be 32 bytes" } val result = ScalarN.add(U256.fromBytes(seckey), U256.fromBytes(tweak))
val a = U256.fromBytes(seckey) require(!U256.isZero(result) && U256.cmp(result, ScalarN.N) < 0)
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" }
return U256.toBytes(result) 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( fun pubKeyTweakMul(
pubkey: ByteArray, pubkey: ByteArray,
tweak: ByteArray, tweak: ByteArray,
): ByteArray { ): ByteArray {
require(tweak.size == 32) { "Tweak must be 32 bytes" } require(tweak.size == 32)
val (x, y) = ECPoint.parsePublicKey(pubkey) ?: error("Invalid public key") val x = IntArray(8)
val y = IntArray(8)
check(ECPoint.parsePublicKey(pubkey, x, y))
val scalar = U256.fromBytes(tweak) 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 p = MutablePoint()
val result = ECPoint.mul(point, scalar) p.setAffine(x, y)
val (rx, ry) = ECPoint.toAffine(result) ?: error("Result is infinity") 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) { return if (pubkey.size == 33) {
ECPoint.serializeCompressed(rx, ry) ECPoint.serializeCompressed(rx, ry)
@@ -234,9 +207,7 @@ object Secp256k1 {
} }
} }
// ============ Tagged Hash (BIP-340) ============ /** BIP-340 tagged hash (for non-cached tags) */
/** BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || msg) */
internal fun taggedHash( internal fun taggedHash(
tag: String, tag: String,
msg: ByteArray, msg: ByteArray,