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:
-59
@@ -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))
|
||||
}
|
||||
}
|
||||
+241
@@ -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)
|
||||
}
|
||||
-59
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user