From f81ebb06530da2065bee109fa4e2b8b5f3904b57 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Fri, 8 May 2026 16:30:03 +0200 Subject: [PATCH 01/13] Split voting share nullifier JNI module --- backend-lib/src/main/rust/voting.rs | 60 +------------------ backend-lib/src/main/rust/voting/helpers.rs | 58 ++++++++++++++++++ .../src/main/rust/voting/share_tracking.rs | 29 +++++++++ 3 files changed, 90 insertions(+), 57 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 ad798e174..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,59 +8,7 @@ use jni::{ }; use zcash_voting as voting; -use crate::utils::{self, catch_unwind, exception::unwrap_exc_or}; - -const VOTE_COMMITMENT_BYTES: usize = 32; -const BLIND_BYTES: usize = 32; -const SHARE_NULLIFIER_BYTES: usize = 32; - -/// 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_computeShareNullifierNative< - '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 = - java_fixed_bytes::(env, &vote_commitment, "voteCommitment")?; - let blind = java_fixed_bytes::(env, &blind, "blind")?; - - let nullifier = - voting::share_tracking::compute_share_nullifier(&vote_commitment, share_index, &blind) - .map_err(|e| anyhow!("compute_share_nullifier failed: {}", e))?; - let nullifier_len = nullifier.len(); - let nullifier: [u8; SHARE_NULLIFIER_BYTES] = nullifier.try_into().map_err(|_| { - anyhow!( - "shareNullifier must be exactly {} bytes, got {}", - SHARE_NULLIFIER_BYTES, - nullifier_len - ) - })?; - - Ok(utils::rust_bytes_to_java(env, &nullifier)?.into_raw()) - }); - unwrap_exc_or(&mut env, res, ptr::null_mut()) -} - -fn java_fixed_bytes( - env: &JNIEnv<'_>, - array: &JByteArray<'_>, - field: &str, -) -> anyhow::Result<[u8; N]> { - let bytes = utils::java_bytes_to_rust(env, array)?; - let len = bytes.len(); +use crate::utils::{catch_unwind, exception::unwrap_exc_or}; - bytes - .try_into() - .map_err(|_| anyhow!("{field} must be exactly {N} bytes, got {len}")) -} +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..92da62608 --- /dev/null +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -0,0 +1,58 @@ +use super::*; + +pub(super) const PROTOCOL_FIELD_BYTES: usize = 32; +pub(super) const VOTE_COMMITMENT_BYTES: usize = PROTOCOL_FIELD_BYTES; +pub(super) const BLIND_BYTES: usize = PROTOCOL_FIELD_BYTES; +pub(super) const SHARE_NULLIFIER_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) +} + +pub(super) fn java_fixed_bytes( + env: &mut JNIEnv<'_>, + array: &JByteArray<'_>, + field: &str, +) -> anyhow::Result<[u8; N]> { + fixed_bytes(java_bytes(env, array, field)?, field) +} + +pub(super) fn fixed_bytes( + bytes: Vec, + field: &str, +) -> anyhow::Result<[u8; N]> { + let len = bytes.len(); + + bytes + .try_into() + .map_err(|_| anyhow!("{field} must be exactly {N} bytes, got {len}")) +} 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..5d379e0fa --- /dev/null +++ b/backend-lib/src/main/rust/voting/share_tracking.rs @@ -0,0 +1,29 @@ +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_computeShareNullifierNative< + '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_fixed_bytes::(env, &vote_commitment, "voteCommitment")?, + jint_to_u32(share_index, "share_index")?, + &java_fixed_bytes::(env, &blind, "blind")?, + ) + .map_err(|e| anyhow!("compute_share_nullifier: {}", e))?; + let nullifier = fixed_bytes::(nullifier, "shareNullifier")?; + Ok(env.byte_array_from_slice(&nullifier)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, std::ptr::null_mut()) +} From af36d9675f93b3cc83810d3cc29882cd69e9d212 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Fri, 8 May 2026 16:30:30 +0200 Subject: [PATCH 02/13] Add voting DB round lifecycle JNI --- backend-lib/Cargo.lock | 2 + backend-lib/Cargo.toml | 2 + backend-lib/src/main/rust/voting.rs | 22 ++- backend-lib/src/main/rust/voting/db.rs | 78 +++++++++++ backend-lib/src/main/rust/voting/helpers.rs | 41 +++++- backend-lib/src/main/rust/voting/json.rs | 63 +++++++++ backend-lib/src/main/rust/voting/rounds.rs | 144 ++++++++++++++++++++ 7 files changed, 345 insertions(+), 7 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 ea309ceae..e0f15f880 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..6b23eaf99 100644 --- a/backend-lib/src/main/rust/voting.rs +++ b/backend-lib/src/main/rust/voting.rs @@ -3,12 +3,28 @@ use anyhow::anyhow; use jni::{ JNIEnv, - objects::{JByteArray, JClass}, - sys::{jbyteArray, jint}, + objects::{JByteArray, JClass, JObject, JString, JValue}, + sys::{jboolean, jbyteArray, jint, jlong, jobject, jstring}, +}; +use serde::Serialize; +use std::{ + collections::HashMap, + sync::{ + Arc, Mutex, OnceLock, + atomic::{AtomicI64, Ordering}, + }, }; 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..670db4747 --- /dev/null +++ b/backend-lib/src/main/rust/voting/db.rs @@ -0,0 +1,78 @@ +use super::*; + +static NEXT_DB_HANDLE: AtomicI64 = AtomicI64::new(1); +static DB_REGISTRY: OnceLock>>> = OnceLock::new(); + +fn registry() -> &'static Mutex>> { + DB_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn next_handle() -> anyhow::Result { + NEXT_DB_HANDLE + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| { + id.checked_add(1).filter(|next| *next > 0) + }) + .map_err(|_| anyhow!("voting DB handle space exhausted")) +} + +pub(super) fn db_from_handle(handle: jlong) -> anyhow::Result> { + if handle <= 0 { + return Err(anyhow!("Voting DB handle must be positive, got {handle}")); + } + + registry() + .lock() + .map_err(|_| anyhow!("voting DB registry mutex poisoned"))? + .get(&handle) + .cloned() + .ok_or_else(|| anyhow!("Voting DB handle is closed or unknown: {handle}")) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_openVotingDbNative< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_path: JString<'local>, + wallet_id: JString<'local>, +) -> jlong { + let res = catch_unwind(&mut env, |env| { + let path = java_string_to_rust(env, &db_path)?; + let wallet_id = java_string_to_rust(env, &wallet_id)?; + if wallet_id.is_empty() { + return Err(anyhow!("walletId must not be empty")); + } + + let db = VotingDb::open(&path).map_err(|e| anyhow!("VotingDb::open failed: {}", e))?; + db.set_wallet_id(&wallet_id); + let handle = next_handle()?; + registry() + .lock() + .map_err(|_| anyhow!("voting DB registry mutex poisoned"))? + .insert(handle, Arc::new(db)); + + Ok(handle) + }); + unwrap_exc_or(&mut env, res, 0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_closeVotingDbNative< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, +) { + let res = catch_unwind(&mut env, |_| { + if db_handle > 0 { + registry() + .lock() + .map_err(|_| anyhow!("voting DB registry mutex poisoned"))? + .remove(&db_handle); + } + Ok(()) + }); + unwrap_exc_or(&mut env, res, ()) +} diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index 92da62608..66d665cda 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; @@ -9,6 +10,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) @@ -46,13 +51,41 @@ pub(super) fn java_fixed_bytes( fixed_bytes(java_bytes(env, array, field)?, field) } -pub(super) fn fixed_bytes( - bytes: Vec, - field: &str, -) -> anyhow::Result<[u8; N]> { +pub(super) fn fixed_bytes(bytes: Vec, field: &str) -> anyhow::Result<[u8; N]> { let len = bytes.len(); bytes .try_into() .map_err(|_| anyhow!("{field} must be exactly {N} bytes, got {len}")) } + +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..52a00ba65 --- /dev/null +++ b/backend-lib/src/main/rust/voting/json.rs @@ -0,0 +1,63 @@ +use super::*; + +const PHASE_INITIALIZED: u32 = 0; +const PHASE_HOTKEY_GENERATED: u32 = 1; +const PHASE_DELEGATION_CONSTRUCTED: u32 = 2; +const PHASE_DELEGATION_PROVED: u32 = 3; +const PHASE_VOTE_READY: u32 = 4; + +#[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 => PHASE_INITIALIZED, + RoundPhase::HotkeyGenerated => PHASE_HOTKEY_GENERATED, + RoundPhase::DelegationConstructed => PHASE_DELEGATION_CONSTRUCTED, + RoundPhase::DelegationProved => PHASE_DELEGATION_PROVED, + RoundPhase::VoteReady => PHASE_VOTE_READY, + } +} + +#[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..fdae5aee1 --- /dev/null +++ b/backend-lib/src/main/rust/voting/rounds.rs @@ -0,0 +1,144 @@ +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_initRoundNative< + '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>, +) { + let res = catch_unwind(&mut env, |env| { + let db = db_from_handle(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)?; + db.init_round(¶ms, session.as_deref()) + .map_err(|e| anyhow!("init_round: {}", e))?; + Ok(()) + }); + unwrap_exc_or(&mut env, res, ()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getRoundStateNative< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let db = db_from_handle(db_handle)?; + match 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_listRoundsJsonNative< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let db = db_from_handle(db_handle)?; + let rounds: Vec = 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_getVotesJsonNative< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) -> jstring { + let res = catch_unwind(&mut env, |env| { + let db = db_from_handle(db_handle)?; + let votes: Vec = 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_clearRoundNative< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_handle: jlong, + round_id: JString<'local>, +) { + let res = catch_unwind(&mut env, |env| { + let db = db_from_handle(db_handle)?; + db.clear_round(&java_string_to_rust(env, &round_id)?) + .map_err(|e| anyhow!("clear_round: {}", e))?; + Ok(()) + }); + unwrap_exc_or(&mut env, res, ()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_deleteSkippedBundlesNative< + '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 db = db_from_handle(db_handle)?; + let deleted_rows = 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 f0106ae99ce8ef43fd728e316b1efc6ba1aad302 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Fri, 8 May 2026 16:30:41 +0200 Subject: [PATCH 03/13] Add internal voting round lifecycle wrappers --- .../sdk/internal/jni/VotingRustBackendTest.kt | 121 +++++++++++++++ .../sdk/internal/jni/VotingRustBackend.kt | 141 ++++++++++++++++++ .../internal/model/voting/FfiVotingModels.kt | 48 ++++++ .../sdk/internal/TypesafeVotingBackend.kt | 43 ++++++ .../sdk/internal/TypesafeVotingBackendImpl.kt | 76 ++++++++++ 5 files changed, 429 insertions(+) create mode 100644 backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt 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 diff --git a/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt b/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt index 67885dac7..46bd2b60f 100644 --- a/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt +++ b/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt @@ -1,12 +1,19 @@ package cash.z.ecc.android.sdk.internal.jni +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundPhase import kotlinx.coroutines.test.runTest +import org.json.JSONArray import org.junit.Test +import kotlin.io.path.createTempDirectory import kotlin.test.assertContentEquals +import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull @OptIn(ExperimentalStdlibApi::class) +@Suppress("MagicNumber") class VotingRustBackendTest { companion object { private const val FIELD_BYTES = 32 @@ -17,6 +24,16 @@ class VotingRustBackendTest { private val SHORT_FIELD = ByteArray(FIELD_BYTES - 1) private val EXPECTED_NULLIFIER = "8d6d97caa19a20e5e67e7cc24aaaa7beb72b4a513863f6adbe7b62ba1b1b0010".hexToByteArray() + + private const val WALLET_ID = "wallet-1" + private const val OTHER_WALLET_ID = "wallet-2" + private const val ROUND_ID = "round-1" + private const val SNAPSHOT_HEIGHT = 123_456L + private const val JSON_ARRAY_START_INDEX = 0 + private const val SESSION_JSON = "{\"round\":\"one\"}" + private val EA_PK = ByteArray(FIELD_BYTES) { 3 } + private val NC_ROOT = ByteArray(FIELD_BYTES) { 4 } + private val NULLIFIER_IMT_ROOT = ByteArray(FIELD_BYTES) { 5 } } @Test @@ -45,4 +62,108 @@ class VotingRustBackendTest { backend.computeShareNullifier(VOTE_COMMITMENT, OUT_OF_RANGE_SHARE_INDEX, BLIND) } } + + @Test + fun voting_db_round_state_round_trips() = + runTest { + val db = VotingRustBackend.new().openVotingDb(newDbPath(), WALLET_ID) + try { + assertNull(db.getRoundState(ROUND_ID)) + + db.initRound( + roundId = ROUND_ID, + snapshotHeight = SNAPSHOT_HEIGHT, + eaPK = EA_PK, + ncRoot = NC_ROOT, + nullifierIMTRoot = NULLIFIER_IMT_ROOT, + sessionJson = SESSION_JSON + ) + + val state = assertNotNull(db.getRoundState(ROUND_ID)) + assertEquals(ROUND_ID, state.roundId) + assertEquals(FfiRoundPhase.INITIALIZED.value, state.phase) + assertEquals(FfiRoundPhase.INITIALIZED, state.roundPhase) + assertEquals(SNAPSHOT_HEIGHT, state.snapshotHeight) + assertNull(state.hotkeyAddress) + assertNull(state.delegatedWeight) + assertFalse(state.proofGenerated) + + val rounds = JSONArray(db.listRoundsJson()) + assertEquals(1, rounds.length()) + val round = rounds.getJSONObject(0) + assertEquals(ROUND_ID, round.getString("round_id")) + assertEquals(FfiRoundPhase.INITIALIZED.value, round.getInt("phase")) + assertEquals(SNAPSHOT_HEIGHT, round.getLong("snapshot_height")) + + assertEquals(emptyList(), JSONArray(db.getVotesJson(ROUND_ID)).toList()) + + db.clearRound(ROUND_ID) + assertNull(db.getRoundState(ROUND_ID)) + } finally { + db.close() + } + } + + @Test + fun voting_db_keeps_wallet_state_isolated() = + runTest { + val dbPath = newDbPath() + val firstWallet = VotingRustBackend.new().openVotingDb(dbPath, WALLET_ID) + val secondWallet = VotingRustBackend.new().openVotingDb(dbPath, OTHER_WALLET_ID) + try { + firstWallet.initRound( + roundId = ROUND_ID, + snapshotHeight = SNAPSHOT_HEIGHT, + eaPK = EA_PK, + ncRoot = NC_ROOT, + nullifierIMTRoot = NULLIFIER_IMT_ROOT, + sessionJson = null + ) + + assertNotNull(firstWallet.getRoundState(ROUND_ID)) + assertNull(secondWallet.getRoundState(ROUND_ID)) + } finally { + firstWallet.close() + secondWallet.close() + } + } + + @Test + fun voting_db_rejects_malformed_inputs_and_closed_handle() = + runTest { + val db = VotingRustBackend.new().openVotingDb(newDbPath(), WALLET_ID) + + assertFailsWith { + db.initRound( + roundId = ROUND_ID, + snapshotHeight = -1, + eaPK = EA_PK, + ncRoot = NC_ROOT, + nullifierIMTRoot = NULLIFIER_IMT_ROOT, + sessionJson = null + ) + } + assertFailsWith { + db.initRound( + roundId = ROUND_ID, + snapshotHeight = SNAPSHOT_HEIGHT, + eaPK = SHORT_FIELD, + ncRoot = NC_ROOT, + nullifierIMTRoot = NULLIFIER_IMT_ROOT, + sessionJson = null + ) + } + + db.close() + db.close() + assertFailsWith { + db.getRoundState(ROUND_ID) + } + } + + private fun newDbPath() = + createTempDirectory("voting-db-").resolve("voting.db").toFile().absolutePath + + private fun JSONArray.toList(): List = + (JSON_ARRAY_START_INDEX until length()).map { index -> get(index) } } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt index 5e5ad2dd9..c4cd7b3f9 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt @@ -1,5 +1,14 @@ package cash.z.ecc.android.sdk.internal.jni +import androidx.annotation.Keep +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +@Keep +@Suppress("TooManyFunctions", "LongParameterList") class VotingRustBackend private constructor() { @Throws(RuntimeException::class) fun computeShareNullifier( @@ -8,6 +17,94 @@ class VotingRustBackend private constructor() { blind: ByteArray ): ByteArray = computeShareNullifierNative(voteCommitment, shareIndex, blind) + suspend fun openVotingDb(dbPath: String, walletId: String): VotingDb = + withContext(Dispatchers.IO) { + openVotingDbNative(dbPath, walletId).let { dbHandle -> + check(dbHandle != 0L) { + "openVotingDb failed for dbPath=$dbPath" + } + VotingDb(dbHandle) + } + } + + @Suppress("TooManyFunctions", "LongParameterList") + class VotingDb internal constructor( + private var dbHandle: Long? + ) { + private val accessMutex = Mutex() + + suspend fun close() { + accessMutex.withLock { + dbHandle?.let { handle -> + withContext(Dispatchers.IO) { + closeVotingDbNative(handle) + } + dbHandle = null + } + } + } + + @Throws(RuntimeException::class) + suspend fun initRound( + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) = withHandle { handle -> + initRoundNative( + handle, + roundId, + snapshotHeight, + eaPK, + ncRoot, + nullifierIMTRoot, + sessionJson + ) + } + + @Throws(RuntimeException::class) + suspend fun getRoundState(roundId: String): FfiRoundState? = + withHandle { handle -> getRoundStateNative(handle, roundId) } + + @Throws(RuntimeException::class) + suspend fun listRoundsJson(): String = + withHandle { handle -> listRoundsJsonNative(handle) } + + @Throws(RuntimeException::class) + suspend fun getVotesJson(roundId: String): String = + withHandle { handle -> getVotesJsonNative(handle, roundId) } + + @Throws(RuntimeException::class) + suspend fun clearRound(roundId: String) = + withHandle { handle -> clearRoundNative(handle, roundId) } + + @Throws(RuntimeException::class) + suspend fun deleteSkippedBundles( + roundId: String, + keepCount: Int + ): Long = + withHandle { handle -> + deleteSkippedBundlesNative(handle, roundId, keepCount).also { deletedRows -> + check(deletedRows >= 0) { + "deleteSkippedBundles failed for roundId=$roundId keepCount=$keepCount" + } + } + } + + private suspend fun withHandle(block: (Long) -> T): T = + accessMutex.withLock { + val handle = + checkNotNull(dbHandle) { + "Voting DB handle is closed" + } + withContext(Dispatchers.IO) { + block(handle) + } + } + } + companion object { suspend fun new(): VotingRustBackend { RustBackend.loadLibrary() @@ -22,5 +119,49 @@ class VotingRustBackend private constructor() { shareIndex: Int, blind: ByteArray ): ByteArray + + @JvmStatic + @Throws(RuntimeException::class) + private external fun openVotingDbNative(dbPath: String, walletId: String): Long + + @JvmStatic + @Throws(RuntimeException::class) + private external fun closeVotingDbNative(dbHandle: Long) + + @JvmStatic + @Throws(RuntimeException::class) + private external fun initRoundNative( + dbHandle: Long, + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) + + @JvmStatic + @Throws(RuntimeException::class) + private external fun getRoundStateNative(dbHandle: Long, roundId: String): FfiRoundState? + + @JvmStatic + @Throws(RuntimeException::class) + private external fun listRoundsJsonNative(dbHandle: Long): String + + @JvmStatic + @Throws(RuntimeException::class) + private external fun getVotesJsonNative(dbHandle: Long, roundId: String): String + + @JvmStatic + @Throws(RuntimeException::class) + private external fun clearRoundNative(dbHandle: Long, roundId: String) + + @JvmStatic + @Throws(RuntimeException::class) + private external fun deleteSkippedBundlesNative( + dbHandle: Long, + roundId: String, + keepCount: Int + ): Long } } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt new file mode 100644 index 000000000..618aa7e1d --- /dev/null +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -0,0 +1,48 @@ +package cash.z.ecc.android.sdk.internal.model.voting + +import androidx.annotation.Keep + +internal const val FFI_ROUND_PHASE_INITIALIZED = 0 +internal const val FFI_ROUND_PHASE_HOTKEY_GENERATED = 1 +internal const val FFI_ROUND_PHASE_DELEGATION_CONSTRUCTED = 2 +internal const val FFI_ROUND_PHASE_DELEGATION_PROVED = 3 +internal const val FFI_ROUND_PHASE_VOTE_READY = 4 + +@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.fromInt(phase) +} + +@Keep +enum class FfiRoundPhase( + val value: Int +) { + INITIALIZED(FFI_ROUND_PHASE_INITIALIZED), + HOTKEY_GENERATED(FFI_ROUND_PHASE_HOTKEY_GENERATED), + DELEGATION_CONSTRUCTED(FFI_ROUND_PHASE_DELEGATION_CONSTRUCTED), + DELEGATION_PROVED(FFI_ROUND_PHASE_DELEGATION_PROVED), + VOTE_READY(FFI_ROUND_PHASE_VOTE_READY); + + companion object { + fun fromInt(value: Int) = + entries.firstOrNull { it.value == value } + ?: error("Unknown round phase: $value") + } +} + +@Keep +data class FfiRoundSummary( + val roundId: String, + val phase: Int, + val snapshotHeight: Long, + val createdAt: Long +) { + val roundPhase = FfiRoundPhase.fromInt(phase) +} 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..91885eeac --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt @@ -0,0 +1,43 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundSummary + +@Suppress("TooManyFunctions", "LongParameterList") +interface TypesafeVotingBackend { + suspend fun openVotingDb(dbPath: String, walletId: String): TypesafeVotingDb +} + +@Suppress("TooManyFunctions", "LongParameterList") +interface TypesafeVotingDb { + suspend fun close() + + suspend fun initRound( + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) + + suspend fun getRoundState(roundId: String): FfiRoundState? + + suspend fun listRounds(): List + + suspend fun getVotes(roundId: String): List + + suspend fun clearRound(roundId: String) + + suspend fun deleteSkippedBundles( + 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..7fd570423 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt @@ -0,0 +1,76 @@ +package cash.z.ecc.android.sdk.internal + +import cash.z.ecc.android.sdk.internal.jni.VotingRustBackend +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundSummary +import org.json.JSONArray + +@Suppress("TooManyFunctions", "LongParameterList") +class TypesafeVotingBackendImpl : TypesafeVotingBackend { + override suspend fun openVotingDb(dbPath: String, walletId: String): TypesafeVotingDb = + TypesafeVotingDbImpl(VotingRustBackend.new().openVotingDb(dbPath, walletId)) +} + +@Suppress("TooManyFunctions", "LongParameterList") +private class TypesafeVotingDbImpl( + private val votingDb: VotingRustBackend.VotingDb +) : TypesafeVotingDb { + override suspend fun close() = votingDb.close() + + override suspend fun initRound( + roundId: String, + snapshotHeight: Long, + eaPK: ByteArray, + ncRoot: ByteArray, + nullifierIMTRoot: ByteArray, + sessionJson: String? + ) = votingDb.initRound( + roundId, + snapshotHeight, + eaPK, + ncRoot, + nullifierIMTRoot, + sessionJson + ) + + override suspend fun getRoundState(roundId: String): FfiRoundState? = + votingDb.getRoundState(roundId) + + override suspend fun listRounds(): List = + JSONArray(votingDb.listRoundsJson()).toList { obj -> + FfiRoundSummary( + roundId = obj.getString("round_id"), + phase = obj.getCheckedInt("phase"), + snapshotHeight = obj.getLong("snapshot_height"), + createdAt = obj.getLong("created_at") + ) + } + + override suspend fun getVotes(roundId: String): List = + JSONArray(votingDb.getVotesJson(roundId)).toList { obj -> + VoteRecord( + proposalId = obj.getCheckedInt("proposal_id"), + bundleIndex = obj.getCheckedInt("bundle_index"), + choice = obj.getCheckedInt("choice"), + submitted = obj.getBoolean("submitted") + ) + } + + override suspend fun clearRound(roundId: String) = + votingDb.clearRound(roundId) + + override suspend fun deleteSkippedBundles( + roundId: String, + keepCount: Int + ): Long = votingDb.deleteSkippedBundles(roundId, keepCount) +} + +private fun JSONArray.toList(transform: (org.json.JSONObject) -> T): List = + (JSON_ARRAY_START_INDEX until length()).map { index -> + transform(getJSONObject(index)) + } + +private fun org.json.JSONObject.getCheckedInt(name: String): Int = + Math.toIntExact(getLong(name)) + +private const val JSON_ARRAY_START_INDEX = 0 From 662a87130813f53c2924a2f10ec92dfd2ef13ab0 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:11:00 +0200 Subject: [PATCH 04/13] fix: Document FfiRoundState JNI constructor Addresses PR #1938 review comment #1. --- backend-lib/src/main/rust/voting/helpers.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index 66d665cda..11d282264 100644 --- a/backend-lib/src/main/rust/voting/helpers.rs +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -77,6 +77,8 @@ pub(super) fn make_ffi_round_state<'local>( }; let obj = env.new_object( &class, + // Matches FfiRoundState(roundId, phase, snapshotHeight, hotkeyAddress, + // delegatedWeight, proofGenerated). "(Ljava/lang/String;IJLjava/lang/String;Ljava/lang/Long;Z)V", &[ JValue::Object(&round_id_obj), From 2098c324f998d29e414b6f71890466923e9004ab Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:11:16 +0200 Subject: [PATCH 05/13] fix: Narrow missing round state handling Addresses PR #1938 review comment #2. --- backend-lib/src/main/rust/voting.rs | 1 - backend-lib/src/main/rust/voting/helpers.rs | 14 ++++++++++++++ backend-lib/src/main/rust/voting/rounds.rs | 12 ++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/backend-lib/src/main/rust/voting.rs b/backend-lib/src/main/rust/voting.rs index 6b23eaf99..e6268241e 100644 --- a/backend-lib/src/main/rust/voting.rs +++ b/backend-lib/src/main/rust/voting.rs @@ -17,7 +17,6 @@ use std::{ use zcash_voting as voting; 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, diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index 11d282264..af90b9bdd 100644 --- a/backend-lib/src/main/rust/voting/helpers.rs +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -91,3 +91,17 @@ pub(super) fn make_ffi_round_state<'local>( )?; Ok(obj.into_raw()) } + +pub(super) fn round_exists(db: &VotingDb, round_id: &str) -> anyhow::Result { + let conn = db.conn(); + let wallet_id = db.wallet_id(); + match conn.query_row( + "SELECT 1 FROM rounds WHERE round_id = ?1 AND wallet_id = ?2 LIMIT 1", + rusqlite::params![round_id, wallet_id], + |_| Ok(()), + ) { + Ok(()) => Ok(true), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), + Err(e) => Err(anyhow!("round_exists query failed: {}", e)), + } +} diff --git a/backend-lib/src/main/rust/voting/rounds.rs b/backend-lib/src/main/rust/voting/rounds.rs index fdae5aee1..de4969ea8 100644 --- a/backend-lib/src/main/rust/voting/rounds.rs +++ b/backend-lib/src/main/rust/voting/rounds.rs @@ -50,10 +50,14 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_get ) -> jobject { let res = catch_unwind(&mut env, |env| { let db = db_from_handle(db_handle)?; - match 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)), + let round_id = java_string_to_rust(env, &round_id)?; + if !round_exists(&db, &round_id)? { + Ok(JObject::null().into_raw()) + } else { + let state = db + .get_round_state(&round_id) + .map_err(|e| anyhow!("get_round_state: {}", e))?; + make_ffi_round_state(env, state) } }); unwrap_exc_or(&mut env, res, JObject::null().into_raw()) From 4447de6972776c433ed9cba442d9ee0d321b61d3 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:11:28 +0200 Subject: [PATCH 06/13] fix: Document round phase FFI constants Addresses PR #1938 review comment #3. --- .../z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt | 1 + backend-lib/src/main/rust/voting/json.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt index 618aa7e1d..c7b303af0 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.model.voting import androidx.annotation.Keep +// Must match PHASE_* constants in backend-lib/src/main/rust/voting/json.rs. internal const val FFI_ROUND_PHASE_INITIALIZED = 0 internal const val FFI_ROUND_PHASE_HOTKEY_GENERATED = 1 internal const val FFI_ROUND_PHASE_DELEGATION_CONSTRUCTED = 2 diff --git a/backend-lib/src/main/rust/voting/json.rs b/backend-lib/src/main/rust/voting/json.rs index 52a00ba65..d35f16573 100644 --- a/backend-lib/src/main/rust/voting/json.rs +++ b/backend-lib/src/main/rust/voting/json.rs @@ -1,5 +1,6 @@ use super::*; +// Must match FFI_ROUND_PHASE_* constants in FfiVotingModels.kt. const PHASE_INITIALIZED: u32 = 0; const PHASE_HOTKEY_GENERATED: u32 = 1; const PHASE_DELEGATION_CONSTRUCTED: u32 = 2; From 7cd088b7ee91f21860583810f5815f6fe382da32 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:11:38 +0200 Subject: [PATCH 07/13] fix: Remove Keep from JSON round summary Addresses PR #1938 review comment #5. --- .../z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt index c7b303af0..e02e641e6 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -38,7 +38,6 @@ enum class FfiRoundPhase( } } -@Keep data class FfiRoundSummary( val roundId: String, val phase: Int, From fe73e0feeb0e4bb9e687a77bcf0f5d0bf7a1b09c Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:11:44 +0200 Subject: [PATCH 08/13] fix: Remove redundant deleted rows guard Addresses PR #1938 review comment #6. --- .../z/ecc/android/sdk/internal/jni/VotingRustBackend.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt index c4cd7b3f9..39b018197 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt @@ -85,13 +85,7 @@ class VotingRustBackend private constructor() { roundId: String, keepCount: Int ): Long = - withHandle { handle -> - deleteSkippedBundlesNative(handle, roundId, keepCount).also { deletedRows -> - check(deletedRows >= 0) { - "deleteSkippedBundles failed for roundId=$roundId keepCount=$keepCount" - } - } - } + withHandle { handle -> deleteSkippedBundlesNative(handle, roundId, keepCount) } private suspend fun withHandle(block: (Long) -> T): T = accessMutex.withLock { From e7307118c06016030b9a78dc22b0617aff6bc1d4 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:11:56 +0200 Subject: [PATCH 09/13] fix: Move VoteRecord into voting models Addresses PR #1938 review comment #7. --- .../android/sdk/internal/model/voting/FfiVotingModels.kt | 7 +++++++ .../z/ecc/android/sdk/internal/TypesafeVotingBackend.kt | 8 +------- .../ecc/android/sdk/internal/TypesafeVotingBackendImpl.kt | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt index e02e641e6..622d2d3fe 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -46,3 +46,10 @@ data class FfiRoundSummary( ) { val roundPhase = FfiRoundPhase.fromInt(phase) } + +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/TypesafeVotingBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeVotingBackend.kt index 91885eeac..f09d77965 100644 --- 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 @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundSummary +import cash.z.ecc.android.sdk.internal.model.voting.VoteRecord @Suppress("TooManyFunctions", "LongParameterList") interface TypesafeVotingBackend { @@ -34,10 +35,3 @@ interface TypesafeVotingDb { 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 index 7fd570423..0b40a94ef 100644 --- 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 @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.jni.VotingRustBackend import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundSummary +import cash.z.ecc.android.sdk.internal.model.voting.VoteRecord import org.json.JSONArray @Suppress("TooManyFunctions", "LongParameterList") From 3c8bd285ce0c1182bfd80bfae3fe7ff6692789db Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Mon, 11 May 2026 18:12:02 +0200 Subject: [PATCH 10/13] fix: Reuse voting Rust backend instance Addresses PR #1938 review comment #8. --- .../android/sdk/internal/TypesafeVotingBackendImpl.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 index 0b40a94ef..0abc418fc 100644 --- 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 @@ -8,8 +8,15 @@ import org.json.JSONArray @Suppress("TooManyFunctions", "LongParameterList") class TypesafeVotingBackendImpl : TypesafeVotingBackend { + private val rustBackendLazy = + SuspendingLazy { + VotingRustBackend.new() + } + override suspend fun openVotingDb(dbPath: String, walletId: String): TypesafeVotingDb = - TypesafeVotingDbImpl(VotingRustBackend.new().openVotingDb(dbPath, walletId)) + TypesafeVotingDbImpl(rustBackend().openVotingDb(dbPath, walletId)) + + private suspend fun rustBackend() = rustBackendLazy.getInstance(Unit) } @Suppress("TooManyFunctions", "LongParameterList") From c07f8e79491f641ac1086a95f1837974c97b8299 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Tue, 12 May 2026 01:12:42 +0200 Subject: [PATCH 11/13] fix: Use typed JNI for voting round lists --- backend-lib/Cargo.lock | 2 - backend-lib/Cargo.toml | 2 - .../sdk/internal/jni/VotingRustBackendTest.kt | 20 ++- .../sdk/internal/jni/VotingRustBackend.kt | 14 +- .../internal/model/voting/FfiVotingModels.kt | 8 +- backend-lib/src/main/rust/voting.rs | 5 +- backend-lib/src/main/rust/voting/helpers.rs | 124 +++++++++++++++++- backend-lib/src/main/rust/voting/json.rs | 64 --------- backend-lib/src/main/rust/voting/rounds.rs | 27 ++-- .../sdk/internal/TypesafeVotingBackend.kt | 8 +- .../sdk/internal/TypesafeVotingBackendImpl.kt | 37 +----- 11 files changed, 166 insertions(+), 145 deletions(-) delete mode 100644 backend-lib/src/main/rust/voting/json.rs diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index c3135426c..f704c9866 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -6715,8 +6715,6 @@ 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 e0f15f880..ea309ceae 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -54,8 +54,6 @@ 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/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt b/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt index 46bd2b60f..29ea56fdf 100644 --- a/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt +++ b/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt @@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk.internal.jni import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundPhase import kotlinx.coroutines.test.runTest -import org.json.JSONArray import org.junit.Test import kotlin.io.path.createTempDirectory import kotlin.test.assertContentEquals @@ -29,7 +28,6 @@ class VotingRustBackendTest { private const val OTHER_WALLET_ID = "wallet-2" private const val ROUND_ID = "round-1" private const val SNAPSHOT_HEIGHT = 123_456L - private const val JSON_ARRAY_START_INDEX = 0 private const val SESSION_JSON = "{\"round\":\"one\"}" private val EA_PK = ByteArray(FIELD_BYTES) { 3 } private val NC_ROOT = ByteArray(FIELD_BYTES) { 4 } @@ -88,14 +86,15 @@ class VotingRustBackendTest { assertNull(state.delegatedWeight) assertFalse(state.proofGenerated) - val rounds = JSONArray(db.listRoundsJson()) - assertEquals(1, rounds.length()) - val round = rounds.getJSONObject(0) - assertEquals(ROUND_ID, round.getString("round_id")) - assertEquals(FfiRoundPhase.INITIALIZED.value, round.getInt("phase")) - assertEquals(SNAPSHOT_HEIGHT, round.getLong("snapshot_height")) + val rounds = db.listRounds() + assertEquals(1, rounds.size) + val round = rounds.single() + assertEquals(ROUND_ID, round.roundId) + assertEquals(FfiRoundPhase.INITIALIZED.value, round.phase) + assertEquals(FfiRoundPhase.INITIALIZED, round.roundPhase) + assertEquals(SNAPSHOT_HEIGHT, round.snapshotHeight) - assertEquals(emptyList(), JSONArray(db.getVotesJson(ROUND_ID)).toList()) + assertEquals(emptyList(), db.getVotes(ROUND_ID).asList()) db.clearRound(ROUND_ID) assertNull(db.getRoundState(ROUND_ID)) @@ -163,7 +162,4 @@ class VotingRustBackendTest { private fun newDbPath() = createTempDirectory("voting-db-").resolve("voting.db").toFile().absolutePath - - private fun JSONArray.toList(): List = - (JSON_ARRAY_START_INDEX until length()).map { index -> get(index) } } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt index 39b018197..57d892a98 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt @@ -2,6 +2,8 @@ package cash.z.ecc.android.sdk.internal.jni import androidx.annotation.Keep import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundSummary +import cash.z.ecc.android.sdk.internal.model.voting.JniVoteRecord import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -69,12 +71,12 @@ class VotingRustBackend private constructor() { withHandle { handle -> getRoundStateNative(handle, roundId) } @Throws(RuntimeException::class) - suspend fun listRoundsJson(): String = - withHandle { handle -> listRoundsJsonNative(handle) } + suspend fun listRounds(): Array = + withHandle { handle -> listRoundsNative(handle) } @Throws(RuntimeException::class) - suspend fun getVotesJson(roundId: String): String = - withHandle { handle -> getVotesJsonNative(handle, roundId) } + suspend fun getVotes(roundId: String): Array = + withHandle { handle -> getVotesNative(handle, roundId) } @Throws(RuntimeException::class) suspend fun clearRound(roundId: String) = @@ -140,11 +142,11 @@ class VotingRustBackend private constructor() { @JvmStatic @Throws(RuntimeException::class) - private external fun listRoundsJsonNative(dbHandle: Long): String + private external fun listRoundsNative(dbHandle: Long): Array @JvmStatic @Throws(RuntimeException::class) - private external fun getVotesJsonNative(dbHandle: Long, roundId: String): String + private external fun getVotesNative(dbHandle: Long, roundId: String): Array @JvmStatic @Throws(RuntimeException::class) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt index 622d2d3fe..8049d0ea0 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt @@ -2,7 +2,7 @@ package cash.z.ecc.android.sdk.internal.model.voting import androidx.annotation.Keep -// Must match PHASE_* constants in backend-lib/src/main/rust/voting/json.rs. +// Must match PHASE_* constants in backend-lib/src/main/rust/voting/helpers.rs. internal const val FFI_ROUND_PHASE_INITIALIZED = 0 internal const val FFI_ROUND_PHASE_HOTKEY_GENERATED = 1 internal const val FFI_ROUND_PHASE_DELEGATION_CONSTRUCTED = 2 @@ -38,7 +38,8 @@ enum class FfiRoundPhase( } } -data class FfiRoundSummary( +@Keep +data class JniRoundSummary( val roundId: String, val phase: Int, val snapshotHeight: Long, @@ -47,7 +48,8 @@ data class FfiRoundSummary( val roundPhase = FfiRoundPhase.fromInt(phase) } -data class VoteRecord( +@Keep +data class JniVoteRecord( val proposalId: Int, val bundleIndex: Int, val choice: Int, diff --git a/backend-lib/src/main/rust/voting.rs b/backend-lib/src/main/rust/voting.rs index e6268241e..bbea42485 100644 --- a/backend-lib/src/main/rust/voting.rs +++ b/backend-lib/src/main/rust/voting.rs @@ -4,9 +4,8 @@ use anyhow::anyhow; use jni::{ JNIEnv, objects::{JByteArray, JClass, JObject, JString, JValue}, - sys::{jboolean, jbyteArray, jint, jlong, jobject, jstring}, + sys::{jboolean, jbyteArray, jint, jlong, jobject, jobjectArray}, }; -use serde::Serialize; use std::{ collections::HashMap, sync::{ @@ -20,10 +19,10 @@ use voting::storage::{RoundPhase, RoundState, RoundSummary, VoteRecord, VotingDb use crate::utils::{ catch_unwind, exception::unwrap_exc_or, java_nullable_string_to_rust, java_string_to_rust, + rust_vec_to_java, }; mod db; mod helpers; -mod json; mod rounds; mod share_tracking; diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index af90b9bdd..9267be8e1 100644 --- a/backend-lib/src/main/rust/voting/helpers.rs +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -1,11 +1,34 @@ -use super::json::round_phase_to_u32; use super::*; +// Must match FFI_ROUND_PHASE_* constants in FfiVotingModels.kt. +const PHASE_INITIALIZED: u32 = 0; +const PHASE_HOTKEY_GENERATED: u32 = 1; +const PHASE_DELEGATION_CONSTRUCTED: u32 = 2; +const PHASE_DELEGATION_PROVED: u32 = 3; +const PHASE_VOTE_READY: u32 = 4; + +const JNI_ROUND_SUMMARY: &str = "cash/z/ecc/android/sdk/internal/model/voting/JniRoundSummary"; +const JNI_VOTE_RECORD: &str = "cash/z/ecc/android/sdk/internal/model/voting/JniVoteRecord"; + pub(super) const PROTOCOL_FIELD_BYTES: usize = 32; pub(super) const VOTE_COMMITMENT_BYTES: usize = PROTOCOL_FIELD_BYTES; pub(super) const BLIND_BYTES: usize = PROTOCOL_FIELD_BYTES; pub(super) const SHARE_NULLIFIER_BYTES: usize = 32; +struct JniRoundSummaryPayload { + round_id: String, + phase: jint, + snapshot_height: jlong, + created_at: jlong, +} + +struct JniVoteRecordPayload { + proposal_id: jint, + bundle_index: jint, + choice: jint, + submitted: bool, +} + 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}")) } @@ -14,6 +37,14 @@ 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}")) } +fn u32_to_jint(value: u32, field: &str) -> anyhow::Result { + jint::try_from(value).map_err(|_| anyhow!("{field} exceeds signed Int range: {value}")) +} + +fn u64_to_jlong(value: u64, field: &str) -> anyhow::Result { + jlong::try_from(value).map_err(|_| anyhow!("{field} exceeds signed Long range: {value}")) +} + pub(super) fn require_len(bytes: Vec, field: &str, expected: usize) -> anyhow::Result> { if bytes.len() == expected { Ok(bytes) @@ -59,6 +90,16 @@ pub(super) fn fixed_bytes(bytes: Vec, field: &str) -> anyhow .map_err(|_| anyhow!("{field} must be exactly {N} bytes, got {len}")) } +pub(super) fn round_phase_to_u32(phase: RoundPhase) -> u32 { + match phase { + RoundPhase::Initialized => PHASE_INITIALIZED, + RoundPhase::HotkeyGenerated => PHASE_HOTKEY_GENERATED, + RoundPhase::DelegationConstructed => PHASE_DELEGATION_CONSTRUCTED, + RoundPhase::DelegationProved => PHASE_DELEGATION_PROVED, + RoundPhase::VoteReady => PHASE_VOTE_READY, + } +} + pub(super) fn make_ffi_round_state<'local>( env: &mut JNIEnv<'local>, state: RoundState, @@ -92,6 +133,61 @@ pub(super) fn make_ffi_round_state<'local>( Ok(obj.into_raw()) } +pub(super) fn make_jni_round_summaries( + env: &mut JNIEnv<'_>, + rounds: Vec, +) -> anyhow::Result { + let payloads = rounds + .into_iter() + .map(JniRoundSummaryPayload::try_from) + .collect::>>()?; + + Ok( + rust_vec_to_java(env, payloads, JNI_ROUND_SUMMARY, |env, round| { + let round_id_obj: JObject<'_> = env.new_string(round.round_id)?.into(); + env.new_object( + JNI_ROUND_SUMMARY, + // Matches JniRoundSummary(roundId, phase, snapshotHeight, createdAt). + "(Ljava/lang/String;IJJ)V", + &[ + JValue::Object(&round_id_obj), + JValue::Int(round.phase), + JValue::Long(round.snapshot_height), + JValue::Long(round.created_at), + ], + ) + })? + .into_raw(), + ) +} + +pub(super) fn make_jni_vote_records( + env: &mut JNIEnv<'_>, + votes: Vec, +) -> anyhow::Result { + let payloads = votes + .into_iter() + .map(JniVoteRecordPayload::try_from) + .collect::>>()?; + + Ok( + rust_vec_to_java(env, payloads, JNI_VOTE_RECORD, |env, vote| { + env.new_object( + JNI_VOTE_RECORD, + // Matches JniVoteRecord(proposalId, bundleIndex, choice, submitted). + "(IIIZ)V", + &[ + JValue::Int(vote.proposal_id), + JValue::Int(vote.bundle_index), + JValue::Int(vote.choice), + JValue::Bool(vote.submitted as jboolean), + ], + ) + })? + .into_raw(), + ) +} + pub(super) fn round_exists(db: &VotingDb, round_id: &str) -> anyhow::Result { let conn = db.conn(); let wallet_id = db.wallet_id(); @@ -105,3 +201,29 @@ pub(super) fn round_exists(db: &VotingDb, round_id: &str) -> anyhow::Result Err(anyhow!("round_exists query failed: {}", e)), } } + +impl TryFrom for JniRoundSummaryPayload { + type Error = anyhow::Error; + + fn try_from(round: RoundSummary) -> anyhow::Result { + Ok(JniRoundSummaryPayload { + round_id: round.round_id, + phase: u32_to_jint(round_phase_to_u32(round.phase), "phase")?, + snapshot_height: u64_to_jlong(round.snapshot_height, "snapshot_height")?, + created_at: u64_to_jlong(round.created_at, "created_at")?, + }) + } +} + +impl TryFrom for JniVoteRecordPayload { + type Error = anyhow::Error; + + fn try_from(record: VoteRecord) -> anyhow::Result { + Ok(JniVoteRecordPayload { + proposal_id: u32_to_jint(record.proposal_id, "proposal_id")?, + bundle_index: u32_to_jint(record.bundle_index, "bundle_index")?, + choice: u32_to_jint(record.choice, "choice")?, + submitted: record.submitted, + }) + } +} diff --git a/backend-lib/src/main/rust/voting/json.rs b/backend-lib/src/main/rust/voting/json.rs deleted file mode 100644 index d35f16573..000000000 --- a/backend-lib/src/main/rust/voting/json.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::*; - -// Must match FFI_ROUND_PHASE_* constants in FfiVotingModels.kt. -const PHASE_INITIALIZED: u32 = 0; -const PHASE_HOTKEY_GENERATED: u32 = 1; -const PHASE_DELEGATION_CONSTRUCTED: u32 = 2; -const PHASE_DELEGATION_PROVED: u32 = 3; -const PHASE_VOTE_READY: u32 = 4; - -#[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 => PHASE_INITIALIZED, - RoundPhase::HotkeyGenerated => PHASE_HOTKEY_GENERATED, - RoundPhase::DelegationConstructed => PHASE_DELEGATION_CONSTRUCTED, - RoundPhase::DelegationProved => PHASE_DELEGATION_PROVED, - RoundPhase::VoteReady => PHASE_VOTE_READY, - } -} - -#[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 index de4969ea8..a52207603 100644 --- a/backend-lib/src/main/rust/voting/rounds.rs +++ b/backend-lib/src/main/rust/voting/rounds.rs @@ -1,6 +1,5 @@ use super::db::*; use super::helpers::*; -use super::json::*; use super::*; #[unsafe(no_mangle)] @@ -64,44 +63,38 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_get } #[unsafe(no_mangle)] -pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_listRoundsJsonNative< +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_listRoundsNative< 'local, >( mut env: JNIEnv<'local>, _: JClass<'local>, db_handle: jlong, -) -> jstring { +) -> jobjectArray { let res = catch_unwind(&mut env, |env| { let db = db_from_handle(db_handle)?; - let rounds: Vec = db + let rounds = db .list_rounds() - .map_err(|e| anyhow!("list_rounds: {}", e))? - .into_iter() - .map(JsonRoundSummary::from) - .collect(); - json_to_jstring(env, &rounds) + .map_err(|e| anyhow!("list_rounds: {}", e))?; + make_jni_round_summaries(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_getVotesJsonNative< +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_getVotesNative< 'local, >( mut env: JNIEnv<'local>, _: JClass<'local>, db_handle: jlong, round_id: JString<'local>, -) -> jstring { +) -> jobjectArray { let res = catch_unwind(&mut env, |env| { let db = db_from_handle(db_handle)?; - let votes: Vec = db + let votes = 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) + .map_err(|e| anyhow!("get_votes: {}", e))?; + make_jni_vote_records(env, votes) }); unwrap_exc_or(&mut env, res, std::ptr::null_mut()) } 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 index f09d77965..6e9281cba 100644 --- 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 @@ -1,8 +1,8 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState -import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundSummary -import cash.z.ecc.android.sdk.internal.model.voting.VoteRecord +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundSummary +import cash.z.ecc.android.sdk.internal.model.voting.JniVoteRecord @Suppress("TooManyFunctions", "LongParameterList") interface TypesafeVotingBackend { @@ -24,9 +24,9 @@ interface TypesafeVotingDb { suspend fun getRoundState(roundId: String): FfiRoundState? - suspend fun listRounds(): List + suspend fun listRounds(): List - suspend fun getVotes(roundId: String): List + suspend fun getVotes(roundId: String): List suspend fun clearRound(roundId: String) 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 index 0abc418fc..7d5e6b583 100644 --- 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 @@ -2,9 +2,8 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.jni.VotingRustBackend import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState -import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundSummary -import cash.z.ecc.android.sdk.internal.model.voting.VoteRecord -import org.json.JSONArray +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundSummary +import cash.z.ecc.android.sdk.internal.model.voting.JniVoteRecord @Suppress("TooManyFunctions", "LongParameterList") class TypesafeVotingBackendImpl : TypesafeVotingBackend { @@ -44,25 +43,11 @@ private class TypesafeVotingDbImpl( override suspend fun getRoundState(roundId: String): FfiRoundState? = votingDb.getRoundState(roundId) - override suspend fun listRounds(): List = - JSONArray(votingDb.listRoundsJson()).toList { obj -> - FfiRoundSummary( - roundId = obj.getString("round_id"), - phase = obj.getCheckedInt("phase"), - snapshotHeight = obj.getLong("snapshot_height"), - createdAt = obj.getLong("created_at") - ) - } + override suspend fun listRounds(): List = + votingDb.listRounds().asList() - override suspend fun getVotes(roundId: String): List = - JSONArray(votingDb.getVotesJson(roundId)).toList { obj -> - VoteRecord( - proposalId = obj.getCheckedInt("proposal_id"), - bundleIndex = obj.getCheckedInt("bundle_index"), - choice = obj.getCheckedInt("choice"), - submitted = obj.getBoolean("submitted") - ) - } + override suspend fun getVotes(roundId: String): List = + votingDb.getVotes(roundId).asList() override suspend fun clearRound(roundId: String) = votingDb.clearRound(roundId) @@ -72,13 +57,3 @@ private class TypesafeVotingDbImpl( keepCount: Int ): Long = votingDb.deleteSkippedBundles(roundId, keepCount) } - -private fun JSONArray.toList(transform: (org.json.JSONObject) -> T): List = - (JSON_ARRAY_START_INDEX until length()).map { index -> - transform(getJSONObject(index)) - } - -private fun org.json.JSONObject.getCheckedInt(name: String): Int = - Math.toIntExact(getLong(name)) - -private const val JSON_ARRAY_START_INDEX = 0 From dad02c384595f31d52b7a168ebf6301e2e33baf7 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Tue, 12 May 2026 01:26:12 +0200 Subject: [PATCH 12/13] fix: Use zcash_voting round helper --- backend-lib/Cargo.lock | 4 ++-- backend-lib/Cargo.toml | 4 ++-- backend-lib/src/main/rust/voting/helpers.rs | 14 -------------- backend-lib/src/main/rust/voting/rounds.rs | 5 ++++- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index f704c9866..1932f5c08 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -7037,9 +7037,9 @@ dependencies = [ [[package]] name = "zcash_voting" -version = "0.5.3" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90abca13341b344d82315895bf50f26e93a66b5c4d2fbd8591728af604d4d4db" +checksum = "e1bb9e0ae40320acb03358d257a6fdc951866a21fb5fdd94ce5a09d0bccf25d2" dependencies = [ "anyhow", "blake2b_simd", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index ea309ceae..c2db05eac 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -86,14 +86,14 @@ rust-analyzer = "0.0.1" # The Android JNI boundary for voting code is `src/main/rust/voting.rs`. # Its share-nullifier entry point forwards to # `zcash_voting::share_tracking::compute_share_nullifier`: -# https://github.com/valargroup/zcash_voting/blob/zcash_voting-v0.5.3/zcash_voting/src/share_tracking.rs +# https://github.com/valargroup/zcash_voting/blob/zcash_voting-v0.5.9/zcash_voting/src/share_tracking.rs # # The transitive `voting-circuits` dependency contains the share-reveal circuit # and the Poseidon-based `share_nullifier_hash` implementation: # https://github.com/valargroup/voting-circuits/blob/v0.4.1/voting-circuits/src/share_reveal/circuit.rs # Default features stay disabled so this foundation change does not pull in the # optional client PIR, tree-sync, or networking stacks. -zcash_voting = { version = "0.5.3", default-features = false } +zcash_voting = { version = "0.5.9", default-features = false } ## Uncomment this to test librustzcash changes locally #[patch.crates-io] diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index 9267be8e1..424f6259d 100644 --- a/backend-lib/src/main/rust/voting/helpers.rs +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -188,20 +188,6 @@ pub(super) fn make_jni_vote_records( ) } -pub(super) fn round_exists(db: &VotingDb, round_id: &str) -> anyhow::Result { - let conn = db.conn(); - let wallet_id = db.wallet_id(); - match conn.query_row( - "SELECT 1 FROM rounds WHERE round_id = ?1 AND wallet_id = ?2 LIMIT 1", - rusqlite::params![round_id, wallet_id], - |_| Ok(()), - ) { - Ok(()) => Ok(true), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), - Err(e) => Err(anyhow!("round_exists query failed: {}", e)), - } -} - impl TryFrom for JniRoundSummaryPayload { type Error = anyhow::Error; diff --git a/backend-lib/src/main/rust/voting/rounds.rs b/backend-lib/src/main/rust/voting/rounds.rs index a52207603..7dd98dbf2 100644 --- a/backend-lib/src/main/rust/voting/rounds.rs +++ b/backend-lib/src/main/rust/voting/rounds.rs @@ -50,7 +50,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_get let res = catch_unwind(&mut env, |env| { let db = db_from_handle(db_handle)?; let round_id = java_string_to_rust(env, &round_id)?; - if !round_exists(&db, &round_id)? { + if !db + .has_round(&round_id) + .map_err(|e| anyhow!("has_round: {}", e))? + { Ok(JObject::null().into_raw()) } else { let state = db From 450fe29f397fae05c107e6f130357de6fbfce485 Mon Sep 17 00:00:00 2001 From: Greg Nagy Date: Tue, 12 May 2026 01:26:54 +0200 Subject: [PATCH 13/13] fix: Rename voting JNI models --- .../sdk/internal/jni/VotingRustBackendTest.kt | 10 +++---- .../sdk/internal/jni/VotingRustBackend.kt | 6 ++-- ...{FfiVotingModels.kt => JniVotingModels.kt} | 28 +++++++++---------- backend-lib/src/main/rust/voting/helpers.rs | 8 +++--- backend-lib/src/main/rust/voting/rounds.rs | 2 +- .../sdk/internal/TypesafeVotingBackend.kt | 4 +-- .../sdk/internal/TypesafeVotingBackendImpl.kt | 4 +-- 7 files changed, 31 insertions(+), 31 deletions(-) rename backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/{FfiVotingModels.kt => JniVotingModels.kt} (54%) diff --git a/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt b/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt index 29ea56fdf..5ae97617d 100644 --- a/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt +++ b/backend-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackendTest.kt @@ -1,6 +1,6 @@ package cash.z.ecc.android.sdk.internal.jni -import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundPhase +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundPhase import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.io.path.createTempDirectory @@ -79,8 +79,8 @@ class VotingRustBackendTest { val state = assertNotNull(db.getRoundState(ROUND_ID)) assertEquals(ROUND_ID, state.roundId) - assertEquals(FfiRoundPhase.INITIALIZED.value, state.phase) - assertEquals(FfiRoundPhase.INITIALIZED, state.roundPhase) + assertEquals(JniRoundPhase.INITIALIZED.value, state.phase) + assertEquals(JniRoundPhase.INITIALIZED, state.roundPhase) assertEquals(SNAPSHOT_HEIGHT, state.snapshotHeight) assertNull(state.hotkeyAddress) assertNull(state.delegatedWeight) @@ -90,8 +90,8 @@ class VotingRustBackendTest { assertEquals(1, rounds.size) val round = rounds.single() assertEquals(ROUND_ID, round.roundId) - assertEquals(FfiRoundPhase.INITIALIZED.value, round.phase) - assertEquals(FfiRoundPhase.INITIALIZED, round.roundPhase) + assertEquals(JniRoundPhase.INITIALIZED.value, round.phase) + assertEquals(JniRoundPhase.INITIALIZED, round.roundPhase) assertEquals(SNAPSHOT_HEIGHT, round.snapshotHeight) assertEquals(emptyList(), db.getVotes(ROUND_ID).asList()) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt index 57d892a98..95c1fc521 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/VotingRustBackend.kt @@ -1,7 +1,7 @@ package cash.z.ecc.android.sdk.internal.jni import androidx.annotation.Keep -import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundState import cash.z.ecc.android.sdk.internal.model.voting.JniRoundSummary import cash.z.ecc.android.sdk.internal.model.voting.JniVoteRecord import kotlinx.coroutines.Dispatchers @@ -67,7 +67,7 @@ class VotingRustBackend private constructor() { } @Throws(RuntimeException::class) - suspend fun getRoundState(roundId: String): FfiRoundState? = + suspend fun getRoundState(roundId: String): JniRoundState? = withHandle { handle -> getRoundStateNative(handle, roundId) } @Throws(RuntimeException::class) @@ -138,7 +138,7 @@ class VotingRustBackend private constructor() { @JvmStatic @Throws(RuntimeException::class) - private external fun getRoundStateNative(dbHandle: Long, roundId: String): FfiRoundState? + private external fun getRoundStateNative(dbHandle: Long, roundId: String): JniRoundState? @JvmStatic @Throws(RuntimeException::class) diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/JniVotingModels.kt similarity index 54% rename from backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt rename to backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/JniVotingModels.kt index 8049d0ea0..cf017d986 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/FfiVotingModels.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/voting/JniVotingModels.kt @@ -3,14 +3,14 @@ package cash.z.ecc.android.sdk.internal.model.voting import androidx.annotation.Keep // Must match PHASE_* constants in backend-lib/src/main/rust/voting/helpers.rs. -internal const val FFI_ROUND_PHASE_INITIALIZED = 0 -internal const val FFI_ROUND_PHASE_HOTKEY_GENERATED = 1 -internal const val FFI_ROUND_PHASE_DELEGATION_CONSTRUCTED = 2 -internal const val FFI_ROUND_PHASE_DELEGATION_PROVED = 3 -internal const val FFI_ROUND_PHASE_VOTE_READY = 4 +internal const val JNI_ROUND_PHASE_INITIALIZED = 0 +internal const val JNI_ROUND_PHASE_HOTKEY_GENERATED = 1 +internal const val JNI_ROUND_PHASE_DELEGATION_CONSTRUCTED = 2 +internal const val JNI_ROUND_PHASE_DELEGATION_PROVED = 3 +internal const val JNI_ROUND_PHASE_VOTE_READY = 4 @Keep -data class FfiRoundState( +data class JniRoundState( val roundId: String, val phase: Int, val snapshotHeight: Long, @@ -18,18 +18,18 @@ data class FfiRoundState( val delegatedWeight: Long?, val proofGenerated: Boolean ) { - val roundPhase = FfiRoundPhase.fromInt(phase) + val roundPhase = JniRoundPhase.fromInt(phase) } @Keep -enum class FfiRoundPhase( +enum class JniRoundPhase( val value: Int ) { - INITIALIZED(FFI_ROUND_PHASE_INITIALIZED), - HOTKEY_GENERATED(FFI_ROUND_PHASE_HOTKEY_GENERATED), - DELEGATION_CONSTRUCTED(FFI_ROUND_PHASE_DELEGATION_CONSTRUCTED), - DELEGATION_PROVED(FFI_ROUND_PHASE_DELEGATION_PROVED), - VOTE_READY(FFI_ROUND_PHASE_VOTE_READY); + INITIALIZED(JNI_ROUND_PHASE_INITIALIZED), + HOTKEY_GENERATED(JNI_ROUND_PHASE_HOTKEY_GENERATED), + DELEGATION_CONSTRUCTED(JNI_ROUND_PHASE_DELEGATION_CONSTRUCTED), + DELEGATION_PROVED(JNI_ROUND_PHASE_DELEGATION_PROVED), + VOTE_READY(JNI_ROUND_PHASE_VOTE_READY); companion object { fun fromInt(value: Int) = @@ -45,7 +45,7 @@ data class JniRoundSummary( val snapshotHeight: Long, val createdAt: Long ) { - val roundPhase = FfiRoundPhase.fromInt(phase) + val roundPhase = JniRoundPhase.fromInt(phase) } @Keep diff --git a/backend-lib/src/main/rust/voting/helpers.rs b/backend-lib/src/main/rust/voting/helpers.rs index 424f6259d..7960c98a9 100644 --- a/backend-lib/src/main/rust/voting/helpers.rs +++ b/backend-lib/src/main/rust/voting/helpers.rs @@ -1,6 +1,6 @@ use super::*; -// Must match FFI_ROUND_PHASE_* constants in FfiVotingModels.kt. +// Must match JNI_ROUND_PHASE_* constants in JniVotingModels.kt. const PHASE_INITIALIZED: u32 = 0; const PHASE_HOTKEY_GENERATED: u32 = 1; const PHASE_DELEGATION_CONSTRUCTED: u32 = 2; @@ -100,12 +100,12 @@ pub(super) fn round_phase_to_u32(phase: RoundPhase) -> u32 { } } -pub(super) fn make_ffi_round_state<'local>( +pub(super) fn make_jni_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 class = env.find_class("cash/z/ecc/android/sdk/internal/model/voting/JniRoundState")?; 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(), @@ -118,7 +118,7 @@ pub(super) fn make_ffi_round_state<'local>( }; let obj = env.new_object( &class, - // Matches FfiRoundState(roundId, phase, snapshotHeight, hotkeyAddress, + // Matches JniRoundState(roundId, phase, snapshotHeight, hotkeyAddress, // delegatedWeight, proofGenerated). "(Ljava/lang/String;IJLjava/lang/String;Ljava/lang/Long;Z)V", &[ diff --git a/backend-lib/src/main/rust/voting/rounds.rs b/backend-lib/src/main/rust/voting/rounds.rs index 7dd98dbf2..2dce8f909 100644 --- a/backend-lib/src/main/rust/voting/rounds.rs +++ b/backend-lib/src/main/rust/voting/rounds.rs @@ -59,7 +59,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_VotingRustBackend_get let state = db .get_round_state(&round_id) .map_err(|e| anyhow!("get_round_state: {}", e))?; - make_ffi_round_state(env, state) + make_jni_round_state(env, state) } }); unwrap_exc_or(&mut env, res, JObject::null().into_raw()) 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 index 6e9281cba..e983b4aad 100644 --- 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 @@ -1,6 +1,6 @@ package cash.z.ecc.android.sdk.internal -import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundState import cash.z.ecc.android.sdk.internal.model.voting.JniRoundSummary import cash.z.ecc.android.sdk.internal.model.voting.JniVoteRecord @@ -22,7 +22,7 @@ interface TypesafeVotingDb { sessionJson: String? ) - suspend fun getRoundState(roundId: String): FfiRoundState? + suspend fun getRoundState(roundId: String): JniRoundState? suspend fun listRounds(): List 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 index 7d5e6b583..3985ccb38 100644 --- 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 @@ -1,7 +1,7 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.jni.VotingRustBackend -import cash.z.ecc.android.sdk.internal.model.voting.FfiRoundState +import cash.z.ecc.android.sdk.internal.model.voting.JniRoundState import cash.z.ecc.android.sdk.internal.model.voting.JniRoundSummary import cash.z.ecc.android.sdk.internal.model.voting.JniVoteRecord @@ -40,7 +40,7 @@ private class TypesafeVotingDbImpl( sessionJson ) - override suspend fun getRoundState(roundId: String): FfiRoundState? = + override suspend fun getRoundState(roundId: String): JniRoundState? = votingDb.getRoundState(roundId) override suspend fun listRounds(): List =