zcash_client_backend/
fees.rs

1use std::{
2    fmt::{self, Debug, Display},
3    num::{NonZeroU64, NonZeroUsize},
4};
5
6use ::transparent::bundle::OutPoint;
7use zcash_primitives::transaction::fees::{
8    transparent::{self, InputSize},
9    zip317::{self as prim_zip317},
10    FeeRule,
11};
12use zcash_protocol::{
13    consensus::{self, BlockHeight},
14    memo::MemoBytes,
15    value::Zatoshis,
16    PoolType, ShieldedProtocol,
17};
18
19use crate::data_api::InputSource;
20
21pub mod common;
22#[cfg(feature = "non-standard-fees")]
23pub mod fixed;
24#[cfg(feature = "orchard")]
25pub mod orchard;
26pub mod sapling;
27pub mod standard;
28pub mod zip317;
29
30/// An enumeration of the standard fee rules supported by the wallet backend.
31#[derive(Debug, Copy, Clone, PartialEq, Eq)]
32pub enum StandardFeeRule {
33    Zip317,
34}
35
36impl FeeRule for StandardFeeRule {
37    type Error = prim_zip317::FeeError;
38
39    fn fee_required<P: consensus::Parameters>(
40        &self,
41        params: &P,
42        target_height: BlockHeight,
43        transparent_input_sizes: impl IntoIterator<Item = InputSize>,
44        transparent_output_sizes: impl IntoIterator<Item = usize>,
45        sapling_input_count: usize,
46        sapling_output_count: usize,
47        orchard_action_count: usize,
48    ) -> Result<Zatoshis, Self::Error> {
49        #[allow(deprecated)]
50        match self {
51            Self::Zip317 => prim_zip317::FeeRule::standard().fee_required(
52                params,
53                target_height,
54                transparent_input_sizes,
55                transparent_output_sizes,
56                sapling_input_count,
57                sapling_output_count,
58                orchard_action_count,
59            ),
60        }
61    }
62}
63
64/// `ChangeValue` represents either a proposed change output to a shielded pool
65/// (with an optional change memo), or if the "transparent-inputs" feature is
66/// enabled, an ephemeral output to the transparent pool.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct ChangeValue(ChangeValueInner);
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71enum ChangeValueInner {
72    Shielded {
73        protocol: ShieldedProtocol,
74        value: Zatoshis,
75        memo: Option<MemoBytes>,
76    },
77    #[cfg(feature = "transparent-inputs")]
78    EphemeralTransparent { value: Zatoshis },
79}
80
81impl ChangeValue {
82    /// Constructs a new ephemeral transparent output value.
83    #[cfg(feature = "transparent-inputs")]
84    pub fn ephemeral_transparent(value: Zatoshis) -> Self {
85        Self(ChangeValueInner::EphemeralTransparent { value })
86    }
87
88    /// Constructs a new change value that will be created as a shielded output.
89    pub fn shielded(protocol: ShieldedProtocol, value: Zatoshis, memo: Option<MemoBytes>) -> Self {
90        Self(ChangeValueInner::Shielded {
91            protocol,
92            value,
93            memo,
94        })
95    }
96
97    /// Constructs a new change value that will be created as a Sapling output.
98    pub fn sapling(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
99        Self::shielded(ShieldedProtocol::Sapling, value, memo)
100    }
101
102    /// Constructs a new change value that will be created as an Orchard output.
103    #[cfg(feature = "orchard")]
104    pub fn orchard(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
105        Self::shielded(ShieldedProtocol::Orchard, value, memo)
106    }
107
108    /// Returns the pool to which the change or ephemeral output should be sent.
109    pub fn output_pool(&self) -> PoolType {
110        match &self.0 {
111            ChangeValueInner::Shielded { protocol, .. } => PoolType::Shielded(*protocol),
112            #[cfg(feature = "transparent-inputs")]
113            ChangeValueInner::EphemeralTransparent { .. } => PoolType::Transparent,
114        }
115    }
116
117    /// Returns the value of the change or ephemeral output to be created, in zatoshis.
118    pub fn value(&self) -> Zatoshis {
119        match &self.0 {
120            ChangeValueInner::Shielded { value, .. } => *value,
121            #[cfg(feature = "transparent-inputs")]
122            ChangeValueInner::EphemeralTransparent { value } => *value,
123        }
124    }
125
126    /// Returns the memo to be associated with the output.
127    pub fn memo(&self) -> Option<&MemoBytes> {
128        match &self.0 {
129            ChangeValueInner::Shielded { memo, .. } => memo.as_ref(),
130            #[cfg(feature = "transparent-inputs")]
131            ChangeValueInner::EphemeralTransparent { .. } => None,
132        }
133    }
134
135    /// Whether this is to be an ephemeral output.
136    #[cfg_attr(
137        not(feature = "transparent-inputs"),
138        doc = "This is always false because the `transparent-inputs` feature is
139               not enabled."
140    )]
141    pub fn is_ephemeral(&self) -> bool {
142        match &self.0 {
143            ChangeValueInner::Shielded { .. } => false,
144            #[cfg(feature = "transparent-inputs")]
145            ChangeValueInner::EphemeralTransparent { .. } => true,
146        }
147    }
148}
149
150/// The amount of change and fees required to make a transaction's inputs and
151/// outputs balance under a specific fee rule, as computed by a particular
152/// [`ChangeStrategy`] that is aware of that rule.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub struct TransactionBalance {
155    proposed_change: Vec<ChangeValue>,
156    fee_required: Zatoshis,
157
158    // A cache for the sum of proposed change and fee; we compute it on construction anyway, so we
159    // cache the resulting value.
160    total: Zatoshis,
161}
162
163impl TransactionBalance {
164    /// Constructs a new balance from its constituent parts.
165    pub fn new(proposed_change: Vec<ChangeValue>, fee_required: Zatoshis) -> Result<Self, ()> {
166        let total = proposed_change
167            .iter()
168            .map(|c| c.value())
169            .chain(Some(fee_required).into_iter())
170            .sum::<Option<Zatoshis>>()
171            .ok_or(())?;
172
173        Ok(Self {
174            proposed_change,
175            fee_required,
176            total,
177        })
178    }
179
180    /// The change values proposed by the [`ChangeStrategy`] that computed this balance.
181    pub fn proposed_change(&self) -> &[ChangeValue] {
182        &self.proposed_change
183    }
184
185    /// Returns the fee computed for the transaction, assuming that the suggested
186    /// change outputs are added to the transaction.
187    pub fn fee_required(&self) -> Zatoshis {
188        self.fee_required
189    }
190
191    /// Returns the sum of the proposed change outputs and the required fee.
192    pub fn total(&self) -> Zatoshis {
193        self.total
194    }
195}
196
197/// Errors that can occur in computing suggested change and/or fees.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub enum ChangeError<E, NoteRefT> {
200    /// Insufficient inputs were provided to change selection to fund the
201    /// required outputs and fees.
202    InsufficientFunds {
203        /// The total of the inputs provided to change selection
204        available: Zatoshis,
205        /// The total amount of input value required to fund the requested outputs,
206        /// including the required fees.
207        required: Zatoshis,
208    },
209    /// Some of the inputs provided to the transaction have value less than the
210    /// marginal fee, and could not be determined to have any economic value in
211    /// the context of this input selection.
212    ///
213    /// This determination is potentially conservative in the sense that inputs
214    /// with value less than or equal to the marginal fee might be excluded, even
215    /// though in practice they would not cause the fee to increase. Inputs with
216    /// value greater than the marginal fee will never be excluded.
217    ///
218    /// The ordering of the inputs in each list is unspecified.
219    DustInputs {
220        /// The outpoints for transparent inputs that could not be determined to
221        /// have economic value in the context of this input selection.
222        transparent: Vec<OutPoint>,
223        /// The identifiers for Sapling inputs that could not be determined to
224        /// have economic value in the context of this input selection.
225        sapling: Vec<NoteRefT>,
226        /// The identifiers for Orchard inputs that could not be determined to
227        /// have economic value in the context of this input selection.
228        #[cfg(feature = "orchard")]
229        orchard: Vec<NoteRefT>,
230    },
231    /// An error occurred that was specific to the change selection strategy in use.
232    StrategyError(E),
233    /// The proposed bundle structure would violate bundle type construction rules.
234    BundleError(&'static str),
235}
236
237impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
238    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
239        match &self {
240            ChangeError::InsufficientFunds {
241                available,
242                required,
243            } => write!(
244                f,
245                "Insufficient funds: required {} zatoshis, but only {} zatoshis were available.",
246                u64::from(*required),
247                u64::from(*available)
248            ),
249            ChangeError::DustInputs {
250                transparent,
251                sapling,
252                #[cfg(feature = "orchard")]
253                orchard,
254            } => {
255                #[cfg(feature = "orchard")]
256                let orchard_len = orchard.len();
257                #[cfg(not(feature = "orchard"))]
258                let orchard_len = 0;
259
260                // we can't encode the UA to its string representation because we
261                // don't have network parameters here
262                write!(
263                    f,
264                    "Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.",
265                    transparent.len() + sapling.len() + orchard_len,
266                )
267            }
268            ChangeError::StrategyError(err) => {
269                write!(f, "{}", err)
270            }
271            ChangeError::BundleError(err) => {
272                write!(
273                    f,
274                    "The proposed transaction structure violates bundle type constraints: {}",
275                    err
276                )
277            }
278        }
279    }
280}
281
282impl<E, N> std::error::Error for ChangeError<E, N>
283where
284    E: Debug + Display + std::error::Error + 'static,
285    N: Debug + Display + 'static,
286{
287    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
288        match &self {
289            ChangeError::StrategyError(e) => Some(e),
290            _ => None,
291        }
292    }
293}
294
295/// An enumeration of actions to take when a transaction would potentially create dust
296/// outputs (outputs that are likely to be without economic value due to fee rules).
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
298pub enum DustAction {
299    /// Do not allow creation of dust outputs; instead, require that additional inputs be provided.
300    Reject,
301    /// Explicitly allow the creation of dust change amounts greater than the specified value.
302    AllowDustChange,
303    /// Allow dust amounts to be added to the transaction fee.
304    AddDustToFee,
305}
306
307/// A policy describing how a [`ChangeStrategy`] should treat potentially dust-valued change
308/// outputs (outputs that are likely to be without economic value due to fee rules).
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub struct DustOutputPolicy {
311    action: DustAction,
312    dust_threshold: Option<Zatoshis>,
313}
314
315impl DustOutputPolicy {
316    /// Constructs a new dust output policy.
317    ///
318    /// A dust policy created with `None` as the dust threshold will delegate determination
319    /// of the dust threshold to the change strategy that is evaluating the strategy; this
320    /// is recommended, but an explicit value (including zero) may be provided to explicitly
321    /// override the determination of the change strategy.
322    pub fn new(action: DustAction, dust_threshold: Option<Zatoshis>) -> Self {
323        Self {
324            action,
325            dust_threshold,
326        }
327    }
328
329    /// Returns the action to take in the event that a dust change amount would be produced.
330    pub fn action(&self) -> DustAction {
331        self.action
332    }
333    /// Returns a value that will be used to override the dust determination logic of the
334    /// change policy, if any.
335    pub fn dust_threshold(&self) -> Option<Zatoshis> {
336        self.dust_threshold
337    }
338}
339
340impl Default for DustOutputPolicy {
341    fn default() -> Self {
342        DustOutputPolicy::new(DustAction::Reject, None)
343    }
344}
345
346/// A policy that describes how change output should be split into multiple notes for the purpose
347/// of note management.
348///
349/// If an account contains at least [`Self::target_output_count`] notes having at least value
350/// [`Self::min_split_output_value`], this policy will recommend a single output; if the account
351/// contains fewer such notes, this policy will recommend that multiple outputs be produced in
352/// order to achieve the target.
353#[derive(Clone, Copy, Debug)]
354pub struct SplitPolicy {
355    target_output_count: NonZeroUsize,
356    min_split_output_value: Option<Zatoshis>,
357}
358
359impl SplitPolicy {
360    /// In the case that no other conditions provided by the user are available to fall back on,
361    /// a default value of [`MARGINAL_FEE`] * 100 will be used as the "minimum usable note value"
362    /// when retrieving wallet metadata.
363    ///
364    /// [`MARGINAL_FEE`]: zcash_primitives::transaction::fees::zip317::MARGINAL_FEE
365    pub(crate) const MIN_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(500000);
366
367    /// Constructs a new [`SplitPolicy`] that splits change to ensure the given number of spendable
368    /// outputs exists within an account, each having at least the specified minimum note value.
369    pub fn with_min_output_value(
370        target_output_count: NonZeroUsize,
371        min_split_output_value: Zatoshis,
372    ) -> Self {
373        Self {
374            target_output_count,
375            min_split_output_value: Some(min_split_output_value),
376        }
377    }
378
379    /// Constructs a [`SplitPolicy`] that prescribes a single output (no splitting).
380    pub fn single_output() -> Self {
381        Self {
382            target_output_count: NonZeroUsize::MIN,
383            min_split_output_value: None,
384        }
385    }
386
387    /// Returns the number of outputs that this policy will attempt to ensure that the wallet has
388    /// available for spending.
389    pub fn target_output_count(&self) -> NonZeroUsize {
390        self.target_output_count
391    }
392
393    /// Returns the minimum value for a note resulting from splitting of change.
394    pub fn min_split_output_value(&self) -> Option<Zatoshis> {
395        self.min_split_output_value
396    }
397
398    /// Returns the number of output notes to produce from the given total change value, given the
399    /// total value and number of existing unspent notes in the account and this policy.
400    ///
401    /// If splitting change to produce [`Self::target_output_count`] would result in notes of value
402    /// less than [`Self::min_split_output_value`], then this will suggest a smaller number of
403    /// splits so that each resulting change note has sufficient value.
404    pub fn split_count(
405        &self,
406        existing_notes: Option<usize>,
407        existing_notes_total: Option<Zatoshis>,
408        total_change: Zatoshis,
409    ) -> NonZeroUsize {
410        fn to_nonzero_u64(value: usize) -> NonZeroU64 {
411            NonZeroU64::new(u64::try_from(value).expect("usize fits into u64"))
412                .expect("NonZeroU64 input derived from NonZeroUsize")
413        }
414
415        let mut split_count = NonZeroUsize::new(
416            usize::from(self.target_output_count)
417                .saturating_sub(existing_notes.unwrap_or(usize::MAX)),
418        )
419        .unwrap_or(NonZeroUsize::MIN);
420
421        let min_split_output_value = self.min_split_output_value.or_else(|| {
422            // If no minimum split output size is set, we choose the minimum split size to be a
423            // quarter of the average value of notes in the wallet after the transaction.
424            (existing_notes_total + total_change).map(|total| {
425                *total
426                    .div_with_remainder(to_nonzero_u64(
427                        usize::from(self.target_output_count).saturating_mul(4),
428                    ))
429                    .quotient()
430            })
431        });
432
433        if let Some(min_split_output_value) = min_split_output_value {
434            loop {
435                let per_output_change =
436                    total_change.div_with_remainder(to_nonzero_u64(usize::from(split_count)));
437                if *per_output_change.quotient() >= min_split_output_value {
438                    return split_count;
439                } else if let Some(new_count) = NonZeroUsize::new(usize::from(split_count) - 1) {
440                    split_count = new_count;
441                } else {
442                    // We always create at least one change output.
443                    return NonZeroUsize::MIN;
444                }
445            }
446        } else {
447            NonZeroUsize::MIN
448        }
449    }
450}
451
452/// `EphemeralBalance` describes the ephemeral input or output value for a transaction. It is used
453/// in fee computation for series of transactions that use an ephemeral transparent output in an
454/// intermediate step, such as when sending from a shielded pool to a [ZIP 320] "TEX" address.
455///
456/// [ZIP 320]: https://zips.z.cash/zip-0320
457#[derive(Clone, Debug, PartialEq, Eq)]
458pub enum EphemeralBalance {
459    Input(Zatoshis),
460    Output(Zatoshis),
461}
462
463impl EphemeralBalance {
464    pub fn is_input(&self) -> bool {
465        matches!(self, EphemeralBalance::Input(_))
466    }
467
468    pub fn is_output(&self) -> bool {
469        matches!(self, EphemeralBalance::Output(_))
470    }
471
472    pub fn ephemeral_input_amount(&self) -> Option<Zatoshis> {
473        match self {
474            EphemeralBalance::Input(v) => Some(*v),
475            EphemeralBalance::Output(_) => None,
476        }
477    }
478
479    pub fn ephemeral_output_amount(&self) -> Option<Zatoshis> {
480        match self {
481            EphemeralBalance::Input(_) => None,
482            EphemeralBalance::Output(v) => Some(*v),
483        }
484    }
485}
486
487/// A trait that represents the ability to compute the suggested change and fees that must be paid
488/// by a transaction having a specified set of inputs and outputs.
489pub trait ChangeStrategy {
490    type FeeRule: FeeRule + Clone;
491    type Error;
492
493    /// The type of metadata source that this change strategy requires in order to be able to
494    /// retrieve required wallet metadata. If more capabilities are required of the backend than
495    /// are exposed in the [`InputSource`] trait, the implementer of this trait should define their
496    /// own trait that descends from [`InputSource`] and adds the required capabilities there, and
497    /// then implement that trait for their desired database backend.
498    type MetaSource: InputSource;
499
500    /// Tye type of wallet metadata that this change strategy relies upon in order to compute
501    /// change.
502    type AccountMetaT;
503
504    /// Returns the fee rule that this change strategy will respect when performing
505    /// balance computations.
506    fn fee_rule(&self) -> &Self::FeeRule;
507
508    /// Uses the provided metadata source to obtain the wallet metadata required for change
509    /// creation determinations.
510    fn fetch_wallet_meta(
511        &self,
512        meta_source: &Self::MetaSource,
513        account: <Self::MetaSource as InputSource>::AccountId,
514        exclude: &[<Self::MetaSource as InputSource>::NoteRef],
515    ) -> Result<Self::AccountMetaT, <Self::MetaSource as InputSource>::Error>;
516
517    /// Computes the totals of inputs, suggested change amounts, and fees given the
518    /// provided inputs and outputs being used to construct a transaction.
519    ///
520    /// The fee computed as part of this operation should take into account the prospective
521    /// change outputs recommended by this operation. If insufficient funds are available to
522    /// supply the requested outputs and required fees, implementations should return
523    /// [`ChangeError::InsufficientFunds`].
524    ///
525    /// If the inputs include notes or UTXOs that are not economic to spend in the context
526    /// of this input selection, a [`ChangeError::DustInputs`] error can be returned
527    /// indicating inputs that should be removed from the selection (all of which will
528    /// have value less than or equal to the marginal fee). The caller should order the
529    /// inputs from most to least preferred to spend within each pool, so that the most
530    /// preferred ones are less likely to be indicated to remove.
531    ///
532    /// - `ephemeral_balance`: if the transaction is to be constructed with either an
533    ///   ephemeral transparent input or an ephemeral transparent output this argument
534    ///   may be used to provide the value of that input or output. The value of this
535    ///   argument should be `None` in the case that there are no such items.
536    /// - `wallet_meta`: Additional wallet metadata that the change strategy may use
537    ///   in determining how to construct change outputs. This wallet metadata value
538    ///   should be computed excluding the inputs provided in the `transparent_inputs`,
539    ///   `sapling`, and `orchard` arguments.
540    ///
541    /// [ZIP 320]: https://zips.z.cash/zip-0320
542    #[allow(clippy::too_many_arguments)]
543    fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
544        &self,
545        params: &P,
546        target_height: BlockHeight,
547        transparent_inputs: &[impl transparent::InputView],
548        transparent_outputs: &[impl transparent::OutputView],
549        sapling: &impl sapling::BundleView<NoteRefT>,
550        #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
551        ephemeral_balance: Option<&EphemeralBalance>,
552        wallet_meta: &Self::AccountMetaT,
553    ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
554}
555
556#[cfg(test)]
557pub(crate) mod tests {
558    use ::transparent::bundle::{OutPoint, TxOut};
559    use zcash_primitives::transaction::fees::transparent;
560    use zcash_protocol::value::Zatoshis;
561
562    use super::sapling;
563
564    #[derive(Debug)]
565    pub(crate) struct TestTransparentInput {
566        pub outpoint: OutPoint,
567        pub coin: TxOut,
568    }
569
570    impl transparent::InputView for TestTransparentInput {
571        fn outpoint(&self) -> &OutPoint {
572            &self.outpoint
573        }
574        fn coin(&self) -> &TxOut {
575            &self.coin
576        }
577    }
578
579    pub(crate) struct TestSaplingInput {
580        pub note_id: u32,
581        pub value: Zatoshis,
582    }
583
584    impl sapling::InputView<u32> for TestSaplingInput {
585        fn note_id(&self) -> &u32 {
586            &self.note_id
587        }
588        fn value(&self) -> Zatoshis {
589            self.value
590        }
591    }
592}