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}