refactor(quartz): migrate in-tree C secp256k1 to libschnorr256k1-kmp

The custom C secp256k1 implementation under quartz/src/main/c/ was never
wired into Gradle (no externalNativeBuild block) and only existed for the
3-way benchmark + cross-validation test against ACINQ on JVM/Android.
The C library has been split out to vitorpamplona/libschnorr256k1{,-kmp},
so the in-repo copy is dead weight.

This change replaces it with the published Maven Central artifact
`com.vitorpamplona:schnorr256k1-kmp:1.0.0`, scoped to test/benchmark
configurations only — production crypto continues to use ACINQ
secp256k1-kmp on JVM/Android and the pure-Kotlin Secp256k1 on native.

The Android AAR ships libschnorr256k1_jni.so for arm64-v8a and x86_64,
so the Android benchmark now exercises the C row automatically (no more
manual build_android.sh). On JVM the .so still has to be installed by the
developer; the cross-validation test and triple-benchmark gracefully skip
the C row when System.loadLibrary fails, matching prior behavior.

Verified on JVM: all 13 secp256k1 jvmTest classes pass (188 tests), and
ACINQ vs pure-Kotlin benchmark numbers match the documented baseline
within sandbox noise (verifySchnorr ~15k ops/s, signSchnorr cached
~33k ops/s, pubkeyCreate+Compress ~36k ops/s).

Removes ~5,100 LOC: quartz/src/main/c/ (4,500), Secp256k1InstanceC.*
expect/actual shim (560), Secp256k1C JNI declaration object.

