From 4c1237fa501717061c2d4ef3ecf4896b81e836f5 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 9 Mar 2019 03:20:32 +0000 Subject: [PATCH] zcash_client_sqlite::transact::create_to_address() --- Cargo.lock | 2 + zcash_client_backend/src/constants.rs | 4 + zcash_client_sqlite/Cargo.toml | 4 +- zcash_client_sqlite/src/error.rs | 29 +- zcash_client_sqlite/src/lib.rs | 1 + zcash_client_sqlite/src/transact.rs | 661 ++++++++++++++++++++ zcash_primitives/src/transaction/builder.rs | 23 + 7 files changed, 722 insertions(+), 2 deletions(-) create mode 100644 zcash_client_sqlite/src/transact.rs diff --git a/Cargo.lock b/Cargo.lock index aa11a84..6fd7629 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,8 +674,10 @@ dependencies = [ "rand_os 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "zcash_client_backend 0.0.0", "zcash_primitives 0.0.0", + "zcash_proofs 0.0.0", ] [[package]] diff --git a/zcash_client_backend/src/constants.rs b/zcash_client_backend/src/constants.rs index 8e5727c..ee858a1 100644 --- a/zcash_client_backend/src/constants.rs +++ b/zcash_client_backend/src/constants.rs @@ -2,3 +2,7 @@ pub mod mainnet; pub mod testnet; + +pub const SPROUT_CONSENSUS_BRANCH_ID: u32 = 0; +pub const OVERWINTER_CONSENSUS_BRANCH_ID: u32 = 0x5ba8_1b19; +pub const SAPLING_CONSENSUS_BRANCH_ID: u32 = 0x76b8_09bb; diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index d572a38..167068f 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -9,13 +9,15 @@ edition = "2018" [dependencies] bech32 = "0.7" ff = { path = "../ff" } +pairing = { path = "../pairing" } protobuf = "2" rusqlite = { version = "0.20", features = ["bundled"] } +time = "0.1" zcash_client_backend = { path = "../zcash_client_backend" } zcash_primitives = { path = "../zcash_primitives" } [dev-dependencies] -pairing = { path = "../pairing" } rand_core = "0.5" rand_os = "0.2" tempfile = "3" +zcash_proofs = { path = "../zcash_proofs" } diff --git a/zcash_client_sqlite/src/error.rs b/zcash_client_sqlite/src/error.rs index fcdc1a0..52ff230 100644 --- a/zcash_client_sqlite/src/error.rs +++ b/zcash_client_sqlite/src/error.rs @@ -1,17 +1,25 @@ use std::error; use std::fmt; -use zcash_primitives::{sapling::Node, transaction::TxId}; +use zcash_primitives::{ + sapling::Node, + transaction::{builder, TxId}, +}; #[derive(Debug)] pub enum ErrorKind { CorruptedData(&'static str), IncorrectHRPExtFVK, + InsufficientBalance(u64, u64), + InvalidExtSK(u32), InvalidHeight(i32, i32), + InvalidMemo(std::str::Utf8Error), InvalidNewWitnessAnchor(usize, TxId, i32, Node), + InvalidNote, InvalidWitnessAnchor(i64, i32), ScanRequired, TableNotEmpty, Bech32(bech32::Error), + Builder(builder::Error), Database(rusqlite::Error), Io(std::io::Error), Protobuf(protobuf::ProtobufError), @@ -25,16 +33,26 @@ impl fmt::Display for Error { match &self.0 { ErrorKind::CorruptedData(reason) => write!(f, "Data DB is corrupted: {}", reason), ErrorKind::IncorrectHRPExtFVK => write!(f, "Incorrect HRP for extfvk"), + ErrorKind::InsufficientBalance(have, need) => write!( + f, + "Insufficient balance (have {}, need {} including fee)", + have, need + ), + ErrorKind::InvalidExtSK(account) => { + write!(f, "Incorrect ExtendedSpendingKey for account {}", account) + } ErrorKind::InvalidHeight(expected, actual) => write!( f, "Expected height of next CompactBlock to be {}, but was {}", expected, actual ), + ErrorKind::InvalidMemo(e) => write!(f, "{}", e), ErrorKind::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!( f, "New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}", output, txid, last_height, anchor, ), + ErrorKind::InvalidNote => write!(f, "Invalid note"), ErrorKind::InvalidWitnessAnchor(id_note, last_height) => write!( f, "Witness for note {} has incorrect anchor after scanning block {}", @@ -43,6 +61,7 @@ impl fmt::Display for Error { ErrorKind::ScanRequired => write!(f, "Must scan blocks first"), ErrorKind::TableNotEmpty => write!(f, "Table is not empty"), ErrorKind::Bech32(e) => write!(f, "{}", e), + ErrorKind::Builder(e) => write!(f, "{:?}", e), ErrorKind::Database(e) => write!(f, "{}", e), ErrorKind::Io(e) => write!(f, "{}", e), ErrorKind::Protobuf(e) => write!(f, "{}", e), @@ -53,7 +72,9 @@ impl fmt::Display for Error { impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self.0 { + ErrorKind::InvalidMemo(e) => Some(e), ErrorKind::Bech32(e) => Some(e), + ErrorKind::Builder(e) => Some(e), ErrorKind::Database(e) => Some(e), ErrorKind::Io(e) => Some(e), ErrorKind::Protobuf(e) => Some(e), @@ -68,6 +89,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: builder::Error) -> Self { + Error(ErrorKind::Builder(e)) + } +} + impl From for Error { fn from(e: rusqlite::Error) -> Self { Error(ErrorKind::Database(e)) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index c9fa4c8..1da09d5 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -30,6 +30,7 @@ pub mod error; pub mod init; pub mod query; pub mod scan; +pub mod transact; const ANCHOR_OFFSET: u32 = 10; const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; diff --git a/zcash_client_sqlite/src/transact.rs b/zcash_client_sqlite/src/transact.rs new file mode 100644 index 0000000..eb9cd68 --- /dev/null +++ b/zcash_client_sqlite/src/transact.rs @@ -0,0 +1,661 @@ +//! Functions for creating transactions. + +use ff::{PrimeField, PrimeFieldRepr}; +use pairing::bls12_381::Bls12; +use rusqlite::{types::ToSql, Connection, NO_PARAMS}; +use std::path::Path; +use zcash_client_backend::{ + constants::testnet::{HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_PAYMENT_ADDRESS}, + encoding::{encode_extended_full_viewing_key, encode_payment_address}, +}; +use zcash_primitives::{ + jubjub::fs::{Fs, FsRepr}, + merkle_tree::IncrementalWitness, + note_encryption::Memo, + primitives::{Diversifier, Note, PaymentAddress}, + prover::TxProver, + sapling::Node, + transaction::{ + builder::Builder, + components::{amount::DEFAULT_FEE, Amount}, + }, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + JUBJUB, +}; + +use crate::{ + error::{Error, ErrorKind}, + get_target_and_anchor_heights, +}; + +struct SelectedNoteRow { + diversifier: Diversifier, + note: Note, + witness: IncrementalWitness, +} + +/// Creates a transaction paying the specified address from the given account. +/// +/// Returns the row index of the newly-created transaction in the `transactions` table +/// within the data database. The caller can read the raw transaction bytes from the `raw` +/// column in order to broadcast the transaction to the network. +/// +/// Do not call this multiple times in parallel, or you will generate transactions that +/// double-spend the same notes. +/// +/// # Examples +/// +/// ``` +/// use zcash_client_backend::{ +/// constants::{testnet::COIN_TYPE, SAPLING_CONSENSUS_BRANCH_ID}, +/// keys::spending_key, +/// }; +/// use zcash_client_sqlite::transact::create_to_address; +/// use zcash_primitives::transaction::components::Amount; +/// use zcash_proofs::prover::LocalTxProver; +/// +/// let tx_prover = match LocalTxProver::with_default_location() { +/// Some(tx_prover) => tx_prover, +/// None => { +/// 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."); +/// } +/// }; +/// +/// let account = 0; +/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account); +/// let to = extsk.default_address().unwrap().1; +/// match create_to_address( +/// "/path/to/data.db", +/// SAPLING_CONSENSUS_BRANCH_ID, +/// tx_prover, +/// (account, &extsk), +/// &to, +/// Amount::from_u64(1).unwrap(), +/// None, +/// ) { +/// Ok(tx_row) => (), +/// Err(e) => (), +/// } +/// ``` +pub fn create_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, &extfvk) + .to_sql()?, + ])? + { + return Err(Error(ErrorKind::InvalidExtSK(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 = i64::from(value + DEFAULT_FEE); + 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 diversifier = { + let d: Vec<_> = row.get(0)?; + if d.len() != 11 { + return Err(Error(ErrorKind::CorruptedData( + "Invalid diversifier length", + ))); + } + let mut tmp = [0; 11]; + tmp.copy_from_slice(&d); + Diversifier(tmp) + }; + + let note_value: i64 = row.get(1)?; + + let rcm = { + let d: Vec<_> = row.get(2)?; + let mut tmp = FsRepr::default(); + tmp.read_le(&d[..])?; + Fs::from_repr(tmp).map_err(|_| Error(ErrorKind::InvalidNote))? + }; + + let from = extfvk + .fvk + .vk + .into_payment_address(diversifier, &JUBJUB) + .unwrap(); + let note = from.create_note(note_value as u64, rcm, &JUBJUB).unwrap(); + + let witness = { + let d: Vec<_> = row.get(3)?; + 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(Error(ErrorKind::InsufficientBalance( + selected_value, + target_value as u64, + ))); + } + + // 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 + // create_to_address() before this transaction has been mined (at which point the notes + // get re-marked as spent). + // + // Assumes that create_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, 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()?, + i64::from(value).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()?, + i64::from(value).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 tempfile::NamedTempFile; + use zcash_primitives::{ + block::BlockHash, + prover::TxProver, + transaction::components::Amount, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + }; + use zcash_proofs::prover::LocalTxProver; + + use super::create_to_address; + use crate::{ + init::{init_accounts_table, init_blocks_table, init_cache_database, init_data_database}, + query::{get_balance, get_verified_balance}, + scan::scan_cached_blocks, + tests::{fake_compact_block, insert_into_cache}, + SAPLING_ACTIVATION_HEIGHT, + }; + + fn test_prover() -> impl TxProver { + match LocalTxProver::with_default_location() { + Some(tx_prover) => tx_prover, + None => { + 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."); + } + } + } + + #[test] + fn create_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 create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk1), + &to, + Amount::from_u64(1).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"), + } + match create_to_address( + db_data, + 1, + test_prover(), + (1, &extsk0), + &to, + Amount::from_u64(1).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"), + } + } + + #[test] + fn create_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 create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(1).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"), + } + } + + #[test] + fn create_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, BlockHash([1; 32]), 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::zero()); + + // We cannot spend anything + match create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(1).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!( + e.to_string(), + "Insufficient balance (have 0, need 10001 including fee)" + ), + } + } + + #[test] + fn create_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::from_u64(50000).unwrap(); + let (cb, _) = fake_compact_block( + SAPLING_ACTIVATION_HEIGHT, + BlockHash([0; 32]), + 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( + SAPLING_ACTIVATION_HEIGHT + 1, + cb.hash(), + 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(), value + value); + 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 create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(70000).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!( + e.to_string(), + "Insufficient balance (have 50000, need 80000 including fee)" + ), + } + + // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second + // note is verified + for i in 2..10 { + let (cb, _) = fake_compact_block( + SAPLING_ACTIVATION_HEIGHT + i, + cb.hash(), + extfvk.clone(), + value, + ); + insert_into_cache(db_cache, &cb); + } + scan_cached_blocks(db_cache, db_data).unwrap(); + + // Second spend still fails + match create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(70000).unwrap(), + 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( + SAPLING_ACTIVATION_HEIGHT + 10, + cb.hash(), + extfvk.clone(), + value, + ); + insert_into_cache(db_cache, &cb); + scan_cached_blocks(db_cache, db_data).unwrap(); + + // Second spend should now succeed + create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(70000).unwrap(), + None, + ) + .unwrap(); + } + + #[test] + fn create_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::from_u64(50000).unwrap(); + let (cb, _) = fake_compact_block( + SAPLING_ACTIVATION_HEIGHT, + BlockHash([0; 32]), + 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; + create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(15000).unwrap(), + None, + ) + .unwrap(); + + // A second spend fails because there are no usable notes + match create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(2000).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!( + e.to_string(), + "Insufficient balance (have 0, need 12000 including fee)" + ), + } + + // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds) + // until just before the first transaction expires + for i in 1..22 { + let (cb, _) = fake_compact_block( + SAPLING_ACTIVATION_HEIGHT + i, + cb.hash(), + 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 create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(2000).unwrap(), + None, + ) { + Ok(_) => panic!("Should have failed"), + Err(e) => assert_eq!( + e.to_string(), + "Insufficient balance (have 0, need 12000 including fee)" + ), + } + + // Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires + let (cb, _) = fake_compact_block( + SAPLING_ACTIVATION_HEIGHT + 22, + cb.hash(), + ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])), + value, + ); + insert_into_cache(db_cache, &cb); + scan_cached_blocks(db_cache, db_data).unwrap(); + + // Second spend should now succeed + create_to_address( + db_data, + 1, + test_prover(), + (0, &extsk), + &to, + Amount::from_u64(2000).unwrap(), + None, + ) + .unwrap(); + } +} diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index b0e96f5..63e27cd 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -7,6 +7,8 @@ use crate::{ use ff::Field; use pairing::bls12_381::{Bls12, Fr}; use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore}; +use std::error; +use std::fmt; use zip32::ExtendedSpendingKey; use crate::{ @@ -42,6 +44,27 @@ pub enum Error { SpendProof, } +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::AnchorMismatch => { + write!(f, "Anchor mismatch (anchors for all spends must be equal)") + } + Error::BindingSig => write!(f, "Failed to create bindingSig"), + Error::ChangeIsNegative(amount) => { + write!(f, "Change is negative ({:?} zatoshis)", amount) + } + Error::InvalidAddress => write!(f, "Invalid address"), + Error::InvalidAmount => write!(f, "Invalid amount"), + Error::InvalidWitness => write!(f, "Invalid note witness"), + Error::NoChangeAddress => write!(f, "No change address specified or discoverable"), + Error::SpendProof => write!(f, "Failed to create Sapling spend proof"), + } + } +} + +impl error::Error for Error {} + struct SpendDescriptionInfo { extsk: ExtendedSpendingKey, diversifier: Diversifier,