diff --git a/zcash_client_backend/src/fees.rs b/zcash_client_backend/src/fees.rs index e6d9d6b31..64a79f7cf 100644 --- a/zcash_client_backend/src/fees.rs +++ b/zcash_client_backend/src/fees.rs @@ -12,6 +12,7 @@ use zcash_primitives::{ }, }; +pub mod common; pub mod fixed; pub mod sapling; pub mod standard; diff --git a/zcash_client_backend/src/fees/common.rs b/zcash_client_backend/src/fees/common.rs new file mode 100644 index 000000000..4368bda4d --- /dev/null +++ b/zcash_client_backend/src/fees/common.rs @@ -0,0 +1,122 @@ +use zcash_primitives::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + transaction::{ + components::amount::{BalanceError, NonNegativeAmount}, + fees::{transparent, FeeRule}, + }, +}; + +use super::{sapling, ChangeError, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance}; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn single_change_output_balance< + P: consensus::Parameters, + NoteRefT: Clone, + F: FeeRule, + E, +>( + params: &P, + fee_rule: &F, + target_height: BlockHeight, + transparent_inputs: &[impl transparent::InputView], + transparent_outputs: &[impl transparent::OutputView], + sapling_inputs: &[impl sapling::InputView], + sapling_outputs: &[impl sapling::OutputView], + dust_output_policy: &DustOutputPolicy, + default_dust_threshold: NonNegativeAmount, + change_memo: Option, +) -> Result> +where + E: From + From, +{ + let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow)); + let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow)); + + let t_in = transparent_inputs + .iter() + .map(|t_in| t_in.coin().value) + .sum::>() + .ok_or_else(overflow)?; + let t_out = transparent_outputs + .iter() + .map(|t_out| t_out.value()) + .sum::>() + .ok_or_else(overflow)?; + let sapling_in = sapling_inputs + .iter() + .map(|s_in| s_in.value()) + .sum::>() + .ok_or_else(overflow)?; + let sapling_out = sapling_outputs + .iter() + .map(|s_out| s_out.value()) + .sum::>() + .ok_or_else(overflow)?; + + let fee_amount = fee_rule + .fee_required( + params, + target_height, + transparent_inputs, + transparent_outputs, + sapling_inputs.len(), + if sapling_inputs.is_empty() { + sapling_outputs.len() + 1 + } else { + std::cmp::max(sapling_outputs.len() + 1, 2) + }, + //Orchard is not yet supported in zcash_client_backend + 0, + ) + .map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?; + + let total_in = (t_in + sapling_in).ok_or_else(overflow)?; + + let total_out = [t_out, sapling_out, fee_amount] + .iter() + .sum::>() + .ok_or_else(overflow)?; + + let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds { + available: total_in, + required: total_out, + })?; + + if proposed_change == NonNegativeAmount::ZERO { + TransactionBalance::new(vec![], fee_amount).map_err(|_| overflow()) + } else { + let dust_threshold = dust_output_policy + .dust_threshold() + .unwrap_or(default_dust_threshold); + + if proposed_change < dust_threshold { + match dust_output_policy.action() { + DustAction::Reject => { + let shortfall = (dust_threshold - proposed_change).ok_or_else(underflow)?; + + Err(ChangeError::InsufficientFunds { + available: total_in, + required: (total_in + shortfall).ok_or_else(overflow)?, + }) + } + DustAction::AllowDustChange => TransactionBalance::new( + vec![ChangeValue::sapling(proposed_change, change_memo)], + fee_amount, + ) + .map_err(|_| overflow()), + DustAction::AddDustToFee => TransactionBalance::new( + vec![], + (fee_amount + proposed_change).ok_or_else(overflow)?, + ) + .map_err(|_| overflow()), + } + } else { + TransactionBalance::new( + vec![ChangeValue::sapling(proposed_change, change_memo)], + fee_amount, + ) + .map_err(|_| overflow()) + } + } +} diff --git a/zcash_client_backend/src/fees/fixed.rs b/zcash_client_backend/src/fees/fixed.rs index 39314dcf7..b17863ec2 100644 --- a/zcash_client_backend/src/fees/fixed.rs +++ b/zcash_client_backend/src/fees/fixed.rs @@ -4,13 +4,13 @@ use zcash_primitives::{ consensus::{self, BlockHeight}, memo::MemoBytes, transaction::{ - components::amount::{BalanceError, NonNegativeAmount}, - fees::{fixed::FeeRule as FixedFeeRule, transparent, FeeRule}, + components::amount::BalanceError, + fees::{fixed::FeeRule as FixedFeeRule, transparent}, }, }; use super::{ - sapling, ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, + common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance, }; @@ -50,113 +50,18 @@ impl ChangeStrategy for SingleOutputChangeStrategy { sapling_outputs: &[impl sapling::OutputView], dust_output_policy: &DustOutputPolicy, ) -> Result> { - let t_in = transparent_inputs - .iter() - .map(|t_in| t_in.coin().value) - .sum::>() - .ok_or(BalanceError::Overflow)?; - let t_out = transparent_outputs - .iter() - .map(|t_out| t_out.value()) - .sum::>() - .ok_or(BalanceError::Overflow)?; - let sapling_in = sapling_inputs - .iter() - .map(|s_in| s_in.value()) - .sum::>() - .ok_or(BalanceError::Overflow)?; - let sapling_out = sapling_outputs - .iter() - .map(|s_out| s_out.value()) - .sum::>() - .ok_or(BalanceError::Overflow)?; - - let fee_amount = self - .fee_rule - .fee_required( - params, - target_height, - transparent_inputs, - transparent_outputs, - sapling_inputs.len(), - sapling_outputs.len() + 1, - //Orchard is not yet supported in zcash_client_backend - 0, - ) - .unwrap(); // fixed::FeeRule::fee_required is infallible. - - let total_in = (t_in + sapling_in).ok_or(BalanceError::Overflow)?; - - if (!transparent_inputs.is_empty() || !sapling_inputs.is_empty()) && fee_amount > total_in { - // For the fixed-fee selection rule, the only time we consider inputs dust is when the fee - // exceeds the value of all input values. - Err(ChangeError::DustInputs { - transparent: transparent_inputs - .iter() - .map(|i| i.outpoint()) - .cloned() - .collect(), - sapling: sapling_inputs - .iter() - .map(|i| i.note_id()) - .cloned() - .collect(), - }) - } else { - let total_out = [t_out, sapling_out, fee_amount] - .iter() - .sum::>() - .ok_or(BalanceError::Overflow)?; - - let overflow = |_| ChangeError::StrategyError(BalanceError::Overflow); - - let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds { - available: total_in, - required: total_out, - })?; - if proposed_change == NonNegativeAmount::ZERO { - TransactionBalance::new(vec![], fee_amount).map_err(overflow) - } else { - let dust_threshold = dust_output_policy - .dust_threshold() - .unwrap_or_else(|| self.fee_rule.fixed_fee()); - - if dust_threshold > proposed_change { - match dust_output_policy.action() { - DustAction::Reject => { - let shortfall = (dust_threshold - proposed_change) - .ok_or(BalanceError::Underflow)?; - Err(ChangeError::InsufficientFunds { - available: total_in, - required: (total_in + shortfall).ok_or(BalanceError::Overflow)?, - }) - } - DustAction::AllowDustChange => TransactionBalance::new( - vec![ChangeValue::sapling( - proposed_change, - self.change_memo.clone(), - )], - fee_amount, - ) - .map_err(overflow), - DustAction::AddDustToFee => TransactionBalance::new( - vec![], - (fee_amount + proposed_change).ok_or(BalanceError::Overflow)?, - ) - .map_err(overflow), - } - } else { - TransactionBalance::new( - vec![ChangeValue::sapling( - proposed_change, - self.change_memo.clone(), - )], - fee_amount, - ) - .map_err(overflow) - } - } - } + single_change_output_balance( + params, + &self.fee_rule, + target_height, + transparent_inputs, + transparent_outputs, + sapling_inputs, + sapling_outputs, + dust_output_policy, + self.fee_rule().fixed_fee(), + self.change_memo.clone(), + ) } } diff --git a/zcash_client_backend/src/fees/zip317.rs b/zcash_client_backend/src/fees/zip317.rs index 0a9315759..ab88bcd26 100644 --- a/zcash_client_backend/src/fees/zip317.rs +++ b/zcash_client_backend/src/fees/zip317.rs @@ -7,18 +7,14 @@ use zcash_primitives::{ consensus::{self, BlockHeight}, memo::MemoBytes, - transaction::{ - components::amount::{BalanceError, NonNegativeAmount}, - fees::{ - transparent, - zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule}, - FeeRule, - }, + transaction::fees::{ + transparent, + zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule}, }, }; use super::{ - sapling, ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, + common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance, }; @@ -133,101 +129,18 @@ impl ChangeStrategy for SingleOutputChangeStrategy { } } - let overflow = || ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Overflow)); - let underflow = - || ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Underflow)); - - let t_in = transparent_inputs - .iter() - .map(|t_in| t_in.coin().value) - .sum::>() - .ok_or_else(overflow)?; - let t_out = transparent_outputs - .iter() - .map(|t_out| t_out.value()) - .sum::>() - .ok_or_else(overflow)?; - let sapling_in = sapling_inputs - .iter() - .map(|s_in| s_in.value()) - .sum::>() - .ok_or_else(overflow)?; - let sapling_out = sapling_outputs - .iter() - .map(|s_out| s_out.value()) - .sum::>() - .ok_or_else(overflow)?; - - let fee_amount = self - .fee_rule - .fee_required( - params, - target_height, - transparent_inputs, - transparent_outputs, - sapling_inputs.len(), - // add one for Sapling change, then account for Sapling output padding performed by - // the transaction builder - std::cmp::max(sapling_outputs.len() + 1, 2), - //Orchard is not yet supported in zcash_client_backend - 0, - ) - .map_err(ChangeError::StrategyError)?; - - let total_in = (t_in + sapling_in).ok_or_else(overflow)?; - - let total_out = [t_out, sapling_out, fee_amount] - .iter() - .sum::>() - .ok_or_else(overflow)?; - - let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds { - available: total_in, - required: total_out, - })?; - - if proposed_change == NonNegativeAmount::ZERO { - TransactionBalance::new(vec![], fee_amount).map_err(|_| overflow()) - } else { - let dust_threshold = dust_output_policy - .dust_threshold() - .unwrap_or_else(|| self.fee_rule.marginal_fee()); - - if dust_threshold > proposed_change { - match dust_output_policy.action() { - DustAction::Reject => { - let shortfall = (dust_threshold - proposed_change).ok_or_else(underflow)?; - - Err(ChangeError::InsufficientFunds { - available: total_in, - required: (total_in + shortfall).ok_or_else(overflow)?, - }) - } - DustAction::AllowDustChange => TransactionBalance::new( - vec![ChangeValue::sapling( - proposed_change, - self.change_memo.clone(), - )], - fee_amount, - ) - .map_err(|_| overflow()), - DustAction::AddDustToFee => TransactionBalance::new( - vec![], - (fee_amount + proposed_change).ok_or_else(overflow)?, - ) - .map_err(|_| overflow()), - } - } else { - TransactionBalance::new( - vec![ChangeValue::sapling( - proposed_change, - self.change_memo.clone(), - )], - fee_amount, - ) - .map_err(|_| overflow()) - } - } + single_change_output_balance( + params, + &self.fee_rule, + target_height, + transparent_inputs, + transparent_outputs, + sapling_inputs, + sapling_outputs, + dust_output_policy, + self.fee_rule.marginal_fee(), + self.change_memo.clone(), + ) } } diff --git a/zcash_primitives/src/transaction/components/amount.rs b/zcash_primitives/src/transaction/components/amount.rs index f352c463e..b005bd43d 100644 --- a/zcash_primitives/src/transaction/components/amount.rs +++ b/zcash_primitives/src/transaction/components/amount.rs @@ -1,4 +1,4 @@ -use std::convert::TryFrom; +use std::convert::{Infallible, TryFrom}; use std::error; use std::iter::Sum; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; @@ -422,6 +422,12 @@ impl std::fmt::Display for BalanceError { } } +impl From for BalanceError { + fn from(_value: Infallible) -> Self { + unreachable!() + } +} + #[cfg(any(test, feature = "test-dependencies"))] pub mod testing { use proptest::prelude::prop_compose;