From e99ed5bdeb578922ea808d1e80215bde552f9964 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Feb 2019 12:50:40 +0000 Subject: [PATCH 1/4] Extract SQL API into a separate module --- src/main/rust/lib.rs | 626 +------------------------------------------ src/main/rust/sql.rs | 620 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 624 insertions(+), 622 deletions(-) create mode 100644 src/main/rust/sql.rs diff --git a/src/main/rust/lib.rs b/src/main/rust/lib.rs index 5e4d3f98..c5897bd4 100644 --- a/src/main/rust/lib.rs +++ b/src/main/rust/lib.rs @@ -12,629 +12,10 @@ extern crate zcash_client_backend; extern crate zcash_primitives; extern crate zip32; -use failure::{format_err, Error}; -use ff::{PrimeField, PrimeFieldRepr}; -use pairing::bls12_381::Bls12; -use protobuf::parse_from_bytes; -use rusqlite::{types::ToSql, Connection, NO_PARAMS}; -use sapling_crypto::{ - jubjub::fs::{Fs, FsRepr}, - primitives::{Diversifier, Note, PaymentAddress}, -}; -use zcash_client_backend::{ - constants::{HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, HRP_SAPLING_PAYMENT_ADDRESS_TEST}, - encoding::{ - decode_extended_full_viewing_key, encode_extended_full_viewing_key, encode_payment_address, - }, - note_encryption::Memo, - proto::compact_formats::CompactBlock, - prover::TxProver, - transaction::Builder, - welding_rig::scan_block, -}; -use zcash_primitives::{ - merkle_tree::{CommitmentTree, IncrementalWitness}, - transaction::components::Amount, - JUBJUB, -}; -use zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; +mod sql; const SAPLING_CONSENSUS_BRANCH_ID: u32 = 0x76b8_09bb; -const ANCHOR_OFFSET: u32 = 10; - -fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String { - let addr = extfvk.default_address().unwrap().1; - encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &addr) -} - -fn init_data_database(db_data: &str) -> rusqlite::Result<()> { - let data = Connection::open(db_data)?; - data.execute( - "CREATE TABLE IF NOT EXISTS accounts ( - account INTEGER PRIMARY KEY, - extfvk TEXT NOT NULL, - address TEXT NOT NULL - )", - NO_PARAMS, - )?; - data.execute( - "CREATE TABLE IF NOT EXISTS blocks ( - height INTEGER PRIMARY KEY, - time INTEGER NOT NULL, - sapling_tree BLOB NOT NULL - )", - NO_PARAMS, - )?; - data.execute( - "CREATE TABLE IF NOT EXISTS transactions ( - id_tx INTEGER PRIMARY KEY, - txid BLOB NOT NULL UNIQUE, - block INTEGER, - tx_index INTEGER, - raw BLOB, - FOREIGN KEY (block) REFERENCES blocks(height) - )", - NO_PARAMS, - )?; - data.execute( - "CREATE TABLE IF NOT EXISTS received_notes ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - account INTEGER NOT NULL, - diversifier BLOB NOT NULL, - value INTEGER NOT NULL, - rcm BLOB NOT NULL, - nf BLOB NOT NULL UNIQUE, - is_change BOOLEAN NOT NULL, - memo BLOB, - spent INTEGER, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), - CONSTRAINT tx_output UNIQUE (tx, output_index) - )", - NO_PARAMS, - )?; - data.execute( - "CREATE TABLE IF NOT EXISTS sapling_witnesses ( - id_witness INTEGER PRIMARY KEY, - note INTEGER NOT NULL, - block INTEGER NOT NULL, - witness BLOB NOT NULL, - FOREIGN KEY (note) REFERENCES received_notes(id_note), - FOREIGN KEY (block) REFERENCES blocks(height), - CONSTRAINT witness_height UNIQUE (note, block) - )", - NO_PARAMS, - )?; - data.execute( - "CREATE TABLE IF NOT EXISTS sent_notes ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - from_account INTEGER NOT NULL, - address TEXT NOT NULL, - value INTEGER NOT NULL, - memo BLOB, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (from_account) REFERENCES accounts(account), - CONSTRAINT tx_output UNIQUE (tx, output_index) - )", - NO_PARAMS, - )?; - Ok(()) -} - -fn init_accounts_table(db_data: &str, extfvks: &[ExtendedFullViewingKey]) -> Result<(), Error> { - let data = Connection::open(db_data)?; - - let mut empty_check = data.prepare("SELECT * FROM accounts LIMIT 1")?; - if empty_check.exists(NO_PARAMS)? { - return Err(format_err!("accounts table is not empty")); - } - - // Insert accounts atomically - data.execute("BEGIN IMMEDIATE", NO_PARAMS)?; - for (account, extfvk) in extfvks.iter().enumerate() { - let address = address_from_extfvk(extfvk); - let extfvk = - encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, extfvk); - data.execute( - "INSERT INTO accounts (account, extfvk, address) - VALUES (?, ?, ?)", - &[ - (account as u32).to_sql()?, - extfvk.to_sql()?, - address.to_sql()?, - ], - )?; - } - data.execute("COMMIT", NO_PARAMS)?; - - Ok(()) -} - -fn init_blocks_table( - db_data: &str, - height: i32, - time: u32, - sapling_tree: &[u8], -) -> Result<(), Error> { - let data = Connection::open(db_data)?; - - let mut empty_check = data.prepare("SELECT * FROM blocks LIMIT 1")?; - if empty_check.exists(NO_PARAMS)? { - return Err(format_err!("blocks table is not empty")); - } - - data.execute( - "INSERT INTO blocks (height, time, sapling_tree) - VALUES (?, ?, ?)", - &[height.to_sql()?, time.to_sql()?, sapling_tree.to_sql()?], - )?; - - Ok(()) -} - -struct CompactBlockRow { - height: i32, - data: Vec, -} - -#[derive(Clone)] -struct WitnessRow { - id_note: i64, - witness: IncrementalWitness, -} - -fn get_address(db_data: &str, account: u32) -> Result { - let data = Connection::open(db_data)?; - - let addr = data.query_row( - "SELECT address FROM accounts - WHERE account = ?", - &[account], - |row| row.get(0), - )?; - - Ok(addr) -} - -fn get_balance(db_data: &str, account: u32) -> Result { - let data = Connection::open(db_data)?; - - let balance = data.query_row( - "SELECT SUM(value) FROM received_notes - WHERE account = ? AND spent IS NULL", - &[account], - |row| row.get_checked(0).unwrap_or(0), - )?; - - Ok(Amount(balance)) -} - -/// Scans new blocks added to the cache for any transactions received by the -/// tracked accounts. -/// -/// Assumes that the caller is handling rollbacks. -fn scan_cached_blocks(db_cache: &str, db_data: &str) -> Result<(), Error> { - let cache = Connection::open(db_cache)?; - let data = Connection::open(db_data)?; - - // Recall where we synced up to previously. - // If we have never synced, use 0 to select all cached CompactBlocks. - let mut last_height = - data.query_row( - "SELECT MAX(height) FROM blocks", - NO_PARAMS, - |row| match row.get_checked(0) { - Ok(h) => h, - Err(_) => 0, - }, - )?; - - // Prepare necessary SQL statements - let mut stmt_blocks = cache - .prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC")?; - let mut stmt_fetch_tree = data.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?; - let mut stmt_fetch_witnesses = - data.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?; - let mut stmt_fetch_nullifiers = - data.prepare("SELECT id_note, nf, account FROM received_notes WHERE spent IS NULL")?; - let mut stmt_insert_block = data.prepare( - "INSERT INTO blocks (height, time, sapling_tree) - VALUES (?, ?, ?)", - )?; - let mut stmt_update_tx = data.prepare( - "UPDATE transactions - SET block = ?, tx_index = ? WHERE txid = ?", - )?; - let mut stmt_insert_tx = data.prepare( - "INSERT INTO transactions (txid, block, tx_index) - VALUES (?, ?, ?)", - )?; - let mut stmt_select_tx = data.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?; - let mut stmt_mark_spent_note = - data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?; - let mut stmt_insert_note = data.prepare( - "INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - )?; - let mut stmt_insert_witness = data.prepare( - "INSERT INTO sapling_witnesses (note, block, witness) - VALUES (?, ?, ?)", - )?; - let mut stmt_prune_witnesses = data.prepare("DELETE FROM sapling_witnesses WHERE block < ?")?; - - // Fetch the CompactBlocks we need to scan - let rows = stmt_blocks.query_map(&[last_height], |row| CompactBlockRow { - height: row.get(0), - data: row.get(1), - })?; - - // Fetch the ExtendedFullViewingKeys we are tracking - let mut stmt_fetch_accounts = - data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?; - let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| { - let extfvk: String = row.get(0); - decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, &extfvk) - })?; - // Raise SQL errors from the query, and IO errors from parsing. - let extfvks: Vec<_> = extfvks.collect::, _>>()??; - - // Get the most recent CommitmentTree - let mut tree = match stmt_fetch_tree.query_row(&[last_height], |row| match row.get_checked(0) { - Ok(data) => { - let data: Vec<_> = data; - CommitmentTree::read(&data[..]).unwrap() - } - Err(_) => CommitmentTree::new(), - }) { - Ok(tree) => tree, - Err(_) => CommitmentTree::new(), - }; - - // Get most recent incremental witnesses for the notes we are tracking - let witnesses = stmt_fetch_witnesses.query_map(&[last_height], |row| { - let data: Vec<_> = row.get(1); - WitnessRow { - id_note: row.get(0), - witness: IncrementalWitness::read(&data[..]).unwrap(), - } - })?; - let mut witnesses: Vec<_> = witnesses.collect::>()?; - - // Get the nullifiers for the notes we are tracking - let nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| { - let nf: Vec<_> = row.get(1); - let account: i64 = row.get(2); - (nf, account as usize) - })?; - let mut nullifiers: Vec<_> = nullifiers.collect::>()?; - - for row in rows { - let row = row?; - - // Start an SQL transaction for this block. - data.execute("BEGIN IMMEDIATE", NO_PARAMS)?; - - // Scanned blocks MUST be height-ascending, but they might not be height-sequential - // (e.g. if blocks that don't contain Sapling data are skipped). Note that this - // introduces a risk that a block containing Sapling data is skipped; it is up to - // the caller to ensure this does not happen. - if row.height <= last_height { - return Err(format_err!( - "Expected height of next CompactBlock to be at least {}, but was {}", - last_height + 1, - row.height - )); - } - last_height = row.height; - - let block: CompactBlock = parse_from_bytes(&row.data)?; - let block_time = block.time; - - let txs = { - let nf_refs: Vec<_> = nullifiers.iter().map(|(nf, acc)| (&nf[..], *acc)).collect(); - let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.witness).collect(); - scan_block( - block, - &extfvks[..], - &nf_refs, - &mut tree, - &mut witness_refs[..], - ) - }; - - // Insert the block into the database. - let mut encoded_tree = Vec::new(); - tree.write(&mut encoded_tree).unwrap(); - stmt_insert_block.execute(&[ - row.height.to_sql()?, - block_time.to_sql()?, - encoded_tree.to_sql()?, - ])?; - - for (tx, new_witnesses) in txs { - // First try update an existing transaction in the database. - let txid = tx.txid.0.to_vec(); - let tx_row = if stmt_update_tx.execute(&[ - row.height.to_sql()?, - (tx.index as i64).to_sql()?, - txid.to_sql()?, - ])? == 0 - { - // It isn't there, so insert our transaction into the database. - stmt_insert_tx.execute(&[ - txid.to_sql()?, - row.height.to_sql()?, - (tx.index as i64).to_sql()?, - ])?; - data.last_insert_rowid() - } else { - // It was there, so grab its row number. - stmt_select_tx.query_row(&[txid], |row| row.get(0))? - }; - - // Mark notes as spent and remove them from the scanning cache - for spend in &tx.shielded_spends { - stmt_mark_spent_note.execute(&[tx_row.to_sql()?, spend.nf.to_sql()?])?; - } - nullifiers = nullifiers - .into_iter() - .filter(|(nf, _acc)| { - tx.shielded_spends - .iter() - .find(|spend| &spend.nf == nf) - .is_none() - }) - .collect(); - - for (output, witness) in tx - .shielded_outputs - .into_iter() - .zip(new_witnesses.into_iter()) - { - let mut rcm = [0; 32]; - output.note.r.into_repr().write_le(&mut rcm[..])?; - let nf = output.note.nf( - &extfvks[output.account].fvk.vk, - witness.position() as u64, - &JUBJUB, - ); - - // Insert received note into the database. - // Assumptions: - // - A transaction will not contain more than 2^63 shielded outputs. - // - A note value will never exceed 2^63 zatoshis. - stmt_insert_note.execute(&[ - tx_row.to_sql()?, - (output.index as i64).to_sql()?, - (output.account as i64).to_sql()?, - output.to.diversifier.0.to_sql()?, - (output.note.value as i64).to_sql()?, - rcm.to_sql()?, - nf.to_sql()?, - output.is_change.to_sql()?, - ])?; - let note_row = data.last_insert_rowid(); - - // Save witness for note. - witnesses.push(WitnessRow { - id_note: note_row, - witness, - }); - - // Cache nullifier for note (to detect subsequent spends in this scan). - nullifiers.push((nf, output.account)); - } - } - - // Insert current witnesses into the database. - let mut encoded = Vec::new(); - for witness_row in witnesses.iter() { - encoded.clear(); - witness_row.witness.write(&mut encoded).unwrap(); - stmt_insert_witness.execute(&[ - witness_row.id_note.to_sql()?, - last_height.to_sql()?, - encoded.to_sql()?, - ])?; - } - - // Prune the stored witnesses (we only expect rollbacks of at most 100 blocks). - stmt_prune_witnesses.execute(&[last_height - 100])?; - - // Commit the SQL transaction, writing this block's data atomically. - data.execute("COMMIT", NO_PARAMS)?; - } - - Ok(()) -} - -struct SelectedNoteRow { - diversifier: Diversifier, - note: Note, - witness: IncrementalWitness, -} - -/// Creates a transaction paying the specified address. -fn send_to_address( - db_data: &str, - consensus_branch_id: u32, - prover: impl TxProver, - account: u32, - extsk: &ExtendedSpendingKey, - to: &PaymentAddress, - value: Amount, - memo: Option, -) -> Result { - let data = Connection::open(db_data)?; - - // Check that the ExtendedSpendingKey we have been given corresponds to the - // ExtendedFullViewingKey for the account we are spending from. - let extfvk = ExtendedFullViewingKey::from(extsk); - if !data - .prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")? - .exists(&[ - account.to_sql()?, - encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, &extfvk) - .to_sql()?, - ])? - { - return Err(format_err!( - "Incorrect ExtendedSpendingKey for account {}", - account - )); - } - let ovk = extfvk.fvk.ovk; - - // Target the next block, assuming we are up-to-date. - let height = data.query_row_and_then("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| { - let ret: Result = row.get_checked(0); - ret - })? + 1; - - // The goal of this SQL statement is to select the oldest notes until the required - // value has been reached, and then fetch the witnesses at the desired height for the - // selected notes. This is achieved in several steps: - // - // 1) Use a window function to create a view of all notes, ordered from oldest to - // newest, with an additional column containing a running sum: - // - Unspent notes accumulate the values of all unspent notes in that note's - // account, up to itself. - // - Spent notes accumulate the values of all notes in the transaction they were - // spent in, up to itself. - // - // 2) Select all unspent notes in the desired account, along with their running sum. - // - // 3) Select all notes for which the running sum was less than the required value, as - // well as a single note for which the sum was greater than or equal to the - // required value, bringing the sum of all selected notes across the threshold. - // - // 4) Match the selected notes against the witnesses at the desired height. - let mut stmt_select_notes = data.prepare( - "WITH selected AS ( - WITH eligible AS ( - SELECT id_note, diversifier, value, rcm, - SUM(value) OVER - (PARTITION BY account, spent ORDER BY id_note) AS so_far - FROM received_notes - WHERE account = ? AND spent IS NULL - ) - SELECT * FROM eligible WHERE so_far < ? - UNION - SELECT * FROM (SELECT * FROM eligible WHERE so_far >= ? LIMIT 1) - ), witnesses AS ( - SELECT note, witness FROM sapling_witnesses - WHERE block = ? - ) - SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness - FROM selected - INNER JOIN witnesses ON selected.id_note = witnesses.note", - )?; - - // Select notes - let notes = stmt_select_notes.query_and_then::<_, Error, _, _>( - &[ - account as i64, - value.0, - value.0, - (height - ANCHOR_OFFSET) as i64, - ], - |row| { - let mut diversifier = Diversifier([0; 11]); - let d: Vec<_> = row.get(0); - diversifier.0.copy_from_slice(&d); - - let note_value: i64 = row.get(1); - - let d: Vec<_> = row.get(2); - let rcm = { - let mut tmp = FsRepr::default(); - tmp.read_le(&d[..])?; - Fs::from_repr(tmp)? - }; - - let from = extfvk - .fvk - .vk - .into_payment_address(diversifier, &JUBJUB) - .unwrap(); - let note = from.create_note(note_value as u64, rcm, &JUBJUB).unwrap(); - - let d: Vec<_> = row.get(3); - let witness = IncrementalWitness::read(&d[..])?; - - Ok(SelectedNoteRow { - diversifier, - note, - witness, - }) - }, - )?; - - // Create the transaction - let mut builder = Builder::new(height); - for selected in notes { - let selected = selected?; - builder.add_sapling_spend( - extsk.clone(), - selected.diversifier, - selected.note, - selected.witness, - )?; - } - builder.add_sapling_output(ovk, to.clone(), value, memo.clone())?; - let (tx, tx_metadata) = builder.build(consensus_branch_id, prover)?; - // We only called add_sapling_output() once. - let output_index = tx_metadata.output_index(0).unwrap() as i64; - - // Save the transaction in the database. - let mut raw_tx = vec![]; - tx.write(&mut raw_tx)?; - let mut stmt_insert_tx = data.prepare( - "INSERT INTO transactions (txid, raw) - VALUES (?, ?)", - )?; - stmt_insert_tx.execute(&[&tx.txid().0[..], &raw_tx[..]])?; - let id_tx = data.last_insert_rowid(); - - // Save the sent note in the database. - let to_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, to); - if memo.is_some() { - let mut stmt_insert_sent_note = data.prepare( - "INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo) - VALUES (?, ?, ?, ?, ?, ?)", - )?; - stmt_insert_sent_note.execute(&[ - id_tx.to_sql()?, - output_index.to_sql()?, - account.to_sql()?, - to_str.to_sql()?, - value.0.to_sql()?, - memo.unwrap().as_bytes().to_sql()?, - ])?; - } else { - let mut stmt_insert_sent_note = data.prepare( - "INSERT INTO sent_notes (tx, output_index, from_account, address, value) - VALUES (?, ?, ?, ?, ?)", - )?; - stmt_insert_sent_note.execute(&[ - id_tx.to_sql()?, - output_index.to_sql()?, - account.to_sql()?, - to_str.to_sql()?, - value.0.to_sql()?, - ])?; - } - - // Return the row number of the transaction, so the caller can fetch it for sending. - Ok(id_tx) -} - /// JNI interface #[cfg(target_os = "android")] #[allow(non_snake_case)] @@ -663,9 +44,10 @@ pub mod android { }; use self::jni::JNIEnv; - use super::{ + use super::SAPLING_CONSENSUS_BRANCH_ID; + use crate::sql::{ get_address, get_balance, init_accounts_table, init_blocks_table, init_data_database, - scan_cached_blocks, send_to_address, SAPLING_CONSENSUS_BRANCH_ID, + scan_cached_blocks, send_to_address, }; #[no_mangle] diff --git a/src/main/rust/sql.rs b/src/main/rust/sql.rs new file mode 100644 index 00000000..63830fa4 --- /dev/null +++ b/src/main/rust/sql.rs @@ -0,0 +1,620 @@ +use failure::{format_err, Error}; +use ff::{PrimeField, PrimeFieldRepr}; +use pairing::bls12_381::Bls12; +use protobuf::parse_from_bytes; +use rusqlite::{types::ToSql, Connection, NO_PARAMS}; +use sapling_crypto::{ + jubjub::fs::{Fs, FsRepr}, + primitives::{Diversifier, Note, PaymentAddress}, +}; +use zcash_client_backend::{ + constants::{HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, HRP_SAPLING_PAYMENT_ADDRESS_TEST}, + encoding::{ + decode_extended_full_viewing_key, encode_extended_full_viewing_key, encode_payment_address, + }, + note_encryption::Memo, + proto::compact_formats::CompactBlock, + prover::TxProver, + transaction::Builder, + welding_rig::scan_block, +}; +use zcash_primitives::{ + merkle_tree::{CommitmentTree, IncrementalWitness}, + transaction::components::Amount, + JUBJUB, +}; +use zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; + +const ANCHOR_OFFSET: u32 = 10; + +fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String { + let addr = extfvk.default_address().unwrap().1; + encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &addr) +} + +pub fn init_data_database(db_data: &str) -> rusqlite::Result<()> { + let data = Connection::open(db_data)?; + data.execute( + "CREATE TABLE IF NOT EXISTS accounts ( + account INTEGER PRIMARY KEY, + extfvk TEXT NOT NULL, + address TEXT NOT NULL + )", + NO_PARAMS, + )?; + data.execute( + "CREATE TABLE IF NOT EXISTS blocks ( + height INTEGER PRIMARY KEY, + time INTEGER NOT NULL, + sapling_tree BLOB NOT NULL + )", + NO_PARAMS, + )?; + data.execute( + "CREATE TABLE IF NOT EXISTS transactions ( + id_tx INTEGER PRIMARY KEY, + txid BLOB NOT NULL UNIQUE, + block INTEGER, + tx_index INTEGER, + raw BLOB, + FOREIGN KEY (block) REFERENCES blocks(height) + )", + NO_PARAMS, + )?; + data.execute( + "CREATE TABLE IF NOT EXISTS received_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + account INTEGER NOT NULL, + diversifier BLOB NOT NULL, + value INTEGER NOT NULL, + rcm BLOB NOT NULL, + nf BLOB NOT NULL UNIQUE, + is_change BOOLEAN NOT NULL, + memo BLOB, + spent INTEGER, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (account) REFERENCES accounts(account), + FOREIGN KEY (spent) REFERENCES transactions(id_tx), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + NO_PARAMS, + )?; + data.execute( + "CREATE TABLE IF NOT EXISTS sapling_witnesses ( + id_witness INTEGER PRIMARY KEY, + note INTEGER NOT NULL, + block INTEGER NOT NULL, + witness BLOB NOT NULL, + FOREIGN KEY (note) REFERENCES received_notes(id_note), + FOREIGN KEY (block) REFERENCES blocks(height), + CONSTRAINT witness_height UNIQUE (note, block) + )", + NO_PARAMS, + )?; + data.execute( + "CREATE TABLE IF NOT EXISTS sent_notes ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + address TEXT NOT NULL, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_index) + )", + NO_PARAMS, + )?; + Ok(()) +} + +pub fn init_accounts_table(db_data: &str, extfvks: &[ExtendedFullViewingKey]) -> Result<(), Error> { + let data = Connection::open(db_data)?; + + let mut empty_check = data.prepare("SELECT * FROM accounts LIMIT 1")?; + if empty_check.exists(NO_PARAMS)? { + return Err(format_err!("accounts table is not empty")); + } + + // Insert accounts atomically + data.execute("BEGIN IMMEDIATE", NO_PARAMS)?; + for (account, extfvk) in extfvks.iter().enumerate() { + let address = address_from_extfvk(extfvk); + let extfvk = + encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, extfvk); + data.execute( + "INSERT INTO accounts (account, extfvk, address) + VALUES (?, ?, ?)", + &[ + (account as u32).to_sql()?, + extfvk.to_sql()?, + address.to_sql()?, + ], + )?; + } + data.execute("COMMIT", NO_PARAMS)?; + + Ok(()) +} + +pub fn init_blocks_table( + db_data: &str, + height: i32, + time: u32, + sapling_tree: &[u8], +) -> Result<(), Error> { + let data = Connection::open(db_data)?; + + let mut empty_check = data.prepare("SELECT * FROM blocks LIMIT 1")?; + if empty_check.exists(NO_PARAMS)? { + return Err(format_err!("blocks table is not empty")); + } + + data.execute( + "INSERT INTO blocks (height, time, sapling_tree) + VALUES (?, ?, ?)", + &[height.to_sql()?, time.to_sql()?, sapling_tree.to_sql()?], + )?; + + Ok(()) +} + +struct CompactBlockRow { + height: i32, + data: Vec, +} + +#[derive(Clone)] +struct WitnessRow { + id_note: i64, + witness: IncrementalWitness, +} + +pub fn get_address(db_data: &str, account: u32) -> Result { + let data = Connection::open(db_data)?; + + let addr = data.query_row( + "SELECT address FROM accounts + WHERE account = ?", + &[account], + |row| row.get(0), + )?; + + Ok(addr) +} + +pub fn get_balance(db_data: &str, account: u32) -> Result { + let data = Connection::open(db_data)?; + + let balance = data.query_row( + "SELECT SUM(value) FROM received_notes + WHERE account = ? AND spent IS NULL", + &[account], + |row| row.get_checked(0).unwrap_or(0), + )?; + + Ok(Amount(balance)) +} + +/// Scans new blocks added to the cache for any transactions received by the +/// tracked accounts. +/// +/// Assumes that the caller is handling rollbacks. +pub fn scan_cached_blocks(db_cache: &str, db_data: &str) -> Result<(), Error> { + let cache = Connection::open(db_cache)?; + let data = Connection::open(db_data)?; + + // Recall where we synced up to previously. + // If we have never synced, use 0 to select all cached CompactBlocks. + let mut last_height = + data.query_row( + "SELECT MAX(height) FROM blocks", + NO_PARAMS, + |row| match row.get_checked(0) { + Ok(h) => h, + Err(_) => 0, + }, + )?; + + // Prepare necessary SQL statements + let mut stmt_blocks = cache + .prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC")?; + let mut stmt_fetch_tree = data.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?; + let mut stmt_fetch_witnesses = + data.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?; + let mut stmt_fetch_nullifiers = + data.prepare("SELECT id_note, nf, account FROM received_notes WHERE spent IS NULL")?; + let mut stmt_insert_block = data.prepare( + "INSERT INTO blocks (height, time, sapling_tree) + VALUES (?, ?, ?)", + )?; + let mut stmt_update_tx = data.prepare( + "UPDATE transactions + SET block = ?, tx_index = ? WHERE txid = ?", + )?; + let mut stmt_insert_tx = data.prepare( + "INSERT INTO transactions (txid, block, tx_index) + VALUES (?, ?, ?)", + )?; + let mut stmt_select_tx = data.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?; + let mut stmt_mark_spent_note = + data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?; + let mut stmt_insert_note = data.prepare( + "INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + )?; + let mut stmt_insert_witness = data.prepare( + "INSERT INTO sapling_witnesses (note, block, witness) + VALUES (?, ?, ?)", + )?; + let mut stmt_prune_witnesses = data.prepare("DELETE FROM sapling_witnesses WHERE block < ?")?; + + // Fetch the CompactBlocks we need to scan + let rows = stmt_blocks.query_map(&[last_height], |row| CompactBlockRow { + height: row.get(0), + data: row.get(1), + })?; + + // Fetch the ExtendedFullViewingKeys we are tracking + let mut stmt_fetch_accounts = + data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?; + let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| { + let extfvk: String = row.get(0); + decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, &extfvk) + })?; + // Raise SQL errors from the query, and IO errors from parsing. + let extfvks: Vec<_> = extfvks.collect::, _>>()??; + + // Get the most recent CommitmentTree + let mut tree = match stmt_fetch_tree.query_row(&[last_height], |row| match row.get_checked(0) { + Ok(data) => { + let data: Vec<_> = data; + CommitmentTree::read(&data[..]).unwrap() + } + Err(_) => CommitmentTree::new(), + }) { + Ok(tree) => tree, + Err(_) => CommitmentTree::new(), + }; + + // Get most recent incremental witnesses for the notes we are tracking + let witnesses = stmt_fetch_witnesses.query_map(&[last_height], |row| { + let data: Vec<_> = row.get(1); + WitnessRow { + id_note: row.get(0), + witness: IncrementalWitness::read(&data[..]).unwrap(), + } + })?; + let mut witnesses: Vec<_> = witnesses.collect::>()?; + + // Get the nullifiers for the notes we are tracking + let nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| { + let nf: Vec<_> = row.get(1); + let account: i64 = row.get(2); + (nf, account as usize) + })?; + let mut nullifiers: Vec<_> = nullifiers.collect::>()?; + + for row in rows { + let row = row?; + + // Start an SQL transaction for this block. + data.execute("BEGIN IMMEDIATE", NO_PARAMS)?; + + // Scanned blocks MUST be height-ascending, but they might not be height-sequential + // (e.g. if blocks that don't contain Sapling data are skipped). Note that this + // introduces a risk that a block containing Sapling data is skipped; it is up to + // the caller to ensure this does not happen. + if row.height <= last_height { + return Err(format_err!( + "Expected height of next CompactBlock to be at least {}, but was {}", + last_height + 1, + row.height + )); + } + last_height = row.height; + + let block: CompactBlock = parse_from_bytes(&row.data)?; + let block_time = block.time; + + let txs = { + let nf_refs: Vec<_> = nullifiers.iter().map(|(nf, acc)| (&nf[..], *acc)).collect(); + let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.witness).collect(); + scan_block( + block, + &extfvks[..], + &nf_refs, + &mut tree, + &mut witness_refs[..], + ) + }; + + // Insert the block into the database. + let mut encoded_tree = Vec::new(); + tree.write(&mut encoded_tree).unwrap(); + stmt_insert_block.execute(&[ + row.height.to_sql()?, + block_time.to_sql()?, + encoded_tree.to_sql()?, + ])?; + + for (tx, new_witnesses) in txs { + // First try update an existing transaction in the database. + let txid = tx.txid.0.to_vec(); + let tx_row = if stmt_update_tx.execute(&[ + row.height.to_sql()?, + (tx.index as i64).to_sql()?, + txid.to_sql()?, + ])? == 0 + { + // It isn't there, so insert our transaction into the database. + stmt_insert_tx.execute(&[ + txid.to_sql()?, + row.height.to_sql()?, + (tx.index as i64).to_sql()?, + ])?; + data.last_insert_rowid() + } else { + // It was there, so grab its row number. + stmt_select_tx.query_row(&[txid], |row| row.get(0))? + }; + + // Mark notes as spent and remove them from the scanning cache + for spend in &tx.shielded_spends { + stmt_mark_spent_note.execute(&[tx_row.to_sql()?, spend.nf.to_sql()?])?; + } + nullifiers = nullifiers + .into_iter() + .filter(|(nf, _acc)| { + tx.shielded_spends + .iter() + .find(|spend| &spend.nf == nf) + .is_none() + }) + .collect(); + + for (output, witness) in tx + .shielded_outputs + .into_iter() + .zip(new_witnesses.into_iter()) + { + let mut rcm = [0; 32]; + output.note.r.into_repr().write_le(&mut rcm[..])?; + let nf = output.note.nf( + &extfvks[output.account].fvk.vk, + witness.position() as u64, + &JUBJUB, + ); + + // Insert received note into the database. + // Assumptions: + // - A transaction will not contain more than 2^63 shielded outputs. + // - A note value will never exceed 2^63 zatoshis. + stmt_insert_note.execute(&[ + tx_row.to_sql()?, + (output.index as i64).to_sql()?, + (output.account as i64).to_sql()?, + output.to.diversifier.0.to_sql()?, + (output.note.value as i64).to_sql()?, + rcm.to_sql()?, + nf.to_sql()?, + output.is_change.to_sql()?, + ])?; + let note_row = data.last_insert_rowid(); + + // Save witness for note. + witnesses.push(WitnessRow { + id_note: note_row, + witness, + }); + + // Cache nullifier for note (to detect subsequent spends in this scan). + nullifiers.push((nf, output.account)); + } + } + + // Insert current witnesses into the database. + let mut encoded = Vec::new(); + for witness_row in witnesses.iter() { + encoded.clear(); + witness_row.witness.write(&mut encoded).unwrap(); + stmt_insert_witness.execute(&[ + witness_row.id_note.to_sql()?, + last_height.to_sql()?, + encoded.to_sql()?, + ])?; + } + + // Prune the stored witnesses (we only expect rollbacks of at most 100 blocks). + stmt_prune_witnesses.execute(&[last_height - 100])?; + + // Commit the SQL transaction, writing this block's data atomically. + data.execute("COMMIT", NO_PARAMS)?; + } + + Ok(()) +} + +struct SelectedNoteRow { + diversifier: Diversifier, + note: Note, + witness: IncrementalWitness, +} + +/// Creates a transaction paying the specified address. +pub fn send_to_address( + db_data: &str, + consensus_branch_id: u32, + prover: impl TxProver, + account: u32, + extsk: &ExtendedSpendingKey, + to: &PaymentAddress, + value: Amount, + memo: Option, +) -> Result { + let data = Connection::open(db_data)?; + + // Check that the ExtendedSpendingKey we have been given corresponds to the + // ExtendedFullViewingKey for the account we are spending from. + let extfvk = ExtendedFullViewingKey::from(extsk); + if !data + .prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")? + .exists(&[ + account.to_sql()?, + encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY_TEST, &extfvk) + .to_sql()?, + ])? + { + return Err(format_err!( + "Incorrect ExtendedSpendingKey for account {}", + account + )); + } + let ovk = extfvk.fvk.ovk; + + // Target the next block, assuming we are up-to-date. + let height = data.query_row_and_then("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| { + let ret: Result = row.get_checked(0); + ret + })? + 1; + + // The goal of this SQL statement is to select the oldest notes until the required + // value has been reached, and then fetch the witnesses at the desired height for the + // selected notes. This is achieved in several steps: + // + // 1) Use a window function to create a view of all notes, ordered from oldest to + // newest, with an additional column containing a running sum: + // - Unspent notes accumulate the values of all unspent notes in that note's + // account, up to itself. + // - Spent notes accumulate the values of all notes in the transaction they were + // spent in, up to itself. + // + // 2) Select all unspent notes in the desired account, along with their running sum. + // + // 3) Select all notes for which the running sum was less than the required value, as + // well as a single note for which the sum was greater than or equal to the + // required value, bringing the sum of all selected notes across the threshold. + // + // 4) Match the selected notes against the witnesses at the desired height. + let mut stmt_select_notes = data.prepare( + "WITH selected AS ( + WITH eligible AS ( + SELECT id_note, diversifier, value, rcm, + SUM(value) OVER + (PARTITION BY account, spent ORDER BY id_note) AS so_far + FROM received_notes + WHERE account = ? AND spent IS NULL + ) + SELECT * FROM eligible WHERE so_far < ? + UNION + SELECT * FROM (SELECT * FROM eligible WHERE so_far >= ? LIMIT 1) + ), witnesses AS ( + SELECT note, witness FROM sapling_witnesses + WHERE block = ? + ) + SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness + FROM selected + INNER JOIN witnesses ON selected.id_note = witnesses.note", + )?; + + // Select notes + let notes = stmt_select_notes.query_and_then::<_, Error, _, _>( + &[ + account as i64, + value.0, + value.0, + (height - ANCHOR_OFFSET) as i64, + ], + |row| { + let mut diversifier = Diversifier([0; 11]); + let d: Vec<_> = row.get(0); + diversifier.0.copy_from_slice(&d); + + let note_value: i64 = row.get(1); + + let d: Vec<_> = row.get(2); + let rcm = { + let mut tmp = FsRepr::default(); + tmp.read_le(&d[..])?; + Fs::from_repr(tmp)? + }; + + let from = extfvk + .fvk + .vk + .into_payment_address(diversifier, &JUBJUB) + .unwrap(); + let note = from.create_note(note_value as u64, rcm, &JUBJUB).unwrap(); + + let d: Vec<_> = row.get(3); + let witness = IncrementalWitness::read(&d[..])?; + + Ok(SelectedNoteRow { + diversifier, + note, + witness, + }) + }, + )?; + + // Create the transaction + let mut builder = Builder::new(height); + for selected in notes { + let selected = selected?; + builder.add_sapling_spend( + extsk.clone(), + selected.diversifier, + selected.note, + selected.witness, + )?; + } + builder.add_sapling_output(ovk, to.clone(), value, memo.clone())?; + let (tx, tx_metadata) = builder.build(consensus_branch_id, prover)?; + // We only called add_sapling_output() once. + let output_index = tx_metadata.output_index(0).unwrap() as i64; + + // Save the transaction in the database. + let mut raw_tx = vec![]; + tx.write(&mut raw_tx)?; + let mut stmt_insert_tx = data.prepare( + "INSERT INTO transactions (txid, raw) + VALUES (?, ?)", + )?; + stmt_insert_tx.execute(&[&tx.txid().0[..], &raw_tx[..]])?; + let id_tx = data.last_insert_rowid(); + + // Save the sent note in the database. + let to_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, to); + if memo.is_some() { + let mut stmt_insert_sent_note = data.prepare( + "INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo) + VALUES (?, ?, ?, ?, ?, ?)", + )?; + stmt_insert_sent_note.execute(&[ + id_tx.to_sql()?, + output_index.to_sql()?, + account.to_sql()?, + to_str.to_sql()?, + value.0.to_sql()?, + memo.unwrap().as_bytes().to_sql()?, + ])?; + } else { + let mut stmt_insert_sent_note = data.prepare( + "INSERT INTO sent_notes (tx, output_index, from_account, address, value) + VALUES (?, ?, ?, ?, ?)", + )?; + stmt_insert_sent_note.execute(&[ + id_tx.to_sql()?, + output_index.to_sql()?, + account.to_sql()?, + to_str.to_sql()?, + value.0.to_sql()?, + ])?; + } + + // Return the row number of the transaction, so the caller can fetch it for sending. + Ok(id_tx) +} From a9674a148b3c051197778e496a9942b577b9dfe6 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Feb 2019 12:56:04 +0000 Subject: [PATCH 2/4] Remove cfg(target_os="android") restriction The crate lives inside the Android SDK, and will only ever target Android devices, so it's fine to simplify here. --- Cargo.toml | 8 +- src/main/rust/lib.rs | 658 +++++++++++++++++++++---------------------- 2 files changed, 327 insertions(+), 339 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dcc6738c..8c52ef14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,10 @@ description = "JNI backend for the Android wallet SDK" publish = false [dependencies] +android_logger = "0.6" failure = "0.1" +jni = { version = "0.10", default-features = false } +log-panics = "2.0.0" protobuf = "2" rand = "0.4" rusqlite = { version = "0.15", features = ["bundled"] } @@ -39,11 +42,6 @@ rev = "89cfef8515d5d88809c485a44fdc54572b9e5666" git = "https://github.com/str4d/librustzcash.git" rev = "89cfef8515d5d88809c485a44fdc54572b9e5666" -[target.'cfg(target_os="android")'.dependencies] -jni = { version = "0.10", default-features = false } -android_logger = "0.6" -log-panics = "2.0.0" - [lib] name = "zcashwalletsdk" path = "src/main/rust/lib.rs" diff --git a/src/main/rust/lib.rs b/src/main/rust/lib.rs index c5897bd4..7abfeed5 100644 --- a/src/main/rust/lib.rs +++ b/src/main/rust/lib.rs @@ -16,348 +16,338 @@ mod sql; const SAPLING_CONSENSUS_BRANCH_ID: u32 = 0x76b8_09bb; -/// JNI interface -#[cfg(target_os = "android")] -#[allow(non_snake_case)] -pub mod android { - extern crate android_logger; - extern crate jni; - extern crate log_panics; +extern crate android_logger; +extern crate jni; +extern crate log_panics; - use log::Level; - use std::path::Path; - use zcash_client_backend::{ - constants::{HRP_SAPLING_EXTENDED_SPENDING_KEY_TEST, HRP_SAPLING_PAYMENT_ADDRESS_TEST}, - encoding::{ - decode_extended_spending_key, decode_payment_address, encode_extended_spending_key, - }, - note_encryption::Memo, - prover::LocalTxProver, - }; - use zcash_primitives::transaction::components::Amount; - use zip32::{ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey}; +use log::Level; +use std::path::Path; +use zcash_client_backend::{ + constants::{HRP_SAPLING_EXTENDED_SPENDING_KEY_TEST, HRP_SAPLING_PAYMENT_ADDRESS_TEST}, + encoding::{ + decode_extended_spending_key, decode_payment_address, encode_extended_spending_key, + }, + note_encryption::Memo, + prover::LocalTxProver, +}; +use zcash_primitives::transaction::components::Amount; +use zip32::{ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey}; - use self::android_logger::Filter; - use self::jni::objects::{JClass, JString}; - use self::jni::sys::{ - jboolean, jbyteArray, jint, jlong, jobjectArray, jsize, jstring, JNI_FALSE, JNI_TRUE, - }; - use self::jni::JNIEnv; +use self::android_logger::Filter; +use self::jni::objects::{JClass, JString}; +use self::jni::sys::{ + jboolean, jbyteArray, jint, jlong, jobjectArray, jsize, jstring, JNI_FALSE, JNI_TRUE, +}; +use self::jni::JNIEnv; - use super::SAPLING_CONSENSUS_BRANCH_ID; - use crate::sql::{ - get_address, get_balance, init_accounts_table, init_blocks_table, init_data_database, - scan_cached_blocks, send_to_address, - }; +use crate::sql::{ + get_address, get_balance, init_accounts_table, init_blocks_table, init_data_database, + scan_cached_blocks, send_to_address, +}; - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initLogs( - _env: JNIEnv, - _: JClass, - ) { - android_logger::init_once( - Filter::default().with_min_level(Level::Trace), - Some("cash.z.rust.logs"), - ); +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initLogs(_env: JNIEnv, _: JClass) { + android_logger::init_once( + Filter::default().with_min_level(Level::Trace), + Some("cash.z.rust.logs"), + ); - log_panics::init(); + log_panics::init(); - debug!("logs have been initialized successfully"); - } + debug!("logs have been initialized successfully"); +} - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initDataDb( - env: JNIEnv, - _: JClass, - db_data: JString, - ) -> jboolean { - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initDataDb( + env: JNIEnv, + _: JClass, + db_data: JString, +) -> jboolean { + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); - match init_data_database(&db_data) { - Ok(()) => JNI_TRUE, - Err(e) => { - error!("Error while initializing data DB: {}", e); - JNI_FALSE - } - } - } - - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initAccountsTable( - env: JNIEnv, - _: JClass, - db_data: JString, - seed: jbyteArray, - accounts: jint, - ) -> jobjectArray { - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); - let seed = env.convert_byte_array(seed).unwrap(); - - let ret = if accounts >= 0 { - let master = ExtendedSpendingKey::master(&seed); - let extsks: Vec<_> = (0..accounts as u32) - .map(|account| { - ExtendedSpendingKey::from_path( - &master, - &[ - ChildIndex::Hardened(32), - ChildIndex::Hardened(1), - ChildIndex::Hardened(account), - ], - ) - }) - .collect(); - let extfvks: Vec<_> = extsks - .iter() - .map(|extsk| ExtendedFullViewingKey::from(extsk)) - .collect(); - - match init_accounts_table(&db_data, &extfvks) { - Ok(()) => { - // Return the ExtendedSpendingKeys for the created accounts - extsks - } - Err(e) => { - error!("Error while initializing accounts: {}", e); - // Return an empty array to indicate an error - vec![] - } - } - } else { - error!("accounts argument must be positive"); - // Return an empty array to indicate an error - vec![] - }; - - let jempty = env.new_string("").expect("Couldn't create Java string!"); - let jret = env - .new_object_array(ret.len() as jsize, "java/lang/String", *jempty) - .expect("Couldn't create Java array!"); - for (i, extsk) in ret.into_iter().enumerate() { - let jextsk = env - .new_string(encode_extended_spending_key( - HRP_SAPLING_EXTENDED_SPENDING_KEY_TEST, - &extsk, - )) - .expect("Couldn't create Java string!"); - env.set_object_array_element(jret, i as jsize, *jextsk) - .expect("Couldn't set Java array element!"); - } - jret - } - - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initBlocksTable( - env: JNIEnv, - _: JClass, - db_data: JString, - height: jint, - time: jlong, - sapling_tree: jbyteArray, - ) -> jboolean { - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); - let time = if time >= 0 && time <= (u32::max_value() as jlong) { - time as u32 - } else { - error!("time argument must fit in a u32"); - return JNI_FALSE; - }; - let sapling_tree = env.convert_byte_array(sapling_tree).unwrap(); - - match init_blocks_table(&db_data, height, time, &sapling_tree) { - Ok(()) => JNI_TRUE, - Err(e) => { - error!("Error while initializing data DB: {}", e); - JNI_FALSE - } - } - } - - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getAddress( - env: JNIEnv, - _: JClass, - db_data: JString, - account: jint, - ) -> jstring { - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); - - let addr = match account { - acc if acc >= 0 => match get_address(&db_data, acc as u32) { - Ok(addr) => addr, - Err(e) => { - error!("Error while fetching address: {}", e); - // Return an empty string to indicate an error - String::default() - } - }, - _ => { - error!("account argument must be positive"); - // Return an empty string to indicate an error - String::default() - } - }; - - let output = env.new_string(addr).expect("Couldn't create Java string!"); - output.into_inner() - } - - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getBalance( - env: JNIEnv, - _: JClass, - db_data: JString, - account: jint, - ) -> jlong { - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); - let account = if account >= 0 { - account as u32 - } else { - error!("account argument must be positive"); - return -1; - }; - - match get_balance(&db_data, account) { - Ok(balance) => balance.0, - Err(e) => { - error!("Error while fetching balance: {}", e); - -1 - } - } - } - - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_scanBlocks( - env: JNIEnv, - _: JClass, - db_cache: JString, - db_data: JString, - ) -> jboolean { - let db_cache: String = env - .get_string(db_cache) - .expect("Couldn't get Java string!") - .into(); - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); - - match scan_cached_blocks(&db_cache, &db_data) { - Ok(()) => JNI_TRUE, - Err(e) => { - error!("Error while scanning blocks: {}", e); - JNI_FALSE - } - } - } - - #[no_mangle] - pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_sendToAddress( - env: JNIEnv, - _: JClass, - db_data: JString, - account: jint, - extsk: JString, - to: JString, - value: jlong, - memo: JString, - spend_params: JString, - output_params: JString, - ) -> jlong { - let db_data: String = env - .get_string(db_data) - .expect("Couldn't get Java string!") - .into(); - let account = if account >= 0 { - account as u32 - } else { - error!("account argument must be positive"); - return -1; - }; - let extsk: String = env - .get_string(extsk) - .expect("Couldn't get Java string!") - .into(); - let to: String = env - .get_string(to) - .expect("Couldn't get Java string!") - .into(); - let value = Amount(value); - let memo: String = env - .get_string(memo) - .expect("Couldn't get Java string!") - .into(); - let spend_params: String = env - .get_string(spend_params) - .expect("Couldn't get Java string!") - .into(); - let output_params: String = env - .get_string(output_params) - .expect("Couldn't get Java string!") - .into(); - - let extsk = - match decode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY_TEST, &extsk) { - Ok(extsk) => extsk, - Err(e) => { - error!("Invalid ExtendedSpendingKey: {}", e); - return -1; - } - }; - - let to = match decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &to) { - Ok(to) => to, - Err(e) => { - error!("Invalid PaymentAddress: {}", e); - return -1; - } - }; - - let memo = match Memo::from_str(&memo) { - Ok(memo) => Some(memo), - Err(()) => { - error!("Memo is too long"); - return -1; - } - }; - - let prover = LocalTxProver::new( - Path::new(&spend_params), - "8270785a1a0d0bc77196f000ee6d221c9c9894f55307bd9357c3f0105d31ca63991ab91324160d8f53e2bbd3c2633a6eb8bdf5205d822e7f3f73edac51b2b70c", - Path::new(&output_params), - "657e3d38dbb5cb5e7dd2970e8b03d69b4787dd907285b5a7f0790dcc8072f60bf593b32cc2d1c030e00ff5ae64bf84c5c3beb84ddc841d48264b4a171744d028", - ); - - match send_to_address( - &db_data, - SAPLING_CONSENSUS_BRANCH_ID, - prover, - account, - &extsk, - &to, - value, - memo, - ) { - Ok(tx_row) => tx_row, - Err(e) => { - error!("Error while sending funds: {}", e); - -1 - } + match init_data_database(&db_data) { + Ok(()) => JNI_TRUE, + Err(e) => { + error!("Error while initializing data DB: {}", e); + JNI_FALSE + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initAccountsTable( + env: JNIEnv, + _: JClass, + db_data: JString, + seed: jbyteArray, + accounts: jint, +) -> jobjectArray { + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); + let seed = env.convert_byte_array(seed).unwrap(); + + let ret = if accounts >= 0 { + let master = ExtendedSpendingKey::master(&seed); + let extsks: Vec<_> = (0..accounts as u32) + .map(|account| { + ExtendedSpendingKey::from_path( + &master, + &[ + ChildIndex::Hardened(32), + ChildIndex::Hardened(1), + ChildIndex::Hardened(account), + ], + ) + }) + .collect(); + let extfvks: Vec<_> = extsks + .iter() + .map(|extsk| ExtendedFullViewingKey::from(extsk)) + .collect(); + + match init_accounts_table(&db_data, &extfvks) { + Ok(()) => { + // Return the ExtendedSpendingKeys for the created accounts + extsks + } + Err(e) => { + error!("Error while initializing accounts: {}", e); + // Return an empty array to indicate an error + vec![] + } + } + } else { + error!("accounts argument must be positive"); + // Return an empty array to indicate an error + vec![] + }; + + let jempty = env.new_string("").expect("Couldn't create Java string!"); + let jret = env + .new_object_array(ret.len() as jsize, "java/lang/String", *jempty) + .expect("Couldn't create Java array!"); + for (i, extsk) in ret.into_iter().enumerate() { + let jextsk = env + .new_string(encode_extended_spending_key( + HRP_SAPLING_EXTENDED_SPENDING_KEY_TEST, + &extsk, + )) + .expect("Couldn't create Java string!"); + env.set_object_array_element(jret, i as jsize, *jextsk) + .expect("Couldn't set Java array element!"); + } + jret +} + +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initBlocksTable( + env: JNIEnv, + _: JClass, + db_data: JString, + height: jint, + time: jlong, + sapling_tree: jbyteArray, +) -> jboolean { + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); + let time = if time >= 0 && time <= (u32::max_value() as jlong) { + time as u32 + } else { + error!("time argument must fit in a u32"); + return JNI_FALSE; + }; + let sapling_tree = env.convert_byte_array(sapling_tree).unwrap(); + + match init_blocks_table(&db_data, height, time, &sapling_tree) { + Ok(()) => JNI_TRUE, + Err(e) => { + error!("Error while initializing data DB: {}", e); + JNI_FALSE + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getAddress( + env: JNIEnv, + _: JClass, + db_data: JString, + account: jint, +) -> jstring { + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); + + let addr = match account { + acc if acc >= 0 => match get_address(&db_data, acc as u32) { + Ok(addr) => addr, + Err(e) => { + error!("Error while fetching address: {}", e); + // Return an empty string to indicate an error + String::default() + } + }, + _ => { + error!("account argument must be positive"); + // Return an empty string to indicate an error + String::default() + } + }; + + let output = env.new_string(addr).expect("Couldn't create Java string!"); + output.into_inner() +} + +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_getBalance( + env: JNIEnv, + _: JClass, + db_data: JString, + account: jint, +) -> jlong { + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); + let account = if account >= 0 { + account as u32 + } else { + error!("account argument must be positive"); + return -1; + }; + + match get_balance(&db_data, account) { + Ok(balance) => balance.0, + Err(e) => { + error!("Error while fetching balance: {}", e); + -1 + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_scanBlocks( + env: JNIEnv, + _: JClass, + db_cache: JString, + db_data: JString, +) -> jboolean { + let db_cache: String = env + .get_string(db_cache) + .expect("Couldn't get Java string!") + .into(); + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); + + match scan_cached_blocks(&db_cache, &db_data) { + Ok(()) => JNI_TRUE, + Err(e) => { + error!("Error while scanning blocks: {}", e); + JNI_FALSE + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_sendToAddress( + env: JNIEnv, + _: JClass, + db_data: JString, + account: jint, + extsk: JString, + to: JString, + value: jlong, + memo: JString, + spend_params: JString, + output_params: JString, +) -> jlong { + let db_data: String = env + .get_string(db_data) + .expect("Couldn't get Java string!") + .into(); + let account = if account >= 0 { + account as u32 + } else { + error!("account argument must be positive"); + return -1; + }; + let extsk: String = env + .get_string(extsk) + .expect("Couldn't get Java string!") + .into(); + let to: String = env + .get_string(to) + .expect("Couldn't get Java string!") + .into(); + let value = Amount(value); + let memo: String = env + .get_string(memo) + .expect("Couldn't get Java string!") + .into(); + let spend_params: String = env + .get_string(spend_params) + .expect("Couldn't get Java string!") + .into(); + let output_params: String = env + .get_string(output_params) + .expect("Couldn't get Java string!") + .into(); + + let extsk = match decode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY_TEST, &extsk) { + Ok(extsk) => extsk, + Err(e) => { + error!("Invalid ExtendedSpendingKey: {}", e); + return -1; + } + }; + + let to = match decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &to) { + Ok(to) => to, + Err(e) => { + error!("Invalid PaymentAddress: {}", e); + return -1; + } + }; + + let memo = match Memo::from_str(&memo) { + Ok(memo) => Some(memo), + Err(()) => { + error!("Memo is too long"); + return -1; + } + }; + + let prover = LocalTxProver::new( + Path::new(&spend_params), + "8270785a1a0d0bc77196f000ee6d221c9c9894f55307bd9357c3f0105d31ca63991ab91324160d8f53e2bbd3c2633a6eb8bdf5205d822e7f3f73edac51b2b70c", + Path::new(&output_params), + "657e3d38dbb5cb5e7dd2970e8b03d69b4787dd907285b5a7f0790dcc8072f60bf593b32cc2d1c030e00ff5ae64bf84c5c3beb84ddc841d48264b4a171744d028", + ); + + match send_to_address( + &db_data, + SAPLING_CONSENSUS_BRANCH_ID, + prover, + account, + &extsk, + &to, + value, + memo, + ) { + Ok(tx_row) => tx_row, + Err(e) => { + error!("Error while sending funds: {}", e); + -1 } } } From acd8c395f3a214c3ec28d96c494c20f79ba00683 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Feb 2019 12:58:33 +0000 Subject: [PATCH 3/4] Tidy up imports --- Cargo.toml | 2 +- src/main/rust/lib.rs | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c52ef14..3e5cac5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,11 @@ publish = false android_logger = "0.6" failure = "0.1" jni = { version = "0.10", default-features = false } +log = "0.4" log-panics = "2.0.0" protobuf = "2" rand = "0.4" rusqlite = { version = "0.15", features = ["bundled"] } -log = "0.4" [dependencies.ff] git = "https://github.com/str4d/librustzcash.git" diff --git a/src/main/rust/lib.rs b/src/main/rust/lib.rs index 7abfeed5..308a043c 100644 --- a/src/main/rust/lib.rs +++ b/src/main/rust/lib.rs @@ -3,7 +3,10 @@ extern crate failure; #[macro_use] extern crate log; +extern crate android_logger; extern crate ff; +extern crate jni; +extern crate log_panics; extern crate pairing; extern crate protobuf; extern crate rusqlite; @@ -16,10 +19,12 @@ mod sql; const SAPLING_CONSENSUS_BRANCH_ID: u32 = 0x76b8_09bb; -extern crate android_logger; -extern crate jni; -extern crate log_panics; - +use android_logger::Filter; +use jni::{ + objects::{JClass, JString}, + sys::{jboolean, jbyteArray, jint, jlong, jobjectArray, jsize, jstring, JNI_FALSE, JNI_TRUE}, + JNIEnv, +}; use log::Level; use std::path::Path; use zcash_client_backend::{ @@ -33,13 +38,6 @@ use zcash_client_backend::{ use zcash_primitives::transaction::components::Amount; use zip32::{ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey}; -use self::android_logger::Filter; -use self::jni::objects::{JClass, JString}; -use self::jni::sys::{ - jboolean, jbyteArray, jint, jlong, jobjectArray, jsize, jstring, JNI_FALSE, JNI_TRUE, -}; -use self::jni::JNIEnv; - use crate::sql::{ get_address, get_balance, init_accounts_table, init_blocks_table, init_data_database, scan_cached_blocks, send_to_address, From c51a90f4de04ecb97cbfcedcd37c48e1db45fb84 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Feb 2019 13:01:54 +0000 Subject: [PATCH 4/4] Fix clippy lints --- src/main/rust/lib.rs | 10 +++------- src/main/rust/sql.rs | 7 +++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/rust/lib.rs b/src/main/rust/lib.rs index 308a043c..c16b5b27 100644 --- a/src/main/rust/lib.rs +++ b/src/main/rust/lib.rs @@ -103,10 +103,7 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initAccountsTab ) }) .collect(); - let extfvks: Vec<_> = extsks - .iter() - .map(|extsk| ExtendedFullViewingKey::from(extsk)) - .collect(); + let extfvks: Vec<_> = extsks.iter().map(ExtendedFullViewingKey::from).collect(); match init_accounts_table(&db_data, &extfvks) { Ok(()) => { @@ -155,7 +152,7 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_initBlocksTable .get_string(db_data) .expect("Couldn't get Java string!") .into(); - let time = if time >= 0 && time <= (u32::max_value() as jlong) { + let time = if time >= 0 && time <= jlong::from(u32::max_value()) { time as u32 } else { error!("time argument must fit in a u32"); @@ -336,8 +333,7 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_JniConverter_sendToAddress( &db_data, SAPLING_CONSENSUS_BRANCH_ID, prover, - account, - &extsk, + (account, &extsk), &to, value, memo, diff --git a/src/main/rust/sql.rs b/src/main/rust/sql.rs index 63830fa4..867018f1 100644 --- a/src/main/rust/sql.rs +++ b/src/main/rust/sql.rs @@ -449,8 +449,7 @@ pub fn send_to_address( db_data: &str, consensus_branch_id: u32, prover: impl TxProver, - account: u32, - extsk: &ExtendedSpendingKey, + (account, extsk): (u32, &ExtendedSpendingKey), to: &PaymentAddress, value: Amount, memo: Option, @@ -523,10 +522,10 @@ pub fn send_to_address( // Select notes let notes = stmt_select_notes.query_and_then::<_, Error, _, _>( &[ - account as i64, + i64::from(account), value.0, value.0, - (height - ANCHOR_OFFSET) as i64, + i64::from(height - ANCHOR_OFFSET), ], |row| { let mut diversifier = Diversifier([0; 11]);