diff --git a/.claude/skills/nostr-expert/references/crypto-and-encryption.md b/.claude/skills/nostr-expert/references/crypto-and-encryption.md index a17d4328c..0a1715bf6 100644 --- a/.claude/skills/nostr-expert/references/crypto-and-encryption.md +++ b/.claude/skills/nostr-expert/references/crypto-and-encryption.md @@ -16,9 +16,9 @@ Event signing, hashing, and NIP-44 payload encryption. ### secp256k1 abstraction (`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/`) - `Secp256k1Instance.kt` — `expect object` with `signSchnorr`, `verifySchnorr`, `pubKey(seckey)`, `sharedSecret`. -- `Secp256k1InstanceC.kt` — C-based actual using secp256k1 JNI (Android/JVM). - `Secp256k1InstanceKotlin.kt` — pure-Kotlin actual (iOS via native, etc.). - Android actual: `secp256k1-kmp-jni-android` (0.23.0). JVM actual: `secp256k1-kmp-jni-jvm`. +- Tests/benchmarks pull in `com.vitorpamplona:schnorr256k1-kmp` (libschnorr256k1) for the in-house C JNI baseline used in `Secp256k1CrossValidationTest` and the 3-way benchmarks; production never ships it. ### NIP-44 encryption (`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/`) diff --git a/benchmark/build.gradle b/benchmark/build.gradle index d2a5ae81f..2db4e6e8d 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -62,6 +62,9 @@ dependencies { androidTestImplementation project(path: ':commons') androidTestImplementation project(path: ':ammolite') + // Custom C secp256k1 (libschnorr256k1) for the 3-way Android benchmark + androidTestImplementation libs.schnorr256k1.kmp + androidTestImplementation libs.androidx.compose.foundation // Add your dependencies here. Note that you cannot benchmark code diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt index c3ac6eb2f..4dbb62771 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/Secp256k1CBenchmark.kt @@ -23,8 +23,8 @@ 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 com.vitorpamplona.schnorr256k1.Schnorr256k1 import fr.acinq.secp256k1.Secp256k1 import org.junit.Rule import org.junit.Test @@ -34,7 +34,7 @@ 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" + * 3. Custom C (libschnorr256k1 via JNI) — "FooC" * * Run with: ./gradlew :benchmark:connectedAndroidTest */ @@ -110,48 +110,42 @@ class Secp256k1CBenchmark { ) } - // ==================== Custom C (Our JNI) ==================== + // ==================== Custom C (libschnorr256k1 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) } + val cSig = Schnorr256k1.schnorrSign(msg32, privKey, auxRand)!! + val cXOnly = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!!.copyOfRange(1, 33) + benchmarkRule.measureRepeated { Schnorr256k1.schnorrVerify(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) } + val cSig = Schnorr256k1.schnorrSign(msg32, privKey, auxRand)!! + val cXOnly = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!!.copyOfRange(1, 33) + benchmarkRule.measureRepeated { Schnorr256k1.schnorrVerifyFast(cSig, msg32, cXOnly) } } @Test fun signSchnorrC() { - Secp256k1InstanceC.init() - benchmarkRule.measureRepeated { Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) } + benchmarkRule.measureRepeated { Schnorr256k1.schnorrSign(msg32, privKey, auxRand) } } @Test fun signSchnorrXOnlyC() { - Secp256k1InstanceC.init() - val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) - benchmarkRule.measureRepeated { Secp256k1InstanceC.signSchnorrWithXOnlyPubKey(msg32, privKey, cXOnly, auxRand) } + val cXOnly = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!!.copyOfRange(1, 33) + benchmarkRule.measureRepeated { Schnorr256k1.schnorrSignXOnly(msg32, privKey, cXOnly, auxRand) } } @Test fun pubkeyCreateC() { - Secp256k1InstanceC.init() - benchmarkRule.measureRepeated { Secp256k1InstanceC.compressedPubKeyFor(privKey) } + benchmarkRule.measureRepeated { Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!) } } @Test fun ecdhXOnlyC() { - Secp256k1InstanceC.init() val pub2xOnly = hexToBytes("c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133") - benchmarkRule.measureRepeated { Secp256k1InstanceC.ecdhXOnly(pub2xOnly, privKey) } + benchmarkRule.measureRepeated { Schnorr256k1.ecdhXOnly(pub2xOnly, privKey) } } // ==================== Batch Verification (all three) ==================== @@ -172,16 +166,15 @@ class Secp256k1CBenchmark { @Test fun verifySchnorrBatch16C() { - Secp256k1InstanceC.init() - val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + val cXOnly = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!!.copyOfRange(1, 33) val sigs = (0 until 16).map { i -> val m = ByteArray(32) { (i * 7 + it).toByte() } - Secp256k1InstanceC.signSchnorr(m, privKey, auxRand) + Schnorr256k1.schnorrSign(m, privKey, auxRand)!! } val msgs = (0 until 16).map { i -> ByteArray(32) { (i * 7 + it).toByte() } } benchmarkRule.measureRepeated { - Secp256k1InstanceC.verifySchnorrBatch(cXOnly, sigs, msgs) + Schnorr256k1.schnorrVerifyBatch(cXOnly, sigs, msgs) } } @@ -201,16 +194,15 @@ class Secp256k1CBenchmark { @Test fun verifySchnorrBatch200C() { - Secp256k1InstanceC.init() - val cXOnly = Secp256k1InstanceC.compressedPubKeyFor(privKey).copyOfRange(1, 33) + val cXOnly = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!!.copyOfRange(1, 33) val sigs = (0 until 200).map { i -> val m = ByteArray(32) { (i * 7 + it).toByte() } - Secp256k1InstanceC.signSchnorr(m, privKey, auxRand) + Schnorr256k1.schnorrSign(m, privKey, auxRand)!! } val msgs = (0 until 200).map { i -> ByteArray(32) { (i * 7 + it).toByte() } } benchmarkRule.measureRepeated { - Secp256k1InstanceC.verifySchnorrBatch(cXOnly, sigs, msgs) + Schnorr256k1.schnorrVerifyBatch(cXOnly, sigs, msgs) } } @@ -231,11 +223,9 @@ class Secp256k1CBenchmark { @Test fun sha256OurC() { - // Our C SHA-256 (ARM64 CE hardware acceleration) - Secp256k1InstanceC.init() + // libschnorr256k1 SHA-256 (ARM64 CE hardware acceleration where available) benchmarkRule.measureRepeated { - com.vitorpamplona.quartz.utils.Secp256k1C - .nativeSha256(sha256Input) + Schnorr256k1.sha256(sha256Input) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c51f30efd..cf3444037 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,7 @@ navigationCompose = "2.9.8" okhttp = "5.3.2" runner = "1.7.0" secp256k1KmpJniAndroid = "0.23.0" +schnorr256k1Kmp = "1.0.0" securityCryptoKtx = "1.1.0" slf4j = "2.0.17" spotless = "8.4.0" @@ -177,6 +178,7 @@ okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } +schnorr256k1-kmp = { group = "com.vitorpamplona", name = "schnorr256k1-kmp", version.ref = "schnorr256k1Kmp" } stream-webrtc-android = { group = "io.getstream", name = "stream-webrtc-android", version.ref = "streamWebrtcAndroid" } tarsosdsp = { group = "be.tarsos.dsp", name = "core", version.ref = "tarsosdsp" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index cddf5558d..b591f62f7 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -197,6 +197,9 @@ kotlin { // Bitcoin secp256k1 bindings implementation(libs.secp256k1.kmp.jni.jvm) + // Custom C secp256k1 (libschnorr256k1) for cross-validation + 3-way benchmark + implementation(libs.schnorr256k1.kmp) + // SQLite bundled driver for JVM tests implementation(libs.androidx.sqlite.bundled.jvm) } @@ -237,6 +240,9 @@ kotlin { // Bitcoin secp256k1 bindings to Android api(libs.secp256k1.kmp.jni.android) + + // Custom C secp256k1 (libschnorr256k1) for cross-validation + 3-way benchmark + implementation(libs.schnorr256k1.kmp) } } 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 deleted file mode 100644 index 9c9c3b6d7..000000000 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -/** - * Android actual: delegates to Secp256k1C JNI binding class. - * The native library is packaged as libsecp256k1_amethyst_jni.so - * in the APK's jniLibs (built via CMake in quartz/src/main/c/). - */ -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? - - @JvmStatic external fun nativeSha256(data: ByteArray): ByteArray? -} - -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/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.kt deleted file mode 100644 index 39d09ace7..000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -/** - * 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 deleted file mode 100644 index 079f65024..000000000 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1C.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -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? - - @JvmStatic external fun nativeSha256(data: 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 deleted file mode 100644 index 9488d1aad..000000000 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.jvm.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -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/Secp256k1CrossValidationTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1CrossValidationTest.kt index 2232f7f4b..98c34b338 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1CrossValidationTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1CrossValidationTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.utils.secp256k1 -import com.vitorpamplona.quartz.utils.Secp256k1InstanceC +import com.vitorpamplona.schnorr256k1.Schnorr256k1 import java.util.Random import kotlin.test.Test import kotlin.test.assertContentEquals @@ -32,7 +32,7 @@ import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1 * available on JVM: * 1. ACINQ / libsecp256k1-kmp (reference implementation) * 2. Pure Kotlin ([Secp256k1]) - * 3. Custom C library via JNI ([Secp256k1InstanceC]) — when loadable + * 3. Custom C library via JNI ([Schnorr256k1] from libschnorr256k1-kmp) — when loadable * * BIP-340 signatures are deterministic given (privkey, message, aux_rand), * so two correct implementations MUST produce identical signature bytes. @@ -124,15 +124,17 @@ class Secp256k1CrossValidationTest { @Test fun customCImplementationMatchesAcinqAndKotlin() { + // Probe libschnorr256k1's native lib before exercising it: on platforms + // where the JNI .so isn't packaged we skip rather than fail the test. val cAvailable = try { - Secp256k1InstanceC.init() + Schnorr256k1.seckeyVerify(ByteArray(32) { 1 }) true } catch (e: UnsatisfiedLinkError) { - println("SKIP: custom C library not loadable (${e.message})") + println("SKIP: libschnorr256k1 native lib not loadable (${e.message})") false } catch (e: Throwable) { - println("SKIP: custom C library init failed (${e.message})") + println("SKIP: libschnorr256k1 init failed (${e.message})") false } if (!cAvailable) return @@ -146,13 +148,13 @@ class Secp256k1CrossValidationTest { val kotlinPubCompressed = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)) val acinqPubCompressed = acinq.pubKeyCompress(acinq.pubkeyCreate(seckey)) - val cPubCompressed = Secp256k1InstanceC.compressedPubKeyFor(seckey) + val cPubCompressed = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(seckey)!!)!! assertContentEquals(acinqPubCompressed, kotlinPubCompressed, "pubkey: Kotlin vs ACINQ") assertContentEquals(acinqPubCompressed, cPubCompressed, "pubkey: C vs ACINQ") val kotlinSig = Secp256k1.signSchnorr(msg, seckey, aux) val acinqSig = acinq.signSchnorr(msg, seckey, aux) - val cSig = Secp256k1InstanceC.signSchnorr(msg, seckey, aux) + val cSig = Schnorr256k1.schnorrSign(msg, seckey, aux)!! assertContentEquals(acinqSig, kotlinSig, "sig: Kotlin vs ACINQ") assertContentEquals(acinqSig, cSig, "sig: C vs ACINQ") } 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 index b02a59673..7ac2b1f1f 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1TripleBenchmark.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1TripleBenchmark.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.utils.secp256k1 -import com.vitorpamplona.quartz.utils.Secp256k1InstanceC +import com.vitorpamplona.schnorr256k1.Schnorr256k1 import kotlin.test.Test import kotlin.test.assertTrue import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1 @@ -29,7 +29,7 @@ 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 + * 3. Custom C (libschnorr256k1 via JNI) — maximum performance target * * Run with: ./gradlew :quartz:jvmTest --tests "*.Secp256k1TripleBenchmark" */ @@ -112,22 +112,21 @@ class Secp256k1TripleBenchmark { // 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) + // Probe libschnorr256k1's native lib — skip the C row if the JNI .so + // isn't packaged for this platform (e.g. obscure JVM target). var cAvailable = false try { - Secp256k1InstanceC.init() + Schnorr256k1.seckeyVerify(ByteArray(32) { 1 }) 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") + println("NOTE: libschnorr256k1 native lib not available (${e.message})") } - val cPubKey = if (cAvailable) Secp256k1InstanceC.compressedPubKeyFor(privKey) else null + val cPubKey = if (cAvailable) Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!! else null val cXOnlyPub = cPubKey?.copyOfRange(1, 33) val cSig = if (cAvailable) { - Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) + Schnorr256k1.schnorrSign(msg32, privKey, auxRand)!! } else { null } @@ -139,7 +138,7 @@ class Secp256k1TripleBenchmark { "ACINQ should verify C signature", ) assertTrue( - Secp256k1InstanceC.verifySchnorr(acinqSig, msg32, acinqXOnlyPub), + Schnorr256k1.schnorrVerify(acinqSig, msg32, acinqXOnlyPub), "C should verify ACINQ signature", ) } @@ -156,7 +155,7 @@ class Secp256k1TripleBenchmark { kotlinOp = { Secp256k1.verifySchnorrFast(kotlinSig, msg32, kotlinXOnlyPub) }, cOp = if (cAvailable && cSig != null && cXOnlyPub != null) { - { Secp256k1InstanceC.verifySchnorrFast(cSig, msg32, cXOnlyPub) } + { Schnorr256k1.schnorrVerifyFast(cSig, msg32, cXOnlyPub) } } else { null }, @@ -172,7 +171,7 @@ class Secp256k1TripleBenchmark { kotlinOp = { Secp256k1.verifySchnorr(kotlinSig, msg32, kotlinXOnlyPub) }, cOp = if (cAvailable && cSig != null && cXOnlyPub != null) { - { Secp256k1InstanceC.verifySchnorr(cSig, msg32, cXOnlyPub) } + { Schnorr256k1.schnorrVerify(cSig, msg32, cXOnlyPub) } } else { null }, @@ -188,7 +187,7 @@ class Secp256k1TripleBenchmark { kotlinOp = { Secp256k1.signSchnorrWithXOnlyPubKey(msg32, privKey, kotlinXOnlyPub, auxRand) }, cOp = if (cAvailable && cXOnlyPub != null) { - { Secp256k1InstanceC.signSchnorrWithXOnlyPubKey(msg32, privKey, cXOnlyPub, auxRand) } + { Schnorr256k1.schnorrSignXOnly(msg32, privKey, cXOnlyPub, auxRand) } } else { null }, @@ -204,7 +203,7 @@ class Secp256k1TripleBenchmark { kotlinOp = { Secp256k1.signSchnorr(msg32, privKey, auxRand) }, cOp = if (cAvailable) { - { Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) } + { Schnorr256k1.schnorrSign(msg32, privKey, auxRand) } } else { null }, @@ -220,7 +219,7 @@ class Secp256k1TripleBenchmark { kotlinOp = { Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privKey)) }, cOp = if (cAvailable) { - { Secp256k1InstanceC.compressedPubKeyFor(privKey) } + { Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!) } } else { null }, @@ -236,7 +235,7 @@ class Secp256k1TripleBenchmark { kotlinOp = { Secp256k1.ecdhXOnly(pub2xOnly, privKey) }, cOp = if (cAvailable) { - { Secp256k1InstanceC.ecdhXOnly(pub2xOnly, privKey) } + { Schnorr256k1.ecdhXOnly(pub2xOnly, privKey) } } else { null }, @@ -298,9 +297,9 @@ class Secp256k1TripleBenchmark { // Time C batch (if available) var cBatchEvSec = 0L if (cAvailable) { - repeat(500) { Secp256k1InstanceC.verifySchnorrBatch(batchPub, sigs, msgs) } + repeat(500) { Schnorr256k1.schnorrVerifyBatch(batchPub, sigs, msgs) } val cBatchStart = System.nanoTime() - repeat(iters) { Secp256k1InstanceC.verifySchnorrBatch(batchPub, sigs, msgs) } + repeat(iters) { Schnorr256k1.schnorrVerifyBatch(batchPub, sigs, msgs) } val cBatchNs = System.nanoTime() - cBatchStart cBatchEvSec = iters.toLong() * batchSize * 1_000_000_000L / cBatchNs } diff --git a/quartz/src/main/c/CMakeLists.txt b/quartz/src/main/c/CMakeLists.txt deleted file mode 100644 index eb3fcd234..000000000 --- a/quartz/src/main/c/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -# 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 deleted file mode 100644 index a4f0555da..000000000 --- a/quartz/src/main/c/secp256k1/CMakeLists.txt +++ /dev/null @@ -1,82 +0,0 @@ -cmake_minimum_required(VERSION 3.18) -project(secp256k1_amethyst C) - -set(CMAKE_C_STANDARD 11) - -# ==================== Link-Time Optimization ==================== -# -# LTO lets the linker inline fe_mul / fe_sqr across translation units -# (field.c ↔ point.c ↔ scalar.c ↔ schnorr.c), recovering ~5-15% on the -# hot Jacobian formulas where fe_mul would otherwise be a call boundary. -# Disabled automatically when the toolchain doesn't support it. - -include(CheckIPOSupported) -check_ipo_supported(RESULT LTO_SUPPORTED OUTPUT LTO_ERROR) -if(LTO_SUPPORTED) - message(STATUS "LTO supported - enabling interprocedural optimization") - set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) -else() - message(STATUS "LTO not supported: ${LTO_ERROR}") -endif() - -# ==================== 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 -mbmi2 -msha -msse4.1 -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(ANDROID) - # Android NDK: JNI headers are always available, build shared lib with JNI bridge - target_sources(secp256k1_amethyst_jni PRIVATE jni_bridge.c) - find_library(log-lib log) - if(log-lib) - target_link_libraries(secp256k1_amethyst_jni ${log-lib}) - endif() -elseif(JNI_INCLUDE_DIR) - # Desktop JVM: JNI headers provided externally - 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() - -# ==================== 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/bench_vs_acinq.c b/quartz/src/main/c/secp256k1/bench_vs_acinq.c deleted file mode 100644 index faef2313e..000000000 --- a/quartz/src/main/c/secp256k1/bench_vs_acinq.c +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Native C-to-C benchmark: Our secp256k1 vs ACINQ's libsecp256k1 - * No JNI, no JVM — pure native comparison on the same machine. - * - * Build: - * cd quartz/src/main/c/secp256k1/build - * gcc -O2 -march=x86-64-v2 -mbmi2 -I.. bench_vs_acinq.c \ - * -L. -lsecp256k1_amethyst \ - * -L/tmp/acinq_secp/fr/acinq/secp256k1/jni/native/linux-x86_64 \ - * -l:libsecp256k1-jni.so \ - * -Wl,-rpath,/tmp/acinq_secp/fr/acinq/secp256k1/jni/native/linux-x86_64 \ - * -o bench_vs_acinq -lm - */ -#include "secp256k1_c.h" -#include "sha256.h" -#include -#include -#include -#include - -/* ==================== ACINQ libsecp256k1 API declarations ==================== */ - -typedef struct secp256k1_context_struct secp256k1_context; -typedef struct { unsigned char data[64]; } secp256k1_pubkey; -typedef struct { unsigned char data[64]; } secp256k1_xonly_pubkey; -typedef struct { unsigned char data[96]; } secp256k1_keypair; - -#define SECP256K1_CONTEXT_NONE 0 -#define SECP256K1_CONTEXT_SIGN 0x201 -#define SECP256K1_CONTEXT_VERIFY 0x101 - -extern secp256k1_context *secp256k1_context_create(unsigned int flags); -extern void secp256k1_context_destroy(secp256k1_context *ctx); -extern int secp256k1_ec_pubkey_create(const secp256k1_context *ctx, - secp256k1_pubkey *pubkey, const unsigned char *seckey); -extern int secp256k1_ec_pubkey_serialize(const secp256k1_context *ctx, - unsigned char *output, size_t *outputlen, const secp256k1_pubkey *pubkey, - unsigned int flags); -extern int secp256k1_keypair_create(const secp256k1_context *ctx, - secp256k1_keypair *keypair, const unsigned char *seckey); -extern int secp256k1_keypair_xonly_pub(const secp256k1_context *ctx, - secp256k1_xonly_pubkey *pubkey, int *pk_parity, const secp256k1_keypair *keypair); -extern int secp256k1_xonly_pubkey_serialize(const secp256k1_context *ctx, - unsigned char *output32, const secp256k1_xonly_pubkey *pubkey); -extern int secp256k1_xonly_pubkey_parse(const secp256k1_context *ctx, - secp256k1_xonly_pubkey *pubkey, const unsigned char *input32); -extern int secp256k1_schnorrsig_sign32(const secp256k1_context *ctx, - unsigned char *sig64, const unsigned char *msg32, - const secp256k1_keypair *keypair, const unsigned char *aux_rand32); -extern int secp256k1_schnorrsig_verify(const secp256k1_context *ctx, - const unsigned char *sig64, const unsigned char *msg, - size_t msglen, const secp256k1_xonly_pubkey *pubkey); -extern int secp256k1_ec_pubkey_tweak_mul(const secp256k1_context *ctx, - secp256k1_pubkey *pubkey, const unsigned char *tweak32); -extern int secp256k1_ec_pubkey_parse(const secp256k1_context *ctx, - secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen); - -#define SECP256K1_EC_COMPRESSED 258 - -/* ==================== Timing ==================== */ - -static double now_us(void) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return ts.tv_sec * 1e6 + ts.tv_nsec / 1e3; -} - -/* ==================== Test data ==================== */ - -static const uint8_t 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 -}; - -int main(void) { - printf("================================================================\n"); - printf(" Native C-to-C Benchmark: Ours vs ACINQ libsecp256k1\n"); - printf(" No JNI, no JVM — pure native performance comparison\n"); - printf("================================================================\n\n"); - - /* ===== Init ===== */ - secp256k1c_init(); - secp256k1_context *acinq_ctx = secp256k1_context_create( - SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); - - /* ===== Setup: create keypairs and signatures for both ===== */ - - /* ACINQ setup */ - secp256k1_keypair acinq_kp; - secp256k1_keypair_create(acinq_ctx, &acinq_kp, PRIVKEY); - secp256k1_xonly_pubkey acinq_xonly; - secp256k1_keypair_xonly_pub(acinq_ctx, &acinq_xonly, NULL, &acinq_kp); - uint8_t acinq_xonly_bytes[32]; - secp256k1_xonly_pubkey_serialize(acinq_ctx, acinq_xonly_bytes, &acinq_xonly); - - /* Our setup */ - uint8_t our_pub65[65]; - secp256k1c_pubkey_create(our_pub65, PRIVKEY); - uint8_t our_xonly[32]; - memcpy(our_xonly, our_pub65 + 1, 32); - - /* Messages */ - uint8_t msg[32]; - secp256k1_sha256_hash(msg, (const uint8_t *)"benchmark msg", 13); - - /* Sign with both */ - uint8_t acinq_sig[64], our_sig[64]; - secp256k1_schnorrsig_sign32(acinq_ctx, acinq_sig, msg, &acinq_kp, NULL); - secp256k1c_schnorr_sign(our_sig, msg, 32, PRIVKEY, NULL); - - /* Cross-verify: make sure both produce valid signatures */ - int acinq_verify_ours = secp256k1_schnorrsig_verify(acinq_ctx, our_sig, msg, 32, &acinq_xonly); - int our_verify_acinq = secp256k1c_schnorr_verify_fast(acinq_sig, msg, 32, our_xonly); - printf("Cross-verification: ACINQ verifies ours=%d, We verify ACINQ=%d\n\n", - acinq_verify_ours, our_verify_acinq); - - if (!acinq_verify_ours || !our_verify_acinq) { - printf("ERROR: Cross-verification failed!\n"); - /* Continue anyway to get timing data */ - } - - /* ===== Benchmarks ===== */ - - int N; - double t0, t1; - - printf("%-30s %12s %12s %10s\n", "Operation", "ACINQ (µs)", "Ours (µs)", "Speedup"); - printf("─────────────────────────────────────────────────────────────────\n"); - - /* --- pubkeyCreate --- */ - N = 5000; - t0 = now_us(); - for (int i = 0; i < N; i++) { - secp256k1_pubkey pk; - secp256k1_ec_pubkey_create(acinq_ctx, &pk, PRIVKEY); - } - double acinq_pubkey = (now_us() - t0) / N; - - t0 = now_us(); - for (int i = 0; i < N; i++) { - uint8_t p65[65]; - secp256k1c_pubkey_create(p65, PRIVKEY); - } - double our_pubkey = (now_us() - t0) / N; - printf("%-30s %10.1f %10.1f %8.2fx\n", "pubkeyCreate", acinq_pubkey, our_pubkey, acinq_pubkey/our_pubkey); - - /* --- signSchnorr (FAIR: both derive pubkey each call) --- */ - N = 5000; - t0 = now_us(); - for (int i = 0; i < N; i++) { - uint8_t s[64]; - secp256k1_keypair kp_fresh; - secp256k1_keypair_create(acinq_ctx, &kp_fresh, PRIVKEY); - secp256k1_schnorrsig_sign32(acinq_ctx, s, msg, &kp_fresh, NULL); - } - double acinq_sign = (now_us() - t0) / N; - - t0 = now_us(); - for (int i = 0; i < N; i++) { - uint8_t s[64]; - secp256k1c_schnorr_sign(s, msg, 32, PRIVKEY, NULL); - } - double our_sign = (now_us() - t0) / N; - printf("%-30s %10.1f %10.1f %8.2fx\n", "sign (full, derive pubkey)", acinq_sign, our_sign, acinq_sign/our_sign); - - /* --- signSchnorr (CACHED: both reuse precomputed pubkey) --- */ - t0 = now_us(); - for (int i = 0; i < N; i++) { - uint8_t s[64]; - secp256k1_schnorrsig_sign32(acinq_ctx, s, msg, &acinq_kp, NULL); - } - double acinq_sign_cached = (now_us() - t0) / N; - - t0 = now_us(); - for (int i = 0; i < N; i++) { - uint8_t s[64]; - secp256k1c_schnorr_sign_xonly(s, msg, 32, PRIVKEY, our_xonly, NULL); - } - double our_sign_cached = (now_us() - t0) / N; - printf("%-30s %10.1f %10.1f %8.2fx\n", "sign (cached pubkey)", acinq_sign_cached, our_sign_cached, acinq_sign_cached/our_sign_cached); - - /* --- verifySchnorr (ACINQ = always full BIP-340) --- */ - N = 5000; - /* Warmup */ - for (int i = 0; i < 1000; i++) { - secp256k1_schnorrsig_verify(acinq_ctx, acinq_sig, msg, 32, &acinq_xonly); - secp256k1c_schnorr_verify_fast(our_sig, msg, 32, our_xonly); - } - - t0 = now_us(); - for (int i = 0; i < N; i++) { - secp256k1_schnorrsig_verify(acinq_ctx, acinq_sig, msg, 32, &acinq_xonly); - } - double acinq_verify = (now_us() - t0) / N; - - t0 = now_us(); - for (int i = 0; i < N; i++) { - secp256k1c_schnorr_verify_fast(our_sig, msg, 32, our_xonly); - } - double our_verify_fast = (now_us() - t0) / N; - - t0 = now_us(); - for (int i = 0; i < N; i++) { - secp256k1c_schnorr_verify(our_sig, msg, 32, our_xonly); - } - double our_verify_full = (now_us() - t0) / N; - printf("%-30s %10.1f %10.1f %8.2fx\n", "verify (ACINQ=BIP340)", acinq_verify, our_verify_full, acinq_verify/our_verify_full); - printf("%-30s %10.1f %10.1f %8.2fx\n", "verifyFast (Nostr safe)", acinq_verify, our_verify_fast, acinq_verify/our_verify_fast); - - /* --- ECDH (pubKeyTweakMul) --- */ - N = 3000; - /* ACINQ: parse pubkey, tweak_mul, serialize */ - uint8_t compressed_pub[33]; - compressed_pub[0] = 0x02; - memcpy(compressed_pub + 1, our_xonly, 32); - - t0 = now_us(); - for (int i = 0; i < N; i++) { - secp256k1_pubkey pk; - secp256k1_ec_pubkey_parse(acinq_ctx, &pk, compressed_pub, 33); - secp256k1_ec_pubkey_tweak_mul(acinq_ctx, &pk, PRIVKEY); - uint8_t out[33]; size_t outlen = 33; - secp256k1_ec_pubkey_serialize(acinq_ctx, out, &outlen, &pk, SECP256K1_EC_COMPRESSED); - } - double acinq_ecdh = (now_us() - t0) / N; - - t0 = now_us(); - for (int i = 0; i < N; i++) { - uint8_t result[32]; - secp256k1c_ecdh_xonly(result, our_xonly, PRIVKEY); - } - double our_ecdh = (now_us() - t0) / N; - printf("%-30s %10.1f %10.1f %8.2fx\n", "ECDH (cached pubkey)", acinq_ecdh, our_ecdh, acinq_ecdh/our_ecdh); - - /* --- Batch verify (ours only — ACINQ has no batch API) --- */ - printf("\n%-30s %12s %12s %10s\n", "Batch Verify", "ACINQ indiv", "Ours batch", "Speedup"); - printf("─────────────────────────────────────────────────────────────────\n"); - - for (int batch_size = 4; batch_size <= 200; batch_size = (batch_size < 64) ? batch_size * 2 : 200) { - uint8_t **sigs = malloc(batch_size * sizeof(uint8_t*)); - uint8_t **msgs_arr = malloc(batch_size * sizeof(uint8_t*)); - size_t *lens = malloc(batch_size * sizeof(size_t)); - for (int i = 0; i < batch_size; i++) { - sigs[i] = malloc(64); - msgs_arr[i] = malloc(32); - lens[i] = 32; - uint8_t seed[4] = {(uint8_t)i, (uint8_t)(i>>8), 0, 0}; - secp256k1_sha256_hash(msgs_arr[i], seed, 4); - secp256k1c_schnorr_sign(sigs[i], msgs_arr[i], 32, PRIVKEY, NULL); - } - - /* Parse pubkey for ACINQ individual verify */ - secp256k1_xonly_pubkey acinq_xpk; - secp256k1_xonly_pubkey_parse(acinq_ctx, &acinq_xpk, our_xonly); - - int iters = (batch_size <= 16) ? 1000 : (batch_size <= 64) ? 300 : 100; - - /* ACINQ individual */ - t0 = now_us(); - for (int it = 0; it < iters; it++) { - for (int i = 0; i < batch_size; i++) { - secp256k1_schnorrsig_verify(acinq_ctx, sigs[i], msgs_arr[i], 32, &acinq_xpk); - } - } - double acinq_indiv = (now_us() - t0) / (iters * batch_size); - - /* Ours batch */ - t0 = now_us(); - for (int it = 0; it < iters; it++) { - secp256k1c_schnorr_verify_batch(our_xonly, - (const uint8_t *const *)sigs, (const uint8_t *const *)msgs_arr, lens, batch_size); - } - double our_batch = (now_us() - t0) / (iters * batch_size); - - printf(" batch(%3d) %10.1f %10.1f %8.1fx\n", - batch_size, acinq_indiv, our_batch, acinq_indiv / our_batch); - - for (int i = 0; i < batch_size; i++) { free(sigs[i]); free(msgs_arr[i]); } - free(sigs); free(msgs_arr); free(lens); - if (batch_size == 200) break; - if (batch_size == 64) batch_size = 100; /* next iteration will be 200 */ - } - - printf("\n================================================================\n"); - secp256k1_context_destroy(acinq_ctx); - return 0; -} diff --git a/quartz/src/main/c/secp256k1/benchmark.c b/quartz/src/main/c/secp256k1/benchmark.c deleted file mode 100644 index f834e1809..000000000 --- a/quartz/src/main/c/secp256k1/benchmark.c +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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(sig, msg, 32, TEST_PRIVKEY, 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(sigs[i], msgs[i], 32, TEST_PRIVKEY, TEST_AUXRAND); - } - - const char *name = - batch_size == 4 ? "verifyBatch(4)" : - batch_size == 8 ? "verifyBatch(8)" : - batch_size == 16 ? "verifyBatch(16)" : - batch_size == 32 ? "verifyBatch(32)" : - batch_size == 64 ? "verifyBatch(64)" : - batch_size == 200 ? "verifyBatch(200)" : "verifyBatch(?)"; - - 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(4, N / 2); - bench_verify_batch(8, N / 4); - bench_verify_batch(16, N / 8); - bench_verify_batch(32, N / 16); - bench_verify_batch(64, N / 32); - 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/build_android.sh b/quartz/src/main/c/secp256k1/build_android.sh deleted file mode 100755 index bee92b073..000000000 --- a/quartz/src/main/c/secp256k1/build_android.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -# -# Cross-compile secp256k1 C library for Android ARM64 and package -# into the benchmark APK's jniLibs directory. -# -# Run from your development machine (macOS or Linux) with Android NDK installed. -# -# Usage: -# cd quartz/src/main/c/secp256k1 -# ./build_android.sh -# # Then run the benchmark: -# cd ../../../../.. -# ./gradlew :benchmark:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.vitorpamplona.quartz.benchmark.Secp256k1CBenchmark -# - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$SCRIPT_DIR/../../../../.." -JNILIB_DIR="$PROJECT_ROOT/benchmark/src/main/jniLibs" - -# Find NDK -NDK="${ANDROID_NDK_HOME:-${ANDROID_HOME:-$HOME/Library/Android/sdk}/ndk/$(ls ${ANDROID_HOME:-$HOME/Library/Android/sdk}/ndk/ 2>/dev/null | sort -V | tail -1)}" -if [ ! -d "$NDK" ]; then - echo "ERROR: Android NDK not found." - echo "Set ANDROID_NDK_HOME or install NDK via: sdkmanager 'ndk;27.2.12479018'" - exit 1 -fi - -# Find clang for ARM64 -HOST_TAG="linux-x86_64" -if [[ "$(uname)" == "Darwin" ]]; then HOST_TAG="darwin-x86_64"; fi -CC="$NDK/toolchains/llvm/prebuilt/$HOST_TAG/bin/aarch64-linux-android26-clang" -CC_X86="$NDK/toolchains/llvm/prebuilt/$HOST_TAG/bin/x86_64-linux-android26-clang" - -if [ ! -f "$CC" ]; then - echo "ERROR: ARM64 clang not found at: $CC" - echo "NDK path: $NDK" - exit 1 -fi - -echo "NDK: $NDK" -echo "Output: $JNILIB_DIR" -echo "" - -SOURCES="field.c scalar.c point.c schnorr.c sha256.c jni_bridge.c" - -# Build ARM64 shared library -echo "Building arm64-v8a..." -mkdir -p "$JNILIB_DIR/arm64-v8a" -$CC -O3 -march=armv8-a+crypto -fomit-frame-pointer \ - -shared -fPIC \ - -I"$SCRIPT_DIR" \ - $(cd "$SCRIPT_DIR" && echo $SOURCES) \ - -o "$JNILIB_DIR/arm64-v8a/libsecp256k1_amethyst_jni.so" \ - -lm -echo " → $(wc -c < "$JNILIB_DIR/arm64-v8a/libsecp256k1_amethyst_jni.so") bytes" - -# Build x86_64 shared library (for emulator) -if [ -f "$CC_X86" ]; then - echo "Building x86_64..." - mkdir -p "$JNILIB_DIR/x86_64" - $CC_X86 -O3 -march=x86-64-v2 -mbmi2 -msha -msse4.1 -fomit-frame-pointer \ - -shared -fPIC \ - -I"$SCRIPT_DIR" \ - $(cd "$SCRIPT_DIR" && echo $SOURCES) \ - -o "$JNILIB_DIR/x86_64/libsecp256k1_amethyst_jni.so" \ - -lm - echo " → $(wc -c < "$JNILIB_DIR/x86_64/libsecp256k1_amethyst_jni.so") bytes" -fi - -echo "" -echo "Done! Native libraries placed in:" -echo " $JNILIB_DIR/" -echo "" -echo "Now run the benchmark:" -echo " ./gradlew :benchmark:connectedAndroidTest \\" -echo " -Pandroid.testInstrumentationRunnerArguments.class=com.vitorpamplona.quartz.benchmark.Secp256k1CBenchmark" diff --git a/quartz/src/main/c/secp256k1/field.c b/quartz/src/main/c/secp256k1/field.c deleted file mode 100644 index 14ba30c11..000000000 --- a/quartz/src/main/c/secp256k1/field.c +++ /dev/null @@ -1,411 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Field arithmetic modulo p = 2^256 - 2^32 - 977 using 4x64-bit limbs. - * - * Same representation as the Kotlin Fe4 class. Performance analysis showed - * 4x64 is faster than 5x52 because fewer multiplies (16 vs 25) outweighs - * the lazy reduction advantage of 5x52 on both JVM and native. - */ -#include "field.h" -#include "field_asm.h" -#include - -#define FIELD_C 0x1000003D1ULL - -/* ==================== 8-limb product computation ==================== */ - -/* - * Compute 512-bit product of two 256-bit numbers in 4x64 representation. - * Output: 8 limbs in little-endian. Uses __int128 for 64x64->128 products. - * - * The key insight: we CAN'T accumulate multiple 128-bit products in a single - * uint128 because 4 products overflow (4 * 2^128 > 2^128). Instead, we compute - * each column separately and propagate carries explicitly. - */ -#if HAVE_INT128 - -void mul_wide(uint64_t out[8], const uint64_t a[4], const uint64_t b[4]) { - /* - * Schoolbook 4x4 multiplication into 8 limbs. Uses a row-based approach: - * multiply each a[i] by the full b[0..3] vector and accumulate into out. - * This avoids the column-based carry overflow problem. - */ - uint128_t acc; - - /* Row 0: out += a[0] * b */ - acc = (uint128_t)a[0] * b[0]; - out[0] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a[0] * b[1]; - out[1] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a[0] * b[2]; - out[2] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a[0] * b[3]; - out[3] = (uint64_t)acc; - out[4] = (uint64_t)(acc >> 64); - out[5] = out[6] = out[7] = 0; - - /* Row 1: out[1..5] += a[1] * b */ - acc = (uint128_t)out[1] + (uint128_t)a[1] * b[0]; - out[1] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[2] + (uint128_t)a[1] * b[1]; - out[2] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[3] + (uint128_t)a[1] * b[2]; - out[3] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[4] + (uint128_t)a[1] * b[3]; - out[4] = (uint64_t)acc; - out[5] = (uint64_t)(acc >> 64); - - /* Row 2: out[2..6] += a[2] * b */ - acc = (uint128_t)out[2] + (uint128_t)a[2] * b[0]; - out[2] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[3] + (uint128_t)a[2] * b[1]; - out[3] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[4] + (uint128_t)a[2] * b[2]; - out[4] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[5] + (uint128_t)a[2] * b[3]; - out[5] = (uint64_t)acc; - out[6] = (uint64_t)(acc >> 64); - - /* Row 3: out[3..7] += a[3] * b */ - acc = (uint128_t)out[3] + (uint128_t)a[3] * b[0]; - out[3] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[4] + (uint128_t)a[3] * b[1]; - out[4] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[5] + (uint128_t)a[3] * b[2]; - out[5] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)out[6] + (uint128_t)a[3] * b[3]; - out[6] = (uint64_t)acc; - out[7] = (uint64_t)(acc >> 64); -} - -/* - * Reduce a 512-bit value (8 limbs) modulo p. - * Uses: 2^256 ≡ C (mod p) where C = 0x1000003D1. - * Two reduction rounds: first folds hi[0..3] into lo[0..3] using C, - * second handles any remaining overflow. - */ -void reduce_wide(secp256k1_fe *r, const uint64_t w[8]) { - uint128_t acc; - - /* Round 1: result = w[0..3] + w[4..7] * C */ - acc = (uint128_t)w[0] + (uint128_t)w[4] * FIELD_C; - r->d[0] = (uint64_t)acc; - acc >>= 64; - - acc += (uint128_t)w[1] + (uint128_t)w[5] * FIELD_C; - r->d[1] = (uint64_t)acc; - acc >>= 64; - - acc += (uint128_t)w[2] + (uint128_t)w[6] * FIELD_C; - r->d[2] = (uint64_t)acc; - acc >>= 64; - - acc += (uint128_t)w[3] + (uint128_t)w[7] * FIELD_C; - r->d[3] = (uint64_t)acc; - uint64_t carry = (uint64_t)(acc >> 64); - - /* Round 2+: fold remaining carry through C. Looped for correctness - * against any reachable input (see fe_mul for the detailed bound). */ - while (carry) { - acc = (uint128_t)r->d[0] + (uint128_t)carry * FIELD_C; - r->d[0] = (uint64_t)acc; - uint64_t c1 = (uint64_t)(acc >> 64); - if (c1) { - uint64_t s = r->d[1] + c1; - uint64_t cc = (s < c1) ? 1 : 0; - r->d[1] = s; - if (cc) { - s = r->d[2] + 1; - cc = (s == 0) ? 1 : 0; - r->d[2] = s; - if (cc) { - s = r->d[3] + 1; - r->d[3] = s; - carry = (s == 0) ? 1 : 0; - continue; - } - } - } - carry = 0; - } - - /* No fe_normalize — lazy. Output is in [0, 2^256), possibly in [P, P+C). - * This is safe: mul/add/sub all handle unreduced inputs. - * Only neg/half/isZero/cmp/toBytes need explicit normalize. */ -} - -#if FE_MUL_ASM -/* fe_mul and fe_sqr are static inline in field.h when ASM is available. - * They get inlined directly into gej_double/gej_add_ge callers, - * eliminating ~9 function call boundaries per doublePoint. */ -#else -void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { -#if HAVE_INT128 - /* Inline mul + reduce to avoid function call overhead and enable - * the compiler to keep intermediates in registers. */ - uint64_t a0=a->d[0], a1=a->d[1], a2=a->d[2], a3=a->d[3]; - uint64_t b0=b->d[0], b1=b->d[1], b2=b->d[2], b3=b->d[3]; - uint128_t acc; - uint64_t lo0, lo1, lo2, lo3, hi0, hi1, hi2, hi3; - - /* 4x4 schoolbook product (row-based, no overflow) */ - acc = (uint128_t)a0*b0; - lo0 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a0*b1; - lo1 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a0*b2; - lo2 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a0*b3; - lo3 = (uint64_t)acc; hi0 = (uint64_t)(acc>>64); - - acc = (uint128_t)lo1 + (uint128_t)a1*b0; - lo1 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)lo2 + (uint128_t)a1*b1; - lo2 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)lo3 + (uint128_t)a1*b2; - lo3 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)hi0 + (uint128_t)a1*b3; - hi0 = (uint64_t)acc; hi1 = (uint64_t)(acc>>64); - - acc = (uint128_t)lo2 + (uint128_t)a2*b0; - lo2 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)lo3 + (uint128_t)a2*b1; - lo3 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)hi0 + (uint128_t)a2*b2; - hi0 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)hi1 + (uint128_t)a2*b3; - hi1 = (uint64_t)acc; hi2 = (uint64_t)(acc>>64); - - acc = (uint128_t)lo3 + (uint128_t)a3*b0; - lo3 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)hi0 + (uint128_t)a3*b1; - hi0 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)hi1 + (uint128_t)a3*b2; - hi1 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)hi2 + (uint128_t)a3*b3; - hi2 = (uint64_t)acc; hi3 = (uint64_t)(acc>>64); - - /* Reduce: lo + hi * C */ - acc = (uint128_t)lo0 + (uint128_t)hi0 * FIELD_C; - r->d[0] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)lo1 + (uint128_t)hi1 * FIELD_C; - r->d[1] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)lo2 + (uint128_t)hi2 * FIELD_C; - r->d[2] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)lo3 + (uint128_t)hi3 * FIELD_C; - r->d[3] = (uint64_t)acc; - uint64_t carry = (uint64_t)(acc >> 64); - /* Final fold: carry * C back into the low limbs. In theory two rounds - * suffice because carry ≤ ~2^34 on the first pass and carry*C < 2^67 - * produces at most a 3-bit secondary carry. The loop is cheap and makes - * the invariant "output < 2^256" hold for any reachable inputs — not - * only from fe_mul itself but also from chained lazy fe_add results. */ - while (carry) { - acc = (uint128_t)r->d[0] + (uint128_t)carry * FIELD_C; - r->d[0] = (uint64_t)acc; - uint64_t c1 = (uint64_t)(acc >> 64); - if (c1) { - uint64_t s = r->d[1] + c1; - uint64_t cc = (s < c1) ? 1 : 0; - r->d[1] = s; - if (cc) { - s = r->d[2] + 1; - cc = (s == 0) ? 1 : 0; - r->d[2] = s; - if (cc) { - s = r->d[3] + 1; - r->d[3] = s; - carry = (s == 0) ? 1 : 0; - continue; - } - } - } - carry = 0; - } - /* No fe_normalize — lazy. Output is in [0, 2^256), possibly in [P, P+C). - * This is safe: mul/add/sub all handle unreduced inputs. - * Only neg/half/isZero/cmp/toBytes need explicit normalize. */ -#else - uint64_t w[8]; - mul_wide(w, a->d, b->d); - reduce_wide(r, w); -#endif -} - -/* fe_sqr is now defined as static inline in field.h (10-mul dedicated - * squaring using __int128). This routes through fe_sqr_inline to save 6 - * multiplications per squaring compared to fe_mul(a, a). */ -#endif /* !FE_MUL_ASM */ - -#else /* Portable fallback (no HAVE_INT128) */ - -static inline void mul64(uint64_t *hi, uint64_t *lo, uint64_t a, uint64_t b) { - uint64_t a_lo = a & 0xFFFFFFFF, a_hi = a >> 32; - uint64_t b_lo = b & 0xFFFFFFFF, b_hi = b >> 32; - uint64_t ll = a_lo * b_lo, lh = a_lo * b_hi, hl = a_hi * b_lo, 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) { - uint64_t w[8] = {0}; - for (int i = 0; i < 4; i++) { - uint64_t carry = 0; - for (int j = 0; j < 4; j++) { - uint64_t hi, lo; - mul64(&hi, &lo, a->d[i], b->d[j]); - uint64_t sum = w[i+j] + lo + carry; - carry = hi + (sum < w[i+j] ? 1 : 0); - w[i+j] = sum; - } - w[i+4] += carry; - } - /* Reduce: w[0..3] + w[4..7] * C */ - uint64_t c_lo, c_hi; - uint64_t carry2 = 0; - for (int i = 0; i < 4; i++) { - mul64(&c_hi, &c_lo, w[i+4], FIELD_C); - uint64_t sum = w[i] + c_lo + carry2; - carry2 = c_hi + (sum < w[i] ? 1 : 0); - r->d[i] = sum; - } - while (carry2) { - mul64(&c_hi, &c_lo, carry2, FIELD_C); - uint64_t sum = r->d[0] + c_lo; - uint64_t new_carry = (sum < c_lo) ? 1 : 0; - r->d[0] = sum; - uint64_t s1 = r->d[1] + c_hi + new_carry; - uint64_t nc1 = (s1 < c_hi) || (new_carry && s1 == c_hi) ? 1 : 0; - r->d[1] = s1; - if (nc1) { - uint64_t s2 = r->d[2] + 1; - uint64_t nc2 = (s2 == 0) ? 1 : 0; - r->d[2] = s2; - if (nc2) { - uint64_t s3 = r->d[3] + 1; - r->d[3] = s3; - carry2 = (s3 == 0) ? 1 : 0; - continue; - } - } - carry2 = 0; - } - /* No fe_normalize — lazy. Output is in [0, 2^256), possibly in [P, P+C). - * This is safe: mul/add/sub all handle unreduced inputs. - * Only neg/half/isZero/cmp/toBytes need explicit normalize. */ -} - -/* Portable fallback squaring: use fe_mul(a, a) because the 10-mul inline path - * requires __int128 and this branch is for compilers without it. */ -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, 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); - 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); - fe_sqr(&check, r); - return fe_equal(&check, a); -} - -/* ==================== Half ==================== */ - -void fe_half(secp256k1_fe *r, const secp256k1_fe *a) { - /* Branchless: mask = all-1s if odd, all-0s if even. - * Conditionally add P before shifting. Avoids normalization. */ - uint64_t mask = -(a->d[0] & 1); /* 0xFFF...F if odd, 0 if even */ - uint64_t p0 = 0xFFFFFFFEFFFFFC2FULL & mask; - /* P[1..3] = 0xFFFF...FFFF, so P[i] & mask = mask */ - - uint64_t s0 = a->d[0] + p0; - uint64_t c0 = (s0 < a->d[0]) ? 1ULL : 0ULL; - uint64_t s1 = a->d[1] + mask + c0; - uint64_t c1 = (s1 < a->d[1]) || (c0 && s1 == a->d[1]) ? 1ULL : 0ULL; - uint64_t s2 = a->d[2] + mask + c1; - uint64_t c2 = (s2 < a->d[2]) || (c1 && s2 == a->d[2]) ? 1ULL : 0ULL; - uint64_t s3 = a->d[3] + mask + c2; - uint64_t c3 = (s3 < a->d[3]) || (c2 && s3 == a->d[3]) ? 1ULL : 0ULL; - - r->d[0] = (s0 >> 1) | (s1 << 63); - r->d[1] = (s1 >> 1) | (s2 << 63); - r->d[2] = (s2 >> 1) | (s3 << 63); - r->d[3] = (s3 >> 1) | (c3 << 63); -} - -/* ==================== Serialization ==================== */ - -void fe_to_bytes(uint8_t *out32, const secp256k1_fe *a) { - secp256k1_fe t = *a; - fe_normalize_full(&t); - for (int i = 0; i < 4; i++) { - uint64_t v = t.d[3 - i]; - for (int j = 0; j < 8; j++) - out32[i * 8 + j] = (uint8_t)(v >> ((7 - j) * 8)); - } -} - -int fe_from_bytes(secp256k1_fe *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; - } - return 1; -} - -int fe_cmp(const secp256k1_fe *a, const secp256k1_fe *b) { - /* Compare raw limb values without normalization. - * fe_normalize reduces values in [P, 2^256) to [0, 2^32+977), - * which would turn P itself into 0 and break comparisons against P. */ - 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; -} diff --git a/quartz/src/main/c/secp256k1/field.h b/quartz/src/main/c/secp256k1/field.h deleted file mode 100644 index bb0300749..000000000 --- a/quartz/src/main/c/secp256k1/field.h +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Field arithmetic modulo p = 2^256 - 2^32 - 977 using 4x64-bit limbs. - * - * Same representation as the Kotlin implementation (Fe4): 4 fully-packed - * 64-bit limbs in little-endian order. This was chosen over the 5x52-bit - * representation because benchmark testing showed fewer multiplies (16 vs 25) - * outweighs the lazy reduction advantage of 5x52 on both JVM and native. - */ -#ifndef SECP256K1_FIELD_H -#define SECP256K1_FIELD_H - -#include "secp256k1_c.h" - -/* ==================== Field Element (4x64-bit limbs, little-endian) ==================== */ - -/* p = 2^256 - 2^32 - 977 in 4x64 little-endian */ -static const secp256k1_fe FE_P = {{ - 0xFFFFFFFEFFFFFC2FULL, - 0xFFFFFFFFFFFFFFFFULL, - 0xFFFFFFFFFFFFFFFFULL, - 0xFFFFFFFFFFFFFFFFULL -}}; - -static const secp256k1_fe FE_ZERO = {{0, 0, 0, 0}}; -static const secp256k1_fe FE_ONE = {{1, 0, 0, 0}}; - -/* P[0] cached for hot path */ -#define FE_P0 0xFFFFFFFEFFFFFC2FULL - -/* ==================== Inline Helpers ==================== */ - -static inline int fe_is_zero(const secp256k1_fe *a) { - /* Normalize before checking — elements may be unreduced */ - secp256k1_fe t = *a; - /* Quick check: if all limbs are in range and < p, it's normalized */ - if (t.d[3] == UINT64_MAX && t.d[2] == UINT64_MAX && - t.d[1] == UINT64_MAX && t.d[0] >= FE_P0) { - /* >= p, reduce */ - t.d[0] -= FE_P0; t.d[1] = 0; t.d[2] = 0; t.d[3] = 0; - } - return (t.d[0] | t.d[1] | t.d[2] | t.d[3]) == 0; -} - -static inline int fe_equal(const secp256k1_fe *a, const secp256k1_fe *b) { - secp256k1_fe ta = *a, tb = *b; - /* Normalize both */ - if (ta.d[3] == UINT64_MAX && ta.d[2] == UINT64_MAX && - ta.d[1] == UINT64_MAX && ta.d[0] >= FE_P0) { - ta.d[0] -= FE_P0; ta.d[1] = 0; ta.d[2] = 0; ta.d[3] = 0; - } - if (tb.d[3] == UINT64_MAX && tb.d[2] == UINT64_MAX && - tb.d[1] == UINT64_MAX && tb.d[0] >= FE_P0) { - tb.d[0] -= FE_P0; tb.d[1] = 0; tb.d[2] = 0; tb.d[3] = 0; - } - 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]); -} - -static inline int fe_is_odd(const secp256k1_fe *a) { - secp256k1_fe t = *a; - if (t.d[3] == UINT64_MAX && t.d[2] == UINT64_MAX && - t.d[1] == UINT64_MAX && t.d[0] >= FE_P0) { - t.d[0] -= FE_P0; t.d[1] = 0; t.d[2] = 0; t.d[3] = 0; - } - return (int)(t.d[0] & 1); -} - -/* Normalize: if a >= p, subtract p. - * On ARM64: branchless (CSEL avoids misprediction on mobile SoCs). - * On x86_64: branching (>99.99% correct prediction, branch is cheaper). */ -static inline void fe_normalize(secp256k1_fe *a) { -#if SECP_ARM64 - /* Branchless for ARM64: compute mask, conditionally subtract */ - uint64_t ge = (a->d[3] == UINT64_MAX) & (a->d[2] == UINT64_MAX) & - (a->d[1] == UINT64_MAX) & (a->d[0] >= FE_P0); - uint64_t mask = -(uint64_t)ge; - a->d[0] -= FE_P0 & mask; - a->d[1] &= ~mask; - a->d[2] &= ~mask; - a->d[3] &= ~mask; -#else - /* Branching for x86_64: branch predictor handles the >99.99% case */ - if (a->d[3] == UINT64_MAX && a->d[2] == UINT64_MAX && - a->d[1] == UINT64_MAX && a->d[0] >= FE_P0) { - a->d[0] -= FE_P0; - a->d[1] = 0; - a->d[2] = 0; - a->d[3] = 0; - } -#endif -} - -static inline void fe_normalize_full(secp256k1_fe *a) { - fe_normalize(a); -} - -/* r = a + b. - * LAZY: does NOT normalize. Result may be in [0, 2^256 + C). - * fe_mul/fe_sqr handle unnormalized inputs via their reduction. - * Call fe_normalize() explicitly before comparisons or serialization. */ -static inline void fe_add(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { - uint64_t carry = 0; - for (int i = 0; i < 4; i++) { - uint64_t sum = a->d[i] + b->d[i] + carry; - carry = (sum < a->d[i]) || (carry && sum == a->d[i]) ? 1 : 0; - r->d[i] = sum; - } - if (carry) { - /* Overflow past 2^256: fold using 2^256 mod p = C = 0x1000003D1 */ - uint64_t s = r->d[0] + 0x1000003D1ULL; - uint64_t c = (s < r->d[0]) ? 1 : 0; - r->d[0] = s; - if (c) { r->d[1]++; if (!r->d[1]) { r->d[2]++; if (!r->d[2]) r->d[3]++; } } - } - /* No normalize — result may be in [P, 2^256). This is fine because: - * - fe_mul/fe_sqr reduce any 256-bit input correctly - * - fe_negate uses 2P - a which handles values up to 2P - * - fe_half handles values up to 2P - * Only fe_is_zero, fe_cmp, fe_to_bytes need explicit normalize first. */ -} - -/* r += a (lazy, no normalize) */ -static inline void fe_add_assign(secp256k1_fe *r, const secp256k1_fe *a) { - secp256k1_fe t = *r; - fe_add(r, &t, a); -} - -/* r = -a mod p. - * - * Normalizes a first (fast — usually a no-op), then computes P - a. When - * a == 0, P - 0 = P, which a final fe_normalize collapses back to 0. This - * branch-free form matches the Kotlin FieldP.neg path and avoids the - * data-dependent early-return the previous version had. */ -static inline void fe_negate(secp256k1_fe *r, const secp256k1_fe *a, int m) { - (void)m; - secp256k1_fe t = *a; - fe_normalize(&t); - uint64_t borrow = 0; - for (int i = 0; i < 4; i++) { - uint64_t diff = FE_P.d[i] - t.d[i] - borrow; - borrow = (FE_P.d[i] < t.d[i] + borrow) || (borrow && t.d[i] == UINT64_MAX) ? 1 : 0; - r->d[i] = diff; - } - /* If a was 0, r is now P; fold back to 0. Normal inputs leave r < P. */ - fe_normalize(r); -} - -/* ==================== Function declarations ==================== */ - -/* Field multiply and square — declared here, defined in field.c. - * On platforms with ASM (x86_64 MULX, ARM64 CE), fe_mul dispatches - * to the inline fe_mul_asm which the compiler can inline into callers - * within the same compilation unit. For cross-unit inlining (point.c - * calling fe_mul), we rely on LTO or the static inline below. */ -#include "field_asm.h" - -#if HAVE_INT128 -/* - * Dedicated field squaring using 10 multiplications instead of the 16 that - * an unspecialized 4x4 schoolbook mul would use. Exploits symmetry - * a[i]*a[j] == a[j]*a[i] with 4 diagonal + 6 doubled cross products. - * - * Three-pass structure (same as the Kotlin U256.sqrWide): - * Pass 1: compute un-doubled cross-product sum in 6 limbs - * Pass 2: shift left by 1 (doubles every cross product simultaneously) - * Pass 3: add the 4 diagonal products a[i]^2 - * - * NOTE: this inline is only wired up for the non-FE_MUL_ASM build path. - * On x86_64 GCC with BMI2/ADX and on ARM64 GCC, fe_mul_asm's hand-tuned - * MULX + ADCX/ADOX (or ARM64 MUL/UMULH) carry-chain scheduling beats the - * __int128 path despite fe_mul_asm using 16 muls. Benchmark measurement - * (bench_vs_acinq) showed fe_sqr_inline regressed verify/batch-verify by - * ~10-25% on x86_64 when used in place of fe_mul_asm(r, a, a), so on - * ASM platforms we keep fe_mul_asm(r, a, a) for fe_sqr. The 10-mul - * version still helps portable builds (clang, MSVC, non-GCC toolchains). - */ -static inline void fe_sqr_inline(secp256k1_fe *r, const secp256k1_fe *a) { - uint64_t a0 = a->d[0], a1 = a->d[1], a2 = a->d[2], a3 = a->d[3]; - uint128_t acc; - - /* ===== Pass 1: un-doubled cross-product sum ===== - * Cross products and their column positions: - * col 1: a0*a1 - * col 2: a0*a2 - * col 3: a0*a3 + a1*a2 (two products in one column) - * col 4: a1*a3 - * col 5: a2*a3 - * - * Cols 1,2,4,5 each have a single product, so the 128-bit accumulator - * trick (`acc += new_product; extract low 64`) stays within bounds: - * after the extract, acc < 2^64, and adding another full 128-bit product - * gives at most (2^64-1)+(2^128-2^65+1) = 2^128-2^64 which still fits. - * - * Col 3 is the only column with two products, so the straightforward - * `acc += p1; acc += p2` sum can overflow 128 bits. We instead extract - * the low limb after the first product, then manually fold the second - * product's low/high halves into the running accumulator with an - * explicit carry flag. - */ - acc = (uint128_t)a0 * a1; - uint64_t x1 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a0 * a2; - uint64_t x2 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a0 * a3; - uint64_t x3_partial = (uint64_t)acc; acc >>= 64; - /* Fold the second col-3 product (a1*a2) into x3 and the running carry. */ - uint128_t p12 = (uint128_t)a1 * a2; - uint64_t p12_lo = (uint64_t)p12; - uint64_t p12_hi = (uint64_t)(p12 >> 64); - uint64_t x3 = x3_partial + p12_lo; - uint64_t x3_carry = (x3 < x3_partial) ? 1ULL : 0ULL; - acc += (uint128_t)p12_hi + x3_carry; - acc += (uint128_t)a1 * a3; - uint64_t x4 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)a2 * a3; - uint64_t x5 = (uint64_t)acc; - uint64_t x6 = (uint64_t)(acc >> 64); - - /* ===== Pass 2: shift the cross-product sum left by one bit ===== */ - uint64_t x7 = x6 >> 63; - x6 = (x6 << 1) | (x5 >> 63); - x5 = (x5 << 1) | (x4 >> 63); - x4 = (x4 << 1) | (x3 >> 63); - x3 = (x3 << 1) | (x2 >> 63); - x2 = (x2 << 1) | (x1 >> 63); - x1 = x1 << 1; - - /* ===== Pass 3: add the 4 diagonal products a[i]^2 ===== */ - uint64_t d0lo, d0hi, d1lo, d1hi, d2lo, d2hi, d3lo, d3hi; - acc = (uint128_t)a0 * a0; - d0lo = (uint64_t)acc; d0hi = (uint64_t)(acc >> 64); - acc = (uint128_t)a1 * a1; - d1lo = (uint64_t)acc; d1hi = (uint64_t)(acc >> 64); - acc = (uint128_t)a2 * a2; - d2lo = (uint64_t)acc; d2hi = (uint64_t)(acc >> 64); - acc = (uint128_t)a3 * a3; - d3lo = (uint64_t)acc; d3hi = (uint64_t)(acc >> 64); - - /* w[] = full 8-limb product, cross-sum already doubled. */ - uint64_t w0 = d0lo; - acc = (uint128_t)x1 + d0hi; - uint64_t w1 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)x2 + d1lo; - uint64_t w2 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)x3 + d1hi; - uint64_t w3 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)x4 + d2lo; - uint64_t w4 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)x5 + d2hi; - uint64_t w5 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)x6 + d3lo; - uint64_t w6 = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)x7 + d3hi; - uint64_t w7 = (uint64_t)acc; - /* Any carry out of w7 is impossible: the full product a^2 < 2^512, - * and we've accounted for every bit. */ - - /* ===== Reduce: r = w[0..3] + w[4..7] * C ===== */ - const uint64_t FC = 0x1000003D1ULL; - acc = (uint128_t)w0 + (uint128_t)w4 * FC; - r->d[0] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)w1 + (uint128_t)w5 * FC; - r->d[1] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)w2 + (uint128_t)w6 * FC; - r->d[2] = (uint64_t)acc; acc >>= 64; - acc += (uint128_t)w3 + (uint128_t)w7 * FC; - r->d[3] = (uint64_t)acc; - uint64_t carry = (uint64_t)(acc >> 64); - - /* Final fold loop (same shape as fe_mul). */ - while (carry) { - acc = (uint128_t)r->d[0] + (uint128_t)carry * FC; - r->d[0] = (uint64_t)acc; - uint64_t c1 = (uint64_t)(acc >> 64); - if (c1) { - uint64_t s = r->d[1] + c1; - uint64_t cc = (s < c1) ? 1 : 0; - r->d[1] = s; - if (cc) { - s = r->d[2] + 1; - cc = (s == 0) ? 1 : 0; - r->d[2] = s; - if (cc) { - s = r->d[3] + 1; - r->d[3] = s; - carry = (s == 0) ? 1 : 0; - continue; - } - } - } - carry = 0; - } -} -#endif /* HAVE_INT128 */ - -#if FE_MUL_ASM -/* Use the ASM version directly as static inline so point.c can inline it. - * - * fe_sqr: on FE_MUL_ASM platforms (x86_64 GCC with BMI2/ADX, ARM64 GCC) the - * hand-tuned fe_mul_asm uses MULX + dual ADCX/ADOX carry chains that the - * __int128-based fe_sqr_inline cannot match in practice. Measurement showed - * that saving 6 multiplications via symmetry doesn't compensate for losing - * the ASM's optimized carry-chain scheduling and register usage. So on ASM - * platforms we just square via fe_mul_asm(r, a, a), which is faster in - * wall-clock terms. The 10-mul fe_sqr_inline is still used for compilers - * without the ASM path (clang, MSVC, portable fallback). */ -static inline void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { - fe_mul_asm(r, a, b); -} -static inline void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) { - fe_mul_asm(r, a, a); -} -#else -void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b); -#if HAVE_INT128 -static inline void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) { - fe_sqr_inline(r, a); -} -#else -void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a); -#endif -#endif -void fe_inv(secp256k1_fe *r, const secp256k1_fe *a); -int fe_sqrt(secp256k1_fe *r, const secp256k1_fe *a); -void fe_half(secp256k1_fe *r, const secp256k1_fe *a); -void fe_to_bytes(uint8_t *out32, const secp256k1_fe *a); -int fe_from_bytes(secp256k1_fe *r, const uint8_t *in32); -int fe_cmp(const secp256k1_fe *a, const secp256k1_fe *b); - -#endif /* SECP256K1_FIELD_H */ diff --git a/quartz/src/main/c/secp256k1/field_asm.h b/quartz/src/main/c/secp256k1/field_asm.h deleted file mode 100644 index 1ec5a465e..000000000 --- a/quartz/src/main/c/secp256k1/field_asm.h +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Platform-specific field multiply/square using inline assembly. - * - * ARM64: MUL + UMULH pairs for 64x64->128 products (2 instructions, 3 cycles each) - * x86_64: MULQ for 64x64->128 products (1 instruction, 3 cycles each) - * - * The compiler's __int128 code is decent but not optimal: - * - On x86_64: gcc generates MULQ correctly but adds unnecessary MOVs - * - On ARM64: gcc sometimes uses UMULL (32-bit) instead of MUL+UMULH (64-bit) - * - * These hand-tuned versions keep intermediates in registers and avoid - * redundant moves, saving ~2-3ns per fe_mul (~10% of verify). - */ -#ifndef SECP256K1_FIELD_ASM_H -#define SECP256K1_FIELD_ASM_H - -#include "secp256k1_c.h" - -#define FIELD_C_ASM 0x1000003D1ULL - -#if SECP_X86_64 && defined(__GNUC__) && !defined(__clang_analyzer__) - -/* - * x86_64 field multiply using MULX (BMI2) + ADCX/ADOX (ADX) instructions. - * - * MULX: rdx * src -> hi:lo (two arbitrary output regs, NO flags clobbered) - * ADCX: add-with-carry using CF only (ignores OF) - * ADOX: add-with-carry using OF only (ignores CF) - * - * This enables TWO INDEPENDENT carry chains running in parallel: - * - CF chain: accumulates the low parts of products - * - OF chain: accumulates the high parts of products - * - * The CPU can pipeline MULX+ADCX+ADOX since they don't conflict on flags. - * Compared to MULQ+ADC: ~20-30% faster due to eliminated serial dependencies. - * - * If BMI2/ADX not available at runtime, falls back to MULQ. - */ -/* - * x86_64 field multiply using MULX (BMI2) + ADCX/ADOX (ADX). - * - * Hand-tuned inline assembly implementing the dual carry chain pattern: - * - MULX produces hi:lo without clobbering flags - * - ADCX accumulates using CF chain (for low parts) - * - ADOX accumulates using OF chain (for high parts) - * - * This eliminates the serial dependency in MULQ+ADC chains, allowing - * the CPU to pipeline multiply-accumulate operations. - * - * Compiled with -mbmi2 -madx for runtime detection, but the instructions - * are encoded directly so the binary requires BMI2+ADX capable CPU. - */ -static inline void fe_mul_asm(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { - uint64_t r0, r1, r2, r3, r4, r5, r6, r7; - - __asm__ __volatile__( - /* ===== Row 0: a[0] * b[0..3] ===== */ - "movq (%[a]), %%rdx\n\t" - "mulx (%[b]), %[r0], %[r1]\n\t" - "mulx 8(%[b]), %%rax, %[r2]\n\t" - "addq %%rax, %[r1]\n\t" - "mulx 16(%[b]), %%rax, %[r3]\n\t" - "adcq %%rax, %[r2]\n\t" - "mulx 24(%[b]), %%rax, %[r4]\n\t" - "adcq %%rax, %[r3]\n\t" - "adcq $0, %[r4]\n\t" - - /* ===== Row 1: a[1] * b[0..3], accumulate into r1..r5 ===== */ - "movq 8(%[a]), %%rdx\n\t" - "mulx (%[b]), %%rax, %%rcx\n\t" - "addq %%rax, %[r1]\n\t" - "adcq %%rcx, %[r2]\n\t" - "mulx 16(%[b]), %%rax, %%rcx\n\t" - "adcq %%rax, %[r3]\n\t" - "adcq %%rcx, %[r4]\n\t" - "movq $0, %[r5]\n\t" - "adcq $0, %[r5]\n\t" - /* a1*b1 */ - "movq 8(%[a]), %%rdx\n\t" - "mulx 8(%[b]), %%rax, %%rcx\n\t" - "addq %%rax, %[r2]\n\t" - "adcq %%rcx, %[r3]\n\t" - /* a1*b3 */ - "mulx 24(%[b]), %%rax, %%rcx\n\t" - "adcq %%rax, %[r4]\n\t" - "adcq %%rcx, %[r5]\n\t" - - /* ===== Row 2: a[2] * b[0..3] ===== */ - "movq 16(%[a]), %%rdx\n\t" - "mulx (%[b]), %%rax, %%rcx\n\t" - "addq %%rax, %[r2]\n\t" - "adcq %%rcx, %[r3]\n\t" - "mulx 16(%[b]), %%rax, %%rcx\n\t" - "adcq %%rax, %[r4]\n\t" - "adcq %%rcx, %[r5]\n\t" - "movq $0, %[r6]\n\t" - "adcq $0, %[r6]\n\t" - "mulx 8(%[b]), %%rax, %%rcx\n\t" - "addq %%rax, %[r3]\n\t" - "adcq %%rcx, %[r4]\n\t" - "mulx 24(%[b]), %%rax, %%rcx\n\t" - "adcq %%rax, %[r5]\n\t" - "adcq %%rcx, %[r6]\n\t" - - /* ===== Row 3: a[3] * b[0..3] ===== */ - "movq 24(%[a]), %%rdx\n\t" - "mulx (%[b]), %%rax, %%rcx\n\t" - "addq %%rax, %[r3]\n\t" - "adcq %%rcx, %[r4]\n\t" - "mulx 16(%[b]), %%rax, %%rcx\n\t" - "adcq %%rax, %[r5]\n\t" - "adcq %%rcx, %[r6]\n\t" - "movq $0, %[r7]\n\t" - "adcq $0, %[r7]\n\t" - "mulx 8(%[b]), %%rax, %%rcx\n\t" - "addq %%rax, %[r4]\n\t" - "adcq %%rcx, %[r5]\n\t" - "mulx 24(%[b]), %%rax, %%rcx\n\t" - "adcq %%rax, %[r6]\n\t" - "adcq %%rcx, %[r7]\n\t" - - : [r0]"=&r"(r0), [r1]"=&r"(r1), [r2]"=&r"(r2), [r3]"=&r"(r3), - [r4]"=&r"(r4), [r5]"=&r"(r5), [r6]"=&r"(r6), [r7]"=&r"(r7) - : [a]"r"(a->d), [b]"r"(b->d) - : "rax", "rcx", "rdx", "cc", "memory" - ); - - /* Reduce: r[0..3] + r[4..7] * C */ - uint64_t c = FIELD_C_ASM; - __asm__ __volatile__( - "movq %[r4], %%rdx\n\t" - "mulx %[c], %%rax, %%rcx\n\t" - "addq %%rax, %[r0]\n\t" - "adcq %%rcx, %[r1]\n\t" - - "movq %[r5], %%rdx\n\t" - "mulx %[c], %%rax, %%rcx\n\t" - "adcq $0, %[r2]\n\t" - "adcq $0, %[r3]\n\t" - "addq %%rax, %[r1]\n\t" - "adcq %%rcx, %[r2]\n\t" - - "movq %[r6], %%rdx\n\t" - "mulx %[c], %%rax, %%rcx\n\t" - "adcq $0, %[r3]\n\t" - "sbbq %%r8, %%r8\n\t" - "negq %%r8\n\t" - "addq %%rax, %[r2]\n\t" - "adcq %%rcx, %[r3]\n\t" - "adcq $0, %%r8\n\t" - - "movq %[r7], %%rdx\n\t" - "mulx %[c], %%rax, %%rcx\n\t" - "addq %%rax, %[r3]\n\t" - "adcq %%rcx, %%r8\n\t" - - /* Final fold: r8 * C */ - "movq %%r8, %%rdx\n\t" - "mulx %[c], %%rax, %%rcx\n\t" - "addq %%rax, %[r0]\n\t" - "adcq %%rcx, %[r1]\n\t" - "adcq $0, %[r2]\n\t" - "adcq $0, %[r3]\n\t" - - : [r0]"+r"(r0), [r1]"+r"(r1), [r2]"+r"(r2), [r3]"+r"(r3) - : [r4]"r"(r4), [r5]"r"(r5), [r6]"r"(r6), [r7]"r"(r7), [c]"r"(c) - : "rax", "rcx", "rdx", "r8", "cc" - ); - - r->d[0] = r0; r->d[1] = r1; r->d[2] = r2; r->d[3] = r3; - /* No normalize — lazy mul. Result in [0, 2^256). */ -} - -#define FE_MUL_ASM 1 - -#elif SECP_ARM64 && defined(__GNUC__) - -/* - * ARM64 field multiply optimized for Cortex-A76+ (Android phones). - * - * Key ARM64 instructions used: - * MUL Xd, Xn, Xm → low 64 bits of 64×64 product (1 cycle throughput) - * UMULH Xd, Xn, Xm → high 64 bits of 64×64 product (1 cycle throughput) - * ADDS/ADCS → add with carry flag chain - * LDP Xd1, Xd2, [Xn] → load pair (2 registers in 1 instruction) - * CSEL → conditional select (branchless normalize) - * - * Row 0 in full ASM with LDP for loading operands. - * Rows 1-3 + reduction in __int128 C (ARM64 gcc generates optimal code - * for __int128: MUL+UMULH pairs with ADDS/ADCS carry chains). - * - * The main ARM64-specific wins vs generic C: - * 1. LDP loads 2 limbs per instruction (vs 2 separate LDR) - * 2. Explicit ADDS/ADCS chain avoids compiler's carry tracking overhead - * 3. The compiler generates MADD (fused multiply-add) for acc += (u128)a*b - */ -static inline void fe_mul_asm(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) { - uint64_t a0, a1, a2, a3, b0, b1, b2, b3; - uint64_t lo0, lo1, lo2, lo3, hi0, hi1, hi2, hi3; - - /* Load all 8 limbs using LDP (load pair) — 4 instructions vs 8 LDR */ - __asm__ __volatile__( - "ldp %[a0], %[a1], [%[ap]]\n\t" - "ldp %[a2], %[a3], [%[ap], #16]\n\t" - "ldp %[b0], %[b1], [%[bp]]\n\t" - "ldp %[b2], %[b3], [%[bp], #16]\n\t" - : [a0]"=&r"(a0), [a1]"=&r"(a1), [a2]"=&r"(a2), [a3]"=&r"(a3), - [b0]"=&r"(b0), [b1]"=&r"(b1), [b2]"=&r"(b2), [b3]"=&r"(b3) - : [ap]"r"(a->d), [bp]"r"(b->d) - : "memory" - ); - - /* Full 4x4 multiply + reduction in ARM64 ASM. - * Uses 20 registers: 4 inputs a, 4 inputs b, 8 product, 4 temps. - * All 31 ARM64 GPRs available — zero stack spills. - * - * Scheduling: interleave MUL/UMULH from adjacent columns so the - * 3-cycle multiply latency is hidden by independent additions. - * - * Row 0: a0 * b[0..3] → r0..r4 - * Row 1: a1 * b[0..3] → accumulate into r1..r5 - * Row 2: a2 * b[0..3] → accumulate into r2..r6 - * Row 3: a3 * b[0..3] → accumulate into r3..r7 - * Reduce: r[0..3] + r[4..7] * C - */ - __asm__ __volatile__( - /* === Row 0: a0 * b[0..3] === */ - "mul %[lo0], %[a0], %[b0]\n\t" - "umulh %[lo1], %[a0], %[b0]\n\t" /* lo1 = hi(a0*b0) = carry */ - "mul x16, %[a0], %[b1]\n\t" - "umulh x17, %[a0], %[b1]\n\t" - "adds %[lo1], %[lo1], x16\n\t" - "adc %[lo2], x17, xzr\n\t" - "mul x16, %[a0], %[b2]\n\t" - "umulh x17, %[a0], %[b2]\n\t" - "adds %[lo2], %[lo2], x16\n\t" - "adc %[lo3], x17, xzr\n\t" - "mul x16, %[a0], %[b3]\n\t" - "umulh %[hi0], %[a0], %[b3]\n\t" - "adds %[lo3], %[lo3], x16\n\t" - "adc %[hi0], %[hi0], xzr\n\t" - - /* === Row 1: a1 * b[0..3], accumulate === */ - "mul x16, %[a1], %[b0]\n\t" - "umulh x17, %[a1], %[b0]\n\t" - "adds %[lo1], %[lo1], x16\n\t" - "adcs %[lo2], %[lo2], x17\n\t" - "mul x16, %[a1], %[b2]\n\t" /* interleave: start b2 while b1 pending */ - "umulh x17, %[a1], %[b2]\n\t" - "adcs %[lo3], %[lo3], x16\n\t" - "adcs %[hi0], %[hi0], x17\n\t" - "adc %[hi1], xzr, xzr\n\t" - "mul x16, %[a1], %[b1]\n\t" - "umulh x17, %[a1], %[b1]\n\t" - "adds %[lo2], %[lo2], x16\n\t" - "adcs %[lo3], %[lo3], x17\n\t" - "mul x16, %[a1], %[b3]\n\t" - "umulh x17, %[a1], %[b3]\n\t" - "adcs %[hi0], %[hi0], x16\n\t" - "adc %[hi1], %[hi1], x17\n\t" - - /* === Row 2: a2 * b[0..3] === */ - "mul x16, %[a2], %[b0]\n\t" - "umulh x17, %[a2], %[b0]\n\t" - "adds %[lo2], %[lo2], x16\n\t" - "adcs %[lo3], %[lo3], x17\n\t" - "mul x16, %[a2], %[b2]\n\t" - "umulh x17, %[a2], %[b2]\n\t" - "adcs %[hi0], %[hi0], x16\n\t" - "adcs %[hi1], %[hi1], x17\n\t" - "adc %[hi2], xzr, xzr\n\t" - "mul x16, %[a2], %[b1]\n\t" - "umulh x17, %[a2], %[b1]\n\t" - "adds %[lo3], %[lo3], x16\n\t" - "adcs %[hi0], %[hi0], x17\n\t" - "mul x16, %[a2], %[b3]\n\t" - "umulh x17, %[a2], %[b3]\n\t" - "adcs %[hi1], %[hi1], x16\n\t" - "adc %[hi2], %[hi2], x17\n\t" - - /* === Row 3: a3 * b[0..3] === */ - "mul x16, %[a3], %[b0]\n\t" - "umulh x17, %[a3], %[b0]\n\t" - "adds %[lo3], %[lo3], x16\n\t" - "adcs %[hi0], %[hi0], x17\n\t" - "mul x16, %[a3], %[b2]\n\t" - "umulh x17, %[a3], %[b2]\n\t" - "adcs %[hi1], %[hi1], x16\n\t" - "adcs %[hi2], %[hi2], x17\n\t" - "adc %[hi3], xzr, xzr\n\t" - "mul x16, %[a3], %[b1]\n\t" - "umulh x17, %[a3], %[b1]\n\t" - "adds %[hi0], %[hi0], x16\n\t" - "adcs %[hi1], %[hi1], x17\n\t" - "mul x16, %[a3], %[b3]\n\t" - "umulh x17, %[a3], %[b3]\n\t" - "adcs %[hi2], %[hi2], x16\n\t" - "adc %[hi3], %[hi3], x17\n\t" - - : [lo0]"=&r"(lo0), [lo1]"=&r"(lo1), [lo2]"=&r"(lo2), [lo3]"=&r"(lo3), - [hi0]"=&r"(hi0), [hi1]"=&r"(hi1), [hi2]"=&r"(hi2), [hi3]"=&r"(hi3) - : [a0]"r"(a0), [a1]"r"(a1), [a2]"r"(a2), [a3]"r"(a3), - [b0]"r"(b0), [b1]"r"(b1), [b2]"r"(b2), [b3]"r"(b3) - : "x16", "x17", "cc" - ); - - /* Reduction: lo + hi * C using MUL+UMULH+ADDS chain */ - { - uint64_t c = FIELD_C_ASM; - __asm__ __volatile__( - /* hi0 * C + lo0 */ - "mul x16, %[h0], %[c]\n\t" - "umulh x17, %[h0], %[c]\n\t" - "adds %[r0], %[l0], x16\n\t" - "adc x17, x17, xzr\n\t" - /* hi1 * C + lo1 + carry */ - "mul x16, %[h1], %[c]\n\t" - "adds %[r1], %[l1], x17\n\t" - "umulh x17, %[h1], %[c]\n\t" - "adc x17, x17, xzr\n\t" - "adds %[r1], %[r1], x16\n\t" - "adc x17, x17, xzr\n\t" - /* hi2 * C + lo2 + carry */ - "mul x16, %[h2], %[c]\n\t" - "adds %[r2], %[l2], x17\n\t" - "umulh x17, %[h2], %[c]\n\t" - "adc x17, x17, xzr\n\t" - "adds %[r2], %[r2], x16\n\t" - "adc x17, x17, xzr\n\t" - /* hi3 * C + lo3 + carry */ - "mul x16, %[h3], %[c]\n\t" - "adds %[r3], %[l3], x17\n\t" - "umulh x17, %[h3], %[c]\n\t" - "adc x17, x17, xzr\n\t" - "adds %[r3], %[r3], x16\n\t" - "adc x17, x17, xzr\n\t" - /* Final fold: carry * C */ - "mul x16, x17, %[c]\n\t" - "adds %[r0], %[r0], x16\n\t" - "umulh x16, x17, %[c]\n\t" - "adcs %[r1], %[r1], x16\n\t" - "adcs %[r2], %[r2], xzr\n\t" - "adc %[r3], %[r3], xzr\n\t" - : [r0]"=&r"(lo0), [r1]"=&r"(lo1), [r2]"=&r"(lo2), [r3]"=&r"(lo3) - : [l0]"r"(lo0), [l1]"r"(lo1), [l2]"r"(lo2), [l3]"r"(lo3), - [h0]"r"(hi0), [h1]"r"(hi1), [h2]"r"(hi2), [h3]"r"(hi3), [c]"r"(c) - : "x16", "x17", "cc" - ); - } - - /* Store result using STP (store pair) — 2 instructions vs 4 STR */ - __asm__ __volatile__( - "stp %[lo0], %[lo1], [%[rp]]\n\t" - "stp %[lo2], %[lo3], [%[rp], #16]\n\t" - : : [rp]"r"(r->d), [lo0]"r"(lo0), [lo1]"r"(lo1), [lo2]"r"(lo2), [lo3]"r"(lo3) - : "memory" - ); - /* No normalize — lazy mul. Result in [0, 2^256). */ -} - -#define FE_MUL_ASM 1 - -#else -#define FE_MUL_ASM 0 -#endif - -#endif /* SECP256K1_FIELD_ASM_H */ diff --git a/quartz/src/main/c/secp256k1/jni_bridge.c b/quartz/src/main/c/secp256k1/jni_bridge.c deleted file mode 100644 index a1e74b894..000000000 --- a/quartz/src/main/c/secp256k1/jni_bridge.c +++ /dev/null @@ -1,350 +0,0 @@ -/* - * 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 "sha256.h" -#include -#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; -} - -/* - * Copy a variable-length Java byte[] into a native buffer without pinning. - * - * GetByteArrayElements pins the underlying Java array, blocking GC compaction - * for the duration of the call. For short-lived crypto operations (signing - * or verifying a Nostr event, almost always a 32-byte message digest), copying - * via GetByteArrayRegion into a stack or heap buffer is both cheaper and - * friendlier to the GC. Small messages (<=512 B) go on the stack; larger - * ones allocate via malloc. Caller frees `*out_buf` only if `*needs_free`. - */ -static int copy_msg_bytes(JNIEnv *env, jbyteArray arr, - uint8_t *stack_buf, size_t stack_cap, - uint8_t **out_buf, size_t *out_len, - int *needs_free) { - if (!arr) return 0; - jint len = (*env)->GetArrayLength(env, arr); - if (len < 0) return 0; - *out_len = (size_t)len; - *needs_free = 0; - if ((size_t)len <= stack_cap) { - *out_buf = stack_buf; - } else { - *out_buf = (uint8_t *)malloc((size_t)len); - if (!*out_buf) return 0; - *needs_free = 1; - } - if (len > 0) { - (*env)->GetByteArrayRegion(env, arr, 0, len, (jbyte *)*out_buf); - } - return 1; -} - -/* ==================== 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; - - uint8_t stack_msg[512]; - uint8_t *msg_buf = NULL; - size_t msg_len = 0; - int needs_free = 0; - if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg), - &msg_buf, &msg_len, &needs_free)) { - 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, msg_len, sk, aux_ptr); - if (needs_free) free(msg_buf); - - 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; - - uint8_t stack_msg[512]; - uint8_t *msg_buf = NULL; - size_t msg_len = 0; - int needs_free = 0; - if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg), - &msg_buf, &msg_len, &needs_free)) { - 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, msg_len, sk, xonly, aux_ptr); - if (needs_free) free(msg_buf); - - 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; - - uint8_t stack_msg[512]; - uint8_t *msg_buf = NULL; - size_t msg_len = 0; - int needs_free = 0; - if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg), - &msg_buf, &msg_len, &needs_free)) { - return JNI_FALSE; - } - - int ok = secp256k1c_schnorr_verify(s, msg_buf, msg_len, p); - if (needs_free) free(msg_buf); - 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; - - uint8_t stack_msg[512]; - uint8_t *msg_buf = NULL; - size_t msg_len = 0; - int needs_free = 0; - if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg), - &msg_buf, &msg_len, &needs_free)) { - return JNI_FALSE; - } - - int ok = secp256k1c_schnorr_verify_fast(s, msg_buf, msg_len, p); - if (needs_free) free(msg_buf); - 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); -} - -/* ==================== SHA-256 (for benchmarking hardware vs software) ==================== */ - -JNIEXPORT jbyteArray JNICALL -Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSha256( - JNIEnv *env, jclass cls, jbyteArray data -) { - (void)cls; - uint8_t stack_buf[1024]; - uint8_t *buf = NULL; - size_t len = 0; - int needs_free = 0; - if (!copy_msg_bytes(env, data, stack_buf, sizeof(stack_buf), - &buf, &len, &needs_free)) { - return NULL; - } - - uint8_t out[32]; - secp256k1_sha256_hash(out, buf, len); - if (needs_free) free(buf); - return make_bytes(env, out, 32); -} diff --git a/quartz/src/main/c/secp256k1/point.c b/quartz/src/main/c/secp256k1/point.c deleted file mode 100644 index 14d60c629..000000000 --- a/quartz/src/main/c/secp256k1/point.c +++ /dev/null @@ -1,779 +0,0 @@ -/* - * 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 */ -/* 4x64 little-endian */ -const secp256k1_ge SECP256K1_G = { - .x = {{0x59F2815B16F81798ULL, 0x029BFCDB2DCE28D9ULL, - 0x55A06295CE870B07ULL, 0x79BE667EF9DCBBACULL}}, - .y = {{0x9C47D08FFB10D4B8ULL, 0xFD17B448A6855419ULL, - 0x5DA4FBFC0E1108A8ULL, 0x483ADA7726A3C465ULL}} -}; - -/* GLV beta: cube root of unity mod p (4x64 little-endian) */ -/* beta = 0x7AE96A2B657C07106E64479EAC3434E99CF0497512F58995C1396C28719501EE */ -static const secp256k1_fe GLV_BETA = {{ - 0xC1396C28719501EEULL, 0x9CF0497512F58995ULL, - 0x6E64479EAC3434E9ULL, 0x7AE96A2B657C0710ULL -}}; - -/* Curve constant b = 7 */ -static const secp256k1_fe FE_SEVEN = {{7, 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; - -/* ==================== P-side wNAF Table Cache ==================== */ -/* - * Cache P-side affine wNAF tables keyed by pubkey x-coordinate. - * In Nostr, the same pubkeys are verified repeatedly (feed from one author). - * Building the P-side table costs ~437 field ops (~27% of verify). Caching - * skips this entirely on repeat verifications. - * - * 1024 entries × 16 AffinePoints × 64 bytes ≈ 1MB. Acceptable for mobile. - */ -#define P_TABLE_CACHE_SIZE 1024 -#define P_TABLE_CACHE_MASK (P_TABLE_CACHE_SIZE - 1) -#define P_TABLE_ENTRIES 8 /* 2^(wP-2) for wP=5 */ - -typedef struct { - secp256k1_fe px; /* cache key: x-coordinate */ - secp256k1_ge p_odd[P_TABLE_ENTRIES]; /* affine odd-multiples of P */ - secp256k1_ge p_lam_odd[P_TABLE_ENTRIES]; /* affine odd-multiples of lambda(P) */ - int valid; -} cached_p_table; - -static cached_p_table p_table_cache[P_TABLE_CACHE_SIZE]; - -static int p_cache_slot(const secp256k1_fe *px) { - return ((int)(px->d[0] ^ (px->d[1] << 3))) & P_TABLE_CACHE_MASK; -} - -/* ==================== 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; - - /* Handle aliasing: if r == p, copy input first */ - secp256k1_gej tmp; - if (r == p) { - tmp = *p; - p = &tmp; - } - - 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. - * Callers in the hot path (ecmult loops) always pass r != p. - * The aliasing check is kept for safety in table-build code. */ -void gej_add_ge(secp256k1_gej *r, const secp256k1_gej *p, const secp256k1_ge *q) { - secp256k1_gej _tmp; - if (__builtin_expect(r == p, 0)) { _tmp = *p; p = &_tmp; } - 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); - - if (fe_is_zero(&h)) { - fe_negate(&t, &p->y, 1); - fe_add(&t, &s2, &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); /* I = (2H)² */ - fe_mul(&j, &h, &i); /* J = H*I */ - fe_negate(&t, &p->y, 1); - fe_add(&rr, &s2, &t); - fe_add(&rr, &rr, &rr); /* r = 2*(S2-Y1) */ - fe_mul(&v, &p->x, &i); /* V = X1*I */ - - fe_sqr(&r->x, &rr); /* X3 = r² */ - 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_negate(&t, &r->x, 5); - fe_add(&t, &v, &t); /* V - X3 */ - fe_mul(&r->y, &rr, &t); /* r*(V-X3) */ - fe_mul(&t, &p->y, &j); - fe_add(&t, &t, &t); - fe_negate(&t, &t, 2); - fe_add_assign(&r->y, &t); /* - 2*Y1*J */ - - fe_mul(&r->z, &p->z, &h); - fe_add(&r->z, &r->z, &r->z); /* Z3 = 2*Z1*H */ - - 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) { - /* Handle aliasing */ - secp256k1_gej tp, tq; - if (r == p) { tp = *p; p = &tp; } - if (r == q) { tq = *q; q = &tq; } - 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; - - /* Build cumulative Z products, skipping infinity points (Z=0). - * For infinity points, carry forward the previous cumulative product. */ - int first_valid = -1; - for (int i = 0; i < count; i++) { - if (in[i].infinity) { - /* Infinity: carry previous product (or set to 1 if first) */ - if (i == 0) { - cumz[0] = FE_ONE; - } else { - cumz[i] = cumz[i-1]; - } - out[i] = (secp256k1_ge){ .x = FE_ZERO, .y = FE_ZERO }; - } else { - if (first_valid < 0) first_valid = i; - if (i == 0 || first_valid == i) { - cumz[i] = in[i].z; - } else { - fe_mul(&cumz[i], &cumz[i-1], &in[i].z); - } - } - } - - if (first_valid < 0) { free(cumz); return; } /* All infinity */ - - secp256k1_fe inv, zi, zi2, zi3; - fe_inv(&inv, &cumz[count-1]); - - for (int i = count - 1; i >= 1; i--) { - if (in[i].infinity) continue; /* Skip, already set to zero */ - 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 */ - if (!in[0].infinity) { - 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 using comb method: only 3 doublings + ~43 table lookups. - * ~2.2x faster than GLV+wNAF (~130 doublings + ~32 additions). */ -void ecmult_gen(secp256k1_gej *r, const secp256k1_scalar *scalar) { - if (scalar_is_zero(scalar)) { - gej_set_infinity(r); - return; - } - - const secp256k1_ge *table = comb_table; - gej_set_infinity(r); - - for (int comb_off = COMB_SPACING - 1; comb_off >= 0; comb_off--) { - if (comb_off < COMB_SPACING - 1) { - secp256k1_gej tmp; - gej_double(&tmp, r); - *r = tmp; - } - for (int block = 0; block < COMB_BLOCKS; block++) { - int mask = 0; - for (int tooth = 0; tooth < COMB_TEETH; tooth++) { - int bit_pos = (block * COMB_TEETH + tooth) * COMB_SPACING + comb_off; - if (bit_pos < 256 && scalar_test_bit(scalar, bit_pos)) { - mask |= (1 << tooth); - } - } - if (mask != 0) { - const secp256k1_ge *entry = &table[block * COMB_POINTS + mask]; - secp256k1_gej tmp; - gej_add_ge(&tmp, r, entry); - *r = tmp; - } - } - } -} - -/* 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); - - /* P-side tables: check cache first (same cache as ecmult_double_g). - * For ECDH, the same peer key is used repeatedly (NIP-44 conversations). */ - const secp256k1_ge *p_odd; - const secp256k1_ge *p_lam_odd; - int slot = p_cache_slot(&p->x); - cached_p_table *cached = &p_table_cache[slot]; - - if (cached->valid && fe_equal(&cached->px, &p->x)) { - p_odd = cached->p_odd; - p_lam_odd = cached->p_lam_odd; - } else { - 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); - } - - 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; - } - - batch_to_affine(cached->p_odd, p_odd_jac, table_size); - batch_to_affine(cached->p_lam_odd, p_lam_jac, table_size); - cached->px = p->x; - cached->valid = 1; - - p_odd = cached->p_odd; - p_lam_odd = cached->p_lam_odd; - } - - /* 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); - - /* P-side tables: check cache first, build only on miss */ - const secp256k1_ge *p_odd; - const secp256k1_ge *p_lam_odd; - int slot = p_cache_slot(&p->x); - cached_p_table *cached = &p_table_cache[slot]; - - if (cached->valid && fe_equal(&cached->px, &p->x)) { - /* Cache hit — use cached tables directly */ - p_odd = cached->p_odd; - p_lam_odd = cached->p_lam_odd; - } else { - /* Cache miss — build tables and store in cache */ - 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; - } - - batch_to_affine(cached->p_odd, p_odd_jac, p_table_size); - batch_to_affine(cached->p_lam_odd, p_lam_jac, p_table_size); - cached->px = p->x; - cached->valid = 1; - - p_odd = cached->p_odd; - p_lam_odd = cached->p_lam_odd; - } - - /* 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 deleted file mode 100644 index f08cccc63..000000000 --- a/quartz/src/main/c/secp256k1/point.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 deleted file mode 100644 index 051de7b4a..000000000 --- a/quartz/src/main/c/secp256k1/scalar.c +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * Scalar arithmetic mod n with GLV decomposition and wNAF encoding. - */ -#include "scalar.h" -#include - -/* 2^256 mod n — a ~129-bit constant. Used by scalar_add and scalar_mul. */ -static const uint64_t SCALAR_NC[4] = { - 0x402DA1732FC9BEBFULL, 0x4551231950B75FC4ULL, 1, 0 -}; - -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 past 2^256: r_true = (a+b) - 2^256 + 2^256 = r + 2^256. - * Since 2^256 ≡ NC (mod n), adding NC undoes the wraparound mod n. */ - add256(r->d, r->d, SCALAR_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); -} - -/* Use the field module's proven mul_wide for 4x4 → 8-limb product */ -extern void mul_wide(uint64_t out[8], const uint64_t a[4], const uint64_t b[4]); - -/* - * Multiply mod n: r = (a * b) mod n. - * - * Fully reduces the 512-bit product via repeated folding: 2^256 ≡ NC (mod n). - * Because NC < 2^130, each round strictly shrinks the high half, so the fold - * converges in at most 3 iterations before hi is entirely zero. Loop-driven - * to avoid the subtle third-fold carry-drop bug the previous unrolled version - * had, and to give correct results in the portable (!HAVE_INT128) path too. - */ -void scalar_mul(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b) { - uint64_t t[8]; - mul_wide(t, a->d, b->d); - - uint64_t lo[4] = { t[0], t[1], t[2], t[3] }; - uint64_t hi[4] = { t[4], t[5], t[6], t[7] }; - - while ((hi[0] | hi[1] | hi[2] | hi[3]) != 0) { - /* wide = hi * NC (an 8-limb product), then wide += lo. */ - uint64_t wide[8]; - mul_wide(wide, hi, SCALAR_NC); - uint64_t carry = 0; - for (int i = 0; i < 4; i++) { - uint64_t s1 = lo[i] + wide[i]; - uint64_t c1 = (s1 < lo[i]) ? 1ULL : 0ULL; - uint64_t s2 = s1 + carry; - uint64_t c2 = (s2 < s1) ? 1ULL : 0ULL; - wide[i] = s2; - carry = c1 + c2; - } - for (int i = 4; i < 8 && carry != 0; i++) { - uint64_t s = wide[i] + carry; - carry = (s < wide[i]) ? 1ULL : 0ULL; - wide[i] = s; - } - /* carry out of wide[7] is impossible: NC < 2^130, so hi * NC is - * bounded by 2^256 * 2^130 = 2^386, fitting comfortably in wide[]. - * Adding lo (< 2^256) and a tiny running carry stays inside wide[]. */ - - lo[0] = wide[0]; lo[1] = wide[1]; lo[2] = wide[2]; lo[3] = wide[3]; - hi[0] = wide[4]; hi[1] = wide[5]; hi[2] = wide[6]; hi[3] = wide[7]; - } - - r->d[0] = lo[0]; r->d[1] = lo[1]; r->d[2] = lo[2]; r->d[3] = lo[3]; - /* At this point r < n + small-epsilon; a single subtract is enough. */ - 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]; - mul_wide(t, k->d, g); /* Use the proven mul_wide from field.c */ - /* 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 deleted file mode 100644 index 4d7622870..000000000 --- a/quartz/src/main/c/secp256k1/scalar.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 deleted file mode 100644 index 7596fcbe3..000000000 --- a/quartz/src/main/c/secp256k1/schnorr.c +++ /dev/null @@ -1,489 +0,0 @@ -/* - * 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); -} - -/* - * Fast signing with pre-computed x-only pubkey. - * ASSUMES the private key already produces an even-y pubkey (BIP-340 convention). - * This is the case for Nostr keys managed by KeyPair, which pre-negates if needed. - * For arbitrary keys, use secp256k1c_schnorr_sign which derives y-parity. - */ -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 by convention. - * The caller must ensure the private key produces an even-y pubkey. */ - 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) { - /* Use cached liftX — same peer key in NIP-44 conversations */ - secp256k1_fe px, py; - if (!lift_x_cached(&px, &py, xonly_pub32)) 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 deleted file mode 100644 index b60b46825..000000000 --- a/quartz/src/main/c/secp256k1/secp256k1_c.h +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -#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. - * 4 limbs of 64 bits each, fully packed, little-endian. - * Same representation as the Kotlin Fe4 class. - */ -typedef struct { - uint64_t d[4]; -} secp256k1_fe; - -/* ==================== 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 deleted file mode 100644 index 4b537594a..000000000 --- a/quartz/src/main/c/secp256k1/sha256.c +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * Minimal SHA-256 for BIP-340. No external dependencies. - */ -#include "sha256.h" -#include "sha256_hw.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; -} - -#if SHA256_HW_AVAILABLE -/* Use hardware-accelerated transform (SHA-NI on x86_64, CE on ARM64) */ -static void sha256_transform(uint32_t state[8], const uint8_t block[64]) { - sha256_transform_hw(state, block); -} -#else -/* Software fallback */ -static void sha256_transform_sw(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; -} -static void sha256_transform(uint32_t state[8], const uint8_t block[64]) { - sha256_transform_sw(state, block); -} -#endif /* SHA256_HW_AVAILABLE */ - -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 deleted file mode 100644 index a3bb23f88..000000000 --- a/quartz/src/main/c/secp256k1/sha256.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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/main/c/secp256k1/sha256_hw.h b/quartz/src/main/c/secp256k1/sha256_hw.h deleted file mode 100644 index c3ba816e7..000000000 --- a/quartz/src/main/c/secp256k1/sha256_hw.h +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Hardware-accelerated SHA-256 using platform crypto extensions. - * - * x86_64: SHA-NI (Intel Goldmont+, AMD Zen+) - * SHA256RNDS2, SHA256MSG1, SHA256MSG2 — 4 rounds per instruction - * - * ARM64: Crypto Extensions (all ARMv8.0-A Android phones) - * SHA256H, SHA256H2, SHA256SU0, SHA256SU1 — 4 rounds per instruction - * - * Both achieve ~100-150ns per 64-byte block vs ~800ns in software. - * For BIP-340 tagged hashes (96-160 bytes), this saves ~0.5-1µs per hash. - */ -#ifndef SECP256K1_SHA256_HW_H -#define SECP256K1_SHA256_HW_H - -#include -#include - -/* ==================== x86_64 SHA-NI ==================== */ - -#if defined(__x86_64__) && defined(__SHA__) - -#include - -static inline void sha256_transform_hw(uint32_t state[8], const uint8_t block[64]) { - const __m128i MASK = _mm_set_epi64x(0x0c0d0e0f08090a0bULL, 0x0405060700010203ULL); - - /* Load state */ - __m128i STATE0 = _mm_loadu_si128((const __m128i*)&state[0]); - __m128i STATE1 = _mm_loadu_si128((const __m128i*)&state[4]); - - /* Shuffle for SHA-NI format: STATE0=[A,B,E,F], STATE1=[C,D,G,H] */ - __m128i TMP = _mm_shuffle_epi32(STATE0, 0xB1); /* [B,A,F,E] */ - STATE1 = _mm_shuffle_epi32(STATE1, 0x1B); /* [H,G,D,C] */ - STATE0 = _mm_alignr_epi8(TMP, STATE1, 8); /* [A,B,E,F] */ - STATE1 = _mm_blend_epi16(STATE1, TMP, 0xF0); /* [C,D,G,H] */ - - __m128i ABEF_SAVE = STATE0; - __m128i CDGH_SAVE = STATE1; - - /* Load message */ - __m128i MSG0 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(block + 0)), MASK); - __m128i MSG1 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(block + 16)), MASK); - __m128i MSG2 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(block + 32)), MASK); - __m128i MSG3 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(block + 48)), MASK); - - static const uint32_t K[64] __attribute__((aligned(16))) = { - 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 - }; - - __m128i MSG; - - /* Rounds 0-3 */ - MSG = _mm_add_epi32(MSG0, _mm_load_si128((const __m128i*)&K[0])); - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); - MSG = _mm_shuffle_epi32(MSG, 0x0E); - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); - - /* Rounds 4-7 */ - MSG = _mm_add_epi32(MSG1, _mm_load_si128((const __m128i*)&K[4])); - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); - MSG = _mm_shuffle_epi32(MSG, 0x0E); - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); - MSG0 = _mm_sha256msg1_epu32(MSG0, MSG1); - - /* Rounds 8-11 */ - MSG = _mm_add_epi32(MSG2, _mm_load_si128((const __m128i*)&K[8])); - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); - MSG = _mm_shuffle_epi32(MSG, 0x0E); - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); - MSG1 = _mm_sha256msg1_epu32(MSG1, MSG2); - - /* Rounds 12-15 */ - MSG = _mm_add_epi32(MSG3, _mm_load_si128((const __m128i*)&K[12])); - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); - __m128i TMP2 = _mm_alignr_epi8(MSG3, MSG2, 4); - MSG0 = _mm_add_epi32(MSG0, TMP2); - MSG0 = _mm_sha256msg2_epu32(MSG0, MSG3); - MSG = _mm_shuffle_epi32(MSG, 0x0E); - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); - MSG2 = _mm_sha256msg1_epu32(MSG2, MSG3); - - /* Rounds 16-19 through 60-63 (unrolled loop) */ - #define SHA_ROUND(i, m0, m1, m2, m3) do { \ - MSG = _mm_add_epi32(m0, _mm_load_si128((const __m128i*)&K[i])); \ - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); \ - TMP2 = _mm_alignr_epi8(m0, m3, 4); \ - m1 = _mm_add_epi32(m1, TMP2); \ - m1 = _mm_sha256msg2_epu32(m1, m0); \ - MSG = _mm_shuffle_epi32(MSG, 0x0E); \ - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); \ - m3 = _mm_sha256msg1_epu32(m3, m0); \ - } while(0) - - SHA_ROUND(16, MSG0, MSG1, MSG2, MSG3); - SHA_ROUND(20, MSG1, MSG2, MSG3, MSG0); - SHA_ROUND(24, MSG2, MSG3, MSG0, MSG1); - SHA_ROUND(28, MSG3, MSG0, MSG1, MSG2); - SHA_ROUND(32, MSG0, MSG1, MSG2, MSG3); - SHA_ROUND(36, MSG1, MSG2, MSG3, MSG0); - SHA_ROUND(40, MSG2, MSG3, MSG0, MSG1); - SHA_ROUND(44, MSG3, MSG0, MSG1, MSG2); - SHA_ROUND(48, MSG0, MSG1, MSG2, MSG3); - SHA_ROUND(52, MSG1, MSG2, MSG3, MSG0); - - #undef SHA_ROUND - - /* Rounds 56-59 */ - MSG = _mm_add_epi32(MSG2, _mm_load_si128((const __m128i*)&K[56])); - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); - TMP2 = _mm_alignr_epi8(MSG2, MSG1, 4); - MSG3 = _mm_add_epi32(MSG3, TMP2); - MSG3 = _mm_sha256msg2_epu32(MSG3, MSG2); - MSG = _mm_shuffle_epi32(MSG, 0x0E); - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); - - /* Rounds 60-63 */ - MSG = _mm_add_epi32(MSG3, _mm_load_si128((const __m128i*)&K[60])); - STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); - MSG = _mm_shuffle_epi32(MSG, 0x0E); - STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, MSG); - - /* Add saved state */ - STATE0 = _mm_add_epi32(STATE0, ABEF_SAVE); - STATE1 = _mm_add_epi32(STATE1, CDGH_SAVE); - - /* Unshuffle */ - TMP = _mm_shuffle_epi32(STATE0, 0x1B); /* [F,E,B,A] */ - STATE1 = _mm_shuffle_epi32(STATE1, 0xB1); /* [D,C,H,G] */ - STATE0 = _mm_blend_epi16(TMP, STATE1, 0xF0); /* [A,B,C,D] */ - STATE1 = _mm_alignr_epi8(STATE1, TMP, 8); /* [E,F,G,H] */ - - _mm_storeu_si128((__m128i*)&state[0], STATE0); - _mm_storeu_si128((__m128i*)&state[4], STATE1); -} - -#define SHA256_HW_AVAILABLE 1 - -#elif defined(__aarch64__) && defined(__ARM_FEATURE_CRYPTO) - -#include - -static inline void sha256_transform_hw(uint32_t state[8], const uint8_t block[64]) { - 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 - }; - - /* Load state: ABCD and EFGH */ - uint32x4_t STATE0 = vld1q_u32(&state[0]); - uint32x4_t STATE1 = vld1q_u32(&state[4]); - uint32x4_t ABCD_SAVE = STATE0; - uint32x4_t EFGH_SAVE = STATE1; - - /* Load message with big-endian byte swap */ - uint32x4_t MSG0 = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(block + 0))); - uint32x4_t MSG1 = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(block + 16))); - uint32x4_t MSG2 = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(block + 32))); - uint32x4_t MSG3 = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(block + 48))); - - uint32x4_t TMP0, TMP1, TMP2; - - /* Rounds 0-3 */ - TMP0 = vaddq_u32(MSG0, vld1q_u32(&K[0])); - TMP2 = STATE0; - STATE0 = vsha256hq_u32(STATE0, STATE1, TMP0); - STATE1 = vsha256h2q_u32(STATE1, TMP2, TMP0); - MSG0 = vsha256su0q_u32(MSG0, MSG1); - - /* Rounds 4-7 */ - TMP0 = vaddq_u32(MSG1, vld1q_u32(&K[4])); - TMP2 = STATE0; - STATE0 = vsha256hq_u32(STATE0, STATE1, TMP0); - STATE1 = vsha256h2q_u32(STATE1, TMP2, TMP0); - MSG0 = vsha256su1q_u32(MSG0, MSG2, MSG3); - MSG1 = vsha256su0q_u32(MSG1, MSG2); - - #define ARM_SHA_ROUND(i, m0, m1, m2, m3) do { \ - TMP0 = vaddq_u32(m2, vld1q_u32(&K[i])); \ - TMP2 = STATE0; \ - STATE0 = vsha256hq_u32(STATE0, STATE1, TMP0); \ - STATE1 = vsha256h2q_u32(STATE1, TMP2, TMP0); \ - m1 = vsha256su1q_u32(m1, m3, m0); \ - m2 = vsha256su0q_u32(m2, m3); \ - } while(0) - - ARM_SHA_ROUND( 8, MSG0, MSG1, MSG2, MSG3); - ARM_SHA_ROUND(12, MSG1, MSG2, MSG3, MSG0); - ARM_SHA_ROUND(16, MSG2, MSG3, MSG0, MSG1); - ARM_SHA_ROUND(20, MSG3, MSG0, MSG1, MSG2); - ARM_SHA_ROUND(24, MSG0, MSG1, MSG2, MSG3); - ARM_SHA_ROUND(28, MSG1, MSG2, MSG3, MSG0); - ARM_SHA_ROUND(32, MSG2, MSG3, MSG0, MSG1); - ARM_SHA_ROUND(36, MSG3, MSG0, MSG1, MSG2); - ARM_SHA_ROUND(40, MSG0, MSG1, MSG2, MSG3); - ARM_SHA_ROUND(44, MSG1, MSG2, MSG3, MSG0); - ARM_SHA_ROUND(48, MSG2, MSG3, MSG0, MSG1); - - #undef ARM_SHA_ROUND - - /* Rounds 52-55 */ - TMP0 = vaddq_u32(MSG3, vld1q_u32(&K[52])); - TMP2 = STATE0; - STATE0 = vsha256hq_u32(STATE0, STATE1, TMP0); - STATE1 = vsha256h2q_u32(STATE1, TMP2, TMP0); - MSG0 = vsha256su1q_u32(MSG0, MSG2, MSG3); - - /* Rounds 56-59 */ - TMP0 = vaddq_u32(MSG0, vld1q_u32(&K[56])); - TMP2 = STATE0; - STATE0 = vsha256hq_u32(STATE0, STATE1, TMP0); - STATE1 = vsha256h2q_u32(STATE1, TMP2, TMP0); - - /* Rounds 60-63 */ - TMP0 = vaddq_u32(MSG1, vld1q_u32(&K[60])); - TMP2 = STATE0; - STATE0 = vsha256hq_u32(STATE0, STATE1, TMP0); - STATE1 = vsha256h2q_u32(STATE1, TMP2, TMP0); - - /* Add saved state */ - STATE0 = vaddq_u32(STATE0, ABCD_SAVE); - STATE1 = vaddq_u32(STATE1, EFGH_SAVE); - - vst1q_u32(&state[0], STATE0); - vst1q_u32(&state[4], STATE1); -} - -#define SHA256_HW_AVAILABLE 1 - -#else -#define SHA256_HW_AVAILABLE 0 -#endif - -#endif /* SECP256K1_SHA256_HW_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 deleted file mode 100644 index b04bc7da0..000000000 --- a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.native.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -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 - } -}