1use core::cmp::{Ordering, max, min};
2use std::num::{NonZeroU64, NonZeroUsize};
3
4#[cfg(feature = "orchard")]
5use zcash_primitives::transaction::builder::BundlePadding;
6use zcash_primitives::transaction::fees::{
7 FeeRule, transparent, zip317::MINIMUM_FEE, zip317::P2PKH_STANDARD_OUTPUT_SIZE,
8};
9#[cfg(feature = "orchard")]
10use zcash_protocol::zip318::PoolMigrationConstants;
11
12use crate::data_api::anchor_retention::PoolMigrationParams;
13use zcash_protocol::{
14 PoolType, ShieldedPool,
15 consensus::{self, BlockHeight, NetworkUpgrade},
16 memo::MemoBytes,
17 value::{BalanceError, Zatoshis},
18};
19
20use crate::data_api::{AccountMeta, ChangeReceivers, wallet::TargetHeight};
21
22use super::{
23 ChangeError, ChangeValue, DummyOutputCounts, DustAction, DustOutputPolicy, EphemeralBalance,
24 SplitPolicy, TransactionBalance, sapling as sapling_fees,
25};
26
27#[cfg(feature = "orchard")]
28use super::orchard::{self as orchard_fees, OutputView as _};
29#[cfg(feature = "transparent-inputs")]
30use {super::TransparentChangePolicy, crate::data_api::TransparentChangeKind};
31
32pub(crate) struct NetFlows {
33 t_in: Zatoshis,
34 t_out: Zatoshis,
35 sapling_in: Zatoshis,
36 sapling_out: Zatoshis,
37 orchard_in: Zatoshis,
38 orchard_out: Zatoshis,
39 ironwood_in: Zatoshis,
45 ironwood_out: Zatoshis,
46}
47
48impl NetFlows {
49 fn total_in(&self) -> Result<Zatoshis, BalanceError> {
50 (self.t_in + self.sapling_in + self.orchard_in + self.ironwood_in)
51 .ok_or(BalanceError::Overflow)
52 }
53 fn total_out(&self) -> Result<Zatoshis, BalanceError> {
54 (self.t_out + self.sapling_out + self.orchard_out + self.ironwood_out)
55 .ok_or(BalanceError::Overflow)
56 }
57 fn is_transparent(&self) -> bool {
59 !(self.sapling_in.is_positive()
60 || self.sapling_out.is_positive()
61 || self.orchard_in.is_positive()
62 || self.orchard_out.is_positive()
63 || self.ironwood_in.is_positive()
64 || self.ironwood_out.is_positive())
65 }
66}
67
68#[allow(clippy::too_many_arguments)]
69pub(crate) fn calculate_net_flows<NoteRefT: Clone, F: FeeRule, E>(
70 transparent_inputs: &[impl transparent::InputView],
71 transparent_outputs: &[impl transparent::OutputView],
72 sapling: &impl sapling_fees::BundleView<NoteRefT>,
73 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
74 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
75 ephemeral_balance: Option<EphemeralBalance>,
76) -> Result<NetFlows, ChangeError<E, NoteRefT>>
77where
78 E: From<F::Error> + From<BalanceError>,
79{
80 let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
81
82 let t_in = transparent_inputs
83 .iter()
84 .map(|t_in| t_in.coin().value())
85 .chain(ephemeral_balance.and_then(|b| b.ephemeral_input_amount()))
86 .sum::<Option<_>>()
87 .ok_or_else(overflow)?;
88 let t_out = transparent_outputs
89 .iter()
90 .map(|t_out| t_out.value())
91 .chain(ephemeral_balance.and_then(|b| b.ephemeral_output_amount()))
92 .sum::<Option<_>>()
93 .ok_or_else(overflow)?;
94 let sapling_in = sapling
95 .inputs()
96 .iter()
97 .map(sapling_fees::InputView::<NoteRefT>::value)
98 .sum::<Option<_>>()
99 .ok_or_else(overflow)?;
100 let sapling_out = sapling
101 .outputs()
102 .iter()
103 .map(sapling_fees::OutputView::value)
104 .sum::<Option<_>>()
105 .ok_or_else(overflow)?;
106
107 #[cfg(feature = "orchard")]
108 let orchard_in = orchard
109 .inputs()
110 .iter()
111 .map(orchard_fees::InputView::<NoteRefT>::value)
112 .sum::<Option<_>>()
113 .ok_or_else(overflow)?;
114 #[cfg(not(feature = "orchard"))]
115 let orchard_in = Zatoshis::ZERO;
116
117 #[cfg(feature = "orchard")]
118 let orchard_out = orchard
119 .outputs()
120 .iter()
121 .map(orchard_fees::OutputView::value)
122 .sum::<Option<_>>()
123 .ok_or_else(overflow)?;
124 #[cfg(not(feature = "orchard"))]
125 let orchard_out = Zatoshis::ZERO;
126
127 #[cfg(feature = "orchard")]
128 let ironwood_in = ironwood
129 .inputs()
130 .iter()
131 .map(orchard_fees::InputView::<NoteRefT>::value)
132 .sum::<Option<_>>()
133 .ok_or_else(overflow)?;
134 #[cfg(not(feature = "orchard"))]
135 let ironwood_in = Zatoshis::ZERO;
136
137 #[cfg(feature = "orchard")]
138 let ironwood_out = ironwood
139 .outputs()
140 .iter()
141 .map(orchard_fees::OutputView::value)
142 .sum::<Option<_>>()
143 .ok_or_else(overflow)?;
144 #[cfg(not(feature = "orchard"))]
145 let ironwood_out = Zatoshis::ZERO;
146
147 Ok(NetFlows {
148 t_in,
149 t_out,
150 sapling_in,
151 sapling_out,
152 orchard_in,
153 orchard_out,
154 ironwood_in,
155 ironwood_out,
156 })
157}
158
159pub(crate) fn select_change_pool(
174 _net_flows: &NetFlows,
175 _fallback_change_pool: ShieldedPool,
176 _change_receivers: &ChangeReceivers,
177 _ironwood_active: bool,
178 _max_change_value: Zatoshis,
179) -> ShieldedPool {
180 #[cfg(feature = "orchard")]
182 {
183 let flow_pools = [
187 (
188 ShieldedPool::Orchard,
189 _net_flows.orchard_in.is_positive() || _net_flows.orchard_out.is_positive(),
190 ),
191 (
192 ShieldedPool::Ironwood,
193 _net_flows.ironwood_in.is_positive() || _net_flows.ironwood_out.is_positive(),
194 ),
195 (
196 ShieldedPool::Sapling,
197 _net_flows.sapling_in.is_positive() || _net_flows.sapling_out.is_positive(),
198 ),
199 ]
200 .into_iter()
201 .filter_map(|(pool, has_flows)| has_flows.then_some(pool));
202
203 let flow_preferred = flow_pools.clone().next().unwrap_or(_fallback_change_pool);
206
207 let preferred = flow_pools
208 .chain([
209 _fallback_change_pool,
210 ShieldedPool::Orchard,
211 ShieldedPool::Sapling,
212 ])
213 .find(|pool| _change_receivers.can_receive(PoolType::Shielded(*pool)))
214 .unwrap_or(flow_preferred);
215
216 if _ironwood_active
223 && preferred == ShieldedPool::Orchard
224 && (!_net_flows.orchard_in.is_positive() || _max_change_value >= _net_flows.orchard_in)
225 {
226 ShieldedPool::Ironwood
227 } else {
228 preferred
229 }
230 }
231 #[cfg(not(feature = "orchard"))]
232 ShieldedPool::Sapling
233}
234
235#[derive(Clone, Copy, Debug)]
236pub(crate) struct OutputManifest {
237 transparent: usize,
238 sapling: usize,
239 orchard: usize,
240 ironwood: usize,
241}
242
243impl OutputManifest {
244 const ZERO: OutputManifest = OutputManifest {
245 transparent: 0,
246 sapling: 0,
247 orchard: 0,
248 ironwood: 0,
249 };
250
251 pub(crate) fn sapling(&self) -> usize {
252 self.sapling
253 }
254
255 pub(crate) fn orchard(&self) -> usize {
256 self.orchard
257 }
258
259 pub(crate) fn ironwood(&self) -> usize {
260 self.ironwood
261 }
262
263 #[cfg(feature = "orchard")]
264 pub(crate) fn transparent(&self) -> usize {
265 self.transparent
266 }
267
268 pub(crate) fn total_shielded(&self) -> usize {
269 self.sapling + self.orchard + self.ironwood
270 }
271
272 fn for_pool(pool: ShieldedPool, count: usize) -> Self {
274 Self {
275 transparent: 0,
276 sapling: if pool == ShieldedPool::Sapling {
277 count
278 } else {
279 0
280 },
281 orchard: if pool == ShieldedPool::Orchard {
282 count
283 } else {
284 0
285 },
286 ironwood: if pool == ShieldedPool::Ironwood {
287 count
288 } else {
289 0
290 },
291 }
292 }
293}
294
295pub(crate) struct SinglePoolBalanceConfig<'a, P, F> {
296 params: &'a P,
297 fee_rule: &'a F,
298 dust_output_policy: &'a DustOutputPolicy,
299 default_dust_threshold: Zatoshis,
300 split_policy: &'a SplitPolicy,
301 fallback_change_pool: ShieldedPool,
302 #[cfg(feature = "transparent-inputs")]
303 transparent_change_policy: TransparentChangePolicy,
304 marginal_fee: Zatoshis,
305 grace_actions: usize,
306}
307
308impl<'a, P, F> SinglePoolBalanceConfig<'a, P, F> {
309 #[allow(clippy::too_many_arguments)]
310 pub(crate) fn new(
311 params: &'a P,
312 fee_rule: &'a F,
313 dust_output_policy: &'a DustOutputPolicy,
314 default_dust_threshold: Zatoshis,
315 split_policy: &'a SplitPolicy,
316 fallback_change_pool: ShieldedPool,
317 #[cfg(feature = "transparent-inputs")] transparent_change_policy: TransparentChangePolicy,
318 marginal_fee: Zatoshis,
319 grace_actions: usize,
320 ) -> Self {
321 Self {
322 params,
323 fee_rule,
324 dust_output_policy,
325 default_dust_threshold,
326 split_policy,
327 fallback_change_pool,
328 #[cfg(feature = "transparent-inputs")]
329 transparent_change_policy,
330 marginal_fee,
331 grace_actions,
332 }
333 }
334}
335
336#[allow(clippy::too_many_arguments)]
337pub(crate) fn single_pool_output_balance<P: consensus::Parameters, NoteRefT: Clone, F: FeeRule, E>(
338 cfg: SinglePoolBalanceConfig<P, F>,
339 wallet_meta: Option<&AccountMeta>,
340 change_receivers: &ChangeReceivers,
341 target_height: TargetHeight,
342 transparent_inputs: &[impl transparent::InputView],
343 transparent_outputs: &[impl transparent::OutputView],
344 sapling: &impl sapling_fees::BundleView<NoteRefT>,
345 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
346 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
347 #[cfg(feature = "orchard")] orchard_padding: BundlePadding,
359 _anchor_height: BlockHeight,
365 _zip318: &PoolMigrationParams,
366 change_memo: Option<&MemoBytes>,
367 ephemeral_balance: Option<EphemeralBalance>,
368) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
369where
370 E: From<F::Error> + From<BalanceError>,
371{
372 let change_memo = change_memo.filter(|_| ephemeral_balance.is_none_or(|b| !b.is_input()));
376
377 let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
378 let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow));
379
380 let net_flows = calculate_net_flows::<NoteRefT, F, E>(
381 transparent_inputs,
382 transparent_outputs,
383 sapling,
384 #[cfg(feature = "orchard")]
385 orchard,
386 #[cfg(feature = "orchard")]
387 ironwood,
388 ephemeral_balance,
389 )?;
390
391 let fully_transparent = net_flows.is_transparent() && change_memo.is_none();
393
394 #[cfg(feature = "transparent-inputs")]
399 let wants_transparent_change = fully_transparent
400 && cfg.transparent_change_policy == TransparentChangePolicy::TransparentChangeAllowed;
401 #[cfg(not(feature = "transparent-inputs"))]
402 let wants_transparent_change = false;
403
404 #[cfg(feature = "transparent-inputs")]
409 let transparent_change_size = change_receivers
410 .transparent()
411 .filter(|_| wants_transparent_change)
412 .map(|kind| kind.output_size());
413 #[cfg(not(feature = "transparent-inputs"))]
414 let transparent_change_size = None;
415
416 let total_in = net_flows
417 .total_in()
418 .map_err(|e| ChangeError::StrategyError(E::from(e)))?;
419 let subtotal_out = net_flows
420 .total_out()
421 .map_err(|e| ChangeError::StrategyError(E::from(e)))?;
422
423 let sapling_input_count = sapling
424 .bundle_type()
425 .num_spends(sapling.inputs().len())
426 .map_err(ChangeError::BundleError)?;
427 let sapling_output_count = |change_count| {
428 sapling
429 .bundle_type()
430 .num_outputs(
431 sapling.inputs().len(),
432 sapling.outputs().len() + change_count,
433 )
434 .map_err(ChangeError::BundleError)
435 };
436
437 #[cfg(feature = "orchard")]
438 let orchard_action_count = |change_count| {
439 orchard_fees::transactional_action_count(
440 orchard_padding.bundle_type(),
441 orchard.bundle_version(),
442 orchard.inputs().len(),
443 orchard.outputs().len() + change_count,
444 )
445 .map_err(ChangeError::BundleError)
446 };
447 #[cfg(not(feature = "orchard"))]
448 let orchard_action_count = |change_count: usize| -> Result<usize, ChangeError<E, NoteRefT>> {
449 if change_count != 0 {
450 Err(ChangeError::BundleError(
451 "Nonzero Orchard change requested but the `orchard` feature is not enabled.",
452 ))
453 } else {
454 Ok(0)
455 }
456 };
457
458 #[cfg(feature = "orchard")]
476 let ironwood_is_canonical_crossing = |change: OutputManifest| {
477 let constants = _zip318;
478 orchard.inputs().len() == 1
479 && ironwood.inputs().is_empty()
480 && change.ironwood() == 0
481 && change.orchard() <= 1
487 && change.sapling() == 0
488 && change.transparent() == 0
489 && match ironwood.outputs() {
490 [output] => constants.is_canonical_denomination(output.value()),
491 _ => false,
492 }
493 && constants
494 .anchor_grid().is_boundary(_anchor_height)
495 };
496 #[cfg(feature = "orchard")]
497 let ironwood_action_count = |change: OutputManifest| {
498 let padding = if ironwood_is_canonical_crossing(change) {
501 BundlePadding::UNPADDED
502 } else {
503 BundlePadding::DEFAULT
504 };
505 orchard_fees::transactional_action_count(
506 padding.bundle_type(),
507 ironwood.bundle_version(),
508 ironwood.inputs().len(),
509 ironwood.outputs().len() + change.ironwood(),
510 )
511 .map_err(ChangeError::BundleError)
512 };
513 #[cfg(not(feature = "orchard"))]
514 let ironwood_action_count =
515 |change: OutputManifest| -> Result<usize, ChangeError<E, NoteRefT>> {
516 if change.ironwood() != 0 {
517 Err(ChangeError::BundleError(
518 "Nonzero Ironwood change requested but the `orchard` feature is not enabled.",
519 ))
520 } else {
521 Ok(0)
522 }
523 };
524
525 let transparent_input_sizes = transparent_inputs
526 .iter()
527 .map(|i| i.serialized_size())
528 .chain(
529 ephemeral_balance
530 .and_then(|b| b.ephemeral_input_amount())
531 .map(|_| transparent::InputSize::STANDARD_P2PKH),
532 );
533 let transparent_output_sizes = transparent_outputs
534 .iter()
535 .map(|i| i.serialized_size())
536 .chain(
537 ephemeral_balance
538 .and_then(|b| b.ephemeral_output_amount())
539 .map(|_| P2PKH_STANDARD_OUTPUT_SIZE),
540 );
541
542 let min_fee = cfg
563 .fee_rule
564 .fee_required(
565 cfg.params,
566 BlockHeight::from(target_height),
567 transparent_input_sizes.clone(),
568 transparent_output_sizes.clone(),
569 sapling_input_count,
570 sapling_output_count(0)?,
571 orchard_action_count(0)?,
572 ironwood_action_count(OutputManifest::ZERO)?,
573 )
574 .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
575
576 let total_out_with_min_fee = (subtotal_out + min_fee).ok_or_else(overflow)?;
577
578 let change_pool = select_change_pool(
581 &net_flows,
582 cfg.fallback_change_pool,
583 change_receivers,
584 cfg.params
585 .is_nu_active(NetworkUpgrade::Nu6_3, target_height.into()),
586 (total_in - total_out_with_min_fee).unwrap_or(Zatoshis::ZERO),
587 );
588
589 let (target_change_count, target_change_counts) = if wants_transparent_change {
590 (
594 1,
595 OutputManifest {
596 transparent: 1,
597 sapling: 0,
598 orchard: 0,
599 ironwood: 0,
600 },
601 )
602 } else {
603 let target_change_count = wallet_meta.map_or(1, |m| {
604 usize::from(cfg.split_policy.target_output_count)
605 .saturating_sub(m.total_note_count().unwrap_or(usize::MAX))
607 .max(1)
608 });
609 let target_change_counts = OutputManifest {
610 transparent: 0,
611 sapling: if change_pool == ShieldedPool::Sapling {
612 target_change_count
613 } else {
614 0
615 },
616 orchard: if change_pool == ShieldedPool::Orchard {
617 target_change_count
618 } else {
619 0
620 },
621 ironwood: if change_pool == ShieldedPool::Ironwood {
622 target_change_count
623 } else {
624 0
625 },
626 };
627 assert!(target_change_counts.total_shielded() == target_change_count);
628 (target_change_count, target_change_counts)
629 };
630
631 if cfg.marginal_fee.is_positive() {
635 let possible_change = {
639 if fully_transparent
641 || (cfg.dust_output_policy.action() == DustAction::AddDustToFee
642 && change_memo.is_none())
643 {
644 vec![OutputManifest::ZERO, target_change_counts]
645 } else {
646 vec![target_change_counts]
647 }
648 };
649
650 check_for_uneconomic_inputs(
651 transparent_inputs,
652 transparent_outputs,
653 sapling,
654 #[cfg(feature = "orchard")]
655 orchard,
656 #[cfg(feature = "orchard")]
657 ironwood,
658 #[cfg(feature = "orchard")]
659 orchard_padding,
660 #[cfg(feature = "orchard")]
661 _anchor_height,
662 #[cfg(feature = "orchard")]
663 _zip318,
664 cfg.marginal_fee,
665 cfg.grace_actions,
666 &possible_change[..],
667 ephemeral_balance,
668 )?;
669 }
670
671 #[allow(unused_mut)]
672 let (mut change, fee) = match total_in.cmp(&total_out_with_min_fee) {
673 Ordering::Less => {
674 return Err(ChangeError::InsufficientFunds {
677 available: total_in,
678 required: total_out_with_min_fee,
679 });
680 }
681 Ordering::Equal if fully_transparent => {
682 (vec![], min_fee)
685 }
686 _ => {
687 let max_fee = max(
688 min_fee,
689 cfg.fee_rule
690 .fee_required(
691 cfg.params,
692 BlockHeight::from(target_height),
693 transparent_input_sizes.clone(),
694 transparent_output_sizes
695 .clone()
696 .chain(transparent_change_size),
699 sapling_input_count,
700 sapling_output_count(target_change_counts.sapling())?,
701 orchard_action_count(target_change_counts.orchard())?,
702 ironwood_action_count(target_change_counts)?,
703 )
704 .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?,
705 );
706
707 let total_out_with_max_fee = (subtotal_out + max_fee).ok_or_else(overflow)?;
708
709 let split_count = if wants_transparent_change {
714 1
715 } else {
716 usize::from(wallet_meta.map_or(NonZeroUsize::MIN, |wm| {
717 cfg.split_policy.split_count(
718 wm.total_note_count(),
719 wm.total_value(),
720 (total_in - total_out_with_max_fee).unwrap_or(Zatoshis::ZERO),
724 )
725 }))
726 };
727
728 let total_fee = if split_count < target_change_count {
730 cfg.fee_rule
731 .fee_required(
732 cfg.params,
733 BlockHeight::from(target_height),
734 transparent_input_sizes,
735 transparent_output_sizes,
736 sapling_input_count,
737 sapling_output_count(if change_pool == ShieldedPool::Sapling {
738 split_count
739 } else {
740 0
741 })?,
742 orchard_action_count(if change_pool == ShieldedPool::Orchard {
743 split_count
744 } else {
745 0
746 })?,
747 ironwood_action_count(OutputManifest::for_pool(change_pool, split_count))?,
748 )
749 .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?
750 } else {
751 max_fee
752 };
753
754 let total_out = (subtotal_out + total_fee).ok_or_else(overflow)?;
755 let total_change =
756 (total_in - total_out).ok_or_else(|| ChangeError::InsufficientFunds {
757 available: total_in,
758 required: total_out,
759 })?;
760
761 let per_output_change = total_change.div_with_remainder(
762 NonZeroU64::new(u64::try_from(split_count).expect("usize fits into u64")).unwrap(),
763 );
764 let simple_case = || {
765 #[cfg(feature = "transparent-inputs")]
766 if wants_transparent_change {
767 return (
768 if total_change.is_zero() {
769 vec![]
774 } else {
775 vec![ChangeValue::transparent(total_change)]
776 },
777 total_fee,
778 );
779 }
780
781 (
782 (0usize..split_count)
783 .map(|i| {
784 ChangeValue::shielded(
785 change_pool,
786 if i == 0 {
787 (*per_output_change.quotient() + *per_output_change.remainder())
789 .unwrap()
790 } else {
791 *per_output_change.quotient()
794 },
795 change_memo.cloned(),
796 )
797 })
798 .collect(),
799 total_fee,
800 )
801 };
802
803 let change_dust_threshold = cfg
804 .dust_output_policy
805 .dust_threshold()
806 .unwrap_or(cfg.default_dust_threshold);
807
808 if total_change < change_dust_threshold {
809 match cfg.dust_output_policy.action() {
810 DustAction::Reject => {
811 if total_change.is_zero() {
818 simple_case()
819 } else {
820 let shortfall =
821 (change_dust_threshold - total_change).ok_or_else(underflow)?;
822
823 return Err(ChangeError::InsufficientFunds {
824 available: total_in,
825 required: (total_in + shortfall).ok_or_else(overflow)?,
826 });
827 }
828 }
829 DustAction::AllowDustChange => simple_case(),
830 DustAction::AddDustToFee => {
831 let fee_with_dust = (total_change + total_fee).ok_or_else(overflow)?;
834
835 let reasonable_fee =
836 (total_fee + (MINIMUM_FEE * 10u64).unwrap()).ok_or_else(overflow)?;
837
838 if fee_with_dust > reasonable_fee {
839 simple_case()
842 } else if change_memo.is_some() {
843 (
844 vec![ChangeValue::shielded(
845 change_pool,
846 Zatoshis::ZERO,
847 change_memo.cloned(),
848 )],
849 fee_with_dust,
850 )
851 } else {
852 (vec![], fee_with_dust)
853 }
854 }
855 }
856 } else {
857 simple_case()
858 }
859 }
860 };
861
862 #[cfg(feature = "transparent-inputs")]
863 change.extend(
864 ephemeral_balance
865 .and_then(|b| b.ephemeral_output_amount())
866 .map(ChangeValue::ephemeral_transparent),
867 );
868
869 for change_value in &change {
870 if !can_receive_change(change_receivers, change_value) {
871 return Err(ChangeError::UnsupportedChangePool(
872 change_value.output_pool(),
873 ));
874 }
875 }
876
877 let final_change = OutputManifest {
881 transparent: change
882 .iter()
883 .filter(|c| c.output_pool() == PoolType::TRANSPARENT)
884 .count(),
885 sapling: change
886 .iter()
887 .filter(|c| c.output_pool() == PoolType::SAPLING)
888 .count(),
889 orchard: change
890 .iter()
891 .filter(|c| c.output_pool() == PoolType::ORCHARD)
892 .count(),
893 ironwood: change
894 .iter()
895 .filter(|c| c.output_pool() == PoolType::IRONWOOD)
896 .count(),
897 };
898 let sapling_real_outputs = sapling.outputs().len() + final_change.sapling();
899 let sapling_dummy_outputs = sapling_output_count(final_change.sapling())?
900 .checked_sub(sapling_real_outputs)
901 .expect("the Sapling action count includes every real output");
902 #[cfg(feature = "orchard")]
903 let orchard_dummy_outputs = orchard_action_count(final_change.orchard())?
904 .checked_sub(orchard.outputs().len() + final_change.orchard())
905 .expect("the Orchard action count includes every real output");
906 #[cfg(feature = "orchard")]
907 let ironwood_dummy_outputs = ironwood_action_count(final_change)?
908 .checked_sub(ironwood.outputs().len() + final_change.ironwood())
909 .expect("the Ironwood action count includes every real output");
910
911 TransactionBalance::new(change, fee)
912 .map(|balance| {
913 balance.with_dummy_outputs(DummyOutputCounts::new(
914 sapling_dummy_outputs,
915 #[cfg(feature = "orchard")]
916 orchard_dummy_outputs,
917 #[cfg(feature = "orchard")]
918 ironwood_dummy_outputs,
919 ))
920 })
921 .map_err(|_| overflow())
922}
923
924fn can_receive_change(change_receivers: &ChangeReceivers, change: &ChangeValue) -> bool {
927 #[cfg(feature = "transparent-inputs")]
928 if change.is_ephemeral() {
929 return matches!(
930 change_receivers.transparent(),
931 Some(TransparentChangeKind::P2pkh)
932 );
933 }
934 change_receivers.can_receive(change.output_pool())
935}
936
937#[allow(clippy::too_many_arguments)]
951pub(crate) fn check_for_uneconomic_inputs<NoteRefT: Clone, E>(
952 transparent_inputs: &[impl transparent::InputView],
953 transparent_outputs: &[impl transparent::OutputView],
954 sapling: &impl sapling_fees::BundleView<NoteRefT>,
955 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
956 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
957 #[cfg(feature = "orchard")] orchard_padding: BundlePadding,
960 #[cfg(feature = "orchard")] anchor_height: BlockHeight,
961 #[cfg(feature = "orchard")] zip318: &PoolMigrationParams,
962 marginal_fee: Zatoshis,
963 grace_actions: usize,
964 possible_change: &[OutputManifest],
965 ephemeral_balance: Option<EphemeralBalance>,
966) -> Result<(), ChangeError<E, NoteRefT>> {
967 let mut t_dust: Vec<_> = transparent_inputs
968 .iter()
969 .filter_map(|i| {
970 if i.coin().value() <= marginal_fee {
973 Some(i.outpoint().clone())
974 } else {
975 None
976 }
977 })
978 .collect();
979
980 let mut s_dust: Vec<_> = sapling
981 .inputs()
982 .iter()
983 .filter_map(|i| {
984 if sapling_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
985 Some(sapling_fees::InputView::<NoteRefT>::note_id(i).clone())
986 } else {
987 None
988 }
989 })
990 .collect();
991
992 #[cfg(feature = "orchard")]
993 let mut o_dust: Vec<NoteRefT> = orchard
994 .inputs()
995 .iter()
996 .filter_map(|i| {
997 if orchard_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
998 Some(orchard_fees::InputView::<NoteRefT>::note_id(i).clone())
999 } else {
1000 None
1001 }
1002 })
1003 .collect();
1004 #[cfg(not(feature = "orchard"))]
1005 let mut o_dust: Vec<NoteRefT> = vec![];
1006
1007 #[cfg(feature = "orchard")]
1008 let mut i_dust: Vec<NoteRefT> = ironwood
1009 .inputs()
1010 .iter()
1011 .filter_map(|i| {
1012 if orchard_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
1013 Some(orchard_fees::InputView::<NoteRefT>::note_id(i).clone())
1014 } else {
1015 None
1016 }
1017 })
1018 .collect();
1019 #[cfg(not(feature = "orchard"))]
1020 let mut i_dust: Vec<NoteRefT> = vec![];
1021
1022 if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() && i_dust.is_empty() {
1024 return Ok(());
1025 }
1026
1027 let (t_inputs_len, t_outputs_len) = (
1028 transparent_inputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_input())),
1029 transparent_outputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_output())),
1030 );
1031 let (s_inputs_len, s_outputs_len) = (sapling.inputs().len(), sapling.outputs().len());
1032 #[cfg(feature = "orchard")]
1033 let (o_inputs_len, o_outputs_len) = (orchard.inputs().len(), orchard.outputs().len());
1034 #[cfg(not(feature = "orchard"))]
1035 let (o_inputs_len, o_outputs_len) = (0usize, 0usize);
1036 #[cfg(feature = "orchard")]
1037 let (i_inputs_len, i_outputs_len) = (ironwood.inputs().len(), ironwood.outputs().len());
1038 #[cfg(not(feature = "orchard"))]
1039 let (i_inputs_len, i_outputs_len) = (0usize, 0usize);
1040
1041 let t_non_dust = t_inputs_len.checked_sub(t_dust.len()).unwrap();
1042 let s_non_dust = s_inputs_len.checked_sub(s_dust.len()).unwrap();
1043 let o_non_dust = o_inputs_len.checked_sub(o_dust.len()).unwrap();
1044 let i_non_dust = i_inputs_len.checked_sub(i_dust.len()).unwrap();
1045
1046 let allowed_dust = |change: &OutputManifest| {
1048 let t_allowed = min(
1061 t_dust.len(),
1062 (t_outputs_len + change.transparent).saturating_sub(t_non_dust),
1063 );
1064 let s_allowed = min(
1065 s_dust.len(),
1066 (s_outputs_len + change.sapling).saturating_sub(s_non_dust),
1067 );
1068 let o_allowed = min(
1069 o_dust.len(),
1070 (o_outputs_len + change.orchard).saturating_sub(o_non_dust),
1071 );
1072 let i_allowed = min(
1073 i_dust.len(),
1074 (i_outputs_len + change.ironwood).saturating_sub(i_non_dust),
1075 );
1076
1077 let t_req_inputs = t_non_dust + t_allowed;
1079 let s_req_inputs = s_non_dust + s_allowed;
1080 #[cfg(feature = "orchard")]
1081 let o_req_inputs = o_non_dust + o_allowed;
1082 #[cfg(feature = "orchard")]
1083 let i_req_inputs = i_non_dust + i_allowed;
1084
1085 let hypothetical_actions = |t_extra, s_extra, _o_extra, _i_extra| {
1093 let s_spend_count = sapling
1094 .bundle_type()
1095 .num_spends(s_req_inputs + s_extra)
1096 .map_err(ChangeError::BundleError)?;
1097
1098 let s_output_count = sapling
1099 .bundle_type()
1100 .num_outputs(s_req_inputs + s_extra, s_outputs_len + change.sapling)
1101 .map_err(ChangeError::BundleError)?;
1102
1103 #[cfg(feature = "orchard")]
1104 let o_action_count = orchard_fees::transactional_action_count(
1105 orchard_padding.bundle_type(),
1106 orchard.bundle_version(),
1107 o_req_inputs + _o_extra,
1108 o_outputs_len + change.orchard,
1109 )
1110 .map_err(ChangeError::BundleError)?;
1111 #[cfg(not(feature = "orchard"))]
1112 let o_action_count = 0;
1113
1114 #[cfg(feature = "orchard")]
1118 let i_padding = {
1119 let constants = zip318;
1120 let canonical = o_req_inputs + _o_extra == 1
1121 && i_req_inputs + _i_extra == 0
1122 && change.ironwood == 0
1123 && match ironwood.outputs() {
1124 [output] => constants.is_canonical_denomination(output.value()),
1125 _ => false,
1126 }
1127 && constants.anchor_grid().is_boundary(anchor_height);
1128 if canonical {
1129 BundlePadding::UNPADDED
1130 } else {
1131 BundlePadding::DEFAULT
1132 }
1133 };
1134 #[cfg(feature = "orchard")]
1135 let i_action_count = orchard_fees::transactional_action_count(
1136 i_padding.bundle_type(),
1137 ironwood.bundle_version(),
1138 i_req_inputs + _i_extra,
1139 i_outputs_len + change.ironwood,
1140 )
1141 .map_err(ChangeError::BundleError)?;
1142 #[cfg(not(feature = "orchard"))]
1143 let i_action_count = 0;
1144
1145 Ok(
1148 max(t_req_inputs + t_extra, t_outputs_len + change.transparent)
1149 + max(s_spend_count, s_output_count)
1150 + o_action_count
1151 + i_action_count,
1152 )
1153 };
1154
1155 let baseline = hypothetical_actions(0, 0, 0, 0)?;
1166
1167 let (t_extra, s_extra, o_extra, i_extra) = if baseline >= grace_actions {
1168 (0, 0, 0, 0)
1169 } else if t_dust.len() > t_allowed && hypothetical_actions(1, 0, 0, 0)? <= baseline {
1170 (1, 0, 0, 0)
1171 } else if s_dust.len() > s_allowed && hypothetical_actions(0, 1, 0, 0)? <= baseline {
1172 (0, 1, 0, 0)
1173 } else if o_dust.len() > o_allowed && hypothetical_actions(0, 0, 1, 0)? <= baseline {
1174 (0, 0, 1, 0)
1175 } else if i_dust.len() > i_allowed && hypothetical_actions(0, 0, 0, 1)? <= baseline {
1176 (0, 0, 0, 1)
1177 } else {
1178 (0, 0, 0, 0)
1179 };
1180 Ok(OutputManifest {
1181 transparent: t_allowed + t_extra,
1182 sapling: s_allowed + s_extra,
1183 orchard: o_allowed + o_extra,
1184 ironwood: i_allowed + i_extra,
1185 })
1186 };
1187
1188 let allowed = possible_change
1190 .iter()
1191 .map(allowed_dust)
1192 .collect::<Result<Vec<_>, _>>()?
1193 .into_iter()
1194 .reduce(|l, r| OutputManifest {
1195 transparent: min(l.transparent, r.transparent),
1196 sapling: min(l.sapling, r.sapling),
1197 orchard: min(l.orchard, r.orchard),
1198 ironwood: min(l.ironwood, r.ironwood),
1199 })
1200 .expect("possible_change is nonempty");
1201
1202 let t_dust = t_dust.split_off(allowed.transparent);
1205 let s_dust = s_dust.split_off(allowed.sapling);
1206 let o_dust = o_dust.split_off(allowed.orchard);
1207 let i_dust = i_dust.split_off(allowed.ironwood);
1208
1209 if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() && i_dust.is_empty() {
1210 Ok(())
1211 } else {
1212 Err(ChangeError::DustInputs {
1213 transparent: t_dust,
1214 sapling: s_dust,
1215 #[cfg(feature = "orchard")]
1216 orchard: o_dust,
1217 #[cfg(feature = "orchard")]
1218 ironwood: i_dust,
1219 })
1220 }
1221}
1222
1223#[cfg(all(test, feature = "orchard"))]
1224mod tests {
1225 use super::{NetFlows, select_change_pool};
1226 use crate::data_api::ChangeReceivers;
1227 use zcash_protocol::{ShieldedPool, value::Zatoshis};
1228
1229 fn receivers(sapling: bool, orchard: bool) -> ChangeReceivers {
1232 ChangeReceivers::new(
1233 sapling,
1234 orchard,
1235 #[cfg(feature = "transparent-inputs")]
1236 None,
1237 )
1238 }
1239
1240 fn all_receivers() -> ChangeReceivers {
1242 receivers(true, true)
1243 }
1244
1245 fn flows(orchard_in: u64, ironwood_in: u64, sapling_in: u64) -> NetFlows {
1246 NetFlows {
1247 t_in: Zatoshis::ZERO,
1248 t_out: Zatoshis::ZERO,
1249 sapling_in: Zatoshis::const_from_u64(sapling_in),
1250 sapling_out: Zatoshis::ZERO,
1251 orchard_in: Zatoshis::const_from_u64(orchard_in),
1252 orchard_out: Zatoshis::ZERO,
1253 ironwood_in: Zatoshis::const_from_u64(ironwood_in),
1254 ironwood_out: Zatoshis::ZERO,
1255 }
1256 }
1257
1258 #[test]
1259 fn select_change_pool_routes_ironwood_spend_to_ironwood() {
1260 assert_eq!(
1263 select_change_pool(
1264 &flows(0, 10_000, 0),
1265 ShieldedPool::Sapling,
1266 &all_receivers(),
1267 true,
1268 Zatoshis::const_from_u64(5_000)
1269 ),
1270 ShieldedPool::Ironwood
1271 );
1272
1273 assert_eq!(
1276 select_change_pool(
1277 &flows(10_000, 10_000, 0),
1278 ShieldedPool::Sapling,
1279 &all_receivers(),
1280 true,
1281 Zatoshis::const_from_u64(5_000)
1282 ),
1283 ShieldedPool::Orchard
1284 );
1285
1286 assert_eq!(
1289 select_change_pool(
1290 &flows(0, 10_000, 10_000),
1291 ShieldedPool::Sapling,
1292 &all_receivers(),
1293 true,
1294 Zatoshis::const_from_u64(5_000)
1295 ),
1296 ShieldedPool::Ironwood
1297 );
1298
1299 assert_eq!(
1301 select_change_pool(
1302 &flows(0, 0, 10_000),
1303 ShieldedPool::Orchard,
1304 &all_receivers(),
1305 true,
1306 Zatoshis::const_from_u64(5_000)
1307 ),
1308 ShieldedPool::Sapling
1309 );
1310 }
1311
1312 #[test]
1313 fn select_change_pool_enforces_orchard_turnstile() {
1314 assert_eq!(
1317 select_change_pool(
1318 &flows(10_000, 0, 10_000),
1319 ShieldedPool::Sapling,
1320 &all_receivers(),
1321 false,
1322 Zatoshis::const_from_u64(15_000)
1323 ),
1324 ShieldedPool::Orchard
1325 );
1326
1327 assert_eq!(
1330 select_change_pool(
1331 &flows(10_000, 0, 10_000),
1332 ShieldedPool::Sapling,
1333 &all_receivers(),
1334 true,
1335 Zatoshis::const_from_u64(9_999)
1336 ),
1337 ShieldedPool::Orchard
1338 );
1339
1340 assert_eq!(
1343 select_change_pool(
1344 &flows(10_000, 0, 10_000),
1345 ShieldedPool::Sapling,
1346 &all_receivers(),
1347 true,
1348 Zatoshis::const_from_u64(10_000)
1349 ),
1350 ShieldedPool::Ironwood
1351 );
1352
1353 assert_eq!(
1356 select_change_pool(
1357 &NetFlows {
1358 t_in: Zatoshis::const_from_u64(10_000),
1359 t_out: Zatoshis::ZERO,
1360 sapling_in: Zatoshis::ZERO,
1361 sapling_out: Zatoshis::ZERO,
1362 orchard_in: Zatoshis::ZERO,
1363 orchard_out: Zatoshis::ZERO,
1364 ironwood_in: Zatoshis::ZERO,
1365 ironwood_out: Zatoshis::ZERO,
1366 },
1367 ShieldedPool::Orchard,
1368 &all_receivers(),
1369 true,
1370 Zatoshis::const_from_u64(10_000)
1371 ),
1372 ShieldedPool::Ironwood
1373 );
1374 }
1375
1376 #[test]
1377 fn select_change_pool_uses_a_pool_the_account_can_receive() {
1378 let orchard_out = |sapling_in: u64, orchard_out: u64| NetFlows {
1379 sapling_in: Zatoshis::const_from_u64(sapling_in),
1380 orchard_out: Zatoshis::const_from_u64(orchard_out),
1381 ..flows(0, 0, 0)
1382 };
1383
1384 assert_eq!(
1387 select_change_pool(
1388 &orchard_out(10_000, 5_000),
1389 ShieldedPool::Orchard,
1390 &receivers(true, false),
1391 false,
1392 Zatoshis::const_from_u64(4_000)
1393 ),
1394 ShieldedPool::Sapling
1395 );
1396
1397 assert_eq!(
1400 select_change_pool(
1401 &flows(0, 0, 10_000),
1402 ShieldedPool::Sapling,
1403 &receivers(false, true),
1404 false,
1405 Zatoshis::const_from_u64(5_000)
1406 ),
1407 ShieldedPool::Orchard
1408 );
1409
1410 assert_eq!(
1413 select_change_pool(
1414 &flows(0, 0, 10_000),
1415 ShieldedPool::Sapling,
1416 &receivers(false, true),
1417 true,
1418 Zatoshis::const_from_u64(5_000)
1419 ),
1420 ShieldedPool::Ironwood
1421 );
1422
1423 let transparent_flows = NetFlows {
1426 t_in: Zatoshis::const_from_u64(10_000),
1427 ..flows(0, 0, 0)
1428 };
1429 assert_eq!(
1430 select_change_pool(
1431 &transparent_flows,
1432 ShieldedPool::Orchard,
1433 &receivers(true, false),
1434 true,
1435 Zatoshis::const_from_u64(5_000)
1436 ),
1437 ShieldedPool::Sapling
1438 );
1439
1440 assert_eq!(
1443 select_change_pool(
1444 &orchard_out(10_000, 5_000),
1445 ShieldedPool::Sapling,
1446 &receivers(false, false),
1447 false,
1448 Zatoshis::const_from_u64(4_000)
1449 ),
1450 ShieldedPool::Orchard
1451 );
1452 }
1453}