diff --git a/Cargo.toml b/Cargo.toml index 846f8c7a7b..78e6fdb3b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "zcash_client_backend", "zcash_client_sqlite", "zcash_extensions", + "zcash_extras", "zcash_history", "zcash_primitives", "zcash_proofs", diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index b169ba2d12..7660f01f04 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT OR Apache-2.0" edition = "2018" [dependencies] +async-trait = "0.1.52" bech32 = "0.9.1" bls12_381 = "0.3.1" bs58 = { version = "0.4", features = ["check"] } @@ -27,10 +28,15 @@ proptest = { version = "0.10.1", optional = true } protobuf = "2.20" rand_core = "0.5.1" subtle = "2.2.3" -time = "0.3" zcash_note_encryption = { version = "0.0", path = "../components/zcash_note_encryption" } zcash_primitives = { version = "0.5", path = "../zcash_primitives" } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +time = "0.3.20" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +time = { version = "0.3.20", features = ["wasm-bindgen"]} + [build-dependencies] protobuf-codegen-pure = "2.20" @@ -40,8 +46,13 @@ rand_core = "0.5.1" rand_xorshift = "0.2" tempfile = "3.1.0" zcash_client_sqlite = { version = "0.3", path = "../zcash_client_sqlite" } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] zcash_proofs = { version = "0.5", path = "../zcash_proofs" } +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +zcash_proofs = { version = "0.5", path = "../zcash_proofs", default-features = false, features = ["local-prover"]} + [features] test-dependencies = ["proptest", "zcash_primitives/test-dependencies"] diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 82a9a9a9b5..090bb14a79 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -1,21 +1,23 @@ //! Functions for scanning the chain and extracting relevant information. use std::fmt::Debug; +use zcash_primitives::consensus::NetworkUpgrade; +use zcash_primitives::transaction::Transaction; use zcash_primitives::{ - consensus::{self, BranchId, NetworkUpgrade}, + consensus::{self, BranchId}, memo::MemoBytes, sapling::prover::TxProver, transaction::{ builder::Builder, components::{amount::DEFAULT_FEE, Amount}, - Transaction, }, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, }; +use crate::data_api::ReceivedTransaction; use crate::{ address::RecipientAddress, - data_api::{error::Error, ReceivedTransaction, SentTransaction, WalletWrite}, + data_api::{error::Error, SentTransaction, WalletWrite}, decrypt_transaction, wallet::{AccountId, OvkPolicy}, }; diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index 3a0f85a7bc..0f3951fdfb 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT OR Apache-2.0" edition = "2018" [dependencies] +async-trait = "0.1.52" bech32 = "0.9.1" bs58 = { version = "0.4", features = ["check"] } ff = "0.8" @@ -22,8 +23,10 @@ protobuf = "2.20" rand_core = "0.5.1" rusqlite = { version = "0.28", features = ["time"] } libsqlite3-sys= { version = "0.25.2", features = ["bundled"] } -time = "0.3" +time = "0.3.20" +tokio = { version = "1.20", features = ["rt", "rt-multi-thread"] } zcash_client_backend = { version = "0.5", path = "../zcash_client_backend" } +zcash_extras = { version = "0.1", path = "../zcash_extras" } zcash_primitives = { version = "0.5", path = "../zcash_primitives" } [dev-dependencies] diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index b416ebe889..f3f88c33a1 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -79,14 +79,12 @@ mod tests { chain::{scan_cached_blocks, validate_chain}, error::{ChainInvalid, Error}, }; + use zcash_extras::{fake_compact_block, fake_compact_block_spending}; use crate::{ chain::init::init_cache_database, error::SqliteClientError, - tests::{ - self, fake_compact_block, fake_compact_block_spending, insert_into_cache, - sapling_activation_height, - }, + tests::{self, insert_into_cache, sapling_activation_height}, wallet::{ get_balance, init::{init_accounts_table, init_wallet_db}, diff --git a/zcash_client_sqlite/src/for_async/init.rs b/zcash_client_sqlite/src/for_async/init.rs new file mode 100644 index 0000000000..3bdaf7ffcb --- /dev/null +++ b/zcash_client_sqlite/src/for_async/init.rs @@ -0,0 +1,56 @@ +use crate::error::SqliteClientError; +use crate::for_async::{async_blocking, WalletDbAsync}; +use crate::wallet; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus; +use zcash_primitives::consensus::BlockHeight; +use zcash_primitives::zip32::ExtendedFullViewingKey; + +pub async fn init_wallet_db( + wdb: &WalletDbAsync

, +) -> Result<(), rusqlite::Error> +where + P: Clone + Send + Sync, +{ + let wdb = wdb.inner(); + async_blocking(move || { + let wdb = wdb.lock().unwrap(); + wallet::init::init_wallet_db(&wdb) + }) + .await +} + +pub async fn init_accounts_table( + wdb: &WalletDbAsync

, + extfvks: &[ExtendedFullViewingKey], +) -> Result<(), SqliteClientError> +where + P: Clone + Send + Sync, +{ + let wdb = wdb.inner(); + let extfvks = extfvks.to_vec(); + async_blocking(move || { + let wdb = wdb.lock().unwrap(); + wallet::init::init_accounts_table(&wdb, &extfvks) + }) + .await +} + +pub async fn init_blocks_table( + wdb: &WalletDbAsync

, + height: BlockHeight, + hash: BlockHash, + time: u32, + sapling_tree: &[u8], +) -> Result<(), SqliteClientError> +where + P: Clone + Send + Sync, +{ + let wdb = wdb.inner(); + let sapling_tree = sapling_tree.to_vec(); + async_blocking(move || { + let wdb = wdb.lock().unwrap(); + wallet::init::init_blocks_table(&wdb, height, hash, time, &sapling_tree) + }) + .await +} diff --git a/zcash_client_sqlite/src/for_async/mod.rs b/zcash_client_sqlite/src/for_async/mod.rs new file mode 100644 index 0000000000..ebe60393a0 --- /dev/null +++ b/zcash_client_sqlite/src/for_async/mod.rs @@ -0,0 +1,375 @@ +pub mod init; +pub mod wallet_actions; + +use std::collections::HashMap; +use std::path::Path; +use tokio::task::block_in_place; +use zcash_client_backend::data_api::WalletWrite as WalletWriteSync; +use zcash_client_backend::data_api::{PrunedBlock, ReceivedTransaction, SentTransaction}; +use zcash_client_backend::wallet::{AccountId, SpendableNote}; +use zcash_extras::{WalletRead, WalletWrite}; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::BlockHeight; +use zcash_primitives::memo::Memo; +use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; +use zcash_primitives::sapling::{Node, Nullifier, PaymentAddress}; +use zcash_primitives::transaction::components::Amount; +use zcash_primitives::transaction::TxId; +use zcash_primitives::zip32::ExtendedFullViewingKey; + +pub async fn async_blocking(blocking_fn: F) -> R +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + tokio::task::spawn_blocking(blocking_fn) + .await + .expect("spawn_blocking to succeed") +} + +use crate::error::SqliteClientError; +use crate::{wallet, NoteId, WalletDb}; +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; + +use zcash_primitives::consensus; + +/// A wrapper for the SQLite connection to the wallet database. +#[derive(Clone)] +pub struct WalletDbAsync

{ + inner: Arc>>, +} + +impl WalletDbAsync

{ + pub fn inner(&self) -> Arc>> { + self.inner.clone() + } + + /// Construct a connection to the wallet database stored at the specified path. + pub fn for_path>(path: F, params: P) -> Result { + let db = Connection::open(path).map(move |conn| WalletDb { conn, params })?; + Ok(Self { + inner: Arc::new(Mutex::new(db)), + }) + } + + /// Given a wallet database connection, obtain a handle for the write operations + /// for that database. This operation may eagerly initialize and cache sqlite + /// prepared statements that are used in write operations. + pub fn get_update_ops(&self) -> Result, SqliteClientError> { + Ok(DataConnStmtCacheAsync { + wallet_db: self.clone(), + }) + } +} + +#[async_trait::async_trait] +impl WalletRead for WalletDbAsync

{ + type Error = SqliteClientError; + type NoteRef = NoteId; + type TxRef = i64; + + async fn block_height_extrema( + &self, + ) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::block_height_extrema(&db).map_err(SqliteClientError::from) + }) + .await + } + + async fn get_block_hash( + &self, + block_height: BlockHeight, + ) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_block_hash(&db, block_height).map_err(SqliteClientError::from) + }) + .await + } + + async fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_tx_height(&db, txid).map_err(SqliteClientError::from) + }) + .await + } + + async fn get_address(&self, account: AccountId) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_address(&db, account).map_err(SqliteClientError::from) + }) + .await + } + + async fn get_extended_full_viewing_keys( + &self, + ) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_extended_full_viewing_keys(&db).map_err(SqliteClientError::from) + }) + .await + } + + async fn is_valid_account_extfvk( + &self, + account: AccountId, + extfvk: &ExtendedFullViewingKey, + ) -> Result { + let db = self.clone(); + let extfvk = extfvk.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::is_valid_account_extfvk(&db, account, &extfvk) + }) + .await + } + + async fn get_balance_at( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_balance_at(&db, account, anchor_height) + }) + .await + } + + async fn get_memo(&self, id_note: Self::NoteRef) -> Result { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + match id_note { + NoteId::SentNoteId(id_note) => wallet::get_sent_memo(&db, id_note), + NoteId::ReceivedNoteId(id_note) => wallet::get_received_memo(&db, id_note), + } + }) + .await + } + + async fn get_commitment_tree( + &self, + block_height: BlockHeight, + ) -> Result>, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_commitment_tree(&db, block_height) + }) + .await + } + + #[allow(clippy::type_complexity)] + async fn get_witnesses( + &self, + block_height: BlockHeight, + ) -> Result)>, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_witnesses(&db, block_height) + }) + .await + } + + async fn get_nullifiers(&self) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::get_nullifiers(&db) + }) + .await + } + + async fn get_spendable_notes( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::transact::get_spendable_notes(&db, account, anchor_height) + }) + .await + } + + async fn select_spendable_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.inner.lock().unwrap(); + wallet::transact::select_spendable_notes(&db, account, target_value, anchor_height) + }) + .await + } +} + +#[derive(Clone)] +pub struct DataConnStmtCacheAsync

