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 fixed;
|
||||||
pub mod sapling;
|
pub mod sapling;
|
||||||
pub mod standard;
|
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},
|
consensus::{self, BlockHeight},
|
||||||
memo::MemoBytes,
|
memo::MemoBytes,
|
||||||
transaction::{
|
transaction::{
|
||||||
components::amount::{BalanceError, NonNegativeAmount},
|
components::amount::BalanceError,
|
||||||
fees::{fixed::FeeRule as FixedFeeRule, transparent, FeeRule},
|
fees::{fixed::FeeRule as FixedFeeRule, transparent},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
sapling, ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy,
|
common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||||
TransactionBalance,
|
TransactionBalance,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,113 +50,18 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
sapling_outputs: &[impl sapling::OutputView],
|
sapling_outputs: &[impl sapling::OutputView],
|
||||||
dust_output_policy: &DustOutputPolicy,
|
dust_output_policy: &DustOutputPolicy,
|
||||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||||
let t_in = transparent_inputs
|
single_change_output_balance(
|
||||||
.iter()
|
params,
|
||||||
.map(|t_in| t_in.coin().value)
|
&self.fee_rule,
|
||||||
.sum::<Option<_>>()
|
target_height,
|
||||||
.ok_or(BalanceError::Overflow)?;
|
transparent_inputs,
|
||||||
let t_out = transparent_outputs
|
transparent_outputs,
|
||||||
.iter()
|
sapling_inputs,
|
||||||
.map(|t_out| t_out.value())
|
sapling_outputs,
|
||||||
.sum::<Option<_>>()
|
dust_output_policy,
|
||||||
.ok_or(BalanceError::Overflow)?;
|
self.fee_rule().fixed_fee(),
|
||||||
let sapling_in = sapling_inputs
|
self.change_memo.clone(),
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,14 @@
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
memo::MemoBytes,
|
memo::MemoBytes,
|
||||||
transaction::{
|
transaction::fees::{
|
||||||
components::amount::{BalanceError, NonNegativeAmount},
|
transparent,
|
||||||
fees::{
|
zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule},
|
||||||
transparent,
|
|
||||||
zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule},
|
|
||||||
FeeRule,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
sapling, ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy,
|
common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||||
TransactionBalance,
|
TransactionBalance,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,101 +129,18 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let overflow = || ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Overflow));
|
single_change_output_balance(
|
||||||
let underflow =
|
params,
|
||||||
|| ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Underflow));
|
&self.fee_rule,
|
||||||
|
target_height,
|
||||||
let t_in = transparent_inputs
|
transparent_inputs,
|
||||||
.iter()
|
transparent_outputs,
|
||||||
.map(|t_in| t_in.coin().value)
|
sapling_inputs,
|
||||||
.sum::<Option<_>>()
|
sapling_outputs,
|
||||||
.ok_or_else(overflow)?;
|
dust_output_policy,
|
||||||
let t_out = transparent_outputs
|
self.fee_rule.marginal_fee(),
|
||||||
.iter()
|
self.change_memo.clone(),
|
||||||
.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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::convert::TryFrom;
|
use std::convert::{Infallible, TryFrom};
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::iter::Sum;
|
use std::iter::Sum;
|
||||||
use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
|
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"))]
|
#[cfg(any(test, feature = "test-dependencies"))]
|
||||||
pub mod testing {
|
pub mod testing {
|
||||||
use proptest::prelude::prop_compose;
|
use proptest::prelude::prop_compose;
|
||||||
|
|
Loading…
Reference in New Issue