zcash_client_backend: Factor out common single-output change strategy logic.
This commit is contained in:
parent
926c5dcb3f
commit
d74f635d9d
|
@ -12,6 +12,7 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
pub mod common;
|
||||
pub mod fixed;
|
||||
pub mod sapling;
|
||||
pub mod standard;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue