Merge pull request #2627 from vitorpamplona/claude/review-libsecp-migration-Ql9hH

Remove custom C secp256k1 implementation, migrate to libschnorr256k1
This commit is contained in:
Vitor Pamplona
2026-04-28 11:42:45 -04:00
committed by GitHub
30 changed files with 60 additions and 5114 deletions
@@ -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/`)
+3
View File
@@ -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
@@ -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)
}
}
+2
View File
@@ -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" }
+6
View File
@@ -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)
}
}
@@ -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<ByteArray>,
msgs: Array<ByteArray>,
): 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<ByteArray>,
messages: List<ByteArray>,
): 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")
}
}
@@ -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<ByteArray>,
messages: List<ByteArray>,
): Boolean
fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray
fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray
fun ecdhXOnly(
xOnlyPub: ByteArray,
scalar: ByteArray,
): ByteArray
}
@@ -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<ByteArray>,
msgs: Array<ByteArray>,
): 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?
}
@@ -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<ByteArray>,
messages: List<ByteArray>,
): 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")
}
}
@@ -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")
}
@@ -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
}
-6
View File
@@ -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)
@@ -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()
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ==================== 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;
}
-277
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ==================== 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;
}
@@ -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"
-411
View File
@@ -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 <string.h>
#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;
}
-332
View File
@@ -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 */
-369
View File
@@ -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 */
-350
View File
@@ -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 <jni.h>
#include "secp256k1_c.h"
#include "sha256.h"
#include <string.h>
#include <stdlib.h>
#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);
}
-779
View File
@@ -1,779 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Point operations on secp256k1 with comb, GLV+wNAF, Strauss/Shamir.
*/
#include "point.h"
#include <string.h>
#include <stdlib.h>
/* ==================== 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);
}
}
}
-84
View File
@@ -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 */
-269
View File
@@ -1,269 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
* Scalar arithmetic mod n with GLV decomposition and wNAF encoding.
*/
#include "scalar.h"
#include <string.h>
/* 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;
}
-75
View File
@@ -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 */
-489
View File
@@ -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 <string.h>
#include <stdlib.h>
/* ==================== 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;
}
-161
View File
@@ -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 <stdint.h>
#include <stddef.h>
#include <string.h>
/*
* 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 */
-164
View File
@@ -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 <string.h>
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);
}
-33
View File
@@ -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 <stdint.h>
#include <stddef.h>
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 */
-249
View File
@@ -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 <stdint.h>
#include <string.h>
/* ==================== x86_64 SHA-NI ==================== */
#if defined(__x86_64__) && defined(__SHA__)
#include <immintrin.h>
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 <arm_neon.h>
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 */
@@ -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<ByteArray>,
messages: List<ByteArray>,
): 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
}
}