zcash_client_backend/fees.rs
1use std::{
2 convert::Infallible,
3 fmt::{self, Debug, Display},
4 num::{NonZeroU64, NonZeroUsize},
5};
6
7use ::transparent::bundle::OutPoint;
8use zcash_primitives::transaction::fees::{
9 FeeRule,
10 transparent::{self, InputSize},
11 zip317 as prim_zip317,
12};
13use zcash_protocol::{
14 PoolType, ShieldedPool,
15 consensus::{self, BlockHeight},
16 memo::MemoBytes,
17 value::{BalanceError, Zatoshis},
18};
19
20use crate::data_api::{
21 ChangeReceivers, InputSource, anchor_retention::PoolMigrationParams, wallet::TargetHeight,
22};
23
24pub mod common;
25#[cfg(feature = "non-standard-fees")]
26pub mod fixed;
27#[cfg(feature = "orchard")]
28pub mod orchard;
29pub mod sapling;
30pub mod standard;
31pub mod zip317;
32
33/// An enumeration of the standard fee rules supported by the wallet backend.
34#[derive(Debug, Copy, Clone, PartialEq, Eq)]
35pub enum StandardFeeRule {
36 Zip317,
37}
38
39impl FeeRule for StandardFeeRule {
40 type Error = prim_zip317::FeeError;
41
42 fn fee_required<P: consensus::Parameters>(
43 &self,
44 params: &P,
45 target_height: BlockHeight,
46 transparent_input_sizes: impl IntoIterator<Item = InputSize>,
47 transparent_output_sizes: impl IntoIterator<Item = usize>,
48 sapling_input_count: usize,
49 sapling_output_count: usize,
50 orchard_action_count: usize,
51 ironwood_action_count: usize,
52 ) -> Result<Zatoshis, Self::Error> {
53 #[allow(deprecated)]
54 match self {
55 Self::Zip317 => prim_zip317::FeeRule::standard().fee_required(
56 params,
57 target_height,
58 transparent_input_sizes,
59 transparent_output_sizes,
60 sapling_input_count,
61 sapling_output_count,
62 orchard_action_count,
63 ironwood_action_count,
64 ),
65 }
66 }
67}
68
69/// A policy that determines how change should be returned to the wallet when the net flows of a
70/// transaction under construction are fully transparent.
71///
72/// This policy has no effect on transactions that have any shielded inputs or outputs; change
73/// for such transactions is always returned to a shielded pool, irrespective of the policy in
74/// use. When the flows of a transaction are fully transparent, shielding change (the default)
75/// reveals the change amount as the value of the shielded output(s) in an otherwise-transparent
76/// transaction; returning the change to the transparent pool matches the behavior of
77/// transparent-only wallets (including `zcashd`) at the cost of the change remaining unshielded.
78///
79/// Transparent change is currently always sent to a P2PKH address derived under the wallet
80/// account's internal scope; returning change to the originating address when spending from a
81/// P2SH (e.g. multisig) address is not yet supported. See [zcash/librustzcash#2570] for
82/// details.
83///
84/// [zcash/librustzcash#2570]: https://github.com/zcash/librustzcash/issues/2570
85#[cfg(feature = "transparent-inputs")]
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
87pub enum TransparentChangePolicy {
88 /// Change is always returned to a shielded pool, even when the net flows of the transaction
89 /// are fully transparent.
90 ///
91 /// This is the default policy.
92 #[default]
93 ShieldChange,
94 /// When the net flows of the transaction are fully transparent, change is returned to the
95 /// transparent pool at an internal-scope (change) transparent address of the wallet, as
96 /// described in [BIP 44].
97 ///
98 /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
99 TransparentChangeAllowed,
100}
101
102/// `ChangeValue` represents either a proposed change output to a shielded pool
103/// (with an optional change memo), or if the "transparent-inputs" feature is
104/// enabled, an output to the transparent pool: either an ephemeral output as
105/// part of a [ZIP 320] transaction pair, or a change output to an
106/// internal-scope (change) transparent address of the wallet.
107///
108/// [ZIP 320]: https://zips.z.cash/zip-0320
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct ChangeValue(ChangeValueInner);
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113enum ChangeValueInner {
114 Shielded {
115 protocol: ShieldedPool,
116 value: Zatoshis,
117 memo: Option<MemoBytes>,
118 },
119 #[cfg(feature = "transparent-inputs")]
120 EphemeralTransparent { value: Zatoshis },
121 #[cfg(feature = "transparent-inputs")]
122 Transparent { value: Zatoshis },
123}
124
125impl ChangeValue {
126 /// Constructs a new ephemeral transparent output value.
127 #[cfg(feature = "transparent-inputs")]
128 pub fn ephemeral_transparent(value: Zatoshis) -> Self {
129 Self(ChangeValueInner::EphemeralTransparent { value })
130 }
131
132 /// Constructs a new change value that will be created as a non-ephemeral transparent output
133 /// sent to an internal-scope (change) transparent address of the wallet.
134 #[cfg(feature = "transparent-inputs")]
135 pub fn transparent(value: Zatoshis) -> Self {
136 Self(ChangeValueInner::Transparent { value })
137 }
138
139 /// Constructs a new change value that will be created as a shielded output.
140 pub fn shielded(protocol: ShieldedPool, value: Zatoshis, memo: Option<MemoBytes>) -> Self {
141 Self(ChangeValueInner::Shielded {
142 protocol,
143 value,
144 memo,
145 })
146 }
147
148 /// Constructs a new change value that will be created as a Sapling output.
149 pub fn sapling(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
150 Self::shielded(ShieldedPool::Sapling, value, memo)
151 }
152
153 /// Constructs a new change value that will be created as an Orchard output.
154 #[cfg(feature = "orchard")]
155 pub fn orchard(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
156 Self::shielded(ShieldedPool::Orchard, value, memo)
157 }
158
159 /// Constructs a new change value that will be created as an Ironwood output.
160 #[cfg(feature = "orchard")]
161 pub fn ironwood(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
162 Self::shielded(ShieldedPool::Ironwood, value, memo)
163 }
164
165 /// Returns the pool to which the change or ephemeral output should be sent.
166 pub fn output_pool(&self) -> PoolType {
167 match &self.0 {
168 ChangeValueInner::Shielded { protocol, .. } => PoolType::Shielded(*protocol),
169 #[cfg(feature = "transparent-inputs")]
170 ChangeValueInner::EphemeralTransparent { .. } => PoolType::Transparent,
171 #[cfg(feature = "transparent-inputs")]
172 ChangeValueInner::Transparent { .. } => PoolType::Transparent,
173 }
174 }
175
176 /// Returns the value of the change or ephemeral output to be created, in zatoshis.
177 pub fn value(&self) -> Zatoshis {
178 match &self.0 {
179 ChangeValueInner::Shielded { value, .. } => *value,
180 #[cfg(feature = "transparent-inputs")]
181 ChangeValueInner::EphemeralTransparent { value } => *value,
182 #[cfg(feature = "transparent-inputs")]
183 ChangeValueInner::Transparent { value } => *value,
184 }
185 }
186
187 /// Returns the memo to be associated with the output.
188 pub fn memo(&self) -> Option<&MemoBytes> {
189 match &self.0 {
190 ChangeValueInner::Shielded { memo, .. } => memo.as_ref(),
191 #[cfg(feature = "transparent-inputs")]
192 ChangeValueInner::EphemeralTransparent { .. } => None,
193 #[cfg(feature = "transparent-inputs")]
194 ChangeValueInner::Transparent { .. } => None,
195 }
196 }
197
198 /// Whether this is to be an ephemeral output.
199 #[cfg_attr(
200 not(feature = "transparent-inputs"),
201 doc = "This is always false because the `transparent-inputs` feature is
202 not enabled."
203 )]
204 pub fn is_ephemeral(&self) -> bool {
205 match &self.0 {
206 ChangeValueInner::Shielded { .. } => false,
207 #[cfg(feature = "transparent-inputs")]
208 ChangeValueInner::EphemeralTransparent { .. } => true,
209 #[cfg(feature = "transparent-inputs")]
210 ChangeValueInner::Transparent { .. } => false,
211 }
212 }
213}
214
215/// Orchard actions in a canonical ZIP 318 crossing: the spend and its change, or a padding dummy
216/// when the note's value exactly covers the crossing and its fee.
217#[cfg(feature = "orchard")]
218const CANONICAL_CROSSING_ORCHARD_ACTIONS: usize = 2;
219
220/// Ironwood actions in a canonical ZIP 318 crossing: the single unpadded output.
221#[cfg(feature = "orchard")]
222const CANONICAL_CROSSING_IRONWOOD_ACTIONS: usize = 1;
223
224/// The fee a canonical ZIP 318 crossing pays at `target_height`, obtained by asking the STANDARD
225/// ZIP 317 rule what the canonical shape costs.
226///
227/// ZIP 318 requires this exact fee. Any other value partitions the anonymity set, so a transaction
228/// paying a non-standard fee is not a canonical crossing however well its structure matches. The
229/// standard rule is used rather than the caller's: a proposal built on a fixed non-standard rule
230/// would otherwise be compared against its own fee and always agree.
231#[cfg(feature = "orchard")]
232pub fn canonical_crossing_fee<P: consensus::Parameters>(
233 params: &P,
234 target_height: BlockHeight,
235) -> Result<Zatoshis, zcash_primitives::transaction::fees::zip317::FeeError> {
236 prim_zip317::FeeRule::standard().fee_required(
237 params,
238 target_height,
239 std::iter::empty::<InputSize>(),
240 std::iter::empty::<usize>(),
241 0,
242 0,
243 CANONICAL_CROSSING_ORCHARD_ACTIONS,
244 CANONICAL_CROSSING_IRONWOOD_ACTIONS,
245 )
246}
247
248/// The amount of change and fees required to make a transaction's inputs and
249/// outputs balance under a specific fee rule, as computed by a particular
250/// [`ChangeStrategy`] that is aware of that rule.
251#[derive(Clone, Debug, PartialEq, Eq)]
252pub struct TransactionBalance {
253 proposed_change: Vec<ChangeValue>,
254 fee_required: Zatoshis,
255
256 // A cache for the sum of proposed change and fee; we compute it on construction anyway, so we
257 // cache the resulting value.
258 total: Zatoshis,
259
260 // The exact number of dummy outputs in each shielded bundle this balance was costed for.
261 // `None` is retained for compatibility with callers and serialized proposals that predate
262 // explicit transaction-shape modelling.
263 dummy_outputs: Option<DummyOutputCounts>,
264}
265
266/// The number of dummy outputs in each shielded value pool.
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub struct DummyOutputCounts {
269 sapling: usize,
270 #[cfg(feature = "orchard")]
271 orchard: usize,
272 #[cfg(feature = "orchard")]
273 ironwood: usize,
274}
275
276impl DummyOutputCounts {
277 /// Constructs per-pool dummy-output counts.
278 pub fn new(
279 sapling: usize,
280 #[cfg(feature = "orchard")] orchard: usize,
281 #[cfg(feature = "orchard")] ironwood: usize,
282 ) -> Self {
283 Self {
284 sapling,
285 #[cfg(feature = "orchard")]
286 orchard,
287 #[cfg(feature = "orchard")]
288 ironwood,
289 }
290 }
291
292 /// Returns the number of Sapling dummy outputs.
293 pub fn sapling(&self) -> usize {
294 self.sapling
295 }
296
297 /// Returns the number of Orchard dummy outputs.
298 #[cfg(feature = "orchard")]
299 pub fn orchard(&self) -> usize {
300 self.orchard
301 }
302
303 /// Returns the number of Ironwood dummy outputs.
304 #[cfg(feature = "orchard")]
305 pub fn ironwood(&self) -> usize {
306 self.ironwood
307 }
308}
309
310impl TransactionBalance {
311 /// Constructs a new balance from its constituent parts.
312 pub fn new(
313 proposed_change: Vec<ChangeValue>,
314 fee_required: Zatoshis,
315 ) -> Result<Self, BalanceError> {
316 let total = proposed_change
317 .iter()
318 .map(|c| c.value())
319 .chain(Some(fee_required))
320 .sum::<Option<Zatoshis>>()
321 .ok_or(BalanceError::Overflow)?;
322
323 Ok(Self {
324 proposed_change,
325 fee_required,
326 total,
327 dummy_outputs: None,
328 })
329 }
330
331 /// Records the exact dummy-output counts this balance was computed for.
332 pub fn with_dummy_outputs(mut self, dummy_outputs: DummyOutputCounts) -> Self {
333 self.dummy_outputs = Some(dummy_outputs);
334 self
335 }
336
337 /// Returns the exact dummy-output counts this balance was computed for, when recorded.
338 pub fn dummy_outputs(&self) -> Option<DummyOutputCounts> {
339 self.dummy_outputs
340 }
341
342 /// The change values proposed by the [`ChangeStrategy`] that computed this balance.
343 pub fn proposed_change(&self) -> &[ChangeValue] {
344 &self.proposed_change
345 }
346
347 /// Returns the fee computed for the transaction, assuming that the suggested
348 /// change outputs are added to the transaction.
349 pub fn fee_required(&self) -> Zatoshis {
350 self.fee_required
351 }
352
353 /// Returns the sum of the proposed change outputs and the required fee.
354 pub fn total(&self) -> Zatoshis {
355 self.total
356 }
357}
358
359/// Errors that can occur in computing suggested change and/or fees.
360#[derive(Clone, Debug, PartialEq, Eq)]
361#[non_exhaustive]
362pub enum ChangeError<E, NoteRefT> {
363 /// Insufficient inputs were provided to change selection to fund the
364 /// required outputs and fees.
365 InsufficientFunds {
366 /// The total of the inputs provided to change selection
367 available: Zatoshis,
368 /// The total amount of input value required to fund the requested outputs,
369 /// including the required fees.
370 required: Zatoshis,
371 },
372 /// Some of the inputs provided to the transaction have value less than the
373 /// marginal fee, and could not be determined to have any economic value in
374 /// the context of this input selection.
375 ///
376 /// This determination is potentially conservative in the sense that inputs
377 /// with value less than or equal to the marginal fee might be excluded, even
378 /// though in practice they would not cause the fee to increase. Inputs with
379 /// value greater than the marginal fee will never be excluded.
380 ///
381 /// The ordering of the inputs in each list is unspecified.
382 DustInputs {
383 /// The outpoints for transparent inputs that could not be determined to
384 /// have economic value in the context of this input selection.
385 transparent: Vec<OutPoint>,
386 /// The identifiers for Sapling inputs that could not be determined to
387 /// have economic value in the context of this input selection.
388 sapling: Vec<NoteRefT>,
389 /// The identifiers for Orchard inputs that could not be determined to
390 /// have economic value in the context of this input selection.
391 #[cfg(feature = "orchard")]
392 orchard: Vec<NoteRefT>,
393 /// The identifiers for Ironwood inputs that could not be determined to
394 /// have economic value in the context of this input selection.
395 #[cfg(feature = "orchard")]
396 ironwood: Vec<NoteRefT>,
397 },
398 /// The change strategy would send change to a pool in which the account cannot receive
399 /// it: the account's viewing key cannot derive an address in that pool (for an ephemeral
400 /// transparent output, a P2PKH address).
401 UnsupportedChangePool(PoolType),
402 /// An error occurred that was specific to the change selection strategy in use.
403 StrategyError(E),
404 /// The proposed bundle structure would violate bundle type construction rules.
405 BundleError(&'static str),
406}
407
408impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
409 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
410 match &self {
411 ChangeError::InsufficientFunds {
412 available,
413 required,
414 } => write!(
415 f,
416 "Insufficient funds: required {} zatoshis, but only {} zatoshis were available.",
417 u64::from(*required),
418 u64::from(*available)
419 ),
420 ChangeError::DustInputs {
421 transparent,
422 sapling,
423 #[cfg(feature = "orchard")]
424 orchard,
425 #[cfg(feature = "orchard")]
426 ironwood,
427 } => {
428 #[cfg(feature = "orchard")]
429 let orchard_len = orchard.len() + ironwood.len();
430 #[cfg(not(feature = "orchard"))]
431 let orchard_len = 0;
432
433 // we can't encode the UA to its string representation because we
434 // don't have network parameters here
435 write!(
436 f,
437 "Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.",
438 transparent.len() + sapling.len() + orchard_len,
439 )
440 }
441 ChangeError::UnsupportedChangePool(pool) => {
442 write!(f, "The account cannot receive change in the {pool} pool.")
443 }
444 ChangeError::StrategyError(err) => {
445 write!(f, "{err}")
446 }
447 ChangeError::BundleError(err) => {
448 write!(
449 f,
450 "The proposed transaction structure violates bundle type constraints: {err}"
451 )
452 }
453 }
454 }
455}
456
457impl<E, N> std::error::Error for ChangeError<E, N>
458where
459 E: Debug + Display + std::error::Error + 'static,
460 N: Debug + Display + 'static,
461{
462 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
463 match &self {
464 ChangeError::StrategyError(e) => Some(e),
465 _ => None,
466 }
467 }
468}
469
470/// An enumeration of actions to take when a transaction would potentially create dust
471/// outputs (outputs that are likely to be without economic value due to fee rules).
472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
473pub enum DustAction {
474 /// Do not allow creation of dust outputs; instead, require that additional inputs be provided.
475 Reject,
476 /// Explicitly allow the creation of dust change amounts greater than the specified value.
477 AllowDustChange,
478 /// Allow dust amounts to be added to the transaction fee.
479 AddDustToFee,
480}
481
482/// A policy describing how a [`ChangeStrategy`] should treat potentially dust-valued change
483/// outputs (outputs that are likely to be without economic value due to fee rules).
484#[derive(Clone, Copy, Debug, PartialEq, Eq)]
485pub struct DustOutputPolicy {
486 action: DustAction,
487 dust_threshold: Option<Zatoshis>,
488}
489
490impl DustOutputPolicy {
491 /// Constructs a new dust output policy.
492 ///
493 /// A dust policy created with `None` as the dust threshold will delegate determination
494 /// of the dust threshold to the change strategy that is evaluating the strategy; this
495 /// is recommended, but an explicit value (including zero) may be provided to explicitly
496 /// override the determination of the change strategy.
497 pub fn new(action: DustAction, dust_threshold: Option<Zatoshis>) -> Self {
498 Self {
499 action,
500 dust_threshold,
501 }
502 }
503
504 /// Returns the action to take in the event that a dust change amount would be produced.
505 pub fn action(&self) -> DustAction {
506 self.action
507 }
508 /// Returns a value that will be used to override the dust determination logic of the
509 /// change policy, if any.
510 pub fn dust_threshold(&self) -> Option<Zatoshis> {
511 self.dust_threshold
512 }
513}
514
515impl Default for DustOutputPolicy {
516 fn default() -> Self {
517 DustOutputPolicy::new(DustAction::Reject, None)
518 }
519}
520
521/// A policy that describes how change output should be split into multiple notes for the purpose
522/// of note management.
523///
524/// If an account contains at least [`Self::target_output_count`] notes having at least value
525/// [`Self::min_split_output_value`], this policy will recommend a single output; if the account
526/// contains fewer such notes, this policy will recommend that multiple outputs be produced in
527/// order to achieve the target.
528#[derive(Clone, Copy, Debug)]
529pub struct SplitPolicy {
530 target_output_count: NonZeroUsize,
531 min_split_output_value: Option<Zatoshis>,
532}
533
534impl SplitPolicy {
535 /// In the case that no other conditions provided by the user are available to fall back on,
536 /// a default value of [`MARGINAL_FEE`] * 100 will be used as the "minimum usable note value"
537 /// when retrieving wallet metadata.
538 ///
539 /// [`MARGINAL_FEE`]: zcash_primitives::transaction::fees::zip317::MARGINAL_FEE
540 pub(crate) const MIN_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(500000);
541
542 /// Constructs a new [`SplitPolicy`] that splits change to ensure the given number of spendable
543 /// outputs exists within an account, each having at least the specified minimum note value.
544 pub fn with_min_output_value(
545 target_output_count: NonZeroUsize,
546 min_split_output_value: Zatoshis,
547 ) -> Self {
548 Self {
549 target_output_count,
550 min_split_output_value: Some(min_split_output_value),
551 }
552 }
553
554 /// Constructs a [`SplitPolicy`] that prescribes a single output (no splitting).
555 pub fn single_output() -> Self {
556 Self {
557 target_output_count: NonZeroUsize::MIN,
558 min_split_output_value: None,
559 }
560 }
561
562 /// Returns the number of outputs that this policy will attempt to ensure that the wallet has
563 /// available for spending.
564 pub fn target_output_count(&self) -> NonZeroUsize {
565 self.target_output_count
566 }
567
568 /// Returns the minimum value for a note resulting from splitting of change.
569 pub fn min_split_output_value(&self) -> Option<Zatoshis> {
570 self.min_split_output_value
571 }
572
573 /// Returns the number of output notes to produce from the given total change value, given the
574 /// total value and number of existing unspent notes in the account and this policy.
575 ///
576 /// If splitting change to produce [`Self::target_output_count`] would result in notes of value
577 /// less than [`Self::min_split_output_value`], then this will suggest a smaller number of
578 /// splits so that each resulting change note has sufficient value.
579 pub fn split_count(
580 &self,
581 existing_notes: Option<usize>,
582 existing_notes_total: Option<Zatoshis>,
583 total_change: Zatoshis,
584 ) -> NonZeroUsize {
585 fn to_nonzero_u64(value: usize) -> NonZeroU64 {
586 NonZeroU64::new(u64::try_from(value).expect("usize fits into u64"))
587 .expect("NonZeroU64 input derived from NonZeroUsize")
588 }
589
590 let mut split_count = NonZeroUsize::new(
591 usize::from(self.target_output_count)
592 .saturating_sub(existing_notes.unwrap_or(usize::MAX)),
593 )
594 .unwrap_or(NonZeroUsize::MIN);
595
596 let min_split_output_value = self.min_split_output_value.or_else(|| {
597 // If no minimum split output size is set, we choose the minimum split size to be a
598 // quarter of the average value of notes in the wallet after the transaction.
599 (existing_notes_total + total_change).map(|total| {
600 *total
601 .div_with_remainder(to_nonzero_u64(
602 usize::from(self.target_output_count).saturating_mul(4),
603 ))
604 .quotient()
605 })
606 });
607
608 if let Some(min_split_output_value) = min_split_output_value {
609 loop {
610 let per_output_change =
611 total_change.div_with_remainder(to_nonzero_u64(usize::from(split_count)));
612 if *per_output_change.quotient() >= min_split_output_value {
613 return split_count;
614 } else if let Some(new_count) = NonZeroUsize::new(usize::from(split_count) - 1) {
615 split_count = new_count;
616 } else {
617 // We always create at least one change output.
618 return NonZeroUsize::MIN;
619 }
620 }
621 } else {
622 NonZeroUsize::MIN
623 }
624 }
625}
626
627/// `EphemeralBalance` describes the ephemeral input or output value for a transaction. It is used
628/// in fee computation for series of transactions that use an ephemeral transparent output in an
629/// intermediate step, such as when sending from a shielded pool to a [ZIP 320] "TEX" address.
630///
631/// [ZIP 320]: https://zips.z.cash/zip-0320
632#[derive(Clone, Copy, Debug, PartialEq, Eq)]
633pub enum EphemeralBalance {
634 Input(Zatoshis),
635 Output(Zatoshis),
636}
637
638impl EphemeralBalance {
639 pub fn is_input(&self) -> bool {
640 matches!(self, EphemeralBalance::Input(_))
641 }
642
643 pub fn is_output(&self) -> bool {
644 matches!(self, EphemeralBalance::Output(_))
645 }
646
647 pub fn ephemeral_input_amount(&self) -> Option<Zatoshis> {
648 match self {
649 EphemeralBalance::Input(v) => Some(*v),
650 EphemeralBalance::Output(_) => None,
651 }
652 }
653
654 pub fn ephemeral_output_amount(&self) -> Option<Zatoshis> {
655 match self {
656 EphemeralBalance::Input(_) => None,
657 EphemeralBalance::Output(v) => Some(*v),
658 }
659 }
660}
661
662/// A trait that defines a set of types used in wallet metadata retrieval. Ordinarily, this will
663/// correspond to a type that implements [`InputSource`], and a blanket implementation of this
664/// trait is provided for all types that implement [`InputSource`].
665///
666/// If more capabilities are required of the backend than are exposed in the [`InputSource`] trait,
667/// the implementer of this trait should define their own trait that descends from [`InputSource`]
668/// and adds the required capabilities there, and then implement that trait for their desired
669/// database backend.
670pub trait MetaSource {
671 type Error;
672 type AccountId;
673 type NoteRef;
674
675 /// Returns the outputs in which the given account can receive change.
676 fn change_receivers(&self, account: Self::AccountId) -> Result<ChangeReceivers, Self::Error>;
677}
678
679impl MetaSource for Infallible {
680 type Error = Infallible;
681 type AccountId = Infallible;
682 type NoteRef = Infallible;
683
684 fn change_receivers(&self, account: Infallible) -> Result<ChangeReceivers, Infallible> {
685 match account {}
686 }
687}
688
689impl<I: InputSource> MetaSource for I {
690 type Error = I::Error;
691 type AccountId = I::AccountId;
692 type NoteRef = I::NoteRef;
693
694 fn change_receivers(&self, account: I::AccountId) -> Result<ChangeReceivers, I::Error> {
695 self.get_change_receivers(account)
696 }
697}
698
699/// A trait that represents the ability to compute the suggested change and fees that must be paid
700/// by a transaction having a specified set of inputs and outputs.
701pub trait ChangeStrategy {
702 type FeeRule: FeeRule + Clone;
703 type Error: From<<Self::FeeRule as FeeRule>::Error>;
704
705 /// The type of metadata source that this change strategy requires in order to be able to
706 /// retrieve required wallet metadata.
707 type MetaSource: MetaSource;
708
709 /// Tye type of wallet metadata that this change strategy relies upon in order to compute
710 /// change.
711 type AccountMetaT;
712
713 /// Returns the fee rule that this change strategy will respect when performing
714 /// balance computations.
715 fn fee_rule(&self) -> &Self::FeeRule;
716
717 /// Uses the provided metadata source to obtain the wallet metadata required for change
718 /// creation determinations.
719 fn fetch_wallet_meta(
720 &self,
721 meta_source: &Self::MetaSource,
722 account: <Self::MetaSource as MetaSource>::AccountId,
723 target_height: TargetHeight,
724 exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
725 ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error>;
726
727 /// Computes the totals of inputs, suggested change amounts, and fees given the
728 /// provided inputs and outputs being used to construct a transaction.
729 ///
730 /// The fee computed as part of this operation should take into account the prospective
731 /// change outputs recommended by this operation. If insufficient funds are available to
732 /// supply the requested outputs and required fees, implementations should return
733 /// [`ChangeError::InsufficientFunds`], with a `required` value that exceeds the total
734 /// value of the inputs provided.
735 ///
736 /// If the inputs include notes or UTXOs that are not economic to spend in the context
737 /// of this input selection, a [`ChangeError::DustInputs`] error can be returned
738 /// indicating inputs that should be removed from the selection (all of which will
739 /// have value less than or equal to the marginal fee). Every input it names must be
740 /// one of the inputs provided, and it must name at least one. The caller should order
741 /// the inputs from most to least preferred to spend within each pool, so that the most
742 /// preferred ones are less likely to be indicated to remove.
743 ///
744 /// - `ironwood`: the Ironwood bundle view (behind the `orchard` feature). A V6
745 /// transaction carries a separate Ironwood bundle, distinct from `orchard`,
746 /// with its own action count; pass an empty view when nothing targets the
747 /// Ironwood pool.
748 /// - `ephemeral_balance`: if the transaction is to be constructed with either an
749 /// ephemeral transparent input or an ephemeral transparent output this argument
750 /// may be used to provide the value of that input or output. The value of this
751 /// argument should be `None` in the case that there are no such items.
752 /// - `wallet_meta`: Additional wallet metadata that the change strategy may use
753 /// in determining how to construct change outputs. This wallet metadata value
754 /// should be computed excluding the inputs provided in the `transparent_inputs`,
755 /// `sapling`, `orchard`, and `ironwood` arguments.
756 ///
757 /// [ZIP 320]: https://zips.z.cash/zip-0320
758 #[allow(clippy::too_many_arguments)]
759 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
760 &self,
761 params: &P,
762 target_height: TargetHeight,
763 anchor_height: BlockHeight,
764 zip318: &PoolMigrationParams,
765 transparent_inputs: &[impl transparent::InputView],
766 transparent_outputs: &[impl transparent::OutputView],
767 sapling: &impl sapling::BundleView<NoteRefT>,
768 #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
769 #[cfg(feature = "orchard")] ironwood: &impl orchard::BundleView<NoteRefT>,
770 ephemeral_balance: Option<EphemeralBalance>,
771 wallet_meta: &Self::AccountMetaT,
772 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
773}
774
775#[cfg(test)]
776pub(crate) mod tests {
777 #[cfg(feature = "orchard")]
778 use {
779 zcash_primitives::transaction::fees::zip317::MARGINAL_FEE,
780 zcash_protocol::consensus::{BlockHeight, MAIN_NETWORK},
781 };
782
783 use ::transparent::bundle::{OutPoint, TxOut};
784 use zcash_keys::keys::UnifiedSpendingKey;
785 use zcash_primitives::transaction::fees::transparent;
786 use zcash_protocol::{consensus::Network, value::Zatoshis};
787
788 use crate::data_api::ChangeReceivers;
789
790 /// The seed of the account of [`all_change_receivers`].
791 const ALL_RECEIVERS_SEED: [u8; 32] = [0; 32];
792
793 /// Returns the change receivers of an account that has a key in every pool.
794 pub(crate) fn all_change_receivers() -> ChangeReceivers {
795 ChangeReceivers::from_uivk(
796 &UnifiedSpendingKey::from_seed(
797 &Network::TestNetwork,
798 &ALL_RECEIVERS_SEED,
799 zip32::AccountId::ZERO,
800 )
801 .unwrap()
802 .to_unified_full_viewing_key()
803 .to_unified_incoming_viewing_key(),
804 )
805 }
806
807 /// The canonical crossing fee is three ZIP 317 marginal fees: the Orchard bundle's two actions
808 /// plus the single unpadded Ironwood one, which together exceed the grace allowance. Pinning it
809 /// means a change to the marginal fee or to the canonical shape surfaces here rather than
810 /// silently reclassifying transactions.
811 #[test]
812 #[cfg(feature = "orchard")]
813 fn canonical_crossing_fee_is_three_marginal_fees() {
814 let fee = super::canonical_crossing_fee(&MAIN_NETWORK, BlockHeight::from_u32(2_000_000))
815 .expect("the canonical shape is a valid input to the ZIP 317 rule");
816 assert_eq!(fee, (MARGINAL_FEE * 3u64).expect("a valid amount"));
817 assert_eq!(u64::from(fee), 15_000);
818 }
819
820 use super::sapling;
821
822 #[derive(Debug)]
823 pub(crate) struct TestTransparentInput {
824 pub outpoint: OutPoint,
825 pub coin: TxOut,
826 }
827
828 impl transparent::InputView for TestTransparentInput {
829 fn outpoint(&self) -> &OutPoint {
830 &self.outpoint
831 }
832 fn coin(&self) -> &TxOut {
833 &self.coin
834 }
835 }
836
837 pub(crate) struct TestSaplingInput {
838 pub note_id: u32,
839 pub value: Zatoshis,
840 }
841
842 impl sapling::InputView<u32> for TestSaplingInput {
843 fn note_id(&self) -> &u32 {
844 &self.note_id
845 }
846 fn value(&self) -> Zatoshis {
847 self.value
848 }
849 }
850
851 #[cfg(feature = "orchard")]
852 pub(crate) struct TestOrchardInput {
853 pub note_id: u32,
854 pub value: Zatoshis,
855 }
856
857 #[cfg(feature = "orchard")]
858 impl super::orchard::InputView<u32> for TestOrchardInput {
859 fn note_id(&self) -> &u32 {
860 &self.note_id
861 }
862 fn value(&self) -> Zatoshis {
863 self.value
864 }
865 }
866}