https://claude.ai/code/session_01KnvpK2amcVZKfFiZJvHjVe
This commit is contained in:
Claude
2026-04-28 15:36:25 +00:00
parent bc0179aac6
commit 2ad1a48123
30 changed files with 60 additions and 5114 deletions
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.quartz.utils.secp256k1
import com.vitorpamplona.quartz.utils.Secp256k1InstanceC
import com.vitorpamplona.schnorr256k1.Schnorr256k1
import java.util.Random
import kotlin.test.Test
import kotlin.test.assertContentEquals
@@ -32,7 +32,7 @@ import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1
* available on JVM:
* 1. ACINQ / libsecp256k1-kmp (reference implementation)
* 2. Pure Kotlin ([Secp256k1])
* 3. Custom C library via JNI ([Secp256k1InstanceC]) — when loadable
* 3. Custom C library via JNI ([Schnorr256k1] from libschnorr256k1-kmp) — when loadable
*
* BIP-340 signatures are deterministic given (privkey, message, aux_rand),
* so two correct implementations MUST produce identical signature bytes.
@@ -124,15 +124,17 @@ class Secp256k1CrossValidationTest {
@Test
fun customCImplementationMatchesAcinqAndKotlin() {
// Probe libschnorr256k1's native lib before exercising it: on platforms
// where the JNI .so isn't packaged we skip rather than fail the test.
val cAvailable =
try {
Secp256k1InstanceC.init()
Schnorr256k1.seckeyVerify(ByteArray(32) { 1 })
true
} catch (e: UnsatisfiedLinkError) {
println("SKIP: custom C library not loadable (${e.message})")
println("SKIP: libschnorr256k1 native lib not loadable (${e.message})")
false
} catch (e: Throwable) {
println("SKIP: custom C library init failed (${e.message})")
println("SKIP: libschnorr256k1 init failed (${e.message})")
false
}
if (!cAvailable) return
@@ -146,13 +148,13 @@ class Secp256k1CrossValidationTest {
val kotlinPubCompressed = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey))
val acinqPubCompressed = acinq.pubKeyCompress(acinq.pubkeyCreate(seckey))
val cPubCompressed = Secp256k1InstanceC.compressedPubKeyFor(seckey)
val cPubCompressed = Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(seckey)!!)!!
assertContentEquals(acinqPubCompressed, kotlinPubCompressed, "pubkey: Kotlin vs ACINQ")
assertContentEquals(acinqPubCompressed, cPubCompressed, "pubkey: C vs ACINQ")
val kotlinSig = Secp256k1.signSchnorr(msg, seckey, aux)
val acinqSig = acinq.signSchnorr(msg, seckey, aux)
val cSig = Secp256k1InstanceC.signSchnorr(msg, seckey, aux)
val cSig = Schnorr256k1.schnorrSign(msg, seckey, aux)!!
assertContentEquals(acinqSig, kotlinSig, "sig: Kotlin vs ACINQ")
assertContentEquals(acinqSig, cSig, "sig: C vs ACINQ")
}
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.quartz.utils.secp256k1
import com.vitorpamplona.quartz.utils.Secp256k1InstanceC
import com.vitorpamplona.schnorr256k1.Schnorr256k1
import kotlin.test.Test
import kotlin.test.assertTrue
import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1
@@ -29,7 +29,7 @@ import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1
* Three-way benchmark comparing:
* 1. ACINQ C (libsecp256k1 via JNI) — the established reference
* 2. Pure Kotlin (our KMP implementation) — portable, no native deps
* 3. Custom C (our new C implementation via JNI) — maximum performance target
* 3. Custom C (libschnorr256k1 via JNI) — maximum performance target
*
* Run with: ./gradlew :quartz:jvmTest --tests "*.Secp256k1TripleBenchmark"
*/
@@ -112,22 +112,21 @@ class Secp256k1TripleBenchmark {
// Verify signature compatibility
assertTrue(acinqSig.contentEquals(kotlinSig), "ACINQ and Kotlin signatures must match")
// Try to load C library (may not be available in all environments)
// Probe libschnorr256k1's native lib — skip the C row if the JNI .so
// isn't packaged for this platform (e.g. obscure JVM target).
var cAvailable = false
try {
Secp256k1InstanceC.init()
Schnorr256k1.seckeyVerify(ByteArray(32) { 1 })
cAvailable = true
} catch (e: UnsatisfiedLinkError) {
println("NOTE: Custom C library not available (${e.message})")
println(" Build it with: cd quartz/src/main/c/secp256k1 && mkdir build && cd build && cmake .. && make")
println(" Then add build/ to java.library.path")
println("NOTE: libschnorr256k1 native lib not available (${e.message})")
}
val cPubKey = if (cAvailable) Secp256k1InstanceC.compressedPubKeyFor(privKey) else null
val cPubKey = if (cAvailable) Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!)!! else null
val cXOnlyPub = cPubKey?.copyOfRange(1, 33)
val cSig =
if (cAvailable) {
Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand)
Schnorr256k1.schnorrSign(msg32, privKey, auxRand)!!
} else {
null
}
@@ -139,7 +138,7 @@ class Secp256k1TripleBenchmark {
"ACINQ should verify C signature",
)
assertTrue(
Secp256k1InstanceC.verifySchnorr(acinqSig, msg32, acinqXOnlyPub),
Schnorr256k1.schnorrVerify(acinqSig, msg32, acinqXOnlyPub),
"C should verify ACINQ signature",
)
}
@@ -156,7 +155,7 @@ class Secp256k1TripleBenchmark {
kotlinOp = { Secp256k1.verifySchnorrFast(kotlinSig, msg32, kotlinXOnlyPub) },
cOp =
if (cAvailable && cSig != null && cXOnlyPub != null) {
{ Secp256k1InstanceC.verifySchnorrFast(cSig, msg32, cXOnlyPub) }
{ Schnorr256k1.schnorrVerifyFast(cSig, msg32, cXOnlyPub) }
} else {
null
},
@@ -172,7 +171,7 @@ class Secp256k1TripleBenchmark {
kotlinOp = { Secp256k1.verifySchnorr(kotlinSig, msg32, kotlinXOnlyPub) },
cOp =
if (cAvailable && cSig != null && cXOnlyPub != null) {
{ Secp256k1InstanceC.verifySchnorr(cSig, msg32, cXOnlyPub) }
{ Schnorr256k1.schnorrVerify(cSig, msg32, cXOnlyPub) }
} else {
null
},
@@ -188,7 +187,7 @@ class Secp256k1TripleBenchmark {
kotlinOp = { Secp256k1.signSchnorrWithXOnlyPubKey(msg32, privKey, kotlinXOnlyPub, auxRand) },
cOp =
if (cAvailable && cXOnlyPub != null) {
{ Secp256k1InstanceC.signSchnorrWithXOnlyPubKey(msg32, privKey, cXOnlyPub, auxRand) }
{ Schnorr256k1.schnorrSignXOnly(msg32, privKey, cXOnlyPub, auxRand) }
} else {
null
},
@@ -204,7 +203,7 @@ class Secp256k1TripleBenchmark {
kotlinOp = { Secp256k1.signSchnorr(msg32, privKey, auxRand) },
cOp =
if (cAvailable) {
{ Secp256k1InstanceC.signSchnorr(msg32, privKey, auxRand) }
{ Schnorr256k1.schnorrSign(msg32, privKey, auxRand) }
} else {
null
},
@@ -220,7 +219,7 @@ class Secp256k1TripleBenchmark {
kotlinOp = { Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(privKey)) },
cOp =
if (cAvailable) {
{ Secp256k1InstanceC.compressedPubKeyFor(privKey) }
{ Schnorr256k1.pubkeyCompress(Schnorr256k1.pubkeyCreate(privKey)!!) }
} else {
null
},
@@ -236,7 +235,7 @@ class Secp256k1TripleBenchmark {
kotlinOp = { Secp256k1.ecdhXOnly(pub2xOnly, privKey) },
cOp =
if (cAvailable) {
{ Secp256k1InstanceC.ecdhXOnly(pub2xOnly, privKey) }
{ Schnorr256k1.ecdhXOnly(pub2xOnly, privKey) }
} else {
null
},
@@ -298,9 +297,9 @@ class Secp256k1TripleBenchmark {
// Time C batch (if available)
var cBatchEvSec = 0L
if (cAvailable) {
repeat(500) { Secp256k1InstanceC.verifySchnorrBatch(batchPub, sigs, msgs) }
repeat(500) { Schnorr256k1.schnorrVerifyBatch(batchPub, sigs, msgs) }
val cBatchStart = System.nanoTime()
repeat(iters) { Secp256k1InstanceC.verifySchnorrBatch(batchPub, sigs, msgs) }
repeat(iters) { Schnorr256k1.schnorrVerifyBatch(batchPub, sigs, msgs) }
val cBatchNs = System.nanoTime() - cBatchStart
cBatchEvSec = iters.toLong() * batchSize * 1_000_000_000L / cBatchNs
}