Skip to main content

zcash_client_backend/fees/
zip317.rs

1//! Change strategies designed to implement the ZIP 317 fee rules.
2//!
3//! Change selection in ZIP 317 requires careful handling of low-valued inputs
4//! to ensure that inputs added to a transaction do not cause fees to rise by
5//! an amount greater than their value.
6
7use core::marker::PhantomData;
8
9use zcash_primitives::transaction::fees::{FeeRule, transparent, zip317 as prim_zip317};
10use zcash_protocol::{
11    ShieldedPool,
12    consensus::{self, BlockHeight},
13    memo::MemoBytes,
14    value::{BalanceError, Zatoshis},
15};
16
17use crate::{
18    data_api::{
19        AccountMeta, ChangeReceivers, InputSource, NoteFilter,
20        anchor_retention::PoolMigrationParams,
21        wallet::{
22            TargetHeight,
23            input_selection::{LockFilter, LockedInputPolicy},
24        },
25    },
26    fees::StandardFeeRule,
27};
28
29use super::{
30    ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, MetaSource, SplitPolicy,
31    TransactionBalance,
32    common::{SinglePoolBalanceConfig, single_pool_output_balance},
33    sapling as sapling_fees,
34};
35
36#[cfg(feature = "transparent-inputs")]
37use super::TransparentChangePolicy;
38#[cfg(feature = "orchard")]
39use {super::orchard as orchard_fees, zcash_primitives::transaction::builder::BundlePadding};
40
41/// An extension to the [`FeeRule`] trait that exposes methods required for
42/// ZIP 317 fee calculation.
43pub trait Zip317FeeRule: FeeRule {
44    /// Returns the ZIP 317 marginal fee.
45    fn marginal_fee(&self) -> Zatoshis;
46
47    /// Returns the ZIP 317 number of grace actions
48    fn grace_actions(&self) -> usize;
49}
50
51impl Zip317FeeRule for prim_zip317::FeeRule {
52    fn marginal_fee(&self) -> Zatoshis {
53        self.marginal_fee()
54    }
55
56    fn grace_actions(&self) -> usize {
57        self.grace_actions()
58    }
59}
60
61impl Zip317FeeRule for StandardFeeRule {
62    fn marginal_fee(&self) -> Zatoshis {
63        prim_zip317::FeeRule::standard().marginal_fee()
64    }
65
66    fn grace_actions(&self) -> usize {
67        prim_zip317::FeeRule::standard().grace_actions()
68    }
69}
70
71/// A change strategy that proposes change as a single output. The output pool is chosen
72/// as the most current pool that avoids unnecessary pool-crossing (with a specified
73/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
74/// to the provided fee rule.
75pub struct SingleOutputChangeStrategy<R, I> {
76    fee_rule: R,
77    change_memo: Option<MemoBytes>,
78    fallback_change_pool: ShieldedPool,
79    dust_output_policy: DustOutputPolicy,
80    #[cfg(feature = "transparent-inputs")]
81    transparent_change_policy: TransparentChangePolicy,
82    meta_source: PhantomData<I>,
83}
84
85impl<R, I> SingleOutputChangeStrategy<R, I> {
86    /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
87    /// fee parameters and change memo.
88    ///
89    /// `fallback_change_pool` is used when more than one shielded pool is enabled via
90    /// feature flags, and the transaction has no shielded inputs.
91    pub fn new(
92        fee_rule: R,
93        change_memo: Option<MemoBytes>,
94        fallback_change_pool: ShieldedPool,
95        dust_output_policy: DustOutputPolicy,
96    ) -> Self {
97        Self {
98            fee_rule,
99            change_memo,
100            fallback_change_pool,
101            dust_output_policy,
102            #[cfg(feature = "transparent-inputs")]
103            transparent_change_policy: TransparentChangePolicy::ShieldChange,
104            meta_source: PhantomData,
105        }
106    }
107
108    /// Sets the [`TransparentChangePolicy`] to be used by this change strategy, determining
109    /// whether change may be returned to the transparent pool when the flows of the transaction
110    /// under construction are fully transparent.
111    ///
112    /// The default is [`TransparentChangePolicy::ShieldChange`]. This policy has no effect on
113    /// transactions that involve any shielded flows.
114    #[cfg(feature = "transparent-inputs")]
115    pub fn with_transparent_change_policy(
116        mut self,
117        transparent_change_policy: TransparentChangePolicy,
118    ) -> Self {
119        self.transparent_change_policy = transparent_change_policy;
120        self
121    }
122}
123
124impl<R, I> ChangeStrategy for SingleOutputChangeStrategy<R, I>
125where
126    R: Zip317FeeRule + Clone,
127    I: MetaSource,
128    <R as FeeRule>::Error: From<BalanceError>,
129{
130    type FeeRule = R;
131    type Error = <R as FeeRule>::Error;
132    type MetaSource = I;
133    type AccountMetaT = ChangeReceivers;
134
135    fn fee_rule(&self) -> &Self::FeeRule {
136        &self.fee_rule
137    }
138
139    fn fetch_wallet_meta(
140        &self,
141        meta_source: &Self::MetaSource,
142        account: <Self::MetaSource as MetaSource>::AccountId,
143        _target_height: TargetHeight,
144        _exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
145    ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error> {
146        meta_source.change_receivers(account)
147    }
148
149    fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
150        &self,
151        params: &P,
152        target_height: TargetHeight,
153        anchor_height: BlockHeight,
154        zip318: &PoolMigrationParams,
155        transparent_inputs: &[impl transparent::InputView],
156        transparent_outputs: &[impl transparent::OutputView],
157        sapling: &impl sapling_fees::BundleView<NoteRefT>,
158        #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
159        #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
160        ephemeral_balance: Option<EphemeralBalance>,
161        wallet_meta: &Self::AccountMetaT,
162    ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
163        let split_policy = SplitPolicy::single_output();
164        let cfg = SinglePoolBalanceConfig::new(
165            params,
166            &self.fee_rule,
167            &self.dust_output_policy,
168            self.fee_rule.marginal_fee(),
169            &split_policy,
170            self.fallback_change_pool,
171            #[cfg(feature = "transparent-inputs")]
172            self.transparent_change_policy,
173            self.fee_rule.marginal_fee(),
174            self.fee_rule.grace_actions(),
175        );
176
177        single_pool_output_balance(
178            cfg,
179            None,
180            wallet_meta,
181            target_height,
182            transparent_inputs,
183            transparent_outputs,
184            sapling,
185            #[cfg(feature = "orchard")]
186            orchard,
187            #[cfg(feature = "orchard")]
188            ironwood,
189            // The Orchard bundle is always padded to the default floor. Only the Ironwood
190            // bundle's padding varies, and it is derived from the transaction's shape rather
191            // than chosen here.
192            #[cfg(feature = "orchard")]
193            BundlePadding::DEFAULT,
194            anchor_height,
195            zip318,
196            self.change_memo.as_ref(),
197            ephemeral_balance,
198        )
199    }
200}
201
202/// A change strategy that attempts to split the change value into some number of equal-sized notes
203/// as dictated by the included [`SplitPolicy`] value.
204pub struct MultiOutputChangeStrategy<R, I> {
205    fee_rule: R,
206    change_memo: Option<MemoBytes>,
207    fallback_change_pool: ShieldedPool,
208    dust_output_policy: DustOutputPolicy,
209    split_policy: SplitPolicy,
210    #[cfg(feature = "transparent-inputs")]
211    transparent_change_policy: TransparentChangePolicy,
212    meta_source: PhantomData<I>,
213}
214
215impl<R, I> MultiOutputChangeStrategy<R, I> {
216    /// Constructs a new [`MultiOutputChangeStrategy`] with the specified ZIP 317
217    /// fee parameters, change memo, and change splitting policy.
218    ///
219    /// This change strategy will fall back to creating a single change output if insufficient
220    /// change value is available to create notes with at least the minimum value dictated by the
221    /// split policy.
222    ///
223    /// - `fallback_change_pool`: the pool to which change will be sent if when more than one
224    ///   shielded pool is enabled via feature flags, and the transaction has no shielded inputs.
225    /// - `split_policy`: A policy value describing how the change value should be returned as
226    ///   multiple notes.
227    pub fn new(
228        fee_rule: R,
229        change_memo: Option<MemoBytes>,
230        fallback_change_pool: ShieldedPool,
231        dust_output_policy: DustOutputPolicy,
232        split_policy: SplitPolicy,
233    ) -> Self {
234        Self {
235            fee_rule,
236            change_memo,
237            fallback_change_pool,
238            dust_output_policy,
239            split_policy,
240            #[cfg(feature = "transparent-inputs")]
241            transparent_change_policy: TransparentChangePolicy::ShieldChange,
242            meta_source: PhantomData,
243        }
244    }
245
246    /// Sets the [`TransparentChangePolicy`] to be used by this change strategy, determining
247    /// whether change may be returned to the transparent pool when the flows of the transaction
248    /// under construction are fully transparent.
249    ///
250    /// The default is [`TransparentChangePolicy::ShieldChange`]. This policy has no effect on
251    /// transactions that involve any shielded flows. When transparent change is produced, it is
252    /// always emitted as a single output; the [`SplitPolicy`] configured for this strategy applies
253    /// only to shielded change.
254    #[cfg(feature = "transparent-inputs")]
255    pub fn with_transparent_change_policy(
256        mut self,
257        transparent_change_policy: TransparentChangePolicy,
258    ) -> Self {
259        self.transparent_change_policy = transparent_change_policy;
260        self
261    }
262}
263
264impl<R, I> ChangeStrategy for MultiOutputChangeStrategy<R, I>
265where
266    R: Zip317FeeRule + Clone,
267    I: InputSource,
268    <R as FeeRule>::Error: From<BalanceError>,
269{
270    type FeeRule = R;
271    type Error = <R as FeeRule>::Error;
272    type MetaSource = I;
273    type AccountMetaT = AccountMeta;
274
275    fn fee_rule(&self) -> &Self::FeeRule {
276        &self.fee_rule
277    }
278
279    fn fetch_wallet_meta(
280        &self,
281        meta_source: &Self::MetaSource,
282        account: <Self::MetaSource as InputSource>::AccountId,
283        target_height: TargetHeight,
284        exclude: &[<Self::MetaSource as InputSource>::NoteRef],
285    ) -> Result<Self::AccountMetaT, <Self::MetaSource as InputSource>::Error> {
286        let note_selector = NoteFilter::ExceedsMinValue(
287            self.split_policy
288                .min_split_output_value()
289                .unwrap_or(SplitPolicy::MIN_NOTE_VALUE),
290        );
291
292        // Account metadata feeds change-splitting decisions, which reason about the
293        // notes that selection can actually draw on; locked notes are excluded from
294        // selection, so they are excluded here as well.
295        meta_source.get_account_metadata(
296            account,
297            &note_selector,
298            target_height,
299            exclude,
300            LockFilter::Policy(&LockedInputPolicy::Exclude),
301        )
302    }
303
304    fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
305        &self,
306        params: &P,
307        target_height: TargetHeight,
308        anchor_height: BlockHeight,
309        zip318: &PoolMigrationParams,
310        transparent_inputs: &[impl transparent::InputView],
311        transparent_outputs: &[impl transparent::OutputView],
312        sapling: &impl sapling_fees::BundleView<NoteRefT>,
313        #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
314        #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
315        ephemeral_balance: Option<EphemeralBalance>,
316        wallet_meta: &Self::AccountMetaT,
317    ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
318        let cfg = SinglePoolBalanceConfig::new(
319            params,
320            &self.fee_rule,
321            &self.dust_output_policy,
322            self.fee_rule.marginal_fee(),
323            &self.split_policy,
324            self.fallback_change_pool,
325            #[cfg(feature = "transparent-inputs")]
326            self.transparent_change_policy,
327            self.fee_rule.marginal_fee(),
328            self.fee_rule.grace_actions(),
329        );
330
331        single_pool_output_balance(
332            cfg,
333            Some(wallet_meta),
334            wallet_meta.change_receivers(),
335            target_height,
336            transparent_inputs,
337            transparent_outputs,
338            sapling,
339            #[cfg(feature = "orchard")]
340            orchard,
341            #[cfg(feature = "orchard")]
342            ironwood,
343            // The Orchard bundle is always padded to the default floor. Only the Ironwood
344            // bundle's padding varies, and it is derived from the transaction's shape rather
345            // than chosen here.
346            #[cfg(feature = "orchard")]
347            BundlePadding::DEFAULT,
348            anchor_height,
349            zip318,
350            self.change_memo.as_ref(),
351            ephemeral_balance,
352        )
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use crate::fees::tests::all_change_receivers;
359    // `sapling_fees` is named by both the orchard and the transparent-inputs tests.
360    #[cfg(any(feature = "orchard", feature = "transparent-inputs"))]
361    use crate::fees::sapling as sapling_fees;
362
363    #[cfg(feature = "transparent-inputs")]
364    use {
365        crate::{
366            data_api::TransparentChangeKind,
367            fees::{EphemeralBalance, TransparentChangePolicy},
368        },
369        ::transparent::{address::TransparentAddress, bundle::OutPoint},
370        zcash_primitives::transaction::fees::zip317::MINIMUM_FEE,
371    };
372
373    #[cfg(feature = "orchard")]
374    use {
375        crate::{
376            data_api::wallet::{TargetHeight, input_selection::OrchardPayment},
377            fees::{orchard as orchard_fees, tests::TestOrchardInput},
378        },
379        zcash_protocol::zip318::{AnchorBucketInterval, MAX_RESIDUAL_VALUE},
380    };
381
382    use crate::{
383        data_api::{
384            AccountMeta, ChangeReceivers, PoolMeta,
385            anchor_retention::{AnchorRetentionInterval, PoolMigrationParams},
386            testing::MockWalletDb,
387            wallet::input_selection::SaplingPayment,
388        },
389        fees::{
390            ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, SplitPolicy,
391            tests::{TestSaplingInput, TestTransparentInput},
392            zip317::MultiOutputChangeStrategy,
393        },
394    };
395    use core::{convert::Infallible, num::NonZeroUsize};
396    use zcash_protocol::{
397        PoolType, ShieldedPool,
398        consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
399        value::Zatoshis,
400    };
401
402    use ::transparent::{address::Script, bundle::TxOut};
403    use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule;
404
405    use super::SingleOutputChangeStrategy;
406
407    #[test]
408    fn change_without_dust() {
409        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
410            Zip317FeeRule::standard(),
411            None,
412            ShieldedPool::Sapling,
413            DustOutputPolicy::default(),
414        );
415
416        // spend a single Sapling note that is sufficient to pay the fee
417        let result = change_strategy.compute_balance(
418            &Network::TestNetwork,
419            Network::TestNetwork
420                .activation_height(NetworkUpgrade::Nu5)
421                .unwrap()
422                .into(),
423            BlockHeight::from_u32(1),
424            &PoolMigrationParams::new(
425                zcash_protocol::zip318::BlockSpacing::UNIFORM,
426                AnchorRetentionInterval::ZIP_318,
427            ),
428            &[] as &[TestTransparentInput],
429            &[] as &[TxOut],
430            &(
431                sapling::builder::BundleType::DEFAULT,
432                &[TestSaplingInput {
433                    note_id: 0,
434                    value: Zatoshis::const_from_u64(55000),
435                }][..],
436                &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
437            ),
438            #[cfg(feature = "orchard")]
439            &orchard_fees::EmptyBundleView,
440            #[cfg(feature = "orchard")]
441            &orchard_fees::EmptyBundleView,
442            None,
443            &all_change_receivers(),
444        );
445
446        assert_matches!(
447            result,
448            Ok(balance) if
449                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
450                balance.fee_required() == Zatoshis::const_from_u64(10000)
451        );
452    }
453
454    #[test]
455    fn change_without_dust_multi() {
456        let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
457            Zip317FeeRule::standard(),
458            None,
459            ShieldedPool::Sapling,
460            DustOutputPolicy::default(),
461            SplitPolicy::with_min_output_value(
462                NonZeroUsize::new(5).unwrap(),
463                Zatoshis::const_from_u64(100_0000),
464            ),
465        );
466
467        {
468            // spend a single Sapling note and produce 5 outputs
469            let balance = |existing_notes, total| {
470                change_strategy.compute_balance(
471                    &Network::TestNetwork,
472                    Network::TestNetwork
473                        .activation_height(NetworkUpgrade::Nu5)
474                        .unwrap()
475                        .into(),
476                    BlockHeight::from_u32(1),
477                    &PoolMigrationParams::new(
478                        zcash_protocol::zip318::BlockSpacing::UNIFORM,
479                        AnchorRetentionInterval::ZIP_318,
480                    ),
481                    &[] as &[TestTransparentInput],
482                    &[] as &[TxOut],
483                    &(
484                        sapling::builder::BundleType::DEFAULT,
485                        &[TestSaplingInput {
486                            note_id: 0,
487                            value: Zatoshis::const_from_u64(750_0000),
488                        }][..],
489                        &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..],
490                    ),
491                    #[cfg(feature = "orchard")]
492                    &orchard_fees::EmptyBundleView,
493                    #[cfg(feature = "orchard")]
494                    &orchard_fees::EmptyBundleView,
495                    None,
496                    &AccountMeta::new(
497                        Some(PoolMeta::new(existing_notes, total)),
498                        None,
499                        None,
500                        all_change_receivers(),
501                    ),
502                )
503            };
504
505            assert_matches!(
506                balance(0, Zatoshis::ZERO),
507                Ok(balance) if
508                    balance.proposed_change() == [
509                        ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
510                        ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
511                        ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
512                        ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
513                        ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
514                    ] &&
515                    balance.fee_required() == Zatoshis::const_from_u64(30000)
516            );
517
518            assert_matches!(
519                balance(2, Zatoshis::const_from_u64(100_0000)),
520                Ok(balance) if
521                    balance.proposed_change() == [
522                        ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
523                        ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
524                        ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
525                    ] &&
526                    balance.fee_required() == Zatoshis::const_from_u64(20000)
527            );
528        }
529
530        {
531            // spend a single Sapling note and produce 4 outputs, as the value of the note isn't
532            // sufficient to produce 5
533            let result = change_strategy.compute_balance(
534                &Network::TestNetwork,
535                Network::TestNetwork
536                    .activation_height(NetworkUpgrade::Nu5)
537                    .unwrap()
538                    .into(),
539                BlockHeight::from_u32(1),
540                &PoolMigrationParams::new(
541                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
542                    AnchorRetentionInterval::ZIP_318,
543                ),
544                &[] as &[TestTransparentInput],
545                &[] as &[TxOut],
546                &(
547                    sapling::builder::BundleType::DEFAULT,
548                    &[TestSaplingInput {
549                        note_id: 0,
550                        value: Zatoshis::const_from_u64(600_0000),
551                    }][..],
552                    &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..],
553                ),
554                #[cfg(feature = "orchard")]
555                &orchard_fees::EmptyBundleView,
556                #[cfg(feature = "orchard")]
557                &orchard_fees::EmptyBundleView,
558                None,
559                &AccountMeta::new(
560                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
561                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
562                    None,
563                    all_change_receivers(),
564                ),
565            );
566
567            assert_matches!(
568                result,
569                Ok(balance) if
570                    balance.proposed_change() == [
571                        ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
572                        ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
573                        ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
574                        ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
575                    ] &&
576                    balance.fee_required() == Zatoshis::const_from_u64(25000)
577            );
578        }
579
580        {
581            // spend a single Sapling note and produce no change outputs, as the value of outputs
582            // has been requested such that it exactly empties the wallet
583            let result = change_strategy.compute_balance(
584                &Network::TestNetwork,
585                Network::TestNetwork
586                    .activation_height(NetworkUpgrade::Nu5)
587                    .unwrap()
588                    .into(),
589                BlockHeight::from_u32(1),
590                &PoolMigrationParams::new(
591                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
592                    AnchorRetentionInterval::ZIP_318,
593                ),
594                &[] as &[TestTransparentInput],
595                &[] as &[TxOut],
596                &(
597                    sapling::builder::BundleType::DEFAULT,
598                    &[TestSaplingInput {
599                        note_id: 0,
600                        value: Zatoshis::const_from_u64(50000),
601                    }][..],
602                    &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
603                ),
604                #[cfg(feature = "orchard")]
605                &orchard_fees::EmptyBundleView,
606                #[cfg(feature = "orchard")]
607                &orchard_fees::EmptyBundleView,
608                None,
609                // after excluding the inputs we're spending, we have no notes in the wallet
610                &AccountMeta::new(
611                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
612                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
613                    None,
614                    all_change_receivers(),
615                ),
616            );
617
618            assert_matches!(
619                result,
620                Ok(balance) if
621                    balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] &&
622                    balance.fee_required() == Zatoshis::const_from_u64(10000)
623            );
624        }
625
626        {
627            // spend a single Sapling note, with insufficient funds to cover the minimum fee.
628            let result = change_strategy.compute_balance(
629                &Network::TestNetwork,
630                Network::TestNetwork
631                    .activation_height(NetworkUpgrade::Nu5)
632                    .unwrap()
633                    .into(),
634                BlockHeight::from_u32(1),
635                &PoolMigrationParams::new(
636                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
637                    AnchorRetentionInterval::ZIP_318,
638                ),
639                &[] as &[TestTransparentInput],
640                &[] as &[TxOut],
641                &(
642                    sapling::builder::BundleType::DEFAULT,
643                    &[TestSaplingInput {
644                        note_id: 0,
645                        value: Zatoshis::const_from_u64(50000),
646                    }][..],
647                    &[SaplingPayment::new(Zatoshis::const_from_u64(40001))][..],
648                ),
649                #[cfg(feature = "orchard")]
650                &orchard_fees::EmptyBundleView,
651                #[cfg(feature = "orchard")]
652                &orchard_fees::EmptyBundleView,
653                None,
654                // after excluding the inputs we're spending, we have no notes in the wallet
655                &AccountMeta::new(
656                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
657                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
658                    None,
659                    all_change_receivers(),
660                ),
661            );
662
663            assert_matches!(
664                result,
665                Err(ChangeError::InsufficientFunds { available, required })
666                    if available == Zatoshis::const_from_u64(50000)
667                       && required == Zatoshis::const_from_u64(50001)
668            );
669        }
670
671        {
672            // Spend a single Sapling note, creating two output notes that cause the transaction to
673            // balance exactly. This will fail, because even though there are enough funds in the
674            // wallet for the transaction to go through, and the fee is correct for a two-output
675            // transaction, we prohibit this case in order to prevent the transaction recipients
676            // from being able to reason about the value of the input note via knowledge that there
677            // is no change output.
678            let result = change_strategy.compute_balance(
679                &Network::TestNetwork,
680                Network::TestNetwork
681                    .activation_height(NetworkUpgrade::Nu5)
682                    .unwrap()
683                    .into(),
684                BlockHeight::from_u32(1),
685                &PoolMigrationParams::new(
686                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
687                    AnchorRetentionInterval::ZIP_318,
688                ),
689                &[] as &[TestTransparentInput],
690                &[] as &[TxOut],
691                &(
692                    sapling::builder::BundleType::DEFAULT,
693                    &[TestSaplingInput {
694                        note_id: 0,
695                        value: Zatoshis::const_from_u64(50000),
696                    }][..],
697                    &[
698                        SaplingPayment::new(Zatoshis::const_from_u64(30000)),
699                        SaplingPayment::new(Zatoshis::const_from_u64(10000)),
700                    ][..],
701                ),
702                #[cfg(feature = "orchard")]
703                &orchard_fees::EmptyBundleView,
704                #[cfg(feature = "orchard")]
705                &orchard_fees::EmptyBundleView,
706                None,
707                // after excluding the inputs we're spending, we have no notes in the wallet
708                &AccountMeta::new(
709                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
710                    Some(PoolMeta::new(0, Zatoshis::ZERO)),
711                    None,
712                    all_change_receivers(),
713                ),
714            );
715
716            assert_matches!(
717                result,
718                Err(ChangeError::InsufficientFunds { available, required })
719                    if available == Zatoshis::const_from_u64(50000)
720                       && required == Zatoshis::const_from_u64(55000)
721            );
722        }
723    }
724
725    #[test]
726    #[cfg(feature = "orchard")]
727    fn cross_pool_change_without_dust() {
728        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
729            Zip317FeeRule::standard(),
730            None,
731            ShieldedPool::Orchard,
732            DustOutputPolicy::default(),
733        );
734
735        // spend a single Sapling note that is sufficient to pay the fee
736        let result = change_strategy.compute_balance(
737            &Network::TestNetwork,
738            Network::TestNetwork
739                .activation_height(NetworkUpgrade::Nu5)
740                .unwrap()
741                .into(),
742            BlockHeight::from_u32(1),
743            &PoolMigrationParams::new(
744                zcash_protocol::zip318::BlockSpacing::UNIFORM,
745                AnchorRetentionInterval::ZIP_318,
746            ),
747            &[] as &[TestTransparentInput],
748            &[] as &[TxOut],
749            &(
750                sapling::builder::BundleType::DEFAULT,
751                &[TestSaplingInput {
752                    note_id: 0,
753                    value: Zatoshis::const_from_u64(55000),
754                }][..],
755                &[] as &[Infallible],
756            ),
757            &(
758                ::orchard::bundle::BundleVersion::orchard_v2(),
759                &[] as &[Infallible],
760                &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
761            ),
762            &orchard_fees::EmptyBundleView,
763            None,
764            &all_change_receivers(),
765        );
766
767        assert_matches!(
768            result,
769            Ok(balance) if
770                balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(5000), None)] &&
771                balance.fee_required() == Zatoshis::const_from_u64(20000)
772        );
773    }
774
775    #[test]
776    #[cfg(feature = "orchard")]
777    fn orchard_v3_change_counts_spends_and_outputs_separately() {
778        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
779            Zip317FeeRule::standard(),
780            None,
781            ShieldedPool::Orchard,
782            DustOutputPolicy::default(),
783        );
784
785        // Under the post-NU6.3 Orchard pool restriction (cross-address transfers
786        // disabled), every spend and output occupies its own action: one spend plus a
787        // payment and a change output make three logical actions, where the legacy
788        // policy would count `max(1, 2) == 2`.
789        let result = change_strategy.compute_balance(
790            &Network::TestNetwork,
791            Network::TestNetwork
792                .activation_height(NetworkUpgrade::Nu6_3)
793                .unwrap()
794                .into(),
795            BlockHeight::from_u32(1),
796            &PoolMigrationParams::new(
797                zcash_protocol::zip318::BlockSpacing::UNIFORM,
798                AnchorRetentionInterval::ZIP_318,
799            ),
800            &[] as &[TestTransparentInput],
801            &[] as &[TxOut],
802            &sapling_fees::EmptyBundleView,
803            &(
804                ::orchard::bundle::BundleVersion::orchard_v3(),
805                &[TestOrchardInput {
806                    note_id: 0,
807                    value: Zatoshis::const_from_u64(80000),
808                }][..],
809                &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
810            ),
811            &orchard_fees::EmptyBundleView,
812            None,
813            &all_change_receivers(),
814        );
815
816        assert_matches!(
817            result,
818            Ok(balance) if
819                balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(35000), None)] &&
820                balance.fee_required() == Zatoshis::const_from_u64(15000)
821        );
822    }
823
824    #[test]
825    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
826    fn orchard_fallback_change_pool_is_promoted_to_ironwood_after_nu6_3() {
827        // A caller that names Orchard as its fallback change pool.
828        let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
829            Zip317FeeRule::standard(),
830            None,
831            ShieldedPool::Orchard,
832            DustOutputPolicy::default(),
833            SplitPolicy::with_min_output_value(
834                NonZeroUsize::new(2).unwrap(),
835                Zatoshis::const_from_u64(100_0000),
836            ),
837        );
838
839        // A single transparent UTXO, shielded to the change pool. The fallback pool only
840        // decides where change goes for a transaction whose flows are fully transparent: one
841        // with shielded flows infers its change pool from the pool it already uses. So this
842        // is the case in which naming Orchard as the fallback can actually direct change
843        // into the Orchard pool.
844        let transparent_inputs = [TestTransparentInput {
845            outpoint: OutPoint::fake(),
846            coin: TxOut::new(
847                Zatoshis::const_from_u64(63000),
848                TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
849            ),
850        }];
851        let transparent_outputs = [TxOut::new(
852            Zatoshis::const_from_u64(40000),
853            Script::default(),
854        )];
855
856        // The shielded views are empty: the transaction has no shielded flows, so the change
857        // output the strategy proposes is the only thing that will populate one of them.
858        let sapling_view = sapling_fees::EmptyBundleView;
859        let ironwood_view = (
860            ::orchard::bundle::BundleVersion::ironwood_v3(),
861            &[] as &[Infallible],
862            &[] as &[Infallible],
863        );
864
865        // This transaction is not one half of a ZIP 320 pair, so it has no ephemeral balance.
866        let ephemeral_balance = None;
867
868        // No note counts are known for the account, so the split policy proposes a single
869        // change output: the assertions below are about the pool it lands in, not the split.
870        let wallet_meta = AccountMeta::new(None, None, None, all_change_receivers());
871
872        // The Orchard bundle version whose action-count policy applies at each height. The
873        // Orchard view is empty in both cases and so contributes no actions, but the version
874        // is what the transaction builder will be configured with.
875        let pre_nu6_3_orchard_view = (
876            ::orchard::bundle::BundleVersion::orchard_v2(),
877            &[] as &[Infallible],
878            &[] as &[Infallible],
879        );
880        let post_nu6_3_orchard_view = (
881            ::orchard::bundle::BundleVersion::orchard_v3(),
882            &[] as &[Infallible],
883            &[] as &[Infallible],
884        );
885
886        let pre_nu6_3_height: TargetHeight = Network::TestNetwork
887            .activation_height(NetworkUpgrade::Nu5)
888            .unwrap()
889            .into();
890        let post_nu6_3_height: TargetHeight = Network::TestNetwork
891            .activation_height(NetworkUpgrade::Nu6_3)
892            .unwrap()
893            .into();
894
895        // Before NU6.3, value may freely enter the Orchard pool, so the fallback is honoured
896        // as given and the change is returned to Orchard.
897        let pre_nu6_3_balance = change_strategy.compute_balance::<_, Infallible>(
898            &Network::TestNetwork,
899            pre_nu6_3_height,
900            BlockHeight::from_u32(1),
901            &PoolMigrationParams::new(
902                zcash_protocol::zip318::BlockSpacing::UNIFORM,
903                AnchorRetentionInterval::ZIP_318,
904            ),
905            &transparent_inputs,
906            &transparent_outputs,
907            &sapling_view,
908            &pre_nu6_3_orchard_view,
909            &ironwood_view,
910            ephemeral_balance,
911            &wallet_meta,
912        );
913
914        assert_matches!(
915            pre_nu6_3_balance,
916            Ok(balance) if
917                balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(8000), None)] &&
918                balance.fee_required() == Zatoshis::const_from_u64(15000)
919        );
920
921        // After NU6.3, the Orchard turnstile forbids value from entering the Orchard pool.
922        // This transaction spends no Orchard notes, so no amount of change may return to
923        // Orchard; the strategy promotes the Orchard fallback to Ironwood rather than
924        // proposing change that consensus would reject. The fee is unchanged: the change
925        // output is charged to the Ironwood bundle instead of the Orchard one, and each pads
926        // to the same two-action floor.
927        let post_nu6_3_balance = change_strategy.compute_balance::<_, Infallible>(
928            &Network::TestNetwork,
929            post_nu6_3_height,
930            BlockHeight::from_u32(1),
931            &PoolMigrationParams::new(
932                zcash_protocol::zip318::BlockSpacing::UNIFORM,
933                AnchorRetentionInterval::ZIP_318,
934            ),
935            &transparent_inputs,
936            &transparent_outputs,
937            &sapling_view,
938            &post_nu6_3_orchard_view,
939            &ironwood_view,
940            ephemeral_balance,
941            &wallet_meta,
942        );
943
944        assert_matches!(
945            post_nu6_3_balance,
946            Ok(balance) if
947                balance.proposed_change() == [ChangeValue::ironwood(Zatoshis::const_from_u64(8000), None)] &&
948                balance.fee_required() == Zatoshis::const_from_u64(15000)
949        );
950    }
951
952    /// The change strategy records the exact dummy outputs it charged the fee against, so the
953    /// builder can reproduce that action count. A canonical crossing has no Ironwood dummy output;
954    /// a payment one zatoshi off the denomination grid has one.
955    #[test]
956    #[cfg(feature = "orchard")]
957    fn the_change_strategy_records_the_dummy_outputs_it_costed() {
958        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
959            Zip317FeeRule::standard(),
960            None,
961            ShieldedPool::Orchard,
962            DustOutputPolicy::default(),
963        );
964        let zip318 = PoolMigrationParams::new(
965            zcash_protocol::zip318::BlockSpacing::UNIFORM,
966            AnchorRetentionInterval::ZIP_318,
967        );
968        let interval = AnchorBucketInterval::ZIP_318;
969
970        // An anchor ON the grid, as a canonical crossing requires.
971        let anchor = zcash_protocol::zip318::AnchorGrid::new(
972            interval,
973            zcash_protocol::zip318::BlockSpacing::UNIFORM,
974        )
975        .boundary_at_or_below(BlockHeight::from_u32(2_000_000));
976        let height = TargetHeight::from(BlockHeight::from_u32(u32::from(anchor) + 10));
977
978        // One Orchard input, large enough that its change stays in Orchard rather than being
979        // promoted to Ironwood by the turnstile rule.
980        let orchard_inputs = [TestOrchardInput {
981            note_id: 0,
982            value: Zatoshis::const_from_u64(10_000_000),
983        }];
984        let orchard_view = (
985            ::orchard::bundle::BundleVersion::orchard_v3(),
986            &orchard_inputs[..],
987            &[] as &[Infallible],
988        );
989        let sapling_view = (
990            sapling::builder::BundleType::DEFAULT,
991            &[] as &[Infallible],
992            &[] as &[Infallible],
993        );
994
995        let recorded_for = |value: Zatoshis| {
996            let ironwood_outputs = [OrchardPayment::new(value)];
997            let ironwood_view = (
998                ::orchard::bundle::BundleVersion::ironwood_v3(),
999                &[] as &[Infallible],
1000                &ironwood_outputs[..],
1001            );
1002            change_strategy
1003                .compute_balance::<_, u32>(
1004                    &Network::TestNetwork,
1005                    height,
1006                    anchor,
1007                    &zip318,
1008                    &[] as &[TestTransparentInput],
1009                    &[] as &[TxOut],
1010                    &sapling_view,
1011                    &orchard_view,
1012                    &ironwood_view,
1013                    None,
1014                    &all_change_receivers(),
1015                )
1016                .expect("the input covers the payment and its fee")
1017                .dummy_outputs()
1018                .expect("the change strategy records dummy outputs")
1019                .ironwood()
1020        };
1021
1022        assert_eq!(recorded_for(MAX_RESIDUAL_VALUE), 0);
1023        assert_eq!(
1024            recorded_for((MAX_RESIDUAL_VALUE + Zatoshis::const_from_u64(1)).unwrap()),
1025            1
1026        );
1027    }
1028
1029    #[test]
1030    #[cfg(feature = "orchard")]
1031    fn ironwood_outputs_are_charged_actions() {
1032        // V6 transactions carry a separate Ironwood bundle, so a populated
1033        // Ironwood view must contribute its own actions to the fee rather than
1034        // being treated as zero. Compare two otherwise-identical balances that
1035        // differ only by the presence of an Ironwood output.
1036        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1037            Zip317FeeRule::standard(),
1038            None,
1039            ShieldedPool::Orchard,
1040            DustOutputPolicy::default(),
1041        );
1042
1043        let height = Network::TestNetwork
1044            .activation_height(NetworkUpgrade::Nu5)
1045            .unwrap()
1046            .into();
1047        let sapling_inputs = [TestSaplingInput {
1048            note_id: 0,
1049            value: Zatoshis::const_from_u64(100000),
1050        }];
1051        let orchard_outputs = [OrchardPayment::new(Zatoshis::const_from_u64(30000))];
1052        let sapling_view = (
1053            sapling::builder::BundleType::DEFAULT,
1054            &sapling_inputs[..],
1055            &[] as &[Infallible],
1056        );
1057        let orchard_view = (
1058            ::orchard::bundle::BundleVersion::orchard_v2(),
1059            &[] as &[Infallible],
1060            &orchard_outputs[..],
1061        );
1062
1063        let without_ironwood = change_strategy
1064            .compute_balance(
1065                &Network::TestNetwork,
1066                height,
1067                BlockHeight::from_u32(1),
1068                &PoolMigrationParams::new(
1069                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
1070                    AnchorRetentionInterval::ZIP_318,
1071                ),
1072                &[] as &[TestTransparentInput],
1073                &[] as &[TxOut],
1074                &sapling_view,
1075                &orchard_view,
1076                &orchard_fees::EmptyBundleView,
1077                None,
1078                &all_change_receivers(),
1079            )
1080            .unwrap();
1081
1082        let with_ironwood = change_strategy
1083            .compute_balance(
1084                &Network::TestNetwork,
1085                height,
1086                BlockHeight::from_u32(1),
1087                &PoolMigrationParams::new(
1088                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
1089                    AnchorRetentionInterval::ZIP_318,
1090                ),
1091                &[] as &[TestTransparentInput],
1092                &[] as &[TxOut],
1093                &sapling_view,
1094                &orchard_view,
1095                &(
1096                    ::orchard::bundle::BundleVersion::ironwood_v3(),
1097                    &[] as &[Infallible],
1098                    &orchard_outputs[..],
1099                ),
1100                None,
1101                &all_change_receivers(),
1102            )
1103            .unwrap();
1104
1105        // ZIP 317 floors each shielded bundle that is used at 2 actions. Without
1106        // an Ironwood bundle: sapling (2) + orchard (2 outputs) = 4 actions; with
1107        // an Ironwood output: + ironwood (2) = 6 actions. At 5000 zat/action that
1108        // is 20000 vs 30000.
1109        assert_eq!(
1110            without_ironwood.fee_required(),
1111            Zatoshis::const_from_u64(20000)
1112        );
1113        assert_eq!(
1114            with_ironwood.fee_required(),
1115            Zatoshis::const_from_u64(30000)
1116        );
1117    }
1118
1119    #[test]
1120    fn change_with_transparent_payments_implicitly_allowing_zero_change() {
1121        change_with_transparent_payments(DustOutputPolicy::default())
1122    }
1123
1124    #[test]
1125    fn change_with_transparent_payments_explicitly_allowing_zero_change() {
1126        change_with_transparent_payments(DustOutputPolicy::new(
1127            DustAction::AllowDustChange,
1128            Some(Zatoshis::ZERO),
1129        ))
1130    }
1131
1132    fn change_with_transparent_payments(dust_output_policy: DustOutputPolicy) {
1133        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1134            Zip317FeeRule::standard(),
1135            None,
1136            ShieldedPool::Sapling,
1137            dust_output_policy,
1138        );
1139
1140        // spend a single Sapling note that is sufficient to pay the fee
1141        let result = change_strategy.compute_balance(
1142            &Network::TestNetwork,
1143            Network::TestNetwork
1144                .activation_height(NetworkUpgrade::Nu5)
1145                .unwrap()
1146                .into(),
1147            BlockHeight::from_u32(1),
1148            &PoolMigrationParams::new(
1149                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1150                AnchorRetentionInterval::ZIP_318,
1151            ),
1152            &[] as &[TestTransparentInput],
1153            &[TxOut::new(
1154                Zatoshis::const_from_u64(40000),
1155                Script::default(),
1156            )],
1157            &(
1158                sapling::builder::BundleType::DEFAULT,
1159                &[TestSaplingInput {
1160                    note_id: 0,
1161                    value: Zatoshis::const_from_u64(55000),
1162                }][..],
1163                &[] as &[Infallible],
1164            ),
1165            #[cfg(feature = "orchard")]
1166            &orchard_fees::EmptyBundleView,
1167            #[cfg(feature = "orchard")]
1168            &orchard_fees::EmptyBundleView,
1169            None,
1170            &all_change_receivers(),
1171        );
1172
1173        assert_matches!(
1174            result,
1175            Ok(balance) if
1176                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)]
1177                && balance.fee_required() == Zatoshis::const_from_u64(15000)
1178        );
1179    }
1180
1181    #[test]
1182    #[cfg(feature = "transparent-inputs")]
1183    fn change_fully_transparent_no_change() {
1184        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1185            Zip317FeeRule::standard(),
1186            None,
1187            ShieldedPool::Sapling,
1188            DustOutputPolicy::default(),
1189        );
1190
1191        // Spend a single transparent UTXO that is exactly sufficient to pay the fee.
1192        let result = change_strategy.compute_balance::<_, Infallible>(
1193            &Network::TestNetwork,
1194            Network::TestNetwork
1195                .activation_height(NetworkUpgrade::Nu5)
1196                .unwrap()
1197                .into(),
1198            BlockHeight::from_u32(1),
1199            &PoolMigrationParams::new(
1200                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1201                AnchorRetentionInterval::ZIP_318,
1202            ),
1203            &[TestTransparentInput {
1204                outpoint: OutPoint::fake(),
1205                coin: TxOut::new(
1206                    Zatoshis::const_from_u64(50000),
1207                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1208                ),
1209            }],
1210            &[TxOut::new(
1211                Zatoshis::const_from_u64(40000),
1212                Script::default(),
1213            )],
1214            &sapling_fees::EmptyBundleView,
1215            #[cfg(feature = "orchard")]
1216            &orchard_fees::EmptyBundleView,
1217            #[cfg(feature = "orchard")]
1218            &orchard_fees::EmptyBundleView,
1219            None,
1220            &all_change_receivers(),
1221        );
1222
1223        assert_matches!(
1224            result,
1225            Ok(balance) if
1226                balance.proposed_change().is_empty() &&
1227                balance.fee_required() == Zatoshis::const_from_u64(10000)
1228        );
1229    }
1230
1231    #[test]
1232    #[cfg(feature = "transparent-inputs")]
1233    fn change_transparent_flows_with_shielded_change() {
1234        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1235            Zip317FeeRule::standard(),
1236            None,
1237            ShieldedPool::Sapling,
1238            DustOutputPolicy::default(),
1239        );
1240
1241        // Spend a single transparent UTXO that is sufficient to pay the fee.
1242        let result = change_strategy.compute_balance::<_, Infallible>(
1243            &Network::TestNetwork,
1244            Network::TestNetwork
1245                .activation_height(NetworkUpgrade::Nu5)
1246                .unwrap()
1247                .into(),
1248            BlockHeight::from_u32(1),
1249            &PoolMigrationParams::new(
1250                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1251                AnchorRetentionInterval::ZIP_318,
1252            ),
1253            &[TestTransparentInput {
1254                outpoint: OutPoint::fake(),
1255                coin: TxOut::new(
1256                    Zatoshis::const_from_u64(63000),
1257                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1258                ),
1259            }],
1260            &[TxOut::new(
1261                Zatoshis::const_from_u64(40000),
1262                Script::default(),
1263            )],
1264            &sapling_fees::EmptyBundleView,
1265            #[cfg(feature = "orchard")]
1266            &orchard_fees::EmptyBundleView,
1267            #[cfg(feature = "orchard")]
1268            &orchard_fees::EmptyBundleView,
1269            None,
1270            &all_change_receivers(),
1271        );
1272
1273        assert_matches!(
1274            result,
1275            Ok(balance) if
1276                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(8000), None)] &&
1277                balance.fee_required() == Zatoshis::const_from_u64(15000)
1278        );
1279    }
1280
1281    #[test]
1282    #[cfg(feature = "transparent-inputs")]
1283    fn change_transparent_flows_with_shielded_dust_change() {
1284        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1285            Zip317FeeRule::standard(),
1286            None,
1287            ShieldedPool::Sapling,
1288            DustOutputPolicy::new(
1289                DustAction::AllowDustChange,
1290                Some(Zatoshis::const_from_u64(1000)),
1291            ),
1292        );
1293
1294        // Spend a single transparent UTXO that is sufficient to pay the fee.
1295        // The change will go to the fallback shielded change pool even though all inputs
1296        // and payments are transparent, and even though the change amount (1000) would
1297        // normally be considered dust, because we set the dust policy to allow that.
1298        let result = change_strategy.compute_balance::<_, Infallible>(
1299            &Network::TestNetwork,
1300            Network::TestNetwork
1301                .activation_height(NetworkUpgrade::Nu5)
1302                .unwrap()
1303                .into(),
1304            BlockHeight::from_u32(1),
1305            &PoolMigrationParams::new(
1306                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1307                AnchorRetentionInterval::ZIP_318,
1308            ),
1309            &[TestTransparentInput {
1310                outpoint: OutPoint::fake(),
1311                coin: TxOut::new(
1312                    Zatoshis::const_from_u64(56000),
1313                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1314                ),
1315            }],
1316            &[TxOut::new(
1317                Zatoshis::const_from_u64(40000),
1318                Script::default(),
1319            )],
1320            &sapling_fees::EmptyBundleView,
1321            #[cfg(feature = "orchard")]
1322            &orchard_fees::EmptyBundleView,
1323            #[cfg(feature = "orchard")]
1324            &orchard_fees::EmptyBundleView,
1325            None,
1326            &all_change_receivers(),
1327        );
1328
1329        assert_matches!(
1330            result,
1331            Ok(balance) if
1332                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(1000), None)] &&
1333                balance.fee_required() == Zatoshis::const_from_u64(15000)
1334        );
1335    }
1336
1337    #[test]
1338    #[cfg(feature = "transparent-inputs")]
1339    fn change_fully_transparent_with_transparent_change() {
1340        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1341            Zip317FeeRule::standard(),
1342            None,
1343            ShieldedPool::Sapling,
1344            DustOutputPolicy::default(),
1345        )
1346        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1347
1348        // Spend a single transparent UTXO that is sufficient to pay the fee. The change is
1349        // returned to the transparent pool: one P2PKH input and two P2PKH outputs (the
1350        // payment plus the change output) require `5000 * max(1, 2) = 10000` zats in fees,
1351        // rather than the 15000 zats required when the change is shielded.
1352        let result = change_strategy.compute_balance::<_, Infallible>(
1353            &Network::TestNetwork,
1354            Network::TestNetwork
1355                .activation_height(NetworkUpgrade::Nu5)
1356                .unwrap()
1357                .into(),
1358            BlockHeight::from_u32(1),
1359            &PoolMigrationParams::new(
1360                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1361                AnchorRetentionInterval::ZIP_318,
1362            ),
1363            &[TestTransparentInput {
1364                outpoint: OutPoint::fake(),
1365                coin: TxOut::new(
1366                    Zatoshis::const_from_u64(63000),
1367                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1368                ),
1369            }],
1370            &[TxOut::new(
1371                Zatoshis::const_from_u64(40000),
1372                Script::default(),
1373            )],
1374            &sapling_fees::EmptyBundleView,
1375            #[cfg(feature = "orchard")]
1376            &orchard_fees::EmptyBundleView,
1377            #[cfg(feature = "orchard")]
1378            &orchard_fees::EmptyBundleView,
1379            None,
1380            &all_change_receivers(),
1381        );
1382
1383        assert_matches!(
1384            result,
1385            Ok(balance) if
1386                balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(13000))] &&
1387                balance.fee_required() == Zatoshis::const_from_u64(10000)
1388        );
1389    }
1390
1391    #[test]
1392    #[cfg(feature = "transparent-inputs")]
1393    fn change_fully_transparent_exact_match_with_transparent_change() {
1394        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1395            Zip317FeeRule::standard(),
1396            None,
1397            ShieldedPool::Sapling,
1398            DustOutputPolicy::default(),
1399        )
1400        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1401
1402        // Spend a single transparent UTXO that exactly covers the payment plus the minimum
1403        // fee; no change output should be produced.
1404        let result = change_strategy.compute_balance::<_, Infallible>(
1405            &Network::TestNetwork,
1406            Network::TestNetwork
1407                .activation_height(NetworkUpgrade::Nu5)
1408                .unwrap()
1409                .into(),
1410            BlockHeight::from_u32(1),
1411            &PoolMigrationParams::new(
1412                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1413                AnchorRetentionInterval::ZIP_318,
1414            ),
1415            &[TestTransparentInput {
1416                outpoint: OutPoint::fake(),
1417                coin: TxOut::new(
1418                    Zatoshis::const_from_u64(50000),
1419                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1420                ),
1421            }],
1422            &[TxOut::new(
1423                Zatoshis::const_from_u64(40000),
1424                Script::default(),
1425            )],
1426            &sapling_fees::EmptyBundleView,
1427            #[cfg(feature = "orchard")]
1428            &orchard_fees::EmptyBundleView,
1429            #[cfg(feature = "orchard")]
1430            &orchard_fees::EmptyBundleView,
1431            None,
1432            &all_change_receivers(),
1433        );
1434
1435        assert_matches!(
1436            result,
1437            Ok(balance) if
1438                balance.proposed_change().is_empty() &&
1439                balance.fee_required() == Zatoshis::const_from_u64(10000)
1440        );
1441    }
1442
1443    #[test]
1444    #[cfg(feature = "transparent-inputs")]
1445    fn transparent_change_policy_has_no_effect_on_shielded_flows() {
1446        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1447            Zip317FeeRule::standard(),
1448            None,
1449            ShieldedPool::Sapling,
1450            DustOutputPolicy::default(),
1451        )
1452        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1453
1454        // Spend a single Sapling note; because the transaction involves shielded flows, the
1455        // change must be shielded even though transparent change is allowed by the policy.
1456        let result = change_strategy.compute_balance(
1457            &Network::TestNetwork,
1458            Network::TestNetwork
1459                .activation_height(NetworkUpgrade::Nu5)
1460                .unwrap()
1461                .into(),
1462            BlockHeight::from_u32(1),
1463            &PoolMigrationParams::new(
1464                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1465                AnchorRetentionInterval::ZIP_318,
1466            ),
1467            &[] as &[TestTransparentInput],
1468            &[] as &[TxOut],
1469            &(
1470                sapling::builder::BundleType::DEFAULT,
1471                &[TestSaplingInput {
1472                    note_id: 0,
1473                    value: Zatoshis::const_from_u64(55000),
1474                }][..],
1475                &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
1476            ),
1477            #[cfg(feature = "orchard")]
1478            &orchard_fees::EmptyBundleView,
1479            #[cfg(feature = "orchard")]
1480            &orchard_fees::EmptyBundleView,
1481            None,
1482            &all_change_receivers(),
1483        );
1484
1485        assert_matches!(
1486            result,
1487            Ok(balance) if
1488                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
1489                balance.fee_required() == Zatoshis::const_from_u64(10000)
1490        );
1491    }
1492
1493    #[test]
1494    #[cfg(feature = "transparent-inputs")]
1495    fn transparent_change_is_not_split() {
1496        let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
1497            Zip317FeeRule::standard(),
1498            None,
1499            ShieldedPool::Sapling,
1500            DustOutputPolicy::default(),
1501            SplitPolicy::with_min_output_value(
1502                NonZeroUsize::new(5).unwrap(),
1503                Zatoshis::const_from_u64(100_0000),
1504            ),
1505        )
1506        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1507
1508        // Spend a single transparent UTXO with change value sufficient to produce five
1509        // split outputs under the split policy; because the change is returned to the
1510        // transparent pool, it must nevertheless be emitted as a single output.
1511        let result = change_strategy.compute_balance::<_, Infallible>(
1512            &Network::TestNetwork,
1513            Network::TestNetwork
1514                .activation_height(NetworkUpgrade::Nu5)
1515                .unwrap()
1516                .into(),
1517            BlockHeight::from_u32(1),
1518            &PoolMigrationParams::new(
1519                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1520                AnchorRetentionInterval::ZIP_318,
1521            ),
1522            &[TestTransparentInput {
1523                outpoint: OutPoint::fake(),
1524                coin: TxOut::new(
1525                    Zatoshis::const_from_u64(750_0000),
1526                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1527                ),
1528            }],
1529            &[TxOut::new(
1530                Zatoshis::const_from_u64(100_0000),
1531                Script::default(),
1532            )],
1533            &sapling_fees::EmptyBundleView,
1534            #[cfg(feature = "orchard")]
1535            &orchard_fees::EmptyBundleView,
1536            #[cfg(feature = "orchard")]
1537            &orchard_fees::EmptyBundleView,
1538            None,
1539            &AccountMeta::new(
1540                Some(PoolMeta::new(0, Zatoshis::ZERO)),
1541                None,
1542                None,
1543                all_change_receivers(),
1544            ),
1545        );
1546
1547        assert_matches!(
1548            result,
1549            Ok(balance) if
1550                balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(649_0000))] &&
1551                balance.fee_required() == Zatoshis::const_from_u64(10000)
1552        );
1553    }
1554
1555    #[test]
1556    #[cfg(feature = "transparent-inputs")]
1557    fn transparent_change_rejects_dust() {
1558        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1559            Zip317FeeRule::standard(),
1560            None,
1561            ShieldedPool::Sapling,
1562            DustOutputPolicy::default(),
1563        )
1564        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1565
1566        // Spend a single transparent UTXO that would result in a 100-zat transparent change
1567        // output; under the default dust policy this must be rejected. The 55000-zat
1568        // requirement reflects the 5000-zat default dust threshold: adding 4900 zats to the
1569        // input value would produce change exactly at the threshold.
1570        let result = change_strategy.compute_balance::<_, Infallible>(
1571            &Network::TestNetwork,
1572            Network::TestNetwork
1573                .activation_height(NetworkUpgrade::Nu5)
1574                .unwrap()
1575                .into(),
1576            BlockHeight::from_u32(1),
1577            &PoolMigrationParams::new(
1578                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1579                AnchorRetentionInterval::ZIP_318,
1580            ),
1581            &[TestTransparentInput {
1582                outpoint: OutPoint::fake(),
1583                coin: TxOut::new(
1584                    Zatoshis::const_from_u64(50100),
1585                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1586                ),
1587            }],
1588            &[TxOut::new(
1589                Zatoshis::const_from_u64(40000),
1590                Script::default(),
1591            )],
1592            &sapling_fees::EmptyBundleView,
1593            #[cfg(feature = "orchard")]
1594            &orchard_fees::EmptyBundleView,
1595            #[cfg(feature = "orchard")]
1596            &orchard_fees::EmptyBundleView,
1597            None,
1598            &all_change_receivers(),
1599        );
1600
1601        assert_matches!(
1602            result,
1603            Err(ChangeError::InsufficientFunds { available, required })
1604                if available == Zatoshis::const_from_u64(50100)
1605                   && required == Zatoshis::const_from_u64(55000)
1606        );
1607    }
1608
1609    #[test]
1610    #[cfg(feature = "transparent-inputs")]
1611    fn transparent_change_allows_dust() {
1612        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1613            Zip317FeeRule::standard(),
1614            None,
1615            ShieldedPool::Sapling,
1616            DustOutputPolicy::new(
1617                DustAction::AllowDustChange,
1618                Some(Zatoshis::const_from_u64(1000)),
1619            ),
1620        )
1621        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1622
1623        // Spend a single transparent UTXO that results in a 100-zat transparent change
1624        // output; the `AllowDustChange` policy permits emitting it even though it is below
1625        // the 1000-zat dust threshold.
1626        let result = change_strategy.compute_balance::<_, Infallible>(
1627            &Network::TestNetwork,
1628            Network::TestNetwork
1629                .activation_height(NetworkUpgrade::Nu5)
1630                .unwrap()
1631                .into(),
1632            BlockHeight::from_u32(1),
1633            &PoolMigrationParams::new(
1634                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1635                AnchorRetentionInterval::ZIP_318,
1636            ),
1637            &[TestTransparentInput {
1638                outpoint: OutPoint::fake(),
1639                coin: TxOut::new(
1640                    Zatoshis::const_from_u64(50100),
1641                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1642                ),
1643            }],
1644            &[TxOut::new(
1645                Zatoshis::const_from_u64(40000),
1646                Script::default(),
1647            )],
1648            &sapling_fees::EmptyBundleView,
1649            #[cfg(feature = "orchard")]
1650            &orchard_fees::EmptyBundleView,
1651            #[cfg(feature = "orchard")]
1652            &orchard_fees::EmptyBundleView,
1653            None,
1654            &all_change_receivers(),
1655        );
1656
1657        assert_matches!(
1658            result,
1659            Ok(balance) if
1660                balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(100))] &&
1661                balance.fee_required() == Zatoshis::const_from_u64(10000)
1662        );
1663    }
1664
1665    #[test]
1666    #[cfg(feature = "transparent-inputs")]
1667    fn transparent_change_dust_added_to_fee() {
1668        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1669            Zip317FeeRule::standard(),
1670            None,
1671            ShieldedPool::Sapling,
1672            DustOutputPolicy::new(DustAction::AddDustToFee, None),
1673        )
1674        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1675
1676        // Spend a single transparent UTXO that would result in a 100-zat transparent change
1677        // output; under the `AddDustToFee` policy the dust value is instead added to the
1678        // fee and no change output is produced.
1679        let result = change_strategy.compute_balance::<_, Infallible>(
1680            &Network::TestNetwork,
1681            Network::TestNetwork
1682                .activation_height(NetworkUpgrade::Nu5)
1683                .unwrap()
1684                .into(),
1685            BlockHeight::from_u32(1),
1686            &PoolMigrationParams::new(
1687                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1688                AnchorRetentionInterval::ZIP_318,
1689            ),
1690            &[TestTransparentInput {
1691                outpoint: OutPoint::fake(),
1692                coin: TxOut::new(
1693                    Zatoshis::const_from_u64(50100),
1694                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1695                ),
1696            }],
1697            &[TxOut::new(
1698                Zatoshis::const_from_u64(40000),
1699                Script::default(),
1700            )],
1701            &sapling_fees::EmptyBundleView,
1702            #[cfg(feature = "orchard")]
1703            &orchard_fees::EmptyBundleView,
1704            #[cfg(feature = "orchard")]
1705            &orchard_fees::EmptyBundleView,
1706            None,
1707            &all_change_receivers(),
1708        );
1709
1710        assert_matches!(
1711            result,
1712            Ok(balance) if
1713                balance.proposed_change().is_empty() &&
1714                balance.fee_required() == Zatoshis::const_from_u64(10100)
1715        );
1716    }
1717
1718    #[test]
1719    fn change_with_allowable_dust_implicitly_allowing_zero_change() {
1720        change_with_allowable_dust(DustOutputPolicy::default())
1721    }
1722
1723    #[test]
1724    fn change_with_allowable_dust_explicitly_allowing_zero_change() {
1725        change_with_allowable_dust(DustOutputPolicy::new(
1726            DustAction::AllowDustChange,
1727            Some(Zatoshis::ZERO),
1728        ))
1729    }
1730
1731    fn change_with_allowable_dust(dust_output_policy: DustOutputPolicy) {
1732        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1733            Zip317FeeRule::standard(),
1734            None,
1735            ShieldedPool::Sapling,
1736            dust_output_policy,
1737        );
1738
1739        // Spend two Sapling notes, one of them dust. There is sufficient to
1740        // pay the fee: if only one note is spent then we are 1000 short, but
1741        // if both notes are spent then the fee stays at 10000 (even with a
1742        // zero-valued change output), so we have just enough.
1743        let result = change_strategy.compute_balance(
1744            &Network::TestNetwork,
1745            Network::TestNetwork
1746                .activation_height(NetworkUpgrade::Nu5)
1747                .unwrap()
1748                .into(),
1749            BlockHeight::from_u32(1),
1750            &PoolMigrationParams::new(
1751                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1752                AnchorRetentionInterval::ZIP_318,
1753            ),
1754            &[] as &[TestTransparentInput],
1755            &[] as &[TxOut],
1756            &(
1757                sapling::builder::BundleType::DEFAULT,
1758                &[
1759                    TestSaplingInput {
1760                        note_id: 0,
1761                        value: Zatoshis::const_from_u64(49000),
1762                    },
1763                    TestSaplingInput {
1764                        note_id: 1,
1765                        value: Zatoshis::const_from_u64(1000),
1766                    },
1767                ][..],
1768                &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
1769            ),
1770            #[cfg(feature = "orchard")]
1771            &orchard_fees::EmptyBundleView,
1772            #[cfg(feature = "orchard")]
1773            &orchard_fees::EmptyBundleView,
1774            None,
1775            &all_change_receivers(),
1776        );
1777
1778        assert_matches!(
1779            result,
1780            Ok(balance) if
1781                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] &&
1782                balance.fee_required() == Zatoshis::const_from_u64(10000)
1783        );
1784    }
1785
1786    #[test]
1787    fn change_with_disallowed_dust() {
1788        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1789            Zip317FeeRule::standard(),
1790            None,
1791            ShieldedPool::Sapling,
1792            DustOutputPolicy::default(),
1793        );
1794
1795        // Attempt to spend three Sapling notes, one of them dust. Adding the third
1796        // note increases the number of actions, and so it is uneconomic to spend it.
1797        let result = change_strategy.compute_balance(
1798            &Network::TestNetwork,
1799            Network::TestNetwork
1800                .activation_height(NetworkUpgrade::Nu5)
1801                .unwrap()
1802                .into(),
1803            BlockHeight::from_u32(1),
1804            &PoolMigrationParams::new(
1805                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1806                AnchorRetentionInterval::ZIP_318,
1807            ),
1808            &[] as &[TestTransparentInput],
1809            &[] as &[TxOut],
1810            &(
1811                sapling::builder::BundleType::DEFAULT,
1812                &[
1813                    TestSaplingInput {
1814                        note_id: 0,
1815                        value: Zatoshis::const_from_u64(29000),
1816                    },
1817                    TestSaplingInput {
1818                        note_id: 1,
1819                        value: Zatoshis::const_from_u64(20000),
1820                    },
1821                    TestSaplingInput {
1822                        note_id: 2,
1823                        value: Zatoshis::const_from_u64(1000),
1824                    },
1825                ][..],
1826                &[SaplingPayment::new(Zatoshis::const_from_u64(30000))][..],
1827            ),
1828            #[cfg(feature = "orchard")]
1829            &orchard_fees::EmptyBundleView,
1830            #[cfg(feature = "orchard")]
1831            &orchard_fees::EmptyBundleView,
1832            None,
1833            &all_change_receivers(),
1834        );
1835
1836        // We will get an error here, because the dust input isn't free to add
1837        // to the transaction.
1838        assert_matches!(
1839            result,
1840            Err(ChangeError::DustInputs { sapling, .. }) if sapling == vec![2]
1841        );
1842    }
1843
1844    #[test]
1845    #[cfg(feature = "orchard")]
1846    fn sapling_only_account_paying_orchard_gets_sapling_change() {
1847        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1848            Zip317FeeRule::standard(),
1849            None,
1850            ShieldedPool::Orchard,
1851            DustOutputPolicy::default(),
1852        );
1853
1854        // The account has no Orchard key, so the change from paying an Orchard recipient
1855        // goes to the Sapling pool that the transaction spends from.
1856        let result = change_strategy.compute_balance(
1857            &Network::TestNetwork,
1858            Network::TestNetwork
1859                .activation_height(NetworkUpgrade::Nu5)
1860                .unwrap()
1861                .into(),
1862            BlockHeight::from_u32(1),
1863            &PoolMigrationParams::new(
1864                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1865                AnchorRetentionInterval::ZIP_318,
1866            ),
1867            &[] as &[TestTransparentInput],
1868            &[] as &[TxOut],
1869            &(
1870                sapling::builder::BundleType::DEFAULT,
1871                &[TestSaplingInput {
1872                    note_id: 0,
1873                    value: Zatoshis::const_from_u64(55000),
1874                }][..],
1875                &[] as &[Infallible],
1876            ),
1877            &(
1878                ::orchard::bundle::BundleVersion::orchard_v2(),
1879                &[] as &[Infallible],
1880                &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
1881            ),
1882            &orchard_fees::EmptyBundleView,
1883            None,
1884            &ChangeReceivers::new(
1885                true,
1886                false,
1887                #[cfg(feature = "transparent-inputs")]
1888                None,
1889            ),
1890        );
1891
1892        assert_matches!(
1893            result,
1894            Ok(balance) if
1895                balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
1896                balance.fee_required() == Zatoshis::const_from_u64(20000)
1897        );
1898    }
1899
1900    #[test]
1901    fn shielded_change_requires_a_shielded_receiver() {
1902        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1903            Zip317FeeRule::standard(),
1904            None,
1905            ShieldedPool::Sapling,
1906            DustOutputPolicy::default(),
1907        );
1908
1909        // An account with no shielded key cannot receive the change of a shielded spend.
1910        let result = change_strategy.compute_balance(
1911            &Network::TestNetwork,
1912            Network::TestNetwork
1913                .activation_height(NetworkUpgrade::Nu5)
1914                .unwrap()
1915                .into(),
1916            BlockHeight::from_u32(1),
1917            &PoolMigrationParams::new(
1918                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1919                AnchorRetentionInterval::ZIP_318,
1920            ),
1921            &[] as &[TestTransparentInput],
1922            &[] as &[TxOut],
1923            &(
1924                sapling::builder::BundleType::DEFAULT,
1925                &[TestSaplingInput {
1926                    note_id: 0,
1927                    value: Zatoshis::const_from_u64(55000),
1928                }][..],
1929                &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
1930            ),
1931            #[cfg(feature = "orchard")]
1932            &orchard_fees::EmptyBundleView,
1933            #[cfg(feature = "orchard")]
1934            &orchard_fees::EmptyBundleView,
1935            None,
1936            &ChangeReceivers::new(
1937                false,
1938                false,
1939                #[cfg(feature = "transparent-inputs")]
1940                Some(TransparentChangeKind::P2pkh),
1941            ),
1942        );
1943
1944        assert_matches!(
1945            result,
1946            Err(ChangeError::UnsupportedChangePool(pool)) if pool == PoolType::SAPLING
1947        );
1948    }
1949
1950    #[test]
1951    #[cfg(feature = "transparent-inputs")]
1952    fn transparent_change_requires_a_transparent_receiver() {
1953        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1954            Zip317FeeRule::standard(),
1955            None,
1956            ShieldedPool::Sapling,
1957            DustOutputPolicy::default(),
1958        )
1959        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1960
1961        // Transparent change is allowed, but the account has no transparent key.
1962        let result = change_strategy.compute_balance::<_, Infallible>(
1963            &Network::TestNetwork,
1964            Network::TestNetwork
1965                .activation_height(NetworkUpgrade::Nu5)
1966                .unwrap()
1967                .into(),
1968            BlockHeight::from_u32(1),
1969            &PoolMigrationParams::new(
1970                zcash_protocol::zip318::BlockSpacing::UNIFORM,
1971                AnchorRetentionInterval::ZIP_318,
1972            ),
1973            &[TestTransparentInput {
1974                outpoint: OutPoint::fake(),
1975                coin: TxOut::new(
1976                    Zatoshis::const_from_u64(63000),
1977                    TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1978                ),
1979            }],
1980            &[TxOut::new(
1981                Zatoshis::const_from_u64(40000),
1982                Script::default(),
1983            )],
1984            &sapling_fees::EmptyBundleView,
1985            #[cfg(feature = "orchard")]
1986            &orchard_fees::EmptyBundleView,
1987            #[cfg(feature = "orchard")]
1988            &orchard_fees::EmptyBundleView,
1989            None,
1990            &ChangeReceivers::new(true, true, None),
1991        );
1992
1993        assert_matches!(
1994            result,
1995            Err(ChangeError::UnsupportedChangePool(pool)) if pool == PoolType::TRANSPARENT
1996        );
1997    }
1998
1999    #[test]
2000    #[cfg(feature = "transparent-inputs")]
2001    fn missing_transparent_receiver_does_not_hide_insufficient_funds() {
2002        /// The value of the transparent payment.
2003        const PAYMENT_VALUE: Zatoshis = Zatoshis::const_from_u64(40000);
2004
2005        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2006            Zip317FeeRule::standard(),
2007            None,
2008            ShieldedPool::Sapling,
2009            DustOutputPolicy::default(),
2010        )
2011        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
2012
2013        // Input selection starts with no inputs. A shielded-only account that pays a
2014        // transparent address must learn that it needs more funds, so that shielded notes
2015        // can be selected.
2016        let result = change_strategy.compute_balance::<_, Infallible>(
2017            &Network::TestNetwork,
2018            Network::TestNetwork
2019                .activation_height(NetworkUpgrade::Nu5)
2020                .unwrap()
2021                .into(),
2022            BlockHeight::from_u32(1),
2023            &PoolMigrationParams::new(
2024                zcash_protocol::zip318::BlockSpacing::UNIFORM,
2025                AnchorRetentionInterval::ZIP_318,
2026            ),
2027            &[] as &[TestTransparentInput],
2028            &[TxOut::new(PAYMENT_VALUE, Script::default())],
2029            &sapling_fees::EmptyBundleView,
2030            #[cfg(feature = "orchard")]
2031            &orchard_fees::EmptyBundleView,
2032            #[cfg(feature = "orchard")]
2033            &orchard_fees::EmptyBundleView,
2034            None,
2035            &ChangeReceivers::new(true, true, None),
2036        );
2037
2038        assert_matches!(
2039            result,
2040            Err(ChangeError::InsufficientFunds { available, required })
2041                if available == Zatoshis::ZERO && required == (PAYMENT_VALUE + MINIMUM_FEE).unwrap()
2042        );
2043    }
2044
2045    #[test]
2046    #[cfg(feature = "transparent-inputs")]
2047    fn changeless_transparent_transaction_needs_no_transparent_receiver() {
2048        /// The value of the transparent payment.
2049        const PAYMENT_VALUE: Zatoshis = Zatoshis::const_from_u64(40000);
2050
2051        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2052            Zip317FeeRule::standard(),
2053            None,
2054            ShieldedPool::Sapling,
2055            DustOutputPolicy::default(),
2056        )
2057        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
2058
2059        // The second transaction of a ZIP 320 pair spends the ephemeral output exactly, so it
2060        // has no change output. The account needs no transparent receiver for it.
2061        let result = change_strategy.compute_balance::<_, Infallible>(
2062            &Network::TestNetwork,
2063            Network::TestNetwork
2064                .activation_height(NetworkUpgrade::Nu5)
2065                .unwrap()
2066                .into(),
2067            BlockHeight::from_u32(1),
2068            &PoolMigrationParams::new(
2069                zcash_protocol::zip318::BlockSpacing::UNIFORM,
2070                AnchorRetentionInterval::ZIP_318,
2071            ),
2072            &[] as &[TestTransparentInput],
2073            &[TxOut::new(PAYMENT_VALUE, Script::default())],
2074            &sapling_fees::EmptyBundleView,
2075            #[cfg(feature = "orchard")]
2076            &orchard_fees::EmptyBundleView,
2077            #[cfg(feature = "orchard")]
2078            &orchard_fees::EmptyBundleView,
2079            Some(EphemeralBalance::Input(
2080                (PAYMENT_VALUE + MINIMUM_FEE).unwrap(),
2081            )),
2082            &ChangeReceivers::new(true, true, None),
2083        );
2084
2085        assert_matches!(
2086            result,
2087            Ok(balance) if balance.proposed_change().is_empty() && balance.fee_required() == MINIMUM_FEE
2088        );
2089    }
2090
2091    #[test]
2092    #[cfg(feature = "transparent-inputs")]
2093    fn transparent_change_is_sized_by_its_kind() {
2094        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2095            Zip317FeeRule::standard(),
2096            None,
2097            ShieldedPool::Sapling,
2098            DustOutputPolicy::default(),
2099        )
2100        .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
2101
2102        // Sixteen 32-byte P2SH payments make 512 bytes of outputs. A 32-byte P2SH change
2103        // output brings this to 544 bytes, which is 16 logical actions of 34 bytes. A
2104        // 34-byte P2PKH change output brings it to 546 bytes, which is 17 logical actions.
2105        let payments = vec![
2106            TxOut::new(
2107                Zatoshis::const_from_u64(5000),
2108                TransparentAddress::ScriptHash([0u8; 20]).script().into(),
2109            );
2110            16
2111        ];
2112        let balance = |kind| {
2113            change_strategy.compute_balance::<_, Infallible>(
2114                &Network::TestNetwork,
2115                Network::TestNetwork
2116                    .activation_height(NetworkUpgrade::Nu5)
2117                    .unwrap()
2118                    .into(),
2119                BlockHeight::from_u32(1),
2120                &PoolMigrationParams::new(
2121                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
2122                    AnchorRetentionInterval::ZIP_318,
2123                ),
2124                &[TestTransparentInput {
2125                    outpoint: OutPoint::fake(),
2126                    coin: TxOut::new(
2127                        Zatoshis::const_from_u64(200000),
2128                        TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
2129                    ),
2130                }],
2131                &payments[..],
2132                &sapling_fees::EmptyBundleView,
2133                #[cfg(feature = "orchard")]
2134                &orchard_fees::EmptyBundleView,
2135                #[cfg(feature = "orchard")]
2136                &orchard_fees::EmptyBundleView,
2137                None,
2138                &ChangeReceivers::new(false, false, Some(kind)),
2139            )
2140        };
2141
2142        assert_matches!(
2143            balance(TransparentChangeKind::P2sh),
2144            Ok(balance) if
2145                balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(40000))] &&
2146                balance.fee_required() == Zatoshis::const_from_u64(80000)
2147        );
2148        assert_matches!(
2149            balance(TransparentChangeKind::P2pkh),
2150            Ok(balance) if
2151                balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(35000))] &&
2152                balance.fee_required() == Zatoshis::const_from_u64(85000)
2153        );
2154    }
2155
2156    #[test]
2157    #[cfg(feature = "transparent-inputs")]
2158    fn ephemeral_output_requires_a_p2pkh_receiver() {
2159        let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2160            Zip317FeeRule::standard(),
2161            None,
2162            ShieldedPool::Sapling,
2163            DustOutputPolicy::default(),
2164        );
2165
2166        // The first transaction of a ZIP 320 pair sends value to an ephemeral P2PKH
2167        // address of the account. An account whose transparent key is a P2SH key has no
2168        // such address.
2169        let balance = |kind| {
2170            change_strategy.compute_balance::<_, Infallible>(
2171                &Network::TestNetwork,
2172                Network::TestNetwork
2173                    .activation_height(NetworkUpgrade::Nu5)
2174                    .unwrap()
2175                    .into(),
2176                BlockHeight::from_u32(1),
2177                &PoolMigrationParams::new(
2178                    zcash_protocol::zip318::BlockSpacing::UNIFORM,
2179                    AnchorRetentionInterval::ZIP_318,
2180                ),
2181                &[TestTransparentInput {
2182                    outpoint: OutPoint::fake(),
2183                    coin: TxOut::new(
2184                        Zatoshis::const_from_u64(100000),
2185                        TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
2186                    ),
2187                }],
2188                &[] as &[TxOut],
2189                &sapling_fees::EmptyBundleView,
2190                #[cfg(feature = "orchard")]
2191                &orchard_fees::EmptyBundleView,
2192                #[cfg(feature = "orchard")]
2193                &orchard_fees::EmptyBundleView,
2194                Some(EphemeralBalance::Output(Zatoshis::const_from_u64(50000))),
2195                &ChangeReceivers::new(true, false, Some(kind)),
2196            )
2197        };
2198
2199        assert_matches!(
2200            balance(TransparentChangeKind::P2sh),
2201            Err(ChangeError::UnsupportedChangePool(pool)) if pool == PoolType::TRANSPARENT
2202        );
2203        assert_matches!(
2204            balance(TransparentChangeKind::P2pkh),
2205            Ok(balance) if balance.proposed_change().iter().any(|c| c.is_ephemeral())
2206        );
2207    }
2208}