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