`zcash_client_backend::{fixed,standard,zip317}::SingleOutputChangeStrategy`
now implement a different strategy for choosing whether there will be any change, and its value. The aims are: * Ensure that it is possible to create fully transparent transactions with no change (this will be needed for ZIP 320). The `InsufficientFunds` error in this case should have a `required` field that reflects the additional amount needed, according to the fee calculated without an extra change output. * Avoid leaking information about note amounts in some cases: an adversary that knew the number of external recipients and the sum of their outputs was able to learn the sum of the inputs if no change output was present. * Defend against losing money by using `DustAction::AddDustToFee` with a too-high dust threshold. * Ensure that if a "change memo" is requested, there will always be a shielded change output in which to put it. Previously, this would not be the case when using `DustAction::AddDustToFee`. Co-authored-by: Jack Grigg <jack@electriccoin.co> Co-authored-by: Kris Nuttycombe <kris@nutty.land> Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
This commit is contained in:
parent
06c089535a
commit
21d573122c
|
@ -20,6 +20,13 @@ and this library adheres to Rust's notion of
|
|||
|
||||
### Changed
|
||||
- MSRV is now 1.70.0.
|
||||
- `zcash_client_backend::{fixed,standard,zip317}::SingleOutputChangeStrategy`
|
||||
now implement a different strategy for choosing whether there will be any
|
||||
change, and its value. This can avoid leaking information about note amounts
|
||||
in some cases. It also ensures that there will be a change output whenever a
|
||||
`change_memo` is given, and defends against losing money by using
|
||||
`DustAction::AddDustToFee` with a too-high dust threshold.
|
||||
See [#1430](https://github.com/zcash/librustzcash/pull/1430) for details.
|
||||
- `zcash_client_backend::zip321` has been extracted to, and is now a reexport
|
||||
of the root module of the `zip321` crate. Several of the APIs of this module
|
||||
have changed as a consequence of this extraction; please see the `zip321`
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use core::cmp::max;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
transaction::{
|
||||
components::amount::{BalanceError, NonNegativeAmount},
|
||||
fees::{transparent, FeeRule},
|
||||
fees::{transparent, zip317::MINIMUM_FEE, FeeRule},
|
||||
},
|
||||
};
|
||||
use zcash_protocol::ShieldedProtocol;
|
||||
|
@ -25,6 +27,22 @@ pub(crate) struct NetFlows {
|
|||
orchard_out: NonNegativeAmount,
|
||||
}
|
||||
|
||||
impl NetFlows {
|
||||
fn total_in(&self) -> Result<NonNegativeAmount, BalanceError> {
|
||||
(self.t_in + self.sapling_in + self.orchard_in).ok_or(BalanceError::Overflow)
|
||||
}
|
||||
fn total_out(&self) -> Result<NonNegativeAmount, BalanceError> {
|
||||
(self.t_out + self.sapling_out + self.orchard_out).ok_or(BalanceError::Overflow)
|
||||
}
|
||||
/// Returns true iff the flows excluding change are fully transparent.
|
||||
fn is_transparent(&self) -> bool {
|
||||
!(self.sapling_in.is_positive()
|
||||
|| self.sapling_out.is_positive()
|
||||
|| self.orchard_in.is_positive()
|
||||
|| self.orchard_out.is_positive())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn calculate_net_flows<NoteRefT: Clone, F: FeeRule, E>(
|
||||
transparent_inputs: &[impl transparent::InputView],
|
||||
|
@ -137,7 +155,7 @@ pub(crate) fn single_change_output_balance<
|
|||
dust_output_policy: &DustOutputPolicy,
|
||||
default_dust_threshold: NonNegativeAmount,
|
||||
change_memo: Option<MemoBytes>,
|
||||
_fallback_change_pool: ShieldedProtocol,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
|
||||
where
|
||||
E: From<F::Error> + From<BalanceError>,
|
||||
|
@ -152,14 +170,26 @@ where
|
|||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
)?;
|
||||
let (change_pool, sapling_change, _orchard_change) =
|
||||
single_change_output_policy(&net_flows, _fallback_change_pool);
|
||||
let total_in = net_flows
|
||||
.total_in()
|
||||
.map_err(|e| ChangeError::StrategyError(E::from(e)))?;
|
||||
let total_out = net_flows
|
||||
.total_out()
|
||||
.map_err(|e| ChangeError::StrategyError(E::from(e)))?;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let (change_pool, sapling_change, orchard_change) =
|
||||
single_change_output_policy(&net_flows, fallback_change_pool);
|
||||
|
||||
let sapling_input_count = sapling
|
||||
.bundle_type()
|
||||
.num_spends(sapling.inputs().len())
|
||||
.map_err(ChangeError::BundleError)?;
|
||||
let sapling_output_count = sapling
|
||||
.bundle_type()
|
||||
.num_outputs(sapling.inputs().len(), sapling.outputs().len())
|
||||
.map_err(ChangeError::BundleError)?;
|
||||
let sapling_output_count_with_change = sapling
|
||||
.bundle_type()
|
||||
.num_outputs(
|
||||
sapling.inputs().len(),
|
||||
|
@ -169,16 +199,48 @@ where
|
|||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_action_count = orchard
|
||||
.bundle_type()
|
||||
.num_actions(orchard.inputs().len(), orchard.outputs().len())
|
||||
.map_err(ChangeError::BundleError)?;
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_action_count_with_change = orchard
|
||||
.bundle_type()
|
||||
.num_actions(
|
||||
orchard.inputs().len(),
|
||||
orchard.outputs().len() + _orchard_change,
|
||||
orchard.outputs().len() + orchard_change,
|
||||
)
|
||||
.map_err(ChangeError::BundleError)?;
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_action_count = 0;
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_action_count_with_change = 0;
|
||||
|
||||
let fee_amount = fee_rule
|
||||
// Once we calculate the balance with and without change, there are five cases:
|
||||
//
|
||||
// 1. Insufficient funds even without change.
|
||||
// 2. The fee amount without change exactly cancels out the net flow balance.
|
||||
// 3. The fee amount without change is smaller than the change.
|
||||
// 3a. Insufficient funds once the change output is added.
|
||||
// 3b. The fee amount with change exactly cancels out the net flow balance.
|
||||
// 3c. The fee amount with change leaves a non-zero change value.
|
||||
//
|
||||
// Case 2 happens for the second transaction of a ZIP 320 pair. In that case
|
||||
// the transaction will be fully transparent, and there must be no change.
|
||||
//
|
||||
// If cases 2 or 3b happen for a transaction with any shielded flows, we
|
||||
// want there to be a zero-value shielded change output anyway (i.e. treat
|
||||
// case 2 as case 3, and case 3b as case 3c), because:
|
||||
// * being able to distinguish these cases potentially leaks too much
|
||||
// information (an adversary that knows the number of external recipients
|
||||
// and the sum of their outputs learns the sum of the inputs if no change
|
||||
// output is present); and
|
||||
// * we will then always have an shielded output in which to put change_memo,
|
||||
// if one is given.
|
||||
//
|
||||
// Note that using the `DustAction::AddDustToFee` policy inherently leaks
|
||||
// more information.
|
||||
|
||||
let fee_without_change = fee_rule
|
||||
.fee_required(
|
||||
params,
|
||||
target_height,
|
||||
|
@ -190,58 +252,116 @@ where
|
|||
)
|
||||
.map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
|
||||
|
||||
let total_in =
|
||||
(net_flows.t_in + net_flows.sapling_in + net_flows.orchard_in).ok_or_else(overflow)?;
|
||||
let total_out = (net_flows.t_out + net_flows.sapling_out + net_flows.orchard_out + fee_amount)
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds {
|
||||
available: total_in,
|
||||
required: total_out,
|
||||
})?;
|
||||
|
||||
if proposed_change.is_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::shielded(
|
||||
change_pool,
|
||||
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::shielded(
|
||||
change_pool,
|
||||
proposed_change,
|
||||
change_memo,
|
||||
)],
|
||||
fee_amount,
|
||||
let fee_with_change = max(
|
||||
fee_without_change,
|
||||
fee_rule
|
||||
.fee_required(
|
||||
params,
|
||||
target_height,
|
||||
transparent_inputs.iter().map(|i| i.serialized_size()),
|
||||
transparent_outputs.iter().map(|i| i.serialized_size()),
|
||||
sapling_input_count,
|
||||
sapling_output_count_with_change,
|
||||
orchard_action_count_with_change,
|
||||
)
|
||||
.map_err(|_| overflow())
|
||||
.map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?,
|
||||
);
|
||||
|
||||
// We don't create a fully-transparent transaction if a change memo is requested.
|
||||
let transparent = net_flows.is_transparent() && change_memo.is_none();
|
||||
|
||||
let total_out_plus_fee_without_change =
|
||||
(total_out + fee_without_change).ok_or_else(overflow)?;
|
||||
let total_out_plus_fee_with_change = (total_out + fee_with_change).ok_or_else(overflow)?;
|
||||
|
||||
let (change, fee) = {
|
||||
if transparent && total_in < total_out_plus_fee_without_change {
|
||||
// Case 1 for a tx with all transparent flows.
|
||||
return Err(ChangeError::InsufficientFunds {
|
||||
available: total_in,
|
||||
required: total_out_plus_fee_without_change,
|
||||
});
|
||||
} else if transparent && total_in == total_out_plus_fee_without_change {
|
||||
// Case 2 for a tx with all transparent flows.
|
||||
(vec![], fee_without_change)
|
||||
} else if total_in < total_out_plus_fee_with_change {
|
||||
// Case 3a, or case 1 or 2 with non-transparent flows.
|
||||
return Err(ChangeError::InsufficientFunds {
|
||||
available: total_in,
|
||||
required: total_out_plus_fee_with_change,
|
||||
});
|
||||
} else {
|
||||
// Case 3b or 3c.
|
||||
let proposed_change =
|
||||
(total_in - total_out_plus_fee_with_change).expect("checked above");
|
||||
let simple_case = |memo| {
|
||||
(
|
||||
vec![ChangeValue::shielded(change_pool, proposed_change, memo)],
|
||||
fee_with_change,
|
||||
)
|
||||
};
|
||||
|
||||
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 => {
|
||||
// Always allow zero-valued change even for the `Reject` policy:
|
||||
// * it should be allowed in order to record change memos and to improve
|
||||
// indistinguishability;
|
||||
// * this case occurs in practice when sending all funds from an account;
|
||||
// * zero-valued notes do not require witness tracking;
|
||||
// * the effect on trial decryption overhead is small.
|
||||
if proposed_change.is_zero() {
|
||||
simple_case(change_memo)
|
||||
} else {
|
||||
let shortfall =
|
||||
(dust_threshold - proposed_change).ok_or_else(underflow)?;
|
||||
|
||||
return Err(ChangeError::InsufficientFunds {
|
||||
available: total_in,
|
||||
required: (total_in + shortfall).ok_or_else(overflow)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
DustAction::AllowDustChange => simple_case(change_memo),
|
||||
DustAction::AddDustToFee => {
|
||||
// Zero-valued change is also always allowed for this policy, but when
|
||||
// no change memo is given, we might omit the change output instead.
|
||||
|
||||
let fee_with_dust = (total_in - total_out)
|
||||
.expect("we already checked for sufficient funds");
|
||||
// We can add a change output if necessary.
|
||||
assert!(fee_with_change <= fee_with_dust);
|
||||
|
||||
let reasonable_fee =
|
||||
(fee_with_change + (MINIMUM_FEE * 10).unwrap()).ok_or_else(overflow)?;
|
||||
|
||||
if fee_with_dust > reasonable_fee {
|
||||
// Defend against losing money by using AddDustToFee with a too-high
|
||||
// dust threshold.
|
||||
simple_case(change_memo)
|
||||
} else if change_memo.is_some() {
|
||||
(
|
||||
vec![ChangeValue::shielded(
|
||||
change_pool,
|
||||
NonNegativeAmount::ZERO,
|
||||
change_memo,
|
||||
)],
|
||||
fee_with_dust,
|
||||
)
|
||||
} else {
|
||||
(vec![], fee_with_dust)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
simple_case(change_memo)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TransactionBalance::new(change, fee).map_err(|_| overflow())
|
||||
}
|
||||
|
|
|
@ -212,7 +212,6 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::convert::Infallible;
|
||||
|
||||
use zcash_primitives::{
|
||||
|
@ -229,7 +228,7 @@ mod tests {
|
|||
data_api::wallet::input_selection::SaplingPayment,
|
||||
fees::{
|
||||
tests::{TestSaplingInput, TestTransparentInput},
|
||||
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
|
||||
ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy,
|
||||
},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
@ -323,7 +322,19 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_transparent_payments() {
|
||||
fn change_with_transparent_payments_implicitly_allowing_zero_change() {
|
||||
change_with_transparent_payments(&DustOutputPolicy::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_transparent_payments_explicitly_allowing_zero_change() {
|
||||
change_with_transparent_payments(&DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::ZERO),
|
||||
))
|
||||
}
|
||||
|
||||
fn change_with_transparent_payments(dust_output_policy: &DustOutputPolicy) {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
|
@ -351,25 +362,176 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
dust_output_policy,
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Ok(balance) if balance.proposed_change().is_empty()
|
||||
Ok(balance) if
|
||||
balance.proposed_change() == [ChangeValue::sapling(NonNegativeAmount::ZERO, None)]
|
||||
&& balance.fee_required() == NonNegativeAmount::const_from_u64(15000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_allowable_dust() {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn change_fully_transparent_no_change() {
|
||||
use crate::fees::sapling as sapling_fees;
|
||||
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
|
||||
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
// Spend a single transparent UTXO that is exactly sufficient to pay the fee.
|
||||
let result = change_strategy.compute_balance::<_, Infallible>(
|
||||
&Network::TestNetwork,
|
||||
Network::TestNetwork
|
||||
.activation_height(NetworkUpgrade::Nu5)
|
||||
.unwrap(),
|
||||
&[TestTransparentInput {
|
||||
outpoint: OutPoint::fake(),
|
||||
coin: TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(50000),
|
||||
script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(),
|
||||
},
|
||||
}],
|
||||
&[TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(40000),
|
||||
script_pubkey: Script(vec![]),
|
||||
}],
|
||||
&sapling_fees::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Ok(balance) if
|
||||
balance.proposed_change().is_empty() &&
|
||||
balance.fee_required() == NonNegativeAmount::const_from_u64(10000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn change_transparent_flows_with_shielded_change() {
|
||||
use crate::fees::sapling as sapling_fees;
|
||||
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
|
||||
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// Spend a single transparent UTXO that is sufficient to pay the fee.
|
||||
let result = change_strategy.compute_balance::<_, Infallible>(
|
||||
&Network::TestNetwork,
|
||||
Network::TestNetwork
|
||||
.activation_height(NetworkUpgrade::Nu5)
|
||||
.unwrap(),
|
||||
&[TestTransparentInput {
|
||||
outpoint: OutPoint::fake(),
|
||||
coin: TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(63000),
|
||||
script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(),
|
||||
},
|
||||
}],
|
||||
&[TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(40000),
|
||||
script_pubkey: Script(vec![]),
|
||||
}],
|
||||
&sapling_fees::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Ok(balance) if
|
||||
balance.proposed_change() == [ChangeValue::sapling(NonNegativeAmount::const_from_u64(8000), None)] &&
|
||||
balance.fee_required() == NonNegativeAmount::const_from_u64(15000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn change_transparent_flows_with_shielded_dust_change() {
|
||||
use crate::fees::sapling as sapling_fees;
|
||||
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
|
||||
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// Spend a single transparent UTXO that is sufficient to pay the fee.
|
||||
// The change will go to the fallback shielded change pool even though all inputs
|
||||
// and payments are transparent, and even though the change amount (1000) would
|
||||
// normally be considered dust, because we set the dust policy to allow that.
|
||||
let result = change_strategy.compute_balance::<_, Infallible>(
|
||||
&Network::TestNetwork,
|
||||
Network::TestNetwork
|
||||
.activation_height(NetworkUpgrade::Nu5)
|
||||
.unwrap(),
|
||||
&[TestTransparentInput {
|
||||
outpoint: OutPoint::fake(),
|
||||
coin: TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(56000),
|
||||
script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(),
|
||||
},
|
||||
}],
|
||||
&[TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(40000),
|
||||
script_pubkey: Script(vec![]),
|
||||
}],
|
||||
&sapling_fees::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::const_from_u64(1000)),
|
||||
),
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Ok(balance) if
|
||||
balance.proposed_change() == [ChangeValue::sapling(NonNegativeAmount::const_from_u64(1000), None)] &&
|
||||
balance.fee_required() == NonNegativeAmount::const_from_u64(15000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_allowable_dust_implicitly_allowing_zero_change() {
|
||||
change_with_allowable_dust(&DustOutputPolicy::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_allowable_dust_explicitly_allowing_zero_change() {
|
||||
change_with_allowable_dust(&DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::ZERO),
|
||||
))
|
||||
}
|
||||
|
||||
fn change_with_allowable_dust(dust_output_policy: &DustOutputPolicy) {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// Spend two Sapling notes, one of them dust. There is sufficient to
|
||||
// pay the fee: if only one note is spent then we are 1000 short, but
|
||||
// if both notes are spent then the fee stays at 10000 (even with a
|
||||
// zero-valued change output), so we have just enough.
|
||||
let result = change_strategy.compute_balance(
|
||||
&Network::TestNetwork,
|
||||
Network::TestNetwork
|
||||
|
@ -395,13 +557,14 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
dust_output_policy,
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Ok(balance) if balance.proposed_change().is_empty()
|
||||
&& balance.fee_required() == NonNegativeAmount::const_from_u64(10000)
|
||||
Ok(balance) if
|
||||
balance.proposed_change() == [ChangeValue::sapling(NonNegativeAmount::ZERO, None)] &&
|
||||
balance.fee_required() == NonNegativeAmount::const_from_u64(10000)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -413,7 +576,10 @@ mod tests {
|
|||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
// Attempt to spend three Sapling notes, one of them dust. Taking into account
|
||||
// Sapling output padding, the dust note would be free to add to the transaction
|
||||
// if there were only two notes (as in the `change_with_allowable_dust` test), but
|
||||
// it is not free when there are three notes.
|
||||
let result = change_strategy.compute_balance(
|
||||
&Network::TestNetwork,
|
||||
Network::TestNetwork
|
||||
|
|
|
@ -304,9 +304,14 @@ pub(crate) fn send_single_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use zcash_client_backend::proposal::{Proposal, StepOutput, StepOutputIndex};
|
||||
use zcash_primitives::legacy::keys::IncomingViewingKey;
|
||||
use zcash_client_backend::{
|
||||
fees::ChangeValue,
|
||||
proposal::{Proposal, StepOutput, StepOutputIndex},
|
||||
};
|
||||
use zcash_primitives::{legacy::keys::IncomingViewingKey, transaction::TxId};
|
||||
|
||||
let mut st = TestBuilder::new()
|
||||
.with_block_cache()
|
||||
|
@ -317,7 +322,7 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
let dfvk = T::test_account_fvk(&st);
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = NonNegativeAmount::const_from_u64(65000);
|
||||
let value = NonNegativeAmount::const_from_u64(100000);
|
||||
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
st.scan_cached_blocks(h, 1);
|
||||
|
||||
|
@ -362,7 +367,14 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
let min_target_height = proposal0.min_target_height();
|
||||
let step0 = &proposal0.steps().head;
|
||||
|
||||
assert!(step0.balance().proposed_change().is_empty());
|
||||
assert_eq!(
|
||||
step0.balance().proposed_change(),
|
||||
[ChangeValue::shielded(
|
||||
T::SHIELDED_PROTOCOL,
|
||||
NonNegativeAmount::const_from_u64(35000),
|
||||
None
|
||||
)]
|
||||
);
|
||||
assert_eq!(
|
||||
step0.balance().fee_required(),
|
||||
NonNegativeAmount::const_from_u64(15000)
|
||||
|
@ -425,10 +437,10 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let confirmed_sent = txids
|
||||
// Check that there are sent outputs with the correct values for each transaction.
|
||||
let confirmed_sent: Vec<BTreeSet<(&TxId, u32)>> = txids
|
||||
.iter()
|
||||
.map(|sent_txid| {
|
||||
// check that there's a sent output with the correct value corresponding to
|
||||
stmt_sent
|
||||
.query(rusqlite::params![sent_txid.as_ref()])
|
||||
.unwrap()
|
||||
|
@ -436,18 +448,23 @@ pub(crate) fn send_multi_step_proposed_transfer<T: ShieldedPoolTester>() {
|
|||
let value: u32 = row.get(0)?;
|
||||
Ok((sent_txid, value))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.collect::<Result<BTreeSet<_>, _>>()
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
confirmed_sent.get(0).and_then(|v| v.get(0)),
|
||||
Some(&(&txids[0], 50000))
|
||||
confirmed_sent.get(0),
|
||||
Some(
|
||||
&[(&txids[0], 35000), (&txids[0], 50000)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
confirmed_sent.get(1).and_then(|v| v.get(0)),
|
||||
Some(&(&txids[1], 40000))
|
||||
confirmed_sent.get(1),
|
||||
Some(&[(&txids[1], 40000)].iter().cloned().collect()),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue