1use core::cmp::{max, min, Ordering};
2use std::num::{NonZeroU64, NonZeroUsize};
3
4use zcash_primitives::transaction::fees::{
5 transparent, zip317::MINIMUM_FEE, zip317::P2PKH_STANDARD_OUTPUT_SIZE, FeeRule,
6};
7use zcash_protocol::{
8 consensus::{self, BlockHeight},
9 memo::MemoBytes,
10 value::{BalanceError, Zatoshis},
11 ShieldedProtocol,
12};
13
14use crate::data_api::AccountMeta;
15
16use super::{
17 sapling as sapling_fees, ChangeError, ChangeValue, DustAction, DustOutputPolicy,
18 EphemeralBalance, SplitPolicy, TransactionBalance,
19};
20
21#[cfg(feature = "orchard")]
22use super::orchard as orchard_fees;
23
24pub(crate) struct NetFlows {
25 t_in: Zatoshis,
26 t_out: Zatoshis,
27 sapling_in: Zatoshis,
28 sapling_out: Zatoshis,
29 orchard_in: Zatoshis,
30 orchard_out: Zatoshis,
31}
32
33impl NetFlows {
34 fn total_in(&self) -> Result<Zatoshis, BalanceError> {
35 (self.t_in + self.sapling_in + self.orchard_in).ok_or(BalanceError::Overflow)
36 }
37 fn total_out(&self) -> Result<Zatoshis, BalanceError> {
38 (self.t_out + self.sapling_out + self.orchard_out).ok_or(BalanceError::Overflow)
39 }
40 fn is_transparent(&self) -> bool {
42 !(self.sapling_in.is_positive()
43 || self.sapling_out.is_positive()
44 || self.orchard_in.is_positive()
45 || self.orchard_out.is_positive())
46 }
47}
48
49#[allow(clippy::too_many_arguments)]
50pub(crate) fn calculate_net_flows<NoteRefT: Clone, F: FeeRule, E>(
51 transparent_inputs: &[impl transparent::InputView],
52 transparent_outputs: &[impl transparent::OutputView],
53 sapling: &impl sapling_fees::BundleView<NoteRefT>,
54 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
55 ephemeral_balance: Option<&EphemeralBalance>,
56) -> Result<NetFlows, ChangeError<E, NoteRefT>>
57where
58 E: From<F::Error> + From<BalanceError>,
59{
60 let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
61
62 let t_in = transparent_inputs
63 .iter()
64 .map(|t_in| t_in.coin().value)
65 .chain(ephemeral_balance.and_then(|b| b.ephemeral_input_amount()))
66 .sum::<Option<_>>()
67 .ok_or_else(overflow)?;
68 let t_out = transparent_outputs
69 .iter()
70 .map(|t_out| t_out.value())
71 .chain(ephemeral_balance.and_then(|b| b.ephemeral_output_amount()))
72 .sum::<Option<_>>()
73 .ok_or_else(overflow)?;
74 let sapling_in = sapling
75 .inputs()
76 .iter()
77 .map(sapling_fees::InputView::<NoteRefT>::value)
78 .sum::<Option<_>>()
79 .ok_or_else(overflow)?;
80 let sapling_out = sapling
81 .outputs()
82 .iter()
83 .map(sapling_fees::OutputView::value)
84 .sum::<Option<_>>()
85 .ok_or_else(overflow)?;
86
87 #[cfg(feature = "orchard")]
88 let orchard_in = orchard
89 .inputs()
90 .iter()
91 .map(orchard_fees::InputView::<NoteRefT>::value)
92 .sum::<Option<_>>()
93 .ok_or_else(overflow)?;
94 #[cfg(not(feature = "orchard"))]
95 let orchard_in = Zatoshis::ZERO;
96
97 #[cfg(feature = "orchard")]
98 let orchard_out = orchard
99 .outputs()
100 .iter()
101 .map(orchard_fees::OutputView::value)
102 .sum::<Option<_>>()
103 .ok_or_else(overflow)?;
104 #[cfg(not(feature = "orchard"))]
105 let orchard_out = Zatoshis::ZERO;
106
107 Ok(NetFlows {
108 t_in,
109 t_out,
110 sapling_in,
111 sapling_out,
112 orchard_in,
113 orchard_out,
114 })
115}
116
117pub(crate) fn select_change_pool(
119 _net_flows: &NetFlows,
120 _fallback_change_pool: ShieldedProtocol,
121) -> ShieldedProtocol {
122 #[cfg(feature = "orchard")]
124 if _net_flows.orchard_in.is_positive() || _net_flows.orchard_out.is_positive() {
125 ShieldedProtocol::Orchard
127 } else if _net_flows.sapling_in.is_positive() || _net_flows.sapling_out.is_positive() {
128 ShieldedProtocol::Sapling
131 } else {
132 _fallback_change_pool
135 }
136 #[cfg(not(feature = "orchard"))]
137 ShieldedProtocol::Sapling
138}
139
140#[derive(Clone, Copy, Debug)]
141pub(crate) struct OutputManifest {
142 transparent: usize,
143 sapling: usize,
144 orchard: usize,
145}
146
147impl OutputManifest {
148 const ZERO: OutputManifest = OutputManifest {
149 transparent: 0,
150 sapling: 0,
151 orchard: 0,
152 };
153
154 pub(crate) fn sapling(&self) -> usize {
155 self.sapling
156 }
157
158 pub(crate) fn orchard(&self) -> usize {
159 self.orchard
160 }
161
162 pub(crate) fn total_shielded(&self) -> usize {
163 self.sapling + self.orchard
164 }
165}
166
167pub(crate) struct SinglePoolBalanceConfig<'a, P, F> {
168 params: &'a P,
169 fee_rule: &'a F,
170 dust_output_policy: &'a DustOutputPolicy,
171 default_dust_threshold: Zatoshis,
172 split_policy: &'a SplitPolicy,
173 fallback_change_pool: ShieldedProtocol,
174 marginal_fee: Zatoshis,
175 grace_actions: usize,
176}
177
178impl<'a, P, F> SinglePoolBalanceConfig<'a, P, F> {
179 #[allow(clippy::too_many_arguments)]
180 pub(crate) fn new(
181 params: &'a P,
182 fee_rule: &'a F,
183 dust_output_policy: &'a DustOutputPolicy,
184 default_dust_threshold: Zatoshis,
185 split_policy: &'a SplitPolicy,
186 fallback_change_pool: ShieldedProtocol,
187 marginal_fee: Zatoshis,
188 grace_actions: usize,
189 ) -> Self {
190 Self {
191 params,
192 fee_rule,
193 dust_output_policy,
194 default_dust_threshold,
195 split_policy,
196 fallback_change_pool,
197 marginal_fee,
198 grace_actions,
199 }
200 }
201}
202
203#[allow(clippy::too_many_arguments)]
204pub(crate) fn single_pool_output_balance<P: consensus::Parameters, NoteRefT: Clone, F: FeeRule, E>(
205 cfg: SinglePoolBalanceConfig<P, F>,
206 wallet_meta: Option<&AccountMeta>,
207 target_height: BlockHeight,
208 transparent_inputs: &[impl transparent::InputView],
209 transparent_outputs: &[impl transparent::OutputView],
210 sapling: &impl sapling_fees::BundleView<NoteRefT>,
211 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
212 change_memo: Option<&MemoBytes>,
213 ephemeral_balance: Option<&EphemeralBalance>,
214) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
215where
216 E: From<F::Error> + From<BalanceError>,
217{
218 let change_memo = change_memo.filter(|_| ephemeral_balance.map_or(true, |b| !b.is_input()));
222
223 let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
224 let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow));
225
226 let net_flows = calculate_net_flows::<NoteRefT, F, E>(
227 transparent_inputs,
228 transparent_outputs,
229 sapling,
230 #[cfg(feature = "orchard")]
231 orchard,
232 ephemeral_balance,
233 )?;
234
235 let change_pool = select_change_pool(&net_flows, cfg.fallback_change_pool);
236 let target_change_count = wallet_meta.map_or(1, |m| {
237 usize::from(cfg.split_policy.target_output_count)
238 .saturating_sub(m.total_note_count().unwrap_or(usize::MAX))
240 .max(1)
241 });
242 let target_change_counts = OutputManifest {
243 transparent: 0,
244 sapling: if change_pool == ShieldedProtocol::Sapling {
245 target_change_count
246 } else {
247 0
248 },
249 orchard: if change_pool == ShieldedProtocol::Orchard {
250 target_change_count
251 } else {
252 0
253 },
254 };
255 assert!(target_change_counts.total_shielded() == target_change_count);
256
257 let fully_transparent = net_flows.is_transparent() && change_memo.is_none();
259
260 if cfg.marginal_fee.is_positive() {
264 let possible_change = {
268 if fully_transparent
270 || (cfg.dust_output_policy.action() == DustAction::AddDustToFee
271 && change_memo.is_none())
272 {
273 vec![OutputManifest::ZERO, target_change_counts]
274 } else {
275 vec![target_change_counts]
276 }
277 };
278
279 check_for_uneconomic_inputs(
280 transparent_inputs,
281 transparent_outputs,
282 sapling,
283 #[cfg(feature = "orchard")]
284 orchard,
285 cfg.marginal_fee,
286 cfg.grace_actions,
287 &possible_change[..],
288 ephemeral_balance,
289 )?;
290 }
291
292 let total_in = net_flows
293 .total_in()
294 .map_err(|e| ChangeError::StrategyError(E::from(e)))?;
295 let subtotal_out = net_flows
296 .total_out()
297 .map_err(|e| ChangeError::StrategyError(E::from(e)))?;
298
299 let sapling_input_count = sapling
300 .bundle_type()
301 .num_spends(sapling.inputs().len())
302 .map_err(ChangeError::BundleError)?;
303 let sapling_output_count = |change_count| {
304 sapling
305 .bundle_type()
306 .num_outputs(
307 sapling.inputs().len(),
308 sapling.outputs().len() + change_count,
309 )
310 .map_err(ChangeError::BundleError)
311 };
312
313 #[cfg(feature = "orchard")]
314 let orchard_action_count = |change_count| {
315 orchard
316 .bundle_type()
317 .num_actions(
318 orchard.inputs().len(),
319 orchard.outputs().len() + change_count,
320 )
321 .map_err(ChangeError::BundleError)
322 };
323 #[cfg(not(feature = "orchard"))]
324 let orchard_action_count = |change_count: usize| -> Result<usize, ChangeError<E, NoteRefT>> {
325 if change_count != 0 {
326 Err(ChangeError::BundleError(
327 "Nonzero Orchard change requested but the `orchard` feature is not enabled.",
328 ))
329 } else {
330 Ok(0)
331 }
332 };
333
334 let transparent_input_sizes = transparent_inputs
335 .iter()
336 .map(|i| i.serialized_size())
337 .chain(
338 ephemeral_balance
339 .and_then(|b| b.ephemeral_input_amount())
340 .map(|_| transparent::InputSize::STANDARD_P2PKH),
341 );
342 let transparent_output_sizes = transparent_outputs
343 .iter()
344 .map(|i| i.serialized_size())
345 .chain(
346 ephemeral_balance
347 .and_then(|b| b.ephemeral_output_amount())
348 .map(|_| P2PKH_STANDARD_OUTPUT_SIZE),
349 );
350
351 let min_fee = cfg
372 .fee_rule
373 .fee_required(
374 cfg.params,
375 target_height,
376 transparent_input_sizes.clone(),
377 transparent_output_sizes.clone(),
378 sapling_input_count,
379 sapling_output_count(0)?,
380 orchard_action_count(0)?,
381 )
382 .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
383
384 let total_out_with_min_fee = (subtotal_out + min_fee).ok_or_else(overflow)?;
385
386 #[allow(unused_mut)]
387 let (mut change, fee) = match total_in.cmp(&total_out_with_min_fee) {
388 Ordering::Less => {
389 return Err(ChangeError::InsufficientFunds {
392 available: total_in,
393 required: total_out_with_min_fee,
394 });
395 }
396 Ordering::Equal if fully_transparent => {
397 (vec![], min_fee)
400 }
401 _ => {
402 let max_fee = max(
403 min_fee,
404 cfg.fee_rule
405 .fee_required(
406 cfg.params,
407 target_height,
408 transparent_input_sizes.clone(),
409 transparent_output_sizes.clone(),
410 sapling_input_count,
411 sapling_output_count(target_change_counts.sapling())?,
412 orchard_action_count(target_change_counts.orchard())?,
413 )
414 .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?,
415 );
416
417 let total_out_with_max_fee = (subtotal_out + max_fee).ok_or_else(overflow)?;
418
419 let split_count = usize::from(wallet_meta.map_or(NonZeroUsize::MIN, |wm| {
423 cfg.split_policy.split_count(
424 wm.total_note_count(),
425 wm.total_value(),
426 (total_in - total_out_with_max_fee).unwrap_or(Zatoshis::ZERO),
430 )
431 }));
432
433 let total_fee = if split_count < target_change_count {
435 cfg.fee_rule
436 .fee_required(
437 cfg.params,
438 target_height,
439 transparent_input_sizes,
440 transparent_output_sizes,
441 sapling_input_count,
442 sapling_output_count(if change_pool == ShieldedProtocol::Sapling {
443 split_count
444 } else {
445 0
446 })?,
447 orchard_action_count(if change_pool == ShieldedProtocol::Orchard {
448 split_count
449 } else {
450 0
451 })?,
452 )
453 .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?
454 } else {
455 max_fee
456 };
457
458 let total_out = (subtotal_out + total_fee).ok_or_else(overflow)?;
459 let total_change =
460 (total_in - total_out).ok_or_else(|| ChangeError::InsufficientFunds {
461 available: total_in,
462 required: total_out,
463 })?;
464
465 let per_output_change = total_change.div_with_remainder(
466 NonZeroU64::new(u64::try_from(split_count).expect("usize fits into u64")).unwrap(),
467 );
468 let simple_case = || {
469 (
470 (0usize..split_count)
471 .map(|i| {
472 ChangeValue::shielded(
473 change_pool,
474 if i == 0 {
475 (*per_output_change.quotient() + *per_output_change.remainder())
477 .unwrap()
478 } else {
479 *per_output_change.quotient()
482 },
483 change_memo.cloned(),
484 )
485 })
486 .collect(),
487 total_fee,
488 )
489 };
490
491 let change_dust_threshold = cfg
492 .dust_output_policy
493 .dust_threshold()
494 .unwrap_or(cfg.default_dust_threshold);
495
496 if total_change < change_dust_threshold {
497 match cfg.dust_output_policy.action() {
498 DustAction::Reject => {
499 if total_change.is_zero() {
506 simple_case()
507 } else {
508 let shortfall =
509 (change_dust_threshold - total_change).ok_or_else(underflow)?;
510
511 return Err(ChangeError::InsufficientFunds {
512 available: total_in,
513 required: (total_in + shortfall).ok_or_else(overflow)?,
514 });
515 }
516 }
517 DustAction::AllowDustChange => simple_case(),
518 DustAction::AddDustToFee => {
519 let fee_with_dust = (total_change + total_fee).ok_or_else(overflow)?;
522
523 let reasonable_fee =
524 (total_fee + (MINIMUM_FEE * 10u64).unwrap()).ok_or_else(overflow)?;
525
526 if fee_with_dust > reasonable_fee {
527 simple_case()
530 } else if change_memo.is_some() {
531 (
532 vec![ChangeValue::shielded(
533 change_pool,
534 Zatoshis::ZERO,
535 change_memo.cloned(),
536 )],
537 fee_with_dust,
538 )
539 } else {
540 (vec![], fee_with_dust)
541 }
542 }
543 }
544 } else {
545 simple_case()
546 }
547 }
548 };
549
550 #[cfg(feature = "transparent-inputs")]
551 change.extend(
552 ephemeral_balance
553 .and_then(|b| b.ephemeral_output_amount())
554 .map(ChangeValue::ephemeral_transparent),
555 );
556
557 TransactionBalance::new(change, fee).map_err(|_| overflow())
558}
559
560#[allow(clippy::too_many_arguments)]
574pub(crate) fn check_for_uneconomic_inputs<NoteRefT: Clone, E>(
575 transparent_inputs: &[impl transparent::InputView],
576 transparent_outputs: &[impl transparent::OutputView],
577 sapling: &impl sapling_fees::BundleView<NoteRefT>,
578 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
579 marginal_fee: Zatoshis,
580 grace_actions: usize,
581 possible_change: &[OutputManifest],
582 ephemeral_balance: Option<&EphemeralBalance>,
583) -> Result<(), ChangeError<E, NoteRefT>> {
584 let mut t_dust: Vec<_> = transparent_inputs
585 .iter()
586 .filter_map(|i| {
587 if i.coin().value <= marginal_fee {
590 Some(i.outpoint().clone())
591 } else {
592 None
593 }
594 })
595 .collect();
596
597 let mut s_dust: Vec<_> = sapling
598 .inputs()
599 .iter()
600 .filter_map(|i| {
601 if sapling_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
602 Some(sapling_fees::InputView::<NoteRefT>::note_id(i).clone())
603 } else {
604 None
605 }
606 })
607 .collect();
608
609 #[cfg(feature = "orchard")]
610 let mut o_dust: Vec<NoteRefT> = orchard
611 .inputs()
612 .iter()
613 .filter_map(|i| {
614 if orchard_fees::InputView::<NoteRefT>::value(i) <= marginal_fee {
615 Some(orchard_fees::InputView::<NoteRefT>::note_id(i).clone())
616 } else {
617 None
618 }
619 })
620 .collect();
621 #[cfg(not(feature = "orchard"))]
622 let mut o_dust: Vec<NoteRefT> = vec![];
623
624 if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() {
626 return Ok(());
627 }
628
629 let (t_inputs_len, t_outputs_len) = (
630 transparent_inputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_input())),
631 transparent_outputs.len() + usize::from(ephemeral_balance.is_some_and(|b| b.is_output())),
632 );
633 let (s_inputs_len, s_outputs_len) = (sapling.inputs().len(), sapling.outputs().len());
634 #[cfg(feature = "orchard")]
635 let (o_inputs_len, o_outputs_len) = (orchard.inputs().len(), orchard.outputs().len());
636 #[cfg(not(feature = "orchard"))]
637 let (o_inputs_len, o_outputs_len) = (0usize, 0usize);
638
639 let t_non_dust = t_inputs_len.checked_sub(t_dust.len()).unwrap();
640 let s_non_dust = s_inputs_len.checked_sub(s_dust.len()).unwrap();
641 let o_non_dust = o_inputs_len.checked_sub(o_dust.len()).unwrap();
642
643 let allowed_dust = |change: &OutputManifest| {
645 let t_allowed = min(
658 t_dust.len(),
659 (t_outputs_len + change.transparent).saturating_sub(t_non_dust),
660 );
661 let s_allowed = min(
662 s_dust.len(),
663 (s_outputs_len + change.sapling).saturating_sub(s_non_dust),
664 );
665 let o_allowed = min(
666 o_dust.len(),
667 (o_outputs_len + change.orchard).saturating_sub(o_non_dust),
668 );
669
670 let t_req_inputs = t_non_dust + t_allowed;
672 let s_req_inputs = s_non_dust + s_allowed;
673 #[cfg(feature = "orchard")]
674 let o_req_inputs = o_non_dust + o_allowed;
675
676 let hypothetical_actions = |t_extra, s_extra, _o_extra| {
684 let s_spend_count = sapling
685 .bundle_type()
686 .num_spends(s_req_inputs + s_extra)
687 .map_err(ChangeError::BundleError)?;
688
689 let s_output_count = sapling
690 .bundle_type()
691 .num_outputs(s_req_inputs + s_extra, s_outputs_len + change.sapling)
692 .map_err(ChangeError::BundleError)?;
693
694 #[cfg(feature = "orchard")]
695 let o_action_count = orchard
696 .bundle_type()
697 .num_actions(o_req_inputs + _o_extra, o_outputs_len + change.orchard)
698 .map_err(ChangeError::BundleError)?;
699 #[cfg(not(feature = "orchard"))]
700 let o_action_count = 0;
701
702 Ok(
705 max(t_req_inputs + t_extra, t_outputs_len + change.transparent)
706 + max(s_spend_count, s_output_count)
707 + o_action_count,
708 )
709 };
710
711 let baseline = hypothetical_actions(0, 0, 0)?;
721
722 let (t_extra, s_extra, o_extra) = if baseline >= grace_actions {
723 (0, 0, 0)
724 } else if t_dust.len() > t_allowed && hypothetical_actions(1, 0, 0)? <= baseline {
725 (1, 0, 0)
726 } else if s_dust.len() > s_allowed && hypothetical_actions(0, 1, 0)? <= baseline {
727 (0, 1, 0)
728 } else if o_dust.len() > o_allowed && hypothetical_actions(0, 0, 1)? <= baseline {
729 (0, 0, 1)
730 } else {
731 (0, 0, 0)
732 };
733 Ok(OutputManifest {
734 transparent: t_allowed + t_extra,
735 sapling: s_allowed + s_extra,
736 orchard: o_allowed + o_extra,
737 })
738 };
739
740 let allowed = possible_change
742 .iter()
743 .map(allowed_dust)
744 .collect::<Result<Vec<_>, _>>()?
745 .into_iter()
746 .reduce(|l, r| OutputManifest {
747 transparent: min(l.transparent, r.transparent),
748 sapling: min(l.sapling, r.sapling),
749 orchard: min(l.orchard, r.orchard),
750 })
751 .expect("possible_change is nonempty");
752
753 let t_dust = t_dust.split_off(allowed.transparent);
756 let s_dust = s_dust.split_off(allowed.sapling);
757 let o_dust = o_dust.split_off(allowed.orchard);
758
759 if t_dust.is_empty() && s_dust.is_empty() && o_dust.is_empty() {
760 Ok(())
761 } else {
762 Err(ChangeError::DustInputs {
763 transparent: t_dust,
764 sapling: s_dust,
765 #[cfg(feature = "orchard")]
766 orchard: o_dust,
767 })
768 }
769}