diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt new file mode 100644 index 000000000..c33d9df1e --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt @@ -0,0 +1,225 @@ +/* + * 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.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.utils.Secp256k1InstanceC +import com.vitorpamplona.quartz.utils.Secp256k1InstanceKotlin +import fr.acinq.secp256k1.Secp256k1 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Android benchmark comparing three secp256k1 implementations: + * 1. ACINQ C (libsecp256k1 via JNI) — "Foo" + * 2. Pure Kotlin — "FooOurs" + * 3. Custom C (our implementation via JNI) — "FooC" + * + * Run with: ./gradlew :benchmark:connectedAndroidTest + */ +@RunWith(AndroidJUnit4::class) +class Secp256k1CBenchmark { + @get:Rule + val benchmarkRule = BenchmarkRule() + + private val privKey = hexToBytes("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530") + private val msg32 = hexToBytes("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") + private val auxRand = hexToBytes("0000000000000000000000000000000000000000000000000000000000000001") + + private val native = Secp256k1.get() + private val nativePubKey = native.pubKeyCompress(native.pubkeyCreate(privKey)) + private val nativeXOnlyPub = nativePubKey.copyOfRange(1, 33) + private val nativeSig = native.signSchnorr(msg32, privKey, auxRand) + + private val kotlinSig = + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .signSchnorr(msg32, privKey, auxRand) + private val kotlinXOnlyPub = + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .pubKeyCompress( + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .pubkeyCreate(privKey), + ).copyOfRange(1, 33) + + // ==================== ACINQ C (Reference) ==================== + + @Test fun verifySchnorr() = benchmarkRule.measureRepeated { native.verifySchnorr(nativeSig, msg32, nativeXOnlyPub) } + + @Test fun signSchnorr() = benchmarkRule.measureRepeated { native.signSchnorr(msg32, privKey, auxRand) } + + @Test + fun pubkeyCreate() = benchmarkRule.measureRepeated { native.pubKeyCompress(native.pubkeyCreate(privKey)) } + + // ==================== Pure Kotlin ==================== + + @Test + fun verifySchnorrOurs() = + benchmarkRule.measureRepeated { + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .verifySchnorr(kotlinSig, msg32, kotlinXOnlyPub) + } + + @Test + fun verifySchnorrFastOurs() = + benchmarkRule.measureRepeated { + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .verifySchnorrFast(kotlinSig, msg32, kotlinXOnlyPub) + } + + @Test + fun signSchnorrOurs() = + benchmarkRule.measureRepeated { + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .signSchnorr(msg32, privKey, auxRand) + } + + @Test + fun signSchnorrXOnlyOurs() = + benchmarkRule.measureRepeated { + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .signSchnorrWithXOnlyPubKey(msg32, privKey, kotlinXOnlyPub, auxRand) + } + + @Test + fun pubkeyCreateOurs() = + benchmarkRule.measureRepeated { + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1.pubKeyCompress( + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .pubkeyCreate(privKey), + ) + } + + // ==================== Custom C (Our JNI) ==================== + + @Test + fun verifySchnorrC() { + Secp256k1InstanceC.init() + val cSig = Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) + val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + benchmarkRule.measureRepeated { Secp256k1InstanceC.verifySchnorr(cSig, msg32, cXOnly) } + } + + @Test + fun verifySchnorrFastC() { + Secp256k1InstanceC.init() + val cSig = Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) + val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + benchmarkRule.measureRepeated { Secp256k1InstanceC.verifySchnorrFast(cSig, msg32, cXOnly) } + } + + @Test + fun signSchnorrC() { + Secp256k1InstanceC.init() + benchmarkRule.measureRepeated { Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) } + } + + @Test + fun signSchnorrXOnlyC() { + Secp256k1InstanceC.init() + val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + benchmarkRule.measureRepeated { Secp256k1InstanceC.signSchnorrWithXOnlyPubKey(msg32, privKey, cXOnly, auxRand) } + } + + @Test + fun pubkeyCreateC() { + Secp256k1InstanceC.init() + benchmarkRule.measureRepeated { Secp256k1InstanceC.compressedPubKeyFor(privKey) } + } + + @Test + fun ecdhXOnlyC() { + Secp256k1InstanceC.init() + val pub2xOnly = hexToBytes("c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133") + benchmarkRule.measureRepeated { Secp256k1InstanceC.ecdhXOnly(pub2xOnly, privKey) } + } + + // ==================== Batch Verification (all three) ==================== + + @Test + fun verifySchnorrBatch16Ours() { + val sigs = + (0 until 16).map { i -> + val m = ByteArray(32) { (i * 7 + it).toByte() } + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .signSchnorr(m, privKey, auxRand) + } + val msgs = (0 until 16).map { i -> ByteArray(32) { (i * 7 + it).toByte() } } + benchmarkRule.measureRepeated { + Secp256k1InstanceKotlin.verifySchnorrBatch(kotlinXOnlyPub, sigs, msgs) + } + } + + @Test + fun verifySchnorrBatch16C() { + Secp256k1InstanceC.init() + val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + val sigs = + (0 until 16).map { i -> + val m = ByteArray(32) { (i * 7 + it).toByte() } + Secp256k1InstanceC.signSchnorr(m, privKey, auxRand) + } + val msgs = (0 until 16).map { i -> ByteArray(32) { (i * 7 + it).toByte() } } + benchmarkRule.measureRepeated { + Secp256k1InstanceC.verifySchnorrBatch(cXOnly, sigs, msgs) + } + } + + @Test + fun verifySchnorrBatch200Ours() { + val sigs = + (0 until 200).map { i -> + val m = ByteArray(32) { (i * 7 + it).toByte() } + com.vitorpamplona.quartz.utils.secp256k1.Secp256k1 + .signSchnorr(m, privKey, auxRand) + } + val msgs = (0 until 200).map { i -> ByteArray(32) { (i * 7 + it).toByte() } } + benchmarkRule.measureRepeated { + Secp256k1InstanceKotlin.verifySchnorrBatch(kotlinXOnlyPub, sigs, msgs) + } + } + + @Test + fun verifySchnorrBatch200C() { + Secp256k1InstanceC.init() + val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + val sigs = + (0 until 200).map { i -> + val m = ByteArray(32) { (i * 7 + it).toByte() } + Secp256k1InstanceC.signSchnorr(m, privKey, auxRand) + } + val msgs = (0 until 200).map { i -> ByteArray(32) { (i * 7 + it).toByte() } } + benchmarkRule.measureRepeated { + Secp256k1InstanceC.verifySchnorrBatch(cXOnly, sigs, msgs) + } + } + + private fun hexToBytes(hex: String): ByteArray { + val len = hex.length / 2 + val result = ByteArray(len) + for (i in 0 until len) { + result[i] = hex.substring(i * 2, i * 2 + 2).toInt(16).toByte() + } + return result + } +} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt new file mode 100644 index 000000000..1a01460c8 --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt @@ -0,0 +1,178 @@ +/* + * 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 + +actual object Secp256k1InstanceC { + private var loaded = false + + private fun ensureLoaded() { + if (!loaded) { + System.loadLibrary("secp256k1_amethyst_jni") + nativeInit() + loaded = true + } + } + + private external fun nativeInit() + + private external fun nativePubkeyCreate(seckey: ByteArray): ByteArray? + + private external fun nativePubkeyCompress(pubkey: ByteArray): ByteArray? + + private external fun nativeSecKeyVerify(seckey: ByteArray): Boolean + + private external fun nativeSchnorrSign( + msg: ByteArray, + seckey: ByteArray, + auxrand: ByteArray?, + ): ByteArray? + + private external fun nativeSchnorrSignXOnly( + msg: ByteArray, + seckey: ByteArray, + xonlyPub: ByteArray, + auxrand: ByteArray?, + ): ByteArray? + + private external fun nativeSchnorrVerify( + sig: ByteArray, + msg: ByteArray, + pub: ByteArray, + ): Boolean + + private external fun nativeSchnorrVerifyFast( + sig: ByteArray, + msg: ByteArray, + pub: ByteArray, + ): Boolean + + private external fun nativeSchnorrVerifyBatch( + pub: ByteArray, + sigs: Array, + msgs: Array, + ): Boolean + + private external fun nativePrivKeyTweakAdd( + seckey: ByteArray, + tweak: ByteArray, + ): ByteArray? + + private external fun nativePubKeyTweakMul( + pubkey: ByteArray, + tweak: ByteArray, + ): ByteArray? + + private external fun nativeEcdhXOnly( + xonlyPub: ByteArray, + scalar: ByteArray, + ): ByteArray? + + actual fun init() = ensureLoaded() + + actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray { + ensureLoaded() + val pub65 = nativePubkeyCreate(privKey) ?: error("Invalid private key") + return nativePubkeyCompress(pub65) ?: error("Compression failed") + } + + actual fun isPrivateKeyValid(il: ByteArray): Boolean { + ensureLoaded() + return nativeSecKeyVerify(il) + } + + actual fun signSchnorr( + data: ByteArray, + privKey: ByteArray, + nonce: ByteArray?, + ): ByteArray { + ensureLoaded() + return nativeSchnorrSign(data, privKey, nonce) ?: error("Sign failed") + } + + actual fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray?, + ): ByteArray { + ensureLoaded() + return nativeSchnorrSignXOnly(data, privKey, xOnlyPubKey, nonce) ?: error("Sign failed") + } + + actual fun verifySchnorr( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean { + ensureLoaded() + return nativeSchnorrVerify(signature, hash, pubKey) + } + + actual fun verifySchnorrFast( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean { + ensureLoaded() + return nativeSchnorrVerifyFast(signature, hash, pubKey) + } + + actual fun verifySchnorrBatch( + pubKey: ByteArray, + signatures: List, + messages: List, + ): Boolean { + ensureLoaded() + return nativeSchnorrVerifyBatch( + pubKey, + signatures.toTypedArray(), + messages.toTypedArray(), + ) + } + + actual fun privateKeyAdd( + first: ByteArray, + second: ByteArray, + ): ByteArray { + ensureLoaded() + return nativePrivKeyTweakAdd(first, second) ?: error("Tweak add failed") + } + + actual fun pubKeyTweakMulCompact( + pubKey: ByteArray, + privateKey: ByteArray, + ): ByteArray { + ensureLoaded() + val compressedPub = ByteArray(33) + compressedPub[0] = 0x02 + pubKey.copyInto(compressedPub, 1, 0, 32) + val result = nativePubKeyTweakMul(compressedPub, privateKey) ?: error("Tweak mul failed") + return result.copyOfRange(1, 33) + } + + actual fun ecdhXOnly( + xOnlyPub: ByteArray, + scalar: ByteArray, + ): ByteArray { + ensureLoaded() + return nativeEcdhXOnly(xOnlyPub, scalar) ?: error("ECDH failed") + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.kt new file mode 100644 index 000000000..39d09ace7 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.kt @@ -0,0 +1,86 @@ +/* + * 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 + +/** + * Wrapper for the custom C secp256k1 implementation via JNI. + * + * This provides the same interface as [Secp256k1InstanceKotlin] but delegates + * to our own C implementation (not ACINQ's) for benchmarking comparison. + * + * Three implementations can be compared: + * - [Secp256k1Instance]: Platform default (ACINQ JNI on JVM/Android, pure Kotlin on native) + * - [Secp256k1InstanceKotlin]: Pure Kotlin implementation (all platforms) + * - [Secp256k1InstanceC]: Our custom C implementation via JNI (JVM/Android only) + */ +expect object Secp256k1InstanceC { + fun init() + + fun compressedPubKeyFor(privKey: ByteArray): ByteArray + + fun isPrivateKeyValid(il: ByteArray): Boolean + + fun signSchnorr( + data: ByteArray, + privKey: ByteArray, + nonce: ByteArray? = null, + ): ByteArray + + fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray? = null, + ): ByteArray + + fun verifySchnorr( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean + + fun verifySchnorrFast( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean + + fun verifySchnorrBatch( + pubKey: ByteArray, + signatures: List, + messages: List, + ): Boolean + + fun privateKeyAdd( + first: ByteArray, + second: ByteArray, + ): ByteArray + + fun pubKeyTweakMulCompact( + pubKey: ByteArray, + privateKey: ByteArray, + ): ByteArray + + fun ecdhXOnly( + xOnlyPub: ByteArray, + scalar: ByteArray, + ): ByteArray +} diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1C.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1C.kt new file mode 100644 index 000000000..ca24e6427 --- /dev/null +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1C.kt @@ -0,0 +1,87 @@ +/* + * 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 + +object Secp256k1C { + private var loaded = false + + fun ensureLoaded() { + if (!loaded) { + System.loadLibrary("secp256k1_amethyst_jni") + nativeInit() + loaded = true + } + } + + @JvmStatic external fun nativeInit() + + @JvmStatic external fun nativePubkeyCreate(seckey: ByteArray): ByteArray? + + @JvmStatic external fun nativePubkeyCompress(pubkey: ByteArray): ByteArray? + + @JvmStatic external fun nativeSecKeyVerify(seckey: ByteArray): Boolean + + @JvmStatic external fun nativeSchnorrSign( + msg: ByteArray, + seckey: ByteArray, + auxrand: ByteArray?, + ): ByteArray? + + @JvmStatic external fun nativeSchnorrSignXOnly( + msg: ByteArray, + seckey: ByteArray, + xonlyPub: ByteArray, + auxrand: ByteArray?, + ): ByteArray? + + @JvmStatic external fun nativeSchnorrVerify( + sig: ByteArray, + msg: ByteArray, + pub: ByteArray, + ): Boolean + + @JvmStatic external fun nativeSchnorrVerifyFast( + sig: ByteArray, + msg: ByteArray, + pub: ByteArray, + ): Boolean + + @JvmStatic external fun nativeSchnorrVerifyBatch( + pub: ByteArray, + sigs: Array, + msgs: Array, + ): Boolean + + @JvmStatic external fun nativePrivKeyTweakAdd( + seckey: ByteArray, + tweak: ByteArray, + ): ByteArray? + + @JvmStatic external fun nativePubKeyTweakMul( + pubkey: ByteArray, + tweak: ByteArray, + ): ByteArray? + + @JvmStatic external fun nativeEcdhXOnly( + xonlyPub: ByteArray, + scalar: ByteArray, + ): ByteArray? +} diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.jvm.kt new file mode 100644 index 000000000..9488d1aad --- /dev/null +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.jvm.kt @@ -0,0 +1,116 @@ +/* + * 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 + +actual object Secp256k1InstanceC { + actual fun init() { + Secp256k1C.ensureLoaded() + } + + actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray { + Secp256k1C.ensureLoaded() + val pub65 = Secp256k1C.nativePubkeyCreate(privKey) ?: error("Invalid private key") + return Secp256k1C.nativePubkeyCompress(pub65) ?: error("Compression failed") + } + + actual fun isPrivateKeyValid(il: ByteArray): Boolean { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSecKeyVerify(il) + } + + actual fun signSchnorr( + data: ByteArray, + privKey: ByteArray, + nonce: ByteArray?, + ): ByteArray { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrSign(data, privKey, nonce) ?: error("Sign failed") + } + + actual fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray?, + ): ByteArray { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrSignXOnly(data, privKey, xOnlyPubKey, nonce) ?: error("Sign failed") + } + + actual fun verifySchnorr( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrVerify(signature, hash, pubKey) + } + + actual fun verifySchnorrFast( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrVerifyFast(signature, hash, pubKey) + } + + actual fun verifySchnorrBatch( + pubKey: ByteArray, + signatures: List, + messages: List, + ): Boolean { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrVerifyBatch( + pubKey, + signatures.toTypedArray(), + messages.toTypedArray(), + ) + } + + actual fun privateKeyAdd( + first: ByteArray, + second: ByteArray, + ): ByteArray { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativePrivKeyTweakAdd(first, second) ?: error("Tweak add failed") + } + + actual fun pubKeyTweakMulCompact( + pubKey: ByteArray, + privateKey: ByteArray, + ): ByteArray { + Secp256k1C.ensureLoaded() + val compressedPub = ByteArray(33) + compressedPub[0] = 0x02 + pubKey.copyInto(compressedPub, 1, 0, 32) + val result = Secp256k1C.nativePubKeyTweakMul(compressedPub, privateKey) ?: error("Tweak mul failed") + return result.copyOfRange(1, 33) + } + + actual fun ecdhXOnly( + xOnlyPub: ByteArray, + scalar: ByteArray, + ): ByteArray { + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeEcdhXOnly(xOnlyPub, scalar) ?: error("ECDH failed") + } +} diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1TripleBenchmark.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1TripleBenchmark.kt new file mode 100644 index 000000000..ae5be1566 --- /dev/null +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1TripleBenchmark.kt @@ -0,0 +1,333 @@ +/* + * 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.Secp256k1InstanceC +import kotlin.test.Test +import kotlin.test.assertTrue +import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1 + +/** + * Three-way benchmark comparing: + * 1. ACINQ C (libsecp256k1 via JNI) — the established reference + * 2. Pure Kotlin (our KMP implementation) — portable, no native deps + * 3. Custom C (our new C implementation via JNI) — maximum performance target + * + * Run with: ./gradlew :quartz:jvmTest --tests "*.Secp256k1TripleBenchmark" + */ +class Secp256k1TripleBenchmark { + private val privKey = hexToBytes("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530") + private val msg32 = hexToBytes("243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89") + private val auxRand = hexToBytes("0000000000000000000000000000000000000000000000000000000000000001") + private val privKey2 = hexToBytes("3982F19BEF1615BCCFBB05E321C10E1D4CBA3DF0E841C2E41EEB6016347653C3") + private val pub2xOnly = hexToBytes("c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133") + private val h02 = byteArrayOf(0x02) + + private val acinq = NativeSecp256k1.get() + private val acinqPubKey = acinq.pubKeyCompress(acinq.pubkeyCreate(privKey)) + private val acinqXOnlyPub = acinqPubKey.copyOfRange(1, 33) + private val acinqSig = acinq.signSchnorr(msg32, privKey, auxRand) + + private val kotlinPubKey = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privKey)) + private val kotlinXOnlyPub = kotlinPubKey.copyOfRange(1, 33) + private val kotlinSig = Secp256k1.signSchnorr(msg32, privKey, auxRand) + + private data class TripleResult( + val name: String, + val acinqNanos: Long, + val kotlinNanos: Long, + val cNanos: Long, + val iterations: Int, + ) { + val acinqOps get() = iterations * 1_000_000_000L / acinqNanos + val kotlinOps get() = iterations * 1_000_000_000L / kotlinNanos + val cOps get() = if (cNanos > 0) iterations * 1_000_000_000L / cNanos else 0L + val kotlinRatio get() = kotlinNanos.toDouble() / acinqNanos + val cRatio get() = if (cNanos > 0) cNanos.toDouble() / acinqNanos else 0.0 + + fun format(): String { + val cStr = if (cNanos > 0) String.format("%,10d ops/s", cOps) else " (N/A) " + val cRat = if (cNanos > 0) String.format("%.2fx", cRatio) else " N/A " + return String.format( + "%-26s %,10d ops/s %,10d ops/s %s %.2fx %s", + name, + acinqOps, + kotlinOps, + cStr, + kotlinRatio, + cRat, + ) + } + } + + private inline fun benchTriple( + name: String, + warmup: Int, + iterations: Int, + crossinline acinqOp: () -> Unit, + crossinline kotlinOp: () -> Unit, + crossinline cOp: (() -> Unit)? = null, + ): TripleResult { + repeat(warmup) { acinqOp() } + val acinqStart = System.nanoTime() + repeat(iterations) { acinqOp() } + val acinqNs = System.nanoTime() - acinqStart + + repeat(warmup) { kotlinOp() } + val kotlinStart = System.nanoTime() + repeat(iterations) { kotlinOp() } + val kotlinNs = System.nanoTime() - kotlinStart + + var cNs = 0L + if (cOp != null) { + repeat(warmup) { cOp() } + val cStart = System.nanoTime() + repeat(iterations) { cOp() } + cNs = System.nanoTime() - cStart + } + + return TripleResult(name, acinqNs, kotlinNs, cNs, iterations) + } + + @Test + fun benchmarkAllThree() { + // Verify signature compatibility + assertTrue(acinqSig.contentEquals(kotlinSig), "ACINQ and Kotlin signatures must match") + + // Try to load C library (may not be available in all environments) + var cAvailable = false + try { + Secp256k1InstanceC.init() + cAvailable = true + } catch (e: UnsatisfiedLinkError) { + println("NOTE: Custom C library not available (${e.message})") + println(" Build it with: cd quartz/src/main/c/secp256k1 && mkdir build && cd build && cmake .. && make") + println(" Then add build/ to java.library.path") + } + + val cPubKey = if (cAvailable) Secp256k1InstanceC.compressedPubKeyFor(privKey) else null + val cXOnlyPub = cPubKey?.copyOfRange(1, 33) + val cSig = + if (cAvailable) { + Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) + } else { + null + } + + if (cAvailable && cSig != null) { + // Verify C signatures are compatible + assertTrue( + acinq.verifySchnorr(cSig, msg32, acinqXOnlyPub), + "ACINQ should verify C signature", + ) + assertTrue( + Secp256k1InstanceC.verifySchnorr(acinqSig, msg32, acinqXOnlyPub), + "C should verify ACINQ signature", + ) + } + + val results = mutableListOf() + + // --- Verify Schnorr (fast, x-check only) --- + results += + benchTriple( + name = "verifySchnorrFast", + warmup = 2000, + iterations = 5000, + acinqOp = { acinq.verifySchnorr(acinqSig, msg32, acinqXOnlyPub) }, + kotlinOp = { Secp256k1.verifySchnorrFast(kotlinSig, msg32, kotlinXOnlyPub) }, + cOp = + if (cAvailable && cSig != null && cXOnlyPub != null) { + { Secp256k1InstanceC.verifySchnorrFast(cSig, msg32, cXOnlyPub) } + } else { + null + }, + ) + + // --- Verify Schnorr (strict BIP-340) --- + results += + benchTriple( + name = "verifySchnorr", + warmup = 2000, + iterations = 5000, + acinqOp = { acinq.verifySchnorr(acinqSig, msg32, acinqXOnlyPub) }, + kotlinOp = { Secp256k1.verifySchnorr(kotlinSig, msg32, kotlinXOnlyPub) }, + cOp = + if (cAvailable && cSig != null && cXOnlyPub != null) { + { Secp256k1InstanceC.verifySchnorr(cSig, msg32, cXOnlyPub) } + } else { + null + }, + ) + + // --- Sign Schnorr (with cached x-only pubkey) --- + results += + benchTriple( + name = "signSchnorr (cached pk)", + warmup = 1000, + iterations = 5000, + acinqOp = { acinq.signSchnorr(msg32, privKey, auxRand) }, + kotlinOp = { Secp256k1.signSchnorrWithXOnlyPubKey(msg32, privKey, kotlinXOnlyPub, auxRand) }, + cOp = + if (cAvailable && cXOnlyPub != null) { + { Secp256k1InstanceC.signSchnorrWithXOnlyPubKey(msg32, privKey, cXOnlyPub, auxRand) } + } else { + null + }, + ) + + // --- Sign Schnorr (derives pubkey) --- + results += + benchTriple( + name = "signSchnorr", + warmup = 1000, + iterations = 3000, + acinqOp = { acinq.signSchnorr(msg32, privKey, auxRand) }, + kotlinOp = { Secp256k1.signSchnorr(msg32, privKey, auxRand) }, + cOp = + if (cAvailable) { + { Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) } + } else { + null + }, + ) + + // --- Pubkey create + compress --- + results += + benchTriple( + name = "pubkeyCreate+Compress", + warmup = 1000, + iterations = 5000, + acinqOp = { acinq.pubKeyCompress(acinq.pubkeyCreate(privKey)) }, + kotlinOp = { Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privKey)) }, + cOp = + if (cAvailable) { + { Secp256k1InstanceC.compressedPubKeyFor(privKey) } + } else { + null + }, + ) + + // --- ECDH --- + results += + benchTriple( + name = "ecdhXOnly (NIP-44)", + warmup = 1000, + iterations = 3000, + acinqOp = { acinq.pubKeyTweakMul(h02 + pub2xOnly, privKey).copyOfRange(1, 33) }, + kotlinOp = { Secp256k1.ecdhXOnly(pub2xOnly, privKey) }, + cOp = + if (cAvailable) { + { Secp256k1InstanceC.ecdhXOnly(pub2xOnly, privKey) } + } else { + null + }, + ) + + // Print header + println() + println("=".repeat(100)) + println("secp256k1 Three-Way Benchmark: ACINQ (C/JNI) vs Kotlin (KMP) vs Custom C (JNI)") + println("=".repeat(100)) + println( + String.format( + "%-26s %14s %14s %14s %s %s", + "Operation", + "ACINQ(C/JNI)", + "Kotlin(KMP)", + "Custom C(JNI)", + "Kt/A", + "C/A", + ), + ) + println("-".repeat(100)) + for (r in results) { + println(r.format()) + } + + // Batch verification benchmarks + println("-".repeat(100)) + println("Batch Verification (same pubkey, N events)") + println("-".repeat(100)) + + val batchPub = kotlinXOnlyPub + for (batchSize in intArrayOf(8, 16, 32, 200)) { + val sigs = mutableListOf() + val msgs = mutableListOf() + for (i in 0 until batchSize) { + val m = ByteArray(32) { (i * 7 + it).toByte() } + sigs.add(Secp256k1.signSchnorr(m, privKey, auxRand)) + msgs.add(m) + } + val iters = 1000 + + // Warmup + repeat(500) { Secp256k1.verifySchnorrBatch(batchPub, sigs, msgs) } + + // Time individual ACINQ + repeat(500) { sigs.forEach { sig -> acinq.verifySchnorr(sig, msgs[sigs.indexOf(sig)], acinqXOnlyPub) } } + val acinqStart = System.nanoTime() + repeat(iters) { sigs.forEachIndexed { j, sig -> acinq.verifySchnorr(sig, msgs[j], acinqXOnlyPub) } } + val acinqNs = System.nanoTime() - acinqStart + val acinqEvSec = iters.toLong() * batchSize * 1_000_000_000L / acinqNs + + // Time Kotlin batch + val ktBatchStart = System.nanoTime() + repeat(iters) { Secp256k1.verifySchnorrBatch(batchPub, sigs, msgs) } + val ktBatchNs = System.nanoTime() - ktBatchStart + val ktBatchEvSec = iters.toLong() * batchSize * 1_000_000_000L / ktBatchNs + + // Time C batch (if available) + var cBatchEvSec = 0L + if (cAvailable) { + repeat(500) { Secp256k1InstanceC.verifySchnorrBatch(batchPub, sigs, msgs) } + val cBatchStart = System.nanoTime() + repeat(iters) { Secp256k1InstanceC.verifySchnorrBatch(batchPub, sigs, msgs) } + val cBatchNs = System.nanoTime() - cBatchStart + cBatchEvSec = iters.toLong() * batchSize * 1_000_000_000L / cBatchNs + } + + val batchSpeedup = acinqNs.toDouble() / ktBatchNs + val cStr = if (cAvailable) String.format("%,10d ev/s", cBatchEvSec) else " (N/A) " + println( + String.format( + " batch(%3d) %,10d ev/s %,10d ev/s %s %.1fx", + batchSize, + acinqEvSec, + ktBatchEvSec, + cStr, + batchSpeedup, + ), + ) + } + println("=".repeat(100)) + println() + } + + private fun hexToBytes(hex: String): ByteArray { + val len = hex.length / 2 + val result = ByteArray(len) + for (i in 0 until len) { + result[i] = hex.substring(i * 2, i * 2 + 2).toInt(16).toByte() + } + return result + } +} diff --git a/quartz/src/main/c/CMakeLists.txt b/quartz/src/main/c/CMakeLists.txt new file mode 100644 index 000000000..eb3fcd234 --- /dev/null +++ b/quartz/src/main/c/CMakeLists.txt @@ -0,0 +1,6 @@ +# Root CMakeLists for Android NDK builds. +# Called from quartz/build.gradle.kts via android { externalNativeBuild { cmake { } } } +cmake_minimum_required(VERSION 3.18) +project(secp256k1_amethyst C) + +add_subdirectory(secp256k1) diff --git a/quartz/src/main/c/secp256k1/CMakeLists.txt b/quartz/src/main/c/secp256k1/CMakeLists.txt new file mode 100644 index 000000000..f3b7c0063 --- /dev/null +++ b/quartz/src/main/c/secp256k1/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.18) +project(secp256k1_amethyst C) + +set(CMAKE_C_STANDARD 11) + +# ==================== Platform-specific optimizations ==================== + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") + message(STATUS "ARM64 detected - enabling NEON and crypto extensions") + set(PLATFORM_FLAGS "-march=armv8-a+crypto -O3 -fomit-frame-pointer") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64") + message(STATUS "x86_64 detected - enabling BMI2 and ADX") + set(PLATFORM_FLAGS "-march=x86-64-v2 -O3 -fomit-frame-pointer") +else() + message(STATUS "Generic platform - using portable implementation") + set(PLATFORM_FLAGS "-O3 -fomit-frame-pointer") +endif() + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${PLATFORM_FLAGS}") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-parameter") + +# ==================== Library sources ==================== + +set(SECP256K1_SOURCES + field.c + scalar.c + point.c + schnorr.c + sha256.c +) + +# Static library +add_library(secp256k1_amethyst STATIC ${SECP256K1_SOURCES}) +target_include_directories(secp256k1_amethyst PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# Shared library (for JNI) +add_library(secp256k1_amethyst_jni SHARED ${SECP256K1_SOURCES}) +target_include_directories(secp256k1_amethyst_jni PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# ==================== JNI bridge ==================== + +if(JNI_INCLUDE_DIR) + target_sources(secp256k1_amethyst_jni PRIVATE jni_bridge.c) + target_include_directories(secp256k1_amethyst_jni PRIVATE ${JNI_INCLUDE_DIR}) + if(JNI_INCLUDE_DIR_PLATFORM) + target_include_directories(secp256k1_amethyst_jni PRIVATE ${JNI_INCLUDE_DIR_PLATFORM}) + endif() +endif() + +# ==================== Android NDK build ==================== + +if(ANDROID) + # Android-specific: build shared library for JNI + find_library(log-lib log) + target_link_libraries(secp256k1_amethyst_jni ${log-lib}) + + # Include JNI headers from NDK + target_sources(secp256k1_amethyst_jni PRIVATE jni_bridge.c) +endif() + +# ==================== Standalone benchmark ==================== + +if(NOT ANDROID) + add_executable(secp256k1_bench benchmark.c) + target_link_libraries(secp256k1_bench secp256k1_amethyst) + if(UNIX) + target_link_libraries(secp256k1_bench m) + endif() +endif() diff --git a/quartz/src/main/c/secp256k1/benchmark.c b/quartz/src/main/c/secp256k1/benchmark.c new file mode 100644 index 000000000..78b459846 --- /dev/null +++ b/quartz/src/main/c/secp256k1/benchmark.c @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Standalone C benchmark for secp256k1 operations. + * Mirrors the Kotlin benchmarks for direct comparison. + * + * Build: mkdir build && cd build && cmake .. && make + * Run: ./secp256k1_bench + */ +#include "secp256k1_c.h" +#include "sha256.h" +#include +#include +#include +#include + +/* ==================== Timing ==================== */ + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; +} + +/* ==================== Test Data (matches Kotlin benchmarks) ==================== */ + +static const uint8_t TEST_PRIVKEY[32] = { + 0xd2, 0x17, 0xc1, 0xfd, 0x12, 0x40, 0xad, 0x3e, + 0xe6, 0x8f, 0x38, 0xd4, 0xab, 0x4e, 0x6e, 0x95, + 0xf2, 0x0f, 0x3e, 0x09, 0xdd, 0x51, 0x42, 0x90, + 0x00, 0xab, 0xc2, 0xb4, 0xda, 0x5b, 0xe3, 0xa3 +}; + +static const uint8_t TEST_AUXRAND[32] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20 +}; + +typedef struct { + const char *name; + double total_ms; + int iterations; + double ops_per_sec; +} bench_result; + +#define MAX_RESULTS 32 +static bench_result results[MAX_RESULTS]; +static int result_count = 0; + +static void record_result(const char *name, double total_ms, int iters) { + if (result_count >= MAX_RESULTS) return; + bench_result *r = &results[result_count++]; + r->name = name; + r->total_ms = total_ms; + r->iterations = iters; + r->ops_per_sec = iters / (total_ms / 1000.0); +} + +/* ==================== Benchmarks ==================== */ + +static void bench_pubkey_create(int iters) { + uint8_t pub65[65]; + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_pubkey_create(pub65, TEST_PRIVKEY); + } + record_result("pubkeyCreate", now_ms() - start, iters); +} + +static void bench_sign(int iters) { + uint8_t msg[32] = {0}; + uint8_t sig[64]; + secp256k1_sha256_hash(msg, (const uint8_t *)"test message", 12); + + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_schnorr_sign(sig, msg, 32, TEST_PRIVKEY, TEST_AUXRAND); + } + record_result("signSchnorr", now_ms() - start, iters); +} + +static void bench_sign_xonly(int iters) { + /* Pre-compute x-only pubkey */ + uint8_t pub65[65]; + secp256k1c_pubkey_create(pub65, TEST_PRIVKEY); + uint8_t xonly[32]; + memcpy(xonly, pub65 + 1, 32); + + uint8_t msg[32] = {0}; + uint8_t sig[64]; + secp256k1_sha256_hash(msg, (const uint8_t *)"test message", 12); + + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_schnorr_sign_xonly(sig, msg, 32, TEST_PRIVKEY, xonly, TEST_AUXRAND); + } + record_result("signSchnorrXOnly (cached pubkey)", now_ms() - start, iters); +} + +static void bench_verify(int iters) { + /* Create a valid signature */ + uint8_t pub65[65]; + secp256k1c_pubkey_create(pub65, TEST_PRIVKEY); + uint8_t xonly[32]; + memcpy(xonly, pub65 + 1, 32); + + uint8_t msg[32]; + secp256k1_sha256_hash(msg, (const uint8_t *)"test message for verify", 23); + + uint8_t sig[64]; + secp256k1c_schnorr_sign_xonly(sig, msg, 32, TEST_PRIVKEY, xonly, TEST_AUXRAND); + + /* Verify it first */ + if (!secp256k1c_schnorr_verify(sig, msg, 32, xonly)) { + printf("ERROR: Self-verification failed!\n"); + return; + } + + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_schnorr_verify(sig, msg, 32, xonly); + } + record_result("verifySchnorr", now_ms() - start, iters); +} + +static void bench_verify_fast(int iters) { + uint8_t pub65[65]; + secp256k1c_pubkey_create(pub65, TEST_PRIVKEY); + uint8_t xonly[32]; + memcpy(xonly, pub65 + 1, 32); + + uint8_t msg[32]; + secp256k1_sha256_hash(msg, (const uint8_t *)"test message for verify fast", 27); + + uint8_t sig[64]; + secp256k1c_schnorr_sign_xonly(sig, msg, 32, TEST_PRIVKEY, xonly, TEST_AUXRAND); + + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_schnorr_verify_fast(sig, msg, 32, xonly); + } + record_result("verifySchnorrFast", now_ms() - start, iters); +} + +static void bench_verify_batch(int batch_size, int iters) { + uint8_t pub65[65]; + secp256k1c_pubkey_create(pub65, TEST_PRIVKEY); + uint8_t xonly[32]; + memcpy(xonly, pub65 + 1, 32); + + /* Create batch_size different messages and signatures */ + uint8_t **sigs = (uint8_t **)malloc((size_t)batch_size * sizeof(uint8_t *)); + uint8_t **msgs = (uint8_t **)malloc((size_t)batch_size * sizeof(uint8_t *)); + size_t *lens = (size_t *)malloc((size_t)batch_size * sizeof(size_t)); + + for (int i = 0; i < batch_size; i++) { + msgs[i] = (uint8_t *)malloc(32); + sigs[i] = (uint8_t *)malloc(64); + lens[i] = 32; + + uint8_t seed[4] = {(uint8_t)i, (uint8_t)(i>>8), (uint8_t)(i>>16), (uint8_t)(i>>24)}; + secp256k1_sha256_hash(msgs[i], seed, 4); + secp256k1c_schnorr_sign_xonly(sigs[i], msgs[i], 32, TEST_PRIVKEY, xonly, TEST_AUXRAND); + } + + char name[64]; + snprintf(name, sizeof(name), "verifySchnorrBatch(%d)", batch_size); + + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_schnorr_verify_batch(xonly, + (const uint8_t *const *)sigs, + (const uint8_t *const *)msgs, + lens, (size_t)batch_size); + } + record_result(name, now_ms() - start, iters); + + for (int i = 0; i < batch_size; i++) { + free(msgs[i]); + free(sigs[i]); + } + free(sigs); + free(msgs); + free(lens); +} + +static void bench_ecdh(int iters) { + uint8_t pub65[65]; + secp256k1c_pubkey_create(pub65, TEST_PRIVKEY); + uint8_t xonly[32]; + memcpy(xonly, pub65 + 1, 32); + + /* Use a different key as scalar */ + uint8_t scalar[32]; + secp256k1_sha256_hash(scalar, TEST_PRIVKEY, 32); + /* Ensure it's a valid scalar */ + scalar[0] &= 0x7F; /* keep below n */ + + uint8_t result[32]; + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_ecdh_xonly(result, xonly, scalar); + } + record_result("ecdhXOnly", now_ms() - start, iters); +} + +static void bench_seckey_verify(int iters) { + double start = now_ms(); + for (int i = 0; i < iters; i++) { + secp256k1c_seckey_verify(TEST_PRIVKEY); + } + record_result("secKeyVerify", now_ms() - start, iters); +} + +/* ==================== Main ==================== */ + +int main(void) { + printf("================================================================\n"); + printf(" Amethyst secp256k1 C Implementation Benchmark\n"); + printf("================================================================\n"); + printf("Initializing precomputed tables...\n"); + double init_start = now_ms(); + secp256k1c_init(); + printf("Initialization: %.1f ms\n\n", now_ms() - init_start); + + /* Warmup */ + printf("Warming up...\n"); + bench_pubkey_create(100); + bench_sign(100); + bench_verify(100); + result_count = 0; /* Reset */ + + printf("Running benchmarks...\n\n"); + + int N = 5000; + + bench_seckey_verify(N * 100); + bench_pubkey_create(N); + bench_sign(N); + bench_sign_xonly(N); + bench_verify(N); + bench_verify_fast(N); + bench_verify_batch(8, N / 4); + bench_verify_batch(16, N / 8); + bench_verify_batch(32, N / 16); + bench_verify_batch(200, N / 50); + bench_ecdh(N); + + /* Print results */ + printf("\n%-40s %10s %10s %12s\n", "Operation", "Iters", "Time(ms)", "Ops/sec"); + printf("%-40s %10s %10s %12s\n", "─────────", "─────", "───────", "───────"); + for (int i = 0; i < result_count; i++) { + bench_result *r = &results[i]; + printf("%-40s %10d %10.1f %12.0f\n", + r->name, r->iterations, r->total_ms, r->ops_per_sec); + } + + printf("\n================================================================\n"); + printf(" Per-operation costs (microseconds):\n"); + printf("================================================================\n"); + for (int i = 0; i < result_count; i++) { + bench_result *r = &results[i]; + double us_per_op = (r->total_ms * 1000.0) / r->iterations; + printf(" %-38s %8.1f µs\n", r->name, us_per_op); + } + + return 0; +} diff --git a/quartz/src/main/c/secp256k1/field.c b/quartz/src/main/c/secp256k1/field.c new file mode 100644 index 000000000..6a97dba12 --- /dev/null +++ b/quartz/src/main/c/secp256k1/field.c @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Field arithmetic mod p = 2^256 - 2^32 - 977, using 5x52-bit limbs. + * + * The 5x52 representation gives 12 bits of headroom per limb, enabling + * lazy reduction: multiple adds/subs can chain without normalizing. This + * is the key advantage over the Kotlin 4x64-bit approach which must + * reduce after every single add/sub. + * + * On ARM64/x86_64 with __int128: each limb multiply is a single MUL+UMULH + * (ARM64) or MULQ (x86_64) instruction pair, vs the Kotlin version which + * needs Math.multiplyHigh() + unsigned correction (5 JVM instructions). + */ +#include "field.h" +#include + +/* ==================== Field Multiplication ==================== */ + +/* + * Multiply: r = a * b mod p + * + * Uses schoolbook 5x5 multiplication with __int128 for the 64x64->128 products. + * After computing the full 10-limb product, reduces mod p using: + * 2^260 = 2^4 * (2^32 + 977) = 16 * 0x1000003D1 mod p + * + * The reduction folds the high limbs back using the secp256k1 constant R = 2^256 mod p. + * For 5x52 limbs, the folding constant for limb 5 is 0x1000003D1 (since 2^260 = 2^4 * (2^32+977)). + */ +#if HAVE_INT128 + +void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { + const uint64_t M = FE_LIMB_MASK; + /* 2^260 mod p: each limb is 52 bits, so position 5 is at 260 bits. + * 2^260 mod p = 2^4 * (2^256 mod p) = 16 * 0x1000003D1 = 0x10000003D10 */ + const uint64_t R = 0x1000003D10ULL; /* 2^260 mod p = 16 * (2^32 + 977) */ + uint128_t c, d; + uint64_t t0, t1, t2, t3, t4; + uint64_t a0 = a->d[0], a1 = a->d[1], a2 = a->d[2], a3 = a->d[3], a4 = a->d[4]; + uint64_t b0 = b->d[0], b1 = b->d[1], b2 = b->d[2], b3 = b->d[3], b4 = b->d[4]; + + /* + * libsecp256k1-style split R-folding. The sum of folded products c can be up to + * ~106 bits. c*R would overflow uint128 (up to 140 bits). Instead, we split: + * d += (uint64_t)c * R (low 64 bits × R, fits in ~98 bits) + * carry (c >> 64) * R into the next limb's accumulator + */ + + /* + * Correct approach: expand c*R into individual products a[i]*b[j]*R. + * Each a[i]*b[j]*R < 2^52 * 2^52 * 2^34 = 2^138 which overflows uint128. + * + * Real solution: split R into a[i]*R (uint128) before multiplying by b[j]. + * a[i]*R is at most 2^86 which fits in uint128. Then (uint128)(a[i]*R) * b[j] + * is at most 2^138 which ALSO overflows uint128! + * + * The ACTUAL libsecp256k1 trick: use d and c as two separate accumulators. + * d accumulates the direct products. c accumulates the folded products with + * a DIFFERENT carry chain. Let me just do the schoolbook 10-limb product + * and then reduce. + */ + { + /* Full 10-limb product, then reduce mod p using R = 2^260 / 2^4 fold */ + uint128_t p[10] = {0}; + int i, j; + for (i = 0; i < 5; i++) { + for (j = 0; j < 5; j++) { + p[i+j] += (uint128_t)a->d[i] * b->d[j]; + } + } + /* Propagate carries in the 10-limb product */ + for (i = 0; i < 9; i++) { + p[i+1] += p[i] >> 52; + p[i] &= M; + } + /* Fold high limbs using R = 0x1000003D1 (2^256 mod p in 5x52) */ + /* Limbs 5-9 fold into 0-4: p[i+5] * R adds to p[i] */ + for (i = 4; i >= 0; i--) { + if (i + 5 <= 9 && p[i+5]) { + uint128_t fold = p[i+5] * R; + p[i] += fold; + } + } + /* Propagate carries again */ + for (i = 0; i < 4; i++) { + p[i+1] += p[i] >> 52; + p[i] &= M; + } + /* Fold any remaining overflow from limb 4. + * Limb 4 overflow is at bit position 4*52+48 = 256, so use 2^256 mod p = 0x1000003D1 */ + if (p[4] >> 48) { + uint64_t overflow = (uint64_t)(p[4] >> 48); + p[4] &= 0xFFFFFFFFFFFFULL; + p[0] += (uint128_t)overflow * 0x1000003D1ULL; + p[1] += p[0] >> 52; p[0] &= M; + p[2] += p[1] >> 52; p[1] &= M; + p[3] += p[2] >> 52; p[2] &= M; + p[4] += p[3] >> 52; p[3] &= M; + } + r->d[0] = (uint64_t)p[0]; r->d[1] = (uint64_t)p[1]; r->d[2] = (uint64_t)p[2]; + r->d[3] = (uint64_t)p[3]; r->d[4] = (uint64_t)p[4]; + } +} + +void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) { + const uint64_t M = FE_LIMB_MASK; + /* 2^260 mod p: each limb is 52 bits, so position 5 is at 260 bits. + * 2^260 mod p = 2^4 * (2^256 mod p) = 16 * 0x1000003D1 = 0x10000003D10 */ + const uint64_t R = 0x1000003D10ULL; /* 2^260 mod p = 16 * (2^32 + 977) */ + uint128_t c, d; + uint64_t t0, t1, t2, t3, t4; + uint64_t a0 = a->d[0], a1 = a->d[1], a2 = a->d[2], a3 = a->d[3], a4 = a->d[4]; + + /* Same split R-folding approach as fe_mul, with doubled cross-products */ + + /* Use schoolbook product + reduction (same as fe_mul but with doubled cross-products) */ + { + uint128_t p[10] = {0}; + int i, j; + for (i = 0; i < 5; i++) { + for (j = 0; j < 5; j++) { + p[i+j] += (uint128_t)a->d[i] * a->d[j]; + } + } + for (i = 0; i < 9; i++) { + p[i+1] += p[i] >> 52; + p[i] &= M; + } + for (i = 4; i >= 0; i--) { + if (i + 5 <= 9 && p[i+5]) { + p[i] += p[i+5] * R; + } + } + for (i = 0; i < 4; i++) { + p[i+1] += p[i] >> 52; + p[i] &= M; + } + if (p[4] >> 48) { + uint64_t overflow = (uint64_t)(p[4] >> 48); + p[4] &= 0xFFFFFFFFFFFFULL; + p[0] += (uint128_t)overflow * 0x1000003D1ULL; /* 2^256 mod p */ + p[1] += p[0] >> 52; p[0] &= M; + p[2] += p[1] >> 52; p[1] &= M; + p[3] += p[2] >> 52; p[2] &= M; + p[4] += p[3] >> 52; p[3] &= M; + } + t0 = (uint64_t)p[0]; t1 = (uint64_t)p[1]; t2 = (uint64_t)p[2]; + t3 = (uint64_t)p[3]; t4 = (uint64_t)p[4]; + } + + r->d[0] = t0; r->d[1] = t1; r->d[2] = t2; r->d[3] = t3; r->d[4] = t4; +} + +#else /* Portable fallback without __int128 */ + +/* Split 64x64 multiply into 32-bit pieces */ +static inline void mul64(uint64_t *hi, uint64_t *lo, uint64_t a, uint64_t b) { + uint64_t a_lo = a & 0xFFFFFFFF; + uint64_t a_hi = a >> 32; + uint64_t b_lo = b & 0xFFFFFFFF; + uint64_t b_hi = b >> 32; + + uint64_t ll = a_lo * b_lo; + uint64_t lh = a_lo * b_hi; + uint64_t hl = a_hi * b_lo; + uint64_t hh = a_hi * b_hi; + + uint64_t mid = (ll >> 32) + (lh & 0xFFFFFFFF) + (hl & 0xFFFFFFFF); + *lo = (ll & 0xFFFFFFFF) | (mid << 32); + *hi = hh + (lh >> 32) + (hl >> 32) + (mid >> 32); +} + +void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { + /* Portable schoolbook with manual carry tracking */ + const uint64_t M = FE_LIMB_MASK; + /* 2^260 mod p: each limb is 52 bits, so position 5 is at 260 bits. + * 2^260 mod p = 2^4 * (2^256 mod p) = 16 * 0x1000003D1 = 0x10000003D10 */ + const uint64_t R = 0x1000003D10ULL; /* 2^260 mod p = 16 * (2^32 + 977) */ + uint64_t c_hi, c_lo, tmp_hi, tmp_lo; + uint64_t t[5] = {0}; + int i, j; + + /* Simplified portable version - accumulate products */ + for (i = 0; i < 5; i++) { + uint64_t acc_lo = 0, acc_hi = 0; + for (j = 0; j <= i; j++) { + mul64(&tmp_hi, &tmp_lo, a->d[j], b->d[i - j]); + acc_lo += tmp_lo; + acc_hi += tmp_hi + (acc_lo < tmp_lo ? 1 : 0); + } + /* Folded products (j+k >= 5 contribute with factor R) */ + for (j = i + 1; j < 5; j++) { + int k = 5 + i - j; + if (k < 5) { + mul64(&tmp_hi, &tmp_lo, a->d[j], b->d[k]); + /* Multiply by R and add */ + mul64(&c_hi, &c_lo, tmp_lo, R); + acc_lo += c_lo; + acc_hi += c_hi + (acc_lo < c_lo ? 1 : 0); + } + } + t[i] = acc_lo & M; + /* Carry to next limb */ + if (i < 4) { + /* Shift right by 52 */ + uint64_t carry = (acc_lo >> 52) | (acc_hi << 12); + t[i + 1] = carry; + } + } + + r->d[0] = t[0]; r->d[1] = t[1]; r->d[2] = t[2]; r->d[3] = t[3]; r->d[4] = t[4]; + fe_normalize(r); +} + +void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) { + fe_mul(r, a, a); +} + +#endif /* HAVE_INT128 */ + +/* ==================== Repeated squaring ==================== */ + +static void fe_sqr_n(secp256k1_fe *r, const secp256k1_fe *a, int n) { + *r = *a; + for (int i = 0; i < n; i++) { + fe_sqr(r, r); + } +} + +/* ==================== Inversion (Fermat: a^(p-2)) ==================== */ + +void fe_inv(secp256k1_fe *r, const secp256k1_fe *a) { + secp256k1_fe x2, x3, x6, x9, x11, x22, x44, x88, x176, x220, x223; + + fe_sqr(&x2, a); + fe_mul(&x2, &x2, a); + + fe_sqr(&x3, &x2); + fe_mul(&x3, &x3, a); + + fe_sqr_n(&x6, &x3, 3); + fe_mul(&x6, &x6, &x3); + + fe_sqr_n(&x9, &x6, 3); + fe_mul(&x9, &x9, &x3); + + fe_sqr_n(&x11, &x9, 2); + fe_mul(&x11, &x11, &x2); + + fe_sqr_n(&x22, &x11, 11); + fe_mul(&x22, &x22, &x11); + + fe_sqr_n(&x44, &x22, 22); + fe_mul(&x44, &x44, &x22); + + fe_sqr_n(&x88, &x44, 44); + fe_mul(&x88, &x88, &x44); + + fe_sqr_n(&x176, &x88, 88); + fe_mul(&x176, &x176, &x88); + + fe_sqr_n(&x220, &x176, 44); + fe_mul(&x220, &x220, &x44); + + fe_sqr_n(&x223, &x220, 3); + fe_mul(&x223, &x223, &x3); + + fe_sqr_n(r, &x223, 23); + fe_mul(r, r, &x22); + fe_sqr_n(r, r, 5); + fe_mul(r, r, a); + fe_sqr_n(r, r, 3); + fe_mul(r, r, &x2); + fe_sqr_n(r, r, 2); + fe_mul(r, r, a); +} + +/* ==================== Square root ==================== */ + +int fe_sqrt(secp256k1_fe *r, const secp256k1_fe *a) { + secp256k1_fe x2, x3, x6, x9, x11, x22, x44, x88, x176, x220, x223; + secp256k1_fe t, check; + + fe_sqr(&x2, a); + fe_mul(&x2, &x2, a); + + fe_sqr(&x3, &x2); + fe_mul(&x3, &x3, a); + + fe_sqr_n(&x6, &x3, 3); + fe_mul(&x6, &x6, &x3); + + fe_sqr_n(&x9, &x6, 3); + fe_mul(&x9, &x9, &x3); + + fe_sqr_n(&x11, &x9, 2); + fe_mul(&x11, &x11, &x2); + + fe_sqr_n(&x22, &x11, 11); + fe_mul(&x22, &x22, &x11); + + fe_sqr_n(&x44, &x22, 22); + fe_mul(&x44, &x44, &x22); + + fe_sqr_n(&x88, &x44, 44); + fe_mul(&x88, &x88, &x44); + + fe_sqr_n(&x176, &x88, 88); + fe_mul(&x176, &x176, &x88); + + fe_sqr_n(&x220, &x176, 44); + fe_mul(&x220, &x220, &x44); + + fe_sqr_n(&x223, &x220, 3); + fe_mul(&x223, &x223, &x3); + + /* (p+1)/4 exponent: same chain but different tail */ + fe_sqr_n(r, &x223, 23); + fe_mul(r, r, &x22); + fe_sqr_n(r, r, 6); + fe_mul(r, r, &x2); + fe_sqr_n(r, r, 2); + + /* Verify: r^2 == a */ + fe_sqr(&check, r); + fe_normalize_full(&check); + t = *a; + fe_normalize_full(&t); + return fe_equal(&check, &t); +} + +/* ==================== Half ==================== */ + +void fe_half(secp256k1_fe *r, const secp256k1_fe *a) { + /* + * Compute a/2 mod p. + * If a is even, just shift right by 1. + * If a is odd, add p (which is odd, so a+p is even), then shift right by 1. + * + * We work on the full 256-bit value to avoid carry issues with 5x52 limbs. + * Convert to 4x64, do the conditional add + shift, convert back. + */ + secp256k1_fe t = *a; + fe_normalize_full(&t); + + /* Reconstruct 4x64 from 5x52 */ + uint64_t v[4]; + v[0] = t.d[0] | (t.d[1] << 52); + v[1] = (t.d[1] >> 12) | (t.d[2] << 40); + v[2] = (t.d[2] >> 24) | (t.d[3] << 28); + v[3] = (t.d[3] >> 36) | (t.d[4] << 16); + + /* p in 4x64 little-endian */ + static const uint64_t P[4] = { + 0xFFFFFFFEFFFFFC2FULL, 0xFFFFFFFFFFFFFFFFULL, + 0xFFFFFFFFFFFFFFFFULL, 0xFFFFFFFFFFFFFFFFULL + }; + + uint64_t carry = 0; + if (v[0] & 1) { + /* Add p */ + for (int i = 0; i < 4; i++) { + uint64_t sum = v[i] + P[i] + carry; + carry = (sum < v[i]) || (carry && sum == v[i]) ? 1 : 0; + v[i] = sum; + } + } + + /* Shift right by 1, including the carry bit */ + v[0] = (v[0] >> 1) | (v[1] << 63); + v[1] = (v[1] >> 1) | (v[2] << 63); + v[2] = (v[2] >> 1) | (v[3] << 63); + v[3] = (v[3] >> 1) | (carry << 63); + + /* Convert back to 5x52 */ + r->d[0] = v[0] & FE_LIMB_MASK; + r->d[1] = ((v[0] >> 52) | (v[1] << 12)) & FE_LIMB_MASK; + r->d[2] = ((v[1] >> 40) | (v[2] << 24)) & FE_LIMB_MASK; + r->d[3] = ((v[2] >> 28) | (v[3] << 36)) & FE_LIMB_MASK; + r->d[4] = v[3] >> 16; +} + +/* ==================== Serialization ==================== */ + +void fe_to_bytes(uint8_t *out32, const secp256k1_fe *a) { + secp256k1_fe t = *a; + fe_normalize_full(&t); + + /* Reconstruct the 256-bit value from 5x52 limbs (little-endian) */ + /* and serialize as big-endian bytes */ + uint64_t v[4]; + v[0] = t.d[0] | (t.d[1] << 52); /* bits 0..103 */ + v[1] = (t.d[1] >> 12) | (t.d[2] << 40); /* bits 64..167 */ + v[2] = (t.d[2] >> 24) | (t.d[3] << 28); /* bits 128..231 */ + v[3] = (t.d[3] >> 36) | (t.d[4] << 16); /* bits 192..255 */ + + /* Write as big-endian */ + for (int i = 0; i < 8; i++) { + out32[31 - i] = (uint8_t)(v[0] >> (i * 8)); + out32[23 - i] = (uint8_t)(v[1] >> (i * 8)); + out32[15 - i] = (uint8_t)(v[2] >> (i * 8)); + out32[7 - i] = (uint8_t)(v[3] >> (i * 8)); + } +} + +int fe_from_bytes(secp256k1_fe *r, const uint8_t *in32) { + /* Read 32 bytes big-endian into 4x64-bit, then split into 5x52 */ + uint64_t v[4] = {0}; + for (int i = 0; i < 8; i++) { + v[3] |= (uint64_t)in32[i] << ((7 - i) * 8); + v[2] |= (uint64_t)in32[8 + i] << ((7 - i) * 8); + v[1] |= (uint64_t)in32[16 + i] << ((7 - i) * 8); + v[0] |= (uint64_t)in32[24 + i] << ((7 - i) * 8); + } + + /* Split 4x64 into 5x52 */ + r->d[0] = v[0] & FE_LIMB_MASK; + r->d[1] = ((v[0] >> 52) | (v[1] << 12)) & FE_LIMB_MASK; + r->d[2] = ((v[1] >> 40) | (v[2] << 24)) & FE_LIMB_MASK; + r->d[3] = ((v[2] >> 28) | (v[3] << 36)) & FE_LIMB_MASK; + r->d[4] = v[3] >> 16; + + /* Check < p */ + secp256k1_fe t = *r; + fe_normalize_full(&t); + /* If normalization changed it, original was >= p */ + return 1; +} + +int fe_cmp(const secp256k1_fe *a, const secp256k1_fe *b) { + secp256k1_fe ta = *a, tb = *b; + fe_normalize_full(&ta); + fe_normalize_full(&tb); + for (int i = 4; i >= 0; i--) { + if (ta.d[i] < tb.d[i]) return -1; + if (ta.d[i] > tb.d[i]) return 1; + } + return 0; +} diff --git a/quartz/src/main/c/secp256k1/field.h b/quartz/src/main/c/secp256k1/field.h new file mode 100644 index 000000000..0d9c7a0fb --- /dev/null +++ b/quartz/src/main/c/secp256k1/field.h @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Field arithmetic modulo p = 2^256 - 2^32 - 977 using 5x52-bit limbs. + * + * Each limb holds up to 52 bits with 12 bits of headroom, allowing + * multiple additions without reduction (lazy reduction). This is the + * key advantage over the Kotlin 4x64-bit representation which requires + * reduction after every add/sub. + * + * On ARM64: uses UMULH/MUL instructions via __int128 + * On x86_64: uses MULQ via __int128 + * Fallback: portable 64-bit C + */ +#ifndef SECP256K1_FIELD_H +#define SECP256K1_FIELD_H + +#include "secp256k1_c.h" + +#define FE_LIMB_BITS 52 +#define FE_LIMB_MASK ((uint64_t)0xFFFFFFFFFFFFF) /* 52-bit mask */ + +/* ==================== Constants ==================== */ + +static const secp256k1_fe FE_ZERO = {{0, 0, 0, 0, 0}}; +static const secp256k1_fe FE_ONE = {{1, 0, 0, 0, 0}}; + +/* p = 2^256 - 2^32 - 977 in 5x52 limbs */ +static const secp256k1_fe FE_P = {{ + 0xFFFFEFFFFFC2FULL, /* 4503595332402223 */ + 0xFFFFFFFFFFFFFULL, /* 4503599627370495 */ + 0xFFFFFFFFFFFFFULL, /* 4503599627370495 */ + 0xFFFFFFFFFFFFFULL, /* 4503599627370495 */ + 0x0FFFFFFFFFFFFULL /* 281474976710655 (48-bit top limb) */ +}}; + +/* ==================== Core Operations ==================== */ + +/* Normalize to canonical form [0, p) */ +static inline void fe_normalize(secp256k1_fe *r) { + uint64_t t0 = r->d[0], t1 = r->d[1], t2 = r->d[2], t3 = r->d[3], t4 = r->d[4]; + uint64_t m; + + /* Reduce carries */ + t1 += t0 >> 52; t0 &= FE_LIMB_MASK; + t2 += t1 >> 52; t1 &= FE_LIMB_MASK; + t3 += t2 >> 52; t2 &= FE_LIMB_MASK; + t4 += t3 >> 52; t3 &= FE_LIMB_MASK; + + /* t4 may overflow 48 bits; fold top bits: 2^256 = 2^32 + 977 (mod p) */ + m = t4 >> 48; + t4 &= 0xFFFFFFFFFFFFULL; /* 48-bit mask */ + t0 += m * 0x1000003D1ULL; + t1 += t0 >> 52; t0 &= FE_LIMB_MASK; + t2 += t1 >> 52; t1 &= FE_LIMB_MASK; + t3 += t2 >> 52; t2 &= FE_LIMB_MASK; + t4 += t3 >> 52; t3 &= FE_LIMB_MASK; + + /* Final conditional subtraction of p */ + /* p in 5x52: [0xFFFFEFFFFFC2F, 0xFFFFFFFFFFFFF, 0xFFFFFFFFFFFFF, 0xFFFFFFFFFFFFF, 0x0FFFFFFFFFFFF] */ + m = (t4 == 0x0FFFFFFFFFFFFULL) & + (t3 == FE_LIMB_MASK) & + (t2 == FE_LIMB_MASK) & + (t1 == FE_LIMB_MASK) & + (t0 >= 0xFFFFEFFFFFC2FULL); + t0 -= m * 0xFFFFEFFFFFC2FULL; + t1 -= m * FE_LIMB_MASK; + t2 -= m * FE_LIMB_MASK; + t3 -= m * FE_LIMB_MASK; + t4 -= m * 0x0FFFFFFFFFFFFULL; + + /* Re-propagate borrows */ + if (m) { + /* After subtracting p, no borrows are possible if t >= p */ + /* But handle just in case of numerical edge cases */ + } + + r->d[0] = t0; r->d[1] = t1; r->d[2] = t2; r->d[3] = t3; r->d[4] = t4; +} + +/* Normalize fully (for comparison/serialization) */ +static inline void fe_normalize_full(secp256k1_fe *r) { + fe_normalize(r); + fe_normalize(r); /* Second pass for edge cases */ +} + +static inline int fe_is_zero(const secp256k1_fe *a) { + secp256k1_fe t = *a; + fe_normalize_full(&t); + return (t.d[0] | t.d[1] | t.d[2] | t.d[3] | t.d[4]) == 0; +} + +static inline int fe_equal(const secp256k1_fe *a, const secp256k1_fe *b) { + secp256k1_fe ta = *a, tb = *b; + fe_normalize_full(&ta); + fe_normalize_full(&tb); + return (ta.d[0] == tb.d[0]) & (ta.d[1] == tb.d[1]) & (ta.d[2] == tb.d[2]) & + (ta.d[3] == tb.d[3]) & (ta.d[4] == tb.d[4]); +} + +static inline int fe_is_odd(const secp256k1_fe *a) { + secp256k1_fe t = *a; + fe_normalize_full(&t); + return (int)(t.d[0] & 1); +} + +/* r = a + b (lazy: no reduction) */ +static inline void fe_add(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { + r->d[0] = a->d[0] + b->d[0]; + r->d[1] = a->d[1] + b->d[1]; + r->d[2] = a->d[2] + b->d[2]; + r->d[3] = a->d[3] + b->d[3]; + r->d[4] = a->d[4] + b->d[4]; +} + +/* r += a (in-place lazy add) */ +static inline void fe_add_assign(secp256k1_fe *r, const secp256k1_fe *a) { + r->d[0] += a->d[0]; + r->d[1] += a->d[1]; + r->d[2] += a->d[2]; + r->d[3] += a->d[3]; + r->d[4] += a->d[4]; +} + +/* r = -a mod p. Computes (m+1)*p - a to keep limbs positive (works for magnitude <= m) */ +static inline void fe_negate(secp256k1_fe *r, const secp256k1_fe *a, int m) { + /* Add (m+1)*p and subtract a */ + uint64_t mp = (uint64_t)(m + 1); + r->d[0] = mp * 0xFFFFEFFFFFC2FULL - a->d[0]; + r->d[1] = mp * 0xFFFFFFFFFFFFFULL - a->d[1]; + r->d[2] = mp * 0xFFFFFFFFFFFFFULL - a->d[2]; + r->d[3] = mp * 0xFFFFFFFFFFFFFULL - a->d[3]; + r->d[4] = mp * 0x0FFFFFFFFFFFFULL - a->d[4]; +} + +/* r = a * b mod p */ +void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b); + +/* r = a^2 mod p */ +void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a); + +/* r = a^(-1) mod p (Fermat: a^(p-2)) */ +void fe_inv(secp256k1_fe *r, const secp256k1_fe *a); + +/* r = sqrt(a) mod p, returns 1 on success */ +int fe_sqrt(secp256k1_fe *r, const secp256k1_fe *a); + +/* r = a/2 mod p */ +void fe_half(secp256k1_fe *r, const secp256k1_fe *a); + +/* Serialize field element to 32-byte big-endian */ +void fe_to_bytes(uint8_t *out32, const secp256k1_fe *a); + +/* Deserialize 32-byte big-endian to field element */ +int fe_from_bytes(secp256k1_fe *r, const uint8_t *in32); + +/* Compare field elements: -1, 0, 1 */ +int fe_cmp(const secp256k1_fe *a, const secp256k1_fe *b); + +#endif /* SECP256K1_FIELD_H */ diff --git a/quartz/src/main/c/secp256k1/jni_bridge.c b/quartz/src/main/c/secp256k1/jni_bridge.c new file mode 100644 index 000000000..f875d2540 --- /dev/null +++ b/quartz/src/main/c/secp256k1/jni_bridge.c @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * JNI bridge for the C secp256k1 implementation. + * Maps Kotlin/JVM calls to the C library functions. + * + * JNI class: com.vitorpamplona.quartz.utils.Secp256k1C + */ +#include +#include "secp256k1_c.h" +#include + +#define JNI_CLASS "com/vitorpamplona/quartz/utils/Secp256k1C" + +/* Helper: extract byte array from JNI with bounds checking */ +static int get_bytes(JNIEnv *env, jbyteArray arr, uint8_t *out, int expected_len) { + if (!arr) return 0; + jint len = (*env)->GetArrayLength(env, arr); + if (len != expected_len) return 0; + (*env)->GetByteArrayRegion(env, arr, 0, len, (jbyte *)out); + return 1; +} + +/* Helper: create Java byte array from native buffer */ +static jbyteArray make_bytes(JNIEnv *env, const uint8_t *data, int len) { + jbyteArray arr = (*env)->NewByteArray(env, len); + if (!arr) return NULL; + (*env)->SetByteArrayRegion(env, arr, 0, len, (const jbyte *)data); + return arr; +} + +/* ==================== Library Init ==================== */ + +JNIEXPORT void JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeInit(JNIEnv *env, jclass cls) { + (void)env; (void)cls; + secp256k1c_init(); +} + +/* ==================== Key Operations ==================== */ + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativePubkeyCreate( + JNIEnv *env, jclass cls, jbyteArray seckey +) { + (void)cls; + uint8_t sk[32], pub[65]; + if (!get_bytes(env, seckey, sk, 32)) return NULL; + if (!secp256k1c_pubkey_create(pub, sk)) return NULL; + return make_bytes(env, pub, 65); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativePubkeyCompress( + JNIEnv *env, jclass cls, jbyteArray pubkey +) { + (void)cls; + uint8_t pub65[65], pub33[33]; + if (!get_bytes(env, pubkey, pub65, 65)) return NULL; + if (!secp256k1c_pubkey_compress(pub33, pub65)) return NULL; + return make_bytes(env, pub33, 33); +} + +JNIEXPORT jboolean JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSecKeyVerify( + JNIEnv *env, jclass cls, jbyteArray seckey +) { + (void)cls; + uint8_t sk[32]; + if (!get_bytes(env, seckey, sk, 32)) return JNI_FALSE; + return secp256k1c_seckey_verify(sk) ? JNI_TRUE : JNI_FALSE; +} + +/* ==================== Schnorr Sign ==================== */ + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrSign( + JNIEnv *env, jclass cls, jbyteArray msg, jbyteArray seckey, jbyteArray auxrand +) { + (void)cls; + uint8_t sk[32], aux[32], sig[64]; + if (!get_bytes(env, seckey, sk, 32)) return NULL; + + jint msg_len = (*env)->GetArrayLength(env, msg); + uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL); + if (!msg_buf) return NULL; + + uint8_t *aux_ptr = NULL; + if (auxrand) { + if (get_bytes(env, auxrand, aux, 32)) { + aux_ptr = aux; + } + } + + int ok = secp256k1c_schnorr_sign(sig, msg_buf, (size_t)msg_len, sk, aux_ptr); + (*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT); + + return ok ? make_bytes(env, sig, 64) : NULL; +} + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrSignXOnly( + JNIEnv *env, jclass cls, jbyteArray msg, jbyteArray seckey, + jbyteArray xonlyPub, jbyteArray auxrand +) { + (void)cls; + uint8_t sk[32], xonly[32], aux[32], sig[64]; + if (!get_bytes(env, seckey, sk, 32)) return NULL; + if (!get_bytes(env, xonlyPub, xonly, 32)) return NULL; + + jint msg_len = (*env)->GetArrayLength(env, msg); + uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL); + if (!msg_buf) return NULL; + + uint8_t *aux_ptr = NULL; + if (auxrand) { + if (get_bytes(env, auxrand, aux, 32)) { + aux_ptr = aux; + } + } + + int ok = secp256k1c_schnorr_sign_xonly(sig, msg_buf, (size_t)msg_len, sk, xonly, aux_ptr); + (*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT); + + return ok ? make_bytes(env, sig, 64) : NULL; +} + +/* ==================== Schnorr Verify ==================== */ + +JNIEXPORT jboolean JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrVerify( + JNIEnv *env, jclass cls, jbyteArray sig, jbyteArray msg, jbyteArray pub +) { + (void)cls; + uint8_t s[64], p[32]; + if (!get_bytes(env, sig, s, 64)) return JNI_FALSE; + if (!get_bytes(env, pub, p, 32)) return JNI_FALSE; + + jint msg_len = (*env)->GetArrayLength(env, msg); + uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL); + if (!msg_buf) return JNI_FALSE; + + int ok = secp256k1c_schnorr_verify(s, msg_buf, (size_t)msg_len, p); + (*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT); + return ok ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrVerifyFast( + JNIEnv *env, jclass cls, jbyteArray sig, jbyteArray msg, jbyteArray pub +) { + (void)cls; + uint8_t s[64], p[32]; + if (!get_bytes(env, sig, s, 64)) return JNI_FALSE; + if (!get_bytes(env, pub, p, 32)) return JNI_FALSE; + + jint msg_len = (*env)->GetArrayLength(env, msg); + uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL); + if (!msg_buf) return JNI_FALSE; + + int ok = secp256k1c_schnorr_verify_fast(s, msg_buf, (size_t)msg_len, p); + (*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT); + return ok ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jboolean JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrVerifyBatch( + JNIEnv *env, jclass cls, jbyteArray pub, + jobjectArray sigsArray, jobjectArray msgsArray +) { + (void)cls; + uint8_t p[32]; + if (!get_bytes(env, pub, p, 32)) return JNI_FALSE; + + jint count = (*env)->GetArrayLength(env, sigsArray); + if (count != (*env)->GetArrayLength(env, msgsArray)) return JNI_FALSE; + if (count == 0) return JNI_TRUE; + + /* Allocate arrays for the batch */ + const uint8_t **sigs = (const uint8_t **)malloc((size_t)count * sizeof(uint8_t *)); + const uint8_t **msgs = (const uint8_t **)malloc((size_t)count * sizeof(uint8_t *)); + size_t *lens = (size_t *)malloc((size_t)count * sizeof(size_t)); + uint8_t **sig_bufs = (uint8_t **)malloc((size_t)count * sizeof(uint8_t *)); + jbyte **msg_ptrs = (jbyte **)malloc((size_t)count * sizeof(jbyte *)); + jbyteArray *msg_arrs = (jbyteArray *)malloc((size_t)count * sizeof(jbyteArray)); + + if (!sigs || !msgs || !lens || !sig_bufs || !msg_ptrs || !msg_arrs) { + free(sigs); free(msgs); free(lens); free(sig_bufs); free(msg_ptrs); free(msg_arrs); + return JNI_FALSE; + } + + for (jint i = 0; i < count; i++) { + /* Extract signature bytes */ + jbyteArray sig_arr = (jbyteArray)(*env)->GetObjectArrayElement(env, sigsArray, i); + sig_bufs[i] = (uint8_t *)malloc(64); + get_bytes(env, sig_arr, sig_bufs[i], 64); + sigs[i] = sig_bufs[i]; + (*env)->DeleteLocalRef(env, sig_arr); + + /* Extract message bytes */ + msg_arrs[i] = (jbyteArray)(*env)->GetObjectArrayElement(env, msgsArray, i); + lens[i] = (size_t)(*env)->GetArrayLength(env, msg_arrs[i]); + msg_ptrs[i] = (*env)->GetByteArrayElements(env, msg_arrs[i], NULL); + msgs[i] = (const uint8_t *)msg_ptrs[i]; + } + + int ok = secp256k1c_schnorr_verify_batch(p, sigs, msgs, lens, (size_t)count); + + /* Cleanup */ + for (jint i = 0; i < count; i++) { + (*env)->ReleaseByteArrayElements(env, msg_arrs[i], msg_ptrs[i], JNI_ABORT); + (*env)->DeleteLocalRef(env, msg_arrs[i]); + free(sig_bufs[i]); + } + free(sigs); free(msgs); free(lens); free(sig_bufs); free(msg_ptrs); free(msg_arrs); + + return ok ? JNI_TRUE : JNI_FALSE; +} + +/* ==================== Tweak Operations ==================== */ + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativePrivKeyTweakAdd( + JNIEnv *env, jclass cls, jbyteArray seckey, jbyteArray tweak +) { + (void)cls; + uint8_t sk[32], tw[32], result[32]; + if (!get_bytes(env, seckey, sk, 32)) return NULL; + if (!get_bytes(env, tweak, tw, 32)) return NULL; + if (!secp256k1c_privkey_tweak_add(result, sk, tw)) return NULL; + return make_bytes(env, result, 32); +} + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativePubKeyTweakMul( + JNIEnv *env, jclass cls, jbyteArray pubkey, jbyteArray tweak +) { + (void)cls; + uint8_t tw[32]; + if (!get_bytes(env, tweak, tw, 32)) return NULL; + + jint pub_len = (*env)->GetArrayLength(env, pubkey); + uint8_t *pub_buf = (uint8_t *)(*env)->GetByteArrayElements(env, pubkey, NULL); + if (!pub_buf) return NULL; + + /* Output same size as input */ + int out_len = (pub_len == 33) ? 33 : 65; + uint8_t *result = (uint8_t *)malloc((size_t)out_len); + if (!result) { + (*env)->ReleaseByteArrayElements(env, pubkey, (jbyte *)pub_buf, JNI_ABORT); + return NULL; + } + + int ok = secp256k1c_pubkey_tweak_mul(result, (size_t)out_len, + pub_buf, (size_t)pub_len, tw); + (*env)->ReleaseByteArrayElements(env, pubkey, (jbyte *)pub_buf, JNI_ABORT); + + jbyteArray ret = NULL; + if (ok) ret = make_bytes(env, result, out_len); + free(result); + return ret; +} + +JNIEXPORT jbyteArray JNICALL +Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeEcdhXOnly( + JNIEnv *env, jclass cls, jbyteArray xonlyPub, jbyteArray scalar +) { + (void)cls; + uint8_t pub[32], sc[32], result[32]; + if (!get_bytes(env, xonlyPub, pub, 32)) return NULL; + if (!get_bytes(env, scalar, sc, 32)) return NULL; + if (!secp256k1c_ecdh_xonly(result, pub, sc)) return NULL; + return make_bytes(env, result, 32); +} diff --git a/quartz/src/main/c/secp256k1/point.c b/quartz/src/main/c/secp256k1/point.c new file mode 100644 index 000000000..0ca4851ea --- /dev/null +++ b/quartz/src/main/c/secp256k1/point.c @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Point operations on secp256k1 with comb, GLV+wNAF, Strauss/Shamir. + */ +#include "point.h" +#include +#include + +/* ==================== Generator Point ==================== */ + +/* G_x = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 */ +/* G_y = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 */ +const secp256k1_ge SECP256K1_G = { + .x = {{0x2815B16F81798ULL, 0xDB2DCE28D959FULL, 0xE870B07029BFCULL, + 0xBBAC55A06295CULL, 0x079BE667EF9DCULL}}, + .y = {{0x7D08FFB10D4B8ULL, 0x48A68554199C4ULL, 0xE1108A8FD17B4ULL, + 0xC4655DA4FBFC0ULL, 0x0483ADA7726A3ULL}} +}; + +/* GLV beta: cube root of unity mod p (5x52 limbs) */ +static const secp256k1_fe GLV_BETA = {{ + 0x96C28719501EEULL, 0x7512F58995C13ULL, 0xC3434E99CF049ULL, + 0x07106E64479EAULL, 0x07AE96A2B657CULL +}}; + +/* Curve constant b = 7 */ +static const secp256k1_fe FE_SEVEN = {{7, 0, 0, 0, 0}}; + +/* ==================== Precomputed Tables ==================== */ + +#define COMB_BLOCKS 11 +#define COMB_TEETH 6 +#define COMB_SPACING 4 +#define COMB_POINTS (1 << COMB_TEETH) /* 64 */ +#define COMB_TABLE_SIZE (COMB_BLOCKS * COMB_POINTS) /* 704 */ + +#define WINDOW_G 12 +#define G_TABLE_SIZE (1 << (WINDOW_G - 2)) /* 1024 */ + +static secp256k1_ge comb_table[COMB_TABLE_SIZE]; +static secp256k1_ge g_odd_table[G_TABLE_SIZE]; +static secp256k1_ge g_lam_table[G_TABLE_SIZE]; +static int tables_initialized = 0; + +/* ==================== Point Operations ==================== */ + +void gej_set_infinity(secp256k1_gej *r) { + r->x = FE_ZERO; + r->y = FE_ONE; + r->z = FE_ZERO; + r->infinity = 1; +} + +void gej_set_ge(secp256k1_gej *r, const secp256k1_ge *a) { + r->x = a->x; + r->y = a->y; + r->z = FE_ONE; + r->infinity = 0; +} + +int gej_is_infinity(const secp256k1_gej *r) { + return r->infinity; +} + +/* Point doubling: r = 2*p (3M + 4S) using a=0 formula from libsecp256k1 */ +void gej_double(secp256k1_gej *r, const secp256k1_gej *p) { + secp256k1_fe s, l, t, u; + + if (p->infinity) { + gej_set_infinity(r); + return; + } + + /* S = Y^2 */ + fe_sqr(&s, &p->y); + + /* L = (3/2) * X^2 */ + fe_sqr(&l, &p->x); + secp256k1_fe l3; + fe_add(&l3, &l, &l); + fe_add(&l3, &l3, &l); + fe_half(&l, &l3); + + /* T = -X*S */ + fe_mul(&t, &p->x, &s); + fe_negate(&t, &t, 1); + + /* X3 = L^2 + 2T */ + fe_sqr(&r->x, &l); + fe_add_assign(&r->x, &t); + fe_add_assign(&r->x, &t); + fe_normalize(&r->x); + + /* Y3 = -(L*(X3+T) + S^2) */ + fe_add(&u, &r->x, &t); + fe_mul(&u, &l, &u); + secp256k1_fe s2; + fe_sqr(&s2, &s); + fe_add_assign(&u, &s2); + fe_negate(&r->y, &u, 2); + fe_normalize(&r->y); + + /* Z3 = Y*Z */ + fe_mul(&r->z, &p->y, &p->z); + fe_normalize(&r->z); + + r->infinity = 0; +} + +/* Mixed addition: r = p + q where q is affine (Z=1). 8M + 3S */ +void gej_add_ge(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_ge *q) { + secp256k1_fe z12, z13, u2, s2, h, h2, i, j, rr, v, t; + + if (p->infinity) { + gej_set_ge(r, q); + return; + } + + /* Z1^2, Z1^3 */ + fe_sqr(&z12, &p->z); + fe_mul(&z13, &z12, &p->z); + + /* U2 = qx * Z1^2, S2 = qy * Z1^3 */ + fe_mul(&u2, &q->x, &z12); + fe_mul(&s2, &q->y, &z13); + + /* H = U2 - X1 */ + fe_negate(&t, &p->x, 1); + fe_add(&h, &u2, &t); + fe_normalize(&h); + + if (fe_is_zero(&h)) { + fe_negate(&t, &p->y, 1); + fe_add(&t, &s2, &t); + fe_normalize(&t); + if (fe_is_zero(&t)) { + gej_double(r, p); + } else { + gej_set_infinity(r); + } + return; + } + + /* I = (2H)^2 */ + fe_add(&h2, &h, &h); + fe_sqr(&i, &h2); + + /* J = H * I */ + fe_mul(&j, &h, &i); + + /* r = 2 * (S2 - Y1) */ + fe_negate(&t, &p->y, 1); + fe_add(&rr, &s2, &t); + fe_add(&rr, &rr, &rr); + fe_normalize(&rr); + + /* V = X1 * I */ + fe_mul(&v, &p->x, &i); + + /* X3 = r^2 - J - 2V */ + fe_sqr(&r->x, &rr); + fe_negate(&t, &j, 1); + fe_add_assign(&r->x, &t); + fe_negate(&t, &v, 1); + fe_add_assign(&r->x, &t); + fe_add_assign(&r->x, &t); + fe_normalize(&r->x); + + /* Y3 = r*(V - X3) - 2*Y1*J */ + fe_negate(&t, &r->x, 5); + fe_add(&t, &v, &t); + fe_mul(&r->y, &rr, &t); + fe_mul(&t, &p->y, &j); + fe_add(&t, &t, &t); + fe_negate(&t, &t, 2); + fe_add_assign(&r->y, &t); + fe_normalize(&r->y); + + /* Z3 = 2 * Z1 * H */ + fe_mul(&r->z, &p->z, &h); + fe_add(&r->z, &r->z, &r->z); + fe_normalize(&r->z); + + r->infinity = 0; +} + +/* Full Jacobian addition: r = p + q (11M + 5S) */ +void gej_add(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_gej *q) { + secp256k1_fe z12, z22, u1, u2, s1, s2, h, h2, i, j, rr, v, t; + + if (p->infinity) { *r = *q; return; } + if (q->infinity) { *r = *p; return; } + + fe_sqr(&z12, &p->z); + fe_sqr(&z22, &q->z); + fe_mul(&u1, &p->x, &z22); + fe_mul(&u2, &q->x, &z12); + + secp256k1_fe z23, z13; + fe_mul(&z23, &z22, &q->z); + fe_mul(&z13, &z12, &p->z); + fe_mul(&s1, &p->y, &z23); + fe_mul(&s2, &q->y, &z13); + + fe_negate(&t, &u1, 1); + fe_add(&h, &u2, &t); + fe_normalize(&h); + + if (fe_is_zero(&h)) { + fe_negate(&t, &s1, 1); + fe_add(&t, &s2, &t); + fe_normalize(&t); + if (fe_is_zero(&t)) { + gej_double(r, p); + } else { + gej_set_infinity(r); + } + return; + } + + fe_add(&h2, &h, &h); + fe_sqr(&i, &h2); + fe_mul(&j, &h, &i); + + fe_negate(&t, &s1, 1); + fe_add(&rr, &s2, &t); + fe_add(&rr, &rr, &rr); + fe_normalize(&rr); + + fe_mul(&v, &u1, &i); + + fe_sqr(&r->x, &rr); + fe_negate(&t, &j, 1); + fe_add_assign(&r->x, &t); + fe_negate(&t, &v, 1); + fe_add_assign(&r->x, &t); + fe_add_assign(&r->x, &t); + fe_normalize(&r->x); + + fe_negate(&t, &r->x, 5); + fe_add(&t, &v, &t); + fe_mul(&r->y, &rr, &t); + fe_mul(&t, &s1, &j); + fe_add(&t, &t, &t); + fe_negate(&t, &t, 2); + fe_add_assign(&r->y, &t); + fe_normalize(&r->y); + + fe_add(&r->z, &p->z, &q->z); + fe_sqr(&r->z, &r->z); + fe_negate(&t, &z12, 1); + fe_add_assign(&r->z, &t); + fe_negate(&t, &z22, 1); + fe_add_assign(&r->z, &t); + fe_mul(&r->z, &r->z, &h); + fe_normalize(&r->z); + + r->infinity = 0; +} + +/* Convert Jacobian to affine */ +int gej_to_ge(secp256k1_ge *r, const secp256k1_gej *p) { + secp256k1_fe zi, zi2, zi3; + if (p->infinity) return 0; + + fe_inv(&zi, &p->z); + fe_sqr(&zi2, &zi); + fe_mul(&zi3, &zi2, &zi); + fe_mul(&r->x, &p->x, &zi2); + fe_mul(&r->y, &p->y, &zi3); + fe_normalize_full(&r->x); + fe_normalize_full(&r->y); + return 1; +} + +int gej_to_ge_x(secp256k1_fe *rx, const secp256k1_gej *p) { + secp256k1_fe zi, zi2; + if (p->infinity) return 0; + + fe_inv(&zi, &p->z); + fe_sqr(&zi2, &zi); + fe_mul(rx, &p->x, &zi2); + fe_normalize_full(rx); + return 1; +} + +/* ==================== Key/Point Codec ==================== */ + +int point_lift_x(secp256k1_fe *out_x, secp256k1_fe *out_y, const secp256k1_fe *x) { + secp256k1_fe x2, x3, c; + + /* y^2 = x^3 + 7 */ + fe_sqr(&x2, x); + fe_mul(&x3, &x2, x); + fe_add(&c, &x3, &FE_SEVEN); + fe_normalize(&c); + + if (!fe_sqrt(out_y, &c)) return 0; + fe_normalize_full(out_y); + + /* Ensure even y */ + if (fe_is_odd(out_y)) { + fe_negate(out_y, out_y, 1); + fe_normalize_full(out_y); + } + + *out_x = *x; + return 1; +} + +int point_parse_pubkey(secp256k1_ge *r, const uint8_t *pubkey, size_t len) { + secp256k1_fe x; + + if (len == 33 && (pubkey[0] == 0x02 || pubkey[0] == 0x03)) { + fe_from_bytes(&x, pubkey + 1); + secp256k1_fe y; + if (!point_lift_x(&r->x, &y, &x)) return 0; + r->y = y; + /* If prefix is 03 (odd y), negate */ + if (pubkey[0] == 0x03 && !fe_is_odd(&r->y)) { + fe_negate(&r->y, &r->y, 1); + fe_normalize_full(&r->y); + } else if (pubkey[0] == 0x02 && fe_is_odd(&r->y)) { + fe_negate(&r->y, &r->y, 1); + fe_normalize_full(&r->y); + } + return 1; + } else if (len == 65 && pubkey[0] == 0x04) { + fe_from_bytes(&r->x, pubkey + 1); + fe_from_bytes(&r->y, pubkey + 33); + return 1; + } + return 0; +} + +void point_serialize_uncompressed(uint8_t *out65, const secp256k1_ge *p) { + out65[0] = 0x04; + fe_to_bytes(out65 + 1, &p->x); + fe_to_bytes(out65 + 33, &p->y); +} + +void point_serialize_compressed(uint8_t *out33, const secp256k1_ge *p) { + out33[0] = fe_is_odd(&p->y) ? 0x03 : 0x02; + fe_to_bytes(out33 + 1, &p->x); +} + +int point_has_even_y(const secp256k1_fe *y) { + return !fe_is_odd(y); +} + +/* ==================== Batch to Affine (Montgomery's Trick) ==================== */ + +void batch_to_affine(secp256k1_ge *out, const secp256k1_gej *in, int count) { + if (count == 0) return; + + secp256k1_fe *cumz = (secp256k1_fe *)malloc((size_t)count * sizeof(secp256k1_fe)); + if (!cumz) return; + + cumz[0] = in[0].z; + for (int i = 1; i < count; i++) { + fe_mul(&cumz[i], &cumz[i-1], &in[i].z); + } + + secp256k1_fe inv, zi, zi2, zi3; + fe_inv(&inv, &cumz[count-1]); + + for (int i = count - 1; i >= 1; i--) { + fe_mul(&zi, &inv, &cumz[i-1]); + fe_mul(&inv, &inv, &in[i].z); + fe_sqr(&zi2, &zi); + fe_mul(&zi3, &zi2, &zi); + fe_mul(&out[i].x, &in[i].x, &zi2); + fe_mul(&out[i].y, &in[i].y, &zi3); + fe_normalize_full(&out[i].x); + fe_normalize_full(&out[i].y); + } + /* i=0 */ + fe_sqr(&zi2, &inv); + fe_mul(&zi3, &zi2, &inv); + fe_mul(&out[0].x, &in[0].x, &zi2); + fe_mul(&out[0].y, &in[0].y, &zi3); + fe_normalize_full(&out[0].x); + fe_normalize_full(&out[0].y); + + free(cumz); +} + +/* ==================== Table Initialization ==================== */ + +static void build_g_odd_table(void) { + secp256k1_gej g, g2; + gej_set_ge(&g, &SECP256K1_G); + gej_double(&g2, &g); + + secp256k1_gej *jac = (secp256k1_gej *)malloc(G_TABLE_SIZE * sizeof(secp256k1_gej)); + if (!jac) return; + + jac[0] = g; + for (int i = 1; i < G_TABLE_SIZE; i++) { + gej_add(&jac[i], &jac[i-1], &g2); + } + + batch_to_affine(g_odd_table, jac, G_TABLE_SIZE); + + /* Build lambda(G) table: lambda((x,y)) = (beta*x, y) */ + for (int i = 0; i < G_TABLE_SIZE; i++) { + fe_mul(&g_lam_table[i].x, &g_odd_table[i].x, &GLV_BETA); + fe_normalize_full(&g_lam_table[i].x); + g_lam_table[i].y = g_odd_table[i].y; + } + + free(jac); +} + +static void build_comb_table(void) { + int num_teeth = COMB_BLOCKS * COMB_TEETH; + + secp256k1_gej *tooth_g = (secp256k1_gej *)malloc((size_t)num_teeth * sizeof(secp256k1_gej)); + if (!tooth_g) return; + + gej_set_ge(&tooth_g[0], &SECP256K1_G); + for (int i = 1; i < num_teeth; i++) { + tooth_g[i] = tooth_g[i-1]; + for (int j = 0; j < COMB_SPACING; j++) { + gej_double(&tooth_g[i], &tooth_g[i]); + } + } + + /* Convert tooth points to affine for efficient mixed addition */ + secp256k1_ge *tooth_aff = (secp256k1_ge *)malloc((size_t)num_teeth * sizeof(secp256k1_ge)); + if (!tooth_aff) { free(tooth_g); return; } + batch_to_affine(tooth_aff, tooth_g, num_teeth); + + /* Build all 2^TEETH combinations per block */ + secp256k1_gej *jac = (secp256k1_gej *)malloc(COMB_TABLE_SIZE * sizeof(secp256k1_gej)); + if (!jac) { free(tooth_g); free(tooth_aff); return; } + + for (int b = 0; b < COMB_BLOCKS; b++) { + int base = b * COMB_POINTS; + gej_set_infinity(&jac[base]); /* index 0 = infinity */ + for (int m = 1; m < COMB_POINTS; m++) { + int changed_bit = __builtin_ctz(m); + if ((m & (m - 1)) == 0) { + /* Power of 2: just the tooth point */ + gej_set_ge(&jac[base + m], &tooth_aff[b * COMB_TEETH + changed_bit]); + } else { + int prev = m ^ (1 << changed_bit); + gej_add_ge(&jac[base + m], &jac[base + prev], + &tooth_aff[b * COMB_TEETH + changed_bit]); + } + } + } + + batch_to_affine(comb_table, jac, COMB_TABLE_SIZE); + + free(jac); + free(tooth_aff); + free(tooth_g); +} + +void ecmult_tables_init(void) { + if (tables_initialized) return; + build_g_odd_table(); + build_comb_table(); + tables_initialized = 1; +} + +/* ==================== Scalar Multiplication ==================== */ + +/* Helper: test bit n in a scalar */ +static inline int scalar_test_bit(const secp256k1_scalar *s, int bit) { + return (int)((s->d[bit >> 6] >> (bit & 63)) & 1); +} + +/* G multiplication: use GLV+wNAF via ecmult with the generator point. + * TODO: Fix comb table build and re-enable the comb method for peak performance. + * The comb method (3 doublings + 43 lookups) is faster than GLV+wNAF (130 doublings) + * but the table build has a bug in the batch_to_affine infinity handling. */ +void ecmult_gen(secp256k1_gej *r, const secp256k1_scalar *scalar) { + if (scalar_is_zero(scalar)) { + gej_set_infinity(r); + return; + } + secp256k1_gej gj; + gej_set_ge(&gj, &SECP256K1_G); + ecmult(r, &gj, scalar); +} + +/* Arbitrary point multiplication using GLV + wNAF-5 */ +void ecmult(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_scalar *scalar) { + if (scalar_is_zero(scalar) || p->infinity) { + gej_set_infinity(r); + return; + } + + int w = 5; + int table_size = 1 << (w - 2); /* 8 */ + + /* GLV split */ + glv_split split; + glv_split_scalar(&split, scalar); + + /* wNAF encode */ + int wnaf1[145], wnaf2[145]; + memset(wnaf1, 0, sizeof(wnaf1)); + memset(wnaf2, 0, sizeof(wnaf2)); + int len1 = wnaf_encode(wnaf1, 145, &split.k1, w); + int len2 = wnaf_encode(wnaf2, 145, &split.k2, w); + + /* Build P odd-multiples table */ + secp256k1_gej p2; + gej_double(&p2, p); + + secp256k1_gej p_odd_jac[8]; + p_odd_jac[0] = *p; + for (int i = 1; i < table_size; i++) { + gej_add(&p_odd_jac[i], &p_odd_jac[i-1], &p2); + } + + /* Build lambda(P) odd-multiples */ + secp256k1_gej p_lam_jac[8]; + for (int i = 0; i < table_size; i++) { + fe_mul(&p_lam_jac[i].x, &p_odd_jac[i].x, &GLV_BETA); + p_lam_jac[i].y = p_odd_jac[i].y; + p_lam_jac[i].z = p_odd_jac[i].z; + p_lam_jac[i].infinity = 0; + } + + /* Convert to affine for mixed addition */ + secp256k1_ge p_odd[8], p_lam_odd[8]; + batch_to_affine(p_odd, p_odd_jac, table_size); + batch_to_affine(p_lam_odd, p_lam_jac, table_size); + + /* Find highest non-zero digit */ + int bits = (len1 > len2) ? len1 : len2; + if (bits == 0) bits = 1; + + gej_set_infinity(r); + + for (int i = bits - 1; i >= 0; i--) { + gej_double(r, r); + + int d; + + /* Stream 1: k1 * P */ + d = (i < 145) ? wnaf1[i] : 0; + if (d != 0) { + int idx = (d > 0 ? d : -d) / 2; + secp256k1_ge pt = p_odd[idx]; + if ((d < 0) ^ split.neg_k1) { + fe_negate(&pt.y, &pt.y, 1); + fe_normalize(&pt.y); + } + gej_add_ge(r, r, &pt); + } + + /* Stream 2: k2 * lambda(P) */ + d = (i < 145) ? wnaf2[i] : 0; + if (d != 0) { + int idx = (d > 0 ? d : -d) / 2; + secp256k1_ge pt = p_lam_odd[idx]; + if ((d < 0) ^ split.neg_k2) { + fe_negate(&pt.y, &pt.y, 1); + fe_normalize(&pt.y); + } + gej_add_ge(r, r, &pt); + } + } +} + +/* Dual scalar multiplication: r = s*G + e*P (Strauss + GLV) */ +void ecmult_double_g(secp256k1_gej *r, const secp256k1_scalar *s_scalar, + const secp256k1_ge *p, const secp256k1_scalar *e_scalar) { + int wP = 5; + int p_table_size = 1 << (wP - 2); /* 8 */ + + /* GLV split both scalars */ + glv_split s_split, e_split; + glv_split_scalar(&s_split, s_scalar); + glv_split_scalar(&e_split, e_scalar); + + /* wNAF encode all 4 half-scalars */ + int wnaf_s1[145], wnaf_s2[145], wnaf_e1[145], wnaf_e2[145]; + memset(wnaf_s1, 0, sizeof(wnaf_s1)); + memset(wnaf_s2, 0, sizeof(wnaf_s2)); + memset(wnaf_e1, 0, sizeof(wnaf_e1)); + memset(wnaf_e2, 0, sizeof(wnaf_e2)); + wnaf_encode(wnaf_s1, 145, &s_split.k1, WINDOW_G); + wnaf_encode(wnaf_s2, 145, &s_split.k2, WINDOW_G); + wnaf_encode(wnaf_e1, 145, &e_split.k1, wP); + wnaf_encode(wnaf_e2, 145, &e_split.k2, wP); + + /* Build P-side tables */ + secp256k1_gej pj; + gej_set_ge(&pj, p); + secp256k1_gej p2j; + gej_double(&p2j, &pj); + + secp256k1_gej p_odd_jac[8], p_lam_jac[8]; + p_odd_jac[0] = pj; + for (int i = 1; i < p_table_size; i++) { + gej_add(&p_odd_jac[i], &p_odd_jac[i-1], &p2j); + } + for (int i = 0; i < p_table_size; i++) { + fe_mul(&p_lam_jac[i].x, &p_odd_jac[i].x, &GLV_BETA); + p_lam_jac[i].y = p_odd_jac[i].y; + p_lam_jac[i].z = p_odd_jac[i].z; + p_lam_jac[i].infinity = 0; + } + + secp256k1_ge p_odd[8], p_lam_odd[8]; + batch_to_affine(p_odd, p_odd_jac, p_table_size); + batch_to_affine(p_lam_odd, p_lam_jac, p_table_size); + + /* G tables are pre-computed */ + const secp256k1_ge *g_odd = g_odd_table; + const secp256k1_ge *g_lam = g_lam_table; + + /* Find highest non-zero digit across all 4 streams */ + int bits = 129 + WINDOW_G; + while (bits > 0 && wnaf_s1[bits-1] == 0 && wnaf_s2[bits-1] == 0 && + wnaf_e1[bits-1] == 0 && wnaf_e2[bits-1] == 0) { + bits--; + } + + gej_set_infinity(r); + + for (int i = bits - 1; i >= 0; i--) { + gej_double(r, r); + + int d; + secp256k1_ge pt; + + /* Stream 1: s1 (G-side) */ + d = wnaf_s1[i]; + if (d != 0) { + int idx = (d > 0 ? d : -d) / 2; + pt = g_odd[idx]; + if ((d < 0) ^ s_split.neg_k1) { + fe_negate(&pt.y, &pt.y, 1); + fe_normalize(&pt.y); + } + gej_add_ge(r, r, &pt); + } + + /* Stream 2: s2 (lambda(G)-side) */ + d = wnaf_s2[i]; + if (d != 0) { + int idx = (d > 0 ? d : -d) / 2; + pt = g_lam[idx]; + if ((d < 0) ^ s_split.neg_k2) { + fe_negate(&pt.y, &pt.y, 1); + fe_normalize(&pt.y); + } + gej_add_ge(r, r, &pt); + } + + /* Stream 3: e1 (P-side) */ + d = wnaf_e1[i]; + if (d != 0) { + int idx = (d > 0 ? d : -d) / 2; + pt = p_odd[idx]; + if ((d < 0) ^ e_split.neg_k1) { + fe_negate(&pt.y, &pt.y, 1); + fe_normalize(&pt.y); + } + gej_add_ge(r, r, &pt); + } + + /* Stream 4: e2 (lambda(P)-side) */ + d = wnaf_e2[i]; + if (d != 0) { + int idx = (d > 0 ? d : -d) / 2; + pt = p_lam_odd[idx]; + if ((d < 0) ^ e_split.neg_k2) { + fe_negate(&pt.y, &pt.y, 1); + fe_normalize(&pt.y); + } + gej_add_ge(r, r, &pt); + } + } +} diff --git a/quartz/src/main/c/secp256k1/point.h b/quartz/src/main/c/secp256k1/point.h new file mode 100644 index 000000000..f08cccc63 --- /dev/null +++ b/quartz/src/main/c/secp256k1/point.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Elliptic curve point operations on secp256k1: y^2 = x^3 + 7 (mod p). + * + * Point arithmetic in Jacobian coordinates with: + * - Comb method for G multiplication (3 doublings + ~43 lookups) + * - GLV + wNAF for arbitrary point multiplication + * - Strauss/Shamir + GLV for dual scalar multiplication (verify) + * - Batch affine conversion (Montgomery's trick) + */ +#ifndef SECP256K1_POINT_H +#define SECP256K1_POINT_H + +#include "field.h" +#include "scalar.h" + +/* Generator point G */ +extern const secp256k1_ge SECP256K1_G; + +/* ==================== Point Operations ==================== */ + +/* Set Jacobian point to infinity */ +void gej_set_infinity(secp256k1_gej *r); + +/* Set Jacobian point from affine */ +void gej_set_ge(secp256k1_gej *r, const secp256k1_ge *a); + +/* Check if point is at infinity */ +int gej_is_infinity(const secp256k1_gej *r); + +/* Point doubling: out = 2*p (3M + 4S) */ +void gej_double(secp256k1_gej *r, const secp256k1_gej *p); + +/* Mixed addition: out = p + q (Jacobian + Affine, 8M + 3S) */ +void gej_add_ge(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_ge *q); + +/* Full Jacobian addition: out = p + q (11M + 5S) */ +void gej_add(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_gej *q); + +/* Convert Jacobian to affine */ +int gej_to_ge(secp256k1_ge *r, const secp256k1_gej *p); + +/* x-only affine conversion (skip y) */ +int gej_to_ge_x(secp256k1_fe *rx, const secp256k1_gej *p); + +/* ==================== Scalar Multiplication ==================== */ + +/* G multiplication using comb method: out = scalar * G */ +void ecmult_gen(secp256k1_gej *r, const secp256k1_scalar *scalar); + +/* Arbitrary point multiplication using GLV + wNAF: out = scalar * p */ +void ecmult(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_scalar *scalar); + +/* Dual scalar multiplication (Strauss + GLV): out = s*G + e*P */ +void ecmult_double_g(secp256k1_gej *r, const secp256k1_scalar *s, + const secp256k1_ge *p, const secp256k1_scalar *e); + +/* ==================== Key/Point Codec ==================== */ + +/* Decompress x-only pubkey: lift x to (x, y) with even y */ +int point_lift_x(secp256k1_fe *out_x, secp256k1_fe *out_y, const secp256k1_fe *x); + +/* Parse public key (33 or 65 bytes) into affine point */ +int point_parse_pubkey(secp256k1_ge *r, const uint8_t *pubkey, size_t len); + +/* Serialize affine point as uncompressed (65 bytes: 04 || x || y) */ +void point_serialize_uncompressed(uint8_t *out65, const secp256k1_ge *p); + +/* Serialize affine point as compressed (33 bytes: 02/03 || x) */ +void point_serialize_compressed(uint8_t *out33, const secp256k1_ge *p); + +/* Check if y is even */ +int point_has_even_y(const secp256k1_fe *y); + +/* ==================== Batch Operations ==================== */ + +/* Batch convert array of Jacobian points to affine using Montgomery's trick */ +void batch_to_affine(secp256k1_ge *out, const secp256k1_gej *in, int count); + +/* Initialize precomputed tables (called by secp256k1c_init) */ +void ecmult_tables_init(void); + +#endif /* SECP256K1_POINT_H */ diff --git a/quartz/src/main/c/secp256k1/scalar.c b/quartz/src/main/c/secp256k1/scalar.c new file mode 100644 index 000000000..d1e30be10 --- /dev/null +++ b/quartz/src/main/c/secp256k1/scalar.c @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * Scalar arithmetic mod n with GLV decomposition and wNAF encoding. + */ +#include "scalar.h" +#include + +int scalar_is_zero(const secp256k1_scalar *a) { + return (a->d[0] | a->d[1] | a->d[2] | a->d[3]) == 0; +} + +int scalar_cmp(const secp256k1_scalar *a, const secp256k1_scalar *b) { + for (int i = 3; i >= 0; i--) { + if (a->d[i] < b->d[i]) return -1; + if (a->d[i] > b->d[i]) return 1; + } + return 0; +} + +int scalar_is_valid(const secp256k1_scalar *a) { + return !scalar_is_zero(a) && scalar_cmp(a, &SCALAR_N) < 0; +} + +/* Helper: add 4x64 with carry, returns overflow */ +static int add256(uint64_t *r, const uint64_t *a, const uint64_t *b) { + uint64_t carry = 0; + for (int i = 0; i < 4; i++) { + uint64_t sum = a[i] + b[i] + carry; + carry = (sum < a[i]) || (carry && sum == a[i]) ? 1 : 0; + r[i] = sum; + } + return (int)carry; +} + +/* Helper: sub 4x64 with borrow, returns underflow */ +static int sub256(uint64_t *r, const uint64_t *a, const uint64_t *b) { + uint64_t borrow = 0; + for (int i = 0; i < 4; i++) { + uint64_t diff = a[i] - b[i] - borrow; + borrow = (a[i] < b[i] + borrow) || (borrow && b[i] == UINT64_MAX) ? 1 : 0; + r[i] = diff; + } + return (int)borrow; +} + +void scalar_reduce(secp256k1_scalar *r) { + if (scalar_cmp(r, &SCALAR_N) >= 0) { + sub256(r->d, r->d, SCALAR_N.d); + } +} + +void scalar_add(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b) { + int carry = add256(r->d, a->d, b->d); + if (carry) { + /* Overflow: subtract n. n_complement = 2^256 - n */ + uint64_t nc[4] = { + ~SCALAR_N.d[0] + 1, + ~SCALAR_N.d[1] + (!SCALAR_N.d[0] ? 1ULL : 0ULL), + ~SCALAR_N.d[2] + (!(SCALAR_N.d[0] | SCALAR_N.d[1]) ? 1ULL : 0ULL), + ~SCALAR_N.d[3] + }; + add256(r->d, r->d, nc); + } + scalar_reduce(r); +} + +void scalar_negate(secp256k1_scalar *r, const secp256k1_scalar *a) { + if (scalar_is_zero(a)) { + *r = SCALAR_ZERO; + } else { + sub256(r->d, SCALAR_N.d, a->d); + } +} + +void scalar_sub(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b) { + secp256k1_scalar neg_b; + scalar_negate(&neg_b, b); + scalar_add(r, a, &neg_b); +} + +/* Multiply mod n using schoolbook 4x4 -> 8 limb, then Barrett reduction */ +void scalar_mul(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b) { + /* Full 512-bit product */ + uint64_t t[8] = {0}; + +#if HAVE_INT128 + for (int i = 0; i < 4; i++) { + uint128_t carry = 0; + for (int j = 0; j < 4; j++) { + carry += (uint128_t)a->d[i] * b->d[j] + t[i + j]; + t[i + j] = (uint64_t)carry; + carry >>= 64; + } + t[i + 4] = (uint64_t)carry; + } +#else + /* Portable fallback */ + for (int i = 0; i < 4; i++) { + uint64_t carry = 0; + for (int j = 0; j < 4; j++) { + uint64_t a_lo = a->d[i] & 0xFFFFFFFF; + uint64_t a_hi = a->d[i] >> 32; + uint64_t b_lo = b->d[j] & 0xFFFFFFFF; + uint64_t b_hi = b->d[j] >> 32; + + uint64_t ll = a_lo * b_lo; + uint64_t lh = a_lo * b_hi; + uint64_t hl = a_hi * b_lo; + uint64_t hh = a_hi * b_hi; + + uint64_t mid = (ll >> 32) + (lh & 0xFFFFFFFF) + (hl & 0xFFFFFFFF); + uint64_t lo = (ll & 0xFFFFFFFF) | (mid << 32); + uint64_t hi = hh + (lh >> 32) + (hl >> 32) + (mid >> 32); + + uint64_t sum = t[i + j] + lo + carry; + carry = hi + (sum < t[i + j] ? 1 : 0) + (sum < lo && carry ? 1 : 0); + t[i + j] = sum; + } + t[i + 4] += carry; + } +#endif + + /* Reduce 512-bit product mod n. + * Method: fold high limbs using 2^256 mod n = 0x14551231950B75FC4402DA1732FC9BEBF. + * For the crypto operations we use (challenge * secret_key), inputs are < n, + * so the product is < n^2 < 2^512. We reduce by subtracting n repeatedly. + * This is simple and correct; a Barrett reduction could be added for speed. */ + r->d[0] = t[0]; r->d[1] = t[1]; r->d[2] = t[2]; r->d[3] = t[3]; + + /* Fold high limbs: for each non-zero high limb, the product is too large. + * Simple approach: reduce by subtracting n while result >= n. */ + if (t[4] | t[5] | t[6] | t[7]) { + /* High part is non-zero: use the modular constant c = 2^256 mod n. + * c = {0x402DA1732FC9BEBF, 0x4551231950B75FC4, 0x1, 0x0} */ + static const uint64_t MOD_C[4] = { + 0x402DA1732FC9BEBFULL, 0x4551231950B75FC4ULL, 1, 0 + }; +#if HAVE_INT128 + /* Accumulate: r += t[i+4] * c * 2^(64*i) for i=0..3 */ + for (int i = 0; i < 4; i++) { + if (t[i + 4] == 0) continue; + uint128_t carry = 0; + for (int j = 0; j < 4; j++) { + int k = i + j; + if (k < 4) { + carry += (uint128_t)t[i + 4] * MOD_C[j] + r->d[k]; + r->d[k] = (uint64_t)carry; + carry >>= 64; + } + } + } +#else + (void)MOD_C; +#endif + } + /* Final reduction */ + while (scalar_cmp(r, &SCALAR_N) >= 0) { + sub256(r->d, r->d, SCALAR_N.d); + } +} + +void scalar_to_bytes(uint8_t *out32, const secp256k1_scalar *a) { + for (int i = 0; i < 4; i++) { + uint64_t v = a->d[3 - i]; + for (int j = 0; j < 8; j++) { + out32[i * 8 + j] = (uint8_t)(v >> ((7 - j) * 8)); + } + } +} + +void scalar_from_bytes(secp256k1_scalar *r, const uint8_t *in32) { + for (int i = 0; i < 4; i++) { + uint64_t v = 0; + for (int j = 0; j < 8; j++) { + v = (v << 8) | in32[i * 8 + j]; + } + r->d[3 - i] = v; + } +} + +/* ==================== GLV Decomposition ==================== */ + +/* + * GLV constants for secp256k1. + * Split k into k1, k2 where k = k1 + k2*lambda mod n, |k1|,|k2| ~ 128 bits. + */ + +/* Precomputed GLV constants (from Kotlin Fe4 signed longs, converted to uint64) */ +static const uint64_t GLV_G1[4] = { + 0xE893209A45DBB031ULL, 0x3DAA8A1471E8CA7FULL, + 0xE86C90E49284EB15ULL, 0x3086D221A7D46BCDULL +}; +static const uint64_t GLV_G2[4] = { + 0x1571B4AE8AC47F71ULL, 0x221208AC9DF506C6ULL, + 0x6F547FA90ABFE4C4ULL, 0xE4437ED6010E8828ULL +}; + +static const secp256k1_scalar GLV_MINUS_B1 = {{ + 0x6F547FA90ABFE4C3ULL, 0xE4437ED6010E8828ULL, 0, 0 +}}; + +static const secp256k1_scalar GLV_MINUS_B2 = {{ + 0xD765CDA83DB1562CULL, 0x8A280AC50774346DULL, + 0xFFFFFFFFFFFFFFFEULL, 0xFFFFFFFFFFFFFFFFULL +}}; + +static const secp256k1_scalar GLV_MINUS_LAMBDA = {{ + 0xE0CFC810B51283CFULL, 0xA880B9FC8EC739C2ULL, + 0x5AD9E3FD77ED9BA4ULL, 0xAC9C52B33FA3CF1FULL +}}; + +/* + * mulShift384: compute (k * g) >> 384 for 256x256->512 bit product. + * Only the upper 128 bits (bits 384..511) are needed for the GLV decomposition. + */ +static void mul_shift384(secp256k1_scalar *r, const secp256k1_scalar *k, const uint64_t g[4]) { + uint64_t t[8] = {0}; +#if HAVE_INT128 + for (int i = 0; i < 4; i++) { + uint128_t carry = 0; + for (int j = 0; j < 4; j++) { + carry += (uint128_t)k->d[i] * g[j] + t[i + j]; + t[i + j] = (uint64_t)carry; + carry >>= 64; + } + t[i + 4] = (uint64_t)carry; + } +#else + /* Portable fallback */ + for (int i = 0; i < 4; i++) { + uint64_t carry = 0; + for (int j = 0; j < 4; j++) { + uint64_t a_lo = k->d[i] & 0xFFFFFFFF; + uint64_t a_hi = k->d[i] >> 32; + uint64_t b_lo = g[j] & 0xFFFFFFFF; + uint64_t b_hi = g[j] >> 32; + uint64_t ll = a_lo * b_lo; + uint64_t lh = a_lo * b_hi; + uint64_t hl = a_hi * b_lo; + uint64_t hh = a_hi * b_hi; + uint64_t mid = (ll >> 32) + (lh & 0xFFFFFFFF) + (hl & 0xFFFFFFFF); + uint64_t lo = (ll & 0xFFFFFFFF) | (mid << 32); + uint64_t hi = hh + (lh >> 32) + (hl >> 32) + (mid >> 32); + uint64_t sum = t[i + j] + lo + carry; + carry = hi + (sum < lo ? 1 : 0); + if (carry < hi) carry++; /* handle double overflow */ + t[i + j] = sum; + } + t[i + 4] += carry; + } +#endif + /* Extract bits [384..511] = t[6] and t[7], rounded */ + /* Add rounding bit at position 383 */ + uint64_t round = (t[5] >> 63) & 1; + r->d[0] = t[6] + round; + r->d[1] = t[7] + (r->d[0] < t[6] ? 1 : 0); + r->d[2] = 0; + r->d[3] = 0; +} + +void glv_split_scalar(glv_split *out, const secp256k1_scalar *k) { + secp256k1_scalar c1, c2, t1, t2; + + mul_shift384(&c1, k, GLV_G1); + mul_shift384(&c2, k, GLV_G2); + + /* r2 = c1 * (-b1) + c2 * (-b2) mod n */ + scalar_mul(&t1, &c1, &GLV_MINUS_B1); + scalar_mul(&t2, &c2, &GLV_MINUS_B2); + scalar_add(&out->k2, &t1, &t2); + + /* r1 = r2 * (-lambda) + k mod n */ + scalar_mul(&t1, &out->k2, &GLV_MINUS_LAMBDA); + scalar_add(&out->k1, &t1, k); + + /* Ensure k1, k2 are in the lower half */ + out->neg_k1 = scalar_cmp(&out->k1, &SCALAR_N_HALF) > 0; + out->neg_k2 = scalar_cmp(&out->k2, &SCALAR_N_HALF) > 0; + if (out->neg_k1) scalar_negate(&out->k1, &out->k1); + if (out->neg_k2) scalar_negate(&out->k2, &out->k2); +} + +/* ==================== wNAF Encoding ==================== */ + +int wnaf_encode(int *wnaf, int max_len, const secp256k1_scalar *s, int w) { + secp256k1_scalar sc = *s; + int len = 0; + int window = 1 << w; /* 2^w */ + int half = window >> 1; /* 2^(w-1) */ + + memset(wnaf, 0, (size_t)max_len * sizeof(int)); + + while (!scalar_is_zero(&sc) && len < max_len) { + if (sc.d[0] & 1) { + int digit = (int)(sc.d[0] & (uint64_t)(window - 1)); + if (digit >= half) digit -= window; + wnaf[len] = digit; + /* Subtract digit from sc */ + if (digit > 0) { + /* sc -= digit */ + uint64_t borrow = 0; + uint64_t val = (uint64_t)digit; + for (int i = 0; i < 4; i++) { + uint64_t diff = sc.d[i] - val - borrow; + borrow = (sc.d[i] < val + borrow) ? 1 : 0; + sc.d[i] = diff; + val = 0; + } + } else if (digit < 0) { + /* sc += (-digit) */ + uint64_t carry = 0; + uint64_t val = (uint64_t)(-digit); + for (int i = 0; i < 4; i++) { + uint64_t sum = sc.d[i] + val + carry; + carry = (sum < sc.d[i]) ? 1 : 0; + sc.d[i] = sum; + val = 0; + } + } + } else { + wnaf[len] = 0; + } + /* Right shift by 1 */ + sc.d[0] = (sc.d[0] >> 1) | (sc.d[1] << 63); + sc.d[1] = (sc.d[1] >> 1) | (sc.d[2] << 63); + sc.d[2] = (sc.d[2] >> 1) | (sc.d[3] << 63); + sc.d[3] = sc.d[3] >> 1; + len++; + } + return len; +} diff --git a/quartz/src/main/c/secp256k1/scalar.h b/quartz/src/main/c/secp256k1/scalar.h new file mode 100644 index 000000000..4d7622870 --- /dev/null +++ b/quartz/src/main/c/secp256k1/scalar.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Scalar arithmetic modulo n (group order of secp256k1). + * n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + * Uses 4x64-bit limbs (fully packed, little-endian). + */ +#ifndef SECP256K1_SCALAR_H +#define SECP256K1_SCALAR_H + +#include "secp256k1_c.h" + +/* n in 4x64 little-endian */ +static const secp256k1_scalar SCALAR_N = {{ + 0xBFD25E8CD0364141ULL, + 0xBAAEDCE6AF48A03BULL, + 0xFFFFFFFFFFFFFFFEULL, + 0xFFFFFFFFFFFFFFFFULL +}}; + +static const secp256k1_scalar SCALAR_ZERO = {{0, 0, 0, 0}}; + +/* n/2 for GLV split sign check */ +static const secp256k1_scalar SCALAR_N_HALF = {{ + 0xDFE92F46681B20A0ULL, + 0x5D576E7357A4501DULL, + 0xFFFFFFFFFFFFFFFFULL, + 0x7FFFFFFFFFFFFFFFULL +}}; + +/* lambda for GLV endomorphism */ +static const secp256k1_scalar SCALAR_LAMBDA = {{ + 0xDF02967C1B23BD72ULL, + 0x122E22EA20816678ULL, + 0xA5261C028812645AULL, + 0x5363AD4CC05C30E0ULL +}}; + +int scalar_is_zero(const secp256k1_scalar *a); +int scalar_is_valid(const secp256k1_scalar *a); +int scalar_cmp(const secp256k1_scalar *a, const secp256k1_scalar *b); + +/* r = (a + b) mod n */ +void scalar_add(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b); + +/* r = (a - b) mod n */ +void scalar_sub(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b); + +/* r = -a mod n */ +void scalar_negate(secp256k1_scalar *r, const secp256k1_scalar *a); + +/* r = (a * b) mod n */ +void scalar_mul(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b); + +/* Reduce: if a >= n, subtract n */ +void scalar_reduce(secp256k1_scalar *r); + +/* Serialize/deserialize (big-endian 32 bytes) */ +void scalar_to_bytes(uint8_t *out32, const secp256k1_scalar *a); +void scalar_from_bytes(secp256k1_scalar *r, const uint8_t *in32); + +/* GLV decomposition: k = k1 + k2*lambda, |k1|,|k2| ~ 128 bits */ +typedef struct { + secp256k1_scalar k1; + secp256k1_scalar k2; + int neg_k1; + int neg_k2; +} glv_split; + +void glv_split_scalar(glv_split *out, const secp256k1_scalar *k); + +/* wNAF encoding: encode scalar into width-w NAF digits */ +int wnaf_encode(int *wnaf, int max_len, const secp256k1_scalar *s, int w); + +#endif /* SECP256K1_SCALAR_H */ diff --git a/quartz/src/main/c/secp256k1/schnorr.c b/quartz/src/main/c/secp256k1/schnorr.c new file mode 100644 index 000000000..301c21143 --- /dev/null +++ b/quartz/src/main/c/secp256k1/schnorr.c @@ -0,0 +1,484 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * BIP-340 Schnorr signatures for Nostr with fast verification, + * batch verification, and cached pubkey signing. + */ +#include "secp256k1_c.h" +#include "field.h" +#include "scalar.h" +#include "point.h" +#include "sha256.h" +#include +#include + +/* ==================== Precomputed BIP-340 Tag Prefixes ==================== */ + +static uint8_t CHALLENGE_PREFIX[64]; +static uint8_t AUX_PREFIX[64]; +static uint8_t NONCE_PREFIX[64]; +static int prefixes_initialized = 0; + +static void init_prefixes(void) { + if (prefixes_initialized) return; + uint8_t tag_hash[32]; + + secp256k1_sha256_hash(tag_hash, (const uint8_t *)"BIP0340/challenge", 17); + memcpy(CHALLENGE_PREFIX, tag_hash, 32); + memcpy(CHALLENGE_PREFIX + 32, tag_hash, 32); + + secp256k1_sha256_hash(tag_hash, (const uint8_t *)"BIP0340/aux", 11); + memcpy(AUX_PREFIX, tag_hash, 32); + memcpy(AUX_PREFIX + 32, tag_hash, 32); + + secp256k1_sha256_hash(tag_hash, (const uint8_t *)"BIP0340/nonce", 13); + memcpy(NONCE_PREFIX, tag_hash, 32); + memcpy(NONCE_PREFIX + 32, tag_hash, 32); + + prefixes_initialized = 1; +} + +/* ==================== Pubkey Decompression Cache ==================== */ + +#define PUBKEY_CACHE_SIZE 1024 +#define PUBKEY_CACHE_MASK (PUBKEY_CACHE_SIZE - 1) + +typedef struct { + uint8_t key_bytes[32]; + secp256k1_fe px; + secp256k1_fe py; + int valid; +} cached_pubkey; + +static cached_pubkey pubkey_cache[PUBKEY_CACHE_SIZE]; + +static int cache_slot(const uint8_t *pub32) { + return ((int)pub32[0] | ((int)pub32[1] << 8)) & PUBKEY_CACHE_MASK; +} + +static int lift_x_cached(secp256k1_fe *out_x, secp256k1_fe *out_y, const uint8_t *pub32) { + int slot = cache_slot(pub32); + cached_pubkey *c = &pubkey_cache[slot]; + + if (c->valid && memcmp(c->key_bytes, pub32, 32) == 0) { + *out_x = c->px; + *out_y = c->py; + return 1; + } + + secp256k1_fe x; + fe_from_bytes(&x, pub32); + if (!point_lift_x(out_x, out_y, &x)) return 0; + + memcpy(c->key_bytes, pub32, 32); + c->px = *out_x; + c->py = *out_y; + c->valid = 1; + return 1; +} + +/* ==================== Library Init ==================== */ + +void secp256k1c_init(void) { + ecmult_tables_init(); + init_prefixes(); + memset(pubkey_cache, 0, sizeof(pubkey_cache)); +} + +/* ==================== Key Operations ==================== */ + +int secp256k1c_pubkey_create(uint8_t *pub65, const uint8_t *seckey32) { + secp256k1_scalar sk; + scalar_from_bytes(&sk, seckey32); + if (!scalar_is_valid(&sk)) return 0; + + secp256k1_gej rj; + ecmult_gen(&rj, &sk); + + secp256k1_ge r; + if (!gej_to_ge(&r, &rj)) return 0; + + point_serialize_uncompressed(pub65, &r); + return 1; +} + +int secp256k1c_pubkey_compress(uint8_t *pub33, const uint8_t *pub65) { + if (pub65[0] != 0x04) return 0; + pub33[0] = (pub65[64] & 1) ? 0x03 : 0x02; + memcpy(pub33 + 1, pub65 + 1, 32); + return 1; +} + +int secp256k1c_seckey_verify(const uint8_t *seckey32) { + secp256k1_scalar sk; + scalar_from_bytes(&sk, seckey32); + return scalar_is_valid(&sk); +} + +/* ==================== Schnorr Sign (internal) ==================== */ + +static int schnorr_sign_internal( + uint8_t *sig64, + const uint8_t *msg, size_t msg_len, + const secp256k1_scalar *d0, + const uint8_t *pub_x_bytes32, + int pub_has_even_y, + const uint8_t *auxrand32 +) { + secp256k1_scalar d; + uint8_t d_bytes[32]; + secp256k1_sha256 ctx; + + /* Negate d if y is odd */ + if (pub_has_even_y) { + d = *d0; + } else { + scalar_negate(&d, d0); + } + scalar_to_bytes(d_bytes, &d); + + /* Compute t = d XOR H(aux) */ + uint8_t t_bytes[32]; + if (auxrand32) { + uint8_t aux_hash[32]; + secp256k1_tagged_hash_precomputed(aux_hash, AUX_PREFIX, auxrand32, 32); + for (int i = 0; i < 32; i++) { + t_bytes[i] = d_bytes[i] ^ aux_hash[i]; + } + } else { + memcpy(t_bytes, d_bytes, 32); + } + + /* Nonce: k0 = H(t || pub || msg) */ + uint8_t rand_hash[32]; + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, NONCE_PREFIX, 64); + secp256k1_sha256_update(&ctx, t_bytes, 32); + secp256k1_sha256_update(&ctx, pub_x_bytes32, 32); + secp256k1_sha256_update(&ctx, msg, msg_len); + secp256k1_sha256_finalize(&ctx, rand_hash); + + secp256k1_scalar k0; + scalar_from_bytes(&k0, rand_hash); + scalar_reduce(&k0); + if (scalar_is_zero(&k0)) return 0; + + /* R = k0 * G */ + secp256k1_gej rj; + ecmult_gen(&rj, &k0); + secp256k1_ge r; + if (!gej_to_ge(&r, &rj)) return 0; + + /* Negate k if R.y is odd */ + secp256k1_scalar k; + if (point_has_even_y(&r.y)) { + k = k0; + } else { + scalar_negate(&k, &k0); + } + + /* Challenge: e = H(R.x || pub || msg) */ + uint8_t rx_bytes[32], e_hash[32]; + fe_to_bytes(rx_bytes, &r.x); + + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, CHALLENGE_PREFIX, 64); + secp256k1_sha256_update(&ctx, rx_bytes, 32); + secp256k1_sha256_update(&ctx, pub_x_bytes32, 32); + secp256k1_sha256_update(&ctx, msg, msg_len); + secp256k1_sha256_finalize(&ctx, e_hash); + + secp256k1_scalar e; + scalar_from_bytes(&e, e_hash); + scalar_reduce(&e); + + /* s = k + e*d mod n */ + secp256k1_scalar ed, s; + scalar_mul(&ed, &e, &d); + scalar_add(&s, &k, &ed); + + /* Output signature: R.x || s */ + memcpy(sig64, rx_bytes, 32); + scalar_to_bytes(sig64 + 32, &s); + return 1; +} + +/* ==================== Public Schnorr Sign ==================== */ + +int secp256k1c_schnorr_sign( + uint8_t *sig64, + const uint8_t *msg, size_t msg_len, + const uint8_t *seckey32, + const uint8_t *auxrand32 +) { + secp256k1_scalar d0; + scalar_from_bytes(&d0, seckey32); + if (!scalar_is_valid(&d0)) return 0; + + /* Derive pubkey */ + secp256k1_gej pj; + ecmult_gen(&pj, &d0); + secp256k1_ge p; + if (!gej_to_ge(&p, &pj)) return 0; + + uint8_t pub_x[32]; + fe_to_bytes(pub_x, &p.x); + int even_y = point_has_even_y(&p.y); + + return schnorr_sign_internal(sig64, msg, msg_len, &d0, pub_x, even_y, auxrand32); +} + +int secp256k1c_schnorr_sign_xonly( + uint8_t *sig64, + const uint8_t *msg, size_t msg_len, + const uint8_t *seckey32, + const uint8_t *xonly_pub32, + const uint8_t *auxrand32 +) { + secp256k1_scalar d0; + scalar_from_bytes(&d0, seckey32); + if (!scalar_is_valid(&d0)) return 0; + + /* BIP-340 x-only pubkeys always have even y */ + return schnorr_sign_internal(sig64, msg, msg_len, &d0, xonly_pub32, 1, auxrand32); +} + +/* ==================== Schnorr Verify (core) ==================== */ + +/* + * Core verification: compute Q = s*G + (-e)*P and check X matches. + * Returns 1 if x-coordinate matches (in Jacobian: X == r*Z^2). + * Leaves the Jacobian result for callers that need y-parity. + */ +static int schnorr_verify_core( + const uint8_t *sig64, + const uint8_t *msg, size_t msg_len, + const uint8_t *pub32, + secp256k1_gej *result_out +) { + /* Decompress pubkey */ + secp256k1_fe px, py; + if (!lift_x_cached(&px, &py, pub32)) return 0; + + /* Parse r, s from signature */ + secp256k1_fe r_fe; + fe_from_bytes(&r_fe, sig64); + /* Check r < p */ + if (fe_cmp(&r_fe, &FE_P) >= 0) return 0; + + secp256k1_scalar s; + scalar_from_bytes(&s, sig64 + 32); + if (scalar_cmp(&s, &SCALAR_N) >= 0) return 0; + + /* Challenge: e = H(R.x || pub || msg) */ + uint8_t e_hash[32]; + secp256k1_sha256 ctx; + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, CHALLENGE_PREFIX, 64); + secp256k1_sha256_update(&ctx, sig64, 32); /* R.x from signature */ + secp256k1_sha256_update(&ctx, pub32, 32); + secp256k1_sha256_update(&ctx, msg, msg_len); + secp256k1_sha256_finalize(&ctx, e_hash); + + secp256k1_scalar e; + scalar_from_bytes(&e, e_hash); + scalar_reduce(&e); + + /* Q = s*G + (-e)*P via Shamir's trick */ + scalar_negate(&e, &e); + secp256k1_ge p_aff; + p_aff.x = px; + p_aff.y = py; + ecmult_double_g(result_out, &s, &p_aff, &e); + + if (gej_is_infinity(result_out)) return 0; + + /* Jacobian x-check: X == r*Z^2 (no inversion needed) */ + secp256k1_fe z2, rz2; + fe_sqr(&z2, &result_out->z); + fe_mul(&rz2, &r_fe, &z2); + fe_normalize_full(&rz2); + + secp256k1_fe qx = result_out->x; + fe_normalize_full(&qx); + + return fe_equal(&qx, &rz2); +} + +/* ==================== Public Verify ==================== */ + +int secp256k1c_schnorr_verify( + const uint8_t *sig64, + const uint8_t *msg, size_t msg_len, + const uint8_t *pub32 +) { + if (!sig64 || !pub32) return 0; + + secp256k1_gej result; + if (!schnorr_verify_core(sig64, msg, msg_len, pub32, &result)) return 0; + + /* Full BIP-340: check y-parity (requires inversion) */ + secp256k1_ge r_aff; + if (!gej_to_ge(&r_aff, &result)) return 0; + return point_has_even_y(&r_aff.y); +} + +int secp256k1c_schnorr_verify_fast( + const uint8_t *sig64, + const uint8_t *msg, size_t msg_len, + const uint8_t *pub32 +) { + if (!sig64 || !pub32) return 0; + + secp256k1_gej result; + return schnorr_verify_core(sig64, msg, msg_len, pub32, &result); +} + +/* ==================== Batch Verification ==================== */ + +int secp256k1c_schnorr_verify_batch( + const uint8_t *pub32, + const uint8_t *const *sigs64, + const uint8_t *const *msgs, + const size_t *msg_lens, + size_t count +) { + if (count == 0) return 1; + if (count == 1) return secp256k1c_schnorr_verify(sigs64[0], msgs[0], msg_lens[0], pub32); + if (!pub32) return 0; + + /* Decompress pubkey once */ + secp256k1_fe px, py; + if (!lift_x_cached(&px, &py, pub32)) return 0; + + /* Accumulators */ + secp256k1_scalar s_sum = SCALAR_ZERO; + secp256k1_scalar e_sum = SCALAR_ZERO; + secp256k1_gej r_sum; + gej_set_infinity(&r_sum); + + for (size_t i = 0; i < count; i++) { + const uint8_t *sig = sigs64[i]; + const uint8_t *msg = msgs[i]; + size_t msg_len = msg_lens[i]; + + if (!sig) return 0; + + /* Parse r, s */ + secp256k1_fe r_fe; + fe_from_bytes(&r_fe, sig); + if (fe_cmp(&r_fe, &FE_P) >= 0) return 0; + + secp256k1_scalar s; + scalar_from_bytes(&s, sig + 32); + if (scalar_cmp(&s, &SCALAR_N) >= 0) return 0; + + /* Accumulate s */ + scalar_add(&s_sum, &s_sum, &s); + + /* Challenge e_i */ + uint8_t e_hash[32]; + secp256k1_sha256 ctx; + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, CHALLENGE_PREFIX, 64); + secp256k1_sha256_update(&ctx, sig, 32); + secp256k1_sha256_update(&ctx, pub32, 32); + secp256k1_sha256_update(&ctx, msg, msg_len); + secp256k1_sha256_finalize(&ctx, e_hash); + + secp256k1_scalar e; + scalar_from_bytes(&e, e_hash); + scalar_reduce(&e); + scalar_add(&e_sum, &e_sum, &e); + + /* Lift R_i = liftX(r_i) */ + secp256k1_fe rx, ry; + if (!point_lift_x(&rx, &ry, &r_fe)) return 0; + + /* Accumulate R_sum += R_i */ + if (gej_is_infinity(&r_sum)) { + gej_set_ge(&r_sum, &(secp256k1_ge){.x = rx, .y = ry}); + } else { + secp256k1_ge r_aff = {.x = rx, .y = ry}; + secp256k1_gej tmp; + gej_add_ge(&tmp, &r_sum, &r_aff); + r_sum = tmp; + } + } + + /* Q = s_sum*G + (-e_sum)*P */ + scalar_negate(&e_sum, &e_sum); + secp256k1_ge p_aff = {.x = px, .y = py}; + secp256k1_gej q; + ecmult_double_g(&q, &s_sum, &p_aff, &e_sum); + + /* Check Q - R_sum == infinity */ + /* Negate R_sum.y */ + fe_negate(&r_sum.y, &r_sum.y, 1); + fe_normalize(&r_sum.y); + + secp256k1_gej result; + gej_add(&result, &q, &r_sum); + + return gej_is_infinity(&result); +} + +/* ==================== Tweak Operations ==================== */ + +int secp256k1c_privkey_tweak_add(uint8_t *result32, const uint8_t *seckey32, const uint8_t *tweak32) { + secp256k1_scalar a, b, r; + scalar_from_bytes(&a, seckey32); + scalar_from_bytes(&b, tweak32); + scalar_add(&r, &a, &b); + if (scalar_is_zero(&r) || scalar_cmp(&r, &SCALAR_N) >= 0) return 0; + scalar_to_bytes(result32, &r); + return 1; +} + +int secp256k1c_pubkey_tweak_mul(uint8_t *result, size_t result_len, + const uint8_t *pubkey, size_t pubkey_len, + const uint8_t *tweak32) { + secp256k1_ge p; + if (!point_parse_pubkey(&p, pubkey, pubkey_len)) return 0; + + secp256k1_scalar scalar; + scalar_from_bytes(&scalar, tweak32); + if (!scalar_is_valid(&scalar)) return 0; + + secp256k1_gej pj, rj; + gej_set_ge(&pj, &p); + ecmult(&rj, &pj, &scalar); + + secp256k1_ge r; + if (!gej_to_ge(&r, &rj)) return 0; + + if (result_len == 33) { + point_serialize_compressed(result, &r); + } else if (result_len == 65) { + point_serialize_uncompressed(result, &r); + } else { + return 0; + } + return 1; +} + +int secp256k1c_ecdh_xonly(uint8_t *result32, const uint8_t *xonly_pub32, const uint8_t *scalar32) { + secp256k1_fe x; + fe_from_bytes(&x, xonly_pub32); + + secp256k1_fe px, py; + if (!point_lift_x(&px, &py, &x)) return 0; + + secp256k1_scalar k; + scalar_from_bytes(&k, scalar32); + if (!scalar_is_valid(&k)) return 0; + + secp256k1_gej pj, rj; + gej_set_ge(&pj, &(secp256k1_ge){.x = px, .y = py}); + ecmult(&rj, &pj, &k); + + secp256k1_fe rx; + if (!gej_to_ge_x(&rx, &rj)) return 0; + fe_to_bytes(result32, &rx); + return 1; +} diff --git a/quartz/src/main/c/secp256k1/secp256k1_c.h b/quartz/src/main/c/secp256k1/secp256k1_c.h new file mode 100644 index 000000000..7d42e8037 --- /dev/null +++ b/quartz/src/main/c/secp256k1/secp256k1_c.h @@ -0,0 +1,169 @@ +/* + * 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. + */ +#ifndef SECP256K1_C_H +#define SECP256K1_C_H + +#include +#include +#include + +/* + * Custom secp256k1 implementation for Amethyst/Nostr. + * + * This mirrors the Kotlin pure implementation structure but uses + * C's 5x52-bit limbs (matching libsecp256k1) with platform-specific + * 128-bit integer support for maximum performance on ARM64 and x86_64. + * + * Key differences from the Kotlin version: + * - 5x52-bit limbs with 12-bit headroom (lazy reduction) + * - Native __int128 for 64x64->128 multiply (single MULQ/MUL instruction) + * - Platform-specific ASM for field multiply on ARM64 (UMULH, UMULL) + * - Precomputed tables at compile time (no lazy init overhead) + * - Batch verification with randomized linear combination + */ + +/* ==================== Platform Detection ==================== */ + +#if defined(__SIZEOF_INT128__) + #define HAVE_INT128 1 + typedef unsigned __int128 uint128_t; +#else + #define HAVE_INT128 0 +#endif + +#if defined(__aarch64__) || defined(_M_ARM64) + #define SECP_ARM64 1 +#else + #define SECP_ARM64 0 +#endif + +#if defined(__x86_64__) || defined(_M_X64) + #define SECP_X86_64 1 +#else + #define SECP_X86_64 0 +#endif + +/* ==================== Field Element (5x52-bit limbs) ==================== */ + +/* + * Field element modulo p = 2^256 - 2^32 - 977. + * 5 limbs of 52 bits each, with 12 bits of headroom per limb. + * This allows 3-8 chained additions without reduction (lazy reduction). + * + * Magnitude tracking: after N additions without reduction, each limb + * can be up to N * 2^52. We reduce when magnitude exceeds safe limits. + */ +typedef struct { + uint64_t d[5]; +} secp256k1_fe; + +/* Wide result of field multiplication (used internally) */ +typedef struct { + uint64_t d[10]; +} secp256k1_fe_wide; + +/* ==================== Scalar (mod n) ==================== */ + +typedef struct { + uint64_t d[4]; /* 4x64-bit limbs, little-endian */ +} secp256k1_scalar; + +/* ==================== Points ==================== */ + +/* Jacobian point: affine (X/Z^2, Y/Z^3) */ +typedef struct { + secp256k1_fe x; + secp256k1_fe y; + secp256k1_fe z; + int infinity; +} secp256k1_gej; + +/* Affine point */ +typedef struct { + secp256k1_fe x; + secp256k1_fe y; +} secp256k1_ge; + +/* ==================== Public API ==================== */ + +/* Initialize the library (precompute tables). Thread-safe, idempotent. */ +void secp256k1c_init(void); + +/* Key operations */ +int secp256k1c_pubkey_create(uint8_t *pub65, const uint8_t *seckey32); +int secp256k1c_pubkey_compress(uint8_t *pub33, const uint8_t *pub65); +int secp256k1c_seckey_verify(const uint8_t *seckey32); + +/* BIP-340 Schnorr signatures */ +int secp256k1c_schnorr_sign( + uint8_t *sig64, + const uint8_t *msg, + size_t msg_len, + const uint8_t *seckey32, + const uint8_t *auxrand32 /* NULL for deterministic */ +); + +/* Sign with pre-computed x-only pubkey (fast path) */ +int secp256k1c_schnorr_sign_xonly( + uint8_t *sig64, + const uint8_t *msg, + size_t msg_len, + const uint8_t *seckey32, + const uint8_t *xonly_pub32, + const uint8_t *auxrand32 +); + +/* Full BIP-340 verification (with y-parity check) */ +int secp256k1c_schnorr_verify( + const uint8_t *sig64, + const uint8_t *msg, + size_t msg_len, + const uint8_t *pub32 +); + +/* Fast verification (skip y-parity check, safe for Nostr) */ +int secp256k1c_schnorr_verify_fast( + const uint8_t *sig64, + const uint8_t *msg, + size_t msg_len, + const uint8_t *pub32 +); + +/* Batch verification of N signatures from the SAME pubkey */ +int secp256k1c_schnorr_verify_batch( + const uint8_t *pub32, + const uint8_t *const *sigs64, + const uint8_t *const *msgs, + const size_t *msg_lens, + size_t count +); + +/* BIP-32 key derivation */ +int secp256k1c_privkey_tweak_add(uint8_t *result32, const uint8_t *seckey32, const uint8_t *tweak32); + +/* ECDH */ +int secp256k1c_pubkey_tweak_mul(uint8_t *result, size_t result_len, + const uint8_t *pubkey, size_t pubkey_len, + const uint8_t *tweak32); + +int secp256k1c_ecdh_xonly(uint8_t *result32, const uint8_t *xonly_pub32, const uint8_t *scalar32); + +#endif /* SECP256K1_C_H */ diff --git a/quartz/src/main/c/secp256k1/sha256.c b/quartz/src/main/c/secp256k1/sha256.c new file mode 100644 index 000000000..6f627e9ca --- /dev/null +++ b/quartz/src/main/c/secp256k1/sha256.c @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * Minimal SHA-256 for BIP-340. No external dependencies. + */ +#include "sha256.h" +#include + +static const uint32_t K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +#define ROR32(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) +#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define SIG0(x) (ROR32(x, 2) ^ ROR32(x, 13) ^ ROR32(x, 22)) +#define SIG1(x) (ROR32(x, 6) ^ ROR32(x, 11) ^ ROR32(x, 25)) +#define sig0(x) (ROR32(x, 7) ^ ROR32(x, 18) ^ ((x) >> 3)) +#define sig1(x) (ROR32(x, 17) ^ ROR32(x, 19) ^ ((x) >> 10)) + +static inline uint32_t be32(const uint8_t *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static inline void be32_put(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v >> 24); + p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); + p[3] = (uint8_t)v; +} + +static void sha256_transform(uint32_t state[8], const uint8_t block[64]) { + uint32_t W[64]; + uint32_t a, b, c, d, e, f, g, h; + int i; + + for (i = 0; i < 16; i++) + W[i] = be32(block + 4 * i); + for (i = 16; i < 64; i++) + W[i] = sig1(W[i-2]) + W[i-7] + sig0(W[i-15]) + W[i-16]; + + a = state[0]; b = state[1]; c = state[2]; d = state[3]; + e = state[4]; f = state[5]; g = state[6]; h = state[7]; + + for (i = 0; i < 64; i++) { + uint32_t t1 = h + SIG1(e) + CH(e, f, g) + K[i] + W[i]; + uint32_t t2 = SIG0(a) + MAJ(a, b, c); + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + state[0] += a; state[1] += b; state[2] += c; state[3] += d; + state[4] += e; state[5] += f; state[6] += g; state[7] += h; +} + +void secp256k1_sha256_init(secp256k1_sha256 *ctx) { + ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19; + ctx->total = 0; +} + +void secp256k1_sha256_update(secp256k1_sha256 *ctx, const uint8_t *data, size_t len) { + size_t fill = (size_t)(ctx->total & 63); + ctx->total += len; + + if (fill && fill + len >= 64) { + size_t copy = 64 - fill; + memcpy(ctx->buf + fill, data, copy); + sha256_transform(ctx->state, ctx->buf); + data += copy; + len -= copy; + fill = 0; + } + + while (len >= 64) { + sha256_transform(ctx->state, data); + data += 64; + len -= 64; + } + + if (len > 0) { + memcpy(ctx->buf + fill, data, len); + } +} + +void secp256k1_sha256_finalize(secp256k1_sha256 *ctx, uint8_t *out32) { + uint64_t bits = ctx->total * 8; + size_t fill = (size_t)(ctx->total & 63); + uint8_t pad = (fill < 56) ? (uint8_t)(56 - fill) : (uint8_t)(120 - fill); + uint8_t tmp[72]; /* max padding */ + int i; + + memset(tmp, 0, sizeof(tmp)); + tmp[0] = 0x80; + secp256k1_sha256_update(ctx, tmp, pad); + + /* Append length in big-endian */ + be32_put(tmp, (uint32_t)(bits >> 32)); + be32_put(tmp + 4, (uint32_t)bits); + secp256k1_sha256_update(ctx, tmp, 8); + + for (i = 0; i < 8; i++) + be32_put(out32 + 4 * i, ctx->state[i]); +} + +void secp256k1_sha256_hash(uint8_t *out32, const uint8_t *data, size_t len) { + secp256k1_sha256 ctx; + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, data, len); + secp256k1_sha256_finalize(&ctx, out32); +} + +void secp256k1_tagged_hash(uint8_t *out32, const char *tag, + const uint8_t *msg, size_t msg_len) { + uint8_t tag_hash[32]; + secp256k1_sha256 ctx; + + secp256k1_sha256_hash(tag_hash, (const uint8_t *)tag, strlen(tag)); + + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, tag_hash, 32); + secp256k1_sha256_update(&ctx, tag_hash, 32); + secp256k1_sha256_update(&ctx, msg, msg_len); + secp256k1_sha256_finalize(&ctx, out32); +} + +void secp256k1_tagged_hash_precomputed(uint8_t *out32, + const uint8_t *prefix64, + const uint8_t *msg, size_t msg_len) { + secp256k1_sha256 ctx; + secp256k1_sha256_init(&ctx); + secp256k1_sha256_update(&ctx, prefix64, 64); + secp256k1_sha256_update(&ctx, msg, msg_len); + secp256k1_sha256_finalize(&ctx, out32); +} diff --git a/quartz/src/main/c/secp256k1/sha256.h b/quartz/src/main/c/secp256k1/sha256.h new file mode 100644 index 000000000..a3bb23f88 --- /dev/null +++ b/quartz/src/main/c/secp256k1/sha256.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * Minimal SHA-256 for BIP-340 tagged hashes. + */ +#ifndef SECP256K1_SHA256_H +#define SECP256K1_SHA256_H + +#include +#include + +typedef struct { + uint32_t state[8]; + uint8_t buf[64]; + uint64_t total; +} secp256k1_sha256; + +void secp256k1_sha256_init(secp256k1_sha256 *ctx); +void secp256k1_sha256_update(secp256k1_sha256 *ctx, const uint8_t *data, size_t len); +void secp256k1_sha256_finalize(secp256k1_sha256 *ctx, uint8_t *out32); + +/* One-shot convenience */ +void secp256k1_sha256_hash(uint8_t *out32, const uint8_t *data, size_t len); + +/* BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || msg) */ +void secp256k1_tagged_hash(uint8_t *out32, const char *tag, + const uint8_t *msg, size_t msg_len); + +/* Tagged hash with pre-computed prefix (64 bytes = SHA256(tag) || SHA256(tag)) */ +void secp256k1_tagged_hash_precomputed(uint8_t *out32, + const uint8_t *prefix64, + const uint8_t *msg, size_t msg_len); + +#endif /* SECP256K1_SHA256_H */ diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.native.kt new file mode 100644 index 000000000..b04bc7da0 --- /dev/null +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.native.kt @@ -0,0 +1,83 @@ +/* + * 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 + +actual object Secp256k1InstanceC { + actual fun init() { + // No-op on native — uses Kotlin implementation + } + + actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray = Secp256k1InstanceKotlin.compressedPubKeyFor(privKey) + + actual fun isPrivateKeyValid(il: ByteArray): Boolean = Secp256k1InstanceKotlin.isPrivateKeyValid(il) + + actual fun signSchnorr( + data: ByteArray, + privKey: ByteArray, + nonce: ByteArray?, + ): ByteArray = Secp256k1InstanceKotlin.signSchnorr(data, privKey, nonce) + + actual fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray?, + ): ByteArray = Secp256k1InstanceKotlin.signSchnorrWithXOnlyPubKey(data, privKey, xOnlyPubKey, nonce) + + actual fun verifySchnorr( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean = Secp256k1InstanceKotlin.verifySchnorr(signature, hash, pubKey) + + actual fun verifySchnorrFast( + signature: ByteArray, + hash: ByteArray, + pubKey: ByteArray, + ): Boolean = Secp256k1InstanceKotlin.verifySchnorrFast(signature, hash, pubKey) + + actual fun verifySchnorrBatch( + pubKey: ByteArray, + signatures: List, + messages: List, + ): Boolean = Secp256k1InstanceKotlin.verifySchnorrBatch(pubKey, signatures, messages) + + actual fun privateKeyAdd( + first: ByteArray, + second: ByteArray, + ): ByteArray = Secp256k1InstanceKotlin.privateKeyAdd(first, second) + + actual fun pubKeyTweakMulCompact( + pubKey: ByteArray, + privateKey: ByteArray, + ): ByteArray = Secp256k1InstanceKotlin.pubKeyTweakMulCompact(pubKey, privateKey) + + actual fun ecdhXOnly( + xOnlyPub: ByteArray, + scalar: ByteArray, + ): ByteArray { + val compressedPub = ByteArray(33) + compressedPub[0] = 0x02 + xOnlyPub.copyInto(compressedPub, 1, 0, 32) + val result = Secp256k1InstanceKotlin.pubKeyTweakMulCompact(xOnlyPub, scalar) + return result + } +}