feat: replace fr.acinq.secp256k1 with pure-Kotlin secp256k1 implementation

Implements all secp256k1 operations used by Secp256k1Instance in pure Kotlin,
eliminating the dependency on native secp256k1 bindings (fr.acinq.secp256k1-kmp).

Implementation:
- Field.kt: 256-bit unsigned integer arithmetic, field mod p, scalar mod n
- Point.kt: EC point operations (Jacobian coords), point parsing/serialization
- Secp256k1.kt: Public API - pubkeyCreate, pubKeyCompress, secKeyVerify,
  signSchnorr/verifySchnorr (BIP-340), privKeyTweakAdd, pubKeyTweakMul

Tests (36 total):
- All 19 BIP-340 test vectors (signing vectors 0-3, 15-18; verify vectors 4-14)
- ACINQ test vectors for key creation, compression, secKeyVerify, privKeyTweakAdd,
  pubKeyTweakMul
- ECDH symmetry and sign/verify round-trip tests

Secp256k1Instance is now a concrete object in commonMain instead of expect/actual,
delegating to the pure-Kotlin implementation. All existing NIP-44 and BIP-32 tests
pass with the new implementation.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-05 04:47:19 +00:00
parent e1304ddcab
commit 4c3b31fe8e
9 changed files with 1734 additions and 185 deletions
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import fr.acinq.secp256k1.Secp256k1
actual object Secp256k1Instance {
private val h02 = Hex.decode("02")
private val secp256k1 = Secp256k1.get()
actual fun compressedPubKeyFor(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
actual fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray?,
): ByteArray = secp256k1.signSchnorr(data, privKey, nonce)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray = secp256k1.signSchnorr(data, privKey, null)
actual fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
actual fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1.privKeyTweakAdd(first, second)
actual fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}
@@ -20,35 +20,39 @@
*/
package com.vitorpamplona.quartz.utils
expect object Secp256k1Instance {
fun compressedPubKeyFor(privKey: ByteArray): ByteArray
import com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
fun isPrivateKeyValid(il: ByteArray): Boolean
object Secp256k1Instance {
private val h02 = Hex.decode("02")
fun compressedPubKeyFor(privKey: ByteArray) = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privKey))
fun isPrivateKeyValid(il: ByteArray): Boolean = Secp256k1.secKeyVerify(il)
fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray? = RandomInstance.bytes(32),
): ByteArray
): ByteArray = Secp256k1.signSchnorr(data, privKey, nonce)
fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray
): ByteArray = Secp256k1.signSchnorr(data, privKey, null)
fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean
): Boolean = Secp256k1.verifySchnorr(signature, hash, pubKey)
fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray
): ByteArray = Secp256k1.privKeyTweakAdd(first, second)
fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray
): ByteArray = Secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}
@@ -0,0 +1,559 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
/**
* 256-bit unsigned integer arithmetic for secp256k1 field and scalar operations.
*
* 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.
*/
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
}
/** Compare: returns negative if a < b, 0 if equal, positive if a > b */
fun cmp(
a: IntArray,
b: IntArray,
): Int {
for (i in 7 downTo 0) {
val ai = a[i].toLong() and 0xFFFFFFFFL
val bi = b[i].toLong() and 0xFFFFFFFFL
if (ai < bi) return -1
if (ai > bi) return 1
}
return 0
}
/** a + b, returns (result, carry) where carry is 0 or 1 */
fun addCarry(
a: IntArray,
b: IntArray,
): Pair<IntArray, Int> {
val r = IntArray(8)
var carry = 0L
for (i in 0 until 8) {
carry += (a[i].toLong() and 0xFFFFFFFFL) + (b[i].toLong() and 0xFFFFFFFFL)
r[i] = carry.toInt()
carry = carry ushr 32
}
return Pair(r, carry.toInt())
}
/** a - b, returns (result, borrow) where borrow is 0 or 1 */
fun subBorrow(
a: IntArray,
b: IntArray,
): Pair<IntArray, Int> {
val r = IntArray(8)
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()
borrow = if (diff < 0) 1L else 0L
}
return Pair(r, borrow.toInt())
}
/** Full 256x256 -> 512 bit multiplication. Result is IntArray(16). */
fun mulWide(
a: IntArray,
b: IntArray,
): IntArray {
val r = IntArray(16)
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()
carry = prod ushr 32
}
r[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}" }
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)
}
return r
}
/** Convert IntArray(8) little-endian limbs to big-endian 32-byte array */
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()
}
return r
}
/** Check if bit at position pos is set (pos 0 = LSB) */
fun testBit(
a: IntArray,
pos: Int,
): Boolean {
val limb = pos / 32
val bit = pos % 32
return (a[limb] ushr bit) and 1 == 1
}
/** XOR two 256-bit values */
fun xor(
a: IntArray,
b: IntArray,
): IntArray {
val r = IntArray(8)
for (i in 0 until 8) r[i] = a[i] xor b[i]
return r
}
fun clone(a: IntArray): IntArray = a.copyOf()
}
/**
* Field arithmetic modulo p = 2^256 - 2^32 - 977 (= 2^256 - 4294968273).
* This is the base field of the secp256k1 curve.
*/
internal object FieldP {
// p = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
val P =
intArrayOf(
0xFFFFFC2F.toInt(),
0xFFFFFFFE.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
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)
/** Reduce a value that might be >= p (but < 2p) */
fun reduce(a: IntArray): IntArray =
if (U256.cmp(a, P) >= 0) {
U256.subBorrow(a, P).first
} else {
a
}
/** 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)
//
// 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)
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()
carry = carry ushr 32
}
// Remaining overflow: carry + hiTimes977[8] + hi[7]
var overflow =
carry +
(hiTimes977[8].toLong() and 0xFFFFFFFFL) +
(hi[7].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)
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()
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)
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()
c3 = c3 ushr 32
}
}
}
// Final reduction: result might still be >= p
return reduce(result)
}
fun add(
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)
}
}
fun sub(
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
}
}
fun mul(
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)
}
// p - 2
private val P_MINUS_2 =
intArrayOf(
0xFFFFFC2D.toInt(),
0xFFFFFFFE.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
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 =
intArrayOf(
0xBFFFFF0C.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
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
}
/** Generic modular exponentiation mod p using square-and-multiply */
private fun powModP(
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
var highBit = 255
while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit--
for (i in 0..highBit) {
if (U256.testBit(exp, i)) {
result = mul(result, b)
}
if (i < highBit) {
b = sqr(b)
}
}
return result
}
}
/**
* Scalar arithmetic modulo n (the order of the secp256k1 group).
* n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
*/
internal object ScalarN {
val N =
intArrayOf(
0xD0364141.toInt(),
0xBFD25E8C.toInt(),
0xAF48A03B.toInt(),
0xBAAEDCE6.toInt(),
0xFFFFFFFE.toInt(),
0xFFFFFFFF.toInt(),
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 =
intArrayOf(
0x2FC9BEBF.toInt(),
0x402DA173.toInt(),
0x50B75FC4.toInt(),
0x45512319.toInt(),
0x00000001,
0,
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 =
intArrayOf(
0xD036413F.toInt(),
0xBFD25E8C.toInt(),
0xAF48A03B.toInt(),
0xBAAEDCE6.toInt(),
0xFFFFFFFE.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
0xFFFFFFFF.toInt(),
)
private fun powModN(
base: IntArray,
exp: IntArray,
): IntArray {
var result = intArrayOf(1, 0, 0, 0, 0, 0, 0, 0)
var b = base.copyOf()
var highBit = 255
while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit--
for (i in 0..highBit) {
if (U256.testBit(exp, i)) {
result = mul(result, b)
}
if (i < highBit) {
b = mul(b, b)
}
}
return result
}
}
@@ -0,0 +1,293 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
/**
* 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.
*/
internal class JPoint(
val x: IntArray,
val y: IntArray,
val z: IntArray,
) {
companion object {
val INFINITY = JPoint(IntArray(8), intArrayOf(1, 0, 0, 0, 0, 0, 0, 0), IntArray(8))
}
fun isInfinity(): Boolean = U256.isZero(z)
}
internal object ECPoint {
// Generator point G
val GX =
intArrayOf(
0x16F81798.toInt(),
0x59F2815B.toInt(),
0x2DCE28D9.toInt(),
0x029BFCDB.toInt(),
0xCE870B07.toInt(),
0x55A06295.toInt(),
0xF9DCBBAC.toInt(),
0x79BE667E.toInt(),
)
val GY =
intArrayOf(
0xFB10D4B8.toInt(),
0x9C47D08F.toInt(),
0xA6855419.toInt(),
0xFD17B448.toInt(),
0x0E1108A8.toInt(),
0x5DA4FBFC.toInt(),
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)
/**
* Point doubling in Jacobian coordinates.
* Formula from https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l
*/
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)
}
/**
* Point addition in Jacobian coordinates.
* Mixed addition when q.z = 1 (affine point) for efficiency.
*/
fun add(
p: JPoint,
q: JPoint,
): JPoint {
if (p.isInfinity()) return q
if (q.isInfinity()) return p
val z1sq = FieldP.sqr(p.z)
val z2sq = FieldP.sqr(q.z)
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
} else {
JPoint.INFINITY // Inverse points
}
}
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)
}
/**
* Scalar multiplication using double-and-add (left-to-right).
*/
fun mul(
p: JPoint,
scalar: IntArray,
): JPoint {
if (U256.isZero(scalar) || p.isInfinity()) return JPoint.INFINITY
var result = JPoint.INFINITY
// Find highest set bit
var highBit = 255
while (highBit >= 0 && !U256.testBit(scalar, highBit)) highBit--
for (i in highBit downTo 0) {
result = double(result)
if (U256.testBit(scalar, i)) {
result = add(result, p)
}
}
return result
}
/**
* Convert Jacobian point to affine coordinates.
* Returns null if the point is at infinity.
*/
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))
}
}
/** 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
}
/**
* 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>? {
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
val wantOdd = pubkey[0] == 0x03.toByte()
if (isOdd != wantOdd) Pair(x, FieldP.neg(y)) else Pair(x, y)
}
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)
}
else -> {
null
}
}
}
/** 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
}
/** 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
}
}
@@ -0,0 +1,247 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
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
*/
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" }
val point = ECPoint.mul(ECPoint.G, scalar)
val (x, y) = ECPoint.toAffine(point) ?: error("Unexpected infinity")
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")
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)
}
/**
* 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
val d0 = U256.fromBytes(seckey)
require(ScalarN.isValid(d0)) { "Invalid secret key" }
val pubPoint = ECPoint.mul(ECPoint.G, d0)
val (px, py) = ECPoint.toAffine(pubPoint) ?: error("Unexpected infinity")
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
// Step 4: Compute 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))
} 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
val k0 = ScalarN.reduce(U256.fromBytes(rand))
require(!U256.isZero(k0)) { "Nonce is zero" }
// Step 7: R = k'·G
val rPoint = ECPoint.mul(ECPoint.G, k0)
val (rx, ry) = ECPoint.toAffine(rPoint) ?: error("Unexpected infinity")
// 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
val rBytes = U256.toBytes(rx)
val challengeInput = rBytes + pBytes + data
val eHash = taggedHash("BIP0340/challenge", challengeInput)
val e = ScalarN.reduce(U256.fromBytes(eHash))
// Step 10: sig = bytes(R) || bytes((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" }
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
// Step 1: P = lift_x(int(pk))
val px = U256.fromBytes(pub)
val (pxCoord, pyCoord) = ECPoint.liftX(px) ?: return false
// Step 2: r = int(sig[0:32])
val rBytes = signature.copyOfRange(0, 32)
val r = U256.fromBytes(rBytes)
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)
val e = ScalarN.reduce(U256.fromBytes(eHash))
// Step 5: R = s·G - e·P
val sG = ECPoint.mul(ECPoint.G, s)
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)
// 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
if (!ECPoint.hasEvenY(ry)) return false
if (U256.cmp(rx, r) != 0) return false
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(
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" }
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")
val scalar = U256.fromBytes(tweak)
require(ScalarN.isValid(scalar)) { "Invalid tweak" }
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")
return if (pubkey.size == 33) {
ECPoint.serializeCompressed(rx, ry)
} else {
ECPoint.serializeUncompressed(rx, ry)
}
}
// ============ Tagged Hash (BIP-340) ============
/** BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || msg) */
internal fun taggedHash(
tag: String,
msg: ByteArray,
): ByteArray {
val tagHash = sha256(tag.encodeToByteArray())
return sha256(tagHash + tagHash + msg)
}
}
@@ -0,0 +1,382 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Full BIP-340 test vectors from
* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv
*/
class SchnorrTest {
// ============================================================
// BIP-340 signing tests (vectors with secret keys)
// ============================================================
@Test
fun bip340Vector0Sign() {
val sig =
Secp256k1.signSchnorr(
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000003".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
)
assertEquals(
"e907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215" +
"25f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0",
sig.toHexKey(),
)
}
@Test
fun bip340Vector0Verify() {
assertTrue(
Secp256k1.verifySchnorr(
(
"E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215" +
"25F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0"
).hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9".hexToByteArray(),
),
)
}
@Test
fun bip340Vector1Sign() {
val sig =
Secp256k1.signSchnorr(
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000001".hexToByteArray(),
)
assertEquals(
"6896bd60eeae296db48a229ff71dfe071bde413e6d43f917dc8dcf8c78de3341" +
"8906d11ac976abccb20b091292bff4ea897efcb639ea871cfa95f6de339e4b0a",
sig.toHexKey(),
)
}
@Test
fun bip340Vector1Verify() {
assertTrue(
Secp256k1.verifySchnorr(
(
"6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE3341" +
"8906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector2Sign() {
val sig =
Secp256k1.signSchnorr(
"7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C".hexToByteArray(),
"C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9".hexToByteArray(),
"C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906".hexToByteArray(),
)
assertEquals(
"5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1b" +
"ab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7",
sig.toHexKey(),
)
}
@Test
fun bip340Vector3Sign() {
val sig =
Secp256k1.signSchnorr(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".hexToByteArray(),
"0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710".hexToByteArray(),
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF".hexToByteArray(),
)
assertEquals(
"7eb0509757e246f19449885651611cb965ecc1a187dd51b64fda1edc9637d5ec" +
"97582b9cb13db3933705b32ba982af5af25fd78881ebb32771fc5922efc66ea3",
sig.toHexKey(),
)
}
@Test
fun bip340Vector15Sign() {
// Empty message
val sig =
Secp256k1.signSchnorr(
ByteArray(0),
"0340034003400340034003400340034003400340034003400340034003400340".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
)
assertEquals(
"71535db165ecd9fbbc046e5ffaea61186bb6ad436732fccc25291a55895464cf" +
"6069ce26bf03466228f19a3a62db8a649f2d560fac652827d1af0574e427ab63",
sig.toHexKey(),
)
}
@Test
fun bip340Vector16Sign() {
// 1-byte message
val sig =
Secp256k1.signSchnorr(
"11".hexToByteArray(),
"0340034003400340034003400340034003400340034003400340034003400340".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
)
assertEquals(
"08a20a0afef64124649232e0693c583ab1b9934ae63b4c3511f3ae1134c6a303" +
"ea3173bfea6683bd101fa5aa5dbc1996fe7cacfc5a577d33ec14564cec2bacbf",
sig.toHexKey(),
)
}
@Test
fun bip340Vector17Sign() {
// 17-byte message
val sig =
Secp256k1.signSchnorr(
"0102030405060708090A0B0C0D0E0F1011".hexToByteArray(),
"0340034003400340034003400340034003400340034003400340034003400340".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
)
assertEquals(
"5130f39a4059b43bc7cac09a19ece52b5d8699d1a71e3c52da9afdb6b50ac370" +
"c4a482b77bf960f8681540e25b6771ece1e5a37fd80e5a51897c5566a97ea5a5",
sig.toHexKey(),
)
}
@Test
fun bip340Vector18Sign() {
// 100-byte message
val sig =
Secp256k1.signSchnorr(
(
"9999999999999999999999999999999999999999" +
"9999999999999999999999999999999999999999" +
"9999999999999999999999999999999999999999" +
"9999999999999999999999999999999999999999" +
"9999999999999999999999999999999999999999"
).hexToByteArray(),
"0340034003400340034003400340034003400340034003400340034003400340".hexToByteArray(),
"0000000000000000000000000000000000000000000000000000000000000000".hexToByteArray(),
)
assertEquals(
"403b12b0d8555a344175ea7ec746566303321e5dbfa8be6f091635163eca79a8" +
"585ed3e3170807e7c03b720fc54c7b23897fcba0e9d0b4a06894cfd249f22367",
sig.toHexKey(),
)
}
// ============================================================
// BIP-340 verify-only tests (vectors 4-14)
// ============================================================
@Test
fun bip340Vector4Verify() {
assertTrue(
Secp256k1.verifySchnorr(
(
"00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C63" +
"76AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4"
).hexToByteArray(),
"4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703".hexToByteArray(),
"D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9".hexToByteArray(),
),
)
}
@Test
fun bip340Vector5VerifyFail() {
// public key not on the curve
assertFalse(
Secp256k1.verifySchnorr(
(
"6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" +
"69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34".hexToByteArray(),
),
)
}
@Test
fun bip340Vector6VerifyFail() {
// has_even_y(R) is false
assertFalse(
Secp256k1.verifySchnorr(
(
"FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A1460297556" +
"3CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector7VerifyFail() {
// negated message
assertFalse(
Secp256k1.verifySchnorr(
(
"1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F" +
"28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector8VerifyFail() {
// negated s value
assertFalse(
Secp256k1.verifySchnorr(
(
"6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" +
"961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector9VerifyFail() {
// sG - eP is infinite (x(inf) as 0)
assertFalse(
Secp256k1.verifySchnorr(
(
"0000000000000000000000000000000000000000000000000000000000000000" +
"123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector10VerifyFail() {
// sG - eP is infinite (x(inf) as 1)
assertFalse(
Secp256k1.verifySchnorr(
(
"0000000000000000000000000000000000000000000000000000000000000001" +
"7615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector11VerifyFail() {
// sig[0:32] is not an X coordinate on the curve
assertFalse(
Secp256k1.verifySchnorr(
(
"4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D" +
"69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector12VerifyFail() {
// sig[0:32] is equal to field size
assertFalse(
Secp256k1.verifySchnorr(
(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F" +
"69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector13VerifyFail() {
// sig[32:64] is equal to curve order
assertFalse(
Secp256k1.verifySchnorr(
(
"6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" +
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659".hexToByteArray(),
),
)
}
@Test
fun bip340Vector14VerifyFail() {
// public key exceeds field size
assertFalse(
Secp256k1.verifySchnorr(
(
"6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" +
"69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B"
).hexToByteArray(),
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89".hexToByteArray(),
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30".hexToByteArray(),
),
)
}
// ============================================================
// Sign then verify round-trip
// ============================================================
@Test
fun signVerifyRoundTrip() {
val seckey =
"f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561"
.hexToByteArray()
val msg = ByteArray(32) { 0x42 }
val sig = Secp256k1.signSchnorr(msg, seckey, null)
val pubkey = Secp256k1.pubkeyCreate(seckey)
val compressed = Secp256k1.pubKeyCompress(pubkey)
// x-only pubkey is compressed[1..33]
val xonly = compressed.copyOfRange(1, 33)
assertTrue(Secp256k1.verifySchnorr(sig, msg, xonly))
}
}
@@ -0,0 +1,241 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class Secp256k1Test {
// ============================================================
// secKeyVerify
// ============================================================
@Test
fun verifyValidPrivateKey() {
assertTrue(
Secp256k1.secKeyVerify(
"67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530"
.hexToByteArray(),
),
)
}
@Test
fun verifyInvalidPrivateKeyWrongSize() {
assertFalse(
Secp256k1.secKeyVerify(
"67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A106353001"
.hexToByteArray(),
),
)
}
@Test
fun verifyInvalidPrivateKeyCurveOrder() {
assertFalse(
Secp256k1.secKeyVerify(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"
.hexToByteArray(),
),
)
}
@Test
fun verifyInvalidPrivateKeyZero() {
assertFalse(
Secp256k1.secKeyVerify(
"0000000000000000000000000000000000000000000000000000000000000000"
.hexToByteArray(),
),
)
}
// ============================================================
// pubkeyCreate
// ============================================================
@Test
fun createValidPublicKey() {
val pubkey =
Secp256k1.pubkeyCreate(
"67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530"
.hexToByteArray(),
)
assertEquals(
"04c591a8ff19ac9c4e4e5793673b83123437e975285e7b442f4ee2654dffca5e2d" +
"2103ed494718c697ac9aebcfd19612e224db46661011863ed2fc54e71861e2a6",
pubkey.toHexKey(),
)
}
@Test
fun createPublicKeyFromKeyOne() {
val pubkey =
Secp256k1.pubkeyCreate(
"0000000000000000000000000000000000000000000000000000000000000001"
.hexToByteArray(),
)
// G itself
assertEquals(
"0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" +
"483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8",
pubkey.toHexKey(),
)
}
@Test
fun createPublicKeyFromKeyThree() {
val pubkey =
Secp256k1.pubkeyCreate(
"0000000000000000000000000000000000000000000000000000000000000003"
.hexToByteArray(),
)
val compressed = Secp256k1.pubKeyCompress(pubkey)
// BIP-340 test vector 0 public key (x-only)
assertEquals(
"02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
compressed.toHexKey(),
)
}
// ============================================================
// pubKeyCompress
// ============================================================
@Test
fun compressPublicKey() {
val compressed =
Secp256k1.pubKeyCompress(
(
"04C591A8FF19AC9C4E4E5793673B83123437E975285E7B442F4EE2654DFFCA5E2D" +
"2103ED494718C697AC9AEBCFD19612E224DB46661011863ED2FC54E71861E2A6"
).hexToByteArray(),
)
assertEquals(
"02c591a8ff19ac9c4e4e5793673b83123437e975285e7b442f4ee2654dffca5e2d",
compressed.toHexKey(),
)
}
@Test
fun compressPublicKeyWithOddY() {
// From existing Amethyst test: key with 03 prefix
val privKey =
"65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a"
.hexToByteArray()
val pubkey = Secp256k1.pubkeyCreate(privKey)
val compressed = Secp256k1.pubKeyCompress(pubkey)
assertEquals(
"033dcef7585efbdb68747d919152bd481e21f5e952aaaef5a19604fbd096a93dd5",
compressed.toHexKey(),
)
}
@Test
fun compressPublicKeyWithEvenY() {
val privKey =
"e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0"
.hexToByteArray()
val pubkey = Secp256k1.pubkeyCreate(privKey)
val compressed = Secp256k1.pubKeyCompress(pubkey)
assertEquals(
"029fa4ce8c87ca546b196e6518db80a6780e1bd5552b61f9f17bafee5d4e34e09b",
compressed.toHexKey(),
)
}
// ============================================================
// privKeyTweakAdd
// ============================================================
@Test
fun privateKeyTweakAdd() {
val result =
Secp256k1.privKeyTweakAdd(
"67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530"
.hexToByteArray(),
"3982F19BEF1615BCCFBB05E321C10E1D4CBA3DF0E841C2E41EEB6016347653C3"
.hexToByteArray(),
)
assertEquals(
"a168571e189e6f9a7e2d657a4b53ae99b909f7e712d1c23ced28093cd57c88f3",
result.toHexKey(),
)
}
// ============================================================
// pubKeyTweakMul
// ============================================================
@Test
fun publicKeyTweakMul() {
val result =
Secp256k1.pubKeyTweakMul(
(
"040A629506E1B65CD9D2E0BA9C75DF9C4FED0DB16DC9625ED14397F0AFC836FAE5" +
"95DC53F8B0EFE61E703075BD9B143BAC75EC0E19F82A2208CAEB32BE53414C40"
).hexToByteArray(),
"3982F19BEF1615BCCFBB05E321C10E1D4CBA3DF0E841C2E41EEB6016347653C3"
.hexToByteArray(),
)
assertEquals(
"04e0fe6fe55ebca626b98a807f6caf654139e14e5e3698f01a9a658e21dc1d2791" +
"ec060d4f412a794d5370f672bc94b722640b5f76914151cfca6e712ca48cc589",
result.toHexKey(),
)
}
@Test
fun publicKeyTweakMulCompressed() {
// Test the pattern used by Secp256k1Instance.pubKeyTweakMulCompact
val pubKey =
"c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133"
.hexToByteArray()
val privKey =
"315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268"
.hexToByteArray()
val h02 = byteArrayOf(0x02)
val result = Secp256k1.pubKeyTweakMul(h02 + pubKey, privKey)
// Should return 33-byte compressed key; take bytes 1..33 for x-only
assertEquals(33, result.size)
}
@Test
fun ecdhSymmetry() {
// Shared secret A->B should equal B->A
val privA =
"315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268"
.hexToByteArray()
val privB =
"a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e"
.hexToByteArray()
val pubA = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privA))
val pubB = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privB))
val secretAB = Secp256k1.pubKeyTweakMul(pubB, privA)
val secretBA = Secp256k1.pubKeyTweakMul(pubA, privB)
assertEquals(secretAB.toHexKey(), secretBA.toHexKey())
}
}
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import fr.acinq.secp256k1.Secp256k1
actual object Secp256k1Instance {
private val h02 = Hex.decode("02")
private val secp256k1 = Secp256k1.get()
actual fun compressedPubKeyFor(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
actual fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray?,
): ByteArray = secp256k1.signSchnorr(data, privKey, nonce)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray = secp256k1.signSchnorr(data, privKey, null)
actual fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
actual fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1.privKeyTweakAdd(first, second)
actual fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import fr.acinq.secp256k1.Secp256k1
actual object Secp256k1Instance {
private val h02 = Hex.decode("02")
private val secp256k1Ref = Secp256k1.get()
actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray = secp256k1Ref.pubKeyCompress(secp256k1Ref.pubkeyCreate(privKey))
actual fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1Ref.secKeyVerify(il)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray?,
): ByteArray = secp256k1Ref.signSchnorr(data, privKey, nonce)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray = secp256k1Ref.signSchnorr(data, privKey, null)
actual fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1Ref.verifySchnorr(signature, hash, pubKey)
actual fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1Ref.privKeyTweakAdd(first, second)
actual fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray = secp256k1Ref.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}