Skip to main content

zcash_client_backend/fees/
common.rs

1use core::cmp::{Ordering, max, min};
2use std::num::{NonZeroU64, NonZeroUsize};
3
4#[cfg(feature = "orchard")]
5use zcash_primitives::transaction::builder::BundlePadding;
6use zcash_primitives::transaction::fees::{
7    FeeRule, transparent, zip317::MINIMUM_FEE, zip317::P2PKH_STANDARD_OUTPUT_SIZE,
8};
9#[cfg(feature = "orchard")]
10use zcash_protocol::zip318::PoolMigrationConstants;
11
12use crate::data_api::anchor_retention::PoolMigrationParams;
13use zcash_protocol::{
14    PoolType, ShieldedPool,
15    consensus::{self, BlockHeight, NetworkUpgrade},
16    memo::MemoBytes,
17    value::{BalanceError, Zatoshis},
18};
19
20use crate::data_api::{AccountMeta, ChangeReceivers, wallet::TargetHeight};
21
22use super::{
23    ChangeError, ChangeValue, DummyOutputCounts, DustAction, DustOutputPolicy, EphemeralBalance,
24    SplitPolicy, TransactionBalance, sapling as sapling_fees,
25};
26
27#[cfg(feature = "orchard")]
28use super::orchard::{self as orchard_fees, OutputView as _};
29#[cfg(feature = "transparent-inputs")]
30use {super::TransparentChangePolicy, crate::data_api::TransparentChangeKind};
31
32pub(crate) struct NetFlows {
33    t_in: Zatoshis,
34    t_out: Zatoshis,
35    sapling_in: Zatoshis,
36    sapling_out: Zatoshis,
37    orchard_in: Zatoshis,
38    orchard_out: Zatoshis,
39    // Value flowing through the Ironwood bundle, accounted separately from
40    // Orchard because V6 transactions carry distinct Orchard and Ironwood
41    // bundles. Splitting output value between the Orchard and Ironwood views
42    // leaves `total_in`/`total_out` unchanged; the separate fields exist so each
43    // bundle's action count can be derived from its own inputs and outputs.
44    ironwood_in: Zatoshis,
45    ironwood_out: Zatoshis,
46}
47
48impl NetFlows {
49    fn total_in(&self) -> Result<Zatoshis, BalanceError> {
50        (self.t_in + self.sapling_in + self.orchard_in + self.ironwood_in)
51            .ok_or(BalanceError::Overflow)
52    }
53    fn total_out(&self) -> Result<Zatoshis, BalanceError> {
54        (self.t_out + self.sapling_out + self.orchard_out + self.ironwood_out)
55            .ok_or(BalanceError::Overflow)
56    }
57    /// Returns true iff the flows excluding change are fully transparent.
58    fn is_transparent(&self) -> bool {
59        !(self.sapling_in.is_positive()
60            || self.sapling_out.is_positive()
61            || self.orchard_in.is_positive()
62            || self.orchard_out.is_positive()
63            || self.ironwood_in.is_positive()
64            || self.ironwood_out.is_positive())
65    }
66}
67
68#[allow(clippy::too_many_arguments)]
69pub(crate) fn calculate_net_flows<NoteRefT: Clone, F: FeeRule, E>(
70    transparent_inputs: &[impl transparent::InputView],
71    transparent_outputs: &[impl transparent::OutputView],
72    sapling: &impl sapling_fees::BundleView<NoteRefT>,
73    #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
74    #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
75    ephemeral_balance: Option<EphemeralBalance>,
76) -> Result<NetFlows, ChangeError<E, NoteRefT>>
77where
78    E: From<F::Error> + From<BalanceError>,
79{
80    let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
81
82    let t_in = transparent_inputs
83        .iter()
84        .map(|t_in| t_in.coin().value())
85        .chain(ephemeral_balance.and_then(|b| b.ephemeral_input_amount()))
86        .sum::<Option<_>>()
87        .ok_or_else(overflow)?;
88    let t_out = transparent_outputs
89        .iter()
90        .map(|t_out| t_out.value())
91        .chain(ephemeral_balance.and_then(|b| b.ephemeral_output_amount()))
92        .sum::<Option<_>>()
93        .ok_or_else(overflow)?;
94    let sapling_in = sapling
95        .inputs()
96        .iter()
97        .map(sapling_fees::InputView::<NoteRefT>::value)
98        .sum::<Option<_>>()
99        .ok_or_else(overflow)?;
100    let sapling_out = sapling
101        .outputs()
102        .iter()
103        .map(sapling_fees::OutputView::value)
104        .sum::<Option<_>>()
105        .ok_or_else(overflow)?;
106
107    #[cfg(feature = "orchard")]
108    let orchard_in = orchard
109        .inputs()
110        .iter()
111        .map(orchard_fees::InputView::<NoteRefT>::value)
112        .sum::<Option<_>>()
113        .ok_or_else(overflow)?;
114    #[cfg(not(feature = "orchard"))]
115    let orchard_in = Zatoshis::ZERO;
116
117    #[cfg(feature = "orchard")]
118    let orchard_out = orchard
119        .outputs()
120        .iter()
121        .map(orchard_fees::OutputView::value)
122        .sum::<Option<_>>()
123        .ok_or_else(overflow)?;
124    #[cfg(not(feature = "orchard"))]
125    let orchard_out = Zatoshis::ZERO;
126
127    #[cfg(feature = "orchard")]
128    let ironwood_in = ironwood
129        .inputs()
130        .iter()
131        .map(orchard_fees::InputView::<NoteRefT>::value)
132        .sum::<Option<_>>()
133        .ok_or_else(overflow)?;
134    #[cfg(not(feature = "orchard"))]
135    let ironwood_in = Zatoshis::ZERO;
136
137    #[cfg(feature = "orchard")]
138    let ironwood_out = ironwood
139        .outputs()
140        .iter()
141        .map(orchard_fees::OutputView::value)
142        .sum::<Option<_>>()
143        .ok_or_else(overflow)?;
144    #[cfg(not(feature = "orchard"))]
145    let ironwood_out = Zatoshis::ZERO;
146
147    Ok(NetFlows {
148        t_in,
149        t_out,
150        sapling_in,
151        sapling_out,
152        orchard_in,
153        orchard_out,
154        ironwood_in,
155        ironwood_out,
156    })
157}
158
159/// Decide which shielded pool change should go to if there is any.
160///
161/// Change goes to a pool in which the account can receive it, as given by
162/// `change_receivers`. Among those pools, the pools that the transaction already uses
163/// come first, so that change does not cross into another pool when this can be
164/// avoided. The fallback pool comes next, and then any other pool. If the account cannot
165/// receive change in any shielded pool, the result is the pool that the transaction's
166/// flows indicate, and the caller rejects any change that it would place there.
167///
168/// `max_change_value` is an upper bound on the value of the change the transaction will
169/// produce: the value that would remain if it paid only the minimum (changeless) fee.
170/// After Ironwood activation it determines whether change may be returned to the Orchard
171/// pool without violating the turnstile requirement that the pool's balance strictly
172/// decrease.
173pub(crate) fn select_change_pool(
174    _net_flows: &NetFlows,
175    _fallback_change_pool: ShieldedPool,
176    _change_receivers: &ChangeReceivers,
177    _ironwood_active: bool,
178    _max_change_value: Zatoshis,
179) -> ShieldedPool {
180    // TODO: implement a less naive strategy for selecting the pool to which change will be sent.
181    #[cfg(feature = "orchard")]
182    {
183        // The pools with flows in this transaction, in order of preference. Orchard comes
184        // first. Ironwood comes before Sapling, so that change from an Ironwood spend stays
185        // in the Ironwood pool rather than crossing the turnstile back into Orchard.
186        let flow_pools = [
187            (
188                ShieldedPool::Orchard,
189                _net_flows.orchard_in.is_positive() || _net_flows.orchard_out.is_positive(),
190            ),
191            (
192                ShieldedPool::Ironwood,
193                _net_flows.ironwood_in.is_positive() || _net_flows.ironwood_out.is_positive(),
194            ),
195            (
196                ShieldedPool::Sapling,
197                _net_flows.sapling_in.is_positive() || _net_flows.sapling_out.is_positive(),
198            ),
199        ]
200        .into_iter()
201        .filter_map(|(pool, has_flows)| has_flows.then_some(pool));
202
203        // If the flows are transparent, there may not be change. If there is, the caller
204        // gets to decide where to shield it.
205        let flow_preferred = flow_pools.clone().next().unwrap_or(_fallback_change_pool);
206
207        let preferred = flow_pools
208            .chain([
209                _fallback_change_pool,
210                ShieldedPool::Orchard,
211                ShieldedPool::Sapling,
212            ])
213            .find(|pool| _change_receivers.can_receive(PoolType::Shielded(*pool)))
214            .unwrap_or(flow_preferred);
215
216        // After Ironwood activation, the turnstile forbids value from entering the
217        // Orchard pool: change may return to Orchard only when the transaction spends
218        // Orchard notes, and only if strictly less value returns to the pool than the
219        // notes remove from it. Change that cannot go to Orchard flows onward to the
220        // Ironwood pool. An account that can receive change in one of these pools can
221        // receive change in the other, as both use the account's Orchard key.
222        if _ironwood_active
223            && preferred == ShieldedPool::Orchard
224            && (!_net_flows.orchard_in.is_positive() || _max_change_value >= _net_flows.orchard_in)
225        {
226            ShieldedPool::Ironwood
227        } else {
228            preferred
229        }
230    }
231    #[cfg(not(feature = "orchard"))]
232    ShieldedPool::Sapling
233}
234
235#[derive(Clone, Copy, Debug)]
236pub(crate) struct OutputManifest {
237    transparent: usize,
238    sapling: usize,
239    orchard: usize,
240    ironwood: usize,
241}
242
243impl OutputManifest {
244    const ZERO: OutputManifest = OutputManifest {
245        transparent: 0,
246        sapling: 0,
247        orchard: 0,
248        ironwood: 0,
249    };
250
251    pub(crate) fn sapling(&self) -> usize {
252        self.sapling
253    }
254
255    pub(crate) fn orchard(&self) -> usize {
256        self.orchard
257    }
258
259    pub(crate) fn ironwood(&self) -> usize {
260        self.ironwood
261    }
262
263    #[cfg(feature = "orchard")]
264    pub(crate) fn transparent(&self) -> usize {
265        self.transparent
266    }
267
268    pub(crate) fn total_shielded(&self) -> usize {
269        self.sapling + self.orchard + self.ironwood
270    }
271
272    /// A manifest placing `count` change outputs in `pool` and none elsewhere.
273    fn for_pool(pool: ShieldedPool, count: usize) -> Self {
274        Self {
275            transparent: 0,
276            sapling: if pool == ShieldedPool::Sapling {
277                count
278            } else {
279                0
280            },
281            orchard: if pool == ShieldedPool::Orchard {
282                count
283            } else {
284                0
285            },
286            ironwood: if pool == ShieldedPool::Ironwood {
287                count
288            } else {
289                0
290            },
291        }
292    }
293}
294
295pub(crate) struct SinglePoolBalanceConfig<'a, P, F> {
296    params: &'a P,
297    fee_rule: &'a F,
298    dust_output_policy: &'a DustOutputPolicy,
299    default_dust_threshold: Zatoshis,
300    split_policy: &'a SplitPolicy,
301    fallback_change_pool: ShieldedPool,
302    #[cfg(feature = "transparent-inputs")]
303    transparent_change_policy: TransparentChangePolicy,
304    marginal_fee: Zatoshis,
305    grace_actions: usize,
306}
307
308impl<'a, P, F> SinglePoolBalanceConfig<'a, P, F> {
309    #[allow(clippy::too_many_arguments)]
310    pub(crate) fn new(
311        params: &'a P,
312        fee_rule: &'a F,
313        dust_output_policy: &'a DustOutputPolicy,
314        default_dust_threshold: Zatoshis,
315        split_policy: &'a SplitPolicy,
316        fallback_change_pool: ShieldedPool,
317        #[cfg(feature = "transparent-inputs")] transparent_change_policy: TransparentChangePolicy,
318        marginal_fee: Zatoshis,
319        grace_actions: usize,
320    ) -> Self {
321        Self {
322            params,
323            fee_rule,
324            dust_output_policy,
325            default_dust_threshold,
326            split_policy,
327            fallback_change_pool,
328            #[cfg(feature = "transparent-inputs")]
329            transparent_change_policy,
330            marginal_fee,
331            grace_actions,
332        }
333    }
334}
335
336#[allow(clippy::too_many_arguments)]
337pub(crate) fn single_pool_output_balance<P: consensus::Parameters, NoteRefT: Clone, F: FeeRule, E>(
338    cfg: SinglePoolBalanceConfig<P, F>,
339    wallet_meta: Option<&AccountMeta>,
340    change_receivers: &ChangeReceivers,
341    target_height: TargetHeight,
342    transparent_inputs: &[impl transparent::InputView],
343    transparent_outputs: &[impl transparent::OutputView],
344    sapling: &impl sapling_fees::BundleView<NoteRefT>,
345    #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
346    #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
347    // The transactional bundle padding the transaction builder will use for the Orchard and
348    // Ironwood bundles respectively; the action counts computed here must match them so the
349    // builder's exact-balance check succeeds (see `orchard_fees::transactional_action_count`).
350    //
351    // This is `BundlePadding` rather than `BundleType` deliberately: coinbase construction is a
352    // property of the whole transaction, never of one pool, and a fee is never computed for a
353    // coinbase transaction at all. Carrying padding keeps a per-pool coinbase unrepresentable, as
354    // `BuildConfig::Standard` does for the builder.
355    //
356    // There is no matching Ironwood parameter. That bundle's padding is DERIVED from the
357    // transaction's shape (see `ironwood_is_canonical_crossing` below), not chosen by the caller.
358    #[cfg(feature = "orchard")] orchard_padding: BundlePadding,
359    // The anchor the shielded bundles will be proved against, and the ZIP 318 parameters in force
360    // for the wallet proposing the transaction. A canonical crossing must be anchored to a
361    // boundary of the bucket grid, so the padding decision depends on both. The grid comes from
362    // the wallet rather than the network defaults: the wallet is the side that retains the
363    // checkpoints, so its grid is the only one a crossing can actually be proved against.
364    _anchor_height: BlockHeight,
365    _zip318: &PoolMigrationParams,
366    change_memo: Option<&MemoBytes>,
367    ephemeral_balance: Option<EphemeralBalance>,
368) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
369where
370    E: From<F::Error> + From<BalanceError>,
371{
372    // The change memo, if any, must be attached to the change in the intermediate step that
373    // produces the ephemeral output, and so it should be discarded in the ultimate step; this is
374    // distinguished by identifying that this transaction has ephemeral inputs.
375    let change_memo = change_memo.filter(|_| ephemeral_balance.is_none_or(|b| !b.is_input()));
376
377    let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
378    let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow));
379
380    let net_flows = calculate_net_flows::<NoteRefT, F, E>(
381        transparent_inputs,
382        transparent_outputs,
383        sapling,
384        #[cfg(feature = "orchard")]
385        orchard,
386        #[cfg(feature = "orchard")]
387        ironwood,
388        ephemeral_balance,
389    )?;
390
391    // We don't create a fully-transparent transaction if a change memo is used.
392    let fully_transparent = net_flows.is_transparent() && change_memo.is_none();
393
394    // Whether change should be returned to the transparent pool instead of being shielded.
395    // Transparent change is only ever produced when the flows of the transaction are fully
396    // transparent, so that shielded flows never leak change information to the transparent
397    // pool.
398    #[cfg(feature = "transparent-inputs")]
399    let wants_transparent_change = fully_transparent
400        && cfg.transparent_change_policy == TransparentChangePolicy::TransparentChangeAllowed;
401    #[cfg(not(feature = "transparent-inputs"))]
402    let wants_transparent_change = false;
403
404    // The size of the transparent change output, which depends on the kind of address the
405    // account receives it at. An account with no transparent receiver has no size here. Any
406    // transparent change proposed for such an account is rejected below, when the change is
407    // known. An earlier rejection would hide insufficient funds and changeless transactions.
408    #[cfg(feature = "transparent-inputs")]
409    let transparent_change_size = change_receivers
410        .transparent()
411        .filter(|_| wants_transparent_change)
412        .map(|kind| kind.output_size());
413    #[cfg(not(feature = "transparent-inputs"))]
414    let transparent_change_size = None;
415
416    let total_in = net_flows
417        .total_in()
418        .map_err(|e| ChangeError::StrategyError(E::from(e)))?;
419    let subtotal_out = net_flows
420        .total_out()
421        .map_err(|e| ChangeError::StrategyError(E::from(e)))?;
422
423    let sapling_input_count = sapling
424        .bundle_type()
425        .num_spends(sapling.inputs().len())
426        .map_err(ChangeError::BundleError)?;
427    let sapling_output_count = |change_count| {
428        sapling
429            .bundle_type()
430            .num_outputs(
431                sapling.inputs().len(),
432                sapling.outputs().len() + change_count,
433            )
434            .map_err(ChangeError::BundleError)
435    };
436
437    #[cfg(feature = "orchard")]
438    let orchard_action_count = |change_count| {
439        orchard_fees::transactional_action_count(
440            orchard_padding.bundle_type(),
441            orchard.bundle_version(),
442            orchard.inputs().len(),
443            orchard.outputs().len() + change_count,
444        )
445        .map_err(ChangeError::BundleError)
446    };
447    #[cfg(not(feature = "orchard"))]
448    let orchard_action_count = |change_count: usize| -> Result<usize, ChangeError<E, NoteRefT>> {
449        if change_count != 0 {
450            Err(ChangeError::BundleError(
451                "Nonzero Orchard change requested but the `orchard` feature is not enabled.",
452            ))
453        } else {
454            Ok(0)
455        }
456    };
457
458    // The Ironwood bundle is accounted separately from Orchard: a V6 transaction
459    // carries distinct Orchard and Ironwood bundles, each padded to its own
460    // action floor. Callers route Ironwood inputs/outputs into the `ironwood`
461    // view; it is empty (contributing no actions) when nothing targets the
462    // Ironwood pool.
463    //
464    // A CANONICAL CROSSING drops the default padding: no Ironwood spends and a single Ironwood
465    // output whose value is a canonical ZIP 318 denomination, which is exactly the shape of a
466    // ZIP 318 migration transfer. Building it unpadded puts an ordinary turnstile-crossing
467    // payment into that anonymity set rather than leaving it distinguishable by action count.
468    // The resulting dummy-output count is recorded below. `Step::ironwood_bundle_padding`
469    // reconstructs the builder's action target from that finished transaction shape.
470    //
471    // The value is only known here when the sole output is a PAYMENT, i.e. `change_count == 0`;
472    // an Ironwood change value is what this function is in the middle of solving for. That costs
473    // nothing: with a change output there are two real Ironwood outputs, so the bundle is at or
474    // above the default floor and the padding is irrelevant.
475    #[cfg(feature = "orchard")]
476    let ironwood_is_canonical_crossing = |change: OutputManifest| {
477        let constants = _zip318;
478        orchard.inputs().len() == 1
479            && ironwood.inputs().is_empty()
480            && change.ironwood() == 0
481            // The Orchard bundle must be exactly two actions, and from NU6.3 a spend and an output
482            // no longer share one; a second Orchard change output would make three. Change in any
483            // other pool adds a bundle no migration transfer carries. `Step::is_canonical_crossing`
484            // applies the identical bounds, and the two must agree or the builder's exact-balance
485            // check rejects the transaction.
486            && change.orchard() <= 1
487            && change.sapling() == 0
488            && change.transparent() == 0
489            && match ironwood.outputs() {
490                [output] => constants.is_canonical_denomination(output.value()),
491                _ => false,
492            }
493            && constants
494                .anchor_grid().is_boundary(_anchor_height)
495    };
496    #[cfg(feature = "orchard")]
497    let ironwood_action_count = |change: OutputManifest| {
498        // The Ironwood bundle drops its padding exactly when doing so makes the transaction look
499        // like a migration transfer, and is padded otherwise. This is not the caller's to choose.
500        let padding = if ironwood_is_canonical_crossing(change) {
501            BundlePadding::UNPADDED
502        } else {
503            BundlePadding::DEFAULT
504        };
505        orchard_fees::transactional_action_count(
506            padding.bundle_type(),
507            ironwood.bundle_version(),
508            ironwood.inputs().len(),
509            ironwood.outputs().len() + change.ironwood(),
510        )
511        .map_err(ChangeError::BundleError)
512    };
513    #[cfg(not(feature = "orchard"))]
514    let ironwood_action_count =
515        |change: OutputManifest| -> Result<usize, ChangeError<E, NoteRefT>> {
516            if change.ironwood() != 0 {
517                Err(ChangeError::BundleError(
518                    "Nonzero Ironwood change requested but the `orchard` feature is not enabled.",
519                ))
520            } else {
521                Ok(0)
522            }
523        };
524
525    let transparent_input_sizes = transparent_inputs
526        .iter()
527        .map(|i| i.serialized_size())
528        .chain(
529            ephemeral_balance
530                .and_then(|b| b.ephemeral_input_amount())
531                .map(|_| transparent::InputSize::STANDARD_P2PKH),
532        );
533    let transparent_output_sizes = transparent_outputs
534        .iter()
535        .map(|i| i.serialized_size())
536        .chain(
537            ephemeral_balance
538                .and_then(|b| b.ephemeral_output_amount())
539                .map(|_| P2PKH_STANDARD_OUTPUT_SIZE),
540        );
541
542    // Once we calculate the balance with minimum fee (i.e. with no change),
543    // there are three cases:
544    //
545    // 1. Insufficient funds even with minimum fee.
546    // 2. The minimum fee exactly cancels out the net flow balance.
547    // 3. The minimum fee is smaller than the change.
548    //
549    // If case 2 happens for a transaction with any shielded flows, we want there
550    // to be a zero-value shielded change output anyway (i.e. treat this like case 3),
551    // because:
552    // * being able to distinguish these cases potentially leaks too much
553    //   information (an adversary that knows the number of external recipients
554    //   and the sum of their outputs learns the sum of the inputs if no change
555    //   output is present); and
556    // * we will then always have an shielded output in which to put change_memo,
557    //   if one is used.
558    //
559    // Note that using the `DustAction::AddDustToFee` policy inherently leaks
560    // more information.
561
562    let min_fee = cfg
563        .fee_rule
564        .fee_required(
565            cfg.params,
566            BlockHeight::from(target_height),
567            transparent_input_sizes.clone(),
568            transparent_output_sizes.clone(),
569            sapling_input_count,
570            sapling_output_count(0)?,
571            orchard_action_count(0)?,
572            ironwood_action_count(OutputManifest::ZERO)?,
573        )
574        .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
575
576    let total_out_with_min_fee = (subtotal_out + min_fee).ok_or_else(overflow)?;
577
578    // The value that would remain if the transaction paid only the minimum (changeless)
579    // fee is an upper bound on the change value: the fee never falls below `min_fee`.
580    let change_pool = select_change_pool(
581        &net_flows,
582        cfg.fallback_change_pool,
583        change_receivers,
584        cfg.params
585            .is_nu_active(NetworkUpgrade::Nu6_3, target_height.into()),
586        (total_in - total_out_with_min_fee).unwrap_or(Zatoshis::ZERO),
587    );
588
589    let (target_change_count, target_change_counts) = if wants_transparent_change {
590        // Transparent change is always emitted as a single output; the note-splitting policy
591        // exists to improve the spendability of shielded notes and does not apply to
592        // transparent outputs.
593        (
594            1,
595            OutputManifest {
596                transparent: 1,
597                sapling: 0,
598                orchard: 0,
599                ironwood: 0,
600            },
601        )
602    } else {
603        let target_change_count = wallet_meta.map_or(1, |m| {
604            usize::from(cfg.split_policy.target_output_count)
605                // If we cannot determine a total note count, fall back to a single output
606                .saturating_sub(m.total_note_count().unwrap_or(usize::MAX))
607                .max(1)
608        });
609        let target_change_counts = OutputManifest {
610            transparent: 0,
611            sapling: if change_pool == ShieldedPool::Sapling {
612                target_change_count
613            } else {
614                0
615            },
616            orchard: if change_pool == ShieldedPool::Orchard {
617                target_change_count
618            } else {
619                0
620            },
621            ironwood: if change_pool == ShieldedPool::Ironwood {
622                target_change_count
623            } else {
624                0
625            },
626        };
627        assert!(target_change_counts.total_shielded() == target_change_count);
628        (target_change_count, target_change_counts)
629    };
630
631    // If we have a non-zero marginal fee, we need to check for uneconomic inputs.
632    // This is basically assuming that fee rules with non-zero marginal fee are
633    // "ZIP 317-like", but we can generalize later if needed.
634    if cfg.marginal_fee.is_positive() {
635        // Is it certain that there will be a change output? If it is not certain,
636        // we should call `check_for_uneconomic_inputs` with `possible_change`
637        // including both possibilities.
638        let possible_change = {
639            // These are the situations where we might not have a change output.
640            if fully_transparent
641                || (cfg.dust_output_policy.action() == DustAction::AddDustToFee
642                    && change_memo.is_none())
643            {
644                vec![OutputManifest::ZERO, target_change_counts]
645            } else {
646                vec![target_change_counts]
647            }
648        };
649
650        check_for_uneconomic_inputs(
651            transparent_inputs,
652            transparent_outputs,
653            sapling,
654            #[cfg(feature = "orchard")]
655            orchard,
656            #[cfg(feature = "orchard")]
657            ironwood,
658            #[cfg(feature = "orchard")]
659            orchard_padding,
660            #[cfg(feature = "orchard")]
661            _anchor_height,
662            #[cfg(feature = "orchard")]
663            _zip318,
664            cfg.marginal_fee,
665            cfg.grace_actions,
666            &possible_change[..],
667            ephemeral_balance,
668        )?;
669    }
670
671    #[allow(unused_mut)]
672    let (mut change, fee) = match total_in.cmp(&total_out_with_min_fee) {
673        Ordering::Less => {
674            // Case 1. Insufficient input value exists to pay the minimum fee; there's no way
675            // we can construct the transaction.
676            return Err(ChangeError::InsufficientFunds {
677                available: total_in,
678                required: total_out_with_min_fee,
679            });
680        }
681        Ordering::Equal if fully_transparent => {
682            // Case 2 for a tx with all transparent flows and no change memo
683            // (e.g. the second transaction of a ZIP 320 pair).
684            (vec![], min_fee)
685        }
686        _ => {
687            let max_fee = max(
688                min_fee,
689                cfg.fee_rule
690                    .fee_required(
691                        cfg.params,
692                        BlockHeight::from(target_height),
693                        transparent_input_sizes.clone(),
694                        transparent_output_sizes
695                            .clone()
696                            // Count the change output when change is to be returned to the
697                            // transparent pool.
698                            .chain(transparent_change_size),
699                        sapling_input_count,
700                        sapling_output_count(target_change_counts.sapling())?,
701                        orchard_action_count(target_change_counts.orchard())?,
702                        ironwood_action_count(target_change_counts)?,
703                    )
704                    .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?,
705            );
706
707            let total_out_with_max_fee = (subtotal_out + max_fee).ok_or_else(overflow)?;
708
709            // We obtain a split count based on the total number of notes of sufficient size
710            // available in the wallet, irrespective of pool. If we don't have any wallet metadata
711            // available, we fall back to generating a single change output. Transparent change is
712            // always emitted as a single output.
713            let split_count = if wants_transparent_change {
714                1
715            } else {
716                usize::from(wallet_meta.map_or(NonZeroUsize::MIN, |wm| {
717                    cfg.split_policy.split_count(
718                        wm.total_note_count(),
719                        wm.total_value(),
720                        // We use a saturating subtraction here because there may be insufficient funds to pay
721                        // the fee, *if* the requested number of split outputs are created. If there is no
722                        // proposed change, the split policy should recommend only a single change output.
723                        (total_in - total_out_with_max_fee).unwrap_or(Zatoshis::ZERO),
724                    )
725                }))
726            };
727
728            // If we don't have as many change outputs as we expected, recompute the fee.
729            let total_fee = if split_count < target_change_count {
730                cfg.fee_rule
731                    .fee_required(
732                        cfg.params,
733                        BlockHeight::from(target_height),
734                        transparent_input_sizes,
735                        transparent_output_sizes,
736                        sapling_input_count,
737                        sapling_output_count(if change_pool == ShieldedPool::Sapling {
738                            split_count
739                        } else {
740                            0
741                        })?,
742                        orchard_action_count(if change_pool == ShieldedPool::Orchard {
743                            split_count
744                        } else {
745                            0
746                        })?,
747                        ironwood_action_count(OutputManifest::for_pool(change_pool, split_count))?,
748                    )
749                    .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?
750            } else {
751                max_fee
752            };
753
754            let total_out = (subtotal_out + total_fee).ok_or_else(overflow)?;
755            let total_change =
756                (total_in - total_out).ok_or_else(|| ChangeError::InsufficientFunds {
757                    available: total_in,
758                    required: total_out,
759                })?;
760
761            let per_output_change = total_change.div_with_remainder(
762                NonZeroU64::new(u64::try_from(split_count).expect("usize fits into u64")).unwrap(),
763            );
764            let simple_case = || {
765                #[cfg(feature = "transparent-inputs")]
766                if wants_transparent_change {
767                    return (
768                        if total_change.is_zero() {
769                            // A zero-valued transparent output would be unspendable, so we omit
770                            // it. Unlike the shielded change case, omitting the output does not
771                            // reveal additional information, because transparent output values
772                            // are already publicly visible.
773                            vec![]
774                        } else {
775                            vec![ChangeValue::transparent(total_change)]
776                        },
777                        total_fee,
778                    );
779                }
780
781                (
782                    (0usize..split_count)
783                        .map(|i| {
784                            ChangeValue::shielded(
785                                change_pool,
786                                if i == 0 {
787                                    // Add any remainder to the first output only
788                                    (*per_output_change.quotient() + *per_output_change.remainder())
789                                        .unwrap()
790                                } else {
791                                    // For any other output, the change value will just be the
792                                    // quotient.
793                                    *per_output_change.quotient()
794                                },
795                                change_memo.cloned(),
796                            )
797                        })
798                        .collect(),
799                    total_fee,
800                )
801            };
802
803            let change_dust_threshold = cfg
804                .dust_output_policy
805                .dust_threshold()
806                .unwrap_or(cfg.default_dust_threshold);
807
808            if total_change < change_dust_threshold {
809                match cfg.dust_output_policy.action() {
810                    DustAction::Reject => {
811                        // Always allow zero-valued change even for the `Reject` policy:
812                        // * it should be allowed in order to record change memos and to improve
813                        //   indistinguishability;
814                        // * this case occurs in practice when sending all funds from an account;
815                        // * zero-valued notes do not require witness tracking;
816                        // * the effect on trial decryption overhead is small.
817                        if total_change.is_zero() {
818                            simple_case()
819                        } else {
820                            let shortfall =
821                                (change_dust_threshold - total_change).ok_or_else(underflow)?;
822
823                            return Err(ChangeError::InsufficientFunds {
824                                available: total_in,
825                                required: (total_in + shortfall).ok_or_else(overflow)?,
826                            });
827                        }
828                    }
829                    DustAction::AllowDustChange => simple_case(),
830                    DustAction::AddDustToFee => {
831                        // Zero-valued change is also always allowed for this policy, but when
832                        // no change memo is given, we might omit the change output instead.
833                        let fee_with_dust = (total_change + total_fee).ok_or_else(overflow)?;
834
835                        let reasonable_fee =
836                            (total_fee + (MINIMUM_FEE * 10u64).unwrap()).ok_or_else(overflow)?;
837
838                        if fee_with_dust > reasonable_fee {
839                            // Defend against losing money by using AddDustToFee with a too-high
840                            // dust threshold.
841                            simple_case()
842                        } else if change_memo.is_some() {
843                            (
844                                vec![ChangeValue::shielded(
845                                    change_pool,
846                                    Zatoshis::ZERO,
847                                    change_memo.cloned(),
848                                )],
849                                fee_with_dust,
850                            )
851                        } else {
852                            (vec![], fee_with_dust)
853                        }
854                    }
855                }
856            } else {
857                simple_case()
858            }
859        }
860    };
861
862    #[cfg(feature = "transparent-inputs")]
863    change.extend(
864        ephemeral_balance
865            .and_then(|b| b.ephemeral_output_amount())
866            .map(ChangeValue::ephemeral_transparent),
867    );
868
869    for change_value in &change {
870        if !can_receive_change(change_receivers, change_value) {
871            return Err(ChangeError::UnsupportedChangePool(
872                change_value.output_pool(),
873            ));
874        }
875    }
876
877    // Record the exact number of dummy outputs in each shielded bundle. This is transaction
878    // shape, rather than a builder policy; it can therefore be serialized in a proposal and
879    // reproduced by every construction path without re-running the fee model.
880    let final_change = OutputManifest {
881        transparent: change
882            .iter()
883            .filter(|c| c.output_pool() == PoolType::TRANSPARENT)
884            .count(),
885        sapling: change
886            .iter()
887            .filter(|c| c.output_pool() == PoolType::SAPLING)
888            .count(),
889        orchard: change
890            .iter()
891            .filter(|c| c.output_pool() == PoolType::ORCHARD)
892            .count(),
893        ironwood: change
894            .iter()
895            .filter(|c| c.output_pool() == PoolType::IRONWOOD)
896            .count(),
897    };
898    let sapling_real_outputs = sapling.outputs().len() + final_change.sapling();
899    let sapling_dummy_outputs = sapling_output_count(final_change.sapling())?
900        .checked_sub(sapling_real_outputs)
901        .expect("the Sapling action count includes every real output");
902    #[cfg(feature = "orchard")]
903    let orchard_dummy_outputs = orchard_action_count(final_change.orchard())?
904        .checked_sub(orchard.outputs().len() + final_change.orchard())
905        .expect("the Orchard action count includes every real output");
906    #[cfg(feature = "orchard")]
907    let ironwood_dummy_outputs = ironwood_action_count(final_change)?
908        .checked_sub(ironwood.outputs().len() + final_change.ironwood())
909        .expect("the Ironwood action count includes every real output");
910
911    TransactionBalance::new(change, fee)
912        .map(|balance| {
913            balance.with_dummy_outputs(DummyOutputCounts::new(
914                sapling_dummy_outputs,
915                #[cfg(feature = "orchard")]
916                orchard_dummy_outputs,
917                #[cfg(feature = "orchard")]
918                ironwood_dummy_outputs,
919            ))
920        })
921        .map_err(|_| overflow())
922}
923
924/// Returns whether an account that can receive change in `change_receivers` can receive
925/// `change`. Ephemeral outputs are sent to P2PKH addresses.
926fn can_receive_change(change_receivers: &ChangeReceivers, change: &ChangeValue) -> bool {
927    #[cfg(feature = "transparent-inputs")]
928    if change.is_ephemeral() {
929        return matches!(
930            change_receivers.transparent(),
931            Some(TransparentChangeKind::P2pkh)
932        );
933    }
934    change_receivers.can_receive(change.output_pool())
935}
936
937/// Returns a `[ChangeStrategy::DustInputs]` error if some of the inputs provided
938/// to the transaction have value less than or equal to the marginal fee, and could not be
939/// determined to have any economic value in the context of this input selection.
940///
941/// This determination is potentially conservative in the sense that outputs
942/// with value less than or equal to the marginal fee might be excluded, even though in
943/// practice they would not cause the fee to increase. Outputs with value
944/// greater than the marginal fee will never be excluded.
945///
946/// `possible_change` is a slice of [`OutputManifest`] values indicating possible
947/// combinations of how many change outputs (0 or 1) might be included in the
948/// transaction for each pool. The shape of the manifest does not depend on which
949/// protocol features are enabled.
950#[allow(clippy::too_many_arguments)]
951pub(crate) fn check_for_uneconomic_inputs<NoteRefT: Clone, E>(
952    transparent_inputs: &[impl transparent::InputView],
953    transparent_outputs: &[impl transparent::OutputView],
954    sapling: &impl sapling_fees::BundleView<NoteRefT>,
955    #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
956    #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
957    // The Orchard-pool bundle type the builder will use; the action counts computed
958    // for the grace-input check must match it (see `single_pool_output_balance`).
959    #[cfg(feature = "orchard")] orchard_padding: BundlePadding,
960    #[cfg(feature = "orchard")] anchor_height: BlockHeight,
961    #[cfg(feature = "orchard")] zip318: &PoolMigrationParams,
962    marginal_fee: Zatoshis,
963    grace_actions: usize,
964    possible_change: &[OutputManifest],
965    ephemeral_balance: Option<EphemeralBalance>,
966) -> Result<(), ChangeError<E, NoteRefT>> {
967    let mut t_dust: Vec<_> = transparent_inputs
968        .iter()
969        .filter_map(|i| {
970            // For now, we're just assuming P2PKH inputs, so we don't check the
971            // size of the input script.
972            if i.coin().value() <= marginal_fee {
973                Some(i.outpoint().clone())
974            } else {
975                None
976            }
977        })
978        .collect();
979
980    let mut s_dust: Vec<_> = sapling
981        .inputs()
982        .iter()
983        .filter_map(|i| {
984            if sapling_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
985                Some(sapling_fees::InputView::<NoteRefT>::note_id(i).clone())
986            } else {
987                None
988            }
989        })
990        .collect();
991
992    #[cfg(feature = "orchard")]
993    let mut o_dust: Vec<NoteRefT> = orchard
994        .inputs()
995        .iter()
996        .filter_map(|i| {
997            if orchard_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
998                Some(orchard_fees::InputView::<NoteRefT>::note_id(i).clone())
999            } else {
1000                None
1001            }
1002        })
1003        .collect();
1004    #[cfg(not(feature = "orchard"))]
1005    let mut o_dust: Vec<NoteRefT> = vec![];
1006
1007    #[cfg(feature = "orchard")]
1008    let mut i_dust: Vec<NoteRefT> = ironwood
1009        .inputs()
1010        .iter()
1011        .filter_map(|i| {
1012            if orchard_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
1013                Some(orchard_fees::InputView::<NoteRefT>::note_id(i).clone())
1014            } else {
1015                None
1016            }
1017        })
1018        .collect();
1019    #[cfg(not(feature = "orchard"))]
1020    let mut i_dust: Vec<NoteRefT> = vec![];
1021
1022    // If we don't have any dust inputs, there is nothing to check.
1023    if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() && i_dust.is_empty() {
1024        return Ok(());
1025    }
1026
1027    let (t_inputs_len, t_outputs_len) = (
1028        transparent_inputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_input())),
1029        transparent_outputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_output())),
1030    );
1031    let (s_inputs_len, s_outputs_len) = (sapling.inputs().len(), sapling.outputs().len());
1032    #[cfg(feature = "orchard")]
1033    let (o_inputs_len, o_outputs_len) = (orchard.inputs().len(), orchard.outputs().len());
1034    #[cfg(not(feature = "orchard"))]
1035    let (o_inputs_len, o_outputs_len) = (0usize, 0usize);
1036    #[cfg(feature = "orchard")]
1037    let (i_inputs_len, i_outputs_len) = (ironwood.inputs().len(), ironwood.outputs().len());
1038    #[cfg(not(feature = "orchard"))]
1039    let (i_inputs_len, i_outputs_len) = (0usize, 0usize);
1040
1041    let t_non_dust = t_inputs_len.checked_sub(t_dust.len()).unwrap();
1042    let s_non_dust = s_inputs_len.checked_sub(s_dust.len()).unwrap();
1043    let o_non_dust = o_inputs_len.checked_sub(o_dust.len()).unwrap();
1044    let i_non_dust = i_inputs_len.checked_sub(i_dust.len()).unwrap();
1045
1046    // Return the number of allowed dust inputs from each pool.
1047    let allowed_dust = |change: &OutputManifest| {
1048        // Here we assume a "ZIP 317-like" fee model in which the existence of an output
1049        // to a given pool implies that a corresponding input from that pool can be
1050        // provided without increasing the fee. (This is also likely to be true for
1051        // future fee models if we do not want to penalize use of Orchard relative to
1052        // other pools.)
1053        //
1054        // Under that assumption, we want to calculate the maximum number of dust inputs
1055        // from each pool, out of the ones we actually have, that can be economically
1056        // spent along with the non-dust inputs. Get an initial estimate by calculating
1057        // the number of dust inputs in each pool that will be allowed regardless of
1058        // padding or grace.
1059
1060        let t_allowed = min(
1061            t_dust.len(),
1062            (t_outputs_len + change.transparent).saturating_sub(t_non_dust),
1063        );
1064        let s_allowed = min(
1065            s_dust.len(),
1066            (s_outputs_len + change.sapling).saturating_sub(s_non_dust),
1067        );
1068        let o_allowed = min(
1069            o_dust.len(),
1070            (o_outputs_len + change.orchard).saturating_sub(o_non_dust),
1071        );
1072        let i_allowed = min(
1073            i_dust.len(),
1074            (i_outputs_len + change.ironwood).saturating_sub(i_non_dust),
1075        );
1076
1077        // We'll be spending the non-dust and allowed dust in each pool.
1078        let t_req_inputs = t_non_dust + t_allowed;
1079        let s_req_inputs = s_non_dust + s_allowed;
1080        #[cfg(feature = "orchard")]
1081        let o_req_inputs = o_non_dust + o_allowed;
1082        #[cfg(feature = "orchard")]
1083        let i_req_inputs = i_non_dust + i_allowed;
1084
1085        // This calculates the hypothetical number of actions with given extra inputs,
1086        // for ZIP 317 and the padding rules in effect. The padding rules for each
1087        // pool are subtle (they also depend on `bundle_required` for example), so we
1088        // must actually call them rather than try to predict their effect. To tell
1089        // whether we can freely add an extra input from a given pool, we need to call
1090        // them both with and without that input; if the number of actions does not
1091        // increase, then the input is free to add.
1092        let hypothetical_actions = |t_extra, s_extra, _o_extra, _i_extra| {
1093            let s_spend_count = sapling
1094                .bundle_type()
1095                .num_spends(s_req_inputs + s_extra)
1096                .map_err(ChangeError::BundleError)?;
1097
1098            let s_output_count = sapling
1099                .bundle_type()
1100                .num_outputs(s_req_inputs + s_extra, s_outputs_len + change.sapling)
1101                .map_err(ChangeError::BundleError)?;
1102
1103            #[cfg(feature = "orchard")]
1104            let o_action_count = orchard_fees::transactional_action_count(
1105                orchard_padding.bundle_type(),
1106                orchard.bundle_version(),
1107                o_req_inputs + _o_extra,
1108                o_outputs_len + change.orchard,
1109            )
1110            .map_err(ChangeError::BundleError)?;
1111            #[cfg(not(feature = "orchard"))]
1112            let o_action_count = 0;
1113
1114            // The Ironwood padding is derived here on the same rule as in
1115            // `single_pool_output_balance`, over this hypothetical change manifest rather than the
1116            // chosen one, so that the two agree about what each candidate would cost.
1117            #[cfg(feature = "orchard")]
1118            let i_padding = {
1119                let constants = zip318;
1120                let canonical = o_req_inputs + _o_extra == 1
1121                    && i_req_inputs + _i_extra == 0
1122                    && change.ironwood == 0
1123                    && match ironwood.outputs() {
1124                        [output] => constants.is_canonical_denomination(output.value()),
1125                        _ => false,
1126                    }
1127                    && constants.anchor_grid().is_boundary(anchor_height);
1128                if canonical {
1129                    BundlePadding::UNPADDED
1130                } else {
1131                    BundlePadding::DEFAULT
1132                }
1133            };
1134            #[cfg(feature = "orchard")]
1135            let i_action_count = orchard_fees::transactional_action_count(
1136                i_padding.bundle_type(),
1137                ironwood.bundle_version(),
1138                i_req_inputs + _i_extra,
1139                i_outputs_len + change.ironwood,
1140            )
1141            .map_err(ChangeError::BundleError)?;
1142            #[cfg(not(feature = "orchard"))]
1143            let i_action_count = 0;
1144
1145            // To calculate the number of unused actions, we assume that transparent inputs
1146            // and outputs are P2PKH.
1147            Ok(
1148                max(t_req_inputs + t_extra, t_outputs_len + change.transparent)
1149                    + max(s_spend_count, s_output_count)
1150                    + o_action_count
1151                    + i_action_count,
1152            )
1153        };
1154
1155        // First calculate the baseline number of logical actions with only the definitely
1156        // allowed inputs estimated above. If it is less than `grace_actions`, try to allocate
1157        // a grace input first to transparent dust, then to Sapling dust, then to Orchard
1158        // dust, then to Ironwood dust. If the number of actions increases, it was not
1159        // possible to allocate that input for free. This approach is sufficient because at
1160        // most one such input can be allocated, since `grace_actions` is at most 2 for
1161        // ZIP 317 and there must be at least one logical action. (If `grace_actions` were
1162        // greater than 2 then the code would still be correct, it would just not find all
1163        // potential extra inputs.)
1164
1165        let baseline = hypothetical_actions(0, 0, 0, 0)?;
1166
1167        let (t_extra, s_extra, o_extra, i_extra) = if baseline >= grace_actions {
1168            (0, 0, 0, 0)
1169        } else if t_dust.len() > t_allowed && hypothetical_actions(1, 0, 0, 0)? <= baseline {
1170            (1, 0, 0, 0)
1171        } else if s_dust.len() > s_allowed && hypothetical_actions(0, 1, 0, 0)? <= baseline {
1172            (0, 1, 0, 0)
1173        } else if o_dust.len() > o_allowed && hypothetical_actions(0, 0, 1, 0)? <= baseline {
1174            (0, 0, 1, 0)
1175        } else if i_dust.len() > i_allowed && hypothetical_actions(0, 0, 0, 1)? <= baseline {
1176            (0, 0, 0, 1)
1177        } else {
1178            (0, 0, 0, 0)
1179        };
1180        Ok(OutputManifest {
1181            transparent: t_allowed + t_extra,
1182            sapling: s_allowed + s_extra,
1183            orchard: o_allowed + o_extra,
1184            ironwood: i_allowed + i_extra,
1185        })
1186    };
1187
1188    // Find the least number of allowed dust inputs for each pool for any `possible_change`.
1189    let allowed = possible_change
1190        .iter()
1191        .map(allowed_dust)
1192        .collect::<Result<Vec<_>, _>>()?
1193        .into_iter()
1194        .reduce(|l, r| OutputManifest {
1195            transparent: min(l.transparent, r.transparent),
1196            sapling: min(l.sapling, r.sapling),
1197            orchard: min(l.orchard, r.orchard),
1198            ironwood: min(l.ironwood, r.ironwood),
1199        })
1200        .expect("possible_change is nonempty");
1201
1202    // The inputs in the tail of each list after the first `*_allowed` are returned as uneconomic.
1203    // The caller should order the inputs from most to least preferred to spend.
1204    let t_dust = t_dust.split_off(allowed.transparent);
1205    let s_dust = s_dust.split_off(allowed.sapling);
1206    let o_dust = o_dust.split_off(allowed.orchard);
1207    let i_dust = i_dust.split_off(allowed.ironwood);
1208
1209    if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() && i_dust.is_empty() {
1210        Ok(())
1211    } else {
1212        Err(ChangeError::DustInputs {
1213            transparent: t_dust,
1214            sapling: s_dust,
1215            #[cfg(feature = "orchard")]
1216            orchard: o_dust,
1217            #[cfg(feature = "orchard")]
1218            ironwood: i_dust,
1219        })
1220    }
1221}
1222
1223#[cfg(all(test, feature = "orchard"))]
1224mod tests {
1225    use super::{NetFlows, select_change_pool};
1226    use crate::data_api::ChangeReceivers;
1227    use zcash_protocol::{ShieldedPool, value::Zatoshis};
1228
1229    /// Returns the change receivers of an account with the given shielded keys and no
1230    /// transparent key.
1231    fn receivers(sapling: bool, orchard: bool) -> ChangeReceivers {
1232        ChangeReceivers::new(
1233            sapling,
1234            orchard,
1235            #[cfg(feature = "transparent-inputs")]
1236            None,
1237        )
1238    }
1239
1240    /// Returns the change receivers of an account with a key in each shielded pool.
1241    fn all_receivers() -> ChangeReceivers {
1242        receivers(true, true)
1243    }
1244
1245    fn flows(orchard_in: u64, ironwood_in: u64, sapling_in: u64) -> NetFlows {
1246        NetFlows {
1247            t_in: Zatoshis::ZERO,
1248            t_out: Zatoshis::ZERO,
1249            sapling_in: Zatoshis::const_from_u64(sapling_in),
1250            sapling_out: Zatoshis::ZERO,
1251            orchard_in: Zatoshis::const_from_u64(orchard_in),
1252            orchard_out: Zatoshis::ZERO,
1253            ironwood_in: Zatoshis::const_from_u64(ironwood_in),
1254            ironwood_out: Zatoshis::ZERO,
1255        }
1256    }
1257
1258    #[test]
1259    fn select_change_pool_routes_ironwood_spend_to_ironwood() {
1260        // Spending Ironwood funds (no Orchard or Sapling flows) sends change to Ironwood,
1261        // keeping it in the pool being spent rather than crossing back into Orchard.
1262        assert_eq!(
1263            select_change_pool(
1264                &flows(0, 10_000, 0),
1265                ShieldedPool::Sapling,
1266                &all_receivers(),
1267                true,
1268                Zatoshis::const_from_u64(5_000)
1269            ),
1270            ShieldedPool::Ironwood
1271        );
1272
1273        // Spending Orchard funds keeps change in Orchard even when Ironwood funds are also
1274        // spent, so that Ironwood-routed change cannot reveal the spent Orchard notes' balances.
1275        assert_eq!(
1276            select_change_pool(
1277                &flows(10_000, 10_000, 0),
1278                ShieldedPool::Sapling,
1279                &all_receivers(),
1280                true,
1281                Zatoshis::const_from_u64(5_000)
1282            ),
1283            ShieldedPool::Orchard
1284        );
1285
1286        // A combined Sapling + Ironwood spend routes change to Ironwood (Ironwood is preferred
1287        // over Sapling).
1288        assert_eq!(
1289            select_change_pool(
1290                &flows(0, 10_000, 10_000),
1291                ShieldedPool::Sapling,
1292                &all_receivers(),
1293                true,
1294                Zatoshis::const_from_u64(5_000)
1295            ),
1296            ShieldedPool::Ironwood
1297        );
1298
1299        // A Sapling-only spend keeps change in Sapling.
1300        assert_eq!(
1301            select_change_pool(
1302                &flows(0, 0, 10_000),
1303                ShieldedPool::Orchard,
1304                &all_receivers(),
1305                true,
1306                Zatoshis::const_from_u64(5_000)
1307            ),
1308            ShieldedPool::Sapling
1309        );
1310    }
1311
1312    #[test]
1313    fn select_change_pool_enforces_orchard_turnstile() {
1314        // Before Ironwood activation, Orchard-spend change stays in Orchard regardless of
1315        // the change bound: value may freely enter the pool.
1316        assert_eq!(
1317            select_change_pool(
1318                &flows(10_000, 0, 10_000),
1319                ShieldedPool::Sapling,
1320                &all_receivers(),
1321                false,
1322                Zatoshis::const_from_u64(15_000)
1323            ),
1324            ShieldedPool::Orchard
1325        );
1326
1327        // After activation, change may return to Orchard while the pool balance strictly
1328        // decreases: the change bound is below the Orchard input value.
1329        assert_eq!(
1330            select_change_pool(
1331                &flows(10_000, 0, 10_000),
1332                ShieldedPool::Sapling,
1333                &all_receivers(),
1334                true,
1335                Zatoshis::const_from_u64(9_999)
1336            ),
1337            ShieldedPool::Orchard
1338        );
1339
1340        // After activation, change that could equal or exceed the Orchard input value would
1341        // grow the pool, so it flows onward to Ironwood instead.
1342        assert_eq!(
1343            select_change_pool(
1344                &flows(10_000, 0, 10_000),
1345                ShieldedPool::Sapling,
1346                &all_receivers(),
1347                true,
1348                Zatoshis::const_from_u64(10_000)
1349            ),
1350            ShieldedPool::Ironwood
1351        );
1352
1353        // A post-activation Orchard fallback for transparent-only flows is corrected to
1354        // Ironwood: with no Orchard inputs, no value may enter the Orchard pool.
1355        assert_eq!(
1356            select_change_pool(
1357                &NetFlows {
1358                    t_in: Zatoshis::const_from_u64(10_000),
1359                    t_out: Zatoshis::ZERO,
1360                    sapling_in: Zatoshis::ZERO,
1361                    sapling_out: Zatoshis::ZERO,
1362                    orchard_in: Zatoshis::ZERO,
1363                    orchard_out: Zatoshis::ZERO,
1364                    ironwood_in: Zatoshis::ZERO,
1365                    ironwood_out: Zatoshis::ZERO,
1366                },
1367                ShieldedPool::Orchard,
1368                &all_receivers(),
1369                true,
1370                Zatoshis::const_from_u64(10_000)
1371            ),
1372            ShieldedPool::Ironwood
1373        );
1374    }
1375
1376    #[test]
1377    fn select_change_pool_uses_a_pool_the_account_can_receive() {
1378        let orchard_out = |sapling_in: u64, orchard_out: u64| NetFlows {
1379            sapling_in: Zatoshis::const_from_u64(sapling_in),
1380            orchard_out: Zatoshis::const_from_u64(orchard_out),
1381            ..flows(0, 0, 0)
1382        };
1383
1384        // A Sapling-only account that pays an Orchard recipient from Sapling notes gets
1385        // Sapling change, not Orchard change.
1386        assert_eq!(
1387            select_change_pool(
1388                &orchard_out(10_000, 5_000),
1389                ShieldedPool::Orchard,
1390                &receivers(true, false),
1391                false,
1392                Zatoshis::const_from_u64(4_000)
1393            ),
1394            ShieldedPool::Sapling
1395        );
1396
1397        // An Orchard-only account that spends Sapling notes gets Orchard change before
1398        // Ironwood activation.
1399        assert_eq!(
1400            select_change_pool(
1401                &flows(0, 0, 10_000),
1402                ShieldedPool::Sapling,
1403                &receivers(false, true),
1404                false,
1405                Zatoshis::const_from_u64(5_000)
1406            ),
1407            ShieldedPool::Orchard
1408        );
1409
1410        // After activation, that change cannot enter the Orchard pool, so it goes to the
1411        // Ironwood pool, which uses the same key.
1412        assert_eq!(
1413            select_change_pool(
1414                &flows(0, 0, 10_000),
1415                ShieldedPool::Sapling,
1416                &receivers(false, true),
1417                true,
1418                Zatoshis::const_from_u64(5_000)
1419            ),
1420            ShieldedPool::Ironwood
1421        );
1422
1423        // For transparent flows, a fallback pool that the account cannot receive in is
1424        // replaced by one that it can receive in.
1425        let transparent_flows = NetFlows {
1426            t_in: Zatoshis::const_from_u64(10_000),
1427            ..flows(0, 0, 0)
1428        };
1429        assert_eq!(
1430            select_change_pool(
1431                &transparent_flows,
1432                ShieldedPool::Orchard,
1433                &receivers(true, false),
1434                true,
1435                Zatoshis::const_from_u64(5_000)
1436            ),
1437            ShieldedPool::Sapling
1438        );
1439
1440        // An account that can receive in no shielded pool gets the pool that the flows
1441        // indicate. The caller rejects change that would go there.
1442        assert_eq!(
1443            select_change_pool(
1444                &orchard_out(10_000, 5_000),
1445                ShieldedPool::Sapling,
1446                &receivers(false, false),
1447                false,
1448                Zatoshis::const_from_u64(4_000)
1449            ),
1450            ShieldedPool::Orchard
1451        );
1452    }
1453}