//! Types related to the process of selecting inputs to be spent given a transaction request. use core::marker::PhantomData; use std::{ collections::BTreeMap, error, fmt::{self, Debug, Display}, }; use nonempty::NonEmpty; use zcash_address::ConversionError; use zcash_primitives::{ consensus::{self, BlockHeight}, transaction::{ components::{ amount::{BalanceError, NonNegativeAmount}, TxOut, }, fees::FeeRule, }, }; use crate::{ address::{Address, UnifiedAddress}, data_api::{InputSource, SimpleNoteRetention, SpendableNotes}, fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy}, proposal::{Proposal, ProposalError, ShieldedInputs}, wallet::WalletTransparentOutput, zip321::TransactionRequest, PoolType, ShieldedProtocol, }; #[cfg(feature = "transparent-inputs")] use { std::collections::BTreeSet, std::convert::Infallible, zcash_primitives::legacy::TransparentAddress, zcash_primitives::transaction::components::OutPoint, }; #[cfg(feature = "orchard")] use crate::fees::orchard as orchard_fees; /// The type of errors that may be produced in input selection. #[derive(Debug)] pub enum InputSelectorError { /// An error occurred accessing the underlying data store. DataSource(DbErrT), /// An error occurred specific to the provided input selector's selection rules. Selection(SelectorErrT), /// Input selection attempted to generate an invalid transaction proposal. Proposal(ProposalError), /// An error occurred parsing the address from a payment request. Address(ConversionError<&'static str>), /// Insufficient funds were available to satisfy the payment request that inputs were being /// selected to attempt to satisfy. InsufficientFunds { available: NonNegativeAmount, required: NonNegativeAmount, }, /// The data source does not have enough information to choose an expiry height /// for the transaction. SyncRequired, } impl From> for InputSelectorError { fn from(value: ConversionError<&'static str>) -> Self { InputSelectorError::Address(value) } } impl fmt::Display for InputSelectorError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { InputSelectorError::DataSource(e) => { write!( f, "The underlying datasource produced the following error: {}", e ) } InputSelectorError::Selection(e) => { write!(f, "Note selection encountered the following error: {}", e) } InputSelectorError::Proposal(e) => { write!( f, "Input selection attempted to generate an invalid proposal: {}", e ) } InputSelectorError::Address(e) => { write!( f, "An error occurred decoding the address from a payment request: {}.", e ) } InputSelectorError::InsufficientFunds { available, required, } => write!( f, "Insufficient balance (have {}, need {} including fee)", u64::from(*available), u64::from(*required) ), InputSelectorError::SyncRequired => { write!(f, "Insufficient chain data is available, sync required.") } } } } impl error::Error for InputSelectorError where DE: Debug + Display + error::Error + 'static, SE: Debug + Display + error::Error + 'static, { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match &self { Self::DataSource(e) => Some(e), Self::Selection(e) => Some(e), Self::Proposal(e) => Some(e), _ => None, } } } /// A strategy for selecting transaction inputs and proposing transaction outputs. /// /// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`; /// that is, do not return inputs that cause fees to increase by an amount greater than the value /// of the input. pub trait InputSelector { /// The type of errors that may be generated in input selection type Error; /// The type of data source that the input selector expects to access to obtain input Sapling /// notes. This associated type permits input selectors that may use specialized knowledge of /// the internals of a particular backing data store, if the generic API of /// `InputSource` does not provide sufficiently fine-grained operations for a particular /// backing store to optimally perform input selection. type InputSource: InputSource; /// The type of the fee rule that this input selector uses when computing fees. type FeeRule: FeeRule; /// Performs input selection and returns a proposal for transaction construction including /// change and fee outputs. /// /// Implementations of this method should return inputs sufficient to satisfy the given /// transaction request using a best-effort strategy to preserve user privacy, as follows: /// * If it is possible to satisfy the specified transaction request by creating /// a fully-shielded transaction without requiring value to cross pool boundaries, /// return the inputs necessary to construct such a transaction; otherwise /// * If it is possible to satisfy the transaction request by creating a fully-shielded /// transaction with some amounts crossing between shielded pools, return the inputs /// necessary. /// /// If insufficient funds are available to satisfy the required outputs for the shielding /// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`]. #[allow(clippy::type_complexity)] fn propose_transaction( &self, params: &ParamsT, wallet_db: &Self::InputSource, target_height: BlockHeight, anchor_height: BlockHeight, account: ::AccountId, transaction_request: TransactionRequest, ) -> Result< Proposal::NoteRef>, InputSelectorError<::Error, Self::Error>, > where ParamsT: consensus::Parameters; } /// A strategy for selecting transaction inputs and proposing transaction outputs /// for shielding-only transactions (transactions which spend transparent UTXOs and /// send all transaction outputs to the wallet's shielded internal address(es)). #[cfg(feature = "transparent-inputs")] pub trait ShieldingSelector { /// The type of errors that may be generated in input selection type Error; /// The type of data source that the input selector expects to access to obtain input /// transparent UTXOs. This associated type permits input selectors that may use specialized /// knowledge of the internals of a particular backing data store, if the generic API of /// [`InputSource`] does not provide sufficiently fine-grained operations for a /// particular backing store to optimally perform input selection. type InputSource: InputSource; /// The type of the fee rule that this input selector uses when computing fees. type FeeRule: FeeRule; /// Performs input selection and returns a proposal for the construction of a shielding /// transaction. /// /// Implementations should return the maximum possible number of economically useful inputs /// required to supply at least the requested value, choosing only inputs received at the /// specified source addresses. If insufficient funds are available to satisfy the required /// outputs for the shielding request, this operation must fail and return /// [`InputSelectorError::InsufficientFunds`]. #[allow(clippy::type_complexity)] fn propose_shielding( &self, params: &ParamsT, wallet_db: &Self::InputSource, shielding_threshold: NonNegativeAmount, source_addrs: &[TransparentAddress], target_height: BlockHeight, min_confirmations: u32, ) -> Result< Proposal, InputSelectorError<::Error, Self::Error>, > where ParamsT: consensus::Parameters; } /// Errors that can occur as a consequence of greedy input selection. #[derive(Debug, Clone, PartialEq, Eq)] pub enum GreedyInputSelectorError { /// An intermediate value overflowed or underflowed the valid monetary range. Balance(BalanceError), /// A unified address did not contain a supported receiver. UnsupportedAddress(Box), /// An error was encountered in change selection. Change(ChangeError), } impl fmt::Display for GreedyInputSelectorError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { GreedyInputSelectorError::Balance(e) => write!( f, "A balance calculation violated amount validity bounds: {:?}.", e ), GreedyInputSelectorError::UnsupportedAddress(_) => { // we can't encode the UA to its string representation because we // don't have network parameters here write!(f, "Unified address contains no supported receivers.") } GreedyInputSelectorError::Change(err) => { write!(f, "An error occurred computing change and fees: {}", err) } } } } impl From> for InputSelectorError> { fn from(err: GreedyInputSelectorError) -> Self { InputSelectorError::Selection(err) } } impl From> for InputSelectorError> { fn from(err: ChangeError) -> Self { InputSelectorError::Selection(GreedyInputSelectorError::Change(err)) } } impl From for InputSelectorError> { fn from(err: BalanceError) -> Self { InputSelectorError::Selection(GreedyInputSelectorError::Balance(err)) } } pub(crate) struct SaplingPayment(NonNegativeAmount); #[cfg(test)] impl SaplingPayment { pub(crate) fn new(amount: NonNegativeAmount) -> Self { SaplingPayment(amount) } } impl sapling::OutputView for SaplingPayment { fn value(&self) -> NonNegativeAmount { self.0 } } #[cfg(feature = "orchard")] pub(crate) struct OrchardPayment(NonNegativeAmount); #[cfg(test)] #[cfg(feature = "orchard")] impl OrchardPayment { pub(crate) fn new(amount: NonNegativeAmount) -> Self { OrchardPayment(amount) } } #[cfg(feature = "orchard")] impl orchard_fees::OutputView for OrchardPayment { fn value(&self) -> NonNegativeAmount { self.0 } } /// An [`InputSelector`] implementation that uses a greedy strategy to select between available /// notes. /// /// This implementation performs input selection using methods available via the /// [`InputSource`] interface. pub struct GreedyInputSelector { change_strategy: ChangeT, dust_output_policy: DustOutputPolicy, _ds_type: PhantomData, } impl GreedyInputSelector { /// Constructs a new greedy input selector that uses the provided change strategy to determine /// change values and fee amounts. pub fn new(change_strategy: ChangeT, dust_output_policy: DustOutputPolicy) -> Self { GreedyInputSelector { change_strategy, dust_output_policy, _ds_type: PhantomData, } } } impl InputSelector for GreedyInputSelector where DbT: InputSource, ChangeT: ChangeStrategy, ChangeT::FeeRule: Clone, { type Error = GreedyInputSelectorError; type InputSource = DbT; type FeeRule = ChangeT::FeeRule; #[allow(clippy::type_complexity)] fn propose_transaction( &self, params: &ParamsT, wallet_db: &Self::InputSource, target_height: BlockHeight, anchor_height: BlockHeight, account: ::AccountId, transaction_request: TransactionRequest, ) -> Result< Proposal, InputSelectorError<::Error, Self::Error>, > where ParamsT: consensus::Parameters, Self::InputSource: InputSource, { let mut transparent_outputs = vec![]; let mut sapling_outputs = vec![]; #[cfg(feature = "orchard")] let mut orchard_outputs = vec![]; let mut payment_pools = BTreeMap::new(); for (idx, payment) in transaction_request.payments() { let recipient_address: Address = payment .recipient_address() .clone() .convert_if_network(params.network_type())?; match recipient_address { Address::Transparent(addr) => { payment_pools.insert(*idx, PoolType::Transparent); transparent_outputs.push(TxOut { value: payment.amount(), script_pubkey: addr.script(), }); } Address::Sapling(_) => { payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling)); sapling_outputs.push(SaplingPayment(payment.amount())); } Address::Unified(addr) => { #[cfg(feature = "orchard")] if addr.orchard().is_some() { payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard)); orchard_outputs.push(OrchardPayment(payment.amount())); continue; } if addr.sapling().is_some() { payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling)); sapling_outputs.push(SaplingPayment(payment.amount())); continue; } if let Some(addr) = addr.transparent() { payment_pools.insert(*idx, PoolType::Transparent); transparent_outputs.push(TxOut { value: payment.amount(), script_pubkey: addr.script(), }); continue; } return Err(InputSelectorError::Selection( GreedyInputSelectorError::UnsupportedAddress(Box::new(addr)), )); } } } let mut shielded_inputs = SpendableNotes::empty(); let mut prior_available = NonNegativeAmount::ZERO; let mut amount_required = NonNegativeAmount::ZERO; let mut exclude: Vec = vec![]; // This loop is guaranteed to terminate because on each iteration we check that the amount // of funds selected is strictly increasing. The loop will either return a successful // result or the wallet will eventually run out of funds to select. loop { #[cfg(not(feature = "orchard"))] let use_sapling = true; #[cfg(feature = "orchard")] let (use_sapling, use_orchard) = { let (sapling_input_total, orchard_input_total) = ( shielded_inputs.sapling_value()?, shielded_inputs.orchard_value()?, ); // Use Sapling inputs if there are no Orchard outputs or there are not sufficient // Orchard outputs to cover the amount required. let use_sapling = orchard_outputs.is_empty() || amount_required > orchard_input_total; // Use Orchard inputs if there are insufficient Sapling funds to cover the amount // reqiuired. let use_orchard = !use_sapling || amount_required > sapling_input_total; (use_sapling, use_orchard) }; let sapling_inputs = if use_sapling { shielded_inputs .sapling() .iter() .map(|i| (*i.internal_note_id(), i.note().value())) .collect() } else { vec![] }; #[cfg(feature = "orchard")] let orchard_inputs = if use_orchard { shielded_inputs .orchard() .iter() .map(|i| (*i.internal_note_id(), i.note().value())) .collect() } else { vec![] }; let balance = self.change_strategy.compute_balance( params, target_height, &Vec::::new(), &transparent_outputs, &( ::sapling::builder::BundleType::DEFAULT, &sapling_inputs[..], &sapling_outputs[..], ), #[cfg(feature = "orchard")] &( ::orchard::builder::BundleType::DEFAULT, &orchard_inputs[..], &orchard_outputs[..], ), &self.dust_output_policy, ); match balance { Ok(balance) => { return Proposal::single_step( transaction_request, payment_pools, vec![], NonEmpty::from_vec(shielded_inputs.into_vec(&SimpleNoteRetention { sapling: use_sapling, #[cfg(feature = "orchard")] orchard: use_orchard, })) .map(|notes| ShieldedInputs::from_parts(anchor_height, notes)), balance, (*self.change_strategy.fee_rule()).clone(), target_height, false, ) .map_err(InputSelectorError::Proposal); } Err(ChangeError::DustInputs { mut sapling, #[cfg(feature = "orchard")] mut orchard, .. }) => { exclude.append(&mut sapling); #[cfg(feature = "orchard")] exclude.append(&mut orchard); } Err(ChangeError::InsufficientFunds { required, .. }) => { amount_required = required; } Err(other) => return Err(other.into()), } #[cfg(not(feature = "orchard"))] let selectable_pools = &[ShieldedProtocol::Sapling]; #[cfg(feature = "orchard")] let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard]; shielded_inputs = wallet_db .select_spendable_notes( account, amount_required, selectable_pools, anchor_height, &exclude, ) .map_err(InputSelectorError::DataSource)?; let new_available = shielded_inputs.total_value()?; if new_available <= prior_available { return Err(InputSelectorError::InsufficientFunds { required: amount_required, available: new_available, }); } else { // If the set of selected inputs has changed after selection, we will loop again // and see whether we now have enough funds. prior_available = new_available; } } } } #[cfg(feature = "transparent-inputs")] impl ShieldingSelector for GreedyInputSelector where DbT: InputSource, ChangeT: ChangeStrategy, ChangeT::FeeRule: Clone, { type Error = GreedyInputSelectorError; type InputSource = DbT; type FeeRule = ChangeT::FeeRule; #[allow(clippy::type_complexity)] fn propose_shielding( &self, params: &ParamsT, wallet_db: &Self::InputSource, shielding_threshold: NonNegativeAmount, source_addrs: &[TransparentAddress], target_height: BlockHeight, min_confirmations: u32, ) -> Result< Proposal, InputSelectorError<::Error, Self::Error>, > where ParamsT: consensus::Parameters, { let mut transparent_inputs: Vec = source_addrs .iter() .map(|taddr| { wallet_db.get_unspent_transparent_outputs( taddr, target_height - min_confirmations, &[], ) }) .collect::>, _>>() .map_err(InputSelectorError::DataSource)? .into_iter() .flat_map(|v| v.into_iter()) .collect(); let trial_balance = self.change_strategy.compute_balance( params, target_height, &transparent_inputs, &Vec::::new(), &( ::sapling::builder::BundleType::DEFAULT, &Vec::::new()[..], &Vec::::new()[..], ), #[cfg(feature = "orchard")] &( orchard::builder::BundleType::DEFAULT, &Vec::::new()[..], &Vec::::new()[..], ), &self.dust_output_policy, ); let balance = match trial_balance { Ok(balance) => balance, Err(ChangeError::DustInputs { transparent, .. }) => { let exclusions: BTreeSet = transparent.into_iter().collect(); transparent_inputs.retain(|i| !exclusions.contains(i.outpoint())); self.change_strategy.compute_balance( params, target_height, &transparent_inputs, &Vec::::new(), &( ::sapling::builder::BundleType::DEFAULT, &Vec::::new()[..], &Vec::::new()[..], ), #[cfg(feature = "orchard")] &( orchard::builder::BundleType::DEFAULT, &Vec::::new()[..], &Vec::::new()[..], ), &self.dust_output_policy, )? } Err(other) => { return Err(other.into()); } }; if balance.total() >= shielding_threshold { Proposal::single_step( TransactionRequest::empty(), BTreeMap::new(), transparent_inputs, None, balance, (*self.change_strategy.fee_rule()).clone(), target_height, true, ) .map_err(InputSelectorError::Proposal) } else { Err(InputSelectorError::InsufficientFunds { available: balance.total(), required: shielding_threshold, }) } } }