Merge pull request #1066 from nuttycom/wallet/common_fee_logic

Factor out common logic from zip317 and fixed-fee single output change strategies.
This commit is contained in:
Kris Nuttycombe 2023-12-20 20:50:53 -07:00 committed by GitHub
commit 1ef6bf6656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 214 deletions

View File

@ -12,6 +12,7 @@ use zcash_primitives::{
},
};
pub mod common;
pub mod fixed;
pub mod sapling;
pub mod standard;

View File

@ -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<NoteRefT>],
sapling_outputs: &[impl sapling::OutputView],
dust_output_policy: &DustOutputPolicy,
default_dust_threshold: NonNegativeAmount,
change_memo: Option<MemoBytes>,
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
where
E: From<F::Error> + From<BalanceError>,
{
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::<Option<_>>()
.ok_or_else(overflow)?;
let t_out = transparent_outputs
.iter()
.map(|t_out| t_out.value())
.sum::<Option<_>>()
.ok_or_else(overflow)?;
let sapling_in = sapling_inputs
.iter()
.map(|s_in| s_in.value())
.sum::<Option<_>>()
.ok_or_else(overflow)?;
let sapling_out = sapling_outputs
.iter()
.map(|s_out| s_out.value())
.sum::<Option<_>>()
.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::<Option<NonNegativeAmount>>()
.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())
}
}
}

View File

@ -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<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
let t_in = transparent_inputs
.iter()
.map(|t_in| t_in.coin().value)
.sum::<Option<_>>()
.ok_or(BalanceError::Overflow)?;
let t_out = transparent_outputs
.iter()
.map(|t_out| t_out.value())
.sum::<Option<_>>()
.ok_or(BalanceError::Overflow)?;
let sapling_in = sapling_inputs
.iter()
.map(|s_in| s_in.value())
.sum::<Option<_>>()
.ok_or(BalanceError::Overflow)?;
let sapling_out = sapling_outputs
.iter()
.map(|s_out| s_out.value())
.sum::<Option<_>>()
.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::<Option<NonNegativeAmount>>()
.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(),
)
}
}

View File

@ -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::<Option<_>>()
.ok_or_else(overflow)?;
let t_out = transparent_outputs
.iter()
.map(|t_out| t_out.value())
.sum::<Option<_>>()
.ok_or_else(overflow)?;
let sapling_in = sapling_inputs
.iter()
.map(|s_in| s_in.value())
.sum::<Option<_>>()
.ok_or_else(overflow)?;
let sapling_out = sapling_outputs
.iter()
.map(|s_out| s_out.value())
.sum::<Option<_>>()
.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::<Option<NonNegativeAmount>>()
.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(),
)
}
}

View File

@ -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<Infallible> for BalanceError {
fn from(_value: Infallible) -> Self {
unreachable!()
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::prop_compose;