From caf65a4d3f6d5d6dbb62e9642511737056c064c6 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Fri, 8 May 2026 16:30:03 +0200 Subject: [PATCH 1/3] Split voting share nullifier JNI module --- backend-lib/src/main/rust/voting.rs | 34 ++---------------- backend-lib/src/main/rust/voting/helpers.rs | 36 +++++++++++++++++++ .../src/main/rust/voting/share_tracking.rs | 33 +++++++++++++++++ 3 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 backend-lib/src/main/rust/voting/helpers.rs create mode 100644 backend-lib/src/main/rust/voting/share_tracking.rs diff --git a/backend-lib/src/main/rust/voting.rs b/backend-lib/src/main/rust/voting.rs index a0dee9b3c..ee2797be6 100644 --- a/backend-lib/src/main/rust/voting.rs +++ b/backend-lib/src/main/rust/voting.rs @@ -1,7 +1,5 @@ //! JNI bindings for the zcash_voting crate. -use std::ptr; - use anyhow::anyhow; use jni::{ JNIEnv, @@ -10,33 +8,7 @@ use jni::{ }; use zcash_voting as voting; -use crate::utils::{self, catch_unwind, exception::unwrap_exc_or}; - -/// Compute the share reveal nullifier from client-known inputs. -/// -/// Returns the 32-byte nullifier, or throws a RuntimeException and returns null -/// on malformed inputs. -#[unsafe(no_mangle)] -pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_computeShareNullifier< - 'local, ->( - mut env: JNIEnv<'local>, - _: JClass<'local>, - vote_commitment: JByteArray<'local>, - share_index: jint, - blind: JByteArray<'local>, -) -> jbyteArray { - let res = catch_unwind(&mut env, |env| { - let share_index = - u32::try_from(share_index).map_err(|_| anyhow!("shareIndex must be non-negative"))?; - let vote_commitment = utils::java_bytes_to_rust(env, &vote_commitment)?; - let blind = utils::java_bytes_to_rust(env, &blind)?; - - let nullifier = - voting::share_tracking::compute_share_nullifier(&vote_commitment, share_index, &blind) - .map_err(|e| anyhow!("compute_share_nullifier failed: {}", e))?; +use crate::utils::{catch_unwind, exception::unwrap_exc_or}; - Ok(utils::rust_bytes_to_java(env, &nullifier)?.into_raw()) - }); - unwrap_exc_or(&mut env, res, ptr::null_mut()) -} +mod helpers; +mod share_tracking; diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs new file mode 100644 index 000000000..7ea168f34 --- /dev/null +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -0,0 +1,36 @@ +use super::*; + +pub(super) const PROTOCOL_FIELD_BYTES: usize = 32; + +pub(super) fn jint_to_u32(value: jint, field: &str) -> anyhow::Result { + u32::try_from(value).map_err(|_| anyhow!("{field} must be non-negative, got {value}")) +} + +pub(super) fn require_len(bytes: Vec, field: &str, expected: usize) -> anyhow::Result> { + if bytes.len() == expected { + Ok(bytes) + } else { + Err(anyhow!( + "{field} must be exactly {expected} bytes, got {}", + bytes.len() + )) + } +} + +pub(super) fn java_bytes( + env: &mut JNIEnv<'_>, + array: &JByteArray<'_>, + field: &str, +) -> anyhow::Result> { + env.convert_byte_array(array) + .map_err(|e| anyhow!("{field}: failed to read byte array: {e}")) +} + +pub(super) fn java_bytes_exact( + env: &mut JNIEnv<'_>, + array: &JByteArray<'_>, + field: &str, + expected: usize, +) -> anyhow::Result> { + require_len(java_bytes(env, array, field)?, field, expected) +} diff --git a/backend-lib/src/main/rust/voting/share_tracking.rs b/backend-lib/src/main/rust/voting/share_tracking.rs new file mode 100644 index 000000000..72c4f7685 --- /dev/null +++ b/backend-lib/src/main/rust/voting/share_tracking.rs @@ -0,0 +1,33 @@ +use super::helpers::*; +use super::*; + +/// Compute the share reveal nullifier from client-known inputs. +/// +/// Returns the 32-byte nullifier, or throws a RuntimeException and returns null +/// on malformed inputs. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_computeShareNullifier< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + vote_commitment: JByteArray<'local>, + share_index: jint, + blind: JByteArray<'local>, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let nullifier = voting::share_tracking::compute_share_nullifier( + &java_bytes_exact( + env, + &vote_commitment, + "vote_commitment", + PROTOCOL_FIELD_BYTES, + )?, + jint_to_u32(share_index, "share_index")?, + &java_bytes_exact(env, &blind, "blind", PROTOCOL_FIELD_BYTES)?, + ) + .map_err(|e| anyhow!("compute_share_nullifier: {}", e))?; + Ok(env.byte_array_from_slice(&nullifier)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} From 079d80acc4e1af63fd09e5470c26789750ae2080 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Fri, 8 May 2026 16:30:30 +0200 Subject: [PATCH 2/3] Add voting DB round lifecycle JNI --- backend-lib/Cargo.lock | 2 + backend-lib/Cargo.toml | 2 + backend-lib/src/main/rust/voting.rs | 16 ++- backend-lib/src/main/rust/voting/db.rs | 61 ++++++++ backend-lib/src/main/rust/voting/helpers.rs | 36 +++++ backend-lib/src/main/rust/voting/json.rs | 57 ++++++++ backend-lib/src/main/rust/voting/rounds.rs | 150 ++++++++++++++++++++ 7 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 backend-lib/src/main/rust/voting/db.rs create mode 100644 backend-lib/src/main/rust/voting/json.rs create mode 100644 backend-lib/src/main/rust/voting/rounds.rs diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index f704c9866..c3135426c 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -6715,6 +6715,8 @@ dependencies = [ "rust_decimal", "sapling-crypto", "secrecy", + "serde", + "serde_json", "tonic", "tor-rtcompat", "tracing", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index 3ca3b8940..bb624d944 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -54,6 +54,8 @@ anyhow = "1" jni = { version = "0.21", default-features = false } uuid = "1" bitflags = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" # lightwalletd tonic = "0.14" diff --git a/backend-lib/src/main/rust/voting.rs b/backend-lib/src/main/rust/voting.rs index ee2797be6..ce6cd79f0 100644 --- a/backend-lib/src/main/rust/voting.rs +++ b/backend-lib/src/main/rust/voting.rs @@ -3,12 +3,22 @@ use anyhow::anyhow; use jni::{ JNIEnv, - objects::{JByteArray, JClass}, - sys::{jbyteArray, jint}, + objects::{JByteArray, JClass, JObject, JString, JValue}, + sys::{JNI_FALSE, JNI_TRUE, jboolean, jbyteArray, jint, jlong, jobject, jstring}, }; +use serde::Serialize; +use std::sync::Arc; use zcash_voting as voting; -use crate::utils::{catch_unwind, exception::unwrap_exc_or}; +use voting::storage::{RoundPhase, RoundState, RoundSummary, VoteRecord, VotingDb}; +use voting::types::VotingError; +use crate::utils::{ + catch_unwind, exception::unwrap_exc_or, java_nullable_string_to_rust, java_string_to_rust, +}; + +mod db; mod helpers; +mod json; +mod rounds; mod share_tracking; diff --git a/backend-lib/src/main/rust/voting/db.rs b/backend-lib/src/main/rust/voting/db.rs new file mode 100644 index 000000000..e1b4ff53d --- /dev/null +++ b/backend-lib/src/main/rust/voting/db.rs @@ -0,0 +1,61 @@ +use super::*; + +pub(super) struct VotingDatabaseHandle { + pub(super) db: Arc, +} + +pub(super) fn handle_from_jlong(handle: jlong) -> anyhow::Result<&'static VotingDatabaseHandle> { + if handle == 0 { + return Err(anyhow!("VotingDatabaseHandle is null")); + } + + // SAFETY: The pointer is allocated by openVotingDb with Box::into_raw and + // remains valid until closeVotingDb receives the same handle. + Ok(unsafe { &*(handle as *const VotingDatabaseHandle) }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_openVotingDb< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_path: JString<'local>, +) -> jlong { + let res = catch_unwind(&mut env, |env| { + let path = java_string_to_rust(env, &db_path)?; + let db = VotingDb::open(&path).map_err(|e| anyhow!("VotingDb::open failed: {}", e))?; + Ok(Box::into_raw(Box::new(VotingDatabaseHandle { db: Arc::new(db) })) as jlong) + }); + unwrap_exc_or(&mut env, res, 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_closeVotingDb< + 'local, +>( + mut _env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, +) { + if db_handle != 0 { + // SAFETY: The handle must be a pointer returned by openVotingDb. + unsafe { drop(Box::from_raw(db_handle as *mut VotingDatabaseHandle)) }; + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_setWalletId<'local>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + wallet_id: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let id = java_string_to_rust(env, &wallet_id)?; + handle.db.set_wallet_id(&id); + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index 7ea168f34..fa308cfac 100644 --- a/backend-lib/src/main/rust/voting/helpers.rs +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -1,3 +1,4 @@ +use super::json::round_phase_to_u32; use super::*; pub(super) const PROTOCOL_FIELD_BYTES: usize = 32; @@ -6,6 +7,10 @@ pub(super) fn jint_to_u32(value: jint, field: &str) -> anyhow::Result { u32::try_from(value).map_err(|_| anyhow!("{field} must be non-negative, got {value}")) } +pub(super) fn jlong_to_u64(value: jlong, field: &str) -> anyhow::Result { + u64::try_from(value).map_err(|_| anyhow!("{field} must be non-negative, got {value}")) +} + pub(super) fn require_len(bytes: Vec, field: &str, expected: usize) -> anyhow::Result> { if bytes.len() == expected { Ok(bytes) @@ -34,3 +39,34 @@ pub(super) fn java_bytes_exact( ) -> anyhow::Result> { require_len(java_bytes(env, array, field)?, field, expected) } + +pub(super) fn make_ffi_round_state<'local>( + env: &mut JNIEnv<'local>, + state: RoundState, +) -> anyhow::Result { + let phase = round_phase_to_u32(state.phase) as i32; + let class = env.find_class("cash/z/ecc/android/sdk/internal/model/voting/FfiRoundState")?; + let round_id_obj: JObject<'local> = env.new_string(&state.round_id)?.into(); + let hotkey_obj: JObject<'local> = match &state.hotkey_address { + Some(a) => env.new_string(a)?.into(), + None => JObject::null(), + }; + let long_class = env.find_class("java/lang/Long")?; + let weight_obj: JObject<'local> = match state.delegated_weight { + Some(w) => env.new_object(&long_class, "(J)V", &[JValue::Long(w as i64)])?, + None => JObject::null(), + }; + let obj = env.new_object( + &class, + "(Ljava/lang/String;IJLjava/lang/String;Ljava/lang/Long;Z)V", + &[ + JValue::Object(&round_id_obj), + JValue::Int(phase), + JValue::Long(state.snapshot_height as i64), + JValue::Object(&hotkey_obj), + JValue::Object(&weight_obj), + JValue::Bool(state.proof_generated as jboolean), + ], + )?; + Ok(obj.into_raw()) +} diff --git a/backend-lib/src/main/rust/voting/json.rs b/backend-lib/src/main/rust/voting/json.rs new file mode 100644 index 000000000..c42f2d287 --- /dev/null +++ b/backend-lib/src/main/rust/voting/json.rs @@ -0,0 +1,57 @@ +use super::*; + +#[derive(Serialize)] +pub(super) struct JsonRoundSummary { + pub(super) round_id: String, + pub(super) phase: u32, + pub(super) snapshot_height: u64, + pub(super) created_at: u64, +} + +impl From for JsonRoundSummary { + fn from(round: RoundSummary) -> Self { + JsonRoundSummary { + round_id: round.round_id, + phase: round_phase_to_u32(round.phase), + snapshot_height: round.snapshot_height, + created_at: round.created_at, + } + } +} + +pub(super) fn round_phase_to_u32(phase: RoundPhase) -> u32 { + match phase { + RoundPhase::Initialized => 0, + RoundPhase::HotkeyGenerated => 1, + RoundPhase::DelegationConstructed => 2, + RoundPhase::DelegationProved => 3, + RoundPhase::VoteReady => 4, + } +} + +#[derive(Serialize)] +pub(super) struct JsonVoteRecord { + pub(super) proposal_id: u32, + pub(super) bundle_index: u32, + pub(super) choice: u32, + pub(super) submitted: bool, +} + +impl From for JsonVoteRecord { + fn from(record: VoteRecord) -> Self { + JsonVoteRecord { + proposal_id: record.proposal_id, + bundle_index: record.bundle_index, + choice: record.choice, + submitted: record.submitted, + } + } +} + +pub(super) fn json_to_jstring( + env: &mut JNIEnv<'_>, + value: &T, +) -> anyhow::Result { + let s = serde_json::to_string(value).map_err(|e| anyhow!("JSON serialization error: {}", e))?; + Ok(env.new_string(s)?.into_raw()) +} diff --git a/backend-lib/src/main/rust/voting/rounds.rs b/backend-lib/src/main/rust/voting/rounds.rs new file mode 100644 index 000000000..8f46c34d0 --- /dev/null +++ b/backend-lib/src/main/rust/voting/rounds.rs @@ -0,0 +1,150 @@ +use super::db::*; +use super::helpers::*; +use super::json::*; +use super::*; + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_initRound<'local>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + snapshot_height: jlong, + ea_pk: JByteArray<'local>, + nc_root: JByteArray<'local>, + nullifier_imt_root: JByteArray<'local>, + session_json: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let params = voting::types::VotingRoundParams { + vote_round_id: java_string_to_rust(env, &round_id)?, + snapshot_height: jlong_to_u64(snapshot_height, "snapshot_height")?, + ea_pk: java_bytes_exact(env, &ea_pk, "ea_pk", PROTOCOL_FIELD_BYTES)?, + nc_root: java_bytes_exact(env, &nc_root, "nc_root", PROTOCOL_FIELD_BYTES)?, + nullifier_imt_root: java_bytes_exact( + env, + &nullifier_imt_root, + "nullifier_imt_root", + PROTOCOL_FIELD_BYTES, + )?, + }; + let session = java_nullable_string_to_rust(env, &session_json)?; + handle + .db + .init_round(¶ms, session.as_deref()) + .map_err(|e| anyhow!("init_round: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getRoundState< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + match handle + .db + .get_round_state(&java_string_to_rust(env, &round_id)?) + { + Ok(state) => make_ffi_round_state(env, state), + Err(VotingError::InvalidInput { .. }) => Ok(JObject::null().into_raw()), + Err(e) => Err(anyhow!("get_round_state: {}", e)), + } + }); + unwrap_exc_or(&mut env, res, JObject::null().into_raw()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_listRoundsJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let rounds: Vec = handle + .db + .list_rounds() + .map_err(|e| anyhow!("list_rounds: {}", e))? + .into_iter() + .map(JsonRoundSummary::from) + .collect(); + json_to_jstring(env, &rounds) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getVotesJson< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let votes: Vec = handle + .db + .get_votes(&java_string_to_rust(env, &round_id)?) + .map_err(|e| anyhow!("get_votes: {}", e))? + .into_iter() + .map(JsonVoteRecord::from) + .collect(); + json_to_jstring(env, &votes) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_clearRound<'local>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jboolean { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + handle + .db + .clear_round(&java_string_to_rust(env, &round_id)?) + .map_err(|e| anyhow!("clear_round: {}", e))?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&mut env, res, JNI_FALSE) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_deleteSkippedBundles< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, + keep_count: jint, +) -> jlong { + let res = catch_unwind(&mut env, |env| { + let handle = handle_from_jlong(db_handle)?; + let deleted_rows = handle + .db + .delete_skipped_bundles( + &java_string_to_rust(env, &round_id)?, + jint_to_u32(keep_count, "keep_count")?, + ) + .map_err(|e| anyhow!("delete_skipped_bundles: {}", e))?; + Ok(deleted_rows as jlong) + }); + unwrap_exc_or(&mut env, res, -1) +} From 7171d4a3b61cb0cb5e6df44cf10f8a8db73c6a01 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Fri, 8 May 2026 16:30:41 +0200 Subject: [PATCH 3/3] Add internal voting round lifecycle wrappers --- .../sdk/internal/TypesafeVotingBackend.kt | 46 +++++++++ .../sdk/internal/TypesafeVotingBackendImpl.kt | 99 +++++++++++++++++++ .../sdk/internal/jni/VotingRustBackend.kt | 45 +++++++++ .../internal/model/voting/FfiVotingModels.kt | 30 ++++++ 4 files changed, 220 insertions(+) create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt new file mode 100644 index 000000000..4a8ceda4a --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt @@ -0,0 +1,46 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState + +@Suppress("TooManyFunctions", "LongParameterList") +interface TypesafeVotingBackend { + suspend fun openVotingDb(dbPath: String): Long + + /** + * Must be called exactly once for [dbHandle]; using [dbHandle] after close is undefined behavior. + */ + suspend fun closeVotingDb(dbHandle: Long) + + suspend fun setWalletId(dbHandle: Long, walletId: String) + + suspend fun initRound( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) + + suspend fun getRoundState(dbHandle: Long, roundId: String): FfiRoundState? + + suspend fun listRoundsJson(dbHandle: Long): String + + suspend fun getVotes(dbHandle: Long, roundId: String): List + + suspend fun clearRound(dbHandle: Long, roundId: String) + + suspend fun deleteSkippedBundles( + dbHandle: Long, + roundId: String, + keepCount: Int + ): Long +} + +data class VoteRecord( + val proposalId: Int, + val bundleIndex: Int, + val choice: Int, + val submitted: Boolean +) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt new file mode 100644 index 000000000..e645ecfe2 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt @@ -0,0 +1,99 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.internal.jni.RustBackend +import cash.z.ecc.android.sdk.internal.jni.VotingRustBackend +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONArray + +@Suppress("TooManyFunctions", "LongParameterList") +class TypesafeVotingBackendImpl : TypesafeVotingBackend { + override suspend fun openVotingDb(dbPath: String): Long = + io { + RustBackend.loadLibrary() + VotingRustBackend.openVotingDb(dbPath).also { dbHandle -> + check(dbHandle != 0L) { + "openVotingDb failed for dbPath=$dbPath" + } + } + } + + override suspend fun closeVotingDb(dbHandle: Long) = + io { VotingRustBackend.closeVotingDb(dbHandle) } + + override suspend fun setWalletId(dbHandle: Long, walletId: String) = + io { + check(VotingRustBackend.setWalletId(dbHandle, walletId)) { + "setWalletId failed" + } + } + + override suspend fun initRound( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) = io { + check( + VotingRustBackend.initRound( + dbHandle, + roundId, + snapshotHeight, + eaPK, + ncRoot, + nullifierIMTRoot, + sessionJson + ) + ) { + "initRound failed for roundId=$roundId" + } + } + + override suspend fun getRoundState(dbHandle: Long, roundId: String): FfiRoundState? = + io { VotingRustBackend.getRoundState(dbHandle, roundId) } + + override suspend fun listRoundsJson(dbHandle: Long): String = + io { VotingRustBackend.listRoundsJson(dbHandle) } + + override suspend fun getVotes(dbHandle: Long, roundId: String): List = + io { + val arr = JSONArray(VotingRustBackend.getVotesJson(dbHandle, roundId)) + (0 until arr.length()).map { index -> + val obj = arr.getJSONObject(index) + VoteRecord( + proposalId = obj.getInt("proposal_id"), + bundleIndex = obj.getInt("bundle_index"), + choice = obj.getInt("choice"), + submitted = obj.getBoolean("submitted") + ) + } + } + + override suspend fun clearRound(dbHandle: Long, roundId: String) = + io { + check(VotingRustBackend.clearRound(dbHandle, roundId)) { + "clearRound failed for roundId=$roundId" + } + } + + override suspend fun deleteSkippedBundles( + dbHandle: Long, + roundId: String, + keepCount: Int + ): Long = + io { + VotingRustBackend + .deleteSkippedBundles(dbHandle, roundId, keepCount) + .also { deletedRows -> + check(deletedRows >= 0) { + "deleteSkippedBundles failed for roundId=$roundId keepCount=$keepCount" + } + } + } + + private suspend fun io(block: suspend () -> T): T = withContext(Dispatchers.IO) { block() } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt index a3ae194f2..4c3cc737b 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt @@ -1,6 +1,51 @@ package cash.z.ecc.android.sdk.internal.jni +import androidx.annotation.Keep +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState + +@Keep +@Suppress("TooManyFunctions", "LongParameterList") internal object VotingRustBackend { + @JvmStatic + external fun openVotingDb(dbPath: String): Long + + /** Must be called exactly once; using [dbHandle] after close is undefined behavior. */ + @JvmStatic + external fun closeVotingDb(dbHandle: Long) + + @JvmStatic + external fun setWalletId(dbHandle: Long, walletId: String): Boolean + + @JvmStatic + external fun initRound( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ): Boolean + + @JvmStatic + external fun getRoundState(dbHandle: Long, roundId: String): FfiRoundState? + + @JvmStatic + external fun listRoundsJson(dbHandle: Long): String + + @JvmStatic + external fun getVotesJson(dbHandle: Long, roundId: String): String + + @JvmStatic + external fun clearRound(dbHandle: Long, roundId: String): Boolean + + @JvmStatic + external fun deleteSkippedBundles( + dbHandle: Long, + roundId: String, + keepCount: Int + ): Long + @JvmStatic external fun computeShareNullifier( voteCommitment: ByteArray, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt new file mode 100644 index 000000000..0e9a128e1 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -0,0 +1,30 @@ +package cash.z.ecc.android.sdk.internal.model.voting + +import androidx.annotation.Keep + +@Keep +data class FfiRoundState( + val roundId: String, + val phase: Int, + val snapshotHeight: Long, + val hotkeyAddress: String?, + val delegatedWeight: Long?, + val proofGenerated: Boolean +) { + val roundPhase: FfiRoundPhase get() = FfiRoundPhase.fromInt(phase) +} + +@Suppress("MagicNumber") +enum class FfiRoundPhase( + val value: Int +) { + INITIALIZED(0), + HOTKEY_GENERATED(1), + DELEGATION_CONSTRUCTED(2), + DELEGATION_PROVED(3), + VOTE_READY(4); + + companion object { + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: INITIALIZED + } +}