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