From 40a51d855c8ca72467d8f7ec7b415b443a2c01ca Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 14:40:31 +0000 Subject: [PATCH] feat: add Android cross-compile script for phone benchmarking Add build_android.sh that cross-compiles the C secp256k1 library for ARM64 using the Android NDK and places the .so in benchmark/src/main/ jniLibs/ where the benchmark APK will package it. Usage: cd quartz/src/main/c/secp256k1 ./build_android.sh cd ../../../../.. ./gradlew :benchmark:connectedAndroidTest Also: - Fix Secp256k1InstanceC.android.kt to use Secp256k1C JNI binding class (same pattern as JVM) so the JNI method names match Java_com_vitorpamplona_quartz_utils_Secp256k1C_native* - Add benchmark/src/main/jniLibs/ to .gitignore https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY --- .gitignore | 1 + .../utils/Secp256k1InstanceC.android.kt | 79 ++++++++++--------- quartz/src/main/c/secp256k1/CMakeLists.txt | 21 +++-- quartz/src/main/c/secp256k1/build_android.sh | 78 ++++++++++++++++++ 4 files changed, 131 insertions(+), 48 deletions(-) create mode 100755 quartz/src/main/c/secp256k1/build_android.sh diff --git a/.gitignore b/.gitignore index 81dfb17d9..aa8cf0a66 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,4 @@ desktopApp/src/jvmMain/appResources/windows/ # Git worktrees .worktrees/ .claude/worktrees/ +benchmark/src/main/jniLibs/ diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt index 1a01460c8..adc8cb42d 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceC.android.kt @@ -20,10 +20,15 @@ */ package com.vitorpamplona.quartz.utils -actual object Secp256k1InstanceC { +/** + * 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 - private fun ensureLoaded() { + fun ensureLoaded() { if (!loaded) { System.loadLibrary("secp256k1_amethyst_jni") nativeInit() @@ -31,71 +36,73 @@ actual object Secp256k1InstanceC { } } - private external fun nativeInit() + @JvmStatic external fun nativeInit() - private external fun nativePubkeyCreate(seckey: ByteArray): ByteArray? + @JvmStatic external fun nativePubkeyCreate(seckey: ByteArray): ByteArray? - private external fun nativePubkeyCompress(pubkey: ByteArray): ByteArray? + @JvmStatic external fun nativePubkeyCompress(pubkey: ByteArray): ByteArray? - private external fun nativeSecKeyVerify(seckey: ByteArray): Boolean + @JvmStatic external fun nativeSecKeyVerify(seckey: ByteArray): Boolean - private external fun nativeSchnorrSign( + @JvmStatic external fun nativeSchnorrSign( msg: ByteArray, seckey: ByteArray, auxrand: ByteArray?, ): ByteArray? - private external fun nativeSchnorrSignXOnly( + @JvmStatic external fun nativeSchnorrSignXOnly( msg: ByteArray, seckey: ByteArray, xonlyPub: ByteArray, auxrand: ByteArray?, ): ByteArray? - private external fun nativeSchnorrVerify( + @JvmStatic external fun nativeSchnorrVerify( sig: ByteArray, msg: ByteArray, pub: ByteArray, ): Boolean - private external fun nativeSchnorrVerifyFast( + @JvmStatic external fun nativeSchnorrVerifyFast( sig: ByteArray, msg: ByteArray, pub: ByteArray, ): Boolean - private external fun nativeSchnorrVerifyBatch( + @JvmStatic external fun nativeSchnorrVerifyBatch( pub: ByteArray, sigs: Array, msgs: Array, ): Boolean - private external fun nativePrivKeyTweakAdd( + @JvmStatic external fun nativePrivKeyTweakAdd( seckey: ByteArray, tweak: ByteArray, ): ByteArray? - private external fun nativePubKeyTweakMul( + @JvmStatic external fun nativePubKeyTweakMul( pubkey: ByteArray, tweak: ByteArray, ): ByteArray? - private external fun nativeEcdhXOnly( + @JvmStatic external fun nativeEcdhXOnly( xonlyPub: ByteArray, scalar: ByteArray, ): ByteArray? +} - actual fun init() = ensureLoaded() +actual object Secp256k1InstanceC { + actual fun init() = Secp256k1C.ensureLoaded() actual fun compressedPubKeyFor(privKey: ByteArray): ByteArray { - ensureLoaded() - val pub65 = nativePubkeyCreate(privKey) ?: error("Invalid private key") - return nativePubkeyCompress(pub65) ?: error("Compression failed") + Secp256k1C.ensureLoaded() + val pub65 = Secp256k1C.nativePubkeyCreate(privKey) ?: error("Invalid private key") + return Secp256k1C.nativePubkeyCompress(pub65) ?: error("Compression failed") } actual fun isPrivateKeyValid(il: ByteArray): Boolean { - ensureLoaded() - return nativeSecKeyVerify(il) + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSecKeyVerify(il) } actual fun signSchnorr( @@ -103,8 +110,8 @@ actual object Secp256k1InstanceC { privKey: ByteArray, nonce: ByteArray?, ): ByteArray { - ensureLoaded() - return nativeSchnorrSign(data, privKey, nonce) ?: error("Sign failed") + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrSign(data, privKey, nonce) ?: error("Sign failed") } actual fun signSchnorrWithXOnlyPubKey( @@ -113,8 +120,8 @@ actual object Secp256k1InstanceC { xOnlyPubKey: ByteArray, nonce: ByteArray?, ): ByteArray { - ensureLoaded() - return nativeSchnorrSignXOnly(data, privKey, xOnlyPubKey, nonce) ?: error("Sign failed") + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrSignXOnly(data, privKey, xOnlyPubKey, nonce) ?: error("Sign failed") } actual fun verifySchnorr( @@ -122,8 +129,8 @@ actual object Secp256k1InstanceC { hash: ByteArray, pubKey: ByteArray, ): Boolean { - ensureLoaded() - return nativeSchnorrVerify(signature, hash, pubKey) + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrVerify(signature, hash, pubKey) } actual fun verifySchnorrFast( @@ -131,8 +138,8 @@ actual object Secp256k1InstanceC { hash: ByteArray, pubKey: ByteArray, ): Boolean { - ensureLoaded() - return nativeSchnorrVerifyFast(signature, hash, pubKey) + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrVerifyFast(signature, hash, pubKey) } actual fun verifySchnorrBatch( @@ -140,8 +147,8 @@ actual object Secp256k1InstanceC { signatures: List, messages: List, ): Boolean { - ensureLoaded() - return nativeSchnorrVerifyBatch( + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeSchnorrVerifyBatch( pubKey, signatures.toTypedArray(), messages.toTypedArray(), @@ -152,19 +159,19 @@ actual object Secp256k1InstanceC { first: ByteArray, second: ByteArray, ): ByteArray { - ensureLoaded() - return nativePrivKeyTweakAdd(first, second) ?: error("Tweak add failed") + Secp256k1C.ensureLoaded() + return Secp256k1C.nativePrivKeyTweakAdd(first, second) ?: error("Tweak add failed") } actual fun pubKeyTweakMulCompact( pubKey: ByteArray, privateKey: ByteArray, ): ByteArray { - ensureLoaded() + Secp256k1C.ensureLoaded() val compressedPub = ByteArray(33) compressedPub[0] = 0x02 pubKey.copyInto(compressedPub, 1, 0, 32) - val result = nativePubKeyTweakMul(compressedPub, privateKey) ?: error("Tweak mul failed") + val result = Secp256k1C.nativePubKeyTweakMul(compressedPub, privateKey) ?: error("Tweak mul failed") return result.copyOfRange(1, 33) } @@ -172,7 +179,7 @@ actual object Secp256k1InstanceC { xOnlyPub: ByteArray, scalar: ByteArray, ): ByteArray { - ensureLoaded() - return nativeEcdhXOnly(xOnlyPub, scalar) ?: error("ECDH failed") + Secp256k1C.ensureLoaded() + return Secp256k1C.nativeEcdhXOnly(xOnlyPub, scalar) ?: error("ECDH failed") } } diff --git a/quartz/src/main/c/secp256k1/CMakeLists.txt b/quartz/src/main/c/secp256k1/CMakeLists.txt index 9fcd022aa..08dee3102 100644 --- a/quartz/src/main/c/secp256k1/CMakeLists.txt +++ b/quartz/src/main/c/secp256k1/CMakeLists.txt @@ -39,7 +39,15 @@ target_include_directories(secp256k1_amethyst_jni PUBLIC ${CMAKE_CURRENT_SOURCE_ # ==================== JNI bridge ==================== -if(JNI_INCLUDE_DIR) +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) @@ -47,17 +55,6 @@ if(JNI_INCLUDE_DIR) endif() endif() -# ==================== Android NDK build ==================== - -if(ANDROID) - # Android-specific: build shared library for JNI - find_library(log-lib log) - target_link_libraries(secp256k1_amethyst_jni ${log-lib}) - - # Include JNI headers from NDK - target_sources(secp256k1_amethyst_jni PRIVATE jni_bridge.c) -endif() - # ==================== Standalone benchmark ==================== if(NOT ANDROID) diff --git a/quartz/src/main/c/secp256k1/build_android.sh b/quartz/src/main/c/secp256k1/build_android.sh new file mode 100755 index 000000000..bee92b073 --- /dev/null +++ b/quartz/src/main/c/secp256k1/build_android.sh @@ -0,0 +1,78 @@ +#!/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"