diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 013662a97b..b72bb15596 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -21,6 +21,7 @@ and this library adheres to Rust's notion of ### Changed - `zcash_client_backend::data_api::chain::scan_cached_blocks` now returns a `ScanSummary` containing metadata about the scanned blocks on success. + - The fields of `zcash_client_backend::wallet::ReceivedSaplingNote` are now private. Use `ReceivedSaplingNote::from_parts` for construction instead. Accessor methods are provided for each previously-public field. @@ -63,6 +64,16 @@ and this library adheres to Rust's notion of updated accordingly. - Almost all uses of `Amount` in `zcash_client_backend::zip321` have been replaced with `NonNegativeAmount`. +- In order to support better reusability for input selection code, three new + supertraits have been factored out from `zcash_client_backend::data_api::WalletRead`: + - `zcash_client_backend::data_api::TransparentInputSource` + - `zcash_client_backend::data_api::SaplingInputSource` + - `zcash_client_backend::data_api::InputSource` +- In `zcash_client_backend::data_api::wallet::input_selection::InputSelector`, + the signatures of `propose_transaction` and `propose_shielding` have been altered + such that these methods no longer take `min_confirmations` as an argument, instead + taking explicit `target_height` and `anchor_height` arguments. This helps to minimize + the set of capabilities that the `data_api::InputSource` trait must expose. ## [0.10.0] - 2023-09-25 diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 2522f80efd..cf6a3e76df 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -87,6 +87,7 @@ gumdrop = "0.8" jubjub.workspace = true proptest.workspace = true rand_core.workspace = true +shardtree = { workspace = true, features = ["test-dependencies"] } zcash_proofs.workspace = true zcash_address = { workspace = true, features = ["test-dependencies"] } diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 1253dbf98e..d9b4dd2f5a 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -210,12 +210,7 @@ impl WalletSummary { } } -/// Read-only operations required for light wallet functions. -/// -/// This trait defines the read-only portion of the storage interface atop which -/// higher-level wallet operations are implemented. It serves to allow wallet functions to -/// be abstracted away from any particular data storage substrate. -pub trait WalletRead { +pub trait SaplingInputSource { /// The type of errors produced by a wallet backend. type Error; @@ -225,14 +220,57 @@ pub trait WalletRead { /// or a UUID. type NoteRef: Copy + Debug + Eq + Ord; + /// Returns a list of spendable Sapling notes sufficient to cover the specified target value, + /// if possible. + fn select_spendable_sapling_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + exclude: &[Self::NoteRef], + ) -> Result>, Self::Error>; +} + +pub trait TransparentInputSource { + /// The type of errors produced by a wallet backend. + type Error; + + /// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and + /// including `max_height`. + fn get_unspent_transparent_outputs( + &self, + address: &TransparentAddress, + max_height: BlockHeight, + exclude: &[OutPoint], + ) -> Result, Self::Error>; +} + +pub trait InputSource: + SaplingInputSource::Error> + + TransparentInputSource::Error> +{ + type Error; +} + +/// Read-only operations required for light wallet functions. +/// +/// This trait defines the read-only portion of the storage interface atop which +/// higher-level wallet operations are implemented. It serves to allow wallet functions to +/// be abstracted away from any particular data storage substrate. +pub trait WalletRead: InputSource::Error> { + type Error; + /// Returns the height of the chain as known to the wallet as of the most recent call to /// [`WalletWrite::update_chain_tip`]. /// /// This will return `Ok(None)` if the height of the current consensus chain tip is unknown. - fn chain_height(&self) -> Result, Self::Error>; + fn chain_height(&self) -> Result, ::Error>; /// Returns the available block metadata for the block at the specified height, if any. - fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error>; + fn block_metadata( + &self, + height: BlockHeight, + ) -> Result, ::Error>; /// Returns the metadata for the block at the height to which the wallet has been fully /// scanned. @@ -241,14 +279,14 @@ pub trait WalletRead { /// blocks above the wallet's birthday height. Along with this height, this method returns /// metadata describing the state of the wallet's note commitment trees as of the end of that /// block. - fn block_fully_scanned(&self) -> Result, Self::Error>; + fn block_fully_scanned(&self) -> Result, ::Error>; /// Returns block metadata for the maximum height that the wallet has scanned. /// /// If the wallet is fully synced, this will be equivalent to `block_fully_scanned`; /// otherwise the maximal scanned height is likely to be greater than the fully scanned height /// due to the fact that out-of-order scanning can leave gaps. - fn block_max_scanned(&self) -> Result, Self::Error>; + fn block_max_scanned(&self) -> Result, ::Error>; /// Returns a vector of suggested scan ranges based upon the current wallet state. /// @@ -265,7 +303,7 @@ pub trait WalletRead { /// /// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock /// [`ScanPriority::Verify`]: crate::data_api::scanning::ScanPriority - fn suggest_scan_ranges(&self) -> Result, Self::Error>; + fn suggest_scan_ranges(&self) -> Result, ::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 @@ -276,34 +314,43 @@ pub trait WalletRead { fn get_target_and_anchor_heights( &self, min_confirmations: NonZeroU32, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Returns the minimum block height corresponding to an unspent note in the wallet. - fn get_min_unspent_height(&self) -> Result, Self::Error>; + fn get_min_unspent_height(&self) -> Result, ::Error>; /// 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. - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error>; + fn get_block_hash( + &self, + block_height: BlockHeight, + ) -> Result, ::Error>; /// Returns the block height and hash for the block at the maximum scanned block height. /// /// This will return `Ok(None)` if no blocks have been scanned. - fn get_max_height_hash(&self) -> Result, Self::Error>; + fn get_max_height_hash( + &self, + ) -> Result, ::Error>; /// Returns the block height in which the specified transaction was mined, or `Ok(None)` if the /// transaction is not in the main chain. - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error>; + fn get_tx_height(&self, txid: TxId) + -> Result, ::Error>; /// Returns the birthday height for the wallet. /// /// This returns the earliest birthday height among accounts maintained by this wallet, /// or `Ok(None)` if the wallet has no initialized accounts. - fn get_wallet_birthday(&self) -> Result, Self::Error>; + fn get_wallet_birthday(&self) -> Result, ::Error>; /// Returns the birthday height for the given account, or an error if the account is not known /// to the wallet. - fn get_account_birthday(&self, account: AccountId) -> Result; + fn get_account_birthday( + &self, + account: AccountId, + ) -> Result::Error>; /// Returns the most recently generated unified address for the specified account, if the /// account identifier specified refers to a valid account for this wallet. @@ -313,42 +360,42 @@ pub trait WalletRead { fn get_current_address( &self, account: AccountId, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Returns all unified full viewing keys known to this wallet. fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], if any. fn get_account_for_ufvk( &self, ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Checks whether the specified extended full viewing key is associated with the account. fn is_valid_account_extfvk( &self, account: AccountId, extfvk: &ExtendedFullViewingKey, - ) -> Result; + ) -> Result::Error>; /// Returns the wallet balances and sync status for an account given the specified minimum /// number of confirmations, or `Ok(None)` if the wallet has no balance data available. fn get_wallet_summary( &self, min_confirmations: u32, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Returns the memo for a note. /// /// Returns `Ok(None)` if the note is known to the wallet but memo data has not yet been /// populated for that note, or if the note identifier does not correspond to a note /// that is known to the wallet. - fn get_memo(&self, note_id: NoteId) -> Result, Self::Error>; + fn get_memo(&self, note_id: NoteId) -> Result, ::Error>; /// Returns a transaction. - fn get_transaction(&self, txid: TxId) -> Result; + fn get_transaction(&self, txid: TxId) -> Result::Error>; /// Returns the nullifiers for notes that the wallet is tracking, along with their associated /// account IDs, that are either unspent or have not yet been confirmed as spent (in that a @@ -356,7 +403,7 @@ pub trait WalletRead { fn get_sapling_nullifiers( &self, query: NullifierQuery, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Return all unspent Sapling notes, excluding the specified note IDs. fn get_spendable_sapling_notes( @@ -364,17 +411,7 @@ pub trait WalletRead { account: AccountId, anchor_height: BlockHeight, exclude: &[Self::NoteRef], - ) -> Result>, Self::Error>; - - /// Returns a list of spendable Sapling notes sufficient to cover the specified target value, - /// if possible. - fn select_spendable_sapling_notes( - &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error>; + ) -> Result>, ::Error>; /// Returns the set of all transparent receivers associated with the given account. /// @@ -384,16 +421,7 @@ pub trait WalletRead { fn get_transparent_receivers( &self, account: AccountId, - ) -> Result, Self::Error>; - - /// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and - /// including `max_height`. - fn get_unspent_transparent_outputs( - &self, - address: &TransparentAddress, - max_height: BlockHeight, - exclude: &[OutPoint], - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance, /// for each address associated with a nonzero balance. @@ -401,7 +429,7 @@ pub trait WalletRead { &self, account: AccountId, max_height: BlockHeight, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; } /// Metadata describing the sizes of the zcash note commitment trees as of a particular block. @@ -832,7 +860,7 @@ pub trait WalletWrite: WalletRead { &mut self, seed: &SecretVec, birthday: AccountBirthday, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error>; + ) -> Result<(AccountId, UnifiedSpendingKey), ::Error>; /// Generates and persists the next available diversified address, given the current /// addresses known to the wallet. @@ -842,7 +870,7 @@ pub trait WalletWrite: WalletRead { fn get_next_available_address( &mut self, account: AccountId, - ) -> Result, Self::Error>; + ) -> Result, ::Error>; /// Updates the state of the wallet database by persisting the provided block information, /// along with the note commitments that were detected when scanning the block for transactions @@ -852,7 +880,7 @@ pub trait WalletWrite: WalletRead { fn put_blocks( &mut self, blocks: Vec>, - ) -> Result<(), Self::Error>; + ) -> Result<(), ::Error>; /// Updates the wallet's view of the blockchain. /// @@ -861,14 +889,23 @@ pub trait WalletWrite: WalletRead { /// before proceeding with scanning. It should be called at wallet startup prior to calling /// [`WalletRead::suggest_scan_ranges`] in order to provide the wallet with the information it /// needs to correctly prioritize scanning operations. - fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error>; + fn update_chain_tip( + &mut self, + tip_height: BlockHeight, + ) -> Result<(), ::Error>; /// Caches a decrypted transaction in the persistent wallet store. - fn store_decrypted_tx(&mut self, received_tx: DecryptedTransaction) -> Result<(), Self::Error>; + fn store_decrypted_tx( + &mut self, + received_tx: DecryptedTransaction, + ) -> Result<(), ::Error>; /// Saves information about a transaction that was constructed and sent by the wallet to the /// persistent wallet store. - fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<(), Self::Error>; + fn store_sent_tx( + &mut self, + sent_tx: &SentTransaction, + ) -> Result<(), ::Error>; /// Truncates the wallet database to the specified height. /// @@ -883,13 +920,16 @@ pub trait WalletWrite: WalletRead { /// as the chain tip for balance determination purposes. /// /// There may be restrictions on heights to which it is possible to truncate. - fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>; + fn truncate_to_height( + &mut self, + block_height: BlockHeight, + ) -> Result<(), ::Error>; /// Adds a transparent UTXO received by the wallet to the data store. fn put_received_transparent_utxo( &mut self, output: &WalletTransparentOutput, - ) -> Result; + ) -> Result::Error>; } /// This trait describes a capability for manipulating wallet note commitment trees. @@ -961,8 +1001,9 @@ pub mod testing { use super::{ chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, - DecryptedTransaction, NoteId, NullifierQuery, ScannedBlock, SentTransaction, - WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + DecryptedTransaction, InputSource, NoteId, NullifierQuery, SaplingInputSource, + ScannedBlock, SentTransaction, TransparentInputSource, WalletCommitmentTrees, WalletRead, + WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, }; pub struct MockWalletDb { @@ -983,84 +1024,128 @@ pub mod testing { } } - impl WalletRead for MockWalletDb { + impl SaplingInputSource for MockWalletDb { type Error = (); type NoteRef = u32; - fn chain_height(&self) -> Result, Self::Error> { + fn select_spendable_sapling_notes( + &self, + _account: AccountId, + _target_value: Amount, + _anchor_height: BlockHeight, + _exclude: &[Self::NoteRef], + ) -> Result>, Self::Error> { + Ok(Vec::new()) + } + } + + impl TransparentInputSource for MockWalletDb { + type Error = (); + + fn get_unspent_transparent_outputs( + &self, + _address: &TransparentAddress, + _anchor_height: BlockHeight, + _exclude: &[OutPoint], + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + } + + impl InputSource for MockWalletDb { + type Error = (); + } + + impl WalletRead for MockWalletDb { + type Error = (); + + fn chain_height(&self) -> Result, ::Error> { Ok(None) } fn get_target_and_anchor_heights( &self, _min_confirmations: NonZeroU32, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } fn block_metadata( &self, _height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } - fn block_fully_scanned(&self) -> Result, Self::Error> { + fn block_fully_scanned( + &self, + ) -> Result, ::Error> { Ok(None) } - fn block_max_scanned(&self) -> Result, Self::Error> { + fn block_max_scanned(&self) -> Result, ::Error> { Ok(None) } - fn suggest_scan_ranges(&self) -> Result, Self::Error> { + fn suggest_scan_ranges(&self) -> Result, ::Error> { Ok(vec![]) } - fn get_min_unspent_height(&self) -> Result, Self::Error> { + fn get_min_unspent_height( + &self, + ) -> Result, ::Error> { Ok(None) } fn get_block_hash( &self, _block_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } - fn get_max_height_hash(&self) -> Result, Self::Error> { + fn get_max_height_hash( + &self, + ) -> Result, ::Error> { Ok(None) } - fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { + fn get_tx_height( + &self, + _txid: TxId, + ) -> Result, ::Error> { Ok(None) } - fn get_wallet_birthday(&self) -> Result, Self::Error> { + fn get_wallet_birthday(&self) -> Result, ::Error> { Ok(None) } - fn get_account_birthday(&self, _account: AccountId) -> Result { + fn get_account_birthday( + &self, + _account: AccountId, + ) -> Result::Error> { Err(()) } fn get_current_address( &self, _account: AccountId, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { + ) -> Result, ::Error> + { Ok(HashMap::new()) } fn get_account_for_ufvk( &self, _ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } @@ -1068,29 +1153,29 @@ pub mod testing { &self, _account: AccountId, _extfvk: &ExtendedFullViewingKey, - ) -> Result { + ) -> Result::Error> { Ok(false) } fn get_wallet_summary( &self, _min_confirmations: u32, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } - fn get_memo(&self, _id_note: NoteId) -> Result, Self::Error> { + fn get_memo(&self, _id_note: NoteId) -> Result, ::Error> { Ok(None) } - fn get_transaction(&self, _txid: TxId) -> Result { + fn get_transaction(&self, _txid: TxId) -> Result::Error> { Err(()) } fn get_sapling_nullifiers( &self, _query: NullifierQuery, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(Vec::new()) } @@ -1099,41 +1184,23 @@ pub mod testing { _account: AccountId, _anchor_height: BlockHeight, _exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - Ok(Vec::new()) - } - - fn select_spendable_sapling_notes( - &self, - _account: AccountId, - _target_value: Amount, - _anchor_height: BlockHeight, - _exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { + ) -> Result>, ::Error> { Ok(Vec::new()) } fn get_transparent_receivers( &self, _account: AccountId, - ) -> Result, Self::Error> { + ) -> Result, ::Error> + { Ok(HashMap::new()) } - fn get_unspent_transparent_outputs( - &self, - _address: &TransparentAddress, - _anchor_height: BlockHeight, - _exclude: &[OutPoint], - ) -> Result, Self::Error> { - Ok(Vec::new()) - } - fn get_transparent_balances( &self, _account: AccountId, _max_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(HashMap::new()) } } @@ -1145,7 +1212,7 @@ pub mod testing { &mut self, seed: &SecretVec, _birthday: AccountBirthday, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> { + ) -> Result<(AccountId, UnifiedSpendingKey), ::Error> { let account = AccountId::from(0); UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account) .map(|k| (account, k)) @@ -1155,7 +1222,7 @@ pub mod testing { fn get_next_available_address( &mut self, _account: AccountId, - ) -> Result, Self::Error> { + ) -> Result, ::Error> { Ok(None) } @@ -1163,26 +1230,35 @@ pub mod testing { fn put_blocks( &mut self, _blocks: Vec>, - ) -> Result<(), Self::Error> { + ) -> Result<(), ::Error> { Ok(()) } - fn update_chain_tip(&mut self, _tip_height: BlockHeight) -> Result<(), Self::Error> { + fn update_chain_tip( + &mut self, + _tip_height: BlockHeight, + ) -> Result<(), ::Error> { Ok(()) } fn store_decrypted_tx( &mut self, _received_tx: DecryptedTransaction, - ) -> Result<(), Self::Error> { + ) -> Result<(), ::Error> { Ok(()) } - fn store_sent_tx(&mut self, _sent_tx: &SentTransaction) -> Result<(), Self::Error> { + fn store_sent_tx( + &mut self, + _sent_tx: &SentTransaction, + ) -> Result<(), ::Error> { Ok(()) } - fn truncate_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> { + fn truncate_to_height( + &mut self, + _block_height: BlockHeight, + ) -> Result<(), ::Error> { Ok(()) } @@ -1190,7 +1266,7 @@ pub mod testing { fn put_received_transparent_utxo( &mut self, _output: &WalletTransparentOutput, - ) -> Result { + ) -> Result::Error> { Ok(0) } } diff --git a/zcash_client_backend/src/data_api/chain.rs b/zcash_client_backend/src/data_api/chain.rs index e4353fc26a..6e543ce012 100644 --- a/zcash_client_backend/src/data_api/chain.rs +++ b/zcash_client_backend/src/data_api/chain.rs @@ -163,6 +163,8 @@ use crate::{ pub mod error; use error::Error; +use super::WalletRead; + /// A struct containing metadata about a subtree root of the note commitment tree. /// /// This stores the block height at which the leaf that completed the subtree was @@ -274,7 +276,7 @@ pub fn scan_cached_blocks( data_db: &mut DbT, from_height: BlockHeight, limit: usize, -) -> Result> +) -> Result::Error, BlockSourceT::Error>> where ParamsT: consensus::Parameters + Send + 'static, BlockSourceT: BlockSource, @@ -327,7 +329,7 @@ where }; let mut continuity_check_metadata = prior_block_metadata; - block_source.with_blocks::<_, DbT::Error>( + block_source.with_blocks::<_, ::Error>( Some(from_height), Some(limit), |block: CompactBlock| { @@ -378,7 +380,7 @@ where let mut scan_end_height = from_height; let mut received_note_count = 0; let mut spent_note_count = 0; - block_source.with_blocks::<_, DbT::Error>( + block_source.with_blocks::<_, ::Error>( Some(from_height), Some(limit), |block: CompactBlock| { diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index c2de33e26d..8ebbd478af 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -51,7 +51,7 @@ pub fn decrypt_and_store_transaction( params: &ParamsT, data: &mut DbT, tx: &Transaction, -) -> Result<(), DbT::Error> +) -> Result<(), ::Error> where ParamsT: consensus::Parameters, DbT: WalletWrite, @@ -357,7 +357,12 @@ pub fn propose_transfer( min_confirmations: NonZeroU32, ) -> Result< Proposal, - Error::Error>, + Error< + ::Error, + CommitmentTreeErrT, + InputsT::Error, + ::Error, + >, > where DbT: WalletWrite, @@ -365,13 +370,20 @@ where ParamsT: consensus::Parameters + Clone, InputsT: InputSelector, { + use self::input_selection::InputSelectorError; + let (target_height, anchor_height) = wallet_db + .get_target_and_anchor_heights(min_confirmations) + .map_err(|e| Error::from(InputSelectorError::DataSource(e)))? + .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?; + input_selector .propose_transaction( params, wallet_db, + target_height, + anchor_height, spend_from_account, request, - min_confirmations, ) .map_err(Error::from) } @@ -410,7 +422,7 @@ pub fn propose_standard_transfer_to_address( ) -> Result< Proposal, Error< - DbT::Error, + ::Error, CommitmentTreeErrT, GreedyInputSelectorError, Zip317FeeError, @@ -459,7 +471,12 @@ pub fn propose_shielding( min_confirmations: NonZeroU32, ) -> Result< Proposal, - Error::Error>, + Error< + ::Error, + CommitmentTreeErrT, + InputsT::Error, + ::Error, + >, > where ParamsT: consensus::Parameters, @@ -467,13 +484,20 @@ where DbT::NoteRef: Copy + Eq + Ord, InputsT: InputSelector, { + use self::input_selection::InputSelectorError; + let (target_height, anchor_height) = wallet_db + .get_target_and_anchor_heights(min_confirmations) + .map_err(|e| Error::from(InputSelectorError::DataSource(e)))? + .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?; + input_selector .propose_shielding( params, wallet_db, shielding_threshold, + target_height, + anchor_height, from_addrs, - min_confirmations, ) .map_err(Error::from) } diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index 1c3685ed03..bdfe56bcd6 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -2,7 +2,6 @@ use core::marker::PhantomData; use std::fmt; -use std::num::NonZeroU32; use std::{collections::BTreeSet, fmt::Debug}; use zcash_primitives::{ @@ -19,9 +18,10 @@ use zcash_primitives::{ zip32::AccountId, }; +use crate::data_api::{SaplingInputSource, TransparentInputSource}; use crate::{ address::{RecipientAddress, UnifiedAddress}, - data_api::WalletRead, + data_api::InputSource, fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance}, wallet::{ReceivedSaplingNote, WalletTransparentOutput}, zip321::TransactionRequest, @@ -158,7 +158,7 @@ pub trait InputSelector { /// the internals of a particular backing data store, if the generic API of `WalletRead` does /// not provide sufficiently fine-grained operations for a particular backing store to /// optimally perform input selection. - type DataSource: WalletRead; + type DataSource: InputSource; /// The type of the fee rule that this input selector uses when computing fees. type FeeRule: FeeRule; @@ -182,12 +182,19 @@ pub trait InputSelector { &self, params: &ParamsT, wallet_db: &Self::DataSource, + target_height: BlockHeight, + anchor_height: BlockHeight, account: AccountId, transaction_request: TransactionRequest, - min_confirmations: NonZeroU32, ) -> Result< - Proposal::DataSource as WalletRead>::NoteRef>, - InputSelectorError<<::DataSource as WalletRead>::Error, Self::Error>, + Proposal< + Self::FeeRule, + <::DataSource as SaplingInputSource>::NoteRef, + >, + InputSelectorError< + <::DataSource as InputSource>::Error, + Self::Error, + >, > where ParamsT: consensus::Parameters; @@ -207,11 +214,18 @@ pub trait InputSelector { params: &ParamsT, wallet_db: &Self::DataSource, shielding_threshold: NonNegativeAmount, + target_height: BlockHeight, + latest_anchor: BlockHeight, source_addrs: &[TransparentAddress], - min_confirmations: NonZeroU32, ) -> Result< - Proposal::DataSource as WalletRead>::NoteRef>, - InputSelectorError<<::DataSource as WalletRead>::Error, Self::Error>, + Proposal< + Self::FeeRule, + <::DataSource as SaplingInputSource>::NoteRef, + >, + InputSelectorError< + <::DataSource as InputSource>::Error, + Self::Error, + >, > where ParamsT: consensus::Parameters; @@ -313,7 +327,7 @@ impl GreedyInputSelector { impl InputSelector for GreedyInputSelector where - DbT: WalletRead, + DbT: InputSource, ChangeT: ChangeStrategy, ChangeT::FeeRule: Clone, { @@ -326,19 +340,17 @@ where &self, params: &ParamsT, wallet_db: &Self::DataSource, + target_height: BlockHeight, + anchor_height: BlockHeight, account: AccountId, transaction_request: TransactionRequest, - min_confirmations: NonZeroU32, - ) -> Result, InputSelectorError> + ) -> Result< + Proposal, + InputSelectorError<::Error, Self::Error>, + > where ParamsT: consensus::Parameters, { - // Target the next block, assuming we are up-to-date. - let (target_height, anchor_height) = wallet_db - .get_target_and_anchor_heights(min_confirmations) - .map_err(InputSelectorError::DataSource) - .and_then(|x| x.ok_or(InputSelectorError::SyncRequired))?; - let mut transparent_outputs = vec![]; let mut sapling_outputs = vec![]; for payment in transaction_request.payments() { @@ -447,17 +459,16 @@ where params: &ParamsT, wallet_db: &Self::DataSource, shielding_threshold: NonNegativeAmount, + target_height: BlockHeight, + latest_anchor: BlockHeight, source_addrs: &[TransparentAddress], - min_confirmations: NonZeroU32, - ) -> Result, InputSelectorError> + ) -> Result< + Proposal, + InputSelectorError<::Error, Self::Error>, + > where ParamsT: consensus::Parameters, { - let (target_height, latest_anchor) = wallet_db - .get_target_and_anchor_heights(min_confirmations) - .map_err(InputSelectorError::DataSource) - .and_then(|x| x.ok_or(InputSelectorError::SyncRequired))?; - let mut transparent_inputs: Vec = source_addrs .iter() .map(|taddr| wallet_db.get_unspent_transparent_outputs(taddr, latest_anchor, &[])) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index c35a42d188..ec64f7e0c8 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -70,9 +70,10 @@ use zcash_client_backend::{ self, chain::{BlockSource, CommitmentTreeRoot}, scanning::{ScanPriority, ScanRange}, - AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType, - Recipient, ScannedBlock, SentTransaction, ShieldedProtocol, WalletCommitmentTrees, - WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NoteId, NullifierQuery, + PoolType, Recipient, SaplingInputSource, ScannedBlock, SentTransaction, ShieldedProtocol, + TransparentInputSource, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, + SAPLING_SHARD_HEIGHT, }, keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, proto::compact_formats::CompactBlock, @@ -167,29 +168,85 @@ impl WalletDb { } } -impl, P: consensus::Parameters> WalletRead for WalletDb { +impl, P: consensus::Parameters> SaplingInputSource + for WalletDb +{ type Error = SqliteClientError; type NoteRef = ReceivedNoteId; - fn chain_height(&self) -> Result, Self::Error> { + fn select_spendable_sapling_notes( + &self, + account: AccountId, + target_value: Amount, + anchor_height: BlockHeight, + exclude: &[Self::NoteRef], + ) -> Result>, Self::Error> { + wallet::sapling::select_spendable_sapling_notes( + self.conn.borrow(), + account, + target_value, + anchor_height, + exclude, + ) + } +} + +impl, P: consensus::Parameters> TransparentInputSource + for WalletDb +{ + type Error = SqliteClientError; + + fn get_unspent_transparent_outputs( + &self, + _address: &TransparentAddress, + _max_height: BlockHeight, + _exclude: &[OutPoint], + ) -> Result, Self::Error> { + #[cfg(feature = "transparent-inputs")] + return wallet::get_unspent_transparent_outputs( + self.conn.borrow(), + &self.params, + _address, + _max_height, + _exclude, + ); + + #[cfg(not(feature = "transparent-inputs"))] + panic!( + "The wallet must be compiled with the transparent-inputs feature to use this method." + ); + } +} + +impl, P: consensus::Parameters> InputSource for WalletDb { + type Error = SqliteClientError; +} + +impl, P: consensus::Parameters> WalletRead for WalletDb { + type Error = SqliteClientError; + + fn chain_height(&self) -> Result, SqliteClientError> { wallet::scan_queue_extrema(self.conn.borrow()) .map(|h| h.map(|range| *range.end())) .map_err(SqliteClientError::from) } - fn block_metadata(&self, height: BlockHeight) -> Result, Self::Error> { + fn block_metadata( + &self, + height: BlockHeight, + ) -> Result, SqliteClientError> { wallet::block_metadata(self.conn.borrow(), height) } - fn block_fully_scanned(&self) -> Result, Self::Error> { + fn block_fully_scanned(&self) -> Result, SqliteClientError> { wallet::block_fully_scanned(self.conn.borrow()) } - fn block_max_scanned(&self) -> Result, Self::Error> { + fn block_max_scanned(&self) -> Result, SqliteClientError> { wallet::block_max_scanned(self.conn.borrow()) } - fn suggest_scan_ranges(&self) -> Result, Self::Error> { + fn suggest_scan_ranges(&self) -> Result, SqliteClientError> { wallet::scanning::suggest_scan_ranges(self.conn.borrow(), ScanPriority::Historic) .map_err(SqliteClientError::from) } @@ -197,53 +254,56 @@ impl, P: consensus::Parameters> WalletRead for W fn get_target_and_anchor_heights( &self, min_confirmations: NonZeroU32, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations) .map_err(SqliteClientError::from) } - fn get_min_unspent_height(&self) -> Result, Self::Error> { + fn get_min_unspent_height(&self) -> Result, SqliteClientError> { wallet::get_min_unspent_height(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { + fn get_block_hash( + &self, + block_height: BlockHeight, + ) -> Result, SqliteClientError> { wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from) } - fn get_max_height_hash(&self) -> Result, Self::Error> { + fn get_max_height_hash(&self) -> Result, SqliteClientError> { wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { + fn get_tx_height(&self, txid: TxId) -> Result, SqliteClientError> { wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from) } - fn get_wallet_birthday(&self) -> Result, Self::Error> { + fn get_wallet_birthday(&self) -> Result, SqliteClientError> { wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from) } - fn get_account_birthday(&self, account: AccountId) -> Result { + fn get_account_birthday(&self, account: AccountId) -> Result { wallet::account_birthday(self.conn.borrow(), account).map_err(SqliteClientError::from) } fn get_current_address( &self, account: AccountId, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { wallet::get_current_address(self.conn.borrow(), &self.params, account) .map(|res| res.map(|(addr, _)| addr)) } fn get_unified_full_viewing_keys( &self, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params) } fn get_account_for_ufvk( &self, ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk) } @@ -251,18 +311,18 @@ impl, P: consensus::Parameters> WalletRead for W &self, account: AccountId, extfvk: &ExtendedFullViewingKey, - ) -> Result { + ) -> Result { wallet::is_valid_account_extfvk(self.conn.borrow(), &self.params, account, extfvk) } fn get_wallet_summary( &self, min_confirmations: u32, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { wallet::get_wallet_summary(self.conn.borrow(), min_confirmations, &SubtreeScanProgress) } - fn get_memo(&self, note_id: NoteId) -> Result, Self::Error> { + fn get_memo(&self, note_id: NoteId) -> Result, SqliteClientError> { let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?; if sent_memo.is_some() { Ok(sent_memo) @@ -271,14 +331,14 @@ impl, P: consensus::Parameters> WalletRead for W } } - fn get_transaction(&self, txid: TxId) -> Result { + fn get_transaction(&self, txid: TxId) -> Result { wallet::get_transaction(self.conn.borrow(), &self.params, txid).map(|(_, tx)| tx) } fn get_sapling_nullifiers( &self, query: NullifierQuery, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { match query { NullifierQuery::Unspent => wallet::sapling::get_sapling_nullifiers(self.conn.borrow()), NullifierQuery::All => wallet::sapling::get_all_sapling_nullifiers(self.conn.borrow()), @@ -290,7 +350,7 @@ impl, P: consensus::Parameters> WalletRead for W account: AccountId, anchor_height: BlockHeight, exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { + ) -> Result>, SqliteClientError> { wallet::sapling::get_spendable_sapling_notes( self.conn.borrow(), account, @@ -299,26 +359,10 @@ impl, P: consensus::Parameters> WalletRead for W ) } - fn select_spendable_sapling_notes( - &self, - account: AccountId, - target_value: Amount, - anchor_height: BlockHeight, - exclude: &[Self::NoteRef], - ) -> Result>, Self::Error> { - wallet::sapling::select_spendable_sapling_notes( - self.conn.borrow(), - account, - target_value, - anchor_height, - exclude, - ) - } - fn get_transparent_receivers( &self, _account: AccountId, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { #[cfg(feature = "transparent-inputs")] return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account); @@ -328,32 +372,11 @@ impl, P: consensus::Parameters> WalletRead for W ); } - fn get_unspent_transparent_outputs( - &self, - _address: &TransparentAddress, - _max_height: BlockHeight, - _exclude: &[OutPoint], - ) -> Result, Self::Error> { - #[cfg(feature = "transparent-inputs")] - return wallet::get_unspent_transparent_outputs( - self.conn.borrow(), - &self.params, - _address, - _max_height, - _exclude, - ); - - #[cfg(not(feature = "transparent-inputs"))] - panic!( - "The wallet must be compiled with the transparent-inputs feature to use this method." - ); - } - fn get_transparent_balances( &self, _account: AccountId, _max_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { #[cfg(feature = "transparent-inputs")] return wallet::get_transparent_balances( self.conn.borrow(), @@ -376,7 +399,7 @@ impl WalletWrite for WalletDb &mut self, seed: &SecretVec, birthday: AccountBirthday, - ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> { + ) -> Result<(AccountId, UnifiedSpendingKey), SqliteClientError> { self.transactionally(|wdb| { let account = wallet::get_max_account_id(wdb.conn.0)? .map(|a| AccountId::from(u32::from(a) + 1)) @@ -399,7 +422,7 @@ impl WalletWrite for WalletDb fn get_next_available_address( &mut self, account: AccountId, - ) -> Result, Self::Error> { + ) -> Result, SqliteClientError> { self.transactionally( |wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) { Some(ufvk) => { @@ -438,7 +461,7 @@ impl WalletWrite for WalletDb fn put_blocks( &mut self, blocks: Vec>, - ) -> Result<(), Self::Error> { + ) -> Result<(), SqliteClientError> { self.transactionally(|wdb| { let start_positions = blocks.first().map(|block| { ( @@ -568,14 +591,14 @@ impl WalletWrite for WalletDb }) } - fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> { + fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), SqliteClientError> { let tx = self.conn.transaction()?; wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?; tx.commit()?; Ok(()) } - fn store_decrypted_tx(&mut self, d_tx: DecryptedTransaction) -> Result<(), Self::Error> { + fn store_decrypted_tx(&mut self, d_tx: DecryptedTransaction) -> Result<(), SqliteClientError> { self.transactionally(|wdb| { let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None)?; @@ -666,7 +689,7 @@ impl WalletWrite for WalletDb }) } - fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<(), Self::Error> { + fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<(), SqliteClientError> { self.transactionally(|wdb| { let tx_ref = wallet::put_tx_data( wdb.conn.0, @@ -729,7 +752,7 @@ impl WalletWrite for WalletDb }) } - fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> { + fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), SqliteClientError> { self.transactionally(|wdb| { wallet::truncate_to_height(wdb.conn.0, &wdb.params, block_height) }) @@ -738,7 +761,7 @@ impl WalletWrite for WalletDb fn put_received_transparent_utxo( &mut self, _output: &WalletTransparentOutput, - ) -> Result { + ) -> Result { #[cfg(feature = "transparent-inputs")] return wallet::put_received_transparent_utxo(&self.conn, &self.params, _output); diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index a74ee67f0e..88470e8e22 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -1949,7 +1949,9 @@ mod tests { PRUNING_DEPTH, }, zcash_client_backend::{ - data_api::{wallet::input_selection::GreedyInputSelector, WalletWrite}, + data_api::{ + wallet::input_selection::GreedyInputSelector, TransparentInputSource, WalletWrite, + }, encoding::AddressCodec, fees::{fixed, DustOutputPolicy}, wallet::WalletTransparentOutput,