Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend-lib/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 14 additions & 32 deletions backend-lib/src/main/rust/voting.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,24 @@
//! JNI bindings for the zcash_voting crate.

use std::ptr;

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::{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)?;
use voting::storage::{RoundPhase, RoundState, RoundSummary, VoteRecord, VotingDb};
use voting::types::VotingError;

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, java_nullable_string_to_rust, java_string_to_rust,
};

Ok(utils::rust_bytes_to_java(env, &nullifier)?.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
mod db;
mod helpers;
mod json;
mod rounds;
mod share_tracking;
61 changes: 61 additions & 0 deletions backend-lib/src/main/rust/voting/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use super::*;

pub(super) struct VotingDatabaseHandle {
pub(super) db: Arc<VotingDb>,
}

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)
}
72 changes: 72 additions & 0 deletions backend-lib/src/main/rust/voting/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use super::json::round_phase_to_u32;
use super::*;

pub(super) const PROTOCOL_FIELD_BYTES: usize = 32;

pub(super) fn jint_to_u32(value: jint, field: &str) -> anyhow::Result<u32> {
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> {
u64::try_from(value).map_err(|_| anyhow!("{field} must be non-negative, got {value}"))
}

pub(super) fn require_len(bytes: Vec<u8>, field: &str, expected: usize) -> anyhow::Result<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<jobject> {
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())
}
57 changes: 57 additions & 0 deletions backend-lib/src/main/rust/voting/json.rs
Original file line number Diff line number Diff line change
@@ -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<RoundSummary> 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<VoteRecord> 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<T: Serialize>(
env: &mut JNIEnv<'_>,
value: &T,
) -> anyhow::Result<jstring> {
let s = serde_json::to_string(value).map_err(|e| anyhow!("JSON serialization error: {}", e))?;
Ok(env.new_string(s)?.into_raw())
}
Loading