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 std::cmp; use std::path::Path; 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}; // TODO: Expose this in zcash_client_backend const DEFAULT_FEE: i64 = 10000; const ANCHOR_OFFSET: u32 = 10; const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String { let addr = extfvk.default_address().unwrap().1; encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &addr) } /// Determine the target height for a transaction, and the height from which to /// select anchors, based on the current synchronised block chain. fn get_target_and_anchor_heights(data: &Connection) -> Result<(u32, u32), Error> { data.query_row_and_then( "SELECT MIN(height), MAX(height) FROM blocks", NO_PARAMS, |row| match (row.get_checked::<_, u32>(0), row.get_checked::<_, u32>(1)) { // If there are no blocks, the query returns NULL. (Err(rusqlite::Error::InvalidColumnType(_, _)), _) | (_, Err(rusqlite::Error::InvalidColumnType(_, _))) => { Err(format_err!("Must scan blocks first")) } (Err(e), _) | (_, Err(e)) => Err(e.into()), (Ok(min_height), Ok(max_height)) => { let target_height = max_height + 1; // Select an anchor ANCHOR_OFFSET back from the target block, // unless that would be before the earliest block we have. let anchor_height = cmp::max(target_height.saturating_sub(ANCHOR_OFFSET), min_height); Ok((target_height, anchor_height)) } }, ) } pub fn init_cache_database>(db_cache: P) -> Result<(), Error> { let cache = Connection::open(db_cache)?; cache.execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, data BLOB NOT NULL )", NO_PARAMS, )?; Ok(()) } pub fn init_data_database>(db_data: P) -> Result<(), Error> { 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, created TEXT, block INTEGER, tx_index INTEGER, expiry_height 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: P, 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: P, 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: P, 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: P, 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)) } /// Returns the verified balance for the account, which ignores notes that have been /// received too recently and are not yet deemed spendable. pub fn get_verified_balance>(db_data: P, account: u32) -> Result { let data = Connection::open(db_data)?; let (_, anchor_height) = get_target_and_anchor_heights(&data)?; let balance = data.query_row( "SELECT SUM(value) FROM received_notes INNER JOIN transactions ON transactions.id_tx = received_notes.tx WHERE account = ? AND spent IS NULL AND transactions.block <= ?", &[account, anchor_height], |row| row.get_checked(0).unwrap_or(0), )?; Ok(Amount(balance)) } pub fn get_received_memo_as_utf8>( db_data: P, id_note: i64, ) -> Result, Error> { let data = Connection::open(db_data)?; let memo: Vec<_> = data.query_row( "SELECT memo FROM received_notes WHERE id_note = ?", &[id_note], |row| row.get(0), )?; Memo::from_bytes(&memo).and_then(|memo| memo.to_utf8()) } pub fn get_sent_memo_as_utf8>( db_data: P, id_note: i64, ) -> Result, Error> { let data = Connection::open(db_data)?; let memo: Vec<_> = data.query_row( "SELECT memo FROM sent_notes WHERE id_note = ?", &[id_note], |row| row.get(0), )?; Memo::from_bytes(&memo).and_then(|memo| memo.to_utf8()) } /// 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, Q: AsRef>( db_cache: P, db_data: Q, ) -> 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 sapling activation height 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(_) => SAPLING_ACTIVATION_HEIGHT - 1, }, )?; // 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 < ?")?; let mut stmt_update_expired = data.prepare( "UPDATE received_notes SET spent = NULL WHERE EXISTS ( SELECT id_tx FROM transactions WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ? )", )?; // 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[..]) } Err(_) => Ok(CommitmentTree::new()), }) { Ok(tree) => tree, Err(_) => Ok(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); IncrementalWitness::read(&data[..]).map(|witness| WitnessRow { id_note: row.get(0), witness, }) })?; 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-sequential. if row.height != (last_height + 1) { return Err(format_err!( "Expected height of next CompactBlock to be {}, 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[..], ) }; // Enforce that all roots match { let cur_root = tree.root(); for row in &witnesses { if row.witness.root() != cur_root { return Err(format_err!( "Witness for note {} has incorrect anchor after scanning block {}", row.id_note, last_height )); } } for (tx, new_witnesses) in &txs { for (i, witness) in new_witnesses.iter().enumerate() { if witness.root() != cur_root { return Err(format_err!( "New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}", tx.shielded_outputs[i].index, tx.txid, last_height, witness.root(), )); } } } } // Insert the block into the database. let mut encoded_tree = Vec::new(); tree.write(&mut encoded_tree) .expect("Should be able to write to a Vec"); 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) .expect("Should be able to write to a Vec"); 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])?; // Update now-expired transactions that didn't get mined. stmt_update_expired.execute(&[last_height])?; // 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. /// /// Do not call this multiple times in parallel, or you will generate transactions that /// double-spend the same notes. pub fn send_to_address>( db_data: P, consensus_branch_id: u32, prover: impl TxProver, (account, extsk): (u32, &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, anchor_height) = { let (target_height, anchor_height) = get_target_and_anchor_heights(&data)?; (target_height, i64::from(anchor_height)) }; // 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 target_value = value.0 + DEFAULT_FEE; debug!( "Selecting notes from account {} targeting {} zatoshis and anchor height {}", account, target_value, anchor_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 INNER JOIN transactions ON transactions.id_tx = received_notes.tx WHERE account = ? AND spent IS NULL AND transactions.block <= ? ) 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, _, _>( &[ i64::from(account), anchor_height, target_value, target_value, anchor_height, ], |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); debug!("Selected note with value {}", note_value); 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, }) }, )?; let notes: Vec = notes.collect::>()?; // Confirm we were able to select sufficient value let selected_value = notes .iter() .fold(0, |acc, selected| acc + selected.note.value); if selected_value < target_value as u64 { return Err(format_err!( "Insufficient balance (have {}, need {} including fee)", selected_value, target_value )); } // Create the transaction let mut builder = Builder::new(height); for selected in notes { 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 = match tx_metadata.output_index(0) { Some(idx) => idx as i64, None => panic!("Output 0 should exist in the transaction"), }; let created = time::get_time(); // Update the database atomically, to ensure the result is internally consistent. data.execute("BEGIN IMMEDIATE", NO_PARAMS)?; // 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, created, expiry_height, raw) VALUES (?, ?, ?, ?)", )?; stmt_insert_tx.execute(&[ tx.txid().0.to_sql()?, created.to_sql()?, tx.expiry_height.to_sql()?, raw_tx.to_sql()?, ])?; let id_tx = data.last_insert_rowid(); // Mark notes as spent. // // This locks the notes so they aren't selected again by a subsequent call to // send_to_address() before this transaction has been mined (at which point the notes // get re-marked as spent). // // Assumes that send_to_address() will never be called in parallel, which is a // reasonable assumption for a light client such as a mobile phone. let mut stmt_mark_spent_note = data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?; for spend in &tx.shielded_spends { stmt_mark_spent_note.execute(&[id_tx.to_sql()?, spend.nullifier.to_sql()?])?; } // Save the sent note in the database. let to_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, to); if let Some(memo) = memo { 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.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()?, ])?; } data.execute("COMMIT", NO_PARAMS)?; // Return the row number of the transaction, so the caller can fetch it for sending. Ok(id_tx) } #[cfg(test)] mod tests { use ff::{PrimeField, PrimeFieldRepr}; use pairing::bls12_381::Bls12; use protobuf::Message; use rand::{thread_rng, Rand, Rng}; use rusqlite::{types::ToSql, Connection}; use sapling_crypto::{ jubjub::fs::Fs, primitives::{Note, PaymentAddress}, }; use std::path::Path; use tempfile::NamedTempFile; use zcash_client_backend::{ constants::HRP_SAPLING_PAYMENT_ADDRESS_TEST, encoding::decode_payment_address, note_encryption::{Memo, SaplingNoteEncryption}, proto::compact_formats::{CompactBlock, CompactOutput, CompactSpend, CompactTx}, prover::{LocalTxProver, TxProver}, }; use zcash_primitives::{transaction::components::Amount, JUBJUB}; use zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; use super::{ get_address, get_balance, get_verified_balance, init_accounts_table, init_blocks_table, init_cache_database, init_data_database, scan_cached_blocks, send_to_address, }; fn test_prover() -> impl TxProver { let unix_params_dir = dirs::home_dir().map(|path| path.join(".zcash-params")); let win_osx_params_dir = dirs::data_dir().map(|path| path.join("ZcashParams")); let (spend_path, output_path) = match (unix_params_dir, win_osx_params_dir) { (Some(ref params_dir), _) if params_dir.exists() => ( params_dir.join("sapling-spend.params"), params_dir.join("sapling-output.params"), ), (_, Some(ref params_dir)) if params_dir.exists() => ( params_dir.join("sapling-spend.params"), params_dir.join("sapling-output.params"), ), _ => { panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests."); } }; LocalTxProver::new( &spend_path, "8270785a1a0d0bc77196f000ee6d221c9c9894f55307bd9357c3f0105d31ca63991ab91324160d8f53e2bbd3c2633a6eb8bdf5205d822e7f3f73edac51b2b70c", &output_path, "657e3d38dbb5cb5e7dd2970e8b03d69b4787dd907285b5a7f0790dcc8072f60bf593b32cc2d1c030e00ff5ae64bf84c5c3beb84ddc841d48264b4a171744d028", ) } /// Create a fake CompactBlock at the given height, containing a single output paying /// the given address. Returns the CompactBlock and the nullifier for the new note. fn fake_compact_block( height: i32, extfvk: ExtendedFullViewingKey, value: Amount, ) -> (CompactBlock, Vec) { let to = extfvk.default_address().unwrap().1; // Create a fake Note for the account let mut rng = thread_rng(); let note = Note { g_d: to.diversifier.g_d::(&JUBJUB).unwrap(), pk_d: to.pk_d.clone(), value: value.0 as u64, r: Fs::rand(&mut rng), }; let encryptor = SaplingNoteEncryption::new(extfvk.fvk.ovk, note.clone(), to.clone(), Memo::default()); let mut cmu = vec![]; note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); let mut epk = vec![]; encryptor.epk().write(&mut epk).unwrap(); let enc_ciphertext = encryptor.encrypt_note_plaintext(); // Create a fake CompactBlock containing the note let mut cout = CompactOutput::new(); cout.set_cmu(cmu); cout.set_epk(epk); cout.set_ciphertext(enc_ciphertext[..52].to_vec()); let mut ctx = CompactTx::new(); let mut txid = vec![0; 32]; rng.fill_bytes(&mut txid); ctx.set_hash(txid); ctx.outputs.push(cout); let mut cb = CompactBlock::new(); cb.set_height(height as u64); cb.vtx.push(ctx); (cb, note.nf(&extfvk.fvk.vk, 0, &JUBJUB)) } /// Create a fake CompactBlock at the given height, spending a single note from the /// given address. fn fake_compact_block_spending( height: i32, (nf, in_value): (Vec, Amount), extfvk: ExtendedFullViewingKey, to: PaymentAddress, value: Amount, ) -> CompactBlock { let mut rng = thread_rng(); // Create a fake CompactBlock containing the note let mut cspend = CompactSpend::new(); cspend.set_nf(nf); let mut ctx = CompactTx::new(); let mut txid = vec![0; 32]; rng.fill_bytes(&mut txid); ctx.set_hash(txid); ctx.spends.push(cspend); // Create a fake Note for the payment ctx.outputs.push({ let note = Note { g_d: to.diversifier.g_d::(&JUBJUB).unwrap(), pk_d: to.pk_d.clone(), value: value.0 as u64, r: Fs::rand(&mut rng), }; let encryptor = SaplingNoteEncryption::new(extfvk.fvk.ovk, note.clone(), to, Memo::default()); let mut cmu = vec![]; note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); let mut epk = vec![]; encryptor.epk().write(&mut epk).unwrap(); let enc_ciphertext = encryptor.encrypt_note_plaintext(); let mut cout = CompactOutput::new(); cout.set_cmu(cmu); cout.set_epk(epk); cout.set_ciphertext(enc_ciphertext[..52].to_vec()); cout }); // Create a fake Note for the change ctx.outputs.push({ let change_addr = extfvk.default_address().unwrap().1; let note = Note { g_d: change_addr.diversifier.g_d::(&JUBJUB).unwrap(), pk_d: change_addr.pk_d.clone(), value: (in_value.0 - value.0) as u64, r: Fs::rand(&mut rng), }; let encryptor = SaplingNoteEncryption::new( extfvk.fvk.ovk, note.clone(), change_addr, Memo::default(), ); let mut cmu = vec![]; note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); let mut epk = vec![]; encryptor.epk().write(&mut epk).unwrap(); let enc_ciphertext = encryptor.encrypt_note_plaintext(); let mut cout = CompactOutput::new(); cout.set_cmu(cmu); cout.set_epk(epk); cout.set_ciphertext(enc_ciphertext[..52].to_vec()); cout }); let mut cb = CompactBlock::new(); cb.set_height(height as u64); cb.vtx.push(ctx); cb } /// Insert a fake CompactBlock into the cache DB. fn insert_into_cache>(db_cache: P, cb: &CompactBlock) { let cb_bytes = cb.write_to_bytes().unwrap(); let cache = Connection::open(&db_cache).unwrap(); cache .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") .unwrap() .execute(&[ (cb.height as i32).to_sql().unwrap(), cb_bytes.to_sql().unwrap(), ]) .unwrap(); } #[test] fn init_accounts_table_only_works_once() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // We can call the function as many times as we want with no data init_accounts_table(&db_data, &[]).unwrap(); init_accounts_table(&db_data, &[]).unwrap(); // First call with data should initialise the accounts table let extfvks = [ExtendedFullViewingKey::from(&ExtendedSpendingKey::master( &[], ))]; init_accounts_table(&db_data, &extfvks).unwrap(); // Subsequent calls should return an error init_accounts_table(&db_data, &[]).unwrap_err(); init_accounts_table(&db_data, &extfvks).unwrap_err(); } #[test] fn init_blocks_table_only_works_once() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // First call with data should initialise the blocks table init_blocks_table(&db_data, 1, 1, &[]).unwrap(); // Subsequent calls should return an error init_blocks_table(&db_data, 2, 2, &[]).unwrap_err(); } #[test] fn init_accounts_table_stores_correct_address() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvks = [ExtendedFullViewingKey::from(&extsk)]; init_accounts_table(&db_data, &extfvks).unwrap(); // The account's address should be in the data DB let addr = get_address(&db_data, 0).unwrap(); let pa = decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS_TEST, &addr).unwrap(); assert_eq!(pa, extsk.default_address().unwrap().1); } #[test] fn scan_cached_blocks_requires_sequential_blocks() { let cache_file = NamedTempFile::new().unwrap(); let db_cache = cache_file.path(); init_cache_database(&db_cache).unwrap(); let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvk = ExtendedFullViewingKey::from(&extsk); init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); // Create a block with height 1 let value = Amount(50000); let (cb1, _) = fake_compact_block(1, extfvk.clone(), value); insert_into_cache(db_cache, &cb1); scan_cached_blocks(db_cache, db_data).unwrap(); assert_eq!(get_balance(db_data, 0).unwrap(), value); // We cannot scan a block of height 3 next let (cb3, _) = fake_compact_block(3, extfvk.clone(), value); insert_into_cache(db_cache, &cb3); match scan_cached_blocks(db_cache, db_data) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!( e.to_string(), "Expected height of next CompactBlock to be 2, but was 3" ), } // If we add a block of height 2, we can now scan both let (cb2, _) = fake_compact_block(2, extfvk.clone(), value); insert_into_cache(db_cache, &cb2); scan_cached_blocks(db_cache, db_data).unwrap(); assert_eq!(get_balance(db_data, 0).unwrap(), Amount(150_000)); } #[test] fn scan_cached_blocks_finds_received_notes() { let cache_file = NamedTempFile::new().unwrap(); let db_cache = cache_file.path(); init_cache_database(&db_cache).unwrap(); let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvk = ExtendedFullViewingKey::from(&extsk); init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); // Account balance should be zero assert_eq!(get_balance(db_data, 0).unwrap(), Amount(0)); // Create a fake CompactBlock sending value to the address let value = Amount(5); let (cb, _) = fake_compact_block(1, extfvk.clone(), value); insert_into_cache(db_cache, &cb); // Scan the cache scan_cached_blocks(db_cache, db_data).unwrap(); // Account balance should reflect the received note assert_eq!(get_balance(db_data, 0).unwrap(), value); // Create a second fake CompactBlock sending more value to the address let value2 = Amount(7); let (cb2, _) = fake_compact_block(2, extfvk, value2); insert_into_cache(db_cache, &cb2); // Scan the cache again scan_cached_blocks(db_cache, db_data).unwrap(); // Account balance should reflect both received notes // TODO: impl Sum for Amount assert_eq!(get_balance(db_data, 0).unwrap(), Amount(value.0 + value2.0)); } #[test] fn scan_cached_blocks_finds_change_notes() { let cache_file = NamedTempFile::new().unwrap(); let db_cache = cache_file.path(); init_cache_database(&db_cache).unwrap(); let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvk = ExtendedFullViewingKey::from(&extsk); init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); // Account balance should be zero assert_eq!(get_balance(db_data, 0).unwrap(), Amount(0)); // Create a fake CompactBlock sending value to the address let value = Amount(5); let (cb, nf) = fake_compact_block(1, extfvk.clone(), value); insert_into_cache(db_cache, &cb); // Scan the cache scan_cached_blocks(db_cache, db_data).unwrap(); // Account balance should reflect the received note assert_eq!(get_balance(db_data, 0).unwrap(), value); // Create a second fake CompactBlock spending value from the address let extsk2 = ExtendedSpendingKey::master(&[0]); let to2 = extsk2.default_address().unwrap().1; let value2 = Amount(2); insert_into_cache( db_cache, &fake_compact_block_spending(2, (nf, value), extfvk, to2, value2), ); // Scan the cache again scan_cached_blocks(db_cache, db_data).unwrap(); // Account balance should equal the change // TODO: impl Sum for Amount assert_eq!(get_balance(db_data, 0).unwrap(), Amount(value.0 - value2.0)); } #[test] fn send_to_address_fails_on_incorrect_extsk() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add two accounts to the wallet let extsk0 = ExtendedSpendingKey::master(&[]); let extsk1 = ExtendedSpendingKey::master(&[0]); let extfvks = [ ExtendedFullViewingKey::from(&extsk0), ExtendedFullViewingKey::from(&extsk1), ]; init_accounts_table(&db_data, &extfvks).unwrap(); let to = extsk0.default_address().unwrap().1; // Invalid extsk for the given account should cause an error match send_to_address( db_data, 1, test_prover(), (0, &extsk1), &to, Amount(1), None, ) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"), } match send_to_address( db_data, 1, test_prover(), (1, &extsk0), &to, Amount(1), None, ) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"), } } #[test] fn send_to_address_fails_with_no_blocks() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvks = [ExtendedFullViewingKey::from(&extsk)]; init_accounts_table(&db_data, &extfvks).unwrap(); let to = extsk.default_address().unwrap().1; // We cannot do anything if we aren't synchronised match send_to_address(db_data, 1, test_prover(), (0, &extsk), &to, Amount(1), None) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"), } } #[test] fn send_to_address_fails_on_insufficient_balance() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); init_blocks_table(&db_data, 1, 1, &[]).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvks = [ExtendedFullViewingKey::from(&extsk)]; init_accounts_table(&db_data, &extfvks).unwrap(); let to = extsk.default_address().unwrap().1; // Account balance should be zero assert_eq!(get_balance(db_data, 0).unwrap(), Amount(0)); // We cannot spend anything match send_to_address(db_data, 1, test_prover(), (0, &extsk), &to, Amount(1), None) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!( e.to_string(), "Insufficient balance (have 0, need 10001 including fee)" ), } } #[test] fn send_to_address_fails_on_unverified_notes() { let cache_file = NamedTempFile::new().unwrap(); let db_cache = cache_file.path(); init_cache_database(&db_cache).unwrap(); let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvk = ExtendedFullViewingKey::from(&extsk); init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); // Add funds to the wallet in a single note let value = Amount(50000); let (cb, _) = fake_compact_block(1, extfvk.clone(), value); insert_into_cache(db_cache, &cb); scan_cached_blocks(db_cache, db_data).unwrap(); // Verified balance matches total balance assert_eq!(get_balance(db_data, 0).unwrap(), value); assert_eq!(get_verified_balance(db_data, 0).unwrap(), value); // Add more funds to the wallet in a second note let (cb, _) = fake_compact_block(2, extfvk.clone(), value); insert_into_cache(db_cache, &cb); scan_cached_blocks(db_cache, db_data).unwrap(); // Verified balance does not include the second note assert_eq!(get_balance(db_data, 0).unwrap().0, 2 * value.0); assert_eq!(get_verified_balance(db_data, 0).unwrap(), value); // Spend fails because there are insufficient verified notes let extsk2 = ExtendedSpendingKey::master(&[]); let to = extsk2.default_address().unwrap().1; match send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(70000), None, ) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!( e.to_string(), "Insufficient balance (have 50000, need 80000 including fee)" ), } // Mine blocks 3 to 10 until just before the second note is verified for i in 3..11 { let (cb, _) = fake_compact_block(i, extfvk.clone(), value); insert_into_cache(db_cache, &cb); } scan_cached_blocks(db_cache, db_data).unwrap(); // Second spend still fails match send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(70000), None, ) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!( e.to_string(), "Insufficient balance (have 50000, need 80000 including fee)" ), } // Mine block 11 so that the second note becomes verified let (cb, _) = fake_compact_block(11, extfvk.clone(), value); insert_into_cache(db_cache, &cb); scan_cached_blocks(db_cache, db_data).unwrap(); // Second spend should now succeed send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(70000), None, ) .unwrap(); } #[test] fn send_to_address_fails_on_locked_notes() { let cache_file = NamedTempFile::new().unwrap(); let db_cache = cache_file.path(); init_cache_database(&db_cache).unwrap(); let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // Add an account to the wallet let extsk = ExtendedSpendingKey::master(&[]); let extfvk = ExtendedFullViewingKey::from(&extsk); init_accounts_table(&db_data, &[extfvk.clone()]).unwrap(); // Add funds to the wallet in a single note let value = Amount(50000); let (cb, _) = fake_compact_block(1, extfvk.clone(), value); insert_into_cache(db_cache, &cb); scan_cached_blocks(db_cache, db_data).unwrap(); assert_eq!(get_balance(db_data, 0).unwrap(), value); // Send some of the funds to another address let extsk2 = ExtendedSpendingKey::master(&[]); let to = extsk2.default_address().unwrap().1; send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(15000), None, ) .unwrap(); // A second spend fails because there are no usable notes match send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(2000), None, ) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!( e.to_string(), "Insufficient balance (have 0, need 12000 including fee)" ), } // Mine blocks 2 to 22 (that don't send us funds) until just before the first // transaction expires for i in 2..23 { let (cb, _) = fake_compact_block( i, ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])), value, ); insert_into_cache(db_cache, &cb); } scan_cached_blocks(db_cache, db_data).unwrap(); // Second spend still fails match send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(2000), None, ) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!( e.to_string(), "Insufficient balance (have 0, need 12000 including fee)" ), } // Mine block 23 so that the first transaction expires let (cb, _) = fake_compact_block( 23, ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[23])), value, ); insert_into_cache(db_cache, &cb); scan_cached_blocks(db_cache, db_data).unwrap(); // Second spend should now succeed send_to_address( db_data, 1, test_prover(), (0, &extsk), &to, Amount(2000), None, ) .unwrap(); } }