Skip to main content

zcash_client_backend/data_api/wallet/
input_selection.rs

1//! Types related to the process of selecting inputs to be spent given a transaction request.
2#[cfg(feature = "transparent-inputs")]
3use {
4    crate::{
5        data_api::CoinbaseFilter,
6        fees::{ChangeValue, StandardFeeRule},
7        proposal::{Step, StepOutput, StepOutputIndex},
8    },
9    std::convert::Infallible,
10    transparent::{address::TransparentAddress, bundle::OutPoint, keys::TransparentKeyScope},
11    zcash_primitives::transaction::fees::{
12        transparent as transparent_fees, transparent::InputSize, zip317::P2PKH_STANDARD_INPUT_SIZE,
13    },
14    zcash_protocol::constants::MAX_BLOCK_BYTES,
15    zip321::Payment,
16};
17
18use core::marker::PhantomData;
19use nonempty::NonEmpty;
20use std::{
21    collections::{BTreeMap, BTreeSet},
22    error,
23    fmt::{self, Debug, Display},
24};
25
26use transparent::bundle::TxOut;
27use zcash_address::{ConversionError, ZcashAddress};
28use zcash_keys::address::{Address, UnifiedAddress};
29use zcash_primitives::transaction::{
30    TxVersion,
31    fees::{FeeRule, zip317::P2PKH_STANDARD_OUTPUT_SIZE},
32};
33use zcash_protocol::{
34    PoolType, ShieldedPool,
35    consensus::{self, BlockHeight},
36    memo::MemoBytes,
37    value::{BalanceError, MAX_MONEY, Zatoshis},
38};
39use zip321::TransactionRequest;
40
41use crate::{
42    data_api::{
43        InputSource, MaxSpendMode, ReceivedNotes, SimpleNoteRetention, TargetValue,
44        anchor_retention::PoolMigrationParams, wallet::TargetHeight,
45    },
46    fees::{ChangeError, ChangeStrategy, EphemeralBalance, TransactionBalance, sapling},
47    proposal::{Proposal, ProposalError, ShieldedInputs},
48    wallet::{ReceivedNote, WalletTransparentOutput},
49};
50
51pub use crate::data_api::locking::{LockFilter, LockedInputPolicy};
52
53use super::ConfirmationsPolicy;
54
55#[cfg(feature = "orchard")]
56use crate::{data_api::wallet::ironwood_active_at, fees::orchard as orchard_fees};
57
58/// The type of errors that may be produced in input selection.
59#[derive(Debug)]
60#[non_exhaustive]
61pub enum InputSelectorError<DbErrT, SelectorErrT, ChangeErrT, N> {
62    /// An error occurred accessing the underlying data store.
63    DataSource(DbErrT),
64    /// An error occurred specific to the provided input selector's selection rules.
65    Selection(SelectorErrT),
66    /// An error occurred in computing the change or fee for the proposed transfer.
67    Change(ChangeError<ChangeErrT, N>),
68    /// Input selection attempted to generate an invalid transaction proposal.
69    Proposal(ProposalError),
70    /// An error occurred parsing the address from a payment request.
71    Address(ConversionError<&'static str>),
72    /// Insufficient funds were available to satisfy the payment request that inputs were being
73    /// selected to attempt to satisfy.
74    InsufficientFunds {
75        available: Zatoshis,
76        required: Zatoshis,
77    },
78    /// The data source does not have enough information to choose an expiry height
79    /// for the transaction.
80    SyncRequired,
81}
82
83impl<DE: fmt::Display, SE: fmt::Display, CE: fmt::Display, N: fmt::Display> fmt::Display
84    for InputSelectorError<DE, SE, CE, N>
85{
86    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87        match &self {
88            InputSelectorError::DataSource(e) => {
89                write!(
90                    f,
91                    "The underlying datasource produced the following error: {e}"
92                )
93            }
94            InputSelectorError::Selection(e) => {
95                write!(f, "Note selection encountered the following error: {e}")
96            }
97            InputSelectorError::Change(e) => write!(
98                f,
99                "Proposal generation failed due to an error in computing change or transaction fees: {e}"
100            ),
101            InputSelectorError::Proposal(e) => {
102                write!(
103                    f,
104                    "Input selection attempted to generate an invalid proposal: {e}"
105                )
106            }
107            InputSelectorError::Address(e) => {
108                write!(
109                    f,
110                    "An error occurred decoding the address from a payment request: {e}."
111                )
112            }
113            InputSelectorError::InsufficientFunds {
114                available,
115                required,
116            } => write!(
117                f,
118                "Insufficient balance (have {}, need {} including fee)",
119                u64::from(*available),
120                u64::from(*required)
121            ),
122            InputSelectorError::SyncRequired => {
123                write!(f, "Insufficient chain data is available, sync required.")
124            }
125        }
126    }
127}
128
129impl<DE, SE, CE, N> error::Error for InputSelectorError<DE, SE, CE, N>
130where
131    DE: Debug + Display + error::Error + 'static,
132    SE: Debug + Display + error::Error + 'static,
133    CE: Debug + Display + error::Error + 'static,
134    N: Debug + Display + 'static,
135{
136    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
137        match &self {
138            Self::DataSource(e) => Some(e),
139            Self::Selection(e) => Some(e),
140            Self::Change(e) => Some(e),
141            Self::Proposal(e) => Some(e),
142            _ => None,
143        }
144    }
145}
146
147impl<E, S, F, N> From<ConversionError<&'static str>> for InputSelectorError<E, S, F, N> {
148    fn from(value: ConversionError<&'static str>) -> Self {
149        InputSelectorError::Address(value)
150    }
151}
152
153impl<E, S, C, N> From<ChangeError<C, N>> for InputSelectorError<E, S, C, N> {
154    fn from(err: ChangeError<C, N>) -> Self {
155        InputSelectorError::Change(err)
156    }
157}
158
159impl<E, S, C, N> From<ProposalError> for InputSelectorError<E, S, C, N> {
160    fn from(err: ProposalError) -> Self {
161        InputSelectorError::Proposal(err)
162    }
163}
164
165/// A strategy for selecting transaction inputs and proposing transaction outputs.
166///
167/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
168/// that is, do not return inputs that cause fees to increase by an amount greater than the value
169/// of the input.
170pub trait InputSelector {
171    /// The type of errors that may be generated in input selection
172    type Error;
173
174    /// The type of data source that the input selector expects to access to obtain input notes.
175    /// This associated type permits input selectors that may use specialized knowledge of the
176    /// internals of a particular backing data store, if the generic API of `InputSource` does not
177    /// provide sufficiently fine-grained operations for a particular backing store to optimally
178    /// perform input selection.
179    type InputSource: InputSource;
180
181    /// Performs input selection and returns a proposal for transaction construction including
182    /// change and fee outputs.
183    ///
184    /// Implementations of this method should return inputs sufficient to satisfy the given
185    /// transaction request using a best-effort strategy to preserve user privacy, as follows:
186    /// * If it is possible to satisfy the specified transaction request by creating
187    ///   a fully-shielded transaction without requiring value to cross pool boundaries,
188    ///   return the inputs necessary to construct such a transaction; otherwise
189    /// * If it is possible to satisfy the transaction request by creating a fully-shielded
190    ///   transaction with some amounts crossing between shielded pools, return the inputs
191    ///   necessary.
192    ///
193    /// If insufficient funds are available to satisfy the required outputs for the shielding
194    /// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`].
195    ///
196    /// `spend_policy` controls which sources of funds the implementation may draw upon. It names
197    /// the shielded pools from which notes may be selected — the implementation must not select
198    /// notes from a pool the policy does not permit, returning
199    /// [`InputSelectorError::InsufficientFunds`] rather than crossing into a non-permitted pool —
200    /// and, behind the `transparent-inputs` feature flag, whether and from which addresses the
201    /// account's transparent UTXOs may additionally be spent. Spending transparent funds, or
202    /// combining notes across shielded pools, reduces privacy, so the caller must opt in
203    /// explicitly by naming the permitted sources.
204    #[allow(clippy::type_complexity)]
205    #[allow(clippy::too_many_arguments)]
206    fn propose_transaction<ParamsT, ChangeT>(
207        &self,
208        params: &ParamsT,
209        wallet_db: &Self::InputSource,
210        target_height: TargetHeight,
211        anchor_height: BlockHeight,
212        zip318: &PoolMigrationParams,
213        confirmations_policy: ConfirmationsPolicy,
214        account: <Self::InputSource as InputSource>::AccountId,
215        transaction_request: TransactionRequest,
216        change_strategy: &ChangeT,
217        spend_policy: &SpendPolicy,
218        proposed_version: Option<TxVersion>,
219    ) -> Result<
220        Proposal<<ChangeT as ChangeStrategy>::FeeRule, <Self::InputSource as InputSource>::NoteRef>,
221        InputSelectorError<
222            <Self::InputSource as InputSource>::Error,
223            Self::Error,
224            ChangeT::Error,
225            <Self::InputSource as InputSource>::NoteRef,
226        >,
227    >
228    where
229        ParamsT: consensus::Parameters,
230        ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
231}
232
233/// A strategy for selecting transaction inputs and proposing transaction outputs
234/// for shielding-only transactions (transactions which spend transparent UTXOs and
235/// send all transaction outputs to the wallet's shielded internal address(es)).
236#[cfg(feature = "transparent-inputs")]
237pub trait ShieldingSelector {
238    /// The type of errors that may be generated in input selection
239    type Error;
240    /// The type of data source that the input selector expects to access to obtain input
241    /// transparent UTXOs. This associated type permits input selectors that may use specialized
242    /// knowledge of the internals of a particular backing data store, if the generic API of
243    /// [`InputSource`] does not provide sufficiently fine-grained operations for a
244    /// particular backing store to optimally perform input selection.
245    type InputSource: InputSource;
246
247    /// Performs input selection and returns a proposal for the construction of a shielding
248    /// transaction.
249    ///
250    /// Implementations should return the maximum possible number of economically useful inputs
251    /// required to supply at least the requested value, choosing only inputs received at the
252    /// specified source addresses. If insufficient funds are available to satisfy the required
253    /// outputs for the shielding request, this operation must fail and return
254    /// [`InputSelectorError::InsufficientFunds`].
255    ///
256    /// The `output_filter` parameter controls which transparent outputs are eligible for
257    /// inclusion in the proposal. See [`CoinbaseFilter`] for details.
258    #[allow(clippy::type_complexity)]
259    #[allow(clippy::too_many_arguments)]
260    fn propose_shielding<ParamsT, ChangeT>(
261        &self,
262        params: &ParamsT,
263        wallet_db: &Self::InputSource,
264        change_strategy: &ChangeT,
265        shielding_threshold: Zatoshis,
266        source_addrs: &[TransparentAddress],
267        to_account: <Self::InputSource as InputSource>::AccountId,
268        target_height: TargetHeight,
269        anchor_height: BlockHeight,
270        zip318: &PoolMigrationParams,
271        confirmations_policy: ConfirmationsPolicy,
272        output_filter: CoinbaseFilter,
273    ) -> Result<
274        Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
275        InputSelectorError<
276            <Self::InputSource as InputSource>::Error,
277            Self::Error,
278            ChangeT::Error,
279            Infallible,
280        >,
281    >
282    where
283        ParamsT: consensus::Parameters,
284        ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
285
286    /// Performs input selection and returns a proposal for the construction of a transaction
287    /// that shields coinbase transparent outputs to an arbitrary shielded recipient.
288    ///
289    /// This method differs from [`Self::propose_shielding`] in the following ways:
290    ///
291    /// - Only coinbase transparent outputs are eligible for inclusion in the proposal. This
292    ///   restriction is hard-coded; callers cannot opt in to selecting non-coinbase outputs
293    ///   via this method. Coinbase outputs are uniquely suited to being sent to arbitrary
294    ///   shielded recipients because they have no prior transparent transaction graph that
295    ///   could be exposed to the recipient.
296    /// - The `to_address` argument specifies the destination of the shielded value. It must
297    ///   be a shielded address (Sapling, or a Unified Address with a shielded receiver). It
298    ///   may be an external address not belonging to any account we control.
299    /// - The resulting proposal carries an explicit ZIP-321 payment to `to_address` for the
300    ///   full available value (input total minus fee). **No change is produced**, in either
301    ///   the transparent or any shielded pool. This is a privacy invariant: producing a
302    ///   shielded change output would allow the recipient (or any chain observer) to learn
303    ///   the sender's total selected-coinbase value by summing the public transparent input
304    ///   values and subtracting the visible payment amount. Since this method targets the
305    ///   `z_shieldcoinbase`-style "sweep coinbase to a recipient" workflow, where the
306    ///   recipient may not belong to the sender's wallet, change is forbidden by design.
307    ///
308    /// Because no change is produced, this method takes a `fee_rule` directly rather than a
309    /// [`ChangeStrategy`]: there is no change to compute, and no per-account metadata is
310    /// required.
311    ///
312    /// The `memo` parameter is stored in the shielded output's memo field; it is always
313    /// permitted because a shielded payment is always present.
314    ///
315    /// The `limit` parameter, when `Some(n)`, caps the number of transparent inputs to at
316    /// most `n`, keeping the highest-value UTXOs (with a stable tiebreaker by outpoint).
317    /// `Some(0)` selects no inputs and will therefore return
318    /// [`InputSelectorError::InsufficientFunds`].
319    ///
320    /// If the total value of selected inputs (after any cap imposed by `limit`), minus the
321    /// fee, is less than `shielding_threshold`, this method returns
322    /// [`InputSelectorError::InsufficientFunds`].
323    #[allow(clippy::type_complexity)]
324    #[allow(clippy::too_many_arguments)]
325    fn propose_shielding_coinbase<ParamsT, FeeRuleT>(
326        &self,
327        params: &ParamsT,
328        wallet_db: &Self::InputSource,
329        fee_rule: &FeeRuleT,
330        shielding_threshold: Zatoshis,
331        source_addrs: &[TransparentAddress],
332        to_address: ZcashAddress,
333        memo: Option<MemoBytes>,
334        limit: Option<usize>,
335        target_height: TargetHeight,
336        anchor_height: BlockHeight,
337    ) -> Result<
338        Proposal<FeeRuleT, Infallible>,
339        InputSelectorError<
340            <Self::InputSource as InputSource>::Error,
341            Self::Error,
342            FeeRuleT::Error,
343            Infallible,
344        >,
345    >
346    where
347        ParamsT: consensus::Parameters,
348        FeeRuleT: FeeRule + Clone;
349}
350
351/// Errors that can occur as a consequence of greedy input selection.
352#[derive(Debug, Clone, PartialEq, Eq)]
353#[non_exhaustive]
354pub enum GreedyInputSelectorError {
355    /// An intermediate value overflowed or underflowed the valid monetary range.
356    Balance(BalanceError),
357    /// A unified address did not contain a supported receiver.
358    UnsupportedAddress(Box<UnifiedAddress>),
359    /// Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.
360    UnsupportedTexAddress,
361    /// The change strategy reported insufficient funds with a requirement that the inputs it
362    /// was given already cover, breaking the contract of [`ChangeStrategy::compute_balance`].
363    RequirementCovered {
364        /// The requirement the change strategy reported.
365        required: Zatoshis,
366        /// The value of the inputs it was given.
367        available: Zatoshis,
368    },
369    /// The change strategy reported as dust an input it was not given, or no input at all,
370    /// breaking the contract of [`ChangeStrategy::compute_balance`].
371    DustInputsNotGiven,
372    /// The data source returned a note it was asked to exclude, breaking the contract of
373    /// [`InputSource::select_spendable_notes`] or
374    /// [`InputSource::select_single_spendable_note`].
375    ExcludedNoteSelected,
376    /// The data source returned a note from a pool it was not asked to select from, breaking
377    /// the contract of [`InputSource::select_spendable_notes`] or
378    /// [`InputSource::select_single_spendable_note`].
379    UnrequestedPoolSelected,
380}
381
382impl fmt::Display for GreedyInputSelectorError {
383    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
384        match &self {
385            GreedyInputSelectorError::Balance(e) => write!(
386                f,
387                "A balance calculation violated amount validity bounds: {e:?}."
388            ),
389            GreedyInputSelectorError::UnsupportedAddress(_) => {
390                // we can't encode the UA to its string representation because we
391                // don't have network parameters here
392                write!(f, "Unified address contains no supported receivers.")
393            }
394            GreedyInputSelectorError::UnsupportedTexAddress => {
395                write!(
396                    f,
397                    "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature."
398                )
399            }
400            GreedyInputSelectorError::RequirementCovered {
401                required,
402                available,
403            } => write!(
404                f,
405                "The change strategy required {} of inputs worth {}, which already cover it.",
406                u64::from(*required),
407                u64::from(*available)
408            ),
409            GreedyInputSelectorError::DustInputsNotGiven => {
410                write!(
411                    f,
412                    "The change strategy reported as dust an input it was not given, or none at all."
413                )
414            }
415            GreedyInputSelectorError::ExcludedNoteSelected => {
416                write!(
417                    f,
418                    "The data source returned a note that input selection asked it to exclude."
419                )
420            }
421            GreedyInputSelectorError::UnrequestedPoolSelected => {
422                write!(
423                    f,
424                    "The data source returned a note from a pool that input selection did not ask for."
425                )
426            }
427        }
428    }
429}
430
431impl<DbErrT, ChangeErrT, N> From<GreedyInputSelectorError>
432    for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
433{
434    fn from(err: GreedyInputSelectorError) -> Self {
435        InputSelectorError::Selection(err)
436    }
437}
438
439impl<DbErrT, ChangeErrT, N> From<BalanceError>
440    for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
441{
442    fn from(err: BalanceError) -> Self {
443        InputSelectorError::Selection(GreedyInputSelectorError::Balance(err))
444    }
445}
446
447pub(crate) struct SaplingPayment(Zatoshis);
448
449#[cfg(test)]
450impl SaplingPayment {
451    pub(crate) fn new(amount: Zatoshis) -> Self {
452        SaplingPayment(amount)
453    }
454}
455
456impl sapling::OutputView for SaplingPayment {
457    fn value(&self) -> Zatoshis {
458        self.0
459    }
460}
461
462#[cfg(feature = "orchard")]
463pub(crate) struct OrchardPayment(Zatoshis);
464
465#[cfg(test)]
466#[cfg(feature = "orchard")]
467impl OrchardPayment {
468    pub(crate) fn new(amount: Zatoshis) -> Self {
469        OrchardPayment(amount)
470    }
471}
472
473#[cfg(feature = "orchard")]
474impl orchard_fees::OutputView for OrchardPayment {
475    fn value(&self) -> Zatoshis {
476        self.0
477    }
478}
479
480/// The default maximum fraction of a block's space, as an integer percentage, that a single
481/// shielding transaction's transparent inputs may occupy.
482#[cfg(feature = "transparent-inputs")]
483const DEFAULT_SHIELDING_BLOCK_SPACE_PERCENT: u32 = 10;
484
485/// A `BTreeSet` that is guaranteed to contain at least one element.
486///
487/// Non-emptiness is maintained by construction: every constructor requires at least one
488/// element, and no mutating operations are exposed.
489#[derive(Clone, Debug, PartialEq, Eq)]
490pub struct NonEmptyBTreeSet<T>(BTreeSet<T>);
491
492impl<T: Ord> NonEmptyBTreeSet<T> {
493    /// Constructs a set containing only the given element.
494    pub fn singleton(value: T) -> Self {
495        Self(BTreeSet::from_iter([value]))
496    }
497
498    /// Constructs a set containing the elements of the given non-empty list, collapsing
499    /// duplicates.
500    pub fn from_nonempty(values: NonEmpty<T>) -> Self {
501        Self(values.into_iter().collect())
502    }
503
504    /// Constructs a set from the given `BTreeSet`, or returns `None` if the set is empty.
505    pub fn from_set(values: BTreeSet<T>) -> Option<Self> {
506        (!values.is_empty()).then_some(Self(values))
507    }
508}
509
510impl<T> NonEmptyBTreeSet<T> {
511    /// Returns a reference to the wrapped set.
512    pub fn as_set(&self) -> &BTreeSet<T> {
513        &self.0
514    }
515
516    /// Returns an iterator over the elements of the set, in ascending order.
517    pub fn iter(&self) -> std::collections::btree_set::Iter<'_, T> {
518        self.0.iter()
519    }
520}
521
522/// The sources of funds an [`InputSelector`] is permitted to draw upon when satisfying a
523/// transaction request.
524///
525/// Crossing a shielded pool boundary reduces privacy, so it must be an explicit choice of the
526/// caller: the selector only spends notes from the shielded pools named in [`Self::shielded`], and
527/// only spends transparent UTXOs when a [`TransparentSpendPolicy`] is provided. When a single
528/// permitted pool cannot cover the request, the selector may combine the permitted pools (drawing
529/// on the legacy Orchard pool last); if no combination of permitted sources suffices it returns
530/// [`InputSelectorError::InsufficientFunds`] rather than reaching into a pool the caller did not
531/// permit.
532///
533/// The default permits every shielded pool present in the build and no transparent spending,
534/// preserving the historical fully-shielded behavior while letting a caller restrict the set to,
535/// for example, `{Orchard}` to forbid pool crossing.
536#[derive(Clone, Debug)]
537pub struct SpendPolicy {
538    shielded: BTreeSet<ShieldedPool>,
539    #[cfg(feature = "transparent-inputs")]
540    transparent: Option<TransparentSpendPolicy>,
541    locked_input_policy: LockedInputPolicy,
542    note_selection: NoteSelection,
543}
544
545/// How an [`InputSelector`] chooses among eligible notes when funding a payment.
546#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
547pub enum NoteSelection {
548    /// Accumulate the oldest eligible notes until the target value is covered.
549    #[default]
550    Accumulate,
551    /// Prefer funding from a SINGLE note — the oldest eligible note whose value alone covers the
552    /// target — falling back to accumulation when no such note exists.
553    ///
554    /// A ZIP 318 migration transfer spends exactly one note, so a canonical pool crossing is
555    /// achievable only under single-note funding; multi-note funding is not an error, but the
556    /// resulting proposal does not have the canonical shape.
557    PreferSingle,
558}
559
560impl Default for SpendPolicy {
561    fn default() -> Self {
562        Self::shielded_pools([
563            ShieldedPool::Sapling,
564            #[cfg(feature = "orchard")]
565            ShieldedPool::Orchard,
566            #[cfg(feature = "orchard")]
567            ShieldedPool::Ironwood,
568        ])
569    }
570}
571
572impl SpendPolicy {
573    /// Constructs a policy permitting selection from exactly the given shielded pools, with no
574    /// transparent spending.
575    pub fn shielded_pools(pools: impl IntoIterator<Item = ShieldedPool>) -> Self {
576        Self {
577            shielded: pools.into_iter().collect(),
578            #[cfg(feature = "transparent-inputs")]
579            transparent: None,
580            locked_input_policy: LockedInputPolicy::Exclude,
581            note_selection: NoteSelection::Accumulate,
582        }
583    }
584
585    /// Returns whether notes may be selected from the given shielded pool.
586    pub fn permits_shielded(&self, pool: ShieldedPool) -> bool {
587        self.shielded.contains(&pool)
588    }
589
590    /// Returns the set of shielded pools from which notes may be selected.
591    pub fn shielded(&self) -> &BTreeSet<ShieldedPool> {
592        &self.shielded
593    }
594
595    /// Adds a transparent spend policy, permitting transparent UTXOs to be spent as described.
596    #[cfg(feature = "transparent-inputs")]
597    pub fn with_transparent(mut self, transparent: TransparentSpendPolicy) -> Self {
598        self.transparent = Some(transparent);
599        self
600    }
601
602    /// Returns the transparent spend policy, or `None` if transparent UTXOs may not be spent.
603    #[cfg(feature = "transparent-inputs")]
604    pub fn transparent(&self) -> Option<&TransparentSpendPolicy> {
605        self.transparent.as_ref()
606    }
607
608    /// Sets how input selection treats locked outputs (default: `LockedInputPolicy::Exclude`).
609    pub fn with_locked_input_policy(mut self, policy: LockedInputPolicy) -> Self {
610        self.locked_input_policy = policy;
611        self
612    }
613
614    /// Returns the policy governing selection of locked outputs.
615    pub fn locked_input_policy(&self) -> &LockedInputPolicy {
616        &self.locked_input_policy
617    }
618
619    /// Sets how the selector chooses among eligible notes (default:
620    /// [`NoteSelection::Accumulate`]).
621    pub fn with_note_selection(mut self, note_selection: NoteSelection) -> Self {
622        self.note_selection = note_selection;
623        self
624    }
625
626    /// Returns how the selector chooses among eligible notes.
627    pub fn note_selection(&self) -> NoteSelection {
628        self.note_selection
629    }
630}
631
632/// The caller's choice of which coinbase transparent outputs a transparent spend may draw upon.
633///
634/// Consensus requires coinbase funds to be spent to a single shielded output with no change and
635/// without being mixed with non-coinbase inputs, so a transparent spend commits to one or the
636/// other rather than combining them.
637#[derive(Clone, Copy, Debug, PartialEq, Eq)]
638pub enum CoinbasePolicy {
639    /// Spend only coinbase transparent outputs.
640    OnlyCoinbase,
641    /// Spend only non-coinbase transparent outputs.
642    NonCoinbase,
643}
644
645#[cfg(feature = "transparent-inputs")]
646impl From<CoinbasePolicy> for CoinbaseFilter {
647    fn from(policy: CoinbasePolicy) -> Self {
648        match policy {
649            CoinbasePolicy::OnlyCoinbase => CoinbaseFilter::CoinbaseOnly,
650            CoinbasePolicy::NonCoinbase => CoinbaseFilter::NonCoinbaseOnly,
651        }
652    }
653}
654
655/// Specifies how transparent UTXOs may be spent in a transfer, when a [`SpendPolicy`] permits
656/// transparent spending.
657///
658/// Spending transparent funds links the chosen transparent addresses on-chain, reducing privacy,
659/// so a caller opts in by attaching this to a [`SpendPolicy`] via [`SpendPolicy::with_transparent`]
660/// (the absence of a policy — the default — spends no transparent UTXOs). The policy names the
661/// [`TransparentSource`] the UTXOs may be drawn from and, via [`CoinbasePolicy`], whether coinbase
662/// or non-coinbase outputs are spent.
663#[cfg(feature = "transparent-inputs")]
664#[derive(Clone, Debug)]
665pub struct TransparentSpendPolicy {
666    source: TransparentSource,
667    coinbase: CoinbasePolicy,
668}
669
670#[cfg(feature = "transparent-inputs")]
671impl TransparentSpendPolicy {
672    /// Spends non-coinbase UTXOs from arbitrary transparent receivers belonging to the account,
673    /// as needed to satisfy the request. The proposer chooses the addresses, potentially linking
674    /// them. (The legacy `ANY_TADDR` behavior.)
675    pub fn any_account_addr() -> Self {
676        Self {
677            source: TransparentSource::AnyAccountAddr,
678            coinbase: CoinbasePolicy::NonCoinbase,
679        }
680    }
681
682    /// Spends non-coinbase UTXOs only from the specified transparent addresses, intentionally
683    /// linking them.
684    pub fn from_addresses(taddrs: NonEmpty<TransparentAddress>) -> Self {
685        Self {
686            source: TransparentSource::FromAddresses(NonEmptyBTreeSet::from_nonempty(taddrs)),
687            coinbase: CoinbasePolicy::NonCoinbase,
688        }
689    }
690
691    /// Spends non-coinbase UTXOs only from a single transparent address.
692    pub fn from_one_address(taddr: TransparentAddress) -> Self {
693        Self {
694            source: TransparentSource::FromAddresses(NonEmptyBTreeSet::singleton(taddr)),
695            coinbase: CoinbasePolicy::NonCoinbase,
696        }
697    }
698
699    /// Returns a copy of this policy with the given coinbase policy in effect.
700    pub fn with_coinbase(mut self, coinbase: CoinbasePolicy) -> Self {
701        self.coinbase = coinbase;
702        self
703    }
704
705    /// Returns the transparent source from which UTXOs may be drawn.
706    pub fn source(&self) -> &TransparentSource {
707        &self.source
708    }
709
710    /// Returns the coinbase policy in effect for this transparent spend.
711    pub fn coinbase(&self) -> CoinbasePolicy {
712        self.coinbase
713    }
714
715    /// Returns the explicit list of transparent addresses UTXOs may be drawn from, or `None` if
716    /// any of the account's transparent receivers are permitted.
717    fn address_allow_list(&self) -> Option<Vec<TransparentAddress>> {
718        match &self.source {
719            TransparentSource::FromAddresses(addrs) => Some(addrs.iter().copied().collect()),
720            TransparentSource::AnyAccountAddr => None,
721        }
722    }
723}
724
725/// The transparent receivers a [`TransparentSpendPolicy`] may draw UTXOs from.
726#[cfg(feature = "transparent-inputs")]
727#[derive(Clone, Debug)]
728pub enum TransparentSource {
729    /// Any transparent receiver belonging to the account. The proposer chooses which, potentially
730    /// linking them.
731    AnyAccountAddr,
732    /// Only the specified transparent addresses.
733    FromAddresses(NonEmptyBTreeSet<TransparentAddress>),
734}
735
736/// An [`InputSelector`] implementation that uses a greedy strategy to select between available
737/// notes.
738///
739/// This implementation performs input selection using methods available via the
740/// [`InputSource`] interface.
741pub struct GreedyInputSelector<DbT> {
742    /// The maximum fraction of a block's space, as an integer percentage (0–100), that a
743    /// single transaction's transparent inputs may occupy. Bounds both shielding
744    /// transactions and the transparent gather performed for general (non-shielding)
745    /// transfers when the active [`TransparentSpendPolicy`] requires it.
746    #[cfg(feature = "transparent-inputs")]
747    shielding_block_space_percent: u32,
748    /// The policy governing whether the transparent UTXOs gathered by
749    /// [`ShieldingSelector::propose_shielding`] and
750    /// [`ShieldingSelector::propose_shielding_coinbase`] may be drawn from locked outputs.
751    /// Defaults to [`LockedInputPolicy::Exclude`]. Unlike [`SpendPolicy::locked_input_policy`],
752    /// which a caller supplies per call to [`InputSelector::propose_transaction`], shielding has
753    /// no per-call policy argument, so this is configured once on the selector instance via
754    /// [`Self::with_locked_input_policy`].
755    #[cfg(feature = "transparent-inputs")]
756    locked_input_policy: LockedInputPolicy,
757    _ds_type: PhantomData<DbT>,
758}
759
760impl<DbT> GreedyInputSelector<DbT> {
761    /// Constructs a new greedy input selector that uses the provided change strategy to determine
762    /// change values and fee amounts.
763    ///
764    /// The [`ChangeStrategy`] provided must produce exactly one ephemeral change value when
765    /// computing a transaction balance if an [`EphemeralBalance::Output`] value is provided for
766    /// its ephemeral balance, or the resulting [`GreedyInputSelector`] will return an error when
767    /// attempting to construct a transaction proposal that requires such an output.
768    ///
769    /// [`EphemeralBalance::Output`]: crate::fees::EphemeralBalance::Output
770    pub fn new() -> Self {
771        GreedyInputSelector {
772            #[cfg(feature = "transparent-inputs")]
773            shielding_block_space_percent: DEFAULT_SHIELDING_BLOCK_SPACE_PERCENT,
774            #[cfg(feature = "transparent-inputs")]
775            locked_input_policy: LockedInputPolicy::Exclude,
776            _ds_type: PhantomData,
777        }
778    }
779
780    /// Sets the maximum fraction of a block's space, as an integer percentage (0–100), that a
781    /// single transaction's transparent inputs may occupy.
782    ///
783    /// When shielding gathers more spendable transparent outputs than will fit within this
784    /// bound, the highest-value outputs are selected first and the remainder are left unspent,
785    /// to be consolidated by a subsequent shielding transaction. When a general (non-shielding)
786    /// transfer's transparent gather would otherwise require more inputs than fit within this
787    /// bound, the gather stops at the cap even if the requested value has not yet been
788    /// reached; the caller's input-selection loop surfaces this as an `InsufficientFunds`
789    /// error, the same as for any other value shortfall. Values above 100 are clamped to 100.
790    /// Defaults to 10.
791    #[cfg(feature = "transparent-inputs")]
792    pub fn with_shielding_block_space_percent(mut self, percent: u32) -> Self {
793        self.shielding_block_space_percent = percent.min(100);
794        self
795    }
796
797    /// Sets the policy governing whether shielding (via
798    /// [`ShieldingSelector::propose_shielding`] and
799    /// [`ShieldingSelector::propose_shielding_coinbase`]) may draw on transparent UTXOs locked
800    /// by one of the given policy's owners (default: [`LockedInputPolicy::Exclude`], which never
801    /// selects a locked output). This overrides a lock only for the purpose of *selecting through*
802    /// it during shielding; it does not release the lock.
803    #[cfg(feature = "transparent-inputs")]
804    pub fn with_locked_input_policy(mut self, policy: LockedInputPolicy) -> Self {
805        self.locked_input_policy = policy;
806        self
807    }
808
809    #[cfg(feature = "transparent-inputs")]
810    #[allow(clippy::too_many_arguments)]
811    #[allow(clippy::type_complexity)]
812    fn gather_transparent<ChangeT>(
813        &self,
814        wallet_db: &DbT,
815        target_height: TargetHeight,
816        confirmations_policy: ConfirmationsPolicy,
817        account: <DbT as InputSource>::AccountId,
818        // The list used to filter addresses.
819        // If `None`, any address is allowed.
820        address_allow_list: Option<&[TransparentAddress]>,
821        // Which coinbase outputs are eligible for selection.
822        coinbase: CoinbaseFilter,
823        transaction_request: &TransactionRequest,
824        amount_at_transparent_gather: &mut Zatoshis,
825        locked_input_policy: &LockedInputPolicy,
826    ) -> Result<
827        Vec<WalletTransparentOutput<()>>,
828        InputSelectorError<
829            <DbT as InputSource>::Error,
830            <GreedyInputSelector<DbT> as InputSelector>::Error,
831            <ChangeT as ChangeStrategy>::Error,
832            <DbT as InputSource>::NoteRef,
833        >,
834    >
835    where
836        DbT: InputSource,
837        ChangeT: ChangeStrategy<MetaSource = DbT>,
838    {
839        let max_money = Zatoshis::const_from_u64(zcash_protocol::value::MAX_MONEY);
840        let mut total_opt: Option<Zatoshis> = Some(Zatoshis::ZERO);
841        for payment in transaction_request.payments().values() {
842            let Some(payment_amount) = payment.amount() else {
843                total_opt = None;
844                break;
845            };
846            if let Some(t) = total_opt {
847                match t + payment_amount {
848                    Some(sum) => total_opt = Some(sum),
849                    None => {
850                        return Err(InputSelectorError::InsufficientFunds {
851                            available: Zatoshis::ZERO,
852                            required: max_money,
853                        });
854                    }
855                }
856            }
857        }
858        let (target_value, amount_at_gather) = match total_opt {
859            Some(z) => (TargetValue::AtLeast(z), z),
860            None => (
861                TargetValue::AllFunds(MaxSpendMode::MaxSpendable),
862                Zatoshis::ZERO,
863            ),
864        };
865        *amount_at_transparent_gather = amount_at_gather;
866        // Input selection honors the caller's `SpendPolicy::locked_input_policy`: by default
867        // (`Exclude`) a locked output is never drawn upon, since it belongs to another in-flight
868        // proposal and spending it would recreate the conflict that locking exists to prevent; the
869        // `PreferUnlocked`/`PreferLocked` overrides let a caller draw through a lock it recognizes
870        // (e.g. its own pool-migration PCZTs).
871        Ok(wallet_db
872            .select_spendable_transparent_outputs(
873                account,
874                target_height,
875                confirmations_policy,
876                coinbase,
877                address_allow_list,
878                target_value,
879                shielding_max_inputs(self.shielding_block_space_percent),
880                &StandardFeeRule::Zip317,
881                LockFilter::Policy(locked_input_policy),
882            )
883            .map_err(InputSelectorError::DataSource)?
884            .into_iter()
885            .map(|utxo| utxo.redact_account_data())
886            .collect::<Vec<_>>())
887    }
888}
889
890/// Returns the maximum number of transparent inputs that a single transaction may select,
891/// given the configured fraction of a block's space (as an integer percentage) that its
892/// inputs may occupy. Used to bound both shielding transactions and the transparent gather
893/// for general (non-shielding) transfers.
894#[cfg(feature = "transparent-inputs")]
895fn shielding_max_inputs(block_space_percent: u32) -> usize {
896    (MAX_BLOCK_BYTES.saturating_mul(block_space_percent as usize) / 100) / P2PKH_STANDARD_INPUT_SIZE
897}
898
899impl<DbT> Default for GreedyInputSelector<DbT> {
900    fn default() -> Self {
901        Self::new()
902    }
903}
904
905impl<DbT: InputSource> InputSelector for GreedyInputSelector<DbT> {
906    type Error = GreedyInputSelectorError;
907    type InputSource = DbT;
908
909    #[allow(clippy::type_complexity)]
910    fn propose_transaction<ParamsT, ChangeT>(
911        &self,
912        params: &ParamsT,
913        wallet_db: &Self::InputSource,
914        target_height: TargetHeight,
915        anchor_height: BlockHeight,
916        zip318: &PoolMigrationParams,
917        confirmations_policy: ConfirmationsPolicy,
918        account: <DbT as InputSource>::AccountId,
919        transaction_request: TransactionRequest,
920        change_strategy: &ChangeT,
921        spend_policy: &SpendPolicy,
922        proposed_version: Option<TxVersion>,
923    ) -> Result<
924        Proposal<<ChangeT as ChangeStrategy>::FeeRule, DbT::NoteRef>,
925        InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, DbT::NoteRef>,
926    >
927    where
928        ParamsT: consensus::Parameters,
929        Self::InputSource: InputSource,
930        ChangeT: ChangeStrategy<MetaSource = DbT>,
931    {
932        let (sapling_supported, orchard_supported) =
933            proposed_version.map_or(Ok((true, true)), |v| {
934                let branch_id =
935                    consensus::BranchId::for_height(params, BlockHeight::from(target_height));
936                if v.valid_in_branch(branch_id) {
937                    Ok((
938                        v.has_sapling(),
939                        cfg!(feature = "orchard") && v.has_orchard(),
940                    ))
941                } else {
942                    Err(ProposalError::IncompatibleTxVersion(branch_id))
943                }
944            })?;
945        // Without the `orchard` feature there are no Orchard-family pools to select from, so
946        // `orchard_supported` (always false) is only referenced by Orchard-gated code.
947        #[cfg(not(feature = "orchard"))]
948        let _ = orchard_supported;
949
950        let mut transparent_outputs = vec![];
951        let mut sapling_outputs = vec![];
952        #[cfg(feature = "orchard")]
953        let mut orchard_outputs = vec![];
954        let mut payment_pools = BTreeMap::new();
955
956        // In a ZIP 320 pair, tr0 refers to the first transaction request that
957        // collects shielded value and sends it to an ephemeral address, and tr1
958        // refers to the second transaction request that pays the TEX addresses.
959        #[cfg(feature = "transparent-inputs")]
960        let mut tr1_transparent_outputs = vec![];
961        #[cfg(feature = "transparent-inputs")]
962        let mut tr1_payments = vec![];
963        #[cfg(feature = "transparent-inputs")]
964        let mut tr1_payment_pools = BTreeMap::new();
965        // This balance value is just used for overflow checking; the actual value of ephemeral
966        // outputs will be computed from the constructed `tr1_transparent_outputs` value
967        // constructed below.
968        #[cfg(feature = "transparent-inputs")]
969        let mut total_ephemeral = Zatoshis::ZERO;
970
971        for (idx, payment) in transaction_request.payments() {
972            let payment_amount = payment
973                .amount()
974                .ok_or(ProposalError::PaymentAmountMissing(*idx))?;
975            let recipient_address: Address = payment
976                .recipient_address()
977                .clone()
978                .convert_if_network(params.network_type())?;
979
980            match recipient_address {
981                Address::Transparent(addr) => {
982                    payment_pools.insert(*idx, PoolType::TRANSPARENT);
983                    transparent_outputs.push(TxOut::new(payment_amount, addr.script().into()));
984                }
985                #[cfg(feature = "transparent-inputs")]
986                Address::Tex(data) => {
987                    let p2pkh_addr = TransparentAddress::PublicKeyHash(data);
988
989                    tr1_payment_pools.insert(*idx, PoolType::TRANSPARENT);
990                    tr1_transparent_outputs
991                        .push(TxOut::new(payment_amount, p2pkh_addr.script().into()));
992                    tr1_payments.push(
993                        Payment::new(
994                            payment.recipient_address().clone(),
995                            payment.amount(),
996                            None,
997                            payment.label().cloned(),
998                            payment.message().cloned(),
999                            payment.other_params().to_vec(),
1000                        )
1001                        .expect("cannot fail because memo is None and amount is nonzero"),
1002                    );
1003                    total_ephemeral = (total_ephemeral + payment_amount)
1004                        .ok_or(GreedyInputSelectorError::Balance(BalanceError::Overflow))?;
1005                }
1006                #[cfg(not(feature = "transparent-inputs"))]
1007                Address::Tex(_) => {
1008                    return Err(InputSelectorError::Selection(
1009                        GreedyInputSelectorError::UnsupportedTexAddress,
1010                    ));
1011                }
1012                Address::Sapling(_) => {
1013                    payment_pools.insert(*idx, PoolType::SAPLING);
1014                    sapling_outputs.push(SaplingPayment(payment_amount));
1015                }
1016                Address::Unified(addr) => {
1017                    #[cfg(feature = "orchard")]
1018                    if addr.has_orchard() && orchard_supported {
1019                        // Represent an Orchard-receiver payment as an Ironwood-pool output once
1020                        // Ironwood is active (its value is accounted to the Ironwood bundle
1021                        // below), and as an Orchard-pool output otherwise.
1022                        let pool = if ironwood_active_at(params, target_height) {
1023                            // After NU6.3 the Orchard turnstile (a consensus rule) forbids adding
1024                            // value to the Orchard pool, so the payment must be delivered through
1025                            // the Ironwood bundle, which only a version 6 transaction carries. If a
1026                            // transaction version was explicitly requested that cannot carry an
1027                            // Ironwood bundle, reject the proposal here rather than constructing one
1028                            // that could only fail at build time.
1029                            if let Some(v) = proposed_version
1030                                && !v.has_ironwood()
1031                            {
1032                                return Err(
1033                                    ProposalError::OrchardReceiverRequiresIronwood(v).into()
1034                                );
1035                            }
1036                            PoolType::IRONWOOD
1037                        } else {
1038                            PoolType::ORCHARD
1039                        };
1040                        payment_pools.insert(*idx, pool);
1041                        orchard_outputs.push(OrchardPayment(payment_amount));
1042                        continue;
1043                    }
1044
1045                    if addr.has_sapling() && sapling_supported {
1046                        payment_pools.insert(*idx, PoolType::SAPLING);
1047                        sapling_outputs.push(SaplingPayment(payment_amount));
1048                        continue;
1049                    }
1050
1051                    if let Some(addr) = addr.transparent() {
1052                        payment_pools.insert(*idx, PoolType::TRANSPARENT);
1053                        transparent_outputs.push(TxOut::new(payment_amount, addr.script().into()));
1054                        continue;
1055                    }
1056
1057                    return Err(InputSelectorError::Selection(
1058                        GreedyInputSelectorError::UnsupportedAddress(addr),
1059                    ));
1060                }
1061            }
1062        }
1063
1064        #[cfg(not(feature = "transparent-inputs"))]
1065        let transparent_inputs = vec![];
1066        #[cfg(feature = "transparent-inputs")]
1067        let mut amount_at_transparent_gather = Zatoshis::ZERO;
1068        #[cfg(feature = "transparent-inputs")]
1069        let mut transparent_inputs = match spend_policy.transparent() {
1070            None => {
1071                // No transparent spending is permitted; skip the gather entirely.
1072                Vec::new()
1073            }
1074            Some(transparent) => {
1075                let address_allow_list = transparent.address_allow_list();
1076                self.gather_transparent::<ChangeT>(
1077                    wallet_db,
1078                    target_height,
1079                    confirmations_policy,
1080                    account,
1081                    address_allow_list.as_deref(),
1082                    transparent.coinbase().into(),
1083                    &transaction_request,
1084                    &mut amount_at_transparent_gather,
1085                    spend_policy.locked_input_policy(),
1086                )?
1087            }
1088        };
1089        // Outpoints of gathered transparent inputs that the change strategy has identified as
1090        // dust. Accumulated across loop iterations so that a re-gather (triggered by
1091        // `ChangeError::InsufficientFunds`, below) does not re-introduce previously pruned
1092        // outputs.
1093        #[cfg(feature = "transparent-inputs")]
1094        let mut transparent_dust: BTreeSet<OutPoint> = BTreeSet::new();
1095
1096        let mut shielded_inputs = ReceivedNotes::empty();
1097        let mut amount_required = Zatoshis::ZERO;
1098        let mut exclude: Vec<DbT::NoteRef> = vec![];
1099
1100        // The single pool-preference order that governs both which pools notes are
1101        // selected from and which of the selected notes are spent: the pool matching
1102        // the payment's outputs comes first, and later pools are drawn upon only when
1103        // the earlier ones cannot cover the required amount, so that pool crossing is
1104        // minimized. For a payment to an Orchard receiver the Orchard-family pools
1105        // lead: once NU6.3 is active such payments are constructed in the Ironwood
1106        // bundle — moving their value into the Ironwood pool — so Ironwood is
1107        // preferred, with the legacy Orchard pool last within the family.
1108        #[cfg(feature = "orchard")]
1109        let mut pool_preference = selectable_pool_preference(
1110            params,
1111            target_height,
1112            sapling_supported,
1113            orchard_supported,
1114            !orchard_outputs.is_empty(),
1115        );
1116        #[cfg(not(feature = "orchard"))]
1117        let mut pool_preference = {
1118            let mut pools = vec![];
1119            if sapling_supported {
1120                pools.push(ShieldedPool::Sapling);
1121            }
1122            pools
1123        };
1124
1125        // Restrict selection to the shielded pools the caller's spend policy permits. Crossing a
1126        // pool boundary is privacy-breaking, so a pool the policy does not name is never drawn
1127        // upon — not even as a fallback when the permitted pools cannot cover the request, in
1128        // which case input selection reports `InsufficientFunds`.
1129        pool_preference.retain(|pool| spend_policy.permits_shielded(*pool));
1130
1131        // Every pass that continues advances a bounded measure. After `InsufficientFunds` the
1132        // requirement exceeds the value the balance was computed over, so the next selection,
1133        // trimmed to the pools in use, is worth more. After `DustInputs` a note the change
1134        // strategy was given is excluded, which happens at most once per note since the data
1135        // source never returns an excluded note, or a transparent input is pruned. Transparent
1136        // inputs are re-gathered only at a higher bound. The arms return an error when the
1137        // change strategy reports none of these, since the next pass would repeat this one,
1138        // and the check at the bottom returns `InsufficientFunds` when the data source offers
1139        // no more value toward the requirement.
1140        loop {
1141            // `balance_input_value` is the value of the selected notes in the pools in use: the
1142            // value the balance below is computed over, and the one the check at the bottom
1143            // measures the next selection against.
1144            #[cfg(not(feature = "orchard"))]
1145            let sapling_bundle_required = true;
1146            #[cfg(not(feature = "orchard"))]
1147            let balance_input_value = shielded_inputs.total_value()?;
1148            #[cfg(feature = "orchard")]
1149            let (
1150                sapling_bundle_required,
1151                orchard_bundle_required,
1152                ironwood_bundle_required,
1153                balance_input_value,
1154            ) = {
1155                // Trim the selected notes to the pools that are actually needed: the first
1156                // pool (in `pool_preference` order) whose selected notes cover the required
1157                // amount is spent alone; otherwise pools are accumulated in preference order
1158                // until the running total covers the amount, or all pools are in use.
1159                let pool_values = [
1160                    (ShieldedPool::Sapling, shielded_inputs.sapling_value()?),
1161                    (ShieldedPool::Orchard, shielded_inputs.orchard_value()?),
1162                    (ShieldedPool::Ironwood, shielded_inputs.ironwood_value()?),
1163                ];
1164                let value_of = |pool: ShieldedPool| {
1165                    pool_values
1166                        .iter()
1167                        .find(|(p, _)| *p == pool)
1168                        .map(|(_, v)| *v)
1169                        .expect("all shielded pools are present in pool_values")
1170                };
1171
1172                let (use_pools, trimmed_value) = if let Some(single) = pool_preference
1173                    .iter()
1174                    .find(|p| value_of(**p) >= amount_required)
1175                {
1176                    (vec![*single], value_of(*single))
1177                } else {
1178                    let mut running = Zatoshis::ZERO;
1179                    let mut used = vec![];
1180                    for pool in &pool_preference {
1181                        if running >= amount_required {
1182                            break;
1183                        }
1184                        running = (running + value_of(*pool))
1185                            .ok_or(GreedyInputSelectorError::Balance(BalanceError::Overflow))?;
1186                        used.push(*pool);
1187                    }
1188                    (used, running)
1189                };
1190
1191                (
1192                    use_pools.contains(&ShieldedPool::Sapling),
1193                    use_pools.contains(&ShieldedPool::Orchard),
1194                    use_pools.contains(&ShieldedPool::Ironwood),
1195                    trimmed_value,
1196                )
1197            };
1198
1199            let sapling_inputs = if sapling_bundle_required {
1200                shielded_inputs
1201                    .sapling()
1202                    .iter()
1203                    .map(|i| (*i.internal_note_id(), i.note().value()))
1204                    .collect()
1205            } else {
1206                vec![]
1207            };
1208
1209            #[cfg(feature = "orchard")]
1210            let orchard_inputs = if orchard_bundle_required {
1211                shielded_inputs
1212                    .orchard()
1213                    .iter()
1214                    .map(|i| (*i.internal_note_id(), i.note().value()))
1215                    .collect()
1216            } else {
1217                vec![]
1218            };
1219
1220            // Ironwood inputs are attributed to the Ironwood bundle for action-count and fee
1221            // purposes.
1222            #[cfg(feature = "orchard")]
1223            let ironwood_inputs = if ironwood_bundle_required {
1224                shielded_inputs
1225                    .ironwood()
1226                    .iter()
1227                    .map(|i| (*i.internal_note_id(), i.note().value()))
1228                    .collect()
1229            } else {
1230                vec![]
1231            };
1232
1233            let selected_input_ids = sapling_inputs.iter().map(|(id, _)| id);
1234            #[cfg(feature = "orchard")]
1235            let selected_input_ids =
1236                selected_input_ids.chain(orchard_inputs.iter().map(|(id, _)| id));
1237            #[cfg(feature = "orchard")]
1238            let selected_input_ids =
1239                selected_input_ids.chain(ironwood_inputs.iter().map(|(id, _)| id));
1240
1241            let selected_input_ids = selected_input_ids.cloned().collect::<Vec<_>>();
1242
1243            let wallet_meta = change_strategy
1244                .fetch_wallet_meta(wallet_db, account, target_height, &selected_input_ids)
1245                .map_err(InputSelectorError::DataSource)?;
1246
1247            #[cfg(not(feature = "transparent-inputs"))]
1248            let ephemeral_output_value = None;
1249
1250            #[cfg(feature = "transparent-inputs")]
1251            let (ephemeral_output_value, tr1_balance_opt) = {
1252                if tr1_transparent_outputs.is_empty() {
1253                    (None, None)
1254                } else {
1255                    // The ephemeral input going into transaction 1 must be able to pay that
1256                    // transaction's fee, as well as the TEX address payments.
1257
1258                    // Transaction 1 carries no shielded spends or outputs, but the change
1259                    // strategy may still model hypothetical shielded change against these
1260                    // views, so they carry the bundle versions in effect at the target
1261                    // height rather than a fixed default.
1262                    #[cfg(feature = "orchard")]
1263                    let empty_orchard_view = (
1264                        orchard_bundle_version_for_height(params, target_height),
1265                        &[] as &[Infallible],
1266                        &[] as &[Infallible],
1267                    );
1268                    #[cfg(feature = "orchard")]
1269                    let empty_ironwood_view = (
1270                        ironwood_bundle_version_for_height(params, target_height),
1271                        &[] as &[Infallible],
1272                        &[] as &[Infallible],
1273                    );
1274
1275                    // First compute the required total with an additional zero input,
1276                    // catching the `InsufficientFunds` error to obtain the required amount
1277                    // given the provided change strategy. Ignore the change memo in order
1278                    // to avoid adding a change output.
1279                    let tr1_required_input_value = match change_strategy
1280                        .compute_balance::<_, DbT::NoteRef>(
1281                            params,
1282                            target_height,
1283                            anchor_height,
1284                            zip318,
1285                            &[] as &[WalletTransparentOutput<<DbT as InputSource>::AccountId>],
1286                            &tr1_transparent_outputs,
1287                            &sapling::EmptyBundleView,
1288                            #[cfg(feature = "orchard")]
1289                            &empty_orchard_view,
1290                            #[cfg(feature = "orchard")]
1291                            &empty_ironwood_view,
1292                            Some(EphemeralBalance::Input(Zatoshis::ZERO)),
1293                            &wallet_meta,
1294                        ) {
1295                        Err(ChangeError::InsufficientFunds { required, .. })
1296                            if required.is_positive() =>
1297                        {
1298                            required
1299                        }
1300                        // The zero-valued input covers no requirement but zero.
1301                        Err(ChangeError::InsufficientFunds { required, .. }) => {
1302                            return Err(InputSelectorError::Selection(
1303                                GreedyInputSelectorError::RequirementCovered {
1304                                    required,
1305                                    available: Zatoshis::ZERO,
1306                                },
1307                            ));
1308                        }
1309                        // No input was supplied, so none can be named as dust.
1310                        Err(ChangeError::DustInputs { .. }) => {
1311                            return Err(InputSelectorError::Selection(
1312                                GreedyInputSelectorError::DustInputsNotGiven,
1313                            ));
1314                        }
1315                        Err(other) => return Err(InputSelectorError::Change(other)),
1316                        Ok(_) => Zatoshis::ZERO, // shouldn't happen
1317                    };
1318
1319                    // Now recompute to obtain the `TransactionBalance` and verify that it
1320                    // fully accounts for the required fees.
1321                    let tr1_balance = change_strategy.compute_balance::<_, DbT::NoteRef>(
1322                        params,
1323                        target_height,
1324                        anchor_height,
1325                        zip318,
1326                        &[] as &[WalletTransparentOutput<<DbT as InputSource>::AccountId>],
1327                        &tr1_transparent_outputs,
1328                        &sapling::EmptyBundleView,
1329                        #[cfg(feature = "orchard")]
1330                        &empty_orchard_view,
1331                        #[cfg(feature = "orchard")]
1332                        &empty_ironwood_view,
1333                        Some(EphemeralBalance::Input(tr1_required_input_value)),
1334                        &wallet_meta,
1335                    )?;
1336                    assert_eq!(tr1_balance.total(), tr1_balance.fee_required());
1337
1338                    (Some(tr1_required_input_value), Some(tr1_balance))
1339                }
1340            };
1341
1342            // The Orchard bundle keeps the Orchard (version 2) spends; its outputs move to the
1343            // Ironwood bundle when routing is active. The Ironwood bundle takes the Ironwood
1344            // (version 3) spends, and its outputs when routing is active. Attributing each pool's
1345            // spends to its own bundle keeps the action counts (and hence the fee) matching the
1346            // transaction the builder produces.
1347            #[cfg(feature = "orchard")]
1348            let orchard_view = (
1349                orchard_bundle_version_for_height(params, target_height),
1350                &orchard_inputs[..],
1351                if ironwood_active_at(params, target_height) {
1352                    &[]
1353                } else {
1354                    &orchard_outputs[..]
1355                },
1356            );
1357            #[cfg(feature = "orchard")]
1358            let ironwood_view = (
1359                ironwood_bundle_version_for_height(params, target_height),
1360                &ironwood_inputs[..],
1361                if ironwood_active_at(params, target_height) {
1362                    &orchard_outputs[..]
1363                } else {
1364                    &[]
1365                },
1366            );
1367
1368            // Tracks whether this iteration's error handling changed the transparent input
1369            // set, either by re-gathering with a corrected value bound (`InsufficientFunds`)
1370            // or by pruning dust (`DustInputs`). A changed transparent input set is a valid
1371            // form of progress in its own right (distinct from the shielded-note progress
1372            // tracked by `balance_input_value`/`new_available` below): without this, an account
1373            // with no spendable shielded notes at all (or none beyond what's already
1374            // excluded) would spuriously report `InsufficientFunds` on the very next check
1375            // below, even though the changed transparent input set might already be
1376            // sufficient to satisfy the request on the next iteration. Termination is
1377            // preserved: `amount_at_transparent_gather` increases strictly across
1378            // re-gathers, and each outpoint can be pruned as dust at most once (pruned
1379            // outpoints accumulate in `transparent_dust` and are never re-gathered).
1380            #[cfg(not(feature = "transparent-inputs"))]
1381            let transparent_inputs_changed = false;
1382            #[cfg(feature = "transparent-inputs")]
1383            let mut transparent_inputs_changed = false;
1384            // Set when the change strategy names as dust a shielded note it was given. The next
1385            // selection lacks that note and may be worth less than this one, yet it has not
1386            // been balanced, so this too is progress.
1387            let mut shielded_dust_excluded = false;
1388
1389            // In the ZIP 320 case, this is the balance for transaction 0, taking into account
1390            // the ephemeral output.
1391            let tr0_balance = change_strategy.compute_balance(
1392                params,
1393                target_height,
1394                anchor_height,
1395                zip318,
1396                &transparent_inputs,
1397                &transparent_outputs,
1398                &(
1399                    ::sapling::builder::BundleType::DEFAULT,
1400                    &sapling_inputs[..],
1401                    &sapling_outputs[..],
1402                ),
1403                #[cfg(feature = "orchard")]
1404                &orchard_view,
1405                #[cfg(feature = "orchard")]
1406                &ironwood_view,
1407                ephemeral_output_value.map(EphemeralBalance::Output),
1408                &wallet_meta,
1409            );
1410
1411            match tr0_balance {
1412                Ok(tr0_balance) => {
1413                    // At this point, we have enough input value to pay for everything, so we
1414                    // return here.
1415                    let shielded_inputs =
1416                        NonEmpty::from_vec(shielded_inputs.into_vec(&SimpleNoteRetention {
1417                            sapling: sapling_bundle_required,
1418                            #[cfg(feature = "orchard")]
1419                            orchard: orchard_bundle_required,
1420                            #[cfg(feature = "orchard")]
1421                            ironwood: ironwood_bundle_required,
1422                        }))
1423                        .map(ShieldedInputs::from_parts);
1424
1425                    return build_proposal(
1426                        change_strategy.fee_rule(),
1427                        tr0_balance,
1428                        target_height,
1429                        anchor_height,
1430                        confirmations_policy,
1431                        shielded_inputs,
1432                        transparent_inputs,
1433                        transaction_request,
1434                        payment_pools,
1435                        #[cfg(feature = "orchard")]
1436                        ironwood_active_at(params, target_height),
1437                        #[cfg(feature = "transparent-inputs")]
1438                        ephemeral_output_value.zip(tr1_balance_opt).map(
1439                            |(ephemeral_output_value, tr1_balance)| EphemeralStepConfig {
1440                                ephemeral_output_value,
1441                                tr1_balance,
1442                                tr1_payments,
1443                                tr1_payment_pools,
1444                            },
1445                        ),
1446                    )
1447                    .map_err(InputSelectorError::Proposal);
1448                }
1449                Err(ChangeError::DustInputs {
1450                    #[cfg(feature = "transparent-inputs")]
1451                    transparent,
1452                    mut sapling,
1453                    #[cfg(feature = "orchard")]
1454                    mut orchard,
1455                    #[cfg(feature = "orchard")]
1456                    mut ironwood,
1457                    ..
1458                }) => {
1459                    // A named note the change strategy was given leaves the next selection once
1460                    // excluded; one it was not given breaks the strategy's contract.
1461                    let named = sapling.iter();
1462                    #[cfg(feature = "orchard")]
1463                    let named = named.chain(orchard.iter()).chain(ironwood.iter());
1464                    let mut other_named = false;
1465                    for note in named {
1466                        if selected_input_ids.contains(note) {
1467                            shielded_dust_excluded = true;
1468                        } else {
1469                            other_named = true;
1470                        }
1471                    }
1472                    exclude.append(&mut sapling);
1473                    #[cfg(feature = "orchard")]
1474                    exclude.append(&mut orchard);
1475                    #[cfg(feature = "orchard")]
1476                    exclude.append(&mut ironwood);
1477                    #[cfg(feature = "transparent-inputs")]
1478                    {
1479                        let len_before = transparent_inputs.len();
1480                        other_named |= transparent.iter().any(|outpoint| {
1481                            !transparent_inputs
1482                                .iter()
1483                                .any(|input| input.outpoint() == outpoint)
1484                        });
1485                        transparent_dust.extend(transparent);
1486                        transparent_inputs.retain(|i| !transparent_dust.contains(i.outpoint()));
1487                        // Pruning dust changes the balance computation, so give the loop a
1488                        // chance to re-evaluate the pruned set before concluding that funds
1489                        // are insufficient.
1490                        if transparent_inputs.len() != len_before {
1491                            transparent_inputs_changed = true;
1492                        }
1493                    }
1494                    // With no given input named, the next selection would be the one just
1495                    // balanced.
1496                    if other_named || (!shielded_dust_excluded && !transparent_inputs_changed) {
1497                        return Err(InputSelectorError::Selection(
1498                            GreedyInputSelectorError::DustInputsNotGiven,
1499                        ));
1500                    }
1501                }
1502                Err(ChangeError::InsufficientFunds { required, .. }) => {
1503                    // A requirement the inputs already cover cannot be met by selecting more.
1504                    let available = available_value(balance_input_value, &transparent_inputs)?;
1505                    if required <= available {
1506                        return Err(InputSelectorError::Selection(
1507                            GreedyInputSelectorError::RequirementCovered {
1508                                required,
1509                                available,
1510                            },
1511                        ));
1512                    }
1513                    amount_required = required;
1514                    // The initial transparent-input gather was bounded by the payouts
1515                    // alone, but `required` includes the fee. If the bound was too
1516                    // low, re-gather transparents with the corrected value as a
1517                    // defensive fallback. The common case (fee estimate was close) is
1518                    // a no-op.
1519                    #[cfg(feature = "transparent-inputs")]
1520                    if let Some(transparent) = spend_policy.transparent()
1521                        && required > amount_at_transparent_gather
1522                    {
1523                        let address_allow_list = transparent.address_allow_list();
1524                        // Honor the caller's `SpendPolicy::locked_input_policy`, as at the
1525                        // initial gather above.
1526                        transparent_inputs = wallet_db
1527                            .select_spendable_transparent_outputs(
1528                                account,
1529                                target_height,
1530                                confirmations_policy,
1531                                transparent.coinbase().into(),
1532                                address_allow_list.as_deref(),
1533                                TargetValue::AtLeast(required),
1534                                shielding_max_inputs(self.shielding_block_space_percent),
1535                                &StandardFeeRule::Zip317,
1536                                LockFilter::Policy(spend_policy.locked_input_policy()),
1537                            )
1538                            .map_err(InputSelectorError::DataSource)?
1539                            .into_iter()
1540                            // Do not re-introduce outputs previously pruned as dust; the
1541                            // value they would contribute is (approximately) consumed by
1542                            // their own fee cost, so their absence does not meaningfully
1543                            // reduce the gathered value.
1544                            .filter(|utxo| !transparent_dust.contains(utxo.outpoint()))
1545                            .map(|utxo| utxo.redact_account_data())
1546                            .collect::<Vec<_>>();
1547                        amount_at_transparent_gather = required;
1548                        transparent_inputs_changed = true;
1549                    }
1550                }
1551                Err(other) => return Err(InputSelectorError::Change(other)),
1552            }
1553
1554            // Candidate notes are selected from the pools of `pool_preference` — the
1555            // same order the pool-usage trimming at the top of the loop applies — so
1556            // the notes offered for spending and the notes actually spent are governed
1557            // by one policy: pool crossing is minimized, and a payment to an Orchard
1558            // receiver draws on the pool its output is constructed in (the Ironwood
1559            // pool once NU6.3 is active).
1560            // Input selection honors the caller's `SpendPolicy::locked_input_policy`: by default
1561            // (`Exclude`) a locked output is never drawn upon, since it belongs to another
1562            // in-flight proposal and spending it would recreate the conflict that locking exists
1563            // to prevent; the `PreferUnlocked`/`PreferLocked` overrides let a caller draw through
1564            // a lock it recognizes (e.g. its own pool-migration PCZTs).
1565            //
1566            // Under `NoteSelection::PreferSingle`, the single oldest note covering the required
1567            // amount alone is tried first; when no pool holds one, selection falls back to
1568            // ordinary accumulation, which still funds the payment but cannot produce the
1569            // single-input shape the caller preferred.
1570            let single_note = match spend_policy.note_selection() {
1571                NoteSelection::PreferSingle => Some(
1572                    wallet_db
1573                        .select_single_spendable_note(
1574                            account,
1575                            amount_required,
1576                            &pool_preference,
1577                            target_height,
1578                            confirmations_policy,
1579                            &exclude,
1580                            LockFilter::Policy(spend_policy.locked_input_policy()),
1581                        )
1582                        .map_err(InputSelectorError::DataSource)?,
1583                )
1584                .filter(|notes| !notes.is_empty()),
1585                NoteSelection::Accumulate => None,
1586            };
1587            shielded_inputs = match single_note {
1588                Some(single) => single,
1589                None => wallet_db
1590                    .select_spendable_notes(
1591                        account,
1592                        TargetValue::AtLeast(amount_required),
1593                        &pool_preference,
1594                        target_height,
1595                        confirmations_policy,
1596                        &exclude,
1597                        LockFilter::Policy(spend_policy.locked_input_policy()),
1598                    )
1599                    .map_err(InputSelectorError::DataSource)?,
1600            };
1601
1602            // The loop rests on the data source selecting from the pools asked for and
1603            // withholding excluded notes: a note outside those pools is never balanced yet
1604            // counts below, and a note named as dust and returned again would be named again.
1605            check_selection(&shielded_inputs, &pool_preference, &exclude)?;
1606
1607            // Loop again only when selection offers more value than the balance above was
1608            // computed over, a given shielded note was excluded, or the transparent input set
1609            // changed this iteration.
1610            let new_available = shielded_inputs.total_value()?;
1611            if new_available <= balance_input_value
1612                && !transparent_inputs_changed
1613                && !shielded_dust_excluded
1614            {
1615                return Err(InputSelectorError::InsufficientFunds {
1616                    required: amount_required,
1617                    available: available_value(balance_input_value, &transparent_inputs)?,
1618                });
1619            }
1620        }
1621    }
1622}
1623
1624/// Returns the value input selection reports as available: the shielded value a balance was
1625/// computed over plus the transparent inputs it was given.
1626fn available_value<AccountId>(
1627    shielded: Zatoshis,
1628    transparent_inputs: &[WalletTransparentOutput<AccountId>],
1629) -> Result<Zatoshis, BalanceError> {
1630    let transparent = transparent_inputs
1631        .iter()
1632        .map(|input| input.value())
1633        .sum::<Option<Zatoshis>>()
1634        .ok_or(BalanceError::Overflow)?;
1635    (shielded + transparent).ok_or(BalanceError::Overflow)
1636}
1637
1638/// Checks a selection against the contract of [`InputSource`]: it holds no note from a pool
1639/// outside `sources` and no note in `exclude`.
1640fn check_selection<NoteRef: PartialEq>(
1641    notes: &ReceivedNotes<NoteRef>,
1642    sources: &[ShieldedPool],
1643    exclude: &[NoteRef],
1644) -> Result<(), GreedyInputSelectorError> {
1645    fn check_pool<NoteRef: PartialEq, NoteT>(
1646        notes: &[ReceivedNote<NoteRef, NoteT>],
1647        pool: ShieldedPool,
1648        sources: &[ShieldedPool],
1649        exclude: &[NoteRef],
1650    ) -> Result<(), GreedyInputSelectorError> {
1651        if !notes.is_empty() && !sources.contains(&pool) {
1652            return Err(GreedyInputSelectorError::UnrequestedPoolSelected);
1653        }
1654        if notes
1655            .iter()
1656            .any(|note| exclude.contains(note.internal_note_id()))
1657        {
1658            return Err(GreedyInputSelectorError::ExcludedNoteSelected);
1659        }
1660        Ok(())
1661    }
1662    check_pool(notes.sapling(), ShieldedPool::Sapling, sources, exclude)?;
1663    #[cfg(feature = "orchard")]
1664    {
1665        check_pool(notes.orchard(), ShieldedPool::Orchard, sources, exclude)?;
1666        check_pool(notes.ironwood(), ShieldedPool::Ironwood, sources, exclude)?;
1667    }
1668    Ok(())
1669}
1670
1671/// Returns the shielded pools from which the greedy input selector may spend at the
1672/// given target height, in preference order.
1673///
1674/// The pool family matching the payment's outputs comes first, so that single-pool
1675/// coverage avoids unnecessary pool crossings: for an Orchard-family payment this is
1676/// Ironwood (when active) and then Orchard — an Orchard-receiver payment is delivered
1677/// via the Ironwood bundle once Ironwood is active — and for other payments it is
1678/// Sapling. The legacy Orchard pool comes last otherwise, so that it is drawn upon
1679/// only when the more current pools cannot cover the required amount.
1680#[cfg(feature = "orchard")]
1681fn selectable_pool_preference<ParamsT: consensus::Parameters>(
1682    params: &ParamsT,
1683    target_height: TargetHeight,
1684    sapling_supported: bool,
1685    orchard_supported: bool,
1686    prefer_orchard_family: bool,
1687) -> Vec<ShieldedPool> {
1688    let ironwood_selectable = orchard_supported && ironwood_active_at(params, target_height);
1689    let mut preference = Vec::with_capacity(3);
1690    if prefer_orchard_family {
1691        if ironwood_selectable {
1692            preference.push(ShieldedPool::Ironwood);
1693        }
1694        if orchard_supported {
1695            preference.push(ShieldedPool::Orchard);
1696        }
1697        if sapling_supported {
1698            preference.push(ShieldedPool::Sapling);
1699        }
1700    } else {
1701        if sapling_supported {
1702            preference.push(ShieldedPool::Sapling);
1703        }
1704        if ironwood_selectable {
1705            preference.push(ShieldedPool::Ironwood);
1706        }
1707        if orchard_supported {
1708            preference.push(ShieldedPool::Orchard);
1709        }
1710    }
1711    preference
1712}
1713
1714/// Returns the Orchard bundle version whose action-count policy applies to
1715/// transactions constructed for the given target height.
1716#[cfg(feature = "orchard")]
1717fn orchard_bundle_version_for_height<ParamsT: consensus::Parameters, H: Into<BlockHeight>>(
1718    params: &ParamsT,
1719    target_height: H,
1720) -> ::orchard::bundle::BundleVersion {
1721    zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
1722        consensus::BranchId::for_height(params, target_height.into()),
1723        ::orchard::ValuePool::Orchard,
1724    )
1725    // Orchard did not exist prior to NU5, so no Orchard bundle (and no Orchard
1726    // action) can be produced for a pre-NU5 target height; every bundle version
1727    // yields the correct action count (zero) for an empty bundle.
1728    .unwrap_or(::orchard::bundle::BundleVersion::orchard_insecure_v1())
1729}
1730
1731/// Returns the Ironwood bundle version whose action-count policy applies to
1732/// transactions constructed for the given target height.
1733#[cfg(feature = "orchard")]
1734fn ironwood_bundle_version_for_height<ParamsT: consensus::Parameters, H: Into<BlockHeight>>(
1735    params: &ParamsT,
1736    target_height: H,
1737) -> ::orchard::bundle::BundleVersion {
1738    zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
1739        consensus::BranchId::for_height(params, target_height.into()),
1740        ::orchard::ValuePool::Ironwood,
1741    )
1742    // The Ironwood pool did not exist prior to NU6.3, so no Ironwood bundle (and
1743    // no Ironwood action) can be produced for an earlier target height; every
1744    // bundle version yields the correct action count (zero) for an empty bundle.
1745    .unwrap_or(::orchard::bundle::BundleVersion::ironwood_v3())
1746}
1747
1748#[allow(clippy::type_complexity, clippy::too_many_arguments)]
1749pub(crate) fn propose_send_max<ParamsT, InputSourceT, FeeRuleT>(
1750    params: &ParamsT,
1751    wallet_db: &InputSourceT,
1752    fee_rule: &FeeRuleT,
1753    source_account: InputSourceT::AccountId,
1754    spend_pools: &[ShieldedPool],
1755    target_height: TargetHeight,
1756    anchor_height: BlockHeight,
1757    mode: MaxSpendMode,
1758    confirmations_policy: ConfirmationsPolicy,
1759    recipient: ZcashAddress,
1760    memo: Option<MemoBytes>,
1761    locked_input_policy: &LockedInputPolicy,
1762) -> Result<
1763    Proposal<FeeRuleT, InputSourceT::NoteRef>,
1764    InputSelectorError<
1765        InputSourceT::Error,
1766        GreedyInputSelectorError,
1767        FeeRuleT::Error,
1768        InputSourceT::NoteRef,
1769    >,
1770>
1771where
1772    ParamsT: consensus::Parameters,
1773    InputSourceT: InputSource,
1774    FeeRuleT: FeeRule + Clone,
1775{
1776    // Input selection honors the caller's `locked_input_policy`: by default (`Exclude`) a
1777    // locked output is never drawn upon, since it belongs to another in-flight proposal and
1778    // spending it would recreate the conflict that locking exists to prevent; the
1779    // `PreferUnlocked`/`PreferLocked` overrides let a caller draw through a lock it recognizes
1780    // (e.g. its own pool-migration PCZTs).
1781    let spendable_notes = wallet_db
1782        .select_spendable_notes(
1783            source_account,
1784            TargetValue::AllFunds(mode),
1785            spend_pools,
1786            target_height,
1787            confirmations_policy,
1788            &[],
1789            LockFilter::Policy(locked_input_policy),
1790        )
1791        .map_err(InputSelectorError::DataSource)?;
1792
1793    let input_total = spendable_notes
1794        .total_value()
1795        .map_err(|e| InputSelectorError::Selection(GreedyInputSelectorError::Balance(e)))?;
1796
1797    let mut payment_pools = BTreeMap::new();
1798
1799    // An Orchard receiver takes delivery precedence over a Sapling receiver only when
1800    // this build is able to produce Orchard-family outputs; without the `orchard`
1801    // feature, a payment to a recipient having both receivers is delivered via the
1802    // Sapling receiver.
1803    #[cfg(feature = "orchard")]
1804    let orchard_receiver_payable = recipient.can_receive_as(PoolType::ORCHARD);
1805    #[cfg(not(feature = "orchard"))]
1806    let orchard_receiver_payable = false;
1807
1808    let sapling_output_count = {
1809        // we require a sapling output if the recipient has a Sapling receiver and its
1810        // payment is not deliverable via an Orchard receiver.
1811        let requested_sapling_outputs: usize =
1812            if recipient.can_receive_as(PoolType::SAPLING) && !orchard_receiver_payable {
1813                payment_pools.insert(0, PoolType::SAPLING);
1814                1
1815            } else {
1816                0
1817            };
1818
1819        ::sapling::builder::BundleType::DEFAULT
1820            .num_outputs(spendable_notes.sapling.len(), requested_sapling_outputs)
1821            .map_err(|s| InputSelectorError::Change(ChangeError::BundleError(s)))?
1822    };
1823
1824    let sapling_bundle_required = !spendable_notes.sapling().is_empty() || sapling_output_count > 0;
1825
1826    // A payment to an Orchard receiver is represented in the proposal as an Ironwood-pool output
1827    // once Ironwood is active (delivered to the Orchard receiver via the Ironwood bundle), and as
1828    // an Orchard-pool output otherwise. The per-bundle action counts below reflect that split.
1829    #[cfg(feature = "orchard")]
1830    let orchard_receivers_fill_ironwood = ironwood_active_at(params, target_height);
1831    #[cfg(feature = "orchard")]
1832    if orchard_receiver_payable {
1833        payment_pools.insert(
1834            0,
1835            if orchard_receivers_fill_ironwood {
1836                PoolType::IRONWOOD
1837            } else {
1838                PoolType::ORCHARD
1839            },
1840        );
1841    }
1842
1843    #[cfg(feature = "orchard")]
1844    let orchard_action_count = orchard_fees::transactional_action_count(
1845        // Input selection estimates fees with the padded default bundle type; the
1846        // unpadded opt-in is applied later by the change strategy.
1847        ::orchard::builder::BundleType::DEFAULT,
1848        orchard_bundle_version_for_height(params, target_height),
1849        spendable_notes.orchard.len(),
1850        usize::from(orchard_receiver_payable && !orchard_receivers_fill_ironwood),
1851    )
1852    .map_err(|e| InputSelectorError::Change(ChangeError::BundleError(e)))?;
1853    #[cfg(not(feature = "orchard"))]
1854    let orchard_action_count: usize = 0;
1855
1856    #[cfg(feature = "orchard")]
1857    let orchard_bundle_required = orchard_action_count > 0;
1858
1859    #[cfg(feature = "orchard")]
1860    let ironwood_action_count = orchard_fees::transactional_action_count(
1861        ::orchard::builder::BundleType::DEFAULT,
1862        ironwood_bundle_version_for_height(params, target_height),
1863        spendable_notes.ironwood.len(),
1864        usize::from(orchard_receiver_payable && orchard_receivers_fill_ironwood),
1865    )
1866    .map_err(|s| InputSelectorError::Change(ChangeError::BundleError(s)))?;
1867    #[cfg(not(feature = "orchard"))]
1868    let ironwood_action_count: usize = 0;
1869
1870    #[cfg(feature = "orchard")]
1871    let ironwood_bundle_required = ironwood_action_count > 0;
1872
1873    let recipient_address: Address = recipient
1874        .clone()
1875        .convert_if_network(params.network_type())?;
1876
1877    // A recipient that can only receive funds via a transparent output — a bare
1878    // transparent address, or a unified address with no shielded receiver — is paid
1879    // directly from the proposed transaction. TEX recipients are excluded: their
1880    // payment is delivered by the ephemeral second step, which carries its own
1881    // payment pool assignment.
1882    let pays_transparent_directly = match &recipient_address {
1883        Address::Transparent(_) => true,
1884        Address::Unified(addr) => {
1885            addr.has_transparent() && !(addr.has_sapling() || addr.has_orchard())
1886        }
1887        _ => false,
1888    };
1889    if pays_transparent_directly {
1890        payment_pools.insert(0, PoolType::Transparent);
1891    }
1892
1893    // A unified address that has been assigned no payment pool by this point contains
1894    // no receiver that this build is able to pay (for example, an address containing
1895    // only an Orchard receiver, in a build made without the `orchard` feature). Other
1896    // recipient kinds always receive an assignment above, except for TEX addresses,
1897    // whose payment is delivered by the ephemeral second step.
1898    if payment_pools.is_empty()
1899        && let Address::Unified(addr) = &recipient_address
1900    {
1901        return Err(InputSelectorError::Selection(
1902            GreedyInputSelectorError::UnsupportedAddress(addr.clone()),
1903        ));
1904    }
1905
1906    let (tr0_fee, tr1_fee) = match recipient_address {
1907        Address::Sapling(_) => fee_rule
1908            .fee_required(
1909                params,
1910                BlockHeight::from(target_height),
1911                [],
1912                [],
1913                spendable_notes.sapling().len(),
1914                sapling_output_count,
1915                orchard_action_count,
1916                ironwood_action_count,
1917            )
1918            .map(|fee| (fee, None)),
1919        Address::Transparent(_) => fee_rule
1920            .fee_required(
1921                params,
1922                BlockHeight::from(target_height),
1923                [],
1924                [P2PKH_STANDARD_OUTPUT_SIZE],
1925                spendable_notes.sapling().len(),
1926                sapling_output_count,
1927                orchard_action_count,
1928                ironwood_action_count,
1929            )
1930            .map(|fee| (fee, None)),
1931        Address::Unified(_) => fee_rule
1932            .fee_required(
1933                params,
1934                BlockHeight::from(target_height),
1935                [],
1936                if pays_transparent_directly {
1937                    vec![P2PKH_STANDARD_OUTPUT_SIZE]
1938                } else {
1939                    vec![]
1940                },
1941                spendable_notes.sapling().len(),
1942                sapling_output_count,
1943                orchard_action_count,
1944                ironwood_action_count,
1945            )
1946            .map(|fee| (fee, None)),
1947        // Paying a TEX recipient requires a second, purely transparent transaction that
1948        // spends an ephemeral output of the first; constructing that ZIP 320 pair is
1949        // only supported when the `transparent-inputs` feature is enabled.
1950        #[cfg(not(feature = "transparent-inputs"))]
1951        Address::Tex(_) => {
1952            return Err(InputSelectorError::Selection(
1953                GreedyInputSelectorError::UnsupportedTexAddress,
1954            ));
1955        }
1956        #[cfg(feature = "transparent-inputs")]
1957        Address::Tex(_) => fee_rule
1958            .fee_required(
1959                params,
1960                BlockHeight::from(target_height),
1961                [],
1962                [P2PKH_STANDARD_OUTPUT_SIZE],
1963                spendable_notes.sapling().len(),
1964                sapling_output_count,
1965                orchard_action_count,
1966                ironwood_action_count,
1967            )
1968            .and_then(|tr0_fee| {
1969                let tr1_fee = fee_rule.fee_required(
1970                    params,
1971                    BlockHeight::from(target_height),
1972                    [InputSize::Known(P2PKH_STANDARD_INPUT_SIZE)],
1973                    [P2PKH_STANDARD_OUTPUT_SIZE],
1974                    0,
1975                    0,
1976                    0,
1977                    0,
1978                )?;
1979
1980                Ok((tr0_fee, Some(tr1_fee)))
1981            }),
1982    }
1983    .map_err(|fee_error| InputSelectorError::Change(ChangeError::StrategyError(fee_error)))?;
1984
1985    // the total fee required for the all the involved transactions. For the case
1986    // of TEX it means the fee requied to send the max value to the ephemeral
1987    // address + the fee to send the value in that ephemeral change address to
1988    // the TEX address. The sum can only exceed the maximum monetary amount for a
1989    // fee rule that produces fees outside that amount's intended bounds.
1990    let total_fee_required = (tr0_fee + tr1_fee.unwrap_or(Zatoshis::ZERO)).ok_or(
1991        InputSelectorError::Selection(GreedyInputSelectorError::Balance(BalanceError::Overflow)),
1992    )?;
1993
1994    // the total amount involved in the "send max" operation. This is the total
1995    // spendable value present in the wallet minus the fees required to perform
1996    // the send max operation. The proposal must deliver a nonzero amount to the
1997    // recipient: a send-max operation on a wallet whose entire balance would be
1998    // consumed by fees is reported as insufficient funds rather than proposed as
1999    // a fee-only transaction.
2000    let total_to_recipient = (input_total - total_fee_required)
2001        .filter(|amount| *amount > Zatoshis::ZERO)
2002        .ok_or(InputSelectorError::InsufficientFunds {
2003            available: input_total,
2004            required: (total_fee_required + Zatoshis::const_from_u64(1))
2005                .unwrap_or(Zatoshis::const_from_u64(MAX_MONEY)),
2006        })?;
2007
2008    // when the recipient of the send max operation is a TEX address this is the
2009    // amount that will be needed to send the max available amount accounting the
2010    // fees needed to propose a transaction involving one transparent input and
2011    // one transparent output (the TEX address recipient.)
2012    #[cfg(feature = "transparent-inputs")]
2013    let ephemeral_output_value =
2014        tr1_fee.map(|fee| (total_to_recipient + fee).expect("overflow already checked"));
2015
2016    #[cfg(feature = "transparent-inputs")]
2017    let tr0_change = ephemeral_output_value
2018        .into_iter()
2019        .map(ChangeValue::ephemeral_transparent)
2020        .collect();
2021    #[cfg(not(feature = "transparent-inputs"))]
2022    let tr0_change = vec![];
2023
2024    // The transaction produces no change, unless this is a transaction to a TEX address; in this
2025    // case, the first transaction produces a single ephemeral change output.
2026    let tr0_balance = TransactionBalance::new(tr0_change, tr0_fee)
2027        .expect("the sum of an single-element vector of fee values cannot overflow");
2028
2029    let payment = zip321::Payment::new(
2030        recipient,
2031        Some(total_to_recipient),
2032        memo,
2033        None,
2034        None,
2035        vec![],
2036    )
2037    .map_err(|e| InputSelectorError::Proposal(ProposalError::Zip321(e.with_index(0))))?;
2038
2039    let transaction_request =
2040        TransactionRequest::new(vec![payment.clone()]).map_err(|payment_error| {
2041            InputSelectorError::Proposal(ProposalError::Zip321(payment_error))
2042        })?;
2043
2044    let shielded_inputs = NonEmpty::from_vec(spendable_notes.into_vec(&SimpleNoteRetention {
2045        sapling: sapling_bundle_required,
2046        #[cfg(feature = "orchard")]
2047        orchard: orchard_bundle_required,
2048        #[cfg(feature = "orchard")]
2049        ironwood: ironwood_bundle_required,
2050    }))
2051    .map(ShieldedInputs::from_parts);
2052
2053    build_proposal(
2054        fee_rule,
2055        tr0_balance,
2056        target_height,
2057        anchor_height,
2058        confirmations_policy,
2059        shielded_inputs,
2060        vec![],
2061        transaction_request,
2062        payment_pools,
2063        #[cfg(feature = "orchard")]
2064        ironwood_active_at(params, target_height),
2065        #[cfg(feature = "transparent-inputs")]
2066        ephemeral_output_value
2067            .zip(tr1_fee)
2068            .map(|(ephemeral_output_value, tr1_fee)| EphemeralStepConfig {
2069                ephemeral_output_value,
2070                tr1_balance: TransactionBalance::new(vec![], tr1_fee)
2071                    .expect("the sum of an empty vector of fee values cannot overflow"),
2072                tr1_payments: vec![payment],
2073                tr1_payment_pools: BTreeMap::from_iter([(0, PoolType::Transparent)]),
2074            }),
2075    )
2076    .map_err(InputSelectorError::Proposal)
2077}
2078
2079#[cfg(feature = "transparent-inputs")]
2080struct EphemeralStepConfig {
2081    ephemeral_output_value: Zatoshis,
2082    tr1_balance: TransactionBalance,
2083    tr1_payments: Vec<Payment>,
2084    tr1_payment_pools: BTreeMap<usize, PoolType>,
2085}
2086
2087#[allow(clippy::too_many_arguments)]
2088fn build_proposal<FeeRuleT: FeeRule + Clone, NoteRef>(
2089    fee_rule: &FeeRuleT,
2090    tr0_balance: TransactionBalance,
2091    target_height: TargetHeight,
2092    anchor_height: BlockHeight,
2093    confirmations_policy: ConfirmationsPolicy,
2094    shielded_inputs: Option<ShieldedInputs<NoteRef>>,
2095    transparent_inputs: Vec<WalletTransparentOutput<()>>,
2096    transaction_request: TransactionRequest,
2097    payment_pools: BTreeMap<usize, PoolType>,
2098    #[cfg(feature = "orchard")] ironwood_active: bool,
2099    #[cfg(feature = "transparent-inputs")] ephemeral_step_opt: Option<EphemeralStepConfig>,
2100) -> Result<Proposal<FeeRuleT, NoteRef>, ProposalError> {
2101    #[cfg(feature = "transparent-inputs")]
2102    if let Some(ephemeral_step) = ephemeral_step_opt {
2103        let tr1_balance = ephemeral_step.tr1_balance;
2104        // Construct two new `TransactionRequest`s:
2105        // * `tr0` excludes the TEX outputs, and in their place includes
2106        //   a single additional ephemeral output to the transparent pool.
2107        // * `tr1` spends from that ephemeral output to each TEX output.
2108
2109        // Find exactly one ephemeral change output.
2110        let ephemeral_outputs = tr0_balance
2111            .proposed_change()
2112            .iter()
2113            .enumerate()
2114            .filter(|(_, c)| c.is_ephemeral())
2115            .collect::<Vec<_>>();
2116
2117        let ephemeral_output_index = match &ephemeral_outputs[..] {
2118            [(i, change_value)]
2119                if change_value.value() == ephemeral_step.ephemeral_output_value =>
2120            {
2121                Ok(*i)
2122            }
2123            _ => Err(ProposalError::EphemeralOutputsInvalid),
2124        }?;
2125
2126        let ephemeral_stepoutput =
2127            StepOutput::new(0, StepOutputIndex::Change(ephemeral_output_index));
2128
2129        let tr0 = TransactionRequest::from_indexed(
2130            transaction_request
2131                .payments()
2132                .iter()
2133                .filter(|(idx, _payment)| !ephemeral_step.tr1_payment_pools.contains_key(idx))
2134                .map(|(k, v)| (*k, v.clone()))
2135                .collect(),
2136        )
2137        .expect("removing payments from a TransactionRequest preserves validity");
2138
2139        let mut steps = vec![];
2140        let step0 = Step::from_parts(
2141            &[],
2142            tr0,
2143            payment_pools,
2144            transparent_inputs,
2145            shielded_inputs,
2146            Some(anchor_height),
2147            vec![],
2148            tr0_balance,
2149            false,
2150            #[cfg(feature = "orchard")]
2151            ironwood_active,
2152        )?;
2153        steps.push(step0);
2154
2155        let tr1 =
2156            TransactionRequest::new(ephemeral_step.tr1_payments).expect("valid by construction");
2157        let step1 = Step::from_parts(
2158            &steps,
2159            tr1,
2160            ephemeral_step.tr1_payment_pools,
2161            vec![],
2162            None,
2163            Some(anchor_height),
2164            vec![ephemeral_stepoutput],
2165            tr1_balance,
2166            false,
2167            #[cfg(feature = "orchard")]
2168            ironwood_active,
2169        )?;
2170        steps.push(step1);
2171
2172        return Proposal::multi_step(
2173            fee_rule.clone(),
2174            target_height,
2175            confirmations_policy,
2176            NonEmpty::from_vec(steps).expect("steps is known to be nonempty"),
2177        );
2178    }
2179
2180    Proposal::single_step(
2181        transaction_request,
2182        payment_pools,
2183        transparent_inputs,
2184        shielded_inputs,
2185        anchor_height,
2186        tr0_balance,
2187        fee_rule.clone(),
2188        target_height,
2189        confirmations_policy,
2190        false,
2191        #[cfg(feature = "orchard")]
2192        ironwood_active,
2193    )
2194}
2195
2196#[cfg(feature = "transparent-inputs")]
2197impl<DbT: InputSource> ShieldingSelector for GreedyInputSelector<DbT> {
2198    type Error = GreedyInputSelectorError;
2199    type InputSource = DbT;
2200
2201    #[allow(clippy::type_complexity)]
2202    fn propose_shielding<ParamsT, ChangeT>(
2203        &self,
2204        params: &ParamsT,
2205        wallet_db: &Self::InputSource,
2206        change_strategy: &ChangeT,
2207        shielding_threshold: Zatoshis,
2208        source_addrs: &[TransparentAddress],
2209        to_account: <Self::InputSource as InputSource>::AccountId,
2210        target_height: TargetHeight,
2211        anchor_height: BlockHeight,
2212        zip318: &PoolMigrationParams,
2213        confirmations_policy: ConfirmationsPolicy,
2214        output_filter: CoinbaseFilter,
2215    ) -> Result<
2216        Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
2217        InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, Infallible>,
2218    >
2219    where
2220        ParamsT: consensus::Parameters,
2221        ChangeT: ChangeStrategy<MetaSource = Self::InputSource>,
2222    {
2223        let mut transparent_inputs = gather_shielding_inputs::<DbT, ChangeT::Error>(
2224            wallet_db,
2225            source_addrs,
2226            target_height,
2227            confirmations_policy,
2228            output_filter,
2229            shielding_max_inputs(self.shielding_block_space_percent),
2230            &self.locked_input_policy,
2231        )?;
2232
2233        let wallet_meta = change_strategy
2234            .fetch_wallet_meta(wallet_db, to_account, target_height, &[])
2235            .map_err(InputSelectorError::DataSource)?;
2236
2237        let balance = compute_shielding_balance_with_dust_retry::<DbT, ChangeT, ParamsT>(
2238            change_strategy,
2239            params,
2240            target_height,
2241            anchor_height,
2242            zip318,
2243            &mut transparent_inputs,
2244            &wallet_meta,
2245        )?;
2246
2247        if balance.total() >= shielding_threshold {
2248            Proposal::single_step(
2249                TransactionRequest::empty(),
2250                BTreeMap::new(),
2251                transparent_inputs,
2252                None,
2253                anchor_height,
2254                balance,
2255                (*change_strategy.fee_rule()).clone(),
2256                target_height,
2257                confirmations_policy,
2258                true,
2259                #[cfg(feature = "orchard")]
2260                ironwood_active_at(params, target_height),
2261            )
2262            .map_err(InputSelectorError::Proposal)
2263        } else {
2264            Err(InputSelectorError::InsufficientFunds {
2265                available: balance.total(),
2266                required: shielding_threshold,
2267            })
2268        }
2269    }
2270
2271    #[allow(clippy::type_complexity)]
2272    fn propose_shielding_coinbase<ParamsT, FeeRuleT>(
2273        &self,
2274        params: &ParamsT,
2275        wallet_db: &Self::InputSource,
2276        fee_rule: &FeeRuleT,
2277        shielding_threshold: Zatoshis,
2278        source_addrs: &[TransparentAddress],
2279        to_address: ZcashAddress,
2280        memo: Option<MemoBytes>,
2281        limit: Option<usize>,
2282        target_height: TargetHeight,
2283        anchor_height: BlockHeight,
2284    ) -> Result<
2285        Proposal<FeeRuleT, Infallible>,
2286        InputSelectorError<<DbT as InputSource>::Error, Self::Error, FeeRuleT::Error, Infallible>,
2287    >
2288    where
2289        ParamsT: consensus::Parameters,
2290        FeeRuleT: FeeRule + Clone,
2291    {
2292        // Coinbase-only is enforced here at the API boundary: callers cannot bypass
2293        // it. This is the privacy property that motivates having a dedicated method
2294        // rather than a more general "shield to address" path; only coinbase
2295        // outputs are eligible because they have no prior transparent transaction
2296        // graph that could be exposed to the shielded recipient.
2297        // The block-space cap and the caller-supplied `limit` (when present) both bound the number
2298        // of transparent inputs; `gather_shielding_inputs` applies the more restrictive of the two,
2299        // keeping the highest-value UTXOs first. When `limit` is `Some(0)` this empties the set, and
2300        // the subsequent `InsufficientFunds` check fires; this is the documented behavior.
2301        let transparent_inputs = gather_shielding_inputs::<DbT, FeeRuleT::Error>(
2302            wallet_db,
2303            source_addrs,
2304            target_height,
2305            // It doesn't matter here if we pass a 100 confirmations or 1 confirmations policy,
2306            // as coinbase txs require 100, which will be enforced by note selection.
2307            ConfirmationsPolicy::MIN,
2308            CoinbaseFilter::CoinbaseOnly,
2309            limit
2310                .unwrap_or(usize::MAX)
2311                .min(shielding_max_inputs(self.shielding_block_space_percent)),
2312            &self.locked_input_policy,
2313        )?;
2314
2315        let destination_pool = resolve_shielded_destination::<DbT, FeeRuleT::Error, ParamsT>(
2316            &to_address,
2317            params,
2318            target_height,
2319        )?;
2320
2321        let (sapling_output_count, orchard_action_count, ironwood_action_count) =
2322            match destination_pool {
2323                PoolType::SAPLING => {
2324                    let count = ::sapling::builder::BundleType::DEFAULT
2325                        .num_outputs(0, 1)
2326                        .expect("sapling DEFAULT bundle type permits any (spends, outputs) count");
2327                    (count, 0usize, 0usize)
2328                }
2329                // A pre-NU6.3 payment to an Orchard receiver; after Ironwood activation,
2330                // `resolve_shielded_destination` assigns such payments to the Ironwood pool.
2331                #[cfg(feature = "orchard")]
2332                PoolType::ORCHARD => {
2333                    let count = orchard_fees::transactional_action_count(
2334                        ::orchard::builder::BundleType::DEFAULT,
2335                        orchard_bundle_version_for_height(params, target_height),
2336                        0,
2337                        1,
2338                    )
2339                    .expect("every Orchard bundle version permits spending and output creation");
2340                    (0usize, count, 0usize)
2341                }
2342                // A post-NU6.3 payment to an Orchard receiver, delivered via the Ironwood
2343                // bundle and charged to its action count.
2344                #[cfg(feature = "orchard")]
2345                PoolType::IRONWOOD => {
2346                    let count = orchard_fees::transactional_action_count(
2347                        ::orchard::builder::BundleType::DEFAULT,
2348                        ironwood_bundle_version_for_height(params, target_height),
2349                        0,
2350                        1,
2351                    )
2352                    .expect("the Ironwood bundle version permits spending and output creation");
2353                    (0usize, 0usize, count)
2354                }
2355                // Unreachable: `resolve_shielded_destination` rejects transparent
2356                // destinations earlier with `ShieldingRequiresShieldedRecipient`.
2357                _ => {
2358                    return Err(InputSelectorError::Proposal(
2359                        ProposalError::ShieldingRequiresShieldedRecipient,
2360                    ));
2361                }
2362            };
2363
2364        let fee = fee_rule
2365            .fee_required(
2366                params,
2367                BlockHeight::from(target_height),
2368                transparent_inputs
2369                    .iter()
2370                    .map(transparent_fees::InputView::serialized_size),
2371                std::iter::empty::<usize>(),
2372                0,
2373                sapling_output_count,
2374                orchard_action_count,
2375                ironwood_action_count,
2376            )
2377            // The `InputSelectorError::Change` variant is the only existing
2378            // carrier capable of holding an arbitrary fee-rule error
2379            // (`ChangeError::StrategyError` wraps `FeeRuleT::Error` in the
2380            // generic position). We reuse it here rather than introduce a new
2381            // top-level variant.
2382            .map_err(|e| InputSelectorError::Change(ChangeError::StrategyError(e)))?;
2383
2384        // Route the full available value (input_total - fee) as an explicit
2385        // payment to the supplied destination. No change is produced.
2386        let input_total = transparent_inputs
2387            .iter()
2388            .map(|utxo| utxo.value())
2389            .try_fold(Zatoshis::ZERO, |acc, v| acc + v)
2390            .ok_or(InputSelectorError::Selection(
2391                GreedyInputSelectorError::Balance(BalanceError::Overflow),
2392            ))?;
2393        let payment_amount =
2394            (input_total - fee).ok_or_else(|| InputSelectorError::InsufficientFunds {
2395                available: input_total,
2396                required: fee,
2397            })?;
2398
2399        if payment_amount < shielding_threshold {
2400            return Err(InputSelectorError::InsufficientFunds {
2401                available: payment_amount,
2402                required: shielding_threshold,
2403            });
2404        }
2405
2406        let payment = Payment::new(to_address, Some(payment_amount), memo, None, None, vec![])
2407            .map_err(|payment_error| {
2408                InputSelectorError::Proposal(ProposalError::Zip321(payment_error.with_index(0)))
2409            })?;
2410        let request = TransactionRequest::new(vec![payment]).map_err(|payment_error| {
2411            InputSelectorError::Proposal(ProposalError::Zip321(payment_error))
2412        })?;
2413        let mut payment_pools = BTreeMap::new();
2414        payment_pools.insert(0usize, destination_pool);
2415        let final_balance = TransactionBalance::new(vec![], fee).map_err(|_| {
2416            InputSelectorError::Selection(GreedyInputSelectorError::Balance(BalanceError::Overflow))
2417        })?;
2418
2419        // `is_shielding` is `false` because the proposal layer reserves
2420        // `is_shielding = true` for the legacy "no payment, all value in change"
2421        // shape produced by `propose_shielding`. From the wallet's perspective
2422        // this is still a transparent -> shielded transfer of coinbase value.
2423        Proposal::single_step(
2424            request,
2425            payment_pools,
2426            transparent_inputs,
2427            None,
2428            anchor_height,
2429            final_balance,
2430            fee_rule.clone(),
2431            target_height,
2432            // Coinbase shielding spends no shielded notes, so the anchor the resulting step defers
2433            // to is resolved from this policy at interpretation; the exact confirmation depth does
2434            // not matter for an input-less step.
2435            ConfirmationsPolicy::default(),
2436            false,
2437            #[cfg(feature = "orchard")]
2438            ironwood_active_at(params, target_height),
2439        )
2440        .map_err(InputSelectorError::Proposal)
2441    }
2442}
2443
2444/// Gathers spendable transparent UTXOs from each source address, applying the
2445/// supplied [`CoinbaseFilter`] and rejecting input sets that would
2446/// link activity on an ephemeral address to other wallet activity.
2447///
2448/// Shared between `propose_shielding` and `propose_shielding_coinbase`.
2449#[cfg(feature = "transparent-inputs")]
2450#[allow(clippy::type_complexity)]
2451fn gather_shielding_inputs<DbT, ChangeErrT>(
2452    wallet_db: &DbT,
2453    source_addrs: &[TransparentAddress],
2454    target_height: TargetHeight,
2455    confirmations_policy: ConfirmationsPolicy,
2456    output_filter: CoinbaseFilter,
2457    max_inputs: usize,
2458    locked_input_policy: &LockedInputPolicy,
2459) -> Result<
2460    Vec<WalletTransparentOutput<()>>,
2461    InputSelectorError<
2462        <DbT as InputSource>::Error,
2463        GreedyInputSelectorError,
2464        ChangeErrT,
2465        Infallible,
2466    >,
2467>
2468where
2469    DbT: InputSource,
2470{
2471    // Gather the spendable UTXOs for every source address in a single query. This avoids issuing
2472    // one query per address (including for the many addresses that have no spendable outputs),
2473    // which is prohibitively expensive for wallets that hold large numbers of transparent
2474    // addresses.
2475    // Input selection honors the selector's configured `locked_input_policy` (see
2476    // `GreedyInputSelector::with_locked_input_policy`): by default (`Exclude`) a locked output is
2477    // never drawn upon, since it belongs to another in-flight proposal and spending it would
2478    // recreate the conflict that locking exists to prevent.
2479    let mut utxos = wallet_db
2480        .get_spendable_transparent_outputs_for_addresses(
2481            source_addrs,
2482            target_height,
2483            confirmations_policy,
2484            output_filter,
2485            LockFilter::Policy(locked_input_policy),
2486        )
2487        .map_err(InputSelectorError::DataSource)?;
2488
2489    // Cap the number of transparent inputs that a single shielding transaction may consume,
2490    // keeping the highest-value UTXOs first (stable tiebreaker by outpoint for determinism). UTXOs
2491    // beyond the cap are left unspent, to be consolidated by a subsequent shielding transaction.
2492    // When `max_inputs` is 0 this empties the set, and the caller's `InsufficientFunds` check
2493    // fires. The cap is applied before the linkability check below so that the check reflects the
2494    // outputs that will actually be spent.
2495    utxos.sort_by(|a, b| {
2496        b.value()
2497            .cmp(&a.value())
2498            .then_with(|| a.outpoint().cmp(b.outpoint()))
2499    });
2500    utxos.truncate(max_inputs);
2501
2502    // We use `recipient_key_scope()` and `recipient_address()` from the returned outputs to
2503    // determine the set of input addresses and which of them are ephemeral, rather than querying
2504    // the wallet again per address.
2505    let ephemeral_addrs = utxos
2506        .iter()
2507        .filter_map(|utxo| {
2508            (utxo.recipient_key_scope() == Some(TransparentKeyScope::EPHEMERAL))
2509                .then_some(utxo.recipient_address())
2510        })
2511        .collect::<BTreeSet<_>>();
2512    let input_addrs = utxos
2513        .iter()
2514        .map(|utxo| utxo.recipient_address())
2515        .collect::<BTreeSet<_>>();
2516
2517    // Funds may be spent from at most one ephemeral address at a time. If there are no
2518    // ephemeral addresses, we allow shielding from multiple transparent addresses.
2519    if !ephemeral_addrs.is_empty() && input_addrs.len() > 1 {
2520        return Err(InputSelectorError::Proposal(
2521            ProposalError::EphemeralAddressLinkability,
2522        ));
2523    }
2524
2525    Ok(utxos
2526        .into_iter()
2527        .map(|utxo| utxo.redact_account_data())
2528        .collect())
2529}
2530
2531/// Resolves a [`ZcashAddress`] destination for a shielding proposal to the
2532/// shielded pool it should be received in.
2533///
2534/// Rejects transparent and TEX addresses with
2535/// [`ProposalError::ShieldingRequiresShieldedRecipient`], and rejects Unified
2536/// Addresses without a shielded receiver with
2537/// [`GreedyInputSelectorError::UnsupportedAddress`].
2538#[cfg(feature = "transparent-inputs")]
2539#[allow(clippy::type_complexity)]
2540fn resolve_shielded_destination<DbT, ChangeErrT, ParamsT>(
2541    addr: &ZcashAddress,
2542    params: &ParamsT,
2543    target_height: TargetHeight,
2544) -> Result<
2545    PoolType,
2546    InputSelectorError<
2547        <DbT as InputSource>::Error,
2548        GreedyInputSelectorError,
2549        ChangeErrT,
2550        Infallible,
2551    >,
2552>
2553where
2554    DbT: InputSource,
2555    ParamsT: consensus::Parameters,
2556{
2557    #[cfg(not(feature = "orchard"))]
2558    let _ = target_height;
2559
2560    let resolved: Address = addr
2561        .clone()
2562        .convert_if_network(params.network_type())
2563        .map_err(InputSelectorError::Address)?;
2564    match resolved {
2565        Address::Sapling(_) => Ok(PoolType::SAPLING),
2566        // A payment to an Orchard-protocol receiver is an Ironwood-pool output once
2567        // Ironwood is active (delivered to the recipient's Orchard receiver via the
2568        // Ironwood bundle), and an Orchard-pool output otherwise.
2569        #[cfg(feature = "orchard")]
2570        Address::Unified(ua) if ua.has_orchard() => {
2571            Ok(if ironwood_active_at(params, target_height) {
2572                PoolType::IRONWOOD
2573            } else {
2574                PoolType::ORCHARD
2575            })
2576        }
2577        Address::Unified(ua) if ua.has_sapling() => Ok(PoolType::SAPLING),
2578        Address::Unified(ua) => Err(InputSelectorError::Selection(
2579            GreedyInputSelectorError::UnsupportedAddress(ua),
2580        )),
2581        Address::Transparent(_) | Address::Tex(_) => Err(InputSelectorError::Proposal(
2582            ProposalError::ShieldingRequiresShieldedRecipient,
2583        )),
2584    }
2585}
2586
2587/// Helper that performs the dust-input retry pattern used by `propose_shielding`.
2588///
2589/// On the first call to [`ChangeStrategy::compute_balance`], if the strategy
2590/// reports [`ChangeError::DustInputs`], those inputs are removed from
2591/// `transparent_inputs` and the balance is recomputed. The resulting proposal
2592/// directs all available value into change (the legacy `propose_shielding`
2593/// "all-change" shape).
2594#[cfg(feature = "transparent-inputs")]
2595#[allow(clippy::type_complexity)]
2596fn compute_shielding_balance_with_dust_retry<DbT, ChangeT, ParamsT>(
2597    change_strategy: &ChangeT,
2598    params: &ParamsT,
2599    target_height: TargetHeight,
2600    anchor_height: BlockHeight,
2601    zip318: &PoolMigrationParams,
2602    transparent_inputs: &mut Vec<WalletTransparentOutput<()>>,
2603    wallet_meta: &<ChangeT as ChangeStrategy>::AccountMetaT,
2604) -> Result<
2605    TransactionBalance,
2606    InputSelectorError<
2607        <DbT as InputSource>::Error,
2608        GreedyInputSelectorError,
2609        ChangeT::Error,
2610        Infallible,
2611    >,
2612>
2613where
2614    DbT: InputSource,
2615    ChangeT: ChangeStrategy<MetaSource = DbT>,
2616    ParamsT: consensus::Parameters,
2617{
2618    let trial = compute_shielding_balance::<DbT, ChangeT, ParamsT>(
2619        change_strategy,
2620        params,
2621        target_height,
2622        anchor_height,
2623        zip318,
2624        transparent_inputs,
2625        wallet_meta,
2626    );
2627
2628    match trial {
2629        Ok(balance) => Ok(balance),
2630        Err(ChangeError::DustInputs { transparent, .. }) => {
2631            let exclusions: BTreeSet<OutPoint> = transparent.into_iter().collect();
2632            transparent_inputs.retain(|i| !exclusions.contains(i.outpoint()));
2633
2634            compute_shielding_balance::<DbT, ChangeT, ParamsT>(
2635                change_strategy,
2636                params,
2637                target_height,
2638                anchor_height,
2639                zip318,
2640                transparent_inputs,
2641                wallet_meta,
2642            )
2643            .map_err(InputSelectorError::Change)
2644        }
2645        Err(other) => Err(InputSelectorError::Change(other)),
2646    }
2647}
2648
2649/// Helper for `propose_shielding`'s balance computation that calls
2650/// `change_strategy.compute_balance` with empty shielded bundle views, allowing
2651/// the change strategy to direct all available transparent input value into
2652/// change.
2653///
2654/// The empty Orchard-family views carry the bundle versions in effect at the
2655/// target height (rather than a fixed default), because the change the strategy
2656/// directs into a shielded pool is charged against that pool's bundle under its
2657/// version's action-count policy.
2658#[cfg(feature = "transparent-inputs")]
2659#[allow(clippy::type_complexity)]
2660fn compute_shielding_balance<DbT, ChangeT, ParamsT>(
2661    change_strategy: &ChangeT,
2662    params: &ParamsT,
2663    target_height: TargetHeight,
2664    anchor_height: BlockHeight,
2665    zip318: &PoolMigrationParams,
2666    transparent_inputs: &[WalletTransparentOutput<()>],
2667    wallet_meta: &<ChangeT as ChangeStrategy>::AccountMetaT,
2668) -> Result<TransactionBalance, ChangeError<ChangeT::Error, Infallible>>
2669where
2670    DbT: InputSource,
2671    ChangeT: ChangeStrategy<MetaSource = DbT>,
2672    ParamsT: consensus::Parameters,
2673{
2674    #[cfg(feature = "orchard")]
2675    let empty_orchard_view = (
2676        orchard_bundle_version_for_height(params, target_height),
2677        &[] as &[Infallible],
2678        &[] as &[Infallible],
2679    );
2680    #[cfg(feature = "orchard")]
2681    let empty_ironwood_view = (
2682        ironwood_bundle_version_for_height(params, target_height),
2683        &[] as &[Infallible],
2684        &[] as &[Infallible],
2685    );
2686
2687    change_strategy.compute_balance(
2688        params,
2689        target_height,
2690        anchor_height,
2691        zip318,
2692        transparent_inputs,
2693        &[] as &[TxOut],
2694        &sapling::EmptyBundleView,
2695        #[cfg(feature = "orchard")]
2696        &empty_orchard_view,
2697        #[cfg(feature = "orchard")]
2698        &empty_ironwood_view,
2699        None,
2700        wallet_meta,
2701    )
2702}
2703
2704#[cfg(all(test, feature = "transparent-inputs"))]
2705mod tests {
2706    use super::shielding_max_inputs;
2707
2708    #[test]
2709    fn shielding_max_inputs_from_block_space_percent() {
2710        // max_inputs = (MAX_BLOCK_BYTES * percent / 100) / P2PKH_STANDARD_INPUT_SIZE
2711        //            = (2_000_000 * percent / 100) / 150
2712        assert_eq!(shielding_max_inputs(0), 0);
2713        assert_eq!(shielding_max_inputs(1), 133); // 20_000 / 150
2714        assert_eq!(shielding_max_inputs(10), 1333); // 200_000 / 150
2715        assert_eq!(shielding_max_inputs(100), 13333); // 2_000_000 / 150
2716    }
2717}
2718
2719#[cfg(test)]
2720mod spend_policy_tests {
2721    use super::*;
2722    #[cfg(feature = "transparent-inputs")]
2723    use crate::data_api::CoinbaseFilter;
2724    use crate::wallet::LockOwner;
2725
2726    // The default spend policy preserves the historical `ShieldedOnly` behavior: notes may be
2727    // selected from every shielded pool present in the build, and no transparent UTXOs are
2728    // spent. Restricting the set is what a caller does to prevent pool crossing.
2729    #[test]
2730    fn default_permits_all_shielded_pools_and_no_transparent() {
2731        let policy = SpendPolicy::default();
2732        assert!(policy.permits_shielded(ShieldedPool::Sapling));
2733        #[cfg(feature = "orchard")]
2734        {
2735            assert!(policy.permits_shielded(ShieldedPool::Orchard));
2736            assert!(policy.permits_shielded(ShieldedPool::Ironwood));
2737        }
2738        #[cfg(feature = "transparent-inputs")]
2739        assert!(policy.transparent().is_none());
2740    }
2741
2742    // A caller can restrict selection to a single pool; other pools are then not permitted.
2743    #[test]
2744    fn shielded_pools_restricts_the_permitted_set() {
2745        let policy = SpendPolicy::shielded_pools([ShieldedPool::Orchard]);
2746        assert!(policy.permits_shielded(ShieldedPool::Orchard));
2747        assert!(!policy.permits_shielded(ShieldedPool::Sapling));
2748        assert!(!policy.permits_shielded(ShieldedPool::Ironwood));
2749    }
2750
2751    // The caller-facing coinbase choice maps onto the internal `CoinbaseFilter` query control.
2752    #[cfg(feature = "transparent-inputs")]
2753    #[test]
2754    fn coinbase_policy_maps_to_filter() {
2755        assert_eq!(
2756            CoinbaseFilter::from(CoinbasePolicy::OnlyCoinbase),
2757            CoinbaseFilter::CoinbaseOnly
2758        );
2759        assert_eq!(
2760            CoinbaseFilter::from(CoinbasePolicy::NonCoinbase),
2761            CoinbaseFilter::NonCoinbaseOnly
2762        );
2763    }
2764
2765    // A transparent spend policy spends non-coinbase UTXOs by default, and `with_coinbase`
2766    // overrides that choice while preserving the source.
2767    #[cfg(feature = "transparent-inputs")]
2768    #[test]
2769    fn transparent_policy_coinbase_defaults_and_override() {
2770        let policy = TransparentSpendPolicy::any_account_addr();
2771        assert_eq!(policy.coinbase(), CoinbasePolicy::NonCoinbase);
2772
2773        let policy = policy.with_coinbase(CoinbasePolicy::OnlyCoinbase);
2774        assert_eq!(policy.coinbase(), CoinbasePolicy::OnlyCoinbase);
2775        assert!(matches!(policy.source(), TransparentSource::AnyAccountAddr));
2776    }
2777
2778    // The default `SpendPolicy` excludes locked inputs, and `with_locked_input_policy` overrides
2779    // that choice.
2780    #[test]
2781    fn spend_policy_locked_input_policy_roundtrips() {
2782        let default = SpendPolicy::default();
2783        assert_eq!(default.locked_input_policy(), &LockedInputPolicy::Exclude);
2784        let owners = NonEmptyBTreeSet::singleton(LockOwner::new([9u8; 32]));
2785        let policy = SpendPolicy::default()
2786            .with_locked_input_policy(LockedInputPolicy::PreferLocked(owners.clone()));
2787        assert_eq!(
2788            policy.locked_input_policy(),
2789            &LockedInputPolicy::PreferLocked(owners)
2790        );
2791    }
2792}
2793
2794#[cfg(test)]
2795mod greedy_selection_tests {
2796    use ::sapling::zip32::ExtendedSpendingKey;
2797    use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule;
2798    use zcash_protocol::consensus::{Network, NetworkUpgrade, Parameters};
2799    use zip321::Payment;
2800
2801    use super::*;
2802    use crate::{
2803        data_api::{anchor_retention::AnchorRetentionInterval, testing::FixedNotes},
2804        fees::{DustOutputPolicy, zip317::SingleOutputChangeStrategy},
2805    };
2806
2807    /// The account the fixture serves.
2808    const ACCOUNT: u32 = 0;
2809    /// More selections than any of these tests makes before the selector stops; a run that
2810    /// reaches it is looping over the same selection.
2811    const SELECTION_LIMIT: usize = 4;
2812
2813    /// A payment that a Sapling note of `SAPLING_NOTE_VALUE` covers alone, with change under
2814    /// the ZIP 317 dust threshold.
2815    const PAYMENT_VALUE: Zatoshis = Zatoshis::const_from_u64(47_000);
2816    /// The value of a Sapling note that covers `PAYMENT_VALUE` alone.
2817    const SAPLING_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(60_000);
2818    const SAPLING_NOTE_ID: u32 = 0;
2819
2820    /// How a proposal failed.
2821    #[derive(Debug, PartialEq, Eq)]
2822    enum Failure {
2823        InsufficientFunds {
2824            available: Zatoshis,
2825            required: Zatoshis,
2826        },
2827        Selection(GreedyInputSelectorError),
2828    }
2829
2830    fn standard_strategy() -> SingleOutputChangeStrategy<Zip317FeeRule, FixedNotes> {
2831        SingleOutputChangeStrategy::new(
2832            Zip317FeeRule::standard(),
2833            None,
2834            ShieldedPool::Sapling,
2835            DustOutputPolicy::default(),
2836        )
2837    }
2838
2839    /// Proposes a payment of `payment` to a Sapling address from `wallet` with
2840    /// `change_strategy` under the default spend policy, returning the ids of the notes spent,
2841    /// or how the proposal failed.
2842    fn propose_payment<S>(
2843        wallet: &FixedNotes,
2844        payment: Zatoshis,
2845        change_strategy: &S,
2846    ) -> Result<Vec<u32>, Failure>
2847    where
2848        S: ChangeStrategy<MetaSource = FixedNotes>,
2849        S::Error: Debug,
2850    {
2851        propose_payment_under(wallet, payment, change_strategy, &SpendPolicy::default())
2852    }
2853
2854    /// Proposes a payment of `payment` to a Sapling address from `wallet` with
2855    /// `change_strategy` under `spend_policy`, returning the ids of the notes spent, or how
2856    /// the proposal failed.
2857    fn propose_payment_under<S>(
2858        wallet: &FixedNotes,
2859        payment: Zatoshis,
2860        change_strategy: &S,
2861        spend_policy: &SpendPolicy,
2862    ) -> Result<Vec<u32>, Failure>
2863    where
2864        S: ChangeStrategy<MetaSource = FixedNotes>,
2865        S::Error: Debug,
2866    {
2867        let params = Network::TestNetwork;
2868        let anchor_height = params
2869            .activation_height(NetworkUpgrade::Nu5)
2870            .expect("NU5 is active on testnet");
2871        let (_, recipient) = ExtendedSpendingKey::master(&[])
2872            .expect("the master key of the empty seed is valid")
2873            .default_address();
2874        let recipient = Address::from(recipient).to_zcash_address(&params);
2875        let request = TransactionRequest::new(vec![Payment::without_memo(recipient, payment)])
2876            .expect("a single payment is a valid request");
2877
2878        GreedyInputSelector::new()
2879            .propose_transaction(
2880                &params,
2881                wallet,
2882                TargetHeight::from(anchor_height + 1),
2883                anchor_height,
2884                &PoolMigrationParams::new(
2885                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
2886                    AnchorRetentionInterval::ZIP_318,
2887                ),
2888                ConfirmationsPolicy::default(),
2889                ACCOUNT,
2890                request,
2891                change_strategy,
2892                spend_policy,
2893                None,
2894            )
2895            .map(|proposal| {
2896                proposal
2897                    .steps()
2898                    .iter()
2899                    .flat_map(|step| step.shielded_inputs())
2900                    .flat_map(|inputs| inputs.notes().iter().map(|n| *n.internal_note_id()))
2901                    .collect()
2902            })
2903            .map_err(|e| match e {
2904                InputSelectorError::InsufficientFunds {
2905                    available,
2906                    required,
2907                } => Failure::InsufficientFunds {
2908                    available,
2909                    required,
2910                },
2911                InputSelectorError::Selection(e) => Failure::Selection(e),
2912                other => panic!("unexpected input selection error: {other:?}"),
2913            })
2914    }
2915
2916    /// The scenario in which the change from the preferred pool's note alone would be dust, so
2917    /// a standard change strategy draws on a note in another pool as well.
2918    #[cfg(feature = "orchard")]
2919    mod across_pools {
2920        use super::*;
2921
2922        /// The value of the Orchard note, which does not cover the payment alone.
2923        const ORCHARD_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(20_000);
2924        const ORCHARD_NOTE_ID: u32 = 1;
2925
2926        /// The Orchard note is listed first, so a selection that reaches the Sapling note
2927        /// holds both, as a source that orders notes by value returns them.
2928        fn two_pool_notes() -> FixedNotes {
2929            FixedNotes::new(&[
2930                (ShieldedPool::Orchard, ORCHARD_NOTE_ID, ORCHARD_NOTE_VALUE),
2931                (ShieldedPool::Sapling, SAPLING_NOTE_ID, SAPLING_NOTE_VALUE),
2932            ])
2933            .with_selection_limit(SELECTION_LIMIT)
2934        }
2935
2936        #[test]
2937        fn a_standard_strategy_spends_both_notes_at_a_dust_shortfall() {
2938            assert_eq!(
2939                propose_payment(&two_pool_notes(), PAYMENT_VALUE, &standard_strategy()),
2940                Ok(vec![SAPLING_NOTE_ID, ORCHARD_NOTE_ID])
2941            );
2942        }
2943
2944        #[test]
2945        fn a_source_returning_a_pool_not_asked_for_is_a_contract_violation() {
2946            // Only Sapling may be spent, yet every selection returns the Orchard note too.
2947            let wallet = two_pool_notes().ignoring_sources();
2948            let spend_policy = SpendPolicy::shielded_pools([ShieldedPool::Sapling]);
2949
2950            assert_eq!(
2951                propose_payment_under(&wallet, PAYMENT_VALUE, &standard_strategy(), &spend_policy),
2952                Err(Failure::Selection(
2953                    GreedyInputSelectorError::UnrequestedPoolSelected
2954                ))
2955            );
2956        }
2957    }
2958
2959    /// The scenario in which the change strategy names a selected Sapling note as dust.
2960    mod dust_exclusion {
2961        use zcash_primitives::transaction::fees::zip317::MARGINAL_FEE;
2962        use zcash_protocol::value::COIN;
2963
2964        use super::*;
2965
2966        /// A note of exactly the marginal fee, which the ZIP 317 change strategy reports as
2967        /// uneconomic once the transaction has more spends than outputs, counting the change
2968        /// output.
2969        const DUST_NOTE_VALUE: Zatoshis = MARGINAL_FEE;
2970        /// The value of each of the two notes that fund the payment. One of them, even with
2971        /// the dust note, falls short of a payment of this value plus its fee; both together
2972        /// cover it.
2973        const FUNDING_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(COIN);
2974        const DUST_NOTE_ID: u32 = 0;
2975        const FIRST_FUNDING_NOTE_ID: u32 = 1;
2976        const SECOND_FUNDING_NOTE_ID: u32 = 2;
2977
2978        /// The ZIP 317 fee of a transaction with two logical actions.
2979        fn two_action_fee() -> Zatoshis {
2980            (MARGINAL_FEE + MARGINAL_FEE).expect("no overflow")
2981        }
2982
2983        /// A dust note followed by the two funding notes, in selection order.
2984        fn dust_then_funding_notes() -> FixedNotes {
2985            FixedNotes::new(&[
2986                (ShieldedPool::Sapling, DUST_NOTE_ID, DUST_NOTE_VALUE),
2987                (
2988                    ShieldedPool::Sapling,
2989                    FIRST_FUNDING_NOTE_ID,
2990                    FUNDING_NOTE_VALUE,
2991                ),
2992                (
2993                    ShieldedPool::Sapling,
2994                    SECOND_FUNDING_NOTE_ID,
2995                    FUNDING_NOTE_VALUE,
2996                ),
2997            ])
2998            .with_selection_limit(SELECTION_LIMIT)
2999        }
3000
3001        #[test]
3002        fn excluding_a_dust_note_retries_the_remaining_selection() {
3003            assert_eq!(
3004                propose_payment(
3005                    &dust_then_funding_notes(),
3006                    FUNDING_NOTE_VALUE,
3007                    &standard_strategy()
3008                ),
3009                Ok(vec![FIRST_FUNDING_NOTE_ID, SECOND_FUNDING_NOTE_ID])
3010            );
3011        }
3012
3013        #[test]
3014        fn excluding_a_dust_note_still_reports_a_real_shortfall() {
3015            let funding_value = (FUNDING_NOTE_VALUE + FUNDING_NOTE_VALUE).expect("no overflow");
3016            // Two spends and one output cost the ZIP 317 minimum of two logical actions, so
3017            // the funding notes fall one marginal fee short of the payment and its fee.
3018            // Spending the dust note as well adds one marginal fee of value and one of fee,
3019            // so nothing covers the payment.
3020            let payment = (funding_value - MARGINAL_FEE).expect("no underflow");
3021
3022            assert_eq!(
3023                propose_payment(&dust_then_funding_notes(), payment, &standard_strategy()),
3024                Err(Failure::InsufficientFunds {
3025                    available: funding_value,
3026                    required: (payment + two_action_fee()).expect("no overflow"),
3027                })
3028            );
3029        }
3030
3031        #[test]
3032        fn a_source_returning_an_excluded_note_is_a_contract_violation() {
3033            // Every selection returns all three notes, so the dust note comes back after
3034            // being excluded.
3035            let wallet = dust_then_funding_notes().ignoring_exclude();
3036
3037            assert_eq!(
3038                propose_payment(&wallet, FUNDING_NOTE_VALUE, &standard_strategy()),
3039                Err(Failure::Selection(
3040                    GreedyInputSelectorError::ExcludedNoteSelected
3041                ))
3042            );
3043        }
3044    }
3045
3046    /// Change strategies that break the contract of `ChangeStrategy::compute_balance`.
3047    mod contract_violations {
3048        use transparent::bundle::OutPoint;
3049        use zcash_primitives::transaction::fees::{
3050            transparent as transparent_fees,
3051            zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule},
3052        };
3053
3054        use super::*;
3055        #[cfg(feature = "orchard")]
3056        use crate::fees::orchard::InputView as _;
3057        use crate::fees::{MetaSource, sapling::InputView as _};
3058
3059        /// A single Sapling note that covers the payment.
3060        fn single_sapling_note() -> FixedNotes {
3061            FixedNotes::new(&[(ShieldedPool::Sapling, SAPLING_NOTE_ID, SAPLING_NOTE_VALUE)])
3062                .with_selection_limit(SELECTION_LIMIT)
3063        }
3064
3065        /// The total value of the shielded inputs a change strategy was given.
3066        fn given_value<NoteRefT>(
3067            sapling: &impl sapling::BundleView<NoteRefT>,
3068            #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
3069            #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
3070        ) -> Zatoshis {
3071            let given = sapling.inputs().iter().map(|input| input.value());
3072            #[cfg(feature = "orchard")]
3073            let given = given
3074                .chain(orchard.inputs().iter().map(|input| input.value()))
3075                .chain(ironwood.inputs().iter().map(|input| input.value()));
3076            given.sum::<Option<Zatoshis>>().expect("no overflow")
3077        }
3078
3079        /// A change strategy that reports the payment as its requirement whatever it is
3080        /// given, as one that forgot the fee would: once the inputs cover the payment, the
3081        /// requirement no longer exceeds them.
3082        struct FeeBlindStrategy {
3083            fee_rule: Zip317FeeRule,
3084        }
3085
3086        impl ChangeStrategy for FeeBlindStrategy {
3087            type FeeRule = Zip317FeeRule;
3088            type Error = Zip317FeeError;
3089            type MetaSource = FixedNotes;
3090            type AccountMetaT = ();
3091
3092            fn fee_rule(&self) -> &Self::FeeRule {
3093                &self.fee_rule
3094            }
3095
3096            fn fetch_wallet_meta(
3097                &self,
3098                _meta_source: &Self::MetaSource,
3099                _account: <Self::MetaSource as MetaSource>::AccountId,
3100                _target_height: TargetHeight,
3101                _exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
3102            ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error> {
3103                Ok(())
3104            }
3105
3106            fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
3107                &self,
3108                _params: &P,
3109                _target_height: TargetHeight,
3110                _anchor_height: BlockHeight,
3111                _zip318: &PoolMigrationParams,
3112                _transparent_inputs: &[impl transparent_fees::InputView],
3113                _transparent_outputs: &[impl transparent_fees::OutputView],
3114                sapling: &impl sapling::BundleView<NoteRefT>,
3115                #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
3116                #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
3117                _ephemeral_balance: Option<EphemeralBalance>,
3118                _wallet_meta: &Self::AccountMetaT,
3119            ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
3120                Err(ChangeError::InsufficientFunds {
3121                    available: given_value(
3122                        sapling,
3123                        #[cfg(feature = "orchard")]
3124                        orchard,
3125                        #[cfg(feature = "orchard")]
3126                        ironwood,
3127                    ),
3128                    required: PAYMENT_VALUE,
3129                })
3130            }
3131        }
3132
3133        /// A change strategy that reports the payment as its requirement when given nothing,
3134        /// and a `DustInputs` error otherwise: naming every Sapling input it was given when
3135        /// `names_given`, plus the transparent outpoints in `transparent`, which it is never
3136        /// given.
3137        struct DustNamingStrategy {
3138            fee_rule: Zip317FeeRule,
3139            names_given: bool,
3140            transparent: Vec<OutPoint>,
3141        }
3142
3143        impl ChangeStrategy for DustNamingStrategy {
3144            type FeeRule = Zip317FeeRule;
3145            type Error = Zip317FeeError;
3146            type MetaSource = FixedNotes;
3147            type AccountMetaT = ();
3148
3149            fn fee_rule(&self) -> &Self::FeeRule {
3150                &self.fee_rule
3151            }
3152
3153            fn fetch_wallet_meta(
3154                &self,
3155                _meta_source: &Self::MetaSource,
3156                _account: <Self::MetaSource as MetaSource>::AccountId,
3157                _target_height: TargetHeight,
3158                _exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
3159            ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error> {
3160                Ok(())
3161            }
3162
3163            fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
3164                &self,
3165                _params: &P,
3166                _target_height: TargetHeight,
3167                _anchor_height: BlockHeight,
3168                _zip318: &PoolMigrationParams,
3169                _transparent_inputs: &[impl transparent_fees::InputView],
3170                _transparent_outputs: &[impl transparent_fees::OutputView],
3171                sapling: &impl sapling::BundleView<NoteRefT>,
3172                #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
3173                #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
3174                _ephemeral_balance: Option<EphemeralBalance>,
3175                _wallet_meta: &Self::AccountMetaT,
3176            ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
3177                let given = given_value(
3178                    sapling,
3179                    #[cfg(feature = "orchard")]
3180                    orchard,
3181                    #[cfg(feature = "orchard")]
3182                    ironwood,
3183                );
3184                if given == Zatoshis::ZERO {
3185                    Err(ChangeError::InsufficientFunds {
3186                        available: given,
3187                        required: PAYMENT_VALUE,
3188                    })
3189                } else {
3190                    let sapling = if self.names_given {
3191                        sapling
3192                            .inputs()
3193                            .iter()
3194                            .map(|input| input.note_id().clone())
3195                            .collect()
3196                    } else {
3197                        vec![]
3198                    };
3199                    Err(ChangeError::DustInputs {
3200                        transparent: self.transparent.clone(),
3201                        sapling,
3202                        #[cfg(feature = "orchard")]
3203                        orchard: vec![],
3204                        #[cfg(feature = "orchard")]
3205                        ironwood: vec![],
3206                    })
3207                }
3208            }
3209        }
3210
3211        #[test]
3212        fn a_requirement_the_inputs_cover_is_a_contract_violation() {
3213            let change_strategy = FeeBlindStrategy {
3214                fee_rule: Zip317FeeRule::standard(),
3215            };
3216
3217            assert_eq!(
3218                propose_payment(&single_sapling_note(), PAYMENT_VALUE, &change_strategy),
3219                Err(Failure::Selection(
3220                    GreedyInputSelectorError::RequirementCovered {
3221                        required: PAYMENT_VALUE,
3222                        available: SAPLING_NOTE_VALUE,
3223                    }
3224                ))
3225            );
3226        }
3227
3228        #[test]
3229        fn dust_inputs_naming_nothing_is_a_contract_violation() {
3230            let change_strategy = DustNamingStrategy {
3231                fee_rule: Zip317FeeRule::standard(),
3232                names_given: false,
3233                transparent: vec![],
3234            };
3235
3236            assert_eq!(
3237                propose_payment(&single_sapling_note(), PAYMENT_VALUE, &change_strategy),
3238                Err(Failure::Selection(
3239                    GreedyInputSelectorError::DustInputsNotGiven
3240                ))
3241            );
3242        }
3243
3244        #[cfg(feature = "transparent-inputs")]
3245        #[test]
3246        fn dust_inputs_naming_an_input_not_given_is_a_contract_violation() {
3247            let change_strategy = DustNamingStrategy {
3248                fee_rule: Zip317FeeRule::standard(),
3249                names_given: true,
3250                transparent: vec![OutPoint::new([0; 32], 0)],
3251            };
3252
3253            assert_eq!(
3254                propose_payment(&single_sapling_note(), PAYMENT_VALUE, &change_strategy),
3255                Err(Failure::Selection(
3256                    GreedyInputSelectorError::DustInputsNotGiven
3257                ))
3258            );
3259        }
3260    }
3261}