Reputation: 13
I am planning to implement a zero-knowledge proof based geolocation tracking, but unfortunately the zkp are only available as Rust libraries (if you don't know what zero-knowledge proof is, assume it as a library which helps you to prove something, just telling this for context).
The desired behaviour is the .so file to be linked to my Kotlin file via JNI.
Now my Android project structure is:
majorapplication/
|
|-app/
|--src/
|---main/
|----cpp/
|-----CMakeLists.TXT
|-----majorapplication.cpp
|----java/
|----jniLibs/
|-----x86/
|-----x86_64/
|------lib_zkp.so
|------libmajorapplication.so
|-----arm64-v8a/
|-----armeabi-v7a/
|-rustbulletproof(rust project)
|--cargo.toml
|--cbindgen.toml
|--build.rs
|--src/
|---lib.rs(contains the FFI code)
First, I wrote the Rust library with the following configuration:
cargo.toml:
[package]
name = "rustbulletproof"
version = "0.1.0"
edition = "2021"
[dependencies]
bulletproofs = "4.0.0"
curve25519-dalek-ng = "4.1.0"
merlin = "3.0.0"
rand = "0.8.5"
libc = "0.2.169"
geo-types = "0.7.9"
anyhow = "1.0.71"
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3"
[lib]
name = "zkp_proximity"
crate-type = ["cdylib"] # For FFI with Android
[build-dependencies]
cbindgen = "0.26"
cbindgen.toml:
language = "C++"
include_guard = "BULLETPROOFS_GEO_H"
namespace = "bulletproofs_geo"
[defines]
"target_os = android" = "ANDROID"
[export]
prefix = "BPG_"
include = ["ProofData"]
[parse]
parse_deps = true
include = ["bulletproofs", "curve25519_dalek_ng","merlin","rand","libc","geo-types","anyhow","serde","bincode"]
build.rs:
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let config = cbindgen::Config::from_file("cbindgen.toml")
.expect("Unable to find cbindgen.toml");
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_config(config)
.generate()
.expect("Unable to generate bindings")
.write_to_file("include/bulletproofs_geo.h");
}
lib.rs:
use std::mem;
mod distance;
mod prover;
#[repr(C)]
pub struct ProofData {
proof_bytes: *const u8,
proof_len: usize,
ristretto_bytes: *const u8,
ristretto_len: usize,
bp_gens_capacity: usize,
bp_gens_party_capacity: usize,
// We'll serialize these into bytes for FFI
pedersen_bytes: [u8; 64], // 2 compressed points, 32 bytes each
}
#[no_mangle]
pub extern "C" fn start_proof(x1: f64, y1: f64, x2: f64, y2: f64) -> ProofData {
let c1 = geo_types::Coord { x: x1, y: y1 };
let c2 = geo_types::Coord { x: x2, y: y2 };
let mut distance: u64 = 0;
if let Ok(d) = distance::distance_from_coords(&c1, &c2) {
distance = (d * 1000.0) as u64;
}
let range_proof_res_obj = prover::fun_prover(distance);
let (pc_gens, bp_gens, proof_result) = range_proof_res_obj;
match proof_result {
Ok((proof, ristretto)) => {
let proof_bytes = proof.to_bytes();
let ristretto_bytes = bincode::serialize(&ristretto).unwrap();
// Convert Pedersen generators to bytes
let mut pedersen_bytes = [0u8; 64];
pedersen_bytes[..32].copy_from_slice(&pc_gens.B.compress().to_bytes());
pedersen_bytes[32..].copy_from_slice(&pc_gens.B_blinding.compress().to_bytes());
let proof_ptr = proof_bytes.as_ptr();
let proof_len = proof_bytes.len();
let ristretto_ptr = ristretto_bytes.as_ptr();
let ristretto_len = ristretto_bytes.len();
// Prevent deallocation
mem::forget(proof_bytes);
mem::forget(ristretto_bytes);
ProofData {
proof_bytes: proof_ptr,
proof_len,
ristretto_bytes: ristretto_ptr,
ristretto_len,
bp_gens_capacity: bp_gens.gens_capacity,
bp_gens_party_capacity: bp_gens.party_capacity,
pedersen_bytes,
}
}
Err(_) => ProofData {
proof_bytes: std::ptr::null(),
proof_len: 0,
ristretto_bytes: std::ptr::null(),
ristretto_len: 0,
bp_gens_capacity: 0,
bp_gens_party_capacity: 0,
pedersen_bytes: [0u8; 64],
}
}
}
#[no_mangle]
pub extern "C" fn free_proof_data(data: ProofData) {
if !data.proof_bytes.is_null() {
unsafe {
Vec::from_raw_parts(
data.proof_bytes as *mut u8,
data.proof_len,
data.proof_len
);
}
}
if !data.ristretto_bytes.is_null() {
unsafe {
Vec::from_raw_parts(
data.ristretto_bytes as *mut u8,
data.ristretto_len,
data.ristretto_len
);
}
}
}
Now I built the above code to .so files using the following command:
cargo ndk -t x86 -t x86_64 -t arm64-v8a -t armeabi-v7a -o ./../app/src/main/jniLibs build --release
This concludes the Rust part.
Now, using the NDK from Android, I built this with the following configuration:
app/build.gradle.kts:
import com.android.sdklib.devices.Abi
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.majorapplication"
compileSdk = 35
defaultConfig {
applicationId = "com.example.majorapplication"
minSdk = 29
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
sourceSets {
getByName("main") {
jniLibs {
srcDirs("src/main/jniLibs")
}
}
}
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
// Enable debugging for native code
isJniDebuggable = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
ndkVersion = rootProject.extra["ndkVersion"] as String
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.play.services.location)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
CMakeLists.TXT:
cmake_minimum_required(VERSION 3.22.1)
project("majorapplication")
set(RUST_LIB_NAME zkp_proximity)
set(JNI_LIB_NAME majorapplication)
# Set the path relative to CMakeLists.txt location
set(RUST_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
message(STATUS "ANDROID_ABI: ${ANDROID_ABI}")
message(STATUS "Looking for library at: ${RUST_LIB_DIR}/${ANDROID_ABI}/lib${RUST_LIB_NAME}.so")
add_library(${CMAKE_PROJECT_NAME} SHARED
majorapplication.cpp)
# Add Rust library - use consistent naming without 'lib' prefix in target name
add_library(${RUST_LIB_NAME} SHARED IMPORTED)
if(NOT EXISTS "${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so")
message(FATAL_ERROR "Rust library not found at: ${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so")
endif()
set_target_properties(${RUST_LIB_NAME} PROPERTIES
IMPORTED_LOCATION "${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so"
)
#set_target_properties(${RUST_LIB_NAME} PROPERTIES
# IMPORTED_LOCATION ${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so)
# Add JNI library
include_directories(${CMAKE_SOURCE_DIR}/../../../../rustbulletproof/include)
# Link against ${RUST_LIB_NAME} instead of libzkp_proximity
target_link_libraries(
${CMAKE_PROJECT_NAME}
${RUST_LIB_NAME}
android
log)
majorapplication.cpp:
#include <jni.h>
#include <string>
#include <dlfcn.h>
#include <android/log.h>
#include "bulletproofs_geo.h" // Your generated header
extern "C" {
JNIEXPORT jobject JNICALL
Java_com_example_majorapplication_BulletproofsBinding_generateProof(
JNIEnv *env,
jobject /* this */,
jdouble x1,
jdouble y1,
jdouble x2,
jdouble y2) {
// Call the Rust function
bulletproofs_geo::BPG_ProofData proof_data = bulletproofs_geo::start_proof(x1, y1, x2, y2);
// Create byte arrays for proof and commitment
jbyteArray proofBytes = env->NewByteArray(proof_data.proof_len);
jbyteArray ristrettoBytes = env->NewByteArray(proof_data.ristretto_len);
jbyteArray pedersenBytes = env->NewByteArray(64); // Fixed size for Pedersen gens
// Copy data to Java byte arrays
env->SetByteArrayRegion(proofBytes, 0, proof_data.proof_len,
reinterpret_cast<const jbyte*>(proof_data.proof_bytes));
env->SetByteArrayRegion(ristrettoBytes, 0, proof_data.ristretto_len,
reinterpret_cast<const jbyte*>(proof_data.ristretto_bytes));
env->SetByteArrayRegion(pedersenBytes, 0, 64,
reinterpret_cast<const jbyte*>(proof_data.pedersen_bytes));
// Get the ProofResult class
jclass proofResultClass = env->FindClass("com/example/majorapplication/ProofResult");
if (proofResultClass == nullptr) {
return nullptr;
}
// Get the constructor
jmethodID constructor = env->GetMethodID(proofResultClass, "<init>",
"([B[B[BJJ)V");
if (constructor == nullptr) {
return nullptr;
}
// Create and return the ProofResult object
jobject result = env->NewObject(proofResultClass, constructor,
proofBytes,
ristrettoBytes,
pedersenBytes,
(jlong)proof_data.bp_gens_capacity,
(jlong)proof_data.bp_gens_party_capacity);
// Free the native memory
free_proof_data(proof_data);
return result;
}
}
This concludes the JNI part.
Now lets tackle the issue of this post: inside the src directory of the Android app module, I have a class which uses the shared library:
BulletproofBindings.kt:
package com.example.majorapplication
import android.util.Log
class BulletproofsBinding {
companion object {
init {
try {
System.loadLibrary("majorapplication")
} catch (e: UnsatisfiedLinkError) {
Log.e("BulletproofsBinding", "Error loading library: ${e.message}")
throw e
}
}
}
private external fun generateProof(x1: Double, y1: Double, x2: Double, y2: Double): ProofResult?
fun generateProofForCoordinates(
lat1: Double,
lon1: Double,
lat2: Double,
lon2: Double
): ProofResult? {
return generateProof(lat1, lon1, lat2, lon2)
}
}
proofresult.kt:
package com.example.majorapplication
data class ProofResult(
val proofBytes: ByteArray,
val ristrettoBytes: ByteArray,
val pedersenBytes: ByteArray,
val bpGensCapacity: Long,
val bpGensPartyCapacity: Long
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ProofResult
if (!proofBytes.contentEquals(other.proofBytes)) return false
if (!ristrettoBytes.contentEquals(other.ristrettoBytes)) return false
if (!pedersenBytes.contentEquals(other.pedersenBytes)) return false
if (bpGensCapacity != other.bpGensCapacity) return false
if (bpGensPartyCapacity != other.bpGensPartyCapacity) return false
return true
}
override fun hashCode(): Int {
var result = proofBytes.contentHashCode()
result = 31 * result + ristrettoBytes.contentHashCode()
result = 31 * result + pedersenBytes.contentHashCode()
result = 31 * result + bpGensCapacity.hashCode()
result = 31 * result + bpGensPartyCapacity.hashCode()
return result
}
}
Now the issue is, whenever I build the app, it is executing properly, the .so files are copied into the jni libs folders properly, as I analysed through the apk inspector, but when I run it, it is throwing an UnsatisfiedLink error.
Output in logcat when app is built:
BulletproofsBinding com.example.majorapplication E Error loading library: dlopen failed:
library "C:/Users/mahes/AndroidStudioProjects/MajorApplication/app/src/main/cpp/../jniLibs/x86_64/libzkp_proximity.so" not found:
needed by /data/app/~~-72oLmY0_i-Hq9cSQjWwCg==/com.example.majorapplication-qlCmojO1J_C3_1icpRzu4g==/base.apk!/lib/x86_64/libmajorapplication.so
in namespace classloader-namespace
2025-02-23 17:22:44.529 13474-13474 AndroidRuntime com.example.majorapplication D Shutting down VM
2025-02-23 17:22:44.531 13474-13474 AndroidRuntime com.example.majorapplication E FATAL EXCEPTION: main
Process: com.example.majorapplication, PID: 13474
java.lang.UnsatisfiedLinkError: dlopen failed:
library "C:/Users/mahes/AndroidStudioProjects/MajorApplication/app/src/main/cpp/../jniLibs/x86_64/libzkp_proximity.so" not found:
needed by /data/app/~~-72oLmY0_i-Hq9cSQjWwCg==/com.example.majorapplication-qlCmojO1J_C3_1icpRzu4g==/base.apk!/lib/x86_64/libmajorapplication.so
in namespace classloader-namespace
at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
at java.lang.Runtime.loadLibrary0(Runtime.java:998)
at java.lang.System.loadLibrary(System.java:1661)
at com.example.majorapplication.BulletproofsBinding.<clinit>(BulletproofsBinding.kt:9)
at com.example.majorapplication.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:29)
at com.example.majorapplication.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:24)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke
Upvotes: -4
Views: 37