{ + wallet_db: WalletDbAsync

, +} + +#[async_trait::async_trait] +impl WalletRead for DataConnStmtCacheAsync

{ + type Error = SqliteClientError; + type NoteRef = NoteId; + type TxRef = i64; + + async fn block_height_extrema( + &self, + ) -> Result, Self::Error> { + self.wallet_db.block_height_extrema().await + } + + async fn get_block_hash( + &self, + block_height: BlockHeight, + ) -> Result, Self::Error> { + self.wallet_db.get_block_hash(block_height).await + } + + async fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + self.wallet_db.get_tx_height(txid).await + } + + async fn get_address(&self, account: AccountId) -> Result, Self::Error> { + self.wallet_db.get_address(account).await + } + + async fn get_extended_full_viewing_keys( + &self, + ) -> Result, Self::Error> { + self.wallet_db.get_extended_full_viewing_keys().await + } + + async fn is_valid_account_extfvk( + &self, + account: AccountId, + extfvk: &ExtendedFullViewingKey, + ) -> Result { + self.wallet_db + .is_valid_account_extfvk(account, extfvk) + .await + } + + async fn get_balance_at( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result { + self.wallet_db.get_balance_at(account, anchor_height).await + } + + async fn get_memo(&self, id_note: Self::NoteRef) -> Result { + self.wallet_db.get_memo(id_note).await + } + + async fn get_commitment_tree( + &self, + block_height: BlockHeight, + ) -> Result>, Self::Error> { + self.wallet_db.get_commitment_tree(block_height).await + } + + #[allow(clippy::type_complexity)] + async fn get_witnesses( + &self, + block_height: BlockHeight, + ) -> Result)>, Self::Error> { + self.wallet_db.get_witnesses(block_height).await + } + + async fn get_nullifiers(&self) -> Result, Self::Error> { + self.wallet_db.get_nullifiers().await + } + + async fn get_spendable_notes( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + self.wallet_db + .get_spendable_notes(account, anchor_height) + .await + } + + async fn select_spendable_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + self.wallet_db + .select_spendable_notes(account, target_value, anchor_height) + .await + } +} + +#[async_trait::async_trait] +impl WalletWrite for DataConnStmtCacheAsync

{ + #[allow(clippy::type_complexity)] + async fn advance_by_block( + &mut self, + block: &PrunedBlock, + updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], + ) -> Result)>, Self::Error> { + // database updates for each block are transactional + block_in_place(|| { + let db = self.wallet_db.inner.lock().unwrap(); + let mut update_ops = db.get_update_ops()?; + update_ops.advance_by_block(&block, updated_witnesses) + }) + } + + async fn store_received_tx( + &mut self, + received_tx: &ReceivedTransaction, + ) -> Result { + // database updates for each block are transactional + block_in_place(|| { + let db = self.wallet_db.inner.lock().unwrap(); + let mut update_ops = db.get_update_ops()?; + update_ops.store_received_tx(&received_tx) + }) + } + + async fn store_sent_tx( + &mut self, + sent_tx: &SentTransaction, + ) -> Result { + // Update the database atomically, to ensure the result is internally consistent. + + block_in_place(|| { + let db = self.wallet_db.inner.lock().unwrap(); + let mut update_ops = db.get_update_ops()?; + update_ops.store_sent_tx(&sent_tx) + }) + } + + async fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { + let db = self.clone(); + async_blocking(move || { + let db = db.wallet_db.inner.lock().unwrap(); + wallet::rewind_to_height(&db, block_height) + }) + .await + } +} diff --git a/zcash_client_sqlite/src/for_async/wallet_actions.rs b/zcash_client_sqlite/src/for_async/wallet_actions.rs new file mode 100644 index 0000000000..bf95c35a6f --- /dev/null +++ b/zcash_client_sqlite/src/for_async/wallet_actions.rs @@ -0,0 +1,122 @@ +use crate::error::SqliteClientError; +use crate::{wallet, NoteId, WalletDb}; +use std::sync::MutexGuard; +use zcash_client_backend::address::RecipientAddress; +use zcash_client_backend::wallet::{AccountId, WalletTx}; +use zcash_client_backend::DecryptedOutput; +use zcash_extras::ShieldedOutput; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::{BlockHeight, Parameters}; +use zcash_primitives::memo::MemoBytes; +use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; +use zcash_primitives::sapling::{Node, Nullifier}; +use zcash_primitives::transaction::components::Amount; +use zcash_primitives::transaction::Transaction; + +pub fn insert_block( + db: &MutexGuard>, + block_height: BlockHeight, + block_hash: BlockHash, + block_time: u32, + commitment_tree: &CommitmentTree, +) -> Result<(), SqliteClientError> { + let mut update_ops = db.get_update_ops()?; + wallet::insert_block( + &mut update_ops, + block_height, + block_hash, + block_time, + commitment_tree, + ) +} + +pub fn put_tx_meta( + db: &MutexGuard>, + tx: &WalletTx, + height: BlockHeight, +) -> Result { + let mut update_ops = db.get_update_ops()?; + wallet::put_tx_meta(&mut update_ops, tx, height) +} + +pub fn mark_spent( + db: &MutexGuard>, + tx_ref: i64, + nf: &Nullifier, +) -> Result<(), SqliteClientError> { + wallet::mark_spent(&mut db.get_update_ops()?, tx_ref, nf) +} + +pub fn put_received_note( + db: &MutexGuard>, + output: &T, + tx_ref: i64, +) -> Result { + let mut update_ops = db.get_update_ops()?; + wallet::put_received_note(&mut update_ops, output, tx_ref) +} + +pub fn insert_witness( + db: &MutexGuard>, + note_id: i64, + witness: &IncrementalWitness, + height: BlockHeight, +) -> Result<(), SqliteClientError> { + let mut update_ops = db.get_update_ops()?; + wallet::insert_witness(&mut update_ops, note_id, witness, height) +} + +pub fn prune_witnesses( + db: &MutexGuard>, + below_height: BlockHeight, +) -> Result<(), SqliteClientError> { + let mut update_ops = db.get_update_ops()?; + wallet::prune_witnesses(&mut update_ops, below_height) +} + +pub fn update_expired_notes( + db: &MutexGuard>, + height: BlockHeight, +) -> Result<(), SqliteClientError> { + let mut update_ops = db.get_update_ops()?; + wallet::update_expired_notes(&mut update_ops, height) +} + +pub fn put_tx_data( + db: &MutexGuard>, + tx: &Transaction, + created_at: Option, +) -> Result { + let mut update_ops = db.get_update_ops()?; + wallet::put_tx_data(&mut update_ops, tx, created_at) +} + +pub fn put_sent_note( + db: &MutexGuard>, + output: &DecryptedOutput, + tx_ref: i64, +) -> Result<(), SqliteClientError> { + let mut update_ops = db.get_update_ops()?; + wallet::put_sent_note(&mut update_ops, output, tx_ref) +} + +pub fn insert_sent_note( + db: &MutexGuard>, + tx_ref: i64, + output_index: usize, + account: AccountId, + to: &RecipientAddress, + value: Amount, + memo: Option<&MemoBytes>, +) -> Result<(), SqliteClientError> { + let mut update_ops = db.get_update_ops()?; + wallet::insert_sent_note( + &mut update_ops, + tx_ref, + output_index, + account, + to, + value, + memo, + ) +} diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index b2f4218a53..97064a64cc 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -32,8 +32,9 @@ // Catch documentation errors caused by code changes. #![deny(broken_intra_doc_links)] +extern crate core; + use std::collections::HashMap; -use std::fmt; use std::path::Path; use rusqlite::{Connection, Statement}; @@ -56,30 +57,15 @@ use zcash_client_backend::{ proto::compact_formats::CompactBlock, wallet::{AccountId, SpendableNote}, }; +use zcash_extras::NoteId; use crate::error::SqliteClientError; pub mod chain; pub mod error; +pub mod for_async; pub mod wallet; -/// A newtype wrapper for sqlite primary key values for the notes -/// table. -#[derive(Debug, Copy, Clone)] -pub enum NoteId { - SentNoteId(i64), - ReceivedNoteId(i64), -} - -impl fmt::Display for NoteId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - NoteId::SentNoteId(id) => write!(f, "Sent Note {}", id), - NoteId::ReceivedNoteId(id) => write!(f, "Received Note {}", id), - } - } -} - /// A wrapper for the SQLite connection to the wallet database. pub struct WalletDb

{ conn: Connection, @@ -558,27 +544,12 @@ fn address_from_extfvk( #[cfg(test)] mod tests { - use ff::PrimeField; - use group::GroupEncoding; use protobuf::Message; - use rand_core::{OsRng, RngCore}; use rusqlite::params; - use zcash_client_backend::proto::compact_formats::{ - CompactBlock, CompactOutput, CompactSpend, CompactTx, - }; - - use zcash_primitives::{ - block::BlockHash, - consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}, - memo::MemoBytes, - sapling::{ - note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier, - PaymentAddress, - }, - transaction::components::Amount, - zip32::ExtendedFullViewingKey, - }; + use zcash_client_backend::proto::compact_formats::CompactBlock; + + use zcash_primitives::consensus::{BlockHeight, Network, NetworkUpgrade, Parameters}; use super::BlockDb; @@ -606,140 +577,6 @@ mod tests { .unwrap() } - /// 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. - pub(crate) fn fake_compact_block( - height: BlockHeight, - prev_hash: BlockHash, - extfvk: ExtendedFullViewingKey, - value: Amount, - ) -> (CompactBlock, Nullifier) { - let to = extfvk.default_address().unwrap().1; - - // Create a fake Note for the account - let mut rng = OsRng; - let rseed = generate_random_rseed(&network(), height, &mut rng); - let note = Note { - g_d: to.diversifier().g_d().unwrap(), - pk_d: *to.pk_d(), - value: value.into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - to, - MemoBytes::empty(), - &mut rng, - ); - let cmu = note.cmu().to_repr().as_ref().to_vec(); - let epk = encryptor.epk().to_bytes().to_vec(); - 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.as_ref()[..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(u64::from(height)); - cb.hash.resize(32, 0); - rng.fill_bytes(&mut cb.hash); - cb.prevHash.extend_from_slice(&prev_hash.0); - cb.vtx.push(ctx); - (cb, note.nf(&extfvk.fvk.vk, 0)) - } - - /// Create a fake CompactBlock at the given height, spending a single note from the - /// given address. - pub(crate) fn fake_compact_block_spending( - height: BlockHeight, - prev_hash: BlockHash, - (nf, in_value): (Nullifier, Amount), - extfvk: ExtendedFullViewingKey, - to: PaymentAddress, - value: Amount, - ) -> CompactBlock { - let mut rng = OsRng; - let rseed = generate_random_rseed(&network(), height, &mut rng); - - // Create a fake CompactBlock containing the note - let mut cspend = CompactSpend::new(); - cspend.set_nf(nf.to_vec()); - 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().unwrap(), - pk_d: *to.pk_d(), - value: value.into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - to, - MemoBytes::empty(), - &mut rng, - ); - let cmu = note.cmu().to_repr().as_ref().to_vec(); - let epk = encryptor.epk().to_bytes().to_vec(); - 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.as_ref()[..52].to_vec()); - cout - }); - - // Create a fake Note for the change - ctx.outputs.push({ - let change_addr = extfvk.default_address().unwrap().1; - let rseed = generate_random_rseed(&network(), height, &mut rng); - let note = Note { - g_d: change_addr.diversifier().g_d().unwrap(), - pk_d: *change_addr.pk_d(), - value: (in_value - value).into(), - rseed, - }; - let encryptor = sapling_note_encryption::<_, Network>( - Some(extfvk.fvk.ovk), - note.clone(), - change_addr, - MemoBytes::empty(), - &mut rng, - ); - let cmu = note.cmu().to_repr().as_ref().to_vec(); - let epk = encryptor.epk().to_bytes().to_vec(); - 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.as_ref()[..52].to_vec()); - cout - }); - - let mut cb = CompactBlock::new(); - cb.set_height(u64::from(height)); - cb.hash.resize(32, 0); - rng.fill_bytes(&mut cb.hash); - cb.prevHash.extend_from_slice(&prev_hash.0); - cb.vtx.push(ctx); - cb - } - /// Insert a fake CompactBlock into the cache DB. pub(crate) fn insert_into_cache(db_cache: &BlockDb, cb: &CompactBlock) { let cb_bytes = cb.write_to_bytes().unwrap(); diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index c134417da2..a983f72af9 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -17,7 +17,7 @@ use zcash_primitives::{ consensus::{self, BlockHeight, NetworkUpgrade}, memo::{Memo, MemoBytes}, merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Note, Nullifier, PaymentAddress}, + sapling::{Node, Nullifier, PaymentAddress}, transaction::{components::Amount, Transaction, TxId}, zip32::ExtendedFullViewingKey, }; @@ -29,75 +29,16 @@ use zcash_client_backend::{ decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, encode_payment_address, }, - wallet::{AccountId, WalletShieldedOutput, WalletTx}, + wallet::{AccountId, WalletTx}, DecryptedOutput, }; +use zcash_extras::ShieldedOutput; use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb}; pub mod init; pub mod transact; -/// This trait provides a generalization over shielded output representations. -pub trait ShieldedOutput { - fn index(&self) -> usize; - fn account(&self) -> AccountId; - fn to(&self) -> &PaymentAddress; - fn note(&self) -> &Note; - fn memo(&self) -> Option<&MemoBytes>; - fn is_change(&self) -> Option; - fn nullifier(&self) -> Option; -} - -impl ShieldedOutput for WalletShieldedOutput { - fn index(&self) -> usize { - self.index - } - fn account(&self) -> AccountId { - self.account - } - fn to(&self) -> &PaymentAddress { - &self.to - } - fn note(&self) -> &Note { - &self.note - } - fn memo(&self) -> Option<&MemoBytes> { - None - } - fn is_change(&self) -> Option { - Some(self.is_change) - } - - fn nullifier(&self) -> Option { - Some(self.nf) - } -} - -impl ShieldedOutput for DecryptedOutput { - fn index(&self) -> usize { - self.index - } - fn account(&self) -> AccountId { - self.account - } - fn to(&self) -> &PaymentAddress { - &self.to - } - fn note(&self) -> &Note { - &self.note - } - fn memo(&self) -> Option<&MemoBytes> { - Some(&self.memo) - } - fn is_change(&self) -> Option { - None - } - fn nullifier(&self) -> Option { - None - } -} - /// Returns the address for the account. /// /// # Examples diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs index 5fccb430f3..32a7cf3946 100644 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ b/zcash_client_sqlite/src/wallet/transact.rs @@ -166,10 +166,11 @@ mod tests { data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead}, wallet::OvkPolicy, }; + use zcash_extras::fake_compact_block; use crate::{ chain::init::init_cache_database, - tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height}, + tests::{self, insert_into_cache, sapling_activation_height}, wallet::{ get_balance, get_balance_at, init::{init_accounts_table, init_blocks_table, init_wallet_db}, diff --git a/zcash_extras/Cargo.toml b/zcash_extras/Cargo.toml new file mode 100644 index 0000000000..aa07c7ca88 --- /dev/null +++ b/zcash_extras/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "zcash_extras" +version = "0.1.0" +edition = "2018" + +[lib] +doctest = false + +[dependencies] +async-trait = "0.1.52" +group = "0.8" +ff = "0.8" +jubjub = "0.5.1" +protobuf = "2.20" +rand_core = "0.5.1" +zcash_client_backend = { version = "0.5", path = "../zcash_client_backend" } +zcash_primitives = { version = "0.5", path = "../zcash_primitives" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +time = "0.3.20" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +time = { version = "0.3.20", features = ["wasm-bindgen"]} + diff --git a/zcash_extras/src/lib.rs b/zcash_extras/src/lib.rs new file mode 100644 index 0000000000..73f357b619 --- /dev/null +++ b/zcash_extras/src/lib.rs @@ -0,0 +1,420 @@ +pub mod wallet; + +use ff::PrimeField; +use group::GroupEncoding; +use rand_core::{OsRng, RngCore}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::{cmp, fmt}; +use zcash_client_backend::data_api::wallet::ANCHOR_OFFSET; +use zcash_client_backend::data_api::PrunedBlock; +use zcash_client_backend::data_api::ReceivedTransaction; +use zcash_client_backend::data_api::SentTransaction; +use zcash_client_backend::proto::compact_formats::{ + CompactBlock, CompactOutput, CompactSpend, CompactTx, +}; +use zcash_client_backend::wallet::SpendableNote; +use zcash_client_backend::wallet::{AccountId, WalletShieldedOutput}; +use zcash_client_backend::DecryptedOutput; +use zcash_primitives::block::BlockHash; +use zcash_primitives::consensus::BlockHeight; +use zcash_primitives::memo::{Memo, MemoBytes}; +use zcash_primitives::merkle_tree::CommitmentTree; +use zcash_primitives::merkle_tree::IncrementalWitness; +use zcash_primitives::sapling::{Node, Note, Nullifier, PaymentAddress}; +use zcash_primitives::transaction::components::Amount; +use zcash_primitives::transaction::TxId; +use zcash_primitives::zip32::ExtendedFullViewingKey; +use zcash_primitives::{ + consensus::Network, + sapling::{note_encryption::sapling_note_encryption, util::generate_random_rseed}, +}; + +#[async_trait::async_trait] +pub trait WalletRead: Send + Sync + 'static { + type Error; + type NoteRef: Debug; + type TxRef: Copy + Debug; + + /// Returns the minimum and maximum block heights for stored blocks. + /// + /// This will return `Ok(None)` if no block data is present in the database. + async fn block_height_extrema(&self) + -> Result, Self::Error>; + + /// Returns the default target height (for the block in which a new + /// transaction would be mined) and anchor height (to use for a new + /// transaction), given the range of block heights that the backend + /// knows about. + /// + /// This will return `Ok(None)` if no block data is present in the database. + async fn get_target_and_anchor_heights( + &self, + ) -> Result, Self::Error> { + self.block_height_extrema().await.map(|heights| { + heights.map(|(min_height, 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 = BlockHeight::from(cmp::max( + u32::from(target_height).saturating_sub(ANCHOR_OFFSET), + u32::from(min_height), + )); + + (target_height, anchor_height) + }) + }) + } + + /// Returns the block hash for the block at the given height, if the + /// associated block data is available. Returns `Ok(None)` if the hash + /// is not found in the database. + async fn get_block_hash( + &self, + block_height: BlockHeight, + ) -> Result, Self::Error>; + + /// Returns the block hash for the block at the maximum height known + /// in stored data. + /// + /// This will return `Ok(None)` if no block data is present in the database. + async fn get_max_height_hash(&self) -> Result, Self::Error> { + let extrema = self.block_height_extrema().await?; + let res = if let Some((_, max_height)) = extrema { + self.get_block_hash(max_height) + .await + .map(|hash_opt| hash_opt.map(move |hash| (max_height, hash)))? + } else { + None + }; + + Ok(res) + } + + /// Returns the block height in which the specified transaction was mined, + /// or `Ok(None)` if the transaction is not mined in the main chain. + async fn get_tx_height(&self, txid: TxId) -> Result, Self::Error>; + + /// Returns the payment address for the specified account, if the account + /// identifier specified refers to a valid account for this wallet. + /// + /// This will return `Ok(None)` if the account identifier does not correspond + /// to a known account. + async fn get_address(&self, account: AccountId) -> Result, Self::Error>; + + /// Returns all extended full viewing keys known about by this wallet. + async fn get_extended_full_viewing_keys( + &self, + ) -> Result, Self::Error>; + + /// Checks whether the specified extended full viewing key is + /// associated with the account. + async fn is_valid_account_extfvk( + &self, + account: AccountId, + extfvk: &ExtendedFullViewingKey, + ) -> Result; + + /// Returns the wallet balance for an account as of the specified block + /// height. + /// + /// This may be used to obtain a balance that ignores notes that have been + /// received so recently that they are not yet deemed spendable. + async fn get_balance_at( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result; + + /// Returns the memo for a note. + /// + /// Implementations of this method must return an error if the note identifier + /// does not appear in the backing data store. + async fn get_memo(&self, id_note: Self::NoteRef) -> Result; + + /// Returns the note commitment tree at the specified block height. + async fn get_commitment_tree( + &self, + block_height: BlockHeight, + ) -> Result>, Self::Error>; + + /// Returns the incremental witnesses as of the specified block height. + #[allow(clippy::type_complexity)] + async fn get_witnesses( + &self, + block_height: BlockHeight, + ) -> Result)>, Self::Error>; + + /// Returns the unspent nullifiers, along with the account identifiers + /// with which they are associated. + async fn get_nullifiers(&self) -> Result, Self::Error>; + + /// Return all spendable notes. + async fn get_spendable_notes( + &self, + account: AccountId, + anchor_height: BlockHeight, + ) -> Result, Self::Error>; + + /// Returns a list of spendable notes sufficient to cover the specified + /// target value, if possible. + async fn select_spendable_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + ) -> Result, Self::Error>; +} + +/// This trait encapsulates the write capabilities required to update stored +/// wallet data. +#[async_trait::async_trait] +pub trait WalletWrite: WalletRead { + #[allow(clippy::type_complexity)] + async fn advance_by_block( + &mut self, + block: &PrunedBlock, + updated_witnesses: &[(Self::NoteRef, IncrementalWitness)], + ) -> Result)>, Self::Error>; + + async fn store_received_tx( + &mut self, + received_tx: &ReceivedTransaction, + ) -> Result; + + async fn store_sent_tx( + &mut self, + sent_tx: &SentTransaction, + ) -> Result; + + /// Rewinds the wallet database to the specified height. + /// + /// This method assumes that the state of the underlying data store is + /// consistent up to a particular block height. Since it is possible that + /// a chain reorg might invalidate some stored state, this method must be + /// implemented in order to allow users of this API to "reset" the data store + /// to correctly represent chainstate as of a specified block height. + /// + /// After calling this method, the block at the given height will be the + /// most recent block and all other operations will treat this block + /// as the chain tip for balance determination purposes. + /// + /// There may be restrictions on how far it is possible to rewind. + async fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>; +} + +/// This trait provides a generalization over shielded output representations. +pub trait ShieldedOutput { + fn index(&self) -> usize; + fn account(&self) -> AccountId; + fn to(&self) -> &PaymentAddress; + fn note(&self) -> &Note; + fn memo(&self) -> Option<&MemoBytes>; + fn is_change(&self) -> Option; + fn nullifier(&self) -> Option; +} + +impl ShieldedOutput for WalletShieldedOutput { + fn index(&self) -> usize { + self.index + } + fn account(&self) -> AccountId { + self.account + } + fn to(&self) -> &PaymentAddress { + &self.to + } + fn note(&self) -> &Note { + &self.note + } + fn memo(&self) -> Option<&MemoBytes> { + None + } + fn is_change(&self) -> Option { + Some(self.is_change) + } + + fn nullifier(&self) -> Option { + Some(self.nf) + } +} + +impl ShieldedOutput for DecryptedOutput { + fn index(&self) -> usize { + self.index + } + fn account(&self) -> AccountId { + self.account + } + fn to(&self) -> &PaymentAddress { + &self.to + } + fn note(&self) -> &Note { + &self.note + } + fn memo(&self) -> Option<&MemoBytes> { + Some(&self.memo) + } + fn is_change(&self) -> Option { + None + } + fn nullifier(&self) -> Option { + None + } +} + +/// A newtype wrapper for sqlite primary key values for the notes +/// table. +#[derive(Debug, Copy, Clone)] +pub enum NoteId { + SentNoteId(i64), + ReceivedNoteId(i64), +} + +impl fmt::Display for NoteId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NoteId::SentNoteId(id) => write!(f, "Sent Note {}", id), + NoteId::ReceivedNoteId(id) => write!(f, "Received Note {}", id), + } + } +} + +pub(crate) fn test_network() -> Network { + Network::TestNetwork +} + +/// 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. +pub fn fake_compact_block( + height: BlockHeight, + prev_hash: BlockHash, + extfvk: ExtendedFullViewingKey, + value: Amount, +) -> (CompactBlock, Nullifier) { + let to = extfvk.default_address().unwrap().1; + + // Create a fake Note for the account + let mut rng = OsRng; + let rseed = generate_random_rseed(&test_network(), height, &mut rng); + let note = Note { + g_d: to.diversifier().g_d().unwrap(), + pk_d: *to.pk_d(), + value: value.into(), + rseed, + }; + let encryptor = sapling_note_encryption::<_, Network>( + Some(extfvk.fvk.ovk), + note.clone(), + to, + MemoBytes::empty(), + &mut rng, + ); + let cmu = note.cmu().to_repr().as_ref().to_vec(); + let epk = encryptor.epk().to_bytes().to_vec(); + 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.as_ref()[..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(u64::from(height)); + cb.hash.resize(32, 0); + rng.fill_bytes(&mut cb.hash); + cb.prevHash.extend_from_slice(&prev_hash.0); + cb.vtx.push(ctx); + (cb, note.nf(&extfvk.fvk.vk, 0)) +} + +/// Create a fake CompactBlock at the given height, spending a single note from the +/// given address. +pub fn fake_compact_block_spending( + height: BlockHeight, + prev_hash: BlockHash, + (nf, in_value): (Nullifier, Amount), + extfvk: ExtendedFullViewingKey, + to: PaymentAddress, + value: Amount, +) -> CompactBlock { + let mut rng = OsRng; + let rseed = generate_random_rseed(&test_network(), height, &mut rng); + + // Create a fake CompactBlock containing the note + let mut cspend = CompactSpend::new(); + cspend.set_nf(nf.to_vec()); + 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().unwrap(), + pk_d: *to.pk_d(), + value: value.into(), + rseed, + }; + let encryptor = sapling_note_encryption::<_, Network>( + Some(extfvk.fvk.ovk), + note.clone(), + to, + MemoBytes::empty(), + &mut rng, + ); + let cmu = note.cmu().to_repr().as_ref().to_vec(); + let epk = encryptor.epk().to_bytes().to_vec(); + 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.as_ref()[..52].to_vec()); + cout + }); + + // Create a fake Note for the change + ctx.outputs.push({ + let change_addr = extfvk.default_address().unwrap().1; + let rseed = generate_random_rseed(&test_network(), height, &mut rng); + let note = Note { + g_d: change_addr.diversifier().g_d().unwrap(), + pk_d: *change_addr.pk_d(), + value: (in_value - value).into(), + rseed, + }; + let encryptor = sapling_note_encryption::<_, Network>( + Some(extfvk.fvk.ovk), + note.clone(), + change_addr, + MemoBytes::empty(), + &mut rng, + ); + let cmu = note.cmu().to_repr().as_ref().to_vec(); + let epk = encryptor.epk().to_bytes().to_vec(); + 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.as_ref()[..52].to_vec()); + cout + }); + + let mut cb = CompactBlock::new(); + cb.set_height(u64::from(height)); + cb.hash.resize(32, 0); + rng.fill_bytes(&mut cb.hash); + cb.prevHash.extend_from_slice(&prev_hash.0); + cb.vtx.push(ctx); + cb +} diff --git a/zcash_extras/src/wallet.rs b/zcash_extras/src/wallet.rs new file mode 100644 index 0000000000..c097bd99ba --- /dev/null +++ b/zcash_extras/src/wallet.rs @@ -0,0 +1,223 @@ +//! Functions for scanning the chain and extracting relevant information. +use std::fmt::{Debug, Display}; + +use zcash_primitives::{ + consensus::{self, BranchId}, + memo::MemoBytes, + sapling::prover::TxProver, + transaction::{ + builder::Builder, + components::{amount::DEFAULT_FEE, Amount}, + }, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, +}; + +use crate::WalletWrite; +use zcash_client_backend::{ + address::RecipientAddress, + data_api::{error::Error, SentTransaction}, + wallet::{AccountId, OvkPolicy}, +}; + +#[allow(clippy::needless_doctest_main)] +/// 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. +/// +/// # Transaction privacy +/// +/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be +/// able to decrypt the outputs of this transaction. This is primarily relevant to +/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the +/// recipient's address, and the contents of `memo`, from ever being recovered from the +/// block chain. (The total value sent can always be inferred by the sender from the spent +/// notes and received change.) +/// +/// Regardless of the specified policy, `create_spend_to_address` saves `to`, `value`, and +/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`. +/// +/// For details on what transaction information is visible to the holder of a full or +/// outgoing viewing key, refer to [ZIP 310]. +/// +/// [ZIP 310]: https://zips.z.cash/zip-0310 +/// +/// # Examples +/// +/// ``` +/// use tempfile::NamedTempFile; +/// use zcash_primitives::{ +/// consensus::{self, Network}, +/// constants::testnet::COIN_TYPE, +/// transaction::components::Amount +/// }; +/// use zcash_proofs::prover::LocalTxProver; +/// use zcash_client_backend::{ +/// keys::spending_key, +/// data_api::wallet::create_spend_to_address, +/// wallet::{AccountId, OvkPolicy}, +/// }; +/// use zcash_client_sqlite::{ +/// WalletDb, +/// error::SqliteClientError, +/// wallet::init::init_wallet_db, +/// }; +/// +/// # // doctests have a problem with sqlite IO, so we ignore errors +/// # // generated in this example code as it's not really testing anything +/// # fn main() { +/// # test(); +/// # } +/// # +/// # fn test() -> Result<(), SqliteClientError> { +/// 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 = AccountId(0); +/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account.0); +/// let to = extsk.default_address().unwrap().1.into(); +/// +/// let data_file = NamedTempFile::new().unwrap(); +/// let db_read = WalletDb::for_path(data_file, Network::TestNetwork).unwrap(); +/// init_wallet_db(&db_read)?; +/// let mut db = db_read.get_update_ops()?; +/// +/// create_spend_to_address( +/// &mut db, +/// &Network::TestNetwork, +/// tx_prover, +/// account, +/// &extsk, +/// &to, +/// Amount::from_u64(1).unwrap(), +/// None, +/// OvkPolicy::Sender, +/// )?; +/// +/// # Ok(()) +/// # } +/// ``` +#[allow(clippy::too_many_arguments)] +pub async fn create_spend_to_address( + wallet_db: &mut D, + params: &P, + prover: impl TxProver, + account: AccountId, + extsk: &ExtendedSpendingKey, + to: &RecipientAddress, + value: Amount, + memo: Option, + ovk_policy: OvkPolicy, +) -> Result +where + N: Display, + E: From>, + P: consensus::Parameters + Clone, + R: Copy + Debug, + D: WalletWrite, +{ + // 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 !wallet_db.is_valid_account_extfvk(account, &extfvk).await? { + return Err(E::from(Error::InvalidExtSk(account))); + } + + // Apply the outgoing viewing key policy. + let ovk = match ovk_policy { + OvkPolicy::Sender => Some(extfvk.fvk.ovk), + OvkPolicy::Custom(ovk) => Some(ovk), + OvkPolicy::Discard => None, + }; + + // Target the next block, assuming we are up-to-date. + let (height, anchor_height) = wallet_db + .get_target_and_anchor_heights() + .await + .and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?; + + let target_value = value + DEFAULT_FEE; + let spendable_notes = wallet_db + .select_spendable_notes(account, target_value, anchor_height) + .await?; + + // Confirm we were able to select sufficient value + let selected_value = spendable_notes.iter().map(|n| n.note_value).sum(); + if selected_value < target_value { + return Err(E::from(Error::InsufficientBalance( + selected_value, + target_value, + ))); + } + + // Create the transaction + let mut builder = Builder::new(params.clone(), height); + for selected in spendable_notes { + let from = extfvk + .fvk + .vk + .to_payment_address(selected.diversifier) + .unwrap(); //DiversifyHash would have to unexpectedly return the zero point for this to be None + + let note = from + .create_note(selected.note_value.into(), selected.rseed) + .unwrap(); + + let merkle_path = selected.witness.path().expect("the tree is not empty"); + + builder + .add_sapling_spend(extsk.clone(), selected.diversifier, note, merkle_path) + .map_err(Error::Builder)?; + } + + match to { + RecipientAddress::Shielded(to) => { + builder.add_sapling_output(ovk, to.clone(), value, memo.clone()) + } + + RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value), + } + .map_err(Error::Builder)?; + + let consensus_branch_id = BranchId::for_height(params, height); + let (tx, tx_metadata) = builder + .build(consensus_branch_id, &prover) + .map_err(Error::Builder)?; + + let output_index = match to { + // Sapling outputs are shuffled, so we need to look up where the output ended up. + RecipientAddress::Shielded(_) => match tx_metadata.output_index(0) { + Some(idx) => idx, + None => panic!("Output 0 should exist in the transaction"), + }, + RecipientAddress::Transparent(addr) => { + let script = addr.script(); + tx.vout + .iter() + .enumerate() + .find(|(_, tx_out)| tx_out.script_pubkey == script) + .map(|(index, _)| index) + .expect("we sent to this address") + } + }; + + wallet_db + .store_sent_tx(&SentTransaction { + tx: &tx, + created: time::OffsetDateTime::now_utc(), + output_index, + account, + recipient_address: to, + value, + memo, + }) + .await +} diff --git a/zcash_primitives/Cargo.toml b/zcash_primitives/Cargo.toml index 5d90d3e51a..de8815b468 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -16,7 +16,7 @@ edition = "2018" all-features = true [dependencies] -aes = "0.8" +aes = "0.7" bitvec = "0.18" blake2b_simd = "0.5" blake2s_simd = "0.5" @@ -25,7 +25,7 @@ byteorder = "1" crypto_api_chachapoly = "0.4" equihash = { version = "0.1", path = "../components/equihash" } ff = "0.8" -fpe = "0.6" +fpe = "0.5" group = "0.8" hex = "0.4" jubjub = "0.5.1"