Merge pull request #2627 from vitorpamplona/claude/review-libsecp-migration-Ql9hH
Remove custom C secp256k1 implementation, migrate to libschnorr256k1
This commit is contained in:
@@ -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/`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+22
-32
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-187
@@ -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")
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -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")
|
||||
}
|
||||
|
||||
+17
-18
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
-83
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user